OpenBSD: Networking and Routing

OpenBSD: Networking and Routing

OpenBSD routing with ospfd, the dhcpd DHCP server, IPv6 configuration, and network troubleshooting. A practical guide to stable and secure networking.

Routing is the backbone of all network communication. OpenBSD provides a transparent, auditable framework with a clear separation between kernel and user-space functionality. The kernel routing table manages packet forwarding, ospfd and bgpd implement dynamic routing protocols, and dhcpd automatically distributes IP addresses. IPv6 support is fully integrated.

The article on System Configuration explains basic network configuration using hostname.if files. This article covers routing concepts, DHCP server operation, IPv6 configuration, and network troubleshooting. The Network Fundamentals series introduces the underlying concepts—in this article, we focus on their OpenBSD-specific implementation.

Routing Fundamentals

Kernel Routing Table

The kernel routing table determines how packets are forwarded. OpenBSD stores routes in kernel memory, while persistent configuration is maintained in configuration files.

Display the current routing table:

$ route -n show
Routing tables

Internet:
Destination        Gateway            Flags   Refs      Use   Mtu  Prio Iface
default            192.168.1.1        UGS        0        0     -     8 vio0
224/4              127.0.0.1          URS        0        0 32768     8 lo0
127/8              127.0.0.1          UGRS       0        0 32768     8 lo0
127.0.0.1          127.0.0.1          UHhl       1        2 32768     1 lo0
192.168.1/24       192.168.1.65       UCn        2        0     -     4 vio0
192.168.1.1        cc:32:e5:e9:1b:0b  UHLch      1        2     -     3 vio0
192.168.1.65       52:54:00:ab:cd:09  UHLl       0        3     -     1 vio0
192.168.1.255      192.168.1.65       UHb        1        2     -     1 vio0
[...]
Internet6:
[IPv6 entries omitted—lo0 internal routes for compatibility addresses]

The columns represent:

  • Destination – Destination network or host
  • Gateway – Next hop; for directly connected hosts, the MAC address
  • Flags – Active route attributes (see below)
  • Refs – Number of active references to this route
  • Use – Number of packets forwarded through this route
  • Mtu – Path MTU; - indicates the interface default
  • Prio – Routing priority; lower values are preferred
  • Iface – Outgoing interface

Priority values follow a fixed scheme:

  • 1 – Reserved for the kernel (local addresses, lo0)
  • 3 – ARP-learned host routes
  • 4 – Directly connected network routes
  • 8 – Default route and kernel-internal routes

The flags describe the state of each route. The most important ones in day-to-day use are:

Flag Name Meaning
U UP Route is active
G GATEWAY Packets are forwarded through a gateway
S STATIC Manually configured route
H HOST Route to a single host rather than a network
L LLINFO Link-layer (MAC) address is known
C CLONING Creates new host routes as needed
c CLONED Generated from a cloning route
n CONNECTED Directly connected interface
h CACHED Referenced by a gateway route
l LOCAL Local address of this system
b BROADCAST Local broadcast address

The default route (default) represents 0.0.0.0/0. It is used for all packets that do not match a more specific route and points to the gateway on the local network.

Static Routes

Static routes define explicit paths for specific networks. They are configured in /etc/hostname.if files.

Default route via the interface configuration:

# /etc/hostname.vio0
inet 192.168.1.10/24
!route add default 192.168.1.1

The exclamation mark (!) executes commands during interface activation. The default route is added after the IP configuration has been applied.

Additional network routes for multi-subnet environments:

# /etc/hostname.vio0
inet 192.168.1.10/24
!route add default 192.168.1.1
!route add 10.0.0.0/8 192.168.1.254
!route add 172.16.0.0/12 192.168.1.254

Private networks are routed through a dedicated gateway, while the default route handles Internet traffic separately.

Host-specific route for a single IP address:

!route add -host 203.0.113.1 192.168.1.253

Traffic destined for this host takes an alternate path, which is useful for policy routing or VPN configurations. The -host modifier ensures that the address is interpreted as a host route.

ℹ️ Info

IP routing follows the longest prefix match principle: the most specific route always wins.

A /32 route (single host) takes precedence over a /24 network route, which in turn takes precedence over the default route (/0).

Default Route via /etc/mygate

The /etc/mygate file defines the system-wide default route. It is read by netstart(8) during boot, after all interfaces have been configured and all hostname.if files have already been processed.

The simplest case—an IPv4 gateway:

# /etc/mygate
192.168.1.1

IPv4 and IPv6 can be combined, with one address per line:

# /etc/mygate
192.168.1.1
2001:db8:1::1

Only the first address of each address family is used. Any additional IPv4 or IPv6 entries are silently ignored.

