DIY Debian Router - Part 9: Port forwarding with nftables

Oct 11, 2025

Introduction

This is Part 9 of the DIY Debian Router series. See Part 1 for the series introduction and links to all parts.

Port forwarding enables external Internet access to services hosted on the LAN by translating incoming WAN traffic to internal addresses. This allows hosting services on our residential Internet connections, making them accessible from anywhere on the Internet.

This part covers:

  • Port forwarding implementation via nftables DNAT
  • Firewall rules for inbound service access
  • Security considerations for exposed services
  • IPv6 considerations
  • Testing and troubleshooting

Port forwarding architecture

Port forwarding uses Destination Network Address Translation (DNAT) to rewrite packet destinations:

  1. External client sends packet to [WAN IP]:443
  2. Router (DNAT) rewrites destination to 192.168.0.10:443
  3. Router (forward chain) permits traffic to internal server
  4. Internal server receives packet, responds
  5. Router (SNAT/masquerading) rewrites source back to WAN IP
  6. External client receives response

This is bidirectional NAT: DNAT for inbound, SNAT for outbound.

Security considerations before exposing services

Exposing services to the Internet introduces significant risk. Every forwarded port is a potential attack vector. Before enabling port forwarding:

  1. Ensure service is hardened: Apply all security updates, disable unnecessary features
  2. Use strong authentication: SSH keys (no passwords), complex credentials for web services, etc
  3. Implement rate limiting: Fail2ban, application-level throttling
  4. Monitor logs: Automated alerting for suspicious activity
  5. Minimize exposed services: Only forward what is absolutely necessary

Recommendation: Use port forwarding only when VPN (covered in Part 8) or reverse proxy solutions are unsuitable.

nftables port forwarding configuration

Port forwarding rules are added to the ip nat table's prerouting chain (for DNAT) and the inet filter table's forward chain (for stateful filtering).

Edit /etc/nftables.conf (configured in Part 6):

Step 1: Add DNAT rules, locate the table ip nat section and modify the prerouting chain:

table ip nat {
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;
        # Port forwarding: HTTP to internal web server
        iifname $wan tcp dport 80 dnat to 192.168.0.10:443 comment "HTTPS to server"
        # Port forwarding: SSH to internal server (non-standard external port)
        iifname $wan tcp dport 2222 dnat to 192.168.0.10:22 comment "SSH to server"
    }

    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname $wan ip saddr $home_ipv4 masquerade comment "NAT for LAN to Internet"
    }
}

Port translation example: External port 2222 → internal port 22 allows SSH access without exposing standard port 22 (reduces automated attack traffic).

Step 2: Add forward chain rules, permitting translated traffic in the forward chain:

table inet filter {
    chain forward {
        type filter hook forward priority filter; policy drop;
        # Drop invalid connection states immediately
        ct state invalid drop
        # Allow established/related connections first (most common traffic)
        ct state established,related accept
        # Allow traffic within main LAN (full internal connectivity)
        iifname $main_lan oifname $main_lan accept comment "LAN to LAN traffic"
        # Allow main LAN to WAN (outbound Internet access)
        iifname $main_lan oifname $wan accept comment "LAN to WAN (Internet)"

        # === Port Forwarding Rules ===
        # Allow forwarded HTTP traffic to web server
        iifname $wan oifname $main_lan ip daddr 192.168.0.10 tcp dport 443 ct state new accept comment "Forward HTTPS to server"
        # Allow forwarded SSH traffic to server
        iifname $wan oifname $main_lan ip daddr 192.168.0.10 tcp dport 22 ct state new accept comment "Forward SSH to server"
        # WAN to LAN: only established/related (handled above, explicit for clarity)
        # Everything else is dropped by policy
    }
    # ... rest of configuration ...
}

Important: Forward chain rules must match the post-DNAT destination (internal IP and port, not external).

Complete example configuration snippet

################## NAT table (IPv4 only) ##################

table ip nat {
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;

        # Port forwarding rules
        iifname $wan tcp dport 80 dnat to 192.168.0.10:443 comment "HTTPS to server"
        iifname $wan tcp dport 2222 dnat to 192.168.0.10:22 comment "SSH to server"
    }

    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname $wan ip saddr $home_ipv4 masquerade comment "NAT for LAN to Internet"
    }
}

Applying configuration changes

After editing /etc/nftables.conf, apply changes:

nft -f /etc/nftables.conf

Verify rules loaded correctly:

nft list table ip nat
nft list chain inet filter forward

Output should include DNAT rules in the prerouting chain and corresponding accept rules in the forward chain.

IPv6 port forwarding

IPv6 does not use NAT; traffic routes directly to clients' globally routable addresses, so "port forwarding" is simply firewall rule creation.

To expose an IPv6 service, add a forward chain rule:

# In forward chain, for IPv6 web server on fd09:dead:beef::10
iifname $wan oifname $main_lan ip6 daddr fd09:dead:beef::10 tcp dport 80 ct state new accept comment "Allow HTTP to IPv6 server"

⚠️ IPv6 clients are directly accessible from the Internet (no NAT hiding). Firewall rules are critical.

Testing port forwarding

Internal testing (same network)

From a LAN client, test access to the internal service:

curl http://192.168.0.10

Web server should respond (confirms service is running).

External testing

From an external host (mobile phone on cellular, VPS, friend's network):

curl http://example.absurdum.ca

Same web server response is expected (confirms port forwarding works).

Connection tracking

On the router, monitor connection tracking during external access:

conntrack -E | grep 192.168.0.10

In another terminal, trigger external access. Expected output:

[NEW] tcp 6 120 SYN_SENT src=X.X.X.X dst=Y.Y.Y.Y sport=54321 dport=80 ...
[UPDATE] tcp 6 60 SYN_RECV src=192.168.0.10 dst=X.X.X.X sport=80 dport=54321 ...
[UPDATE] tcp 6 432000 ESTABLISHED src=192.168.0.10 dst=X.X.X.X sport=80 dport=54321 ...

This shows the connection being tracked and translated correctly.

Firewall log inspection

If traffic is blocked, enable logging temporarily:

# In forward chain, add before existing rules:
log prefix "nft-forward: "

Reload firewall:

nft -f /etc/nftables.conf

Monitor logs:

journalctl -kf | grep nft-forward

Trigger external connection and observe log entries.

Security hardening for exposed services

Rate limiting with nftables

Limit connection rate to prevent brute-force attacks:

# In forward chain, before port forwarding rules:
iifname $wan oifname $main_lan ip daddr 192.168.0.10 tcp dport 443 \
    ct state new limit rate 100/minute \
    accept comment "Rate-limit HTTPS to 100 connections/minute"

Connections exceeding 100 per minute are dropped.

Application-level security

  • SSH: Disable password authentication (PasswordAuthentication no), use key-based auth
  • Web servers: Enable HTTPS with Let's Encrypt certificates, strong TLS configuration
  • Minimize exposed functionality: Disable admin interfaces, unnecessary endpoints, etc

Next Steps

Port forwarding exposes services to the Internet but requires a consistent way to reach the router. Residential ISPs typically assign dynamic IP addresses that change periodically. Part 10 covers Dynamic DNS configuration to automatically track and update DNS records when the public IP changes.

For secure remote access to the entire LAN without exposing individual services, see Part 8 for WireGuard VPN deployment.

RSS
https://yusefkarim.absurdum.ca/posts/feed.xml