Skip to main content

geph5_client/
get_dialer.rs

1use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
2
3use anyctx::AnyCtx;
4use anyhow::Context;
5use async_native_tls::TlsConnector;
6use ed25519_dalek::VerifyingKey;
7use geph5_broker_protocol::{
8    DOMAIN_EXIT_ROUTE, ExitConstraint, ExitDescriptor, ExitRouteDescriptor, GetExitRouteArgs,
9    JsonSigned, RouteDescriptor,
10};
11use isocountry::CountryCode;
12use rand::seq::SliceRandom;
13use sillad::{
14    dialer::{DialerExt, DynDialer, FailingDialer},
15    tcp::TcpDialer,
16};
17use sillad_conntest::ConnTestDialer;
18use sillad_hex::HexDialer;
19use sillad_meeklike::MeeklikeDialer;
20use sillad_sosistab3::{Cookie, dialer::SosistabDialer};
21
22use crate::{
23    auth::get_connect_token,
24    broker::broker_client,
25    client::Config,
26    device_metadata::get_device_metadata,
27    dial_logging::logged,
28    route_cache::{read_cached_exit_route, write_cached_exit_route},
29    vpn::smart_vpn_whitelist,
30};
31
32fn route_subtree_json(route: &RouteDescriptor) -> String {
33    serde_json::to_string(route).expect("route subtree must serialize")
34}
35
36/// Gets a sillad Dialer that produces a single, pre-authentication pipe, as well as the public key.
37pub async fn get_dialer(
38    ctx: &AnyCtx<Config>,
39) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
40    if let ExitConstraint::Direct(dir) = &ctx.init().exit_constraint {
41        let (dir, pubkey_hex) = dir
42            .split_once('/')
43            .context("did not find / in a direct constraint")?;
44        let pubkey = VerifyingKey::from_bytes(
45            hex::decode(pubkey_hex)
46                .context("cannot decode pubkey as hex")?
47                .as_slice()
48                .try_into()
49                .context("pubkey wrong length")?,
50        )?;
51        let dest_addr = *smol::net::resolve(dir)
52            .await?
53            .choose(&mut rand::thread_rng())
54            .context("could not resolve destination for direct exit connection")?;
55        let direct_route = RouteDescriptor::ConnTest {
56            ping_count: 1,
57            lower: Box::new(RouteDescriptor::Tcp(dest_addr)),
58        };
59        smart_vpn_whitelist(ctx, dest_addr.ip());
60        return Ok((
61            pubkey,
62            ExitDescriptor {
63                c2e_listen: "0.0.0.0:0".parse()?,
64                b2e_listen: "0.0.0.0:0".parse()?,
65                country: CountryCode::ABW,
66                city: "".to_string(),
67                load: 0.0,
68                expiry: 0,
69            },
70            logged(
71                "overall",
72                route_subtree_json(&direct_route),
73                logged(
74                    "conntest",
75                    route_subtree_json(&direct_route),
76                    ConnTestDialer {
77                        ping_count: 1,
78                        inner: logged(
79                            "tcp",
80                            route_subtree_json(&RouteDescriptor::Tcp(dest_addr)),
81                            TcpDialer { dest_addr },
82                        )
83                        .dynamic(),
84                    },
85                ),
86            )
87            .dynamic(),
88        ));
89    }
90
91    if let Some(cached) = read_cached_exit_route(ctx, &ctx.init().exit_constraint).await?
92        && exit_route_is_unexpired(&cached)
93    {
94        tracing::debug!(
95            expiry = cached.exit.expiry,
96            "returning unexpired cached exit route"
97        );
98        return dialer_from_exit_route(ctx, cached);
99    }
100
101    let res: anyhow::Result<ExitRouteDescriptor> = async {
102        let (_level, conn_token, sig) = get_connect_token(ctx)
103            .await
104            .context("could not get connect token")?;
105
106        let start = Instant::now();
107        let metadata = match get_device_metadata(ctx).await {
108            Ok(metadata) => {
109                tracing::debug!(
110                    metadata = debug(&metadata),
111                    elapsed = debug(start.elapsed()),
112                    "DEVICE METADATA OBTAINED"
113                );
114                serde_json::to_value(&metadata)?
115            }
116            Err(err) => {
117                tracing::warn!(
118                    err = debug(err),
119                    "CANNOT GET DEVICE METADATA, PROCEEDING NONETHELESS"
120                );
121                serde_json::Value::Null
122            }
123        };
124
125        tracing::debug!(token = %conn_token, "CONN TOKEN");
126        let broker = broker_client(ctx)?;
127        let signed_exit_route = broker
128            .get_exit_route(GetExitRouteArgs {
129                token: conn_token,
130                sig,
131                exit_constraint: ctx.init().exit_constraint.clone(),
132                client_metadata: metadata,
133            })
134            .await?
135            .map_err(|e| anyhow::anyhow!("broker refused to serve exit routes: {e}"))?;
136        let exit_route = verify_exit_route(ctx, signed_exit_route)?;
137
138        if let Err(err) =
139            write_cached_exit_route(ctx, &ctx.init().exit_constraint, &exit_route).await
140        {
141            tracing::warn!(err = debug(&err), "could not persist exit route cache");
142        }
143
144        Ok(exit_route)
145    }
146    .await;
147
148    let exit_route = match res {
149        Ok(val) => val,
150        Err(err) => {
151            tracing::warn!(err = %err, "failed to get fresh exit route");
152            match read_cached_exit_route(ctx, &ctx.init().exit_constraint).await {
153                Ok(Some(cached)) => {
154                    tracing::warn!("returning cached exit route instead");
155                    cached
156                }
157                Ok(None) => {
158                    return Err(
159                        err.context("fresh exit route unavailable and no cached route found")
160                    );
161                }
162                Err(cache_err) => {
163                    return Err(err.context(format!(
164                        "fresh exit route unavailable and cached route lookup failed: {cache_err}"
165                    )));
166                }
167            }
168        }
169    };
170
171    dialer_from_exit_route(ctx, exit_route)
172}
173
174fn exit_route_is_unexpired(route: &ExitRouteDescriptor) -> bool {
175    let now = SystemTime::now()
176        .duration_since(UNIX_EPOCH)
177        .unwrap_or_default()
178        .as_secs();
179    route.exit.expiry > now
180}
181
182fn dialer_from_exit_route(
183    ctx: &AnyCtx<Config>,
184    exit_route: ExitRouteDescriptor,
185) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
186    let ExitRouteDescriptor {
187        exit_pubkey,
188        exit,
189        route,
190    } = exit_route;
191
192    smart_vpn_whitelist(ctx, exit.c2e_listen.ip());
193    tracing::debug!(exit = ?exit, "exit route obtained: {}", serde_json::to_string(&route)?);
194
195    let combined_routes = combine_exit_route(exit.clone(), route, ctx.init().allow_direct);
196    let bridge_dialer = logged(
197        "overall",
198        route_subtree_json(&combined_routes),
199        route_to_dialer(ctx, &combined_routes),
200    )
201    .dynamic();
202
203    Ok((exit_pubkey, exit, bridge_dialer))
204}
205
206fn verify_exit_route(
207    ctx: &AnyCtx<Config>,
208    signed: JsonSigned<ExitRouteDescriptor>,
209) -> anyhow::Result<ExitRouteDescriptor> {
210    signed
211        .verify(DOMAIN_EXIT_ROUTE, |their_pk| {
212            if let Some(broker_pk) = &ctx.init().broker_keys {
213                hex::encode(their_pk.as_bytes()) == broker_pk.master
214            } else {
215                tracing::warn!("trusting exit route blindly since broker_keys was not provided");
216                true
217            }
218        })
219        .context("could not verify exit route")
220}
221
222fn combine_exit_route(
223    exit: ExitDescriptor,
224    route: RouteDescriptor,
225    allow_direct: bool,
226) -> RouteDescriptor {
227    if allow_direct {
228        RouteDescriptor::Race(vec![
229            RouteDescriptor::ConnTest {
230                ping_count: 1,
231                lower: Box::new(RouteDescriptor::Tcp(exit.c2e_listen)),
232            },
233            RouteDescriptor::Delay {
234                milliseconds: 1000,
235                lower: Box::new(route),
236            },
237        ])
238    } else {
239        route
240    }
241}
242
243fn route_to_dialer(ctx: &AnyCtx<Config>, route: &RouteDescriptor) -> DynDialer {
244    use sillad_native_tls::TlsDialer;
245
246    match route {
247        RouteDescriptor::Tcp(addr) => {
248            smart_vpn_whitelist(ctx, addr.ip());
249            let addr = *addr;
250            logged(
251                "tcp",
252                route_subtree_json(route),
253                TcpDialer { dest_addr: addr },
254            )
255            .dynamic()
256        }
257        RouteDescriptor::Sosistab3 { cookie, lower } => {
258            let inner = route_to_dialer(ctx, lower);
259            logged(
260                "sosistab3",
261                route_subtree_json(route),
262                SosistabDialer {
263                    inner,
264                    cookie: Cookie::new(cookie),
265                },
266            )
267            .dynamic()
268        }
269        RouteDescriptor::Race(inside) => inside
270            .iter()
271            .map(|s| route_to_dialer(ctx, s))
272            .reduce(|a, b| a.race(b).dynamic())
273            .unwrap_or_else(|| FailingDialer.dynamic()),
274        RouteDescriptor::Fallback(a) => a
275            .iter()
276            .map(|s| route_to_dialer(ctx, s))
277            .reduce(|a, b| a.fallback(b).dynamic())
278            .unwrap_or_else(|| FailingDialer.dynamic()),
279        RouteDescriptor::Timeout {
280            milliseconds,
281            lower,
282        } => route_to_dialer(ctx, lower)
283            .timeout(Duration::from_millis(*milliseconds as _))
284            .dynamic(),
285        RouteDescriptor::Delay {
286            milliseconds,
287            lower,
288        } => route_to_dialer(ctx, lower)
289            .delay(Duration::from_millis((*milliseconds).into()))
290            .dynamic(),
291        RouteDescriptor::ConnTest { ping_count, lower } => {
292            let lower = route_to_dialer(ctx, lower);
293            logged(
294                "conntest",
295                route_subtree_json(route),
296                ConnTestDialer {
297                    inner: lower,
298                    ping_count: *ping_count as _,
299                },
300            )
301            .dynamic()
302        }
303        RouteDescriptor::Hex { lower } => {
304            let lower = route_to_dialer(ctx, lower);
305            logged("hex", route_subtree_json(route), HexDialer { inner: lower }).dynamic()
306        }
307        RouteDescriptor::Other(_) => FailingDialer.dynamic(),
308        RouteDescriptor::PlainTls { sni_domain, lower } => {
309            let lower = route_to_dialer(ctx, lower);
310            logged(
311                "tls",
312                route_subtree_json(route),
313                TlsDialer::new(
314                    lower,
315                    TlsConnector::new()
316                        .use_sni(sni_domain.is_some())
317                        .danger_accept_invalid_certs(true)
318                        .danger_accept_invalid_hostnames(true)
319                        .min_protocol_version(None)
320                        .max_protocol_version(None),
321                    sni_domain
322                        .clone()
323                        .unwrap_or_else(|| "example.com".to_string()),
324                ),
325            )
326            .dynamic()
327        }
328        RouteDescriptor::Meeklike { key, cfg, lower } => {
329            let lower = route_to_dialer(ctx, lower);
330            logged(
331                "meeklike",
332                route_subtree_json(route),
333                MeeklikeDialer {
334                    inner: lower.into(),
335                    cfg: *cfg,
336                    key: *blake3::hash(key.as_bytes()).as_bytes(),
337                },
338            )
339            .dynamic()
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use ed25519_dalek::SigningKey;
347
348    use super::*;
349    use crate::client::{BrokerKeys, Config};
350
351    fn test_config() -> Config {
352        Config {
353            socks5_listen: None,
354            http_proxy_listen: None,
355            pac_listen: None,
356            control_listen: None,
357            exit_constraint: ExitConstraint::Auto,
358            allow_direct: false,
359            cache: None,
360            broker: None,
361            tunneled_broker: None,
362            broker_keys: None,
363            port_forward: vec![],
364            vpn: false,
365            vpn_fd: None,
366            spoof_dns: false,
367            passthrough_china: false,
368            dry_run: true,
369            credentials: Default::default(),
370            sess_metadata: serde_json::Value::Null,
371            task_limit: None,
372        }
373    }
374
375    fn sample_exit_with_expiry(expiry: u64) -> ExitDescriptor {
376        ExitDescriptor {
377            c2e_listen: "127.0.0.1:9000".parse().unwrap(),
378            b2e_listen: "127.0.0.1:9001".parse().unwrap(),
379            country: CountryCode::CAN,
380            city: "Toronto".into(),
381            load: 0.1,
382            expiry,
383        }
384    }
385
386    fn sample_exit() -> ExitDescriptor {
387        sample_exit_with_expiry(1)
388    }
389
390    fn sample_exit_route(expiry: u64) -> ExitRouteDescriptor {
391        let signing_key = SigningKey::from_bytes(&[7; 32]);
392        ExitRouteDescriptor {
393            exit_pubkey: signing_key.verifying_key(),
394            exit: sample_exit_with_expiry(expiry),
395            route: RouteDescriptor::Tcp("127.0.0.1:9002".parse().unwrap()),
396        }
397    }
398
399    #[test]
400    fn verify_exit_route_rejects_bad_signature() {
401        let trusted = SigningKey::from_bytes(&[4; 32]);
402        let attacker = SigningKey::from_bytes(&[5; 32]);
403        let mut cfg = test_config();
404        cfg.broker_keys = Some(BrokerKeys {
405            master: hex::encode(trusted.verifying_key().as_bytes()),
406            mizaru_free: String::new(),
407            mizaru_plus: String::new(),
408            mizaru_bw: String::new(),
409        });
410        let ctx = AnyCtx::new(cfg);
411        let signed = JsonSigned::new(
412            ExitRouteDescriptor {
413                exit_pubkey: attacker.verifying_key(),
414                exit: sample_exit(),
415                route: RouteDescriptor::Tcp("127.0.0.1:9002".parse().unwrap()),
416            },
417            DOMAIN_EXIT_ROUTE,
418            &attacker,
419        );
420        assert!(verify_exit_route(&ctx, signed).is_err());
421    }
422
423    #[test]
424    fn combine_exit_route_wraps_direct_path() {
425        let exit = sample_exit();
426        let combined = combine_exit_route(
427            exit.clone(),
428            RouteDescriptor::Tcp("127.0.0.1:9002".parse().unwrap()),
429            true,
430        );
431        match combined {
432            RouteDescriptor::Race(routes) => {
433                assert_eq!(routes.len(), 2);
434                match &routes[0] {
435                    RouteDescriptor::ConnTest { lower, .. } => match lower.as_ref() {
436                        RouteDescriptor::Tcp(addr) => assert_eq!(*addr, exit.c2e_listen),
437                        _ => panic!("expected direct tcp route"),
438                    },
439                    _ => panic!("expected direct route"),
440                }
441            }
442            _ => panic!("expected race route"),
443        }
444    }
445
446    #[test]
447    fn exit_route_expiry_is_compared_to_current_unix_time() {
448        let now = SystemTime::now()
449            .duration_since(UNIX_EPOCH)
450            .unwrap()
451            .as_secs();
452
453        assert!(exit_route_is_unexpired(&sample_exit_route(now + 1)));
454        assert!(!exit_route_is_unexpired(&sample_exit_route(now)));
455        assert!(!exit_route_is_unexpired(&sample_exit_route(now - 1)));
456    }
457}