As in hostname.if(5), !command directives can execute shell commands. This is useful for configuration steps that are only possible after interface initialization has completed:

# /etc/mygate
192.168.1.1
!doas rcctl start unbound
⚠️ Important

Do not combine /etc/mygate with !route add default in hostname.if files—both mechanisms install a default route. The result is a duplicate default route with undefined behavior.

If a hostname.if file contains inet autoconf, netstart automatically ignores IPv4 entries in /etc/mygate. The same applies to IPv6 entries when inet6 autoconf is used.

Enabling Routing Between Interfaces

By default, OpenBSD blocks packet forwarding between interfaces. To operate as a router, IP forwarding must be enabled.

Check the current status:

$ sysctl net.inet.ip.forwarding
net.inet.ip.forwarding=0
$ sysctl net.inet6.ip6.forwarding
net.inet6.ip6.forwarding=0

Temporarily enable forwarding for the running system:

$ doas sysctl net.inet.ip.forwarding=1
$ doas sysctl net.inet6.ip6.forwarding=1

Enable it permanently through /etc/sysctl.conf:

net.inet.ip.forwarding=1
net.inet6.ip6.forwarding=1

IPv4 and IPv6 forwarding are controlled independently. The entries in /etc/sysctl.conf become active after the next reboot. For the currently running system, the sysctl command is required.

Policy Routing with rdomains

Routing domains (rdomains) create separate kernel routing tables. Each domain has its own routes and default gateway. Interfaces are permanently assigned to a domain, and processes can be started explicitly within a specific routing domain.

A common use case is a multi-WAN setup with two uplinks, two independent routing tables, and no interaction between them.

Basic configuration in the interface files:

# /etc/hostname.vio0
inet 192.168.1.10/24
rdomain 0
!route -T 0 add default 192.168.1.1

# /etc/hostname.vio1
inet 10.0.0.10/24
rdomain 1
!route -T 1 add default 10.0.0.1

Domain 0 is the default routing domain for all processes. Domain 1 exists alongside it with its own gateway, and the two routing tables operate completely independently.

Start a process explicitly within a routing domain:

$ doas route -T 1 exec /usr/sbin/unbound

The process and all of its child processes use only routing table 1.

All network traffic is sent through 10.0.0.1 instead of the default gateway in routing domain 0.

Display the active routing tables:

$ route -T 0 -n show -inet
Routing tables

Internet:
Destination        Gateway            Flags   Refs      Use   Mtu  Prio Iface
default            192.168.1.1        UGS        5      133     -     8 vio0
127/8              127.0.0.1          UGRS       0        0 32768     8 lo0
192.168.1/24       192.168.1.65       UCn        2      897     -     4 vio0
192.168.1.1        cc:32:e5:e9:1b:0b  UHLch      1      699     -     3 vio0
[...]

Each routing domain displays its own routing table. Default routes and directly connected networks are completely isolated from one another. Domain 1 exists only if it has been explicitly configured. Running route -T 1 on a system without rdomain 1 simply returns an empty routing table.

ℹ️ Info
pf(4) can use the rtable keyword to perform route lookups through a different routing table. This works only for incoming packets (in). Details of the pf integration are covered in the article Firewall and pf Fundamentals (Planed).

Dynamic Routing with ospfd

Routing Protocols

Static routes work well for small networks. Larger infrastructures require dynamic routing, where routes are exchanged automatically based on the network topology.

OpenBSD includes separate daemons for each routing protocol—all part of the base system, so no pkg_add installation is required:

  • ospfd(8) — Open Shortest Path First for internal networks (IGP)
  • bgpd(8) — Border Gateway Protocol for Internet routing (EGP)
  • ripd(8) — Routing Information Protocol, a legacy IGP
  • eigrpd(8) — Enhanced Interior Gateway Routing Protocol

OSPF Configuration

OSPF distributes routes within an organization. All routers in the same OSPF area exchange routing information.

Basic OSPF configuration in /etc/ospfd.conf:

router-id 10.0.0.1

area 0.0.0.0 {
	interface vio0
	interface vio1
}

The parameters define:

  • router-id — Unique router identifier, typically an interface IP address
  • area 0.0.0.0 — OSPF backbone area, required for simple configurations
  • interface — Interfaces on which OSPF is active

Set the correct permissions—ospfd refuses to start if the file permissions are incorrect:

doas chmod 640 /etc/ospfd.conf
doas chown root:_ospfd /etc/ospfd.conf

Enable and start the daemon:

doas rcctl enable ospfd
doas rcctl start ospfd

Check neighbors—FULL indicates that adjacency has been established completely:

