Skip Navigation

IPv4 NAT Router

A simple router supporting only Internet Protocol version 4 (IPv4) with Network Address Translation (NAT) is fairly easy to configure on a Linux system.

Page Contents

Video Lecture


Watch at Internet Archive

The Use Case

This example was originally designed for a virtual machine, which routes traffic from the private LAN with IPv4 address range 172.16.0.1 through 172.16.0.255. The WAN side of the router uses network interface enp0s3 and connects to an upstream router with a dynamic IPv4 address supplied by DHCP. The LAN side of the router uses network interface enp0s8 and has static IP address 172.16.0.1.

Although this router is designed for a virtual machine use case, it is easily adapted to work on a physical machine. Simply changing the network interfaces and address ranges to match those of the physical system should be sufficient to build a router from any Linux computer. The only hardware requirement is that the router will need two separate network ports: one for the private LAN and the other for the WAN (Internet) connection.

iptables Rules

Since this router only works with IPv4, only iptables rules are presented. We assume that IPv6 is not configured on this virtual machine.

*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A POSTROUTING -o enp0s3 -j MASQUERADE
COMMIT
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -s 172.16.0.0/24 -i enp0s8 -p icmp -j ACCEPT
-A INPUT -s 172.16.0.0/24 -i enp0s8 -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
-A INPUT -s 172.16.0.0/24 -i enp0s8 -p udp -m state --state NEW -m udp --dport 67 -j ACCEPT
-A INPUT -s 172.16.0.0/24 -i enp0s8 -j REJECT --reject-with icmp-host-prohibited
-A INPUT -j DROP
-A FORWARD -d 172.16.0.0/24 -i enp0s8 -j DROP
-A FORWARD -s 172.16.0.0/24 -i enp0s8 -j ACCEPT
-A FORWARD -d 172.16.0.0/24 -i enp0s3 -j ACCEPT
-A FORWARD -i enp0s8 -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j DROP
-A OUTPUT -j ACCEPT
COMMIT

Other Settings

In addition to enabling routing in the iptables configuration, it is also necessary to enable IPv4 packet forwarding in the kernel. Some distributions, such as Alpine Linux, have a setting in the iptables service configuration that can enable forwarding automatically. However, the general cross-distribution approach is to use the sysctl facility.

To enable IPv4 forwarding temporarily, run (as root):

sysctl -w net.ipv4.ip_forward=1

This setting will be lost whenever the machine reboots. To make it permanent, it is necessary to edit the sysctl configuration files, the locations of which vary by distribution. For many modern distributions, check to see if the directory /etc/sysctl.d exists. If it does, you should be able to create a file in that directory as the root user. The name doesn’t matter, but it needs to end in .conf. For example, one could create /etc/sysctl.d/ipv4-forward.conf with the contents:

net.ipv4.ip_forward=1

Some distributions might provide an /etc/sysctl.conf file, either in addition to, or instead of, the /etc/sysctl.d directory. In this case, simply add the above line to that file.

Explaining the iptables Rules

Since we’re doing Network Address Translation (NAT), we have a rule in the nat table to enable masquerading, which is the term for changing the source IP address on a packet from that of the machine that sent it to that of the router. From the outside network, the packet appears to originate at the router. However, the router internally maintains a list of connections with their associated original sources, destinations, and port information. Whenever a response to a packet that has been masqueraded is received at the router, the router uses this table to rewrite the packet to be destined to the machine that originally sent it. This process is mostly transparent to the systems on the LAN side of the router.

annotated nat table

Figure 1: Annotated code for the basic router’s nat table.

As explained in Figure 1, the router’s nat table is configured with default ACCEPT policies for its PREROUTING, POSTROUTING, and OUTPUT chains. Packet counters are also set to zero packets and zero bytes. We have only one rule, which is appended to the POSTROUTING chain. This rule tells the kernel to rewrite the source address on packets leaving the router on interface enp0s3 to the IPv4 address of the router itself. Response packets received in reply to these packets will be rewritten to the destination address of the original source of the outgoing packets. This behavior is the function of the MASQUERADE target.

annotated filter table

Figure 2: Annotated code for the basic router’s filter table.

In Figure 2, we can see that the filter table is more complex for this router configuration. First, we use a default policy of DROP for the INPUT and FORWARD chains. In a VM environment with an upstream private network, we could safely use REJECT here instead. Technically, REJECT complies with network standards and is a better choice from a purely network-oriented perspective. However, if this router were ever to become public on the Internet, DROP offers a bit of security hardening by making the system appear invisible to port scanners and third-party attackers to whom we do not directly connect first. In addition, DROP can improve privacy by making the router harder to discover.

