Skip to main content
RunBook Academy

← All labs in OPNsense

Lab · intermediate · ~60 min

Lab: Configure outbound NAT and a port forward, end-to-end

B · Nested virtualisationC · Simulation

Objectives

  • Configure hybrid outbound NAT mode for the LAN subnet
  • Verify outbound NAT translation with pfctl -s state and the verbose view
  • Configure a port forward from the WAN address to an internal server
  • Read the auto-generated rdr and pass rules in the compiled ruleset
  • Test the inbound flow from an external client and confirm it works

Prerequisites

This lab walks the most common NAT configuration on an OPNsense firewall: outbound NAT for the LAN and a port forward for an inbound service. You will configure both, verify both with pfctl, and prove end-to-end that an external client can reach the internal server through the port forward. The discipline on display is the one a production change request requires: snapshot before, change deliberately, verify after, document the result.

By the end you will have used the GUI to set up NAT that the firewall can actually pass traffic through, and you will have read the compiled PF ruleset to confirm the GUI’s output is what the kernel sees.

Objective

By the end of this lab, you can:

  • Configure hybrid outbound NAT mode for the LAN subnet.
  • Verify the outbound NAT translation in the state table.
  • Configure a port forward that maps a WAN port to an internal server.
  • Read the auto-generated rdr and pass rules in pfctl -s rules.
  • Test the port forward from an external client and confirm the traffic flows.

Requirements

  • An OPNsense instance with at least two interfaces configured (LAN and WAN).
  • An internal server on the LAN with a service listening on the port you will forward (HTTP on 8080, SSH on 2222, anything).
  • An external client on the WAN side (or a second nested VM that can reach the WAN IP).
  • A snapshot of the current configuration that you can revert to if the change does not go as planned.

Tasks

Task 1: Snapshot the current state

Capture the ruleset, NAT rules, and state-table statistics before the change. The diff between before and after is the evidence that the change did what you intended.

pfctl -s rules > /tmp/rules-before.txt
pfctl -s nat > /tmp/nat-before.txt
pfctl -s state | wc -l > /tmp/state-count-before.txt
cp /conf/config.xml /tmp/config-before.xml

Verify the snapshots are non-empty:

ls -lh /tmp/rules-before.txt /tmp/nat-before.txt /tmp/config-before.xml
cat /tmp/state-count-before.txt

Task 2: Configure hybrid outbound NAT

In the OPNsense GUI, navigate to Firewall → NAT → Outbound. The mode selector shows three options: Automatic, Hybrid, and Manual. Switch to Hybrid.

In hybrid mode, automatic rules continue to work for any interface without an explicit manual rule, and manual rules override the automatic ones for the interfaces they cover. The common pattern is to leave the LAN on automatic (the default rule suffices) and add explicit rules for special cases (a guest VLAN, a DMZ, a server subnet that needs to egress through a specific WAN).

For this lab, add a manual rule:

  • Interface: WAN
  • Source: 192.0.2.0/24 (your LAN subnet)
  • Destination: any
  • Translation: Interface address
  • Description: LAN outbound — manual hybrid rule

Apply the change.

Task 3: Verify the outbound NAT in the state table

From a LAN client, generate outbound traffic:

curl -v --max-time 5 https://example.com/ > /dev/null 2>&1

On the firewall, inspect the state table for this flow:

# Substitute your own values before running:
LAN_CLIENT_IP=192.0.2.50        # the LAN host that made the request

pfctl -s state | grep "$LAN_CLIENT_IP"

You should see the flow with the post-NAT source address (the firewall’s WAN IP), not the LAN client’s private IP. The NAT translation is active.

Get the verbose view to confirm:

pfctl -ss | grep -A 1 'tcp 192.0.2' | head -8

The metadata line shows the post-NAT translation:

all tcp <lan-client>:random <- <remote>:443     ESTABLISHED:ESTABLISHED
  [ <epoch> + <timeout> ] age <elapsed>, <firewall-wan-ip>:random -> <remote>:443

The second line is the post-NAT translation: the source has been rewritten from the LAN client’s IP to the firewall’s WAN IP. This is the outbound NAT doing its work.

