Synopsis #
A virtual LAN (VLAN) separates one Ethernet link into tagged Layer 2 networks. OpenBSD represents each tagged network with a vlan(4) interface. This guide configures two VLANs on one trunk link, enables IPv4 routing, applies an explicit pf.conf(5) policy, and verifies traffic at each layer.
The connected switch must already carry the selected VLAN identifiers as tagged traffic. Management access must not depend on the rules being changed. Keep a console or an established recovery session available until a new administrative session succeeds.
Example Topology #
The example uses:
| Role | Interface or network |
|---|---|
| Internet uplink | em0, configured separately |
| VLAN trunk | em1 |
| Administrative VLAN | VLAN 10, 10.10.10.0/24 |
| Service VLAN | VLAN 20, 10.20.20.0/24 |
| Router addresses | 10.10.10.1 and 10.20.20.1 |
Replace every interface, VLAN identifier, address, and policy with values from the actual network plan.
Configure the Trunk and VLAN Interfaces #
Leave the physical trunk without an IP address. Create /etc/hostname.em1:
up
Create /etc/hostname.vlan10:
parent em1
vnetid 10
inet 10.10.10.1 255.255.255.0
description "administration"
up
Create /etc/hostname.vlan20:
parent em1
vnetid 20
inet 10.20.20.1 255.255.255.0
description "services"
up
The file names do not have to match the VLAN identifiers, but matching names make the configuration easier to audit. Apply the three interfaces with netstart(8) :
doas sh /etc/netstart em1 vlan10 vlan20
Confirm the parent, tag, addresses, and link state with ifconfig(8) :
ifconfig em1
# Confirm that the physical trunk is active
ifconfig vlan10
# Confirm parent em1, vnetid 10, and 10.10.10.1/24
ifconfig vlan20
# Confirm parent em1, vnetid 20, and 10.20.20.1/24
Enable IPv4 Forwarding #
Add the following line to /etc/sysctl.conf:
net.inet.ip.forwarding=1
Apply it immediately with sysctl(8) :
doas sysctl net.inet.ip.forwarding=1
Forwarding only permits the kernel to route packets. PF still determines which traffic may cross between networks.
Add a PF Policy #
Integrate the following policy into the complete /etc/pf.conf; do not replace an established ruleset without reviewing its existing macros, tables, anchors, and management rules:
wan = "em0"
admin_if = "vlan10"
service_if = "vlan20"
admin_net = "10.10.10.0/24"
service_net = "10.20.20.0/24"
local_nets = "{ 10.10.10.0/24, 10.20.20.0/24 }"
set skip on lo
block all
pass out on $wan inet from $local_nets nat-to ($wan)
pass out on $service_if from $admin_net to $service_net
pass in quick on $admin_if inet proto icmp from $admin_net to ($admin_if) icmp-type echoreq
pass in quick on $admin_if proto { tcp udp } from $admin_net to ($admin_if) port domain
pass in quick on $admin_if proto tcp from $admin_net to ($admin_if) port ssh
block in quick on $admin_if from $admin_net to self
pass in on $admin_if from $admin_net to any
pass in quick on $service_if inet proto icmp from $service_net to ($service_if) icmp-type echoreq
block in quick on $service_if from $service_net to self
block in quick on $service_if from $service_net to $admin_net
pass in on $service_if from $service_net to any
This example permits both VLANs to test their own router address, permits the administrative VLAN to use SSH and an optional local DNS resolver, permits administrators to reach the service network, prevents the service network from initiating other connections to the router or administrative network, and provides outbound Internet access through IPv4 network address translation. It does not configure DHCP or DNS service; a permitted port has no effect unless a daemon is bound to that address.
Traffic from the administration VLAN to the service VLAN must pass PF rules on both interfaces. The outgoing rule allows this path. Connections started from the service VLAN to the administration VLAN remain blocked.
Parse the complete ruleset before loading it with pfctl(8) :
doas pfctl -nf /etc/pf.conf
# Reject syntax errors without changing the active ruleset
doas pfctl -f /etc/pf.conf
# Load the rules only after parsing succeeds
Keep the previous administrative session open. Test a new SSH connection from the administrative VLAN before ending the recovery session.
Verify Tagged Traffic and Routing #
Use tcpdump(8) on the physical trunk to confirm that tags arrive from the switch:
doas tcpdump -eni em1 vlan
Then inspect each logical interface:
doas tcpdump -ni vlan10
# Observe untagged packets after VLAN 10 decapsulation
doas tcpdump -ni vlan20
# Observe untagged packets after VLAN 20 decapsulation
From a host in each VLAN, verify its address, default gateway, DNS configuration, and reachability to the router address. Then test only the cross-VLAN and Internet paths that the PF policy permits.
Display routes, PF rules, and states when a test fails:
route -n show
# Confirm connected routes for both VLAN networks
doas pfctl -vvsr
# Inspect rule counters
doas pfctl -ss
# Inspect active states
No tagged packets on em1 indicates a switch, cabling, parent-interface, or VLAN-identifier problem. Packets on a VLAN interface but no forwarding indicate an address, route, forwarding, or PF problem.
Operational Notes #
- A switch access port normally carries one untagged network and does not replace the tagged trunk used here.
- Native or untagged traffic on the trunk requires a separate, explicit design. Do not assume a switch’s native VLAN behavior.
- DHCP service requires one subnet declaration per served VLAN and explicit daemon interface flags.
- IPv6 requires routed prefixes, router advertisements, ICMPv6, and a separate PF policy. Route IPv6 Networks without NAT covers that design.
- VLAN separation does not itself enforce policy. PF, switch policy, and service binding must agree.
See Networking for the concise VLAN interface reference and Troubleshoot PF, NAT, and Routing for a layer-by-layer diagnostic procedure.