* docs: add best exit selection design * feat: add best exit selection
8.2 KiB
Best Exit Selection Design
Goal
Add a multi-exit tunnel strategy named best that always sends new connections through the currently best-quality exit. The feature should prevent traffic from continuing to use an exit whose latency or packet loss has degraded while the exit is still technically online.
Existing connections must not be interrupted. Switching affects only new connections created after the runtime chain update is applied.
Current Context
- Tunnel forwarding stores entry, chain, and exit nodes in
chain_tunnel. - Multi-exit runtime chains are currently rendered as one GOST hop with multiple nodes.
- GOST selectors support
fifo,round,rand, andhash, plus fail filtering throughmaxFailsandfailTimeout. - The current fail filter only reacts to dial, handshake, or transport failures. It does not react to high latency when the exit is still reachable.
tunnel_quality_proberalready runs panel-side TCP probes and stores tunnel quality history, but it currently probes representative nodes and does not drive runtime routing decisions.
User Decisions
- Add a
bestoption for multi-exit tunnels. bestmeans always choose the current best exit for new connections.- Score exits by end-to-end quality.
- Keep the existing public probe target:
www.bing.com:443. - Do not disrupt established connections.
Approach
Implement best as a panel-driven control-plane strategy.
The database stores the user's intended strategy as best. When the panel renders runtime GOST config for a best exit group, it sends a GOST selector strategy of fifo. The panel dynamically sorts the candidate exits so the current best exit is first. GOST then chooses the first node for new connections.
This avoids adding active probing logic inside every GOST agent and reuses the existing panel-to-agent command path.
Components
Frontend
The tunnel form adds 最优 to the multi-exit load strategy selector.
- Label:
最优 - Value:
best - Scope: tunnel forwarding exit groups, alongside
主备/fifo,轮询/round, and随机/rand - Create and edit forms must submit and restore
bestunchanged.
Backend Data Model
No schema change is required.
The existing chain_tunnel.strategy column stores best. Repository and handler paths should preserve the value in API responses and updates.
Runtime Chain Rendering
When building runtime chain config:
- If the configured strategy is not
best, keep existing behavior. - If the configured strategy is
best, emit GOST selector strategyfifo. - Sort the target nodes using the panel's latest best-exit decision before rendering the node list.
- If no quality decision exists yet, keep the saved node order.
This preserves the user's best intent in storage while using a GOST selector that can execute the panel's sorted decision.
Quality Prober
Extend tunnel_quality_prober to evaluate all candidates in best exit groups.
For each chain owner node and candidate exit, measure:
- Chain owner node to candidate exit using TCP ping.
- Candidate exit to
www.bing.com:443using TCP ping.
For direct entry-to-exit tunnels, each entry node owns its own chain decision. For tunnels with intermediate chain hops, each node in the last hop group before the exits owns its own chain decision. This allows different entry or chain nodes to choose different best exits when their path quality differs.
Scoring
Each exit candidate gets an end-to-end score for a specific chain owner node.
- Total latency is the sum of owner-to-exit latency and exit-to-Bing latency.
- Total loss combines both legs by success probability:
1 - (1 - lossA) * (1 - lossB). - Failed or unreachable candidates are sorted behind successful candidates.
- The score should heavily penalize packet loss so that low-latency but lossy exits are not selected over stable exits.
A practical scoring formula can be:
score = totalLatencyMs + (totalLossPercent * lossPenaltyMsPerPercent)
Use lossPenaltyMsPerPercent = 100 initially. For example, 5% loss adds 500ms to the score.
Switching Rules
The panel should not update chains on every probe round.
Switch only when all conditions are true:
- The candidate best exit is different from the currently applied first exit.
- The candidate is successful.
- The candidate remains best for consecutive probe rounds.
- The candidate beats the current exit by a minimum advantage threshold.
- The chain owner node has passed a minimum switch cooldown.
Initial constants:
- Consecutive confirmations: 3 rounds.
- Switch cooldown: 30 seconds per chain owner node.
- Minimum advantage: the candidate score must improve by at least
max(20ms, currentScore * 0.15).
If all exits fail, keep the current runtime order and do not issue a destructive update.
Runtime Update
When a best chain owner node changes best exit:
- Rebuild that node's
chains_<tunnelID>payload with the best exit first and remaining candidates sorted by quality for that node. - Send
UpdateChainsto that chain owner node. - Do not restart or update tunnel services.
- Record success or failure in logs and in the in-memory decision state.
This affects only future connections. Existing TCP connections keep using the net.Conn created before the update and continue through their original exit.
Agent Safety Improvement
The current agent UpdateChains path unregisters the old chain before registering the new chain. This does not kill existing connections, but it creates a small window where a new connection can fail because the chain name is temporarily absent.
Improve the update path so it parses the new chain first and only replaces the registered chain after parsing succeeds. The replacement window should be as small as possible. If parsing fails, the old chain must remain active.
Error Handling
- If probing one candidate fails, continue scoring other candidates.
- If a chain owner node is offline or times out, skip decisions for that owner during the round instead of marking every candidate failed.
- If a candidate has no successful required probe data, mark it failed for that round.
- If
UpdateChainsfails, keep the current applied order and retry on a later round. - If the tunnel has one exit or an incomplete config,
bestbehaves like the saved order and does not trigger dynamic switching. - If
monitor_tunnel_quality_enabled=false, dynamicbestswitching pauses. The last applied runtime order remains in effect.
Observability
The prober should maintain in-memory decision state per best tunnel and chain owner node.
Useful fields:
- Tunnel ID and chain owner node ID.
- Current applied best exit node ID.
- Candidate best exit node ID.
- Candidate scores.
- Last switch timestamp.
- Last switch result.
- Reason for not switching, such as cooldown, insufficient advantage, candidate unstable, or all exits failed.
Initial UI scope is limited to supporting create, update, and display of the best strategy. A later enhancement can expose current best exit and candidate scores in the tunnel monitor view.
Testing
Backend tests:
- Score calculation orders candidates by latency and packet loss.
- Packet loss penalty prevents lossy exits from winning only because latency is low.
- All-failed candidates do not trigger a switch.
- Consecutive confirmation and cooldown prevent flapping.
strategy=bestpersists inchain_tunnel.strategyand is returned by tunnel list/get APIs.- Runtime rendering maps
bestto GOSTfifoand places the chosen best exit first.
Agent tests:
UpdateChainsparse failure keeps the old chain registered.- Successful
UpdateChainsupdates the chain used by new connections.
Frontend verification:
- Tunnel form includes
最优in the exit strategy selector. - Existing tunnels with
strategy=bestrender correctly. - Create and update requests submit
bestunchanged.
Verification commands:
(cd go-backend && go test ./...)
(cd go-gost && go test ./...)
(cd vite-frontend && pnpm run build)
Non-Goals
- Do not move existing live connections to a new exit.
- Do not add per-tunnel custom probe targets in this phase.
- Do not implement active best-exit probing inside GOST agents.
- Do not add a detailed best-exit UI dashboard in this phase.