You can build a surprisingly effective load balancer using just iptables and its Network Address Translation (NAT) capabilities.
Let’s say you have a single public IP address, 192.0.2.10, and you want to distribute incoming traffic for port 80 (HTTP) across three backend web servers: 10.0.0.1, 10.0.0.2, and 10.0.0.3.
Here’s how you’d set it up on your load balancer machine (which has the public IP 192.0.2.10 and is on the same network as the backend servers).
First, you need to enable IP forwarding on the load balancer. This allows the machine to pass packets between interfaces.
sudo sysctl net.ipv4.ip_forward=1
This sets the ip_forward kernel parameter to 1, telling the kernel to allow packet forwarding. Without this, the iptables rules would have no effect as the system wouldn’t route the packets.
Now, for the iptables rules. We’ll use the nat table for this.
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode random --rand-between 1 3 -j DNAT --to-destination 10.0.0.1:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode random --rand-between 1 2 -j DNAT --to-destination 10.0.0.2:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.0.0.3:80
Let’s break this down:
-t nat: We’re working with the NAT table.-A PREROUTING: We’re appending rules to thePREROUTINGchain, which is processed as soon as a packet arrives on the network interface, before any routing decisions are made. This is crucial for modifying the destination IP before the system decides where to send it.-p tcp --dport 80: This matches incoming TCP traffic destined for port 80.-m statistic --mode random --rand-between 1 3: This is where the load balancing magic happens. Thestatisticmodule allows us to use packet statistics. Here,--mode randommeans we’ll randomly select a target.--rand-between 1 3means it will pick a number between 1 and 3 (inclusive) for each incoming packet.-j DNAT --to-destination <ip>:<port>: This is the Destination Network Address Translation target. It rewrites the destination IP address and port of the packet.
The three rules work in sequence. The first rule uses --rand-between 1 3. If the random number generator picks 1, the packet is sent to 10.0.0.1. If it picks 2 or 3, the packet falls through to the next rule.
The second rule uses --rand-between 1 2. If the random number generator picks 1, the packet is sent to 10.0.0.2. If it picks 2, it falls through to the third rule.
The third rule has no random module. It simply catches all remaining packets and sends them to 10.0.0.3.
This effectively distributes traffic:
- About 1/3 goes to
10.0.0.1. - Of the remaining 2/3, about half (1/3 of the total) goes to
10.0.0.2. - The rest (1/3 of the total) goes to
10.0.0.3.
This gives you a rough 33/33/33 split. You can adjust the --rand-between values for different distributions. For example, to send 50% to server A and 50% to server B:
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode random --rand-between 1 2 -j DNAT --to-destination 10.0.0.1:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.0.0.2:80
Now, a critical piece for any load balancer: how do the backend servers respond? If they send their replies directly back to the client using their own private IP addresses, the client will get confused because the original request came from 192.0.2.10. This is known as the "Direct Server Return" or "Asymmetric Routing" problem.
To fix this, we need to ensure that the backend servers send their replies back through the load balancer. This is achieved by setting the load balancer as the default gateway for the backend servers.
On each backend server (10.0.0.1, 10.0.0.2, 10.0.0.3), you would configure their network settings to use 192.0.2.10 as their default gateway. This means any traffic destined for an IP address not on their local subnet will be sent to 192.0.2.10.
However, there’s a subtle but important iptables rule needed on the load balancer to handle the return traffic. The PREROUTING chain modifies the destination IP. For the return traffic, the source IP will be one of the backend servers, and the destination IP will be the original client. The load balancer needs to know how to handle this.
The SNAT (Source Network Address Translation) rule in the POSTROUTING chain accomplishes this. It makes the load balancer’s public IP (192.0.2.10) appear as the source IP for all outgoing traffic from the backend servers.
sudo iptables -t nat -A POSTROUTING -j SNAT --to-source 192.0.2.10
-A POSTROUTING: This chain is processed just before the packet leaves the network interface.-j SNAT --to-source 192.0.2.10: This rewrites the source IP address of any packet leaving the load balancer to be192.0.2.10. This ensures that when the backend servers reply, the reply packet’s source IP is192.0.2.10, which the client expects.
This simple iptables setup provides basic, stateless load balancing. It doesn’t perform health checks, so if a backend server goes down, traffic will continue to be sent to it, resulting in errors for those connections. For more advanced features like health checks, session persistence, or SSL termination, you’d typically look at dedicated load balancing software like HAProxy or Nginx.
If you’ve set up the PREROUTING rules correctly but are still seeing traffic hitting only one server, you’ll likely encounter the next problem: your iptables rules aren’t being saved and are lost on reboot. You’ll need to use iptables-save and iptables-restore or a service like iptables-persistent.