In Ethernet Lab - Broadcast domain we learned how to use switches (bridges) to merge multiple L2 segments into a single broadcast domain. In this lab, we'll see how to use the VLAN capabilities of a switch to split a single broadcast domain into multiple smaller domains.

Prerequisite

We will use a Linux bridge to emulate a network switch and Linux network namespaces to emulate network nodes.

The following helper functions should be defined (copy and paste them to helpers.sh or download this gist):

Create Linux bridge

Creates an isolated network namespace with a Linux bridge inside and activates VLAN filtering on the bridge:

create_bridge() {
    local nsname="$1"
    local ifname="$2"

    echo "Creating bridge ${nsname}/${ifname}"

    ip netns add ${nsname}
    ip netns exec ${nsname} ip link set lo up
    ip netns exec ${nsname} ip link add ${ifname} type bridge
    ip netns exec ${nsname} ip link set ${ifname} up

    # Enable VLAN filtering on bridge.
    ip netns exec ${nsname} ip link set ${ifname} type bridge vlan_filtering 1
}

Create End Host

Creates an isolated network namespace, connects it using a veth pair to one of the bridge's ports and enables VLAN tagging of Ethernet frames on that port:

create_end_host() {
    local host_nsname="$1"
    local peer1_ifname="$2a"
    local peer2_ifname="$2b"
    local vlan_vid="$3"
    local bridge_nsname="$4"
    local bridge_ifname="$5"

    echo "Creating end host ${host_nsname} connected to ${bridge_nsname}/${bridge_ifname} bridge (VLAN ${vlan_vid})"

    # Create end host network namespace.
    ip netns add ${host_nsname}
    ip netns exec ${host_nsname} ip link set lo up

    # Create a veth pair connecting end host and bridge namespaces.
    ip link add ${peer1_ifname} netns ${host_nsname} type veth peer \
                ${peer2_ifname} netns ${bridge_nsname}
    ip netns exec ${host_nsname} ip link set ${peer1_ifname} up
    ip netns exec ${bridge_nsname} ip link set ${peer2_ifname} up

    # Attach peer2 interface to the bridge.
    ip netns exec ${bridge_nsname} ip link set ${peer2_ifname} master ${bridge_ifname}

    # Put host into right VLAN
    ip netns exec ${bridge_nsname} bridge vlan del dev ${peer2_ifname} vid 1
    ip netns exec ${bridge_nsname} bridge vlan add dev ${peer2_ifname} vid ${vlan_vid} pvid ${vlan_vid}
}

How to send Ethernet frames from command line

Most of the available networking tools operate on L3 or above. Hence, we need to make our own tool to manually transmit arbitrary data on the data link layer. To send Ethernet frames programmatically we can leverage packet sockets (AF_PACKET) operating in raw mode (SOCK_RAW). Thus, we'll need to build Ethernet frames in the code and then write them into the raw packet socket. Luckily Ethernet frames have a fairly simple structure.

Layer 2 Ethernet Frame structure.

We will use the following (hopefully self-explanatory) python code to create and send Ethernet frames (gist):

#!/usr/bin/env python3

# Usage: ethsend.py eth0 ff:ff:ff:ff:ff:ff 'Hello everybody!'
#        ethsend.py eth0 06:e5:f0:20:af:7a 'Hello 06:e5:f0:20:af:7a!'
#
# Note: CAP_NET_RAW capability is required to use SOCK_RAW

import fcntl
import socket
import struct
import sys

def send_frame(ifname, dstmac, eth_type, payload):
    # Open raw socket and bind it to network interface.
    s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
    s.bind((ifname, 0))

    # Get source interface's MAC address.
    info = fcntl.ioctl(s.fileno(),
                       0x8927,
                       struct.pack('256s', bytes(ifname, 'utf-8')[:15]))
    srcmac = ':'.join('%02x' % b for b in info[18:24])

    # Build Ethernet frame
    payload_bytes = payload.encode('utf-8')
    assert len(payload_bytes) <= 1500  # Ethernet MTU

    frame = human_mac_to_bytes(dstmac) + \
            human_mac_to_bytes(srcmac) + \
            eth_type + \
            payload_bytes

    # Send Ethernet frame
    return s.send(frame)

def human_mac_to_bytes(addr):
    return bytes.fromhex(addr.replace(':', ''))

