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