Skip to content

macvlan, ipvlan, host-device and cluster-wide IPAM

S1·E5Whose ticket it actually was · A customer bridge call at 02:40, day nineteen, the network team already on the line

S1·E5Analyze~30 minsources checked todaylab mutates hardwareverified against cni.dev macvlan / ipvlan / host-device plugin pages, k8snetworkplumbingwg/whereabouts README and Mellanox/nvidia-k8s-ipam README all re-fetched 2026-09-09; NVIDIA Network Operator v26.7.0 quick-start use-case list re-fetched 2026-09-09; deployment guide, Spectrum-X quick start, Kubernetes device-plugin and k8s-rdma-shared-dev-plugin pages as fetched 2026-09-07

Builds on: Multus and the NetworkAttachmentDefinition, MTU budgeting across switch, host PF and pod

Before you read: what do you already know?

3 quick questions. Wrong answers are fine and expected; trying first makes the lesson stick.

After this lesson you can

  • Distinguish macvlan, ipvlan and host-device by what each one gives the pod and what it takes away.
  • Predict which of the three secondary CNIs a given customer requirement rules out, and cite the documented rule that rules it out.
  • Explain why `host-local` IPAM produces duplicate addresses across nodes and choose between Whereabouts and nv-ipam for a cluster-wide fix.
  • Map an nv-ipam `IPPool` or `CIDRPool` to the per-node ranges it actually produces.
  • Give a customer the one-pod-per-NIC versus several-pods-per-NIC versus IP-level-only decision tree without over-promising RDMA.

Episode 5 — Whose ticket it actually was

The situation · A customer bridge call at 02:40, day nineteen, the network team already on the line

Two training pods on different nodes hold the same address on the rail subnet. The network team has spent two hours proving the leaves are clean, and they have proved it: the counters are boring, which is the network lead’s favourite kind of evidence. The operator who found it has his labels in front of him; they all match. Nobody has read the NAD, because the NAD is four lines and was copied from a working example.

Those four lines are the bug. The example used host-local, and Whereabouts exists precisely because “host-local only knows how to assign IPs to pods on the same node”; people “assume it’ll work across nodes – and then wind up with IP address collisions”.[4] On the primary CNI that is invisible, because each node owns a disjoint pod CIDR. On a secondary network where eight nodes draw from one flat subnet, it is a latent outage whose symptom points at the wrong team.

That is the second half of the fast path: choosing the attachment is only half the design, and choosing the address source is the half that fails weeks later. The attachments have their own documented rules to respect - macvlan “functions like a switch that is already connected to the host interface”, ipvlan “devices all share the same MAC”, and host-device simply moves the requested device into the container.[1][2][3]

An interface is a capacity decision; an address is a bookkeeping decision. They fail on different days.

Start with what each plugin gives the pod, and what it quietly takes away.

1macvlan: a switch already connected to the host interface

The macvlan plugin describes itself in one sentence that is worth memorising, because it predicts most of its behaviour: it “functions like a switch that is already connected to the host interface”.[1] The host interface is enslaved, and each virtual interface shares the physical device while keeping a distinct MAC address - which is why existing DHCP servers keep working against macvlan children.[1]

Required fields are name, type set to "macvlan", and ipam, which may be an empty dictionary if you want an interface with no address at all.[1] The optional fields are the ones that matter in a lab: master (defaults to the default-route interface, which is almost never what you want on a GPU node), mode, mtu, and linkInContainer.[1]

Two documented facts carry weight later. First, mtu has an explicit range - zero to the master’s MTU - so a pod interface can never be raised above the PF it hangs off.[1] That is the hard cap from the MTU lesson, restated as a plugin rule rather than as arithmetic. Second, one master interface cannot be enslaved by macvlan and ipvlan at the same time, so the two plugins are mutually exclusive per PF, not per cluster.[1] Wireless cards mostly cannot be enslaved at all, which matters only as a reminder that “enslavement” is a driver capability and not a guarantee.[1]

9000 B
The three MTUs that must agree — switch port, host PF, pod interfaceSwitch portleaf port9000 BHost PFip link set <pf> mtu9000 BPod eth0Calico · VXLAN v48950 B50Pod net1net1 on the master PF9000 B
2 warnings
Pod eth0

