1use std::time::{Duration, 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, ExitDescriptor, ExitList, RouteDescriptor, DOMAIN_EXIT_DESCRIPTOR,
12};
13use isocountry::CountryCode;
14use ordered_float::OrderedFloat;
15use rand::seq::SliceRandom;
16use serde::{Deserialize, Serialize};
17use sillad::{
18 dialer::{DialerExt, DynDialer, FailingDialer},
19 tcp::TcpDialer,
20};
21use sillad_conntest::ConnTestDialer;
22use sillad_sosistab3::{dialer::SosistabDialer, Cookie};
23
24use smol_timeout2::TimeoutExt as _;
25
26use crate::{
27 auth::get_connect_token,
28 broker::broker_client, client::{Config, CtxField},
30 vpn::smart_vpn_whitelist,
31};
32
33#[derive(Serialize, Deserialize, Clone, Debug)]
34#[serde(rename_all = "snake_case")]
35pub enum ExitConstraint {
36 Auto,
37 Direct(String),
38 Hostname(String),
39 Country(CountryCode),
40 CountryCity(CountryCode, String),
41}
42
43pub async fn get_dialer(
45 ctx: &AnyCtx<Config>,
46) -> anyhow::Result<(VerifyingKey, ExitDescriptor, DynDialer)> {
47 static SEMAPH: CtxField<
48 smol::lock::Mutex<Option<(VerifyingKey, ExitDescriptor, DynDialer, SystemTime)>>,
49 > = |_| smol::lock::Mutex::new(None);
50 let mut cached_value = ctx.get(SEMAPH).lock().await;
51
52 if let Some(inner) = cached_value.clone() {
53 if inner.3.elapsed()? < Duration::from_secs(10) {
54 tracing::debug!("returning very recently cached dialer");
55 return Ok((inner.0, inner.1, inner.2));
56 }
57 }
58
59 let res = get_dialer_inner(ctx)
60 .timeout(Duration::from_secs(5))
61 .await
62 .ok_or_else(|| anyhow::anyhow!("get_dialer_inner timed out"))
63 .and_then(|x| x);
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 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 let (level, conn_token, sig) = get_connect_token(ctx)
121 .await
122 .context("could not get connect token")?;
123
124 let broker = broker_client(ctx).context("could not get broker client")?;
125 let exits_response = match level {
126 AccountLevel::Plus => broker.get_exits().await,
127 AccountLevel::Free => broker.get_free_exits().await,
128 }?
129 .map_err(|e| anyhow::anyhow!("broker refused to serve exits: {e}"))?;
130
131 let exits_verified = exits_response
133 .verify(DOMAIN_EXIT_DESCRIPTOR, |their_pk| {
134 if let Some(broker_pk) = &ctx.init().broker_keys {
135 hex::encode(their_pk.as_bytes()) == broker_pk.master
136 } else {
137 true
138 }
139 })
140 .context("could not verify exits")?;
141
142 let rendezvous_key = blake3::hash(serde_json::to_string(&ctx.init().credentials)?.as_bytes());
144 let (pubkey, exit) =
145 pick_exit_with_constraint(rendezvous_key, &ctx.init().exit_constraint, &exits_verified)?;
146
147 tracing::debug!(exit = ?exit, "narrowed down choice of exit");
148 smart_vpn_whitelist(ctx, exit.c2e_listen.ip());
149
150 let exit_c2e = exit.c2e_listen;
151 let direct_dialer = ConnTestDialer {
152 ping_count: 2,
153 inner: TcpDialer {
154 dest_addr: exit_c2e,
155 },
156 };
157
158 tracing::debug!(token = %conn_token, "CONN TOKEN");
159
160 let bridge_routes = broker
162 .get_routes(conn_token, sig, exit.b2e_listen)
163 .await?
164 .map_err(|e| anyhow::anyhow!("broker refused to serve bridge routes: {e}"))?;
165 tracing::debug!(
166 "bridge routes obtained: {}",
167 serde_json::to_string(&bridge_routes)?
168 );
169
170 let bridge_dialer = route_to_dialer(ctx, &bridge_routes);
171
172 let final_dialer = match ctx.init().bridge_mode {
173 crate::BridgeMode::Auto => direct_dialer
174 .race(bridge_dialer.delay(Duration::from_millis(1000)))
175 .dynamic(),
176 crate::BridgeMode::ForceBridges => bridge_dialer,
177 crate::BridgeMode::ForceDirect => direct_dialer.dynamic(),
178 };
179
180 Ok((*pubkey, exit.clone(), final_dialer))
181}
182
183fn pick_exit_with_constraint<'a>(
186 rendezvous_key: blake3::Hash,
187 constraint: &ExitConstraint,
188 exits_verified: &'a ExitList,
189) -> anyhow::Result<(&'a VerifyingKey, &'a ExitDescriptor)> {
190 let all_exits = &exits_verified.all_exits;
192
193 let mut country_constraint = None;
195 let mut city_constraint = None;
196 let mut hostname_constraint = None;
197
198 match constraint {
199 ExitConstraint::Hostname(host) => {
200 hostname_constraint = Some(host.clone());
201 }
202 ExitConstraint::Country(country) => {
203 country_constraint = Some(*country);
204 }
205 ExitConstraint::CountryCity(country, city) => {
206 country_constraint = Some(*country);
207 city_constraint = Some(city.clone());
208 }
209 ExitConstraint::Auto => {}
210 ExitConstraint::Direct(_) => panic!("should not reach here"),
211 }
212
213 let filtered = all_exits
215 .iter()
216 .filter(|(_, exit)| {
217 let country_pass = match country_constraint {
218 Some(c) => exit.country == c,
219 None => true,
220 };
221 let city_pass = match &city_constraint {
222 Some(city) => exit.city == *city,
223 None => true,
224 };
225 let hostname_pass = match &hostname_constraint {
226 Some(hn) => exit.b2e_listen.ip().to_string() == *hn,
227 None => true,
228 };
229 country_pass && city_pass && hostname_pass
230 })
231 .collect::<Vec<_>>();
232
233 if filtered.is_empty() {
234 anyhow::bail!("no exits match the constraints")
235 }
236
237 let first = filtered
240 .iter()
241 .min_by_key(|rh| {
242 let hash = blake3::keyed_hash(rendezvous_key.as_bytes(), &rh.0.as_bytes()[..]);
243 let hash = &hash.as_bytes()[..];
244 let hash = u64::from_be_bytes(*array_ref![hash, 0, 8]) as f64 / u64::MAX as f64;
245 let picker = -hash.ln() / (rh.1.load as f64);
246 tracing::debug!(
247 "picking exit, {}/{}/{} => {:.5}",
248 rh.1.country,
249 rh.1.city,
250 rh.1.b2e_listen.ip(),
251 hash
252 );
253 OrderedFloat(picker)
254 })
255 .unwrap();
256 Ok((&first.0, &first.1))
257}
258
259fn route_to_dialer(ctx: &AnyCtx<Config>, route: &RouteDescriptor) -> DynDialer {
260 use sillad_native_tls::TlsDialer;
261
262 match route {
263 RouteDescriptor::Tcp(addr) => {
264 smart_vpn_whitelist(ctx, addr.ip());
265 let addr = *addr;
266 TcpDialer { dest_addr: addr }.dynamic()
267 }
268 RouteDescriptor::Sosistab3 { cookie, lower } => {
269 let inner = route_to_dialer(ctx, lower);
270 SosistabDialer {
271 inner,
272 cookie: Cookie::new(cookie),
273 }
274 .dynamic()
275 }
276 RouteDescriptor::Race(inside) => inside
277 .iter()
278 .map(|s| route_to_dialer(ctx, s))
279 .reduce(|a, b| a.race(b).dynamic())
280 .unwrap_or_else(|| FailingDialer.dynamic()),
281 RouteDescriptor::Fallback(a) => a
282 .iter()
283 .map(|s| route_to_dialer(ctx, s))
284 .reduce(|a, b| a.fallback(b).dynamic())
285 .unwrap_or_else(|| FailingDialer.dynamic()),
286 RouteDescriptor::Timeout {
287 milliseconds,
288 lower,
289 } => route_to_dialer(ctx, lower)
290 .timeout(Duration::from_millis(*milliseconds as _))
291 .dynamic(),
292 RouteDescriptor::Delay {
293 milliseconds,
294 lower,
295 } => route_to_dialer(ctx, lower)
296 .delay(Duration::from_millis((*milliseconds).into()))
297 .dynamic(),
298 RouteDescriptor::ConnTest { ping_count, lower } => {
299 let lower = route_to_dialer(ctx, lower);
300 ConnTestDialer {
301 inner: lower,
302 ping_count: *ping_count as _,
303 }
304 .dynamic()
305 }
306
307 RouteDescriptor::Other(_) => FailingDialer.dynamic(),
308 RouteDescriptor::PlainTls { sni_domain, lower } => {
309 let lower = route_to_dialer(ctx, lower);
310 TlsDialer::new(
311 lower,
312 TlsConnector::new()
313 .use_sni(sni_domain.is_some())
314 .danger_accept_invalid_certs(true)
315 .danger_accept_invalid_hostnames(true)
316 .min_protocol_version(None)
317 .max_protocol_version(None),
318 sni_domain
319 .clone()
320 .unwrap_or_else(|| "example.com".to_string()),
321 )
322 .dynamic()
323 }
324 }
325}