webtor-rs Relay Selection: How the Tor Circuit Builder Chooses Relays
webtor-rs isolates relay selection logic in webtor/src/relay.rs, using a RelayManager that filters candidates by flags and bandwidth, sorts by consensus weight, and returns the top matches for circuit construction.
webtor-rs is a Rust implementation of a Tor client that requires sophisticated relay selection to build secure circuits. The webtor-rs relay selection system is implemented in the relay.rs module, which provides the RelayManager struct and associated criteria for filtering and ranking Tor relays based on consensus data.
Core Components of the Relay Selection System
The relay selection architecture centers on three main abstractions defined in webtor/src/relay.rs.
The Relay Struct
The Relay struct holds all data parsed from the Tor consensus for a single relay. It stores the fingerprint, cryptographic keys (RSA and Ed25519), bandwidth, flags, and the ntor onion key required for circuit handshakes.
Source: Relay struct definition.
RelayCriteria for Filtering
The RelayCriteria struct describes the constraints a caller wants when picking relays. It supports builder-style methods to specify required or excluded flags (such as FAST, STABLE, EXIT), a fingerprint blacklist, minimum bandwidth thresholds, and the maximum number of results to return.
Source: RelayCriteria definition.
RelayManager for Selection Logic
The RelayManager holds the full relay list and implements the selection algorithms. It maintains the consensus state and provides the primary entry points for filtering and ranking relays.
Source: RelayManager definition.
How webtor-rs Filters and Ranks Relays
The RelayManager::select_relays method implements a multi-stage pipeline to choose the best relays for a circuit.
Step 1: Filtering by Criteria
The algorithm first iterates over the stored relays and discards any that violate the provided RelayCriteria:
- Matches an excluded fingerprint in the blacklist.
- Missing a required flag (e.g.,
FASTorSTABLE). - Contains an excluded flag (e.g.,
BAD_EXIT). - Has bandwidth below the
min_bandwidththreshold.
Source: Filtering logic.
Step 2: Sorting by Consensus Weight
If no relays remain after filtering, the method returns a TorError::relay_selection error. Otherwise, the remaining candidates are sorted by consensus weight in descending order. Higher weight indicates a more reliable and performant relay according to the Tor network consensus.
Source: Sorting logic.
Step 3: Truncation and Random Selection
The sorted list is truncated to the max_selection limit specified in the criteria. For cases requiring a single relay, the select_relay convenience wrapper picks a random entry from the already-filtered top set using rand::seq::SliceRandom. This randomization prevents deterministic paths and achieves load balancing across suitable relays.
Source: Truncation and select_relay.
Building Circuit Targets from Selected Relays
Once a Relay is selected, it must be converted into a format usable by the Arti Tor stack. The Relay::as_circ_target method performs this transformation.
It validates the relay's address, RSA identity, Ed25519 identity, and ntor onion key, then constructs an OwnedCircTarget that the circuit builder can use to open connections. This method ensures that only properly validated relays with complete cryptographic material are used for circuit construction.
Source: as_circ_target.
Pre-Configured Selection Criteria
The selection sub-module in webtor/src/relay.rs provides ready-made RelayCriteria for common Tor circuit roles. These helpers encapsulate the standard flag requirements used by the Tor protocol:
middle_relays()– RequiresFAST,STABLE, andV2Dirflags for middle hops.exit_relays()– RequiresFAST,STABLE, andEXITflags while excludingBAD_EXITfor the final hop.guard_relays()– RequiresFAST,STABLE, andGUARDflags for entry positions.
Source: selection module.
Updating the Relay List from Consensus
Tor networks periodically publish new consensus documents containing updated relay information. The RelayManager::update_relays method accepts a new Vec<Relay> parsed from a fresh consensus (typically produced by webtor::directory::ConsensusParser) and swaps the internal relay list atomically.
This ensures that the selection algorithms always operate on current network state without requiring reconstruction of the RelayManager instance.
Source: update_relays.
Practical Code Examples
Selecting Middle Relays for a Circuit
use webtor::relay::{RelayManager, selection};
// Assume manager is initialized with consensus data
let criteria = selection::middle_relays()
.with_min_bandwidth(5_000_000) // 5 MiB/s minimum
.with_max_selection(3); // Need three middle hops
let middle_relays = manager.select_relays(&criteria)
.expect("No suitable middle relays found");
Picking a Single Guard Relay Randomly
use webtor::relay::selection;
// select_relay randomly chooses from the top candidates
let guard = manager.select_relay(&selection::guard_relays())
.expect("Failed to select a guard relay");
println!("Selected guard: {}", guard.fingerprint);
Converting a Relay to a Circuit Target
use webtor::relay::Relay;
// After selecting a relay, convert it for the Arti stack
let circ_target = relay.as_circ_target()
.expect("Relay missing required cryptographic keys");
// circ_target can now be used to open connections via the circuit builder
Updating Relays After Consensus Refresh
use webtor::relay::RelayManager;
use webtor::directory::ConsensusParser;
fn refresh_relays(manager: &mut RelayManager, consensus_bytes: &[u8]) {
let parser = ConsensusParser::new();
let new_relays = parser.parse(consensus_bytes)
.expect("Failed to parse consensus");
// Atomically update the relay list
manager.update_relays(new_relays);
}
Summary
- webtor-rs relay selection is implemented in
webtor/src/relay.rsthrough theRelayManagerstruct. - The system uses
RelayCriteriato filter relays by flags (Fast, Stable, Exit, etc.), bandwidth, and fingerprints. - Selection follows a four-stage pipeline: filtering by criteria, sorting by consensus weight, truncating to the requested limit, and randomizing the final pick.
- The
as_circ_targetmethod converts selected relays intoOwnedCircTargetobjects compatible with the Arti Tor stack. - Pre-configured helpers in the
selectionmodule provide standard criteria for guard, middle, and exit relays. - The relay list is updated atomically via
update_relayswhen new consensus documents are fetched.
Frequently Asked Questions
How does webtor-rs ensure selected relays are trustworthy?
webtor-rs relies on the Tor consensus document to establish trust. The Relay struct validates cryptographic material including RSA identity keys, Ed25519 identities, and ntor onion keys through the as_circ_target method. Additionally, the RelayCriteria system allows exclusion of relays with the BAD_EXIT flag or specific fingerprints, ensuring only properly flagged and validated relays are selected for circuit construction.
What happens if no relays match the selection criteria?
If the filtering stage eliminates all available relays, the RelayManager::select_relays method returns a TorError::relay_selection error. This typically occurs when criteria are too restrictive—for example, requiring a minimum bandwidth higher than any available relay, or excluding all relays with specific flags. The error signals that the caller must relax constraints or wait for updated consensus data.
Can I customize the relay selection beyond the pre-canned criteria?
Yes, the RelayCriteria struct provides a builder pattern for complete customization. You can chain methods like with_flag(), without_flag(), with_min_bandwidth(), with_max_selection(), and with_excluded_fingerprint() to create specific constraints. This allows implementation of custom path selection strategies, such as avoiding specific countries' relays or requiring experimental flags not covered by the standard guard_relays(), middle_relays(), or exit_relays() helpers.
How often should the relay list be updated?
The relay list should be updated whenever a new Tor consensus document is published, which typically occurs every hour in the live Tor network. The RelayManager::update_relays method accepts a new Vec<Relay> parsed from fresh consensus data and atomically replaces the internal relay list. Frequent updates ensure that the selection algorithms operate on current relay statuses, bandwidth measurements, and flags, preventing the use of relays that have gone offline or changed their exit policies.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →