GooseRelayVPN: Tunneling TCP through Google Apps Script with Go
GooseRelayVPN does something audacious: it shoves raw TCP traffic through Google Apps Script. Your SOCKS5 client sends encrypted frames to a Google Apps Script URL, which relays them to your VPS exit server. To anyone watching the wire, it’s just HTTPS requests to script.google.com. Good luck blocking that without taking down all of Google. The whole thing is written in Go — client and server — and it’s a surprisingly clean read if you’re into framing protocols, AES-GCM encryption, session multiplexing, and domain fronting. Almost everything is built on the standard library.
The architecture in brief
Here’s the flow:
- A local SOCKS5 server listens on your machine (
internal/socks/server.go) - The client encrypts TCP data into frames using AES-256-GCM (
internal/frame/crypto.go) - Encrypted frames get sent as HTTP requests to a Google Apps Script relay (
internal/carrier/client.go) - The relay forwards them to your VPS
- The VPS exit server (
internal/exit/exit.go) decrypts frames and dials the real destination
The SNI that any observer sees is script.google.com or another Google domain, so it looks like ordinary Google traffic. That’s domain fronting — the TLS SNI and the HTTP Host header point to different places.
Framing: how bytes become messages
TCP is a stream. GooseRelayVPN needs discrete messages to stuff into HTTP request/response pairs. The framing layer in internal/frame/frame.go solves this.
Each frame has a header with the session ID, sequence number, and payload length, followed by the encrypted payload. If you’ve written Go networking code before, the pattern is familiar: define a binary wire format with encoding/binary, read and write it with fixed-size operations.
The crypto layer in internal/frame/crypto.go wraps each frame’s payload with AES-256-GCM. Here’s what that looks like in Go, and what GooseRelayVPN follows:
package frame
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
)
func Encrypt(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
Note: This is a simplified illustration of the standard crypto/aes + crypto/cipher pattern the project uses — not a line-for-line copy of the source. The real implementation in internal/frame/crypto.go follows this same approach with the project’s key management wired in.
AES-GCM gives you confidentiality and integrity in one shot. If anyone tampers with a frame in transit, gcm.Open fails. This matters because the relay (Google Apps Script) is untrusted — you want end-to-end encryption between client and exit server, and the relay should never see plaintext. The crypto package docs are worth reading if you want the full picture of how Go handles this.
The SOCKS5 server
The local proxy lives in internal/socks/server.go and internal/socks/conn.go. It handles the SOCKS5 handshake — version negotiation, auth method selection, and the connect request — then bridges the local TCP connection to the carrier layer.
The connection bridging in internal/socks/conn.go is where Go’s concurrency model earns its keep. Bidirectional TCP proxying typically looks like this:
func bridge(local net.Conn, remote io.ReadWriteCloser) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
io.Copy(remote, local)
// Signal remote that we're done writing
}()
go func() {
defer wg.Done()
io.Copy(local, remote)
// Signal local that we're done writing
}()
wg.Wait()
}
Two goroutines, one per direction. io.Copy blocks until the source closes or errors. sync.WaitGroup makes sure we wait for both directions before cleanup. This is bread-and-butter Go networking — goroutines are cheap enough that one-per-direction is the idiomatic move. If you want a refresher on how Go’s context package fits into patterns like this, see What is Context in Go?.
Domain fronting with the carrier
The carrier package (internal/carrier/) is where the censorship evasion lives. internal/carrier/fronting.go implements the domain fronting logic. The TLS connection uses one SNI hostname (a Google domain), but the HTTP Host header inside that encrypted connection points to the actual Google Apps Script endpoint.
The client in internal/carrier/client.go sends encrypted frames as HTTP POST bodies to the Apps Script URL. Responses carry return traffic. This turns a bidirectional TCP stream into a series of HTTP request/response pairs — not elegant, but effective.
internal/carrier/stats.go tracks throughput and latency for the relay connection, and internal/carrier/diagnose.go provides diagnostic tooling. That last one is genuinely useful when you’re trying to figure out whether a particular network is blocking the fronted domain or something else is broken.
Session management
internal/session/session.go multiplexes multiple SOCKS5 connections over the single relay channel. Each connection gets a session ID. The framing layer tags every frame with it so the exit server knows which outbound TCP connection a frame belongs to.
You see this same pattern in HTTP/2 and SSH — multiplexing streams over a single transport. In Go, it usually means a map[string]*Session behind a sync.Mutex, with goroutines handling reads and writes per session.
The exit server
On the VPS side, internal/exit/exit.go receives frames from the relay, decrypts them, and dials the actual destination. internal/exit/dnscache.go caches DNS lookups to avoid hammering resolvers when you’re proxying many connections to the same domains. Small optimization, but it adds up. internal/exit/stats.go mirrors the carrier-side stats tracking.
The server entry point is cmd/server/main.go. The client entry point is cmd/client/main.go, which also includes platform-specific terminal color support (cmd/client/color.go, cmd/client/color_windows.go, cmd/client/color_other.go) using build tags. If you haven’t seen this before: Go lets you use file name suffixes like _windows.go so the compiler picks the right implementation per OS. It’s one of those features that seems trivial until you need it.
Configuration
internal/config/client.go and internal/config/server.go define the configuration structures. The config includes the AES key, relay URL, listen addresses, and fronting parameters. Client and server configs are separate types, which is the right call — mixing them into one struct is a common mistake that leads to confusion about which fields matter where.
Benchmarking
The project ships a benchmarking suite in bench/. bench/harness/main.go drives the tests, bench/sink/main.go acts as a data sink, and bench/diff/main.go compares results. I like seeing benchmarks baked into the repo, especially for a proxy where throughput and latency are the whole ballgame. If you’re building Go projects that need performance validation, check out how Go handles options patterns for keeping benchmark configs clean.
What’s interesting from a Go perspective
A few things stand out when reading through this codebase:
The go.mod is lean. Most of the work uses crypto/aes, crypto/cipher, net, net/http, encoding/binary, and sync — all standard library. I appreciate the restraint. Too many Go projects reach for third-party packages when the stdlib already does the job.
The color output files (color_windows.go, color_other.go) show proper use of Go’s build constraint system. It’s a small thing, but it tells you someone thought about cross-platform from the start.
The SOCKS5 server and session layer follow Go’s natural concurrency model: spawn a goroutine per connection, coordinate with channels or mutexes. Nothing clever, nothing surprising. That’s a compliment.
And the internal/ directory structure is well-drawn. Each package (frame, carrier, socks, session, exit, config) owns one thing. The internal/ prefix means none of these are importable by external code, which gives the authors freedom to change internals without breaking anyone.
If you’re building network tools in Go — proxies, tunnels, VPNs — this repo is worth reading. It’s compact, and it covers a lot of ground: custom framing, AES-GCM, SOCKS5, HTTP-based transport, domain fronting. All in a small codebase you can actually hold in your head. For another angle on clean Go architecture, take a look at AList’s storage abstraction patterns.