Skip to content

feat: implement IPVLAN Subinterface configuration and provision - #241

Open
ngcxy wants to merge 5 commits into
kubernetes-sigs:mainfrom
ngcxy:ipvlan-config
Open

feat: implement IPVLAN Subinterface configuration and provision#241
ngcxy wants to merge 5 commits into
kubernetes-sigs:mainfrom
ngcxy:ipvlan-config

Conversation

@ngcxy

@ngcxy ngcxy commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

/kind feature

What this PR does / why we need it:

This PR introduces support for subinterface provisioning in the DRANET driver, currently implementing IPVLAN as the subinterface type. The feature addresses two networking requirements:

  • Support RDMA NIC sharing #63: It provides the implementing foundation for sharing a physical interface among multiple pods by allowing multiple subinterfaces to be created on a single NIC.
  • Support LACP bonded network interfaces #239: It satisfies the requirement for LACP-bonded interfaces by creating a new subinterface inside the pod network namespace without moving the host’s parent interface.

The key changes include:

  • Add SubInterfaceConfig and IPVlanConfig to NetworkConfig, including IPRanges — a list of allocation ranges, each expressed as a CIDR or an explicit start/end pair.
  • During ResourceClaim preparation, assign the subinterface name, allocate its IP(s), and configure source-based routing. User-configured routes/rules are preserved — automatic source-based routing is skipped when the user already specified them.
  • Add a dedicated pkg/ipam package with a concurrency-safe node-local LocalIPAM that owns the set of in-use addresses and hands out one address per IP family from the configured ranges; allocated IPs are released on claim teardown.
  • Integrate subinterface create/delete (IPVLAN) inside the pod network namespace via the NRI run/stop sandbox hooks.
  • Derive the subinterface range from GCE VM metadata in GetDeviceConfig, using a shared, cloud-agnostic cloudutil.IPRangeFromCIDR helper (network and broadcast always excluded; providers specify any additional reserved counts).

Which issue(s) this PR is related to:

Fixes #63, #239

Special notes for your reviewer:

The code was tested by requesting a subinterface in ResourceClaim. The pod runs successfully with the IPVlan interface (connectivity verified via ping test), and the parent interface remains in the host’s namespace.
ResourceClaimTemplate yaml example:

apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: one-ipvlan
spec:
  spec:
    devices:
      requests:
      - name: one-ipvlan
        exactly:
          deviceClassName: netdev.google.com
          allocationMode: ExactCount
          count: 1
      config:
      - opaque:
          driver: dra.net
          parameters:
            interface:
              subInterface:
                type: "ipvlan"

Does this PR introduce a user-facing change?

Enable the support for IPVlan subinterfaces provisioning on parent NICs, configured with dynamic IP allocation and source-based routing which support both IPv4 and IPv6.
Subinterface creation can be triggered either by the cloud provider or by a user request in a ResourceClaim.
`Type` (only `ipvlan` is supported for now) is required to enable subinterface creation, and `IPRanges` should be provided when no generic IPAM is enabled.

@kubernetes-prow kubernetes-prow Bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. kind/feature Categorizes issue or PR as related to a new feature. labels Jun 26, 2026
@netlify

netlify Bot commented Jun 26, 2026

Copy link
Copy Markdown

Deploy Preview for dranet ready!

Name Link
🔨 Latest commit 9517532
🔍 Latest deploy log https://app.netlify.com/projects/dranet/deploys/6a68e965f666080008446841
😎 Deploy Preview https://deploy-preview-241--dranet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@kubernetes-prow kubernetes-prow Bot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Jun 26, 2026
@kubernetes-prow

Copy link
Copy Markdown
Contributor

Hi @ngcxy. Thanks for your PR.

I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Tip

