Skip to content

feat: add Device::suggest_exit_node mirroring Go SuggestExitNode#267

Merged
GeiserX merged 1 commit into
mainfrom
feat/suggest-exit-node
Jun 15, 2026
Merged

feat: add Device::suggest_exit_node mirroring Go SuggestExitNode#267
GeiserX merged 1 commit into
mainfrom
feat/suggest-exit-node

Conversation

@GeiserX

@GeiserX GeiserX commented Jun 15, 2026

Copy link
Copy Markdown
Owner

What

Engine ask #24Device::suggest_exit_node(), reproducing Go v1.100.0's suggestExitNodeUsingDERP (the classic DERP-region-latency path) behaviorally. The daemon consumes it as tnet exit-node suggest (bead tsd-jz2).

Algorithm (faithful to Go ipn/ipnlocal/local.go)

A pure suggest_exit_node(report, candidates, prev_suggestion, select_region, select_node) function, with the select_region/select_node closures injected exactly as Go does (so tests pass deterministic stubs; production uses uniform RNG):

  1. No netcheck report / no preferred DERP → ErrNoPreferredDerp ("try again later").
  2. Candidate = valid + reachable (online) + carries the suggest-exit-node node-cap + advertises an exit route. (Self excluded — not a peer.)
  3. 0 candidates → Ok(None) (no suggestion, not an error). 1 candidate → returned directly.
  4. 2+ → partition by home DERP region; pick the region with lowest netcheck latency (tiebreak lowest region ID, mirroring minLatencyDERPRegion); no usable latency → select_region (uniform). Within the region, select_node applies prev-suggestion stickiness (return the prior pick if still a candidate) else uniform random.

Determinism (matches Go exactly)

There is no seed/hash tiebreak. Determinism comes only from (a) lowest-latency-region + lowest-region-ID tiebreak and (b) prev-suggestion stickiness (persisted in a Runtime cell, mirroring Go's lastSuggestedExitNode); the pick among equal candidates is uniform-random, as in Go.

Parity decision (IPv4-only fork)

Go's ContainsExitRoutes requires both 0.0.0.0/0 and ::/0. This fork is IPv4-only, so the candidate predicate accepts a peer advertising 0.0.0.0/0 (family-agnostic prefix_len == 0, matching the existing is_exit_node check) — documented as a deliberate deviation, recorded in DEFERRED-QUESTIONS. Without it the feature would be inert on a v4-only tailnet.

Deferred (Phase 2, documented)

The Mullvad location/geo-distance path (only reached when no candidate has a home DERP) and the traffic-steering algorithm (off unless the tailnet enables it). ExitNodeSuggestion ships {id, name}; Location is deferred with the geo path.

Tests

17 tests ported from Go's local_test.go table cases via the pure fn + deterministic stubs: no-report error, 0/1/2+ candidates, lower-latency-region wins, region-ID tiebreak, no-latency→select_region fallback, prev-suggestion stickiness, and the predicate exclusions (no cap / no exit route / offline).

Gates (local, all green — re-verified after rebasing onto current main)

cargo test -p geiserx_ts_control -p geiserx_ts_runtime -p geiserx_tailscale (248 + 371 + facade) · clippy -D warnings (3 crates + tun lane) · cargo fmt --check · cargo doc (broken_intra_doc_links=deny) · cargo run -p checks.

Part of the daemon engine-asks batch; lands in the next release. After merge the daemon bumps + adds tnet exit-node suggest.

Signed-off-by: Sergio sergio@geiser.cloud

Created using Claude Code (Opus 4.8)

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@GeiserX, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 2 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fc2b4a6e-d487-4cec-9d90-5368bbce11d5

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc1192 and bcc2780.

📒 Files selected for processing (7)
  • src/error.rs
  • src/lib.rs
  • ts_control/src/lib.rs
  • ts_control_serde/src/lib.rs
  • ts_control_serde/src/service_vip.rs
  • ts_runtime/src/exit_node_suggest.rs
  • ts_runtime/src/lib.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/suggest-exit-node

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Port Go v1.100.0 suggestExitNodeUsingDERP (the classic DERP-region-latency
exit-node suggestion path) as a pure, unit-testable function with the region
and node selectors injected as closures, exactly as Go does, so the algorithm
is deterministic under test while production uses uniform-random selection.

The candidate predicate requires a peer be online, carry the suggest-exit-node
node-capability, and advertise an exit route. Per the IPv4-only fork posture it
accepts a peer advertising 0.0.0.0/0 rather than Go's strict both-v4-and-v6
requirement, matching the existing family-agnostic exit-node check. Among 2+
candidates it partitions by home DERP region, picks the lowest-latency region
with a lowest-region-id tiebreak, and applies prev-suggestion stickiness within
the winning region. Region-less peers fall back without geo weighting; the
Mullvad geo path, traffic-steering path, and Location field are deferred.

Runtime::suggest_exit_node gathers the latest netcheck report and peer set,
runs the pure function, and remembers the result for stickiness. Device exposes
it for the daemon's exit-node suggest command; a no-preferred-DERP precondition
surfaces as a typed error, an empty candidate set as Ok(None).

Signed-off-by: Sergio <sergio1993_1@hotmail.com>
@GeiserX GeiserX force-pushed the feat/suggest-exit-node branch from dd42e1f to bcc2780 Compare June 15, 2026 20:59
@GeiserX GeiserX merged commit 73f56b1 into main Jun 15, 2026
16 checks passed
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.

1 participant