// DevOps

Routing traffic from a local subnet through a remote server (IPIP + Policy Routing)

Published on 2026-09-22

The guide shows how to configure two Linux servers so that all internet traffic from a specific local subnet (for example, 10.100.10.0/24) goes not via its usual gateway but through an IPIP tunnel to a remote server that releases it to the Internet.

This scheme is needed when services from one subnet must access the Internet using the IP address of another server: for centralized NAT, working with partners by whitelisting addresses, or a single egress point for multiple sites. Other traffic from the gateway itself continues via the normal path.

IPIP does not encrypt traffic. If the tunnel traverses the Internet and data must be protected, use WireGuard or IPsec — see the article “Redundant links: between offices” for options for inter-site connectivity.


Initial data (example addresses)

RoleNameExternal (WAN) IPWAN interfaceAddress in tunnelNote
Server ALocal gateway198.51.100.10eth010.254.0.1/30Gateway for subnet 10.100.10.0/24
Server BRemote egress node203.0.113.20eth010.254.0.2/30Releases traffic to the Internet
  • Subnet to route: 10.100.10.0/24
  • Tunnel name: ipip0
  • Tunnel network: 10.254.0.0/30

Step 1: Configure Server B (egress node)

Goal: accept the IPIP tunnel from Server A and release traffic from the subnet 10.100.10.0/24 to the Internet via NAT using address 203.0.113.20.

1.1. Enable IP forwarding

bash
sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward = 1" > /etc/sysctl.d/90-ipip-forward.conf

1.2. Tunnel, return route, firewall and NAT

bash
#!/bin/sh
set -e

TUN_NAME="ipip0"
REMOTE_IP="198.51.100.10"
LOCAL_IP="203.0.113.20"
TUN_NET="10.254.0.2/30"
TUN_PEER="10.254.0.1"
WAN_IFACE="eth0"
CLIENT_NET="10.100.10.0/24"

echo "[*] Creating tunnel $TUN_NAME"
ip tunnel del "$TUN_NAME" 2>/dev/null || true
ip tunnel add "$TUN_NAME" mode ipip remote "$REMOTE_IP" local "$LOCAL_IP" ttl 64
ip addr add "$TUN_NET" dev "$TUN_NAME"
ip link set "$TUN_NAME" up mtu 1480

echo "[*] Adding return route to client subnet via tunnel"
ip route replace "$CLIENT_NET" via "$TUN_PEER" dev "$TUN_NAME"

echo "[*] Configuring iptables"
iptables -I INPUT -p 4 -s "$REMOTE_IP" -j ACCEPT
iptables -A FORWARD -i "$TUN_NAME" -s "$CLIENT_NET" -o "$WAN_IFACE" -j ACCEPT
iptables -A FORWARD -i "$WAN_IFACE" -d "$CLIENT_NET" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

iptables -t nat -A POSTROUTING -s "$CLIENT_NET" -o "$WAN_IFACE" -j SNAT --to-source "$LOCAL_IP"

iptables -t mangle -A FORWARD -o "$WAN_IFACE" -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

echo "[OK] Server B is ready."

The return route is mandatory: after address translation the responses are addressed to machines in 10.100.10.0/24, and without this route Server B will send them to the Internet instead of into the tunnel.

IPIP adds 20 bytes of header to each packet, so the tunnel MTU is 1480. The TCPMSS --clamp-mss-to-pmtu rule reduces TCP segment size to match this MTU so that connections don’t stall due to PMTU Discovery issues.


Step 2: Configure Server A (local gateway)

Goal: direct traffic from subnet 10.100.10.0/24 that is destined for the Internet into the tunnel to Server B.

2.1. IP forwarding and rp_filter

bash
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.default.rp_filter=2

cat <<EOF > /etc/sysctl.d/90-ipip-gw.conf
net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
EOF

The mode rp_filter=2 (loose) is needed because routing here is asymmetric: responses from the Internet arrive via the tunnel rather than via the interface that the default route points to. The kernel applies the maximum of the all and interface values — why this is and how to check it is discussed in the article “What is rp_filter”.


2.2. Create the IPIP tunnel

bash
#!/bin/sh
set -e

TUN_NAME="ipip0"
REMOTE_IP="203.0.113.20"
LOCAL_IP="198.51.100.10"
TUN_NET="10.254.0.1/30"
TUN_PEER="10.254.0.2"

echo "[*] Bringing up tunnel $TUN_NAME"
ip tunnel del "$TUN_NAME" 2>/dev/null || true
ip tunnel add "$TUN_NAME" mode ipip remote "$REMOTE_IP" local "$LOCAL_IP" ttl 64
ip addr add "$TUN_NET" dev "$TUN_NAME"
ip link set "$TUN_NAME" up mtu 1480

echo "[OK] Tunnel is up."

2.3. Policy routing

Add a routing table

bash
grep -q "100[[:space:]]tunnel_route" /etc/iproute2/rt_tables || echo "100 tunnel_route" >> /etc/iproute2/rt_tables