Pod eth0 — 8950 B

900050 = 8950. network MTU − 50. Cilium documents the same 50 bytes per packet for VXLAN, which is why jumbo frames pay for themselves on either CNI.

Calico: You set it. Calico does not measure the fabric for you.

  • Rule, verbatim: “Set the workload endpoint MTU and the tunnel MTUs to the same value (so all paths have the same MTU).”
  • Manifest installs change it through the calico-config ConfigMap key veth_mtu, then the DaemonSet is restarted.
  • Verify the tunnel with ip link show; the IP-in-IP tunnel appears as tunlx.
  • Policy-only mode (CALICO_NETWORKING_BACKEND=none) is the clean answer when a different fabric owns routing — then there is no tunnel to budget for.

Workload MTU and tunnel MTU are both 8950 — Calico wants them identical so every path has the same MTU.

  • ⚠ macvlan mtu has range 0 to the master's MTU. A NAD asking mtu: 9216 on a 9000-MTU master is out of range — the pod does not get a bigger MTU than the PF, ever.
  • ⚠ Classic bug shape: a 9000 fabric with the pod network left at 1500. NCCL's TCP bootstrap still works and the data path crawls, so the ticket reads "training is slow", never "MTU is wrong".
  • Pod eth0 (8950) and pod net1 (9000) differ by 50 bytes. That is correct — the secondary path skips the tunnel — but net1 must still equal the PF and the switch port.
Set it and verify it
kubectl patch configmap/calico-config -n kube-system --type merge -p '{"data":{"veth_mtu": "8950"}}'
kubectl rollout restart daemonset/calico-node -n kube-system   # the doc says: restart the DaemonSet
ip link show   # the tunnel device MTU must read 8950
kubectl exec -it <pod> -- ip link   # eth0 must read 8950, net1 must read 9000
# NAD delegate:  {"type": "macvlan", "master": "<pf>", "mtu": 9000, "ipam": {...}}
Set the fabric to 9000 with a macvlan secondary and read the warning: a NAD asking for the 9216 rail MTU is out of range against a 9000 master, because macvlan's mtu has a documented range of 0 to the master's MTU. Raise the fabric to 9216 and watch the warning disappear - that is the host change that has to come first.

2ipvlan: same wire, one MAC, two limits people trip over

ipvlan is the same idea with one variable changed. The documentation states it directly: “Like its cousin macvlan, it virtualizes the host interface. However unlike macvlan which generates a new MAC address for each interface, ipvlan devices all share the same MAC.”[2] Fields are master, mode (one of "l2", "l3", "l3s", defaulting to "l2"), mtu (defaulting to the kernel-chosen value rather than to the master’s), ipam, and linkInContainer.[2]

The shared MAC buys you a switch that does not have to learn a new address per pod. It costs you two things, both documented, both the source of tickets.

The first is DHCP: “Because all ipvlan interfaces share the MAC address with the host interface, DHCP can only be used in conjunction with ClientID (currently not supported by DHCP plugin).”[2] In practice that means static or cluster-wide IPAM, which is the next segment anyway.

The second is the one that surprises senior people. A container cannot reach its own host through the ipvlan interface - the plugin page says so plainly and tells you to attach another network, such as ptp, if host communication is needed.[2] Anything that assumes a pod can curl a node-local agent, a node exporter or a health endpoint over the fast interface will fail on ipvlan and work on macvlan, for a reason that has nothing to do with either being broken.

3host-device: moving the whole thing, once

host-device is the bluntest of the three: “This simple plugin will move the requested device from the host’s network namespace to the container’s.”[3] That single sentence contains the entire capacity model. The device is moved, not shared, so the node has exactly as many host-device attachments as it has devices, and the second pod that asks for the same one does not get a degraded interface - it gets nothing.

Four fields select the device: device (a name such as eth0), hwaddr (a MAC), kernelpath (for example /sys/devices/pci0000:00/0000:00:1f.6) and pciBusID (for example 0000:00:1f.6).[3] On top of those it supports the deviceID runtime capability, which takes a PCI address - that is the hook a device plugin uses to hand host-device a specific NIC at allocation time rather than having it hard-coded in the NAD.[3]

