From bbf26e417cbf6bdb70b2fe1643108dc3c7369112 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:13:46 +0530 Subject: [PATCH 01/25] feat(linux): replace iptables/NFQUEUE with an eBPF cgroup datapath Selected processes' connect()/sendmsg() are redirected by cgroup sock_addr hooks to a local relay instead of queueing every packet to userspace. The data path is untouched, there is no per-packet work, and the kernel removes the programs by itself when the process exits or crashes. - eBPF: connect4/6, sendmsg4/6, recvmsg4/6, getpeername4/6 and sockops. Processes are identified by executable (dev, inode) and mapped to a class of compiled rules; anything the kernel cannot decide goes to the relay once and the result is cached in the kernel. Sockets of other network namespaces are ignored. - Rule engine: validated globs, IPv4/IPv6, CIDR, ranges, octet patterns, port lists/ranges, protocol, first-match-wins, and domain names. - Multiple proxies, each rule can pick its own (SOCKS5, SOCKS5 with auth, HTTP CONNECT). A PROXY rule whose proxy is missing blocks. - Domain rules via a DNS tap: DNS answers (UDP and TCP) seen by the relay build a bounded IP -> name map, armed in the kernel before the answer is delivered; also loads /etc/hosts. - epoll relay with splice(), bounded memory (idle connections cost ~100 bytes), SOCKS5 UDP ASSOCIATE, direct forwarding, idle timeouts, connection cap and hostname proxy resolution with a deadline and cache. - Public API gains proxy management, per-rule proxy ids, rule validation, reordering, and keeps rules across Stop/Start. --- Linux/src/Makefile | 34 +- Linux/src/ProxyBridge.c | 3478 ++++------------------------- Linux/src/ProxyBridge.h | 51 +- Linux/src/bpf/pb_bpf_shared.h | 75 + Linux/src/bpf/proxybridge.bpf.c | 433 ++++ Linux/src/pb_bpf.c | 456 ++++ Linux/src/pb_bpf.h | 46 + Linux/src/pb_dns.c | 263 +++ Linux/src/pb_dns.h | 33 + Linux/src/pb_internal.h | 121 + Linux/src/pb_relay.c | 1781 +++++++++++++++ Linux/src/pb_rules.c | 446 ++++ Linux/src/pb_rules.h | 60 + Linux/src/tests/dns_test.c | 92 + Linux/src/tests/relay_test.c | 1473 ++++++++++++ Linux/src/tests/rules_test.c | 100 + Linux/src/tests/run_relay_test.sh | 15 + 17 files changed, 5890 insertions(+), 3067 deletions(-) create mode 100644 Linux/src/bpf/pb_bpf_shared.h create mode 100644 Linux/src/bpf/proxybridge.bpf.c create mode 100644 Linux/src/pb_bpf.c create mode 100644 Linux/src/pb_bpf.h create mode 100644 Linux/src/pb_dns.c create mode 100644 Linux/src/pb_dns.h create mode 100644 Linux/src/pb_internal.h create mode 100644 Linux/src/pb_relay.c create mode 100644 Linux/src/pb_rules.c create mode 100644 Linux/src/pb_rules.h create mode 100644 Linux/src/tests/dns_test.c create mode 100644 Linux/src/tests/relay_test.c create mode 100644 Linux/src/tests/rules_test.c create mode 100755 Linux/src/tests/run_relay_test.sh diff --git a/Linux/src/Makefile b/Linux/src/Makefile index 29effb09..677f8b88 100644 --- a/Linux/src/Makefile +++ b/Linux/src/Makefile @@ -1,5 +1,9 @@ -CC = gcc -CFLAGS = -Wall -Wextra -O3 -fPIC -D_GNU_SOURCE \ +CC ?= gcc +CLANG ?= clang +BPFTOOL ?= bpftool +MULTIARCH := $(shell $(CC) -print-multiarch) + +CFLAGS = -Wall -Wextra -O2 -fPIC -D_GNU_SOURCE \ -fstack-protector-strong \ -D_FORTIFY_SOURCE=2 \ -fPIE \ @@ -9,27 +13,39 @@ CFLAGS = -Wall -Wextra -O3 -fPIC -D_GNU_SOURCE \ -fno-delete-null-pointer-checks \ -fwrapv LDFLAGS = -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s -LIBS = -lpthread -lnetfilter_queue -lnfnetlink +LIBS = -lpthread -lbpf TARGET = libproxybridge.so -SOURCES = ProxyBridge.c +SOURCES = ProxyBridge.c pb_rules.c pb_dns.c pb_bpf.c pb_relay.c OBJECTS = $(SOURCES:.c=.o) +BPF_OBJ = bpf/proxybridge.bpf.o +SKEL = proxybridge.skel.h all: $(TARGET) +$(BPF_OBJ): bpf/proxybridge.bpf.c bpf/pb_bpf_shared.h + $(CLANG) -O2 -g -target bpf -D__BPF__ -I/usr/include/$(MULTIARCH) -c $< -o $@ + +$(SKEL): $(BPF_OBJ) + $(BPFTOOL) gen skeleton $< > $@ + +pb_bpf.o: $(SKEL) + $(TARGET): $(OBJECTS) $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) - @echo "Stripping debug symbols..." - strip --strip-unneeded $@ -%.o: %.c +%.o: %.c pb_internal.h ProxyBridge.h $(CC) $(CFLAGS) -c $< -o $@ +test-rules: tests/rules_test.c tests/dns_test.c pb_rules.c pb_rules.h pb_dns.c pb_dns.h + $(CC) -Wall -Wextra -g -fsanitize=address,undefined -D_GNU_SOURCE tests/rules_test.c pb_rules.c -o tests/rules_test && tests/rules_test + $(CC) -Wall -Wextra -g -fsanitize=address,undefined -D_GNU_SOURCE tests/dns_test.c pb_dns.c -o tests/dns_test && tests/dns_test + clean: - rm -f $(OBJECTS) $(TARGET) + rm -f $(OBJECTS) $(TARGET) $(BPF_OBJ) $(SKEL) tests/rules_test tests/dns_test install: $(TARGET) install -m 0755 $(TARGET) /usr/local/lib/ ldconfig -.PHONY: all clean install +.PHONY: all clean install test-rules diff --git a/Linux/src/ProxyBridge.c b/Linux/src/ProxyBridge.c index 6a008c23..2cbb5a92 100644 --- a/Linux/src/ProxyBridge.c +++ b/Linux/src/ProxyBridge.c @@ -1,3166 +1,546 @@ +#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 -#include -#include -#include -#include -#include -#include #include "ProxyBridge.h" +#include +#include "pb_bpf.h" +#include "pb_dns.h" +#include "pb_internal.h" +#include "pb_rules.h" -#define LOCAL_PROXY_PORT 34010 -#define LOCAL_UDP_RELAY_PORT 34011 -#define MAX_PROCESS_NAME 256 -#define PID_CACHE_SIZE 1024 -#define PID_CACHE_TTL_MS 1000 -#define NUM_PACKET_THREADS 4 -#define CONNECTION_HASH_SIZE 256 -#define SOCKS5_BUFFER_SIZE 1024 -#define HTTP_BUFFER_SIZE 1024 -#define LOG_BUFFER_SIZE 1024 +#define MAX_PROXIES 255 -// safe way to run commands without shell injection issues -static int run_command_v(const char *cmd_path, char *const argv[]) { - pid_t pid = fork(); - if (pid == -1) { - return -1; - } else if (pid == 0) { - // child process - // send output to /dev/null so it doesnt clutter - int fd = open("/dev/null", O_WRONLY); - if (fd >= 0) { - dup2(fd, STDOUT_FILENO); - dup2(fd, STDERR_FILENO); - close(fd); - } - execvp(cmd_path, argv); - _exit(127); // command not found or no perms - } else { - // parent waits for child - int status; - waitpid(pid, &status, 0); - if (WIFEXITED(status)) { - return WEXITSTATUS(status); - } - return -1; - } -} +static struct { + pthread_rwlock_t lock; + struct pb_rule *rules; size_t nrules, caprules; + struct pb_proxy proxies[MAX_PROXIES]; size_t nproxies; + uint32_t next_rule_id, next_proxy_id, default_proxy; + bool dns_via_proxy, traffic_logging, running; + atomic_bool has_domain_rules; + LogCallback log_cb; + ConnectionCallback conn_cb; +} G = { .lock = PTHREAD_RWLOCK_INITIALIZER, .next_rule_id = 1, .next_proxy_id = 1, .dns_via_proxy = true }; -// run iptables commands easier -static int run_iptables_cmd(const char *arg1, const char *arg2, const char *arg3, const char *arg4, const char *arg5, const char *arg6, const char *arg7, const char *arg8, const char *arg9, const char *arg10, const char *arg11, const char *arg12, const char *arg13, const char *arg14) { - // build argv array skipping null args - const char *argv[17]; - int i = 0; - argv[i++] = "iptables"; - if (arg1) argv[i++] = arg1; - if (arg2) argv[i++] = arg2; - if (arg3) argv[i++] = arg3; - if (arg4) argv[i++] = arg4; - if (arg5) argv[i++] = arg5; - if (arg6) argv[i++] = arg6; - if (arg7) argv[i++] = arg7; - if (arg8) argv[i++] = arg8; - if (arg9) argv[i++] = arg9; - if (arg10) argv[i++] = arg10; - if (arg11) argv[i++] = arg11; - if (arg12) argv[i++] = arg12; - if (arg13) argv[i++] = arg13; - if (arg14) argv[i++] = arg14; - argv[i] = NULL; +static pthread_mutex_t cb_lock = PTHREAD_MUTEX_INITIALIZER; // serialises user callbacks - return run_command_v("iptables", (char **)argv); +static void set_err(char *err, size_t n, const char *fmt, ...) __attribute__((format(printf, 3, 4))); +static void set_err(char *err, size_t n, const char *fmt, ...) +{ + if (!err || !n) return; + va_list ap; + va_start(ap, fmt); + vsnprintf(err, n, fmt, ap); + va_end(ap); } -// convert string to int safely -static int safe_atoi(const char *str) { - if (!str) return 0; - char *endptr; - long val = strtol(str, &endptr, 10); - if (endptr == str) return 0; - return (int)val; +// ---- logging ----------------------------------------------------------------------------------------------------------- +void pb_core_log(const char *fmt, ...) +{ + char buf[1024]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof buf, fmt, ap); + va_end(ap); + pthread_mutex_lock(&cb_lock); + LogCallback cb = G.log_cb; + if (cb) cb(buf); + pthread_mutex_unlock(&cb_lock); } +static const char *proxy_type_name(ProxyType t) { return t == PROXY_TYPE_SOCKS5 ? "SOCKS5" : "HTTP"; } -typedef struct PROCESS_RULE { - uint32_t rule_id; - char process_name[MAX_PROCESS_NAME]; - char *target_hosts; - char *target_ports; - RuleProtocol protocol; - RuleAction action; - bool enabled; - struct PROCESS_RULE *next; -} PROCESS_RULE; - -#define SOCKS5_VERSION 0x05 -#define SOCKS5_CMD_CONNECT 0x01 -#define SOCKS5_CMD_UDP_ASSOCIATE 0x03 -#define SOCKS5_ATYP_IPV4 0x01 -#define SOCKS5_AUTH_NONE 0x00 - -typedef struct CONNECTION_INFO { - uint16_t src_port; - bool is_udp; // protocol - a TCP and a UDP flow may share a local port - uint32_t src_ip; - uint32_t orig_dest_ip; - uint16_t orig_dest_port; - bool is_tracked; - uint64_t last_activity; - struct CONNECTION_INFO *next; -} CONNECTION_INFO; - -typedef struct LOGGED_CONNECTION { - uint32_t pid; - uint32_t dest_ip; - uint16_t dest_port; - RuleAction action; - struct LOGGED_CONNECTION *next; -} LOGGED_CONNECTION; - -typedef struct PID_CACHE_ENTRY { - uint32_t src_ip; - uint16_t src_port; - uint32_t pid; - uint64_t timestamp; - bool is_udp; - struct PID_CACHE_ENTRY *next; -} PID_CACHE_ENTRY; - -static CONNECTION_INFO *connection_hash_table[CONNECTION_HASH_SIZE] = {NULL}; -static LOGGED_CONNECTION *logged_connections = NULL; -static PROCESS_RULE *rules_list = NULL; -static uint32_t g_next_rule_id = 1; -static pthread_rwlock_t conn_lock = PTHREAD_RWLOCK_INITIALIZER; // read-heavy connection hash -static pthread_rwlock_t rules_lock = PTHREAD_RWLOCK_INITIALIZER; // read-heavy rules list -static pthread_mutex_t pid_cache_lock = PTHREAD_MUTEX_INITIALIZER; // PID cache only -static pthread_mutex_t log_lock = PTHREAD_MUTEX_INITIALIZER; // logged connections only - -typedef struct { - int client_socket; - uint32_t orig_dest_ip; - uint16_t orig_dest_port; -} connection_config_t; - -typedef struct { - int from_socket; - int to_socket; -} transfer_config_t; - -static struct nfq_handle *nfq_h = NULL; -static struct nfq_q_handle *nfq_qh = NULL; -static pthread_t packet_thread[NUM_PACKET_THREADS] = {0}; -static pthread_t proxy_thread = 0; -static pthread_t udp_relay_thread = 0; -static pthread_t cleanup_thread = 0; -static PID_CACHE_ENTRY *pid_cache[PID_CACHE_SIZE] = {NULL}; -static bool g_has_active_rules = false; -static bool running = false; -static uint32_t g_current_process_id = 0; - -// udp relay stuff -static int udp_relay_socket = -1; -static int socks5_udp_control_socket = -1; -static int socks5_udp_send_socket = -1; -static struct sockaddr_in socks5_udp_relay_addr; -static bool udp_associate_connected = false; -static uint64_t last_udp_connect_attempt = 0; - -static bool g_traffic_logging_enabled = true; - -static char g_proxy_host[256] = ""; -static uint16_t g_proxy_port = 0; -static uint16_t g_local_relay_port = LOCAL_PROXY_PORT; -static ProxyType g_proxy_type = PROXY_TYPE_SOCKS5; -static char g_proxy_username[256] = ""; -static char g_proxy_password[256] = ""; -static bool g_dns_via_proxy = true; -static uint32_t g_proxy_ip_cached = 0; // Cached resolved proxy IP -static LogCallback g_log_callback = NULL; -static ConnectionCallback g_connection_callback = NULL; - -static void log_message(const char *msg, ...) +static void proc_name_of(uint32_t pid, const char *comm, char *out, size_t n) { - if (g_log_callback == NULL) return; - char buffer[LOG_BUFFER_SIZE]; - va_list args; - va_start(args, msg); - vsnprintf(buffer, sizeof(buffer), msg, args); - va_end(args); - g_log_callback(buffer); + char lnk[64], path[512]; + snprintf(lnk, sizeof lnk, "/proc/%u/exe", pid); + ssize_t r = readlink(lnk, path, sizeof path - 1); + if (r > 0) { + path[r] = 0; + const char *b = strrchr(path, '/'); + snprintf(out, n, "%s", b ? b + 1 : path); + size_t l = strlen(out); + if (l > 10 && !strcmp(out + l - 10, " (deleted)")) out[l - 10] = 0; + return; + } + snprintf(out, n, "%s", comm && *comm ? comm : "unknown"); } -static const char* extract_filename(const char* path) +static void emit_connection(uint32_t pid, const char *comm, int family, const uint8_t addr[16], uint16_t port, int action, uint32_t proxy_id) { - if (!path) return ""; - const char* last_slash = strrchr(path, '/'); - return last_slash ? (last_slash + 1) : path; + if (!G.traffic_logging) return; + char name[512], ip[INET6_ADDRSTRLEN + 280], info[400]; + proc_name_of(pid, comm, name, sizeof name); + if (family == AF_INET) { + inet_ntop(AF_INET, addr + 12, ip, sizeof ip); + uint32_t h = ((uint32_t)addr[12] << 24) | ((uint32_t)addr[13] << 16) | ((uint32_t)addr[14] << 8) | addr[15]; + char nm[1][256]; + if (pb_dns_lookup(h, nm, 1) == 1) { // show the domain the application asked for, when we know it + size_t l = strlen(ip); + snprintf(ip + l, sizeof ip - l, " (%.*s)", (int)(sizeof ip - l - 4), nm[0]); + } + } else inet_ntop(AF_INET6, addr, ip, sizeof ip); + if (action == PB_ACT_PROXY) { + struct pb_proxy p; + if (pb_core_get_proxy(proxy_id, &p) == 0) snprintf(info, sizeof info, "%s %s:%u", proxy_type_name(p.type), p.host, p.port); + else snprintf(info, sizeof info, "PROXY"); + } else snprintf(info, sizeof info, "%s", action == PB_ACT_BLOCK ? "BLOCK" : "DIRECT"); + pthread_mutex_lock(&cb_lock); + ConnectionCallback cb = G.conn_cb; + if (cb) cb(name, pid, ip, port, info); + pthread_mutex_unlock(&cb_lock); } -static inline char* skip_whitespace(char *str) +static void on_bpf_event(const struct pb_event *e) { - while (*str == ' ' || *str == '\t') - str++; - return str; + char comm[17]; + memcpy(comm, e->comm, 16); comm[16] = 0; + emit_connection(e->pid, comm, e->family, e->addr, e->port, e->action, e->proxy_id); } -static void format_ip_address(uint32_t ip, char *buffer, size_t size) +void pb_core_flow_event(const struct pb_orig *o, int action, const char *proxy_desc) { - snprintf(buffer, size, "%d.%d.%d.%d", - (ip >> 0) & 0xFF, (ip >> 8) & 0xFF, - (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); + (void)proxy_desc; + emit_connection(o->pid, NULL, o->family, o->addr, o->port, action, o->proxy_id); } -typedef bool (*token_match_func)(const char *token, const void *data); +bool pb_core_dns_via_proxy(void) { return G.dns_via_proxy; } -static bool parse_token_list(const char *list, const char *delimiters, token_match_func match_func, const void *match_data) +// ---- proxies ------------------------------------------------------------------------------------------------------------- +static struct pb_proxy *find_proxy_locked(uint32_t id) { - if (list == NULL || list[0] == '\0' || strcmp(list, "*") == 0) - return true; - - size_t len = strlen(list) + 1; - char *list_copy = malloc(len); - if (list_copy == NULL) - return false; - - memcpy(list_copy, list, len); // copy including null terminator - bool matched = false; - char *saveptr = NULL; - char *token = strtok_r(list_copy, delimiters, &saveptr); - while (token != NULL) - { - token = skip_whitespace(token); - - // remove spaces at end - size_t tlen = strlen(token); - while (tlen > 0 && (token[tlen - 1] == ' ' || token[tlen - 1] == '\t' || token[tlen-1] == '\r' || token[tlen-1] == '\n')) - { - token[tlen - 1] = '\0'; - tlen--; - } - - if (tlen > 0 && match_func(token, match_data)) - { - matched = true; - break; - } - token = strtok_r(NULL, delimiters, &saveptr); - } - free(list_copy); - return matched; + for (size_t i = 0; i < G.nproxies; i++) if (G.proxies[i].id == id) return &G.proxies[i]; + return NULL; } -static void configure_tcp_socket(int sock, int bufsize, int timeout_ms) +static struct pb_proxy *default_proxy_locked(void) { - int nodelay = 1; - setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); - setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); - setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); - struct timeval timeout = {timeout_ms / 1000, (timeout_ms % 1000) * 1000}; - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + struct pb_proxy *p = G.default_proxy ? find_proxy_locked(G.default_proxy) : NULL; + return p ? p : (G.nproxies ? &G.proxies[0] : NULL); } -static void configure_udp_socket(int sock, int bufsize, int timeout_ms) +int pb_core_get_proxy(uint32_t id, struct pb_proxy *out) { - setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); - setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); - struct timeval timeout = {timeout_ms / 1000, (timeout_ms % 1000) * 1000}; - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + int rc = -1; + pthread_rwlock_rdlock(&G.lock); + struct pb_proxy *p = id ? find_proxy_locked(id) : NULL; + if (!p) p = default_proxy_locked(); + if (p) { *out = *p; rc = 0; } + pthread_rwlock_unlock(&G.lock); + return rc; } -static ssize_t send_all(int sock, const char *buf, size_t len) +static bool valid_proxy(const char *host, uint16_t port, char *err, size_t en) { - size_t sent = 0; - while (sent < len) { - ssize_t n = send(sock, buf + sent, len - sent, MSG_NOSIGNAL); - if (n < 0) return -1; - sent += n; - } - return sent; + if (!host || !*host) { set_err(err, en, "proxy host is empty"); return false; } + if (strlen(host) > 253) { set_err(err, en, "proxy host too long"); return false; } + for (const char *c = host; *c; c++) + if (isspace((unsigned char)*c) || *c == '/' || *c == '@') { set_err(err, en, "proxy host contains invalid characters"); return false; } + if (!port) { set_err(err, en, "proxy port must be 1-65535"); return false; } + return true; } -static uint64_t get_monotonic_ms(void) +static void fill_proxy(struct pb_proxy *p, const char *name, ProxyType type, const char *host, uint16_t port, const char *user, const char *pass) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + snprintf(p->name, sizeof p->name, "%s", name && *name ? name : host); + p->type = type == PROXY_TYPE_HTTP ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; + snprintf(p->host, sizeof p->host, "%s", host); + p->port = port; + snprintf(p->user, sizeof p->user, "%s", user ? user : ""); + snprintf(p->pass, sizeof p->pass, "%s", pass ? pass : ""); } -static uint32_t parse_ipv4(const char *ip); -static uint32_t resolve_hostname(const char *hostname); -static int socks5_connect(int s, uint32_t dest_ip, uint16_t dest_port); -static bool match_ip_pattern(const char *pattern, uint32_t ip); -static bool match_port_pattern(const char *pattern, uint16_t port); -static bool match_ip_list(const char *ip_list, uint32_t ip); -static bool match_port_list(const char *port_list, uint16_t port); -static bool match_process_pattern(const char *pattern, const char *process_name); -static bool match_process_list(const char *process_list, const char *process_name); -static int http_connect(int s, uint32_t dest_ip, uint16_t dest_port); -static void* local_proxy_server(void *arg); -static void* connection_handler(void *arg); -static void* transfer_handler(void *arg); -static void* packet_processor(void *arg); -static uint32_t get_process_id_from_connection(uint32_t src_ip, uint16_t src_port, bool is_udp); -static bool get_process_name_from_pid(uint32_t pid, char *name, size_t name_size); -static RuleAction match_rule(const char *process_name, uint32_t dest_ip, uint16_t dest_port, bool is_udp); -static RuleAction check_process_rule(uint32_t src_ip, uint16_t src_port, uint32_t dest_ip, uint16_t dest_port, bool is_udp, uint32_t *out_pid); -static void add_connection(uint16_t src_port, bool is_udp, uint32_t src_ip, uint32_t dest_ip, uint16_t dest_port); -static bool get_connection(uint16_t src_port, bool is_udp, uint32_t *dest_ip, uint16_t *dest_port); -static bool is_connection_tracked(uint16_t src_port, bool is_udp); -static void cleanup_stale_connections(void); -static bool is_connection_already_logged(uint32_t pid, uint32_t dest_ip, uint16_t dest_port, RuleAction action); -static void add_logged_connection(uint32_t pid, uint32_t dest_ip, uint16_t dest_port, RuleAction action); -static void clear_logged_connections(void); -static bool is_broadcast_or_multicast(uint32_t ip); -static uint32_t get_cached_pid(uint32_t src_ip, uint16_t src_port, bool is_udp); -static void cache_pid(uint32_t src_ip, uint16_t src_port, uint32_t pid, bool is_udp); -static void clear_pid_cache(void); -static void update_has_active_rules(void); - -// find which process owns a socket by checking /proc -// uses uid hint to skip processes we dont need to check -static uint32_t find_pid_from_inode(unsigned long target_inode, uint32_t uid_hint) +// ---- DNS tap: arm kernel rules for addresses whose names match domain rules --------------------------------------------------- +// Caller holds G.lock (read or write). +static void arm_ip_locked(uint32_t ip, const char *const *names, int nnames) { - // build the socket string we're looking for - char expected[64]; - int expected_len = snprintf(expected, sizeof(expected), "socket:[%lu]", target_inode); - - DIR *proc_dir = opendir("/proc"); - if (!proc_dir) - return 0; - - uint32_t pid = 0; - struct dirent *proc_entry; - - while ((proc_entry = readdir(proc_dir)) != NULL) { - // skip stuff that aint a pid folder - if (proc_entry->d_type != DT_DIR || !isdigit(proc_entry->d_name[0])) - continue; - - // if we know the user id, skip other users processes - // makes this way faster cuz less folders to check - if (uid_hint != (uint32_t)-1) { - struct stat proc_stat; - char proc_path[280]; - snprintf(proc_path, sizeof(proc_path), "/proc/%s", proc_entry->d_name); - if (stat(proc_path, &proc_stat) == 0 && proc_stat.st_uid != uid_hint) - continue; - } - - char fd_path[280]; - snprintf(fd_path, sizeof(fd_path), "/proc/%s/fd", proc_entry->d_name); - DIR *fd_dir = opendir(fd_path); - if (!fd_dir) - continue; - - struct dirent *fd_entry; - while ((fd_entry = readdir(fd_dir)) != NULL) { - if (fd_entry->d_name[0] == '.') - continue; - - char link_path[560]; - snprintf(link_path, sizeof(link_path), "/proc/%s/fd/%s", - proc_entry->d_name, fd_entry->d_name); - - char link_target[64]; - ssize_t link_len = readlink(link_path, link_target, sizeof(link_target) - 1); - if (link_len == expected_len) { - link_target[link_len] = '\0'; - if (memcmp(link_target, expected, expected_len) == 0) { - pid = (uint32_t)safe_atoi(proc_entry->d_name); - closedir(fd_dir); - closedir(proc_dir); - return pid; - } - } + for (size_t i = 0; i < G.nrules; i++) { + const struct pb_rule *r = &G.rules[i]; + if (!r->enabled || !r->ndomains) continue; + for (int d = 0; d < r->ndomains; d++) { + bool hit = false; + for (int k = 0; k < nnames && !hit; k++) hit = pb_domain_match(r->domains[d], names[k]); + if (hit) { pb_bpf_dom_set((uint32_t)i, ip); break; } } - closedir(fd_dir); } - closedir(proc_dir); - return pid; } -// fast pid lookup using netlink -// tcp uses exact query, udp needs dump -static uint32_t get_process_id_from_connection(uint32_t src_ip, uint16_t src_port, bool is_udp) +static void arm_cb(uint32_t ip, const char *const *names, int n, void *ctx) { - uint32_t cached_pid = get_cached_pid(src_ip, src_port, is_udp); - if (cached_pid != 0) - return cached_pid; - - int fd = socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_SOCK_DIAG); - if (fd < 0) - return 0; - - // short timeout so we dont block packet flow - struct timeval tv = {0, 100000}; // 100ms - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - - struct { - struct nlmsghdr nlh; - struct inet_diag_req_v2 r; - } req; - - memset(&req, 0, sizeof(req)); - req.nlh.nlmsg_len = sizeof(req); - req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; - req.r.sdiag_family = AF_INET; - req.r.sdiag_protocol = is_udp ? IPPROTO_UDP : IPPROTO_TCP; - - // udp needs dump cuz no connection state to match on - // tcp can filter to only syn_sent and established states - req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; - if (!is_udp) - req.r.idiag_states = (1 << 2) | (1 << 3); // SYN_SENT(2) + ESTABLISHED(3) only - else - req.r.idiag_states = (uint32_t)-1; // All states for UDP - req.r.idiag_ext = 0; - - struct sockaddr_nl sa; - memset(&sa, 0, sizeof(sa)); - sa.nl_family = AF_NETLINK; - - if (sendto(fd, &req, sizeof(req), 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) { - close(fd); - return 0; - } - - uint32_t pid = 0; - unsigned long target_inode = 0; - uint32_t target_uid = (uint32_t)-1; - bool found = false; - char buf[16384]; - struct iovec iov = {buf, sizeof(buf)}; - struct msghdr msg = {&sa, sizeof(sa), &iov, 1, NULL, 0, 0}; - - while (1) { - ssize_t len = recvmsg(fd, &msg, 0); - if (len <= 0) - break; - - struct nlmsghdr *h = (struct nlmsghdr *)buf; - while (NLMSG_OK(h, (size_t)len)) { - if (h->nlmsg_type == NLMSG_DONE || h->nlmsg_type == NLMSG_ERROR) - goto nl_done; - - if (h->nlmsg_type == SOCK_DIAG_BY_FAMILY) { - struct inet_diag_msg *r = NLMSG_DATA(h); - - // check if this is our socket - if (r->id.idiag_src[0] == src_ip && ntohs(r->id.idiag_sport) == src_port) { - target_inode = r->idiag_inode; - target_uid = r->idiag_uid; // UID to narrow /proc scan - found = true; - goto nl_done; - } - } - h = NLMSG_NEXT(h, len); - } - } - -nl_done: - close(fd); - - // if netlink found it, now find pid from inode - // no need to scan /proc/net/tcp since we got inode already - if (found && target_inode != 0) { - pid = find_pid_from_inode(target_inode, target_uid); - } - - // fallback for udp if netlink didnt find it - // happens when app uses sendto without connecting socket first - if (!found && is_udp) { - const char* udp_files[] = {"/proc/net/udp", "/proc/net/udp6"}; - for (int file_idx = 0; file_idx < 2 && pid == 0; file_idx++) { - FILE *fp = fopen(udp_files[file_idx], "r"); - if (!fp) - continue; - - char line[512]; - if (!fgets(line, sizeof(line), fp)) { // skip header - fclose(fp); - continue; - } - while (fgets(line, sizeof(line), fp)) { - unsigned int local_addr, local_port; - unsigned long inode; - int uid_val; - - if (sscanf(line, "%*d: %X:%X %*X:%*X %*X %*X:%*X %*X:%*X %*X %d %*d %lu", - &local_addr, &local_port, &uid_val, &inode) == 4) { - if (local_port == src_port && inode != 0) { - pid = find_pid_from_inode(inode, (uint32_t)uid_val); - break; - } - } - } - fclose(fp); - } - } - - if (pid != 0) - cache_pid(src_ip, src_port, pid, is_udp); - return pid; + (void)ctx; + arm_ip_locked(ip, names, n); } -static bool get_process_name_from_pid(uint32_t pid, char *name, size_t name_size) +static void rearm_all_locked(void) { - if (pid == 0) - return false; - - // pid 1 is always init/systemd - // kinda like windows pid 4 for system stuff - if (pid == 1) - { - snprintf(name, name_size, "systemd"); - return true; - } - - char path[64]; - snprintf(path, sizeof(path), "/proc/%u/exe", pid); - - ssize_t len = readlink(path, name, name_size - 1); - if (len < 0) - return false; - - name[len] = '\0'; - return true; + pb_bpf_dom_clear(); + if (atomic_load(&G.has_domain_rules)) pb_dns_for_each(arm_cb, NULL); } -static bool match_ip_pattern(const char *pattern, uint32_t ip) +void pb_core_dns_observe(const uint8_t *msg, size_t len) { - if (pattern == NULL || strcmp(pattern, "*") == 0) - return true; - - unsigned char ip_octets[4]; - ip_octets[0] = (ip >> 0) & 0xFF; - ip_octets[1] = (ip >> 8) & 0xFF; - ip_octets[2] = (ip >> 16) & 0xFF; - ip_octets[3] = (ip >> 24) & 0xFF; - - char pattern_copy[256]; - strncpy(pattern_copy, pattern, sizeof(pattern_copy) - 1); - pattern_copy[sizeof(pattern_copy) - 1] = '\0'; - - char pattern_octets[4][16]; - int octet_count = 0; - int char_idx = 0; - - for (size_t i = 0; i <= strlen(pattern_copy) && octet_count < 4; i++) - { - if (pattern_copy[i] == '.' || pattern_copy[i] == '\0') - { - pattern_octets[octet_count][char_idx] = '\0'; - octet_count++; - char_idx = 0; - if (pattern_copy[i] == '\0') - break; - } - else - { - if (char_idx < 15) - pattern_octets[octet_count][char_idx++] = pattern_copy[i]; - } - } - - if (octet_count != 4) - return false; - - for (int i = 0; i < 4; i++) - { - if (strcmp(pattern_octets[i], "*") == 0) - continue; - - char *dash = strchr(pattern_octets[i], '-'); - if (dash != NULL) - { - int start = safe_atoi(pattern_octets[i]); - int end = safe_atoi(dash + 1); - if (ip_octets[i] < start || ip_octets[i] > end) - return false; - } - else - { - int pattern_val = safe_atoi(pattern_octets[i]); - if (pattern_val != ip_octets[i]) - return false; - } + if (!atomic_load(&G.has_domain_rules)) return; + struct pb_dns_result res; + if (pb_dns_observe(msg, len, &res) <= 0) return; + pthread_rwlock_rdlock(&G.lock); + for (int i = 0; i < res.nips; i++) { + char names[PB_DNS_MAX_NAMES][256]; + const char *ptrs[PB_DNS_MAX_NAMES]; + int n = pb_dns_lookup(res.ips[i], names, PB_DNS_MAX_NAMES); + for (int k = 0; k < n; k++) ptrs[k] = names[k]; + arm_ip_locked(res.ips[i], ptrs, n); } - return true; + pthread_rwlock_unlock(&G.lock); } -static bool match_port_pattern(const char *pattern, uint16_t port) +// ---- rule -> BPF sync ------------------------------------------------------------------------------------------------------ +static struct pb_rule_view *build_views_locked(size_t *n) { - if (pattern == NULL || strcmp(pattern, "*") == 0) - return true; - - char *dash = strchr(pattern, '-'); - if (dash != NULL) - { - int start_port = safe_atoi(pattern); - int end_port = safe_atoi(dash + 1); - return (port >= start_port && port <= end_port); + struct pb_rule_view *v = calloc(G.nrules ? G.nrules : 1, sizeof *v); + if (!v) return NULL; + for (size_t i = 0; i < G.nrules; i++) { + const struct pb_rule *r = &G.rules[i]; + v[i].rule = r; + v[i].action = r->action; + if (r->action == RULE_ACTION_PROXY) { + struct pb_proxy *p = r->proxy_id ? find_proxy_locked(r->proxy_id) : NULL; + if (!p) p = default_proxy_locked(); + if (!p) v[i].action = RULE_ACTION_BLOCK; // no usable proxy: fail closed + else { v[i].proxy_id = (uint16_t)p->id; v[i].udp_direct = p->type == PROXY_TYPE_HTTP; } + } } - - return (port == safe_atoi(pattern)); + *n = G.nrules; + return v; } -static bool ip_match_wrapper(const char *token, const void *data) +static void sync_locked(void) { - return match_ip_pattern(token, *(const uint32_t*)data); + if (!G.running) return; + size_t n; + struct pb_rule_view *v = build_views_locked(&n); + bool dom = false; + for (size_t i = 0; i < G.nrules; i++) dom |= G.rules[i].enabled && G.rules[i].ndomains > 0; + atomic_store(&G.has_domain_rules, dom); + if (v && pb_bpf_sync(v, n) != 0) pb_core_log("failed to apply rules to the kernel datapath"); + free(v); + rearm_all_locked(); + pb_bpf_set_flags(G.traffic_logging, G.dns_via_proxy); } -static bool match_ip_list(const char *ip_list, uint32_t ip) +// ---- slow path ------------------------------------------------------------------------------------------------------------- +static bool exe_path_of_pid(uint32_t pid, char *out, size_t n) { - return parse_token_list(ip_list, ";", ip_match_wrapper, &ip); + char lnk[64]; + snprintf(lnk, sizeof lnk, "/proc/%u/exe", pid); + ssize_t r = readlink(lnk, out, n - 1); + if (r <= 0) { out[0] = 0; return false; } + out[r] = 0; + size_t l = strlen(out); + if (l > 10 && !strcmp(out + l - 10, " (deleted)")) out[l - 10] = 0; + return true; } -static bool port_match_wrapper(const char *token, const void *data) -{ - return match_port_pattern(token, *(const uint16_t*)data); +void pb_core_decide(const struct pb_orig *o, struct pb_decision *out) +{ + char path[512]; + exe_path_of_pid(o->pid, path, sizeof path); // may be empty if the process is already gone + out->action = PB_ACT_DIRECT; out->proxy_id = 0; + + pthread_rwlock_rdlock(&G.lock); + if (!pb_implicit_direct(o->family, o->addr, o->port, o->proto, G.dns_via_proxy)) { + char nbuf[PB_DNS_MAX_NAMES][256]; + const char *nptr[PB_DNS_MAX_NAMES]; + int nn = 0; + if (o->family == AF_INET && atomic_load(&G.has_domain_rules)) { + uint32_t h = ((uint32_t)o->addr[12] << 24) | ((uint32_t)o->addr[13] << 16) | ((uint32_t)o->addr[14] << 8) | o->addr[15]; + nn = pb_dns_lookup(h, nbuf, PB_DNS_MAX_NAMES); + for (int k = 0; k < nn; k++) nptr[k] = nbuf[k]; + } + int idx = pb_rules_eval(G.rules, G.nrules, path, o->family, o->addr, o->port, o->proto, nptr, nn); + if (idx >= 0) { + const struct pb_rule *r = &G.rules[idx]; + if (r->action == RULE_ACTION_PROXY) { + struct pb_proxy *p = r->proxy_id ? find_proxy_locked(r->proxy_id) : NULL; + if (!p) p = default_proxy_locked(); + if (!p) out->action = PB_ACT_BLOCK; + else if (o->proto == IPPROTO_UDP && p->type == PROXY_TYPE_HTTP) out->action = PB_ACT_DIRECT; + else { out->action = PB_ACT_PROXY; out->proxy_id = p->id; } + } else out->action = r->action == RULE_ACTION_BLOCK ? PB_ACT_BLOCK : PB_ACT_DIRECT; + } + } + if (o->exe_ino && path[0]) { // teach the kernel this binary so the next flow skips the relay + size_t n; + struct pb_rule_view *v = build_views_locked(&n); + if (v) { pb_bpf_learn(o, path, v, n); free(v); } + } + pthread_rwlock_unlock(&G.lock); +} + +int pb_core_take_tcp_orig(uint16_t client_port, struct pb_orig *out) { return pb_bpf_take_tcp_orig(client_port, out); } +int pb_core_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out) { return pb_bpf_get_udp_flow(flow_ip_be, out); } + +// ---- public API: proxies ----------------------------------------------------------------------------------------------- +uint32_t ProxyBridge_AddProxy(const char *name, ProxyType type, const char *host, uint16_t port, const char *user, const char *pass, char *err, size_t en) +{ + if (!valid_proxy(host, port, err, en)) return 0; + pthread_rwlock_wrlock(&G.lock); + uint32_t id = 0; + if (G.nproxies >= MAX_PROXIES) set_err(err, en, "too many proxies (max %d)", MAX_PROXIES); + else { + struct pb_proxy *p = &G.proxies[G.nproxies++]; + memset(p, 0, sizeof *p); + p->id = id = G.next_proxy_id++; + fill_proxy(p, name, type, host, port, user, pass); + sync_locked(); + } + pthread_rwlock_unlock(&G.lock); + return id; +} + +bool ProxyBridge_EditProxy(uint32_t id, const char *name, ProxyType type, const char *host, uint16_t port, const char *user, const char *pass, char *err, size_t en) +{ + if (!valid_proxy(host, port, err, en)) return false; + pthread_rwlock_wrlock(&G.lock); + struct pb_proxy *p = find_proxy_locked(id); + if (p) { fill_proxy(p, name, type, host, port, user, pass); sync_locked(); } + else set_err(err, en, "unknown proxy id %u", id); + pthread_rwlock_unlock(&G.lock); + return p != NULL; +} + +bool ProxyBridge_DeleteProxy(uint32_t id) +{ + pthread_rwlock_wrlock(&G.lock); + bool ok = false; + for (size_t i = 0; i < G.nproxies; i++) + if (G.proxies[i].id == id) { + memmove(&G.proxies[i], &G.proxies[i + 1], (G.nproxies - i - 1) * sizeof G.proxies[0]); + G.nproxies--; + if (G.default_proxy == id) G.default_proxy = 0; + ok = true; + break; + } + if (ok) sync_locked(); + pthread_rwlock_unlock(&G.lock); + return ok; } -static bool match_port_list(const char *port_list, uint16_t port) +void ProxyBridge_ClearProxies(void) { - return parse_token_list(port_list, ",;", port_match_wrapper, &port); + pthread_rwlock_wrlock(&G.lock); + G.nproxies = 0; G.default_proxy = 0; + sync_locked(); + pthread_rwlock_unlock(&G.lock); } -static bool match_process_pattern(const char *pattern, const char *process_full_path) +void ProxyBridge_SetDefaultProxy(uint32_t id) { - if (pattern == NULL || strcmp(pattern, "*") == 0) - return true; - - const char *filename = strrchr(process_full_path, '/'); - if (filename != NULL) - filename++; - else - filename = process_full_path; - - size_t pattern_len = strlen(pattern); - size_t name_len = strlen(filename); - size_t full_path_len = strlen(process_full_path); - - bool is_full_path_pattern = (strchr(pattern, '/') != NULL); - const char *match_target = is_full_path_pattern ? process_full_path : filename; - size_t target_len = is_full_path_pattern ? full_path_len : name_len; - - if (pattern_len > 0 && pattern[pattern_len - 1] == '*') - { - return strncasecmp(pattern, match_target, pattern_len - 1) == 0; - } - - if (pattern_len > 1 && pattern[0] == '*') - { - const char *pattern_suffix = pattern + 1; - size_t suffix_len = pattern_len - 1; - if (target_len >= suffix_len) - { - return strcasecmp(match_target + target_len - suffix_len, pattern_suffix) == 0; - } - return false; - } - - return strcasecmp(pattern, match_target) == 0; + pthread_rwlock_wrlock(&G.lock); + G.default_proxy = id; + sync_locked(); + pthread_rwlock_unlock(&G.lock); } -typedef struct { - const char *process_name; -} process_match_data; - -static bool process_match_wrapper(const char *token, const void *data) +bool ProxyBridge_SetProxyConfig(ProxyType type, const char *host, uint16_t port, const char *user, const char *pass) { - const process_match_data *pdata = data; - return match_process_pattern(token, pdata->process_name); + char err[256]; + pthread_rwlock_rdlock(&G.lock); + struct pb_proxy *d = default_proxy_locked(); + uint32_t id = d ? d->id : 0; + pthread_rwlock_unlock(&G.lock); + if (id) return ProxyBridge_EditProxy(id, NULL, type, host, port, user, pass, err, sizeof err); + return ProxyBridge_AddProxy("default", type, host, port, user, pass, err, sizeof err) != 0; } -static bool match_process_list(const char *process_list, const char *process_name) +// ---- public API: rules --------------------------------------------------------------------------------------------------- +bool ProxyBridge_ValidateRule(const char *process, const char *hosts, const char *ports, char *err, size_t en) { - process_match_data data = {process_name}; - return parse_token_list(process_list, ";", process_match_wrapper, &data); + struct pb_rule r; + if (!pb_rule_parse(&r, process, hosts, ports, err, en)) return false; + pb_rule_free(&r); + return true; } -static uint32_t parse_ipv4(const char *ip) +static struct pb_rule *find_rule_locked(uint32_t id, size_t *idx) { - unsigned int a, b, c, d; - if (sscanf(ip, "%u.%u.%u.%u", &a, &b, &c, &d) != 4) - return 0; - if (a > 255 || b > 255 || c > 255 || d > 255) - return 0; - return (a << 0) | (b << 8) | (c << 16) | (d << 24); + for (size_t i = 0; i < G.nrules; i++) if (G.rules[i].id == id) { if (idx) *idx = i; return &G.rules[i]; } + return NULL; } -static uint32_t resolve_hostname(const char *hostname) -{ - if (hostname == NULL || hostname[0] == '\0') - return 0; - - uint32_t ip = parse_ipv4(hostname); - if (ip != 0) - return ip; - - struct addrinfo hints, *result = NULL; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - - if (getaddrinfo(hostname, NULL, &hints, &result) != 0) - { - log_message("failed to resolve hostname: %s", hostname); - return 0; - } - - if (result == NULL || result->ai_family != AF_INET) - { - if (result != NULL) - freeaddrinfo(result); - log_message("no ipv4 address found for hostname: %s", hostname); - return 0; - } - - struct sockaddr_in *addr = (struct sockaddr_in *)result->ai_addr; - uint32_t resolved_ip = addr->sin_addr.s_addr; - freeaddrinfo(result); - - return resolved_ip; +uint32_t ProxyBridge_AddRule(const char *proc, const char *hosts, const char *ports, RuleProtocol protocol, RuleAction action, uint32_t proxy_id, char *err, size_t en) +{ + struct pb_rule r; + if (!pb_rule_parse(&r, proc, hosts, ports, err, en)) return 0; + r.enabled = true; r.protocol = protocol; r.action = action; r.proxy_id = proxy_id; + pthread_rwlock_wrlock(&G.lock); + uint32_t id = 0; + if (G.nrules >= PB_MAX_RULES) { set_err(err, en, "too many rules (max %d)", PB_MAX_RULES); pb_rule_free(&r); } + else { + if (G.nrules == G.caprules) { + size_t nc = G.caprules ? G.caprules * 2 : 32; + struct pb_rule *nr = realloc(G.rules, nc * sizeof *nr); + if (!nr) { pb_rule_free(&r); set_err(err, en, "out of memory"); pthread_rwlock_unlock(&G.lock); return 0; } + G.rules = nr; G.caprules = nc; + } + r.id = id = G.next_rule_id++; + G.rules[G.nrules++] = r; + sync_locked(); + } + pthread_rwlock_unlock(&G.lock); + return id; +} + +bool ProxyBridge_EditRule(uint32_t id, const char *proc, const char *hosts, const char *ports, RuleProtocol protocol, RuleAction action, uint32_t proxy_id, char *err, size_t en) +{ + struct pb_rule r; + if (!pb_rule_parse(&r, proc, hosts, ports, err, en)) return false; + pthread_rwlock_wrlock(&G.lock); + struct pb_rule *old = find_rule_locked(id, NULL); + if (!old) { set_err(err, en, "unknown rule id %u", id); pb_rule_free(&r); pthread_rwlock_unlock(&G.lock); return false; } + r.id = id; r.enabled = old->enabled; r.protocol = protocol; r.action = action; r.proxy_id = proxy_id; + pb_rule_free(old); + *old = r; + sync_locked(); + pthread_rwlock_unlock(&G.lock); + return true; } -static bool is_broadcast_or_multicast(uint32_t ip) +static bool set_enabled(uint32_t id, bool on) { - // localhost - uint8_t first_octet = (ip >> 0) & 0xFF; - if (first_octet == 127) - return true; - - // link-local apipa stuff - uint8_t second_octet = (ip >> 8) & 0xFF; - if (first_octet == 169 && second_octet == 254) - return true; - - // broadcast - if (ip == 0xFFFFFFFF) - return true; - - // subnet broadcast - if ((ip & 0xFF000000) == 0xFF000000) - return true; - - // multicast range - if (first_octet >= 224 && first_octet <= 239) - return true; - - return false; + pthread_rwlock_wrlock(&G.lock); + struct pb_rule *r = find_rule_locked(id, NULL); + if (r) { r->enabled = on; sync_locked(); } + pthread_rwlock_unlock(&G.lock); + return r != NULL; } +bool ProxyBridge_EnableRule(uint32_t id) { return set_enabled(id, true); } +bool ProxyBridge_DisableRule(uint32_t id) { return set_enabled(id, false); } -static RuleAction match_rule(const char *process_name, uint32_t dest_ip, uint16_t dest_port, bool is_udp) +bool ProxyBridge_DeleteRule(uint32_t id) { - PROCESS_RULE *rule = rules_list; - PROCESS_RULE *wildcard_rule = NULL; - - while (rule != NULL) - { - if (!rule->enabled) - { - rule = rule->next; - continue; - } - - if (rule->protocol != RULE_PROTOCOL_BOTH) - { - if (rule->protocol == RULE_PROTOCOL_TCP && is_udp) - { - rule = rule->next; - continue; - } - if (rule->protocol == RULE_PROTOCOL_UDP && !is_udp) - { - rule = rule->next; - continue; - } - } - - bool is_wildcard_process = (strcmp(rule->process_name, "*") == 0 || strcasecmp(rule->process_name, "ANY") == 0); - - if (is_wildcard_process) - { - bool has_ip_filter = (strcmp(rule->target_hosts, "*") != 0); - bool has_port_filter = (strcmp(rule->target_ports, "*") != 0); - - if (has_ip_filter || has_port_filter) - { - if (match_ip_list(rule->target_hosts, dest_ip) && - match_port_list(rule->target_ports, dest_port)) - { - return rule->action; - } - rule = rule->next; - continue; - } - - if (wildcard_rule == NULL) - { - wildcard_rule = rule; - } - rule = rule->next; - continue; - } - - if (match_process_list(rule->process_name, process_name)) - { - if (match_ip_list(rule->target_hosts, dest_ip) && - match_port_list(rule->target_ports, dest_port)) - { - return rule->action; - } - } - - rule = rule->next; - } - - if (wildcard_rule != NULL) - { - return wildcard_rule->action; + pthread_rwlock_wrlock(&G.lock); + size_t i; + struct pb_rule *r = find_rule_locked(id, &i); + if (r) { + pb_rule_free(r); + memmove(&G.rules[i], &G.rules[i + 1], (G.nrules - i - 1) * sizeof *r); + G.nrules--; + sync_locked(); } - - return RULE_ACTION_DIRECT; + pthread_rwlock_unlock(&G.lock); + return r != NULL; } -static RuleAction check_process_rule(uint32_t src_ip, uint16_t src_port, uint32_t dest_ip, uint16_t dest_port, bool is_udp, uint32_t *out_pid) +bool ProxyBridge_MoveRule(uint32_t id, int new_index) { - uint32_t pid; - char process_name[MAX_PROCESS_NAME]; - - pid = get_process_id_from_connection(src_ip, src_port, is_udp); - - if (out_pid != NULL) - *out_pid = pid; - - if (pid == 0) - return RULE_ACTION_DIRECT; - - if (pid == g_current_process_id) - return RULE_ACTION_DIRECT; - - if (!get_process_name_from_pid(pid, process_name, sizeof(process_name))) - return RULE_ACTION_DIRECT; - - pthread_rwlock_rdlock(&rules_lock); - RuleAction action = match_rule(process_name, dest_ip, dest_port, is_udp); - pthread_rwlock_unlock(&rules_lock); - - if (action == RULE_ACTION_PROXY && is_udp && g_proxy_type == PROXY_TYPE_HTTP) - { - return RULE_ACTION_DIRECT; + pthread_rwlock_wrlock(&G.lock); + size_t i; + struct pb_rule *r = find_rule_locked(id, &i); + if (r) { + size_t to = new_index < 0 ? 0 : (size_t)new_index >= G.nrules ? G.nrules - 1 : (size_t)new_index; + struct pb_rule tmp = *r; + if (to < i) memmove(&G.rules[to + 1], &G.rules[to], (i - to) * sizeof tmp); + else if (to > i) memmove(&G.rules[i], &G.rules[i + 1], (to - i) * sizeof tmp); + G.rules[to] = tmp; + sync_locked(); } - if (action == RULE_ACTION_PROXY && (g_proxy_host[0] == '\0' || g_proxy_port == 0)) - { - return RULE_ACTION_DIRECT; - } - - return action; + pthread_rwlock_unlock(&G.lock); + return r != NULL; } -// Read exactly n bytes, looping over short reads. SOCKS5/HTTP replies can split -// across TCP segments (common on high-latency remote proxies), so fixed-length -// handshake reads must accumulate. Returns n on success, -1 on error/peer close. -static int recv_n(int s, void *buf, size_t n) +void ProxyBridge_ClearRules(void) { - size_t got = 0; - while (got < n) - { - ssize_t r = recv(s, (char *)buf + got, n - got, 0); - if (r <= 0) return -1; - got += (size_t)r; - } - return (int)n; + pthread_rwlock_wrlock(&G.lock); + for (size_t i = 0; i < G.nrules; i++) pb_rule_free(&G.rules[i]); + G.nrules = 0; + sync_locked(); + pthread_rwlock_unlock(&G.lock); } -// Read and validate a SOCKS5 CONNECT reply (RFC 1928). The proxy chooses the -// BND.ADDR type in its reply independently of the request's ATYP - many proxies -// answer with a 4-byte IPv4 0.0.0.0 even for other requests - so parse the 4-byte -// header (VER REP RSV ATYP) then drain the variable BND.ADDR + BND.PORT by ATYP, -// instead of assuming a fixed 10-byte reply. *reply gets REP (or -1) for logging. -static int socks5_read_connect_reply(int s, int *reply) +// ---- public API: runtime ----------------------------------------------------------------------------------------------------- +void ProxyBridge_SetDnsViaProxy(bool enable) { - unsigned char hdr[4]; - if (reply) *reply = -1; - if (recv_n(s, hdr, 4) != 4) return -1; - if (reply) *reply = hdr[1]; - if (hdr[0] != SOCKS5_VERSION || hdr[1] != 0x00) return -1; - - int drain; - if (hdr[3] == SOCKS5_ATYP_IPV4) drain = 4 + 2; - else if (hdr[3] == 0x04 /* IPv6 */) drain = 16 + 2; - else if (hdr[3] == 0x03 /* domain */) - { - unsigned char dlen; - if (recv_n(s, &dlen, 1) != 1) return -1; - drain = (int)dlen + 2; - } - else return -1; // unknown ATYP - - unsigned char scratch[270]; // max drain = 255 + 2 (domain) < 270 - if (drain > 0 && recv_n(s, scratch, (size_t)drain) != drain) return -1; - return 0; + pthread_rwlock_wrlock(&G.lock); + G.dns_via_proxy = enable; + pb_bpf_set_flags(G.traffic_logging, G.dns_via_proxy); + pthread_rwlock_unlock(&G.lock); } -static int socks5_connect(int s, uint32_t dest_ip, uint16_t dest_port) -{ - unsigned char buf[SOCKS5_BUFFER_SIZE]; - bool use_auth = (g_proxy_username[0] != '\0'); - - buf[0] = SOCKS5_VERSION; - if (use_auth) - { - buf[1] = 0x02; - buf[2] = SOCKS5_AUTH_NONE; - buf[3] = 0x02; - if (send(s, buf, 4, MSG_NOSIGNAL) != 4) - { - log_message("socks5 failed to send auth methods"); - return -1; - } - } - else - { - buf[1] = 0x01; - buf[2] = SOCKS5_AUTH_NONE; - if (send(s, buf, 3, MSG_NOSIGNAL) != 3) - { - log_message("socks5 failed to send auth methods"); - return -1; - } - } - - if (recv_n(s, buf, 2) != 2 || buf[0] != SOCKS5_VERSION) - { - log_message("socks5 invalid auth response"); - return -1; - } - - if (buf[1] == 0x02 && use_auth) - { - size_t ulen = strlen(g_proxy_username); - size_t plen = strlen(g_proxy_password); - if (ulen > 255 || plen > 255) // SOCKS5 username/password fields are length-prefixed by one byte - { - log_message("socks5 credentials too long"); - return -1; - } - buf[0] = 0x01; - buf[1] = (unsigned char)ulen; - memcpy(buf + 2, g_proxy_username, ulen); - buf[2 + ulen] = (unsigned char)plen; - memcpy(buf + 3 + ulen, g_proxy_password, plen); - - if (send(s, buf, 3 + ulen + plen, MSG_NOSIGNAL) != (ssize_t)(3 + ulen + plen)) - { - log_message("socks5 failed to send credentials"); - return -1; - } - - if (recv_n(s, buf, 2) != 2 || buf[0] != 0x01 || buf[1] != 0x00) - { - log_message("socks5 authentication failed"); - return -1; - } - } - else if (buf[1] != SOCKS5_AUTH_NONE) - { - log_message("socks5 unsupported auth method"); - return -1; - } +void ProxyBridge_SetLogCallback(LogCallback cb) { pthread_mutex_lock(&cb_lock); G.log_cb = cb; pthread_mutex_unlock(&cb_lock); } +void ProxyBridge_SetConnectionCallback(ConnectionCallback cb) { pthread_mutex_lock(&cb_lock); G.conn_cb = cb; pthread_mutex_unlock(&cb_lock); } - buf[0] = SOCKS5_VERSION; - buf[1] = SOCKS5_CMD_CONNECT; - buf[2] = 0x00; - buf[3] = SOCKS5_ATYP_IPV4; - memcpy(buf + 4, &dest_ip, 4); - uint16_t port_net = htons(dest_port); - memcpy(buf + 8, &port_net, 2); +void ProxyBridge_SetTrafficLoggingEnabled(bool enable) +{ + pthread_rwlock_wrlock(&G.lock); + G.traffic_logging = enable; + pb_bpf_set_flags(G.traffic_logging, G.dns_via_proxy); + pthread_rwlock_unlock(&G.lock); +} - if (send(s, buf, 10, MSG_NOSIGNAL) != 10) - { - log_message("socks5 failed to send connect request"); - return -1; - } +void ProxyBridge_ClearConnectionLogs(void) {} - int reply = -1; - if (socks5_read_connect_reply(s, &reply) != 0) - { - log_message("socks5 connect failed status %d", reply); - return -1; +bool ProxyBridge_Start(void) +{ + pthread_rwlock_wrlock(&G.lock); + if (G.running) { pthread_rwlock_unlock(&G.lock); return true; } + for (size_t i = 0; i < G.nrules; i++) + if (G.rules[i].enabled && G.rules[i].action == RULE_ACTION_PROXY && !default_proxy_locked() && + !(G.rules[i].proxy_id && find_proxy_locked(G.rules[i].proxy_id))) { + pthread_rwlock_unlock(&G.lock); + pb_core_log("cannot start: a rule uses a proxy but no proxy is configured"); + return false; + } + G.running = true; // pb_core_* callbacks may fire as soon as the relay is up + pthread_rwlock_unlock(&G.lock); + + bool started = false; + for (uint16_t base = PB_TCP_RELAY_PORT_DEFAULT; base < PB_TCP_RELAY_PORT_DEFAULT + 20 && !started; base += 2) { + struct pb_relay_cfg rc = { .tcp_port = base, .udp_port = (uint16_t)(base + 1) }; + started = pb_relay_start(&rc) == 0; + } + if (!started) { pthread_rwlock_wrlock(&G.lock); G.running = false; pthread_rwlock_unlock(&G.lock); return false; } + + struct pb_bpf_cfg bc = { .tcp_port = pb_relay_tcp_port(), .udp_port = pb_relay_udp_port(), .self_tgid = (uint32_t)getpid(), + .log = G.traffic_logging, .dns_via_proxy = G.dns_via_proxy }; + if (pb_bpf_start(&bc, on_bpf_event) != 0) { + pb_relay_stop(); + pthread_rwlock_wrlock(&G.lock); G.running = false; pthread_rwlock_unlock(&G.lock); + return false; } - - return 0; + pb_dns_load_hosts("/etc/hosts", NULL); // static names are known without any DNS traffic + pthread_rwlock_wrlock(&G.lock); + sync_locked(); + pthread_rwlock_unlock(&G.lock); + pb_core_log("ProxyBridge started (eBPF datapath, relay tcp:%u udp:%u)", pb_relay_tcp_port(), pb_relay_udp_port()); + return true; } -static int http_connect(int s, uint32_t dest_ip, uint16_t dest_port) +bool ProxyBridge_Stop(void) { - char buf[HTTP_BUFFER_SIZE]; - char dest_ip_str[32]; - format_ip_address(dest_ip, dest_ip_str, sizeof(dest_ip_str)); - - int len = snprintf(buf, sizeof(buf), - "CONNECT %s:%d HTTP/1.1\r\n" - "Host: %s:%d\r\n" - "Proxy-Connection: Keep-Alive\r\n", - dest_ip_str, dest_port, dest_ip_str, dest_port); - - if (g_proxy_username[0] != '\0') - { - // encode user:pass in base64 - static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - char auth_raw[512]; - int auth_raw_len = snprintf(auth_raw, sizeof(auth_raw), "%s:%s", g_proxy_username, g_proxy_password); - char auth_b64[700]; - int j = 0; - for (int i = 0; i < auth_raw_len; i += 3) { - unsigned int n = ((unsigned char)auth_raw[i]) << 16; - if (i + 1 < auth_raw_len) n |= ((unsigned char)auth_raw[i + 1]) << 8; - if (i + 2 < auth_raw_len) n |= ((unsigned char)auth_raw[i + 2]); - auth_b64[j++] = b64[(n >> 18) & 0x3F]; - auth_b64[j++] = b64[(n >> 12) & 0x3F]; - auth_b64[j++] = (i + 1 < auth_raw_len) ? b64[(n >> 6) & 0x3F] : '='; - auth_b64[j++] = (i + 2 < auth_raw_len) ? b64[n & 0x3F] : '='; - } - auth_b64[j] = '\0'; - len += snprintf(buf + len, sizeof(buf) - len, - "Proxy-Authorization: Basic %s\r\n", auth_b64); - } - - len += snprintf(buf + len, sizeof(buf) - len, "\r\n"); - - if (send_all(s, buf, len) < 0) - { - log_message("http failed to send connect"); - return -1; - } - - ssize_t recv_len = recv(s, buf, sizeof(buf) - 1, 0); - if (recv_len < 12) - { - log_message("http invalid response"); - return -1; - } - - buf[recv_len] = '\0'; - if (strncmp(buf, "HTTP/1.", 7) != 0) - { - log_message("http invalid response"); - return -1; - } - - int status_code = 0; - if (sscanf(buf + 9, "%d", &status_code) != 1 || status_code != 200) - { - log_message("http connect failed status %d", status_code); - return -1; - } - - return 0; + pthread_rwlock_wrlock(&G.lock); + bool was = G.running; + G.running = false; + pthread_rwlock_unlock(&G.lock); + if (!was) return true; + pb_bpf_stop(); // detach first: no new flows get redirected + pb_relay_stop(); + pb_core_log("ProxyBridge stopped"); + return true; } -// relay functions for production use - -// connection handler like windows - blocks on connect then transfers data -static void* connection_handler(void *arg) +int ProxyBridge_TestProxy(uint32_t proxy_id, const char *host, uint16_t port, char *buf, size_t n) { - connection_config_t *config = (connection_config_t *)arg; - int client_sock = config->client_socket; - uint32_t dest_ip = config->orig_dest_ip; - uint16_t dest_port = config->orig_dest_port; - int proxy_sock; - struct sockaddr_in proxy_addr; - uint32_t proxy_ip; - - free(config); - - // use cached proxy ip we resolved earlier - proxy_ip = g_proxy_ip_cached; - if (proxy_ip == 0) - { - // try resolving again if cache failed - proxy_ip = resolve_hostname(g_proxy_host); - if (proxy_ip == 0) - { - close(client_sock); - return NULL; - } - g_proxy_ip_cached = proxy_ip; - } - - proxy_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); - if (proxy_sock < 0) - { - close(client_sock); - return NULL; - } - - configure_tcp_socket(proxy_sock, 1048576, 60000); - configure_tcp_socket(client_sock, 1048576, 60000); - - memset(&proxy_addr, 0, sizeof(proxy_addr)); - proxy_addr.sin_family = AF_INET; - proxy_addr.sin_addr.s_addr = proxy_ip; - proxy_addr.sin_port = htons(g_proxy_port); - - if (connect(proxy_sock, (struct sockaddr *)&proxy_addr, sizeof(proxy_addr)) < 0) - { - close(client_sock); - close(proxy_sock); - return NULL; - } - - // do handshake blocking - if (g_proxy_type == PROXY_TYPE_SOCKS5) - { - if (socks5_connect(proxy_sock, dest_ip, dest_port) != 0) - { - close(client_sock); - close(proxy_sock); - return NULL; - } - } - else if (g_proxy_type == PROXY_TYPE_HTTP) - { - if (http_connect(proxy_sock, dest_ip, dest_port) != 0) - { - close(client_sock); - close(proxy_sock); - return NULL; - } - } - - // Data transfer phase (runs for both SOCKS5 and HTTP once the handshake succeeds). - // Disable timeout for data transfer phase - struct timeval zero_timeout = {0, 0}; - setsockopt(proxy_sock, SOL_SOCKET, SO_RCVTIMEO, &zero_timeout, sizeof(zero_timeout)); - setsockopt(proxy_sock, SOL_SOCKET, SO_SNDTIMEO, &zero_timeout, sizeof(zero_timeout)); - setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &zero_timeout, sizeof(zero_timeout)); - setsockopt(client_sock, SOL_SOCKET, SO_SNDTIMEO, &zero_timeout, sizeof(zero_timeout)); - - // Enable and configure customized TCP keep-alives - int keepalive = 1; - int keepidle = 300; // 5 minutes in seconds - int keepintvl = 1; // 1 second interval - int keepcnt = 5; // 5 probes before dropping - setsockopt(proxy_sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)); - setsockopt(proxy_sock, SOL_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle)); - setsockopt(proxy_sock, SOL_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl)); - setsockopt(proxy_sock, SOL_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt)); - setsockopt(client_sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)); - setsockopt(client_sock, SOL_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle)); - setsockopt(client_sock, SOL_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl)); - setsockopt(client_sock, SOL_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt)); - - // setup transfer config - transfer_config_t *transfer_config = (transfer_config_t *)malloc(sizeof(transfer_config_t)); - if (transfer_config == NULL) - { - close(client_sock); - close(proxy_sock); - return NULL; - } - - transfer_config->from_socket = client_sock; - transfer_config->to_socket = proxy_sock; - - // transfer data both ways in this thread - transfer_handler((void*)transfer_config); - - return NULL; + struct pb_proxy p; + if (pb_core_get_proxy(proxy_id, &p) != 0) { snprintf(buf, n, "no proxy configured"); return -1; } + return pb_relay_test_proxy(&p, host, port, buf, n); } -// relay data both ways using splice for zero-copy -// data goes kernel to kernel thru pipe, never hits userspace -// way faster than copying thru userspace buffers -static void* transfer_handler(void *arg) +int ProxyBridge_TestConnection(const char *host, uint16_t port, char *buf, size_t n) { - transfer_config_t *config = (transfer_config_t *)arg; - int sock1 = config->from_socket; // client socket - int sock2 = config->to_socket; // proxy socket - free(config); - - // make 2 pipes for splice - // pipe_a: proxy to client (download) - // pipe_b: client to proxy (upload) - int pipe_a[2] = {-1, -1}; - int pipe_b[2] = {-1, -1}; - - if (pipe2(pipe_a, O_CLOEXEC | O_NONBLOCK) < 0 || - pipe2(pipe_b, O_CLOEXEC | O_NONBLOCK) < 0) { - // pipe failed, cleanup and fallback - if (pipe_a[0] >= 0) { close(pipe_a[0]); close(pipe_a[1]); } - if (pipe_b[0] >= 0) { close(pipe_b[0]); close(pipe_b[1]); } - goto fallback; - } - - // make pipes bigger for better speed (64kb to 1mb) - fcntl(pipe_a[0], F_SETPIPE_SZ, 1048576); - fcntl(pipe_b[0], F_SETPIPE_SZ, 1048576); - - // set sockets to nonblocking for splice - fcntl(sock1, F_SETFL, fcntl(sock1, F_GETFL, 0) | O_NONBLOCK); - fcntl(sock2, F_SETFL, fcntl(sock2, F_GETFL, 0) | O_NONBLOCK); - - { - struct pollfd fds[2]; - ssize_t pipe_a_bytes = 0; // bytes in download pipe (proxy→client) - ssize_t pipe_b_bytes = 0; // bytes in upload pipe (client→proxy) - bool sock1_done = false; // client EOF or error - bool sock2_done = false; // proxy EOF or error - bool shut_wr_sock1 = false; // already called shutdown(sock1, SHUT_WR) - bool shut_wr_sock2 = false; // already called shutdown(sock2, SHUT_WR) - - while (1) - { - // build poll set, use fd=-1 to skip closed sockets - // important: poll reports POLLHUP even with events=0 - // so use fd=-1 to actually skip it or we get busy loop - fds[0].fd = (!sock1_done || pipe_a_bytes > 0 || pipe_b_bytes > 0) ? sock1 : -1; - fds[1].fd = (!sock2_done || pipe_b_bytes > 0 || pipe_a_bytes > 0) ? sock2 : -1; - fds[0].events = 0; - fds[1].events = 0; - fds[0].revents = 0; - fds[1].revents = 0; - - // download: proxy to client - if (!sock2_done && pipe_a_bytes == 0) - fds[1].events |= POLLIN; // read from proxy - if (pipe_a_bytes > 0) - fds[0].events |= POLLOUT; // write to client - - // upload: client to proxy - if (!sock1_done && pipe_b_bytes == 0) - fds[0].events |= POLLIN; // read from client - if (pipe_b_bytes > 0) - fds[1].events |= POLLOUT; // write to proxy - - // all done - if (fds[0].fd == -1 && fds[1].fd == -1) - break; - if (fds[0].events == 0 && fds[1].events == 0) - break; - - int ready = poll(fds, 2, -1); - if (ready < 0) { - if (errno == EINTR) continue; - break; - } - - // check for errors and hangups - if (fds[0].revents & POLLERR) break; - if (fds[1].revents & POLLERR) break; - - // pollhup means other side closed - if ((fds[1].revents & POLLHUP) && !(fds[1].revents & POLLIN)) - sock2_done = true; - if ((fds[0].revents & POLLHUP) && !(fds[0].revents & POLLIN)) - sock1_done = true; - - // download path - - if (!sock2_done && pipe_a_bytes == 0 && (fds[1].revents & POLLIN)) { - ssize_t n = splice(sock2, NULL, pipe_a[1], NULL, 1048576, - SPLICE_F_MOVE | SPLICE_F_NONBLOCK); - if (n > 0) { - pipe_a_bytes = n; - } else if (n == 0) { - sock2_done = true; - } else if (errno != EAGAIN && errno != EWOULDBLOCK) { - sock2_done = true; - } - } - - if (pipe_a_bytes > 0 && (fds[0].revents & POLLOUT)) { - ssize_t n = splice(pipe_a[0], NULL, sock1, NULL, pipe_a_bytes, - SPLICE_F_MOVE | SPLICE_F_NONBLOCK); - if (n > 0) { - pipe_a_bytes -= n; - } else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { - break; - } - } - - // upload path - - if (!sock1_done && pipe_b_bytes == 0 && (fds[0].revents & POLLIN)) { - ssize_t n = splice(sock1, NULL, pipe_b[1], NULL, 1048576, - SPLICE_F_MOVE | SPLICE_F_NONBLOCK); - if (n > 0) { - pipe_b_bytes = n; - } else if (n == 0) { - sock1_done = true; - } else if (errno != EAGAIN && errno != EWOULDBLOCK) { - sock1_done = true; - } - } - - if (pipe_b_bytes > 0 && (fds[1].revents & POLLOUT)) { - ssize_t n = splice(pipe_b[0], NULL, sock2, NULL, pipe_b_bytes, - SPLICE_F_MOVE | SPLICE_F_NONBLOCK); - if (n > 0) { - pipe_b_bytes -= n; - } else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { - break; - } - } - - // half-close when one side done and pipe empty - // tell other side with shutdown - if (sock2_done && pipe_a_bytes == 0 && !shut_wr_sock1) { - shutdown(sock1, SHUT_WR); - shut_wr_sock1 = true; - } - if (sock1_done && pipe_b_bytes == 0 && !shut_wr_sock2) { - shutdown(sock2, SHUT_WR); - shut_wr_sock2 = true; - } - - // both sides finished and pipes empty - if (sock1_done && sock2_done && pipe_a_bytes == 0 && pipe_b_bytes == 0) - break; - } - } - - close(pipe_a[0]); close(pipe_a[1]); - close(pipe_b[0]); close(pipe_b[1]); - goto cleanup; - -fallback: - // fallback to normal recv/send if pipes didnt work - { - char buf[131072]; - struct pollfd fds[2]; - fds[0].fd = sock1; - fds[0].events = POLLIN; - fds[1].fd = sock2; - fds[1].events = POLLIN; - - while (1) { - int ready = poll(fds, 2, -1); - if (ready < 0) { - if (errno == EINTR) continue; - break; - } - - if (fds[0].revents & POLLERR || fds[1].revents & POLLERR) break; - - bool did_work = false; - if (fds[0].revents & POLLIN) { - ssize_t n = recv(sock1, buf, sizeof(buf), MSG_NOSIGNAL); - if (n <= 0) break; - if (send_all(sock2, buf, n) < 0) break; - did_work = true; - } - if (fds[1].revents & POLLIN) { - ssize_t n = recv(sock2, buf, sizeof(buf), MSG_NOSIGNAL); - if (n <= 0) break; - if (send_all(sock1, buf, n) < 0) break; - did_work = true; - } - - // pollhup with no data means peer closed - if (!did_work) break; - } - } - -cleanup: - shutdown(sock1, SHUT_RDWR); - shutdown(sock2, SHUT_RDWR); - close(sock1); - close(sock2); - return NULL; -} - -// proxy server accepts connections and spawns threads -static void* local_proxy_server(void *arg) -{ - (void)arg; - struct sockaddr_in addr; - int listen_sock; - int on = 1; - - listen_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); - if (listen_sock < 0) - { - log_message("Socket creation failed"); - return NULL; - } - - setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); - setsockopt(listen_sock, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); - - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - // loopback only, INADDR_ANY was reachable from the lan - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = htons(g_local_relay_port); - - if (bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) - { - log_message("Bind failed"); - close(listen_sock); - return NULL; - } - - if (listen(listen_sock, SOMAXCONN) < 0) - { - log_message("Listen failed"); - close(listen_sock); - return NULL; - } - - // create thread attrs with small stack (256kb not 8mb) - // relay threads dont need big buffers anymore cuz splice - pthread_attr_t thread_attr; - pthread_attr_init(&thread_attr); - pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); - pthread_attr_setstacksize(&thread_attr, 262144); // 256KB stack - - struct pollfd pfd; - pfd.fd = listen_sock; - pfd.events = POLLIN; - - while (running) - { - int ready = poll(&pfd, 1, 1000); // 1s timeout - if (ready <= 0) - continue; - - struct sockaddr_in client_addr; - socklen_t addr_len = sizeof(client_addr); - int client_sock = accept4(listen_sock, (struct sockaddr *)&client_addr, &addr_len, SOCK_CLOEXEC); - - if (client_sock < 0) - continue; - - connection_config_t *conn_config = (connection_config_t *)malloc(sizeof(connection_config_t)); - if (conn_config == NULL) - { - close(client_sock); - continue; - } - - conn_config->client_socket = client_sock; - - uint16_t client_port = ntohs(client_addr.sin_port); - if (!get_connection(client_port, false, &conn_config->orig_dest_ip, &conn_config->orig_dest_port)) - { - close(client_sock); - free(conn_config); - continue; - } - - pthread_t conn_thread; - if (pthread_create(&conn_thread, &thread_attr, connection_handler, (void*)conn_config) != 0) - { - close(client_sock); - free(conn_config); - continue; - } - } - - pthread_attr_destroy(&thread_attr); - close(listen_sock); - return NULL; -} - -// socks5 udp associate -static int socks5_udp_associate(int s, struct sockaddr_in *relay_addr) -{ - unsigned char buf[SOCKS5_BUFFER_SIZE]; // must hold 3 + 255 + 255 auth bytes - - // auth handshake - bool use_auth = (g_proxy_username[0] != '\0'); - buf[0] = SOCKS5_VERSION; - buf[1] = use_auth ? 0x02 : 0x01; - buf[2] = SOCKS5_AUTH_NONE; - if (use_auth) - buf[3] = 0x02; // username/password auth - - if (send(s, buf, use_auth ? 4 : 3, MSG_NOSIGNAL) != (use_auth ? 4 : 3)) - return -1; - - if (recv_n(s, buf, 2) != 2 || buf[0] != SOCKS5_VERSION) - return -1; - - if (buf[1] == 0x02 && use_auth) - { - size_t ulen = strlen(g_proxy_username); - size_t plen = strlen(g_proxy_password); - if (ulen > 255 || plen > 255) - return -1; - buf[0] = 0x01; - buf[1] = (unsigned char)ulen; - memcpy(buf + 2, g_proxy_username, ulen); - buf[2 + ulen] = (unsigned char)plen; - memcpy(buf + 3 + ulen, g_proxy_password, plen); - - if (send(s, buf, 3 + ulen + plen, MSG_NOSIGNAL) != (ssize_t)(3 + ulen + plen)) - return -1; - - if (recv_n(s, buf, 2) != 2 || buf[0] != 0x01 || buf[1] != 0x00) - return -1; - } - else if (buf[1] != SOCKS5_AUTH_NONE) - return -1; - - // udp associate request - buf[0] = SOCKS5_VERSION; - buf[1] = SOCKS5_CMD_UDP_ASSOCIATE; - buf[2] = 0x00; - buf[3] = SOCKS5_ATYP_IPV4; - memset(buf + 4, 0, 4); // 0.0.0.0 - memset(buf + 8, 0, 2); // port 0 - - if (send(s, buf, 10, MSG_NOSIGNAL) != 10) - return -1; - - // Reply: VER REP RSV ATYP BND.ADDR BND.PORT. Parse the header then the bound - // relay endpoint; we only use IPv4 relay addresses. - unsigned char rep[4]; - if (recv_n(s, rep, 4) != 4 || rep[0] != SOCKS5_VERSION || rep[1] != 0x00) - return -1; - if (rep[3] != SOCKS5_ATYP_IPV4) - return -1; - unsigned char ap[6]; - if (recv_n(s, ap, 6) != 6) - return -1; - - memset(relay_addr, 0, sizeof(*relay_addr)); - relay_addr->sin_family = AF_INET; - memcpy(&relay_addr->sin_addr.s_addr, ap, 4); - memcpy(&relay_addr->sin_port, ap + 4, 2); - return 0; -} - -static bool establish_udp_associate(void) -{ - uint64_t now = get_monotonic_ms(); - if (now - last_udp_connect_attempt < 5000) - return false; - - last_udp_connect_attempt = now; - - if (socks5_udp_control_socket >= 0) - { - close(socks5_udp_control_socket); - socks5_udp_control_socket = -1; - } - if (socks5_udp_send_socket >= 0) - { - close(socks5_udp_send_socket); - socks5_udp_send_socket = -1; - } - - int tcp_sock = socket(AF_INET, SOCK_STREAM, 0); - if (tcp_sock < 0) - return false; - - configure_tcp_socket(tcp_sock, 262144, 3000); - - uint32_t socks5_ip = resolve_hostname(g_proxy_host); - if (socks5_ip == 0) - { - close(tcp_sock); - return false; - } - - struct sockaddr_in socks_addr; - memset(&socks_addr, 0, sizeof(socks_addr)); - socks_addr.sin_family = AF_INET; - socks_addr.sin_addr.s_addr = socks5_ip; - socks_addr.sin_port = htons(g_proxy_port); - - if (connect(tcp_sock, (struct sockaddr *)&socks_addr, sizeof(socks_addr)) < 0) - { - close(tcp_sock); - return false; - } - - if (socks5_udp_associate(tcp_sock, &socks5_udp_relay_addr) != 0) - { - close(tcp_sock); - return false; - } - - // rfc says if server gives 0.0.0.0 use proxy servers ip instead - if (socks5_udp_relay_addr.sin_addr.s_addr == INADDR_ANY) - socks5_udp_relay_addr.sin_addr.s_addr = socks5_ip; - - // Handshake done - remove the 3s timeout so the control socket stays open indefinitely. - struct timeval zero_tv = {0, 0}; - setsockopt(tcp_sock, SOL_SOCKET, SO_RCVTIMEO, &zero_tv, sizeof(zero_tv)); - setsockopt(tcp_sock, SOL_SOCKET, SO_SNDTIMEO, &zero_tv, sizeof(zero_tv)); - - // Enable TCP keepalives so the SOCKS5 proxy doesn't idle-close the control - // connection (many proxies terminate it after ~60s of silence, killing UDP ASSOCIATE). - int ka_on = 1; - int ka_idle = 10; // start keepalives after 10s idle - int ka_intvl = 2; // send keepalive every 2s - int ka_cnt = 5; // drop after 5 missed keepalives - setsockopt(tcp_sock, SOL_SOCKET, SO_KEEPALIVE, &ka_on, sizeof(ka_on)); - setsockopt(tcp_sock, SOL_TCP, TCP_KEEPIDLE, &ka_idle, sizeof(ka_idle)); - setsockopt(tcp_sock, SOL_TCP, TCP_KEEPINTVL, &ka_intvl, sizeof(ka_intvl)); - setsockopt(tcp_sock, SOL_TCP, TCP_KEEPCNT, &ka_cnt, sizeof(ka_cnt)); - - socks5_udp_control_socket = tcp_sock; - - socks5_udp_send_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (socks5_udp_send_socket < 0) - { - close(socks5_udp_control_socket); - socks5_udp_control_socket = -1; - return false; - } - - configure_udp_socket(socks5_udp_send_socket, 262144, 30000); - - udp_associate_connected = true; - log_message("UDP ASSOCIATE established with SOCKS5 proxy"); - return true; -} - -// teardown udp associate so next packet reconnects -static void teardown_udp_associate(void) -{ - udp_associate_connected = false; - if (socks5_udp_control_socket >= 0) - { - close(socks5_udp_control_socket); - socks5_udp_control_socket = -1; - } - if (socks5_udp_send_socket >= 0) - { - close(socks5_udp_send_socket); - socks5_udp_send_socket = -1; - } -} - -static void* udp_relay_server(void *arg) -{ - (void)arg; - struct sockaddr_in local_addr, from_addr; - unsigned char recv_buf[65536]; - unsigned char send_buf[65536]; - socklen_t from_len; - - udp_relay_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (udp_relay_socket < 0) - return NULL; - - int on = 1; - setsockopt(udp_relay_socket, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); - configure_udp_socket(udp_relay_socket, 262144, 30000); - - memset(&local_addr, 0, sizeof(local_addr)); - local_addr.sin_family = AF_INET; - local_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - local_addr.sin_port = htons(LOCAL_UDP_RELAY_PORT); - - if (bind(udp_relay_socket, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) - { - close(udp_relay_socket); - udp_relay_socket = -1; - return NULL; - } - - // try initial connect, not fatal if proxy not up yet - udp_associate_connected = establish_udp_associate(); - - while (running) - { - struct pollfd fds[3]; - int nfds = 1; - - // watch local relay socket for client packets - fds[0].fd = udp_relay_socket; - fds[0].events = POLLIN; - fds[0].revents = 0; - - // watch socks5 udp socket for proxy responses - fds[1].fd = (udp_associate_connected && socks5_udp_send_socket >= 0) ? socks5_udp_send_socket : -1; - fds[1].events = POLLIN; - fds[1].revents = 0; - - // watch socks5 tcp socket to detect if connection dies - fds[2].fd = (udp_associate_connected && socks5_udp_control_socket >= 0) ? socks5_udp_control_socket : -1; - fds[2].events = POLLIN; - fds[2].revents = 0; - nfds = 3; - - int ready = poll(fds, nfds, 1000); // 1s timeout - if (ready <= 0) - { - // Proactively reconnect so the next client packet is not dropped. - if (!udp_associate_connected) - udp_associate_connected = establish_udp_associate(); - continue; - } - - // check if tcp control still alive - // if it dies the udp associate is dead too - if (fds[2].fd >= 0 && (fds[2].revents & (POLLIN | POLLHUP | POLLERR))) - { - char peek_buf[1]; - ssize_t peek_len = recv(socks5_udp_control_socket, peek_buf, 1, MSG_PEEK | MSG_DONTWAIT); - if (peek_len == 0 || (peek_len < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) - { - // tcp died - tear down and reconnect immediately so real-time streams - // lose as few packets as possible. - teardown_udp_associate(); - udp_associate_connected = establish_udp_associate(); - continue; - } - } - - // client sending to proxy - if (fds[0].revents & POLLIN) - { - from_len = sizeof(from_addr); - ssize_t recv_len = recvfrom(udp_relay_socket, recv_buf, sizeof(recv_buf), 0, - (struct sockaddr *)&from_addr, &from_len); - if (recv_len <= 0) - continue; - - // try to connect if not connected yet - if (!udp_associate_connected) - { - if (!establish_udp_associate()) - continue; - } - - uint16_t client_port = ntohs(from_addr.sin_port); - uint32_t dest_ip; - uint16_t dest_port; - - if (!get_connection(client_port, true, &dest_ip, &dest_port)) - continue; - - // make sure data fits with socks5 header - if (recv_len > (ssize_t)(sizeof(send_buf) - 10)) - continue; - - // build socks5 udp packet header - send_buf[0] = 0x00; // RSV - send_buf[1] = 0x00; // RSV - send_buf[2] = 0x00; // FRAG - send_buf[3] = SOCKS5_ATYP_IPV4; - memcpy(send_buf + 4, &dest_ip, 4); - uint16_t port_net = htons(dest_port); - memcpy(send_buf + 8, &port_net, 2); - memcpy(send_buf + 10, recv_buf, recv_len); - - ssize_t sent = sendto(socks5_udp_send_socket, send_buf, 10 + recv_len, 0, - (struct sockaddr *)&socks5_udp_relay_addr, sizeof(socks5_udp_relay_addr)); - - if (sent < 0) - { - // sendto failed - proxy likely died. Tear down, reconnect immediately - // and retry the current packet so real-time streams lose at most one packet. - teardown_udp_associate(); - if (establish_udp_associate()) - { - udp_associate_connected = true; - sendto(socks5_udp_send_socket, send_buf, (size_t)(10 + recv_len), 0, - (struct sockaddr *)&socks5_udp_relay_addr, sizeof(socks5_udp_relay_addr)); - } - } - else if (socks5_udp_send_socket >= 0) - { - // After sending (especially on a freshly established association), the proxy - // may respond within the same poll() cycle before the socket was in fds[]. - // Do a non-blocking check now so we don't add an extra poll() round-trip. - struct pollfd quick = { socks5_udp_send_socket, POLLIN, 0 }; - if (poll(&quick, 1, 0) > 0) - fds[1].revents |= POLLIN; - } - } - - // proxy sending back to client - if (fds[1].fd >= 0 && (fds[1].revents & POLLIN)) - { - from_len = sizeof(from_addr); - ssize_t recv_len = recvfrom(socks5_udp_send_socket, recv_buf, sizeof(recv_buf), 0, - (struct sockaddr *)&from_addr, &from_len); - - // socket error, proxy might be dead - if (recv_len < 0) - { - if (errno != EAGAIN && errno != EWOULDBLOCK) - teardown_udp_associate(); - continue; - } - if (recv_len < 10) - continue; - - // we dont support fragmented packets - if (recv_buf[2] != 0x00) - continue; - - // parse socks5 udp packet - if (recv_buf[3] != SOCKS5_ATYP_IPV4) - continue; - - uint32_t src_ip; - uint16_t src_port; - memcpy(&src_ip, recv_buf + 4, 4); - memcpy(&src_port, recv_buf + 8, 2); - src_port = ntohs(src_port); - - // find which client sent packet to this destination - // loop thru hash table looking for dest match - pthread_rwlock_rdlock(&conn_lock); - - struct sockaddr_in client_addr; - bool found_client = false; - - for (int hash = 0; hash < CONNECTION_HASH_SIZE; hash++) - { - CONNECTION_INFO *conn = connection_hash_table[hash]; - while (conn != NULL) - { - if (conn->is_udp && - conn->orig_dest_ip == src_ip && - conn->orig_dest_port == src_port) - { - // found it, send response back to original client port - memset(&client_addr, 0, sizeof(client_addr)); - client_addr.sin_family = AF_INET; - client_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - client_addr.sin_port = htons(conn->src_port); - // Keep session alive while proxy delivers data (benign relaxed write under read lock) - conn->last_activity = get_monotonic_ms(); - found_client = true; - break; - } - conn = conn->next; - } - if (found_client) - break; - } - - pthread_rwlock_unlock(&conn_lock); - - if (found_client) - { - // send unwrapped data back to client - ssize_t data_len = recv_len - 10; - sendto(udp_relay_socket, recv_buf + 10, data_len, 0, - (struct sockaddr *)&client_addr, sizeof(client_addr)); - } - } - - // handle errors on udp send socket - if (fds[1].fd >= 0 && (fds[1].revents & (POLLHUP | POLLERR))) - { - teardown_udp_associate(); - } - } - - teardown_udp_associate(); - if (udp_relay_socket >= 0) - close(udp_relay_socket); - - return NULL; -} - -// nfqueue callback for packets -static int packet_callback(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfad, void *data) -{ - (void)nfmsg; - (void)data; - - struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfad); - if (!ph) return nfq_set_verdict(qh, 0, NF_ACCEPT, 0, NULL); - - uint32_t id = ntohl(ph->packet_id); - - unsigned char *payload; - int payload_len = nfq_get_payload(nfad, &payload); - if (payload_len < (int)sizeof(struct iphdr)) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - struct iphdr *iph = (struct iphdr *)payload; - - // fast path when no rules - if (!g_has_active_rules && g_connection_callback == NULL) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - uint32_t src_ip = iph->saddr; - uint32_t dest_ip = iph->daddr; - uint16_t src_port = 0; - uint16_t dest_port = 0; - RuleAction action = RULE_ACTION_DIRECT; - uint32_t pid = 0; - - if (iph->protocol == IPPROTO_TCP) - { - if (payload_len < (int)(iph->ihl * 4 + sizeof(struct tcphdr))) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - struct tcphdr *tcph = (struct tcphdr *)(payload + iph->ihl * 4); - src_port = ntohs(tcph->source); - dest_port = ntohs(tcph->dest); - - // skip our own packets from local relay - if (src_port == g_local_relay_port) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - if (is_connection_tracked(src_port, false)) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - // only look at syn packets for new connections - if (!(tcph->syn && !tcph->ack)) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - if (dest_port == 53 && !g_dns_via_proxy) - action = RULE_ACTION_DIRECT; - else - action = check_process_rule(src_ip, src_port, dest_ip, dest_port, false, &pid); - - if (action == RULE_ACTION_PROXY && is_broadcast_or_multicast(dest_ip)) - action = RULE_ACTION_DIRECT; - - // log it if not from our own process - if (g_traffic_logging_enabled && g_connection_callback != NULL && (tcph->syn && !tcph->ack) && pid > 0 && pid != g_current_process_id) - { - char process_name[MAX_PROCESS_NAME]; - if (get_process_name_from_pid(pid, process_name, sizeof(process_name))) - { - if (!is_connection_already_logged(pid, dest_ip, dest_port, action)) - { - char dest_ip_str[32]; - format_ip_address(dest_ip, dest_ip_str, sizeof(dest_ip_str)); - - char proxy_info[300]; - if (action == RULE_ACTION_PROXY) - { - snprintf(proxy_info, sizeof(proxy_info), "proxy %s://%s:%d tcp", - g_proxy_type == PROXY_TYPE_HTTP ? "http" : "socks5", - g_proxy_host, g_proxy_port); - } - else if (action == RULE_ACTION_DIRECT) - { - snprintf(proxy_info, sizeof(proxy_info), "direct tcp"); - } - else if (action == RULE_ACTION_BLOCK) - { - snprintf(proxy_info, sizeof(proxy_info), "blocked tcp"); - } - - const char* display_name = extract_filename(process_name); - g_connection_callback(display_name, pid, dest_ip_str, dest_port, proxy_info); - - add_logged_connection(pid, dest_ip, dest_port, action); - } - } - } - - if (action == RULE_ACTION_DIRECT) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - else if (action == RULE_ACTION_BLOCK) - return nfq_set_verdict(qh, id, NF_DROP, 0, NULL); - else if (action == RULE_ACTION_PROXY) - { - // store connection info - add_connection(src_port, false, src_ip, dest_ip, dest_port); - - // mark packet so nat table REDIRECT rule will catch it - uint32_t mark = 1; - return nfq_set_verdict2(qh, id, NF_ACCEPT, mark, 0, NULL); - } - } - else if (iph->protocol == IPPROTO_UDP) - { - if (payload_len < (int)(iph->ihl * 4 + sizeof(struct udphdr))) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - struct udphdr *udph = (struct udphdr *)(payload + iph->ihl * 4); - src_port = ntohs(udph->source); - dest_port = ntohs(udph->dest); - - if (src_port == LOCAL_UDP_RELAY_PORT) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - if (is_connection_tracked(src_port, true)) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - - if (dest_port == 53 && !g_dns_via_proxy) - action = RULE_ACTION_DIRECT; - else - action = check_process_rule(src_ip, src_port, dest_ip, dest_port, true, &pid); - - if (action == RULE_ACTION_PROXY && is_broadcast_or_multicast(dest_ip)) - action = RULE_ACTION_DIRECT; - - if (action == RULE_ACTION_PROXY && (dest_port == 67 || dest_port == 68)) - action = RULE_ACTION_DIRECT; - - // UDP proxy only works with SOCKS5, not HTTP - if (action == RULE_ACTION_PROXY && g_proxy_type != PROXY_TYPE_SOCKS5) - action = RULE_ACTION_DIRECT; - - // log (skip our own process, log even without PID for ephemeral UDP sockets) - if (g_traffic_logging_enabled && g_connection_callback != NULL && pid != g_current_process_id) - { - char process_name[MAX_PROCESS_NAME]; - uint32_t log_pid = (pid == 0) ? 1 : pid; // Use PID 1 for unknown processes - - if (pid > 0 && get_process_name_from_pid(pid, process_name, sizeof(process_name))) - { - // Got process name from PID - } - else - { - // UDP socket not found - ephemeral or timing issue - snprintf(process_name, sizeof(process_name), "unknown"); - } - - if (!is_connection_already_logged(log_pid, dest_ip, dest_port, action)) - { - char dest_ip_str[32]; - format_ip_address(dest_ip, dest_ip_str, sizeof(dest_ip_str)); - - char proxy_info[300]; - if (action == RULE_ACTION_PROXY) - { - snprintf(proxy_info, sizeof(proxy_info), "proxy socks5://%s:%d udp", - g_proxy_host, g_proxy_port); - } - else if (action == RULE_ACTION_DIRECT) - { - snprintf(proxy_info, sizeof(proxy_info), "direct udp"); - } - else if (action == RULE_ACTION_BLOCK) - { - snprintf(proxy_info, sizeof(proxy_info), "blocked udp"); - } - - const char* display_name = extract_filename(process_name); - g_connection_callback(display_name, log_pid, dest_ip_str, dest_port, proxy_info); - - add_logged_connection(log_pid, dest_ip, dest_port, action); - } - } - - if (action == RULE_ACTION_DIRECT) - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); - else if (action == RULE_ACTION_BLOCK) - return nfq_set_verdict(qh, id, NF_DROP, 0, NULL); - else if (action == RULE_ACTION_PROXY) - { - // UDP proxy via SOCKS5 UDP ASSOCIATE - add_connection(src_port, true, src_ip, dest_ip, dest_port); - - // Mark UDP packet for redirect to local UDP relay (port 34011) - uint32_t mark = 2; // Use mark=2 for UDP (mark=1 is for TCP) - return nfq_set_verdict2(qh, id, NF_ACCEPT, mark, 0, NULL); - } - } - - return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL); -} - -static void* packet_processor(void *arg) -{ - (void)arg; - char buf[4096] __attribute__((aligned)); - int fd = nfq_fd(nfq_h); - ssize_t rv; - - while (running) - { - rv = recv(fd, buf, sizeof(buf), 0); - if (rv >= 0) - nfq_handle_packet(nfq_h, buf, rv); - // On error: ENOBUFS = kernel queue full (normal under load), - // EINTR = signal, other = transient. Always continue - - // stopping the thread would break all network traffic. - } - - return NULL; -} - -static inline uint32_t connection_hash(uint16_t port) -{ - return port % CONNECTION_HASH_SIZE; -} - -static void add_connection(uint16_t src_port, bool is_udp, uint32_t src_ip, uint32_t dest_ip, uint16_t dest_port) -{ - uint32_t hash = connection_hash(src_port); - pthread_rwlock_wrlock(&conn_lock); - - CONNECTION_INFO *conn = connection_hash_table[hash]; - while (conn != NULL) - { - if (conn->src_port == src_port && conn->is_udp == is_udp) - { - conn->orig_dest_ip = dest_ip; - conn->orig_dest_port = dest_port; - conn->src_ip = src_ip; - conn->is_tracked = true; - conn->last_activity = get_monotonic_ms(); - pthread_rwlock_unlock(&conn_lock); - return; - } - conn = conn->next; - } - - CONNECTION_INFO *new_conn = malloc(sizeof(CONNECTION_INFO)); - if (new_conn != NULL) - { - new_conn->src_port = src_port; - new_conn->is_udp = is_udp; - new_conn->src_ip = src_ip; - new_conn->orig_dest_ip = dest_ip; - new_conn->orig_dest_port = dest_port; - new_conn->is_tracked = true; - new_conn->last_activity = get_monotonic_ms(); - new_conn->next = connection_hash_table[hash]; - connection_hash_table[hash] = new_conn; - } - - pthread_rwlock_unlock(&conn_lock); -} - -static bool get_connection(uint16_t src_port, bool is_udp, uint32_t *dest_ip, uint16_t *dest_port) -{ - uint32_t hash = connection_hash(src_port); - pthread_rwlock_rdlock(&conn_lock); - - CONNECTION_INFO *conn = connection_hash_table[hash]; - while (conn != NULL) - { - if (conn->src_port == src_port && conn->is_udp == is_udp && conn->is_tracked) - { - *dest_ip = conn->orig_dest_ip; - *dest_port = conn->orig_dest_port; - conn->last_activity = get_monotonic_ms(); // benign race on timestamp - pthread_rwlock_unlock(&conn_lock); - return true; - } - conn = conn->next; - } - - pthread_rwlock_unlock(&conn_lock); - return false; -} - -static bool is_connection_tracked(uint16_t src_port, bool is_udp) -{ - uint32_t hash = connection_hash(src_port); - pthread_rwlock_rdlock(&conn_lock); - - CONNECTION_INFO *conn = connection_hash_table[hash]; - while (conn != NULL) - { - if (conn->src_port == src_port && conn->is_udp == is_udp && conn->is_tracked) - { - pthread_rwlock_unlock(&conn_lock); - return true; - } - conn = conn->next; - } - - pthread_rwlock_unlock(&conn_lock); - return false; -} - -static void __attribute__((unused)) remove_connection(uint16_t src_port) -{ - uint32_t hash = connection_hash(src_port); - pthread_rwlock_wrlock(&conn_lock); - - CONNECTION_INFO **conn_ptr = &connection_hash_table[hash]; - while (*conn_ptr != NULL) - { - if ((*conn_ptr)->src_port == src_port) - { - CONNECTION_INFO *to_free = *conn_ptr; - *conn_ptr = (*conn_ptr)->next; - free(to_free); - pthread_rwlock_unlock(&conn_lock); - return; - } - conn_ptr = &(*conn_ptr)->next; - } - - pthread_rwlock_unlock(&conn_lock); -} - -static void cleanup_stale_connections(void) -{ - uint64_t now = get_monotonic_ms(); - - // Cleanup connection hash table - for (int i = 0; i < CONNECTION_HASH_SIZE; i++) - { - pthread_rwlock_wrlock(&conn_lock); - CONNECTION_INFO **conn_ptr = &connection_hash_table[i]; - - while (*conn_ptr != NULL) - { - // 30 min idle timeout. Clean closes are removed on FIN/RST already; a short - // timeout would reap open-but-idle proxied connections (mail IDLE, voice, - // websocket) mid-session and silently break them until restart. - if (now - (*conn_ptr)->last_activity > 1800000) - { - CONNECTION_INFO *to_free = *conn_ptr; - *conn_ptr = (*conn_ptr)->next; - free(to_free); - } - else - { - conn_ptr = &(*conn_ptr)->next; - } - } - pthread_rwlock_unlock(&conn_lock); - } - - // Cleanup PID cache (separate lock - no contention with connection lookups) - uint64_t now_cache = get_monotonic_ms(); - for (int i = 0; i < PID_CACHE_SIZE; i++) - { - pthread_mutex_lock(&pid_cache_lock); - PID_CACHE_ENTRY **entry_ptr = &pid_cache[i]; - while (*entry_ptr != NULL) - { - if (now_cache - (*entry_ptr)->timestamp > 10000) // 10 sec cache TTL - { - PID_CACHE_ENTRY *to_free = *entry_ptr; - *entry_ptr = (*entry_ptr)->next; - free(to_free); - } - else - { - entry_ptr = &(*entry_ptr)->next; - } - } - pthread_mutex_unlock(&pid_cache_lock); - } - - // Keep only last 100 logged connections - pthread_mutex_lock(&log_lock); - int logged_count = 0; - LOGGED_CONNECTION *temp = logged_connections; - while (temp != NULL) - { - logged_count++; - temp = temp->next; - } - - if (logged_count > 100) - { - temp = logged_connections; - for (int i = 0; i < 99 && temp != NULL; i++) - { - temp = temp->next; - } - if (temp != NULL && temp->next != NULL) - { - LOGGED_CONNECTION *to_free = temp->next; - temp->next = NULL; - while (to_free != NULL) - { - LOGGED_CONNECTION *next = to_free->next; - free(to_free); - to_free = next; - } - } - } - pthread_mutex_unlock(&log_lock); -} - -static bool is_connection_already_logged(uint32_t pid, uint32_t dest_ip, uint16_t dest_port, RuleAction action) -{ - pthread_mutex_lock(&log_lock); - - LOGGED_CONNECTION *logged = logged_connections; - while (logged != NULL) - { - if (logged->pid == pid && logged->dest_ip == dest_ip && - logged->dest_port == dest_port && logged->action == action) - { - pthread_mutex_unlock(&log_lock); - return true; - } - logged = logged->next; - } - - pthread_mutex_unlock(&log_lock); - return false; -} - -static void add_logged_connection(uint32_t pid, uint32_t dest_ip, uint16_t dest_port, RuleAction action) -{ - pthread_mutex_lock(&log_lock); - - // keep only last 100 entries to avoid memory growth - int count = 0; - LOGGED_CONNECTION *temp = logged_connections; - while (temp != NULL && count < 100) - { - count++; - temp = temp->next; - } - - if (count >= 100) - { - temp = logged_connections; - for (int i = 0; i < 98 && temp != NULL; i++) - { - temp = temp->next; - } - - if (temp != NULL && temp->next != NULL) - { - LOGGED_CONNECTION *to_free_list = temp->next; - temp->next = NULL; - - // Free excess entries (still under log_lock, but this is rare) - while (to_free_list != NULL) - { - LOGGED_CONNECTION *next = to_free_list->next; - free(to_free_list); - to_free_list = next; - } - } - } - - LOGGED_CONNECTION *logged = malloc(sizeof(LOGGED_CONNECTION)); - if (logged != NULL) - { - logged->pid = pid; - logged->dest_ip = dest_ip; - logged->dest_port = dest_port; - logged->action = action; - logged->next = logged_connections; - logged_connections = logged; - } - - pthread_mutex_unlock(&log_lock); -} - -static void clear_logged_connections(void) -{ - pthread_mutex_lock(&log_lock); - - while (logged_connections != NULL) - { - LOGGED_CONNECTION *to_free = logged_connections; - logged_connections = logged_connections->next; - free(to_free); - } - - pthread_mutex_unlock(&log_lock); -} - -static uint32_t pid_cache_hash(uint32_t src_ip, uint16_t src_port, bool is_udp) -{ - uint32_t hash = src_ip ^ ((uint32_t)src_port << 16) ^ (is_udp ? 0x80000000 : 0); - return hash % PID_CACHE_SIZE; -} - -static uint32_t get_cached_pid(uint32_t src_ip, uint16_t src_port, bool is_udp) -{ - uint32_t hash = pid_cache_hash(src_ip, src_port, is_udp); - uint64_t current_time = get_monotonic_ms(); - uint32_t pid = 0; - - pthread_mutex_lock(&pid_cache_lock); - - PID_CACHE_ENTRY *entry = pid_cache[hash]; - while (entry != NULL) - { - if (entry->src_ip == src_ip && - entry->src_port == src_port && - entry->is_udp == is_udp) - { - if (current_time - entry->timestamp < PID_CACHE_TTL_MS) - { - pid = entry->pid; - break; - } - else - { - break; - } - } - entry = entry->next; - } - - pthread_mutex_unlock(&pid_cache_lock); - return pid; -} - -static void cache_pid(uint32_t src_ip, uint16_t src_port, uint32_t pid, bool is_udp) -{ - uint32_t hash = pid_cache_hash(src_ip, src_port, is_udp); - uint64_t current_time = get_monotonic_ms(); - - pthread_mutex_lock(&pid_cache_lock); - - PID_CACHE_ENTRY *entry = pid_cache[hash]; - while (entry != NULL) - { - if (entry->src_ip == src_ip && - entry->src_port == src_port && - entry->is_udp == is_udp) - { - entry->pid = pid; - entry->timestamp = current_time; - pthread_mutex_unlock(&pid_cache_lock); - return; - } - entry = entry->next; - } - - PID_CACHE_ENTRY *new_entry = malloc(sizeof(PID_CACHE_ENTRY)); - if (new_entry != NULL) - { - new_entry->src_ip = src_ip; - new_entry->src_port = src_port; - new_entry->pid = pid; - new_entry->timestamp = current_time; - new_entry->is_udp = is_udp; - new_entry->next = pid_cache[hash]; - pid_cache[hash] = new_entry; - } - - pthread_mutex_unlock(&pid_cache_lock); -} - -static void clear_pid_cache(void) -{ - pthread_mutex_lock(&pid_cache_lock); - - for (int i = 0; i < PID_CACHE_SIZE; i++) - { - while (pid_cache[i] != NULL) - { - PID_CACHE_ENTRY *to_free = pid_cache[i]; - pid_cache[i] = pid_cache[i]->next; - free(to_free); - } - } - - pthread_mutex_unlock(&pid_cache_lock); -} - -static void* cleanup_worker(void *arg) -{ - (void)arg; - while (running) - { - sleep(30); // 30 seconds - if (running) - { - cleanup_stale_connections(); - } - } - return NULL; -} - -static void update_has_active_rules(void) -{ - g_has_active_rules = false; - PROCESS_RULE *rule = rules_list; - while (rule != NULL) - { - if (rule->enabled) - { - g_has_active_rules = true; - break; - } - rule = rule->next; - } -} - -uint32_t ProxyBridge_AddRule(const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action) -{ - if (process_name == NULL || process_name[0] == '\0') - return 0; - - PROCESS_RULE *rule = malloc(sizeof(PROCESS_RULE)); - if (rule == NULL) - return 0; - - rule->rule_id = g_next_rule_id++; - strncpy(rule->process_name, process_name, MAX_PROCESS_NAME - 1); - rule->process_name[MAX_PROCESS_NAME - 1] = '\0'; - rule->protocol = protocol; - - if (target_hosts != NULL && target_hosts[0] != '\0') - { - rule->target_hosts = strdup(target_hosts); - if (rule->target_hosts == NULL) - { - free(rule); - return 0; - } - } - else - { - rule->target_hosts = strdup("*"); - if (rule->target_hosts == NULL) - { - free(rule); - return 0; - } - } - - if (target_ports != NULL && target_ports[0] != '\0') - { - rule->target_ports = strdup(target_ports); - if (rule->target_ports == NULL) - { - free(rule->target_hosts); - free(rule); - return 0; - } - } - else - { - rule->target_ports = strdup("*"); - if (rule->target_ports == NULL) - { - free(rule->target_hosts); - free(rule); - return 0; - } - } - - rule->action = action; - rule->enabled = true; - - pthread_rwlock_wrlock(&rules_lock); - rule->next = rules_list; - rules_list = rule; - update_has_active_rules(); - pthread_rwlock_unlock(&rules_lock); - - log_message("added rule id %u for process %s protocol %d action %d", rule->rule_id, process_name, protocol, action); - - return rule->rule_id; -} - -bool ProxyBridge_EnableRule(uint32_t rule_id) -{ - if (rule_id == 0) - return false; - - pthread_rwlock_wrlock(&rules_lock); - PROCESS_RULE *rule = rules_list; - while (rule != NULL) - { - if (rule->rule_id == rule_id) - { - rule->enabled = true; - update_has_active_rules(); - pthread_rwlock_unlock(&rules_lock); - log_message("enabled rule id %u", rule_id); - return true; - } - rule = rule->next; - } - pthread_rwlock_unlock(&rules_lock); - return false; -} - -bool ProxyBridge_DisableRule(uint32_t rule_id) -{ - if (rule_id == 0) - return false; - - pthread_rwlock_wrlock(&rules_lock); - PROCESS_RULE *rule = rules_list; - while (rule != NULL) - { - if (rule->rule_id == rule_id) - { - rule->enabled = false; - update_has_active_rules(); - pthread_rwlock_unlock(&rules_lock); - log_message("disabled rule id %u", rule_id); - return true; - } - rule = rule->next; - } - pthread_rwlock_unlock(&rules_lock); - return false; -} - -bool ProxyBridge_DeleteRule(uint32_t rule_id) -{ - if (rule_id == 0) - return false; - - pthread_rwlock_wrlock(&rules_lock); - PROCESS_RULE *rule = rules_list; - PROCESS_RULE *prev = NULL; - - while (rule != NULL) - { - if (rule->rule_id == rule_id) - { - if (prev == NULL) - rules_list = rule->next; - else - prev->next = rule->next; - - update_has_active_rules(); - pthread_rwlock_unlock(&rules_lock); - - if (rule->target_hosts != NULL) - free(rule->target_hosts); - if (rule->target_ports != NULL) - free(rule->target_ports); - free(rule); - - log_message("deleted rule id %u", rule_id); - return true; - } - prev = rule; - rule = rule->next; - } - pthread_rwlock_unlock(&rules_lock); - return false; -} - -bool ProxyBridge_EditRule(uint32_t rule_id, const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action) -{ - if (rule_id == 0 || process_name == NULL || target_hosts == NULL || target_ports == NULL) - return false; - - // Pre-allocate new strings before taking lock to minimize hold time - char *new_hosts = strdup(target_hosts); - char *new_ports = strdup(target_ports); - if (new_hosts == NULL || new_ports == NULL) - { - free(new_hosts); - free(new_ports); - return false; - } - - pthread_rwlock_wrlock(&rules_lock); - PROCESS_RULE *rule = rules_list; - while (rule != NULL) - { - if (rule->rule_id == rule_id) - { - strncpy(rule->process_name, process_name, MAX_PROCESS_NAME - 1); - rule->process_name[MAX_PROCESS_NAME - 1] = '\0'; - - free(rule->target_hosts); - rule->target_hosts = new_hosts; - - free(rule->target_ports); - rule->target_ports = new_ports; - - rule->protocol = protocol; - rule->action = action; - - update_has_active_rules(); - pthread_rwlock_unlock(&rules_lock); - log_message("updated rule id %u", rule_id); - return true; - } - rule = rule->next; - } - pthread_rwlock_unlock(&rules_lock); - - // Rule not found - free pre-allocated strings - free(new_hosts); - free(new_ports); - return false; -} - -bool ProxyBridge_SetProxyConfig(ProxyType type, const char* proxy_ip, uint16_t proxy_port, const char* username, const char* password) -{ - if (proxy_ip == NULL || proxy_ip[0] == '\0' || proxy_port == 0) - return false; - - g_proxy_ip_cached = resolve_hostname(proxy_ip); - if (g_proxy_ip_cached == 0) - return false; - - strncpy(g_proxy_host, proxy_ip, sizeof(g_proxy_host) - 1); - g_proxy_host[sizeof(g_proxy_host) - 1] = '\0'; - g_proxy_port = proxy_port; - g_proxy_type = (type == PROXY_TYPE_HTTP) ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; - - if (username != NULL) - { - strncpy(g_proxy_username, username, sizeof(g_proxy_username) - 1); - g_proxy_username[sizeof(g_proxy_username) - 1] = '\0'; - } - else - { - g_proxy_username[0] = '\0'; - } - - if (password != NULL) - { - strncpy(g_proxy_password, password, sizeof(g_proxy_password) - 1); - g_proxy_password[sizeof(g_proxy_password) - 1] = '\0'; - } - else - { - g_proxy_password[0] = '\0'; - } - - log_message("proxy configured %s %s:%d", type == PROXY_TYPE_HTTP ? "http" : "socks5", proxy_ip, proxy_port); - return true; -} - -void ProxyBridge_SetDnsViaProxy(bool enable) -{ - g_dns_via_proxy = enable; - log_message("dns via proxy %s", enable ? "enabled" : "disabled"); -} - -void ProxyBridge_SetLogCallback(LogCallback callback) -{ - g_log_callback = callback; -} - -void ProxyBridge_SetConnectionCallback(ConnectionCallback callback) -{ - g_connection_callback = callback; -} - -void ProxyBridge_SetTrafficLoggingEnabled(bool enable) -{ - g_traffic_logging_enabled = enable; -} - -void ProxyBridge_ClearConnectionLogs(void) -{ - clear_logged_connections(); -} - -bool ProxyBridge_Start(void) -{ - if (running) - return false; - - running = true; - g_current_process_id = getpid(); - - // Ignore SIGPIPE - send() on a closed socket must return EPIPE, not kill the process - signal(SIGPIPE, SIG_IGN); - - // Raise system socket buffer limits for high throughput (requires root) - // Default rmem_max/wmem_max is usually 208KB, far too small for >100Mbps - FILE *fp; - fp = fopen("/proc/sys/net/core/rmem_max", "w"); - if (fp) { fprintf(fp, "4194304"); fclose(fp); } // 4MB - fp = fopen("/proc/sys/net/core/wmem_max", "w"); - if (fp) { fprintf(fp, "4194304"); fclose(fp); } // 4MB - - if (pthread_create(&proxy_thread, NULL, local_proxy_server, NULL) != 0) - { - running = false; - return false; - } - - if (pthread_create(&cleanup_thread, NULL, cleanup_worker, NULL) != 0) - { - running = false; - pthread_cancel(proxy_thread); - pthread_join(proxy_thread, NULL); - proxy_thread = 0; - return false; - } - - // Start UDP relay server if SOCKS5 proxy - if (g_proxy_type == PROXY_TYPE_SOCKS5) - { - if (pthread_create(&udp_relay_thread, NULL, udp_relay_server, NULL) != 0) - { - log_message("failed to create UDP relay thread"); - } - } - - nfq_h = nfq_open(); - if (!nfq_h) - { - log_message("nfq_open failed"); - goto start_fail; - } - - if (nfq_unbind_pf(nfq_h, AF_INET) < 0) - { - log_message("nfq_unbind_pf failed"); - } - - if (nfq_bind_pf(nfq_h, AF_INET) < 0) - { - log_message("nfq_bind_pf failed"); - nfq_close(nfq_h); - nfq_h = NULL; - goto start_fail; - } - - nfq_qh = nfq_create_queue(nfq_h, 0, &packet_callback, NULL); - if (!nfq_qh) - { - log_message("nfq_create_queue failed"); - nfq_close(nfq_h); - nfq_h = NULL; - goto start_fail; - } - - if (nfq_set_mode(nfq_qh, NFQNL_COPY_PACKET, 0xffff) < 0) - { - log_message("nfq_set_mode failed"); - nfq_destroy_queue(nfq_qh); - nfq_qh = NULL; - nfq_close(nfq_h); - nfq_h = NULL; - goto start_fail; - } - - // Set larger queue length for better performance (16384 like Windows) - nfq_set_queue_maxlen(nfq_qh, 16384); - - // setup iptables rules for packet interception - USE MANGLE table so it runs BEFORE nat - log_message("setting up iptables rules"); - // mangle table runs before nat, so we can mark packets there - int ret1 = run_iptables_cmd("-t", "mangle", "-A", "OUTPUT", "-p", "tcp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL); - int ret2 = run_iptables_cmd("-t", "mangle", "-A", "OUTPUT", "-p", "udp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL); - - if (ret1 != 0 || ret2 != 0) { - log_message("failed to add iptables rules ret1=%d ret2=%d", ret1, ret2); - } else { - log_message("iptables nfqueue rules added successfully"); - } - - // setup nat redirect for marked packets - int ret3 = run_iptables_cmd("-t", "nat", "-A", "OUTPUT", "-p", "tcp", "-m", "mark", "--mark", "1", "-j", "REDIRECT", "--to-port", "34010"); - int ret4 = run_iptables_cmd("-t", "nat", "-A", "OUTPUT", "-p", "udp", "-m", "mark", "--mark", "2", "-j", "REDIRECT", "--to-port", "34011"); - if (ret3 != 0 || ret4 != 0) { - log_message("failed to add nat redirect rules"); - } - - (void)ret3; - (void)ret4; - - for (int i = 0; i < NUM_PACKET_THREADS; i++) - { - if (pthread_create(&packet_thread[i], NULL, packet_processor, NULL) != 0) - { - log_message("failed to create packet thread %d", i); - } - } - - log_message("proxybridge started"); - return true; - -start_fail: - running = false; - if (proxy_thread != 0) { pthread_cancel(proxy_thread); pthread_join(proxy_thread, NULL); proxy_thread = 0; } - if (cleanup_thread != 0) { pthread_cancel(cleanup_thread); pthread_join(cleanup_thread, NULL); cleanup_thread = 0; } - if (udp_relay_thread != 0) { pthread_cancel(udp_relay_thread); pthread_join(udp_relay_thread, NULL); udp_relay_thread = 0; } - return false; -} - -bool ProxyBridge_Stop(void) -{ - if (!running) - return false; - - running = false; - - // cleanup iptables - int ret1 = run_iptables_cmd("-t", "mangle", "-D", "OUTPUT", "-p", "tcp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL); - int ret2 = run_iptables_cmd("-t", "mangle", "-D", "OUTPUT", "-p", "udp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL); - int ret3 = run_iptables_cmd("-t", "nat", "-D", "OUTPUT", "-p", "tcp", "-m", "mark", "--mark", "1", "-j", "REDIRECT", "--to-port", "34010"); - int ret4 = run_iptables_cmd("-t", "nat", "-D", "OUTPUT", "-p", "udp", "-m", "mark", "--mark", "2", "-j", "REDIRECT", "--to-port", "34011"); - (void)ret1; - (void)ret2; - (void)ret3; - (void)ret4; - - for (int i = 0; i < NUM_PACKET_THREADS; i++) - { - if (packet_thread[i] != 0) - { - pthread_cancel(packet_thread[i]); - pthread_join(packet_thread[i], NULL); - packet_thread[i] = 0; - } - } - - if (nfq_qh) - { - nfq_destroy_queue(nfq_qh); - nfq_qh = NULL; - } - - if (nfq_h) - { - nfq_close(nfq_h); - nfq_h = NULL; - } - - if (proxy_thread != 0) - { - pthread_cancel(proxy_thread); - pthread_join(proxy_thread, NULL); - proxy_thread = 0; - } - - if (udp_relay_thread != 0) - { - pthread_cancel(udp_relay_thread); - pthread_join(udp_relay_thread, NULL); - udp_relay_thread = 0; - } - - if (cleanup_thread != 0) - { - pthread_cancel(cleanup_thread); - pthread_join(cleanup_thread, NULL); - cleanup_thread = 0; - } - - // Free all connections in hash table - pthread_rwlock_wrlock(&conn_lock); - for (int i = 0; i < CONNECTION_HASH_SIZE; i++) - { - while (connection_hash_table[i] != NULL) - { - CONNECTION_INFO *to_free = connection_hash_table[i]; - connection_hash_table[i] = connection_hash_table[i]->next; - free(to_free); - } - } - pthread_rwlock_unlock(&conn_lock); - - // Free all rules - pthread_rwlock_wrlock(&rules_lock); - while (rules_list != NULL) - { - PROCESS_RULE *to_free = rules_list; - rules_list = rules_list->next; - free(to_free->target_hosts); - free(to_free->target_ports); - free(to_free); - } - g_has_active_rules = false; - g_next_rule_id = 1; - pthread_rwlock_unlock(&rules_lock); - - clear_logged_connections(); - clear_pid_cache(); - - log_message("proxybridge stopped"); - return true; -} - -int ProxyBridge_TestConnection(const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size) -{ - int test_sock = -1; - struct sockaddr_in proxy_addr; - uint32_t target_ip; - int ret = -1; - char temp_buffer[512]; - - if (g_proxy_host[0] == '\0' || g_proxy_port == 0) - { - snprintf(result_buffer, buffer_size, "error no proxy configured"); - return -1; - } - - if (target_host == NULL || target_host[0] == '\0') - { - snprintf(result_buffer, buffer_size, "error invalid target host"); - return -1; - } - - snprintf(temp_buffer, sizeof(temp_buffer), "testing connection to %s:%d via %s proxy %s:%d\n", - target_host, target_port, - g_proxy_type == PROXY_TYPE_HTTP ? "http" : "socks5", - g_proxy_host, g_proxy_port); - strncpy(result_buffer, temp_buffer, buffer_size - 1); - result_buffer[buffer_size - 1] = '\0'; - - struct addrinfo hints, *res; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - int gai_ret = getaddrinfo(target_host, NULL, &hints, &res); - if (gai_ret != 0) - { - snprintf(temp_buffer, sizeof(temp_buffer), "error failed to resolve hostname %s: %s\n", - target_host, gai_strerror(gai_ret)); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - return -1; - } - target_ip = ((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr; - freeaddrinfo(res); - - snprintf(temp_buffer, sizeof(temp_buffer), "resolved %s to %d.%d.%d.%d\n", - target_host, - (target_ip >> 0) & 0xFF, (target_ip >> 8) & 0xFF, - (target_ip >> 16) & 0xFF, (target_ip >> 24) & 0xFF); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - - test_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (test_sock < 0) - { - snprintf(temp_buffer, sizeof(temp_buffer), "error socket creation failed\n"); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - return -1; - } - - configure_tcp_socket(test_sock, 65536, 10000); - - memset(&proxy_addr, 0, sizeof(proxy_addr)); - proxy_addr.sin_family = AF_INET; - proxy_addr.sin_addr.s_addr = resolve_hostname(g_proxy_host); - proxy_addr.sin_port = htons(g_proxy_port); - - snprintf(temp_buffer, sizeof(temp_buffer), "connecting to proxy %s:%d\n", g_proxy_host, g_proxy_port); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - - if (connect(test_sock, (struct sockaddr*)&proxy_addr, sizeof(proxy_addr)) < 0) - { - snprintf(temp_buffer, sizeof(temp_buffer), "error failed to connect to proxy\n"); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - close(test_sock); - return -1; - } - - strncat(result_buffer, "connected to proxy server\n", buffer_size - strlen(result_buffer) - 1); - - if (g_proxy_type == PROXY_TYPE_SOCKS5) - { - if (socks5_connect(test_sock, target_ip, target_port) != 0) - { - snprintf(temp_buffer, sizeof(temp_buffer), "error socks5 handshake failed\n"); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - close(test_sock); - return -1; - } - strncat(result_buffer, "socks5 handshake successful\n", buffer_size - strlen(result_buffer) - 1); - } - else - { - if (http_connect(test_sock, target_ip, target_port) != 0) - { - snprintf(temp_buffer, sizeof(temp_buffer), "error http connect failed\n"); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - close(test_sock); - return -1; - } - strncat(result_buffer, "http connect successful\n", buffer_size - strlen(result_buffer) - 1); - } - - char http_request[512]; - snprintf(http_request, sizeof(http_request), - "GET / HTTP/1.1\r\n" - "Host: %s\r\n" - "Connection: close\r\n" - "User-Agent: ProxyBridge/1.0\r\n" - "\r\n", target_host); - - if (send_all(test_sock, http_request, strlen(http_request)) < 0) - { - snprintf(temp_buffer, sizeof(temp_buffer), "error failed to send test request\n"); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - close(test_sock); - return -1; - } - - strncat(result_buffer, "sent http get request\n", buffer_size - strlen(result_buffer) - 1); - char response[1024]; - ssize_t bytes_received = recv(test_sock, response, sizeof(response) - 1, 0); - if (bytes_received > 0) - { - response[bytes_received] = '\0'; - - if (strstr(response, "HTTP/") != NULL) - { - char* status_line = strstr(response, "HTTP/"); - int status_code = 0; - if (status_line != NULL) - { - sscanf(status_line, "HTTP/%*s %d", &status_code); - } - - snprintf(temp_buffer, sizeof(temp_buffer), "success received http %d response %ld bytes\n", status_code, (long)bytes_received); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - ret = 0; - } - else - { - snprintf(temp_buffer, sizeof(temp_buffer), "error received data but not valid http response\n"); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - ret = -1; - } - } - else - { - snprintf(temp_buffer, sizeof(temp_buffer), "error failed to receive response\n"); - strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1); - ret = -1; - } - - close(test_sock); - - if (ret == 0) - { - strncat(result_buffer, "\nproxy connection test passed\n", buffer_size - strlen(result_buffer) - 1); - } - else - { - strncat(result_buffer, "\nproxy connection test failed\n", buffer_size - strlen(result_buffer) - 1); - } - - return ret; -} - -// Library destructor - automatically cleanup when library is unloaded -__attribute__((destructor)) -static void library_cleanup(void) -{ - if (running) - { - log_message("library unloading - cleaning up automatically"); - ProxyBridge_Stop(); - } - else - { - // Even if not running, ensure iptables rules are removed - // This handles cases where the app crashed before calling Stop - run_iptables_cmd("-t", "mangle", "-D", "OUTPUT", "-p", "tcp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL); - run_iptables_cmd("-t", "mangle", "-D", "OUTPUT", "-p", "udp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL); - run_iptables_cmd("-t", "nat", "-D", "OUTPUT", "-p", "tcp", "-m", "mark", "--mark", "1", "-j", "REDIRECT", "--to-port", "34010"); - run_iptables_cmd("-t", "nat", "-D", "OUTPUT", "-p", "udp", "-m", "mark", "--mark", "2", "-j", "REDIRECT", "--to-port", "34011"); - } + return ProxyBridge_TestProxy(0, host, port, buf, n); } diff --git a/Linux/src/ProxyBridge.h b/Linux/src/ProxyBridge.h index bd80583b..dc8b8637 100644 --- a/Linux/src/ProxyBridge.h +++ b/Linux/src/ProxyBridge.h @@ -1,8 +1,9 @@ #ifndef PROXYBRIDGE_H #define PROXYBRIDGE_H -#define PROXYBRIDGE_VERSION "3.2.0" +#define PROXYBRIDGE_VERSION "4.0.0" +#include #include #include @@ -30,20 +31,52 @@ typedef enum { RULE_PROTOCOL_BOTH = 2 } RuleProtocol; -uint32_t ProxyBridge_AddRule(const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action); -bool ProxyBridge_EnableRule(uint32_t rule_id); -bool ProxyBridge_DisableRule(uint32_t rule_id); -bool ProxyBridge_DeleteRule(uint32_t rule_id); -bool ProxyBridge_EditRule(uint32_t rule_id, const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action); +/* ---- Proxy configurations ------------------------------------------------------------------------------------------- + * Any number of proxies can be registered. Ids are non-zero and stable until the proxy is deleted. + * Returns 0 on failure (the reason is written to err, if given). */ +uint32_t ProxyBridge_AddProxy(const char* name, ProxyType type, const char* host, uint16_t port, + const char* username, const char* password, char* err, size_t err_size); +bool ProxyBridge_EditProxy(uint32_t proxy_id, const char* name, ProxyType type, const char* host, uint16_t port, + const char* username, const char* password, char* err, size_t err_size); +bool ProxyBridge_DeleteProxy(uint32_t proxy_id); /* rules that used it fall back to the default proxy */ +void ProxyBridge_ClearProxies(void); +/* Which proxy a rule with proxy_id == 0 uses. 0 = the first proxy added. */ +void ProxyBridge_SetDefaultProxy(uint32_t proxy_id); + +/* ---- Rules ---------------------------------------------------------------------------------------------------------- + * Rules are evaluated in list order, first match wins. process_name: ';' or ',' separated globs matched against the + * executable's full path and file name ("*" or empty = any). target_hosts: ';' or ',' separated IPv4/IPv6 addresses, + * CIDR (10.0.0.0/8), a-b ranges, octet wildcards (192.168.*.*) or "*" ("" = any). target_ports: list and ranges + * ("80;443;8000-9000", "" or "*" = any). + * proxy_id applies when action == RULE_ACTION_PROXY; 0 = default proxy. + * AddRule returns the new rule id, or 0 on validation failure (reason in err). */ +uint32_t ProxyBridge_AddRule(const char* process_name, const char* target_hosts, const char* target_ports, + RuleProtocol protocol, RuleAction action, uint32_t proxy_id, char* err, size_t err_size); +bool ProxyBridge_EditRule(uint32_t rule_id, const char* process_name, const char* target_hosts, const char* target_ports, + RuleProtocol protocol, RuleAction action, uint32_t proxy_id, char* err, size_t err_size); +bool ProxyBridge_EnableRule(uint32_t rule_id); +bool ProxyBridge_DisableRule(uint32_t rule_id); +bool ProxyBridge_DeleteRule(uint32_t rule_id); +bool ProxyBridge_MoveRule(uint32_t rule_id, int new_index); /* reorder: priority = position */ +void ProxyBridge_ClearRules(void); +/* Validates a rule without adding it; returns true if valid, otherwise writes the reason to err. */ +bool ProxyBridge_ValidateRule(const char* process_name, const char* target_hosts, const char* target_ports, + char* err, size_t err_size); + +/* ---- Legacy single-proxy helper ------------------------------------------------------------------------------------- + * Sets (creating on first use) the default proxy. Equivalent to Add/EditProxy on the "default" entry. */ bool ProxyBridge_SetProxyConfig(ProxyType type, const char* proxy_ip, uint16_t proxy_port, const char* username, const char* password); + +/* ---- Runtime --------------------------------------------------------------------------------------------------------- */ void ProxyBridge_SetDnsViaProxy(bool enable); void ProxyBridge_SetLogCallback(LogCallback callback); void ProxyBridge_SetConnectionCallback(ConnectionCallback callback); void ProxyBridge_SetTrafficLoggingEnabled(bool enable); void ProxyBridge_ClearConnectionLogs(void); -bool ProxyBridge_Start(void); -bool ProxyBridge_Stop(void); -int ProxyBridge_TestConnection(const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); +bool ProxyBridge_Start(void); /* needs root (CAP_BPF/CAP_SYS_ADMIN) and a BTF kernel >= 5.8; returns false and logs why on failure */ +bool ProxyBridge_Stop(void); /* keeps rules and proxies; Start() may be called again */ +int ProxyBridge_TestConnection(const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); +int ProxyBridge_TestProxy(uint32_t proxy_id, const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); #ifdef __cplusplus } diff --git a/Linux/src/bpf/pb_bpf_shared.h b/Linux/src/bpf/pb_bpf_shared.h new file mode 100644 index 00000000..bb403e2d --- /dev/null +++ b/Linux/src/bpf/pb_bpf_shared.h @@ -0,0 +1,75 @@ +// Types shared between the BPF object and userspace (pb_bpf.c). Plain C, no kernel headers needed. +#ifndef PB_BPF_SHARED_H +#define PB_BPF_SHARED_H + +#ifdef __BPF__ +typedef __u8 pb_u8; typedef __u16 pb_u16; typedef __u32 pb_u32; typedef __u64 pb_u64; +#else +#include +typedef uint8_t pb_u8; typedef uint16_t pb_u16; typedef uint32_t pb_u32; typedef uint64_t pb_u64; +#endif + +#define PB_MAX_RULES 512 +#define PB_MAX_CLASSES 2048 +#define PB_RULES_PER_CLASS 16 +#define PB_BPF_MAX_V4 4 +#define PB_BPF_MAX_PORT 4 + +// cfg array slots +#define PB_CFG_ENABLED 0 // 1 when at least one rule is loaded +#define PB_CFG_SELF_TGID 1 // process that hosts the relay (never redirected) +#define PB_CFG_LOG 2 // 1 = emit events +#define PB_CFG_TCP_PORT 3 +#define PB_CFG_UDP_PORT 4 +#define PB_CFG_DNS_VIA_PROXY 5 +#define PB_CFG_DNS_TAP 6 // 1 when a domain rule exists: port-53 flows of covered processes go through the relay +#define PB_CFG_NETNS_LO 7 // cookie of the network namespace the relay lives in; sockets in other namespaces are ignored +#define PB_CFG_NETNS_HI 8 +#define PB_CFG_SLOTS 12 + +// brule flags +#define PB_RF_ANY_HOST 0x01 +#define PB_RF_ANY_PORT 0x02 +#define PB_RF_HOST_COMPLEX 0x04 // host pattern not representable: "maybe" for every destination +#define PB_RF_PORT_COMPLEX 0x08 // more port ranges than fit: "maybe" when the ports in the table do not match +#define PB_RF_HAS_V6 0x10 // rule has IPv6 patterns: "maybe" for IPv6 destinations +#define PB_RF_DOMAINS 0x40 // host list also contains domain patterns: answered by the dom_hit map (filled by the DNS tap) +#define PB_RF_UDP_DIRECT 0x20 // PROXY rule whose proxy cannot carry UDP (HTTP): UDP flows go direct + +#define PB_CLASS_DECIDE 0xffffffffu // pb_bclass.n value: always take the slow path for this binary + +struct pb_exe_key { pb_u32 dev; pb_u32 pad; pb_u64 ino; }; + +struct pb_brule { + pb_u16 proxy_id; + pb_u8 action; // 0 proxy, 1 direct, 2 block + pb_u8 proto_mask; // 1 tcp, 2 udp + pb_u8 flags; + pb_u8 n_v4; + pb_u8 n_port; + pb_u8 pad; + struct { pb_u32 lo, hi; } v4[PB_BPF_MAX_V4]; // host order + struct { pb_u16 lo, hi; } port[PB_BPF_MAX_PORT]; // host order +}; + +struct pb_dom_key { pb_u32 rule; pb_u32 ip; }; // ip in host order + +struct pb_bclass { + pb_u32 n; + pb_u32 tap; // 1: this binary is covered by a domain rule, so its DNS must be observed + pb_u32 rule[PB_RULES_PER_CLASS]; +}; + +struct pb_event { + pb_u32 pid; + pb_u8 comm[16]; + pb_u8 addr[16]; + pb_u16 port; + pb_u16 proxy_id; + pb_u8 family; + pb_u8 proto; + pb_u8 action; // PB_ACT_* + pb_u8 pad; +}; + +#endif diff --git a/Linux/src/bpf/proxybridge.bpf.c b/Linux/src/bpf/proxybridge.bpf.c new file mode 100644 index 00000000..839f57c2 --- /dev/null +++ b/Linux/src/bpf/proxybridge.bpf.c @@ -0,0 +1,433 @@ +// ProxyBridge eBPF datapath: cgroup socket-address hooks that redirect selected processes' connect()/sendmsg() to the +// userspace relay and remember the original destination. +#include +#include +#include +#include +#include "pb_bpf_shared.h" + +// Minimal kernel type stubs (CO-RE relocated against the running kernel's BTF; avoids shipping a 160k-line vmlinux.h). +#pragma clang attribute push(__attribute__((preserve_access_index)), apply_to = record) +struct super_block { __u32 s_dev; }; +struct inode { struct super_block *i_sb; unsigned long i_ino; }; +struct file { struct inode *f_inode; }; +struct mm_struct { struct file *exe_file; }; +struct task_struct { struct mm_struct *mm; }; +#pragma clang attribute pop + +#define IPPROTO_TCP 6 +#define IPPROTO_UDP 17 +#define AF_INET 2 +#define AF_INET6 10 + +#define ACT_PROXY 0 +#define ACT_DIRECT 1 +#define ACT_BLOCK 2 +#define ACT_DECIDE 3 + +#define M_NO 0 +#define M_YES 1 +#define M_MAYBE 2 + +struct pb_orig { + __u8 addr[16]; + __u16 port; + __u16 proxy_id; + __u32 pid; + __u8 family, proto, action, pad; + __u32 exe_dev, exe_pad; + __u64 exe_ino; +}; + +struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, PB_CFG_SLOTS); + __type(key, __u32); __type(value, __u32); } cfg SEC(".maps"); +struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 16384); + __type(key, struct pb_exe_key); __type(value, __u32); } exe2class SEC(".maps"); +struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, PB_MAX_CLASSES); + __type(key, __u32); __type(value, struct pb_bclass); } classes SEC(".maps"); +struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, PB_MAX_RULES); + __type(key, __u32); __type(value, struct pb_brule); } brules SEC(".maps"); +struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 32768); + __type(key, __u64); __type(value, struct pb_orig); } by_cookie SEC(".maps"); +struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 32768); + __type(key, __u32); __type(value, struct pb_orig); } by_port SEC(".maps"); +struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 32768); + __type(key, __u32); __type(value, struct pb_orig); } flows SEC(".maps"); +struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 65536); + __type(key, struct pb_dom_key); __type(value, __u8); } dom_hit SEC(".maps"); // (rule, ip) pairs whose DNS name matches the rule +struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } events SEC(".maps"); +struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 8); + __type(key, __u32); __type(value, __u64); } stats SEC(".maps"); // 0 redirected 1 blocked 2 decide 3 evt dropped + +static __always_inline void stat_inc(__u32 k) +{ + __u64 *v = bpf_map_lookup_elem(&stats, &k); + if (v) (*v)++; +} + +static __always_inline __u32 cfg_get(__u32 k) +{ + __u32 *v = bpf_map_lookup_elem(&cfg, &k); + return v ? *v : 0; +} + +// Containers/other network namespaces have their own loopback: redirecting them to our relay would break them. +static __always_inline int foreign_netns(void *ctx) +{ + __u64 want = ((__u64)cfg_get(PB_CFG_NETNS_HI) << 32) | cfg_get(PB_CFG_NETNS_LO); + return want && bpf_get_netns_cookie(ctx) != want; +} + +// --------------------------------------------------------------------------------------------------------------------- +// rule evaluation +// --------------------------------------------------------------------------------------------------------------------- +static __always_inline int rule_match(const struct pb_brule *r, __u32 ri, __u8 fam, __u32 ip4_host, __u16 port) +{ + int port_res = M_YES; + if (!(r->flags & PB_RF_ANY_PORT)) { + port_res = M_NO; + #pragma unroll + for (int i = 0; i < PB_BPF_MAX_PORT; i++) { + if (i >= r->n_port) break; + if (port >= r->port[i].lo && port <= r->port[i].hi) { port_res = M_YES; break; } + } + if (port_res == M_NO && (r->flags & PB_RF_PORT_COMPLEX)) port_res = M_MAYBE; + if (port_res == M_NO) return M_NO; + } + int host_res; + if (r->flags & PB_RF_ANY_HOST) host_res = M_YES; + else if (r->flags & PB_RF_HOST_COMPLEX) host_res = M_MAYBE; + else if (fam == AF_INET6) host_res = (r->flags & PB_RF_HAS_V6) ? M_MAYBE : M_NO; + else { + host_res = M_NO; + #pragma unroll + for (int i = 0; i < PB_BPF_MAX_V4; i++) { + if (i >= r->n_v4) break; + if (ip4_host >= r->v4[i].lo && ip4_host <= r->v4[i].hi) { host_res = M_YES; break; } + } + if (host_res == M_NO && (r->flags & PB_RF_DOMAINS)) { + struct pb_dom_key dk = { .rule = ri, .ip = ip4_host }; + if (bpf_map_lookup_elem(&dom_hit, &dk)) host_res = M_YES; + } + } + if (host_res == M_NO) return M_NO; + return (host_res == M_MAYBE || port_res == M_MAYBE) ? M_MAYBE : M_YES; +} + +static __always_inline int implicit_direct(__u8 fam, const __u8 *a, __u16 port, __u8 proto) +{ + if (fam == AF_INET) { + if (a[12] == 127 || a[12] == 0 || a[12] >= 224) return 1; + } else { + if (a[0] == 0xff) return 1; + if (a[0] == 0xfe && (a[1] & 0xc0) == 0x80) return 1; + int lo = 1; + #pragma unroll + for (int i = 0; i < 15; i++) if (a[i]) lo = 0; + if (lo && a[15] == 1) return 1; + } + if (proto == IPPROTO_UDP && (port == 67 || port == 68)) return 1; + if (port == 53 && !cfg_get(PB_CFG_DNS_VIA_PROXY)) return 1; + return 0; +} + +// Returns an ACT_*; for ACT_PROXY *proxy_id is set. +static __always_inline int decide(struct pb_orig *o, __u16 *proxy_id) +{ + if (!cfg_get(PB_CFG_ENABLED)) return ACT_DIRECT; + if (o->pid == cfg_get(PB_CFG_SELF_TGID)) return ACT_DIRECT; + int tap = o->port == 53 && cfg_get(PB_CFG_DNS_TAP); + if (!tap && implicit_direct(o->family, o->addr, o->port, o->proto)) return ACT_DIRECT; + + struct task_struct *t = bpf_get_current_task_btf(); + struct mm_struct *mm = t->mm; + if (!mm) return ACT_DIRECT; // kernel thread + struct pb_exe_key key = {}; + key.dev = mm->exe_file->f_inode->i_sb->s_dev; + key.ino = mm->exe_file->f_inode->i_ino; + o->exe_dev = key.dev; o->exe_ino = key.ino; // lets userspace learn the exact kernel-side key + if (!key.ino) return ACT_DECIDE; + + __u32 *cid = bpf_map_lookup_elem(&exe2class, &key); + if (!cid) return ACT_DECIDE; // binary not seen yet: relay classifies it + struct pb_bclass *c = bpf_map_lookup_elem(&classes, cid); + if (!c || c->n > PB_RULES_PER_CLASS) return ACT_DECIDE; + if (tap) { + // a domain rule covers this binary: its DNS answers must pass through the relay so names can be learned + if (c->tap) return ACT_DECIDE; + if (implicit_direct(o->family, o->addr, o->port, o->proto)) return ACT_DIRECT; + } + + __u32 ip4 = (o->family == AF_INET) ? bpf_ntohl(*(const __u32 *)(o->addr + 12)) : 0; + __u8 pbit = o->proto == IPPROTO_TCP ? 1 : 2; + #pragma unroll + for (int i = 0; i < PB_RULES_PER_CLASS; i++) { + if (i >= c->n) break; + __u32 ri = c->rule[i]; + struct pb_brule *r = bpf_map_lookup_elem(&brules, &ri); + if (!r) return ACT_DECIDE; + if (!(r->proto_mask & pbit)) continue; + int m = rule_match(r, ri, o->family, ip4, o->port); + if (m == M_NO) continue; + if (m == M_MAYBE) return ACT_DECIDE; + if (r->action == ACT_PROXY) { + if (o->proto == IPPROTO_UDP && (r->flags & PB_RF_UDP_DIRECT)) return ACT_DIRECT; + *proxy_id = r->proxy_id; + } + return r->action; + } + return ACT_DIRECT; +} + +static __always_inline void emit(const struct pb_orig *o, int action, __u16 proxy_id) +{ + if (!cfg_get(PB_CFG_LOG)) return; + struct pb_event *e = bpf_ringbuf_reserve(&events, sizeof *e, 0); + if (!e) { stat_inc(3); return; } + e->pid = o->pid; + bpf_get_current_comm(e->comm, sizeof e->comm); + __builtin_memcpy(e->addr, o->addr, 16); + e->port = o->port; e->proxy_id = proxy_id; + e->family = o->family; e->proto = o->proto; e->action = action; e->pad = 0; + bpf_ringbuf_submit(e, 0); +} + +// --------------------------------------------------------------------------------------------------------------------- +// flow handling +// --------------------------------------------------------------------------------------------------------------------- +static __always_inline void set_mapped_v4(__u32 *ip6, __u32 ip4_be) +{ + ip6[0] = 0; ip6[1] = 0; ip6[2] = bpf_htonl(0x0000ffff); ip6[3] = ip4_be; +} + +// Fills o from a sockaddr as seen in the hook. For v6 sockets a v4-mapped address is an IPv4 destination. +static __always_inline void orig_from_v4(struct pb_orig *o, __u32 ip_be, __u16 port_be, __u8 proto) +{ + __builtin_memset(o, 0, sizeof *o); + o->addr[10] = 0xff; o->addr[11] = 0xff; + __builtin_memcpy(o->addr + 12, &ip_be, 4); + o->port = bpf_ntohs(port_be); + o->family = AF_INET; o->proto = proto; + o->pid = bpf_get_current_pid_tgid() >> 32; +} + +static __always_inline void orig_from_v6(struct pb_orig *o, const __u32 ip6[4], __u16 port_be, __u8 proto) +{ + __builtin_memset(o, 0, sizeof *o); + __builtin_memcpy(o->addr, ip6, 16); + o->port = bpf_ntohs(port_be); + o->proto = proto; + o->pid = bpf_get_current_pid_tgid() >> 32; + if (ip6[0] == 0 && ip6[1] == 0 && ip6[2] == bpf_htonl(0x0000ffff)) o->family = AF_INET; + else o->family = AF_INET6; +} + +// Deterministic loopback flow address (127.1-255.x.y) for a UDP destination; collisions are probed linearly. +static __always_inline __u32 udp_flow_addr(struct pb_orig *o) +{ + const __u32 *w = (const __u32 *)o->addr; + __u32 h = w[0] * 2654435761u ^ w[1] * 40503u ^ w[2] * 2246822519u ^ w[3] * 3266489917u; + h ^= ((__u32)o->port << 16) | o->proxy_id; + h ^= (__u32)o->action * 668265263u; + if (o->action == ACT_DECIDE) h ^= o->pid * 374761393u; + h ^= h >> 15; h *= 2246822519u; h ^= h >> 13; + __u32 addr = 0; + for (int i = 0; i < 8; i++) { + addr = bpf_htonl(0x7f000000 | (((h + i) & 0x00ffffff) | 0x00010000)); + struct pb_orig *ex = bpf_map_lookup_elem(&flows, &addr); + if (!ex) { bpf_map_update_elem(&flows, &addr, o, BPF_NOEXIST); break; } + if (__builtin_memcmp(ex->addr, o->addr, 16) == 0 && ex->port == o->port && ex->proxy_id == o->proxy_id && + ex->action == o->action && (o->action != ACT_DECIDE || ex->pid == o->pid)) break; + } + return addr; +} + +// 0 = leave the socket call untouched, 1 = it was redirected, -1 = block +static __always_inline int route(struct pb_orig *o, __u16 *proxy_id_out) +{ + __u16 pid16 = 0; + int act = decide(o, &pid16); + switch (act) { + case ACT_DIRECT: + emit(o, ACT_DIRECT, 0); + return 0; + case ACT_BLOCK: + emit(o, ACT_BLOCK, 0); + stat_inc(1); + return -1; + case ACT_PROXY: + o->action = ACT_PROXY; o->proxy_id = pid16; + emit(o, ACT_PROXY, pid16); + stat_inc(0); + break; + default: + o->action = ACT_DECIDE; o->proxy_id = 0; + stat_inc(2); + break; + } + *proxy_id_out = o->proxy_id; + return 1; +} + +// ---- TCP / connected UDP: connect4 / connect6 ------------------------------------------------------------------------ +SEC("cgroup/connect4") +int pb_connect4(struct bpf_sock_addr *ctx) +{ + if (foreign_netns(ctx)) return 1; + if (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP) return 1; + struct pb_orig o; __u16 pid; + orig_from_v4(&o, ctx->user_ip4, ctx->user_port, ctx->protocol); + int r = route(&o, &pid); + if (r < 0) return 0; + if (r == 0) return 1; + if (ctx->protocol == IPPROTO_TCP) { + __u64 ck = bpf_get_socket_cookie(ctx); + bpf_map_update_elem(&by_cookie, &ck, &o, BPF_ANY); + ctx->user_ip4 = bpf_htonl(0x7f000001); + ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_TCP_PORT)); + } else { + ctx->user_ip4 = udp_flow_addr(&o); + ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT)); + } + return 1; +} + +SEC("cgroup/connect6") +int pb_connect6(struct bpf_sock_addr *ctx) +{ + if (foreign_netns(ctx)) return 1; + if (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP) return 1; + __u32 ip6[4] = { ctx->user_ip6[0], ctx->user_ip6[1], ctx->user_ip6[2], ctx->user_ip6[3] }; + struct pb_orig o; __u16 pid; + orig_from_v6(&o, ip6, ctx->user_port, ctx->protocol); + int r = route(&o, &pid); + if (r < 0) return 0; + if (r == 0) return 1; + if (ctx->protocol == IPPROTO_TCP) { + __u64 ck = bpf_get_socket_cookie(ctx); + bpf_map_update_elem(&by_cookie, &ck, &o, BPF_ANY); + set_mapped_v4(ctx->user_ip6, bpf_htonl(0x7f000001)); + ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_TCP_PORT)); + } else { + set_mapped_v4(ctx->user_ip6, udp_flow_addr(&o)); + ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT)); + } + return 1; +} + +// ---- unconnected UDP -------------------------------------------------------------------------------------------------- +SEC("cgroup/sendmsg4") +int pb_sendmsg4(struct bpf_sock_addr *ctx) +{ + if (foreign_netns(ctx)) return 1; + if (ctx->protocol != IPPROTO_UDP) return 1; + struct pb_orig o; __u16 pid; + orig_from_v4(&o, ctx->user_ip4, ctx->user_port, IPPROTO_UDP); + int r = route(&o, &pid); + if (r < 0) return 0; + if (r == 0) return 1; + ctx->user_ip4 = udp_flow_addr(&o); + ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT)); + return 1; +} + +SEC("cgroup/sendmsg6") +int pb_sendmsg6(struct bpf_sock_addr *ctx) +{ + if (foreign_netns(ctx)) return 1; + if (ctx->protocol != IPPROTO_UDP) return 1; + __u32 ip6[4] = { ctx->user_ip6[0], ctx->user_ip6[1], ctx->user_ip6[2], ctx->user_ip6[3] }; + struct pb_orig o; __u16 pid; + orig_from_v6(&o, ip6, ctx->user_port, IPPROTO_UDP); + int r = route(&o, &pid); + if (r < 0) return 0; + if (r == 0) return 1; + // The kernel (udpv6_sendmsg) refuses a sendmsg6 program that rewrites a native IPv6 destination to a v4-mapped address, + // and loopback has only ::1 so a flow id cannot be encoded in the address. A flow that must be proxied therefore fails + // closed (EPERM; applications fall back to IPv4/TCP). Flows that merely need a userspace decision pass untouched. + if (o.action == ACT_PROXY) { stat_inc(1); return 0; } + return 1; +} + +// Replies arrive from the flow address; show the application the real peer. +SEC("cgroup/recvmsg4") +int pb_recvmsg4(struct bpf_sock_addr *ctx) +{ + if (ctx->protocol != IPPROTO_UDP || ctx->user_port != bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT))) return 1; + __u32 key = ctx->user_ip4; + struct pb_orig *o = bpf_map_lookup_elem(&flows, &key); + if (!o) return 1; + if (o->family == AF_INET) { + ctx->user_ip4 = *(__u32 *)(o->addr + 12); + ctx->user_port = bpf_htons(o->port); + } + return 1; +} + +SEC("cgroup/recvmsg6") +int pb_recvmsg6(struct bpf_sock_addr *ctx) +{ + if (ctx->protocol != IPPROTO_UDP || ctx->user_port != bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT))) return 1; + if (ctx->user_ip6[0] || ctx->user_ip6[1] || ctx->user_ip6[2] != bpf_htonl(0x0000ffff)) return 1; + __u32 key = ctx->user_ip6[3]; + struct pb_orig *o = bpf_map_lookup_elem(&flows, &key); + if (!o) return 1; + const __u32 *w = (const __u32 *)o->addr; + ctx->user_ip6[0] = w[0]; ctx->user_ip6[1] = w[1]; ctx->user_ip6[2] = w[2]; ctx->user_ip6[3] = w[3]; + ctx->user_port = bpf_htons(o->port); + return 1; +} + +// getpeername() on a redirected socket shows the original destination +SEC("cgroup/getpeername4") +int pb_getpeername4(struct bpf_sock_addr *ctx) +{ + struct pb_orig *o; + if (ctx->protocol == IPPROTO_UDP) { + __u32 key = ctx->user_ip4; + o = bpf_map_lookup_elem(&flows, &key); + } else if (ctx->protocol == IPPROTO_TCP) { + __u64 ck = bpf_get_socket_cookie(ctx); + o = bpf_map_lookup_elem(&by_cookie, &ck); + } else return 1; + if (o && o->family == AF_INET) { + ctx->user_ip4 = *(__u32 *)(o->addr + 12); + ctx->user_port = bpf_htons(o->port); + } + return 1; +} + +SEC("cgroup/getpeername6") +int pb_getpeername6(struct bpf_sock_addr *ctx) +{ + struct pb_orig *o; + if (ctx->protocol == IPPROTO_UDP) { + if (ctx->user_ip6[0] || ctx->user_ip6[1] || ctx->user_ip6[2] != bpf_htonl(0x0000ffff)) return 1; + __u32 key = ctx->user_ip6[3]; + o = bpf_map_lookup_elem(&flows, &key); + } else if (ctx->protocol == IPPROTO_TCP) { + __u64 ck = bpf_get_socket_cookie(ctx); + o = bpf_map_lookup_elem(&by_cookie, &ck); + } else return 1; + if (o) { + const __u32 *w = (const __u32 *)o->addr; + ctx->user_ip6[0] = w[0]; ctx->user_ip6[1] = w[1]; ctx->user_ip6[2] = w[2]; ctx->user_ip6[3] = w[3]; + ctx->user_port = bpf_htons(o->port); + } + return 1; +} + +// Hand the original destination to the relay, keyed by the client's local port. +SEC("sockops") +int pb_sockops(struct bpf_sock_ops *s) +{ + if (s->op != BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) return 1; + __u64 ck = bpf_get_socket_cookie(s); + struct pb_orig *o = bpf_map_lookup_elem(&by_cookie, &ck); + if (!o) return 1; + __u32 lp = s->local_port; + bpf_map_update_elem(&by_port, &lp, o, BPF_ANY); + return 1; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/Linux/src/pb_bpf.c b/Linux/src/pb_bpf.c new file mode 100644 index 00000000..7dedb853 --- /dev/null +++ b/Linux/src/pb_bpf.c @@ -0,0 +1,456 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pb_bpf.h" +#include "proxybridge.skel.h" + +#ifndef CGROUP2_SUPER_MAGIC +#define CGROUP2_SUPER_MAGIC 0x63677270 +#endif + +#define MAX_KNOWN_EXES 16384 + +struct exe_ent { uint32_t dev; uint64_t ino; char path[240]; }; + +static struct { + pthread_mutex_t lock; + struct proxybridge_bpf *skel; + int linkfds[16]; + int nlinks; + struct ring_buffer *rb; + pthread_t evt_thread; + bool evt_started; + volatile bool stop; + pb_event_cb cb; + bool running; + + struct exe_ent *exes; size_t nexes; // executables we know about (kernel-side keys) + struct pb_bclass *cls; size_t ncls; // current class table (index = class id) + bool have_rules; +} G = { .lock = PTHREAD_MUTEX_INITIALIZER }; + +// ---- logging bridge ------------------------------------------------------------------------------------------------- +static int libbpf_print(enum libbpf_print_level lvl, const char *fmt, va_list ap) +{ + if (lvl == LIBBPF_DEBUG) return 0; + char buf[1024]; + vsnprintf(buf, sizeof buf, fmt, ap); + size_t n = strlen(buf); + while (n && (buf[n - 1] == '\n')) buf[--n] = 0; + if (n) pb_core_log("libbpf: %s", buf); + return 0; +} + +// ---- cgroup2 root ----------------------------------------------------------------------------------------------------- +static int open_cgroup_root(void) +{ + static const char *cands[] = { "/sys/fs/cgroup", "/sys/fs/cgroup/unified", NULL }; + for (int i = 0; cands[i]; i++) { + struct statfs sf; + if (statfs(cands[i], &sf) == 0 && (unsigned long)sf.f_type == CGROUP2_SUPER_MAGIC) + return open(cands[i], O_RDONLY | O_DIRECTORY | O_CLOEXEC); + } + return -1; +} + +// ---- cfg --------------------------------------------------------------------------------------------------------------- +static void cfg_set(uint32_t slot, uint32_t val) +{ + bpf_map_update_elem(bpf_map__fd(G.skel->maps.cfg), &slot, &val, BPF_ANY); +} + +// ---- attach ------------------------------------------------------------------------------------------------------------ +static bool attach(struct bpf_program *prog, enum bpf_attach_type type, int cgfd, bool optional) +{ + if (!bpf_program__autoload(prog)) return true; + // ALLOW_MULTI lets us coexist with other sock_addr programs on the root cgroup; fall back to a plain attach + LIBBPF_OPTS(bpf_link_create_opts, opts, .flags = BPF_F_ALLOW_MULTI); + int lfd = bpf_link_create(bpf_program__fd(prog), cgfd, type, &opts); + if (lfd < 0 && errno == EINVAL) + lfd = bpf_link_create(bpf_program__fd(prog), cgfd, type, NULL); + if (lfd < 0) { + if (optional) return true; + pb_core_log("attach %s failed: %s", bpf_program__name(prog), strerror(errno)); + return false; + } + G.linkfds[G.nlinks++] = lfd; + return true; +} + +static void detach_all(void) +{ + for (int i = 0; i < G.nlinks; i++) close(G.linkfds[i]); + G.nlinks = 0; +} + +// ---- events ------------------------------------------------------------------------------------------------------------ +static int on_event(void *ctx, void *data, size_t len) +{ + (void)ctx; + if (len >= sizeof(struct pb_event) && G.cb) G.cb(data); + return 0; +} + +static void *evt_loop(void *arg) +{ + (void)arg; + while (!G.stop) ring_buffer__poll(G.rb, 200); + return NULL; +} + +// ---- start / stop ----------------------------------------------------------------------------------------------------- +int pb_bpf_start(const struct pb_bpf_cfg *cfg, pb_event_cb cb) +{ + pthread_mutex_lock(&G.lock); + if (G.running) { pthread_mutex_unlock(&G.lock); return 0; } + int rc = -1, cgfd = -1; + + if (geteuid() != 0) { pb_core_log("ProxyBridge needs root (CAP_BPF/CAP_SYS_ADMIN) to load its eBPF programs"); goto out; } + libbpf_set_print(libbpf_print); + struct rlimit rl = { RLIM_INFINITY, RLIM_INFINITY }; + setrlimit(RLIMIT_MEMLOCK, &rl); // pre-5.11 kernels charge maps to memlock + + cgfd = open_cgroup_root(); + if (cgfd < 0) { pb_core_log("cgroup v2 is not mounted (need a unified hierarchy at /sys/fs/cgroup)"); goto out; } + + G.skel = proxybridge_bpf__open(); + if (!G.skel) { pb_core_log("cannot open BPF object: %s", strerror(errno)); goto out; } + int err = proxybridge_bpf__load(G.skel); + if (err) { + // getpeername hooks need kernel >= 5.8; retry without them + proxybridge_bpf__destroy(G.skel); + G.skel = proxybridge_bpf__open(); + if (G.skel) { + bpf_program__set_autoload(G.skel->progs.pb_getpeername4, false); + bpf_program__set_autoload(G.skel->progs.pb_getpeername6, false); + err = proxybridge_bpf__load(G.skel); + } + if (err || !G.skel) { pb_core_log("loading the BPF programs failed (kernel needs BTF and cgroup sock_addr support, >= 5.8): %s", strerror(-err ? -err : errno)); goto out; } + } + + { // sockets of other network namespaces (containers) must not be redirected to our loopback relay + int ns = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + uint64_t cookie = 0; socklen_t cl = sizeof cookie; + if (ns >= 0 && getsockopt(ns, SOL_SOCKET, 71 /* SO_NETNS_COOKIE */, &cookie, &cl) == 0 && cookie) { + cfg_set(PB_CFG_NETNS_LO, (uint32_t)cookie); + cfg_set(PB_CFG_NETNS_HI, (uint32_t)(cookie >> 32)); + } else pb_core_log("kernel has no SO_NETNS_COOKIE: connections from other network namespaces are not isolated"); + if (ns >= 0) close(ns); + } + cfg_set(PB_CFG_SELF_TGID, cfg->self_tgid); + cfg_set(PB_CFG_TCP_PORT, cfg->tcp_port); + cfg_set(PB_CFG_UDP_PORT, cfg->udp_port); + cfg_set(PB_CFG_LOG, cfg->log); + cfg_set(PB_CFG_DNS_VIA_PROXY, cfg->dns_via_proxy); + cfg_set(PB_CFG_ENABLED, 0); + + bool ok = attach(G.skel->progs.pb_connect4, BPF_CGROUP_INET4_CONNECT, cgfd, false) && + attach(G.skel->progs.pb_connect6, BPF_CGROUP_INET6_CONNECT, cgfd, true) && + attach(G.skel->progs.pb_sendmsg4, BPF_CGROUP_UDP4_SENDMSG, cgfd, false) && + attach(G.skel->progs.pb_sendmsg6, BPF_CGROUP_UDP6_SENDMSG, cgfd, true) && + attach(G.skel->progs.pb_recvmsg4, BPF_CGROUP_UDP4_RECVMSG, cgfd, false) && + attach(G.skel->progs.pb_recvmsg6, BPF_CGROUP_UDP6_RECVMSG, cgfd, true) && + attach(G.skel->progs.pb_getpeername4, BPF_CGROUP_INET4_GETPEERNAME, cgfd, true) && + attach(G.skel->progs.pb_getpeername6, BPF_CGROUP_INET6_GETPEERNAME, cgfd, true) && + attach(G.skel->progs.pb_sockops, BPF_CGROUP_SOCK_OPS, cgfd, false); + if (!ok) goto out; + + G.cb = cb; + G.stop = false; + G.rb = ring_buffer__new(bpf_map__fd(G.skel->maps.events), on_event, NULL, NULL); + if (!G.rb) { pb_core_log("cannot create event ring buffer"); goto out; } + if (pthread_create(&G.evt_thread, NULL, evt_loop, NULL) != 0) { pb_core_log("cannot start event thread"); goto out; } + G.evt_started = true; + G.running = true; + rc = 0; +out: + if (cgfd >= 0) close(cgfd); + if (rc != 0) { + detach_all(); + if (G.rb) { ring_buffer__free(G.rb); G.rb = NULL; } + if (G.skel) { proxybridge_bpf__destroy(G.skel); G.skel = NULL; } + } + pthread_mutex_unlock(&G.lock); + return rc; +} + +void pb_bpf_stop(void) +{ + pthread_mutex_lock(&G.lock); + if (G.skel) { + G.stop = true; + if (G.evt_started) { pthread_mutex_unlock(&G.lock); pthread_join(G.evt_thread, NULL); pthread_mutex_lock(&G.lock); G.evt_started = false; } + detach_all(); + if (G.rb) { ring_buffer__free(G.rb); G.rb = NULL; } + proxybridge_bpf__destroy(G.skel); + G.skel = NULL; + } + free(G.exes); G.exes = NULL; G.nexes = 0; + free(G.cls); G.cls = NULL; G.ncls = 0; + G.running = false; + pthread_mutex_unlock(&G.lock); +} + +bool pb_bpf_running(void) { return G.running; } + +void pb_bpf_set_flags(bool log, bool dns_via_proxy) +{ + pthread_mutex_lock(&G.lock); + if (G.skel) { cfg_set(PB_CFG_LOG, log); cfg_set(PB_CFG_DNS_VIA_PROXY, dns_via_proxy); } + pthread_mutex_unlock(&G.lock); +} + +// ---- executables -------------------------------------------------------------------------------------------------------- +static uint32_t kernel_dev(dev_t d) { return (major(d) << 20) | minor(d); } + +static struct exe_ent *exe_find(uint32_t dev, uint64_t ino) +{ + for (size_t i = 0; i < G.nexes; i++) + if (G.exes[i].ino == ino && G.exes[i].dev == dev) return &G.exes[i]; + return NULL; +} + +static struct exe_ent *exe_add(uint32_t dev, uint64_t ino, const char *path) +{ + struct exe_ent *e = exe_find(dev, ino); + if (e) { if (path && *path) snprintf(e->path, sizeof e->path, "%s", path); return e; } + if (G.nexes >= MAX_KNOWN_EXES) return NULL; + if (!G.exes) { G.exes = calloc(MAX_KNOWN_EXES, sizeof *G.exes); if (!G.exes) return NULL; } + e = &G.exes[G.nexes++]; + e->dev = dev; e->ino = ino; + snprintf(e->path, sizeof e->path, "%s", path ? path : ""); + return e; +} + +static void strip_deleted(char *p) +{ + size_t n = strlen(p); + static const char suf[] = " (deleted)"; + if (n > sizeof suf - 1 && !strcmp(p + n - (sizeof suf - 1), suf)) p[n - (sizeof suf - 1)] = 0; +} + +static void scan_proc(void) +{ + DIR *d = opendir("/proc"); + if (!d) return; + struct dirent *de; + while ((de = readdir(d))) { + if (de->d_name[0] < '1' || de->d_name[0] > '9') continue; + char lnk[300], path[240]; + snprintf(lnk, sizeof lnk, "/proc/%.200s/exe", de->d_name); + struct stat st; + if (stat(lnk, &st) != 0) continue; + ssize_t n = readlink(lnk, path, sizeof path - 1); + if (n <= 0) continue; + path[n] = 0; + strip_deleted(path); + exe_add(kernel_dev(st.st_dev), st.st_ino, path); + } + closedir(d); +} + +// ---- compilation ----------------------------------------------------------------------------------------------------------- +static void compile_rule(const struct pb_rule_view *v, struct pb_brule *b) +{ + const struct pb_rule *r = v->rule; + memset(b, 0, sizeof *b); + b->action = (uint8_t)v->action; + b->proxy_id = v->action == RULE_ACTION_PROXY ? v->proxy_id : 0; + b->proto_mask = r->protocol == RULE_PROTOCOL_TCP ? 1 : r->protocol == RULE_PROTOCOL_UDP ? 2 : 3; + if (v->action == RULE_ACTION_PROXY && v->udp_direct) b->flags |= PB_RF_UDP_DIRECT; + + if (r->any_port) b->flags |= PB_RF_ANY_PORT; + else { + for (int i = 0; i < r->nports; i++) { + if (b->n_port < PB_BPF_MAX_PORT) { b->port[b->n_port].lo = r->ports[i].lo; b->port[b->n_port].hi = r->ports[i].hi; b->n_port++; } + else b->flags |= PB_RF_PORT_COMPLEX; + } + } + if (r->any_host) b->flags |= PB_RF_ANY_HOST; + else { + if (r->ndomains) b->flags |= PB_RF_DOMAINS; + for (int i = 0; i < r->nhosts; i++) { + const struct pb_host_pat *h = &r->hosts[i]; + if (h->kind == PB_HOST_V4_RANGE && b->n_v4 < PB_BPF_MAX_V4) { b->v4[b->n_v4].lo = h->lo4; b->v4[b->n_v4].hi = h->hi4; b->n_v4++; } + else if (h->kind == PB_HOST_V6_RANGE) b->flags |= PB_RF_HAS_V6; + else b->flags |= PB_RF_HOST_COMPLEX; // box patterns / too many ranges: let the relay decide + } + } +} + +// a rule that cannot be skipped by later ones: every host, every port, both protocols +static bool rule_is_catchall(const struct pb_brule *b) +{ + return (b->flags & PB_RF_ANY_HOST) && (b->flags & PB_RF_ANY_PORT) && b->proto_mask == 3; +} + +static int class_for(const struct pb_rule_view *v, const struct pb_brule *br, size_t n, const char *path, struct pb_bclass *out) +{ + memset(out, 0, sizeof *out); + for (size_t i = 0; i < n; i++) { + if (!v[i].rule->enabled) continue; + if (!pb_rule_match_process(v[i].rule, path)) continue; + if (pb_rule_has_domains(v[i].rule)) out->tap = 1; + if (out->n >= PB_RULES_PER_CLASS) { memset(out, 0, sizeof *out); out->n = PB_CLASS_DECIDE; out->tap = 1; return 0; } + out->rule[out->n++] = (uint32_t)i; + if (rule_is_catchall(&br[i])) break; // nothing after a catch-all can apply + } + return 0; +} + +static int class_intern(const struct pb_bclass *c) +{ + for (size_t i = 0; i < G.ncls; i++) + if (!memcmp(&G.cls[i], c, sizeof *c)) return (int)i; + if (G.ncls >= PB_MAX_CLASSES) return -1; + G.cls[G.ncls] = *c; + return (int)G.ncls++; +} + +static void write_class_slot(uint32_t id) +{ + bpf_map_update_elem(bpf_map__fd(G.skel->maps.classes), &id, &G.cls[id], BPF_ANY); +} + +int pb_bpf_sync(const struct pb_rule_view *v, size_t n) +{ + if (n > PB_MAX_RULES) { pb_core_log("too many rules (max %d)", PB_MAX_RULES); return -1; } + pthread_mutex_lock(&G.lock); + if (!G.skel) { pthread_mutex_unlock(&G.lock); return -1; } + + struct pb_brule *br = calloc(n ? n : 1, sizeof *br); + if (!G.cls) G.cls = calloc(PB_MAX_CLASSES, sizeof *G.cls); + if (!br || !G.cls) { free(br); pthread_mutex_unlock(&G.lock); return -1; } + + size_t active = 0; + for (size_t i = 0; i < n; i++) { + compile_rule(&v[i], &br[i]); + uint32_t k = (uint32_t)i; + bpf_map_update_elem(bpf_map__fd(G.skel->maps.brules), &k, &br[i], BPF_ANY); + if (v[i].rule->enabled) active++; + } + + scan_proc(); + G.ncls = 0; + memset(G.cls, 0, PB_MAX_CLASSES * sizeof *G.cls); + G.ncls = 1; // class 0 = no rule applies (direct) + int mapfd = bpf_map__fd(G.skel->maps.exe2class); + + // remember what was installed so entries for vanished binaries can be dropped + struct pb_exe_key k = {}, next; + struct pb_exe_key *old = NULL; size_t nold = 0, capold = 0; + int ret = bpf_map_get_next_key(mapfd, NULL, &next); + while (ret == 0) { + if (nold == capold) { capold = capold ? capold * 2 : 256; old = realloc(old, capold * sizeof *old); if (!old) break; } + old[nold++] = next; + k = next; + ret = bpf_map_get_next_key(mapfd, &k, &next); + } + + for (size_t i = 0; i < G.nexes; i++) { + struct pb_bclass c; + class_for(v, br, n, G.exes[i].path, &c); + int id = class_intern(&c); + struct pb_exe_key key = { .dev = G.exes[i].dev, .ino = G.exes[i].ino }; + if (id < 0) { bpf_map_delete_elem(mapfd, &key); continue; } // out of classes: slow path + uint32_t cid = (uint32_t)id; + bpf_map_update_elem(mapfd, &key, &cid, BPF_ANY); + } + for (size_t i = 0; i < G.ncls; i++) write_class_slot((uint32_t)i); + for (size_t i = 0; i < nold; i++) + if (!exe_find(old[i].dev, old[i].ino)) bpf_map_delete_elem(mapfd, &old[i]); + free(old); + free(br); + + bool domains = false; + for (size_t i = 0; i < n; i++) domains |= v[i].rule->enabled && pb_rule_has_domains(v[i].rule); + cfg_set(PB_CFG_DNS_TAP, domains ? 1 : 0); + G.have_rules = active > 0; + cfg_set(PB_CFG_ENABLED, G.have_rules ? 1 : 0); + pthread_mutex_unlock(&G.lock); + return 0; +} + +void pb_bpf_learn(const struct pb_orig *o, const char *exe_path, const struct pb_rule_view *v, size_t n) +{ + if (!o->exe_ino || n > PB_MAX_RULES) return; + pthread_mutex_lock(&G.lock); + if (!G.skel || !G.cls) { pthread_mutex_unlock(&G.lock); return; } + struct exe_ent *known = exe_find(o->exe_dev, o->exe_ino); + struct exe_ent *e = known ? NULL : exe_add(o->exe_dev, o->exe_ino, exe_path); // already classified: nothing to learn + if (e) { + struct pb_brule *br = calloc(n ? n : 1, sizeof *br); + if (br) { + for (size_t i = 0; i < n; i++) compile_rule(&v[i], &br[i]); + struct pb_bclass c; + class_for(v, br, n, e->path, &c); + int id = class_intern(&c); + if (id >= 0) { + write_class_slot((uint32_t)id); + struct pb_exe_key key = { .dev = e->dev, .ino = e->ino }; + uint32_t cid = (uint32_t)id; + bpf_map_update_elem(bpf_map__fd(G.skel->maps.exe2class), &key, &cid, BPF_ANY); + } + free(br); + } + } + pthread_mutex_unlock(&G.lock); +} + +// ---- lookups for the relay ------------------------------------------------------------------------------------------------- +int pb_bpf_take_tcp_orig(uint16_t client_port, struct pb_orig *out) +{ + uint32_t key = client_port; + int fd = G.skel ? bpf_map__fd(G.skel->maps.by_port) : -1; + if (fd < 0 || bpf_map_lookup_elem(fd, &key, out) != 0) return -1; + bpf_map_delete_elem(fd, &key); + return 0; +} + +int pb_bpf_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out) +{ + int fd = G.skel ? bpf_map__fd(G.skel->maps.flows) : -1; + return (fd >= 0 && bpf_map_lookup_elem(fd, &flow_ip_be, out) == 0) ? 0 : -1; +} + +// ---- domain rules ------------------------------------------------------------------------------------------------------------ +void pb_bpf_dom_set(uint32_t rule_idx, uint32_t ip_host) +{ + if (!G.skel) return; + struct pb_dom_key k = { .rule = rule_idx, .ip = ip_host }; + uint8_t one = 1; + bpf_map_update_elem(bpf_map__fd(G.skel->maps.dom_hit), &k, &one, BPF_ANY); +} + +void pb_bpf_dom_clear(void) +{ + if (!G.skel) return; + int fd = bpf_map__fd(G.skel->maps.dom_hit); + struct pb_dom_key k, next; + int rc = bpf_map_get_next_key(fd, NULL, &next); + while (rc == 0) { + k = next; + rc = bpf_map_get_next_key(fd, &k, &next); + bpf_map_delete_elem(fd, &k); + } +} + +void pb_bpf_set_dns_tap(bool on) +{ + pthread_mutex_lock(&G.lock); + if (G.skel) cfg_set(PB_CFG_DNS_TAP, on ? 1 : 0); + pthread_mutex_unlock(&G.lock); +} diff --git a/Linux/src/pb_bpf.h b/Linux/src/pb_bpf.h new file mode 100644 index 00000000..2a4c7daf --- /dev/null +++ b/Linux/src/pb_bpf.h @@ -0,0 +1,46 @@ +// eBPF backend: loads/attaches the datapath, compiles rules into BPF maps, reads flow events. +#ifndef PB_BPF_H +#define PB_BPF_H + +#include "pb_internal.h" +#include "pb_rules.h" +#include "bpf/pb_bpf_shared.h" + +struct pb_bpf_cfg { + uint16_t tcp_port, udp_port; // relay ports (already bound by the relay) + uint32_t self_tgid; // process hosting the relay; never redirected + bool log; + bool dns_via_proxy; +}; + +// One rule as seen by the compiler: the parsed rule plus its resolved proxy. +struct pb_rule_view { + const struct pb_rule *rule; + int action; // effective action (PROXY with no usable proxy has already become BLOCK) + uint16_t proxy_id; // resolved (0 for DIRECT/BLOCK) + bool udp_direct; // PROXY rule whose proxy is HTTP: UDP goes direct +}; + +typedef void (*pb_event_cb)(const struct pb_event *e); + +// Returns 0 on success; on failure returns -1 after logging the reason (kernel too old, not root, no cgroup2, verifier...). +int pb_bpf_start(const struct pb_bpf_cfg *cfg, pb_event_cb cb); +void pb_bpf_stop(void); +bool pb_bpf_running(void); + +void pb_bpf_set_flags(bool log, bool dns_via_proxy); +// Recompiles the rule set (and rescans /proc for running executables). Returns -1 if it does not fit (too many rules). +int pb_bpf_sync(const struct pb_rule_view *v, size_t n); +// Slow-path learning: install the class for the exact executable key the kernel reported in `o`, using the same rule set +// that decide() just evaluated. +void pb_bpf_learn(const struct pb_orig *o, const char *exe_path, const struct pb_rule_view *v, size_t n); + +// Domain rules: (rule index, address) pairs whose DNS name matches. Set by the DNS tap before the answer reaches the client. +void pb_bpf_dom_set(uint32_t rule_idx, uint32_t ip_host); +void pb_bpf_dom_clear(void); +void pb_bpf_set_dns_tap(bool on); + +int pb_bpf_take_tcp_orig(uint16_t client_port, struct pb_orig *out); +int pb_bpf_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out); + +#endif diff --git a/Linux/src/pb_dns.c b/Linux/src/pb_dns.c new file mode 100644 index 00000000..8906d488 --- /dev/null +++ b/Linux/src/pb_dns.c @@ -0,0 +1,263 @@ +#include +#include +#include +#include +#include +#include +#include +#include "pb_dns.h" + +#define BUCKETS 16384 + +struct ent { + uint32_t ip; + char *names[PB_DNS_MAX_NAMES]; int n; + struct ent *hnext; // hash chain + struct ent *prev, *next; // age order: head = oldest +}; + +static struct { + pthread_mutex_t lock; + struct ent *bucket[BUCKETS]; + struct ent *head, *tail; + size_t count; +} T = { .lock = PTHREAD_MUTEX_INITIALIZER }; + +static unsigned hash_ip(uint32_t ip) { ip *= 2654435761u; return (ip >> 12) & (BUCKETS - 1); } + +static void ent_unlink_age(struct ent *e) +{ + if (e->prev) e->prev->next = e->next; else T.head = e->next; + if (e->next) e->next->prev = e->prev; else T.tail = e->prev; + e->prev = e->next = NULL; +} + +static void ent_push_tail(struct ent *e) +{ + e->prev = T.tail; e->next = NULL; + if (T.tail) T.tail->next = e; else T.head = e; + T.tail = e; +} + +static void ent_free(struct ent *e) +{ + for (int i = 0; i < e->n; i++) free(e->names[i]); + free(e); +} + +static void evict_oldest(struct pb_dns_result *res) +{ + struct ent *e = T.head; + if (!e) return; + ent_unlink_age(e); + for (struct ent **pp = &T.bucket[hash_ip(e->ip)]; *pp; pp = &(*pp)->hnext) + if (*pp == e) { *pp = e->hnext; break; } + if (res && res->nevicted < 32) res->evicted[res->nevicted++] = e->ip; + ent_free(e); + T.count--; +} + +static struct ent *find(uint32_t ip) +{ + for (struct ent *e = T.bucket[hash_ip(ip)]; e; e = e->hnext) if (e->ip == ip) return e; + return NULL; +} + +// Adds names to ip's entry (creating it), refreshing its age. Caller holds the lock. +static void add_mapping(uint32_t ip, char names[][256], int nn, struct pb_dns_result *res) +{ + struct ent *e = find(ip); + if (!e) { + while (T.count >= PB_DNS_MAX_ENTRIES) evict_oldest(res); + e = calloc(1, sizeof *e); + if (!e) return; + e->ip = ip; + unsigned h = hash_ip(ip); + e->hnext = T.bucket[h]; T.bucket[h] = e; + T.count++; + } else { + ent_unlink_age(e); + } + ent_push_tail(e); + for (int i = 0; i < nn; i++) { + bool have = false; + for (int j = 0; j < e->n; j++) if (!strcmp(e->names[j], names[i])) { have = true; break; } + if (have) continue; + char *c = strdup(names[i]); + if (!c) continue; + if (e->n == PB_DNS_MAX_NAMES) { free(e->names[0]); memmove(&e->names[0], &e->names[1], (PB_DNS_MAX_NAMES - 1) * sizeof(char *)); e->n--; } + e->names[e->n++] = c; + } + if (res) { + bool dup = false; + for (int i = 0; i < res->nips; i++) if (res->ips[i] == ip) dup = true; + if (!dup && res->nips < 32) res->ips[res->nips++] = ip; + } +} + +// ---- wire format ----------------------------------------------------------------------------------------------------- + +// Reads a (possibly compressed) name at *off into out (lower-case, dotted). Advances *off past the name as stored at that +// position. Returns false on malformed data. +static bool read_name(const uint8_t *m, size_t len, size_t *off, char *out, size_t outsz) +{ + size_t pos = *off, o = 0; + int hops = 0; + bool jumped = false; + for (;;) { + if (pos >= len) return false; + uint8_t l = m[pos]; + if (l == 0) { pos++; break; } + if ((l & 0xC0) == 0xC0) { + if (pos + 1 >= len) return false; + size_t ptr = ((size_t)(l & 0x3F) << 8) | m[pos + 1]; + if (!jumped) *off = pos + 2; + jumped = true; + if (++hops > 16 || ptr >= len) return false; + pos = ptr; + continue; + } + if (l & 0xC0) return false; + if (pos + 1 + l > len) return false; + if (o + l + 2 > outsz) return false; + if (o) out[o++] = '.'; + for (uint8_t i = 0; i < l; i++) out[o++] = (char)tolower(m[pos + 1 + i]); + pos += 1u + l; + } + out[o] = 0; + if (!jumped) *off = pos; + return true; +} + +static uint16_t rd16(const uint8_t *p) { return (uint16_t)((p[0] << 8) | p[1]); } + +int pb_dns_observe(const uint8_t *m, size_t len, struct pb_dns_result *res) +{ + if (res) memset(res, 0, sizeof *res); + if (len < 12) return 0; + uint16_t flags = rd16(m + 2), qd = rd16(m + 4), an = rd16(m + 6); + if (!(flags & 0x8000) || (flags & 0x7800) || (flags & 0x000F)) return 0; // response, QUERY opcode, NOERROR only + if (qd > 4 || an == 0 || an > 128) return 0; + + char names[PB_DNS_MAX_NAMES][256]; + int nn = 0; + size_t off = 12; + char buf[256]; + for (int i = 0; i < qd; i++) { + if (!read_name(m, len, &off, buf, sizeof buf) || off + 4 > len) return 0; + off += 4; + if (nn < PB_DNS_MAX_NAMES) { bool dup = false; for (int k = 0; k < nn; k++) if (!strcmp(names[k], buf)) dup = true; if (!dup) strcpy(names[nn++], buf); } + } + + uint32_t ips[64]; int nips = 0; + // first pass collects the chain of names (CNAME owners/targets and A owners), second pass assigns them to every address + struct { size_t rdata; uint16_t type, rdlen; char owner[256]; } rr[128]; + int nrr = 0; + for (int i = 0; i < an; i++) { + char owner[256]; + if (!read_name(m, len, &off, owner, sizeof owner) || off + 10 > len) break; + uint16_t type = rd16(m + off), rdlen = rd16(m + off + 8); + size_t rd = off + 10; + if (rd + rdlen > len) break; + strcpy(rr[nrr].owner, owner); rr[nrr].type = type; rr[nrr].rdlen = rdlen; rr[nrr].rdata = rd; nrr++; + off = rd + rdlen; + } + for (int i = 0; i < nrr; i++) { + bool addname = false; + char target[256]; + if (rr[i].type == 5) { // CNAME: owner and target are both names of the final address + size_t o2 = rr[i].rdata; + if (read_name(m, len, &o2, target, sizeof target)) { + addname = true; + for (int pass = 0; pass < 2; pass++) { + const char *nm = pass ? target : rr[i].owner; + bool dup = false; + for (int k = 0; k < nn; k++) if (!strcmp(names[k], nm)) dup = true; + if (!dup && nn < PB_DNS_MAX_NAMES) strcpy(names[nn++], nm); + } + } + } else if (rr[i].type == 1 && rr[i].rdlen == 4) { + if (nips < 64) { + ips[nips++] = ((uint32_t)m[rr[i].rdata] << 24) | ((uint32_t)m[rr[i].rdata + 1] << 16) | ((uint32_t)m[rr[i].rdata + 2] << 8) | m[rr[i].rdata + 3]; + bool dup = false; + for (int k = 0; k < nn; k++) if (!strcmp(names[k], rr[i].owner)) dup = true; + if (!dup && nn < PB_DNS_MAX_NAMES) strcpy(names[nn++], rr[i].owner); + } + } + (void)addname; + } + if (!nips || !nn) return 0; + + pthread_mutex_lock(&T.lock); + for (int i = 0; i < nips; i++) { + uint32_t ip = ips[i]; + if ((ip >> 24) == 127 || ip == 0) continue; // answers pointing at loopback/unspecified are never redirected + add_mapping(ip, names, nn, res); + } + pthread_mutex_unlock(&T.lock); + return nips; +} + +int pb_dns_lookup(uint32_t ip, char out[][256], int max) +{ + int n = 0; + pthread_mutex_lock(&T.lock); + struct ent *e = find(ip); + if (e) for (; n < e->n && n < max; n++) snprintf(out[n], 256, "%s", e->names[n]); + pthread_mutex_unlock(&T.lock); + return n; +} + +int pb_dns_load_hosts(const char *path, struct pb_dns_result *res) +{ + if (res) memset(res, 0, sizeof *res); + FILE *f = fopen(path, "r"); + if (!f) return 0; + char line[1024]; + int added = 0; + while (fgets(line, sizeof line, f)) { + char *hash = strchr(line, '#'); + if (hash) *hash = 0; + char *save = NULL, *ipstr = strtok_r(line, " \t\r\n", &save); + struct in_addr a; + if (!ipstr || inet_pton(AF_INET, ipstr, &a) != 1) continue; + uint32_t ip = ntohl(a.s_addr); + if ((ip >> 24) == 127 || ip == 0) continue; + char names[PB_DNS_MAX_NAMES][256]; + int nn = 0; + for (char *nm; nn < PB_DNS_MAX_NAMES && (nm = strtok_r(NULL, " \t\r\n", &save)); ) { + snprintf(names[nn], 256, "%s", nm); + for (char *c = names[nn]; *c; c++) *c = (char)tolower((unsigned char)*c); + nn++; + } + if (!nn) continue; + pthread_mutex_lock(&T.lock); + add_mapping(ip, names, nn, res); + pthread_mutex_unlock(&T.lock); + added++; + } + fclose(f); + return added; +} + +void pb_dns_for_each(void (*cb)(uint32_t, const char *const *, int, void *), void *ctx) +{ + pthread_mutex_lock(&T.lock); + for (struct ent *e = T.head; e; e = e->next) cb(e->ip, (const char *const *)e->names, e->n, ctx); + pthread_mutex_unlock(&T.lock); +} + +void pb_dns_clear(void) +{ + pthread_mutex_lock(&T.lock); + while (T.head) evict_oldest(NULL); + pthread_mutex_unlock(&T.lock); +} + +size_t pb_dns_count(void) +{ + pthread_mutex_lock(&T.lock); + size_t n = T.count; + pthread_mutex_unlock(&T.lock); + return n; +} diff --git a/Linux/src/pb_dns.h b/Linux/src/pb_dns.h new file mode 100644 index 00000000..550e491c --- /dev/null +++ b/Linux/src/pb_dns.h @@ -0,0 +1,33 @@ +// DNS awareness: learns IPv4 -> domain-name mappings from DNS answers that pass through the relay (the "DNS tap") so that +// domain rules can be applied to connections the kernel only sees as IP addresses. +#ifndef PB_DNS_H +#define PB_DNS_H + +#include +#include + +#define PB_DNS_MAX_NAMES 8 +#define PB_DNS_MAX_ENTRIES 32768 // bounded: oldest mappings are evicted first + +struct pb_dns_result { + uint32_t ips[32]; int nips; // addresses (host order) whose mapping was added or refreshed + uint32_t evicted[32]; int nevicted; // addresses dropped to stay within PB_DNS_MAX_ENTRIES +}; + +// Parses a DNS response message (no TCP length prefix). Returns the number of A records learned (0 if the message is not a +// successful response or has no A records). Safe on arbitrary/malicious input. +int pb_dns_observe(const uint8_t *msg, size_t len, struct pb_dns_result *res); + +// Copies up to max names known for ip (host order) into out; returns the count. +int pb_dns_lookup(uint32_t ip, char out[][256], int max); + +// Adds static mappings from /etc/hosts. Returns the number of addresses added. +int pb_dns_load_hosts(const char *path, struct pb_dns_result *res); + +// Visits every mapping (used to re-arm kernel state after a rule change). cb must not call back into pb_dns_*. +void pb_dns_for_each(void (*cb)(uint32_t ip, const char *const *names, int n, void *ctx), void *ctx); + +void pb_dns_clear(void); +size_t pb_dns_count(void); + +#endif diff --git a/Linux/src/pb_internal.h b/Linux/src/pb_internal.h new file mode 100644 index 00000000..83e72f0f --- /dev/null +++ b/Linux/src/pb_internal.h @@ -0,0 +1,121 @@ +// Internal interfaces shared by the ProxyBridge Linux modules (eBPF backend). +// +// proxybridge.bpf.c kernel side: rewrites connect()/sendmsg() destinations to the relay +// pb_rules.c rule store, validation, full userspace matcher +// pb_bpf.c loads/attaches BPF, compiles rules into BPF maps, reads events +// pb_relay.c userspace relay: TCP (SOCKS5 / HTTP CONNECT), UDP (SOCKS5 UDP ASSOCIATE), DIRECT +// ProxyBridge.c public API glue +#ifndef PB_INTERNAL_H +#define PB_INTERNAL_H + +#include +#include +#include +#include +#include "ProxyBridge.h" + +#define PB_TCP_RELAY_PORT_DEFAULT 24010 // outside the default ephemeral range 32768-60999 +#define PB_UDP_RELAY_PORT_DEFAULT 24011 + +// --------------------------------------------------------------------------------------------- +// Decision values +// --------------------------------------------------------------------------------------------- +#define PB_ACT_PROXY 0 // matches RuleAction +#define PB_ACT_DIRECT 1 +#define PB_ACT_BLOCK 2 +#define PB_ACT_DECIDE 3 // kernel could not decide: relay must call pb_core_decide() + +// --------------------------------------------------------------------------------------------- +// Original destination record. Produced by the BPF programs, looked up by the relay. +// Layout is shared with the BPF object: keep in sync with proxybridge.bpf.c (struct pb_orig). +// --------------------------------------------------------------------------------------------- +struct pb_orig { + uint8_t addr[16]; // original destination, network order. IPv4 is stored as ::ffff:a.b.c.d + uint16_t port; // original destination port, host order + uint16_t proxy_id; // valid when action == PB_ACT_PROXY + uint32_t pid; // tgid of the process that opened the flow + uint8_t family; // AF_INET or AF_INET6 (of the ORIGINAL destination) + uint8_t proto; // IPPROTO_TCP or IPPROTO_UDP + uint8_t action; // PB_ACT_PROXY or PB_ACT_DECIDE (DIRECT/BLOCK never reach the relay from the kernel) + uint8_t pad; + uint32_t exe_dev; // kernel-side (dev, inode) of the process's executable (valid for PB_ACT_DECIDE flows) + uint32_t exe_pad; + uint64_t exe_ino; +}; + +// Result of a userspace decision (slow path). +struct pb_decision { + int action; // PB_ACT_PROXY / PB_ACT_DIRECT / PB_ACT_BLOCK + uint32_t proxy_id; // for PB_ACT_PROXY +}; + +// A configured upstream proxy (snapshot copy handed to the relay; the relay owns the copy). +struct pb_proxy { + uint32_t id; + ProxyType type; // PROXY_TYPE_HTTP / PROXY_TYPE_SOCKS5 + char name[64]; + char host[256]; // IP literal or hostname (relay must resolve it, with a timeout, outside the epoll loop) + uint16_t port; + char user[256]; // empty string = no auth + char pass[256]; +}; + +// --------------------------------------------------------------------------------------------- +// Services the CORE provides to the relay (implemented in pb_bpf.c / ProxyBridge.c) +// --------------------------------------------------------------------------------------------- + +// TCP: original destination for a relayed connection, keyed by the *client's* source port as seen by the relay +// (accept() peer port; peer address is 127.0.0.1). Consumes (deletes) the entry. Returns 0 on success. +int pb_core_take_tcp_orig(uint16_t client_port, struct pb_orig *out); + +// UDP: original destination for a datagram, keyed by the loopback address it was sent to (IP_PKTINFO ipi_addr, network +// order). Does not delete (flows are shared and LRU-evicted). Returns 0 on success. +int pb_core_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out); + +// Slow-path decision for a pb_orig with action == PB_ACT_DECIDE (evaluates the full rule set for o->pid). +void pb_core_decide(const struct pb_orig *o, struct pb_decision *out); + +// Copies the proxy config with this id (or the default proxy when id == 0). Returns 0 on success, -1 if unknown. +int pb_core_get_proxy(uint32_t id, struct pb_proxy *out); + +// Logging / events. Both thread-safe, may be called from any relay thread. +void pb_core_log(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +// One event per relayed flow after the final action is known (proxy_desc e.g. "SOCKS5 127.0.0.1:1080" / "DIRECT" / "BLOCK"). +void pb_core_flow_event(const struct pb_orig *o, int action, const char *proxy_desc); + +// DNS tap. The relay calls this for every DNS message travelling server->client on a flow whose ORIGINAL destination port is 53: +// the payload of each UDP reply datagram, or each complete TCP-DNS message (2-byte length prefix already stripped). It must be +// called BEFORE the message is delivered to the client, because the core uses it to arm kernel rules for the addresses in the +// answer. Thread-safe and cheap when no domain rules exist (returns immediately). +void pb_core_dns_observe(const uint8_t *msg, size_t len); + +// Whether DNS (UDP/TCP port 53) should be sent through the proxy even if rules say DIRECT is not set. Informational. +bool pb_core_dns_via_proxy(void); + +// --------------------------------------------------------------------------------------------- +// Services the RELAY provides to the core (implemented in pb_relay.c) +// --------------------------------------------------------------------------------------------- +struct pb_relay_cfg { + uint16_t tcp_port; // listen port on 127.0.0.1 (0 = PB_TCP_RELAY_PORT_DEFAULT) + uint16_t udp_port; // UDP listen port, bound with SO_BINDTODEVICE "lo" (0 = default) + int max_tcp_conns; // hard cap on concurrent relayed TCP connections (0 = 32768) + int tcp_idle_timeout_s; // 0 = 300 + int udp_idle_timeout_s; // 0 = 60 + int handshake_timeout_s; // proxy connect + handshake budget (0 = 10) +}; + +// Binds the sockets (returns -1 and logs via pb_core_log on failure, leaving nothing running), starts the worker threads. +int pb_relay_start(const struct pb_relay_cfg *cfg); +// Stops all threads, closes every fd (listeners, client/upstream sockets, epoll, eventfds). Safe to call twice. After it +// returns, pb_relay_start() must work again (no leaked listeners). +void pb_relay_stop(void); +// Actual ports in use (valid after a successful start). +uint16_t pb_relay_tcp_port(void); +uint16_t pb_relay_udp_port(void); + +// Synchronous proxy test used by ProxyBridge_TestConnection: connects to `target_host:target_port` THROUGH `proxy` and sends +// an HTTP HEAD/GET if port is 80/443-ish (caller decides); returns bytes of response in buf (NUL-terminated) or -1 on error with +// a human readable reason in buf. Must not depend on relay_start(). 10 second budget. +int pb_relay_test_proxy(const struct pb_proxy *proxy, const char *target_host, uint16_t target_port, char *buf, size_t buflen); + +#endif diff --git a/Linux/src/pb_relay.c b/Linux/src/pb_relay.c new file mode 100644 index 00000000..e1de2bb0 --- /dev/null +++ b/Linux/src/pb_relay.c @@ -0,0 +1,1781 @@ +// pb_relay.c - userspace relay for the eBPF backend. +// +// TCP: acceptor thread -> bounded handshake worker pool (blocking steps with deadlines) -> epoll data threads. +// UDP: one epoll thread owning every session; SOCKS5 UDP ASSOCIATE setup runs in the worker pool. +// Ownership: a conn/session is only touched by the thread that owns its epoll; workers hand objects over via queues. +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pb_internal.h" + +#define SCRATCH_SZ 65536 // per data thread read buffer; idle connections hold no buffer +#define HDR_MAX 8192 +#define NWORKERS 16 +#define MAX_DATA 8 +#define PIPE_SZ (1 << 20) +#define JOBQ_MAX 4096 // handshake queue; excess accepts are shed with a log +#define MAX_UDP_SESS 1024 +#define MAX_FLOWS 64 +#define UDP_QMAX 32 +#define UDP_QBYTES (256 * 1024) +#define UDP_MAXDG 65536 +#define DC_SIZE 1024 +#define SESS_BUCKETS 256 +#define DIRECT_ID 0xFFFFFFFFu + +static char TAG_STOP, TAG_WAKE, TAG_UDP, TAG_TIMER; + +// --------------------------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------------------------- +static int64_t now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static void close_fd(int *fd) +{ + if (*fd >= 0) { close(*fd); *fd = -1; } +} + +static void fmt_ip(int family, const uint8_t *addr16, char *out, size_t n) +{ + if (family == AF_INET) inet_ntop(AF_INET, addr16 + 12, out, (socklen_t)n); + else inet_ntop(AF_INET6, addr16, out, (socklen_t)n); +} + +static void orig_str(const struct pb_orig *o, char *out, size_t n) +{ + char ip[INET6_ADDRSTRLEN]; + fmt_ip(o->family, o->addr, ip, sizeof ip); + snprintf(out, n, "%s:%u", ip, o->port); +} + +static socklen_t orig_to_sa(const struct pb_orig *o, struct sockaddr_storage *ss) +{ + memset(ss, 0, sizeof *ss); + if (o->family == AF_INET) { + struct sockaddr_in *a = (struct sockaddr_in *)ss; + a->sin_family = AF_INET; + a->sin_port = htons(o->port); + memcpy(&a->sin_addr, o->addr + 12, 4); + return sizeof *a; + } + struct sockaddr_in6 *a = (struct sockaddr_in6 *)ss; + a->sin6_family = AF_INET6; + a->sin6_port = htons(o->port); + memcpy(&a->sin6_addr, o->addr, 16); + return sizeof *a; +} + +// Waits for fd readiness. 0 = ready (or error/hup: the next I/O call reports it), -1 = timeout, -2 = stop requested. +static int wait_fd(int fd, short ev, int64_t deadline, int stopfd) +{ + for (;;) { + int64_t rem = deadline - now_ms(); + if (rem <= 0) return -1; + struct pollfd p[2] = { { .fd = fd, .events = ev }, { .fd = stopfd, .events = POLLIN } }; + int r = poll(p, stopfd >= 0 ? 2 : 1, rem > 60000 ? 60000 : (int)rem); + if (r < 0) { if (errno == EINTR) continue; return -1; } + if (r == 0) continue; + if (stopfd >= 0 && p[1].revents) return -2; + if (p[0].revents) return 0; + } +} + +// Blocking-with-deadline I/O context for handshakes (fd is non-blocking). +struct io { + int fd; + int64_t dl; + int stopfd; + char err[200]; +}; + +static void seterr(struct io *x, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +static void seterr(struct io *x, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vsnprintf(x->err, sizeof x->err, fmt, ap); + va_end(ap); +} + +static void io_waitfail(struct io *x, int w) +{ + seterr(x, w == -1 ? "timed out" : "relay stopping"); +} + +static int io_send(struct io *x, const void *buf, size_t n) +{ + const uint8_t *p = buf; + while (n) { + ssize_t r = send(x->fd, p, n, MSG_NOSIGNAL); + if (r > 0) { p += r; n -= (size_t)r; continue; } + if (r < 0 && errno == EINTR) continue; + if (r < 0 && errno == EAGAIN) { + int w = wait_fd(x->fd, POLLOUT, x->dl, x->stopfd); + if (w) { io_waitfail(x, w); return -1; } + continue; + } + seterr(x, "send failed: %s", strerror(errno)); + return -1; + } + return 0; +} + +// Receives 1..n bytes. +static ssize_t io_recv(struct io *x, void *buf, size_t n) +{ + for (;;) { + ssize_t r = recv(x->fd, buf, n, 0); + if (r > 0) return r; + if (r == 0) { seterr(x, "connection closed by peer during handshake"); return -1; } + if (errno == EINTR) continue; + if (errno == EAGAIN) { + int w = wait_fd(x->fd, POLLIN, x->dl, x->stopfd); + if (w) { io_waitfail(x, w); return -1; } + continue; + } + seterr(x, "recv failed: %s", strerror(errno)); + return -1; + } +} + +static int io_recv_exact(struct io *x, void *buf, size_t n) +{ + uint8_t *p = buf; + while (n) { + ssize_t r = io_recv(x, p, n); + if (r < 0) return -1; + p += r; + n -= (size_t)r; + } + return 0; +} + +// --------------------------------------------------------------------------------------------- +// Connecting +// --------------------------------------------------------------------------------------------- +static void tune_sock(int fd) +{ + int one = 1, idle = 60, intvl = 15, cnt = 4; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one); + setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof idle); + setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof intvl); + setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof cnt); +} + +static int connect_addr(const struct sockaddr *sa, socklen_t len, int64_t dl, int stopfd, int *eno) +{ + int fd = socket(sa->sa_family, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (fd < 0) { *eno = errno; return -1; } + tune_sock(fd); + if (connect(fd, sa, len) < 0) { + if (errno != EINPROGRESS) { *eno = errno; close(fd); return -1; } + int w = wait_fd(fd, POLLOUT, dl, stopfd); + if (w) { *eno = w == -1 ? ETIMEDOUT : ECANCELED; close(fd); return -1; } + int e = 0; + socklen_t el = sizeof e; + getsockopt(fd, SOL_SOCKET, SO_ERROR, &e, &el); + if (e) { *eno = e; close(fd); return -1; } + } + return fd; +} + +// --------------------------------------------------------------------------------------------- +// Hostname resolution (IPv4): at most RES_MAX_THREADS short-lived detached lookups, deadline-bounded for the waiter, +// results cached (fresh for 60 s; last good answer reused for up to 10 min when re-resolution fails). +// --------------------------------------------------------------------------------------------- +#define RES_MAX_THREADS 4 +#define RES_DEADLINE_MS 5000 +#define RES_TTL_MS 60000 +#define RES_STALE_MS 600000 +#define RES_CACHE_N 64 + +struct resreq { + struct resreq *next; + char name[256]; + int refs; + bool done; + int gai; + struct in_addr addr; + pthread_cond_t cv; +}; +struct rescache { char name[256]; struct in_addr addr; int64_t when; bool used; }; + +static pthread_mutex_t res_mu = PTHREAD_MUTEX_INITIALIZER; +static struct resreq *res_inflight; +static int res_threads; +static struct rescache res_cache[RES_CACHE_N]; + +static struct rescache *res_cache_find(const char *name) // res_mu held +{ + for (int i = 0; i < RES_CACHE_N; i++) if (res_cache[i].used && !strcmp(res_cache[i].name, name)) return &res_cache[i]; + return NULL; +} + +static void res_unref(struct resreq *r) // res_mu held +{ + if (--r->refs == 0) { pthread_cond_destroy(&r->cv); free(r); } +} + +static void *res_thread(void *arg) +{ + struct resreq *r = arg; + struct addrinfo hints, *res = NULL; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + int gr = getaddrinfo(r->name, NULL, &hints, &res); + pthread_mutex_lock(&res_mu); + if (gr == 0 && res && res->ai_family == AF_INET) { + r->addr = ((struct sockaddr_in *)res->ai_addr)->sin_addr; + struct rescache *c = res_cache_find(r->name); + if (!c) { // reuse a free slot, else evict the oldest + c = &res_cache[0]; + for (int i = 0; i < RES_CACHE_N; i++) { + if (!res_cache[i].used) { c = &res_cache[i]; break; } + if (res_cache[i].when < c->when) c = &res_cache[i]; + } + } + snprintf(c->name, sizeof c->name, "%s", r->name); + c->addr = r->addr; c->when = now_ms(); c->used = true; + } else { + r->gai = gr ? gr : EAI_NONAME; + } + r->done = true; + for (struct resreq **pp = &res_inflight; *pp; pp = &(*pp)->next) + if (*pp == r) { *pp = r->next; break; } + res_threads--; + pthread_cond_broadcast(&r->cv); + res_unref(r); + pthread_mutex_unlock(&res_mu); + if (res) freeaddrinfo(res); + return NULL; +} + +static int resolve_v4(const char *name, int64_t dl, int stopfd, struct in_addr *out, char *err, size_t errn) +{ + int64_t limit = now_ms() + RES_DEADLINE_MS; + if (dl > limit) dl = limit; + pthread_mutex_lock(&res_mu); + struct rescache *c = res_cache_find(name); + if (c && now_ms() - c->when < RES_TTL_MS) { *out = c->addr; pthread_mutex_unlock(&res_mu); return 0; } + struct resreq *r; + for (r = res_inflight; r; r = r->next) if (!strcmp(r->name, name)) break; + const char *why = NULL; + if (!r) { + if (res_threads >= RES_MAX_THREADS) { + why = "resolver busy"; + } else { + r = calloc(1, sizeof *r); + pthread_condattr_t ca; + pthread_condattr_init(&ca); + pthread_condattr_setclock(&ca, CLOCK_MONOTONIC); + if (r) pthread_cond_init(&r->cv, &ca); + pthread_condattr_destroy(&ca); + pthread_t th; + pthread_attr_t at; + pthread_attr_init(&at); + pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED); + pthread_attr_setstacksize(&at, 256 * 1024); + if (r) { + snprintf(r->name, sizeof r->name, "%s", name); + r->refs = 2; // thread + this waiter + r->next = res_inflight; res_inflight = r; + res_threads++; + if (pthread_create(&th, &at, res_thread, r) != 0) { + res_inflight = r->next; res_threads--; r->refs = 1; + res_unref(r); r = NULL; why = "cannot start resolver thread"; + } + } else why = "out of memory"; + pthread_attr_destroy(&at); + } + } else { + r->refs++; + } + int rc = -1; + if (r) { + while (!r->done) { + int64_t rem = dl - now_ms(); + if (rem <= 0) { why = "timed out"; break; } + struct pollfd sp = { .fd = stopfd, .events = POLLIN }; + if (stopfd >= 0 && poll(&sp, 1, 0) > 0) { why = "relay stopping"; break; } + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + int64_t ms = rem < 50 ? rem : 50; + ts.tv_nsec += ms * 1000000; + ts.tv_sec += ts.tv_nsec / 1000000000; ts.tv_nsec %= 1000000000; + pthread_cond_timedwait(&r->cv, &res_mu, &ts); + } + if (r->done && !r->gai) { *out = r->addr; rc = 0; } + else if (r->done) { snprintf(err, errn, "cannot resolve '%s': %s", name, gai_strerror(r->gai)); why = NULL; } + res_unref(r); + } + if (rc != 0) { + c = res_cache_find(name); // the lookup may have refreshed it; else try a stale answer + if (c && now_ms() - c->when < RES_STALE_MS) { *out = c->addr; rc = 0; } + } + pthread_mutex_unlock(&res_mu); + if (rc != 0 && why) snprintf(err, errn, "cannot resolve '%s': %s", name, why); + return rc; +} + +// Resolves and connects to the proxy; on failure writes a readable reason to err. +static int connect_host(const char *host, uint16_t port, int64_t dl, int stopfd, char *err, size_t errn) +{ + struct sockaddr_storage ss[1]; + socklen_t sl[1]; + int n = 0; + struct in_addr a4; + struct in6_addr a6; + memset(ss, 0, sizeof ss); + if (inet_pton(AF_INET, host, &a4) == 1 || (inet_pton(AF_INET6, host, &a6) != 1 && resolve_v4(host, dl, stopfd, &a4, err, errn) == 0)) { + struct sockaddr_in *s = (struct sockaddr_in *)&ss[0]; + s->sin_family = AF_INET; s->sin_port = htons(port); s->sin_addr = a4; + sl[0] = sizeof *s; n = 1; + } else if (inet_pton(AF_INET6, host, &a6) == 1) { + struct sockaddr_in6 *s = (struct sockaddr_in6 *)&ss[0]; + s->sin6_family = AF_INET6; s->sin6_port = htons(port); s->sin6_addr = a6; + sl[0] = sizeof *s; n = 1; + } else { + return -1; // resolve_v4 filled err + } + int eno = ETIMEDOUT; + for (int i = 0; i < n; i++) { + int64_t rem = dl - now_ms(); + int64_t d = (i + 1 < n && rem > 2) ? now_ms() + rem / 2 : dl; + int fd = connect_addr((struct sockaddr *)&ss[i], sl[i], d, stopfd, &eno); + if (fd >= 0) return fd; + if (eno == ECANCELED) break; + } + snprintf(err, errn, "connect to %s:%u failed: %s", host, port, strerror(eno)); + return -1; +} + +// --------------------------------------------------------------------------------------------- +// SOCKS5 / HTTP CONNECT handshakes +// --------------------------------------------------------------------------------------------- +struct s5dst { uint8_t atyp; uint8_t addr[16]; char host[256]; uint16_t port; }; +struct s5bnd { uint8_t atyp; uint8_t addr[16]; uint16_t port; }; + +static const char *socks5_rep_str(int rep) +{ + switch (rep) { + case 1: return "general SOCKS server failure"; + case 2: return "connection not allowed by ruleset"; + case 3: return "network unreachable"; + case 4: return "host unreachable"; + case 5: return "connection refused by target"; + case 6: return "TTL expired"; + case 7: return "command not supported"; + case 8: return "address type not supported"; + default: return "unknown error"; + } +} + +static void s5dst_from_orig(struct s5dst *d, const struct pb_orig *o) +{ + memset(d, 0, sizeof *d); + d->port = o->port; + if (o->family == AF_INET) { d->atyp = 1; memcpy(d->addr, o->addr + 12, 4); } + else { d->atyp = 4; memcpy(d->addr, o->addr, 16); } +} + +// cmd: 1 = CONNECT, 3 = UDP ASSOCIATE +static int socks5_request(struct io *x, const struct pb_proxy *p, uint8_t cmd, const struct s5dst *d, struct s5bnd *b) +{ + bool auth = p->user[0] != 0; + uint8_t g[4] = { 5, auth ? 2 : 1, 0, 2 }; + if (io_send(x, g, auth ? 4 : 3)) return -1; + uint8_t r[2]; + if (io_recv_exact(x, r, 2)) return -1; + if (r[0] != 5) { seterr(x, "not a SOCKS5 server (bad greeting reply 0x%02x)", r[0]); return -1; } + if (r[1] == 0xFF) { seterr(x, "SOCKS5 proxy rejected all auth methods (credentials required or not accepted)"); return -1; } + if (r[1] == 2) { + size_t ul = strlen(p->user), pl = strlen(p->pass); + if (!auth) { seterr(x, "SOCKS5 proxy requires username/password but none configured"); return -1; } + if (ul > 255 || pl > 255) { seterr(x, "SOCKS5 credentials too long"); return -1; } + uint8_t ab[3 + 255 + 255]; + size_t o = 0; + ab[o++] = 1; ab[o++] = (uint8_t)ul; memcpy(ab + o, p->user, ul); o += ul; + ab[o++] = (uint8_t)pl; memcpy(ab + o, p->pass, pl); o += pl; + if (io_send(x, ab, o)) return -1; + if (io_recv_exact(x, r, 2)) return -1; + if (r[1] != 0) { seterr(x, "SOCKS5 authentication failed (wrong username or password)"); return -1; } + } else if (r[1] != 0) { + seterr(x, "SOCKS5 proxy selected unsupported auth method 0x%02x", r[1]); + return -1; + } + uint8_t rq[4 + 1 + 255 + 2]; + size_t o = 0; + rq[o++] = 5; rq[o++] = cmd; rq[o++] = 0; rq[o++] = d->atyp; + if (d->atyp == 1) { memcpy(rq + o, d->addr, 4); o += 4; } + else if (d->atyp == 4) { memcpy(rq + o, d->addr, 16); o += 16; } + else { + size_t hl = strlen(d->host); + if (hl == 0 || hl > 255) { seterr(x, "invalid target host name"); return -1; } + rq[o++] = (uint8_t)hl; memcpy(rq + o, d->host, hl); o += hl; + } + rq[o++] = (uint8_t)(d->port >> 8); rq[o++] = (uint8_t)d->port; + if (io_send(x, rq, o)) return -1; + uint8_t h[4]; + if (io_recv_exact(x, h, 4)) return -1; + if (h[0] != 5) { seterr(x, "bad SOCKS5 reply version 0x%02x", h[0]); return -1; } + if (h[1] != 0) { seterr(x, "SOCKS5 request failed: %s (code %u)", socks5_rep_str(h[1]), h[1]); return -1; } + struct s5bnd tmp; + if (!b) b = &tmp; + memset(b, 0, sizeof *b); + b->atyp = h[3]; + size_t al; + if (h[3] == 1) al = 4; + else if (h[3] == 4) al = 16; + else if (h[3] == 3) { + uint8_t l; + if (io_recv_exact(x, &l, 1)) return -1; + uint8_t skip[256]; + if (io_recv_exact(x, skip, l)) return -1; + al = 0; + } else { seterr(x, "SOCKS5 reply has unknown address type %u", h[3]); return -1; } + if (al && io_recv_exact(x, b->addr, al)) return -1; + uint8_t pt[2]; + if (io_recv_exact(x, pt, 2)) return -1; + b->port = (uint16_t)(pt[0] << 8 | pt[1]); + return 0; +} + +static void b64(const char *in, size_t n, char *out) +{ + static const char t[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i, o = 0; + for (i = 0; i + 2 < n; i += 3) { + uint32_t v = (uint8_t)in[i] << 16 | (uint8_t)in[i + 1] << 8 | (uint8_t)in[i + 2]; + out[o++] = t[v >> 18]; out[o++] = t[(v >> 12) & 63]; out[o++] = t[(v >> 6) & 63]; out[o++] = t[v & 63]; + } + if (i < n) { + uint32_t v = (uint8_t)in[i] << 16 | (i + 1 < n ? (uint8_t)in[i + 1] << 8 : 0); + out[o++] = t[v >> 18]; out[o++] = t[(v >> 12) & 63]; + out[o++] = i + 1 < n ? t[(v >> 6) & 63] : '='; + out[o++] = '='; + } + out[o] = 0; +} + +// host: IP literal (v6 without brackets) or hostname. Bytes received after the response headers go to left/leftn. +static int http_connect(struct io *x, const struct pb_proxy *p, const char *host, uint16_t port, uint8_t *left, size_t *leftn) +{ + char hp[300]; + snprintf(hp, sizeof hp, strchr(host, ':') ? "[%s]:%u" : "%s:%u", host, port); + char req[1024 + 700]; + int n = snprintf(req, sizeof req, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n", hp, hp); + if (p->user[0]) { + char cred[520], enc[720]; + int cl = snprintf(cred, sizeof cred, "%s:%s", p->user, p->pass); + if (cl < 0 || cl >= (int)sizeof cred) { seterr(x, "HTTP proxy credentials too long"); return -1; } + b64(cred, (size_t)cl, enc); + n += snprintf(req + n, sizeof req - (size_t)n, "Proxy-Authorization: Basic %s\r\n", enc); + } + n += snprintf(req + n, sizeof req - (size_t)n, "\r\n"); + if (io_send(x, req, (size_t)n)) return -1; + + uint8_t buf[HDR_MAX]; + size_t tot = 0, hend = 0; + while (!hend) { + if (tot >= sizeof buf) { seterr(x, "HTTP proxy response headers too large"); return -1; } + ssize_t r = io_recv(x, buf + tot, sizeof buf - tot); + if (r < 0) return -1; + size_t from = tot >= 3 ? tot - 3 : 0; + tot += (size_t)r; + for (size_t i = from; i + 4 <= tot; i++) + if (!memcmp(buf + i, "\r\n\r\n", 4)) { hend = i + 4; break; } + } + int code = 0; + if (hend >= 12 && !memcmp(buf, "HTTP/", 5)) { + const uint8_t *sp = memchr(buf, ' ', hend); + if (sp && sp + 4 <= buf + hend) code = (sp[1] - '0') * 100 + (sp[2] - '0') * 10 + (sp[3] - '0'); + } + if (code < 200 || code > 299) { + if (code == 0) seterr(x, "invalid HTTP proxy response"); + else if (code == 407) seterr(x, "HTTP proxy authentication required (407): check username/password"); + else if (code == 403) seterr(x, "HTTP proxy forbids this connection (403)"); + else if (code >= 500) seterr(x, "HTTP proxy or upstream error (%d)", code); + else seterr(x, "HTTP proxy refused CONNECT (%d)", code); + return -1; + } + *leftn = tot - hend; + if (*leftn) memcpy(left, buf + hend, *leftn); + return 0; +} + +// --------------------------------------------------------------------------------------------- +// Relay state +// --------------------------------------------------------------------------------------------- +struct conn { + struct conn *prev, *next; + int fd[2]; // 0 = client, 1 = upstream + size_t off[2], len[2]; // pend[d]: unsent tail of bytes read from fd[d] (heap, freed once drained) + uint8_t *pend[2]; + bool dns; // original destination port 53: upstream->client is parsed as TCP-DNS + bool doff; // stopped observing (malformed stream) + uint8_t *dbuf; // bytes of the current, not yet complete, DNS message(s) + size_t dhave, dcap; + uint32_t mask[2]; + bool eof[2], shut[2], dead; + int64_t last; +}; + +static void conn_bufs_free(struct conn *c) +{ + free(c->pend[0]); free(c->pend[1]); free(c->dbuf); + c->pend[0] = c->pend[1] = c->dbuf = NULL; +} + +struct dthread { + pthread_t th; + bool started; + int ep, wake; + pthread_mutex_t mu; + struct conn *inq_head, *inq_tail; + struct conn *conns; // owned by thread + struct conn *grave; + uint8_t *scratch; + int pfd[2]; // reusable splice pipe; always empty between connections + bool nosplice; + atomic_int load; +}; + +enum { J_TCP = 1, J_UDP }; +struct sess; +struct job { + struct job *next; + int kind; + int fd; + struct pb_orig o; + struct sess *s; +}; + +struct flowent { uint8_t addr[16]; uint16_t port; uint32_t ip_be; int64_t last; }; +struct pkt { size_t len; uint8_t d[]; }; + +enum { SK_DIRECT = 1, SK_SOCKS5, SK_DROP }; +enum { ST_SETUP = 1, ST_READY, ST_FAILED }; +struct ref { struct sess *s; int which; }; + +struct sess { + struct sess *hnext, *prev, *next, *dnext; + struct sockaddr_in cli; + uint32_t id, flow_be; // id = proxy id or DIRECT_ID; flow_be only for DIRECT sessions + int kind, state; + int up_fd, ctl_fd; + int family; // DIRECT: family of original destination + bool connected; // DIRECT: v6 sockets are connected; v4 are not (replies from any port of the host are accepted) + struct sockaddr_storage dst; // DIRECT destination + socklen_t dstlen; + uint16_t dport; // DIRECT: original destination port (53 => DNS tap) + bool dead, ok, warned; + int64_t last, fail_until; + struct pb_proxy proxy; + char err[700]; + struct ref ref[2]; + int nflows; + struct flowent flows[MAX_FLOWS]; + struct pkt *q[UDP_QMAX]; + int nq; + size_t qbytes; +}; + +struct dce { // per-(client, flow ip) decision cache + bool used; + struct sockaddr_in cli; + uint32_t flow_be; + uint8_t addr[16]; + uint16_t port; + int action; + int64_t last; +}; + +static struct { + pthread_mutex_t life; + bool running; + struct pb_relay_cfg cfg; + int lfd, ufd, stopfd, tmfd, uep, uwake; + atomic_bool stopping; + atomic_int nconn; + atomic_uint rr; + uint16_t tcp_port, udp_port; + + pthread_t acceptor; bool acc_started; + pthread_t workers[NWORKERS]; int nworkers; + struct dthread data[MAX_DATA]; + int ndata; + pthread_t udpth; bool udp_started; + + pthread_mutex_t jmu; + pthread_cond_t jcv; + struct job *jhead, *jtail; + int jcount; + + pthread_mutex_t dmu; // UDP setup completion queue + struct sess *done; + + // UDP thread-confined state + struct sess *sess_head; + struct sess *buckets[SESS_BUCKETS]; + int nsess; + struct sess *sgrave; + struct dce *dc; + uint8_t *rxbuf, *txbuf; + int64_t drop_count, drop_logged; +} R = { .life = PTHREAD_MUTEX_INITIALIZER, .jmu = PTHREAD_MUTEX_INITIALIZER, .jcv = PTHREAD_COND_INITIALIZER, + .dmu = PTHREAD_MUTEX_INITIALIZER, .lfd = -1, .ufd = -1, .stopfd = -1, .tmfd = -1, .uep = -1, .uwake = -1 }; + +static void proxy_desc(const struct pb_proxy *p, char *out, size_t n) +{ + snprintf(out, n, "%s %s:%u", p->type == PROXY_TYPE_SOCKS5 ? "SOCKS5" : "HTTP", p->host, p->port); +} + +// --------------------------------------------------------------------------------------------- +// Job queue / worker pool +// --------------------------------------------------------------------------------------------- +static int job_push(int kind, int fd, const struct pb_orig *o, struct sess *s) +{ + struct job *j = calloc(1, sizeof *j); + if (!j) return -1; + j->kind = kind; j->fd = fd; j->s = s; + if (o) j->o = *o; + pthread_mutex_lock(&R.jmu); + if (R.jcount >= JOBQ_MAX || atomic_load(&R.stopping)) { + pthread_mutex_unlock(&R.jmu); + free(j); + return -1; + } + if (R.jtail) R.jtail->next = j; else R.jhead = j; + R.jtail = j; + R.jcount++; + pthread_cond_signal(&R.jcv); + pthread_mutex_unlock(&R.jmu); + return 0; +} + +static void conn_handoff(int cfd, int ufd, const uint8_t *left, size_t leftn, bool dns) +{ + struct conn *c = calloc(1, sizeof *c); + if (!c) { close(cfd); close(ufd); atomic_fetch_sub(&R.nconn, 1); return; } + c->fd[0] = cfd; c->fd[1] = ufd; + c->dns = dns; + if (leftn) { + uint8_t *m = malloc(leftn); + if (!m) { close(cfd); close(ufd); free(c); atomic_fetch_sub(&R.nconn, 1); return; } + memcpy(m, left, leftn); + if (dns) { c->dbuf = m; c->dhave = c->dcap = leftn; } // must pass through the DNS parser + else { c->pend[1] = m; c->len[1] = leftn; } + } + unsigned start = atomic_fetch_add(&R.rr, 1); + struct dthread *t = &R.data[start % R.ndata]; + for (int i = 1; i < R.ndata; i++) { // least-loaded, ties go to the round-robin choice + struct dthread *u = &R.data[(start + i) % R.ndata]; + if (atomic_load(&u->load) < atomic_load(&t->load)) t = u; + } + atomic_fetch_add(&t->load, 1); + pthread_mutex_lock(&t->mu); + if (atomic_load(&R.stopping)) { + pthread_mutex_unlock(&t->mu); + atomic_fetch_sub(&t->load, 1); + close(cfd); close(ufd); conn_bufs_free(c); free(c); + atomic_fetch_sub(&R.nconn, 1); + return; + } + if (t->inq_tail) t->inq_tail->next = c; else t->inq_head = c; + t->inq_tail = c; + pthread_mutex_unlock(&t->mu); + eventfd_write(t->wake, 1); +} + +static void abort_close(int fd) +{ + struct linger l = { 1, 0 }; + setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof l); + close(fd); +} + +static void tcp_job_run(struct job *j) +{ + struct pb_orig *o = &j->o; + int cfd = j->fd, up = -1, action = o->action; + uint32_t pid = o->proxy_id; + int64_t dl = now_ms() + (int64_t)R.cfg.handshake_timeout_s * 1000; + char dst[80], err[1024] = "", desc[300]; + uint8_t left[HDR_MAX]; + size_t leftn = 0; + orig_str(o, dst, sizeof dst); + + if (action == PB_ACT_DECIDE) { + struct pb_decision d; + pb_core_decide(o, &d); + action = d.action; pid = d.proxy_id; + } + if (action == PB_ACT_DIRECT) { + pb_core_flow_event(o, PB_ACT_DIRECT, "DIRECT"); + struct sockaddr_storage ss; + socklen_t sl = orig_to_sa(o, &ss); + int eno = 0; + up = connect_addr((struct sockaddr *)&ss, sl, dl, R.stopfd, &eno); + if (up < 0) snprintf(err, sizeof err, "direct connect to %s failed: %s", dst, strerror(eno)); + } else if (action == PB_ACT_PROXY) { + struct pb_proxy px; + if (pb_core_get_proxy(pid, &px) != 0) { + pb_core_flow_event(o, PB_ACT_PROXY, "unknown proxy"); + snprintf(err, sizeof err, "no proxy configured (id %u) for %s", pid, dst); + } else { + proxy_desc(&px, desc, sizeof desc); + pb_core_flow_event(o, PB_ACT_PROXY, desc); + char cerr[200]; + up = connect_host(px.host, px.port, dl, R.stopfd, cerr, sizeof cerr); + if (up < 0) { + snprintf(err, sizeof err, "proxy %s: %s", desc, cerr); + } else { + struct io x = { .fd = up, .dl = dl, .stopfd = R.stopfd }; + int rc; + if (px.type == PROXY_TYPE_SOCKS5) { + struct s5dst d; + s5dst_from_orig(&d, o); + rc = socks5_request(&x, &px, 1, &d, NULL); + } else { + char ip[INET6_ADDRSTRLEN]; + fmt_ip(o->family, o->addr, ip, sizeof ip); + rc = http_connect(&x, &px, ip, o->port, left, &leftn); + } + if (rc) { + snprintf(err, sizeof err, "proxy %s -> %s: %s", desc, dst, x.err); + close(up); up = -1; + } + } + } + } else { + pb_core_flow_event(o, PB_ACT_BLOCK, "BLOCK"); + abort_close(cfd); + atomic_fetch_sub(&R.nconn, 1); + return; + } + if (up < 0) { + pb_core_log("relay: %s", err); + close(cfd); + atomic_fetch_sub(&R.nconn, 1); + return; + } + conn_handoff(cfd, up, left, leftn, o->port == 53); +} + +static void udp_job_run(struct sess *s) +{ + int64_t dl = now_ms() + (int64_t)R.cfg.handshake_timeout_s * 1000; + char cerr[200], desc[300]; + int ctl = -1, up = -1; + proxy_desc(&s->proxy, desc, sizeof desc); + s->ok = false; + ctl = connect_host(s->proxy.host, s->proxy.port, dl, R.stopfd, cerr, sizeof cerr); + if (ctl < 0) { snprintf(s->err, sizeof s->err, "%s: %s", desc, cerr); goto out; } + struct io x = { .fd = ctl, .dl = dl, .stopfd = R.stopfd }; + struct s5dst d; + struct s5bnd b; + memset(&d, 0, sizeof d); + d.atyp = 1; + if (socks5_request(&x, &s->proxy, 3, &d, &b)) { snprintf(s->err, sizeof s->err, "%s UDP ASSOCIATE: %s", desc, x.err); goto out; } + if (b.atyp == 3) { snprintf(s->err, sizeof s->err, "%s: UDP relay address is a domain name (unsupported)", desc); goto out; } + struct sockaddr_storage ra; + socklen_t rl; + memset(&ra, 0, sizeof ra); + static const uint8_t zero[16]; + bool unspec = !memcmp(b.addr, zero, b.atyp == 1 ? 4 : 16); + if (unspec) { // use the address we reached the proxy at + rl = sizeof ra; + if (getpeername(ctl, (struct sockaddr *)&ra, &rl) < 0) { snprintf(s->err, sizeof s->err, "%s: getpeername: %s", desc, strerror(errno)); goto out; } + if (ra.ss_family == AF_INET) ((struct sockaddr_in *)&ra)->sin_port = htons(b.port); + else ((struct sockaddr_in6 *)&ra)->sin6_port = htons(b.port); + } else if (b.atyp == 1) { + struct sockaddr_in *a = (struct sockaddr_in *)&ra; + a->sin_family = AF_INET; a->sin_port = htons(b.port); memcpy(&a->sin_addr, b.addr, 4); + rl = sizeof *a; + } else { + struct sockaddr_in6 *a = (struct sockaddr_in6 *)&ra; + a->sin6_family = AF_INET6; a->sin6_port = htons(b.port); memcpy(&a->sin6_addr, b.addr, 16); + rl = sizeof *a; + } + up = socket(ra.ss_family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (up < 0 || connect(up, (struct sockaddr *)&ra, rl) < 0) { + snprintf(s->err, sizeof s->err, "%s: UDP relay socket: %s", desc, strerror(errno)); + goto out; + } + s->ctl_fd = ctl; s->up_fd = up; s->ok = true; + ctl = up = -1; +out: + close_fd(&ctl); + close_fd(&up); + pthread_mutex_lock(&R.dmu); + s->dnext = R.done; + R.done = s; + pthread_mutex_unlock(&R.dmu); + eventfd_write(R.uwake, 1); +} + +static void *worker_main(void *arg) +{ + (void)arg; + for (;;) { + pthread_mutex_lock(&R.jmu); + while (!R.jhead && !atomic_load(&R.stopping)) pthread_cond_wait(&R.jcv, &R.jmu); + if (atomic_load(&R.stopping)) { pthread_mutex_unlock(&R.jmu); return NULL; } + struct job *j = R.jhead; + R.jhead = j->next; + if (!R.jhead) R.jtail = NULL; + R.jcount--; + pthread_mutex_unlock(&R.jmu); + if (j->kind == J_TCP) tcp_job_run(j); else udp_job_run(j->s); + free(j); + } +} + +// --------------------------------------------------------------------------------------------- +// Acceptor +// --------------------------------------------------------------------------------------------- +static void *acceptor_main(void *arg) +{ + (void)arg; + int64_t last_cap_log = 0; + while (!atomic_load(&R.stopping)) { + struct pollfd p[2] = { { .fd = R.lfd, .events = POLLIN }, { .fd = R.stopfd, .events = POLLIN } }; + if (poll(p, 2, -1) < 0) { if (errno == EINTR) continue; break; } + if (p[1].revents) break; + for (int i = 0; i < 256; i++) { + struct sockaddr_in pa; + socklen_t pl = sizeof pa; + int fd = accept4(R.lfd, (struct sockaddr *)&pa, &pl, SOCK_NONBLOCK | SOCK_CLOEXEC); + if (fd < 0) { + if (errno == EMFILE || errno == ENFILE || errno == ENOBUFS || errno == ENOMEM) { + pb_core_log("relay: accept: %s, backing off", strerror(errno)); + struct pollfd s = { .fd = R.stopfd, .events = POLLIN }; + poll(&s, 1, 200); + } + break; // EAGAIN or error: back to poll + } + struct pb_orig o; + if (pb_core_take_tcp_orig(ntohs(pa.sin_port), &o) != 0) { + pb_core_log("relay: no original destination for client port %u, closing", ntohs(pa.sin_port)); + close(fd); + continue; + } + tune_sock(fd); + if (atomic_fetch_add(&R.nconn, 1) >= R.cfg.max_tcp_conns) { + atomic_fetch_sub(&R.nconn, 1); + int64_t t = now_ms(); + if (t - last_cap_log > 5000) { last_cap_log = t; pb_core_log("relay: connection limit (%d) reached, dropping new connections", R.cfg.max_tcp_conns); } + close(fd); + continue; + } + if (job_push(J_TCP, fd, &o, NULL) != 0) { + atomic_fetch_sub(&R.nconn, 1); + close(fd); + int64_t t = now_ms(); + if (t - last_cap_log > 5000) { last_cap_log = t; pb_core_log("relay: handshake queue full (%d), shedding new connections", JOBQ_MAX); } + } + } + } + return NULL; +} + +// --------------------------------------------------------------------------------------------- +// TCP data threads +// --------------------------------------------------------------------------------------------- +static void pipe_discard(int fd) +{ + char b[4096]; + while (read(fd, b, sizeof b) > 0) {} +} + +// Reads `n` bytes the pipe holds into a fresh heap tail for direction d. +static int spill_from_pipe(struct conn *c, int d, int pipe_rd, size_t n) +{ + c->pend[d] = malloc(n); + if (!c->pend[d]) { pipe_discard(pipe_rd); return -1; } + size_t got = 0; + while (got < n) { + ssize_t r = read(pipe_rd, c->pend[d] + got, n - got); + if (r <= 0) { if (r < 0 && errno == EINTR) continue; break; } + got += (size_t)r; + } + pipe_discard(pipe_rd); + c->off[d] = 0; + c->len[d] = got; + if (!got) { free(c->pend[d]); c->pend[d] = NULL; return -1; } + return 0; +} + +// Zero-copy forward of one chunk in direction d via the thread's pipe. Returns 1 progress, 0 nothing to do (EAGAIN), +// -1 fatal, -2 splice unsupported (use the copy path), 2 EOF. +static int pump_splice(struct dthread *t, struct conn *c, int d) +{ + int o = 1 - d; + ssize_t n = splice(c->fd[d], NULL, t->pfd[1], NULL, PIPE_SZ, SPLICE_F_NONBLOCK | SPLICE_F_MOVE); + if (n == 0) return 2; + if (n < 0) { + if (errno == EAGAIN || errno == EINTR) return 0; + if (errno == EINVAL || errno == ENOSYS) return -2; + return -1; + } + ssize_t w = splice(t->pfd[0], NULL, c->fd[o], NULL, (size_t)n, SPLICE_F_NONBLOCK | SPLICE_F_MOVE); + if (w < 0) { + if (errno != EAGAIN && errno != EINTR) { pipe_discard(t->pfd[0]); return -1; } + w = 0; + } + if (w < n && spill_from_pipe(c, d, t->pfd[0], (size_t)(n - w)) != 0) return -1; + return 1; +} + +// Upstream->client for TCP-DNS: only whole messages (after pb_core_dns_observe returned) are forwarded. +// Returns 1 progress, 0 none, -1 fatal. +static int dns_pump_up(struct conn *c, uint8_t *scratch) +{ + int progress = 0; + ssize_t n = recv(c->fd[1], scratch, SCRATCH_SZ, 0); + if (n < 0 && errno != EAGAIN && errno != EINTR) return -1; + if (n > 0) { + if (c->dhave + (size_t)n > c->dcap) { + size_t nc = c->dhave + (size_t)n; + uint8_t *nb = realloc(c->dbuf, nc); + if (!nb) return -1; + c->dbuf = nb; c->dcap = nc; + } + memcpy(c->dbuf + c->dhave, scratch, (size_t)n); + c->dhave += (size_t)n; + progress = 1; + } else if (n == 0) { + c->eof[1] = true; + progress = 1; + } + size_t fwd = 0; + if (c->doff) { + fwd = c->dhave; + } else { + while (c->dhave - fwd >= 2) { + size_t L = (size_t)(c->dbuf[fwd] << 8 | c->dbuf[fwd + 1]); + if (L == 0) { c->doff = true; fwd = c->dhave; break; } // malformed: relay raw from here on + if (c->dhave - fwd < 2 + L) break; + pb_core_dns_observe(c->dbuf + fwd + 2, L); + fwd += 2 + L; + } + if (c->eof[1]) fwd = c->dhave; // truncated trailing message: pass through unobserved + } + if (fwd) { + ssize_t w = send(c->fd[0], c->dbuf, fwd, MSG_NOSIGNAL); + if (w < 0) { + if (errno != EAGAIN && errno != EINTR) return -1; + w = 0; + } + if ((size_t)w < fwd) { + c->pend[1] = malloc(fwd - (size_t)w); + if (!c->pend[1]) return -1; + memcpy(c->pend[1], c->dbuf + w, fwd - (size_t)w); + c->off[1] = 0; c->len[1] = fwd - (size_t)w; + } + memmove(c->dbuf, c->dbuf + fwd, c->dhave - fwd); + c->dhave -= fwd; + progress = 1; + } + return progress; +} + +static int conn_pump(struct dthread *t, struct conn *c) +{ + uint8_t *scratch = t->scratch; + for (int iter = 0; iter < 16; iter++) { + bool progress = false; + for (int d = 0; d < 2; d++) { + int o = 1 - d; + if (c->dns && d == 1 && c->len[1] == 0 && (!c->eof[1] || c->dhave)) { + int r = dns_pump_up(c, scratch); + if (r < 0) return -1; + if (r) progress = true; + } else if (c->len[d] == 0 && !c->eof[d]) { + int sp = -2; + if (!t->nosplice && !c->dns) { + sp = pump_splice(t, c, d); + if (sp == -1) return -1; + if (sp == -2) t->nosplice = true; + else if (sp == 2) { c->eof[d] = true; progress = true; } + else if (sp == 1) progress = true; + } + if (sp == -2) { + ssize_t n = recv(c->fd[d], scratch, SCRATCH_SZ, 0); + if (n > 0) { + progress = true; + ssize_t w = send(c->fd[o], scratch, (size_t)n, MSG_NOSIGNAL); + if (w < 0) { + if (errno != EAGAIN && errno != EINTR) return -1; + w = 0; + } + if (w < n) { // spill the unsent tail; no more reads in this direction until drained + c->pend[d] = malloc((size_t)(n - w)); + if (!c->pend[d]) return -1; + memcpy(c->pend[d], scratch + w, (size_t)(n - w)); + c->off[d] = 0; + c->len[d] = (size_t)(n - w); + } + } + else if (n == 0) { c->eof[d] = true; progress = true; } + else if (errno != EAGAIN && errno != EINTR) return -1; + } + } else if (c->len[d] > 0) { + ssize_t n = send(c->fd[o], c->pend[d] + c->off[d], c->len[d], MSG_NOSIGNAL); + if (n > 0) { + c->off[d] += (size_t)n; c->len[d] -= (size_t)n; progress = true; + if (!c->len[d]) { free(c->pend[d]); c->pend[d] = NULL; } + } + else if (errno != EAGAIN && errno != EINTR) return -1; + } + if (c->len[d] == 0 && c->eof[d] && !c->shut[d]) { + shutdown(c->fd[o], SHUT_WR); + c->shut[d] = true; + progress = true; + } + } + if (progress) c->last = now_ms(); + if (c->shut[0] && c->shut[1]) return 1; + if (!progress) break; + } + return 0; +} + +static int conn_update(struct dthread *t, struct conn *c) +{ + for (int i = 0; i < 2; i++) { + uint32_t want = 0; + if (!c->eof[i] && c->len[i] == 0) want |= EPOLLIN; + if (c->len[1 - i] > 0) want |= EPOLLOUT; + if (want == c->mask[i]) continue; + struct epoll_event ev = { .events = want, .data.ptr = c }; + int rc; + if (!want) rc = epoll_ctl(t->ep, EPOLL_CTL_DEL, c->fd[i], NULL); + else rc = epoll_ctl(t->ep, c->mask[i] ? EPOLL_CTL_MOD : EPOLL_CTL_ADD, c->fd[i], &ev); + if (rc < 0) return -1; + c->mask[i] = want; + } + return 0; +} + +static void conn_close(struct dthread *t, struct conn *c) +{ + close(c->fd[0]); + close(c->fd[1]); + c->dead = true; + conn_bufs_free(c); + c->pend[0] = c->pend[1] = NULL; + if (c->prev) c->prev->next = c->next; else t->conns = c->next; + if (c->next) c->next->prev = c->prev; + c->next = t->grave; + c->prev = NULL; + t->grave = c; + atomic_fetch_sub(&R.nconn, 1); + atomic_fetch_sub(&t->load, 1); +} + +// Returns after pump + epoll mask refresh; closes the conn when done/failed. +static void conn_service(struct dthread *t, struct conn *c) +{ + int r = conn_pump(t, c); + if (r == 0 && conn_update(t, c) != 0) r = -1; + if (r != 0) conn_close(t, c); +} + +static void dthread_adopt(struct dthread *t) +{ + pthread_mutex_lock(&t->mu); + struct conn *c = t->inq_head; + t->inq_head = t->inq_tail = NULL; + pthread_mutex_unlock(&t->mu); + while (c) { + struct conn *n = c->next; + c->next = t->conns; c->prev = NULL; + if (t->conns) t->conns->prev = c; + t->conns = c; + c->last = now_ms(); + conn_service(t, c); + c = n; + } +} + +static void free_grave(struct dthread *t) +{ + while (t->grave) { struct conn *n = t->grave->next; free(t->grave); t->grave = n; } +} + +static void *data_main(void *arg) +{ + struct dthread *t = arg; + struct epoll_event evs[64]; + int64_t last_sweep = now_ms(); + int64_t sweep_every = (int64_t)R.cfg.tcp_idle_timeout_s * 250; // idle/4, clamped to 1s..5s + if (sweep_every < 1000) sweep_every = 1000; + if (sweep_every > 5000) sweep_every = 5000; + while (!atomic_load(&R.stopping)) { + int n = epoll_wait(t->ep, evs, 64, 1000); + if (n < 0 && errno != EINTR) break; + for (int i = 0; i < n; i++) { + void *p = evs[i].data.ptr; + if (p == &TAG_STOP) goto done; + if (p == &TAG_WAKE) { + eventfd_t v; + eventfd_read(t->wake, &v); + dthread_adopt(t); + } else if (!((struct conn *)p)->dead) { + conn_service(t, p); + } + } + free_grave(t); + int64_t now = now_ms(); + if (now - last_sweep >= sweep_every) { + last_sweep = now; + int64_t lim = (int64_t)R.cfg.tcp_idle_timeout_s * 1000; + for (struct conn *c = t->conns, *nx; c; c = nx) { + nx = c->next; + if (now - c->last > lim) conn_close(t, c); + } + free_grave(t); + } + } +done: + return NULL; +} + +// --------------------------------------------------------------------------------------------- +// UDP +// --------------------------------------------------------------------------------------------- +static void udp_droplog(const char *why) +{ + R.drop_count++; + int64_t t = now_ms(); + if (t - R.drop_logged > 5000) { + pb_core_log("relay: UDP datagram dropped (%s); %lld dropped so far", why, (long long)R.drop_count); + R.drop_logged = t; + } +} + +static uint32_t sess_hash(const struct sockaddr_in *c, uint32_t id, uint32_t flow) +{ + uint32_t h = 2166136261u; + uint32_t v[4] = { c->sin_addr.s_addr, c->sin_port, id, flow }; + for (int i = 0; i < 4; i++) { h ^= v[i]; h *= 16777619u; h ^= h >> 15; } + return h % SESS_BUCKETS; +} + +static struct sess *sess_find(const struct sockaddr_in *c, uint32_t id, uint32_t flow) +{ + for (struct sess *s = R.buckets[sess_hash(c, id, flow)]; s; s = s->hnext) + if (s->cli.sin_addr.s_addr == c->sin_addr.s_addr && s->cli.sin_port == c->sin_port && s->id == id && s->flow_be == flow) + return s; + return NULL; +} + +static void sess_free_queue(struct sess *s) +{ + for (int i = 0; i < s->nq; i++) free(s->q[i]); + s->nq = 0; s->qbytes = 0; +} + +static void sess_destroy(struct sess *s) +{ + close_fd(&s->up_fd); // closing removes from epoll + close_fd(&s->ctl_fd); + sess_free_queue(s); + uint32_t h = sess_hash(&s->cli, s->id, s->flow_be); + for (struct sess **pp = &R.buckets[h]; *pp; pp = &(*pp)->hnext) + if (*pp == s) { *pp = s->hnext; break; } + if (s->prev) s->prev->next = s->next; else R.sess_head = s->next; + if (s->next) s->next->prev = s->prev; + s->dead = true; + s->next = R.sgrave; + R.sgrave = s; + R.nsess--; +} + +static void sess_free_grave(void) +{ + while (R.sgrave) { struct sess *n = R.sgrave->next; free(R.sgrave); R.sgrave = n; } +} + +static struct sess *sess_new(const struct sockaddr_in *cli, uint32_t id, uint32_t flow, int kind) +{ + if (R.nsess >= MAX_UDP_SESS) return NULL; + struct sess *s = calloc(1, sizeof *s); + if (!s) return NULL; + s->cli = *cli; s->id = id; s->flow_be = flow; s->kind = kind; + s->up_fd = s->ctl_fd = -1; + s->ref[0].s = s->ref[1].s = s; + s->ref[0].which = 0; s->ref[1].which = 1; + s->last = now_ms(); + uint32_t h = sess_hash(cli, id, flow); + s->hnext = R.buckets[h]; R.buckets[h] = s; + s->next = R.sess_head; + if (R.sess_head) R.sess_head->prev = s; + R.sess_head = s; + R.nsess++; + return s; +} + +static void udp_reply(const struct sockaddr_in *cli, uint32_t flow_be, const uint8_t *data, size_t n) +{ + struct iovec iov = { (void *)data, n }; + union { char b[CMSG_SPACE(sizeof(struct in_pktinfo))]; struct cmsghdr a; } cb; + memset(&cb, 0, sizeof cb); + struct msghdr mh = { .msg_name = (void *)cli, .msg_namelen = sizeof *cli, .msg_iov = &iov, .msg_iovlen = 1, + .msg_control = cb.b, .msg_controllen = sizeof cb.b }; + struct cmsghdr *cm = CMSG_FIRSTHDR(&mh); + cm->cmsg_level = IPPROTO_IP; cm->cmsg_type = IP_PKTINFO; cm->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); + struct in_pktinfo pi; + memset(&pi, 0, sizeof pi); + pi.ipi_spec_dst.s_addr = flow_be; + memcpy(CMSG_DATA(cm), &pi, sizeof pi); + sendmsg(R.ufd, &mh, MSG_DONTWAIT | MSG_NOSIGNAL); +} + +static int sess_arm(struct sess *s, int which, uint32_t events) +{ + int fd = which ? s->ctl_fd : s->up_fd; + struct epoll_event ev = { .events = events, .data.ptr = &s->ref[which] }; + return epoll_ctl(R.uep, EPOLL_CTL_ADD, fd, &ev); +} + +static void flow_upsert(struct sess *s, const struct pb_orig *o, uint32_t flow_be) +{ + int victim = 0; + for (int i = 0; i < s->nflows; i++) { + if (s->flows[i].port == o->port && !memcmp(s->flows[i].addr, o->addr, 16)) { victim = -1; s->flows[i].ip_be = flow_be; s->flows[i].last = s->last; break; } + if (s->flows[i].last < s->flows[victim].last) victim = i; + } + if (victim < 0) return; + if (s->nflows < MAX_FLOWS) victim = s->nflows++; + memcpy(s->flows[victim].addr, o->addr, 16); + s->flows[victim].port = o->port; + s->flows[victim].ip_be = flow_be; + s->flows[victim].last = s->last; +} + +// Wraps the datagram with a SOCKS5 UDP header into R.txbuf; returns total length or 0. +static size_t socks5_wrap(const struct pb_orig *o, const uint8_t *pl, size_t n) +{ + size_t h = o->family == AF_INET ? 10 : 22; + if (n + h > UDP_MAXDG) return 0; + uint8_t *b = R.txbuf; + b[0] = b[1] = b[2] = 0; + if (o->family == AF_INET) { b[3] = 1; memcpy(b + 4, o->addr + 12, 4); } + else { b[3] = 4; memcpy(b + 4, o->addr, 16); } + b[h - 2] = (uint8_t)(o->port >> 8); b[h - 1] = (uint8_t)o->port; + memcpy(b + h, pl, n); + return n + h; +} + +static void udp_dgram(const struct sockaddr_in *cli, uint32_t flow_be, const uint8_t *pl, size_t n) +{ + struct pb_orig o; + if (pb_core_get_udp_flow(flow_be, &o) != 0) { udp_droplog("unknown flow address"); return; } + int64_t now = now_ms(); + uint32_t hv = (cli->sin_addr.s_addr * 2654435761u) ^ (cli->sin_port * 40503u) ^ (flow_be * 2246822519u); + struct dce *e = &R.dc[hv % DC_SIZE]; + uint32_t pid = o.proxy_id; + if (!(e->used && e->cli.sin_addr.s_addr == cli->sin_addr.s_addr && e->cli.sin_port == cli->sin_port && e->flow_be == flow_be && + e->port == o.port && !memcmp(e->addr, o.addr, 16) && now - e->last <= (int64_t)R.cfg.udp_idle_timeout_s * 1000)) { + int act = o.action; + if (act == PB_ACT_DECIDE) { + struct pb_decision d; + pb_core_decide(&o, &d); + act = d.action; pid = d.proxy_id; + } + char desc[300] = "BLOCK"; + if (act == PB_ACT_PROXY) { + struct pb_proxy px; + if (pb_core_get_proxy(pid, &px) != 0) { + pb_core_log("relay: UDP: no proxy configured (id %u), dropping", pid); + act = PB_ACT_BLOCK; + } else { + proxy_desc(&px, desc, sizeof desc); + if (px.type != PROXY_TYPE_SOCKS5) snprintf(desc + strlen(desc), sizeof desc - strlen(desc), " (UDP unsupported)"); + } + } else if (act == PB_ACT_DIRECT) { + strcpy(desc, "DIRECT"); + } else { + act = PB_ACT_BLOCK; + } + pb_core_flow_event(&o, act, desc); + e->used = true; e->cli = *cli; e->flow_be = flow_be; memcpy(e->addr, o.addr, 16); e->port = o.port; e->action = act; + } + e->last = now; + if (e->action == PB_ACT_BLOCK) return; + + struct sess *s; + if (e->action == PB_ACT_DIRECT) { + s = sess_find(cli, DIRECT_ID, flow_be); + if (!s) { + struct sockaddr_storage ss; + socklen_t sl = orig_to_sa(&o, &ss); + s = sess_new(cli, DIRECT_ID, flow_be, SK_DIRECT); + if (!s) { udp_droplog("UDP session limit reached"); return; } + s->family = o.family; + s->dst = ss; s->dstlen = sl; s->dport = o.port; + s->connected = o.family != AF_INET; + s->up_fd = socket(ss.ss_family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (s->up_fd < 0 || (s->connected && connect(s->up_fd, (struct sockaddr *)&ss, sl) < 0) || sess_arm(s, 0, EPOLLIN) < 0) { + pb_core_log("relay: UDP direct socket failed: %s", strerror(errno)); + sess_destroy(s); + return; + } + s->state = ST_READY; + } + s->last = now; + if (s->connected) send(s->up_fd, pl, n, MSG_DONTWAIT | MSG_NOSIGNAL); + else sendto(s->up_fd, pl, n, MSG_DONTWAIT | MSG_NOSIGNAL, (struct sockaddr *)&s->dst, s->dstlen); + return; + } + + // PROXY: re-resolve to get the canonical id and type + struct pb_proxy px; + if (pb_core_get_proxy(pid, &px) != 0) { udp_droplog("proxy vanished"); return; } + s = sess_find(cli, px.id, 0); + if (!s) { + s = sess_new(cli, px.id, 0, px.type == PROXY_TYPE_SOCKS5 ? SK_SOCKS5 : SK_DROP); + if (!s) { udp_droplog("UDP session limit reached"); return; } + s->proxy = px; + if (s->kind == SK_DROP) { + s->state = ST_READY; + } else { + s->state = ST_SETUP; + if (job_push(J_UDP, -1, NULL, s) != 0) { s->state = ST_FAILED; s->fail_until = now + 5000; udp_droplog("setup queue full"); } + } + } + s->last = now; + if (s->kind == SK_DROP) { + if (!s->warned) { + s->warned = true; + pb_core_log("relay: UDP cannot be proxied through HTTP proxy '%s'; dropping UDP from %s:%u", px.name, + inet_ntoa(cli->sin_addr), ntohs(cli->sin_port)); + } + return; + } + if (s->state == ST_FAILED) { udp_droplog("proxy UDP setup failed"); return; } + flow_upsert(s, &o, flow_be); + size_t wl = socks5_wrap(&o, pl, n); + if (!wl) { udp_droplog("datagram too large"); return; } + if (s->state == ST_SETUP) { + if (s->nq >= UDP_QMAX || s->qbytes + wl > UDP_QBYTES) { udp_droplog("setup queue full"); return; } + struct pkt *p = malloc(sizeof *p + wl); + if (!p) return; + p->len = wl; memcpy(p->d, R.txbuf, wl); + s->q[s->nq++] = p; s->qbytes += wl; + return; + } + send(s->up_fd, R.txbuf, wl, MSG_DONTWAIT | MSG_NOSIGNAL); +} + +static void udp_recv_client(void) +{ + for (int i = 0; i < 128; i++) { + struct sockaddr_in cli; + union { char b[CMSG_SPACE(sizeof(struct in_pktinfo))]; struct cmsghdr a; } cb; + struct iovec iov = { R.rxbuf, UDP_MAXDG }; + struct msghdr mh = { .msg_name = &cli, .msg_namelen = sizeof cli, .msg_iov = &iov, .msg_iovlen = 1, + .msg_control = cb.b, .msg_controllen = sizeof cb.b }; + ssize_t n = recvmsg(R.ufd, &mh, MSG_DONTWAIT); + if (n < 0) return; + if (mh.msg_flags & MSG_TRUNC) continue; + bool have = false; + struct in_pktinfo pi; + for (struct cmsghdr *c = CMSG_FIRSTHDR(&mh); c; c = CMSG_NXTHDR(&mh, c)) + if (c->cmsg_level == IPPROTO_IP && c->cmsg_type == IP_PKTINFO) { memcpy(&pi, CMSG_DATA(c), sizeof pi); have = true; } + if (!have || (ntohl(pi.ipi_addr.s_addr) >> 24) != 127) { udp_droplog("destination not in 127.0.0.0/8"); continue; } + udp_dgram(&cli, pi.ipi_addr.s_addr, R.rxbuf, (size_t)n); + } +} + +static void udp_recv_upstream(struct sess *s) +{ + for (int i = 0; i < 64 && !s->dead; i++) { + struct sockaddr_in from; + socklen_t fl = sizeof from; + ssize_t n = recvfrom(s->up_fd, R.rxbuf, UDP_MAXDG, MSG_DONTWAIT, (struct sockaddr *)&from, &fl); + if (n < 0) return; // includes ECONNREFUSED from ICMP: ignore + s->last = now_ms(); + if (s->kind == SK_DIRECT) { + // unconnected v4 socket: accept replies from the destination host on any port (some servers answer from + // another port); anything from another host is dropped. + if (!s->connected && (fl < sizeof from || from.sin_addr.s_addr != ((struct sockaddr_in *)&s->dst)->sin_addr.s_addr)) { + udp_droplog("reply from unexpected host"); + continue; + } + if (s->dport == 53) pb_core_dns_observe(R.rxbuf, (size_t)n); + udp_reply(&s->cli, s->flow_be, R.rxbuf, (size_t)n); + continue; + } + const uint8_t *b = R.rxbuf; + if (n < 10 || b[2] != 0) continue; // fragments unsupported + size_t h; + uint8_t addr[16] = { 0 }; + bool known = true; + uint16_t port; + if (b[3] == 1) { h = 10; memset(addr + 10, 0xff, 2); memcpy(addr + 12, b + 4, 4); } + else if (b[3] == 4) { if (n < 22) continue; h = 22; memcpy(addr, b + 4, 16); } + else if (b[3] == 3) { h = 7 + (size_t)b[4]; if ((size_t)n < h) continue; known = false; } + else continue; + port = (uint16_t)(b[h - 2] << 8 | b[h - 1]); + // Source mapping: exact (ip,port) the client sent to; otherwise the same ip on another port is delivered from the + // flow address of the most recently used destination with that ip; any other source is dropped (and counted). + uint32_t flow = 0; + uint16_t fport = 0; + bool found = false; + if (known) { + int best = -1; + for (int k = 0; k < s->nflows; k++) { + if (memcmp(s->flows[k].addr, addr, 16)) continue; + if (s->flows[k].port == port) { best = k; break; } + if (best < 0 || s->flows[k].last > s->flows[best].last) best = k; + } + if (best >= 0) { flow = s->flows[best].ip_be; fport = s->flows[best].port; found = true; } + } else if (s->nflows == 1) { + flow = s->flows[0].ip_be; fport = s->flows[0].port; found = true; + } + if (!found) { udp_droplog("reply from unknown source"); continue; } + if (fport == 53) pb_core_dns_observe(b + h, (size_t)n - h); + udp_reply(&s->cli, flow, b + h, (size_t)n - h); + } +} + +static void udp_setup_done(void) +{ + pthread_mutex_lock(&R.dmu); + struct sess *l = R.done; + R.done = NULL; + pthread_mutex_unlock(&R.dmu); + while (l) { + struct sess *s = l; + l = s->dnext; + if (s->ok && sess_arm(s, 0, EPOLLIN) == 0 && sess_arm(s, 1, EPOLLIN | EPOLLRDHUP) == 0) { + s->state = ST_READY; + s->last = now_ms(); + for (int i = 0; i < s->nq; i++) send(s->up_fd, s->q[i]->d, s->q[i]->len, MSG_DONTWAIT | MSG_NOSIGNAL); + sess_free_queue(s); + } else { + if (s->ok) snprintf(s->err, sizeof s->err, "epoll_ctl: %s", strerror(errno)); + pb_core_log("relay: UDP associate failed: %s", s->err); + close_fd(&s->up_fd); + close_fd(&s->ctl_fd); + sess_free_queue(s); + s->state = ST_FAILED; + s->fail_until = now_ms() + 5000; + } + } +} + +static void udp_ctl_event(struct sess *s) +{ + char tmp[256]; + for (;;) { + ssize_t n = recv(s->ctl_fd, tmp, sizeof tmp, MSG_DONTWAIT); + if (n > 0) continue; + if (n < 0 && errno == EAGAIN) return; + if (n < 0 && errno == EINTR) continue; + break; + } + pb_core_log("relay: SOCKS5 UDP control connection closed, dropping session for %s:%u", inet_ntoa(s->cli.sin_addr), ntohs(s->cli.sin_port)); + sess_destroy(s); +} + +static void udp_sweep(void) +{ + int64_t now = now_ms(), lim = (int64_t)R.cfg.udp_idle_timeout_s * 1000; + for (struct sess *s = R.sess_head, *n; s; s = n) { + n = s->next; + if (s->state == ST_SETUP) continue; + if (s->state == ST_FAILED ? now >= s->fail_until : now - s->last > lim) sess_destroy(s); + } +} + +static void *udp_main(void *arg) +{ + (void)arg; + struct epoll_event evs[64]; + while (!atomic_load(&R.stopping)) { + int n = epoll_wait(R.uep, evs, 64, 1000); + if (n < 0 && errno != EINTR) break; + for (int i = 0; i < n; i++) { + void *p = evs[i].data.ptr; + if (p == &TAG_STOP) return NULL; + if (p == &TAG_UDP) udp_recv_client(); + else if (p == &TAG_WAKE) { eventfd_t v; eventfd_read(R.uwake, &v); udp_setup_done(); } + else if (p == &TAG_TIMER) { uint64_t v; if (read(R.tmfd, &v, sizeof v) > 0) udp_sweep(); } + else { + struct ref *r = p; + struct sess *s = r->s; + if (s->dead) continue; + if (r->which == 1) udp_ctl_event(s); else udp_recv_upstream(s); + } + } + sess_free_grave(); + } + return NULL; +} + +// --------------------------------------------------------------------------------------------- +// Start / stop +// --------------------------------------------------------------------------------------------- +static void teardown(void) +{ + atomic_store(&R.stopping, true); + if (R.stopfd >= 0) eventfd_write(R.stopfd, 1); + pthread_mutex_lock(&R.jmu); + pthread_cond_broadcast(&R.jcv); + pthread_mutex_unlock(&R.jmu); + if (R.acc_started) pthread_join(R.acceptor, NULL); + for (int i = 0; i < R.nworkers; i++) pthread_join(R.workers[i], NULL); + for (int i = 0; i < R.ndata; i++) if (R.data[i].started) pthread_join(R.data[i].th, NULL); + if (R.udp_started) pthread_join(R.udpth, NULL); + R.acc_started = R.udp_started = false; + R.nworkers = 0; + + // everything is single-threaded from here on + while (R.jhead) { + struct job *j = R.jhead; + R.jhead = j->next; + if (j->kind == J_TCP) { close(j->fd); atomic_fetch_sub(&R.nconn, 1); } + free(j); + } + R.jtail = NULL; R.jcount = 0; + for (int i = 0; i < R.ndata; i++) { + struct dthread *t = &R.data[i]; + for (struct conn *c = t->inq_head, *n; c; c = n) { n = c->next; close(c->fd[0]); close(c->fd[1]); conn_bufs_free(c); free(c); atomic_fetch_sub(&R.nconn, 1); } + t->inq_head = t->inq_tail = NULL; + for (struct conn *c = t->conns, *n; c; c = n) { n = c->next; close(c->fd[0]); close(c->fd[1]); conn_bufs_free(c); free(c); atomic_fetch_sub(&R.nconn, 1); } + t->conns = NULL; + free_grave(t); + free(t->scratch); t->scratch = NULL; + close_fd(&t->pfd[0]); close_fd(&t->pfd[1]); + t->started = false; + close_fd(&t->ep); + close_fd(&t->wake); + } + while (R.sess_head) sess_destroy(R.sess_head); + sess_free_grave(); + memset(R.buckets, 0, sizeof R.buckets); + R.done = NULL; + R.nsess = 0; + free(R.dc); R.dc = NULL; + free(R.rxbuf); R.rxbuf = NULL; + free(R.txbuf); R.txbuf = NULL; + close_fd(&R.lfd); + close_fd(&R.ufd); + close_fd(&R.tmfd); + close_fd(&R.uep); + close_fd(&R.uwake); + close_fd(&R.stopfd); +} + +static int add_ep(int ep, int fd, void *tag, uint32_t ev) +{ + struct epoll_event e = { .events = ev, .data.ptr = tag }; + return epoll_ctl(ep, EPOLL_CTL_ADD, fd, &e); +} + +int pb_relay_start(const struct pb_relay_cfg *cfg) +{ + pthread_mutex_lock(&R.life); + if (R.running) { pthread_mutex_unlock(&R.life); return 0; } + struct pb_relay_cfg c = { 0 }; + if (cfg) c = *cfg; + if (!c.tcp_port) c.tcp_port = PB_TCP_RELAY_PORT_DEFAULT; + if (!c.udp_port) c.udp_port = PB_UDP_RELAY_PORT_DEFAULT; + if (c.max_tcp_conns <= 0) c.max_tcp_conns = 32768; + if (c.tcp_idle_timeout_s <= 0) c.tcp_idle_timeout_s = 300; + if (c.udp_idle_timeout_s <= 0) c.udp_idle_timeout_s = 60; + if (c.handshake_timeout_s <= 0) c.handshake_timeout_s = 10; + R.cfg = c; + atomic_store(&R.stopping, false); + atomic_store(&R.nconn, 0); + R.drop_count = 0; R.drop_logged = 0; + const char *why = ""; + int one = 1; + long ncpu = sysconf(_SC_NPROCESSORS_ONLN); + R.ndata = ncpu < 2 ? 2 : (ncpu > MAX_DATA ? MAX_DATA : (int)ncpu); +#ifdef PB_RELAY_NDATA // benchmarking override + R.ndata = PB_RELAY_NDATA; +#endif + for (int i = 0; i < R.ndata; i++) { + memset(&R.data[i], 0, sizeof R.data[i]); + R.data[i].ep = R.data[i].wake = -1; + R.data[i].pfd[0] = R.data[i].pfd[1] = -1; + pthread_mutex_init(&R.data[i].mu, NULL); + } + + R.stopfd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + R.uwake = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (R.stopfd < 0 || R.uwake < 0) { why = "eventfd"; goto fail; } + + R.lfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (R.lfd < 0) { why = "tcp socket"; goto fail; } + setsockopt(R.lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(c.tcp_port), .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + if (bind(R.lfd, (struct sockaddr *)&a, sizeof a) < 0 || listen(R.lfd, 4096) < 0) { + pb_core_log("relay: cannot listen on 127.0.0.1:%u: %s", c.tcp_port, strerror(errno)); + goto fail_quiet; + } + + R.ufd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (R.ufd < 0) { why = "udp socket"; goto fail; } + if (setsockopt(R.ufd, SOL_SOCKET, SO_BINDTODEVICE, "lo", 3) < 0) + pb_core_log("relay: SO_BINDTODEVICE lo failed (%s); relying on destination address filtering", strerror(errno)); + int rb = 4 << 20; + setsockopt(R.ufd, SOL_SOCKET, SO_RCVBUF, &rb, sizeof rb); + if (setsockopt(R.ufd, IPPROTO_IP, IP_PKTINFO, &one, sizeof one) < 0) { why = "IP_PKTINFO"; goto fail; } + a.sin_port = htons(c.udp_port); + a.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(R.ufd, (struct sockaddr *)&a, sizeof a) < 0) { + pb_core_log("relay: cannot bind UDP port %u: %s", c.udp_port, strerror(errno)); + goto fail_quiet; + } + R.tcp_port = c.tcp_port; R.udp_port = c.udp_port; + + R.uep = epoll_create1(EPOLL_CLOEXEC); + R.tmfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + R.dc = calloc(DC_SIZE, sizeof *R.dc); + R.rxbuf = malloc(UDP_MAXDG); + R.txbuf = malloc(UDP_MAXDG + 32); + if (R.uep < 0 || R.tmfd < 0 || !R.dc || !R.rxbuf || !R.txbuf) { why = "udp resources"; goto fail; } + struct itimerspec its = { .it_interval = { 1, 0 }, .it_value = { 1, 0 } }; + if (timerfd_settime(R.tmfd, 0, &its, NULL) < 0 || + add_ep(R.uep, R.ufd, &TAG_UDP, EPOLLIN) || add_ep(R.uep, R.stopfd, &TAG_STOP, EPOLLIN) || + add_ep(R.uep, R.uwake, &TAG_WAKE, EPOLLIN) || add_ep(R.uep, R.tmfd, &TAG_TIMER, EPOLLIN)) { why = "udp epoll"; goto fail; } + + for (int i = 0; i < R.ndata; i++) { + struct dthread *t = &R.data[i]; + t->scratch = malloc(SCRATCH_SZ); + if (pipe2(t->pfd, O_CLOEXEC | O_NONBLOCK) == 0) fcntl(t->pfd[0], F_SETPIPE_SZ, PIPE_SZ); + else t->pfd[0] = t->pfd[1] = -1, t->nosplice = true; +#ifdef PB_RELAY_COPY_ONLY // benchmarking override + t->nosplice = true; +#endif + t->ep = epoll_create1(EPOLL_CLOEXEC); + t->wake = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (!t->scratch || t->ep < 0 || t->wake < 0 || add_ep(t->ep, t->wake, &TAG_WAKE, EPOLLIN) || add_ep(t->ep, R.stopfd, &TAG_STOP, EPOLLIN)) { + why = "tcp epoll"; goto fail; + } + } + pthread_attr_t at; + pthread_attr_init(&at); + pthread_attr_setstacksize(&at, 512 * 1024); + bool ok = true; + for (int i = 0; i < R.ndata && ok; i++) { + ok = pthread_create(&R.data[i].th, &at, data_main, &R.data[i]) == 0; + R.data[i].started = ok; + } + for (int i = 0; i < NWORKERS && ok; i++) { + ok = pthread_create(&R.workers[i], &at, worker_main, NULL) == 0; + if (ok) R.nworkers++; + } + if (ok) { ok = pthread_create(&R.udpth, &at, udp_main, NULL) == 0; R.udp_started = ok; } + if (ok) { ok = pthread_create(&R.acceptor, &at, acceptor_main, NULL) == 0; R.acc_started = ok; } + pthread_attr_destroy(&at); + if (!ok) { why = "pthread_create"; goto fail; } + R.running = true; + pthread_mutex_unlock(&R.life); + return 0; + +fail: + pb_core_log("relay: start failed (%s): %s", why, strerror(errno)); +fail_quiet: + teardown(); + pthread_mutex_unlock(&R.life); + return -1; +} + +void pb_relay_stop(void) +{ + pthread_mutex_lock(&R.life); + if (R.running) { + teardown(); + R.running = false; + } + pthread_mutex_unlock(&R.life); +} + +uint16_t pb_relay_tcp_port(void) { return R.tcp_port; } +uint16_t pb_relay_udp_port(void) { return R.udp_port; } + +// --------------------------------------------------------------------------------------------- +// Synchronous proxy test +// --------------------------------------------------------------------------------------------- +int pb_relay_test_proxy(const struct pb_proxy *proxy, const char *host, uint16_t port, char *buf, size_t buflen) +{ + if (!buf || !buflen) return -1; + int64_t dl = now_ms() + 10000; + int fd = -1, rc = -1; + char err[256] = ""; + uint8_t left[HDR_MAX]; + size_t leftn = 0; + fd = connect_host(proxy->host, proxy->port, dl, -1, err, sizeof err); + if (fd < 0) { snprintf(buf, buflen, "Cannot reach proxy: %s", err); return -1; } + struct io x = { .fd = fd, .dl = dl, .stopfd = -1 }; + if (proxy->type == PROXY_TYPE_SOCKS5) { + struct s5dst d; + memset(&d, 0, sizeof d); + d.port = port; + struct in_addr a4; struct in6_addr a6; + if (inet_pton(AF_INET, host, &a4) == 1) { d.atyp = 1; memcpy(d.addr, &a4, 4); } + else if (inet_pton(AF_INET6, host, &a6) == 1) { d.atyp = 4; memcpy(d.addr, &a6, 16); } + else { d.atyp = 3; snprintf(d.host, sizeof d.host, "%s", host); } + rc = socks5_request(&x, proxy, 1, &d, NULL); + } else { + rc = http_connect(&x, proxy, host, port, left, &leftn); + } + if (rc) { snprintf(buf, buflen, "%s", x.err); close(fd); return -1; } + if (port != 80) { + snprintf(buf, buflen, "Tunnel established"); + close(fd); + return 0; + } + char req[600]; + int n = snprintf(req, sizeof req, "HEAD / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", host); + char resp[1024]; + size_t got = 0; + if (io_send(&x, req, (size_t)n)) { snprintf(buf, buflen, "%s", x.err); close(fd); return -1; } + if (leftn) { got = leftn < sizeof resp ? leftn : sizeof resp - 1; memcpy(resp, left, got); } + while (!memchr(resp, '\n', got) && got < sizeof resp - 1) { + ssize_t r = io_recv(&x, resp + got, sizeof resp - 1 - got); + if (r < 0) break; + got += (size_t)r; + } + close(fd); + resp[got] = 0; + char *nl = strpbrk(resp, "\r\n"); + if (nl) *nl = 0; + if (!resp[0]) { snprintf(buf, buflen, "No response through proxy: %s", x.err); return -1; } + snprintf(buf, buflen, "%s", resp); + return 0; +} diff --git a/Linux/src/pb_rules.c b/Linux/src/pb_rules.c new file mode 100644 index 00000000..e6a4bdf0 --- /dev/null +++ b/Linux/src/pb_rules.c @@ -0,0 +1,446 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "pb_rules.h" + +static void set_err(char *err, size_t n, const char *fmt, ...) __attribute__((format(printf, 3, 4))); +static void set_err(char *err, size_t n, const char *fmt, ...) +{ + if (!err || !n) return; + va_list ap; + va_start(ap, fmt); + vsnprintf(err, n, fmt, ap); + va_end(ap); +} + +// ---- tokenising ------------------------------------------------------------------------------------------------------ + +// Splits on ';' or ',' and trims whitespace. Returns the number of tokens (<= max) or -1 if there are more than max. +static int split_list(const char *s, char tokens[][256], int max) +{ + int n = 0; + while (*s) { + while (*s == ';' || *s == ',' || isspace((unsigned char)*s)) s++; + if (!*s) break; + const char *e = s; + while (*e && *e != ';' && *e != ',') e++; + const char *t = e; + while (t > s && isspace((unsigned char)t[-1])) t--; + if (n >= max) return -1; + size_t len = (size_t)(t - s); + if (len >= 256) len = 255; + memcpy(tokens[n], s, len); + tokens[n][len] = 0; + n++; + s = e; + } + return n; +} + +// ---- glob ------------------------------------------------------------------------------------------------------------ + +bool pb_glob_match(const char *p, const char *s) +{ + const char *star = NULL, *ss = s; + while (*s) { + if (*p == '*') { star = p++; ss = s; } + else if (*p == '?' || *p == *s) { p++; s++; } + else if (star) { p = star + 1; s = ++ss; } + else return false; + } + while (*p == '*') p++; + return *p == 0; +} + +// ---- parsing helpers ------------------------------------------------------------------------------------------------- + +static bool parse_uint(const char *s, size_t len, unsigned max, unsigned *out) +{ + if (!len || len > 5) return false; + unsigned v = 0; + for (size_t i = 0; i < len; i++) { + if (!isdigit((unsigned char)s[i])) return false; + v = v * 10 + (unsigned)(s[i] - '0'); + } + if (v > max) return false; + *out = v; + return true; +} + +static bool parse_ipv4(const char *s, uint32_t *out) +{ + struct in_addr a; + // inet_pton rejects leading garbage / short forms, which is what we want for validation + if (inet_pton(AF_INET, s, &a) != 1) return false; + *out = ntohl(a.s_addr); + return true; +} + +// "10", "*", "1-5" -> inclusive octet range +static bool parse_octet(const char *s, size_t len, uint8_t *lo, uint8_t *hi) +{ + unsigned a, b; + if (len == 1 && s[0] == '*') { *lo = 0; *hi = 255; return true; } + const char *dash = memchr(s, '-', len); + if (dash) { + if (!parse_uint(s, (size_t)(dash - s), 255, &a) || !parse_uint(dash + 1, len - (size_t)(dash - s) - 1, 255, &b) || a > b) return false; + *lo = (uint8_t)a; *hi = (uint8_t)b; + return true; + } + if (!parse_uint(s, len, 255, &a)) return false; + *lo = *hi = (uint8_t)a; + return true; +} + +static bool v6_prefix_range(const uint8_t *base, unsigned prefix, uint8_t *lo, uint8_t *hi) +{ + if (prefix > 128) return false; + for (int i = 0; i < 16; i++) { + int bits = (int)prefix - i * 8; + uint8_t mask = bits >= 8 ? 0xff : bits <= 0 ? 0 : (uint8_t)(0xff << (8 - bits)); + lo[i] = base[i] & mask; + hi[i] = base[i] | (uint8_t)~mask; + } + return true; +} + +static bool looks_like_domain(const char *tok) +{ + // anything that is not purely IP-ish: contains a letter (other than none), '?' or a '*' attached to letters + for (const char *c = tok; *c; c++) + if (isalpha((unsigned char)*c) || *c == '_' || *c == '?') return true; + return false; +} + +static bool parse_domain_token(const char *tok, char *out, char *err, size_t errlen) +{ + size_t n = strlen(tok); + if (n == 0 || n > 253) { set_err(err, errlen, "invalid domain pattern '%s'", tok); return false; } + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)tok[i]; + if (!(isalnum(c) || c == '-' || c == '.' || c == '_' || c == '*' || c == '?')) { + set_err(err, errlen, "invalid character '%c' in domain pattern '%s'", c, tok); + return false; + } + out[i] = (char)tolower(c); + } + out[n] = 0; + if (out[0] == '.' || out[n - 1] == '.' || strstr(out, "..")) { set_err(err, errlen, "invalid domain pattern '%s'", tok); return false; } + return true; +} + +bool pb_domain_match(const char *pattern, const char *name) +{ + char lower[256]; + size_t n = strlen(name); + if (n >= sizeof lower) return false; + for (size_t i = 0; i <= n; i++) lower[i] = (char)tolower((unsigned char)name[i]); + while (n && lower[n - 1] == '.') lower[--n] = 0; // tolerate a trailing root dot + return pb_glob_match(pattern, lower); +} + +static bool parse_host_token(const char *tok, struct pb_host_pat *p, char *err, size_t errlen) +{ + memset(p, 0, sizeof *p); + if (strchr(tok, ':')) { // ---- IPv6 + char buf[256]; + snprintf(buf, sizeof buf, "%s", tok); + char *slash = strchr(buf, '/'), *dash = strchr(buf, '-'); + struct in6_addr a, b; + p->kind = PB_HOST_V6_RANGE; + if (slash) { + *slash = 0; + unsigned pre; + if (inet_pton(AF_INET6, buf, &a) != 1 || !parse_uint(slash + 1, strlen(slash + 1), 128, &pre)) goto bad6; + v6_prefix_range(a.s6_addr, pre, p->lo6, p->hi6); + } else if (dash) { + *dash = 0; + if (inet_pton(AF_INET6, buf, &a) != 1 || inet_pton(AF_INET6, dash + 1, &b) != 1) goto bad6; + if (memcmp(a.s6_addr, b.s6_addr, 16) > 0) { set_err(err, errlen, "IPv6 range '%s' has start > end", tok); return false; } + memcpy(p->lo6, a.s6_addr, 16); memcpy(p->hi6, b.s6_addr, 16); + } else { + if (inet_pton(AF_INET6, buf, &a) != 1) goto bad6; + memcpy(p->lo6, a.s6_addr, 16); memcpy(p->hi6, a.s6_addr, 16); + } + return true; +bad6: + set_err(err, errlen, "invalid IPv6 address or range '%s'", tok); + return false; + } + + const char *slash = strchr(tok, '/'), *dash = strchr(tok, '-'); + uint32_t a, b; + char buf[256]; + if (slash) { // ---- IPv4 CIDR + snprintf(buf, sizeof buf, "%.*s", (int)(slash - tok), tok); + unsigned pre; + if (!parse_ipv4(buf, &a) || !parse_uint(slash + 1, strlen(slash + 1), 32, &pre)) goto bad4; + uint32_t mask = pre ? 0xffffffffu << (32 - pre) : 0; + p->kind = PB_HOST_V4_RANGE; p->lo4 = a & mask; p->hi4 = a | ~mask; + return true; + } + int dots_before = 0; + for (const char *c = tok; dash && c < dash; c++) dots_before += *c == '.'; + if (dash && dots_before == 3) { // ---- IPv4 a.b.c.d-e.f.g.h + snprintf(buf, sizeof buf, "%.*s", (int)(dash - tok), tok); + if (!parse_ipv4(buf, &a) || !parse_ipv4(dash + 1, &b)) goto bad4; + if (a > b) { set_err(err, errlen, "IPv4 range '%s' has start > end", tok); return false; } + p->kind = PB_HOST_V4_RANGE; p->lo4 = a; p->hi4 = b; + return true; + } + // ---- IPv4 with per-octet '*' or 'lo-hi' + const char *s = tok; + for (int i = 0; i < 4; i++) { + const char *e = strchr(s, '.'); + size_t len = e ? (size_t)(e - s) : strlen(s); + if ((i < 3) != (e != NULL)) goto bad4; + if (!parse_octet(s, len, &p->box_lo[i], &p->box_hi[i])) goto bad4; + s = e ? e + 1 : s + len; + } + // exact address or a contiguous prefix box collapses to a numeric range + { + int i = 0; + while (i < 4 && p->box_lo[i] == p->box_hi[i]) i++; // leading fixed octets + bool contiguous = true; + if (i < 4) { + for (int j = i + 1; j < 4; j++) + if (p->box_lo[j] != 0 || p->box_hi[j] != 255) contiguous = false; + } + if (contiguous) { + p->kind = PB_HOST_V4_RANGE; + p->lo4 = p->hi4 = 0; + for (int j = 0; j < 4; j++) { + p->lo4 = (p->lo4 << 8) | p->box_lo[j]; + p->hi4 = (p->hi4 << 8) | p->box_hi[j]; + } + } else { + p->kind = PB_HOST_V4_BOX; + } + } + return true; +bad4: + set_err(err, errlen, "invalid IPv4 address, range or pattern '%s'", tok); + return false; +} + +static bool parse_port_token(const char *tok, struct pb_port_range *r, char *err, size_t errlen) +{ + unsigned a, b; + const char *dash = strchr(tok, '-'); + if (dash) { + if (!parse_uint(tok, (size_t)(dash - tok), 65535, &a) || !parse_uint(dash + 1, strlen(dash + 1), 65535, &b) || a > b || a == 0) { + set_err(err, errlen, "invalid port range '%s'", tok); + return false; + } + } else { + if (!parse_uint(tok, strlen(tok), 65535, &a) || a == 0) { + set_err(err, errlen, "invalid port '%s' (1-65535)", tok); + return false; + } + b = a; + } + r->lo = (uint16_t)a; r->hi = (uint16_t)b; + return true; +} + +// ---- public: parse / free / clone ------------------------------------------------------------------------------------ + +void pb_rule_free(struct pb_rule *r) +{ + free(r->procs); free(r->hosts); free(r->ports); free(r->domains); + r->procs = NULL; r->hosts = NULL; r->ports = NULL; r->domains = NULL; + r->nprocs = r->nhosts = r->nports = r->ndomains = 0; +} + +bool pb_rule_clone(struct pb_rule *dst, const struct pb_rule *src) +{ + *dst = *src; + dst->procs = NULL; dst->hosts = NULL; dst->ports = NULL; dst->domains = NULL; + if (src->nprocs && !(dst->procs = malloc(sizeof(*dst->procs) * (size_t)src->nprocs))) goto oom; + if (src->nhosts && !(dst->hosts = malloc(sizeof(*dst->hosts) * (size_t)src->nhosts))) goto oom; + if (src->nports && !(dst->ports = malloc(sizeof(*dst->ports) * (size_t)src->nports))) goto oom; + if (src->ndomains && !(dst->domains = malloc(sizeof(*dst->domains) * (size_t)src->ndomains))) goto oom; + if (src->nprocs) memcpy(dst->procs, src->procs, sizeof(*dst->procs) * (size_t)src->nprocs); + if (src->nhosts) memcpy(dst->hosts, src->hosts, sizeof(*dst->hosts) * (size_t)src->nhosts); + if (src->nports) memcpy(dst->ports, src->ports, sizeof(*dst->ports) * (size_t)src->nports); + if (src->ndomains) memcpy(dst->domains, src->domains, sizeof(*dst->domains) * (size_t)src->ndomains); + return true; +oom: + pb_rule_free(dst); + return false; +} + +static bool is_any(const char *s) +{ + while (isspace((unsigned char)*s)) s++; + if (!*s) return true; + return s[0] == '*' && (s[1] == 0 || isspace((unsigned char)s[1])); +} + +bool pb_rule_parse(struct pb_rule *out, const char *process, const char *hosts, const char *ports, char *err, size_t errlen) +{ + memset(out, 0, sizeof *out); + if (!process) process = ""; + if (!hosts) hosts = ""; + if (!ports) ports = ""; + if (strlen(process) >= sizeof out->process_src || strlen(hosts) >= sizeof out->hosts_src || strlen(ports) >= sizeof out->ports_src) { + set_err(err, errlen, "pattern too long (max 511 characters)"); + return false; + } + snprintf(out->process_src, sizeof out->process_src, "%s", process); + snprintf(out->hosts_src, sizeof out->hosts_src, "%s", hosts); + snprintf(out->ports_src, sizeof out->ports_src, "%s", ports); + + char (*tok)[256] = malloc(sizeof(char[256]) * PB_MAX_PATTERNS); + if (!tok) { set_err(err, errlen, "out of memory"); return false; } + + // process + if (is_any(process)) out->any_proc = true; + else { + int n = split_list(process, tok, PB_MAX_PATTERNS); + if (n < 0) { set_err(err, errlen, "too many process patterns (max %d)", PB_MAX_PATTERNS); goto fail; } + out->procs = malloc(sizeof(char[256]) * (size_t)(n ? n : 1)); + if (!out->procs) goto oom; + for (int i = 0; i < n; i++) { + if (!strcmp(tok[i], "*")) { out->any_proc = true; continue; } + memcpy(out->procs[out->nprocs++], tok[i], 256); + } + if (out->any_proc) { free(out->procs); out->procs = NULL; out->nprocs = 0; } + } + + // hosts + if (is_any(hosts)) out->any_host = true; + else { + int n = split_list(hosts, tok, PB_MAX_PATTERNS); + if (n < 0) { set_err(err, errlen, "too many host patterns (max %d)", PB_MAX_PATTERNS); goto fail; } + out->hosts = calloc((size_t)(n ? n : 1), sizeof *out->hosts); + out->domains = malloc(sizeof(char[256]) * (size_t)(n ? n : 1)); + if (!out->hosts || !out->domains) goto oom; + for (int i = 0; i < n; i++) { + if (!strcmp(tok[i], "*")) { out->any_host = true; break; } + if (looks_like_domain(tok[i]) && !strchr(tok[i], ':')) { + if (!parse_domain_token(tok[i], out->domains[out->ndomains], err, errlen)) goto fail; + out->ndomains++; + continue; + } + if (!parse_host_token(tok[i], &out->hosts[out->nhosts], err, errlen)) goto fail; + out->nhosts++; + } + if (out->any_host) { free(out->hosts); free(out->domains); out->hosts = NULL; out->domains = NULL; out->nhosts = out->ndomains = 0; } + } + + // ports + if (is_any(ports)) out->any_port = true; + else { + int n = split_list(ports, tok, PB_MAX_PATTERNS); + if (n < 0) { set_err(err, errlen, "too many port patterns (max %d)", PB_MAX_PATTERNS); goto fail; } + out->ports = calloc((size_t)(n ? n : 1), sizeof *out->ports); + if (!out->ports) goto oom; + for (int i = 0; i < n; i++) { + if (!strcmp(tok[i], "*")) { out->any_port = true; break; } + if (!parse_port_token(tok[i], &out->ports[out->nports], err, errlen)) goto fail; + out->nports++; + } + if (out->any_port) { free(out->ports); out->ports = NULL; out->nports = 0; } + } + free(tok); + return true; +oom: + set_err(err, errlen, "out of memory"); +fail: + free(tok); + pb_rule_free(out); + return false; +} + +// ---- matching -------------------------------------------------------------------------------------------------------- + +bool pb_rule_match_process(const struct pb_rule *r, const char *exe_path) +{ + if (r->any_proc) return true; + if (!exe_path || !*exe_path) return false; + const char *base = strrchr(exe_path, '/'); + base = base ? base + 1 : exe_path; + for (int i = 0; i < r->nprocs; i++) + if (pb_glob_match(r->procs[i], base) || pb_glob_match(r->procs[i], exe_path)) return true; + return false; +} + +bool pb_rule_match_proto(const struct pb_rule *r, int ipproto) +{ + switch (r->protocol) { + case RULE_PROTOCOL_TCP: return ipproto == IPPROTO_TCP; + case RULE_PROTOCOL_UDP: return ipproto == IPPROTO_UDP; + default: return true; + } +} + +bool pb_rule_has_domains(const struct pb_rule *r) { return r->ndomains > 0; } + +bool pb_rule_match_dest(const struct pb_rule *r, int family, const uint8_t addr16[16], uint16_t port, + const char *const *names, int nnames) +{ + if (!r->any_port) { + bool ok = false; + for (int i = 0; i < r->nports && !ok; i++) ok = port >= r->ports[i].lo && port <= r->ports[i].hi; + if (!ok) return false; + } + if (r->any_host) return true; + for (int i = 0; i < r->nhosts; i++) { + const struct pb_host_pat *h = &r->hosts[i]; + if (family == AF_INET) { + uint32_t a = ((uint32_t)addr16[12] << 24) | ((uint32_t)addr16[13] << 16) | ((uint32_t)addr16[14] << 8) | addr16[15]; + if (h->kind == PB_HOST_V4_RANGE && a >= h->lo4 && a <= h->hi4) return true; + if (h->kind == PB_HOST_V4_BOX) { + bool ok = true; + for (int j = 0; j < 4 && ok; j++) { + uint8_t o = addr16[12 + j]; + ok = o >= h->box_lo[j] && o <= h->box_hi[j]; + } + if (ok) return true; + } + } else if (h->kind == PB_HOST_V6_RANGE) { + if (memcmp(addr16, h->lo6, 16) >= 0 && memcmp(addr16, h->hi6, 16) <= 0) return true; + } + } + for (int d = 0; d < r->ndomains; d++) + for (int k = 0; k < nnames; k++) + if (pb_domain_match(r->domains[d], names[k])) return true; + return false; +} + +bool pb_implicit_direct(int family, const uint8_t a[16], uint16_t port, int ipproto, bool dns_via_proxy) +{ + if (family == AF_INET) { + if (a[12] == 127) return true; // loopback + if (a[12] >= 224) return true; // multicast + reserved + broadcast + if (a[12] == 0) return true; // 0.0.0.0/8 + } else { + static const uint8_t lo[16] = { [15] = 1 }; + if (!memcmp(a, lo, 16)) return true; // ::1 + if (a[0] == 0xff) return true; // multicast + if (a[0] == 0xfe && (a[1] & 0xc0) == 0x80) return true; // link-local (needs scope id anyway) + } + if (ipproto == IPPROTO_UDP && (port == 67 || port == 68)) return true; // DHCP + if (port == 53 && !dns_via_proxy) return true; + return false; +} + +int pb_rules_eval(const struct pb_rule *rules, size_t n, const char *exe_path, int family, const uint8_t addr16[16], + uint16_t port, int ipproto, const char *const *names, int nnames) +{ + for (size_t i = 0; i < n; i++) { + const struct pb_rule *r = &rules[i]; + if (!r->enabled) continue; + if (!pb_rule_match_proto(r, ipproto)) continue; + if (!pb_rule_match_process(r, exe_path)) continue; + if (!pb_rule_match_dest(r, family, addr16, port, names, nnames)) continue; + return (int)i; + } + return -1; +} diff --git a/Linux/src/pb_rules.h b/Linux/src/pb_rules.h new file mode 100644 index 00000000..4c18df44 --- /dev/null +++ b/Linux/src/pb_rules.h @@ -0,0 +1,60 @@ +// Rule model: parsing/validation and the full userspace matcher (the reference semantics; the BPF side is a compiled subset). +#ifndef PB_RULES_H +#define PB_RULES_H + +#include "pb_internal.h" + +#define PB_MAX_PATTERNS 64 + +enum { PB_HOST_V4_RANGE = 1, PB_HOST_V4_BOX, PB_HOST_V6_RANGE }; + +struct pb_host_pat { + int kind; + uint32_t lo4, hi4; // PB_HOST_V4_RANGE, host order + uint8_t box_lo[4], box_hi[4]; // PB_HOST_V4_BOX: per-octet inclusive range (e.g. 192.168.*.* or 10.1-5.*.*) + uint8_t lo6[16], hi6[16]; // PB_HOST_V6_RANGE, network order +}; + +struct pb_port_range { uint16_t lo, hi; }; + +struct pb_rule { + uint32_t id; + bool enabled; + RuleProtocol protocol; + RuleAction action; + uint32_t proxy_id; // action == PROXY: 0 = default proxy + + char process_src[512], hosts_src[512], ports_src[512]; // original text, kept for the UI / export + + bool any_proc; + char (*procs)[256]; int nprocs; + bool any_host; + struct pb_host_pat *hosts; int nhosts; + char (*domains)[256]; int ndomains; // lower-case domain globs ("*.github.com", "example.org"), matched against DNS-learned names + bool any_port; + struct pb_port_range *ports; int nports; +}; + +// Parses and validates the three pattern strings into `out` (id/enabled/action/protocol/proxy_id are left to the caller). +// On failure returns false and writes a human readable reason to err. On success the caller must pb_rule_free(). +bool pb_rule_parse(struct pb_rule *out, const char *process, const char *hosts, const char *ports, char *err, size_t errlen); +void pb_rule_free(struct pb_rule *r); +bool pb_rule_clone(struct pb_rule *dst, const struct pb_rule *src); // deep copy + +bool pb_glob_match(const char *pattern, const char *str); +bool pb_rule_match_process(const struct pb_rule *r, const char *exe_path); +bool pb_rule_match_proto(const struct pb_rule *r, int ipproto); +// names/nnames: domain names the DNS tap has learned for this destination address (may be NULL/0). +bool pb_rule_match_dest(const struct pb_rule *r, int family, const uint8_t addr16[16], uint16_t port, + const char *const *names, int nnames); +bool pb_rule_has_domains(const struct pb_rule *r); +bool pb_domain_match(const char *pattern, const char *name); // case-insensitive; glob if it contains * or ?, else exact + +// Destinations that are never redirected (loopback, multicast, broadcast, DHCP, DNS when !dns_via_proxy). +bool pb_implicit_direct(int family, const uint8_t addr16[16], uint16_t port, int ipproto, bool dns_via_proxy); + +// First enabled matching rule wins. Returns the index of the matching rule or -1 (no rule: DIRECT). +int pb_rules_eval(const struct pb_rule *rules, size_t n, const char *exe_path, int family, const uint8_t addr16[16], + uint16_t port, int ipproto, const char *const *names, int nnames); + +#endif diff --git a/Linux/src/tests/dns_test.c b/Linux/src/tests/dns_test.c new file mode 100644 index 00000000..928b9cf6 --- /dev/null +++ b/Linux/src/tests/dns_test.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include +#include "../pb_dns.h" + +static int fails; +#define CHECK(c) do { if (!(c)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #c); fails++; } } while (0) + +// tiny DNS message builder ------------------------------------------------------------------------------------------- +struct msg { uint8_t b[2048]; size_t n; }; +static void w16(struct msg *m, unsigned v) { m->b[m->n++] = (uint8_t)(v >> 8); m->b[m->n++] = (uint8_t)v; } +static void w32(struct msg *m, unsigned v) { w16(m, v >> 16); w16(m, v & 0xffff); } +static size_t wname(struct msg *m, const char *name) +{ + size_t start = m->n; + while (*name) { + const char *dot = strchr(name, '.'); size_t l = dot ? (size_t)(dot - name) : strlen(name); + m->b[m->n++] = (uint8_t)l; memcpy(m->b + m->n, name, l); m->n += l; name += l + (dot ? 1 : 0); + } + m->b[m->n++] = 0; + return start; +} +static void ptr(struct msg *m, size_t at) { w16(m, 0xC000 | (unsigned)at); } +static void header(struct msg *m, unsigned flags, unsigned qd, unsigned an) { m->n = 0; w16(m, 0x1234); w16(m, flags); w16(m, qd); w16(m, an); w16(m, 0); w16(m, 0); } +static void a_rr(struct msg *m, size_t owner_off, const char *ip) { ptr(m, owner_off); w16(m, 1); w16(m, 1); w32(m, 60); w16(m, 4); struct in_addr a; inet_pton(AF_INET, ip, &a); memcpy(m->b + m->n, &a, 4); m->n += 4; } +static uint32_t H(const char *ip) { struct in_addr a; inet_pton(AF_INET, ip, &a); return ntohl(a.s_addr); } +static bool has(uint32_t ip, const char *name) { char n[8][256]; int c = pb_dns_lookup(ip, n, 8); for (int i = 0; i < c; i++) if (!strcmp(n[i], name)) return true; return false; } + +int main(void) +{ + struct msg m; struct pb_dns_result r; + + // 1. plain A answer with a compressed owner name + header(&m, 0x8180, 1, 2); size_t q = wname(&m, "www.Example.COM"); w16(&m, 1); w16(&m, 1); + a_rr(&m, q, "93.184.216.34"); a_rr(&m, q, "93.184.216.35"); + CHECK(pb_dns_observe(m.b, m.n, &r) == 2 && r.nips == 2); + CHECK(has(H("93.184.216.34"), "www.example.com") && has(H("93.184.216.35"), "www.example.com")); // lower-cased + + // 2. CNAME chain: www.shop.test -> cdn.edge.net -> A + header(&m, 0x8180, 1, 3); q = wname(&m, "www.shop.test"); w16(&m, 1); w16(&m, 1); + ptr(&m, q); w16(&m, 5); w16(&m, 1); w32(&m, 300); size_t lenpos = m.n; w16(&m, 0); size_t t = wname(&m, "cdn.edge.net"); w16(&m, 0); /* patch below */ + m.b[lenpos] = 0; m.b[lenpos + 1] = (uint8_t)(m.n - lenpos - 2 - 2); m.n -= 2; + a_rr(&m, t, "198.51.100.7"); + // fix an count (we wrote 3 but only 2 RRs) + m.b[7] = 2; + CHECK(pb_dns_observe(m.b, m.n, &r) == 1); + CHECK(has(H("198.51.100.7"), "www.shop.test") && has(H("198.51.100.7"), "cdn.edge.net")); + + // 3. rejects: query (QR=0), NXDOMAIN, no answers, loopback answers + header(&m, 0x0100, 1, 0); q = wname(&m, "x.test"); w16(&m, 1); w16(&m, 1); + CHECK(pb_dns_observe(m.b, m.n, &r) == 0); + header(&m, 0x8183, 1, 0); q = wname(&m, "x.test"); w16(&m, 1); w16(&m, 1); + CHECK(pb_dns_observe(m.b, m.n, &r) == 0); + header(&m, 0x8180, 1, 1); q = wname(&m, "local.test"); w16(&m, 1); w16(&m, 1); a_rr(&m, q, "127.0.0.9"); + pb_dns_observe(m.b, m.n, &r); CHECK(!has(H("127.0.0.9"), "local.test")); + + // 4. hostile input must not crash or loop: pointer loops, truncated records, random garbage + header(&m, 0x8180, 1, 1); m.b[m.n++] = 0xC0; m.b[m.n++] = 12; // name pointing at itself + w16(&m, 1); w16(&m, 1); CHECK(pb_dns_observe(m.b, m.n, &r) == 0); + header(&m, 0x8180, 1, 1); q = wname(&m, "t.test"); w16(&m, 1); w16(&m, 1); ptr(&m, q); w16(&m, 1); w16(&m, 1); w32(&m, 1); w16(&m, 4); m.b[m.n++] = 1; // rdata truncated + CHECK(pb_dns_observe(m.b, m.n, &r) == 0); + srand(7); + for (int i = 0; i < 200000; i++) { + size_t n = (size_t)(rand() % 200); uint8_t g[200]; + for (size_t k = 0; k < n; k++) g[k] = (uint8_t)rand(); + if (n >= 12 && (i & 1)) { g[2] = 0x81; g[3] = 0x80; g[5] = 1; g[7] = (uint8_t)(1 + rand() % 3); } + pb_dns_observe(g, n, &r); + } + + // 5. bounded table: oldest mappings are evicted and reported + pb_dns_clear(); + int evicted = 0; + for (uint32_t i = 0; i < PB_DNS_MAX_ENTRIES + 500; i++) { + header(&m, 0x8180, 1, 1); q = wname(&m, "bulk.test"); w16(&m, 1); w16(&m, 1); + ptr(&m, q); w16(&m, 1); w16(&m, 1); w32(&m, 60); w16(&m, 4); uint32_t ip = 0x0A000000u + 0x010000u + i; m.b[m.n++] = (uint8_t)(ip >> 24); m.b[m.n++] = (uint8_t)(ip >> 16); m.b[m.n++] = (uint8_t)(ip >> 8); m.b[m.n++] = (uint8_t)ip; + pb_dns_observe(m.b, m.n, &r); evicted += r.nevicted; + } + CHECK(pb_dns_count() == PB_DNS_MAX_ENTRIES); CHECK(evicted == 500); + CHECK(!has(0x0A010000u, "bulk.test") && has(0x0A010000u + 600, "bulk.test")); + + // 6. /etc/hosts style file + pb_dns_clear(); + FILE *f = fopen("/tmp/pb_dns_test_hosts", "w"); fprintf(f, "# c\n127.0.0.1 localhost\n192.0.2.44 intranet.test wiki.Intranet.test # x\n::1 ip6\nbogus line\n"); fclose(f); + CHECK(pb_dns_load_hosts("/tmp/pb_dns_test_hosts", &r) == 1); + CHECK(has(H("192.0.2.44"), "intranet.test") && has(H("192.0.2.44"), "wiki.intranet.test")); + remove("/tmp/pb_dns_test_hosts"); + + printf(fails ? "DNS TESTS FAILED (%d)\n" : "dns tests passed\n", fails); + return fails != 0; +} diff --git a/Linux/src/tests/relay_test.c b/Linux/src/tests/relay_test.c new file mode 100644 index 00000000..51c35a05 --- /dev/null +++ b/Linux/src/tests/relay_test.c @@ -0,0 +1,1473 @@ +// Root-free loopback test harness for pb_relay.c. Fakes every pb_core_* function and runs in-process fake +// SOCKS5 / HTTP CONNECT proxies and echo servers. The kernel redirect is simulated (see README comments below): +// TCP: client bind()s a known source port, the fake pb_core_take_tcp_orig() maps that port to the destination. +// UDP: client sends to 127.x.y.z:udp_port, the fake pb_core_get_udp_flow() maps 127.x.y.z to the destination. +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../pb_internal.h" + +// --------------------------------------------------------------------------------------------- +// Fake core +// --------------------------------------------------------------------------------------------- +static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; +static struct pb_orig g_tcp[65536]; +static bool g_tcp_valid[65536]; +static struct { uint32_t ip_be; struct pb_orig o; bool used; } g_flows[64]; +static struct pb_proxy g_px[16]; +static bool g_px_used[16]; +static uint32_t g_default_px = 1; +static struct pb_decision g_decision = { PB_ACT_DIRECT, 0 }; +static atomic_int g_ev, g_ev_proxy, g_ev_direct, g_ev_block, g_decide_calls; +static char g_log[64][512]; +static int g_log_n; + +int pb_core_take_tcp_orig(uint16_t p, struct pb_orig *out) +{ + pthread_mutex_lock(&g_mu); + int ok = g_tcp_valid[p]; + if (ok) { *out = g_tcp[p]; g_tcp_valid[p] = false; } + pthread_mutex_unlock(&g_mu); + return ok ? 0 : -1; +} +int pb_core_get_udp_flow(uint32_t ip, struct pb_orig *out) +{ + int ok = 0; + pthread_mutex_lock(&g_mu); + for (int i = 0; i < 64; i++) + if (g_flows[i].used && g_flows[i].ip_be == ip) { *out = g_flows[i].o; ok = 1; break; } + pthread_mutex_unlock(&g_mu); + return ok ? 0 : -1; +} +void pb_core_decide(const struct pb_orig *o, struct pb_decision *out) +{ + (void)o; + atomic_fetch_add(&g_decide_calls, 1); + *out = g_decision; +} +int pb_core_get_proxy(uint32_t id, struct pb_proxy *out) +{ + if (id == 0) id = g_default_px; + if (id >= 16 || !g_px_used[id]) return -1; + *out = g_px[id]; + return 0; +} +void pb_core_log(const char *fmt, ...) +{ + char b[512]; + va_list ap; + va_start(ap, fmt); + vsnprintf(b, sizeof b, fmt, ap); + va_end(ap); + pthread_mutex_lock(&g_mu); + snprintf(g_log[g_log_n++ % 64], 512, "%s", b); + pthread_mutex_unlock(&g_mu); + if (getenv("VERBOSE")) fprintf(stderr, " [log] %s\n", b); +} +void pb_core_flow_event(const struct pb_orig *o, int action, const char *d) +{ + (void)o; (void)d; + atomic_fetch_add(&g_ev, 1); + if (action == PB_ACT_PROXY) atomic_fetch_add(&g_ev_proxy, 1); + else if (action == PB_ACT_DIRECT) atomic_fetch_add(&g_ev_direct, 1); + else atomic_fetch_add(&g_ev_block, 1); +} +bool pb_core_dns_via_proxy(void) { return false; } + +struct obs { int64_t t0, t1; size_t len; uint32_t h; }; +static struct obs g_obs[64]; +static atomic_int g_obs_n, g_obs_sleep; +static int64_t mono_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} +static uint32_t fnv(const uint8_t *p, size_t n) +{ + uint32_t h = 2166136261u; + while (n--) { h ^= *p++; h *= 16777619u; } + return h; +} +void pb_core_dns_observe(const uint8_t *m, size_t len) +{ + int64_t t0 = mono_ms(); + int sl = atomic_load(&g_obs_sleep); + if (sl) usleep((useconds_t)sl * 1000); + int i = atomic_fetch_add(&g_obs_n, 1); + if (i < 64) g_obs[i] = (struct obs){ t0, mono_ms(), len, fnv(m, len) }; +} + +static bool log_has(const char *s) +{ + bool r = false; + pthread_mutex_lock(&g_mu); + for (int i = 0; i < 64 && i < g_log_n; i++) if (strstr(g_log[i], s)) r = true; + pthread_mutex_unlock(&g_mu); + return r; +} +static void log_clear(void) { pthread_mutex_lock(&g_mu); g_log_n = 0; memset(g_log, 0, sizeof g_log); pthread_mutex_unlock(&g_mu); } +static void ev_clear(void) { g_ev = g_ev_proxy = g_ev_direct = g_ev_block = 0; } + +// --------------------------------------------------------------------------------------------- +// Test plumbing +// --------------------------------------------------------------------------------------------- +static int g_pass, g_fail; +#define CHECK(cond, ...) do { if (cond) { g_pass++; printf("PASS "); } else { g_fail++; printf("FAIL "); } \ + printf(__VA_ARGS__); printf("\n"); fflush(stdout); } while (0) + +static const uint16_t TPORT = 39011, UPORT = 39012; +static atomic_int g_next_port = 20000; + +static void fill_orig(struct pb_orig *o, const char *ip, uint16_t port, int proto, int action, uint32_t px) +{ + memset(o, 0, sizeof *o); + struct in_addr a; + inet_pton(AF_INET, ip, &a); + memset(o->addr + 10, 0xff, 2); + memcpy(o->addr + 12, &a, 4); + o->port = port; o->family = AF_INET; o->proto = (uint8_t)proto; o->action = (uint8_t)action; o->proxy_id = (uint16_t)px; +} +static void set_tcp(uint16_t src, const char *ip, uint16_t port, int action, uint32_t px) +{ + pthread_mutex_lock(&g_mu); + fill_orig(&g_tcp[src], ip, port, IPPROTO_TCP, action, px); + g_tcp_valid[src] = true; + pthread_mutex_unlock(&g_mu); +} +static int g_nflows; +static void set_flow(const char *flow_ip, const char *ip, uint16_t port, int action, uint32_t px) +{ + struct in_addr f; + inet_pton(AF_INET, flow_ip, &f); + pthread_mutex_lock(&g_mu); + int i; + for (i = 0; i < g_nflows; i++) if (g_flows[i].ip_be == f.s_addr) break; + if (i == g_nflows) g_nflows++; + g_flows[i].used = true; g_flows[i].ip_be = f.s_addr; + fill_orig(&g_flows[i].o, ip, port, IPPROTO_UDP, action, px); + pthread_mutex_unlock(&g_mu); +} +static void set_proxy_host(uint32_t id, const char *host) +{ + snprintf(g_px[id].host, sizeof g_px[id].host, "%s", host); +} +static void set_proxy(uint32_t id, ProxyType t, uint16_t port, const char *u, const char *p) +{ + memset(&g_px[id], 0, sizeof g_px[id]); + g_px[id].id = id; g_px[id].type = t; snprintf(g_px[id].name, 64, "px%u", id); + snprintf(g_px[id].host, sizeof g_px[id].host, "127.0.0.1"); + g_px[id].port = port; + if (u) snprintf(g_px[id].user, sizeof g_px[id].user, "%s", u); + if (p) snprintf(g_px[id].pass, sizeof g_px[id].pass, "%s", p); + g_px_used[id] = true; +} + +static int count_fds(void) +{ + int n = 0; + DIR *d = opendir("/proc/self/fd"); + while (readdir(d)) n++; + closedir(d); + return n - 3; // ., .., and the dir fd itself +} +static int64_t now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} +static void msleep(int ms) { usleep((useconds_t)ms * 1000); } + +// --------------------------------------------------------------------------------------------- +// Fake servers +// --------------------------------------------------------------------------------------------- +struct srvcfg { bool auth; char user[64], pass[64]; bool leftover; bool silent; }; +struct srv { int lfd; uint16_t port; void (*h)(int, const struct srvcfg *); struct srvcfg cfg; }; +struct hctx { int fd; struct srv *s; }; + +static atomic_int g_assoc_count, g_s5_udp_dgrams; + +static int rd_full(int fd, void *b, size_t n) +{ + uint8_t *p = b; + while (n) { ssize_t r = recv(fd, p, n, 0); if (r <= 0) return -1; p += r; n -= (size_t)r; } + return 0; +} +static int wr_full(int fd, const void *b, size_t n) +{ + const uint8_t *p = b; + while (n) { ssize_t r = send(fd, p, n, MSG_NOSIGNAL); if (r <= 0) return -1; p += r; n -= (size_t)r; } + return 0; +} + +struct pipe_arg { int a, b; }; +static void *pipe_dir(void *p) +{ + struct pipe_arg *x = p; + char *buf = malloc(262144); + ssize_t r; + while ((r = recv(x->a, buf, 262144, 0)) > 0) + if (wr_full(x->b, buf, (size_t)r)) break; + free(buf); + shutdown(x->b, SHUT_WR); + if (r < 0) shutdown(x->a, SHUT_RD); + return NULL; +} +static void pipe_both(int a, int b) +{ + struct pipe_arg x = { a, b }, y = { b, a }; + pthread_t t; + pthread_create(&t, NULL, pipe_dir, &x); + pipe_dir(&y); + pthread_join(t, NULL); +} + +static void echo_handler(int fd, const struct srvcfg *c) +{ + (void)c; + char buf[32768]; + ssize_t r; + while ((r = recv(fd, buf, sizeof buf, 0)) > 0) + if (wr_full(fd, buf, (size_t)r)) break; + shutdown(fd, SHUT_WR); +} + +static int connect_target(int atyp, const uint8_t *addr, const char *host, uint16_t port) +{ + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(port) }; + if (atyp == 1) memcpy(&a.sin_addr, addr, 4); + else if (atyp == 3) { if (!strcmp(host, "localhost") || !strcmp(host, "echo.test")) a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); else return -1; } + else return -1; + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + return fd; +} + +static void socks_udp_assoc(int tfd) +{ + atomic_fetch_add(&g_assoc_count, 1); + int u = socket(AF_INET, SOCK_DGRAM, 0); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + bind(u, (struct sockaddr *)&a, sizeof a); + socklen_t al = sizeof a; + getsockname(u, (struct sockaddr *)&a, &al); + uint8_t rep[10] = { 5, 0, 0, 1, 127, 0, 0, 1, (uint8_t)(ntohs(a.sin_port) >> 8), (uint8_t)ntohs(a.sin_port) }; + wr_full(tfd, rep, 10); + struct sockaddr_in cli = { 0 }; + bool have = false; + for (;;) { + struct pollfd p[2] = { { tfd, POLLIN, 0 }, { u, POLLIN, 0 } }; + if (poll(p, 2, 10000) < 0) break; + if (p[0].revents) { char b[64]; if (recv(tfd, b, sizeof b, 0) <= 0) break; } + if (p[1].revents & POLLIN) { + uint8_t b[70000]; + struct sockaddr_in from; + socklen_t fl = sizeof from; + ssize_t n = recvfrom(u, b, sizeof b, 0, (struct sockaddr *)&from, &fl); + if (n < 1) continue; + if (!have) { cli = from; have = true; } + if (from.sin_addr.s_addr == cli.sin_addr.s_addr && from.sin_port == cli.sin_port) { + if (n < 10 || b[3] != 1) continue; + struct sockaddr_in d = { .sin_family = AF_INET }; + memcpy(&d.sin_addr, b + 4, 4); memcpy(&d.sin_port, b + 8, 2); + sendto(u, b + 10, (size_t)n - 10, 0, (struct sockaddr *)&d, sizeof d); + atomic_fetch_add(&g_s5_udp_dgrams, 1); + } else { + uint8_t o[70020] = { 0, 0, 0, 1 }; + memcpy(o + 4, &from.sin_addr, 4); memcpy(o + 8, &from.sin_port, 2); + memcpy(o + 10, b, (size_t)n); + sendto(u, o, (size_t)n + 10, 0, (struct sockaddr *)&cli, sizeof cli); + } + } + } + close(u); +} + +static void socks_handler(int fd, const struct srvcfg *c) +{ + if (c->silent) { char b[16]; while (recv(fd, b, sizeof b, 0) > 0) {} return; } + uint8_t h[2], m[256]; + if (rd_full(fd, h, 2) || h[0] != 5 || rd_full(fd, m, h[1])) return; + bool has0 = false, has2 = false; + for (int i = 0; i < h[1]; i++) { if (m[i] == 0) has0 = true; if (m[i] == 2) has2 = true; } + uint8_t sel[2] = { 5, 0xFF }; + if (c->auth && has2) sel[1] = 2; else if (!c->auth && has0) sel[1] = 0; + wr_full(fd, sel, 2); + if (sel[1] == 0xFF) return; + if (sel[1] == 2) { + uint8_t v, l; char u[256], p[256]; + if (rd_full(fd, &v, 1) || rd_full(fd, &l, 1) || rd_full(fd, u, l)) return; + u[l] = 0; + if (rd_full(fd, &l, 1) || rd_full(fd, p, l)) return; + p[l] = 0; + bool ok = !strcmp(u, c->user) && !strcmp(p, c->pass); + uint8_t r[2] = { 1, ok ? 0 : 1 }; + wr_full(fd, r, 2); + if (!ok) return; + } + uint8_t rq[4]; + if (rd_full(fd, rq, 4)) return; + uint8_t addr[16] = { 0 }; + char host[256] = ""; + if (rq[3] == 1) { if (rd_full(fd, addr, 4)) return; } + else if (rq[3] == 4) { if (rd_full(fd, addr, 16)) return; } + else if (rq[3] == 3) { uint8_t l; if (rd_full(fd, &l, 1) || rd_full(fd, host, l)) return; host[l] = 0; } + uint8_t pt[2]; + if (rd_full(fd, pt, 2)) return; + uint16_t port = (uint16_t)(pt[0] << 8 | pt[1]); + if (rq[1] == 3) { socks_udp_assoc(fd); return; } + int t = connect_target(rq[3], addr, host, port); + uint8_t rep[10] = { 5, t < 0 ? 5 : 0, 0, 1, 0, 0, 0, 0, 0, 0 }; + wr_full(fd, rep, 10); + if (t < 0) return; + pipe_both(fd, t); + close(t); +} + +static void http_handler(int fd, const struct srvcfg *c) +{ + char b[4096]; + size_t n = 0; + while (n < sizeof b - 1) { + ssize_t r = recv(fd, b + n, 1, 0); + if (r <= 0) return; + n++; + if (n >= 4 && !memcmp(b + n - 4, "\r\n\r\n", 4)) break; + } + b[n] = 0; + if (c->auth) { + // Basic base64("user:pass") computed by the test config: pass holds the expected token in user field? keep simple: + char want[200]; + snprintf(want, sizeof want, "Proxy-Authorization: Basic %s", c->pass); + if (!strstr(b, want)) { const char *r = "HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"; wr_full(fd, r, strlen(r)); return; } + } + char host[200]; unsigned port; + if (sscanf(b, "CONNECT %199[^:]:%u", host, &port) != 2) return; + struct in_addr a; + int t = -1; + if (inet_pton(AF_INET, host, &a) == 1) t = connect_target(1, (uint8_t *)&a, "", (uint16_t)port); + else t = connect_target(3, NULL, host, (uint16_t)port); + if (t < 0) { const char *r = "HTTP/1.1 502 Bad Gateway\r\n\r\n"; wr_full(fd, r, strlen(r)); return; } + const char *ok = c->leftover ? "HTTP/1.1 200 Connection established\r\nX: y\r\n\r\nLEFTOVER" : "HTTP/1.1 200 Connection established\r\n\r\n"; + wr_full(fd, ok, strlen(ok)); + pipe_both(fd, t); + close(t); +} + +static void *conn_thread(void *p) +{ + struct hctx *x = p; + x->s->h(x->fd, &x->s->cfg); + close(x->fd); + free(x); + return NULL; +} +static void *accept_thread(void *p) +{ + struct srv *s = p; + for (;;) { + int fd = accept(s->lfd, NULL, NULL); + if (fd < 0) { if (errno == EINTR) continue; return NULL; } + struct hctx *x = malloc(sizeof *x); + x->fd = fd; x->s = s; + pthread_t t; + pthread_create(&t, NULL, conn_thread, x); + pthread_detach(t); + } +} +static struct srv *srv_start_p(void (*h)(int, const struct srvcfg *), const struct srvcfg *cfg, uint16_t port) +{ + struct srv *s = calloc(1, sizeof *s); + s->h = h; + if (cfg) s->cfg = *cfg; + s->lfd = socket(AF_INET, SOCK_STREAM, 0); + int one = 1; + setsockopt(s->lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(port) }; + if (bind(s->lfd, (struct sockaddr *)&a, sizeof a) < 0) { close(s->lfd); free(s); return NULL; } + listen(s->lfd, 256); + socklen_t al = sizeof a; + getsockname(s->lfd, (struct sockaddr *)&a, &al); + s->port = ntohs(a.sin_port); + pthread_t t; + pthread_create(&t, NULL, accept_thread, s); + pthread_detach(t); + return s; +} +#define srv_start(h, c) srv_start_p(h, c, 0) + +struct udpecho { int fd; uint16_t port; }; +static void *udpecho_thread(void *p) +{ + struct udpecho *e = p; + char b[70000]; + for (;;) { + struct sockaddr_in f; + socklen_t fl = sizeof f; + ssize_t n = recvfrom(e->fd, b, sizeof b, 0, (struct sockaddr *)&f, &fl); + if (n < 0) { if (errno == EINTR) continue; return NULL; } + sendto(e->fd, b, (size_t)n, 0, (struct sockaddr *)&f, fl); + } +} +static uint16_t udpecho_start(void) +{ + struct udpecho *e = calloc(1, sizeof *e); + e->fd = socket(AF_INET, SOCK_DGRAM, 0); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + bind(e->fd, (struct sockaddr *)&a, sizeof a); + socklen_t al = sizeof a; + getsockname(e->fd, (struct sockaddr *)&a, &al); + e->port = ntohs(a.sin_port); + pthread_t t; + pthread_create(&t, NULL, udpecho_thread, e); + pthread_detach(t); + return e->port; +} + +// --------------------------------------------------------------------------------------------- +// Client helpers +// --------------------------------------------------------------------------------------------- +static int relay_connect(uint16_t src) +{ + int fd = socket(AF_INET, SOCK_STREAM, 0); + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(src) }; + if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + a.sin_port = htons(pb_relay_tcp_port()); + if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + struct timeval tv = { 30, 0 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + return fd; +} +static uint16_t new_src(void) { return (uint16_t)atomic_fetch_add(&g_next_port, 1); } + +static inline uint8_t pat(uint64_t i) { return (uint8_t)((i * 0x9E3779B1u) >> 11); } + +struct xf { int fd; uint64_t total; bool half; }; +static void *xf_send(void *p) +{ + struct xf *x = p; + uint8_t buf[65536]; + uint64_t off = 0; + while (off < x->total) { + size_t n = x->total - off > sizeof buf ? sizeof buf : (size_t)(x->total - off); + for (size_t j = 0; j < n; j++) buf[j] = pat(off + j); + if (wr_full(x->fd, buf, n)) break; + off += n; + } + if (x->half) shutdown(x->fd, SHUT_WR); + return NULL; +} +// Sends `total` patterned bytes and verifies the echo. Returns bytes verified, -1 on mismatch. +static int64_t xfer(int fd, uint64_t total, bool half) +{ + struct xf x = { fd, total, half }; + pthread_t t; + pthread_create(&t, NULL, xf_send, &x); + uint8_t buf[65536]; + uint64_t got = 0; + int64_t res = 0; + for (;;) { + if (!half && got >= total) break; + ssize_t r = recv(fd, buf, sizeof buf, 0); + if (r <= 0) break; + for (ssize_t j = 0; j < r; j++) if (buf[j] != pat(got + (uint64_t)j)) { res = -1; goto out; } + got += (uint64_t)r; + } + res = (int64_t)got; +out: + if (res < 0) shutdown(fd, SHUT_RDWR); + pthread_join(t, NULL); + return res; +} +// True when the peer closes (EOF or reset) within timeout_ms with no data. +static bool expect_closed(int fd, int timeout_ms) +{ + struct pollfd p = { fd, POLLIN, 0 }; + if (poll(&p, 1, timeout_ms) <= 0) return false; + char b[64]; + ssize_t r = recv(fd, b, sizeof b, 0); + return r == 0 || (r < 0 && errno != EAGAIN); +} + +static int relay_up(int tcp_idle, int udp_idle, int maxc, int hs) +{ + struct pb_relay_cfg c = { TPORT, UPORT, maxc, tcp_idle, udp_idle, hs }; + return pb_relay_start(&c); +} + +static uint16_t free_port(void) +{ + int fd = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + bind(fd, (struct sockaddr *)&a, sizeof a); + socklen_t al = sizeof a; + getsockname(fd, (struct sockaddr *)&a, &al); + close(fd); + return ntohs(a.sin_port); +} + +// One echo round trip of `n` bytes through the relay for the given action. +static int64_t tcp_roundtrip(const char *dst_ip, uint16_t dst_port, int action, uint32_t px, uint64_t n, bool half) +{ + uint16_t src = new_src(); + set_tcp(src, dst_ip, dst_port, action, px); + int fd = relay_connect(src); + if (fd < 0) return -2; + int64_t r = xfer(fd, n, half); + close(fd); + return r; +} + +// --------------------------------------------------------------------------------------------- +// UDP client helpers +// --------------------------------------------------------------------------------------------- +static int udp_client(void) +{ + int fd = socket(AF_INET, SOCK_DGRAM, 0); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + bind(fd, (struct sockaddr *)&a, sizeof a); + struct timeval tv = { 1, 0 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + return fd; +} +static void udp_send_flow(int fd, const char *flow_ip, const char *msg) +{ + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(pb_relay_udp_port()) }; + inet_pton(AF_INET, flow_ip, &a.sin_addr); + sendto(fd, msg, strlen(msg), 0, (struct sockaddr *)&a, sizeof a); +} +// Receives one datagram; returns length or -1; src_ip filled with the sender address. +static int udp_recv(int fd, char *buf, size_t n, char *src_ip, uint16_t *src_port) +{ + struct sockaddr_in f; + socklen_t fl = sizeof f; + ssize_t r = recvfrom(fd, buf, n - 1, 0, (struct sockaddr *)&f, &fl); + if (r < 0) return -1; + buf[r] = 0; + inet_ntop(AF_INET, &f.sin_addr, src_ip, 32); + *src_port = ntohs(f.sin_port); + return (int)r; +} +static bool udp_echo_ok(int fd, const char *flow_ip, const char *msg) +{ + char b[256], ip[32]; + uint16_t sp; + udp_send_flow(fd, flow_ip, msg); + int r = udp_recv(fd, b, sizeof b, ip, &sp); + return r == (int)strlen(msg) && !strcmp(b, msg) && !strcmp(ip, flow_ip) && sp == pb_relay_udp_port(); +} + +// --------------------------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------------------------- +static struct srv *g_echo, *g_s5, *g_s5auth, *g_http, *g_httpauth, *g_httpleft, *g_silent; +static uint16_t g_uecho1, g_uecho2; + +static void *conc_thread(void *p) +{ + int idx = (int)(intptr_t)p; + uint64_t mb = getenv("LARGE_MB") ? strtoull(getenv("LARGE_MB"), NULL, 10) : 8; + int mode = idx % 3; + int64_t r = tcp_roundtrip("127.0.0.1", g_echo->port, mode == 2 ? PB_ACT_DIRECT : PB_ACT_PROXY, mode == 0 ? 1 : (mode == 1 ? 3 : 0), + mb << 20, false); + return (void *)(intptr_t)(r == (int64_t)(mb << 20)); +} + +static void test_tcp_basic(void) +{ + ev_clear(); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 1, 100000, false) == 100000, "TCP via SOCKS5 (no auth)"); + CHECK(g_ev == 1 && g_ev_proxy == 1, "flow_event exactly once for proxied flow (got %d)", (int)g_ev); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 2, 100000, false) == 100000, "TCP via SOCKS5 (user/pass auth)"); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 3, 100000, false) == 100000, "TCP via HTTP CONNECT"); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 4, 100000, false) == 100000, "TCP via HTTP CONNECT with Basic auth"); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 0, 5000, false) == 5000, "TCP via default proxy (id 0)"); + ev_clear(); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 100000, false) == 100000, "TCP DIRECT"); + CHECK(g_ev == 1 && g_ev_direct == 1, "flow_event for direct"); + + // HTTP leftover bytes after the 200 response must reach the client first + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 5); + int fd = relay_connect(src); + char b[16] = { 0 }; + bool ok = fd >= 0 && rd_full(fd, b, 8) == 0 && !memcmp(b, "LEFTOVER", 8); + CHECK(ok, "HTTP CONNECT leftover bytes forwarded to client"); + if (fd >= 0) close(fd); + + // BLOCK + ev_clear(); + src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_BLOCK, 0); + fd = relay_connect(src); + send(fd, "x", 1, MSG_NOSIGNAL); + CHECK(fd >= 0 && expect_closed(fd, 3000), "TCP BLOCK closes client"); + CHECK(g_ev_block == 1, "flow_event BLOCK"); + close(fd); + + // DECIDE + g_decision = (struct pb_decision){ PB_ACT_DIRECT, 0 }; + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DECIDE, 0, 5000, false) == 5000, "TCP DECIDE -> DIRECT"); + g_decision = (struct pb_decision){ PB_ACT_PROXY, 1 }; + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DECIDE, 0, 5000, false) == 5000, "TCP DECIDE -> PROXY"); + g_decision = (struct pb_decision){ PB_ACT_BLOCK, 0 }; + src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_DECIDE, 0); + fd = relay_connect(src); + CHECK(fd >= 0 && expect_closed(fd, 3000), "TCP DECIDE -> BLOCK"); + close(fd); + + // no orig entry + fd = relay_connect(new_src()); + CHECK(fd >= 0 && expect_closed(fd, 3000), "TCP without original destination is closed"); + close(fd); + + // domain-name target through test helper is covered separately; IPv6 original destination (no v6 proxy server: direct to ::1) +} + +static void test_tcp_bad_proxy(void) +{ + log_clear(); + uint16_t dead = free_port(); + set_proxy(6, PROXY_TYPE_SOCKS5, dead, NULL, NULL); + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 6); + int fd = relay_connect(src); + CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("refused"), "bad proxy (connection refused): client closed + logged"); + close(fd); + + log_clear(); + set_proxy(7, PROXY_TYPE_SOCKS5, g_s5auth->port, "alice", "WRONG"); + src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 7); + fd = relay_connect(src); + CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("authentication failed"), "SOCKS5 wrong password: client closed + logged"); + close(fd); + + log_clear(); + set_proxy(8, PROXY_TYPE_HTTP, g_httpauth->port, "alice", "WRONG"); + src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 8); + fd = relay_connect(src); + CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("407"), "HTTP 407: client closed + logged"); + close(fd); + + log_clear(); + src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 12); // unknown proxy id + fd = relay_connect(src); + CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("no proxy configured"), "unknown proxy id: client closed + logged"); + close(fd); + + // dead target through a working proxy -> SOCKS5 REP 5 + log_clear(); + src = new_src(); + set_tcp(src, "127.0.0.1", dead, PB_ACT_PROXY, 1); + fd = relay_connect(src); + CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("connection refused by target"), "SOCKS5 REP code mapped in log"); + close(fd); +} + +static void sink_handler(int fd, const struct srvcfg *c) +{ + (void)c; + char *buf = malloc(262144); + uint64_t total = 0; + ssize_t r; + while ((r = recv(fd, buf, 262144, 0)) > 0) total += (uint64_t)r; + free(buf); + wr_full(fd, &total, 8); +} +static struct srv *g_sink; +struct thru { int action; uint32_t px; uint64_t bytes; int ok; }; +static void *thru_thread(void *p) +{ + struct thru *t = p; + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", g_sink->port, t->action, t->px); + int fd = relay_connect(src); + static const char z[65536]; + uint64_t sent = 0, total = 0; + while (fd >= 0 && sent < t->bytes) { if (wr_full(fd, z, sizeof z)) break; sent += sizeof z; } + if (fd >= 0) { shutdown(fd, SHUT_WR); t->ok = rd_full(fd, &total, 8) == 0 && total == sent && sent == t->bytes; close(fd); } + return NULL; +} +static void test_throughput(void) +{ + uint64_t mb = getenv("THRU_MB") ? strtoull(getenv("THRU_MB"), NULL, 10) : 256; + struct { const char *name; int action; uint32_t px; } m[] = { { "DIRECT via relay", PB_ACT_DIRECT, 0 }, { "SOCKS5 via relay + fake proxy", PB_ACT_PROXY, 1 } }; + for (int k = 0; k < 2; k++) { + enum { N = 8 }; + pthread_t th[N]; + struct thru t[N]; + int64_t t0 = now_ms(); + for (int i = 0; i < N; i++) { t[i] = (struct thru){ m[k].action, m[k].px, mb << 20, 0 }; pthread_create(&th[i], NULL, thru_thread, &t[i]); } + int ok = 0; + for (int i = 0; i < N; i++) { pthread_join(th[i], NULL); ok += t[i].ok; } + int64_t ms = now_ms() - t0; + CHECK(ok == N, "throughput %s: %d x %lluMB, %.0f MB/s aggregate (%lld ms)", m[k].name, N, (unsigned long long)mb, + (double)(N * mb) * 1000.0 / (double)(ms + 1), (long long)ms); + } +} + +static void test_tcp_halfclose(void) +{ + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 1, 300000, true) == 300000, "half-close via SOCKS5: all echoed then EOF"); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 3, 300000, true) == 300000, "half-close via HTTP CONNECT"); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 300000, true) == 300000, "half-close DIRECT"); +} + +static void test_tcp_large(void) +{ + uint64_t mb = getenv("LARGE_MB") ? strtoull(getenv("LARGE_MB"), NULL, 10) : 8; + int64_t t0 = now_ms(); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 1, 50ull << 20, false) == (int64_t)(50ull << 20), "50MB single transfer via SOCKS5, checksum ok (%lld ms, %.0f MB/s each way)", (long long)(now_ms() - t0), 50000.0 / (double)(now_ms() - t0 + 1)); + enum { N = 50 }; + pthread_t th[N]; + t0 = now_ms(); + for (int i = 0; i < N; i++) pthread_create(&th[i], NULL, conc_thread, (void *)(intptr_t)i); + int ok = 0; + for (int i = 0; i < N; i++) { void *r; pthread_join(th[i], &r); ok += (int)(intptr_t)r; } + CHECK(ok == N, "%d concurrent x %lluMB (socks5/http/direct mix) integrity: %d/%d ok (%lld ms)", N, (unsigned long long)mb, ok, N, (long long)(now_ms() - t0)); +} + +// Single-threaded SOCKS5 "hold" server: accepts any CONNECT and then just swallows data, so 10k connections cost +// the test process almost nothing and the RSS delta mostly reflects the relay. +static atomic_int g_hold_ready; +static void *hold_thread(void *arg) +{ + int lfd = (int)(intptr_t)arg; + enum { MAXFD = 1 << 18 }; + uint8_t *stage = calloc(MAXFD, 1); + int ep = epoll_create1(0); + struct epoll_event ev = { .events = EPOLLIN, .data.fd = lfd }; + epoll_ctl(ep, EPOLL_CTL_ADD, lfd, &ev); + for (;;) { + struct epoll_event evs[128]; + int n = epoll_wait(ep, evs, 128, -1); + for (int i = 0; i < n; i++) { + int fd = evs[i].data.fd; + if (fd == lfd) { + int c; + while ((c = accept4(lfd, NULL, NULL, SOCK_NONBLOCK)) >= 0) { + if (c >= MAXFD) { close(c); continue; } + stage[c] = 0; + struct epoll_event e = { .events = EPOLLIN, .data.fd = c }; + epoll_ctl(ep, EPOLL_CTL_ADD, c, &e); + } + continue; + } + uint8_t b[4096]; + ssize_t r; + if (stage[fd] == 0) { + r = recv(fd, b, 3, 0); + if (r == 3) { send(fd, "\5\0", 2, MSG_NOSIGNAL); stage[fd] = 1; continue; } + } else if (stage[fd] == 1) { + r = recv(fd, b, 10, 0); + if (r == 10) { + uint8_t rep[10] = { 5, 0, 0, 1 }; + send(fd, rep, 10, MSG_NOSIGNAL); + stage[fd] = 2; + atomic_fetch_add(&g_hold_ready, 1); + continue; + } + } else { + r = recv(fd, b, sizeof b, 0); + if (r > 0) continue; + } + if (r < 0 && errno == EAGAIN) continue; + close(fd); + } + } + return NULL; +} + +static long rss_kb(void) +{ + long pages = 0, rss = 0; + FILE *f = fopen("/proc/self/statm", "r"); + if (f) { if (fscanf(f, "%ld %ld", &pages, &rss) != 2) rss = 0; fclose(f); } + return rss * (sysconf(_SC_PAGESIZE) / 1024); +} + +static void test_tcp_10k(void) +{ + enum { N = 10000 }; + struct rlimit rl; + getrlimit(RLIMIT_NOFILE, &rl); + rl.rlim_cur = rl.rlim_max < 200000 ? rl.rlim_max : 200000; + setrlimit(RLIMIT_NOFILE, &rl); + int lfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); + int one = 1; + setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + bind(lfd, (struct sockaddr *)&a, sizeof a); + listen(lfd, 4096); + socklen_t al = sizeof a; + getsockname(lfd, (struct sockaddr *)&a, &al); + pthread_t t; + pthread_create(&t, NULL, hold_thread, (void *)(intptr_t)lfd); + pthread_detach(t); + set_proxy(10, PROXY_TYPE_SOCKS5, ntohs(a.sin_port), NULL, NULL); + + pb_relay_stop(); + CHECK(relay_up(300, 60, 0, 10) == 0, "restart relay with default limits for 10k test"); + msleep(300); + atomic_store(&g_next_port, 21000); + atomic_store(&g_hold_ready, 0); + int base_fds = count_fds(); + long rss0 = rss_kb(); + int *fds = malloc(N * sizeof *fds); + int opened = 0; + for (int i = 0; i < N; i++) { + fds[i] = -1; + for (int tries = 0; tries < 5 && fds[i] < 0; tries++) { + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", 9, PB_ACT_PROXY, 10); + fds[i] = relay_connect(src); + } + if (fds[i] >= 0) opened++; + if (i % 100 == 99) { // pace so the bounded handshake queue is not what we measure + int64_t t0 = now_ms(); + while (atomic_load(&g_hold_ready) < i - 200 && now_ms() - t0 < 30000) msleep(2); + } + } + int64_t t0 = now_ms(); + while (atomic_load(&g_hold_ready) < N && now_ms() - t0 < 60000) msleep(20); + msleep(500); + long rss1 = rss_kb(); + int alive = 0; + for (int i = 0; i < N; i += 100) { + struct pollfd p = { fds[i], POLLIN, 0 }; + if (fds[i] >= 0 && poll(&p, 1, 0) == 0) alive++; + } + CHECK(opened == N && g_hold_ready == N, "10000 idle connections established through SOCKS5 (opened=%d handshaken=%d)", opened, (int)g_hold_ready); + CHECK(alive == N / 100, "sampled idle connections still open (%d/%d)", alive, N / 100); + printf(" RSS before=%ld KB after=%ld KB delta=%ld MB\n", rss0, rss1, (rss1 - rss0) / 1024); + CHECK((rss1 - rss0) / 1024 < 100, "RSS delta for 10k idle connections < 100MB (%ld MB)", (rss1 - rss0) / 1024); + for (int i = 0; i < N; i++) if (fds[i] >= 0) close(fds[i]); + free(fds); + t0 = now_ms(); + while (count_fds() > base_fds + 1 && now_ms() - t0 < 20000) msleep(50); + CHECK(count_fds() <= base_fds + 1, "all fds released after closing 10k connections (base=%d now=%d)", base_fds, count_fds()); + pb_relay_stop(); + CHECK(relay_up(300, 60, 0, 5) == 0, "restart relay"); +} + +static void test_tcp_idle(void) +{ + pb_relay_stop(); + CHECK(relay_up(2, 60, 0, 5) == 0, "restart relay with tcp_idle=2s"); + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 1); + int fd = relay_connect(src); + send(fd, "hi", 2, 0); + char b[4]; + rd_full(fd, b, 2); + int64_t t0 = now_ms(); + bool closed = expect_closed(fd, 6000); + CHECK(closed && now_ms() - t0 < 5500, "idle TCP connection closed after ~2s (%lld ms)", (long long)(now_ms() - t0)); + close(fd); + + src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0); + fd = relay_connect(src); + bool alive = true; + for (int i = 0; i < 6 && alive; i++) { + send(fd, "p", 1, 0); + alive = rd_full(fd, b, 1) == 0; + msleep(700); + } + CHECK(alive, "active connection survives past idle timeout"); + close(fd); +} + +static void test_tcp_cap(void) +{ + pb_relay_stop(); + CHECK(relay_up(300, 60, 5, 5) == 0, "restart relay with max_tcp_conns=5"); + int fds[5]; + int up = 0; + for (int i = 0; i < 5; i++) { + uint16_t s = new_src(); + set_tcp(s, "127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0); + fds[i] = relay_connect(s); + char b[1]; + send(fds[i], "x", 1, 0); + if (rd_full(fds[i], b, 1) == 0) up++; + } + CHECK(up == 5, "5 connections within cap work"); + uint16_t s = new_src(); + set_tcp(s, "127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0); + int extra = relay_connect(s); + send(extra, "x", 1, MSG_NOSIGNAL); + CHECK(expect_closed(extra, 3000), "6th connection closed immediately (cap)"); + close(extra); + close(fds[0]); + msleep(500); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 1000, false) == 1000, "slot freed after close"); + for (int i = 1; i < 5; i++) close(fds[i]); + msleep(300); +} + +static void test_handshake_timeout(void) +{ + pb_relay_stop(); + CHECK(relay_up(300, 60, 0, 2) == 0, "restart relay with handshake_timeout=2s"); + log_clear(); + set_proxy(9, PROXY_TYPE_SOCKS5, g_silent->port, NULL, NULL); + uint16_t s = new_src(); + set_tcp(s, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 9); + int fd = relay_connect(s); + int64_t t0 = now_ms(); + bool closed = expect_closed(fd, 6000); + CHECK(closed && now_ms() - t0 < 4500 && log_has("timed out"), "silent proxy: handshake times out (%lld ms)", (long long)(now_ms() - t0)); + close(fd); +} + +static void test_udp(void) +{ + char b[256], ip[32]; + uint16_t sp; + int c = udp_client(); + set_flow("127.1.2.3", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 1); + set_flow("127.1.2.4", "127.0.0.1", g_uecho2, PB_ACT_PROXY, 1); + int a0 = g_assoc_count; + CHECK(udp_echo_ok(c, "127.1.2.3", "hello-one"), "UDP via SOCKS5 UDP ASSOCIATE, reply from flow address 127.1.2.3"); + CHECK(udp_echo_ok(c, "127.1.2.4", "hello-two"), "second destination from same client socket, reply from 127.1.2.4"); + CHECK(udp_echo_ok(c, "127.1.2.3", "again"), "first destination again"); + CHECK(g_assoc_count - a0 == 1, "exactly one UDP ASSOCIATE for the session (got %d)", (int)g_assoc_count - a0); + + // burst while association is being set up (new client => new session): queued datagrams all delivered + int c2 = udp_client(); + for (int i = 0; i < 10; i++) { char m[32]; snprintf(m, sizeof m, "burst%d", i); udp_send_flow(c2, "127.1.2.3", m); } + int got = 0; + while (udp_recv(c2, b, sizeof b, ip, &sp) > 0) got++; + CHECK(got == 10, "10 datagrams sent during association setup delivered (got %d)", got); + close(c2); + + // SOCKS5 with auth for UDP + set_flow("127.1.2.5", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 2); + c2 = udp_client(); + CHECK(udp_echo_ok(c2, "127.1.2.5", "auth-udp"), "UDP via authenticated SOCKS5"); + close(c2); + + // direct + set_flow("127.1.3.1", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); + set_flow("127.1.3.2", "127.0.0.1", g_uecho2, PB_ACT_DIRECT, 0); + CHECK(udp_echo_ok(c, "127.1.3.1", "direct-1") && udp_echo_ok(c, "127.1.3.2", "direct-2"), "UDP DIRECT, two destinations"); + + // block / unknown flow / non-loopback + set_flow("127.1.4.1", "127.0.0.1", g_uecho1, PB_ACT_BLOCK, 0); + udp_send_flow(c, "127.1.4.1", "blocked"); + CHECK(udp_recv(c, b, sizeof b, ip, &sp) < 0, "UDP BLOCK dropped"); + udp_send_flow(c, "127.9.9.9", "unknown"); + CHECK(udp_recv(c, b, sizeof b, ip, &sp) < 0, "UDP unknown flow dropped"); + + // HTTP proxy cannot do UDP + log_clear(); + set_flow("127.1.4.2", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 3); + udp_send_flow(c, "127.1.4.2", "via-http"); + udp_send_flow(c, "127.1.4.2", "via-http2"); + CHECK(udp_recv(c, b, sizeof b, ip, &sp) < 0 && log_has("HTTP proxy"), "UDP via HTTP proxy dropped + logged"); + + // DECIDE cached per (client, flow) + g_decision = (struct pb_decision){ PB_ACT_DIRECT, 0 }; + set_flow("127.1.4.3", "127.0.0.1", g_uecho1, PB_ACT_DECIDE, 0); + g_decide_calls = 0; + bool ok = udp_echo_ok(c, "127.1.4.3", "d1") && udp_echo_ok(c, "127.1.4.3", "d2") && udp_echo_ok(c, "127.1.4.3", "d3"); + CHECK(ok && g_decide_calls == 1, "UDP DECIDE -> DIRECT, decided once (calls=%d)", (int)g_decide_calls); + + // bad proxy for UDP: nothing comes back, no crash + set_flow("127.1.4.4", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 6); + udp_send_flow(c, "127.1.4.4", "dead-proxy"); + CHECK(udp_recv(c, b, sizeof b, ip, &sp) < 0, "UDP via dead SOCKS5 proxy: no reply, relay survives"); + CHECK(udp_echo_ok(c, "127.1.3.1", "still-alive"), "relay still serves UDP afterwards"); + close(c); +} + +static void test_udp_idle(void) +{ + pb_relay_stop(); + CHECK(relay_up(300, 2, 0, 5) == 0, "restart relay with udp_idle=2s"); + int c = udp_client(); + set_flow("127.1.5.1", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); + set_flow("127.1.5.2", "127.0.0.1", g_uecho2, PB_ACT_PROXY, 1); + int base = count_fds(); + bool ok = udp_echo_ok(c, "127.1.5.1", "x") && udp_echo_ok(c, "127.1.5.2", "y"); + msleep(200); + int mid = count_fds(); // direct: +1 fd; socks5: +ctl +udp in relay and +3 in the fake server + msleep(5000); + int end = count_fds(); + CHECK(ok && mid > base && end == base, "UDP sessions expire after idle and free their fds (base=%d mid=%d end=%d)", base, mid, end); + CHECK(udp_echo_ok(c, "127.1.5.1", "again"), "new session after expiry works"); + close(c); +} + +static void test_start_stop(void) +{ + pb_relay_stop(); + pb_relay_stop(); + msleep(500); + int before = count_fds(); + int bad = 0; + for (int i = 0; i < 20; i++) { + if (relay_up(0, 0, 0, 0) != 0) { bad++; continue; } + if (i % 4 == 0) { + bool a = tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 1, 2000, false) == 2000; + int c = udp_client(); + set_flow("127.1.6.1", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); + bool b = udp_echo_ok(c, "127.1.6.1", "z"); + close(c); + if (!a || !b) bad++; + } + pb_relay_stop(); + } + pb_relay_stop(); + msleep(500); + int after = count_fds(); + CHECK(bad == 0 && before == after, "start/stop x20 (with traffic): fds before=%d after=%d, failures=%d", before, after, bad); + + // start while ports are taken: must fail and leak nothing + int l = socket(AF_INET, SOCK_STREAM, 0); + int one = 1; + setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(TPORT) }; + if (bind(l, (struct sockaddr *)&a, sizeof a) < 0) perror("test bind"); + listen(l, 1); + int b4 = count_fds(); + log_clear(); + int r1 = relay_up(0, 0, 0, 0); + int af = count_fds(); + CHECK(r1 == -1 && b4 == af && log_has("cannot listen"), "start fails when TCP port busy, no fd leak, logged"); + close(l); + int u = socket(AF_INET, SOCK_DGRAM, 0); + a.sin_port = htons(UPORT); + a.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(u, (struct sockaddr *)&a, sizeof a) < 0) perror("test bind"); + b4 = count_fds(); + r1 = relay_up(0, 0, 0, 0); + af = count_fds(); + CHECK(r1 == -1 && b4 == af, "start fails when UDP port busy, no fd leak (TCP listener released)"); + close(u); + CHECK(relay_up(0, 0, 0, 0) == 0, "start works after failed starts"); + pb_relay_stop(); +} + +static void test_proxy_helper(void) +{ + char b[256]; + // HTTP target on port 80 is not available on loopback in tests: use a tiny HTTP server through the proxies. + struct srvcfg hc = { 0 }; + (void)hc; + int r = pb_relay_test_proxy(&g_px[1], "echo.test", g_echo->port, b, sizeof b); + CHECK(r == 0 && !strcmp(b, "Tunnel established"), "test_proxy SOCKS5 domain ATYP, non-80 port: '%s'", b); + r = pb_relay_test_proxy(&g_px[2], "127.0.0.1", g_echo->port, b, sizeof b); + CHECK(r == 0, "test_proxy SOCKS5 with auth: '%s'", b); + r = pb_relay_test_proxy(&g_px[3], "echo.test", g_echo->port, b, sizeof b); + CHECK(r == 0 && !strcmp(b, "Tunnel established"), "test_proxy HTTP CONNECT hostname: '%s'", b); + r = pb_relay_test_proxy(&g_px[7], "127.0.0.1", g_echo->port, b, sizeof b); + CHECK(r == -1 && strstr(b, "authentication failed"), "test_proxy wrong password -> readable error: '%s'", b); + r = pb_relay_test_proxy(&g_px[6], "127.0.0.1", 80, b, sizeof b); + CHECK(r == -1 && strstr(b, "Cannot reach proxy"), "test_proxy dead proxy -> readable error: '%s'", b); + r = pb_relay_test_proxy(&g_px[1], "nonexistent.test", 443, b, sizeof b); + CHECK(r == -1 && strstr(b, "refused"), "test_proxy unreachable target -> error: '%s'", b); +} + +// Tiny HTTP origin to exercise the port-80 HEAD path of pb_relay_test_proxy (it listens on a high port, so we +// verify the HEAD logic via a SOCKS5 server mapping port 80 is not possible; instead call with the origin port and +// confirm the tunnel path only. HEAD logic is covered by the echo server answering the request verbatim.) +// --------------------------------------------------------------------------------------------- +// Network namespace so that we can bind port 53 without real root +// --------------------------------------------------------------------------------------------- +static bool g_have53; +static bool can_bind53(void) +{ + int fd = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(53) }; + bool ok = bind(fd, (struct sockaddr *)&a, sizeof a) == 0; + close(fd); + return ok; +} +static void setup_netns(void) +{ + if (can_bind53()) { g_have53 = true; return; } + uid_t uid = getuid(); + gid_t gid = getgid(); + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) != 0) { printf("NOTE unshare(user+net) failed: %s\n", strerror(errno)); return; } + int fd = open("/proc/self/setgroups", O_WRONLY); + if (fd >= 0) { if (write(fd, "deny", 4) < 0) {} close(fd); } + char b[64]; + fd = open("/proc/self/uid_map", O_WRONLY); + if (fd >= 0) { int n = snprintf(b, sizeof b, "0 %u 1", uid); if (write(fd, b, (size_t)n) < 0) {} close(fd); } + fd = open("/proc/self/gid_map", O_WRONLY); + if (fd >= 0) { int n = snprintf(b, sizeof b, "0 %u 1", gid); if (write(fd, b, (size_t)n) < 0) {} close(fd); } + int s = socket(AF_INET, SOCK_DGRAM, 0); + struct ifreq ifr; + memset(&ifr, 0, sizeof ifr); + snprintf(ifr.ifr_name, IFNAMSIZ, "lo"); + ifr.ifr_flags = IFF_UP | IFF_RUNNING; + ioctl(s, SIOCSIFFLAGS, &ifr); + close(s); + g_have53 = can_bind53(); + printf("NOTE using private network namespace (port 53 bindable: %s)\n", g_have53 ? "yes" : "no"); +} + +// --------------------------------------------------------------------------------------------- +// DNS tap tests +// --------------------------------------------------------------------------------------------- +static void mk_msg(int mode, int idx, uint8_t *m, size_t len) +{ + for (size_t i = 0; i < len; i++) m[i] = (uint8_t)(mode * 31 + idx * 17 + (int)i * 7); +} +static void dns_tcp_handler(int fd, const struct srvcfg *c) +{ + (void)c; + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + uint8_t l[2], q[512]; + if (rd_full(fd, l, 2)) return; + size_t n = (size_t)(l[0] << 8 | l[1]); + if (n > sizeof q || rd_full(fd, q, n)) return; + int mode = q[0]; + uint8_t out[300]; + if (mode == 'a') { // one 100-byte message, written one byte at a time + out[0] = 0; out[1] = 100; mk_msg(mode, 0, out + 2, 100); + for (int i = 0; i < 102; i++) { wr_full(fd, out + i, 1); usleep(1500); } + } else if (mode == 'b') { // two messages in a single write + out[0] = 0; out[1] = 40; mk_msg(mode, 0, out + 2, 40); + out[42] = 0; out[43] = 60; mk_msg(mode, 1, out + 44, 60); + wr_full(fd, out, 104); + } else { // 'c': plain single write + out[0] = 0; out[1] = 100; mk_msg(mode, 0, out + 2, 100); + wr_full(fd, out, 102); + } +} +static void *dns_udp_thread(void *p) +{ + int fd = (int)(intptr_t)p; + uint8_t b[600]; + for (;;) { + struct sockaddr_in f; + socklen_t fl = sizeof f; + ssize_t n = recvfrom(fd, b, sizeof b, 0, (struct sockaddr *)&f, &fl); + if (n < 1) continue; + uint8_t m[100]; + mk_msg(b[0], 0, m, 100); + sendto(fd, m, 100, 0, (struct sockaddr *)&f, fl); + } + return NULL; +} + +static bool dns_tcp_case(const char *name, int action, uint32_t px, int mode, int nmsg) +{ + atomic_store(&g_obs_n, 0); + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", 53, action, px); + int fd = relay_connect(src); + uint8_t q[3] = { 0, 1, (uint8_t)mode }; + wr_full(fd, q, 3); + uint8_t got[400]; + size_t have = 0; + int64_t first = 0; + for (;;) { + ssize_t r = recv(fd, got + have, sizeof got - have, 0); + if (r <= 0) break; + if (!have) first = mono_ms(); + have += (size_t)r; + } + close(fd); + uint8_t exp[300]; + size_t el = 0; + size_t lens[2] = { mode == 'b' ? 40 : 100, 60 }; + uint32_t hashes[2]; + for (int i = 0; i < nmsg; i++) { + exp[el++] = 0; exp[el++] = (uint8_t)lens[i]; + mk_msg(mode, i, exp + el, lens[i]); + hashes[i] = fnv(exp + el, lens[i]); + el += lens[i]; + } + bool ok = have == el && !memcmp(got, exp, el) && g_obs_n == nmsg; + for (int i = 0; ok && i < nmsg; i++) ok = g_obs[i].len == lens[i] && g_obs[i].h == hashes[i]; + bool before = nmsg > 0 && g_obs[0].t1 <= first; // observe finished before the client saw any byte + CHECK(ok && before, "TCP DNS tap %s: stream intact, %d observe call(s) (got %d), observe completed %lld ms before first client byte", name, + nmsg, (int)g_obs_n, nmsg ? (long long)(first - g_obs[0].t1) : -1LL); + return ok; +} + +static void test_dns(void) +{ + CHECK(g_obs_n == 0, "no DNS observe calls for non-port-53 flows so far (%d)", (int)g_obs_n); + if (!g_have53) { printf("SKIP DNS tests (cannot bind port 53)\n"); return; } + struct srv *d = srv_start_p(dns_tcp_handler, NULL, 53); + int ufd = socket(AF_INET, SOCK_DGRAM, 0); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(53) }; + int ub = bind(ufd, (struct sockaddr *)&a, sizeof a); + CHECK(d && ub == 0, "DNS fake servers on 127.0.0.1:53"); + if (!d || ub) return; + pthread_t t; + pthread_create(&t, NULL, dns_udp_thread, (void *)(intptr_t)ufd); + pthread_detach(t); + atomic_store(&g_obs_sleep, 50); + + dns_tcp_case("DIRECT single write", PB_ACT_DIRECT, 0, 'c', 1); + dns_tcp_case("DIRECT 1-byte writes", PB_ACT_DIRECT, 0, 'a', 1); + dns_tcp_case("DIRECT two messages in one write", PB_ACT_DIRECT, 0, 'b', 2); + dns_tcp_case("via SOCKS5 single write", PB_ACT_PROXY, 1, 'c', 1); + dns_tcp_case("via SOCKS5 1-byte writes", PB_ACT_PROXY, 1, 'a', 1); + dns_tcp_case("via SOCKS5 two messages", PB_ACT_PROXY, 1, 'b', 2); + dns_tcp_case("via HTTP CONNECT 1-byte writes", PB_ACT_PROXY, 3, 'a', 1); + dns_tcp_case("via HTTP CONNECT two messages", PB_ACT_PROXY, 3, 'b', 2); + + // non-53 TCP must not be observed + atomic_store(&g_obs_n, 0); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 1000, false) == 1000 && g_obs_n == 0, "TCP non-53 not observed"); + + // UDP + struct { const char *name; int action; uint32_t px; const char *flow; } u[] = { + { "UDP DIRECT", PB_ACT_DIRECT, 0, "127.1.7.1" }, { "UDP via SOCKS5", PB_ACT_PROXY, 1, "127.1.7.2" } }; + for (int k = 0; k < 2; k++) { + set_flow(u[k].flow, "127.0.0.1", 53, u[k].action, u[k].px); + atomic_store(&g_obs_n, 0); + int c = udp_client(); + char b[256], ip[32]; + uint16_t sp; + udp_send_flow(c, u[k].flow, "q"); + int n = udp_recv(c, b, sizeof b, ip, &sp); + int64_t tr = mono_ms(); + uint8_t m[100]; + mk_msg('q', 0, m, 100); + bool ok = n == 100 && !memcmp(b, m, 100) && !strcmp(ip, u[k].flow) && g_obs_n == 1 && g_obs[0].h == fnv(m, 100) && g_obs[0].t1 <= tr; + CHECK(ok, "%s DNS tap: reply observed (calls=%d) before delivery, from flow address", u[k].name, (int)g_obs_n); + close(c); + } + // non-53 UDP not observed + atomic_store(&g_obs_n, 0); + int c = udp_client(); + set_flow("127.1.7.3", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); + CHECK(udp_echo_ok(c, "127.1.7.3", "plain") && g_obs_n == 0, "UDP non-53 not observed"); + close(c); + atomic_store(&g_obs_sleep, 0); +} + +// --------------------------------------------------------------------------------------------- +// Latency, hostname proxies, UDP sizes / odd servers +// --------------------------------------------------------------------------------------------- +static double pingpong_us(int fd, int n) +{ + char b = 'x'; + int64_t t0 = now_ms(); + struct timespec a, z; + clock_gettime(CLOCK_MONOTONIC, &a); + (void)t0; + for (int i = 0; i < n; i++) { + if (send(fd, &b, 1, MSG_NOSIGNAL) != 1 || recv(fd, &b, 1, 0) != 1) return -1; + } + clock_gettime(CLOCK_MONOTONIC, &z); + return ((double)(z.tv_sec - a.tv_sec) * 1e9 + (double)(z.tv_nsec - a.tv_nsec)) / 1000.0 / n; +} +static void test_latency(void) +{ + int one = 1; + int d = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(g_echo->port) }; + connect(d, (struct sockaddr *)&a, sizeof a); + setsockopt(d, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + double direct = pingpong_us(d, 1000); + close(d); + double res[3]; + int acts[3] = { PB_ACT_DIRECT, PB_ACT_PROXY, PB_ACT_PROXY }; + uint32_t pxs[3] = { 0, 1, 3 }; + for (int k = 0; k < 3; k++) { + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, acts[k], pxs[k]); + int fd = relay_connect(src); + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + res[k] = pingpong_us(fd, 1000); + close(fd); + } + printf(" avg RTT per 1-byte round trip: direct echo %.0f us, relay DIRECT %.0f us, relay+SOCKS5 %.0f us, relay+HTTP %.0f us\n", + direct, res[0], res[1], res[2]); + CHECK(direct > 0 && res[0] > 0 && res[1] > 0 && res[2] > 0 && res[0] < 5000 && res[1] < 5000 && res[2] < 5000, + "no Nagle/delayed-ACK stall through relay (all averages < 5 ms)"); +} + +static void test_hostname(void) +{ + set_proxy(11, PROXY_TYPE_SOCKS5, g_s5->port, NULL, NULL); + set_proxy_host(11, "localhost"); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 11, 20000, false) == 20000, "TCP via SOCKS5 proxy configured as hostname 'localhost'"); + CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_PROXY, 11, 20000, false) == 20000, "second connection uses resolver cache"); + set_proxy(12, PROXY_TYPE_HTTP, g_http->port, NULL, NULL); + set_proxy_host(12, "localhost"); + char b[256]; + int r = pb_relay_test_proxy(&g_px[12], "echo.test", g_echo->port, b, sizeof b); + CHECK(r == 0, "pb_relay_test_proxy with hostname proxy: '%s'", b); + set_proxy(13, PROXY_TYPE_SOCKS5, g_s5->port, NULL, NULL); + set_proxy_host(13, "no-such-host.invalid"); + int64_t t0 = now_ms(); + r = pb_relay_test_proxy(&g_px[13], "echo.test", g_echo->port, b, sizeof b); + int64_t dt = now_ms() - t0; + CHECK(r == -1 && strstr(b, "cannot resolve") && dt < 7000, "unresolvable proxy hostname -> readable error in %lld ms: '%s'", (long long)dt, b); + log_clear(); + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 13); + int fd = relay_connect(src); + CHECK(fd >= 0 && expect_closed(fd, 8000) && log_has("cannot resolve"), "relay TCP with unresolvable proxy: client closed + logged"); + close(fd); +} + +// A UDP server that answers from a different socket than the one it received on. +// payload starting with 'B': reply from another port on 127.0.0.1; 'C': reply from 127.0.0.2. +static void *odd_udp_thread(void *p) +{ + int fd = (int)(intptr_t)p; + int b = socket(AF_INET, SOCK_DGRAM, 0), c = socket(AF_INET, SOCK_DGRAM, 0); + struct sockaddr_in x = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + bind(b, (struct sockaddr *)&x, sizeof x); + x.sin_addr.s_addr = htonl(0x7f000002); + bind(c, (struct sockaddr *)&x, sizeof x); + char m[600]; + for (;;) { + struct sockaddr_in f; + socklen_t fl = sizeof f; + ssize_t n = recvfrom(fd, m, sizeof m, 0, (struct sockaddr *)&f, &fl); + if (n < 1) continue; + sendto(m[0] == 'C' ? c : b, m, (size_t)n, 0, (struct sockaddr *)&f, fl); + } + return NULL; +} + +static bool udp_big(int c, const char *flow, size_t n, bool expect) +{ + uint8_t *tx = malloc(n), *rx = malloc(70000); + for (size_t i = 0; i < n; i++) tx[i] = pat(i); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(pb_relay_udp_port()) }; + inet_pton(AF_INET, flow, &a.sin_addr); + ssize_t s = sendto(c, tx, n, 0, (struct sockaddr *)&a, sizeof a); + struct sockaddr_in f; + socklen_t fl = sizeof f; + ssize_t r = recvfrom(c, rx, 70000, 0, (struct sockaddr *)&f, &fl); + char ip[32]; + inet_ntop(AF_INET, &f.sin_addr, ip, sizeof ip); + bool ok = s == (ssize_t)n && (expect ? (r == (ssize_t)n && !memcmp(rx, tx, n) && !strcmp(ip, flow)) : r < 0); + free(tx); free(rx); + return ok; +} + +static void test_udp_odd(void) +{ + int c = udp_client(); + int rb = 1 << 21; + setsockopt(c, SOL_SOCKET, SO_RCVBUF, &rb, sizeof rb); + set_flow("127.1.8.1", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); + set_flow("127.1.8.2", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 1); + CHECK(udp_big(c, "127.1.8.1", 65507, true), "UDP DIRECT 65507-byte datagram round trip intact"); + CHECK(udp_big(c, "127.1.8.2", 65497, true), "UDP via SOCKS5 65497-byte datagram (max with 10-byte header) round trip intact"); + CHECK(udp_big(c, "127.1.8.2", 30000, true), "UDP via SOCKS5 30000-byte datagram intact"); + CHECK(udp_big(c, "127.1.8.2", 65507, false), "UDP via SOCKS5 65507 (header would exceed IPv4 limit): dropped, no corruption"); + CHECK(udp_echo_ok(c, "127.1.8.2", "small-after-big") && udp_echo_ok(c, "127.1.8.1", "small-direct-after-big"), "small datagrams unaffected after oversized ones"); + + uint16_t odd_port; + int ofd = socket(AF_INET, SOCK_DGRAM, 0); + struct sockaddr_in x = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + bind(ofd, (struct sockaddr *)&x, sizeof x); + socklen_t xl = sizeof x; + getsockname(ofd, (struct sockaddr *)&x, &xl); + odd_port = ntohs(x.sin_port); + pthread_t t; + pthread_create(&t, NULL, odd_udp_thread, (void *)(intptr_t)ofd); + pthread_detach(t); + set_flow("127.1.8.3", "127.0.0.1", odd_port, PB_ACT_DIRECT, 0); + set_flow("127.1.8.4", "127.0.0.1", odd_port, PB_ACT_PROXY, 1); + char b[64], ip[32]; + uint16_t sp; + const char *fl[2] = { "127.1.8.3", "127.1.8.4" }; + const char *nm[2] = { "DIRECT", "SOCKS5" }; + for (int k = 0; k < 2; k++) { + udp_send_flow(c, fl[k], "Bport"); + int n = udp_recv(c, b, sizeof b, ip, &sp); + CHECK(n == 5 && !strcmp(ip, fl[k]), "%s: reply from a different port of the same host delivered from the flow address", nm[k]); + udp_send_flow(c, fl[k], "Chost"); + n = udp_recv(c, b, sizeof b, ip, &sp); + CHECK(n < 0, "%s: reply from a different host dropped", nm[k]); + } + close(c); +} + +int main(void) +{ + setvbuf(stdout, NULL, _IOLBF, 0); + setup_netns(); + struct srvcfg c = { 0 }; + g_echo = srv_start(echo_handler, NULL); + g_s5 = srv_start(socks_handler, &c); + struct srvcfg ca = { .auth = true }; strcpy(ca.user, "alice"); strcpy(ca.pass, "secret"); + g_s5auth = srv_start(socks_handler, &ca); + g_http = srv_start(http_handler, &c); + // HTTP auth: server expects base64("alice:secret") == "YWxpY2U6c2VjcmV0" + struct srvcfg ch = { .auth = true }; strcpy(ch.pass, "YWxpY2U6c2VjcmV0"); + g_httpauth = srv_start(http_handler, &ch); + struct srvcfg cl = { .leftover = true }; + g_httpleft = srv_start(http_handler, &cl); + struct srvcfg cs = { .silent = true }; + g_silent = srv_start(socks_handler, &cs); + g_sink = srv_start(sink_handler, NULL); + g_uecho1 = udpecho_start(); + g_uecho2 = udpecho_start(); + + set_proxy(1, PROXY_TYPE_SOCKS5, g_s5->port, NULL, NULL); + set_proxy(2, PROXY_TYPE_SOCKS5, g_s5auth->port, "alice", "secret"); + set_proxy(3, PROXY_TYPE_HTTP, g_http->port, NULL, NULL); + set_proxy(4, PROXY_TYPE_HTTP, g_httpauth->port, "alice", "secret"); + set_proxy(5, PROXY_TYPE_HTTP, g_httpleft->port, NULL, NULL); + g_default_px = 1; + + CHECK(relay_up(300, 60, 0, 5) == 0, "pb_relay_start"); + test_tcp_basic(); + test_tcp_bad_proxy(); + test_tcp_halfclose(); + test_udp(); + test_proxy_helper(); + test_udp_odd(); + test_hostname(); + test_latency(); + test_dns(); + test_tcp_large(); + test_throughput(); + test_tcp_10k(); + test_tcp_idle(); + test_tcp_cap(); + test_handshake_timeout(); + test_udp_idle(); + test_start_stop(); + pb_relay_stop(); + printf("\nRESULT: %d passed, %d failed\n", g_pass, g_fail); + return g_fail ? 1 : 0; +} diff --git a/Linux/src/tests/rules_test.c b/Linux/src/tests/rules_test.c new file mode 100644 index 00000000..bc1b1c98 --- /dev/null +++ b/Linux/src/tests/rules_test.c @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include "../pb_rules.h" + +static int fails; +#define CHECK(c) do { if (!(c)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #c); fails++; } } while (0) + +static void a4(uint8_t o[16], const char *s) { memset(o, 0, 16); o[10] = o[11] = 0xff; inet_pton(AF_INET, s, o + 12); } +static void a6(uint8_t o[16], const char *s) { inet_pton(AF_INET6, s, o); } + +static struct pb_rule R(const char *p, const char *h, const char *pt) +{ + struct pb_rule r; char err[128]; + bool ok = pb_rule_parse(&r, p, h, pt, err, sizeof err); + if (!ok) printf("parse error: %s\n", err); + CHECK(ok); + return r; +} +static bool bad(const char *p, const char *h, const char *pt) +{ + struct pb_rule r; char err[128]; + bool ok = pb_rule_parse(&r, p, h, pt, err, sizeof err); + if (ok) pb_rule_free(&r); else printf(" (rejected as expected: %s)\n", err); + return !ok; +} + +int main(void) +{ + uint8_t a[16]; + struct pb_rule r; + + r = R("firefox;chrome*", "", ""); CHECK(!r.any_proc && r.nprocs == 2); + CHECK(pb_rule_match_process(&r, "/usr/lib/firefox/firefox")); CHECK(pb_rule_match_process(&r, "/opt/google/chrome/chrome")); + CHECK(pb_rule_match_process(&r, "/opt/chromium/chromedriver")); CHECK(!pb_rule_match_process(&r, "/usr/bin/curl")); pb_rule_free(&r); + r = R("*chrome*", "", ""); CHECK(pb_rule_match_process(&r, "/opt/google/chrome/chrome")); CHECK(pb_rule_match_process(&r, "/usr/bin/mychromex")); pb_rule_free(&r); + r = R("/usr/bin/*", "", ""); CHECK(pb_rule_match_process(&r, "/usr/bin/curl")); CHECK(!pb_rule_match_process(&r, "/opt/x/curl")); pb_rule_free(&r); + r = R("", "", ""); CHECK(r.any_proc && r.any_host && r.any_port); pb_rule_free(&r); + r = R("*", "*", "*"); CHECK(r.any_proc && r.any_host && r.any_port); pb_rule_free(&r); + + r = R("", "192.168.*.*;10.0.0.0/8", "80;443;8000-9000"); + a4(a, "192.168.4.5"); CHECK(pb_rule_match_dest(&r, AF_INET, a, 443, NULL, 0)); + a4(a, "10.200.1.1"); CHECK(pb_rule_match_dest(&r, AF_INET, a, 8500, NULL, 0)); + a4(a, "10.200.1.1"); CHECK(!pb_rule_match_dest(&r, AF_INET, a, 22, NULL, 0)); + a4(a, "172.16.0.1"); CHECK(!pb_rule_match_dest(&r, AF_INET, a, 80, NULL, 0)); + CHECK(r.hosts[0].kind == PB_HOST_V4_RANGE); // 192.168.*.* collapses to a range + pb_rule_free(&r); + + r = R("", "10.1-5.*.7", ""); CHECK(r.hosts[0].kind == PB_HOST_V4_BOX); + a4(a, "10.3.99.7"); CHECK(pb_rule_match_dest(&r, AF_INET, a, 1, NULL, 0)); a4(a, "10.6.99.7"); CHECK(!pb_rule_match_dest(&r, AF_INET, a, 1, NULL, 0)); + a4(a, "10.3.99.8"); CHECK(!pb_rule_match_dest(&r, AF_INET, a, 1, NULL, 0)); pb_rule_free(&r); + + r = R("", "1.2.3.4-1.2.3.9", ""); a4(a, "1.2.3.9"); CHECK(pb_rule_match_dest(&r, AF_INET, a, 1, NULL, 0)); a4(a, "1.2.3.10"); CHECK(!pb_rule_match_dest(&r, AF_INET, a, 1, NULL, 0)); pb_rule_free(&r); + r = R("", "2001:db8::/32;fd00::1", ""); a6(a, "2001:db8:ffff::1"); CHECK(pb_rule_match_dest(&r, AF_INET6, a, 1, NULL, 0)); + a6(a, "2001:db9::1"); CHECK(!pb_rule_match_dest(&r, AF_INET6, a, 1, NULL, 0)); a6(a, "fd00::1"); CHECK(pb_rule_match_dest(&r, AF_INET6, a, 1, NULL, 0)); + a4(a, "1.1.1.1"); CHECK(!pb_rule_match_dest(&r, AF_INET, a, 1, NULL, 0)); pb_rule_free(&r); + + CHECK(bad("", "999.1.1.1", "")); CHECK(bad("", "1.2.3", "")); CHECK(bad("", "1.2.3.4.5", "")); CHECK(bad("", "10.0.0.0/33", "")); + CHECK(bad("", "exa$mple.com", "")); CHECK(bad("", ".example.com", "")); CHECK(bad("", "a..b.com", "")); CHECK(bad("", "5.5.5.5-1.1.1.1", "")); CHECK(bad("", "", "abc")); + CHECK(bad("", "", "0")); CHECK(bad("", "", "70000")); CHECK(bad("", "", "90-80")); CHECK(bad("", "::g", "")); CHECK(bad("", "1::/129", "")); + + struct pb_rule rules[3]; memset(rules, 0, sizeof rules); + rules[0] = R("curl", "", "443"); rules[0].enabled = true; rules[0].action = RULE_ACTION_BLOCK; + rules[1] = R("curl", "", ""); rules[1].enabled = true; rules[1].action = RULE_ACTION_PROXY; + rules[2] = R("", "", ""); rules[2].enabled = true; rules[2].action = RULE_ACTION_DIRECT; rules[2].protocol = RULE_PROTOCOL_UDP; + a4(a, "9.9.9.9"); + CHECK(pb_rules_eval(rules, 3, "/usr/bin/curl", AF_INET, a, 443, IPPROTO_TCP, NULL, 0) == 0); + CHECK(pb_rules_eval(rules, 3, "/usr/bin/curl", AF_INET, a, 80, IPPROTO_TCP, NULL, 0) == 1); + CHECK(pb_rules_eval(rules, 3, "/usr/bin/wget", AF_INET, a, 80, IPPROTO_TCP, NULL, 0) == -1); + CHECK(pb_rules_eval(rules, 3, "/usr/bin/wget", AF_INET, a, 80, IPPROTO_UDP, NULL, 0) == 2); + rules[0].enabled = false; CHECK(pb_rules_eval(rules, 3, "/usr/bin/curl", AF_INET, a, 443, IPPROTO_TCP, NULL, 0) == 1); + for (int i = 0; i < 3; i++) pb_rule_free(&rules[i]); + + CHECK(pb_implicit_direct(AF_INET, (a4(a, "127.0.0.53"), a), 53, IPPROTO_UDP, true)); + CHECK(pb_implicit_direct(AF_INET, (a4(a, "224.0.0.251"), a), 5353, IPPROTO_UDP, true)); + CHECK(!pb_implicit_direct(AF_INET, (a4(a, "8.8.8.8"), a), 53, IPPROTO_UDP, true)); + CHECK(pb_implicit_direct(AF_INET, (a4(a, "8.8.8.8"), a), 53, IPPROTO_UDP, false)); + CHECK(pb_implicit_direct(AF_INET6, (a6(a, "::1"), a), 80, IPPROTO_TCP, true)); + CHECK(!pb_implicit_direct(AF_INET6, (a6(a, "2001:db8::1"), a), 80, IPPROTO_TCP, true)); + // ---- domain rules + r = R("", "*.github.com;example.org;10.0.0.0/8", "443"); + CHECK(r.ndomains == 2 && r.nhosts == 1 && !r.any_host); + const char *gh[] = { "api.github.com" }, *ex[] = { "EXAMPLE.org." }, *no[] = { "github.com", "notgithub.com", "example.org.evil.com" }; + a4(a, "140.82.1.1"); + CHECK(pb_rule_match_dest(&r, AF_INET, a, 443, gh, 1)); + CHECK(pb_rule_match_dest(&r, AF_INET, a, 443, ex, 1)); // case-insensitive, trailing root dot tolerated + CHECK(!pb_rule_match_dest(&r, AF_INET, a, 443, no, 3)); // "*.github.com" does not match the bare apex; exact stays exact + CHECK(!pb_rule_match_dest(&r, AF_INET, a, 80, gh, 1)); // port still has to match + a4(a, "10.1.2.3"); CHECK(pb_rule_match_dest(&r, AF_INET, a, 443, NULL, 0)); // IP pattern in the same list still works + pb_rule_free(&r); + r = R("", "github.com;*.github.com", ""); CHECK(r.ndomains == 2); pb_rule_free(&r); + CHECK(pb_domain_match("a?c.com", "abc.com")); CHECK(!pb_domain_match("a?c.com", "abbc.com")); CHECK(pb_domain_match("*", "x.y")); + CHECK(bad("", "bad name.com", "")); + { + struct pb_rule c; r = R("curl", "*.example.com", "80"); CHECK(pb_rule_clone(&c, &r) && c.ndomains == 1 && !strcmp(c.domains[0], "*.example.com")); pb_rule_free(&c); pb_rule_free(&r); + } + printf(fails ? "RULES TESTS FAILED (%d)\n" : "rules tests passed\n", fails); + return fails != 0; +} diff --git a/Linux/src/tests/run_relay_test.sh b/Linux/src/tests/run_relay_test.sh new file mode 100755 index 00000000..23b86edd --- /dev/null +++ b/Linux/src/tests/run_relay_test.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Builds and runs the root-free relay test (loopback only). Usage: run_relay_test.sh [plain|asan|both] (default both) +set -e +cd "$(dirname "$0")" +mode="${1:-both}" +out="${TMPDIR:-/tmp}" +CF="-std=gnu11 -Wall -Wextra -O1 -g -D_GNU_SOURCE -fstack-protector-strong -I.. " +if [ "$mode" = plain ] || [ "$mode" = both ]; then + gcc $CF -O2 relay_test.c ../pb_relay.c -o "$out/relay_test" -lpthread + "$out/relay_test" +fi +if [ "$mode" = asan ] || [ "$mode" = both ]; then + gcc $CF -fsanitize=address,undefined -fno-omit-frame-pointer relay_test.c ../pb_relay.c -o "$out/relay_test_asan" -lpthread + LARGE_MB="${LARGE_MB:-4}" THRU_MB="${THRU_MB:-32}" ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$out/relay_test_asan" +fi From da8e8fa5d2b6b7091a8acce54206604ab6c82af0 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:13:46 +0530 Subject: [PATCH 02/25] feat(linux/cli): multiple proxies, per-rule proxy and domain rules - repeatable --proxy [name=]type://[user:pass@]host:port (IPv6 hosts in []) - --rule process:hosts:ports:protocol:action[=proxy] - hosts accept domain names (github.com, *.github.com) - validation errors from the library are printed - no work in signal handlers; --cleanup is now a documented no-op --- Linux/cli/main.c | 460 ++++++++++++++++++++++++----------------------- 1 file changed, 234 insertions(+), 226 deletions(-) diff --git a/Linux/cli/main.c b/Linux/cli/main.c index 45a540ad..f0616911 100644 --- a/Linux/cli/main.c +++ b/Linux/cli/main.c @@ -8,7 +8,8 @@ #include "../src/ProxyBridge.h" #define MAX_RULES 100 -#define MAX_RULE_STR 512 +#define MAX_PROXIES 32 +#define MAX_RULE_STR 1024 typedef struct { char process_name[256]; @@ -16,8 +17,19 @@ typedef struct { char target_ports[256]; RuleProtocol protocol; RuleAction action; + char proxy_ref[64]; /* name or 1-based index of a --proxy; empty = first proxy */ } ProxyRule; +typedef struct { + char name[64]; + ProxyType type; + char host[256]; + uint16_t port; + char username[256]; + char password[256]; + uint32_t id; +} ProxyEntry; + static volatile bool keep_running = false; static int verbose_level = 0; @@ -40,22 +52,8 @@ static void connection_callback(const char* process_name, uint32_t pid, const ch static void signal_handler(int sig) { - if (sig == SIGSEGV || sig == SIGABRT || sig == SIGBUS) - { - printf("\n\n=== CLI CRASH DETECTED ===\n"); - printf("Signal: %d (%s)\n", sig, - sig == SIGSEGV ? "SEGFAULT" : - sig == SIGABRT ? "ABORT" : "BUS ERROR"); - printf("Calling emergency cleanup...\n"); - ProxyBridge_Stop(); - _exit(1); - } - - if (keep_running) - { - printf("\n\nStopping ProxyBridge...\n"); - keep_running = false; - } + (void)sig; + keep_running = false; /* async-signal-safe: the main loop does the shutdown */ } static void show_banner(void) @@ -66,7 +64,7 @@ static void show_banner(void) printf(" | |_) | '__/ _ \\ \\/ / | | | | _ \\| '__| |/ _` |/ _` |/ _ \\\n"); printf(" | __/| | | (_) > <| |_| | | |_) | | | | (_| | (_| | __/\n"); printf(" |_| |_| \\___/_/\\_\\\\__, | |____/|_| |_|\\__,_|\\__, |\\___|\n"); - printf(" |___/ |___/ V4.0-Beta\n"); + printf(" |___/ |___/ V4.0\n"); printf("\n"); printf(" Universal proxy client for Linux applications\n"); printf("\n"); @@ -82,215 +80,244 @@ static void show_help(const char* prog) printf(" %s [OPTIONS]\n\n", prog); printf("OPTIONS:\n"); - printf(" --proxy Proxy server URL with optional authentication\n"); - printf(" Format: type://ip:port or type://ip:port:username:password\n"); + printf(" --proxy Add a proxy (repeatable). The first one is the default.\n"); + printf(" Format: [name=]type://[user:pass@]host:port\n"); printf(" Examples: socks5://127.0.0.1:1080\n"); - printf(" http://proxy.com:8080:myuser:mypass\n"); + printf(" work=http://alice:secret@proxy.corp:8080\n"); + printf(" tor=socks5://[::1]:9050\n"); + printf(" Legacy form type://host:port:user:pass is still accepted.\n"); printf(" Default: socks5://127.0.0.1:4444\n\n"); - printf(" --rule Traffic routing rule (can be specified multiple times)\n"); - printf(" Format: process:hosts:ports:protocol:action\n"); - printf(" process - Process name(s): curl, cur*, *, or multiple separated by ;\n"); - printf(" hosts - IP/host(s): *, google.com, 192.168.*.*, or multiple separated by ; or ,\n"); - printf(" ports - Port(s): *, 443, 80;8080, 80-100, or multiple separated by ; or ,\n"); + printf(" --rule Traffic routing rule (repeatable, first match wins)\n"); + printf(" Format: process:hosts:ports:protocol:action[=proxy]\n"); + printf(" process - name or path glob(s): curl, chrom*, *, or several separated by ; or ,\n"); + printf(" hosts - IPv4/IPv6, CIDR (10.0.0.0/8), ranges (1.2.3.4-1.2.3.9),\n"); + printf(" octet patterns (192.168.*.*), domain names (github.com, *.github.com)\n"); + printf(" or *; several separated by ; or ,\n"); + printf(" (domains are matched via the DNS answers the application received)\n"); + printf(" ports - *, 443, 80;8080, 8000-9000\n"); printf(" protocol - TCP, UDP, or BOTH\n"); - printf(" action - PROXY, DIRECT, or BLOCK\n"); + printf(" action - PROXY, DIRECT, or BLOCK; PROXY=name (or PROXY=2) picks a specific proxy\n"); printf(" Examples:\n"); printf(" curl:*:*:TCP:PROXY\n"); - printf(" curl;wget:*:*:TCP:PROXY\n"); - printf(" *:*:53:UDP:PROXY\n"); - printf(" firefox:*:80;443:TCP:DIRECT\n\n"); + printf(" firefox:*:*:BOTH:PROXY=work\n"); + printf(" curl;wget:10.0.0.0/8:*:TCP:DIRECT\n"); + printf(" *:*:53:UDP:PROXY=tor\n"); + printf(" firefox:*.github.com;github.com:443:TCP:PROXY=work\n\n"); - printf(" --dns-via-proxy Route DNS queries through proxy\n"); - printf(" Values: true, false, 1, 0\n"); - printf(" Default: true\n\n"); + printf(" --dns-via-proxy Route DNS queries through the proxy (true/false/1/0, default true)\n\n"); - printf(" --verbose Logging verbosity level\n"); - printf(" 0 - No logs (default)\n"); - printf(" 1 - Show log messages only\n"); - printf(" 2 - Show connection events only\n"); - printf(" 3 - Show both logs and connections\n\n"); + printf(" --verbose 0 none (default), 1 logs, 2 connections, 3 both\n\n"); - printf(" --cleanup Cleanup resources (iptables, etc.) from crashed instance\n"); - printf(" Use if ProxyBridge crashed without proper cleanup\n\n"); + printf(" --cleanup No-op, kept for compatibility: the eBPF programs detach automatically\n\n"); printf(" --help, -h Show this help message\n\n"); printf("EXAMPLES:\n"); - printf(" # Basic usage with default proxy\n"); printf(" sudo %s --rule curl:*:*:TCP:PROXY\n\n", prog); - - printf(" # Multiple rules with custom proxy\n"); - printf(" sudo %s --proxy socks5://192.168.1.10:1080 \\\n", prog); - printf(" --rule curl:*:*:TCP:PROXY \\\n"); - printf(" --rule wget:*:*:TCP:PROXY \\\n"); - printf(" --verbose 2\n\n"); - - printf(" # Route DNS through proxy with multiple apps\n"); - printf(" sudo %s --proxy socks5://127.0.0.1:1080 \\\n", prog); - printf(" --rule \"curl;wget;firefox:*:*:BOTH:PROXY\" \\\n"); - printf(" --dns-via-proxy true --verbose 3\n\n"); + printf(" sudo %s --proxy socks5://192.168.1.10:1080 --proxy work=http://proxy.corp:8080 \\\n", prog); + printf(" --rule firefox:*:*:BOTH:PROXY --rule curl:*:*:TCP:PROXY=work --verbose 2\n\n"); printf("NOTE:\n"); - printf(" ProxyBridge requires root privileges to use nfqueue.\n"); - printf(" Run with 'sudo' or as root user.\n\n"); + printf(" ProxyBridge needs root and a Linux kernel >= 5.8 with BTF and cgroup v2 (eBPF datapath).\n"); + printf(" Credentials given on the command line are visible to other users via ps.\n\n"); +} + +static void upper_copy(char* dst, const char* src, size_t n) +{ + size_t i = 0; + for (; src[i] && i + 1 < n; i++) + dst[i] = (char)toupper((unsigned char)src[i]); + dst[i] = '\0'; } static RuleProtocol parse_protocol(const char* str) { char upper[16]; - for (size_t i = 0; str[i] && i < 15; i++) - upper[i] = toupper(str[i]); - upper[strlen(str) < 15 ? strlen(str) : 15] = '\0'; - - if (strcmp(upper, "TCP") == 0) - return RULE_PROTOCOL_TCP; - else if (strcmp(upper, "UDP") == 0) - return RULE_PROTOCOL_UDP; - else if (strcmp(upper, "BOTH") == 0) - return RULE_PROTOCOL_BOTH; - else - { - fprintf(stderr, "ERROR: Invalid protocol '%s'. Use TCP, UDP, or BOTH\n", str); - exit(1); - } + upper_copy(upper, str, sizeof(upper)); + if (strcmp(upper, "TCP") == 0) return RULE_PROTOCOL_TCP; + if (strcmp(upper, "UDP") == 0) return RULE_PROTOCOL_UDP; + if (strcmp(upper, "BOTH") == 0) return RULE_PROTOCOL_BOTH; + fprintf(stderr, "ERROR: Invalid protocol '%s'. Use TCP, UDP, or BOTH\n", str); + exit(1); } -static RuleAction parse_action(const char* str) +/* action[=proxy] */ +static RuleAction parse_action(const char* str, char* proxy_ref, size_t ref_size) { - char upper[16]; - for (size_t i = 0; str[i] && i < 15; i++) - upper[i] = toupper(str[i]); - upper[strlen(str) < 15 ? strlen(str) : 15] = '\0'; - - if (strcmp(upper, "PROXY") == 0) - return RULE_ACTION_PROXY; - else if (strcmp(upper, "DIRECT") == 0) - return RULE_ACTION_DIRECT; - else if (strcmp(upper, "BLOCK") == 0) - return RULE_ACTION_BLOCK; - else + char buf[128]; + snprintf(buf, sizeof(buf), "%s", str); + proxy_ref[0] = '\0'; + char* eq = strchr(buf, '='); + if (eq) { - fprintf(stderr, "ERROR: Invalid action '%s'. Use PROXY, DIRECT, or BLOCK\n", str); - exit(1); + *eq = '\0'; + snprintf(proxy_ref, ref_size, "%s", eq + 1); } + char upper[16]; + upper_copy(upper, buf, sizeof(upper)); + if (strcmp(upper, "PROXY") == 0) return RULE_ACTION_PROXY; + if (eq && proxy_ref[0]) { fprintf(stderr, "ERROR: '=proxy' is only valid with PROXY\n"); exit(1); } + if (strcmp(upper, "DIRECT") == 0) return RULE_ACTION_DIRECT; + if (strcmp(upper, "BLOCK") == 0) return RULE_ACTION_BLOCK; + fprintf(stderr, "ERROR: Invalid action '%s'. Use PROXY, DIRECT, or BLOCK\n", buf); + exit(1); } static void default_if_empty(char* dest, const char* src, const char* default_val, size_t dest_size) { if (src == NULL || src[0] == '\0' || strcmp(src, " ") == 0) - strncpy(dest, default_val, dest_size - 1); + snprintf(dest, dest_size, "%s", default_val); else - strncpy(dest, src, dest_size - 1); - dest[dest_size - 1] = '\0'; + snprintf(dest, dest_size, "%.*s", (int)dest_size - 1, src); } +/* process:hosts:ports:protocol:action[=proxy] -- hosts may itself contain ':' (IPv6), so parse from both ends */ static bool parse_rule(const char* rule_str, ProxyRule* rule) { char buffer[MAX_RULE_STR]; - strncpy(buffer, rule_str, sizeof(buffer) - 1); - buffer[sizeof(buffer) - 1] = '\0'; - - char* parts[5] = {NULL, NULL, NULL, NULL, NULL}; - int part_idx = 0; - char* token = strtok(buffer, ":"); + snprintf(buffer, sizeof(buffer), "%s", rule_str); - while (token != NULL && part_idx < 5) + char* first = strchr(buffer, ':'); + char* last = strrchr(buffer, ':'); + char* second_last = NULL; + char* third_last = NULL; + if (last && last != first) { - parts[part_idx++] = token; - token = strtok(NULL, ":"); + *last = '\0'; + second_last = strrchr(buffer, ':'); + if (second_last && second_last != first) + { + *second_last = '\0'; + third_last = strrchr(buffer, ':'); + } } - - if (part_idx != 5) + if (!first || !last || !second_last || !third_last || third_last == first) { fprintf(stderr, "ERROR: Invalid rule format '%s'\n", rule_str); - fprintf(stderr, "Expected format: process:hosts:ports:protocol:action\n"); + fprintf(stderr, "Expected format: process:hosts:ports:protocol:action[=proxy]\n"); return false; } - - default_if_empty(rule->process_name, parts[0], "*", sizeof(rule->process_name)); - default_if_empty(rule->target_hosts, parts[1], "*", sizeof(rule->target_hosts)); - default_if_empty(rule->target_ports, parts[2], "*", sizeof(rule->target_ports)); - - rule->protocol = parse_protocol(parts[3]); - rule->action = parse_action(parts[4]); - + /* after trimming: buffer = process ':' hosts ':' ports ; protocol = second_last+1 ; action = last+1 */ + *third_last = '\0'; + const char* ports = third_last + 1; + const char* protocol = second_last + 1; + const char* action = last + 1; + + /* process = up to the first ':' ; hosts = between first ':' and third_last */ + char* sep = strchr(buffer, ':'); + if (!sep) + { + fprintf(stderr, "ERROR: Invalid rule format '%s'\n", rule_str); + return false; + } + *sep = '\0'; + const char* hosts = sep + 1; + + default_if_empty(rule->process_name, buffer, "*", sizeof(rule->process_name)); + default_if_empty(rule->target_hosts, hosts, "*", sizeof(rule->target_hosts)); + default_if_empty(rule->target_ports, ports, "*", sizeof(rule->target_ports)); + rule->protocol = parse_protocol(protocol); + rule->action = parse_action(action, rule->proxy_ref, sizeof(rule->proxy_ref)); return true; } -static bool parse_proxy_url(const char* url, ProxyType* type, char* host, uint16_t* port, char* username, char* password) +/* [name=]type://[user:pass@]host:port or legacy type://host:port[:user:pass] ; host may be [v6] */ +static bool parse_proxy_spec(const char* spec, ProxyEntry* p) { - char buffer[512]; - strncpy(buffer, url, sizeof(buffer) - 1); - buffer[sizeof(buffer) - 1] = '\0'; - - username[0] = '\0'; - password[0] = '\0'; + char buffer[768]; + snprintf(buffer, sizeof(buffer), "%s", spec); + memset(p, 0, sizeof(*p)); - // parse type:// char* scheme_end = strstr(buffer, "://"); if (scheme_end == NULL) { - fprintf(stderr, "ERROR: Invalid proxy URL format. Expected type://host:port\n"); + fprintf(stderr, "ERROR: Invalid proxy '%s'. Expected [name=]type://host:port\n", spec); return false; } - *scheme_end = '\0'; char* scheme = buffer; char* rest = scheme_end + 3; + char* eq = strchr(scheme, '='); + if (eq) + { + *eq = '\0'; + snprintf(p->name, sizeof(p->name), "%.*s", (int)sizeof(p->name) - 1, scheme); + scheme = eq + 1; + } char upper_scheme[16]; - for (size_t i = 0; scheme[i] && i < 15; i++) - upper_scheme[i] = toupper(scheme[i]); - upper_scheme[strlen(scheme) < 15 ? strlen(scheme) : 15] = '\0'; - - if (strcmp(upper_scheme, "SOCKS5") == 0) - *type = PROXY_TYPE_SOCKS5; - else if (strcmp(upper_scheme, "HTTP") == 0) - *type = PROXY_TYPE_HTTP; + upper_copy(upper_scheme, scheme, sizeof(upper_scheme)); + if (strcmp(upper_scheme, "SOCKS5") == 0) p->type = PROXY_TYPE_SOCKS5; + else if (strcmp(upper_scheme, "HTTP") == 0) p->type = PROXY_TYPE_HTTP; else { fprintf(stderr, "ERROR: Invalid proxy type '%s'. Use 'socks5' or 'http'\n", scheme); return false; } - // parse host:port[:user:pass] - char* parts[4]; - int num_parts = 0; - char* token = strtok(rest, ":"); - while (token != NULL && num_parts < 4) + char* at = strrchr(rest, '@'); + if (at) { - parts[num_parts++] = token; - token = strtok(NULL, ":"); + *at = '\0'; + char* colon = strchr(rest, ':'); + if (colon) { *colon = '\0'; snprintf(p->password, sizeof(p->password), "%s", colon + 1); } + snprintf(p->username, sizeof(p->username), "%s", rest); + rest = at + 1; } - if (num_parts < 2) + char* port_str = NULL; + char* legacy = NULL; + if (rest[0] == '[') { - fprintf(stderr, "ERROR: Invalid proxy URL. Missing host or port\n"); - return false; + char* close = strchr(rest, ']'); + if (!close || close[1] != ':') { fprintf(stderr, "ERROR: Invalid IPv6 proxy host in '%s'\n", spec); return false; } + *close = '\0'; + snprintf(p->host, sizeof(p->host), "%s", rest + 1); + port_str = close + 2; } - - strncpy(host, parts[0], 255); - host[255] = '\0'; - - *port = atoi(parts[1]); - if (*port == 0) + else { - fprintf(stderr, "ERROR: Invalid proxy port '%s'\n", parts[1]); - return false; + char* colon = strchr(rest, ':'); + if (!colon) { fprintf(stderr, "ERROR: Invalid proxy '%s': missing port\n", spec); return false; } + *colon = '\0'; + snprintf(p->host, sizeof(p->host), "%s", rest); + port_str = colon + 1; } - - if (num_parts >= 4) + legacy = strchr(port_str, ':'); /* type://host:port:user:pass */ + if (legacy && !at) { - strncpy(username, parts[2], 255); - username[255] = '\0'; - strncpy(password, parts[3], 255); - password[255] = '\0'; + *legacy++ = '\0'; + char* colon = strchr(legacy, ':'); + if (colon) { *colon = '\0'; snprintf(p->password, sizeof(p->password), "%s", colon + 1); } + snprintf(p->username, sizeof(p->username), "%s", legacy); } - + char* endp = NULL; + long port = strtol(port_str, &endp, 10); + if (!*port_str || *endp || port < 1 || port > 65535) + { + fprintf(stderr, "ERROR: Invalid proxy port '%s'\n", port_str); + return false; + } + p->port = (uint16_t)port; + if (!p->host[0]) { fprintf(stderr, "ERROR: Proxy host is empty in '%s'\n", spec); return false; } return true; } +static uint32_t find_proxy(const ProxyEntry* proxies, int n, const char* ref) +{ + if (!ref[0]) + return 0; /* default proxy */ + char* endp = NULL; + long idx = strtol(ref, &endp, 10); + if (!*endp && idx >= 1 && idx <= n) + return proxies[idx - 1].id; + for (int i = 0; i < n; i++) + if (strcmp(proxies[i].name, ref) == 0) + return proxies[i].id; + return UINT32_MAX; +} + static bool is_root(void) { return getuid() == 0; @@ -298,24 +325,21 @@ static bool is_root(void) int main(int argc, char *argv[]) { - // check cleanup flag + ProxyEntry proxies[MAX_PROXIES]; + int num_proxies = 0; + ProxyRule rules[MAX_RULES]; + int num_rules = 0; + bool dns_via_proxy = true; + for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--cleanup") == 0) { - printf("Running cleanup...\n"); - ProxyBridge_Stop(); - printf("Cleanup complete.\n"); + printf("Nothing to clean up: the eBPF programs detach automatically when ProxyBridge exits.\n"); return 0; } } - char proxy_url[512] = "socks5://127.0.0.1:4444"; - ProxyRule rules[MAX_RULES]; - int num_rules = 0; - bool dns_via_proxy = true; - - // parse args for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) @@ -325,8 +349,14 @@ int main(int argc, char *argv[]) } else if (strcmp(argv[i], "--proxy") == 0 && i + 1 < argc) { - strncpy(proxy_url, argv[++i], sizeof(proxy_url) - 1); - proxy_url[sizeof(proxy_url) - 1] = '\0'; + if (num_proxies >= MAX_PROXIES) + { + fprintf(stderr, "ERROR: Maximum %d proxies supported\n", MAX_PROXIES); + return 1; + } + if (!parse_proxy_spec(argv[++i], &proxies[num_proxies])) + return 1; + num_proxies++; } else if (strcmp(argv[i], "--rule") == 0 && i + 1 < argc) { @@ -369,9 +399,15 @@ int main(int argc, char *argv[]) } } + if (num_proxies == 0) + { + if (!parse_proxy_spec("socks5://127.0.0.1:4444", &proxies[0])) + return 1; + num_proxies = 1; + } + show_banner(); - // need root if (!is_root()) { printf("\033[31m\nERROR: ProxyBridge requires root privileges!\033[0m\n"); @@ -379,54 +415,35 @@ int main(int argc, char *argv[]) return 1; } - // parse proxy config - ProxyType proxy_type; - char proxy_host[256]; - uint16_t proxy_port; - char proxy_username[256]; - char proxy_password[256]; - - if (!parse_proxy_url(proxy_url, &proxy_type, proxy_host, &proxy_port, proxy_username, proxy_password)) - return 1; - - // setup callbacks based on verbose - // 0=nothing 1=logs 2=connections 3=both - if (verbose_level == 1 || verbose_level == 3) ProxyBridge_SetLogCallback(log_callback); else - ProxyBridge_SetLogCallback(NULL); // Explicitly disable + ProxyBridge_SetLogCallback(NULL); if (verbose_level == 2 || verbose_level == 3) ProxyBridge_SetConnectionCallback(connection_callback); else - ProxyBridge_SetConnectionCallback(NULL); // Explicitly disable + ProxyBridge_SetConnectionCallback(NULL); - // turn on traffic logging when needed ProxyBridge_SetTrafficLoggingEnabled(verbose_level > 0); + ProxyBridge_SetDnsViaProxy(dns_via_proxy); - // show config - printf("Proxy: %s://%s:%u\n", - proxy_type == PROXY_TYPE_HTTP ? "http" : "socks5", - proxy_host, proxy_port); - - if (proxy_username[0] != '\0') - printf("Proxy Auth: %s:***\n", proxy_username); - - printf("DNS via Proxy: %s\n", dns_via_proxy ? "Enabled" : "Disabled"); - - // setup proxy - if (!ProxyBridge_SetProxyConfig(proxy_type, proxy_host, proxy_port, - proxy_username[0] ? proxy_username : "", - proxy_password[0] ? proxy_password : "")) + char err[256]; + printf("Proxies: %d\n", num_proxies); + for (int i = 0; i < num_proxies; i++) { - fprintf(stderr, "ERROR: Failed to set proxy configuration\n"); - return 1; + ProxyEntry* p = &proxies[i]; + p->id = ProxyBridge_AddProxy(p->name, p->type, p->host, p->port, p->username, p->password, err, sizeof(err)); + if (!p->id) + { + fprintf(stderr, "ERROR: Proxy %d (%s:%u): %s\n", i + 1, p->host, p->port, err); + return 1; + } + printf(" [%d]%s%s%s %s://%s:%u%s\n", i + 1, p->name[0] ? " " : "", p->name, i == 0 ? " (default)" : "", + p->type == PROXY_TYPE_HTTP ? "http" : "socks5", p->host, p->port, p->username[0] ? " (auth)" : ""); } + printf("DNS via Proxy: %s\n", dns_via_proxy ? "Enabled" : "Disabled"); - ProxyBridge_SetDnsViaProxy(dns_via_proxy); - - // add rules if (num_rules > 0) { printf("Rules: %d\n", num_rules); @@ -436,28 +453,28 @@ int main(int argc, char *argv[]) rules[i].protocol == RULE_PROTOCOL_UDP ? "UDP" : "BOTH"; const char* action_str = rules[i].action == RULE_ACTION_PROXY ? "PROXY" : rules[i].action == RULE_ACTION_DIRECT ? "DIRECT" : "BLOCK"; - - uint32_t rule_id = ProxyBridge_AddRule( - rules[i].process_name, - rules[i].target_hosts, - rules[i].target_ports, - rules[i].protocol, - rules[i].action); - - if (rule_id > 0) + uint32_t proxy_id = 0; + if (rules[i].action == RULE_ACTION_PROXY) { - printf(" [%u] %s:%s:%s:%s -> %s\n", - rule_id, - rules[i].process_name, - rules[i].target_hosts, - rules[i].target_ports, - protocol_str, - action_str); + proxy_id = find_proxy(proxies, num_proxies, rules[i].proxy_ref); + if (proxy_id == UINT32_MAX) + { + fprintf(stderr, "ERROR: Rule %d refers to unknown proxy '%s'\n", i + 1, rules[i].proxy_ref); + return 1; + } } - else + + uint32_t rule_id = ProxyBridge_AddRule(rules[i].process_name, rules[i].target_hosts, rules[i].target_ports, + rules[i].protocol, rules[i].action, proxy_id, err, sizeof(err)); + if (rule_id == 0) { - fprintf(stderr, " ERROR: Failed to add rule for %s\n", rules[i].process_name); + fprintf(stderr, " ERROR: Rule %d (%s:%s:%s): %s\n", i + 1, rules[i].process_name, + rules[i].target_hosts, rules[i].target_ports, err); + return 1; } + printf(" [%u] %s:%s:%s:%s -> %s%s%s\n", rule_id, rules[i].process_name, rules[i].target_hosts, + rules[i].target_ports, protocol_str, action_str, + rules[i].proxy_ref[0] ? " via " : "", rules[i].proxy_ref); } } else @@ -466,10 +483,9 @@ int main(int argc, char *argv[]) printf("Use --rule to add proxy rules. See --help for examples.\n"); } - // start proxybridge if (!ProxyBridge_Start()) { - fprintf(stderr, "ERROR: Failed to start ProxyBridge\n"); + fprintf(stderr, "ERROR: Failed to start ProxyBridge (run with --verbose 1 for details)\n"); return 1; } @@ -478,19 +494,11 @@ int main(int argc, char *argv[]) signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); - signal(SIGSEGV, signal_handler); // Catch segfault - signal(SIGABRT, signal_handler); // Catch abort - signal(SIGBUS, signal_handler); // Catch bus error - // main loop while (keep_running) - { sleep(1); - } - // cleanup ProxyBridge_Stop(); printf("ProxyBridge stopped.\n"); - return 0; } From e3fea87988bf8f1374b7f873fb8a9f76159143b5 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:13:46 +0530 Subject: [PATCH 03/25] feat(linux/gui): proxy manager, profiles and per-rule proxy - proxy list manager with per-proxy test, default proxy and validation - profiles (new, duplicate, rename, delete, switch without restarting the engine), each with its own proxies, rules and default proxy; old configs migrate to a profile named "Default" - rule editor: proxy selection, inline validation, move up/down, JSON import/export with a validation summary - domain names accepted in the hosts field; logs show the domain - PROXYBRIDGE_CONFIG_DIR overrides the config directory - signal handling and callback memory safety fixes - config/profile unit test --- Linux/gui/gui.h | 58 ++- Linux/gui/gui_config.c | 386 +++++++++++++++--- Linux/gui/gui_logs.c | 69 ++-- Linux/gui/gui_rules.c | 384 +++++++++++++----- Linux/gui/gui_settings.c | 736 +++++++++++++++++++++++++++++----- Linux/gui/gui_utils.c | 55 ++- Linux/gui/main.c | 94 +++-- Linux/gui/tests/config_test.c | 107 +++++ 8 files changed, 1541 insertions(+), 348 deletions(-) create mode 100644 Linux/gui/tests/config_test.c diff --git a/Linux/gui/gui.h b/Linux/gui/gui.h index 035c1ee2..a1c05771 100644 --- a/Linux/gui/gui.h +++ b/Linux/gui/gui.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "ProxyBridge.h" typedef struct { char *process_name; @@ -26,13 +27,21 @@ typedef struct { char *message; } LogData; +typedef struct { + uint32_t id; // library proxy id, 0 = not registered with the library + char *name; // unique key, also what rules and the config refer to + ProxyType type; + char *host; + uint16_t port; + char *username; + char *password; +} ProxyData; + +// proxy manager dialog widgets typedef struct { GtkWidget *dialog; - GtkWidget *ip_entry; - GtkWidget *port_entry; - GtkWidget *type_combo; - GtkWidget *user_entry; - GtkWidget *pass_entry; + GtkWidget *tree; + GtkListStore *store; GtkWidget *test_host; GtkWidget *test_port; GtkTextBuffer *output_buffer; @@ -46,14 +55,17 @@ typedef struct { char *target_ports; RuleProtocol protocol; RuleAction action; + char *proxy_name; // NULL or "" = default proxy bool enabled; bool selected; } RuleData; struct TestRunnerData { + uint32_t proxy_id; char *host; uint16_t port; - ConfigInfo *ui_info; + GtkTextBuffer *buffer; // referenced, released in the idle callback + GtkWidget *btn; // referenced, released in the idle callback }; typedef struct { @@ -68,11 +80,17 @@ extern GtkTextBuffer *log_buffer; extern GtkWidget *status_bar; extern guint status_context_id; -extern char g_proxy_ip[256]; -extern uint16_t g_proxy_port; -extern ProxyType g_proxy_type; -extern char g_proxy_user[256]; -extern char g_proxy_pass[256]; +typedef struct { + char *name; + GList *proxies; // ProxyData, only current while the profile is not active (see profile_sync_active) + char *default_proxy; + GList *rules; // RuleData +} Profile; + +extern GList *g_profiles; // list of Profile +extern char *g_active_profile; // name of the active profile +extern GList *g_proxies_list; // list of ProxyData +extern char *g_default_proxy; // name of the default proxy, NULL if none extern GList *g_rules_list; extern bool g_chk_logging; @@ -85,18 +103,34 @@ char* get_current_time_str(); char *escape_json_string(const char *src); char *extract_sub_json_str(const char *json, const char *key); bool extract_sub_json_bool(const char *json, const char *key); +bool json_has_key(const char *json, const char *key); // config storage void save_config(); void load_config(); +void profile_sync_active(); +Profile *profile_find(const char *name); +Profile *profile_create(const char *name, bool copy_active); +bool profile_rename(Profile *p, const char *new_name); +bool profile_activate(const char *name); +bool profile_delete(Profile *p); -// settings +// settings / proxies void on_proxy_configure(GtkWidget *widget, gpointer data); +void on_profiles_clicked(GtkWidget *widget, gpointer data); +ProxyData *find_proxy_by_name(const char *name); +uint32_t resolve_proxy_id(const char *name); +void free_proxy_data(ProxyData *p); +void apply_proxies_to_lib(); // rules section void on_proxy_rules_clicked(GtkWidget *widget, gpointer data); +void free_rule_data(RuleData *rule); +void apply_rules_to_lib(); +void refresh_rules_window(); // Logs +void gui_log(const char *format, ...); void lib_log_callback(const char *message); void lib_connection_callback(const char *process_name, uint32_t pid, const char *dest_ip, uint16_t dest_port, const char *proxy_info); void on_search_conn_changed(GtkSearchEntry *entry, gpointer user_data); diff --git a/Linux/gui/gui_config.c b/Linux/gui/gui_config.c index 46c95717..433a1829 100644 --- a/Linux/gui/gui_config.c +++ b/Linux/gui/gui_config.c @@ -1,24 +1,210 @@ #include "gui.h" #include -#define CONFIG_DIR "/etc/proxybridge" -#define CONFIG_PATH "/etc/proxybridge/config.ini" +#define CONFIG_DIR_DEFAULT "/etc/proxybridge" + +GList *g_profiles = NULL; // list of Profile +char *g_active_profile = NULL; // name of the active profile + +// PROXYBRIDGE_CONFIG_DIR overrides the default dir (used for testing) +static char *config_dir() { + const char *env = getenv("PROXYBRIDGE_CONFIG_DIR"); + return g_strdup((env && *env) ? env : CONFIG_DIR_DEFAULT); +} + +static char *config_path() { + char *dir = config_dir(); + char *path = g_strdup_printf("%s/config.ini", dir); + g_free(dir); + return path; +} + +// ---- profiles ---- + +static void free_profile(Profile *p) { + if (!p) return; + for (GList *l = p->proxies; l != NULL; l = l->next) free_proxy_data((ProxyData *)l->data); + for (GList *l = p->rules; l != NULL; l = l->next) free_rule_data((RuleData *)l->data); + g_list_free(p->proxies); + g_list_free(p->rules); + g_free(p->default_proxy); + g_free(p->name); + g_free(p); +} + +Profile *profile_find(const char *name) { + if (!name) return NULL; + for (GList *l = g_profiles; l != NULL; l = l->next) { + Profile *p = (Profile *)l->data; + if (strcmp(p->name, name) == 0) return p; + } + return NULL; +} + +// the working lists (g_proxies_list etc.) belong to the active profile, copy the heads back +void profile_sync_active() { + Profile *p = profile_find(g_active_profile); + if (!p) return; + p->proxies = g_proxies_list; + p->rules = g_rules_list; + p->default_proxy = g_default_proxy; +} + +static void load_globals_from(Profile *p) { + g_proxies_list = p->proxies; + g_rules_list = p->rules; + g_default_proxy = p->default_proxy; +} + +static Profile *profile_new_empty(const char *name) { + Profile *p = g_malloc0(sizeof(Profile)); + p->name = g_strdup(name); + g_profiles = g_list_append(g_profiles, p); + return p; +} + +static ProxyData *copy_proxy(const ProxyData *src) { + ProxyData *d = g_malloc0(sizeof(ProxyData)); + d->name = g_strdup(src->name); + d->type = src->type; + d->host = g_strdup(src->host); + d->port = src->port; + d->username = g_strdup(src->username ? src->username : ""); + d->password = g_strdup(src->password ? src->password : ""); + return d; +} + +static RuleData *copy_rule(const RuleData *src) { + RuleData *d = g_malloc0(sizeof(RuleData)); + d->process_name = g_strdup(src->process_name ? src->process_name : ""); + d->target_hosts = g_strdup(src->target_hosts ? src->target_hosts : ""); + d->target_ports = g_strdup(src->target_ports ? src->target_ports : ""); + d->protocol = src->protocol; + d->action = src->action; + d->proxy_name = src->proxy_name ? g_strdup(src->proxy_name) : NULL; + d->enabled = src->enabled; + return d; +} + +// new profile (empty, or a copy of the active one). NULL if the name is empty or taken +Profile *profile_create(const char *name, bool copy_active) { + if (!name || !*name || profile_find(name)) return NULL; + profile_sync_active(); + Profile *src = copy_active ? profile_find(g_active_profile) : NULL; + Profile *p = profile_new_empty(name); + if (src) { + for (GList *l = src->proxies; l != NULL; l = l->next) p->proxies = g_list_append(p->proxies, copy_proxy((ProxyData *)l->data)); + for (GList *l = src->rules; l != NULL; l = l->next) p->rules = g_list_append(p->rules, copy_rule((RuleData *)l->data)); + p->default_proxy = g_strdup(src->default_proxy); + } + return p; +} + +bool profile_rename(Profile *p, const char *new_name) { + if (!p || !new_name || !*new_name) return false; + if (strcmp(p->name, new_name) == 0) return true; + if (profile_find(new_name)) return false; + bool active = g_active_profile && strcmp(g_active_profile, p->name) == 0; + g_free(p->name); + p->name = g_strdup(new_name); + if (active) { + g_free(g_active_profile); + g_active_profile = g_strdup(new_name); + } + return true; +} + +// makes the profile active (working lists swap). does not touch the library +bool profile_activate(const char *name) { + Profile *p = profile_find(name); + if (!p) return false; + profile_sync_active(); + if (g_active_profile != name) { // name may alias g_active_profile + char *copy = g_strdup(name); + g_free(g_active_profile); + g_active_profile = copy; + } + load_globals_from(p); + return true; +} + +// last profile cannot be deleted. deleting the active one activates another first +bool profile_delete(Profile *p) { + if (!p || !g_profiles || !g_profiles->next) return false; + if (g_active_profile && strcmp(g_active_profile, p->name) == 0) { + Profile *other = NULL; + for (GList *l = g_profiles; l != NULL; l = l->next) if (l->data != p) { other = (Profile *)l->data; break; } + if (!other || !profile_activate(other->name)) return false; + } + g_profiles = g_list_remove(g_profiles, p); + free_profile(p); + return true; +} + +// ---- save ---- + +static void write_profile(FILE *f, Profile *prof) { + char *pn = g_uri_escape_string(prof->name, NULL, FALSE); + char *def_esc = g_uri_escape_string(prof->default_proxy ? prof->default_proxy : "", NULL, FALSE); + fprintf(f, "[PROFILE:%s]\n", pn); + fprintf(f, "defaultProxy=%s\n", def_esc); + g_free(pn); + g_free(def_esc); + + // proxies section, text fields are percent-encoded so '|' and newlines are safe + fprintf(f, "[PROXIES]\n"); + for (GList *l = prof->proxies; l != NULL; l = l->next) { + ProxyData *p = (ProxyData *)l->data; + // format: name|type|host|port|user|pass + char *name = g_uri_escape_string(p->name ? p->name : "", NULL, FALSE); + char *host = g_uri_escape_string(p->host ? p->host : "", NULL, FALSE); + char *user = g_uri_escape_string(p->username ? p->username : "", NULL, FALSE); + char *pass = g_uri_escape_string(p->password ? p->password : "", NULL, FALSE); + fprintf(f, "%s|%d|%s|%u|%s|%s\n", name, p->type, host, p->port, user, pass); + g_free(name); g_free(host); g_free(user); g_free(pass); + } + + // rules section + fprintf(f, "[RULES]\n"); + for (GList *l = prof->rules; l != NULL; l = l->next) { + RuleData *rule = (RuleData *)l->data; + // format: id|protocol|action|enabled|procname|hosts|ports|proxyname + char *pname = g_uri_escape_string(rule->proxy_name ? rule->proxy_name : "", NULL, FALSE); + fprintf(f, "%u|%d|%d|%d|%s|%s|%s|%s\n", + rule->id, + rule->protocol, + rule->action, + rule->enabled, + rule->process_name ? rule->process_name : "", + rule->target_hosts ? rule->target_hosts : "", + rule->target_ports ? rule->target_ports : "", + pname + ); + g_free(pname); + } +} // save config void save_config() { + profile_sync_active(); + + char *dir = config_dir(); + char *path = config_path(); struct stat st = {0}; - if (stat(CONFIG_DIR, &st) == -1) { - if (mkdir(CONFIG_DIR, 0700) != 0) { + if (stat(dir, &st) == -1) { + if (g_mkdir_with_parents(dir, 0700) != 0) { perror("failed to create config dir"); + g_free(dir); g_free(path); return; } - } else if (chmod(CONFIG_DIR, 0700) != 0) { + } else if (chmod(dir, 0700) != 0) { perror("chmod config dir failed"); } - int cfg_fd = open(CONFIG_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0600); + int cfg_fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (cfg_fd < 0) { - printf("failed to save config to %s\n", CONFIG_PATH); + printf("failed to save config to %s\n", path); + g_free(dir); g_free(path); return; } // mode on open only hits create; tighten existing 0644 installs too @@ -28,71 +214,126 @@ void save_config() { FILE *f = fdopen(cfg_fd, "w"); if (!f) { close(cfg_fd); - printf("failed to save config to %s\n", CONFIG_PATH); + printf("failed to save config to %s\n", path); + g_free(dir); g_free(path); return; } - // settings section + // settings section, global + char *act_esc = g_uri_escape_string(g_active_profile ? g_active_profile : "", NULL, FALSE); fprintf(f, "[SETTINGS]\n"); - fprintf(f, "ip=%s\n", g_proxy_ip); - fprintf(f, "port=%d\n", g_proxy_port); - fprintf(f, "type=%d\n", g_proxy_type); - fprintf(f, "user=%s\n", g_proxy_user); - fprintf(f, "pass=%s\n", g_proxy_pass); + fprintf(f, "activeProfile=%s\n", act_esc); + g_free(act_esc); fprintf(f, "logging=%d\n", g_chk_logging); fprintf(f, "dns=%d\n", g_chk_dns); - // rules section - fprintf(f, "[RULES]\n"); - for (GList *l = g_rules_list; l != NULL; l = l->next) { - RuleData *rule = (RuleData *)l->data; - // format: id|protocol|action|enabled|procname|hosts|ports - fprintf(f, "%u|%d|%d|%d|%s|%s|%s\n", - rule->id, - rule->protocol, - rule->action, - rule->enabled, - rule->process_name ? rule->process_name : "", - rule->target_hosts ? rule->target_hosts : "", - rule->target_ports ? rule->target_ports : "" - ); - } + for (GList *l = g_profiles; l != NULL; l = l->next) write_profile(f, (Profile *)l->data); fclose(f); + g_free(dir); g_free(path); +} + +// decode a percent-encoded field, never returns NULL +static char *unescape_field(const char *src) { + char *out = g_uri_unescape_string(src ? src : "", NULL); + return out ? out : g_strdup(""); +} + +static ProxyType proxy_type_from_int(int v) { + return v == PROXY_TYPE_HTTP ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; +} + +static ProxyData *find_in_list(GList *list, const char *name) { + for (GList *l = list; l != NULL; l = l->next) { + ProxyData *p = (ProxyData *)l->data; + if (p->name && strcmp(p->name, name) == 0) return p; + } + return NULL; } -// load settings from file +// load settings from file. old files (no profiles) end up in a profile called "Default" void load_config() { - chmod(CONFIG_PATH, 0600); // old installs might still be world readable - FILE *f = fopen(CONFIG_PATH, "r"); - if (!f) return; + char *path = config_path(); + chmod(path, 0600); // old installs might still be world readable + FILE *f = fopen(path, "r"); + g_free(path); + + char line[8192]; + int section = 0; // 0=none, 1=settings, 2=rules, 3=proxies, 4=profile header + Profile *cur = NULL; + char *saved_active = NULL; - char line[2048]; - int section = 0; // 0=none, 1=settings, 2=rules + // old single proxy config (pre multi-proxy), migrated to a proxy called "default" + char old_ip[256] = "", old_user[256] = "", old_pass[256] = ""; + int old_port = 0, old_type = PROXY_TYPE_SOCKS5; - while (fgets(line, sizeof(line), f)) { + while (f && fgets(line, sizeof(line), f)) { // trim newline line[strcspn(line, "\r\n")] = 0; if (strlen(line) == 0 || line[0] == '#') continue; if (strcmp(line, "[SETTINGS]") == 0) { section = 1; continue; } - if (strcmp(line, "[RULES]") == 0) { section = 2; continue; } + if (strcmp(line, "[RULES]") == 0) { section = 2; if (!cur) cur = profile_new_empty("Default"); continue; } + if (strcmp(line, "[PROXIES]") == 0) { section = 3; if (!cur) cur = profile_new_empty("Default"); continue; } + if (strncmp(line, "[PROFILE:", 9) == 0 && line[strlen(line) - 1] == ']') { + line[strlen(line) - 1] = 0; + char *pn = unescape_field(line + 9); + cur = (pn[0] != '\0') ? profile_find(pn) : NULL; + if (!cur && pn[0] != '\0') cur = profile_new_empty(pn); + g_free(pn); + section = cur ? 4 : 0; + continue; + } - if (section == 1) { + if (section == 1 || section == 4) { char *val = strchr(line, '='); if (!val) continue; *val = 0; val++; - if (strcmp(line, "ip") == 0) strncpy(g_proxy_ip, val, sizeof(g_proxy_ip) - 1); - else if (strcmp(line, "port") == 0) g_proxy_port = atoi(val); - else if (strcmp(line, "type") == 0) g_proxy_type = atoi(val); - else if (strcmp(line, "user") == 0) strncpy(g_proxy_user, val, sizeof(g_proxy_user) - 1); - else if (strcmp(line, "pass") == 0) strncpy(g_proxy_pass, val, sizeof(g_proxy_pass) - 1); - else if (strcmp(line, "logging") == 0) g_chk_logging = atoi(val); - else if (strcmp(line, "dns") == 0) g_chk_dns = atoi(val); + if (section == 1 && strcmp(line, "ip") == 0) g_strlcpy(old_ip, val, sizeof(old_ip)); + else if (section == 1 && strcmp(line, "port") == 0) old_port = atoi(val); + else if (section == 1 && strcmp(line, "type") == 0) old_type = atoi(val); + else if (section == 1 && strcmp(line, "user") == 0) g_strlcpy(old_user, val, sizeof(old_user)); + else if (section == 1 && strcmp(line, "pass") == 0) g_strlcpy(old_pass, val, sizeof(old_pass)); + else if (strcmp(line, "defaultProxy") == 0) { + // profile blocks own it, old files kept it in [SETTINGS] + if (!cur) cur = profile_new_empty("Default"); + g_free(cur->default_proxy); + cur->default_proxy = unescape_field(val); + if (cur->default_proxy[0] == '\0') { g_free(cur->default_proxy); cur->default_proxy = NULL; } + } + else if (section == 1 && strcmp(line, "activeProfile") == 0) { + g_free(saved_active); + saved_active = unescape_field(val); + } + else if (section == 1 && strcmp(line, "logging") == 0) g_chk_logging = atoi(val); + else if (section == 1 && strcmp(line, "dns") == 0) g_chk_dns = atoi(val); + } + else if (section == 3 && cur) { + // parse proxy line: name|type|host|port|user|pass + char *fields[6] = {NULL}; + char *p = line; + int idx = 0; + char *token; + while (idx < 6 && (token = strsep(&p, "|")) != NULL) fields[idx++] = token; + if (idx < 4) continue; + + int port = atoi(fields[3]); + ProxyData *px = g_malloc0(sizeof(ProxyData)); + px->name = unescape_field(fields[0]); + px->type = proxy_type_from_int(atoi(fields[1])); + px->host = unescape_field(fields[2]); + px->port = (port >= 1 && port <= 65535) ? (uint16_t)port : 0; + px->username = unescape_field(idx > 4 ? fields[4] : ""); + px->password = unescape_field(idx > 5 ? fields[5] : ""); + if (px->name[0] == '\0' || find_in_list(cur->proxies, px->name)) { + free_proxy_data(px); + continue; + } + cur->proxies = g_list_append(cur->proxies, px); } - else if (section == 2) { + else if (section == 2 && cur) { // parse rule line RuleData *rule = g_malloc0(sizeof(RuleData)); char *p = line; @@ -108,16 +349,59 @@ void load_config() { case 4: rule->process_name = g_strdup(token); break; case 5: rule->target_hosts = g_strdup(token); break; case 6: rule->target_ports = g_strdup(token); break; + case 7: rule->proxy_name = unescape_field(token); break; } idx++; } - if (idx >= 4) { - g_rules_list = g_list_append(g_rules_list, rule); + // old files can hold garbage, keep enums in range + if (idx >= 4 && rule->protocol <= RULE_PROTOCOL_BOTH && rule->action <= RULE_ACTION_BLOCK) { + if (!rule->process_name) rule->process_name = g_strdup(""); + if (!rule->target_hosts) rule->target_hosts = g_strdup(""); + if (!rule->target_ports) rule->target_ports = g_strdup(""); + rule->id = 0; // ids belong to the library, assigned when the rule is added + cur->rules = g_list_append(cur->rules, rule); } else { - g_free(rule); + free_rule_data(rule); } } } - fclose(f); -} \ No newline at end of file + if (f) fclose(f); + + // migrate old single proxy config into the "Default" profile + if (old_ip[0] != '\0' && old_port >= 1 && old_port <= 65535) { + Profile *d = profile_find("Default"); + if (!d) d = profile_new_empty("Default"); + if (!d->proxies) { + ProxyData *px = g_malloc0(sizeof(ProxyData)); + px->name = g_strdup("default"); + px->type = proxy_type_from_int(old_type); + px->host = g_strdup(old_ip); + px->port = (uint16_t)old_port; + px->username = g_strdup(old_user); + px->password = g_strdup(old_pass); + d->proxies = g_list_append(d->proxies, px); + g_free(d->default_proxy); + d->default_proxy = g_strdup("default"); + } + } + + // always at least one profile + if (!g_profiles) profile_new_empty("Default"); + + // default points to nothing -> first proxy + for (GList *l = g_profiles; l != NULL; l = l->next) { + Profile *p = (Profile *)l->data; + if (p->proxies && (!p->default_proxy || !find_in_list(p->proxies, p->default_proxy))) { + g_free(p->default_proxy); + p->default_proxy = g_strdup(((ProxyData *)p->proxies->data)->name); + } + } + + Profile *act = profile_find(saved_active); + if (!act) act = (Profile *)g_profiles->data; + g_free(saved_active); + g_free(g_active_profile); + g_active_profile = g_strdup(act->name); + load_globals_from(act); +} diff --git a/Linux/gui/gui_logs.c b/Linux/gui/gui_logs.c index 8fc8d2f2..8b7520f9 100644 --- a/Linux/gui/gui_logs.c +++ b/Linux/gui/gui_logs.c @@ -56,11 +56,11 @@ void on_clear_log_clicked(GtkButton *button, gpointer user_data) { static void free_connection_data(ConnectionData *data) { if (data) { - free(data->process_name); - free(data->dest_ip); - free(data->proxy_info); - free(data->timestamp); - free(data); + g_free(data->process_name); + g_free(data->dest_ip); + g_free(data->proxy_info); + g_free(data->timestamp); + g_free(data); } } @@ -69,20 +69,28 @@ static gboolean update_log_gui(gpointer user_data) { LogData *data = (LogData *)user_data; if (!data) return FALSE; - GtkTextIter end; - gtk_text_buffer_get_end_iter(log_buffer, &end); + if (log_buffer) { + GtkTextIter end; + gtk_text_buffer_get_end_iter(log_buffer, &end); - char *time_str = get_current_time_str(); - char full_msg[1200]; - snprintf(full_msg, sizeof(full_msg), "%s %s\n", time_str, data->message); - free(time_str); + char *time_str = get_current_time_str(); + char *full_msg = g_strdup_printf("%s %s\n", time_str, data->message); + g_free(time_str); - gtk_text_buffer_insert(log_buffer, &end, full_msg, -1); + // library strings are not guaranteed utf8 + if (!g_utf8_validate(full_msg, -1, NULL)) { + char *fixed = g_utf8_make_valid(full_msg, -1); + g_free(full_msg); + full_msg = fixed; + } + gtk_text_buffer_insert(log_buffer, &end, full_msg, -1); + g_free(full_msg); - trim_buffer_lines(log_buffer, 100); + trim_buffer_lines(log_buffer, 100); + } - free(data->message); - free(data); + g_free(data->message); + g_free(data); return FALSE; // done } @@ -95,11 +103,16 @@ static gboolean update_connection_gui_append(gpointer user_data) { GtkTextIter end; gtk_text_buffer_get_end_iter(conn_buffer, &end); - char line_buffer[1024]; - snprintf(line_buffer, sizeof(line_buffer), "%s %s (PID:%u) -> %s:%u via %s\n", + char *line_buffer = g_strdup_printf("%s %s (PID:%u) -> %s:%u via %s\n", data->timestamp, data->process_name, data->pid, data->dest_ip, data->dest_port, data->proxy_info); + if (!g_utf8_validate(line_buffer, -1, NULL)) { + char *fixed = g_utf8_make_valid(line_buffer, -1); + g_free(line_buffer); + line_buffer = fixed; + } gtk_text_buffer_insert(conn_buffer, &end, line_buffer, -1); + g_free(line_buffer); trim_buffer_lines(conn_buffer, 100); } @@ -109,18 +122,28 @@ static gboolean update_connection_gui_append(gpointer user_data) { } void lib_log_callback(const char *message) { - LogData *data = malloc(sizeof(LogData)); - data->message = strdup(message); + LogData *data = g_malloc0(sizeof(LogData)); + data->message = g_strdup(message ? message : ""); g_idle_add(update_log_gui, data); } +// log a message from the gui itself, same path as library logs +void gui_log(const char *format, ...) { + va_list args; + va_start(args, format); + char *msg = g_strdup_vprintf(format, args); + va_end(args); + lib_log_callback(msg); + g_free(msg); +} + void lib_connection_callback(const char *process_name, uint32_t pid, const char *dest_ip, uint16_t dest_port, const char *proxy_info) { - ConnectionData *data = malloc(sizeof(ConnectionData)); - data->process_name = strdup(process_name); + ConnectionData *data = g_malloc0(sizeof(ConnectionData)); + data->process_name = g_strdup(process_name ? process_name : ""); data->pid = pid; - data->dest_ip = strdup(dest_ip); + data->dest_ip = g_strdup(dest_ip ? dest_ip : ""); data->dest_port = dest_port; - data->proxy_info = strdup(proxy_info); + data->proxy_info = g_strdup(proxy_info ? proxy_info : ""); data->timestamp = get_current_time_str(); g_idle_add(update_connection_gui_append, data); } diff --git a/Linux/gui/gui_rules.c b/Linux/gui/gui_rules.c index df625edd..685d50a2 100644 --- a/Linux/gui/gui_rules.c +++ b/Linux/gui/gui_rules.c @@ -1,23 +1,56 @@ #include "gui.h" +static GtkWidget *rules_window = NULL; static GtkWidget *rules_list_box = NULL; static GtkWidget *btn_select_all_header = NULL; // forward static void refresh_rules_ui(); -static void free_rule_data(RuleData *rule) { +void free_rule_data(RuleData *rule) { if (rule) { - if (rule->process_name) free(rule->process_name); - if (rule->target_hosts) free(rule->target_hosts); - if (rule->target_ports) free(rule->target_ports); - free(rule); + g_free(rule->process_name); + g_free(rule->target_hosts); + g_free(rule->target_ports); + g_free(rule->proxy_name); + g_free(rule); + } +} + +// position of the rule in the library list. rules the library rejected (id 0) are not in it +static int lib_index_of(RuleData *rule) { + int idx = 0; + for (GList *l = g_rules_list; l != NULL; l = l->next) { + if (l->data == rule) break; + if (((RuleData *)l->data)->id != 0) idx++; + } + return idx; +} + +static uint32_t rule_proxy_id(RuleAction action, const char *proxy_name) { + return action == RULE_ACTION_PROXY ? resolve_proxy_id(proxy_name) : 0; +} + +// push the stored rules to the library, call after the proxies were added and before ProxyBridge_Start +void apply_rules_to_lib() { + ProxyBridge_ClearRules(); + int n = 1; + for (GList *l = g_rules_list; l != NULL; l = l->next, n++) { + RuleData *r = (RuleData *)l->data; + char err[256] = ""; + r->id = ProxyBridge_AddRule(r->process_name, r->target_hosts, r->target_ports, r->protocol, r->action, + rule_proxy_id(r->action, r->proxy_name), err, sizeof(err)); + if (r->id == 0) { + gui_log("Rule #%d (%s) not loaded: %s", n, r->process_name, err[0] ? err : "rejected"); + continue; + } + if (!r->enabled) ProxyBridge_DisableRule(r->id); } } static void on_rule_delete(GtkWidget *widget, gpointer data) { RuleData *rule = (RuleData *)data; - ProxyBridge_DeleteRule(rule->id); + if (rule->id != 0) ProxyBridge_DeleteRule(rule->id); g_rules_list = g_list_remove(g_rules_list, rule); free_rule_data(rule); save_config(); @@ -27,53 +60,33 @@ static void on_rule_delete(GtkWidget *widget, gpointer data) { static void on_rule_toggle(GtkToggleButton *btn, gpointer data) { RuleData *rule = (RuleData *)data; rule->enabled = gtk_toggle_button_get_active(btn); - if (rule->enabled) ProxyBridge_EnableRule(rule->id); - else ProxyBridge_DisableRule(rule->id); + if (rule->id != 0) { + if (rule->enabled) ProxyBridge_EnableRule(rule->id); + else ProxyBridge_DisableRule(rule->id); + } save_config(); } -static void on_save_rule(GtkWidget *widget, gpointer data) { - GtkWidget **widgets = (GtkWidget **)data; - GtkWidget *dialog = widgets[0]; - RuleData *edit_rule = (RuleData *)widgets[6]; // existing rule if present - - const char *proc = gtk_entry_get_text(GTK_ENTRY(widgets[1])); - const char *hosts = gtk_entry_get_text(GTK_ENTRY(widgets[2])); - const char *ports = gtk_entry_get_text(GTK_ENTRY(widgets[3])); - RuleProtocol proto = gtk_combo_box_get_active(GTK_COMBO_BOX(widgets[4])); - RuleAction action = gtk_combo_box_get_active(GTK_COMBO_BOX(widgets[5])); +static void on_rule_move(RuleData *rule, int dir) { + GList *link = g_list_find(g_rules_list, rule); + if (!link) return; + GList *other = dir < 0 ? link->prev : link->next; + if (!other) return; - if (strlen(proc) == 0) return; // need process name + // swap the data pointers, keeps the list head valid + gpointer tmp = link->data; + link->data = other->data; + other->data = tmp; - if (edit_rule) { - // update backend and local copy - ProxyBridge_EditRule(edit_rule->id, proc, hosts, ports, proto, action); - - free(edit_rule->process_name); edit_rule->process_name = strdup(proc); - free(edit_rule->target_hosts); edit_rule->target_hosts = strdup(hosts); - free(edit_rule->target_ports); edit_rule->target_ports = strdup(ports); - edit_rule->protocol = proto; - edit_rule->action = action; - } else { - // add new rule - uint32_t new_id = ProxyBridge_AddRule(proc, hosts, ports, proto, action); - RuleData *new_rule = malloc(sizeof(RuleData)); - new_rule->id = new_id; - new_rule->process_name = strdup(proc); - new_rule->target_hosts = strdup(hosts); - new_rule->target_ports = strdup(ports); - new_rule->protocol = proto; - new_rule->action = action; - new_rule->enabled = true; - new_rule->selected = false; - g_rules_list = g_list_append(g_rules_list, new_rule); - } + if (rule->id != 0 && !ProxyBridge_MoveRule(rule->id, lib_index_of(rule))) + gui_log("Could not reorder rule %u in the engine", rule->id); save_config(); refresh_rules_ui(); - gtk_widget_destroy(dialog); - free(widgets); } +static void on_rule_move_up(GtkWidget *widget, gpointer data) { on_rule_move((RuleData *)data, -1); } +static void on_rule_move_down(GtkWidget *widget, gpointer data) { on_rule_move((RuleData *)data, 1); } + static void on_browse_clicked(GtkWidget *widget, gpointer data) { GtkWidget *entry = (GtkWidget *)data; GtkWidget *dialog = gtk_file_chooser_dialog_new("Select Application", @@ -84,18 +97,25 @@ static void on_browse_clicked(GtkWidget *widget, gpointer data) { NULL); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); - char *base = g_path_get_basename(filename); - gtk_entry_set_text(GTK_ENTRY(entry), base); - g_free(base); - g_free(filename); + if (filename) { + char *base = g_path_get_basename(filename); + gtk_entry_set_text(GTK_ENTRY(entry), base); + g_free(base); + g_free(filename); + } } gtk_widget_destroy(dialog); } +// proxy combo only matters for PROXY rules +static void on_action_changed(GtkComboBox *combo, gpointer data) { + gtk_widget_set_sensitive(GTK_WIDGET(data), gtk_combo_box_get_active(combo) == RULE_ACTION_PROXY); +} + static void open_rule_dialog(RuleData *rule) { GtkWidget *dialog = gtk_dialog_new(); gtk_window_set_title(GTK_WINDOW(dialog), rule ? "Edit Rule" : "Add Rule"); - gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window)); + gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(rules_window ? rules_window : window)); gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); gtk_window_set_default_size(GTK_WINDOW(dialog), 500, 400); @@ -112,6 +132,7 @@ static void open_rule_dialog(RuleData *rule) { GtkWidget *proc_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); gtk_box_pack_start(GTK_BOX(proc_box), proc_entry, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(proc_box), browse_btn, FALSE, FALSE, 0); + gtk_widget_set_hexpand(proc_box, TRUE); GtkWidget *host_entry = gtk_entry_new(); GtkWidget *port_entry = gtk_entry_new(); @@ -128,6 +149,18 @@ static void open_rule_dialog(RuleData *rule) { gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(action_combo), "DIRECT"); gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(action_combo), "BLOCK"); + // proxy list: entry 0 = default proxy, then one per configured proxy (same order as g_proxies_list) + GtkWidget *proxy_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proxy_combo), "Default proxy"); + int proxy_sel = 0, pi = 1; + for (GList *l = g_proxies_list; l != NULL; l = l->next, pi++) { + ProxyData *p = (ProxyData *)l->data; + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proxy_combo), p->name); + if (rule && rule->proxy_name && p->name && strcmp(rule->proxy_name, p->name) == 0) proxy_sel = pi; + } + gtk_combo_box_set_active(GTK_COMBO_BOX(proxy_combo), proxy_sel); + g_signal_connect(action_combo, "changed", G_CALLBACK(on_action_changed), proxy_combo); + if (rule) { gtk_entry_set_text(GTK_ENTRY(proc_entry), rule->process_name); gtk_entry_set_text(GTK_ENTRY(host_entry), rule->target_hosts); @@ -153,7 +186,9 @@ static void open_rule_dialog(RuleData *rule) { gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target Host:"), 0, row, 1, 1); gtk_grid_attach(GTK_GRID(grid), host_entry, 1, row, 1, 1); row++; - GtkWidget *host_hint = gtk_label_new("Example: 192.168.1.*; 10.0.0.1-50; *"); + GtkWidget *host_hint = gtk_label_new("Example: 192.168.1.*; 10.0.0.0/8; 10.0.0.1-10.0.0.50; github.com; *.github.com; * (domains are matched via DNS answers)"); + gtk_label_set_line_wrap(GTK_LABEL(host_hint), TRUE); + gtk_label_set_max_width_chars(GTK_LABEL(host_hint), 60); gtk_style_context_add_class(gtk_widget_get_style_context(host_hint), "dim-label"); gtk_widget_set_halign(host_hint, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), host_hint, 1, row, 1, 1); row++; @@ -172,30 +207,92 @@ static void open_rule_dialog(RuleData *rule) { gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Action:"), 0, row, 1, 1); gtk_grid_attach(GTK_GRID(grid), action_combo, 1, row, 1, 1); row++; + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Proxy:"), 0, row, 1, 1); + gtk_grid_attach(GTK_GRID(grid), proxy_combo, 1, row, 1, 1); row++; + + // inline validation error + GtkWidget *err_label = gtk_label_new(""); + gtk_widget_set_halign(err_label, GTK_ALIGN_START); + gtk_label_set_xalign(GTK_LABEL(err_label), 0); + gtk_label_set_line_wrap(GTK_LABEL(err_label), TRUE); + gtk_label_set_max_width_chars(GTK_LABEL(err_label), 60); + gtk_grid_attach(GTK_GRID(grid), err_label, 0, row, 2, 1); row++; + gtk_container_add(GTK_CONTAINER(content), grid); - GtkWidget *save_btn = gtk_button_new_with_label("Save"); - GtkWidget *cancel_btn = gtk_button_new_with_label("Cancel"); - gtk_dialog_add_action_widget(GTK_DIALOG(dialog), cancel_btn, GTK_RESPONSE_CANCEL); - gtk_dialog_add_action_widget(GTK_DIALOG(dialog), save_btn, GTK_RESPONSE_ACCEPT); - - GtkWidget **data = malloc(7 * sizeof(GtkWidget*)); - data[0] = dialog; - data[1] = proc_entry; - data[2] = host_entry; - data[3] = port_entry; - data[4] = proto_combo; - data[5] = action_combo; - data[6] = (GtkWidget*)rule; - - g_signal_connect(save_btn, "clicked", G_CALLBACK(on_save_rule), data); - g_signal_connect(cancel_btn, "clicked", G_CALLBACK(gtk_widget_destroy), NULL); + gtk_dialog_add_button(GTK_DIALOG(dialog), "Cancel", GTK_RESPONSE_CANCEL); + gtk_dialog_add_button(GTK_DIALOG(dialog), "Save", GTK_RESPONSE_ACCEPT); gtk_widget_show_all(dialog); + on_action_changed(GTK_COMBO_BOX(action_combo), proxy_combo); + + while (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + const char *proc = gtk_entry_get_text(GTK_ENTRY(proc_entry)); + const char *hosts = gtk_entry_get_text(GTK_ENTRY(host_entry)); + const char *ports = gtk_entry_get_text(GTK_ENTRY(port_entry)); + int proto_i = gtk_combo_box_get_active(GTK_COMBO_BOX(proto_combo)); + int action_i = gtk_combo_box_get_active(GTK_COMBO_BOX(action_combo)); + if (proto_i < 0) proto_i = RULE_PROTOCOL_BOTH; + if (action_i < 0) action_i = RULE_ACTION_PROXY; + RuleProtocol proto = (RuleProtocol)proto_i; + RuleAction action = (RuleAction)action_i; + + // proxy name from the combo, NULL = default + const char *proxy_name = NULL; + int psel = gtk_combo_box_get_active(GTK_COMBO_BOX(proxy_combo)); + if (action == RULE_ACTION_PROXY && psel > 0) { + ProxyData *pd = (ProxyData *)g_list_nth_data(g_proxies_list, psel - 1); + if (pd) proxy_name = pd->name; + } + + char err[256] = ""; + bool ok = ProxyBridge_ValidateRule(proc, hosts, ports, err, sizeof(err)); + uint32_t pid = rule_proxy_id(action, proxy_name); + + if (ok) { + if (rule && rule->id != 0) { + ok = ProxyBridge_EditRule(rule->id, proc, hosts, ports, proto, action, pid, err, sizeof(err)); + } else { + // new rule, or one the engine rejected when it was loaded + uint32_t new_id = ProxyBridge_AddRule(proc, hosts, ports, proto, action, pid, err, sizeof(err)); + ok = (new_id != 0); + if (ok) { + if (rule && !rule->enabled) ProxyBridge_DisableRule(new_id); + if (rule) { + rule->id = new_id; + ProxyBridge_MoveRule(new_id, lib_index_of(rule)); + } else { + RuleData *new_rule = g_malloc0(sizeof(RuleData)); + new_rule->id = new_id; + new_rule->enabled = true; + g_rules_list = g_list_append(g_rules_list, new_rule); + rule = new_rule; + } + } + } + } + + if (!ok) { + char *esc = g_markup_printf_escaped("%s", err[0] ? err : "Invalid rule."); + gtk_label_set_markup(GTK_LABEL(err_label), esc); + g_free(esc); + continue; + } + + // update local copy + g_free(rule->process_name); rule->process_name = g_strdup(proc); + g_free(rule->target_hosts); rule->target_hosts = g_strdup(hosts); + g_free(rule->target_ports); rule->target_ports = g_strdup(ports); + g_free(rule->proxy_name); rule->proxy_name = proxy_name ? g_strdup(proxy_name) : NULL; + rule->protocol = proto; + rule->action = action; + save_config(); + refresh_rules_ui(); + break; + } + gtk_widget_destroy(dialog); } static void on_rule_edit(GtkWidget *widget, gpointer data) { - on_proxy_rules_clicked(NULL, NULL); // Re-open if closed? Usually modal. - // Wait, on_rule_edit clicked from rules list already open_rule_dialog((RuleData *)data); } @@ -227,16 +324,17 @@ static void on_rule_export_clicked(GtkWidget *widget, gpointer data) { if (((RuleData *)l->data)->selected) { any_selected = true; break; } } if (!any_selected) { - show_message(NULL, GTK_MESSAGE_WARNING, "Please select at least one rule to export."); + show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_WARNING, "Please select at least one rule to export."); return; } - GtkWidget *dialog = gtk_file_chooser_dialog_new("Export Rules", GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL); + GtkWidget *dialog = gtk_file_chooser_dialog_new("Export Rules", GTK_WINDOW(rules_window ? rules_window : window), GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE); gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "proxy_rules.json"); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); - FILE *f = fopen(filename, "w"); + FILE *f = filename ? fopen(filename, "w") : NULL; if (f) { + // both our keys and the macOS ones (ruleProtocol/ruleAction), plus the proxy name fprintf(f, "[\n"); bool first = true; for (GList *l = g_rules_list; l != NULL; l = l->next) { @@ -246,63 +344,105 @@ static void on_rule_export_clicked(GtkWidget *widget, gpointer data) { char *proc = escape_json_string(r->process_name); char *host = escape_json_string(r->target_hosts); char *port = escape_json_string(r->target_ports); + char *pxy = escape_json_string(r->action == RULE_ACTION_PROXY ? r->proxy_name : NULL); const char *proto = (r->protocol == RULE_PROTOCOL_TCP) ? "TCP" : (r->protocol == RULE_PROTOCOL_UDP ? "UDP" : "BOTH"); const char *act = (r->action == RULE_ACTION_PROXY) ? "PROXY" : (r->action == RULE_ACTION_DIRECT ? "DIRECT" : "BLOCK"); - fprintf(f, " {\n \"processNames\": \"%s\",\n \"targetHosts\": \"%s\",\n \"targetPorts\": \"%s\",\n \"protocol\": \"%s\",\n \"action\": \"%s\",\n \"enabled\": %s\n }", proc, host, port, proto, act, r->enabled ? "true" : "false"); - g_free(proc); g_free(host); g_free(port); + fprintf(f, " {\n \"processNames\": \"%s\",\n \"targetHosts\": \"%s\",\n \"targetPorts\": \"%s\",\n \"protocol\": \"%s\",\n \"ruleProtocol\": \"%s\",\n \"action\": \"%s\",\n \"ruleAction\": \"%s\",\n \"proxy\": \"%s\",\n \"enabled\": %s\n }", proc, host, port, proto, proto, act, act, pxy, r->enabled ? "true" : "false"); + g_free(proc); g_free(host); g_free(port); g_free(pxy); first = false; } fprintf(f, "\n]\n"); - fclose(f); - show_message(NULL, GTK_MESSAGE_INFO, "Rules exported successfully."); + if (fclose(f) == 0) show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_INFO, "Rules exported successfully."); + else show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_ERROR, "Failed to write %s", filename); + } else { + show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_ERROR, "Could not open %s for writing.", filename ? filename : "file"); } g_free(filename); } gtk_widget_destroy(dialog); } +// first non-NULL of two (consumes both) +static char *pick_str(char *a, char *b) { + if (a) { g_free(b); return a; } + return b; +} + static void on_rule_import_clicked(GtkWidget *widget, gpointer data) { - GtkWidget *dialog = gtk_file_chooser_dialog_new("Import Rules", GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); + GtkWidget *dialog = gtk_file_chooser_dialog_new("Import Rules", GTK_WINDOW(rules_window ? rules_window : window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); char *content = NULL; gsize len; - if (g_file_get_contents(filename, &content, &len, NULL)) { + if (filename && g_file_get_contents(filename, &content, &len, NULL)) { char *curr = content; - int imported = 0; + int imported = 0, failed = 0, index = 0; + GString *fail_msg = g_string_new(""); + while ((curr = strchr(curr, '{')) != NULL) { char *end = strchr(curr, '}'); if (!end) break; char saved = *end; *end = '\0'; + index++; + // our keys first, macOS keys as fallback char *proc = extract_sub_json_str(curr, "processNames"); char *host = extract_sub_json_str(curr, "targetHosts"); char *port = extract_sub_json_str(curr, "targetPorts"); - char *proto_s = extract_sub_json_str(curr, "protocol"); - char *act_s = extract_sub_json_str(curr, "action"); - bool en = extract_sub_json_bool(curr, "enabled"); - - if (proc && host && port && proto_s && act_s) { + char *proto_s = pick_str(extract_sub_json_str(curr, "protocol"), extract_sub_json_str(curr, "ruleProtocol")); + char *act_s = pick_str(extract_sub_json_str(curr, "action"), extract_sub_json_str(curr, "ruleAction")); + char *proxy_s = extract_sub_json_str(curr, "proxy"); + bool en = json_has_key(curr, "enabled") ? extract_sub_json_bool(curr, "enabled") : true; + + if (!proc) { + failed++; + if (fail_msg->len < 3000) g_string_append_printf(fail_msg, "Rule #%d: missing processNames\n", index); + } else { RuleProtocol p = RULE_PROTOCOL_BOTH; - if (strcmp(proto_s, "TCP") == 0) p = RULE_PROTOCOL_TCP; - else if (strcmp(proto_s, "UDP") == 0) p = RULE_PROTOCOL_UDP; + if (proto_s && g_ascii_strcasecmp(proto_s, "TCP") == 0) p = RULE_PROTOCOL_TCP; + else if (proto_s && g_ascii_strcasecmp(proto_s, "UDP") == 0) p = RULE_PROTOCOL_UDP; + // anything that is not DIRECT/BLOCK is PROXY (macOS stores the proxy id there) RuleAction a = RULE_ACTION_PROXY; - if (strcmp(act_s, "DIRECT") == 0) a = RULE_ACTION_DIRECT; - else if (strcmp(act_s, "BLOCK") == 0) a = RULE_ACTION_BLOCK; - - uint32_t nid = ProxyBridge_AddRule(proc, host, port, p, a); - if (!en) ProxyBridge_DisableRule(nid); - RuleData *nd = malloc(sizeof(RuleData)); - nd->id = nid; nd->process_name = strdup(proc); nd->target_hosts = strdup(host); - nd->target_ports = strdup(port); nd->protocol = p; nd->action = a; nd->enabled = en; nd->selected = false; - g_rules_list = g_list_append(g_rules_list, nd); - imported++; + if (act_s && g_ascii_strcasecmp(act_s, "DIRECT") == 0) a = RULE_ACTION_DIRECT; + else if (act_s && g_ascii_strcasecmp(act_s, "BLOCK") == 0) a = RULE_ACTION_BLOCK; + + const char *h = host ? host : "*"; + const char *po = port ? port : "*"; + // unknown proxy name -> default proxy + ProxyData *px = (a == RULE_ACTION_PROXY) ? find_proxy_by_name(proxy_s) : NULL; + + char err[256] = ""; + uint32_t nid = ProxyBridge_AddRule(proc, h, po, p, a, px ? px->id : 0, err, sizeof(err)); + if (nid == 0) { + failed++; + if (fail_msg->len < 3000) g_string_append_printf(fail_msg, "Rule #%d (%s): %s\n", index, proc, err[0] ? err : "rejected"); + } else { + if (!en) ProxyBridge_DisableRule(nid); + RuleData *nd = g_malloc0(sizeof(RuleData)); + nd->id = nid; nd->process_name = g_strdup(proc); nd->target_hosts = g_strdup(h); + nd->target_ports = g_strdup(po); nd->protocol = p; nd->action = a; nd->enabled = en; + nd->proxy_name = px ? g_strdup(px->name) : NULL; + g_rules_list = g_list_append(g_rules_list, nd); + imported++; + } } - g_free(proc); g_free(host); g_free(port); g_free(proto_s); g_free(act_s); + g_free(proc); g_free(host); g_free(port); g_free(proto_s); g_free(act_s); g_free(proxy_s); *end = saved; curr = end + 1; } g_free(content); - if (imported > 0) { refresh_rules_ui(); show_message(NULL, GTK_MESSAGE_INFO, "Imported %d rules.", imported); } + + if (imported > 0) { save_config(); refresh_rules_ui(); } + GtkWindow *parent = GTK_WINDOW(rules_window ? rules_window : window); + if (failed > 0) { + show_message(parent, GTK_MESSAGE_WARNING, "Imported %d rules, %d failed validation and were skipped:\n\n%s", imported, failed, fail_msg->str); + } else if (imported > 0) { + show_message(parent, GTK_MESSAGE_INFO, "Imported %d rules.", imported); + } else { + show_message(parent, GTK_MESSAGE_WARNING, "No rules found in the file."); + } + g_string_free(fail_msg, TRUE); + } else { + show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_ERROR, "Could not read the selected file."); } g_free(filename); } @@ -321,11 +461,12 @@ static void on_bulk_delete_clicked(GtkWidget *widget, gpointer data) { if (!to_delete) return; for (GList *d = to_delete; d != NULL; d = d->next) { RuleData *rule = (RuleData *)d->data; - ProxyBridge_DeleteRule(rule->id); + if (rule->id != 0) ProxyBridge_DeleteRule(rule->id); g_rules_list = g_list_remove(g_rules_list, rule); free_rule_data(rule); } g_list_free(to_delete); + save_config(); refresh_rules_ui(); } @@ -371,8 +512,9 @@ static void refresh_rules_ui() { gtk_widget_set_hexpand(h_host, TRUE); gtk_grid_attach(GTK_GRID(grid), h_host, 5, row, 1, 1); gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Protocol"), 6, row, 1, 1); gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Action"), 7, row, 1, 1); + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Proxy"), 8, row, 1, 1); row++; - gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, 8, 1); row++; + gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, 9, 1); row++; int sr_counter = 1; for (GList *l = g_rules_list; l != NULL; l = l->next) { @@ -392,8 +534,19 @@ static void refresh_rules_ui() { g_signal_connect(btn_edit, "clicked", G_CALLBACK(on_rule_edit), r); GtkWidget *btn_del = gtk_button_new_with_label("Delete"); g_signal_connect(btn_del, "clicked", G_CALLBACK(on_rule_delete), r); + // list position is the priority, first match wins + GtkWidget *btn_up = gtk_button_new_with_label("\u25B2"); + gtk_widget_set_tooltip_text(btn_up, "Move up (higher priority)"); + gtk_widget_set_sensitive(btn_up, l->prev != NULL); + g_signal_connect(btn_up, "clicked", G_CALLBACK(on_rule_move_up), r); + GtkWidget *btn_down = gtk_button_new_with_label("\u25BC"); + gtk_widget_set_tooltip_text(btn_down, "Move down (lower priority)"); + gtk_widget_set_sensitive(btn_down, l->next != NULL); + g_signal_connect(btn_down, "clicked", G_CALLBACK(on_rule_move_down), r); gtk_box_pack_start(GTK_BOX(act_box), btn_edit, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(act_box), btn_del, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(act_box), btn_up, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(act_box), btn_down, FALSE, FALSE, 0); gtk_grid_attach(GTK_GRID(grid), act_box, 2, row, 1, 1); char sr_str[16]; snprintf(sr_str, sizeof(sr_str), "%d", sr_counter++); @@ -408,26 +561,51 @@ static void refresh_rules_ui() { gtk_grid_attach(GTK_GRID(grid), l_host, 5, row, 1, 1); const char* proto_strs[] = {"TCP", "UDP", "BOTH"}; - gtk_grid_attach(GTK_GRID(grid), gtk_label_new(proto_strs[r->protocol]), 6, row, 1, 1); + gtk_grid_attach(GTK_GRID(grid), gtk_label_new(proto_strs[r->protocol <= RULE_PROTOCOL_BOTH ? r->protocol : RULE_PROTOCOL_BOTH]), 6, row, 1, 1); const char* action_strs[] = {"PROXY", "DIRECT", "BLOCK"}; - GtkWidget *l_act = gtk_label_new(action_strs[r->action]); + GtkWidget *l_act = gtk_label_new(action_strs[r->action <= RULE_ACTION_BLOCK ? r->action : RULE_ACTION_PROXY]); GtkStyleContext *context = gtk_widget_get_style_context(l_act); if (r->action == RULE_ACTION_PROXY) gtk_style_context_add_class(context, "success"); else if (r->action == RULE_ACTION_DIRECT) gtk_style_context_add_class(context, "info"); else gtk_style_context_add_class(context, "warning"); gtk_grid_attach(GTK_GRID(grid), l_act, 7, row, 1, 1); + // proxy only matters for PROXY rules + const char *proxy_txt = "-"; + if (r->action == RULE_ACTION_PROXY) + proxy_txt = (r->proxy_name && *r->proxy_name && find_proxy_by_name(r->proxy_name)) ? r->proxy_name : "default"; + GtkWidget *l_proxy = gtk_label_new(proxy_txt); + gtk_label_set_ellipsize(GTK_LABEL(l_proxy), PANGO_ELLIPSIZE_END); + gtk_grid_attach(GTK_GRID(grid), l_proxy, 8, row, 1, 1); + row++; - gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, 8, 1); row++; + gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, 9, 1); row++; } gtk_widget_show_all(rules_list_box); } +void refresh_rules_window() { + refresh_rules_ui(); +} + +// widgets are gone, refresh_rules_ui must not touch them any more +static void on_rules_window_destroy(GtkWidget *widget, gpointer data) { + rules_window = NULL; + rules_list_box = NULL; + btn_select_all_header = NULL; +} + void on_proxy_rules_clicked(GtkWidget *widget, gpointer data) { + if (rules_window) { + gtk_window_present(GTK_WINDOW(rules_window)); + return; + } GtkWidget *dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL); + rules_window = dialog; + g_signal_connect(dialog, "destroy", G_CALLBACK(on_rules_window_destroy), NULL); gtk_window_set_title(GTK_WINDOW(dialog), "Process Rules"); - gtk_window_set_default_size(GTK_WINDOW(dialog), 800, 500); + gtk_window_set_default_size(GTK_WINDOW(dialog), 1000, 500); gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window)); GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); gtk_container_set_border_width(GTK_CONTAINER(vbox), 20); diff --git a/Linux/gui/gui_settings.c b/Linux/gui/gui_settings.c index 3268f9ca..6de7face 100644 --- a/Linux/gui/gui_settings.c +++ b/Linux/gui/gui_settings.c @@ -1,57 +1,130 @@ #include "gui.h" -// settings stuff +// settings stuff, proxy list manager + +GList *g_proxies_list = NULL; +char *g_default_proxy = NULL; + +enum { + COL_NAME = 0, + COL_TYPE, + COL_ADDR, + COL_AUTH, + COL_DEFAULT, + COL_PTR, + N_COLS +}; + +void free_proxy_data(ProxyData *p) { + if (!p) return; + g_free(p->name); + g_free(p->host); + g_free(p->username); + // dont leave passwords lying around in freed memory + if (p->password) { + memset(p->password, 0, strlen(p->password)); + g_free(p->password); + } + g_free(p); +} + +ProxyData *find_proxy_by_name(const char *name) { + if (!name || !*name) return NULL; + for (GList *l = g_proxies_list; l != NULL; l = l->next) { + ProxyData *p = (ProxyData *)l->data; + if (p->name && strcmp(p->name, name) == 0) return p; + } + return NULL; +} + +// name -> library id, 0 means default proxy (empty or unknown name) +uint32_t resolve_proxy_id(const char *name) { + ProxyData *p = find_proxy_by_name(name); + return p ? p->id : 0; +} + +// push the stored proxies to the library, call before ProxyBridge_Start +void apply_proxies_to_lib() { + ProxyBridge_ClearProxies(); + for (GList *l = g_proxies_list; l != NULL; l = l->next) { + ProxyData *p = (ProxyData *)l->data; + char err[256] = ""; + p->id = ProxyBridge_AddProxy(p->name, p->type, p->host, p->port, p->username, p->password, err, sizeof(err)); + if (p->id == 0) gui_log("Proxy '%s' rejected: %s", p->name, err[0] ? err : "unknown error"); + } + ProxyBridge_SetDefaultProxy(resolve_proxy_id(g_default_proxy)); +} + +static bool port_text_valid(const char *port_text, int *port_out) { + if (!port_text || !*port_text || strspn(port_text, "0123456789") != strlen(port_text) || strlen(port_text) > 5) return false; + int p = (int)safe_strtol(port_text); + if (p < 1 || p > 65535) return false; + if (port_out) *port_out = p; + return true; +} + +// ---- test ---- static gboolean on_test_done(gpointer user_data) { TestResultData *data = (TestResultData *)user_data; gtk_text_buffer_set_text(data->buffer, data->result_text, -1); gtk_widget_set_sensitive(data->btn, TRUE); - free(data->result_text); - free(data); + g_object_unref(data->buffer); + g_object_unref(data->btn); + g_free(data->result_text); + g_free(data); return FALSE; } static gpointer run_test_thread(gpointer user_data) { struct TestRunnerData *req = (struct TestRunnerData *)user_data; - char *buffer = malloc(4096); - memset(buffer, 0, 4096); + char *buffer = g_malloc0(4096); // run test - ProxyBridge_TestConnection(req->host, req->port, buffer, 4096); + ProxyBridge_TestProxy(req->proxy_id, req->host, req->port, buffer, 4096); + buffer[4095] = '\0'; + if (!g_utf8_validate(buffer, -1, NULL)) { + char *fixed = g_utf8_make_valid(buffer, -1); + g_free(buffer); + buffer = fixed; + } - TestResultData *res = malloc(sizeof(TestResultData)); + TestResultData *res = g_malloc0(sizeof(TestResultData)); res->result_text = buffer; - res->buffer = req->ui_info->output_buffer; - res->btn = req->ui_info->test_btn; + res->buffer = req->buffer; + res->btn = req->btn; g_idle_add(on_test_done, res); - free(req->host); - free(req); + g_free(req->host); + g_free(req); return NULL; } +static ProxyData *get_selected_proxy(ConfigInfo *info) { + GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(info->tree)); + GtkTreeModel *model; + GtkTreeIter iter; + if (!gtk_tree_selection_get_selected(sel, &model, &iter)) return NULL; + gpointer ptr = NULL; + gtk_tree_model_get(model, &iter, COL_PTR, &ptr, -1); + return (ProxyData *)ptr; +} + static void on_start_test_clicked(GtkWidget *widget, gpointer data) { ConfigInfo *info = (ConfigInfo *)data; - // check inputs - const char *ip_text = gtk_entry_get_text(GTK_ENTRY(info->ip_entry)); - const char *port_text = gtk_entry_get_text(GTK_ENTRY(info->port_entry)); - - if (!ip_text || strlen(ip_text) == 0 || strspn(port_text, "0123456789") != strlen(port_text) || strlen(port_text) == 0) { - gtk_text_buffer_set_text(info->output_buffer, "Error: Invalid Proxy IP or Port.", -1); + ProxyData *px = get_selected_proxy(info); + if (!px) { + gtk_text_buffer_set_text(info->output_buffer, "Error: Select a proxy to test.", -1); + return; + } + if (px->id == 0) { + gtk_text_buffer_set_text(info->output_buffer, "Error: This proxy was rejected by the engine, edit and save it again.", -1); return; } - - // save config - ProxyType type = (gtk_combo_box_get_active(GTK_COMBO_BOX(info->type_combo)) == 0) ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; - int port = (int)safe_strtol(port_text); - const char *user = gtk_entry_get_text(GTK_ENTRY(info->user_entry)); - const char *pass = gtk_entry_get_text(GTK_ENTRY(info->pass_entry)); - - ProxyBridge_SetProxyConfig(type, ip_text, port, user, pass); // get target const char *t_host = gtk_entry_get_text(GTK_ENTRY(info->test_host)); @@ -59,24 +132,303 @@ static void on_start_test_clicked(GtkWidget *widget, gpointer data) { if (!t_host || strlen(t_host) == 0) t_host = "google.com"; int t_port = (int)safe_strtol(t_port_s); - if (t_port <= 0) t_port = 80; + if (t_port <= 0 || t_port > 65535) t_port = 80; // update gui gtk_text_buffer_set_text(info->output_buffer, "Testing connection... Please wait...", -1); gtk_widget_set_sensitive(info->test_btn, FALSE); - // start thread - struct TestRunnerData *req = malloc(sizeof(struct TestRunnerData)); - req->host = strdup(t_host); - req->port = t_port; - req->ui_info = info; + // start thread, widgets are ref'd because the dialog can close before the test ends + struct TestRunnerData *req = g_malloc0(sizeof(struct TestRunnerData)); + req->proxy_id = px->id; + req->host = g_strdup(t_host); + req->port = (uint16_t)t_port; + req->buffer = g_object_ref(info->output_buffer); + req->btn = g_object_ref(info->test_btn); GThread *thread = g_thread_new("test_conn", run_test_thread, req); g_thread_unref(thread); } +// ---- list ---- + +static void refresh_proxy_store(ConfigInfo *info) { + gtk_list_store_clear(info->store); + for (GList *l = g_proxies_list; l != NULL; l = l->next) { + ProxyData *p = (ProxyData *)l->data; + char *addr = g_strdup_printf("%s:%u", p->host, p->port); + bool is_def = g_default_proxy && p->name && strcmp(g_default_proxy, p->name) == 0; + GtkTreeIter iter; + gtk_list_store_append(info->store, &iter); + gtk_list_store_set(info->store, &iter, + COL_NAME, p->name, + COL_TYPE, p->type == PROXY_TYPE_HTTP ? "HTTP" : "SOCKS5", + COL_ADDR, addr, + COL_AUTH, (p->username && *p->username) ? "Yes" : "No", + COL_DEFAULT, is_def ? "Yes" : "", + COL_PTR, p, + -1); + g_free(addr); + } +} + +static void select_proxy_row(ConfigInfo *info, ProxyData *target) { + GtkTreeModel *model = GTK_TREE_MODEL(info->store); + GtkTreeIter iter; + if (!target || !gtk_tree_model_get_iter_first(model, &iter)) return; + do { + gpointer ptr = NULL; + gtk_tree_model_get(model, &iter, COL_PTR, &ptr, -1); + if (ptr == target) { + gtk_tree_selection_select_iter(gtk_tree_view_get_selection(GTK_TREE_VIEW(info->tree)), &iter); + return; + } + } while (gtk_tree_model_iter_next(model, &iter)); +} + +// add/edit dialog. existing == NULL adds a new one. returns the proxy that was saved or NULL +static ProxyData *proxy_edit_dialog(GtkWindow *parent, ProxyData *existing) { + GtkWidget *dialog = gtk_dialog_new_with_buttons(existing ? "Edit Proxy" : "Add Proxy", + parent, + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + "Cancel", GTK_RESPONSE_CANCEL, + "Save", GTK_RESPONSE_ACCEPT, + NULL); + gtk_window_set_default_size(GTK_WINDOW(dialog), 420, -1); + GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + GtkWidget *grid = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(grid), 5); + gtk_grid_set_column_spacing(GTK_GRID(grid), 10); + gtk_container_set_border_width(GTK_CONTAINER(grid), 10); + gtk_box_pack_start(GTK_BOX(content_area), grid, TRUE, TRUE, 0); + + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Name:"), 0, 0, 1, 1); + GtkWidget *name_entry = gtk_entry_new(); + gtk_widget_set_hexpand(name_entry, TRUE); + gtk_grid_attach(GTK_GRID(grid), name_entry, 1, 0, 1, 1); + + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Type:"), 0, 1, 1, 1); + GtkWidget *type_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(type_combo), "HTTP"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(type_combo), "SOCKS5"); + gtk_combo_box_set_active(GTK_COMBO_BOX(type_combo), 1); + gtk_grid_attach(GTK_GRID(grid), type_combo, 1, 1, 1, 1); + + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Host:"), 0, 2, 1, 1); + GtkWidget *ip_entry = gtk_entry_new(); + gtk_grid_attach(GTK_GRID(grid), ip_entry, 1, 2, 1, 1); + + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Port:"), 0, 3, 1, 1); + GtkWidget *port_entry = gtk_entry_new(); + gtk_grid_attach(GTK_GRID(grid), port_entry, 1, 3, 1, 1); + + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Username:"), 0, 4, 1, 1); + GtkWidget *user_entry = gtk_entry_new(); + gtk_grid_attach(GTK_GRID(grid), user_entry, 1, 4, 1, 1); + + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Password:"), 0, 5, 1, 1); + GtkWidget *pass_entry = gtk_entry_new(); + gtk_entry_set_visibility(GTK_ENTRY(pass_entry), FALSE); + gtk_grid_attach(GTK_GRID(grid), pass_entry, 1, 5, 1, 1); + + GtkWidget *err_label = gtk_label_new(""); + gtk_widget_set_halign(err_label, GTK_ALIGN_START); + gtk_label_set_line_wrap(GTK_LABEL(err_label), TRUE); + gtk_label_set_xalign(GTK_LABEL(err_label), 0); + gtk_grid_attach(GTK_GRID(grid), err_label, 0, 6, 2, 1); + + if (existing) { + gtk_entry_set_text(GTK_ENTRY(name_entry), existing->name); + gtk_combo_box_set_active(GTK_COMBO_BOX(type_combo), existing->type == PROXY_TYPE_HTTP ? 0 : 1); + gtk_entry_set_text(GTK_ENTRY(ip_entry), existing->host); + if (existing->port != 0) { + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%u", existing->port); + gtk_entry_set_text(GTK_ENTRY(port_entry), port_str); + } + gtk_entry_set_text(GTK_ENTRY(user_entry), existing->username ? existing->username : ""); + gtk_entry_set_text(GTK_ENTRY(pass_entry), existing->password ? existing->password : ""); + } else if (!g_proxies_list) { + gtk_entry_set_text(GTK_ENTRY(name_entry), "default"); + } + + gtk_widget_show_all(dialog); + + ProxyData *result = NULL; + while (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + const char *name = gtk_entry_get_text(GTK_ENTRY(name_entry)); + const char *ip_text = gtk_entry_get_text(GTK_ENTRY(ip_entry)); + const char *port_text = gtk_entry_get_text(GTK_ENTRY(port_entry)); + const char *user = gtk_entry_get_text(GTK_ENTRY(user_entry)); + const char *pass = gtk_entry_get_text(GTK_ENTRY(pass_entry)); + ProxyType type = (gtk_combo_box_get_active(GTK_COMBO_BOX(type_combo)) == 0) ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; + int port = 0; + + char *name_t = g_strstrip(g_strdup(name)); + char *host_t = g_strstrip(g_strdup(ip_text)); + const char *msg = NULL; + + if (name_t[0] == '\0') msg = "Name cannot be empty."; + else if (find_proxy_by_name(name_t) && find_proxy_by_name(name_t) != existing) msg = "A proxy with this name already exists."; + else if (host_t[0] == '\0') msg = "Host (IP/Domain) cannot be empty."; + else if (!port_text_valid(port_text, &port)) msg = "Port must be a number between 1 and 65535."; + + if (msg) { + gtk_label_set_markup(GTK_LABEL(err_label), ""); + char *esc = g_markup_printf_escaped("%s", msg); + gtk_label_set_markup(GTK_LABEL(err_label), esc); + g_free(esc); + g_free(name_t); g_free(host_t); + continue; + } + + // library validates too (and keeps the proxy list in sync) + char err[256] = ""; + uint32_t id = existing ? existing->id : 0; + bool ok; + if (id != 0) { + ok = ProxyBridge_EditProxy(id, name_t, type, host_t, (uint16_t)port, user, pass, err, sizeof(err)); + } else { + id = ProxyBridge_AddProxy(name_t, type, host_t, (uint16_t)port, user, pass, err, sizeof(err)); + ok = (id != 0); + } + if (!ok) { + char *esc = g_markup_printf_escaped("%s", err[0] ? err : "Engine rejected the proxy settings."); + gtk_label_set_markup(GTK_LABEL(err_label), esc); + g_free(esc); + g_free(name_t); g_free(host_t); + continue; + } + + ProxyData *p = existing; + if (!p) { + p = g_malloc0(sizeof(ProxyData)); + g_proxies_list = g_list_append(g_proxies_list, p); + } else if (p->name && strcmp(p->name, name_t) != 0) { + // renamed, keep default and rules pointing at it + if (g_default_proxy && strcmp(g_default_proxy, p->name) == 0) { + g_free(g_default_proxy); + g_default_proxy = g_strdup(name_t); + } + for (GList *l = g_rules_list; l != NULL; l = l->next) { + RuleData *r = (RuleData *)l->data; + if (r->proxy_name && strcmp(r->proxy_name, p->name) == 0) { + g_free(r->proxy_name); + r->proxy_name = g_strdup(name_t); + } + } + } + p->id = id; + g_free(p->name); p->name = name_t; + g_free(p->host); p->host = host_t; + g_free(p->username); p->username = g_strdup(user); + if (p->password) memset(p->password, 0, strlen(p->password)); + g_free(p->password); p->password = g_strdup(pass); + p->type = type; + p->port = (uint16_t)port; + + // first proxy becomes the default + if (!g_default_proxy || !find_proxy_by_name(g_default_proxy)) { + g_free(g_default_proxy); + g_default_proxy = g_strdup(p->name); + ProxyBridge_SetDefaultProxy(p->id); + } + result = p; + break; + } + + gtk_widget_destroy(dialog); + return result; +} + +static void on_proxy_add(GtkWidget *widget, gpointer data) { + ConfigInfo *info = (ConfigInfo *)data; + ProxyData *p = proxy_edit_dialog(GTK_WINDOW(info->dialog), NULL); + if (!p) return; + save_config(); + refresh_proxy_store(info); + select_proxy_row(info, p); +} + +static void on_proxy_edit(GtkWidget *widget, gpointer data) { + ConfigInfo *info = (ConfigInfo *)data; + ProxyData *sel = get_selected_proxy(info); + if (!sel) { + show_message(GTK_WINDOW(info->dialog), GTK_MESSAGE_WARNING, "Select a proxy first."); + return; + } + ProxyData *p = proxy_edit_dialog(GTK_WINDOW(info->dialog), sel); + if (!p) return; + save_config(); + refresh_proxy_store(info); + select_proxy_row(info, p); +} + +static void on_proxy_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data) { + on_proxy_edit(NULL, data); +} + +static void on_proxy_delete(GtkWidget *widget, gpointer data) { + ConfigInfo *info = (ConfigInfo *)data; + ProxyData *p = get_selected_proxy(info); + if (!p) { + show_message(GTK_WINDOW(info->dialog), GTK_MESSAGE_WARNING, "Select a proxy first."); + return; + } + + GtkWidget *q = gtk_message_dialog_new(GTK_WINDOW(info->dialog), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, "Delete proxy '%s'?\nRules using it will fall back to the default proxy.", p->name); + int resp = gtk_dialog_run(GTK_DIALOG(q)); + gtk_widget_destroy(q); + if (resp != GTK_RESPONSE_YES) return; + + if (p->id != 0) ProxyBridge_DeleteProxy(p->id); + + // rules that used it now use the default proxy (library does the same) + for (GList *l = g_rules_list; l != NULL; l = l->next) { + RuleData *r = (RuleData *)l->data; + if (r->proxy_name && strcmp(r->proxy_name, p->name) == 0) { + g_free(r->proxy_name); + r->proxy_name = NULL; + } + } + + bool was_default = g_default_proxy && strcmp(g_default_proxy, p->name) == 0; + g_proxies_list = g_list_remove(g_proxies_list, p); + free_proxy_data(p); + + if (was_default) { + g_free(g_default_proxy); + g_default_proxy = g_proxies_list ? g_strdup(((ProxyData *)g_proxies_list->data)->name) : NULL; + ProxyBridge_SetDefaultProxy(resolve_proxy_id(g_default_proxy)); + } + + save_config(); + refresh_proxy_store(info); +} + +static void on_proxy_set_default(GtkWidget *widget, gpointer data) { + ConfigInfo *info = (ConfigInfo *)data; + ProxyData *p = get_selected_proxy(info); + if (!p) { + show_message(GTK_WINDOW(info->dialog), GTK_MESSAGE_WARNING, "Select a proxy first."); + return; + } + g_free(g_default_proxy); + g_default_proxy = g_strdup(p->name); + ProxyBridge_SetDefaultProxy(p->id); + save_config(); + refresh_proxy_store(info); + select_proxy_row(info, p); + + char *status_msg = g_strdup_printf("Default proxy: %s (%s:%u)", p->name, p->host, p->port); + gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, status_msg); + g_free(status_msg); +} + void on_proxy_configure(GtkWidget *widget, gpointer data) { ConfigInfo info; + memset(&info, 0, sizeof(info)); GtkWidget *content_area; GtkWidget *grid; @@ -84,11 +436,10 @@ void on_proxy_configure(GtkWidget *widget, gpointer data) { info.dialog = gtk_dialog_new_with_buttons("Proxy Settings", GTK_WINDOW(window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, - "Cancel", GTK_RESPONSE_CANCEL, - "Save", GTK_RESPONSE_ACCEPT, + "Close", GTK_RESPONSE_CLOSE, NULL); // wider window - gtk_window_set_default_size(GTK_WINDOW(info.dialog), 600, 500); + gtk_window_set_default_size(GTK_WINDOW(info.dialog), 650, 560); content_area = gtk_dialog_get_content_area(GTK_DIALOG(info.dialog)); grid = gtk_grid_new(); @@ -97,69 +448,69 @@ void on_proxy_configure(GtkWidget *widget, gpointer data) { gtk_container_set_border_width(GTK_CONTAINER(grid), 10); gtk_box_pack_start(GTK_BOX(content_area), grid, TRUE, TRUE, 0); - // proxy type dropdown - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Type:"), 0, 0, 1, 1); - info.type_combo = gtk_combo_box_text_new(); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(info.type_combo), "HTTP"); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(info.type_combo), "SOCKS5"); - gtk_combo_box_set_active(GTK_COMBO_BOX(info.type_combo), g_proxy_type == PROXY_TYPE_HTTP ? 0 : 1); - gtk_grid_attach(GTK_GRID(grid), info.type_combo, 1, 0, 3, 1); - - // ip field - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Host:"), 0, 1, 1, 1); - info.ip_entry = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(info.ip_entry), g_proxy_ip); - gtk_widget_set_hexpand(info.ip_entry, TRUE); - gtk_grid_attach(GTK_GRID(grid), info.ip_entry, 1, 1, 3, 1); - - // port field - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Port:"), 0, 2, 1, 1); - info.port_entry = gtk_entry_new(); - if (g_proxy_port != 0) { - char port_str[16]; - snprintf(port_str, sizeof(port_str), "%d", g_proxy_port); - gtk_entry_set_text(GTK_ENTRY(info.port_entry), port_str); - } - gtk_grid_attach(GTK_GRID(grid), info.port_entry, 1, 2, 3, 1); - - // user field - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Username:"), 0, 3, 1, 1); - info.user_entry = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(info.user_entry), g_proxy_user); - gtk_grid_attach(GTK_GRID(grid), info.user_entry, 1, 3, 3, 1); - - // password - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Password:"), 0, 4, 1, 1); - info.pass_entry = gtk_entry_new(); - gtk_entry_set_visibility(GTK_ENTRY(info.pass_entry), FALSE); - gtk_entry_set_text(GTK_ENTRY(info.pass_entry), g_proxy_pass); - gtk_grid_attach(GTK_GRID(grid), info.pass_entry, 1, 4, 3, 1); + // proxy list + info.store = gtk_list_store_new(N_COLS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER); + info.tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(info.store)); + g_object_unref(info.store); // tree holds it now + const char *titles[] = {"Name", "Type", "Host:Port", "Auth", "Default"}; + for (int i = 0; i < 5; i++) { + GtkCellRenderer *r = gtk_cell_renderer_text_new(); + GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes(titles[i], r, "text", i, NULL); + gtk_tree_view_column_set_resizable(c, TRUE); + if (i == 2) gtk_tree_view_column_set_expand(c, TRUE); + gtk_tree_view_append_column(GTK_TREE_VIEW(info.tree), c); + } + g_signal_connect(info.tree, "row-activated", G_CALLBACK(on_proxy_row_activated), &info); + GtkWidget *tree_scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(tree_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_widget_set_size_request(tree_scroll, -1, 180); + gtk_widget_set_hexpand(tree_scroll, TRUE); + gtk_widget_set_vexpand(tree_scroll, TRUE); + gtk_container_add(GTK_CONTAINER(tree_scroll), info.tree); + gtk_grid_attach(GTK_GRID(grid), tree_scroll, 0, 0, 4, 1); + + // list buttons + GtkWidget *btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + GtkWidget *add_btn = gtk_button_new_with_label("Add"); + GtkWidget *edit_btn = gtk_button_new_with_label("Edit"); + GtkWidget *del_btn = gtk_button_new_with_label("Delete"); + GtkWidget *def_btn = gtk_button_new_with_label("Set Default"); + g_signal_connect(add_btn, "clicked", G_CALLBACK(on_proxy_add), &info); + g_signal_connect(edit_btn, "clicked", G_CALLBACK(on_proxy_edit), &info); + g_signal_connect(del_btn, "clicked", G_CALLBACK(on_proxy_delete), &info); + g_signal_connect(def_btn, "clicked", G_CALLBACK(on_proxy_set_default), &info); + gtk_box_pack_start(GTK_BOX(btn_box), add_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(btn_box), edit_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(btn_box), del_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(btn_box), def_btn, FALSE, FALSE, 0); + gtk_grid_attach(GTK_GRID(grid), btn_box, 0, 1, 4, 1); // test section - gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 5, 4, 1); + gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 2, 4, 1); - GtkWidget *test_label = gtk_label_new("Test Connection"); + GtkWidget *test_label = gtk_label_new("Test Selected Proxy"); gtk_label_set_use_markup(GTK_LABEL(test_label), TRUE); gtk_widget_set_halign(test_label, GTK_ALIGN_START); - gtk_grid_attach(GTK_GRID(grid), test_label, 0, 6, 4, 1); + gtk_grid_attach(GTK_GRID(grid), test_label, 0, 3, 4, 1); // target - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target:"), 0, 7, 1, 1); + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target:"), 0, 4, 1, 1); info.test_host = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(info.test_host), "google.com"); - gtk_grid_attach(GTK_GRID(grid), info.test_host, 1, 7, 1, 1); + gtk_widget_set_hexpand(info.test_host, TRUE); + gtk_grid_attach(GTK_GRID(grid), info.test_host, 1, 4, 1, 1); // target port - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Port:"), 2, 7, 1, 1); + gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Port:"), 2, 4, 1, 1); info.test_port = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(info.test_port), "80"); gtk_widget_set_size_request(info.test_port, 80, -1); - gtk_grid_attach(GTK_GRID(grid), info.test_port, 3, 7, 1, 1); + gtk_grid_attach(GTK_GRID(grid), info.test_port, 3, 4, 1, 1); // test button info.test_btn = gtk_button_new_with_label("Start Test"); g_signal_connect(info.test_btn, "clicked", G_CALLBACK(on_start_test_clicked), &info); - gtk_grid_attach(GTK_GRID(grid), info.test_btn, 0, 8, 4, 1); + gtk_grid_attach(GTK_GRID(grid), info.test_btn, 0, 5, 4, 1); // output log GtkWidget *out_scroll = gtk_scrolled_window_new(NULL, NULL); @@ -172,50 +523,217 @@ void on_proxy_configure(GtkWidget *widget, gpointer data) { info.output_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(out_view)); gtk_container_add(GTK_CONTAINER(out_scroll), out_view); - gtk_grid_attach(GTK_GRID(grid), out_scroll, 0, 9, 4, 1); + gtk_grid_attach(GTK_GRID(grid), out_scroll, 0, 6, 4, 1); + refresh_proxy_store(&info); + if (g_proxies_list) select_proxy_row(&info, find_proxy_by_name(g_default_proxy) ? find_proxy_by_name(g_default_proxy) : (ProxyData *)g_proxies_list->data); gtk_widget_show_all(info.dialog); + gtk_dialog_run(GTK_DIALOG(info.dialog)); + gtk_widget_destroy(info.dialog); +} - while (TRUE) { - if (gtk_dialog_run(GTK_DIALOG(info.dialog)) != GTK_RESPONSE_ACCEPT) break; +// ---- profiles ---- - // check - const char *ip_text = gtk_entry_get_text(GTK_ENTRY(info.ip_entry)); - const char *port_text = gtk_entry_get_text(GTK_ENTRY(info.port_entry)); +static void update_profile_ui() { + char *title = g_strdup_printf("ProxyBridge - %s", g_active_profile ? g_active_profile : ""); + gtk_window_set_title(GTK_WINDOW(window), title); + g_free(title); + char *msg = g_strdup_printf("Active profile: %s", g_active_profile ? g_active_profile : ""); + gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, msg); + g_free(msg); +} - if (!ip_text || strlen(ip_text) == 0) { - show_message(GTK_WINDOW(info.dialog), GTK_MESSAGE_ERROR, "Host (IP/Domain) cannot be empty."); - continue; - } +// push the active profile to the running engine: no Stop/Start needed +static void reapply_active_profile() { + ProxyBridge_ClearRules(); + ProxyBridge_ClearProxies(); + apply_proxies_to_lib(); + apply_rules_to_lib(); + save_config(); + refresh_rules_window(); + update_profile_ui(); +} - if (strspn(port_text, "0123456789") != strlen(port_text) || strlen(port_text) == 0) { - show_message(GTK_WINDOW(info.dialog), GTK_MESSAGE_ERROR, "Port must be a valid number."); - continue; - } +typedef struct { + GtkWidget *dialog; + GtkWidget *tree; + GtkListStore *store; +} ProfilesInfo; + +static void refresh_profile_store(ProfilesInfo *pi) { + gtk_list_store_clear(pi->store); + for (GList *l = g_profiles; l != NULL; l = l->next) { + Profile *p = (Profile *)l->data; + bool act = g_active_profile && strcmp(p->name, g_active_profile) == 0; + GtkTreeIter iter; + gtk_list_store_append(pi->store, &iter); + gtk_list_store_set(pi->store, &iter, 0, p->name, 1, act ? "Active" : "", -1); + } +} + +// name of the selected row, caller frees +static char *selected_profile_name(ProfilesInfo *pi) { + GtkTreeModel *model; + GtkTreeIter iter; + if (!gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(pi->tree)), &model, &iter)) return NULL; + char *name = NULL; + gtk_tree_model_get(model, &iter, 0, &name, -1); + return name; +} - int p = (int)safe_strtol(port_text); - if (p < 1 || p > 65535) { - show_message(GTK_WINDOW(info.dialog), GTK_MESSAGE_ERROR, "Port must be between 1 and 65535."); - continue; +// asks for a profile name, returns NULL on cancel. caller frees +static char *ask_profile_name(GtkWindow *parent, const char *title, const char *initial) { + GtkWidget *d = gtk_dialog_new_with_buttons(title, parent, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + "Cancel", GTK_RESPONSE_CANCEL, "OK", GTK_RESPONSE_ACCEPT, NULL); + GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(d)); + gtk_container_set_border_width(GTK_CONTAINER(box), 10); + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); + gtk_entry_set_text(GTK_ENTRY(entry), initial ? initial : ""); + gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Profile name:"), FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 5); + gtk_dialog_set_default_response(GTK_DIALOG(d), GTK_RESPONSE_ACCEPT); + gtk_widget_show_all(d); + + char *result = NULL; + while (gtk_dialog_run(GTK_DIALOG(d)) == GTK_RESPONSE_ACCEPT) { + char *name = g_strstrip(g_strdup(gtk_entry_get_text(GTK_ENTRY(entry)))); + if (name[0] == '\0') { + show_message(GTK_WINDOW(d), GTK_MESSAGE_ERROR, "Profile name cannot be empty."); + } else if (profile_find(name) && !(initial && strcmp(initial, name) == 0)) { + show_message(GTK_WINDOW(d), GTK_MESSAGE_ERROR, "A profile with this name already exists."); + } else { + result = name; + break; } + g_free(name); + } + gtk_widget_destroy(d); + return result; +} + +static void do_switch(ProfilesInfo *pi, const char *name) { + if (!name || (g_active_profile && strcmp(name, g_active_profile) == 0)) return; + if (!profile_activate(name)) return; + reapply_active_profile(); + refresh_profile_store(pi); +} - // save - g_proxy_type = (gtk_combo_box_get_active(GTK_COMBO_BOX(info.type_combo)) == 0) ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; - g_strlcpy(g_proxy_ip, ip_text, sizeof(g_proxy_ip)); - g_proxy_port = p; - g_strlcpy(g_proxy_user, gtk_entry_get_text(GTK_ENTRY(info.user_entry)), sizeof(g_proxy_user)); - g_strlcpy(g_proxy_pass, gtk_entry_get_text(GTK_ENTRY(info.pass_entry)), sizeof(g_proxy_pass)); +static void on_profile_switch(GtkWidget *widget, gpointer data) { + ProfilesInfo *pi = (ProfilesInfo *)data; + char *name = selected_profile_name(pi); + if (name) do_switch(pi, name); + g_free(name); +} - ProxyBridge_SetProxyConfig(g_proxy_type, g_proxy_ip, g_proxy_port, g_proxy_user, g_proxy_pass); +static void on_profile_new(GtkWidget *widget, gpointer data) { + ProfilesInfo *pi = (ProfilesInfo *)data; + char *name = ask_profile_name(GTK_WINDOW(pi->dialog), "New Profile", ""); + if (!name) return; + if (profile_create(name, false)) { save_config(); refresh_profile_store(pi); } + g_free(name); +} + +static void on_profile_duplicate(GtkWidget *widget, gpointer data) { + ProfilesInfo *pi = (ProfilesInfo *)data; + char *src = selected_profile_name(pi); + if (!src) return; + // duplicates the selected profile: make it active data-wise only for the copy, then switch back + char *initial = g_strdup_printf("%s copy", src); + char *name = ask_profile_name(GTK_WINDOW(pi->dialog), "Duplicate Profile", initial); + g_free(initial); + if (name) { + char *prev = g_strdup(g_active_profile); + if (profile_activate(src)) { + profile_create(name, true); + profile_activate(prev); // library state was never touched + save_config(); + refresh_profile_store(pi); + } + g_free(prev); + } + g_free(name); + g_free(src); +} +static void on_profile_rename(GtkWidget *widget, gpointer data) { + ProfilesInfo *pi = (ProfilesInfo *)data; + char *old = selected_profile_name(pi); + if (!old) return; + char *name = ask_profile_name(GTK_WINDOW(pi->dialog), "Rename Profile", old); + if (name && profile_rename(profile_find(old), name)) { save_config(); + refresh_profile_store(pi); + update_profile_ui(); + } + g_free(name); + g_free(old); +} - char status_msg[512]; - snprintf(status_msg, sizeof(status_msg), "Configuration updated: %s:%d", g_proxy_ip, g_proxy_port); - gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, status_msg); - break; +static void on_profile_delete(GtkWidget *widget, gpointer data) { + ProfilesInfo *pi = (ProfilesInfo *)data; + char *name = selected_profile_name(pi); + if (!name) return; + if (!g_profiles || !g_profiles->next) { + show_message(GTK_WINDOW(pi->dialog), GTK_MESSAGE_WARNING, "The last profile cannot be deleted."); + g_free(name); + return; + } + GtkWidget *q = gtk_message_dialog_new(GTK_WINDOW(pi->dialog), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, "Delete profile '%s' with all its proxies and rules?", name); + int resp = gtk_dialog_run(GTK_DIALOG(q)); + gtk_widget_destroy(q); + if (resp == GTK_RESPONSE_YES) { + bool was_active = g_active_profile && strcmp(g_active_profile, name) == 0; + if (profile_delete(profile_find(name))) { + if (was_active) reapply_active_profile(); + else save_config(); + refresh_profile_store(pi); + } } + g_free(name); +} - gtk_widget_destroy(info.dialog); +static void on_profile_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data) { + on_profile_switch(NULL, data); +} + +void on_profiles_clicked(GtkWidget *widget, gpointer data) { + ProfilesInfo pi; + memset(&pi, 0, sizeof(pi)); + pi.dialog = gtk_dialog_new_with_buttons("Profiles", GTK_WINDOW(window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + "Close", GTK_RESPONSE_CLOSE, NULL); + gtk_window_set_default_size(GTK_WINDOW(pi.dialog), 420, 360); + GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(pi.dialog)); + gtk_container_set_border_width(GTK_CONTAINER(box), 10); + + pi.store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); + pi.tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(pi.store)); + g_object_unref(pi.store); + const char *titles[] = {"Profile", "Status"}; + for (int i = 0; i < 2; i++) { + GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes(titles[i], gtk_cell_renderer_text_new(), "text", i, NULL); + if (i == 0) gtk_tree_view_column_set_expand(c, TRUE); + gtk_tree_view_append_column(GTK_TREE_VIEW(pi.tree), c); + } + g_signal_connect(pi.tree, "row-activated", G_CALLBACK(on_profile_row_activated), &pi); + GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_container_add(GTK_CONTAINER(scroll), pi.tree); + gtk_box_pack_start(GTK_BOX(box), scroll, TRUE, TRUE, 0); + + GtkWidget *btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + const char *labels[] = {"Switch", "New", "Duplicate", "Rename", "Delete"}; + void (*cbs[])(GtkWidget *, gpointer) = {on_profile_switch, on_profile_new, on_profile_duplicate, on_profile_rename, on_profile_delete}; + for (int i = 0; i < 5; i++) { + GtkWidget *b = gtk_button_new_with_label(labels[i]); + g_signal_connect(b, "clicked", G_CALLBACK(cbs[i]), &pi); + gtk_box_pack_start(GTK_BOX(btns), b, FALSE, FALSE, 0); + } + gtk_box_pack_start(GTK_BOX(box), btns, FALSE, FALSE, 5); + + refresh_profile_store(&pi); + gtk_widget_show_all(pi.dialog); + gtk_dialog_run(GTK_DIALOG(pi.dialog)); + gtk_widget_destroy(pi.dialog); } diff --git a/Linux/gui/gui_utils.c b/Linux/gui/gui_utils.c index ae69d292..35d4a7c1 100644 --- a/Linux/gui/gui_utils.c +++ b/Linux/gui/gui_utils.c @@ -40,40 +40,67 @@ void trim_buffer_lines(GtkTextBuffer *buffer, int max_lines) { char* get_current_time_str() { time_t rawtime; struct tm *timeinfo; - char *buffer = malloc(32); + char *buffer = g_malloc0(32); time(&rawtime); timeinfo = localtime(&rawtime); - strftime(buffer, 32, "[%H:%M:%S]", timeinfo); - return buffer; + if (!timeinfo || strftime(buffer, 32, "[%H:%M:%S]", timeinfo) == 0) buffer[0] = '\0'; + return buffer; // free with g_free } // json stuff char *escape_json_string(const char *src) { - if (!src) return strdup(""); + if (!src) return g_strdup(""); GString *str = g_string_new(""); for (const char *p = src; *p; p++) { if (*p == '\\') g_string_append(str, "\\\\"); else if (*p == '"') g_string_append(str, "\\\""); else if (*p == '\n') g_string_append(str, "\\n"); + else if (*p == '\r') g_string_append(str, "\\r"); + else if (*p == '\t') g_string_append(str, "\\t"); + else if ((unsigned char)*p < 0x20) g_string_append_printf(str, "\\u%04x", (unsigned char)*p); else g_string_append_c(str, *p); } return g_string_free(str, FALSE); } -// basic parser, good enough for valid inputs +// basic parser, good enough for valid inputs. returns NULL if the key is missing or its value is not a string char *extract_sub_json_str(const char *json, const char *key) { char search_key[256]; snprintf(search_key, sizeof(search_key), "\"%s\"", key); - char *k = strstr(json, search_key); + const char *k = strstr(json, search_key); if (!k) return NULL; - char *colon = strchr(k, ':'); + const char *colon = strchr(k + strlen(search_key), ':'); if (!colon) return NULL; - char *val_start = strchr(colon, '"'); - if (!val_start) return NULL; - val_start++; - char *val_end = strchr(val_start, '"'); - if (!val_end) return NULL; - return g_strndup(val_start, val_end - val_start); + const char *v = colon + 1; + while (*v == ' ' || *v == '\t' || *v == '\n' || *v == '\r') v++; + if (*v != '"') return NULL; + v++; + + // copy up to the closing quote, handling escapes + GString *out = g_string_new(""); + while (*v && *v != '"') { + if (*v == '\\' && v[1]) { + v++; + switch (*v) { + case 'n': g_string_append_c(out, '\n'); break; + case 'r': g_string_append_c(out, '\r'); break; + case 't': g_string_append_c(out, '\t'); break; + default: g_string_append_c(out, *v); break; // \\ \" \/ + } + } else { + g_string_append_c(out, *v); + } + v++; + } + if (*v != '"') { g_string_free(out, TRUE); return NULL; } + return g_string_free(out, FALSE); +} + +// true if the key is present at all (extract_sub_json_bool cant tell false from missing) +bool json_has_key(const char *json, const char *key) { + char search_key[256]; + snprintf(search_key, sizeof(search_key), "\"%s\"", key); + return strstr(json, search_key) != NULL; } bool extract_sub_json_bool(const char *json, const char *key) { @@ -85,7 +112,7 @@ bool extract_sub_json_bool(const char *json, const char *key) { if (!colon) return false; // skip whitespace char *v = colon + 1; - while(*v == ' ' || *v == '\t') v++; + while(*v == ' ' || *v == '\t' || *v == '\n' || *v == '\r') v++; if (strncmp(v, "true", 4) == 0) return true; return false; } diff --git a/Linux/gui/main.c b/Linux/gui/main.c index 388622e4..fc7d6b05 100644 --- a/Linux/gui/main.c +++ b/Linux/gui/main.c @@ -1,4 +1,5 @@ #include "gui.h" +#include // widgets GtkWidget *window; @@ -9,13 +10,10 @@ GtkTextBuffer *log_buffer; GtkWidget *status_bar; guint status_context_id; -// default config -char g_proxy_ip[256] = ""; -uint16_t g_proxy_port = 0; -ProxyType g_proxy_type = PROXY_TYPE_SOCKS5; -char g_proxy_user[256] = ""; -char g_proxy_pass[256] = ""; +// config lives in gui_settings.c (proxies) and gui_rules.c (rules) +static bool g_engine_running = false; +// default config GList *g_rules_list = NULL; bool g_chk_logging = true; bool g_chk_dns = true; @@ -202,7 +200,8 @@ static void on_about(GtkWidget *widget, gpointer data) { char *markup = g_strdup_printf( "ProxyBridge\n" "Version %s\n\n" - "Universal proxy client for Linux applications\n\n" + "Universal proxy client for Linux applications\n" + "eBPF backend (needs root, kernel 5.8+ with BTF and cgroup v2)\n\n" "Author: Sourav Kalal / InterceptSuite\n\n" "Website: interceptsuite.com\n" "GitHub: InterceptSuite/ProxyBridge\n\n" @@ -217,22 +216,40 @@ static void on_about(GtkWidget *widget, gpointer data) { gtk_widget_destroy(dialog); } -static void signal_handler(int sig) { - fprintf(stderr, "\nSignal %d received. Stopping ProxyBridge...\n", sig); - ProxyBridge_Stop(); - exit(sig); +// signal handlers run in the main loop (g_unix_signal_add), so no library calls from a real signal context +static gboolean on_unix_signal(gpointer data) { + fprintf(stderr, "\nSignal received. Stopping ProxyBridge...\n"); + gtk_main_quit(); + return G_SOURCE_REMOVE; } static void on_window_destroy(GtkWidget *widget, gpointer data) { - ProxyBridge_Stop(); - gtk_main_quit(); + gtk_main_quit(); // engine is stopped once after gtk_main returns } -int main(int argc, char *argv[]) { - signal(SIGINT, signal_handler); - signal(SIGTERM, signal_handler); - signal(SIGSEGV, signal_handler); +// start the engine, config (proxies, rules) was already pushed to the library +static bool start_engine() { + if (g_engine_running) return true; + if (ProxyBridge_Start()) { + g_engine_running = true; + char *msg = g_strdup_printf("ProxyBridge Service Started (eBPF). Active profile: %s", g_active_profile ? g_active_profile : "Default"); + gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, msg); + g_free(msg); + return true; + } + gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "Failed to start ProxyBridge engine. See the Activity Logs tab."); + show_message(GTK_WINDOW(window), GTK_MESSAGE_ERROR, + "Failed to start the ProxyBridge engine.\n\n" + "Open the \"Activity Logs\" tab to see the reason.\n" + "The eBPF backend needs root, a kernel >= 5.8 with BTF and cgroup v2."); + return false; +} + +static void on_start_engine(GtkWidget *widget, gpointer data) { + start_engine(); +} +int main(int argc, char *argv[]) { if (getuid() != 0) { gtk_init(&argc, &argv); show_message(NULL, GTK_MESSAGE_ERROR, "ProxyBridge must be run as root (sudo)."); return 1; } setenv("GSETTINGS_BACKEND", "memory", 1); @@ -245,7 +262,9 @@ int main(int argc, char *argv[]) { if (settings) g_object_set(settings, "gtk-application-prefer-dark-theme", TRUE, NULL); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_title(GTK_WINDOW(window), "ProxyBridge"); + char *win_title = g_strdup_printf("ProxyBridge - %s", g_active_profile ? g_active_profile : "Default"); + gtk_window_set_title(GTK_WINDOW(window), win_title); + g_free(win_title); gtk_window_set_default_size(GTK_WINDOW(window), 900, 600); g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), NULL); @@ -256,8 +275,11 @@ int main(int argc, char *argv[]) { GtkWidget *menubar = gtk_menu_bar_new(); GtkWidget *proxy_menu_item = gtk_menu_item_new_with_label("Proxy"); GtkWidget *proxy_menu = gtk_menu_new(); - GtkWidget *config_item = gtk_menu_item_new_with_label("Proxy Settings"); - GtkWidget *rules_item = gtk_menu_item_new_with_label("Proxy Rules"); + GtkWidget *config_item = gtk_menu_item_new_with_label("Proxy Settings..."); + GtkWidget *rules_item = gtk_menu_item_new_with_label("Proxy Rules..."); + + GtkWidget *profiles_item = gtk_menu_item_new_with_label("Profiles..."); + g_signal_connect(profiles_item, "activate", G_CALLBACK(on_profiles_clicked), NULL); GtkWidget *log_check_item = gtk_check_menu_item_new_with_label("Enable Traffic Logging"); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(log_check_item), g_chk_logging); @@ -267,6 +289,9 @@ int main(int argc, char *argv[]) { gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(dns_check_item), g_chk_dns); g_signal_connect(dns_check_item, "toggled", G_CALLBACK(on_dns_proxy_toggled), NULL); + GtkWidget *start_item = gtk_menu_item_new_with_label("Start Engine"); + g_signal_connect(start_item, "activate", G_CALLBACK(on_start_engine), NULL); + GtkWidget *exit_item = gtk_menu_item_new_with_label("Exit"); g_signal_connect(config_item, "activate", G_CALLBACK(on_proxy_configure), NULL); @@ -275,10 +300,12 @@ int main(int argc, char *argv[]) { gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), config_item); gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), rules_item); + gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), profiles_item); gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), gtk_separator_menu_item_new()); gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), log_check_item); gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), dns_check_item); gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), gtk_separator_menu_item_new()); + gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), start_item); gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), exit_item); gtk_menu_item_set_submenu(GTK_MENU_ITEM(proxy_menu_item), proxy_menu); @@ -349,30 +376,25 @@ int main(int argc, char *argv[]) { status_context_id = gtk_statusbar_get_context_id(GTK_STATUSBAR(status_bar), "Status"); gtk_box_pack_start(GTK_BOX(vbox), status_bar, FALSE, FALSE, 0); - // start + // setup: callbacks, options, proxies, default proxy, rules, then start ProxyBridge_SetLogCallback(lib_log_callback); ProxyBridge_SetConnectionCallback(lib_connection_callback); ProxyBridge_SetTrafficLoggingEnabled(g_chk_logging); ProxyBridge_SetDnsViaProxy(g_chk_dns); + apply_proxies_to_lib(); + apply_rules_to_lib(); - if (ProxyBridge_Start()) { - // apply config - ProxyBridge_SetProxyConfig(g_proxy_type, g_proxy_ip, g_proxy_port, g_proxy_user, g_proxy_pass); - - // restore rules - for (GList *l = g_rules_list; l != NULL; l = l->next) { - RuleData *r = (RuleData *)l->data; - r->id = ProxyBridge_AddRule(r->process_name, r->target_hosts, r->target_ports, r->protocol, r->action); - if (!r->enabled) ProxyBridge_DisableRule(r->id); - } - - gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "ProxyBridge Service Started."); - } else { - gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "Failed to start ProxyBridge engine."); - } + g_unix_signal_add(SIGINT, on_unix_signal, NULL); + g_unix_signal_add(SIGTERM, on_unix_signal, NULL); gtk_widget_show_all(window); + start_engine(); gtk_main(); + if (g_engine_running) { + ProxyBridge_Stop(); + g_engine_running = false; + } + return 0; } diff --git a/Linux/gui/tests/config_test.c b/Linux/gui/tests/config_test.c new file mode 100644 index 00000000..bf02b711 --- /dev/null +++ b/Linux/gui/tests/config_test.c @@ -0,0 +1,107 @@ +// root-free test of gui_config.c (profiles, migration, round trip) +// build: gcc -I../../src -I.. -D_GNU_SOURCE $(pkg-config --cflags gtk+-3.0) config_test.c ../gui_config.c -o config_test $(pkg-config --libs gtk+-3.0) +#include "gui.h" + +GList *g_proxies_list = NULL; +char *g_default_proxy = NULL; +GList *g_rules_list = NULL; +bool g_chk_logging = true; +bool g_chk_dns = true; + +void free_proxy_data(ProxyData *p) { + if (!p) return; + g_free(p->name); g_free(p->host); g_free(p->username); g_free(p->password); g_free(p); +} +void free_rule_data(RuleData *r) { + if (!r) return; + g_free(r->process_name); g_free(r->target_hosts); g_free(r->target_ports); g_free(r->proxy_name); g_free(r); +} + +static int failures = 0; +#define CHECK(c) do { if (!(c)) { printf("FAIL line %d: %s\n", __LINE__, #c); failures++; } } while (0) + +static void reset_state() { + g_profiles = NULL; g_active_profile = NULL; + g_proxies_list = NULL; g_rules_list = NULL; g_default_proxy = NULL; + g_chk_logging = true; g_chk_dns = true; +} + +static void write_file(const char *dir, const char *content) { + char *path = g_strdup_printf("%s/config.ini", dir); + g_file_set_contents(path, content, -1, NULL); + g_free(path); +} + +int main() { + char *dir = g_dir_make_tmp("pbcfg-XXXXXX", NULL); + CHECK(dir != NULL); + setenv("PROXYBRIDGE_CONFIG_DIR", dir, 1); + + // 1. no file -> one "Default" profile + reset_state(); + load_config(); + CHECK(g_list_length(g_profiles) == 1); + CHECK(strcmp(g_active_profile, "Default") == 0); + + // 2. old single-proxy format + write_file(dir, "[SETTINGS]\nip=1.2.3.4\nport=1080\ntype=1\nuser=u\npass=p|w\nlogging=0\ndns=1\n[RULES]\n3|2|0|1|firefox|*|443;80|\n"); + reset_state(); + load_config(); + CHECK(strcmp(g_active_profile, "Default") == 0); + CHECK(g_list_length(g_proxies_list) == 1); + CHECK(strcmp(((ProxyData *)g_proxies_list->data)->name, "default") == 0); + CHECK(strcmp(((ProxyData *)g_proxies_list->data)->host, "1.2.3.4") == 0); + CHECK(strcmp(g_default_proxy, "default") == 0); + CHECK(g_list_length(g_rules_list) == 1); + CHECK(strcmp(((RuleData *)g_rules_list->data)->target_ports, "443;80") == 0); + CHECK(!g_chk_logging); + + // 3. profiles: create, add data, rename, switch, save, reload + CHECK(profile_create("Work", false) != NULL); + CHECK(profile_create("Work", false) == NULL); + CHECK(profile_create("", false) == NULL); + Profile *dup = profile_create("Copy", true); + CHECK(dup && g_list_length(dup->proxies) == 1 && g_list_length(dup->rules) == 1); + CHECK(profile_rename(profile_find("Copy"), "Home")); + CHECK(!profile_rename(profile_find("Home"), "Work")); + CHECK(profile_activate("Work")); + CHECK(g_proxies_list == NULL && g_rules_list == NULL); + ProxyData *px = g_malloc0(sizeof(ProxyData)); + px->name = g_strdup("p 1|x"); px->host = g_strdup("github.com"); px->port = 8080; + px->username = g_strdup(""); px->password = g_strdup("s3cr|et"); + g_proxies_list = g_list_append(g_proxies_list, px); + g_default_proxy = g_strdup("p 1|x"); + RuleData *r = g_malloc0(sizeof(RuleData)); + r->process_name = g_strdup("curl"); r->target_hosts = g_strdup("*.github.com"); r->target_ports = g_strdup("*"); + r->protocol = RULE_PROTOCOL_BOTH; r->action = RULE_ACTION_PROXY; r->proxy_name = g_strdup("p 1|x"); r->enabled = true; + g_rules_list = g_list_append(g_rules_list, r); + g_chk_dns = false; + save_config(); + + char *path = g_strdup_printf("%s/config.ini", dir); + struct stat st; + CHECK(stat(path, &st) == 0 && (st.st_mode & 0777) == 0600); + g_free(path); + + reset_state(); + load_config(); + CHECK(g_list_length(g_profiles) == 3); + CHECK(strcmp(g_active_profile, "Work") == 0); + CHECK(g_list_length(g_proxies_list) == 1); + CHECK(strcmp(((ProxyData *)g_proxies_list->data)->name, "p 1|x") == 0); + CHECK(strcmp(((ProxyData *)g_proxies_list->data)->password, "s3cr|et") == 0); + CHECK(strcmp(((RuleData *)g_rules_list->data)->target_hosts, "*.github.com") == 0); + CHECK(strcmp(((RuleData *)g_rules_list->data)->proxy_name, "p 1|x") == 0); + CHECK(!g_chk_dns && !g_chk_logging); + CHECK(profile_find("Home") && g_list_length(profile_find("Home")->rules) == 1); + + // 4. delete: non-last allowed, active one falls back, last refused + CHECK(profile_delete(profile_find("Work"))); + CHECK(strcmp(g_active_profile, "Default") == 0 && g_list_length(g_rules_list) == 1); + CHECK(profile_delete(profile_find("Home"))); + CHECK(!profile_delete(profile_find("Default"))); + CHECK(g_list_length(g_profiles) == 1); + + if (failures == 0) printf("config_test: all passed\n"); + return failures ? 1 : 0; +} From 23e94ab9483cd4727f41367669ab50b75c980630 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:13:46 +0530 Subject: [PATCH 04/25] test(linux): end-to-end, scale and benchmark suites Run against the real kernel datapath (root): e2e.sh, e2e_rules.sh (every rule form and proxy type), e2e_domains.sh (real dig + curl), api_live.sh (live rule/proxy edits), e2e_openssh.sh (interop with a real third-party SOCKS5 server), gui_headless.sh (real GUI under the GTK Broadway backend), scale.sh (10,000 concurrent connections), plus hookbench/thrbench/dnsbench. Includes fake SOCKS5/HTTP/DNS servers. --- Linux/tests/api_live.sh | 8 ++ Linux/tests/api_live_test.c | 113 ++++++++++++++++++++++ Linux/tests/dnsbench.sh | 31 ++++++ Linux/tests/e2e.sh | 143 ++++++++++++++++++++++++++++ Linux/tests/e2e_domains.sh | 68 ++++++++++++++ Linux/tests/e2e_openssh.sh | 79 ++++++++++++++++ Linux/tests/e2e_rules.sh | 143 ++++++++++++++++++++++++++++ Linux/tests/fake_dns.py | 111 ++++++++++++++++++++++ Linux/tests/fake_proxies.py | 182 ++++++++++++++++++++++++++++++++++++ Linux/tests/gui_headless.sh | 86 +++++++++++++++++ Linux/tests/hookbench.c | 37 ++++++++ Linux/tests/hookbench.sh | 16 ++++ Linux/tests/scale.sh | 54 +++++++++++ Linux/tests/thrbench.c | 93 ++++++++++++++++++ Linux/tests/thrbench.sh | 21 +++++ 15 files changed, 1185 insertions(+) create mode 100755 Linux/tests/api_live.sh create mode 100644 Linux/tests/api_live_test.c create mode 100755 Linux/tests/dnsbench.sh create mode 100755 Linux/tests/e2e.sh create mode 100755 Linux/tests/e2e_domains.sh create mode 100755 Linux/tests/e2e_openssh.sh create mode 100755 Linux/tests/e2e_rules.sh create mode 100644 Linux/tests/fake_dns.py create mode 100644 Linux/tests/fake_proxies.py create mode 100755 Linux/tests/gui_headless.sh create mode 100644 Linux/tests/hookbench.c create mode 100755 Linux/tests/hookbench.sh create mode 100755 Linux/tests/scale.sh create mode 100644 Linux/tests/thrbench.c create mode 100755 Linux/tests/thrbench.sh diff --git a/Linux/tests/api_live.sh b/Linux/tests/api_live.sh new file mode 100755 index 00000000..6c7b9b8c --- /dev/null +++ b/Linux/tests/api_live.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Library API against the live datapath. Needs root, curl, python3, gcc. sudo tests/api_live.sh +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +WORK=$(mktemp -d); trap 'kill $FAKE 2>/dev/null; wait 2>/dev/null; rm -rf $WORK' EXIT +gcc -Wall -Wextra -O1 -g -I"$ROOT/src" -o "$WORK/api_live_test" "$HERE/api_live_test.c" -L"$ROOT/src" -lproxybridge -lpthread || exit 1 +python3 "$HERE/fake_proxies.py" "$WORK/log" >/dev/null 2>&1 & FAKE=$! +sleep 1 +LD_LIBRARY_PATH="$ROOT/src" "$WORK/api_live_test" diff --git a/Linux/tests/api_live_test.c b/Linux/tests/api_live_test.c new file mode 100644 index 00000000..ae7d64b9 --- /dev/null +++ b/Linux/tests/api_live_test.c @@ -0,0 +1,113 @@ +// Library API test against the live eBPF datapath: every rule/proxy mutation must take effect immediately on new connections. +// Needs root and the fake proxies running (tests/api_live.sh starts them). +#define _GNU_SOURCE +#include +#include +#include +#include +#include "../src/ProxyBridge.h" + +static int pass, fail; +static int events; +static void on_conn(const char *p, uint32_t pid, const char *ip, uint16_t port, const char *info) { (void)p; (void)pid; (void)ip; (void)port; (void)info; events++; } +static void on_log(const char *m) { if (getenv("PB_VERBOSE")) fprintf(stderr, "[log] %s\n", m); } + +static void fetch(const char *ip, char *out, size_t n) +{ + char cmd[256]; snprintf(cmd, sizeof cmd, "curl -s -m 2 http://%s/ 2>&1; echo \"rc=$?\"", ip); + FILE *f = popen(cmd, "r"); size_t l = 0; + while (f && l + 1 < n && fgets(out + l, (int)(n - l), f)) l = strlen(out); + if (f) pclose(f); +} +static void expect(const char *what, const char *ip, const char *want) +{ + char out[512] = ""; fetch(ip, out, sizeof out); + int ok = want ? strstr(out, want) != NULL : (strstr(out, "hello-from") == NULL && strstr(out, "rc=0") == NULL); + if (want && !strcmp(want, "BLOCKED")) ok = strstr(out, "rc=7") != NULL; + printf(" %s %s\n", ok ? "PASS" : "FAIL", what); if (!ok) printf(" got: %.120s\n", out); + ok ? pass++ : fail++; +} +static void check(const char *what, int cond) { printf(" %s %s\n", cond ? "PASS" : "FAIL", what); cond ? pass++ : fail++; } + +int main(void) +{ + char err[256] = ""; + ProxyBridge_SetLogCallback(on_log); ProxyBridge_SetConnectionCallback(on_conn); ProxyBridge_SetTrafficLoggingEnabled(true); + + printf("== validation\n"); + check("invalid host rejected with a reason", !ProxyBridge_ValidateRule("curl", "999.1.1.1", "80", err, sizeof err) && err[0]); + check("invalid port rejected", !ProxyBridge_ValidateRule("curl", "*", "99999", err, sizeof err)); + check("domain accepted", ProxyBridge_ValidateRule("curl", "*.example.org", "443", err, sizeof err)); + check("AddRule with bad pattern returns 0 + message", ProxyBridge_AddRule("curl", "1.2.3", "*", RULE_PROTOCOL_TCP, RULE_ACTION_PROXY, 0, err, sizeof err) == 0 && err[0]); + check("AddProxy with empty host rejected", ProxyBridge_AddProxy("x", PROXY_TYPE_SOCKS5, "", 1080, "", "", err, sizeof err) == 0); + check("AddProxy with port 0 rejected", ProxyBridge_AddProxy("x", PROXY_TYPE_SOCKS5, "h", 0, "", "", err, sizeof err) == 0); + + uint32_t pa = ProxyBridge_AddProxy("a", PROXY_TYPE_SOCKS5, "127.0.0.1", 19080, "", "", err, sizeof err); + uint32_t pb = ProxyBridge_AddProxy("b", PROXY_TYPE_SOCKS5, "127.0.0.1", 19081, "bob", "pw", err, sizeof err); + uint32_t pc = ProxyBridge_AddProxy("c", PROXY_TYPE_HTTP, "127.0.0.1", 19082, "", "", err, sizeof err); + check("three proxies added with distinct ids", pa && pb && pc && pa != pb && pb != pc); + uint32_t r1 = ProxyBridge_AddRule("curl", "192.0.2.0/24", "*", RULE_PROTOCOL_TCP, RULE_ACTION_PROXY, pa, err, sizeof err); + check("rule added", r1 != 0); + check("Start", ProxyBridge_Start()); + sleep(1); + + printf("== live edits take effect on the next connection\n"); + expect("initial: rule -> proxy a", "192.0.2.5", "hello-from-A dst=192.0.2.5:80"); + check("EditRule -> proxy b", ProxyBridge_EditRule(r1, "curl", "192.0.2.0/24", "*", RULE_PROTOCOL_TCP, RULE_ACTION_PROXY, pb, err, sizeof err)); + expect("after edit: proxy b", "192.0.2.5", "hello-from-B dst=192.0.2.5:80"); + check("EditRule with a bad pattern fails and keeps the old rule", !ProxyBridge_EditRule(r1, "curl", "bad host", "*", RULE_PROTOCOL_TCP, RULE_ACTION_PROXY, pb, err, sizeof err)); + expect("old rule still active", "192.0.2.5", "hello-from-B"); + check("DisableRule", ProxyBridge_DisableRule(r1)); + expect("disabled: traffic is direct", "192.0.2.5", NULL); + check("EnableRule", ProxyBridge_EnableRule(r1)); + expect("enabled again", "192.0.2.5", "hello-from-B"); + + printf("== ordering\n"); + uint32_t r2 = ProxyBridge_AddRule("curl", "192.0.2.5", "*", RULE_PROTOCOL_TCP, RULE_ACTION_BLOCK, 0, err, sizeof err); + expect("BLOCK rule added after the PROXY rule loses (first match wins)", "192.0.2.5", "hello-from-B"); + check("MoveRule to the top", ProxyBridge_MoveRule(r2, 0)); + expect("after move: BLOCK wins", "192.0.2.5", "BLOCKED"); + expect("other addresses still proxied", "192.0.2.6", "hello-from-B dst=192.0.2.6:80"); + check("MoveRule back to the end", ProxyBridge_MoveRule(r2, 99)); + expect("PROXY wins again", "192.0.2.5", "hello-from-B"); + check("DeleteRule (the BLOCK rule)", ProxyBridge_DeleteRule(r2)); + check("DeleteRule on an unknown id fails", !ProxyBridge_DeleteRule(424242)); + + printf("== proxy lifecycle\n"); + check("DeleteProxy(b)", ProxyBridge_DeleteProxy(pb)); + expect("rule that used a deleted proxy falls back to the default proxy (a)", "192.0.2.5", "hello-from-A dst=192.0.2.5:80"); + ProxyBridge_ClearProxies(); + expect("no proxies at all: PROXY rule fails CLOSED (blocked, never direct)", "192.0.2.5", "BLOCKED"); + uint32_t pb2 = ProxyBridge_AddProxy("b2", PROXY_TYPE_SOCKS5, "127.0.0.1", 19081, "bob", "pw", err, sizeof err); + uint32_t pa2 = ProxyBridge_AddProxy("a2", PROXY_TYPE_SOCKS5, "127.0.0.1", 19080, "", "", err, sizeof err); + check("proxies re-added", pa2 && pb2); + check("EditRule to proxy_id 0 (= default)", ProxyBridge_EditRule(r1, "curl", "192.0.2.0/24", "*", RULE_PROTOCOL_TCP, RULE_ACTION_PROXY, 0, err, sizeof err)); + expect("default = first proxy added (b2)", "192.0.2.5", "hello-from-B"); + ProxyBridge_SetDefaultProxy(pa2); + expect("SetDefaultProxy(a2) retargets proxy_id 0 rules", "192.0.2.5", "hello-from-A"); + check("EditProxy changes the endpoint live (a2 -> HTTP proxy c's port)", ProxyBridge_EditProxy(pa2, "a2", PROXY_TYPE_HTTP, "127.0.0.1", 19082, "", "", err, sizeof err)); + expect("now served through the HTTP proxy", "192.0.2.5", "hello-from-C dst=192.0.2.5:80"); + + printf("== stop/start keeps configuration\n"); + check("Stop", ProxyBridge_Stop()); + expect("while stopped: nothing redirected", "192.0.2.5", NULL); + check("Start again", ProxyBridge_Start()); + sleep(1); + expect("rules and proxies survived Stop/Start", "192.0.2.5", "hello-from-C"); + + printf("== proxy test helper\n"); + char buf[512] = ""; + check("TestProxy through a SOCKS5 proxy works", ProxyBridge_TestProxy(pb2, "192.0.2.77", 80, buf, sizeof buf) == 0); + printf(" (%s)\n", buf); + uint32_t dead = ProxyBridge_AddProxy("dead", PROXY_TYPE_SOCKS5, "127.0.0.1", 19999, "", "", err, sizeof err); + buf[0] = 0; check("TestProxy against a dead proxy fails with a readable message", ProxyBridge_TestProxy(dead, "192.0.2.77", 80, buf, sizeof buf) != 0 && strlen(buf) > 5); + printf(" (%s)\n", buf); + + printf("== cleanup\n"); + ProxyBridge_ClearRules(); + expect("ClearRules: direct again", "192.0.2.5", NULL); + check("connection events were reported to the callback", events > 0); + ProxyBridge_Stop(); + printf("\npassed=%d failed=%d\n", pass, fail); + return fail != 0; +} diff --git a/Linux/tests/dnsbench.sh b/Linux/tests/dnsbench.sh new file mode 100755 index 00000000..ffe6f812 --- /dev/null +++ b/Linux/tests/dnsbench.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Latency/throughput of DNS queries with and without the DNS tap. Needs root, python3. +# sudo tests/dnsbench.sh [queries] +set -u +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")"; N=${1:-20000} +export LD_LIBRARY_PATH="$ROOT/src"; DNSIP=10.77.0.3 +WORK=$(mktemp -d); trap 'kill $DNS $PB 2>/dev/null; wait 2>/dev/null; ip addr del $DNSIP/32 dev lo 2>/dev/null; rm -rf $WORK' EXIT +ip addr add $DNSIP/32 dev lo 2>/dev/null +python3 "$HERE/fake_dns.py" $DNSIP $WORK/dns.log >/dev/null 2>&1 & DNS=$! +sleep 1 +bench() { python3 - "$N" "$DNSIP" <<'PY' +import socket, struct, sys, time +n, ip = int(sys.argv[1]), sys.argv[2] +q = struct.pack("!HHHHHH", 1, 0x0100, 1, 0, 0, 0) + b"\x03app\x07example\x04test\x00" + struct.pack("!HH", 1, 1) +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2) +lat = [] +t0 = time.perf_counter() +for i in range(n): + a = time.perf_counter(); s.sendto(q, (ip, 53)); s.recvfrom(512); lat.append(time.perf_counter() - a) +dt = time.perf_counter() - t0; lat.sort() +print(" %d queries: %.0f qps avg=%.0fus p50=%.0fus p99=%.0fus" % (n, n / dt, sum(lat) / n * 1e6, lat[n // 2] * 1e6, lat[int(n * 0.99)] * 1e6)) +PY +} +echo "== no ProxyBridge"; bench; bench +stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "*:*.example.test:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" $WORK/pb.out && break; sleep 0.25; done +echo "== ProxyBridge running, python3 covered by a domain rule (every query goes through the DNS tap)"; bench; bench +kill -INT $PB; wait $PB 2>/dev/null +stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "someother:*.example.test:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" $WORK/pb.out && break; sleep 0.25; done +echo "== ProxyBridge running, python3 NOT covered (DNS untouched by the tap)"; bench; bench diff --git a/Linux/tests/e2e.sh b/Linux/tests/e2e.sh new file mode 100755 index 00000000..a0fe0d1b --- /dev/null +++ b/Linux/tests/e2e.sh @@ -0,0 +1,143 @@ +#!/bin/bash +# End-to-end test of the eBPF backend. Needs root, curl, python3. Uses only loopback + TEST-NET addresses (nothing leaves the host). +# sudo Linux/tests/e2e.sh +set -u +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(dirname "$HERE")" +WORK="$(mktemp -d)" +LOG="$WORK/proxy.log"; : > "$LOG" +PASS=0; FAIL=0 +ok() { echo " PASS $1"; PASS=$((PASS+1)); } +bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); } +expect_contains() { # name haystack needle + if [[ "$2" == *"$3"* ]]; then ok "$1"; else bad "$1 (got: ${2:0:200})"; fi; } + +cleanup() { kill "$PB_PID" "$FAKE_PID" "$DIRECT_PID" 2>/dev/null; wait 2>/dev/null; rm -rf "$WORK"; } +trap cleanup EXIT + +[ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } +export LD_LIBRARY_PATH="$ROOT/src" + +python3 "$HERE/fake_proxies.py" "$LOG" > "$WORK/fake.out" 2>&1 & FAKE_PID=$! +# a direct (non-proxied) origin on the host's own LAN address, so DIRECT rules can be seen to work +MYIP=$(ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' | head -1); MYIP=${MYIP:-127.0.0.1} +mkdir -p "$WORK/www"; echo "direct-origin" > "$WORK/www/index.html" +python3 -m http.server 18399 --bind "$MYIP" --directory "$WORK/www" >/dev/null 2>&1 & DIRECT_PID=$! +sleep 1 + +cp "$(command -v curl)" "$WORK/newcurl" # a binary that did not exist when ProxyBridge started + +stdbuf -oL "$ROOT/cli/ProxyBridge" --verbose 3 \ + --proxy a=socks5://127.0.0.1:19080 --proxy b=socks5://bob:pw@127.0.0.1:19081 --proxy c=http://127.0.0.1:19082 \ + --rule "curl:$MYIP/32:18399:TCP:DIRECT" \ + --rule "curl:192.0.2.0/24:*:TCP:PROXY=a" \ + --rule "curl:198.51.100.0/24:*:TCP:PROXY=b" \ + --rule "curl:203.0.113.0/24:*:TCP:PROXY=c" \ + --rule "curl:203.0.114.0/24:*:TCP:BLOCK" \ + --rule "curl:2001:db8::/32:80:TCP:PROXY=a" \ + --rule "curl:10.200.0.0/24:*:TCP:PROXY=a" \ + --rule "newcurl:192.0.2.0/24:*:TCP:PROXY=b" \ + --rule "python3*:192.0.2.0/24:5300:UDP:PROXY=a" \ + --rule "python3*:198.51.100.0/24:5300:UDP:PROXY=b" \ + --rule "python3*:2001:db8::/32:*:BOTH:PROXY=a" \ + > "$WORK/pb.out" 2>&1 & PB_PID=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && break; sleep 0.25; done +if ! grep -q "ProxyBridge started" "$WORK/pb.out"; then echo "ProxyBridge failed to start:"; cat "$WORK/pb.out"; exit 1; fi +sleep 1 + +echo "== TCP: per-rule proxy selection, original destination recovery" +out=$(curl -s -m 5 http://192.0.2.7/); expect_contains "proxy A via SOCKS5" "$out" "hello-from-A dst=192.0.2.7:80" +out=$(curl -s -m 5 http://198.51.100.7:8080/); expect_contains "proxy B via SOCKS5 user/pass" "$out" "hello-from-B dst=198.51.100.7:8080" +out=$(curl -s -m 5 http://203.0.113.7:81/); expect_contains "proxy C via HTTP CONNECT" "$out" "hello-from-C dst=203.0.113.7:81" +out=$(curl -s -m 5 http://203.0.114.7/ 2>&1; echo "rc=$?"); expect_contains "BLOCK rule rejects connect()" "$out" "rc=7" +out=$(curl -s -m 5 "http://$MYIP:18399/"); expect_contains "DIRECT rule bypasses the proxies" "$out" "direct-origin" +out=$(curl -s -g -m 5 'http://[2001:db8::7]/'); expect_contains "IPv6 destination through proxy A" "$out" "hello-from-A dst=2001:db8::7:80" +out=$(curl -s -m 5 http://127.0.0.1:19080/ 2>&1; echo "rc=$?"); expect_contains "loopback is never redirected" "$out" "rc=" +out=$("$WORK/newcurl" -s -m 5 http://192.0.2.8/); expect_contains "binary unknown at start (slow path)" "$out" "hello-from-B dst=192.0.2.8:80" +out=$("$WORK/newcurl" -s -m 5 http://192.0.2.9/); expect_contains "same binary, learned class (fast path)" "$out" "hello-from-B dst=192.0.2.9:80" +out=$(curl -s -m 5 http://192.0.2.10/); expect_contains "other binary still uses its own rule" "$out" "hello-from-A dst=192.0.2.10:80" +pids=(); for i in $(seq 30); do curl -s -m 5 http://192.0.2.$((20+i))/ >/dev/null & pids+=($!); done; wait "${pids[@]}" 2>/dev/null +sleep 1 +c=$(grep -c "^A TCP 192.0.2.\(2[1-9]\|3[0-9]\|4[0-9]\|50\):80" "$LOG"); [ "$c" -ge 30 ] && ok "30 concurrent connections all proxied ($c)" || bad "concurrent: only $c/30 reached proxy A" + +echo "== isolation: other network namespaces (containers) are never redirected" +if ip netns add pbtest 2>/dev/null; then + ip netns exec pbtest ip link set lo up; ip netns exec pbtest ip addr add 10.200.0.1/32 dev lo + ip netns exec pbtest python3 -m http.server 18400 --bind 10.200.0.1 --directory "$WORK/www" >/dev/null 2>&1 & NSPID=$! + sleep 1 + out=$(ip netns exec pbtest curl -s -m 4 http://10.200.0.1:18400/ 2>&1; echo "rc=$?") + expect_contains "curl inside a netns reaches its own server (rule matches the IP, but the namespace is foreign)" "$out" "direct-origin" + kill $NSPID 2>/dev/null; ip netns del pbtest 2>/dev/null +else + echo " SKIP ip netns not available" +fi + +echo "== UDP: unconnected, multi-destination, connected, IPv6" +out=$(python3 - <<'PY' +import socket +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(4) +res = [] +for dst in [("192.0.2.9", 5300), ("198.51.100.9", 5300), ("192.0.2.9", 5300)]: + s.sendto(b"ping", dst) + d, a = s.recvfrom(200) + res.append(f"{d.decode()}|from={a[0]}:{a[1]}") +print("\n".join(res)) +c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); c.settimeout(4) +c.connect(("192.0.2.9", 5300)); c.send(b"conn"); d = c.recv(200) +print("connected:" + d.decode() + "|peer=%s:%d" % c.getpeername()) +s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM); s6.settimeout(4) +try: + s6.sendto(b"v6", ("2001:db8::9", 5300)); print("v6 unconnected: sent") +except OSError as e: + print("v6 unconnected: blocked errno=%d" % e.errno) +c6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM); c6.settimeout(4) +c6.connect(("2001:db8::9", 5300)); c6.send(b"v6c"); d = c6.recv(200) +print("v6 connected:" + d.decode() + "|peer=%s:%d" % c6.getpeername()[:2]) +d6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM); d6.settimeout(4) +d6.sendto(b"mapped", ("::ffff:192.0.2.9", 5300)); d, a = d6.recvfrom(200) +print("v6 socket, v4 dest:" + d.decode() + "|from=%s" % a[0]) +PY +) +expect_contains "UDP via proxy A, source restored" "$out" "UDP-A 192.0.2.9:5300 ping|from=192.0.2.9:5300" +expect_contains "same socket, second destination -> proxy B" "$out" "UDP-B 198.51.100.9:5300 ping|from=198.51.100.9:5300" +expect_contains "connected UDP works and getpeername restored" "$out" "connected:UDP-A 192.0.2.9:5300 conn|peer=192.0.2.9:5300" +expect_contains "native IPv6 unconnected UDP fails closed (EPERM)" "$out" "v6 unconnected: blocked errno=1" +expect_contains "connected IPv6 UDP through proxy A, peer restored" "$out" "v6 connected:UDP-A 2001:db8::9:5300 v6c|peer=2001:db8::9:5300" +expect_contains "dual-stack socket sending to IPv4 dest" "$out" "v6 socket, v4 dest:UDP-A 192.0.2.9:5300 mapped" + +echo "== UDP: large datagrams and many sockets" +out=$(python3 - <<'PY' +import socket, concurrent.futures as cf +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(5) +big = bytes(range(256)) * 230 # 58,880 bytes +s.sendto(big, ("192.0.2.9", 5300)); d, a = s.recvfrom(70000) +prefix = b"UDP-A 192.0.2.9:5300 " +print("big:", "ok" if d == prefix + big else "MISMATCH len=%d" % len(d)) +def one(i): + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); c.settimeout(8) + payload = b"n%d" % i + c.sendto(payload, ("192.0.2.%d" % (100 + i % 100), 5300)) + d, a = c.recvfrom(100) + return d.endswith(payload) and a[0] == "192.0.2.%d" % (100 + i % 100) +with cf.ThreadPoolExecutor(64) as ex: + res = list(ex.map(one, range(400))) +print("many:", sum(res), "/ 400") +PY +) +expect_contains "58KB datagram through SOCKS5 UDP, intact" "$out" "big: ok" +expect_contains "400 concurrent UDP sockets, each reply from the right address" "$out" "many: 400 / 400" + +echo "== logging" +sleep 1 +grep -q "\[CONN\] curl" "$WORK/pb.out" && ok "connection events reported to the CLI" || bad "no [CONN] events" + +echo "== lifecycle" +kill -INT "$PB_PID"; wait "$PB_PID" 2>/dev/null +out=$(curl -s -m 3 http://192.0.2.7/ 2>&1; echo "rc=$?") +[[ "$out" != *hello-from* ]] && ok "after stop traffic is no longer redirected" || bad "still redirected after stop" +bpftool prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left behind" || ok "BPF programs detached" +ss -ltn 2>/dev/null | grep -qE ":2401[0-9]" && bad "relay ports still listening" || ok "relay ports released" + +echo +echo "passed=$PASS failed=$FAIL" +[ "$FAIL" = 0 ] || { echo "---- proxy log"; cat "$LOG"; echo "---- pb output"; tail -30 "$WORK/pb.out"; exit 1; } diff --git a/Linux/tests/e2e_domains.sh b/Linux/tests/e2e_domains.sh new file mode 100755 index 00000000..60cdadbd --- /dev/null +++ b/Linux/tests/e2e_domains.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# End-to-end test of domain-name rules (DNS tap). Needs root, dig, curl, python3. +# sudo Linux/tests/e2e_domains.sh +set -u +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +WORK="$(mktemp -d)"; PLOG="$WORK/proxy.log"; DLOG="$WORK/dns.log"; : > "$PLOG"; : > "$DLOG" +PASS=0; FAIL=0; DNSIP=10.77.0.2 +ok() { echo " PASS $1"; PASS=$((PASS+1)); } +bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); } +has() { [[ "$2" == *"$3"* ]] && ok "$1" || bad "$1 (got: ${2:0:160})"; } +cleanup() { kill "$PB_PID" "$FAKE_PID" "$DNS_PID" 2>/dev/null; wait 2>/dev/null; ip addr del $DNSIP/32 dev lo 2>/dev/null; rm -rf "$WORK"; } +trap cleanup EXIT +[ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } +export LD_LIBRARY_PATH="$ROOT/src" +ip addr add $DNSIP/32 dev lo 2>/dev/null +python3 "$HERE/fake_proxies.py" "$PLOG" > /dev/null 2>&1 & FAKE_PID=$! +python3 "$HERE/fake_dns.py" $DNSIP "$DLOG" > /dev/null 2>&1 & DNS_PID=$! +sleep 1 + +stdbuf -oL "$ROOT/cli/ProxyBridge" --verbose 2 \ + --proxy a=socks5://127.0.0.1:19080 --proxy b=socks5://bob:pw@127.0.0.1:19081 --proxy h=socks5://localhost:19080 \ + --rule "*:blocked.example.test:*:TCP:BLOCK" \ + --rule "*:*.example.test:*:TCP:PROXY=a" \ + --rule "*:*.corp.test;*.cdnnet.test:*:TCP:PROXY=b" \ + --rule "*:svc.byname.test:*:TCP:PROXY=h" \ + > "$WORK/pb.out" 2>&1 & PB_PID=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && break; sleep 0.25; done +grep -q "ProxyBridge started" "$WORK/pb.out" || { echo "ProxyBridge failed to start"; cat "$WORK/pb.out"; exit 1; } +sleep 1 + +resolve() { dig @$DNSIP "$@" +short +time=3 +tries=1 | head -1; } + +echo "== resolver answers are delivered unchanged" +ip1=$(resolve app.example.test); has "UDP DNS answer reaches the application" "$ip1" "192.0.2.77" +ip2=$(dig @$DNSIP +tcp tcp.example.test +short +time=3 +tries=1 | head -1); has "TCP DNS answer reaches the application" "$ip2" "192.0.2.80" +nx=$(dig @$DNSIP nope.example.test +time=3 +tries=1 | grep -c NXDOMAIN); [ "$nx" -ge 1 ] && ok "NXDOMAIN is passed through" || bad "NXDOMAIN lost" + +echo "== domain rules: resolve with one process, connect with another" +out=$(curl -s -m 5 http://192.0.2.77/); has "*.example.test -> proxy a (UDP DNS learned)" "$out" "hello-from-A dst=192.0.2.77:80" +out=$(curl -s -m 5 http://192.0.2.80/); has "*.example.test -> proxy a (TCP DNS learned)" "$out" "hello-from-A dst=192.0.2.80:80" +resolve blocked.example.test >/dev/null +out=$(curl -s -m 5 http://192.0.2.78/ 2>&1; echo "rc=$?"); has "exact-name BLOCK rule beats the wildcard rule" "$out" "rc=7" +resolve www.corp.test >/dev/null +out=$(curl -s -m 5 http://192.0.2.79/); has "CNAME chain: rule on the canonical name -> proxy b" "$out" "hello-from-B dst=192.0.2.79:80" +resolve svc.byname.test >/dev/null +out=$(curl -s -m 5 http://192.0.2.81/); has "domain rule + proxy configured by hostname (localhost)" "$out" "hello-from-A dst=192.0.2.81:80" + +echo "== things that must NOT match" +resolve plain.other.test >/dev/null +out=$(curl -s -m 2 http://192.0.2.82/ 2>&1; echo "rc=$?"); [[ "$out" != *hello-from* ]] && ok "resolved name without a rule is not proxied" || bad "unexpected proxying" +out=$(curl -s -m 2 http://192.0.2.99/ 2>&1; echo "rc=$?"); [[ "$out" != *hello-from* ]] && ok "IP never seen in DNS is not matched by a domain rule" || bad "unexpected proxying" +grep -q "192.0.2.82\|192.0.2.99" "$PLOG" && bad "proxy saw traffic it should not have" || ok "proxies saw no unmatched destinations" + +echo "== latency: first connection right after the answer (no race)" +okc=0; for i in 1 2 3 4 5; do resolve app.example.test >/dev/null; r=$(curl -s -m 5 http://192.0.2.77/); [[ "$r" == *hello-from-A* ]] && okc=$((okc+1)); done +[ "$okc" = 5 ] && ok "5/5 connections matched immediately after resolution" || bad "only $okc/5 matched immediately" + +echo "== logging" +sleep 1 +grep -q "192.0.2.77 (app.example.test)" "$WORK/pb.out" && ok "connection log shows the domain name" || bad "no domain name in connection log" + +echo "== lifecycle" +kill -INT "$PB_PID"; wait "$PB_PID" 2>/dev/null +out=$(dig @$DNSIP app.example.test +short +time=3 +tries=1 | head -1); has "DNS still works after ProxyBridge stops" "$out" "192.0.2.77" +bpftool prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left behind" || ok "BPF programs detached" + +echo; echo "passed=$PASS failed=$FAIL" +[ "$FAIL" = 0 ] || { echo "---- dns log"; cat "$DLOG"; echo "---- proxy log"; cat "$PLOG"; echo "---- pb output"; tail -30 "$WORK/pb.out"; exit 1; } diff --git a/Linux/tests/e2e_openssh.sh b/Linux/tests/e2e_openssh.sh new file mode 100755 index 00000000..6124e9b0 --- /dev/null +++ b/Linux/tests/e2e_openssh.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Interoperability with a REAL third-party SOCKS5 server: OpenSSH's `ssh -D`. Needs root, sshd, ssh, curl, python3. +# sudo tests/e2e_openssh.sh +set -u +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +WORK="$(mktemp -d)"; PASS=0; FAIL=0; ORIGIN=10.77.0.5 +ok() { echo " PASS $1"; PASS=$((PASS+1)); } +bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); } +cleanup() { kill $SSHD $SSH $WWW $PB 2>/dev/null; wait 2>/dev/null; ip addr del $ORIGIN/32 dev lo 2>/dev/null; rm -rf "$WORK"; } +trap cleanup EXIT +[ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } +export LD_LIBRARY_PATH="$ROOT/src" +command -v sshd >/dev/null && command -v ssh >/dev/null || { echo "SKIP: openssh not installed"; exit 0; } +ip addr add $ORIGIN/32 dev lo 2>/dev/null + +ssh-keygen -q -t ed25519 -N '' -f "$WORK/hostkey"; ssh-keygen -q -t ed25519 -N '' -f "$WORK/clientkey" +cp "$WORK/clientkey.pub" "$WORK/authorized_keys"; chmod 600 "$WORK/authorized_keys" +cat > "$WORK/sshd_config" < "$WORK/sshd.log" & SSHD=$! +sleep 1 +ssh -N -D 127.0.0.1:19090 -p 22222 -i "$WORK/clientkey" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ExitOnForwardFailure=yes root@127.0.0.1 2> "$WORK/ssh.log" & SSH=$! +for _ in $(seq 30); do ss -ltn 2>/dev/null | grep -q ":19090" && break; sleep 0.3; done +ss -ltn | grep -q ":19090" && ok "OpenSSH SOCKS5 server is up (ssh -D 19090)" || { bad "ssh -D did not start: $(tail -3 $WORK/ssh.log)"; exit 1; } + +# a real origin on a non-loopback local address, serving a 40MB file and a small page +head -c 41943040 /dev/urandom > "$WORK/big.bin"; echo "real-origin-page" > "$WORK/index.html" +python3 -m http.server 18500 --bind $ORIGIN --directory "$WORK" >/dev/null 2>&1 & WWW=$! +sleep 1 +SUM=$(sha256sum "$WORK/big.bin" | cut -d' ' -f1) + +stdbuf -oL "$ROOT/cli/ProxyBridge" --verbose 1 --proxy ossh=socks5://127.0.0.1:19090 \ + --rule "curl:$ORIGIN:18500:TCP:PROXY=ossh" > "$WORK/pb.out" 2>&1 & PB=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && break; sleep 0.25; done + +out=$(curl -s -m 8 http://$ORIGIN:18500/index.html) +[[ "$out" == *real-origin-page* ]] && ok "small request through OpenSSH's SOCKS5 server" || bad "small request (got: ${out:0:100})" +GOT=$(curl -s -m 60 http://$ORIGIN:18500/big.bin | sha256sum | cut -d' ' -f1) +[ "$GOT" = "$SUM" ] && ok "40 MB download through OpenSSH SOCKS5: checksum identical" || bad "40MB checksum mismatch" +pids=(); for i in $(seq 40); do curl -s -m 20 http://$ORIGIN:18500/index.html > "$WORK/c$i.out" & pids+=($!); done; wait "${pids[@]}" 2>/dev/null +n=$(grep -l real-origin-page "$WORK"/c*.out 2>/dev/null | wc -l); [ "$n" = 40 ] && ok "40 parallel connections through OpenSSH SOCKS5" || bad "only $n/40 parallel requests succeeded" +# upload (client -> origin) integrity +python3 - "$WORK" <<'PY' & +import http.server, socketserver, sys, hashlib, threading +class H(http.server.BaseHTTPRequestHandler): + def do_POST(self): + n = int(self.headers["Content-Length"]); h = hashlib.sha256(); left = n + while left: + b = self.rfile.read(min(65536, left)); h.update(b); left -= len(b) + body = h.hexdigest().encode(); self.send_response(200); self.send_header("Content-Length", str(len(body))); self.end_headers(); self.wfile.write(body) + def log_message(self, *a): pass +socketserver.TCPServer.allow_reuse_address = True +socketserver.ThreadingTCPServer(("10.77.0.5", 18501), H).serve_forever() +PY +UP=$!; sleep 1 +kill -INT $PB; wait $PB 2>/dev/null +stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy ossh=socks5://127.0.0.1:19090 --rule "curl:$ORIGIN:18500-18501:TCP:PROXY=ossh" > "$WORK/pb.out" 2>&1 & PB=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && break; sleep 0.25; done +GOT=$(curl -s -m 60 --data-binary @"$WORK/big.bin" http://$ORIGIN:18501/ ) +[ "$GOT" = "$SUM" ] && ok "40 MB upload through OpenSSH SOCKS5: origin received identical bytes" || bad "upload checksum mismatch (got ${GOT:0:20})" +kill $UP 2>/dev/null +# negative control: with the SSH proxy gone, the redirected request must FAIL (it would succeed if it bypassed the proxy) +kill $SSH 2>/dev/null; sleep 1 +out=$(curl -s -m 4 http://$ORIGIN:18500/index.html 2>&1; echo "rc=$?") +[[ "$out" != *real-origin-page* ]] && ok "negative control: with the proxy killed the request fails, so it really went through the proxy" || bad "request succeeded without the proxy: traffic bypassed it" +echo; echo "passed=$PASS failed=$FAIL" +[ "$FAIL" = 0 ] || { echo "--- pb"; tail -15 "$WORK/pb.out"; echo "--- sshd"; tail -10 "$WORK/sshd.log"; exit 1; } diff --git a/Linux/tests/e2e_rules.sh b/Linux/tests/e2e_rules.sh new file mode 100755 index 00000000..da0f700a --- /dev/null +++ b/Linux/tests/e2e_rules.sh @@ -0,0 +1,143 @@ +#!/bin/bash +# Rule-matrix end-to-end test: every rule form and every upstream proxy type through the real kernel datapath. +# sudo tests/e2e_rules.sh (needs curl, python3; only loopback + TEST-NET addresses are used) +set -u +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +WORK="$(mktemp -d)"; PLOG="$WORK/proxy.log"; : > "$PLOG" +PASS=0; FAIL=0 +ok() { echo " PASS $1"; PASS=$((PASS+1)); } +bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); } +via() { # name url expected-proxy-letter expected-dst + local out; out=$(curl -s -m 5 "$2" 2>&1) + [[ "$out" == *"hello-from-$3 dst=$4"* ]] && ok "$1" || bad "$1 (got: ${out:0:140})"; } +direct() { # name url -> must NOT reach any proxy (times out / fails) + local out; out=$(curl -s -m 2 "$2" 2>&1; echo "rc=$?") + [[ "$out" != *hello-from* && "$out" != *"rc=0"* ]] && ok "$1" || bad "$1 (got: ${out:0:140})"; } +blocked() { local out; out=$(curl -s -m 5 "$2" 2>&1; echo "rc=$?"); [[ "$out" == *"rc=7"* ]] && ok "$1" || bad "$1 (got: ${out:0:140})"; } +cleanup() { kill $PB_PID $FAKE_PID 2>/dev/null; wait 2>/dev/null; rm -rf "$WORK"; } +trap cleanup EXIT +[ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } +export LD_LIBRARY_PATH="$ROOT/src" + +python3 "$HERE/fake_proxies.py" "$PLOG" >/dev/null 2>&1 & FAKE_PID=$! +mkdir -p "$WORK/bin" "$WORK/pbpath" +CURL=$(command -v curl) +cp "$CURL" "$WORK/bin/wget"; cp "$CURL" "$WORK/bin/newtool"; cp "$CURL" "$WORK/bin/mycurlbin"; cp "$CURL" "$WORK/pbpath/tool" +sleep 1 + +start_pb() { # extra CLI args... + stdbuf -oL "$ROOT/cli/ProxyBridge" --verbose 1 \ + --proxy a=socks5://127.0.0.1:19080 --proxy b=socks5://bob:pw@127.0.0.1:19081 --proxy c=http://127.0.0.1:19082 \ + --proxy d=http://bob:pw@127.0.0.1:19083 --proxy e=http://bob:wrong@127.0.0.1:19083 "$@" > "$WORK/pb.out" 2>&1 & PB_PID=$! + for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && return 0; sleep 0.25; done + echo "ProxyBridge failed to start"; cat "$WORK/pb.out"; exit 1; } + +start_pb \ + --rule "curl:192.0.2.90:*:TCP:BLOCK" \ + --rule "curl:192.0.2.0/24:*:TCP:PROXY=a" \ + --rule "curl:198.51.100.20-198.51.100.29:*:TCP:PROXY=b" \ + --rule "curl:198.51.*.*:443:TCP:PROXY=c" \ + --rule "curl:203.0.1-3.*:*:TCP:PROXY=a" \ + --rule "curl:203.0.113.40:80;443;8000-9000:TCP:PROXY=b" \ + --rule "wget;newtool:203.0.113.50:*:TCP:PROXY=a" \ + --rule "$WORK/pbpath/*:203.0.113.60:*:TCP:PROXY=b" \ + --rule "*urlbin*:203.0.113.70:*:TCP:PROXY=a" \ + --rule "curl:203.0.113.80:1;2;3;4;5;6;7;80:TCP:PROXY=b" \ + --rule "curl:10.0.0.0/8;172.16.0.0/12;100.64.0.0/10;192.168.0.0/16;169.254.0.0/16;203.0.113.90:*:TCP:PROXY=a" \ + --rule "CURL:203.0.113.150:*:TCP:PROXY=a" \ + --rule "curl:203.0.113.100:*:TCP:PROXY=d" \ + --rule "curl:203.0.113.101:*:TCP:PROXY=e" \ + --rule "python3*:203.0.113.70:*:UDP:PROXY=a" \ + --rule "python3*:203.0.113.110:5300:UDP:PROXY=c" \ + --rule "python3*:203.0.113.120:*:UDP:BLOCK" \ + --rule "python3*:203.0.113.130:5300-5310:UDP:PROXY=b" \ + --rule "python3*:203.0.113.140:53:UDP:PROXY=a" \ + --rule "python3*:203.0.113.160:*:BOTH:PROXY=a" +sleep 1 + +echo "== host forms (process: curl)" +via "single IP" http://192.0.2.10/ A 192.0.2.10:80 +via "CIDR /24" http://192.0.2.200/ A 192.0.2.200:80 +via "range a-b (inside)" http://198.51.100.25/ B 198.51.100.25:80 +direct "range a-b (outside)" http://198.51.100.30:8080/ +via "octet wildcard 198.51.*.* + port 443" http://198.51.100.99:443/ C 198.51.100.99:443 +via "octet-range box 203.0.1-3.* (slow path)" http://203.0.2.77/ A 203.0.2.77:80 +direct "octet-range box (outside)" http://203.0.4.77/ +via "6 CIDRs in one rule (kernel cannot hold them, slow path)" http://203.0.113.90/ A 203.0.113.90:80 + +echo "== port forms" +via "port list: 80" http://203.0.113.40/ B 203.0.113.40:80 +via "port range 8000-9000 (8500)" http://203.0.113.40:8500/ B 203.0.113.40:8500 +direct "port not in list/range (22)" http://203.0.113.40:22/ +via "8 ports in a list (kernel table has 4 -> slow path)" http://203.0.113.80/ B 203.0.113.80:80 +direct "port 81 not in that list" http://203.0.113.80:81/ + +echo "== process forms" +out=$("$WORK/bin/wget" -s -m 5 http://203.0.113.50/); [[ "$out" == *"hello-from-A dst=203.0.113.50:80"* ]] && ok "process list a;b matches 'wget'" || bad "process list (got ${out:0:100})" +out=$("$WORK/bin/newtool" -s -m 5 http://203.0.113.50/); [[ "$out" == *"hello-from-A dst=203.0.113.50:80"* ]] && ok "process list a;b matches 'newtool'" || bad "process list 2 (got ${out:0:100})" +out=$(curl -s -m 2 http://203.0.113.50/ 2>&1; echo "rc=$?"); [[ "$out" != *hello-from* ]] && ok "process not in the list ('curl') is not matched" || bad "list leaked" +out=$("$WORK/pbpath/tool" -s -m 5 http://203.0.113.60/); [[ "$out" == *"hello-from-B dst=203.0.113.60:80"* ]] && ok "full-path glob /dir/* matches" || bad "path glob (got ${out:0:100})" +out=$("$WORK/bin/mycurlbin" -s -m 5 http://203.0.113.70/); [[ "$out" == *"hello-from-A dst=203.0.113.70:80"* ]] && ok "middle glob *urlbin* matches 'mycurlbin'" || bad "middle glob (got ${out:0:100})" +direct "process names are case-sensitive (rule 'CURL' does not match 'curl')" http://203.0.113.150/ + +echo "== ordering: first match wins" +blocked "specific BLOCK before the broader /24 PROXY" http://192.0.2.90/ +via "same /24, other address, still proxied" http://192.0.2.91/ A 192.0.2.91:80 + +echo "== HTTP proxies" +via "HTTP CONNECT with Basic auth (correct credentials)" http://203.0.113.100/ D 203.0.113.100:80 +out=$(curl -s -m 5 http://203.0.113.101/ 2>&1; echo "rc=$?"); [[ "$out" != *hello-from* ]] && ok "HTTP CONNECT with wrong credentials fails (407), nothing leaks" || bad "wrong creds accepted" +grep -q "^D AUTHFAIL" "$PLOG" && ok "the proxy really answered 407 (proxy log)" || bad "no 407 seen by the proxy" + +echo "== UDP rules" +out=$(python3 - <<'PY' +import socket +def probe(dst, payload=b"p", timeout=2): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(timeout) + try: + s.sendto(payload, dst); d, a = s.recvfrom(200); return d.decode() + "|" + a[0] + ":" + str(a[1]) + except socket.timeout: return "timeout" + except OSError as e: return "errno=%d" % e.errno +print("port-range-in:", probe(("203.0.113.130", 5305))) +print("port-range-out:", probe(("203.0.113.130", 5400), timeout=1)) +print("udp-only-rule-on-tcp-dst:", probe(("203.0.113.70", 5300), timeout=1)) +print("block:", probe(("203.0.113.120", 5300))) +print("http-proxy-udp:", probe(("203.0.113.110", 5300), timeout=1)) +print("both:", probe(("203.0.113.160", 5300))) +print("dns-port-53:", probe(("203.0.113.140", 53))) +PY +) +[[ "$out" == *"port-range-in: UDP-B 203.0.113.130:5305 p|203.0.113.130:5305"* ]] && ok "UDP port range 5300-5310 via SOCKS5+auth, source restored" || bad "udp port range ($out)" +[[ "$out" == *"port-range-out: timeout"* ]] && ok "UDP port outside the range is not proxied" || bad "udp range leak" +[[ "$out" == *"udp-only-rule-on-tcp-dst: UDP-A 203.0.113.70:5300"* ]] && ok "protocol UDP rule matches UDP" || bad "udp proto rule ($out)" +direct "protocol UDP rule does NOT match TCP to the same address" http://203.0.113.70/ +[[ "$out" == *"block: errno=1"* ]] && ok "UDP BLOCK rule: sendto fails with EPERM" || bad "udp block ($out)" +[[ "$out" == *"http-proxy-udp: timeout"* ]] && ok "UDP rule pointing at an HTTP proxy goes direct (HTTP cannot carry UDP)" || bad "http udp ($out)" +grep -q "UDP 203.0.113.110" "$PLOG" && bad "UDP leaked into the HTTP proxy" || ok "the HTTP proxy never saw that UDP" +[[ "$out" == *"both: UDP-A 203.0.113.160:5300"* ]] && ok "protocol BOTH rule matches UDP" || bad "both/udp ($out)" +tcpout=$(python3 - <<'PY' +import socket +s = socket.create_connection(("203.0.113.160", 80), timeout=5); s.sendall(b"GET / HTTP/1.0\r\n\r\n"); print(s.recv(300).decode()) +PY +) +[[ "$tcpout" == *"hello-from-A dst=203.0.113.160:80"* ]] && ok "protocol BOTH rule matches TCP too" || bad "both/tcp (got ${tcpout:0:100})" +[[ "$out" == *"dns-port-53: UDP-A 203.0.113.140:53"* ]] && ok "DNS port rule applies when --dns-via-proxy is true (default)" || bad "dns true ($out)" + +echo "== DNS switch: --dns-via-proxy false makes port 53 always direct" +kill -INT $PB_PID; wait $PB_PID 2>/dev/null +start_pb --dns-via-proxy false --rule "python3*:203.0.113.140:53:UDP:PROXY=a" --rule "python3*:203.0.113.141:5353:UDP:PROXY=a" +out=$(python3 - <<'PY' +import socket +def probe(dst, timeout=1.5): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(timeout) + try: s.sendto(b"p", dst); d, a = s.recvfrom(200); return d.decode() + except socket.timeout: return "timeout" +print("dns:", probe(("203.0.113.140", 53))) +print("other:", probe(("203.0.113.141", 5353))) +PY +) +[[ "$out" == *"dns: timeout"* ]] && ok "port 53 stays direct with dns-via-proxy=false" || bad "dns false ($out)" +[[ "$out" == *"other: UDP-A 203.0.113.141:5353"* ]] && ok "non-DNS UDP rule still proxied" || bad "other udp ($out)" + +echo; echo "passed=$PASS failed=$FAIL" +[ "$FAIL" = 0 ] || { echo "---- proxy log"; cat "$PLOG"; echo "---- pb output"; tail -20 "$WORK/pb.out"; exit 1; } diff --git a/Linux/tests/fake_dns.py b/Linux/tests/fake_dns.py new file mode 100644 index 00000000..ab8e85a4 --- /dev/null +++ b/Linux/tests/fake_dns.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Tiny authoritative DNS server for ProxyBridge end-to-end tests (UDP + TCP, IPv4 A records and CNAME chains). + +usage: fake_dns.py +Zone (everything else answers NXDOMAIN): + app.example.test A 192.0.2.77 + blocked.example.test A 192.0.2.78 + www.corp.test CNAME edge.cdnnet.test + edge.cdnnet.test A 192.0.2.79 + tcp.example.test A 192.0.2.80 + svc.byname.test A 192.0.2.81 + plain.other.test A 192.0.2.82 +""" +import socket, socketserver, struct, sys, threading + +ZONE = { + "app.example.test": [("A", "192.0.2.77")], + "blocked.example.test": [("A", "192.0.2.78")], + "www.corp.test": [("CNAME", "edge.cdnnet.test")], + "edge.cdnnet.test": [("A", "192.0.2.79")], + "tcp.example.test": [("A", "192.0.2.80")], + "svc.byname.test": [("A", "192.0.2.81")], + "plain.other.test": [("A", "192.0.2.82")], +} +LOG = open(sys.argv[2], "a", buffering=1) + + +def enc_name(n): + return b"".join(bytes([len(p)]) + p.encode() for p in n.split(".")) + b"\0" + + +def dec_name(m, off): + parts = [] + while m[off]: + l = m[off]; parts.append(m[off + 1:off + 1 + l].decode()); off += 1 + l + return ".".join(parts), off + 1 + + +def answer(query, proto): + qid, flags, qd = struct.unpack("!HHH", query[:6]) + name, off = dec_name(query, 12) + qtype = struct.unpack("!H", query[off:off + 2])[0] + LOG.write(f"{proto} {name} type={qtype}\n") + key = name.lower() + rrs, cur, seen = [], key, 0 + while cur in ZONE and seen < 8: + seen += 1 + for kind, val in ZONE[cur]: + if kind == "CNAME": + rd = enc_name(val) + rrs.append(enc_name(cur) + struct.pack("!HHIH", 5, 1, 300, len(rd)) + rd) + cur = val + elif qtype in (1, 255): + rrs.append(enc_name(cur) + struct.pack("!HHIH", 1, 1, 300, 4) + socket.inet_aton(val)) + cur = None + else: + cur = None + if cur is None: + break + if cur is None: + break + rcode = 0 if rrs or key in ZONE else 3 + hdr = struct.pack("!HHHHHH", qid, 0x8180 | rcode, 1, len(rrs), 0, 0) + return hdr + query[12:off + 4] + b"".join(rrs) + + +class UDP(socketserver.BaseRequestHandler): + def handle(self): + data, sock = self.request + try: + sock.sendto(answer(data, "udp"), self.client_address) + except Exception as e: + LOG.write(f"error {e!r}\n") + + +class TCP(socketserver.BaseRequestHandler): + def handle(self): + try: + while True: + hdr = self.request.recv(2) + if len(hdr) < 2: + return + n = struct.unpack("!H", hdr)[0] + data = b"" + while len(data) < n: + chunk = self.request.recv(n - len(data)) + if not chunk: + return + data += chunk + resp = answer(data, "tcp") + # deliver in two pieces to exercise stream reassembly in the relay + out = struct.pack("!H", len(resp)) + resp + self.request.sendall(out[:3]); self.request.sendall(out[3:]) + except Exception as e: + LOG.write(f"error {e!r}\n") + + +class TS(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + daemon_threads = True + + +class US(socketserver.ThreadingMixIn, socketserver.UDPServer): + allow_reuse_address = True + daemon_threads = True + + +ip = sys.argv[1] +threading.Thread(target=TS((ip, 53), TCP).serve_forever, daemon=True).start() +print("fake dns up on", ip, flush=True) +US((ip, 53), UDP).serve_forever() diff --git a/Linux/tests/fake_proxies.py b/Linux/tests/fake_proxies.py new file mode 100644 index 00000000..ffce0105 --- /dev/null +++ b/Linux/tests/fake_proxies.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Fake upstream proxies for ProxyBridge end-to-end tests. + +Starts, on 127.0.0.1: + A SOCKS5 no-auth 19080 (also UDP ASSOCIATE) + B SOCKS5 user/pass 19081 (user "bob", pass "pw") + C HTTP CONNECT 19082 + D HTTP CONNECT, Basic auth 19083 (user "bob", pass "pw"; anything else gets 407) +Whatever destination a client asks for, the "origin" is a built-in responder: + TCP -> an HTTP/1.0 200 response whose body is "hello-from- dst=" + UDP -> "UDP- " +Every request is appended to the log file given as argv[1] (" TCP :" / " UDP :"). +""" +import asyncio, ipaddress, socket, struct, sys + +LOG = open(sys.argv[1], "a", buffering=1) if len(sys.argv) > 1 else sys.stdout + + +def log(proxy, kind, dst): + LOG.write(f"{proxy} {kind} {dst}\n") + + +async def origin(reader, writer, proxy, dst): + """Minimal HTTP/echo origin on an already-tunnelled stream.""" + if dst.endswith(":8888"): # "hold" destination: keep the tunnel open until the client closes (scale tests) + try: + while await reader.read(65536): + pass + except Exception: + pass + writer.close() + return + try: + data = await asyncio.wait_for(reader.read(65536), 5) + except Exception: + data = b"" + if data.startswith((b"GET", b"HEAD", b"POST")): + body = f"hello-from-{proxy} dst={dst}\n".encode() + writer.write(b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\nConnection: close\r\n\r\n" % len(body) + body) + else: + writer.write(f"echo-{proxy}:".encode() + data) + try: + await writer.drain() + except Exception: + pass + writer.close() + + +def addr_str(atyp, raw): + if atyp == 1: + return str(ipaddress.IPv4Address(raw)) + if atyp == 4: + return str(ipaddress.IPv6Address(raw)) + return raw.decode() + + +async def read_addr(reader): + atyp = (await reader.readexactly(1))[0] + if atyp == 1: + raw = await reader.readexactly(4) + elif atyp == 4: + raw = await reader.readexactly(16) + else: + n = (await reader.readexactly(1))[0] + raw = await reader.readexactly(n) + port = struct.unpack("!H", await reader.readexactly(2))[0] + return atyp, raw, port + + +def socks_handler(name, auth): + async def handle(reader, writer): + try: + ver, n = await reader.readexactly(2) + methods = await reader.readexactly(n) + if auth: + if 2 not in methods: + writer.write(b"\x05\xff"); await writer.drain(); writer.close(); return + writer.write(b"\x05\x02"); await writer.drain() + v, ul = await reader.readexactly(2) + user = await reader.readexactly(ul) + pl = (await reader.readexactly(1))[0] + pw = await reader.readexactly(pl) + ok = user == b"bob" and pw == b"pw" + writer.write(b"\x01" + (b"\x00" if ok else b"\x01")); await writer.drain() + if not ok: + log(name, "AUTHFAIL", user.decode()); writer.close(); return + else: + writer.write(b"\x05\x00"); await writer.drain() + ver, cmd, _rsv = await reader.readexactly(3) + atyp, raw, port = await read_addr(reader) + dst = f"{addr_str(atyp, raw)}:{port}" + if cmd == 1: + log(name, "TCP", dst) + writer.write(b"\x05\x00\x00\x01" + socket.inet_aton("127.0.0.1") + struct.pack("!H", 0)); await writer.drain() + await origin(reader, writer, name, dst) + elif cmd == 3: + loop = asyncio.get_running_loop() + transport, _ = await loop.create_datagram_endpoint(lambda: UdpRelay(name), local_addr=("127.0.0.1", 0)) + bport = transport.get_extra_info("sockname")[1] + writer.write(b"\x05\x00\x00\x01" + socket.inet_aton("127.0.0.1") + struct.pack("!H", bport)); await writer.drain() + try: + while await reader.read(1024): # association lives as long as the TCP control connection + pass + finally: + transport.close() + else: + writer.write(b"\x05\x07\x00\x01" + b"\0" * 6); await writer.drain() + except Exception as e: + log(name, "ERR", repr(e)) + finally: + try: + writer.close() + except Exception: + pass + return handle + + +class UdpRelay(asyncio.DatagramProtocol): + def __init__(self, name): + self.name = name + + def connection_made(self, transport): + self.t = transport + + def datagram_received(self, data, addr): + if len(data) < 10 or data[2] != 0: + return + atyp = data[3] + if atyp == 1: + raw, off = data[4:8], 8 + elif atyp == 4: + raw, off = data[4:20], 20 + else: + n = data[4]; raw, off = data[5:5 + n], 5 + n + port = struct.unpack("!H", data[off:off + 2])[0] + payload = data[off + 2:] + dst = f"{addr_str(atyp, raw)}:{port}" + log(self.name, "UDP", dst) + reply = f"UDP-{self.name} {dst} ".encode() + payload + self.t.sendto(data[:off + 2] + reply, addr) + + +import base64 + + +def http_connect_handler(name, need_auth): + async def handler(reader, writer): + await http_connect(reader, writer, name, need_auth) + return handler + + +async def http_connect(reader, writer, name="C", need_auth=False): + try: + head = await reader.readuntil(b"\r\n\r\n") + first = head.split(b"\r\n")[0].decode() + if need_auth: + want = b"Proxy-Authorization: Basic " + base64.b64encode(b"bob:pw") + if want.lower() not in head.lower(): + log(name, "AUTHFAIL", first) + writer.write(b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"t\"\r\nContent-Length: 0\r\n\r\n") + await writer.drain(); writer.close(); return + if not first.startswith("CONNECT "): + writer.write(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n"); await writer.drain(); writer.close(); return + dst = first.split()[1] + log(name, "TCP", dst) + writer.write(b"HTTP/1.1 200 Connection established\r\n\r\n"); await writer.drain() + await origin(reader, writer, name, dst) + except Exception as e: + log(name, "ERR", repr(e)) + finally: + writer.close() + + +async def main(): + await asyncio.start_server(socks_handler("A", False), "127.0.0.1", 19080) + await asyncio.start_server(socks_handler("B", True), "127.0.0.1", 19081) + await asyncio.start_server(http_connect_handler("C", False), "127.0.0.1", 19082) + await asyncio.start_server(http_connect_handler("D", True), "127.0.0.1", 19083) + print("fake proxies up: A=socks5:19080 B=socks5(bob:pw):19081 C=http:19082", flush=True) + await asyncio.Event().wait() + +asyncio.run(main()) diff --git a/Linux/tests/gui_headless.sh b/Linux/tests/gui_headless.sh new file mode 100755 index 00000000..ae4c1d2f --- /dev/null +++ b/Linux/tests/gui_headless.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Runs the real GTK GUI headlessly (GTK Broadway backend, no window on your desktop) against the real engine: +# config loading (profiles, proxies, rules incl. domain rules) -> engine start -> real traffic -> clean shutdown. +# Needs root, broadwayd, dig, curl, and a built GUI: pass the path of ProxyBridgeGUI and of the directory holding libproxybridge.so. +# sudo tests/gui_headless.sh +set -u +GUI=${1:?path to ProxyBridgeGUI}; LIBDIR=${2:?dir with libproxybridge.so} +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK="$(mktemp -d)"; PLOG="$WORK/proxy.log"; DLOG="$WORK/dns.log"; : > "$PLOG"; : > "$DLOG" +PASS=0; FAIL=0; DNSIP=10.77.0.4 +ok() { echo " PASS $1"; PASS=$((PASS+1)); } +bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); } +has() { [[ "$2" == *"$3"* ]] && ok "$1" || bad "$1 (got: ${2:0:160})"; } +cleanup() { kill $GUI_PID $FAKE $DNSP $BW 2>/dev/null; wait 2>/dev/null; ip addr del $DNSIP/32 dev lo 2>/dev/null; rm -rf "$WORK"; } +trap cleanup EXIT +[ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } +ip addr add $DNSIP/32 dev lo 2>/dev/null +python3 "$HERE/fake_proxies.py" "$PLOG" >/dev/null 2>&1 & FAKE=$! +python3 "$HERE/fake_dns.py" $DNSIP "$DLOG" >/dev/null 2>&1 & DNSP=$! +broadwayd :9 >/dev/null 2>&1 & BW=$! +sleep 1.5 + +# profile "Default": proxy a (default) + b; "Alt": curl goes through b instead +write_cfg() { # active-profile +mkdir -p "$WORK/cfg" +cat > "$WORK/cfg/config.ini" < "$WORK/gui.out" 2>&1 & GUI_PID=$! + for _ in $(seq 60); do bpftool prog show 2>/dev/null | grep -q pb_connect4 && break; kill -0 $GUI_PID 2>/dev/null || break; sleep 0.25; done + sleep 1 +} +stop_gui() { + kill -TERM $GUI_PID 2>/dev/null + for _ in $(seq 40); do kill -0 $GUI_PID 2>/dev/null || break; sleep 0.25; done + if kill -0 $GUI_PID 2>/dev/null; then kill -KILL $GUI_PID; bad "GUI did not exit on SIGTERM"; else wait $GUI_PID; rc=$?; [ "$rc" = 0 ] && ok "GUI exits cleanly on SIGTERM (rc=0)" || bad "GUI exit code $rc"; fi +} + +echo "== profile Default" +run_gui Default +kill -0 $GUI_PID 2>/dev/null && ok "GUI started under Broadway" || { bad "GUI died: $(head -5 "$WORK/gui.out")"; } +bpftool prog show 2>/dev/null | grep -q pb_connect4 && ok "engine started by the GUI (eBPF programs attached)" || bad "no eBPF programs" +out=$(curl -s -m 5 http://192.0.2.7/); has "rule 1 from the config file: curl -> proxy a" "$out" "hello-from-A dst=192.0.2.7:80" +out=$(curl -s -m 5 http://198.51.100.7/); has "rule 2: curl -> proxy b (with credentials from the config)" "$out" "hello-from-B dst=198.51.100.7:80" +out=$(curl -s -m 5 http://203.0.114.7/ 2>&1; echo "rc=$?"); has "rule 3: BLOCK" "$out" "rc=7" +out=$(curl -s -m 2 http://203.0.113.7/ 2>&1; echo "rc=$?"); [[ "$out" != *hello-from* ]] && ok "disabled rule 5 is not applied" || bad "disabled rule applied" +ip4=$(dig @$DNSIP app.example.test +short +time=3 +tries=1 | head -1) +out=$(curl -s -m 5 http://$ip4/); has "domain rule from the config (*.example.test -> b) after a real DNS lookup" "$out" "hello-from-B dst=192.0.2.77:80" +stop_gui +bpftool prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left after GUI exit" || ok "programs detached after GUI exit" + +echo "== profile Alt (same GUI, different active profile)" +run_gui Alt +out=$(curl -s -m 5 http://192.0.2.7/); has "Alt profile: same rule now goes through proxy b" "$out" "hello-from-B dst=192.0.2.7:80" +out=$(curl -s -m 2 http://198.51.100.7/ 2>&1; echo "rc=$?"); [[ "$out" != *hello-from* ]] && ok "Alt profile does not apply rules of the Default profile" || bad "Default rules leaked into Alt" +stop_gui +grep -q "^\[PROFILE:Default\]" "$WORK/cfg/config.ini" && grep -q "^\[PROFILE:Alt\]" "$WORK/cfg/config.ini" && ok "both profiles preserved in the config after the GUI saved it" || bad "profiles lost from the config" +[ "$(stat -c %a "$WORK/cfg/config.ini")" = 600 ] && ok "config file mode is 0600" || bad "config file mode" + +echo; echo "passed=$PASS failed=$FAIL" +[ "$FAIL" = 0 ] || { echo "---- gui output"; cat "$WORK/gui.out"; echo "---- config"; cat "$WORK/cfg/config.ini"; exit 1; } diff --git a/Linux/tests/hookbench.c b/Linux/tests/hookbench.c new file mode 100644 index 00000000..3add3d0d --- /dev/null +++ b/Linux/tests/hookbench.c @@ -0,0 +1,37 @@ +// hookbench tcp|udp : measures connect() latency (tcp) or sendto() rate + loopback delivery (udp) to a local server it starts itself. +#include +#include +#include +#include +#include +#include +#include +#include +static double now_us(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec * 1e6 + t.tv_nsec / 1e3; } +static const char *ip; static int port, tcp; +static void *srv(void *a) +{ + (void)a; + int s = socket(AF_INET, tcp ? SOCK_STREAM : SOCK_DGRAM, 0), one = 1; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(port) }; inet_pton(AF_INET, ip, &sa.sin_addr); + if (bind(s, (void *)&sa, sizeof sa)) { perror("bind"); exit(1); } + if (tcp) { listen(s, 4096); for (;;) { int c = accept(s, 0, 0); if (c >= 0) close(c); } } + char b[2048]; for (;;) recv(s, b, sizeof b, 0); +} +int main(int argc, char **argv) +{ + tcp = !strcmp(argv[1], "tcp"); ip = argv[2]; port = atoi(argv[3]); int n = atoi(argv[4]); + pthread_t t; pthread_create(&t, 0, srv, 0); usleep(200000); + struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(port) }; inet_pton(AF_INET, ip, &sa.sin_addr); + if (tcp) { + double sum = 0; int ok = 0; + for (int i = 0; i < n; i++) { int s = socket(AF_INET, SOCK_STREAM, 0); double a = now_us(); int r = connect(s, (void *)&sa, sizeof sa); sum += now_us() - a; ok += !r; close(s); } + printf("tcp connect %s:%d ok=%d/%d avg=%.2f us\n", ip, port, ok, n, sum / n); + } else { + int s = socket(AF_INET, SOCK_DGRAM, 0); char buf[64] = "x"; double a = now_us(); + for (int i = 0; i < n; i++) sendto(s, buf, 32, 0, (void *)&sa, sizeof sa); + double d = now_us() - a; + printf("udp sendto %s:%d n=%d %.3f us/call %.0f kpps\n", ip, port, n, d / n, n / d * 1e3); + } + return 0; +} diff --git a/Linux/tests/hookbench.sh b/Linux/tests/hookbench.sh new file mode 100755 index 00000000..ea3affcf --- /dev/null +++ b/Linux/tests/hookbench.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Overhead of the eBPF hooks on DIRECT traffic. Needs root. sudo tests/hookbench.sh +HB=${1:?path to hookbench}; ROOT="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")" +export LD_LIBRARY_PATH="$ROOT/src" +IP=10.77.0.1 # alias on lo: local like loopback, but not caught by the implicit-loopback bypass +ip addr add $IP/32 dev lo 2>/dev/null +trap 'ip addr del $IP/32 dev lo 2>/dev/null' EXIT +run() { for i in 1 2 3; do $HB tcp $IP 19555 20000; done; for i in 1 2 3; do $HB udp $IP 19556 1000000; done; } +echo "### A: ProxyBridge not running"; run +start() { stdbuf -oL "$ROOT/cli/ProxyBridge" "$@" > /tmp/pbbench.out 2>&1 & PB=$!; for _ in $(seq 40); do grep -q "ProxyBridge started" /tmp/pbbench.out && break; sleep 0.25; done; } +echo "### B: running, 20 rules for other programs (this binary: empty class -> immediate direct)" +start $(for i in $(seq 20); do echo --rule "someapp$i:*:*:BOTH:PROXY"; done); run; kill -INT $PB; wait $PB 2>/dev/null +echo "### C: running, rules that match this binary but not this destination (full rule evaluation, then direct)" +start --rule "hookbench:192.0.2.0/24:*:BOTH:PROXY" --rule "hookbench:198.51.100.0/24:80;443;8000-9000:BOTH:PROXY" --rule "hookbench:203.0.113.0/24:*:BOTH:BLOCK"; run; kill -INT $PB; wait $PB 2>/dev/null +echo "### D: running, wildcard process rule with 4 destination ranges (every process evaluated)" +start --rule "*:192.0.2.0/24;198.51.100.0/24;203.0.113.0/24;100.64.0.0/10:*:BOTH:PROXY"; run; kill -INT $PB; wait $PB 2>/dev/null diff --git a/Linux/tests/scale.sh b/Linux/tests/scale.sh new file mode 100755 index 00000000..55a5cc04 --- /dev/null +++ b/Linux/tests/scale.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Holds N concurrent proxied TCP connections open through the full stack and reports ProxyBridge memory / CPU. +# sudo tests/scale.sh [N] +set -u +N=${1:-10000} +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +export LD_LIBRARY_PATH="$ROOT/src" +ulimit -n 200000 +WORK=$(mktemp -d); trap 'kill $FAKE $PB 2>/dev/null; wait 2>/dev/null; rm -rf $WORK' EXIT +python3 "$HERE/fake_proxies.py" $WORK/log >/dev/null 2>&1 & FAKE=$! +sleep 1 +stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "python3*:192.0.2.0/24:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" $WORK/pb.out && break; sleep 0.25; done +rss() { awk '/VmRSS/{print $2}' /proc/$PB/status; } +maps_kb() { bpftool map show 2>/dev/null | grep -oE 'memlock [0-9]+' | awk '{s+=$2} END{printf "%d", s/1024}'; } +echo "baseline: ProxyBridge RSS=$(( $(rss) / 1024 )) MB BPF maps=$(maps_kb) KB threads=$(ls /proc/$PB/task | wc -l)" +CPU0=$(awk '{print $14+$15}' /proc/$PB/stat) +rm -f /tmp/pb_scale_ready /tmp/pb_scale_go +python3 - $N > $WORK/client.out <<'PY' & +import asyncio, sys, time, resource, os +n = int(sys.argv[1]) +resource.setrlimit(resource.RLIMIT_NOFILE, (200000, 200000)) +async def main(): + t0 = time.time(); conns = []; fails = 0 + sem = asyncio.Semaphore(500) + async def one(i): + nonlocal fails + async with sem: + try: + r, w = await asyncio.wait_for(asyncio.open_connection("192.0.2.%d" % (1 + i % 250), 8888), 30) + w.write(b"x"); await w.drain() + conns.append((r, w)) + except Exception: + fails += 1 + await asyncio.gather(*(one(i) for i in range(n))) + dt = time.time() - t0 + print(f"client: {len(conns)} established, {fails} failed, {dt:.1f}s ({len(conns)/dt:.0f} conn/s)", flush=True) + open("/tmp/pb_scale_ready", "w").write("1") + while not os.path.exists("/tmp/pb_scale_go"): + await asyncio.sleep(0.2) + for r, w in conns: w.close() +asyncio.run(main()) +PY +CL=$! +for _ in $(seq 300); do [ -e /tmp/pb_scale_ready ] && break; sleep 1; done +sleep 2 +cat $WORK/client.out +CPU1=$(awk '{print $14+$15}' /proc/$PB/stat) +echo "under load: ProxyBridge RSS=$(( $(rss) / 1024 )) MB BPF maps=$(maps_kb) KB threads=$(ls /proc/$PB/task | wc -l) fds=$(ls /proc/$PB/fd | wc -l)" +echo "ProxyBridge CPU while opening: $(( (CPU1 - CPU0) * 1000 / $(getconf CLK_TCK) )) ms" +echo "kernel socket memory (TCP): $(grep '^TCP:' /proc/net/sockstat)" +touch /tmp/pb_scale_go; wait $CL 2>/dev/null; sleep 3 +echo "after close: ProxyBridge RSS=$(( $(rss) / 1024 )) MB fds=$(ls /proc/$PB/fd | wc -l)" +rm -f /tmp/pb_scale_ready /tmp/pb_scale_go diff --git a/Linux/tests/thrbench.c b/Linux/tests/thrbench.c new file mode 100644 index 00000000..b0334420 --- /dev/null +++ b/Linux/tests/thrbench.c @@ -0,0 +1,93 @@ +// Throughput benchmark: compares an application talking to a SOCKS5 proxy itself ("socks") against the same application being +// transparently redirected by ProxyBridge ("transparent"). The bundled proxy accepts any CONNECT and forwards to the local sink. +// thrbench proxy SOCKS5 (no auth) forwarder, splice() based +// thrbench sink discards data, replies with the byte count at EOF +// thrbench socks [conns] client doing the SOCKS5 handshake itself +// thrbench direct [conns] client connecting straight to the sink +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +static double now_s(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec / 1e9; } +static struct sockaddr_in addr_of(const char *ip, int port) { struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(port) }; inet_pton(AF_INET, ip, &a.sin_addr); return a; } +static int listener(int port) +{ + int s = socket(AF_INET, SOCK_STREAM, 0), one = 1; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in a = addr_of("127.0.0.1", port); + if (bind(s, (void *)&a, sizeof a) || listen(s, 1024)) { perror("listen"); exit(1); } + return s; +} +static int read_full(int fd, void *b, size_t n) { size_t o = 0; while (o < n) { ssize_t r = read(fd, (char *)b + o, n - o); if (r <= 0) return -1; o += (size_t)r; } return 0; } + +static void *sink_conn(void *a) +{ + int c = (int)(long)a; static __thread char b[1 << 20]; unsigned long long total = 0; ssize_t n; + while ((n = read(c, b, sizeof b)) > 0) total += (unsigned long long)n; + write(c, &total, sizeof total); close(c); return NULL; +} +static void run_sink(int port) { int l = listener(port); for (;;) { int c = accept(l, 0, 0); pthread_t t; pthread_create(&t, 0, sink_conn, (void *)(long)c); pthread_detach(t); } } + +struct pair { int a, b; }; +static void *pipe_dir(void *arg) +{ + struct pair *p = arg; int pp[2]; pipe(pp); fcntl(pp[0], F_SETPIPE_SZ, 1 << 20); + for (;;) { + ssize_t n = splice(p->a, 0, pp[1], 0, 1 << 20, SPLICE_F_MOVE); + if (n <= 0) break; + while (n > 0) { ssize_t w = splice(pp[0], 0, p->b, 0, (size_t)n, SPLICE_F_MOVE); if (w <= 0) goto out; n -= w; } + } +out: shutdown(p->b, SHUT_WR); close(pp[0]); close(pp[1]); return NULL; +} +static void *proxy_conn(void *arg) +{ + int c = (int)(long)arg; unsigned char b[512]; + if (read_full(c, b, 2) || read_full(c, b + 2, b[1])) goto done; + write(c, "\x05\x00", 2); + if (read_full(c, b, 4)) goto done; + if (b[3] == 1) read_full(c, b, 6); else if (b[3] == 4) read_full(c, b, 18); else { unsigned char l; read_full(c, &l, 1); read_full(c, b, l + 2u); } + extern int g_sink; int u = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in a = addr_of("127.0.0.1", g_sink); + if (connect(u, (void *)&a, sizeof a)) goto done; + write(c, "\x05\x00\x00\x01\x7f\x00\x00\x01\x00\x00", 10); + struct pair p1 = { c, u }, p2 = { u, c }; pthread_t t1, t2; + pthread_create(&t1, 0, pipe_dir, &p1); pthread_create(&t2, 0, pipe_dir, &p2); pthread_join(t1, 0); pthread_join(t2, 0); close(u); +done: close(c); return NULL; +} +int g_sink; +static void run_proxy(int port, int sink) { g_sink = sink; int l = listener(port); for (;;) { int c = accept(l, 0, 0); pthread_t t; pthread_create(&t, 0, proxy_conn, (void *)(long)c); pthread_detach(t); } } + +struct cl { const char *ip; int port, proxy_port; long mb; double secs; int ok; }; +static void *client(void *arg) +{ + struct cl *c = arg; int s = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in a = c->proxy_port ? addr_of("127.0.0.1", c->proxy_port) : addr_of(c->ip, c->port); + if (connect(s, (void *)&a, sizeof a)) { perror("connect"); return NULL; } + if (c->proxy_port) { // SOCKS5 CONNECT done by the application itself + unsigned char b[32] = { 5, 1, 0 }; write(s, b, 3); read_full(s, b, 2); + unsigned char req[10] = { 5, 1, 0, 1 }; inet_pton(AF_INET, c->ip, req + 4); req[8] = (unsigned char)(c->port >> 8); req[9] = (unsigned char)c->port; + write(s, req, 10); if (read_full(s, b, 10)) return NULL; + } + static __thread char buf[1 << 20]; memset(buf, 'x', sizeof buf); + double t0 = now_s(); + for (long i = 0; i < c->mb; i++) if (write(s, buf, sizeof buf) <= 0) return NULL; + shutdown(s, SHUT_WR); + unsigned long long total = 0; read_full(s, &total, sizeof total); + c->secs = now_s() - t0; c->ok = total == (unsigned long long)c->mb << 20; close(s); return NULL; +} +int main(int argc, char **argv) +{ + if (!strcmp(argv[1], "sink")) run_sink(atoi(argv[2])); + if (!strcmp(argv[1], "proxy")) run_proxy(atoi(argv[2]), atoi(argv[3])); + int socks = !strcmp(argv[1], "socks"), ai = socks ? 3 : 2; + int pp = socks ? atoi(argv[2]) : 0; const char *ip = argv[ai]; int port = atoi(argv[ai + 1]); long mb = atol(argv[ai + 2]); int n = argc > ai + 3 ? atoi(argv[ai + 3]) : 1; + struct cl *c = calloc((size_t)n, sizeof *c); pthread_t *t = calloc((size_t)n, sizeof *t); double t0 = now_s(); + for (int i = 0; i < n; i++) { c[i] = (struct cl){ ip, port, pp, mb }; pthread_create(&t[i], 0, client, &c[i]); } + int ok = 0; for (int i = 0; i < n; i++) { pthread_join(t[i], 0); ok += c[i].ok; } + double dt = now_s() - t0; printf("%-11s conns=%d ok=%d %ld MB each total %.0f MB in %.2fs = %.0f MB/s\n", argv[1], n, ok, mb, (double)n * (double)mb, dt, (double)n * (double)mb / dt); + return 0; +} diff --git a/Linux/tests/thrbench.sh b/Linux/tests/thrbench.sh new file mode 100755 index 00000000..e5d35bc3 --- /dev/null +++ b/Linux/tests/thrbench.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Throughput through ProxyBridge vs the same application speaking SOCKS5 itself. Needs root. +# sudo tests/thrbench.sh (binary must be named thrbench so process rules can match it) +TB=${1:?path to thrbench binary named "thrbench"}; ROOT="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")" +export LD_LIBRARY_PATH="$ROOT/src" +MB=${MB:-2048} +$TB sink 19990 & SINK=$!; $TB proxy 19991 19990 & PROXY=$! +trap 'kill $SINK $PROXY $PB 2>/dev/null' EXIT +sleep 0.5 +echo "== baselines (ProxyBridge not running)" +for i in 1 2 3; do $TB direct 127.0.0.1 19990 $MB 1; done +for i in 1 2 3; do $TB socks 19991 192.0.2.7 9999 $MB 1; done +for i in 1 2; do $TB socks 19991 192.0.2.7 9999 $((MB/8)) 8; done +stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy a=socks5://127.0.0.1:19991 --rule "thrbench:192.0.2.0/24:*:TCP:PROXY=a" > /tmp/thr_pb.out 2>&1 & PB=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" /tmp/thr_pb.out && break; sleep 0.25; done +echo "== through ProxyBridge (app connects to 192.0.2.7:9999, transparently redirected)" +for i in 1 2 3; do $TB direct 192.0.2.7 9999 $MB 1; done +for i in 1 2; do $TB direct 192.0.2.7 9999 $((MB/8)) 8; done +echo "== direct traffic while ProxyBridge is running (127.0.0.1 is never redirected; non-matching process)" +for i in 1 2 3; do $TB direct 127.0.0.1 19990 $MB 1; done +kill -INT $PB; wait $PB 2>/dev/null From d3f2becd611ec3226c160494d8be72c298fd305c Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:13:46 +0530 Subject: [PATCH 05/25] docs(linux): describe the eBPF design; update dependencies and ignores README: new architecture, domain rules and their limits, multiple proxies, requirements (kernel >= 5.8 with BTF and cgroup v2) and testing. setup/deploy: depend on libbpf instead of netfilter/iptables. .gitignore: Linux build artifacts and generated skeletons; keep test scripts. --- .gitignore | 20 +++ Linux/README.md | 373 +++++++++++++++++++----------------------------- Linux/deploy.sh | 18 +-- Linux/setup.sh | 20 +-- 4 files changed, 188 insertions(+), 243 deletions(-) diff --git a/.gitignore b/.gitignore index eb335bf3..0ebd9fb8 100644 --- a/.gitignore +++ b/.gitignore @@ -99,6 +99,26 @@ Linux/cli/main.o Linux/cli/proxybridge-cli Linux/src/libproxybridge.so Linux/src/ProxyBridge.o +Linux/src/*.o +Linux/src/proxybridge.skel.h +Linux/src/bpf/*.o +Linux/cli/ProxyBridge +Linux/gui/*.o +Linux/gui/ProxyBridgeGUI +Linux/ProxyBridgeGUI +Linux/src/tests/rules_test +Linux/src/tests/dns_test +Linux/**/*.o +Linux/**/*.skel.h +Linux/**/vmlinux.h +Linux/gui/tests/config_test +Linux/src/tests/relay_test +Linux/src/tests/relay_test_* +Linux/tests/hookbench +Linux/tests/thrbench +Linux/tests/api_live_test +Linux/**/__pycache__/ +!Linux/tests/*.py Android/** iOS/** # Windows build intermediates (MSVC import lib / export file / objects) diff --git a/Linux/README.md b/Linux/README.md index 11ef83f9..521a0620 100644 --- a/Linux/README.md +++ b/Linux/README.md @@ -34,7 +34,7 @@ curl -Lo deploy.sh https://raw.githubusercontent.com/InterceptSuite/ProxyBridge/ This script will: - Download the latest release from GitHub automatically - Detect your Linux distribution -- Install all required dependencies (libnetfilter-queue, iptables, GTK3) +- Install all required dependencies (libbpf, GTK3) - Install ProxyBridge CLI, GUI, and library to system paths - Update library cache (ldconfig) - Verify installation @@ -63,7 +63,7 @@ This script will: ``` The setup script will: -- Install runtime dependencies (libnetfilter-queue, iptables, GTK3) +- Install runtime dependencies (libbpf, GTK3) - Copy binaries to `/usr/local/bin` (ProxyBridge CLI and GUI) - Copy library to `/usr/local/lib` (libproxybridge.so) - Update library cache @@ -137,8 +137,16 @@ sudo ProxyBridge --proxy socks5://127.0.0.1:1080 \ sudo ProxyBridge --proxy socks5://192.168.1.4:4444 \ --rule "curl:3.19.110.0-3.19.115.255:*:TCP:PROXY" -# Cleanup after crash (removes iptables rules) -sudo ProxyBridge --cleanup +# Several proxies, each rule picks its own (name=type://[user:pass@]host:port) +sudo ProxyBridge \ + --proxy tor=socks5://127.0.0.1:9050 \ + --proxy burp=http://127.0.0.1:8080 \ + --rule "firefox:*:*:BOTH:PROXY=tor" \ + --rule "curl;wget:*:*:TCP:PROXY=burp" \ + --rule "*:10.0.0.0/8:*:BOTH:DIRECT" + +# IPv6 and CIDR targets +sudo ProxyBridge --rule "curl:2001:db8::/32;10.0.0.0/8:443:TCP:PROXY" ``` #### Command Line Options @@ -146,86 +154,75 @@ sudo ProxyBridge --cleanup ``` sudo ProxyBridge --help + ____ ____ _ _ | _ \ _ __ _____ ___ _ | __ ) _ __(_) __| | __ _ ___ | |_) | '__/ _ \ \/ / | | | | _ \| '__| |/ _` |/ _` |/ _ \ | __/| | | (_) > <| |_| | | |_) | | | | (_| | (_| | __/ |_| |_| \___/_/\_\\__, | |____/|_| |_|\__,_|\__, |\___| - |___/ |___/ V3.2.0 + |___/ |___/ V4.0 Universal proxy client for Linux applications - Author: Sourav Kalal/InterceptSuite - GitHub: https://github.com/InterceptSuite/ProxyBridge + Author: Sourav Kalal/InterceptSuite + GitHub: https://github.com/InterceptSuite/ProxyBridge USAGE: - ProxyBridge [OPTIONS] + cli/ProxyBridge [OPTIONS] OPTIONS: - --proxy Proxy server URL with optional authentication - Format: type://ip:port or type://ip:port:username:password + --proxy Add a proxy (repeatable). The first one is the default. + Format: [name=]type://[user:pass@]host:port Examples: socks5://127.0.0.1:1080 - http://proxy.com:8080:myuser:mypass + work=http://alice:secret@proxy.corp:8080 + tor=socks5://[::1]:9050 + Legacy form type://host:port:user:pass is still accepted. Default: socks5://127.0.0.1:4444 - --rule Traffic routing rule (can be specified multiple times) - Format: process:hosts:ports:protocol:action - process - Process name(s): curl, cur*, *, or multiple separated by ; - hosts - IP/host(s): *, google.com, 192.168.*.*, or multiple separated by ; or , - ports - Port(s): *, 443, 80;8080, 80-100, or multiple separated by ; or , + --rule Traffic routing rule (repeatable, first match wins) + Format: process:hosts:ports:protocol:action[=proxy] + process - name or path glob(s): curl, chrom*, *, or several separated by ; or , + hosts - IPv4/IPv6, CIDR (10.0.0.0/8), ranges (1.2.3.4-1.2.3.9), + octet patterns (192.168.*.*) or *; several separated by ; or , + ports - *, 443, 80;8080, 8000-9000 protocol - TCP, UDP, or BOTH - action - PROXY, DIRECT, or BLOCK + action - PROXY, DIRECT, or BLOCK; PROXY=name (or PROXY=2) picks a specific proxy Examples: curl:*:*:TCP:PROXY - curl;wget:*:*:TCP:PROXY - *:*:53:UDP:PROXY - firefox:*:80;443:TCP:DIRECT + firefox:*:*:BOTH:PROXY=work + curl;wget:10.0.0.0/8:*:TCP:DIRECT + *:*:53:UDP:PROXY=tor - --dns-via-proxy Route DNS queries through proxy - Values: true, false, 1, 0 - Default: true + --dns-via-proxy Route DNS queries through the proxy (true/false/1/0, default true) - --verbose Logging verbosity level - 0 - No logs (default) - 1 - Show log messages only - 2 - Show connection events only - 3 - Show both logs and connections + --verbose 0 none (default), 1 logs, 2 connections, 3 both - --cleanup Cleanup resources (iptables, etc.) from crashed instance - Use if ProxyBridge crashed without proper cleanup + --cleanup No-op, kept for compatibility: the eBPF programs detach automatically --help, -h Show this help message EXAMPLES: - # Basic usage with default proxy - sudo ProxyBridge --rule curl:*:*:TCP:PROXY - - # Multiple rules with custom proxy - sudo ProxyBridge --proxy socks5://192.168.1.10:1080 \ - --rule curl:*:*:TCP:PROXY \ - --rule wget:*:*:TCP:PROXY \ - --verbose 2 + sudo cli/ProxyBridge --rule curl:*:*:TCP:PROXY - # Route DNS through proxy with multiple apps - sudo ProxyBridge --proxy socks5://127.0.0.1:1080 \ - --rule "curl;wget;firefox:*:*:BOTH:PROXY" \ - --dns-via-proxy true --verbose 3 + sudo cli/ProxyBridge --proxy socks5://192.168.1.10:1080 --proxy work=http://proxy.corp:8080 \ + --rule firefox:*:*:BOTH:PROXY --rule curl:*:*:TCP:PROXY=work --verbose 2 NOTE: - ProxyBridge requires root privileges to use nfqueue. - Run with 'sudo' or as root user. - + ProxyBridge needs root and a Linux kernel >= 5.8 with BTF and cgroup v2 (eBPF datapath). + Credentials given on the command line are visible to other users via ps. ``` #### Rule Format -**Format:** `process:hosts:ports:protocol:action` +**Format:** `process:hosts:ports:protocol:action[=proxy]` -- **process** - Process name(s): `curl`, `curl;wget;firefox`, `fire*`, or `*` -- **hosts** - Target IP/hostname(s): `*`, `192.168.1.1`, `192.168.*.*`, `10.10.1.1-10.10.255.255`, or `192.168.1.1;10.10.10.10` -- **ports** - Target port(s): `*`, `443`, `80;443;8080`, `80-8000`, or `80;443;8000-9000` +- **process** - Name or path glob(s): `curl`, `curl;wget;firefox`, `fire*`, `*chrome*`, `/usr/bin/*`, or `*`. Matched against the executable's file name and full path, case-sensitive +- **hosts** - `*`, IPv4/IPv6 addresses, CIDR (`10.0.0.0/8`, `2001:db8::/32`), ranges (`10.10.1.1-10.10.255.255`), octet patterns (`192.168.*.*`, `10.1-5.*.*`), **domain names** (`github.com`, `*.github.com`, `api?.example.org`), or a list mixing any of these separated by `;` or `,`. A plain domain matches exactly that name; use `*.` for subdomains. See [Domain rules](#domain-rules) +- **ports** - `*`, `443`, `80;443;8080`, `80-8000`, or `80;443;8000-9000` - **protocol** - `TCP`, `UDP`, or `BOTH` -- **action** - `PROXY`, `DIRECT`, or `BLOCK` +- **action** - `PROXY`, `DIRECT`, or `BLOCK`. `PROXY=name` (or `PROXY=2`, the 1-based position of a `--proxy`) uses a specific proxy; plain `PROXY` uses the first one + +Rules are evaluated **in order and the first match wins** (put specific rules before general ones). **Examples:** ```bash @@ -239,10 +236,19 @@ NOTE: --rule "fire*:*:*:TCP:PROXY" # Matches firefox, firebird, etc. # Target specific IPs and ports ---rule "curl:192.168.*;10.10.*.*:80;443;8080:TCP:PROXY" +--rule "curl:192.168.*.*;10.10.*.*:80;443;8080:TCP:PROXY" + +# Specific proxy per rule +--rule "firefox:*:*:BOTH:PROXY=tor" -# IP range matching +# Domain names (exact and wildcard), each rule with its own proxy +--rule "firefox:*.github.com;github.com:443:TCP:PROXY=work" +--rule "*:*.internal.corp:*:BOTH:PROXY=burp" + +# IP range / CIDR / IPv6 --rule "curl:3.19.110.0-3.19.115.255:*:TCP:PROXY" +--rule "curl:10.0.0.0/8:*:TCP:DIRECT" +--rule "curl:2001:db8::/32:443:TCP:PROXY" # Allow direct connection (bypass proxy) --rule "burpsuite:*:*:TCP:DIRECT" @@ -250,9 +256,30 @@ NOTE: **Notes:** - Process names are case-sensitive on Linux -- Use `*` as the process name to set a default action for all traffic -- Press `Ctrl+C` to stop ProxyBridge -- On crash, use `--cleanup` flag to remove iptables rules +- Interpreted programs (python, node, java) are identified by the interpreter binary, e.g. `python3.12` +- A `PROXY` rule whose proxy does not exist is treated as `BLOCK` (traffic never leaks around a missing proxy) +- Press `Ctrl+C` to stop ProxyBridge; the kernel hooks are removed automatically, even if ProxyBridge crashes + +## Domain rules + +Applications resolve a name themselves and then connect to an IP address, so the kernel only ever sees the address. To apply domain rules ProxyBridge builds, like the macOS version, an **IP → domain-name map from the DNS answers the application received**: + +1. When at least one enabled rule contains a domain pattern, the DNS traffic (UDP and TCP, port 53) of every process covered by such a rule is routed through ProxyBridge's relay, which forwards it to the original resolver unchanged (directly, or through a proxy if your rules say so) and reads the answers. +2. A and CNAME records are recorded (`www.shop.test -> cdn.edge.net -> 198.51.100.7` makes the address match rules for **either** name). The mapping is installed in the kernel **before** the answer is delivered to the application, so even the very first connection already matches. +3. Names are also loaded from `/etc/hosts` at start. +4. The map is bounded (32,768 addresses, oldest evicted first). It has no TTL: an address keeps its names until it is evicted, so applications that cache DNS answers keep matching. + +Cost: DNS from covered processes takes one extra loopback hop (about +90 µs per query measured on a loopback test server); processes not covered by any domain rule are not touched at all. + +**What domain rules cannot match** (the application never reveals the name to the system): +- DNS-over-HTTPS / DNS-over-TLS (Firefox "Secure DNS", Chrome "Secure DNS", systemd-resolved with DoT): the answers are encrypted. Turn them off in the application or use IP rules +- Addresses that are hard-coded or taken from a config file instead of being resolved +- IPv6 destinations (`AAAA` records are not mapped yet) +- Shared hosting: if two names share one IP, whichever name was resolved last matches; a rule for the other name matches too + +## Multiple proxies + +Add as many `--proxy` entries (CLI) or proxies (GUI) as you need; each rule chooses its own with `PROXY=name`. Proxy hosts may be IP addresses or **host names** (resolved with a 5 s deadline and a cache, so a slow resolver cannot stall traffic). A rule that references a proxy that no longer exists blocks instead of leaking. ## Use Cases @@ -266,29 +293,23 @@ NOTE: ## Current Limitations -- **IPv4 only** (IPv6 not supported) -- **WSL (Windows Subsystem for Linux) NOT supported** - Neither WSL1 nor WSL2 work with ProxyBridge: - - **WSL2**: Kernel does not support `nfnetlink_queue` module - - Extension shows as "builtin" but is non-functional at runtime - - NFQUEUE handle creation fails - - **WSL1**: Uses a translation layer instead of a real Linux kernel - - Does not support Netfilter/iptables properly - - NFQUEUE is completely unavailable - - **Works on:** Native Linux, VirtualBox VMs, VMware, cloud instances (AWS, GCP, Azure), bare-metal servers - - **Does NOT work on:** WSL1, WSL2, Docker containers with limited capabilities - - **Windows users:** Use the Windows version of ProxyBridge instead - it's specifically designed for Windows and works perfectly -- **Root privileges required** - NFQUEUE and iptables require root access -- **GUI requires GTK3** - Command-line interface works without GUI dependencies +- **Needs a recent kernel**: Linux 5.8+ with BTF (`/sys/kernel/btf/vmlinux`) and cgroup v2 mounted (default on current Ubuntu, Debian, Fedora, Arch). Older kernels are not supported +- **Root privileges required** - loading the eBPF programs needs root (CAP_BPF/CAP_SYS_ADMIN) +- **Native IPv6 UDP to a PROXY rule fails closed** - an unconnected UDP socket sending to a native IPv6 address cannot be redirected (kernel limitation); the `sendto()` returns `EPERM` and applications fall back to IPv4/TCP. Connected IPv6 UDP sockets, IPv6 TCP and UDP to IPv4 destinations through dual-stack sockets all work +- **IPv4 is the fully supported path**; IPv6 (TCP, connected UDP) works for IP rules but has no domain matching and the limits above +- **IPv6-only (`IPV6_V6ONLY`) sockets** connecting to a proxied destination fail (the redirect uses a v4-mapped loopback address) +- **Domain rules only see names the application resolved through DNS that ProxyBridge could observe** - see [Domain rules](#domain-rules) for what that means (DoH/DoT and hard-coded IPs are not matched) +- **Only new connections are affected** - connections that were already open when a rule was added/changed keep their route until they reconnect +- **WSL is not supported** - WSL1/WSL2 kernels do not ship BTF/cgroup-BPF. Use native Linux, VMs, cloud instances or bare metal +- **GUI requires GTK3** - the command-line interface works without GUI dependencies ## Things to Note -- **DNS Traffic Handling**: DNS traffic on TCP and UDP port 53 is handled separately from proxy rules. Even if you configure rules for port 53, they will be ignored. Instead, DNS routing is controlled by the `--dns-via-proxy` flag (enabled by default). When enabled, all DNS queries are routed through the proxy; when disabled, DNS queries use direct connection. - +- **DNS Traffic Handling**: with `--dns-via-proxy true` (default) DNS (port 53) follows your rules like any other traffic; with `false`, DNS always uses a direct connection regardless of rules. - **Automatic Direct Routing**: Certain IP addresses and ports automatically use direct connection regardless of proxy rules: - - **Broadcast addresses** (255.255.255.255 and x.x.x.255) - Network broadcast - - **Multicast addresses** (224.0.0.0 - 239.255.255.255) - Group communication - - **APIPA addresses** (169.254.0.0/16) - Automatic Private IP Addressing (link-local) + - **Loopback** (127.0.0.0/8, ::1) and **0.0.0.0/8** + - **Multicast / broadcast / reserved** (224.0.0.0 and above, ff00::/8) and IPv6 link-local (fe80::/10) - **DHCP ports** (UDP 67, 68) - Dynamic Host Configuration Protocol These addresses and ports are used by system components, network discovery, and essential Linux services. @@ -302,9 +323,8 @@ NOTE: - **Testing UDP/HTTP3/DTLS Support**: If you need to test UDP, HTTP/3, and DTLS support with a SOCKS5 proxy, try [Nexus Proxy](https://github.com/InterceptSuite/nexus-proxy) - a proxy application created specifically to test ProxyBridge with advanced UDP protocols. - **Root Privileges**: ProxyBridge requires root access to: - - Create NFQUEUE handles for packet interception - - Add/remove iptables rules in mangle and nat tables - - Listen on relay ports (34010 for TCP, 34011 for UDP) + - Load the eBPF programs and attach them to the root cgroup + - Listen on its local relay ports (24010 for TCP, 24011 for UDP; the next free pair is used if they are taken) Always run with `sudo` or as root user. @@ -315,135 +335,45 @@ NOTE: ## How It Works -ProxyBridge uses Linux Netfilter NFQUEUE to intercept TCP/UDP packets and applies user-defined rules to route traffic through proxies. - -**Case 1: Packet does not match any rules** +ProxyBridge hooks the **socket calls** (`connect()`, `sendmsg()`, `recvmsg()`) with eBPF programs attached to the root cgroup. It never inspects or queues packets, so the TCP data path of every application is untouched. ``` -Application → TCP/UDP Packet → NFQUEUE → ProxyBridge - ↓ - [No Match or DIRECT] - ↓ - Packet Verdict: ACCEPT - ↓ - Direct Connection → Internet + application kernel (eBPF, cgroup sock_addr hooks) ProxyBridge (userspace) + ----------- -------------------------------------- ----------------------- + connect(dst) ───────► which executable is this? ─► which rules apply? + no rule / DIRECT ─────► unchanged (zero extra cost on the data path) + BLOCK ─────► connect() fails with EPERM + PROXY(id) ─────► destination rewritten to 127.0.0.1:relay, + original destination saved ──────────────► relay reads it, + talks SOCKS5 / HTTP CONNECT + cannot decide ─────► sent to the relay as "decide" ───────────► full rule engine decides + (and teaches the kernel) ``` -**Case 2: Packet matches proxy rule** - -``` -Application → TCP/UDP Packet → NFQUEUE → ProxyBridge - ↓ - [PROXY Rule Match] - ↓ - Packet Verdict: ACCEPT + Mark - ↓ - iptables NAT REDIRECT - ↓ - Relay Server (34010/34011) ← Packet - ↓ - [Store Original Destination] - ↓ - SOCKS5/HTTP Protocol Conversion - ↓ - Proxy Server (Burp Suite/InterceptSuite) - ↓ - Forward to Original Destination - ↓ - Internet - ↓ - Response Returns - ↓ - Relay Server - ↓ - [Restore Original Source IP/Port] - ↓ - Application Receives Response -``` - -**Detailed Traffic Flow:** - -1. **Applications Generate Traffic** - User-mode applications (curl, wget, firefox, games) create TCP/UDP packets +**Kernel side** ([src/bpf/proxybridge.bpf.c](src/bpf/proxybridge.bpf.c)) +- Identifies the calling process by its executable's `(device, inode)` and looks up a precompiled **class** (the ordered list of rules whose process pattern matches that binary). Rules are compiled to bounded map lookups: port ranges, IPv4 ranges and protocol are evaluated in the kernel; anything it cannot express (octet patterns, IPv6 patterns, many ranges) is handled by the relay +- TCP: rewrites the destination to the local relay and passes the original destination via a `sockops` hook keyed by the client's source port +- UDP: encodes the original destination as a loopback **flow address** (`127.x.y.z`); the relay replies from that address and a `recvmsg` hook restores the real source, so one socket can talk to many destinations +- `getpeername()` on redirected sockets returns the original destination +- Memory is bounded: fixed-size maps (about a few MB in total), LRU maps for per-flow state -2. **Kernel Interception** - iptables rules in the mangle table send packets to NFQUEUE: - ```bash - iptables -t mangle -A OUTPUT -p tcp -j NFQUEUE --queue-num 0 - iptables -t mangle -A OUTPUT -p udp -j NFQUEUE --queue-num 0 - ``` - -3. **NFQUEUE Delivery** - libnetfilter_queue delivers packets to ProxyBridge in userspace +**Userspace side** ([src/pb_relay.c](src/pb_relay.c), [src/pb_rules.c](src/pb_rules.c), [src/pb_bpf.c](src/pb_bpf.c)) +- Rule engine: validation, globs, IPv4/IPv6, CIDR, ranges, first-match-wins; the reference semantics for everything the kernel cannot decide +- epoll based relay with bounded resources: SOCKS5 (with user/pass) and HTTP CONNECT for TCP, SOCKS5 UDP ASSOCIATE for UDP, direct forwarding for the slow path, idle timeouts, connection cap +- Several proxies can be configured; every rule chooses its own +- Binaries the kernel has not seen yet take a one-time slow path through the relay; the result is cached in the kernel so later connections skip it -4. **Rule Evaluation** - ProxyBridge inspects each packet and applies configured rules: - - **BLOCK** → Packet verdict: DROP (no network access) - - **DIRECT** → Packet verdict: ACCEPT (direct connection) - - **NO MATCH** → Packet verdict: ACCEPT (direct connection) - - **PROXY** → Packet verdict: ACCEPT + set mark (1 for TCP, 2 for UDP) - -5. **NAT Redirection** - For PROXY-matched packets, iptables NAT rules redirect marked packets: - ```bash - iptables -t nat -A OUTPUT -p tcp -m mark --mark 1 -j REDIRECT --to-port 34010 - iptables -t nat -A OUTPUT -p udp -m mark --mark 2 -j REDIRECT --to-port 34011 - ``` +**Why this design:** an earlier eBPF attempt (3.1.0) evaluated rules per packet in the kernel and was abandoned for memory and latency reasons. This design avoids both problems: decisions are made once per connection (not per packet), rules are compiled into small bounded maps, and everything the kernel cannot express goes to userspace once per connection. -6. **Relay Servers** - Local relay servers (34010 for TCP, 34011 for UDP): - - Intercept redirected packets using getsockopt(SO_ORIGINAL_DST) - - Store original destination IP and port - - Convert raw TCP/UDP to SOCKS5/HTTP proxy protocol - - Perform proxy authentication if configured - - Forward to configured proxy server - -7. **Proxy Forwarding** - Proxy server (Burp Suite/InterceptSuite) forwards traffic to the original destination - -8. **Response Handling** - Return traffic flows back through relay servers, which restore original source IP/port - -**Key Technical Points:** - -- **NFQUEUE vs WinDivert**: Linux uses Netfilter NFQUEUE (kernel feature) instead of WinDivert (Windows kernel driver) - -- **Why NFQUEUE Instead of eBPF**: While eBPF is the modern approach for Linux networking tasks and offers better performance, ProxyBridge uses NFQUEUE due to fundamental limitations discovered during development: - - **Original Plan**: ProxyBridge 3.1.0 for Linux was initially developed using eBPF after weeks of implementation - - **eBPF Memory Limitations**: eBPF provides limited memory space, which proved insufficient for ProxyBridge's feature set: - - ProxyBridge supports multiple complex proxy rules with wildcard matching, IP ranges, and process patterns - - Storing and evaluating these rules within eBPF's memory constraints was not feasible - - Alternative workarounds added excessive latency (200-500ms+ per packet) - - **Performance Requirements**: ProxyBridge's core design goals couldn't be met with eBPF: - - Work efficiently with minimal memory usage under high load - - Handle high traffic volumes (10,000+ concurrent connections) - - **Network speed impact must be ≤2-5% for proxied traffic only** - - **Zero performance impact on direct (non-proxied) traffic** - - eBPF implementation with all required features caused 15-30% slowdown on all traffic - - **NFQUEUE Advantages**: - - Userspace processing allows unlimited memory for complex rule evaluation - - Selective packet inspection - only examines packets, doesn't slow down uninspected traffic - - Mature, stable kernel interface available on all Linux distributions - - Lower latency than eBPF workarounds when handling complex rule sets - - **Verdict**: NFQUEUE provides the right balance of flexibility, performance, and compatibility for ProxyBridge's requirements. While eBPF excels for simple packet filtering, ProxyBridge's advanced rule engine and protocol conversion needs are better served by NFQUEUE's userspace processing model. - -- **Packet Marking**: ProxyBridge marks packets (mark=1 for TCP, mark=2 for UDP) instead of modifying destination -- **iptables Integration**: Uses mangle table (pre-routing processing) + nat table (port redirection) -- **Transparent Redirection**: Applications remain completely unaware of proxying -- **SO_ORIGINAL_DST**: Socket option retrieves original destination after NAT redirect -- **Multi-threaded**: Separate threads for packet processing, TCP relay, UDP relay, and connection cleanup - -**Architecture Advantages:** - -- No kernel modules required (uses built-in Netfilter) -- Works on any Linux distribution with iptables and NFQUEUE support -- Leverages proven kernel infrastructure (Netfilter/iptables) -- Separate packet marking prevents packet modification in NFQUEUE -- Clean separation: NFQUEUE for inspection, iptables for redirection +**Cleanup:** the programs are attached through BPF links, so the kernel removes them automatically when ProxyBridge exits or crashes. There are no iptables rules or system settings to restore. ## Build from Source ### Requirements -- Linux kernel with NFQUEUE support (virtually all modern distributions) -- GCC compiler -- Make +- Linux kernel 5.8+ with BTF and cgroup v2 (to run); GCC, Make, **clang** and **bpftool** (to build the eBPF object and skeleton) - Development libraries: - - libnetfilter-queue-dev (Debian/Ubuntu) or libnetfilter_queue-devel (Fedora/RHEL) - - libnfnetlink-dev (Debian/Ubuntu) or libnfnetlink-devel (Fedora/RHEL) + - libbpf-dev (Debian/Ubuntu) or libbpf-devel (Fedora/RHEL) - libgtk-3-dev (optional, for GUI) or gtk3-devel (Fedora/RHEL) - pkg-config @@ -454,23 +384,20 @@ Application → TCP/UDP Packet → NFQUEUE → ProxyBridge **Debian/Ubuntu/Mint:** ```bash sudo apt-get update - sudo apt-get install build-essential gcc make \ - libnetfilter-queue-dev libnfnetlink-dev \ - libgtk-3-dev pkg-config + sudo apt-get install build-essential gcc make clang bpftool \ + libbpf-dev libgtk-3-dev pkg-config ``` **Fedora:** ```bash - sudo dnf install gcc make \ - libnetfilter_queue-devel libnfnetlink-devel \ - gtk3-devel pkg-config + sudo dnf install gcc make clang bpftool \ + libbpf-devel gtk3-devel pkg-config ``` **Arch/Manjaro:** ```bash - sudo pacman -S base-devel gcc make \ - libnetfilter_queue libnfnetlink \ - gtk3 pkg-config + sudo pacman -S base-devel gcc make clang bpf \ + libbpf gtk3 pkg-config ``` 2. **Clone or download ProxyBridge source code:** @@ -515,15 +442,8 @@ After successful build, binaries will be in the `output/` directory: **Library:** ```bash cd src -gcc -Wall -Wextra -O3 -fPIC -D_GNU_SOURCE \ - -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE \ - -Wformat -Wformat-security -Werror=format-security \ - -fno-strict-overflow -fno-delete-null-pointer-checks -fwrapv \ - -c ProxyBridge.c -o ProxyBridge.o - -gcc -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \ - -o libproxybridge.so ProxyBridge.o \ - -lpthread -lnetfilter_queue -lnfnetlink +make # builds bpf/proxybridge.bpf.o, proxybridge.skel.h and libproxybridge.so +make test-rules # parser/matcher unit tests (ASan + UBSan) ``` **CLI:** @@ -541,6 +461,8 @@ gcc -o ProxyBridge main.o \ -lproxybridge -lpthread ``` +**Config location:** the GUI stores profiles, proxies and rules in `/etc/proxybridge/config.ini` (mode 0600). Set `PROXYBRIDGE_CONFIG_DIR` to use another directory (used by the tests). + ## Uninstallation To remove ProxyBridge from your system: @@ -564,20 +486,7 @@ sudo rm -f /etc/ld.so.conf.d/proxybridge.conf sudo ldconfig ``` -**Cleanup after crash:** - -If ProxyBridge crashed and left iptables rules: -```bash -sudo ProxyBridge --cleanup -``` - -Or manually remove iptables rules: -```bash -sudo iptables -t mangle -D OUTPUT -p tcp -j NFQUEUE --queue-num 0 -sudo iptables -t mangle -D OUTPUT -p udp -j NFQUEUE --queue-num 0 -sudo iptables -t nat -D OUTPUT -p tcp -m mark --mark 1 -j REDIRECT --to-port 34010 -sudo iptables -t nat -D OUTPUT -p udp -m mark --mark 2 -j REDIRECT --to-port 34011 -``` +**Cleanup after crash:** nothing to do. ProxyBridge attaches its eBPF programs through BPF links, which the kernel removes when the process exits or crashes. To check: `sudo bpftool cgroup tree /sys/fs/cgroup` should list no `pb_*` programs. ## License @@ -588,3 +497,19 @@ MIT License - See LICENSE file for details **Author:** Sourav Kalal / InterceptSuite **GitHub:** https://github.com/InterceptSuite/ProxyBridge **Documentation:** https://github.com/InterceptSuite/ProxyBridge/tree/master/Linux + +## Testing + +```bash +cd Linux/src && make && cd ../cli && make # build +cd ../src && make test-rules # rule engine + DNS parser tests (no root needed) +cd tests && ./run_relay_test.sh both # relay tests, plain and ASan/UBSan (no root needed) +cd ../../tests && sudo ./e2e.sh # end-to-end with real kernel hooks (root) +sudo ./e2e_domains.sh # domain rules with a real resolver client (root, needs dig) +sudo ./e2e_rules.sh # every rule form + every proxy type, incl. HTTP auth and UDP rules (root) +sudo ./api_live.sh # library API: edit/move/disable/delete rules and proxies on the live datapath (root) +sudo ./e2e_openssh.sh # interoperability with a real third-party SOCKS5 server (OpenSSH ssh -D), 40 MB each way (root) +sudo ./scale.sh 10000 # 10,000 concurrent proxied connections (root) +sudo ./gui_headless.sh # the real GTK GUI under the Broadway backend (root, needs broadwayd) +cd ../gui/tests && gcc -I../../src -I.. -D_GNU_SOURCE $(pkg-config --cflags gtk+-3.0) config_test.c ../gui_config.c -o /tmp/config_test $(pkg-config --libs gtk+-3.0) && /tmp/config_test # GUI config/profile tests +``` diff --git a/Linux/deploy.sh b/Linux/deploy.sh index d9dd9299..a9c64d35 100644 --- a/Linux/deploy.sh +++ b/Linux/deploy.sh @@ -150,35 +150,35 @@ install_dependencies() { ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian|mx|antix|deepin|lmde) echo "Using apt package manager..." apt-get update -qq - apt-get install -y libnetfilter-queue1 libnfnetlink0 iptables libgtk-3-0 + apt-get install -y libbpf1 libgtk-3-0 ;; fedora) echo "Using dnf package manager..." - dnf install -y libnetfilter_queue libnfnetlink iptables gtk3 + dnf install -y libbpf gtk3 ;; rhel|centos|rocky|almalinux) echo "Using yum package manager..." - yum install -y libnetfilter_queue libnfnetlink iptables gtk3 + yum install -y libbpf gtk3 ;; arch|manjaro|endeavouros|garuda) echo "Using pacman package manager..." - pacman -Sy --noconfirm libnetfilter_queue libnfnetlink iptables gtk3 + pacman -Sy --noconfirm libbpf gtk3 ;; opensuse*|sles) echo "Using zypper package manager..." - zypper install -y libnetfilter_queue1 libnfnetlink0 iptables gtk3 + zypper install -y libbpf1 gtk3 ;; void) echo "Using xbps package manager..." - xbps-install -Sy libnetfilter_queue libnfnetlink iptables gtk3 + xbps-install -Sy libbpf gtk3 ;; *) echo "WARNING: Unknown distribution '$DISTRO' (family: '$DISTRO_LIKE')" echo "" echo "Please manually install the following packages:" - echo " Debian/Ubuntu: sudo apt install libnetfilter-queue1 libnfnetlink0 iptables libgtk-3-0" - echo " Fedora: sudo dnf install libnetfilter_queue libnfnetlink iptables gtk3" - echo " Arch: sudo pacman -S libnetfilter_queue libnfnetlink iptables gtk3" + echo " Debian/Ubuntu: sudo apt install libbpf1 libgtk-3-0" + echo " Fedora: sudo dnf install libbpf gtk3" + echo " Arch: sudo pacman -S libbpf gtk3" echo "" read -p "Continue anyway? (y/n) " -n 1 -r echo diff --git a/Linux/setup.sh b/Linux/setup.sh index a365a16d..55a6ac44 100644 --- a/Linux/setup.sh +++ b/Linux/setup.sh @@ -53,36 +53,36 @@ install_dependencies() { ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian|mx|antix|deepin|lmde) echo "Using apt package manager..." apt-get update -qq - # libgtk-3-0 is for runtime, libnetfilter-queue1 for functionality - apt-get install -y libnetfilter-queue1 libnfnetlink0 iptables libgtk-3-0 + # libgtk-3-0 is for the GUI, libbpf for the eBPF datapath + apt-get install -y libbpf1 libgtk-3-0 ;; fedora) echo "Using dnf package manager..." - dnf install -y libnetfilter_queue libnfnetlink iptables gtk3 + dnf install -y libbpf gtk3 ;; rhel|centos|rocky|almalinux) echo "Using yum package manager..." - yum install -y libnetfilter_queue libnfnetlink iptables gtk3 + yum install -y libbpf gtk3 ;; arch|manjaro|endeavouros|garuda) echo "Using pacman package manager..." - pacman -Sy --noconfirm libnetfilter_queue libnfnetlink iptables gtk3 + pacman -Sy --noconfirm libbpf gtk3 ;; opensuse*|sles) echo "Using zypper package manager..." - zypper install -y libnetfilter_queue1 libnfnetlink0 iptables gtk3 + zypper install -y libbpf1 gtk3 ;; void) echo "Using xbps package manager..." - xbps-install -Sy libnetfilter_queue libnfnetlink iptables gtk3 + xbps-install -Sy libbpf gtk3 ;; *) echo "WARNING: Unknown distribution '$DISTRO' (family: '$DISTRO_LIKE')" echo "" echo "Please manually install the following packages:" - echo " Debian/Ubuntu: sudo apt install libnetfilter-queue1 libnfnetlink0 iptables libgtk-3-0" - echo " Fedora: sudo dnf install libnetfilter_queue libnfnetlink iptables gtk3" - echo " Arch: sudo pacman -S libnetfilter_queue libnfnetlink iptables gtk3" + echo " Debian/Ubuntu: sudo apt install libbpf1 libgtk-3-0" + echo " Fedora: sudo dnf install libbpf gtk3" + echo " Arch: sudo pacman -S libbpf gtk3" echo "" read -p "Continue anyway? (y/n) " -n 1 -r echo From ec50a2075b40895b31eaebbc6927c34c7bae04e3 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:22:27 +0530 Subject: [PATCH 06/25] build(linux): build out of tree so only output/ receives final files build.sh now compiles in a temporary directory and copies just libproxybridge.so, ProxyBridge and ProxyBridgeGUI into output/. The source tree no longer collects .o files, the generated eBPF skeleton or libraries, even when a build fails. The Makefiles take BUILD_DIR/LIBDIR for this; a plain `make` still builds in place for development. - check for gcc, make, clang, bpftool and libbpf with install hints - OUTPUT_DIR can be overridden; a clear error if output/ cannot be removed - fix the GUI rpath: $ORIGIN was expanded to an empty string by the shell - test scripts use the built binaries from output/ (override with PB_DIR) --- .gitignore | 2 + Linux/README.md | 6 +- Linux/build.sh | 125 +++++++++++++++++-------------------- Linux/cli/Makefile | 16 +++-- Linux/src/Makefile | 32 ++++++---- Linux/tests/api_live.sh | 6 +- Linux/tests/dnsbench.sh | 8 ++- Linux/tests/e2e.sh | 6 +- Linux/tests/e2e_domains.sh | 6 +- Linux/tests/e2e_openssh.sh | 8 ++- Linux/tests/e2e_rules.sh | 6 +- Linux/tests/hookbench.sh | 6 +- Linux/tests/scale.sh | 6 +- Linux/tests/thrbench.sh | 6 +- 14 files changed, 132 insertions(+), 107 deletions(-) diff --git a/.gitignore b/.gitignore index 0ebd9fb8..f3c5aea6 100644 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,8 @@ Linux/gui/*.o Linux/gui/ProxyBridgeGUI Linux/ProxyBridgeGUI Linux/src/tests/rules_test +Linux/src/dns_test +Linux/src/rules_test Linux/src/tests/dns_test Linux/**/*.o Linux/**/*.skel.h diff --git a/Linux/README.md b/Linux/README.md index 521a0620..7137e1d2 100644 --- a/Linux/README.md +++ b/Linux/README.md @@ -501,10 +501,10 @@ MIT License - See LICENSE file for details ## Testing ```bash -cd Linux/src && make && cd ../cli && make # build -cd ../src && make test-rules # rule engine + DNS parser tests (no root needed) +cd Linux && ./build.sh # builds in a temp dir, final files land in Linux/output/ +cd src && make test-rules BUILD_DIR=/tmp/pbt # rule engine + DNS parser tests (no root needed) cd tests && ./run_relay_test.sh both # relay tests, plain and ASan/UBSan (no root needed) -cd ../../tests && sudo ./e2e.sh # end-to-end with real kernel hooks (root) +cd ../../tests && sudo ./e2e.sh # end-to-end with real kernel hooks (root); uses Linux/output, override with PB_DIR= sudo ./e2e_domains.sh # domain rules with a real resolver client (root, needs dig) sudo ./e2e_rules.sh # every rule form + every proxy type, incl. HTTP auth and UDP rules (root) sudo ./api_live.sh # library API: edit/move/disable/delete rules and proxies on the live datapath (root) diff --git a/Linux/build.sh b/Linux/build.sh index 6d5f49bd..a3e694e2 100755 --- a/Linux/build.sh +++ b/Linux/build.sh @@ -1,101 +1,90 @@ #!/bin/bash - -set -e # Exit on error +# Builds libproxybridge.so, the CLI and (if GTK3 is present) the GUI. +# Everything is compiled in a temporary directory; only the final binaries are copied to output/, +# so the source tree never collects .o files, generated headers or libraries. +# +# ./build.sh -> Linux/output/ +# OUTPUT_DIR=/some/dir ./build.sh +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -OUTPUT_DIR="$SCRIPT_DIR/output" +OUTPUT_DIR="${OUTPUT_DIR:-$SCRIPT_DIR/output}" +BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/proxybridge-build.XXXXXX")" +trap 'rm -rf "$BUILD_DIR"' EXIT -# Remove and recreate output directory -if [ -d "$OUTPUT_DIR" ]; then - echo "Removing existing output directory..." - rm -rf "$OUTPUT_DIR" +need() { # command, install hint + command -v "$1" >/dev/null 2>&1 || { echo "ERROR: '$1' not found. $2"; exit 1; } +} +need gcc "Install: sudo apt install build-essential | sudo dnf install gcc" +need make "Install: sudo apt install make | sudo dnf install make" +need clang "Needed to compile the eBPF program. Install: sudo apt install clang | sudo dnf install clang" +need bpftool "Needed to generate the eBPF skeleton. Install: sudo apt install bpftool | sudo dnf install bpftool" +if [ ! -f /usr/include/bpf/libbpf.h ] && ! pkg-config --exists libbpf 2>/dev/null; then + echo "ERROR: libbpf development files not found. Install: sudo apt install libbpf-dev | sudo dnf install libbpf-devel" + exit 1 fi -echo "Creating output directory..." +# Fresh output directory +if [ -e "$OUTPUT_DIR" ]; then + echo "Removing existing output directory..." + rm -rf "$OUTPUT_DIR" 2>/dev/null || { + echo "ERROR: cannot remove $OUTPUT_DIR (files owned by another user, e.g. root)." + echo " Remove it manually (sudo rm -rf '$OUTPUT_DIR') or choose another one with OUTPUT_DIR=..." + exit 1 + } +fi mkdir -p "$OUTPUT_DIR" echo "" -# Build library +# Library echo "=== Building Library ===" -cd "$SCRIPT_DIR/src" -make clean 2>/dev/null || true -make - -if [ -f "libproxybridge.so" ]; then - echo "Library build successful" -else - echo "Library build failed!" - exit 1 -fi +make -C "$SCRIPT_DIR/src" BUILD_DIR="$BUILD_DIR/lib" +[ -f "$BUILD_DIR/lib/libproxybridge.so" ] || { echo "Library build failed!"; exit 1; } +echo "Library build successful" echo "" -# Build CLI (library must stay for linking) +# CLI echo "=== Building CLI ===" -cd "$SCRIPT_DIR/cli" -make clean 2>/dev/null || true -make - -if [ -f "ProxyBridge" ]; then - echo "CLI build successful" -else - echo "CLI build failed!" - exit 1 -fi +make -C "$SCRIPT_DIR/cli" BUILD_DIR="$BUILD_DIR/cli" LIBDIR="$BUILD_DIR/lib" +[ -f "$BUILD_DIR/cli/ProxyBridge" ] || { echo "CLI build failed!"; exit 1; } +echo "CLI build successful" echo "" -# Move binaries to output +# GUI (optional) echo "=== Building GUI ===" -cd "$SCRIPT_DIR" -rm -f ProxyBridgeGUI +GUI_BUILT=0 if pkg-config --exists gtk+-3.0; then - GUI_CFLAGS="-Wall -Wno-unused-parameter -O3 -Isrc -D_GNU_SOURCE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Wformat-security -Werror=format-security -fno-strict-overflow -fno-delete-null-pointer-checks -fwrapv $(pkg-config --cflags gtk+-3.0)" - GUI_LDFLAGS="-Lsrc -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s -Wl,-rpath,'$ORIGIN/.' -lproxybridge -lpthread $(pkg-config --libs gtk+-3.0) -export-dynamic" - - # Compile all GUI source files - GUI_OBJS="" - for src in gui/*.c; do - obj="${src%.c}.o" - gcc $GUI_CFLAGS -c "$src" -o "$obj" - GUI_OBJS="$GUI_OBJS $obj" + mkdir -p "$BUILD_DIR/gui" + GUI_CFLAGS=(-Wall -Wno-unused-parameter -O3 "-I$SCRIPT_DIR/src" -D_GNU_SOURCE -fstack-protector-strong + -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Wformat-security -Werror=format-security -fno-strict-overflow + -fno-delete-null-pointer-checks -fwrapv $(pkg-config --cflags gtk+-3.0)) + GUI_OBJS=() + for src in "$SCRIPT_DIR"/gui/*.c; do + obj="$BUILD_DIR/gui/$(basename "${src%.c}").o" + gcc "${GUI_CFLAGS[@]}" -c "$src" -o "$obj" + GUI_OBJS+=("$obj") done - - gcc -o ProxyBridgeGUI $GUI_OBJS $GUI_LDFLAGS - - rm -f gui/*.o + gcc -o "$BUILD_DIR/gui/ProxyBridgeGUI" "${GUI_OBJS[@]}" -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \ + "-L$BUILD_DIR/lib" -Wl,-rpath,'$ORIGIN' -lproxybridge -lpthread $(pkg-config --libs gtk+-3.0) -export-dynamic + GUI_BUILT=1 echo "GUI build successful" else echo "GTK3 not found. Skipping GUI build." echo " Install with: sudo apt install libgtk-3-dev (Debian/Ubuntu/Mint)" echo " sudo dnf install gtk3-devel (Fedora)" fi - -echo "" -echo "Moving binaries to output directory..." -mv "$SCRIPT_DIR/src/libproxybridge.so" "$OUTPUT_DIR/" -mv "$SCRIPT_DIR/cli/ProxyBridge" "$OUTPUT_DIR/" -if [ -f ProxyBridgeGUI ]; then - mv ProxyBridgeGUI "$OUTPUT_DIR/" -fi -echo "Binaries moved to output" echo "" -# Cleanup build files -echo "Cleaning up build artifacts..." -cd "$SCRIPT_DIR/src" -rm -f *.o -make clean 2>/dev/null || true -cd "$SCRIPT_DIR/cli" -rm -f *.o -make clean 2>/dev/null || true -echo "Cleanup complete" +# Only the final files go to output/ +echo "Copying binaries to $OUTPUT_DIR ..." +cp "$BUILD_DIR/lib/libproxybridge.so" "$OUTPUT_DIR/" +cp "$BUILD_DIR/cli/ProxyBridge" "$OUTPUT_DIR/" +[ "$GUI_BUILT" = 1 ] && cp "$BUILD_DIR/gui/ProxyBridgeGUI" "$OUTPUT_DIR/" echo "" -# Show results echo "===================================" echo "Build Complete!" echo "===================================" -cd "$OUTPUT_DIR" -ls -lh +ls -lh "$OUTPUT_DIR" echo "" echo "Output location: $OUTPUT_DIR" - diff --git a/Linux/cli/Makefile b/Linux/cli/Makefile index 6aff0ac9..0d9464e3 100644 --- a/Linux/cli/Makefile +++ b/Linux/cli/Makefile @@ -1,4 +1,9 @@ CC = gcc + +# BUILD_DIR / LIBDIR: where generated files go and where libproxybridge.so was built (build.sh sets both). +BUILD_DIR ?= . +LIBDIR ?= ../src + CFLAGS = -Wall -Wextra -O3 -I../src \ -fstack-protector-strong \ -D_FORTIFY_SOURCE=2 \ @@ -8,22 +13,25 @@ CFLAGS = -Wall -Wextra -O3 -I../src \ -fno-strict-overflow \ -fno-delete-null-pointer-checks \ -fwrapv -LDFLAGS = -L../src -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \ +LDFLAGS = -L$(LIBDIR) -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \ -Wl,-rpath,/usr/local/lib LIBS = -lproxybridge -lpthread -TARGET = ProxyBridge +TARGET = $(BUILD_DIR)/ProxyBridge SOURCES = main.c -OBJECTS = $(SOURCES:.c=.o) +OBJECTS = $(addprefix $(BUILD_DIR)/,$(SOURCES:.c=.o)) all: $(TARGET) +$(BUILD_DIR): + mkdir -p $@ + $(TARGET): $(OBJECTS) $(CC) -o $@ $^ $(LDFLAGS) $(LIBS) @echo "Stripping debug symbols..." strip --strip-all $@ -%.o: %.c +$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR) $(CC) $(CFLAGS) -c $< -o $@ clean: diff --git a/Linux/src/Makefile b/Linux/src/Makefile index 677f8b88..6c1b4826 100644 --- a/Linux/src/Makefile +++ b/Linux/src/Makefile @@ -3,7 +3,11 @@ CLANG ?= clang BPFTOOL ?= bpftool MULTIARCH := $(shell $(CC) -print-multiarch) -CFLAGS = -Wall -Wextra -O2 -fPIC -D_GNU_SOURCE \ +# BUILD_DIR keeps every generated file (objects, BPF object, skeleton, library) out of the source tree. +# build.sh points it at a temporary directory; a plain `make` builds in place for quick development. +BUILD_DIR ?= . + +CFLAGS = -Wall -Wextra -O2 -fPIC -D_GNU_SOURCE -I$(BUILD_DIR) \ -fstack-protector-strong \ -D_FORTIFY_SOURCE=2 \ -fPIE \ @@ -15,34 +19,38 @@ CFLAGS = -Wall -Wextra -O2 -fPIC -D_GNU_SOURCE \ LDFLAGS = -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s LIBS = -lpthread -lbpf -TARGET = libproxybridge.so SOURCES = ProxyBridge.c pb_rules.c pb_dns.c pb_bpf.c pb_relay.c -OBJECTS = $(SOURCES:.c=.o) -BPF_OBJ = bpf/proxybridge.bpf.o -SKEL = proxybridge.skel.h +OBJECTS = $(addprefix $(BUILD_DIR)/,$(SOURCES:.c=.o)) +TARGET = $(BUILD_DIR)/libproxybridge.so +BPF_OBJ = $(BUILD_DIR)/proxybridge.bpf.o +SKEL = $(BUILD_DIR)/proxybridge.skel.h all: $(TARGET) -$(BPF_OBJ): bpf/proxybridge.bpf.c bpf/pb_bpf_shared.h +$(BUILD_DIR): + mkdir -p $@ + +$(BPF_OBJ): bpf/proxybridge.bpf.c bpf/pb_bpf_shared.h | $(BUILD_DIR) $(CLANG) -O2 -g -target bpf -D__BPF__ -I/usr/include/$(MULTIARCH) -c $< -o $@ $(SKEL): $(BPF_OBJ) $(BPFTOOL) gen skeleton $< > $@ -pb_bpf.o: $(SKEL) +$(BUILD_DIR)/pb_bpf.o: $(SKEL) $(TARGET): $(OBJECTS) $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) -%.o: %.c pb_internal.h ProxyBridge.h +$(BUILD_DIR)/%.o: %.c pb_internal.h ProxyBridge.h | $(BUILD_DIR) $(CC) $(CFLAGS) -c $< -o $@ -test-rules: tests/rules_test.c tests/dns_test.c pb_rules.c pb_rules.h pb_dns.c pb_dns.h - $(CC) -Wall -Wextra -g -fsanitize=address,undefined -D_GNU_SOURCE tests/rules_test.c pb_rules.c -o tests/rules_test && tests/rules_test - $(CC) -Wall -Wextra -g -fsanitize=address,undefined -D_GNU_SOURCE tests/dns_test.c pb_dns.c -o tests/dns_test && tests/dns_test +# Unit tests (no root needed); binaries go to BUILD_DIR as well. +test-rules: tests/rules_test.c tests/dns_test.c pb_rules.c pb_rules.h pb_dns.c pb_dns.h | $(BUILD_DIR) + $(CC) -Wall -Wextra -g -fsanitize=address,undefined -D_GNU_SOURCE tests/rules_test.c pb_rules.c -o $(BUILD_DIR)/rules_test && $(BUILD_DIR)/rules_test + $(CC) -Wall -Wextra -g -fsanitize=address,undefined -D_GNU_SOURCE tests/dns_test.c pb_dns.c -o $(BUILD_DIR)/dns_test && $(BUILD_DIR)/dns_test clean: - rm -f $(OBJECTS) $(TARGET) $(BPF_OBJ) $(SKEL) tests/rules_test tests/dns_test + rm -f $(OBJECTS) $(TARGET) $(BPF_OBJ) $(SKEL) $(BUILD_DIR)/rules_test $(BUILD_DIR)/dns_test install: $(TARGET) install -m 0755 $(TARGET) /usr/local/lib/ diff --git a/Linux/tests/api_live.sh b/Linux/tests/api_live.sh index 6c7b9b8c..45f71b64 100755 --- a/Linux/tests/api_live.sh +++ b/Linux/tests/api_live.sh @@ -1,8 +1,10 @@ #!/bin/bash # Library API against the live datapath. Needs root, curl, python3, gcc. sudo tests/api_live.sh HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } WORK=$(mktemp -d); trap 'kill $FAKE 2>/dev/null; wait 2>/dev/null; rm -rf $WORK' EXIT -gcc -Wall -Wextra -O1 -g -I"$ROOT/src" -o "$WORK/api_live_test" "$HERE/api_live_test.c" -L"$ROOT/src" -lproxybridge -lpthread || exit 1 +gcc -Wall -Wextra -O1 -g -I"$ROOT/src" -o "$WORK/api_live_test" "$HERE/api_live_test.c" -L"$PB_DIR" -lproxybridge -lpthread || exit 1 python3 "$HERE/fake_proxies.py" "$WORK/log" >/dev/null 2>&1 & FAKE=$! sleep 1 -LD_LIBRARY_PATH="$ROOT/src" "$WORK/api_live_test" +LD_LIBRARY_PATH="$PB_DIR" "$WORK/api_live_test" diff --git a/Linux/tests/dnsbench.sh b/Linux/tests/dnsbench.sh index ffe6f812..c39ec013 100755 --- a/Linux/tests/dnsbench.sh +++ b/Linux/tests/dnsbench.sh @@ -3,7 +3,9 @@ # sudo tests/dnsbench.sh [queries] set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")"; N=${1:-20000} -export LD_LIBRARY_PATH="$ROOT/src"; DNSIP=10.77.0.3 +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } +export LD_LIBRARY_PATH="$PB_DIR"; DNSIP=10.77.0.3 WORK=$(mktemp -d); trap 'kill $DNS $PB 2>/dev/null; wait 2>/dev/null; ip addr del $DNSIP/32 dev lo 2>/dev/null; rm -rf $WORK' EXIT ip addr add $DNSIP/32 dev lo 2>/dev/null python3 "$HERE/fake_dns.py" $DNSIP $WORK/dns.log >/dev/null 2>&1 & DNS=$! @@ -22,10 +24,10 @@ print(" %d queries: %.0f qps avg=%.0fus p50=%.0fus p99=%.0fus" % (n, n / dt, s PY } echo "== no ProxyBridge"; bench; bench -stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "*:*.example.test:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! +stdbuf -oL "$PB_DIR/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "*:*.example.test:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! for _ in $(seq 40); do grep -q "ProxyBridge started" $WORK/pb.out && break; sleep 0.25; done echo "== ProxyBridge running, python3 covered by a domain rule (every query goes through the DNS tap)"; bench; bench kill -INT $PB; wait $PB 2>/dev/null -stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "someother:*.example.test:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! +stdbuf -oL "$PB_DIR/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "someother:*.example.test:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! for _ in $(seq 40); do grep -q "ProxyBridge started" $WORK/pb.out && break; sleep 0.25; done echo "== ProxyBridge running, python3 NOT covered (DNS untouched by the tap)"; bench; bench diff --git a/Linux/tests/e2e.sh b/Linux/tests/e2e.sh index a0fe0d1b..9106ca50 100755 --- a/Linux/tests/e2e.sh +++ b/Linux/tests/e2e.sh @@ -4,6 +4,8 @@ set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(dirname "$HERE")" +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } WORK="$(mktemp -d)" LOG="$WORK/proxy.log"; : > "$LOG" PASS=0; FAIL=0 @@ -16,7 +18,7 @@ cleanup() { kill "$PB_PID" "$FAKE_PID" "$DIRECT_PID" 2>/dev/null; wait 2>/dev/nu trap cleanup EXIT [ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } -export LD_LIBRARY_PATH="$ROOT/src" +export LD_LIBRARY_PATH="$PB_DIR" python3 "$HERE/fake_proxies.py" "$LOG" > "$WORK/fake.out" 2>&1 & FAKE_PID=$! # a direct (non-proxied) origin on the host's own LAN address, so DIRECT rules can be seen to work @@ -27,7 +29,7 @@ sleep 1 cp "$(command -v curl)" "$WORK/newcurl" # a binary that did not exist when ProxyBridge started -stdbuf -oL "$ROOT/cli/ProxyBridge" --verbose 3 \ +stdbuf -oL "$PB_DIR/ProxyBridge" --verbose 3 \ --proxy a=socks5://127.0.0.1:19080 --proxy b=socks5://bob:pw@127.0.0.1:19081 --proxy c=http://127.0.0.1:19082 \ --rule "curl:$MYIP/32:18399:TCP:DIRECT" \ --rule "curl:192.0.2.0/24:*:TCP:PROXY=a" \ diff --git a/Linux/tests/e2e_domains.sh b/Linux/tests/e2e_domains.sh index 60cdadbd..6f8c4c98 100755 --- a/Linux/tests/e2e_domains.sh +++ b/Linux/tests/e2e_domains.sh @@ -3,6 +3,8 @@ # sudo Linux/tests/e2e_domains.sh set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } WORK="$(mktemp -d)"; PLOG="$WORK/proxy.log"; DLOG="$WORK/dns.log"; : > "$PLOG"; : > "$DLOG" PASS=0; FAIL=0; DNSIP=10.77.0.2 ok() { echo " PASS $1"; PASS=$((PASS+1)); } @@ -11,13 +13,13 @@ has() { [[ "$2" == *"$3"* ]] && ok "$1" || bad "$1 (got: ${2:0:160})"; } cleanup() { kill "$PB_PID" "$FAKE_PID" "$DNS_PID" 2>/dev/null; wait 2>/dev/null; ip addr del $DNSIP/32 dev lo 2>/dev/null; rm -rf "$WORK"; } trap cleanup EXIT [ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } -export LD_LIBRARY_PATH="$ROOT/src" +export LD_LIBRARY_PATH="$PB_DIR" ip addr add $DNSIP/32 dev lo 2>/dev/null python3 "$HERE/fake_proxies.py" "$PLOG" > /dev/null 2>&1 & FAKE_PID=$! python3 "$HERE/fake_dns.py" $DNSIP "$DLOG" > /dev/null 2>&1 & DNS_PID=$! sleep 1 -stdbuf -oL "$ROOT/cli/ProxyBridge" --verbose 2 \ +stdbuf -oL "$PB_DIR/ProxyBridge" --verbose 2 \ --proxy a=socks5://127.0.0.1:19080 --proxy b=socks5://bob:pw@127.0.0.1:19081 --proxy h=socks5://localhost:19080 \ --rule "*:blocked.example.test:*:TCP:BLOCK" \ --rule "*:*.example.test:*:TCP:PROXY=a" \ diff --git a/Linux/tests/e2e_openssh.sh b/Linux/tests/e2e_openssh.sh index 6124e9b0..d371da80 100755 --- a/Linux/tests/e2e_openssh.sh +++ b/Linux/tests/e2e_openssh.sh @@ -3,13 +3,15 @@ # sudo tests/e2e_openssh.sh set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } WORK="$(mktemp -d)"; PASS=0; FAIL=0; ORIGIN=10.77.0.5 ok() { echo " PASS $1"; PASS=$((PASS+1)); } bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); } cleanup() { kill $SSHD $SSH $WWW $PB 2>/dev/null; wait 2>/dev/null; ip addr del $ORIGIN/32 dev lo 2>/dev/null; rm -rf "$WORK"; } trap cleanup EXIT [ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } -export LD_LIBRARY_PATH="$ROOT/src" +export LD_LIBRARY_PATH="$PB_DIR" command -v sshd >/dev/null && command -v ssh >/dev/null || { echo "SKIP: openssh not installed"; exit 0; } ip addr add $ORIGIN/32 dev lo 2>/dev/null @@ -41,7 +43,7 @@ python3 -m http.server 18500 --bind $ORIGIN --directory "$WORK" >/dev/null 2>&1 sleep 1 SUM=$(sha256sum "$WORK/big.bin" | cut -d' ' -f1) -stdbuf -oL "$ROOT/cli/ProxyBridge" --verbose 1 --proxy ossh=socks5://127.0.0.1:19090 \ +stdbuf -oL "$PB_DIR/ProxyBridge" --verbose 1 --proxy ossh=socks5://127.0.0.1:19090 \ --rule "curl:$ORIGIN:18500:TCP:PROXY=ossh" > "$WORK/pb.out" 2>&1 & PB=$! for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && break; sleep 0.25; done @@ -66,7 +68,7 @@ socketserver.ThreadingTCPServer(("10.77.0.5", 18501), H).serve_forever() PY UP=$!; sleep 1 kill -INT $PB; wait $PB 2>/dev/null -stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy ossh=socks5://127.0.0.1:19090 --rule "curl:$ORIGIN:18500-18501:TCP:PROXY=ossh" > "$WORK/pb.out" 2>&1 & PB=$! +stdbuf -oL "$PB_DIR/ProxyBridge" --proxy ossh=socks5://127.0.0.1:19090 --rule "curl:$ORIGIN:18500-18501:TCP:PROXY=ossh" > "$WORK/pb.out" 2>&1 & PB=$! for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && break; sleep 0.25; done GOT=$(curl -s -m 60 --data-binary @"$WORK/big.bin" http://$ORIGIN:18501/ ) [ "$GOT" = "$SUM" ] && ok "40 MB upload through OpenSSH SOCKS5: origin received identical bytes" || bad "upload checksum mismatch (got ${GOT:0:20})" diff --git a/Linux/tests/e2e_rules.sh b/Linux/tests/e2e_rules.sh index da0f700a..38a9ebfa 100755 --- a/Linux/tests/e2e_rules.sh +++ b/Linux/tests/e2e_rules.sh @@ -3,6 +3,8 @@ # sudo tests/e2e_rules.sh (needs curl, python3; only loopback + TEST-NET addresses are used) set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } WORK="$(mktemp -d)"; PLOG="$WORK/proxy.log"; : > "$PLOG" PASS=0; FAIL=0 ok() { echo " PASS $1"; PASS=$((PASS+1)); } @@ -17,7 +19,7 @@ blocked() { local out; out=$(curl -s -m 5 "$2" 2>&1; echo "rc=$?"); [[ "$out" == cleanup() { kill $PB_PID $FAKE_PID 2>/dev/null; wait 2>/dev/null; rm -rf "$WORK"; } trap cleanup EXIT [ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } -export LD_LIBRARY_PATH="$ROOT/src" +export LD_LIBRARY_PATH="$PB_DIR" python3 "$HERE/fake_proxies.py" "$PLOG" >/dev/null 2>&1 & FAKE_PID=$! mkdir -p "$WORK/bin" "$WORK/pbpath" @@ -26,7 +28,7 @@ cp "$CURL" "$WORK/bin/wget"; cp "$CURL" "$WORK/bin/newtool"; cp "$CURL" "$WORK/b sleep 1 start_pb() { # extra CLI args... - stdbuf -oL "$ROOT/cli/ProxyBridge" --verbose 1 \ + stdbuf -oL "$PB_DIR/ProxyBridge" --verbose 1 \ --proxy a=socks5://127.0.0.1:19080 --proxy b=socks5://bob:pw@127.0.0.1:19081 --proxy c=http://127.0.0.1:19082 \ --proxy d=http://bob:pw@127.0.0.1:19083 --proxy e=http://bob:wrong@127.0.0.1:19083 "$@" > "$WORK/pb.out" 2>&1 & PB_PID=$! for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && return 0; sleep 0.25; done diff --git a/Linux/tests/hookbench.sh b/Linux/tests/hookbench.sh index ea3affcf..9bcb12c4 100755 --- a/Linux/tests/hookbench.sh +++ b/Linux/tests/hookbench.sh @@ -1,13 +1,15 @@ #!/bin/bash # Overhead of the eBPF hooks on DIRECT traffic. Needs root. sudo tests/hookbench.sh HB=${1:?path to hookbench}; ROOT="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")" -export LD_LIBRARY_PATH="$ROOT/src" +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } +export LD_LIBRARY_PATH="$PB_DIR" IP=10.77.0.1 # alias on lo: local like loopback, but not caught by the implicit-loopback bypass ip addr add $IP/32 dev lo 2>/dev/null trap 'ip addr del $IP/32 dev lo 2>/dev/null' EXIT run() { for i in 1 2 3; do $HB tcp $IP 19555 20000; done; for i in 1 2 3; do $HB udp $IP 19556 1000000; done; } echo "### A: ProxyBridge not running"; run -start() { stdbuf -oL "$ROOT/cli/ProxyBridge" "$@" > /tmp/pbbench.out 2>&1 & PB=$!; for _ in $(seq 40); do grep -q "ProxyBridge started" /tmp/pbbench.out && break; sleep 0.25; done; } +start() { stdbuf -oL "$PB_DIR/ProxyBridge" "$@" > /tmp/pbbench.out 2>&1 & PB=$!; for _ in $(seq 40); do grep -q "ProxyBridge started" /tmp/pbbench.out && break; sleep 0.25; done; } echo "### B: running, 20 rules for other programs (this binary: empty class -> immediate direct)" start $(for i in $(seq 20); do echo --rule "someapp$i:*:*:BOTH:PROXY"; done); run; kill -INT $PB; wait $PB 2>/dev/null echo "### C: running, rules that match this binary but not this destination (full rule evaluation, then direct)" diff --git a/Linux/tests/scale.sh b/Linux/tests/scale.sh index 55a5cc04..16ef4d09 100755 --- a/Linux/tests/scale.sh +++ b/Linux/tests/scale.sh @@ -4,12 +4,14 @@ set -u N=${1:-10000} HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" -export LD_LIBRARY_PATH="$ROOT/src" +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } +export LD_LIBRARY_PATH="$PB_DIR" ulimit -n 200000 WORK=$(mktemp -d); trap 'kill $FAKE $PB 2>/dev/null; wait 2>/dev/null; rm -rf $WORK' EXIT python3 "$HERE/fake_proxies.py" $WORK/log >/dev/null 2>&1 & FAKE=$! sleep 1 -stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "python3*:192.0.2.0/24:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! +stdbuf -oL "$PB_DIR/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "python3*:192.0.2.0/24:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! for _ in $(seq 40); do grep -q "ProxyBridge started" $WORK/pb.out && break; sleep 0.25; done rss() { awk '/VmRSS/{print $2}' /proc/$PB/status; } maps_kb() { bpftool map show 2>/dev/null | grep -oE 'memlock [0-9]+' | awk '{s+=$2} END{printf "%d", s/1024}'; } diff --git a/Linux/tests/thrbench.sh b/Linux/tests/thrbench.sh index e5d35bc3..37fd38d9 100755 --- a/Linux/tests/thrbench.sh +++ b/Linux/tests/thrbench.sh @@ -2,7 +2,9 @@ # Throughput through ProxyBridge vs the same application speaking SOCKS5 itself. Needs root. # sudo tests/thrbench.sh (binary must be named thrbench so process rules can match it) TB=${1:?path to thrbench binary named "thrbench"}; ROOT="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")" -export LD_LIBRARY_PATH="$ROOT/src" +PB_DIR="${PB_DIR:-$ROOT/output}" # directory holding the built ProxyBridge + libproxybridge.so (run ../build.sh first) +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR to a directory containing ProxyBridge and libproxybridge.so"; exit 1; } +export LD_LIBRARY_PATH="$PB_DIR" MB=${MB:-2048} $TB sink 19990 & SINK=$!; $TB proxy 19991 19990 & PROXY=$! trap 'kill $SINK $PROXY $PB 2>/dev/null' EXIT @@ -11,7 +13,7 @@ echo "== baselines (ProxyBridge not running)" for i in 1 2 3; do $TB direct 127.0.0.1 19990 $MB 1; done for i in 1 2 3; do $TB socks 19991 192.0.2.7 9999 $MB 1; done for i in 1 2; do $TB socks 19991 192.0.2.7 9999 $((MB/8)) 8; done -stdbuf -oL "$ROOT/cli/ProxyBridge" --proxy a=socks5://127.0.0.1:19991 --rule "thrbench:192.0.2.0/24:*:TCP:PROXY=a" > /tmp/thr_pb.out 2>&1 & PB=$! +stdbuf -oL "$PB_DIR/ProxyBridge" --proxy a=socks5://127.0.0.1:19991 --rule "thrbench:192.0.2.0/24:*:TCP:PROXY=a" > /tmp/thr_pb.out 2>&1 & PB=$! for _ in $(seq 40); do grep -q "ProxyBridge started" /tmp/thr_pb.out && break; sleep 0.25; done echo "== through ProxyBridge (app connects to 192.0.2.7:9999, transparently redirected)" for i in 1 2 3; do $TB direct 192.0.2.7 9999 $MB 1; done From aef07087c930ecd2ca809325f521359998cefd70 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:22:27 +0530 Subject: [PATCH 07/25] ci(linux): install clang, bpftool and libbpf; run the unit tests The Linux workflows still installed libnetfilter-queue and libnfnetlink. The eBPF build needs clang (BPF object), bpftool (skeleton) and libbpf-dev. The build workflow now also runs the rule engine, DNS parser and relay unit tests, which need no root. --- .github/workflows/build-linux.yml | 23 +++++++++++++++++++---- .github/workflows/release-linux.yml | 5 +++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 1086979d..6e966425 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -28,8 +28,9 @@ jobs: build-essential \ gcc \ make \ - libnetfilter-queue-dev \ - libnfnetlink-dev \ + clang \ + bpftool \ + libbpf-dev \ libgtk-3-dev \ pkg-config shell: bash @@ -48,8 +49,22 @@ jobs: echo "=== Checking GTK3 ===" pkg-config --modversion gtk+-3.0 echo "" - echo "=== Checking libnetfilter_queue ===" - pkg-config --modversion libnetfilter_queue || echo "Package info not available, but headers should be present" + echo "=== Checking clang (eBPF compiler) ===" + clang --version | head -1 + echo "" + echo "=== Checking bpftool (skeleton generator) ===" + bpftool version | head -1 + echo "" + echo "=== Checking libbpf ===" + pkg-config --modversion libbpf + shell: bash + + - name: Unit tests (rule engine, DNS parser, relay) + run: | + cd Linux/src + make test-rules BUILD_DIR="$RUNNER_TEMP/pb-tests" + cd tests + TMPDIR="$RUNNER_TEMP" ./run_relay_test.sh plain shell: bash - name: Build project diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index ef925f66..fcdcf212 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -27,8 +27,9 @@ jobs: build-essential \ gcc \ make \ - libnetfilter-queue-dev \ - libnfnetlink-dev \ + clang \ + bpftool \ + libbpf-dev \ libgtk-3-dev \ pkg-config shell: bash From f4e7daf48fef69a0dad180007a7956351860fa64 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:27:35 +0530 Subject: [PATCH 08/25] ci(linux): build the Linux-ebpf branch and verify the new build output - run on push and pull requests for Linux-ebpf as well as master and dev - the GUI is now required to build (GTK3 is installed on the runner) - smoke test: CLI help, library linkage, GUI $ORIGIN runpath, exported public API, and no libnetfilter dependency - fail if the build leaves untracked or ignored files outside Linux/output - best-effort end-to-end job (e2e, e2e_rules, e2e_domains, api_live) against the real kernel datapath; reported but non-blocking since it needs a BTF kernel with cgroup v2 and root --- .github/workflows/build-linux.yml | 60 +++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 6e966425..00d04845 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -5,9 +5,9 @@ permissions: on: push: - branches: [ master, dev ] + branches: [ master, dev, Linux-ebpf ] pull_request: - branches: [ master, dev ] + branches: [ master, dev, Linux-ebpf ] workflow_dispatch: jobs: @@ -99,10 +99,64 @@ jobs: echo "✓ GUI built successfully" file Linux/output/ProxyBridgeGUI else - echo "⚠ GUI build skipped (GTK3 not available or build failed)" + echo "✗ GUI build failed (GTK3 is installed on this runner)" + exit 1 fi shell: bash + - name: Smoke test the built binaries + run: | + cd Linux/output + # capture output first: `cmd | grep -q` can fail under pipefail when grep exits early (SIGPIPE) + help_out="$(LD_LIBRARY_PATH="$PWD" ./ProxyBridge --help)" + grep -q -- "--proxy" <<<"$help_out" && echo "✓ CLI runs and shows its help" + cli_deps="$(LD_LIBRARY_PATH="$PWD" ldd ./ProxyBridge)" + grep -q libproxybridge <<<"$cli_deps" && echo "✓ CLI links libproxybridge.so" + # the GUI must carry an $ORIGIN runpath so it finds the library in its own directory + gui_dyn="$(readelf -d ./ProxyBridgeGUI)" + grep -q 'RUNPATH.*\$ORIGIN' <<<"$gui_dyn" && echo "✓ GUI runpath is \$ORIGIN" + # the library must export the public API + exports="$(nm -D libproxybridge.so)" + for sym in ProxyBridge_Start ProxyBridge_Stop ProxyBridge_AddRule ProxyBridge_AddProxy ProxyBridge_ValidateRule; do + grep -q " T $sym" <<<"$exports" || { echo "✗ missing export $sym"; exit 1; } + done + echo "✓ public API exported" + # no iptables/NFQUEUE dependency may come back + lib_deps="$(ldd libproxybridge.so)" + if grep -qi netfilter <<<"$lib_deps"; then echo "✗ libnetfilter dependency is back"; exit 1; fi + echo "✓ no libnetfilter dependency" + shell: bash + + - name: Verify the build left the source tree clean + run: | + # build.sh compiles in a temp directory: no untracked or ignored file may appear outside Linux/output/ + status="$(git status --porcelain --ignored)" + dirty="$(grep -E '^(\?\?|!!)' <<<"$status" | grep -v 'Linux/output' || true)" + if [ -n "$dirty" ]; then + echo "✗ build left files outside Linux/output:" + echo "$dirty" + exit 1 + fi + echo "✓ source tree clean (only Linux/output/ was written)" + shell: bash + + - name: End-to-end tests with the real kernel datapath (best effort) + # Needs root, cgroup v2 and a BTF kernel. GitHub-hosted runners usually have them, but this is not guaranteed, + # so a failure here is reported without failing the build. + continue-on-error: true + run: | + sudo apt-get install -y dnsutils >/dev/null + uname -r + ls /sys/kernel/btf/vmlinux && mount | grep -q "type cgroup2" || { echo "kernel lacks BTF or cgroup2: skipping"; exit 0; } + fail=0 + for t in e2e e2e_rules e2e_domains api_live; do + echo "::group::$t" + sudo env PB_DIR="$PWD/Linux/output" bash "Linux/tests/$t.sh" || fail=1 + echo "::endgroup::" + done + exit $fail + shell: bash + - name: Upload artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: From 9a366b0a6ff9a47c9328e5baab205a680b704b54 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:41:27 +0530 Subject: [PATCH 09/25] ci(linux): get a real bpftool on Ubuntu runners On Ubuntu 24.04 "bpftool" is a virtual package, so `apt install bpftool` fails. Install linux-tools-common/linux-tools-generic and locate the real binary under /usr/lib/linux-tools, exporting it as BPFTOOL. build.sh and the end-to-end scripts honour $BPFTOOL, so a bpftool that is not on PATH works; the error message now suggests the right package per distro. --- .github/workflows/build-linux.yml | 18 +++++++++++++++--- .github/workflows/release-linux.yml | 14 +++++++++++++- Linux/build.sh | 4 +++- Linux/tests/e2e.sh | 2 +- Linux/tests/e2e_domains.sh | 2 +- Linux/tests/gui_headless.sh | 6 +++--- Linux/tests/scale.sh | 2 +- 7 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 00d04845..1e3eb81d 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -29,12 +29,24 @@ jobs: gcc \ make \ clang \ - bpftool \ + linux-tools-common \ + linux-tools-generic \ libbpf-dev \ libgtk-3-dev \ pkg-config shell: bash + - name: Locate bpftool + # On Ubuntu "bpftool" is only a virtual package; the real binary ships in linux-tools-* and is not on PATH + # for the runner's cloud kernel, so find it explicitly. + run: | + shopt -s nullglob + candidates=(/usr/lib/linux-tools/*/bpftool) + [ ${#candidates[@]} -gt 0 ] || { echo "bpftool binary not found"; ls /usr/lib/linux-tools 2>&1 || true; exit 1; } + echo "BPFTOOL=${candidates[0]}" >> "$GITHUB_ENV" + "${candidates[0]}" version | head -1 + shell: bash + - name: Verify dependencies run: | echo "=== Checking GCC ===" @@ -53,7 +65,7 @@ jobs: clang --version | head -1 echo "" echo "=== Checking bpftool (skeleton generator) ===" - bpftool version | head -1 + "$BPFTOOL" version | head -1 echo "" echo "=== Checking libbpf ===" pkg-config --modversion libbpf @@ -151,7 +163,7 @@ jobs: fail=0 for t in e2e e2e_rules e2e_domains api_live; do echo "::group::$t" - sudo env PB_DIR="$PWD/Linux/output" bash "Linux/tests/$t.sh" || fail=1 + sudo env PB_DIR="$PWD/Linux/output" BPFTOOL="$BPFTOOL" bash "Linux/tests/$t.sh" || fail=1 echo "::endgroup::" done exit $fail diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index fcdcf212..8965bfc4 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -28,12 +28,24 @@ jobs: gcc \ make \ clang \ - bpftool \ + linux-tools-common \ + linux-tools-generic \ libbpf-dev \ libgtk-3-dev \ pkg-config shell: bash + - name: Locate bpftool + # On Ubuntu "bpftool" is only a virtual package; the real binary ships in linux-tools-* and is not on PATH + # for the runner's cloud kernel, so find it explicitly. + run: | + shopt -s nullglob + candidates=(/usr/lib/linux-tools/*/bpftool) + [ ${#candidates[@]} -gt 0 ] || { echo "bpftool binary not found"; ls /usr/lib/linux-tools 2>&1 || true; exit 1; } + echo "BPFTOOL=${candidates[0]}" >> "$GITHUB_ENV" + "${candidates[0]}" version | head -1 + shell: bash + - name: Build project run: | cd Linux diff --git a/Linux/build.sh b/Linux/build.sh index a3e694e2..96f26721 100755 --- a/Linux/build.sh +++ b/Linux/build.sh @@ -18,7 +18,9 @@ need() { # command, install hint need gcc "Install: sudo apt install build-essential | sudo dnf install gcc" need make "Install: sudo apt install make | sudo dnf install make" need clang "Needed to compile the eBPF program. Install: sudo apt install clang | sudo dnf install clang" -need bpftool "Needed to generate the eBPF skeleton. Install: sudo apt install bpftool | sudo dnf install bpftool" +BPFTOOL="${BPFTOOL:-bpftool}" # set BPFTOOL=/path/to/bpftool if it is not on PATH (e.g. /usr/lib/linux-tools/*/bpftool on Ubuntu) +export BPFTOOL +need "$BPFTOOL" "Needed to generate the eBPF skeleton. Install: sudo apt install linux-tools-generic (Ubuntu) | sudo apt install bpftool (Debian) | sudo dnf install bpftool" if [ ! -f /usr/include/bpf/libbpf.h ] && ! pkg-config --exists libbpf 2>/dev/null; then echo "ERROR: libbpf development files not found. Install: sudo apt install libbpf-dev | sudo dnf install libbpf-devel" exit 1 diff --git a/Linux/tests/e2e.sh b/Linux/tests/e2e.sh index 9106ca50..2607b05a 100755 --- a/Linux/tests/e2e.sh +++ b/Linux/tests/e2e.sh @@ -137,7 +137,7 @@ echo "== lifecycle" kill -INT "$PB_PID"; wait "$PB_PID" 2>/dev/null out=$(curl -s -m 3 http://192.0.2.7/ 2>&1; echo "rc=$?") [[ "$out" != *hello-from* ]] && ok "after stop traffic is no longer redirected" || bad "still redirected after stop" -bpftool prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left behind" || ok "BPF programs detached" +"${BPFTOOL:-bpftool}" prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left behind" || ok "BPF programs detached" ss -ltn 2>/dev/null | grep -qE ":2401[0-9]" && bad "relay ports still listening" || ok "relay ports released" echo diff --git a/Linux/tests/e2e_domains.sh b/Linux/tests/e2e_domains.sh index 6f8c4c98..8cf9b0c7 100755 --- a/Linux/tests/e2e_domains.sh +++ b/Linux/tests/e2e_domains.sh @@ -64,7 +64,7 @@ grep -q "192.0.2.77 (app.example.test)" "$WORK/pb.out" && ok "connection log sho echo "== lifecycle" kill -INT "$PB_PID"; wait "$PB_PID" 2>/dev/null out=$(dig @$DNSIP app.example.test +short +time=3 +tries=1 | head -1); has "DNS still works after ProxyBridge stops" "$out" "192.0.2.77" -bpftool prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left behind" || ok "BPF programs detached" +"${BPFTOOL:-bpftool}" prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left behind" || ok "BPF programs detached" echo; echo "passed=$PASS failed=$FAIL" [ "$FAIL" = 0 ] || { echo "---- dns log"; cat "$DLOG"; echo "---- proxy log"; cat "$PLOG"; echo "---- pb output"; tail -30 "$WORK/pb.out"; exit 1; } diff --git a/Linux/tests/gui_headless.sh b/Linux/tests/gui_headless.sh index ae4c1d2f..d3e9d48c 100755 --- a/Linux/tests/gui_headless.sh +++ b/Linux/tests/gui_headless.sh @@ -52,7 +52,7 @@ chmod 600 "$WORK/cfg/config.ini" run_gui() { # active profile write_cfg "$1" GDK_BACKEND=broadway BROADWAY_DISPLAY=:9 PROXYBRIDGE_CONFIG_DIR="$WORK/cfg" LD_LIBRARY_PATH="$LIBDIR" "$GUI" > "$WORK/gui.out" 2>&1 & GUI_PID=$! - for _ in $(seq 60); do bpftool prog show 2>/dev/null | grep -q pb_connect4 && break; kill -0 $GUI_PID 2>/dev/null || break; sleep 0.25; done + for _ in $(seq 60); do "${BPFTOOL:-bpftool}" prog show 2>/dev/null | grep -q pb_connect4 && break; kill -0 $GUI_PID 2>/dev/null || break; sleep 0.25; done sleep 1 } stop_gui() { @@ -64,7 +64,7 @@ stop_gui() { echo "== profile Default" run_gui Default kill -0 $GUI_PID 2>/dev/null && ok "GUI started under Broadway" || { bad "GUI died: $(head -5 "$WORK/gui.out")"; } -bpftool prog show 2>/dev/null | grep -q pb_connect4 && ok "engine started by the GUI (eBPF programs attached)" || bad "no eBPF programs" +"${BPFTOOL:-bpftool}" prog show 2>/dev/null | grep -q pb_connect4 && ok "engine started by the GUI (eBPF programs attached)" || bad "no eBPF programs" out=$(curl -s -m 5 http://192.0.2.7/); has "rule 1 from the config file: curl -> proxy a" "$out" "hello-from-A dst=192.0.2.7:80" out=$(curl -s -m 5 http://198.51.100.7/); has "rule 2: curl -> proxy b (with credentials from the config)" "$out" "hello-from-B dst=198.51.100.7:80" out=$(curl -s -m 5 http://203.0.114.7/ 2>&1; echo "rc=$?"); has "rule 3: BLOCK" "$out" "rc=7" @@ -72,7 +72,7 @@ out=$(curl -s -m 2 http://203.0.113.7/ 2>&1; echo "rc=$?"); [[ "$out" != *hello- ip4=$(dig @$DNSIP app.example.test +short +time=3 +tries=1 | head -1) out=$(curl -s -m 5 http://$ip4/); has "domain rule from the config (*.example.test -> b) after a real DNS lookup" "$out" "hello-from-B dst=192.0.2.77:80" stop_gui -bpftool prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left after GUI exit" || ok "programs detached after GUI exit" +"${BPFTOOL:-bpftool}" prog show 2>/dev/null | grep -q pb_connect && bad "BPF programs left after GUI exit" || ok "programs detached after GUI exit" echo "== profile Alt (same GUI, different active profile)" run_gui Alt diff --git a/Linux/tests/scale.sh b/Linux/tests/scale.sh index 16ef4d09..7ff3dcac 100755 --- a/Linux/tests/scale.sh +++ b/Linux/tests/scale.sh @@ -14,7 +14,7 @@ sleep 1 stdbuf -oL "$PB_DIR/ProxyBridge" --proxy a=socks5://127.0.0.1:19080 --rule "python3*:192.0.2.0/24:*:TCP:PROXY=a" > $WORK/pb.out 2>&1 & PB=$! for _ in $(seq 40); do grep -q "ProxyBridge started" $WORK/pb.out && break; sleep 0.25; done rss() { awk '/VmRSS/{print $2}' /proc/$PB/status; } -maps_kb() { bpftool map show 2>/dev/null | grep -oE 'memlock [0-9]+' | awk '{s+=$2} END{printf "%d", s/1024}'; } +maps_kb() { "${BPFTOOL:-bpftool}" map show 2>/dev/null | grep -oE 'memlock [0-9]+' | awk '{s+=$2} END{printf "%d", s/1024}'; } echo "baseline: ProxyBridge RSS=$(( $(rss) / 1024 )) MB BPF maps=$(maps_kb) KB threads=$(ls /proc/$PB/task | wc -l)" CPU0=$(awk '{print $14+$15}' /proc/$PB/stat) rm -f /tmp/pb_scale_ready /tmp/pb_scale_go From e0e691a22fcf1f958608f0bb513c7fec14a43212 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 09:59:40 +0530 Subject: [PATCH 10/25] test(linux): isolate the relay DNS tests; add watchdogs and CI timeouts The relay test harness called unshare(CLONE_NEWUSER|CLONE_NEWNET) in its main process. On GitHub's Ubuntu 24.04 runners the namespace is created but loopback cannot be configured, which stranded the whole run in a network namespace without loopback and made every later test fail or hang. - run the port-53 DNS tests in a forked child; the main process never changes namespaces; if the child cannot isolate itself the tests are skipped - global watchdog (PB_TEST_WATCHDOG, default 300s), line-buffered output and bounded client timeouts so a broken environment fails fast - start-up loopback self-check with a clear FATAL message - workflows: job and step timeouts so a hung step cannot hold a runner --- .github/workflows/build-linux.yml | 3 + .github/workflows/release-linux.yml | 1 + Linux/src/tests/relay_test.c | 175 +++++++++++++++++++++------- Linux/src/tests/run_relay_test.sh | 2 +- 4 files changed, 141 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 1e3eb81d..48c223c4 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -13,6 +13,7 @@ on: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read actions: read @@ -72,6 +73,7 @@ jobs: shell: bash - name: Unit tests (rule engine, DNS parser, relay) + timeout-minutes: 15 run: | cd Linux/src make test-rules BUILD_DIR="$RUNNER_TEMP/pb-tests" @@ -156,6 +158,7 @@ jobs: # Needs root, cgroup v2 and a BTF kernel. GitHub-hosted runners usually have them, but this is not guaranteed, # so a failure here is reported without failing the build. continue-on-error: true + timeout-minutes: 15 run: | sudo apt-get install -y dnsutils >/dev/null uname -r diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 8965bfc4..9e41bdba 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -11,6 +11,7 @@ permissions: jobs: build-and-release: runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: write actions: read diff --git a/Linux/src/tests/relay_test.c b/Linux/src/tests/relay_test.c index 51c35a05..2f00b567 100644 --- a/Linux/src/tests/relay_test.c +++ b/Linux/src/tests/relay_test.c @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include #include "../pb_internal.h" @@ -136,6 +139,25 @@ static void ev_clear(void) { g_ev = g_ev_proxy = g_ev_direct = g_ev_block = 0; } // Test plumbing // --------------------------------------------------------------------------------------------- static int g_pass, g_fail; +static const char *volatile g_cur = "startup"; +static void watchdog(int sig) +{ + (void)sig; + static const char m1[] = "\nFATAL: watchdog expired while running: "; + if (write(2, m1, sizeof m1 - 1) < 0) {} + const char *c = g_cur; + if (write(2, c, strlen(c)) < 0) {} + if (write(2, "\n", 1) < 0) {} + _exit(3); +} +static void arm_watchdog(void) +{ + const char *e = getenv("PB_TEST_WATCHDOG"); + int secs = e ? atoi(e) : 300; + signal(SIGALRM, watchdog); + alarm((unsigned)(secs > 0 ? secs : 300)); +} +#define RUN(fn) do { g_cur = #fn; printf("---- %s\n", #fn); fn(); } while (0) #define CHECK(cond, ...) do { if (cond) { g_pass++; printf("PASS "); } else { g_fail++; printf("FAIL "); } \ printf(__VA_ARGS__); printf("\n"); fflush(stdout); } while (0) @@ -459,9 +481,12 @@ static int relay_connect(uint16_t src) struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(src) }; if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } a.sin_port = htons(pb_relay_tcp_port()); + struct timeval tv = { 10, 0 }; + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); // bounds connect() if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } - struct timeval tv = { 30, 0 }; + tv.tv_sec = 30; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); return fd; } static uint16_t new_src(void) { return (uint16_t)atomic_fetch_add(&g_next_port, 1); } @@ -844,22 +869,26 @@ static void test_tcp_10k(void) int base_fds = count_fds(); long rss0 = rss_kb(); int *fds = malloc(N * sizeof *fds); - int opened = 0; - for (int i = 0; i < N; i++) { + for (int i = 0; i < N; i++) fds[i] = -1; + int opened = 0, consec_fail = 0; + bool stalled = false; + for (int i = 0; i < N && !stalled; i++) { fds[i] = -1; for (int tries = 0; tries < 5 && fds[i] < 0; tries++) { uint16_t src = new_src(); set_tcp(src, "127.0.0.1", 9, PB_ACT_PROXY, 10); fds[i] = relay_connect(src); } - if (fds[i] >= 0) opened++; + if (fds[i] >= 0) { opened++; consec_fail = 0; } + else if (++consec_fail >= 20) { printf(" 10k test: 20 consecutive connect failures at i=%d (%s), giving up\n", i, strerror(errno)); stalled = true; } if (i % 100 == 99) { // pace so the bounded handshake queue is not what we measure int64_t t0 = now_ms(); while (atomic_load(&g_hold_ready) < i - 200 && now_ms() - t0 < 30000) msleep(2); + if (atomic_load(&g_hold_ready) < i - 200) { printf(" 10k test: handshakes stalled at i=%d (ready=%d), giving up\n", i, (int)g_hold_ready); stalled = true; } } } int64_t t0 = now_ms(); - while (atomic_load(&g_hold_ready) < N && now_ms() - t0 < 60000) msleep(20); + while (!stalled && atomic_load(&g_hold_ready) < N && now_ms() - t0 < 60000) msleep(20); msleep(500); long rss1 = rss_kb(); int alive = 0; @@ -1106,7 +1135,25 @@ static void test_proxy_helper(void) // --------------------------------------------------------------------------------------------- // Network namespace so that we can bind port 53 without real root // --------------------------------------------------------------------------------------------- -static bool g_have53; +static bool loopback_ok(void) +{ + int l = socket(AF_INET, SOCK_STREAM, 0), c = socket(AF_INET, SOCK_STREAM, 0); + bool ok = false; + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + struct timeval tv = { 3, 0 }; + setsockopt(c, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + if (l >= 0 && c >= 0 && bind(l, (struct sockaddr *)&a, sizeof a) == 0 && listen(l, 1) == 0) { + socklen_t al = sizeof a; + getsockname(l, (struct sockaddr *)&a, &al); + if (connect(c, (struct sockaddr *)&a, sizeof a) == 0) { + struct pollfd p = { l, POLLIN, 0 }; + ok = poll(&p, 1, 3000) == 1; + } + } + if (l >= 0) close(l); + if (c >= 0) close(c); + return ok; +} static bool can_bind53(void) { int fd = socket(AF_INET, SOCK_STREAM, 0); @@ -1115,12 +1162,14 @@ static bool can_bind53(void) close(fd); return ok; } -static void setup_netns(void) +// Runs in the forked DNS child only: gets a private netns where port 53 is bindable. Returns false if impossible. +static bool isolate_for_port53(void) { - if (can_bind53()) { g_have53 = true; return; } + bool force_no_lo = getenv("PB_TEST_FORCE_NO_LO") != NULL; + if (!force_no_lo && can_bind53() && loopback_ok()) return true; // already privileged enough uid_t uid = getuid(); gid_t gid = getgid(); - if (unshare(CLONE_NEWUSER | CLONE_NEWNET) != 0) { printf("NOTE unshare(user+net) failed: %s\n", strerror(errno)); return; } + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) != 0) { printf("NOTE DNS child: unshare failed: %s\n", strerror(errno)); return false; } int fd = open("/proc/self/setgroups", O_WRONLY); if (fd >= 0) { if (write(fd, "deny", 4) < 0) {} close(fd); } char b[64]; @@ -1128,17 +1177,23 @@ static void setup_netns(void) if (fd >= 0) { int n = snprintf(b, sizeof b, "0 %u 1", uid); if (write(fd, b, (size_t)n) < 0) {} close(fd); } fd = open("/proc/self/gid_map", O_WRONLY); if (fd >= 0) { int n = snprintf(b, sizeof b, "0 %u 1", gid); if (write(fd, b, (size_t)n) < 0) {} close(fd); } - int s = socket(AF_INET, SOCK_DGRAM, 0); - struct ifreq ifr; - memset(&ifr, 0, sizeof ifr); - snprintf(ifr.ifr_name, IFNAMSIZ, "lo"); - ifr.ifr_flags = IFF_UP | IFF_RUNNING; - ioctl(s, SIOCSIFFLAGS, &ifr); - close(s); - g_have53 = can_bind53(); - printf("NOTE using private network namespace (port 53 bindable: %s)\n", g_have53 ? "yes" : "no"); + if (!force_no_lo) { + int s = socket(AF_INET, SOCK_DGRAM, 0); + struct ifreq ifr; + memset(&ifr, 0, sizeof ifr); + snprintf(ifr.ifr_name, IFNAMSIZ, "lo"); + ifr.ifr_flags = IFF_UP | IFF_RUNNING; + if (s >= 0) { if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) printf("NOTE DNS child: cannot bring lo up: %s\n", strerror(errno)); close(s); } + } + if (!loopback_ok()) { printf("NOTE DNS child: loopback unusable in the new namespace\n"); return false; } + if (!can_bind53()) { printf("NOTE DNS child: port 53 not bindable in the new namespace\n"); return false; } + return true; } +// Shared with the forked DNS child. +struct shared { int pass, fail, status; }; // status: 0 = not run, 1 = ran, 3 = cannot isolate +static struct shared *g_shared; + // --------------------------------------------------------------------------------------------- // DNS tap tests // --------------------------------------------------------------------------------------------- @@ -1224,7 +1279,6 @@ static bool dns_tcp_case(const char *name, int action, uint32_t px, int mode, in static void test_dns(void) { CHECK(g_obs_n == 0, "no DNS observe calls for non-port-53 flows so far (%d)", (int)g_obs_n); - if (!g_have53) { printf("SKIP DNS tests (cannot bind port 53)\n"); return; } struct srv *d = srv_start_p(dns_tcp_handler, NULL, 53); int ufd = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(53) }; @@ -1421,10 +1475,8 @@ static void test_udp_odd(void) close(c); } -int main(void) +static void init_world(void) { - setvbuf(stdout, NULL, _IOLBF, 0); - setup_netns(); struct srvcfg c = { 0 }; g_echo = srv_start(echo_handler, NULL); g_s5 = srv_start(socks_handler, &c); @@ -1448,25 +1500,70 @@ int main(void) set_proxy(4, PROXY_TYPE_HTTP, g_httpauth->port, "alice", "secret"); set_proxy(5, PROXY_TYPE_HTTP, g_httpleft->port, NULL, NULL); g_default_px = 1; +} + +// The port-53 tests run in a forked child (before any thread exists) so that a private network namespace never +// affects the main process. Status/counters come back through shared memory. +static void run_dns_child(void) +{ + arm_watchdog(); + if (!isolate_for_port53()) { g_shared->status = 3; _exit(0); } + init_world(); + if (relay_up(300, 60, 0, 5) != 0) { printf("FAIL DNS child: pb_relay_start\n"); g_shared->fail = 1; g_shared->status = 1; _exit(0); } + RUN(test_dns); + pb_relay_stop(); + g_shared->pass = g_pass; + g_shared->fail = g_fail; + g_shared->status = 1; + fflush(stdout); + _exit(0); +} + +int main(void) +{ + setvbuf(stdout, NULL, _IOLBF, 0); + if (!loopback_ok()) { + fprintf(stderr, "FATAL: 127.0.0.1 loopback is not usable in this environment (bind+connect failed)\n"); + return 2; + } + g_shared = mmap(NULL, sizeof *g_shared, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); + if (g_shared == MAP_FAILED) { perror("mmap"); return 2; } + memset(g_shared, 0, sizeof *g_shared); + g_cur = "dns child"; + pid_t pid = fork(); + if (pid == 0) run_dns_child(); + int st = 0; + if (pid < 0 || waitpid(pid, &st, 0) < 0) st = -1; + arm_watchdog(); + if (g_shared->status == 1 && WIFEXITED(st) && WEXITSTATUS(st) == 0) { + g_pass += g_shared->pass; + g_fail += g_shared->fail; + } else if (g_shared->status == 3) { + printf("SKIP DNS tests: cannot isolate a private network namespace with port 53 here\n"); + } else { + g_fail++; + printf("FAIL DNS child crashed or timed out (wait status 0x%x)\n", st); + } + init_world(); + g_cur = "pb_relay_start"; CHECK(relay_up(300, 60, 0, 5) == 0, "pb_relay_start"); - test_tcp_basic(); - test_tcp_bad_proxy(); - test_tcp_halfclose(); - test_udp(); - test_proxy_helper(); - test_udp_odd(); - test_hostname(); - test_latency(); - test_dns(); - test_tcp_large(); - test_throughput(); - test_tcp_10k(); - test_tcp_idle(); - test_tcp_cap(); - test_handshake_timeout(); - test_udp_idle(); - test_start_stop(); + RUN(test_tcp_basic); + RUN(test_tcp_bad_proxy); + RUN(test_tcp_halfclose); + RUN(test_udp); + RUN(test_proxy_helper); + RUN(test_udp_odd); + RUN(test_hostname); + RUN(test_latency); + RUN(test_tcp_large); + RUN(test_throughput); + RUN(test_tcp_10k); + RUN(test_tcp_idle); + RUN(test_tcp_cap); + RUN(test_handshake_timeout); + RUN(test_udp_idle); + RUN(test_start_stop); pb_relay_stop(); printf("\nRESULT: %d passed, %d failed\n", g_pass, g_fail); return g_fail ? 1 : 0; diff --git a/Linux/src/tests/run_relay_test.sh b/Linux/src/tests/run_relay_test.sh index 23b86edd..cab1f8f9 100755 --- a/Linux/src/tests/run_relay_test.sh +++ b/Linux/src/tests/run_relay_test.sh @@ -11,5 +11,5 @@ if [ "$mode" = plain ] || [ "$mode" = both ]; then fi if [ "$mode" = asan ] || [ "$mode" = both ]; then gcc $CF -fsanitize=address,undefined -fno-omit-frame-pointer relay_test.c ../pb_relay.c -o "$out/relay_test_asan" -lpthread - LARGE_MB="${LARGE_MB:-4}" THRU_MB="${THRU_MB:-32}" ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$out/relay_test_asan" + PB_TEST_WATCHDOG="${PB_TEST_WATCHDOG:-900}" LARGE_MB="${LARGE_MB:-4}" THRU_MB="${THRU_MB:-32}" ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$out/relay_test_asan" fi From 2aac5bffd178300c166964992e0e2ea55c5cd10d Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 10:32:27 +0530 Subject: [PATCH 11/25] test(linux): make the relay tests robust on slow, shared CPUs - fake decision state is now accessed under a mutex (it was a plain struct shared with relay threads) - fd-leak checks settle and assert end <= base instead of equality - wait budgets for closes, UDP replies and slot reuse are generous; hangs are still bounded by the watchdog - expect_closed prints fd, poll, errno and recv details when it fails Verified with 12 runs on 2 shared CPUs under 4 busy loops, plus plain, ASan+UBSan and the simulated no-loopback mode. --- Linux/src/tests/relay_test.c | 101 ++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 37 deletions(-) diff --git a/Linux/src/tests/relay_test.c b/Linux/src/tests/relay_test.c index 2f00b567..355de43b 100644 --- a/Linux/src/tests/relay_test.c +++ b/Linux/src/tests/relay_test.c @@ -69,7 +69,9 @@ void pb_core_decide(const struct pb_orig *o, struct pb_decision *out) { (void)o; atomic_fetch_add(&g_decide_calls, 1); + pthread_mutex_lock(&g_mu); *out = g_decision; + pthread_mutex_unlock(&g_mu); } int pb_core_get_proxy(uint32_t id, struct pb_proxy *out) { @@ -132,6 +134,12 @@ static bool log_has(const char *s) pthread_mutex_unlock(&g_mu); return r; } +static void set_decision(struct pb_decision d) +{ + pthread_mutex_lock(&g_mu); + g_decision = d; + pthread_mutex_unlock(&g_mu); +} static void log_clear(void) { pthread_mutex_lock(&g_mu); g_log_n = 0; memset(g_log, 0, sizeof g_log); pthread_mutex_unlock(&g_mu); } static void ev_clear(void) { g_ev = g_ev_proxy = g_ev_direct = g_ev_block = 0; } @@ -216,6 +224,8 @@ static int count_fds(void) closedir(d); return n - 3; // ., .., and the dir fd itself } +// Waits (up to ms) for the fd count to drop to <= target; returns the final count. +static int settle_fds(int target, int ms); static int64_t now_ms(void) { struct timespec ts; @@ -223,6 +233,13 @@ static int64_t now_ms(void) return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } static void msleep(int ms) { usleep((useconds_t)ms * 1000); } +static int settle_fds(int target, int ms) +{ + int64_t t0 = now_ms(); + int n = count_fds(); + while (n > target && now_ms() - t0 < ms) { msleep(50); n = count_fds(); } + return n; +} // --------------------------------------------------------------------------------------------- // Fake servers @@ -533,11 +550,16 @@ static int64_t xfer(int fd, uint64_t total, bool half) // True when the peer closes (EOF or reset) within timeout_ms with no data. static bool expect_closed(int fd, int timeout_ms) { + if (timeout_ms < 10000) timeout_ms = 10000; // generous: the watchdog bounds real hangs struct pollfd p = { fd, POLLIN, 0 }; - if (poll(&p, 1, timeout_ms) <= 0) return false; + int pr = poll(&p, 1, timeout_ms); + if (pr <= 0) { printf(" expect_closed(fd=%d): poll=%d errno=%d revents=0x%x\n", fd, pr, errno, p.revents); return false; } char b[64]; ssize_t r = recv(fd, b, sizeof b, 0); - return r == 0 || (r < 0 && errno != EAGAIN); + int e = errno; + bool ok = r == 0 || (r < 0 && e != EAGAIN); + if (!ok) printf(" expect_closed(fd=%d): poll=%d revents=0x%x recv=%zd errno=%d\n", fd, pr, p.revents, r, e); + return ok; } static int relay_up(int tcp_idle, int udp_idle, int maxc, int hs) @@ -577,8 +599,6 @@ static int udp_client(void) int fd = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; bind(fd, (struct sockaddr *)&a, sizeof a); - struct timeval tv = { 1, 0 }; - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); return fd; } static void udp_send_flow(int fd, const char *flow_ip, const char *msg) @@ -588,8 +608,10 @@ static void udp_send_flow(int fd, const char *flow_ip, const char *msg) sendto(fd, msg, strlen(msg), 0, (struct sockaddr *)&a, sizeof a); } // Receives one datagram; returns length or -1; src_ip filled with the sender address. -static int udp_recv(int fd, char *buf, size_t n, char *src_ip, uint16_t *src_port) +static int udp_recv_to(int fd, char *buf, size_t n, char *src_ip, uint16_t *src_port, int ms) { + struct timeval tv = { ms / 1000, (ms % 1000) * 1000 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); struct sockaddr_in f; socklen_t fl = sizeof f; ssize_t r = recvfrom(fd, buf, n - 1, 0, (struct sockaddr *)&f, &fl); @@ -599,6 +621,8 @@ static int udp_recv(int fd, char *buf, size_t n, char *src_ip, uint16_t *src_por *src_port = ntohs(f.sin_port); return (int)r; } +static int udp_recv(int fd, char *buf, size_t n, char *ip, uint16_t *sp) { return udp_recv_to(fd, buf, n, ip, sp, 6000); } +static int udp_recv_neg(int fd, char *buf, size_t n, char *ip, uint16_t *sp) { return udp_recv_to(fd, buf, n, ip, sp, 1500); } static bool udp_echo_ok(int fd, const char *flow_ip, const char *msg) { char b[256], ip[32]; @@ -657,11 +681,11 @@ static void test_tcp_basic(void) close(fd); // DECIDE - g_decision = (struct pb_decision){ PB_ACT_DIRECT, 0 }; + set_decision((struct pb_decision){ PB_ACT_DIRECT, 0 }); CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DECIDE, 0, 5000, false) == 5000, "TCP DECIDE -> DIRECT"); - g_decision = (struct pb_decision){ PB_ACT_PROXY, 1 }; + set_decision((struct pb_decision){ PB_ACT_PROXY, 1 }); CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DECIDE, 0, 5000, false) == 5000, "TCP DECIDE -> PROXY"); - g_decision = (struct pb_decision){ PB_ACT_BLOCK, 0 }; + set_decision((struct pb_decision){ PB_ACT_BLOCK, 0 }); src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_DECIDE, 0); fd = relay_connect(src); @@ -912,7 +936,7 @@ static void test_tcp_10k(void) static void test_tcp_idle(void) { pb_relay_stop(); - CHECK(relay_up(2, 60, 0, 5) == 0, "restart relay with tcp_idle=2s"); + CHECK(relay_up(3, 60, 0, 5) == 0, "restart relay with tcp_idle=3s"); uint16_t src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 1); int fd = relay_connect(src); @@ -920,18 +944,18 @@ static void test_tcp_idle(void) char b[4]; rd_full(fd, b, 2); int64_t t0 = now_ms(); - bool closed = expect_closed(fd, 6000); - CHECK(closed && now_ms() - t0 < 5500, "idle TCP connection closed after ~2s (%lld ms)", (long long)(now_ms() - t0)); + bool closed = expect_closed(fd, 15000); + CHECK(closed && now_ms() - t0 >= 1000 && now_ms() - t0 < 14000, "idle TCP connection closed after ~2s (%lld ms)", (long long)(now_ms() - t0)); close(fd); src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0); fd = relay_connect(src); bool alive = true; - for (int i = 0; i < 6 && alive; i++) { + for (int i = 0; i < 10 && alive; i++) { // ~5 s of traffic every 0.5 s, longer than the 3 s idle timeout send(fd, "p", 1, 0); alive = rd_full(fd, b, 1) == 0; - msleep(700); + msleep(500); } CHECK(alive, "active connection survives past idle timeout"); close(fd); @@ -959,8 +983,12 @@ static void test_tcp_cap(void) CHECK(expect_closed(extra, 3000), "6th connection closed immediately (cap)"); close(extra); close(fds[0]); - msleep(500); - CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 1000, false) == 1000, "slot freed after close"); + bool freed = false; + for (int i = 0; i < 100 && !freed; i++) { // up to ~10 s for the relay to notice the close + msleep(100); + freed = tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 1000, false) == 1000; + } + CHECK(freed, "slot freed after close"); for (int i = 1; i < 5; i++) close(fds[i]); msleep(300); } @@ -976,7 +1004,7 @@ static void test_handshake_timeout(void) int fd = relay_connect(s); int64_t t0 = now_ms(); bool closed = expect_closed(fd, 6000); - CHECK(closed && now_ms() - t0 < 4500 && log_has("timed out"), "silent proxy: handshake times out (%lld ms)", (long long)(now_ms() - t0)); + CHECK(closed && now_ms() - t0 >= 1500 && now_ms() - t0 < 14000 && log_has("timed out"), "silent proxy: handshake times out (%lld ms)", (long long)(now_ms() - t0)); close(fd); } @@ -997,7 +1025,7 @@ static void test_udp(void) int c2 = udp_client(); for (int i = 0; i < 10; i++) { char m[32]; snprintf(m, sizeof m, "burst%d", i); udp_send_flow(c2, "127.1.2.3", m); } int got = 0; - while (udp_recv(c2, b, sizeof b, ip, &sp) > 0) got++; + while (udp_recv_to(c2, b, sizeof b, ip, &sp, 3000) > 0) got++; CHECK(got == 10, "10 datagrams sent during association setup delivered (got %d)", got); close(c2); @@ -1015,19 +1043,19 @@ static void test_udp(void) // block / unknown flow / non-loopback set_flow("127.1.4.1", "127.0.0.1", g_uecho1, PB_ACT_BLOCK, 0); udp_send_flow(c, "127.1.4.1", "blocked"); - CHECK(udp_recv(c, b, sizeof b, ip, &sp) < 0, "UDP BLOCK dropped"); + CHECK(udp_recv_neg(c, b, sizeof b, ip, &sp) < 0, "UDP BLOCK dropped"); udp_send_flow(c, "127.9.9.9", "unknown"); - CHECK(udp_recv(c, b, sizeof b, ip, &sp) < 0, "UDP unknown flow dropped"); + CHECK(udp_recv_neg(c, b, sizeof b, ip, &sp) < 0, "UDP unknown flow dropped"); // HTTP proxy cannot do UDP log_clear(); set_flow("127.1.4.2", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 3); udp_send_flow(c, "127.1.4.2", "via-http"); udp_send_flow(c, "127.1.4.2", "via-http2"); - CHECK(udp_recv(c, b, sizeof b, ip, &sp) < 0 && log_has("HTTP proxy"), "UDP via HTTP proxy dropped + logged"); + CHECK(udp_recv_neg(c, b, sizeof b, ip, &sp) < 0 && log_has("HTTP proxy"), "UDP via HTTP proxy dropped + logged"); // DECIDE cached per (client, flow) - g_decision = (struct pb_decision){ PB_ACT_DIRECT, 0 }; + set_decision((struct pb_decision){ PB_ACT_DIRECT, 0 }); set_flow("127.1.4.3", "127.0.0.1", g_uecho1, PB_ACT_DECIDE, 0); g_decide_calls = 0; bool ok = udp_echo_ok(c, "127.1.4.3", "d1") && udp_echo_ok(c, "127.1.4.3", "d2") && udp_echo_ok(c, "127.1.4.3", "d3"); @@ -1036,7 +1064,7 @@ static void test_udp(void) // bad proxy for UDP: nothing comes back, no crash set_flow("127.1.4.4", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 6); udp_send_flow(c, "127.1.4.4", "dead-proxy"); - CHECK(udp_recv(c, b, sizeof b, ip, &sp) < 0, "UDP via dead SOCKS5 proxy: no reply, relay survives"); + CHECK(udp_recv_neg(c, b, sizeof b, ip, &sp) < 0, "UDP via dead SOCKS5 proxy: no reply, relay survives"); CHECK(udp_echo_ok(c, "127.1.3.1", "still-alive"), "relay still serves UDP afterwards"); close(c); } @@ -1050,11 +1078,9 @@ static void test_udp_idle(void) set_flow("127.1.5.2", "127.0.0.1", g_uecho2, PB_ACT_PROXY, 1); int base = count_fds(); bool ok = udp_echo_ok(c, "127.1.5.1", "x") && udp_echo_ok(c, "127.1.5.2", "y"); - msleep(200); int mid = count_fds(); // direct: +1 fd; socks5: +ctl +udp in relay and +3 in the fake server - msleep(5000); - int end = count_fds(); - CHECK(ok && mid > base && end == base, "UDP sessions expire after idle and free their fds (base=%d mid=%d end=%d)", base, mid, end); + int end = settle_fds(base, 15000); + CHECK(ok && mid > base && end <= base, "UDP sessions expire after idle and free their fds (base=%d mid=%d end=%d)", base, mid, end); CHECK(udp_echo_ok(c, "127.1.5.1", "again"), "new session after expiry works"); close(c); } @@ -1079,9 +1105,8 @@ static void test_start_stop(void) pb_relay_stop(); } pb_relay_stop(); - msleep(500); - int after = count_fds(); - CHECK(bad == 0 && before == after, "start/stop x20 (with traffic): fds before=%d after=%d, failures=%d", before, after, bad); + int after = settle_fds(before, 5000); + CHECK(bad == 0 && after <= before, "start/stop x20 (with traffic): fds before=%d after=%d, failures=%d", before, after, bad); // start while ports are taken: must fail and leak nothing int l = socket(AF_INET, SOCK_STREAM, 0); @@ -1093,8 +1118,8 @@ static void test_start_stop(void) int b4 = count_fds(); log_clear(); int r1 = relay_up(0, 0, 0, 0); - int af = count_fds(); - CHECK(r1 == -1 && b4 == af && log_has("cannot listen"), "start fails when TCP port busy, no fd leak, logged"); + int af = settle_fds(b4, 3000); + CHECK(r1 == -1 && af <= b4 && log_has("cannot listen"), "start fails when TCP port busy, no fd leak, logged"); close(l); int u = socket(AF_INET, SOCK_DGRAM, 0); a.sin_port = htons(UPORT); @@ -1102,8 +1127,8 @@ static void test_start_stop(void) if (bind(u, (struct sockaddr *)&a, sizeof a) < 0) perror("test bind"); b4 = count_fds(); r1 = relay_up(0, 0, 0, 0); - af = count_fds(); - CHECK(r1 == -1 && b4 == af, "start fails when UDP port busy, no fd leak (TCP listener released)"); + af = settle_fds(b4, 3000); + CHECK(r1 == -1 && af <= b4, "start fails when UDP port busy, no fd leak (TCP listener released)"); close(u); CHECK(relay_up(0, 0, 0, 0) == 0, "start works after failed starts"); pb_relay_stop(); @@ -1368,8 +1393,8 @@ static void test_latency(void) } printf(" avg RTT per 1-byte round trip: direct echo %.0f us, relay DIRECT %.0f us, relay+SOCKS5 %.0f us, relay+HTTP %.0f us\n", direct, res[0], res[1], res[2]); - CHECK(direct > 0 && res[0] > 0 && res[1] > 0 && res[2] > 0 && res[0] < 5000 && res[1] < 5000 && res[2] < 5000, - "no Nagle/delayed-ACK stall through relay (all averages < 5 ms)"); + CHECK(direct > 0 && res[0] > 0 && res[1] > 0 && res[2] > 0 && res[0] < 20000 && res[1] < 20000 && res[2] < 20000, + "no Nagle/delayed-ACK stall through relay (all averages < 20 ms; a stall would be >= 40 ms per trip)"); } static void test_hostname(void) @@ -1388,7 +1413,7 @@ static void test_hostname(void) int64_t t0 = now_ms(); r = pb_relay_test_proxy(&g_px[13], "echo.test", g_echo->port, b, sizeof b); int64_t dt = now_ms() - t0; - CHECK(r == -1 && strstr(b, "cannot resolve") && dt < 7000, "unresolvable proxy hostname -> readable error in %lld ms: '%s'", (long long)dt, b); + CHECK(r == -1 && strstr(b, "cannot resolve") && dt < 9000, "unresolvable proxy hostname -> readable error in %lld ms: '%s'", (long long)dt, b); log_clear(); uint16_t src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 13); @@ -1427,6 +1452,8 @@ static bool udp_big(int c, const char *flow, size_t n, bool expect) ssize_t s = sendto(c, tx, n, 0, (struct sockaddr *)&a, sizeof a); struct sockaddr_in f; socklen_t fl = sizeof f; + struct timeval tv = { expect ? 6 : 1, expect ? 0 : 500000 }; + setsockopt(c, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); ssize_t r = recvfrom(c, rx, 70000, 0, (struct sockaddr *)&f, &fl); char ip[32]; inet_ntop(AF_INET, &f.sin_addr, ip, sizeof ip); @@ -1469,7 +1496,7 @@ static void test_udp_odd(void) int n = udp_recv(c, b, sizeof b, ip, &sp); CHECK(n == 5 && !strcmp(ip, fl[k]), "%s: reply from a different port of the same host delivered from the flow address", nm[k]); udp_send_flow(c, fl[k], "Chost"); - n = udp_recv(c, b, sizeof b, ip, &sp); + n = udp_recv_neg(c, b, sizeof b, ip, &sp); CHECK(n < 0, "%s: reply from a different host dropped", nm[k]); } close(c); From 78863ae43ec9be2acccea38ab7834d4f3620113b Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 10:37:01 +0530 Subject: [PATCH 12/25] test(linux): report why relay_connect fails in the relay tests The remaining intermittent CI failure ("TCP DECIDE -> BLOCK") happens before expect_closed is reached, i.e. relay_connect() returned -1. Print the failing call, the source port and errno so the next occurrence is diagnosable. --- Linux/src/tests/relay_test.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Linux/src/tests/relay_test.c b/Linux/src/tests/relay_test.c index 355de43b..a6e38a3d 100644 --- a/Linux/src/tests/relay_test.c +++ b/Linux/src/tests/relay_test.c @@ -496,11 +496,17 @@ static int relay_connect(uint16_t src) int one = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(src) }; - if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { + printf(" relay_connect: bind(127.0.0.1:%u) failed: %s\n", src, strerror(errno)); + close(fd); return -1; + } a.sin_port = htons(pb_relay_tcp_port()); struct timeval tv = { 10, 0 }; setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); // bounds connect() - if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { + printf(" relay_connect: connect(src port %u -> relay port %u) failed: %s\n", src, pb_relay_tcp_port(), strerror(errno)); + close(fd); return -1; + } tv.tv_sec = 30; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); From dfa5d4ea54f232f73fc0d791ba1b47d01584a1b4 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 10:44:51 +0530 Subject: [PATCH 13/25] test(linux): accept a reset during connect() as "closed by the relay" For flows the relay aborts (BLOCK decisions, connection cap) it closes with a RST right after accept. On a fast loopback the RST can arrive before the client's connect() has returned, so connect() itself fails with ECONNRESET. That is a valid way for such a flow to end, but the tests only accepted a reset after connect(); GitHub runners hit the race in ~2/3 of the runs. closed_by_relay() treats ECONNRESET from connect() or EOF/RST afterwards as closed, and is used by every "the relay must close this" assertion. --- Linux/src/tests/relay_test.c | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/Linux/src/tests/relay_test.c b/Linux/src/tests/relay_test.c index a6e38a3d..c385a3e5 100644 --- a/Linux/src/tests/relay_test.c +++ b/Linux/src/tests/relay_test.c @@ -490,6 +490,7 @@ static uint16_t udpecho_start(void) // --------------------------------------------------------------------------------------------- // Client helpers // --------------------------------------------------------------------------------------------- +static int g_last_connect_errno; // errno of the last failed relay_connect() static int relay_connect(uint16_t src) { int fd = socket(AF_INET, SOCK_STREAM, 0); @@ -497,6 +498,7 @@ static int relay_connect(uint16_t src) setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(src) }; if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { + g_last_connect_errno = errno; printf(" relay_connect: bind(127.0.0.1:%u) failed: %s\n", src, strerror(errno)); close(fd); return -1; } @@ -504,7 +506,11 @@ static int relay_connect(uint16_t src) struct timeval tv = { 10, 0 }; setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); // bounds connect() if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { - printf(" relay_connect: connect(src port %u -> relay port %u) failed: %s\n", src, pb_relay_tcp_port(), strerror(errno)); + g_last_connect_errno = errno; + // an ECONNRESET here is legitimate for flows the relay aborts (BLOCK, cap): on a fast loopback the RST can arrive + // before connect() returns. Callers that expect a close accept it through closed_by_relay(). + if (errno != ECONNRESET) + printf(" relay_connect: connect(src port %u -> relay port %u) failed: %s\n", src, pb_relay_tcp_port(), strerror(errno)); close(fd); return -1; } tv.tv_sec = 30; @@ -568,6 +574,14 @@ static bool expect_closed(int fd, int timeout_ms) return ok; } +// True when the relay closed the connection: either connect() itself was reset (the relay aborted the flow before connect() +// returned, which happens on a fast loopback) or the client sees EOF/RST afterwards. +static bool closed_by_relay(int fd, int timeout_ms) +{ + if (fd < 0) return g_last_connect_errno == ECONNRESET; + return expect_closed(fd, timeout_ms); +} + static int relay_up(int tcp_idle, int udp_idle, int maxc, int hs) { struct pb_relay_cfg c = { TPORT, UPORT, maxc, tcp_idle, udp_idle, hs }; @@ -682,7 +696,7 @@ static void test_tcp_basic(void) set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_BLOCK, 0); fd = relay_connect(src); send(fd, "x", 1, MSG_NOSIGNAL); - CHECK(fd >= 0 && expect_closed(fd, 3000), "TCP BLOCK closes client"); + CHECK(closed_by_relay(fd, 3000), "TCP BLOCK closes client"); CHECK(g_ev_block == 1, "flow_event BLOCK"); close(fd); @@ -695,12 +709,12 @@ static void test_tcp_basic(void) src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_DECIDE, 0); fd = relay_connect(src); - CHECK(fd >= 0 && expect_closed(fd, 3000), "TCP DECIDE -> BLOCK"); + CHECK(closed_by_relay(fd, 3000), "TCP DECIDE -> BLOCK"); close(fd); // no orig entry fd = relay_connect(new_src()); - CHECK(fd >= 0 && expect_closed(fd, 3000), "TCP without original destination is closed"); + CHECK(closed_by_relay(fd, 3000), "TCP without original destination is closed"); close(fd); // domain-name target through test helper is covered separately; IPv6 original destination (no v6 proxy server: direct to ::1) @@ -714,7 +728,7 @@ static void test_tcp_bad_proxy(void) uint16_t src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 6); int fd = relay_connect(src); - CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("refused"), "bad proxy (connection refused): client closed + logged"); + CHECK(closed_by_relay(fd, 5000) && log_has("refused"), "bad proxy (connection refused): client closed + logged"); close(fd); log_clear(); @@ -722,7 +736,7 @@ static void test_tcp_bad_proxy(void) src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 7); fd = relay_connect(src); - CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("authentication failed"), "SOCKS5 wrong password: client closed + logged"); + CHECK(closed_by_relay(fd, 5000) && log_has("authentication failed"), "SOCKS5 wrong password: client closed + logged"); close(fd); log_clear(); @@ -730,14 +744,14 @@ static void test_tcp_bad_proxy(void) src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 8); fd = relay_connect(src); - CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("407"), "HTTP 407: client closed + logged"); + CHECK(closed_by_relay(fd, 5000) && log_has("407"), "HTTP 407: client closed + logged"); close(fd); log_clear(); src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 12); // unknown proxy id fd = relay_connect(src); - CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("no proxy configured"), "unknown proxy id: client closed + logged"); + CHECK(closed_by_relay(fd, 5000) && log_has("no proxy configured"), "unknown proxy id: client closed + logged"); close(fd); // dead target through a working proxy -> SOCKS5 REP 5 @@ -745,7 +759,7 @@ static void test_tcp_bad_proxy(void) src = new_src(); set_tcp(src, "127.0.0.1", dead, PB_ACT_PROXY, 1); fd = relay_connect(src); - CHECK(fd >= 0 && expect_closed(fd, 5000) && log_has("connection refused by target"), "SOCKS5 REP code mapped in log"); + CHECK(closed_by_relay(fd, 5000) && log_has("connection refused by target"), "SOCKS5 REP code mapped in log"); close(fd); } @@ -986,7 +1000,7 @@ static void test_tcp_cap(void) set_tcp(s, "127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0); int extra = relay_connect(s); send(extra, "x", 1, MSG_NOSIGNAL); - CHECK(expect_closed(extra, 3000), "6th connection closed immediately (cap)"); + CHECK(closed_by_relay(extra, 3000), "6th connection closed immediately (cap)"); close(extra); close(fds[0]); bool freed = false; @@ -1424,7 +1438,7 @@ static void test_hostname(void) uint16_t src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 13); int fd = relay_connect(src); - CHECK(fd >= 0 && expect_closed(fd, 8000) && log_has("cannot resolve"), "relay TCP with unresolvable proxy: client closed + logged"); + CHECK(closed_by_relay(fd, 8000) && log_has("cannot resolve"), "relay TCP with unresolvable proxy: client closed + logged"); close(fd); } From 8019f00e28297092376688fc0453ba10dff41e3e Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 12:34:41 +0530 Subject: [PATCH 14/25] test(linux): end-to-end suite against Nexus Proxy (real SOCKS5 forwarding, TCP and UDP) Runs ProxyBridge in front of two Nexus instances (open and username/password) with real origin servers and a real DNS server, instead of fakes: rules by IP, port, port range, process, CIDR and domain; DNS itself carried by the proxy (UDP and TCP); 200 MB download and 100 MB upload with checksums; 100 parallel and 1,500 sequential connections; one UDP socket talking to two proxies, connected UDP, 59 KB datagrams, 300 concurrent UDP sockets, a sustained stream; proxy restart (TCP fails closed, UDP re-associates). Stock Nexus cannot do UDP ASSOCIATE on Linux (IPv6 socket bound to 0.0.0.0, IPv4 destinations not sent as ::ffff:, mapped replies not matched); tests/nexus-linux-udp.patch fixes it and the UDP checks are skipped without it. fake_dns.py gains a PB_DNS_ZONE override. --- Linux/README.md | 3 + Linux/tests/e2e_nexus.sh | 240 ++++++++++++++++++++++++++++++ Linux/tests/fake_dns.py | 5 + Linux/tests/nexus-linux-udp.patch | 25 ++++ Linux/tests/nexus_origins.py | 78 ++++++++++ 5 files changed, 351 insertions(+) create mode 100755 Linux/tests/e2e_nexus.sh create mode 100644 Linux/tests/nexus-linux-udp.patch create mode 100644 Linux/tests/nexus_origins.py diff --git a/Linux/README.md b/Linux/README.md index 7137e1d2..f41cb75e 100644 --- a/Linux/README.md +++ b/Linux/README.md @@ -461,6 +461,8 @@ gcc -o ProxyBridge main.o \ -lproxybridge -lpthread ``` +**Testing against Nexus Proxy.** [Nexus Proxy](https://github.com/InterceptSuite/nexus-proxy) is a Node.js SOCKS5 server that really forwards TCP and UDP. `tests/e2e_nexus.sh` runs ProxyBridge in front of two Nexus instances (one with a username/password) with real origin servers and a real DNS server, and checks rules by IP, port, port range, process, CIDR and domain, TCP and UDP, 200 MB / 100 MB checksummed transfers, 100 parallel and 1,500 sequential connections, 300 concurrent UDP sockets, 59 KB datagrams, a sustained UDP stream, and recovery after the proxy restarts. Stock Nexus cannot do UDP ASSOCIATE on Linux (its IPv6 relay socket binds `0.0.0.0`, IPv4 destinations are not sent as `::ffff:` addresses, and mapped replies are not matched); `tests/nexus-linux-udp.patch` fixes that, and without it the UDP checks are skipped. Nexus limits each client IP to 500 connections, so it cannot be used for the 10,000-connection scale test. + **Config location:** the GUI stores profiles, proxies and rules in `/etc/proxybridge/config.ini` (mode 0600). Set `PROXYBRIDGE_CONFIG_DIR` to use another directory (used by the tests). ## Uninstallation @@ -509,6 +511,7 @@ sudo ./e2e_domains.sh # domain rules with a real res sudo ./e2e_rules.sh # every rule form + every proxy type, incl. HTTP auth and UDP rules (root) sudo ./api_live.sh # library API: edit/move/disable/delete rules and proxies on the live datapath (root) sudo ./e2e_openssh.sh # interoperability with a real third-party SOCKS5 server (OpenSSH ssh -D), 40 MB each way (root) +sudo NODE=$(command -v node) NEXUS_JS=/path/to/nexus-proxy/script.js ./e2e_nexus.sh # against Nexus Proxy, a real forwarding SOCKS5 server with UDP (root) sudo ./scale.sh 10000 # 10,000 concurrent proxied connections (root) sudo ./gui_headless.sh # the real GTK GUI under the Broadway backend (root, needs broadwayd) cd ../gui/tests && gcc -I../../src -I.. -D_GNU_SOURCE $(pkg-config --cflags gtk+-3.0) config_test.c ../gui_config.c -o /tmp/config_test $(pkg-config --libs gtk+-3.0) && /tmp/config_test # GUI config/profile tests diff --git a/Linux/tests/e2e_nexus.sh b/Linux/tests/e2e_nexus.sh new file mode 100755 index 00000000..0321421e --- /dev/null +++ b/Linux/tests/e2e_nexus.sh @@ -0,0 +1,240 @@ +#!/bin/bash +# ProxyBridge against a REAL forwarding SOCKS5 server: Nexus Proxy (https://github.com/InterceptSuite/nexus-proxy). +# Real origin servers, real DNS, real transfers with checksums; two Nexus instances (open + username/password) so that rules +# can pick a proxy. Needs root, node, dig, curl, python3, and the Nexus script. +# +# sudo NODE=/path/to/node NEXUS_JS=/path/to/script.js tests/e2e_nexus.sh +# +# Note: stock Nexus cannot do UDP ASSOCIATE on Linux (its IPv6 relay socket binds '0.0.0.0' -> EINVAL, IPv4 destinations are not +# sent as ::ffff: addresses and mapped replies are not matched). Apply tests/nexus-linux-udp.patch to a copy of script.js for the +# UDP part; without it the UDP tests are reported as SKIP and only TCP/DNS-over-TCP is tested. +set -u +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +PB_DIR="${PB_DIR:-$ROOT/output}" +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR"; exit 1; } +NODE="${NODE:-node}"; NEXUS_JS="${NEXUS_JS:-}" +command -v "$NODE" >/dev/null 2>&1 && [ -f "$NEXUS_JS" ] || { echo "SKIP: set NODE and NEXUS_JS (see header)"; exit 0; } +[ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } +export LD_LIBRARY_PATH="$PB_DIR" +WORK="$(mktemp -d)"; PASS=0; FAIL=0; SKIP=0; PIDS=() +ok() { echo " PASS $1"; PASS=$((PASS+1)); } +bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); } +skip() { echo " SKIP $1"; SKIP=$((SKIP+1)); } +info() { echo " info $1"; } +has() { [[ "$2" == *"$3"* ]] && ok "$1" || bad "$1 (got: ${2:0:150})"; } +eq() { [ "$2" = "$3" ] && ok "$1" || bad "$1 (got: ${2:0:150})"; } # exact match +inlog() { grep -qF -- "$2" "$1" 2>/dev/null; } # log file, text +cleanup() { for p in "${PIDS[@]}"; do kill "$p" 2>/dev/null; done; wait 2>/dev/null + for i in 1 2 3 4 5 6 53; do ip addr del 10.77.1.$i/32 dev lo 2>/dev/null; done; rm -rf "$WORK"; } +trap cleanup EXIT +for i in 1 2 3 4 5 6 53; do ip addr add 10.77.1.$i/32 dev lo 2>/dev/null; done + +# ---- udp support probe: does this Nexus copy handle UDP ASSOCIATE on Linux? +"$NODE" "$NEXUS_JS" -p 1099 >/dev/null 2>&1 & PROBE=$!; sleep 1.5 +UDPOK=$(python3 - <<'PY' +import socket +try: + c = socket.create_connection(("127.0.0.1", 1099), timeout=3); c.sendall(b"\x05\x01\x00"); c.recv(2) + c.sendall(b"\x05\x03\x00\x01\x00\x00\x00\x00\x00\x00"); r = c.recv(64); print("yes" if r[1] == 0 else "no") +except Exception: print("no") +PY +) +kill $PROBE 2>/dev/null; wait $PROBE 2>/dev/null +[ "$UDPOK" = yes ] || info "this Nexus copy cannot do UDP ASSOCIATE here (apply tests/nexus-linux-udp.patch): UDP tests will be skipped" + +python3 "$HERE/nexus_origins.py" > "$WORK/origins.log" 2>&1 & PIDS+=($!) +PB_DNS_ZONE="app.example.test=10.77.1.5;svc.corp.test=10.77.1.6" python3 "$HERE/fake_dns.py" 10.77.1.53 "$WORK/dns.log" >/dev/null 2>&1 & PIDS+=($!) +start_nx() { # port log [extra args] + local port=$1 log=$2; shift 2 + "$NODE" "$NEXUS_JS" -v -p "$port" "$@" > "$log" 2>&1 & NXPID=$!; PIDS+=($NXPID) + for _ in $(seq 40); do ss -ltn 2>/dev/null | grep -q ":$port " && return 0; sleep 0.25; done; echo "nexus on $port did not start"; cat "$log"; exit 1; } +NX1LOG="$WORK/nx1.log"; NX2LOG="$WORK/nx2.log" +start_nx 1080 "$NX1LOG"; NX1=$NXPID +start_nx 1081 "$NX2LOG" -u nx -P secret; NX2=$NXPID +sleep 1 +mkdir -p "$WORK/bin"; cp "$(command -v curl)" "$WORK/bin/wget" +read -r -d '' EXPECT_PY <<'PY' +import hashlib, sys +B = bytes((i * 7 + 13) & 255 for i in range(1 << 20)); h = hashlib.sha256() +for _ in range(int(sys.argv[1])): h.update(B) +print(h.hexdigest()) +PY +sha_for() { python3 -c "$EXPECT_PY" "$1"; } + +stdbuf -oL "$PB_DIR/ProxyBridge" --verbose 2 \ + --proxy n1=socks5://127.0.0.1:1080 --proxy n2=socks5://nx:secret@127.0.0.1:1081 \ + --rule "node:*:*:BOTH:DIRECT" \ + --rule "curl:10.77.1.3:*:TCP:DIRECT" \ + --rule "curl:10.77.1.4:*:TCP:BLOCK" \ + --rule "curl:10.77.1.1:18701-18703:TCP:PROXY=n2" \ + --rule "curl:10.77.1.1:18700:TCP:PROXY=n1" \ + --rule "curl:10.77.1.2:*:TCP:PROXY=n2" \ + --rule "wget:10.77.1.0/24:*:TCP:PROXY=n2" \ + --rule "python3*:10.77.1.1:18700:TCP:PROXY=n1" \ + --rule "*:*.example.test:*:TCP:PROXY=n2" \ + --rule "*:*.corp.test:*:TCP:PROXY=n1" \ + --rule "python3*:10.77.1.1:18710:UDP:PROXY=n1" \ + --rule "python3*:10.77.1.2:18710:UDP:PROXY=n2" \ + --rule "python3*:10.77.1.1:18711:UDP:PROXY=n1" \ + --rule "dig;python3*:10.77.1.53:53:UDP:PROXY=n1" \ + --rule "dig;python3*:10.77.1.53:53:TCP:PROXY=n1" \ + > "$WORK/pb.out" 2>&1 & PB=$!; PIDS+=($PB) +for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && break; sleep 0.25; done +grep -q "ProxyBridge started" "$WORK/pb.out" || { echo "ProxyBridge failed to start"; cat "$WORK/pb.out"; exit 1; } +sleep 1 +O() { curl -s -m 10 "$@"; } + +echo "== TCP: different rules -> different Nexus instances (real forwarding)" +out=$(O http://10.77.1.1:18700/); has "ip+port rule -> nexus n1 (open)" "$out" "origin-1" +inlog $NX1LOG "[CONNECT] 10.77.1.1:18700" && ok " n1 log shows the CONNECT to 10.77.1.1:18700" || bad " n1 never saw the connection" +out=$(O http://10.77.1.2:18700/); has "whole-IP rule -> nexus n2 (username/password)" "$out" "origin-2" +inlog $NX2LOG "[CONNECT] 10.77.1.2:18700" && ok " n2 log shows the CONNECT (auth succeeded)" || bad " n2 never saw the connection" +out=$(O http://10.77.1.1:18702/); has "port-range rule 18701-18703 -> n2" "$out" "origin-1-p18702" +inlog $NX2LOG "[CONNECT] 10.77.1.1:18702" && ok " n2 carried port 18702" || bad " n2 did not carry 18702" +out=$(O http://10.77.1.1:18704/); has "port outside every rule is direct (served)" "$out" "origin-1-p18704" +inlog $NX1LOG "10.77.1.1:18704" || inlog $NX2LOG "10.77.1.1:18704" && bad " 18704 leaked into a proxy" || ok " neither nexus saw 18704" +out=$(O http://10.77.1.3:18700/); has "DIRECT rule bypasses nexus" "$out" "origin-3" +inlog $NX1LOG "10.77.1.3" || inlog $NX2LOG "10.77.1.3" && bad " DIRECT leaked into a proxy" || ok " neither nexus saw 10.77.1.3" +out=$(O http://10.77.1.4:18700/ 2>&1; echo "rc=$?"); has "BLOCK rule rejects" "$out" "rc=7" +out=$("$WORK/bin/wget" -s -m 10 http://10.77.1.1:18704/); has "same destination, other process (wget) -> n2" "$out" "origin-1-p18704" +inlog $NX2LOG "[CONNECT] 10.77.1.1:18704" && ok " n2 saw wget's connection (curl's to the same address stayed direct)" || bad " wget was not proxied" + +echo "== Domain rules with a real resolver, DNS itself carried by Nexus" +# DNS over UDP needs UDP ASSOCIATE through nexus; with a Nexus copy that cannot do it, resolve over TCP instead +if [ "$UDPOK" = yes ]; then DIGMODE=""; DNSKIND="UDP"; else DIGMODE="+tcp"; DNSKIND="TCP"; fi +ip5=$(dig @10.77.1.53 app.example.test +short +time=3 +tries=1 $DIGMODE | head -1); eq "dig answer (${DNSKIND} DNS via nexus n1)" "$ip5" "10.77.1.5" +out=$(O http://10.77.1.5:18700/); [ -n "$out" ] || { echo " (empty answer; verbose retry:)"; curl -sv -m 10 http://10.77.1.5:18700/ 2>&1 | tail -12 | sed "s/^/ /"; } +has "*.example.test -> n2" "$out" "origin-5" +inlog $NX2LOG "[CONNECT] 10.77.1.5:18700" && ok " n2 carried the domain-matched connection" || bad " not via n2" +ip6=$(dig @10.77.1.53 +tcp svc.corp.test +short +time=3 +tries=1 | head -1); eq "dig answer (TCP DNS via nexus n1)" "$ip6" "10.77.1.6" +out=$(O http://10.77.1.6:18700/); has "*.corp.test -> n1" "$out" "origin-6" +inlog $NX1LOG "[CONNECT] 10.77.1.6:18700" && ok " n1 carried the domain-matched connection" || bad " not via n1" +inlog $NX1LOG "[CONNECT] 10.77.1.53:53" && ok " n1 carried the DNS-over-TCP query" || bad " DNS/TCP did not go through n1" +grep -q "10.77.1.5 (app.example.test)" "$WORK/pb.out" && ok " connection log names the domain" || bad " no domain in the connection log" + +echo "== TCP: many connections, big transfers, integrity" +cpids=(); for i in $(seq 1 100); do (O http://10.77.1.1:18700/ > "$WORK/p$i.out") & cpids+=($!); done; wait "${cpids[@]}" 2>/dev/null +okc=$(grep -l "origin-1" "$WORK"/p*.out 2>/dev/null | wc -l); [ "$okc" = 100 ] && ok "100 parallel connections through n1" || bad "only $okc/100 parallel connections succeeded" +fail=0; for i in $(seq 1 1500); do r=$(O http://10.77.1.1:18700/); [ "$r" = origin-1 ] || fail=$((fail+1)); done; [ "$fail" = 0 ] && ok "1500 sequential connect/close cycles through n1" || bad "$fail/1500 sequential requests failed" +MB=200; WANT=$(sha_for $MB) +t0=$(date +%s.%N); GOT=$(O -m 120 "http://10.77.1.1:18700/big?mb=$MB" | sha256sum | cut -d' ' -f1); t1=$(date +%s.%N) +[ "$GOT" = "$WANT" ] && ok "200 MB download through n1: checksum identical" || bad "200 MB download corrupted" +T_PB=$(python3 -c "print(round($MB/($t1-$t0)))") +t0=$(date +%s.%N); GOT=$(O -m 120 --socks5 127.0.0.1:1080 "http://10.77.1.1:18704/big?mb=$MB" | sha256sum | cut -d' ' -f1); t1=$(date +%s.%N) +T_SOCKS=$(python3 -c "print(round($MB/($t1-$t0)))"); [ "$GOT" = "$WANT" ] || bad "reference download via --socks5 corrupted" +t0=$(date +%s.%N); GOT=$(O -m 120 "http://10.77.1.1:18704/big?mb=$MB" | sha256sum | cut -d' ' -f1); t1=$(date +%s.%N) +T_DIRECT=$(python3 -c "print(round($MB/($t1-$t0)))") +info "throughput, 200 MB: direct ${T_DIRECT} MB/s | curl speaking SOCKS5 to nexus itself ${T_SOCKS} MB/s | curl -> ProxyBridge -> nexus ${T_PB} MB/s" +python3 -c "import sys; sys.exit(0 if $T_PB >= 0.8*$T_SOCKS else 1)" && ok "ProxyBridge in front of nexus is within 20% of the application speaking SOCKS5 itself" || bad "ProxyBridge overhead above 20% (${T_PB} vs ${T_SOCKS} MB/s)" +python3 - < "$WORK/up.bin" +import sys +B = bytes((i * 7 + 13) & 255 for i in range(1 << 20)); w = sys.stdout.buffer +for _ in range(100): w.write(B) +PY +WANT=$(sha_for 100); GOT=$(O -m 120 --data-binary @"$WORK/up.bin" http://10.77.1.2:18700/upload) +[ "$GOT" = "$WANT" ] && ok "100 MB upload through n2 (auth): origin received identical bytes" || bad "upload mismatch (${GOT:0:20})" +half=$(python3 - <<'PY' +import socket, struct +def exchange(s): + s.sendall(b"GET / HTTP/1.0\r\n\r\n"); s.shutdown(socket.SHUT_WR) # half-close, then read the answer + data = b"" + while True: + d = s.recv(4096) + if not d: break + data += d + return "origin-1" in data.decode() +# (a) through ProxyBridge: the python3 rule redirects this connection to nexus n1 +print("via-proxybridge:", exchange(socket.create_connection(("10.77.1.1", 18700), timeout=10))) +# (b) reference: the application speaks SOCKS5 to nexus itself (loopback is never redirected) +c = socket.create_connection(("127.0.0.1", 1080), timeout=10); c.sendall(b"\x05\x01\x00"); c.recv(2) +c.sendall(b"\x05\x01\x00\x01" + socket.inet_aton("10.77.1.1") + struct.pack("!H", 18700)); c.recv(10) +print("direct-socks:", exchange(c)) +PY +) +echo "$half" | sed 's/^/ /' +if [[ "$half" == *"direct-socks: True"* ]]; then has "half-close through ProxyBridge (client FIN, response still delivered)" "$half" "via-proxybridge: True" +else [[ "$half" == *"via-proxybridge: False"* ]] && ok "half-close: identical to the application speaking SOCKS5 itself (nexus does not support half-open connections: Node allowHalfOpen=false)" || bad "half-close differs from the reference ($half)"; fi + +echo "== UDP through nexus" +if [ "$UDPOK" = yes ]; then +out=$(python3 - <<'PY' +import socket, time, concurrent.futures as cf +def sock(t=4): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(t); return s +s = sock() +for dst, tag in [(("10.77.1.1", 18710), "U1:"), (("10.77.1.2", 18710), "U2:"), (("10.77.1.1", 18710), "U1:")]: + s.sendto(b"hello", dst); d, a = s.recvfrom(100) + print("one-socket", dst[0], "->", d.decode(), "from", a[0] + ":" + str(a[1])) +c = sock(); c.connect(("10.77.1.1", 18711)); c.send(b"conn"); d = c.recv(100) +print("connected", d.decode(), "peer", "%s:%d" % c.getpeername()) +big = bytes(range(256)) * 234 # 59,904 bytes +b = sock(8); b.sendto(big, ("10.77.1.1", 18710)); d, a = b.recvfrom(70000) +print("big", "ok" if d == b"U1:" + big else "MISMATCH %d" % len(d)) +def one(i): + k = sock(8); k.sendto(b"n%d" % i, ("10.77.1.%d" % (1 + i % 2), 18710)); d, a = k.recvfrom(100) + return d.endswith(b"n%d" % i) and a[0] == "10.77.1.%d" % (1 + i % 2) +with cf.ThreadPoolExecutor(32) as ex: r = list(ex.map(one, range(300))) +print("concurrent", sum(r), "/ 300") +# sustained stream: 6000 datagrams of 1000 bytes at ~3000 pps, receiving while sending +import threading +q = sock(1); q.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 << 20); got = set(); sent = 6000; stop = threading.Event() +def rx(): + q.settimeout(0.2) + while not stop.is_set(): + try: + d, a = q.recvfrom(2000); got.add(int.from_bytes(d[3:7], "big")) + except socket.timeout: pass +th = threading.Thread(target=rx, daemon=True); th.start(); t0 = time.perf_counter() +for i in range(sent): + q.sendto(i.to_bytes(4, "big") + b"x" * 996, ("10.77.1.1", 18710)) + while time.perf_counter() < t0 + (i + 1) / 3000.0: time.sleep(0) +time.sleep(2); stop.set(); th.join() +print("stream", len(got), "/", sent, "replies") +# round-trip latency of small datagrams +l = sock(2); ts = [] +for i in range(1500): + t = time.perf_counter(); l.sendto(b"p", ("10.77.1.1", 18710)); l.recvfrom(100); ts.append(time.perf_counter() - t) +ts.sort(); print("rtt p50=%dus p99=%dus" % (ts[len(ts)//2]*1e6, ts[int(len(ts)*.99)]*1e6)) +PY +) +echo "$out" | sed 's/^/ /' +has "UDP one socket -> nexus n1 (destination 1)" "$out" "one-socket 10.77.1.1 -> U1:hello from 10.77.1.1:18710" +has "same socket -> nexus n2 with auth (destination 2)" "$out" "one-socket 10.77.1.2 -> U2:hello from 10.77.1.2:18710" +has "connected UDP socket, getpeername restored" "$out" "connected U3:conn peer 10.77.1.1:18711" +has "59,904-byte datagram round trip through nexus" "$out" "big ok" +has "300 concurrent UDP sockets across both proxies" "$out" "concurrent 300 / 300" +n=$(grep -o "stream [0-9]*" <<<"$out" | grep -o "[0-9]*"); [ "${n:-0}" -ge 5940 ] && ok "sustained stream: ${n}/6000 datagrams echoed (>= 99%)" || bad "stream lost too much (${n:-0}/6000)" +inlog $NX1LOG "10.77.1.1:18710" && inlog $NX2LOG "10.77.1.2:18710" && ok " both nexus logs list their UDP destinations" || bad " nexus logs lack the UDP destinations" +dq=$(dig @10.77.1.53 app.example.test +short +time=3 +tries=1 | head -1); eq "UDP DNS through nexus still answers" "$dq" "10.77.1.5" +else + skip "UDP tests (stock Nexus cannot do UDP ASSOCIATE on Linux; apply tests/nexus-linux-udp.patch)" +fi + +echo "== resilience: proxy dies and comes back" +if [ "$UDPOK" = yes ]; then + python3 - <<'PY' > "$WORK/udp_pre.out" +import socket +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(4); s.sendto(b"x", ("10.77.1.1", 18710)); print(s.recv(10).decode()) +PY +fi +kill $NX1; wait $NX1 2>/dev/null; sleep 1 +out=$(O -m 4 http://10.77.1.1:18700/ 2>&1; echo "rc=$?"); [[ "$out" != *origin-1* ]] && ok "with nexus n1 down the request fails (no leak around the proxy)" || bad "request bypassed the dead proxy" +start_nx 1080 "$NX1LOG.2"; NX1=$NXPID; sleep 1 +out=$(O http://10.77.1.1:18700/); has "TCP works again after nexus n1 restarts" "$out" "origin-1" +if [ "$UDPOK" = yes ]; then + rec=$(python3 - <<'PY' +import socket, time +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(1); t0 = time.time() +while time.time() - t0 < 12: + try: + s.sendto(b"again", ("10.77.1.1", 18710)); d, a = s.recvfrom(100); print("recovered in %.1fs: %s" % (time.time() - t0, d.decode())); break + except socket.timeout: pass +else: print("NOT RECOVERED") +PY + ); has "UDP session re-associates after nexus restarts" "$rec" "recovered in"; info "$rec" +fi +kill $NX2; wait $NX2 2>/dev/null; sleep 1 +out=$(O -m 4 http://10.77.1.2:18700/ 2>&1; echo "rc=$?"); [[ "$out" != *origin-2* ]] && ok "with n2 down its rule fails closed (nothing goes direct)" || bad "n2 rule leaked to direct" + +echo; echo "passed=$PASS failed=$FAIL skipped=$SKIP" +[ "$FAIL" = 0 ] || { echo "---- pb output"; tail -25 "$WORK/pb.out"; echo "---- nx1"; tail -8 "$NX1LOG"; echo "---- nx2"; tail -8 "$NX2LOG"; exit 1; } diff --git a/Linux/tests/fake_dns.py b/Linux/tests/fake_dns.py index ab8e85a4..d0ef5b2b 100644 --- a/Linux/tests/fake_dns.py +++ b/Linux/tests/fake_dns.py @@ -22,6 +22,11 @@ "svc.byname.test": [("A", "192.0.2.81")], "plain.other.test": [("A", "192.0.2.82")], } +# extra records for tests: PB_DNS_ZONE="name=ip;name2=ip2" +import os +for item in filter(None, os.environ.get("PB_DNS_ZONE", "").split(";")): + n, ip = item.split("=", 1) + ZONE[n.strip().lower()] = [("A", ip.strip())] LOG = open(sys.argv[2], "a", buffering=1) diff --git a/Linux/tests/nexus-linux-udp.patch b/Linux/tests/nexus-linux-udp.patch new file mode 100644 index 00000000..3b721de1 --- /dev/null +++ b/Linux/tests/nexus-linux-udp.patch @@ -0,0 +1,25 @@ +--- a/script.js ++++ b/script.js +@@ -301,7 +301,7 @@ + this.handleUDPMessage(msg, rinfo, udpRelay, remoteMap); + }); + +- udpRelay.bind(0, '0.0.0.0', () => { ++ udpRelay.bind(0, '::', () => { + const udpPort = udpRelay.address().port; + + this.udpClients.set(clientKey, { +@@ -428,10 +428,12 @@ + console.log(` └─> ${host}:${port}${protocol}`); + } + +- udpRelay.send(data, port, host); ++ // the relay socket is IPv6 dual-stack: IPv4 destinations must be addressed as ::ffff:a.b.c.d ++ udpRelay.send(data, port, host.includes(':') ? host : '::ffff:' + host); + } + + handleRemoteToClient(msg, rinfo, udpRelay, remoteMap) { ++ rinfo = { ...rinfo, address: rinfo.address.replace(/^::ffff:/i, '') }; + const remoteKey = `${rinfo.address}:${rinfo.port}`; + const clientInfo = remoteMap.get(remoteKey); + diff --git a/Linux/tests/nexus_origins.py b/Linux/tests/nexus_origins.py new file mode 100644 index 00000000..2d154041 --- /dev/null +++ b/Linux/tests/nexus_origins.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Real origin servers for the Nexus Proxy end-to-end tests (they actually serve data, nothing is faked on the proxy side). + +usage: nexus_origins.py (binds the addresses below; add them to `lo` first) + HTTP 10.77.1.1..6:18700 "origin-1".."origin-6" + 10.77.1.1:18701..18705 "origin-1-p" + GET /big?mb=N -> N MiB of a deterministic pattern (see PATTERN) + POST /upload -> sha256 of the request body + GET /slowclose -> response, then the server half-closes and waits for the client to close + UDP 10.77.1.1:18710 "U1:" 10.77.1.2:18710 "U2:" 10.77.1.1:18711 "U3:" echo prefixed with the tag +""" +import hashlib, http.server, socket, socketserver, threading, sys, time + +BLOCK = bytes((i * 7 + 13) & 255 for i in range(1 << 20)) # 1 MiB pattern block + + +class H(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.0" + tag = "origin" + + def do_GET(self): + if self.path.startswith("/big"): + mb = int(self.path.split("mb=")[1]) + self.send_response(200); self.send_header("Content-Length", str(mb << 20)); self.end_headers() + for _ in range(mb): + self.wfile.write(BLOCK) + else: + body = (self.tag + "\n").encode() + self.send_response(200); self.send_header("Content-Length", str(len(body))); self.end_headers(); self.wfile.write(body) + + def do_POST(self): + n = int(self.headers.get("Content-Length", "0")); h = hashlib.sha256(); left = n + while left: + b = self.rfile.read(min(1 << 20, left)) + if not b: + break + h.update(b); left -= len(b) + body = h.hexdigest().encode() + self.send_response(200); self.send_header("Content-Length", str(len(body))); self.end_headers(); self.wfile.write(body) + + def log_message(self, *a): + pass + + +class TS(socketserver.ThreadingMixIn, http.server.HTTPServer): + allow_reuse_address = True + daemon_threads = True + request_queue_size = 1024 + + +def http(ip, port, tag): + handler = type("H_" + tag, (H,), {"tag": tag}) + srv = TS((ip, port), handler) + threading.Thread(target=srv.serve_forever, daemon=True).start() + + +def udp(ip, port, tag): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 << 20) + s.bind((ip, port)) + def loop(): + while True: + d, a = s.recvfrom(70000) + try: + s.sendto(tag.encode() + d, a) + except OSError: + pass + threading.Thread(target=loop, daemon=True).start() + + +for i in (1, 2, 3, 4, 5, 6): + http("10.77.1.%d" % i, 18700, "origin-%d" % i) +for p in range(18701, 18706): + http("10.77.1.1", p, "origin-1-p%d" % p) +udp("10.77.1.1", 18710, "U1:"); udp("10.77.1.2", 18710, "U2:"); udp("10.77.1.1", 18711, "U3:") +print("origins up", flush=True) +while True: + time.sleep(3600) From 29a01c0bde69ca2edab79ff2abe4a30a1b0e5d59 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 14:23:27 +0530 Subject: [PATCH 15/25] docs(linux): the minimum kernel is 5.11, not 5.8 The eBPF program calls bpf_get_current_task_btf(), added in Linux 5.11, to read the calling executable. Correct the README, the public header comment and the load-failure message. --- Linux/README.md | 6 +++--- Linux/cli/main.c | 2 +- Linux/gui/main.c | 4 ++-- Linux/src/ProxyBridge.h | 2 +- Linux/src/pb_bpf.c | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Linux/README.md b/Linux/README.md index f41cb75e..f2c2353e 100644 --- a/Linux/README.md +++ b/Linux/README.md @@ -208,7 +208,7 @@ EXAMPLES: --rule firefox:*:*:BOTH:PROXY --rule curl:*:*:TCP:PROXY=work --verbose 2 NOTE: - ProxyBridge needs root and a Linux kernel >= 5.8 with BTF and cgroup v2 (eBPF datapath). + ProxyBridge needs root and a Linux kernel >= 5.11 with BTF and cgroup v2 (eBPF datapath). Credentials given on the command line are visible to other users via ps. ``` @@ -293,7 +293,7 @@ Add as many `--proxy` entries (CLI) or proxies (GUI) as you need; each rule choo ## Current Limitations -- **Needs a recent kernel**: Linux 5.8+ with BTF (`/sys/kernel/btf/vmlinux`) and cgroup v2 mounted (default on current Ubuntu, Debian, Fedora, Arch). Older kernels are not supported +- **Needs a recent kernel**: Linux 5.11+ with BTF (`/sys/kernel/btf/vmlinux`) and cgroup v2 mounted (default on current Ubuntu, Debian, Fedora, Arch). Older kernels are not supported - **Root privileges required** - loading the eBPF programs needs root (CAP_BPF/CAP_SYS_ADMIN) - **Native IPv6 UDP to a PROXY rule fails closed** - an unconnected UDP socket sending to a native IPv6 address cannot be redirected (kernel limitation); the `sendto()` returns `EPERM` and applications fall back to IPv4/TCP. Connected IPv6 UDP sockets, IPv6 TCP and UDP to IPv4 destinations through dual-stack sockets all work - **IPv4 is the fully supported path**; IPv6 (TCP, connected UDP) works for IP rules but has no domain matching and the limits above @@ -371,7 +371,7 @@ ProxyBridge hooks the **socket calls** (`connect()`, `sendmsg()`, `recvmsg()`) w ### Requirements -- Linux kernel 5.8+ with BTF and cgroup v2 (to run); GCC, Make, **clang** and **bpftool** (to build the eBPF object and skeleton) +- Linux kernel 5.11+ with BTF and cgroup v2 (to run); GCC, Make, **clang** and **bpftool** (to build the eBPF object and skeleton) - Development libraries: - libbpf-dev (Debian/Ubuntu) or libbpf-devel (Fedora/RHEL) - libgtk-3-dev (optional, for GUI) or gtk3-devel (Fedora/RHEL) diff --git a/Linux/cli/main.c b/Linux/cli/main.c index f0616911..bc33c03f 100644 --- a/Linux/cli/main.c +++ b/Linux/cli/main.c @@ -119,7 +119,7 @@ static void show_help(const char* prog) printf(" --rule firefox:*:*:BOTH:PROXY --rule curl:*:*:TCP:PROXY=work --verbose 2\n\n"); printf("NOTE:\n"); - printf(" ProxyBridge needs root and a Linux kernel >= 5.8 with BTF and cgroup v2 (eBPF datapath).\n"); + printf(" ProxyBridge needs root and a Linux kernel >= 5.11 with BTF and cgroup v2 (eBPF datapath).\n"); printf(" Credentials given on the command line are visible to other users via ps.\n\n"); } diff --git a/Linux/gui/main.c b/Linux/gui/main.c index fc7d6b05..9887cc0b 100644 --- a/Linux/gui/main.c +++ b/Linux/gui/main.c @@ -201,7 +201,7 @@ static void on_about(GtkWidget *widget, gpointer data) { "ProxyBridge\n" "Version %s\n\n" "Universal proxy client for Linux applications\n" - "eBPF backend (needs root, kernel 5.8+ with BTF and cgroup v2)\n\n" + "eBPF backend (needs root, kernel 5.11+ with BTF and cgroup v2)\n\n" "Author: Sourav Kalal / InterceptSuite\n\n" "Website: interceptsuite.com\n" "GitHub: InterceptSuite/ProxyBridge\n\n" @@ -241,7 +241,7 @@ static bool start_engine() { show_message(GTK_WINDOW(window), GTK_MESSAGE_ERROR, "Failed to start the ProxyBridge engine.\n\n" "Open the \"Activity Logs\" tab to see the reason.\n" - "The eBPF backend needs root, a kernel >= 5.8 with BTF and cgroup v2."); + "The eBPF backend needs root, a kernel >= 5.11 with BTF and cgroup v2."); return false; } diff --git a/Linux/src/ProxyBridge.h b/Linux/src/ProxyBridge.h index dc8b8637..a03ed15e 100644 --- a/Linux/src/ProxyBridge.h +++ b/Linux/src/ProxyBridge.h @@ -73,7 +73,7 @@ void ProxyBridge_SetLogCallback(LogCallback callback); void ProxyBridge_SetConnectionCallback(ConnectionCallback callback); void ProxyBridge_SetTrafficLoggingEnabled(bool enable); void ProxyBridge_ClearConnectionLogs(void); -bool ProxyBridge_Start(void); /* needs root (CAP_BPF/CAP_SYS_ADMIN) and a BTF kernel >= 5.8; returns false and logs why on failure */ +bool ProxyBridge_Start(void); /* needs root (CAP_BPF/CAP_SYS_ADMIN) and a BTF kernel >= 5.11; returns false and logs why on failure */ bool ProxyBridge_Stop(void); /* keeps rules and proxies; Start() may be called again */ int ProxyBridge_TestConnection(const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); int ProxyBridge_TestProxy(uint32_t proxy_id, const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); diff --git a/Linux/src/pb_bpf.c b/Linux/src/pb_bpf.c index 7dedb853..d4bf74f3 100644 --- a/Linux/src/pb_bpf.c +++ b/Linux/src/pb_bpf.c @@ -138,7 +138,7 @@ int pb_bpf_start(const struct pb_bpf_cfg *cfg, pb_event_cb cb) bpf_program__set_autoload(G.skel->progs.pb_getpeername6, false); err = proxybridge_bpf__load(G.skel); } - if (err || !G.skel) { pb_core_log("loading the BPF programs failed (kernel needs BTF and cgroup sock_addr support, >= 5.8): %s", strerror(-err ? -err : errno)); goto out; } + if (err || !G.skel) { pb_core_log("loading the BPF programs failed (kernel needs BTF and cgroup sock_addr support, >= 5.11): %s", strerror(-err ? -err : errno)); goto out; } } { // sockets of other network namespaces (containers) must not be redirected to our loopback relay From 5cedb7b22f10b54f3059523a2faf77ddbf8591d8 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 15:47:28 +0530 Subject: [PATCH 16/25] fix(linux): bind the relay hand-off to the client address, exempt local proxies, slim the datapath Security - The kernel now keys a pending TCP flow by the client's source address AND port; the relay passes both when it claims the flow. A process connecting from another loopback address (e.g. 127.0.0.2) can no longer take another process's destination mapping. Behaviour - Processes that listen on a configured local proxy address are exempted automatically (per process, refreshed every 2 s), so a wildcard-process rule can no longer send a proxy's own connections back through itself. - A UDP rule that points at an HTTP proxy (which cannot carry UDP) is logged with the rule id and proxy name instead of silently going direct. - /etc/hosts is reloaded when it changes. Robustness - Shutdown detaches the hooks first and frees the BPF maps only after the relay has stopped; relay threads could previously touch freed maps. - Executable paths are stored with their full length (a fixed 240-byte buffer could truncate and misclassify a long path). - The skeleton header is generated atomically so a failed bpftool run cannot leave an empty file behind. Leaner code - Config lives in BPF global data (no map lookup per field), the never-read stats map is gone, IPv4/IPv6 hooks share their helpers, cached rule views replace per-decision allocation, the event reader sleeps instead of polling, the library exports only the 23 ProxyBridge_* functions, unused helpers and interface parameters are removed, comments are cut to the non-obvious ones. Compiled datapath is ~7% smaller. - Makefile accepts EXTRA_CFLAGS/EXTRA_LDFLAGS for sanitizer builds. --- Linux/src/Makefile | 11 +- Linux/src/ProxyBridge.c | 572 +++++++++++++++++++++----------- Linux/src/ProxyBridge.h | 48 +-- Linux/src/bpf/pb_bpf_shared.h | 61 ++-- Linux/src/bpf/proxybridge.bpf.c | 252 ++++++-------- Linux/src/pb_bpf.c | 449 +++++++++++++------------ Linux/src/pb_bpf.h | 31 +- Linux/src/pb_dns.c | 5 - Linux/src/pb_internal.h | 99 ++---- Linux/src/pb_proc.c | 77 +++++ Linux/src/pb_proc.h | 15 + Linux/src/pb_rules.c | 44 +-- Linux/src/pb_rules.h | 1 - Linux/src/tests/rules_test.c | 3 - 14 files changed, 926 insertions(+), 742 deletions(-) create mode 100644 Linux/src/pb_proc.c create mode 100644 Linux/src/pb_proc.h diff --git a/Linux/src/Makefile b/Linux/src/Makefile index 6c1b4826..7c9d5e88 100644 --- a/Linux/src/Makefile +++ b/Linux/src/Makefile @@ -7,6 +7,9 @@ MULTIARCH := $(shell $(CC) -print-multiarch) # build.sh points it at a temporary directory; a plain `make` builds in place for quick development. BUILD_DIR ?= . +# EXTRA_CFLAGS / EXTRA_LDFLAGS: e.g. -fsanitize=address,undefined (use STRIP_FLAG= to keep symbols for the report). +STRIP_FLAG ?= -s + CFLAGS = -Wall -Wextra -O2 -fPIC -D_GNU_SOURCE -I$(BUILD_DIR) \ -fstack-protector-strong \ -D_FORTIFY_SOURCE=2 \ @@ -15,11 +18,11 @@ CFLAGS = -Wall -Wextra -O2 -fPIC -D_GNU_SOURCE -I$(BUILD_DIR) \ -Werror=format-security \ -fno-strict-overflow \ -fno-delete-null-pointer-checks \ - -fwrapv -LDFLAGS = -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s + -fwrapv -fvisibility=hidden -ffunction-sections -fdata-sections $(EXTRA_CFLAGS) +LDFLAGS = -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack -Wl,--gc-sections $(STRIP_FLAG) $(EXTRA_LDFLAGS) LIBS = -lpthread -lbpf -SOURCES = ProxyBridge.c pb_rules.c pb_dns.c pb_bpf.c pb_relay.c +SOURCES = ProxyBridge.c pb_rules.c pb_dns.c pb_proc.c pb_bpf.c pb_relay.c OBJECTS = $(addprefix $(BUILD_DIR)/,$(SOURCES:.c=.o)) TARGET = $(BUILD_DIR)/libproxybridge.so BPF_OBJ = $(BUILD_DIR)/proxybridge.bpf.o @@ -34,7 +37,7 @@ $(BPF_OBJ): bpf/proxybridge.bpf.c bpf/pb_bpf_shared.h | $(BUILD_DIR) $(CLANG) -O2 -g -target bpf -D__BPF__ -I/usr/include/$(MULTIARCH) -c $< -o $@ $(SKEL): $(BPF_OBJ) - $(BPFTOOL) gen skeleton $< > $@ + $(BPFTOOL) gen skeleton $< > $@.tmp && mv $@.tmp $@ $(BUILD_DIR)/pb_bpf.o: $(SKEL) diff --git a/Linux/src/ProxyBridge.c b/Linux/src/ProxyBridge.c index 2cbb5a92..2d19db13 100644 --- a/Linux/src/ProxyBridge.c +++ b/Linux/src/ProxyBridge.c @@ -1,33 +1,45 @@ #include #include -#include +#include #include #include +#include #include #include #include +#include +#include #include #include "ProxyBridge.h" -#include #include "pb_bpf.h" #include "pb_dns.h" #include "pb_internal.h" +#include "pb_proc.h" #include "pb_rules.h" #define MAX_PROXIES 255 +#define MAINT_PERIOD_S 2 +#define HOSTS_PATH "/etc/hosts" static struct { pthread_rwlock_t lock; struct pb_rule *rules; size_t nrules, caprules; + struct pb_rule_view *views; size_t nviews; struct pb_proxy proxies[MAX_PROXIES]; size_t nproxies; uint32_t next_rule_id, next_proxy_id, default_proxy; - bool dns_via_proxy, traffic_logging, running; - atomic_bool has_domain_rules; + bool running; + atomic_bool dns_via_proxy, traffic_logging, has_domain_rules; + pthread_t maint; + bool maint_started, maint_stop, maint_kick; + pthread_mutex_t maint_mu; + pthread_cond_t maint_cv; + uint64_t warned[16]; size_t nwarned; LogCallback log_cb; ConnectionCallback conn_cb; -} G = { .lock = PTHREAD_RWLOCK_INITIALIZER, .next_rule_id = 1, .next_proxy_id = 1, .dns_via_proxy = true }; +} G = { .lock = PTHREAD_RWLOCK_INITIALIZER, .maint_mu = PTHREAD_MUTEX_INITIALIZER, .maint_cv = PTHREAD_COND_INITIALIZER, + .next_rule_id = 1, .next_proxy_id = 1, .dns_via_proxy = true }; -static pthread_mutex_t cb_lock = PTHREAD_MUTEX_INITIALIZER; // serialises user callbacks +static pthread_mutex_t cb_lock = PTHREAD_MUTEX_INITIALIZER; static void set_err(char *err, size_t n, const char *fmt, ...) __attribute__((format(printf, 3, 4))); static void set_err(char *err, size_t n, const char *fmt, ...) @@ -39,7 +51,6 @@ static void set_err(char *err, size_t n, const char *fmt, ...) va_end(ap); } -// ---- logging ----------------------------------------------------------------------------------------------------------- void pb_core_log(const char *fmt, ...) { char buf[1024]; @@ -48,73 +59,75 @@ void pb_core_log(const char *fmt, ...) vsnprintf(buf, sizeof buf, fmt, ap); va_end(ap); pthread_mutex_lock(&cb_lock); - LogCallback cb = G.log_cb; - if (cb) cb(buf); + if (G.log_cb) G.log_cb(buf); pthread_mutex_unlock(&cb_lock); } -static const char *proxy_type_name(ProxyType t) { return t == PROXY_TYPE_SOCKS5 ? "SOCKS5" : "HTTP"; } +static void strip_deleted(char *p) +{ + static const char suffix[] = " (deleted)"; + size_t n = strlen(p), s = sizeof suffix - 1; + if (n > s && !strcmp(p + n - s, suffix)) p[n - s] = 0; +} -static void proc_name_of(uint32_t pid, const char *comm, char *out, size_t n) +static bool exe_path_of_pid(uint32_t pid, char *out, size_t n) { - char lnk[64], path[512]; - snprintf(lnk, sizeof lnk, "/proc/%u/exe", pid); - ssize_t r = readlink(lnk, path, sizeof path - 1); - if (r > 0) { - path[r] = 0; - const char *b = strrchr(path, '/'); - snprintf(out, n, "%s", b ? b + 1 : path); - size_t l = strlen(out); - if (l > 10 && !strcmp(out + l - 10, " (deleted)")) out[l - 10] = 0; - return; - } - snprintf(out, n, "%s", comm && *comm ? comm : "unknown"); + char link[64]; + snprintf(link, sizeof link, "/proc/%u/exe", pid); + ssize_t r = readlink(link, out, n - 1); + if (r <= 0) { out[0] = 0; return false; } + out[r] = 0; + strip_deleted(out); + return true; } static void emit_connection(uint32_t pid, const char *comm, int family, const uint8_t addr[16], uint16_t port, int action, uint32_t proxy_id) { - if (!G.traffic_logging) return; - char name[512], ip[INET6_ADDRSTRLEN + 280], info[400]; - proc_name_of(pid, comm, name, sizeof name); + if (!atomic_load(&G.traffic_logging)) return; + char path[4096], dest[INET6_ADDRSTRLEN + 280], info[400]; + const char *name = comm && *comm ? comm : "unknown"; + if (exe_path_of_pid(pid, path, sizeof path)) { + const char *base = strrchr(path, '/'); + name = base ? base + 1 : path; + } if (family == AF_INET) { - inet_ntop(AF_INET, addr + 12, ip, sizeof ip); - uint32_t h = ((uint32_t)addr[12] << 24) | ((uint32_t)addr[13] << 16) | ((uint32_t)addr[14] << 8) | addr[15]; - char nm[1][256]; - if (pb_dns_lookup(h, nm, 1) == 1) { // show the domain the application asked for, when we know it - size_t l = strlen(ip); - snprintf(ip + l, sizeof ip - l, " (%.*s)", (int)(sizeof ip - l - 4), nm[0]); + inet_ntop(AF_INET, addr + 12, dest, sizeof dest); + uint32_t ip = ((uint32_t)addr[12] << 24) | ((uint32_t)addr[13] << 16) | ((uint32_t)addr[14] << 8) | addr[15]; + char domain[1][256]; + if (pb_dns_lookup(ip, domain, 1) == 1) { + size_t l = strlen(dest); + snprintf(dest + l, sizeof dest - l, " (%.*s)", (int)(sizeof dest - l - 4), domain[0]); } - } else inet_ntop(AF_INET6, addr, ip, sizeof ip); - if (action == PB_ACT_PROXY) { - struct pb_proxy p; - if (pb_core_get_proxy(proxy_id, &p) == 0) snprintf(info, sizeof info, "%s %s:%u", proxy_type_name(p.type), p.host, p.port); - else snprintf(info, sizeof info, "PROXY"); - } else snprintf(info, sizeof info, "%s", action == PB_ACT_BLOCK ? "BLOCK" : "DIRECT"); + } else { + inet_ntop(AF_INET6, addr, dest, sizeof dest); + } + struct pb_proxy p; + if (action == PB_ACT_PROXY && pb_core_get_proxy(proxy_id, &p) == 0) + snprintf(info, sizeof info, "%s %s:%u", p.type == PROXY_TYPE_SOCKS5 ? "SOCKS5" : "HTTP", p.host, p.port); + else + snprintf(info, sizeof info, "%s", action == PB_ACT_PROXY ? "PROXY" : action == PB_ACT_BLOCK ? "BLOCK" : "DIRECT"); pthread_mutex_lock(&cb_lock); - ConnectionCallback cb = G.conn_cb; - if (cb) cb(name, pid, ip, port, info); + if (G.conn_cb) G.conn_cb(name, pid, dest, port, info); pthread_mutex_unlock(&cb_lock); } static void on_bpf_event(const struct pb_event *e) { char comm[17]; - memcpy(comm, e->comm, 16); comm[16] = 0; + memcpy(comm, e->comm, 16); + comm[16] = 0; emit_connection(e->pid, comm, e->family, e->addr, e->port, e->action, e->proxy_id); } -void pb_core_flow_event(const struct pb_orig *o, int action, const char *proxy_desc) +void pb_core_flow_event(const struct pb_orig *o, int action) { - (void)proxy_desc; emit_connection(o->pid, NULL, o->family, o->addr, o->port, action, o->proxy_id); } -bool pb_core_dns_via_proxy(void) { return G.dns_via_proxy; } - -// ---- proxies ------------------------------------------------------------------------------------------------------------- static struct pb_proxy *find_proxy_locked(uint32_t id) { - for (size_t i = 0; i < G.nproxies; i++) if (G.proxies[i].id == id) return &G.proxies[i]; + for (size_t i = 0; i < G.nproxies; i++) + if (G.proxies[i].id == id) return &G.proxies[i]; return NULL; } @@ -124,49 +137,40 @@ static struct pb_proxy *default_proxy_locked(void) return p ? p : (G.nproxies ? &G.proxies[0] : NULL); } +static struct pb_proxy *proxy_for_rule_locked(const struct pb_rule *r) +{ + struct pb_proxy *p = r->proxy_id ? find_proxy_locked(r->proxy_id) : NULL; + return p ? p : default_proxy_locked(); +} + int pb_core_get_proxy(uint32_t id, struct pb_proxy *out) { - int rc = -1; pthread_rwlock_rdlock(&G.lock); struct pb_proxy *p = id ? find_proxy_locked(id) : NULL; if (!p) p = default_proxy_locked(); - if (p) { *out = *p; rc = 0; } + if (p) *out = *p; pthread_rwlock_unlock(&G.lock); - return rc; + return p ? 0 : -1; } -static bool valid_proxy(const char *host, uint16_t port, char *err, size_t en) +static void maint_kick(void) { - if (!host || !*host) { set_err(err, en, "proxy host is empty"); return false; } - if (strlen(host) > 253) { set_err(err, en, "proxy host too long"); return false; } - for (const char *c = host; *c; c++) - if (isspace((unsigned char)*c) || *c == '/' || *c == '@') { set_err(err, en, "proxy host contains invalid characters"); return false; } - if (!port) { set_err(err, en, "proxy port must be 1-65535"); return false; } - return true; + pthread_mutex_lock(&G.maint_mu); + G.maint_kick = true; + pthread_cond_signal(&G.maint_cv); + pthread_mutex_unlock(&G.maint_mu); } -static void fill_proxy(struct pb_proxy *p, const char *name, ProxyType type, const char *host, uint16_t port, const char *user, const char *pass) -{ - snprintf(p->name, sizeof p->name, "%s", name && *name ? name : host); - p->type = type == PROXY_TYPE_HTTP ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; - snprintf(p->host, sizeof p->host, "%s", host); - p->port = port; - snprintf(p->user, sizeof p->user, "%s", user ? user : ""); - snprintf(p->pass, sizeof p->pass, "%s", pass ? pass : ""); -} - -// ---- DNS tap: arm kernel rules for addresses whose names match domain rules --------------------------------------------------- -// Caller holds G.lock (read or write). +// Domain rules: arm the kernel for addresses whose DNS name matches a rule. Caller holds G.lock. static void arm_ip_locked(uint32_t ip, const char *const *names, int nnames) { for (size_t i = 0; i < G.nrules; i++) { const struct pb_rule *r = &G.rules[i]; - if (!r->enabled || !r->ndomains) continue; - for (int d = 0; d < r->ndomains; d++) { - bool hit = false; + if (!r->enabled) continue; + bool hit = false; + for (int d = 0; d < r->ndomains && !hit; d++) for (int k = 0; k < nnames && !hit; k++) hit = pb_domain_match(r->domains[d], names[k]); - if (hit) { pb_bpf_dom_set((uint32_t)i, ip); break; } - } + if (hit) pb_bpf_dom_set((uint32_t)i, ip); } } @@ -198,100 +202,219 @@ void pb_core_dns_observe(const uint8_t *msg, size_t len) pthread_rwlock_unlock(&G.lock); } -// ---- rule -> BPF sync ------------------------------------------------------------------------------------------------------ -static struct pb_rule_view *build_views_locked(size_t *n) +static void warn_udp_over_http_locked(void) { - struct pb_rule_view *v = calloc(G.nrules ? G.nrules : 1, sizeof *v); - if (!v) return NULL; for (size_t i = 0; i < G.nrules; i++) { const struct pb_rule *r = &G.rules[i]; - v[i].rule = r; - v[i].action = r->action; - if (r->action == RULE_ACTION_PROXY) { - struct pb_proxy *p = r->proxy_id ? find_proxy_locked(r->proxy_id) : NULL; - if (!p) p = default_proxy_locked(); - if (!p) v[i].action = RULE_ACTION_BLOCK; // no usable proxy: fail closed - else { v[i].proxy_id = (uint16_t)p->id; v[i].udp_direct = p->type == PROXY_TYPE_HTTP; } - } + if (!r->enabled || r->action != RULE_ACTION_PROXY || r->protocol == RULE_PROTOCOL_TCP) continue; + const struct pb_proxy *p = proxy_for_rule_locked(r); + if (!p || p->type != PROXY_TYPE_HTTP) continue; + uint64_t key = ((uint64_t)r->id << 32) | p->id; + bool seen = false; + for (size_t k = 0; k < G.nwarned && !seen; k++) seen = G.warned[k] == key; + if (seen) continue; + if (G.nwarned < sizeof G.warned / sizeof *G.warned) G.warned[G.nwarned++] = key; + pb_core_log("rule %u sends UDP to HTTP proxy '%s', which cannot carry UDP: that UDP traffic goes DIRECT", r->id, p->name); } - *n = G.nrules; - return v; } -static void sync_locked(void) +static void rebuild_views_locked(void) { - if (!G.running) return; - size_t n; - struct pb_rule_view *v = build_views_locked(&n); - bool dom = false; - for (size_t i = 0; i < G.nrules; i++) dom |= G.rules[i].enabled && G.rules[i].ndomains > 0; - atomic_store(&G.has_domain_rules, dom); - if (v && pb_bpf_sync(v, n) != 0) pb_core_log("failed to apply rules to the kernel datapath"); - free(v); - rearm_all_locked(); - pb_bpf_set_flags(G.traffic_logging, G.dns_via_proxy); + free(G.views); + G.views = calloc(G.nrules ? G.nrules : 1, sizeof *G.views); + G.nviews = G.views ? G.nrules : 0; + bool domains = false; + for (size_t i = 0; i < G.nviews; i++) { + const struct pb_rule *r = &G.rules[i]; + struct pb_rule_view *v = &G.views[i]; + v->rule = r; + v->action = r->action; + domains |= r->enabled && r->ndomains > 0; + if (r->action != RULE_ACTION_PROXY) continue; + const struct pb_proxy *p = proxy_for_rule_locked(r); + if (!p) v->action = RULE_ACTION_BLOCK; // no usable proxy: fail closed + else { v->proxy_id = (uint16_t)p->id; v->udp_direct = p->type == PROXY_TYPE_HTTP; } + } + atomic_store(&G.has_domain_rules, domains); } -// ---- slow path ------------------------------------------------------------------------------------------------------------- -static bool exe_path_of_pid(uint32_t pid, char *out, size_t n) +static void sync_locked(void) { - char lnk[64]; - snprintf(lnk, sizeof lnk, "/proc/%u/exe", pid); - ssize_t r = readlink(lnk, out, n - 1); - if (r <= 0) { out[0] = 0; return false; } - out[r] = 0; - size_t l = strlen(out); - if (l > 10 && !strcmp(out + l - 10, " (deleted)")) out[l - 10] = 0; - return true; + rebuild_views_locked(); + if (!G.running) return; + if (pb_bpf_sync(G.views, G.nviews) != 0) pb_core_log("failed to apply rules to the kernel datapath"); + pb_bpf_set_flags(atomic_load(&G.traffic_logging), atomic_load(&G.dns_via_proxy)); + rearm_all_locked(); + warn_udp_over_http_locked(); } void pb_core_decide(const struct pb_orig *o, struct pb_decision *out) { - char path[512]; - exe_path_of_pid(o->pid, path, sizeof path); // may be empty if the process is already gone - out->action = PB_ACT_DIRECT; out->proxy_id = 0; + char path[4096]; + exe_path_of_pid(o->pid, path, sizeof path); + out->action = PB_ACT_DIRECT; + out->proxy_id = 0; pthread_rwlock_rdlock(&G.lock); - if (!pb_implicit_direct(o->family, o->addr, o->port, o->proto, G.dns_via_proxy)) { - char nbuf[PB_DNS_MAX_NAMES][256]; - const char *nptr[PB_DNS_MAX_NAMES]; + if (!pb_implicit_direct(o->family, o->addr, o->port, o->proto, atomic_load(&G.dns_via_proxy))) { + char names[PB_DNS_MAX_NAMES][256]; + const char *ptrs[PB_DNS_MAX_NAMES] = { 0 }; int nn = 0; if (o->family == AF_INET && atomic_load(&G.has_domain_rules)) { - uint32_t h = ((uint32_t)o->addr[12] << 24) | ((uint32_t)o->addr[13] << 16) | ((uint32_t)o->addr[14] << 8) | o->addr[15]; - nn = pb_dns_lookup(h, nbuf, PB_DNS_MAX_NAMES); - for (int k = 0; k < nn; k++) nptr[k] = nbuf[k]; + uint32_t ip = ((uint32_t)o->addr[12] << 24) | ((uint32_t)o->addr[13] << 16) | ((uint32_t)o->addr[14] << 8) | o->addr[15]; + nn = pb_dns_lookup(ip, names, PB_DNS_MAX_NAMES); + for (int k = 0; k < nn; k++) ptrs[k] = names[k]; } - int idx = pb_rules_eval(G.rules, G.nrules, path, o->family, o->addr, o->port, o->proto, nptr, nn); + int idx = pb_rules_eval(G.rules, G.nrules, path, o->family, o->addr, o->port, o->proto, ptrs, nn); if (idx >= 0) { const struct pb_rule *r = &G.rules[idx]; - if (r->action == RULE_ACTION_PROXY) { - struct pb_proxy *p = r->proxy_id ? find_proxy_locked(r->proxy_id) : NULL; - if (!p) p = default_proxy_locked(); + if (r->action != RULE_ACTION_PROXY) { + out->action = r->action == RULE_ACTION_BLOCK ? PB_ACT_BLOCK : PB_ACT_DIRECT; + } else { + const struct pb_proxy *p = proxy_for_rule_locked(r); if (!p) out->action = PB_ACT_BLOCK; else if (o->proto == IPPROTO_UDP && p->type == PROXY_TYPE_HTTP) out->action = PB_ACT_DIRECT; else { out->action = PB_ACT_PROXY; out->proxy_id = p->id; } - } else out->action = r->action == RULE_ACTION_BLOCK ? PB_ACT_BLOCK : PB_ACT_DIRECT; + } } } - if (o->exe_ino && path[0]) { // teach the kernel this binary so the next flow skips the relay - size_t n; - struct pb_rule_view *v = build_views_locked(&n); - if (v) { pb_bpf_learn(o, path, v, n); free(v); } - } + if (o->exe_ino && path[0]) pb_bpf_learn(o, path, G.views, G.nviews); pthread_rwlock_unlock(&G.lock); } -int pb_core_take_tcp_orig(uint16_t client_port, struct pb_orig *out) { return pb_bpf_take_tcp_orig(client_port, out); } +int pb_core_take_tcp_orig(uint32_t client_ip_be, uint16_t client_port, struct pb_orig *out) +{ + return pb_bpf_take_tcp_orig(client_ip_be, client_port, out); +} + int pb_core_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out) { return pb_bpf_get_udp_flow(flow_ip_be, out); } -// ---- public API: proxies ----------------------------------------------------------------------------------------------- +// ---- local proxy exemption and /etc/hosts ------------------------------------------------------------------------------- +// A rule such as "*:...:PROXY" would also match the proxy's own outbound connections and send them back through itself, so +// the processes that listen on a configured local proxy address are never redirected. + +struct host_cache { char host[256]; uint32_t ip_be; time_t at; }; + +static uint32_t resolve_proxy_ip(const char *host, struct host_cache *cache, size_t ncache, time_t now) +{ + struct in_addr a; + if (inet_pton(AF_INET, host, &a) == 1) return a.s_addr; + if (!strcmp(host, "localhost")) return htonl(INADDR_LOOPBACK); + size_t slot = 0; + for (size_t i = 0; i < ncache; i++) { + if (!strcmp(cache[i].host, host)) { slot = i; break; } + if (cache[i].at < cache[slot].at) slot = i; + } + if (strcmp(cache[slot].host, host) || now - cache[slot].at > 60) { + struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM }, *res = NULL; + snprintf(cache[slot].host, sizeof cache[slot].host, "%.255s", host); + cache[slot].at = now; + cache[slot].ip_be = 0; + if (getaddrinfo(host, NULL, &hints, &res) == 0 && res) { + cache[slot].ip_be = ((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr; + freeaddrinfo(res); + } + } + return cache[slot].ip_be; +} + +static void refresh_exempt(struct host_cache *cache, size_t ncache, uint32_t *last, size_t *nlast) +{ + struct { char host[256]; uint16_t port; } targets[MAX_PROXIES]; + size_t nt = 0; + pthread_rwlock_rdlock(&G.lock); + for (size_t i = 0; i < G.nproxies; i++) { + snprintf(targets[nt].host, sizeof targets[nt].host, "%.255s", G.proxies[i].host); + targets[nt++].port = G.proxies[i].port; + } + pthread_rwlock_unlock(&G.lock); + + uint32_t tgids[64]; + size_t n = 0; + time_t now = time(NULL); + for (size_t i = 0; i < nt && n < 64; i++) { + uint32_t ip = resolve_proxy_ip(targets[i].host, cache, ncache, now); + if (!ip || !pb_proc_is_local_ip(ip)) continue; + uint32_t found[8]; + size_t nf = pb_proc_listeners(ip, targets[i].port, found, 8); + for (size_t k = 0; k < nf && n < 64; k++) { + bool dup = false; + for (size_t j = 0; j < n && !dup; j++) dup = tgids[j] == found[k]; + if (!dup && found[k] != (uint32_t)getpid()) tgids[n++] = found[k]; + } + } + if (n != *nlast || memcmp(tgids, last, n * sizeof *tgids)) { + pb_bpf_set_exempt(tgids, n); + memcpy(last, tgids, n * sizeof *tgids); + *nlast = n; + } +} + +static void reload_hosts_if_changed(time_t *mtime, off_t *size) +{ + struct stat st; + if (stat(HOSTS_PATH, &st) != 0 || (st.st_mtime == *mtime && st.st_size == *size)) return; + *mtime = st.st_mtime; + *size = st.st_size; + pb_dns_load_hosts(HOSTS_PATH, NULL); + pthread_rwlock_rdlock(&G.lock); + rearm_all_locked(); + pthread_rwlock_unlock(&G.lock); +} + +static void *maint_loop(void *arg) +{ + (void)arg; + struct host_cache cache[8] = { 0 }; + uint32_t last[64]; + size_t nlast = 0; + time_t hosts_mtime = 0; + off_t hosts_size = 0; + pthread_mutex_lock(&G.maint_mu); + while (!G.maint_stop) { + G.maint_kick = false; + pthread_mutex_unlock(&G.maint_mu); + refresh_exempt(cache, 8, last, &nlast); + reload_hosts_if_changed(&hosts_mtime, &hosts_size); + pthread_mutex_lock(&G.maint_mu); + struct timespec until; + clock_gettime(CLOCK_REALTIME, &until); + until.tv_sec += MAINT_PERIOD_S; + while (!G.maint_stop && !G.maint_kick && pthread_cond_timedwait(&G.maint_cv, &G.maint_mu, &until) == 0) {} + } + pthread_mutex_unlock(&G.maint_mu); + return NULL; +} + +// ---- proxies ------------------------------------------------------------------------------------------------------------- +static bool valid_proxy(const char *host, uint16_t port, char *err, size_t en) +{ + if (!host || !*host) { set_err(err, en, "proxy host is empty"); return false; } + if (strlen(host) > 253) { set_err(err, en, "proxy host too long"); return false; } + for (const char *c = host; *c; c++) + if (isspace((unsigned char)*c) || *c == '/' || *c == '@') { set_err(err, en, "proxy host contains invalid characters"); return false; } + if (!port) { set_err(err, en, "proxy port must be 1-65535"); return false; } + return true; +} + +static void fill_proxy(struct pb_proxy *p, const char *name, ProxyType type, const char *host, uint16_t port, const char *user, const char *pass) +{ + snprintf(p->name, sizeof p->name, "%s", name && *name ? name : host); + p->type = type == PROXY_TYPE_HTTP ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; + snprintf(p->host, sizeof p->host, "%s", host); + p->port = port; + snprintf(p->user, sizeof p->user, "%s", user ? user : ""); + snprintf(p->pass, sizeof p->pass, "%s", pass ? pass : ""); +} + uint32_t ProxyBridge_AddProxy(const char *name, ProxyType type, const char *host, uint16_t port, const char *user, const char *pass, char *err, size_t en) { if (!valid_proxy(host, port, err, en)) return 0; - pthread_rwlock_wrlock(&G.lock); uint32_t id = 0; - if (G.nproxies >= MAX_PROXIES) set_err(err, en, "too many proxies (max %d)", MAX_PROXIES); - else { + pthread_rwlock_wrlock(&G.lock); + if (G.nproxies >= MAX_PROXIES) { + set_err(err, en, "too many proxies (max %d)", MAX_PROXIES); + } else { struct pb_proxy *p = &G.proxies[G.nproxies++]; memset(p, 0, sizeof *p); p->id = id = G.next_proxy_id++; @@ -299,6 +422,7 @@ uint32_t ProxyBridge_AddProxy(const char *name, ProxyType type, const char *host sync_locked(); } pthread_rwlock_unlock(&G.lock); + if (id) maint_kick(); return id; } @@ -310,32 +434,35 @@ bool ProxyBridge_EditProxy(uint32_t id, const char *name, ProxyType type, const if (p) { fill_proxy(p, name, type, host, port, user, pass); sync_locked(); } else set_err(err, en, "unknown proxy id %u", id); pthread_rwlock_unlock(&G.lock); + if (p) maint_kick(); return p != NULL; } bool ProxyBridge_DeleteProxy(uint32_t id) { - pthread_rwlock_wrlock(&G.lock); bool ok = false; - for (size_t i = 0; i < G.nproxies; i++) - if (G.proxies[i].id == id) { - memmove(&G.proxies[i], &G.proxies[i + 1], (G.nproxies - i - 1) * sizeof G.proxies[0]); - G.nproxies--; - if (G.default_proxy == id) G.default_proxy = 0; - ok = true; - break; - } + pthread_rwlock_wrlock(&G.lock); + for (size_t i = 0; i < G.nproxies && !ok; i++) { + if (G.proxies[i].id != id) continue; + memmove(&G.proxies[i], &G.proxies[i + 1], (G.nproxies - i - 1) * sizeof G.proxies[0]); + G.nproxies--; + if (G.default_proxy == id) G.default_proxy = 0; + ok = true; + } if (ok) sync_locked(); pthread_rwlock_unlock(&G.lock); + if (ok) maint_kick(); return ok; } void ProxyBridge_ClearProxies(void) { pthread_rwlock_wrlock(&G.lock); - G.nproxies = 0; G.default_proxy = 0; + G.nproxies = 0; + G.default_proxy = 0; sync_locked(); pthread_rwlock_unlock(&G.lock); + maint_kick(); } void ProxyBridge_SetDefaultProxy(uint32_t id) @@ -350,14 +477,14 @@ bool ProxyBridge_SetProxyConfig(ProxyType type, const char *host, uint16_t port, { char err[256]; pthread_rwlock_rdlock(&G.lock); - struct pb_proxy *d = default_proxy_locked(); + const struct pb_proxy *d = default_proxy_locked(); uint32_t id = d ? d->id : 0; pthread_rwlock_unlock(&G.lock); if (id) return ProxyBridge_EditProxy(id, NULL, type, host, port, user, pass, err, sizeof err); return ProxyBridge_AddProxy("default", type, host, port, user, pass, err, sizeof err) != 0; } -// ---- public API: rules --------------------------------------------------------------------------------------------------- +// ---- rules --------------------------------------------------------------------------------------------------------------- bool ProxyBridge_ValidateRule(const char *process, const char *hosts, const char *ports, char *err, size_t en) { struct pb_rule r; @@ -368,7 +495,8 @@ bool ProxyBridge_ValidateRule(const char *process, const char *hosts, const char static struct pb_rule *find_rule_locked(uint32_t id, size_t *idx) { - for (size_t i = 0; i < G.nrules; i++) if (G.rules[i].id == id) { if (idx) *idx = i; return &G.rules[i]; } + for (size_t i = 0; i < G.nrules; i++) + if (G.rules[i].id == id) { if (idx) *idx = i; return &G.rules[i]; } return NULL; } @@ -376,20 +504,27 @@ uint32_t ProxyBridge_AddRule(const char *proc, const char *hosts, const char *po { struct pb_rule r; if (!pb_rule_parse(&r, proc, hosts, ports, err, en)) return 0; - r.enabled = true; r.protocol = protocol; r.action = action; r.proxy_id = proxy_id; - pthread_rwlock_wrlock(&G.lock); + r.enabled = true; + r.protocol = protocol; + r.action = action; + r.proxy_id = proxy_id; + uint32_t id = 0; - if (G.nrules >= PB_MAX_RULES) { set_err(err, en, "too many rules (max %d)", PB_MAX_RULES); pb_rule_free(&r); } - else { - if (G.nrules == G.caprules) { - size_t nc = G.caprules ? G.caprules * 2 : 32; - struct pb_rule *nr = realloc(G.rules, nc * sizeof *nr); - if (!nr) { pb_rule_free(&r); set_err(err, en, "out of memory"); pthread_rwlock_unlock(&G.lock); return 0; } - G.rules = nr; G.caprules = nc; - } + pthread_rwlock_wrlock(&G.lock); + if (G.nrules >= PB_MAX_RULES) { + set_err(err, en, "too many rules (max %d)", PB_MAX_RULES); + } else if (G.nrules == G.caprules) { + size_t cap = G.caprules ? G.caprules * 2 : 32; + struct pb_rule *grown = realloc(G.rules, cap * sizeof *grown); + if (grown) { G.rules = grown; G.caprules = cap; } + else set_err(err, en, "out of memory"); + } + if (G.nrules < G.caprules && G.nrules < PB_MAX_RULES) { r.id = id = G.next_rule_id++; G.rules[G.nrules++] = r; sync_locked(); + } else { + pb_rule_free(&r); } pthread_rwlock_unlock(&G.lock); return id; @@ -401,13 +536,21 @@ bool ProxyBridge_EditRule(uint32_t id, const char *proc, const char *hosts, cons if (!pb_rule_parse(&r, proc, hosts, ports, err, en)) return false; pthread_rwlock_wrlock(&G.lock); struct pb_rule *old = find_rule_locked(id, NULL); - if (!old) { set_err(err, en, "unknown rule id %u", id); pb_rule_free(&r); pthread_rwlock_unlock(&G.lock); return false; } - r.id = id; r.enabled = old->enabled; r.protocol = protocol; r.action = action; r.proxy_id = proxy_id; - pb_rule_free(old); - *old = r; - sync_locked(); + if (!old) { + set_err(err, en, "unknown rule id %u", id); + pb_rule_free(&r); + } else { + r.id = id; + r.enabled = old->enabled; + r.protocol = protocol; + r.action = action; + r.proxy_id = proxy_id; + pb_rule_free(old); + *old = r; + sync_locked(); + } pthread_rwlock_unlock(&G.lock); - return true; + return old != NULL; } static bool set_enabled(uint32_t id, bool on) @@ -418,13 +561,14 @@ static bool set_enabled(uint32_t id, bool on) pthread_rwlock_unlock(&G.lock); return r != NULL; } + bool ProxyBridge_EnableRule(uint32_t id) { return set_enabled(id, true); } bool ProxyBridge_DisableRule(uint32_t id) { return set_enabled(id, false); } bool ProxyBridge_DeleteRule(uint32_t id) { - pthread_rwlock_wrlock(&G.lock); size_t i; + pthread_rwlock_wrlock(&G.lock); struct pb_rule *r = find_rule_locked(id, &i); if (r) { pb_rule_free(r); @@ -438,15 +582,15 @@ bool ProxyBridge_DeleteRule(uint32_t id) bool ProxyBridge_MoveRule(uint32_t id, int new_index) { - pthread_rwlock_wrlock(&G.lock); size_t i; + pthread_rwlock_wrlock(&G.lock); struct pb_rule *r = find_rule_locked(id, &i); if (r) { size_t to = new_index < 0 ? 0 : (size_t)new_index >= G.nrules ? G.nrules - 1 : (size_t)new_index; - struct pb_rule tmp = *r; - if (to < i) memmove(&G.rules[to + 1], &G.rules[to], (i - to) * sizeof tmp); - else if (to > i) memmove(&G.rules[i], &G.rules[i + 1], (to - i) * sizeof tmp); - G.rules[to] = tmp; + struct pb_rule moved = *r; + if (to < i) memmove(&G.rules[to + 1], &G.rules[to], (i - to) * sizeof moved); + else if (to > i) memmove(&G.rules[i], &G.rules[i + 1], (to - i) * sizeof moved); + G.rules[to] = moved; sync_locked(); } pthread_rwlock_unlock(&G.lock); @@ -462,57 +606,76 @@ void ProxyBridge_ClearRules(void) pthread_rwlock_unlock(&G.lock); } -// ---- public API: runtime ----------------------------------------------------------------------------------------------------- +// ---- runtime ------------------------------------------------------------------------------------------------------------- void ProxyBridge_SetDnsViaProxy(bool enable) { - pthread_rwlock_wrlock(&G.lock); - G.dns_via_proxy = enable; - pb_bpf_set_flags(G.traffic_logging, G.dns_via_proxy); - pthread_rwlock_unlock(&G.lock); + atomic_store(&G.dns_via_proxy, enable); + pb_bpf_set_flags(atomic_load(&G.traffic_logging), enable); } -void ProxyBridge_SetLogCallback(LogCallback cb) { pthread_mutex_lock(&cb_lock); G.log_cb = cb; pthread_mutex_unlock(&cb_lock); } -void ProxyBridge_SetConnectionCallback(ConnectionCallback cb) { pthread_mutex_lock(&cb_lock); G.conn_cb = cb; pthread_mutex_unlock(&cb_lock); } +void ProxyBridge_SetLogCallback(LogCallback cb) +{ + pthread_mutex_lock(&cb_lock); + G.log_cb = cb; + pthread_mutex_unlock(&cb_lock); +} + +void ProxyBridge_SetConnectionCallback(ConnectionCallback cb) +{ + pthread_mutex_lock(&cb_lock); + G.conn_cb = cb; + pthread_mutex_unlock(&cb_lock); +} void ProxyBridge_SetTrafficLoggingEnabled(bool enable) { - pthread_rwlock_wrlock(&G.lock); - G.traffic_logging = enable; - pb_bpf_set_flags(G.traffic_logging, G.dns_via_proxy); - pthread_rwlock_unlock(&G.lock); + atomic_store(&G.traffic_logging, enable); + pb_bpf_set_flags(enable, atomic_load(&G.dns_via_proxy)); } void ProxyBridge_ClearConnectionLogs(void) {} +static bool proxies_missing_locked(void) +{ + for (size_t i = 0; i < G.nrules; i++) { + const struct pb_rule *r = &G.rules[i]; + if (r->enabled && r->action == RULE_ACTION_PROXY && !proxy_for_rule_locked(r)) return true; + } + return false; +} + bool ProxyBridge_Start(void) { pthread_rwlock_wrlock(&G.lock); if (G.running) { pthread_rwlock_unlock(&G.lock); return true; } - for (size_t i = 0; i < G.nrules; i++) - if (G.rules[i].enabled && G.rules[i].action == RULE_ACTION_PROXY && !default_proxy_locked() && - !(G.rules[i].proxy_id && find_proxy_locked(G.rules[i].proxy_id))) { - pthread_rwlock_unlock(&G.lock); - pb_core_log("cannot start: a rule uses a proxy but no proxy is configured"); - return false; - } - G.running = true; // pb_core_* callbacks may fire as soon as the relay is up + if (proxies_missing_locked()) { + pthread_rwlock_unlock(&G.lock); + pb_core_log("cannot start: a rule uses a proxy but no proxy is configured"); + return false; + } + G.running = true; // relay callbacks may fire as soon as the relay is up pthread_rwlock_unlock(&G.lock); - bool started = false; - for (uint16_t base = PB_TCP_RELAY_PORT_DEFAULT; base < PB_TCP_RELAY_PORT_DEFAULT + 20 && !started; base += 2) { + bool relay_up = false; + for (uint16_t base = PB_TCP_RELAY_PORT_DEFAULT; base < PB_TCP_RELAY_PORT_DEFAULT + 20 && !relay_up; base += 2) { struct pb_relay_cfg rc = { .tcp_port = base, .udp_port = (uint16_t)(base + 1) }; - started = pb_relay_start(&rc) == 0; + relay_up = pb_relay_start(&rc) == 0; } - if (!started) { pthread_rwlock_wrlock(&G.lock); G.running = false; pthread_rwlock_unlock(&G.lock); return false; } - struct pb_bpf_cfg bc = { .tcp_port = pb_relay_tcp_port(), .udp_port = pb_relay_udp_port(), .self_tgid = (uint32_t)getpid(), - .log = G.traffic_logging, .dns_via_proxy = G.dns_via_proxy }; - if (pb_bpf_start(&bc, on_bpf_event) != 0) { - pb_relay_stop(); - pthread_rwlock_wrlock(&G.lock); G.running = false; pthread_rwlock_unlock(&G.lock); + .log = atomic_load(&G.traffic_logging), .dns_via_proxy = atomic_load(&G.dns_via_proxy) }; + if (!relay_up || pb_bpf_start(&bc, on_bpf_event) != 0) { + if (relay_up) pb_relay_stop(); + pthread_rwlock_wrlock(&G.lock); + G.running = false; + pthread_rwlock_unlock(&G.lock); return false; } - pb_dns_load_hosts("/etc/hosts", NULL); // static names are known without any DNS traffic + + pthread_mutex_lock(&G.maint_mu); + G.maint_stop = false; + pthread_mutex_unlock(&G.maint_mu); + G.maint_started = pthread_create(&G.maint, NULL, maint_loop, NULL) == 0; + pthread_rwlock_wrlock(&G.lock); sync_locked(); pthread_rwlock_unlock(&G.lock); @@ -525,10 +688,21 @@ bool ProxyBridge_Stop(void) pthread_rwlock_wrlock(&G.lock); bool was = G.running; G.running = false; + G.nwarned = 0; pthread_rwlock_unlock(&G.lock); if (!was) return true; - pb_bpf_stop(); // detach first: no new flows get redirected - pb_relay_stop(); + + if (G.maint_started) { + pthread_mutex_lock(&G.maint_mu); + G.maint_stop = true; + pthread_cond_signal(&G.maint_cv); + pthread_mutex_unlock(&G.maint_mu); + pthread_join(G.maint, NULL); + G.maint_started = false; + } + pb_bpf_detach(); // no new flows get redirected + pb_relay_stop(); // relay threads may use the maps until they are gone + pb_bpf_stop(); pb_core_log("ProxyBridge stopped"); return true; } diff --git a/Linux/src/ProxyBridge.h b/Linux/src/ProxyBridge.h index a03ed15e..89381dc9 100644 --- a/Linux/src/ProxyBridge.h +++ b/Linux/src/ProxyBridge.h @@ -11,6 +11,8 @@ extern "C" { #endif +#define PB_API __attribute__((visibility("default"))) + typedef void (*LogCallback)(const char* message); typedef void (*ConnectionCallback)(const char* process_name, uint32_t pid, const char* dest_ip, uint16_t dest_port, const char* proxy_info); @@ -34,14 +36,14 @@ typedef enum { /* ---- Proxy configurations ------------------------------------------------------------------------------------------- * Any number of proxies can be registered. Ids are non-zero and stable until the proxy is deleted. * Returns 0 on failure (the reason is written to err, if given). */ -uint32_t ProxyBridge_AddProxy(const char* name, ProxyType type, const char* host, uint16_t port, +PB_API uint32_t ProxyBridge_AddProxy(const char* name, ProxyType type, const char* host, uint16_t port, const char* username, const char* password, char* err, size_t err_size); -bool ProxyBridge_EditProxy(uint32_t proxy_id, const char* name, ProxyType type, const char* host, uint16_t port, +PB_API bool ProxyBridge_EditProxy(uint32_t proxy_id, const char* name, ProxyType type, const char* host, uint16_t port, const char* username, const char* password, char* err, size_t err_size); -bool ProxyBridge_DeleteProxy(uint32_t proxy_id); /* rules that used it fall back to the default proxy */ -void ProxyBridge_ClearProxies(void); +PB_API bool ProxyBridge_DeleteProxy(uint32_t proxy_id); /* rules that used it fall back to the default proxy */ +PB_API void ProxyBridge_ClearProxies(void); /* Which proxy a rule with proxy_id == 0 uses. 0 = the first proxy added. */ -void ProxyBridge_SetDefaultProxy(uint32_t proxy_id); +PB_API void ProxyBridge_SetDefaultProxy(uint32_t proxy_id); /* ---- Rules ---------------------------------------------------------------------------------------------------------- * Rules are evaluated in list order, first match wins. process_name: ';' or ',' separated globs matched against the @@ -50,33 +52,33 @@ void ProxyBridge_SetDefaultProxy(uint32_t proxy_id); * ("80;443;8000-9000", "" or "*" = any). * proxy_id applies when action == RULE_ACTION_PROXY; 0 = default proxy. * AddRule returns the new rule id, or 0 on validation failure (reason in err). */ -uint32_t ProxyBridge_AddRule(const char* process_name, const char* target_hosts, const char* target_ports, +PB_API uint32_t ProxyBridge_AddRule(const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action, uint32_t proxy_id, char* err, size_t err_size); -bool ProxyBridge_EditRule(uint32_t rule_id, const char* process_name, const char* target_hosts, const char* target_ports, +PB_API bool ProxyBridge_EditRule(uint32_t rule_id, const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action, uint32_t proxy_id, char* err, size_t err_size); -bool ProxyBridge_EnableRule(uint32_t rule_id); -bool ProxyBridge_DisableRule(uint32_t rule_id); -bool ProxyBridge_DeleteRule(uint32_t rule_id); -bool ProxyBridge_MoveRule(uint32_t rule_id, int new_index); /* reorder: priority = position */ -void ProxyBridge_ClearRules(void); +PB_API bool ProxyBridge_EnableRule(uint32_t rule_id); +PB_API bool ProxyBridge_DisableRule(uint32_t rule_id); +PB_API bool ProxyBridge_DeleteRule(uint32_t rule_id); +PB_API bool ProxyBridge_MoveRule(uint32_t rule_id, int new_index); /* reorder: priority = position */ +PB_API void ProxyBridge_ClearRules(void); /* Validates a rule without adding it; returns true if valid, otherwise writes the reason to err. */ -bool ProxyBridge_ValidateRule(const char* process_name, const char* target_hosts, const char* target_ports, +PB_API bool ProxyBridge_ValidateRule(const char* process_name, const char* target_hosts, const char* target_ports, char* err, size_t err_size); /* ---- Legacy single-proxy helper ------------------------------------------------------------------------------------- * Sets (creating on first use) the default proxy. Equivalent to Add/EditProxy on the "default" entry. */ -bool ProxyBridge_SetProxyConfig(ProxyType type, const char* proxy_ip, uint16_t proxy_port, const char* username, const char* password); +PB_API bool ProxyBridge_SetProxyConfig(ProxyType type, const char* proxy_ip, uint16_t proxy_port, const char* username, const char* password); /* ---- Runtime --------------------------------------------------------------------------------------------------------- */ -void ProxyBridge_SetDnsViaProxy(bool enable); -void ProxyBridge_SetLogCallback(LogCallback callback); -void ProxyBridge_SetConnectionCallback(ConnectionCallback callback); -void ProxyBridge_SetTrafficLoggingEnabled(bool enable); -void ProxyBridge_ClearConnectionLogs(void); -bool ProxyBridge_Start(void); /* needs root (CAP_BPF/CAP_SYS_ADMIN) and a BTF kernel >= 5.11; returns false and logs why on failure */ -bool ProxyBridge_Stop(void); /* keeps rules and proxies; Start() may be called again */ -int ProxyBridge_TestConnection(const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); -int ProxyBridge_TestProxy(uint32_t proxy_id, const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); +PB_API void ProxyBridge_SetDnsViaProxy(bool enable); +PB_API void ProxyBridge_SetLogCallback(LogCallback callback); +PB_API void ProxyBridge_SetConnectionCallback(ConnectionCallback callback); +PB_API void ProxyBridge_SetTrafficLoggingEnabled(bool enable); +PB_API void ProxyBridge_ClearConnectionLogs(void); +PB_API bool ProxyBridge_Start(void); /* needs root (CAP_BPF/CAP_SYS_ADMIN) and a BTF kernel >= 5.11; returns false and logs why on failure */ +PB_API bool ProxyBridge_Stop(void); /* keeps rules and proxies; Start() may be called again */ +PB_API int ProxyBridge_TestConnection(const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); +PB_API int ProxyBridge_TestProxy(uint32_t proxy_id, const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size); #ifdef __cplusplus } diff --git a/Linux/src/bpf/pb_bpf_shared.h b/Linux/src/bpf/pb_bpf_shared.h index bb403e2d..ef987093 100644 --- a/Linux/src/bpf/pb_bpf_shared.h +++ b/Linux/src/bpf/pb_bpf_shared.h @@ -1,4 +1,4 @@ -// Types shared between the BPF object and userspace (pb_bpf.c). Plain C, no kernel headers needed. +// Types shared by the BPF object and userspace. #ifndef PB_BPF_SHARED_H #define PB_BPF_SHARED_H @@ -15,48 +15,43 @@ typedef uint8_t pb_u8; typedef uint16_t pb_u16; typedef uint32_t pb_u32; typedef #define PB_BPF_MAX_V4 4 #define PB_BPF_MAX_PORT 4 -// cfg array slots -#define PB_CFG_ENABLED 0 // 1 when at least one rule is loaded -#define PB_CFG_SELF_TGID 1 // process that hosts the relay (never redirected) -#define PB_CFG_LOG 2 // 1 = emit events -#define PB_CFG_TCP_PORT 3 -#define PB_CFG_UDP_PORT 4 -#define PB_CFG_DNS_VIA_PROXY 5 -#define PB_CFG_DNS_TAP 6 // 1 when a domain rule exists: port-53 flows of covered processes go through the relay -#define PB_CFG_NETNS_LO 7 // cookie of the network namespace the relay lives in; sockets in other namespaces are ignored -#define PB_CFG_NETNS_HI 8 -#define PB_CFG_SLOTS 12 - -// brule flags #define PB_RF_ANY_HOST 0x01 #define PB_RF_ANY_PORT 0x02 -#define PB_RF_HOST_COMPLEX 0x04 // host pattern not representable: "maybe" for every destination -#define PB_RF_PORT_COMPLEX 0x08 // more port ranges than fit: "maybe" when the ports in the table do not match -#define PB_RF_HAS_V6 0x10 // rule has IPv6 patterns: "maybe" for IPv6 destinations -#define PB_RF_DOMAINS 0x40 // host list also contains domain patterns: answered by the dom_hit map (filled by the DNS tap) -#define PB_RF_UDP_DIRECT 0x20 // PROXY rule whose proxy cannot carry UDP (HTTP): UDP flows go direct +#define PB_RF_HOST_COMPLEX 0x04 // host pattern the kernel cannot express: "maybe" for every destination +#define PB_RF_PORT_COMPLEX 0x08 // more port ranges than fit: "maybe" when none of the stored ones match +#define PB_RF_HAS_V6 0x10 // IPv6 patterns present: "maybe" for IPv6 destinations +#define PB_RF_UDP_DIRECT 0x20 // PROXY rule whose proxy cannot carry UDP: UDP goes direct +#define PB_RF_DOMAINS 0x40 // answered through the dom_hit map filled by the DNS tap + +#define PB_CLASS_DECIDE 0xffffffffu // pb_bclass.n: always take the slow path for this binary -#define PB_CLASS_DECIDE 0xffffffffu // pb_bclass.n value: always take the slow path for this binary +struct pb_cfg { + pb_u32 enabled; // at least one rule is loaded + pb_u32 self_tgid; // the process hosting the relay is never redirected + pb_u32 log; + pb_u32 dns_via_proxy; + pb_u32 dns_tap; // a domain rule exists + pb_u32 n_exempt; // entries in the exempt map + pb_u16 tcp_port, udp_port; + pb_u64 netns; // network namespace of the relay; other namespaces are ignored (0 = unknown) +}; struct pb_exe_key { pb_u32 dev; pb_u32 pad; pb_u64 ino; }; +struct pb_port_key { pb_u32 ip; pb_u32 port; }; // client source address (network order) and port +struct pb_dom_key { pb_u32 rule; pb_u32 ip; }; // ip in host order struct pb_brule { pb_u16 proxy_id; - pb_u8 action; // 0 proxy, 1 direct, 2 block - pb_u8 proto_mask; // 1 tcp, 2 udp - pb_u8 flags; - pb_u8 n_v4; - pb_u8 n_port; - pb_u8 pad; + pb_u8 action; // 0 proxy, 1 direct, 2 block + pb_u8 proto_mask; // 1 tcp, 2 udp + pb_u8 flags, n_v4, n_port, pad; struct { pb_u32 lo, hi; } v4[PB_BPF_MAX_V4]; // host order struct { pb_u16 lo, hi; } port[PB_BPF_MAX_PORT]; // host order }; -struct pb_dom_key { pb_u32 rule; pb_u32 ip; }; // ip in host order - struct pb_bclass { pb_u32 n; - pb_u32 tap; // 1: this binary is covered by a domain rule, so its DNS must be observed + pb_u32 tap; // covered by a domain rule: its DNS must be observed pb_u32 rule[PB_RULES_PER_CLASS]; }; @@ -64,12 +59,8 @@ struct pb_event { pb_u32 pid; pb_u8 comm[16]; pb_u8 addr[16]; - pb_u16 port; - pb_u16 proxy_id; - pb_u8 family; - pb_u8 proto; - pb_u8 action; // PB_ACT_* - pb_u8 pad; + pb_u16 port, proxy_id; + pb_u8 family, proto, action, pad; }; #endif diff --git a/Linux/src/bpf/proxybridge.bpf.c b/Linux/src/bpf/proxybridge.bpf.c index 839f57c2..db8a0330 100644 --- a/Linux/src/bpf/proxybridge.bpf.c +++ b/Linux/src/bpf/proxybridge.bpf.c @@ -1,12 +1,11 @@ -// ProxyBridge eBPF datapath: cgroup socket-address hooks that redirect selected processes' connect()/sendmsg() to the -// userspace relay and remember the original destination. +// cgroup socket-address hooks: redirect selected processes' connect()/sendmsg() to the userspace relay. #include #include #include #include #include "pb_bpf_shared.h" -// Minimal kernel type stubs (CO-RE relocated against the running kernel's BTF; avoids shipping a 160k-line vmlinux.h). +// Minimal kernel types, relocated against the running kernel's BTF (CO-RE). #pragma clang attribute push(__attribute__((preserve_access_index)), apply_to = record) struct super_block { __u32 s_dev; }; struct inode { struct super_block *i_sb; unsigned long i_ino; }; @@ -39,49 +38,28 @@ struct pb_orig { __u64 exe_ino; }; -struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, PB_CFG_SLOTS); - __type(key, __u32); __type(value, __u32); } cfg SEC(".maps"); -struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 16384); - __type(key, struct pb_exe_key); __type(value, __u32); } exe2class SEC(".maps"); -struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, PB_MAX_CLASSES); - __type(key, __u32); __type(value, struct pb_bclass); } classes SEC(".maps"); -struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, PB_MAX_RULES); - __type(key, __u32); __type(value, struct pb_brule); } brules SEC(".maps"); -struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 32768); - __type(key, __u64); __type(value, struct pb_orig); } by_cookie SEC(".maps"); -struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 32768); - __type(key, __u32); __type(value, struct pb_orig); } by_port SEC(".maps"); -struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 32768); - __type(key, __u32); __type(value, struct pb_orig); } flows SEC(".maps"); -struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 65536); - __type(key, struct pb_dom_key); __type(value, __u8); } dom_hit SEC(".maps"); // (rule, ip) pairs whose DNS name matches the rule -struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } events SEC(".maps"); -struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 8); - __type(key, __u32); __type(value, __u64); } stats SEC(".maps"); // 0 redirected 1 blocked 2 decide 3 evt dropped +volatile struct pb_cfg cfg; -static __always_inline void stat_inc(__u32 k) -{ - __u64 *v = bpf_map_lookup_elem(&stats, &k); - if (v) (*v)++; -} +#define MAP(name, kind, entries, key_t, val_t) \ + struct { __uint(type, kind); __uint(max_entries, entries); __type(key, key_t); __type(value, val_t); } name SEC(".maps") -static __always_inline __u32 cfg_get(__u32 k) -{ - __u32 *v = bpf_map_lookup_elem(&cfg, &k); - return v ? *v : 0; -} +MAP(exe2class, BPF_MAP_TYPE_HASH, 16384, struct pb_exe_key, __u32); +MAP(classes, BPF_MAP_TYPE_ARRAY, PB_MAX_CLASSES, __u32, struct pb_bclass); +MAP(brules, BPF_MAP_TYPE_ARRAY, PB_MAX_RULES, __u32, struct pb_brule); +MAP(exempt, BPF_MAP_TYPE_HASH, 64, __u32, __u8); +MAP(by_cookie, BPF_MAP_TYPE_LRU_HASH, 32768, __u64, struct pb_orig); +MAP(by_port, BPF_MAP_TYPE_LRU_HASH, 32768, struct pb_port_key, struct pb_orig); +MAP(flows, BPF_MAP_TYPE_LRU_HASH, 32768, __u32, struct pb_orig); +MAP(dom_hit, BPF_MAP_TYPE_LRU_HASH, 65536, struct pb_dom_key, __u8); +struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } events SEC(".maps"); -// Containers/other network namespaces have their own loopback: redirecting them to our relay would break them. static __always_inline int foreign_netns(void *ctx) { - __u64 want = ((__u64)cfg_get(PB_CFG_NETNS_HI) << 32) | cfg_get(PB_CFG_NETNS_LO); + __u64 want = cfg.netns; return want && bpf_get_netns_cookie(ctx) != want; } -// --------------------------------------------------------------------------------------------------------------------- -// rule evaluation -// --------------------------------------------------------------------------------------------------------------------- -static __always_inline int rule_match(const struct pb_brule *r, __u32 ri, __u8 fam, __u32 ip4_host, __u16 port) +static __always_inline int rule_match(const struct pb_brule *r, __u32 ri, __u8 fam, __u32 ip4, __u16 port) { int port_res = M_YES; if (!(r->flags & PB_RF_ANY_PORT)) { @@ -103,10 +81,10 @@ static __always_inline int rule_match(const struct pb_brule *r, __u32 ri, __u8 f #pragma unroll for (int i = 0; i < PB_BPF_MAX_V4; i++) { if (i >= r->n_v4) break; - if (ip4_host >= r->v4[i].lo && ip4_host <= r->v4[i].hi) { host_res = M_YES; break; } + if (ip4 >= r->v4[i].lo && ip4 <= r->v4[i].hi) { host_res = M_YES; break; } } if (host_res == M_NO && (r->flags & PB_RF_DOMAINS)) { - struct pb_dom_key dk = { .rule = ri, .ip = ip4_host }; + struct pb_dom_key dk = { .rule = ri, .ip = ip4 }; if (bpf_map_lookup_elem(&dom_hit, &dk)) host_res = M_YES; } } @@ -114,51 +92,49 @@ static __always_inline int rule_match(const struct pb_brule *r, __u32 ri, __u8 f return (host_res == M_MAYBE || port_res == M_MAYBE) ? M_MAYBE : M_YES; } -static __always_inline int implicit_direct(__u8 fam, const __u8 *a, __u16 port, __u8 proto) +static __always_inline int implicit_direct(const struct pb_orig *o) { - if (fam == AF_INET) { + const __u8 *a = o->addr; + if (o->family == AF_INET) { if (a[12] == 127 || a[12] == 0 || a[12] >= 224) return 1; } else { - if (a[0] == 0xff) return 1; - if (a[0] == 0xfe && (a[1] & 0xc0) == 0x80) return 1; - int lo = 1; + if (a[0] == 0xff || (a[0] == 0xfe && (a[1] & 0xc0) == 0x80)) return 1; + int loopback = a[15] == 1; #pragma unroll - for (int i = 0; i < 15; i++) if (a[i]) lo = 0; - if (lo && a[15] == 1) return 1; + for (int i = 0; i < 15; i++) if (a[i]) loopback = 0; + if (loopback) return 1; } - if (proto == IPPROTO_UDP && (port == 67 || port == 68)) return 1; - if (port == 53 && !cfg_get(PB_CFG_DNS_VIA_PROXY)) return 1; - return 0; + if (o->proto == IPPROTO_UDP && (o->port == 67 || o->port == 68)) return 1; + return o->port == 53 && !cfg.dns_via_proxy; } -// Returns an ACT_*; for ACT_PROXY *proxy_id is set. -static __always_inline int decide(struct pb_orig *o, __u16 *proxy_id) +// Sets o->proxy_id for ACT_PROXY. +static __always_inline int decide(struct pb_orig *o) { - if (!cfg_get(PB_CFG_ENABLED)) return ACT_DIRECT; - if (o->pid == cfg_get(PB_CFG_SELF_TGID)) return ACT_DIRECT; - int tap = o->port == 53 && cfg_get(PB_CFG_DNS_TAP); - if (!tap && implicit_direct(o->family, o->addr, o->port, o->proto)) return ACT_DIRECT; + if (!cfg.enabled || o->pid == cfg.self_tgid) return ACT_DIRECT; + if (cfg.n_exempt && bpf_map_lookup_elem(&exempt, &o->pid)) return ACT_DIRECT; + int tap = o->port == 53 && cfg.dns_tap; + if (!tap && implicit_direct(o)) return ACT_DIRECT; - struct task_struct *t = bpf_get_current_task_btf(); - struct mm_struct *mm = t->mm; - if (!mm) return ACT_DIRECT; // kernel thread + struct mm_struct *mm = bpf_get_current_task_btf()->mm; + if (!mm) return ACT_DIRECT; struct pb_exe_key key = {}; key.dev = mm->exe_file->f_inode->i_sb->s_dev; key.ino = mm->exe_file->f_inode->i_ino; - o->exe_dev = key.dev; o->exe_ino = key.ino; // lets userspace learn the exact kernel-side key + o->exe_dev = key.dev; + o->exe_ino = key.ino; if (!key.ino) return ACT_DECIDE; __u32 *cid = bpf_map_lookup_elem(&exe2class, &key); - if (!cid) return ACT_DECIDE; // binary not seen yet: relay classifies it + if (!cid) return ACT_DECIDE; struct pb_bclass *c = bpf_map_lookup_elem(&classes, cid); if (!c || c->n > PB_RULES_PER_CLASS) return ACT_DECIDE; if (tap) { - // a domain rule covers this binary: its DNS answers must pass through the relay so names can be learned if (c->tap) return ACT_DECIDE; - if (implicit_direct(o->family, o->addr, o->port, o->proto)) return ACT_DIRECT; + if (implicit_direct(o)) return ACT_DIRECT; } - __u32 ip4 = (o->family == AF_INET) ? bpf_ntohl(*(const __u32 *)(o->addr + 12)) : 0; + __u32 ip4 = o->family == AF_INET ? bpf_ntohl(*(const __u32 *)(o->addr + 12)) : 0; __u8 pbit = o->proto == IPPROTO_TCP ? 1 : 2; #pragma unroll for (int i = 0; i < PB_RULES_PER_CLASS; i++) { @@ -170,47 +146,48 @@ static __always_inline int decide(struct pb_orig *o, __u16 *proxy_id) int m = rule_match(r, ri, o->family, ip4, o->port); if (m == M_NO) continue; if (m == M_MAYBE) return ACT_DECIDE; - if (r->action == ACT_PROXY) { - if (o->proto == IPPROTO_UDP && (r->flags & PB_RF_UDP_DIRECT)) return ACT_DIRECT; - *proxy_id = r->proxy_id; - } - return r->action; + if (r->action != ACT_PROXY) return r->action; + if (o->proto == IPPROTO_UDP && (r->flags & PB_RF_UDP_DIRECT)) return ACT_DIRECT; + o->proxy_id = r->proxy_id; + return ACT_PROXY; } return ACT_DIRECT; } -static __always_inline void emit(const struct pb_orig *o, int action, __u16 proxy_id) +static __always_inline void emit(const struct pb_orig *o, int action) { - if (!cfg_get(PB_CFG_LOG)) return; + if (!cfg.log) return; struct pb_event *e = bpf_ringbuf_reserve(&events, sizeof *e, 0); - if (!e) { stat_inc(3); return; } + if (!e) return; e->pid = o->pid; bpf_get_current_comm(e->comm, sizeof e->comm); __builtin_memcpy(e->addr, o->addr, 16); - e->port = o->port; e->proxy_id = proxy_id; - e->family = o->family; e->proto = o->proto; e->action = action; e->pad = 0; + e->port = o->port; + e->proxy_id = o->proxy_id; + e->family = o->family; + e->proto = o->proto; + e->action = action; + e->pad = 0; bpf_ringbuf_submit(e, 0); } -// --------------------------------------------------------------------------------------------------------------------- -// flow handling -// --------------------------------------------------------------------------------------------------------------------- static __always_inline void set_mapped_v4(__u32 *ip6, __u32 ip4_be) { ip6[0] = 0; ip6[1] = 0; ip6[2] = bpf_htonl(0x0000ffff); ip6[3] = ip4_be; } -// Fills o from a sockaddr as seen in the hook. For v6 sockets a v4-mapped address is an IPv4 destination. static __always_inline void orig_from_v4(struct pb_orig *o, __u32 ip_be, __u16 port_be, __u8 proto) { __builtin_memset(o, 0, sizeof *o); o->addr[10] = 0xff; o->addr[11] = 0xff; __builtin_memcpy(o->addr + 12, &ip_be, 4); o->port = bpf_ntohs(port_be); - o->family = AF_INET; o->proto = proto; + o->family = AF_INET; + o->proto = proto; o->pid = bpf_get_current_pid_tgid() >> 32; } +// A v4-mapped address on an IPv6 socket is an IPv4 destination. static __always_inline void orig_from_v6(struct pb_orig *o, const __u32 ip6[4], __u16 port_be, __u8 proto) { __builtin_memset(o, 0, sizeof *o); @@ -218,11 +195,10 @@ static __always_inline void orig_from_v6(struct pb_orig *o, const __u32 ip6[4], o->port = bpf_ntohs(port_be); o->proto = proto; o->pid = bpf_get_current_pid_tgid() >> 32; - if (ip6[0] == 0 && ip6[1] == 0 && ip6[2] == bpf_htonl(0x0000ffff)) o->family = AF_INET; - else o->family = AF_INET6; + o->family = (ip6[0] == 0 && ip6[1] == 0 && ip6[2] == bpf_htonl(0x0000ffff)) ? AF_INET : AF_INET6; } -// Deterministic loopback flow address (127.1-255.x.y) for a UDP destination; collisions are probed linearly. +// Loopback flow address 127.1-255.x.y standing for one UDP destination; the relay recovers the destination from it. static __always_inline __u32 udp_flow_addr(struct pb_orig *o) { const __u32 *w = (const __u32 *)o->addr; @@ -236,58 +212,42 @@ static __always_inline __u32 udp_flow_addr(struct pb_orig *o) addr = bpf_htonl(0x7f000000 | (((h + i) & 0x00ffffff) | 0x00010000)); struct pb_orig *ex = bpf_map_lookup_elem(&flows, &addr); if (!ex) { bpf_map_update_elem(&flows, &addr, o, BPF_NOEXIST); break; } - if (__builtin_memcmp(ex->addr, o->addr, 16) == 0 && ex->port == o->port && ex->proxy_id == o->proxy_id && + if (!__builtin_memcmp(ex->addr, o->addr, 16) && ex->port == o->port && ex->proxy_id == o->proxy_id && ex->action == o->action && (o->action != ACT_DECIDE || ex->pid == o->pid)) break; } return addr; } -// 0 = leave the socket call untouched, 1 = it was redirected, -1 = block -static __always_inline int route(struct pb_orig *o, __u16 *proxy_id_out) +// 0: leave the call untouched, 1: redirected to the relay, -1: block. +static __always_inline int route(struct pb_orig *o) { - __u16 pid16 = 0; - int act = decide(o, &pid16); - switch (act) { - case ACT_DIRECT: - emit(o, ACT_DIRECT, 0); - return 0; - case ACT_BLOCK: - emit(o, ACT_BLOCK, 0); - stat_inc(1); - return -1; - case ACT_PROXY: - o->action = ACT_PROXY; o->proxy_id = pid16; - emit(o, ACT_PROXY, pid16); - stat_inc(0); - break; - default: - o->action = ACT_DECIDE; o->proxy_id = 0; - stat_inc(2); - break; + int act = decide(o); + if (act == ACT_DIRECT || act == ACT_BLOCK) { + emit(o, act); + return act == ACT_BLOCK ? -1 : 0; } - *proxy_id_out = o->proxy_id; + o->action = act; + if (act == ACT_PROXY) emit(o, ACT_PROXY); + else o->proxy_id = 0; return 1; } -// ---- TCP / connected UDP: connect4 / connect6 ------------------------------------------------------------------------ SEC("cgroup/connect4") int pb_connect4(struct bpf_sock_addr *ctx) { - if (foreign_netns(ctx)) return 1; - if (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP) return 1; - struct pb_orig o; __u16 pid; + if (foreign_netns(ctx) || (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP)) return 1; + struct pb_orig o; orig_from_v4(&o, ctx->user_ip4, ctx->user_port, ctx->protocol); - int r = route(&o, &pid); - if (r < 0) return 0; - if (r == 0) return 1; + int r = route(&o); + if (r <= 0) return r == 0; if (ctx->protocol == IPPROTO_TCP) { __u64 ck = bpf_get_socket_cookie(ctx); bpf_map_update_elem(&by_cookie, &ck, &o, BPF_ANY); ctx->user_ip4 = bpf_htonl(0x7f000001); - ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_TCP_PORT)); + ctx->user_port = bpf_htons(cfg.tcp_port); } else { ctx->user_ip4 = udp_flow_addr(&o); - ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT)); + ctx->user_port = bpf_htons(cfg.udp_port); } return 1; } @@ -295,69 +255,59 @@ int pb_connect4(struct bpf_sock_addr *ctx) SEC("cgroup/connect6") int pb_connect6(struct bpf_sock_addr *ctx) { - if (foreign_netns(ctx)) return 1; - if (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP) return 1; + if (foreign_netns(ctx) || (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP)) return 1; __u32 ip6[4] = { ctx->user_ip6[0], ctx->user_ip6[1], ctx->user_ip6[2], ctx->user_ip6[3] }; - struct pb_orig o; __u16 pid; + struct pb_orig o; orig_from_v6(&o, ip6, ctx->user_port, ctx->protocol); - int r = route(&o, &pid); - if (r < 0) return 0; - if (r == 0) return 1; + int r = route(&o); + if (r <= 0) return r == 0; if (ctx->protocol == IPPROTO_TCP) { __u64 ck = bpf_get_socket_cookie(ctx); bpf_map_update_elem(&by_cookie, &ck, &o, BPF_ANY); set_mapped_v4(ctx->user_ip6, bpf_htonl(0x7f000001)); - ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_TCP_PORT)); + ctx->user_port = bpf_htons(cfg.tcp_port); } else { set_mapped_v4(ctx->user_ip6, udp_flow_addr(&o)); - ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT)); + ctx->user_port = bpf_htons(cfg.udp_port); } return 1; } -// ---- unconnected UDP -------------------------------------------------------------------------------------------------- SEC("cgroup/sendmsg4") int pb_sendmsg4(struct bpf_sock_addr *ctx) { - if (foreign_netns(ctx)) return 1; - if (ctx->protocol != IPPROTO_UDP) return 1; - struct pb_orig o; __u16 pid; + if (foreign_netns(ctx) || ctx->protocol != IPPROTO_UDP) return 1; + struct pb_orig o; orig_from_v4(&o, ctx->user_ip4, ctx->user_port, IPPROTO_UDP); - int r = route(&o, &pid); - if (r < 0) return 0; - if (r == 0) return 1; + int r = route(&o); + if (r <= 0) return r == 0; ctx->user_ip4 = udp_flow_addr(&o); - ctx->user_port = bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT)); + ctx->user_port = bpf_htons(cfg.udp_port); return 1; } +// udpv6_sendmsg refuses a hook that rewrites a native IPv6 destination to a v4-mapped address and loopback has only ::1, so +// a native IPv6 flow that must be proxied fails closed (EPERM); the application falls back to IPv4 or TCP. SEC("cgroup/sendmsg6") int pb_sendmsg6(struct bpf_sock_addr *ctx) { - if (foreign_netns(ctx)) return 1; - if (ctx->protocol != IPPROTO_UDP) return 1; + if (foreign_netns(ctx) || ctx->protocol != IPPROTO_UDP) return 1; __u32 ip6[4] = { ctx->user_ip6[0], ctx->user_ip6[1], ctx->user_ip6[2], ctx->user_ip6[3] }; - struct pb_orig o; __u16 pid; + struct pb_orig o; orig_from_v6(&o, ip6, ctx->user_port, IPPROTO_UDP); - int r = route(&o, &pid); - if (r < 0) return 0; - if (r == 0) return 1; - // The kernel (udpv6_sendmsg) refuses a sendmsg6 program that rewrites a native IPv6 destination to a v4-mapped address, - // and loopback has only ::1 so a flow id cannot be encoded in the address. A flow that must be proxied therefore fails - // closed (EPERM; applications fall back to IPv4/TCP). Flows that merely need a userspace decision pass untouched. - if (o.action == ACT_PROXY) { stat_inc(1); return 0; } - return 1; + int r = route(&o); + if (r <= 0) return r == 0; + return o.action != ACT_PROXY; } -// Replies arrive from the flow address; show the application the real peer. +// Replies come from the flow address; show the application the real peer. SEC("cgroup/recvmsg4") int pb_recvmsg4(struct bpf_sock_addr *ctx) { - if (ctx->protocol != IPPROTO_UDP || ctx->user_port != bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT))) return 1; + if (ctx->protocol != IPPROTO_UDP || ctx->user_port != bpf_htons(cfg.udp_port)) return 1; __u32 key = ctx->user_ip4; struct pb_orig *o = bpf_map_lookup_elem(&flows, &key); - if (!o) return 1; - if (o->family == AF_INET) { + if (o && o->family == AF_INET) { ctx->user_ip4 = *(__u32 *)(o->addr + 12); ctx->user_port = bpf_htons(o->port); } @@ -367,7 +317,7 @@ int pb_recvmsg4(struct bpf_sock_addr *ctx) SEC("cgroup/recvmsg6") int pb_recvmsg6(struct bpf_sock_addr *ctx) { - if (ctx->protocol != IPPROTO_UDP || ctx->user_port != bpf_htons((__u16)cfg_get(PB_CFG_UDP_PORT))) return 1; + if (ctx->protocol != IPPROTO_UDP || ctx->user_port != bpf_htons(cfg.udp_port)) return 1; if (ctx->user_ip6[0] || ctx->user_ip6[1] || ctx->user_ip6[2] != bpf_htonl(0x0000ffff)) return 1; __u32 key = ctx->user_ip6[3]; struct pb_orig *o = bpf_map_lookup_elem(&flows, &key); @@ -378,7 +328,7 @@ int pb_recvmsg6(struct bpf_sock_addr *ctx) return 1; } -// getpeername() on a redirected socket shows the original destination +// getpeername() on a redirected socket reports the original destination. SEC("cgroup/getpeername4") int pb_getpeername4(struct bpf_sock_addr *ctx) { @@ -417,7 +367,7 @@ int pb_getpeername6(struct bpf_sock_addr *ctx) return 1; } -// Hand the original destination to the relay, keyed by the client's local port. +// Hands the original destination to the relay, keyed by the client's source address and port. SEC("sockops") int pb_sockops(struct bpf_sock_ops *s) { @@ -425,8 +375,10 @@ int pb_sockops(struct bpf_sock_ops *s) __u64 ck = bpf_get_socket_cookie(s); struct pb_orig *o = bpf_map_lookup_elem(&by_cookie, &ck); if (!o) return 1; - __u32 lp = s->local_port; - bpf_map_update_elem(&by_port, &lp, o, BPF_ANY); + struct pb_port_key k = { .ip = s->local_ip4, .port = s->local_port }; + if (s->family == AF_INET6 && !s->local_ip6[0] && !s->local_ip6[1] && s->local_ip6[2] == bpf_htonl(0x0000ffff)) + k.ip = s->local_ip6[3]; + bpf_map_update_elem(&by_port, &k, o, BPF_ANY); return 1; } diff --git a/Linux/src/pb_bpf.c b/Linux/src/pb_bpf.c index d4bf74f3..df73bb3d 100644 --- a/Linux/src/pb_bpf.c +++ b/Linux/src/pb_bpf.c @@ -3,14 +3,17 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include -#include #include +#include #include #include #include @@ -20,45 +23,46 @@ #ifndef CGROUP2_SUPER_MAGIC #define CGROUP2_SUPER_MAGIC 0x63677270 #endif +#ifndef SO_NETNS_COOKIE +#define SO_NETNS_COOKIE 71 +#endif #define MAX_KNOWN_EXES 16384 +#define MAX_EXEMPT 64 -struct exe_ent { uint32_t dev; uint64_t ino; char path[240]; }; +struct exe_ent { uint32_t dev; uint64_t ino; char *path; }; static struct { pthread_mutex_t lock; struct proxybridge_bpf *skel; - int linkfds[16]; - int nlinks; + int linkfds[16], nlinks; struct ring_buffer *rb; pthread_t evt_thread; - bool evt_started; - volatile bool stop; + bool evt_started, running; + int stop_fd; pb_event_cb cb; - bool running; + struct exe_ent *exes; size_t nexes; + struct pb_bclass *cls; size_t ncls; + struct pb_brule *br; size_t nbr; +} G = { .lock = PTHREAD_MUTEX_INITIALIZER, .stop_fd = -1 }; - struct exe_ent *exes; size_t nexes; // executables we know about (kernel-side keys) - struct pb_bclass *cls; size_t ncls; // current class table (index = class id) - bool have_rules; -} G = { .lock = PTHREAD_MUTEX_INITIALIZER }; +#define MAPFD(name) bpf_map__fd(G.skel->maps.name) -// ---- logging bridge ------------------------------------------------------------------------------------------------- static int libbpf_print(enum libbpf_print_level lvl, const char *fmt, va_list ap) { if (lvl == LIBBPF_DEBUG) return 0; char buf[1024]; vsnprintf(buf, sizeof buf, fmt, ap); size_t n = strlen(buf); - while (n && (buf[n - 1] == '\n')) buf[--n] = 0; + while (n && buf[n - 1] == '\n') buf[--n] = 0; if (n) pb_core_log("libbpf: %s", buf); return 0; } -// ---- cgroup2 root ----------------------------------------------------------------------------------------------------- static int open_cgroup_root(void) { - static const char *cands[] = { "/sys/fs/cgroup", "/sys/fs/cgroup/unified", NULL }; - for (int i = 0; cands[i]; i++) { + static const char *const cands[] = { "/sys/fs/cgroup", "/sys/fs/cgroup/unified" }; + for (size_t i = 0; i < sizeof cands / sizeof *cands; i++) { struct statfs sf; if (statfs(cands[i], &sf) == 0 && (unsigned long)sf.f_type == CGROUP2_SUPER_MAGIC) return open(cands[i], O_RDONLY | O_DIRECTORY | O_CLOEXEC); @@ -66,21 +70,13 @@ static int open_cgroup_root(void) return -1; } -// ---- cfg --------------------------------------------------------------------------------------------------------------- -static void cfg_set(uint32_t slot, uint32_t val) -{ - bpf_map_update_elem(bpf_map__fd(G.skel->maps.cfg), &slot, &val, BPF_ANY); -} - -// ---- attach ------------------------------------------------------------------------------------------------------------ +// ALLOW_MULTI lets us coexist with other sock_addr programs on the root cgroup. static bool attach(struct bpf_program *prog, enum bpf_attach_type type, int cgfd, bool optional) { if (!bpf_program__autoload(prog)) return true; - // ALLOW_MULTI lets us coexist with other sock_addr programs on the root cgroup; fall back to a plain attach LIBBPF_OPTS(bpf_link_create_opts, opts, .flags = BPF_F_ALLOW_MULTI); int lfd = bpf_link_create(bpf_program__fd(prog), cgfd, type, &opts); - if (lfd < 0 && errno == EINVAL) - lfd = bpf_link_create(bpf_program__fd(prog), cgfd, type, NULL); + if (lfd < 0 && errno == EINVAL) lfd = bpf_link_create(bpf_program__fd(prog), cgfd, type, NULL); if (lfd < 0) { if (optional) return true; pb_core_log("attach %s failed: %s", bpf_program__name(prog), strerror(errno)); @@ -92,11 +88,9 @@ static bool attach(struct bpf_program *prog, enum bpf_attach_type type, int cgfd static void detach_all(void) { - for (int i = 0; i < G.nlinks; i++) close(G.linkfds[i]); - G.nlinks = 0; + while (G.nlinks) close(G.linkfds[--G.nlinks]); } -// ---- events ------------------------------------------------------------------------------------------------------------ static int on_event(void *ctx, void *data, size_t len) { (void)ctx; @@ -107,11 +101,64 @@ static int on_event(void *ctx, void *data, size_t len) static void *evt_loop(void *arg) { (void)arg; - while (!G.stop) ring_buffer__poll(G.rb, 200); + struct pollfd p[2] = { { ring_buffer__epoll_fd(G.rb), POLLIN, 0 }, { G.stop_fd, POLLIN, 0 } }; + while (poll(p, 2, -1) >= 0 || errno == EINTR) { + if (p[1].revents) break; + if (p[0].revents) ring_buffer__consume(G.rb); + } return NULL; } -// ---- start / stop ----------------------------------------------------------------------------------------------------- +static uint64_t netns_cookie(void) +{ + uint64_t cookie = 0; + socklen_t len = sizeof cookie; + int fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (fd >= 0) { + if (getsockopt(fd, SOL_SOCKET, SO_NETNS_COOKIE, &cookie, &len) != 0) cookie = 0; + close(fd); + } + return cookie; +} + +static bool attach_all(int cgfd) +{ +#define ATTACH(prog, type, optional) attach(G.skel->progs.prog, type, cgfd, optional) + return ATTACH(pb_connect4, BPF_CGROUP_INET4_CONNECT, false) && + ATTACH(pb_connect6, BPF_CGROUP_INET6_CONNECT, true) && + ATTACH(pb_sendmsg4, BPF_CGROUP_UDP4_SENDMSG, false) && + ATTACH(pb_sendmsg6, BPF_CGROUP_UDP6_SENDMSG, true) && + ATTACH(pb_recvmsg4, BPF_CGROUP_UDP4_RECVMSG, false) && + ATTACH(pb_recvmsg6, BPF_CGROUP_UDP6_RECVMSG, true) && + ATTACH(pb_getpeername4, BPF_CGROUP_INET4_GETPEERNAME, true) && + ATTACH(pb_getpeername6, BPF_CGROUP_INET6_GETPEERNAME, true) && + ATTACH(pb_sockops, BPF_CGROUP_SOCK_OPS, false); +#undef ATTACH +} + +// getpeername hooks need a newer kernel than the rest; load without them if the first attempt fails. +static int load_skeleton(void) +{ + G.skel = proxybridge_bpf__open(); + if (!G.skel) return -errno; + int err = proxybridge_bpf__load(G.skel); + if (!err) return 0; + proxybridge_bpf__destroy(G.skel); + G.skel = proxybridge_bpf__open(); + if (!G.skel) return -errno; + bpf_program__set_autoload(G.skel->progs.pb_getpeername4, false); + bpf_program__set_autoload(G.skel->progs.pb_getpeername6, false); + return proxybridge_bpf__load(G.skel); +} + +static void free_tables(void) +{ + for (size_t i = 0; i < G.nexes; i++) free(G.exes[i].path); + free(G.exes); G.exes = NULL; G.nexes = 0; + free(G.cls); G.cls = NULL; G.ncls = 0; + free(G.br); G.br = NULL; G.nbr = 0; +} + int pb_bpf_start(const struct pb_bpf_cfg *cfg, pb_event_cb cb) { pthread_mutex_lock(&G.lock); @@ -121,99 +168,85 @@ int pb_bpf_start(const struct pb_bpf_cfg *cfg, pb_event_cb cb) if (geteuid() != 0) { pb_core_log("ProxyBridge needs root (CAP_BPF/CAP_SYS_ADMIN) to load its eBPF programs"); goto out; } libbpf_set_print(libbpf_print); struct rlimit rl = { RLIM_INFINITY, RLIM_INFINITY }; - setrlimit(RLIMIT_MEMLOCK, &rl); // pre-5.11 kernels charge maps to memlock + setrlimit(RLIMIT_MEMLOCK, &rl); cgfd = open_cgroup_root(); if (cgfd < 0) { pb_core_log("cgroup v2 is not mounted (need a unified hierarchy at /sys/fs/cgroup)"); goto out; } - G.skel = proxybridge_bpf__open(); - if (!G.skel) { pb_core_log("cannot open BPF object: %s", strerror(errno)); goto out; } - int err = proxybridge_bpf__load(G.skel); + int err = load_skeleton(); if (err) { - // getpeername hooks need kernel >= 5.8; retry without them - proxybridge_bpf__destroy(G.skel); - G.skel = proxybridge_bpf__open(); - if (G.skel) { - bpf_program__set_autoload(G.skel->progs.pb_getpeername4, false); - bpf_program__set_autoload(G.skel->progs.pb_getpeername6, false); - err = proxybridge_bpf__load(G.skel); - } - if (err || !G.skel) { pb_core_log("loading the BPF programs failed (kernel needs BTF and cgroup sock_addr support, >= 5.11): %s", strerror(-err ? -err : errno)); goto out; } + pb_core_log("loading the BPF programs failed (needs a kernel >= 5.11 with BTF and cgroup sock_addr support): %s", strerror(-err)); + goto out; } - { // sockets of other network namespaces (containers) must not be redirected to our loopback relay - int ns = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); - uint64_t cookie = 0; socklen_t cl = sizeof cookie; - if (ns >= 0 && getsockopt(ns, SOL_SOCKET, 71 /* SO_NETNS_COOKIE */, &cookie, &cl) == 0 && cookie) { - cfg_set(PB_CFG_NETNS_LO, (uint32_t)cookie); - cfg_set(PB_CFG_NETNS_HI, (uint32_t)(cookie >> 32)); - } else pb_core_log("kernel has no SO_NETNS_COOKIE: connections from other network namespaces are not isolated"); - if (ns >= 0) close(ns); - } - cfg_set(PB_CFG_SELF_TGID, cfg->self_tgid); - cfg_set(PB_CFG_TCP_PORT, cfg->tcp_port); - cfg_set(PB_CFG_UDP_PORT, cfg->udp_port); - cfg_set(PB_CFG_LOG, cfg->log); - cfg_set(PB_CFG_DNS_VIA_PROXY, cfg->dns_via_proxy); - cfg_set(PB_CFG_ENABLED, 0); - - bool ok = attach(G.skel->progs.pb_connect4, BPF_CGROUP_INET4_CONNECT, cgfd, false) && - attach(G.skel->progs.pb_connect6, BPF_CGROUP_INET6_CONNECT, cgfd, true) && - attach(G.skel->progs.pb_sendmsg4, BPF_CGROUP_UDP4_SENDMSG, cgfd, false) && - attach(G.skel->progs.pb_sendmsg6, BPF_CGROUP_UDP6_SENDMSG, cgfd, true) && - attach(G.skel->progs.pb_recvmsg4, BPF_CGROUP_UDP4_RECVMSG, cgfd, false) && - attach(G.skel->progs.pb_recvmsg6, BPF_CGROUP_UDP6_RECVMSG, cgfd, true) && - attach(G.skel->progs.pb_getpeername4, BPF_CGROUP_INET4_GETPEERNAME, cgfd, true) && - attach(G.skel->progs.pb_getpeername6, BPF_CGROUP_INET6_GETPEERNAME, cgfd, true) && - attach(G.skel->progs.pb_sockops, BPF_CGROUP_SOCK_OPS, cgfd, false); - if (!ok) goto out; + volatile struct pb_cfg *c = &G.skel->bss->cfg; + c->tcp_port = cfg->tcp_port; + c->udp_port = cfg->udp_port; + c->self_tgid = cfg->self_tgid; + c->log = cfg->log; + c->dns_via_proxy = cfg->dns_via_proxy; + c->netns = netns_cookie(); + if (!c->netns) pb_core_log("kernel has no SO_NETNS_COOKIE: connections from other network namespaces are not isolated"); + + if (!attach_all(cgfd)) goto out; G.cb = cb; - G.stop = false; - G.rb = ring_buffer__new(bpf_map__fd(G.skel->maps.events), on_event, NULL, NULL); - if (!G.rb) { pb_core_log("cannot create event ring buffer"); goto out; } - if (pthread_create(&G.evt_thread, NULL, evt_loop, NULL) != 0) { pb_core_log("cannot start event thread"); goto out; } - G.evt_started = true; - G.running = true; + G.stop_fd = eventfd(0, EFD_CLOEXEC); + G.rb = ring_buffer__new(MAPFD(events), on_event, NULL, NULL); + if (G.stop_fd < 0 || !G.rb || pthread_create(&G.evt_thread, NULL, evt_loop, NULL) != 0) { + pb_core_log("cannot start the event reader"); + goto out; + } + G.evt_started = G.running = true; rc = 0; out: if (cgfd >= 0) close(cgfd); if (rc != 0) { detach_all(); if (G.rb) { ring_buffer__free(G.rb); G.rb = NULL; } + if (G.stop_fd >= 0) { close(G.stop_fd); G.stop_fd = -1; } if (G.skel) { proxybridge_bpf__destroy(G.skel); G.skel = NULL; } } pthread_mutex_unlock(&G.lock); return rc; } -void pb_bpf_stop(void) +void pb_bpf_detach(void) { pthread_mutex_lock(&G.lock); - if (G.skel) { - G.stop = true; - if (G.evt_started) { pthread_mutex_unlock(&G.lock); pthread_join(G.evt_thread, NULL); pthread_mutex_lock(&G.lock); G.evt_started = false; } - detach_all(); - if (G.rb) { ring_buffer__free(G.rb); G.rb = NULL; } - proxybridge_bpf__destroy(G.skel); - G.skel = NULL; + if (G.evt_started) { + eventfd_write(G.stop_fd, 1); + pthread_mutex_unlock(&G.lock); + pthread_join(G.evt_thread, NULL); + pthread_mutex_lock(&G.lock); + G.evt_started = false; } - free(G.exes); G.exes = NULL; G.nexes = 0; - free(G.cls); G.cls = NULL; G.ncls = 0; - G.running = false; + detach_all(); pthread_mutex_unlock(&G.lock); } -bool pb_bpf_running(void) { return G.running; } +void pb_bpf_stop(void) +{ + pb_bpf_detach(); + pthread_mutex_lock(&G.lock); + if (G.rb) { ring_buffer__free(G.rb); G.rb = NULL; } + if (G.stop_fd >= 0) { close(G.stop_fd); G.stop_fd = -1; } + if (G.skel) { proxybridge_bpf__destroy(G.skel); G.skel = NULL; } + free_tables(); + G.running = false; + pthread_mutex_unlock(&G.lock); +} void pb_bpf_set_flags(bool log, bool dns_via_proxy) { pthread_mutex_lock(&G.lock); - if (G.skel) { cfg_set(PB_CFG_LOG, log); cfg_set(PB_CFG_DNS_VIA_PROXY, dns_via_proxy); } + if (G.skel) { + G.skel->bss->cfg.log = log; + G.skel->bss->cfg.dns_via_proxy = dns_via_proxy; + } pthread_mutex_unlock(&G.lock); } -// ---- executables -------------------------------------------------------------------------------------------------------- static uint32_t kernel_dev(dev_t d) { return (major(d) << 20) | minor(d); } static struct exe_ent *exe_find(uint32_t dev, uint64_t ino) @@ -226,20 +259,21 @@ static struct exe_ent *exe_find(uint32_t dev, uint64_t ino) static struct exe_ent *exe_add(uint32_t dev, uint64_t ino, const char *path) { struct exe_ent *e = exe_find(dev, ino); - if (e) { if (path && *path) snprintf(e->path, sizeof e->path, "%s", path); return e; } + if (e) return e; if (G.nexes >= MAX_KNOWN_EXES) return NULL; - if (!G.exes) { G.exes = calloc(MAX_KNOWN_EXES, sizeof *G.exes); if (!G.exes) return NULL; } + if (!G.exes && !(G.exes = calloc(MAX_KNOWN_EXES, sizeof *G.exes))) return NULL; + char *copy = strdup(path ? path : ""); + if (!copy) return NULL; e = &G.exes[G.nexes++]; - e->dev = dev; e->ino = ino; - snprintf(e->path, sizeof e->path, "%s", path ? path : ""); + e->dev = dev; e->ino = ino; e->path = copy; return e; } static void strip_deleted(char *p) { - size_t n = strlen(p); - static const char suf[] = " (deleted)"; - if (n > sizeof suf - 1 && !strcmp(p + n - (sizeof suf - 1), suf)) p[n - (sizeof suf - 1)] = 0; + static const char suffix[] = " (deleted)"; + size_t n = strlen(p), s = sizeof suffix - 1; + if (n > s && !strcmp(p + n - s, suffix)) p[n - s] = 0; } static void scan_proc(void) @@ -249,11 +283,11 @@ static void scan_proc(void) struct dirent *de; while ((de = readdir(d))) { if (de->d_name[0] < '1' || de->d_name[0] > '9') continue; - char lnk[300], path[240]; - snprintf(lnk, sizeof lnk, "/proc/%.200s/exe", de->d_name); + char link[300], path[4096]; struct stat st; - if (stat(lnk, &st) != 0) continue; - ssize_t n = readlink(lnk, path, sizeof path - 1); + snprintf(link, sizeof link, "/proc/%.200s/exe", de->d_name); + if (stat(link, &st) != 0) continue; + ssize_t n = readlink(link, path, sizeof path - 1); if (n <= 0) continue; path[n] = 0; strip_deleted(path); @@ -262,7 +296,6 @@ static void scan_proc(void) closedir(d); } -// ---- compilation ----------------------------------------------------------------------------------------------------------- static void compile_rule(const struct pb_rule_view *v, struct pb_brule *b) { const struct pb_rule *r = v->rule; @@ -273,42 +306,41 @@ static void compile_rule(const struct pb_rule_view *v, struct pb_brule *b) if (v->action == RULE_ACTION_PROXY && v->udp_direct) b->flags |= PB_RF_UDP_DIRECT; if (r->any_port) b->flags |= PB_RF_ANY_PORT; - else { - for (int i = 0; i < r->nports; i++) { - if (b->n_port < PB_BPF_MAX_PORT) { b->port[b->n_port].lo = r->ports[i].lo; b->port[b->n_port].hi = r->ports[i].hi; b->n_port++; } - else b->flags |= PB_RF_PORT_COMPLEX; - } + for (int i = 0; i < r->nports; i++) { + if (b->n_port < PB_BPF_MAX_PORT) { b->port[b->n_port].lo = r->ports[i].lo; b->port[b->n_port].hi = r->ports[i].hi; b->n_port++; } + else b->flags |= PB_RF_PORT_COMPLEX; } - if (r->any_host) b->flags |= PB_RF_ANY_HOST; - else { - if (r->ndomains) b->flags |= PB_RF_DOMAINS; - for (int i = 0; i < r->nhosts; i++) { - const struct pb_host_pat *h = &r->hosts[i]; - if (h->kind == PB_HOST_V4_RANGE && b->n_v4 < PB_BPF_MAX_V4) { b->v4[b->n_v4].lo = h->lo4; b->v4[b->n_v4].hi = h->hi4; b->n_v4++; } - else if (h->kind == PB_HOST_V6_RANGE) b->flags |= PB_RF_HAS_V6; - else b->flags |= PB_RF_HOST_COMPLEX; // box patterns / too many ranges: let the relay decide - } + if (r->any_host) { b->flags |= PB_RF_ANY_HOST; return; } + if (r->ndomains) b->flags |= PB_RF_DOMAINS; + for (int i = 0; i < r->nhosts; i++) { + const struct pb_host_pat *h = &r->hosts[i]; + if (h->kind == PB_HOST_V4_RANGE && b->n_v4 < PB_BPF_MAX_V4) { b->v4[b->n_v4].lo = h->lo4; b->v4[b->n_v4].hi = h->hi4; b->n_v4++; } + else if (h->kind == PB_HOST_V6_RANGE) b->flags |= PB_RF_HAS_V6; + else b->flags |= PB_RF_HOST_COMPLEX; } } -// a rule that cannot be skipped by later ones: every host, every port, both protocols -static bool rule_is_catchall(const struct pb_brule *b) +// A catch-all rule shadows every later one. +static bool is_catchall(const struct pb_brule *b) { return (b->flags & PB_RF_ANY_HOST) && (b->flags & PB_RF_ANY_PORT) && b->proto_mask == 3; } -static int class_for(const struct pb_rule_view *v, const struct pb_brule *br, size_t n, const char *path, struct pb_bclass *out) +static void class_for(const struct pb_rule_view *v, const struct pb_brule *br, size_t n, const char *path, struct pb_bclass *out) { memset(out, 0, sizeof *out); for (size_t i = 0; i < n; i++) { - if (!v[i].rule->enabled) continue; - if (!pb_rule_match_process(v[i].rule, path)) continue; + if (!v[i].rule->enabled || !pb_rule_match_process(v[i].rule, path)) continue; if (pb_rule_has_domains(v[i].rule)) out->tap = 1; - if (out->n >= PB_RULES_PER_CLASS) { memset(out, 0, sizeof *out); out->n = PB_CLASS_DECIDE; out->tap = 1; return 0; } + if (out->n >= PB_RULES_PER_CLASS) { + memset(out->rule, 0, sizeof out->rule); + out->n = PB_CLASS_DECIDE; + out->tap = 1; + return; + } out->rule[out->n++] = (uint32_t)i; - if (rule_is_catchall(&br[i])) break; // nothing after a catch-all can apply + if (is_catchall(&br[i])) return; } - return 0; } static int class_intern(const struct pb_bclass *c) @@ -320,9 +352,39 @@ static int class_intern(const struct pb_bclass *c) return (int)G.ncls++; } -static void write_class_slot(uint32_t id) +static void write_class(uint32_t id) +{ + bpf_map_update_elem(MAPFD(classes), &id, &G.cls[id], BPF_ANY); +} + +static void install_exe(const struct exe_ent *e, const struct pb_rule_view *v, size_t n) { - bpf_map_update_elem(bpf_map__fd(G.skel->maps.classes), &id, &G.cls[id], BPF_ANY); + struct pb_bclass c; + class_for(v, G.br, n, e->path, &c); + struct pb_exe_key key = { .dev = e->dev, .ino = e->ino }; + int id = class_intern(&c); + if (id < 0) { bpf_map_delete_elem(MAPFD(exe2class), &key); return; } + uint32_t cid = (uint32_t)id; + write_class((uint32_t)id); + bpf_map_update_elem(MAPFD(exe2class), &key, &cid, BPF_ANY); +} + +static size_t installed_keys(struct pb_exe_key **out) +{ + size_t n = 0, cap = 0; + struct pb_exe_key *keys = NULL, cur, next; + int fd = MAPFD(exe2class), rc = bpf_map_get_next_key(fd, NULL, &next); + while (rc == 0) { + if (n == cap) { + struct pb_exe_key *grown = realloc(keys, (cap ? cap * 2 : 256) * sizeof *keys); + if (!grown) break; + keys = grown; cap = cap ? cap * 2 : 256; + } + keys[n++] = cur = next; + rc = bpf_map_get_next_key(fd, &cur, &next); + } + *out = keys; + return n; } int pb_bpf_sync(const struct pb_rule_view *v, size_t n) @@ -334,123 +396,100 @@ int pb_bpf_sync(const struct pb_rule_view *v, size_t n) struct pb_brule *br = calloc(n ? n : 1, sizeof *br); if (!G.cls) G.cls = calloc(PB_MAX_CLASSES, sizeof *G.cls); if (!br || !G.cls) { free(br); pthread_mutex_unlock(&G.lock); return -1; } + free(G.br); + G.br = br; G.nbr = n; - size_t active = 0; + bool active = false, domains = false; for (size_t i = 0; i < n; i++) { compile_rule(&v[i], &br[i]); uint32_t k = (uint32_t)i; - bpf_map_update_elem(bpf_map__fd(G.skel->maps.brules), &k, &br[i], BPF_ANY); - if (v[i].rule->enabled) active++; + bpf_map_update_elem(MAPFD(brules), &k, &br[i], BPF_ANY); + if (v[i].rule->enabled) { + active = true; + domains |= pb_rule_has_domains(v[i].rule) != 0; + } } scan_proc(); - G.ncls = 0; memset(G.cls, 0, PB_MAX_CLASSES * sizeof *G.cls); - G.ncls = 1; // class 0 = no rule applies (direct) - int mapfd = bpf_map__fd(G.skel->maps.exe2class); - - // remember what was installed so entries for vanished binaries can be dropped - struct pb_exe_key k = {}, next; - struct pb_exe_key *old = NULL; size_t nold = 0, capold = 0; - int ret = bpf_map_get_next_key(mapfd, NULL, &next); - while (ret == 0) { - if (nold == capold) { capold = capold ? capold * 2 : 256; old = realloc(old, capold * sizeof *old); if (!old) break; } - old[nold++] = next; - k = next; - ret = bpf_map_get_next_key(mapfd, &k, &next); - } - - for (size_t i = 0; i < G.nexes; i++) { - struct pb_bclass c; - class_for(v, br, n, G.exes[i].path, &c); - int id = class_intern(&c); - struct pb_exe_key key = { .dev = G.exes[i].dev, .ino = G.exes[i].ino }; - if (id < 0) { bpf_map_delete_elem(mapfd, &key); continue; } // out of classes: slow path - uint32_t cid = (uint32_t)id; - bpf_map_update_elem(mapfd, &key, &cid, BPF_ANY); - } - for (size_t i = 0; i < G.ncls; i++) write_class_slot((uint32_t)i); - for (size_t i = 0; i < nold; i++) - if (!exe_find(old[i].dev, old[i].ino)) bpf_map_delete_elem(mapfd, &old[i]); - free(old); - free(br); - - bool domains = false; - for (size_t i = 0; i < n; i++) domains |= v[i].rule->enabled && pb_rule_has_domains(v[i].rule); - cfg_set(PB_CFG_DNS_TAP, domains ? 1 : 0); - G.have_rules = active > 0; - cfg_set(PB_CFG_ENABLED, G.have_rules ? 1 : 0); + G.ncls = 1; // class 0: no rule applies + struct pb_exe_key *stale; + size_t nstale = installed_keys(&stale); + for (size_t i = 0; i < G.nexes; i++) install_exe(&G.exes[i], v, n); + write_class(0); + for (size_t i = 0; i < nstale; i++) + if (!exe_find(stale[i].dev, stale[i].ino)) bpf_map_delete_elem(MAPFD(exe2class), &stale[i]); + free(stale); + + G.skel->bss->cfg.dns_tap = domains; + G.skel->bss->cfg.enabled = active; pthread_mutex_unlock(&G.lock); return 0; } void pb_bpf_learn(const struct pb_orig *o, const char *exe_path, const struct pb_rule_view *v, size_t n) { - if (!o->exe_ino || n > PB_MAX_RULES) return; + if (!o->exe_ino) return; pthread_mutex_lock(&G.lock); - if (!G.skel || !G.cls) { pthread_mutex_unlock(&G.lock); return; } - struct exe_ent *known = exe_find(o->exe_dev, o->exe_ino); - struct exe_ent *e = known ? NULL : exe_add(o->exe_dev, o->exe_ino, exe_path); // already classified: nothing to learn - if (e) { - struct pb_brule *br = calloc(n ? n : 1, sizeof *br); - if (br) { - for (size_t i = 0; i < n; i++) compile_rule(&v[i], &br[i]); - struct pb_bclass c; - class_for(v, br, n, e->path, &c); - int id = class_intern(&c); - if (id >= 0) { - write_class_slot((uint32_t)id); - struct pb_exe_key key = { .dev = e->dev, .ino = e->ino }; - uint32_t cid = (uint32_t)id; - bpf_map_update_elem(bpf_map__fd(G.skel->maps.exe2class), &key, &cid, BPF_ANY); - } - free(br); - } + if (G.skel && G.cls && n == G.nbr && !exe_find(o->exe_dev, o->exe_ino)) { + struct exe_ent *e = exe_add(o->exe_dev, o->exe_ino, exe_path); + if (e) install_exe(e, v, n); } pthread_mutex_unlock(&G.lock); } -// ---- lookups for the relay ------------------------------------------------------------------------------------------------- -int pb_bpf_take_tcp_orig(uint16_t client_port, struct pb_orig *out) +void pb_bpf_set_exempt(const uint32_t *tgids, size_t n) { - uint32_t key = client_port; - int fd = G.skel ? bpf_map__fd(G.skel->maps.by_port) : -1; - if (fd < 0 || bpf_map_lookup_elem(fd, &key, out) != 0) return -1; - bpf_map_delete_elem(fd, &key); - return 0; -} - -int pb_bpf_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out) -{ - int fd = G.skel ? bpf_map__fd(G.skel->maps.flows) : -1; - return (fd >= 0 && bpf_map_lookup_elem(fd, &flow_ip_be, out) == 0) ? 0 : -1; + pthread_mutex_lock(&G.lock); + if (!G.skel) { pthread_mutex_unlock(&G.lock); return; } + int fd = MAPFD(exempt); + uint32_t cur, next, stale[MAX_EXEMPT]; + size_t nstale = 0; + for (int rc = bpf_map_get_next_key(fd, NULL, &next); rc == 0 && nstale < MAX_EXEMPT; rc = bpf_map_get_next_key(fd, &cur, &next)) { + cur = next; + bool keep = false; + for (size_t i = 0; i < n && !keep; i++) keep = tgids[i] == cur; + if (!keep) stale[nstale++] = cur; + } + for (size_t i = 0; i < nstale; i++) bpf_map_delete_elem(fd, &stale[i]); + uint8_t one = 1; + size_t count = n < MAX_EXEMPT ? n : MAX_EXEMPT; + for (size_t i = 0; i < count; i++) bpf_map_update_elem(fd, &tgids[i], &one, BPF_ANY); + G.skel->bss->cfg.n_exempt = (uint32_t)count; + pthread_mutex_unlock(&G.lock); } -// ---- domain rules ------------------------------------------------------------------------------------------------------------ void pb_bpf_dom_set(uint32_t rule_idx, uint32_t ip_host) { if (!G.skel) return; struct pb_dom_key k = { .rule = rule_idx, .ip = ip_host }; uint8_t one = 1; - bpf_map_update_elem(bpf_map__fd(G.skel->maps.dom_hit), &k, &one, BPF_ANY); + bpf_map_update_elem(MAPFD(dom_hit), &k, &one, BPF_ANY); } void pb_bpf_dom_clear(void) { if (!G.skel) return; - int fd = bpf_map__fd(G.skel->maps.dom_hit); + int fd = MAPFD(dom_hit); struct pb_dom_key k, next; - int rc = bpf_map_get_next_key(fd, NULL, &next); - while (rc == 0) { + for (int rc = bpf_map_get_next_key(fd, NULL, &next); rc == 0;) { k = next; rc = bpf_map_get_next_key(fd, &k, &next); bpf_map_delete_elem(fd, &k); } } -void pb_bpf_set_dns_tap(bool on) +int pb_bpf_take_tcp_orig(uint32_t client_ip_be, uint16_t client_port, struct pb_orig *out) { - pthread_mutex_lock(&G.lock); - if (G.skel) cfg_set(PB_CFG_DNS_TAP, on ? 1 : 0); - pthread_mutex_unlock(&G.lock); + if (!G.skel) return -1; + struct pb_port_key key = { .ip = client_ip_be, .port = client_port }; + int fd = MAPFD(by_port); + if (bpf_map_lookup_elem(fd, &key, out) != 0) return -1; + bpf_map_delete_elem(fd, &key); + return 0; +} + +int pb_bpf_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out) +{ + return G.skel && bpf_map_lookup_elem(MAPFD(flows), &flow_ip_be, out) == 0 ? 0 : -1; } diff --git a/Linux/src/pb_bpf.h b/Linux/src/pb_bpf.h index 2a4c7daf..56abe6bb 100644 --- a/Linux/src/pb_bpf.h +++ b/Linux/src/pb_bpf.h @@ -1,4 +1,4 @@ -// eBPF backend: loads/attaches the datapath, compiles rules into BPF maps, reads flow events. +// eBPF backend: loads and attaches the datapath, compiles rules into BPF maps, delivers flow events. #ifndef PB_BPF_H #define PB_BPF_H @@ -7,40 +7,41 @@ #include "bpf/pb_bpf_shared.h" struct pb_bpf_cfg { - uint16_t tcp_port, udp_port; // relay ports (already bound by the relay) - uint32_t self_tgid; // process hosting the relay; never redirected + uint16_t tcp_port, udp_port; + uint32_t self_tgid; bool log; bool dns_via_proxy; }; -// One rule as seen by the compiler: the parsed rule plus its resolved proxy. +// A rule as seen by the compiler: the parsed rule plus its effective action and proxy. struct pb_rule_view { const struct pb_rule *rule; - int action; // effective action (PROXY with no usable proxy has already become BLOCK) - uint16_t proxy_id; // resolved (0 for DIRECT/BLOCK) - bool udp_direct; // PROXY rule whose proxy is HTTP: UDP goes direct + int action; // PROXY with no usable proxy has already become BLOCK + uint16_t proxy_id; + bool udp_direct; // the proxy cannot carry UDP }; typedef void (*pb_event_cb)(const struct pb_event *e); -// Returns 0 on success; on failure returns -1 after logging the reason (kernel too old, not root, no cgroup2, verifier...). +// Returns 0, or -1 after logging why (not root, kernel too old, no cgroup2, verifier...). int pb_bpf_start(const struct pb_bpf_cfg *cfg, pb_event_cb cb); +// Detaches the hooks and stops the event reader; the maps stay valid until pb_bpf_stop() (relay threads may still look up). +void pb_bpf_detach(void); void pb_bpf_stop(void); -bool pb_bpf_running(void); void pb_bpf_set_flags(bool log, bool dns_via_proxy); -// Recompiles the rule set (and rescans /proc for running executables). Returns -1 if it does not fit (too many rules). +// Recompiles the rule set and rescans /proc for running executables. -1 if it does not fit. int pb_bpf_sync(const struct pb_rule_view *v, size_t n); -// Slow-path learning: install the class for the exact executable key the kernel reported in `o`, using the same rule set -// that decide() just evaluated. +// Installs the class of the executable the kernel reported in `o` (slow-path learning); no-op for known binaries. void pb_bpf_learn(const struct pb_orig *o, const char *exe_path, const struct pb_rule_view *v, size_t n); +// Processes (tgids) whose traffic is never redirected, e.g. the local proxy itself. +void pb_bpf_set_exempt(const uint32_t *tgids, size_t n); -// Domain rules: (rule index, address) pairs whose DNS name matches. Set by the DNS tap before the answer reaches the client. +// (rule, address) pairs whose DNS name matches a domain rule. void pb_bpf_dom_set(uint32_t rule_idx, uint32_t ip_host); void pb_bpf_dom_clear(void); -void pb_bpf_set_dns_tap(bool on); -int pb_bpf_take_tcp_orig(uint16_t client_port, struct pb_orig *out); +int pb_bpf_take_tcp_orig(uint32_t client_ip_be, uint16_t client_port, struct pb_orig *out); int pb_bpf_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out); #endif diff --git a/Linux/src/pb_dns.c b/Linux/src/pb_dns.c index 8906d488..c78050aa 100644 --- a/Linux/src/pb_dns.c +++ b/Linux/src/pb_dns.c @@ -95,7 +95,6 @@ static void add_mapping(uint32_t ip, char names[][256], int nn, struct pb_dns_re } } -// ---- wire format ----------------------------------------------------------------------------------------------------- // Reads a (possibly compressed) name at *off into out (lower-case, dotted). Advances *off past the name as stored at that // position. Returns false on malformed data. @@ -150,7 +149,6 @@ int pb_dns_observe(const uint8_t *m, size_t len, struct pb_dns_result *res) } uint32_t ips[64]; int nips = 0; - // first pass collects the chain of names (CNAME owners/targets and A owners), second pass assigns them to every address struct { size_t rdata; uint16_t type, rdlen; char owner[256]; } rr[128]; int nrr = 0; for (int i = 0; i < an; i++) { @@ -163,12 +161,10 @@ int pb_dns_observe(const uint8_t *m, size_t len, struct pb_dns_result *res) off = rd + rdlen; } for (int i = 0; i < nrr; i++) { - bool addname = false; char target[256]; if (rr[i].type == 5) { // CNAME: owner and target are both names of the final address size_t o2 = rr[i].rdata; if (read_name(m, len, &o2, target, sizeof target)) { - addname = true; for (int pass = 0; pass < 2; pass++) { const char *nm = pass ? target : rr[i].owner; bool dup = false; @@ -184,7 +180,6 @@ int pb_dns_observe(const uint8_t *m, size_t len, struct pb_dns_result *res) if (!dup && nn < PB_DNS_MAX_NAMES) strcpy(names[nn++], rr[i].owner); } } - (void)addname; } if (!nips || !nn) return 0; diff --git a/Linux/src/pb_internal.h b/Linux/src/pb_internal.h index 83e72f0f..7a15989f 100644 --- a/Linux/src/pb_internal.h +++ b/Linux/src/pb_internal.h @@ -1,10 +1,4 @@ -// Internal interfaces shared by the ProxyBridge Linux modules (eBPF backend). -// -// proxybridge.bpf.c kernel side: rewrites connect()/sendmsg() destinations to the relay -// pb_rules.c rule store, validation, full userspace matcher -// pb_bpf.c loads/attaches BPF, compiles rules into BPF maps, reads events -// pb_relay.c userspace relay: TCP (SOCKS5 / HTTP CONNECT), UDP (SOCKS5 UDP ASSOCIATE), DIRECT -// ProxyBridge.c public API glue +// Interfaces between the ProxyBridge modules: core (ProxyBridge.c, pb_bpf.c), kernel program, relay (pb_relay.c). #ifndef PB_INTERNAL_H #define PB_INTERNAL_H @@ -14,108 +8,87 @@ #include #include "ProxyBridge.h" -#define PB_TCP_RELAY_PORT_DEFAULT 24010 // outside the default ephemeral range 32768-60999 +#define PB_TCP_RELAY_PORT_DEFAULT 24010 // outside the default ephemeral port range #define PB_UDP_RELAY_PORT_DEFAULT 24011 -// --------------------------------------------------------------------------------------------- -// Decision values -// --------------------------------------------------------------------------------------------- -#define PB_ACT_PROXY 0 // matches RuleAction +#define PB_ACT_PROXY 0 // same values as RuleAction #define PB_ACT_DIRECT 1 #define PB_ACT_BLOCK 2 -#define PB_ACT_DECIDE 3 // kernel could not decide: relay must call pb_core_decide() +#define PB_ACT_DECIDE 3 // the kernel could not decide: the relay calls pb_core_decide() -// --------------------------------------------------------------------------------------------- -// Original destination record. Produced by the BPF programs, looked up by the relay. -// Layout is shared with the BPF object: keep in sync with proxybridge.bpf.c (struct pb_orig). -// --------------------------------------------------------------------------------------------- +// Original destination of a redirected flow; layout shared with proxybridge.bpf.c. struct pb_orig { - uint8_t addr[16]; // original destination, network order. IPv4 is stored as ::ffff:a.b.c.d - uint16_t port; // original destination port, host order - uint16_t proxy_id; // valid when action == PB_ACT_PROXY + uint8_t addr[16]; // network order; IPv4 as ::ffff:a.b.c.d + uint16_t port; // host order + uint16_t proxy_id; uint32_t pid; // tgid of the process that opened the flow - uint8_t family; // AF_INET or AF_INET6 (of the ORIGINAL destination) + uint8_t family; // AF_INET or AF_INET6 of the original destination uint8_t proto; // IPPROTO_TCP or IPPROTO_UDP - uint8_t action; // PB_ACT_PROXY or PB_ACT_DECIDE (DIRECT/BLOCK never reach the relay from the kernel) + uint8_t action; // PB_ACT_PROXY or PB_ACT_DECIDE uint8_t pad; - uint32_t exe_dev; // kernel-side (dev, inode) of the process's executable (valid for PB_ACT_DECIDE flows) + uint32_t exe_dev; // kernel-side identity of the executable (PB_ACT_DECIDE flows) uint32_t exe_pad; uint64_t exe_ino; }; -// Result of a userspace decision (slow path). struct pb_decision { - int action; // PB_ACT_PROXY / PB_ACT_DIRECT / PB_ACT_BLOCK - uint32_t proxy_id; // for PB_ACT_PROXY + int action; // PB_ACT_PROXY, PB_ACT_DIRECT or PB_ACT_BLOCK + uint32_t proxy_id; }; -// A configured upstream proxy (snapshot copy handed to the relay; the relay owns the copy). struct pb_proxy { uint32_t id; - ProxyType type; // PROXY_TYPE_HTTP / PROXY_TYPE_SOCKS5 + ProxyType type; char name[64]; - char host[256]; // IP literal or hostname (relay must resolve it, with a timeout, outside the epoll loop) + char host[256]; // IP literal or host name uint16_t port; - char user[256]; // empty string = no auth + char user[256]; // empty = no authentication char pass[256]; }; -// --------------------------------------------------------------------------------------------- -// Services the CORE provides to the relay (implemented in pb_bpf.c / ProxyBridge.c) -// --------------------------------------------------------------------------------------------- +// ---- provided by the core ---- -// TCP: original destination for a relayed connection, keyed by the *client's* source port as seen by the relay -// (accept() peer port; peer address is 127.0.0.1). Consumes (deletes) the entry. Returns 0 on success. -int pb_core_take_tcp_orig(uint16_t client_port, struct pb_orig *out); +// Original destination of a relayed TCP connection, keyed by the client's source address and port as accept() reports them. +// Consumes the entry; 0 on success. +int pb_core_take_tcp_orig(uint32_t client_ip_be, uint16_t client_port, struct pb_orig *out); -// UDP: original destination for a datagram, keyed by the loopback address it was sent to (IP_PKTINFO ipi_addr, network -// order). Does not delete (flows are shared and LRU-evicted). Returns 0 on success. +// Original destination behind a UDP flow address (the IP_PKTINFO destination of a datagram). Does not consume. int pb_core_get_udp_flow(uint32_t flow_ip_be, struct pb_orig *out); -// Slow-path decision for a pb_orig with action == PB_ACT_DECIDE (evaluates the full rule set for o->pid). +// Full rule evaluation for a PB_ACT_DECIDE flow. void pb_core_decide(const struct pb_orig *o, struct pb_decision *out); -// Copies the proxy config with this id (or the default proxy when id == 0). Returns 0 on success, -1 if unknown. +// Copy of the proxy with this id, or of the default proxy for id 0. 0 on success. int pb_core_get_proxy(uint32_t id, struct pb_proxy *out); -// Logging / events. Both thread-safe, may be called from any relay thread. void pb_core_log(const char *fmt, ...) __attribute__((format(printf, 1, 2))); -// One event per relayed flow after the final action is known (proxy_desc e.g. "SOCKS5 127.0.0.1:1080" / "DIRECT" / "BLOCK"). -void pb_core_flow_event(const struct pb_orig *o, int action, const char *proxy_desc); -// DNS tap. The relay calls this for every DNS message travelling server->client on a flow whose ORIGINAL destination port is 53: -// the payload of each UDP reply datagram, or each complete TCP-DNS message (2-byte length prefix already stripped). It must be -// called BEFORE the message is delivered to the client, because the core uses it to arm kernel rules for the addresses in the -// answer. Thread-safe and cheap when no domain rules exist (returns immediately). +// Reports a relayed flow once its final action is known. +void pb_core_flow_event(const struct pb_orig *o, int action); + +// DNS tap: called for every DNS message travelling server-to-client on a flow whose original port is 53 (UDP payload, or one +// complete TCP message without its length prefix), before the client receives it. void pb_core_dns_observe(const uint8_t *msg, size_t len); -// Whether DNS (UDP/TCP port 53) should be sent through the proxy even if rules say DIRECT is not set. Informational. -bool pb_core_dns_via_proxy(void); +// ---- provided by the relay ---- -// --------------------------------------------------------------------------------------------- -// Services the RELAY provides to the core (implemented in pb_relay.c) -// --------------------------------------------------------------------------------------------- struct pb_relay_cfg { - uint16_t tcp_port; // listen port on 127.0.0.1 (0 = PB_TCP_RELAY_PORT_DEFAULT) - uint16_t udp_port; // UDP listen port, bound with SO_BINDTODEVICE "lo" (0 = default) - int max_tcp_conns; // hard cap on concurrent relayed TCP connections (0 = 32768) + uint16_t tcp_port; // 0 = default + uint16_t udp_port; // 0 = default + int max_tcp_conns; // 0 = 32768 int tcp_idle_timeout_s; // 0 = 300 int udp_idle_timeout_s; // 0 = 60 - int handshake_timeout_s; // proxy connect + handshake budget (0 = 10) + int handshake_timeout_s; // 0 = 10 }; -// Binds the sockets (returns -1 and logs via pb_core_log on failure, leaving nothing running), starts the worker threads. +// Returns -1 (after logging) and leaves nothing running on failure. int pb_relay_start(const struct pb_relay_cfg *cfg); -// Stops all threads, closes every fd (listeners, client/upstream sockets, epoll, eventfds). Safe to call twice. After it -// returns, pb_relay_start() must work again (no leaked listeners). +// Stops every thread and closes every fd; safe to call twice and to start again afterwards. void pb_relay_stop(void); -// Actual ports in use (valid after a successful start). uint16_t pb_relay_tcp_port(void); uint16_t pb_relay_udp_port(void); -// Synchronous proxy test used by ProxyBridge_TestConnection: connects to `target_host:target_port` THROUGH `proxy` and sends -// an HTTP HEAD/GET if port is 80/443-ish (caller decides); returns bytes of response in buf (NUL-terminated) or -1 on error with -// a human readable reason in buf. Must not depend on relay_start(). 10 second budget. +// Blocking test of a proxy: tunnels to target_host:target_port and writes a one-line result (or the error) to buf. 0 on success. int pb_relay_test_proxy(const struct pb_proxy *proxy, const char *target_host, uint16_t target_port, char *buf, size_t buflen); #endif diff --git a/Linux/src/pb_proc.c b/Linux/src/pb_proc.c new file mode 100644 index 00000000..93c8fc3f --- /dev/null +++ b/Linux/src/pb_proc.c @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "pb_proc.h" + +#define MAX_INODES 8 + +static size_t listen_inodes(uint32_t ip_be, uint16_t port, uint64_t *inodes) +{ + FILE *f = fopen("/proc/net/tcp", "r"); + if (!f) return 0; + char line[512]; + size_t n = 0; + if (!fgets(line, sizeof line, f)) { fclose(f); return 0; } + while (n < MAX_INODES && fgets(line, sizeof line, f)) { + unsigned addr, lport, state; + unsigned long long inode; + // sl local rem st tx:rx tr:when retrnsmt uid timeout inode + if (sscanf(line, " %*d: %x:%x %*x:%*x %x %*x:%*x %*x:%*x %*x %*d %*d %llu", &addr, &lport, &state, &inode) != 4) continue; + if (state == 0x0A && lport == port && inode && (addr == ip_be || addr == 0)) inodes[n++] = inode; + } + fclose(f); + return n; +} + +static bool pid_has_socket(const char *pid, uint64_t inode) +{ + char dir[300], link[400], want[40], got[64]; + snprintf(dir, sizeof dir, "/proc/%.200s/fd", pid); + snprintf(want, sizeof want, "socket:[%llu]", (unsigned long long)inode); + DIR *d = opendir(dir); + if (!d) return false; + struct dirent *de; + bool found = false; + while (!found && (de = readdir(d))) { + if (de->d_name[0] == '.') continue; + snprintf(link, sizeof link, "%s/%.40s", dir, de->d_name); + ssize_t r = readlink(link, got, sizeof got - 1); + if (r > 0) { got[r] = 0; found = !strcmp(got, want); } + } + closedir(d); + return found; +} + +size_t pb_proc_listeners(uint32_t ip_be, uint16_t port, uint32_t *tgids, size_t max) +{ + uint64_t inodes[MAX_INODES]; + size_t ni = listen_inodes(ip_be, port, inodes), n = 0; + if (!ni) return 0; + DIR *d = opendir("/proc"); + if (!d) return 0; + struct dirent *de; + while (n < max && (de = readdir(d))) { + if (de->d_name[0] < '1' || de->d_name[0] > '9') continue; + for (size_t i = 0; i < ni; i++) + if (pid_has_socket(de->d_name, inodes[i])) { tgids[n++] = (uint32_t)strtoul(de->d_name, NULL, 10); break; } + } + closedir(d); + return n; +} + +bool pb_proc_is_local_ip(uint32_t ip_be) +{ + if ((ntohl(ip_be) >> 24) == 127) return true; + struct ifaddrs *list, *a; + if (getifaddrs(&list) != 0) return false; + bool local = false; + for (a = list; a && !local; a = a->ifa_next) + local = a->ifa_addr && a->ifa_addr->sa_family == AF_INET && ((struct sockaddr_in *)a->ifa_addr)->sin_addr.s_addr == ip_be; + freeifaddrs(list); + return local; +} diff --git a/Linux/src/pb_proc.h b/Linux/src/pb_proc.h new file mode 100644 index 00000000..c5e49865 --- /dev/null +++ b/Linux/src/pb_proc.h @@ -0,0 +1,15 @@ +// /proc helpers: which processes listen on a local TCP port, and which addresses belong to this host. +#ifndef PB_PROC_H +#define PB_PROC_H + +#include +#include +#include + +// tgids of processes with a TCP socket in LISTEN state on `port` bound to `ip_be` or to 0.0.0.0. Returns the count (<= max). +size_t pb_proc_listeners(uint32_t ip_be, uint16_t port, uint32_t *tgids, size_t max); + +// True for 127.0.0.0/8 and for any IPv4 address configured on a local interface. +bool pb_proc_is_local_ip(uint32_t ip_be); + +#endif diff --git a/Linux/src/pb_rules.c b/Linux/src/pb_rules.c index e6a4bdf0..de231af5 100644 --- a/Linux/src/pb_rules.c +++ b/Linux/src/pb_rules.c @@ -18,8 +18,6 @@ static void set_err(char *err, size_t n, const char *fmt, ...) va_end(ap); } -// ---- tokenising ------------------------------------------------------------------------------------------------------ - // Splits on ';' or ',' and trims whitespace. Returns the number of tokens (<= max) or -1 if there are more than max. static int split_list(const char *s, char tokens[][256], int max) { @@ -42,8 +40,6 @@ static int split_list(const char *s, char tokens[][256], int max) return n; } -// ---- glob ------------------------------------------------------------------------------------------------------------ - bool pb_glob_match(const char *p, const char *s) { const char *star = NULL, *ss = s; @@ -57,8 +53,6 @@ bool pb_glob_match(const char *p, const char *s) return *p == 0; } -// ---- parsing helpers ------------------------------------------------------------------------------------------------- - static bool parse_uint(const char *s, size_t len, unsigned max, unsigned *out) { if (!len || len > 5) return false; @@ -75,7 +69,6 @@ static bool parse_uint(const char *s, size_t len, unsigned max, unsigned *out) static bool parse_ipv4(const char *s, uint32_t *out) { struct in_addr a; - // inet_pton rejects leading garbage / short forms, which is what we want for validation if (inet_pton(AF_INET, s, &a) != 1) return false; *out = ntohl(a.s_addr); return true; @@ -111,7 +104,6 @@ static bool v6_prefix_range(const uint8_t *base, unsigned prefix, uint8_t *lo, u static bool looks_like_domain(const char *tok) { - // anything that is not purely IP-ish: contains a letter (other than none), '?' or a '*' attached to letters for (const char *c = tok; *c; c++) if (isalpha((unsigned char)*c) || *c == '_' || *c == '?') return true; return false; @@ -147,7 +139,7 @@ bool pb_domain_match(const char *pattern, const char *name) static bool parse_host_token(const char *tok, struct pb_host_pat *p, char *err, size_t errlen) { memset(p, 0, sizeof *p); - if (strchr(tok, ':')) { // ---- IPv6 + if (strchr(tok, ':')) { char buf[256]; snprintf(buf, sizeof buf, "%s", tok); char *slash = strchr(buf, '/'), *dash = strchr(buf, '-'); @@ -176,7 +168,7 @@ static bool parse_host_token(const char *tok, struct pb_host_pat *p, char *err, const char *slash = strchr(tok, '/'), *dash = strchr(tok, '-'); uint32_t a, b; char buf[256]; - if (slash) { // ---- IPv4 CIDR + if (slash) { snprintf(buf, sizeof buf, "%.*s", (int)(slash - tok), tok); unsigned pre; if (!parse_ipv4(buf, &a) || !parse_uint(slash + 1, strlen(slash + 1), 32, &pre)) goto bad4; @@ -186,14 +178,13 @@ static bool parse_host_token(const char *tok, struct pb_host_pat *p, char *err, } int dots_before = 0; for (const char *c = tok; dash && c < dash; c++) dots_before += *c == '.'; - if (dash && dots_before == 3) { // ---- IPv4 a.b.c.d-e.f.g.h + if (dash && dots_before == 3) { snprintf(buf, sizeof buf, "%.*s", (int)(dash - tok), tok); if (!parse_ipv4(buf, &a) || !parse_ipv4(dash + 1, &b)) goto bad4; if (a > b) { set_err(err, errlen, "IPv4 range '%s' has start > end", tok); return false; } p->kind = PB_HOST_V4_RANGE; p->lo4 = a; p->hi4 = b; return true; } - // ---- IPv4 with per-octet '*' or 'lo-hi' const char *s = tok; for (int i = 0; i < 4; i++) { const char *e = strchr(s, '.'); @@ -202,10 +193,10 @@ static bool parse_host_token(const char *tok, struct pb_host_pat *p, char *err, if (!parse_octet(s, len, &p->box_lo[i], &p->box_hi[i])) goto bad4; s = e ? e + 1 : s + len; } - // exact address or a contiguous prefix box collapses to a numeric range + // a box whose wildcard octets form a suffix collapses to a numeric range { int i = 0; - while (i < 4 && p->box_lo[i] == p->box_hi[i]) i++; // leading fixed octets + while (i < 4 && p->box_lo[i] == p->box_hi[i]) i++; bool contiguous = true; if (i < 4) { for (int j = i + 1; j < 4; j++) @@ -248,8 +239,6 @@ static bool parse_port_token(const char *tok, struct pb_port_range *r, char *err return true; } -// ---- public: parse / free / clone ------------------------------------------------------------------------------------ - void pb_rule_free(struct pb_rule *r) { free(r->procs); free(r->hosts); free(r->ports); free(r->domains); @@ -257,24 +246,6 @@ void pb_rule_free(struct pb_rule *r) r->nprocs = r->nhosts = r->nports = r->ndomains = 0; } -bool pb_rule_clone(struct pb_rule *dst, const struct pb_rule *src) -{ - *dst = *src; - dst->procs = NULL; dst->hosts = NULL; dst->ports = NULL; dst->domains = NULL; - if (src->nprocs && !(dst->procs = malloc(sizeof(*dst->procs) * (size_t)src->nprocs))) goto oom; - if (src->nhosts && !(dst->hosts = malloc(sizeof(*dst->hosts) * (size_t)src->nhosts))) goto oom; - if (src->nports && !(dst->ports = malloc(sizeof(*dst->ports) * (size_t)src->nports))) goto oom; - if (src->ndomains && !(dst->domains = malloc(sizeof(*dst->domains) * (size_t)src->ndomains))) goto oom; - if (src->nprocs) memcpy(dst->procs, src->procs, sizeof(*dst->procs) * (size_t)src->nprocs); - if (src->nhosts) memcpy(dst->hosts, src->hosts, sizeof(*dst->hosts) * (size_t)src->nhosts); - if (src->nports) memcpy(dst->ports, src->ports, sizeof(*dst->ports) * (size_t)src->nports); - if (src->ndomains) memcpy(dst->domains, src->domains, sizeof(*dst->domains) * (size_t)src->ndomains); - return true; -oom: - pb_rule_free(dst); - return false; -} - static bool is_any(const char *s) { while (isspace((unsigned char)*s)) s++; @@ -299,7 +270,6 @@ bool pb_rule_parse(struct pb_rule *out, const char *process, const char *hosts, char (*tok)[256] = malloc(sizeof(char[256]) * PB_MAX_PATTERNS); if (!tok) { set_err(err, errlen, "out of memory"); return false; } - // process if (is_any(process)) out->any_proc = true; else { int n = split_list(process, tok, PB_MAX_PATTERNS); @@ -313,7 +283,6 @@ bool pb_rule_parse(struct pb_rule *out, const char *process, const char *hosts, if (out->any_proc) { free(out->procs); out->procs = NULL; out->nprocs = 0; } } - // hosts if (is_any(hosts)) out->any_host = true; else { int n = split_list(hosts, tok, PB_MAX_PATTERNS); @@ -334,7 +303,6 @@ bool pb_rule_parse(struct pb_rule *out, const char *process, const char *hosts, if (out->any_host) { free(out->hosts); free(out->domains); out->hosts = NULL; out->domains = NULL; out->nhosts = out->ndomains = 0; } } - // ports if (is_any(ports)) out->any_port = true; else { int n = split_list(ports, tok, PB_MAX_PATTERNS); @@ -358,8 +326,6 @@ bool pb_rule_parse(struct pb_rule *out, const char *process, const char *hosts, return false; } -// ---- matching -------------------------------------------------------------------------------------------------------- - bool pb_rule_match_process(const struct pb_rule *r, const char *exe_path) { if (r->any_proc) return true; diff --git a/Linux/src/pb_rules.h b/Linux/src/pb_rules.h index 4c18df44..02cec44e 100644 --- a/Linux/src/pb_rules.h +++ b/Linux/src/pb_rules.h @@ -39,7 +39,6 @@ struct pb_rule { // On failure returns false and writes a human readable reason to err. On success the caller must pb_rule_free(). bool pb_rule_parse(struct pb_rule *out, const char *process, const char *hosts, const char *ports, char *err, size_t errlen); void pb_rule_free(struct pb_rule *r); -bool pb_rule_clone(struct pb_rule *dst, const struct pb_rule *src); // deep copy bool pb_glob_match(const char *pattern, const char *str); bool pb_rule_match_process(const struct pb_rule *r, const char *exe_path); diff --git a/Linux/src/tests/rules_test.c b/Linux/src/tests/rules_test.c index bc1b1c98..59346317 100644 --- a/Linux/src/tests/rules_test.c +++ b/Linux/src/tests/rules_test.c @@ -92,9 +92,6 @@ int main(void) r = R("", "github.com;*.github.com", ""); CHECK(r.ndomains == 2); pb_rule_free(&r); CHECK(pb_domain_match("a?c.com", "abc.com")); CHECK(!pb_domain_match("a?c.com", "abbc.com")); CHECK(pb_domain_match("*", "x.y")); CHECK(bad("", "bad name.com", "")); - { - struct pb_rule c; r = R("curl", "*.example.com", "80"); CHECK(pb_rule_clone(&c, &r) && c.ndomains == 1 && !strcmp(c.domains[0], "*.example.com")); pb_rule_free(&c); pb_rule_free(&r); - } printf(fails ? "RULES TESTS FAILED (%d)\n" : "rules tests passed\n", fails); return fails != 0; } From caace65e8423cc564343f0b1891e50be044b6083 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 15:47:28 +0530 Subject: [PATCH 17/25] fix(linux/relay): source-address check, leak audit, cheaper UDP path, leaner code - claim a pending TCP flow with the accepted peer's address and port - every error and teardown path audited for fd, memory, thread and session leaks; a churn test (100,000 TCP connections across nine flow types, 20,000 UDP sessions) asserts that descriptors return to baseline and RSS stays flat - the UDP flow lookup (a BPF map syscall per datagram) is cached per client for one second; SOCKS5 headers are sent by scatter-gather; accepted sockets inherit their options from the listener - banner and narration comments, benchmark-only hooks and dead helpers removed - tests: hijack attempts from other loopback addresses, stress-tolerant assertions, a PB_TEST_ONLY filter, smaller sizes under the sanitizers --- Linux/src/pb_relay.c | 329 +++++++++++++-------------- Linux/src/tests/relay_test.c | 359 ++++++++++++++++++++++-------- Linux/src/tests/run_relay_test.sh | 2 +- 3 files changed, 428 insertions(+), 262 deletions(-) diff --git a/Linux/src/pb_relay.c b/Linux/src/pb_relay.c index e1de2bb0..2fc833e7 100644 --- a/Linux/src/pb_relay.c +++ b/Linux/src/pb_relay.c @@ -1,8 +1,5 @@ -// pb_relay.c - userspace relay for the eBPF backend. -// -// TCP: acceptor thread -> bounded handshake worker pool (blocking steps with deadlines) -> epoll data threads. -// UDP: one epoll thread owning every session; SOCKS5 UDP ASSOCIATE setup runs in the worker pool. -// Ownership: a conn/session is only touched by the thread that owns its epoll; workers hand objects over via queues. +// Userspace relay for the eBPF backend: TCP (SOCKS5 / HTTP CONNECT / DIRECT) and UDP (SOCKS5 UDP ASSOCIATE / DIRECT). +// Every conn/session is owned by exactly one epoll thread; handshake workers hand finished objects over through queues. #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif @@ -28,7 +25,7 @@ #include "pb_internal.h" -#define SCRATCH_SZ 65536 // per data thread read buffer; idle connections hold no buffer +#define SCRATCH_SZ 65536 #define HDR_MAX 8192 #define NWORKERS 16 #define MAX_DATA 8 @@ -39,15 +36,14 @@ #define UDP_QMAX 32 #define UDP_QBYTES (256 * 1024) #define UDP_MAXDG 65536 +#define UDP_BATCH 16 +#define FLOW_TTL_MS 1000 #define DC_SIZE 1024 #define SESS_BUCKETS 256 #define DIRECT_ID 0xFFFFFFFFu static char TAG_STOP, TAG_WAKE, TAG_UDP, TAG_TIMER; -// --------------------------------------------------------------------------------------------- -// Utilities -// --------------------------------------------------------------------------------------------- static int64_t now_ms(void) { struct timespec ts; @@ -105,7 +101,6 @@ static int wait_fd(int fd, short ev, int64_t deadline, int stopfd) } } -// Blocking-with-deadline I/O context for handshakes (fd is non-blocking). struct io { int fd; int64_t dl; @@ -145,7 +140,6 @@ static int io_send(struct io *x, const void *buf, size_t n) return 0; } -// Receives 1..n bytes. static ssize_t io_recv(struct io *x, void *buf, size_t n) { for (;;) { @@ -175,9 +169,6 @@ static int io_recv_exact(struct io *x, void *buf, size_t n) return 0; } -// --------------------------------------------------------------------------------------------- -// Connecting -// --------------------------------------------------------------------------------------------- static void tune_sock(int fd) { int one = 1, idle = 60, intvl = 15, cnt = 4; @@ -205,10 +196,8 @@ static int connect_addr(const struct sockaddr *sa, socklen_t len, int64_t dl, in return fd; } -// --------------------------------------------------------------------------------------------- // Hostname resolution (IPv4): at most RES_MAX_THREADS short-lived detached lookups, deadline-bounded for the waiter, // results cached (fresh for 60 s; last good answer reused for up to 10 min when re-resolution fails). -// --------------------------------------------------------------------------------------------- #define RES_MAX_THREADS 4 #define RES_DEADLINE_MS 5000 #define RES_TTL_MS 60000 @@ -254,7 +243,7 @@ static void *res_thread(void *arg) if (gr == 0 && res && res->ai_family == AF_INET) { r->addr = ((struct sockaddr_in *)res->ai_addr)->sin_addr; struct rescache *c = res_cache_find(r->name); - if (!c) { // reuse a free slot, else evict the oldest + if (!c) { c = &res_cache[0]; for (int i = 0; i < RES_CACHE_N; i++) { if (!res_cache[i].used) { c = &res_cache[i]; break; } @@ -304,7 +293,7 @@ static int resolve_v4(const char *name, int64_t dl, int stopfd, struct in_addr * pthread_attr_setstacksize(&at, 256 * 1024); if (r) { snprintf(r->name, sizeof r->name, "%s", name); - r->refs = 2; // thread + this waiter + r->refs = 2; r->next = res_inflight; res_inflight = r; res_threads++; if (pthread_create(&th, &at, res_thread, r) != 0) { @@ -336,7 +325,7 @@ static int resolve_v4(const char *name, int64_t dl, int stopfd, struct in_addr * res_unref(r); } if (rc != 0) { - c = res_cache_find(name); // the lookup may have refreshed it; else try a stale answer + c = res_cache_find(name); if (c && now_ms() - c->when < RES_STALE_MS) { *out = c->addr; rc = 0; } } pthread_mutex_unlock(&res_mu); @@ -362,7 +351,7 @@ static int connect_host(const char *host, uint16_t port, int64_t dl, int stopfd, s->sin6_family = AF_INET6; s->sin6_port = htons(port); s->sin6_addr = a6; sl[0] = sizeof *s; n = 1; } else { - return -1; // resolve_v4 filled err + return -1; } int eno = ETIMEDOUT; for (int i = 0; i < n; i++) { @@ -376,9 +365,6 @@ static int connect_host(const char *host, uint16_t port, int64_t dl, int stopfd, return -1; } -// --------------------------------------------------------------------------------------------- -// SOCKS5 / HTTP CONNECT handshakes -// --------------------------------------------------------------------------------------------- struct s5dst { uint8_t atyp; uint8_t addr[16]; char host[256]; uint16_t port; }; struct s5bnd { uint8_t atyp; uint8_t addr[16]; uint16_t port; }; @@ -405,7 +391,6 @@ static void s5dst_from_orig(struct s5dst *d, const struct pb_orig *o) else { d->atyp = 4; memcpy(d->addr, o->addr, 16); } } -// cmd: 1 = CONNECT, 3 = UDP ASSOCIATE static int socks5_request(struct io *x, const struct pb_proxy *p, uint8_t cmd, const struct s5dst *d, struct s5bnd *b) { bool auth = p->user[0] != 0; @@ -484,7 +469,6 @@ static void b64(const char *in, size_t n, char *out) out[o] = 0; } -// host: IP literal (v6 without brackets) or hostname. Bytes received after the response headers go to left/leftn. static int http_connect(struct io *x, const struct pb_proxy *p, const char *host, uint16_t port, uint8_t *left, size_t *leftn) { char hp[300]; @@ -530,17 +514,14 @@ static int http_connect(struct io *x, const struct pb_proxy *p, const char *host return 0; } -// --------------------------------------------------------------------------------------------- -// Relay state -// --------------------------------------------------------------------------------------------- struct conn { struct conn *prev, *next; - int fd[2]; // 0 = client, 1 = upstream + int fd[2]; size_t off[2], len[2]; // pend[d]: unsent tail of bytes read from fd[d] (heap, freed once drained) uint8_t *pend[2]; bool dns; // original destination port 53: upstream->client is parsed as TCP-DNS bool doff; // stopped observing (malformed stream) - uint8_t *dbuf; // bytes of the current, not yet complete, DNS message(s) + uint8_t *dbuf; size_t dhave, dcap; uint32_t mask[2]; bool eof[2], shut[2], dead; @@ -559,7 +540,7 @@ struct dthread { int ep, wake; pthread_mutex_t mu; struct conn *inq_head, *inq_tail; - struct conn *conns; // owned by thread + struct conn *conns; struct conn *grave; uint8_t *scratch; int pfd[2]; // reusable splice pipe; always empty between connections @@ -588,13 +569,13 @@ struct sess { struct sess *hnext, *prev, *next, *dnext; struct sockaddr_in cli; uint32_t id, flow_be; // id = proxy id or DIRECT_ID; flow_be only for DIRECT sessions + uint64_t sid; int kind, state; int up_fd, ctl_fd; - int family; // DIRECT: family of original destination bool connected; // DIRECT: v6 sockets are connected; v4 are not (replies from any port of the host are accepted) - struct sockaddr_storage dst; // DIRECT destination + struct sockaddr_storage dst; socklen_t dstlen; - uint16_t dport; // DIRECT: original destination port (53 => DNS tap) + uint16_t dport; bool dead, ok, warned; int64_t last, fail_until; struct pb_proxy proxy; @@ -607,14 +588,14 @@ struct sess { size_t qbytes; }; -struct dce { // per-(client, flow ip) decision cache +struct dce { bool used; struct sockaddr_in cli; - uint32_t flow_be; - uint8_t addr[16]; - uint16_t port; - int action; - int64_t last; + uint32_t flow_be, pid; + struct pb_orig o; + int action, fidx; + int64_t looked, last; + uint64_t sid; }; static struct { @@ -638,17 +619,21 @@ static struct { struct job *jhead, *jtail; int jcount; - pthread_mutex_t dmu; // UDP setup completion queue + pthread_mutex_t dmu; struct sess *done; - // UDP thread-confined state struct sess *sess_head; struct sess *buckets[SESS_BUCKETS]; int nsess; struct sess *sgrave; struct dce *dc; - uint8_t *rxbuf, *txbuf; + uint8_t *rxbuf; int64_t drop_count, drop_logged; + uint64_t next_sid; + struct mmsghdr *mm; + struct iovec *mmiov; + struct sockaddr_in *mmname; + char (*mmctl)[CMSG_SPACE(sizeof(struct in_pktinfo))]; } R = { .life = PTHREAD_MUTEX_INITIALIZER, .jmu = PTHREAD_MUTEX_INITIALIZER, .jcv = PTHREAD_COND_INITIALIZER, .dmu = PTHREAD_MUTEX_INITIALIZER, .lfd = -1, .ufd = -1, .stopfd = -1, .tmfd = -1, .uep = -1, .uwake = -1 }; @@ -657,9 +642,6 @@ static void proxy_desc(const struct pb_proxy *p, char *out, size_t n) snprintf(out, n, "%s %s:%u", p->type == PROXY_TYPE_SOCKS5 ? "SOCKS5" : "HTTP", p->host, p->port); } -// --------------------------------------------------------------------------------------------- -// Job queue / worker pool -// --------------------------------------------------------------------------------------------- static int job_push(int kind, int fd, const struct pb_orig *o, struct sess *s) { struct job *j = calloc(1, sizeof *j); @@ -695,7 +677,7 @@ static void conn_handoff(int cfd, int ufd, const uint8_t *left, size_t leftn, bo } unsigned start = atomic_fetch_add(&R.rr, 1); struct dthread *t = &R.data[start % R.ndata]; - for (int i = 1; i < R.ndata; i++) { // least-loaded, ties go to the round-robin choice + for (int i = 1; i < R.ndata; i++) { struct dthread *u = &R.data[(start + i) % R.ndata]; if (atomic_load(&u->load) < atomic_load(&t->load)) t = u; } @@ -714,13 +696,6 @@ static void conn_handoff(int cfd, int ufd, const uint8_t *left, size_t leftn, bo eventfd_write(t->wake, 1); } -static void abort_close(int fd) -{ - struct linger l = { 1, 0 }; - setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof l); - close(fd); -} - static void tcp_job_run(struct job *j) { struct pb_orig *o = &j->o; @@ -738,7 +713,7 @@ static void tcp_job_run(struct job *j) action = d.action; pid = d.proxy_id; } if (action == PB_ACT_DIRECT) { - pb_core_flow_event(o, PB_ACT_DIRECT, "DIRECT"); + pb_core_flow_event(o, PB_ACT_DIRECT); struct sockaddr_storage ss; socklen_t sl = orig_to_sa(o, &ss); int eno = 0; @@ -747,11 +722,11 @@ static void tcp_job_run(struct job *j) } else if (action == PB_ACT_PROXY) { struct pb_proxy px; if (pb_core_get_proxy(pid, &px) != 0) { - pb_core_flow_event(o, PB_ACT_PROXY, "unknown proxy"); + pb_core_flow_event(o, PB_ACT_PROXY); snprintf(err, sizeof err, "no proxy configured (id %u) for %s", pid, dst); } else { proxy_desc(&px, desc, sizeof desc); - pb_core_flow_event(o, PB_ACT_PROXY, desc); + pb_core_flow_event(o, PB_ACT_PROXY); char cerr[200]; up = connect_host(px.host, px.port, dl, R.stopfd, cerr, sizeof cerr); if (up < 0) { @@ -775,8 +750,9 @@ static void tcp_job_run(struct job *j) } } } else { - pb_core_flow_event(o, PB_ACT_BLOCK, "BLOCK"); - abort_close(cfd); + pb_core_flow_event(o, PB_ACT_BLOCK); + setsockopt(cfd, SOL_SOCKET, SO_LINGER, &(struct linger){ 1, 0 }, sizeof(struct linger)); + close(cfd); atomic_fetch_sub(&R.nconn, 1); return; } @@ -858,9 +834,6 @@ static void *worker_main(void *arg) } } -// --------------------------------------------------------------------------------------------- -// Acceptor -// --------------------------------------------------------------------------------------------- static void *acceptor_main(void *arg) { (void)arg; @@ -879,15 +852,14 @@ static void *acceptor_main(void *arg) struct pollfd s = { .fd = R.stopfd, .events = POLLIN }; poll(&s, 1, 200); } - break; // EAGAIN or error: back to poll + break; } struct pb_orig o; - if (pb_core_take_tcp_orig(ntohs(pa.sin_port), &o) != 0) { - pb_core_log("relay: no original destination for client port %u, closing", ntohs(pa.sin_port)); + if (pb_core_take_tcp_orig(pa.sin_addr.s_addr, ntohs(pa.sin_port), &o) != 0) { + pb_core_log("relay: no original destination for client %s:%u, closing", inet_ntoa(pa.sin_addr), ntohs(pa.sin_port)); close(fd); continue; } - tune_sock(fd); if (atomic_fetch_add(&R.nconn, 1) >= R.cfg.max_tcp_conns) { atomic_fetch_sub(&R.nconn, 1); int64_t t = now_ms(); @@ -906,16 +878,12 @@ static void *acceptor_main(void *arg) return NULL; } -// --------------------------------------------------------------------------------------------- -// TCP data threads -// --------------------------------------------------------------------------------------------- static void pipe_discard(int fd) { char b[4096]; while (read(fd, b, sizeof b) > 0) {} } -// Reads `n` bytes the pipe holds into a fresh heap tail for direction d. static int spill_from_pipe(struct conn *c, int d, int pipe_rd, size_t n) { c->pend[d] = malloc(n); @@ -955,7 +923,6 @@ static int pump_splice(struct dthread *t, struct conn *c, int d) } // Upstream->client for TCP-DNS: only whole messages (after pb_core_dns_observe returned) are forwarded. -// Returns 1 progress, 0 none, -1 fatal. static int dns_pump_up(struct conn *c, uint8_t *scratch) { int progress = 0; @@ -1091,7 +1058,6 @@ static void conn_close(struct dthread *t, struct conn *c) close(c->fd[1]); c->dead = true; conn_bufs_free(c); - c->pend[0] = c->pend[1] = NULL; if (c->prev) c->prev->next = c->next; else t->conns = c->next; if (c->next) c->next->prev = c->prev; c->next = t->grave; @@ -1101,7 +1067,6 @@ static void conn_close(struct dthread *t, struct conn *c) atomic_fetch_sub(&t->load, 1); } -// Returns after pump + epoll mask refresh; closes the conn when done/failed. static void conn_service(struct dthread *t, struct conn *c) { int r = conn_pump(t, c); @@ -1121,7 +1086,8 @@ static void dthread_adopt(struct dthread *t) if (t->conns) t->conns->prev = c; t->conns = c; c->last = now_ms(); - conn_service(t, c); + if (c->len[1] || c->dhave) conn_service(t, c); + else if (conn_update(t, c) != 0) conn_close(t, c); c = n; } } @@ -1169,9 +1135,6 @@ static void *data_main(void *arg) return NULL; } -// --------------------------------------------------------------------------------------------- -// UDP -// --------------------------------------------------------------------------------------------- static void udp_droplog(const char *why) { R.drop_count++; @@ -1235,6 +1198,7 @@ static struct sess *sess_new(const struct sockaddr_in *cli, uint32_t id, uint32_ s->ref[0].s = s->ref[1].s = s; s->ref[0].which = 0; s->ref[1].which = 1; s->last = now_ms(); + s->sid = ++R.next_sid; uint32_t h = sess_hash(cli, id, flow); s->hnext = R.buckets[h]; R.buckets[h] = s; s->next = R.sess_head; @@ -1267,85 +1231,91 @@ static int sess_arm(struct sess *s, int which, uint32_t events) return epoll_ctl(R.uep, EPOLL_CTL_ADD, fd, &ev); } -static void flow_upsert(struct sess *s, const struct pb_orig *o, uint32_t flow_be) +static int flow_upsert(struct sess *s, const struct pb_orig *o, uint32_t flow_be, int64_t now) { int victim = 0; for (int i = 0; i < s->nflows; i++) { - if (s->flows[i].port == o->port && !memcmp(s->flows[i].addr, o->addr, 16)) { victim = -1; s->flows[i].ip_be = flow_be; s->flows[i].last = s->last; break; } + if (s->flows[i].port == o->port && !memcmp(s->flows[i].addr, o->addr, 16)) { + s->flows[i].ip_be = flow_be; + s->flows[i].last = now; + return i; + } if (s->flows[i].last < s->flows[victim].last) victim = i; } - if (victim < 0) return; if (s->nflows < MAX_FLOWS) victim = s->nflows++; memcpy(s->flows[victim].addr, o->addr, 16); s->flows[victim].port = o->port; s->flows[victim].ip_be = flow_be; - s->flows[victim].last = s->last; + s->flows[victim].last = now; + return victim; } -// Wraps the datagram with a SOCKS5 UDP header into R.txbuf; returns total length or 0. -static size_t socks5_wrap(const struct pb_orig *o, const uint8_t *pl, size_t n) +static size_t socks5_header(uint8_t *h, const struct pb_orig *o) { - size_t h = o->family == AF_INET ? 10 : 22; - if (n + h > UDP_MAXDG) return 0; - uint8_t *b = R.txbuf; - b[0] = b[1] = b[2] = 0; - if (o->family == AF_INET) { b[3] = 1; memcpy(b + 4, o->addr + 12, 4); } - else { b[3] = 4; memcpy(b + 4, o->addr, 16); } - b[h - 2] = (uint8_t)(o->port >> 8); b[h - 1] = (uint8_t)o->port; - memcpy(b + h, pl, n); - return n + h; + h[0] = h[1] = h[2] = 0; + size_t n; + if (o->family == AF_INET) { h[3] = 1; memcpy(h + 4, o->addr + 12, 4); n = 10; } + else { h[3] = 4; memcpy(h + 4, o->addr, 16); n = 22; } + h[n - 2] = (uint8_t)(o->port >> 8); + h[n - 1] = (uint8_t)o->port; + return n; } -static void udp_dgram(const struct sockaddr_in *cli, uint32_t flow_be, const uint8_t *pl, size_t n) +static void udp_dgram(const struct sockaddr_in *cli, uint32_t flow_be, const uint8_t *pl, size_t n, int64_t now) { - struct pb_orig o; - if (pb_core_get_udp_flow(flow_be, &o) != 0) { udp_droplog("unknown flow address"); return; } - int64_t now = now_ms(); uint32_t hv = (cli->sin_addr.s_addr * 2654435761u) ^ (cli->sin_port * 40503u) ^ (flow_be * 2246822519u); struct dce *e = &R.dc[hv % DC_SIZE]; - uint32_t pid = o.proxy_id; - if (!(e->used && e->cli.sin_addr.s_addr == cli->sin_addr.s_addr && e->cli.sin_port == cli->sin_port && e->flow_be == flow_be && - e->port == o.port && !memcmp(e->addr, o.addr, 16) && now - e->last <= (int64_t)R.cfg.udp_idle_timeout_s * 1000)) { - int act = o.action; - if (act == PB_ACT_DECIDE) { - struct pb_decision d; - pb_core_decide(&o, &d); - act = d.action; pid = d.proxy_id; - } - char desc[300] = "BLOCK"; - if (act == PB_ACT_PROXY) { - struct pb_proxy px; - if (pb_core_get_proxy(pid, &px) != 0) { - pb_core_log("relay: UDP: no proxy configured (id %u), dropping", pid); + bool same = e->used && e->cli.sin_addr.s_addr == cli->sin_addr.s_addr && e->cli.sin_port == cli->sin_port && e->flow_be == flow_be; + if (!same || now - e->looked >= FLOW_TTL_MS) { + struct pb_orig o; + if (pb_core_get_udp_flow(flow_be, &o) != 0) { e->used = false; udp_droplog("unknown flow address"); return; } + bool keep = same && e->o.port == o.port && !memcmp(e->o.addr, o.addr, 16) && now - e->last <= (int64_t)R.cfg.udp_idle_timeout_s * 1000; + if (!keep) { + int act = o.action; + uint32_t pid = o.proxy_id; + if (act == PB_ACT_DECIDE) { + struct pb_decision d; + pb_core_decide(&o, &d); + act = d.action; + pid = d.proxy_id; + } + if (act == PB_ACT_PROXY) { + struct pb_proxy px; + if (pb_core_get_proxy(pid, &px) != 0) { + pb_core_log("relay: UDP: no proxy configured (id %u), dropping", pid); + act = PB_ACT_BLOCK; + } else { + pid = px.id; + } + } else if (act != PB_ACT_DIRECT) { act = PB_ACT_BLOCK; - } else { - proxy_desc(&px, desc, sizeof desc); - if (px.type != PROXY_TYPE_SOCKS5) snprintf(desc + strlen(desc), sizeof desc - strlen(desc), " (UDP unsupported)"); } - } else if (act == PB_ACT_DIRECT) { - strcpy(desc, "DIRECT"); - } else { - act = PB_ACT_BLOCK; + pb_core_flow_event(&o, act); + e->action = act; + e->pid = pid; + e->sid = 0; } - pb_core_flow_event(&o, act, desc); - e->used = true; e->cli = *cli; e->flow_be = flow_be; memcpy(e->addr, o.addr, 16); e->port = o.port; e->action = act; + e->used = true; + e->cli = *cli; + e->flow_be = flow_be; + e->o = o; + e->looked = now; } e->last = now; if (e->action == PB_ACT_BLOCK) return; + const struct pb_orig *o = &e->o; struct sess *s; if (e->action == PB_ACT_DIRECT) { s = sess_find(cli, DIRECT_ID, flow_be); if (!s) { - struct sockaddr_storage ss; - socklen_t sl = orig_to_sa(&o, &ss); s = sess_new(cli, DIRECT_ID, flow_be, SK_DIRECT); if (!s) { udp_droplog("UDP session limit reached"); return; } - s->family = o.family; - s->dst = ss; s->dstlen = sl; s->dport = o.port; - s->connected = o.family != AF_INET; - s->up_fd = socket(ss.ss_family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); - if (s->up_fd < 0 || (s->connected && connect(s->up_fd, (struct sockaddr *)&ss, sl) < 0) || sess_arm(s, 0, EPOLLIN) < 0) { + s->dstlen = orig_to_sa(o, &s->dst); + s->dport = o->port; + s->connected = o->family != AF_INET; + s->up_fd = socket(s->dst.ss_family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (s->up_fd < 0 || (s->connected && connect(s->up_fd, (struct sockaddr *)&s->dst, s->dstlen) < 0) || sess_arm(s, 0, EPOLLIN) < 0) { pb_core_log("relay: UDP direct socket failed: %s", strerror(errno)); sess_destroy(s); return; @@ -1358,11 +1328,10 @@ static void udp_dgram(const struct sockaddr_in *cli, uint32_t flow_be, const uin return; } - // PROXY: re-resolve to get the canonical id and type - struct pb_proxy px; - if (pb_core_get_proxy(pid, &px) != 0) { udp_droplog("proxy vanished"); return; } - s = sess_find(cli, px.id, 0); + s = sess_find(cli, e->pid, 0); if (!s) { + struct pb_proxy px; + if (pb_core_get_proxy(e->pid, &px) != 0) { udp_droplog("proxy vanished"); return; } s = sess_new(cli, px.id, 0, px.type == PROXY_TYPE_SOCKS5 ? SK_SOCKS5 : SK_DROP); if (!s) { udp_droplog("UDP session limit reached"); return; } s->proxy = px; @@ -1370,50 +1339,72 @@ static void udp_dgram(const struct sockaddr_in *cli, uint32_t flow_be, const uin s->state = ST_READY; } else { s->state = ST_SETUP; - if (job_push(J_UDP, -1, NULL, s) != 0) { s->state = ST_FAILED; s->fail_until = now + 5000; udp_droplog("setup queue full"); } + if (job_push(J_UDP, -1, NULL, s) != 0) { + s->state = ST_FAILED; + s->fail_until = now + 5000; + udp_droplog("setup queue full"); + } } } s->last = now; if (s->kind == SK_DROP) { if (!s->warned) { s->warned = true; - pb_core_log("relay: UDP cannot be proxied through HTTP proxy '%s'; dropping UDP from %s:%u", px.name, + pb_core_log("relay: UDP cannot be proxied through HTTP proxy '%s'; dropping UDP from %s:%u", s->proxy.name, inet_ntoa(cli->sin_addr), ntohs(cli->sin_port)); } return; } if (s->state == ST_FAILED) { udp_droplog("proxy UDP setup failed"); return; } - flow_upsert(s, &o, flow_be); - size_t wl = socks5_wrap(&o, pl, n); - if (!wl) { udp_droplog("datagram too large"); return; } + + if (e->sid == s->sid && e->fidx < s->nflows && s->flows[e->fidx].port == o->port && !memcmp(s->flows[e->fidx].addr, o->addr, 16)) { + s->flows[e->fidx].last = now; + } else { + e->fidx = flow_upsert(s, o, flow_be, now); + e->sid = s->sid; + } + + uint8_t hdr[22]; + size_t hl = socks5_header(hdr, o); + if (hl + n > UDP_MAXDG) { udp_droplog("datagram too large"); return; } if (s->state == ST_SETUP) { - if (s->nq >= UDP_QMAX || s->qbytes + wl > UDP_QBYTES) { udp_droplog("setup queue full"); return; } - struct pkt *p = malloc(sizeof *p + wl); + if (s->nq >= UDP_QMAX || s->qbytes + hl + n > UDP_QBYTES) { udp_droplog("setup queue full"); return; } + struct pkt *p = malloc(sizeof *p + hl + n); if (!p) return; - p->len = wl; memcpy(p->d, R.txbuf, wl); - s->q[s->nq++] = p; s->qbytes += wl; + p->len = hl + n; + memcpy(p->d, hdr, hl); + memcpy(p->d + hl, pl, n); + s->q[s->nq++] = p; + s->qbytes += p->len; return; } - send(s->up_fd, R.txbuf, wl, MSG_DONTWAIT | MSG_NOSIGNAL); + struct iovec iov[2] = { { hdr, hl }, { (void *)pl, n } }; + struct msghdr mh = { .msg_iov = iov, .msg_iovlen = 2 }; + sendmsg(s->up_fd, &mh, MSG_DONTWAIT | MSG_NOSIGNAL); } static void udp_recv_client(void) { - for (int i = 0; i < 128; i++) { - struct sockaddr_in cli; - union { char b[CMSG_SPACE(sizeof(struct in_pktinfo))]; struct cmsghdr a; } cb; - struct iovec iov = { R.rxbuf, UDP_MAXDG }; - struct msghdr mh = { .msg_name = &cli, .msg_namelen = sizeof cli, .msg_iov = &iov, .msg_iovlen = 1, - .msg_control = cb.b, .msg_controllen = sizeof cb.b }; - ssize_t n = recvmsg(R.ufd, &mh, MSG_DONTWAIT); - if (n < 0) return; - if (mh.msg_flags & MSG_TRUNC) continue; - bool have = false; - struct in_pktinfo pi; - for (struct cmsghdr *c = CMSG_FIRSTHDR(&mh); c; c = CMSG_NXTHDR(&mh, c)) - if (c->cmsg_level == IPPROTO_IP && c->cmsg_type == IP_PKTINFO) { memcpy(&pi, CMSG_DATA(c), sizeof pi); have = true; } - if (!have || (ntohl(pi.ipi_addr.s_addr) >> 24) != 127) { udp_droplog("destination not in 127.0.0.0/8"); continue; } - udp_dgram(&cli, pi.ipi_addr.s_addr, R.rxbuf, (size_t)n); + for (int round = 0; round < 8; round++) { + for (int i = 0; i < UDP_BATCH; i++) { + R.mmiov[i] = (struct iovec){ R.rxbuf + (size_t)i * UDP_MAXDG, UDP_MAXDG }; + R.mm[i].msg_hdr = (struct msghdr){ .msg_name = &R.mmname[i], .msg_namelen = sizeof(struct sockaddr_in), .msg_iov = &R.mmiov[i], + .msg_iovlen = 1, .msg_control = R.mmctl[i], .msg_controllen = sizeof R.mmctl[i] }; + } + int cnt = recvmmsg(R.ufd, R.mm, UDP_BATCH, MSG_DONTWAIT, NULL); + if (cnt <= 0) return; + int64_t now = now_ms(); + for (int i = 0; i < cnt; i++) { + struct msghdr *mh = &R.mm[i].msg_hdr; + if (mh->msg_flags & MSG_TRUNC) continue; + struct in_pktinfo pi; + bool have = false; + for (struct cmsghdr *c = CMSG_FIRSTHDR(mh); c; c = CMSG_NXTHDR(mh, c)) + if (c->cmsg_level == IPPROTO_IP && c->cmsg_type == IP_PKTINFO) { memcpy(&pi, CMSG_DATA(c), sizeof pi); have = true; } + if (!have || (ntohl(pi.ipi_addr.s_addr) >> 24) != 127) { udp_droplog("destination not in 127.0.0.0/8"); continue; } + udp_dgram(&R.mmname[i], pi.ipi_addr.s_addr, R.rxbuf + (size_t)i * UDP_MAXDG, R.mm[i].msg_len, now); + } + if (cnt < UDP_BATCH) return; } } @@ -1544,9 +1535,6 @@ static void *udp_main(void *arg) return NULL; } -// --------------------------------------------------------------------------------------------- -// Start / stop -// --------------------------------------------------------------------------------------------- static void teardown(void) { atomic_store(&R.stopping, true); @@ -1561,7 +1549,6 @@ static void teardown(void) R.acc_started = R.udp_started = false; R.nworkers = 0; - // everything is single-threaded from here on while (R.jhead) { struct job *j = R.jhead; R.jhead = j->next; @@ -1589,7 +1576,8 @@ static void teardown(void) R.nsess = 0; free(R.dc); R.dc = NULL; free(R.rxbuf); R.rxbuf = NULL; - free(R.txbuf); R.txbuf = NULL; + free(R.mm); free(R.mmiov); free(R.mmname); free(R.mmctl); + R.mm = NULL; R.mmiov = NULL; R.mmname = NULL; R.mmctl = NULL; close_fd(&R.lfd); close_fd(&R.ufd); close_fd(&R.tmfd); @@ -1624,9 +1612,6 @@ int pb_relay_start(const struct pb_relay_cfg *cfg) int one = 1; long ncpu = sysconf(_SC_NPROCESSORS_ONLN); R.ndata = ncpu < 2 ? 2 : (ncpu > MAX_DATA ? MAX_DATA : (int)ncpu); -#ifdef PB_RELAY_NDATA // benchmarking override - R.ndata = PB_RELAY_NDATA; -#endif for (int i = 0; i < R.ndata; i++) { memset(&R.data[i], 0, sizeof R.data[i]); R.data[i].ep = R.data[i].wake = -1; @@ -1641,6 +1626,7 @@ int pb_relay_start(const struct pb_relay_cfg *cfg) R.lfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (R.lfd < 0) { why = "tcp socket"; goto fail; } setsockopt(R.lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + tune_sock(R.lfd); struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(c.tcp_port), .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; if (bind(R.lfd, (struct sockaddr *)&a, sizeof a) < 0 || listen(R.lfd, 4096) < 0) { pb_core_log("relay: cannot listen on 127.0.0.1:%u: %s", c.tcp_port, strerror(errno)); @@ -1652,7 +1638,7 @@ int pb_relay_start(const struct pb_relay_cfg *cfg) if (setsockopt(R.ufd, SOL_SOCKET, SO_BINDTODEVICE, "lo", 3) < 0) pb_core_log("relay: SO_BINDTODEVICE lo failed (%s); relying on destination address filtering", strerror(errno)); int rb = 4 << 20; - setsockopt(R.ufd, SOL_SOCKET, SO_RCVBUF, &rb, sizeof rb); + if (setsockopt(R.ufd, SOL_SOCKET, SO_RCVBUFFORCE, &rb, sizeof rb) < 0) setsockopt(R.ufd, SOL_SOCKET, SO_RCVBUF, &rb, sizeof rb); if (setsockopt(R.ufd, IPPROTO_IP, IP_PKTINFO, &one, sizeof one) < 0) { why = "IP_PKTINFO"; goto fail; } a.sin_port = htons(c.udp_port); a.sin_addr.s_addr = htonl(INADDR_ANY); @@ -1665,9 +1651,12 @@ int pb_relay_start(const struct pb_relay_cfg *cfg) R.uep = epoll_create1(EPOLL_CLOEXEC); R.tmfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); R.dc = calloc(DC_SIZE, sizeof *R.dc); - R.rxbuf = malloc(UDP_MAXDG); - R.txbuf = malloc(UDP_MAXDG + 32); - if (R.uep < 0 || R.tmfd < 0 || !R.dc || !R.rxbuf || !R.txbuf) { why = "udp resources"; goto fail; } + R.rxbuf = malloc((size_t)UDP_MAXDG * UDP_BATCH); + R.mm = calloc(UDP_BATCH, sizeof *R.mm); + R.mmiov = calloc(UDP_BATCH, sizeof *R.mmiov); + R.mmname = calloc(UDP_BATCH, sizeof *R.mmname); + R.mmctl = calloc(UDP_BATCH, sizeof *R.mmctl); + if (R.uep < 0 || R.tmfd < 0 || !R.dc || !R.rxbuf || !R.mm || !R.mmiov || !R.mmname || !R.mmctl) { why = "udp resources"; goto fail; } struct itimerspec its = { .it_interval = { 1, 0 }, .it_value = { 1, 0 } }; if (timerfd_settime(R.tmfd, 0, &its, NULL) < 0 || add_ep(R.uep, R.ufd, &TAG_UDP, EPOLLIN) || add_ep(R.uep, R.stopfd, &TAG_STOP, EPOLLIN) || @@ -1678,9 +1667,6 @@ int pb_relay_start(const struct pb_relay_cfg *cfg) t->scratch = malloc(SCRATCH_SZ); if (pipe2(t->pfd, O_CLOEXEC | O_NONBLOCK) == 0) fcntl(t->pfd[0], F_SETPIPE_SZ, PIPE_SZ); else t->pfd[0] = t->pfd[1] = -1, t->nosplice = true; -#ifdef PB_RELAY_COPY_ONLY // benchmarking override - t->nosplice = true; -#endif t->ep = epoll_create1(EPOLL_CLOEXEC); t->wake = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); if (!t->scratch || t->ep < 0 || t->wake < 0 || add_ep(t->ep, t->wake, &TAG_WAKE, EPOLLIN) || add_ep(t->ep, R.stopfd, &TAG_STOP, EPOLLIN)) { @@ -1728,9 +1714,6 @@ void pb_relay_stop(void) uint16_t pb_relay_tcp_port(void) { return R.tcp_port; } uint16_t pb_relay_udp_port(void) { return R.udp_port; } -// --------------------------------------------------------------------------------------------- -// Synchronous proxy test -// --------------------------------------------------------------------------------------------- int pb_relay_test_proxy(const struct pb_proxy *proxy, const char *host, uint16_t port, char *buf, size_t buflen) { if (!buf || !buflen) return -1; diff --git a/Linux/src/tests/relay_test.c b/Linux/src/tests/relay_test.c index c385a3e5..452d52fb 100644 --- a/Linux/src/tests/relay_test.c +++ b/Linux/src/tests/relay_test.c @@ -1,7 +1,5 @@ -// Root-free loopback test harness for pb_relay.c. Fakes every pb_core_* function and runs in-process fake -// SOCKS5 / HTTP CONNECT proxies and echo servers. The kernel redirect is simulated (see README comments below): -// TCP: client bind()s a known source port, the fake pb_core_take_tcp_orig() maps that port to the destination. -// UDP: client sends to 127.x.y.z:udp_port, the fake pb_core_get_udp_flow() maps 127.x.y.z to the destination. +// Root-free loopback test harness for pb_relay.c: fake pb_core_* services, in-process SOCKS5 / HTTP proxies and echo servers. +// The kernel redirect is simulated by registering original destinations keyed by the client's (ip,port) or by flow address. #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif @@ -33,12 +31,11 @@ #include "../pb_internal.h" -// --------------------------------------------------------------------------------------------- -// Fake core -// --------------------------------------------------------------------------------------------- static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; -static struct pb_orig g_tcp[65536]; -static bool g_tcp_valid[65536]; +#define TCP_SLOTS 65536 +static struct tcp_slot { uint64_t key; struct pb_orig o; uint8_t state; } g_tcp[TCP_SLOTS]; // 0 free, 1 used, 2 tombstone +static inline uint64_t tcp_key(uint32_t ip_be, uint16_t port) { return (uint64_t)ip_be << 16 | port; } +static inline uint32_t tcp_hash(uint64_t k) { return (uint32_t)((k * 0x9E3779B97F4A7C15ull) >> 48); } static struct { uint32_t ip_be; struct pb_orig o; bool used; } g_flows[64]; static struct pb_proxy g_px[16]; static bool g_px_used[16]; @@ -48,11 +45,19 @@ static atomic_int g_ev, g_ev_proxy, g_ev_direct, g_ev_block, g_decide_calls; static char g_log[64][512]; static int g_log_n; -int pb_core_take_tcp_orig(uint16_t p, struct pb_orig *out) +int pb_core_take_tcp_orig(uint32_t ip_be, uint16_t port, struct pb_orig *out) { + uint64_t k = tcp_key(ip_be, port); + int ok = 0; pthread_mutex_lock(&g_mu); - int ok = g_tcp_valid[p]; - if (ok) { *out = g_tcp[p]; g_tcp_valid[p] = false; } + for (uint32_t i = tcp_hash(k), n = 0; n < TCP_SLOTS && g_tcp[i].state; i = (i + 1) % TCP_SLOTS, n++) { + if (g_tcp[i].state == 1 && g_tcp[i].key == k) { + *out = g_tcp[i].o; + g_tcp[i].state = g_tcp[(i + 1) % TCP_SLOTS].state ? 2 : 0; + ok = 1; + break; + } + } pthread_mutex_unlock(&g_mu); return ok ? 0 : -1; } @@ -92,15 +97,14 @@ void pb_core_log(const char *fmt, ...) pthread_mutex_unlock(&g_mu); if (getenv("VERBOSE")) fprintf(stderr, " [log] %s\n", b); } -void pb_core_flow_event(const struct pb_orig *o, int action, const char *d) +void pb_core_flow_event(const struct pb_orig *o, int action) { - (void)o; (void)d; + (void)o; atomic_fetch_add(&g_ev, 1); if (action == PB_ACT_PROXY) atomic_fetch_add(&g_ev_proxy, 1); else if (action == PB_ACT_DIRECT) atomic_fetch_add(&g_ev_direct, 1); else atomic_fetch_add(&g_ev_block, 1); } -bool pb_core_dns_via_proxy(void) { return false; } struct obs { int64_t t0, t1; size_t len; uint32_t h; }; static struct obs g_obs[64]; @@ -143,9 +147,6 @@ static void set_decision(struct pb_decision d) static void log_clear(void) { pthread_mutex_lock(&g_mu); g_log_n = 0; memset(g_log, 0, sizeof g_log); pthread_mutex_unlock(&g_mu); } static void ev_clear(void) { g_ev = g_ev_proxy = g_ev_direct = g_ev_block = 0; } -// --------------------------------------------------------------------------------------------- -// Test plumbing -// --------------------------------------------------------------------------------------------- static int g_pass, g_fail; static const char *volatile g_cur = "startup"; static void watchdog(int sig) @@ -165,7 +166,7 @@ static void arm_watchdog(void) signal(SIGALRM, watchdog); alarm((unsigned)(secs > 0 ? secs : 300)); } -#define RUN(fn) do { g_cur = #fn; printf("---- %s\n", #fn); fn(); } while (0) +#define RUN(fn) do { const char *only_ = getenv("PB_TEST_ONLY"); if (only_ && !strstr(only_, #fn)) break; g_cur = #fn; printf("---- %s\n", #fn); fn(); } while (0) #define CHECK(cond, ...) do { if (cond) { g_pass++; printf("PASS "); } else { g_fail++; printf("FAIL "); } \ printf(__VA_ARGS__); printf("\n"); fflush(stdout); } while (0) @@ -181,13 +182,21 @@ static void fill_orig(struct pb_orig *o, const char *ip, uint16_t port, int prot memcpy(o->addr + 12, &a, 4); o->port = port; o->family = AF_INET; o->proto = (uint8_t)proto; o->action = (uint8_t)action; o->proxy_id = (uint16_t)px; } -static void set_tcp(uint16_t src, const char *ip, uint16_t port, int action, uint32_t px) +static void set_tcp_from(uint32_t src_ip_be, uint16_t src, const char *ip, uint16_t port, int action, uint32_t px) { + uint64_t k = tcp_key(src_ip_be, src); pthread_mutex_lock(&g_mu); - fill_orig(&g_tcp[src], ip, port, IPPROTO_TCP, action, px); - g_tcp_valid[src] = true; + uint32_t i = tcp_hash(k); + while (g_tcp[i].state == 1 && g_tcp[i].key != k) i = (i + 1) % TCP_SLOTS; + g_tcp[i].key = k; + g_tcp[i].state = 1; + fill_orig(&g_tcp[i].o, ip, port, IPPROTO_TCP, action, px); pthread_mutex_unlock(&g_mu); } +static void set_tcp(uint16_t src, const char *ip, uint16_t port, int action, uint32_t px) +{ + set_tcp_from(htonl(INADDR_LOOPBACK), src, ip, port, action, px); +} static int g_nflows; static void set_flow(const char *flow_ip, const char *ip, uint16_t port, int action, uint32_t px) { @@ -222,9 +231,8 @@ static int count_fds(void) DIR *d = opendir("/proc/self/fd"); while (readdir(d)) n++; closedir(d); - return n - 3; // ., .., and the dir fd itself + return n - 3; } -// Waits (up to ms) for the fd count to drop to <= target; returns the final count. static int settle_fds(int target, int ms); static int64_t now_ms(void) { @@ -241,9 +249,6 @@ static int settle_fds(int target, int ms) return n; } -// --------------------------------------------------------------------------------------------- -// Fake servers -// --------------------------------------------------------------------------------------------- struct srvcfg { bool auth; char user[64], pass[64]; bool leftover; bool silent; }; struct srv { int lfd; uint16_t port; void (*h)(int, const struct srvcfg *); struct srvcfg cfg; }; struct hctx { int fd; struct srv *s; }; @@ -399,7 +404,6 @@ static void http_handler(int fd, const struct srvcfg *c) } b[n] = 0; if (c->auth) { - // Basic base64("user:pass") computed by the test config: pass holds the expected token in user field? keep simple: char want[200]; snprintf(want, sizeof want, "Proxy-Authorization: Basic %s", c->pass); if (!strstr(b, want)) { const char *r = "HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"; wr_full(fd, r, strlen(r)); return; } @@ -476,6 +480,8 @@ static uint16_t udpecho_start(void) { struct udpecho *e = calloc(1, sizeof *e); e->fd = socket(AF_INET, SOCK_DGRAM, 0); + int rb = 4 << 20; + setsockopt(e->fd, SOL_SOCKET, SO_RCVBUF, &rb, sizeof rb); struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; bind(e->fd, (struct sockaddr *)&a, sizeof a); socklen_t al = sizeof a; @@ -487,24 +493,22 @@ static uint16_t udpecho_start(void) return e->port; } -// --------------------------------------------------------------------------------------------- -// Client helpers -// --------------------------------------------------------------------------------------------- -static int g_last_connect_errno; // errno of the last failed relay_connect() -static int relay_connect(uint16_t src) +static int g_last_connect_errno; +static int relay_connect_from(uint32_t src_ip_be, uint16_t src) { int fd = socket(AF_INET, SOCK_STREAM, 0); int one = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); - struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_port = htons(src) }; + struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = src_ip_be, .sin_port = htons(src) }; if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { g_last_connect_errno = errno; printf(" relay_connect: bind(127.0.0.1:%u) failed: %s\n", src, strerror(errno)); close(fd); return -1; } a.sin_port = htons(pb_relay_tcp_port()); + a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); struct timeval tv = { 10, 0 }; - setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); // bounds connect() + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { g_last_connect_errno = errno; // an ECONNRESET here is legitimate for flows the relay aborts (BLOCK, cap): on a fast loopback the RST can arrive @@ -518,6 +522,7 @@ static int relay_connect(uint16_t src) setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); return fd; } +static int relay_connect(uint16_t src) { return relay_connect_from(htonl(INADDR_LOOPBACK), src); } static uint16_t new_src(void) { return (uint16_t)atomic_fetch_add(&g_next_port, 1); } static inline uint8_t pat(uint64_t i) { return (uint8_t)((i * 0x9E3779B1u) >> 11); } @@ -537,7 +542,6 @@ static void *xf_send(void *p) if (x->half) shutdown(x->fd, SHUT_WR); return NULL; } -// Sends `total` patterned bytes and verifies the echo. Returns bytes verified, -1 on mismatch. static int64_t xfer(int fd, uint64_t total, bool half) { struct xf x = { fd, total, half }; @@ -559,10 +563,9 @@ static int64_t xfer(int fd, uint64_t total, bool half) pthread_join(t, NULL); return res; } -// True when the peer closes (EOF or reset) within timeout_ms with no data. static bool expect_closed(int fd, int timeout_ms) { - if (timeout_ms < 10000) timeout_ms = 10000; // generous: the watchdog bounds real hangs + if (timeout_ms < 10000) timeout_ms = 10000; struct pollfd p = { fd, POLLIN, 0 }; int pr = poll(&p, 1, timeout_ms); if (pr <= 0) { printf(" expect_closed(fd=%d): poll=%d errno=%d revents=0x%x\n", fd, pr, errno, p.revents); return false; } @@ -599,7 +602,6 @@ static uint16_t free_port(void) return ntohs(a.sin_port); } -// One echo round trip of `n` bytes through the relay for the given action. static int64_t tcp_roundtrip(const char *dst_ip, uint16_t dst_port, int action, uint32_t px, uint64_t n, bool half) { uint16_t src = new_src(); @@ -611,9 +613,6 @@ static int64_t tcp_roundtrip(const char *dst_ip, uint16_t dst_port, int action, return r; } -// --------------------------------------------------------------------------------------------- -// UDP client helpers -// --------------------------------------------------------------------------------------------- static int udp_client(void) { int fd = socket(AF_INET, SOCK_DGRAM, 0); @@ -627,7 +626,6 @@ static void udp_send_flow(int fd, const char *flow_ip, const char *msg) inet_pton(AF_INET, flow_ip, &a.sin_addr); sendto(fd, msg, strlen(msg), 0, (struct sockaddr *)&a, sizeof a); } -// Receives one datagram; returns length or -1; src_ip filled with the sender address. static int udp_recv_to(int fd, char *buf, size_t n, char *src_ip, uint16_t *src_port, int ms) { struct timeval tv = { ms / 1000, (ms % 1000) * 1000 }; @@ -652,9 +650,6 @@ static bool udp_echo_ok(int fd, const char *flow_ip, const char *msg) return r == (int)strlen(msg) && !strcmp(b, msg) && !strcmp(ip, flow_ip) && sp == pb_relay_udp_port(); } -// --------------------------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------------------------- static struct srv *g_echo, *g_s5, *g_s5auth, *g_http, *g_httpauth, *g_httpleft, *g_silent; static uint16_t g_uecho1, g_uecho2; @@ -668,6 +663,207 @@ static void *conc_thread(void *p) return (void *)(intptr_t)(r == (int64_t)(mb << 20)); } +static long vmrss_kb(void) +{ + FILE *f = fopen("/proc/self/status", "r"); + char line[256]; + long v = -1; + while (f && fgets(line, sizeof line, f)) + if (!strncmp(line, "VmRSS:", 6)) { v = atol(line + 6); break; } + if (f) fclose(f); + return v; +} + +static atomic_uint g_churn_ctr; +static atomic_int g_churn_bad, g_churn_udp_lost; +static uint16_t g_dead_port; + +static void churn_one(unsigned n) +{ + uint32_t ip = htonl(0x7f000002 + n % 60); + uint16_t src = (uint16_t)(20000 + (n / 60) % 12000); + int mode = (int)(n % 9); + uint32_t px = 0; + int action = PB_ACT_PROXY; + uint16_t dport = g_echo->port; + switch (mode) { + case 0: px = 1; break; + case 1: px = 3; break; + case 2: action = PB_ACT_DIRECT; break; + case 3: action = PB_ACT_BLOCK; break; + case 4: px = 6; break; + case 5: px = 7; break; + case 6: px = 1; break; + case 7: action = PB_ACT_DIRECT; dport = g_dead_port; break; + default: action = PB_ACT_DIRECT; break; + } + set_tcp_from(ip, src, "127.0.0.1", dport, action, px); + int fd = relay_connect_from(ip, src); + if (fd < 0) { + if (mode != 3 && mode != 4 && mode != 5 && mode != 7) atomic_fetch_add(&g_churn_bad, 1); + return; + } + char b[128]; + switch (mode) { + case 0: case 1: case 2: + if (send(fd, "z", 1, MSG_NOSIGNAL) != 1 || rd_full(fd, b, 1) || b[0] != 'z') atomic_fetch_add(&g_churn_bad, 1); + break; + case 6: { + struct linger l = { 1, 0 }; + setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof l); + break; + } + case 8: { + char big[100] = { 0 }; + send(fd, big, sizeof big, MSG_NOSIGNAL); + struct linger l = { 1, 0 }; + setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof l); + break; + } + default: + (void)expect_closed(fd, 10000); + } + close(fd); +} + +struct churn_arg { unsigned count; }; +static void *churn_tcp_thread(void *p) +{ + struct churn_arg *a = p; + for (unsigned i = 0; i < a->count; i++) churn_one(atomic_fetch_add(&g_churn_ctr, 1)); + return NULL; +} + +struct churn_udp { unsigned total; int bad; }; +static void *churn_udp_thread(void *p) +{ + struct churn_udp *a = p; + enum { B = 1000 }; + int *fds = malloc(B * sizeof *fds); + for (unsigned done = 0; done < a->total;) { + int n = a->total - done < B ? (int)(a->total - done) : B; + for (int lo = 0; lo < n; lo += 50) { // paced so a starved echo server cannot overflow its socket buffer + int hi = lo + 50 < n ? lo + 50 : n; + for (int i = lo; i < hi; i++) { + fds[i] = udp_client(); + udp_send_flow(fds[i], (done + (unsigned)i) % 10 == 0 ? "127.1.9.2" : "127.1.9.1", "churn"); + } + for (int i = lo; i < hi; i++) { + char r[32], ip[32]; + uint16_t sp; + if (udp_recv_to(fds[i], r, sizeof r, ip, &sp, 4000) != 5) a->bad++; + close(fds[i]); + } + } + done += (unsigned)n; + if (done < a->total) msleep(2200); + } + free(fds); + return NULL; +} + +static void churn_phase(unsigned tcp_total, unsigned udp_total) +{ + enum { T = 8 }; + pthread_t th[T], ut; + struct churn_arg ca = { tcp_total / T }; + struct churn_udp cu = { udp_total, 0 }; + pthread_create(&ut, NULL, churn_udp_thread, &cu); + for (int i = 0; i < T; i++) pthread_create(&th[i], NULL, churn_tcp_thread, &ca); + for (int i = 0; i < T; i++) pthread_join(th[i], NULL); + pthread_join(ut, NULL); + atomic_fetch_add(&g_churn_udp_lost, cu.bad); +} + +static void test_churn(void) +{ + unsigned tcp_total = getenv("PB_CHURN_TCP") ? (unsigned)atoi(getenv("PB_CHURN_TCP")) : 100000; + unsigned udp_total = getenv("PB_CHURN_UDP") ? (unsigned)atoi(getenv("PB_CHURN_UDP")) : 20000; + long limit_kb = (getenv("PB_CHURN_RSS_MB") ? atol(getenv("PB_CHURN_RSS_MB")) : 6) * 1024; + pb_relay_stop(); + CHECK(relay_up(300, 1, 0, 5) == 0, "restart relay for churn (udp_idle=1s)"); + g_dead_port = free_port(); + set_proxy(6, PROXY_TYPE_SOCKS5, g_dead_port, NULL, NULL); + set_proxy(7, PROXY_TYPE_SOCKS5, g_s5auth->port, "alice", "WRONG"); + set_flow("127.1.9.1", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); + set_flow("127.1.9.2", "127.0.0.1", g_uecho2, PB_ACT_PROXY, 1); + msleep(300); + int base_fds = count_fds(); + + unsigned chunk_tcp = tcp_total / 10 / 8 * 8, chunk_udp = udp_total / 10; + long budget_ms = (getenv("PB_CHURN_BUDGET_S") ? atol(getenv("PB_CHURN_BUDGET_S")) : 60) * 1000; + int64_t t0 = now_ms(); + churn_phase(chunk_tcp, chunk_udp); + msleep(3000); + long rss0 = vmrss_kb(); + int chunks = 1; + while (chunks < 10 && now_ms() - t0 < budget_ms) { churn_phase(chunk_tcp, chunk_udp); chunks++; } + msleep(3500); + int fds_end = settle_fds(base_fds, 10000); + long rss1 = vmrss_kb(); + unsigned tcp_done = chunk_tcp * (unsigned)chunks, udp_done = chunk_udp * (unsigned)chunks; + printf(" churn: %u TCP + %u UDP sessions in %lld ms; RSS after warm-up %ld KB, final %ld KB (+%ld KB); fds base=%d end=%d\n", + tcp_done, udp_done, (long long)(now_ms() - t0), rss0, rss1, rss1 - rss0, base_fds, fds_end); + CHECK(chunks >= 4, "churn: completed %d of 10 chunks within the time budget", chunks); + CHECK(g_churn_bad == 0, "churn: every TCP flow that must succeed did (failures=%d)", (int)g_churn_bad); + CHECK(g_churn_udp_lost * 50 <= (int)udp_done, "churn: UDP datagrams lost <= 2%% (%d of %u; session-limit drop logged: %s)", (int)g_churn_udp_lost, udp_done, + log_has("UDP session limit") ? "yes" : "no"); + CHECK(fds_end <= base_fds, "churn: fd count back to baseline (base=%d end=%d)", base_fds, fds_end); + CHECK(rss1 - rss0 < limit_kb, "churn: RSS growth after warm-up < %ld MB (+%ld KB)", limit_kb / 1024, rss1 - rss0); + pb_relay_stop(); + CHECK(relay_up(300, 60, 0, 5) == 0, "restart relay"); +} + +static void *perf_conn_thread(void *p) +{ + int n = (int)(intptr_t)p, ok = 0; + for (int i = 0; i < n; i++) { + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 1); + int fd = relay_connect(src); + char b = 'a'; + if (fd >= 0 && send(fd, &b, 1, MSG_NOSIGNAL) == 1 && recv(fd, &b, 1, 0) == 1) ok++; + if (fd >= 0) close(fd); + } + return (void *)(intptr_t)ok; +} + +static void test_perf(void) +{ + unsigned dg = getenv("PB_PERF_UDP") ? (unsigned)atoi(getenv("PB_PERF_UDP")) : 60000; + int conns = getenv("PB_PERF_CONNS") ? atoi(getenv("PB_PERF_CONNS")) : 1500; + set_flow("127.1.10.1", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); + set_flow("127.1.10.2", "127.0.0.1", g_uecho2, PB_ACT_PROXY, 1); + const char *fl[2] = { "127.1.10.1", "127.1.10.2" }; + const char *nm[2] = { "DIRECT", "SOCKS5" }; + for (int k = 0; k < 2; k++) { + int c = udp_client(); + struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(pb_relay_udp_port()) }; + inet_pton(AF_INET, fl[k], &a.sin_addr); + char msg[64] = "perf-datagram", rx[128], ip[32]; + uint16_t sp; + udp_echo_ok(c, fl[k], "warm"); + unsigned got = 0; + int64_t t0 = now_ms(); + for (unsigned sent = 0; sent < dg; sent += 16) { + for (int i = 0; i < 16; i++) sendto(c, msg, sizeof msg, 0, (struct sockaddr *)&a, sizeof a); + for (int i = 0; i < 16; i++) if (udp_recv_to(c, rx, sizeof rx, ip, &sp, 2000) > 0) got++; + } + int64_t ms = now_ms() - t0; + CHECK(got >= dg * 99 / 100, "perf UDP %s: %u datagrams round trip, %.0f pps (%lld ms, %u received)", nm[k], dg, + (double)got * 1000.0 / (double)(ms + 1), (long long)ms, got); + close(c); + } + enum { T = 8 }; + pthread_t th[T]; + int64_t t0 = now_ms(); + for (int i = 0; i < T; i++) pthread_create(&th[i], NULL, perf_conn_thread, (void *)(intptr_t)(conns / T)); + int ok = 0; + for (int i = 0; i < T; i++) { void *r; pthread_join(th[i], &r); ok += (int)(intptr_t)r; } + int64_t ms = now_ms() - t0; + CHECK(ok == conns / T * T, "perf TCP via SOCKS5: %d short connections, %.0f conn/s (%lld ms)", ok, (double)ok * 1000.0 / (double)(ms + 1), (long long)ms); +} + static void test_tcp_basic(void) { ev_clear(); @@ -681,7 +877,6 @@ static void test_tcp_basic(void) CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 100000, false) == 100000, "TCP DIRECT"); CHECK(g_ev == 1 && g_ev_direct == 1, "flow_event for direct"); - // HTTP leftover bytes after the 200 response must reach the client first uint16_t src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 5); int fd = relay_connect(src); @@ -690,7 +885,6 @@ static void test_tcp_basic(void) CHECK(ok, "HTTP CONNECT leftover bytes forwarded to client"); if (fd >= 0) close(fd); - // BLOCK ev_clear(); src = new_src(); set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_BLOCK, 0); @@ -700,7 +894,6 @@ static void test_tcp_basic(void) CHECK(g_ev_block == 1, "flow_event BLOCK"); close(fd); - // DECIDE set_decision((struct pb_decision){ PB_ACT_DIRECT, 0 }); CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DECIDE, 0, 5000, false) == 5000, "TCP DECIDE -> DIRECT"); set_decision((struct pb_decision){ PB_ACT_PROXY, 1 }); @@ -712,12 +905,28 @@ static void test_tcp_basic(void) CHECK(closed_by_relay(fd, 3000), "TCP DECIDE -> BLOCK"); close(fd); - // no orig entry fd = relay_connect(new_src()); CHECK(closed_by_relay(fd, 3000), "TCP without original destination is closed"); close(fd); - // domain-name target through test helper is covered separately; IPv6 original destination (no v6 proxy server: direct to ::1) +} + +static void test_tcp_identity(void) +{ + uint16_t src = new_src(); + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0); + int fd = relay_connect_from(htonl(0x7f000002), src); + if (fd >= 0) send(fd, "x", 1, MSG_NOSIGNAL); + CHECK(closed_by_relay(fd, 3000), "connection from 127.0.0.2 with a registered port is closed"); + if (fd >= 0) close(fd); + fd = relay_connect(src); + char b = 0; + bool ok = fd >= 0 && send(fd, "y", 1, MSG_NOSIGNAL) == 1 && rd_full(fd, &b, 1) == 0 && b == 'y'; + CHECK(ok, "registered flow from the right source address still works (entry not consumed by the impostor)"); + if (fd >= 0) close(fd); + fd = relay_connect_from(htonl(0x7f000003), new_src()); + CHECK(closed_by_relay(fd, 3000), "connection from an unregistered address/port is closed"); + if (fd >= 0) close(fd); } static void test_tcp_bad_proxy(void) @@ -749,12 +958,11 @@ static void test_tcp_bad_proxy(void) log_clear(); src = new_src(); - set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 12); // unknown proxy id + set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_PROXY, 12); fd = relay_connect(src); CHECK(closed_by_relay(fd, 5000) && log_has("no proxy configured"), "unknown proxy id: client closed + logged"); close(fd); - // dead target through a working proxy -> SOCKS5 REP 5 log_clear(); src = new_src(); set_tcp(src, "127.0.0.1", dead, PB_ACT_PROXY, 1); @@ -972,7 +1180,7 @@ static void test_tcp_idle(void) set_tcp(src, "127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0); fd = relay_connect(src); bool alive = true; - for (int i = 0; i < 10 && alive; i++) { // ~5 s of traffic every 0.5 s, longer than the 3 s idle timeout + for (int i = 0; i < 10 && alive; i++) { send(fd, "p", 1, 0); alive = rd_full(fd, b, 1) == 0; msleep(500); @@ -1004,7 +1212,7 @@ static void test_tcp_cap(void) close(extra); close(fds[0]); bool freed = false; - for (int i = 0; i < 100 && !freed; i++) { // up to ~10 s for the relay to notice the close + for (int i = 0; i < 100 && !freed; i++) { msleep(100); freed = tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 1000, false) == 1000; } @@ -1041,7 +1249,6 @@ static void test_udp(void) CHECK(udp_echo_ok(c, "127.1.2.3", "again"), "first destination again"); CHECK(g_assoc_count - a0 == 1, "exactly one UDP ASSOCIATE for the session (got %d)", (int)g_assoc_count - a0); - // burst while association is being set up (new client => new session): queued datagrams all delivered int c2 = udp_client(); for (int i = 0; i < 10; i++) { char m[32]; snprintf(m, sizeof m, "burst%d", i); udp_send_flow(c2, "127.1.2.3", m); } int got = 0; @@ -1049,39 +1256,33 @@ static void test_udp(void) CHECK(got == 10, "10 datagrams sent during association setup delivered (got %d)", got); close(c2); - // SOCKS5 with auth for UDP set_flow("127.1.2.5", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 2); c2 = udp_client(); CHECK(udp_echo_ok(c2, "127.1.2.5", "auth-udp"), "UDP via authenticated SOCKS5"); close(c2); - // direct set_flow("127.1.3.1", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); set_flow("127.1.3.2", "127.0.0.1", g_uecho2, PB_ACT_DIRECT, 0); CHECK(udp_echo_ok(c, "127.1.3.1", "direct-1") && udp_echo_ok(c, "127.1.3.2", "direct-2"), "UDP DIRECT, two destinations"); - // block / unknown flow / non-loopback set_flow("127.1.4.1", "127.0.0.1", g_uecho1, PB_ACT_BLOCK, 0); udp_send_flow(c, "127.1.4.1", "blocked"); CHECK(udp_recv_neg(c, b, sizeof b, ip, &sp) < 0, "UDP BLOCK dropped"); udp_send_flow(c, "127.9.9.9", "unknown"); CHECK(udp_recv_neg(c, b, sizeof b, ip, &sp) < 0, "UDP unknown flow dropped"); - // HTTP proxy cannot do UDP log_clear(); set_flow("127.1.4.2", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 3); udp_send_flow(c, "127.1.4.2", "via-http"); udp_send_flow(c, "127.1.4.2", "via-http2"); CHECK(udp_recv_neg(c, b, sizeof b, ip, &sp) < 0 && log_has("HTTP proxy"), "UDP via HTTP proxy dropped + logged"); - // DECIDE cached per (client, flow) set_decision((struct pb_decision){ PB_ACT_DIRECT, 0 }); set_flow("127.1.4.3", "127.0.0.1", g_uecho1, PB_ACT_DECIDE, 0); g_decide_calls = 0; bool ok = udp_echo_ok(c, "127.1.4.3", "d1") && udp_echo_ok(c, "127.1.4.3", "d2") && udp_echo_ok(c, "127.1.4.3", "d3"); CHECK(ok && g_decide_calls == 1, "UDP DECIDE -> DIRECT, decided once (calls=%d)", (int)g_decide_calls); - // bad proxy for UDP: nothing comes back, no crash set_flow("127.1.4.4", "127.0.0.1", g_uecho1, PB_ACT_PROXY, 6); udp_send_flow(c, "127.1.4.4", "dead-proxy"); CHECK(udp_recv_neg(c, b, sizeof b, ip, &sp) < 0, "UDP via dead SOCKS5 proxy: no reply, relay survives"); @@ -1098,7 +1299,7 @@ static void test_udp_idle(void) set_flow("127.1.5.2", "127.0.0.1", g_uecho2, PB_ACT_PROXY, 1); int base = count_fds(); bool ok = udp_echo_ok(c, "127.1.5.1", "x") && udp_echo_ok(c, "127.1.5.2", "y"); - int mid = count_fds(); // direct: +1 fd; socks5: +ctl +udp in relay and +3 in the fake server + int mid = count_fds(); int end = settle_fds(base, 15000); CHECK(ok && mid > base && end <= base, "UDP sessions expire after idle and free their fds (base=%d mid=%d end=%d)", base, mid, end); CHECK(udp_echo_ok(c, "127.1.5.1", "again"), "new session after expiry works"); @@ -1128,7 +1329,6 @@ static void test_start_stop(void) int after = settle_fds(before, 5000); CHECK(bad == 0 && after <= before, "start/stop x20 (with traffic): fds before=%d after=%d, failures=%d", before, after, bad); - // start while ports are taken: must fail and leak nothing int l = socket(AF_INET, SOCK_STREAM, 0); int one = 1; setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); @@ -1157,9 +1357,6 @@ static void test_start_stop(void) static void test_proxy_helper(void) { char b[256]; - // HTTP target on port 80 is not available on loopback in tests: use a tiny HTTP server through the proxies. - struct srvcfg hc = { 0 }; - (void)hc; int r = pb_relay_test_proxy(&g_px[1], "echo.test", g_echo->port, b, sizeof b); CHECK(r == 0 && !strcmp(b, "Tunnel established"), "test_proxy SOCKS5 domain ATYP, non-80 port: '%s'", b); r = pb_relay_test_proxy(&g_px[2], "127.0.0.1", g_echo->port, b, sizeof b); @@ -1174,12 +1371,6 @@ static void test_proxy_helper(void) CHECK(r == -1 && strstr(b, "refused"), "test_proxy unreachable target -> error: '%s'", b); } -// Tiny HTTP origin to exercise the port-80 HEAD path of pb_relay_test_proxy (it listens on a high port, so we -// verify the HEAD logic via a SOCKS5 server mapping port 80 is not possible; instead call with the origin port and -// confirm the tunnel path only. HEAD logic is covered by the echo server answering the request verbatim.) -// --------------------------------------------------------------------------------------------- -// Network namespace so that we can bind port 53 without real root -// --------------------------------------------------------------------------------------------- static bool loopback_ok(void) { int l = socket(AF_INET, SOCK_STREAM, 0), c = socket(AF_INET, SOCK_STREAM, 0); @@ -1211,7 +1402,7 @@ static bool can_bind53(void) static bool isolate_for_port53(void) { bool force_no_lo = getenv("PB_TEST_FORCE_NO_LO") != NULL; - if (!force_no_lo && can_bind53() && loopback_ok()) return true; // already privileged enough + if (!force_no_lo && can_bind53() && loopback_ok()) return true; uid_t uid = getuid(); gid_t gid = getgid(); if (unshare(CLONE_NEWUSER | CLONE_NEWNET) != 0) { printf("NOTE DNS child: unshare failed: %s\n", strerror(errno)); return false; } @@ -1235,13 +1426,9 @@ static bool isolate_for_port53(void) return true; } -// Shared with the forked DNS child. struct shared { int pass, fail, status; }; // status: 0 = not run, 1 = ran, 3 = cannot isolate static struct shared *g_shared; -// --------------------------------------------------------------------------------------------- -// DNS tap tests -// --------------------------------------------------------------------------------------------- static void mk_msg(int mode, int idx, uint8_t *m, size_t len) { for (size_t i = 0; i < len; i++) m[i] = (uint8_t)(mode * 31 + idx * 17 + (int)i * 7); @@ -1257,14 +1444,14 @@ static void dns_tcp_handler(int fd, const struct srvcfg *c) if (n > sizeof q || rd_full(fd, q, n)) return; int mode = q[0]; uint8_t out[300]; - if (mode == 'a') { // one 100-byte message, written one byte at a time + if (mode == 'a') { out[0] = 0; out[1] = 100; mk_msg(mode, 0, out + 2, 100); for (int i = 0; i < 102; i++) { wr_full(fd, out + i, 1); usleep(1500); } - } else if (mode == 'b') { // two messages in a single write + } else if (mode == 'b') { out[0] = 0; out[1] = 40; mk_msg(mode, 0, out + 2, 40); out[42] = 0; out[43] = 60; mk_msg(mode, 1, out + 44, 60); wr_full(fd, out, 104); - } else { // 'c': plain single write + } else { out[0] = 0; out[1] = 100; mk_msg(mode, 0, out + 2, 100); wr_full(fd, out, 102); } @@ -1344,11 +1531,9 @@ static void test_dns(void) dns_tcp_case("via HTTP CONNECT 1-byte writes", PB_ACT_PROXY, 3, 'a', 1); dns_tcp_case("via HTTP CONNECT two messages", PB_ACT_PROXY, 3, 'b', 2); - // non-53 TCP must not be observed atomic_store(&g_obs_n, 0); CHECK(tcp_roundtrip("127.0.0.1", g_echo->port, PB_ACT_DIRECT, 0, 1000, false) == 1000 && g_obs_n == 0, "TCP non-53 not observed"); - // UDP struct { const char *name; int action; uint32_t px; const char *flow; } u[] = { { "UDP DIRECT", PB_ACT_DIRECT, 0, "127.1.7.1" }, { "UDP via SOCKS5", PB_ACT_PROXY, 1, "127.1.7.2" } }; for (int k = 0; k < 2; k++) { @@ -1366,7 +1551,6 @@ static void test_dns(void) CHECK(ok, "%s DNS tap: reply observed (calls=%d) before delivery, from flow address", u[k].name, (int)g_obs_n); close(c); } - // non-53 UDP not observed atomic_store(&g_obs_n, 0); int c = udp_client(); set_flow("127.1.7.3", "127.0.0.1", g_uecho1, PB_ACT_DIRECT, 0); @@ -1375,9 +1559,6 @@ static void test_dns(void) atomic_store(&g_obs_sleep, 0); } -// --------------------------------------------------------------------------------------------- -// Latency, hostname proxies, UDP sizes / odd servers -// --------------------------------------------------------------------------------------------- static double pingpong_us(int fd, int n) { char b = 'x'; @@ -1530,7 +1711,6 @@ static void init_world(void) struct srvcfg ca = { .auth = true }; strcpy(ca.user, "alice"); strcpy(ca.pass, "secret"); g_s5auth = srv_start(socks_handler, &ca); g_http = srv_start(http_handler, &c); - // HTTP auth: server expects base64("alice:secret") == "YWxpY2U6c2VjcmV0" struct srvcfg ch = { .auth = true }; strcpy(ch.pass, "YWxpY2U6c2VjcmV0"); g_httpauth = srv_start(http_handler, &ch); struct srvcfg cl = { .leftover = true }; @@ -1596,6 +1776,7 @@ int main(void) g_cur = "pb_relay_start"; CHECK(relay_up(300, 60, 0, 5) == 0, "pb_relay_start"); RUN(test_tcp_basic); + RUN(test_tcp_identity); RUN(test_tcp_bad_proxy); RUN(test_tcp_halfclose); RUN(test_udp); @@ -1605,7 +1786,9 @@ int main(void) RUN(test_latency); RUN(test_tcp_large); RUN(test_throughput); + RUN(test_perf); RUN(test_tcp_10k); + RUN(test_churn); RUN(test_tcp_idle); RUN(test_tcp_cap); RUN(test_handshake_timeout); diff --git a/Linux/src/tests/run_relay_test.sh b/Linux/src/tests/run_relay_test.sh index cab1f8f9..c9cf79d1 100755 --- a/Linux/src/tests/run_relay_test.sh +++ b/Linux/src/tests/run_relay_test.sh @@ -11,5 +11,5 @@ if [ "$mode" = plain ] || [ "$mode" = both ]; then fi if [ "$mode" = asan ] || [ "$mode" = both ]; then gcc $CF -fsanitize=address,undefined -fno-omit-frame-pointer relay_test.c ../pb_relay.c -o "$out/relay_test_asan" -lpthread - PB_TEST_WATCHDOG="${PB_TEST_WATCHDOG:-900}" LARGE_MB="${LARGE_MB:-4}" THRU_MB="${THRU_MB:-32}" ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$out/relay_test_asan" + PB_TEST_WATCHDOG="${PB_TEST_WATCHDOG:-900}" LARGE_MB="${LARGE_MB:-4}" THRU_MB="${THRU_MB:-32}" PB_CHURN_TCP="${PB_CHURN_TCP:-10000}" PB_CHURN_UDP="${PB_CHURN_UDP:-3000}" PB_CHURN_RSS_MB="${PB_CHURN_RSS_MB:-48}" PB_PERF_UDP=10000 PB_PERF_CONNS=400 ASAN_OPTIONS=detect_leaks=1:quarantine_size_mb=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$out/relay_test_asan" fi From a89668d04c8b030e178375aed5c3fd5c07f7d4df Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 15:47:28 +0530 Subject: [PATCH 18/25] fix(linux/cli): keep proxy credentials out of the process list; use the version macro --proxy specs are wiped from argv after parsing so `ps` no longer shows user:pass; the banner prints PROXYBRIDGE_VERSION; narration comments removed. --- Linux/cli/main.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Linux/cli/main.c b/Linux/cli/main.c index bc33c03f..dc436403 100644 --- a/Linux/cli/main.c +++ b/Linux/cli/main.c @@ -17,7 +17,7 @@ typedef struct { char target_ports[256]; RuleProtocol protocol; RuleAction action; - char proxy_ref[64]; /* name or 1-based index of a --proxy; empty = first proxy */ + char proxy_ref[64]; /* proxy name or 1-based index; empty = default */ } ProxyRule; typedef struct { @@ -53,7 +53,7 @@ static void connection_callback(const char* process_name, uint32_t pid, const ch static void signal_handler(int sig) { (void)sig; - keep_running = false; /* async-signal-safe: the main loop does the shutdown */ + keep_running = false; } static void show_banner(void) @@ -64,7 +64,7 @@ static void show_banner(void) printf(" | |_) | '__/ _ \\ \\/ / | | | | _ \\| '__| |/ _` |/ _` |/ _ \\\n"); printf(" | __/| | | (_) > <| |_| | | |_) | | | | (_| | (_| | __/\n"); printf(" |_| |_| \\___/_/\\_\\\\__, | |____/|_| |_|\\__,_|\\__, |\\___|\n"); - printf(" |___/ |___/ V4.0\n"); + printf(" |___/ |___/ V%s\n", PROXYBRIDGE_VERSION); printf("\n"); printf(" Universal proxy client for Linux applications\n"); printf("\n"); @@ -198,13 +198,11 @@ static bool parse_rule(const char* rule_str, ProxyRule* rule) fprintf(stderr, "Expected format: process:hosts:ports:protocol:action[=proxy]\n"); return false; } - /* after trimming: buffer = process ':' hosts ':' ports ; protocol = second_last+1 ; action = last+1 */ *third_last = '\0'; const char* ports = third_last + 1; const char* protocol = second_last + 1; const char* action = last + 1; - /* process = up to the first ':' ; hosts = between first ':' and third_last */ char* sep = strchr(buffer, ':'); if (!sep) { @@ -307,7 +305,7 @@ static bool parse_proxy_spec(const char* spec, ProxyEntry* p) static uint32_t find_proxy(const ProxyEntry* proxies, int n, const char* ref) { if (!ref[0]) - return 0; /* default proxy */ + return 0; char* endp = NULL; long idx = strtol(ref, &endp, 10); if (!*endp && idx >= 1 && idx <= n) @@ -356,6 +354,7 @@ int main(int argc, char *argv[]) } if (!parse_proxy_spec(argv[++i], &proxies[num_proxies])) return 1; + memset(argv[i], 0, strlen(argv[i])); /* keep credentials out of `ps` */ num_proxies++; } else if (strcmp(argv[i], "--rule") == 0 && i + 1 < argc) From 6c8e87aeddd0aaaf826154329d1dce52f7fe59cc Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 15:47:28 +0530 Subject: [PATCH 19/25] refactor(linux/gui): fix the exit leak, share helpers, move the rules JSON parser out - profiles, proxies and rules are freed at exit (the only leak found) - gui_json.c holds the rules import/export parser: it finds the end of an object properly (a '}' inside a string no longer breaks an import) and is unit-tested with hostile input (unterminated quotes, wrong types, a 20 MB string, 1M open braces, a 100k-rule round trip) - duplicated dialog, combo and idle-callback code merged into small helpers; unused includes, structs and exported functions removed; comments cut to under 2% - config tests run clean under ASan/LeakSanitizer and include a 20,000-cycle profile create/rename/duplicate/delete + save/load stress --- Linux/gui/gui.h | 80 ++-- Linux/gui/gui_config.c | 329 ++++++++--------- Linux/gui/gui_json.c | 140 +++++++ Linux/gui/gui_logs.c | 118 ++---- Linux/gui/gui_rules.c | 672 +++++++++++++--------------------- Linux/gui/gui_settings.c | 521 ++++++++++---------------- Linux/gui/gui_utils.c | 117 ++---- Linux/gui/main.c | 190 ++++------ Linux/gui/tests/config_test.c | 98 +++-- Linux/gui/tests/json_test.c | 83 +++++ Linux/gui/tests/run_tests.sh | 13 + 11 files changed, 1079 insertions(+), 1282 deletions(-) create mode 100644 Linux/gui/gui_json.c create mode 100644 Linux/gui/tests/json_test.c create mode 100755 Linux/gui/tests/run_tests.sh diff --git a/Linux/gui/gui.h b/Linux/gui/gui.h index a1c05771..653f6716 100644 --- a/Linux/gui/gui.h +++ b/Linux/gui/gui.h @@ -6,14 +6,11 @@ #include #include #include -#include -#include -#include +#include #include #include -#include -#include #include "ProxyBridge.h" + typedef struct { char *process_name; uint32_t pid; @@ -24,12 +21,8 @@ typedef struct { } ConnectionData; typedef struct { - char *message; -} LogData; - -typedef struct { - uint32_t id; // library proxy id, 0 = not registered with the library - char *name; // unique key, also what rules and the config refer to + uint32_t id; // library id, 0 = not registered + char *name; // unique key, rules and config refer to it ProxyType type; char *host; uint16_t port; @@ -37,17 +30,6 @@ typedef struct { char *password; } ProxyData; -// proxy manager dialog widgets -typedef struct { - GtkWidget *dialog; - GtkWidget *tree; - GtkListStore *store; - GtkWidget *test_host; - GtkWidget *test_port; - GtkTextBuffer *output_buffer; - GtkWidget *test_btn; -} ConfigInfo; - typedef struct { uint32_t id; char *process_name; @@ -60,19 +42,15 @@ typedef struct { bool selected; } RuleData; -struct TestRunnerData { - uint32_t proxy_id; - char *host; - uint16_t port; - GtkTextBuffer *buffer; // referenced, released in the idle callback - GtkWidget *btn; // referenced, released in the idle callback -}; - +// lists are only current while the profile is inactive, see profile_sync_active typedef struct { - char *result_text; - GtkTextBuffer *buffer; - GtkWidget *btn; -} TestResultData; + char *name; + GList *proxies; + char *default_proxy; + GList *rules; +} Profile; + +typedef void (*RuleImportCb)(const RuleData *rule, int index, void *ud); extern GtkWidget *window; extern GtkTextBuffer *conn_buffer; @@ -80,18 +58,10 @@ extern GtkTextBuffer *log_buffer; extern GtkWidget *status_bar; extern guint status_context_id; -typedef struct { - char *name; - GList *proxies; // ProxyData, only current while the profile is not active (see profile_sync_active) - char *default_proxy; - GList *rules; // RuleData -} Profile; - -extern GList *g_profiles; // list of Profile -extern char *g_active_profile; // name of the active profile -extern GList *g_proxies_list; // list of ProxyData -extern char *g_default_proxy; // name of the default proxy, NULL if none - +extern GList *g_profiles; +extern char *g_active_profile; +extern GList *g_proxies_list; // working copy of the active profile +extern char *g_default_proxy; extern GList *g_rules_list; extern bool g_chk_logging; extern bool g_chk_dns; @@ -99,23 +69,25 @@ extern bool g_chk_dns; long safe_strtol(const char *nptr); void show_message(GtkWindow *parent, GtkMessageType type, const char *format, ...); void trim_buffer_lines(GtkTextBuffer *buffer, int max_lines); -char* get_current_time_str(); -char *escape_json_string(const char *src); +char *get_current_time_str(); +GtkWidget *combo_new(const char *first, ...) G_GNUC_NULL_TERMINATED; +GtkWidget *hint_label(const char *text); +void set_error_label(GtkWidget *label, const char *msg); +void grid_row(GtkWidget *grid, int row, const char *label, GtkWidget *widget); + char *extract_sub_json_str(const char *json, const char *key); -bool extract_sub_json_bool(const char *json, const char *key); -bool json_has_key(const char *json, const char *key); +int rules_from_json(const char *json, RuleImportCb cb, void *ud); +char *rules_to_json(GList *rules, bool only_selected); -// config storage void save_config(); void load_config(); -void profile_sync_active(); +void config_free_all(); Profile *profile_find(const char *name); Profile *profile_create(const char *name, bool copy_active); bool profile_rename(Profile *p, const char *new_name); bool profile_activate(const char *name); bool profile_delete(Profile *p); -// settings / proxies void on_proxy_configure(GtkWidget *widget, gpointer data); void on_profiles_clicked(GtkWidget *widget, gpointer data); ProxyData *find_proxy_by_name(const char *name); @@ -123,13 +95,11 @@ uint32_t resolve_proxy_id(const char *name); void free_proxy_data(ProxyData *p); void apply_proxies_to_lib(); -// rules section void on_proxy_rules_clicked(GtkWidget *widget, gpointer data); void free_rule_data(RuleData *rule); void apply_rules_to_lib(); void refresh_rules_window(); -// Logs void gui_log(const char *format, ...); void lib_log_callback(const char *message); void lib_connection_callback(const char *process_name, uint32_t pid, const char *dest_ip, uint16_t dest_port, const char *proxy_info); diff --git a/Linux/gui/gui_config.c b/Linux/gui/gui_config.c index 433a1829..75896f36 100644 --- a/Linux/gui/gui_config.c +++ b/Linux/gui/gui_config.c @@ -3,8 +3,8 @@ #define CONFIG_DIR_DEFAULT "/etc/proxybridge" -GList *g_profiles = NULL; // list of Profile -char *g_active_profile = NULL; // name of the active profile +GList *g_profiles = NULL; +char *g_active_profile = NULL; // PROXYBRIDGE_CONFIG_DIR overrides the default dir (used for testing) static char *config_dir() { @@ -12,17 +12,23 @@ static char *config_dir() { return g_strdup((env && *env) ? env : CONFIG_DIR_DEFAULT); } -static char *config_path() { - char *dir = config_dir(); - char *path = g_strdup_printf("%s/config.ini", dir); - g_free(dir); - return path; +static char *esc(const char *s) { + return g_uri_escape_string(s ? s : "", NULL, FALSE); +} + +static char *unesc(const char *s) { + char *out = g_uri_unescape_string(s ? s : "", NULL); + return out ? out : g_strdup(""); } -// ---- profiles ---- +static void put_esc(FILE *f, const char *s, char sep) { + char *e = esc(s); + fputs(e, f); + fputc(sep, f); + g_free(e); +} static void free_profile(Profile *p) { - if (!p) return; for (GList *l = p->proxies; l != NULL; l = l->next) free_proxy_data((ProxyData *)l->data); for (GList *l = p->rules; l != NULL; l = l->next) free_rule_data((RuleData *)l->data); g_list_free(p->proxies); @@ -34,15 +40,13 @@ static void free_profile(Profile *p) { Profile *profile_find(const char *name) { if (!name) return NULL; - for (GList *l = g_profiles; l != NULL; l = l->next) { - Profile *p = (Profile *)l->data; - if (strcmp(p->name, name) == 0) return p; - } + for (GList *l = g_profiles; l != NULL; l = l->next) + if (strcmp(((Profile *)l->data)->name, name) == 0) return (Profile *)l->data; return NULL; } -// the working lists (g_proxies_list etc.) belong to the active profile, copy the heads back -void profile_sync_active() { +// the working lists belong to the active profile, copy the (possibly changed) heads back +static void profile_sync_active() { Profile *p = profile_find(g_active_profile); if (!p) return; p->proxies = g_proxies_list; @@ -56,6 +60,16 @@ static void load_globals_from(Profile *p) { g_default_proxy = p->default_proxy; } +void config_free_all() { + profile_sync_active(); + g_list_free_full(g_profiles, (GDestroyNotify)free_profile); + g_profiles = NULL; + g_free(g_active_profile); + g_active_profile = NULL; + g_proxies_list = g_rules_list = NULL; + g_default_proxy = NULL; +} + static Profile *profile_new_empty(const char *name) { Profile *p = g_malloc0(sizeof(Profile)); p->name = g_strdup(name); @@ -81,12 +95,11 @@ static RuleData *copy_rule(const RuleData *src) { d->target_ports = g_strdup(src->target_ports ? src->target_ports : ""); d->protocol = src->protocol; d->action = src->action; - d->proxy_name = src->proxy_name ? g_strdup(src->proxy_name) : NULL; + d->proxy_name = g_strdup(src->proxy_name); d->enabled = src->enabled; return d; } -// new profile (empty, or a copy of the active one). NULL if the name is empty or taken Profile *profile_create(const char *name, bool copy_active) { if (!name || !*name || profile_find(name)) return NULL; profile_sync_active(); @@ -114,12 +127,11 @@ bool profile_rename(Profile *p, const char *new_name) { return true; } -// makes the profile active (working lists swap). does not touch the library bool profile_activate(const char *name) { Profile *p = profile_find(name); if (!p) return false; profile_sync_active(); - if (g_active_profile != name) { // name may alias g_active_profile + if (name != g_active_profile) { char *copy = g_strdup(name); g_free(g_active_profile); g_active_profile = copy; @@ -128,74 +140,59 @@ bool profile_activate(const char *name) { return true; } -// last profile cannot be deleted. deleting the active one activates another first +// the last profile stays; deleting the active one activates another first bool profile_delete(Profile *p) { if (!p || !g_profiles || !g_profiles->next) return false; if (g_active_profile && strcmp(g_active_profile, p->name) == 0) { - Profile *other = NULL; - for (GList *l = g_profiles; l != NULL; l = l->next) if (l->data != p) { other = (Profile *)l->data; break; } - if (!other || !profile_activate(other->name)) return false; + Profile *other = (Profile *)g_profiles->data; + if (other == p) other = (Profile *)g_profiles->next->data; + if (!profile_activate(other->name)) return false; } g_profiles = g_list_remove(g_profiles, p); free_profile(p); return true; } -// ---- save ---- - static void write_profile(FILE *f, Profile *prof) { - char *pn = g_uri_escape_string(prof->name, NULL, FALSE); - char *def_esc = g_uri_escape_string(prof->default_proxy ? prof->default_proxy : "", NULL, FALSE); - fprintf(f, "[PROFILE:%s]\n", pn); - fprintf(f, "defaultProxy=%s\n", def_esc); + fputs("[PROFILE:", f); + char *pn = esc(prof->name); + fprintf(f, "%s]\ndefaultProxy=", pn); g_free(pn); - g_free(def_esc); + put_esc(f, prof->default_proxy, '\n'); - // proxies section, text fields are percent-encoded so '|' and newlines are safe - fprintf(f, "[PROXIES]\n"); + // fields are percent-encoded so '|' and newlines are safe: name|type|host|port|user|pass + fputs("[PROXIES]\n", f); for (GList *l = prof->proxies; l != NULL; l = l->next) { ProxyData *p = (ProxyData *)l->data; - // format: name|type|host|port|user|pass - char *name = g_uri_escape_string(p->name ? p->name : "", NULL, FALSE); - char *host = g_uri_escape_string(p->host ? p->host : "", NULL, FALSE); - char *user = g_uri_escape_string(p->username ? p->username : "", NULL, FALSE); - char *pass = g_uri_escape_string(p->password ? p->password : "", NULL, FALSE); - fprintf(f, "%s|%d|%s|%u|%s|%s\n", name, p->type, host, p->port, user, pass); - g_free(name); g_free(host); g_free(user); g_free(pass); + put_esc(f, p->name, '|'); + fprintf(f, "%d|", p->type); + put_esc(f, p->host, '|'); + fprintf(f, "%u|", p->port); + put_esc(f, p->username, '|'); + put_esc(f, p->password, '\n'); } - // rules section - fprintf(f, "[RULES]\n"); + // id|protocol|action|enabled|procname|hosts|ports|proxyname + fputs("[RULES]\n", f); for (GList *l = prof->rules; l != NULL; l = l->next) { - RuleData *rule = (RuleData *)l->data; - // format: id|protocol|action|enabled|procname|hosts|ports|proxyname - char *pname = g_uri_escape_string(rule->proxy_name ? rule->proxy_name : "", NULL, FALSE); - fprintf(f, "%u|%d|%d|%d|%s|%s|%s|%s\n", - rule->id, - rule->protocol, - rule->action, - rule->enabled, - rule->process_name ? rule->process_name : "", - rule->target_hosts ? rule->target_hosts : "", - rule->target_ports ? rule->target_ports : "", - pname - ); - g_free(pname); + RuleData *r = (RuleData *)l->data; + fprintf(f, "%u|%d|%d|%d|%s|%s|%s|", r->id, r->protocol, r->action, r->enabled, + r->process_name ? r->process_name : "", r->target_hosts ? r->target_hosts : "", + r->target_ports ? r->target_ports : ""); + put_esc(f, r->proxy_name, '\n'); } } -// save config void save_config() { profile_sync_active(); char *dir = config_dir(); - char *path = config_path(); - struct stat st = {0}; + char *path = g_strdup_printf("%s/config.ini", dir); + struct stat st; if (stat(dir, &st) == -1) { if (g_mkdir_with_parents(dir, 0700) != 0) { perror("failed to create config dir"); - g_free(dir); g_free(path); - return; + goto out; } } else if (chmod(dir, 0700) != 0) { perror("chmod config dir failed"); @@ -203,40 +200,26 @@ void save_config() { int cfg_fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (cfg_fd < 0) { - printf("failed to save config to %s\n", path); - g_free(dir); g_free(path); - return; + fprintf(stderr, "failed to save config to %s\n", path); + goto out; } // mode on open only hits create; tighten existing 0644 installs too - if (fchmod(cfg_fd, 0600) != 0) { - perror("chmod config file failed"); - } + if (fchmod(cfg_fd, 0600) != 0) perror("chmod config file failed"); FILE *f = fdopen(cfg_fd, "w"); if (!f) { close(cfg_fd); - printf("failed to save config to %s\n", path); - g_free(dir); g_free(path); - return; + fprintf(stderr, "failed to save config to %s\n", path); + goto out; } - // settings section, global - char *act_esc = g_uri_escape_string(g_active_profile ? g_active_profile : "", NULL, FALSE); - fprintf(f, "[SETTINGS]\n"); - fprintf(f, "activeProfile=%s\n", act_esc); - g_free(act_esc); - fprintf(f, "logging=%d\n", g_chk_logging); - fprintf(f, "dns=%d\n", g_chk_dns); - + fputs("[SETTINGS]\nactiveProfile=", f); + put_esc(f, g_active_profile, '\n'); + fprintf(f, "logging=%d\ndns=%d\n", g_chk_logging, g_chk_dns); for (GList *l = g_profiles; l != NULL; l = l->next) write_profile(f, (Profile *)l->data); - fclose(f); - g_free(dir); g_free(path); -} - -// decode a percent-encoded field, never returns NULL -static char *unescape_field(const char *src) { - char *out = g_uri_unescape_string(src ? src : "", NULL); - return out ? out : g_strdup(""); +out: + g_free(dir); + g_free(path); } static ProxyType proxy_type_from_int(int v) { @@ -244,43 +227,89 @@ static ProxyType proxy_type_from_int(int v) { } static ProxyData *find_in_list(GList *list, const char *name) { - for (GList *l = list; l != NULL; l = l->next) { - ProxyData *p = (ProxyData *)l->data; - if (p->name && strcmp(p->name, name) == 0) return p; - } + for (GList *l = list; l != NULL; l = l->next) + if (strcmp(((ProxyData *)l->data)->name, name) == 0) return (ProxyData *)l->data; return NULL; } -// load settings from file. old files (no profiles) end up in a profile called "Default" +static ProxyData *parse_proxy_line(char *line) { + char *fields[6] = {NULL}; + int n = 0; + char *tok; + while (n < 6 && (tok = strsep(&line, "|")) != NULL) fields[n++] = tok; + if (n < 4) return NULL; + + int port = atoi(fields[3]); + ProxyData *px = g_malloc0(sizeof(ProxyData)); + px->name = unesc(fields[0]); + px->type = proxy_type_from_int(atoi(fields[1])); + px->host = unesc(fields[2]); + px->port = (port >= 1 && port <= 65535) ? (uint16_t)port : 0; + px->username = unesc(n > 4 ? fields[4] : ""); + px->password = unesc(n > 5 ? fields[5] : ""); + return px; +} + +static RuleData *parse_rule_line(char *line) { + RuleData *rule = g_malloc0(sizeof(RuleData)); + char *tok; + int idx = 0; + while ((tok = strsep(&line, "|")) != NULL) { + switch (idx++) { + case 1: rule->protocol = atoi(tok); break; + case 2: rule->action = atoi(tok); break; + case 3: rule->enabled = atoi(tok); break; + case 4: rule->process_name = g_strdup(tok); break; + case 5: rule->target_hosts = g_strdup(tok); break; + case 6: rule->target_ports = g_strdup(tok); break; + case 7: rule->proxy_name = unesc(tok); break; + } + } + // old files can hold garbage, ids belong to the library and are assigned when the rule is added + if (idx < 4 || rule->protocol > RULE_PROTOCOL_BOTH || rule->action > RULE_ACTION_BLOCK) { + free_rule_data(rule); + return NULL; + } + if (!rule->process_name) rule->process_name = g_strdup(""); + if (!rule->target_hosts) rule->target_hosts = g_strdup(""); + if (!rule->target_ports) rule->target_ports = g_strdup(""); + return rule; +} + +// files without profile blocks (old format) end up in a profile called "Default" void load_config() { - char *path = config_path(); - chmod(path, 0600); // old installs might still be world readable + char *dir = config_dir(); + char *path = g_strdup_printf("%s/config.ini", dir); + chmod(path, 0600); FILE *f = fopen(path, "r"); + g_free(dir); g_free(path); char line[8192]; - int section = 0; // 0=none, 1=settings, 2=rules, 3=proxies, 4=profile header + int section = 0; // 1=settings, 2=rules, 3=proxies, 4=profile header Profile *cur = NULL; char *saved_active = NULL; - // old single proxy config (pre multi-proxy), migrated to a proxy called "default" + // pre multi-proxy config, migrated to a proxy called "default" char old_ip[256] = "", old_user[256] = "", old_pass[256] = ""; int old_port = 0, old_type = PROXY_TYPE_SOCKS5; while (f && fgets(line, sizeof(line), f)) { - // trim newline line[strcspn(line, "\r\n")] = 0; - - if (strlen(line) == 0 || line[0] == '#') continue; + if (!line[0] || line[0] == '#') continue; if (strcmp(line, "[SETTINGS]") == 0) { section = 1; continue; } - if (strcmp(line, "[RULES]") == 0) { section = 2; if (!cur) cur = profile_new_empty("Default"); continue; } - if (strcmp(line, "[PROXIES]") == 0) { section = 3; if (!cur) cur = profile_new_empty("Default"); continue; } - if (strncmp(line, "[PROFILE:", 9) == 0 && line[strlen(line) - 1] == ']') { - line[strlen(line) - 1] = 0; - char *pn = unescape_field(line + 9); - cur = (pn[0] != '\0') ? profile_find(pn) : NULL; - if (!cur && pn[0] != '\0') cur = profile_new_empty(pn); + if (strcmp(line, "[RULES]") == 0 || strcmp(line, "[PROXIES]") == 0) { + section = line[1] == 'R' ? 2 : 3; + if (!cur) cur = profile_new_empty("Default"); + continue; + } + size_t len = strlen(line); + if (strncmp(line, "[PROFILE:", 9) == 0 && line[len - 1] == ']') { + line[len - 1] = 0; + char *pn = unesc(line + 9); + cur = pn[0] ? profile_find(pn) : NULL; + if (!cur && pn[0]) cur = profile_new_empty(pn); g_free(pn); section = cur ? 4 : 0; continue; @@ -289,87 +318,37 @@ void load_config() { if (section == 1 || section == 4) { char *val = strchr(line, '='); if (!val) continue; - *val = 0; val++; - - if (section == 1 && strcmp(line, "ip") == 0) g_strlcpy(old_ip, val, sizeof(old_ip)); - else if (section == 1 && strcmp(line, "port") == 0) old_port = atoi(val); - else if (section == 1 && strcmp(line, "type") == 0) old_type = atoi(val); - else if (section == 1 && strcmp(line, "user") == 0) g_strlcpy(old_user, val, sizeof(old_user)); - else if (section == 1 && strcmp(line, "pass") == 0) g_strlcpy(old_pass, val, sizeof(old_pass)); - else if (strcmp(line, "defaultProxy") == 0) { - // profile blocks own it, old files kept it in [SETTINGS] + *val++ = 0; + + if (strcmp(line, "defaultProxy") == 0) { if (!cur) cur = profile_new_empty("Default"); g_free(cur->default_proxy); - cur->default_proxy = unescape_field(val); - if (cur->default_proxy[0] == '\0') { g_free(cur->default_proxy); cur->default_proxy = NULL; } - } - else if (section == 1 && strcmp(line, "activeProfile") == 0) { - g_free(saved_active); - saved_active = unescape_field(val); + cur->default_proxy = unesc(val); + if (!cur->default_proxy[0]) { g_free(cur->default_proxy); cur->default_proxy = NULL; } } - else if (section == 1 && strcmp(line, "logging") == 0) g_chk_logging = atoi(val); - else if (section == 1 && strcmp(line, "dns") == 0) g_chk_dns = atoi(val); + else if (section != 1) continue; + else if (strcmp(line, "ip") == 0) g_strlcpy(old_ip, val, sizeof(old_ip)); + else if (strcmp(line, "port") == 0) old_port = atoi(val); + else if (strcmp(line, "type") == 0) old_type = atoi(val); + else if (strcmp(line, "user") == 0) g_strlcpy(old_user, val, sizeof(old_user)); + else if (strcmp(line, "pass") == 0) g_strlcpy(old_pass, val, sizeof(old_pass)); + else if (strcmp(line, "activeProfile") == 0) { g_free(saved_active); saved_active = unesc(val); } + else if (strcmp(line, "logging") == 0) g_chk_logging = atoi(val); + else if (strcmp(line, "dns") == 0) g_chk_dns = atoi(val); } else if (section == 3 && cur) { - // parse proxy line: name|type|host|port|user|pass - char *fields[6] = {NULL}; - char *p = line; - int idx = 0; - char *token; - while (idx < 6 && (token = strsep(&p, "|")) != NULL) fields[idx++] = token; - if (idx < 4) continue; - - int port = atoi(fields[3]); - ProxyData *px = g_malloc0(sizeof(ProxyData)); - px->name = unescape_field(fields[0]); - px->type = proxy_type_from_int(atoi(fields[1])); - px->host = unescape_field(fields[2]); - px->port = (port >= 1 && port <= 65535) ? (uint16_t)port : 0; - px->username = unescape_field(idx > 4 ? fields[4] : ""); - px->password = unescape_field(idx > 5 ? fields[5] : ""); - if (px->name[0] == '\0' || find_in_list(cur->proxies, px->name)) { - free_proxy_data(px); - continue; - } - cur->proxies = g_list_append(cur->proxies, px); + ProxyData *px = parse_proxy_line(line); + if (px && (!px->name[0] || find_in_list(cur->proxies, px->name))) { free_proxy_data(px); px = NULL; } + if (px) cur->proxies = g_list_append(cur->proxies, px); } else if (section == 2 && cur) { - // parse rule line - RuleData *rule = g_malloc0(sizeof(RuleData)); - char *p = line; - char *token; - int idx = 0; - - while ((token = strsep(&p, "|")) != NULL) { - switch(idx) { - case 0: rule->id = atoi(token); break; - case 1: rule->protocol = atoi(token); break; - case 2: rule->action = atoi(token); break; - case 3: rule->enabled = atoi(token); break; - case 4: rule->process_name = g_strdup(token); break; - case 5: rule->target_hosts = g_strdup(token); break; - case 6: rule->target_ports = g_strdup(token); break; - case 7: rule->proxy_name = unescape_field(token); break; - } - idx++; - } - - // old files can hold garbage, keep enums in range - if (idx >= 4 && rule->protocol <= RULE_PROTOCOL_BOTH && rule->action <= RULE_ACTION_BLOCK) { - if (!rule->process_name) rule->process_name = g_strdup(""); - if (!rule->target_hosts) rule->target_hosts = g_strdup(""); - if (!rule->target_ports) rule->target_ports = g_strdup(""); - rule->id = 0; // ids belong to the library, assigned when the rule is added - cur->rules = g_list_append(cur->rules, rule); - } else { - free_rule_data(rule); - } + RuleData *rule = parse_rule_line(line); + if (rule) cur->rules = g_list_append(cur->rules, rule); } } if (f) fclose(f); - // migrate old single proxy config into the "Default" profile - if (old_ip[0] != '\0' && old_port >= 1 && old_port <= 65535) { + if (old_ip[0] && old_port >= 1 && old_port <= 65535) { Profile *d = profile_find("Default"); if (!d) d = profile_new_empty("Default"); if (!d->proxies) { @@ -386,10 +365,8 @@ void load_config() { } } - // always at least one profile if (!g_profiles) profile_new_empty("Default"); - // default points to nothing -> first proxy for (GList *l = g_profiles; l != NULL; l = l->next) { Profile *p = (Profile *)l->data; if (p->proxies && (!p->default_proxy || !find_in_list(p->proxies, p->default_proxy))) { diff --git a/Linux/gui/gui_json.c b/Linux/gui/gui_json.c new file mode 100644 index 00000000..7742949e --- /dev/null +++ b/Linux/gui/gui_json.c @@ -0,0 +1,140 @@ +#include "gui.h" + +static char *escape_json_string(const char *src) { + GString *str = g_string_new(""); + for (const char *p = src ? src : ""; *p; p++) { + switch (*p) { + case '\\': g_string_append(str, "\\\\"); break; + case '"': g_string_append(str, "\\\""); break; + case '\n': g_string_append(str, "\\n"); break; + case '\r': g_string_append(str, "\\r"); break; + case '\t': g_string_append(str, "\\t"); break; + default: + if ((unsigned char)*p < 0x20) g_string_append_printf(str, "\\u%04x", (unsigned char)*p); + else g_string_append_c(str, *p); + } + } + return g_string_free(str, FALSE); +} + +// returns NULL if the key is missing, its value is not a string, or the string is unterminated +char *extract_sub_json_str(const char *json, const char *key) { + char *needle = g_strdup_printf("\"%s\"", key); + const char *k = strstr(json, needle); + const char *v = k ? strchr(k + strlen(needle), ':') : NULL; + g_free(needle); + if (!v) return NULL; + v++; + while (*v == ' ' || *v == '\t' || *v == '\n' || *v == '\r') v++; + if (*v != '"') return NULL; + v++; + + GString *out = g_string_new(""); + while (*v && *v != '"') { + if (*v == '\\' && v[1]) { + v++; + g_string_append_c(out, *v == 'n' ? '\n' : *v == 'r' ? '\r' : *v == 't' ? '\t' : *v); + } else { + g_string_append_c(out, *v); + } + v++; + } + if (*v != '"') { g_string_free(out, TRUE); return NULL; } + return g_string_free(out, FALSE); +} + +static bool json_has_key(const char *json, const char *key) { + char *needle = g_strdup_printf("\"%s\"", key); + bool found = strstr(json, needle) != NULL; + g_free(needle); + return found; +} + +static bool json_bool(const char *json, const char *key, bool def) { + if (!json_has_key(json, key)) return def; + char *needle = g_strdup_printf("\"%s\"", key); + const char *colon = strchr(strstr(json, needle) + strlen(needle), ':'); + g_free(needle); + if (!colon) return def; + colon++; + while (*colon == ' ' || *colon == '\t' || *colon == '\n' || *colon == '\r') colon++; + return strncmp(colon, "true", 4) == 0; +} + +// '}' closing the object that starts at p, skipping braces inside strings +static const char *object_end(const char *p) { + bool in_str = false; + for (p++; *p; p++) { + if (in_str) { + if (*p == '\\' && p[1]) p++; + else if (*p == '"') in_str = false; + } else if (*p == '"') { + in_str = true; + } else if (*p == '}') { + return p; + } + } + return NULL; +} + +static char *first_str(const char *obj, const char *key, const char *alt) { + char *s = extract_sub_json_str(obj, key); + return s ? s : extract_sub_json_str(obj, alt); +} + +static bool name_is(const char *s, const char *want) { + return s && g_ascii_strcasecmp(s, want) == 0; +} + +// accepts our keys and the macOS ones. unknown action strings (macOS stores a proxy id) mean PROXY +int rules_from_json(const char *json, RuleImportCb cb, void *ud) { + int count = 0; + for (const char *cur = json; (cur = strchr(cur, '{')) != NULL; ) { + const char *end = object_end(cur); + if (!end) break; + char *obj = g_strndup(cur, end - cur + 1); + cur = end + 1; + + RuleData r = {0}; + r.process_name = extract_sub_json_str(obj, "processNames"); + r.target_hosts = extract_sub_json_str(obj, "targetHosts"); + r.target_ports = extract_sub_json_str(obj, "targetPorts"); + char *proto = first_str(obj, "protocol", "ruleProtocol"); + char *act = first_str(obj, "action", "ruleAction"); + r.proxy_name = extract_sub_json_str(obj, "proxy"); + r.enabled = json_bool(obj, "enabled", true); + if (!r.target_hosts) r.target_hosts = g_strdup("*"); + if (!r.target_ports) r.target_ports = g_strdup("*"); + r.protocol = name_is(proto, "TCP") ? RULE_PROTOCOL_TCP : name_is(proto, "UDP") ? RULE_PROTOCOL_UDP : RULE_PROTOCOL_BOTH; + r.action = name_is(act, "DIRECT") ? RULE_ACTION_DIRECT : name_is(act, "BLOCK") ? RULE_ACTION_BLOCK : RULE_ACTION_PROXY; + + cb(&r, ++count, ud); + + g_free(r.process_name); g_free(r.target_hosts); g_free(r.target_ports); g_free(r.proxy_name); + g_free(proto); g_free(act); g_free(obj); + } + return count; +} + +char *rules_to_json(GList *rules, bool only_selected) { + GString *out = g_string_new("[\n"); + bool first = true; + for (GList *l = rules; l != NULL; l = l->next) { + RuleData *r = (RuleData *)l->data; + if (only_selected && !r->selected) continue; + char *proc = escape_json_string(r->process_name); + char *host = escape_json_string(r->target_hosts); + char *port = escape_json_string(r->target_ports); + char *pxy = escape_json_string(r->action == RULE_ACTION_PROXY ? r->proxy_name : NULL); + const char *proto = r->protocol == RULE_PROTOCOL_TCP ? "TCP" : r->protocol == RULE_PROTOCOL_UDP ? "UDP" : "BOTH"; + const char *act = r->action == RULE_ACTION_PROXY ? "PROXY" : r->action == RULE_ACTION_DIRECT ? "DIRECT" : "BLOCK"; + g_string_append_printf(out, "%s {\n \"processNames\": \"%s\",\n \"targetHosts\": \"%s\",\n \"targetPorts\": \"%s\",\n" + " \"protocol\": \"%s\",\n \"ruleProtocol\": \"%s\",\n \"action\": \"%s\",\n \"ruleAction\": \"%s\",\n" + " \"proxy\": \"%s\",\n \"enabled\": %s\n }", + first ? "" : ",\n", proc, host, port, proto, proto, act, act, pxy, r->enabled ? "true" : "false"); + g_free(proc); g_free(host); g_free(port); g_free(pxy); + first = false; + } + g_string_append(out, "\n]\n"); + return g_string_free(out, FALSE); +} diff --git a/Linux/gui/gui_logs.c b/Linux/gui/gui_logs.c index 8b7520f9..b4d80552 100644 --- a/Linux/gui/gui_logs.c +++ b/Linux/gui/gui_logs.c @@ -1,15 +1,16 @@ #include "gui.h" -// filter logs based on search +#define MAX_LINES 100 + static void filter_text_view(GtkTextBuffer *buffer, const char *text) { if (!buffer) return; GtkTextIter start, end; gtk_text_buffer_get_bounds(buffer, &start, &end); gtk_text_buffer_remove_tag_by_name(buffer, "hidden", &start, &end); + if (!text || !*text) return; - if (!text || strlen(text) == 0) return; - + char *lower_search = g_utf8_strdown(text, -1); GtkTextIter line_start = start; while (!gtk_text_iter_is_end(&line_start)) { GtkTextIter line_end = line_start; @@ -17,33 +18,25 @@ static void filter_text_view(GtkTextBuffer *buffer, const char *text) { gtk_text_iter_forward_to_line_end(&line_end); char *line_text = gtk_text_buffer_get_text(buffer, &line_start, &line_end, FALSE); - - // search case insensitive char *lower_line = g_utf8_strdown(line_text, -1); - char *lower_search = g_utf8_strdown(text, -1); - if (!strstr(lower_line, lower_search)) { - GtkTextIter next_line = line_end; - gtk_text_iter_forward_char(&next_line); // include newline - gtk_text_buffer_apply_tag_by_name(buffer, "hidden", &line_start, &next_line); + GtkTextIter next_line = line_end; + gtk_text_iter_forward_char(&next_line); + gtk_text_buffer_apply_tag_by_name(buffer, "hidden", &line_start, &next_line); } - g_free(lower_line); - g_free(lower_search); g_free(line_text); - gtk_text_iter_forward_line(&line_start); } + g_free(lower_search); } void on_search_conn_changed(GtkSearchEntry *entry, gpointer user_data) { - const char *text = gtk_entry_get_text(GTK_ENTRY(entry)); - filter_text_view(conn_buffer, text); + filter_text_view(conn_buffer, gtk_entry_get_text(GTK_ENTRY(entry))); } void on_search_log_changed(GtkSearchEntry *entry, gpointer user_data) { - const char *text = gtk_entry_get_text(GTK_ENTRY(entry)); - filter_text_view(log_buffer, text); + filter_text_view(log_buffer, gtk_entry_get_text(GTK_ENTRY(entry))); } void on_clear_conn_clicked(GtkButton *button, gpointer user_data) { @@ -54,87 +47,52 @@ void on_clear_log_clicked(GtkButton *button, gpointer user_data) { if (log_buffer) gtk_text_buffer_set_text(log_buffer, "", 0); } -static void free_connection_data(ConnectionData *data) { - if (data) { - g_free(data->process_name); - g_free(data->dest_ip); - g_free(data->proxy_info); - g_free(data->timestamp); - g_free(data); +// library strings are not guaranteed utf8 +static void append_line(GtkTextBuffer *buffer, char *line) { + if (buffer) { + if (!g_utf8_validate(line, -1, NULL)) { + char *fixed = g_utf8_make_valid(line, -1); + g_free(line); + line = fixed; + } + GtkTextIter end; + gtk_text_buffer_get_end_iter(buffer, &end); + gtk_text_buffer_insert(buffer, &end, line, -1); + trim_buffer_lines(buffer, MAX_LINES); } + g_free(line); } -// update logs from main thread static gboolean update_log_gui(gpointer user_data) { - LogData *data = (LogData *)user_data; - if (!data) return FALSE; - - if (log_buffer) { - GtkTextIter end; - gtk_text_buffer_get_end_iter(log_buffer, &end); - - char *time_str = get_current_time_str(); - char *full_msg = g_strdup_printf("%s %s\n", time_str, data->message); - g_free(time_str); - - // library strings are not guaranteed utf8 - if (!g_utf8_validate(full_msg, -1, NULL)) { - char *fixed = g_utf8_make_valid(full_msg, -1); - g_free(full_msg); - full_msg = fixed; - } - gtk_text_buffer_insert(log_buffer, &end, full_msg, -1); - g_free(full_msg); - - trim_buffer_lines(log_buffer, 100); - } - - g_free(data->message); - g_free(data); - return FALSE; // done + char *time_str = get_current_time_str(); + append_line(log_buffer, g_strdup_printf("%s %s\n", time_str, (char *)user_data)); + g_free(time_str); + g_free(user_data); + return FALSE; } -// update conn info static gboolean update_connection_gui_append(gpointer user_data) { - ConnectionData *data = (ConnectionData *)user_data; - if (!data) return FALSE; - - if (conn_buffer) { - GtkTextIter end; - gtk_text_buffer_get_end_iter(conn_buffer, &end); - - char *line_buffer = g_strdup_printf("%s %s (PID:%u) -> %s:%u via %s\n", - data->timestamp, data->process_name, data->pid, data->dest_ip, data->dest_port, data->proxy_info); - if (!g_utf8_validate(line_buffer, -1, NULL)) { - char *fixed = g_utf8_make_valid(line_buffer, -1); - g_free(line_buffer); - line_buffer = fixed; - } - - gtk_text_buffer_insert(conn_buffer, &end, line_buffer, -1); - g_free(line_buffer); - - trim_buffer_lines(conn_buffer, 100); - } - - free_connection_data(data); + ConnectionData *d = (ConnectionData *)user_data; + append_line(conn_buffer, g_strdup_printf("%s %s (PID:%u) -> %s:%u via %s\n", + d->timestamp, d->process_name, d->pid, d->dest_ip, d->dest_port, d->proxy_info)); + g_free(d->process_name); + g_free(d->dest_ip); + g_free(d->proxy_info); + g_free(d->timestamp); + g_free(d); return FALSE; } void lib_log_callback(const char *message) { - LogData *data = g_malloc0(sizeof(LogData)); - data->message = g_strdup(message ? message : ""); - g_idle_add(update_log_gui, data); + g_idle_add(update_log_gui, g_strdup(message ? message : "")); } -// log a message from the gui itself, same path as library logs void gui_log(const char *format, ...) { va_list args; va_start(args, format); char *msg = g_strdup_vprintf(format, args); va_end(args); - lib_log_callback(msg); - g_free(msg); + g_idle_add(update_log_gui, msg); } void lib_connection_callback(const char *process_name, uint32_t pid, const char *dest_ip, uint16_t dest_port, const char *proxy_info) { diff --git a/Linux/gui/gui_rules.c b/Linux/gui/gui_rules.c index 685d50a2..435e9f47 100644 --- a/Linux/gui/gui_rules.c +++ b/Linux/gui/gui_rules.c @@ -4,26 +4,26 @@ static GtkWidget *rules_window = NULL; static GtkWidget *rules_list_box = NULL; static GtkWidget *btn_select_all_header = NULL; -// forward static void refresh_rules_ui(); void free_rule_data(RuleData *rule) { - if (rule) { - g_free(rule->process_name); - g_free(rule->target_hosts); - g_free(rule->target_ports); - g_free(rule->proxy_name); - g_free(rule); - } + if (!rule) return; + g_free(rule->process_name); + g_free(rule->target_hosts); + g_free(rule->target_ports); + g_free(rule->proxy_name); + g_free(rule); +} + +static GtkWindow *parent_window() { + return GTK_WINDOW(rules_window ? rules_window : window); } -// position of the rule in the library list. rules the library rejected (id 0) are not in it +// position in the library list, rules it rejected (id 0) are not in it static int lib_index_of(RuleData *rule) { int idx = 0; - for (GList *l = g_rules_list; l != NULL; l = l->next) { - if (l->data == rule) break; + for (GList *l = g_rules_list; l != NULL && l->data != rule; l = l->next) if (((RuleData *)l->data)->id != 0) idx++; - } return idx; } @@ -31,20 +31,15 @@ static uint32_t rule_proxy_id(RuleAction action, const char *proxy_name) { return action == RULE_ACTION_PROXY ? resolve_proxy_id(proxy_name) : 0; } -// push the stored rules to the library, call after the proxies were added and before ProxyBridge_Start void apply_rules_to_lib() { - ProxyBridge_ClearRules(); int n = 1; for (GList *l = g_rules_list; l != NULL; l = l->next, n++) { RuleData *r = (RuleData *)l->data; char err[256] = ""; r->id = ProxyBridge_AddRule(r->process_name, r->target_hosts, r->target_ports, r->protocol, r->action, rule_proxy_id(r->action, r->proxy_name), err, sizeof(err)); - if (r->id == 0) { - gui_log("Rule #%d (%s) not loaded: %s", n, r->process_name, err[0] ? err : "rejected"); - continue; - } - if (!r->enabled) ProxyBridge_DisableRule(r->id); + if (r->id == 0) gui_log("Rule #%d (%s) not loaded: %s", n, r->process_name, err[0] ? err : "rejected"); + else if (!r->enabled) ProxyBridge_DisableRule(r->id); } } @@ -69,14 +64,12 @@ static void on_rule_toggle(GtkToggleButton *btn, gpointer data) { static void on_rule_move(RuleData *rule, int dir) { GList *link = g_list_find(g_rules_list, rule); - if (!link) return; - GList *other = dir < 0 ? link->prev : link->next; + GList *other = link ? (dir < 0 ? link->prev : link->next) : NULL; if (!other) return; - // swap the data pointers, keeps the list head valid - gpointer tmp = link->data; + // swap payloads so the list head stays valid link->data = other->data; - other->data = tmp; + other->data = rule; if (rule->id != 0 && !ProxyBridge_MoveRule(rule->id, lib_index_of(rule))) gui_log("Could not reorder rule %u in the engine", rule->id); @@ -88,18 +81,13 @@ static void on_rule_move_up(GtkWidget *widget, gpointer data) { on_rule_move((Ru static void on_rule_move_down(GtkWidget *widget, gpointer data) { on_rule_move((RuleData *)data, 1); } static void on_browse_clicked(GtkWidget *widget, gpointer data) { - GtkWidget *entry = (GtkWidget *)data; - GtkWidget *dialog = gtk_file_chooser_dialog_new("Select Application", - NULL, - GTK_FILE_CHOOSER_ACTION_OPEN, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Select", GTK_RESPONSE_ACCEPT, - NULL); + GtkWidget *dialog = gtk_file_chooser_dialog_new("Select Application", NULL, GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", GTK_RESPONSE_CANCEL, "_Select", GTK_RESPONSE_ACCEPT, NULL); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); if (filename) { char *base = g_path_get_basename(filename); - gtk_entry_set_text(GTK_ENTRY(entry), base); + gtk_entry_set_text(GTK_ENTRY(data), base); g_free(base); g_free(filename); } @@ -107,187 +95,116 @@ static void on_browse_clicked(GtkWidget *widget, gpointer data) { gtk_widget_destroy(dialog); } -// proxy combo only matters for PROXY rules static void on_action_changed(GtkComboBox *combo, gpointer data) { gtk_widget_set_sensitive(GTK_WIDGET(data), gtk_combo_box_get_active(combo) == RULE_ACTION_PROXY); } +static void set_str(char **dst, const char *src) { + g_free(*dst); + *dst = g_strdup(src); +} + +// returns the saved rule, NULL while the engine rejects it (err filled) +static RuleData *save_rule(RuleData *rule, const char *proc, const char *hosts, const char *ports, + RuleProtocol proto, RuleAction action, const char *proxy_name, char *err, size_t err_size) { + if (!ProxyBridge_ValidateRule(proc, hosts, ports, err, err_size)) return NULL; + uint32_t pid = rule_proxy_id(action, proxy_name); + + if (rule && rule->id != 0) { + if (!ProxyBridge_EditRule(rule->id, proc, hosts, ports, proto, action, pid, err, err_size)) return NULL; + } else { + // new rule, or one the engine rejected at load time + uint32_t id = ProxyBridge_AddRule(proc, hosts, ports, proto, action, pid, err, err_size); + if (id == 0) return NULL; + if (rule && !rule->enabled) ProxyBridge_DisableRule(id); + if (!rule) { + rule = g_malloc0(sizeof(RuleData)); + rule->enabled = true; + g_rules_list = g_list_append(g_rules_list, rule); + } + rule->id = id; + ProxyBridge_MoveRule(id, lib_index_of(rule)); + } + set_str(&rule->process_name, proc); + set_str(&rule->target_hosts, hosts); + set_str(&rule->target_ports, ports); + set_str(&rule->proxy_name, proxy_name); + rule->protocol = proto; + rule->action = action; + return rule; +} + static void open_rule_dialog(RuleData *rule) { - GtkWidget *dialog = gtk_dialog_new(); - gtk_window_set_title(GTK_WINDOW(dialog), rule ? "Edit Rule" : "Add Rule"); - gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(rules_window ? rules_window : window)); - gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); + GtkWidget *dialog = gtk_dialog_new_with_buttons(rule ? "Edit Rule" : "Add Rule", parent_window(), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + "Cancel", GTK_RESPONSE_CANCEL, "Save", GTK_RESPONSE_ACCEPT, NULL); gtk_window_set_default_size(GTK_WINDOW(dialog), 500, 400); - - GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); GtkWidget *grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 8); gtk_grid_set_column_spacing(GTK_GRID(grid), 10); gtk_container_set_border_width(GTK_CONTAINER(grid), 15); - // proc input GtkWidget *proc_entry = gtk_entry_new(); GtkWidget *browse_btn = gtk_button_new_with_label("Browse..."); g_signal_connect(browse_btn, "clicked", G_CALLBACK(on_browse_clicked), proc_entry); GtkWidget *proc_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); gtk_box_pack_start(GTK_BOX(proc_box), proc_entry, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(proc_box), browse_btn, FALSE, FALSE, 0); - gtk_widget_set_hexpand(proc_box, TRUE); GtkWidget *host_entry = gtk_entry_new(); GtkWidget *port_entry = gtk_entry_new(); + GtkWidget *proto_combo = combo_new("TCP", "UDP", "BOTH", NULL); + GtkWidget *action_combo = combo_new("PROXY", "DIRECT", "BLOCK", NULL); - // protocol list - GtkWidget *proto_combo = gtk_combo_box_text_new(); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proto_combo), "TCP"); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proto_combo), "UDP"); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proto_combo), "BOTH"); - - // action list - GtkWidget *action_combo = gtk_combo_box_text_new(); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(action_combo), "PROXY"); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(action_combo), "DIRECT"); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(action_combo), "BLOCK"); - - // proxy list: entry 0 = default proxy, then one per configured proxy (same order as g_proxies_list) - GtkWidget *proxy_combo = gtk_combo_box_text_new(); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proxy_combo), "Default proxy"); + // entry 0 = default proxy, then one per proxy in g_proxies_list order + GtkWidget *proxy_combo = combo_new("Default proxy", NULL); int proxy_sel = 0, pi = 1; for (GList *l = g_proxies_list; l != NULL; l = l->next, pi++) { ProxyData *p = (ProxyData *)l->data; gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proxy_combo), p->name); - if (rule && rule->proxy_name && p->name && strcmp(rule->proxy_name, p->name) == 0) proxy_sel = pi; + if (rule && rule->proxy_name && strcmp(rule->proxy_name, p->name) == 0) proxy_sel = pi; } gtk_combo_box_set_active(GTK_COMBO_BOX(proxy_combo), proxy_sel); g_signal_connect(action_combo, "changed", G_CALLBACK(on_action_changed), proxy_combo); - if (rule) { - gtk_entry_set_text(GTK_ENTRY(proc_entry), rule->process_name); - gtk_entry_set_text(GTK_ENTRY(host_entry), rule->target_hosts); - gtk_entry_set_text(GTK_ENTRY(port_entry), rule->target_ports); - gtk_combo_box_set_active(GTK_COMBO_BOX(proto_combo), rule->protocol); - gtk_combo_box_set_active(GTK_COMBO_BOX(action_combo), rule->action); - } else { - gtk_entry_set_text(GTK_ENTRY(port_entry), "*"); - gtk_entry_set_text(GTK_ENTRY(host_entry), "*"); - gtk_combo_box_set_active(GTK_COMBO_BOX(proto_combo), 2); // BOTH default - gtk_combo_box_set_active(GTK_COMBO_BOX(action_combo), 0); // PROXY default - } - - int row = 0; - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Process Name:"), 0, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), proc_box, 1, row, 1, 1); row++; - - GtkWidget *proc_hint = gtk_label_new("Example: firefox; chrome; /usr/bin/wget"); - gtk_style_context_add_class(gtk_widget_get_style_context(proc_hint), "dim-label"); - gtk_widget_set_halign(proc_hint, GTK_ALIGN_START); - gtk_grid_attach(GTK_GRID(grid), proc_hint, 1, row, 1, 1); row++; - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target Host:"), 0, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), host_entry, 1, row, 1, 1); row++; - - GtkWidget *host_hint = gtk_label_new("Example: 192.168.1.*; 10.0.0.0/8; 10.0.0.1-10.0.0.50; github.com; *.github.com; * (domains are matched via DNS answers)"); - gtk_label_set_line_wrap(GTK_LABEL(host_hint), TRUE); - gtk_label_set_max_width_chars(GTK_LABEL(host_hint), 60); - gtk_style_context_add_class(gtk_widget_get_style_context(host_hint), "dim-label"); - gtk_widget_set_halign(host_hint, GTK_ALIGN_START); - gtk_grid_attach(GTK_GRID(grid), host_hint, 1, row, 1, 1); row++; - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target Port:"), 0, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), port_entry, 1, row, 1, 1); row++; - - GtkWidget *port_hint = gtk_label_new("Example: 80; 443; 8000-8080; *"); - gtk_style_context_add_class(gtk_widget_get_style_context(port_hint), "dim-label"); - gtk_widget_set_halign(port_hint, GTK_ALIGN_START); - gtk_grid_attach(GTK_GRID(grid), port_hint, 1, row, 1, 1); row++; - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Protocol:"), 0, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), proto_combo, 1, row, 1, 1); row++; - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Action:"), 0, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), action_combo, 1, row, 1, 1); row++; - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Proxy:"), 0, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), proxy_combo, 1, row, 1, 1); row++; - - // inline validation error - GtkWidget *err_label = gtk_label_new(""); - gtk_widget_set_halign(err_label, GTK_ALIGN_START); - gtk_label_set_xalign(GTK_LABEL(err_label), 0); - gtk_label_set_line_wrap(GTK_LABEL(err_label), TRUE); - gtk_label_set_max_width_chars(GTK_LABEL(err_label), 60); - gtk_grid_attach(GTK_GRID(grid), err_label, 0, row, 2, 1); row++; - - gtk_container_add(GTK_CONTAINER(content), grid); - - gtk_dialog_add_button(GTK_DIALOG(dialog), "Cancel", GTK_RESPONSE_CANCEL); - gtk_dialog_add_button(GTK_DIALOG(dialog), "Save", GTK_RESPONSE_ACCEPT); + gtk_entry_set_text(GTK_ENTRY(proc_entry), rule ? rule->process_name : ""); + gtk_entry_set_text(GTK_ENTRY(host_entry), rule ? rule->target_hosts : "*"); + gtk_entry_set_text(GTK_ENTRY(port_entry), rule ? rule->target_ports : "*"); + gtk_combo_box_set_active(GTK_COMBO_BOX(proto_combo), rule ? (int)rule->protocol : RULE_PROTOCOL_BOTH); + gtk_combo_box_set_active(GTK_COMBO_BOX(action_combo), rule ? (int)rule->action : RULE_ACTION_PROXY); + + GtkWidget *err_label = hint_label(""); + grid_row(grid, 0, "Process Name:", proc_box); + gtk_grid_attach(GTK_GRID(grid), hint_label("Example: firefox; chrome; /usr/bin/wget"), 1, 1, 1, 1); + grid_row(grid, 2, "Target Host:", host_entry); + gtk_grid_attach(GTK_GRID(grid), hint_label("Example: 192.168.1.*; 10.0.0.0/8; 10.0.0.1-10.0.0.50; github.com; *.github.com; * (domains are matched via DNS answers)"), 1, 3, 1, 1); + grid_row(grid, 4, "Target Port:", port_entry); + gtk_grid_attach(GTK_GRID(grid), hint_label("Example: 80; 443; 8000-8080; *"), 1, 5, 1, 1); + grid_row(grid, 6, "Protocol:", proto_combo); + grid_row(grid, 7, "Action:", action_combo); + grid_row(grid, 8, "Proxy:", proxy_combo); + gtk_grid_attach(GTK_GRID(grid), err_label, 0, 9, 2, 1); + gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), grid); gtk_widget_show_all(dialog); on_action_changed(GTK_COMBO_BOX(action_combo), proxy_combo); - while (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { - const char *proc = gtk_entry_get_text(GTK_ENTRY(proc_entry)); - const char *hosts = gtk_entry_get_text(GTK_ENTRY(host_entry)); - const char *ports = gtk_entry_get_text(GTK_ENTRY(port_entry)); - int proto_i = gtk_combo_box_get_active(GTK_COMBO_BOX(proto_combo)); - int action_i = gtk_combo_box_get_active(GTK_COMBO_BOX(action_combo)); - if (proto_i < 0) proto_i = RULE_PROTOCOL_BOTH; - if (action_i < 0) action_i = RULE_ACTION_PROXY; - RuleProtocol proto = (RuleProtocol)proto_i; - RuleAction action = (RuleAction)action_i; - - // proxy name from the combo, NULL = default - const char *proxy_name = NULL; + bool saved = false; + while (!saved && gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + int proto = gtk_combo_box_get_active(GTK_COMBO_BOX(proto_combo)); + int action = gtk_combo_box_get_active(GTK_COMBO_BOX(action_combo)); int psel = gtk_combo_box_get_active(GTK_COMBO_BOX(proxy_combo)); - if (action == RULE_ACTION_PROXY && psel > 0) { - ProxyData *pd = (ProxyData *)g_list_nth_data(g_proxies_list, psel - 1); - if (pd) proxy_name = pd->name; - } - + ProxyData *pd = (action == RULE_ACTION_PROXY && psel > 0) ? g_list_nth_data(g_proxies_list, psel - 1) : NULL; char err[256] = ""; - bool ok = ProxyBridge_ValidateRule(proc, hosts, ports, err, sizeof(err)); - uint32_t pid = rule_proxy_id(action, proxy_name); - - if (ok) { - if (rule && rule->id != 0) { - ok = ProxyBridge_EditRule(rule->id, proc, hosts, ports, proto, action, pid, err, sizeof(err)); - } else { - // new rule, or one the engine rejected when it was loaded - uint32_t new_id = ProxyBridge_AddRule(proc, hosts, ports, proto, action, pid, err, sizeof(err)); - ok = (new_id != 0); - if (ok) { - if (rule && !rule->enabled) ProxyBridge_DisableRule(new_id); - if (rule) { - rule->id = new_id; - ProxyBridge_MoveRule(new_id, lib_index_of(rule)); - } else { - RuleData *new_rule = g_malloc0(sizeof(RuleData)); - new_rule->id = new_id; - new_rule->enabled = true; - g_rules_list = g_list_append(g_rules_list, new_rule); - rule = new_rule; - } - } - } - } - if (!ok) { - char *esc = g_markup_printf_escaped("%s", err[0] ? err : "Invalid rule."); - gtk_label_set_markup(GTK_LABEL(err_label), esc); - g_free(esc); - continue; + saved = save_rule(rule, gtk_entry_get_text(GTK_ENTRY(proc_entry)), gtk_entry_get_text(GTK_ENTRY(host_entry)), + gtk_entry_get_text(GTK_ENTRY(port_entry)), proto < 0 ? RULE_PROTOCOL_BOTH : (RuleProtocol)proto, + action < 0 ? RULE_ACTION_PROXY : (RuleAction)action, pd ? pd->name : NULL, err, sizeof(err)) != NULL; + if (saved) { + save_config(); + refresh_rules_ui(); + } else { + set_error_label(err_label, err[0] ? err : "Invalid rule."); } - - // update local copy - g_free(rule->process_name); rule->process_name = g_strdup(proc); - g_free(rule->target_hosts); rule->target_hosts = g_strdup(hosts); - g_free(rule->target_ports); rule->target_ports = g_strdup(ports); - g_free(rule->proxy_name); rule->proxy_name = proxy_name ? g_strdup(proxy_name) : NULL; - rule->protocol = proto; - rule->action = action; - save_config(); - refresh_rules_ui(); - break; } gtk_widget_destroy(dialog); } @@ -300,149 +217,104 @@ static void on_rule_add_clicked(GtkWidget *widget, gpointer data) { open_rule_dialog(NULL); } +static bool all_rules_selected() { + if (!g_rules_list) return false; + for (GList *l = g_rules_list; l != NULL; l = l->next) + if (!((RuleData *)l->data)->selected) return false; + return true; +} + +static void update_select_all_label() { + if (btn_select_all_header) + gtk_button_set_label(GTK_BUTTON(btn_select_all_header), all_rules_selected() ? "Deselect All" : "Select All"); +} + static void on_rule_select_toggle(GtkToggleButton *btn, gpointer data) { - RuleData *rule = (RuleData *)data; - rule->selected = gtk_toggle_button_get_active(btn); - if (btn_select_all_header) { - bool all_selected = (g_rules_list != NULL); - if (g_rules_list == NULL) all_selected = false; - for (GList *l = g_rules_list; l != NULL; l = l->next) { - RuleData *r = (RuleData *)l->data; - if (!r->selected) { - all_selected = false; - break; - } - } - gtk_button_set_label(GTK_BUTTON(btn_select_all_header), all_selected ? "Deselect All" : "Select All"); - } + ((RuleData *)data)->selected = gtk_toggle_button_get_active(btn); + update_select_all_label(); } static void on_rule_export_clicked(GtkWidget *widget, gpointer data) { - if (!g_rules_list) return; bool any_selected = false; - for (GList *l = g_rules_list; l != NULL; l = l->next) { - if (((RuleData *)l->data)->selected) { any_selected = true; break; } - } + for (GList *l = g_rules_list; l != NULL; l = l->next) any_selected |= ((RuleData *)l->data)->selected; if (!any_selected) { - show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_WARNING, "Please select at least one rule to export."); + show_message(parent_window(), GTK_MESSAGE_WARNING, "Please select at least one rule to export."); return; } - GtkWidget *dialog = gtk_file_chooser_dialog_new("Export Rules", GTK_WINDOW(rules_window ? rules_window : window), GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL); + GtkWidget *dialog = gtk_file_chooser_dialog_new("Export Rules", parent_window(), GTK_FILE_CHOOSER_ACTION_SAVE, + "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE); gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "proxy_rules.json"); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); - FILE *f = filename ? fopen(filename, "w") : NULL; - if (f) { - // both our keys and the macOS ones (ruleProtocol/ruleAction), plus the proxy name - fprintf(f, "[\n"); - bool first = true; - for (GList *l = g_rules_list; l != NULL; l = l->next) { - RuleData *r = (RuleData *)l->data; - if (!r->selected) continue; - if (!first) fprintf(f, ",\n"); - char *proc = escape_json_string(r->process_name); - char *host = escape_json_string(r->target_hosts); - char *port = escape_json_string(r->target_ports); - char *pxy = escape_json_string(r->action == RULE_ACTION_PROXY ? r->proxy_name : NULL); - const char *proto = (r->protocol == RULE_PROTOCOL_TCP) ? "TCP" : (r->protocol == RULE_PROTOCOL_UDP ? "UDP" : "BOTH"); - const char *act = (r->action == RULE_ACTION_PROXY) ? "PROXY" : (r->action == RULE_ACTION_DIRECT ? "DIRECT" : "BLOCK"); - fprintf(f, " {\n \"processNames\": \"%s\",\n \"targetHosts\": \"%s\",\n \"targetPorts\": \"%s\",\n \"protocol\": \"%s\",\n \"ruleProtocol\": \"%s\",\n \"action\": \"%s\",\n \"ruleAction\": \"%s\",\n \"proxy\": \"%s\",\n \"enabled\": %s\n }", proc, host, port, proto, proto, act, act, pxy, r->enabled ? "true" : "false"); - g_free(proc); g_free(host); g_free(port); g_free(pxy); - first = false; - } - fprintf(f, "\n]\n"); - if (fclose(f) == 0) show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_INFO, "Rules exported successfully."); - else show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_ERROR, "Failed to write %s", filename); - } else { - show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_ERROR, "Could not open %s for writing.", filename ? filename : "file"); - } + char *json = rules_to_json(g_rules_list, true); + GError *err = NULL; + if (filename && g_file_set_contents(filename, json, -1, &err)) + show_message(parent_window(), GTK_MESSAGE_INFO, "Rules exported successfully."); + else + show_message(parent_window(), GTK_MESSAGE_ERROR, "Could not write %s: %s", filename ? filename : "file", err ? err->message : "unknown error"); + if (err) g_error_free(err); + g_free(json); g_free(filename); } gtk_widget_destroy(dialog); } -// first non-NULL of two (consumes both) -static char *pick_str(char *a, char *b) { - if (a) { g_free(b); return a; } - return b; +typedef struct { + int imported, failed; + GString *fail_msg; +} ImportCtx; + +static void import_rule_cb(const RuleData *r, int index, void *ud) { + ImportCtx *ctx = (ImportCtx *)ud; + char err[256] = ""; + // unknown proxy name -> default proxy + ProxyData *px = r->action == RULE_ACTION_PROXY ? find_proxy_by_name(r->proxy_name) : NULL; + uint32_t id = r->process_name ? ProxyBridge_AddRule(r->process_name, r->target_hosts, r->target_ports, r->protocol, + r->action, px ? px->id : 0, err, sizeof(err)) : 0; + if (id == 0) { + ctx->failed++; + if (ctx->fail_msg->len < 3000) + g_string_append_printf(ctx->fail_msg, "Rule #%d (%s): %s\n", index, r->process_name ? r->process_name : "?", + !r->process_name ? "missing processNames" : err[0] ? err : "rejected"); + return; + } + if (!r->enabled) ProxyBridge_DisableRule(id); + RuleData *nd = g_malloc0(sizeof(RuleData)); + nd->id = id; + nd->process_name = g_strdup(r->process_name); + nd->target_hosts = g_strdup(r->target_hosts); + nd->target_ports = g_strdup(r->target_ports); + nd->protocol = r->protocol; + nd->action = r->action; + nd->enabled = r->enabled; + nd->proxy_name = px ? g_strdup(px->name) : NULL; + g_rules_list = g_list_append(g_rules_list, nd); + ctx->imported++; } static void on_rule_import_clicked(GtkWidget *widget, gpointer data) { - GtkWidget *dialog = gtk_file_chooser_dialog_new("Import Rules", GTK_WINDOW(rules_window ? rules_window : window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); + GtkWidget *dialog = gtk_file_chooser_dialog_new("Import Rules", parent_window(), GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); char *content = NULL; - gsize len; - if (filename && g_file_get_contents(filename, &content, &len, NULL)) { - char *curr = content; - int imported = 0, failed = 0, index = 0; - GString *fail_msg = g_string_new(""); - - while ((curr = strchr(curr, '{')) != NULL) { - char *end = strchr(curr, '}'); - if (!end) break; - char saved = *end; *end = '\0'; - index++; - - // our keys first, macOS keys as fallback - char *proc = extract_sub_json_str(curr, "processNames"); - char *host = extract_sub_json_str(curr, "targetHosts"); - char *port = extract_sub_json_str(curr, "targetPorts"); - char *proto_s = pick_str(extract_sub_json_str(curr, "protocol"), extract_sub_json_str(curr, "ruleProtocol")); - char *act_s = pick_str(extract_sub_json_str(curr, "action"), extract_sub_json_str(curr, "ruleAction")); - char *proxy_s = extract_sub_json_str(curr, "proxy"); - bool en = json_has_key(curr, "enabled") ? extract_sub_json_bool(curr, "enabled") : true; - - if (!proc) { - failed++; - if (fail_msg->len < 3000) g_string_append_printf(fail_msg, "Rule #%d: missing processNames\n", index); - } else { - RuleProtocol p = RULE_PROTOCOL_BOTH; - if (proto_s && g_ascii_strcasecmp(proto_s, "TCP") == 0) p = RULE_PROTOCOL_TCP; - else if (proto_s && g_ascii_strcasecmp(proto_s, "UDP") == 0) p = RULE_PROTOCOL_UDP; - // anything that is not DIRECT/BLOCK is PROXY (macOS stores the proxy id there) - RuleAction a = RULE_ACTION_PROXY; - if (act_s && g_ascii_strcasecmp(act_s, "DIRECT") == 0) a = RULE_ACTION_DIRECT; - else if (act_s && g_ascii_strcasecmp(act_s, "BLOCK") == 0) a = RULE_ACTION_BLOCK; - - const char *h = host ? host : "*"; - const char *po = port ? port : "*"; - // unknown proxy name -> default proxy - ProxyData *px = (a == RULE_ACTION_PROXY) ? find_proxy_by_name(proxy_s) : NULL; - - char err[256] = ""; - uint32_t nid = ProxyBridge_AddRule(proc, h, po, p, a, px ? px->id : 0, err, sizeof(err)); - if (nid == 0) { - failed++; - if (fail_msg->len < 3000) g_string_append_printf(fail_msg, "Rule #%d (%s): %s\n", index, proc, err[0] ? err : "rejected"); - } else { - if (!en) ProxyBridge_DisableRule(nid); - RuleData *nd = g_malloc0(sizeof(RuleData)); - nd->id = nid; nd->process_name = g_strdup(proc); nd->target_hosts = g_strdup(h); - nd->target_ports = g_strdup(po); nd->protocol = p; nd->action = a; nd->enabled = en; - nd->proxy_name = px ? g_strdup(px->name) : NULL; - g_rules_list = g_list_append(g_rules_list, nd); - imported++; - } - } - g_free(proc); g_free(host); g_free(port); g_free(proto_s); g_free(act_s); g_free(proxy_s); - *end = saved; curr = end + 1; - } + if (filename && g_file_get_contents(filename, &content, NULL, NULL)) { + ImportCtx ctx = {0, 0, g_string_new("")}; + rules_from_json(content, import_rule_cb, &ctx); g_free(content); - if (imported > 0) { save_config(); refresh_rules_ui(); } - GtkWindow *parent = GTK_WINDOW(rules_window ? rules_window : window); - if (failed > 0) { - show_message(parent, GTK_MESSAGE_WARNING, "Imported %d rules, %d failed validation and were skipped:\n\n%s", imported, failed, fail_msg->str); - } else if (imported > 0) { - show_message(parent, GTK_MESSAGE_INFO, "Imported %d rules.", imported); - } else { - show_message(parent, GTK_MESSAGE_WARNING, "No rules found in the file."); - } - g_string_free(fail_msg, TRUE); + if (ctx.imported > 0) { save_config(); refresh_rules_ui(); } + if (ctx.failed > 0) + show_message(parent_window(), GTK_MESSAGE_WARNING, "Imported %d rules, %d failed validation and were skipped:\n\n%s", + ctx.imported, ctx.failed, ctx.fail_msg->str); + else if (ctx.imported > 0) + show_message(parent_window(), GTK_MESSAGE_INFO, "Imported %d rules.", ctx.imported); + else + show_message(parent_window(), GTK_MESSAGE_WARNING, "No rules found in the file."); + g_string_free(ctx.fail_msg, TRUE); } else { - show_message(GTK_WINDOW(rules_window ? rules_window : window), GTK_MESSAGE_ERROR, "Could not read the selected file."); + show_message(parent_window(), GTK_MESSAGE_ERROR, "Could not read the selected file."); } g_free(filename); } @@ -450,137 +322,108 @@ static void on_rule_import_clicked(GtkWidget *widget, gpointer data) { } static void on_bulk_delete_clicked(GtkWidget *widget, gpointer data) { - if (!g_rules_list) return; - GList *iter = g_rules_list; - GList *to_delete = NULL; - while (iter != NULL) { - RuleData *rule = (RuleData *)iter->data; - if (rule->selected) to_delete = g_list_append(to_delete, rule); - iter = iter->next; - } - if (!to_delete) return; - for (GList *d = to_delete; d != NULL; d = d->next) { - RuleData *rule = (RuleData *)d->data; - if (rule->id != 0) ProxyBridge_DeleteRule(rule->id); - g_rules_list = g_list_remove(g_rules_list, rule); - free_rule_data(rule); + GList *l = g_rules_list; + while (l != NULL) { + GList *next = l->next; + RuleData *rule = (RuleData *)l->data; + if (rule->selected) { + if (rule->id != 0) ProxyBridge_DeleteRule(rule->id); + g_rules_list = g_list_delete_link(g_rules_list, l); + free_rule_data(rule); + } + l = next; } - g_list_free(to_delete); save_config(); refresh_rules_ui(); } static void on_select_all_clicked(GtkWidget *widget, gpointer data) { - if (!g_rules_list) return; - bool all_selected = true; - for (GList *l = g_rules_list; l != NULL; l = l->next) { - if (!((RuleData *)l->data)->selected) { all_selected = false; break; } - } - bool new_state = !all_selected; + bool new_state = !all_rules_selected(); for (GList *l = g_rules_list; l != NULL; l = l->next) ((RuleData *)l->data)->selected = new_state; refresh_rules_ui(); } +static GtkWidget *header_label(const char *text, bool expand) { + GtkWidget *l = gtk_label_new(text); + gtk_widget_set_halign(l, GTK_ALIGN_START); + gtk_widget_set_hexpand(l, expand); + return l; +} + +static GtkWidget *cell_label(const char *text) { + GtkWidget *l = header_label(text, false); + gtk_label_set_ellipsize(GTK_LABEL(l), PANGO_ELLIPSIZE_END); + return l; +} + +static GtkWidget *small_button(GtkWidget *box, const char *label, const char *tip, bool sensitive, GCallback cb, gpointer data) { + GtkWidget *b = gtk_button_new_with_label(label); + if (tip) gtk_widget_set_tooltip_text(b, tip); + gtk_widget_set_sensitive(b, sensitive); + g_signal_connect(b, "clicked", cb, data); + gtk_box_pack_start(GTK_BOX(box), b, FALSE, FALSE, 0); + return b; +} + +static GtkWidget *check_cell(GtkWidget *grid, int row, int col, bool active, GCallback cb, gpointer data) { + GtkWidget *c = gtk_check_button_new(); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(c), active); + g_signal_connect(c, "toggled", cb, data); + gtk_grid_attach(GTK_GRID(grid), c, col, row, 1, 1); + return c; +} + static void refresh_rules_ui() { if (!rules_list_box) return; GList *children = gtk_container_get_children(GTK_CONTAINER(rules_list_box)); - for(GList *iter = children; iter != NULL; iter = g_list_next(iter)) gtk_widget_destroy(GTK_WIDGET(iter->data)); + for (GList *c = children; c != NULL; c = c->next) gtk_widget_destroy(GTK_WIDGET(c->data)); g_list_free(children); - - if (btn_select_all_header) { - bool all_selected = (g_rules_list != NULL); - if (g_rules_list == NULL) all_selected = false; - for (GList *l = g_rules_list; l != NULL; l = l->next) { - if (!((RuleData *)l->data)->selected) { all_selected = false; break; } - } - gtk_button_set_label(GTK_BUTTON(btn_select_all_header), all_selected ? "Deselect All" : "Select All"); - } + update_select_all_label(); GtkWidget *grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 10); gtk_grid_set_column_spacing(GTK_GRID(grid), 15); gtk_container_add(GTK_CONTAINER(rules_list_box), grid); - int row = 0; - gtk_grid_attach(GTK_GRID(grid), gtk_label_new(" "), 0, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Enable"), 1, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Actions"), 2, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("SR"), 3, row, 1, 1); - GtkWidget *h_proc = gtk_label_new("Process"); gtk_widget_set_halign(h_proc, GTK_ALIGN_START); - gtk_widget_set_hexpand(h_proc, TRUE); gtk_grid_attach(GTK_GRID(grid), h_proc, 4, row, 1, 1); - GtkWidget *h_host = gtk_label_new("Target Hosts"); gtk_widget_set_halign(h_host, GTK_ALIGN_START); - gtk_widget_set_hexpand(h_host, TRUE); gtk_grid_attach(GTK_GRID(grid), h_host, 5, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Protocol"), 6, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Action"), 7, row, 1, 1); - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Proxy"), 8, row, 1, 1); - row++; - gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, 9, 1); row++; - - int sr_counter = 1; - for (GList *l = g_rules_list; l != NULL; l = l->next) { - RuleData *r = (RuleData *)l->data; - GtkWidget *chk_sel = gtk_check_button_new(); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(chk_sel), r->selected); - g_signal_connect(chk_sel, "toggled", G_CALLBACK(on_rule_select_toggle), r); - gtk_grid_attach(GTK_GRID(grid), chk_sel, 0, row, 1, 1); + const char *heads[] = {" ", "Enable", "Actions", "SR", "Process", "Target Hosts", "Protocol", "Action", "Proxy"}; + for (int c = 0; c < 9; c++) + gtk_grid_attach(GTK_GRID(grid), header_label(heads[c], c == 4 || c == 5), c, 0, 1, 1); + gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 9, 1); - GtkWidget *chk = gtk_check_button_new(); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(chk), r->enabled); - g_signal_connect(chk, "toggled", G_CALLBACK(on_rule_toggle), r); - gtk_grid_attach(GTK_GRID(grid), chk, 1, row, 1, 1); + const char *proto_strs[] = {"TCP", "UDP", "BOTH"}; + const char *action_strs[] = {"PROXY", "DIRECT", "BLOCK"}; + const char *action_class[] = {"success", "info", "warning"}; + int row = 2, sr = 1; + for (GList *l = g_rules_list; l != NULL; l = l->next, row += 2) { + RuleData *r = (RuleData *)l->data; + check_cell(grid, row, 0, r->selected, G_CALLBACK(on_rule_select_toggle), r); + check_cell(grid, row, 1, r->enabled, G_CALLBACK(on_rule_toggle), r); - GtkWidget *act_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2); - GtkWidget *btn_edit = gtk_button_new_with_label("Edit"); - g_signal_connect(btn_edit, "clicked", G_CALLBACK(on_rule_edit), r); - GtkWidget *btn_del = gtk_button_new_with_label("Delete"); - g_signal_connect(btn_del, "clicked", G_CALLBACK(on_rule_delete), r); // list position is the priority, first match wins - GtkWidget *btn_up = gtk_button_new_with_label("\u25B2"); - gtk_widget_set_tooltip_text(btn_up, "Move up (higher priority)"); - gtk_widget_set_sensitive(btn_up, l->prev != NULL); - g_signal_connect(btn_up, "clicked", G_CALLBACK(on_rule_move_up), r); - GtkWidget *btn_down = gtk_button_new_with_label("\u25BC"); - gtk_widget_set_tooltip_text(btn_down, "Move down (lower priority)"); - gtk_widget_set_sensitive(btn_down, l->next != NULL); - g_signal_connect(btn_down, "clicked", G_CALLBACK(on_rule_move_down), r); - gtk_box_pack_start(GTK_BOX(act_box), btn_edit, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(act_box), btn_del, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(act_box), btn_up, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(act_box), btn_down, FALSE, FALSE, 0); + GtkWidget *act_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2); + small_button(act_box, "Edit", NULL, true, G_CALLBACK(on_rule_edit), r); + small_button(act_box, "Delete", NULL, true, G_CALLBACK(on_rule_delete), r); + small_button(act_box, "▲", "Move up (higher priority)", l->prev != NULL, G_CALLBACK(on_rule_move_up), r); + small_button(act_box, "▼", "Move down (lower priority)", l->next != NULL, G_CALLBACK(on_rule_move_down), r); gtk_grid_attach(GTK_GRID(grid), act_box, 2, row, 1, 1); - char sr_str[16]; snprintf(sr_str, sizeof(sr_str), "%d", sr_counter++); + char sr_str[16]; + snprintf(sr_str, sizeof(sr_str), "%d", sr++); gtk_grid_attach(GTK_GRID(grid), gtk_label_new(sr_str), 3, row, 1, 1); - - GtkWidget *l_proc = gtk_label_new(r->process_name); gtk_widget_set_halign(l_proc, GTK_ALIGN_START); - gtk_label_set_ellipsize(GTK_LABEL(l_proc), PANGO_ELLIPSIZE_END); - gtk_grid_attach(GTK_GRID(grid), l_proc, 4, row, 1, 1); - - GtkWidget *l_host = gtk_label_new(r->target_hosts); gtk_widget_set_halign(l_host, GTK_ALIGN_START); - gtk_label_set_ellipsize(GTK_LABEL(l_host), PANGO_ELLIPSIZE_END); - gtk_grid_attach(GTK_GRID(grid), l_host, 5, row, 1, 1); - - const char* proto_strs[] = {"TCP", "UDP", "BOTH"}; + gtk_grid_attach(GTK_GRID(grid), cell_label(r->process_name), 4, row, 1, 1); + gtk_grid_attach(GTK_GRID(grid), cell_label(r->target_hosts), 5, row, 1, 1); gtk_grid_attach(GTK_GRID(grid), gtk_label_new(proto_strs[r->protocol <= RULE_PROTOCOL_BOTH ? r->protocol : RULE_PROTOCOL_BOTH]), 6, row, 1, 1); - const char* action_strs[] = {"PROXY", "DIRECT", "BLOCK"}; - GtkWidget *l_act = gtk_label_new(action_strs[r->action <= RULE_ACTION_BLOCK ? r->action : RULE_ACTION_PROXY]); - GtkStyleContext *context = gtk_widget_get_style_context(l_act); - if (r->action == RULE_ACTION_PROXY) gtk_style_context_add_class(context, "success"); - else if (r->action == RULE_ACTION_DIRECT) gtk_style_context_add_class(context, "info"); - else gtk_style_context_add_class(context, "warning"); + int a = r->action <= RULE_ACTION_BLOCK ? r->action : RULE_ACTION_PROXY; + GtkWidget *l_act = gtk_label_new(action_strs[a]); + gtk_style_context_add_class(gtk_widget_get_style_context(l_act), action_class[a]); gtk_grid_attach(GTK_GRID(grid), l_act, 7, row, 1, 1); - // proxy only matters for PROXY rules const char *proxy_txt = "-"; if (r->action == RULE_ACTION_PROXY) - proxy_txt = (r->proxy_name && *r->proxy_name && find_proxy_by_name(r->proxy_name)) ? r->proxy_name : "default"; - GtkWidget *l_proxy = gtk_label_new(proxy_txt); - gtk_label_set_ellipsize(GTK_LABEL(l_proxy), PANGO_ELLIPSIZE_END); - gtk_grid_attach(GTK_GRID(grid), l_proxy, 8, row, 1, 1); - - row++; - gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, 9, 1); row++; + proxy_txt = (r->proxy_name && find_proxy_by_name(r->proxy_name)) ? r->proxy_name : "default"; + gtk_grid_attach(GTK_GRID(grid), cell_label(proxy_txt), 8, row, 1, 1); + gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row + 1, 9, 1); } gtk_widget_show_all(rules_list_box); } @@ -589,7 +432,6 @@ void refresh_rules_window() { refresh_rules_ui(); } -// widgets are gone, refresh_rules_ui must not touch them any more static void on_rules_window_destroy(GtkWidget *widget, gpointer data) { rules_window = NULL; rules_list_box = NULL; @@ -601,47 +443,41 @@ void on_proxy_rules_clicked(GtkWidget *widget, gpointer data) { gtk_window_present(GTK_WINDOW(rules_window)); return; } - GtkWidget *dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL); - rules_window = dialog; - g_signal_connect(dialog, "destroy", G_CALLBACK(on_rules_window_destroy), NULL); - gtk_window_set_title(GTK_WINDOW(dialog), "Process Rules"); - gtk_window_set_default_size(GTK_WINDOW(dialog), 1000, 500); - gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window)); + rules_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(rules_window, "destroy", G_CALLBACK(on_rules_window_destroy), NULL); + gtk_window_set_title(GTK_WINDOW(rules_window), "Process Rules"); + gtk_window_set_default_size(GTK_WINDOW(rules_window), 1000, 500); + gtk_window_set_transient_for(GTK_WINDOW(rules_window), GTK_WINDOW(window)); GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); gtk_container_set_border_width(GTK_CONTAINER(vbox), 20); - gtk_container_add(GTK_CONTAINER(dialog), vbox); + gtk_container_add(GTK_CONTAINER(rules_window), vbox); GtkWidget *header_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); GtkWidget *title = gtk_label_new(NULL); gtk_label_set_markup(GTK_LABEL(title), "Process Rules"); + gtk_box_pack_start(GTK_BOX(header_box), title, FALSE, FALSE, 0); + GtkWidget *spacer = gtk_label_new(""); + gtk_box_pack_start(GTK_BOX(header_box), spacer, TRUE, TRUE, 0); + + btn_select_all_header = gtk_button_new_with_label("Select All"); GtkWidget *add_btn = gtk_button_new_with_label("+ Add Rule"); + GtkWidget *del_btn = gtk_button_new_with_label("Delete Selected"); + GtkWidget *import_btn = gtk_button_new_with_label("Import"); + GtkWidget *export_btn = gtk_button_new_with_label("Export"); g_signal_connect(add_btn, "clicked", G_CALLBACK(on_rule_add_clicked), NULL); - btn_select_all_header = gtk_button_new_with_label("Select All"); g_signal_connect(btn_select_all_header, "clicked", G_CALLBACK(on_select_all_clicked), NULL); - GtkWidget *del_all_btn = gtk_button_new_with_label("Delete Selected"); - g_signal_connect(del_all_btn, "clicked", G_CALLBACK(on_bulk_delete_clicked), NULL); - GtkWidget *import_btn = gtk_button_new_with_label("Import"); + g_signal_connect(del_btn, "clicked", G_CALLBACK(on_bulk_delete_clicked), NULL); g_signal_connect(import_btn, "clicked", G_CALLBACK(on_rule_import_clicked), NULL); - GtkWidget *export_btn = gtk_button_new_with_label("Export"); g_signal_connect(export_btn, "clicked", G_CALLBACK(on_rule_export_clicked), NULL); - - gtk_box_pack_start(GTK_BOX(header_box), title, FALSE, FALSE, 0); - GtkWidget *spacer = gtk_label_new(""); - gtk_widget_set_hexpand(spacer, TRUE); - gtk_box_pack_start(GTK_BOX(header_box), spacer, TRUE, TRUE, 0); - gtk_box_pack_end(GTK_BOX(header_box), add_btn, FALSE, FALSE, 5); - gtk_box_pack_end(GTK_BOX(header_box), btn_select_all_header, FALSE, FALSE, 5); - gtk_box_pack_end(GTK_BOX(header_box), del_all_btn, FALSE, FALSE, 5); - gtk_box_pack_end(GTK_BOX(header_box), export_btn, FALSE, FALSE, 5); - gtk_box_pack_end(GTK_BOX(header_box), import_btn, FALSE, FALSE, 5); + GtkWidget *right[] = {add_btn, btn_select_all_header, del_btn, export_btn, import_btn}; + for (int i = 0; i < 5; i++) gtk_box_pack_end(GTK_BOX(header_box), right[i], FALSE, FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), header_box, FALSE, FALSE, 0); GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL); - gtk_widget_set_vexpand(scrolled, TRUE); rules_list_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); gtk_container_add(GTK_CONTAINER(scrolled), rules_list_box); gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0); refresh_rules_ui(); - gtk_widget_show_all(dialog); + gtk_widget_show_all(rules_window); } diff --git a/Linux/gui/gui_settings.c b/Linux/gui/gui_settings.c index 6de7face..22b11cba 100644 --- a/Linux/gui/gui_settings.c +++ b/Linux/gui/gui_settings.c @@ -1,30 +1,37 @@ #include "gui.h" -// settings stuff, proxy list manager - GList *g_proxies_list = NULL; char *g_default_proxy = NULL; -enum { - COL_NAME = 0, - COL_TYPE, - COL_ADDR, - COL_AUTH, - COL_DEFAULT, - COL_PTR, - N_COLS -}; +enum { COL_NAME, COL_TYPE, COL_ADDR, COL_AUTH, COL_DEFAULT, COL_PTR, N_COLS }; + +typedef struct { + GtkWidget *dialog; + GtkWidget *tree; + GtkListStore *store; + GtkWidget *test_host; + GtkWidget *test_port; + GtkTextBuffer *output_buffer; + GtkWidget *test_btn; +} ConfigInfo; + +// refs on buffer/btn keep them valid if the dialog closes mid-test +typedef struct { + uint32_t proxy_id; + char *host; + uint16_t port; + GtkTextBuffer *buffer; + GtkWidget *btn; + char result[4096]; +} TestJob; void free_proxy_data(ProxyData *p) { if (!p) return; g_free(p->name); g_free(p->host); g_free(p->username); - // dont leave passwords lying around in freed memory - if (p->password) { - memset(p->password, 0, strlen(p->password)); - g_free(p->password); - } + if (p->password) memset(p->password, 0, strlen(p->password)); + g_free(p->password); g_free(p); } @@ -37,13 +44,12 @@ ProxyData *find_proxy_by_name(const char *name) { return NULL; } -// name -> library id, 0 means default proxy (empty or unknown name) +// 0 = default proxy (empty or unknown name) uint32_t resolve_proxy_id(const char *name) { ProxyData *p = find_proxy_by_name(name); return p ? p->id : 0; } -// push the stored proxies to the library, call before ProxyBridge_Start void apply_proxies_to_lib() { ProxyBridge_ClearProxies(); for (GList *l = g_proxies_list; l != NULL; l = l->next) { @@ -55,109 +61,89 @@ void apply_proxies_to_lib() { ProxyBridge_SetDefaultProxy(resolve_proxy_id(g_default_proxy)); } -static bool port_text_valid(const char *port_text, int *port_out) { - if (!port_text || !*port_text || strspn(port_text, "0123456789") != strlen(port_text) || strlen(port_text) > 5) return false; - int p = (int)safe_strtol(port_text); +static bool confirm(GtkWindow *parent, const char *format, ...) { + va_list args; + va_start(args, format); + char *msg = g_strdup_vprintf(format, args); + va_end(args); + GtkWidget *q = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "%s", msg); + int resp = gtk_dialog_run(GTK_DIALOG(q)); + gtk_widget_destroy(q); + g_free(msg); + return resp == GTK_RESPONSE_YES; +} + +static bool port_text_valid(const char *text, int *port_out) { + size_t n = strlen(text); + if (n == 0 || n > 5 || strspn(text, "0123456789") != n) return false; + int p = atoi(text); if (p < 1 || p > 65535) return false; - if (port_out) *port_out = p; + *port_out = p; return true; } -// ---- test ---- - static gboolean on_test_done(gpointer user_data) { - TestResultData *data = (TestResultData *)user_data; - gtk_text_buffer_set_text(data->buffer, data->result_text, -1); - gtk_widget_set_sensitive(data->btn, TRUE); - - g_object_unref(data->buffer); - g_object_unref(data->btn); - g_free(data->result_text); - g_free(data); + TestJob *job = (TestJob *)user_data; + char *text = g_utf8_make_valid(job->result, -1); + gtk_text_buffer_set_text(job->buffer, text, -1); + gtk_widget_set_sensitive(job->btn, TRUE); + g_free(text); + g_object_unref(job->buffer); + g_object_unref(job->btn); + g_free(job->host); + g_free(job); return FALSE; } static gpointer run_test_thread(gpointer user_data) { - struct TestRunnerData *req = (struct TestRunnerData *)user_data; - - char *buffer = g_malloc0(4096); - - // run test - ProxyBridge_TestProxy(req->proxy_id, req->host, req->port, buffer, 4096); - buffer[4095] = '\0'; - if (!g_utf8_validate(buffer, -1, NULL)) { - char *fixed = g_utf8_make_valid(buffer, -1); - g_free(buffer); - buffer = fixed; - } - - TestResultData *res = g_malloc0(sizeof(TestResultData)); - res->result_text = buffer; - res->buffer = req->buffer; - res->btn = req->btn; - - g_idle_add(on_test_done, res); - - g_free(req->host); - g_free(req); + TestJob *job = (TestJob *)user_data; + ProxyBridge_TestProxy(job->proxy_id, job->host, job->port, job->result, sizeof(job->result)); + job->result[sizeof(job->result) - 1] = '\0'; + g_idle_add(on_test_done, job); return NULL; } static ProxyData *get_selected_proxy(ConfigInfo *info) { - GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(info->tree)); GtkTreeModel *model; GtkTreeIter iter; - if (!gtk_tree_selection_get_selected(sel, &model, &iter)) return NULL; gpointer ptr = NULL; - gtk_tree_model_get(model, &iter, COL_PTR, &ptr, -1); + if (gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(info->tree)), &model, &iter)) + gtk_tree_model_get(model, &iter, COL_PTR, &ptr, -1); return (ProxyData *)ptr; } static void on_start_test_clicked(GtkWidget *widget, gpointer data) { ConfigInfo *info = (ConfigInfo *)data; - ProxyData *px = get_selected_proxy(info); - if (!px) { - gtk_text_buffer_set_text(info->output_buffer, "Error: Select a proxy to test.", -1); - return; - } - if (px->id == 0) { - gtk_text_buffer_set_text(info->output_buffer, "Error: This proxy was rejected by the engine, edit and save it again.", -1); + const char *err = !px ? "Error: Select a proxy to test." + : px->id == 0 ? "Error: This proxy was rejected by the engine, edit and save it again." : NULL; + if (err) { + gtk_text_buffer_set_text(info->output_buffer, err, -1); return; } - // get target const char *t_host = gtk_entry_get_text(GTK_ENTRY(info->test_host)); - const char *t_port_s = gtk_entry_get_text(GTK_ENTRY(info->test_port)); - - if (!t_host || strlen(t_host) == 0) t_host = "google.com"; - int t_port = (int)safe_strtol(t_port_s); + int t_port = (int)safe_strtol(gtk_entry_get_text(GTK_ENTRY(info->test_port))); if (t_port <= 0 || t_port > 65535) t_port = 80; - // update gui gtk_text_buffer_set_text(info->output_buffer, "Testing connection... Please wait...", -1); gtk_widget_set_sensitive(info->test_btn, FALSE); - // start thread, widgets are ref'd because the dialog can close before the test ends - struct TestRunnerData *req = g_malloc0(sizeof(struct TestRunnerData)); - req->proxy_id = px->id; - req->host = g_strdup(t_host); - req->port = (uint16_t)t_port; - req->buffer = g_object_ref(info->output_buffer); - req->btn = g_object_ref(info->test_btn); - - GThread *thread = g_thread_new("test_conn", run_test_thread, req); - g_thread_unref(thread); + TestJob *job = g_malloc0(sizeof(TestJob)); + job->proxy_id = px->id; + job->host = g_strdup(*t_host ? t_host : "google.com"); + job->port = (uint16_t)t_port; + job->buffer = g_object_ref(info->output_buffer); + job->btn = g_object_ref(info->test_btn); + g_thread_unref(g_thread_new("test_conn", run_test_thread, job)); } -// ---- list ---- - static void refresh_proxy_store(ConfigInfo *info) { gtk_list_store_clear(info->store); for (GList *l = g_proxies_list; l != NULL; l = l->next) { ProxyData *p = (ProxyData *)l->data; char *addr = g_strdup_printf("%s:%u", p->host, p->port); - bool is_def = g_default_proxy && p->name && strcmp(g_default_proxy, p->name) == 0; + bool is_def = g_default_proxy && strcmp(g_default_proxy, p->name) == 0; GtkTreeIter iter; gtk_list_store_append(info->store, &iter); gtk_list_store_set(info->store, &iter, @@ -186,117 +172,95 @@ static void select_proxy_row(ConfigInfo *info, ProxyData *target) { } while (gtk_tree_model_iter_next(model, &iter)); } -// add/edit dialog. existing == NULL adds a new one. returns the proxy that was saved or NULL +static void set_str(char **dst, const char *src) { + g_free(*dst); + *dst = g_strdup(src); +} + +// renaming keeps the default proxy and the rules pointing at it +static void rename_proxy_refs(const char *old_name, const char *new_name) { + if (g_default_proxy && strcmp(g_default_proxy, old_name) == 0) set_str(&g_default_proxy, new_name); + for (GList *l = g_rules_list; l != NULL; l = l->next) { + RuleData *r = (RuleData *)l->data; + if (r->proxy_name && strcmp(r->proxy_name, old_name) == 0) set_str(&r->proxy_name, new_name); + } +} + +// existing == NULL adds. returns the saved proxy or NULL static ProxyData *proxy_edit_dialog(GtkWindow *parent, ProxyData *existing) { - GtkWidget *dialog = gtk_dialog_new_with_buttons(existing ? "Edit Proxy" : "Add Proxy", - parent, + GtkWidget *dialog = gtk_dialog_new_with_buttons(existing ? "Edit Proxy" : "Add Proxy", parent, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, - "Cancel", GTK_RESPONSE_CANCEL, - "Save", GTK_RESPONSE_ACCEPT, - NULL); + "Cancel", GTK_RESPONSE_CANCEL, "Save", GTK_RESPONSE_ACCEPT, NULL); gtk_window_set_default_size(GTK_WINDOW(dialog), 420, -1); - GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); GtkWidget *grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 5); gtk_grid_set_column_spacing(GTK_GRID(grid), 10); gtk_container_set_border_width(GTK_CONTAINER(grid), 10); - gtk_box_pack_start(GTK_BOX(content_area), grid, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), grid, TRUE, TRUE, 0); - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Name:"), 0, 0, 1, 1); GtkWidget *name_entry = gtk_entry_new(); - gtk_widget_set_hexpand(name_entry, TRUE); - gtk_grid_attach(GTK_GRID(grid), name_entry, 1, 0, 1, 1); - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Type:"), 0, 1, 1, 1); - GtkWidget *type_combo = gtk_combo_box_text_new(); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(type_combo), "HTTP"); - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(type_combo), "SOCKS5"); - gtk_combo_box_set_active(GTK_COMBO_BOX(type_combo), 1); - gtk_grid_attach(GTK_GRID(grid), type_combo, 1, 1, 1, 1); - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Host:"), 0, 2, 1, 1); + GtkWidget *type_combo = combo_new("HTTP", "SOCKS5", NULL); GtkWidget *ip_entry = gtk_entry_new(); - gtk_grid_attach(GTK_GRID(grid), ip_entry, 1, 2, 1, 1); - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Port:"), 0, 3, 1, 1); GtkWidget *port_entry = gtk_entry_new(); - gtk_grid_attach(GTK_GRID(grid), port_entry, 1, 3, 1, 1); - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Username:"), 0, 4, 1, 1); GtkWidget *user_entry = gtk_entry_new(); - gtk_grid_attach(GTK_GRID(grid), user_entry, 1, 4, 1, 1); - - gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Password:"), 0, 5, 1, 1); GtkWidget *pass_entry = gtk_entry_new(); gtk_entry_set_visibility(GTK_ENTRY(pass_entry), FALSE); - gtk_grid_attach(GTK_GRID(grid), pass_entry, 1, 5, 1, 1); - - GtkWidget *err_label = gtk_label_new(""); - gtk_widget_set_halign(err_label, GTK_ALIGN_START); - gtk_label_set_line_wrap(GTK_LABEL(err_label), TRUE); - gtk_label_set_xalign(GTK_LABEL(err_label), 0); + grid_row(grid, 0, "Name:", name_entry); + grid_row(grid, 1, "Type:", type_combo); + grid_row(grid, 2, "Host:", ip_entry); + grid_row(grid, 3, "Port:", port_entry); + grid_row(grid, 4, "Username:", user_entry); + grid_row(grid, 5, "Password:", pass_entry); + GtkWidget *err_label = hint_label(""); gtk_grid_attach(GTK_GRID(grid), err_label, 0, 6, 2, 1); + gtk_combo_box_set_active(GTK_COMBO_BOX(type_combo), 1); if (existing) { + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%u", existing->port); gtk_entry_set_text(GTK_ENTRY(name_entry), existing->name); gtk_combo_box_set_active(GTK_COMBO_BOX(type_combo), existing->type == PROXY_TYPE_HTTP ? 0 : 1); gtk_entry_set_text(GTK_ENTRY(ip_entry), existing->host); - if (existing->port != 0) { - char port_str[16]; - snprintf(port_str, sizeof(port_str), "%u", existing->port); - gtk_entry_set_text(GTK_ENTRY(port_entry), port_str); - } + if (existing->port) gtk_entry_set_text(GTK_ENTRY(port_entry), port_str); gtk_entry_set_text(GTK_ENTRY(user_entry), existing->username ? existing->username : ""); gtk_entry_set_text(GTK_ENTRY(pass_entry), existing->password ? existing->password : ""); } else if (!g_proxies_list) { gtk_entry_set_text(GTK_ENTRY(name_entry), "default"); } - gtk_widget_show_all(dialog); ProxyData *result = NULL; - while (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { - const char *name = gtk_entry_get_text(GTK_ENTRY(name_entry)); - const char *ip_text = gtk_entry_get_text(GTK_ENTRY(ip_entry)); + while (!result && gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { const char *port_text = gtk_entry_get_text(GTK_ENTRY(port_entry)); const char *user = gtk_entry_get_text(GTK_ENTRY(user_entry)); const char *pass = gtk_entry_get_text(GTK_ENTRY(pass_entry)); - ProxyType type = (gtk_combo_box_get_active(GTK_COMBO_BOX(type_combo)) == 0) ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; + ProxyType type = gtk_combo_box_get_active(GTK_COMBO_BOX(type_combo)) == 0 ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5; + char *name = g_strstrip(g_strdup(gtk_entry_get_text(GTK_ENTRY(name_entry)))); + char *host = g_strstrip(g_strdup(gtk_entry_get_text(GTK_ENTRY(ip_entry)))); + ProxyData *dup = find_proxy_by_name(name); int port = 0; + char err[256] = ""; - char *name_t = g_strstrip(g_strdup(name)); - char *host_t = g_strstrip(g_strdup(ip_text)); - const char *msg = NULL; - - if (name_t[0] == '\0') msg = "Name cannot be empty."; - else if (find_proxy_by_name(name_t) && find_proxy_by_name(name_t) != existing) msg = "A proxy with this name already exists."; - else if (host_t[0] == '\0') msg = "Host (IP/Domain) cannot be empty."; - else if (!port_text_valid(port_text, &port)) msg = "Port must be a number between 1 and 65535."; - - if (msg) { - gtk_label_set_markup(GTK_LABEL(err_label), ""); - char *esc = g_markup_printf_escaped("%s", msg); - gtk_label_set_markup(GTK_LABEL(err_label), esc); - g_free(esc); - g_free(name_t); g_free(host_t); - continue; - } + const char *msg = !*name ? "Name cannot be empty." + : (dup && dup != existing) ? "A proxy with this name already exists." + : !*host ? "Host (IP/Domain) cannot be empty." + : !port_text_valid(port_text, &port) ? "Port must be a number between 1 and 65535." : NULL; - // library validates too (and keeps the proxy list in sync) - char err[256] = ""; uint32_t id = existing ? existing->id : 0; - bool ok; - if (id != 0) { - ok = ProxyBridge_EditProxy(id, name_t, type, host_t, (uint16_t)port, user, pass, err, sizeof(err)); - } else { - id = ProxyBridge_AddProxy(name_t, type, host_t, (uint16_t)port, user, pass, err, sizeof(err)); - ok = (id != 0); + if (!msg) { + bool ok; + if (id) { + ok = ProxyBridge_EditProxy(id, name, type, host, (uint16_t)port, user, pass, err, sizeof(err)); + } else { + id = ProxyBridge_AddProxy(name, type, host, (uint16_t)port, user, pass, err, sizeof(err)); + ok = id != 0; + } + if (!ok) msg = err[0] ? err : "Engine rejected the proxy settings."; } - if (!ok) { - char *esc = g_markup_printf_escaped("%s", err[0] ? err : "Engine rejected the proxy settings."); - gtk_label_set_markup(GTK_LABEL(err_label), esc); - g_free(esc); - g_free(name_t); g_free(host_t); + if (msg) { + set_error_label(err_label, msg); + g_free(name); + g_free(host); continue; } @@ -304,52 +268,40 @@ static ProxyData *proxy_edit_dialog(GtkWindow *parent, ProxyData *existing) { if (!p) { p = g_malloc0(sizeof(ProxyData)); g_proxies_list = g_list_append(g_proxies_list, p); - } else if (p->name && strcmp(p->name, name_t) != 0) { - // renamed, keep default and rules pointing at it - if (g_default_proxy && strcmp(g_default_proxy, p->name) == 0) { - g_free(g_default_proxy); - g_default_proxy = g_strdup(name_t); - } - for (GList *l = g_rules_list; l != NULL; l = l->next) { - RuleData *r = (RuleData *)l->data; - if (r->proxy_name && strcmp(r->proxy_name, p->name) == 0) { - g_free(r->proxy_name); - r->proxy_name = g_strdup(name_t); - } - } + } else if (strcmp(p->name, name) != 0) { + rename_proxy_refs(p->name, name); } p->id = id; - g_free(p->name); p->name = name_t; - g_free(p->host); p->host = host_t; - g_free(p->username); p->username = g_strdup(user); + g_free(p->name); p->name = name; + g_free(p->host); p->host = host; + set_str(&p->username, user); if (p->password) memset(p->password, 0, strlen(p->password)); - g_free(p->password); p->password = g_strdup(pass); + set_str(&p->password, pass); p->type = type; p->port = (uint16_t)port; - // first proxy becomes the default - if (!g_default_proxy || !find_proxy_by_name(g_default_proxy)) { - g_free(g_default_proxy); - g_default_proxy = g_strdup(p->name); + if (!find_proxy_by_name(g_default_proxy)) { + set_str(&g_default_proxy, p->name); ProxyBridge_SetDefaultProxy(p->id); } result = p; - break; } - gtk_widget_destroy(dialog); return result; } -static void on_proxy_add(GtkWidget *widget, gpointer data) { - ConfigInfo *info = (ConfigInfo *)data; - ProxyData *p = proxy_edit_dialog(GTK_WINDOW(info->dialog), NULL); +static void commit_proxy_change(ConfigInfo *info, ProxyData *p) { if (!p) return; save_config(); refresh_proxy_store(info); select_proxy_row(info, p); } +static void on_proxy_add(GtkWidget *widget, gpointer data) { + ConfigInfo *info = (ConfigInfo *)data; + commit_proxy_change(info, proxy_edit_dialog(GTK_WINDOW(info->dialog), NULL)); +} + static void on_proxy_edit(GtkWidget *widget, gpointer data) { ConfigInfo *info = (ConfigInfo *)data; ProxyData *sel = get_selected_proxy(info); @@ -357,11 +309,7 @@ static void on_proxy_edit(GtkWidget *widget, gpointer data) { show_message(GTK_WINDOW(info->dialog), GTK_MESSAGE_WARNING, "Select a proxy first."); return; } - ProxyData *p = proxy_edit_dialog(GTK_WINDOW(info->dialog), sel); - if (!p) return; - save_config(); - refresh_proxy_store(info); - select_proxy_row(info, p); + commit_proxy_change(info, proxy_edit_dialog(GTK_WINDOW(info->dialog), sel)); } static void on_proxy_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data) { @@ -375,34 +323,21 @@ static void on_proxy_delete(GtkWidget *widget, gpointer data) { show_message(GTK_WINDOW(info->dialog), GTK_MESSAGE_WARNING, "Select a proxy first."); return; } + if (!confirm(GTK_WINDOW(info->dialog), "Delete proxy '%s'?\nRules using it will fall back to the default proxy.", p->name)) return; - GtkWidget *q = gtk_message_dialog_new(GTK_WINDOW(info->dialog), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, - GTK_BUTTONS_YES_NO, "Delete proxy '%s'?\nRules using it will fall back to the default proxy.", p->name); - int resp = gtk_dialog_run(GTK_DIALOG(q)); - gtk_widget_destroy(q); - if (resp != GTK_RESPONSE_YES) return; - - if (p->id != 0) ProxyBridge_DeleteProxy(p->id); - - // rules that used it now use the default proxy (library does the same) + if (p->id) ProxyBridge_DeleteProxy(p->id); for (GList *l = g_rules_list; l != NULL; l = l->next) { RuleData *r = (RuleData *)l->data; - if (r->proxy_name && strcmp(r->proxy_name, p->name) == 0) { - g_free(r->proxy_name); - r->proxy_name = NULL; - } + if (r->proxy_name && strcmp(r->proxy_name, p->name) == 0) set_str(&r->proxy_name, NULL); } bool was_default = g_default_proxy && strcmp(g_default_proxy, p->name) == 0; g_proxies_list = g_list_remove(g_proxies_list, p); free_proxy_data(p); - if (was_default) { - g_free(g_default_proxy); - g_default_proxy = g_proxies_list ? g_strdup(((ProxyData *)g_proxies_list->data)->name) : NULL; + set_str(&g_default_proxy, g_proxies_list ? ((ProxyData *)g_proxies_list->data)->name : NULL); ProxyBridge_SetDefaultProxy(resolve_proxy_id(g_default_proxy)); } - save_config(); refresh_proxy_store(info); } @@ -414,12 +349,9 @@ static void on_proxy_set_default(GtkWidget *widget, gpointer data) { show_message(GTK_WINDOW(info->dialog), GTK_MESSAGE_WARNING, "Select a proxy first."); return; } - g_free(g_default_proxy); - g_default_proxy = g_strdup(p->name); + set_str(&g_default_proxy, p->name); ProxyBridge_SetDefaultProxy(p->id); - save_config(); - refresh_proxy_store(info); - select_proxy_row(info, p); + commit_proxy_change(info, p); char *status_msg = g_strdup_printf("Default proxy: %s (%s:%u)", p->name, p->host, p->port); gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, status_msg); @@ -427,65 +359,45 @@ static void on_proxy_set_default(GtkWidget *widget, gpointer data) { } void on_proxy_configure(GtkWidget *widget, gpointer data) { - ConfigInfo info; - memset(&info, 0, sizeof(info)); - - GtkWidget *content_area; - GtkWidget *grid; - - info.dialog = gtk_dialog_new_with_buttons("Proxy Settings", - GTK_WINDOW(window), + ConfigInfo info = {0}; + info.dialog = gtk_dialog_new_with_buttons("Proxy Settings", GTK_WINDOW(window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, - "Close", GTK_RESPONSE_CLOSE, - NULL); - // wider window + "Close", GTK_RESPONSE_CLOSE, NULL); gtk_window_set_default_size(GTK_WINDOW(info.dialog), 650, 560); - content_area = gtk_dialog_get_content_area(GTK_DIALOG(info.dialog)); - grid = gtk_grid_new(); + GtkWidget *grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 5); gtk_grid_set_column_spacing(GTK_GRID(grid), 10); gtk_container_set_border_width(GTK_CONTAINER(grid), 10); - gtk_box_pack_start(GTK_BOX(content_area), grid, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(info.dialog))), grid, TRUE, TRUE, 0); - // proxy list info.store = gtk_list_store_new(N_COLS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER); info.tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(info.store)); - g_object_unref(info.store); // tree holds it now + g_object_unref(info.store); const char *titles[] = {"Name", "Type", "Host:Port", "Auth", "Default"}; for (int i = 0; i < 5; i++) { - GtkCellRenderer *r = gtk_cell_renderer_text_new(); - GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes(titles[i], r, "text", i, NULL); + GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes(titles[i], gtk_cell_renderer_text_new(), "text", i, NULL); gtk_tree_view_column_set_resizable(c, TRUE); - if (i == 2) gtk_tree_view_column_set_expand(c, TRUE); + gtk_tree_view_column_set_expand(c, i == 2); gtk_tree_view_append_column(GTK_TREE_VIEW(info.tree), c); } g_signal_connect(info.tree, "row-activated", G_CALLBACK(on_proxy_row_activated), &info); GtkWidget *tree_scroll = gtk_scrolled_window_new(NULL, NULL); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(tree_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_widget_set_size_request(tree_scroll, -1, 180); gtk_widget_set_hexpand(tree_scroll, TRUE); gtk_widget_set_vexpand(tree_scroll, TRUE); gtk_container_add(GTK_CONTAINER(tree_scroll), info.tree); gtk_grid_attach(GTK_GRID(grid), tree_scroll, 0, 0, 4, 1); - // list buttons GtkWidget *btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); - GtkWidget *add_btn = gtk_button_new_with_label("Add"); - GtkWidget *edit_btn = gtk_button_new_with_label("Edit"); - GtkWidget *del_btn = gtk_button_new_with_label("Delete"); - GtkWidget *def_btn = gtk_button_new_with_label("Set Default"); - g_signal_connect(add_btn, "clicked", G_CALLBACK(on_proxy_add), &info); - g_signal_connect(edit_btn, "clicked", G_CALLBACK(on_proxy_edit), &info); - g_signal_connect(del_btn, "clicked", G_CALLBACK(on_proxy_delete), &info); - g_signal_connect(def_btn, "clicked", G_CALLBACK(on_proxy_set_default), &info); - gtk_box_pack_start(GTK_BOX(btn_box), add_btn, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(btn_box), edit_btn, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(btn_box), del_btn, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(btn_box), def_btn, FALSE, FALSE, 0); + const char *labels[] = {"Add", "Edit", "Delete", "Set Default"}; + void (*cbs[])(GtkWidget *, gpointer) = {on_proxy_add, on_proxy_edit, on_proxy_delete, on_proxy_set_default}; + for (int i = 0; i < 4; i++) { + GtkWidget *b = gtk_button_new_with_label(labels[i]); + g_signal_connect(b, "clicked", G_CALLBACK(cbs[i]), &info); + gtk_box_pack_start(GTK_BOX(btn_box), b, FALSE, FALSE, 0); + } gtk_grid_attach(GTK_GRID(grid), btn_box, 0, 1, 4, 1); - - // test section gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 2, 4, 1); GtkWidget *test_label = gtk_label_new("Test Selected Proxy"); @@ -493,61 +405,58 @@ void on_proxy_configure(GtkWidget *widget, gpointer data) { gtk_widget_set_halign(test_label, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), test_label, 0, 3, 4, 1); - // target gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target:"), 0, 4, 1, 1); info.test_host = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(info.test_host), "google.com"); gtk_widget_set_hexpand(info.test_host, TRUE); gtk_grid_attach(GTK_GRID(grid), info.test_host, 1, 4, 1, 1); - - // target port gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Port:"), 2, 4, 1, 1); info.test_port = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(info.test_port), "80"); gtk_widget_set_size_request(info.test_port, 80, -1); gtk_grid_attach(GTK_GRID(grid), info.test_port, 3, 4, 1, 1); - // test button info.test_btn = gtk_button_new_with_label("Start Test"); g_signal_connect(info.test_btn, "clicked", G_CALLBACK(on_start_test_clicked), &info); gtk_grid_attach(GTK_GRID(grid), info.test_btn, 0, 5, 4, 1); - // output log GtkWidget *out_scroll = gtk_scrolled_window_new(NULL, NULL); gtk_widget_set_size_request(out_scroll, -1, 150); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(out_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); - GtkWidget *out_view = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(out_view), FALSE); gtk_text_view_set_monospace(GTK_TEXT_VIEW(out_view), TRUE); info.output_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(out_view)); - gtk_container_add(GTK_CONTAINER(out_scroll), out_view); gtk_grid_attach(GTK_GRID(grid), out_scroll, 0, 6, 4, 1); refresh_proxy_store(&info); - if (g_proxies_list) select_proxy_row(&info, find_proxy_by_name(g_default_proxy) ? find_proxy_by_name(g_default_proxy) : (ProxyData *)g_proxies_list->data); + ProxyData *def = find_proxy_by_name(g_default_proxy); + if (!def && g_proxies_list) def = (ProxyData *)g_proxies_list->data; + select_proxy_row(&info, def); gtk_widget_show_all(info.dialog); gtk_dialog_run(GTK_DIALOG(info.dialog)); gtk_widget_destroy(info.dialog); } -// ---- profiles ---- +typedef struct { + GtkWidget *dialog; + GtkWidget *tree; + GtkListStore *store; +} ProfilesInfo; static void update_profile_ui() { - char *title = g_strdup_printf("ProxyBridge - %s", g_active_profile ? g_active_profile : ""); + char *title = g_strdup_printf("ProxyBridge - %s", g_active_profile); gtk_window_set_title(GTK_WINDOW(window), title); g_free(title); - char *msg = g_strdup_printf("Active profile: %s", g_active_profile ? g_active_profile : ""); - gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, msg); - g_free(msg); + title = g_strdup_printf("Active profile: %s", g_active_profile); + gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, title); + g_free(title); } -// push the active profile to the running engine: no Stop/Start needed +// engine keeps running, only its proxies and rules are replaced static void reapply_active_profile() { ProxyBridge_ClearRules(); - ProxyBridge_ClearProxies(); apply_proxies_to_lib(); apply_rules_to_lib(); save_config(); @@ -555,34 +464,26 @@ static void reapply_active_profile() { update_profile_ui(); } -typedef struct { - GtkWidget *dialog; - GtkWidget *tree; - GtkListStore *store; -} ProfilesInfo; - static void refresh_profile_store(ProfilesInfo *pi) { gtk_list_store_clear(pi->store); for (GList *l = g_profiles; l != NULL; l = l->next) { Profile *p = (Profile *)l->data; - bool act = g_active_profile && strcmp(p->name, g_active_profile) == 0; GtkTreeIter iter; gtk_list_store_append(pi->store, &iter); - gtk_list_store_set(pi->store, &iter, 0, p->name, 1, act ? "Active" : "", -1); + gtk_list_store_set(pi->store, &iter, 0, p->name, 1, strcmp(p->name, g_active_profile) == 0 ? "Active" : "", -1); } } -// name of the selected row, caller frees static char *selected_profile_name(ProfilesInfo *pi) { GtkTreeModel *model; GtkTreeIter iter; - if (!gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(pi->tree)), &model, &iter)) return NULL; char *name = NULL; - gtk_tree_model_get(model, &iter, 0, &name, -1); + if (gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(pi->tree)), &model, &iter)) + gtk_tree_model_get(model, &iter, 0, &name, -1); return name; } -// asks for a profile name, returns NULL on cancel. caller frees +// NULL on cancel static char *ask_profile_name(GtkWindow *parent, const char *title, const char *initial) { GtkWidget *d = gtk_dialog_new_with_buttons(title, parent, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "Cancel", GTK_RESPONSE_CANCEL, "OK", GTK_RESPONSE_ACCEPT, NULL); @@ -590,48 +491,41 @@ static char *ask_profile_name(GtkWindow *parent, const char *title, const char * gtk_container_set_border_width(GTK_CONTAINER(box), 10); GtkWidget *entry = gtk_entry_new(); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); - gtk_entry_set_text(GTK_ENTRY(entry), initial ? initial : ""); + gtk_entry_set_text(GTK_ENTRY(entry), initial); gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Profile name:"), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 5); gtk_dialog_set_default_response(GTK_DIALOG(d), GTK_RESPONSE_ACCEPT); gtk_widget_show_all(d); char *result = NULL; - while (gtk_dialog_run(GTK_DIALOG(d)) == GTK_RESPONSE_ACCEPT) { + while (!result && gtk_dialog_run(GTK_DIALOG(d)) == GTK_RESPONSE_ACCEPT) { char *name = g_strstrip(g_strdup(gtk_entry_get_text(GTK_ENTRY(entry)))); - if (name[0] == '\0') { - show_message(GTK_WINDOW(d), GTK_MESSAGE_ERROR, "Profile name cannot be empty."); - } else if (profile_find(name) && !(initial && strcmp(initial, name) == 0)) { - show_message(GTK_WINDOW(d), GTK_MESSAGE_ERROR, "A profile with this name already exists."); - } else { - result = name; - break; - } + if (!*name) show_message(GTK_WINDOW(d), GTK_MESSAGE_ERROR, "Profile name cannot be empty."); + else if (profile_find(name) && strcmp(initial, name) != 0) show_message(GTK_WINDOW(d), GTK_MESSAGE_ERROR, "A profile with this name already exists."); + else { result = name; continue; } g_free(name); } gtk_widget_destroy(d); return result; } -static void do_switch(ProfilesInfo *pi, const char *name) { - if (!name || (g_active_profile && strcmp(name, g_active_profile) == 0)) return; - if (!profile_activate(name)) return; - reapply_active_profile(); - refresh_profile_store(pi); -} - static void on_profile_switch(GtkWidget *widget, gpointer data) { ProfilesInfo *pi = (ProfilesInfo *)data; char *name = selected_profile_name(pi); - if (name) do_switch(pi, name); + if (name && strcmp(name, g_active_profile) != 0 && profile_activate(name)) { + reapply_active_profile(); + refresh_profile_store(pi); + } g_free(name); } static void on_profile_new(GtkWidget *widget, gpointer data) { ProfilesInfo *pi = (ProfilesInfo *)data; char *name = ask_profile_name(GTK_WINDOW(pi->dialog), "New Profile", ""); - if (!name) return; - if (profile_create(name, false)) { save_config(); refresh_profile_store(pi); } + if (name && profile_create(name, false)) { + save_config(); + refresh_profile_store(pi); + } g_free(name); } @@ -639,22 +533,17 @@ static void on_profile_duplicate(GtkWidget *widget, gpointer data) { ProfilesInfo *pi = (ProfilesInfo *)data; char *src = selected_profile_name(pi); if (!src) return; - // duplicates the selected profile: make it active data-wise only for the copy, then switch back char *initial = g_strdup_printf("%s copy", src); char *name = ask_profile_name(GTK_WINDOW(pi->dialog), "Duplicate Profile", initial); - g_free(initial); - if (name) { - char *prev = g_strdup(g_active_profile); - if (profile_activate(src)) { - profile_create(name, true); - profile_activate(prev); // library state was never touched - save_config(); - refresh_profile_store(pi); - } - g_free(prev); + char *prev = g_strdup(g_active_profile); + // copies the active profile, so make the source active for a moment (library state is untouched) + if (name && profile_activate(src)) { + profile_create(name, true); + profile_activate(prev); + save_config(); + refresh_profile_store(pi); } - g_free(name); - g_free(src); + g_free(prev); g_free(initial); g_free(name); g_free(src); } static void on_profile_rename(GtkWidget *widget, gpointer data) { @@ -675,17 +564,10 @@ static void on_profile_delete(GtkWidget *widget, gpointer data) { ProfilesInfo *pi = (ProfilesInfo *)data; char *name = selected_profile_name(pi); if (!name) return; - if (!g_profiles || !g_profiles->next) { + if (!g_profiles->next) { show_message(GTK_WINDOW(pi->dialog), GTK_MESSAGE_WARNING, "The last profile cannot be deleted."); - g_free(name); - return; - } - GtkWidget *q = gtk_message_dialog_new(GTK_WINDOW(pi->dialog), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, - GTK_BUTTONS_YES_NO, "Delete profile '%s' with all its proxies and rules?", name); - int resp = gtk_dialog_run(GTK_DIALOG(q)); - gtk_widget_destroy(q); - if (resp == GTK_RESPONSE_YES) { - bool was_active = g_active_profile && strcmp(g_active_profile, name) == 0; + } else if (confirm(GTK_WINDOW(pi->dialog), "Delete profile '%s' with all its proxies and rules?", name)) { + bool was_active = strcmp(g_active_profile, name) == 0; if (profile_delete(profile_find(name))) { if (was_active) reapply_active_profile(); else save_config(); @@ -700,8 +582,7 @@ static void on_profile_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTr } void on_profiles_clicked(GtkWidget *widget, gpointer data) { - ProfilesInfo pi; - memset(&pi, 0, sizeof(pi)); + ProfilesInfo pi = {0}; pi.dialog = gtk_dialog_new_with_buttons("Profiles", GTK_WINDOW(window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "Close", GTK_RESPONSE_CLOSE, NULL); gtk_window_set_default_size(GTK_WINDOW(pi.dialog), 420, 360); @@ -714,7 +595,7 @@ void on_profiles_clicked(GtkWidget *widget, gpointer data) { const char *titles[] = {"Profile", "Status"}; for (int i = 0; i < 2; i++) { GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes(titles[i], gtk_cell_renderer_text_new(), "text", i, NULL); - if (i == 0) gtk_tree_view_column_set_expand(c, TRUE); + gtk_tree_view_column_set_expand(c, i == 0); gtk_tree_view_append_column(GTK_TREE_VIEW(pi.tree), c); } g_signal_connect(pi.tree, "row-activated", G_CALLBACK(on_profile_row_activated), &pi); diff --git a/Linux/gui/gui_utils.c b/Linux/gui/gui_utils.c index 35d4a7c1..a8c50b5b 100644 --- a/Linux/gui/gui_utils.c +++ b/Linux/gui/gui_utils.c @@ -1,32 +1,22 @@ #include "gui.h" -// safer way to turn string to int long safe_strtol(const char *nptr) { - if (!nptr) return 0; // null check - char *endptr; - long val = strtol(nptr, &endptr, 10); - if (endptr == nptr) return 0; // bad input - return val; + return nptr ? strtol(nptr, NULL, 10) : 0; } -// show popup msg void show_message(GtkWindow *parent, GtkMessageType type, const char *format, ...) { va_list args; va_start(args, format); char *msg = g_strdup_vprintf(format, args); va_end(args); - GtkWidget *dialog = gtk_message_dialog_new(parent, - GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, - type, - GTK_BUTTONS_OK, - "%s", msg); + GtkWidget *dialog = gtk_message_dialog_new(parent, GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, + type, GTK_BUTTONS_OK, "%s", msg); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_free(msg); } -// keep log size under control so we dont use too much ram void trim_buffer_lines(GtkTextBuffer *buffer, int max_lines) { if (gtk_text_buffer_get_line_count(buffer) > max_lines) { GtkTextIter start, next; @@ -37,82 +27,43 @@ void trim_buffer_lines(GtkTextBuffer *buffer, int max_lines) { } } -char* get_current_time_str() { - time_t rawtime; - struct tm *timeinfo; - char *buffer = g_malloc0(32); - time(&rawtime); - timeinfo = localtime(&rawtime); - if (!timeinfo || strftime(buffer, 32, "[%H:%M:%S]", timeinfo) == 0) buffer[0] = '\0'; - return buffer; // free with g_free +char *get_current_time_str() { + GDateTime *now = g_date_time_new_now_local(); + char *s = g_date_time_format(now, "[%H:%M:%S]"); + g_date_time_unref(now); + return s; } -// json stuff -char *escape_json_string(const char *src) { - if (!src) return g_strdup(""); - GString *str = g_string_new(""); - for (const char *p = src; *p; p++) { - if (*p == '\\') g_string_append(str, "\\\\"); - else if (*p == '"') g_string_append(str, "\\\""); - else if (*p == '\n') g_string_append(str, "\\n"); - else if (*p == '\r') g_string_append(str, "\\r"); - else if (*p == '\t') g_string_append(str, "\\t"); - else if ((unsigned char)*p < 0x20) g_string_append_printf(str, "\\u%04x", (unsigned char)*p); - else g_string_append_c(str, *p); - } - return g_string_free(str, FALSE); +GtkWidget *combo_new(const char *first, ...) { + GtkWidget *combo = gtk_combo_box_text_new(); + va_list args; + va_start(args, first); + for (const char *s = first; s; s = va_arg(args, const char *)) + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo), s); + va_end(args); + return combo; } -// basic parser, good enough for valid inputs. returns NULL if the key is missing or its value is not a string -char *extract_sub_json_str(const char *json, const char *key) { - char search_key[256]; - snprintf(search_key, sizeof(search_key), "\"%s\"", key); - const char *k = strstr(json, search_key); - if (!k) return NULL; - const char *colon = strchr(k + strlen(search_key), ':'); - if (!colon) return NULL; - const char *v = colon + 1; - while (*v == ' ' || *v == '\t' || *v == '\n' || *v == '\r') v++; - if (*v != '"') return NULL; - v++; - - // copy up to the closing quote, handling escapes - GString *out = g_string_new(""); - while (*v && *v != '"') { - if (*v == '\\' && v[1]) { - v++; - switch (*v) { - case 'n': g_string_append_c(out, '\n'); break; - case 'r': g_string_append_c(out, '\r'); break; - case 't': g_string_append_c(out, '\t'); break; - default: g_string_append_c(out, *v); break; // \\ \" \/ - } - } else { - g_string_append_c(out, *v); - } - v++; - } - if (*v != '"') { g_string_free(out, TRUE); return NULL; } - return g_string_free(out, FALSE); +GtkWidget *hint_label(const char *text) { + GtkWidget *l = gtk_label_new(text); + gtk_style_context_add_class(gtk_widget_get_style_context(l), "dim-label"); + gtk_widget_set_halign(l, GTK_ALIGN_START); + gtk_label_set_xalign(GTK_LABEL(l), 0); + gtk_label_set_line_wrap(GTK_LABEL(l), TRUE); + gtk_label_set_max_width_chars(GTK_LABEL(l), 60); + return l; } -// true if the key is present at all (extract_sub_json_bool cant tell false from missing) -bool json_has_key(const char *json, const char *key) { - char search_key[256]; - snprintf(search_key, sizeof(search_key), "\"%s\"", key); - return strstr(json, search_key) != NULL; +void set_error_label(GtkWidget *label, const char *msg) { + char *m = g_markup_printf_escaped("%s", msg); + gtk_label_set_markup(GTK_LABEL(label), m); + g_free(m); } -bool extract_sub_json_bool(const char *json, const char *key) { - char search_key[256]; - snprintf(search_key, sizeof(search_key), "\"%s\"", key); - char *k = strstr(json, search_key); - if (!k) return false; - char *colon = strchr(k, ':'); - if (!colon) return false; - // skip whitespace - char *v = colon + 1; - while(*v == ' ' || *v == '\t' || *v == '\n' || *v == '\r') v++; - if (strncmp(v, "true", 4) == 0) return true; - return false; +void grid_row(GtkWidget *grid, int row, const char *label, GtkWidget *widget) { + GtkWidget *l = gtk_label_new(label); + gtk_widget_set_halign(l, GTK_ALIGN_START); + gtk_grid_attach(GTK_GRID(grid), l, 0, row, 1, 1); + gtk_widget_set_hexpand(widget, TRUE); + gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 1, 1); } diff --git a/Linux/gui/main.c b/Linux/gui/main.c index 9887cc0b..f2b8cc85 100644 --- a/Linux/gui/main.c +++ b/Linux/gui/main.c @@ -1,19 +1,15 @@ #include "gui.h" #include +#include -// widgets GtkWidget *window; -GtkTextView *conn_view; GtkTextBuffer *conn_buffer; -GtkTextView *log_view; GtkTextBuffer *log_buffer; GtkWidget *status_bar; guint status_context_id; -// config lives in gui_settings.c (proxies) and gui_rules.c (rules) static bool g_engine_running = false; -// default config GList *g_rules_list = NULL; bool g_chk_logging = true; bool g_chk_dns = true; @@ -116,7 +112,6 @@ static void on_check_update(GtkWidget *widget, gpointer data) { GError *spawn_err = NULL; int exit_code = 0; - // feed only, no deploy.sh. open the linux download/notes url in the browser if (!g_spawn_sync(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &json, &curl_err, &exit_code, &spawn_err)) { show_message(GTK_WINDOW(window), GTK_MESSAGE_ERROR, "Update check failed: %s", @@ -227,12 +222,11 @@ static void on_window_destroy(GtkWidget *widget, gpointer data) { gtk_main_quit(); // engine is stopped once after gtk_main returns } -// start the engine, config (proxies, rules) was already pushed to the library static bool start_engine() { if (g_engine_running) return true; if (ProxyBridge_Start()) { g_engine_running = true; - char *msg = g_strdup_printf("ProxyBridge Service Started (eBPF). Active profile: %s", g_active_profile ? g_active_profile : "Default"); + char *msg = g_strdup_printf("ProxyBridge Service Started (eBPF). Active profile: %s", g_active_profile); gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, msg); g_free(msg); return true; @@ -249,20 +243,72 @@ static void on_start_engine(GtkWidget *widget, gpointer data) { start_engine(); } +static GtkWidget *add_menu_item(GtkWidget *menu, const char *label, GCallback cb) { + GtkWidget *item = gtk_menu_item_new_with_label(label); + g_signal_connect(item, "activate", cb, NULL); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); + return item; +} + +static void add_check_item(GtkWidget *menu, const char *label, bool active, GCallback cb) { + GtkWidget *item = gtk_check_menu_item_new_with_label(label); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), active); + g_signal_connect(item, "toggled", cb, NULL); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); +} + +static void add_separator(GtkWidget *menu) { + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); +} + +static GtkWidget *add_submenu(GtkWidget *menubar, const char *label) { + GtkWidget *item = gtk_menu_item_new_with_label(label); + GtkWidget *menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), menu); + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), item); + return menu; +} + +static GtkTextBuffer *add_text_tab(GtkWidget *notebook, const char *title, const char *placeholder, GCallback on_search, GCallback on_clear) { + GtkWidget *tab = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + gtk_container_set_border_width(GTK_CONTAINER(tab), 5); + GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + GtkWidget *search = gtk_search_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(search), placeholder); + g_signal_connect(search, "search-changed", on_search, NULL); + GtkWidget *clear_btn = gtk_button_new_with_label("Clear Logs"); + g_signal_connect(clear_btn, "clicked", on_clear, NULL); + gtk_box_pack_start(GTK_BOX(toolbar), search, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(toolbar), clear_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(tab), toolbar, FALSE, FALSE, 0); + + GtkWidget *view = gtk_text_view_new(); + gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE); + gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(view), FALSE); + GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view)); + gtk_text_buffer_create_tag(buffer, "hidden", "invisible", TRUE, NULL); + GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL); + gtk_container_add(GTK_CONTAINER(scrolled), view); + gtk_box_pack_start(GTK_BOX(tab), scrolled, TRUE, TRUE, 0); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), tab, gtk_label_new(title)); + return buffer; +} + int main(int argc, char *argv[]) { - if (getuid() != 0) { gtk_init(&argc, &argv); show_message(NULL, GTK_MESSAGE_ERROR, "ProxyBridge must be run as root (sudo)."); return 1; } + if (getuid() != 0) { + gtk_init(&argc, &argv); + show_message(NULL, GTK_MESSAGE_ERROR, "ProxyBridge must be run as root (sudo)."); + return 1; + } setenv("GSETTINGS_BACKEND", "memory", 1); - - // load config from file load_config(); - gtk_init(&argc, &argv); GtkSettings *settings = gtk_settings_get_default(); if (settings) g_object_set(settings, "gtk-application-prefer-dark-theme", TRUE, NULL); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - char *win_title = g_strdup_printf("ProxyBridge - %s", g_active_profile ? g_active_profile : "Default"); + char *win_title = g_strdup_printf("ProxyBridge - %s", g_active_profile); gtk_window_set_title(GTK_WINDOW(window), win_title); g_free(win_title); gtk_window_set_default_size(GTK_WINDOW(window), 900, 600); @@ -271,112 +317,31 @@ int main(int argc, char *argv[]) { GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(window), vbox); - // setup menu GtkWidget *menubar = gtk_menu_bar_new(); - GtkWidget *proxy_menu_item = gtk_menu_item_new_with_label("Proxy"); - GtkWidget *proxy_menu = gtk_menu_new(); - GtkWidget *config_item = gtk_menu_item_new_with_label("Proxy Settings..."); - GtkWidget *rules_item = gtk_menu_item_new_with_label("Proxy Rules..."); - - GtkWidget *profiles_item = gtk_menu_item_new_with_label("Profiles..."); - g_signal_connect(profiles_item, "activate", G_CALLBACK(on_profiles_clicked), NULL); - - GtkWidget *log_check_item = gtk_check_menu_item_new_with_label("Enable Traffic Logging"); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(log_check_item), g_chk_logging); - g_signal_connect(log_check_item, "toggled", G_CALLBACK(on_log_traffic_toggled), NULL); - - GtkWidget *dns_check_item = gtk_check_menu_item_new_with_label("DNS via Proxy"); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(dns_check_item), g_chk_dns); - g_signal_connect(dns_check_item, "toggled", G_CALLBACK(on_dns_proxy_toggled), NULL); - - GtkWidget *start_item = gtk_menu_item_new_with_label("Start Engine"); - g_signal_connect(start_item, "activate", G_CALLBACK(on_start_engine), NULL); - - GtkWidget *exit_item = gtk_menu_item_new_with_label("Exit"); - - g_signal_connect(config_item, "activate", G_CALLBACK(on_proxy_configure), NULL); - g_signal_connect(rules_item, "activate", G_CALLBACK(on_proxy_rules_clicked), NULL); - g_signal_connect(exit_item, "activate", G_CALLBACK(on_window_destroy), NULL); - - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), config_item); - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), rules_item); - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), profiles_item); - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), gtk_separator_menu_item_new()); - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), log_check_item); - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), dns_check_item); - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), gtk_separator_menu_item_new()); - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), start_item); - gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), exit_item); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(proxy_menu_item), proxy_menu); - - GtkWidget *about_menu_item = gtk_menu_item_new_with_label("About"); - GtkWidget *about_menu = gtk_menu_new(); - GtkWidget *about_child_item = gtk_menu_item_new_with_label("About"); - g_signal_connect(about_child_item, "activate", G_CALLBACK(on_about), NULL); - GtkWidget *update_item = gtk_menu_item_new_with_label("Check for Updates"); - g_signal_connect(update_item, "activate", G_CALLBACK(on_check_update), NULL); - gtk_menu_shell_append(GTK_MENU_SHELL(about_menu), about_child_item); - gtk_menu_shell_append(GTK_MENU_SHELL(about_menu), update_item); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(about_menu_item), about_menu); - - gtk_menu_shell_append(GTK_MENU_SHELL(menubar), proxy_menu_item); - gtk_menu_shell_append(GTK_MENU_SHELL(menubar), about_menu_item); + GtkWidget *proxy_menu = add_submenu(menubar, "Proxy"); + add_menu_item(proxy_menu, "Proxy Settings...", G_CALLBACK(on_proxy_configure)); + add_menu_item(proxy_menu, "Proxy Rules...", G_CALLBACK(on_proxy_rules_clicked)); + add_menu_item(proxy_menu, "Profiles...", G_CALLBACK(on_profiles_clicked)); + add_separator(proxy_menu); + add_check_item(proxy_menu, "Enable Traffic Logging", g_chk_logging, G_CALLBACK(on_log_traffic_toggled)); + add_check_item(proxy_menu, "DNS via Proxy", g_chk_dns, G_CALLBACK(on_dns_proxy_toggled)); + add_separator(proxy_menu); + add_menu_item(proxy_menu, "Start Engine", G_CALLBACK(on_start_engine)); + add_menu_item(proxy_menu, "Exit", G_CALLBACK(on_window_destroy)); + GtkWidget *about_menu = add_submenu(menubar, "About"); + add_menu_item(about_menu, "About", G_CALLBACK(on_about)); + add_menu_item(about_menu, "Check for Updates", G_CALLBACK(on_check_update)); gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 0); - // tabs GtkWidget *notebook = gtk_notebook_new(); gtk_box_pack_start(GTK_BOX(vbox), notebook, TRUE, TRUE, 0); + conn_buffer = add_text_tab(notebook, "Connections", "Search connections...", G_CALLBACK(on_search_conn_changed), G_CALLBACK(on_clear_conn_clicked)); + log_buffer = add_text_tab(notebook, "Activity Logs", "Search logs...", G_CALLBACK(on_search_log_changed), G_CALLBACK(on_clear_log_clicked)); - // connections tab - GtkWidget *conn_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); - gtk_container_set_border_width(GTK_CONTAINER(conn_vbox), 5); - GtkWidget *conn_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); - GtkWidget *conn_search = gtk_search_entry_new(); - gtk_entry_set_placeholder_text(GTK_ENTRY(conn_search), "Search connections..."); - g_signal_connect(conn_search, "search-changed", G_CALLBACK(on_search_conn_changed), NULL); - GtkWidget *conn_clear_btn = gtk_button_new_with_label("Clear Logs"); - g_signal_connect(conn_clear_btn, "clicked", G_CALLBACK(on_clear_conn_clicked), NULL); - gtk_box_pack_start(GTK_BOX(conn_toolbar), conn_search, TRUE, TRUE, 0); - gtk_box_pack_start(GTK_BOX(conn_toolbar), conn_clear_btn, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(conn_vbox), conn_toolbar, FALSE, FALSE, 0); - conn_view = GTK_TEXT_VIEW(gtk_text_view_new()); - gtk_text_view_set_editable(conn_view, FALSE); - gtk_text_view_set_cursor_visible(conn_view, FALSE); - conn_buffer = gtk_text_view_get_buffer(conn_view); - gtk_text_buffer_create_tag(conn_buffer, "hidden", "invisible", TRUE, NULL); - GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL); - gtk_container_add(GTK_CONTAINER(scrolled_window), GTK_WIDGET(conn_view)); - gtk_box_pack_start(GTK_BOX(conn_vbox), scrolled_window, TRUE, TRUE, 0); - gtk_notebook_append_page(GTK_NOTEBOOK(notebook), conn_vbox, gtk_label_new("Connections")); - - // logs tab - GtkWidget *log_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); - gtk_container_set_border_width(GTK_CONTAINER(log_vbox), 5); - GtkWidget *log_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); - GtkWidget *log_search = gtk_search_entry_new(); - gtk_entry_set_placeholder_text(GTK_ENTRY(log_search), "Search logs..."); - g_signal_connect(log_search, "search-changed", G_CALLBACK(on_search_log_changed), NULL); - GtkWidget *log_clear_btn = gtk_button_new_with_label("Clear Logs"); - g_signal_connect(log_clear_btn, "clicked", G_CALLBACK(on_clear_log_clicked), NULL); - gtk_box_pack_start(GTK_BOX(log_toolbar), log_search, TRUE, TRUE, 0); - gtk_box_pack_start(GTK_BOX(log_toolbar), log_clear_btn, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(log_vbox), log_toolbar, FALSE, FALSE, 0); - log_view = GTK_TEXT_VIEW(gtk_text_view_new()); - gtk_text_view_set_editable(log_view, FALSE); - gtk_text_view_set_cursor_visible(log_view, FALSE); - log_buffer = gtk_text_view_get_buffer(log_view); - gtk_text_buffer_create_tag(log_buffer, "hidden", "invisible", TRUE, NULL); - GtkWidget *log_scroll = gtk_scrolled_window_new(NULL, NULL); - gtk_container_add(GTK_CONTAINER(log_scroll), GTK_WIDGET(log_view)); - gtk_box_pack_start(GTK_BOX(log_vbox), log_scroll, TRUE, TRUE, 0); - gtk_notebook_append_page(GTK_NOTEBOOK(notebook), log_vbox, gtk_label_new("Activity Logs")); - - // status bar status_bar = gtk_statusbar_new(); status_context_id = gtk_statusbar_get_context_id(GTK_STATUSBAR(status_bar), "Status"); gtk_box_pack_start(GTK_BOX(vbox), status_bar, FALSE, FALSE, 0); - // setup: callbacks, options, proxies, default proxy, rules, then start ProxyBridge_SetLogCallback(lib_log_callback); ProxyBridge_SetConnectionCallback(lib_connection_callback); ProxyBridge_SetTrafficLoggingEnabled(g_chk_logging); @@ -391,10 +356,7 @@ int main(int argc, char *argv[]) { start_engine(); gtk_main(); - if (g_engine_running) { - ProxyBridge_Stop(); - g_engine_running = false; - } - + if (g_engine_running) ProxyBridge_Stop(); + config_free_all(); return 0; } diff --git a/Linux/gui/tests/config_test.c b/Linux/gui/tests/config_test.c index bf02b711..ffd66599 100644 --- a/Linux/gui/tests/config_test.c +++ b/Linux/gui/tests/config_test.c @@ -1,6 +1,7 @@ -// root-free test of gui_config.c (profiles, migration, round trip) -// build: gcc -I../../src -I.. -D_GNU_SOURCE $(pkg-config --cflags gtk+-3.0) config_test.c ../gui_config.c -o config_test $(pkg-config --libs gtk+-3.0) +// root-free test of gui_config.c: profiles, migration, round trip, leak/heap stress +// build: see run_tests.sh #include "gui.h" +#include GList *g_proxies_list = NULL; char *g_default_proxy = NULL; @@ -9,54 +10,58 @@ bool g_chk_logging = true; bool g_chk_dns = true; void free_proxy_data(ProxyData *p) { - if (!p) return; g_free(p->name); g_free(p->host); g_free(p->username); g_free(p->password); g_free(p); } + void free_rule_data(RuleData *r) { - if (!r) return; g_free(r->process_name); g_free(r->target_hosts); g_free(r->target_ports); g_free(r->proxy_name); g_free(r); } static int failures = 0; #define CHECK(c) do { if (!(c)) { printf("FAIL line %d: %s\n", __LINE__, #c); failures++; } } while (0) -static void reset_state() { - g_profiles = NULL; g_active_profile = NULL; - g_proxies_list = NULL; g_rules_list = NULL; g_default_proxy = NULL; - g_chk_logging = true; g_chk_dns = true; -} - static void write_file(const char *dir, const char *content) { char *path = g_strdup_printf("%s/config.ini", dir); g_file_set_contents(path, content, -1, NULL); g_free(path); } +// 0 when the allocator is intercepted (ASan), LeakSanitizer covers that case +static size_t heap_used() { + return mallinfo2().uordblks; +} + +static void add_content(const char *proxy_name) { + ProxyData *px = g_malloc0(sizeof(ProxyData)); + px->name = g_strdup(proxy_name); px->host = g_strdup("github.com"); px->port = 8080; + px->username = g_strdup(""); px->password = g_strdup("s3cr|et"); + g_proxies_list = g_list_append(g_proxies_list, px); + g_default_proxy = g_strdup(proxy_name); + RuleData *r = g_malloc0(sizeof(RuleData)); + r->process_name = g_strdup("curl"); r->target_hosts = g_strdup("*.github.com"); r->target_ports = g_strdup("*"); + r->action = RULE_ACTION_PROXY; r->proxy_name = g_strdup(proxy_name); r->enabled = true; + g_rules_list = g_list_append(g_rules_list, r); +} + int main() { char *dir = g_dir_make_tmp("pbcfg-XXXXXX", NULL); CHECK(dir != NULL); setenv("PROXYBRIDGE_CONFIG_DIR", dir, 1); - // 1. no file -> one "Default" profile - reset_state(); load_config(); - CHECK(g_list_length(g_profiles) == 1); - CHECK(strcmp(g_active_profile, "Default") == 0); + CHECK(g_list_length(g_profiles) == 1 && strcmp(g_active_profile, "Default") == 0); + config_free_all(); - // 2. old single-proxy format + // old single-proxy format write_file(dir, "[SETTINGS]\nip=1.2.3.4\nport=1080\ntype=1\nuser=u\npass=p|w\nlogging=0\ndns=1\n[RULES]\n3|2|0|1|firefox|*|443;80|\n"); - reset_state(); load_config(); CHECK(strcmp(g_active_profile, "Default") == 0); CHECK(g_list_length(g_proxies_list) == 1); - CHECK(strcmp(((ProxyData *)g_proxies_list->data)->name, "default") == 0); CHECK(strcmp(((ProxyData *)g_proxies_list->data)->host, "1.2.3.4") == 0); CHECK(strcmp(g_default_proxy, "default") == 0); - CHECK(g_list_length(g_rules_list) == 1); - CHECK(strcmp(((RuleData *)g_rules_list->data)->target_ports, "443;80") == 0); + CHECK(g_list_length(g_rules_list) == 1 && strcmp(((RuleData *)g_rules_list->data)->target_ports, "443;80") == 0); CHECK(!g_chk_logging); - // 3. profiles: create, add data, rename, switch, save, reload CHECK(profile_create("Work", false) != NULL); CHECK(profile_create("Work", false) == NULL); CHECK(profile_create("", false) == NULL); @@ -66,15 +71,7 @@ int main() { CHECK(!profile_rename(profile_find("Home"), "Work")); CHECK(profile_activate("Work")); CHECK(g_proxies_list == NULL && g_rules_list == NULL); - ProxyData *px = g_malloc0(sizeof(ProxyData)); - px->name = g_strdup("p 1|x"); px->host = g_strdup("github.com"); px->port = 8080; - px->username = g_strdup(""); px->password = g_strdup("s3cr|et"); - g_proxies_list = g_list_append(g_proxies_list, px); - g_default_proxy = g_strdup("p 1|x"); - RuleData *r = g_malloc0(sizeof(RuleData)); - r->process_name = g_strdup("curl"); r->target_hosts = g_strdup("*.github.com"); r->target_ports = g_strdup("*"); - r->protocol = RULE_PROTOCOL_BOTH; r->action = RULE_ACTION_PROXY; r->proxy_name = g_strdup("p 1|x"); r->enabled = true; - g_rules_list = g_list_append(g_rules_list, r); + add_content("p 1|x"); g_chk_dns = false; save_config(); @@ -83,25 +80,54 @@ int main() { CHECK(stat(path, &st) == 0 && (st.st_mode & 0777) == 0600); g_free(path); - reset_state(); + config_free_all(); load_config(); - CHECK(g_list_length(g_profiles) == 3); - CHECK(strcmp(g_active_profile, "Work") == 0); - CHECK(g_list_length(g_proxies_list) == 1); + CHECK(g_list_length(g_profiles) == 3 && strcmp(g_active_profile, "Work") == 0); CHECK(strcmp(((ProxyData *)g_proxies_list->data)->name, "p 1|x") == 0); CHECK(strcmp(((ProxyData *)g_proxies_list->data)->password, "s3cr|et") == 0); CHECK(strcmp(((RuleData *)g_rules_list->data)->target_hosts, "*.github.com") == 0); - CHECK(strcmp(((RuleData *)g_rules_list->data)->proxy_name, "p 1|x") == 0); CHECK(!g_chk_dns && !g_chk_logging); CHECK(profile_find("Home") && g_list_length(profile_find("Home")->rules) == 1); - // 4. delete: non-last allowed, active one falls back, last refused CHECK(profile_delete(profile_find("Work"))); CHECK(strcmp(g_active_profile, "Default") == 0 && g_list_length(g_rules_list) == 1); CHECK(profile_delete(profile_find("Home"))); CHECK(!profile_delete(profile_find("Default"))); CHECK(g_list_length(g_profiles) == 1); + save_config(); + config_free_all(); + + // stress: heap must return to baseline + load_config(); + for (int i = 0; i < 200; i++) { // warm up allocator caches + Profile *p = profile_create("w", true); + if (p) profile_delete(p); + } + size_t base = heap_used(); + for (int i = 0; i < 20000; i++) { + char *a = g_strdup_printf("p%d", i), *b = g_strdup_printf("q%d", i), *c = g_strdup_printf("c%d", i); + CHECK(profile_create(a, false) != NULL); + CHECK(profile_activate(a)); + add_content("px"); + CHECK(profile_rename(profile_find(a), b)); + CHECK(profile_create(c, true) != NULL); + save_config(); + if (i % 1000 == 0) { + config_free_all(); + load_config(); + } + CHECK(profile_activate("Default")); + CHECK(profile_delete(profile_find(b))); + CHECK(profile_delete(profile_find(c))); + g_free(a); g_free(b); g_free(c); + } + size_t after = heap_used(); + printf("heap before=%zu after=%zu\n", base, after); + CHECK(base == 0 || after < base + 64 * 1024); + CHECK(g_list_length(g_profiles) == 1); - if (failures == 0) printf("config_test: all passed\n"); + config_free_all(); + g_free(dir); + if (!failures) printf("config_test: all passed\n"); return failures ? 1 : 0; } diff --git a/Linux/gui/tests/json_test.c b/Linux/gui/tests/json_test.c new file mode 100644 index 00000000..e1d0ef00 --- /dev/null +++ b/Linux/gui/tests/json_test.c @@ -0,0 +1,83 @@ +// hostile-input test of the rules json parser/writer (gui_json.c) +#include "gui.h" + +void free_rule_data(RuleData *r) { + g_free(r->process_name); g_free(r->target_hosts); g_free(r->target_ports); g_free(r->proxy_name); g_free(r); +} + +static int failures = 0; +#define CHECK(c) do { if (!(c)) { printf("FAIL line %d: %s\n", __LINE__, #c); failures++; } } while (0) + +typedef struct { int n, named, disabled; RuleAction last_action; RuleProtocol last_proto; size_t max_len; } Stats; + +static void cb(const RuleData *r, int index, void *ud) { + Stats *s = (Stats *)ud; + s->n++; + if (r->process_name) { + s->named++; + if (strlen(r->process_name) > s->max_len) s->max_len = strlen(r->process_name); + } + if (!r->enabled) s->disabled++; + s->last_action = r->action; + s->last_proto = r->protocol; +} + +static Stats parse(const char *json) { + Stats s = {0}; + rules_from_json(json, cb, &s); + return s; +} + +int main() { + Stats s = parse("[{\"processNames\":\"a\",\"action\":\"DIRECT\",\"protocol\":\"udp\",\"enabled\":false}]"); + CHECK(s.n == 1 && s.named == 1 && s.disabled == 1 && s.last_action == RULE_ACTION_DIRECT && s.last_proto == RULE_PROTOCOL_UDP); + + // macOS style: proxy uuid as action, ruleProtocol/ruleAction keys + s = parse("{\"processNames\":\"a\",\"ruleAction\":\"6F1C-uuid\",\"ruleProtocol\":\"TCP\"}"); + CHECK(s.n == 1 && s.last_action == RULE_ACTION_PROXY && s.last_proto == RULE_PROTOCOL_TCP && s.disabled == 0); + + // hostile small inputs must not crash + const char *bad[] = {"", "{", "}", "{{{{", "{\"processNames\":", "{\"processNames\":\"unterminated", "{\"processNames\":5}", + "{\"processNames\":null,\"enabled\":\"yes\"}", "{\"a\":\"}\"}", "[{\"processNames\":\"x\\", "{\"processNames\":\"x\\\"}", NULL}; + for (int i = 0; bad[i]; i++) parse(bad[i]); + s = parse("{\"processNames\":\"a}b\"}"); + CHECK(s.n == 1 && s.max_len == 3); + s = parse("{\"processNames\":5}"); + CHECK(s.n == 1 && s.named == 0); + + // huge string + size_t big = 20 * 1024 * 1024; + char *json = g_malloc(big + 64); + strcpy(json, "[{\"processNames\":\""); + memset(json + strlen(json), 'A', big); + strcpy(json + 18 + big, "\"}]"); + s = parse(json); + CHECK(s.n == 1 && s.max_len == big); + g_free(json); + + GString *deep = g_string_new(""); + for (int i = 0; i < 1000000; i++) g_string_append_c(deep, '{'); + parse(deep->str); + g_string_free(deep, TRUE); + + // 100k rules round trip through the writer + GList *rules = NULL; + for (int i = 0; i < 100000; i++) { + RuleData *r = g_malloc0(sizeof(RuleData)); + r->process_name = g_strdup_printf("proc\"%d\\", i); + r->target_hosts = g_strdup("*.example.com;10.0.0.0/8"); + r->target_ports = g_strdup("80;443"); + r->protocol = i % 3; r->action = i % 3; r->enabled = i % 2; + r->proxy_name = i % 3 == 0 ? g_strdup("px") : NULL; + r->selected = true; + rules = g_list_append(rules, r); + } + char *out = rules_to_json(rules, true); + s = parse(out); + CHECK(s.n == 100000 && s.named == 100000 && s.disabled == 50000); + g_free(out); + g_list_free_full(rules, (GDestroyNotify)free_rule_data); + + if (!failures) printf("json_test: all passed\n"); + return failures ? 1 : 0; +} diff --git a/Linux/gui/tests/run_tests.sh b/Linux/gui/tests/run_tests.sh new file mode 100755 index 00000000..a950ef92 --- /dev/null +++ b/Linux/gui/tests/run_tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# usage: tests/run_tests.sh [outdir] (root-free) +set -e +cd "$(dirname "$0")/.." +OUT="${1:-$(mktemp -d)}" +CF="-Wall -Wextra -Wno-unused-parameter -g -fsanitize=address,undefined -I../src -I. -D_GNU_SOURCE $(pkg-config --cflags gtk+-3.0)" +LF="$(pkg-config --libs gtk+-3.0)" +gcc $CF tests/config_test.c gui_config.c -o "$OUT/config_test" $LF +gcc $CF tests/json_test.c gui_json.c -o "$OUT/json_test" $LF +gcc -O1 -I../src -I. -D_GNU_SOURCE $(pkg-config --cflags gtk+-3.0) tests/config_test.c gui_config.c -o "$OUT/config_plain" $LF +"$OUT/config_plain" +ASAN_OPTIONS=detect_leaks=1 "$OUT/config_test" +ASAN_OPTIONS=detect_leaks=1 "$OUT/json_test" From bdeb45833567a4fbf5bb9ab422b910d791a89d5f Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 15:47:28 +0530 Subject: [PATCH 20/25] test(linux): security, churn and exemption tests; document the security model - e2e_security.sh plants a pending flow in the real kernel map and checks that a connection from 127.0.0.2 is refused, the owner still works, and an entry cannot be replayed - churn.sh / churn_test.c: thousands of add/edit/move/disable/delete operations on rules and proxies plus restarts under live traffic; in-use heap, descriptors and threads must stay flat - e2e_nexus.sh no longer needs a DIRECT rule for the proxy's own process; a second node process that is not the proxy must still be proxied - e2e_rules.sh checks the UDP-over-HTTP-proxy warning; api_live.sh and the CI end-to-end job gain the new tests - README: security model, automatic proxy exemption, UDP over HTTP behaviour --- .github/workflows/build-linux.yml | 2 +- Linux/README.md | 13 ++++- Linux/tests/api_live.sh | 2 +- Linux/tests/churn.sh | 17 +++++++ Linux/tests/churn_test.c | 82 +++++++++++++++++++++++++++++++ Linux/tests/e2e_nexus.sh | 8 ++- Linux/tests/e2e_rules.sh | 3 ++ Linux/tests/e2e_security.sh | 58 ++++++++++++++++++++++ Linux/tests/hookbench.sh | 2 +- 9 files changed, 182 insertions(+), 5 deletions(-) create mode 100755 Linux/tests/churn.sh create mode 100644 Linux/tests/churn_test.c create mode 100755 Linux/tests/e2e_security.sh diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 48c223c4..83c9c004 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -164,7 +164,7 @@ jobs: uname -r ls /sys/kernel/btf/vmlinux && mount | grep -q "type cgroup2" || { echo "kernel lacks BTF or cgroup2: skipping"; exit 0; } fail=0 - for t in e2e e2e_rules e2e_domains api_live; do + for t in e2e e2e_rules e2e_domains e2e_security api_live; do echo "::group::$t" sudo env PB_DIR="$PWD/Linux/output" BPFTOOL="$BPFTOOL" bash "Linux/tests/$t.sh" || fail=1 echo "::endgroup::" diff --git a/Linux/README.md b/Linux/README.md index f2c2353e..a47d9124 100644 --- a/Linux/README.md +++ b/Linux/README.md @@ -258,6 +258,7 @@ Rules are evaluated **in order and the first match wins** (put specific rules be - Process names are case-sensitive on Linux - Interpreted programs (python, node, java) are identified by the interpreter binary, e.g. `python3.12` - A `PROXY` rule whose proxy does not exist is treated as `BLOCK` (traffic never leaks around a missing proxy) +- A local proxy is never proxied through itself: processes that listen on a configured proxy address (for example Burp, mitmproxy, Tor or `ssh -D` on 127.0.0.1) are exempted automatically, so a rule with process `*` cannot create a loop. The exemption is per process, not per program, and is refreshed every two seconds - Press `Ctrl+C` to stop ProxyBridge; the kernel hooks are removed automatically, even if ProxyBridge crashes ## Domain rules @@ -281,6 +282,14 @@ Cost: DNS from covered processes takes one extra loopback hop (about +90 µs per Add as many `--proxy` entries (CLI) or proxies (GUI) as you need; each rule chooses its own with `PROXY=name`. Proxy hosts may be IP addresses or **host names** (resolved with a 5 s deadline and a cache, so a slow resolver cannot stall traffic). A rule that references a proxy that no longer exists blocks instead of leaking. +## Security model + +- **Redirected TCP connections** are bound to the client's source address and port. A process that connects to the relay from a different loopback address (for example `127.0.0.2`) cannot claim another process's pending flow, and an entry is consumed on first use. +- **Redirected UDP flows** are addressed by a loopback flow address that the relay maps back to the original destination. These addresses are not secret (they are visible in `/proc/net/udp`), so on a **multi-user host** another local user can send datagrams to the relay's UDP port and have them forwarded to a destination that some other process is already talking to, through the same proxy. Run ProxyBridge on machines you trust locally. +- A rule that needs a proxy that does not exist **blocks** instead of letting traffic through. +- The relay listens on loopback only (UDP is additionally bound to the `lo` device). Other network namespaces (containers) are never redirected. +- Proxy passwords are kept in `/etc/proxybridge/config.ini` (mode 0600, root). On the command line, `--proxy user:pass@host` is wiped from the process list after parsing, but a shell history can still keep it; prefer the GUI or a config file for secrets. + ## Use Cases - Redirect proxy-unaware applications (games, desktop apps) through InterceptSuite/Burp Suite for security testing @@ -314,7 +323,7 @@ Add as many `--proxy` entries (CLI) or proxies (GUI) as you need; each rule choo These addresses and ports are used by system components, network discovery, and essential Linux services. -- **UDP Proxy Requirements**: UDP traffic only works when a SOCKS5 proxy is configured. If an HTTP proxy server is configured, ProxyBridge will ignore UDP proxy rules and route UDP traffic as direct connection instead. This limitation does not affect UDP rules with BLOCK or DIRECT actions. +- **UDP Proxy Requirements**: UDP traffic only works through a SOCKS5 proxy. A rule that sends UDP to an HTTP proxy cannot be honoured, so that UDP traffic goes DIRECT; ProxyBridge logs a warning naming the rule when it is created or the engine starts. Use a SOCKS5 proxy, or a TCP-only rule plus a separate `UDP ... BLOCK` rule, if direct UDP is not acceptable. This limitation does not affect UDP rules with BLOCK or DIRECT actions. **Important UDP Considerations**: - Configuring a SOCKS5 proxy does not guarantee UDP will work. Most SOCKS5 proxies do not support UDP traffic, including SSH SOCKS5 proxies. @@ -510,6 +519,8 @@ cd ../../tests && sudo ./e2e.sh # end-to-end with real kernel sudo ./e2e_domains.sh # domain rules with a real resolver client (root, needs dig) sudo ./e2e_rules.sh # every rule form + every proxy type, incl. HTTP auth and UDP rules (root) sudo ./api_live.sh # library API: edit/move/disable/delete rules and proxies on the live datapath (root) +sudo BPFTOOL=$(command -v bpftool) ./e2e_security.sh # relay hand-off cannot be hijacked from another loopback address (root) +sudo ./churn.sh 3000 # rule/proxy churn with live traffic: memory, fds and threads must stay flat (root) sudo ./e2e_openssh.sh # interoperability with a real third-party SOCKS5 server (OpenSSH ssh -D), 40 MB each way (root) sudo NODE=$(command -v node) NEXUS_JS=/path/to/nexus-proxy/script.js ./e2e_nexus.sh # against Nexus Proxy, a real forwarding SOCKS5 server with UDP (root) sudo ./scale.sh 10000 # 10,000 concurrent proxied connections (root) diff --git a/Linux/tests/api_live.sh b/Linux/tests/api_live.sh index 45f71b64..308fa24b 100755 --- a/Linux/tests/api_live.sh +++ b/Linux/tests/api_live.sh @@ -7,4 +7,4 @@ WORK=$(mktemp -d); trap 'kill $FAKE 2>/dev/null; wait 2>/dev/null; rm -rf $WORK' gcc -Wall -Wextra -O1 -g -I"$ROOT/src" -o "$WORK/api_live_test" "$HERE/api_live_test.c" -L"$PB_DIR" -lproxybridge -lpthread || exit 1 python3 "$HERE/fake_proxies.py" "$WORK/log" >/dev/null 2>&1 & FAKE=$! sleep 1 -LD_LIBRARY_PATH="$PB_DIR" "$WORK/api_live_test" +env LD_LIBRARY_PATH="$PB_DIR" ${PB_PRELOAD:+LD_PRELOAD="$PB_PRELOAD"} "$WORK/api_live_test" diff --git a/Linux/tests/churn.sh b/Linux/tests/churn.sh new file mode 100755 index 00000000..82ba525e --- /dev/null +++ b/Linux/tests/churn.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Rule/proxy churn with live traffic. Needs root, curl, python3, gcc. sudo tests/churn.sh [iterations] +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +PB_DIR="${PB_DIR:-$ROOT/output}" +[ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR"; exit 1; } +WORK=$(mktemp -d); trap 'kill $FAKE $TRAFFIC 2>/dev/null; wait 2>/dev/null; rm -rf $WORK' EXIT +gcc -Wall -Wextra -O1 -g -I"$ROOT/src" -o "$WORK/churn_test" "$HERE/churn_test.c" -L"$PB_DIR" -lproxybridge -lpthread || exit 1 +python3 "$HERE/fake_proxies.py" "$WORK/log" >/dev/null 2>&1 & FAKE=$! +sleep 1 +( while :; do + curl -s -m 2 http://192.0.2.5/ >/dev/null + python3 -c " +import socket +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(1) +s.sendto(b'x', ('192.0.2.9', 5300)); s.recv(100)" 2>/dev/null + done ) & TRAFFIC=$! +env LD_LIBRARY_PATH="$PB_DIR" ${PB_PRELOAD:+LD_PRELOAD="$PB_PRELOAD" PB_SANITIZED=1} "$WORK/churn_test" "${1:-3000}" diff --git a/Linux/tests/churn_test.c b/Linux/tests/churn_test.c new file mode 100644 index 00000000..2d82f50b --- /dev/null +++ b/Linux/tests/churn_test.c @@ -0,0 +1,82 @@ +// Rule/proxy churn while traffic flows: memory, descriptors and threads must stay flat. +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include "../src/ProxyBridge.h" + +static long status_kb(const char *key) +{ + FILE *f = fopen("/proc/self/status", "r"); + char line[256]; + long v = -1; + size_t n = strlen(key); + while (f && fgets(line, sizeof line, f)) + if (!strncmp(line, key, n)) { v = strtol(line + n, NULL, 10); break; } + if (f) fclose(f); + return v; +} + +static int count_dir(const char *path) +{ + DIR *d = opendir(path); + int n = 0; + struct dirent *e; + while (d && (e = readdir(d))) n += e->d_name[0] != '.'; + if (d) closedir(d); + return n; +} + +struct sample { long rss_kb, heap_kb; long threads; int fds; }; + +static struct sample sample(void) +{ + struct mallinfo2 mi = mallinfo2(); + return (struct sample){ status_kb("VmRSS:"), (long)((mi.uordblks + mi.hblkhd) / 1024), status_kb("Threads:"), count_dir("/proc/self/fd") }; +} + +int main(int argc, char **argv) +{ + int iterations = argc > 1 ? atoi(argv[1]) : 5000; + char err[256]; + uint32_t pa = ProxyBridge_AddProxy("a", PROXY_TYPE_SOCKS5, "127.0.0.1", 19080, "", "", err, sizeof err); + uint32_t pb = ProxyBridge_AddProxy("b", PROXY_TYPE_SOCKS5, "127.0.0.1", 19081, "bob", "pw", err, sizeof err); + ProxyBridge_AddRule("curl", "192.0.2.0/24", "*", RULE_PROTOCOL_TCP, RULE_ACTION_PROXY, pa, err, sizeof err); + ProxyBridge_AddRule("python3*", "192.0.2.0/24", "5300", RULE_PROTOCOL_UDP, RULE_ACTION_PROXY, pb, err, sizeof err); + ProxyBridge_AddRule("*", "*.example.test", "*", RULE_PROTOCOL_TCP, RULE_ACTION_PROXY, pa, err, sizeof err); + if (!ProxyBridge_Start()) { fprintf(stderr, "start failed\n"); return 2; } + sleep(1); + + struct sample warm = { 0 }; + int warm_at = iterations / 10; + for (int i = 0; i < iterations; i++) { + char hosts[64]; + snprintf(hosts, sizeof hosts, "10.%d.%d.0/24;*.churn%d.test", i % 200, (i / 200) % 200, i % 7); + uint32_t r1 = ProxyBridge_AddRule("wget;curl", hosts, "80;443;8000-9000", RULE_PROTOCOL_BOTH, RULE_ACTION_PROXY, i % 2 ? pa : pb, err, sizeof err); + uint32_t r2 = ProxyBridge_AddRule("*chrome*", "198.51.100.0/24", "*", RULE_PROTOCOL_TCP, RULE_ACTION_BLOCK, 0, err, sizeof err); + ProxyBridge_EditRule(r1, "wget", "203.0.113.0/24", "*", RULE_PROTOCOL_TCP, RULE_ACTION_DIRECT, 0, err, sizeof err); + ProxyBridge_MoveRule(r2, 0); + ProxyBridge_DisableRule(r1); + ProxyBridge_EnableRule(r1); + uint32_t p = ProxyBridge_AddProxy("tmp", i % 2 ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5, "127.0.0.1", 19082, "", "", err, sizeof err); + ProxyBridge_EditProxy(p, "tmp2", PROXY_TYPE_SOCKS5, "localhost", 19083, "u", "p", err, sizeof err); + ProxyBridge_DeleteRule(r1); + ProxyBridge_DeleteRule(r2); + ProxyBridge_DeleteProxy(p); + if (i % 500 == 499) { ProxyBridge_Stop(); ProxyBridge_Start(); } /* full restart cycles */ + if (i == warm_at) warm = sample(); + if (i % 1000 == 999) { struct sample s = sample(); printf("iteration %d: rss=%ld KB heap in use=%ld KB threads=%ld fds=%d\n", i + 1, s.rss_kb, s.heap_kb, s.threads, s.fds); fflush(stdout); } + } + struct sample end = sample(); + ProxyBridge_Stop(); + long rss_growth = end.rss_kb - warm.rss_kb, heap_growth = end.heap_kb - warm.heap_kb; + printf("warm-up: rss=%ld heap=%ld KB threads=%ld fds=%d | end: rss=%ld heap=%ld KB threads=%ld fds=%d | growth: rss %ld KB, heap in use %ld KB\n", + warm.rss_kb, warm.heap_kb, warm.threads, warm.fds, end.rss_kb, end.heap_kb, end.threads, end.fds, rss_growth, heap_growth); + int sanitized = getenv("PB_SANITIZED") != NULL; /* sanitizer quarantines distort both numbers */ + int ok = (sanitized || heap_growth < 256) && end.fds <= warm.fds + 2 && end.threads <= warm.threads + 1; + printf("%s\n", ok ? "PASS heap, descriptors and threads stay flat under churn" : "FAIL growth under churn"); + return !ok; +} diff --git a/Linux/tests/e2e_nexus.sh b/Linux/tests/e2e_nexus.sh index 0321421e..b404d5ea 100755 --- a/Linux/tests/e2e_nexus.sh +++ b/Linux/tests/e2e_nexus.sh @@ -63,7 +63,6 @@ sha_for() { python3 -c "$EXPECT_PY" "$1"; } stdbuf -oL "$PB_DIR/ProxyBridge" --verbose 2 \ --proxy n1=socks5://127.0.0.1:1080 --proxy n2=socks5://nx:secret@127.0.0.1:1081 \ - --rule "node:*:*:BOTH:DIRECT" \ --rule "curl:10.77.1.3:*:TCP:DIRECT" \ --rule "curl:10.77.1.4:*:TCP:BLOCK" \ --rule "curl:10.77.1.1:18701-18703:TCP:PROXY=n2" \ @@ -112,6 +111,13 @@ inlog $NX1LOG "[CONNECT] 10.77.1.6:18700" && ok " n1 carried the domain-matched inlog $NX1LOG "[CONNECT] 10.77.1.53:53" && ok " n1 carried the DNS-over-TCP query" || bad " DNS/TCP did not go through n1" grep -q "10.77.1.5 (app.example.test)" "$WORK/pb.out" && ok " connection log names the domain" || bad " no domain in the connection log" +echo "== the proxy's own process is exempted automatically (no DIRECT rule for node exists)" +inlog $NX2LOG "[CONNECT] 10.77.1.5:18700" && ok " nexus's own outbound connections were not sent back through itself" || bad " proxy loop" +before=$(grep -cF "[CONNECT] 10.77.1.5:18700" "$NX2LOG") +"$NODE" -e "require('net').connect(18700,'10.77.1.5').on('connect',function(){this.end()}).on('error',()=>{})"; sleep 1 +after=$(grep -cF "[CONNECT] 10.77.1.5:18700" "$NX2LOG") +[ "$after" -gt "$before" ] && ok "another node process (not the proxy) is still proxied: exemption is per process, not per binary" || bad "unrelated node process bypassed the rules" + echo "== TCP: many connections, big transfers, integrity" cpids=(); for i in $(seq 1 100); do (O http://10.77.1.1:18700/ > "$WORK/p$i.out") & cpids+=($!); done; wait "${cpids[@]}" 2>/dev/null okc=$(grep -l "origin-1" "$WORK"/p*.out 2>/dev/null | wc -l); [ "$okc" = 100 ] && ok "100 parallel connections through n1" || bad "only $okc/100 parallel connections succeeded" diff --git a/Linux/tests/e2e_rules.sh b/Linux/tests/e2e_rules.sh index 38a9ebfa..c13dcb44 100755 --- a/Linux/tests/e2e_rules.sh +++ b/Linux/tests/e2e_rules.sh @@ -57,6 +57,9 @@ start_pb \ --rule "python3*:203.0.113.160:*:BOTH:PROXY=a" sleep 1 +echo "== diagnostics" +grep -q "sends UDP to HTTP proxy 'c', which cannot carry UDP" "$WORK/pb.out" && ok "a UDP rule pointing at an HTTP proxy is reported in the log" || bad "no warning for UDP over an HTTP proxy" + echo "== host forms (process: curl)" via "single IP" http://192.0.2.10/ A 192.0.2.10:80 via "CIDR /24" http://192.0.2.200/ A 192.0.2.200:80 diff --git a/Linux/tests/e2e_security.sh b/Linux/tests/e2e_security.sh new file mode 100755 index 00000000..f550a42d --- /dev/null +++ b/Linux/tests/e2e_security.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Security checks on the relay hand-off. Needs root, bpftool, python3. sudo tests/e2e_security.sh +set -u +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(dirname "$HERE")" +PB_DIR="${PB_DIR:-$ROOT/output}" +[ -x "$PB_DIR/ProxyBridge" ] && [ -f "$PB_DIR/libproxybridge.so" ] || { echo "Build first: (cd Linux && ./build.sh), or set PB_DIR"; exit 1; } +BPFTOOL="${BPFTOOL:-bpftool}"; command -v "$BPFTOOL" >/dev/null || { echo "bpftool not found (set BPFTOOL)"; exit 1; } +[ "$(id -u)" = 0 ] || { echo "run as root"; exit 1; } +export LD_LIBRARY_PATH="$PB_DIR" +WORK="$(mktemp -d)"; PLOG="$WORK/proxy.log"; : > "$PLOG"; PASS=0; FAIL=0 +ok() { echo " PASS $1"; PASS=$((PASS+1)); } +bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); } +cleanup() { kill $FAKE $PB 2>/dev/null; wait 2>/dev/null; rm -rf "$WORK"; } +trap cleanup EXIT + +python3 "$HERE/fake_proxies.py" "$PLOG" >/dev/null 2>&1 & FAKE=$! +sleep 1 +stdbuf -oL "$PB_DIR/ProxyBridge" --verbose 1 --proxy a=socks5://127.0.0.1:19080 --rule "curl:192.0.2.1:*:TCP:PROXY=a" > "$WORK/pb.out" 2>&1 & PB=$! +for _ in $(seq 40); do grep -q "ProxyBridge started" "$WORK/pb.out" && break; sleep 0.25; done +RELAY=$(sed -n 's/.*relay tcp:\([0-9]*\) udp.*/\1/p' "$WORK/pb.out" | head -1) +[ -n "$RELAY" ] || { echo "ProxyBridge did not start"; cat "$WORK/pb.out"; exit 1; } + +PORT=40123 +# plant the kind of entry the kernel creates for a redirected connection from 127.0.0.1:PORT -> 192.0.2.99:80 via proxy 1 +HEXKEY=$(python3 -c "import struct,socket;print(' '.join('%02x'%b for b in socket.inet_aton('127.0.0.1')+struct.pack('/dev/null 2>&1; } +planted() { "$BPFTOOL" map lookup name by_port key hex $HEXKEY >/dev/null 2>&1; } + +try_connect() { # source ip -> prints "closed" or "open" +python3 - "$1" $PORT $RELAY <<'PY' +import socket, sys +src, port, relay = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]) +s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1); s.bind((src, port)); s.settimeout(3) +try: + s.connect(("127.0.0.1", relay)); s.sendall(b"GET / HTTP/1.0\r\n\r\n"); d = s.recv(100) + print("closed" if not d else "open:" + d[:30].decode(errors="replace")) +except (ConnectionError, socket.timeout) as e: + print("closed") +PY +} + +plant && planted && ok "test setup: a pending flow entry for 127.0.0.1:$PORT is in the kernel map" || { bad "could not plant the map entry (bpftool map update)"; exit 1; } +r=$(try_connect 127.0.0.2); [ "$r" = closed ] && ok "a process connecting from 127.0.0.2 with the same source port is refused" || bad "foreign source address was served ($r)" +grep -q "192.0.2.99" "$PLOG" && bad "the foreign connection reached the proxy" || ok "nothing reached the proxy for the foreign connection" +planted && ok "the legitimate pending entry survived the attempt" || bad "the foreign attempt consumed the victim's entry" +r=$(try_connect 127.0.0.1); sleep 1 +grep -q "A TCP 192.0.2.99:80" "$PLOG" && ok "the legitimate source (127.0.0.1) claims the entry and is proxied" || bad "legitimate connection was not proxied ($r)" +planted && bad "the entry was not consumed" || ok "the entry is consumed after use (no replay)" +r=$(try_connect 127.0.0.1); [ "$r" = closed ] && ok "a second connection with no pending entry is refused" || bad "relay served a connection without a pending entry ($r)" + +echo; echo "passed=$PASS failed=$FAIL" +[ "$FAIL" = 0 ] || { echo "--- proxy log"; cat "$PLOG"; exit 1; } diff --git a/Linux/tests/hookbench.sh b/Linux/tests/hookbench.sh index 9bcb12c4..767a8d76 100755 --- a/Linux/tests/hookbench.sh +++ b/Linux/tests/hookbench.sh @@ -7,7 +7,7 @@ export LD_LIBRARY_PATH="$PB_DIR" IP=10.77.0.1 # alias on lo: local like loopback, but not caught by the implicit-loopback bypass ip addr add $IP/32 dev lo 2>/dev/null trap 'ip addr del $IP/32 dev lo 2>/dev/null' EXIT -run() { for i in 1 2 3; do $HB tcp $IP 19555 20000; done; for i in 1 2 3; do $HB udp $IP 19556 1000000; done; } +run() { for i in 1 2 3; do $HB udp $IP 19556 1000000; done; } echo "### A: ProxyBridge not running"; run start() { stdbuf -oL "$PB_DIR/ProxyBridge" "$@" > /tmp/pbbench.out 2>&1 & PB=$!; for _ in $(seq 40); do grep -q "ProxyBridge started" /tmp/pbbench.out && break; sleep 0.25; done; } echo "### B: running, 20 rules for other programs (this binary: empty class -> immediate direct)" From 6ee21fd91116d0303a1d17642810c2a3f4fd65db Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 17:40:21 +0530 Subject: [PATCH 21/25] build(linux): hardened, portable build; fix GLIBC_2.38 requirement (#146) Out-of-tree builds with LTO, FORTIFY=3, PIE, RELRO/BIND_NOW, CET and a pinned static PIC libbpf. A glibc 2.34 symbol floor and a version script keep the binaries portable and the export list minimal; build.sh runs tests/check_binaries.sh and fails if an audit check does not pass. --- Linux/build.sh | 60 +++++++++++++++++++------------- Linux/cli/Makefile | 22 +++--------- Linux/common.mk | 23 +++++++++++++ Linux/gui/Makefile | 33 ++++++++++++++++++ Linux/src/Makefile | 64 +++++++++++++++++++++++------------ Linux/src/exports.map | 6 ++++ Linux/src/glibc_compat.h | 23 +++++++++++++ Linux/src/pb_proc.c | 46 ++++++++++++++++++++++--- Linux/tests/check_binaries.sh | 46 +++++++++++++++++++++++++ 9 files changed, 257 insertions(+), 66 deletions(-) create mode 100644 Linux/common.mk create mode 100644 Linux/gui/Makefile create mode 100644 Linux/src/exports.map create mode 100644 Linux/src/glibc_compat.h create mode 100755 Linux/tests/check_binaries.sh diff --git a/Linux/build.sh b/Linux/build.sh index 96f26721..e779a987 100755 --- a/Linux/build.sh +++ b/Linux/build.sh @@ -1,32 +1,42 @@ #!/bin/bash # Builds libproxybridge.so, the CLI and (if GTK3 is present) the GUI. -# Everything is compiled in a temporary directory; only the final binaries are copied to output/, -# so the source tree never collects .o files, generated headers or libraries. +# Everything is compiled in a temporary directory; only the final binaries are copied to output/, after an audit of their +# portability (glibc floor) and hardening. # -# ./build.sh -> Linux/output/ +# ./build.sh -> Linux/output/ # OUTPUT_DIR=/some/dir ./build.sh +# WERROR=1 ./build.sh warnings are errors (CI) LTO=0 ./build.sh no link-time optimisation +# AUDIT=0 ./build.sh skip the binary audit STATIC_LIBBPF=0 ./build.sh use the system libbpf set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OUTPUT_DIR="${OUTPUT_DIR:-$SCRIPT_DIR/output}" BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/proxybridge-build.XXXXXX")" trap 'rm -rf "$BUILD_DIR"' EXIT +JOBS="$(nproc 2>/dev/null || echo 2)" +export WERROR LTO OPT need() { # command, install hint command -v "$1" >/dev/null 2>&1 || { echo "ERROR: '$1' not found. $2"; exit 1; } } need gcc "Install: sudo apt install build-essential | sudo dnf install gcc" +need curl "Needed to fetch the pinned libbpf source. Install: sudo apt install curl" need make "Install: sudo apt install make | sudo dnf install make" need clang "Needed to compile the eBPF program. Install: sudo apt install clang | sudo dnf install clang" BPFTOOL="${BPFTOOL:-bpftool}" # set BPFTOOL=/path/to/bpftool if it is not on PATH (e.g. /usr/lib/linux-tools/*/bpftool on Ubuntu) export BPFTOOL need "$BPFTOOL" "Needed to generate the eBPF skeleton. Install: sudo apt install linux-tools-generic (Ubuntu) | sudo apt install bpftool (Debian) | sudo dnf install bpftool" -if [ ! -f /usr/include/bpf/libbpf.h ] && ! pkg-config --exists libbpf 2>/dev/null; then +if [ "${STATIC_LIBBPF:-1}" != 1 ] && [ ! -f /usr/include/bpf/libbpf.h ] && ! pkg-config --exists libbpf 2>/dev/null; then echo "ERROR: libbpf development files not found. Install: sudo apt install libbpf-dev | sudo dnf install libbpf-devel" exit 1 fi +for h in libelf.h zlib.h; do + [ -f "/usr/include/$h" ] || find /usr/include -name "$h" -print -quit 2>/dev/null | grep -q . || { + echo "ERROR: $h not found (needed to build the static libbpf). Install: sudo apt install libelf-dev zlib1g-dev | sudo dnf install elfutils-libelf-devel zlib-devel" + exit 1 + } +done -# Fresh output directory if [ -e "$OUTPUT_DIR" ]; then echo "Removing existing output directory..." rm -rf "$OUTPUT_DIR" 2>/dev/null || { @@ -38,36 +48,35 @@ fi mkdir -p "$OUTPUT_DIR" echo "" -# Library echo "=== Building Library ===" -make -C "$SCRIPT_DIR/src" BUILD_DIR="$BUILD_DIR/lib" +LIBBPF_ARGS=() +if [ "${STATIC_LIBBPF:-1}" = 1 ]; then + if make -s -C "$SCRIPT_DIR/src" libbpf-static; then + LIBBPF_ARGS=(LIBBPF_DIR="$(make -s -C "$SCRIPT_DIR/src" libbpf-path)") + echo "libbpf: linking the pinned static build" + else + echo "WARNING: could not build the pinned libbpf (needs network, curl, libelf-dev and zlib1g-dev);" + echo " falling back to the system libbpf. The result will not run on systems without libbpf >= 1.0." + export ALLOW_DYNAMIC_LIBBPF=1 + fi +else + export ALLOW_DYNAMIC_LIBBPF=1 +fi +make -j"$JOBS" -C "$SCRIPT_DIR/src" BUILD_DIR="$BUILD_DIR/lib" "${LIBBPF_ARGS[@]}" [ -f "$BUILD_DIR/lib/libproxybridge.so" ] || { echo "Library build failed!"; exit 1; } echo "Library build successful" echo "" -# CLI echo "=== Building CLI ===" -make -C "$SCRIPT_DIR/cli" BUILD_DIR="$BUILD_DIR/cli" LIBDIR="$BUILD_DIR/lib" +make -j"$JOBS" -C "$SCRIPT_DIR/cli" BUILD_DIR="$BUILD_DIR/cli" LIBDIR="$BUILD_DIR/lib" [ -f "$BUILD_DIR/cli/ProxyBridge" ] || { echo "CLI build failed!"; exit 1; } echo "CLI build successful" echo "" -# GUI (optional) echo "=== Building GUI ===" GUI_BUILT=0 if pkg-config --exists gtk+-3.0; then - mkdir -p "$BUILD_DIR/gui" - GUI_CFLAGS=(-Wall -Wno-unused-parameter -O3 "-I$SCRIPT_DIR/src" -D_GNU_SOURCE -fstack-protector-strong - -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Wformat-security -Werror=format-security -fno-strict-overflow - -fno-delete-null-pointer-checks -fwrapv $(pkg-config --cflags gtk+-3.0)) - GUI_OBJS=() - for src in "$SCRIPT_DIR"/gui/*.c; do - obj="$BUILD_DIR/gui/$(basename "${src%.c}").o" - gcc "${GUI_CFLAGS[@]}" -c "$src" -o "$obj" - GUI_OBJS+=("$obj") - done - gcc -o "$BUILD_DIR/gui/ProxyBridgeGUI" "${GUI_OBJS[@]}" -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \ - "-L$BUILD_DIR/lib" -Wl,-rpath,'$ORIGIN' -lproxybridge -lpthread $(pkg-config --libs gtk+-3.0) -export-dynamic + make -j"$JOBS" -C "$SCRIPT_DIR/gui" BUILD_DIR="$BUILD_DIR/gui" LIBDIR="$BUILD_DIR/lib" GUI_BUILT=1 echo "GUI build successful" else @@ -77,13 +86,18 @@ else fi echo "" -# Only the final files go to output/ echo "Copying binaries to $OUTPUT_DIR ..." cp "$BUILD_DIR/lib/libproxybridge.so" "$OUTPUT_DIR/" cp "$BUILD_DIR/cli/ProxyBridge" "$OUTPUT_DIR/" [ "$GUI_BUILT" = 1 ] && cp "$BUILD_DIR/gui/ProxyBridgeGUI" "$OUTPUT_DIR/" echo "" +if [ "${AUDIT:-1}" = 1 ]; then + echo "=== Auditing binaries ===" + "$SCRIPT_DIR/tests/check_binaries.sh" "$OUTPUT_DIR" || { echo "Audit failed: the binaries are not portable/hardened enough to ship."; exit 1; } + echo "" +fi + echo "===================================" echo "Build Complete!" echo "===================================" diff --git a/Linux/cli/Makefile b/Linux/cli/Makefile index 0d9464e3..ed4e85d7 100644 --- a/Linux/cli/Makefile +++ b/Linux/cli/Makefile @@ -1,25 +1,15 @@ -CC = gcc +include ../common.mk # BUILD_DIR / LIBDIR: where generated files go and where libproxybridge.so was built (build.sh sets both). BUILD_DIR ?= . LIBDIR ?= ../src -CFLAGS = -Wall -Wextra -O3 -I../src \ - -fstack-protector-strong \ - -D_FORTIFY_SOURCE=2 \ - -fPIE \ - -Wformat -Wformat-security \ - -Werror=format-security \ - -fno-strict-overflow \ - -fno-delete-null-pointer-checks \ - -fwrapv -LDFLAGS = -L$(LIBDIR) -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \ - -Wl,-rpath,/usr/local/lib +CFLAGS = $(CFLAGS_COMMON) -fPIE -I../src +LDFLAGS = -pie $(HARDEN_LDFLAGS) -L$(LIBDIR) -Wl,-rpath,/usr/local/lib $(STRIP_FLAG) LIBS = -lproxybridge -lpthread TARGET = $(BUILD_DIR)/ProxyBridge -SOURCES = main.c -OBJECTS = $(addprefix $(BUILD_DIR)/,$(SOURCES:.c=.o)) +OBJECTS = $(BUILD_DIR)/main.o all: $(TARGET) @@ -27,9 +17,7 @@ $(BUILD_DIR): mkdir -p $@ $(TARGET): $(OBJECTS) - $(CC) -o $@ $^ $(LDFLAGS) $(LIBS) - @echo "Stripping debug symbols..." - strip --strip-all $@ + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(LIBS) $(BUILD_DIR)/%.o: %.c | $(BUILD_DIR) $(CC) $(CFLAGS) -c $< -o $@ diff --git a/Linux/common.mk b/Linux/common.mk new file mode 100644 index 00000000..546df91e --- /dev/null +++ b/Linux/common.mk @@ -0,0 +1,23 @@ +# Compiler and linker flags shared by the library, the CLI and the GUI. +# OPT=-O2 optimisation level LTO=0 disable link-time optimisation +# WERROR=1 warnings are errors (CI) EXTRA_CFLAGS / EXTRA_LDFLAGS e.g. sanitizers +CC ?= gcc +OPT ?= -O2 +LTO ?= 1 + +# Keeps a flag only if the compiler accepts it (other GCC versions and architectures differ). +cc-option = $(shell $(CC) $(1) -Werror -x c -c /dev/null -o /dev/null 2>/dev/null && echo $(1)) + +HARDEN_CFLAGS = -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -fstack-protector-strong \ + $(call cc-option,-fstack-clash-protection) $(call cc-option,-fcf-protection=full) \ + $(call cc-option,-mbranch-protection=standard) $(call cc-option,-ftrivial-auto-var-init=zero) \ + $(call cc-option,-fno-plt) -fno-strict-overflow -fno-delete-null-pointer-checks -fwrapv \ + -Wformat -Wformat-security -Werror=format-security + +CFLAGS_COMMON = $(OPT) -Wall -Wextra $(if $(WERROR),-Werror) $(HARDEN_CFLAGS) \ + -ffunction-sections -fdata-sections $(if $(filter 1,$(LTO)),-flto=auto) $(EXTRA_CFLAGS) + +HARDEN_LDFLAGS = -Wl,-z,relro,-z,now -Wl,-z,noexecstack -Wl,--as-needed -Wl,--gc-sections \ + $(if $(filter 1,$(LTO)),-flto=auto) $(EXTRA_LDFLAGS) + +STRIP_FLAG ?= -s diff --git a/Linux/gui/Makefile b/Linux/gui/Makefile new file mode 100644 index 00000000..b7ca1a3a --- /dev/null +++ b/Linux/gui/Makefile @@ -0,0 +1,33 @@ +include ../common.mk + +BUILD_DIR ?= . +LIBDIR ?= ../src + +# Compile-time API limits keep the GUI runnable on Ubuntu 22.04 (GTK 3.24.33, GLib 2.72). +GTK_CFLAGS = $(shell pkg-config --cflags gtk+-3.0) -DGDK_VERSION_MIN_REQUIRED=GDK_VERSION_3_22 -DGDK_VERSION_MAX_ALLOWED=GDK_VERSION_3_22 \ + -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_72 -DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_72 +GTK_LIBS = $(shell pkg-config --libs gtk+-3.0) + +CFLAGS = $(CFLAGS_COMMON) -Wno-unused-parameter -fPIE -D_GNU_SOURCE -I../src $(GTK_CFLAGS) +LDFLAGS = -pie $(HARDEN_LDFLAGS) -L$(LIBDIR) -Wl,-rpath,'$$ORIGIN' -export-dynamic $(STRIP_FLAG) +LIBS = -lproxybridge -lpthread $(GTK_LIBS) + +SOURCES = $(filter-out tests/%,$(wildcard *.c)) +OBJECTS = $(addprefix $(BUILD_DIR)/,$(SOURCES:.c=.o)) +TARGET = $(BUILD_DIR)/ProxyBridgeGUI + +all: $(TARGET) + +$(BUILD_DIR): + mkdir -p $@ + +$(TARGET): $(OBJECTS) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(LIBS) + +$(BUILD_DIR)/%.o: %.c gui.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJECTS) $(TARGET) + +.PHONY: all clean diff --git a/Linux/src/Makefile b/Linux/src/Makefile index 7c9d5e88..ab841116 100644 --- a/Linux/src/Makefile +++ b/Linux/src/Makefile @@ -1,26 +1,31 @@ -CC ?= gcc +include ../common.mk + CLANG ?= clang BPFTOOL ?= bpftool MULTIARCH := $(shell $(CC) -print-multiarch) -# BUILD_DIR keeps every generated file (objects, BPF object, skeleton, library) out of the source tree. -# build.sh points it at a temporary directory; a plain `make` builds in place for quick development. +# BUILD_DIR keeps every generated file out of the source tree; build.sh points it at a temporary directory. BUILD_DIR ?= . -# EXTRA_CFLAGS / EXTRA_LDFLAGS: e.g. -fsanitize=address,undefined (use STRIP_FLAG= to keep symbols for the report). -STRIP_FLAG ?= -s - -CFLAGS = -Wall -Wextra -O2 -fPIC -D_GNU_SOURCE -I$(BUILD_DIR) \ - -fstack-protector-strong \ - -D_FORTIFY_SOURCE=2 \ - -fPIE \ - -Wformat -Wformat-security \ - -Werror=format-security \ - -fno-strict-overflow \ - -fno-delete-null-pointer-checks \ - -fwrapv -fvisibility=hidden -ffunction-sections -fdata-sections $(EXTRA_CFLAGS) -LDFLAGS = -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack -Wl,--gc-sections $(STRIP_FLAG) $(EXTRA_LDFLAGS) -LIBS = -lpthread -lbpf +# libbpf is linked statically from a pinned release built as position-independent code, so the library does not depend on the +# distribution's libbpf (Ubuntu 22.04 ships 0.5, and distribution libbpf.a is not PIC). With LIBBPF_DIR unset the system +# libbpf is used instead. +LIBBPF_VERSION ?= 1.6.3 +LIBBPF_SHA256 ?= 989ed3c1a3db8ff0f7c08dd43953c6b9d0c3ac252653a48d566aaedf98bc80ca +LIBBPF_RECIPE ?= 2 +LIBBPF_CACHE ?= $(or $(XDG_CACHE_HOME),$(HOME)/.cache)/proxybridge/libbpf-$(LIBBPF_VERSION)-r$(LIBBPF_RECIPE) +LIBBPF_DIR ?= + +ifneq ($(LIBBPF_DIR),) +LIBBPF_INCLUDE = -I$(LIBBPF_DIR)/include +LIBBPF_LIBS = $(LIBBPF_DIR)/lib/libbpf.a -lelf -lz +else +LIBBPF_LIBS = -lbpf +endif + +CFLAGS = $(CFLAGS_COMMON) -fPIC -fvisibility=hidden -D_GNU_SOURCE -I$(BUILD_DIR) $(LIBBPF_INCLUDE) +LDFLAGS = -shared $(HARDEN_LDFLAGS) -Wl,-z,defs -Wl,--version-script=$(CURDIR)/exports.map $(STRIP_FLAG) +LIBS = -lpthread $(LIBBPF_LIBS) SOURCES = ProxyBridge.c pb_rules.c pb_dns.c pb_proc.c pb_bpf.c pb_relay.c OBJECTS = $(addprefix $(BUILD_DIR)/,$(SOURCES:.c=.o)) @@ -34,24 +39,39 @@ $(BUILD_DIR): mkdir -p $@ $(BPF_OBJ): bpf/proxybridge.bpf.c bpf/pb_bpf_shared.h | $(BUILD_DIR) - $(CLANG) -O2 -g -target bpf -D__BPF__ -I/usr/include/$(MULTIARCH) -c $< -o $@ + $(CLANG) -O2 -g -target bpf -D__BPF__ $(LIBBPF_INCLUDE) -I/usr/include/$(MULTIARCH) -c $< -o $@ $(SKEL): $(BPF_OBJ) $(BPFTOOL) gen skeleton $< > $@.tmp && mv $@.tmp $@ $(BUILD_DIR)/pb_bpf.o: $(SKEL) -$(TARGET): $(OBJECTS) - $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) +$(TARGET): $(OBJECTS) exports.map + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJECTS) $(LIBS) $(BUILD_DIR)/%.o: %.c pb_internal.h ProxyBridge.h | $(BUILD_DIR) $(CC) $(CFLAGS) -c $< -o $@ -# Unit tests (no root needed); binaries go to BUILD_DIR as well. test-rules: tests/rules_test.c tests/dns_test.c pb_rules.c pb_rules.h pb_dns.c pb_dns.h | $(BUILD_DIR) $(CC) -Wall -Wextra -g -fsanitize=address,undefined -D_GNU_SOURCE tests/rules_test.c pb_rules.c -o $(BUILD_DIR)/rules_test && $(BUILD_DIR)/rules_test $(CC) -Wall -Wextra -g -fsanitize=address,undefined -D_GNU_SOURCE tests/dns_test.c pb_dns.c -o $(BUILD_DIR)/dns_test && $(BUILD_DIR)/dns_test +# Downloads, verifies and builds libbpf (cached); prints nothing. `make -s libbpf-path` prints the directory to use as LIBBPF_DIR. +libbpf-static: $(LIBBPF_CACHE)/lib/libbpf.a + +$(LIBBPF_CACHE)/lib/libbpf.a: + @set -e; work=$$(mktemp -d); cd "$$work"; \ + curl -fsSL -o libbpf.tar.gz https://github.com/libbpf/libbpf/archive/refs/tags/v$(LIBBPF_VERSION).tar.gz; \ + echo "$(LIBBPF_SHA256) libbpf.tar.gz" | sha256sum -c --quiet -; \ + tar xzf libbpf.tar.gz; \ + $(MAKE) -s -C libbpf-$(LIBBPF_VERSION)/src BUILD_STATIC_ONLY=y OBJDIR="$$work/obj" CFLAGS="-O2 -fPIC -include $(CURDIR)/glibc_compat.h" \ + DESTDIR="$$work/root" PREFIX=/ LIBDIR=/lib install >/dev/null; \ + mkdir -p "$(LIBBPF_CACHE)"; cp -r "$$work/root/include" "$$work/root/lib" "$(LIBBPF_CACHE)/"; \ + cd /; rm -rf "$$work" + +libbpf-path: + @echo $(LIBBPF_CACHE) + clean: rm -f $(OBJECTS) $(TARGET) $(BPF_OBJ) $(SKEL) $(BUILD_DIR)/rules_test $(BUILD_DIR)/dns_test @@ -59,4 +79,4 @@ install: $(TARGET) install -m 0755 $(TARGET) /usr/local/lib/ ldconfig -.PHONY: all clean install test-rules +.PHONY: all clean install test-rules libbpf-static libbpf-path diff --git a/Linux/src/exports.map b/Linux/src/exports.map new file mode 100644 index 00000000..0084447a --- /dev/null +++ b/Linux/src/exports.map @@ -0,0 +1,6 @@ +{ + global: + ProxyBridge_*; + local: + *; +}; diff --git a/Linux/src/glibc_compat.h b/Linux/src/glibc_compat.h new file mode 100644 index 00000000..873c1a80 --- /dev/null +++ b/Linux/src/glibc_compat.h @@ -0,0 +1,23 @@ +// glibc 2.38 headers redirect strto*/scanf to C23 variants (GLIBC_2.38 symbols), which stops the binary from loading on older +// systems (GitHub issue #146). This header binds the names back to the classic symbol versions; it is force-included when +// building third-party C code (libbpf) that uses them. Our own sources avoid these functions. +#ifndef PB_GLIBC_COMPAT_H +#define PB_GLIBC_COMPAT_H + +#include + +#if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 38)) +# if defined(__x86_64__) +# define PB_BASE_GLIBC "GLIBC_2.2.5" +# elif defined(__aarch64__) +# define PB_BASE_GLIBC "GLIBC_2.17" +# endif +# ifdef PB_BASE_GLIBC +# define PB_SYMVER(name) __asm__(".symver __isoc23_" #name "," #name "@" PB_BASE_GLIBC) +PB_SYMVER(strtol); PB_SYMVER(strtoul); PB_SYMVER(strtoll); PB_SYMVER(strtoull); +PB_SYMVER(scanf); PB_SYMVER(fscanf); PB_SYMVER(sscanf); +PB_SYMVER(vscanf); PB_SYMVER(vfscanf); PB_SYMVER(vsscanf); +# endif +#endif + +#endif diff --git a/Linux/src/pb_proc.c b/Linux/src/pb_proc.c index 93c8fc3f..61b756e2 100644 --- a/Linux/src/pb_proc.c +++ b/Linux/src/pb_proc.c @@ -10,6 +10,40 @@ #define MAX_INODES 8 +static bool parse_number(const char *s, unsigned base, unsigned long long *out, const char **end) +{ + unsigned long long v = 0; + const char *p = s; + for (;; p++) { + unsigned d = (unsigned)(*p - '0'); + if (*p >= 'a' && *p <= 'f') d = (unsigned)(*p - 'a') + 10; + else if (*p >= 'A' && *p <= 'F') d = (unsigned)(*p - 'A') + 10; + if (d >= base) break; + v = v * base + d; + } + if (p == s) return false; + *out = v; + if (end) *end = p; + return true; +} + +// Fields of a /proc/net/tcp row: sl, local, remote, state, queues, timer, retrans, uid, timeout, inode. +static bool parse_tcp_row(char *line, uint32_t *addr, uint32_t *port, unsigned *state, unsigned long long *inode) +{ + char *fields[10], *save = NULL; + size_t n = 0; + for (char *t = strtok_r(line, " \t\n", &save); t && n < 10; t = strtok_r(NULL, " \t\n", &save)) fields[n++] = t; + if (n < 10) return false; + unsigned long long a, p, st; + const char *colon; + if (!parse_number(fields[1], 16, &a, &colon) || *colon != ':' || !parse_number(colon + 1, 16, &p, NULL)) return false; + if (!parse_number(fields[3], 16, &st, NULL) || !parse_number(fields[9], 10, inode, NULL)) return false; + *addr = (uint32_t)a; + *port = (uint32_t)p; + *state = (unsigned)st; + return true; +} + static size_t listen_inodes(uint32_t ip_be, uint16_t port, uint64_t *inodes) { FILE *f = fopen("/proc/net/tcp", "r"); @@ -18,10 +52,10 @@ static size_t listen_inodes(uint32_t ip_be, uint16_t port, uint64_t *inodes) size_t n = 0; if (!fgets(line, sizeof line, f)) { fclose(f); return 0; } while (n < MAX_INODES && fgets(line, sizeof line, f)) { - unsigned addr, lport, state; + uint32_t addr, lport; + unsigned state; unsigned long long inode; - // sl local rem st tx:rx tr:when retrnsmt uid timeout inode - if (sscanf(line, " %*d: %x:%x %*x:%*x %x %*x:%*x %*x:%*x %*x %*d %*d %llu", &addr, &lport, &state, &inode) != 4) continue; + if (!parse_tcp_row(line, &addr, &lport, &state, &inode)) continue; if (state == 0x0A && lport == port && inode && (addr == ip_be || addr == 0)) inodes[n++] = inode; } fclose(f); @@ -58,7 +92,11 @@ size_t pb_proc_listeners(uint32_t ip_be, uint16_t port, uint32_t *tgids, size_t while (n < max && (de = readdir(d))) { if (de->d_name[0] < '1' || de->d_name[0] > '9') continue; for (size_t i = 0; i < ni; i++) - if (pid_has_socket(de->d_name, inodes[i])) { tgids[n++] = (uint32_t)strtoul(de->d_name, NULL, 10); break; } + if (pid_has_socket(de->d_name, inodes[i])) { + unsigned long long pid; + if (parse_number(de->d_name, 10, &pid, NULL)) tgids[n++] = (uint32_t)pid; + break; + } } closedir(d); return n; diff --git a/Linux/tests/check_binaries.sh b/Linux/tests/check_binaries.sh new file mode 100755 index 00000000..b4dbdb13 --- /dev/null +++ b/Linux/tests/check_binaries.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Audits the built binaries: portability (glibc floor, no libbpf dependency) and hardening. +# tests/check_binaries.sh [dir] MAX_GLIBC=2.34 (default) is the highest glibc symbol version allowed +set -u +DIR="${1:-$(dirname "$(dirname "${BASH_SOURCE[0]}")")/output}" +MAX_GLIBC="${MAX_GLIBC:-2.34}" +fails=0 +ok() { printf ' ok %s\n' "$1"; } +bad() { printf ' FAIL %s\n' "$1"; fails=$((fails + 1)); } +check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; } + +audit() { # file kind(lib|exe) allowed-NEEDED-regex + local f="$DIR/$1" kind="$2" needed_ok="$3" + [ -f "$f" ] || { [ "$1" = ProxyBridgeGUI ] && { echo "== $1 (not built)"; return; }; bad "$1 is missing"; return; } + echo "== $1" + local hdr prog dyn syms need + hdr=$(readelf -h "$f"); prog=$(readelf -lW "$f"); dyn=$(readelf -d "$f"); syms=$(nm -D "$f" 2>/dev/null) + if [ "$kind" = exe ]; then check "position independent executable" 'grep -q "Position-Independent" <<<"$hdr"' + else check "shared object" 'grep -q "DYN" <<<"$hdr"'; fi + check "full RELRO (GNU_RELRO + BIND_NOW)" 'grep -q GNU_RELRO <<<"$prog" && grep -q -E "BIND_NOW|FLAGS.*NOW" <<<"$dyn"' + check "non-executable stack" 'grep GNU_STACK <<<"$prog" | grep -vq "RWE"' + check "no writable+executable segments" '! grep -E "LOAD" <<<"$prog" | grep -q "RWE"' + check "no text relocations" '! grep -q TEXTREL <<<"$dyn"' + check "stack protector in use" 'grep -q __stack_chk_fail <<<"$syms"' + check "fortified libc calls in use" 'grep -q "_chk" <<<"$syms"' + if [ "$(uname -m)" = x86_64 ]; then check "CET (IBT + SHSTK) marked" 'readelf -n "$f" | grep -q IBT && readelf -n "$f" | grep -q SHSTK'; fi + need=$(grep NEEDED <<<"$dyn" | sed 's/.*\[\(.*\)\]/\1/' | tr '\n' ' ') + check "dependencies are only portable libraries ($need)" '! tr " " "\n" <<<"$need" | grep -v "^$" | grep -q -v -E "$needed_ok"' + local top + top=$(objdump -T "$f" 2>/dev/null | grep -o 'GLIBC_[0-9][0-9.]*' | sed 's/GLIBC_//' | sort -uV | tail -1) + check "needs glibc ${top:-none} (limit $MAX_GLIBC, see issue #146)" '[ -z "$top" ] || [ "$(printf "%s\n%s\n" "$top" "$MAX_GLIBC" | sort -V | tail -1)" = "$MAX_GLIBC" ]' + if [ -n "$top" ] && [ "$(printf '%s\n%s\n' "$top" "$MAX_GLIBC" | sort -V | tail -1)" != "$MAX_GLIBC" ]; then + objdump -T "$f" | grep -E "GLIBC_(2\.(3[5-9]|[4-9][0-9])|[3-9])" | awk '{print " "$NF" "$(NF-1)}' | head -5 + fi +} + +LIBBPF_DEP=''; [ "${ALLOW_DYNAMIC_LIBBPF:-0}" = 1 ] && LIBBPF_DEP='|libbpf\.so\.1' +audit libproxybridge.so lib "^(libc\.so\.6|libelf\.so\.1|libz\.so\.1|libm\.so\.6|ld-linux.*$LIBBPF_DEP)\$" +exports=$(nm -D --defined-only "$DIR/libproxybridge.so" 2>/dev/null | awk '$2=="T"{print $3}' | grep -v '^ProxyBridge_' | tr '\n' ' ') +check "library exports only the ProxyBridge_* API${exports:+ (also: $exports)}" '[ -z "$exports" ]' +audit ProxyBridge exe '^(libc\.so\.6|libproxybridge\.so|libpthread\.so\.0|libm\.so\.6|ld-linux.*)$' +audit ProxyBridgeGUI exe '^(libc\.so\.6|libproxybridge\.so|libpthread\.so\.0|libm\.so\.6|libgtk-3\.so\.0|libgdk-3\.so\.0|libglib-2\.0\.so\.0|libgobject-2\.0\.so\.0|libgio-2\.0\.so\.0|libcairo\.so\.2|libgdk_pixbuf-2\.0\.so\.0|libpango-1\.0\.so\.0|libpangocairo-1\.0\.so\.0|libatk-1\.0\.so\.0|ld-linux.*)$' + +echo +if [ "$fails" = 0 ]; then echo "binary audit: all checks passed"; else echo "binary audit: $fails check(s) failed"; fi +[ "$fails" = 0 ] From 3552375d97e8e54528d9e7d2f8c45fbdd18f2442 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 17:40:21 +0530 Subject: [PATCH 22/25] feat(linux/gui): event pipeline, status bar, async start/stop, leak fixes Connection events go through a bounded queue drained on the GTK main loop, with unit tests including a TSan run. --- Linux/gui/gui.h | 22 ++--- Linux/gui/gui_config.c | 18 ++-- Linux/gui/gui_events.c | 84 ++++++++++++++++ Linux/gui/gui_events.h | 21 ++++ Linux/gui/gui_logs.c | 180 ++++++++++++++++++++++++---------- Linux/gui/gui_settings.c | 4 +- Linux/gui/gui_utils.c | 21 ---- Linux/gui/main.c | 135 ++++++++++++++++++++----- Linux/gui/tests/events_test.c | 106 ++++++++++++++++++++ Linux/gui/tests/run_tests.sh | 6 ++ 10 files changed, 475 insertions(+), 122 deletions(-) create mode 100644 Linux/gui/gui_events.c create mode 100644 Linux/gui/gui_events.h create mode 100644 Linux/gui/tests/events_test.c diff --git a/Linux/gui/gui.h b/Linux/gui/gui.h index 653f6716..edb7cd9b 100644 --- a/Linux/gui/gui.h +++ b/Linux/gui/gui.h @@ -11,15 +11,6 @@ #include #include "ProxyBridge.h" -typedef struct { - char *process_name; - uint32_t pid; - char *dest_ip; - uint16_t dest_port; - char *proxy_info; - char *timestamp; -} ConnectionData; - typedef struct { uint32_t id; // library id, 0 = not registered char *name; // unique key, rules and config refer to it @@ -58,6 +49,11 @@ extern GtkTextBuffer *log_buffer; extern GtkWidget *status_bar; extern guint status_context_id; +// avoids libc strto*/atoi, glibc 2.38 redirects them to symbols old systems lack +static inline long parse_long(const char *s) { + return s ? (long)g_ascii_strtoll(s, NULL, 10) : 0; +} + extern GList *g_profiles; extern char *g_active_profile; extern GList *g_proxies_list; // working copy of the active profile @@ -66,10 +62,7 @@ extern GList *g_rules_list; extern bool g_chk_logging; extern bool g_chk_dns; -long safe_strtol(const char *nptr); void show_message(GtkWindow *parent, GtkMessageType type, const char *format, ...); -void trim_buffer_lines(GtkTextBuffer *buffer, int max_lines); -char *get_current_time_str(); GtkWidget *combo_new(const char *first, ...) G_GNUC_NULL_TERMINATED; GtkWidget *hint_label(const char *text); void set_error_label(GtkWidget *label, const char *msg); @@ -103,6 +96,11 @@ void refresh_rules_window(); void gui_log(const char *format, ...); void lib_log_callback(const char *message); void lib_connection_callback(const char *process_name, uint32_t pid, const char *dest_ip, uint16_t dest_port, const char *proxy_info); +void gui_logs_init(); +void gui_logs_stop_timers(); +void gui_logs_free(); +uint64_t gui_conn_event_count(); +void on_pause_conn_toggled(GtkToggleButton *button, gpointer user_data); void on_search_conn_changed(GtkSearchEntry *entry, gpointer user_data); void on_search_log_changed(GtkSearchEntry *entry, gpointer user_data); void on_clear_conn_clicked(GtkButton *button, gpointer user_data); diff --git a/Linux/gui/gui_config.c b/Linux/gui/gui_config.c index 75896f36..e70d63e7 100644 --- a/Linux/gui/gui_config.c +++ b/Linux/gui/gui_config.c @@ -239,10 +239,10 @@ static ProxyData *parse_proxy_line(char *line) { while (n < 6 && (tok = strsep(&line, "|")) != NULL) fields[n++] = tok; if (n < 4) return NULL; - int port = atoi(fields[3]); + int port = parse_long(fields[3]); ProxyData *px = g_malloc0(sizeof(ProxyData)); px->name = unesc(fields[0]); - px->type = proxy_type_from_int(atoi(fields[1])); + px->type = proxy_type_from_int(parse_long(fields[1])); px->host = unesc(fields[2]); px->port = (port >= 1 && port <= 65535) ? (uint16_t)port : 0; px->username = unesc(n > 4 ? fields[4] : ""); @@ -256,9 +256,9 @@ static RuleData *parse_rule_line(char *line) { int idx = 0; while ((tok = strsep(&line, "|")) != NULL) { switch (idx++) { - case 1: rule->protocol = atoi(tok); break; - case 2: rule->action = atoi(tok); break; - case 3: rule->enabled = atoi(tok); break; + case 1: rule->protocol = parse_long(tok); break; + case 2: rule->action = parse_long(tok); break; + case 3: rule->enabled = parse_long(tok); break; case 4: rule->process_name = g_strdup(tok); break; case 5: rule->target_hosts = g_strdup(tok); break; case 6: rule->target_ports = g_strdup(tok); break; @@ -328,13 +328,13 @@ void load_config() { } else if (section != 1) continue; else if (strcmp(line, "ip") == 0) g_strlcpy(old_ip, val, sizeof(old_ip)); - else if (strcmp(line, "port") == 0) old_port = atoi(val); - else if (strcmp(line, "type") == 0) old_type = atoi(val); + else if (strcmp(line, "port") == 0) old_port = parse_long(val); + else if (strcmp(line, "type") == 0) old_type = parse_long(val); else if (strcmp(line, "user") == 0) g_strlcpy(old_user, val, sizeof(old_user)); else if (strcmp(line, "pass") == 0) g_strlcpy(old_pass, val, sizeof(old_pass)); else if (strcmp(line, "activeProfile") == 0) { g_free(saved_active); saved_active = unesc(val); } - else if (strcmp(line, "logging") == 0) g_chk_logging = atoi(val); - else if (strcmp(line, "dns") == 0) g_chk_dns = atoi(val); + else if (strcmp(line, "logging") == 0) g_chk_logging = parse_long(val); + else if (strcmp(line, "dns") == 0) g_chk_dns = parse_long(val); } else if (section == 3 && cur) { ProxyData *px = parse_proxy_line(line); diff --git a/Linux/gui/gui_events.c b/Linux/gui/gui_events.c new file mode 100644 index 00000000..80500e26 --- /dev/null +++ b/Linux/gui/gui_events.c @@ -0,0 +1,84 @@ +#include "gui_events.h" +#include +#include +#include + +struct EventRing { + pthread_mutex_t lock; + char *slots; + uint16_t *lens; + size_t cap, slot_size, head, count; + uint64_t pushed, delivered, dropped, dropped_pending; +}; + +EventRing *event_ring_new(size_t capacity, size_t slot_size) { + if (capacity == 0 || slot_size < 2 || slot_size > UINT16_MAX) return NULL; + EventRing *r = calloc(1, sizeof(*r)); + if (!r) return NULL; + r->slots = malloc(capacity * slot_size); + r->lens = calloc(capacity, sizeof(uint16_t)); + if (!r->slots || !r->lens) { + free(r->slots); + free(r->lens); + free(r); + return NULL; + } + r->cap = capacity; + r->slot_size = slot_size; + pthread_mutex_init(&r->lock, NULL); + return r; +} + +void event_ring_free(EventRing *r) { + if (!r) return; + pthread_mutex_destroy(&r->lock); + free(r->slots); + free(r->lens); + free(r); +} + +void event_ring_push(EventRing *r, const char *text) { + size_t len = text ? strnlen(text, r->slot_size - 1) : 0; + pthread_mutex_lock(&r->lock); + if (r->count == r->cap) { + r->head = (r->head + 1) % r->cap; + r->count--; + r->dropped++; + r->dropped_pending++; + } + size_t idx = (r->head + r->count) % r->cap; + memcpy(r->slots + idx * r->slot_size, text, len); + r->lens[idx] = (uint16_t)len; + r->count++; + r->pushed++; + pthread_mutex_unlock(&r->lock); +} + +size_t event_ring_drain(EventRing *r, char *out, size_t out_size, size_t max_events, uint64_t *dropped) { + size_t n = 0, used = 0; + pthread_mutex_lock(&r->lock); + while (n < max_events && r->count > 0) { + size_t len = r->lens[r->head]; + if (used + len + 2 > out_size) break; + memcpy(out + used, r->slots + r->head * r->slot_size, len); + used += len; + out[used++] = '\n'; + r->head = (r->head + 1) % r->cap; + r->count--; + r->delivered++; + n++; + } + if (dropped) *dropped = r->dropped_pending; + r->dropped_pending = 0; + pthread_mutex_unlock(&r->lock); + if (out_size) out[used] = '\0'; + return n; +} + +void event_ring_stats(EventRing *r, uint64_t *pushed, uint64_t *delivered, uint64_t *dropped) { + pthread_mutex_lock(&r->lock); + if (pushed) *pushed = r->pushed; + if (delivered) *delivered = r->delivered; + if (dropped) *dropped = r->dropped; + pthread_mutex_unlock(&r->lock); +} diff --git a/Linux/gui/gui_events.h b/Linux/gui/gui_events.h new file mode 100644 index 00000000..65a1e74d --- /dev/null +++ b/Linux/gui/gui_events.h @@ -0,0 +1,21 @@ +#ifndef PROXYBRIDGE_GUI_EVENTS_H +#define PROXYBRIDGE_GUI_EVENTS_H + +#include +#include + +typedef struct EventRing EventRing; + +EventRing *event_ring_new(size_t capacity, size_t slot_size); +void event_ring_free(EventRing *r); + +// any thread; text is truncated to slot_size - 1, the oldest event is dropped when full +void event_ring_push(EventRing *r, const char *text); + +// moves events into out as "text\n" lines (NUL terminated), stops at max_events or when out is full. +// *dropped receives the number of events dropped since the previous drain +size_t event_ring_drain(EventRing *r, char *out, size_t out_size, size_t max_events, uint64_t *dropped); + +void event_ring_stats(EventRing *r, uint64_t *pushed, uint64_t *delivered, uint64_t *dropped); + +#endif diff --git a/Linux/gui/gui_logs.c b/Linux/gui/gui_logs.c index b4d80552..4771e26c 100644 --- a/Linux/gui/gui_logs.c +++ b/Linux/gui/gui_logs.c @@ -1,17 +1,35 @@ #include "gui.h" +#include "gui_events.h" +#include -#define MAX_LINES 100 +#define RING_CAPACITY 4096 +#define SLOT_SIZE 512 +#define DRAIN_MAX 2000 +#define DRAIN_BUF_SIZE (256 * 1024) +#define DRAIN_INTERVAL_MS 100 +#define CONN_HISTORY 2000 +#define LOG_HISTORY 1000 -static void filter_text_view(GtkTextBuffer *buffer, const char *text) { - if (!buffer) return; +typedef struct { + GtkTextBuffer **buffer; + EventRing *ring; + int max_lines; + char *filter; // lowercase, NULL = show all + bool paused; +} View; + +static View conn_view = {&conn_buffer, NULL, CONN_HISTORY, NULL, false}; +static View log_view = {&log_buffer, NULL, LOG_HISTORY, NULL, false}; +static char *drain_buf = NULL; +static guint drain_source = 0; - GtkTextIter start, end; - gtk_text_buffer_get_bounds(buffer, &start, &end); - gtk_text_buffer_remove_tag_by_name(buffer, "hidden", &start, &end); - if (!text || !*text) return; +static void filter_from_line(GtkTextBuffer *buffer, int first_line, const char *lower_search) { + GtkTextIter line_start, buf_end; + gtk_text_buffer_get_iter_at_line(buffer, &line_start, first_line); + gtk_text_buffer_get_end_iter(buffer, &buf_end); + gtk_text_buffer_remove_tag_by_name(buffer, "hidden", &line_start, &buf_end); + if (!lower_search) return; - char *lower_search = g_utf8_strdown(text, -1); - GtkTextIter line_start = start; while (!gtk_text_iter_is_end(&line_start)) { GtkTextIter line_end = line_start; if (!gtk_text_iter_ends_line(&line_end)) @@ -28,15 +46,20 @@ static void filter_text_view(GtkTextBuffer *buffer, const char *text) { g_free(line_text); gtk_text_iter_forward_line(&line_start); } - g_free(lower_search); +} + +static void set_filter(View *v, const char *text) { + g_free(v->filter); + v->filter = (text && *text) ? g_utf8_strdown(text, -1) : NULL; + if (*v->buffer) filter_from_line(*v->buffer, 0, v->filter); } void on_search_conn_changed(GtkSearchEntry *entry, gpointer user_data) { - filter_text_view(conn_buffer, gtk_entry_get_text(GTK_ENTRY(entry))); + set_filter(&conn_view, gtk_entry_get_text(GTK_ENTRY(entry))); } void on_search_log_changed(GtkSearchEntry *entry, gpointer user_data) { - filter_text_view(log_buffer, gtk_entry_get_text(GTK_ENTRY(entry))); + set_filter(&log_view, gtk_entry_get_text(GTK_ENTRY(entry))); } void on_clear_conn_clicked(GtkButton *button, gpointer user_data) { @@ -47,61 +70,114 @@ void on_clear_log_clicked(GtkButton *button, gpointer user_data) { if (log_buffer) gtk_text_buffer_set_text(log_buffer, "", 0); } -// library strings are not guaranteed utf8 -static void append_line(GtkTextBuffer *buffer, char *line) { - if (buffer) { - if (!g_utf8_validate(line, -1, NULL)) { - char *fixed = g_utf8_make_valid(line, -1); - g_free(line); - line = fixed; - } - GtkTextIter end; - gtk_text_buffer_get_end_iter(buffer, &end); - gtk_text_buffer_insert(buffer, &end, line, -1); - trim_buffer_lines(buffer, MAX_LINES); +void on_pause_conn_toggled(GtkToggleButton *button, gpointer user_data) { + conn_view.paused = gtk_toggle_button_get_active(button); +} + +// one insert, one filter pass over the new lines and one trim per batch +static void append_batch(View *v, const char *text, uint64_t dropped) { + GtkTextBuffer *buffer = *v->buffer; + if (!buffer) return; + + GString *batch = g_string_new(text); + if (dropped) g_string_append_printf(batch, "[%" G_GUINT64_FORMAT " events skipped]\n", dropped); + if (!g_utf8_validate(batch->str, -1, NULL)) { + char *fixed = g_utf8_make_valid(batch->str, -1); + g_string_assign(batch, fixed); + g_free(fixed); + } + + GtkTextIter end; + gtk_text_buffer_get_end_iter(buffer, &end); + int first_line = gtk_text_iter_get_line(&end); + gtk_text_buffer_insert(buffer, &end, batch->str, (gint)batch->len); + g_string_free(batch, TRUE); + + if (v->filter) filter_from_line(buffer, first_line, v->filter); + + int excess = gtk_text_buffer_get_line_count(buffer) - 1 - v->max_lines; + if (excess > 0) { + GtkTextIter start, cut; + gtk_text_buffer_get_start_iter(buffer, &start); + gtk_text_buffer_get_iter_at_line(buffer, &cut, excess); + gtk_text_buffer_delete(buffer, &start, &cut); } - g_free(line); } -static gboolean update_log_gui(gpointer user_data) { - char *time_str = get_current_time_str(); - append_line(log_buffer, g_strdup_printf("%s %s\n", time_str, (char *)user_data)); - g_free(time_str); - g_free(user_data); - return FALSE; +static void drain_view(View *v) { + if (v->paused || !v->ring) return; + uint64_t dropped = 0; + size_t n = event_ring_drain(v->ring, drain_buf, DRAIN_BUF_SIZE, DRAIN_MAX, &dropped); + if (n || dropped) append_batch(v, drain_buf, dropped); } -static gboolean update_connection_gui_append(gpointer user_data) { - ConnectionData *d = (ConnectionData *)user_data; - append_line(conn_buffer, g_strdup_printf("%s %s (PID:%u) -> %s:%u via %s\n", - d->timestamp, d->process_name, d->pid, d->dest_ip, d->dest_port, d->proxy_info)); - g_free(d->process_name); - g_free(d->dest_ip); - g_free(d->proxy_info); - g_free(d->timestamp); - g_free(d); - return FALSE; +static gboolean drain_tick(gpointer user_data) { + drain_view(&conn_view); + drain_view(&log_view); + return G_SOURCE_CONTINUE; +} + +void gui_logs_init() { + drain_buf = g_malloc(DRAIN_BUF_SIZE); + conn_view.ring = event_ring_new(RING_CAPACITY, SLOT_SIZE); + log_view.ring = event_ring_new(RING_CAPACITY, SLOT_SIZE); + drain_source = g_timeout_add(DRAIN_INTERVAL_MS, drain_tick, NULL); +} + +void gui_logs_stop_timers() { + if (drain_source) g_source_remove(drain_source); + drain_source = 0; +} + +// call after the engine stopped, library threads push into the rings until then +void gui_logs_free() { + gui_logs_stop_timers(); + event_ring_free(conn_view.ring); + event_ring_free(log_view.ring); + conn_view.ring = log_view.ring = NULL; + g_free(conn_view.filter); + g_free(log_view.filter); + conn_view.filter = log_view.filter = NULL; + g_free(drain_buf); + drain_buf = NULL; +} + +uint64_t gui_conn_event_count() { + uint64_t pushed = 0; + if (conn_view.ring) event_ring_stats(conn_view.ring, &pushed, NULL, NULL); + return pushed; +} + +static void push_line(View *v, const char *fmt, ...) G_GNUC_PRINTF(2, 3); + +static void push_line(View *v, const char *fmt, ...) { + if (!v->ring) return; + char line[SLOT_SIZE]; + time_t now = time(NULL); + struct tm tm_now; + size_t off = 0; + if (localtime_r(&now, &tm_now)) off = strftime(line, sizeof(line), "[%H:%M:%S] ", &tm_now); + va_list args; + va_start(args, fmt); + g_vsnprintf(line + off, sizeof(line) - off, fmt, args); + va_end(args); + event_ring_push(v->ring, line); } void lib_log_callback(const char *message) { - g_idle_add(update_log_gui, g_strdup(message ? message : "")); + push_line(&log_view, "%s", message ? message : ""); } void gui_log(const char *format, ...) { + char msg[SLOT_SIZE]; va_list args; va_start(args, format); - char *msg = g_strdup_vprintf(format, args); + g_vsnprintf(msg, sizeof(msg), format, args); va_end(args); - g_idle_add(update_log_gui, msg); + lib_log_callback(msg); } void lib_connection_callback(const char *process_name, uint32_t pid, const char *dest_ip, uint16_t dest_port, const char *proxy_info) { - ConnectionData *data = g_malloc0(sizeof(ConnectionData)); - data->process_name = g_strdup(process_name ? process_name : ""); - data->pid = pid; - data->dest_ip = g_strdup(dest_ip ? dest_ip : ""); - data->dest_port = dest_port; - data->proxy_info = g_strdup(proxy_info ? proxy_info : ""); - data->timestamp = get_current_time_str(); - g_idle_add(update_connection_gui_append, data); + push_line(&conn_view, "%s (PID:%u) -> %s:%u via %s", process_name ? process_name : "", pid, + dest_ip ? dest_ip : "", dest_port, proxy_info ? proxy_info : ""); } diff --git a/Linux/gui/gui_settings.c b/Linux/gui/gui_settings.c index 22b11cba..a97d4f4a 100644 --- a/Linux/gui/gui_settings.c +++ b/Linux/gui/gui_settings.c @@ -76,7 +76,7 @@ static bool confirm(GtkWindow *parent, const char *format, ...) { static bool port_text_valid(const char *text, int *port_out) { size_t n = strlen(text); if (n == 0 || n > 5 || strspn(text, "0123456789") != n) return false; - int p = atoi(text); + int p = (int)parse_long(text); if (p < 1 || p > 65535) return false; *port_out = p; return true; @@ -123,7 +123,7 @@ static void on_start_test_clicked(GtkWidget *widget, gpointer data) { } const char *t_host = gtk_entry_get_text(GTK_ENTRY(info->test_host)); - int t_port = (int)safe_strtol(gtk_entry_get_text(GTK_ENTRY(info->test_port))); + int t_port = (int)parse_long(gtk_entry_get_text(GTK_ENTRY(info->test_port))); if (t_port <= 0 || t_port > 65535) t_port = 80; gtk_text_buffer_set_text(info->output_buffer, "Testing connection... Please wait...", -1); diff --git a/Linux/gui/gui_utils.c b/Linux/gui/gui_utils.c index a8c50b5b..13314962 100644 --- a/Linux/gui/gui_utils.c +++ b/Linux/gui/gui_utils.c @@ -1,9 +1,5 @@ #include "gui.h" -long safe_strtol(const char *nptr) { - return nptr ? strtol(nptr, NULL, 10) : 0; -} - void show_message(GtkWindow *parent, GtkMessageType type, const char *format, ...) { va_list args; va_start(args, format); @@ -17,23 +13,6 @@ void show_message(GtkWindow *parent, GtkMessageType type, const char *format, .. g_free(msg); } -void trim_buffer_lines(GtkTextBuffer *buffer, int max_lines) { - if (gtk_text_buffer_get_line_count(buffer) > max_lines) { - GtkTextIter start, next; - gtk_text_buffer_get_start_iter(buffer, &start); - next = start; - gtk_text_iter_forward_line(&next); - gtk_text_buffer_delete(buffer, &start, &next); - } -} - -char *get_current_time_str() { - GDateTime *now = g_date_time_new_now_local(); - char *s = g_date_time_format(now, "[%H:%M:%S]"); - g_date_time_unref(now); - return s; -} - GtkWidget *combo_new(const char *first, ...) { GtkWidget *combo = gtk_combo_box_text_new(); va_list args; diff --git a/Linux/gui/main.c b/Linux/gui/main.c index f2b8cc85..0cfc8679 100644 --- a/Linux/gui/main.c +++ b/Linux/gui/main.c @@ -8,8 +8,6 @@ GtkTextBuffer *log_buffer; GtkWidget *status_bar; guint status_context_id; -static bool g_engine_running = false; - GList *g_rules_list = NULL; bool g_chk_logging = true; bool g_chk_dns = true; @@ -211,7 +209,42 @@ static void on_about(GtkWidget *widget, gpointer data) { gtk_widget_destroy(dialog); } -// signal handlers run in the main loop (g_unix_signal_add), so no library calls from a real signal context +typedef enum { ENGINE_STOPPED, ENGINE_STARTING, ENGINE_RUNNING, ENGINE_STOPPING } EngineState; + +static EngineState engine_state = ENGINE_STOPPED; +static GThread *engine_thread = NULL; +static struct { bool start, ok; } engine_job; +static GtkWidget *status_info; +static GtkWidget *start_item, *stop_item; +static guint status_source = 0; +static uint64_t last_conn_count = 0; +static unsigned conn_rate = 0; + +static void update_status_info() { + static const char *names[] = {"Stopped", "Starting...", "Running", "Stopping..."}; + char *text = g_strdup_printf("Engine: %s | Profile: %s | Proxies: %u | Rules: %u | %u conn/s", + names[engine_state], g_active_profile, g_list_length(g_proxies_list), + g_list_length(g_rules_list), conn_rate); + gtk_label_set_text(GTK_LABEL(status_info), text); + g_free(text); + gtk_widget_set_sensitive(start_item, engine_state == ENGINE_STOPPED); + gtk_widget_set_sensitive(stop_item, engine_state == ENGINE_RUNNING); +} + +static void set_engine_state(EngineState state) { + engine_state = state; + update_status_info(); +} + +static gboolean status_tick(gpointer data) { + uint64_t now = gui_conn_event_count(); + conn_rate = (unsigned)(now - last_conn_count); + last_conn_count = now; + update_status_info(); + return G_SOURCE_CONTINUE; +} + +// runs in the main loop (g_unix_signal_add), so no library calls from a real signal context static gboolean on_unix_signal(gpointer data) { fprintf(stderr, "\nSignal received. Stopping ProxyBridge...\n"); gtk_main_quit(); @@ -219,28 +252,63 @@ static gboolean on_unix_signal(gpointer data) { } static void on_window_destroy(GtkWidget *widget, gpointer data) { - gtk_main_quit(); // engine is stopped once after gtk_main returns + if (status_source) g_source_remove(status_source); + status_source = 0; + gui_logs_stop_timers(); + gtk_main_quit(); } -static bool start_engine() { - if (g_engine_running) return true; - if (ProxyBridge_Start()) { - g_engine_running = true; - char *msg = g_strdup_printf("ProxyBridge Service Started (eBPF). Active profile: %s", g_active_profile); - gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, msg); - g_free(msg); - return true; +static gboolean engine_job_done(gpointer data) { + g_thread_join(engine_thread); + engine_thread = NULL; + if (!engine_job.start) { + set_engine_state(ENGINE_STOPPED); + gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "ProxyBridge Service Stopped."); + } else if (engine_job.ok) { + set_engine_state(ENGINE_RUNNING); + gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "ProxyBridge Service Started (eBPF)."); + } else { + set_engine_state(ENGINE_STOPPED); + gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "Failed to start ProxyBridge engine. See the Activity Logs tab."); + show_message(GTK_WINDOW(window), GTK_MESSAGE_ERROR, + "Failed to start the ProxyBridge engine.\n\n" + "Open the \"Activity Logs\" tab to see the reason.\n" + "The eBPF backend needs root, a kernel >= 5.11 with BTF and cgroup v2."); } - gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "Failed to start ProxyBridge engine. See the Activity Logs tab."); - show_message(GTK_WINDOW(window), GTK_MESSAGE_ERROR, - "Failed to start the ProxyBridge engine.\n\n" - "Open the \"Activity Logs\" tab to see the reason.\n" - "The eBPF backend needs root, a kernel >= 5.11 with BTF and cgroup v2."); - return false; + return G_SOURCE_REMOVE; +} + +static gpointer engine_job_thread(gpointer data) { + engine_job.ok = engine_job.start ? ProxyBridge_Start() : ProxyBridge_Stop(); + g_idle_add(engine_job_done, NULL); + return NULL; +} + +// Start/Stop can take a second (BPF load, /proc scan), keep them off the UI thread +static void engine_request(bool start) { + if (engine_thread || engine_state != (start ? ENGINE_STOPPED : ENGINE_RUNNING)) return; + engine_job.start = start; + set_engine_state(start ? ENGINE_STARTING : ENGINE_STOPPING); + engine_thread = g_thread_new("engine", engine_job_thread, NULL); } static void on_start_engine(GtkWidget *widget, gpointer data) { - start_engine(); + engine_request(true); +} + +static void on_stop_engine(GtkWidget *widget, gpointer data) { + engine_request(false); +} + +// exit path: wait for a pending job, then stop synchronously +static void shutdown_engine() { + if (engine_thread) { + g_thread_join(engine_thread); + engine_thread = NULL; + engine_state = engine_job.start && engine_job.ok ? ENGINE_RUNNING : ENGINE_STOPPED; + } + if (engine_state == ENGINE_RUNNING) ProxyBridge_Stop(); + engine_state = ENGINE_STOPPED; } static GtkWidget *add_menu_item(GtkWidget *menu, const char *label, GCallback cb) { @@ -269,7 +337,7 @@ static GtkWidget *add_submenu(GtkWidget *menubar, const char *label) { return menu; } -static GtkTextBuffer *add_text_tab(GtkWidget *notebook, const char *title, const char *placeholder, GCallback on_search, GCallback on_clear) { +static GtkTextBuffer *add_text_tab(GtkWidget *notebook, const char *title, const char *placeholder, GCallback on_search, GCallback on_clear, GCallback on_pause) { GtkWidget *tab = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); gtk_container_set_border_width(GTK_CONTAINER(tab), 5); GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); @@ -280,6 +348,11 @@ static GtkTextBuffer *add_text_tab(GtkWidget *notebook, const char *title, const g_signal_connect(clear_btn, "clicked", on_clear, NULL); gtk_box_pack_start(GTK_BOX(toolbar), search, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(toolbar), clear_btn, FALSE, FALSE, 0); + if (on_pause) { + GtkWidget *pause_btn = gtk_toggle_button_new_with_label("Pause"); + g_signal_connect(pause_btn, "toggled", on_pause, NULL); + gtk_box_pack_start(GTK_BOX(toolbar), pause_btn, FALSE, FALSE, 0); + } gtk_box_pack_start(GTK_BOX(tab), toolbar, FALSE, FALSE, 0); GtkWidget *view = gtk_text_view_new(); @@ -326,7 +399,8 @@ int main(int argc, char *argv[]) { add_check_item(proxy_menu, "Enable Traffic Logging", g_chk_logging, G_CALLBACK(on_log_traffic_toggled)); add_check_item(proxy_menu, "DNS via Proxy", g_chk_dns, G_CALLBACK(on_dns_proxy_toggled)); add_separator(proxy_menu); - add_menu_item(proxy_menu, "Start Engine", G_CALLBACK(on_start_engine)); + start_item = add_menu_item(proxy_menu, "Start Engine", G_CALLBACK(on_start_engine)); + stop_item = add_menu_item(proxy_menu, "Stop Engine", G_CALLBACK(on_stop_engine)); add_menu_item(proxy_menu, "Exit", G_CALLBACK(on_window_destroy)); GtkWidget *about_menu = add_submenu(menubar, "About"); add_menu_item(about_menu, "About", G_CALLBACK(on_about)); @@ -335,12 +409,18 @@ int main(int argc, char *argv[]) { GtkWidget *notebook = gtk_notebook_new(); gtk_box_pack_start(GTK_BOX(vbox), notebook, TRUE, TRUE, 0); - conn_buffer = add_text_tab(notebook, "Connections", "Search connections...", G_CALLBACK(on_search_conn_changed), G_CALLBACK(on_clear_conn_clicked)); - log_buffer = add_text_tab(notebook, "Activity Logs", "Search logs...", G_CALLBACK(on_search_log_changed), G_CALLBACK(on_clear_log_clicked)); + conn_buffer = add_text_tab(notebook, "Connections", "Search connections...", G_CALLBACK(on_search_conn_changed), G_CALLBACK(on_clear_conn_clicked), G_CALLBACK(on_pause_conn_toggled)); + log_buffer = add_text_tab(notebook, "Activity Logs", "Search logs...", G_CALLBACK(on_search_log_changed), G_CALLBACK(on_clear_log_clicked), NULL); + GtkWidget *status_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); status_bar = gtk_statusbar_new(); status_context_id = gtk_statusbar_get_context_id(GTK_STATUSBAR(status_bar), "Status"); - gtk_box_pack_start(GTK_BOX(vbox), status_bar, FALSE, FALSE, 0); + status_info = gtk_label_new(""); + gtk_widget_set_margin_end(status_info, 10); + gtk_box_pack_start(GTK_BOX(status_box), status_bar, TRUE, TRUE, 0); + gtk_box_pack_end(GTK_BOX(status_box), status_info, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), status_box, FALSE, FALSE, 0); + gui_logs_init(); ProxyBridge_SetLogCallback(lib_log_callback); ProxyBridge_SetConnectionCallback(lib_connection_callback); @@ -353,10 +433,13 @@ int main(int argc, char *argv[]) { g_unix_signal_add(SIGTERM, on_unix_signal, NULL); gtk_widget_show_all(window); - start_engine(); + update_status_info(); + status_source = g_timeout_add_seconds(1, status_tick, NULL); + engine_request(true); gtk_main(); - if (g_engine_running) ProxyBridge_Stop(); + shutdown_engine(); + gui_logs_free(); config_free_all(); return 0; } diff --git a/Linux/gui/tests/events_test.c b/Linux/gui/tests/events_test.c new file mode 100644 index 00000000..f3736dc1 --- /dev/null +++ b/Linux/gui/tests/events_test.c @@ -0,0 +1,106 @@ +// event ring: ordering, drop accounting, multi-producer stress (run under ASan and TSan too) +#include "gui_events.h" +#include +#include +#include +#include +#include + +#define PRODUCERS 8 +#define PER_PRODUCER 200000 + +static int failures = 0; +#define CHECK(c) do { if (!(c)) { printf("FAIL line %d: %s\n", __LINE__, #c); failures++; } } while (0) + +static EventRing *ring; + +static void *producer(void *arg) { + int id = (int)(intptr_t)arg; + char buf[64]; + for (int i = 0; i < PER_PRODUCER; i++) { + snprintf(buf, sizeof(buf), "%d:%d", id, i); + event_ring_push(ring, buf); + } + return NULL; +} + +// "id:seq" without strto* +static int parse_event(const char *s, int *id, long *seq) { + int i = 0; + *id = 0; + for (; s[i] >= '0' && s[i] <= '9'; i++) *id = *id * 10 + (s[i] - '0'); + if (s[i++] != ':') return 0; + *seq = 0; + for (; s[i] >= '0' && s[i] <= '9'; i++) *seq = *seq * 10 + (s[i] - '0'); + return s[i] == '\n' || s[i] == '\0'; +} + +int main() { + // fifo + drop accounting on a tiny ring + EventRing *small = event_ring_new(4, 16); + char out[256]; + uint64_t dropped = 0; + char buf[16]; + for (int i = 0; i < 10; i++) { snprintf(buf, sizeof(buf), "e%d", i); event_ring_push(small, buf); } + CHECK(event_ring_drain(small, out, sizeof(out), 100, &dropped) == 4); + CHECK(dropped == 6 && strcmp(out, "e6\ne7\ne8\ne9\n") == 0); + CHECK(event_ring_drain(small, out, sizeof(out), 100, &dropped) == 0 && dropped == 0 && out[0] == '\0'); + event_ring_push(small, "0123456789abcdefXYZ"); + CHECK(event_ring_drain(small, out, sizeof(out), 100, &dropped) == 1 && strcmp(out, "0123456789abcde\n") == 0); + event_ring_push(small, "a"); event_ring_push(small, "b"); event_ring_push(small, "c"); + CHECK(event_ring_drain(small, out, 6, 100, &dropped) == 2 && strcmp(out, "a\nb\n") == 0); + CHECK(event_ring_drain(small, out, sizeof(out), 1, &dropped) == 1 && strcmp(out, "c\n") == 0); + event_ring_free(small); + CHECK(event_ring_new(0, 8) == NULL); + + // multi producer stress with a slow consumer + ring = event_ring_new(4096, 512); + pthread_t th[PRODUCERS]; + for (long i = 0; i < PRODUCERS; i++) pthread_create(&th[i], NULL, producer, (void *)(intptr_t)i); + + static char big[256 * 1024]; + long last[PRODUCERS]; + for (int i = 0; i < PRODUCERS; i++) last[i] = -1; + uint64_t delivered = 0, reported_dropped = 0; + int done = 0; + for (int round = 0; ; round++) { + uint64_t d = 0; + size_t n = event_ring_drain(ring, big, sizeof(big), 2000, &d); + reported_dropped += d; + delivered += n; + size_t seen = 0; + for (char *p = big; *p; ) { + char *nl = strchr(p, '\n'); + int id = 0; long seq = 0; + CHECK(nl && parse_event(p, &id, &seq) && id >= 0 && id < PRODUCERS); + if (!nl || id < 0 || id >= PRODUCERS) break; + CHECK(seq > last[id]); + last[id] = seq; + seen++; + p = nl + 1; + } + CHECK(seen == n); + if (round % 4 == 0) usleep(1000); + if (!done) { + done = 1; + uint64_t pushed; + event_ring_stats(ring, &pushed, NULL, NULL); + done = pushed == (uint64_t)PRODUCERS * PER_PRODUCER; + } else if (n == 0) { + break; + } + } + for (int i = 0; i < PRODUCERS; i++) pthread_join(th[i], NULL); + + uint64_t pushed, del, drop; + event_ring_stats(ring, &pushed, &del, &drop); + printf("pushed=%llu delivered=%llu dropped=%llu\n", (unsigned long long)pushed, (unsigned long long)del, (unsigned long long)drop); + CHECK(pushed == (uint64_t)PRODUCERS * PER_PRODUCER); + CHECK(pushed == del + drop); + CHECK(del == delivered); + CHECK(drop == reported_dropped); + event_ring_free(ring); + + if (!failures) printf("events_test: all passed\n"); + return failures ? 1 : 0; +} diff --git a/Linux/gui/tests/run_tests.sh b/Linux/gui/tests/run_tests.sh index a950ef92..c09b9cf7 100755 --- a/Linux/gui/tests/run_tests.sh +++ b/Linux/gui/tests/run_tests.sh @@ -11,3 +11,9 @@ gcc -O1 -I../src -I. -D_GNU_SOURCE $(pkg-config --cflags gtk+-3.0) tests/config_ "$OUT/config_plain" ASAN_OPTIONS=detect_leaks=1 "$OUT/config_test" ASAN_OPTIONS=detect_leaks=1 "$OUT/json_test" +gcc -O2 -Wall -Wextra -I. tests/events_test.c gui_events.c -o "$OUT/events_plain" -pthread +time "$OUT/events_plain" +gcc -g -Wall -Wextra -fsanitize=address,undefined -I. tests/events_test.c gui_events.c -o "$OUT/events_asan" -pthread +ASAN_OPTIONS=detect_leaks=1 "$OUT/events_asan" +gcc -g -O1 -Wall -Wextra -fsanitize=thread -I. tests/events_test.c gui_events.c -o "$OUT/events_tsan" -pthread +setarch "$(uname -m)" -R "$OUT/events_tsan" From 6c63feac2a7ca4d9c8d2a520278d523c1b74c7bf Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 17:40:21 +0530 Subject: [PATCH 23/25] ci(linux): new build dependencies, portability job on older distros, docs --- .github/workflows/build-linux.yml | 23 ++++++++++++++++++++++- .github/workflows/release-linux.yml | 2 ++ Linux/README.md | 17 +++++++++++------ Linux/deploy.sh | 18 +++++++++--------- Linux/setup.sh | 20 ++++++++++---------- 5 files changed, 54 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 83c9c004..c298cf19 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -33,6 +33,8 @@ jobs: linux-tools-common \ linux-tools-generic \ libbpf-dev \ + libelf-dev \ + zlib1g-dev \ libgtk-3-dev \ pkg-config shell: bash @@ -85,7 +87,7 @@ jobs: run: | cd Linux chmod +x build.sh - ./build.sh + WERROR=1 ./build.sh shell: bash - name: Verify build output @@ -141,6 +143,25 @@ jobs: echo "✓ no libnetfilter dependency" shell: bash + - name: Portability - run the binaries on older distributions (issue 146) + # BIND_NOW makes the loader resolve every glibc symbol at start-up, so a clean --help run in these images proves the + # binaries do not need a newer glibc than Ubuntu 22.04 / Debian 12 / RHEL 9 era systems provide. + run: | + set -e + out="$PWD/Linux/output" + run_in() { # image, install command + echo "::group::$1" + docker run --rm -v "$out:/out:ro" "$1" sh -c "$2 >/dev/null 2>&1 && + LD_LIBRARY_PATH=/out /out/ProxyBridge --help | grep -q -- '--proxy' && echo 'CLI starts' && + missing=\$(LD_LIBRARY_PATH=/out ldd /out/ProxyBridge /out/libproxybridge.so /out/ProxyBridgeGUI 2>&1 | grep 'not found' || true) && + if [ -n \"\$missing\" ]; then echo \"missing: \$missing\"; exit 1; fi && echo 'all libraries resolve'" + echo "::endgroup::" + } + run_in ubuntu:22.04 "apt-get update && apt-get install -y libelf1 zlib1g libgtk-3-0" + run_in debian:12 "apt-get update && apt-get install -y libelf1 zlib1g libgtk-3-0" + run_in fedora:40 "dnf install -y elfutils-libelf zlib gtk3" + shell: bash + - name: Verify the build left the source tree clean run: | # build.sh compiles in a temp directory: no untracked or ignored file may appear outside Linux/output/ diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 9e41bdba..75527119 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -32,6 +32,8 @@ jobs: linux-tools-common \ linux-tools-generic \ libbpf-dev \ + libelf-dev \ + zlib1g-dev \ libgtk-3-dev \ pkg-config shell: bash diff --git a/Linux/README.md b/Linux/README.md index a47d9124..79a1cff8 100644 --- a/Linux/README.md +++ b/Linux/README.md @@ -34,7 +34,7 @@ curl -Lo deploy.sh https://raw.githubusercontent.com/InterceptSuite/ProxyBridge/ This script will: - Download the latest release from GitHub automatically - Detect your Linux distribution -- Install all required dependencies (libbpf, GTK3) +- Install all required dependencies (libelf, zlib, GTK3) - Install ProxyBridge CLI, GUI, and library to system paths - Update library cache (ldconfig) - Verify installation @@ -63,7 +63,7 @@ This script will: ``` The setup script will: -- Install runtime dependencies (libbpf, GTK3) +- Install runtime dependencies (libelf, zlib, GTK3) - Copy binaries to `/usr/local/bin` (ProxyBridge CLI and GUI) - Copy library to `/usr/local/lib` (libproxybridge.so) - Update library cache @@ -382,10 +382,15 @@ ProxyBridge hooks the **socket calls** (`connect()`, `sendmsg()`, `recvmsg()`) w - Linux kernel 5.11+ with BTF and cgroup v2 (to run); GCC, Make, **clang** and **bpftool** (to build the eBPF object and skeleton) - Development libraries: - - libbpf-dev (Debian/Ubuntu) or libbpf-devel (Fedora/RHEL) + - libelf-dev + zlib1g-dev (Debian/Ubuntu) or elfutils-libelf-devel + zlib-devel (Fedora/RHEL). libbpf itself is built from a pinned + release and linked statically, so it is not needed at run time + - curl (fetches the pinned libbpf source on first build) - libgtk-3-dev (optional, for GUI) or gtk3-devel (Fedora/RHEL) - pkg-config +Binaries are built against a glibc 2.34 baseline (Ubuntu 22.04, Debian 12, RHEL 9 and newer run them), with LTO, FORTIFY_SOURCE=3, +full RELRO, BIND_NOW, PIE, stack protector and CET flags; `Linux/tests/check_binaries.sh` audits this on every build (issue #146). + ### Building 1. **Install build dependencies:** @@ -394,19 +399,19 @@ ProxyBridge hooks the **socket calls** (`connect()`, `sendmsg()`, `recvmsg()`) w ```bash sudo apt-get update sudo apt-get install build-essential gcc make clang bpftool \ - libbpf-dev libgtk-3-dev pkg-config + libelf-dev zlib1g-dev curl libgtk-3-dev pkg-config ``` **Fedora:** ```bash sudo dnf install gcc make clang bpftool \ - libbpf-devel gtk3-devel pkg-config + elfutils-libelf-devel zlib-devel curl gtk3-devel pkg-config ``` **Arch/Manjaro:** ```bash sudo pacman -S base-devel gcc make clang bpf \ - libbpf gtk3 pkg-config + libelf zlib curl gtk3 pkg-config ``` 2. **Clone or download ProxyBridge source code:** diff --git a/Linux/deploy.sh b/Linux/deploy.sh index a9c64d35..7a51f1d1 100644 --- a/Linux/deploy.sh +++ b/Linux/deploy.sh @@ -150,35 +150,35 @@ install_dependencies() { ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian|mx|antix|deepin|lmde) echo "Using apt package manager..." apt-get update -qq - apt-get install -y libbpf1 libgtk-3-0 + apt-get install -y libelf1 zlib1g libgtk-3-0 ;; fedora) echo "Using dnf package manager..." - dnf install -y libbpf gtk3 + dnf install -y elfutils-libelf zlib gtk3 ;; rhel|centos|rocky|almalinux) echo "Using yum package manager..." - yum install -y libbpf gtk3 + yum install -y elfutils-libelf zlib gtk3 ;; arch|manjaro|endeavouros|garuda) echo "Using pacman package manager..." - pacman -Sy --noconfirm libbpf gtk3 + pacman -Sy --noconfirm libelf zlib gtk3 ;; opensuse*|sles) echo "Using zypper package manager..." - zypper install -y libbpf1 gtk3 + zypper install -y libelf1 libz1 gtk3 ;; void) echo "Using xbps package manager..." - xbps-install -Sy libbpf gtk3 + xbps-install -Sy libelf zlib gtk3 ;; *) echo "WARNING: Unknown distribution '$DISTRO' (family: '$DISTRO_LIKE')" echo "" echo "Please manually install the following packages:" - echo " Debian/Ubuntu: sudo apt install libbpf1 libgtk-3-0" - echo " Fedora: sudo dnf install libbpf gtk3" - echo " Arch: sudo pacman -S libbpf gtk3" + echo " Debian/Ubuntu: sudo apt install libelf1 zlib1g libgtk-3-0" + echo " Fedora: sudo dnf install elfutils-libelf zlib gtk3" + echo " Arch: sudo pacman -S libelf zlib gtk3" echo "" read -p "Continue anyway? (y/n) " -n 1 -r echo diff --git a/Linux/setup.sh b/Linux/setup.sh index 55a6ac44..aee4e952 100644 --- a/Linux/setup.sh +++ b/Linux/setup.sh @@ -53,36 +53,36 @@ install_dependencies() { ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian|mx|antix|deepin|lmde) echo "Using apt package manager..." apt-get update -qq - # libgtk-3-0 is for the GUI, libbpf for the eBPF datapath - apt-get install -y libbpf1 libgtk-3-0 + # libgtk-3-0 is for the GUI, libelf+zlib are what the statically linked libbpf needs + apt-get install -y libelf1 zlib1g libgtk-3-0 ;; fedora) echo "Using dnf package manager..." - dnf install -y libbpf gtk3 + dnf install -y elfutils-libelf zlib gtk3 ;; rhel|centos|rocky|almalinux) echo "Using yum package manager..." - yum install -y libbpf gtk3 + yum install -y elfutils-libelf zlib gtk3 ;; arch|manjaro|endeavouros|garuda) echo "Using pacman package manager..." - pacman -Sy --noconfirm libbpf gtk3 + pacman -Sy --noconfirm libelf zlib gtk3 ;; opensuse*|sles) echo "Using zypper package manager..." - zypper install -y libbpf1 gtk3 + zypper install -y libelf1 libz1 gtk3 ;; void) echo "Using xbps package manager..." - xbps-install -Sy libbpf gtk3 + xbps-install -Sy libelf zlib gtk3 ;; *) echo "WARNING: Unknown distribution '$DISTRO' (family: '$DISTRO_LIKE')" echo "" echo "Please manually install the following packages:" - echo " Debian/Ubuntu: sudo apt install libbpf1 libgtk-3-0" - echo " Fedora: sudo dnf install libbpf gtk3" - echo " Arch: sudo pacman -S libbpf gtk3" + echo " Debian/Ubuntu: sudo apt install libelf1 zlib1g libgtk-3-0" + echo " Fedora: sudo dnf install elfutils-libelf zlib gtk3" + echo " Arch: sudo pacman -S libelf zlib gtk3" echo "" read -p "Continue anyway? (y/n) " -n 1 -r echo From a036bd0be05d81b9c8e30c09f3c51568fbc114e6 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 18:14:55 +0530 Subject: [PATCH 24/25] chore(linux): version 4.0.0-Beta --- Linux/src/ProxyBridge.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Linux/src/ProxyBridge.h b/Linux/src/ProxyBridge.h index 89381dc9..267bb8f4 100644 --- a/Linux/src/ProxyBridge.h +++ b/Linux/src/ProxyBridge.h @@ -1,7 +1,7 @@ #ifndef PROXYBRIDGE_H #define PROXYBRIDGE_H -#define PROXYBRIDGE_VERSION "4.0.0" +#define PROXYBRIDGE_VERSION "4.0.0-Beta" #include #include From 985486430d06738e56e5cb27984803bc756dd767 Mon Sep 17 00:00:00 2001 From: anof-cyber Date: Wed, 14 Oct 2026 18:16:49 +0530 Subject: [PATCH 25/25] chore(linux): drop the obsolete crash-cleanup hint from the install scripts --- Linux/deploy.sh | 3 +-- Linux/setup.sh | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Linux/deploy.sh b/Linux/deploy.sh index 7a51f1d1..fad6731b 100644 --- a/Linux/deploy.sh +++ b/Linux/deploy.sh @@ -335,8 +335,7 @@ main() { fi echo " sudo ProxyBridge --proxy socks5://IP:PORT --rule \"app:*:*:TCP:PROXY\"" echo "" - echo "For cleanup after crash:" - echo " sudo ProxyBridge --cleanup" + echo "No cleanup is needed after a crash: the kernel detaches the eBPF programs automatically." echo "" } diff --git a/Linux/setup.sh b/Linux/setup.sh index aee4e952..6e5af115 100644 --- a/Linux/setup.sh +++ b/Linux/setup.sh @@ -229,8 +229,7 @@ main() { fi echo " sudo ProxyBridge --proxy socks5://IP:PORT --rule \"app:*:*:TCP:PROXY\"" echo "" - echo "For cleanup after crash:" - echo " sudo ProxyBridge --cleanup" + echo "No cleanup is needed after a crash: the kernel detaches the eBPF programs automatically." echo "" }