Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
22.7 KB c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "threading.h"
#include "wg-obfuscator.h"
#include "obfuscation.h"
#include "masking.h"
#include "runtime.h"

#ifdef __linux__
#include <sys/epoll.h>
#ifndef EPOLLEXCLUSIVE
#define EPOLLEXCLUSIVE (1u << 28)
#endif
#endif

#define CONN_SHARDS 16
static client_entry_t *conn_table[CONN_SHARDS];
static pthread_rwlock_t table_lock[CONN_SHARDS] = { [0 ... CONN_SHARDS - 1] = PTHREAD_RWLOCK_INITIALIZER };
static volatile unsigned int client_count = 0;
static volatile unsigned long table_gen = 0;

static _Thread_local struct sockaddr_in lc_addr;
static _Thread_local client_entry_t *lc_entry = NULL;
static _Thread_local unsigned long lc_gen = 0;

static inline int sa_eq(const struct sockaddr_in *a, const struct sockaddr_in *b) {
    return a->sin_port == b->sin_port && a->sin_addr.s_addr == b->sin_addr.s_addr;
}

static inline int shard_of(const struct sockaddr_in *a) {
    uint32_t h = a->sin_addr.s_addr ^ ((uint32_t)a->sin_port * 2654435761u);
    h ^= h >> 16;
    return (int)(h & (CONN_SHARDS - 1));
}

static long read_cache_size(const char *path) {
    FILE *f = fopen(path, "r");
    if (!f) return 0;
    char buf[32];
    if (!fgets(buf, sizeof(buf), f)) {
        fclose(f);
        return 0;
    }
    fclose(f);
    long v = atol(buf);
    if (strchr(buf, 'M') || strchr(buf, 'm')) v *= 1024 * 1024;
    else if (strchr(buf, 'K') || strchr(buf, 'k')) v *= 1024;
    return v;
}

static long detect_cache_level(int want_level, int want_data) {
    char path[160], val[32];
    for (int i = 0; i < 10; i++) {
        long level = 0;
        snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu0/cache/index%d/level", i);
        if (read_long_file(path, &level) != 0) break;
        if (level != want_level) continue;
        snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu0/cache/index%d/type", i);
        FILE *f = fopen(path, "r");
        if (!f) continue;
        val[0] = 0;
        if (fgets(val, sizeof(val), f)) {}
        fclose(f);
        int is_data = (val[0] == 'D' || val[0] == 'U');
        if (want_data && !is_data) continue;
        snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu0/cache/index%d/size", i);
        long sz = read_cache_size(path);
        if (sz > 0) return sz;
    }
    return 0;
}

static client_entry_t *table_find(struct sockaddr_in *addr) {
    client_entry_t *result;
    int s = shard_of(addr);
    pthread_rwlock_rdlock(&table_lock[s]);
    HASH_FIND(hh, conn_table[s], addr, sizeof(*addr), result);
    pthread_rwlock_unlock(&table_lock[s]);
    return result;
}

static void configure_udp_socket(int sock, obfuscator_config_t *config) {
#ifdef __linux__
    int pmtu = IP_PMTUDISC_DONT;
    setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, &pmtu, sizeof(pmtu));
    if (config->fwmark) {
        if (setsockopt(sock, SOL_SOCKET, SO_MARK, &config->fwmark, sizeof(config->fwmark)) < 0) {
            log(LL_WARN, "Failed to set 'firewall mark': %s", strerror(errno));
        }
    }
#endif
    int bufsize = 4 * 1024 * 1024;
    setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
    setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
}

static void epoll_add(int epfd, int fd, void *ptr) {
#ifdef __linux__
    struct epoll_event e;
    e.events = EPOLLIN | EPOLLEXCLUSIVE;
    e.data.ptr = ptr;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &e) != 0) {
        e.events = EPOLLIN;
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &e);
    }
#else
    (void)epfd; (void)fd; (void)ptr;
#endif
}

