cni/network/multitenancy.go (305 lines of code) (raw):

package network import ( "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "os" "strconv" "strings" "time" "github.com/Azure/azure-container-networking/cni" "github.com/Azure/azure-container-networking/cns" "github.com/Azure/azure-container-networking/cns/client" "github.com/Azure/azure-container-networking/common" "github.com/Azure/azure-container-networking/network" cniTypes "github.com/containernetworking/cni/pkg/types" cniTypesCurr "github.com/containernetworking/cni/pkg/types/100" "go.uber.org/zap" ) const ( filePerm = 0o664 httpTimeout = 5 ) // MultitenancyClient interface type MultitenancyClient interface { SetupRoutingForMultitenancy( nwCfg *cni.NetworkConfig, cnsNetworkConfig *cns.GetNetworkContainerResponse, azIpamResult *cniTypesCurr.Result, epInfo *network.EndpointInfo, result *network.InterfaceInfo) DetermineSnatFeatureOnHost( snatFile string, nmAgentSupportedApisURL string) (bool, bool, error) GetAllNetworkContainers( ctx context.Context, nwCfg *cni.NetworkConfig, podName string, podNamespace string, ifName string) (IPAMAddResult, error) Init(cnsclient cnsclient, netioshim netioshim) } type Multitenancy struct { // cnsclient is used to communicate with CNS cnsclient cnsclient // netioshim is used to interact with networking syscalls netioshim netioshim } type netioshim interface { GetInterfaceSubnetWithSpecificIP(ipAddr string) *net.IPNet } type AzureNetIOShim struct{} func (a AzureNetIOShim) GetInterfaceSubnetWithSpecificIP(ipAddr string) *net.IPNet { return common.GetInterfaceSubnetWithSpecificIP(ipAddr) } var errNmaResponse = errors.New("nmagent request status code") func (m *Multitenancy) Init(cnsclient cnsclient, netioshim netioshim) { m.cnsclient = cnsclient m.netioshim = netioshim } // DetermineSnatFeatureOnHost - Temporary function to determine whether we need to disable SNAT due to NMAgent support func (m *Multitenancy) DetermineSnatFeatureOnHost(snatFile, nmAgentSupportedApisURL string) (snatForDNS, snatOnHost bool, err error) { var ( snatConfig snatConfiguration retrieveSnatConfigErr error jsonFile *os.File httpClient = &http.Client{Timeout: time.Second * httpTimeout} snatConfigFile = snatConfigFileName + jsonFileExtension ) // Check if we've already retrieved NMAgent version and determined whether to disable snat on host if jsonFile, retrieveSnatConfigErr = os.Open(snatFile); retrieveSnatConfigErr == nil { bytes, _ := io.ReadAll(jsonFile) jsonFile.Close() if retrieveSnatConfigErr = json.Unmarshal(bytes, &snatConfig); retrieveSnatConfigErr != nil { logger.Error("failed to unmarshal to snatConfig with error %v", zap.Error(retrieveSnatConfigErr)) } } // If we weren't able to retrieve snatConfiguration, query NMAgent if retrieveSnatConfigErr != nil { var resp *http.Response req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, nmAgentSupportedApisURL, nil) if err != nil { logger.Error("failed creating http request", zap.Error(err)) return false, false, fmt.Errorf("%w", err) } logger.Info("Query nma for dns snat support", zap.String("query", nmAgentSupportedApisURL)) resp, retrieveSnatConfigErr = httpClient.Do(req) if retrieveSnatConfigErr == nil { defer resp.Body.Close() if resp.StatusCode == http.StatusOK { var bodyBytes []byte // if the list of APIs (strings) contains the nmAgentSnatSupportAPI we will disable snat on host if bodyBytes, retrieveSnatConfigErr = io.ReadAll(resp.Body); retrieveSnatConfigErr == nil { bodyStr := string(bodyBytes) if !strings.Contains(bodyStr, nmAgentSnatAndDnsSupportAPI) { snatConfig.EnableSnatForDns = true snatConfig.EnableSnatOnHost = !strings.Contains(bodyStr, nmAgentSnatSupportAPI) } jsonStr, _ := json.Marshal(snatConfig) fp, err := os.OpenFile(snatConfigFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(filePerm)) if err == nil { _, err = fp.Write(jsonStr) if err != nil { logger.Error("DetermineSnatFeatureOnHost: Write to json failed", zap.Error(err)) } fp.Close() } else { logger.Error("failed to save snat settings", zap.String("snatConfgFile", snatConfigFile), zap.Error(err)) } } } else { retrieveSnatConfigErr = fmt.Errorf("%w:%d", errNmaResponse, resp.StatusCode) } } } // Log and return the error when we fail acquire snat configuration for host and dns if retrieveSnatConfigErr != nil { logger.Error("failed to acquire SNAT configuration with error %v", zap.Error(retrieveSnatConfigErr)) return snatConfig.EnableSnatForDns, snatConfig.EnableSnatOnHost, retrieveSnatConfigErr } logger.Info("saved snat settings", zap.Any("snatConfig", snatConfig), zap.String("snatConfigfile", snatConfigFile)) if snatConfig.EnableSnatOnHost { logger.Info("enabling SNAT on container host for outbound connectivity") } if snatConfig.EnableSnatForDns { logger.Info("enabling SNAT on container host for DNS traffic") } if !snatConfig.EnableSnatForDns && !snatConfig.EnableSnatOnHost { logger.Info("disabling SNAT on container host") } return snatConfig.EnableSnatForDns, snatConfig.EnableSnatOnHost, nil } func (m *Multitenancy) SetupRoutingForMultitenancy( nwCfg *cni.NetworkConfig, cnsNetworkConfig *cns.GetNetworkContainerResponse, azIpamResult *cniTypesCurr.Result, epInfo *network.EndpointInfo, result *network.InterfaceInfo, ) { // Adding default gateway // if snat enabled, add 169.254.128.1 as default gateway if nwCfg.EnableSnatOnHost { logger.Info("add default route for multitenancy.snat on host enabled") addDefaultRoute(cnsNetworkConfig.LocalIPConfiguration.GatewayIPAddress, epInfo, result) } else { _, defaultIPNet, _ := net.ParseCIDR("0.0.0.0/0") dstIP := net.IPNet{IP: net.ParseIP("0.0.0.0"), Mask: defaultIPNet.Mask} gwIP := net.ParseIP(cnsNetworkConfig.IPConfiguration.GatewayIPAddress) epInfo.Routes = append(epInfo.Routes, network.RouteInfo{Dst: dstIP, Gw: gwIP}) result.Routes = append(result.Routes, network.RouteInfo{Dst: dstIP, Gw: gwIP}) if epInfo.EnableSnatForDns { logger.Info("add SNAT for DNS enabled") addSnatForDNS(cnsNetworkConfig.LocalIPConfiguration.GatewayIPAddress, epInfo, result) } } setupInfraVnetRoutingForMultitenancy(nwCfg, azIpamResult, epInfo) } // get all network container configuration(s) for given orchestratorContext func (m *Multitenancy) GetAllNetworkContainers( ctx context.Context, nwCfg *cni.NetworkConfig, podName, podNamespace, ifName string, ) (IPAMAddResult, error) { var podNameWithoutSuffix string if !nwCfg.EnableExactMatchForPodName { podNameWithoutSuffix = network.GetPodNameWithoutSuffix(podName) } else { podNameWithoutSuffix = podName } logger.Info("Podname without suffix", zap.String("podName", podNameWithoutSuffix)) ncResponses, hostSubnetPrefixes, err := m.getNetworkContainersInternal(ctx, podNamespace, podNameWithoutSuffix) if err != nil { return IPAMAddResult{}, fmt.Errorf("%w", err) } for i := 0; i < len(ncResponses); i++ { if nwCfg.EnableSnatOnHost { if ncResponses[i].LocalIPConfiguration.IPSubnet.IPAddress == "" { logger.Info("Snat IP is not populated for ncs. Got empty string", zap.Any("response", ncResponses)) return IPAMAddResult{}, errSnatIP } } } ipamResult := IPAMAddResult{} ipamResult.interfaceInfo = make(map[string]network.InterfaceInfo) for i := 0; i < len(ncResponses); i++ { // one ncResponse gets you one interface info in the returned IPAMAddResult ifInfo := network.InterfaceInfo{ NCResponse: &ncResponses[i], HostSubnetPrefix: hostSubnetPrefixes[i], } ipconfig, routes := convertToIPConfigAndRouteInfo(ifInfo.NCResponse) ifInfo.IPConfigs = append(ifInfo.IPConfigs, ipconfig) ifInfo.Routes = routes ifInfo.NICType = cns.InfraNIC // assuming we only assign infra nics in this function ipamResult.interfaceInfo[m.getInterfaceInfoKey(ifInfo.NICType, i)] = ifInfo } return ipamResult, err } // get all network containers configuration for given orchestratorContext func (m *Multitenancy) getNetworkContainersInternal( ctx context.Context, namespace, podName string, ) ([]cns.GetNetworkContainerResponse, []net.IPNet, error) { podInfo := cns.KubernetesPodInfo{ PodName: podName, PodNamespace: namespace, } orchestratorContext, err := json.Marshal(podInfo) if err != nil { logger.Error("Marshalling KubernetesPodInfo failed", zap.Error(err)) return nil, []net.IPNet{}, fmt.Errorf("%w", err) } // First try the new CNS API that returns slice of nc responses. If CNS doesn't support the new API, an error will be returned and as a result // try using the old CNS API that returns single nc response. ncConfigs, err := m.cnsclient.GetAllNetworkContainers(ctx, orchestratorContext) if err != nil && client.IsUnsupportedAPI(err) { ncConfig, errGetNC := m.cnsclient.GetNetworkContainer(ctx, orchestratorContext) if errGetNC != nil { return nil, []net.IPNet{}, fmt.Errorf("%w", errGetNC) } ncConfigs = append(ncConfigs, *ncConfig) } else if err != nil { return nil, []net.IPNet{}, fmt.Errorf("%w", err) } logger.Info("Network config received from cns", zap.Any("nconfig", ncConfigs)) subnetPrefixes := []net.IPNet{} for i := 0; i < len(ncConfigs); i++ { subnetPrefix := m.netioshim.GetInterfaceSubnetWithSpecificIP(ncConfigs[i].PrimaryInterfaceIdentifier) if subnetPrefix == nil { logger.Error(errIfaceNotFound.Error(), zap.String("nodeIP", ncConfigs[i].PrimaryInterfaceIdentifier)) return nil, []net.IPNet{}, errIfaceNotFound } subnetPrefixes = append(subnetPrefixes, *subnetPrefix) } return ncConfigs, subnetPrefixes, nil } func convertToCniResult(networkConfig *cns.GetNetworkContainerResponse, ifName string) *cniTypesCurr.Result { result := &cniTypesCurr.Result{} resultIpconfig := &cniTypesCurr.IPConfig{} ipconfig := networkConfig.IPConfiguration ipAddr := net.ParseIP(ipconfig.IPSubnet.IPAddress) if ipAddr.To4() != nil { resultIpconfig.Address = net.IPNet{IP: ipAddr, Mask: net.CIDRMask(int(ipconfig.IPSubnet.PrefixLength), ipv4FullMask)} } else { resultIpconfig.Address = net.IPNet{IP: ipAddr, Mask: net.CIDRMask(int(ipconfig.IPSubnet.PrefixLength), ipv6FullMask)} } resultIpconfig.Gateway = net.ParseIP(ipconfig.GatewayIPAddress) result.IPs = append(result.IPs, resultIpconfig) if networkConfig.Routes != nil && len(networkConfig.Routes) > 0 { for _, route := range networkConfig.Routes { _, routeIPnet, _ := net.ParseCIDR(route.IPAddress) gwIP := net.ParseIP(route.GatewayIPAddress) result.Routes = append(result.Routes, &cniTypes.Route{Dst: *routeIPnet, GW: gwIP}) } } for _, ipRouteSubnet := range networkConfig.CnetAddressSpace { routeIPnet := net.IPNet{IP: net.ParseIP(ipRouteSubnet.IPAddress), Mask: net.CIDRMask(int(ipRouteSubnet.PrefixLength), ipv4FullMask)} gwIP := net.ParseIP(ipconfig.GatewayIPAddress) result.Routes = append(result.Routes, &cniTypes.Route{Dst: routeIPnet, GW: gwIP}) } iface := &cniTypesCurr.Interface{Name: ifName} result.Interfaces = append(result.Interfaces, iface) return result } func convertToIPConfigAndRouteInfo(networkConfig *cns.GetNetworkContainerResponse) (*network.IPConfig, []network.RouteInfo) { ipconfig := &network.IPConfig{} cnsIPConfig := networkConfig.IPConfiguration ipAddr := net.ParseIP(cnsIPConfig.IPSubnet.IPAddress) if ipAddr.To4() != nil { ipconfig.Address = net.IPNet{IP: ipAddr, Mask: net.CIDRMask(int(cnsIPConfig.IPSubnet.PrefixLength), ipv4FullMask)} } else { ipconfig.Address = net.IPNet{IP: ipAddr, Mask: net.CIDRMask(int(cnsIPConfig.IPSubnet.PrefixLength), ipv6FullMask)} } ipconfig.Gateway = net.ParseIP(cnsIPConfig.GatewayIPAddress) routes := make([]network.RouteInfo, 0) if networkConfig.Routes != nil && len(networkConfig.Routes) > 0 { for _, route := range networkConfig.Routes { _, routeIPnet, _ := net.ParseCIDR(route.IPAddress) gwIP := net.ParseIP(route.GatewayIPAddress) routes = append(routes, network.RouteInfo{Dst: *routeIPnet, Gw: gwIP}) } } for _, ipRouteSubnet := range networkConfig.CnetAddressSpace { routeIPnet := net.IPNet{IP: net.ParseIP(ipRouteSubnet.IPAddress), Mask: net.CIDRMask(int(ipRouteSubnet.PrefixLength), ipv4FullMask)} routes = append(routes, network.RouteInfo{Dst: routeIPnet, Gw: ipconfig.Gateway}) } return ipconfig, routes } func checkIfSubnetOverlaps(enableInfraVnet bool, nwCfg *cni.NetworkConfig, cnsNetworkConfig *cns.GetNetworkContainerResponse) bool { if enableInfraVnet { if cnsNetworkConfig != nil { _, infraNet, _ := net.ParseCIDR(nwCfg.InfraVnetAddressSpace) for _, cnetSpace := range cnsNetworkConfig.CnetAddressSpace { cnetSpaceIPNet := &net.IPNet{ IP: net.ParseIP(cnetSpace.IPAddress), Mask: net.CIDRMask(int(cnetSpace.PrefixLength), ipv4FullMask), } return infraNet.Contains(cnetSpaceIPNet.IP) || cnetSpaceIPNet.Contains(infraNet.IP) } } } return false } func (m *Multitenancy) getInterfaceInfoKey(nicType cns.NICType, i int) string { return string(nicType) + strconv.Itoa(i) } var ( errSnatIP = errors.New("Snat IP not populated") errInfraVnet = errors.New("infravnet not populated") errSubnetOverlap = errors.New("subnet overlap error") errIfaceNotFound = errors.New("Interface not found for this ip") )