How to tag IPv4 and IPv6 packets with different VLAN tags on a Linux box?
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?
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
[NetDev]
Name=vlan4
Kind=vlan
[VLAN]
Id=4
EOF
~# cat > /etc/systemd/network/02-vlan6.netdev <<EOF
[NetDev]
Name=vlan6
Kind=vlan
[VLAN]
Id=6
EOF
~# cat > /etc/systemd/network/03-br0.netdev <<EOF
[NetDev]
Name=br0
Kind=bridge
[Bridge]
DefaultPVID=6
VLANProtocol=802.1q
STP=no
EOF
Then attach the interfaces to eth1 and to the bridge:
~# cat > /etc/systemd/network/12-eth1_attach-vlans.network <<EOF
[Match]
Name=eth1
[Network]
LLMNR=no
LinkLocalAddressing=no
VLAN=vlan4
VLAN=vlan6
EOF
~# cat > /etc/systemd/network/16-ifs_add_to_br0.network <<EOF
[Match]
Name=eth0 vlan4 vlan6
[Network]
Bridge=br0
LLMNR=no
LinkLocalAddressing=no
EOF
Now just bring up the bridge:
~# cat > /etc/systemd/network/20-br0-up.network <<EOF
[Match]
Name=br0
[Network]
LLMNR=no
MulticastDNS=yes
EOF
After a reboot this will give you:
~$ ip -brief address
lo UNKNOWN 127.0.0.1/8 ::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.