OpenBSD Handbook

    Theme
    • Part I. Install & Configure
      • Introduction to OpenBSD
      • Installing OpenBSD
      • The X Window System
      • Networking on OpenBSD
      • System Configuration
      • OpenBSD Basics
      • Managing Software: Packages and Ports
    • Part II. Daily Operations
      • OpenBSD Desktop 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
          • OpenSMTPD Mail Server
          • Dovecot IMAP and POP3 Server
          • Postfix
          • Exim
          • Rspamd
        • Name
          • Unbound DNS Resolver
          • NSD Authoritative DNS Server
          • BIND DNS Server
        • Networking
          • OpenBGPD
          • rad
          • DHCP on OpenBSD
          • slaacd
        • Web
          • Apache HTTP Server
          • nginx Web Server
          • OpenBSD httpd Web Server
          • relayd Reverse Proxy
        • Logging
          • syslogd
        • Monitoring
          • SNMP
        • Remote Access
          • Audit OpenSSH
          • OpenSSH Server
        • File Synchronization
          • rsync
        • Messaging
          • RabbitMQ Message Broker
        • Time
          • NTP Time Synchronization
        • Load Balancing
          • Build Redundant Load-Balanced Gateways
          • Load Balance Services with NAT or DSR
          • Build an HA HTTPS Load Balancer
      • 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
        • Installation and First Boot
          • Complete the First 30 Minutes After Installation
          • Check Hardware Compatibility Before Installing
          • Bootstrap Wi-Fi Firmware Without Ethernet
          • Run OpenBSD as a Virtual Machine Guest
          • Install OpenBSD on a VPS or Cloud Server
          • Install OpenBSD with Full-Disk Encryption
          • Dual-Boot OpenBSD on a UEFI System
          • Automate OpenBSD Installation with Autoinstall and PXE
        • Desktops and Laptops
          • Set Up OpenBSD as a Laptop or Workstation
          • Configure Laptop Power Management
          • Troubleshoot Web Browsers on OpenBSD
          • Use Plan 9 Tools on OpenBSD
          • Build a Minimal Desktop with suckless Tools
          • Install Z shell (zsh)
        • Networking and Remote Access
          • Create an Administrator Account with doas and SSH Keys
          • Configure Secure Remote Access with OpenSSH
          • Use FIDO Security Keys with OpenSSH
          • Configure Wi-Fi Roaming and Wired Failover
          • Troubleshoot PF, NAT, and Routing
          • Route Multiple VLANs with PF
          • Configure a Road-Warrior WireGuard VPN
          • Route IPv6 Networks without NAT
          • Test OpenBGPD with Two Providers
          • Unlock an Internal Service with an SSH Key
          • Build a Simple Router and Firewall
        • Servers and Services
          • Set Up WordPress on OpenBSD
          • Build a Mail Server with OpenSMTPD, Dovecot, and Rspamd
        • Security, Maintenance and Recovery
          • Establish a Conservative Security Baseline
          • Recover Access and Reset the Root Password
          • Collect OpenBSD Diagnostic Evidence
          • Troubleshoot Package Installation and Update Failures
          • Schedule Recurring Maintenance
          • Monitor an OpenBSD System
          • Back Up and Restore an OpenBSD System
          • Configure softraid RAID1
          • Recover an OpenBSD System That Does Not Boot
        • Building, Automation and Virtual Machines
          • Manage OpenBSD with Ansible
          • Build Third-Party Software from Ports
          • Build OpenBSD from Source
          • Plan a Docker Workload Migration to OpenBSD
      • OpenBSD for Linux Users
      • OpenBSD for FreeBSD Users
      • OpenBSD for macOS Users
    • OpenBSD FAQ
    • OpenBSD Architectures
      • OpenBSD Package Search
        • Synopsis
        • Prepare the Shared Network
        • Start with NAT
        • Verify Health Checks and Frontend Failover
        • Change to Direct Server Return
        • Check the Request and Reply Paths
        • Choose Between the Designs

        Load Balance Services with NAT or DSR

        Synopsis #

        Use relayd(8) to check backend health and configure PF to distribute service connections across healthy servers. Begin with destination NAT, where replies cross the frontend, then change to direct server return (DSR), where replies bypass it.

        The examples use IPv4 and a TCP web service so responses are easy to identify. PF forwards packets between the client and the selected server without terminating TLS or inspecting their HTTP requests. HTTP health checks are separate connections made by relayd.

        Prepare the Shared Network #

        One VIP, two reply pathsThe client sends requests to 198.51.100.100 through the active balancer and the selected backend. With NAT, replies return through the balancer. With DSR, the backend holds that VIP on loopback and replies through a separate router at 203.0.113.254, bypassing the balancer.One VIP, two reply pathsClient: 198.51.100.10Connects to 198.51.100.100Active balancerCARP VIP + PFSelected backendDSR: VIP on lo0198.51.100.100/32Return router203.0.113.254NAT replies cross the frontend.DSR replies use the return router.RequestRequestNATDSR
        With NAT, replies pass through the frontend. With DSR, replies bypass it through a separate router.

        Start with the addresses, CARP interfaces, pfsync link and PF rules from the redundant gateway guide . Use active/standby mode for this example. The frontend service address is 198.51.100.100; the backend gateway is 10.10.10.1.

        Configure servers 10.10.10.11/24 and 10.10.10.12/24. For NAT, both need a route to 198.51.100.0/24 through 10.10.10.1. Persist that route using hostname.if(5) or the appropriate default gateway for the network.

        On each backend, configure /etc/httpd.conf:

        server "app.example" {
            listen on 0.0.0.0 port 80
            root "/htdocs/app"
            log style forwarded
        }
        

        Create a page that identifies the server and a health-check endpoint. On the second server, use web2 instead of web1:

        doas install -d /var/www/htdocs/app
        printf '%s\n' web1 | doas tee /var/www/htdocs/app/index.html
        printf '%s\n' ok | doas tee /var/www/htdocs/app/health
        doas httpd -n
        doas rcctl enable httpd
        doas rcctl start httpd
        

        Permit the frontend health checks and intended client traffic in each backend’s PF policy. Check that each server responds directly before configuring the load balancer.

        Start with NAT #

        Use this /etc/relayd.conf on both frontends:

        interval 2
        timeout 1000
        log state changes
        table <web> { 10.10.10.11, 10.10.10.12 }
        redirect "web" {
            listen on 198.51.100.100 tcp port 80
            forward to <web> port 80 mode roundrobin check http "/health" code 200
        }
        

        The relayd/* PF anchor must be present on both nodes. Validate the configuration, then enable and start the service:

        doas relayd -n
        doas rcctl enable relayd
        doas rcctl start relayd
        doas relayctl show summary
        doas pfctl -a 'relayd/*' -sr
        

        From the client, make repeated fresh requests:

        for n in 1 2 3 4 5 6; do
            ftp -V -o - http://198.51.100.100/
        done
        

        Expect both backend identifiers while both hosts are healthy. The destination changes from the service IP to the selected server. Source NAT is not needed in this routed example, so the backend still sees the client’s address. Its reply returns through the translating firewall, which restores the service address as the source.

        If replies do not reach the client, check the backend’s return route. Source NAT is an alternative when that route cannot be changed, but the backend then sees the translated source address instead of the client’s address.

        Verify Health Checks and Frontend Failover #

        Make one backend’s /health endpoint unavailable while leaving its normal page running. Confirm it becomes down in relayctl show summary, and that new requests use the other server. Restore the endpoint and observe its return to the pool.

        relayd marks 10.10.10.12 down while 10.10.10.11 remains up, with the active PF redirection below
        A failed readiness endpoint removes one backend from the active pool while its HTTP service remains reachable.

        Choose a health endpoint that reports whether the application can handle requests. An open TCP port alone may not be enough. Measure recovery from the client: the check interval is only part of the delay before requests succeed again.

        Hold a client connection open, confirm its translated PF state appears on both frontends, and perform the controlled CARP takeover . Check that the original connection continues without reconnecting and that new requests work. If the selected backend fails instead, clients must reconnect to another server; pfsync cannot transfer the backend’s connection or application state.

        Change to Direct Server Return #

        DSR retains the original destination IP. Each selected backend must accept traffic for 198.51.100.100, normally through a loopback alias, while only the frontend pair answers for that address on the client network.

        On each standalone OpenBSD backend, add the service address to loopback and allow packets arriving on a physical interface to reach it:

        doas ifconfig lo0 inet 198.51.100.100/32 alias
        doas sysctl net.inet.ip.forwarding=2
        

        Persist the loopback alias in /etc/hostname.lo0 and the sysctl in /etc/sysctl.conf, merging existing entries. Mode 2 permits delivery to local addresses arriving on another interface without enabling general non-IPsec forwarding. Keep forwarding mode 1 on routers.

        Provide a return path that bypasses the balancers. In this example, add a second backend interface on 203.0.113.0/24: use .11 and .12 for the servers. A separate router has 203.0.113.254 on that network and 198.51.100.254 on the client network. It forwards between those networks and permits the reply traffic.

        That return router sees replies without the initiating SYN. Ordinary stateful TCP rules can reject those packets. With vio1 facing the return network and vio0 facing the clients, the following narrow PF example permits the one-way HTTP replies:

        set skip on lo
        block all
        pass in on vio1 inet proto tcp from 198.51.100.100 port 80 \
            to 198.51.100.0/24 flags any no state
        pass out on vio0 inet proto tcp from 198.51.100.100 port 80 \
            to 198.51.100.0/24 flags any no state
        

        Merge required administration and ICMP policy separately. Keep these stateless rules limited to HTTP replies on the return router. Each backend still sees both directions of its own connections and can filter them statefully.

        Replace each backend’s route to the client network:

        doas route delete -net 198.51.100.0/24
        doas route add -net 198.51.100.0/24 203.0.113.254
        

        Persist the changed route. The service must listen on the loopback VIP as well as the address used by the health checks; the wildcard listener above covers both.

        On both frontends, disable pfsync initial-packet deferral for this DSR configuration:

        doas ifconfig pfsync0 -defer
        

        Replace defer with -defer in each /etc/hostname.pfsync0. PF states still synchronize; initial packets are no longer held for a peer acknowledgment. Keep the quick relayd anchor from the gateway guide.

        On both frontends, replace only the forward to line inside the redirection with:

        route to <web> port 80 mode roundrobin check http "/health" code 200 interface vio1
        

        Validate and reload:

        doas relayd -n
        doas rcctl reload relayd
        doas pfctl -a 'relayd/*' -sr
        

        Existing NAT states are not converted into DSR states by a reload. Test DSR with new connections. During a live migration, allow old connections to finish before removing the NAT configuration; avoid clearing unrelated PF states.

        Check the Request and Reply Paths #

        Capture the backend-facing link on the frontend:

        doas tcpdump -n -i vio1 'tcp port 80 and host 198.51.100.10'
        

        For NAT, requests have the selected backend as their destination and replies cross this link. For DSR, requests still have 198.51.100.100 as their destination; replies should appear on the separate return router instead. Check backend access logs for the actual client address and compare the response body on the client.

        Request packet headers show client 198.51.100.10 sending to service VIP 198.51.100.100 port 80
        On the frontend's backend-facing interface, DSR requests retain the client address and service IP.
        Reply packet headers show the service VIP sending to the same client ports through the return router
        The return router carries replies from the service VIP directly toward the client.

        The DSR frontend sees requests but not replies. Inspect relayd’s generated PF rules and verify that traffic passes; keep stateful filtering for unrelated services. Repeat the backend health and frontend takeover tests in DSR mode. During takeover, check that an open client connection continues to reach the same backend without reconnecting.

        Choose Between the Designs #

        NAT is usually the simpler starting point when both directions can traverse the frontend. DSR removes replies from that path, which can help with response-heavy services, but requires changes to backend addressing and routing. Troubleshooting requires captures on both the frontend and the return router.

        For HTTP path routing or TLS termination, use the HTTPS proxy guide . Application sessions and persistent data need their own sharing or replication strategy with any of these designs. See the design comparison for help choosing.

        Report a bug
        • Synopsis
        • Prepare the Shared Network
        • Start with NAT
        • Verify Health Checks and Frontend Failover
        • Change to Direct Server Return
        • Check the Request and Reply Paths
        • Choose Between the Designs