1use std::time::{Duration, Instant, SystemTime};
2
3use anyctx::AnyCtx;
4use anyhow::Context;
5
6use arrayref::array_ref;
7use async_native_tls::TlsConnector;
8use ed25519_dalek::VerifyingKey;
9
10use geph5_broker_protocol::{
11 AccountLevel, ExitCategory, ExitDescriptor, GetRoutesArgs, NetStatus, RouteDescriptor,
12};
13
14use isocountry::CountryCode;
15use itertools::Itertools;
16use ordered_float::OrderedFloat;
17use rand::seq::SliceRandom;
18use serde::{Deserialize, Serialize};
19use sillad::{
20 dialer::{DialerExt, DynDialer, FailingDialer},
21 tcp::TcpDialer,
22};
23use sillad_conntest::ConnTestDialer;
24use sillad_hex::HexDialer;
25use sillad_meeklike::MeeklikeDialer;
26use sillad_sosistab3::{Cookie, dialer::SosistabDialer};
27
28use crate::{
29 auth::get_connect_token,
30 broker::{broker_client, get_net_status},
31 client::{Config, CtxField},
32 device_metadata::get_device_metadata,
33 vpn::smart_vpn_whitelist,
34};
35
36#[derive(Serialize, Deserialize, Clone, Debug)]
37#[serde(rename_all = "snake_case")]
38pub enum ExitConstraint {
39 Auto,
40 Direct(String),
41 Hostname(String),
42 Country(CountryCode),
43 CountryCity(CountryCode, String),
44}
45
46pub async fn get_dialer(
48 ctx: &AnyCtx<Config>,
49) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
50 static SEMAPH: CtxField<
51 smol::lock::Mutex<Option<(VerifyingKey, ExitDescriptor, DynDialer, SystemTime)>>,
52 > = |_| smol::lock::Mutex::new(None);
53 let mut cached_value = ctx.get(SEMAPH).lock().await;
54
55 if let Some(inner) = cached_value.clone()
56 && inner.3.elapsed()? < Duration::from_secs(120) {
57 return Ok((inner.0, inner.1, inner.2));
58 }
59
60 let res = get_dialer_inner(ctx).await;
61 match res {
62 Ok(val) => {
63 *cached_value = Some((val.0, val.1.clone(), val.2.clone(), SystemTime::now()));
64 Ok((val.0, val.1, val.2))
65 }
66 Err(err) => {
67 tracing::warn!("failed to get dialer: {:?}", err);
68 if let Some(val) = cached_value.clone() {
69 tracing::warn!("returning stale value instead");
70 Ok((val.0, val.1, val.2))
71 } else {
72 Err(err)
73 }
74 }
75 }
76}
77
78async fn get_dialer_inner(
79 ctx: &AnyCtx<Config>,
80) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
81 if let ExitConstraint::Direct(dir) = &ctx.init().exit_constraint {
83 let (dir, pubkey_hex) = dir
84 .split_once('/')
85 .context("did not find / in a direct constraint")?;
86 let pubkey = VerifyingKey::from_bytes(
87 hex::decode(pubkey_hex)
88 .context("cannot decode pubkey as hex")?
89 .as_slice()
90 .try_into()
91 .context("pubkey wrong length")?,
92 )?;
93 let dest_addr = *smol::net::resolve(dir)
94 .await?
95 .choose(&mut rand::thread_rng())
96 .context("could not resolve destination for direct exit connection")?;
97 smart_vpn_whitelist(ctx, dest_addr.ip());
98 return Ok((
99 pubkey,
100 ExitDescriptor {
101 c2e_listen: "0.0.0.0:0".parse()?,
102 b2e_listen: "0.0.0.0:0".parse()?,
103 country: CountryCode::ABW,
104 city: "".to_string(),
105 load: 0.0,
106 expiry: 0,
107 },
108 ConnTestDialer {
109 ping_count: 1,
110 inner: TcpDialer { dest_addr },
111 }
112 .dynamic(),
113 ));
114 }
115
116 let (level, conn_token, sig) = get_connect_token(ctx)
118 .await
119 .context("could not get connect token")?;
120
121 let net_status_verified = get_net_status(ctx).await?;
122
123 tracing::debug!(
124 "verified netstatus: {}",
125 serde_json::to_string(
126 &net_status_verified
127 .exits
128 .iter()
129 .map(|s| &s.1.1)
130 .collect_vec()
131 )?
132 );
133
134 let start = Instant::now();
135 let metadata = match get_device_metadata(ctx).await {
136 Ok(metadata) => {
137 tracing::info!(
138 metadata = debug(&metadata),
139 elapsed = debug(start.elapsed()),
140 "DEVICE METADATA OBTAINED"
141 );
142 serde_json::to_value(&metadata)?
143 }
144 Err(err) => {
145 tracing::warn!(
146 err = debug(err),
147 "CANNOT GET DEVICE METADATA, PROCEEDING NONETHELESS"
148 );
149 serde_json::Value::Null
150 }
151 };
152
153 let rendezvous_key = blake3::hash(serde_json::to_string(&ctx.init().credentials)?.as_bytes());
155 let (pubkey, exit) = pick_exit_with_constraint(
156 rendezvous_key,
157 &ctx.init().exit_constraint,
158 level,
159 &net_status_verified,
160 )?;
161
162 tracing::debug!(exit = ?exit, "narrowed down choice of exit");
163 smart_vpn_whitelist(ctx, exit.c2e_listen.ip());
164
165 tracing::debug!(token = %conn_token, "CONN TOKEN");
166
167 let broker = broker_client(ctx)?;
169 let bridge_routes = broker
170 .get_routes_v2(GetRoutesArgs {
171 token: conn_token,
172 sig,
173 exit_b2e: exit.b2e_listen,
174 client_metadata: metadata,
175 })
176 .await?
177 .map_err(|e| anyhow::anyhow!("broker refused to serve bridge routes: {e}"))?;
178
179 tracing::debug!(
180 "bridge routes obtained: {}",
181 serde_json::to_string(&bridge_routes)?
182 );
183
184 let combined_routes = if ctx.init().allow_direct {
185 RouteDescriptor::Race(vec![
186 RouteDescriptor::ConnTest {
187 ping_count: 1,
188 lower: Box::new(RouteDescriptor::Tcp(exit.c2e_listen)),
189 },
190 RouteDescriptor::Delay {
191 milliseconds: 1000,
192 lower: Box::new(bridge_routes),
193 },
194 ])
195 } else {
196 bridge_routes
197 };
198 let bridge_dialer = route_to_dialer(ctx, &combined_routes);
199
200 Ok((*pubkey, exit.clone(), bridge_dialer))
201}
202
203fn pick_exit_with_constraint<'a>(
206 rendezvous_key: blake3::Hash,
207 constraint: &ExitConstraint,
208 level: AccountLevel,
209 net_status: &'a NetStatus,
210) -> anyhow::Result<(&'a VerifyingKey, &'a ExitDescriptor)> {
211 let all_exits = net_status.exits.values();
212
213 let mut country_constraint = None;
215 let mut city_constraint = None;
216 let mut hostname_constraint = None;
217
218 match constraint {
219 ExitConstraint::Hostname(host) => hostname_constraint = Some(host.clone()),
220 ExitConstraint::Country(country) => country_constraint = Some(*country),
221 ExitConstraint::CountryCity(country, city) => {
222 country_constraint = Some(*country);
223 city_constraint = Some(city.clone());
224 }
225 ExitConstraint::Auto => {}
226 ExitConstraint::Direct(_) => panic!("should not reach here"),
227 }
228
229 let filtered: Vec<_> = all_exits
230 .filter(|(_, exit, meta)| {
231 let mut pass = if let Some(c) = country_constraint {
232 exit.country == c
233 } else {
234 true
235 };
236 pass &= match &city_constraint {
237 Some(city) => exit.city == *city,
238 None => true,
239 };
240 pass &= match &hostname_constraint {
241 Some(hn) => exit.b2e_listen.ip().to_string() == *hn,
242 None => true,
243 };
244 if matches!(constraint, ExitConstraint::Auto) {
245 pass &= meta.category == ExitCategory::Core;
246 }
247 pass &= meta.allowed_levels.contains(&level);
248 pass
249 })
250 .collect();
251
252 if filtered.is_empty() {
253 anyhow::bail!("no exits match the constraints")
254 }
255
256 let first = filtered
258 .iter()
259 .min_by_key(|rh| {
260 let (_, exit, _) = **rh;
261 let hash = blake3::keyed_hash(
262 rendezvous_key.as_bytes(),
263 exit.b2e_listen.ip().to_string().as_bytes(),
264 );
265 let hash = &hash.as_bytes()[..];
266 let hash = u64::from_be_bytes(*array_ref![hash, 0, 8]) as f64 / u64::MAX as f64;
267 let weight = (1.0 - (exit.load as f64)).powi(2);
268 let picker = -hash.ln() / weight;
269 OrderedFloat(picker)
270 })
271 .unwrap();
272 Ok((&first.0, &first.1))
273}
274
275fn route_to_dialer(ctx: &AnyCtx<Config>, route: &RouteDescriptor) -> DynDialer {
276 use sillad_native_tls::TlsDialer;
277
278 match route {
279 RouteDescriptor::Tcp(addr) => {
280 smart_vpn_whitelist(ctx, addr.ip());
281 let addr = *addr;
282 TcpDialer { dest_addr: addr }.dynamic()
288 }
290 RouteDescriptor::Sosistab3 { cookie, lower } => {
291 let inner = route_to_dialer(ctx, lower);
292 SosistabDialer {
293 inner,
294 cookie: Cookie::new(cookie),
295 }
296 .dynamic()
297 }
298 RouteDescriptor::Race(inside) => inside
299 .iter()
300 .map(|s| route_to_dialer(ctx, s))
301 .reduce(|a, b| a.race(b).dynamic())
302 .unwrap_or_else(|| FailingDialer.dynamic()),
303 RouteDescriptor::Fallback(a) => a
304 .iter()
305 .map(|s| route_to_dialer(ctx, s))
306 .reduce(|a, b| a.fallback(b).dynamic())
307 .unwrap_or_else(|| FailingDialer.dynamic()),
308 RouteDescriptor::Timeout {
309 milliseconds,
310 lower,
311 } => route_to_dialer(ctx, lower)
312 .timeout(Duration::from_millis(*milliseconds as _))
313 .dynamic(),
314 RouteDescriptor::Delay {
315 milliseconds,
316 lower,
317 } => route_to_dialer(ctx, lower)
318 .delay(Duration::from_millis((*milliseconds).into()))
319 .dynamic(),
320 RouteDescriptor::ConnTest { ping_count, lower } => {
321 let lower = route_to_dialer(ctx, lower);
322 ConnTestDialer {
323 inner: lower,
324 ping_count: *ping_count as _,
325 }
326 .dynamic()
327 }
328 RouteDescriptor::Hex { lower } => {
329 let lower = route_to_dialer(ctx, lower);
330 HexDialer { inner: lower }.dynamic()
331 }
332 RouteDescriptor::Other(_) => FailingDialer.dynamic(),
333 RouteDescriptor::PlainTls { sni_domain, lower } => {
334 let lower = route_to_dialer(ctx, lower);
335 TlsDialer::new(
336 lower,
337 TlsConnector::new()
338 .use_sni(sni_domain.is_some())
339 .danger_accept_invalid_certs(true)
340 .danger_accept_invalid_hostnames(true)
341 .min_protocol_version(None)
342 .max_protocol_version(None),
343 sni_domain
344 .clone()
345 .unwrap_or_else(|| "example.com".to_string()),
346 )
347 .dynamic()
348 }
349 RouteDescriptor::Meeklike { key, cfg, lower } => {
350 let lower = route_to_dialer(ctx, lower);
351 MeeklikeDialer {
352 inner: lower.into(),
353 cfg: *cfg,
354 key: *blake3::hash(key.as_bytes()).as_bytes(),
355 }
356 .dynamic()
357 }
358 }
359}