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