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., FAST or STABLE).
  • Contains an excluded flag (e.g., BAD_EXIT).
  • Has bandwidth below the min_bandwidth threshold.

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() – Requires FAST, STABLE, and V2Dir flags for middle hops.
  • exit_relays() – Requires FAST, STABLE, and EXIT flags while excluding BAD_EXIT for the final hop.
  • guard_relays() – Requires FAST, STABLE, and GUARD flags 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.rs through the RelayManager struct.
  • The system uses RelayCriteria to 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_target method converts selected relays into OwnedCircTarget objects compatible with the Arti Tor stack.
  • Pre-configured helpers in the selection module provide standard criteria for guard, middle, and exit relays.
  • The relay list is updated atomically via update_relays when 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →