diff options
| author | root <root@sg2.noml.ch> | 2025-12-29 22:18:04 +0800 |
|---|---|---|
| committer | root <root@sg2.noml.ch> | 2025-12-29 22:18:04 +0800 |
| commit | 1951b063d7ec6d6e8db8a0b5074c73f887749208 (patch) | |
| tree | 6ece8dfce605fbff6eca6be4bbeb5d7904417bbb /packet-detector/src/bin/tls_server.rs | |
initial commitmain
Diffstat (limited to 'packet-detector/src/bin/tls_server.rs')
| -rw-r--r-- | packet-detector/src/bin/tls_server.rs | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/packet-detector/src/bin/tls_server.rs b/packet-detector/src/bin/tls_server.rs new file mode 100644 index 0000000..7f68062 --- /dev/null +++ b/packet-detector/src/bin/tls_server.rs | |||
| @@ -0,0 +1,295 @@ | |||
| 1 | //! TLS 1.2 Test Server with Client Tracking (mTLS Enabled) | ||
| 2 | //! | ||
| 3 | //! A mutual TLS (mTLS) HTTPS server for testing the eBPF TLS certificate validator. | ||
| 4 | //! Requires client certificates signed by the CA for authentication. | ||
| 5 | //! Tracks connected clients and displays their status. | ||
| 6 | |||
| 7 | use std::collections::HashMap; | ||
| 8 | use std::env; | ||
| 9 | use std::fs; | ||
| 10 | use std::net::SocketAddr; | ||
| 11 | use std::path::Path; | ||
| 12 | use std::sync::Arc; | ||
| 13 | |||
| 14 | use packet_detector::tls_util::{dn, parse_pem}; | ||
| 15 | use rcgen::{BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType}; | ||
| 16 | use rustls::pki_types::{CertificateDer, PrivateKeyDer}; | ||
| 17 | use rustls::server::{ServerConfig, WebPkiClientVerifier}; | ||
| 18 | use rustls::version::TLS12; | ||
| 19 | use rustls::RootCertStore; | ||
| 20 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; | ||
| 21 | use tokio::net::TcpListener; | ||
| 22 | use tokio::sync::RwLock; | ||
| 23 | use tokio_rustls::TlsAcceptor; | ||
| 24 | |||
| 25 | const DEFAULT_PORT: u16 = 8443; | ||
| 26 | const CA_CERT_PATH: &str = "ca_cert.pem"; | ||
| 27 | const CA_KEY_PATH: &str = "ca_key.pem"; | ||
| 28 | const SERVER_CERT_PATH: &str = "server_cert.pem"; | ||
| 29 | const SERVER_KEY_PATH: &str = "server_key.pem"; | ||
| 30 | const CLIENT_CERT_PATH: &str = "client_cert.pem"; | ||
| 31 | const CLIENT_KEY_PATH: &str = "client_key.pem"; | ||
| 32 | |||
| 33 | #[derive(Clone, Debug, serde::Serialize)] | ||
| 34 | struct Client { | ||
| 35 | ip: String, | ||
| 36 | connected_at: chrono::DateTime<chrono::Utc>, | ||
| 37 | #[serde(skip)] | ||
| 38 | last_seen: chrono::DateTime<chrono::Utc>, | ||
| 39 | requests: u64, | ||
| 40 | } | ||
| 41 | |||
| 42 | #[derive(Default)] | ||
| 43 | struct State { | ||
| 44 | clients: HashMap<String, Client>, | ||
| 45 | connections: u64, | ||
| 46 | requests: u64, | ||
| 47 | } | ||
| 48 | |||
| 49 | /// Generate a CA certificate for signing server and client certificates | ||
| 50 | fn generate_ca_certificate() -> Result<(String, String, KeyPair), Box<dyn std::error::Error>> { | ||
| 51 | println!("Generating CA certificate..."); | ||
| 52 | |||
| 53 | let mut params = CertificateParams::default(); | ||
| 54 | params.distinguished_name = dn("eBPF Test CA", "Zero Trust Network"); | ||
| 55 | params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); | ||
| 56 | params.key_usages = vec![ | ||
| 57 | KeyUsagePurpose::KeyCertSign, | ||
| 58 | KeyUsagePurpose::CrlSign, | ||
| 59 | ]; | ||
| 60 | |||
| 61 | let key_pair = KeyPair::generate()?; | ||
| 62 | let cert = params.self_signed(&key_pair)?; | ||
| 63 | |||
| 64 | Ok((cert.pem(), key_pair.serialize_pem(), key_pair)) | ||
| 65 | } | ||
| 66 | |||
| 67 | /// Generate a server certificate signed by the CA | ||
| 68 | fn generate_server_certificate( | ||
| 69 | ca_cert_pem: &str, | ||
| 70 | ca_key_pem: &str, | ||
| 71 | ) -> Result<(String, String), Box<dyn std::error::Error>> { | ||
| 72 | println!("Generating server certificate signed by CA..."); | ||
| 73 | |||
| 74 | let ca_key = KeyPair::from_pem(ca_key_pem)?; | ||
| 75 | let issuer = Issuer::from_ca_cert_pem(ca_cert_pem, ca_key)?; | ||
| 76 | |||
| 77 | let mut params = CertificateParams::default(); | ||
| 78 | params.distinguished_name = dn("localhost", "eBPF Test Server"); | ||
| 79 | params.subject_alt_names = vec![ | ||
| 80 | SanType::DnsName("localhost".try_into()?), | ||
| 81 | SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))), | ||
| 82 | ]; | ||
| 83 | params.key_usages = vec![ | ||
| 84 | KeyUsagePurpose::DigitalSignature, | ||
| 85 | KeyUsagePurpose::KeyEncipherment, | ||
| 86 | ]; | ||
| 87 | |||
| 88 | let key_pair = KeyPair::generate()?; | ||
| 89 | let cert = params.signed_by(&key_pair, &issuer)?; | ||
| 90 | |||
| 91 | Ok((cert.pem(), key_pair.serialize_pem())) | ||
| 92 | } | ||
| 93 | |||
| 94 | /// Generate a client certificate signed by the CA | ||
| 95 | fn generate_client_certificate( | ||
| 96 | client_name: &str, | ||
| 97 | ca_cert_pem: &str, | ||
| 98 | ca_key_pem: &str, | ||
| 99 | ) -> Result<(String, String), Box<dyn std::error::Error>> { | ||
| 100 | println!("Generating client certificate for: {}", client_name); | ||
| 101 | |||
| 102 | let ca_key = KeyPair::from_pem(ca_key_pem)?; | ||
| 103 | let issuer = Issuer::from_ca_cert_pem(ca_cert_pem, ca_key)?; | ||
| 104 | |||
| 105 | let mut params = CertificateParams::default(); | ||
| 106 | params.distinguished_name = dn(client_name, "eBPF Test Client"); | ||
| 107 | params.key_usages = vec![ | ||
| 108 | KeyUsagePurpose::DigitalSignature, | ||
| 109 | ]; | ||
| 110 | |||
| 111 | let key_pair = KeyPair::generate()?; | ||
| 112 | let cert = params.signed_by(&key_pair, &issuer)?; | ||
| 113 | |||
| 114 | Ok((cert.pem(), key_pair.serialize_pem())) | ||
| 115 | } | ||
| 116 | |||
| 117 | /// PKI setup result | ||
| 118 | struct PkiSetup { | ||
| 119 | ca_cert_pem: String, | ||
| 120 | server_cert: Vec<CertificateDer<'static>>, | ||
| 121 | server_key: PrivateKeyDer<'static>, | ||
| 122 | } | ||
| 123 | |||
| 124 | /// Load or generate the full PKI (CA, server cert, client cert) | ||
| 125 | fn setup_pki() -> Result<PkiSetup, Box<dyn std::error::Error>> { | ||
| 126 | let ca_cert_pem: String; | ||
| 127 | let ca_key_pem: String; | ||
| 128 | |||
| 129 | // Load or generate CA | ||
| 130 | if Path::new(CA_CERT_PATH).exists() && Path::new(CA_KEY_PATH).exists() { | ||
| 131 | println!("Loading existing CA from {} and {}", CA_CERT_PATH, CA_KEY_PATH); | ||
| 132 | ca_cert_pem = fs::read_to_string(CA_CERT_PATH)?; | ||
| 133 | ca_key_pem = fs::read_to_string(CA_KEY_PATH)?; | ||
| 134 | } else { | ||
| 135 | println!("Generating new PKI infrastructure..."); | ||
| 136 | let (cert, key, _) = generate_ca_certificate()?; | ||
| 137 | fs::write(CA_CERT_PATH, &cert)?; | ||
| 138 | fs::write(CA_KEY_PATH, &key)?; | ||
| 139 | println!("Saved CA to {} and {}", CA_CERT_PATH, CA_KEY_PATH); | ||
| 140 | ca_cert_pem = cert; | ||
| 141 | ca_key_pem = key; | ||
| 142 | } | ||
| 143 | |||
| 144 | // Load or generate server certificate | ||
| 145 | let server_cert_pem: String; | ||
| 146 | let server_key_pem: String; | ||
| 147 | |||
| 148 | if Path::new(SERVER_CERT_PATH).exists() && Path::new(SERVER_KEY_PATH).exists() { | ||
| 149 | println!("Loading existing server certificate..."); | ||
| 150 | server_cert_pem = fs::read_to_string(SERVER_CERT_PATH)?; | ||
| 151 | server_key_pem = fs::read_to_string(SERVER_KEY_PATH)?; | ||
| 152 | } else { | ||
| 153 | let (cert, key) = generate_server_certificate(&ca_cert_pem, &ca_key_pem)?; | ||
| 154 | fs::write(SERVER_CERT_PATH, &cert)?; | ||
| 155 | fs::write(SERVER_KEY_PATH, &key)?; | ||
| 156 | println!("Saved server cert to {} and {}", SERVER_CERT_PATH, SERVER_KEY_PATH); | ||
| 157 | server_cert_pem = cert; | ||
| 158 | server_key_pem = key; | ||
| 159 | } | ||
| 160 | |||
| 161 | // Load or generate client certificate | ||
| 162 | if !Path::new(CLIENT_CERT_PATH).exists() || !Path::new(CLIENT_KEY_PATH).exists() { | ||
| 163 | let (cert, key) = generate_client_certificate("test-client", &ca_cert_pem, &ca_key_pem)?; | ||
| 164 | fs::write(CLIENT_CERT_PATH, &cert)?; | ||
| 165 | fs::write(CLIENT_KEY_PATH, &key)?; | ||
| 166 | println!("Saved client cert to {} and {}", CLIENT_CERT_PATH, CLIENT_KEY_PATH); | ||
| 167 | } | ||
| 168 | |||
| 169 | // Parse certificates | ||
| 170 | let server_certs = parse_pem(&server_cert_pem)?; | ||
| 171 | |||
| 172 | let server_key = rustls_pemfile::private_key(&mut server_key_pem.as_bytes())? | ||
| 173 | .ok_or("No server private key found")?; | ||
| 174 | |||
| 175 | Ok(PkiSetup { | ||
| 176 | ca_cert_pem, | ||
| 177 | server_cert: server_certs, | ||
| 178 | server_key, | ||
| 179 | }) | ||
| 180 | } | ||
| 181 | |||
| 182 | fn parse_request(data: &[u8]) -> (&str, Option<String>) { | ||
| 183 | let req = std::str::from_utf8(data).unwrap_or(""); | ||
| 184 | let first_line = req.lines().next().unwrap_or(""); | ||
| 185 | |||
| 186 | // Check X-Client-Name header | ||
| 187 | let client = req.lines() | ||
| 188 | .find(|l| l.to_lowercase().starts_with("x-client-name:")) | ||
| 189 | .map(|l| l.split(':').nth(1).unwrap_or("").trim().to_string()) | ||
| 190 | .or_else(|| { | ||
| 191 | // Check ?client= query param | ||
| 192 | first_line.find("client=").map(|i| { | ||
| 193 | let rest = &first_line[i + 7..]; | ||
| 194 | rest[..rest.find(|c| c == '&' || c == ' ').unwrap_or(rest.len())].to_string() | ||
| 195 | }) | ||
| 196 | }); | ||
| 197 | |||
| 198 | (first_line, client) | ||
| 199 | } | ||
| 200 | |||
| 201 | async fn handle_connection( | ||
| 202 | mut stream: tokio_rustls::server::TlsStream<tokio::net::TcpStream>, | ||
| 203 | peer: SocketAddr, | ||
| 204 | state: Arc<RwLock<State>>, | ||
| 205 | ) { | ||
| 206 | state.write().await.connections += 1; | ||
| 207 | |||
| 208 | let mut buf = vec![0u8; 4096]; | ||
| 209 | let n = match stream.read(&mut buf).await { | ||
| 210 | Ok(0) => return, | ||
| 211 | Ok(n) => n, | ||
| 212 | Err(_) => return, | ||
| 213 | }; | ||
| 214 | |||
| 215 | let (path, client_name) = parse_request(&buf[..n]); | ||
| 216 | |||
| 217 | // Track client if name provided | ||
| 218 | if let Some(name) = &client_name { | ||
| 219 | let mut s = state.write().await; | ||
| 220 | s.requests += 1; | ||
| 221 | let now = chrono::Utc::now(); | ||
| 222 | s.clients.entry(name.clone()) | ||
| 223 | .and_modify(|c| { c.last_seen = now; c.requests += 1; }) | ||
| 224 | .or_insert(Client { | ||
| 225 | ip: peer.ip().to_string(), | ||
| 226 | connected_at: now, | ||
| 227 | last_seen: now, | ||
| 228 | requests: 1, | ||
| 229 | }); | ||
| 230 | } | ||
| 231 | |||
| 232 | // Route request | ||
| 233 | let (ctype, body) = if path.contains("/status") || path.contains("/clients") { | ||
| 234 | let s = state.read().await; | ||
| 235 | ("application/json", serde_json::json!({ | ||
| 236 | "clients": s.clients.len(), | ||
| 237 | "connections": s.connections, | ||
| 238 | "requests": s.requests, | ||
| 239 | "list": &s.clients | ||
| 240 | }).to_string()) | ||
| 241 | } else if path.contains("/register") { | ||
| 242 | ("application/json", serde_json::json!({ | ||
| 243 | "status": "ok", | ||
| 244 | "client": client_name.as_deref().unwrap_or("unknown") | ||
| 245 | }).to_string()) | ||
| 246 | } else { | ||
| 247 | ("text/plain", format!("TLS Server OK - {} clients", state.read().await.clients.len())) | ||
| 248 | }; | ||
| 249 | |||
| 250 | let resp = format!( | ||
| 251 | "HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", | ||
| 252 | ctype, body.len(), body | ||
| 253 | ); | ||
| 254 | let _ = stream.write_all(resp.as_bytes()).await; | ||
| 255 | let _ = stream.shutdown().await; | ||
| 256 | } | ||
| 257 | |||
| 258 | |||
| 259 | #[tokio::main] | ||
| 260 | async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
| 261 | rustls::crypto::ring::default_provider().install_default().ok(); | ||
| 262 | |||
| 263 | let port: u16 = env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_PORT); | ||
| 264 | |||
| 265 | let pki = setup_pki()?; | ||
| 266 | |||
| 267 | // Build root store with CA | ||
| 268 | let mut root_store = RootCertStore::empty(); | ||
| 269 | for cert in parse_pem(&pki.ca_cert_pem)? { | ||
| 270 | root_store.add(cert)?; | ||
| 271 | } | ||
| 272 | |||
| 273 | let verifier = WebPkiClientVerifier::builder(Arc::new(root_store)).build()?; | ||
| 274 | let config = ServerConfig::builder_with_protocol_versions(&[&TLS12]) | ||
| 275 | .with_client_cert_verifier(verifier) | ||
| 276 | .with_single_cert(pki.server_cert, pki.server_key)?; | ||
| 277 | |||
| 278 | let acceptor = TlsAcceptor::from(Arc::new(config)); | ||
| 279 | let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))).await?; | ||
| 280 | let state = Arc::new(RwLock::new(State::default())); | ||
| 281 | |||
| 282 | println!("mTLS server on :{} (endpoints: /, /status, /register)", port); | ||
| 283 | println!("Test: curl -k --tlsv1.2 --cert {} --key {} https://127.0.0.1:{}", CLIENT_CERT_PATH, CLIENT_KEY_PATH, port); | ||
| 284 | |||
| 285 | loop { | ||
| 286 | let Ok((stream, peer)) = listener.accept().await else { continue }; | ||
| 287 | let acceptor = acceptor.clone(); | ||
| 288 | let state = state.clone(); | ||
| 289 | tokio::spawn(async move { | ||
| 290 | if let Ok(tls) = acceptor.accept(stream).await { | ||
| 291 | handle_connection(tls, peer, state).await; | ||
| 292 | } | ||
| 293 | }); | ||
| 294 | } | ||
| 295 | } | ||
