geph5_client/
get_dialer.rs

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