OpenBSD Handbook

    Theme
    • Part I. Install & Configure
      • Introduction
      • Installing OpenBSD
      • The X Window System
      • Networking
      • System Configuration
      • OpenBSD Basics
      • Managing Software: Packages and Ports
    • Part II. Daily Operations
      • Graphical Environments
      • Multimedia
      • Printing
      • Linux Compatibility
      • Windows Compatibility
      • Games
    • Part III. System Administration
      • Virtualization
      • Storage and File Systems
      • Updating and Upgrading
      • Localization
      • The OpenBSD Boot Process
      • Security
    • Part IV. Networking & Daemons
      • Services
        • Database
          • MariaDB
          • PostgreSQL
          • Redis
          • memcached
        • Directory
          • YP (NIS)
          • LDAP
        • File
          • NFS
          • Samba
        • FTP Services
          • ftpd
          • ProFTPD
          • vsftpd
          • Pure-FTPd
          • TFTP
        • Mail
          • smtpd
          • Dovecot
          • Postfix
          • Exim
          • Rspamd
        • Name
          • Unbound
          • NSD
          • BIND
        • Networking
          • OpenBGPD
          • rad
          • DHCP
          • slaacd
        • Web
          • Apache
          • nginx
          • httpd
          • relayd
        • Logging
          • syslogd
        • Monitoring
          • SNMP
        • Remote Access
          • Audit OpenSSH
          • sshd
        • File Synchronization
          • rsync
        • Messaging
          • RabbitMQ
        • Time
          • NTP
      • PF
        • pfctl cheat sheet
        • PF Anchors
        • PF Filter Rules
        • PF Forwarding
        • PF Lists and Macros
        • PF Load Balancing
        • PF Logging
        • PF NAT
        • PF Options
        • PF Policies
        • PF Shortcuts
        • PF Tables
      • Advanced Networking
        • High Availability and State Replication
        • Multi-WAN and Policy-Based Routing
        • VPN and Cryptographic Tunneling
        • Classic and Lightweight Tunnels
        • IPv6 at Scale
        • QoS and Traffic Shaping
        • MPLS and Label Distribution
        • Network Services at Scale
        • Virtualization and Host Networking
        • Large-Scale L2 and L3 Design
        • Telemetry, Logging, and Flow Export
        • Hardening and Operational Safety
        • Reference Architectures
        • Troubleshooting Playbooks
      • Serial Communication
    • Part V. Miscellaneous
      • Virtualization Cheat Sheet
      • OpenBSD Cheatsheet
      • How-to Guides
        • Complete the First 30 Minutes After Installation
        • Check Hardware Compatibility Before Installing
        • Bootstrap Wi-Fi Firmware Without Ethernet
        • Set Up OpenBSD as a Laptop or Workstation
        • Configure Laptop Power Management
        • Run OpenBSD as a Virtual Machine Guest
        • Install OpenBSD on a VPS or Cloud Server
        • Install OpenBSD with Full-Disk Encryption
        • Troubleshoot Web Browsers on OpenBSD
        • Create an Administrator Account with doas and SSH Keys
        • Establish a Conservative Security Baseline
        • Manage OpenBSD with Ansible
        • Recover Access and Reset the Root Password
        • Collect OpenBSD Diagnostic Evidence
        • Configure Secure Remote Access with OpenSSH
        • Set Up WordPress
        • Build Third-Party Software from Ports
        • Use FIDO Security Keys with OpenSSH
        • Build OpenBSD from Source
        • Configure Wi-Fi Roaming and Wired Failover
        • Dual-Boot OpenBSD on a UEFI System
        • Troubleshoot PF, NAT, and Routing
        • Automate OpenBSD Installation with Autoinstall and PXE
        • Route Multiple VLANs with PF
        • Configure a Road-Warrior WireGuard VPN
        • Troubleshoot Package Installation and Update Failures
        • Route IPv6 Networks without NAT
        • Schedule Recurring Maintenance
        • Plan a Docker Workload Migration to OpenBSD
        • Build a Mail Server with OpenSMTPD, Dovecot, and Rspamd
        • Build a Simple Router and Firewall
        • Monitor an OpenBSD System
        • Back Up and Restore an OpenBSD System
        • Configure softraid RAID1
        • Recover an OpenBSD System That Does Not Boot
        • Install Z shell (zsh)
      • OpenBSD for Linux Users
      • OpenBSD for FreeBSD Users
      • OpenBSD for macOS Users
    • OpenBSD FAQ
    • Package Search
      • Synopsis
      • Example Topology
      • Enable IPv4 Forwarding
      • Configure Outbound NAT
      • Add an Inbound Port Forward
      • Complete Example
      • Validate and Load the Rules
      • Test Outbound NAT
      • Test the Port Forward Externally
      • Internal Access to the Published Name
      • One-to-One Translation
      • Troubleshooting
      • IPv6

      PF NAT

      Synopsis #

      Network Address Translation (NAT) changes packet addresses as traffic crosses an OpenBSD router. Outbound NAT commonly allows an IPv4 network using private addresses to share one public address. Redirection, expressed with rdr-to in pf.conf(5) , forwards inbound connections to another address or port.

      Translation and filtering are separate decisions. A packet must match the intended translation and a pass rule. NAT is not a security boundary, and ordinary routed IPv6 should not be replaced by IPv4-style NAT without a specific design requirement.

      Example Topology #

      The examples use:

      RoleInterface or address
      External interfacevio0
      Internal interfacevio1
      LAN192.168.10.0/24
      Router LAN address192.168.10.1
      Internal HTTPS server192.168.10.20
      Published external portTCP 443

      Replace every interface and address with values from the actual system. Confirm them with ifconfig(8) and route(8) .

      Enable IPv4 Forwarding #

      The kernel must forward IPv4 packets between interfaces. Set the runtime value with sysctl(8) and add the persistent setting to /etc/sysctl.conf:

      # sysctl net.inet.ip.forwarding=1
        # Enable IPv4 forwarding now
      # echo 'net.inet.ip.forwarding=1' >> /etc/sysctl.conf
        # Enable it at subsequent boots
      

      Do not enable IPv6 forwarding unless the host also has an intentional IPv6 routing and PF policy.

      Configure Outbound NAT #

      A concise default-block ruleset can use macros for the topology:

      ext_if = "vio0"
      int_if = "vio1"
      lan_net = "192.168.10.0/24"
      
      set skip on lo
      
      block return
      
      match out on $ext_if inet from $lan_net to any nat-to ($ext_if)
      
      pass in on $int_if inet from $lan_net to any
      pass out on $ext_if inet received-on $int_if
      

      The parentheses around ($ext_if) cause PF to track address changes on a dynamically configured external interface. The match rule makes nat-to sticky. Translation changes the source address before the outbound filter decision, so the outbound rule identifies forwarded traffic by its ingress interface with received-on rather than matching the original private source address.

      The internal clients must use 192.168.10.1 as their default gateway.

      Add an Inbound Port Forward #

      Add the internal server macro and a combined redirection and pass rule:

      web_server = "192.168.10.20"
      
      pass in on $ext_if inet proto tcp to ($ext_if) port 443 \
          rdr-to $web_server port 443
      

      The rule changes the destination to 192.168.10.20 and permits the connection. The resulting state handles return traffic.

      Confirm that the server:

      • listens on 192.168.10.20 or an appropriate wildcard address;
      • uses 192.168.10.1 as its route back to the external client, or otherwise returns through the same PF router;
      • permits the connection in any host-local policy;
      • is not already reached through a conflicting rule earlier in the ruleset.

      Complete Example #

      The combined example provides outbound IPv4 NAT and one inbound HTTPS forward:

      ext_if = "vio0"
      int_if = "vio1"
      lan_net = "192.168.10.0/24"
      web_server = "192.168.10.20"
      
      set skip on lo
      
      block return
      
      match out on $ext_if inet from $lan_net to any nat-to ($ext_if)
      
      pass in on $int_if inet from $lan_net to any
      pass out on $ext_if inet received-on $int_if
      
      pass in on $ext_if inet proto tcp to ($ext_if) port 443 \
          rdr-to $web_server port 443
      

      A production ruleset normally also contains explicit ICMP policy, management access, logging, antispoofing decisions, and service-specific restrictions. Add those controls according to the router’s role rather than treating this example as a universal firewall policy.

      Validate and Load the Rules #

      Use pfctl(8) to parse before loading:

      # pfctl -nf /etc/pf.conf
        # Check syntax without changing the active ruleset
      # pfctl -f /etc/pf.conf
        # Load the complete validated ruleset
      # pfctl -vvsr
        # Display active filter rules and counters
      # pfctl -sn
        # Display active translation rules
      

      Keep console access while changing a remote firewall.

      Test Outbound NAT #

      From a LAN client, test the router, a public IP address, and DNS separately:

      $ ping -c 3 192.168.10.1
        # Confirm LAN reachability to the router
      $ ping -c 3 <known-reachable-ip>
        # Confirm routing without relying on DNS
      $ host openbsd.org
        # Confirm resolver operation
      

      On the router, inspect states and capture external traffic:

      # pfctl -ss
        # Confirm that the client connection created state
      # tcpdump -n -e -ttt -i vio0 'host <known-reachable-ip>'
        # Confirm that translated traffic leaves the external interface
      

      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. The capture should show the router’s external address as the source, not the client’s private address.

      Test the Port Forward Externally #

      Use a client on a genuinely external network. A test from the LAN to the router’s external address does not cross the external interface and therefore does not exercise the same rule.

      Capture both sides of the router with tcpdump(8) :

      # tcpdump -n -e -ttt -i vio0 'tcp port 443'
        # Observe the original external connection
      # tcpdump -n -e -ttt -i vio1 'host 192.168.10.20 and tcp port 443'
        # Observe the redirected connection toward the server
      

      If the request reaches the server but no reply returns, inspect the server service and its route back to the client.

      Internal Access to the Published Name #

      The preferred design is split-horizon DNS: internal clients resolve the public service name directly to 192.168.10.20, while external clients resolve it to the public address. This avoids unnecessary translation and keeps the internal packet path explicit.

      When split DNS is not possible, reflection requires both redirection and source NAT because the client and server share the same interface:

      pass in on $int_if inet proto tcp from $lan_net \
          to ($ext_if) port 443 rdr-to $web_server port 443
      
      pass out on $int_if inet proto tcp to $web_server port 443 \
          received-on $int_if nat-to ($int_if)
      

      The second rule forces replies back through the PF router. The internal server therefore sees the router’s LAN address, not the original client address. Use this only when that loss of source identity is acceptable.

      One-to-One Translation #

      The binat-to option creates a bidirectional mapping between an internal and external address:

      pass on $ext_if from 192.168.10.30 to any binat-to 198.51.100.30
      

      The external address must be routed to the OpenBSD system and included in the network design. One-to-one translation does not replace a filter policy.

      Troubleshooting #

      Inspect the packet path in this order:

      1. interface link and addresses;
      2. client and router routes;
      3. net.inet.ip.forwarding;
      4. pfctl -nf /etc/pf.conf;
      5. active filter and translation rules;
      6. state-table entries;
      7. captures on ingress and egress;
      8. destination service and reverse route;
      9. resolver behavior.

      Do not disable PF or flush all states as the first diagnostic step. Troubleshoot PF, NAT, and Routing provides a complete observation-driven workflow.

      IPv6 #

      Globally routable IPv6 networks normally use routing and filtering without address translation. Configure IPv6 forwarding, router advertisements or static routes, and an explicit PF policy only when the complete IPv6 path is understood. Do not copy the IPv4 NAT rules and substitute inet6.

      Report a bug
      • Synopsis
      • Example Topology
      • Enable IPv4 Forwarding
      • Configure Outbound NAT
      • Add an Inbound Port Forward
      • Complete Example
      • Validate and Load the Rules
      • Test Outbound NAT
      • Test the Port Forward Externally
      • Internal Access to the Published Name
      • One-to-One Translation
      • Troubleshooting
      • IPv6