Skip to content

Repository files navigation

Golden GRE

Apache 2.0 License Bash systemd BBR Ubuntu

CI

🥇 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.


✨ Why Golden GRE?

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-readyip_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.

📜 Table of Contents


🛠 How it works

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;
Loading

Each end runs the same recipe:

  1. A FOU listener decapsulates incoming UDP on a chosen port back into GRE.
  2. A greN device sends its GRE frames inside UDP to the peer's FOU port.
  3. The kernel routes your traffic over greN like any other L3 interface.

Because the wire payload is UDP, proto-47 filtering never sees a GRE packet to drop.


📦 Requirements

  • Linux with fou and ip_gre kernel 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 without ethtool the 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"

🚀 Quick start

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@link

Test it:

golden-gre-preflight link   # sanity-check modules, sysctl & config
ping 10.99.99.2             # from server 1

That's a reboot-persistent, loss-tolerant, forwarding-ready tunnel. 🥇


📥 What gets installed

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.


⚙️ Configuration reference

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.


🎛 Managing tunnels

Every tunnel is an independent systemd instance named after its config file (/etc/golden-gre/link.confgolden-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 50

The 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.

Without systemd

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 link

up 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.

Preflight

golden-gre-preflight          # host readiness only
golden-gre-preflight link     # ...plus validate /etc/golden-gre/link.conf

It 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.


🌐 Running multiple tunnels (hub & spoke)

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-b

The 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 up

🔀 Routing & NAT through the tunnel

Route 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=eth0

ROUTES 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.


⚡ Performance & tuning

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.


🔬 Verifying with iperf3

# 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 300M

Healthy 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.


🩺 Troubleshooting

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>.


🔁 WireGuard fallback

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).


🧹 Uninstall

# 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-gre

⚠️ Step 2 reverts bbr/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.


🧪 Development

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.


🔐 Security notes

  • 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. .gitignore guards against accidental secret/.env commits.
  • Restrict the FOU UDP port to the peer IP with your firewall if you want to reduce exposure.

🧠 How it works under the hood

        ┌──────────── 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).


📄 License

Apache 2.0 © LivingG0D

🥇 Golden GRE — because a tunnel that drops your packets isn't a tunnel.

About

GRE-over-FOU tunnels that punch through proto-47 filtering — UDP-encapsulated, BBR-tuned, systemd-managed. Point-to-point or hub-and-spoke.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages