Skip to content

Add lanturn outbound type#257

Open
myleshorton wants to merge 3 commits into
mainfrom
fisk/lanturn-integration
Open

Add lanturn outbound type#257
myleshorton wants to merge 3 commits into
mainfrom
fisk/lanturn-integration

Conversation

@myleshorton
Copy link
Copy Markdown
Contributor

@myleshorton myleshorton commented May 8, 2026

Summary

Adds lanturn as a sing-box outbound type — a TURN-as-cover circumvention transport that mimics WebRTC TURN-relayed media flow on plain UDP/3478 with self-hosted coturn on Lantern's international VPS fleet.

Full protocol design + validation-spike sequence (Phases 0-5): getlantern/lanturn. Integration guide: docs/INTEGRATION.md. Design draft v0.2 is in circumvention-corpus-private (private).

Why a new transport: existing Lantern transports (samizdat / reflex / tlsmasq) all live in the TLS-at-byte-0 wire-shape envelope — the most-attacked surface in 2026 (SNI extraction, JA3/JA4 fingerprinting, fully-encrypted-traffic detection). lanturn moves to a wire-distinct shape (STUN magic cookie at offset 4 + ChannelData on UDP/3478) without losing collateral-freedom protection — wholesale-blocking UDP/3478 breaks Twilio Video, Cloudflare Calls, Microsoft Teams' relay fallback, every video-conferencing vendor's hostile-NAT path.

⚠ v0.1 alpha status — not yet operational

The outbound registers cleanly and validates options at config-time, but DialContext returns a clear error rather than a tunnel. This is deliberate: the destination-forwarding handshake between the lanturn client and the egress (planned as SOCKS5 CONNECT over the lanturn conn) is not yet implemented in pkg/lanturn. Without it, dialed bytes would silently forward to a hardcoded egress destination instead of the caller's actual destination — a correctness bug. Failing fast avoids that misrouting.

What this PR's outbound DOES do today:

  • Registers lanturn as a sing-box outbound type
  • Validates required option fields (coturn_endpoints, peer_addr, lanturn_auth_secret) at config-time
  • Advertises TCP-only at the routing layer (no UDP — ListenPacket is unimplemented)
  • Builds an upstream.ClientConfig against pkg/lanturn
  • Returns a clear "not yet implemented" error from DialContext instead of a misroutable net.Conn

What's deferred to follow-up PRs (working code lives in getlantern/lanturn/cmd/lanturn-phase{2,3,4}/main.go):

  • Destination forwarding via SOCKS5 CONNECT over lanturn (the load-bearing piece)
  • covert-dtls fingerprint randomization for the inner DTLS handshake (deploy-blocking for Russia / China per design §4.4 + §11.2)
  • Session rotation across the SessionDuration / IdleGap pattern
  • TURNS-on-5349 fallback (TLS-wrapped TURN over TCP)
  • Multi-profile selection (currently Opus-only)
  • Recency-weighted fleet selection

What this adds

  • constant.TypeLanturn = "lanturn"
  • option.LanturnOutboundOptions (trimmed to honored fields only: coturn_endpoints, peer_addr, profile, lanturn_auth_secret) + LanturnCoturnEndpoint
  • protocol/lanturn/ package — outbound implementation + unit tests
  • protocol/register.go registers the outbound + adds "lanturn" to supportedProtocols
  • protocol/register_test.go — parallel TestSupportedProtocolsIncludesLanturn regression test
  • protocol/lanturn/outbound_test.go (new) — 6 tests covering option validation, TCP-only advertisement, fail-fast DialContext, UDP rejection
  • README section: protocol overview, alpha callout, jurisdiction guidance, config schema (trimmed to honored fields), wired-vs-deferred matrix

Per-jurisdiction guidance (per lanturn design §11)

  • Iran: rollout target add mutable ruleset and ruleset manager #1 once destination forwarding lands — highest international-WebRTC dependency, weakest DPI, diaspora-run Jitsi/Matrix on the same European VPS providers as the lanturn fleet
  • Russia: rollout target Adding WATER pluggable transport #2 once covert-dtls is wired in — TSPU March 2026 pion-DTLS matcher exists but covert-dtls inner layer defeats it
  • China: NOT enabled in v0.1 — most-sophisticated DPI + lowest WebRTC-collateral budget; may need China-specific design pivot

Test plan

  • Builds clean (go build ./...)
  • All 6 new lanturn outbound tests pass (go test ./protocol/lanturn/)
  • All other protocol tests still pass (go test ./protocol/...)
  • CI green (build, e2e)
  • Copilot's review comments addressed (all 8 threads resolved)
  • Integration test against a real coturn instance — blocked on the SOCKS5-CONNECT-over-lanturn implementation in pkg/lanturn
  • Field test in low-volume Iran rollout — blocked on the destination-forwarding wire-up + covert-dtls integration

🤖 Generated with Claude Code

