diff --git a/CLAUDE.md b/CLAUDE.md index 9299a88..45d9c94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` で一度だけ組み立てる | diff --git a/Cargo.toml b/Cargo.toml index 118f4c3..80e063e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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] diff --git a/src/resolve.rs b/src/resolve.rs index 3d69765..3bd24c2 100644 --- a/src/resolve.rs +++ b/src/resolve.rs @@ -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::().unwrap(), "answers are sorted"); let (tx, rx) = watch::channel(first.clone()); let tx = Arc::new(tx); diff --git a/src/source.rs b/src/source.rs index d3eee3a..4694080 100644 --- a/src/source.rs +++ b/src/source.rs @@ -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::().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"); diff --git a/src/tlsconf.rs b/src/tlsconf.rs index 2184104..cf8477b 100644 --- a/src/tlsconf.rs +++ b/src/tlsconf.rs @@ -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; @@ -735,7 +735,7 @@ pub(crate) fn client_config(up: &Upstream) -> Result, ApiError Ok(Arc::new(config)) } -fn dtls_certificate(files: &CertFiles) -> Result { +fn dtls_certificate(files: &CertFiles) -> Result { 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!( @@ -743,16 +743,43 @@ fn dtls_certificate(files: &CertFiles) -> Result Result { + 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 { + 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. @@ -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, pub connector: Option, - dtls_certs: Vec, + dtls_certs: Vec, dtls_client_verifier: Option>, dtls_upstream_roots: Option, - dtls_upstream_cert: Option, + dtls_upstream_cert: Option, } impl TlsRuntime { @@ -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() } } @@ -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() } } diff --git a/src/udp.rs b/src/udp.rs index 7bc66f5..6da4ff2 100644 --- a/src/udp.rs +++ b/src/udp.rs @@ -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}; diff --git a/tests/chain.rs b/tests/chain.rs index 06051a7..47832f7 100644 --- a/tests/chain.rs +++ b/tests/chain.rs @@ -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}; @@ -154,7 +154,7 @@ async fn mtls_with_multi_tier_client_certificates() { } } -async fn dtls_client(pki: &Pki, port: u16, cert: Option) -> Result { +async fn dtls_client(pki: &Pki, port: u16, cert: Option) -> Result { let sock = UdpSocket::bind("127.0.0.1:0").await.unwrap(); sock.connect(("127.0.0.1", port)).await.unwrap(); let conn: Arc = Arc::new(sock); @@ -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"); diff --git a/tests/common/pki.rs b/tests/common/pki.rs index 763deef..3026fe9 100644 --- a/tests/common/pki.rs +++ b/tests/common/pki.rs @@ -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; @@ -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(), } } } @@ -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)); } @@ -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(); diff --git a/tests/dtls.rs b/tests/dtls.rs index acdf586..1db9f8f 100644 --- a/tests/dtls.rs +++ b/tests/dtls.rs @@ -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}; @@ -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() }, ) @@ -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);