doas ospfctl show neighbor
ID              Pri State        DeadTime Address         Iface     Uptime
10.0.0.2        1   FULL/DR      00:00:39 10.0.0.2        vio1      00:01:00
10.0.0.2        1   FULL/DR      00:00:39 192.168.1.65    vio0      00:01:00

The Routing Information Base displays the learned routes:

doas ospfctl show rib
Destination          Nexthop           Path Type    Type      Cost    Uptime
10.0.0.0/24          10.0.0.1        C Intra-Area   Network   10      00:02:13
192.168.1.0/24       192.168.1.62    C Intra-Area   Network   10      00:02:13

OSPF automatically updates the kernel routing table whenever the network topology changes. If a router fails, the network converges without manual intervention.

Adjust interface costs:

router-id 10.0.0.1

area 0.0.0.0 {
	interface vio0 {
		metric 100
	}
	interface vio1 {
		metric 10
	}
}

Lower costs make a route more attractive. OSPF calculates the shortest path based on the cumulative interface costs.

BGP Configuration

ℹ️ Info
BGP configuration under OpenBSD using bgpd(8) is covered in detail in the book Sovereign Systems. Incorrect BGP configuration can cause Internet-wide routing issues and requires a solid understanding of AS design and routing policy.

For small and medium-sized networks, OSPF is often entirely sufficient. BGP becomes valuable when routing between autonomous systems.

Routing Metrics and Priorities

Multiple routes to the same destination can have different priorities. OpenBSD uses metrics to determine which route should be selected.

Add routes with explicit priorities:

doas route add -priority 10 10.0.0.0/8 192.168.1.254
doas route add -priority 20 10.0.0.0/8 192.168.1.253

The lower priority value is preferred. The route through .254 becomes the primary path, while .253 serves as a backup.

Dynamic routing protocols assign priorities automatically:

  • OSPF: Based on interface costs (metric in ospfd.conf)
  • BGP: Based on AS path length and local preference (bgpd.conf)

DHCP Server with dhcpd

Basic DHCP Configuration

The dhcpd server automatically assigns IP addresses to clients. Configuration is stored in /etc/dhcpd.conf.

Basic configuration for a single subnet:

# /etc/dhcpd.conf
subnet 192.168.1.0 netmask 255.255.255.0 {
	range 192.168.1.100 192.168.1.200;
	option routers 192.168.1.1;
	option domain-name-servers 192.168.1.1;
	option domain-name "internal.local";
	default-lease-time 3600;
	max-lease-time 7200;
}

The parameters define:

  • subnet — Network served by the DHCP server
  • range — Pool of dynamically assigned IP addresses
  • option routers — Default gateway for clients
  • option domain-name-servers — DNS server list
  • default-lease-time — Default lease duration in seconds
  • max-lease-time — Maximum lease duration

Start the DHCP server:

doas rcctl enable dhcpd
doas rcctl set dhcpd flags vio0
doas rcctl start dhcpd

The vio0 flag specifies the interface that listens for DHCP requests. The server responds only on this interface.

⚠️ Important

Two independently configured DHCP servers on the same network segment compete when assigning leases. Existing clients may receive conflicting or incorrect network configurations. Before testing, verify whether another DHCP server is already active on the target network.

For redundant deployments, dhcpd(8) supports true high availability. Multiple instances can synchronize in real time using the -Y and -y options together with a shared key stored in /var/db/dhcpd.key. They then share a common lease database. This is a documented feature rather than a workaround—see the SYNCHRONISATION section of the dhcpd(8) manual page for details.

Static IP Assignments

Servers and network appliances generally require fixed IP addresses. DHCP can assign fixed addresses based on MAC addresses. The host declaration belongs inside the subnet block for which it applies:

subnet 10.0.0.0 netmask 255.255.255.0 {
	range 10.0.0.100 10.0.0.110;
	option routers 10.0.0.1;
	option domain-name-servers 10.0.0.1;
	default-lease-time 600;
	max-lease-time 1200;

	host client1 {
		hardware ethernet 52:54:00:12:34:06;
		fixed-address 10.0.0.50;
	}
}

The MAC address uniquely identifies the device. After modifying the configuration, dhcpd must be restarted (doas rcctl restart dhcpd). According to dhcpd(8), it does not support automatic configuration reloads via SIGHUP.

On the client side, dhcpleasectl confirms that the fixed address has been assigned:

doas dhcpleasectl vio1
vio1 [Bound]
	inet 10.0.0.50 netmask 255.255.255.0
	default gateway 10.0.0.1
	nameservers 10.0.0.1
	lease 10 minutes
	dhcp server 10.0.0.2
