azure-container-networking/network/network_linux.go

749 строки
21 KiB
Go

// Copyright 2017 Microsoft. All rights reserved.
// MIT License
package network
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/Azure/azure-container-networking/cns"
"github.com/Azure/azure-container-networking/iptables"
"github.com/Azure/azure-container-networking/netio"
"github.com/Azure/azure-container-networking/netlink"
"github.com/Azure/azure-container-networking/network/networkutils"
"github.com/Azure/azure-container-networking/ovsctl"
"github.com/Azure/azure-container-networking/platform"
"github.com/pkg/errors"
"go.uber.org/zap"
"golang.org/x/sys/unix"
)
const (
// Prefix for bridge names.
bridgePrefix = "azure"
// Virtual MAC address used by Azure VNET.
virtualMacAddress = "12:34:56:78:9a:bc"
versionID = "VERSION_ID"
distroID = "ID"
ubuntuStr = "ubuntu"
dnsServersStr = "DNS Servers"
dnsDomainStr = "DNS Domain"
ubuntuVersion17 = 17
// OptVethName key for veth name option
OptVethName = "vethname"
// SnatBridgeIPKey key for the SNAT bridge
SnatBridgeIPKey = "snatBridgeIP"
// LocalIPKey key for local IP
LocalIPKey = "localIP"
// InfraVnetIPKey key for infra vnet
InfraVnetIPKey = "infraVnetIP"
// Ubuntu Release Version for checking which command to use.
Ubuntu22 = "22.04"
)
const (
lineDelimiter = "\n"
colonDelimiter = ":"
dotDelimiter = "."
)
var errorNetworkManager = errors.New("Network_linux pkg error")
func newErrorNetworkManager(errStr string) error {
return fmt.Errorf("%w : %s", errorNetworkManager, errStr)
}
// Linux implementation of route.
type route netlink.Route
// NewNetworkImpl creates a new container network.
func (nm *networkManager) newNetworkImpl(nwInfo *EndpointInfo, extIf *externalInterface) (*network, error) {
// Connect the external interface.
var (
vlanid int
ifName string
)
opt, _ := nwInfo.Options[genericData].(map[string]interface{})
logger.Info("opt options", zap.Any("opt", opt), zap.Any("options", nwInfo.Options))
switch nwInfo.Mode {
case opModeTunnel:
fallthrough
case opModeBridge:
logger.Info("create bridge")
ifName = extIf.BridgeName
if err := nm.connectExternalInterface(extIf, nwInfo); err != nil {
return nil, err
}
if opt != nil && opt[VlanIDKey] != nil {
vlanid, _ = strconv.Atoi(opt[VlanIDKey].(string))
}
case opModeTransparent:
logger.Info("Transparent mode")
ifName = extIf.Name
if nwInfo.IPV6Mode != "" {
nu := networkutils.NewNetworkUtils(nm.netlink, nm.plClient)
if err := nu.EnableIPV6Forwarding(); err != nil {
return nil, fmt.Errorf("Ipv6 forwarding failed: %w", err)
}
}
case opModeTransparentVlan:
logger.Info("Transparent vlan mode")
ifName = extIf.Name
nu := networkutils.NewNetworkUtils(nm.netlink, nm.plClient)
if err := nu.EnableIPV4Forwarding(); err != nil {
return nil, errors.Wrap(err, "ipv4 forwarding failed")
}
if err := nu.UpdateIPV6Setting(1); err != nil {
return nil, errors.Wrap(err, "failed to disable ipv6 on vm")
}
// Blocks wireserver traffic from apipa nic
if err := nu.BlockEgressTrafficFromContainer(nm.iptablesClient, iptables.V4, networkutils.AzureDNS, iptables.TCP, iptables.HTTPPort); err != nil {
return nil, errors.Wrap(err, "unable to insert vm iptables rule drop wireserver packets")
}
default:
return nil, errNetworkModeInvalid
}
err := nm.handleCommonOptions(ifName, nwInfo)
if err != nil {
logger.Error("handleCommonOptions failed with", zap.Error(err))
return nil, err
}
// Create the network object.
nw := &network{
Id: nwInfo.NetworkID,
Mode: nwInfo.Mode,
Endpoints: make(map[string]*endpoint),
extIf: extIf,
VlanId: vlanid,
EnableSnatOnHost: nwInfo.EnableSnatOnHost,
}
return nw, nil
}
func (nm *networkManager) handleCommonOptions(ifName string, nwInfo *EndpointInfo) error {
var err error
if routes, exists := nwInfo.Options[RoutesKey]; exists {
err = addRoutes(nm.netlink, nm.netio, ifName, routes.([]RouteInfo))
if err != nil {
return err
}
}
if iptcmds, exists := nwInfo.Options[IPTablesKey]; exists {
err = nm.addToIptables(iptcmds.([]iptables.IPTableEntry))
if err != nil {
return err
}
}
return nil
}
// DeleteNetworkImpl deletes an existing container network.
func (nm *networkManager) deleteNetworkImpl(nw *network, _ cns.NICType) error {
var networkClient NetworkClient
if nw.VlanId != 0 {
networkClient = NewOVSClient(nw.extIf.BridgeName, nw.extIf.Name, ovsctl.NewOvsctl(), nm.netlink, nm.plClient)
} else {
networkClient = NewLinuxBridgeClient(nw.extIf.BridgeName, nw.extIf.Name, EndpointInfo{}, nm.netlink, nm.plClient)
}
// Disconnect the interface if this was the last network using it.
if len(nw.extIf.Networks) == 1 {
nm.disconnectExternalInterface(nw.extIf, networkClient)
}
return nil
}
// SaveIPConfig saves the IP configuration of an interface.
func (nm *networkManager) saveIPConfig(hostIf *net.Interface, extIf *externalInterface) error {
// Save the default routes on the interface.
routes, err := nm.netlink.GetIPRoute(&netlink.Route{Dst: &net.IPNet{}, LinkIndex: hostIf.Index})
if err != nil {
logger.Error("Failed to query routes", zap.Error(err))
return err
}
for _, r := range routes {
if r.Dst == nil {
if r.Family == unix.AF_INET {
extIf.IPv4Gateway = r.Gw
} else if r.Family == unix.AF_INET6 {
extIf.IPv6Gateway = r.Gw
}
}
extIf.Routes = append(extIf.Routes, (*route)(r))
}
// Save global unicast IP addresses on the interface.
addrs, err := hostIf.Addrs()
for _, addr := range addrs {
ipAddr, ipNet, err := net.ParseCIDR(addr.String())
ipNet.IP = ipAddr
if err != nil {
continue
}
if !ipAddr.IsGlobalUnicast() {
continue
}
extIf.IPAddresses = append(extIf.IPAddresses, ipNet)
logger.Info("Deleting IP address from interface", zap.Any("ipNet", ipNet), zap.String("hostInfName", hostIf.Name))
err = nm.netlink.DeleteIPAddress(hostIf.Name, ipAddr, ipNet)
if err != nil {
break
}
}
logger.Info("Saved interface IP configuration", zap.Any("extIf", extIf))
return err
}
func getMajorVersion(version string) (int, error) {
versionSplit := strings.Split(version, dotDelimiter)
if len(versionSplit) > 0 {
retrieved_version, err := strconv.Atoi(versionSplit[0])
if err != nil {
return 0, err
}
return retrieved_version, err
}
return 0, fmt.Errorf("Error getting major version") //nolint
}
func isGreaterOrEqaulUbuntuVersion(versionToMatch int) bool {
osInfo, err := platform.GetOSDetails()
if err != nil {
logger.Error("Unable to get OS Details", zap.Error(err))
return false
}
logger.Info("OSInfo", zap.Any("osInfo", osInfo))
version := osInfo[versionID]
distro := osInfo[distroID]
if strings.EqualFold(distro, ubuntuStr) {
version = strings.Trim(version, "\"")
retrieved_version, err := getMajorVersion(version)
if err != nil {
logger.Error("Not setting dns. Unable to retrieve major version", zap.Error(err))
return false
}
if retrieved_version >= versionToMatch {
return true
}
}
return false
}
func (nm *networkManager) systemVersion() (string, error) {
osVersion, err := nm.plClient.ExecuteRawCommand("lsb_release -rs")
if err != nil {
return osVersion, errors.Wrap(err, "error retrieving the system distribution version")
}
return osVersion, nil
}
func (nm *networkManager) addDomain(ifName, domain string) (string, error) {
osVersion, err := nm.systemVersion()
if err != nil {
return osVersion, err
}
var cmd string
switch {
case strings.HasPrefix(osVersion, Ubuntu22):
cmd = fmt.Sprintf("resolvectl domain %s %s", ifName, domain)
default:
cmd = fmt.Sprintf("systemd-resolve --interface %s --set-domain %s", ifName, domain)
}
return cmd, nil
}
func (nm *networkManager) addDNSServers(ifName string, dnsServers []string) (string, error) {
osVersion, err := nm.systemVersion()
if err != nil {
return osVersion, err
}
if len(dnsServers) == 0 {
logger.Warn("No dns servers to add")
return "", nil
}
var cmd string
switch {
case strings.HasPrefix(osVersion, Ubuntu22):
cmd = fmt.Sprintf("resolvectl dns %s %s", ifName, strings.Join(dnsServers, " "))
default:
serverList := ""
for _, server := range dnsServers {
serverList = serverList + " --set-dns " + server
}
cmd = fmt.Sprintf("systemd-resolve --interface %s %s", ifName, serverList)
}
return cmd, nil
}
func (nm *networkManager) ifNameStatus(ifName string) (string, error) {
osVersion, err := nm.systemVersion()
if err != nil {
return osVersion, err
}
var cmd string
switch {
case strings.HasPrefix(osVersion, Ubuntu22):
cmd = fmt.Sprintf("resolvectl status %s", ifName)
default:
cmd = fmt.Sprintf("systemd-resolve --status %s", ifName)
}
return cmd, nil
}
func (nm *networkManager) readDNSInfo(ifName string) (DNSInfo, error) {
var dnsInfo DNSInfo
cmd, err := nm.ifNameStatus(ifName)
if err != nil {
return dnsInfo, errors.Wrap(err, "Error generating interface name status cmd")
}
out, err := nm.plClient.ExecuteRawCommand(cmd)
if err != nil {
return dnsInfo, errors.Wrapf(err, "Error executing interface status with cmd %s", cmd)
}
logger.Info("console output for above cmd", zap.Any("out", out))
lineArr := strings.Split(out, lineDelimiter)
if len(lineArr) <= 0 {
return dnsInfo, fmt.Errorf("Console output doesn't have any lines") //nolint
}
dnsServerFound := false
for _, line := range lineArr {
if strings.Contains(line, dnsServersStr) {
dnsServerSplit := strings.Split(line, colonDelimiter)
if len(dnsServerSplit) > 1 {
dnsServerFound = true
dnsServerSplit[1] = strings.TrimSpace(dnsServerSplit[1])
dnsInfo.Servers = append(dnsInfo.Servers, dnsServerSplit[1])
}
} else if !strings.Contains(line, colonDelimiter) && dnsServerFound {
dnsServer := strings.TrimSpace(line)
dnsInfo.Servers = append(dnsInfo.Servers, dnsServer)
} else {
dnsServerFound = false
}
}
for _, line := range lineArr {
if strings.Contains(line, dnsDomainStr) {
dnsDomainSplit := strings.Split(line, colonDelimiter)
if len(dnsDomainSplit) > 1 {
dnsInfo.Suffix = strings.TrimSpace(dnsDomainSplit[1])
}
}
}
return dnsInfo, nil
}
func (nm *networkManager) saveDNSConfig(extIf *externalInterface) error {
dnsInfo, err := nm.readDNSInfo(extIf.Name)
if err != nil || len(dnsInfo.Servers) == 0 || dnsInfo.Suffix == "" {
logger.Info("Failed to read dns info from interface", zap.Any("dnsInfo", dnsInfo), zap.String("extIfName", extIf.Name),
zap.Error(err))
return err
}
extIf.DNSInfo = dnsInfo
logger.Info("Saved DNS Info", zap.Any("DNSInfo", extIf.DNSInfo), zap.String("extIfName", extIf.Name))
return nil
}
// ApplyIPConfig applies a previously saved IP configuration to an interface.
func (nm *networkManager) applyIPConfig(extIf *externalInterface, targetIf *net.Interface) error {
// Add IP addresses.
for _, addr := range extIf.IPAddresses {
logger.Info("Adding IP address to interface", zap.Any("addr", addr), zap.String("Name", targetIf.Name))
err := nm.netlink.AddIPAddress(targetIf.Name, addr.IP, addr)
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "file exists") {
logger.Info("Failed to add IP address", zap.Any("addr", addr), zap.Error(err))
return err
}
}
// Add IP routes.
for _, route := range extIf.Routes {
route.LinkIndex = targetIf.Index
logger.Info("Adding IP route", zap.Any("route", route))
err := nm.netlink.AddIPRoute((*netlink.Route)(route))
if err != nil {
logger.Error("Failed to add IP route", zap.Any("route", route), zap.Error(err))
return err
}
}
return nil
}
func (nm *networkManager) applyDNSConfig(extIf *externalInterface, ifName string) error {
var (
setDNSList []string
cmd string
err error
)
if extIf != nil {
for _, server := range extIf.DNSInfo.Servers {
if net.ParseIP(server).To4() == nil {
logger.Error("Invalid dns ip", zap.String("server", server))
continue
}
setDNSList = append(setDNSList, server)
}
if len(setDNSList) > 0 {
cmd, err = nm.addDNSServers(ifName, setDNSList)
if err != nil {
return errors.Wrap(err, "Error generating add DNS Servers cmd")
}
if cmd != "" {
_, err = nm.plClient.ExecuteRawCommand(cmd)
if err != nil {
return errors.Wrapf(err, "Error executing add DNS Servers with cmd %s", cmd)
}
}
}
if extIf.DNSInfo.Suffix != "" {
cmd, err = nm.addDomain(ifName, extIf.DNSInfo.Suffix)
if err != nil {
return errors.Wrap(err, "Error generating add domain cmd")
}
_, err = nm.plClient.ExecuteRawCommand(cmd)
if err != nil {
return errors.Wrapf(err, "Error executing add Domain with cmd %s", cmd)
}
}
}
return err
}
// ConnectExternalInterface connects the given host interface to a bridge.
func (nm *networkManager) connectExternalInterface(extIf *externalInterface, nwInfo *EndpointInfo) error {
var (
err error
networkClient NetworkClient
)
defer func() {
logger.Info("Connecting interface completed", zap.String("Name", extIf.Name), zap.Error(err))
}()
// Check whether this interface is already connected.
if extIf.BridgeName != "" {
logger.Info("Interface is already connected to bridge", zap.String("BridgeName", extIf.BridgeName))
return nil
}
// Find the external interface.
hostIf, err := net.InterfaceByName(extIf.Name)
if err != nil {
return err
}
// If a bridge name is not specified, generate one based on the external interface index.
bridgeName := nwInfo.BridgeName
if bridgeName == "" {
bridgeName = fmt.Sprintf("%s%d", bridgePrefix, hostIf.Index)
}
opt, _ := nwInfo.Options[genericData].(map[string]interface{})
if opt != nil && opt[VlanIDKey] != nil {
networkClient = NewOVSClient(bridgeName, extIf.Name, ovsctl.NewOvsctl(), nm.netlink, nm.plClient)
} else {
networkClient = NewLinuxBridgeClient(bridgeName, extIf.Name, *nwInfo, nm.netlink, nm.plClient)
}
// Check if the bridge already exists.
bridge, err := net.InterfaceByName(bridgeName)
if err != nil {
// Create the bridge.
if err = networkClient.CreateBridge(); err != nil {
logger.Error("Error while creating bridge", zap.Error(err))
return err
}
bridge, err = net.InterfaceByName(bridgeName)
if err != nil {
return err
}
} else {
// Use the existing bridge.
logger.Info("Found existing bridge", zap.String("bridgeName", bridgeName))
}
defer func() {
if err != nil {
logger.Info("cleanup network")
nm.disconnectExternalInterface(extIf, networkClient)
}
}()
// Save host IP configuration.
err = nm.saveIPConfig(hostIf, extIf)
if err != nil {
logger.Error("Failed to save IP configuration for interface",
zap.String("Name", hostIf.Name), zap.Error(err))
}
/*
If custom dns server is updated, VM needs reboot for the change to take effect.
*/
isGreaterOrEqualUbuntu17 := isGreaterOrEqaulUbuntuVersion(ubuntuVersion17)
isSystemdResolvedActive := false
if isGreaterOrEqualUbuntu17 {
// Don't copy dns servers if systemd-resolved isn't available
if _, cmderr := nm.plClient.ExecuteRawCommand("systemctl status systemd-resolved"); cmderr == nil {
isSystemdResolvedActive = true
logger.Info("Saving dns config from", zap.String("Name", hostIf.Name))
if err = nm.saveDNSConfig(extIf); err != nil {
logger.Error("Failed to save dns config", zap.Error(err))
return err
}
}
}
logger.Info("Modifying interfaces", zap.String("Name", hostIf.Name))
// External interface down.
err = nm.netlink.SetLinkState(hostIf.Name, false)
if err != nil {
return errors.Wrap(err, "failed to set external interface down")
}
// Connect the external interface to the bridge.
if err = networkClient.SetBridgeMasterToHostInterface(); err != nil {
return errors.Wrap(err, "failed to connect external interface to bridge")
}
// External interface up.
err = nm.netlink.SetLinkState(hostIf.Name, true)
if err != nil {
return errors.Wrap(err, "failed to set external interface up")
}
// Bridge up.
err = nm.netlink.SetLinkState(bridgeName, true)
if err != nil {
return errors.Wrap(err, "failed to set bridge link state up")
}
// Add the bridge rules.
err = networkClient.AddL2Rules(extIf)
if err != nil {
return errors.Wrap(err, "failed to add bridge rules")
}
// External interface hairpin on.
if !nwInfo.DisableHairpinOnHostInterface {
logger.Info("Setting link hairpin on", zap.String("Name", hostIf.Name))
if err = networkClient.SetHairpinOnHostInterface(true); err != nil {
return err
}
}
// Apply IP configuration to the bridge for host traffic.
err = nm.applyIPConfig(extIf, bridge)
if err != nil {
logger.Error("Failed to apply interface IP configuration", zap.Error(err))
return err
}
if isGreaterOrEqualUbuntu17 && isSystemdResolvedActive {
if err = nm.applyDNSConfig(extIf, bridgeName); err != nil {
logger.Error("Failed to apply DNS configuration with", zap.Error(err))
return err
}
logger.Info("Applied dns config on", zap.Any("DNSInfo", extIf.DNSInfo), zap.String("bridgeName", bridgeName))
}
if nwInfo.IPV6Mode == IPV6Nat {
// adds pod cidr gateway ip to bridge
if err = nm.addIpv6NatGateway(nwInfo); err != nil {
logger.Error("Adding IPv6 Nat Gateway failed with", zap.Error(err))
return err
}
if err = nm.addIpv6SnatRule(extIf, nwInfo); err != nil {
logger.Error("Adding IPv6 Snat Rule failed with", zap.Error(err))
return err
}
// unmark packet if set by kube-proxy to skip kube-postrouting rule and processed
// by cni snat rule
if err = nm.iptablesClient.InsertIptableRule(iptables.V6, iptables.Mangle, iptables.Postrouting, "", "MARK --set-mark 0x0"); err != nil {
logger.Error("Adding Iptable mangle rule failed", zap.Error(err))
return err
}
}
extIf.BridgeName = bridgeName
logger.Info("Connected interface to bridge", zap.String("Name", extIf.Name), zap.String("BridgeName", extIf.BridgeName))
return nil
}
// DisconnectExternalInterface disconnects a host interface from its bridge.
func (nm *networkManager) disconnectExternalInterface(extIf *externalInterface, networkClient NetworkClient) {
logger.Info("Disconnecting interface and deleting bridge rules", zap.String("Name", extIf.Name))
// Delete bridge rules set on the external interface.
networkClient.DeleteL2Rules(extIf)
logger.Info("Deleting bridge")
// Delete Bridge
networkClient.DeleteBridge()
extIf.BridgeName = ""
logger.Info("Restoring ipconfig with primary interface", zap.String("Name", extIf.Name))
// Restore IP configuration.
hostIf, _ := net.InterfaceByName(extIf.Name)
err := nm.applyIPConfig(extIf, hostIf)
if err != nil {
logger.Error("Failed to apply IP configuration", zap.Error(err))
}
extIf.IPAddresses = nil
extIf.Routes = nil
logger.Info("Disconnected interface", zap.String("Name", extIf.Name))
}
func (nm *networkManager) addToIptables(cmds []iptables.IPTableEntry) error {
logger.Info("Adding additional iptable rules...")
for _, cmd := range cmds {
err := nm.iptablesClient.RunCmd(cmd.Version, cmd.Params)
if err != nil {
return err
}
logger.Info("Successfully run iptables rule", zap.Any("cmd", cmd))
}
return nil
}
// Add ipv6 nat gateway IP on bridge
func (nm *networkManager) addIpv6NatGateway(nwInfo *EndpointInfo) error {
logger.Info("Adding ipv6 nat gateway on azure bridge")
for _, subnetInfo := range nwInfo.Subnets {
if subnetInfo.Family == platform.AfINET6 {
ipAddr := []net.IPNet{{
IP: subnetInfo.Gateway,
Mask: subnetInfo.Prefix.Mask,
}}
nuc := networkutils.NewNetworkUtils(nm.netlink, nm.plClient)
err := nuc.AssignIPToInterface(nwInfo.BridgeName, ipAddr)
if err != nil {
return newErrorNetworkManager(err.Error())
}
}
}
return nil
}
// snat ipv6 traffic to secondary ipv6 ip before leaving VM
func (nm *networkManager) addIpv6SnatRule(extIf *externalInterface, nwInfo *EndpointInfo) error {
var (
ipv6SnatRuleSet bool
ipv6SubnetPrefix net.IPNet
)
for _, subnet := range nwInfo.Subnets {
if subnet.Family == platform.AfINET6 {
ipv6SubnetPrefix = subnet.Prefix
break
}
}
if len(ipv6SubnetPrefix.IP) == 0 {
return errSubnetV6NotFound
}
for _, ipAddr := range extIf.IPAddresses {
if ipAddr.IP.To4() != nil {
continue
}
logger.Info("Adding ipv6 snat rule")
matchSrcPrefix := fmt.Sprintf("-s %s", ipv6SubnetPrefix.String())
nu := networkutils.NewNetworkUtils(nm.netlink, nm.plClient)
if err := nu.AddSnatRule(nm.iptablesClient, matchSrcPrefix, ipAddr.IP); err != nil {
return fmt.Errorf("adding iptable snat rule failed:%w", err)
}
ipv6SnatRuleSet = true
}
if !ipv6SnatRuleSet {
return errV6SnatRuleNotSet
}
return nil
}
func getNetworkInfoImpl(nwInfo *EndpointInfo, nw *network) {
if nw.VlanId != 0 {
vlanMap := make(map[string]interface{})
vlanMap[VlanIDKey] = strconv.Itoa(nw.VlanId)
nwInfo.Options[genericData] = vlanMap
}
}
// AddStaticRoute adds a static route to the interface.
func AddStaticRoute(nl netlink.NetlinkInterface, netioshim netio.NetIOInterface, ip, interfaceName string) error {
logger.Info("Adding static route", zap.String("ip", ip))
var routes []RouteInfo
_, ipNet, _ := net.ParseCIDR(ip)
gwIP := net.ParseIP("0.0.0.0")
route := RouteInfo{Dst: *ipNet, Gw: gwIP}
routes = append(routes, route)
if err := addRoutes(nl, netioshim, interfaceName, routes); err != nil {
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "file exists") {
logger.Error("addroutes failed with error", zap.Error(err))
return err
}
}
return nil
}