static client_entry_t *make_client(worker_ctx_t *w, struct sockaddr_in *client_addr,
                                   int is_static, uint16_t local_port) {
    client_entry_t *entry = malloc(sizeof(client_entry_t));
    if (!entry) {
        log(LL_ERROR, "Failed to allocate memory for client entry");
        return NULL;
    }
    memset(entry, 0, sizeof(*entry));
    entry->version = OBFUSCATION_VERSION;
    memcpy(&entry->client_addr, client_addr, sizeof(entry->client_addr));
    entry->server_sock = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
    if (entry->server_sock < 0) {
        serror("Failed to create server socket for client");
        free(entry);
        return NULL;
    }
    if (is_static) {
        entry->masking_handler = w->config->masking_handler;
        struct sockaddr_in bind_addr;
        memset(&bind_addr, 0, sizeof(bind_addr));
        bind_addr.sin_family = AF_INET;
        bind_addr.sin_addr.s_addr = INADDR_ANY;
        bind_addr.sin_port = htons(local_port);
        if (bind(entry->server_sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
            serror("Failed to bind static server socket to port %d", local_port);
            close(entry->server_sock);
            free(entry);
            return NULL;
        }
        entry->is_static = 1;
    }
    configure_udp_socket(entry->server_sock, w->config);
    connect(entry->server_sock, (struct sockaddr *)w->forward_addr, sizeof(*w->forward_addr));
    epoll_add(w->epfd, entry->server_sock, entry);
    HASH_ADD(hh, conn_table[shard_of(client_addr)], client_addr, sizeof(entry->client_addr), entry);
    __atomic_fetch_add(&client_count, 1, __ATOMIC_RELAXED);
    return entry;
}

static client_entry_t *worker_get_or_create(worker_ctx_t *w, struct sockaddr_in *sender, int allow_dynamic) {
    client_entry_t *entry;
    int s = shard_of(sender);
    pthread_rwlock_wrlock(&table_lock[s]);
    HASH_FIND(hh, conn_table[s], sender, sizeof(*sender), entry);
    if (entry) {
        pthread_rwlock_unlock(&table_lock[s]);
        return entry;
    }
    if (__atomic_load_n(&client_count, __ATOMIC_RELAXED) >= (unsigned)w->config->max_clients) {
        pthread_rwlock_unlock(&table_lock[s]);
        log(LL_ERROR, "Maximum number of clients reached (%d)", w->config->max_clients);
        return NULL;
    }
    for (int i = 0; i < w->config->static_spec_count; i++) {
        static_binding_t *sp = &w->config->static_specs[i];
        if (sp->client_ip.s_addr == sender->sin_addr.s_addr && htons(sp->remote_port) == sender->sin_port) {
            entry = make_client(w, sender, 1, sp->local_port);
            pthread_rwlock_unlock(&table_lock[s]);
            if (entry) log(LL_INFO, "Activated static binding: %s:%d",
                           inet_ntoa(entry->client_addr.sin_addr), ntohs(entry->client_addr.sin_port));
            return entry;
        }
    }
    if (allow_dynamic) {
        entry = make_client(w, sender, 0, 0);
    }
    pthread_rwlock_unlock(&table_lock[s]);
    return entry;
}

static int process_packet(worker_ctx_t *w, client_entry_t **pclient,
                          struct sockaddr_in *sender, direction_t dir,
                          masking_handler_t *masking_handler, int obfuscated,
                          uint8_t *buffer, int length, long now,
                          uint8_t *frame, int *frame_len) {
    obfuscator_config_t *config = w->config;
    client_entry_t *client_entry = *pclient;
    uint8_t version = client_entry ? client_entry->version : OBFUSCATION_VERSION;

    if (obfuscated) {
        int original_length = length;
        length = decode(buffer, length, w->xor_key, w->key_length, &version, config->obfuscate_bytes);
        if (length < 4 || length > original_length) return 0;
    }

    uint32_t packet_type = WG_TYPE(buffer);
    direction_t resp_expect = (dir == DIR_CLIENT_TO_SERVER) ? DIR_SERVER_TO_CLIENT : DIR_CLIENT_TO_SERVER;

    if (packet_type == WG_TYPE_HANDSHAKE) {
        if (!client_entry && sender) {
            client_entry = worker_get_or_create(w, sender, 1);
            if (!client_entry) return 0;
            client_entry->last_activity_time = now;
            client_entry->masking_handler = masking_handler;
            *pclient = client_entry;
        }
        if (!client_entry) return 0;
        if (!obfuscated) {
            masking_send_handshake_req(config, client_entry, w->listen_sock, w->forward_addr, dir);
        }
        client_entry->handshake_direction = dir;
        client_entry->last_handshake_request_time = now;
    } else if (packet_type == WG_TYPE_HANDSHAKE_RESP) {
        if (!client_entry) return 0;
        if (now - client_entry->last_handshake_request_time > HANDSHAKE_TIMEOUT) return 0;
        if (client_entry->handshake_direction != resp_expect) return 0;
        if (dir == DIR_SERVER_TO_CLIENT && !client_entry->handshaked
            && client_entry->masking_handler && !config->masking_handler_set) {
            log(LL_INFO, "Autodetected masking handler for client %s:%d: %s",
                inet_ntoa(client_entry->client_addr.sin_addr), ntohs(client_entry->client_addr.sin_port),
                client_entry->masking_handler->name);
        }
        client_entry->handshaked = 1;
        client_entry->client_obfuscated = (dir == DIR_CLIENT_TO_SERVER) ? obfuscated : !obfuscated;
        client_entry->server_obfuscated = (dir == DIR_CLIENT_TO_SERVER) ? !obfuscated : obfuscated;
    } else if (!client_entry || !client_entry->handshaked) {
        return 0;
    }

    if (version < client_entry->version) {
        client_entry->version = version;
    }

    *frame_len = 0;
    if (!obfuscated) {
        length = encode(buffer, length, w->xor_key, w->key_length, client_entry->version,
                        config->max_dummy_length_data, config->obfuscate_bytes);
        if (length < 4) return 0;
        *frame_len = (dir == DIR_CLIENT_TO_SERVER)
            ? masking_build_frame_to_server(frame, length, config, client_entry)
            : masking_build_frame_to_client(frame, length, config, client_entry);
    }

    client_entry->last_activity_time = now;
    return length;
}

static int process_from_client(worker_ctx_t *w, uint8_t *buffer, int length,
                               struct sockaddr_in *sender, long now,
                               client_entry_t **client_out, uint8_t *frame, int *frame_len,
                               int *payload_off) {
    obfuscator_config_t *config = w->config;
    unsigned long gen = __atomic_load_n(&table_gen, __ATOMIC_ACQUIRE);
    client_entry_t *client_entry;
    if (lc_entry && lc_gen == gen && sa_eq(&lc_addr, sender)) {
        client_entry = lc_entry;
    } else {
        client_entry = table_find(sender);
        if (!client_entry) {
            client_entry = worker_get_or_create(w, sender, 0);
        }
        if (client_entry) {
            lc_addr = *sender;
            lc_entry = client_entry;
            lc_gen = gen;
        }
    }

    uint8_t obfuscated = length >= 4 && is_obfuscated(buffer);
    masking_handler_t *masking_handler = config->masking_handler;
    int off = 0;
    if (obfuscated) {
        length = masking_unwrap_from_client(buffer, length, config, client_entry, w->listen_sock,
                                            sender, w->forward_addr, &masking_handler, &off);
        if (length <= 0) return 0;
    }
    if (length < 4) return 0;

    int out = process_packet(w, &client_entry, sender, DIR_CLIENT_TO_SERVER, masking_handler,
                             obfuscated, buffer + off, length, now, frame, frame_len);
    if (out > 0) {
        *client_out = client_entry;
        *payload_off = off;
        lc_addr = *sender;
        lc_entry = client_entry;
        lc_gen = gen;
    }
    return out;
}

static int process_from_server(worker_ctx_t *w, client_entry_t *client_entry,
                               uint8_t *buffer, int length, long now,
                               uint8_t *frame, int *frame_len, int *payload_off) {
    obfuscator_config_t *config = w->config;

    uint8_t obfuscated = length >= 4 && is_obfuscated(buffer);
    int off = 0;
    if (obfuscated) {
        length = masking_unwrap_from_server(buffer, length, config, client_entry, w->listen_sock, w->forward_addr, &off);
        if (length <= 0) return 0;
    }
    if (length < 4) return 0;

    int out = process_packet(w, &client_entry, NULL, DIR_SERVER_TO_CLIENT, NULL,
                             obfuscated, buffer + off, length, now, frame, frame_len);
    if (out > 0) *payload_off = off;
    return out;
}

static void worker_cleanup(worker_ctx_t *w, long now) {
    client_entry_t *cur, *tmp;
    for (int s = 0; s < CONN_SHARDS; s++) {
        pthread_rwlock_wrlock(&table_lock[s]);
        HASH_ITER(hh, conn_table[s], cur, tmp) {
            if (((now - cur->last_activity_time >= w->config->idle_timeout)
                 || (!cur->handshaked && (now - cur->last_activity_time >= HANDSHAKE_TIMEOUT)))
                && !cur->is_static) {
                log(cur->handshaked ? LL_INFO : LL_DEBUG, "Removing idle client %s:%d",
                    inet_ntoa(cur->client_addr.sin_addr), ntohs(cur->client_addr.sin_port));
#ifdef __linux__
                epoll_ctl(w->epfd, EPOLL_CTL_DEL, cur->server_sock, NULL);
#endif
                close(cur->server_sock);
                HASH_DEL(conn_table[s], cur);
                free(cur);
                __atomic_fetch_sub(&client_count, 1, __ATOMIC_RELAXED);
                __atomic_fetch_add(&table_gen, 1, __ATOMIC_RELEASE);
                continue;
            }
            if (cur->masking_handler && cur->masking_handler->timer_interval_s > 0
                && now - cur->last_masking_timer_time >= cur->masking_handler->timer_interval_s * 1000) {
                cur->last_masking_timer_time = now;
                masking_on_timer(w->config, cur, w->listen_sock, w->forward_addr);
            }
        }
        pthread_rwlock_unlock(&table_lock[s]);
    }
}

#ifdef __linux__

static inline void rx_prep(struct mmsghdr *rxh, struct iovec *rxi, struct sockaddr_in *rxa,
                           uint8_t (*rxb)[WORKER_BUF_SIZE]) {
    for (int i = 0; i < RX_BATCH; i++) {
        rxi[i].iov_base = rxb[i];
        rxi[i].iov_len = WORKER_BUF_SIZE;
        rxh[i].msg_hdr.msg_iov = &rxi[i];
        rxh[i].msg_hdr.msg_iovlen = 1;
        if (rxa) {
            rxh[i].msg_hdr.msg_name = &rxa[i];
            rxh[i].msg_hdr.msg_namelen = sizeof(rxa[i]);
        } else {
            rxh[i].msg_hdr.msg_name = NULL;
            rxh[i].msg_hdr.msg_namelen = 0;
        }
    }
}

static inline void tx_set(struct mmsghdr *h, struct iovec iov[2], uint8_t *frame, int frame_len,
                          uint8_t *payload, int out, struct sockaddr_in *dst) {
    if (dst) {
        h->msg_hdr.msg_name = dst;
        h->msg_hdr.msg_namelen = sizeof(*dst);
    } else {
        h->msg_hdr.msg_name = NULL;
        h->msg_hdr.msg_namelen = 0;
    }
    if (frame_len > 0) {
        iov[0].iov_base = frame;   iov[0].iov_len = frame_len;
        iov[1].iov_base = payload; iov[1].iov_len = out;
        h->msg_hdr.msg_iov = iov;  h->msg_hdr.msg_iovlen = 2;
    } else {
        iov[0].iov_base = payload; iov[0].iov_len = out;
        h->msg_hdr.msg_iov = iov;  h->msg_hdr.msg_iovlen = 1;
    }
}

static void worker_run(worker_ctx_t *w) {
    pin_to_cpu(w->cpu);

    struct epoll_event events[64];
    struct mmsghdr rxh[RX_BATCH];
    struct iovec rxi[RX_BATCH];
    struct sockaddr_in rxa[RX_BATCH];
    static _Thread_local uint8_t rxb[RX_BATCH][WORKER_BUF_SIZE];
    struct mmsghdr txh[RX_BATCH];
    struct iovec txi[RX_BATCH][2];
    static _Thread_local uint8_t txf[RX_BATCH][WORKER_FRAME_MAX];

    memset(rxh, 0, sizeof(rxh));
    memset(txh, 0, sizeof(txh));

    while (w->running) {
        int n = epoll_wait(w->epfd, events, 64, 1000);
        long now = now_ms();

        for (int e = 0; e < n; e++) {
            client_entry_t *src_client = events[e].data.ptr;

            if (src_client == NULL) {
                rx_prep(rxh, rxi, rxa, rxb);
                int rxn = recvmmsg(w->listen_sock, rxh, RX_BATCH, MSG_DONTWAIT | MSG_TRUNC, NULL);
                if (rxn <= 0) continue;

                int txn = 0;
                int txfd = -1;
                for (int b = 0; b < rxn; b++) {
                    int len = rxh[b].msg_len;
                    if (len < 1 || len > WORKER_BUF_SIZE) continue;
                    client_entry_t *client = NULL;
                    int frame_len = 0, off = 0;
                    int out = process_from_client(w, rxb[b], len, &rxa[b], now, &client, txf[txn], &frame_len, &off);
                    if (out <= 0 || !client) continue;

                    if (txn > 0 && txfd != client->server_sock) {
                        sendmmsg(txfd, txh, txn, MSG_DONTWAIT);
                        txn = 0;
                    }
                    txfd = client->server_sock;
                    tx_set(&txh[txn], txi[txn], txf[txn], frame_len, rxb[b] + off, out, NULL);
                    txn++;
                }
                if (txn > 0) sendmmsg(txfd, txh, txn, MSG_DONTWAIT);
                continue;
            }

            rx_prep(rxh, rxi, NULL, rxb);
            int rxn = recvmmsg(src_client->server_sock, rxh, RX_BATCH, MSG_DONTWAIT | MSG_TRUNC, NULL);
            if (rxn <= 0) continue;

            int txn = 0;
            for (int b = 0; b < rxn; b++) {
                int len = rxh[b].msg_len;
                if (len < 1 || len > WORKER_BUF_SIZE) continue;
                int frame_len = 0, off = 0;
                int out = process_from_server(w, src_client, rxb[b], len, now, txf[txn], &frame_len, &off);
                if (out <= 0) continue;

                tx_set(&txh[txn], txi[txn], txf[txn], frame_len, rxb[b] + off, out, &src_client->client_addr);
                txn++;
            }
            if (txn > 0) sendmmsg(w->listen_sock, txh, txn, MSG_DONTWAIT);
        }

        if (w->is_maintainer && now - w->last_cleanup_time >= ITERATE_INTERVAL) {
            worker_cleanup(w, now);
            w->last_cleanup_time = now;
        }
    }
}

#else

static void worker_run(worker_ctx_t *w) {
    pin_to_cpu(w->cpu);
    uint8_t buf[WORKER_BUF_SIZE];
    uint8_t frame[WORKER_FRAME_MAX];
    while (w->running) {
        struct sockaddr_in sender;
        socklen_t slen = sizeof(sender);
        int len = recvfrom(w->listen_sock, buf, sizeof(buf), 0, (struct sockaddr *)&sender, &slen);
        long now = now_ms();
        if (len > 0) {
            client_entry_t *client = NULL;
            int frame_len = 0, off = 0;
            int out = process_from_client(w, buf, len, &sender, now, &client, frame, &frame_len, &off);
            if (out > 0 && client) {
                if (frame_len > 0) {
                    uint8_t pkt[WORKER_BUF_SIZE + WORKER_FRAME_MAX];
                    memcpy(pkt, frame, frame_len);
                    memcpy(pkt + frame_len, buf + off, out);
                    send(client->server_sock, pkt, frame_len + out, MSG_DONTWAIT);
                } else {
                    send(client->server_sock, buf + off, out, MSG_DONTWAIT);
                }
            }
        }
        if (w->is_maintainer && now - w->last_cleanup_time >= ITERATE_INTERVAL) {
            worker_cleanup(w, now);
            w->last_cleanup_time = now;
        }
    }
}

#endif

static void *worker_main(void *arg) {
    worker_ctx_t *w = (worker_ctx_t *)arg;
    log(LL_DEBUG, "Worker #%d started (cpu %d)", w->worker_index, w->cpu);
    worker_run(w);
    log(LL_DEBUG, "Worker #%d stopped", w->worker_index);
    return NULL;
}

int threading_init(threading_context_t *ctx, obfuscator_config_t *config) {
    memset(ctx, 0, sizeof(*ctx));

    int order[MAX_WORKER_THREADS];
    int navail = detect_topology(order, MAX_WORKER_THREADS);

    int limit = cgroup_cpu_limit();
    if (limit > 0 && limit < navail) navail = limit;

    int n = config->threads > 0 ? config->threads : navail;
    if (n < 1) n = 1;
    if (n > MAX_WORKER_THREADS) n = MAX_WORKER_THREADS;

    ctx->num_workers = n;
    for (int i = 0; i < n; i++) {
        ctx->workers[i].worker_index = i;
        ctx->workers[i].cpu = (i < navail) ? order[i] : -1;
        ctx->workers[i].is_maintainer = (i == 0);
    }

    long l1d = detect_cache_level(1, 1);
    long l2 = detect_cache_level(2, 0);
    if (l1d > 0) {
        int cap = (int)((l1d / 2) / (long)sizeof(xor_cache_entry_t));
        xor_set_cache_cap(cap);
    }
    log(LL_INFO, "CPU: %d worker(s), L1d=%ldKB L2=%ldKB, xor-cache=%d entries",
        n, l1d / 1024, l2 / 1024, xor_get_cache_cap());

    return 0;
}

static int open_listen_socket(threading_context_t *ctx, obfuscator_config_t *config) {
    int sock = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
    if (sock < 0) {
        serror("Can't create listen socket");
        return -1;
    }
    int one = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
#ifdef __linux__
    int pmtu = IP_PMTUDISC_DONT;
    setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, &pmtu, sizeof(pmtu));
    if (config->fwmark) {
        setsockopt(sock, SOL_SOCKET, SO_MARK, &config->fwmark, sizeof(config->fwmark));
    }
#endif
    int bufsize = 4 * 1024 * 1024;
    setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
    setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = ctx->listen_addr;
    addr.sin_port = htons(ctx->listen_port);
    if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        serror("Failed to bind listen socket to %s:%d", inet_ntoa(addr.sin_addr), ctx->listen_port);
        close(sock);
        return -1;
    }
    return sock;
}

