Skip to main content
RunBook Academy

← All labs in Linux

Lab · intermediate · ~45 min

Lab: Configure NAT for a small internal network

B · Nested virtualisationC · Simulation

Objectives

  • Configure a host as a NAT gateway
  • Configure SNAT for outbound traffic
  • Configure DNAT for inbound port forwarding
  • Validate with internal and external connectivity tests

Prerequisites

This lab configures a Linux host as a NAT gateway for a small internal network. By the end you will have SNAT for outbound and DNAT for inbound port forwarding, validated end-to-end.

Objective

By the end of this lab, you can:

  • Configure a host as a NAT gateway.
  • Configure source NAT for outbound traffic.
  • Configure destination NAT for inbound port forwarding.
  • Validate the topology from internal and external hosts.

Architecture

You need three hosts (or VMs):

  • Gateway (10.0.0.1, eth0; 192.0.2.10, eth1): the NAT gateway. Two interfaces.
  • Internal (10.0.0.5, eth0): an internal host behind the gateway.
  • External: any host outside the gateway (use the host running this lab).

The gateway’s eth0 faces the internal network; eth1 faces the external network.

Tasks

Task 1: Record the pre-lab state, then configure IP forwarding

Capture what the gateway had before you change it. The cleanup section restores from these two files; without them the only cleanup available is zeroing state that may not have been zero.

# The existing ruleset - Docker, libvirt or firewalld may own tables here
sudo nft list ruleset | sudo tee /root/nft-rollback.conf >/dev/null

# The existing forwarding setting, which may already be 1
sysctl -n net.ipv4.ip_forward | sudo tee /root/ip_forward.orig

Then enable forwarding:

# sudo tee, not a redirect: in `sudo echo 1 > /proc/...` the shell opens the
# file as your unprivileged user and the write fails with Permission denied.
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

# Persist
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-nat.conf
sudo sysctl -p /etc/sysctl.d/99-nat.conf

Task 2: Configure the NAT ruleset

Create /etc/nftables.d/nat.conf:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        iif lo accept
        ip protocol icmp accept
        tcp dport 22 accept   # SSH for management
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept

        # Allow internal hosts to reach external
        iifname "eth0" oifname "eth1" accept

        # Allow external to reach internal port 22 (forwarded)
        iifname "eth1" oifname "eth0" ip daddr 10.0.0.5 tcp dport 22 accept
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

table inet nat {
    chain postrouting {
        type nat hook postrouting priority 100;

        # Source NAT for outbound traffic from internal
        oifname "eth1" ip saddr 10.0.0.0/24 snat to 192.0.2.10
    }

    chain prerouting {
        type nat hook prerouting priority -100;

        # DNAT for inbound port forwarding
        # The `ip` keyword is mandatory here - see the note below.
        iifname "eth1" tcp dport 2222 dnat ip to 10.0.0.5:22
    }
}

Task 3: Apply and validate

sudo nft -c -f /etc/nftables.d/nat.conf
sudo nft -f /etc/nftables.d/nat.conf
sudo nft list ruleset

Task 4: Test outbound NAT

On the internal host:

# Default route via the gateway
sudo ip route add default via 10.0.0.1

# Test external connectivity
ping -c 3 8.8.8.8
curl -I https://example.com

These should work. From the gateway:

sudo conntrack -L -s 10.0.0.5

You should see a NAT entry for the internal host’s connection, with the source rewritten to 192.0.2.10.

Task 5: Test inbound DNAT

From an external host (or the same host):

ssh -p 2222 user@192.0.2.10

The connection should land on the internal host (10.0.0.5). On the gateway:

sudo conntrack -L -d 192.0.2.10

You should see a DNAT entry showing the destination rewritten from 192.0.2.10:2222 to 10.0.0.5:22.

Task 6: Test denial

Pick a port that is neither forwarded nor served by the gateway. 8080 has no DNAT rule and nothing is listening on it, so it is the honest negative test:

nc -vz -w 3 192.0.2.10 8080

With policy drop on the input chain and no rule matching, the packet is dropped silently and nc times out after 3 seconds. A Connection refused here would mean a RST came back, which is what you would see if the chain policy were reject or if something on the gateway were listening — either is a finding worth chasing.

Task 7: Test SSH access to the gateway itself

ssh user@192.0.2.10

This should work (the gateway’s own SSH is allowed by the input chain). DNAT is for traffic forwarded through the gateway, not to the gateway.

Task 8: Document the topology

# NAT topology

## Gateway (192.0.2.10)
- eth0: 10.0.0.1 (internal)
- eth1: 192.0.2.10 (external)
- Forwards internal -> external (SNAT)
- Forwards external:2222 -> internal:22 (DNAT)

## Internal host (10.0.0.5)
- Default route via 10.0.0.1
- Outbound appears as 192.0.2.10

## External clients
- ssh -p 2222 user@192.0.2.10 reaches 10.0.0.5

## Validation
- Internal host: ping 8.8.8.8 (via gateway, SNAT)
- External: ssh -p 2222 192.0.2.10 (DNAT to 10.0.0.5:22)
- conntrack shows both NAT entries

Validation

  • Outbound traffic from internal works.
  • Inbound port forwarding works.
  • Direct access to the gateway’s ports works (input chain).
  • Unforwarded ports are blocked.

Cleanup

Remove only what this lab created, and restore the state you captured in Task 1.

# Delete the lab's own tables. Never `nft flush ruleset` in a cleanup:
# it deletes every table on the host, including Docker's, libvirt's,
# Kubernetes' and firewalld's.
sudo nft delete table inet nat    2>/dev/null || true
sudo nft delete table inet filter 2>/dev/null || true

# Restore the ruleset that was loaded before the lab
if [ -s /root/nft-rollback.conf ]; then
    sudo nft -c -f /root/nft-rollback.conf && sudo nft -f /root/nft-rollback.conf
fi

sudo rm -f /etc/nftables.d/nat.conf /etc/sysctl.d/99-nat.conf

# Restore forwarding to what it was - not to 0. A gateway, a Docker host
# or any box running libvirt NAT needs ip_forward=1, and zeroing it breaks
# container and VM networking with no error message.
sudo sysctl -w net.ipv4.ip_forward="$(cat /root/ip_forward.orig)"

# Confirm
sudo nft list ruleset
sysctl -n net.ipv4.ip_forward

On the internal host:

sudo ip route del default via 10.0.0.1

What you learned

  • A NAT gateway requires IP forwarding enabled and the correct firewall rules.
  • SNAT rewrites outbound traffic; DNAT rewrites inbound.
  • The conntrack table confirms what the firewall actually did.
  • The difference between traffic to the gateway itself (input chain) and traffic through the gateway (forward chain).
  • In an inet table, dnat/snat with a literal address need an explicit ip or ip6 keyword unless an earlier match in the same rule has already pinned the family.
  • nft -f is atomic: one bad rule rejects the entire file, so nft -c -f is a mandatory pre-flight and not an optional courtesy.

Deliverables

  • · A NAT gateway with SNAT and DNAT rules
  • · Connectivity tests from internal and external hosts
  • · A documented NAT topology

Verification status

Last reviewed
2026-08-09
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.