Home / Walkthroughs / OpenBSD Virtual Router
 Networking & Security

OpenBSD Virtual Router
& Multi-VLAN Segmentation

Deploy a full-featured OpenBSD 7 router as a VM on a FreeBSD/bhyve hypervisor — multi-VLAN network segmentation, PF firewall rules, DHCP server per subnet, and relayd for reverse proxying inbound traffic. Replace your existing router with a pure, auditable BSD firewall you configure yourself.

Advanced ~3–5 hours bhyve or KVM host
OpenBSD PF Firewall VLAN bhyve DHCP relayd
📖
Overview & Architecture

OpenBSD is one of the most security-focused operating systems available and ships with PF (Packet Filter) — a powerful, readable firewall syntax. Running it as a VM on your hypervisor lets you place it "in front" of all your other VMs without needing dedicated hardware, and lets you test changes safely before applying them to production.

Final Architecture

# Network layout — virtiz homelab, January 2022 Phone → WiFi AP → Unifi Switch → Mikrotik Switch → OpenBSD VM Router → Modem → Internet VLANs on trunk: VLAN home → 10.10.1.0/24 (home devices, WiFi) VLAN apps → 10.10.2.0/24 (app servers, homelab VMs) VLAN data → 10.10.3.0/24 (NAS, storage, hypervisors) VLAN wan → ISP-assigned (WAN, direct to modem)
Test as downstream router first. Before cutting over production internet, run OpenBSD as a secondary/downstream router on a test VLAN. Once stable, promote it to production and shut down your existing router. This is the safest way to validate the setup before full cutover.
🔀
VLAN Design

The hypervisor carries all VLANs on a LAG (Link Aggregation Group) trunk — VLANs are configured on a bonded bridge and passed through to the switch ports the hypervisor connects to. The OpenBSD VM gets multiple virtual NICs, one per VLAN, each corresponding to a separate subnet. The physical switch tags traffic to the correct VLAN per port.

VM switch VLAN assignment required: On most hypervisors, you cannot bind multiple VM virtual switches to the same physical interface unless each switch has a VLAN tag assigned. Assign a unique VLAN tag to each virtual switch before attempting to attach them to the same uplink.

Switch Port Configuration

Port / Interface Mode Purpose
Hypervisor uplink Trunk (all VLANs) Carries all VLAN traffic to/from hypervisor
WAN port VLAN (ISP/WAN) Modem → switch → hypervisor WAN NIC
WiFi AP port Trunk (home VLAN) Passes home VLAN to AP for client WiFi
Server ports Access (apps or data) Each server port assigned to its VLAN untagged
VLANs must be assigned at the virtual NIC level in VM-hosted routers. Unlike a physical router where VLANs can be configured in the firewall's interface settings, a virtualized router requires VLAN tags to be assigned directly to the VM's virtual NICs. A single trunk NIC can carry both WAN and multiple LAN VLANs simultaneously.
🖥️
Create the OpenBSD VM on bhyve

On a FreeBSD host using the vm-bhyve framework, create a VM config file for OpenBSD. The config below is the exact working config used in production.

# /vm/openbsd-router/openbsd-router.conf loader="grub" cpu=2 memory=4G network0_type="virtio-net" network0_switch="apps" # internal LAN switch disk0_type="virtio-blk" disk0_name="disk0.img" grub_install0="kopenbsd -h com0 /7.0/amd64/bsd.rd" grub_run0="kopenbsd -h com0 -r sd0a /bsd" bhyve_options="-w" uuid="48a1acfc-622e-11ec-a991-0cc47abbd8c8" network0_mac="58:9c:fc:02:27:f6"

For a multi-VLAN router add additional network interfaces — one per VLAN. Each maps to a separate vm-bhyve virtual switch:

