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/main.rs | |
initial commitmain
Diffstat (limited to 'packet-detector/src/main.rs')
| -rw-r--r-- | packet-detector/src/main.rs | 150 |
1 files changed, 150 insertions, 0 deletions
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 | } | ||
