I want to tag incoming IPv4 and IPv6 packets from a dual stack enabled connection with different VLAN tags, e.g. IPv4 packets should go to VLAN4 and IPv6 packets should go to VLAN6. To be more general, I want to split the dual stack ip stream with mixed IPv4 and IPv6 packets into two clean single stack networks so you will not find any IPv4 packet on a IPv6 network and vise versa. I need this to test and support an IPv6 only network. And of course I still need the IPv4 data. It cannot simply be dropped.

                          Linux Box
                       Debian Bullseye
       untagged         ┏━━━━━━━━━━━┓         tagged (trunk)
════════════════════════┫eth0  vlan4┣═╦══════════════════════
     IPv4 and IPv6      ┃      vlan6┣═╝eth1  IPv4 with VLAN4 tag
      dual stack        ┗━━━━━━━━━━━┛        IPv6 with VLAN6 tag

I had a look at the Linux bridge and at nftables but wasn't able to find a solution. How can I achieve this selective tagging?

  • 2
    Show your VLAN interfaces and routing table. Feb 10 at 12:32
  • 1
    Is the Linux box acting as a router, or a bridge? Feb 11 at 9:43
  • @MichaelHampton Thanks for having a look at the question and asking for details. Only looking for an example to answer the details give me the idea for a solution :-)
    – Ingo
    Feb 11 at 23:05
  • @GordonDavisson It is not the question what we have. The goal defines what to use. In this case we have to use a bridge as I found in my answer.
    – Ingo
    Feb 11 at 23:09
  • Is there a reason you cannot simply have the two vlan interfaces and assign the local addresses to these to match what you like, letting the normal routing table do the work instead of all this bridging and filtering and stuff? Feb 12 at 15:30

While your answer apparantly works for you, it does seem overly complicated. I'm doubtful though that it does what you want, as it lacks any IPv4 addresses in the given output (apart from lo).

Creating 2 tagged interfaces (named e.g. vlan4 and vlan6), assigning them IPv4 and IPv6 addresses + gateways, and disabling SLAAC with sysctl for the IPv4 one should be sufficient.

There's neither need for bridging nor messing with nftables apart from what you would need to enable flow between eth0 and eth1.


I have found a solution. Because I want to manipulate VLANs, I have to use a bridge. VLAN is working on OSI layer 2 and a bridge is the device that can handle layer 2 protocols. So first I added two VLAN interfaces to the physical interface eth1. Then added all interfaces eth0, vlan4 and vlan6 to the bridge. The rest is done by nftables.

IPv4 and IPv6 are defined on layer3 and have no different meaning on layer 2. So nftables can handle them just as packets with different "marks", which is the protocol type in the IP header. Fortunately nftables can select them with meta protocol {}. It directs the incoming untagged IPv4 and IPv6 packets to the corresponding VLAN interface. Tagging is done automagically by the interface as usual. I use systemd-networkd and here is the configuration in detail.

First create the network devices vlan4, vlan6 and the bridge br0:

~$ sudo -Es
~# cat > /etc/systemd/network/01-vlan4.netdev <<EOF

~# cat > /etc/systemd/network/02-vlan6.netdev <<EOF

~# cat > /etc/systemd/network/03-br0.netdev <<EOF

Then attach the interfaces to eth1 and to the bridge:

~# cat > /etc/systemd/network/12-eth1_attach-vlans.network <<EOF

~# cat > /etc/systemd/network/16-ifs_add_to_br0.network <<EOF
Name=eth0 vlan4 vlan6

Now just bring up the bridge:

~# cat > /etc/systemd/network/20-br0-up.network <<EOF

After a reboot this will give you:

~$ ip -brief address
lo               UNKNOWN ::1/128
eth0             UP
br0              UP             2003:d5:2721:900:9012:fdff:fef0:ea7f/64 fe80::9012:fdff:fef0:ea7f/64
vlan6@eth1       UP
vlan4@eth1       UP
eth1             UP

# these are the slave interfaces of the bridge
~$ sudo bridge link show
4: vlan6@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 4
5: vlan4@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 4
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 4

It is also worth to check with resolvectl. Now we have to do the final step and redirect the packets with nftables using these rules:

~$ cat /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table bridge filter {
    chain forward {
        type filter hook forward priority 0; policy accept;
        meta protocol { ip6 } iifname "vlan4" drop
        meta protocol { ip6 } oifname "vlan4" drop
        meta protocol { ip6 } iifname "vlan6" accept
        meta protocol { ip6 } oifname "vlan6" accept
        iifname "vlan6" drop
        oifname "vlan6" drop

    chain output {
        type filter hook output priority 0; policy drop;
        meta protocol { ip6 } iifname "vlan6" accept
        meta protocol { ip6 } oifname "vlan6" accept

On the forward chain this will drop all IPv6 to/from interface vlan4 and only allow it on interface vlan6. All other stuff is dropped on interface vlan6, but by the chains default policy accepted on interface vlan4. This ensures that all old stuff like ARP and other broadcasts go also to interface vlan4.

The output chain is only to avoid that the bridge itself sends packets to the wrong VLAN. On my configuration it uses only IPv6 (single stack) so everything will be dropped by the chains default policy, except IPv6 to vlan6.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.