Three behaviours to know before you write the lab. CNI_IFNAME is disregarded, so the interface arrives in the pod under its own name and any manifest that expects net1 is wrong.[3] On CNI DEL the device is returned to the host’s network namespace, which is what makes this reversible and therefore lab-safe.[3] And when the device is bound to a DPDK driver - selected through pciBusID or the deviceID runtime argument - IPAM configuration is bypassed entirely, because there is no kernel interface left to address.[3]

In a Network Operator deployment you rarely write this JSON by hand. The HostDeviceNetwork CR takes networkNamespace, resourceName and ipam and generates the NAD, and the matching pod requests nvidia.com/hostdev: 1 in both requests and limits alongside the IPC_LOCK capability.[6] That resource request is what enforces the one-pod-per-device rule at scheduling time, because extended resources are integer-only, non-overcommittable, and cannot be shared between containers.[8]

4Cluster-wide IPAM: why host-local is a latent outage

The default IPAM plugin most people copy from an example is host-local, and it keeps its allocation state on the node it runs on - Whereabouts exists precisely because “host-local only knows how to assign IPs to pods on the same node”.[4] On the primary CNI that is fine, because each node owns a disjoint pod CIDR. On a secondary network where every node draws from the same flat subnet, it is a defect: Whereabouts’ own README notes that people “assume it’ll work across nodes – and then wind up with IP address collisions”, and the resulting duplicate-address symptoms on a rail fabric look exactly like a switch problem, so the ticket arrives at the wrong team.[4]

Whereabouts is “An IP Address Management (IPAM) CNI plugin that assigns IP addresses cluster-wide.”[4] Required fields are type set to whereabouts and range.[4] Optional fields cover the shapes a real subnet needs: range_start and range_end, exclude (a list of CIDRs to skip), network_name (independent allocation for two networks that use the same CIDR), gateway, routes, enable_overlapping_ranges which defaults to true, and node_slice_size, which turns on the experimental Fast IPAM mode by pre-allocating a slice per node.[4]

{
  "cniVersion": "0.3.0",
  "name": "whereaboutsexample",
  "type": "macvlan",
  "master": "eth0",
  "ipam": {
    "type": "whereabouts",
    "range": "192.168.2.225/28",
    "exclude": ["192.168.2.229/30"]
  }
}

[4] State lives in two CRs - IPPool stores the allocated addresses and OverlappingRangeIPReservation tracks assignments across ranges - so you can read the allocation table with kubectl instead of guessing.[4] Two known issues are worth quoting to a customer before they find them: system crashes may leave stranded allocations, which on an unstable cluster can exhaust the range over weeks; and a wide IPv6 CIDR of /64 or larger is only addressable within its first /65 because of a uint64 offset constraint.[4]

nv-ipam is NVIDIA’s answer and has three parts: ipam-controller, a controller that watches IPPool and CIDRPool CRs in a predefined namespace and assigns each node a cluster-unique range through CR status; ipam-node, a node daemon that installs the CNI binary, performs allocations and persists them to disk behind a gRPC service; and nv-ipam, the CNI binary that calls that daemon.[5] The CNI config takes poolName (comma-separated for dual-stack), poolType (ippool or cidrpool, case-insensitive), daemonSocket, daemonCallTimeoutSeconds and logLevel.[5]

The two pool kinds allocate differently and confusing them is a real mistake:

field 192.168.0.0/16 gives node1 gives node2
IPPool perNodeBlockSize: 24 192.168.0.1 - 192.168.0.24 192.168.0.25 - 192.168.0.48
CIDRPool perNodeNetworkPrefix: 24 192.168.0.0/24 192.168.1.0/24

[5] IPPool also takes subnet, gateway, exclusions, perNodeExclusions, defaultGateway and routes; CIDRPool takes cidr, gatewayIndex, exclusions, perNodeExclusions, staticAllocations, defaultGateway and routes.[5] NVIDIA’s Spectrum-X rail configuration uses CIDRPool with perNodeNetworkPrefix: 31 and gatewayIndex: 0 - one /31 per node per rail, which is the smallest thing that still has a gateway.[10] Network Operator deploys nv-ipam itself through NicClusterPolicy.spec.nvIpam, so on an operator-managed cluster it is already versioned and patched with everything else.[6]

