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