Skip to content

Walkthrough: a two-interface router with a port-forward

This tutorial builds a small, complete firewall from an empty directory: a router with a WAN and a LAN interface that forwards a public web port to an internal host and masquerades outbound LAN traffic. You will write five config files, then drive them through the check → compile → apply lifecycle.

All addresses below are from the documentation ranges (RFC 5737 / RFC 3849), so you can copy the files verbatim without pointing at anything real.

Prerequisites

Install the shorewallnf command first — see Install. check and compile run unprivileged anywhere; only apply touches the live ruleset and needs sudo on a real nftables host.

What we're building

Piece Choice
net zone (WAN) interface eth0
loc zone (LAN) interface eth1
Default policy LAN may reach the internet; everything else is dropped/rejected
Port-forward inbound TCP 80 on the WAN → 192.0.2.10:80 on the LAN (DNAT)
Admin access inbound TCP 22 to the firewall itself
Outbound NAT masquerade the 192.0.2.0/24 LAN out eth0

Step 1 — create the config directory

mkdir router && cd router

Everything below lives in this directory. ShorewallNF only reads the files it recognises, in a fixed order; see the configuration file reference.

Step 2 — zones

Declare the firewall itself plus the two networks it sits between.

#ZONE   TYPE
fw      firewall
net     ipv4
loc     ipv4

Step 3 — interfaces

Bind each zone to a NIC. detect lets ShorewallNF derive the interface's networks.

#ZONE   INTERFACE   OPTIONS
net     eth0        detect
loc     eth1        detect

Step 4 — policy

Default inter-zone verdicts, applied after the per-connection rules fall through. The LAN is allowed out; every other flow hits the fail-closed defaults.

#SOURCE   DEST   POLICY   LOG
loc       net    ACCEPT
net       all    DROP     info
all       all    REJECT   info

Step 5 — rules

Two per-connection rules: admin SSH to the firewall, and the DNAT port-forward that publishes an internal web server. loc:192.0.2.10 is the rewrite target — the DNAT rule both redirects the packet and admits the forwarded flow.

#ACTION   SOURCE   DEST             PROTO   DPORT
ACCEPT    net      fw               tcp     22
DNAT      net      loc:192.0.2.10   tcp     80

Step 6 — snat

Masquerade the LAN subnet as it egresses the WAN interface (source NAT is IPv4-only; see ADR-0002).

#ACTION       SOURCE          DEST (egress interface)
MASQUERADE    192.0.2.0/24    eth0

The directory now holds five files:

router/
├── interfaces
├── policy
├── rules
├── snat
└── zones

Step 7 — check

Preprocess and validate without emitting a ruleset. This catches typos, unknown zones, and malformed rows before you ever touch nftables.

shorewallnf check router
OK: router: 5 files, 16 preprocessed lines

Step 8 — compile

Compile to the inet nftables ruleset and print it as python3-nftables JSON. Nothing is loaded — this is the artifact apply would install.

shorewallnf compile router

The output is the full ruleset (a filter table with input/forward/output hooks and a nat table with prerouting/postrouting). The two NAT rules from this config compile to:

{
  "add": {
    "rule": {
      "family": "inet",
      "table": "nat",
      "chain": "prerouting",
      "expr": [
        { "match": { "op": "==", "left": { "meta": { "key": "iifname" } }, "right": "eth0" } },
        { "match": { "op": "==", "left": { "payload": { "protocol": "tcp", "field": "dport" } }, "right": 80 } },
        { "dnat": { "addr": "192.0.2.10", "family": "ip" } }
      ]
    }
  }
}
{
  "add": {
    "rule": {
      "family": "inet",
      "table": "nat",
      "chain": "postrouting",
      "expr": [
        { "match": { "op": "==", "left": { "meta": { "key": "oifname" } }, "right": "eth0" } },
        { "match": { "op": "==", "left": { "payload": { "protocol": "ip", "field": "saddr" } },
          "right": { "prefix": { "addr": "192.0.2.0", "len": 24 } } } },
        { "masquerade": null }
      ]
    }
  }
}

The inbound TCP 80 on eth0 is rewritten to 192.0.2.10, and traffic leaving eth0 from 192.0.2.0/24 is masqueraded — exactly the router behaviour we set out to build.

Step 9 — apply

On a real nftables host, install the ruleset. apply compiles, dry-run checks it with nft --check, atomically loads it, and persists it to disk so it survives a reboot.

sudo shorewallnf apply router
applied: router

Where to go next