/*===================================================================== * hellgated.c - THE ROUTER FROM HELL - hellgate daemon + CLI (v3) * * ONE binary, two personalities: * * hellgate daemon run the daemon (foreground; OpenRC * backgrounds it) * hellgate [arg] act as a client: connect to the * daemon's socket, send, print reply * * The daemon: * - embeds the XDP program (bpftool-generated skeleton compiled in; * no hellgate.o on disk, no bpftool at runtime) * - loads it once, pins maps under /sys/fs/bpf/hellgate (reuses * existing pins across restarts - counters and rules survive) * - attaches to the interface named in /etc/hellgate.conf * - WATCHES RTNETLINK: if the configured NIC appears later (boot * ordering, driver reload, or you finally swap out the X710), * it attaches automatically. NIC swap = `hellgate move `. * - serves a line protocol on /run/hellgate.sock (root, 0600): * request: one line, e.g. "status\n" / "block 1.2.3.0/24\n" * response: zero+ data lines, then "OK\n" or "ERR \n" * - enforces guard rails centrally: refuses to drop dport 22 or a * local WireGuard listen-port, no matter which client asks * * Commands (CLI == wire protocol, same words): * ping | status | stats | ifaces | seed * attach | attach-generic | detach | move * block | unblock * drop-port | undrop-port * * hellmon connects to the same socket (see hellgate_page.c v2) and * keeps reading the pinned maps directly for per-tick stats - the * socket carries control + status, the maps carry the data plane. * * Footprint: single thread, poll(2) loop, static client slots, one * per-CPU scratch buffer allocated at startup, zero mallocs and zero * forks in steady state (the only popen is `wg show` on a drop-port * request, for the guard). RSS lands around 1 MB, most of it libbpf. * * Build: see Makefile (needs libbpf, clang for the .bpf.o, bpftool * for the skeleton - all build-time only). *=====================================================================*/ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "hellgate.skel.h" #define HG_SOCK "/run/hellgate.sock" #define HG_CONF "/etc/hellgate.conf" #define HG_PIN "/sys/fs/bpf/hellgate" #define HG_DEF_IF "enp16s0f0np0" #define HG_MAX_CLI 4 #define HG_LINE 256 /* must mirror hellgate.bpf.c */ enum { ST_PASS = 0, ST_DROP_SRC, ST_DROP_DPORT, ST_PASS_NONIP, ST_DROP_MALFORMED, ST_MAX }; static const char *ST_NAMES[ST_MAX] = { "passed", "dropped_src", "dropped_dport", "passed_non_ip", "malformed" }; struct lpm_v4_key { __u32 prefixlen; __u32 addr; }; static const char *BOGONS[] = { "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10", /* CGN - drop if Socket CGNs you */ "127.0.0.0/8", "169.254.0.0/16", "192.0.2.0/24", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/3", /* mcast+classE as WAN src */ /* 0.0.0.0/8 deliberately absent: DHCP discover src 0.0.0.0 */ }; #define NBOGONS ((int)(sizeof BOGONS / sizeof BOGONS[0])) /*----- daemon state -------------------------------------------------*/ static struct hellgate_bpf *g_skel; static int g_fd_drop = -1, g_fd_ports = -1, g_fd_stats = -1, g_fd_prog = -1; static int g_ncpu; static __u64 *g_pcpu; /* one scratch buf, startup */ static char g_if[IFNAMSIZ] = HG_DEF_IF; static volatile sig_atomic_t g_quit; struct client { int fd; char buf[HG_LINE]; int len; }; static struct client g_cli[HG_MAX_CLI]; static void logline(const char *fmt, ...){ char ts[32]; time_t t = time(NULL); struct tm tmv; localtime_r(&t, &tmv); strftime(ts, sizeof ts, "%H:%M:%S", &tmv); fprintf(stderr, "[%s] hellgated: ", ts); va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); } /*----- conf ---------------------------------------------------------*/ static void conf_load(void){ FILE *f = fopen(HG_CONF, "r"); if (!f) return; char line[128]; while (fgets(line, sizeof line, f)) { char *p = strstr(line, "WAN_IF="); if (!p) continue; p += 7; if (*p == '"' || *p == '\'') p++; size_t i = 0; while (*p && *p != '"' && *p != '\'' && *p != '\n' && *p != ' ' && i + 1 < sizeof g_if) g_if[i++] = *p++; g_if[i] = 0; } fclose(f); } static int conf_save(void){ FILE *f = fopen(HG_CONF, "w"); if (!f) return -1; fprintf(f, "# written by hellgated - the gate interface\nWAN_IF=\"%s\"\n", g_if); fclose(f); return 0; } /*----- misc helpers -------------------------------------------------*/ static int ifname_ok(const char *s){ if (!*s || strlen(s) >= IFNAMSIZ) return 0; for (; *s; s++) if (!(isalnum((unsigned char)*s) || *s=='_' || *s=='-' || *s=='.')) return 0; return 1; } static int parse_cidr(const char *s, struct lpm_v4_key *k){ char tmp[64], *slash; snprintf(tmp, sizeof tmp, "%s", s); slash = strchr(tmp, '/'); int plen = 32; if (slash) { *slash = 0; plen = atoi(slash + 1); } if (plen < 0 || plen > 32) return -1; struct in_addr a; if (inet_pton(AF_INET, tmp, &a) != 1) return -1; k->prefixlen = (__u32)plen; k->addr = a.s_addr; return 0; } /* current attach mode on ifname: 0 none, else XDP_FLAGS_* used */ static __u32 xdp_state(const char *ifname, const char **modestr){ static const char *none = "-"; if (modestr) *modestr = none; int idx = (int)if_nametoindex(ifname); if (idx <= 0) return 0; LIBBPF_OPTS(bpf_xdp_query_opts, q); if (bpf_xdp_query(idx, 0, &q) != 0) return 0; switch (q.attach_mode) { case XDP_ATTACHED_DRV: if (modestr) *modestr = "xdpdrv"; return XDP_FLAGS_DRV_MODE; case XDP_ATTACHED_SKB: if (modestr) *modestr = "xdpgeneric"; return XDP_FLAGS_SKB_MODE; case XDP_ATTACHED_HW: if (modestr) *modestr = "xdpoffload"; return XDP_FLAGS_HW_MODE; case XDP_ATTACHED_MULTI: if (modestr) *modestr = "multi"; return XDP_FLAGS_DRV_MODE; default: return 0; } } static int xdp_attach_if(const char *ifname, int generic, char *err, size_t en){ int idx = (int)if_nametoindex(ifname); if (idx <= 0) { snprintf(err, en, "no such interface: %s", ifname); return -1; } __u32 flags = generic ? XDP_FLAGS_SKB_MODE : XDP_FLAGS_DRV_MODE; int rc = bpf_xdp_attach(idx, g_fd_prog, flags, NULL); if (rc) { snprintf(err, en, "attach %s: %s", ifname, strerror(-rc)); return -1; } return 0; } static int phys_nics(char names[][IFNAMSIZ], int max){ int n = 0; DIR *d = opendir("/sys/class/net"); if (!d) return 0; struct dirent *de; while ((de = readdir(d)) && n < max) { if (de->d_name[0] == '.') continue; if (!ifname_ok(de->d_name)) continue; char p[300]; snprintf(p, sizeof p, "/sys/class/net/%s/device", de->d_name); if (access(p, F_OK) != 0) continue; /* skip wg/vlan/lo */ snprintf(names[n++], IFNAMSIZ, "%s", de->d_name); } closedir(d); return n; } static void xdp_detach_all(int cfd){ char names[16][IFNAMSIZ]; int n = phys_nics(names, 16), any = 0; for (int i = 0; i < n; i++) { __u32 fl = xdp_state(names[i], NULL); if (!fl) continue; int idx = (int)if_nametoindex(names[i]); if (idx > 0 && bpf_xdp_detach(idx, fl, NULL) == 0) { if (cfd >= 0) dprintf(cfd, "detached %s\n", names[i]); logline("detached from %s", names[i]); any = 1; } } if (!any && cfd >= 0) dprintf(cfd, "was not attached anywhere\n"); } static int drop4_empty(void){ struct lpm_v4_key k; return bpf_map_get_next_key(g_fd_drop, NULL, &k) != 0; } static int seed(int cfd){ __u8 v = 1; int bad = 0; for (int i = 0; i < NBOGONS; i++) { struct lpm_v4_key k; if (parse_cidr(BOGONS[i], &k) || bpf_map_update_elem(g_fd_drop, &k, &v, BPF_ANY)) { bad++; continue; } if (cfd >= 0) dprintf(cfd, "drop %s\n", BOGONS[i]); } return bad; } /* guard: never drop SSH or a local WireGuard listen-port */ static int port_forbidden(unsigned p, char *why, size_t n){ if (p == 22) { snprintf(why, n, "22 is SSH"); return 1; } FILE *f = popen("wg show all listen-port 2>/dev/null", "r"); if (f) { char dev[64]; unsigned lp; while (fscanf(f, "%63s %u", dev, &lp) == 2) if (lp == p) { pclose(f); snprintf(why, n, "%u is WireGuard %s listen-port", p, dev); return 1; } pclose(f); } return 0; } /*----- command handling ---------------------------------------------*/ static void cmd_status(int cfd){ const char *mode; __u32 fl = xdp_state(g_if, &mode); dprintf(cfd, "iface %s\nattached %d %s\n", g_if, fl ? 1 : 0, mode); char names[16][IFNAMSIZ]; int n = phys_nics(names, 16); for (int i = 0; i < n; i++) { const char *m; if (xdp_state(names[i], &m)) dprintf(cfd, "gate %s %s\n", names[i], m); } } static void cmd_stats(int cfd){ for (__u32 i = 0; i < ST_MAX; i++) { unsigned long long sum = 0; if (bpf_map_lookup_elem(g_fd_stats, &i, g_pcpu) == 0) for (int c = 0; c < g_ncpu; c++) sum += g_pcpu[c]; dprintf(cfd, "%s %llu\n", ST_NAMES[i], sum); } } static void cmd_ifaces(int cfd){ char names[16][IFNAMSIZ]; int n = phys_nics(names, 16); for (int i = 0; i < n; i++) { char p[300], drv[64] = "?", st[16] = "?", spd[24] = "-", lnk[256]; snprintf(p, sizeof p, "/sys/class/net/%s/device/driver", names[i]); ssize_t l = readlink(p, lnk, sizeof lnk - 1); if (l > 0) { lnk[l] = 0; char *b = strrchr(lnk, '/'); snprintf(drv, sizeof drv, "%.63s", b ? b + 1 : lnk); } snprintf(p, sizeof p, "/sys/class/net/%s/operstate", names[i]); FILE *f = fopen(p, "r"); if (f) { if (fscanf(f, "%15s", st) != 1) st[0] = '?'; fclose(f); } snprintf(p, sizeof p, "/sys/class/net/%s/speed", names[i]); f = fopen(p, "r"); if (f) { long s; if (fscanf(f, "%ld", &s) == 1 && s > 0) snprintf(spd, sizeof spd, "%ld", s); fclose(f); } dprintf(cfd, "%s %s %s %s %d\n", names[i], drv, st, spd, xdp_state(names[i], NULL) ? 1 : 0); } } static void handle_cmd(struct client *cl, char *line){ int cfd = cl->fd; char *verb = strtok(line, " \t\r\n"); char *arg = strtok(NULL, " \t\r\n"); char err[128]; if (!verb) { dprintf(cfd, "ERR empty\n"); return; } if (!strcasecmp(verb, "ping")) { dprintf(cfd, "pong\nOK\n"); } else if (!strcasecmp(verb, "status")) { cmd_status(cfd); dprintf(cfd, "OK\n"); } else if (!strcasecmp(verb, "stats")) { cmd_stats(cfd); dprintf(cfd, "OK\n"); } else if (!strcasecmp(verb, "ifaces")) { cmd_ifaces(cfd); dprintf(cfd, "OK\n"); } else if (!strcasecmp(verb, "seed")) { int bad = seed(cfd); if (bad) dprintf(cfd, "ERR %d entries failed\n", bad); else dprintf(cfd, "OK\n"); } else if (!strcasecmp(verb, "attach") || !strcasecmp(verb, "attach-generic")) { if (xdp_attach_if(g_if, verb[6] != 0, err, sizeof err)) dprintf(cfd, "ERR %s\n", err); else { logline("attached to %s (%s)", g_if, verb); if (drop4_empty()) seed(-1); dprintf(cfd, "attached %s\nOK\n", g_if); } } else if (!strcasecmp(verb, "detach")) { xdp_detach_all(cfd); dprintf(cfd, "OK\n"); } else if (!strcasecmp(verb, "move")) { if (!arg || !ifname_ok(arg) || if_nametoindex(arg) == 0) { dprintf(cfd, "ERR usage: move \n"); return; } xdp_detach_all(cfd); snprintf(g_if, sizeof g_if, "%s", arg); if (conf_save()) dprintf(cfd, "warn: could not write %s\n", HG_CONF); if (xdp_attach_if(g_if, 0, err, sizeof err)) dprintf(cfd, "ERR moved config but %s\n", err); else { logline("moved gate to %s", g_if); if (drop4_empty()) seed(-1); dprintf(cfd, "gate now on %s\nOK\n", g_if); } } else if (!strcasecmp(verb, "block") || !strcasecmp(verb, "unblock")) { struct lpm_v4_key k; if (!arg || parse_cidr(arg, &k)) { dprintf(cfd, "ERR usage: %s \n", verb); return; } int rc; if (verb[0] == 'b' || verb[0] == 'B') { __u8 v = 1; rc = bpf_map_update_elem(g_fd_drop, &k, &v, BPF_ANY); } else rc = bpf_map_delete_elem(g_fd_drop, &k); if (rc) dprintf(cfd, "ERR %s\n", strerror(-rc)); else dprintf(cfd, "%sed %s\nOK\n", verb, arg); } else if (!strcasecmp(verb, "drop-port") || !strcasecmp(verb, "undrop-port")) { unsigned p = arg ? (unsigned)atoi(arg) : 0; if (p < 1 || p > 65535) { dprintf(cfd, "ERR usage: %s <1-65535>\n", verb); return; } __u16 k = (__u16)p; int rc; if (verb[0] == 'd' || verb[0] == 'D') { char why[96]; if (port_forbidden(p, why, sizeof why)) { dprintf(cfd, "ERR refused: %s\n", why); return; } __u8 v = 1; rc = bpf_map_update_elem(g_fd_ports, &k, &v, BPF_ANY); } else rc = bpf_map_delete_elem(g_fd_ports, &k); if (rc) dprintf(cfd, "ERR %s\n", strerror(-rc)); else dprintf(cfd, "%s %u\nOK\n", verb, p); } else { dprintf(cfd, "ERR unknown command: %s\n", verb); } } /*----- rtnetlink: auto-attach when the configured NIC appears -------*/ static int nl_open(void){ int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_ROUTE); if (fd < 0) return -1; struct sockaddr_nl sa = { .nl_family = AF_NETLINK, .nl_groups = RTMGRP_LINK }; if (bind(fd, (struct sockaddr *)&sa, sizeof sa) < 0) { close(fd); return -1; } return fd; } static void nl_drain(int fd){ char buf[4096]; ssize_t len; int seen_target = 0; while ((len = recv(fd, buf, sizeof buf, 0)) > 0) { for (struct nlmsghdr *nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, (size_t)len); nh = NLMSG_NEXT(nh, len)) { if (nh->nlmsg_type != RTM_NEWLINK) continue; struct ifinfomsg *ifi = NLMSG_DATA(nh); int alen = nh->nlmsg_len - NLMSG_LENGTH(sizeof *ifi); for (struct rtattr *rta = IFLA_RTA(ifi); RTA_OK(rta, alen); rta = RTA_NEXT(rta, alen)) { if (rta->rta_type != IFLA_IFNAME) continue; if (!strncmp((char *)RTA_DATA(rta), g_if, IFNAMSIZ)) seen_target = 1; } } } if (seen_target && !xdp_state(g_if, NULL)) { char err[128]; if (xdp_attach_if(g_if, 0, err, sizeof err) == 0) { logline("%s appeared -> auto-attached", g_if); if (drop4_empty()) seed(-1); } } } /*----- daemon -------------------------------------------------------*/ static void on_sig(int s){ (void)s; g_quit = 1; } static int run_daemon(void){ signal(SIGINT, on_sig); signal(SIGTERM, on_sig); signal(SIGPIPE, SIG_IGN); mkdir(HG_PIN, 0700); /* EEXIST is fine */ libbpf_set_strict_mode(LIBBPF_STRICT_ALL); LIBBPF_OPTS(bpf_object_open_opts, oo, .pin_root_path = HG_PIN); g_skel = hellgate_bpf__open_opts(&oo); if (!g_skel || hellgate_bpf__load(g_skel)) { logline("BPF load failed (kernel BTF present? bpffs mounted?)"); return 1; } g_fd_drop = bpf_map__fd(g_skel->maps.hellgate_drop4); g_fd_ports = bpf_map__fd(g_skel->maps.hellgate_dports); g_fd_stats = bpf_map__fd(g_skel->maps.hellgate_stats); g_fd_prog = bpf_program__fd(g_skel->progs.hellgate); g_ncpu = libbpf_num_possible_cpus(); g_pcpu = calloc((size_t)g_ncpu, sizeof *g_pcpu); if (g_ncpu < 1 || !g_pcpu) { logline("cpu scratch alloc failed"); return 1; } conf_load(); logline("loaded; pins at %s; gate interface: %s", HG_PIN, g_if); char err[128]; if (if_nametoindex(g_if)) { if (xdp_attach_if(g_if, 0, err, sizeof err) == 0) { logline("attached to %s (xdpdrv)", g_if); if (drop4_empty()) { seed(-1); logline("seeded bogons"); } } else logline("%s", err); } else logline("%s not present yet - will auto-attach when it appears", g_if); unlink(HG_SOCK); int ls = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); struct sockaddr_un sa = { .sun_family = AF_UNIX }; snprintf(sa.sun_path, sizeof sa.sun_path, "%s", HG_SOCK); if (ls < 0 || bind(ls, (struct sockaddr *)&sa, sizeof sa) < 0 || listen(ls, HG_MAX_CLI) < 0) { logline("socket %s: %s", HG_SOCK, strerror(errno)); return 1; } chmod(HG_SOCK, 0600); int nl = nl_open(); for (int i = 0; i < HG_MAX_CLI; i++) g_cli[i].fd = -1; logline("listening on %s", HG_SOCK); while (!g_quit) { struct pollfd pf[2 + HG_MAX_CLI]; int np = 0; pf[np++] = (struct pollfd){ .fd = ls, .events = POLLIN }; if (nl >= 0) pf[np++] = (struct pollfd){ .fd = nl, .events = POLLIN }; int cbase = np; for (int i = 0; i < HG_MAX_CLI; i++) if (g_cli[i].fd >= 0) pf[np++] = (struct pollfd){ .fd = g_cli[i].fd, .events = POLLIN }; if (poll(pf, (nfds_t)np, -1) < 0) continue; if (pf[0].revents & POLLIN) { int fd = accept4(ls, NULL, NULL, SOCK_CLOEXEC); if (fd >= 0) { int slot = -1; for (int i = 0; i < HG_MAX_CLI; i++) if (g_cli[i].fd < 0) { slot = i; break; } if (slot < 0) { dprintf(fd, "ERR busy\n"); close(fd); } else { g_cli[slot].fd = fd; g_cli[slot].len = 0; } } } if (nl >= 0 && (pf[1].revents & POLLIN)) nl_drain(nl); int pi = cbase; for (int i = 0; i < HG_MAX_CLI; i++) { if (g_cli[i].fd < 0) continue; struct pollfd *p = &pf[pi++]; if (!(p->revents & (POLLIN | POLLHUP | POLLERR))) continue; struct client *cl = &g_cli[i]; ssize_t r = read(cl->fd, cl->buf + cl->len, sizeof cl->buf - 1 - (size_t)cl->len); if (r <= 0) { close(cl->fd); cl->fd = -1; continue; } cl->len += (int)r; cl->buf[cl->len] = 0; char *nlpos; while ((nlpos = memchr(cl->buf, '\n', (size_t)cl->len))) { *nlpos = 0; handle_cmd(cl, cl->buf); int rest = (int)(cl->len - (nlpos + 1 - cl->buf)); memmove(cl->buf, nlpos + 1, (size_t)rest); cl->len = rest; cl->buf[cl->len] = 0; if (cl->fd < 0) break; } if (cl->len >= (int)sizeof cl->buf - 1) /* line too long */ { close(cl->fd); cl->fd = -1; } } } logline("shutting down (gate stays attached; `hellgate detach` to drop)"); unlink(HG_SOCK); hellgate_bpf__destroy(g_skel); return 0; } /*----- client -------------------------------------------------------*/ static int run_client(int argc, char **argv){ int fd = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un sa = { .sun_family = AF_UNIX }; snprintf(sa.sun_path, sizeof sa.sun_path, "%s", HG_SOCK); if (fd < 0 || connect(fd, (struct sockaddr *)&sa, sizeof sa) < 0) { fprintf(stderr, "hellgate: cannot reach daemon at %s (%s)\n" " service hellgate start?\n", HG_SOCK, strerror(errno)); return 2; } char line[HG_LINE] = ""; for (int i = 1; i < argc; i++) { if (i > 1) strncat(line, " ", sizeof line - strlen(line) - 1); strncat(line, argv[i], sizeof line - strlen(line) - 1); } strncat(line, "\n", sizeof line - strlen(line) - 1); if (write(fd, line, strlen(line)) < 0) { perror("write"); return 2; } FILE *f = fdopen(fd, "r"); char rl[512]; int rc = 1; while (f && fgets(rl, sizeof rl, f)) { fputs(rl, stdout); if (!strncmp(rl, "OK", 2)) { rc = 0; break; } if (!strncmp(rl, "ERR", 3)) { rc = 1; break; } } if (f) fclose(f); else close(fd); return rc; } static void usage(void){ puts("hellgate - XDP early-drop gate (daemon + client)\n" " hellgate daemon run daemon (foreground)\n" " hellgate status|stats|ifaces|ping\n" " hellgate attach|attach-generic|detach|seed\n" " hellgate move \n" " hellgate block | unblock \n" " hellgate drop-port | undrop-port "); } int main(int argc, char **argv){ if (argc < 2) { usage(); return 2; } if (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { usage(); return 0; } if (!strcmp(argv[1], "daemon")) { if (geteuid() != 0) { fprintf(stderr, "daemon needs root\n"); return 1; } return run_daemon(); } return run_client(argc, argv); }