<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.linuxize.net/index.php?action=history&amp;feed=atom&amp;title=Linux_router_with_WAN%2C_LAN_and_DMZ</id>
	<title>Linux router with WAN, LAN and DMZ - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.linuxize.net/index.php?action=history&amp;feed=atom&amp;title=Linux_router_with_WAN%2C_LAN_and_DMZ"/>
	<link rel="alternate" type="text/html" href="https://wiki.linuxize.net/index.php?title=Linux_router_with_WAN,_LAN_and_DMZ&amp;action=history"/>
	<updated>2026-10-17T01:27:18Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.41.5</generator>
	<entry>
		<id>https://wiki.linuxize.net/index.php?title=Linux_router_with_WAN,_LAN_and_DMZ&amp;diff=805&amp;oldid=prev</id>
		<title>Datanasov: New howto: router forwarding, networkd interfaces, minimal iptables with DMZ DNAT</title>
		<link rel="alternate" type="text/html" href="https://wiki.linuxize.net/index.php?title=Linux_router_with_WAN,_LAN_and_DMZ&amp;diff=805&amp;oldid=prev"/>
		<updated>2026-08-09T21:57:35Z</updated>

		<summary type="html">&lt;p&gt;New howto: router forwarding, networkd interfaces, minimal iptables with DMZ DNAT&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;Turning a Linux box into a router: enabling IP forwarding, addressing the three&lt;br /&gt;
