summaryrefslogtreecommitdiff
path: root/packet-detector/src/main.rs
diff options
context:
space:
mode:
authorroot <root@sg2.noml.ch>2025-12-29 22:18:04 +0800
committerroot <root@sg2.noml.ch>2025-12-29 22:18:04 +0800
commit1951b063d7ec6d6e8db8a0b5074c73f887749208 (patch)
tree6ece8dfce605fbff6eca6be4bbeb5d7904417bbb /packet-detector/src/main.rs
initial commitmain
Diffstat (limited to 'packet-detector/src/main.rs')
-rw-r--r--packet-detector/src/main.rs150
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
3use std::collections::HashSet;
4use std::mem::size_of;
5use std::net::Ipv4Addr;
6
7use anyhow::{Context, Result};
8use aya::maps::{HashMap as AyaHashMap, RingBuf};
9use aya::programs::{Xdp, XdpFlags};
10use aya::{include_bytes_aligned, Bpf};
11use log::{info, warn};
12use tls_parser::{parse_tls_plaintext, TlsMessage, TlsMessageHandshake};
13use tokio::signal;
14
15use 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)]
20struct ConnKey {
21 port_lo: u16,
22 port_hi: u16,
23}
24
25unsafe impl aya::Pod for ConnKey {}
26
27fn 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)]
38struct 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
47unsafe impl aya::Pod for Event {}
48
49const EVENT_SIZE: usize = size_of::<Event>();
50
51fn ip(n: u32) -> Ipv4Addr {
52 Ipv4Addr::from(n.to_be_bytes())
53}
54
55fn 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
66enum Decision {
67 Allow(ConnKey),
68 Block(ConnKey),
69 Skip,
70}
71
72fn 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]
100async 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}