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::{Cookie, dialer::SosistabDialer};
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        && inner.3.elapsed()? < Duration::from_secs(120) {
57            return Ok((inner.0, inner.1, inner.2));
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    let start = Instant::now();
135    let metadata = match get_device_metadata(ctx).await {
136        Ok(metadata) => {
137            tracing::info!(
138                metadata = debug(&metadata),
139                elapsed = debug(start.elapsed()),
140                "DEVICE METADATA OBTAINED"
141            );
142            serde_json::to_value(&metadata)?
143        }
144        Err(err) => {
145            tracing::warn!(
146                err = debug(err),
147                "CANNOT GET DEVICE METADATA, PROCEEDING NONETHELESS"
148            );
149            serde_json::Value::Null
150        }
151    };
152
153    // Use our new helper function to pick the best exit:
154    let rendezvous_key = blake3::hash(serde_json::to_string(&ctx.init().credentials)?.as_bytes());
155    let (pubkey, exit) = pick_exit_with_constraint(
156        rendezvous_key,
157        &ctx.init().exit_constraint,
158        level,
159        &net_status_verified,
160    )?;
161
162    tracing::debug!(exit = ?exit, "narrowed down choice of exit");
163    smart_vpn_whitelist(ctx, exit.c2e_listen.ip());
164
165    tracing::debug!(token = %conn_token, "CONN TOKEN");
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
179    tracing::debug!(
180        "bridge routes obtained: {}",
181        serde_json::to_string(&bridge_routes)?
182    );
183
184    let combined_routes = if ctx.init().allow_direct {
185        RouteDescriptor::Race(vec![
186            RouteDescriptor::ConnTest {
187                ping_count: 1,
188                lower: Box::new(RouteDescriptor::Tcp(exit.c2e_listen)),
189            },
190            RouteDescriptor::Delay {
191                milliseconds: 1000,
192                lower: Box::new(bridge_routes),
193            },
194        ])
195    } else {
196        bridge_routes
197    };
198    let bridge_dialer = route_to_dialer(ctx, &combined_routes);
199
200    Ok((*pubkey, exit.clone(), bridge_dialer))
201}
202
203/// A helper that filters the verified exits by the user’s `ExitConstraint`,
204/// then picks the exit with the lowest load.
205fn pick_exit_with_constraint<'a>(
206    rendezvous_key: blake3::Hash,
207    constraint: &ExitConstraint,
208    level: AccountLevel,
209    net_status: &'a NetStatus,
210) -> anyhow::Result<(&'a VerifyingKey, &'a ExitDescriptor)> {
211    let all_exits = net_status.exits.values();
212
213    // Figure out which fields we need to match
214    let mut country_constraint = None;
215    let mut city_constraint = None;
216    let mut hostname_constraint = None;
217
218    match constraint {
219        ExitConstraint::Hostname(host) => hostname_constraint = Some(host.clone()),
220        ExitConstraint::Country(country) => country_constraint = Some(*country),
221        ExitConstraint::CountryCity(country, city) => {
222            country_constraint = Some(*country);
223            city_constraint = Some(city.clone());
224        }
225        ExitConstraint::Auto => {}
226        ExitConstraint::Direct(_) => panic!("should not reach here"),
227    }
228
229    let filtered: Vec<_> = all_exits
230        .filter(|(_, exit, meta)| {
231            let mut pass = if let Some(c) = country_constraint {
232                exit.country == c
233            } else {
234                true
235            };
236            pass &= match &city_constraint {
237                Some(city) => exit.city == *city,
238                None => true,
239            };
240            pass &= match &hostname_constraint {
241                Some(hn) => exit.b2e_listen.ip().to_string() == *hn,
242                None => true,
243            };
244            if matches!(constraint, ExitConstraint::Auto) {
245                pass &= meta.category == ExitCategory::Core;
246            }
247            pass &= meta.allowed_levels.contains(&level);
248            pass
249        })
250        .collect();
251
252    if filtered.is_empty() {
253        anyhow::bail!("no exits match the constraints")
254    }
255
256    // If any matched, we use load-sensitive rendezvous hashing
257    let first = filtered
258        .iter()
259        .min_by_key(|rh| {
260            let (_, exit, _) = **rh;
261            let hash = blake3::keyed_hash(
262                rendezvous_key.as_bytes(),
263                exit.b2e_listen.ip().to_string().as_bytes(),
264            );
265            let hash = &hash.as_bytes()[..];
266            let hash = u64::from_be_bytes(*array_ref![hash, 0, 8]) as f64 / u64::MAX as f64;
267            let weight = (1.0 - (exit.load as f64)).powi(2);
268            let picker = -hash.ln() / weight;
269            OrderedFloat(picker)
270        })
271        .unwrap();
272    Ok((&first.0, &first.1))
273}
274
275fn route_to_dialer(ctx: &AnyCtx<Config>, route: &RouteDescriptor) -> DynDialer {
276    use sillad_native_tls::TlsDialer;
277
278    match route {
279        RouteDescriptor::Tcp(addr) => {
280            smart_vpn_whitelist(ctx, addr.ip());
281            let addr = *addr;
282            // if addr.ip() != IpAddr::V4(Ipv4Addr::new(175, 29, 23, 236))
283            // && addr.ip() != IpAddr::V4(Ipv4Addr::new(175, 29, 23, 240))
284            // if addr.ip() != IpAddr::V4(Ipv4Addr::new(207, 148, 98, 13)) {
285            //     FailingDialer.dynamic()
286            // } else {
287            TcpDialer { dest_addr: addr }.dynamic()
288            // }
289        }
290        RouteDescriptor::Sosistab3 { cookie, lower } => {
291            let inner = route_to_dialer(ctx, lower);
292            SosistabDialer {
293                inner,
294                cookie: Cookie::new(cookie),
295            }
296            .dynamic()
297        }
298        RouteDescriptor::Race(inside) => inside
299            .iter()
300            .map(|s| route_to_dialer(ctx, s))
301            .reduce(|a, b| a.race(b).dynamic())
302            .unwrap_or_else(|| FailingDialer.dynamic()),
303        RouteDescriptor::Fallback(a) => a
304            .iter()
305            .map(|s| route_to_dialer(ctx, s))
306            .reduce(|a, b| a.fallback(b).dynamic())
307            .unwrap_or_else(|| FailingDialer.dynamic()),
308        RouteDescriptor::Timeout {
309            milliseconds,
310            lower,
311        } => route_to_dialer(ctx, lower)
312            .timeout(Duration::from_millis(*milliseconds as _))
313            .dynamic(),
314        RouteDescriptor::Delay {
315            milliseconds,
316            lower,
317        } => route_to_dialer(ctx, lower)
318            .delay(Duration::from_millis((*milliseconds).into()))
319            .dynamic(),
320        RouteDescriptor::ConnTest { ping_count, lower } => {
321            let lower = route_to_dialer(ctx, lower);
322            ConnTestDialer {
323                inner: lower,
324                ping_count: *ping_count as _,
325            }
326            .dynamic()
327        }
328        RouteDescriptor::Hex { lower } => {
329            let lower = route_to_dialer(ctx, lower);
330            HexDialer { inner: lower }.dynamic()
331        }
332        RouteDescriptor::Other(_) => FailingDialer.dynamic(),
333        RouteDescriptor::PlainTls { sni_domain, lower } => {
334            let lower = route_to_dialer(ctx, lower);
335            TlsDialer::new(
336                lower,
337                TlsConnector::new()
338                    .use_sni(sni_domain.is_some())
339                    .danger_accept_invalid_certs(true)
340                    .danger_accept_invalid_hostnames(true)
341                    .min_protocol_version(None)
342                    .max_protocol_version(None),
343                sni_domain
344                    .clone()
345                    .unwrap_or_else(|| "example.com".to_string()),
346            )
347            .dynamic()
348        }
349        RouteDescriptor::Meeklike { key, cfg, lower } => {
350            let lower = route_to_dialer(ctx, lower);
351            MeeklikeDialer {
352                inner: lower.into(),
353                cfg: *cfg,
354                key: *blake3::hash(key.as_bytes()).as_bytes(),
355            }
356            .dynamic()
357        }
358    }
359}