interfaces with systemd-networkd, and a minimal iptables policy that publishes a&lt;br /&gt;
mail server and a web server from the DMZ.&lt;br /&gt;
&lt;br /&gt;
{{Box|1=Interfaces and addresses used throughout: &amp;#039;&amp;#039;&amp;#039;eth0&amp;#039;&amp;#039;&amp;#039; WAN, &amp;#039;&amp;#039;&amp;#039;eth1&amp;#039;&amp;#039;&amp;#039; LAN &amp;lt;code&amp;gt;10.0.1.0/24&amp;lt;/code&amp;gt;, &amp;#039;&amp;#039;&amp;#039;eth2&amp;#039;&amp;#039;&amp;#039; DMZ &amp;lt;code&amp;gt;10.0.0.0/24&amp;lt;/code&amp;gt;. The router holds the &amp;lt;code&amp;gt;.1&amp;lt;/code&amp;gt; in each subnet — &amp;lt;code&amp;gt;10.0.1.1&amp;lt;/code&amp;gt; on the LAN, &amp;lt;code&amp;gt;10.0.0.1&amp;lt;/code&amp;gt; on the DMZ.}}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Role !! Interface !! Address&lt;br /&gt;
|-&lt;br /&gt;
| Router, WAN || eth0 || from the ISP (DHCP or static)&lt;br /&gt;
|-&lt;br /&gt;
| Router, LAN || eth1 || &amp;lt;code&amp;gt;10.0.1.1/24&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Router, DMZ || eth2 || &amp;lt;code&amp;gt;10.0.0.1/24&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Mail server || — || &amp;lt;code&amp;gt;10.0.0.10&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Web server || — || &amp;lt;code&amp;gt;10.0.0.20&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Enable IP forwarding ==&lt;br /&gt;
Without this the kernel drops anything not addressed to itself, and no amount of&lt;br /&gt;
firewall rules will make traffic cross between interfaces.&lt;br /&gt;
&lt;br /&gt;
Immediately, until reboot:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sysctl -w net.ipv4.ip_forward=1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Permanently:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cat &amp;gt;/etc/sysctl.d/99-router.conf &amp;lt;&amp;lt;&amp;#039;EOF&amp;#039;&lt;br /&gt;
net.ipv4.ip_forward = 1&lt;br /&gt;
# Uncomment if the router also routes IPv6:&lt;br /&gt;
# net.ipv6.conf.all.forwarding = 1&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sysctl --system&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Check the value in force:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sysctl net.ipv4.ip_forward&lt;br /&gt;
cat /proc/sys/net/ipv4/ip_forward&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Disable it again ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sysctl -w net.ipv4.ip_forward=0&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To make that stick, set the value to &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; in&lt;br /&gt;
&amp;lt;code&amp;gt;/etc/sysctl.d/99-router.conf&amp;lt;/code&amp;gt; and run &amp;lt;code&amp;gt;sysctl --system&amp;lt;/code&amp;gt;&lt;br /&gt;
— deleting the file only reverts to the kernel default, which is also&lt;br /&gt;
&amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt;, but leaves nothing documenting the intent.&lt;br /&gt;
&lt;br /&gt;
Turning forwarding off stops routing but leaves the NAT and filter rules loaded.&lt;br /&gt;
To stand the router down completely, flush them as well — see&lt;br /&gt;
[[#Tearing it down|Tearing it down]].&lt;br /&gt;
&lt;br /&gt;
{{Box|1=systemd-networkd can set forwarding itself, but the option was renamed: &amp;lt;code&amp;gt;IPForward=&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;[Network]&amp;lt;/code&amp;gt; is deprecated from systemd 256 in favour of &amp;lt;code&amp;gt;IPv4Forwarding=&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;IPv6Forwarding=&amp;lt;/code&amp;gt;. &amp;lt;code&amp;gt;sysctl&amp;lt;/code&amp;gt; behaves the same on every version, so the examples above use it.}}&lt;br /&gt;
&lt;br /&gt;
== Interfaces with systemd-networkd ==&lt;br /&gt;
One file per interface in &amp;lt;code&amp;gt;/etc/systemd/network/&amp;lt;/code&amp;gt;. The numeric prefix&lt;br /&gt;
only controls the order the files are read.&lt;br /&gt;
&lt;br /&gt;
WAN — &amp;lt;code&amp;gt;/etc/systemd/network/10-wan.network&amp;lt;/code&amp;gt;:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ini&amp;quot;&amp;gt;&lt;br /&gt;
[Match]&lt;br /&gt;
Name=eth0&lt;br /&gt;
&lt;br /&gt;
[Network]&lt;br /&gt;
DHCP=ipv4&lt;br /&gt;
# Static instead? Drop DHCP= and use:&lt;br /&gt;
# Address=203.0.113.10/24&lt;br /&gt;
# Gateway=203.0.113.1&lt;br /&gt;
# DNS=1.1.1.1&lt;br /&gt;
&lt;br /&gt;
[DHCPv4]&lt;br /&gt;
UseDNS=yes&lt;br /&gt;
UseNTP=yes&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
LAN — &amp;lt;code&amp;gt;/etc/systemd/network/20-lan.network&amp;lt;/code&amp;gt;:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ini&amp;quot;&amp;gt;&lt;br /&gt;
[Match]&lt;br /&gt;
Name=eth1&lt;br /&gt;
&lt;br /&gt;
[Network]&lt;br /&gt;
Address=10.0.1.1/24&lt;br /&gt;
ConfigureWithoutCarrier=yes&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
DMZ — &amp;lt;code&amp;gt;/etc/systemd/network/30-dmz.network&amp;lt;/code&amp;gt;:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ini&amp;quot;&amp;gt;&lt;br /&gt;
[Match]&lt;br /&gt;
Name=eth2&lt;br /&gt;
&lt;br /&gt;
[Network]&lt;br /&gt;
Address=10.0.0.1/24&lt;br /&gt;
ConfigureWithoutCarrier=yes&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Apply and inspect:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
systemctl enable --now systemd-networkd&lt;br /&gt;
networkctl reload&lt;br /&gt;
networkctl status eth1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Handing out addresses on the LAN is a separate job — see&lt;br /&gt;
[[DHCP and NTP for the LAN with systemd-networkd]].&lt;br /&gt;
&lt;br /&gt;
== Minimal firewall ==&lt;br /&gt;
Default deny inbound and forwarded, allow outbound, and let conntrack carry&lt;br /&gt;
return traffic. Everything published from the DMZ is opened explicitly.&lt;br /&gt;
&lt;br /&gt;
=== Policies and baseline ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Start from a clean slate&lt;br /&gt;
iptables -F&lt;br /&gt;
iptables -t nat -F&lt;br /&gt;
iptables -X&lt;br /&gt;
&lt;br /&gt;
iptables -P INPUT   DROP&lt;br /&gt;
iptables -P FORWARD DROP&lt;br /&gt;
iptables -P OUTPUT  ACCEPT&lt;br /&gt;
&lt;br /&gt;
# Loopback, and anything conntrack already knows about&lt;br /&gt;
iptables -A INPUT -i lo -j ACCEPT&lt;br /&gt;
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT&lt;br /&gt;
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Services on the router itself ===&lt;br /&gt;
Only from the LAN. Nothing on the router listens to the WAN.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -A INPUT -i eth1 -p tcp --dport 22  -j ACCEPT   # ssh&lt;br /&gt;
iptables -A INPUT -i eth1 -p udp --dport 67  -j ACCEPT   # dhcp server&lt;br /&gt;
iptables -A INPUT -i eth1 -p udp --dport 123 -j ACCEPT   # ntp server&lt;br /&gt;
iptables -A INPUT -i eth1 -p udp --dport 53  -j ACCEPT   # dns, if the router resolves&lt;br /&gt;
iptables -A INPUT -i eth1 -p tcp --dport 53  -j ACCEPT&lt;br /&gt;
&lt;br /&gt;
# Ping, rate limited, from anywhere&lt;br /&gt;
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/s -j ACCEPT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Publish the DMZ servers (DNAT) ===&lt;br /&gt;
&amp;lt;code&amp;gt;25&amp;lt;/code&amp;gt; is plain SMTP with STARTTLS, &amp;lt;code&amp;gt;587&amp;lt;/code&amp;gt; is the submission&lt;br /&gt;
port (also STARTTLS) and &amp;lt;code&amp;gt;465&amp;lt;/code&amp;gt; is implicit TLS submission. Drop&lt;br /&gt;
whichever of the two submission ports your mail server does not offer.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Mail -&amp;gt; 10.0.0.10&lt;br /&gt;
iptables -t nat -A PREROUTING -i eth0 -p tcp -m multiport \&lt;br /&gt;
         --dports 25,465,587,993 -j DNAT --to-destination 10.0.0.10&lt;br /&gt;
&lt;br /&gt;
# Web -&amp;gt; 10.0.0.20&lt;br /&gt;
iptables -t nat -A PREROUTING -i eth0 -p tcp -m multiport \&lt;br /&gt;
         --dports 80,443 -j DNAT --to-destination 10.0.0.20&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
DNAT only rewrites the destination. The packet still has to survive&lt;br /&gt;
&amp;lt;code&amp;gt;FORWARD&amp;lt;/code&amp;gt;, which is where the matching accepts belong:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT&lt;br /&gt;
iptables -A FORWARD -m conntrack --ctstate INVALID -j DROP&lt;br /&gt;
&lt;br /&gt;
iptables -A FORWARD -i eth0 -o eth2 -d 10.0.0.10 -p tcp -m multiport \&lt;br /&gt;
         --dports 25,465,587,993 -j ACCEPT&lt;br /&gt;
iptables -A FORWARD -i eth0 -o eth2 -d 10.0.0.20 -p tcp -m multiport \&lt;br /&gt;
         --dports 80,443 -j ACCEPT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Egress and inter-zone policy ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT   # LAN  -&amp;gt; internet&lt;br /&gt;
iptables -A FORWARD -i eth2 -o eth0 -j ACCEPT   # DMZ  -&amp;gt; internet (updates, outbound mail)&lt;br /&gt;
iptables -A FORWARD -i eth1 -o eth2 -j ACCEPT   # LAN  -&amp;gt; DMZ (administration)&lt;br /&gt;
&lt;br /&gt;
# DMZ -&amp;gt; LAN is deliberately absent. That asymmetry is the whole point of a&lt;br /&gt;
# DMZ: if a published server is compromised it must not reach the LAN.&lt;br /&gt;
# Return traffic for LAN-initiated connections is already covered by the&lt;br /&gt;
# ESTABLISHED,RELATED rule above.&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Source NAT ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;MASQUERADE&amp;lt;/code&amp;gt; takes the WAN address as it finds it, which is what you&lt;br /&gt;
want on a dynamic ISP address. On a static WAN address &amp;lt;code&amp;gt;SNAT&amp;lt;/code&amp;gt; is&lt;br /&gt;
marginally cheaper because it does not look the address up per connection:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to-source 203.0.113.10&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Reaching the servers from the LAN ===&lt;br /&gt;
A LAN client resolving the public name gets the WAN address, sends the packet to&lt;br /&gt;
the router, and the reply comes back from &amp;lt;code&amp;gt;10.0.0.10&amp;lt;/code&amp;gt; directly —&lt;br /&gt;
so the connection never completes. Either give the LAN a split-horizon DNS view&lt;br /&gt;
that answers with the internal address, or add hairpin NAT:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -t nat -A POSTROUTING -s 10.0.1.0/24 -d 10.0.0.10 -p tcp -m multiport \&lt;br /&gt;
         --dports 25,465,587,993 -j MASQUERADE&lt;br /&gt;
iptables -t nat -A POSTROUTING -s 10.0.1.0/24 -d 10.0.0.20 -p tcp -m multiport \&lt;br /&gt;
         --dports 80,443 -j MASQUERADE&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Split-horizon DNS is the tidier of the two — hairpin NAT hides the client&amp;#039;s real&lt;br /&gt;
address from the server, so its logs show the router for every internal visitor.&lt;br /&gt;
&lt;br /&gt;
== Make the rules survive a reboot ==&lt;br /&gt;
Debian and Ubuntu:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install iptables-persistent&lt;br /&gt;
iptables-save &amp;gt; /etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
RHEL, CentOS, Rocky:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
dnf install iptables-services&lt;br /&gt;
systemctl enable --now iptables&lt;br /&gt;
iptables-save &amp;gt; /etc/sysconfig/iptables&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Box|type=warning|1=Save only after you have confirmed you can still reach the box. A default-DROP &amp;lt;code&amp;gt;INPUT&amp;lt;/code&amp;gt; policy saved with no working ssh rule locks you out until you get physical or console access.}}&lt;br /&gt;
&lt;br /&gt;
== Verify ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Rules, with counters, as they are actually loaded&lt;br /&gt;
iptables -L -v -n --line-numbers&lt;br /&gt;
iptables -t nat -L -v -n&lt;br /&gt;
&lt;br /&gt;
# Watch a DNAT entry appear while testing from outside&lt;br /&gt;
conntrack -L -d 10.0.0.20&lt;br /&gt;
&lt;br /&gt;
# From an external host&lt;br /&gt;
nc -vz &amp;lt;wan-address&amp;gt; 443&lt;br /&gt;
nc -vz &amp;lt;wan-address&amp;gt; 993&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If a published port does not answer, check the &amp;lt;code&amp;gt;FORWARD&amp;lt;/code&amp;gt; counters&lt;br /&gt;
first. Traffic that was DNATed but has no matching &amp;lt;code&amp;gt;FORWARD&amp;lt;/code&amp;gt; accept is&lt;br /&gt;
the usual cause, and it shows up as a rising policy-drop count rather than an&lt;br /&gt;
error anywhere.&lt;br /&gt;
&lt;br /&gt;
== Tearing it down ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sysctl -w net.ipv4.ip_forward=0&lt;br /&gt;
&lt;br /&gt;
iptables -P INPUT   ACCEPT&lt;br /&gt;
iptables -P FORWARD ACCEPT&lt;br /&gt;
iptables -P OUTPUT  ACCEPT&lt;br /&gt;
iptables -F&lt;br /&gt;
iptables -t nat -F&lt;br /&gt;
iptables -X&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Box|1=On current distributions &amp;lt;code&amp;gt;iptables&amp;lt;/code&amp;gt; is usually a front end to nftables (&amp;lt;code&amp;gt;iptables-nft&amp;lt;/code&amp;gt;). The syntax above is unchanged; &amp;lt;code&amp;gt;iptables -V&amp;lt;/code&amp;gt; reports which backend is in use, and rules created this way are visible with &amp;lt;code&amp;gt;nft list ruleset&amp;lt;/code&amp;gt;.}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Networking]]&lt;br /&gt;
[[Category:Linux]]&lt;br /&gt;
[[Category:Howto]]&lt;/div&gt;</summary>
		<author><name>Datanasov</name></author>
	</entry>
</feed>