Masquerade binding provides an IPv6 address to the guest but no default IPv6 route (no RA), unlike IPv4
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):
- Create a VMI on a dual-stack cluster with a
masquerade: {}default network interface (no other special config needed). - 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):Note there is noip -6 route showdefault ... dev <pod-iface>route present, in contrast to the IPv4 side which does havedefault via 10.0.2.1. - 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):This fails (100% loss), even though:ping6 <pod-ipv6-address>- the DNAT rule in the pod netns's
ip6 nattable correctly fires and rewrites the destination to the guest's internal address (confirmed via packet counters), net.ipv6.conf.all.forwarding=1in 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.
- the DNAT rule in the pod netns's
- 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:
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]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
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]apiVersion: kubevirt.io/v1
kind: VirtualMachineInstance
metadata:
annotations:
harvesterhci.io/dynamic-ssh-key-names: '{}'Source: kubevirt/kubevirt