1use std::time::{Duration, Instant, SystemTime};
2
3use anyctx::AnyCtx;
4use anyhow::Context;
5
6use async_native_tls::TlsConnector;
7use ed25519_dalek::VerifyingKey;
8
9use geph5_broker_protocol::{
10 AccountLevel, ExitDescriptor, RouteDescriptor, DOMAIN_EXIT_DESCRIPTOR,
11};
12use isocountry::CountryCode;
13use rand::seq::SliceRandom;
14use serde::{Deserialize, Serialize};
15use sillad::{
16 dialer::{DialerExt, DynDialer, FailingDialer},
17 tcp::TcpDialer,
18};
19use sillad_conntest::ConnTestDialer;
20use sillad_sosistab3::{dialer::SosistabDialer, Cookie};
21use smol::lock::Semaphore;
22use smol_timeout2::TimeoutExt as _;
23
24use crate::{
25 auth::get_connect_token,
26 broker::broker_client,
27 client::{Config, CtxField},
28 vpn::smart_vpn_whitelist,
29};
30
31#[derive(Serialize, Deserialize, Clone, Debug)]
32#[serde(rename_all = "snake_case")]
33pub enum ExitConstraint {
34 Auto,
35 Direct(String),
36 Hostname(String),
37 Country(CountryCode),
38 CountryCity(CountryCode, String),
39}
40
41pub async fn get_dialer(
43 ctx: &AnyCtx<Config>,
44) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
45 static SEMAPH: CtxField<
46 smol::lock::Mutex<Option<(VerifyingKey, ExitDescriptor, DynDialer, SystemTime)>>,
47 > = |_| smol::lock::Mutex::new(None);
48 let mut cached_value = ctx.get(SEMAPH).lock().await;
49
50 if let Some(inner) = cached_value.clone() {
51 if inner.3.elapsed()? < Duration::from_secs(10) {
52 tracing::debug!("returning very recently cached dialer");
53 return Ok((inner.0, inner.1, inner.2));
54 }
55 }
56
57 let res = get_dialer_inner(ctx)
58 .timeout(Duration::from_secs(5))
59 .await
60 .ok_or_else(|| anyhow::anyhow!("get_dialer_inner timed out"))
61 .and_then(|x| x);
62 match res {
63 Ok(val) => {
64 *cached_value = Some((val.0, val.1.clone(), val.2.clone(), SystemTime::now()));
65 Ok((val.0, val.1, val.2))
66 }
67 Err(err) => {
68 tracing::warn!("failed to get dialer: {:?}", err);
69 if let Some(val) = cached_value.clone() {
70 tracing::warn!("returning stale value instead");
71 Ok((val.0, val.1, val.2))
72 } else {
73 Err(err)
74 }
75 }
76 }
77}
78
79async fn get_dialer_inner(
80 ctx: &AnyCtx<Config>,
81) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
82 let mut country_constraint = None;
83 let mut city_constraint = None;
84 let mut hostname_constraint = None;
85 match &ctx.init().exit_constraint {
86 ExitConstraint::Direct(dir) => {
87 let (dir, pubkey) = dir
88 .split_once('/')
89 .context("did not find / in a direct constraint")?;
90 let pubkey = VerifyingKey::from_bytes(
91 hex::decode(pubkey)
92 .context("cannot decode pubkey as hex")?
93 .as_slice()
94 .try_into()
95 .context("pubkey wrong length")?,
96 )?;
97 let dest_addr = *smol::net::resolve(dir)
98 .await?
99 .choose(&mut rand::thread_rng())
100 .context("could not resolve destination for direct exit connection")?;
101 smart_vpn_whitelist(ctx, dest_addr.ip());
102 return Ok((
103 pubkey,
104 ExitDescriptor {
105 c2e_listen: "0.0.0.0:0".parse()?,
106 b2e_listen: "0.0.0.0:0".parse()?,
107 country: CountryCode::ABW,
108 city: "".to_string(),
109 load: 0.0,
110 expiry: 0,
111 },
112 ConnTestDialer {
113 ping_count: 1,
114 inner: TcpDialer { dest_addr },
115 }
116 .dynamic(),
117 ));
118 }
119 ExitConstraint::Country(country) => country_constraint = Some(*country),
120 ExitConstraint::CountryCity(country, city) => {
121 country_constraint = Some(*country);
122 city_constraint = Some(city.clone())
123 }
124 ExitConstraint::Hostname(hostname) => {
125 hostname_constraint = Some(hostname.clone());
126 }
127 ExitConstraint::Auto => {}
128 }
129
130 let (level, conn_token, sig) = get_connect_token(ctx)
132 .await
133 .context("could not get connect token")?;
134
135 let broker = broker_client(ctx).context("could not get broker client")?;
136 let exits = match level {
137 AccountLevel::Plus => broker.get_exits().await,
138 AccountLevel::Free => broker.get_free_exits().await,
139 }?
140 .map_err(|e| anyhow::anyhow!("broker refused to serve exits: {e}"))?;
141
142 let exits = exits
143 .verify(DOMAIN_EXIT_DESCRIPTOR, |their_pk| {
144 if let Some(broker_pk) = &ctx.init().broker_keys {
145 hex::encode(their_pk.as_bytes()) == broker_pk.master
146 } else {
147 true
148 }
149 })
150 .context("could not verify")?;
151 let (pubkey, exit) = if let Some(min) = exits
153 .all_exits
154 .iter()
155 .filter(|(_, exit)| {
156 let country_pass = if let Some(country) = &country_constraint {
157 exit.country == *country
158 } else {
159 true
160 };
161 let city_pass = if let Some(city) = &city_constraint {
162 &exit.city == city
163 } else {
164 true
165 };
166 let hostname_pass = if let Some(hostname) = &hostname_constraint {
167 &exit.b2e_listen.ip().to_string() == hostname
168 } else {
169 true
170 };
171 country_pass && city_pass && hostname_pass
172 })
173 .min_by_key(|e| (e.1.load * 1000.0) as u64)
174 {
175 min
176 } else {
177 exits
178 .all_exits
179 .iter()
180 .min_by_key(|e| (e.1.load * 1000.0) as u64)
181 .context("no exits that fit the criterion")?
182 };
183
184 tracing::debug!(exit = debug(&exit), "narrowed down choice of exit");
185 smart_vpn_whitelist(ctx, exit.c2e_listen.ip());
186
187 let exit_c2e = exit.c2e_listen;
188 let direct_dialer = ConnTestDialer {
189 ping_count: 2,
190 inner: TcpDialer {
191 dest_addr: exit_c2e,
192 },
193 };
194
195 tracing::debug!(token = display(&conn_token), "CONN TOKEN");
196
197 let bridge_routes = broker
199 .get_routes(conn_token, sig, exit.b2e_listen)
200 .await?
201 .map_err(|e| anyhow::anyhow!("broker refused to serve bridge routes: {e}"))?;
202 tracing::debug!(
203 "bridge routes obtained: {}",
204 serde_yaml::to_string(&serde_json::to_value(&bridge_routes)?)?
205 );
206
207 let bridge_dialer = route_to_dialer(ctx, &bridge_routes);
208
209 let final_dialer = match ctx.init().bridge_mode {
210 crate::BridgeMode::Auto => direct_dialer
211 .race(bridge_dialer.delay(Duration::from_millis(1000)))
212 .dynamic(),
213 crate::BridgeMode::ForceBridges => bridge_dialer,
214 crate::BridgeMode::ForceDirect => direct_dialer.dynamic(),
215 };
216
217 Ok((*pubkey, exit.clone(), final_dialer))
218}
219
220fn route_to_dialer(ctx: &AnyCtx<Config>, route: &RouteDescriptor) -> DynDialer {
305 match route {
306 RouteDescriptor::Tcp(addr) => {
307 smart_vpn_whitelist(ctx, addr.ip());
308 let addr = *addr;
309 TcpDialer { dest_addr: addr }.dynamic()
310 }
311 RouteDescriptor::Sosistab3 { cookie, lower } => {
312 let inner = route_to_dialer(ctx, lower);
313 SosistabDialer {
314 inner,
315 cookie: Cookie::new(cookie),
316 }
317 .dynamic()
318 }
319 RouteDescriptor::Race(inside) => inside
320 .iter()
321 .map(|s| route_to_dialer(ctx, s))
322 .reduce(|a, b| a.race(b).dynamic())
323 .unwrap_or_else(|| FailingDialer.dynamic()),
324 RouteDescriptor::Fallback(a) => a
325 .iter()
326 .map(|s| route_to_dialer(ctx, s))
327 .reduce(|a, b| a.fallback(b).dynamic())
328 .unwrap_or_else(|| FailingDialer.dynamic()),
329 RouteDescriptor::Timeout {
330 milliseconds,
331 lower,
332 } => route_to_dialer(ctx, lower)
333 .timeout(Duration::from_millis(*milliseconds as _))
334 .dynamic(),
335 RouteDescriptor::Delay {
336 milliseconds,
337 lower,
338 } => route_to_dialer(ctx, lower)
339 .delay(Duration::from_millis((*milliseconds).into()))
340 .dynamic(),
341 RouteDescriptor::ConnTest { ping_count, lower } => {
342 let lower = route_to_dialer(ctx, lower);
343 ConnTestDialer {
344 inner: lower,
345 ping_count: *ping_count as _,
346 }
347 .dynamic()
348 }
349
350 RouteDescriptor::Other(_) => FailingDialer.dynamic(),
351 RouteDescriptor::PlainTls { sni_domain, lower } => {
352 let lower = route_to_dialer(ctx, lower);
353 sillad_native_tls::TlsDialer::new(
354 lower,
355 TlsConnector::new()
356 .use_sni(sni_domain.is_some())
357 .danger_accept_invalid_certs(true)
358 .danger_accept_invalid_hostnames(true)
359 .min_protocol_version(None)
360 .max_protocol_version(None),
361 sni_domain
362 .clone()
363 .unwrap_or_else(|| "example.com".to_string()),
364 )
365 .dynamic()
366 }
367 }
368}