Skip to content

e2e: live-machine Unbounded test on a DO droplet#242

Merged
myleshorton merged 6 commits into
mainfrom
fisk/unbounded-live-e2e
May 9, 2026
Merged

e2e: live-machine Unbounded test on a DO droplet#242
myleshorton merged 6 commits into
mainfrom
fisk/unbounded-live-e2e

Conversation

@myleshorton
Copy link
Copy Markdown
Contributor

Summary

Closes getlantern/engineering#3233.

Adds a live-network e2e test for Unbounded, stacked on top of lantern-box#241 (target: `fisk/unbounded-outbound-v2`). Same DigitalOcean-droplet pattern the existing samizdat/algeneva/water/reflex tests use — this adds a fifth protocol.

What's new

`test/e2e/unbounded-rig/`

Small Go binary that runs three things in one process:

  • freddie (signaling, TLS on :9000)
  • broflake egress (SOCKS5 + QUIC-over-WebSocket on :8000; inner QUIC handshake is self-signed, outer WS is plain)
  • broflake widget (native, `ClientType=widget`, connects to the local freddie + egress)

One process so the droplet only runs one extra daemon. All three components are reused via direct imports from `github.com/getlantern/broflake`.

`.github/workflows/e2e.yaml`

  • New `Build unbounded-rig` step
  • Existing `Deploy to droplet` step uploads the rig binary + a fresh self-signed cert, starts the rig alongside the existing sing-box protocol servers, extends the readiness-gate loop to include ports 9000 and 8000
  • New `Test Unbounded` step matches the pattern of the existing protocol tests: builds a client config pointing at `https://DROPLET_IP:9000` (freddie) and `ws://DROPLET_IP:8000` (egress), spawns `lantern-box run`, then curls HTTP + HTTPS through the local SOCKS5 mixed inbound and asserts `Example Domain` is in the response body

New option flag

`UnboundedOutboundOptions.InsecureDoNotVerifyDiscoveryCert` — test-only escape hatch for skipping TLS verification of freddie's self-signed cert. Parallels the existing `InsecureDoNotVerifyClientCert`. No effect in production (where the `signalingClient` prefers the direct-transport RoundTripper injected via context).

Revert: broflake pin back to `fisk/pion-v4.2`

This branch was cut from the PR #241 branch, which had pinned broflake to `fisk/quic-go-v0.59`. That pin forces `quic-go v0.59` + `qpack v0.6.0`, which is incompatible with `sagernet/quic-go@v0.52.0-sing-box-mod.3`'s older `qpack v0.5.1` API (DecodeFull removed, NewDecoder signature changed). Result: lantern-box's production binary can't build with `-tags with_quic`, which CI's `Build lantern-box` step does.

Bumping `sagernet/sing-quic` cascades further (needs newer `sing/common/tls.Config`), putting that change outside this PR's scope.

