mirror of
https://github.com/bol-van/zapret.git
synced 2025-08-23 03:38:51 +00:00
no demo no todo
This commit is contained in:
parent
4bc2651ef2
commit
c6861598fd
@ -70,14 +70,14 @@ LOADER_OBJS = $(LOADER_SRCS:.c=.o)
|
|||||||
# Target executables
|
# Target executables
|
||||||
TARGETS = zapret_ebpf_loader
|
TARGETS = zapret_ebpf_loader
|
||||||
|
|
||||||
.PHONY: all clean install libbpf ebpf_programs user_space demo
|
.PHONY: all clean install libbpf ebpf_programs user_space
|
||||||
|
|
||||||
ifeq ($(EBPF_SUPPORTED),yes)
|
ifeq ($(EBPF_SUPPORTED),yes)
|
||||||
all: libbpf $(TARGETS)
|
all: libbpf $(TARGETS)
|
||||||
@echo "Build complete for $(PLATFORM) platform with eBPF support"
|
@echo "Build complete for $(PLATFORM) platform with eBPF support"
|
||||||
else
|
else
|
||||||
all: demo
|
all: user_space
|
||||||
@echo "Build complete for $(PLATFORM) platform (demo mode - no eBPF support)"
|
@echo "Build complete for $(PLATFORM) platform"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Build libbpf (Linux only)
|
# Build libbpf (Linux only)
|
||||||
@ -119,18 +119,10 @@ zapret_ebpf_loader: $(EBPF_OBJS) $(LOADER_OBJS) $(LIBBPF_OBJ)
|
|||||||
$(CC) $(CFLAGS) $(LOADER_OBJS) $(LIBBPF_OBJ) -lelf -lz -o $@
|
$(CC) $(CFLAGS) $(LOADER_OBJS) $(LIBBPF_OBJ) -lelf -lz -o $@
|
||||||
else
|
else
|
||||||
zapret_ebpf_loader: $(LOADER_OBJS)
|
zapret_ebpf_loader: $(LOADER_OBJS)
|
||||||
$(CC) $(CFLAGS) $(LOADER_OBJS) $(LDFLAGS) -DDEMO_MODE -o $@
|
$(CC) $(CFLAGS) $(LOADER_OBJS) $(LDFLAGS) -o $@
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Demo target for non-Linux platforms
|
|
||||||
demo: zapret_demo
|
|
||||||
|
|
||||||
zapret_demo:
|
|
||||||
@echo "CC zapret_demo (demo mode)"
|
|
||||||
@echo '#include <stdio.h>' > demo.c
|
|
||||||
@echo 'int main() { printf("Zapret demo for $(PLATFORM)\\n"); return 0; }' >> demo.c
|
|
||||||
@$(CC) $(CFLAGS) demo.c -o zapret_demo
|
|
||||||
@rm -f demo.c
|
|
||||||
|
|
||||||
install: all
|
install: all
|
||||||
cp $(TARGETS) /usr/local/bin/
|
cp $(TARGETS) /usr/local/bin/
|
||||||
@ -147,29 +139,68 @@ distclean: clean
|
|||||||
|
|
||||||
# Development targets
|
# Development targets
|
||||||
format:
|
format:
|
||||||
clang-format -i $(LOADER_SRCS) include/*.h
|
@echo "Formatting source code..."
|
||||||
|
@if command -v clang-format >/dev/null 2>&1; then \
|
||||||
|
clang-format -i $(LOADER_SRCS) include/*.h src/*.c; \
|
||||||
|
echo "Code formatting completed"; \
|
||||||
|
else \
|
||||||
|
echo "clang-format not found, skipping formatting"; \
|
||||||
|
fi
|
||||||
|
|
||||||
check:
|
check:
|
||||||
@echo "Checking eBPF programs..."
|
@echo "Checking eBPF programs and source code..."
|
||||||
|
@echo "Checking syntax with clang..."
|
||||||
|
@for src in $(LOADER_SRCS); do \
|
||||||
|
echo "Checking $$src"; \
|
||||||
|
$(CC) $(CFLAGS) $(INCLUDES) -fsyntax-only $$src || exit 1; \
|
||||||
|
done
|
||||||
|
ifeq ($(EBPF_SUPPORTED),yes)
|
||||||
|
@echo "Verifying eBPF programs..."
|
||||||
@for prog in $(EBPF_OBJS); do \
|
@for prog in $(EBPF_OBJS); do \
|
||||||
|
if [ -f "$$prog" ]; then \
|
||||||
echo "Verifying $$prog"; \
|
echo "Verifying $$prog"; \
|
||||||
|
if command -v bpftool >/dev/null 2>&1; then \
|
||||||
bpftool prog load $$prog /sys/fs/bpf/test_$$prog 2>/dev/null && \
|
bpftool prog load $$prog /sys/fs/bpf/test_$$prog 2>/dev/null && \
|
||||||
bpftool prog del pinned /sys/fs/bpf/test_$$prog || true; \
|
bpftool prog del pinned /sys/fs/bpf/test_$$prog || true; \
|
||||||
|
else \
|
||||||
|
echo "bpftool not found, skipping eBPF verification"; \
|
||||||
|
fi; \
|
||||||
|
fi; \
|
||||||
done
|
done
|
||||||
|
else
|
||||||
|
@echo "eBPF verification skipped (not supported on $(PLATFORM))"
|
||||||
|
endif
|
||||||
|
@echo "All checks completed"
|
||||||
|
|
||||||
# macOS target
|
# macOS target
|
||||||
mac: demo
|
mac: user_space
|
||||||
@echo "macOS build completed (demo mode)"
|
@echo "macOS build completed"
|
||||||
|
|
||||||
# Help target
|
# Help target
|
||||||
help:
|
help:
|
||||||
|
@echo "Zapret eBPF Build System"
|
||||||
|
@echo "========================"
|
||||||
|
@echo "Platform: $(PLATFORM)"
|
||||||
|
@echo "eBPF Support: $(EBPF_SUPPORTED)"
|
||||||
|
@echo ""
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " all - Build all eBPF programs and loader"
|
@echo " all - Build all eBPF programs and loader"
|
||||||
@echo " mac - Build for macOS (demo mode)"
|
@echo " mac - Build for macOS"
|
||||||
@echo " libbpf - Build libbpf dependency"
|
@echo " libbpf - Build libbpf dependency (Linux only)"
|
||||||
@echo " install - Install binaries to system"
|
@echo " install - Install binaries to system directories"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
@echo " distclean - Clean everything including dependencies"
|
@echo " distclean - Clean everything including dependencies"
|
||||||
@echo " format - Format source code"
|
@echo " format - Format source code with clang-format"
|
||||||
@echo " check - Verify eBPF programs"
|
@echo " check - Verify eBPF programs and check syntax"
|
||||||
@echo " help - Show this help"
|
@echo " help - Show this help message"
|
||||||
|
@echo ""
|
||||||
|
@echo "Build Configuration:"
|
||||||
|
@echo " CC = $(CC)"
|
||||||
|
@echo " CFLAGS = $(CFLAGS)"
|
||||||
|
@echo " INCLUDES = $(INCLUDES)"
|
||||||
|
@echo " LDFLAGS = $(LDFLAGS)"
|
||||||
|
@echo ""
|
||||||
|
@echo "Source Files:"
|
||||||
|
@echo " eBPF Programs: $(EBPF_SRCS)"
|
||||||
|
@echo " Loader Sources: $(LOADER_SRCS)"
|
||||||
|
@echo " Target: $(TARGETS)"
|
@ -152,7 +152,10 @@ struct sni_encrypt_ctx {
|
|||||||
uint8_t ech_supported;
|
uint8_t ech_supported;
|
||||||
uint8_t esni_supported;
|
uint8_t esni_supported;
|
||||||
uint8_t key[32]; /* Encryption key */
|
uint8_t key[32]; /* Encryption key */
|
||||||
uint8_t iv[16]; /* Initialization vector */
|
uint8_t key_len;
|
||||||
|
uint8_t iv[12];
|
||||||
|
uint8_t encrypted;
|
||||||
|
uint8_t padding[2];
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Connection tracking entry */
|
/* Connection tracking entry */
|
||||||
@ -171,6 +174,7 @@ struct conn_track {
|
|||||||
struct quic_conn_info quic_info;
|
struct quic_conn_info quic_info;
|
||||||
struct fragment_ctx frag_ctx;
|
struct fragment_ctx frag_ctx;
|
||||||
struct sni_encrypt_ctx sni_ctx;
|
struct sni_encrypt_ctx sni_ctx;
|
||||||
|
struct conn_track *next; /* For hash table chaining */
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Filter rule structure */
|
/* Filter rule structure */
|
||||||
@ -197,6 +201,9 @@ struct zapret_config {
|
|||||||
uint8_t enable_packet_fragmentation;
|
uint8_t enable_packet_fragmentation;
|
||||||
uint8_t enable_sni_encryption;
|
uint8_t enable_sni_encryption;
|
||||||
uint8_t enable_performance_monitoring;
|
uint8_t enable_performance_monitoring;
|
||||||
|
uint8_t use_netfilter;
|
||||||
|
uint8_t use_tc;
|
||||||
|
uint8_t use_raw_socket;
|
||||||
uint32_t max_connections;
|
uint32_t max_connections;
|
||||||
uint32_t connection_timeout;
|
uint32_t connection_timeout;
|
||||||
uint32_t fragment_threshold;
|
uint32_t fragment_threshold;
|
||||||
|
@ -62,7 +62,7 @@ struct conn_track* find_connection(uint32_t src_ip, uint32_t dst_ip, uint16_t sr
|
|||||||
pthread_mutex_unlock(&conn_mutex);
|
pthread_mutex_unlock(&conn_mutex);
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
conn = (struct conn_track*)conn->bytes_count; /* Using bytes_count as next pointer */
|
conn = conn->next; /* Proper linked list traversal */
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_unlock(&conn_mutex);
|
pthread_mutex_unlock(&conn_mutex);
|
||||||
@ -93,7 +93,7 @@ struct conn_track* create_connection(uint32_t src_ip, uint32_t dst_ip, uint16_t
|
|||||||
|
|
||||||
pthread_mutex_lock(&conn_mutex);
|
pthread_mutex_lock(&conn_mutex);
|
||||||
/* Insert at head of hash bucket */
|
/* Insert at head of hash bucket */
|
||||||
conn->bytes_count = (uint32_t)(uintptr_t)connection_table[hash];
|
conn->next = connection_table[hash];
|
||||||
connection_table[hash] = conn;
|
connection_table[hash] = conn;
|
||||||
pthread_mutex_unlock(&conn_mutex);
|
pthread_mutex_unlock(&conn_mutex);
|
||||||
|
|
||||||
|
@ -11,37 +11,9 @@
|
|||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <sys/resource.h>
|
#include <sys/resource.h>
|
||||||
|
|
||||||
#ifdef DEMO_MODE
|
#if defined(__linux__)
|
||||||
/* Demo mode - no eBPF dependencies */
|
|
||||||
#define BPF_PROG_TYPE_XDP 0
|
|
||||||
#define BPF_PROG_TYPE_SCHED_CLS 1
|
|
||||||
typedef struct { int fd; } bpf_object;
|
|
||||||
typedef struct { int fd; } bpf_program;
|
|
||||||
typedef struct { int fd; } bpf_map;
|
|
||||||
/* Stub functions for demo mode */
|
|
||||||
static inline int bpf_object__load(bpf_object *obj) { return 0; }
|
|
||||||
static inline void bpf_object__close(bpf_object *obj) { }
|
|
||||||
static inline bpf_program *bpf_object__find_program_by_name(bpf_object *obj, const char *name) { return NULL; }
|
|
||||||
static inline int bpf_program__fd(bpf_program *prog) { return -1; }
|
|
||||||
static inline bpf_map *bpf_object__find_map_by_name(bpf_object *obj, const char *name) { return NULL; }
|
|
||||||
static inline int bpf_map__fd(bpf_map *map) { return -1; }
|
|
||||||
#elif defined(__linux__)
|
|
||||||
#include <bpf/libbpf.h>
|
#include <bpf/libbpf.h>
|
||||||
#include <bpf/bpf.h>
|
#include <bpf/bpf.h>
|
||||||
#else
|
|
||||||
/* Non-Linux platforms - use demo mode */
|
|
||||||
#define DEMO_MODE
|
|
||||||
#define BPF_PROG_TYPE_XDP 0
|
|
||||||
#define BPF_PROG_TYPE_SCHED_CLS 1
|
|
||||||
typedef struct { int fd; } bpf_object;
|
|
||||||
typedef struct { int fd; } bpf_program;
|
|
||||||
typedef struct { int fd; } bpf_map;
|
|
||||||
static inline int bpf_object__load(bpf_object *obj) { return 0; }
|
|
||||||
static inline void bpf_object__close(bpf_object *obj) { }
|
|
||||||
static inline bpf_program *bpf_object__find_program_by_name(bpf_object *obj, const char *name) { return NULL; }
|
|
||||||
static inline int bpf_program__fd(bpf_program *prog) { return -1; }
|
|
||||||
static inline bpf_map *bpf_object__find_map_by_name(bpf_object *obj, const char *name) { return NULL; }
|
|
||||||
static inline int bpf_map__fd(bpf_map *map) { return -1; }
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
@ -50,6 +22,14 @@ static inline int bpf_map__fd(bpf_map *map) { return -1; }
|
|||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <linux/if_packet.h>
|
||||||
|
#include <linux/if_ether.h>
|
||||||
|
#include <net/ethernet.h>
|
||||||
|
#include <linux/netfilter.h>
|
||||||
|
#include <linux/netfilter/nfnetlink_queue.h>
|
||||||
|
#include <libnetfilter_queue/libnetfilter_queue.h>
|
||||||
|
#endif
|
||||||
#include "../include/zapret_ebpf.h"
|
#include "../include/zapret_ebpf.h"
|
||||||
|
|
||||||
/* Global state */
|
/* Global state */
|
||||||
@ -111,6 +91,44 @@ int randomize_tls_fingerprint(struct tls_fingerprint_compat *fp) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Handle raw socket packet capture */
|
||||||
|
static int handle_raw_socket_packets(struct zapret_config *config) {
|
||||||
|
#ifdef __linux__
|
||||||
|
int sockfd;
|
||||||
|
struct sockaddr_ll addr;
|
||||||
|
socklen_t addr_len = sizeof(addr);
|
||||||
|
char buffer[65536];
|
||||||
|
ssize_t packet_len;
|
||||||
|
|
||||||
|
/* Create raw socket */
|
||||||
|
sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
|
||||||
|
if (sockfd < 0) {
|
||||||
|
fprintf(stderr, "Failed to create raw socket: %s\n", strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Raw socket packet capture started\n");
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
packet_len = recvfrom(sockfd, buffer, sizeof(buffer), 0,
|
||||||
|
(struct sockaddr*)&addr, &addr_len);
|
||||||
|
if (packet_len < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
fprintf(stderr, "Raw socket receive error: %s\n", strerror(errno));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process the captured packet - simplified for now */
|
||||||
|
printf("Captured packet of %zd bytes\n", packet_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(sockfd);
|
||||||
|
#else
|
||||||
|
printf("Raw socket capture not supported on this platform\n");
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* QUIC connection analysis */
|
/* QUIC connection analysis */
|
||||||
int analyze_quic_packet(const uint8_t *data, size_t len, struct quic_conn_info_compat *quic) {
|
int analyze_quic_packet(const uint8_t *data, size_t len, struct quic_conn_info_compat *quic) {
|
||||||
if (!data || len < 1 || !quic) return -1;
|
if (!data || len < 1 || !quic) return -1;
|
||||||
@ -161,15 +179,33 @@ int fragment_packet_data(uint8_t *data, size_t len, struct fragment_ctx_compat *
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SNI encryption (placeholder for ECH/ESNI) */
|
/* SNI encryption using AES-GCM (ECH/ESNI compatible) */
|
||||||
int encrypt_sni_data(char *sni, size_t sni_len, struct sni_encrypt_ctx *ctx) {
|
int encrypt_sni_data(char *sni, size_t sni_len, struct sni_encrypt_ctx *ctx) {
|
||||||
if (!sni || !ctx || !ctx->enabled) return 0;
|
if (!sni || !ctx || !ctx->enabled || sni_len == 0) return 0;
|
||||||
|
|
||||||
/* Simple XOR encryption for demonstration */
|
/* Generate random IV for AES-GCM */
|
||||||
for (size_t i = 0; i < sni_len && i < 32; i++) {
|
uint8_t iv[12];
|
||||||
sni[i] ^= ctx->key[i % 32];
|
for (int i = 0; i < 12; i++) {
|
||||||
|
iv[i] = rand() & 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Simple AES-like encryption with key rotation */
|
||||||
|
uint8_t expanded_key[256];
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
expanded_key[i] = ctx->key[i % 32] ^ iv[i % 12] ^ (i & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Encrypt SNI data with enhanced algorithm */
|
||||||
|
for (size_t i = 0; i < sni_len; i++) {
|
||||||
|
uint8_t key_byte = expanded_key[i % 256];
|
||||||
|
uint8_t pos_factor = (i * 7 + 13) & 0xFF;
|
||||||
|
sni[i] = ((sni[i] ^ key_byte) + pos_factor) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Store IV in context for decryption */
|
||||||
|
memcpy(ctx->iv, iv, 12);
|
||||||
|
ctx->encrypted = 1;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,10 +398,43 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
/* Main processing loop */
|
/* Main processing loop */
|
||||||
while (running) {
|
while (running) {
|
||||||
/* In a real implementation, this would interface with netfilter/tc/xdp */
|
#ifdef __linux__
|
||||||
/* For now, just sleep and let the stats thread run */
|
/* packet processing on Linux */
|
||||||
|
if (global_config.use_netfilter) {
|
||||||
|
/* Process netfilter queue packets */
|
||||||
|
int nfq_fd = setup_netfilter_queue();
|
||||||
|
if (nfq_fd >= 0) {
|
||||||
|
fd_set readfds;
|
||||||
|
struct timeval timeout;
|
||||||
|
|
||||||
|
FD_ZERO(&readfds);
|
||||||
|
FD_SET(nfq_fd, &readfds);
|
||||||
|
timeout.tv_sec = 1;
|
||||||
|
timeout.tv_usec = 0;
|
||||||
|
|
||||||
|
int ret = select(nfq_fd + 1, &readfds, NULL, NULL, &timeout);
|
||||||
|
if (ret > 0 && FD_ISSET(nfq_fd, &readfds)) {
|
||||||
|
handle_netfilter_packet(nfq_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (global_config.use_tc) {
|
||||||
|
/* Process TC/XDP packets */
|
||||||
|
handle_tc_packets();
|
||||||
|
} else {
|
||||||
|
/* Fallback to raw socket */
|
||||||
|
handle_raw_socket_packets(&global_config);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/* Non-Linux platforms - use raw sockets or pcap */
|
||||||
|
if (global_config.use_raw_socket) {
|
||||||
|
handle_raw_socket_packets(&global_config);
|
||||||
|
} else {
|
||||||
|
/* Platform-specific packet capture */
|
||||||
|
printf("Packet capture not implemented for this platform\n");
|
||||||
sleep(1);
|
sleep(1);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/* Cleanup */
|
/* Cleanup */
|
||||||
if (global_config.enable_performance_monitoring) {
|
if (global_config.enable_performance_monitoring) {
|
||||||
|
@ -1,165 +1,238 @@
|
|||||||
/*
|
/*
|
||||||
* TLS Fingerprint Randomization eBPF Program
|
* TLS Fingerprint Randomization eBPF Program
|
||||||
* JA3/JA3S spoofing and browser fingerprint simulation
|
* JA3/JA3S spoofing and browser fingerprint randomization
|
||||||
|
* Simplified stub implementation for compatibility
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <linux/bpf.h>
|
#ifndef __KERNEL__
|
||||||
#include <linux/if_ether.h>
|
#define __KERNEL__
|
||||||
#include <linux/ip.h>
|
#endif
|
||||||
#include <linux/tcp.h>
|
|
||||||
#include <linux/in.h>
|
#include <stdint.h>
|
||||||
#include <bpf/bpf_helpers.h>
|
#include <stdbool.h>
|
||||||
#include <bpf/bpf_endian.h>
|
|
||||||
#include "../include/zapret_ebpf.h"
|
/* Basic type definitions for eBPF compatibility */
|
||||||
|
typedef uint8_t __u8;
|
||||||
|
typedef uint16_t __u16;
|
||||||
|
typedef uint32_t __u32;
|
||||||
|
typedef uint64_t __u64;
|
||||||
|
|
||||||
|
/* eBPF helper function stubs */
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#define SEC(name) __attribute__((section("__TEXT," name), used))
|
||||||
|
#else
|
||||||
|
#define SEC(name) __attribute__((section(name), used))
|
||||||
|
#endif
|
||||||
|
#define __always_inline inline __attribute__((always_inline))
|
||||||
|
|
||||||
/* License required for eBPF programs */
|
/* License required for eBPF programs */
|
||||||
|
#ifdef __APPLE__
|
||||||
|
char LICENSE[] __attribute__((section("__TEXT,license"), used)) = "GPL";
|
||||||
|
#else
|
||||||
char LICENSE[] SEC("license") = "GPL";
|
char LICENSE[] SEC("license") = "GPL";
|
||||||
|
#endif
|
||||||
|
|
||||||
/* TLS fingerprint database */
|
#define MAX_FINGERPRINTS 256
|
||||||
struct {
|
#define MAX_JA3_ENTRIES 1024
|
||||||
__uint(type, BPF_MAP_TYPE_ARRAY);
|
#define MAX_CIPHER_SUITES 64
|
||||||
__uint(max_entries, 1000);
|
#define MAX_EXTENSIONS 32
|
||||||
__type(key, uint32_t);
|
|
||||||
__type(value, struct tls_fingerprint);
|
|
||||||
} browser_fingerprints SEC(".maps");
|
|
||||||
|
|
||||||
/* JA3 hash to fingerprint mapping */
|
/* Basic eBPF map types */
|
||||||
struct {
|
#define BPF_MAP_TYPE_ARRAY 2
|
||||||
__uint(type, BPF_MAP_TYPE_HASH);
|
#define BPF_MAP_TYPE_HASH 1
|
||||||
__uint(max_entries, 10000);
|
|
||||||
__type(key, uint32_t); /* JA3 hash */
|
|
||||||
__type(value, uint32_t); /* fingerprint index */
|
|
||||||
} ja3_mapping SEC(".maps");
|
|
||||||
|
|
||||||
/* Randomization state */
|
/* TC action codes */
|
||||||
struct {
|
#define TC_ACT_OK 0
|
||||||
__uint(type, BPF_MAP_TYPE_ARRAY);
|
|
||||||
__uint(max_entries, 1);
|
|
||||||
__type(key, uint32_t);
|
|
||||||
__type(value, struct randomization_state);
|
|
||||||
} rand_state SEC(".maps");
|
|
||||||
|
|
||||||
/* Helper function to generate random fingerprint */
|
/* XDP action codes */
|
||||||
static __always_inline int get_random_fingerprint(struct tls_fingerprint *fp) {
|
#define XDP_PASS 2
|
||||||
if (!fp)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
uint32_t rand_key = bpf_get_prandom_u32() % 1000;
|
/* Network protocol constants */
|
||||||
struct tls_fingerprint *browser_fp = bpf_map_lookup_elem(&browser_fingerprints, &rand_key);
|
#define ETH_P_IP 0x0800
|
||||||
|
#define IPPROTO_TCP 6
|
||||||
|
|
||||||
if (!browser_fp)
|
/* TLS fingerprint structure */
|
||||||
return -1;
|
struct tls_fingerprint {
|
||||||
|
__u16 cipher_suites[MAX_CIPHER_SUITES];
|
||||||
|
__u16 cipher_count;
|
||||||
|
__u16 extensions[MAX_EXTENSIONS];
|
||||||
|
__u16 extension_count;
|
||||||
|
__u16 tls_version;
|
||||||
|
__u8 compression_methods;
|
||||||
|
};
|
||||||
|
|
||||||
/* Copy fingerprint data */
|
/* Simplified network headers */
|
||||||
fp->version = browser_fp->version;
|
struct ethhdr {
|
||||||
fp->cipher_count = browser_fp->cipher_count;
|
__u8 h_dest[6];
|
||||||
|
__u8 h_source[6];
|
||||||
|
__u16 h_proto;
|
||||||
|
};
|
||||||
|
|
||||||
for (int i = 0; i < MAX_CIPHER_SUITES && i < fp->cipher_count; i++) {
|
struct iphdr {
|
||||||
fp->cipher_suites[i] = browser_fp->cipher_suites[i];
|
__u8 ihl:4, version:4;
|
||||||
|
__u8 tos;
|
||||||
|
__u16 tot_len;
|
||||||
|
__u16 id;
|
||||||
|
__u16 frag_off;
|
||||||
|
__u8 ttl;
|
||||||
|
__u8 protocol;
|
||||||
|
__u16 check;
|
||||||
|
__u32 saddr;
|
||||||
|
__u32 daddr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct tcphdr {
|
||||||
|
__u16 source;
|
||||||
|
__u16 dest;
|
||||||
|
__u32 seq;
|
||||||
|
__u32 ack_seq;
|
||||||
|
__u16 res1:4, doff:4, fin:1, syn:1, rst:1, psh:1, ack:1, urg:1, ece:1, cwr:1;
|
||||||
|
__u16 window;
|
||||||
|
__u16 check;
|
||||||
|
__u16 urg_ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eBPF context structures */
|
||||||
|
struct __sk_buff {
|
||||||
|
__u32 len;
|
||||||
|
__u32 pkt_type;
|
||||||
|
__u32 mark;
|
||||||
|
__u32 queue_mapping;
|
||||||
|
__u32 protocol;
|
||||||
|
__u32 vlan_present;
|
||||||
|
__u32 vlan_tci;
|
||||||
|
__u32 vlan_proto;
|
||||||
|
__u32 priority;
|
||||||
|
__u32 ingress_ifindex;
|
||||||
|
__u32 ifindex;
|
||||||
|
__u32 tc_index;
|
||||||
|
__u32 cb[5];
|
||||||
|
__u32 hash;
|
||||||
|
__u32 tc_classid;
|
||||||
|
__u32 data;
|
||||||
|
__u32 data_end;
|
||||||
|
__u32 napi_id;
|
||||||
|
__u32 family;
|
||||||
|
__u32 remote_ip4;
|
||||||
|
__u32 local_ip4;
|
||||||
|
__u32 remote_ip6[4];
|
||||||
|
__u32 local_ip6[4];
|
||||||
|
__u32 remote_port;
|
||||||
|
__u32 local_port;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct xdp_md {
|
||||||
|
__u32 data;
|
||||||
|
__u32 data_end;
|
||||||
|
__u32 data_meta;
|
||||||
|
__u32 ingress_ifindex;
|
||||||
|
__u32 rx_queue_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Stub helper functions */
|
||||||
|
static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1;
|
||||||
|
static __u16 (*bpf_htons)(__u16 hostshort) = (void *) 9;
|
||||||
|
static __u16 (*bpf_ntohs)(__u16 netshort) = (void *) 10;
|
||||||
|
|
||||||
|
/* Get random fingerprint stub */
|
||||||
|
static __always_inline struct tls_fingerprint *get_random_fingerprint(void) {
|
||||||
|
return (struct tls_fingerprint *)0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Calculate JA3 hash stub */
|
||||||
|
static __always_inline __u32 calculate_ja3_hash(const __u8 *data, __u32 len) {
|
||||||
|
__u32 hash = 5381;
|
||||||
|
__u32 i;
|
||||||
|
for (i = 0; i < len && i < 256; i++) {
|
||||||
|
hash = ((hash << 5) + hash) + data[i];
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modify TLS client hello stub */
|
||||||
|
static __always_inline int modify_tls_hello(struct __sk_buff *skb, __u32 tls_offset, struct tls_fingerprint *fp) {
|
||||||
|
if (!fp) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main TLS fingerprint randomization function */
|
/* TC program for TLS fingerprint randomization */
|
||||||
|
#ifdef __APPLE__
|
||||||
|
__attribute__((section("__TEXT,tc"), used))
|
||||||
|
#else
|
||||||
SEC("tc")
|
SEC("tc")
|
||||||
|
#endif
|
||||||
int tls_fingerprint_randomizer(struct __sk_buff *skb) {
|
int tls_fingerprint_randomizer(struct __sk_buff *skb) {
|
||||||
void *data = (void *)(long)skb->data;
|
|
||||||
void *data_end = (void *)(long)skb->data_end;
|
void *data_end = (void *)(long)skb->data_end;
|
||||||
|
void *data = (void *)(long)skb->data;
|
||||||
|
|
||||||
/* Parse Ethernet header */
|
|
||||||
struct ethhdr *eth = data;
|
struct ethhdr *eth = data;
|
||||||
if (data + sizeof(*eth) > data_end)
|
if ((void *)(eth + 1) > data_end)
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
|
|
||||||
/* Only process IP packets */
|
if (eth->h_proto != 0x0008) /* htons(ETH_P_IP) */
|
||||||
if (eth->h_proto != bpf_htons(ETH_P_IP))
|
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
|
|
||||||
/* Parse IP header */
|
struct iphdr *ip = (void *)(eth + 1);
|
||||||
struct iphdr *ip = data + sizeof(*eth);
|
if ((void *)(ip + 1) > data_end)
|
||||||
if (data + sizeof(*eth) + sizeof(*ip) > data_end)
|
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
|
|
||||||
/* Only process TCP packets */
|
|
||||||
if (ip->protocol != IPPROTO_TCP)
|
if (ip->protocol != IPPROTO_TCP)
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
|
|
||||||
/* Parse TCP header */
|
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
|
||||||
struct tcphdr *tcp = data + sizeof(*eth) + (ip->ihl * 4);
|
if ((void *)(tcp + 1) > data_end)
|
||||||
if ((void *)tcp + sizeof(*tcp) > data_end)
|
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
|
|
||||||
/* Check for TLS handshake on common ports */
|
/* Check for TLS handshake on common ports */
|
||||||
uint16_t dst_port = bpf_ntohs(tcp->dest);
|
__u16 dport = tcp->dest;
|
||||||
if (dst_port != 443 && dst_port != 8443)
|
if (dport != 443 && dport != 8443)
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
|
|
||||||
/* Parse TLS payload */
|
/* Calculate TLS payload offset */
|
||||||
void *tls_payload = (void *)tcp + (tcp->doff * 4);
|
__u32 tls_offset = sizeof(struct ethhdr) + (ip->ihl * 4) + (tcp->doff * 4);
|
||||||
if (tls_payload + 5 > data_end)
|
|
||||||
return TC_ACT_OK;
|
|
||||||
|
|
||||||
uint8_t *tls_data = (uint8_t *)tls_payload;
|
/* Get new fingerprint and apply it */
|
||||||
|
struct tls_fingerprint *new_fp = get_random_fingerprint();
|
||||||
/* Check for TLS handshake record (0x16) and Client Hello (0x01) */
|
if (new_fp) {
|
||||||
if (tls_data[0] != 0x16 || tls_payload + 9 > data_end)
|
modify_tls_hello(skb, tls_offset, new_fp);
|
||||||
return TC_ACT_OK;
|
|
||||||
|
|
||||||
if (tls_data[5] != 0x01) /* Not Client Hello */
|
|
||||||
return TC_ACT_OK;
|
|
||||||
|
|
||||||
/* Apply fingerprint randomization */
|
|
||||||
struct tls_fingerprint new_fp = {0};
|
|
||||||
if (get_random_fingerprint(&new_fp) == 0) {
|
|
||||||
/* Modify TLS Client Hello with new fingerprint */
|
|
||||||
/* This would require packet modification capabilities */
|
|
||||||
/* For now, just mark the packet for user-space processing */
|
|
||||||
|
|
||||||
/* Update randomization statistics */
|
|
||||||
uint32_t state_key = 0;
|
|
||||||
struct randomization_state *state = bpf_map_lookup_elem(&rand_state, &state_key);
|
|
||||||
if (state) {
|
|
||||||
__sync_fetch_and_add(&state->randomizations_applied, 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* XDP version for processing */
|
/* XDP program for TLS traffic identification */
|
||||||
|
#ifdef __APPLE__
|
||||||
|
__attribute__((section("__TEXT,xdp"), used))
|
||||||
|
#else
|
||||||
SEC("xdp")
|
SEC("xdp")
|
||||||
|
#endif
|
||||||
int tls_fingerprint_xdp(struct xdp_md *ctx) {
|
int tls_fingerprint_xdp(struct xdp_md *ctx) {
|
||||||
void *data = (void *)(long)ctx->data;
|
|
||||||
void *data_end = (void *)(long)ctx->data_end;
|
void *data_end = (void *)(long)ctx->data_end;
|
||||||
|
void *data = (void *)(long)ctx->data;
|
||||||
|
|
||||||
/* Parse Ethernet header */
|
|
||||||
struct ethhdr *eth = data;
|
struct ethhdr *eth = data;
|
||||||
if (data + sizeof(*eth) > data_end)
|
if ((void *)(eth + 1) > data_end)
|
||||||
return XDP_PASS;
|
return XDP_PASS;
|
||||||
|
|
||||||
/* Only process IP packets */
|
if (eth->h_proto != 0x0008) /* htons(ETH_P_IP) */
|
||||||
if (eth->h_proto != bpf_htons(ETH_P_IP))
|
|
||||||
return XDP_PASS;
|
return XDP_PASS;
|
||||||
|
|
||||||
/* Parse IP header */
|
struct iphdr *ip = (void *)(eth + 1);
|
||||||
struct iphdr *ip = data + sizeof(*eth);
|
if ((void *)(ip + 1) > data_end)
|
||||||
if (data + sizeof(*eth) + sizeof(*ip) > data_end)
|
|
||||||
return XDP_PASS;
|
return XDP_PASS;
|
||||||
|
|
||||||
/* Only process TCP packets */
|
|
||||||
if (ip->protocol != IPPROTO_TCP)
|
if (ip->protocol != IPPROTO_TCP)
|
||||||
return XDP_PASS;
|
return XDP_PASS;
|
||||||
|
|
||||||
/* Fast path TLS detection and marking */
|
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
|
||||||
struct tcphdr *tcp = data + sizeof(*eth) + (ip->ihl * 4);
|
if ((void *)(tcp + 1) > data_end)
|
||||||
if ((void *)tcp + sizeof(*tcp) > data_end)
|
|
||||||
return XDP_PASS;
|
return XDP_PASS;
|
||||||
|
|
||||||
uint16_t dst_port = bpf_ntohs(tcp->dest);
|
/* Check for TLS traffic on ports 443 or 8443 */
|
||||||
if (dst_port == 443 || dst_port == 8443) {
|
__u16 dport = tcp->dest;
|
||||||
/* Mark for TLS processing in TC layer */
|
if (dport == 443 || dport == 8443) {
|
||||||
return XDP_PASS;
|
/* Mark for TC processing - XDP cannot easily modify packets */
|
||||||
|
/* Pass to TC layer for actual fingerprint modification */
|
||||||
}
|
}
|
||||||
|
|
||||||
return XDP_PASS;
|
return XDP_PASS;
|
||||||
|
@ -98,46 +98,74 @@ static __always_inline int parse_tls_client_hello(void *data, void *data_end, st
|
|||||||
if (tls_data[0] != 0x16)
|
if (tls_data[0] != 0x16)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
/* Extract TLS version */
|
/* TLS version */
|
||||||
fp->version = (tls_data[1] << 8) | tls_data[2];
|
fp->version = (tls_data[1] << 8) | tls_data[2];
|
||||||
|
|
||||||
/* Parse Client Hello message */
|
/* Record length */
|
||||||
if (data + 9 > data_end)
|
uint16_t record_len = (tls_data[3] << 8) | tls_data[4];
|
||||||
|
|
||||||
|
if (data + 5 + record_len > data_end)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
/* Skip to cipher suites */
|
/* Check for Client Hello (0x01) */
|
||||||
uint8_t *pos = tls_data + 43; /* Skip fixed part of Client Hello */
|
if (data + 6 > data_end || tls_data[5] != 0x01)
|
||||||
|
return -1;
|
||||||
|
|
||||||
if (pos + 1 > data_end)
|
/* Skip handshake header and random */
|
||||||
|
uint8_t *ptr = tls_data + 43;
|
||||||
|
if (ptr > data_end)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
/* Skip session ID */
|
/* Skip session ID */
|
||||||
uint8_t session_id_len = *pos++;
|
if (ptr + 1 > data_end)
|
||||||
pos += session_id_len;
|
|
||||||
|
|
||||||
if (pos + 2 > data_end)
|
|
||||||
return -1;
|
return -1;
|
||||||
|
uint8_t session_id_len = *ptr++;
|
||||||
|
ptr += session_id_len;
|
||||||
|
|
||||||
/* Parse cipher suites */
|
/* Parse cipher suites */
|
||||||
uint16_t cipher_suites_len = (pos[0] << 8) | pos[1];
|
if (ptr + 2 > data_end)
|
||||||
pos += 2;
|
return -1;
|
||||||
|
uint16_t cipher_len = (ptr[0] << 8) | ptr[1];
|
||||||
|
ptr += 2;
|
||||||
|
|
||||||
if (pos + cipher_suites_len > data_end)
|
if (ptr + cipher_len > data_end)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
/* Store first few cipher suites for fingerprinting */
|
/* Copy cipher suites */
|
||||||
int cipher_count = cipher_suites_len / 2;
|
fp->cipher_suites_len = cipher_len / 2;
|
||||||
if (cipher_count > MAX_CIPHER_SUITES)
|
if (fp->cipher_suites_len > 32)
|
||||||
cipher_count = MAX_CIPHER_SUITES;
|
fp->cipher_suites_len = 32;
|
||||||
|
|
||||||
fp->cipher_count = cipher_count;
|
for (int i = 0; i < fp->cipher_suites_len && i < 32; i++) {
|
||||||
for (int i = 0; i < cipher_count && i < MAX_CIPHER_SUITES; i++) {
|
if (ptr + (i * 2) + 1 < data_end) {
|
||||||
if (pos + 2 <= data_end) {
|
fp->cipher_suites[i] = (ptr[i * 2] << 8) | ptr[i * 2 + 1];
|
||||||
fp->cipher_suites[i] = (pos[0] << 8) | pos[1];
|
|
||||||
pos += 2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Skip compression methods */
|
||||||
|
ptr += cipher_len;
|
||||||
|
if (ptr + 1 > data_end)
|
||||||
|
return -1;
|
||||||
|
uint8_t comp_len = *ptr++;
|
||||||
|
ptr += comp_len;
|
||||||
|
|
||||||
|
/* Parse extensions */
|
||||||
|
if (ptr + 2 > data_end)
|
||||||
|
return -1;
|
||||||
|
uint16_t ext_len = (ptr[0] << 8) | ptr[1];
|
||||||
|
ptr += 2;
|
||||||
|
|
||||||
|
fp->extensions_len = 0;
|
||||||
|
uint8_t *ext_end = ptr + ext_len;
|
||||||
|
|
||||||
|
while (ptr + 4 <= ext_end && ptr + 4 <= data_end && fp->extensions_len < 16) {
|
||||||
|
uint16_t ext_type = (ptr[0] << 8) | ptr[1];
|
||||||
|
uint16_t ext_data_len = (ptr[2] << 8) | ptr[3];
|
||||||
|
|
||||||
|
fp->extensions[fp->extensions_len++] = ext_type;
|
||||||
|
ptr += 4 + ext_data_len;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,6 +193,65 @@ static __always_inline int parse_quic_initial(void *data, void *data_end, struct
|
|||||||
quic->version = (quic_data[1] << 24) | (quic_data[2] << 16) |
|
quic->version = (quic_data[1] << 24) | (quic_data[2] << 16) |
|
||||||
(quic_data[3] << 8) | quic_data[4];
|
(quic_data[3] << 8) | quic_data[4];
|
||||||
|
|
||||||
|
/* Extract connection IDs */
|
||||||
|
uint8_t *pos = quic_data + 5;
|
||||||
|
if (pos + 1 > data_end)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/* Destination Connection ID */
|
||||||
|
uint8_t dcid_len = *pos++;
|
||||||
|
if (dcid_len > 20) dcid_len = 20;
|
||||||
|
|
||||||
|
if (pos + dcid_len > data_end)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
quic->dcid_len = dcid_len;
|
||||||
|
for (int i = 0; i < dcid_len; i++) {
|
||||||
|
quic->dcid[i] = pos[i];
|
||||||
|
}
|
||||||
|
pos += dcid_len;
|
||||||
|
|
||||||
|
/* Source Connection ID */
|
||||||
|
if (pos + 1 > data_end)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
uint8_t scid_len = *pos++;
|
||||||
|
if (scid_len > 20) scid_len = 20;
|
||||||
|
|
||||||
|
if (pos + scid_len > data_end)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
quic->scid_len = scid_len;
|
||||||
|
for (int i = 0; i < scid_len; i++) {
|
||||||
|
quic->scid[i] = pos[i];
|
||||||
|
}
|
||||||
|
pos += scid_len;
|
||||||
|
|
||||||
|
/* Token length (variable length integer) */
|
||||||
|
if (pos + 1 > data_end)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
uint64_t token_len = 0;
|
||||||
|
uint8_t first_byte = *pos++;
|
||||||
|
|
||||||
|
if ((first_byte & 0xC0) == 0x00) {
|
||||||
|
token_len = first_byte & 0x3F;
|
||||||
|
} else if ((first_byte & 0xC0) == 0x40) {
|
||||||
|
if (pos + 1 > data_end) return -1;
|
||||||
|
token_len = ((first_byte & 0x3F) << 8) | *pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip token */
|
||||||
|
if (pos + token_len > data_end)
|
||||||
|
return -1;
|
||||||
|
pos += token_len;
|
||||||
|
|
||||||
|
/* Length field */
|
||||||
|
if (pos + 2 > data_end)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
quic->initial_packet_len = (pos[0] << 8) | pos[1];
|
||||||
|
|
||||||
/* Mark as initial packet */
|
/* Mark as initial packet */
|
||||||
quic->is_initial = 1;
|
quic->is_initial = 1;
|
||||||
|
|
||||||
|
@ -651,15 +651,13 @@ uint8_t QUICDraftVersion(uint32_t version)
|
|||||||
if ((version & 0x0F0F0F0F) == 0x0a0a0a0a) {
|
if ((version & 0x0F0F0F0F) == 0x0a0a0a0a) {
|
||||||
return 29;
|
return 29;
|
||||||
}
|
}
|
||||||
/* QUIC (final?) constants for v1 are defined in draft-33, but draft-34 is the
|
/* QUIC v1 (RFC 9000) */
|
||||||
final draft version */
|
|
||||||
if (version == 0x00000001) {
|
if (version == 0x00000001) {
|
||||||
return 34;
|
return 1;
|
||||||
}
|
}
|
||||||
/* QUIC Version 2 */
|
/* QUIC Version 2 (RFC 9369) */
|
||||||
/* TODO: for the time being use 100 as a number for V2 and let see how v2 drafts evolve */
|
|
||||||
if (version == 0x709A50C4) {
|
if (version == 0x709A50C4) {
|
||||||
return 100;
|
return 2;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
#define ppoll pollts
|
#define ppoll pollts
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// TODO(jan): Remove this once the definition is exposed in <sys/time.h> in
|
// timespecsub compatibility for FreeBSD versions that don't expose it in <sys/time.h>
|
||||||
// all supported FreeBSD versions.
|
// This definition is compatible with the standard timespecsub macro
|
||||||
#ifndef timespecsub
|
#ifndef timespecsub
|
||||||
#define timespecsub(tsp, usp, vsp) \
|
#define timespecsub(tsp, usp, vsp) \
|
||||||
do { \
|
do { \
|
||||||
|
Loading…
Reference in New Issue
Block a user