Implementing VLANs with systemd-networkd on an active physical interface



Setting up a VLAN with networkd is unfortunately not as straightforward as with Debian's ifupdown framework, where you can just append the VLAN to the interface name. Networkd has a more complex approach that boils down to this:

  • Each single VLAN is defined in its own .netdev file.
  • The untagged network interface gets matched to the VLANs in a single 'base' .network file.
  • If you are only using tagged interfaces, you do not need to specify any IP settings (ie no DHCP or static lease, DNS or gateway) in that 'base' .network file.
  • Actual configuration of the tagged interfaces is done through a higher level .network file (again one per VLAN interface).
  • Number your .netdev and .network files hierarchically, so they get parsed in the right order.

File naming can be whatever you like to make things clearer - especially the fact that systemd needs at least two .network files to set this up can be confusing, so you need to be able to tell them apart easily.

Note: I had to load the 8021q module manually first. You can automate this by inserting the module in /etc/modules, so after a reboot it gets autoloaded.

# echo 8021q >> /etc/modules

Netdev file: basic VLAN definition

The .netdev file needs two sections: NetDev to define the name and network device type, and VLAN to define the VLAN ID. Note you can name the VLAN device in the traditional $INTERFACE.$VLAN way, but you do not need to. In the example below I have specified a simple human readable name that indicates what the VLAN will be used for. If you feel more comfortable, just use e.g. eth0.100 here.

# cat /etc/systemd/network/10-guest_vlan.netdev 
[NetDev]
Name=guest
Kind=vlan

[VLAN]
Id=100

Create a file for every single VLAN you want to set up. Make sure to number them lower than any .networkfiles to follow - the .netdev files may share the same number, since they are self-contained.

Lower level network file: physical interface configuration

The lower level network file has two purposes: link all VLANs by name to the physical interface, and (if desired) configure the untagged physical interface. While confusing, that's how systemd seems to handle it. In the example below the physical, untagged interface gets set up with static IPv4 and DHCPv6. The 'ghost' lounge and vip VLANs are added as an example; you simply list them all one after another if you are setting up multiple.

Note the higher number so it gets parsed after the .netdev files.

# cat /etc/systemd/network/11-lan_untagged.network
[Match]
Name=eth0
Type=ether

[Network]
Description=Unconfigured physical Ethernet device
VLAN=guest
VLAN=lounge
VLAN=vip
DHCP=ipv6

# IPv4 untagged
Address=192.168.1.10/24
Gateway=192.168.1.1
DNS=192.168.1.3

If you leave the lower physical interface unconfigured, drop everything from the [Network] section after the VLAN= definitions. Make sure any [Address] sections are removed too, and add the following to the [Network] section:

LinkLocalAddressing=no
LLDP=no
EmitLLDP=no
IPv6AcceptRA=no
IPv6SendRA=no

This will prevent the untagged interface from setting up a link-local address or acquiring an IPv6 lease through DHCPv6.

Upper level network file: tagged VLAN configuration

Just like the .netdev file, you set up each tagged VLAN in a separate .network file. The example below sets up the guest network. Note that, when specifying multiple addresses, you need separate [Address] and [Route] sections for each, as you can see below.

# cat /etc/systemd/network/12-guest_vlan.network 
[Match]
Name=guest
Type=vlan

[Network]
Description=Guest network DNS
DHCP=ipv6
DNS=192.168.100.3

# Link-local is necessary for IPv6
LinkLocalAddressing=ipv6
LLDP=false
EmitLLDP=false
IPv6AcceptRA=true
IPv6SendRA=false

[Address]
Address=192.168.100.10/24

[Address]
Address=dead:beef:badc:ab1e::10/64

[Route]
Gateway=192.168.100.1

[IPv6AcceptRA]
DHCPv6Client=true

Now that everything is set up, reload networkd and inspect the interfaces:

$ sudo networkctl reload
$ sudo networkctl list
IDX LINK            TYPE     OPERATIONAL SETUP
  1 lo              loopback carrier     unmanaged
  2 eth0            ether    routable    configured
  3 guest           vlan     routable    configured

That's looking good - our guest network interface is up. Now check whether the VLANs exist:

$ cat /proc/net/vlan/config
VLAN Dev name    | VLAN ID
Name-Type: VLAN_NAME_TYPE_RAW_PLUS_VID_NO_PAD
guest          | 100  | eth0

And with ip:

# ip a s guest
3: guest@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet 192.168.100.10/24 brd 192.168.100.255 scope global guest
   valid_lft forever preferred_lft forever
inet6 dead:beef:badc:ab1e::10/64 scope global mngtmpaddr noprefixroute 
   valid_lft forever preferred_lft forever

The ip output is truncated, with functional IPv6 you will have routable IPv6 addresses as well.