Skip to content

src/daemon/daemon.c

/*
 * QR-NSP Volcanic Edition — Userspace Daemon
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * Consumes QUIC packets from the XDP ring buffer.
 * This is the Module 1 skeleton — packet inspection and logging.
 * Module 3 (PADDING injection) hooks into the process_event() path.
 *
 * Build: gcc -O2 -march=native -o qrnsp-daemon daemon.c -lbpf -lelf -lz
 * Run:   sudo ./qrnsp-daemon -i <interface> [-p <port>] [-t <target_ip>]
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <time.h>
#include <getopt.h>
#include <arpa/inet.h>
#include <sys/resource.h>
#include <net/if.h>

#include <bpf/libbpf.h>
#include <bpf/bpf.h>

/* ─────────────────────────────────────────────
 * Mirror XDP structures (keep in sync with qrnsp_xdp.c)
 * ───────────────────────────────────────────── */

#define SLOT_PAYLOAD_MAX  2048

#define SLOT_F_VALID      0x01
#define SLOT_F_LONG_HDR   0x02
#define SLOT_F_PADDING    0x04

#define CFG_ENABLED       0
#define CFG_TARGET_IP     1
#define CFG_TARGET_PORT   2
#define CFG_MODE          3

#define STAT_TOTAL_PKT    0
#define STAT_QUIC_PKT     1
#define STAT_QUEUED       2
#define STAT_DROPPED      3

struct qrnsp_event {
    uint64_t  timestamp_ns;
    uint32_t  pkt_len;
    uint32_t  payload_len;
    uint32_t  src_ip;
    uint32_t  dst_ip;
    uint16_t  src_port;
    uint16_t  dst_port;
    uint8_t   quic_flags;
    uint8_t   direction;
    uint8_t   flags;
    uint8_t   _pad0;
    uint32_t  quic_version;
    uint32_t  ifindex;
    uint8_t   payload[SLOT_PAYLOAD_MAX];
};

/* ─────────────────────────────────────────────
 * Globals
 * ───────────────────────────────────────────── */

static volatile sig_atomic_t g_running = 1;
static uint64_t g_events_processed = 0;
static uint64_t g_quic_long_headers = 0;
static uint64_t g_quic_short_headers = 0;

/* ─────────────────────────────────────────────
 * Signal handler
 * ───────────────────────────────────────────── */

static void
sig_handler(int sig)
{
    (void)sig;
    g_running = 0;
}

/* ─────────────────────────────────────────────
 * QUIC frame type decoder (RFC 9000 §19)
 * Used to identify PADDING frames for Module 3
 * ───────────────────────────────────────────── */

static const char *
quic_frame_type_str(uint8_t ftype)
{
    switch (ftype) {
    case 0x00: return "PADDING";
    case 0x01: return "PING";
    case 0x02: return "ACK";
    case 0x03: return "ACK+ECN";
    case 0x04: return "RESET_STREAM";
    case 0x05: return "STOP_SENDING";
    case 0x06: return "CRYPTO";
    case 0x07: return "NEW_TOKEN";
    case 0x08: case 0x09: case 0x0a: case 0x0b:
    case 0x0c: case 0x0d: case 0x0e: case 0x0f:
        return "STREAM";
    case 0x10: return "MAX_DATA";
    case 0x11: return "MAX_STREAM_DATA";
    case 0x12: case 0x13: return "MAX_STREAMS";
    case 0x1c: return "CONNECTION_CLOSE";
    case 0x1d: return "CONNECTION_CLOSE_APP";
    default:   return "UNKNOWN";
    }
}

/* ─────────────────────────────────────────────
 * Scan QUIC payload for PADDING frame regions
 *
 * In a 1-RTT (short header) packet after decryption,
 * PADDING frames are sequences of 0x00 bytes.
 * Pre-decryption, we can only estimate based on entropy.
 *
 * Returns: offset of first padding region, or -1 if none found.
 *          Sets *pad_len to length of the padding region.
 *
 * NOTE: This is a heuristic for unencrypted/initial packets.
 * For encrypted short-header packets, Module 2 (crypto) must
 * decrypt first, then this scanner runs on plaintext.
 * ───────────────────────────────────────────── */