ℹ️ Info
Addresses assigned through host declarations do not appear in /var/db/dhcpd.leases. Unlike addresses allocated from the dynamic range pool, they are treated as static BOOTP-style assignments. The lease database records only dynamically allocated addresses.

Determine the MAC address of a device:

$ arp -a
Host                                 Ethernet Address    Netif Expire    Flags
printer.example.com                  52:54:00:12:34:05    vio0 19m58s

The ARP cache displays the MAC addresses of known hosts. Alternatively, check the device’s own network settings.

Multiple Subnets

A DHCP server can serve multiple IP subnets through the same interface if that interface carries addresses from each subnet (IP aliases) and the subnets are grouped using shared-network.

Add an alias address for the second subnet:

doas ifconfig vio1 alias 10.0.1.1/24

Configuration with two subnets in a shared network:

# /etc/dhcpd.conf
shared-network vio1-testnetz {
	subnet 10.0.0.0 netmask 255.255.255.0 {
		range 10.0.0.100 10.0.0.101;
		option routers 10.0.0.1;
		option domain-name-servers 10.0.0.1;
		default-lease-time 600;
		max-lease-time 1200;
	}
	subnet 10.0.1.0 netmask 255.255.255.0 {
		range 10.0.1.100 10.0.1.101;
		option routers 10.0.1.1;
		option domain-name-servers 10.0.1.1;
		default-lease-time 600;
		max-lease-time 1200;
	}
}

Within a shared-network block, all included subnets form a common address pool. Which subnet a client receives is not determined by the order in the configuration. During testing with three clients on the same segment, the leases were distributed as follows:

doas cat /var/db/dhcpd.leases
lease 10.0.0.100 {
	client-hostname "hans";
}
lease 10.0.1.100 {
	client-hostname "wilhelm";
}
lease 10.0.1.101 {
	client-hostname "xaver";
}

A client is not necessarily assigned an address from the first subnet, even if addresses are still available there. dhcpd distributes leases across all subnets that belong to the shared network.

ℹ️ Info
Alternatively, separate physical interfaces can each serve their own subnet without using shared-network. Each interface then has an independent subnet declaration and address pool. This approach is appropriate when the networks are already physically separated.

DHCP Relay for Remote Subnets

DHCP traffic is not routable. Broadcast packets do not cross subnet boundaries. A DHCP relay agent listens on the remote subnet and forwards requests directly to a DHCP server located on another network.

Enable the relay on the router that connects the client subnet with the DHCP server subnet:

doas rcctl enable dhcrelay
doas rcctl set dhcrelay flags "-i vio2 10.0.0.2"
doas rcctl start dhcrelay

The parameters define:

  • -i vio2 — Interface on which the relay agent receives DHCP broadcasts
  • 10.0.0.2 — IP address of the DHCP server to which requests are forwarded

Requests arriving from the vio2 network are forwarded to 10.0.0.2. The DHCP server sends its replies back through the same relay agent to the client.

⚠️ Important

The DHCP server requires a subnet declaration for every network from which it receives relayed requests, even if no dynamic address pool exists for that subnet. If the declaration is missing, dhcpd silently discards the request: no error message, no log entry, and the client remains in the Init state.

An empty declaration is sufficient to make the subnet known:

subnet 172.16.2.0 netmask 255.255.255.0 {
}

When placed inside the same shared-network block as the address pools, the server automatically assigns relayed clients addresses from the shared pool.

Packet-level verification: running tcpdump on the DHCP server interface shows the relayed request with the relay agent’s address in the G: field (Gateway/giaddr):

doas tcpdump -n -i vio1 port 67 or port 68
172.16.2.1.67 > 10.0.0.2.67: (request) hops:1 xid:0xd76edd47 G:172.16.2.1 ether 52:54:00:cc:cd:01 [|bootp]

The client on the remote subnet receives an address from the server’s address pool despite being physically separated. This can be confirmed on the client itself with dhcpleasectl.

DHCP Client with dhcpleased

On the client side, dhcpleased(8) performs DHCP-based IP configuration. OpenBSD no longer provides a separate dhclient utility.

Enable the daemon:

doas rcctl enable dhcpleased
doas rcctl start dhcpleased

The interface must explicitly tell dhcpleased to use DHCP through its hostname.if file:

# /etc/hostname.vio1
dhcp

After creating the file, OpenBSD verifies the file permissions and automatically corrects them if necessary:

doas sh /etc/netstart vio1
WARNING: /etc/hostname.vio1 is insecure, fixing permissions.
⚠️ Important

Simply bringing an interface down and back up with ifconfig vio1 down / up does not reload /etc/hostname.vio1. Changes to the file only take effect after running netstart or after a reboot.