# Add to the VM config for multi-NIC setup: network0_type="virtio-net" network0_switch="wan" # WAN switch (ISP VLAN) network1_type="virtio-net" network1_switch="home" # Home VLAN network2_type="virtio-net" network2_switch="apps" # Apps VLAN network3_type="virtio-net" network3_switch="data" # Data/storage VLAN
  1. 1

    Create the VM and Disk

    vm create -t openbsd -s 20G openbsd-router vm install openbsd-router OpenBSD-7.0-amd64.iso
  2. 2

    Connect to the Console

    vm console openbsd-router

    OpenBSD's serial console is activated by the -h com0 flag in the grub config. You'll see the OpenBSD installer via the console.

📦
Install OpenBSD 7
  1. 1

    Walk Through the Installer

    At the boot> prompt, press Enter. The installer asks a series of questions:

    • Keyboard layout: default (us)
    • Hostname: e.g. fw01
    • Network interface: configure vio0 with a temporary IP to reach a mirror
    • Root password: set a strong one
    • Enable sshd: yes
    • Disk layout: use whole disk / auto layout for simplicity
    • Sets to install: all defaults are fine; you can deselect game73.tgz
  2. 2

    First Boot — Disable Console Beep

    echo "set tty com0" >> /etc/boot.conf

    This ensures serial console persists across reboots when running headless in bhyve.

  3. 3

    Update Packages

    syspatch # apply OS security patches pkg_add vim wget # install any tools you need
🔌
Configure Network Interfaces

OpenBSD configures each interface via a /etc/hostname.vioN file. The WAN interface gets its IP from DHCP (from the modem/ISP). Each LAN interface gets a static IP — the gateway for that VLAN's subnet. Additional config files handle the rest: pf.conf contains the firewall rules, dhcpd.conf defines DHCP ranges per subnet, sysctl.conf enables IP forwarding, and rc.conf.local controls which daemons start at boot.

# /etc/hostname.vio0 — WAN (ISP) dhcp
# /etc/hostname.vio1 — Home VLAN (10.10.1.0/24) inet 10.10.1.1 255.255.255.0 description "home-lan"
# /etc/hostname.vio2 — Apps VLAN (10.10.2.0/24) inet 10.10.2.1 255.255.255.0 description "apps-lan"
# /etc/hostname.vio3 — Data VLAN (10.10.3.0/24) inet 10.10.3.1 255.255.255.0 description "data-lan"

Bring all interfaces up:

sh /etc/netstart
⚙️
Enable Packet Forwarding

OpenBSD does not forward packets between interfaces by default. Enable it permanently:

# /etc/sysctl.conf net.inet.ip.forwarding=1 net.inet6.ip6.forwarding=1 # if using IPv6

Apply immediately:

sysctl net.inet.ip.forwarding=1
📋
DHCP Server Per VLAN

OpenBSD's built-in dhcpd serves DHCP leases. Configure a subnet block for each LAN interface.

# /etc/dhcpd.conf option domain-name-servers 1.1.1.1, 8.8.8.8; default-lease-time 3600; max-lease-time 86400; subnet 10.10.1.0 netmask 255.255.255.0 { option routers 10.10.1.1; range 10.10.1.100 10.10.1.200; } subnet 10.10.2.0 netmask 255.255.255.0 { option routers 10.10.2.1; range 10.10.2.100 10.10.2.200; } subnet 10.10.3.0 netmask 255.255.255.0 { option routers 10.10.3.1; range 10.10.3.100 10.10.3.200; }

Enable and start dhcpd, specifying which interfaces to listen on:

# /etc/rc.conf.local dhcpd_flags="vio1 vio2 vio3"
rcctl enable dhcpd rcctl start dhcpd
🛡️
PF Firewall Rules

The following is the actual pf.conf used in production.

