Add rscap live packet capture: Rust FFI + (std pcap) Scheme bindings
ober
5f4fa3026acd9e5fdedca73239827ba751dee5f5
--- a/jerboa-native-rs/Cargo.lock +++ b/jerboa-native-rs/Cargo.lock @@ -1291,6 +1291,7 @@ dependencies = [ "rcgen", "regex", "ring", + "rscap", "rusqlite", "rustls", "rustls-pemfile", @@ -1746,6 +1747,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] +name = "pkts-common" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed92b6fedad9f4f2f1653adafd9bf315bfb7879ab6e455d4fffe2e73ded2f4d" + +[[package]] name = "plain" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2144,6 +2151,19 @@ dependencies = [ ] [[package]] +name = "rscap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14be74c573a9860943a23c03c64366d8eec75fbe1eca8f0bbd6dc853c69e5e4" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "pkts-common", + "windows-sys 0.59.0", +] + +[[package]] name = "rusqlite" version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3180,6 +3200,15 @@ dependencies = [ [[package]] name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" --- a/jerboa-native-rs/Cargo.toml +++ b/jerboa-native-rs/Cargo.toml @@ -31,6 +31,7 @@ getrandom = "0.2" duckdb = { version = "1", features = ["bundled"] } mozjs = { git = "https://github.com/servo/mozjs.git", optional = true, default-features = false, features = ["jit", "libz-sys"] } mozjs_sys = { git = "https://github.com/servo/mozjs.git", optional = true } +rscap = "0.3" [features] default = [] --- a/jerboa-native-rs/src/lib.rs +++ b/jerboa-native-rs/src/lib.rs @@ -1,4 +1,5 @@ mod panic; +mod pcap_capture; mod crypto; mod compress; mod duckdb_native; new file mode 100644 --- /dev/null +++ b/jerboa-native-rs/src/pcap_capture.rs @@ -0,0 +1,180 @@ +//! pcap_capture.rs — Live packet capture FFI via rscap +//! +//! Provides: +//! jerboa_pcap_open(iface, iface_len) -> i64 // returns handle or -1 +//! jerboa_pcap_next(handle, buf, buf_len, ts_sec_out, ts_usec_out) -> i32 // bytes read or -1 +//! jerboa_pcap_close(handle) -> i32 +//! jerboa_pcap_list_interfaces(buf, buf_len) -> i32 // bytes written or -1 +//! +//! The handle is a Box<rscap::Sniffer> cast to i64 (raw pointer). +//! The Sniffer is activated (ready to recv) immediately after open. +//! Scheme is responsible for calling jerboa_pcap_close exactly once per handle. + +use crate::panic::set_last_error; +use rscap::{Interface, Sniffer}; +use std::time::{SystemTime, UNIX_EPOCH}; + +// ── Open ────────────────────────────────────────────────────────────────────── + +/// Open and activate a live capture on the named interface. +/// Returns a handle (i64 > 0) on success, or -1 on error (check jerboa_last_error). +#[no_mangle] +pub extern "C" fn jerboa_pcap_open(iface_ptr: *const u8, iface_len: usize) -> i64 { + if iface_ptr.is_null() { + set_last_error("null interface pointer".to_string()); + return -1; + } + let iface_str = unsafe { + match std::str::from_utf8(std::slice::from_raw_parts(iface_ptr, iface_len)) { + Ok(s) => s, + Err(e) => { + set_last_error(format!("invalid UTF-8 in interface name: {e}")); + return -1; + } + } + }; + let iface = match Interface::new(iface_str) { + Ok(i) => i, + Err(e) => { + set_last_error(format!("pcap_open: invalid interface '{iface_str}': {e}")); + return -1; + } + }; + let mut sniffer = match Sniffer::new(iface) { + Ok(s) => s, + Err(e) => { + set_last_error(format!("pcap_open: Sniffer::new failed: {e}")); + return -1; + } + }; + // Activate with no filter (capture all packets) + if let Err(e) = sniffer.activate(None) { + set_last_error(format!("pcap_open: activate failed: {e}")); + return -1; + } + Box::into_raw(Box::new(sniffer)) as i64 +} + +// ── Next packet ─────────────────────────────────────────────────────────────── + +/// Receive the next packet into buf[0..buf_len]. +/// Writes arrival timestamp seconds into *ts_sec_out (if non-null). +/// Writes arrival timestamp microseconds into *ts_usec_out (if non-null). +/// Returns the number of bytes written on success, -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_pcap_next( + handle: i64, + buf: *mut u8, + buf_len: usize, + ts_sec_out: *mut u64, + ts_usec_out: *mut u64, +) -> i32 { + if handle <= 0 { + set_last_error("invalid handle".to_string()); + return -1; + } + if buf.is_null() || buf_len == 0 { + set_last_error("null/empty output buffer".to_string()); + return -1; + } + // Safety: handle was produced by Box::into_raw in jerboa_pcap_open + let sniffer = unsafe { &mut *(handle as *mut Sniffer) }; + let out = unsafe { std::slice::from_raw_parts_mut(buf, buf_len) }; + + match sniffer.recv(out) { + Ok(n) => { + let dur = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + if !ts_sec_out.is_null() { + unsafe { *ts_sec_out = dur.as_secs() }; + } + if !ts_usec_out.is_null() { + unsafe { *ts_usec_out = dur.subsec_micros() as u64 }; + } + n as i32 + } + Err(e) => { + set_last_error(format!("pcap_next: {e}")); + -1 + } + } +} + +// ── Close ───────────────────────────────────────────────────────────────────── + +/// Close and free a capture handle. +#[no_mangle] +pub extern "C" fn jerboa_pcap_close(handle: i64) -> i32 { + if handle <= 0 { + return -1; + } + // Safety: handle was produced by Box::into_raw in jerboa_pcap_open + let _ = unsafe { Box::from_raw(handle as *mut Sniffer) }; + 0 +} + +// ── List interfaces ─────────────────────────────────────────────────────────── + +/// Write a newline-separated list of network interface names into buf (NUL-terminated). +/// Returns bytes written (excluding NUL), or -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_pcap_list_interfaces(buf: *mut u8, buf_len: usize) -> i32 { + if buf.is_null() || buf_len == 0 { + set_last_error("null/empty buffer for interface list".to_string()); + return -1; + } + match collect_interfaces() { + Ok(names) => { + let joined = names.join("\n"); + let bytes = joined.as_bytes(); + let copy_len = bytes.len().min(buf_len.saturating_sub(1)); + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, copy_len); + *buf.add(copy_len) = 0; + } + copy_len as i32 + } + Err(e) => { + set_last_error(format!("list_interfaces: {e}")); + -1 + } + } +} + +// ── Interface enumeration ───────────────────────────────────────────────────── + +#[cfg(unix)] +fn collect_interfaces() -> Result<Vec<String>, String> { + use libc::{freeifaddrs, getifaddrs, ifaddrs}; + use std::ffi::CStr; + + let mut names: Vec<String> = Vec::new(); + let mut ifap: *mut ifaddrs = std::ptr::null_mut(); + + if unsafe { getifaddrs(&mut ifap) } != 0 { + return Err("getifaddrs failed".to_string()); + } + + let mut cursor = ifap; + while !cursor.is_null() { + let ifa = unsafe { &*cursor }; + if !ifa.ifa_name.is_null() { + let name = unsafe { CStr::from_ptr(ifa.ifa_name) } + .to_string_lossy() + .into_owned(); + if !names.contains(&name) { + names.push(name); + } + } + cursor = ifa.ifa_next; + } + + unsafe { freeifaddrs(ifap) }; + Ok(names) +} + +#[cfg(not(unix))] +fn collect_interfaces() -> Result<Vec<String>, String> { + Err("interface enumeration not supported on this platform".to_string()) +} new file mode 100644 --- /dev/null +++ b/lib/std/pcap.sls @@ -0,0 +1,156 @@ +#!chezscheme +;;; (std pcap) — Live packet capture via rscap (Rust) +;;; +;;; Wraps libjerboa_native.so FFI for raw layer-2 packet capture. +;;; +;;; Usage: +;;; (import (std pcap)) +;;; (def cap (pcap-open "eth0")) ; open + activate capture +;;; (def pkt (pcap-next cap)) ; => #(bytevector ts-sec ts-usec) or #f +;;; (pcap-close cap) ; free handle +;;; (pcap-interfaces) ; => ("lo" "eth0" ...) +;;; +;;; Notes: +;;; - Requires root or CAP_NET_RAW on Linux; root on macOS (BPF device). +;;; - pcap-next blocks until a packet arrives. +;;; - The returned bytevector is a fresh copy each call — safe to retain. + +(library (std pcap) + (export + pcap-open + pcap-next + pcap-close + pcap-interfaces + pcap-available?) + + (import (chezscheme)) + + ;; Load native library — try relative paths first, then absolute fallbacks + (define _loaded + (let* ([home (or (getenv "HOME") "")] + [jerboa-dir (or (getenv "JERBOA_HOME") + (string-append home "/mine/jerboa"))]) + (or (guard (e [#t #f]) (load-shared-object "libjerboa_native.so") #t) + (guard (e [#t #f]) (load-shared-object "lib/libjerboa_native.so") #t) + (guard (e [#t #f]) (load-shared-object "libjerboa_native.dylib") #t) + (guard (e [#t #f]) (load-shared-object "lib/libjerboa_native.dylib") #t) + ;; Absolute fallback for macOS (dylib in JERBOA_HOME/lib/) + (guard (e [#t #f]) + (load-shared-object (string-append jerboa-dir "/lib/libjerboa_native.dylib")) + #t) + ;; Absolute fallback for Linux + (guard (e [#t #f]) + (load-shared-object (string-append jerboa-dir "/lib/libjerboa_native.so")) + #t) + #f))) + + (define (pcap-available?) _loaded) + + ;; ── Sub-bytevector helper (bytevector-copy in Chez takes 1 arg, not 3) ──── + + (define (bv-sub bv start len) + (let ([out (make-bytevector len)]) + (bytevector-copy! bv start out 0 len) + out)) + + ;; ── FFI declarations ────────────────────────────────────────────────────── + + (define c-pcap-open + (foreign-procedure "jerboa_pcap_open" (u8* size_t) integer-64)) + + (define c-pcap-next + (foreign-procedure "jerboa_pcap_next" + (integer-64 u8* size_t u8* u8*) int)) + + (define c-pcap-close + (foreign-procedure "jerboa_pcap_close" (integer-64) int)) + + (define c-pcap-list-interfaces + (foreign-procedure "jerboa_pcap_list_interfaces" (u8* size_t) int)) + + (define c-last-error + (foreign-procedure "jerboa_last_error" (u8* size_t) size_t)) + + ;; ── Error helper ───────────────────────────────────────────────────────── + + (define (last-error) + (let ([buf (make-bytevector 1024 0)]) + (let ([n (c-last-error buf 1024)]) + (if (> n 0) + (utf8->string (bv-sub buf 0 (min n 1023))) + "unknown error")))) + + ;; ── pcap-open ───────────────────────────────────────────────────────────── + + ;;; Open a live capture on IFACE (string, e.g. "eth0"). + ;;; Returns an opaque handle. Raises an error on failure. + ;;; Requires root/CAP_NET_RAW. + (define (pcap-open iface) + (unless _loaded + (error 'pcap-open "libjerboa_native not available")) + (let* ([bv (string->utf8 iface)] + [handle (c-pcap-open bv (bytevector-length bv))]) + (when (< handle 0) + (error 'pcap-open (last-error) iface)) + handle)) + + ;; ── pcap-next ──────────────────────────────────────────────────────────── + + ;;; Block until the next packet arrives on the capture. + ;;; Returns a vector #(data ts-sec ts-usec) where data is a bytevector. + ;;; Returns #f if the sniffer is deactivated/closed. + ;;; Raises on hard errors. + (define pcap-next + (let ([buf (make-bytevector 65536)]) ; reused per call — copied on return + (define ts-sec-bv (make-bytevector 8 0)) + (define ts-usec-bv (make-bytevector 8 0)) + (lambda (handle) + (let ([n (c-pcap-next handle buf 65536 ts-sec-bv ts-usec-bv)]) + (cond + [(> n 0) + (let ([data (bv-sub buf 0 n)] + [ts-sec (bytevector-u64-native-ref ts-sec-bv 0)] + [ts-usec (bytevector-u64-native-ref ts-usec-bv 0)]) + (vector data ts-sec ts-usec))] + [(= n 0) #f] ; deactivated / no data (non-blocking WouldBlock) + [else + (let ([msg (last-error)]) + ;; NotConnected = deactivated cleanly + (if (string=? msg "") + #f + (error 'pcap-next msg)))]))))) + + ;; ── pcap-close ─────────────────────────────────────────────────────────── + + ;;; Close a capture handle and free its resources. + (define (pcap-close handle) + (c-pcap-close handle) + (void)) + + ;; ── pcap-interfaces ────────────────────────────────────────────────────── + + ;;; Return a list of network interface name strings available on this host. + (define (pcap-interfaces) + (unless _loaded + (error 'pcap-interfaces "libjerboa_native not available")) + (let ([buf (make-bytevector 4096 0)]) + (let ([n (c-pcap-list-interfaces buf 4096)]) + (if (< n 0) + (error 'pcap-interfaces (last-error)) + (let ([s (utf8->string (bv-sub buf 0 n))]) + (if (string=? s "") + '() + ;; Manual newline split — string-split is Jerboa prelude, not in base Chez + (let loop ([i 0] [start 0] [acc '()] [len (string-length s)]) + (cond + [(= i len) + (let ([part (substring s start i)]) + (reverse (if (string=? part "") acc (cons part acc))))] + [(char=? (string-ref s i) #\newline) + (let ([part (substring s start i)]) + (loop (+ i 1) (+ i 1) + (if (string=? part "") acc (cons part acc)) + len))] + [else (loop (+ i 1) start acc len)])))))))) + + ) ;; end library