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