1use crate::host::session_file_rejection;
4
5mod desktop;
6mod policy;
7pub use policy::ConnectionPolicy;
8mod dispatch;
9mod responses;
10mod transport;
11mod view;
12mod voice;
13
14use crate::telemetry::{StopCause, Trigger};
15use std::collections::{BTreeMap, BTreeSet};
16use std::fs;
17use std::future::Future;
18use std::path::{Path, PathBuf};
19use std::sync::{Arc, Mutex};
20use std::time::{Duration, SystemTime, UNIX_EPOCH};
21
22use chrono::Utc;
23use mobius::agent::validate_submission;
24use mobius::backend::session_files::{PendingSessionFileWrite, SessionFileStore};
25use mobius::protocol::Op;
26use rustls::ServerConfig;
27use rustls::pki_types::{CertificateDer, PrivateKeyDer};
28use tokio::io::{AsyncRead, AsyncWrite};
29use tokio::net::{TcpListener, TcpStream};
30use tokio::sync::broadcast;
31use tokio::task::JoinSet;
32use tokio::time::Instant;
33use tokio_rustls::TlsAcceptor;
34use tokio_tungstenite::accept_hdr_async_with_config;
35use tokio_tungstenite::tungstenite::handshake::server::{
36 Callback, ErrorResponse, Request, Response,
37};
38use tokio_tungstenite::tungstenite::http::StatusCode;
39use tokio_tungstenite::tungstenite::http::header::{HOST, ORIGIN, SEC_WEBSOCKET_PROTOCOL};
40use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
41use uuid::Uuid;
42
43use crate::auth::{AuthStore, ClientIdentity, PairingGrant};
44use crate::bots::BotStore;
45use crate::config::{ConfigStore, CredentialStore, GatewayConfig, TlsConfig};
46use crate::host::{GatewayHost, HostHandle, Rejection};
47use crate::wire::{
48 CatalogHint, ClientFrame, ClientKind, ClientMessage, ClientStatus, DirectoryEntry,
49 DirectoryListing, FrameReader, GatewayNotification, MAX_FRAME_BYTES, ProfileSnapshot,
50 ServerFrame, ServerMessage, SharedFrame, read_frame, read_frame_with_limit, validate_version,
51 websocket_error, write_frame,
52};
53use crate::{Error, Result};
54
55use self::dispatch::*;
56use self::responses::*;
57use self::transport::*;
58use self::view::ClientView;
59
60const ROUTINE_TICK: Duration = Duration::from_secs(15);
61const MAX_DIRECTORY_ENTRIES: usize = 512;
62use crate::wire::WEBSOCKET_BRIDGE_BYTES;
63const ACCESS_EXPIRY_ENV: &str = "MOBIUS_GATEWAY_ACCESS_EXPIRES_AT";
64
65const _: () = assert!(MAX_FRAME_BYTES <= u32::MAX as usize);
66
67pub struct GatewayServer {
69 config: GatewayConfig,
70 listener: TcpListener,
71 access_lease: Option<AccessLease>,
72 auth: Arc<AuthStore>,
73 host: GatewayHost,
74 bots: Arc<BotStore>,
75 ready: Option<tokio::sync::oneshot::Sender<()>>,
76}
77
78impl GatewayServer {
79 pub async fn open(state_dir: PathBuf) -> Result<Self> {
84 let (store, config) = ConfigStore::open(state_dir)?;
85 let listener = TcpListener::bind(config.listen).await?;
86 Self::assemble(store, config, listener).await
87 }
88
89 pub async fn bootstrap(
94 state_dir: PathBuf,
95 listen: std::net::SocketAddr,
96 ) -> Result<(Self, PairingGrant)> {
97 let listener = TcpListener::bind(listen).await?;
98 let listen = listener.local_addr()?;
99 let (store, config) = ConfigStore::initialize(state_dir, listen, None)?;
100 let initialized_state = store.state_dir().to_path_buf();
101 let result = match AuthStore::initialize(store.auth_path(), config.auth) {
102 Ok((_, grant)) => Self::assemble(store, config, listener)
103 .await
104 .map(|server| (server, grant)),
105 Err(error) => Err(error),
106 };
107 match result {
108 Ok(result) => Ok(result),
109 Err(error) => {
110 fs::remove_dir_all(&initialized_state).map_err(|cleanup| {
111 Error::Config(format!(
112 "{error}; failed to remove incomplete gateway state at {}: {cleanup}",
113 initialized_state.display()
114 ))
115 })?;
116 Err(error)
117 }
118 }
119 }
120
121 async fn assemble(
122 store: ConfigStore,
123 config: GatewayConfig,
124 listener: TcpListener,
125 ) -> Result<Self> {
126 let access_lease = configured_access_lease(&config.runtime)?;
127 let auth = Arc::new(AuthStore::open(store.auth_path(), config.auth)?);
128 let credentials = Arc::new(CredentialStore::open(store.credentials_path())?);
129 let bots = Arc::new(BotStore::open(store.state_dir())?);
130 bots.sync_telemetry_cursors(&config.telemetry.sinks)?;
131 let host =
132 GatewayHost::start(store, config.clone(), credentials, Arc::clone(&bots)).await?;
133 Ok(Self {
134 config,
135 listener,
136 access_lease,
137 auth,
138 host,
139 bots,
140 ready: None,
141 })
142 }
143
144 pub(crate) fn notify_ready(&mut self) -> tokio::sync::oneshot::Receiver<()> {
145 let (sender, receiver) = tokio::sync::oneshot::channel();
146 self.ready = Some(sender);
147 receiver
148 }
149
150 pub async fn serve(self) -> Result<()> {
155 let websocket_host = self.configured_websocket_host()?;
156 self.serve_with_host(websocket_host).await
157 }
158
159 pub(crate) async fn serve_cloudflare(self, hostname: String) -> Result<()> {
161 let cloudflare = self.config.cloudflare.as_ref().ok_or_else(|| {
162 Error::Config("a Cloudflare hostname requires tunnel configuration".into())
163 })?;
164 if cloudflare
165 .hostname()
166 .is_some_and(|configured| configured != hostname)
167 {
168 return Err(Error::Config(
169 "runtime Cloudflare hostname does not match gateway configuration".into(),
170 ));
171 }
172 self.serve_with_host(Some(hostname)).await
173 }
174
175 async fn serve_with_host(self, websocket_host: Option<String>) -> Result<()> {
176 let inactivity_timeout = Duration::from_secs(self.config.runtime.idle_exit_seconds);
177 #[cfg(unix)]
178 {
179 use tokio::signal::unix::{SignalKind, signal};
180
181 let mut interrupts = signal(SignalKind::interrupt())?;
182 let mut terminations = signal(SignalKind::terminate())?;
183 self.serve_until_inactive_with_host(
184 async move {
185 tokio::select! {
186 _ = interrupts.recv() => {}
187 _ = terminations.recv() => {}
188 }
189 },
190 inactivity_timeout,
191 websocket_host,
192 )
193 .await
194 }
195 #[cfg(not(unix))]
196 self.serve_until_inactive_with_host(
197 async {
198 let _ = tokio::signal::ctrl_c().await;
199 },
200 inactivity_timeout,
201 websocket_host,
202 )
203 .await
204 }
205
206 pub async fn serve_until(self, shutdown: impl Future<Output = ()>) -> Result<()> {
215 let websocket_host = self.configured_websocket_host()?;
216 let inactivity_timeout = Duration::from_secs(self.config.runtime.idle_exit_seconds);
217 self.serve_until_inactive_with_host(shutdown, inactivity_timeout, websocket_host)
218 .await
219 }
220
221 #[cfg(test)]
222 async fn serve_until_inactive(
223 self,
224 shutdown: impl Future<Output = ()>,
225 inactivity_timeout: Duration,
226 ) -> Result<()> {
227 let websocket_host = self.configured_websocket_host()?;
228 self.serve_until_inactive_with_host(shutdown, inactivity_timeout, websocket_host)
229 .await
230 }
231
232 async fn serve_until_inactive_with_host(
233 mut self,
234 shutdown: impl Future<Output = ()>,
235 inactivity_timeout: Duration,
236 websocket_host: Option<String>,
237 ) -> Result<()> {
238 self.config.validate()?;
239 let tls = self.config.tls.as_ref().map(tls_acceptor).transpose()?;
240 if tls.is_none() && !self.listener.local_addr()?.ip().is_loopback() {
241 return Err(Error::Config(
242 "plaintext listeners are restricted to loopback".into(),
243 ));
244 }
245 let ingress = match self.config.runtime.ingress {
246 Some(address) => Some(TcpListener::bind(address).await?),
247 None => None,
248 };
249 let mut telemetry_tasks = JoinSet::new();
250 let mut stop_cause = StopCause::Signal;
251 let mut connections = JoinSet::new();
252 let mut routine_dispatchers = JoinSet::new();
253 let mut next_nudge = Instant::now();
254 let connection_admission = ConnectionAdmission::new(
255 self.config.connections.pre_authentication,
256 self.config.connections.authenticated,
257 );
258 let client_connections = Arc::new(ClientConnections::default());
259 let mut activity_hook =
260 crate::telemetry::ActivityHook::new(self.config.telemetry.activity_hook.as_ref())?;
261 let (client_revocations, _) = broadcast::channel(self.config.connections.total());
262 let mut has_active_routines = self.bots.has_active_routines(Utc::now().timestamp())?
263 || self.bots.has_pending_deliveries()?
264 || self.bots.has_monitored_sessions()?;
265 let inactivity = tokio::time::sleep(inactivity_timeout);
266 tokio::pin!(inactivity);
267 let mut routine_timer = tokio::time::interval(ROUTINE_TICK);
268 routine_timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
269 tokio::pin!(shutdown);
270 if let Some(ready) = self.ready.take() {
271 let _ = ready.send(());
272 }
273 crate::telemetry::Telemetry::tick(&self.host, 0, Trigger::Start, &mut telemetry_tasks)
274 .await;
275 let mut access_expired = false;
276 let serving = async {
277 loop {
278 tokio::select! {
279 biased;
280 _ = async {
281 tokio::time::sleep_until(next_nudge).await;
282 self.host.telemetry.notify.notified().await;
283 }, if routine_dispatchers.is_empty() => {
284 next_nudge = Instant::now() + Duration::from_secs(1);
285 let host = self.host.clone();
286 routine_dispatchers.spawn(async move {
287 if let Err(error) = host.dispatch_bot_events().await { eprintln!("Bot delivery failed: code={}", error.code); }
288 });
289 }
290 _ = routine_timer.tick() => {
291 if self.access_lease.is_some_and(AccessLease::expired) {
292 access_expired = true;
293 break Ok(());
294 }
295 self.tick_telemetry(&client_connections, Trigger::Interval, &mut telemetry_tasks).await;
296 let Ok(_admission) = self.host.begin_mutation().await else { continue; };
299 let now = Utc::now().timestamp();
300 let poll = self.bots.poll_due(now)?;
301 let routines_active = poll.active || self.bots.has_pending_deliveries()? || self.bots.has_monitored_sessions()?;
302 if has_active_routines && !routines_active && connections.is_empty() {
303 inactivity.as_mut().reset(tokio::time::Instant::now() + inactivity_timeout);
304 }
305 has_active_routines = routines_active;
306 let host = self.host.clone();
307 if routine_dispatchers.is_empty() {
309 routine_dispatchers.spawn(async move {
310 if let Err(error) = host.dispatch_bot_events().await { eprintln!("Bot delivery failed: code={}",error.code); }
311 });
312 }
313 let _ = poll.events;
316
317 }
318 Some(result) = telemetry_tasks.join_next(), if !telemetry_tasks.is_empty() => {
319 if let Err(error) = result { eprintln!("telemetry worker failed: {error}"); }
320 }
321 Some(_) = connections.join_next(), if !connections.is_empty() => {
322 if connections.is_empty() {
323 has_active_routines =
324 self.bots.has_active_routines(Utc::now().timestamp())? || self.bots.has_pending_deliveries()? || self.bots.has_monitored_sessions()?;
325 if !has_active_routines {
326 inactivity.as_mut().reset(tokio::time::Instant::now() + inactivity_timeout);
327 }
328 }
329 }
330 Some(_) = routine_dispatchers.join_next(), if !routine_dispatchers.is_empty() => {
331 self.tick_telemetry(&client_connections, Trigger::Interval, &mut telemetry_tasks).await;
332 }
333 accepted = async {
334 let admission = connection_admission.admit().await;
335 tokio::select! {
336 accepted = self.listener.accept() => accepted.map(|accepted| (accepted, admission, false)),
337 accepted = async { match &ingress { Some(listener) => listener.accept().await, None => std::future::pending().await } } => accepted.map(|accepted| (accepted, admission, true)),
338 }
339 }, if connections.len() < self.config.connections.total() => {
340 let ((stream, peer), admission, ingress_connection) = accepted?;
341 if self.access_lease.is_some_and(AccessLease::expired) {
342 access_expired = true;
343 break Ok(());
344 }
345 let auth = Arc::clone(&self.auth);
346 let host = self.host.clone();
347 let bots = Arc::clone(&self.bots);
348 let client_connections = Arc::clone(&client_connections);
349 let client_revocations = client_revocations.clone();
350 let tls = tls.clone();
351 let websocket_host = websocket_host.clone();
352 connections.spawn(async move {
353 let auth_deadline = Instant::now() + Duration::from_secs(self.config.connections.authentication_timeout_seconds);
354 let connection = ConnectionContext {
355 local: peer.ip().is_loopback(),
356 desktop_transport: tls.is_some(),
357 auth,
358 host,
359 bots,
360 client_connections,
361 client_revocations,
362 admission,
363 access_lease: self.access_lease,
364 };
365 let result = if ingress_connection {
366 serve_websocket(stream, connection, PlaintextHandshake { expected_websocket_host: None, auth_deadline }).await
367 } else if let Some(tls) = tls {
368 let stream = match tokio::time::timeout_at(
369 auth_deadline,
370 tls.accept(stream),
371 )
372 .await
373 {
374 Ok(Ok(stream)) => stream,
375 Ok(Err(error)) => {
376 eprintln!("gateway TLS handshake failed: {:?}", error.kind());
377 return;
378 }
379 Err(_) => {
380 eprintln!("gateway TLS handshake timed out");
381 return;
382 }
383 };
384 serve_connection(stream, connection, auth_deadline, None).await
385 } else {
386 serve_plaintext_connection(
387 stream,
388 connection,
389 PlaintextHandshake {
390 expected_websocket_host: websocket_host,
391 auth_deadline,
392 },
393 )
394 .await
395 };
396 if let Err(error) = result {
397 eprintln!("gateway connection failed: {}", connection_diagnostic(&error));
398 }
399 });
400 }
401 () = &mut inactivity, if connections.is_empty() && !has_active_routines && self.config.runtime.idle_exit_seconds != 0 => {
402 has_active_routines = self.bots.has_active_routines(Utc::now().timestamp())? || self.bots.has_pending_deliveries()? || self.bots.has_monitored_sessions()?;
403 if !has_active_routines {
404 if crate::telemetry::Telemetry::pending(&self.host).await {
405 inactivity.as_mut().reset(Instant::now() + ROUTINE_TICK);
406 continue;
407 }
408 stop_cause = StopCause::Idle;
409 break Ok(());
410 }
411 }
412 }
413 }
414 };
415 let (result, lease_expired) = {
416 let activity = activity_hook.run(&self.host, || client_connections.native_count());
419 tokio::pin!(activity, serving);
420 tokio::select! {
421 biased;
422 () = &mut shutdown => (Ok(()), false),
423 () = async {
424 match self.access_lease {
425 Some(lease) => tokio::time::sleep_until(lease.deadline).await,
426 None => std::future::pending().await,
427 }
428 } => (Ok(()), true),
429 () = &mut activity => (Err(Error::Config("telemetry activity worker stopped".into())), false),
430 result = &mut serving => (result, false),
431 }
432 };
433 access_expired |= lease_expired;
434 telemetry_tasks.shutdown().await;
436 connections.shutdown().await;
437 if access_expired {
438 routine_dispatchers.shutdown().await;
439 } else {
440 while routine_dispatchers.join_next().await.is_some() {}
441 }
442 self.host.shutdown().await;
443 let cause = if access_expired {
444 StopCause::LeaseExpired
445 } else if result.is_err() {
446 StopCause::Error
447 } else {
448 stop_cause
449 };
450 crate::telemetry::Telemetry::stop(&self.host, cause, &mut telemetry_tasks).await;
451 activity_hook.stop().await;
452 result
453 }
454
455 async fn tick_telemetry(
456 &self,
457 connections: &ClientConnections,
458 trigger: Trigger,
459 tasks: &mut JoinSet<()>,
460 ) {
461 match connections.native_count() {
462 Ok(clients) => {
463 crate::telemetry::Telemetry::tick(&self.host, clients, trigger, tasks).await
464 }
465 Err(error) => eprintln!("telemetry client count unavailable: {error}"),
466 }
467 }
468
469 fn configured_websocket_host(&self) -> Result<Option<String>> {
470 self.config
471 .cloudflare
472 .as_ref()
473 .map(|cloudflare| {
474 cloudflare.hostname().map(str::to_owned).ok_or_else(|| {
475 Error::Config(
476 "quick tunnel hostname is unavailable before cloudflared starts".into(),
477 )
478 })
479 })
480 .transpose()
481 }
482
483 #[must_use]
485 pub const fn listen_addr(&self) -> std::net::SocketAddr {
486 self.config.listen
487 }
488}
489
490#[derive(Clone, Copy)]
491struct AccessLease {
492 expires_at: SystemTime,
493 deadline: Instant,
494}
495
496impl AccessLease {
497 fn expired(self) -> bool {
498 SystemTime::now() >= self.expires_at || Instant::now() >= self.deadline
499 }
500}
501
502fn configured_access_lease(runtime: &crate::config::RuntimeConfig) -> Result<Option<AccessLease>> {
503 let expires_at = match std::env::var(ACCESS_EXPIRY_ENV) {
504 Ok(value) => Some(value),
505 Err(std::env::VarError::NotPresent) => None,
506 Err(_) => return Err(Error::Config("gateway access expiry is invalid".into())),
507 };
508 access_lease(
509 expires_at.as_deref(),
510 runtime.require_access_lease,
511 Duration::from_secs(runtime.access_grace_seconds),
512 SystemTime::now(),
513 )
514}
515
516fn access_lease(
517 expires_at: Option<&str>,
518 required: bool,
519 grace: Duration,
520 now: SystemTime,
521) -> Result<Option<AccessLease>> {
522 let Some(expires_at) = expires_at else {
523 return if required {
524 Err(Error::Config("gateway access expiry is required".into()))
525 } else {
526 Ok(None)
527 };
528 };
529 if expires_at.is_empty() || !expires_at.bytes().all(|byte| byte.is_ascii_digit()) {
530 return Err(Error::Config("gateway access expiry is invalid".into()));
531 }
532 let seconds = expires_at
533 .parse::<u64>()
534 .map_err(|_| Error::Config("gateway access expiry is invalid".into()))?;
535 let expires_at = UNIX_EPOCH
536 .checked_add(Duration::from_secs(seconds))
537 .and_then(|expiry| expiry.checked_add(grace))
538 .ok_or_else(|| Error::Config("gateway access expiry is invalid".into()))?;
539 let remaining = expires_at
540 .duration_since(now)
541 .map_err(|_| Error::Config("gateway access has expired".into()))?;
542 if remaining.is_zero() {
543 return Err(Error::Config("gateway access has expired".into()));
544 }
545 let deadline = Instant::now()
546 .checked_add(remaining)
547 .ok_or_else(|| Error::Config("gateway access expiry is invalid".into()))?;
548 Ok(Some(AccessLease {
549 expires_at,
550 deadline,
551 }))
552}
553
554#[cfg(test)]
555mod tests;