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::{dialer::SosistabDialer, Cookie};
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 if inner.3.elapsed()? < Duration::from_secs(120) {
57 return Ok((inner.0, inner.1, inner.2));
58 }
59 }
60
61 let res = get_dialer_inner(ctx).await;
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 if let ExitConstraint::Direct(dir) = &ctx.init().exit_constraint {
84 let (dir, pubkey_hex) = dir
85 .split_once('/')
86 .context("did not find / in a direct constraint")?;
87 let pubkey = VerifyingKey::from_bytes(
88 hex::decode(pubkey_hex)
89 .context("cannot decode pubkey as hex")?
90 .as_slice()
91 .try_into()
92 .context("pubkey wrong length")?,
93 )?;
94 let dest_addr = *smol::net::resolve(dir)
95 .await?
96 .choose(&mut rand::thread_rng())
97 .context("could not resolve destination for direct exit connection")?;
98 smart_vpn_whitelist(ctx, dest_addr.ip());
99 return Ok((
100 pubkey,
101 ExitDescriptor {
102 c2e_listen: "0.0.0.0:0".parse()?,
103 b2e_listen: "0.0.0.0:0".parse()?,
104 country: CountryCode::ABW,
105 city: "".to_string(),
106 load: 0.0,
107 expiry: 0,
108 },
109 ConnTestDialer {
110 ping_count: 1,
111 inner: TcpDialer { dest_addr },
112 }
113 .dynamic(),
114 ));
115 }
116
117 let (level, conn_token, sig) = get_connect_token(ctx)
119 .await
120 .context("could not get connect token")?;
121
122 let net_status_verified = get_net_status(ctx).await?;
123
124 tracing::debug!(
125 "verified netstatus: {}",
126 serde_json::to_string(
127 &net_status_verified
128 .exits
129 .iter()
130 .map(|s| &s.1 .1)
131 .collect_vec()
132 )?
133 );
134
135 let start = Instant::now();
136 let metadata = match get_device_metadata(ctx).await {
137 Ok(metadata) => {
138 tracing::info!(
139 metadata = debug(&metadata),
140 elapsed = debug(start.elapsed()),
141 "DEVICE METADATA OBTAINED"
142 );
143 serde_json::to_value(&metadata)?
144 }
145 Err(err) => {
146 tracing::warn!(
147 err = debug(err),
148 "CANNOT GET DEVICE METADATA, PROCEEDING NONETHELESS"
149 );
150 serde_json::Value::Null
151 }
152 };
153
154 let rendezvous_key = blake3::hash(serde_json::to_string(&ctx.init().credentials)?.as_bytes());
156 let (pubkey, exit) = pick_exit_with_constraint(
157 rendezvous_key,
158 &ctx.init().exit_constraint,
159 level,
160 &net_status_verified,
161 )?;
162
163 tracing::debug!(exit = ?exit, "narrowed down choice of exit");
164 smart_vpn_whitelist(ctx, exit.c2e_listen.ip());
165
166 tracing::debug!(token = %conn_token, "CONN TOKEN");
167
168 let broker = broker_client(ctx)?;
170 let bridge_routes = broker
171 .get_routes_v2(GetRoutesArgs {
172 token: conn_token,
173 sig,
174 exit_b2e: exit.b2e_listen,
175 client_metadata: metadata,
176 })
177 .await?
178 .map_err(|e| anyhow::anyhow!("broker refused to serve bridge routes: {e}"))?;
179
180 tracing::debug!(
181 "bridge routes obtained: {}",
182 serde_json::to_string(&bridge_routes)?
183 );
184
185 let bridge_dialer = route_to_dialer(ctx, &bridge_routes);
186
187 Ok((*pubkey, exit.clone(), bridge_dialer))
188}
189
190fn pick_exit_with_constraint<'a>(
193 rendezvous_key: blake3::Hash,
194 constraint: &ExitConstraint,
195 level: AccountLevel,
196 net_status: &'a NetStatus,
197) -> anyhow::Result<(&'a VerifyingKey, &'a ExitDescriptor)> {
198 let all_exits = net_status.exits.values();
199
200 let mut country_constraint = None;
202 let mut city_constraint = None;
203 let mut hostname_constraint = None;
204
205 match constraint {
206 ExitConstraint::Hostname(host) => hostname_constraint = Some(host.clone()),
207 ExitConstraint::Country(country) => country_constraint = Some(*country),
208 ExitConstraint::CountryCity(country, city) => {
209 country_constraint = Some(*country);
210 city_constraint = Some(city.clone());
211 }
212 ExitConstraint::Auto => {}
213 ExitConstraint::Direct(_) => panic!("should not reach here"),
214 }
215
216 let filtered: Vec<_> = all_exits
217 .filter(|(_, exit, meta)| {
218 let mut pass = if let Some(c) = country_constraint {
219 exit.country == c
220 } else {
221 true
222 };
223 pass &= match &city_constraint {
224 Some(city) => exit.city == *city,
225 None => true,
226 };
227 pass &= match &hostname_constraint {
228 Some(hn) => exit.b2e_listen.ip().to_string() == *hn,
229 None => true,
230 };
231 if matches!(constraint, ExitConstraint::Auto) {
232 pass &= meta.category == ExitCategory::Core;
233 }
234 pass &= meta.allowed_levels.contains(&level);
235 pass
236 })
237 .collect();
238
239 if filtered.is_empty() {
240 anyhow::bail!("no exits match the constraints")
241 }
242
243 let first = filtered
245 .iter()
246 .min_by_key(|rh| {
247 let (_, exit, _) = **rh;
248 let hash = blake3::keyed_hash(
249 rendezvous_key.as_bytes(),
250 exit.b2e_listen.ip().to_string().as_bytes(),
251 );
252 let hash = &hash.as_bytes()[..];
253 let hash = u64::from_be_bytes(*array_ref![hash, 0, 8]) as f64 / u64::MAX as f64;
254 let weight = (1.0 - (exit.load as f64)).powi(2);
255 let picker = -hash.ln() / weight;
256 OrderedFloat(picker)
257 })
258 .unwrap();
259 Ok((&first.0, &first.1))
260}
261
262fn route_to_dialer(ctx: &AnyCtx<Config>, route: &RouteDescriptor) -> DynDialer {
263 use sillad_native_tls::TlsDialer;
264
265 match route {
266 RouteDescriptor::Tcp(addr) => {
267 smart_vpn_whitelist(ctx, addr.ip());
268 let addr = *addr;
269 TcpDialer { dest_addr: addr }.dynamic()
275 }
277 RouteDescriptor::Sosistab3 { cookie, lower } => {
278 let inner = route_to_dialer(ctx, lower);
279 SosistabDialer {
280 inner,
281 cookie: Cookie::new(cookie),
282 }
283 .dynamic()
284 }
285 RouteDescriptor::Race(inside) => inside
286 .iter()
287 .map(|s| route_to_dialer(ctx, s))
288 .reduce(|a, b| a.race(b).dynamic())
289 .unwrap_or_else(|| FailingDialer.dynamic()),
290 RouteDescriptor::Fallback(a) => a
291 .iter()
292 .map(|s| route_to_dialer(ctx, s))
293 .reduce(|a, b| a.fallback(b).dynamic())
294 .unwrap_or_else(|| FailingDialer.dynamic()),
295 RouteDescriptor::Timeout {
296 milliseconds,
297 lower,
298 } => route_to_dialer(ctx, lower)
299 .timeout(Duration::from_millis(*milliseconds as _))
300 .dynamic(),
301 RouteDescriptor::Delay {
302 milliseconds,
303 lower,
304 } => route_to_dialer(ctx, lower)
305 .delay(Duration::from_millis((*milliseconds).into()))
306 .dynamic(),
307 RouteDescriptor::ConnTest { ping_count, lower } => {
308 let lower = route_to_dialer(ctx, lower);
309 ConnTestDialer {
310 inner: lower,
311 ping_count: *ping_count as _,
312 }
313 .dynamic()
314 }
315 RouteDescriptor::Hex { lower } => {
316 let lower = route_to_dialer(ctx, lower);
317 HexDialer { inner: lower }.dynamic()
318 }
319 RouteDescriptor::Other(_) => FailingDialer.dynamic(),
320 RouteDescriptor::PlainTls { sni_domain, lower } => {
321 let lower = route_to_dialer(ctx, lower);
322 TlsDialer::new(
323 lower,
324 TlsConnector::new()
325 .use_sni(sni_domain.is_some())
326 .danger_accept_invalid_certs(true)
327 .danger_accept_invalid_hostnames(true)
328 .min_protocol_version(None)
329 .max_protocol_version(None),
330 sni_domain
331 .clone()
332 .unwrap_or_else(|| "example.com".to_string()),
333 )
334 .dynamic()
335 }
336 RouteDescriptor::Meeklike { key, cfg, lower } => {
337 let lower = route_to_dialer(ctx, lower);
338 MeeklikeDialer {
339 inner: lower.into(),
340 cfg: *cfg,
341 key: *blake3::hash(key.as_bytes()).as_bytes(),
342 }
343 .dynamic()
344 }
345 }
346}