diff options
Diffstat (limited to 'packet-detector-ebpf')
| -rw-r--r-- | packet-detector-ebpf/Cargo.toml | 12 | ||||
| -rw-r--r-- | packet-detector-ebpf/src/main.rs | 170 |
2 files changed, 182 insertions, 0 deletions
diff --git a/packet-detector-ebpf/Cargo.toml b/packet-detector-ebpf/Cargo.toml new file mode 100644 index 0000000..7241610 --- /dev/null +++ b/packet-detector-ebpf/Cargo.toml | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | [package] | ||
| 2 | name = "packet-detector-ebpf" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2021" | ||
| 5 | |||
| 6 | [dependencies] | ||
| 7 | aya-ebpf = { workspace = true } | ||
| 8 | network-types = "0.1.0" | ||
| 9 | |||
| 10 | [[bin]] | ||
| 11 | name = "packet-detector" | ||
| 12 | path = "src/main.rs" | ||
diff --git a/packet-detector-ebpf/src/main.rs b/packet-detector-ebpf/src/main.rs new file mode 100644 index 0000000..db9e410 --- /dev/null +++ b/packet-detector-ebpf/src/main.rs | |||
| @@ -0,0 +1,170 @@ | |||
| 1 | #![no_std] | ||
| 2 | #![no_main] | ||
| 3 | |||
| 4 | use aya_ebpf::{bindings::xdp_action, macros::{map, xdp}, maps::{HashMap, RingBuf}, programs::XdpContext}; | ||
| 5 | use network_types::{eth::{EthHdr, EtherType}, ip::{Ipv4Hdr, IpProto}, tcp::TcpHdr, udp::UdpHdr}; | ||
| 6 | |||
| 7 | #[repr(C)] | ||
| 8 | #[derive(Clone, Copy)] | ||
| 9 | pub struct ConnKey { | ||
| 10 | pub port_lo: u16, // lower port (client ephemeral) | ||
| 11 | pub port_hi: u16, // higher port (server, e.g. 8443) | ||
| 12 | } | ||
| 13 | |||
| 14 | #[repr(C)] | ||
| 15 | pub struct TlsEvent { | ||
| 16 | pub src_ip: u32, | ||
| 17 | pub dst_ip: u32, | ||
| 18 | pub src_port: u16, | ||
| 19 | pub dst_port: u16, | ||
| 20 | pub tls_len: u16, | ||
| 21 | pub _pad: u16, | ||
| 22 | } | ||
| 23 | |||
| 24 | #[map] | ||
| 25 | static TLS_EVENTS: RingBuf = RingBuf::with_byte_size(256 * 1024, 0); | ||
| 26 | |||
| 27 | #[map] | ||
| 28 | static ALLOWED_CONNS: HashMap<ConnKey, u8> = HashMap::with_max_entries(4096, 0); | ||
| 29 | |||
| 30 | #[map] | ||
| 31 | static BLOCKED_CONNS: HashMap<ConnKey, u8> = HashMap::with_max_entries(4096, 0); | ||
| 32 | |||
| 33 | // Magic word: "hell0123" (8 bytes) | ||
| 34 | const MAGIC: [u8; 8] = *b"hell0123"; | ||
| 35 | |||
| 36 | #[xdp] | ||
| 37 | pub fn packet_detector(ctx: XdpContext) -> u32 { | ||
| 38 | match try_process(&ctx) { | ||
| 39 | Ok(action) => action, | ||
| 40 | Err(_) => xdp_action::XDP_PASS | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | fn try_process(ctx: &XdpContext) -> Result<u32, ()> { | ||
| 45 | let eth = ptr::<EthHdr>(ctx, 0)?; | ||
| 46 | if unsafe { (*eth).ether_type } != EtherType::Ipv4.into() { return Ok(xdp_action::XDP_PASS); } | ||
| 47 | |||
| 48 | let ip = ptr::<Ipv4Hdr>(ctx, EthHdr::LEN)?; | ||
| 49 | let src_ip = u32::from_be_bytes(unsafe { (*ip).src_addr }); | ||
| 50 | let ip_hdr_len = (unsafe { (*ip).vihl } & 0x0F) as usize * 4; | ||
| 51 | |||
| 52 | match unsafe { (*ip).proto } { | ||
| 53 | IpProto::Tcp => process_tcp(ctx, ip, ip_hdr_len, src_ip), | ||
| 54 | IpProto::Udp => process_udp(ctx, ip, ip_hdr_len, src_ip), | ||
| 55 | _ => Ok(xdp_action::XDP_PASS), | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | fn make_conn_key(src_port: u16, dst_port: u16) -> ConnKey { | ||
| 60 | if src_port < dst_port { | ||
| 61 | ConnKey { port_lo: src_port, port_hi: dst_port } | ||
| 62 | } else { | ||
| 63 | ConnKey { port_lo: dst_port, port_hi: src_port } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | fn process_tcp(ctx: &XdpContext, ip: *const Ipv4Hdr, ip_hdr_len: usize, src_ip: u32) -> Result<u32, ()> { | ||
| 68 | let tcp = ptr::<TcpHdr>(ctx, EthHdr::LEN + ip_hdr_len)?; | ||
| 69 | let payload_off = EthHdr::LEN + ip_hdr_len + (unsafe { (*tcp).doff() } as usize) * 4; | ||
| 70 | |||
| 71 | let src_port = u16::from_be_bytes(unsafe { (*tcp).source }); | ||
| 72 | let dst_port = u16::from_be_bytes(unsafe { (*tcp).dest }); | ||
| 73 | |||
| 74 | // Only filter TLS ports (443/8443) | ||
| 75 | let is_tls_port = dst_port == 443 || src_port == 443 || dst_port == 8443 || src_port == 8443; | ||
| 76 | if !is_tls_port { | ||
| 77 | return Ok(xdp_action::XDP_PASS); | ||
| 78 | } | ||
| 79 | |||
| 80 | let conn_key = make_conn_key(src_port, dst_port); | ||
| 81 | |||
| 82 | // Explicitly blocked (invalid cert) - drop | ||
| 83 | if unsafe { BLOCKED_CONNS.get(&conn_key) }.is_some() { | ||
| 84 | return Ok(xdp_action::XDP_DROP); | ||
| 85 | } | ||
| 86 | |||
| 87 | // Already validated (valid cert) - allow | ||
| 88 | if unsafe { ALLOWED_CONNS.get(&conn_key) }.is_some() { | ||
| 89 | return Ok(xdp_action::XDP_PASS); | ||
| 90 | } | ||
| 91 | |||
| 92 | // Check if this packet has a TLS cert - send to userspace for validation | ||
| 93 | if is_tls_cert(ctx, payload_off) { | ||
| 94 | const SZ: usize = 512; | ||
| 95 | if let Some(tls_ptr) = bounds(ctx, payload_off, SZ) { | ||
| 96 | if let Some(mut entry) = TLS_EVENTS.reserve::<[u8; 16 + SZ]>(0) { | ||
| 97 | let p = entry.as_mut_ptr() as *mut u8; | ||
| 98 | unsafe { | ||
| 99 | core::ptr::write(p as *mut TlsEvent, TlsEvent { | ||
| 100 | src_ip, | ||
| 101 | dst_ip: u32::from_be_bytes((*ip).dst_addr), | ||
| 102 | src_port, | ||
| 103 | dst_port, | ||
| 104 | tls_len: SZ as u16, | ||
| 105 | _pad: 0, | ||
| 106 | }); | ||
| 107 | for i in 0..SZ { *p.add(16 + i) = *tls_ptr.add(i); } | ||
| 108 | } | ||
| 109 | entry.submit(0); | ||
| 110 | } | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 114 | // Allow packet through - handshake continues, userspace will decide later | ||
| 115 | Ok(xdp_action::XDP_PASS) | ||
| 116 | } | ||
| 117 | |||
| 118 | fn process_udp(ctx: &XdpContext, ip: *const Ipv4Hdr, ip_hdr_len: usize, src_ip: u32) -> Result<u32, ()> { | ||
| 119 | let udp = ptr::<UdpHdr>(ctx, EthHdr::LEN + ip_hdr_len)?; | ||
| 120 | let payload_off = EthHdr::LEN + ip_hdr_len + UdpHdr::LEN; | ||
| 121 | |||
| 122 | // Check for magic word at payload start | ||
| 123 | if let Some(data) = bounds(ctx, payload_off, MAGIC.len()) { | ||
| 124 | let mut matched = true; | ||
| 125 | for i in 0..MAGIC.len() { | ||
| 126 | if unsafe { *data.add(i) } != MAGIC[i] { matched = false; break; } | ||
| 127 | } | ||
| 128 | if matched { | ||
| 129 | // Send event to userspace (reuse TlsEvent struct) | ||
| 130 | if let Some(mut entry) = TLS_EVENTS.reserve::<[u8; 16]>(0) { | ||
| 131 | let p = entry.as_mut_ptr() as *mut u8; | ||
| 132 | unsafe { | ||
| 133 | core::ptr::write(p as *mut TlsEvent, TlsEvent { | ||
| 134 | src_ip, | ||
| 135 | dst_ip: u32::from_be_bytes((*ip).dst_addr), | ||
| 136 | src_port: u16::from_be_bytes((*udp).src), | ||
| 137 | dst_port: u16::from_be_bytes((*udp).dst), | ||
| 138 | tls_len: 0, // 0 = UDP magic match | ||
| 139 | _pad: 0, | ||
| 140 | }); | ||
| 141 | } | ||
| 142 | entry.submit(0); | ||
| 143 | } | ||
| 144 | return Ok(xdp_action::XDP_PASS); | ||
| 145 | } | ||
| 146 | // No match - drop the packet | ||
| 147 | return Ok(xdp_action::XDP_DROP); | ||
| 148 | } | ||
| 149 | Ok(xdp_action::XDP_PASS) | ||
| 150 | } | ||
| 151 | |||
| 152 | #[inline(always)] | ||
| 153 | fn is_tls_cert(ctx: &XdpContext, off: usize) -> bool { | ||
| 154 | if let Some(p) = bounds(ctx, off, 6) { | ||
| 155 | unsafe { *p == 0x16 && *p.add(1) == 0x03 && *p.add(2) <= 0x03 && *p.add(5) == 0x0B } | ||
| 156 | } else { false } | ||
| 157 | } | ||
| 158 | |||
| 159 | #[inline(always)] | ||
| 160 | fn bounds(ctx: &XdpContext, off: usize, len: usize) -> Option<*const u8> { | ||
| 161 | if ctx.data() + off + len > ctx.data_end() { None } else { Some((ctx.data() + off) as *const u8) } | ||
| 162 | } | ||
| 163 | |||
| 164 | #[inline(always)] | ||
| 165 | fn ptr<T>(ctx: &XdpContext, off: usize) -> Result<*const T, ()> { | ||
| 166 | bounds(ctx, off, core::mem::size_of::<T>()).map(|p| p as *const T).ok_or(()) | ||
| 167 | } | ||
| 168 | |||
| 169 | #[panic_handler] | ||
| 170 | fn panic(_: &core::panic::PanicInfo) -> ! { unsafe { core::hint::unreachable_unchecked() } } | ||