If the interface was previously configured with a static address, switching to dhcp does not automatically remove the old address. Both inet entries remain active simultaneously. For a clean migration, remove the old address manually (ifconfig vio1 delete <old-IP>).

Display the current lease status on the client:

doas dhcpleasectl vio1
vio1 [Bound]
	inet 10.0.0.100 netmask 255.255.255.0
	default gateway 10.0.0.1
	nameservers 10.0.0.1
	lease 10 minutes
	dhcp server 10.0.0.2

The output shows the assigned address, default gateway, DNS servers, remaining lease time, and the DHCP server that issued the lease.

Lease Management

Active DHCP leases are stored in /var/db/dhcpd.leases:

doas cat /var/db/dhcpd.leases
lease 10.0.0.100 {
	starts 1 2026/07/27 15:14:17 UTC;
	ends 1 2026/07/27 15:24:17 UTC;
	hardware ethernet 52:54:00:12:34:06;
	uid 01:52:54:00:12:34:06;
	client-hostname "client";
}

The file contains assigned IP addresses, MAC addresses, and lease times. Every lease renewal creates a new entry. Older entries for the same address remain in the file for auditability instead of being overwritten.

Clear the lease database (all clients will obtain new addresses):

doas rcctl stop dhcpd
doas rm /var/db/dhcpd.leases
doas touch /var/db/dhcpd.leases
doas rcctl start dhcpd

DHCP clients automatically renew their leases before expiration. Deleting the lease file forces a new DHCP negotiation.

IPv6 Configuration

IPv6 Addresses and Autoconfiguration

IPv6 uses 128-bit addresses instead of IPv4’s 32-bit addresses. Its addressing architecture supports hierarchical routing and automatic configuration.

Basic IPv6 autoconfiguration in /etc/hostname.if:

# /etc/hostname.vio2
inet6 autoconf
⚠️ Important
A bare inet6 line without an address and without autoconf does not match any format defined by hostname.if(5) and is silently ignored when the configuration is read. Even the automatic link-local address (fe80::/10) is not created. For automatic configuration, inet6 autoconf is required. For a static address, use inet6 <address> <prefixlen>.

With inet6 autoconf, the kernel automatically creates the link-local address and enables slaacd(8) for the interface. slaacd is a separate daemon from dhcpleased and is responsible exclusively for IPv6 autoconfiguration. Router Advertisement (RA) messages from the local router additionally configure global unicast addresses.

Display the current IPv6 addresses:

$ ifconfig vio2 inet6
vio2: flags=2a48843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,AUTOCONF6TEMP,AUTOCONF6,AUTOCONF4,LRO> mtu 1500
	lladdr 52:54:00:cc:cd:01
	inet6 fe80::5054:ff:fecc:cd01%vio2 prefixlen 64 scopeid 0x3
	inet6 2001:db8:2:0:47f0:731b:6691:7f2e prefixlen 64 autoconf pltime 2651 vltime 5351
	inet6 2001:db8:2:0:f185:af7d:7924:cdac prefixlen 64 autoconf temporary pltime 2651 vltime 5351

The fe80 address is link-local and valid only on the local segment. The two 2001:db8 addresses are global unicast addresses: one stable address derived from the interface identifier and one temporary privacy-extension address with a random suffix (see IPv6 Privacy Extensions). pltime and vltime indicate the preferred and valid lifetimes in seconds.

The process can be observed with tcpdump: Router Solicitation, Router Advertisement, and Duplicate Address Detection for the newly generated address:

$ doas tcpdump -n -i vio2 icmp6
fe80::5054:ff:fecc:cd01 > ff02::2: icmp6: router solicitation
fe80::5054:ff:fecc:cd08 > fe80::5054:ff:fecc:cd01: icmp6: router advertisement
:: > ff02::1:ff6a:49e9: icmp6: neighbor sol: who has 2001:db8:2:0:357a:c3ef:b66a:49e9
fe80::5054:ff:fecc:cd01 > ff02::2: icmp6: neighbor adv: tgt is 2001:db8:2:0:357a:c3ef:b66a:49e9

The Neighbor Solicitation message with source address :: verifies that the new address is not already in use on the local segment (Duplicate Address Detection). Only after this check completes does the address become active.

Static IPv6 Configuration

Servers generally require fixed IPv6 addresses. Manual configuration is similar to IPv4:

# /etc/hostname.vio2
inet6 2001:db8:2::10/64

CIDR notation works exactly as it does for IPv4. A /64 prefix is the standard subnet size.

Configure multiple IPv6 addresses with the alias keyword:

# /etc/hostname.vio2
inet6 autoconf
inet6 alias 2001:db8:2::30/64