Task 4: Add an internal server alias

Navigate to Firewall → Aliases and add a host alias:

  • Name: lab_server
  • Type: Host(s)
  • Value: INTERNAL_SERVER_IP
  • Description: Lab port-forward target

Apply the change. Confirm the alias is loaded:

pfctl -t lab_server -T show

The output should show the internal server’s IP.

Task 5: Configure the port forward

Navigate to Firewall → NAT → Port Forward → Add.

  • Interface: WAN
  • Protocol: TCP
  • Destination: WAN address
  • Destination port: 8443 (the external port)
  • Redirect target: lab_server (the alias from Task 4)
  • Redirect port: 443 (the internal port)
  • Description: Lab port forward — WAN:8443 to lab_server:443
  • Filter rule association: Add associated filter rule (checked)

Apply the change.

Task 6: Read the generated NAT and firewall rules

The GUI wrote two rules in the compiled ruleset: an rdr rule under NAT and a pass rule under the firewall. Inspect both:

pfctl -s nat | grep -A 1 lab

You should see the rdr on igb1 inet proto tcp from any to any port = 8443 -> <internal-server-ip> port 443 rule.

pfctl -s rules | grep -A 1 lab_server

You should see the auto-generated pass rule:

pass in quick on igb1 inet proto tcp from any to <internal-server-ip> port = 443

Both rules are present. The two are linked by the post-DNAT destination: the rdr rule rewrites the destination, and the pass rule permits the rewritten packet.

Task 7: Test the port forward from an external client

You need a client that can reach the firewall’s WAN IP. In a nested setup, this is your “external” VM. From that VM:

# Substitute your own values before running:
FW_WAN_IP=203.0.113.10          # the firewall's WAN address

curl -v --max-time 10 -k "https://$FW_WAN_IP:8443/"

If the internal server is listening on 443 with a self-signed cert, the -k flag disables cert verification so the test is about reachability, not TLS.

On the firewall, confirm the state was created:

pfctl -s state | grep ':8443'

You should see one entry for the flow — a single state entry matches traffic in both directions. The state confirms the connection reached the firewall, was DNAT’d to the internal server, and the response was SNAT’d back.

Read the verbose view to see the translation:

pfctl -ss | grep -A 1 ':8443' | head -4

The metadata line shows the post-NAT destination:

all tcp <external-client>:<port> <- <firewall-wan-ip>:8443    ESTABLISHED:ESTABLISHED
  [ <epoch> + <timeout> ] age 00:00:01, <external-client>:<port> -> <internal-server-ip>:443

The second line shows the destination has been rewritten to the internal server’s IP — the post-DNAT address.

Task 8: Capture the packet flow

A packet capture makes the NAT visible:

tcpdump -ni igb1 'port 8443' -c 10

In another session, repeat the curl from Task 7. The capture shows the inbound packet on WAN with destination <firewall-wan-ip>:8443 and the outbound response from the internal server with source <internal-server-ip>:443.

# Substitute your own values before running:
INTERNAL_SERVER_IP=192.0.2.10   # the LAN host behind the port forward

tcpdump -ni igb0 "host $INTERNAL_SERVER_IP and port 443" -c 10

The LAN-side capture shows the translated packet: the source is the external client, the destination is the internal server. The LAN never sees the firewall’s WAN IP for the rewritten packet.

Task 9: Test the firewall rule rejects unauthenticated traffic

What if you try to reach the internal server on port 443 without the port forward? On the LAN side, you should be able to reach the server directly (depending on your LAN rules). On the WAN side, you cannot — the firewall’s default deny on WAN rejects direct connections to internal IPs.

From the external client:

# Substitute your own values before running:
INTERNAL_SERVER_IP=192.0.2.10   # the LAN host behind the port forward

curl -v --max-time 5 -k "https://$INTERNAL_SERVER_IP:443/" 2>&1 | head -5

The connection should fail. The firewall’s WAN rules do not permit traffic from the WAN to the internal server’s IP directly; only the port forward’s rdr rule, in combination with the associated pass rule, allows the rewritten traffic.

