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
36pub 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}