The alias keyword adds an additional static address alongside the automatically generated one. This is useful when an interface should use SLAAC while also providing a stable address. IPv6 supports any number of addresses on a single interface.

Reload the configuration after editing the file:

doas sh /etc/netstart vio2

Verify the result:

$ ifconfig vio2 inet6
vio2: flags=2248843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,AUTOCONF6,LRO> mtu 1500
	lladdr 52:54:00:cc:cd:08
	inet6 fe80::5054:ff:fecc:cd08%vio2 prefixlen 64 scopeid 0x3
	inet6 2001:db8:2::30 prefixlen 64

Addresses without pltime or vltime, such as the one shown above, are statically configured. Unlike autoconfigured addresses, they do not expire automatically.

IPv6 Privacy Extensions

Standard IPv6 autoconfiguration uses the MAC address to derive interface identifiers, making long-term device tracking possible across networks.

Under OpenBSD, temporary addresses (RFC 8981) are enabled by default whenever autoconf is configured. No additional configuration is required. A simple inet6 autoconf line already creates both a stable address and a temporary one:

$ ifconfig vio2 inet6
vio2: flags=2a48843<...,AUTOCONF6TEMP,AUTOCONF6,...>
	inet6 2001:db8:2:0:47f0:731b:6691:7f2e prefixlen 64 autoconf pltime 2651 vltime 5351
	inet6 2001:db8:2:0:f185:af7d:7924:cdac prefixlen 64 autoconf temporary pltime 2651 vltime 5351

The AUTOCONF6TEMP flag indicates that a temporary address has been generated in addition to the stable autoconfigured address. The temporary address is identified by the temporary suffix. The kernel automatically replaces it after 24 hours. Outgoing connections prefer the temporary address, while incoming connections continue to use the stable address.

ℹ️ Info
The autoconfprivacy keyword found in older documentation is obsolete and no longer works reliably together with autoconf on the same line. The current keyword is temporary, which is enabled by default and can be disabled with -temporary.

Disable Privacy Extensions (useful for servers that require stable incoming addresses):

# /etc/hostname.vio2
inet6 autoconf
inet6 -temporary
$ ifconfig vio2 inet6
vio2: flags=2208843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,AUTOCONF6,LRO> mtu 1500
	inet6 fe80::5054:ff:fecc:cd08%vio2 prefixlen 64 scopeid 0x3
	inet6 2001:db8:2::30 prefixlen 64

The AUTOCONF6TEMP flag disappears, leaving only the stable address. Already existing temporary addresses are not removed immediately but expire naturally, as described in the manual page.

Router Advertisement Daemon

IPv6 routers send Router Advertisement (RA) messages for automatic client configuration. OpenBSD can operate as an IPv6 router using rad (Router Advertisement Daemon).

Basic RA configuration in /etc/rad.conf:

interface vio2 {
	prefix 2001:db8:2::/64
}

The parameters define:

  • interface — Outgoing interface for RA messages
  • prefix — IPv6 prefix advertised to clients

A dns block is optional and is only required if IPv6 DNS servers should also be distributed through Router Advertisements.

Start the Router Advertisement daemon:

doas rcctl enable rad
doas rcctl start rad

Reload the configuration without restarting the daemon:

doas ractl reload

Clients on the network automatically configure addresses from the advertised prefix. On the client side, this behavior can be observed through slaacd, as described in the previous sections.

Network Troubleshooting

Connectivity Tests

Verify basic network connectivity with ping:

$ ping -c 4 192.168.1.1
PING 192.168.1.1 (192.168.1.1): 56 data bytes
64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.108 ms
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=1.048 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=1.857 ms
64 bytes from 192.168.1.1: icmp_seq=3 ttl=64 time=1.384 ms
--- 192.168.1.1 ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 1.048/1.349/1.857/0.319 ms

The -c 4 option limits the test to four packets. TTL (Time To Live) indicates the remaining hop count before a packet expires. ping exits with status 0 as soon as at least one reply has been received. If all requests time out, it returns a non-zero exit status, making it useful for scripting.

Test IPv6 connectivity:

$ ping6 -c 4 2001:db8:2::30

The ping6 command works the same way for IPv6 addresses.

Traceroute

Trace the network path with traceroute:

$ traceroute 192.168.1.1
traceroute to 192.168.1.1 (192.168.1.1), 64 hops max, 40 byte packets
 1  kurt (192.168.1.1)  1.616 ms  1.602 ms  2.454 ms

