@@ -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+
1530type hostScopeAllocator struct {
16- allocCIDR * net.IPNet
17- allocator * ipallocator.Range
31+ allocCIDR * net.IPNet
32+ allocator * ipallocator.Range
33+ routingInfo * routingInfo
1834}
1935
2036func 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
2745func (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
3555func (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
4365func (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
5781func (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
6692func (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
97123func (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+ }
0 commit comments