erbridge-core 0.3.0

TCP/UDP forwarding and reverse tunnels over Noise, with a multi-tenant SNI-routed server, as a library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
//! Multi-tenant `serve`: many `connect` clients, each authenticated by its own
//! token and reached through its own SNI host.
//!
//! Two listeners run side by side:
//!
//! * the control listener accepts tunnel clients. The Noise handshake tries
//!   every known PSK ([`noise::accept_any`]); whichever verifies names the
//!   tenant, so a client never states who it is.
//! * the SNI listener accepts TLS connections, reads the ClientHello's SNI
//!   without decrypting anything, and relays the connection through the
//!   matching tenant's session.
//!
//! The tunnel name `connect` receives is always the tenant's own, decided
//! here, so a client cannot steer traffic at another tenant's target.

use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use anyhow::{Context, Result, anyhow};
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{OwnedSemaphorePermit, Semaphore, watch};
use tokio_util::compat::TokioAsyncReadCompatExt;
use tokio_util::sync::CancellationToken;

use crate::config::ServeConfig;
use crate::mux::{self, MuxControl};
use crate::noise;
use crate::noise::Psk;
use crate::reverse::enroll::Credentials;
use crate::reverse::serve::relay;
use crate::reverse::write_frame;
use crate::sni;
use crate::stats::Registry;
use crate::tenant::{self, EnrollCode, Identity, Tenant, TenantSet};
use crate::tenant_store::{FileStore, TenantStore};

/// How long a TLS client gets to send its ClientHello.
const SNI_TIMEOUT: Duration = Duration::from_secs(5);

/// How long a control connection gets to complete the Noise handshake.
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);

/// How many control connections may be mid-handshake at once. Each one costs a
/// trial decryption per known PSK, so the number is bounded.
const MAX_PENDING_HANDSHAKES: usize = 256;

/// How many of those one address may hold, so a single host cannot use up
/// every slot.
const MAX_PENDING_PER_IP: usize = 16;

/// How often the store is re-read for changes.
const DEFAULT_RELOAD_INTERVAL: Duration = Duration::from_secs(1);

/// Where a store-backed multi-tenant `serve` listens, and how often it checks
/// the store for changes.
#[derive(Debug, Clone)]
pub struct TenantServeConfig {
    /// Control address tunnel clients dial.
    pub listen: String,
    /// Address that accepts TLS connections and routes them by SNI.
    pub sni_listen: String,
    pub reload_interval: Duration,
}

struct Session {
    id: u64,
    /// The PSK this session authenticated with, to notice a rotation.
    psk: Psk,
    control: MuxControl,
    cancel: CancellationToken,
}

/// Locks `mutex`, carrying on if another task panicked while holding it. The
/// guarded maps are only ever mutated by single calls, so they are never left
/// half-updated, and one panic must not take every later connection down.
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
    mutex
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

struct Hub {
    store: Arc<dyn TenantStore>,
    set: watch::Sender<Arc<TenantSet>>,
    sessions: Mutex<HashMap<String, Session>>,
    next_session: AtomicU64,
    /// Per-tenant connection budgets, as (capacity, semaphore).
    budgets: Mutex<HashMap<String, (usize, Arc<Semaphore>)>>,
    /// Slots for connections that have not finished authenticating.
    handshakes: Arc<Semaphore>,
    /// Connections turned away because `handshakes` was exhausted.
    refused: AtomicU64,
    /// Connections mid-handshake, per source address.
    pending_by_ip: Mutex<HashMap<IpAddr, usize>>,
}

/// Holds one handshake slot, global and per address, until dropped.
struct PendingSlot<'a> {
    hub: &'a Hub,
    ip: IpAddr,
    _global: OwnedSemaphorePermit,
}

impl Drop for PendingSlot<'_> {
    fn drop(&mut self) {
        let mut by_ip = lock(&self.hub.pending_by_ip);
        if let Some(n) = by_ip.get_mut(&self.ip) {
            *n -= 1;
            if *n == 0 {
                by_ip.remove(&self.ip);
            }
        }
    }
}

impl Hub {
    /// Takes a handshake slot for `ip`, or `None` if the server or that
    /// address is at its limit.
    fn try_pending(&self, ip: IpAddr) -> Option<PendingSlot<'_>> {
        let global = self.handshakes.clone().try_acquire_owned().ok()?;
        let mut by_ip = lock(&self.pending_by_ip);
        let n = by_ip.entry(ip).or_insert(0);
        if *n >= MAX_PENDING_PER_IP {
            return None;
        }
        *n += 1;
        drop(by_ip);
        Some(PendingSlot {
            hub: self,
            ip,
            _global: global,
        })
    }
}