Each line represents one hop to the destination. The latency values help identify slow network segments. In a simple lab network where all systems share the same bridge, the destination is often reached in a single hop. In production networks with multiple routers, one line appears for each hop. With multiple equal-cost paths (ECMP), a hop may show different responding addresses, one for each probe:

 3  198.51.100.1 (198.51.100.1)  29.987 ms  31.592 ms  39.918 ms
 4  198.51.100.5 (198.51.100.5)  42.059 ms  38.817 ms 198.51.100.9 (198.51.100.9)  40.03 ms
[... additional hops ...]

If a hop does not respond within the timeout period, * * * is displayed instead of an address and latency values.

ℹ️ Info
Unlike Linux and FreeBSD, OpenBSD’s traceroute(8) does not provide a -T option for TCP. The equivalent is -P proto, where proto is either a protocol number or a name from /etc/protocols. The manual page explicitly notes that this “does not work reliably for most protocols.” For IP-level troubleshooting, the default ICMP-based mode is generally the most reliable.

Traceroute using TCP instead of UDP:

$ doas traceroute -P tcp example.com
⚠️ Important

If an active pf rule (for example, the default policy block return all) blocks outgoing traffic from traceroute, the kernel does not silently drop locally generated packets. Instead, it immediately returns an error to the calling process:

traceroute: sendto: Permission denied

This primarily affects TCP- and UDP-based traceroute runs. To diagnose the issue, inspect the active rule set:

$ doas pfctl -sr | grep -i block
block return all

With block return instead of block drop, pf actively notifies the sender, which results in the immediate EPERM-style error instead of a timeout.

Checking Interface Status

Display detailed interface information:

$ ifconfig vio0
vio0: flags=2808843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,AUTOCONF4,LRO> mtu 1500
	lladdr 52:54:00:ab:cd:08
	index 1 priority 0 llprio 3
	groups: egress
	media: Ethernet autoselect
	status: active
	inet 192.168.1.62 netmask 0xffffff00 broadcast 192.168.1.255

The most important flags are:

  • UP — Interface is enabled
  • RUNNING — Driver is operational
  • MULTICAST — Multicast is supported
  • AUTOCONF4 — Interface receives its IPv4 address via DHCP (see the DHCP Client section)
  • LRO — Large Receive Offload, where the driver combines multiple incoming packets

status: active indicates that a physical link is present. status: no carrier means there is no network cable or link. Virtual interfaces such as vio under QEMU/KVM often do not display the duplex information normally shown after media: (for example, (1000baseT full-duplex)). This is expected and does not indicate a problem.

Packet Statistics

Identify network errors:

$ netstat -i
Name    Mtu   Network     Address              Ipkts Ifail    Opkts Ofail Colls
lo0     32768 <Link>                               0     0        0     0     0
lo0     32768 127/8       localhost                0     0        0     0     0
vio0    1500  <Link>      52:54:00:ab:cd:08     2762     0     2453     0     0
vio0    1500  192.168.1/2 hans.fridolin.lan     2762     0     2453     0     0
vio2    1500  <Link>      52:54:00:cc:cd:08      228     0       47     0     0
vio2    1500  172.16.2/24 172.16.2.1             228     0       47     0     0
vio2    1500  fe80::%vio2 fe80::5054:ff:fec      228     0       47     0     0
vio2    1500  2001:db8:2: 2001:db8:2::30         228     0       47     0     0
pflog0  33136 <Link>                               0     0        0     0     0

The columns represent:

  • Ipkts — Received packets
  • Ifail — Failed incoming packets
  • Opkts — Transmitted packets
  • Ofail — Failed outgoing packets
  • Colls — Collisions (primarily relevant for hubs, rarely for switched networks)

Each interface appears multiple times: one line for the link-layer statistics (<Link>) and one additional line for each configured address family or network. An interface configured with both IPv4 and IPv6 therefore appears several times. High failure counters usually indicate hardware problems or duplex mismatches.

ARP Table

Verify MAC address resolution:

$ arp -a
Host                                 Ethernet Address    Netif Expire    Flags
198.51.100.1                         52:54:00:12:34:01    vio1 permanent l
client.example.com                   52:54:00:12:34:02    vio0 19m58s
router.example.com                   52:54:00:12:34:03    vio0 permanent l

The columns show the hostname or IP address, the MAC address, and the interface. The l flag marks local or statically configured (permanent) entries. Entries without l are learned dynamically and expire after the displayed timeout.

Display only numeric addresses without DNS lookups:

$ arp -an

This is useful when DNS lookups slow down the output or when the resolver is unavailable.

Clear the ARP cache:

$ doas arp -d -a
198.51.100.1 (198.51.100.1) deleted
203.0.113.132 (203.0.113.132) deleted

Useful after MAC address changes or during network debugging. The cache is automatically rebuilt as needed.

Socket Statistics