def main():
  ifname = sys.argv[1]
  dstmac = sys.argv[2]
  payload = sys.argv[3]
  ethtype = b'\x7A\x05'  # arbitrary, non-reserved
  send_frame(ifname, dstmac, ethtype, payload)

if __name__ == "__main__":
    main()

⚠️ CAP_NET_RAW capability is required to run the code from above (alternatively, you can use sudo).

How VLAN is implemented

To split a single L2 network segment into multiple non-intersecting sub-segments without any rewiring so-called frame tagging technique is used. The Ethernet frame format is altered and an extra 4-bytes-long field is added. Among other things, it carries a VLAN ID. Frames with different VLAN IDs logically belong to different L2 segments.

Layer 2 Ethernet Frame VLAN tagging.

There is more than one way to implement tagging. In this lab, the tagging is transparent to the end-nodes and fully implemented by the bridge. Thus, the command-line tool used to send Ethernet frames actually knows nothing about VLAN IDs.

Simple VLAN example

This example demonstrates how to split hosts connected to a shared switch on multiple broadcast domains by assigning them to different VLANs.

Simple VLAN example.

Use the following script to set it all up:

$ cat > demo.sh <<EOF
#!/usr/bin/env bash

source helpers.sh

create_bridge netns_br0 br0

create_end_host netns_veth10 veth10 10 netns_br0 br0
create_end_host netns_veth11 veth11 10 netns_br0 br0
create_end_host netns_veth12 veth12 10 netns_br0 br0

create_end_host netns_veth20 veth20 20 netns_br0 br0
create_end_host netns_veth21 veth21 20 netns_br0 br0
create_end_host netns_veth22 veth22 20 netns_br0 br0
EOF

$ sudo bash demo.sh
Creating bridge netns_br0/br0
Creating end host netns_veth10 connected to netns_br0/br0 bridge (VLAN 10)
Creating end host netns_veth11 connected to netns_br0/br0 bridge (VLAN 10)
Creating end host netns_veth12 connected to netns_br0/br0 bridge (VLAN 10)
Creating end host netns_veth20 connected to netns_br0/br0 bridge (VLAN 20)
Creating end host netns_veth21 connected to netns_br0/br0 bridge (VLAN 20)
Creating end host netns_veth22 connected to netns_br0/br0 bridge (VLAN 20)

Here is a quick demo demonstrating that there are indeed two broadcast domains on a single bridge:

# Host 1 (Terminal 1)
$ sudo nsenter --net=/var/run/netns/netns_veth10
$ python3 ethsend.py veth10a ff:ff:ff:ff:ff:ff 'Hello VLAN 10!'

# Host 2 (Terminal 2)
$ sudo nsenter --net=/var/run/netns/netns_veth20
$ python3 ethsend.py veth20a ff:ff:ff:ff:ff:ff 'Hello VLAN 20!'

# Host 3 (Terminal 3)
$ sudo nsenter --net=/var/run/netns/netns_veth11
$ tcpdump -i veth11a ether proto 0x7a05

# Host 4 (Terminal 4)
$ sudo nsenter --net=/var/run/netns/netns_veth12
$ tcpdump -i veth12a ether proto 0x7a05

# Host 5 (Terminal 5)
$ sudo nsenter --net=/var/run/netns/netns_veth21
$ tcpdump -i veth21a ether proto 0x7a05

# Host 6 (Terminal 6)
$ sudo nsenter --net=/var/run/netns/netns_veth22
$ tcpdump -i veth22a ether proto 0x7a05

⚠️ MAC address will be different on every setup. Replace them with your values.

Demo - Two VLANs on a bridge.

Two VLANs on a bridge.

Notice, that neither end hosts' interfaces, nor Ethernet sending script have to know anything about VLAN tagging. The VLAN setup in this demo is rather transparent to the end participants. The bridge assigns appropriate VLAN tags based on the frame's ingress port. But that's not the only possible way of doing VLANs. For example, VLAN tagging could be done on the end hosts.

Another interesting thing about VLAN is that it's defined on the L2 segment layer. Thus, VLANs can be configured over multiple bridged segments forming one broadcast domain.

To clean up, just remove the created network namespaces:

$ sudo ip netns delete netns_br0

$ sudo ip netns delete netns_veth10
$ sudo ip netns delete netns_veth11
$ sudo ip netns delete netns_veth12

$ sudo ip netns delete netns_veth20
$ sudo ip netns delete netns_veth21
$ sudo ip netns delete netns_veth22

Further Reading

See also