Skip to main content

onionlink_core/
circuit.rs

1use log::info;
2
3use crate::crypto::{
4    ct_equal, hkdf_sha256_expand, hmac_sha256, kdf_tor, random_bytes, x25519_public_from_private,
5    x25519_shared, DigestKind, RelayCrypto,
6};
7use crate::directory::{decode_http_body, same_relay};
8use crate::directory::{relay_link_specifiers, serialize_link_specifiers, Relay};
9use crate::error::{ensure, err, Result};
10use crate::tor::{
11    TorChannel, CMD_CREATED_FAST, CMD_CREATE_FAST, CMD_DESTROY, CMD_RELAY, CMD_RELAY_EARLY,
12    K_CELL_BODY_LEN, K_RELAY_HEADER_LEN, K_RELAY_PAYLOAD_LEN, RELAY_BEGIN_DIR, RELAY_CONNECTED,
13    RELAY_DATA, RELAY_END, RELAY_EXTEND2, RELAY_EXTENDED2, RELAY_SENDME,
14};
15use crate::util::{from_string, put_u16, read_u16, Bytes};
16
17#[derive(Clone, Debug, Default)]
18pub struct NtorState {
19    pub x: Bytes,
20    pub x_pub: Bytes,
21    pub b: Bytes,
22    pub id: Bytes,
23    pub bx: Bytes,
24}
25
26pub fn build_ntor_onionskin(relay: &Relay, st: &mut NtorState) -> Result<Bytes> {
27    ensure(
28        relay.rsa_id.len() == 20,
29        "relay missing RSA identity for ntor",
30    )?;
31    ensure(relay.ntor_key.len() == 32, "relay missing ntor key")?;
32    st.x = random_bytes(32);
33    st.x_pub = x25519_public_from_private(&st.x)?;
34    st.b = relay.ntor_key.clone();
35    st.id = relay.rsa_id.clone();
36    st.bx = x25519_shared(&st.x, &st.b)?;
37    let mut hdata = st.id.clone();
38    hdata.extend_from_slice(&st.b);
39    hdata.extend_from_slice(&st.x_pub);
40    Ok(hdata)
41}
42
43pub fn finish_ntor(st: &NtorState, hdata: &[u8]) -> Result<RelayCrypto> {
44    ensure(hdata.len() >= 64, "short ntor CREATED2 data")?;
45    let y = &hdata[..32];
46    let auth = &hdata[32..64];
47    let yx = x25519_shared(&st.x, y)?;
48    let proto = from_string("ntor-curve25519-sha256-1");
49    let mut secret = yx;
50    secret.extend_from_slice(&st.bx);
51    secret.extend_from_slice(&st.id);
52    secret.extend_from_slice(&st.b);
53    secret.extend_from_slice(&st.x_pub);
54    secret.extend_from_slice(y);
55    secret.extend_from_slice(&proto);
56    let t_key = from_string("ntor-curve25519-sha256-1:key_extract");
57    let t_verify = from_string("ntor-curve25519-sha256-1:verify");
58    let t_mac = from_string("ntor-curve25519-sha256-1:mac");
59    let key_seed = hmac_sha256(&t_key, &secret)?;
60    let verify = hmac_sha256(&t_verify, &secret)?;
61    let mut auth_input = verify;
62    auth_input.extend_from_slice(&st.id);
63    auth_input.extend_from_slice(&st.b);
64    auth_input.extend_from_slice(y);
65    auth_input.extend_from_slice(&st.x_pub);
66    auth_input.extend_from_slice(&proto);
67    auth_input.extend_from_slice(b"Server");
68    let expected = hmac_sha256(&t_mac, &auth_input)?;
69    ensure(ct_equal(auth, &expected), "ntor auth mismatch")?;
70    let k = hkdf_sha256_expand(&key_seed, b"ntor-curve25519-sha256-1:key_expand", 92)?;
71    Ok(RelayCrypto::new(
72        &k[0..20],
73        &k[20..40],
74        &k[40..56],
75        &k[56..72],
76        DigestKind::Sha1,
77    ))
78}
79
80#[derive(Clone, Debug, Default)]
81pub struct RelayMessage {
82    pub cmd: u8,
83    pub stream_id: u16,
84    pub data: Bytes,
85}
86
87pub struct Circuit {
88    ch: TorChannel,
89    id: u32,
90    hops: Vec<RelayCrypto>,
91}
92
93impl Circuit {
94    pub fn create_fast(mut ch: TorChannel) -> Result<Self> {
95        let id = ch.new_circ_id();
96        info!("creating fast circuit {id}");
97        let x = random_bytes(20);
98        let mut body = x.clone();
99        body.resize(K_CELL_BODY_LEN, 0);
100        ch.write_cell(id, CMD_CREATE_FAST, &body)?;
101        loop {
102            let c = ch.read_cell()?;
103            if c.circ_id != id {
104                continue;
105            }
106            if c.cmd == CMD_DESTROY {
107                return err("CREATE_FAST was destroyed");
108            }
109            ensure(
110                c.cmd == CMD_CREATED_FAST,
111                "unexpected cell while waiting for CREATED_FAST",
112            )?;
113            let y = &c.body[..20];
114            let kh = &c.body[20..40];
115            let mut k0 = x;
116            k0.extend_from_slice(y);
117            let k = kdf_tor(&k0, 92);
118            ensure(ct_equal(kh, &k[..20]), "CREATE_FAST key hash mismatch")?;
119            let rc = RelayCrypto::new(
120                &k[20..40],
121                &k[40..60],
122                &k[60..76],
123                &k[76..92],
124                DigestKind::Sha1,
125            );
126            return Ok(Self {
127                ch,
128                id,
129                hops: vec![rc],
130            });
131        }
132    }
133
134    pub fn send_relay(&mut self, cmd: u8, stream_id: u16, data: &[u8]) -> Result<()> {
135        let hop_index = self.hops.len() - 1;
136        self.send_relay_cell(CMD_RELAY, hop_index, cmd, stream_id, data)
137    }
138
139    pub fn recv_relay(&mut self) -> Result<RelayMessage> {
140        loop {
141            let c = self.ch.read_cell()?;
142            if c.circ_id != self.id {
143                continue;
144            }
145            if c.cmd == CMD_DESTROY {
146                return err("circuit destroyed");
147            }
148            if c.cmd != CMD_RELAY {
149                continue;
150            }
151            let mut body = c.body;
152            for hop in &mut self.hops {
153                body = hop.decrypt_body_only(&body)?;
154                if hop.recognize_decrypted(&body) {
155                    return parse_relay_body(&body);
156                }
157            }
158        }
159    }
160
161    pub fn send_raw_body(&mut self, body: &[u8]) -> Result<()> {
162        ensure(
163            body.len() == K_CELL_BODY_LEN,
164            "raw relay body must be one cell body",
165        )?;
166        let mut encrypted = body.to_vec();
167        for hop in self.hops.iter_mut().rev() {
168            encrypted = hop.encrypt_body_only(&encrypted)?;
169        }
170        self.ch.write_cell(self.id, CMD_RELAY, &encrypted)
171    }
172
173    pub fn recv_raw_body(&mut self) -> Result<Bytes> {
174        loop {
175            let c = self.ch.read_cell()?;
176            if c.circ_id != self.id {
177                continue;
178            }
179            if c.cmd == CMD_DESTROY {
180                return err("rendezvous circuit destroyed");
181            }
182            if c.cmd == CMD_RELAY {
183                let mut body = c.body;
184                for hop in &mut self.hops {
185                    body = hop.decrypt_body_only(&body)?;
186                }
187                return Ok(body);
188            }
189        }
190    }
191
192    pub fn extend_ntor(&mut self, relay: &Relay) -> Result<()> {
193        info!(
194            "extending circuit to relay {} at {}:{}",
195            relay.nickname, relay.ip, relay.or_port
196        );
197        let mut st = NtorState::default();
198        let hdata = build_ntor_onionskin(relay, &mut st)?;
199        let lspec = serialize_link_specifiers(&relay_link_specifiers(relay)?)?;
200        let mut data = lspec;
201        put_u16(&mut data, 2);
202        put_u16(&mut data, hdata.len() as u16);
203        data.extend_from_slice(&hdata);
204        let hop_index = self.hops.len() - 1;
205        self.send_relay_cell(CMD_RELAY_EARLY, hop_index, RELAY_EXTEND2, 0, &data)?;
206        loop {
207            let m = self.recv_relay()?;
208            if m.cmd == RELAY_EXTENDED2 {
209                ensure(m.data.len() >= 2, "short EXTENDED2 body")?;
210                let hlen = read_u16(&m.data, 0)? as usize;
211                ensure(m.data.len() >= 2 + hlen, "truncated EXTENDED2 handshake")?;
212                let created = &m.data[2..2 + hlen];
213                self.hops.push(finish_ntor(&st, created)?);
214                return Ok(());
215            }
216        }
217    }
218
219    fn send_relay_cell(
220        &mut self,
221        cell_cmd: u8,
222        hop_index: usize,
223        relay_cmd: u8,
224        stream_id: u16,
225        data: &[u8],
226    ) -> Result<()> {
227        ensure(hop_index < self.hops.len(), "bad hop index")?;
228        let mut body = self.hops[hop_index].encrypt_relay(relay_cmd, stream_id, data)?;
229        for i in (0..hop_index).rev() {
230            body = self.hops[i].encrypt_body_only(&body)?;
231        }
232        self.ch.write_cell(self.id, cell_cmd, &body)
233    }
234}
235
236pub fn parse_relay_body(body: &[u8]) -> Result<RelayMessage> {
237    ensure(body.len() == K_CELL_BODY_LEN, "bad relay body")?;
238    let len = read_u16(body, 9)? as usize;
239    ensure(len <= K_RELAY_PAYLOAD_LEN, "relay length too large")?;
240    Ok(RelayMessage {
241        cmd: body[0],
242        stream_id: read_u16(body, 3)?,
243        data: body[K_RELAY_HEADER_LEN..K_RELAY_HEADER_LEN + len].to_vec(),
244    })
245}
246
247pub fn begin_dir_get_via(
248    guard: &Relay,
249    target: &Relay,
250    path: &str,
251    timeout_ms: i32,
252) -> Result<Bytes> {
253    info!(
254        "opening BEGIN_DIR request to {} via guard {}",
255        target.nickname, guard.nickname
256    );
257    let ch = TorChannel::new(guard.ip.clone(), guard.or_port, timeout_ms)?;
258    let mut circ = Circuit::create_fast(ch)?;
259    circ.extend_ntor(target)?;
260    circ.send_relay(RELAY_BEGIN_DIR, 1, &[])?;
261    loop {
262        let m = circ.recv_relay()?;
263        if m.cmd == RELAY_CONNECTED && m.stream_id == 1 {
264            break;
265        }
266        if m.cmd == RELAY_END && m.stream_id == 1 {
267            return err(format!("BEGIN_DIR rejected by {}", target.nickname));
268        }
269    }
270    let req = format!(
271        "GET {path} HTTP/1.0\r\nHost: {}\r\nUser-Agent: onionlink/0\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n",
272        target.ip
273    );
274    let rb = from_string(req);
275    for chunk in rb.chunks(K_RELAY_PAYLOAD_LEN) {
276        circ.send_relay(RELAY_DATA, 1, chunk)?;
277    }
278    let mut response = Bytes::new();
279    let mut circ_window = 1000;
280    let mut stream_window = 500;
281    loop {
282        let m = circ.recv_relay()?;
283        if m.cmd == RELAY_DATA && m.stream_id == 1 {
284            response.extend_from_slice(&m.data);
285            circ_window -= 1;
286            if circ_window <= 900 {
287                circ.send_relay(RELAY_SENDME, 0, &[0, 0, 0])?;
288                circ_window += 100;
289            }
290            stream_window -= 1;
291            if stream_window <= 450 {
292                circ.send_relay(RELAY_SENDME, 1, &[])?;
293                stream_window += 50;
294            }
295        } else if m.cmd == RELAY_END && m.stream_id == 1 {
296            break;
297        }
298        if response.len() > 8 * 1024 * 1024 {
299            return err("BEGIN_DIR response too large");
300        }
301    }
302    decode_http_body(&response)
303}
304
305pub fn connect_guard_circuit(guard: &Relay, timeout_ms: i32) -> Result<Circuit> {
306    Circuit::create_fast(TorChannel::new(
307        guard.ip.clone(),
308        guard.or_port,
309        timeout_ms,
310    )?)
311}
312
313pub fn extend_unless_same(circ: &mut Circuit, guard: &Relay, target: &Relay) -> Result<()> {
314    if !same_relay(guard, target) {
315        circ.extend_ntor(target)?;
316    }
317    Ok(())
318}