geph5_client/
route.rs

1use std::time::{Duration, Instant, SystemTime};
2
3use anyctx::AnyCtx;
4use anyhow::Context;
5
6use async_native_tls::TlsConnector;
7use ed25519_dalek::VerifyingKey;
8
9use geph5_broker_protocol::{
10    AccountLevel, ExitDescriptor, RouteDescriptor, DOMAIN_EXIT_DESCRIPTOR,
11};
12use isocountry::CountryCode;
13use rand::seq::SliceRandom;
14use serde::{Deserialize, Serialize};
15use sillad::{
16    dialer::{DialerExt, DynDialer, FailingDialer},
17    tcp::TcpDialer,
18};
19use sillad_conntest::ConnTestDialer;
20use sillad_sosistab3::{dialer::SosistabDialer, Cookie};
21use smol::lock::Semaphore;
22use smol_timeout2::TimeoutExt as _;
23
24use crate::{
25    auth::get_connect_token,
26    broker::broker_client,
27    client::{Config, CtxField},
28    vpn::smart_vpn_whitelist,
29};
30
31#[derive(Serialize, Deserialize, Clone, Debug)]
32#[serde(rename_all = "snake_case")]
33pub enum ExitConstraint {
34    Auto,
35    Direct(String),
36    Hostname(String),
37    Country(CountryCode),
38    CountryCity(CountryCode, String),
39}
40
41/// Gets a sillad Dialer that produces a single, pre-authentication pipe, as well as the public key.
42pub async fn get_dialer(
43    ctx: &AnyCtx<Config>,
44) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
45    static SEMAPH: CtxField<
46        smol::lock::Mutex<Option<(VerifyingKey, ExitDescriptor, DynDialer, SystemTime)>>,
47    > = |_| smol::lock::Mutex::new(None);
48    let mut cached_value = ctx.get(SEMAPH).lock().await;
49
50    if let Some(inner) = cached_value.clone() {
51        if inner.3.elapsed()? < Duration::from_secs(10) {
52            tracing::debug!("returning very recently cached dialer");
53            return Ok((inner.0, inner.1, inner.2));
54        }
55    }
56
57    let res = get_dialer_inner(ctx)
58        .timeout(Duration::from_secs(5))
59        .await
60        .ok_or_else(|| anyhow::anyhow!("get_dialer_inner timed out"))
61        .and_then(|x| x);
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    let mut country_constraint = None;
83    let mut city_constraint = None;
84    let mut hostname_constraint = None;
85    match &ctx.init().exit_constraint {
86        ExitConstraint::Direct(dir) => {
87            let (dir, pubkey) = dir
88                .split_once('/')
89                .context("did not find / in a direct constraint")?;
90            let pubkey = VerifyingKey::from_bytes(
91                hex::decode(pubkey)
92                    .context("cannot decode pubkey as hex")?
93                    .as_slice()
94                    .try_into()
95                    .context("pubkey wrong length")?,
96            )?;
97            let dest_addr = *smol::net::resolve(dir)
98                .await?
99                .choose(&mut rand::thread_rng())
100                .context("could not resolve destination for direct exit connection")?;
101            smart_vpn_whitelist(ctx, dest_addr.ip());
102            return Ok((
103                pubkey,
104                ExitDescriptor {
105                    c2e_listen: "0.0.0.0:0".parse()?,
106                    b2e_listen: "0.0.0.0:0".parse()?,
107                    country: CountryCode::ABW,
108                    city: "".to_string(),
109                    load: 0.0,
110                    expiry: 0,
111                },
112                ConnTestDialer {
113                    ping_count: 1,
114                    inner: TcpDialer { dest_addr },
115                }
116                .dynamic(),
117            ));
118        }
119        ExitConstraint::Country(country) => country_constraint = Some(*country),
120        ExitConstraint::CountryCity(country, city) => {
121            country_constraint = Some(*country);
122            city_constraint = Some(city.clone())
123        }
124        ExitConstraint::Hostname(hostname) => {
125            hostname_constraint = Some(hostname.clone());
126        }
127        ExitConstraint::Auto => {}
128    }
129
130    // First get the conn token
131    let (level, conn_token, sig) = get_connect_token(ctx)
132        .await
133        .context("could not get connect token")?;
134
135    let broker = broker_client(ctx).context("could not get broker client")?;
136    let exits = match level {
137        AccountLevel::Plus => broker.get_exits().await,
138        AccountLevel::Free => broker.get_free_exits().await,
139    }?
140    .map_err(|e| anyhow::anyhow!("broker refused to serve exits: {e}"))?;
141
142    let exits = exits
143        .verify(DOMAIN_EXIT_DESCRIPTOR, |their_pk| {
144            if let Some(broker_pk) = &ctx.init().broker_keys {
145                hex::encode(their_pk.as_bytes()) == broker_pk.master
146            } else {
147                true
148            }
149        })
150        .context("could not verify")?;
151    // filter for things that fit
152    let (pubkey, exit) = if let Some(min) = exits
153        .all_exits
154        .iter()
155        .filter(|(_, exit)| {
156            let country_pass = if let Some(country) = &country_constraint {
157                exit.country == *country
158            } else {
159                true
160            };
161            let city_pass = if let Some(city) = &city_constraint {
162                &exit.city == city
163            } else {
164                true
165            };
166            let hostname_pass = if let Some(hostname) = &hostname_constraint {
167                &exit.b2e_listen.ip().to_string() == hostname
168            } else {
169                true
170            };
171            country_pass && city_pass && hostname_pass
172        })
173        .min_by_key(|e| (e.1.load * 1000.0) as u64)
174    {
175        min
176    } else {
177        exits
178            .all_exits
179            .iter()
180            .min_by_key(|e| (e.1.load * 1000.0) as u64)
181            .context("no exits that fit the criterion")?
182    };
183
184    tracing::debug!(exit = debug(&exit), "narrowed down choice of exit");
185    smart_vpn_whitelist(ctx, exit.c2e_listen.ip());
186
187    let exit_c2e = exit.c2e_listen;
188    let direct_dialer = ConnTestDialer {
189        ping_count: 2,
190        inner: TcpDialer {
191            dest_addr: exit_c2e,
192        },
193    };
194
195    tracing::debug!(token = display(&conn_token), "CONN TOKEN");
196
197    // also get bridges
198    let bridge_routes = broker
199        .get_routes(conn_token, sig, exit.b2e_listen)
200        .await?
201        .map_err(|e| anyhow::anyhow!("broker refused to serve bridge routes: {e}"))?;
202    tracing::debug!(
203        "bridge routes obtained: {}",
204        serde_yaml::to_string(&serde_json::to_value(&bridge_routes)?)?
205    );
206
207    let bridge_dialer = route_to_dialer(ctx, &bridge_routes);
208
209    let final_dialer = match ctx.init().bridge_mode {
210        crate::BridgeMode::Auto => direct_dialer
211            .race(bridge_dialer.delay(Duration::from_millis(1000)))
212            .dynamic(),
213        crate::BridgeMode::ForceBridges => bridge_dialer,
214        crate::BridgeMode::ForceDirect => direct_dialer.dynamic(),
215    };
216
217    Ok((*pubkey, exit.clone(), final_dialer))
218}
219
220// async fn reachability_test(
221//     ctx: AnyCtx<Config>,
222//     dialers: BTreeMap<String, DynDialer>,
223// ) -> anyhow::Result<()> {
224//     let nfo = IP_INFO.get().unwrap();
225//     let country = nfo["country"].as_str().context("country code not found")?;
226//     let asn = nfo["org"]
227//         .as_str()
228//         .context("no org")?
229//         .split_ascii_whitespace()
230//         .next()
231//         .unwrap();
232
233//     for (name, dialer) in dialers {
234//         tracing::debug!(name = display(&name), "doing a reachability test");
235//         let ctx = ctx.clone();
236//         smolscale::spawn(async move {
237//             let broker = broker_client(&ctx).context("could not get broker client")?;
238//             let success = if let Err(err) = dialer.timeout(Duration::from_secs(10)).dial().await {
239//                 tracing::warn!(
240//                     name = display(&name),
241//                     err = debug(err),
242//                     "could not reach something"
243//                 );
244//                 false
245//             } else {
246//                 true
247//             };
248//             broker
249//                 .upload_available(AvailabilityData {
250//                     listen: name,
251//                     country: country.to_string(),
252//                     asn: asn.to_string(),
253//                     success,
254//                 })
255//                 .await?;
256//             anyhow::Ok(())
257//         })
258//         .detach();
259//     }
260
261//     Ok(())
262// }
263
264// fn route_to_flat_dialers(route: &RouteDescriptor) -> BTreeMap<String, DynDialer> {
265//     match route {
266//         RouteDescriptor::Tcp(socket_addr) => std::iter::once((
267//             socket_addr.ip().to_string(),
268//             TcpDialer {
269//                 dest_addr: *socket_addr,
270//             }
271//             .dynamic(),
272//         ))
273//         .collect(),
274//         RouteDescriptor::Sosistab3 { cookie, lower } => route_to_flat_dialers(lower)
275//             .into_iter()
276//             .map(|(k, inner)| {
277//                 (
278//                     k,
279//                     SosistabDialer {
280//                         inner,
281//                         cookie: Cookie::new(cookie),
282//                     }
283//                     .dynamic(),
284//                 )
285//             })
286//             .collect(),
287//         RouteDescriptor::Race(vec) | RouteDescriptor::Fallback(vec) => vec
288//             .iter()
289//             .flat_map(|v| route_to_flat_dialers(v).into_iter())
290//             .collect(),
291//         RouteDescriptor::Timeout {
292//             milliseconds: _,
293//             lower,
294//         }
295//         | RouteDescriptor::Delay {
296//             milliseconds: _,
297//             lower,
298//         } => route_to_flat_dialers(lower),
299
300//         _ => BTreeMap::new(),
301//     }
302// }
303
304fn route_to_dialer(ctx: &AnyCtx<Config>, route: &RouteDescriptor) -> DynDialer {
305    match route {
306        RouteDescriptor::Tcp(addr) => {
307            smart_vpn_whitelist(ctx, addr.ip());
308            let addr = *addr;
309            TcpDialer { dest_addr: addr }.dynamic()
310        }
311        RouteDescriptor::Sosistab3 { cookie, lower } => {
312            let inner = route_to_dialer(ctx, lower);
313            SosistabDialer {
314                inner,
315                cookie: Cookie::new(cookie),
316            }
317            .dynamic()
318        }
319        RouteDescriptor::Race(inside) => inside
320            .iter()
321            .map(|s| route_to_dialer(ctx, s))
322            .reduce(|a, b| a.race(b).dynamic())
323            .unwrap_or_else(|| FailingDialer.dynamic()),
324        RouteDescriptor::Fallback(a) => a
325            .iter()
326            .map(|s| route_to_dialer(ctx, s))
327            .reduce(|a, b| a.fallback(b).dynamic())
328            .unwrap_or_else(|| FailingDialer.dynamic()),
329        RouteDescriptor::Timeout {
330            milliseconds,
331            lower,
332        } => route_to_dialer(ctx, lower)
333            .timeout(Duration::from_millis(*milliseconds as _))
334            .dynamic(),
335        RouteDescriptor::Delay {
336            milliseconds,
337            lower,
338        } => route_to_dialer(ctx, lower)
339            .delay(Duration::from_millis((*milliseconds).into()))
340            .dynamic(),
341        RouteDescriptor::ConnTest { ping_count, lower } => {
342            let lower = route_to_dialer(ctx, lower);
343            ConnTestDialer {
344                inner: lower,
345                ping_count: *ping_count as _,
346            }
347            .dynamic()
348        }
349
350        RouteDescriptor::Other(_) => FailingDialer.dynamic(),
351        RouteDescriptor::PlainTls { sni_domain, lower } => {
352            let lower = route_to_dialer(ctx, lower);
353            sillad_native_tls::TlsDialer::new(
354                lower,
355                TlsConnector::new()
356                    .use_sni(sni_domain.is_some())
357                    .danger_accept_invalid_certs(true)
358                    .danger_accept_invalid_hostnames(true)
359                    .min_protocol_version(None)
360                    .max_protocol_version(None),
361                sni_domain
362                    .clone()
363                    .unwrap_or_else(|| "example.com".to_string()),
364            )
365            .dynamic()
366        }
367    }
368}