If the file /etc/iproute2/rt_tables does not exist on your system, create it manually or use the table number 100 directly in the commands instead of the name.

Add a default route into that table

bash
ip route add default via 10.254.0.2 dev ipip0 table tunnel_route

Create a rule

bash
ip rule add from 10.100.10.0/24 table tunnel_route pref 1000
ip route flush cache

The rule sends all traffic from the subnet into the tunnel, including accesses to other local networks. If the subnet should reach neighboring networks directly, add a rule with a lower preference before it, for example: ip rule add from 10.100.10.0/24 to 10.0.0.0/8 table main pref 900


2.4. Firewall

bash
CLIENT_NET="10.100.10.0/24"
TUN_NAME="ipip0"

iptables -A FORWARD -s "$CLIENT_NET" -o "$TUN_NAME" -j ACCEPT
iptables -A FORWARD -i "$TUN_NAME" -d "$CLIENT_NET" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -t mangle -A FORWARD -o "$TUN_NAME" -p tcp --tcp-flags SYN,RST SYN -j TCPMSS ---clamp-mss-to-pmtu

echo "[OK] Firewall and policy routing configured."

Step 3: Testing

Route for traffic from Server A itself

bash
ip route get 8.8.8.8

Expected normal route:

8.8.8.8 via <your_gateway> dev eth0 src 198.51.100.10 ...

Route for a packet from the subnet

To test forwarding, specify the interface on which the packet arrives from the LAN (iif): without it the kernel checks the route for a locally generated packet.

bash
ip route get 8.8.8.8 from 10.100.10.50 iif eth1

Here eth1 is Server A’s interface towards the subnet 10.100.10.0/24. Expected route via the tunnel:

8.8.8.8 from 10.100.10.50 via 10.254.0.2 dev ipip0 ...

Diagnostics

1. Tunnel

bash
ip addr show dev ipip0
ip -s tunnel show
ping -I ipip0 10.254.0.2

If the ping fails, check that firewalls on both servers and the providers allow the IPIP protocol (IP protocol number 4).


2. Policy routing

bash
ip rule show
ip route show table tunnel_route

The output of ip rule show should contain a line:

1000: from 10.100.10.0/24 lookup tunnel_route

3. NAT on Server B

bash
iptables -t nat -L POSTROUTING -v -n

With active traffic, the counters for the SNAT rule should increase.


4. External address for machines in the subnet

On any machine in the 10.100.10.0/24 subnet:

bash
curl -4 ifconfig.me

Expected 203.0.113.20.


5. MTU and MSS

If TCP connections stall:

bash
ip link show ipip0
ping -M do -s 1452 8.8.8.8

A 1452-byte payload plus 28 bytes of ICMP and IP headers exactly fits the tunnel MTU of 1480. Check that the tunnel shows mtu 1480 and that the --clamp-mss-to-pmtu rule is in place.


6. tcpdump

bash
tcpdump -ni ipip0
tcpdump -ni eth0 host 8.8.8.8

7. Temporary logging

bash
iptables -I FORWARD 1 -j LOG --log-prefix "[FORWARD] "
tail -f /var/log/kern.log

After diagnostics remove this rule: it logs every forwarded packet.


8. Return path

From a machine in the subnet:

bash
traceroute 8.8.8.8

The first hop should be Server A, the next hop should be Server B’s tunnel address 10.254.0.2. From Server B:

bash
traceroute -s 10.254.0.2 10.100.10.10

If traceroute from Server B goes to the Internet instead of into the tunnel, check the return route from step 1.2.


9. Persisting settings

The commands above are effective until reboot. Save iptables rules like this (on Debian and Ubuntu with the iptables-persistent package):

bash
iptables-save > /etc/iptables/rules.v4

The tunnel, routes and policy routing rules must be recreated after reboot: the most convenient way is to wrap the scripts from steps 1.2, 2.2 and 2.3 into systemd services. Saving the output of ip rule show to a file by itself does not restore anything.


Additional recommendations

ItemRecommendation
MTU1480 for the tunnel and TCPMSS clamp on both servers
FirewallOn modern systems consider using nftables
AutostartStart the tunnel and routes via systemd
Monitoringtcpdump -i ipip0 shows tunnel traffic
SecurityAllow IPIP only from the second server’s address; for encryption — WireGuard or IPsec

A similar task on MikroTik — returning traffic via the same gateway it came from — is described in the article “MikroTik: return traffic via the same gateway”.


Conclusion

All Internet traffic from subnet 10.100.10.0/24 exits to the Internet using Server B’s address (203.0.113.20), while Server A continues to use its normal route. Three things are required for this scheme to work: a tunnel with the correct MTU, a policy routing rule on Server A, and a return route to the subnet on Server B.

// Contact

Need help?

Get in touch with me and I'll help solve the problem

I reply within one business day (03:00-13:00 GMT)

Или оставьте заявку здесь:

Confirm that you are not a bot.

Write and get a quick reply