#19193·kubevirt

Masquerade binding provides an IPv6 address to the guest but no default IPv6 route (no RA), unlike IPv4

Author: martindekovCreated Sep 21, 2026Updated Sep 21, 2026
Labelskind/bug

What happened: For a VMI using the default masquerade: {} pod-network interface in a dual-stack cluster, the guest gets an IPv6 address inside the virtual masquerade subnet, but is never given a default IPv6 route to reach anything outside that subnet. This includes the ability to reply to inbound traffic (e.g. ICMPv6 echo) whose destination was DNAT'd to the guest.

For IPv4, masquerade binding's virtual DHCP server hands out both an address and a default gateway (10.0.2.1), so the guest works out of the box. For IPv6, only an address can be obtained (and even that requires the guest to run an unconditional DHCPv6 client - see caveat below, this may be a guest-OS/systemd-networkd characteristic rather than KubeVirt's), and nothing ever configures fd10:0:2::1 as the guest's default IPv6 route.

What you expected to happen: Masquerade binding should provide IPv6 guests a default route the same way it provides one for IPv4 - either via Router Advertisement (with a default lifetime pointing at fd10:0:2::1) or some equivalent mechanism, so IPv6 masquerade "just works" symmetrically with IPv4.

How to reproduce it (as minimally and precisely as possible):

  1. Create a VMI on a dual-stack cluster with a masquerade: {} default network interface (no other special config needed).
  2. Inside the guest, once it has an IPv6 address on the pod-network interface (e.g. via a manually forced dhclient -6, since no RA arrives to trigger it automatically):
    ip -6 route show
    Note there is no default ... dev <pod-iface> route present, in contrast to the IPv4 side which does have default via 10.0.2.1.
  3. From outside the pod (another node, or another pod), try to reach the VM's real pod IPv6 address (the one in k8s.v1.cni.cncf.io/network-status / status.interfaces[].ipAddresses, not the internal masquerade address):
    ping6 <pod-ipv6-address>
    This fails (100% loss), even though:
    • the DNAT rule in the pod netns's ip6 nat table correctly fires and rewrites the destination to the guest's internal address (confirmed via packet counters),
    • net.ipv6.conf.all.forwarding=1 in the pod netns,
    • a route to the guest's internal /120 exists via the tap device,
    • the guest correctly answers Neighbor Discovery for its internal address (proving it's alive and L2/L3-reachable),
    • tcpdump on the guest's own interface confirms the echo request arrives, but the guest never emits a reply - because it has no route back to the external caller's address.
  4. Manually add a default route on the guest (ip -6 route add default via fd10:0:2::1 dev <pod-iface> onlink) and repeat step 3 - it now succeeds, confirming the missing default route is the entire cause.

Evidence

ip6 nat table inside the virt-launcher pod's netns (via nsenter -n nft list ruleset), showing DNAT firing correctly but the guest never sourcing a masqueraded reply until the manual route was added:

table ip6 nat {
        chain prerouting {
                iifname "eth0" counter packets 6 bytes 528 jump KUBEVIRT_PREINBOUND
        }
        chain postrouting {
                ip6 saddr fd10:0:2::2 counter packets 0 bytes 0 masquerade
                oifname "k6t-eth0" counter packets 8 bytes 824 jump KUBEVIRT_POSTINBOUND
        }
        chain KUBEVIRT_PREINBOUND {
                counter packets 6 bytes 528 dnat to fd10:0:2::2
        }
}

(6 packets DNAT'd inbound, 0 packets ever masqueraded outbound. The reply is never generated because the guest has no route for it.)

Guest route table before the fix, showing IPv4 has a default route on the pod-network interface but IPv6 does not:

default via 10.0.2.1 dev enp1s0             (IPv4 - present)
# (no equivalent IPv6 default route on enp1s0 at all)

Additional context: Even after manually adding the route, if the guest has other interfaces with lower-metric RA-installed default routes (e.g. secondary VLAN-attached NICs), the manually-added masquerade default route can lose the routing decision for return traffic and get silently misrouted out the wrong interface. Worth deciding whether any eventual fix (e.g. RA-based default route) should use a metric that reliably wins for this subnet's traffic, or whether more specific routing should be used instead of relying on default-route priority.

Environment:

  • KubeVirt version (use virtctl version):
node-1:/home/rancher # virtctl version
Client Version: version.Info{GitVersion:"v1.7.0", GitCommit:"v1.7.0", GitTreeState:"clean", BuildDate:"2025-11-22T23:34:02Z", GoVersion:"go1.27.1", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{GitVersion:"v1.8.4", GitCommit:"v1.8.4", GitTreeState:"clean", BuildDate:"2026-07-18T21:49:59Z", GoVersion:"go1.26.6-X:nodwarf5", Compiler:"gc", Platform:"linux/amd64"}
  • Kubernetes version (use kubectl version):
node-1:/home/rancher # kubectl version
Client Version: v1.36.4+rke2r1
Kustomize Version: v5.8.1
Server Version: v1.36.4+rke2r1
  • VM or VMI specifications:
Initial VM
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  annotations:
    harvesterhci.io/mac-address: '{"default":"aa:2a:d0:73:47:52","nic-1":"ca:d6:68:21:96:08","nic-2":"da:52:9c:48:02:af"}'
    harvesterhci.io/vmRunStrategy: RerunOnFailure
    harvesterhci.io/volumeClaimTemplates: '[{"metadata":{"name":"vm-2-disk-0-9pb3s","annotations":{"harvesterhci.io/imageId":"default/image-r95k2"}},"spec":{"accessModes":["ReadWriteMany"],"resources":{"requests":{"storage":"10Gi"}},"volumeMode":"Block","storageClassName":"lh-1a108c43-0eb0-470f-b00b-4ee11a8c8e24"}}]'
    kubevirt.io/latest-observed-api-version: v1
    kubevirt.io/storage-observed-api-version: v1
    network.harvesterhci.io/ips: '[]'
  creationTimestamp: "2026-09-21T13:02:04Z"
  finalizers:
  - kubevirt.io/virtualMachineControllerFinalize
  - wrangler.cattle.io/harvester-lb-vm-controller
  - wrangler.cattle.io/VMController.CleanupPVCAndSnapshot
  - wrangler.cattle.io/VMController.ReconcileBackendStorageClone
  generation: 1
  labels:
    harvesterhci.io/creator: harvester
    harvesterhci.io/os: linux
  name: vm-2
  namespace: default
  resourceVersion: "226096"
  uid: 3b935a25-7400-4eef-babe-b99c5f71bd42
spec:
  runStrategy: RerunOnFailure
  template:
    metadata:
      annotations:
        harvesterhci.io/sshNames: '[]'
        kubevirt.io/pci-topology-version: v3
      labels:
        harvesterhci.io/vmName: vm-2
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: network.harvesterhci.io/mgmt
                operator: In
                values:
                - "true"
      architecture: amd64
      domain:
        cpu:
          cores: 2
          maxSockets: 1
          sockets: 1
          threads: 1
        devices:
          disks:
          - bootOrder: 1
            disk:
              bus: virtio
            name: disk-0
          - disk:
              bus: virtio
            name: cloudinitdisk
          inputs:
          - bus: usb
            name: tablet
            type: tablet
          interfaces:
          - macAddress: aa:2a:d0:73:47:52
            masquerade: {}
            model: virtio
            name: default
          - bridge: {}
            macAddress: ca:d6:68:21:96:08
            model: virtio
            name: nic-1
          - bridge: {}
            macAddress: da:52:9c:48:02:af
            model: virtio
            name: nic-2
        features:
          acpi:
            enabled: true
        firmware:
          serial: b4441a50-7e4e-44b5-91df-16ff4fafe156
          uuid: 67b5cfab-70dc-4293-908a-ec8bed8d0091
        machine:
          type: q35
        memory:
          guest: 4Gi
        resources:
          limits:
            cpu: "2"
            memory: 4Gi
          requests:
            cpu: 200m
            memory: 2730Mi
      evictionStrategy: LiveMigrateIfPossible
      hostname: vm-2
      networks:
      - name: default
        pod: {}
      - multus:
          networkName: harvester-system/rwx-network-x5g6q
        name: nic-1
      - multus:
          networkName: harvester-system/vm-migration-network-jgmdb
        name: nic-2
      terminationGracePeriodSeconds: 120
      volumes:
      - name: disk-0
        persistentVolumeClaim:
          claimName: vm-2-disk-0-9pb3s
      - cloudInitNoCloud:
          networkDataSecretRef:
            name: vm-2-k08gb
          secretRef:
            name: vm-2-k08gb
        name: cloudinitdisk
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2026-09-21T13:02:25Z"
    status: "True"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: null
    status: "True"
    type: LiveMigratable
  - lastProbeTime: null
    lastTransitionTime: null
    status: "True"
    type: StorageLiveMigratable
  - lastProbeTime: "2026-09-21T13:04:34Z"
    lastTransitionTime: null
    status: "True"
    type: AgentConnected
  created: true
  desiredGeneration: 1
  observedGeneration: 1
  printableStatus: Running
  ready: true
  runStrategy: RerunOnFailure
  volumeSnapshotStatuses:
  - enabled: true
    name: disk-0
  - enabled: false
    name: cloudinitdisk
    reason: Snapshot is not supported for this volumeSource type [cloudinitdisk]
Initial VMI
apiVersion: kubevirt.io/v1
kind: VirtualMachineInstance
metadata:
  annotations:
    harvesterhci.io/sshNames: '[]'
    kubevirt.io/latest-observed-api-version: v1
    kubevirt.io/pci-topology-version: v3
    kubevirt.io/storage-observed-api-version: v1
    kubevirt.io/vm-generation: "1"
  creationTimestamp: "2026-09-21T13:02:04Z"
  finalizers:
  - kubevirt.io/virtualMachineControllerFinalize
  - kubevirt.io/foregroundDeleteVirtualMachine
  - wrangler.cattle.io/VMController.BackfillObservedNetworkMacAddress
  - wrangler.cattle.io/harvester-lb-vmi-controller
  generation: 12
  labels:
    harvesterhci.io/vmName: vm-2
    kubevirt.io/nodeName: node-2
  name: vm-2
  namespace: default
  ownerReferences:
  - apiVersion: kubevirt.io/v1
    blockOwnerDeletion: true
    controller: true
    kind: VirtualMachine
    name: vm-2
    uid: 3b935a25-7400-4eef-babe-b99c5f71bd42
  resourceVersion: "226097"
  uid: 5b147d76-bf79-4d4f-95e1-b2281238c1c2
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: network.harvesterhci.io/mgmt
            operator: In
            values:
            - "true"
  architecture: amd64
  domain:
    cpu:
      cores: 2
      maxSockets: 1
      model: host-model
      sockets: 1
      threads: 1
    devices:
      disks:
      - bootOrder: 1
        disk:
          bus: virtio
        name: disk-0
      - disk:
          bus: virtio
        name: cloudinitdisk
      inputs:
      - bus: usb
        name: tablet
        type: tablet
      interfaces:
      - macAddress: aa:2a:d0:73:47:52
        masquerade: {}
        model: virtio
        name: default
      - bridge: {}
        macAddress: ca:d6:68:21:96:08
        model: virtio
        name: nic-1
      - bridge: {}
        macAddress: da:52:9c:48:02:af
        model: virtio
        name: nic-2
    features:
      acpi:
        enabled: true
    firmware:
      serial: b4441a50-7e4e-44b5-91df-16ff4fafe156
      uuid: 67b5cfab-70dc-4293-908a-ec8bed8d0091
    machine:
      type: q35
    memory:
      guest: 4Gi
      maxGuest: 16Gi
    resources:
      limits:
        cpu: "2"
        memory: 4Gi
      requests:
        cpu: 200m
        memory: 2730Mi
  evictionStrategy: LiveMigrateIfPossible
  hostname: vm-2
  networks:
  - name: default
    pod: {}
  - multus:
      networkName: harvester-system/rwx-network-x5g6q
    name: nic-1
  - multus:
      networkName: harvester-system/vm-migration-network-jgmdb
    name: nic-2
  terminationGracePeriodSeconds: 120
  volumes:
  - name: disk-0
    persistentVolumeClaim:
      claimName: vm-2-disk-0-9pb3s
  - cloudInitNoCloud:
      networkDataSecretRef:
        name: vm-2-k08gb
      secretRef:
        name: vm-2-k08gb
    name: cloudinitdisk
status:
  activePods:
    50ebbc7e-ad8a-47c8-a612-b1e1a9d82eea: node-2
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2026-09-21T13:02:25Z"
    status: "True"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: null
    status: "True"
    type: LiveMigratable
  - lastProbeTime: null
    lastTransitionTime: null
    status: "True"
    type: StorageLiveMigratable
  - lastProbeTime: "2026-09-21T13:04:34Z"
    lastTransitionTime: null
    status: "True"
    type: AgentConnected
  currentCPUTopology:
    cores: 2
    sockets: 1
    threads: 1
  guestOSInfo:
    id: ubuntu
    kernelRelease: 5.4.0-216-generic
    kernelVersion: '#236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025'
    machine: x86_64
    name: Ubuntu
    prettyName: Ubuntu 20.04.6 LTS
    version: 20.04.6 LTS (Focal Fossa)
    versionId: "20.04"
  interfaces:
  - infoSource: domain, guest-agent
    interfaceName: enp1s0
    ipAddress: 10.52.1.20
    ipAddresses:
    - 10.52.1.20
    linkState: up
    mac: aa:2a:d0:73:47:52
    name: default
    podInterfaceName: eth0
    queueCount: 1
  - infoSource: domain, guest-agent, multus-status
    interfaceName: enp2s0
    linkState: up
    mac: ca:d6:68:21:96:08
    name: nic-1
    podInterfaceName: pod8fe0d3f1ac5
    queueCount: 1
  - infoSource: domain, guest-agent, multus-status
    interfaceName: enp3s0
    linkState: up
    mac: da:52:9c:48:02:af
    name: nic-2
    podInterfaceName: pod3c50acb7c28
    queueCount: 1
  launcherContainerImageVersion: registry.suse.com/suse/sles/16.0/virt-launcher:1.8.4-9.1
  machine:
    type: pc-q35-10.0
  memory:
    guestAtBoot: 4Gi
    guestCurrent: 4Gi
    guestRequested: 4Gi
  migrationMethod: BlockMigration
  migrationTransport: Unix
  nodeName: node-2
  phase: Running
  phaseTransitionTimestamps:
  - phase: Pending
    phaseTransitionTimestamp: "2026-09-21T13:02:04Z"
  - phase: Scheduling
    phaseTransitionTimestamp: "2026-09-21T13:02:04Z"
  - phase: Scheduled
    phaseTransitionTimestamp: "2026-09-21T13:02:26Z"
  - phase: Running
    phaseTransitionTimestamp: "2026-09-21T13:02:28Z"
  qosClass: Burstable
  runtimeUser: 107
  selinuxContext: system_u:object_r:unlabeled_t:s0
  virtualMachineRevisionName: revision-start-vm-3b935a25-7400-4eef-babe-b99c5f71bd42-1
  volumeStatus:
  - name: cloudinitdisk
    size: 1048576
    target: vdb
  - name: disk-0
    persistentVolumeClaimInfo:
      accessModes:
      - ReadWriteMany
      capacity:
        storage: 10Gi
      claimName: vm-2-disk-0-9pb3s
      filesystemOverhead: "0"
      requests:
        storage: 10Gi
      volumeMode: Block
    target: vda
Final VM
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  annotations:
    harvesterhci.io/mac-address: '{"default":"fa:50:0d:9c:79:d5","nic-1":"d6:4e:39:47:77:23","nic-2":"e2:e5:39:d5:c8:2a"}'
    harvesterhci.io/timestamp: "2026-09-21T09:38:22Z"
    harvesterhci.io/vmRunStrategy: RerunOnFailure
    harvesterhci.io/volumeClaimTemplates: '[{"metadata":{"name":"vm-1-disk-0-sdtkp","annotations":{"harvesterhci.io/imageId":"default/image-r95k2"}},"spec":{"accessModes":["ReadWriteMany"],"resources":{"requests":{"storage":"10Gi"}},"volumeMode":"Block","storageClassName":"lh-1a108c43-0eb0-470f-b00b-4ee11a8c8e24"}}]'
    kubevirt.io/latest-observed-api-version: v1
    kubevirt.io/storage-observed-api-version: v1
    network.harvesterhci.io/ips: '[]'
  creationTimestamp: "2026-09-21T09:36:49Z"
  finalizers:
  - kubevirt.io/virtualMachineControllerFinalize
  - wrangler.cattle.io/harvester-lb-vm-controller
  - wrangler.cattle.io/VMController.CleanupPVCAndSnapshot
  - wrangler.cattle.io/VMController.ReconcileBackendStorageClone
  generation: 2
  labels:
    harvesterhci.io/creator: harvester
    harvesterhci.io/os: linux
  name: vm-1
  namespace: default
  resourceVersion: "159514"
  uid: d7cef619-da77-4d5b-88f2-9ae7ee810946
spec:
  runStrategy: RerunOnFailure
  template:
    metadata:
      annotations:
        harvesterhci.io/dynamic-ssh-key-names: '{}'
        harvesterhci.io/dynamic-ssh-key-users: '["ubuntu"]'
        harvesterhci.io/sshNames: '[]'
        kubevirt.io/pci-topology-version: v3
      labels:
        harvesterhci.io/vmName: vm-1
    spec:
      accessCredentials:
      - userPassword:
          propagationMethod:
            qemuGuestAgent: {}
          source:
            secret:
              secretName: vm-1-0gj7t
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: network.harvesterhci.io/mgmt
                operator: In
                values:
                - "true"
      architecture: amd64
      domain:
        cpu:
          cores: 2
          maxSockets: 1
          sockets: 1
          threads: 1
        devices:
          disks:
          - bootOrder: 1
            disk:
              bus: virtio
            name: disk-0
          - disk:
              bus: virtio
            name: cloudinitdisk
          inputs:
          - bus: usb
            name: tablet
            type: tablet
          interfaces:
          - macAddress: fa:50:0d:9c:79:d5
            masquerade: {}
            model: virtio
            name: default
          - bridge: {}
            macAddress: d6:4e:39:47:77:23
            model: virtio
            name: nic-1
          - bridge: {}
            macAddress: e2:e5:39:d5:c8:2a
            model: virtio
            name: nic-2
        features:
          acpi:
            enabled: true
        firmware:
          serial: cdfa5fb4-68a2-436f-8c91-fdbaa95dfff8
          uuid: 093a3dbe-02fd-4e7e-91fd-3f4bbcc688a7
        machine:
          type: q35
        memory:
          guest: 4Gi
        resources:
          limits:
            cpu: "2"
            memory: 4Gi
          requests:
            cpu: 200m
            memory: 2730Mi
      evictionStrategy: LiveMigrateIfPossible
      hostname: vm-1
      networks:
      - name: default
        pod: {}
      - multus:
          networkName: harvester-system/rwx-network-x5g6q
        name: nic-1
      - multus:
          networkName: harvester-system/vm-migration-network-jgmdb
        name: nic-2
      terminationGracePeriodSeconds: 120
      volumes:
      - name: disk-0
        persistentVolumeClaim:
          claimName: vm-1-disk-0-sdtkp
      - cloudInitNoCloud:
          networkDataSecretRef:
            name: vm-1-wd51t
          secretRef:
            name: vm-1-wd51t
        name: cloudinitdisk
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2026-09-21T10:44:30Z"
    status: "True"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: null
    status: "True"
    type: LiveMigratable
  - lastProbeTime: null
    lastTransitionTime: null
    status: "True"
    type: StorageLiveMigratable
  - lastProbeTime: null
    lastTransitionTime: "2026-09-21T10:45:01Z"
    status: "True"
    type: AccessCredentialsSynchronized
  - lastProbeTime: "2026-09-21T11:32:11Z"
    lastTransitionTime: null
    status: "True"
    type: AgentConnected
  created: true
  desiredGeneration: 2
  observedGeneration: 2
  printableStatus: Running
  ready: true
  runStrategy: RerunOnFailure
  volumeSnapshotStatuses:
  - enabled: true
    name: disk-0
  - enabled: false
    name: cloudinitdisk
    reason: Snapshot is not supported for this volumeSource type [cloudinitdisk]
Final VMI
apiVersion: kubevirt.io/v1
kind: VirtualMachineInstance
metadata:
  annotations:
    harvesterhci.io/dynamic-ssh-key-names: '{}'