int threading_start(threading_context_t *ctx, obfuscator_config_t *config,
                    char *xor_key, int key_length, struct sockaddr_in *forward_addr,
                    in_addr_t listen_addr, uint16_t listen_port) {
    ctx->listen_addr = listen_addr;
    ctx->listen_port = listen_port;
    ctx->running = 1;

    ctx->listen_sock = open_listen_socket(ctx, config);
    if (ctx->listen_sock < 0) return -1;

#ifdef __linux__
    ctx->epfd = epoll_create1(0);
    if (ctx->epfd < 0) {
        serror("epoll_create1");
        return -1;
    }
    epoll_add(ctx->epfd, ctx->listen_sock, NULL);
#endif

    for (int i = 0; i < ctx->num_workers; i++) {
        worker_ctx_t *w = &ctx->workers[i];
        w->config = config;
        w->xor_key = xor_key;
        w->key_length = key_length;
        w->forward_addr = forward_addr;
        w->listen_sock = ctx->listen_sock;
        w->epfd = ctx->epfd;
        w->running = 1;
        w->last_cleanup_time = 0;
        if (pthread_create(&w->thread_id, NULL, worker_main, w) != 0) {
            log(LL_ERROR, "Failed to create worker #%d: %s", i, strerror(errno));
            return -1;
        }
    }
    log(LL_INFO, "Started %d worker thread(s)", ctx->num_workers);
    return 0;
}

void threading_join(threading_context_t *ctx) {
    for (int i = 0; i < ctx->num_workers; i++) {
        if (ctx->workers[i].thread_id) {
            pthread_join(ctx->workers[i].thread_id, NULL);
        }
    }
}

void threading_shutdown(threading_context_t *ctx) {
    ctx->running = 0;
    for (int i = 0; i < ctx->num_workers; i++) {
        ctx->workers[i].running = 0;
    }
}