Display active network connections:

$ netstat -an | grep ESTABLISHED
tcp          0     44  192.168.1.65.22        192.168.1.132.52174    ESTABLISHED
ℹ️ Info
OpenBSD’s netstat separates IP addresses and port numbers with a period (192.168.1.65.22) instead of the colon commonly used by many Linux tools.

The columns represent:

  • Recv-Q — Received bytes not yet read
  • Send-Q — Sent bytes not yet acknowledged
  • Local Address — Local IP/port pair
  • Foreign Address — Remote IP/port pair
  • State — TCP connection state

Display listening ports:

$ netstat -anl | grep LISTEN
tcp          0      0  *.22                   *.*                    LISTEN
tcp          0      0  127.0.0.1.25           *.*                    LISTEN
tcp6         0      0  fe80::1%lo0.25         *.*                    LISTEN
tcp6         0      0  ::1.25                 *.*                    LISTEN
tcp6         0      0  *.22                   *.*                    LISTEN

Services listening on all interfaces are shown as *.PORT. Specific addresses (127.0.0.1.PORT, ::1.PORT) limit the service to individual interfaces. IPv4 and IPv6 appear as separate entries (tcp versus tcp6).

For continuously updated live statistics:

$ systat netstat

systat displays network activity in real time and refreshes once per second, making it useful during traffic analysis.

Packet Analysis with tcpdump

Capture network traffic live:

$ doas tcpdump -n -i vio0
tcpdump: listening on vio0, link-type EN10MB
07:40:01.579084 192.168.1.65.22 > 192.168.1.132.52174: P 2031605786:2031605822(36) ack 150708228 win 271 <nop,nop,timestamp 4109385259 2708599083> (DF) [tos 0xb8]
07:40:01.590399 192.168.1.65.22 > 192.168.1.132.52174: P 36:112(76) ack 1 win 271 <nop,nop,timestamp 4109385269 2708599083> (DF) [tos 0xb8]
[... additional packets ...]

Parameters:

  • -n — Disable DNS lookups (faster)
  • -i vio0 — Monitor a specific interface

Filter traffic by port:

$ doas tcpdump -n -i vio0 port 22

Displays only traffic to and from port 22, making it useful for service-specific debugging.

Display ICMP traffic (for example, while running ping from another host):

$ doas tcpdump -n -i vio0 icmp
tcpdump: listening on vio0, link-type EN10MB
07:43:12.985240 192.168.1.62 > 192.168.1.65: icmp: echo request
07:43:12.985575 192.168.1.65 > 192.168.1.62: icmp: echo reply

This displays ping requests, Router Advertisements, and ICMP error messages.

Display packet payloads in hexadecimal and ASCII:

$ doas tcpdump -n -i vio0 -X icmp

The -X option displays packet payloads as both hexadecimal and ASCII. This is useful for protocol debugging, although encrypted traffic such as HTTPS remains unreadable.

ℹ️ Info
The order of options matters with tcpdump. The -i option always expects an interface name. Writing -i -X vio0 causes -X to be interpreted as the interface name, resulting in a “Failed to open bpf device” error. The correct syntax is -i vio0 -X, or place -X anywhere else except immediately after -i.

Testing DNS Resolution

Perform a DNS lookup:

$ host example.org
example.org has address 203.0.113.10
example.org mail is handled by 10 mail.example.org.

This displays the A record and, if present, the MX records for the domain. When troubleshooting, query a specific DNS server instead of the system resolver:

$ host example.org 192.168.1.1
Using domain server:
Name: 192.168.1.1
Address: 192.168.1.1#53

example.org has address 203.0.113.10

This helps determine whether the problem lies with the local resolver or the domain configuration itself. In the example network, the local unbound resolver performs this task. Public third-party resolvers such as 1.1.1.1 or 9.9.9.9 conflict with the sovereignty concept presented throughout this series and are best reserved for occasional comparison tests.

Perform a reverse DNS lookup:

$ host 203.0.113.10
10.113.0.203.in-addr.arpa domain name pointer example.org.

Maps an IP address back to its hostname. Reverse DNS is important for mail servers and certain authentication mechanisms.


Software and Versions

This guide is based on:

  • OpenBSD: 7.8, 7.9
  • ospfd: Included in the OpenBSD base system
  • dhcpd: Included in the OpenBSD base system
  • rad: Included in the OpenBSD base system
  • Context: Server and router systems
  • Status: July 2026

Michael of the Dragons

develops books, software, and open frameworks around technical systems, digital independence, and durable software architectures.
More about Michael →
OpenBSD: Networking and Routing
Previous Article → OpenBSD: Security Architecture and Protection Mechanisms