static int
scan_padding_region(const uint8_t *payload, uint32_t len, uint32_t *pad_len)
{
    /* Skip QUIC header (variable length — use minimum for initial scan) */
    uint32_t hdr_min = (payload[0] & 0x80) ? 7 : 1;  /* long vs short */
    if (len <= hdr_min + 4)  /* Need at least header + some frames */
        return -1;

    /*
     * Walk frame types looking for PADDING (0x00) sequences.
     * In Initial packets, after the CRYPTO frame, remaining space
     * is typically padded to >= 1200 bytes (RFC 9000 §14.1).
     */
    uint32_t zero_start = 0;
    uint32_t zero_run   = 0;
    int found = 0;

    for (uint32_t i = hdr_min; i < len; i++) {
        if (payload[i] == 0x00) {
            if (!found) {
                zero_start = i;
                found = 1;
            }
            zero_run++;
        } else if (found) {
            /* End of zero run — PADDING frames are contiguous 0x00 */
            if (zero_run >= 32) {  /* Minimum useful padding region */
                *pad_len = zero_run;
                return (int)zero_start;
            }
            found = 0;
            zero_run = 0;
        }
    }

    /* Check trailing zeros */
    if (found && zero_run >= 32) {
        *pad_len = zero_run;
        return (int)zero_start;
    }

    return -1;
}

/* ─────────────────────────────────────────────
 * Process a single QUIC event from the ring buffer
 *
 * Module 1: Log + classify + scan for PADDING
 * Module 3 will add: inject payload into PADDING region
 * ───────────────────────────────────────────── */

static void
process_event(const struct qrnsp_event *evt)
{
    g_events_processed++;

    char src_str[INET_ADDRSTRLEN], dst_str[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &evt->src_ip, src_str, sizeof(src_str));
    inet_ntop(AF_INET, &evt->dst_ip, dst_str, sizeof(dst_str));

    int is_long = (evt->flags & SLOT_F_LONG_HDR);
    if (is_long) g_quic_long_headers++;
    else          g_quic_short_headers++;

    /* Terse output — one line per packet */
    printf("[%lu.%06lu] QUIC %s %s:%u → %s:%u  len=%u  hdr=%s",
           evt->timestamp_ns / 1000000000ULL,
           (evt->timestamp_ns / 1000ULL) % 1000000ULL,
           is_long ? "L" : "S",
           src_str, evt->src_port,
           dst_str, evt->dst_port,
           evt->payload_len,
           is_long ? "long" : "short");

    if (is_long && evt->quic_version != 0) {
        printf("  ver=0x%08X", evt->quic_version);
    }

    /* Scan for injectable PADDING regions */
    uint32_t pad_len = 0;
    int pad_off = scan_padding_region(evt->payload, evt->payload_len, &pad_len);
    if (pad_off >= 0) {
        printf("  PADDING@%d+%u", pad_off, pad_len);
    }

    printf("\n");
}

/* ─────────────────────────────────────────────
 * Ring buffer callback (libbpf)
 * ───────────────────────────────────────────── */

static int
ringbuf_event_handler(void *ctx, void *data, size_t data_sz)
{
    (void)ctx;
    if (data_sz < sizeof(struct qrnsp_event))
        return 0;

    const struct qrnsp_event *evt = data;
    if (!(evt->flags & SLOT_F_VALID))
        return 0;

    process_event(evt);
    return 0;
}

/* ─────────────────────────────────────────────
 * Print periodic stats
 * ───────────────────────────────────────────── */

