Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ cargo run # 設定は環境変数 RPROXY_* か .env(.env.e
| `src/resolve.rs` | 名前解決と定期再解決。失敗時は前回の結果(watch の中身)を使い続ける。テスト用に差し替え可能 |
| `src/source.rs` | PROXY protocol v1/v2 ヘッダ(v2 は TLS の TLV つき)、`IP_TRANSPARENT` ソケット、その可否の判定 |
| `src/cidr.rs` | `allow_from` の CIDR(IPv4-mapped IPv6 も IPv4 として扱う) |
| `src/tlsconf.rs` | `tls` の設定の型と検証、証明書・鍵・CA の読み込み、SNI での証明書の選択、rustls / webrtc-dtls の設定の組み立て(`TlsRuntime`) |
| `src/tlsconf.rs` | `tls` の設定の型と検証、証明書・鍵・CA の読み込み、SNI での証明書の選択、rustls / dtls クレート の設定の組み立て(`TlsRuntime`) |
| `src/sni.rs` | ClientHello からサーバ名を読む(`tls.mode: sni` と、`terminate` の `passthrough` の route。読んだバイトは転送先へそのまま送るか、`tcp.rs` の `Prefixed` で rustls に渡し直して終端する) |
| `src/starttls.rs` | SMTP / IMAP / POP3 の STARTTLS 前のやり取りと、TLS 後の転送先の挨拶の読み捨て |
| `src/dtls.rs` | 共有の UDP ソケットから 1 クライアント分のデータグラムを webrtc-dtls に渡す `Conn` |
| `src/dtls.rs` | 共有の UDP ソケットから 1 クライアント分のデータグラムを dtls クレート に渡す `Conn` |
| `src/rule.rs` | ルールの型と検証。`Features`(この版で動かせる v0.3 の設定。`GET /capabilities` の `features`。パッチで中身を入れたら true にする) |
| `src/http/` | L7(ルールの `http`)の設定の型と検証、`match` の式(Traefik と同じ書き方)の解析と評価 |
| `src/http/middleware.rs` | ミドルウェア(リダイレクト、`respond`、`ip_allow`、`headers`、パスの書き換え、`rate_limit`・`in_flight`)。`Router::compile` で一度だけ組み立てる |
Expand Down
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,13 @@ socket2 = { version = "0.6.5", features = ["all"] }
sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "mysql"] }
clap = { version = "4.6.7", features = ["derive", "env"] }
dotenvy = "0.15.7"
webrtc-dtls = { version = "0.12", features = ["pem"] }
webrtc-util = { version = "0.11", default-features = false, features = ["conn"] }
dtls = { version = "0.17.2", features = ["pem"] }
webrtc-util = { version = "0.17.2", default-features = false, features = ["conn"] }
tokio-rustls = { version = "0.26.5", default-features = false, features = ["ring", "tls12", "logging"] }
rustls-pemfile = "2.2.0"
webpki-roots = "1.0.9"
x509-parser = "0.16"
x509-parser = "0.18.1"
async-trait = "0.1.92"
rcgen = "0.13"
if-addrs = "0.15"
serde_yaml_ng = "0.10.0"
regex = "1.13.1"
Expand Down Expand Up @@ -95,6 +94,7 @@ h3-quinn = "0.0.10"
libc = "0.2.189"

[dev-dependencies]
rcgen = { version = "0.14.10", features = ["x509-parser"] }
reqwest = { version = "0.13.5", default-features = false, features = ["json"] }

[profile.release]
Expand Down
2 changes: 1 addition & 1 deletion src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ mod tests {
});

let first = resolve(&lookup, "svc:80").await.unwrap();
assert_eq!(first[0], "10.0.0.1:80".parse().unwrap(), "answers are sorted");
assert_eq!(first[0], "10.0.0.1:80".parse::<SocketAddr>().unwrap(), "answers are sorted");

