/*=====================================================================
 * hellgated.c - THE ROUTER FROM HELL - hellgate daemon + CLI (v3)
 *
 * ONE binary, two personalities:
 *
 *   hellgate daemon              run the daemon (foreground; OpenRC
 *                                backgrounds it)
 *   hellgate <command> [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 <newif>`.
 *   - 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 <reason>\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 <iface>
 *   block <cidr> | unblock <cidr>
 *   drop-port <port> | undrop-port <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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <ctype.h>
#include <dirent.h>
#include <poll.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <linux/if_link.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

#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 <existing-iface>\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 <cidr>\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 <iface>\n"
         "  hellgate block <cidr> | unblock <cidr>\n"
         "  hellgate drop-port <n> | undrop-port <n>");
}

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);
}