Our rules permit incoming SSH connections on TCP port 22 from the LAN side only, which permits system administrators using devices on the LAN to log into the router for maintenance purposes. Since we’re also going to provide a DHCP server on a typical router, we allow UDP port 67 from the LAN. Note that we must conditionally ACCEPT, REJECT, or DROP packets depending on their source interface and IPv4 address. The rules that enable this behavior, as well as the rules than enable forwarding, are described in the following sections.

Conditional Packet Rejection

In many cases, we want a network router that will be connected to the Internet to be “stealthy.” Ideally, if an attacker attempts to connect to the router from the outside world, no ports will appear to be open, giving the attacker nothing to try to attack. In fact, it won’t even be possible for the attacker to determine if the router is online unless something on the LAN happens to be connecting directly to the attacker or the attacker is able to eavesdrop on traffic between the router and a destination (NSA, anyone?).

We can implement conditional rejection by sending a client on the LAN a REJECT response whenever it tries to access a port on the router that we haven’t explicitly opened. On the WAN side, we DROP the packet instead. Rules to do this conditional rejection for our example are:

-A INPUT -s 172.16.0.0/24 -i enp0s8 -j REJECT --reject-with icmp-host-prohibited
-A INPUT -j DROP

The order of the rules is important! We need to match incoming connections from the LAN side first, since the jump to the DROP target is unconditional. If we had the rules reversed, all INPUT traffic that didn’t correspond to an open port would be DROPped, regardless of whether it originated from the LAN or the WAN side. Let’s take a look at the first rule in a bit more detail:

-A INPUT -s 172.16.0.0/24 -i enp0s8 -j REJECT --reject-with icmp-host-prohibited

This rule Appends to the INPUT chain, matching a source address in our private LAN-side address range, coming from interface enp0s8. If both these conditions are true, jump to the REJECT target and send an icmp-host-prohibited response. Otherwise, we fall through to the next rule (which is to DROP). Note that we check for both the private IP address range and the private network interface, since an attacker might send us a packet with a spoofed IP source address.

Packet Forwarding

Enabling packet forwarding in our iptables example is done with the rules:

-A FORWARD -d 172.16.0.0/24 -i enp0s8 -j DROP
-A FORWARD -s 172.16.0.0/24 -i enp0s8 -j ACCEPT
-A FORWARD -d 172.16.0.0/24 -i enp0s3 -j ACCEPT
-A FORWARD -i enp0s8 -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j DROP

Note that these rules are necessary, but not sufficient, for implementing a Linux router. We also need to enable IPv4 forwarding as shown in the above section.

Three rules on the FORWARD chain of the filter table are responsible for allowing traffic from the LAN to be routed to the WAN:

-A FORWARD -d 172.16.0.0/24 -i enp0s8 -j DROP
-A FORWARD -s 172.16.0.0/24 -i enp0s8 -j ACCEPT
-A FORWARD -d 172.16.0.0/24 -i enp0s3 -j ACCEPT

Let’s look at each one in detail, starting with the first one:

-A FORWARD -d 172.16.0.0/24 -i enp0s8 -j DROP

This rule DROPs packets arriving on the LAN side network interface, which have a destination somewhere on the LAN side of the network. If we see a packet that matches this rule, it means that one of our LAN devices isn’t configured correctly. Traffic destined for another machine on the same LAN segment isn’t sent to the router. Instead, it is sent directly between machines, using the Address Resolution Protocol (ARP) to discover the link-layer address of the destination host. We don’t want to route these packets to the WAN, since they’re destined for the LAN. We also don’t want to do the ARP procedure ourselves, since that isn’t the router’s job. Instead, we DROP such packets, which will make it clear to the administrator of the misbehaving device that something isn’t configured correctly when it cannot connect to another device on the LAN.

Now consider the other two rules, which actually enable packet forwarding for packets that we should be routing:

-A FORWARD -s 172.16.0.0/24 -i enp0s8 -j ACCEPT
-A FORWARD -d 172.16.0.0/24 -i enp0s3 -j ACCEPT

We want to ACCEPT packets for FORWARDing if the source address is in the LAN (but the destination address is on the WAN) or if the destination address is in the LAN for packets coming from the WAN. Beware of IP address spoofing! Always check that packets are arriving on the correct interface before forwarding them.

Bastion Firewalls

Our simple router firewall is an example of a bastion firewall. It restricts the types of packets permitted through the router, giving us some security for our LAN and the devices connected to it. Due to NAT, an attacker cannot simply connect directly from the WAN side into a system on the LAN side without first being stopped by the router. Entire classes of potential attacks against devices can be prevented by having a good bastion firewall between the LAN and WAN.

That said, a bastion firewall does NOT protect against all types of attacks! Furthermore, not all attackers are located outside the LAN. Attacks from malicious actors or compromised devices can be launched from the private network. For these reasons, each host on the LAN should have its own firewall to help protect against such threats.

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.