let (tx, rx) = watch::channel(first.clone());
let tx = Arc::new(tx);
Expand Down
2 changes: 1 addition & 1 deletion src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ mod tests {
let v6: SocketAddr = "[fd00:2::2]:80".parse().unwrap();
let mapped: SocketAddr = "[::ffff:198.51.100.7]:5000".parse().unwrap();
let gua: SocketAddr = "[2001:db8:1::2]:5000".parse().unwrap();
assert_eq!(source_for(v4, mapped).unwrap(), "198.51.100.7:5000".parse().unwrap());
assert_eq!(source_for(v4, mapped).unwrap(), "198.51.100.7:5000".parse::<SocketAddr>().unwrap());
assert_eq!(source_for(v6, gua).unwrap(), gua);
assert!(source_for(v4, gua).is_err(), "IPv6 client to an IPv4 target");
assert!(source_for(v6, "198.51.100.7:1".parse().unwrap()).is_err(), "IPv4 client to an IPv6 target");
Expand Down
65 changes: 46 additions & 19 deletions src/tlsconf.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! TLS / DTLS settings of a rule: what the API accepts, and the rustls /
//! webrtc-dtls configurations built from it.
//! the dtls crate configurations built from it.

use std::fs;
use std::io::BufReader;
Expand Down Expand Up @@ -735,24 +735,51 @@ pub(crate) fn client_config(up: &Upstream) -> Result<Arc<ClientConfig>, ApiError
Ok(Arc::new(config))
}

