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};
13use isocountry::CountryCode;
14use itertools::Itertools;
15use ordered_float::OrderedFloat;
16use rand::seq::SliceRandom;
17use serde::{Deserialize, Serialize};
18use sillad::{
19 dialer::{DialerExt, DynDialer, FailingDialer},
20 tcp::TcpDialer,
21};
22use sillad_conntest::ConnTestDialer;
23use sillad_hex::HexDialer;
24use sillad_meeklike::MeeklikeDialer;
25use sillad_sosistab3::{dialer::SosistabDialer, Cookie};
26
27use crate::{
28 auth::get_connect_token,
29 broker::{broker_client, get_net_status},
30 client::{Config, CtxField},
31 device_metadata::get_device_metadata,
32 vpn::smart_vpn_whitelist,
33};
34
35#[derive(Serialize, Deserialize, Clone, Debug)]
36#[serde(rename_all = "snake_case")]
37pub enum ExitConstraint {
38 Auto,
39 Direct(String),
40 Hostname(String),
41 Country(CountryCode),
42 CountryCity(CountryCode, String),
43}
44
45pub async fn get_dialer(
47 ctx: &AnyCtx<Config>,
48) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
49 static SEMAPH: CtxField<
50 smol::lock::Mutex<Option<(VerifyingKey, ExitDescriptor, DynDialer, SystemTime)>>,
51 > = |_| smol::lock::Mutex::new(None);
52 let mut cached_value = ctx.get(SEMAPH).lock().await;
53
54 if let Some(inner) = cached_value.clone() {
55 if inner.3.elapsed()? < Duration::from_secs(120) {
56 return Ok((inner.0, inner.1, inner.2));
57 }
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 rendezvous_key = blake3::hash(serde_json::to_string(&ctx.init().credentials)?.as_bytes());
136 let (pubkey, exit) = pick_exit_with_constraint(
137 rendezvous_key,
138 &ctx.init().exit_constraint,
139 level,
140 &net_status_verified,
141 )?;
142
143 tracing::debug!(exit = ?exit, "narrowed down choice of exit");
144 smart_vpn_whitelist(ctx, exit.c2e_listen.ip());
145
146 tracing::debug!(token = %conn_token, "CONN TOKEN");
147
148 let start = Instant::now();
149 let metadata = match get_device_metadata(ctx).await {
150 Ok(metadata) => {
151 tracing::info!(
152 metadata = debug(&metadata),
153 elapsed = debug(start.elapsed()),
154 "DEVICE METADATA OBTAINED"
155 );
156 serde_json::to_value(&metadata)?
157 }
158 Err(err) => {
159 tracing::warn!(
160 err = debug(err),
161 "CANNOT GET DEVICE METADATA, PROCEEDING NONETHELESS"
162 );
163 serde_json::Value::Null
164 }
165 };
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 tracing::debug!(
179 "bridge routes obtained: {}",
180 serde_json::to_string(&bridge_routes)?
181 );
182
183 let bridge_dialer = route_to_dialer(ctx, &bridge_routes);
184
185 Ok((*pubkey, exit.clone(), bridge_dialer))
186}
187
188fn pick_exit_with_constraint<'a>(
191 rendezvous_key: blake3::Hash,
192 constraint: &ExitConstraint,
193 level: AccountLevel,
194 net_status: &'a NetStatus,
195) -> anyhow::Result<(&'a VerifyingKey, &'a ExitDescriptor)> {
196 let all_exits = net_status.exits.values();
197
198 let mut country_constraint = None;
200 let mut city_constraint = None;
201 let mut hostname_constraint = None;
202
203 match constraint {
204 ExitConstraint::Hostname(host) => hostname_constraint = Some(host.clone()),
205 ExitConstraint::Country(country) => country_constraint = Some(*country),
206 ExitConstraint::CountryCity(country, city) => {
207 country_constraint = Some(*country);
208 city_constraint = Some(city.clone());
209 }
210 ExitConstraint::Auto => {}
211 ExitConstraint::Direct(_) => panic!("should not reach here"),
212 }
213
214 let filtered: Vec<_> = all_exits
215 .filter(|(_, exit, meta)| {
216 let mut pass = match country_constraint {
217 Some(c) => exit.country == c,
218 None => true,
219 };
220 pass &= match &city_constraint {
221 Some(city) => exit.city == *city,
222 None => true,
223 };
224 pass &= match &hostname_constraint {
225 Some(hn) => exit.b2e_listen.ip().to_string() == *hn,
226 None => true,
227 };
228 if matches!(constraint, ExitConstraint::Auto) {
229 pass &= meta.category == ExitCategory::Core;
230 }
231 pass &= meta.allowed_levels.contains(&level);
232 pass
233 })
234 .collect();
235
236 if filtered.is_empty() {
237 anyhow::bail!("no exits match the constraints")
238 }
239
240 let first = filtered
242 .iter()
243 .min_by_key(|rh| {
244 let (_, exit, _) = **rh;
245 let hash = blake3::keyed_hash(
246 rendezvous_key.as_bytes(),
247 exit.b2e_listen.ip().to_string().as_bytes(),
248 );
249 let hash = &hash.as_bytes()[..];
250 let hash = u64::from_be_bytes(*array_ref![hash, 0, 8]) as f64 / u64::MAX as f64;
251 let weight = (1.0 - (exit.load as f64)).powi(2);
252 let picker = -hash.ln() / weight;
253 OrderedFloat(picker)
254 })
255 .unwrap();
256 Ok((&first.0, &first.1))
257}
258
259fn route_to_dialer(ctx: &AnyCtx<Config>, route: &RouteDescriptor) -> DynDialer {
260 use sillad_native_tls::TlsDialer;
261
262 match route {
263 RouteDescriptor::Tcp(addr) => {
264 smart_vpn_whitelist(ctx, addr.ip());
265 let addr = *addr;
266 TcpDialer { dest_addr: addr }.dynamic()
267 }
268 RouteDescriptor::Sosistab3 { cookie, lower } => {
269 let inner = route_to_dialer(ctx, lower);
270 SosistabDialer {
271 inner,
272 cookie: Cookie::new(cookie),
273 }
274 .dynamic()
275 }
276 RouteDescriptor::Race(inside) => inside
277 .iter()
278 .map(|s| route_to_dialer(ctx, s))
279 .reduce(|a, b| a.race(b).dynamic())
280 .unwrap_or_else(|| FailingDialer.dynamic()),
281 RouteDescriptor::Fallback(a) => a
282 .iter()
283 .map(|s| route_to_dialer(ctx, s))
284 .reduce(|a, b| a.fallback(b).dynamic())
285 .unwrap_or_else(|| FailingDialer.dynamic()),
286 RouteDescriptor::Timeout {
287 milliseconds,
288 lower,
289 } => route_to_dialer(ctx, lower)
290 .timeout(Duration::from_millis(*milliseconds as _))
291 .dynamic(),
292 RouteDescriptor::Delay {
293 milliseconds,
294 lower,
295 } => route_to_dialer(ctx, lower)
296 .delay(Duration::from_millis((*milliseconds).into()))
297 .dynamic(),
298 RouteDescriptor::ConnTest { ping_count, lower } => {
299 let lower = route_to_dialer(ctx, lower);
300 ConnTestDialer {
301 inner: lower,
302 ping_count: *ping_count as _,
303 }
304 .dynamic()
305 }
306 RouteDescriptor::Hex { lower } => {
307 let lower = route_to_dialer(ctx, lower);
308 HexDialer { inner: lower }.dynamic()
309 }
310 RouteDescriptor::Other(_) => FailingDialer.dynamic(),
311 RouteDescriptor::PlainTls { sni_domain, lower } => {
312 let lower = route_to_dialer(ctx, lower);
313 TlsDialer::new(
314 lower,
315 TlsConnector::new()
316 .use_sni(sni_domain.is_some())
317 .danger_accept_invalid_certs(true)
318 .danger_accept_invalid_hostnames(true)
319 .min_protocol_version(None)
320 .max_protocol_version(None),
321 sni_domain
322 .clone()
323 .unwrap_or_else(|| "example.com".to_string()),
324 )
325 .dynamic()
326 }
327 RouteDescriptor::Meeklike { key, cfg, lower } => {
328 let lower = route_to_dialer(ctx, lower);
329 MeeklikeDialer {
330 inner: lower.into(),
331 cfg: *cfg,
332 key: *blake3::hash(key.as_bytes()).as_bytes(),
333 }
334 .dynamic()
335 }
336 }
337}