How to Configure Declarative NAT and IP Masquerading in Ubuntu Using systemd-networkd

When turning an Ubuntu Server machine into a network router, local gateway, container host, or virtualization hypervisor, internal clients connected to secondary network interfaces (or internal bridge devices) require access to upstream external networks. Traditionally, establishing Network Address Translation (NAT) and IP masquerading on Linux required configuring low-level iptables or nftables firewall chains manually, saving rules with iptables-persistent, and writing custom shell scripts to ensure rules re-applied across server reboots.

However, modern Ubuntu systems running systemd-networkd can manage network routing, packet forwarding, and IP masquerading declaratively without manual firewall scripts. By enabling IPForward=yes and IPMasquerade=both inside native .network configuration files, systemd-networkd automatically hooks into the Linux kernel’s netfilter subsystem. It creates and maintains ephemeral masquerading chains dynamically as network interfaces link up and down. This guide explains how declarative NAT operates in systemd-networkd and details how to configure an Ubuntu Server as a resilient NAT router and internet gateway.

Architecture of Declarative NAT and IP Masquerading in systemd-networkd

In a standard two-interface Linux router topology, the server has two distinct network boundaries:

  • Uplink Interface (WAN / External): The public-facing or corporate network interface (e.g. eth0 or enp1s0) that receives an IP address and default gateway from an upstream ISP or router. This interface performs IP masquerading.
  • Downlink Interface (LAN / Internal Bridge): The internal-facing interface (e.g. eth1, enp2s0, or a virtual bridge br0) that serves local workstation subnets, virtual machines, or container networks.
  • Kernel Packet Forwarding (IPForward): Dictates whether the Linux kernel is permitted to forward IPv4 and IPv6 packets between different network interfaces (configured via sysctl net.ipv4.ip_forward=1 or declared directly in networkd unit files).
  • Declarative Masquerading (IPMasquerade): When IPMasquerade=both or ipv4 is declared on the external uplink interface, systemd-networkd injects netfilter masquerade rules. As internal packets from the LAN traverse the WAN interface, their source private IP address (e.g. 192.168.10.45) is rewritten to the WAN interface’s public IP, tracking connection states via Linux conntrack tables.

Managing NAT declaratively ensures configuration consistency: network interfaces, static IP addressing, DHCP server pools, and NAT rules are version-controlled together inside /etc/systemd/network/ without managing separate firewall scripts.

Prerequisites and systemd-networkd Activation

Ensure that systemd-networkd is active on your Ubuntu Server. While Ubuntu uses Netplan as its default frontend abstraction, Netplan can generate systemd-networkd backend configurations or you can manage native networkd unit files directly.

Verify that systemd-networkd and systemd-resolved are active:

sudo systemctl enable --now systemd-networkd
sudo systemctl enable --now systemd-resolved

Identify your physical network interface names using ip link:

ip link

In this scenario, we assume:

  • enp1s0: The external WAN uplink connected to the internet.
  • enp2s0: The internal LAN downlink serving the private subnet (192.168.100.0/24).

Configuring the Internal LAN Interface (Downlink)

Create a .network configuration file for the internal interface inside /etc/systemd/network/:

sudo nano /etc/systemd/network/10-internal.network

Define static IP addressing and enable systemd-networkd’s integrated DHCP server to automatically assign IP addresses to local client machines:

[Match]
Name=enp2s0

[Network]
# Static gateway IP for internal clients
Address=192.168.100.1/24

# Enable IP forwarding across this interface
IPForward=yes

# Enable integrated DHCP server for LAN clients
DHCPServer=yes

[DHCPServer]
PoolOffset=100
PoolSize=100
EmitDNS=yes
DNS=192.168.100.1
EmitRouter=yes

Save and close the file. The configuration establishes 192.168.100.1 as the default gateway for local clients and configures an automatic DHCP pool distributing addresses from 192.168.100.100 to 192.168.100.199.

Configuring the External WAN Interface with IPMasquerade (Uplink)

Next, configure the external uplink interface to perform declarative NAT masquerading for all outbound packets.

Create the external network unit file:

sudo nano /etc/systemd/network/20-external.network

Define WAN connectivity and enable IP masquerading:

[Match]
Name=enp1s0

[Network]
# Receive dynamic WAN IP and default gateway from upstream router/ISP
DHCP=yes

# Permit kernel packet forwarding
IPForward=yes

# Automatically inject netfilter NAT masquerade rules for outbound traffic
IPMasquerade=both

Key configuration directives include:

  • IPForward=yes: Instructs systemd-networkd to enable IPv4 and IPv6 kernel packet forwarding automatically across this interface.
  • IPMasquerade=both: Rewrites source IP addresses for both IPv4 and IPv6 outbound traffic. (Alternatively, use IPMasquerade=ipv4 if upstream IPv6 uses native routed prefixes without NAT).

Enabling Kernel IPv4 Forwarding in sysctl

While systemd-networkd manages per-interface forwarding flags, ensure system-wide IPv4 forwarding is permitted in the Linux kernel sysctl configuration:

sudo nano /etc/sysctl.d/99-ip-forward.conf

Add the following kernel parameters:

net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1

Apply the sysctl parameters immediately without rebooting:

sudo sysctl --system

Applying Configuration and Verifying Network State

Reload and restart systemd-networkd to parse the new network unit files and initialize the NAT masquerade chains:

sudo systemctl restart systemd-networkd

Verify that both interfaces are bound and healthy using networkctl:

networkctl status

The output confirms operational states:

● 2: enp1s0
       Link File: /usr/lib/systemd/network/99-default.link
    Network File: /etc/systemd/network/20-external.network
           State: routable (configured)
          Driver: virtio_net
            Type: ether
         Address: 203.0.113.45
         Gateway: 203.0.113.1

● 3: enp2s0
       Link File: /usr/lib/systemd/network/99-default.link
    Network File: /etc/systemd/network/10-internal.network
           State: routable (configured)
          Driver: virtio_net
            Type: ether
         Address: 192.168.100.1

Validating Active NAT Masquerading and Conntrack Tables

To confirm that systemd-networkd successfully injected kernel NAT rules, inspect the netfilter/iptables NAT table:

sudo iptables -t nat -L -v -n

Review the POSTROUTING chain output:

Chain POSTROUTING (policy ACCEPT 120 packets, 7200 bytes)
 pkts bytes target     prot opt in     out     source               destination
 1482  124K MASQUERADE all  --  *      enp1s0  0.0.0.0/0            0.0.0.0/0

The output explicitly confirms that all packets leaving via enp1s0 are processed by the MASQUERADE target.

To monitor active client translation connections in real time, inspect the Linux connection tracking subsystem using conntrack:

sudo apt install -y conntrack
sudo conntrack -L

Each active connection displays the original LAN client source (e.g. src=192.168.100.105) alongside the rewritten masqueraded WAN source (src=203.0.113.45), verifying complete end-to-end declarative NAT routing on Ubuntu Server.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.