mirror of
https://github.com/warp-tech/warpgate.git
synced 2025-09-06 06:34:32 +08:00
parent
b61e06ca48
commit
8ff3e9a5cf
10 changed files with 312 additions and 58 deletions
89
Cargo.lock
generated
89
Cargo.lock
generated
|
@ -197,6 +197,45 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-impl"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.22"
|
||||
|
@ -1020,6 +1059,20 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "10.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
|
@ -2587,6 +2640,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
|
@ -3786,6 +3848,15 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
|
@ -5436,6 +5507,7 @@ dependencies = [
|
|||
"uuid",
|
||||
"warpgate-sso",
|
||||
"webpki",
|
||||
"x509-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6232,6 +6304,23 @@ dependencies = [
|
|||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"data-encoding",
|
||||
"der-parser",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"oid-registry",
|
||||
"rusticata-macros",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust2"
|
||||
version = "0.10.2"
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"key": "",
|
||||
"listen": "[::]:8888",
|
||||
"session_max_age": "30m",
|
||||
"sni_certificates": [],
|
||||
"trust_x_forwarded_headers": false
|
||||
}
|
||||
},
|
||||
|
@ -138,6 +139,13 @@
|
|||
"type": "string",
|
||||
"default": "30m"
|
||||
},
|
||||
"sni_certificates": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/$defs/SniCertificateConfig"
|
||||
}
|
||||
},
|
||||
"trust_x_forwarded_headers": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
|
@ -238,6 +246,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"SniCertificateConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"certificate": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"certificate",
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"SshConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -45,3 +45,4 @@ webpki = { version = "0.22", default-features = false }
|
|||
tokio-stream.workspace = true
|
||||
git-version = { version = "0.3.9", default-features = false }
|
||||
schemars.workspace = true
|
||||
x509-parser = "0.17.0"
|
||||
|
|
|
@ -217,6 +217,12 @@ impl SshConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||
pub struct SniCertificateConfig {
|
||||
pub certificate: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||
pub struct HttpConfig {
|
||||
#[serde(default = "_default_false")]
|
||||
|
@ -244,6 +250,9 @@ pub struct HttpConfig {
|
|||
#[serde(default = "_default_cookie_max_age", with = "humantime_serde")]
|
||||
#[schemars(with = "String")]
|
||||
pub cookie_max_age: Duration,
|
||||
|
||||
#[serde(default)]
|
||||
pub sni_certificates: Vec<SniCertificateConfig>,
|
||||
}
|
||||
|
||||
impl Default for HttpConfig {
|
||||
|
@ -257,6 +266,7 @@ impl Default for HttpConfig {
|
|||
trust_x_forwarded_headers: false,
|
||||
session_max_age: _default_session_max_age(),
|
||||
cookie_max_age: _default_cookie_max_age(),
|
||||
sni_certificates: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use poem::listener::RustlsCertificate;
|
||||
|
@ -6,19 +6,23 @@ use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
|||
use rustls::sign::{CertifiedKey, SigningKey};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use x509_parser::prelude::{FromDer, GeneralName, ParsedExtension, X509Certificate};
|
||||
|
||||
use crate::RustlsSetupError;
|
||||
use crate::{HttpConfig, RustlsSetupError, SniCertificateConfig, WarpgateConfig};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TlsCertificateBundle {
|
||||
bytes: Vec<u8>,
|
||||
certificates: Vec<CertificateDer<'static>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TlsPrivateKey {
|
||||
bytes: Vec<u8>,
|
||||
key: Arc<dyn SigningKey>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TlsCertificateAndPrivateKey {
|
||||
pub certificate: TlsCertificateBundle,
|
||||
pub private_key: TlsPrivateKey,
|
||||
|
@ -44,6 +48,74 @@ impl TlsCertificateBundle {
|
|||
certificates,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sni_names(&self) -> Result<Vec<String>, RustlsSetupError> {
|
||||
if self.certificates.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Parse leaf certificate
|
||||
let cert_der = &self.certificates[0];
|
||||
let (_, cert) =
|
||||
X509Certificate::from_der(cert_der).map_err(|e| RustlsSetupError::X509(e.into()))?;
|
||||
|
||||
let mut names = Vec::new();
|
||||
|
||||
if let Some(san_ext) = cert
|
||||
.extensions()
|
||||
.iter()
|
||||
.find(|ext| ext.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME)
|
||||
{
|
||||
let san = san_ext.parsed_extension();
|
||||
if let ParsedExtension::SubjectAlternativeName(san) = san {
|
||||
for name in &san.general_names {
|
||||
match name {
|
||||
GeneralName::DNSName(dns_name) => {
|
||||
names.push(dns_name.to_string());
|
||||
}
|
||||
GeneralName::IPAddress(ip_bytes) => {
|
||||
// Convert IP bytes to string representation
|
||||
if ip_bytes.len() == 4 {
|
||||
// IPv4
|
||||
names.push(format!(
|
||||
"{}.{}.{}.{}",
|
||||
ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]
|
||||
));
|
||||
} else if ip_bytes.len() == 16 {
|
||||
// IPv6
|
||||
let mut ipv6_parts = Vec::new();
|
||||
for chunk in ip_bytes.chunks(2) {
|
||||
ipv6_parts.push(format!(
|
||||
"{:02x}{:02x}",
|
||||
chunk[0],
|
||||
chunk.get(1).unwrap_or(&0)
|
||||
));
|
||||
}
|
||||
names.push(ipv6_parts.join(":"));
|
||||
}
|
||||
}
|
||||
_ => {} // Ignore other types like email, URI, etc.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(subject) = cert.subject().iter_common_name().next() {
|
||||
if let Ok(cn) = subject.as_str() {
|
||||
names.push(cn.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates while preserving order
|
||||
let mut unique_names = Vec::new();
|
||||
for name in names {
|
||||
if !unique_names.contains(&name) {
|
||||
unique_names.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(unique_names)
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsPrivateKey {
|
||||
|
@ -110,3 +182,42 @@ impl From<TlsCertificateAndPrivateKey> for CertifiedKey {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoTlsCertificateRelativePaths {
|
||||
fn certificate_path(&self) -> PathBuf;
|
||||
fn key_path(&self) -> PathBuf;
|
||||
}
|
||||
|
||||
impl IntoTlsCertificateRelativePaths for HttpConfig {
|
||||
fn certificate_path(&self) -> PathBuf {
|
||||
self.certificate.as_str().into()
|
||||
}
|
||||
|
||||
fn key_path(&self) -> PathBuf {
|
||||
self.key.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTlsCertificateRelativePaths for SniCertificateConfig {
|
||||
fn certificate_path(&self) -> PathBuf {
|
||||
self.certificate.as_str().into()
|
||||
}
|
||||
|
||||
fn key_path(&self) -> PathBuf {
|
||||
self.key.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_certificate_and_key<R: IntoTlsCertificateRelativePaths>(
|
||||
from: &R,
|
||||
config: &WarpgateConfig,
|
||||
) -> Result<TlsCertificateAndPrivateKey, RustlsSetupError> {
|
||||
Ok(TlsCertificateAndPrivateKey {
|
||||
certificate: TlsCertificateBundle::from_file(
|
||||
config.paths_relative_to.join(&from.certificate_path()),
|
||||
)
|
||||
.await?,
|
||||
private_key: TlsPrivateKey::from_file(config.paths_relative_to.join(&from.key_path()))
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use rustls::server::VerifierBuilderError;
|
||||
use x509_parser::error::X509Error;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum RustlsSetupError {
|
||||
|
@ -14,4 +15,6 @@ pub enum RustlsSetupError {
|
|||
Io(#[from] std::io::Error),
|
||||
#[error("PKI: {0}")]
|
||||
Pki(webpki::Error),
|
||||
#[error("parsing certificate: {0}")]
|
||||
X509(#[from] X509Error),
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use bytes::BytesMut;
|
||||
use chrono::format::SecondsFormat;
|
||||
use chrono::Local;
|
||||
use chrono::format::{SecondsFormat};
|
||||
use tokio::net::UnixDatagram;
|
||||
use tracing::*;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
|
@ -33,7 +33,10 @@ where
|
|||
if !got_socket || values.contains_key(&SKIP_KEY) {
|
||||
return;
|
||||
}
|
||||
values.insert("timestamp", Local::now().to_rfc3339_opts(SecondsFormat::Nanos, false));
|
||||
values.insert(
|
||||
"timestamp",
|
||||
Local::now().to_rfc3339_opts(SecondsFormat::Nanos, false),
|
||||
);
|
||||
let _ = tx.try_send(values);
|
||||
});
|
||||
|
||||
|
|
|
@ -29,8 +29,7 @@ use tracing::*;
|
|||
use warpgate_admin::admin_api_app;
|
||||
use warpgate_common::version::warpgate_version;
|
||||
use warpgate_common::{
|
||||
ListenEndpoint, Target, TargetOptions, TlsCertificateAndPrivateKey, TlsCertificateBundle,
|
||||
TlsPrivateKey,
|
||||
load_certificate_and_key, ListenEndpoint, Target, TargetOptions, WarpgateConfig,
|
||||
};
|
||||
use warpgate_core::{ProtocolServer, Services, TargetTestError};
|
||||
use warpgate_web::Assets;
|
||||
|
@ -56,6 +55,30 @@ fn make_session_storage() -> SharedSessionStorage {
|
|||
SharedSessionStorage(Arc::new(Mutex::new(Box::<MemoryStorage>::default())))
|
||||
}
|
||||
|
||||
async fn make_rustls_config(config: &WarpgateConfig) -> Result<RustlsConfig> {
|
||||
let certificate_and_key = load_certificate_and_key(&config.store.http, config)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading TLS certificate and key: {}",
|
||||
config.store.http.certificate,
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut cfg = RustlsConfig::new().fallback(certificate_and_key.into());
|
||||
for sni in &config.store.http.sni_certificates {
|
||||
let certificate_and_key = load_certificate_and_key(sni, &config)
|
||||
.await
|
||||
.with_context(|| format!("loading SNI TLS certificate: {sni:?}",))?;
|
||||
|
||||
for name in certificate_and_key.certificate.sni_names()? {
|
||||
debug!(?name, source=?sni, "Adding SNI certificate");
|
||||
cfg = cfg.certificate(name, certificate_and_key.clone().into());
|
||||
}
|
||||
}
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
impl ProtocolServer for HTTPProtocolServer {
|
||||
async fn run(self, address: ListenEndpoint) -> Result<()> {
|
||||
let admin_api_app = admin_api_app(&self.services).into_endpoint();
|
||||
|
@ -181,37 +204,15 @@ impl ProtocolServer for HTTPProtocolServer {
|
|||
}
|
||||
});
|
||||
|
||||
let certificate_and_key = {
|
||||
let rustls_config = {
|
||||
let config = self.services.config.lock().await;
|
||||
let certificate_path = config
|
||||
.paths_relative_to
|
||||
.join(&config.store.http.certificate);
|
||||
let key_path = config.paths_relative_to.join(&config.store.http.key);
|
||||
|
||||
TlsCertificateAndPrivateKey {
|
||||
certificate: TlsCertificateBundle::from_file(&certificate_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("reading TLS private key from '{}'", key_path.display())
|
||||
})?,
|
||||
private_key: TlsPrivateKey::from_file(&key_path).await.with_context(|| {
|
||||
format!(
|
||||
"reading TLS certificate from '{}'",
|
||||
certificate_path.display()
|
||||
)
|
||||
})?,
|
||||
}
|
||||
make_rustls_config(&config).await?
|
||||
};
|
||||
|
||||
info!(?address, "Listening");
|
||||
Server::new(
|
||||
address
|
||||
.poem_listener()
|
||||
.await?
|
||||
.rustls(RustlsConfig::new().fallback(certificate_and_key.into())),
|
||||
)
|
||||
.run(app)
|
||||
.await?;
|
||||
Server::new(address.poem_listener().await?.rustls(rustls_config))
|
||||
.run(app)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@ use poem::{Body, FromRequest, IntoResponse, Request, Response};
|
|||
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite, Connector};
|
||||
use tracing::*;
|
||||
use url::Url;
|
||||
use warpgate_common::{configure_tls_connector, try_block, TargetHTTPOptions, TlsMode, WarpgateError};
|
||||
use warpgate_common::{
|
||||
configure_tls_connector, try_block, TargetHTTPOptions, TlsMode, WarpgateError,
|
||||
};
|
||||
use warpgate_web::lookup_built_file;
|
||||
|
||||
use crate::common::{SessionAuthorization, SessionExt};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Warpgate HTTP proxy",
|
||||
"version": "v0.14.1-8-g2ef3553-modified"
|
||||
"version": "v0.14.1-13-ge76df34-modified"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -11,21 +11,6 @@
|
|||
],
|
||||
"tags": [],
|
||||
"paths": {
|
||||
"/__stub__": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"AnySecurityScheme": []
|
||||
}
|
||||
],
|
||||
"operationId": "__stub__"
|
||||
}
|
||||
},
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"requestBody": {
|
||||
|
@ -304,7 +289,10 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"AnySecurityScheme": []
|
||||
"TokenSecurityScheme": []
|
||||
},
|
||||
{
|
||||
"CookieSecurityScheme": []
|
||||
}
|
||||
],
|
||||
"operationId": "get_targets"
|
||||
|
@ -451,7 +439,10 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"AnySecurityScheme": []
|
||||
"TokenSecurityScheme": []
|
||||
},
|
||||
{
|
||||
"CookieSecurityScheme": []
|
||||
}
|
||||
],
|
||||
"operationId": "get_my_credentials"
|
||||
|
@ -486,7 +477,10 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"AnySecurityScheme": []
|
||||
"TokenSecurityScheme": []
|
||||
},
|
||||
{
|
||||
"CookieSecurityScheme": []
|
||||
}
|
||||
],
|
||||
"operationId": "change_my_password"
|
||||
|
@ -521,7 +515,10 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"AnySecurityScheme": []
|
||||
"TokenSecurityScheme": []
|
||||
},
|
||||
{
|
||||
"CookieSecurityScheme": []
|
||||
}
|
||||
],
|
||||
"operationId": "add_my_public_key"
|
||||
|
@ -555,7 +552,10 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"AnySecurityScheme": []
|
||||
"TokenSecurityScheme": []
|
||||
},
|
||||
{
|
||||
"CookieSecurityScheme": []
|
||||
}
|
||||
],
|
||||
"operationId": "delete_my_public_key"
|
||||
|
@ -590,7 +590,10 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"AnySecurityScheme": []
|
||||
"TokenSecurityScheme": []
|
||||
},
|
||||
{
|
||||
"CookieSecurityScheme": []
|
||||
}
|
||||
],
|
||||
"operationId": "add_my_otp"
|
||||
|
@ -624,7 +627,10 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"AnySecurityScheme": []
|
||||
"TokenSecurityScheme": []
|
||||
},
|
||||
{
|
||||
"CookieSecurityScheme": []
|
||||
}
|
||||
],
|
||||
"operationId": "delete_my_otp"
|
||||
|
@ -1214,7 +1220,12 @@
|
|||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"AnySecurityScheme": {
|
||||
"CookieSecurityScheme": {
|
||||
"type": "apiKey",
|
||||
"name": "warpgate-http-session",
|
||||
"in": "cookie"
|
||||
},
|
||||
"TokenSecurityScheme": {
|
||||
"type": "apiKey",
|
||||
"name": "X-Warpgate-Token",
|
||||
"in": "header"
|
||||
|
|
Loading…
Add table
Reference in a new issue