Skip to content

Containers that SNAT traffic they should not

Context: self-built VyOS 1.5-rolling-202505061352 on a lab VM, reproducing something seen in production shape: packets leaving the router were source-NAT'd although no NAT rule matched them. The first hint was iptables refusing to show the truth:

# iptables -L -t nat
# Table `nat' contains incompatible base-chains, use 'nft' tool to list them.

The nft view showed the chain:

table ip nat {
    chain NETAVARK-HOSTPORT-MASQ {
         meta mark & 0x00002000 == 0x00002000 counter packets 0 bytes 0 masquerade
    }
}

netavark — the container network stack — masquerades everything carrying mark bit 0x2000. VyOS policy-based routing fwmarks its policy-routed packets with 0x7ffffffe. Bit 13 of 0x7ffffffe is set. Any policy-routed packet therefore matches the masquerade rule — containers running or not. And the rule stays behind after the container is deleted.

Reproduce, nine steps

# 0) have any nat rule configured
# 1) a static table for policy routing
set protocols static table 1 route 0.0.0.0/0 next-hop <wan-gw>

# 2) policy routing that uses it
set policy route pbr-eth0 interface 'eth0'
set policy route pbr-eth0 rule 16 log
set policy route pbr-eth0 rule 16 set table '1'
set policy route pbr-eth0 rule 16 destination address '<captured-host>'

# confirm the fwmark exists
# ip rule list | grep fwmark
1: from all fwmark 0x7ffffffe lookup 1

# 3-5) a container network and a container
set container network ctr prefix '10.0.100.0/24'
run add container image 'debian:bookworm'
set container name ctest image 'debian:bookworm'
set container name ctest network 'ctr'

# 6) ping the captured host from LAN while tcpdumping the WAN side
#    -> replies arrive with correct addresses
# 7) commit any (even partial) nat rule
set nat
commit

# 8) now the same ping leaves MASQUERADE'd — tcpdump on the
#    container interface shows translated addresses
# 9) delete the container — the masquerade rule stays

Counters confirm who did it:

chain NETAVARK-HOSTPORT-MASQ {
     meta mark & 0x00002000 == 0x00002000 counter packets 138 bytes 11592 masquerade
}

Workarounds

  • Put container networks in their own VRF, so their traffic never crosses the policy-routing marks:

    set vrf name container-vrf description "container networks"
    set vrf name container-vrf table 100
    set container network ctr prefix '10.0.100.0/24'
    set container network ctr vrf container-vrf
    
  • Or delete the container and reboot — the masquerade rule does not survive.

Why it matters

Nothing in the config says "SNAT this". The NAT table looks correct. The proof needs nft, tcpdump and reading a bit mask — which is why it looks like random magic in production. If you run VyOS with both policy-based routing and containers, check for 0x2000 in your marks. Filed upstream with VyOS.


This saved you a night? I do this for a living: info@wirt.ee.