Port Forwarding on Linux
Forward ports on Linux using nftables DNAT rules, SSH local/remote tunnels, and router NAT configuration — with persistence and troubleshooting tips.
Before you start
- ▸Root or sudo access on the Linux host
- ▸nftables installed and the nftables systemd service available
- ▸Basic familiarity with IP addressing and TCP/UDP ports
- ▸SSH key-based authentication configured for tunnel examples
Port forwarding lets you redirect traffic arriving on one address and port to a different destination — whether that's another port on the same host, a machine behind a NAT gateway, or a remote server reached through an SSH tunnel. This guide covers three practical scenarios: kernel-level forwarding with nftables, SSH tunnels for quick or secure tunneling, and pointers on router-side configuration when Linux sits behind a consumer gateway.
1. nftables Port Forwarding
nftables is the standard packet-filtering framework on modern Linux. It replaces iptables/ip6tables/arptables and ships as the default on Debian 10+, Ubuntu 20.10+, Fedora 32+, and RHEL 8+. Every example here assumes a working nftables installation and root/sudo access.
Enable IP Forwarding in the Kernel
The kernel drops forwarded packets by default. Enable forwarding persistently before writing any rules:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-ip-forward.conf
sudo sysctl -p /etc/sysctl.d/99-ip-forward.conf
For IPv6 forwarding, add net.ipv6.conf.all.forwarding = 1 to the same file.
Understand the nftables Table/Chain Layout
Port forwarding needs two hooks:
- prerouting — rewrites the destination before routing decisions (DNAT).
- postrouting — masquerades or rewrites the source after routing (MASQUERADE/SNAT).
Redirect a Port Locally (Same Host)
Forward external TCP traffic arriving on port 8080 to local port 80 — useful when an app can't bind to a privileged port:
sudo nft add table ip nat
sudo nft add chain ip nat prerouting '{ type nat hook prerouting priority -100; }'
sudo nft add rule ip nat prerouting tcp dport 8080 redirect to :80
Forward a Port to Another Host (DNAT)
This is the classic gateway scenario: your Linux box has a public IP (203.0.113.1) and you want TCP port 2222 forwarded to an internal server at 192.168.1.50:22.
sudo nft add table ip nat
sudo nft add chain ip nat prerouting '{ type nat hook prerouting priority -100; }'
sudo nft add chain ip nat postrouting '{ type nat hook postrouting priority 100; }'
# DNAT: rewrite destination on the way in
sudo nft add rule ip nat prerouting \
ip daddr 203.0.113.1 tcp dport 2222 \
dnat to 192.168.1.50:22
# MASQUERADE: rewrite source so replies come back through this host
sudo nft add rule ip nat postrouting \
ip daddr 192.168.1.50 masquerade
If the internal server has a static default gateway pointing back to your Linux box, you can use SNAT instead of MASQUERADE for marginally better performance:
sudo nft add rule ip nat postrouting \
ip daddr 192.168.1.50 snat to 192.168.1.1
Make nftables Rules Persistent
Rules written with nft add are lost on reboot. Save them and enable the service:
# Debian/Ubuntu
sudo nft list ruleset | sudo tee /etc/nftables.conf
sudo systemctl enable --now nftables
# Fedora / RHEL / Rocky
sudo nft list ruleset | sudo tee /etc/sysconfig/nftables.conf
sudo systemctl enable --now nftables
# Arch
sudo nft list ruleset | sudo tee /etc/nftables.conf
sudo systemctl enable --now nftables
On Debian and Ubuntu, /etc/nftables.conf is the file the systemd unit already reads. Verify the path in /lib/systemd/system/nftables.service if you're unsure.
Verify the Rules
sudo nft list ruleset
You should see the nat table with your prerouting and postrouting chains populated. Test connectivity with:
nc -zv 203.0.113.1 2222
A successful connection message confirms the forward is working. Use ss -tnp on the destination host to verify the connection arrived.
2. SSH Port Forwarding (Tunnels)
SSH tunnels require no firewall changes on the forwarding host and work entirely in userspace. They're the right tool for developer access, quick secure tunnels, and crossing firewalls that block arbitrary ports.
Local Forward (-L): Reach a Remote Service Locally
Bind a port on your workstation and tunnel it to a remote service. Example: access a database at db.internal:5432 that is reachable from jump.example.com but not from your laptop:
ssh -L 5432:db.internal:5432 [email protected] -N
Now localhost:5432 on your laptop connects through the tunnel to db.internal:5432. The -N flag skips opening a shell — good for background tunnel processes.
Remote Forward (-R): Expose a Local Port to a Remote Host
Let a remote server reach a service running locally — useful for sharing a dev server with a colleague or bypassing inbound firewall rules:
ssh -R 9090:localhost:3000 [email protected] -N
Anyone on public-server.example.com can now hit localhost:9090 and reach your local port 3000. By default, the remote port binds only to 127.0.0.1. To bind it to all interfaces on the remote host, set GatewayPorts yes in the remote server's /etc/ssh/sshd_config and reload sshd.
Dynamic Forward (-D): SOCKS Proxy
A dynamic forward turns the SSH client into a SOCKS5 proxy, forwarding any destination your browser or app requests:
ssh -D 1080 [email protected] -N
Point your browser's SOCKS5 proxy to 127.0.0.1:1080.
Keep Tunnels Alive with systemd
For persistent tunnels, a systemd user service is cleaner than a tmux session. Create ~/.config/systemd/user/ssh-tunnel.service:
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/ssh-tunnel.service <<'EOF'
[Unit]
Description=SSH port forward tunnel
After=network-online.target
[Service]
ExecStart=/usr/bin/ssh -NTC -o ServerAliveInterval=60 \
-o ExitOnForwardFailure=yes \
-L 5432:db.internal:5432 [email protected]
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
EOF
systemctl --user enable --now ssh-tunnel
-C enables compression (optional), ServerAliveInterval=60 prevents idle disconnects, and ExitOnForwardFailure=yes causes the process to exit (and thus restart) if the port bind fails.
3. Router-Side Configuration
When your Linux server sits behind a home router or cloud provider's NAT, kernel-level nftables rules handle forwarding within your LAN, but the router itself must forward inbound traffic from the public internet to your Linux machine's private IP.
Consumer Routers
- Log in to your router's admin UI (commonly
192.168.1.1or192.168.0.1). - Find Port Forwarding, NAT, or Virtual Server — the label varies by vendor.
- Create a rule: external port → your Linux machine's LAN IP → destination port.
- Assign a static DHCP lease or configure a static IP on the Linux host so the LAN address doesn't change.
Cloud Providers
On AWS, GCP, or Azure, security groups and firewall rules are managed at the hypervisor level. You must open the relevant port in your security group / VPC firewall rule in addition to any nftables rules on the instance itself. Opening the port in nftables alone is not sufficient — and vice versa.
Check Your Public IP and Test
curl -s https://ifconfig.me
# Test from an external host or use a service like nmap.online
nmap -p 2222 203.0.113.1
Troubleshooting
- Packets dropped silently: Check that
net.ipv4.ip_forwardis1. A common mistake is setting it only in the running kernel (sysctl -w) without persisting it. - nftables rules exist but traffic doesn't arrive: A distribution may run firewalld or ufw on top of nftables. Those tools manage their own chains; add rules through them or disable the higher-level tool before writing raw nftables rules. Mixing the two without care breaks things.
- firewalld (Fedora/RHEL): Use
firewall-cmd --add-forward-portinstead of raw nftables if firewalld is active:
sudo firewall-cmd --zone=public \ --add-forward-port=port=2222:proto=tcp:toport=22:toaddr=192.168.1.50 \ --permanent sudo firewall-cmd --reload - SSH tunnel drops frequently: Add
ServerAliveInterval 30andServerAliveCountMax 3to~/.ssh/configfor the target host. - Remote forward only binds loopback: Set
GatewayPorts clientspecifiedin the remote sshd config and use-R 0.0.0.0:9090:localhost:3000to bind all interfaces explicitly. - Connection refused on forwarded port: Confirm the destination service is actually listening (
ss -tlnpon the target), not just that the forward rule exists.
Frequently asked questions
- Can I use nftables rules alongside firewalld or ufw?
- Not safely without coordination. firewalld and ufw manage their own nftables chains; adding raw nft rules on top can create conflicts or get overwritten. Either use the higher-level tool's own forwarding commands, or disable it and manage nftables directly.
- Why does my SSH remote tunnel only listen on 127.0.0.1?
- By default sshd binds remote-forwarded ports to loopback only. Set GatewayPorts yes (or clientspecified) in the remote host's /etc/ssh/sshd_config and reload sshd, then specify the bind address explicitly in your -R argument.
- Do I need MASQUERADE if the target server's default gateway already points to my Linux box?
- No. If the internal server routes all traffic back through your Linux gateway, return packets will arrive correctly without rewriting the source. MASQUERADE is only necessary when the destination host would otherwise send replies directly to the original client, bypassing your gateway.
- What is the difference between MASQUERADE and SNAT in nftables?
- SNAT rewrites the source to a fixed IP you specify and is slightly more efficient because the kernel doesn't need to look up the outgoing interface's address each time. MASQUERADE determines the source IP dynamically from the outgoing interface, making it the right choice when your external IP changes (e.g., a DHCP WAN address).
- How do I forward ports on Fedora or RHEL where firewalld is the default?
- Use firewall-cmd --add-forward-port with the port, proto, toport, and optionally toaddr arguments, then pass --permanent and run firewall-cmd --reload. This integrates cleanly with firewalld's nftables backend without conflicts.
Related guides
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.