static void
print_stats(int config_fd)
{
    /* Read per-CPU stats and sum */
    int ncpus = libbpf_num_possible_cpus();
    if (ncpus <= 0) ncpus = 1;

    uint64_t *values = calloc(ncpus, sizeof(uint64_t));
    if (!values) return;

    const char *names[] = {"total_pkt", "quic_pkt", "queued", "dropped"};
    uint64_t sums[4] = {0};

    /* Note: reading PERCPU_ARRAY requires ncpus-sized buffer */
    /* Simplified: just print daemon-side counters */
    free(values);

    fprintf(stderr, "\r[qrnsp] processed=%lu  long=%lu  short=%lu",
            g_events_processed, g_quic_long_headers, g_quic_short_headers);
    fflush(stderr);
}

/* ─────────────────────────────────────────────
 * Bump RLIMIT_MEMLOCK for BPF maps
 * ───────────────────────────────────────────── */

static int
bump_memlock(void)
{
    struct rlimit rl = { RLIM_INFINITY, RLIM_INFINITY };
    if (setrlimit(RLIMIT_MEMLOCK, &rl)) {
        fprintf(stderr, "setrlimit(MEMLOCK): %s\n", strerror(errno));
        return -1;
    }
    return 0;
}

/* ─────────────────────────────────────────────
 * Usage
 * ───────────────────────────────────────────── */

static void
usage(const char *prog)
{
    fprintf(stderr,
        "QR-NSP Daemon v0.1.0 — Censorship-Resistant QUIC Interceptor\n"
        "\n"
        "Usage: %s [options]\n"
        "  -i <iface>    Network interface (required)\n"
        "  -o <bpf.o>    Path to compiled XDP object (default: qrnsp_xdp.o)\n"
        "  -p <port>     Target port filter (default: 443)\n"
        "  -t <ip>       Target IP filter (optional)\n"
        "  -m <mode>     0=monitor, 1=intercept (default: 0)\n"
        "  -s <seconds>  Stats interval (default: 5)\n"
        "  -v            Verbose output\n"
        "  -h            This help\n",
        prog);
}

/* ─────────────────────────────────────────────
 * Main
 * ───────────────────────────────────────────── */

