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