lanturn is a Lantern circumvention transport that mimics WebRTC TURN-
relayed media flow on plain UDP/3478 with self-hosted coturn on
Lantern's international VPS fleet. See https://github.com/getlantern/lanturn
for the protocol details.

What this PR adds:

- constant.TypeLanturn = "lanturn"
- option.LanturnOutboundOptions / LanturnInboundOptions / LanturnCoturnEndpoint
- protocol/lanturn/ package — outbound implementation that wraps
  github.com/getlantern/lanturn/pkg/lanturn.Dial as a sing-box
  outbound. MVP opens a fresh TURN session per DialContext call;
  follow-up will multiplex destinations over a persistent session via
  SOCKS5-over-lanturn (matching the Unbounded pattern).
- protocol/register.go updated to register the outbound + add
  "lanturn" to supportedProtocols
- README.md — Lanturn section with protocol overview, when-to-use
  guidance (Iran primary, Russia experimental, China not v0.1 per
  lanturn design §11), config schema, server config note about the
  separate egress binary

Status: alpha. Working code in github.com/getlantern/lanturn at the
pkg/lanturn package; spike binaries cmd/lanturn-phase{0..4} validate
the wire-format and behavioral mimicry layers separately.

Hard rule recorded in lanturn README + design: the inner DTLS handshake
must use covert-dtls fingerprint randomization in production (the
TSPU March 2026 pion-default-DTLS matcher). MVP outbound passes
fingerprint_mode=mimic by default; do NOT deploy with mode=none to
Russia / China.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 8, 2026 20:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 lanturn outbound type to lantern-box (a TURN-as-cover, WebRTC-shaped transport) and documents its intended deployment/configuration.

Changes:

  • Register lanturn as a supported custom protocol and outbound type.
  • Introduce LanturnOutboundOptions and a new protocol/lanturn outbound implementation backed by github.com/getlantern/lanturn.
  • Extend README with a new Lanturn section including config examples and operational notes.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
README.md Documents Lanturn’s threat model, deployment, and JSON config fields.
protocol/register.go Registers the new lanturn outbound and advertises it in supportedProtocols.
protocol/lanturn/outbound.go Implements the sing-box outbound adapter for Lanturn (MVP per-dial session).
option/lanturn.go Adds JSON option structs for the Lanturn outbound (and placeholder inbound options).
constant/proxy.go Adds TypeLanturn constant.
go.mod Adds the github.com/getlantern/lanturn dependency and bumps the Go toolchain version.
go.sum Records checksums for the new dependency set (incl. lanturn + pion/rtp bump).

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

Comment thread protocol/lanturn/outbound.go Outdated
Comment thread protocol/lanturn/outbound.go
Comment thread protocol/lanturn/outbound.go Outdated
Comment thread protocol/lanturn/outbound.go Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread protocol/lanturn/outbound.go
Comment thread option/lanturn.go Outdated
myleshorton and others added 2 commits May 11, 2026 12:59
Copilot's review on PR #257 flagged that the v0.1 outbound was
advertising config fields and behavior it didn't actually deliver.
Specifically:

1. Package doc claimed "calls lanturn.Dial at Start time to establish a
   persistent connection" — actual impl dials per-DialContext, no Start.
2. LanturnOutboundOptions exposed fingerprint_mode, session_duration_secs,
   idle_gap_*, udp_timeout_ms, prefer_transport — none wired through to
   the upstream pkg/lanturn.ClientConfig MVP. Misleading config schema.
3. Network advertisement included UDP but DialContext rejected it. Could
   cause sing-box to route UDP traffic to a guaranteed-failing outbound.
4. DialContext returned a real net.Conn without conveying the destination
   to the egress — would silently forward bytes to a hardcoded egress
   destination instead of the caller's. Correctness bug.
5. README config example listed unsupported fields.
6. option doc said Credential/FleetSelector/Logger come "via context at
   service startup" — actually hardcoded in NewOutbound.
7. No unit tests for option validation / DialContext behavior.

Fixes:

- option/lanturn.go: trim unsupported fields (fingerprint_mode,
  session_duration_secs, idle_gap_*, udp_timeout_ms, prefer_transport);
  rewrite doc to honestly describe what v0.1 honors. Profile kept but
  documented as "v0.1 falls back to Opus regardless."
- protocol/lanturn/outbound.go:
  - Package doc rewritten to accurately describe per-DialContext sessions
    and the v0.1-alpha "not yet operational" status
  - Network advertisement: TCP-only (was [TCP, UDP])
  - DialContext returns clear "not yet implemented" error with PR URL
    reference instead of returning a misroutable net.Conn
- README.md: scope trim — remove unsupported fields from JSON example and
  options table; add prominent alpha warning callout above config; rewrite
  rollout note to enumerate what's wired vs deferred
- protocol/lanturn/outbound_test.go (new): 6 tests covering required-field
  validation, valid config produces TCP-only adapter, DialContext fails
  fast with the expected message, UDP DialContext rejected with TCP-only
  message