impl Hub {
    /// Takes one connection slot for `tenant`, or `None` if it is at its limit.
    ///
    /// A changed `max_conns` gets a fresh budget. Connections already open keep
    /// holding slots of the old one, so during the switch a tenant can briefly
    /// have a few more than either limit; that is the cost of not tracking
    /// individual connections.
    fn try_acquire(&self, tenant: &Tenant) -> Option<OwnedSemaphorePermit> {
        let semaphore = {
            let mut budgets = lock(&self.budgets);
            let entry = budgets
                .entry(tenant.name.clone())
                .or_insert_with(|| (tenant.max_conns, Arc::new(Semaphore::new(tenant.max_conns))));
            if entry.0 != tenant.max_conns {
                *entry = (tenant.max_conns, Arc::new(Semaphore::new(tenant.max_conns)));
            }
            entry.1.clone()
        };
        semaphore.try_acquire_owned().ok()
    }

    fn snapshot(&self) -> Arc<TenantSet> {
        self.set.borrow().clone()
    }
}

fn now_unix() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

pub(crate) async fn run_tenant_serve(
    cfg: ServeConfig,
    registry: Registry,
    shutdown: CancellationToken,
) -> Result<()> {
    let path = cfg
        .tenants_file
        .as_deref()
        .context("tenants_file is not set")?;
    let sni_listen = cfg.sni_listen.clone().context("sni_listen is not set")?;
    let tcfg = TenantServeConfig {
        listen: cfg.listen.clone(),
        sni_listen,
        reload_interval: DEFAULT_RELOAD_INTERVAL,
    };
    run_tenant_serve_with_store(tcfg, Arc::new(FileStore::new(path)), registry, shutdown).await
}

/// Runs multi-tenant `serve` against any [`TenantStore`]. Returns `Ok(())` once
/// `shutdown` is cancelled and every listener and session has been stopped.
pub async fn run_tenant_serve_with_store(
    cfg: TenantServeConfig,
    store: Arc<dyn TenantStore>,
    registry: Registry,
    shutdown: CancellationToken,
) -> Result<()> {
    let initial = store.snapshot()?;

    let control_listener = TcpListener::bind(&cfg.listen)
        .await
        .with_context(|| format!("binding control listener on {}", cfg.listen))?;
    let sni_listener = TcpListener::bind(&cfg.sni_listen)
        .await
        .with_context(|| format!("binding SNI listener on {}", cfg.sni_listen))?;
    registry.info(format!(
        "serve: multi-tenant, {} tenant(s); tunnel clients on {}, TLS by SNI on {}",
        initial.tenants().len(),
        cfg.listen,
        cfg.sni_listen
    ));

    let hub = Arc::new(Hub {
        store,
        set: watch::channel(Arc::new(initial)).0,
        sessions: Mutex::new(HashMap::new()),
        next_session: AtomicU64::new(1),
        budgets: Mutex::new(HashMap::new()),
        handshakes: Arc::new(Semaphore::new(MAX_PENDING_HANDSHAKES)),
        refused: AtomicU64::new(0),
        pending_by_ip: Mutex::new(HashMap::new()),
    });

    let control_loop = accept_loop(control_listener, &shutdown, &registry, "control", {
        let hub = hub.clone();
        let registry = registry.clone();
        let shutdown = shutdown.clone();
        move |sock, peer| {
            let (hub, registry, shutdown) = (hub.clone(), registry.clone(), shutdown.clone());
            async move { handle_control(hub, sock, peer, &registry, shutdown).await }
        }
    });
    let sni_loop = accept_loop(sni_listener, &shutdown, &registry, "sni", {
        let hub = hub.clone();
        let registry = registry.clone();
        move |sock, peer| {
            let (hub, registry) = (hub.clone(), registry.clone());
            async move { handle_sni(hub, sock, peer, &registry).await }
        }
    });
    let reload = reload_loop(
        hub.clone(),
        cfg.reload_interval,
        registry.clone(),
        shutdown.clone(),
    );
    tokio::join!(control_loop, sni_loop, reload);
    Ok(())
}

/// Accepts connections until `shutdown`, running `handler` for each in its own
/// task that also stops on shutdown.
async fn accept_loop<F, Fut>(
    listener: TcpListener,
    shutdown: &CancellationToken,
    registry: &Registry,
    what: &'static str,
    handler: F,
) where
    F: Fn(TcpStream, SocketAddr) -> Fut + Send + 'static,
    Fut: Future<Output = Result<()>> + Send + 'static,
{
    let accepting = async {
        loop {
            let (sock, peer) = match listener.accept().await {
                Ok(v) => v,
                Err(e) => {
                    registry.error(format!("serve: {what} accept failed: {e:#}"));
                    continue;
                }
            };
            let registry = registry.clone();
            let shutdown = shutdown.clone();
            let conn = handler(sock, peer);
            tokio::spawn(async move {
                if let Some(Err(e)) = shutdown.run_until_cancelled(conn).await {
                    registry.error(format!("serve: {what} connection {peer}: {e:#}"));
                }
            });
        }
    };
    shutdown.run_until_cancelled(accepting).await;
}

