Skip to content

Commit 4fe1647

Browse files
committed
POC CNI and CNS changes to support IPv6 Secondary IPs.
Additional work needed, draft only
1 parent 1cc30b2 commit 4fe1647

File tree

7 files changed

+263
-6
lines changed

7 files changed

+263
-6
lines changed

cni/network/invoker_cns.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type IPResultInfo struct {
5757
routes []cns.Route
5858
pnpID string
5959
endpointPolicies []policy.Policy
60+
secondaryIPs map[string]cns.SecondaryIPConfig
6061
}
6162

6263
func (i IPResultInfo) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
@@ -162,6 +163,7 @@ func (invoker *CNSIPAMInvoker) Add(addConfig IPAMAddConfig) (IPAMAddResult, erro
162163
routes: response.PodIPInfo[i].Routes,
163164
pnpID: response.PodIPInfo[i].PnPID,
164165
endpointPolicies: response.PodIPInfo[i].EndpointPolicies,
166+
secondaryIPs: response.PodIPInfo[i].SecondaryIPConfigs,
165167
}
166168

167169
logger.Info("Received info for pod",
@@ -505,9 +507,63 @@ func configureSecondaryAddResult(info *IPResultInfo, addResult *IPAMAddResult, p
505507
SkipDefaultRoutes: info.skipDefaultRoutes,
506508
}
507509

510+
if len(info.secondaryIPs) > 0 {
511+
secIPConfig, err := BuildIPConfigForV6(info.secondaryIPs)
512+
513+
if err == nil {
514+
// If BuildIPConfigForV6 returns a value, take its address
515+
ifaceInfo := addResult.interfaceInfo[key]
516+
ifaceInfo.IPConfigs = append(ifaceInfo.IPConfigs, &secIPConfig)
517+
addResult.interfaceInfo[key] = ifaceInfo
518+
}
519+
}
520+
508521
return nil
509522
}
510523

