Skip to content
ThaiG2ProPublic

About

L7 reverse proxy + load balancer from raw TCP sockets in Go, zero deps. A measured lab notebook.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

EdgeGate

ci Go deps License: MIT

An L7 reverse proxy and load balancer written in Go from raw TCP sockets. The data path uses only net. No net/http, no third-party modules.

This is not a production proxy. It is a lab notebook. Each phase states hypotheses up front, then confirms or refutes them with a reproducible measurement, a fuzzer, or a counter-proof test that must go red when the defense is compiled out.

🇻🇳 The lab notes (diary/, docs/, code comments) are written in Vietnamese. Vietnamese README: README.vi.md.

Highlights

  • HTTP/1.1 and HTTP/2 implemented by hand. Includes the parser, chunked encoding, HPACK, and flow control. h2spec passes 145/145 over TLS+ALPN and in h2c-only mode.
  • Request smuggling defense, tested against real servers. 66 attack payloads in testdata/smuggle/. Go's net/http agrees with EdgeGate's rejections only 46% of the time. nginx and h2o were used as extra oracles.
  • Key defenses have a counter-proof. 11 *-nodefense targets in the Makefile compile a defense out with a build tag and assert that the matching test goes red.
  • Edge features with numbers attached. Connection pool, RR / least-conn / P2C+EWMA / consistent hashing, active health checks with passive outlier ejection, rate limiting, load shedding, retry budget, graceful drain, TLS with SNI routing, splice(2), and an epoll server.
  • Measured, not claimed. 168 tests and 5 fuzzers. 66 tracked technical debts are all closed, each with the command that closed it, in docs/debts.md.

Quick start

go build -o bin/upstream ./cmd/upstream
go build -o bin/edgegate ./cmd/edgegate

bin/upstream -addr 127.0.0.1:8081 &
bin/edgegate -listen 127.0.0.1:8080 -upstream 127.0.0.1:8081 &

curl -i http://127.0.0.1:8080/hello
# HTTP/1.1 200 OK ... hello from upstream

A CL.TE smuggling attempt is rejected before it reaches the upstream:

printf 'POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n' \
  | nc 127.0.0.1 8080
# HTTP/1.1 400 Bad Request

More configs live in config/: load balancing, resilience, TLS, and HTTP/2.

Findings that surprised me

These are measured results. WSL2 numbers come from the original phase runs. Bare-Linux numbers come from a re-run on CachyOS, 15 threads, on 2026-10-04, with raw output in bench/baseline/thai-computer-20261004/.

Finding WSL2 Bare Linux Source
Closed-loop load generators hide overload. Same system, two tools, different p99. 370× 276–476× phase 0
Nagle + delayed ACK is a constant floor, not a tail effect. p50 ≈ p99. 44 ms 41 ms phase 0
L7 proxy overhead vs direct connection, p50, new upstream dial per request. Hypothesis was 3-5×. 3.39× phase 3
Extra latency through the proxy with the connection pool on. +101 µs +51–78 µs phase 5
P2C+EWMA only helps when the slow node takes under 1% of traffic. It loses when a node recovers. 3.4× vs 0.98× phase 6
Load shedding under overload. p99 352× better phase 7
epoll vs goroutine-per-connection: same throughput, far less memory. 55× less RAM phase 9

Two numbers were withdrawn. The first published coordinated-omission ratio, 1787×, was measured with an open-loop generator that had a race. After the fix it is 370× on WSL2 (errata in phase 0).

The second is a claim about ephemeral port exhaustion. On WSL2 it showed 0 errors, only falling throughput (1502 → 455 conn/s). On bare Linux the same test ended in EADDRNOTAVAIL after falling from 1089 to 386 conn/s. The two runs never reached the same TIME_WAIT level, so it is still open whether WSL2 really stays quiet. See blog post 4.

The wrong hypotheses are recorded too. Phase 0 got 5 of 8 wrong.

Layout

internal/
  frame/      length-prefixed framing over TCP
  httpx/      HTTP/1.1 request/response parser
  proxy/      forwarding, pool, retry, smuggling defense, resilience
  lb/         load-balancing algorithms, health checks, outlier ejection
  limit/      token bucket, retry budget, concurrency limits
  h2/         HTTP/2 frames, HPACK, flow control
  tlsx/       certificate store and SNI selection
  epollsrv/   hand-written epoll event loop (phase 9 comparison)
cmd/
  edgegate/   the proxy
  upstream/   a test backend
  *lab/       one measurement harness per phase (netlab, proxylab, lblab, chaoslab, ...)

Phases

# Topic Result
0 TCP physics: RTT, Nagle, port exhaustion 5/8 hypotheses wrong
1 Framing 4/7 wrong; fuzzed 1.58M execs clean
2 HTTP/1.1 engine differential fuzz: 0 diffs; manual table found 2 real diffs
3 Vertical slice: first end-to-end curl L7 overhead 3.39× on WSL2 with a new dial per request
4 RFC 9112 compliance and smuggling 61 cases; counter-proof red 20/52
5 Upstream connection pool dirty-connection counter-proof red 20/20
6 Load balancing and health 4/7 wrong
7 Resiliency Slowloris did not kill Go; the connection cap did
8 TLS + SNI 421 blocks domain fronting; reload with 0 errors
9 Performance, splice, epoll on par with nginx; httputil.ReverseProxy 3.1× slower (WSL2)
10 HTTP/2 h2spec 145/145; Rapid Reset and CONTINUATION flood defended

Full write-ups: diary/. Roadmap and architecture: ROADMAP.md. A 17-post blog series in Vietnamese, one experiment per post: blog/.

Running the tests

make test        # go test ./... -race
make phase0      # run the phase 0 experiments, results in bench/

Latency numbers from WSL2 are only indicative. docs/REPRODUCE-LINUX.md lists the commands and pass criteria for bare-metal Linux.

License

MIT

About

L7 reverse proxy + load balancer from raw TCP sockets in Go, zero deps. A measured lab notebook.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages