🥇 GRE tunnels that survive networks which drop native GRE — wrapped in UDP, tuned for loss, managed by systemd.
Build a point-to-point or hub-and-spoke overlay between Linux servers, even across providers/transit that filter IP protocol 47.
Plain GRE rides IP protocol 47. Many budget hosts, mobile carriers, and national transit paths silently drop proto 47 — the tunnel comes "up" on both ends but zero payload crosses. You only notice when every ping times out.
Golden GRE wraps GRE inside UDP using the Linux FOU (Foo-over-UDP) encapsulation. To the network it looks like ordinary UDP, so it passes wherever UDP passes — while you keep a real greN L3 interface to route over.
On top of transport it ships the production glue a raw ip tunnel command leaves out:
- 🥇 Punches through proto-47 filtering — GRE-in-UDP via FOU.
- ⚡ Loss-tolerant by default — BBR congestion control +
fq, so a lossy long-haul path doesn't collapse TCP (CUBIC halves its window on every loss; BBR doesn't). - 🧱 Forwarding-ready —
ip_forward, FORWARD accept, and TCP MSS clamping so forwarded TCP never blackholes on PMTUD. - 🔁 Reboot-persistent — one systemd template unit (
golden-gre@<name>) brings every tunnel back on boot. - 🌐 Hub & spoke — run many tunnels on one box; each is an isolated instance (own device, UDP port, /30, config).
- 🧩 Config-driven — one small file per tunnel in
/etc/golden-gre/. No IPs baked into scripts.
- How it works
- Requirements
- Quick start
- What gets installed
- Configuration reference
- Managing tunnels
- Running multiple tunnels (hub & spoke)
- Routing & NAT through the tunnel
- Performance & tuning
- Verifying with iperf3
- Troubleshooting
- WireGuard fallback
- Uninstall
- Development
- Security notes
- How it works under the hood
- License
flowchart LR
subgraph HUB["🥇 Hub (one public IP)"]
G1["gre1 · FOU :5555<br/>10.99.99.1/30"]
G2["gre2 · FOU :5556<br/>10.99.99.5/30"]
end
P1["Spoke A<br/>10.99.99.2/30"]
P2["Spoke B<br/>10.99.99.6/30"]
G1 == "GRE-in-UDP · :5555" ==> P1
G2 == "GRE-in-UDP · :5556" ==> P2
classDef gold fill:#FFD700,stroke:#B8860B,stroke-width:2px,color:#161616;
classDef spoke fill:#2b2b2b,stroke:#DAA520,stroke-width:2px,color:#FFD700;
class G1,G2 gold;
class P1,P2 spoke;
Each end runs the same recipe:
- A FOU listener decapsulates incoming UDP on a chosen port back into GRE.
- A
greNdevice sends its GRE frames inside UDP to the peer's FOU port. - The kernel routes your traffic over
greNlike any other L3 interface.
Because the wire payload is UDP, proto-47 filtering never sees a GRE packet to drop.
- Linux with
fouandip_grekernel modules (stock on Ubuntu 22.04/24.04, Debian, most distros). iproute2,iptables,systemd.ethtool— used to switch GRO off on the underlay NIC at bringup. Install it. The call is deliberately non-fatal, so on a host withoutethtoolthe step is skipped silently and you can land straight in the ~1 Mbit/s TCP collapse described in docs/GRO.md.- Root on both ends.
- UDP reachability between the two public IPs on your chosen port(s). (That's the whole point — UDP gets through where GRE doesn't.)
- A free /30 per tunnel for the overlay, and a unique UDP port + device name per tunnel on any shared host.
Check modules:
modprobe fou && modprobe ip_gre && echo "ready"A two-server, point-to-point tunnel.
# --- on BOTH servers ---
git clone https://github.com/LivingG0D/Golden-GRE.git
cd Golden-GRE
sudo ./install.sh# --- on SERVER 1 (e.g. 203.0.113.10) ---
sudo tee /etc/golden-gre/link.conf >/dev/null <<'EOF'
DEV=gre1
LOCAL_PUB=203.0.113.10
REMOTE_PUB=198.51.100.20
TUN_ADDR=10.99.99.1/30
FOU_PORT=5555
MTU=1400
EOF
sudo systemctl enable --now golden-gre@link# --- on SERVER 2 (e.g. 198.51.100.20) ---
sudo tee /etc/golden-gre/link.conf >/dev/null <<'EOF'
DEV=gre1
LOCAL_PUB=198.51.100.20
REMOTE_PUB=203.0.113.10
TUN_ADDR=10.99.99.2/30
FOU_PORT=5555
MTU=1400
EOF
sudo systemctl enable --now golden-gre@linkTest it:
golden-gre-preflight link # sanity-check modules, sysctl & config
ping 10.99.99.2 # from server 1That's a reboot-persistent, loss-tolerant, forwarding-ready tunnel. 🥇
install.sh must run as root and is idempotent — re-run it any time to upgrade in place. It touches exactly these paths and nothing else:
| Path | Mode | What it is |
|---|---|---|
/usr/local/sbin/golden-gre-up.sh |
0755 |
Brings one tunnel up |
/usr/local/sbin/golden-gre-down.sh |
0755 |
Tears one tunnel down |
/usr/local/sbin/golden-gre-preflight |
0755 |
Readiness checker (installed from scripts/preflight.sh) |
/etc/systemd/system/golden-gre@.service |
0644 |
The systemd template unit |
/etc/sysctl.d/99-golden-gre.conf |
0644 |
BBR/fq, buffers, forwarding — applied immediately via sysctl --system |
/etc/golden-gre/ |
0750 |
Directory for your per-tunnel configs (created empty) |
No packages are installed, no existing network configuration is rewritten, and no tunnel starts until you create a config and enable an instance.
One file per tunnel: /etc/golden-gre/<name>.conf. <name> is the systemd instance (golden-gre@<name>).
| Key | Required | Example | Meaning |
|---|---|---|---|
DEV |
✅ | gre1 |
Tunnel device name. Unique per host. |
LOCAL_PUB |
✅ | 203.0.113.10 |
This server's public IP (GRE/FOU underlay source). |
REMOTE_PUB |
✅ | 198.51.100.20 |
Peer's public IP. |
TUN_ADDR |
✅ | 10.99.99.1/30 |
This end's overlay address. Peer takes the other host in the /30. |
FOU_PORT |
✅ | 5555 |
UDP port for GRE-in-UDP. Unique per tunnel on a shared host. Both ends use the same port. |
MTU |
⬜ | 1400 |
Tunnel MTU. Default 1400 (safe under a 1500 underlay: 20 IP + 8 UDP + 4 GRE overhead). |
ROUTES |
⬜ | "192.0.2.0/24 198.18.0.0/24" |
Space-separated CIDRs to route via this tunnel. |
NAT_SRC |
⬜ | 10.99.99.0/30 |
If set, MASQUERADE this source out NAT_OUT (use this node as an internet exit). |
NAT_OUT |
⬜ | eth0 |
Egress interface for NAT_SRC. Defaults to eth0. |
📝 IPs above use the RFC 5737 documentation ranges. Replace with your real values in
/etc/golden-gre/on each host — never commit them.
Every tunnel is an independent systemd instance named after its config file (/etc/golden-gre/link.conf → golden-gre@link):
systemctl enable --now golden-gre@link # start now + on every boot
systemctl restart golden-gre@link # full down/up — safe, both are idempotent
systemctl stop golden-gre@link # tear down, still enabled at boot
systemctl disable --now golden-gre@link # tear down + don't come back
systemctl status golden-gre@link
journalctl -u golden-gre@link -n 50The unit is Type=oneshot with RemainAfterExit=yes: it runs the up script once and stays active (exited) for as long as the tunnel is meant to exist. It also carries ConditionPathExists=/etc/golden-gre/%i.conf, so an instance whose config is missing is skipped rather than failed — no red units after you delete a config.
The scripts stand alone, which is handy for testing a config before you enable it:
sudo golden-gre-up.sh link
sudo golden-gre-down.sh linkup deletes and recreates the device, so running it twice is fine. down ignores anything that's already gone and always exits 0, so it's safe in teardown scripts.
golden-gre-preflight # host readiness only
golden-gre-preflight link # ...plus validate /etc/golden-gre/link.confIt verifies fou/ip_gre are loadable and ip/iptables are present, reports tcp_congestion_control and ip_forward, and — given an instance name — confirms all five required keys are set. Hard failures exit 1; advisories (a non-BBR qdisc, ip_forward=0) only warn, so it drops cleanly into a provisioning pipeline without failing hosts that don't route.
Note what it can't do: it never tests the actual path. Reachability on your FOU_PORT is the one thing you must confirm yourself — it prints the tcpdump command to run on the peer.
One hub can terminate many tunnels at once. Give each its own device, UDP port, and /30:
# hub: tunnel to spoke A
sudo tee /etc/golden-gre/spoke-a.conf >/dev/null <<'EOF'
DEV=gre1
LOCAL_PUB=203.0.113.10
REMOTE_PUB=198.51.100.20
TUN_ADDR=10.99.99.1/30
FOU_PORT=5555
EOF
# hub: tunnel to spoke B
sudo tee /etc/golden-gre/spoke-b.conf >/dev/null <<'EOF'
DEV=gre2
LOCAL_PUB=203.0.113.10
REMOTE_PUB=192.0.2.30
TUN_ADDR=10.99.99.5/30
FOU_PORT=5556
EOF
sudo systemctl enable --now golden-gre@spoke-a golden-gre@spoke-bThe FOU listeners stack on the hub (:5555 and :5556); the kernel demuxes return traffic to the right device by peer IP. Manage them independently — restarting one never touches the other:
systemctl status golden-gre@spoke-a golden-gre@spoke-b
systemctl restart golden-gre@spoke-b # spoke-a stays upRoute a remote subnet over a tunnel — add to its conf:
ROUTES="10.50.0.0/24"Use a node as an internet exit (e.g. send the overlay's traffic out the USA box):
# in the exit node's conf
NAT_SRC="10.99.99.4/30"
NAT_OUT=eth0ROUTES and NAT_SRC are applied on up and cleaned on down. ip_forward and the MSS clamp are already in place, so forwarded TCP keeps a correct MSS and won't stall on a path-MTU black hole.
install.sh drops sysctl/99-golden-gre.conf:
| Setting | Value | Why |
|---|---|---|
tcp_congestion_control |
bbr |
The headline fix. On a lossy long-haul path, CUBIC reads every drop as congestion and collapses; BBR paces to the measured bottleneck and ignores non-congestive loss. |
default_qdisc |
fq |
BBR's pacing companion. |
tcp_rmem / tcp_wmem max |
128 MiB |
Big enough send/receive windows to fill a high-BDP (high latency × bandwidth) link. |
tcp_mtu_probing |
1 |
Recover gracefully if path MTU is below expectations. |
ip_forward, forwarding |
1 |
Route through the tunnel. |
💡 Real-world impact: on a path with ~0.5–0.7% loss, switching the sender from CUBIC to BBR took a tunnel from ~30 Mbit/s to ~1 Gbit/s. Loss is a property of the path; BBR just stops over-reacting to it.
# peer (server):
iperf3 -s -B 10.99.99.2
# this end (client) — TCP both directions:
iperf3 -c 10.99.99.2 # forward
iperf3 -c 10.99.99.2 -R # reverse
# UDP loss / jitter:
iperf3 -c 10.99.99.2 -u -b 300MHealthy signs: ping at the raw path RTT with ~0% loss, TCP that climbs and holds, UDP loss in the sub-percent range with low jitter. High TCP retransmits with sustained throughput are normal under BBR on a lossy path — that's BBR doing its job, not a fault.
| Symptom | Likely cause | Check / fix |
|---|---|---|
Device is UP but ping 100% loss |
Native GRE leaking, or wrong FOU port | Confirm ip fou show lists your port; tcpdump -ni eth0 udp port <FOU_PORT> should show traffic both ways. |
| UDP leaves one side, never arrives | Provider blocks that UDP port | Try another FOU_PORT (both ends). |
| TCP crawls, ping is fine | Sender not on BBR | sysctl net.ipv4.tcp_congestion_control → should be bbr. Re-run sudo sysctl --system. |
| Forwarded TCP connects then stalls | PMTU black hole | MSS clamp present? iptables -t mangle -S FORWARD (look for mss). Lower MTU. |
| UDP fast, TCP stuck ~1 Mbit/s | NIC GRO corrupting GRE-in-UDP | ethtool -K <underlay-iface> gro off on both ends (automatic in golden-gre-up.sh). Watch UdpInErrors on the receiver with nstat. Full writeup: docs/GRO.md. |
| Gone after reboot | Unit not enabled | systemctl is-enabled golden-gre@<name>. |
RTNETLINK: File exists |
Stale device/addr | systemctl restart golden-gre@<name> (down/up is idempotent). |
Logs: journalctl -u golden-gre@<name>.
In some networks, GRE-in-UDP is still detected and throttled hard even when the tunnel is up and ping works.
For those paths, use WireGuard as the transport instead of GRE-over-FOU: it's encrypted, UDP-based, has a simpler packet path, and usually measures better on DPI-heavy links. Start from docs/WireGuard.md (suggested MTU 1320, keepalive 25) and fill in examples/wireguard.conf.example.
It also covers two things worth reading before you need them. A filtered UDP port makes a WireGuard tunnel fail silently. The handshake ages out, transfer counters freeze, and PersistentKeepalive retries forever without ever logging an error — restarting the interface changes nothing. It walks through confirming it with tcpdump, locating an open port with a bidirectional socat probe (filtering is frequently one-directional, so each direction must be tested separately), and moving the tunnel with wg set — including the trap that wg set is runtime-only, so the change must be written back to the .conf or the next reboot returns you to the dead port.
And some transit polices UDP per flow, not per host — one stream measures far below four, and UDP loses 50–65% at any rate. No amount of host tuning moves that ceiling, because a single tunnel is a single UDP 5-tuple in a single bucket. The fix is several tunnels on different ports, bonded at the layer above; the doc covers the config, the measurement that identifies the policer, and the pitfalls (one keypair per tunnel, and ECMP will not split a single connection).
# 1. stop and disable every tunnel first (this tears down the devices)
systemctl disable --now 'golden-gre@*'
# 2. remove the installed files
sudo rm -f /usr/local/sbin/golden-gre-up.sh \
/usr/local/sbin/golden-gre-down.sh \
/usr/local/sbin/golden-gre-preflight \
/etc/systemd/system/golden-gre@.service \
/etc/sysctl.d/99-golden-gre.conf
sudo systemctl daemon-reload
sudo sysctl --system >/dev/null
# 3. your tunnel configs — only if you're really done
sudo rm -rf /etc/golden-grebbr/fq, the enlarged buffers, and ip_forward to your distro defaults. If anything else on the box (Docker, a VPN, another router role) depends on forwarding, set it explicitly in its own /etc/sysctl.d/ file before you remove ours.
Pure bash, no build step, no runtime dependencies beyond what's in Requirements. CI (.github/workflows/ci.yml) runs on every push to main and every PR:
| Job | What it enforces |
|---|---|
| ShellCheck | Every script lints clean. SC1091/SC2154 are disabled repo-wide (.shellcheckrc) because each tunnel's variables arrive from a sourced /etc config that ShellCheck can't follow. |
| Unit & config sanity | The systemd unit declares [Unit]/[Service]/[Install] plus ExecStart/ExecStop; every script starts with #!/usr/bin/env bash; and the point-to-point example still defines all five required keys. |
Reproduce the lint locally before pushing:
shellcheck scripts/*.sh install.sh.gitattributes pins LF endings on everything that executes on Linux, so contributing from Windows can't ship a CRLF shebang that fails with bad interpreter.
- Golden GRE is unencrypted — like GRE itself. The overlay protects nothing on the wire. If you need confidentiality, run it inside WireGuard/IPsec, or treat the tunnel purely as transport for already-encrypted traffic.
- Never commit real configs. Your per-host IPs live in
/etc/golden-gre/and are intentionally outside this repo..gitignoreguards against accidental secret/.envcommits. - Restrict the FOU UDP port to the peer IP with your firewall if you want to reduce exposure.
┌──────────── your packet ────────────┐
│ inner IP | TCP/UDP/ICMP | payload │ rides gre1 (L3)
└──────────────────────────────────────┘
│ GRE encap (+4 bytes)
▼
┌─────── GRE ───────┬──── inner packet ────┐
└────────────────────┴──────────────────────┘
│ FOU encap (+8 UDP, +20 outer IP)
▼
┌ outer IP (LOCAL_PUB→REMOTE_PUB) | UDP :FOU_PORT | GRE | inner ┐
└───────────────────────────────────────────────────────────────┘
│
▼ looks like plain UDP — proto-47 filters see nothing to drop
The receiving FOU socket strips the UDP, reinjects the GRE, and the matching greN device delivers your inner packet. Overhead is 24 bytes vs. native GRE's encap-less 4 (hence MTU=1400).
🥇 Golden GRE — because a tunnel that drops your packets isn't a tunnel.