Publish a service: forward, accept, and hairpin#
You will end up with one service on your LAN reachable from the internet, on a port you chose, and — if you want it — reachable from inside the LAN on the same public address.
This is the job that goes wrong most often, and it goes wrong the same way every time: the destination NAT rule is written, the packet is translated, and then the firewall drops it because nothing accepts it. A port forward is two rules, and the NAT page stages both.
Before you start#
- The operator role and a licence.
- The inside address of the service, and the port it listens on.
- The uplink the traffic will arrive on.
- A default-drop ruleset — A default-drop ruleset that admits what you meant. If your forward chain does not default to drop, the accept rule below is unnecessary and this guide is more careful than your router needs.
Step 1 — Stage the forward#
Network → NAT → + Add forward.
Fill in the WAN interface, the protocol, the WAN port, the target host and — if it differs — the target port. The panel shows the commands as you type, and picks the next free rule number, ten above the highest that exists.
set nat destination rule 100 inbound-interface name eth1
set nat destination rule 100 protocol tcp
set nat destination rule 100 destination port 443
set nat destination rule 100 translation address 192.0.2.10
set nat destination rule 100 description https-caddytranslation port is added only when the inside port differs from the outside one, so a
forward from 8443 to 443 gets both lines and a forward from 443 to 443 gets one.
A port range works in the WAN port field — 8000-8100 — and so does the tcp_udp
protocol, which is the panel's default.
Step 2 — Leave the firewall checkbox ticked#
The panel's first checkbox is Also stage the matching forward filter accept rule, and its hint says the whole reason it exists: without it the translated packet still hits the default drop.
set firewall ipv4 forward filter rule 100 action accept
set firewall ipv4 forward filter rule 100 inbound-interface name eth1
set firewall ipv4 forward filter rule 100 destination address 192.0.2.10
set firewall ipv4 forward filter rule 100 description 'allow https-caddy'Three things about that rule are worth understanding, because they are why people write it wrong by hand.
It matches the inside address, not the public one. Destination NAT happens before
filtering, so by the time the forward chain sees the packet its destination is already
192.0.2.10.
It is in the forward chain, not input. The packet is passing through the router to
another host. input is for traffic addressed to the router itself.
It takes the next free number in that chain — the same number as the NAT rule when that number is free, which is the usual convention, and never the number of a rule that already exists there.
Step 3 — Hairpin, when the LAN uses the public name#
Tick Also reachable from the LAN on the public address. This is the answer to "it works from my phone on mobile data but not from my desk", and it is what OPNsense calls NAT reflection.
The panel needs the LAN interface and reads the uplink's public address itself. It stages two more things:
set nat destination rule 101 inbound-interface name eth0
set nat destination rule 101 destination address 203.0.113.2
set nat destination rule 101 protocol tcp
set nat destination rule 101 destination port 443
set nat destination rule 101 translation address 192.0.2.10
set nat destination rule 101 description 'hairpin https-caddy'
set nat source rule 100 outbound-interface name eth0
set nat source rule 100 source address 192.0.2.0/24
set nat source rule 100 destination address 192.0.2.10
set nat source rule 100 protocol tcp
set nat source rule 100 destination port 443
set nat source rule 100 translation address masquerade
set nat source rule 100 description 'hairpin https-caddy'The destination rule numbered one above the first is the same forward matched on the LAN side. The source rule is the half people forget: without it the server answers the client directly, the client sees a reply from an address it did not send to, and the connection never completes.
The better answer on most networks: split-horizon DNS#
Rather than making the LAN travel out to the public address and back, give the service a name inside that points straight at its inside address.
set service dns forwarding authoritative-domain example.com records a caddy address 192.0.2.10LAN clients resolve caddy.example.com to 192.0.2.10 and never touch NAT at all.
Authoritative records for local names has the whole
procedure. This is why the OPNsense importer refuses to translate NAT reflection and says
split-horizon DNS is usually the better answer.
Step 4 — Commit#
Read the Commit Bar's diff. You should see between five and fifteen operations depending on the options you took. Then commit.
Check it worked#
From outside, not from the LAN. A phone on mobile data, a shell somewhere else, or an online port checker:
curl -sv https://203.0.113.2/ 2>&1 | head -5
nc -vz 203.0.113.2 443On the router, the counters tell you which half is failing:
show nat destination statistics
show firewall statisticsThe NAT page shows per-rule packet and byte counters and the Firewall page shows per-rule hit counters, both joined onto the rules themselves. That pairing is the diagnostic:
| NAT counter | Firewall counter | Means |
|---|---|---|
| 0 | 0 | Nothing arrived. Wrong uplink, wrong port, or the provider blocks it. |
| rising | 0 on your accept | The translation works and the packet is being dropped. The accept rule does not match — check its inbound interface and its destination address. |
| rising | rising | The router is doing its job. The problem is the server or its own firewall. |
From the LAN, if you hairpinned:
curl -sv https://203.0.113.2/ 2>&1 | head -5When it still does not work#
| Symptom | Check |
|---|---|
| Nothing arrives at all | Is the port actually reachable? Many providers block 25, 80 and 443 on consumer connections. Test a high port first. |
| Works from outside, not from the LAN | The hairpin is missing, incomplete, or the public address has changed. See step 3. |
| Works, then stops after a few days | A DHCP uplink whose address moved, and a hairpin rule holding the old one. |
| The NAT counter rises, the firewall counter does not | The accept rule's inbound interface or destination address does not match. This is the common one. |
| Two forwards, one works | Rule numbers. A lower-numbered rule that matches first wins; look at the order on the NAT page. |
| It worked before an uplink change | The forward names an interface. Uplinks — a second uplink needs its own forward. |
Undoing it#
Delete the rules you added. On the NAT page, the rule's side panel has a delete; on the Firewall page, the same.
delete nat destination rule 100
delete nat destination rule 101
delete nat source rule 100
delete firewall ipv4 forward filter rule 100Or, if the forward was the last thing you committed, roll back one revision.
See also#
- A default-drop ruleset that admits what you meant
- Use groups instead of literals — for the fifth forward to the same host
- Authoritative records for local names — the split-horizon answer
- Masquerade behind each uplink — publishing on a second uplink
- NAT — port forwards and Firewall — rules
Checked against ui/src/pages/Nat.tsx ·
ui/src/pages/Firewall.tsx ·
agent/main.go ·
tools/opnsense-import.py