How to Persistently Enable IP Forwarding for Dynamic Interfaces in Linux with Systemd


10 views

When working with dynamic network interfaces like VPN tunnels (tun0) or temporary interfaces, traditional methods of enabling IP forwarding fall short. The main challenge comes from:

# These fail during early boot:
net.ipv4.conf.enp0s3.forwarding=1
net.ipv4.conf.tun0.forwarding=1

For systemd-based systems (v229+), we can leverage networkd-dispatcher or custom units to handle dynamic interfaces:

# /etc/systemd/network/99-ipforwarding.network
[Match]
Name=enp0s3 tun0

[Network]
IPForward=yes

For physical interfaces that persist across reboots:

# /etc/sysctl.d/99-ipforward.conf
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net.ipv4.conf.all.rp_filter=2

Create a udev rule for interface hotplug events:

# /etc/udev/rules.d/99-ipforward.rules
ACTION=="add", SUBSYSTEM=="net", \
    RUN+="/usr/bin/sh -c 'echo 1 > /proc/sys/net/ipv4/conf/%k/forwarding'"

After implementing any solution, verify with:

sudo sysctl -p
ip route show table all
cat /proc/sys/net/ipv4/conf/*/forwarding | grep -v 1

If forwarding still fails:

  1. Check iptables/nftables rules: sudo iptables -L -v -n
  2. Verify interface MTU settings: ip link show dev tun0
  3. Test basic connectivity: ping -I enp0s3 8.8.8.8

When enabling IP forwarding in Linux, many administrators encounter a frustrating scenario: while net.ipv4.ip_forward=1 gets set globally, individual interfaces (especially dynamic ones like VPN tunnels) may still block forwarding. Here's what's happening under the hood:

# Typical sysctl.conf entry
net.ipv4.ip_forward = 1
net.ipv4.conf.default.forwarding = 1
net.ipv4.conf.all.forwarding = 1

Yet checking interface-specific settings reveals the problem:

cat /proc/sys/net/ipv4/conf/enp0s3/forwarding
0
cat /proc/sys/net/ipv4/conf/tun0/forwarding
0

The root cause lies in the timing of systemd-sysctl service execution. During early boot:

  • Persistent network interfaces (like enp0s3) may not be fully initialized
  • Dynamic interfaces (like tun0) don't exist yet

Systemd logs reveal the failure:

journalctl -u systemd-sysctl | grep forwarding
systemd-sysctl[85]: Couldn't write '1' to 'net/ipv4/conf/enp0s3/forwarding'
systemd-sysctl[85]: Couldn't write '1' to 'net/ipv4/conf/tun0/forwarding'

Method 1: NetworkManager Dispatcher Script (Recommended for Dynamic Interfaces)

Create a script that triggers whenever interfaces change:

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/99-ip-forwarding

INTERFACE="$1"
ACTION="$2"

if [ "$ACTION" = "up" ]; then
    echo 1 > /proc/sys/net/ipv4/conf/${INTERFACE}/forwarding
fi

Make it executable:

chmod +x /etc/NetworkManager/dispatcher.d/99-ip-forwarding

Method 2: Systemd Service for Persistent Interfaces

For static interfaces, create a service that runs after networking:

[Unit]
Description=Set interface forwarding
After=network.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c "echo 1 > /proc/sys/net/ipv4/conf/enp0s3/forwarding"

[Install]
WantedBy=multi-user.target

Method 3: Udev Rule for All Interface Events

This catches both existing and new interfaces:

# /etc/udev/rules.d/99-ip-forwarding.rules
ACTION=="add", SUBSYSTEM=="net", RUN+="/bin/sh -c 'echo 1 > /proc/sys/net/ipv4/conf/%k/forwarding'"

After implementing any solution, verify with:

# Check global setting
sysctl net.ipv4.ip_forward

# Check all interfaces
find /proc/sys/net/ipv4/conf/ -name forwarding -exec sh -c 'echo "$1: $(cat $1)"' sh {} \;

For temporary testing, you can manually enable forwarding:

# For existing interfaces
for intf in /proc/sys/net/ipv4/conf/*/forwarding; do
    echo 1 > "$intf"
done

# For new interfaces (run after creation)
echo 1 > /proc/sys/net/ipv4/conf/tun0/forwarding

While these methods work, consider these additional sysctl parameters for complete forwarding:

# /etc/sysctl.d/99-ip-forward.conf
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.accept_redirects = 0