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 sudo privileges on both servers.
  • IP connectivity between the two servers, ideally over a private network. This guide calls their addresses host_a_ip and host_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.

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. Check sudo systemctl status ovsdb-server and sudo journalctl -u ovs-vswitchd -n 50.
  • Ping fails between ports on the same VLAN: confirm both veth ends are UP (ip link show red1-br and sudo ip -n red1 link), check the tags with sudo ovs-vsctl list port red1-br, and look for leftover drop rules with sudo ovs-ofctl dump-flows br0.
  • VXLAN tunnel carries no traffic: first make sure host_a_ip and host_b_ip can ping each other, then check that UDP 4789 is allowed on both hosts and that key matches. sudo ovs-vsctl list interface vxlan0 shows 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.