diff options
Diffstat (limited to 'packet-detector/src')
| -rw-r--r-- | packet-detector/src/bin/tls_client.rs | 51 | ||||
| -rw-r--r-- | packet-detector/src/bin/tls_server.rs | 295 | ||||
| -rw-r--r-- | packet-detector/src/lib.rs | 6 | ||||
| -rw-r--r-- | packet-detector/src/main.rs | 150 | ||||
| -rw-r--r-- | packet-detector/src/tls_util.rs | 73 | ||||
| -rw-r--r-- | packet-detector/src/validator.rs | 64 |
6 files changed, 639 insertions, 0 deletions
diff --git a/packet-detector/src/bin/tls_client.rs b/packet-detector/src/bin/tls_client.rs new file mode 100644 index 0000000..6098172 --- /dev/null +++ b/packet-detector/src/bin/tls_client.rs | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | //! mTLS test client | ||
| 2 | |||
| 3 | use std::sync::Arc; | ||
| 4 | use packet_detector::tls_util::LoggingVerifier; | ||
| 5 | use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName}; | ||
| 6 | use rustls::version::TLS12; | ||
| 7 | use rustls::ClientConfig; | ||
| 8 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; | ||
| 9 | use tokio::net::TcpStream; | ||
| 10 | use tokio_rustls::TlsConnector; | ||
| 11 | |||
| 12 | const CERT: &str = "client_cert.pem"; | ||
| 13 | const KEY: &str = "client_key.pem"; | ||
| 14 | |||
| 15 | fn load_creds() -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Box<dyn std::error::Error>> { | ||
| 16 | let cert_pem = std::fs::read_to_string(CERT)?; | ||
| 17 | let key_pem = std::fs::read_to_string(KEY)?; | ||
| 18 | let certs = rustls_pemfile::certs(&mut cert_pem.as_bytes()).collect::<Result<Vec<_>, _>>()?; | ||
| 19 | let key = rustls_pemfile::private_key(&mut key_pem.as_bytes())?.ok_or("No key")?; | ||
| 20 | Ok((certs, key)) | ||
| 21 | } | ||
| 22 | |||
| 23 | #[tokio::main] | ||
| 24 | async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
| 25 | rustls::crypto::ring::default_provider().install_default().ok(); | ||
| 26 | |||
| 27 | let host = std::env::args().nth(1).unwrap_or_else(|| "127.0.0.1".into()); | ||
| 28 | let port: u16 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(8443); | ||
| 29 | |||
| 30 | let (certs, key) = load_creds()?; | ||
| 31 | println!("Connecting to {}:{} with client cert", host, port); | ||
| 32 | |||
| 33 | let config = ClientConfig::builder_with_protocol_versions(&[&TLS12]) | ||
| 34 | .dangerous() | ||
| 35 | .with_custom_certificate_verifier(Arc::new(LoggingVerifier)) | ||
| 36 | .with_client_auth_cert(certs, key)?; | ||
| 37 | |||
| 38 | let stream = TcpStream::connect(format!("{}:{}", host, port)).await?; | ||
| 39 | let mut tls = TlsConnector::from(Arc::new(config)) | ||
| 40 | .connect(ServerName::try_from(host.clone())?, stream).await?; | ||
| 41 | println!("TLS handshake complete!"); | ||
| 42 | |||
| 43 | let req = format!("GET / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", host); | ||
| 44 | tls.write_all(req.as_bytes()).await?; | ||
| 45 | |||
| 46 | let mut resp = Vec::new(); | ||
| 47 | tls.read_to_end(&mut resp).await?; | ||
| 48 | println!("\n{}", String::from_utf8_lossy(&resp)); | ||
| 49 | |||
| 50 | Ok(()) | ||
| 51 | } | ||
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 | } | ||
diff --git a/packet-detector/src/lib.rs b/packet-detector/src/lib.rs new file mode 100644 index 0000000..a3a8ac1 --- /dev/null +++ b/packet-detector/src/lib.rs | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | //! Packet Detector Library | ||
| 2 | //! | ||
| 3 | //! Shared utilities for TLS certificate validation. | ||
| 4 | |||
| 5 | pub mod tls_util; | ||
| 6 | pub mod validator; | ||
diff --git a/packet-detector/src/main.rs b/packet-detector/src/main.rs new file mode 100644 index 0000000..69cccec --- /dev/null +++ b/packet-detector/src/main.rs | |||
| @@ -0,0 +1,150 @@ | |||
| 1 | //! TLS Certificate Validator / UDP Magic Detector - eBPF-based | ||
| 2 | |||
| 3 | use std::collections::HashSet; | ||
| 4 | use std::mem::size_of; | ||
| 5 | use std::net::Ipv4Addr; | ||
| 6 | |||
| 7 | use anyhow::{Context, Result}; | ||
| 8 | use aya::maps::{HashMap as AyaHashMap, RingBuf}; | ||
| 9 | use aya::programs::{Xdp, XdpFlags}; | ||
| 10 | use aya::{include_bytes_aligned, Bpf}; | ||
| 11 | use log::{info, warn}; | ||
| 12 | use tls_parser::{parse_tls_plaintext, TlsMessage, TlsMessageHandshake}; | ||
| 13 | use tokio::signal; | ||
| 14 | |||
| 15 | use packet_detector::validator::CertValidator; | ||
| 16 | |||
| 17 | // this has to be the exact same as the struct in kernelspace | ||
| 18 | #[repr(C)] | ||
| 19 | #[derive(Clone, Copy, PartialEq, Eq, Hash)] | ||
| 20 | struct ConnKey { | ||
| 21 | port_lo: u16, | ||
| 22 | port_hi: u16, | ||
| 23 | } | ||
| 24 | |||
| 25 | unsafe impl aya::Pod for ConnKey {} | ||
| 26 | |||
| 27 | fn make_conn_key(src_port: u16, dst_port: u16) -> ConnKey { | ||
| 28 | if src_port < dst_port { | ||
| 29 | ConnKey { port_lo: src_port, port_hi: dst_port } | ||
| 30 | } else { | ||
| 31 | ConnKey { port_lo: dst_port, port_hi: src_port } | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | // this has to be the exact same as the struct in kernelspace | ||
| 36 | #[repr(C)] | ||
| 37 | #[derive(Clone, Copy)] | ||
| 38 | struct Event { | ||
| 39 | src_ip: u32, | ||
| 40 | dst_ip: u32, | ||
| 41 | src_port: u16, | ||
| 42 | dst_port: u16, | ||
| 43 | tls_len: u16, | ||
| 44 | _pad: u16, | ||
| 45 | } | ||
| 46 | |||
| 47 | unsafe impl aya::Pod for Event {} | ||
| 48 | |||
| 49 | const EVENT_SIZE: usize = size_of::<Event>(); | ||
| 50 | |||
| 51 | fn ip(n: u32) -> Ipv4Addr { | ||
| 52 | Ipv4Addr::from(n.to_be_bytes()) | ||
| 53 | } | ||
| 54 | |||
| 55 | fn extract_certs(tls_data: &[u8]) -> Option<Vec<Vec<u8>>> { | ||
| 56 | if tls_data.len() < 6 || tls_data[0] != 0x16 || tls_data[5] != 0x0B { return None; } | ||
| 57 | let (_, rec) = parse_tls_plaintext(tls_data).ok()?; | ||
| 58 | for msg in &rec.msg { | ||
| 59 | if let TlsMessage::Handshake(TlsMessageHandshake::Certificate(c)) = msg { | ||
| 60 | return Some(c.cert_chain.iter().map(|x| x.data.to_vec()).collect()); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | None | ||
| 64 | } | ||
| 65 | |||
| 66 | enum Decision { | ||
| 67 | Allow(ConnKey), | ||
| 68 | Block(ConnKey), | ||
| 69 | Skip, | ||
| 70 | } | ||
| 71 | |||
| 72 | fn handle_event(data: &[u8], validator: Option<&CertValidator>) -> Decision { | ||
| 73 | if data.len() < EVENT_SIZE { return Decision::Skip; } | ||
| 74 | let ev: Event = unsafe { std::ptr::read(data.as_ptr() as *const _) }; | ||
| 75 | let addr = format!("{}:{} -> {}:{}", ip(ev.src_ip), ev.src_port, ip(ev.dst_ip), ev.dst_port); | ||
| 76 | let conn_key = make_conn_key(ev.src_port, ev.dst_port); | ||
| 77 | |||
| 78 | if ev.tls_len == 0 { | ||
| 79 | info!("UDP magic from {}", addr); | ||
| 80 | return Decision::Allow(conn_key); | ||
| 81 | } | ||
| 82 | |||
| 83 | let Some(v) = validator else { return Decision::Skip }; | ||
| 84 | let end = EVENT_SIZE + ev.tls_len as usize; | ||
| 85 | if end > data.len() { return Decision::Skip; } | ||
| 86 | let Some(certs) = extract_certs(&data[EVENT_SIZE..end]) else { return Decision::Skip }; | ||
| 87 | let result = v.validate(&certs); | ||
| 88 | info!("{}: {}", addr, result.subject); | ||
| 89 | |||
| 90 | if result.valid { | ||
| 91 | info!("ALLOW conn {}:{} (signed by {})", conn_key.port_lo, conn_key.port_hi, result.issuer); | ||
| 92 | Decision::Allow(conn_key) | ||
| 93 | } else { | ||
| 94 | warn!("BLOCK conn {}:{} - {}", conn_key.port_lo, conn_key.port_hi, result.error.unwrap_or_default()); | ||
| 95 | Decision::Block(conn_key) | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | #[tokio::main] | ||
| 100 | async fn main() -> Result<()> { | ||
| 101 | rustls::crypto::ring::default_provider().install_default().ok(); | ||
| 102 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); | ||
| 103 | |||
| 104 | let args: Vec<String> = std::env::args().collect(); | ||
| 105 | if args.len() < 2 { | ||
| 106 | eprintln!("Usage: {} <interface> [ca-cert.pem]", args[0]); | ||
| 107 | std::process::exit(1); | ||
| 108 | } | ||
| 109 | |||
| 110 | let iface = &args[1]; | ||
| 111 | let validator = args.get(2).map(|p| CertValidator::with_ca_file(p)).transpose()?; | ||
| 112 | info!("Mode: {}", if validator.is_some() { "TLS cert validation" } else { "UDP magic detection" }); | ||
| 113 | |||
| 114 | let mut bpf = Bpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/release/packet-detector"))?; | ||
| 115 | let program: &mut Xdp = bpf.program_mut("packet_detector").unwrap().try_into()?; | ||
| 116 | program.load()?; | ||
| 117 | program.attach(iface, XdpFlags::default()).context("XDP attach failed")?; | ||
| 118 | info!("XDP attached to {}", iface); | ||
| 119 | |||
| 120 | let mut allowed: AyaHashMap<_, ConnKey, u8> = AyaHashMap::try_from(bpf.take_map("ALLOWED_CONNS").unwrap())?; | ||
| 121 | let mut blocked: AyaHashMap<_, ConnKey, u8> = AyaHashMap::try_from(bpf.take_map("BLOCKED_CONNS").unwrap())?; | ||
| 122 | let mut ring: RingBuf<_> = RingBuf::try_from(bpf.take_map("TLS_EVENTS").unwrap())?; | ||
| 123 | let mut allowed_count = 0u32; | ||
| 124 | let mut blocked_count = 0u32; | ||
| 125 | |||
| 126 | println!("\nRunning on {} - Ctrl+C to stop\n", iface); | ||
| 127 | |||
| 128 | loop { | ||
| 129 | tokio::select! { | ||
| 130 | _ = signal::ctrl_c() => break, | ||
| 131 | _ = tokio::time::sleep(tokio::time::Duration::from_millis(10)) => { | ||
| 132 | while let Some(item) = ring.next() { | ||
| 133 | match handle_event(item.as_ref(), validator.as_ref()) { | ||
| 134 | Decision::Allow(key) => { | ||
| 135 | allowed.insert(key, 1, 0)?; | ||
| 136 | allowed_count += 1; | ||
| 137 | } | ||
| 138 | Decision::Block(key) => { | ||
| 139 | blocked.insert(key, 1, 0)?; | ||
| 140 | blocked_count += 1; | ||
| 141 | } | ||
| 142 | Decision::Skip => {} | ||
| 143 | } | ||
| 144 | } | ||
| 145 | } | ||
| 146 | } | ||
| 147 | } | ||
| 148 | println!("\nAllowed: {}, Blocked: {}", allowed_count, blocked_count); | ||
| 149 | Ok(()) | ||
| 150 | } | ||
diff --git a/packet-detector/src/tls_util.rs b/packet-detector/src/tls_util.rs new file mode 100644 index 0000000..456991b --- /dev/null +++ b/packet-detector/src/tls_util.rs | |||
| @@ -0,0 +1,73 @@ | |||
| 1 | use rcgen::{DistinguishedName, DnType}; | ||
| 2 | use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; | ||
| 3 | use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; | ||
| 4 | use rustls::{DigitallySignedStruct, Error, SignatureScheme}; | ||
| 5 | use sha2::{Digest, Sha256}; | ||
| 6 | use x509_parser::prelude::*; | ||
| 7 | |||
| 8 | /// SHA256 fingerprint (truncated to 16 bytes by default) | ||
| 9 | pub fn fingerprint(cert: &CertificateDer<'_>, full: bool) -> String { | ||
| 10 | let hash = Sha256::digest(cert.as_ref()); | ||
| 11 | if full { hex::encode(hash) } else { hex::encode(&hash[..16]) } | ||
| 12 | } | ||
| 13 | |||
| 14 | pub fn dn(cn: &str, org: &str) -> DistinguishedName { | ||
| 15 | let mut d = DistinguishedName::new(); | ||
| 16 | d.push(DnType::CommonName, cn); | ||
| 17 | d.push(DnType::OrganizationName, org); | ||
| 18 | d.push(DnType::CountryName, "US"); | ||
| 19 | d | ||
| 20 | } | ||
| 21 | |||
| 22 | /// Parse certs from PEM | ||
| 23 | pub fn parse_pem(pem: &str) -> Result<Vec<CertificateDer<'static>>, std::io::Error> { | ||
| 24 | rustls_pemfile::certs(&mut pem.as_bytes()).collect() | ||
| 25 | } | ||
| 26 | |||
| 27 | fn schemes() -> Vec<SignatureScheme> { | ||
| 28 | rustls::crypto::ring::default_provider() | ||
| 29 | .signature_verification_algorithms | ||
| 30 | .supported_schemes() | ||
| 31 | } | ||
| 32 | |||
| 33 | /// Macro to implement the boilerplate verifier methods | ||
| 34 | macro_rules! impl_verifier_base { | ||
| 35 | () => { | ||
| 36 | fn verify_tls12_signature(&self, _: &[u8], _: &CertificateDer<'_>, _: &DigitallySignedStruct) -> Result<HandshakeSignatureValid, Error> { | ||
| 37 | Ok(HandshakeSignatureValid::assertion()) | ||
| 38 | } | ||
| 39 | fn verify_tls13_signature(&self, _: &[u8], _: &CertificateDer<'_>, _: &DigitallySignedStruct) -> Result<HandshakeSignatureValid, Error> { | ||
| 40 | Ok(HandshakeSignatureValid::assertion()) | ||
| 41 | } | ||
| 42 | fn supported_verify_schemes(&self) -> Vec<SignatureScheme> { | ||
| 43 | schemes() | ||
| 44 | } | ||
| 45 | }; | ||
| 46 | } | ||
| 47 | |||
| 48 | /// Accepts all certificates, logs info | ||
| 49 | #[derive(Debug)] | ||
| 50 | pub struct LoggingVerifier; | ||
| 51 | |||
| 52 | impl ServerCertVerifier for LoggingVerifier { | ||
| 53 | fn verify_server_cert(&self, cert: &CertificateDer<'_>, intermediates: &[CertificateDer<'_>], _: &ServerName<'_>, _: &[u8], _: UnixTime) -> Result<ServerCertVerified, Error> { | ||
| 54 | println!("\n=== Server Certificate ==="); | ||
| 55 | match X509Certificate::from_der(cert.as_ref()) { | ||
| 56 | Ok((_, x)) => { | ||
| 57 | println!("Subject: {}", x.subject()); | ||
| 58 | println!("Issuer: {}", x.issuer()); | ||
| 59 | println!("SHA256: {}", fingerprint(cert, true)); | ||
| 60 | if x.subject() == x.issuer() { println!("Type: Self-Signed"); } | ||
| 61 | } | ||
| 62 | Err(e) => println!("Parse failed: {}", e), | ||
| 63 | } | ||
| 64 | for (i, c) in intermediates.iter().enumerate() { | ||
| 65 | if let Ok((_, x)) = X509Certificate::from_der(c.as_ref()) { | ||
| 66 | println!("Intermediate #{}: {}", i + 1, x.subject()); | ||
| 67 | } | ||
| 68 | } | ||
| 69 | println!("Chain length: {}\n", 1 + intermediates.len()); | ||
| 70 | Ok(ServerCertVerified::assertion()) | ||
| 71 | } | ||
| 72 | impl_verifier_base!(); | ||
| 73 | } | ||
diff --git a/packet-detector/src/validator.rs b/packet-detector/src/validator.rs new file mode 100644 index 0000000..92e64d7 --- /dev/null +++ b/packet-detector/src/validator.rs | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | //! Certificate chain validation - signature only | ||
| 2 | |||
| 3 | use anyhow::{anyhow, Result}; | ||
| 4 | use rustls::pki_types::CertificateDer; | ||
| 5 | use x509_parser::prelude::*; | ||
| 6 | |||
| 7 | pub struct ValidationResult { | ||
| 8 | pub valid: bool, | ||
| 9 | pub subject: String, | ||
| 10 | pub issuer: String, | ||
| 11 | pub error: Option<String>, | ||
| 12 | } | ||
| 13 | |||
| 14 | impl ValidationResult { | ||
| 15 | fn fail(subject: String, issuer: String, err: impl ToString) -> Self { | ||
| 16 | Self { valid: false, subject, issuer, error: Some(err.to_string()) } | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | pub struct CertValidator { | ||
| 21 | ca_der: Vec<u8>, | ||
| 22 | } | ||
| 23 | |||
| 24 | impl CertValidator { | ||
| 25 | pub fn with_ca_file(path: &str) -> Result<Self> { | ||
| 26 | let pem = std::fs::read_to_string(path)?; | ||
| 27 | let der = rustls_pemfile::certs(&mut pem.as_bytes()) | ||
| 28 | .next() | ||
| 29 | .ok_or_else(|| anyhow!("No cert in PEM"))??; | ||
| 30 | Ok(Self { ca_der: der.to_vec() }) | ||
| 31 | } | ||
| 32 | |||
| 33 | pub fn validate(&self, chain: &[Vec<u8>]) -> ValidationResult { | ||
| 34 | let Some(ee_der) = chain.first() else { | ||
| 35 | return ValidationResult::fail(String::new(), String::new(), "Empty chain"); | ||
| 36 | }; | ||
| 37 | |||
| 38 | let (subject, issuer) = match X509Certificate::from_der(ee_der) { | ||
| 39 | Ok((_, c)) => (c.subject().to_string(), c.issuer().to_string()), | ||
| 40 | Err(e) => return ValidationResult::fail(String::new(), String::new(), format!("{e:?}")), | ||
| 41 | }; | ||
| 42 | |||
| 43 | let ca = CertificateDer::from(self.ca_der.clone()); | ||
| 44 | let anchor = match webpki::anchor_from_trusted_cert(&ca) { | ||
| 45 | Ok(a) => a, | ||
| 46 | Err(e) => return ValidationResult::fail(subject, issuer, format!("CA: {e:?}")), | ||
| 47 | }; | ||
| 48 | |||
| 49 | let cert = CertificateDer::from(ee_der.clone()); | ||
| 50 | let ee = match webpki::EndEntityCert::try_from(&cert) { | ||
| 51 | Ok(c) => c, | ||
| 52 | Err(e) => return ValidationResult::fail(subject, issuer, format!("{e:?}")), | ||
| 53 | }; | ||
| 54 | |||
| 55 | let intermediates: Vec<_> = chain[1..].iter().map(|c| CertificateDer::from(c.clone())).collect(); | ||
| 56 | let algos = webpki::ALL_VERIFICATION_ALGS; | ||
| 57 | let time = webpki::types::UnixTime::since_unix_epoch(std::time::Duration::from_secs(4102444800)); // 2100 | ||
| 58 | |||
| 59 | match ee.verify_for_usage(algos, &[anchor], &intermediates, time, webpki::KeyUsage::client_auth(), None, None) { | ||
| 60 | Ok(_) => ValidationResult { valid: true, subject, issuer, error: None }, | ||
| 61 | Err(e) => ValidationResult::fail(subject, issuer, format!("{e:?}")), | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