Pulling back to `fisk/pion-v4.2` — the pion bump lands cleanly and the quic-go bump (getlantern/unbounded#352) can be resurrected once sing-box-minimal's deps are ready for it.

What the live e2e catches that the in-process test cannot

  • Real TLS handshakes on both freddie and egress — self-signed + the test-only skip-verify flags exercise the real verification code paths
  • Real public STUN (`stun:stun.l.google.com:19302`), real ICE candidate gathering over the droplet's public interface
  • Real quic-go behavior at internet MTU / latency vs loopback
  • Two distinct pion stacks handshaking with each other — exercises the `pion/transport/v4` + `pion/webrtc v4.2.11` bump against a separate process's pion

Test plan

  • `go build ./...` passes
  • `go test -tags test ./test/e2e/...` (existing in-process TestUnboundedE2E) still green — 5.99s
  • Verified locally: rig + lantern-box client + curl HTTP + HTTPS all working on loopback
  • Verify on the actual DO droplet once this PR runs CI

🤖 Generated with Claude Code

Adds a Go binary at test/e2e/unbounded-rig/ that runs freddie + egress +
a native broflake widget in one process, plus a new Test Unbounded step
in .github/workflows/e2e.yaml that:

  1. Builds the rig
  2. Uploads it to the existing throwaway DigitalOcean droplet
  3. Starts it alongside the four sing-box protocol servers
  4. Runs a lantern-box client with an unbounded outbound and curls
     through it for both HTTP and HTTPS

This validates what the in-process TestUnboundedE2E cannot:
  - real TLS handshakes (freddie TLS + inner QUIC TLS)
  - real public STUN + ICE over a public network interface
  - real quic-go behavior at internet MTU/latency
  - pion/transport/v4 + pion/dtls running in two distinct processes

Also adds a test-only escape hatch to UnboundedOutboundOptions:
InsecureDoNotVerifyDiscoveryCert. Parallels the existing
InsecureDoNotVerifyClientCert. Production freddie always presents a
real cert, so this flag is a no-op there.

Fork/prod behavior preserved: in production the signalingClient prefers
the direct-transport RoundTripper injected via the context (radiance's
kindling), which carries its own verification policy. The
insecureSkipDiscoveryVerify arg is only consulted on the fallback path.

Reverts this branch's broflake pin from fisk/quic-go-v0.59 back to
fisk/pion-v4.2 — the v0.59 bump has a transitive conflict with
sing-box-minimal's sagernet/quic-go fork (qpack API break) that needs
to be resolved separately.

Refs getlantern/engineering#3233

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 17, 2026 23:35
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 live-network end-to-end test for the Unbounded protocol by deploying a single “unbounded-rig” binary (freddie + egress + widget) to the existing DigitalOcean droplet used by other protocol e2e tests, and wires a test-only TLS-verification escape hatch for freddie’s self-signed cert.

Changes:

  • Add test/e2e/unbounded-rig Go binary to run freddie + broflake egress + widget in one process on the droplet.
  • Extend .github/workflows/e2e.yaml to build/deploy the rig, generate a self-signed cert, gate on ports 9000/8000, and run HTTP/HTTPS curl assertions through the Unbounded outbound.
  • Add InsecureDoNotVerifyDiscoveryCert option and plumb it into the Unbounded outbound’s signaling HTTP client.

Reviewed changes

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

Show a summary per file
File Description
test/e2e/unbounded-rig/main.go New droplet-side rig binary providing the Unbounded server topology for live e2e.
protocol/unbounded/outbound.go Plumbs new discovery TLS skip-verify option into signaling HTTP client.
protocol/unbounded/outbound_test.go Updates signaling client unit tests for new function signature.
option/unbounded.go Adds insecure_do_not_verify_discovery_cert JSON option.
.github/workflows/e2e.yaml Builds/deploys rig + cert and adds a Unbounded live e2e step.
go.mod / go.sum Updates broflake pin and reconciles indirect deps (notably QUIC/qpack versions).

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

Comment thread test/e2e/unbounded-rig/main.go Outdated
Comment thread protocol/unbounded/outbound.go
Comment thread test/e2e/unbounded-rig/main.go
Comment thread test/e2e/unbounded-rig/main.go Outdated
myleshorton and others added 2 commits April 17, 2026 17:46
…hatches

- waitForFreddie now log.Fatalfs when freddie doesn't come up within
  the timeout. Previously it logged and returned, letting the widget
  spin against a dead freddie and only surfacing the problem when the
  client test finally timed out.
- All three InsecureSkipVerify sites (widget HTTP client, readiness
  poll, signalingClient fallback) carry an inline rationale and a
  //nolint:gosec suppression so future security lints don't flag them
  and reviewers immediately see they're intentional test/dev escape
  hatches, not general-purpose TLS relaxations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stacked PRs against feature branches (the Unbounded outbound stack
being the current motivator) should get live-machine coverage before
merging into main. The path filter still gates the workflow so doc-only
PRs don't burn droplets.

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

Heads up @noahlevenson this integrates a full e2e test of unbounded with freddie, the egress server, and peer running on a live droplet. It runs against unbounded that's updated to the latest pion and uses covert-dtls to combat dtls blocking in Russia.

fisk/pion-v4.2 and fisk/covert-dtls were siblings off main, so the
previous pin to pion-v4.2 left the live e2e blind to the covert-dtls
ClientHello hooks (the whole reason for the rig in the first place).
The new branch merges both so the rig's widget now emits randomized /
mimicked browser ClientHellos instead of the default pion fingerprint.

This is what the Russian DPI would see in production; the live e2e now
actually validates that the widget-side DTLS handshake succeeds through
that hook path.

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

Heads up @noahlevenson this integrates a full e2e test of unbounded with freddie, the egress server, and peer running on a live droplet. It runs against unbounded that's updated to the latest pion and uses covert-dtls to combat dtls blocking in Russia.

pretty cool!

Base automatically changed from fisk/unbounded-outbound-v2 to main April 21, 2026 16:18
Adam Fisk and others added 2 commits May 9, 2026 05:09
… fisk/unbounded-live-e2e

# Conflicts:
#	go.mod
#	go.sum
Today's run (#25599661331) failed Reflex + Unbounded with DNS lookup
timeouts on the droplet, *after* the earlier ALGeneva/Samizdat/WATER
servers had successfully resolved example.com against the droplet's
default systemd-resolved → DO infrastructure path. The artifact
download confirms the bisect:

  algeneva-server.log  +9s   dns: exchanged example.com NOERROR (50ms)
  reflex-server.log    +29s  dns: lookup failed for example.com:
                              (exchange6: context deadline exceeded |
                               exchange4: context deadline exceeded)

The unbounded-rig.log also shows STUN candidate gather failing because
stun.l.google.com couldn't resolve, which is why the consumer/widget
pairing never completes for the Unbounded test downstream.

Pin /etc/resolv.conf to 1.1.1.1 + 8.8.8.8 directly and disable
systemd-resolved so it can't reclaim resolv.conf on a service restart.
Add a `getent hosts example.com` sanity check so a future DNS flake
fails fast at setup instead of mid-test, where it's harder to
diagnose. options timeout:2 attempts:2 keeps lookups snappy if either
resolver is briefly unhappy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@myleshorton myleshorton merged commit e93d8f3 into main May 9, 2026
2 checks passed
@myleshorton myleshorton deleted the fisk/unbounded-live-e2e branch May 9, 2026 13:43
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.

3 participants