<--

Nat Gateway

Motivation

Sometimes, you need some simple way of “proxying” traffic between regions or subnets. One of the simplest and most performant ways to do that is using NAT.

The thing is, with a “classic” proxy, you create at least two connections, and it often operates at the application layer of the OSI model. But actually, in many cases, simple IP packet forwarding is enough.

An example could be cross-region VPN access, where the goal is to have a server in region A and redirect TCP/UDP traffic coming to, let’s say, port 4443, to a VPN server in region B. In that case, you basically just need to rewrite the UDP packet destination IP address, which is an L3 operation, while selecting the packets by looking at the destination port, which is L4. This method does not require heavy encryption or application-level processing, and it runs entirely in kernel space.

In Linux, there is a kernel framework called netfilter, which is responsible for packet filtering, NAT, and traffic redirection.

Historically, there was the iptables user-space utility for defining packet filtering and NAT rules. But since Linux kernel 3.13, a more modern tool exists: nftables. Conceptually, nftables uses a small virtual machine inside the kernel, which executes bytecode-like rules to inspect packets, match conditions, and apply actions such as filtering, NAT, or marking packets.

So let’s configure simple nftables rules that combine firewalling and IPv4 traffic redirection.

Set up

Check that forwarding enabled:

sysctl net.ipv4.ip_forward

If 0 —> enable it:

sudo sysctl -w net.ipv4.ip_forward=1
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-forwarding.conf
sudo sysctl --system

Create sudo vi /etc/nftables.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
                iifname "lo" accept

                tcp dport 443 accept
                tcp dport 80 accept
                tcp dport 22 accept

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

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

        tcp dport { 4443 } dnat to 2.2.2.2
        udp dport { 4443 } dnat to 2.2.2.2 
    }

    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;

        ip daddr 2.2.2.2 tcp dport { 4443 } masquerade
        ip daddr 2.2.2.2 udp dport { 4443 } masquerade

        iifname "tun0" masquerade
    }
}

Explanation

Remove all existing nftables rules before applying this config. Be careful with this on a remote server: if the new rules are wrong, you can accidentally lock yourself out.

flush ruleset

The “inet” family applies to both IPv4 and IPv6. Here we define what traffic is allowed to reach this machine itself (on ports 443, 80, 22), what traffic can be forwarded through it (any), and what traffic it can send out (all).

table inet filter {
        chain input {
        ...
        }
        chain forward {
        ...
        }
        chain output {
        ...
        }
}

Next, is ipv4 nat for traffic redirection:

table ip nat {
    chain prerouting {
    ...
    }

    chain postrouting {
    ...
    }
}

In prerouting, we catch incoming TCP and UDP traffic on port 4443 before the kernel decides where to send it, and rewrite its destination to the backend server 2.2.2.2. Since we don’t specify another port, the original port is preserved: this_machine:4443 becomes 2.2.2.2:4443.

In postrouting, we apply masquerade for traffic going to that backend. This makes the backend see our machine as the source, so replies come back through the same path and nftables/conntrack can translate them back to the original client. The extra tun0 masquerade rule is useful when the same machine also works as a VPN gateway, though in a stricter setup I’d usually limit it by subnet and outgoing interface.

Running

Apply new config and restart a service:

sudo nft -f /etc/nftables.conf
sudo systemctl restart nftables
sudo systemctl status nftables

If all good - enable the service:

sudo systemctl enable nftables

You may also use iftop for network monitoring.