geph5_client/
get_dialer.rs

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
43/// Gets a sillad Dialer that produces a single, pre-authentication pipe, as well as the public key.
44pub 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 the user specified a direct constraint, handle that path immediately:
80    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    // Otherwise, we need to pick an exit from the broker based on user constraints.
115    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    // Use our new helper function to pick the best exit:
133    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    // Also get potential “bridge routes”:
160    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
180/// A helper that filters the verified exits by the user’s `ExitConstraint`,
181/// then picks the exit with the lowest load.
182fn 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    // Figure out which fields we need to match
191    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    // If any matched, we use load-sensitive rendezvous hashing
233    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}