int
main(int argc, char **argv)
{
    const char *iface    = NULL;
    const char *obj_path = "qrnsp_xdp.o";
    uint32_t target_port = 443;
    uint32_t target_ip   = 0;
    uint32_t mode        = 0;   /* monitor */
    int stats_interval   = 5;
    int verbose          = 0;

    int opt;
    while ((opt = getopt(argc, argv, "i:o:p:t:m:s:vh")) != -1) {
        switch (opt) {
        case 'i': iface = optarg; break;
        case 'o': obj_path = optarg; break;
        case 'p': target_port = atoi(optarg); break;
        case 't': {
            struct in_addr addr;
            if (inet_pton(AF_INET, optarg, &addr) == 1)
                target_ip = addr.s_addr;
            else {
                fprintf(stderr, "Invalid IP: %s\n", optarg);
                return 1;
            }
            break;
        }
        case 'm': mode = atoi(optarg); break;
        case 's': stats_interval = atoi(optarg); break;
        case 'v': verbose = 1; break;
        case 'h': usage(argv[0]); return 0;
        default:  usage(argv[0]); return 1;
        }
    }

    if (!iface) {
        fprintf(stderr, "Error: interface (-i) required\n");
        usage(argv[0]);
        return 1;
    }

    int ifindex = if_nametoindex(iface);
    if (!ifindex) {
        fprintf(stderr, "Interface '%s' not found\n", iface);
        return 1;
    }

    /* Signals */
    signal(SIGINT,  sig_handler);
    signal(SIGTERM, sig_handler);

    /* Bump memlock for BPF */
    if (bump_memlock())
        return 1;

    fprintf(stderr, "[qrnsp] Loading XDP object: %s\n", obj_path);

    /* ── Open BPF object ── */
    struct bpf_object *obj = bpf_object__open_file(obj_path, NULL);
    if (!obj) {
        fprintf(stderr, "Failed to open BPF object: %s\n", strerror(errno));
        return 1;
    }

    /* ── Load BPF programs and maps ── */
    if (bpf_object__load(obj)) {
        fprintf(stderr, "Failed to load BPF object: %s\n", strerror(errno));
        bpf_object__close(obj);
        return 1;
    }

    /* ── Find XDP program ── */
    struct bpf_program *prog = bpf_object__find_program_by_name(obj, "qrnsp_xdp_intercept");
    if (!prog) {
        fprintf(stderr, "XDP program 'qrnsp_xdp_intercept' not found\n");
        bpf_object__close(obj);
        return 1;
    }

    int prog_fd = bpf_program__fd(prog);

    /* ── Attach XDP to interface ── */
    fprintf(stderr, "[qrnsp] Attaching XDP to %s (ifindex=%d)\n", iface, ifindex);

    /* Use SKB mode for broad compatibility; switch to DRV/HW for performance */
    LIBBPF_OPTS(bpf_xdp_attach_opts, attach_opts);
    if (bpf_xdp_attach(ifindex, prog_fd, XDP_FLAGS_SKB_MODE, &attach_opts)) {
        fprintf(stderr, "XDP attach failed: %s\n", strerror(errno));
        bpf_object__close(obj);
        return 1;
    }

    fprintf(stderr, "[qrnsp] XDP attached (SKB mode). Interface: %s\n", iface);

    /* ── Configure via BPF map ── */
    int config_fd = bpf_object__find_map_fd_by_name(obj, "qrnsp_config");
    if (config_fd >= 0) {
        uint32_t key, val;

        key = CFG_ENABLED;     val = 1;            bpf_map_update_elem(config_fd, &key, &val, BPF_ANY);
        key = CFG_TARGET_PORT; val = target_port;   bpf_map_update_elem(config_fd, &key, &val, BPF_ANY);
        key = CFG_TARGET_IP;   val = target_ip;     bpf_map_update_elem(config_fd, &key, &val, BPF_ANY);
        key = CFG_MODE;        val = mode;           bpf_map_update_elem(config_fd, &key, &val, BPF_ANY);

        fprintf(stderr, "[qrnsp] Config: port=%u ip=%s mode=%s\n",
                target_port,
                target_ip ? inet_ntoa((struct in_addr){.s_addr = target_ip}) : "any",
                mode ? "intercept" : "monitor");
    }

    /* ── Open ring buffer ── */
    struct ring_buffer *rb = ring_buffer__new(
        bpf_object__find_map_fd_by_name(obj, "qrnsp_ringbuf"),
        ringbuf_event_handler, NULL, NULL);

    if (!rb) {
        fprintf(stderr, "Failed to create ring buffer: %s\n", strerror(errno));
        goto cleanup;
    }

    fprintf(stderr, "[qrnsp] Ring buffer attached. Monitoring QUIC traffic...\n");

    /* ── Main loop ── */
    time_t last_stats = time(NULL);

    while (g_running) {
        int err = ring_buffer__poll(rb, 100 /* ms timeout */);
        if (err < 0 && err != -EINTR) {
            fprintf(stderr, "\nring_buffer__poll error: %d\n", err);
            break;
        }

        time_t now = time(NULL);
        if (now - last_stats >= stats_interval) {
            print_stats(config_fd);
            last_stats = now;
        }
    }

    fprintf(stderr, "\n[qrnsp] Shutting down...\n");

cleanup:
    /* Detach XDP */
    bpf_xdp_detach(ifindex, XDP_FLAGS_SKB_MODE, NULL);
    fprintf(stderr, "[qrnsp] XDP detached from %s\n", iface);

    if (rb)
        ring_buffer__free(rb);
    bpf_object__close(obj);

    fprintf(stderr, "[qrnsp] Total events processed: %lu\n", g_events_processed);
    return 0;
}