geph5_client/
get_dialer.rs

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