We noticed you've done this a few times! Consider joining the org to skip this step and gain /lgtm and other bot rights. We recommend asking approvers on your previous PRs to sponsor you.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@kubernetes-prow kubernetes-prow Bot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Jun 26, 2026
@aojea
aojea requested review from aojea and Copilot June 26, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This draft PR adds IPVLAN-based subinterface support to the DRANET driver to enable sharing a single NIC across multiple pods, including allocation-time naming/IPAM, netns plumbing for subinterface creation/teardown, and GCE-derived subinterface ranges.

Changes:

  • Added SubInterface API types/defaults/validation and integrated subinterface-aware behavior into DRA + NRI hooks.
  • Introduced a LocalIPAM allocator and PodConfigStore tracking to allocate addresses from subinterface ranges.
  • Implemented GCE-specific subinterface IPRange derivation (IPv6 /80 from node /64) plus bare-metal routing/neighbor config.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
pkg/driver/subinterfaces.go Creates/deletes IPVLAN subinterfaces in the container netns and reports NetworkDeviceData.
pkg/driver/pod_device_config.go Adds in-memory allocated-IP tracking to back LocalIPAM allocation decisions.
pkg/driver/nri_hooks.go Switches Run/Stop hooks to create/delete subinterfaces when configured; factors common netns config.
pkg/driver/local_ipam.go New local sequential allocator for IP selection from a CIDR range.
pkg/driver/driver.go Wires LocalIPAM into driver startup.
pkg/driver/dra_hooks.go Adds subinterface-aware naming + optional IP assignment from IPRange + source-based rule insertion.
pkg/cloudprovider/gce/gce.go Adds GCE-derived subinterface IPRange plus bare-metal route/neighbor configuration.
pkg/cloudprovider/gce/gce_test.go Adds tests for the new GCE device-config behavior and IPv6 range derivation.
pkg/apis/types.go Introduces SubInterfaceConfig, SubInterfaceType, and IPVlanConfig API types.
pkg/apis/constants.go Adds the SubInterfaceTypeIPVlan constant.
pkg/apis/defaults.go Defaults SubInterface type/mode/flag.
pkg/apis/validation.go Validates SubInterface and IPVLAN config and IPRange format.
pkg/apis/validation_test.go Adds test cases for subinterface validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/driver/dra_hooks.go Outdated
Comment thread pkg/driver/pod_device_config.go Outdated
Comment thread pkg/driver/local_ipam.go Outdated
Comment thread pkg/cloudprovider/gce/gce.go Outdated
Comment thread pkg/driver/subinterfaces.go
Comment thread pkg/driver/nri_hooks.go Outdated
Comment thread pkg/driver/local_ipam.go Outdated
@ngcxy
ngcxy force-pushed the ipvlan-config branch 3 times, most recently from e7bc2df to 7dc7879 Compare June 26, 2026 21:18
@kubernetes-prow kubernetes-prow Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jun 30, 2026
@ngcxy ngcxy changed the title feat: implement IPVLAN Subinterface congiuring and provisioning feat: implement IPVLAN Subinterface configuring and provisioning Jun 30, 2026
@tamilmani1989

Copy link
Copy Markdown
Contributor

Does this consider multi nic scenario? There can be multiple shared nics in host.. in that case, it might required to have separate vrf for each nic..

@ngcxy

ngcxy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Does this consider multi nic scenario? There can be multiple shared nics in host.. in that case, it might required to have separate vrf for each nic..

Hi @tamilmani1989 , could you elaborate more on the requirement of separate vrf? This feature will cover the multi-NIC scenario as more generic use cases, but within the current change, the vrf part modification isn't considered. Would like to hear your suggestion on this.

Comment thread pkg/apis/config_merge.go Outdated
Comment thread pkg/apis/types.go Outdated
Type SubInterfaceType `json:"type,omitempty"`

