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 checkcompileapply 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