⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣄⡀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣏⣹⣿⠄⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⠿⠋⢠⣷⣦⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣿⣿⣿⠛⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⢸⣿⣿⡿⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⢀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋⣠⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀ ⠀⠀⠰⢾⣿⣿⣿⡟⠿⠿⣿⣿⠿⠿⠛⠋⣁⣴⣾⣿⣿⠿⠋⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠉⠛⠻⠷⣶⣤⣤⣤⣤⣶⣾⣿⡿⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢀⣶⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠛⠛⠛⠂⠀⠀⠀⠀
A SOCKS5 VPN that tunnels raw TCP through a Google Apps Script web app to your own small VPS exit server. To anything on the network path your client only ever talks TLS to a Google IP with SNI=www.google.com. Everything in flight is AES-256-GCM encrypted end-to-end — Google never sees plaintext and never holds the key.
⚠️ You need a small VPS for the exit server. Unlike pure-Apps-Script proxies, this project tunnels raw TCP — anything SOCKS5 can carry — so a realnet.Dialhas to happen somewhere. A small $4/month VPS is plenty. In exchange you can tunnel SSH, IMAP, custom protocols, anything — not just HTTP.
📖 Table of Contents
Browser/App
-> SOCKS5 (127.0.0.1:1080)
-> Zstd-compressed + AES-256-GCM frame batches
-> HTTPS to a Google edge IP (SNI=www.google.com, Host=script.google.com)
-> Apps Script doPost() (dumb forwarder, never sees plaintext)
-> Your VPS :8443/tunnel (decrypts, demuxes by session_id, dials target)
<- Same path in reverse via long-polling
Your application sends TCP bytes through the SOCKS5 listener on your computer. The client groups them into batches of frames, Zstandard-compresses each batch (for compressible traffic such as plain HTTP or JSON APIs this reduces the body size by up to 65%, keeping you further from Apps Script's daily quota limits), then seals the whole batch under a single AES-256-GCM envelope and POSTs it over a domain-fronted HTTPS connection to your Apps Script web app. The Apps Script is a ~30-line script that forwards the body verbatim to your VPS — it never decrypts and the AES key never touches Google. Your VPS decrypts, dials the real target, and pumps bytes back along the same path. The filter sees only TLS to Google.
Apps Script daily quota. Each Google account has a UrlFetch quota of about 20,000 executions per day on the free tier — shared across every deployment under that account, not per deployment. The quota resets at midnight Pacific (≈10:30 AM Iran time in summer, ≈11:30 AM in winter). The Zstd compression above is what stretches that quota the furthest.
You need a VPS with a public IP. Linux or Windows Server — any provider works.
You need two separate programs:
goose-client— runs on your own computer. This is what you run every day.goose-server— runs on your VPS. You set it up once and leave it running.
🚀 Linux VPS shortcut: If your exit server is Linux and you have root, the installer script below bundles Steps 2–7 for the server (download, config, tunnel key, systemd unit, firewall) into one command. You still need to set up the client and Apps Script (Steps 5 and 8+) on your own machine.
bash <(curl -Ls https://raw.githubusercontent.com/Kianmhz/GooseRelayVPN/main/scripts/goose-server.sh)The script verifies the release tarball against its published
SHA256SUMS.txtbefore installing, generates a freshtunnel_keyyou'll paste into the client config, and exposesinstall/update/uninstall/ reconfigure menus on re-run.
Option A — Download a pre-built release (recommended):
-
Go to the Releases page.
-
Download the right archive for your OS:
- Windows:
GooseRelayVPN-client-vX.Y.Z-windows-amd64.zip - macOS (Intel):
GooseRelayVPN-client-vX.Y.Z-darwin-amd64.tar.gz - macOS (M1/M2/M3):
GooseRelayVPN-client-vX.Y.Z-darwin-arm64.tar.gz - Linux:
GooseRelayVPN-client-vX.Y.Z-linux-amd64.tar.gz - Android / Termux (arm64):
GooseRelayVPN-client-vX.Y.Z-android-arm64.tar.gz
- Windows:
-
For the server, SSH into your VPS and download the binary for your server OS:
- Linux (most common):
wget https://github.com/kianmhz/GooseRelayVPN/releases/latest/download/GooseRelayVPN-server-vX.Y.Z-linux-amd64.tar.gz tar -xzf GooseRelayVPN-server-vX.Y.Z-linux-amd64.tar.gz
- Windows Server: download
GooseRelayVPN-server-vX.Y.Z-windows-amd64.zipfrom the Releases page and extract it to a folder such asC:\goose-relay\. See the Windows Server (NSSM) subsection of Step 8 below for service setup.
(Replace
vX.Y.Zwith the latest version number from the Releases page.) - Linux (most common):
💡 If the Releases page doesn't open, you can download directly using these links (replace
vX.Y.Zwith the latest version):
- Client — Windows:
https://github.com/Kianmhz/GooseRelayVPN/releases/download/vX.Y.Z/GooseRelayVPN-client-vX.Y.Z-windows-amd64.zip- Client — macOS (Apple Silicon):
https://github.com/Kianmhz/GooseRelayVPN/releases/download/vX.Y.Z/GooseRelayVPN-client-vX.Y.Z-darwin-arm64.tar.gz- Client — macOS (Intel):
https://github.com/Kianmhz/GooseRelayVPN/releases/download/vX.Y.Z/GooseRelayVPN-client-vX.Y.Z-darwin-amd64.tar.gz- Client — Linux:
https://github.com/Kianmhz/GooseRelayVPN/releases/download/vX.Y.Z/GooseRelayVPN-client-vX.Y.Z-linux-amd64.tar.gz- Client — Android/Termux:
https://github.com/Kianmhz/GooseRelayVPN/releases/download/vX.Y.Z/GooseRelayVPN-client-vX.Y.Z-android-arm64.tar.gz- Server — Linux:
https://github.com/Kianmhz/GooseRelayVPN/releases/download/vX.Y.Z/GooseRelayVPN-server-vX.Y.Z-linux-amd64.tar.gz
Option B — Build from source (Go 1.22+) — not recommended, may be unstable:
git clone https://github.com/kianmhz/GooseRelayVPN.git
cd GooseRelayVPN
go build -o goose-client ./cmd/client
go build -o goose-server ./cmd/serverOption C — Run only the server with Docker (GHCR):
If you prefer containers on your VPS, you can run goose-server directly from GHCR:
docker pull ghcr.io/kianmhz/gooserelayvpn-server:latestRun this once:
openssl rand -hex 32Copy the 64-character string it prints. You'll use the same value in both the client and server configs. Keep it secret — anyone with this key can use your tunnel.
Copy the example configs:
cp client_config.example.json client_config.json
cp server_config.example.json server_config.jsonOpen both files and paste your key into the tunnel_key field. Leave script_keys empty for now.
client_config.json:
{
"socks_host": "127.0.0.1",
"socks_port": 1080,
"google_host": "216.239.38.120",
"sni": "www.google.com",
"script_keys": ["PASTE_DEPLOYMENT_ID"],
"tunnel_key": "PASTE_OUTPUT_OF_GEN_KEY"
}server_config.json:
{
"server_host": "0.0.0.0",
"server_port": 8443,
"tunnel_key": "SAME_VALUE_AS_CLIENT"
}This is the free Google-side piece that hides your traffic.
- Go to Google Apps Script and sign in.
- Click New project.
- Delete the default code and paste everything from
apps_script/Code.gs. - Change this line to your VPS IP and port:
const RELAY_URLS = [ 'http://YOUR.VPS.IP:8443/tunnel', ];
RELAY_URLSis an array so you can list multiple VPS exit servers —Code.gstries them in order and only falls over to the next one on an error. For a single VPS, one entry is enough. - Click Deploy → New deployment → set type to Web app.
- Set Execute as: Me and Who has access: Anyone.
- Click Deploy. A dialog appears showing the Deployment ID. Copy that value and paste it into
script_keys. - Paste that ID into
script_keysinclient_config.json.
⚠️ Every time you editCode.gsyou must create a new deployment (Deploy → New deployment) and updatescript_keys. Just saving the code is not enough.
The server needs port 8443 to be reachable from the internet. On your VPS run:
sudo ufw allow 8443/tcpThen verify it works from your own computer (replace with your real VPS IP):
curl http://YOUR.VPS.IP:8443/healthzYou should get JSON like { "ok": true, "version": "vX.Y.Z", "protocol": 1 } with HTTP 200. If curl times out or refuses, also check your cloud provider's firewall (called "Security Groups" on AWS/Hetzner, "Firewall Rules" on DigitalOcean/Vultr, etc.) and add an inbound rule for TCP port 8443.
On your VPS, run the server binary:
Linux:
./goose-server -config server_config.jsonWindows Server:
.\goose-server.exe -config server_config.jsonYou should see it print the listening address and the healthz/tunnel URLs. Leave this terminal open, or set up the systemd/NSSM service (Step 8) to keep it running after reboots.
Docker (GHCR image):
⚠️ Important: The container does not auto-generateserver_config.json. You must create and editserver_config.jsonfirst (with your owntunnel_key), then start the container.
docker run -d \
--name goose-server \
--restart unless-stopped \
-p 8443:8443 \
-v $(pwd)/server_config.json:/app/server_config.json:ro \
ghcr.io/kianmhz/gooserelayvpn-server:latestDocker Compose (recommended for container setup):
cp server_config.example.json server_config.json
nano server_config.json
docker compose up -dThe repo includes docker-compose.yml. By default it uses ghcr.io/kianmhz/gooserelayvpn-server:latest, and you can override it with:
GOOSE_SERVER_IMAGE=ghcr.io/kianmhz/gooserelayvpn-server:vX.Y.Z docker compose up -dVerify from your own computer:
curl http://YOUR.VPS.IP:8443/healthzIf you want the exit server to start automatically after a VPS reboot, set up a service for your VPS OS.
Run on your VPS:
sudo nano /etc/systemd/system/goose-relay.servicePaste this (adjust the path if your binary is in a different location):
[Unit]
Description=GooseRelayVPN exit server
After=network.target
[Service]
Type=simple
WorkingDirectory=/root
ExecStart=/root/goose-server -config /root/server_config.json
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetThen run:
sudo systemctl daemon-reload
sudo systemctl enable goose-relay
sudo systemctl start goose-relay
sudo systemctl status goose-relay --no-pagerUse NSSM (Non-Sucking Service Manager) to register goose-server as a Windows service. The goose-server.exe binary is a plain Go binary — no installer needed.
1. Open port 8443 in Windows Firewall (run as Administrator in Command Prompt):
netsh advfirewall firewall add rule name="GooseRelayVPN" protocol=TCP dir=in localport=8443 action=allowAlso add an inbound TCP/8443 rule in your cloud provider's firewall panel (Security Groups / Firewall Rules).
2. Download NSSM from https://nssm.cc/download, extract it, and note the path to nssm.exe (e.g. C:\nssm\win64\nssm.exe).
3. Register and start the service (run as Administrator):
C:\nssm\win64\nssm.exe install GooseRelayVPN "C:\goose-relay\goose-server.exe"
C:\nssm\win64\nssm.exe set GooseRelayVPN AppParameters "-config C:\goose-relay\server_config.json"
C:\nssm\win64\nssm.exe set GooseRelayVPN AppDirectory "C:\goose-relay"
C:\nssm\win64\nssm.exe set GooseRelayVPN Start SERVICE_AUTO_START
C:\nssm\win64\nssm.exe start GooseRelayVPN4. Verify it is running:
C:\nssm\win64\nssm.exe status GooseRelayVPN
curl http://YOUR.VPS.IP:8443/healthzTo stop or uninstall later:
C:\nssm\win64\nssm.exe stop GooseRelayVPN
C:\nssm\win64\nssm.exe remove GooseRelayVPN confirmPick your OS — the client behaves identically on all four; only the launch command and a couple of OS-specific quirks differ.
./goose-client -config client_config.jsonmacOS marks every downloaded binary with com.apple.quarantine, so the first run fails with "Apple cannot check it for malicious software" unless you clear it:
xattr -d com.apple.quarantine goose-client 2>/dev/null || true
chmod +x goose-client
./goose-client -config client_config.json
⚠️ If you seecannot execute binary file: Exec format error, you downloaded the wrong architecture. Apple Silicon (M1/M2/M3/M4) needsdarwin-arm64; older Intel Macs needdarwin-amd64.
.\goose-client.exe -config client_config.jsonUse the backslash form (
.\…). The Unix-style./goose-client.exedoes not work incmd.exe— it tries to run a command literally named.and prints'.' is not recognized as an internal or external command.
There is no APK — the client runs inside Termux. If this is a fresh Termux install, bootstrap it before doing Steps 2–4:
apt update && apt upgrade -y
pkg install wget tar -yThen run the client:
chmod +x goose-client
./goose-client -config client_config.jsonYou should see output like this:
CLIENT INFO GooseRelayVPN client starting
CLIENT INFO SOCKS5 proxy: socks5://127.0.0.1:1080
CLIENT INFO pre-flight OK: relay healthy, AES key matches end-to-end
CLIENT INFO ready: local SOCKS5 is listening on 127.0.0.1:1080
The pre-flight check runs automatically at startup and verifies that Apps Script is reachable, the VPS is up, and the AES key matches. If it fails, the message tells you what went wrong.
Now point an app at SOCKS5 proxy 127.0.0.1:1080:
- Firefox: Settings → Network Settings → Manual proxy → SOCKS5 host
127.0.0.1port1080. Check Proxy DNS when using SOCKS v5. - Chrome/Edge: Use an extension like FoxyProxy or SwitchyOmega.
- System-wide on macOS/Linux: Set SOCKS5 in network settings.
- Android: Use a SOCKS5-aware app like NekoBox or v2rayNG. In per-app settings, enable the proxy for the apps you want and exclude Termux so the tunnel itself stays connected.
By default the client listens on 127.0.0.1:1080 so only your computer can use it. To share with other devices on your local network, set socks_host to 0.0.0.0 in client_config.json and restart.
⚠️ Security note: Anyone on your LAN can then proxy through your tunnel and consume your Apps Script quota. Only do this on trusted networks.
The ~20,000 calls/day quota applies per Google account, not per deployment or project — all deployments under the same account share one quota pool. The client polls about once per second when idle, so a single deployment can sustain steady use, but heavy days hit the cap. Real-time apps like Telegram or X can drain the quota within a few hours due to constant polling. To go beyond that, deploy Code.gs across different Google accounts and put all the Deployment IDs into script_keys.
⚠️ Label every deployment with the Google account it lives under. The client groups deployments by account so it can apply the per-account idle-poll cap correctly — Apps Script's per-second concurrency cap is also per-account. Two deployments under the same account share one bucket and one daily quota; two deployments under different accounts give you two buckets and two quotas. (The worker pool itself always scales with deployment count — 3 workers per deployment — regardless of labeling. What the labels control is how many of those workers may hold a standing idle long-poll against one Google account at the same time.)
{
"script_keys": [
{"id": "FIRST_DEPLOYMENT_ID", "account": "acct-a"},
{"id": "SECOND_DEPLOYMENT_ID", "account": "acct-a"},
{"id": "THIRD_DEPLOYMENT_ID", "account": "acct-b"},
{"id": "FOURTH_DEPLOYMENT_ID", "account": "acct-b"}
]
}The example above is 4 deployments across 2 accounts → 12 poll workers (3 per deployment), 2 buckets, 4 standing idle long-polls (2 per bucket × 2 buckets) — twice the daily quota of a single account, with the per-account idle cap respected so neither account trips Apps Script's anti-abuse.
If you leave the labels off (["ID1", "ID2", ...] plain strings), each deployment becomes its own implicit bucket — so the worker pool and idle slots still scale, but if multiple unlabeled deployments are actually under the same Google account the client cannot enforce the per-account cap and you may see Apps Script HTML error pages mid-session. The startup [carrier] log line tells you which mode is active. Label deployments with account whenever you can; bare strings exist mainly as a backward-compat affordance for older configs.
What the client does for you automatically:
- Round-robin across all configured deployments within active buckets.
- Health-aware blacklist — if one starts failing, the client backs off from it (3 s, 6 s, 12 s, … up to ~48 s) and keeps using the others.
- Same-poll failover — if a poll fails on one deployment, the same payload is retried on another within the same poll cycle, so no traffic is lost during transient quota or 5xx events.
- Per-account stats — the periodic
[stats]line aggregates request counts per account label so you can see how each Google account's daily quota is being spent.
💡 All deployments must use the same
tunnel_keybecause they all forward to the same VPS, which only has one AES key. You don't need to change anything on the VPS when you add more deployments.
💡 You can paste either just the Deployment ID (the part between
/s/and/exec) or the full/execURL — the client extracts the ID either way.
💡 A practical upper bound is 2–3 accounts. Adding more deployments under accounts you already have just spreads quota and rarely improves throughput; what helps is another distinct account.
| Field | Default | What it does |
|---|---|---|
socks_host |
127.0.0.1 |
Host/IP for the local SOCKS5 listener. Set to 0.0.0.0 for LAN sharing. |
socks_port |
1080 |
Port for the local SOCKS5 listener. |
google_host |
216.239.38.120 |
Google edge IP/host to dial (port is fixed to 443). |
sni |
www.google.com |
SNI presented during the TLS handshake. Accepts a single string or an array — ["www.google.com", "mail.google.com", "accounts.google.com"] — where each SNI host gets its own connection and throttle bucket, which can multiply available bandwidth in regions that rate-limit per domain name. |
script_keys |
— | Array of Apps Script deployments. Each entry can be a bare Deployment ID string or an object { "id": "...", "account": "..." } labeling the Google account it's deployed under. The account label is load-bearing: the client groups deployments by account and applies the per-bucket idle-poll cap (idle_slots_per_bucket, default 2) per group, matching Apps Script's per-account concurrency cap. Bare strings (or unlabeled objects) each get their own implicit bucket — fine if every deployment really is under its own Google account, but if multiple are actually under the same account you may trip per-account throttling. Label them with account to make this work correctly. See Increase capacity with multiple deployments. |
tunnel_key |
— | 64-char hex AES-256 key. Must match the server byte-for-byte. |
socks_user |
(optional) | SOCKS5 username (RFC 1929). When set, clients must authenticate or the connection is rejected. Must be paired with socks_pass — set both or neither. |
socks_pass |
(optional) | SOCKS5 password paired with socks_user. |
coalesce_step_ms |
0 (off) |
Adaptive uplink coalescing. Set it to a positive number to make the first kick of a burst of TX operations wait a little for more operations; each new operation resets the timer. This trades a bit of latency for fewer Apps Script calls. A good starting range is 20-40 ms. Set it to 0 to turn coalescing off. The internal safety cap is derived automatically from this value. |
idle_slots_per_bucket |
2 |
Download-throughput tuning. The carrier holds this many concurrent idle long-polls open per account bucket to receive downstream pushes. Default 2 is the best balance for accounts with 2+ deployments (the recommended configuration). Lower to 1 if each Google account has only one deployment. Raise to 3 for accounts with 3+ deployments. Max 3; values above are rejected. |
| Field | Default | What it does |
|---|---|---|
server_host |
0.0.0.0 |
Host/IP where the exit server binds. |
server_port |
8443 |
Port where the exit server listens. Must be reachable from Google's network. |
tunnel_key |
— | 64-char hex AES-256 key. Must match the client. |
upstream_proxy |
(optional) | Route all outbound connections through a local SOCKS5 proxy. Useful when your VPS datacenter IP is blocked by certain sites. Set to socks5://127.0.0.1:40000 to use Cloudflare WARP (DNS is resolved by the proxy, so target sites see the Cloudflare IP instead of your VPS IP). Leave empty or omit to dial directly. |
debug_timing |
false |
When true, logs per-session DNS and TCP dial latency so you can pinpoint where time is going. |
Configs are forward-compatible — new fields in client_config.json / server_config.json default sensibly, and old fields keep working. You generally don't need to start over.
Re-run the installer and pick Update from the menu:
bash <(curl -Ls https://raw.githubusercontent.com/Kianmhz/GooseRelayVPN/main/scripts/goose-server.sh)Or in one shot:
bash <(curl -Ls https://raw.githubusercontent.com/Kianmhz/GooseRelayVPN/main/scripts/goose-server.sh) updateThe script auto-detects the existing install, stops the service, downloads the latest release, verifies the SHA256 against the release's SHA256SUMS.txt, keeps your server_config.json untouched, and restarts. If you originally installed by hand (not via the script), the first run also offers to migrate everything to /root/goose/ so future updates are one command.
- Stop the service (
Stop-Service goose-relayon Windows,sudo systemctl stop goose-relayon Linux). - Download the new release archive from the Releases page and extract it.
- Replace
goose-server/goose-server.exewith the new one (keepserver_config.jsonas-is). - Restart the service.
- Stop the running
goose-client. - Download the new release archive for your platform from Releases.
- Extract and replace
goose-client(orgoose-client.exe); keep your existingclient_config.json. - macOS only — clear the Gatekeeper quarantine on the new binary:
xattr -d com.apple.quarantine goose-client 2>/dev/null || true chmod +x goose-client
- Run it again.
You don't need to touch your Apps Script deployment unless you also change Code.gs — see below.
If you change Code.gs — for example to point at a new VPS IP — you must create a new deployment in the Apps Script editor (Deploy → New deployment, not just "Manage deployments"). Saving alone does nothing; the live /exec URL serves the published version. After redeploying, update script_keys in client_config.json.
The current Code.gs exposes forwarder/protocol metadata via doGet, which the client's pre-flight check uses to detect version mismatches. Per-deployment invocation counting is also implemented, but only fires when you set ENABLE_INVOCATION_COUNTING = true at the top of Code.gs (off by default so doPost stays fast under contention). Enable it if you want the script=N field in the client's periodic [stats] line.
For a deeper walk-through — wire format, package layout, and the rationale behind each non-obvious design decision — see ARCHITECTURE.md.
┌─────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────┐
│ Browser │──►│ goose-client │──►│ Google edge │──►│ Apps Script │──►│ Your │──► Internet
│ / App │◄──│ (SOCKS5) │◄──│ TLS, fronted │◄──│ doPost() │◄──│ VPS │◄──
└─────────┘ └──────────────┘ └──────────────┘ └─────────────┘ └──────────┘
AES-256-GCM SNI=www.google dumb forwarder decrypt +
session multiplex Host=script.… no plaintext net.Dial
Key invariants:
- Authentication = AES-GCM tag. No shared password, no certificates. Frames that fail
Open()are dropped silently. - Apps Script never sees plaintext. The script is a ~30-line forwarder; the AES key lives only on your machine and the VPS.
- DNS travels through the tunnel. The SOCKS5 server uses a no-op resolver; use
socks5h://so DNS is resolved at the exit, not locally. - Long-poll, full-duplex. The VPS holds each request open for up to 8s waiting for downstream bytes; the client runs 3 concurrent poll workers per deployment in
script_keys— so 1 deployment = 3 workers, 4 deployments = 12 workers — while a per-bucket idle-slot semaphore (default 2 per labeledaccount) caps how many of those workers can hold a standing long-poll against one Google account at once. The split between worker scaling and idle-poll capping is the fix for the v1.6 regression where users with multiple IDs under one account hit Apps Script's per-second concurrency cap and saw HTML error pages mid-session. Downstream frames are coalesced in a small (~25 ms) window so streaming workloads send fewer, larger HTTP responses. - Health-aware multi-deployment. When
script_keyslists more than one deployment, the client picks endpoints in round-robin and exponentially blacklists any that misbehave; one same-poll retry is attempted on a fresh deployment so transient failures don't drop traffic.
- Frame (plaintext, inside the sealed batch):
session_id (16) || seq (u64 BE) || flags (u8) || target_len (u8) || target || payload_len (u32 BE) || payload - Batch seal (AES-GCM): the entire batch is sealed once —
nonce (12 bytes) || AES-GCM(u16 frame_count || [u32 frame_len || frame_bytes] …)— one nonce and auth-tag per HTTP body, not per frame. - HTTP body:
base64(nonce || ciphertext+tag), base64 so it survives Apps Script'sContentServicetext round-trip.
GooseRelayVPN exists to defeat a specific kind of adversary: an ISP or state-level actor that controls DNS resolution on the user's network, can redirect traffic via BGP or transparent proxying, and may attempt TLS interception of foreign traffic. This kind of filtering typically still leaves a small set of Google service IPs reachable. This tool routes through that gap.
Four layers stack to defeat that model:
- A hardcoded Google edge IP in
client_config.json— the client never asks the local resolver forscript.google.com, so DNS hijacking has nothing to hijack. - TLS certificate validation on the outer hop — the adversary cannot impersonate Google without Google's private key, and the handshake fails closed if it tries.
- AES-256-GCM end-to-end between client and VPS, with the 16-byte GCM authentication tag — defeats both passive observation (no key, no plaintext) and active tampering (a modified byte fails the tag check and the whole batch is dropped).
- A no-op SOCKS5 resolver on the client — the destination hostname for tunneled traffic travels through the tunnel as a string and is resolved on the VPS, so the adversary cannot see what the user is browsing.
The same architecture also functions as a conventional VPN at the destination end: because the exit is a VPS outside the filtered network, destination sites see the VPS's IP, not the user's. This enables IP masking and geo-unblocking for services that block traffic from the user's region at the network level — cloud providers, app stores, banks, and the like. These are standard VPN benefits that fall out of having the exit in a permitted country.
Like any single-hop tunnel, this isn't a complete security solution: a compromised VPS, a compelled Google account, or DPI pattern-fingerprinting can each break the threat model. For the explicit out-of-scope list and why each one matters, see ARCHITECTURE.md → Threat model.
GooseRelayVPN/
├── cmd/
│ ├── client/main.go # Entry point: SOCKS5 listener + carrier loop
│ └── server/main.go # Entry point: VPS HTTP handler
├── internal/
│ ├── protocol/ # Wire-level constants both peers share (frame cap, batch caps, probe types)
│ ├── frame/ # Wire format, AES-GCM seal/open, batch packer
│ ├── session/ # Per-connection state, seq counters, rx/tx queues
│ ├── socks/ # SOCKS5 server + VirtualConn (net.Conn adapter)
│ ├── carrier/ # Long-poll loop + domain-fronted HTTPS client
│ ├── exit/ # VPS HTTP handler: decrypt, demux, dial upstream
│ └── config/ # JSON config loaders
├── bench/
│ ├── harness/main.go # E2E benchmark: real binaries, loopback sink
│ ├── sink/main.go # TCP sink (echo / sized / source / quick modes)
│ ├── diff/main.go # JSON result comparator with noise-floor logic
│ ├── baselines/ # Committed baseline JSON files
│ └── bench.sh # Build + run + compare orchestrator
├── apps_script/
│ └── Code.gs # ~30-line dumb forwarder
├── scripts/
│ └── goose-relay.service # systemd unit template
├── client_config.example.json
└── server_config.example.json
| Problem | Solution |
|---|---|
cannot execute binary file: Exec format error when running goose-server or goose-client |
You downloaded the wrong archive for your OS/architecture. The folder name tells you what you got — e.g. …-darwin-amd64 is a macOS binary and won't run on Linux. Re-download the matching archive (Linux VPS → linux-amd64; Apple Silicon Mac → darwin-arm64; Termux → android-arm64). |
Pre-flight fails: cannot reach Apps Script |
Your internet connection can't reach Google. Check google_host — try a different IP from the 216.239.x.120 range. |
Pre-flight fails: HTTP 204 — key mismatch |
The tunnel_key in client_config.json doesn't match the one in server_config.json on the VPS. They must be byte-identical. |
Pre-flight fails: Apps Script cannot reach your VPS |
Port 8443 on your VPS is not reachable. Run sudo ufw allow 8443/tcp on the VPS and check your cloud provider's firewall rules. |
Log says relay returned non-batch payload |
Apps Script returned an HTML page instead of an encrypted batch. Three common causes: (1) the deployment in script_keys isn't live, or Who has access is not set to Anyone — re-deploy (Deploy → New deployment) and update script_keys; (2) the deployment was added to an existing Apps Script project alongside other files — create a new project with only Code.gs in it, then deploy from there; (3) you have multiple deployments under the same Google account and are hitting that account's per-second concurrency cap — label script_keys entries with their account so the client throttles per-account (see Increase capacity with multiple deployments). |
Log says relay returned HTTP 404 via … |
The Deployment ID in your config doesn't match a live /exec. Re-deploy and update the config. |
Log says relay returned HTTP 500 via … |
Apps Script can't reach any URL in RELAY_URLS. Check the server address(es) in Code.gs, confirm the VPS is up, and confirm inbound TCP/8443 is open. curl http://your.vps.ip:8443/healthz should return 200. |
Log says relay request failed via …: timeout |
Fronted connection to Google is failing. Try a different google_host — any 216.239.x.120 served by Google works. |
| Browser hangs on every request | Make sure your browser extension uses SOCKS5 with DNS through proxy enabled (not plain SOCKS5). In Firefox, check Proxy DNS when using SOCKS v5. |
[exit] dial X: ... timeout on the VPS server logs |
The target host blocks datacenter IPs, or your VPS has no outbound connectivity for that port. |
| Cloudflare-protected sites show captchas | Expected. Your VPS's IP is on a datacenter ASN, which Cloudflare's bot scoring often flags. Not a tunnel bug. |
| YouTube buffers a lot at 1080p | Expected. The tunnel adds ~300-800ms per round trip due to Apps Script dispatch overhead. 480p is comfortable. Deploying multiple script_keys (see above) helps with sustained throughput. |
| One deployment hits quota mid-session | If script_keys has more than one entry, the client automatically blacklists the failing one for a few seconds and keeps going on the others. With only one entry, browsing stops until the quota resets at midnight Pacific (~10:30 AM Iran time in summer, ~11:30 AM in winter). |
| Mismatched AES keys | Symptom: client logs no errors but no traffic flows; VPS logs no dial ... lines. Confirm tunnel_key is byte-identical in both configs. |
- Never share
client_config.jsonorserver_config.json— the AES key is in there and a leaked key means anyone can tunnel through your VPS. - Generate a fresh key with
openssl rand -hex 32for every deployment. Don't reuse keys across hosts. - AES-GCM is the only authentication. There's no password, no rate-limiting, no per-user accounting. Treat the key like a server-admin password.
- Apps Script logs every
doPostinvocation in Google's dashboard (count and duration only — Apps Script never sees plaintext). - Keep
socks_hoston the client at127.0.0.1unless you specifically want LAN sharing. - Each Google account is rate-limited to ~20,000 Apps Script UrlFetch calls/day on the free tier — shared across every deployment under that account, not per-deployment.
Pull requests are welcome. For any change that touches the carrier loop, session layer, or poll behavior, please include benchmark results so reviewers can evaluate the performance impact.
The bench/ directory contains an end-to-end harness that spins up real goose-client and goose-server binaries against a loopback TCP sink and measures throughput, TTFB, session rate, and idle CPU.
# Build the binaries and run the full benchmark suite
bash bench/bench.shThe harness compares your working tree against the committed baseline in bench/baselines/ and prints a side-by-side table. Regressions above the noise floor fail the script with exit code 1. Include the output in your PR description.
To record a new baseline from a specific git ref:
bash bench/bench.sh --update <ref> # e.g. --update v1.3.0 or --update HEADIf you like this project, please consider starring it on GitHub (⭐). It helps the project get discovered.
You can also support the project financially:
- TRX / USDT TRC20:
TSxg2WAXYnkoR2UiUTzCxbmqNARAt91aqB - BNB / USDT BEP20:
0xe7b48d8fd5fbbb4e3fa9a06723a62a88585139ea - TON:
UQDBzJqzJ5e7uZFPrmarTRSGGbD1UoFK2q5_jWh4D2nnNdUB
GooseRelayVPN is provided for educational, testing, and research purposes only.
- Provided without warranty: This software is provided "AS IS", without express or implied warranty, including merchantability, fitness for a particular purpose, and non-infringement.
- Limitation of liability: The developers and contributors are not responsible for any direct, indirect, incidental, consequential, or other damages resulting from the use of this project.
- User responsibility: Running this project outside controlled test environments may affect networks, accounts, or connected systems. You are solely responsible for installation, configuration, and use.
- Legal compliance: You are responsible for complying with all local, national, and international laws and regulations before using this software.
- Google services compliance: If you use Google Apps Script with this project, you are responsible for complying with Google's Terms of Service, acceptable-use rules, quotas, and platform policies. Misuse may lead to suspension of your Google account or deployment.
- License terms: Use, copying, distribution, and modification are governed by the repository license. Any use outside those terms is prohibited.
Special thanks to @abolix for making this project possible.
This project was inspired by the idea in masterking32/MasterHttpRelayVPN.
MIT