From 08c672c024bad7ab8e42da4d50ec520065766eff Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 4 Mar 2026 16:29:02 -0700 Subject: [PATCH] passthrough feature --- build/image-recipe/build.sh | 2 +- build/manage-release.sh | 46 ++++- core/src/db/model/public.rs | 5 +- core/src/net/acme.rs | 10 +- core/src/net/host/mod.rs | 28 +++ core/src/net/net_controller.rs | 68 ++++++- core/src/net/ssl.rs | 5 +- core/src/net/tls.rs | 99 ++++++---- core/src/net/vhost.rs | 335 ++++++++++++++++++++++++++++----- core/src/tunnel/web.rs | 6 +- 10 files changed, 500 insertions(+), 104 deletions(-) diff --git a/build/image-recipe/build.sh b/build/image-recipe/build.sh index eb5b7fff2..8bd27daf3 100755 --- a/build/image-recipe/build.sh +++ b/build/image-recipe/build.sh @@ -105,7 +105,7 @@ lb config \ --iso-preparer "START9 LABS; HTTPS://START9.COM" \ --iso-publisher "START9 LABS; HTTPS://START9.COM" \ --backports true \ - --bootappend-live "boot=live noautologin" \ + --bootappend-live "boot=live noautologin console=tty0" \ --bootloaders $BOOTLOADERS \ --cache false \ --mirror-bootstrap "https://deb.debian.org/debian/" \ diff --git a/build/manage-release.sh b/build/manage-release.sh index bd98dfbd1..c3b71717a 100755 --- a/build/manage-release.sh +++ b/build/manage-release.sh @@ -11,10 +11,22 @@ START9_GPG_KEY="2D63C217" ARCHES="aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree x86_64 x86_64-nonfree x86_64-nvidia" CLI_ARCHES="aarch64 riscv64 x86_64" +parse_run_id() { + local val="$1" + if [[ "$val" =~ /actions/runs/([0-9]+) ]]; then + echo "${BASH_REMATCH[1]}" + else + echo "$val" + fi +} + require_version() { - if [ -z "$VERSION" ]; then - >&2 echo '$VERSION required' - exit 2 + if [ -z "${VERSION:-}" ]; then + read -rp "VERSION: " VERSION + if [ -z "$VERSION" ]; then + >&2 echo '$VERSION required' + exit 2 + fi fi } @@ -75,6 +87,22 @@ resolve_gh_user() { cmd_download() { require_version + + if [ -z "${RUN_ID:-}" ]; then + read -rp "RUN_ID (OS images, leave blank to skip): " RUN_ID + fi + RUN_ID=$(parse_run_id "${RUN_ID:-}") + + if [ -z "${ST_RUN_ID:-}" ]; then + read -rp "ST_RUN_ID (start-tunnel, leave blank to skip): " ST_RUN_ID + fi + ST_RUN_ID=$(parse_run_id "${ST_RUN_ID:-}") + + if [ -z "${CLI_RUN_ID:-}" ]; then + read -rp "CLI_RUN_ID (start-cli, leave blank to skip): " CLI_RUN_ID + fi + CLI_RUN_ID=$(parse_run_id "${CLI_RUN_ID:-}") + ensure_release_dir if [ -n "$RUN_ID" ]; then @@ -143,10 +171,14 @@ cmd_upload() { enter_release_dir for file in $(release_files); do - gh release upload -R $REPO "v$VERSION" "$file" - done - for file in *.iso *.squashfs; do - s3cmd put -P "$file" "$S3_BUCKET/v$VERSION/$file" + case "$file" in + *.iso|*.squashfs) + s3cmd put -P "$file" "$S3_BUCKET/v$VERSION/$file" + ;; + *) + gh release upload -R $REPO "v$VERSION" "$file" + ;; + esac done } diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index dac5faf11..30ee515fd 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -24,7 +24,7 @@ use crate::net::host::Host; use crate::net::host::binding::{ AddSslOptions, BindInfo, BindOptions, Bindings, DerivedAddressInfo, NetInfo, }; -use crate::net::vhost::AlpnInfo; +use crate::net::vhost::{AlpnInfo, PassthroughInfo}; use crate::prelude::*; use crate::progress::FullProgress; use crate::system::{KeyboardOptions, SmtpValue}; @@ -121,6 +121,7 @@ impl Public { }, dns: Default::default(), default_outbound: None, + passthroughs: Vec::new(), }, status_info: ServerStatus { backup_progress: None, @@ -233,6 +234,8 @@ pub struct NetworkInfo { #[serde(default)] #[ts(type = "string | null")] pub default_outbound: Option, + #[serde(default)] + pub passthroughs: Vec, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] diff --git a/core/src/net/acme.rs b/core/src/net/acme.rs index 056e77e4f..68a352ae7 100644 --- a/core/src/net/acme.rs +++ b/core/src/net/acme.rs @@ -27,7 +27,7 @@ use crate::db::model::public::AcmeSettings; use crate::db::{DbAccess, DbAccessByKey, DbAccessMut}; use crate::error::ErrorData; use crate::net::ssl::should_use_cert; -use crate::net::tls::{SingleCertResolver, TlsHandler}; +use crate::net::tls::{SingleCertResolver, TlsHandler, TlsHandlerAction}; use crate::net::web_server::Accept; use crate::prelude::*; use crate::util::FromStrParser; @@ -173,7 +173,7 @@ where &'a mut self, hello: &'a ClientHello<'a>, _: &'a ::Metadata, - ) -> Option { + ) -> Option { let domain = hello.server_name()?; if hello .alpn() @@ -207,20 +207,20 @@ where cfg.alpn_protocols = vec![ACME_TLS_ALPN_NAME.to_vec()]; tracing::info!("performing ACME auth challenge"); - return Some(cfg); + return Some(TlsHandlerAction::Tls(cfg)); } let domains: BTreeSet = [domain.into()].into_iter().collect(); let crypto_provider = self.crypto_provider.clone(); if let Some(cert) = self.get_cert(&domains).await { - return Some( + return Some(TlsHandlerAction::Tls( ServerConfig::builder_with_provider(crypto_provider) .with_safe_default_protocol_versions() .log_err()? .with_no_client_auth() .with_cert_resolver(Arc::new(SingleCertResolver(Arc::new(cert)))), - ); + )); } None diff --git a/core/src/net/host/mod.rs b/core/src/net/host/mod.rs index aff25ccd5..c77b4aa26 100644 --- a/core/src/net/host/mod.rs +++ b/core/src/net/host/mod.rs @@ -249,6 +249,20 @@ impl Model { port: Some(port), metadata, }); + } else if opt.secure.map_or(false, |s| s.ssl) + && opt.add_ssl.is_none() + && available_ports.is_ssl(opt.preferred_external_port) + && net.assigned_port != Some(opt.preferred_external_port) + { + // Service handles its own TLS and the preferred port is + // allocated as SSL — add an address for passthrough vhost. + available.insert(HostnameInfo { + ssl: true, + public: true, + hostname: domain, + port: Some(opt.preferred_external_port), + metadata, + }); } } @@ -293,6 +307,20 @@ impl Model { gateways: domain_gateways, }, }); + } else if opt.secure.map_or(false, |s| s.ssl) + && opt.add_ssl.is_none() + && available_ports.is_ssl(opt.preferred_external_port) + && net.assigned_port != Some(opt.preferred_external_port) + { + available.insert(HostnameInfo { + ssl: true, + public: true, + hostname: domain, + port: Some(opt.preferred_external_port), + metadata: HostnameMetadata::PrivateDomain { + gateways: domain_gateways, + }, + }); } } bind.as_addresses_mut().as_available_mut().ser(&available)?; diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index dc990ec06..529b8824a 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -76,9 +76,22 @@ impl NetController { ], ) .await?; + let passthroughs = db + .peek() + .await + .as_public() + .as_server_info() + .as_network() + .as_passthroughs() + .de()?; Ok(Self { db: db.clone(), - vhost: VHostController::new(db.clone(), net_iface.clone(), crypto_provider), + vhost: VHostController::new( + db.clone(), + net_iface.clone(), + crypto_provider, + passthroughs, + ), tls_client_config, dns: DnsController::init(db, &net_iface.watcher).await?, forward: InterfacePortForwardController::new(net_iface.watcher.subscribe()), @@ -237,6 +250,7 @@ impl NetServiceData { connect_ssl: connect_ssl .clone() .map(|_| ctrl.tls_client_config.clone()), + passthrough: false, }, ); } @@ -253,7 +267,9 @@ impl NetServiceData { _ => continue, } let domain = &addr_info.hostname; - let domain_ssl_port = addr_info.port.unwrap_or(443); + let Some(domain_ssl_port) = addr_info.port else { + continue; + }; let key = (Some(domain.clone()), domain_ssl_port); let target = vhosts.entry(key).or_insert_with(|| ProxyTarget { public: BTreeSet::new(), @@ -266,6 +282,7 @@ impl NetServiceData { addr, add_x_forwarded_headers: ssl.add_x_forwarded_headers, connect_ssl: connect_ssl.clone().map(|_| ctrl.tls_client_config.clone()), + passthrough: false, }); if addr_info.public { for gw in addr_info.metadata.gateways() { @@ -317,6 +334,53 @@ impl NetServiceData { ), ); } + + // Passthrough vhosts: if the service handles its own TLS + // (secure.ssl && no add_ssl) and a domain address is enabled on + // an SSL port different from assigned_port, add a passthrough + // vhost so the service's TLS endpoint is reachable on that port. + if bind.options.secure.map_or(false, |s| s.ssl) && bind.options.add_ssl.is_none() { + let assigned = bind.net.assigned_port; + for addr_info in &enabled_addresses { + if !addr_info.ssl { + continue; + } + let Some(pt_port) = addr_info.port.filter(|p| assigned != Some(*p)) else { + continue; + }; + match &addr_info.metadata { + HostnameMetadata::PublicDomain { .. } + | HostnameMetadata::PrivateDomain { .. } => {} + _ => continue, + } + let domain = &addr_info.hostname; + let key = (Some(domain.clone()), pt_port); + let target = vhosts.entry(key).or_insert_with(|| ProxyTarget { + public: BTreeSet::new(), + private: BTreeSet::new(), + acme: None, + addr, + add_x_forwarded_headers: false, + connect_ssl: Err(AlpnInfo::Reflect), + passthrough: true, + }); + if addr_info.public { + for gw in addr_info.metadata.gateways() { + target.public.insert(gw.clone()); + } + } else { + for gw in addr_info.metadata.gateways() { + if let Some(info) = net_ifaces.get(gw) { + if let Some(ip_info) = &info.ip_info { + for subnet in &ip_info.subnets { + target.private.insert(subnet.addr()); + } + } + } + } + } + } + } } // ── Phase 3: Reconcile ── diff --git a/core/src/net/ssl.rs b/core/src/net/ssl.rs index 284d224a2..3b8e69c8e 100644 --- a/core/src/net/ssl.rs +++ b/core/src/net/ssl.rs @@ -36,7 +36,7 @@ use crate::db::{DbAccess, DbAccessMut}; use crate::hostname::ServerHostname; use crate::init::check_time_is_synchronized; use crate::net::gateway::GatewayInfo; -use crate::net::tls::TlsHandler; +use crate::net::tls::{TlsHandler, TlsHandlerAction}; use crate::net::web_server::{Accept, ExtractVisitor, TcpMetadata, extract}; use crate::prelude::*; use crate::util::serde::Pem; @@ -620,7 +620,7 @@ where &mut self, hello: &ClientHello<'_>, metadata: &::Metadata, - ) -> Option { + ) -> Option { let hostnames: BTreeSet = hello .server_name() .map(InternedString::from) @@ -684,5 +684,6 @@ where ) } .log_err() + .map(TlsHandlerAction::Tls) } } diff --git a/core/src/net/tls.rs b/core/src/net/tls.rs index 3d8c1b1a4..4f254f6a4 100644 --- a/core/src/net/tls.rs +++ b/core/src/net/tls.rs @@ -16,6 +16,14 @@ use tokio_rustls::rustls::sign::CertifiedKey; use tokio_rustls::rustls::{ClientConfig, RootCertStore, ServerConfig}; use visit_rs::{Visit, VisitFields}; +/// Result of a TLS handler's decision about how to handle a connection. +pub enum TlsHandlerAction { + /// Complete the TLS handshake with this ServerConfig. + Tls(ServerConfig), + /// Don't complete TLS — rewind the BackTrackingIO and return the raw stream. + Passthrough, +} + use crate::net::http::handle_http_on_https; use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor}; use crate::prelude::*; @@ -50,7 +58,7 @@ pub trait TlsHandler<'a, A: Accept> { &'a mut self, hello: &'a ClientHello<'a>, metadata: &'a A::Metadata, - ) -> impl Future> + Send + 'a; + ) -> impl Future> + Send + 'a; } #[derive(Clone)] @@ -66,7 +74,7 @@ where &'a mut self, hello: &'a ClientHello<'a>, metadata: &'a ::Metadata, - ) -> Option { + ) -> Option { if let Some(config) = self.0.get_config(hello, metadata).await { return Some(config); } @@ -86,7 +94,7 @@ pub trait WrapTlsHandler { prev: ServerConfig, hello: &'a ClientHello<'a>, metadata: &'a ::Metadata, - ) -> impl Future> + Send + 'a + ) -> impl Future> + Send + 'a where Self: 'a; } @@ -102,9 +110,12 @@ where &'a mut self, hello: &'a ClientHello<'a>, metadata: &'a ::Metadata, - ) -> Option { - let prev = self.inner.get_config(hello, metadata).await?; - self.wrapper.wrap(prev, hello, metadata).await + ) -> Option { + let action = self.inner.get_config(hello, metadata).await?; + match action { + TlsHandlerAction::Tls(cfg) => self.wrapper.wrap(cfg, hello, metadata).await, + other => Some(other), + } } } @@ -203,34 +214,56 @@ where } }; let hello = mid.client_hello(); - if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { - let buffered = mid.io.stop_buffering(); - mid.io - .write_all(&buffered) - .await - .with_kind(ErrorKind::Network)?; - return Ok(match mid.into_stream(Arc::new(cfg)).await { - Ok(stream) => { - let s = stream.get_ref().1; - Some(( - TlsMetadata { - inner: metadata, - tls_info: TlsHandshakeInfo { - sni: s.server_name().map(InternedString::intern), - alpn: s - .alpn_protocol() - .map(|a| MaybeUtf8String(a.to_vec())), + let sni = hello.server_name().map(InternedString::intern); + match tls_handler.get_config(&hello, &metadata).await { + Some(TlsHandlerAction::Tls(cfg)) => { + let buffered = mid.io.stop_buffering(); + mid.io + .write_all(&buffered) + .await + .with_kind(ErrorKind::Network)?; + return Ok(match mid.into_stream(Arc::new(cfg)).await { + Ok(stream) => { + let s = stream.get_ref().1; + Some(( + TlsMetadata { + inner: metadata, + tls_info: TlsHandshakeInfo { + sni: s + .server_name() + .map(InternedString::intern), + alpn: s + .alpn_protocol() + .map(|a| MaybeUtf8String(a.to_vec())), + }, }, - }, - Box::pin(stream) as AcceptStream, - )) - } - Err(e) => { - tracing::trace!("Error completing TLS handshake: {e}"); - tracing::trace!("{e:?}"); - None - } - }); + Box::pin(stream) as AcceptStream, + )) + } + Err(e) => { + tracing::trace!("Error completing TLS handshake: {e}"); + tracing::trace!("{e:?}"); + None + } + }); + } + Some(TlsHandlerAction::Passthrough) => { + let (dummy, _drop) = tokio::io::duplex(1); + let mut bt = std::mem::replace( + &mut mid.io, + BackTrackingIO::new(Box::pin(dummy) as AcceptStream), + ); + drop(mid); + bt.rewind(); + return Ok(Some(( + TlsMetadata { + inner: metadata, + tls_info: TlsHandshakeInfo { sni, alpn: None }, + }, + Box::pin(bt) as AcceptStream, + ))); + } + None => {} } Ok(None) diff --git a/core/src/net/vhost.rs b/core/src/net/vhost.rs index 85054e62b..970a9ccb9 100644 --- a/core/src/net/vhost.rs +++ b/core/src/net/vhost.rs @@ -6,12 +6,13 @@ use std::sync::{Arc, Weak}; use std::task::{Poll, ready}; use async_acme::acme::ACME_TLS_ALPN_NAME; +use clap::Parser; use color_eyre::eyre::eyre; use futures::FutureExt; use futures::future::BoxFuture; use imbl::OrdMap; use imbl_value::{InOMap, InternedString}; -use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn}; +use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn, from_fn_async}; use serde::{Deserialize, Serialize}; use tokio::net::{TcpListener, TcpStream}; use tokio_rustls::TlsConnector; @@ -35,7 +36,7 @@ use crate::net::gateway::{ }; use crate::net::ssl::{CertStore, RootCaTlsHandler}; use crate::net::tls::{ - ChainedHandler, TlsHandlerWrapper, TlsListener, TlsMetadata, WrapTlsHandler, + ChainedHandler, TlsHandlerAction, TlsHandlerWrapper, TlsListener, TlsMetadata, WrapTlsHandler, }; use crate::net::utils::ipv6_is_link_local; use crate::net::web_server::{Accept, AcceptStream, ExtractVisitor, TcpMetadata, extract}; @@ -46,68 +47,228 @@ use crate::util::serde::{HandlerExtSerde, MaybeUtf8String, display_serializable} use crate::util::sync::{SyncMutex, Watch}; use crate::{GatewayId, ResultExt}; +#[derive(Debug, Clone, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct PassthroughInfo { + #[ts(type = "string")] + pub hostname: InternedString, + pub listen_port: u16, + #[ts(type = "string")] + pub backend: SocketAddr, + #[ts(type = "string[]")] + pub public_gateways: BTreeSet, + #[ts(type = "string[]")] + pub private_ips: BTreeSet, +} + +#[derive(Debug, Clone, Deserialize, Serialize, Parser)] +#[serde(rename_all = "kebab-case")] +struct AddPassthroughParams { + #[arg(long)] + pub hostname: InternedString, + #[arg(long)] + pub listen_port: u16, + #[arg(long)] + pub backend: SocketAddr, + #[arg(long)] + pub public_gateway: Vec, + #[arg(long)] + pub private_ip: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, Parser)] +#[serde(rename_all = "kebab-case")] +struct RemovePassthroughParams { + #[arg(long)] + pub hostname: InternedString, + #[arg(long)] + pub listen_port: u16, +} + pub fn vhost_api() -> ParentHandler { - ParentHandler::new().subcommand( - "dump-table", - from_fn(|ctx: RpcContext| Ok(ctx.net_controller.vhost.dump_table())) - .with_display_serializable() - .with_custom_display_fn(|HandlerArgs { params, .. }, res| { - use prettytable::*; + ParentHandler::new() + .subcommand( + "dump-table", + from_fn(dump_table) + .with_display_serializable() + .with_custom_display_fn(|HandlerArgs { params, .. }, res| { + use prettytable::*; - if let Some(format) = params.format { - display_serializable(format, res)?; - return Ok::<_, Error>(()); - } + if let Some(format) = params.format { + display_serializable(format, res)?; + return Ok::<_, Error>(()); + } - let mut table = Table::new(); - table.add_row(row![bc => "FROM", "TO", "ACTIVE"]); + let mut table = Table::new(); + table.add_row(row![bc => "FROM", "TO", "ACTIVE"]); - for (external, targets) in res { - for (host, targets) in targets { - for (idx, target) in targets.into_iter().enumerate() { - table.add_row(row![ - format!( - "{}:{}", - host.as_ref().map(|s| &**s).unwrap_or("*"), - external.0 - ), - target, - idx == 0 - ]); + for (external, targets) in res { + for (host, targets) in targets { + for (idx, target) in targets.into_iter().enumerate() { + table.add_row(row![ + format!( + "{}:{}", + host.as_ref().map(|s| &**s).unwrap_or("*"), + external.0 + ), + target, + idx == 0 + ]); + } } } - } - table.print_tty(false)?; + table.print_tty(false)?; - Ok(()) - }) - .with_call_remote::(), - ) + Ok(()) + }) + .with_call_remote::(), + ) + .subcommand( + "add-passthrough", + from_fn_async(add_passthrough) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "remove-passthrough", + from_fn_async(remove_passthrough) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "list-passthrough", + from_fn(list_passthrough) + .with_display_serializable() + .with_call_remote::(), + ) +} + +fn dump_table( + ctx: RpcContext, +) -> Result, BTreeMap>, EqSet>>, Error> +{ + Ok(ctx.net_controller.vhost.dump_table()) +} + +async fn add_passthrough( + ctx: RpcContext, + AddPassthroughParams { + hostname, + listen_port, + backend, + public_gateway, + private_ip, + }: AddPassthroughParams, +) -> Result<(), Error> { + let public_gateways: BTreeSet = public_gateway.into_iter().collect(); + let private_ips: BTreeSet = private_ip.into_iter().collect(); + ctx.net_controller.vhost.add_passthrough( + hostname.clone(), + listen_port, + backend, + public_gateways.clone(), + private_ips.clone(), + )?; + ctx.db + .mutate(|db| { + let pts = db + .as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_passthroughs_mut(); + let mut vec: Vec = pts.de()?; + vec.retain(|p| !(p.hostname == hostname && p.listen_port == listen_port)); + vec.push(PassthroughInfo { + hostname, + listen_port, + backend, + public_gateways, + private_ips, + }); + pts.ser(&vec) + }) + .await + .result?; + Ok(()) +} + +async fn remove_passthrough( + ctx: RpcContext, + RemovePassthroughParams { + hostname, + listen_port, + }: RemovePassthroughParams, +) -> Result<(), Error> { + ctx.net_controller + .vhost + .remove_passthrough(&hostname, listen_port); + ctx.db + .mutate(|db| { + let pts = db + .as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_passthroughs_mut(); + let mut vec: Vec = pts.de()?; + vec.retain(|p| !(p.hostname == hostname && p.listen_port == listen_port)); + pts.ser(&vec) + }) + .await + .result?; + Ok(()) +} + +fn list_passthrough(ctx: RpcContext) -> Result, Error> { + Ok(ctx.net_controller.vhost.list_passthrough()) } // not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 +struct PassthroughHandle { + _rc: Arc<()>, + backend: SocketAddr, + public: BTreeSet, + private: BTreeSet, +} + pub struct VHostController { db: TypedPatchDb, interfaces: Arc, crypto_provider: Arc, acme_cache: AcmeTlsAlpnCache, servers: SyncMutex>>, + passthrough_handles: SyncMutex>, } impl VHostController { pub fn new( db: TypedPatchDb, interfaces: Arc, crypto_provider: Arc, + passthroughs: Vec, ) -> Self { - Self { + let controller = Self { db, interfaces, crypto_provider, acme_cache: Arc::new(SyncMutex::new(BTreeMap::new())), servers: SyncMutex::new(BTreeMap::new()), + passthrough_handles: SyncMutex::new(BTreeMap::new()), + }; + for pt in passthroughs { + if let Err(e) = controller.add_passthrough( + pt.hostname, + pt.listen_port, + pt.backend, + pt.public_gateways, + pt.private_ips, + ) { + tracing::warn!("failed to restore passthrough: {e}"); + } } + controller } #[instrument(skip_all)] pub fn add( @@ -120,20 +281,7 @@ impl VHostController { let server = if let Some(server) = writable.remove(&external) { server } else { - let bind_reqs = Watch::new(VHostBindRequirements::default()); - let listener = VHostBindListener { - ip_info: self.interfaces.watcher.subscribe(), - port: external, - bind_reqs: bind_reqs.clone_unseen(), - listeners: BTreeMap::new(), - }; - VHostServer::new( - listener, - bind_reqs, - self.db.clone(), - self.crypto_provider.clone(), - self.acme_cache.clone(), - ) + self.create_server(external) }; let rc = server.add(hostname, target); writable.insert(external, server); @@ -141,6 +289,75 @@ impl VHostController { }) } + fn create_server(&self, port: u16) -> VHostServer { + let bind_reqs = Watch::new(VHostBindRequirements::default()); + let listener = VHostBindListener { + ip_info: self.interfaces.watcher.subscribe(), + port, + bind_reqs: bind_reqs.clone_unseen(), + listeners: BTreeMap::new(), + }; + VHostServer::new( + listener, + bind_reqs, + self.db.clone(), + self.crypto_provider.clone(), + self.acme_cache.clone(), + ) + } + + pub fn add_passthrough( + &self, + hostname: InternedString, + port: u16, + backend: SocketAddr, + public: BTreeSet, + private: BTreeSet, + ) -> Result<(), Error> { + let target = ProxyTarget { + public: public.clone(), + private: private.clone(), + acme: None, + addr: backend, + add_x_forwarded_headers: false, + connect_ssl: Err(AlpnInfo::Reflect), + passthrough: true, + }; + let rc = self.add(Some(hostname.clone()), port, DynVHostTarget::new(target))?; + self.passthrough_handles.mutate(|h| { + h.insert( + (hostname, port), + PassthroughHandle { + _rc: rc, + backend, + public, + private, + }, + ); + }); + Ok(()) + } + + pub fn remove_passthrough(&self, hostname: &InternedString, port: u16) { + self.passthrough_handles + .mutate(|h| h.remove(&(hostname.clone(), port))); + self.gc(Some(hostname.clone()), port); + } + + pub fn list_passthrough(&self) -> Vec { + self.passthrough_handles.peek(|h| { + h.iter() + .map(|((hostname, port), handle)| PassthroughInfo { + hostname: hostname.clone(), + listen_port: *port, + backend: handle.backend, + public_gateways: handle.public.clone(), + private_ips: handle.private.clone(), + }) + .collect() + }) + } + pub fn dump_table( &self, ) -> BTreeMap, BTreeMap>, EqSet>> { @@ -330,6 +547,9 @@ pub trait VHostTarget: std::fmt::Debug + Eq { fn bind_requirements(&self) -> (BTreeSet, BTreeSet) { (BTreeSet::new(), BTreeSet::new()) } + fn is_passthrough(&self) -> bool { + false + } fn preprocess<'a>( &'a self, prev: ServerConfig, @@ -349,6 +569,7 @@ pub trait DynVHostTargetT: std::fmt::Debug + Any { fn filter(&self, metadata: &::Metadata) -> bool; fn acme(&self) -> Option<&AcmeProvider>; fn bind_requirements(&self) -> (BTreeSet, BTreeSet); + fn is_passthrough(&self) -> bool; fn preprocess<'a>( &'a self, prev: ServerConfig, @@ -373,6 +594,9 @@ impl + 'static> DynVHostTargetT for T { fn acme(&self) -> Option<&AcmeProvider> { VHostTarget::acme(self) } + fn is_passthrough(&self) -> bool { + VHostTarget::is_passthrough(self) + } fn bind_requirements(&self) -> (BTreeSet, BTreeSet) { VHostTarget::bind_requirements(self) } @@ -459,6 +683,7 @@ pub struct ProxyTarget { pub addr: SocketAddr, pub add_x_forwarded_headers: bool, pub connect_ssl: Result, AlpnInfo>, // Ok: yes, connect using ssl, pass through alpn; Err: connect tcp, use provided strategy for alpn + pub passthrough: bool, } impl PartialEq for ProxyTarget { fn eq(&self, other: &Self) -> bool { @@ -466,6 +691,7 @@ impl PartialEq for ProxyTarget { && self.private == other.private && self.acme == other.acme && self.addr == other.addr + && self.passthrough == other.passthrough && self.connect_ssl.as_ref().map(Arc::as_ptr) == other.connect_ssl.as_ref().map(Arc::as_ptr) } @@ -480,6 +706,7 @@ impl fmt::Debug for ProxyTarget { .field("addr", &self.addr) .field("add_x_forwarded_headers", &self.add_x_forwarded_headers) .field("connect_ssl", &self.connect_ssl.as_ref().map(|_| ())) + .field("passthrough", &self.passthrough) .finish() } } @@ -524,6 +751,9 @@ where fn bind_requirements(&self) -> (BTreeSet, BTreeSet) { (self.public.clone(), self.private.clone()) } + fn is_passthrough(&self) -> bool { + self.passthrough + } async fn preprocess<'a>( &'a self, mut prev: ServerConfig, @@ -677,7 +907,7 @@ where prev: ServerConfig, hello: &'a ClientHello<'a>, metadata: &'a ::Metadata, - ) -> Option + ) -> Option where Self: 'a, { @@ -687,7 +917,7 @@ where .flatten() .any(|a| a == ACME_TLS_ALPN_NAME) { - return Some(prev); + return Some(TlsHandlerAction::Tls(prev)); } let (target, rc) = self.0.peek(|m| { @@ -700,11 +930,16 @@ where .map(|(t, rc)| (t.clone(), rc.clone())) })?; + let is_pt = target.0.is_passthrough(); let (prev, store) = target.into_preprocessed(rc, prev, hello, metadata).await?; self.1 = Some(store); - Some(prev) + if is_pt { + Some(TlsHandlerAction::Passthrough) + } else { + Some(TlsHandlerAction::Tls(prev)) + } } } diff --git a/core/src/tunnel/web.rs b/core/src/tunnel/web.rs index 598f05fa7..04e7f84c0 100644 --- a/core/src/tunnel/web.rs +++ b/core/src/tunnel/web.rs @@ -20,7 +20,7 @@ use ts_rs::TS; use crate::context::CliContext; use crate::hostname::ServerHostname; use crate::net::ssl::{SANInfo, root_ca_start_time}; -use crate::net::tls::TlsHandler; +use crate::net::tls::{TlsHandler, TlsHandlerAction}; use crate::net::web_server::Accept; use crate::prelude::*; use crate::tunnel::auth::SetPasswordParams; @@ -59,7 +59,7 @@ where &'a mut self, _: &'a ClientHello<'a>, _: &'a ::Metadata, - ) -> Option { + ) -> Option { let cert_info = self .db .peek() @@ -88,7 +88,7 @@ where .log_err()?; cfg.alpn_protocols .extend([b"http/1.1".into(), b"h2".into()]); - Some(cfg) + Some(TlsHandlerAction::Tls(cfg)) } }