feat(tls): add shared TLS acceptor with SNI resolver and session resumption; prefer shared acceptor and fall back to per-connection when routes specify custom TLS versions
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-16 - 25.5.0 - feat(tls)
|
||||||
|
add shared TLS acceptor with SNI resolver and session resumption; prefer shared acceptor and fall back to per-connection when routes specify custom TLS versions
|
||||||
|
|
||||||
|
- Add CertResolver that pre-parses PEM certs/keys into CertifiedKey instances for SNI-based lookup and cheap runtime resolution
|
||||||
|
- Introduce build_shared_tls_acceptor to create a shared ServerConfig with session cache (4096) and Ticketer for session ticket resumption
|
||||||
|
- Add ArcSwap<Option<TlsAcceptor>> shared_tls_acceptor to tcp_listener for hot-reloadable, pre-built acceptor and update accept loop/handlers to use it
|
||||||
|
- set_tls_configs now attempts to build and store the shared TLS acceptor, falling back to per-connection acceptors on failure; raw PEM configs are still retained for route-level fallbacks
|
||||||
|
- Add get_tls_acceptor helper: prefer shared acceptor for performance and session resumption, but build per-connection acceptor when a route requests custom TLS versions
|
||||||
|
|
||||||
## 2026-02-16 - 25.4.0 - feat(rustproxy)
|
## 2026-02-16 - 25.4.0 - feat(rustproxy)
|
||||||
support dynamically loaded TLS certificates via loadCertificate IPC and include them in listener TLS configs for rebuilds and hot-swap
|
support dynamically loaded TLS certificates via loadCertificate IPC and include them in listener TLS configs for rebuilds and hot-swap
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
use tokio_rustls::TlsAcceptor;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{info, error, debug, warn};
|
use tracing::{info, error, debug, warn};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -122,8 +123,10 @@ pub struct TcpListenerManager {
|
|||||||
route_manager: Arc<ArcSwap<RouteManager>>,
|
route_manager: Arc<ArcSwap<RouteManager>>,
|
||||||
/// Shared metrics collector
|
/// Shared metrics collector
|
||||||
metrics: Arc<MetricsCollector>,
|
metrics: Arc<MetricsCollector>,
|
||||||
/// TLS acceptors indexed by domain (ArcSwap for hot-reload visibility in accept loops)
|
/// Raw PEM TLS configs indexed by domain (kept for fallback with custom TLS versions)
|
||||||
tls_configs: Arc<ArcSwap<HashMap<String, TlsCertConfig>>>,
|
tls_configs: Arc<ArcSwap<HashMap<String, TlsCertConfig>>>,
|
||||||
|
/// Shared TLS acceptor (pre-parsed certs + session cache). None when no certs configured.
|
||||||
|
shared_tls_acceptor: Arc<ArcSwap<Option<TlsAcceptor>>>,
|
||||||
/// HTTP proxy service for HTTP-level forwarding
|
/// HTTP proxy service for HTTP-level forwarding
|
||||||
http_proxy: Arc<HttpProxyService>,
|
http_proxy: Arc<HttpProxyService>,
|
||||||
/// Connection configuration
|
/// Connection configuration
|
||||||
@@ -154,6 +157,7 @@ impl TcpListenerManager {
|
|||||||
route_manager: Arc::new(ArcSwap::from(route_manager)),
|
route_manager: Arc::new(ArcSwap::from(route_manager)),
|
||||||
metrics,
|
metrics,
|
||||||
tls_configs: Arc::new(ArcSwap::from(Arc::new(HashMap::new()))),
|
tls_configs: Arc::new(ArcSwap::from(Arc::new(HashMap::new()))),
|
||||||
|
shared_tls_acceptor: Arc::new(ArcSwap::from(Arc::new(None))),
|
||||||
http_proxy,
|
http_proxy,
|
||||||
conn_config: Arc::new(conn_config),
|
conn_config: Arc::new(conn_config),
|
||||||
conn_tracker,
|
conn_tracker,
|
||||||
@@ -179,6 +183,7 @@ impl TcpListenerManager {
|
|||||||
route_manager: Arc::new(ArcSwap::from(route_manager)),
|
route_manager: Arc::new(ArcSwap::from(route_manager)),
|
||||||
metrics,
|
metrics,
|
||||||
tls_configs: Arc::new(ArcSwap::from(Arc::new(HashMap::new()))),
|
tls_configs: Arc::new(ArcSwap::from(Arc::new(HashMap::new()))),
|
||||||
|
shared_tls_acceptor: Arc::new(ArcSwap::from(Arc::new(None))),
|
||||||
http_proxy,
|
http_proxy,
|
||||||
conn_config: Arc::new(conn_config),
|
conn_config: Arc::new(conn_config),
|
||||||
conn_tracker,
|
conn_tracker,
|
||||||
@@ -197,8 +202,26 @@ impl TcpListenerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set TLS certificate configurations.
|
/// Set TLS certificate configurations.
|
||||||
|
/// Builds a shared TLS acceptor with pre-parsed certs and session resumption support.
|
||||||
/// Uses ArcSwap so running accept loops immediately see the new certs.
|
/// Uses ArcSwap so running accept loops immediately see the new certs.
|
||||||
pub fn set_tls_configs(&self, configs: HashMap<String, TlsCertConfig>) {
|
pub fn set_tls_configs(&self, configs: HashMap<String, TlsCertConfig>) {
|
||||||
|
if !configs.is_empty() {
|
||||||
|
match tls_handler::CertResolver::new(&configs)
|
||||||
|
.and_then(tls_handler::build_shared_tls_acceptor)
|
||||||
|
{
|
||||||
|
Ok(acceptor) => {
|
||||||
|
info!("Built shared TLS acceptor for {} domain(s)", configs.len());
|
||||||
|
self.shared_tls_acceptor.store(Arc::new(Some(acceptor)));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to build shared TLS acceptor: {}, falling back to per-connection", e);
|
||||||
|
self.shared_tls_acceptor.store(Arc::new(None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.shared_tls_acceptor.store(Arc::new(None));
|
||||||
|
}
|
||||||
|
// Keep raw PEM configs for fallback (routes with custom TLS versions)
|
||||||
self.tls_configs.store(Arc::new(configs));
|
self.tls_configs.store(Arc::new(configs));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +247,7 @@ impl TcpListenerManager {
|
|||||||
let route_manager_swap = Arc::clone(&self.route_manager);
|
let route_manager_swap = Arc::clone(&self.route_manager);
|
||||||
let metrics = Arc::clone(&self.metrics);
|
let metrics = Arc::clone(&self.metrics);
|
||||||
let tls_configs = Arc::clone(&self.tls_configs);
|
let tls_configs = Arc::clone(&self.tls_configs);
|
||||||
|
let shared_tls_acceptor = Arc::clone(&self.shared_tls_acceptor);
|
||||||
let http_proxy = Arc::clone(&self.http_proxy);
|
let http_proxy = Arc::clone(&self.http_proxy);
|
||||||
let conn_config = Arc::clone(&self.conn_config);
|
let conn_config = Arc::clone(&self.conn_config);
|
||||||
let conn_tracker = Arc::clone(&self.conn_tracker);
|
let conn_tracker = Arc::clone(&self.conn_tracker);
|
||||||
@@ -233,7 +257,7 @@ impl TcpListenerManager {
|
|||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
Self::accept_loop(
|
Self::accept_loop(
|
||||||
listener, port, route_manager_swap, metrics, tls_configs,
|
listener, port, route_manager_swap, metrics, tls_configs,
|
||||||
http_proxy, conn_config, conn_tracker, cancel, relay,
|
shared_tls_acceptor, http_proxy, conn_config, conn_tracker, cancel, relay,
|
||||||
).await;
|
).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -322,6 +346,7 @@ impl TcpListenerManager {
|
|||||||
route_manager_swap: Arc<ArcSwap<RouteManager>>,
|
route_manager_swap: Arc<ArcSwap<RouteManager>>,
|
||||||
metrics: Arc<MetricsCollector>,
|
metrics: Arc<MetricsCollector>,
|
||||||
tls_configs: Arc<ArcSwap<HashMap<String, TlsCertConfig>>>,
|
tls_configs: Arc<ArcSwap<HashMap<String, TlsCertConfig>>>,
|
||||||
|
shared_tls_acceptor: Arc<ArcSwap<Option<TlsAcceptor>>>,
|
||||||
http_proxy: Arc<HttpProxyService>,
|
http_proxy: Arc<HttpProxyService>,
|
||||||
conn_config: Arc<ConnectionConfig>,
|
conn_config: Arc<ConnectionConfig>,
|
||||||
conn_tracker: Arc<ConnectionTracker>,
|
conn_tracker: Arc<ConnectionTracker>,
|
||||||
@@ -353,6 +378,8 @@ impl TcpListenerManager {
|
|||||||
let m = Arc::clone(&metrics);
|
let m = Arc::clone(&metrics);
|
||||||
// Load the latest TLS configs from ArcSwap on each connection
|
// Load the latest TLS configs from ArcSwap on each connection
|
||||||
let tc = tls_configs.load_full();
|
let tc = tls_configs.load_full();
|
||||||
|
// Load the latest shared TLS acceptor from ArcSwap
|
||||||
|
let sa = shared_tls_acceptor.load_full();
|
||||||
let hp = Arc::clone(&http_proxy);
|
let hp = Arc::clone(&http_proxy);
|
||||||
let cc = Arc::clone(&conn_config);
|
let cc = Arc::clone(&conn_config);
|
||||||
let ct = Arc::clone(&conn_tracker);
|
let ct = Arc::clone(&conn_tracker);
|
||||||
@@ -362,7 +389,7 @@ impl TcpListenerManager {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let result = Self::handle_connection(
|
let result = Self::handle_connection(
|
||||||
stream, port, peer_addr, rm, m, tc, hp, cc, cn, sr,
|
stream, port, peer_addr, rm, m, tc, sa, hp, cc, cn, sr,
|
||||||
).await;
|
).await;
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
debug!("Connection error from {}: {}", peer_addr, e);
|
debug!("Connection error from {}: {}", peer_addr, e);
|
||||||
@@ -388,6 +415,7 @@ impl TcpListenerManager {
|
|||||||
route_manager: Arc<RouteManager>,
|
route_manager: Arc<RouteManager>,
|
||||||
metrics: Arc<MetricsCollector>,
|
metrics: Arc<MetricsCollector>,
|
||||||
tls_configs: Arc<HashMap<String, TlsCertConfig>>,
|
tls_configs: Arc<HashMap<String, TlsCertConfig>>,
|
||||||
|
shared_tls_acceptor: Arc<Option<TlsAcceptor>>,
|
||||||
http_proxy: Arc<HttpProxyService>,
|
http_proxy: Arc<HttpProxyService>,
|
||||||
conn_config: Arc<ConnectionConfig>,
|
conn_config: Arc<ConnectionConfig>,
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
@@ -777,13 +805,9 @@ impl TcpListenerManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(rustproxy_config::TlsMode::Terminate) => {
|
Some(rustproxy_config::TlsMode::Terminate) => {
|
||||||
let tls_config = Self::find_tls_config(&domain, &tls_configs)?;
|
// Use shared acceptor (session resumption) or fall back to per-connection
|
||||||
|
|
||||||
// TLS accept with timeout, applying route-level TLS settings
|
|
||||||
let route_tls = route_match.route.action.tls.as_ref();
|
let route_tls = route_match.route.action.tls.as_ref();
|
||||||
let acceptor = tls_handler::build_tls_acceptor_with_config(
|
let acceptor = Self::get_tls_acceptor(&domain, &tls_configs, &*shared_tls_acceptor, route_tls)?;
|
||||||
&tls_config.cert_pem, &tls_config.key_pem, route_tls,
|
|
||||||
)?;
|
|
||||||
let tls_stream = match tokio::time::timeout(
|
let tls_stream = match tokio::time::timeout(
|
||||||
std::time::Duration::from_millis(conn_config.initial_data_timeout_ms),
|
std::time::Duration::from_millis(conn_config.initial_data_timeout_ms),
|
||||||
tls_handler::accept_tls(stream, &acceptor),
|
tls_handler::accept_tls(stream, &acceptor),
|
||||||
@@ -846,7 +870,8 @@ impl TcpListenerManager {
|
|||||||
let route_tls = route_match.route.action.tls.as_ref();
|
let route_tls = route_match.route.action.tls.as_ref();
|
||||||
Self::handle_tls_terminate_reencrypt(
|
Self::handle_tls_terminate_reencrypt(
|
||||||
stream, n, &domain, &target_host, target_port,
|
stream, n, &domain, &target_host, target_port,
|
||||||
peer_addr, &tls_configs, Arc::clone(&metrics), route_id, &conn_config, route_tls,
|
peer_addr, &tls_configs, &shared_tls_acceptor,
|
||||||
|
Arc::clone(&metrics), route_id, &conn_config, route_tls,
|
||||||
).await
|
).await
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -991,15 +1016,14 @@ impl TcpListenerManager {
|
|||||||
target_port: u16,
|
target_port: u16,
|
||||||
peer_addr: std::net::SocketAddr,
|
peer_addr: std::net::SocketAddr,
|
||||||
tls_configs: &HashMap<String, TlsCertConfig>,
|
tls_configs: &HashMap<String, TlsCertConfig>,
|
||||||
|
shared_tls_acceptor: &Option<TlsAcceptor>,
|
||||||
metrics: Arc<MetricsCollector>,
|
metrics: Arc<MetricsCollector>,
|
||||||
route_id: Option<&str>,
|
route_id: Option<&str>,
|
||||||
conn_config: &ConnectionConfig,
|
conn_config: &ConnectionConfig,
|
||||||
route_tls: Option<&rustproxy_config::RouteTls>,
|
route_tls: Option<&rustproxy_config::RouteTls>,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let tls_config = Self::find_tls_config(domain, tls_configs)?;
|
// Use shared acceptor (session resumption) or fall back to per-connection
|
||||||
let acceptor = tls_handler::build_tls_acceptor_with_config(
|
let acceptor = Self::get_tls_acceptor(domain, tls_configs, shared_tls_acceptor, route_tls)?;
|
||||||
&tls_config.cert_pem, &tls_config.key_pem, route_tls,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Accept TLS from client with timeout
|
// Accept TLS from client with timeout
|
||||||
let client_tls = match tokio::time::timeout(
|
let client_tls = match tokio::time::timeout(
|
||||||
@@ -1069,6 +1093,30 @@ impl TcpListenerManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a TLS acceptor, preferring the shared one (with session resumption)
|
||||||
|
/// and falling back to per-connection when custom TLS versions are configured.
|
||||||
|
fn get_tls_acceptor(
|
||||||
|
domain: &Option<String>,
|
||||||
|
tls_configs: &HashMap<String, TlsCertConfig>,
|
||||||
|
shared_tls_acceptor: &Option<TlsAcceptor>,
|
||||||
|
route_tls: Option<&rustproxy_config::RouteTls>,
|
||||||
|
) -> Result<TlsAcceptor, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let has_custom_versions = route_tls
|
||||||
|
.and_then(|t| t.versions.as_ref())
|
||||||
|
.map(|v| !v.is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !has_custom_versions {
|
||||||
|
if let Some(shared) = shared_tls_acceptor {
|
||||||
|
return Ok(shared.clone()); // TlsAcceptor wraps Arc<ServerConfig>, clone is cheap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: per-connection acceptor (custom TLS versions or shared build failed)
|
||||||
|
let tls_config = Self::find_tls_config(domain, tls_configs)?;
|
||||||
|
tls_handler::build_tls_acceptor_with_config(&tls_config.cert_pem, &tls_config.key_pem, route_tls)
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the TLS config for a given domain.
|
/// Find the TLS config for a given domain.
|
||||||
fn find_tls_config<'a>(
|
fn find_tls_config<'a>(
|
||||||
domain: &Option<String>,
|
domain: &Option<String>,
|
||||||
|
|||||||
@@ -1,17 +1,99 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||||
|
use rustls::server::ResolvesServerCert;
|
||||||
|
use rustls::sign::CertifiedKey;
|
||||||
use rustls::ServerConfig;
|
use rustls::ServerConfig;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio_rustls::{TlsAcceptor, TlsConnector, server::TlsStream as ServerTlsStream};
|
use tokio_rustls::{TlsAcceptor, TlsConnector, server::TlsStream as ServerTlsStream};
|
||||||
use tracing::debug;
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
use crate::tcp_listener::TlsCertConfig;
|
||||||
|
|
||||||
/// Ensure the default crypto provider is installed.
|
/// Ensure the default crypto provider is installed.
|
||||||
fn ensure_crypto_provider() {
|
fn ensure_crypto_provider() {
|
||||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SNI-based certificate resolver with pre-parsed CertifiedKeys.
|
||||||
|
/// Enables shared ServerConfig across connections — avoids per-connection PEM parsing
|
||||||
|
/// and enables TLS session resumption.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CertResolver {
|
||||||
|
certs: HashMap<String, Arc<CertifiedKey>>,
|
||||||
|
fallback: Option<Arc<CertifiedKey>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CertResolver {
|
||||||
|
/// Build a resolver from PEM-encoded cert/key configs.
|
||||||
|
/// Parses all PEM data upfront so connections only do a cheap HashMap lookup.
|
||||||
|
pub fn new(configs: &HashMap<String, TlsCertConfig>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
ensure_crypto_provider();
|
||||||
|
let provider = rustls::crypto::ring::default_provider();
|
||||||
|
let mut certs = HashMap::new();
|
||||||
|
let mut fallback = None;
|
||||||
|
|
||||||
|
for (domain, cfg) in configs {
|
||||||
|
let cert_chain = load_certs(&cfg.cert_pem)?;
|
||||||
|
let key = load_private_key(&cfg.key_pem)?;
|
||||||
|
let ck = Arc::new(CertifiedKey::from_der(cert_chain, key, &provider)
|
||||||
|
.map_err(|e| format!("CertifiedKey for {}: {}", domain, e))?);
|
||||||
|
if domain == "*" {
|
||||||
|
fallback = Some(Arc::clone(&ck));
|
||||||
|
}
|
||||||
|
certs.insert(domain.clone(), ck);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no explicit "*" fallback, use the first available cert
|
||||||
|
if fallback.is_none() {
|
||||||
|
fallback = certs.values().next().map(Arc::clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { certs, fallback })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvesServerCert for CertResolver {
|
||||||
|
fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
|
||||||
|
let domain = match client_hello.server_name() {
|
||||||
|
Some(name) => name,
|
||||||
|
None => return self.fallback.clone(),
|
||||||
|
};
|
||||||
|
// Exact match
|
||||||
|
if let Some(ck) = self.certs.get(domain) {
|
||||||
|
return Some(Arc::clone(ck));
|
||||||
|
}
|
||||||
|
// Wildcard: sub.example.com → *.example.com
|
||||||
|
if let Some(dot) = domain.find('.') {
|
||||||
|
let wc = format!("*.{}", &domain[dot + 1..]);
|
||||||
|
if let Some(ck) = self.certs.get(&wc) {
|
||||||
|
return Some(Arc::clone(ck));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.fallback.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a shared TLS acceptor with SNI resolution, session cache, and session tickets.
|
||||||
|
/// The returned acceptor can be reused across all connections (cheap Arc clone).
|
||||||
|
pub fn build_shared_tls_acceptor(resolver: CertResolver) -> Result<TlsAcceptor, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
ensure_crypto_provider();
|
||||||
|
let mut config = ServerConfig::builder()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_cert_resolver(Arc::new(resolver));
|
||||||
|
|
||||||
|
// Shared session cache — enables session ID resumption across connections
|
||||||
|
config.session_storage = rustls::server::ServerSessionMemoryCache::new(4096);
|
||||||
|
// Session ticket resumption (12-hour lifetime, Chacha20Poly1305 encrypted)
|
||||||
|
config.ticketer = rustls::crypto::ring::Ticketer::new()
|
||||||
|
.map_err(|e| format!("Ticketer: {}", e))?;
|
||||||
|
|
||||||
|
info!("Built shared TLS config with session cache (4096) and ticket support");
|
||||||
|
Ok(TlsAcceptor::from(Arc::new(config)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a TLS acceptor from PEM-encoded cert and key data.
|
/// Build a TLS acceptor from PEM-encoded cert and key data.
|
||||||
pub fn build_tls_acceptor(cert_pem: &str, key_pem: &str) -> Result<TlsAcceptor, Box<dyn std::error::Error + Send + Sync>> {
|
pub fn build_tls_acceptor(cert_pem: &str, key_pem: &str) -> Result<TlsAcceptor, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
build_tls_acceptor_with_config(cert_pem, key_pem, None)
|
build_tls_acceptor_with_config(cert_pem, key_pem, None)
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '25.4.0',
|
version: '25.5.0',
|
||||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user