Skip to content

Commit eb80fd0

Browse files
authored
Merge pull request #658 from DataDog/hadrien/1.19/oracle
cilium-cni: Add support for route/rules creation on Kubernetes IPAM
2 parents 26003e8 + 14828af commit eb80fd0

3 files changed

Lines changed: 179 additions & 32 deletions

File tree

daemon/infraendpoints/infra_ip_allocation.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,13 @@ func (r *infraIPAllocator) reallocateRouterIPs(ctx context.Context, family datap
282282
}
283283
}
284284

285-
if (r.daemonConfig.IPAM == ipamOption.IPAMENI ||
286-
r.daemonConfig.IPAM == ipamOption.IPAMAlibabaCloud ||
287-
r.daemonConfig.IPAM == ipamOption.IPAMAzure) && result != nil {
285+
// Configure routing if we have the necessary routing information,
286+
// regardless of IPAM mode. This allows any IPAM mode (including kubernetes)
287+
// to work with multi-VNIC setups by providing routing information.
288+
if result != nil &&
289+
result.GatewayIP != "" &&
290+
result.PrimaryMAC != "" &&
291+
len(result.CIDRs) > 0 {
288292
var routingInfo *linuxrouting.RoutingInfo
289293
routingInfo, err = linuxrouting.NewRoutingInfo(r.logger, result.GatewayIP, result.CIDRs,
290294
result.PrimaryMAC, result.InterfaceNumber, r.daemonConfig.IPAM,
@@ -388,12 +392,12 @@ func (r *infraIPAllocator) allocateHealthIPs(oldV4HealthIP net.IP, oldV6HealthIP
388392

389393
r.logger.Debug("Allocated IPv4 health endpoint address", logfields.IPAddr, result.IP)
390394

391-
// In ENI and AlibabaCloud ENI mode, we require the gateway, CIDRs, and the ENI MAC addr
392-
// in order to set up rules and routes on the local node to direct
393-
// endpoint traffic out of the ENIs.
394-
if r.daemonConfig.IPAM == ipamOption.IPAMENI || r.daemonConfig.IPAM == ipamOption.IPAMAlibabaCloud {
395+
// If routing information is available (gateway, CIDRs, MAC address),
396+
// parse and store it for setting up health endpoint routing rules.
397+
// This works with any IPAM mode that provides routing information.
398+
if result.GatewayIP != "" && result.PrimaryMAC != "" && len(result.CIDRs) > 0 {
395399
if r.healthEndpointRouting, err = r.parseRoutingInfo(result); err != nil {
396-
r.logger.Warn("Unable to allocate health information for ENI", logfields.Error, err)
400+
r.logger.Warn("Unable to parse health endpoint routing information", logfields.Error, err)
397401
}
398402
}
399403
}
@@ -475,12 +479,12 @@ func (r *infraIPAllocator) allocateIngressIPs(oldV4IngressIP net.IP, oldV6Ingres
475479
r.localNodeStore.Update(func(n *node.LocalNode) { n.IPv4IngressIP = result.IP })
476480
r.logger.Debug("Allocated IPv4 Ingress address", logfields.IPAddr, result.IP)
477481

478-
// In ENI and AlibabaCloud ENI mode, we require the gateway, CIDRs, and the
479-
// ENI MAC addr in order to set up rules and routes on the local node to
480-
// direct ingress traffic out of the ENIs.
481-
if r.daemonConfig.IPAM == ipamOption.IPAMENI || r.daemonConfig.IPAM == ipamOption.IPAMAlibabaCloud {
482+
// If routing information is available (gateway, CIDRs, MAC address),
483+
// configure ingress routing rules. This works with any IPAM mode that
484+
// provides routing information.
485+
if result.GatewayIP != "" && result.PrimaryMAC != "" && len(result.CIDRs) > 0 {
482486
if ingressRouting, err := r.parseRoutingInfo(result); err != nil {
483-
r.logger.Warn("Unable to allocate ingress information for ENI", logfields.Error, err)
487+
r.logger.Warn("Unable to parse ingress routing information", logfields.Error, err)
484488
} else {
485489
if err := ingressRouting.Configure(
486490
result.IP,

pkg/ipam/hostscope.go

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,59 @@ import (
77
"fmt"
88
"math/big"
99
"net"
10+
"strconv"
11+
"strings"
1012

13+
"github.com/vishvananda/netlink"
14+
"golang.org/x/sys/unix"
15+
16+
"github.com/cilium/cilium/pkg/datapath/linux/safenetlink"
1117
"github.com/cilium/cilium/pkg/ip"
1218
"github.com/cilium/cilium/pkg/ipam/service/ipallocator"
1319
)
1420

21+
// routingInfo caches the auto-detected routing information for the allocation
22+
// CIDR. nil when no matching interface was found.
23+
type routingInfo struct {
24+
primaryMAC string
25+
interfaceNumber string
26+
cidrs []string
27+
gatewayIP string
28+
}
29+
1530
type hostScopeAllocator struct {
16-
allocCIDR *net.IPNet
17-
allocator *ipallocator.Range
31+
allocCIDR *net.IPNet
32+
allocator *ipallocator.Range
33+
routingInfo *routingInfo
1834
}
1935

2036
func newHostScopeAllocator(n *net.IPNet) Allocator {
21-
return &hostScopeAllocator{
37+
h := &hostScopeAllocator{
2238
allocCIDR: n,
2339
allocator: ipallocator.NewCIDRRange(n),
2440
}
41+
h.routingInfo = detectRoutingInfo(n)
42+
return h
2543
}
2644

2745
func (h *hostScopeAllocator) Allocate(ip net.IP, owner string, pool Pool) (*AllocationResult, error) {
2846
if err := h.allocator.Allocate(ip); err != nil {
2947
return nil, err
3048
}
3149

32-
return &AllocationResult{IP: ip}, nil
50+
result := &AllocationResult{IP: ip}
51+
h.applyRoutingInfo(result)
52+
return result, nil
3353
}
3454

3555
func (h *hostScopeAllocator) AllocateWithoutSyncUpstream(ip net.IP, owner string, pool Pool) (*AllocationResult, error) {
3656
if err := h.allocator.Allocate(ip); err != nil {
3757
return nil, err
3858
}
3959

40-
return &AllocationResult{IP: ip}, nil
60+
result := &AllocationResult{IP: ip}
61+
h.applyRoutingInfo(result)
62+
return result, nil
4163
}
4264

4365
func (h *hostScopeAllocator) Release(ip net.IP, pool Pool) error {
@@ -51,7 +73,9 @@ func (h *hostScopeAllocator) AllocateNext(owner string, pool Pool) (*AllocationR
5173
return nil, err
5274
}
5375

54-
return &AllocationResult{IP: ip}, nil
76+
result := &AllocationResult{IP: ip}
77+
h.applyRoutingInfo(result)
78+
return result, nil
5579
}
5680

5781
func (h *hostScopeAllocator) AllocateNextWithoutSyncUpstream(owner string, pool Pool) (*AllocationResult, error) {
@@ -60,7 +84,9 @@ func (h *hostScopeAllocator) AllocateNextWithoutSyncUpstream(owner string, pool
6084
return nil, err
6185
}
6286

63-
return &AllocationResult{IP: ip}, nil
87+
result := &AllocationResult{IP: ip}
88+
h.applyRoutingInfo(result)
89+
return result, nil
6490
}
6591

6692
func (h *hostScopeAllocator) Dump() (map[Pool]map[string]string, string) {
@@ -95,3 +121,115 @@ func (h *hostScopeAllocator) Capacity() uint64 {
95121

96122
// RestoreFinished marks the status of restoration as done
97123
func (h *hostScopeAllocator) RestoreFinished() {}
124+
125+
// applyRoutingInfo stamps the cached routing information onto an AllocationResult.
126+
func (h *hostScopeAllocator) applyRoutingInfo(result *AllocationResult) {
127+
if h.routingInfo == nil || result == nil {
128+
return
129+
}
130+
result.PrimaryMAC = h.routingInfo.primaryMAC
131+
result.InterfaceNumber = h.routingInfo.interfaceNumber
132+
result.CIDRs = h.routingInfo.cidrs
133+
result.GatewayIP = h.routingInfo.gatewayIP
134+
}
135+
136+
// detectRoutingInfo attempts to auto-detect routing information for the
137+
// allocation CIDR by finding a network interface that has an IP address within
138+
// the same subnet. This enables kubernetes IPAM mode to work with multi-VNIC
139+
// setups (e.g., Oracle Cloud, bare metal) without requiring manual configuration.
140+
// Returns nil when no matching interface is found.
141+
func detectRoutingInfo(allocCIDR *net.IPNet) *routingInfo {
142+
if allocCIDR == nil {
143+
return nil
144+
}
145+
146+
links, err := safenetlink.LinkList()
147+
if err != nil {
148+
return nil
149+
}
150+
151+
for _, link := range links {
152+
// Skip interfaces that are not up and operational
153+
if link.Attrs().OperState != netlink.OperUp &&
154+
link.Attrs().OperState != netlink.OperUnknown {
155+
continue
156+
}
157+
158+
// Skip slave devices (we want the master device)
159+
if link.Attrs().RawFlags&unix.IFF_SLAVE != 0 {
160+
continue
161+
}
162+
163+
// Skip loopback and other special interfaces
164+
if link.Attrs().Flags&net.FlagLoopback != 0 {
165+
continue
166+
}
167+
168+
// Skip Cilium-managed interfaces (cilium_host, cilium_net, lxc*)
169+
if strings.HasPrefix(link.Attrs().Name, "cilium_") ||
170+
strings.HasPrefix(link.Attrs().Name, "lxc") {
171+
continue
172+
}
173+
174+
// Get addresses on this interface
175+
family := netlink.FAMILY_V4
176+
if allocCIDR.IP.To4() == nil {
177+
family = netlink.FAMILY_V6
178+
}
179+
180+
addrs, err := safenetlink.AddrList(link, family)
181+
if err != nil {
182+
continue
183+
}
184+
185+
// Check if any address on this interface is within our allocation CIDR
186+
for _, addr := range addrs {
187+
// The interface's subnet should CONTAIN our allocation CIDR, not the other way around
188+
// This ensures we find the physical interface (e.g., enp1s0 with 100.64.0.0/18)
189+
// rather than virtual interfaces like cilium_host
190+
if addr.IPNet.Contains(allocCIDR.IP) && addr.IPNet.Contains(lastIPInCIDR(allocCIDR)) {
191+
return &routingInfo{
192+
primaryMAC: link.Attrs().HardwareAddr.String(),
193+
interfaceNumber: strconv.Itoa(link.Attrs().Index),
194+
cidrs: []string{addr.IPNet.String()},
195+
gatewayIP: deriveGatewayFromSubnet(addr.IPNet),
196+
}
197+
}
198+
}
199+
}
200+
201+
return nil
202+
}
203+
204+
// lastIPInCIDR returns the last IP address in a CIDR range
205+
func lastIPInCIDR(cidr *net.IPNet) net.IP {
206+
ip := make(net.IP, len(cidr.IP))
207+
copy(ip, cidr.IP)
208+
for i := range ip {
209+
ip[i] |= ^cidr.Mask[i]
210+
}
211+
return ip
212+
}
213+
214+
// deriveGatewayFromSubnet derives the gateway IP from a subnet by using the first
215+
// usable IP address in the subnet (typically x.x.x.1 for IPv4).
216+
func deriveGatewayFromSubnet(subnet *net.IPNet) string {
217+
if subnet == nil {
218+
return ""
219+
}
220+
221+
// Get the network address
222+
ip := subnet.IP.Mask(subnet.Mask)
223+
224+
if ip.To4() != nil {
225+
// For IPv4, use the first address in the subnet (x.x.x.1)
226+
ip = ip.To4()
227+
ip[3] = 1
228+
return ip.String()
229+
} else {
230+
// For IPv6, use the first address in the subnet (ending in ::1)
231+
ip = ip.To16()
232+
ip[15] = 1
233+
return ip.String()
234+
}
235+
}

plugins/cilium-cni/cmd/cmd.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -775,19 +775,17 @@ func (cmd *Cmd) Add(args *skel.CmdArgs) (err error) {
775775
res.Routes = append(res.Routes, routes...)
776776
}
777777

778-
if needsEndpointRoutingOnHost(conf) {
779-
if ipam.IPV4 != nil && ipConfig != nil {
780-
err = interfaceAdd(scopedLogger, ipConfig, ipam.IPV4, conf)
781-
if err != nil {
782-
return fmt.Errorf("unable to setup interface datapath: %w", err)
783-
}
778+
if needsEndpointRoutingOnHost(conf, ipam.IPV4, ipConfig) {
779+
err = interfaceAdd(scopedLogger, ipConfig, ipam.IPV4, conf)
780+
if err != nil {
781+
return fmt.Errorf("unable to setup interface datapath: %w", err)
784782
}
783+
}
785784

786-
if ipam.IPV6 != nil && ipv6Config != nil {
787-
err = interfaceAdd(scopedLogger, ipv6Config, ipam.IPV6, conf)
788-
if err != nil {
789-
return fmt.Errorf("unable to setup interface datapath: %w", err)
790-
}
785+
if needsEndpointRoutingOnHost(conf, ipam.IPV6, ipv6Config) {
786+
err = interfaceAdd(scopedLogger, ipv6Config, ipam.IPV6, conf)
787+
if err != nil {
788+
return fmt.Errorf("unable to setup interface datapath: %w", err)
791789
}
792790
}
793791

@@ -1337,12 +1335,19 @@ func buildLogAttrsWithCNIArgs(logger *slog.Logger, cniArgs *types.ArgsSpec) *slo
13371335
// on host for the Pod. This is needed for following IPAM modes:
13381336
// - Cloud ENI IPAM modes.
13391337
// - DelegatedPlugin mode with InstallUplinkRoutesForDelegatedIPAM set to true.
1340-
func needsEndpointRoutingOnHost(conf *models.DaemonConfigurationStatus) bool {
1338+
// - Some cases where we use Kubernetes IPAM with multiple NICs (eg. Oracle Cloud)
1339+
func needsEndpointRoutingOnHost(conf *models.DaemonConfigurationStatus, ipam *models.IPAMAddressResponse, ipConfig *cniTypesV1.IPConfig) bool {
1340+
if ipam == nil || ipConfig == nil {
1341+
return false
1342+
}
1343+
13411344
switch conf.IpamMode {
13421345
case ipamOption.IPAMENI, ipamOption.IPAMAzure, ipamOption.IPAMAlibabaCloud:
13431346
return true
13441347
case ipamOption.IPAMDelegatedPlugin:
13451348
return conf.InstallUplinkRoutesForDelegatedIPAM
1349+
case ipamOption.IPAMKubernetes:
1350+
return ipam.Gateway != "" && ipam.MasterMac != ""
13461351
}
13471352
return false
13481353
}

0 commit comments

Comments
 (0)