Synopsis #
Routing and Packet Filter failures often involve several layers at once. A port forward can fail because the interface has no address, the route is wrong, forwarding is disabled, PF loaded a different rule, an old state remains, the service is not listening, or the test never crossed the external interface.
This guide follows the packet path in order. It begins with read-only inspection and changes one variable at a time. It does not disable PF, flush every state, or replace the ruleset on an exposed remote host.
Record the Topology and Symptom #
Write down:
- source and destination addresses;
- source and destination ports;
- the expected ingress and egress interfaces;
- the router addresses on each network;
- whether the failure affects routing, DNS, one protocol, or one direction;
- the last known-good configuration.
Test both an IP address and a hostname. If the IP works but the name fails, investigate the resolver before PF.
Check Links and Addresses #
Use ifconfig(8) :
$ ifconfig
# Confirm interface flags, link state, addresses, and groups
$ ifconfig -A
# Display aliases and all address families
An interface expected to carry traffic should be UP and normally RUNNING, with the intended address and prefix.
Check Routes #
Use route(8) to inspect the selected path:
$ route -n show
# Inspect the complete routing table without DNS lookups
$ route -n get 198.51.100.25
# Show the route selected for one destination
On a client behind the router, confirm that the default gateway is the OpenBSD system. On the OpenBSD router, confirm routes to both the source and destination networks.
Check Packet Forwarding #
For an IPv4 router, inspect the forwarding control with sysctl(8) :
$ sysctl net.inet.ip.forwarding
The value must be 1 for IPv4 forwarding. IPv6 routing uses a separate control:
$ sysctl net.inet6.ip6.forwarding
Enable only the address family that the router is configured to route, and persist the decision in /etc/sysctl.conf.
Validate the PF Configuration #
Parse the file without loading it:
# pfctl -nf /etc/pf.conf
Then inspect the running filter and translation rules with pfctl(8) :
# pfctl -si
# Confirm PF status and global counters
# pfctl -vvsr
# Show active filter rules, counters, and evaluations
# pfctl -sn
# Show active translation rules
If the file and active rules differ, determine why before reloading. Rules later in the file can replace earlier decisions unless quick stops evaluation. Translation does not compensate for a missing pass decision.
Inspect States #
Display the state table:
# pfctl -ss
An established state can preserve behavior after a rule change. Avoid flushing the entire table on a production firewall. Reproduce the test with a new source port or remove only the relevant state after confirming the effect on active users.
Capture Each Hop #
tcpdump(8)
shows whether the packet reaches and leaves each interface. For an inbound HTTPS forward from vio0 to server 192.168.10.20 on vio1:
# tcpdump -n -e -ttt -i vio0 'tcp port 443'
# Confirm that the external packet reaches the router
# tcpdump -n -e -ttt -i vio1 'host 192.168.10.20 and tcp port 443'
# Confirm that the translated packet leaves toward the server
Run the captures in separate console sessions while generating one controlled test.
Interpret the first missing observation:
- absent on ingress: upstream routing, addressing, switch, provider, or client problem;
- present on ingress but absent on egress: route, forwarding, PF, or translation problem;
- present on both but no reply: service, host firewall, server route, or server application problem;
- reply reaches the router but not the source: state, reverse route, or asymmetric-path problem.
Confirm the Destination Service #
On the destination host, use netstat(1) to confirm that the application listens on the intended address and port:
$ netstat -na -f inet | grep '\.443 .*LISTEN'
A service bound only to 127.0.0.1 cannot receive a forward to the host’s LAN address.
Test from the Correct Location #
Test an Internet-facing port forward from a genuinely external network. A client on the LAN that connects to the router’s external address does not traverse the same path. Use split-horizon DNS for internal clients or configure deliberate reflection rules as described in PF NAT .
Check DNS Separately #
Compare direct reachability and name resolution:
$ ping -c 3 <known-reachable-ip>
# Test routing without DNS
$ host service.example.com
# Test resolver behavior
Replace <known-reachable-ip> with an address that is expected to answer in the deployment. Documentation prefixes such as 198.51.100.0/24 are not reachability targets.
Inspect /etc/resolv.conf and current route proposals. Do not make the file immutable; resolvd(8)
manages it and preserves user-edited lines.
Apply and Verify One Change #
After correcting /etc/pf.conf:
# pfctl -nf /etc/pf.conf
# Parse before mutation
# pfctl -f /etc/pf.conf
# Load the complete validated ruleset
# pfctl -vvsr
# Confirm the expected active rule and counters
Repeat the same packet capture and client test. Record the change that resolved the failure so that the persistent configuration, monitoring, and recovery notes remain consistent.
For the configuration itself, see PF NAT , Packet Filter , and Build a Simple Router and Firewall .