primary
delegate
ipam
options
pod ns: default · securityContext IPC_LOCKeth0primary CNInet1ipvlan-netMultus CNI (meta-plugin)thick: multus-daemon + multus-shimCalicoowns eth0 + policyipvlanIP-levelrdmachained, after sriovrdma/rdma_shared_device_arequested by the podhost-localaddress assignmenthost: ens1f0 · VF 0000:03:02.3 · mlx5_core

Checks

1 error
  • error{"type":"rdma"} cannot be chained after ipvlan. rdma-cni requires SR-IOV capable hardware and an SR-IOV supporting CNI, and it must come after the sriov / ib-sriov plugin in the plugins array. Over ipvlan, RDMA exists only through the shared device plugin (rdma/rdma_shared_device_a). [rdma-cni prerequisites + chained-plugin order]
  • warnhost-local allocates per node: pods on different nodes get the same address out of the same subnet, and on a rail fabric that reads as a switch fault. Whereabouts and nv-ipam are the two cluster-wide answers. [cluster-wide IPAM]
  • warn"ipvlan does not allow virtual interfaces to communicate with the master interface. Therefore the container will not be able to reach the host via ipvlan interface." Shared MAC also means DHCP needs ClientID, which the DHCP plugin does not support. [ipvlan limitations]
  • infoCalico owns eth0 only. Confirm it has not autodetected ens1f0 as its uplink: sharing one interface with the fast path works until Felix reconfigures it, and then RDMA disappears. [primary CNI must not claim the fast-path interface]
selected block

Delegate — ipvlan

Like macvlan, but every ipvlan device shares the master’s MAC address.

  • Fields: master, mode ("l2" default, "l3", "l3s"), mtu (kernel-chosen by default), ipam, linkInContainer.
  • "ipvlan does not allow virtual interfaces to communicate with the master interface. Therefore the container will not be able to reach the host via ipvlan interface."
  • Because all ipvlan interfaces share the host MAC, DHCP works only with ClientID — currently not supported by the DHCP plugin.
  • Operator route: No operator CR — hand-written NAD.

FAE angle: the host-unreachable rule surprises people who put a node-local health-check endpoint behind the secondary interface. It is documented behaviour, not a driver bug.

source
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: ipvlan-net
  namespace: default
spec:
  config: |
    {
      "cniVersion": "0.3.1",
      "name": "ipvlan-net",
      "plugins": [
        {
          "type": "ipvlan",
          "master": "ens1f0",
          "mode": "l2",
          "ipam": {
            "type": "host-local",
            "subnet": "10.56.217.0/24"
          }
        },
        {
          "type": "rdma"
        }
      ]
    }
Operator route: No operator CR — hand-written NAD. Network Operator deployment guide · Multus how-to-use · quick start
This preset is deliberately wrong in two ways at once. Read both validation errors, then fix them one at a time - switch the IPAM to nv-ipam first and note that the second error does not go away, because it is about the delegate and not the addressing.

5The decision tree you actually give a customer

The customer question is never “compare these three CNI plugins”. It is “which one for GPUDirect”, and the answer has to survive being repeated by someone else in a meeting you are not in. Three lines:

  • host-device moves the whole PF into the pod - full line rate, full RDMA, and exactly one pod per NIC per node.[3][8]
  • SR-IOV slices the PF into virtual functions, so several pods share one NIC with isolation enforced in hardware.[7]
  • macvlan and ipvlan give an IP-level secondary interface, and RDMA over them is available only through the shared RDMA device plugin, which over-advertises one HCA as a counted scheduling resource rather than isolating it per pod.[9]

NVIDIA’s own quick-start use-case list is organised along exactly this seam, which is useful because you can point a customer at their vendor’s page rather than at your opinion: “SR-IOV Network with RDMA” and “Host Device Network with RDMA” on one side, “IP over InfiniBand with RDMA Shared Device” and “MacVLAN Network with RDMA Shared Device” on the other, with “SR-IOV InfiniBand Network with RDMA” for the IB case.[7] The words “RDMA Shared Device” in two of those five titles are doing all the work.