- protocol/register_test.go: parallel TestSupportedProtocolsIncludesLanturn
  matching the Unbounded test (same register-without-slice regression
  hazard)

What's still deferred to follow-up PRs (clearly documented now):
- Destination forwarding via SOCKS5 CONNECT over lanturn — the load-
  bearing missing piece
- Multi-profile selection (vp8/vp9/screen-share)
- covert-dtls fingerprint randomization
- Session rotation, TURNS-on-5349 fallback, recency-weighted fleet

All 6 new lanturn outbound tests pass. Existing tests in
./protocol/... unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pkg/lanturn now takes destination as a Dial parameter and prepends the
sing-box-native M.SocksaddrSerializer wire format to the first inner-
stream bytes — same address-prefix pattern trojan / anytls / vmess /
shadowsocks use. Replaces the planned SOCKS5-CONNECT-over-lanturn
approach (which was overkill since we control both ends of the wire
and don't need SOCKS5's version-negotiation / auth-method ceremony).

This PR's outbound changes:

- DialContext now actually dials. Opens a fresh lanturn session per
  call, passes destination to upstream.Dial, returns the resulting
  net.Conn directly. Removed the "not yet implemented" fail-fast.
- Bumped pkg/lanturn dep to v0.0.0-...-9d796d3 (the destination-
  forwarding commit upstream).
- TestDialContext_AlphaError → TestDialContext_UnreachableCoturn.
  Asserts the dial fails cleanly when coturn is unreachable (127.0.0.1:1)
  with the expected "lanturn dial ..." wrap. End-to-end success path
  tested by test/e2e/lanturn_test.go (Phase-5 follow-up, in-process).
- README: removed "not yet operational" warnings. Rewrote rollout note
  to enumerate what the v0.1 outbound DOES do today (heavy fresh-session
  per dial, no covert-dtls, no profile selection, no rotation, no
  fallback) vs what's still next-milestone work.

All 6 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@myleshorton
Copy link
Copy Markdown
Contributor Author

@myleshorton — picking this back up. The earlier review feedback is all addressed; this is a status snapshot + the test-coverage gap analysis I'd want a second pair of eyes on before merge.

What landed

Commit What it does
1f0c359 Add lanturn outbound type Initial v0.1 scaffolding — constant.TypeLanturn, option.Lanturn*, protocol/lanturn/outbound.go, registration. DialContext was a fail-fast stub.
806d1d1 Address Copilot review on PR #257 Removed misleading option fields (fingerprint_mode, session_duration_secs, idle gaps, udp_timeout_ms, prefer_transport) that the v0.1 outbound silently ignored. Restricted Network() to TCP-only. Pinned each fix with a test. README aligned with what's actually supported.
772ae48 Wire real DialContext via M.SocksaddrSerializer Replaced the fail-fast stub with a real upstream.Dial invocation; pkg/lanturn upstream prepends the sing-box-native M.SocksaddrSerializer wire format so the egress learns the destination. TestDialContext_AlphaErrorTestDialContext_UnreachableCoturn.

CI: e2e + build both green.

Test coverage as it stands

7 tests, all passing, ~0.5s:

  • TestNewOutbound_RequiresCoturnEndpoints / RequiresPeerAddr / RequiresAuthSecret — config-time validation, fails fast with clear error messages
  • TestNewOutbound_ValidConfig — happy-path construction + asserts Network() == ["tcp"] (regression-pins the UDP-advertised-but-unimpl issue Copilot flagged)
  • TestDialContext_UnreachableCoturn — DialContext error wraps with "lanturn dial …" prefix when coturn (127.0.0.1:1) is unreachable
  • TestDialContext_UDPRejected — defense-in-depth UDP path returns clear "TCP only" error
  • TestSupportedProtocolsIncludesLanturn (in protocol/register_test.go) — pins the register-without-slice-add regression class

What's deferred (in #264)

Four follow-up tests not blocking this PR but worth tracking:

  1. test/e2e/lanturn_test.go — the file the 772ae48 commit message references doesn't actually exist. The end-to-end happy path is currently untested; only error paths are. Highest-value follow-up. Wants in-process coturn (pion/turn).
  2. Positive test for destination forwarding via M.SocksaddrSerializer — the load-bearing change in 772ae48 has no direct unit test. If upstream.Dial got accidentally invoked without the destination arg, the missing e2e would be the only thing catching it.
  3. Credential callback shape test — pins that {Username: "", Password: opts.LanturnAuthSecret} is what gets returned.
  4. ListenPacket rejection test — one-liner pin of "lanturn: ListenPacket not supported".

I think shipping the seven existing tests is the right v0.1 call given the Iran context — they cover all the negative contracts you flagged, plus the supportedProtocols registration. The positive-path gap is real but bounded by pkg/lanturn's own coverage of Dial, and the four follow-ups have a home in #264.

Re-requesting review for a final pass + merge. Happy to address anything new you spot.

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