Choosing a server for tunneling has less to do with core count and RAM than with the quality of the network route: a small server on a clean route will outperform a powerful one on a congested route. In this guide we cover the selection criteria, how the tunnel protocols differ, how to set up a GRE tunnel between two servers step by step, and the critical role of MSS clamping.
What is a server tunnel, and what problem does it solve?
A server tunnel means your network packets are wrapped inside other packets and travel over a defined path between two servers of your own. The result is that two separate servers in two datacenters behave as if they were plugged into the same local switch, and they reach each other on private addresses.
The first benefit is network unification: a shared private address space that makes firewalling and access control simpler. The second is route control, and it comes with a condition: if the default route is unstable, sending traffic through a better midpoint can reduce the fluctuation — something you measure, not something you assume.
Four real uses for a tunnel between an Iran server and a foreign server
- A reverse proxy for a site whose origin is abroad. The user connects to the Iran server, and the TCP handshake with them completes over the short path. With raw pass-through (a layer-3 tunnel or a TCP proxy), however, the TLS negotiation still runs end to end with the origin and travels the whole long path; to shorten it, you have to put the certificate on the Iran server.
- Deployments and updates. A server that constantly connects to a repository, a container registry or an external API gets fewer errors on a more stable route.
- Backup transfers. Moving backups with
rsyncover the private network is both safer and less likely to stop halfway. - Joining several servers together. Database, application and monitoring on
10.xaddresses, with ports that are never opened to the internet.
Choosing a server for tunneling: why CPU and RAM come last
A good server for tunneling is one that gives you a clean network route, enough traffic and a kernel you control. Packet encapsulation happens in the kernel, and for a few hundred megabits per second two cores and 1 GB of RAM are enough; spend the budget on the network, not the processor.
Measure peering and route quality in both directions
On the internet, the outbound path and the return path are not necessarily the same; one direction may be clean while the reverse one crosses several congested carriers. So before you choose a tunnel server, measure the route from both ends:
mtr -rwzc 100 <peer-ip>
We explain how to read the Loss% column correctly, and how to tell the false packet loss of intermediate hops from real loss, in our guide to testing Iran server quality. What is specific to tunnels is comparing the two outputs with each other rather than reading just one: if the path from A to B is clean but the path from B to A breaks at the third hop, the tunnel comes up and seems to work, yet throughput collapses in one direction only — and it is exactly this asymmetry that leaves troubleshooting blind.
Jitter and packet loss matter more than average ping
Average ping is a misleading number. A connection with a 90 ms ping and negligible jitter is better for a tunnel than one with a 70 ms ping and 40 ms of jitter, because packet loss shrinks TCP's congestion window, while jitter raises the RTT variance and the retransmission timeout and sometimes triggers needless retransmissions. The result is a collapse in throughput:
ping -c 100 -i 0.2 <peer-ip> | tail -2
# rtt min/avg/max/mdev = 71.2/74.8/191.4/12.6 ms
The mdev figure is what users experience as "it gets slow sometimes"; one test at peak hours tells you more than ten tests at midnight. The basics behind these numbers are in our guide to reducing ping.
Traffic quota and rate: a tunnel moves every byte twice
A relay server moves every byte twice — once in and once out — plus the header overhead. How those two passes are billed depends on the provider's counting policy; how to measure them is covered in our VPS bandwidth guide. Because a relay server is usually small, and often temporary while you are still testing routes, hourly billing is a natural fit for it; we work out where it stops paying off in our guide to hourly server costs and the break-even point.
IP quality and reputation
The relay server's IP is the IP that outside services see. Test it before a long-term commitment: send a few requests with curl to your own real destinations; a 403 or 429 status code means the IP's reputation has a problem.
curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/health
Loading kernel modules: why you need KVM
Layer-3 tunnels rely on kernel modules: ip_gre for GRE, ipip for IPIP and sit for IPv6 over IPv4. Under container virtualization, every server shares the host's kernel, so only the host operator can load modules and modprobe is not allowed for you; under KVM, your server runs its own kernel (KVM and NVMe explained).
sudo modprobe ip_gre && lsmod | grep -E '^ip_gre|^ip_tunnel'
ip tunnel show
# if you get "Operation not permitted", the kernel is not yours to control
Path MTU and header overhead
Every layer of encapsulation adds a few bytes to each packet, and those few bytes are the biggest source of mysterious tunnel problems: on a path with an MTU of 1500, a GRE tunnel leaves only 1476 bytes of usable space.
Tunnel protocols compared: GRE vs IPIP vs VXLAN vs an encrypted tunnel
For two servers with public IPs and non-sensitive data, GRE is the right default: 24 bytes of overhead, support for both IPv4 and IPv6, and a stable in-kernel implementation. If you only have IPv4, IPIP is lighter; if you need a layer-2 network, VXLAN; and if one end is behind NAT or the data is sensitive, an encrypted tunnel over UDP is the only workable option.
| Protocol | Per-packet overhead | What it carries | Encryption | NAT traversal | Best for |
|---|---|---|---|---|---|
| GRE (protocol 47) | 24 bytes | IPv4, IPv6, multicast | None | No | Between two public IPs |
| IPIP (protocol 4) | 20 bytes | Unicast IPv4 only | None | No | The lightest option |
| SIT / 6in4 (protocol 41) | 20 bytes | IPv6 over an IPv4 network | None | No | IPv6 on an IPv4-only server |
| VXLAN (UDP 4789) | About 50 bytes | Layer-2 frames | None | Yes | A shared layer-2 network |
| Encrypted tunnel (WireGuard) | About 60 bytes | IPv4 and IPv6 | Yes | Yes | Sensitive data or NAT |
Step by step: setting up a GRE tunnel between two servers
In this example, server A in Iran has 198.51.100.10 and server B abroad has 203.0.113.20 (both from the address ranges reserved for documentation). Inside the tunnel we use a /30 subnet, which holds exactly two usable addresses. Before you start, connect to both servers over SSH as described in our guide to secure SSH connections.
Step 1: create the tunnel on both ends
On the Iran server:
sudo modprobe ip_gre
sudo ip tunnel add gre1 mode gre local 198.51.100.10 remote 203.0.113.20 ttl 255
sudo ip addr add 10.10.10.1/30 dev gre1
sudo ip link set gre1 mtu 1476 up
On the foreign server, the same commands with local and remote swapped:
sudo modprobe ip_gre
sudo ip tunnel add gre1 mode gre local 203.0.113.20 remote 198.51.100.10 ttl 255
sudo ip addr add 10.10.10.2/30 dev gre1
sudo ip link set gre1 mtu 1476 up
ping -c 4 10.10.10.2
ip -br addr show gre1
# gre1 UNKNOWN 10.10.10.1/30
The UNKNOWN state is normal for a tunnel interface; there is no physical layer to report a link.
Step 2: allow GRE (protocol 47) in the firewall
If ping gets no reply, the first suspect is the firewall. GRE is neither TCP nor UDP; it has no ports, so the firewall has to allow IP protocol number 47:
# iptables
sudo iptables -A INPUT -p 47 -s 203.0.113.20 -j ACCEPT
# nftables — if you don't have a table and chain yet, run these two lines as well
sudo nft add table inet filter
sudo nft 'add chain inet filter input { type filter hook input priority 0 ; policy accept ; }'
sudo nft add rule inet filter input ip protocol gre ip saddr 203.0.113.20 accept
A note for ufw users: the ufw allow syntax only understands TCP and UDP, so the GRE rule has to be written by hand in /etc/ufw/before.rules. If your firewall is firewalld, use firewall-cmd --permanent --add-protocol=gre instead of a manual rule so that it isn't wiped. To see whether any packets are arriving at all:
sudo tcpdump -ni eth0 proto gre
If you see incoming packets but no replies go out, the problem is on your side; if you see no packets at all, either the far end isn't sending or a firewall along the path is dropping them (server security checklist).
Step 3: make the tunnel persistent
Anything you create with ip is gone after the first reboot. On systemd, the cleanest way is two small files:
# /etc/systemd/network/10-gre1.netdev
[NetDev]
Name=gre1
Kind=gre
MTUBytes=1476
[Tunnel]
Local=198.51.100.10
Remote=203.0.113.20
TTL=255
Independent=true
# /etc/systemd/network/10-gre1.network
[Match]
Name=gre1
[Network]
Address=10.10.10.1/30
sudo systemctl enable --now systemd-networkd
networkctl status gre1
The Independent=true key matters: without it, the tunnel is only created when a physical interface asks for it with Tunnel=. If your network is managed by netplan, define the tunnel in /etc/netplan/60-gre.yaml instead:
network:
version: 2
tunnels:
gre1:
mode: gre
local: 198.51.100.10
remote: 203.0.113.20
mtu: 1476
addresses: [10.10.10.1/30]
sudo netplan try # rolls back automatically if you lose the connection
sudo netplan apply
systemd-networkd can cut your connection; do it with the VNC console open.The equivalents on AlmaLinux and Rocky Linux
The examples above follow Ubuntu and Debian defaults. On the RHEL family, NetworkManager manages the network, and the tunnel is created and made persistent with nmcli:
sudo nmcli connection add type ip-tunnel con-name gre1 ifname gre1 \
mode gre local 198.51.100.10 remote 203.0.113.20 \
ip-tunnel.ttl 255 ip-tunnel.mtu 1476 \
ipv4.method manual ipv4.addresses 10.10.10.1/30
sudo nmcli connection up gre1
# firewall: firewalld instead of ufw
sudo firewall-cmd --permanent --add-protocol=gre
sudo firewall-cmd --reload
# make the nftables rules survive a reboot
sudo nft list ruleset | sudo tee /etc/sysconfig/nftables.conf
sudo systemctl enable --now nftables
net.ipv4.conf.all.rp_filter defaults to 1 (strict), and with the asymmetric routing a tunnel creates, packets are silently dropped; on Ubuntu it is usually 2. Check the current value first.Step 4: routing, forwarding and NAT
The two servers can now see each other, but they don't carry anyone else's traffic yet:
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tunnel.conf
sudo sysctl --system
Next you decide which traffic enters the tunnel; until you create a route pointing at gre1, not a single packet goes through the tunnel and the NAT and MSS rules never come into play. The lowest-risk option is a dedicated route for the destinations that matter:
# on the Iran server — send only this destination through the tunnel
sudo ip route add 203.0.113.10/32 via 10.10.10.2 dev gre1
ip route get 203.0.113.10
ip route replace default via 10.10.10.2 unless you have first added a host route to the far end's public IP via the original gateway; otherwise the tunnel's own packets go into the tunnel too, the route loops and SSH drops. Only do it with the VNC console open.If traffic coming through the tunnel has to reach the internet from the far end, you need a masquerade rule:
# iptables
sudo iptables -t nat -A POSTROUTING -s 10.10.10.0/30 -o eth0 -j MASQUERADE
# nftables
sudo nft add table ip nat
sudo nft 'add chain ip nat postrouting { type nat hook postrouting priority 100 ; }'
sudo nft add rule ip nat postrouting ip saddr 10.10.10.0/30 oifname "eth0" masquerade
Replace eth0 with the server's real public interface (from ip -br link). iptables rules are wiped at reboot unless you save them: with iptables-persistent on Debian and iptables-services on AlmaLinux.
MTU and MSS clamping: why ping works but HTTPS hangs
This is the most common and most confusing tunnel problem: ping answers perfectly, SSH connects but freezes a few seconds later, and HTTPS pages load only halfway. The cause is that small packets get through while large ones — the packets carrying real data — are dropped.
Here is the mechanism. At the start of a TCP connection, both sides announce their MSS, usually 1460, based on an MTU of 1500. Inside a GRE tunnel such a packet gets 24 extra bytes of header and exceeds the path's limit. When everything is healthy, the encapsulating server itself sends the sender an ICMP "fragmentation needed" message (type 3, code 4) announcing MTU=1476, so the sender shrinks its packets — that is Path MTU Discovery. When this message never arrives, the culprit is usually the sender's own firewall or an internal network that blocks all ICMP, not some distant carrier; the result is a PMTU black hole.
There are two cures, and it's best to apply both. First, don't block ICMP entirely:
sudo iptables -A INPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT
Second, and more important, clamp the MSS to the path's real MTU as SYN packets pass through:
# iptables — both directions through the tunnel
sudo iptables -t mangle -A FORWARD -o gre1 -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
sudo iptables -t mangle -A FORWARD -i gre1 -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
# nftables — the same two rules. Running the add lines again is harmless.
sudo nft add table inet filter
sudo nft 'add chain inet filter forward { type filter hook forward priority 0 ; policy accept ; }'
sudo nft add rule inet filter forward oifname "gre1" tcp flags syn tcp option maxseg size set rt mtu
sudo nft add rule inet filter forward iifname "gre1" tcp flags syn tcp option maxseg size set rt mtu
Why two rules, and why on both ends? Because the MSS announced in each SYN sets the size limit for packets traveling in the opposite direction; one side clamps the SYN and the other clamps the SYN-ACK. With only one rule, one direction keeps sending large packets and you are left with the same "SSH connects but freezes". The kernel works out the final figure: 1476 minus 40, which is 1436.
To find the real limit, send a packet that is not allowed to be fragmented:
# measure the underlying path's limit directly against the far end's public IP (1472+28=1500)
ping -M do -s 1472 -c 3 203.0.113.20
# then confirm that packets get through the tunnel itself
ping -M do -s 1448 -c 3 10.10.10.2 # should get replies
ping -M do -s 1449 -c 3 10.10.10.2
# ping: local error: Message too long, mtu=1476
The second message is a local error, not a router's reply: because the destination is routed via gre1, the kernel stops the packet before it is sent. If the first test fails, the underlying path's MTU is below 1500; in that case lower the tunnel MTU step by step and record the value that works in the .netdev file. A more robust form of path MTU discovery is described in RFC 4821.
When only one service matters: TCP forwarding instead of a layer-3 tunnel
If your only goal is to deliver one specific service to users in Iran, a layer-3 tunnel is more than you need. A TCP proxy does the same job with fewer points of failure: it needs no kernel module, MTU never becomes a headache, and the firewall doesn't have to let an unusual protocol through. HAProxy in TCP mode passes the connection through without opening the TLS, so no certificate is needed on the relay server — but the TLS handshake isn't shortened either:
# /etc/haproxy/haproxy.cfg
defaults
mode tcp
timeout connect 5s
timeout client 1m
timeout server 1m
frontend fe_https
bind :443
default_backend be_origin
backend be_origin
server origin 203.0.113.10:443 check inter 5s
sudo haproxy -c -f /etc/haproxy/haproxy.cfg # validate the config
sudo systemctl reload haproxy
For a quick test, socat is enough:
socat TCP4-LISTEN:8443,reuseaddr,fork TCP4:203.0.113.10:443
Measure the result: did the tunnel actually help?
The only way to know whether a tunnel helped is to measure before and after with three metrics: transfer rate with iperf3 in both directions, route stability with mtr against the address inside the tunnel, and time to first byte with curl -w. If none of the three improved, drop the tunnel.
# on the far end: open port 5201 to your own IP only
sudo iptables -A INPUT -p tcp --dport 5201 -s 198.51.100.10 -j ACCEPT
iperf3 -s
# when the test is over, delete the rule with -D and stop the server
iperf3 -c 10.10.10.2 -t 30 # through the tunnel
iperf3 -c 203.0.113.20 -t 30 # direct, without the tunnel
iperf3 -c 10.10.10.2 -t 30 -R # reverse direction (download)
Never skip the -R switch; on many routes, upload and download behave differently. A TTFB comparison likewise only means something when you test the same domain and change nothing but the route, which is exactly what --resolve does:
# straight to the origin
curl -o /dev/null -s -w "ttfb: %{time_starttransfer}s\n" \
--resolve example.com:443:203.0.113.10 https://example.com/
# via the relay server
curl -o /dev/null -s -w "ttfb: %{time_starttransfer}s\n" \
--resolve example.com:443:198.51.100.10 https://example.com/
Take each measurement at least ten times and at different hours of the day. A realistic expectation: if the default route is healthy, a tunnel usually makes TTFB slightly worse; the real gain comes where the direct route suffers heavy packet loss and jitter.
Troubleshooting a GRE tunnel: symptom, cause, fix
When a tunnel won't come up or only half works, the cause is almost always one of these: protocol 47 blocked in the firewall, a large MTU combined with blocked ICMP, no route pointing at the tunnel interface, reverse path filtering, forwarding switched off or a missing NAT rule, no kernel of your own, or a tunnel that vanishes after a reboot.
| Symptom | Likely cause | Fix |
|---|---|---|
| The interface is up, but ping inside the tunnel gets no reply | Protocol 47 is blocked in the firewall | Watch incoming packets with tcpdump -ni eth0 proto gre and add a -p 47 rule |
| Ping works, but SSH and HTTPS hang | A large MTU, with ICMP type 3 code 4 blocked | Enable MSS clamping in both directions and allow ICMP |
Traffic never enters the tunnel and the gre1 counters stay at zero | No route points at the tunnel interface | Check with ip route get <destination> that the output says dev gre1 |
| Packets arrive, but the kernel silently drops them | Reverse path filtering with asymmetric routing | net.ipv4.conf.all.rp_filter=2 |
| Traffic enters the tunnel but never reaches the internet | ip_forward is off, or the masquerade rule is missing | Turn forwarding on and add the NAT rule |
Operation not permitted when creating the tunnel | Container virtualization; the kernel is not yours to control | Get a server with full KVM virtualization |
| The tunnel disappears after a reboot | ip commands only last until the next reboot | Make it persistent with systemd-networkd, netplan or nmcli |
| Throughput inside the tunnel is far below the direct route | An MTU that is too small, packet loss, or a worse route | Tune the MTU and measure again with iperf3 in both directions |
Two points about rp_filter: the kernel takes the higher of the all value and the interface's own value as the effective setting, and a temporary setting is lost at reboot. Set the default key too, so interfaces created after boot inherit the right value:
# temporary, for testing
sudo sysctl -w net.ipv4.conf.all.rp_filter=2
sudo sysctl -w net.ipv4.conf.gre1.rp_filter=2
# permanent — the same file you put ip_forward in
# /etc/sysctl.d/99-tunnel.conf
net.ipv4.ip_forward=1
net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.default.rp_filter=2
sudo sysctl --system
And if the tunnel is up but the web service behind it returns a gateway error, see our guide to fixing 502 Bad Gateway; sometimes the origin is to blame.
Checklist: choosing and setting up a server for tunneling
Work through these eight steps in order and you will get your tunnel server running without blind troubleshooting:
- Write down the goal precisely. One service or a whole network? For the first, a TCP proxy is enough.
- Measure the route before you buy —
mtrandpingin both directions, at peak hours. - Check the virtualization. It has to be KVM for
modprobe ip_greto work. - Budget traffic at twice the volume, and ask about the provider's counting policy.
- Pick the protocol from the table. Two public IPs? GRE. One end behind NAT? A UDP tunnel.
- Set up routing, MTU and MSS on day one, not after users start complaining.
- Make the tunnel persistent and monitorable — with
systemd-networkdornmcli. - Document before and after. If the numbers didn't improve, drop it.
Frequently asked questions
What specs does a good server for tunneling need?
A clean network route, enough traffic allowance and KVM virtualization matter far more than a powerful processor. GRE and IPIP encapsulation happens in the kernel, so for typical rates one or two cores and 1 GB of RAM are plenty. The only cases where the CPU starts to matter are an encrypted tunnel running at very high rates, or terminating TLS on the relay server.
Why can't I create a GRE tunnel on my server?
Almost always because the server runs on container virtualization (OpenVZ or LXC) and has no kernel of its own; in that case modprobe ip_gre fails with "Operation not permitted". The fix is a server with full KVM virtualization, where your server runs its own kernel and can load modules. If you already have KVM and still get an error, the module was probably not built for your kernel, which you can check with modinfo ip_gre.
Why does ping work after setting up the tunnel, but websites don't load?
Because small packets get through and large ones don't. The GRE header adds 24 bytes to every packet, so full-size packets exceed the path MTU; if ICMP type 3 code 4 ("fragmentation needed") is also blocked along the way, the sender never finds out and the connection hangs silently. The cure is MSS clamping on SYN packets in both directions, plus leaving that ICMP message allowed.
How is tunnel traffic counted?
A relay server moves every byte twice: once inbound and once outbound. How those two passes are billed depends on the provider's counting policy; if both directions count, 100 GB of user traffic is recorded as roughly 200 GB, and if only outbound traffic counts, as roughly 100 GB. Measure your real usage with vnstat, separately on the public interface and on the tunnel interface.
Next step
In short: measure first, then build. Check the route with mtr in both directions, bring up the GRE tunnel, set up routing and MSS clamping, and prove the result with iperf3. And the best way to find the right server for tunneling is to test several routes before any long-term commitment: create an hourly cloud server at Mehran Host — KVM with NVMe in an Iranian datacenter, with automatic provisioning that usually takes under 60 seconds — test it for a few hours, and if it doesn't work out, delete it and the billing stops too. Cost estimates are in the calculator on our homepage, and for multi-server projects, see our enterprise solutions.