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_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
45/// Gets a sillad Dialer that produces a single, pre-authentication pipe, as well as the public key.
46pub 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 the user specified a direct constraint, handle that path immediately:
82    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    // Otherwise, we need to pick an exit from the broker based on user constraints.
117    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    // Use our new helper function to pick the best exit:
135    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    // Also get potential “bridge routes”:
168    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
188/// A helper that filters the verified exits by the user’s `ExitConstraint`,
189/// then picks the exit with the lowest load.
190fn 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    // Figure out which fields we need to match
199    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    // If any matched, we use load-sensitive rendezvous hashing
241    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}