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.