fn dtls_certificate(files: &CertFiles) -> Result<webrtc_dtls::crypto::Certificate, ApiError> {
fn dtls_certificate(files: &CertFiles) -> Result<dtls::crypto::Certificate, ApiError> {
let chain = load_full_chain(&files.cert_file, files.chain_file.as_deref())?;
let PrivateKeyDer::Pkcs8(key) = load_key(&files.key_file)? else {
return Err(tls_error(format!(
"{}: DTLS needs a PKCS#8 key (convert with: openssl pkcs8 -topk8 -nocrypt -in key.pem)",
files.key_file
)));
};
let pair = rcgen::KeyPair::try_from(key.secret_pkcs8_der()).map_err(|e| tls_error(format!("{}: {e}", files.key_file)))?;
let leaf_spki = x509_parser::parse_x509_certificate(chain[0].as_ref())
.map(|(_, c)| c.public_key().raw.to_vec())
let private_key = dtls_private_key(key.secret_pkcs8_der()).map_err(|e| tls_error(format!("{}: {e}", files.key_file)))?;
let leaf_key = x509_parser::parse_x509_certificate(chain[0].as_ref())
.map(|(_, c)| c.public_key().subject_public_key.data.to_vec())
.map_err(|e| tls_error(format!("{}: {e}", files.cert_file)))?;
if leaf_spki != pair.public_key_der() {
if leaf_key != dtls_public_key(&private_key) {
return Err(tls_error(format!("{} does not belong to {}", files.key_file, files.cert_file)));
}
let private_key = webrtc_dtls::crypto::CryptoPrivateKey::try_from(&pair)
.map_err(|e| tls_error(format!("{}: {e}", files.key_file)))?;
Ok(webrtc_dtls::crypto::Certificate { certificate: chain, private_key })
Ok(dtls::crypto::Certificate { certificate: chain, private_key })
}

/// A DTLS private key from PKCS#8 DER: ECDSA P-256, Ed25519 or RSA (what the
/// dtls crate can sign with). Built with ring directly, so no rcgen type has to
/// cross into the dtls crate (which uses an older rcgen).
pub fn dtls_private_key(pkcs8: &[u8]) -> Result<dtls::crypto::CryptoPrivateKey, String> {
use dtls::crypto::{CryptoPrivateKey, CryptoPrivateKeyKind};
use ring::signature::{EcdsaKeyPair, Ed25519KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let kind = if let Ok(k) = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8, &ring::rand::SystemRandom::new()) {
CryptoPrivateKeyKind::Ecdsa256(k)
} else if let Ok(k) = Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8) {
CryptoPrivateKeyKind::Ed25519(k)
} else if let Ok(k) = ring::rsa::KeyPair::from_pkcs8(pkcs8) {
CryptoPrivateKeyKind::Rsa256(k)
} else {
return Err("DTLS needs an ECDSA P-256, Ed25519 or RSA key".into());
};
Ok(CryptoPrivateKey { kind, serialized_der: pkcs8.to_vec() })
}

/// The public key as it appears in a certificate's subjectPublicKey.
fn dtls_public_key(key: &dtls::crypto::CryptoPrivateKey) -> Vec<u8> {
use dtls::crypto::CryptoPrivateKeyKind;
use ring::signature::KeyPair;
match &key.kind {
CryptoPrivateKeyKind::Ecdsa256(k) => k.public_key().as_ref().to_vec(),
CryptoPrivateKeyKind::Ed25519(k) => k.public_key().as_ref().to_vec(),
CryptoPrivateKeyKind::Rsa256(k) => k.public_key().as_ref().to_vec(),
}
}

/// Everything needed per connection, rebuilt when the rule changes or on SIGHUP.
Expand All @@ -767,10 +794,10 @@ pub struct TlsRuntime {
/// Server side of QUIC for HTTP/3 (tcp terminate); an error text when it cannot be used.
pub quic_config: Option<QuicConfig>,
pub connector: Option<tokio_rustls::TlsConnector>,
dtls_certs: Vec<webrtc_dtls::crypto::Certificate>,
dtls_certs: Vec<dtls::crypto::Certificate>,
dtls_client_verifier: Option<Arc<dyn ClientCertVerifier>>,
dtls_upstream_roots: Option<RootCertStore>,
dtls_upstream_cert: Option<webrtc_dtls::crypto::Certificate>,
dtls_upstream_cert: Option<dtls::crypto::Certificate>,
}

impl TlsRuntime {
Expand Down Expand Up @@ -839,20 +866,20 @@ impl TlsRuntime {
self.spec.mode
}

/// DTLS server settings for one client session. webrtc-dtls proves the
/// DTLS server settings for one client session. the dtls crate proves the
/// client holds its key (CertificateVerify); the chain is checked by
/// `verify_dtls_client` right after the handshake, before any data flows.
pub fn dtls_server_config(&self) -> webrtc_dtls::config::Config {
use webrtc_dtls::config::ClientAuthType;
pub fn dtls_server_config(&self) -> dtls::config::Config {
use dtls::config::ClientAuthType;
let client_auth = match self.spec.client_auth.mode {
ClientAuthMode::None => ClientAuthType::NoClientCert,
ClientAuthMode::Optional => ClientAuthType::RequestClientCert,
ClientAuthMode::Required => ClientAuthType::RequireAnyClientCert,
};
webrtc_dtls::config::Config {
dtls::config::Config {
certificates: self.dtls_certs.clone(),
client_auth,
extended_master_secret: webrtc_dtls::config::ExtendedMasterSecretType::Require,
extended_master_secret: dtls::config::ExtendedMasterSecretType::Require,
..Default::default()
}
}
Expand All @@ -873,14 +900,14 @@ impl TlsRuntime {
}

/// DTLS client settings towards the backend.
pub fn dtls_client_config(&self, target_host: &str) -> webrtc_dtls::config::Config {
pub fn dtls_client_config(&self, target_host: &str) -> dtls::config::Config {
let up = &self.spec.upstream;
webrtc_dtls::config::Config {
dtls::config::Config {
certificates: self.dtls_upstream_cert.clone().into_iter().collect(),
roots_cas: self.dtls_upstream_roots.clone().unwrap_or_else(RootCertStore::empty),
server_name: up.server_name.clone().unwrap_or_else(|| target_host.to_string()),
insecure_skip_verify: up.insecure_skip_verify,
extended_master_secret: webrtc_dtls::config::ExtendedMasterSecretType::Require,
extended_master_secret: dtls::config::ExtendedMasterSecretType::Require,
..Default::default()
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/udp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use tokio::time::sleep_until;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};

use webrtc_dtls::conn::DTLSConn;
use dtls::conn::DTLSConn;
use webrtc_util::conn::Conn;

use crate::balance::{Lease, Member};
Expand Down
10 changes: 5 additions & 5 deletions tests/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use serde_json::{json, Value};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpStream, UdpSocket};
use tokio_rustls::TlsConnector;
use webrtc_dtls::config::{Config, ExtendedMasterSecretType};
use webrtc_dtls::conn::DTLSConn;
use dtls::config::{Config, ExtendedMasterSecretType};
use dtls::conn::DTLSConn;
use webrtc_util::conn::Conn;

use common::pki::{Issued, Pki};
Expand Down Expand Up @@ -154,7 +154,7 @@ async fn mtls_with_multi_tier_client_certificates() {
}
}

async fn dtls_client(pki: &Pki, port: u16, cert: Option<webrtc_dtls::crypto::Certificate>) -> Result<DTLSConn, String> {
async fn dtls_client(pki: &Pki, port: u16, cert: Option<dtls::crypto::Certificate>) -> Result<DTLSConn, String> {
let sock = UdpSocket::bind("127.0.0.1:0").await.unwrap();
sock.connect(("127.0.0.1", port)).await.unwrap();
let conn: Arc<dyn Conn + Send + Sync> = Arc::new(sock);
Expand Down Expand Up @@ -205,9 +205,9 @@ async fn dtls_mtls_with_multi_tier_client_certificates() {
let c = dtls_client(&pki, port, Some(alice.dtls())).await.unwrap();
assert!(dtls_echo(&c).await, "full client chain");
// leaf only: the handshake completes, but rproxy drops the session before forwarding anything
let leaf_only = webrtc_dtls::crypto::Certificate {
let leaf_only = dtls::crypto::Certificate {
certificate: vec![alice.der()],
private_key: webrtc_dtls::crypto::CryptoPrivateKey::try_from(&alice.key).unwrap(),
private_key: rproxy_api::tlsconf::dtls_private_key(&alice.key.serialize_der()).unwrap(),
};
if let Ok(c) = dtls_client(&pki, port, Some(leaf_only.clone())).await {
assert!(!dtls_echo(&c).await, "leaf only must not be forwarded");
Expand Down
16 changes: 8 additions & 8 deletions tests/common/pki.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::path::PathBuf;
use std::sync::Arc;

use rcgen::{
BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose,
BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose,
};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use rustls::RootCertStore;
Expand Down Expand Up @@ -32,11 +32,11 @@ impl Issued {
[vec![self.der()], self.intermediates.clone()].concat()
}

/// The same certificate for webrtc-dtls.
pub fn dtls(&self) -> webrtc_dtls::crypto::Certificate {
webrtc_dtls::crypto::Certificate {
/// The same certificate for the dtls crate.
pub fn dtls(&self) -> dtls::crypto::Certificate {
dtls::crypto::Certificate {
certificate: self.full_chain(),
private_key: webrtc_dtls::crypto::CryptoPrivateKey::try_from(&self.key).unwrap(),
private_key: rproxy_api::tlsconf::dtls_private_key(&self.key.serialize_der()).unwrap(),
}
}
}
Expand Down Expand Up @@ -83,8 +83,8 @@ impl Pki {
params.distinguished_name.push(DnType::CommonName, format!("rproxy test intermediate CA {}", level + 1));
params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature];
let cert = match pki.intermediates.last() {
Some((parent, parent_key)) => params.signed_by(&key, parent, parent_key).unwrap(),
None => params.signed_by(&key, &pki.ca, &pki.ca_key).unwrap(),
Some((parent, parent_key)) => params.signed_by(&key, &Issuer::from_ca_cert_der(parent.der(), parent_key).unwrap()).unwrap(),
None => params.signed_by(&key, &Issuer::from_ca_cert_der(pki.ca.der(), &pki.ca_key).unwrap()).unwrap(),
};
pki.intermediates.push((cert, key));
}
Expand Down Expand Up @@ -125,7 +125,7 @@ impl Pki {
Some((c, k)) => (c, k),
None => (&self.ca, &self.ca_key),
};
let cert = params.signed_by(&key, issuer, issuer_key).unwrap();
let cert = params.signed_by(&key, &Issuer::from_ca_cert_der(issuer.der(), issuer_key).unwrap()).unwrap();
let cert_file = self.dir.join(format!("{name}.pem")).to_string_lossy().into_owned();
let key_file = self.dir.join(format!("{name}.key")).to_string_lossy().into_owned();
std::fs::write(&cert_file, cert.pem()).unwrap();
Expand Down
8 changes: 4 additions & 4 deletions tests/dtls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use std::time::Duration;
use reqwest::StatusCode;
use serde_json::{json, Value};
use tokio::net::UdpSocket;
use webrtc_dtls::config::{Config, ExtendedMasterSecretType};
use webrtc_dtls::conn::DTLSConn;
use dtls::config::{Config, ExtendedMasterSecretType};
use dtls::conn::DTLSConn;
use webrtc_util::conn::{Conn, Listener};

use common::pki::{Issued, Pki};
Expand Down Expand Up @@ -93,7 +93,7 @@ async fn dtls_can_be_re_encrypted_towards_the_backend() {
let back = pki.server("back", &["back.test"]);

// a DTLS echo backend
let listener = webrtc_dtls::listener::listen(
let listener = dtls::listener::listen(
"127.0.0.1:0",
Config { certificates: vec![back.dtls()], extended_master_secret: ExtendedMasterSecretType::Require, ..Default::default() },
)
Expand Down Expand Up @@ -131,7 +131,7 @@ async fn dtls_can_be_re_encrypted_towards_the_backend() {
async fn dtls_needs_a_pkcs8_key() {
let pki = Pki::new("dtls-key");
let cert = pki.server("front", &["media.test"]);
// rewrite the key as SEC1 ("EC PRIVATE KEY"), which webrtc-dtls cannot load
// rewrite the key as SEC1 ("EC PRIVATE KEY"), which the dtls crate cannot load
let pem = std::fs::read_to_string(&cert.key_file).unwrap();
assert!(pem.contains("BEGIN PRIVATE KEY"));
let sec1_file = format!("{}.sec1", cert.key_file);
Expand Down
Loading