Open vSwitch (OVS) is a programmable virtual switch for Linux. It is the data plane behind OpenStack, OVN and many hypervisors, and it lets you control forwarding with VLANs, tunnels and OpenFlow rules instead of physical cabling. In this tutorial you will install OVS on Ubuntu 24.04, build a bridge with VLAN-isolated ports backed by network namespaces, filter traffic with an OpenFlow rule, extend the bridge to a second server over a VXLAN tunnel and mirror a port for packet capture.
The lab never touches your server's public interface, so you can follow it over SSH without losing connectivity.
Prerequisites
To follow this tutorial you need:
- Two servers running Ubuntu 24.04 LTS, for example two CubePath VPS instances. The first one is enough for Steps 1 to 4; the second is needed for the VXLAN tunnel in Step 5.
- A non-root user with
sudoprivileges on both servers. - IP connectivity between the two servers, ideally over a private network. This guide calls their addresses
host_a_ipandhost_b_ip.
Step 1 - Installing Open vSwitch
Ubuntu ships Open vSwitch in its main repository. The openvswitch-switch package installs the ovsdb-server database, the ovs-vswitchd daemon and the command-line tools:
sudo apt update
sudo apt install openvswitch-switch
The service starts automatically. Check it and confirm the version:
sudo systemctl status openvswitch-switch --no-pager
sudo ovs-vsctl --version
ovs-vsctl (Open vSwitch) 3.3.0
DB Schema 8.5.0
Also confirm the kernel datapath module is loaded:
lsmod | grep openvswitch
openvswitch 204800 0
nsh 12288 1 openvswitch
Step 2 - Creating a bridge and attaching ports
An OVS bridge behaves like a physical switch. Create one called br0:
sudo ovs-vsctl add-br br0
To plug "machines" into the switch without real VMs, you will use network namespaces: each namespace has its own interfaces, addresses and routing table, just like a guest. Create three of them:
sudo ip netns add red1
sudo ip netns add red2
sudo ip netns add blue1
Each namespace connects to the bridge through a veth pair, a virtual cable with two ends. Create the pair for red1, put one end in the namespace and attach the other end to br0 as an access port on VLAN 10:
sudo ip link add red1-br type veth peer name red1-eth
sudo ip link set red1-eth netns red1
sudo ovs-vsctl add-port br0 red1-br tag=10
sudo ip link set red1-br up
Repeat for red2 (also VLAN 10) and blue1 (VLAN 20):
sudo ip link add red2-br type veth peer name red2-eth
sudo ip link set red2-eth netns red2
sudo ovs-vsctl add-port br0 red2-br tag=10
sudo ip link set red2-br up
sudo ip link add blue1-br type veth peer name blue1-eth
sudo ip link set blue1-eth netns blue1
sudo ovs-vsctl add-port br0 blue1-br tag=20
sudo ip link set blue1-br up
Now give every namespace an address in the same subnet, 10.0.10.0/24, and bring its interfaces up:
sudo ip -n red1 addr add 10.0.10.1/24 dev red1-eth
sudo ip -n red1 link set red1-eth up
sudo ip -n red1 link set lo up
sudo ip -n red2 addr add 10.0.10.2/24 dev red2-eth
sudo ip -n red2 link set red2-eth up
sudo ip -n red2 link set lo up
sudo ip -n blue1 addr add 10.0.10.3/24 dev blue1-eth
sudo ip -n blue1 link set blue1-eth up
sudo ip -n blue1 link set lo up
Inspect the switch configuration:
sudo ovs-vsctl show
6c1f2a2e-...
Bridge br0
Port red1-br
tag: 10
Interface red1-br
Port red2-br
tag: 10
Interface red2-br
Port blue1-br
tag: 20
Interface blue1-br
Port br0
Interface br0
type: internal
ovs_version: "3.3.0"
The br0 port of type internal is created automatically with the bridge. It is the host's own interface on the switch.
Step 3 - Verifying VLAN isolation
The three namespaces share a subnet, but only ports in the same VLAN can reach each other. Ping red2 from red1:
sudo ip netns exec red1 ping -c 3 10.0.10.2
64 bytes from 10.0.10.2: icmp_seq=1 ttl=64 time=0.412 ms
64 bytes from 10.0.10.2: icmp_seq=2 ttl=64 time=0.061 ms
64 bytes from 10.0.10.2: icmp_seq=3 ttl=64 time=0.058 ms
Now try blue1, which is on VLAN 20:
sudo ip netns exec red1 ping -c 3 -W 1 10.0.10.3
3 packets transmitted, 0 received, 100% packet loss, time 2046ms
The ARP request from red1 never leaves VLAN 10, so blue1 is unreachable even though the IP addresses would allow it. You can view the MAC addresses the bridge has learned, with the VLAN of each one:
sudo ovs-appctl fdb/show br0
port VLAN MAC Age
1 10 2a:5e:0c:91:44:07 3
2 10 9e:13:b7:6a:02:c1 3
Step 4 - Writing OpenFlow rules
By default a new bridge has a single flow that sends every packet to the NORMAL action, which is ordinary MAC-learning switch behavior. List the flow table:
sudo ovs-ofctl dump-flows br0
cookie=0x0, duration=512.3s, table=0, n_packets=46, n_bytes=3612, priority=0 actions=NORMAL
Add a higher-priority rule that drops ICMP coming from the red1 port while leaving all other traffic untouched. OVS 3.x accepts port names in in_port:
sudo ovs-ofctl add-flow br0 "priority=100,icmp,in_port=red1-br,actions=drop"
Test it. Ping now fails, but other protocols keep working, which you can confirm with an ARP probe:
sudo ip netns exec red1 ping -c 2 -W 1 10.0.10.2
sudo ip netns exec red1 ip neigh show
2 packets transmitted, 0 received, 100% packet loss, time 1024ms
10.0.10.2 dev red1-eth lladdr 9e:13:b7:6a:02:c1 REACHABLE
When a rule does not behave as expected, ask OVS how it would process a given packet with ofproto/trace:
sudo ovs-appctl ofproto/trace br0 in_port=red1-br,icmp,nw_src=10.0.10.1,nw_dst=10.0.10.2
Flow: icmp,in_port=1,vlan_tci=0x0000,...,nw_src=10.0.10.1,nw_dst=10.0.10.2,...
bridge("br0")
-------------
0. icmp,in_port=1, priority 100
drop
Final flow: unchanged
Datapath actions: drop
The n_packets counter in dump-flows also shows how many packets matched each rule. Remove the rule when you are done so the rest of the tutorial works:
sudo ovs-ofctl del-flows br0 "icmp,in_port=red1-br"
sudo ovs-ofctl dump-flows br0
Only the priority=0 actions=NORMAL flow should remain.
NoteFlows added with
ovs-ofctllive only inovs-vswitchdmemory and are lost when the service restarts. In production they are normally pushed by a controller such as OVN, or reapplied by a systemd unit at boot.
Step 5 - Connecting two hosts with a VXLAN tunnel
A tunnel port extends the bridge to another server, so namespaces on both hosts share the same VLANs. VXLAN encapsulates each Ethernet frame in UDP port 4789.
On host B, install Open vSwitch as in Step 1, then create the bridge and one namespace on VLAN 10:
sudo ovs-vsctl add-br br0
sudo ip netns add red3
sudo ip link add red3-br type veth peer name red3-eth
sudo ip link set red3-eth netns red3
sudo ovs-vsctl add-port br0 red3-br tag=10
sudo ip link set red3-br up
sudo ip -n red3 addr add 10.0.10.4/24 dev red3-eth
sudo ip -n red3 link set red3-eth up
sudo ip -n red3 link set lo up
Still on host B, add a VXLAN port that points to host A. The key option sets the VXLAN Network Identifier (VNI) and must match on both ends:
sudo ovs-vsctl add-port br0 vxlan0 -- set interface vxlan0 type=vxlan options:remote_ip=host_a_ip options:key=100
On host A, add the mirror-image port pointing to host B:
sudo ovs-vsctl add-port br0 vxlan0 -- set interface vxlan0 type=vxlan options:remote_ip=host_b_ip options:key=100
The tunnel port has no tag, so it acts as a trunk and carries the 802.1Q tag of each frame inside the encapsulation. If UFW is active, allow VXLAN from the peer on each host (use host_a_ip on host B):
sudo ufw allow from host_b_ip to any port 4789 proto udp
VXLAN adds 50 bytes of headers. If the underlay MTU is 1500, lower the MTU of the namespace interfaces so encapsulated frames are not dropped. On host A:
sudo ip -n red1 link set red1-eth mtu 1450
On host B:
sudo ip -n red3 link set red3-eth mtu 1450
Now ping red3 on host B from red1 on host A:
sudo ip netns exec red1 ping -c 3 10.0.10.4
64 bytes from 10.0.10.4: icmp_seq=1 ttl=64 time=1.84 ms
64 bytes from 10.0.10.4: icmp_seq=2 ttl=64 time=0.72 ms
64 bytes from 10.0.10.4: icmp_seq=3 ttl=64 time=0.69 ms
To see the encapsulation on the wire, capture on the underlay interface of either host (replace eth1 with the interface that holds host_a_ip):
sudo tcpdump -ni eth1 udp port 4789 -c 4
IP host_a_ip.51234 > host_b_ip.4789: VXLAN, flags [I] (0x08), vni 100
IP 10.0.10.1 > 10.0.10.4: ICMP echo request, id 3, seq 1, length 64
blue1 still cannot reach red3, because VLAN isolation is preserved across the tunnel.
Step 6 - Mirroring a port for packet capture
A mirror copies the traffic of one or more ports to an output port, which is useful for IDS sensors or debugging. On host A, create an internal port to receive the copy and bring it up:
sudo ovs-vsctl add-port br0 mirror0 -- set interface mirror0 type=internal
sudo ip link set mirror0 up
Create a mirror that copies everything sent and received by red1-br to mirror0. The --id=@name syntax stores a record reference for use later in the same transaction:
sudo ovs-vsctl -- --id=@src get port red1-br \
-- --id=@out get port mirror0 \
-- --id=@m create mirror name=red1-mirror select-src-port=@src select-dst-port=@src output-port=@out \
-- set bridge br0 mirrors=@m
In one terminal, capture on the mirror port:
sudo tcpdump -ni mirror0 icmp
In another terminal, generate traffic from red1:
sudo ip netns exec red1 ping -c 2 10.0.10.2
The capture shows the echo requests and replies of red1 even though mirror0 is not part of that conversation:
IP 10.0.10.1 > 10.0.10.2: ICMP echo request, id 5, seq 1, length 64
IP 10.0.10.2 > 10.0.10.1: ICMP echo reply, id 5, seq 1, length 64
Remove the mirror and its port when you no longer need them:
sudo ovs-vsctl clear bridge br0 mirrors
sudo ovs-vsctl del-port br0 mirror0
Step 7 - Understanding persistence and cleaning up
OVS stores bridges, ports, VLAN tags, tunnels and mirrors in its database (/etc/openvswitch/conf.db), so they survive a reboot. The network namespaces, veth pairs, IP addresses and ovs-ofctl flows do not. For permanent host networking on OVS, Netplan can declare OVS bridges with its openvswitch settings, and hypervisors such as Proxmox VE manage OVS ports for their guests.
To remove the lab on host A:
sudo ovs-vsctl del-br br0
sudo ip netns del red1
sudo ip netns del red2
sudo ip netns del blue1
Deleting a namespace destroys the veth end inside it, which also removes its peer. On host B, delete br0 and the red3 namespace the same way.
Troubleshooting
ovs-vsctl: unix:/var/run/openvswitch/db.sock: database connection failed: the database server is not running. Checksudo systemctl status ovsdb-serverandsudo journalctl -u ovs-vswitchd -n 50.- Ping fails between ports on the same VLAN: confirm both veth ends are
UP(ip link show red1-brandsudo ip -n red1 link), check the tags withsudo ovs-vsctl list port red1-br, and look for leftover drop rules withsudo ovs-ofctl dump-flows br0. - VXLAN tunnel carries no traffic: first make sure
host_a_ipandhost_b_ipcan ping each other, then check that UDP 4789 is allowed on both hosts and thatkeymatches.sudo ovs-vsctl list interface vxlan0shows the options OVS applied. - Small pings work but large transfers stall: the overlay MTU is too high. Keep it 50 bytes below the underlay MTU.
Conclusion
You installed Open vSwitch on Ubuntu 24.04, isolated ports with VLAN tags, controlled forwarding with an OpenFlow rule, extended the bridge across two servers with VXLAN and mirrored a port for analysis. The same ovs-vsctl and ovs-ofctl commands apply when the ports belong to KVM guests instead of namespaces. As next steps, attach virtual machines to br0, try OVN to manage flows and tunnels centrally across many hosts, or add connection tracking rules (ct()) to build a stateful firewall in the switch.