Pick the delegate and the IPAM from a requirement list

Ask: six inference pods per node, each needing an address on the 192.168.2.0/24 storage subnet. The leaf enforces one MAC per server port. No RDMA required today. Two nodes now, twelve by the end of the quarter.

  1. How many pods per node need the interface? Six. That rules out host-device immediately - it moves the device, so the ceiling is one pod per NIC.[3]
  2. Is hardware isolation required? No, this is IP reachability to storage. SR-IOV is available but not required, and it costs VF configuration and a device plugin. Park it.
  3. macvlan or ipvlan? The leaf enforces one MAC per port. macvlan gives each pod a distinct MAC and would violate that policy at pod seven.[1] So ipvlan, whose interfaces share the host MAC.[2]
  4. Check what ipvlan costs here. DHCP is out, because the shared MAC needs ClientID and the DHCP plugin does not support it.[2] And nothing in these pods may curl a node-local endpoint over this interface, because ipvlan cannot reach its own master.[2] Confirm both with the application team before proceeding; if either is a hard requirement, go back to step 3 and negotiate the MAC policy instead.
  5. Choose the IPAM. Twelve nodes drawing from one flat /24 means host-local will duplicate. Cluster-wide is mandatory. There is no Network Operator on this cluster yet, so whereabouts is the lighter answer.[4]
  6. Bound the range and leave room. Use range with range_start and range_end, and exclude the switch VRRP addresses.[4]
  7. Write it:
    {"cniVersion":"0.3.1","type":"ipvlan","master":"ens1f0","mode":"l2",
     "ipam":{"type":"whereabouts","range":"192.168.2.0/24",
             "range_start":"192.168.2.100","range_end":"192.168.2.200",
             "exclude":["192.168.2.1/32"]}}
    [2] [4]
  8. Verify: kubectl get ippools.whereabouts.cni.cncf.io -A shows one pool with six allocations per node and no address appearing twice.[4]

Case closed

How it ended

The leaves were never it. host-local allocated from the same range on every node in ignorance of the others, and the collision surfaced only when two pods landed on the wrong pair.[4] Whereabouts turns the range into one cluster-wide record; nv-ipam does the same job on a cluster that already runs Network Operator.[5] On day twenty-six the acceptance run finishes inside the window: three MTUs that agree, an annotation that resolves, one address per pod, and a rail the primary CNI never touches. Procurement gets its lead time, the network lead closes the notebook at page eleven, and row 14 of the SE’s spreadsheet stops being a promise and becomes a measurement.

Lab

Mutating steps ahead. Needs a maintenance window, out-of-band access (BMC/iDRAC/rshim console) and a rollback path. Record the pre-flight inventory before changing anything. Never on a production host.

