google_guest_agent/addresses.go (361 lines of code) (raw):
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"errors"
"fmt"
"net"
"reflect"
"runtime"
"slices"
"strings"
"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg"
network "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/network/manager"
"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/run"
"github.com/GoogleCloudPlatform/guest-logging-go/logger"
)
var (
addressKey = regKeyBase + `\ForwardedIps`
oldWSFCAddresses string
oldWSFCEnable bool
)
type addressMgr struct{}
func (a *addressMgr) parseWSFCAddresses(config *cfg.Sections) string {
if config.WSFC != nil && config.WSFC.Addresses != "" {
return config.WSFC.Addresses
}
if newMetadata.Instance.Attributes.WSFCAddresses != "" {
return newMetadata.Instance.Attributes.WSFCAddresses
}
if newMetadata.Project.Attributes.WSFCAddresses != "" {
return newMetadata.Project.Attributes.WSFCAddresses
}
return ""
}
func (a *addressMgr) parseWSFCEnable(config *cfg.Sections) bool {
if config.WSFC != nil {
return config.WSFC.Enable
}
if newMetadata.Instance.Attributes.EnableWSFC != nil {
return *newMetadata.Instance.Attributes.EnableWSFC
}
if newMetadata.Project.Attributes.EnableWSFC != nil {
return *newMetadata.Project.Attributes.EnableWSFC
}
return false
}
func getForwardsFromRegistry(mac string) ([]string, error) {
regFwdIPs, err := readRegMultiString(addressKey, mac)
if err == errRegNotExist {
// The old agent stored MAC addresses without the ':',
// check for those and clean them up.
oldName := strings.Replace(mac, ":", "", -1)
regFwdIPs, err = readRegMultiString(addressKey, oldName)
if err == nil {
if err = deleteRegKey(addressKey, oldName); err != nil {
logger.Warningf("Failed to delete key: %q, name: %q from registry", addressKey, oldName)
}
}
} else if err != nil {
return nil, err
}
return regFwdIPs, nil
}
func compareRoutes(configuredRoutes, desiredRoutes []string) (toAdd, toRm []string) {
for _, desiredRoute := range desiredRoutes {
if !slices.Contains(configuredRoutes, desiredRoute) {
toAdd = append(toAdd, desiredRoute)
}
}
for _, configuredRoute := range configuredRoutes {
if !slices.Contains(desiredRoutes, configuredRoute) {
toRm = append(toRm, configuredRoute)
}
}
return toAdd, toRm
}
var badMAC []string
// https://www.ietf.org/rfc/rfc1354.txt
// Only fields that we currently care about.
type ipForwardEntry struct {
ipForwardDest net.IP
ipForwardMask net.IPMask
ipForwardNextHop net.IP
ipForwardIfIndex int32
ipForwardMetric1 int32
}
// TODO: getLocalRoutes and getIPForwardEntries should be merged.
func getLocalRoutes(ctx context.Context, config *cfg.Sections, ifname string) ([]string, error) {
if runtime.GOOS == "windows" {
return nil, errors.New("getLocalRoutes unimplemented on Windows")
}
protoID := config.IPForwarding.EthernetProtoID
args := fmt.Sprintf("route list table local type local scope host dev %s proto %s", ifname, protoID)
out := run.WithOutput(ctx, "ip", strings.Split(args, " ")...)
if out.ExitCode != 0 {
return nil, error(out)
}
var res []string
for _, line := range strings.Split(out.StdOut, "\n") {
line = strings.TrimPrefix(line, "local ")
line = strings.TrimSpace(line)
if line != "" {
res = append(res, line)
}
}
// and again for IPv6 routes, without 'scope host' which is IPv4 only
args = fmt.Sprintf("-6 route list table local type local dev %s proto %s", ifname, protoID)
out = run.WithOutput(ctx, "ip", strings.Split(args, " ")...)
if out.ExitCode != 0 {
return nil, error(out)
}
for _, line := range strings.Split(out.StdOut, "\n") {
line = strings.TrimPrefix(line, "local ")
line = strings.Split(line, " ")[0]
line = strings.TrimSpace(line)
if line != "" {
res = append(res, line)
}
}
return res, nil
}
// TODO: addLocalRoute and addRoute should be merged with the addition of ipForwardType to ipForwardEntry.
func addLocalRoute(ctx context.Context, config *cfg.Sections, ip, ifname string) error {
if runtime.GOOS == "windows" {
return errors.New("addLocalRoute unimplemented on Windows")
}
// TODO: Subnet size should be parsed from alias IP entries.
if !strings.Contains(ip, "/") {
ip = ip + "/32"
}
protoID := config.IPForwarding.EthernetProtoID
args := fmt.Sprintf("route add to local %s scope host dev %s proto %s", ip, ifname, protoID)
return run.Quiet(ctx, "ip", strings.Split(args, " ")...)
}
// TODO: removeLocalRoute should be changed to removeIPForwardEntry and match getIPForwardEntries.
func removeLocalRoute(ctx context.Context, config *cfg.Sections, ip, ifname string) error {
if runtime.GOOS == "windows" {
return errors.New("removeLocalRoute unimplemented on Windows")
}
// TODO: Subnet size should be parsed from alias IP entries.
if !strings.Contains(ip, "/") {
ip = ip + "/32"
}
protoID := config.IPForwarding.EthernetProtoID
args := fmt.Sprintf("route delete to local %s scope host dev %s proto %s", ip, ifname, protoID)
return run.Quiet(ctx, "ip", strings.Split(args, " ")...)
}
// Filter out forwarded ips based on WSFC (Windows Failover Cluster Settings).
// If only EnableWSFC is set, all ips in the ForwardedIps and TargetInstanceIps will be ignored.
// If WSFCAddresses is set (with or without EnableWSFC), only ips in the list will be filtered out.
// TODO return a filtered list rather than modifying the metadata object. liamh@15-11-19
func (a *addressMgr) applyWSFCFilter(config *cfg.Sections) {
wsfcAddresses := a.parseWSFCAddresses(config)
var wsfcAddrs []string
for _, wsfcAddr := range strings.Split(wsfcAddresses, ",") {
if wsfcAddr == "" {
continue
}
if net.ParseIP(wsfcAddr) == nil {
logger.Errorf("Address for WSFC is not in valid form %s", wsfcAddr)
continue
}
wsfcAddrs = append(wsfcAddrs, wsfcAddr)
}
if len(wsfcAddrs) != 0 {
interfaces := newMetadata.Instance.NetworkInterfaces
for idx := range interfaces {
var filteredForwardedIps []string
for _, ip := range interfaces[idx].ForwardedIps {
if !slices.Contains(wsfcAddrs, ip) {
filteredForwardedIps = append(filteredForwardedIps, ip)
}
}
interfaces[idx].ForwardedIps = filteredForwardedIps
var filteredTargetInstanceIps []string
for _, ip := range interfaces[idx].TargetInstanceIps {
if !slices.Contains(wsfcAddrs, ip) {
filteredTargetInstanceIps = append(filteredTargetInstanceIps, ip)
}
}
interfaces[idx].TargetInstanceIps = filteredTargetInstanceIps
}
} else {
wsfcEnable := a.parseWSFCEnable(config)
if wsfcEnable {
for idx := range newMetadata.Instance.NetworkInterfaces {
newMetadata.Instance.NetworkInterfaces[idx].ForwardedIps = nil
newMetadata.Instance.NetworkInterfaces[idx].TargetInstanceIps = nil
}
}
}
}
func (a *addressMgr) Diff(ctx context.Context) (bool, error) {
// Return true if this is the first call (when the first mds descriptor is available).
if oldMetadata == nil {
return true, nil
}
config := cfg.Get()
wsfcAddresses := a.parseWSFCAddresses(config)
wsfcEnable := a.parseWSFCEnable(config)
diff := !reflect.DeepEqual(newMetadata.Instance.NetworkInterfaces, oldMetadata.Instance.NetworkInterfaces) ||
!reflect.DeepEqual(newMetadata.Instance.VlanNetworkInterfaces, oldMetadata.Instance.VlanNetworkInterfaces) ||
wsfcEnable != oldWSFCEnable || wsfcAddresses != oldWSFCAddresses
oldWSFCAddresses = wsfcAddresses
oldWSFCEnable = wsfcEnable
return diff, nil
}
func (a *addressMgr) Timeout(ctx context.Context) (bool, error) {
return false, nil
}
func (a *addressMgr) Disabled(ctx context.Context) (bool, error) {
config := cfg.Get()
// Local configuration takes precedence over metadata's configuration.
if config.AddressManager != nil {
return config.AddressManager.Disable, nil
}
if newMetadata.Instance.Attributes.DisableAddressManager != nil {
return *newMetadata.Instance.Attributes.DisableAddressManager, nil
}
if newMetadata.Project.Attributes.DisableAddressManager != nil {
return *newMetadata.Project.Attributes.DisableAddressManager, nil
}
// This is the linux config key, defaulting to true. On Linux, the
// config file has lower priority since we ship a file with defaults.
return !config.Daemons.NetworkDaemon, nil
}
func (a *addressMgr) Set(ctx context.Context) error {
config := cfg.Get()
if runtime.GOOS == "windows" {
a.applyWSFCFilter(config)
}
// Guest Agent does not manage interfaces on Windows.
if runtime.GOOS != "windows" {
// Setup network interfaces.
err := network.SetupInterfaces(ctx, config, newMetadata)
if err != nil {
return fmt.Errorf("failed to setup network interfaces: %v", err)
}
}
if !config.NetworkInterfaces.IPForwarding {
return nil
}
logger.Debugf("Add routes for aliases, forwarded IP and target-instance IPs")
// Add routes for IP aliases, forwarded and target-instance IPs.
for _, ni := range newMetadata.Instance.NetworkInterfaces {
iface, err := network.GetInterfaceByMAC(ni.Mac)
if err != nil {
if !slices.Contains(badMAC, ni.Mac) {
logger.Errorf("Error getting interface: %s", err)
badMAC = append(badMAC, ni.Mac)
}
continue
}
wantIPs := ni.ForwardedIps
wantIPs = append(wantIPs, ni.ForwardedIpv6s...)
if config.IPForwarding.TargetInstanceIPs {
wantIPs = append(wantIPs, ni.TargetInstanceIps...)
}
// IP Aliases are not supported on windows.
if runtime.GOOS != "windows" && config.IPForwarding.IPAliases {
wantIPs = append(wantIPs, ni.IPAliases...)
}
var forwardedIPs []string
var configuredIPs []string
if runtime.GOOS == "windows" {
addrs, err := iface.Addrs()
if err != nil {
logger.Errorf("Error getting addresses for interface %s: %s", iface.Name, err)
}
for _, addr := range addrs {
configuredIPs = append(configuredIPs, strings.TrimSuffix(addr.String(), "/32"))
}
regFwdIPs, err := getForwardsFromRegistry(ni.Mac)
if err != nil {
logger.Errorf("Error getting forwards from registry: %s", err)
continue
}
for _, ip := range configuredIPs {
// Only add to `forwardedIPs` if it is recorded in the registry.
if slices.Contains(regFwdIPs, ip) {
forwardedIPs = append(forwardedIPs, ip)
}
}
} else {
forwardedIPs, err = getLocalRoutes(ctx, config, iface.Name)
if err != nil {
logger.Errorf("Error getting routes: %v", err)
continue
}
}
// Trims any '/32' suffix for consistency.
trimSuffix := func(entries []string) []string {
var res []string
for _, entry := range entries {
res = append(res, strings.TrimSuffix(entry, "/32"))
}
return res
}
forwardedIPs = trimSuffix(forwardedIPs)
wantIPs = trimSuffix(wantIPs)
toAdd, toRm := compareRoutes(forwardedIPs, wantIPs)
if len(toAdd) != 0 || len(toRm) != 0 {
var msg string
msg = fmt.Sprintf("Changing forwarded IPs for %s from %q to %q by", ni.Mac, forwardedIPs, wantIPs)
if len(toAdd) != 0 {
msg += fmt.Sprintf(" adding %q", toAdd)
}
if len(toRm) != 0 {
if len(toAdd) != 0 {
msg += " and"
}
msg += fmt.Sprintf(" removing %q", toRm)
}
logger.Infof(msg)
}
var registryEntries []string
for _, ip := range wantIPs {
// If the IP is not in toAdd, add to registry list and continue.
if !slices.Contains(toAdd, ip) {
registryEntries = append(registryEntries, ip)
continue
}
var err error
if runtime.GOOS == "windows" {
// Don't addAddress if this is already configured.
if !slices.Contains(configuredIPs, ip) {
// In case of forwardedIpv6 we get IPV6 CIDR and Parse IP will return nil.
netip := net.ParseIP(ip)
if netip != nil && !isIPv6(netip) {
// Retains existing behavior for ipv4 addresses.
err = addAddress(netip, net.IPv4Mask(255, 255, 255, 255), uint32(iface.Index))
} else {
err = addIpv6Address(ip, uint32(iface.Index))
}
}
} else {
err = addLocalRoute(ctx, config, ip, iface.Name)
}
if err == nil {
registryEntries = append(registryEntries, ip)
} else {
logger.Errorf("error adding route: %v", err)
}
}
for _, ip := range toRm {
var err error
if runtime.GOOS == "windows" {
if !slices.Contains(configuredIPs, ip) {
continue
}
netip := net.ParseIP(ip)
// In case of forwardedIpv6 we get IPV6 CIDR and Parse IP will return nil.
if netip != nil && !isIPv6(netip) {
// Retains existing behavior for ipv4 addresses.
err = removeAddress(netip, net.IPv4Mask(255, 255, 255, 255), uint32(iface.Index))
} else {
err = removeIpv6Address(ip, uint32(iface.Index))
}
} else {
err = removeLocalRoute(ctx, config, ip, iface.Name)
}
if err != nil {
logger.Errorf("error removing route: %v", err)
// Add IPs we fail to remove to registry to maintain accurate record.
registryEntries = append(registryEntries, ip)
}
}
if runtime.GOOS == "windows" {
if err := writeRegMultiString(addressKey, ni.Mac, registryEntries); err != nil {
logger.Errorf("error writing registry: %s", err)
}
}
}
logger.Infof("Completed adding/removing routes for aliases, forwarded IP and target-instance IPs")
return nil
}
// isIPv6 returns true if the IP address is an IPv6 address.
func isIPv6(ip net.IP) bool {
return ip.To4() == nil
}
// addIpv6Address adds given IP on the provided network interface.
func addIpv6Address(s string, idx uint32) error {
ip, n, err := net.ParseCIDR(s)
if err != nil {
return err
}
return addAddress(ip, n.Mask, idx)
}
// removeIpv6Address removes the given IP on the network interface.
func removeIpv6Address(s string, idx uint32) error {
ip, n, err := net.ParseCIDR(s)
if err != nil {
return err
}
return removeAddress(ip, n.Mask, idx)
}