kindling: route smart strategy probes through bypass dialer#485
Merged
Conversation
The smart strategy was paying for DNS/TLS strategy probes that the existing wrapTransportWithBypass call then threw away by overwriting http.Transport.DialContext post-construction. After this change the strategy actually uses the bypass dialer, so kindling traffic stays out of the VPN TUN even when the smart path wins the race. Two pieces, matching getlantern/kindling#34 + #35: - bypass.StreamDialer() wraps bypass.DialContext as a transport.StreamDialer for outline-sdk consumers. halfCloseConn adapts net.Conn → transport.StreamConn (the bufferedConn returned on the proxy path doesn't expose CloseRead/CloseWrite via embedding). - kindling/smart/smart_dialer_config.yml is a copy of kindling's embedded YAML with `system: {}` removed. The outline-sdk strategy rejects a non-*transport.TCPDialer base with "cannot use system resolver with base dialer of type %T" (smart/stream_dialer.go:350) whenever the system resolver is selected — semantically correct since system DNS goes through OS routing tables and would loop back through the very TUN we're trying to bypass. Wiring: - kindling/smart/client.go uses NewSmartHTTPTransportWithConfig with the bypass dialer + system-less YAML, replacing the old wrapTransportWithBypass DialContext override. - kindling/client.go's WithProxyless path adds WithStreamDialer + WithSmartDialerConfig for the same reason. Bumps kindling to v0.0.0-20260516120759-a9712f95df03 for the new NewSmartHTTPTransportWithConfig + WithSmartDialerConfig surface.
Contributor
There was a problem hiding this comment.
Pull request overview
Routes kindling smart-strategy probes through radiance's bypass dialer so the strategy's DNS/TLS probing actually avoids the VPN TUN, fixing a bug where wrapTransportWithBypass was overwriting http.Transport.DialContext post-construction and discarding the smart dialer's work.
Changes:
- Add
bypass.StreamDialer()exposingbypass.DialContextas atransport.StreamDialer, with ahalfCloseConnadapter fortransport.StreamConn. - Replace
wrapTransportWithBypasswithkindling.NewSmartHTTPTransportWithConfigusing a system-DNS-less embedded YAML (DialerConfig) so the bypass dialer is wired into probes end-to-end. - Wire
WithStreamDialer+WithSmartDialerConfiginto both staging and productionNewKindlingpaths; bump kindling and surface direct outline-sdk dep.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| bypass/streamdialer.go | New StreamDialer() adapter + halfCloseConn to satisfy outline-sdk's transport.StreamConn interface. |
| kindling/smart/smart_dialer_config.yml | Embedded DoH/DoT-only smart strategy config (system resolver removed). |
| kindling/smart/client.go | Switches to NewSmartHTTPTransportWithConfig with bypass dialer; drops wrapTransportWithBypass hack. |
| kindling/client.go | Plumbs bypass stream dialer + smart dialer config into both kindling constructors. |
| go.mod / go.sum | Bumps kindling; promotes outline-sdk to a direct dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…omments Per AGENTS.md, comments must not reference code locations in other repos (those rot when upstream shifts and belong in commit messages). - DialerConfig doc on kindling/smart/client.go drops the outline-sdk:line reference; states the constraint as a contract. - halfCloseConn doc on bypass/streamdialer.go drops the concrete-type enumeration; documents the half-close contract instead.
2 tasks
myleshorton
added a commit
to getlantern/lantern
that referenced
this pull request
May 16, 2026
Pulls in getlantern/radiance#485, which routes kindling smart-strategy probes through the bypass dialer so kindling traffic doesn't loop back through the VPN TUN. Transitively bumps kindling to a9712f9 (getlantern/kindling#35) for the WithSmartDialerConfig surface.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The smart strategy was paying for DNS/TLS strategy probes that the existing
wrapTransportWithBypasscall threw away by overwritinghttp.Transport.DialContextpost-construction. After this change the strategy actually uses the bypass dialer end-to-end, so kindling traffic stays out of the VPN TUN even when the smart path wins the race.Originated in this Slack thread. Paired with kindling#34 (already merged) which added
NewSmartHTTPTransportWithDialer+WithStreamDialer/WithPacketDialer, and kindling#35 (already merged) which addedWithSmartDialerConfigso radiance can supply asystem-less YAML.Why both pieces are needed
Before (smart strategy compute wasted):
sequenceDiagram autonumber participant R as radiance/smart participant K as kindling participant S as outline-sdk smart R->>K: NewSmartHTTPTransport(domain) K->>S: NewDialer with TCPDialer{} S-->>K: smart dialer (system DNS + chosen TLS strategy) K-->>R: http.Transport Note over R: wrapTransportWithBypass overwrites trans.DialContext Note over R: every request now goes via bypass.DialContext, smart strategy bypassedAfter (smart strategy actually uses bypass end-to-end):
sequenceDiagram autonumber participant R as radiance/smart participant K as kindling participant S as outline-sdk smart participant B as radiance/bypass R->>K: NewSmartHTTPTransportWithConfig(systemLessYAML, bypass.StreamDialer(), domain) K->>S: NewDialer with bypass StreamDialer + DoH-only DNS S->>B: probe DoH resolvers via bypass dialer B-->>S: connections that avoid the VPN TUN S-->>K: smart dialer whose every probe used bypass K-->>R: http.Transport (DialContext goes through bypass + strategy)The system-resolver constraint that made this two-piece is enforced at
outline-sdk/x/smart/stream_dialer.go:350:A custom
StreamDialer(our bypassFuncStreamDialer) is incompatible with system DNS — semantically correct, since system DNS uses OS routing tables and would loop back through the VPN TUN we're trying to bypass. The system-less YAML uses DoH entries that get dialed throughbypass.StreamDialer()instead.Changes
bypass/streamdialer.gobypass.StreamDialer()exposesbypass.DialContextas atransport.StreamDialerfor outline-sdk consumershalfCloseConnadaptsnet.Conn → transport.StreamConn(thebufferedConnreturned on the proxy path doesn't exposeCloseRead/CloseWritevia embedding; falls back to fullClose()when the underlying conn doesn't support half-close)kindling/smart/smart_dialer_config.ymlsystem: {}removed; same DoH/DoT resolver list. Embedded as exportedDialerConfigfor reusekindling/smart/client.gowrapTransportWithBypasswithNewSmartHTTPTransportWithConfig(logWriter, DialerConfig, bypass.StreamDialer(), nil, domain)kindling/client.goWithStreamDialer(bypass.StreamDialer())+WithSmartDialerConfig(radiancesmart.DialerConfig)to both the staging and productionWithProxylesspathsgo.mod/go.sumv0.0.0-20260516120759-a9712f95df03(squash-merge of adding manual configuration functions #35)github.com/Jigsaw-Code/outline-sdkdep for thetransport.StreamDialer/transport.StreamConntypesTest plan
go test ./kindling/...—TestNewClient/{smart,domainfront,amp}all pass on this branch (smart/domainfront were failing on prior bypass-only attempts that didn't include the system-less YAML)go test ./bypass/...passesNote: two preexisting failures on
mainare unrelated to this PR (cmd/lantern/lantern.go:169build error from a different in-flight change, andTestUserIDFormatMatchesMaintmpdir flake inconfig/).🤖 Generated with Claude Code