Skip to main content

onionlink_core/
lib.rs

1mod circuit;
2mod crypto;
3mod directory;
4mod error;
5mod hs;
6mod tor;
7mod util;
8
9use std::sync::Mutex;
10
11use log::info;
12
13pub use circuit::{
14    build_ntor_onionskin, finish_ntor, parse_relay_body, Circuit, NtorState, RelayMessage,
15};
16pub use crypto::{
17    aes_ctr_crypt, ct_equal, hmac_sha256, kdf_tor, random_bytes, sha1, sha256, sha3_256, shake256,
18    tor_mac, x25519_public_from_private, x25519_shared, DigestKind, RelayCrypto,
19};
20pub use directory::{
21    blind_onion_key, candidate_rendezvous_relays, current_period_num, decode_http_body,
22    default_bootstrap, derive_hs_period_keys, fetch_microdescriptor_doc, hydrate_microdescriptors,
23    index_microdescriptors, link_spec_ipv4, onion_subcredential, parse_consensus,
24    parse_link_specifiers, parse_microdescriptor_fields, parse_microdescriptor_into,
25    parse_onion_address, relay_link_specifiers, relay_usable_dir, relay_usable_hsdir,
26    relay_usable_rendezvous, same_relay, select_hsdirs, serialize_link_specifiers,
27    split_microdescriptors, Consensus, HsPeriodKeys, LinkSpecifier, MicrodescriptorFields,
28    OnionAddress, Relay,
29};
30pub use error::{ensure, err, Error, Result};
31pub use hs::{
32    build_introduce1, connect_onion_service, connect_onion_service_with_retries,
33    decrypt_descriptor_layer, decrypt_hs_descriptor, fetch_hidden_service_descriptor,
34    finish_hs_ntor, parse_ed25519_cert_subject, parse_inner_descriptor, DescriptorFetchResult,
35    HiddenServiceDescriptor, HsIntroPayload, HsNtorState, IntroductionPoint, RendezvousStream,
36};
37pub use tor::{Cell, TorChannel};
38pub use util::{
39    base32_decode_onion, base64_decode, base64_encode_unpadded, from_string, hex, lower,
40    parse_hostport, put_u16, put_u32, put_u64, read_u16, read_u32, split_ws, to_string_lossy,
41    Bytes, HostPort,
42};
43
44use directory::{http_get_direct, read_file_string};
45use hs::connect_onion_service_with_retries as connect_with_retries;
46
47#[derive(Clone, Debug)]
48pub struct Options {
49    pub onion: String,
50    pub port: u16,
51    pub bootstrap: HostPort,
52    pub consensus_file: String,
53    pub verbose: bool,
54    pub stdin_mode: bool,
55    pub send_text: String,
56    pub http_get: String,
57    pub timeout_ms: i32,
58}
59
60impl Default for Options {
61    fn default() -> Self {
62        Self {
63            onion: String::new(),
64            port: 0,
65            bootstrap: default_bootstrap(),
66            consensus_file: String::new(),
67            verbose: false,
68            stdin_mode: false,
69            send_text: String::new(),
70            http_get: String::new(),
71            timeout_ms: 30000,
72        }
73    }
74}
75
76impl Options {
77    pub fn for_session(
78        bootstrap: &str,
79        consensus_file: &str,
80        timeout_ms: i32,
81        verbose: bool,
82    ) -> Result<Self> {
83        Ok(Self {
84            bootstrap: parse_hostport(bootstrap, 0)?,
85            consensus_file: consensus_file.to_string(),
86            timeout_ms,
87            verbose,
88            ..Self::default()
89        })
90    }
91}
92
93pub fn load_consensus(opt: &Options) -> Result<Consensus> {
94    if !opt.consensus_file.is_empty() {
95        info!(
96            "loading microdescriptor consensus from {}",
97            opt.consensus_file
98        );
99        let consensus = parse_consensus(&read_file_string(&opt.consensus_file)?)?;
100        info!("loaded consensus with {} relays", consensus.relays.len());
101        return Ok(consensus);
102    }
103    info!(
104        "fetching microdescriptor consensus from {}:{}",
105        opt.bootstrap.host, opt.bootstrap.port
106    );
107    let doc = http_get_direct(
108        &opt.bootstrap,
109        "/tor/status-vote/current/consensus-microdesc",
110        opt.timeout_ms,
111    )?;
112    let consensus = parse_consensus(&to_string_lossy(&doc))?;
113    info!("loaded consensus with {} relays", consensus.relays.len());
114    Ok(consensus)
115}
116
117pub fn request_bytes(
118    opt: &Options,
119    consensus: &Consensus,
120    onion: &str,
121    port: u16,
122    outbound: &[u8],
123    response_limit: usize,
124) -> Result<Bytes> {
125    let mut opt = opt.clone();
126    opt.onion = onion.to_string();
127    opt.port = port;
128    let onion_addr = parse_onion_address(&opt.onion)?;
129    let keys = derive_hs_period_keys(consensus, &onion_addr)?;
130    let desc = fetch_hidden_service_descriptor(consensus, &keys, opt.timeout_ms, opt.verbose)?;
131    let mut stream = connect_with_retries(&opt, consensus, &desc.descriptor, &keys, &[desc.guard])?;
132    const STREAM_ID: u16 = 1;
133    stream.begin(STREAM_ID, opt.port)?;
134    if !outbound.is_empty() {
135        stream.send_data(STREAM_ID, outbound)?;
136    }
137    stream.read_until_end(STREAM_ID, response_limit)
138}
139
140pub fn build_simple_http_get(onion: &str, path: &str) -> Bytes {
141    let mut normalized_path = if path.is_empty() {
142        "/".to_string()
143    } else {
144        path.to_string()
145    };
146    if !normalized_path.starts_with('/') {
147        normalized_path.insert(0, '/');
148    }
149    from_string(format!(
150        "GET {normalized_path} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n",
151        lower(onion)
152    ))
153}
154
155pub struct Session {
156    opt: Options,
157    consensus: Mutex<Consensus>,
158}
159
160impl Session {
161    pub fn new(
162        bootstrap: &str,
163        consensus_file: &str,
164        timeout_ms: i32,
165        verbose: bool,
166    ) -> Result<Self> {
167        let opt = Options::for_session(bootstrap, consensus_file, timeout_ms, verbose)?;
168        info!("initializing onionlink session");
169        let mut consensus = load_consensus(&opt)?;
170        hydrate_microdescriptors(&mut consensus, &opt.bootstrap, opt.timeout_ms, opt.verbose)?;
171        Ok(Self {
172            opt,
173            consensus: Mutex::new(consensus),
174        })
175    }
176
177    pub fn request(
178        &self,
179        onion: &str,
180        port: u16,
181        payload: &[u8],
182        response_limit: usize,
183    ) -> Result<Bytes> {
184        let consensus = self
185            .consensus
186            .lock()
187            .map_err(|_| Error::new("consensus lock poisoned"))?
188            .clone();
189        request_bytes(&self.opt, &consensus, onion, port, payload, response_limit)
190    }
191
192    pub fn http_get(
193        &self,
194        onion: &str,
195        port: u16,
196        path: &str,
197        response_limit: usize,
198    ) -> Result<Bytes> {
199        let payload = build_simple_http_get(onion, path);
200        self.request(onion, port, &payload, response_limit)
201    }
202}