// IPRange is the range to allocate IP addresses for the subinterface
IPRange string `json:"ipRange,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No way to inherit routes/neighbors from the parent interface. The GCE provider hardcodes a virtual gateway MAC, but bond gateways are dynamic (NDP-learned), not available in cloud metadata. Consider copyRoutesFromParent / copyNeighborsFromParent fields so the driver can read from the parent at runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated addSourceBasedRouting in dra_hooks to dynamically detects the gateway from the parent, and create routes based on it. The permanent neighbors are directly copied from the host during prepareResourceClaim.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work!

addSourceBasedRouting clears user-configured routes, which silently discards any routes/rules the user specified in the ResourceClaim. Users can and do write custom static routes.

Should this skip auto source-based routing when the user already configured routes/rules, rather than overwriting them?

@ngcxy ngcxy Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Updated to keep user config routes and skip source-based routing in dra hooks.

@ngcxy ngcxy changed the title feat: implement IPVLAN Subinterface configuring and provisioning feat: implement IPVLAN Subinterface configuration and provision Jul 7, 2026
@kubernetes-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: ngcxy
Once this PR has been reviewed and has the lgtm label, please ask for approval from gauravkghildiyal. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@gauravkghildiyal gauravkghildiyal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ngcxy. Leaving some feedback for mainly the 2nd commit. Will revisit the others later.

Comment thread pkg/driver/local_ipam.go Outdated
Comment thread pkg/driver/pod_device_config.go Outdated
Comment thread pkg/driver/pod_device_config.go Outdated
Comment thread pkg/driver/pod_device_config.go Outdated
Comment thread pkg/driver/pod_device_config.go Outdated
Comment thread pkg/driver/local_ipam.go Outdated
Comment thread pkg/driver/local_ipam.go Outdated
Comment thread pkg/driver/local_ipam.go Outdated
Comment thread pkg/driver/local_ipam_test.go Outdated
Comment thread pkg/driver/local_ipam.go Outdated
@kubernetes-prow kubernetes-prow Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 16, 2026
@tamilmani1989

Copy link
Copy Markdown
Contributor

Does this consider multi nic scenario? There can be multiple shared nics in host.. in that case, it might required to have separate vrf for each nic..

Hi @tamilmani1989 , could you elaborate more on the requirement of separate vrf? This feature will cover the multi-NIC scenario as more generic use cases, but within the current change, the vrf part modification isn't considered. Would like to hear your suggestion on this.

if multiple nic share same address space, then it might need vrf to route traffic via appropriate interface

ngcxy added 5 commits July 28, 2026 09:03
* Add SubInterfaceConfig, IPVlanConfig, and IPRangeConfig definitions to the NetworkConfig schema.
* Support a list of IP ranges per subinterface, each expressed either as a CIDR block or as an explicit start/end address pair.
* Define default configuration parameters and merging rules for subinterfaces, with user-provided values taking precedence and duplicates removed.
* Implement validation logic for subinterface properties (IP ranges, CIDR formatting, start/end ordering, and IPVLAN modes/flags) along with unit tests.
* Implement LocalIPAM to allocate addresses from the configured IP ranges (CIDR or explicit start/end), returning one address per IP family chosen at random within the range bounds, while tracking the set of in-use addresses to prevent conflicts.
* Roll back partial allocations on error, and add Release to return an address to the pool.
* Add PodConfigStore.GetAllocatedIPs to collect the addresses recorded in the stored device configs, and seed LocalIPAM with them at NetworkDriver startup so previously assigned IPs are not reissued.
* Add unit tests verifying IP allocation boundaries, randomized selection, conflict prevention, and lifecycle cleanups.
…laim hooks

* Extend the prepareResourceClaim hook to process subinterface specs:
  * Assign interface name with the network type prefix.
  * Use LocalIPAM to automatically allocate IPs from the configured ranges when no static address is specified.
  * Implement source-based routing to generate custom-table routes and rules for the subinterface, when no custom routes and rules are configured.
* Release allocated subinterface IPs in unprepareResourceClaim so addresses are returned to the pool on claim teardown.
* Add unit tests validating IP allocation and source-based routing configuration for routes and rules.
* Plumb subinterface setup (createSubinterfaceInNS) and cleanup (nsDeleteSubinterface) into NRI runPodSandbox and stopPodSandbox lifecycle hooks.
  * Refactor common network configuration tasks (VRF, routes, neighbors, and rules) into a shared configureNetdevInNS helper.
* Implement netlink utilities nsCreateSubinterface and nsDeleteSubinterface with support for IPVLAN creation (L2 mode, bridge flag), setting up link state, and configuring IPs.
* Add integration tests (TestSubinterface_IPVlan) to verify end-to-end IPVLAN creation, MAC/MTU inheritance, IP assignment, and cleanup inside test network namespaces.
* Implement GetDeviceConfig to lookup GCE VM network interface details matching the device MAC address.
* Add a shared cloudutil.IPRangeFromCIDR helper that derives explicit [start, end] allocation bounds from a CIDR, always excluding the network and broadcast addresses and supporting additional reserved counts at each end.
* For IPv4, derive the range from the primary GCE Alias IP Range.
* For IPv6, compute the range by appending a 16-bit marker (0xC0DE) to the parent interface's base prefix.
* Add unit tests for the range helper, GCE device configuration lookups, and IPv6 prefix derivation.
@kubernetes-prow kubernetes-prow Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 28, 2026
Comment thread pkg/driver/nri_hooks.go
if config.NetworkInterfaceConfigInPod.SubInterface != nil {
subIfName := config.NetworkInterfaceConfigInPod.SubInterface.Name
if err := nsDeleteSubinterface(ns, subIfName); err != nil {
klog.Errorf("fail to delete subinterface %s for device %s: %v", subIfName, deviceName, err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use same logger like line 343.

Comment thread pkg/ipam/local_ipam.go
return netip.MustParseAddr(cfg.StartIP), netip.MustParseAddr(cfg.EndIP), nil
}

// Mode 2: derive boundaries from CIDR.

@Bowser1704 Bowser1704 Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this exclusion applies to IPv6 too?

@Bowser1704

Copy link
Copy Markdown
Contributor

LGTM. I prototyped Aliyun's LACP-bond use case (issue #239) on top of this PR, and it only took about 90 lines in our cloud provider — zero changes to pkg/driver.

@gauravkghildiyal gauravkghildiyal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another round focusing mostly on API.

}{
{
name: "valid subinterface config",
cfg: &SubInterfaceConfig{Type: "ipvlan", IPRanges: []IPRangeConfig{{CIDR: "10.24.3.0/24"}, {StartIP: "10.24.4.10", EndIP: "10.24.4.20"}}, IPVlan: &IPVlanConfig{Mode: "l2", Flag: "bridge"}},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readability: Please format the config across multiple lines.

cfg IPRangeConfig
wantErr bool
}{
{name: "CIDR only", cfg: IPRangeConfig{CIDR: "192.168.1.0/24"}},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readability: It's acceptable to have simpler things be in one line, but I think all these configs together seem to warrant being spread over multiple lines.

{name: "IPv6 CIDR only", cfg: IPRangeConfig{CIDR: "2001:db8::/64"}},
{name: "start and end only", cfg: IPRangeConfig{StartIP: "10.0.0.5", EndIP: "10.0.0.10"}},
{name: "start == end", cfg: IPRangeConfig{StartIP: "10.0.0.5", EndIP: "10.0.0.5"}},
{name: "all three, within cidr", cfg: IPRangeConfig{CIDR: "10.0.0.0/24", StartIP: "10.0.0.5", EndIP: "10.0.0.10"}},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would be permit this use case of allowing both a CIDR and a Start+End together?

}
}

func TestValidateSubInterfaceConfig(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our validation tests should also take defaulting into account.

Comment thread pkg/apis/config_merge.go
// overrides the cloud provider config. For slices, the two configurations are combined,
// but duplicates are resolved in favor of the user config.
// but duplicates are resolved in favor of the user config. For subinterface IPRanges,
// user-provided ranges are ordered before cloud-provided ranges before deduplication.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason to have this divergence in behaviour for IPRanges? If not, let's have consistent behaviour

Comment thread pkg/apis/types.go

// Name is the desired logical name of the subinterface inside the Pod.
// If not specified, it will be derived by adding a type prefix to the parent interface name.
// e.g. for ipvlan type, Name will be "ipvlan-<parent_interface_name>"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interface names have a 15 char limit. Adding something like "-ipvlan" consumes 7 characters itself and it's possible that the "<parent_name>-ipvlan" may easily exceed the 15 char limit.

I would say let's not add any prefix/suffix at all and keep the original name as the default. We can leave the option of choosing a custom name

Comment thread pkg/apis/types.go
// SubInterface defines the properties of the subinterfaces created on the network interface.
// When specified, new subinterfaces will be created in the pod namespace based
// on this config, while the original interface stays in the host namespace.
SubInterface *SubInterfaceConfig `json:"subInterface,omitempty"`

@gauravkghildiyal gauravkghildiyal Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been giving this some more thought and I think merging this together with the existing Interface should be possible and avoid a lot of duplication.

Any thoughts on roughly something like this?

// 1. Remove SubInterface from NetworkConfig
type NetworkConfig struct {
	Profile   string          `json:"profile,omitempty"`
	Interface InterfaceConfig `json:"interface"`
	
	// REMOVED: SubInterface *SubInterfaceConfig `json:"subInterface,omitempty"`
	
	Routes    []RouteConfig   `json:"routes,omitempty"`
	Rules     []RuleConfig    `json:"rules,omitempty"`
	// ... (Neighbors, Ethtool)
}

// 2. Add the 3 new fields to your existing InterfaceConfig
type InterfaceConfig struct {
	// ADDED: Type determines how the interface is provided to the Pod.
	// "passthrough" (default) or "ipvlan"
	Type InterfaceType `json:"type,omitempty"`
	
	// ADDED: IPVlan holds IPVLAN-specific settings; only evaluated when Type is "ipvlan".
	IPVlan *IPVlanConfig `json:"ipvlan,omitempty"`

	// ... (Keep existing fields: Name, Addresses, MTU, VRF, etc.) ...

	// ADDED: IPRanges allows node-local IPAM to generate addresses for this interface.
	IPRanges []IPRangeConfig `json:"ipRanges,omitempty"`
}

// 3. Rename SubInterfaceType to InterfaceType
type InterfaceType string

const (
	InterfaceTypePassthrough InterfaceType = "passthrough"
	InterfaceTypeIPVlan      InterfaceType = "ipvlan"
)

// 4. Keep your existing IPVlanConfig (no changes needed here)
type IPVlanConfig struct {
	Mode string `json:"mode,omitempty"`
	Flag string `json:"flag,omitempty"`
}

There's some good general guidance on API design here https://github.com/kubernetes/community/blob/main/contributors/devel/sig-architecture/api-conventions.md#automatic-resource-allocation-and-deallocation, including things like why we should define things like an InterfaceType

Comment thread pkg/driver/dra_hooks.go
Comment on lines +465 to +478
// If not specified, assign an IP address from the configured IP ranges using node local IPAM.
if len(deviceCfg.NetworkInterfaceConfigInPod.SubInterface.Addresses) == 0 {
ipRanges := deviceCfg.NetworkInterfaceConfigInPod.SubInterface.IPRanges
if len(ipRanges) == 0 {
errorList = append(errorList, fmt.Errorf("can't assign IP for subinterface %s, no IPRanges specified", ifName))
continue
}
if np.localIPAM == nil {
errorList = append(errorList, fmt.Errorf("can't assign IP for subinterface %s, IPAM database not initialized", ifName))
continue
}
// Allocate at most one IP per IP family from the configured ranges.
addresses, err := np.localIPAM.Allocate(ipRanges)
if err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PrepareResourceClaims only calls LocalIPAM.Allocate() when SubInterface.Addresses is empty. When a user statically specifies Addresses instead of IPRanges, that address is used as-is and never registered into LocalIPAM.allocatedIPs.

This means IPAM has no visibility into statically-assigned addresses during the current driver session. If another claim's IPRanges happens to overlap with a manually-specified static address, Allocate() has no way to know it's taken and can hand it out to a different pod — a real collision, not just a hypothetical one.

(After a driver restart, GetAllocatedIPs() reseeds LocalIPAM from every persisted SubInterface.Addresses, static or not — pod_device_config.go:337 — so static addresses become "known" post-restart.)

Suggested direction: make address resolution go through one path regardless of source, instead of two paths that only sometimes touch shared state.

  • If the config specifies Addresses, LocalIPAM should still record/reserve them (and can validate they don't collide with something already allocated), instead of skipping IPAM entirely.
  • If the config specifies IPRanges, LocalIPAM fills in Addresses from the pool, exactly as it does today.

Either way, once Addresses is resolved, everything downstream (addIPVlan, addSourceBasedRouting) is already source-agnostic — it just iterates whatever ended up in Addresses. So this isn't a call to rewrite the consuming side; it's specifically about making the resolution step (static vs. IPAM-derived) go through the same bookkeeping, so LocalIPAM is actually the single source of truth for "what's in use" rather than only knowing about addresses it personally handed out.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not make IPAM part of the interface API, https://github.com/kubernetes-sigs/dranet/pull/241/changes#r3710308724

An Interface or Subinterface define its attributes, IPAM is not an interface attribute, that process has to happen before we get here and populate the existing Addresses field, so each cloud provider can decide how they do IPAM

Comment thread pkg/apis/types.go
Comment on lines +136 to +140
// IPRanges is a list of IP address ranges from which the node-local IPAM
// generates IP addresses for the subinterface. It may be provided by the cloud
// provider in GetDeviceConfig and/or by the user. Exactly one IP address is
// generated per IP family present in the list.
IPRanges []IPRangeConfig `json:"ipRanges,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have Addresses why do we want to leak IPRanges here? Are we not able to do the IPAM without leaking it into the API? we are already doing it for the ones that are not subinterfaces

Comment thread pkg/apis/types.go
Comment on lines +147 to +169
// IPRangeConfig describes an allocatable IP address range for node-local IPAM.
//
// A range can be specified in one of two ways:
// 1. Explicit boundaries: both StartIP and EndIP are set (CIDR may be omitted).
// The driver validates that both are valid IP addresses of the same family
// and that StartIP <= EndIP.
// 2. CIDR: only CIDR is set (StartIP and EndIP omitted). The driver validates
// the CIDR and derives the allocatable boundaries from it, spanning every
// address except the network (base) address and the broadcast (last) address.
//
// If all three fields are set, StartIP/EndIP take priority (when valid) and CIDR
// is used only to sanity-check that the boundaries fall within it.
type IPRangeConfig struct {
// CIDR is the network range in CIDR notation.
// It is optional when both StartIP and EndIP are provided.
CIDR string `json:"cidr,omitempty"`
// StartIP is the first IP address that can be allocated from the range.
// If empty, the driver derives it from CIDR as the first address after the network address.
StartIP string `json:"startIP,omitempty"`
// EndIP is the last IP address that can be allocated from the range.
// If empty, the driver derives it from CIDR as the last address before the broadcast address.
EndIP string `json:"endIP,omitempty"`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really want to avoid leaking IPAM config in the API ... this is about subinterfaces properties, IPAM is network configuration

@aojea

aojea commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

/assign @aojea

let's discuss the ipam addition and why it can not be part of the provider logic

@aojea

aojea commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

/hold

Unless there's some additional limitation I can't see from the current diff, I don't see the need for a second way of handling IPAM here that's different from what we already do for the primary interface.
Profile + ProfileProvider.GetProfileConfig/ReleaseProfileConfig already gives any provider a stateful, per-claim hook to allocate/release addresses and hand back Addresses.

Adding IPRanges/IPRangeConfig to SubInterfaceConfig with a driver-owned LocalIPAM invoked directly from dra_hooks.go, reintroduces IPAM as a first-class API/driver that is something that goes against the philosophy of the project, the API represents Interface properties, not Interface configuration properties , see more in opencontainers/runtime-spec#1271 (comment) about how the design of the container networking netdvice is specified. If there's a concrete requirement that GetProfileConfig can't achieve, let's make it clear, but I honestly don't see one yet.

GetDeviceConfig is for static, cacheable, per-device metadata — called once, merged unconditionally, regardless of which claim ends up using the device, is the declarative state of the Network Interface. GetProfileConfig is the per-claim, stateful hook — the one that's supposed to actually allocate and provide dynamic configuration. In this PR, GCE's GetDeviceConfig hands back IPRanges (fine as device metadata) but then the actual per-claim address allocation happens generically inside driver, against that field, instead of inside GetProfileConfig. That's what's requiring IPRanges to be leaked in the API and the extra merge/dedup logic in config_merge.go.

I think all the use cases can be solved just with SubInterface.Addresses:

  1. Cloud provider with its own address pool (GCE, and AWS/Azure/Alibaba later)
config:
- opaque:
    driver: dra.net
    parameters:
      profile: gce-subinterface   # any non-empty value triggers GetProfileConfig
      interface:
        subInterface:
          type: ipvlan
// pkg/cloudprovider/gce/gce.go
type GCEInstance struct {
    // ...
    subIPAM *ipam.LocalIPAM // internal allocator; never (de)serialized
}

func (g *GCEInstance) GetProfileConfig(id cloudprovider.DeviceIdentifiers, claimUID types.UID, config *apis.NetworkConfig) (*apis.NetworkConfig, error) {
    if config.SubInterface == nil || config.SubInterface.Type == "" {
        return nil, nil // not a subinterface request
    }
    ranges, err := g.subInterfaceRangesFor(id) // derived from instance metadata, stays internal
    if err != nil {
        return nil, err
    }
    addrs, err := g.subIPAM.Allocate(ranges)
    if err != nil {
        return nil, err
    }
    return &apis.NetworkConfig{SubInterface: &apis.SubInterfaceConfig{Addresses: addrs}}, nil
}

func (g *GCEInstance) ReleaseProfileConfig(id cloudprovider.DeviceIdentifiers, claimUID types.UID, config *apis.NetworkConfig) error {
    if config.SubInterface == nil {
        return nil
    }
    for _, a := range config.SubInterface.Addresses {
        g.subIPAM.Release(a)
    }
    return nil
}

`pkg/ipam.LocalIPAM (this PR's allocator) is still 100% reusable here as a library.

  1. User wants a static address, no IPAM involved
parameters:
  interface:
    subInterface:
      type: ipvlan
      addresses: ["192.168.1.50/24"]

No profile field set, no provider round-trip — identical to how static addresses already work for the primary interface today.

  1. Bare metal / no cloud provider: for Support RDMA NIC sharing #63 and Support LACP bonded network interfaces #239

This is where "should we ship a local-IPAM profile webhook" matters, and the answer is: we already ship this with cmd/webhook-whereabouts and returns Interface.Addresses. It's already wired end-to-end today via --profile-provider=webhook --webhook-url=....

So I can see two ways to reuse that pattern instead of inventing a new one:

(a) Extend existing webhook-whereabouts to also populate SubInterface.Addresses when req.Config.SubInterface != nil that is very simple.

(b) Create a new cmd/webhook-local-ipam built directly on this PR's pkg/ipam.LocalIPAM library, for operators who don't want the CNI/whereabouts dependency:

/ cmd/webhook-local-ipam/main.go (sketch)
type Server struct {
    ipam   *ipam.LocalIPAM
    ranges []ipam.Range // from a flag/ConfigMap, e.g. --ranges=10.24.3.0/24,2001:db8::/64
}

func (s *Server) GetProfileConfig(w http.ResponseWriter, r *http.Request) {
    var req webhook.ProfileRequest
    json.NewDecoder(r.Body).Decode(&req)
    if req.Config == nil || req.Config.SubInterface == nil {
        json.NewEncoder(w).Encode(apis.NetworkConfig{})
        return
    }
    addrs, err := s.ipam.Allocate(s.ranges)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(apis.NetworkConfig{SubInterface: &apis.SubInterfaceConfig{Addresses: addrs}})
}

func (s *Server) ReleaseProfileConfig(w http.ResponseWriter, r *http.Request) {
    var req webhook.ProfileRequest
    json.NewDecoder(r.Body).Decode(&req)
    if req.Config != nil && req.Config.SubInterface != nil {
        for _, a := range req.Config.SubInterface.Addresses {
            s.ipam.Release(a)
        }
    }
    w.WriteHeader(http.StatusOK)
}
  1. Unnumbered / L2-only subinterface — no profile, no addresses; dranet just creates the ipvlan link with no source-based routing.

Summary

Scenario profile set? addresses in claim? Who allocates New pkg/apis surface needed
Cloud-provider pool (GCE/AWS/Azure) yes no provider's GetProfileConfig none
Static user IP no yes user none
Bare-metal pool, simple UX yes no webhook-whereabouts (extended) or new webhook-local-ipam none
Unnumbered / L2 only no no n/a none

In every case Addresses is the only thing that ever crosses the API boundary, exactly like the primary interface.

Can we keep pkg/ipam.LocalIPAM as a library consumed by provider implementations (in-tree cloud providers or a standalone webhook), instead of adding IPRanges to SubInterfaceConfig and running allocation inside driver?

@kubernetes-prow kubernetes-prow Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Aug 5, 2026
@kubernetes-prow

Copy link
Copy Markdown
Contributor

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@kubernetes-prow kubernetes-prow Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Aug 5, 2026
@ngcxy

ngcxy commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @aojea, this is really helpful and reframes the whole API comprehension for me. I will refactor the API to drop the config-oriented IPRanges and let Cloud Provider to produce the static Addresses, avoiding driver-side configuration logic for the device.

I see three ways as the next plan, in increasing scope:

Option 1 (minimum change)
Relocate the invocation of LocalIPAM into cloud provider specific profile config. Keep the current interaction GetAllocatedIPs with PodConfigStore persistent storage for initial allocated IPs seeding during the startup.

Option 2 (separate persistent storage)
Besides the relocation, also remove GetAllocatedIPs that connects between LocalIPAM and driver's PodConfigStore, creating a dedicated persistent storage for ipam state to fully detach from the driver.

Option 3 (webhook for bare metal)
Exactly your point 3(b), a standalone webhook built on LocalIPAM, reusing the webhook-whereabouts pattern. The webhook will require its own checkpoint for persistent storage as well.

I believe for the current PR option 1 is sufficient to unblock, unless such interaction with persistent storage is not preferred. For option 3, I can follow up in future work for bare metal support.

Please let me know if this plan sounds right to you.

@aojea

aojea commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Option 1 sounds reasonable to me, once the IPs are allocated we assume they are part of the Interface until they are released

@OguzPastirmaci

Copy link
Copy Markdown
Contributor

Very excited about this work. This will help us at OCI avoid moving RDMA PFs.

I used this PR as the base for OKE tests with Nvidia IPAM. IPv4 worked with IPvlan children.

IPv6 also worked, but our fabric uses SLAAC. The Nvidia IPAM address was not used for the data path. Support for an IPvlan child without an explicit address would make the IPv6 configuration much simpler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. kind/feature Categorizes issue or PR as related to a new feature. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. ok-to-test Indicates a non-member PR verified by an org member that is safe to test. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support RDMA NIC sharing

7 participants