# /etc/pf.conf — Production OpenBSD Router wan="vio0" lan = "{ \ vio1 vio2 vio3}" private_networks = "{ 10.10.1.0/24 10.10.2.0/24 10.10.3.0/24}" set skip on lo # Provide nice blocked messages set block-policy return # Block all unless an allow rule exists (default deny) block all ####################### ### Cleanup Packets ### ####################### # Reassemble fragmented packets set reassemble yes # Antispoof — block bogus/spoofed packets block in quick on $wan from no-route to any block in quick on $wan from any to 255.255.255.255 block in quick on $wan from any to $private_networks block in quick on $wan from $private_networks to any ############ ### NAT #### ############ ##################### ### SPECIAL rules ### ##################### # FTP proxy support anchor "ftp-proxy/*" pass in quick on $lan proto tcp from any to port 21 rdr-to 127.0.0.1 port 8021 # Redirect inbound HTTPS to internal reverse proxy (relayd) pass in quick on $wan proto tcp from any to ($wan) port 443 rdr-to 10.10.1.201 port 18443 # Allow all traffic in from LAN interfaces pass in on $lan ###################### ### Pass IN rules ### ###################### # Allow VPN (GRE protocol) inbound pass in on $wan proto gre all keep state # Allow SSH and ICMP from WAN (restrict SSH source in production) pass in on $wan inet proto tcp from any to ($wan) port 22 pass in on $wan inet proto icmp from any to ($wan) icmp-type echoreq ###################### ### Pass OUT rules ### ###################### # NAT outbound traffic — masquerade all LAN traffic behind WAN IP pass out on $wan from any nat-to ($wan) state # Allow LAN-to-LAN pass out on $lan

Reload PF rules at any time without interrupting connections:

pfctl -f /etc/pf.conf # reload rules pfctl -sr # show current rules pfctl -ss # show current state table
The SSH rule above (pass in from any) allows SSH from the internet. For production, restrict this: pass in on $wan inet proto tcp from <trusted_ip> to ($wan) port 22
Use set block-policy return instead of the default drop. With return, PF sends an immediate TCP reset or ICMP unreachable to blocked connections rather than silently dropping them — resulting in faster failure feedback for clients instead of waiting for a connection timeout.
🔄
relayd Reverse Proxy

relayd is OpenBSD's built-in relay daemon — it handles reverse proxying, load balancing, and TLS termination. Used here to forward inbound HTTPS to internal services. Note that relayd depends on PF and must run on the same OpenBSD instance; it cannot be used independently of the PF firewall.

# /etc/relayd.conf — basic HTTPS relay to an internal Nginx relay "https-relay" { listen on 0.0.0.0 port 18443 tls forward to 10.10.2.201 port 443 }

Enable and start:

rcctl enable relayd rcctl start relayd
🚀
Going Production
Test as a downstream router first. Before cutting over production internet, assign the OpenBSD VM a spare VLAN and secondary subnet. Bring up only that VLAN initially and verify DHCP, routing, and internet access work correctly. A common early issue is a single VLAN (often a storage or data VLAN) failing to route while others work — identifying this in a test environment prevents a production outage. Only promote to the primary router once all VLANs are confirmed stable.
  1. 1

    Test as Downstream Router First

    Assign the OpenBSD VM a spare VLAN and secondary subnet. Plug a test device into that VLAN and verify it gets a DHCP lease, can ping the OpenBSD gateway, and has internet. Only then proceed to cut over production.

  2. 2

    Move the Modem Connection

    Point the ISP modem/ONT to the switch port or VLAN that feeds the OpenBSD VM's WAN interface. Verify vio0 gets a public IP via DHCP from the ISP.

  3. 3

    Verify DHCP Leases on All VLANs

    cat /var/db/dhcpd.leases # confirm leases being issued rcctl check dhcpd
  4. 4

    Monitor PF in Real-Time

    pfctl -ss | grep ESTABLISHED # active connections tcpdump -n -e -i vio0 # watch WAN traffic
  5. 5

    Shut Down Your Existing Router

    Once all VLANs are routing correctly and internet is stable, shut down your previous router. Keep it powered off but available (or as a snapshot/backup) for quick rollback during the first few days.

Full production deployment achieved — all home networking running through a virtualized OpenBSD PF firewall on bhyve. Traffic flows: Phone → WiFi AP → Unifi switch → Mikrotik switch → OpenBSD VM router → modem → internet.