Networking

Choosing a server for tunneling: set up a GRE tunnel between two servers

Choosing a server for tunneling and building a GRE tunnel between an Iran server and a foreign server over a private network

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 rsync over the private network is both safer and less likely to stop halfway.
  • Joining several servers together. Database, application and monitoring on 10.x addresses, 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.

Diagram of a GRE tunnel between an Iran server and a foreign server: the 24 bytes of extra header, the MTU, and MSS clamping on the tunnel server

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.

ProtocolPer-packet overheadWhat it carriesEncryptionNAT traversalBest for
GRE (protocol 47)24 bytesIPv4, IPv6, multicastNoneNoBetween two public IPs
IPIP (protocol 4)20 bytesUnicast IPv4 onlyNoneNoThe lightest option
SIT / 6in4 (protocol 41)20 bytesIPv6 over an IPv4 networkNoneNoIPv6 on an IPv4-only server
VXLAN (UDP 4789)About 50 bytesLayer-2 framesNoneYesA shared layer-2 network
Encrypted tunnel (WireGuard)About 60 bytesIPv4 and IPv6YesYesSensitive data or NAT
⚠️ GRE and IPIP have no encryption and no authentication. If sensitive data passes through the tunnel, either use an encrypted tunnel or encrypt at the application layer — for example, by transferring backups over SSH.

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
⚠️ If your network is managed by NetworkManager or ifupdown, suddenly enabling 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
⚠️ On AlmaLinux and Rocky, 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
⚠️ Don't change the default route with 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
💡 Tip: behind a TCP proxy, the origin sees every connection as coming from the relay server's IP, and the visitor's real IP is lost. The solution is the PROXY protocol; what it involves is covered in our guide to visitor analytics and the user's real IP.

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.

SymptomLikely causeFix
The interface is up, but ping inside the tunnel gets no replyProtocol 47 is blocked in the firewallWatch incoming packets with tcpdump -ni eth0 proto gre and add a -p 47 rule
Ping works, but SSH and HTTPS hangA large MTU, with ICMP type 3 code 4 blockedEnable MSS clamping in both directions and allow ICMP
Traffic never enters the tunnel and the gre1 counters stay at zeroNo route points at the tunnel interfaceCheck with ip route get <destination> that the output says dev gre1
Packets arrive, but the kernel silently drops themReverse path filtering with asymmetric routingnet.ipv4.conf.all.rp_filter=2
Traffic enters the tunnel but never reaches the internetip_forward is off, or the masquerade rule is missingTurn forwarding on and add the NAT rule
Operation not permitted when creating the tunnelContainer virtualization; the kernel is not yours to controlGet a server with full KVM virtualization
The tunnel disappears after a rebootip commands only last until the next rebootMake it persistent with systemd-networkd, netplan or nmcli
Throughput inside the tunnel is far below the direct routeAn MTU that is too small, packet loss, or a worse routeTune 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:

  1. Write down the goal precisely. One service or a whole network? For the first, a TCP proxy is enough.
  2. Measure the route before you buy — mtr and ping in both directions, at peak hours.
  3. Check the virtualization. It has to be KVM for modprobe ip_gre to work.
  4. Budget traffic at twice the volume, and ask about the provider's counting policy.
  5. Pick the protocol from the table. Two public IPs? GRE. One end behind NAT? A UDP tunnel.
  6. Set up routing, MTU and MSS on day one, not after users start complaining.
  7. Make the tunnel persistent and monitorable — with systemd-networkd or nmcli.
  8. 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.

Related tutorials

Ready for some hands-on practice?

A Mehran Host hourly cloud server is ready in under 60 seconds. Practice as much as you like and pay only for the hours you use. Estimate the cost with the pricing calculator before you sign up.