Task 10: Document the lab

Write a short post-lab report:

NAT AND PORT FORWARD LAB REPORT
==============================

Outbound NAT (hybrid mode)
- Manual rule: WAN, source 192.0.2.0/24, dest any,
  translation Interface address
- Verified: post-NAT source in pfctl -ss shows firewall WAN IP

Port forward
- External: WAN:8443
- Internal: lab_server:443 (alias resolves to <internal-server-ip>)
- Filter rule association: enabled (auto-generated pass rule)
- Verified: rdr and pass rules present in pfctl -s nat / -s rules

End-to-end test
- External client reached internal server via WAN:8443
- State created, post-NAT destination in pfctl -ss confirmed
- Direct WAN-to-internal-server attempt blocked by default deny

Attach the /tmp/rules-before.txt, /tmp/nat-before.txt, /tmp/rules-after.txt (capture from Task 6), and the post-lab report.

Validation

  • Hybrid outbound NAT is configured and a manual rule for the LAN subnet is in place.
  • pfctl -s state for an outbound flow from the LAN client shows the post-NAT source as the firewall’s WAN IP.
  • A port forward is configured: WAN:8443 to lab_server:443.
  • The auto-generated rdr rule is in pfctl -s nat; the auto-generated pass rule is in pfctl -s rules.
  • An external client can reach the internal server via https://<firewall-wan-ip>:8443/.
  • A direct attempt from the external client to the internal server’s IP on port 443 is blocked.
  • The post-lab report and snapshot files are in /tmp/.

Cleanup

You have two changes to revert: the manual outbound NAT rule and the port forward. Revert in reverse order — remove the port forward first, then the manual NAT rule, then restore the outbound mode to Automatic.

# 1. Delete the port forward from the GUI
#    Firewall → NAT → Port Forward → delete the lab entry
#    Apply the change

# 2. Delete the manual outbound NAT rule from the GUI
#    Firewall → NAT → Outbound → delete the manual rule
#    Switch back to Automatic outbound NAT mode
#    Apply the change

# 3. Delete the alias
#    Firewall → Aliases → delete lab_server
#    Apply the change

# 4. Confirm the firewall is back to its pre-lab state
pfctl -s nat > /tmp/nat-after.txt
pfctl -s rules > /tmp/rules-after.txt
diff /tmp/nat-before.txt /tmp/nat-after.txt
diff /tmp/rules-before.txt /tmp/rules-after.txt

The diffs should be empty (or show only the order-change artefacts of the GUI’s regeneration). If either diff shows unexpected entries, restore the configuration backup.

cp /tmp/config-before.xml /conf/config.xml
configctl filter reload
# or, if needed:
/usr/local/sbin/pfctl -f /conf/pf.conf

Verify with a final test:

# Substitute your own values before running:
LAN_CLIENT_IP=192.0.2.50        # the LAN host that made the request

curl -v --max-time 5 https://example.com/ > /dev/null 2>&1
pfctl -s state | grep "$LAN_CLIENT_IP"
echo "expected: outbound NAT still working"

What you learned

  • Hybrid outbound NAT lets the default automatic rules coexist with explicit manual rules. The cleanup story is easier than “Manual” mode while still letting you add exceptions.
  • The port forward has two parts: an rdr rule that rewrites the destination, and a pass rule that permits the rewritten packet. The associated filter rule checkbox generates both from one GUI entry.
  • Verify the ruleset after every change. The GUI accepts the change but the kernel applies the compiled ruleset; the two can disagree on edge cases.
  • The state table is the proof that the NAT and firewall rules did what they were supposed to do. The pfctl -ss view shows the post-NAT addresses that the kernel actually used.
  • End-to-end testing matters. A successful NAT inspection without a successful end-to-end test is a partial verification.

Deliverables

  • · A working outbound NAT configuration with a documented rule
  • · A working port forward with an associated firewall rule
  • · Pre-change and post-change pfctl -s state and -s rules snapshots
  • · Evidence that inbound traffic from an external client reaches the internal server

Verification status

Last reviewed
2026-08-14
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.