Skip to content

kindling: route smart strategy probes through bypass dialer#485

Merged
myleshorton merged 2 commits into
mainfrom
fisk/kindling-smart-bypass
May 16, 2026
Merged

kindling: route smart strategy probes through bypass dialer#485
myleshorton merged 2 commits into
mainfrom
fisk/kindling-smart-bypass

Conversation

@myleshorton
Copy link
Copy Markdown
Contributor

@myleshorton myleshorton commented May 16, 2026

Summary

The smart strategy was paying for DNS/TLS strategy probes that the existing wrapTransportWithBypass call threw away by overwriting http.Transport.DialContext post-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 added WithSmartDialerConfig so radiance can supply a system-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 bypassed
Loading

After (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)
Loading

The system-resolver constraint that made this two-piece is enforced at outline-sdk/x/smart/stream_dialer.go:350:

if resolver == nil {  // resolver == nil means "system: {}" was selected
    if _, ok := f.StreamDialer.(*transport.TCPDialer); !ok {
        return nil, fmt.Errorf("cannot use system resolver with base dialer of type %T", f.StreamDialer)
    }
}

A custom StreamDialer (our bypass FuncStreamDialer) 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 through bypass.StreamDialer() instead.

Changes

  • bypass/streamdialer.go
    • bypass.StreamDialer() exposes 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; falls back to full Close() when the underlying conn doesn't support half-close)
  • kindling/smart/smart_dialer_config.yml
    • Copy of kindling's embedded YAML with system: {} removed; same DoH/DoT resolver list. Embedded as exported DialerConfig for reuse
  • kindling/smart/client.go
    • Replaces wrapTransportWithBypass with NewSmartHTTPTransportWithConfig(logWriter, DialerConfig, bypass.StreamDialer(), nil, domain)
  • kindling/client.go
    • Adds WithStreamDialer(bypass.StreamDialer()) + WithSmartDialerConfig(radiancesmart.DialerConfig) to both the staging and production WithProxyless paths
  • go.mod / go.sum
    • Bump kindling to v0.0.0-20260516120759-a9712f95df03 (squash-merge of adding manual configuration functions #35)
    • Surfaces direct github.com/Jigsaw-Code/outline-sdk dep for the transport.StreamDialer / transport.StreamConn types

Test 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/... passes
  • Smoke against a live VPN session: confirm kindling config fetch traffic doesn't appear in the TUN's PCAP (separate manual verification)

Note: two preexisting failures on main are unrelated to this PR (cmd/lantern/lantern.go:169 build error from a different in-flight change, and TestUserIDFormatMatchesMain tmpdir flake in config/).

🤖 Generated with Claude Code

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.
Copilot AI review requested due to automatic review settings May 16, 2026 12:09
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

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() exposing bypass.DialContext as a transport.StreamDialer, with a halfCloseConn adapter for transport.StreamConn.
  • Replace wrapTransportWithBypass with kindling.NewSmartHTTPTransportWithConfig using a system-DNS-less embedded YAML (DialerConfig) so the bypass dialer is wired into probes end-to-end.
  • Wire WithStreamDialer + WithSmartDialerConfig into both staging and production NewKindling paths; 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.

Comment thread kindling/smart/client.go Outdated
Comment thread bypass/streamdialer.go Outdated
…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.
@myleshorton myleshorton merged commit 5f3ce9d into main May 16, 2026
2 checks passed
@myleshorton myleshorton deleted the fisk/kindling-smart-bypass branch May 16, 2026 12:17
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.
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