524+
// BuildIPConfigForV6 takes SecondaryIPConfigs and returns an IPConfig.
525+
// Assumes map has at least one element and uses the first one found.
526+
func BuildIPConfigForV6(secondaryIPs map[string]cns.SecondaryIPConfig) (network.IPConfig, error) {
527+
for _, v := range secondaryIPs {
528+
ip, ipNet, err := net.ParseCIDR(v.IPAddress)
529+
if err != nil {
530+
return network.IPConfig{}, fmt.Errorf("invalid IPAddress %q: %w", v.IPAddress, err)
531+
}
532+
if ip.To4() != nil {
533+
return network.IPConfig{}, fmt.Errorf("expected IPv6, got IPv4: %q", v.IPAddress)
534+
}
535+
536+
// Preserve the original address/prefix (often /128) for the endpoint.
537+
addr := *ipNet
538+
539+
// Compute the gateway from the /64 network:
540+
// If the parsed mask is /128, swap to /64 for the base; otherwise if already <= /64, use it.
541+
ones, bits := ipNet.Mask.Size()
542+
gwMask := ipNet.Mask
543+
if ones > 64 { // e.g., /128
544+
gwMask = net.CIDRMask(64, bits)
545+
}
546+
547+
// Base = ip masked with /64
548+
base := ip.Mask(gwMask).To16()
549+
if base == nil {
550+
return network.IPConfig{}, fmt.Errorf("failed to get 16-byte IPv6 for %q", v.IPAddress)
551+
}
552+
553+
// Set gateway to ...:...:...:1 (i.e., last byte = 1)
554+
gw := make(net.IP, len(base))
555+
copy(gw, base)
556+
gw[15] = 0x01 // ::1 within that /64
557+
558+
return network.IPConfig{
559+
Address: addr, // original ipNet (likely /128)
560+
Gateway: gw, // derived from /64 base
561+
}, nil
562+
}
563+
564+
return network.IPConfig{}, fmt.Errorf("map is empty")
565+
}
566+
511567
func addBackendNICToResult(info *IPResultInfo, addResult *IPAMAddResult, key string) error {
512568
macAddress, err := net.ParseMAC(info.macAddress)
513569
if err != nil {

cni/network/network.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,8 @@ func (plugin *NetPlugin) Add(args *cniSkel.CmdArgs) error {
567567
natInfo := getNATInfo(nwCfg, options[network.SNATIPKey], enableSnatForDNS)
568568
networkID, _ := plugin.getNetworkID(args.Netns, &ifInfo, nwCfg)
569569

570+
isIPv6 := ipamAddResult.ipv6Enabled
571+
570572
createEpInfoOpt := createEpInfoOpt{
571573
nwCfg: nwCfg,
572574
cnsNetworkConfig: ifInfo.NCResponse,
@@ -582,7 +584,7 @@ func (plugin *NetPlugin) Add(args *cniSkel.CmdArgs) error {
582584
networkID: networkID,
583585
ifInfo: &ifInfo,
584586
ipamAddConfig: &ipamAddConfig,
585-
ipv6Enabled: ipamAddResult.ipv6Enabled,
587+
ipv6Enabled: isIPv6,
586588
infraSeen: &infraSeen,
587589
endpointIndex: endpointIndex,
588590
}
@@ -1411,11 +1413,15 @@ func convertInterfaceInfoToCniResult(info network.InterfaceInfo, ifName string)
14111413

14121414
if len(info.IPConfigs) > 0 {
14131415
for _, ipconfig := range info.IPConfigs {
1414-
result.IPs = append(result.IPs, &cniTypesCurr.IPConfig{Address: ipconfig.Address, Gateway: ipconfig.Gateway})
1416+
if ipconfig.Address.IP.To4() != nil {
1417+
result.IPs = append(result.IPs, &cniTypesCurr.IPConfig{Address: ipconfig.Address, Gateway: ipconfig.Gateway})
1418+
}
14151419
}
14161420

14171421
for i := range info.Routes {
1418-
result.Routes = append(result.Routes, &cniTypes.Route{Dst: info.Routes[i].Dst, GW: info.Routes[i].Gw})
1422+
if info.Routes[i].Gw.To4() != nil {
1423+
result.Routes = append(result.Routes, &cniTypes.Route{Dst: info.Routes[i].Dst, GW: info.Routes[i].Gw})
1424+
}
14191425
}
14201426
}
14211427

cns/NetworkContainerContract.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ type GetNetworkContainerResponse struct {
503503

504504
type PodIpInfo struct {
505505
PodIPConfig IPSubnet
506+
SecondaryIPConfigs map[string]SecondaryIPConfig // uuid is key
506507
NetworkContainerPrimaryIPConfig IPConfiguration
507508
HostPrimaryIPInfo HostIPInfo
508509
NICType NICType

cns/restserver/ipam.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (service *HTTPRestService) requestIPConfigHandlerHelperStandalone(ctx conte
137137
// IMPORTANT: although SwiftV2 reuses the concept of NCs, NMAgent doesn't program NCs for SwiftV2, but
138138
// instead programs NICs. When getting SwiftV2 NCs, we want the NIC type and MAC address of the NCs.
139139
// TODO: we need another way to verify and sync NMAgent's NIC programming status. pending new NMAgent API or NIC programming status to be passed in the SwiftV2 create NC request.
140-
resp := service.getAllNetworkContainerResponses(cnsRequest) //nolint:contextcheck // not passed in any methods, appease linter
140+
resp, respCreateRequest := service.getAllNetworkContainerResponsesIPv6(cnsRequest) //nolint:contextcheck // not passed in any methods, appease linter
141141
// return err if returned list has no NCs
142142
if len(resp) == 0 {
143143
return &cns.IPConfigsResponse{
@@ -156,6 +156,7 @@ func (service *HTTPRestService) requestIPConfigHandlerHelperStandalone(ctx conte
156156
MacAddress: resp[i].NetworkInterfaceInfo.MACAddress,
157157
NICType: resp[i].NetworkInterfaceInfo.NICType,
158158
NetworkContainerPrimaryIPConfig: resp[i].IPConfiguration,
159+
SecondaryIPConfigs: respCreateRequest[i].SecondaryIPConfigs,
159160
}
160161
podIPInfoList = append(podIPInfoList, podIPInfo)
161162
}

cns/restserver/util.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,172 @@ func (service *HTTPRestService) getAllNetworkContainerResponses(
550550
return getNetworkContainersResponse
551551
}
552552

553+
// Copy of above function as I can't easly change the GetNetworkContainerResponse, too many dependancies
554+
func (service *HTTPRestService) getAllNetworkContainerResponsesIPv6(
555+
req cns.GetNetworkContainerRequest,
556+
) ([]cns.GetNetworkContainerResponse, []cns.CreateNetworkContainerRequest) {
557+
var (
558+
getNetworkContainerResponse cns.GetNetworkContainerResponse
559+
ncs []string
560+
skipNCVersionCheck = false
561+
)
562+
563+
service.Lock()
564+
defer service.Unlock()
565+
566+
switch service.state.OrchestratorType {
567+
case cns.Kubernetes, cns.ServiceFabric, cns.Batch, cns.DBforPostgreSQL, cns.AzureFirstParty:
568+
podInfo, err := cns.UnmarshalPodInfo(req.OrchestratorContext)
569+
getNetworkContainersResponse := []cns.GetNetworkContainerResponse{}
570+
571+
if err != nil {
572+
response := cns.Response{
573+
ReturnCode: types.UnexpectedError,
574+
Message: fmt.Sprintf("Unmarshalling orchestrator context failed with error %v", err),
575+
}
576+
577+
getNetworkContainerResponse.Response = response
578+
getNetworkContainersResponse = append(getNetworkContainersResponse, getNetworkContainerResponse)
579+
return getNetworkContainersResponse, nil
580+
}
581+
582+
// get networkContainerIDs as string, "nc1, nc2"
583+
orchestratorContext := podInfo.Name() + podInfo.Namespace()
584+
if service.state.ContainerIDByOrchestratorContext[orchestratorContext] != nil {
585+
ncs = strings.Split(string(*service.state.ContainerIDByOrchestratorContext[orchestratorContext]), ",")
586+
}
587+
588+
// This indicates that there are no ncs for the given orchestrator context
589+
if len(ncs) == 0 {
590+
response := cns.Response{
591+
ReturnCode: types.UnknownContainerID,
592+
Message: fmt.Sprintf("Failed to find networkContainerID for orchestratorContext %s", orchestratorContext),
593+
}
594+
595+
getNetworkContainerResponse.Response = response
596+
getNetworkContainersResponse = append(getNetworkContainersResponse, getNetworkContainerResponse)
597+
return getNetworkContainersResponse, nil
598+
}
599+
600+
ctx, cancel := context.WithTimeout(context.Background(), nmaAPICallTimeout)
601+
defer cancel()
602+
ncVersionListResp, err := service.nma.GetNCVersionList(ctx)
603+
if err != nil {
604+
skipNCVersionCheck = true
605+
logger.Errorf("failed to get nc version list from nmagent")
606+
// TODO: Add telemetry as this has potential to have containers in the running state w/o datapath working
607+
}
608+
nmaNCs := map[string]string{}
609+
for _, nc := range ncVersionListResp.Containers {
610+
// store nmaNCID as lower case to allow case insensitive comparison with nc stored in CNS
611+
nmaNCs[strings.TrimPrefix(lowerCaseNCGuid(nc.NetworkContainerID), cns.SwiftPrefix)] = nc.Version
612+
}
613+
614+
if !skipNCVersionCheck {
615+
for _, ncid := range ncs {
616+
waitingForUpdate := false
617+
// If the goal state is available with CNS, check if the NC is pending VFP programming
618+
waitingForUpdate, getNetworkContainerResponse.Response.ReturnCode, getNetworkContainerResponse.Response.Message = service.isNCWaitingForUpdate(service.state.ContainerStatus[ncid].CreateNetworkContainerRequest.Version, ncid, nmaNCs) //nolint:lll // bad code
619+
// If the return code is not success, return the error to the caller
620+
if getNetworkContainerResponse.Response.ReturnCode == types.NetworkContainerVfpProgramPending {
621+
logger.Errorf("[Azure-CNS] isNCWaitingForUpdate failed for NCID: %s", ncid)
622+
}
623+
624+
vfpUpdateComplete := !waitingForUpdate
625+
ncstatus := service.state.ContainerStatus[ncid]
626+
// Update the container status if-
627+
// 1. VfpUpdateCompleted successfully
628+
// 2. VfpUpdateComplete changed to false
629+
if (getNetworkContainerResponse.Response.ReturnCode == types.NetworkContainerVfpProgramComplete &&
630+
vfpUpdateComplete && ncstatus.VfpUpdateComplete != vfpUpdateComplete) ||
631+
(!vfpUpdateComplete && ncstatus.VfpUpdateComplete != vfpUpdateComplete) {
632+
logger.Printf("[Azure-CNS] Setting VfpUpdateComplete to %t for NCID: %s", vfpUpdateComplete, ncid)
633+
ncstatus.VfpUpdateComplete = vfpUpdateComplete
634+
service.state.ContainerStatus[ncid] = ncstatus
635+
if err = service.saveState(); err != nil {
636+
logger.Errorf("Failed to save goal states for nc %+v due to %s", getNetworkContainerResponse, err)
637+
}
638+
}
639+
}
640+
}
641+
642+
if service.ChannelMode == cns.Managed {
643+
// If the NC goal state doesn't exist in CNS running in managed mode, call DNC to retrieve the goal state
644+
var (
645+
dncEP = service.GetOption(acn.OptPrivateEndpoint).(string)
646+
infraVnet = service.GetOption(acn.OptInfrastructureNetworkID).(string)
647+
nodeID = service.GetOption(acn.OptNodeID).(string)
648+
)
649+
650+
service.Unlock()
651+
getNetworkContainerResponse.Response.ReturnCode, getNetworkContainerResponse.Response.Message = service.SyncNodeStatus(dncEP, infraVnet, nodeID, req.OrchestratorContext)
652+
service.Lock()
653+
if getNetworkContainerResponse.Response.ReturnCode == types.NotFound {
654+
getNetworkContainersResponse = append(getNetworkContainersResponse, getNetworkContainerResponse)
655+
return getNetworkContainersResponse, nil
656+
}
657+
}
658+
default:
659+
getNetworkContainersResponse := []cns.GetNetworkContainerResponse{}
660+
response := cns.Response{
661+
ReturnCode: types.UnsupportedOrchestratorType,
662+
Message: fmt.Sprintf("Invalid orchestrator type %v", service.state.OrchestratorType),
663+
}
664+
665+
getNetworkContainerResponse.Response = response
666+
getNetworkContainersResponse = append(getNetworkContainersResponse, getNetworkContainerResponse)
667+
return getNetworkContainersResponse, nil
668+
}
669+
670+
getNetworkContainersResponse := []cns.GetNetworkContainerResponse{}
671+
getCreateNetworkContainersRequest := []cns.CreateNetworkContainerRequest{}
672+
673+
for _, ncid := range ncs {
674+
containerStatus := service.state.ContainerStatus
675+
containerDetails, ok := containerStatus[ncid]
676+
if !ok {
677+
response := cns.Response{
678+
ReturnCode: types.UnknownContainerID,
679+
Message: "NetworkContainer doesn't exist.",
680+
}
681+
682+
getNetworkContainerResponse.Response = response
683+
getNetworkContainersResponse = append(getNetworkContainersResponse, getNetworkContainerResponse)
684+
continue
685+
}
686+
687+
savedReq := containerDetails.CreateNetworkContainerRequest
688+
getNetworkContainerResponse = cns.GetNetworkContainerResponse{
689+
NetworkContainerID: savedReq.NetworkContainerid,
690+
IPConfiguration: savedReq.IPConfiguration,
691+
Routes: savedReq.Routes,
692+
CnetAddressSpace: savedReq.CnetAddressSpace,
693+
MultiTenancyInfo: savedReq.MultiTenancyInfo,
694+
PrimaryInterfaceIdentifier: savedReq.PrimaryInterfaceIdentifier,
695+
LocalIPConfiguration: savedReq.LocalIPConfiguration,
696+
AllowHostToNCCommunication: savedReq.AllowHostToNCCommunication,
697+
AllowNCToHostCommunication: savedReq.AllowNCToHostCommunication,
698+
NetworkInterfaceInfo: savedReq.NetworkInterfaceInfo,
699+
}
700+
701+
// If the NC version check wasn't skipped, take into account the VFP programming status when returning the response
702+
if !skipNCVersionCheck {
703+
if !containerDetails.VfpUpdateComplete {
704+
getNetworkContainerResponse.Response = cns.Response{
705+
ReturnCode: types.NetworkContainerVfpProgramPending,
706+
Message: "NetworkContainer VFP programming is pending",
707+
}
708+
}
709+
}
710+
getNetworkContainersResponse = append(getNetworkContainersResponse, getNetworkContainerResponse)
711+
getCreateNetworkContainersRequest = append(getCreateNetworkContainersRequest, savedReq)
712+
}
713+
714+
logger.Printf("getNetworkContainersResponses are %+v", getNetworkContainersResponse)
715+
716+
return getNetworkContainersResponse, getCreateNetworkContainersRequest
717+
}
718+
553719
// restoreNetworkState restores Network state that existed before reboot.
554720
func (service *HTTPRestService) restoreNetworkState() error {
555721
logger.Printf("[Azure CNS] Enter Restoring Network State")

network/endpoint_windows.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,11 @@ func (nw *network) configureHcnEndpoint(epInfo *EndpointInfo) (*hcn.HostComputeE
343343

344344
for _, ipAddress := range epInfo.IPAddresses {
345345
prefixLength, _ := ipAddress.Mask.Size()
346+
347+
if ipAddress.IP.To4() == nil {
348+
prefixLength = 64
349+
}
350+
346351
ipConfiguration := hcn.IpConfig{
347352
IpAddress: ipAddress.IP.String(),
348353
PrefixLength: uint8(prefixLength),

network/network_windows.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package network
66
import (
77
"encoding/json"
88
"fmt"
9+
"net"
910
"strconv"
1011
"strings"
1112
"time"
@@ -289,13 +290,34 @@ func (nm *networkManager) configureHcnNetwork(nwInfo *EndpointInfo, extIf *exter
289290

290291
// Populate subnets.
291292
for _, subnet := range nwInfo.Subnets {
293+
294+
prefix := subnet.Prefix
295+
if prefix.IP.To4() == nil {
296+
// IPv6: normalize to /64
297+
prefix.Mask = net.CIDRMask(64, 128)
298+
prefix.IP = prefix.IP.Mask(prefix.Mask) // zero out host bits
299+
}
300+
prefixStr := prefix.String() // e.g., fd00:da04:74ff:0::/64
301+
302+
// Check if it's IPv6
303+
if subnet.Prefix.IP.To4() == nil {
304+
// IPv6: replace /128 with /64 if present
305+
prefixStr = strings.Replace(prefixStr, "/128", "/64", 1)
306+
}
307+
308+
// Choose route based on IP family
309+
routeDest := defaultRouteCIDR
310+
if subnet.Prefix.IP.To4() == nil {
311+
routeDest = defaultIPv6Route
312+
}
313+
292314
hnsSubnet := hcn.Subnet{
293-
IpAddressPrefix: subnet.Prefix.String(),
315+
IpAddressPrefix: prefixStr,
294316
// Set the Gateway route
295317
Routes: []hcn.Route{
296318
{
297319
NextHop: subnet.Gateway.String(),
298-
DestinationPrefix: defaultRouteCIDR,
320+
DestinationPrefix: routeDest,
299321
},
300322
},
301323
}

0 commit comments

Comments
 (0)