Goal: on the Dell BF-3 host in NIC mode, demonstrate host-device’s capacity model directly - one pod takes the PF, the second stays Pending - and confirm the device comes back to the host on delete. This lab moves a real interface out of the host namespace, so it is mutating and every step names its rollback.

  1. Pre-flight inventory. Capture this to a file; step 6 diffs against it:
    ip -br link | tee /tmp/pre-link.txt
    ip -br addr | tee /tmp/pre-addr.txt
    ethtool -i ens1f1 | head -3
    lspci -D | grep -i mellanox
    kubectl get net-attach-def -A
    kubectl describe node | grep -A20 Allocatable
    Expected: the PF you intend to use is present, driver mlx5_core, and you have its PCI address in 0000:xx:00.y form.[3]
  2. Choose a PF that is not carrying anything. Confirm it is not the primary CNI’s uplink (lesson 1’s check) and not the management interface. host-device removes the interface from the host, so doing this to the wrong PF drops your SSH session. If the host has only one usable PF, do not run this lab on it.
  3. Create the host-device NAD naming the PF by pciBusID:
    kubectl apply -f - <<'EOF'
    apiVersion: k8s.cni.cncf.io/v1
    kind: NetworkAttachmentDefinition
    metadata: { name: hd-net }
    spec:
      config: '{"cniVersion":"0.3.1","type":"host-device","pciBusID":"0000:3d:00.1"}'
    EOF
    [3] Rollback: kubectl delete net-attach-def hd-net.
  4. Launch one pod against it, then a second identical pod:
    kubectl run hd-a --image=nicolaka/netshoot --restart=Never \
      --annotations="k8s.v1.cni.cncf.io/networks=hd-net" -- sleep 3600
    kubectl exec hd-a -- ip -br link
    ip -br link | grep -c ens1f1   # run on the host
    Expected: the pod has the interface under its own host name, not net1, because CNI_IFNAME is disregarded; and the host no longer lists it.[3] Rollback: kubectl delete pod hd-a - and confirm with ip -br link on the host that the interface returned, which is the documented CNI DEL behaviour.[3]
  5. With hd-a still running, start hd-b the same way. Expected on this NAD as written: the second pod schedules and then fails at CNI ADD, because nothing counted the device. Record the verbatim event. Then note what would have been different with a device plugin advertising nvidia.com/hostdev and the pod requesting it: the second pod would have stayed Pending at scheduling with Insufficient nvidia.com/hostdev and never reached the CNI layer, because extended resources are integer-only and non-shareable.[8][6] That difference - fail at attach versus refuse at schedule - is the whole argument for the device plugin.[3]
  6. Restore and verify. Delete both pods, delete the NAD, then:
    ip -br link | diff /tmp/pre-link.txt - && echo "links restored"
    ip -br addr | diff /tmp/pre-addr.txt - && echo "addresses restored"
    Expected: both diffs clean. If the interface came back without its addresses, re-apply them from /tmp/pre-addr.txt; host-device returns the device, not its configuration.
  7. Deliverable: two verbatim outputs side by side - the in-pod ip -br link from step 4 and the host’s ip -br link for the same moment - plus the event text from step 5. Annotate them with the one sentence from the plugin page that predicts each.

Rollback for the whole lab: kubectl delete pod hd-a hd-b --ignore-not-found, kubectl delete net-attach-def hd-net, then step 6’s diffs must be clean before you walk away from the host.

Retrieval check

10 questions from memory. Answer before looking anything up; misses become flashcards.

Explain it to a Dell SE

Explain to a Dell SE, in five sentences, how you would choose between macvlan, host-device and SR-IOV for a customer's second interface, and why the addressing question is a separate decision that people keep forgetting.

14 flashcards for this lesson — 0 in deck. Spaced review lives at /review.

Sources

Facts in this lesson were checked against cni.dev macvlan / ipvlan / host-device plugin pages, k8snetworkplumbingwg/whereabouts README and Mellanox/nvidia-k8s-ipam README all re-fetched 2026-09-09; NVIDIA Network Operator v26.7.0 quick-start use-case list re-fetched 2026-09-09; deployment guide, Spectrum-X quick start, Kubernetes device-plugin and k8s-rdma-shared-dev-plugin pages as fetched 2026-09-07. Dates are when each page was fetched.

  1. CNI plugins - macvlan · fetched 2026-09-09
  2. CNI plugins - ipvlan · fetched 2026-09-09
  3. CNI plugins - host-device · fetched 2026-09-09
  4. GitHub - k8snetworkplumbingwg/whereabouts · fetched 2026-09-09
  5. GitHub - Mellanox/nvidia-k8s-ipam (nv-ipam) · fetched 2026-09-09
  6. NVIDIA Network Operator v26.7.0 - Deployment Guide with Kubernetes · fetched 2026-09-07
  7. NVIDIA Network Operator v26.7.0 - Quick Start Guide for Kubernetes · fetched 2026-09-09
  8. Kubernetes - Device Plugins · fetched 2026-09-07
  9. GitHub - Mellanox/k8s-rdma-shared-dev-plugin · fetched 2026-09-07
  10. NVIDIA Network Operator v26.7.0 - Spectrum-X Quick Start · fetched 2026-09-07

The same idea elsewhere

Other lessons that cover this ground, sometimes from another course's angle.