async fn handle_control(
    hub: Arc<Hub>,
    sock: TcpStream,
    peer: SocketAddr,
    registry: &Registry,
    shutdown: CancellationToken,
) -> Result<()> {
    let _ = sock.set_nodelay(true);
    let Some(pending) = hub.try_pending(peer.ip()) else {
        // Under a flood this would be one log line per connection, so report
        // the first and then every thousandth.
        let n = hub.refused.fetch_add(1, Ordering::Relaxed);
        if n == 0 || n % 1000 == 999 {
            registry.error(format!(
                "serve: {peer}: too many unauthenticated connections (server or per-address limit); refused {} so far",
                n + 1
            ));
        }
        return Ok(());
    };
    let candidates = hub.snapshot().candidates(now_unix());
    let handshake =
        tokio::time::timeout(HANDSHAKE_TIMEOUT, noise::accept_any(sock, &candidates.psks))
            .await
            .context("Noise handshake timed out")?;
    let (stream, index) = handshake.context("Noise handshake with tunnel client failed")?;
    drop(pending);

    match candidates.identity(index).clone() {
        Identity::Tenant(tenant) => {
            run_session(&hub, tenant, stream, peer, registry, shutdown).await
        }
        Identity::Code(code) => run_enrollment(&hub, code, stream, peer, registry).await,
    }
}

async fn run_session(
    hub: &Hub,
    tenant: Tenant,
    stream: noise::NoiseStream<TcpStream>,
    peer: SocketAddr,
    registry: &Registry,
    shutdown: CancellationToken,
) -> Result<()> {
    let cancel = shutdown.child_token();
    let (control, mut inbound_rx) =
        mux::spawn_with_shutdown(stream.compat(), mux::Mode::Client, cancel.clone());
    let own_cancel = cancel.clone();
    let id = hub.next_session.fetch_add(1, Ordering::Relaxed);

    // A new session takes over from any older one for the same tenant, so a
    // client reconnecting after a network drop is not locked out until the
    // dead connection times out.
    let replaced = lock(&hub.sessions).insert(
        tenant.name.clone(),
        Session {
            id,
            psk: tenant.psk,
            control,
            cancel,
        },
    );
    // `apply` publishes the new snapshot before it sweeps the sessions, so a
    // tenant removed while this handshake was running is caught by one of the
    // two: either the sweep sees this session, or this check sees the snapshot.
    let still_allowed = hub
        .snapshot()
        .tenant(&tenant.name)
        .is_some_and(|t| !t.disabled && t.psk == tenant.psk);
    if !still_allowed {
        own_cancel.cancel();
    }
    if let Some(old) = replaced {
        old.cancel.cancel();
        registry.info(format!(
            "serve: tenant '{}' reconnected from {peer}, replacing its previous session",
            tenant.name
        ));
    } else {
        registry.info(format!(
            "serve: tenant '{}' connected from {peer}",
            tenant.name
        ));
    }

    // B never opens streams back to A; drain so the mux driver keeps making
    // progress, ending once the connection (or this session) goes away.
    while inbound_rx.recv().await.is_some() {}

    let mut sessions = lock(&hub.sessions);
    if sessions.get(&tenant.name).is_some_and(|s| s.id == id) {
        sessions.remove(&tenant.name);
    }
    drop(sessions);
    registry.info(format!(
        "serve: tenant '{}' disconnected ({peer})",
        tenant.name
    ));
    Ok(())
}

async fn handle_sni(
    hub: Arc<Hub>,
    mut client: TcpStream,
    peer: SocketAddr,
    registry: &Registry,
) -> Result<()> {
    let peeked = sni::peek_sni(&mut client, SNI_TIMEOUT).await?;
    let host = peeked.sni.ok_or_else(|| anyhow!("no SNI in ClientHello"))?;

    let tenant = hub
        .snapshot()
        .tenant_by_sni(&host)
        .cloned()
        .ok_or_else(|| anyhow!("no tenant for host '{host}'"))?;
    let control = lock(&hub.sessions)
        .get(&tenant.name)
        .map(|s| s.control.clone())
        .ok_or_else(|| anyhow!("tenant '{}' is offline", tenant.name))?;

    // Held until the relay ends, which frees the slot.
    let _slot = hub.try_acquire(&tenant).ok_or_else(|| {
        anyhow!(
            "tenant '{}' is at its limit of {} connections",
            tenant.name,
            tenant.max_conns
        )
    })?;

    relay(
        client,
        peer,
        &control,
        &tenant.name,
        &peeked.buffered,
        registry,
    )
    .await
}

