Skip to content

protocol/meek: domain-fronted meek outbound (draft)#265

Merged
myleshorton merged 16 commits into
mainfrom
fisk/meek-outbound
Jun 26, 2026
Merged

protocol/meek: domain-fronted meek outbound (draft)#265
myleshorton merged 16 commits into
mainfrom
fisk/meek-outbound

Conversation

@myleshorton

@myleshorton myleshorton commented May 19, 2026

Copy link
Copy Markdown
Contributor

Companion to radiance#488 (fronted/scanner).

Summary

Adds a meek outbound type to lantern-box: a meek-v1 client that tunnels arbitrary TCP through chunked HTTPS POSTs to a meek server endpoint. Domain-fronted via per-dial random pick from a configured Fronts list. Session-keyed by a per-Conn random ID in X-Session-Id. meek originated in Tor's pluggable-transport framework but is architecturally distinct from Tor PTs; in practice the polling-over-HTTPS scheme implemented here is the one Psiphon and Lantern use.

Why

Today our domainfront is a control-plane mechanism only — it routes API calls (config fetch, bandit callbacks) to api.iantem.io through Akamai or CloudFront. User traffic still goes through whichever proxy was assigned. If all proxies are blocked, user traffic dies regardless of how well domainfront is working.

A meek transport closes that gap: bytes flow client → wrapped in HTTPS POST → CDN edge → meek server → unwrapped → routed to internet. When normal proxies are down, fronted traffic continues to flow.

Server-side topology

Deployed. The meek server runs on a dedicated Linode VPS (kept off our API infrastructure, so user data-plane traffic never touches api.iantem.io). The live stack:

Akamai (meek.dsa.akamai.getiantem.org)  →  Caddy :443 (TLS)  →  meek-server :8080  →  microsocks 127.0.0.1:1080 (SOCKS5)  →  internet

meek-server is this PR's cmd/meek-server; it's shipped/updated via cmd/meek-server/deploy.sh (build → scp → sha256-verify → atomic swap → restart → /healthz → rollback) and runs under systemd alongside Caddy + microsocks. The outbound's URL/InnerHost are config knobs, not hardcoded. The cross-language client (spark/flint Rust meek) is live-verified end-to-end against this server. lantern-cloud assigning meek as an outbound is tracked separately (getlantern/engineering#3526).

Wire format

POST <URL> HTTP/1.1
Host: <inner host>
X-Session-Id: <hex session id>
Content-Type: application/octet-stream
Content-Length: <N>

<N bytes of outbound payload>

---

200 OK
Content-Type: application/octet-stream

<up to MaxBodyBytes of inbound payload (or empty)>

Client polls every PollIntervalMs (default 100 ms) so the server can deliver queued inbound bytes even when the client has nothing to send.

