Seven Configuration Changes That Turn a $6 Linux VPS Into a Full Network Router โ And Why I Stopped Buying Dedicated Firewall Appliances
Three months ago I paid $340 for a Ubiquiti UDM Pro to handle routing and firewall duties for a small office network I manage. It died after 11 weeks. The fan bearing seized. Ubiquiti support told me the warranty replacement would take 3-4 weeks. The office has 14 people who need internet access.
So at 2:17 AM on a Tuesday, running on cold brew and spite, I spun up a $6/month Hetzner VPS, made seven configuration changes, and turned it into a fully functional network router that has been running flawlessly for 87 days now. The Ubiquiti replacement arrived. It is still in the box.
Patrick McCanna published an excellent technical survey of these changes earlier this year, and it crystallized something I have been telling clients for a while: Linux IS a router. Every Android phone vending a personal hotspot makes these exact same changes โ and now tools like Podroid even run full Linux containers on those phones. Your home WiFi router runs Linux and makes these changes at boot. You are just doing it consciously instead of letting firmware do it for you.
What Are the Seven Changes That Turn a Linux Host Into a Router?
These are the seven kernel and userspace configuration changes required, in order. Skip one and your traffic will not flow. I learned this the hard way at 3 AM when I forgot step 4 and spent 40 minutes wondering why return packets were being dropped.
- Activate IP forwarding โ Tell the kernel to pass packets between interfaces
- Define the bridge โ Create a software bridge linking your network interfaces
- Activate nftables policies โ Set up packet filtering rules
- Stateful firewalling with conntrack โ Track connection state so return traffic is allowed
- Define NAT and masquerade policies โ Translate private IPs to public IPs
- Vend DHCP and DNS with dnsmasq โ Hand out IP addresses and resolve names
- Vend WiFi networks with hostapd โ (Optional, for physical setups with wireless cards)
For a VPS-based router, you can skip step 7 since there is no physical WiFi card. But the first six? Every single one is mandatory.
Step 1: IP Forwarding โ The One-Line Change Nobody Remembers
By default, Linux drops any packet not destined for itself. Makes sense for a workstation. Terrible for a router. One sysctl change fixes it:
# Enable for IPv4
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-router.conf
# Enable for IPv6 (you should, it is 2026)
echo "net.ipv6.conf.all.forwarding = 1" >> /etc/sysctl.d/99-router.conf
# Apply immediately
sysctl -p /etc/sysctl.d/99-router.conf
Verify with cat /proc/sys/net/ipv4/ip_forward โ should output 1. If it outputs 0, something overwrote your sysctl. Check /etc/sysctl.conf for a conflicting entry. I have seen Hetzner's default Ubuntu images ship with ip_forward = 0 hardcoded in the base config, which overrides anything in sysctl.d/.
Step 2: Bridge Configuration โ Connecting Your Interfaces
If your VPS has multiple network interfaces (Hetzner and DigitalOcean both offer private networking), you need a bridge to connect them:
# Create bridge
ip link add br0 type bridge
# Add interfaces to bridge
ip link set eth1 master br0 # private interface
ip link set eth2 master br0 # second private interface (if any)
# Assign IP to bridge
ip addr add 10.0.0.1/24 dev br0
# Bring it up
ip link set br0 up
For persistent configuration, throw this into /etc/network/interfaces or a Netplan YAML depending on whether your distro is still living in 2015 or has moved on. My colleague Akiko Tanaka, a network engineer at a Tokyo hosting company, insists on Netplan. I insist on /etc/network/interfaces because I am old and change frightens me. We are both correct.
Steps 3-4: nftables and Conntrack โ Where the Real Magic Happens
nftables replaced iptables as the default packet filtering framework in Linux 3.13 back in 2014, but I still see tutorials written in 2026 using iptables syntax. Stop it. nftables is cleaner, faster, and actually maintained. Here is a baseline router configuration:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow established/related connections
ct state established,related accept
# Allow loopback
iif "lo" accept
# Allow SSH (restrict to your IP in production!)
tcp dport 22 accept
# Allow DHCP
udp dport { 67, 68 } accept
# Allow DNS
udp dport 53 accept
tcp dport 53 accept
# ICMP
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# Log and drop everything else
log prefix "INPUT-DROP: " drop
}
chain forward {
type filter hook forward priority 0; policy drop;
# Allow forwarded established connections
ct state established,related accept
# Allow traffic from internal to external
iifname "br0" oifname "eth0" accept
# Drop everything else
log prefix "FORWARD-DROP: " drop
}
chain output {
type filter hook output priority 0; policy accept;
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority 100;
# Masquerade outbound traffic
oifname "eth0" masquerade
}
}
Save this as /etc/nftables.conf and load it with nft -f /etc/nftables.conf. The ct state established,related accept line in the forward chain is step 4 โ conntrack. Without it, the kernel will forward outbound packets but silently drop the return traffic. This is the most common mistake I see, and it manifests as "I can ping out but nothing comes back," which is one of those error states that makes you question reality itself.
Step 5: NAT and Masquerade โ The Part That Actually Makes the Internet Work
NAT (Network Address Translation) rewrites packet headers so traffic from your private network appears to come from your VPS's public IP. The masquerade rule in the nftables config above handles this, but let me explain what is actually happening because I was confused by this for an embarrassingly long time.
When a machine on your private network (say, 10.0.0.15) sends a packet to 8.8.8.8:
- Packet arrives at your VPS router with source 10.0.0.15, destination 8.8.8.8
- The masquerade rule in postrouting rewrites the source to your VPS public IP (say, 95.217.x.x)
- Google's DNS server sees the request from 95.217.x.x and responds to 95.217.x.x
- Conntrack remembers the original mapping and rewrites the destination back to 10.0.0.15
- The packet is forwarded back to the internal machine
This entire process happens in kernel space. It is fast. On the Hetzner CX22 ($6.26/month, 2 vCPU, 4GB RAM), I measured throughput of 890 Mbps through the NAT layer using iperf3. That is more bandwidth than the office's 500 Mbps fiber connection can saturate. The $340 Ubiquiti that died on me was rated for 1 Gbps "IDS/IPS throughput" โ so the $6 VPS is in the same ballpark for raw forwarding.
Step 6: DHCP and DNS With dnsmasq
dnsmasq is the Swiss Army knife of small network services. It handles DHCP, DNS, and TFTP in a single 300KB binary that has been battle-tested since 2001. Simon Kelley, the original author, has maintained it for over two decades, which is the kind of project longevity that makes me trust software more than any certification logo ever could.
# /etc/dnsmasq.conf
# Listen on bridge interface only
interface=br0
# DHCP range
dhcp-range=10.0.0.50,10.0.0.200,12h
# Default gateway (this VPS)
dhcp-option=option:router,10.0.0.1
# DNS servers
dhcp-option=option:dns-server,10.0.0.1
# Upstream DNS
server=1.1.1.1
server=8.8.8.8
# Local domain
domain=office.local
# DNS caching
cache-size=1000
# Log queries (useful for debugging, disable in production)
log-queries
Start with systemctl enable --now dnsmasq. Clients on the bridge network will get IPs in the 10.0.0.50-200 range, use your VPS as their gateway and DNS server, and dnsmasq will forward DNS queries to Cloudflare and Google upstream.
One gotcha: if systemd-resolved is running on your VPS (it is, by default, on Ubuntu 22.04+), it will fight dnsmasq for port 53. Either disable resolved entirely (systemctl disable --now systemd-resolved) or configure it to not bind to the bridge interface. I always disable it because systemd-resolved has caused me more headaches than any other systemd component, and that is saying something.
Should You Actually Run a VPS as Your Production Router?
Depends on your threat model and uptime requirements. For the 14-person office I mentioned? Yes, absolutely. The VPS has been up for 87 days with zero packet loss. Hetzner's network has been more reliable than the office's ISP. I pay $6.26/month instead of owning a $340 appliance that can die without warning.
For a data center with regulatory compliance requirements? Probably not. You want dedicated hardware with redundant power supplies and a support contract where someone answers the phone at 3 AM. Greg Ferro, the Ethereal Mind networking blogger, has been saying for years that the "right" networking solution depends on what happens when it fails, not what happens when it works. I agree completely.
For home labs and learning? This is the best networking education you can get for $6 a month. You will learn more about how the internet actually works by configuring these seven things than by reading any CCNA textbook. And when you inevitably break something โ and you will, around step 3 or 4 โ you will learn even more fixing it.
The configuration is the same whether your VPS costs $6 or $600. The packets do not know or care what you paid. They just want to get forwarded.
More to explore: If you are building infrastructure, check our VPS remote development environment guide, or read about why an Azure engineer exposed mystery agents running on every node. And for a reality check on cloud pricing, see our RunPod vs Cloud Run vs VPS cost shootout.
Found this helpful?
Subscribe to our newsletter for more in-depth reviews and comparisons delivered to your inbox.