/// Re-reads the store every `interval` and applies what changed. A store that
/// cannot be read or parsed leaves the previous tenants in force; the error is
/// logged once per distinct message rather than on every tick.
async fn reload_loop(
    hub: Arc<Hub>,
    interval: Duration,
    registry: Registry,
    shutdown: CancellationToken,
) {
    let watching = async {
        let mut last_error: Option<String> = None;
        loop {
            tokio::time::sleep(interval).await;
            let store = hub.store.clone();
            let read = tokio::task::spawn_blocking(move || store.snapshot()).await;
            match read {
                Ok(Ok(next)) => {
                    last_error = None;
                    if *hub.snapshot() != next {
                        apply(&hub, next, &registry);
                    }
                }
                Ok(Err(e)) => {
                    let message = format!("{e:#}");
                    if last_error.as_deref() != Some(message.as_str()) {
                        registry.error(format!(
                            "serve: keeping the previous tenants, could not reload: {message}"
                        ));
                        last_error = Some(message);
                    }
                }
                Err(e) => registry.error(format!("serve: tenant reload task failed: {e}")),
            }
        }
    };
    shutdown.run_until_cancelled(watching).await;
}

/// Publishes a new snapshot and ends the session of every tenant that is gone,
/// disabled or holds a different PSK now. Other sessions are not touched.
fn apply(hub: &Hub, next: TenantSet, registry: &Registry) {
    let next = Arc::new(next);
    hub.set.send_replace(next.clone());

    let mut sessions = lock(&hub.sessions);
    sessions.retain(|name, session| {
        let still_valid = next
            .tenant(name)
            .is_some_and(|t| !t.disabled && t.psk == session.psk);
        if !still_valid {
            session.cancel.cancel();
            registry.info(format!(
                "serve: tenant '{name}' no longer allowed, session ended"
            ));
        }
        still_valid
    });
    drop(sessions);
    lock(&hub.budgets).retain(|name, _| next.tenant(name).is_some());
    registry.info(format!(
        "serve: reloaded tenants, {} now configured",
        next.tenants().len()
    ));
}

/// Redeems an enrollment code: creates the tenant, persists it, and sends the
/// credentials back inside the encrypted channel. The code is burned before the
/// credentials are sent, so a client that never receives them needs a new code;
/// the tenant created for it is withdrawn again.
async fn run_enrollment(
    hub: &Hub,
    code: EnrollCode,
    mut stream: noise::NoiseStream<TcpStream>,
    peer: SocketAddr,
    registry: &Registry,
) -> Result<()> {
    let name = tenant::new_tenant_name();
    let token = tenant::new_token();
    let psk = noise::derive_psk(&token);

    let store = hub.store.clone();
    let (code_psk, now, new_name) = (code.psk, now_unix(), name.clone());
    let created = tokio::task::spawn_blocking(move || store.redeem(&code_psk, now, new_name, psk))
        .await
        .context("enrollment task failed")??;

    // Let the new tenant connect right away instead of after the next reload.
    let store = hub.store.clone();
    if let Ok(Ok(next)) = tokio::task::spawn_blocking(move || store.snapshot()).await {
        apply(hub, next, registry);
    }

    let credentials = Credentials {
        name,
        token,
        sni: created.sni.clone(),
    };
    let sent = async {
        write_frame(&mut stream, &serde_json::to_vec(&credentials)?).await?;
        stream.shutdown().await?;
        anyhow::Ok(())
    }
    .await;
    if let Err(e) = sent {
        // The client never got the token, so nobody can use this tenant: take
        // it back rather than leave a live entry with no owner.
        let store = hub.store.clone();
        let orphan = created.name.clone();
        if let Ok(Ok(())) = tokio::task::spawn_blocking(move || store.revoke(&orphan)).await {
            let store = hub.store.clone();
            if let Ok(Ok(next)) = tokio::task::spawn_blocking(move || store.snapshot()).await {
                apply(hub, next, registry);
            }
        }
        return Err(e.context("sending credentials; the new tenant was withdrawn"));
    }
    registry.info(format!(
        "serve: enrolled tenant '{}' for {} from {peer}",
        created.name, created.sni
    ));
    Ok(())
}