Pieces

  • option/meek.goMeekOutboundOptions carries:

    • URL: meek server endpoint (e.g. https://meek.lantern.io/meek/)
    • Fronts []FrontSpec: candidate (IPAddress, SNI, VerifyHostname) tuples; one is picked at random per dial
    • Polling/buffering knobs: PollIntervalMs, MaxBodyBytes, SessionIDLen, ConnectTimeout, ReadTimeout
    • Header for fixed extra HTTP headers per request
    • SNI semantics: empty SNI sends no extension (Akamai-style); non-empty SNI is sent verbatim (CloudFront-style)
  • protocol/meek/client.goDial(ctx, Config) (*Conn, error) produces a net.Conn. Background poll goroutine:

    • Drains writeBuf into the next POST body (capped at MaxBodyBytes)
    • Reads response body into readBuf so callers' Read unblocks
    • Ticks every PollInterval or immediately when Write signals
    • SetReadDeadline / SetWriteDeadline honored
  • protocol/meek/outbound.go — sing-box adapter. Builds an *http.Client whose DialTLSContext:

    • Picks a random FrontSpec from Fronts
    • Dials FrontSpec.IPAddress:443 via the standard sing-box dialer (respects DialerOptions)
    • Sets ServerName = FrontSpec.SNI (or omits the extension if empty)
    • Verifies cert chain against FrontSpec.VerifyHostname

    DialContext then performs a SOCKS5 CONNECT to destination over the meek tunnel before returning the conn. sing-box treats meek as a terminal outbound and writes the application stream straight into the conn, so the destination has to be conveyed to the server's upstream — a SOCKS5 proxy (microsocks). Without the CONNECT, the upstream would read the app's opening bytes as a malformed SOCKS handshake. This makes the meek server's Upstream a SOCKS5-server contract (documented on ServerConfig.Upstream).

  • Registration: constant.TypeMeek = "meek", plus the standard RegisterOutbound wiring in protocol/register.go. Added to supportedProtocols.

Sequence

sequenceDiagram
    participant App as app
    participant SB as sing-box
    participant MK as meek outbound
    participant Front as CDN edge
    participant Srv as meek server (Linode)

    App->>SB: TCP connect to destination
    SB->>MK: DialContext
    MK->>MK: Dial(ctx, Config)
    Note over MK: generate sessionID, start pollLoop
    MK-->>SB: net.Conn ready
    SB-->>App: stream open

    loop application bytes flow
        App->>SB: Write(bytes)
        SB->>MK: Write(bytes)
        Note over MK: buffer, signal pollReady
        MK->>Front: POST /meek/ Host:meek.lantern.io<br/>X-Session-Id: ...<br/>body=bytes
        Front->>Srv: route by inner Host
        Srv-->>Front: response body = upstream bytes
        Front-->>MK: response body
        Note over MK: append to readBuf
        SB-->>App: Read returns
    end

    loop on every PollInterval, even when client has nothing
        MK->>Front: POST (empty body)
        Front-->>MK: queued inbound bytes
    end
Loading

Server side (included in this PR)

  • protocol/meek/server.goServer (an http.Handler) implementing the meek-v1 server: per-session upstream TCP connection, request body → upstream, upstream bytes → response body, idle-session reaper. Optional AuthToken shared secret (X-Meek-Auth, constant-time compared) — when set, unauthenticated requests get 403; without it the server is an open relay into the upstream, so production on a public/fronted hostname must set it.
  • cmd/meek-server — deployable binary wrapping the server (-listen, -upstream, -auth-token, -holdoff, -idle-timeout, …). Warns when -auth-token is unset.
  • cmd/meek-server/smoketest/socks5.sh — end-to-end smoke test against the deployed server (SOCKS5 handshake + HTTP GET through the meek tunnel, asserts the proxy egress IP).

Security hardening (from review)

  • Outbound rejects non-https URLs (would bypass the fronted TLS dialer) and fronts with no cert identity (verify_hostname/sni both empty → no real cert check).
  • Bounded client write backlog (MaxWriteBufBytes, default 1 MiB) with backpressure, so a fast sender on a slow front can't OOM the process.
  • Reserved request headers (Host, Content-Type, X-Session-Id) can't be overridden via the header config.
  • Server read pump uses a sync.Cond instead of a sleep-based busy-wait under backpressure.

Second review pass:

  • Destination routing: DialContext SOCKS5-CONNECTs to destination over the tunnel (see the outbound.go bullet above) — previously the destination was dropped and raw app bytes hit the SOCKS5 upstream.
  • Oversized request bodies: the server returns 413 rather than silently truncating a POST larger than MaxBodyBytes and forwarding a corrupted prefix upstream.
  • Read-pump liveness: the pump only blocks when pending is non-empty, so a single upstream read larger than MaxBodyBytes*4 can't deadlock delivery.
  • Write-cap correctness: Write appends in remaining-capacity chunks, so one large slice can't grow writeBuf past MaxWriteBufBytes.

Tests

Unit tests against an in-process meek echo server plus the new hardening:

  • TestConn_RoundTrip, TestConn_SessionPersistence, TestConn_RequiresHTTPClient/TestConn_RequiresURL — core protocol + config validation.
  • TestConn_SetReadDeadlineUnblocksParkedRead, TestConn_ReservedHeadersNotOverridable — deadline wakeup + reserved-header protection.
  • TestServer_AuthTokenRequired — 403 without/with wrong token, proceeds with the right one.
  • TestNewOutbound_RejectsUnsafeConfig — http scheme + identity-less front rejected.
  • TestServer_SOCKS5ConnectOverTunnel — full client→meek→SOCKS5→destination chain via the same ClientHandshake5 the outbound runs.
  • TestServer_RejectsOversizedBody — 413 on a POST over MaxBodyBytes.
  • TestServer_SmallMaxBodyBytesDelivers — read-pump liveness regression with a tiny cap and a 64 KiB upstream burst.
  • TestConn_LargeWriteRespectsBacklogCap — a 1 MiB Write blocks at the cap instead of buffering wholesale.

What's NOT in this PR

  • lantern-cloud assignment: having the config server assign meek as an outbound to clients is a follow-up (getlantern/engineering#3526). The server code + a running deployment (Linode VPS: meek-server + Caddy + microsocks, via cmd/meek-server/deploy.sh) are in/with this PR.
  • Front-list feed: Fronts comes from radiance/fronted/scanner (radiance#488) but the wiring between them is a follow-up. Today you'd hardcode Fronts in the JSON config.
  • uTLS: the outbound uses stdlib crypto/tls for simplicity. Switching to refraction-networking/utls is a follow-up; the rest of lantern-box already uses it.
  • Per-(ASN, country) bandit aggregation: covered by getlantern/engineering#3525.

Reference

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Meek-based, domain-fronted tunneling outbound with configurable front options and headers.
    • Introduced a Meek server command with authentication controls and a /healthz endpoint.
    • Added deployment tooling and an end-to-end SOCKS5 smoke test for live verification.
  • Bug Fixes
    • Improved tunnel reliability with retry-safe session behavior, bounded response/body handling, deadline-safe reads/writes, and more robust SOCKS5 CONNECT handling.
  • Documentation
    • Added live smoke test documentation, topology guidance, and expected results.
  • Tests
    • Added comprehensive client/server integration and regression tests, including retry integrity and session EOF handling.

Adds a first-class meek-style transport (Tor pluggable-transport v1
wire format): chunked TCP-over-HTTPS, session-keyed by a per-Conn
random ID in X-Session-Id, polling-based half-duplex.

The intended deployment is a separate meek server on a non-API
domain (e.g. running on a Linode VPS), reachable through Akamai or
CloudFront via inner Host. This keeps user data-plane traffic off
api.iantem.io and onto independent infrastructure.

Wire shape per request:
  POST <URL> HTTP/1.1
  Host: <inner host>
  X-Session-Id: <hex session id>
  Content-Type: application/octet-stream
  Content-Length: <N>
  <N bytes of outbound payload>

Response body is up to MaxBodyBytes of inbound payload (or empty).
The client polls every PollIntervalMs (default 100) so the server
can deliver inbound bytes even when the client has nothing to send.

Pieces:
- option/meek.go: MeekOutboundOptions carries URL + Fronts list +
  polling/buffering knobs. FrontSpec is (IPAddress, SNI,
  VerifyHostname) — empty SNI sends no extension (Akamai style),
  non-empty SNI is sent verbatim (CloudFront style).
- protocol/meek/client.go: Conn implementing net.Conn over a polling
  HTTP client. Goroutine-driven: Write buffers locally + signals
  the poll loop; Read blocks on inbound buffer; SetReadDeadline
  honored.
- protocol/meek/outbound.go: sing-box adapter. Builds an
  http.Client whose TLS dialer picks a random front from Fronts
  per dial, sets ServerName from FrontSpec.SNI, verifies cert
  chain against VerifyHostname.
- Registered in constant/proxy.go and protocol/register.go.

Tests cover round-trip echo, session-id persistence across writes,
and config validation. Front-list is fed externally — radiance's
fronted/scanner produces the working pool per-(ASN, location, time)
and supplies it to MeekOutboundOptions.Fronts via config.
Adds the server side of the meek-v1 transport: a plain-HTTP
http.Handler that terminates the meek protocol and forwards each
session's bytes to a configured TCP upstream. Deploys behind a CDN
(Akamai DSA, CloudFront alt-domain) that handles TLS termination.

Protocol matches the client in this same package:
- POST /<path> with X-Session-Id: <hex>
- Request body = bytes for upstream
- Response body = up to MaxBodyBytes from upstream
- Per-session state keyed by X-Session-Id; idle sessions reaped

Design:
- One TCP conn per session, dialed lazily on first POST
- Background readPump per session drains upstream into a pending
  buffer; backpressure when buffer exceeds 4x MaxBodyBytes
- ResponseHoldoff (default 50ms) bounds the read window per POST so
  bytes flow back quickly without spinning on empty reads
- Session reaper runs every SessionIdleTimeout/2

cmd/meek-server is a thin main wrapper exposing -listen, -upstream,
-path, -max-body, -holdoff, -idle-timeout, -debug. Includes a
/healthz endpoint that reports SessionCount for monitoring.

Tests cover end-to-end echo (real client + real server + real TCP
echo upstream), 36 KB bidirectional payloads with chunked transfer,
bad-method / missing-session-id rejection, upstream dial failure,
and idle session reap. 10 tests total in protocol/meek, all green.

Deployment: typically runs alongside a sing-box SOCKS5 inbound on
localhost:1080 so the meek tunnel terminates into the existing
proxy backend. CDN-side fronting handles TLS termination plus the
domain-fronting routing (inner Host = the server's CDN hostname).
Captures the verified reference stack (Akamai DSA → Caddy → meek-server
→ microsocks → public internet) and a reproducible test that exercises
the full chain: SOCKS5 handshake + CONNECT + HTTP GET, returning the
origin IP httpbin observed (the Linode's public IP, confirming the
request actually exits via the proxy).

Test currently passes:
  ✅ End-to-end SUCCESS: "origin": "139.162.181.47"
readCond.Wait has no native timeout, so a Read parked there only ever
woke on data arrival, close, or a fresh SetReadDeadline call —
never on the deadline elapsing in real time. Callers setting a
future deadline and waiting for it would hang indefinitely.

Add a time.AfterFunc that broadcasts on readCond at t. Previous
timer is stopped on each SetReadDeadline call (re-arming or
clearing) and on Close. Zero t clears without arming.

Test asserts a SetReadDeadline(now+100ms) followed by a blocking
Read returns errReadDeadline in 50ms–1s.
@myleshorton myleshorton marked this pull request as ready for review May 26, 2026 19:34
Copilot AI review requested due to automatic review settings May 26, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new meek protocol to lantern-box: a Tor-style meek v1 TCP-over-HTTPS transport with a sing-box outbound adapter, plus a reference server/CLI and accompanying unit + smoke tests. This expands domain-fronting from control-plane-only to a potential user data-plane transport.

Changes:

  • Register new outbound type meek (constant.TypeMeek) and expose it via SupportedProtocols().
  • Implement meek client (net.Conn) + sing-box outbound adapter with per-dial random front selection and cert verification hooks.
  • Add a meek server implementation and cmd/meek-server runnable, plus unit tests and an end-to-end smoke test script/docs.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
protocol/register.go Wires the new meek outbound into protocol registration and the supported protocol list.
constant/proxy.go Adds TypeMeek constant.
option/meek.go Introduces MeekOutboundOptions and front selection specs in config schema.
protocol/meek/client.go Implements meek client connection (polling HTTPS POST loop) as net.Conn.
protocol/meek/client_test.go Unit tests for meek client behavior (round trip, session persistence, deadlines, config validation).
protocol/meek/outbound.go sing-box outbound adapter + HTTP client transport that dials via randomly selected fronts.
protocol/meek/server.go Implements meek-v1 server-side handler with session management and upstream relay.
protocol/meek/server_test.go End-to-end unit tests for server behavior (echo, reaping, bad requests).
cmd/meek-server/main.go Adds runnable meek server command with healthz endpoint and flags.
cmd/meek-server/smoketest/socks5.sh Adds manual end-to-end smoke test script for a deployed fronted setup.
cmd/meek-server/smoketest/README.md Documents reference deployment and smoke test usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread protocol/meek/outbound.go Outdated
Comment thread protocol/meek/outbound.go
Comment thread protocol/meek/outbound.go Outdated
Comment thread protocol/meek/client.go
Comment thread protocol/meek/client.go
Comment thread cmd/meek-server/main.go
Comment thread cmd/meek-server/smoketest/socks5.sh
Comment thread option/meek.go Outdated
Comment thread protocol/meek/outbound.go
Comment thread protocol/meek/outbound.go Outdated
outbound.go:
- Reject non-https URL in NewOutbound (http:// would bypass the
  fronted TLS DialTLSContext and the cert pinning, leaking traffic).
- Require each front to set verify_hostname or sni; without either,
  verifyChain runs with an empty DNSName and accepts any trusted cert
  (no real check). Guarded at config time + at dial time.
- Annotate the intentional InsecureSkipVerify with //nolint:gosec and
  a rationale (custom verification via VerifyPeerCertificate).
- Remove unused innerHost field and unused u *url.URL param.

client.go:
- Bound the write backlog (MaxWriteBufBytes, default 1 MiB): Write
  blocks with backpressure instead of buffering without bound, so a
  fast sender on a slow/stalled front can't OOM the process. Wakes on
  drain, close, or write deadline (SetWriteDeadline now arms a timer
  like SetReadDeadline).
- Apply ExtraHeaders before the protocol-critical ones and skip
  reserved headers (Host, Content-Type, X-Session-Id) so config can't
  hijack session keying or framing.

server.go:
- Add optional AuthToken shared secret (X-Meek-Auth, constant-time
  compare). Without it the server is an open relay into Upstream;
  production on a public/fronted hostname MUST set it. Default off
  preserves local tests.
- Replace the readPump sleep-based busy-wait with a sync.Cond
  (drainCond) signaled by takeLocked/close — no more CPU burn / jitter
  under backpressure.

cmd/meek-server: -auth-token flag + an open-relay warning when unset.

option/meek.go: URL example uses meek.dsa.akamai.getiantem.org, not
api.iantem.io.

smoketest/socks5.sh: per-run mktemp -d instead of fixed /tmp paths
(collision/symlink safety).

Tests: auth required (403 without/with wrong token), reserved headers
not overridable, NewOutbound rejects http scheme + identity-less front.
meek originated in Tor's PT framework but is architecturally distinct
from Tor pluggable transports; the polling-over-HTTPS scheme here is
the one Psiphon and Lantern use in practice. Reword the package doc to
say meek-v1 rather than implying a Tor-PT lineage.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Comment thread protocol/meek/server.go
Comment thread protocol/meek/client.go Outdated
Comment thread protocol/meek/server.go Outdated
Comment thread protocol/meek/outbound.go Outdated
myleshorton and others added 7 commits May 26, 2026 16:13
…orrectness

- outbound: DialContext now performs a SOCKS5 CONNECT to destination over
  the tunnel before returning the conn. sing-box treats meek as a terminal
  outbound and writes the application stream directly; without the CONNECT
  the SOCKS5 upstream (microsocks) reads the app's first bytes as a
  malformed handshake and routing fails.
- server: reject POST bodies larger than MaxBodyBytes with 413 instead of
  silently truncating and forwarding a corrupted prefix upstream.
- server: readPump only blocks when pending is non-empty, so a single
  upstream read larger than the cap (possible when MaxBodyBytes*4 < 32 KiB)
  can't wedge the pump waiting for room that never frees.
- client: Write appends in remaining-capacity chunks with backpressure so
  one large slice can't grow writeBuf past MaxWriteBufBytes.

Tests: SOCKS5-connect-over-tunnel chain, oversized-body 413, small-cap
delivery (deadlock regression), and large-write backlog cap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…c it)

DialContext used socks.ClientHandshake5, which reads the SOCKS5 replies
byte-at-a-time via varbin's stub ReadByte — that issues a 1-byte Read and
returns b[0] ignoring n. Over the meek polling Conn this desyncs the handshake:
DialContext returns ~instantly with err=nil while microsocks actually replies
05 ff ("no acceptable methods"), and that rejection then leaks into the
application stream, so every transfer stalls (0 bytes / deadline).

Replace it with an explicit no-auth CONNECT that reads with io.ReadFull and
strictly orders method-select -> reply -> CONNECT -> reply (microsocks requires
no pipelining; sing's writers still encode the requests). Reproduced and verified
with radiance cmd/meek-probe / residential-urltest:
  before: DialContext 0ms, app reads 05ff, stalls
  after:  DialContext ~850ms, full HTTP 200 carried; ~226 KB/s direct download

meek package tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9beSsYGzUaBhRK5ULmtGr
Two resilience/throughput improvements on top of the SOCKS5 fix.

Retriable polls (for flaky networks, e.g. RU): each poll carries a monotonic
X-Meek-Seq; the client holds the in-flight chunk until the poll succeeds and
retries the same seq+body on failure (linear backoff, MaxPollRetries). The
server tracks lastSeq + buffers lastResp per session and *replays* the response
for a repeated seq instead of re-writing upstream / re-draining downstream — so a
lost request or lost response can neither duplicate upstream bytes nor drop
downstream ones. Without a seq header the server behaves exactly as before
(backward compatible).

Larger negotiated poll body: throughput is bytes-per-poll ÷ RTT, so the 64 KiB
cap was the limiter. Default raised to 256 KiB. The client advertises its read
size via X-Meek-Max-Body and the server caps responses at min(its max, advertised),
defaulting to 64 KiB for clients that don't advertise — so a bigger server never
truncates an older client. (Deploy server before clients: it accepts larger
uploads either way.)

Verified with new local end-to-end fault-injection tests (real 2 MiB payloads,
every 4th response dropped *after* the server processed it):
  - TestMeekRetryDownloadIntegrity: stream reassembles byte-for-byte (no gap)
  - TestMeekRetryUploadNoDuplication: upstream receives exactly 2 MiB (no dup)
Full package tests pass with -race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9beSsYGzUaBhRK5ULmtGr
The repo had no automation for updating the fronted meek-server, so the deploy
was manual + undocumented. deploy.sh builds linux/amd64 from the checkout, ships
it, verifies the transfer by sha256, swaps it in atomically (timestamped
backup), restarts the service, and verifies /healthz — rolling back to the
backup if it doesn't come back. Then runs the SOCKS5 smoke test (best-effort;
notes that a failure is usually httpbin being down, not the deploy).

Host/service layout isn't pinned in the repo, so it's overridable via env
(MEEK_HOST/SSH_USER/SSH_KEY/REMOTE_BIN/SERVICE/RESTART_CMD/STATUS_CMD/HEALTHZ_URL)
with sensible defaults; --dry-run previews the plan without touching the host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9beSsYGzUaBhRK5ULmtGr
…andshake test

- Format the SOCKS5 CONNECT reply code and ATYP as hex (%#x), matching the
  auth-reply errors and RFC 1928's hex codes.
- Validate the reply RSV byte is 0x00 (RFC 1928); reject otherwise.
- Add a regression test for the fix: socks5ConnectSequenced over a Conn that
  returns one byte per Read (the polling-Conn pattern that desynced sing's
  ReadByte-based handshake) must complete cleanly and emit a correct no-auth
  method-select + CONNECT; plus a non-zero-RSV rejection test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9beSsYGzUaBhRK5ULmtGr
…dening)

client.go: read one byte past the negotiated MaxBodyBytes and fail if the server's
response exceeds it, rather than silently truncating (which would corrupt the
tunneled byte stream) — mirrors the server's request-size check.

deploy.sh:
- Require MEEK_HOST (no default) so an env-less run can't silently deploy to a live
  origin; checked after --help/-h so those still work without it.
- Hash locally with sha256sum when available, falling back to shasum -a 256 (shasum
  isn't on many Linux distros).
- Make StrictHostKeyChecking configurable (MEEK_SSH_STRICT, default accept-new for
  first-deploy convenience to operator-owned infra; set "yes" for strict).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9beSsYGzUaBhRK5ULmtGr
meek: fix SOCKS5 CONNECT over the polling Conn (byte-wise reads desync it)
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a16e7a5a-3a79-4a68-9d5c-a3e8ee1d63b4

📥 Commits

Reviewing files that changed from the base of the PR and between 446085a and 67937cc.

📒 Files selected for processing (6)
  • cmd/meek-server/deploy.sh
  • cmd/meek-server/main.go
  • cmd/meek-server/smoketest/README.md
  • cmd/meek-server/smoketest/socks5.sh
  • protocol/meek/client.go
  • protocol/meek/server.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/meek-server/smoketest/README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • cmd/meek-server/deploy.sh
  • protocol/meek/server.go
  • protocol/meek/client.go
  • cmd/meek-server/main.go

📝 Walkthrough

Walkthrough

Adds a meek protocol stack with server and client implementations, sing-box outbound registration, a meek-server command, and deploy/smoke-test tooling. The PR also introduces meek-specific configuration types, protocol registration wiring, and tests covering tunnel flow, retries, SOCKS5 handshakes, and deploy health checks.

Changes

Meek protocol rollout

Layer / File(s) Summary
Shared protocol types
constant/proxy.go, option/meek.go, protocol/register.go
Adds the meek proxy type, meek outbound config structs, and registration wiring.
Server request checks
protocol/meek/server.go, protocol/meek/server_test.go
Defines server config defaults, request validation, session lookup, session reaping, and tests for method, auth, body-size, dial-failure, and idle-reap handling.
Server session flow
protocol/meek/server.go, protocol/meek/server_test.go
Implements per-session upstream buffering, downstream draining, sequence replay, and end-to-end tunnel tests including SOCKS5 over meek.
Client tunnel and deadlines
protocol/meek/client.go, protocol/meek/client_test.go
Implements meek connection dialing, polling, read/write buffering, deadlines, reserved-header filtering, and basic connection tests.
Retry integrity tests
protocol/meek/retry_integrity_test.go
Adds a faulting transport and end-to-end tests that drop responses and verify upload and download bytes stay intact.
Outbound adapter
protocol/meek/outbound.go, protocol/meek/outbound_test.go
Registers the meek outbound, validates front and HTTPS inputs, builds front-selecting HTTP clients, and exercises SOCKS5 CONNECT behavior against bytewise and invalid replies.
meek-server command
cmd/meek-server/main.go
Adds the meek-server CLI, HTTP mux wiring, health endpoint, logging, signal handling, and graceful shutdown.
Deploy and smoke-test tooling
cmd/meek-server/deploy.sh, cmd/meek-server/smoketest/README.md, cmd/meek-server/smoketest/socks5.sh
Adds the deploy script, smoke-test README, and SOCKS5 smoke-test script for build, upload, verification, rollback, and health checking.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related issues

  • getlantern/engineering#3526 — The PR adds the meek server endpoint and sing-box outbound transport described in the issue.

Poem

🐰 I hopped through tunnels, meek and bright,
With SOCKS5 stars and healthz light.
The server hummed, the retries spun,
And bytes came back, job well begun.
Now carrots cheer — deploy complete!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the new meek transport, though it omits the matching server and tooling additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fisk/meek-outbound

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Comment thread protocol/meek/server.go
Comment thread protocol/meek/client.go
Comment thread cmd/meek-server/main.go Outdated
Comment thread option/meek.go
Comment thread protocol/meek/server.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/meek-server/deploy.sh`:
- Line 31: The rollback health check is hardcoded to a production health URL, so
staging/canary deployments can be validated against the wrong target. Update the
deploy flow around HEALTHZ_URL and the rollback check to derive the health
endpoint from the selected target (for example, based on MEEK_HOST or an
explicit per-environment health URL) instead of defaulting to
https://meek.getiantem.org/healthz. Make sure the health check logic used later
in deploy.sh and the rollback gate both use the same target-specific URL.

In `@cmd/meek-server/main.go`:
- Around line 82-86: The http.Server setup in main should be extended beyond
ReadHeaderTimeout to also bound request body reads, response writes, and idle
keep-alive time. Update the server initialization in main.go to include
appropriate BodyTimeout, WriteTimeout, and IdleTimeout values alongside the
existing Addr, Handler, and ReadHeaderTimeout settings so the server does not
spend unbounded time on slow bodies, slow responses, or lingering connections.

In `@cmd/meek-server/smoketest/README.md`:
- Around line 9-27: Add explicit language tags to the fenced code blocks in the
meek-server smoketest README to satisfy markdownlint. Update the topology
diagram fence to use text and the sample output fence to use console, and apply
the same fenced-block language labeling to the other referenced block in the
document so all unlabeled fences are fixed.

In `@cmd/meek-server/smoketest/socks5.sh`:
- Around line 55-75: The polling loop in send_and_drain stops based on an
arbitrary byte threshold, which can end phase 3 before the full HTTP response or
success marker is received. Update the logic that consumes meek_post output so
it keeps polling until the expected success marker (for example the origin
marker) is present, or until the response is clearly complete, rather than
relying on the current min-bytes check. Apply the same change anywhere phase 3
uses this helper so the test does not fail on split headers/body across multiple
POSTs.
- Around line 33-52: Support authenticated meek deployments in the smoke test by
updating the meek_post helper to send the required X-Meek-Auth header when an
auth token is configured, so it can exercise the same POST path accepted by
protocol/meek/server.go and cmd/meek-server/main.go with -auth-token. Locate the
request construction in meek_post and make the header conditional on the test
setup/configuration so both authenticated and unauthenticated cases remain
covered.

In `@protocol/meek/client.go`:
- Around line 383-405: The request in roundtrip currently ignores the configured
ReadTimeout because it uses c.ctx directly when building the POST request.
Update the request flow in protocol/meek/client.go around the
http.NewRequestWithContext and c.cfg.HTTPClient.Do path to create a per-request
context with context.WithTimeout using c.cfg.ReadTimeout, and pass that timed
context into the request so hung polls terminate and retries can proceed even
when the provided HTTPClient has no timeout.

In `@protocol/meek/outbound.go`:
- Around line 112-121: NewOutbound currently reuses a single HTTP
client/transport through Config.HTTPClient, which lets idle connections persist
across DialContext calls and bypass pickFront for later requests. Update the
outbound flow so the transport/client is created per meek session or per
DialContext in the relevant outbound constructor and dialing path, and ensure
connection pooling is isolated to one front/session rather than shared globally.

In `@protocol/meek/server.go`:
- Around line 140-160: The request body is validated only after
getOrCreateSession in meek/server.go, which allows malformed or oversized POSTs
to open upstream connections and create live sessions unnecessarily. Reorder the
handling in the request path so the body is read and checked against
MaxBodyBytes before calling getOrCreateSession, then only proceed to session
creation and forwarding when the body is valid; use the existing
getOrCreateSession and session-handling flow in server.go as the place to
refactor.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ffb7e2ba-634f-4b62-af52-d54d098920c2

📥 Commits

Reviewing files that changed from the base of the PR and between 9f277ec and 263f4b5.

📒 Files selected for processing (14)
  • cmd/meek-server/deploy.sh
  • cmd/meek-server/main.go
  • cmd/meek-server/smoketest/README.md
  • cmd/meek-server/smoketest/socks5.sh
  • constant/proxy.go
  • option/meek.go
  • protocol/meek/client.go
  • protocol/meek/client_test.go
  • protocol/meek/outbound.go
  • protocol/meek/outbound_test.go
  • protocol/meek/retry_integrity_test.go
  • protocol/meek/server.go
  • protocol/meek/server_test.go
  • protocol/register.go

Comment thread cmd/meek-server/deploy.sh Outdated
Comment thread cmd/meek-server/main.go
Comment thread cmd/meek-server/smoketest/README.md Outdated
Comment thread cmd/meek-server/smoketest/socks5.sh
Comment thread cmd/meek-server/smoketest/socks5.sh Outdated
Comment thread protocol/meek/client.go Outdated
Comment thread protocol/meek/outbound.go
Comment thread protocol/meek/server.go Outdated
myleshorton and others added 2 commits June 26, 2026 08:40
…lines, doc/default consistency)

Correctness:
- server: when the upstream closes with nothing buffered, end the session (410
  Gone) instead of returning empty 200s forever; the client maps that 410 to
  io.EOF so a read-only caller tears down cleanly (new upstreamFinished() +
  errUpstreamClosed; ServeHTTP returns 410). Regression test
  TestServer_PropagatesUpstreamEOF.
- client: non-200 responses are now permanent (no retry) — the server drops the
  session on every error path, so retrying just resurrects a fresh one instead of
  surfacing end-of-stream. Read/write deadline errors implement net.Error with
  Timeout()==true (net.Conn contract). Test TestConn_DeadlineErrorsAreNetTimeouts.

Docs/defaults (MaxBodyBytes is 256 KiB everywhere, was documented as 64 KiB):
- option/meek.go + server.go ServerConfig comments updated (256 KiB; caps request
  + response bodies).
- cmd/meek-server default max-body 64 KiB -> 256 KiB to match the client default
  (a default client could otherwise hit 413s on chunks 64-256 KiB).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9beSsYGzUaBhRK5ULmtGr
…ing, smoke-test + deploy fixes)

- client: bound each poll by ReadTimeout (context.WithTimeout) so a hung request
  can't block the poll loop forever when the caller's HTTPClient has no timeout.
- server: read + size-check the request body BEFORE creating/dialing a session,
  so spammed unique X-Session-Ids with oversized bodies can't open upstream
  connections and burn sockets until idle reap.
- cmd/meek-server: add Read/Write/Idle timeouts to the http.Server (generous vs
  meek's poll model) to bound slow/abusive clients.
- deploy.sh: don't default MEEK_HEALTHZ_URL to a prod URL (would gate rollback on
  the wrong host for staging/canary); skip the HTTP health check when unset.
- smoketest/socks5.sh: poll phase 3 until the success marker appears (the response
  can span multiple polls — a fixed byte count caused false failures); add optional
  X-Meek-Auth (MEEK_AUTH_TOKEN) to validate hardened deployments.
- smoketest/README.md: label fenced code blocks (markdownlint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9beSsYGzUaBhRK5ULmtGr
@myleshorton myleshorton merged commit d2f270e into main Jun 26, 2026
4 checks passed
@myleshorton myleshorton deleted the fisk/meek-outbound branch June 26, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants