You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
503 lines
16 KiB
503 lines
16 KiB
// net_emulator.c - Network emulator for testing utun with delay, loss, reordering |
|
#define _POSIX_C_SOURCE 200809L |
|
#include "net_emulator.h" |
|
#include "u_async.h" |
|
#include "mem.h" |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <ctype.h> |
|
#include <errno.h> |
|
#include <unistd.h> |
|
#include <fcntl.h> |
|
#include <arpa/inet.h> |
|
#include <time.h> |
|
#include <sys/time.h> |
|
|
|
#define MAX_LINE_LEN 1024 |
|
#define TIME_BASE_MS 0.1 // u_async timebase is 0.1ms |
|
|
|
// Packet buffer with timestamp for delayed sending |
|
typedef struct { |
|
uint8_t data[MAX_PACKET_SIZE]; |
|
size_t len; |
|
struct sockaddr_in from_addr; |
|
struct sockaddr_in to_addr; |
|
endpoint_t *endpoint; |
|
struct timeval send_time; // When to send this packet |
|
} delayed_packet_t; |
|
|
|
// Global emulator state |
|
static net_emulator_t g_emulator = {0}; |
|
static int g_packet_counter = 0; |
|
|
|
// Helper function to trim whitespace |
|
static char* trim(char *str) { |
|
if (!str) return NULL; |
|
|
|
// Trim leading spaces |
|
while (isspace((unsigned char)*str)) str++; |
|
|
|
// Trim trailing spaces |
|
char *end = str + strlen(str) - 1; |
|
while (end > str && isspace((unsigned char)*end)) end--; |
|
*(end + 1) = '\0'; |
|
|
|
return str; |
|
} |
|
|
|
// Parse IP:port from string like "127.0.0.1:20000" |
|
static int parse_ip_port(const char *str, struct sockaddr_in *addr) { |
|
if (!str || !addr) return -1; |
|
|
|
char ip[64]; |
|
int port; |
|
|
|
// Find colon |
|
const char *colon = strchr(str, ':'); |
|
if (!colon) return -1; |
|
|
|
// Extract IP |
|
size_t ip_len = colon - str; |
|
if (ip_len >= sizeof(ip)) return -1; |
|
strncpy(ip, str, ip_len); |
|
ip[ip_len] = '\0'; |
|
|
|
// Parse port |
|
port = atoi(colon + 1); |
|
if (port <= 0 || port > 65535) return -1; |
|
|
|
// Convert IP |
|
if (inet_pton(AF_INET, ip, &addr->sin_addr) != 1) return -1; |
|
addr->sin_family = AF_INET; |
|
addr->sin_port = htons(port); |
|
|
|
return 0; |
|
} |
|
|
|
// Parse delay range like "100-130" |
|
static int parse_delay_range(const char *str, delay_range_t *range) { |
|
if (!str || !range) return -1; |
|
|
|
char *dash = strchr(str, '-'); |
|
if (!dash) { |
|
// Single value |
|
range->min_ms = atoi(str); |
|
range->max_ms = range->min_ms; |
|
} else { |
|
char min_str[32], max_str[32]; |
|
size_t min_len = dash - str; |
|
strncpy(min_str, str, min_len); |
|
min_str[min_len] = '\0'; |
|
strncpy(max_str, dash + 1, sizeof(max_str) - 1); max_str[sizeof(max_str) - 1] = '\0'; |
|
|
|
range->min_ms = atoi(min_str); |
|
range->max_ms = atoi(max_str); |
|
} |
|
|
|
if (range->min_ms < 0 || range->max_ms < range->min_ms) return -1; |
|
return 0; |
|
} |
|
|
|
// Parse probability and delay from "prob=40,delay=100-130" |
|
static int parse_rule(const char *line, rule_t *rule) { |
|
if (!line || !rule) return -1; |
|
|
|
char *prob_start = strstr(line, "prob="); |
|
if (!prob_start) return -1; |
|
|
|
char *comma = strchr(prob_start, ','); |
|
if (!comma) return -1; |
|
|
|
// Parse probability |
|
char prob_str[32]; |
|
strncpy(prob_str, prob_start + 5, comma - (prob_start + 5)); |
|
prob_str[comma - (prob_start + 5)] = '\0'; |
|
rule->probability_percent = atoi(prob_str); |
|
if (rule->probability_percent < 0 || rule->probability_percent > 100) return -1; |
|
|
|
// Parse delay |
|
char *delay_start = strstr(comma, "delay="); |
|
if (!delay_start) return -1; |
|
|
|
char delay_str[64]; |
|
strncpy(delay_str, delay_start + 6, sizeof(delay_str) - 1); delay_str[sizeof(delay_str) - 1] = '\0'; |
|
trim(delay_str); |
|
|
|
// Remove trailing ) if present (from comment) |
|
char *paren = strchr(delay_str, ')'); |
|
if (paren) *paren = '\0'; |
|
|
|
if (parse_delay_range(delay_str, &rule->delay) < 0) return -1; |
|
|
|
return 0; |
|
} |
|
|
|
// Parse configuration file |
|
int parse_emulator_config(const char *filename, net_emulator_t *emulator) { |
|
if (!filename || !emulator) return -1; |
|
|
|
FILE *fp = fopen(filename, "r"); |
|
if (!fp) { |
|
fprintf(stderr, "Failed to open config file: %s\n", filename); |
|
return -1; |
|
} |
|
|
|
char line[MAX_LINE_LEN]; |
|
endpoint_t *current_endpoint = NULL; |
|
|
|
while (fgets(line, sizeof(line), fp)) { |
|
// Remove newline |
|
line[strcspn(line, "\n")] = '\0'; |
|
char *trimmed = trim(line); |
|
|
|
// Skip empty lines and comments starting with ; |
|
if (strlen(trimmed) == 0 || trimmed[0] == ';') { |
|
continue; |
|
} |
|
|
|
// Check for endpoint section [ip:port] |
|
if (trimmed[0] == '[' && trimmed[strlen(trimmed) - 1] == ']') { |
|
// Extract ip:port inside brackets |
|
char endpoint_str[128]; |
|
strncpy(endpoint_str, trimmed + 1, sizeof(endpoint_str) - 1); |
|
endpoint_str[sizeof(endpoint_str) - 1] = '\0'; |
|
endpoint_str[strcspn(endpoint_str, "]")] = '\0'; |
|
trim(endpoint_str); |
|
|
|
// Remove any comments in parentheses |
|
char *paren = strchr(endpoint_str, '('); |
|
if (paren) *paren = '\0'; |
|
trim(endpoint_str); |
|
|
|
// Check if we have space for more endpoints |
|
if (emulator->endpoint_count >= MAX_ENDPOINTS) { |
|
fprintf(stderr, "Too many endpoints, maximum is %d\n", MAX_ENDPOINTS); |
|
fclose(fp); |
|
return -1; |
|
} |
|
|
|
current_endpoint = &emulator->endpoints[emulator->endpoint_count]; |
|
memset(current_endpoint, 0, sizeof(endpoint_t)); |
|
|
|
// Parse listen address |
|
if (parse_ip_port(endpoint_str, ¤t_endpoint->listen_addr) < 0) { |
|
fprintf(stderr, "Invalid endpoint format: %s\n", endpoint_str); |
|
fclose(fp); |
|
return -1; |
|
} |
|
|
|
// Calculate target address (port - 10000 for net_debug mode) |
|
uint16_t listen_port = ntohs(current_endpoint->listen_addr.sin_port); |
|
uint16_t target_port = listen_port; |
|
if (listen_port >= 10000) { |
|
target_port = listen_port - 10000; |
|
printf("[CONFIG] Forwarding %s:%u -> %s:%u (net_debug mode)\n", |
|
inet_ntoa(current_endpoint->listen_addr.sin_addr), listen_port, |
|
inet_ntoa(current_endpoint->listen_addr.sin_addr), target_port); |
|
} else { |
|
printf("[CONFIG] Forwarding %s:%u -> %s:%u (direct)\n", |
|
inet_ntoa(current_endpoint->listen_addr.sin_addr), listen_port, |
|
inet_ntoa(current_endpoint->listen_addr.sin_addr), target_port); |
|
} |
|
|
|
current_endpoint->target_addr = current_endpoint->listen_addr; |
|
current_endpoint->target_addr.sin_port = htons(target_port); |
|
|
|
emulator->endpoint_count++; |
|
continue; |
|
} |
|
|
|
// Parse rule if we're in an endpoint section |
|
if (current_endpoint) { |
|
// Check if line contains prob= (it's a rule) |
|
if (strstr(trimmed, "prob=")) { |
|
if (current_endpoint->rule_count >= MAX_RULES_PER_ENDPOINT) { |
|
fprintf(stderr, "Too many rules for endpoint, maximum is %d\n", |
|
MAX_RULES_PER_ENDPOINT); |
|
fclose(fp); |
|
return -1; |
|
} |
|
|
|
rule_t *rule = ¤t_endpoint->rules[current_endpoint->rule_count]; |
|
if (parse_rule(trimmed, rule) == 0) { |
|
current_endpoint->total_probability += rule->probability_percent; |
|
current_endpoint->rule_count++; |
|
|
|
if (current_endpoint->total_probability > 100) { |
|
fprintf(stderr, "Total probability exceeds 100%% for endpoint %s:%u\n", |
|
inet_ntoa(current_endpoint->listen_addr.sin_addr), |
|
ntohs(current_endpoint->listen_addr.sin_port)); |
|
fclose(fp); |
|
return -1; |
|
} |
|
} else { |
|
fprintf(stderr, "Invalid rule format: %s\n", trimmed); |
|
} |
|
} |
|
// Ignore lines in parentheses (comments) |
|
} |
|
} |
|
|
|
fclose(fp); |
|
|
|
// Validate configuration |
|
for (int i = 0; i < emulator->endpoint_count; i++) { |
|
endpoint_t *ep = &emulator->endpoints[i]; |
|
printf("[CONFIG] Endpoint %s:%u has %d rules (total prob=%d%%)\n", |
|
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port), |
|
ep->rule_count, ep->total_probability); |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
// Create UDP socket for endpoint |
|
static int create_endpoint_socket(endpoint_t *ep) { |
|
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); |
|
if (sockfd < 0) { |
|
perror("socket"); |
|
return -1; |
|
} |
|
|
|
// Enable address reuse |
|
int opt = 1; |
|
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { |
|
perror("setsockopt SO_REUSEADDR"); |
|
close(sockfd); |
|
return -1; |
|
} |
|
|
|
// Bind to listen address |
|
if (bind(sockfd, (struct sockaddr*)&ep->listen_addr, sizeof(ep->listen_addr)) < 0) { |
|
perror("bind"); |
|
close(sockfd); |
|
return -1; |
|
} |
|
|
|
// Set non-blocking |
|
int flags = fcntl(sockfd, F_GETFL, 0); |
|
if (flags < 0) { |
|
perror("fcntl GETFL"); |
|
close(sockfd); |
|
return -1; |
|
} |
|
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0) { |
|
perror("fcntl SETFL"); |
|
close(sockfd); |
|
return -1; |
|
} |
|
|
|
ep->sockfd = sockfd; |
|
return 0; |
|
} |
|
|
|
// Forward declaration |
|
static void send_delayed_packet(void *user_arg); |
|
|
|
// Socket callback for incoming packets |
|
static void socket_callback(int fd, void *user_arg) { |
|
endpoint_t *ep = (endpoint_t*)user_arg; |
|
if (!ep) return; |
|
|
|
uint8_t buffer[MAX_PACKET_SIZE]; |
|
struct sockaddr_in from_addr; |
|
socklen_t addr_len = sizeof(from_addr); |
|
|
|
ssize_t nread = recvfrom(fd, buffer, sizeof(buffer), 0, |
|
(struct sockaddr*)&from_addr, &addr_len); |
|
if (nread <= 0) { |
|
if (errno != EAGAIN && errno != EWOULDBLOCK) { |
|
perror("recvfrom"); |
|
} |
|
return; |
|
} |
|
|
|
g_packet_counter++; |
|
printf("[PACKET #%d] Received %zd bytes from %s:%u -> %s:%u\n", |
|
g_packet_counter, nread, |
|
inet_ntoa(from_addr.sin_addr), ntohs(from_addr.sin_port), |
|
inet_ntoa(ep->target_addr.sin_addr), ntohs(ep->target_addr.sin_port)); |
|
|
|
// Apply rules to decide fate of packet |
|
int random_val = rand() % 100; |
|
int cumulative_prob = 0; |
|
rule_t *selected_rule = NULL; |
|
|
|
for (int i = 0; i < ep->rule_count; i++) { |
|
cumulative_prob += ep->rules[i].probability_percent; |
|
if (random_val < cumulative_prob) { |
|
selected_rule = &ep->rules[i]; |
|
break; |
|
} |
|
} |
|
|
|
// If no rule selected (random_val >= total_probability), packet is dropped |
|
if (!selected_rule) { |
|
printf("[PACKET #%d] DROPPED (no matching rule)\n", g_packet_counter); |
|
return; |
|
} |
|
|
|
// Calculate delay |
|
int delay_ms = selected_rule->delay.min_ms; |
|
if (selected_rule->delay.max_ms > selected_rule->delay.min_ms) { |
|
delay_ms += rand() % (selected_rule->delay.max_ms - selected_rule->delay.min_ms + 1); |
|
} |
|
|
|
printf("[PACKET #%d] Applying delay: %d ms (rule prob=%d%%)\n", |
|
g_packet_counter, delay_ms, selected_rule->probability_percent); |
|
|
|
// Allocate packet buffer for delayed sending |
|
delayed_packet_t *packet = u_malloc(sizeof(delayed_packet_t)); |
|
if (!packet) { |
|
fprintf(stderr, "Failed to allocate packet buffer\n"); |
|
return; |
|
} |
|
|
|
memcpy(packet->data, buffer, nread); |
|
packet->len = nread; |
|
packet->from_addr = from_addr; |
|
packet->to_addr = ep->target_addr; |
|
packet->endpoint = ep; |
|
|
|
// Calculate send time |
|
gettimeofday(&packet->send_time, NULL); |
|
packet->send_time.tv_sec += delay_ms / 1000; |
|
packet->send_time.tv_usec += (delay_ms % 1000) * 1000; |
|
if (packet->send_time.tv_usec >= 1000000) { |
|
packet->send_time.tv_sec++; |
|
packet->send_time.tv_usec -= 1000000; |
|
} |
|
|
|
// Create timeout for delayed sending |
|
// Convert delay to u_async timebase (0.1ms) |
|
int delay_tb = delay_ms * 10; |
|
if (!ep->emulator || !ep->emulator->ua) { |
|
u_free(packet); |
|
return; |
|
} |
|
uasync_set_timeout(ep->emulator->ua, delay_tb, packet, (timeout_callback_t)send_delayed_packet); |
|
} |
|
|
|
// Callback for sending delayed packet |
|
static void send_delayed_packet(void *user_arg) { |
|
delayed_packet_t *packet = (delayed_packet_t*)user_arg; |
|
if (!packet) return; |
|
|
|
ssize_t nsent = sendto(packet->endpoint->sockfd, packet->data, packet->len, 0, |
|
(struct sockaddr*)&packet->to_addr, sizeof(packet->to_addr)); |
|
if (nsent != (ssize_t)packet->len) { |
|
perror("sendto"); |
|
printf("[PACKET] Failed to forward packet\n"); |
|
} else { |
|
printf("[PACKET] Forwarded %zd bytes to %s:%u\n", |
|
nsent, inet_ntoa(packet->to_addr.sin_addr), ntohs(packet->to_addr.sin_port)); |
|
} |
|
|
|
u_free(packet); |
|
} |
|
|
|
// Initialize network emulator |
|
int net_emulator_init(net_emulator_t *emulator) { |
|
if (!emulator) return -1; |
|
|
|
// Initialize random seed |
|
srand(time(NULL)); |
|
|
|
// Initialize u_async instance |
|
emulator->ua = uasync_create(); |
|
if (!emulator->ua) { |
|
fprintf(stderr, "Failed to create uasync instance\n"); |
|
return -1; |
|
} |
|
|
|
// Create sockets for all endpoints |
|
for (int i = 0; i < emulator->endpoint_count; i++) { |
|
endpoint_t *ep = &emulator->endpoints[i]; |
|
if (create_endpoint_socket(ep) < 0) { |
|
fprintf(stderr, "Failed to create socket for endpoint %s:%u\n", |
|
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port)); |
|
uasync_destroy(emulator->ua); |
|
emulator->ua = NULL; |
|
return -1; |
|
} |
|
|
|
ep->emulator = emulator; |
|
|
|
// Add socket to u_async |
|
uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, ep); |
|
printf("[INIT] Listening on %s:%u\n", |
|
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port)); |
|
} |
|
|
|
emulator->running = 1; |
|
return 0; |
|
} |
|
|
|
// Run network emulator main loop |
|
void net_emulator_run(net_emulator_t *emulator) { |
|
if (!emulator || !emulator->running || !emulator->ua) return; |
|
|
|
printf("[EMULATOR] Starting main loop. Press Ctrl+C to stop.\n"); |
|
uasync_mainloop(emulator->ua); // This never returns |
|
} |
|
|
|
// Clean up emulator resources |
|
void net_emulator_cleanup(net_emulator_t *emulator) { |
|
if (!emulator) return; |
|
|
|
for (int i = 0; i < emulator->endpoint_count; i++) { |
|
endpoint_t *ep = &emulator->endpoints[i]; |
|
if (ep->sockfd >= 0) { |
|
close(ep->sockfd); |
|
ep->sockfd = -1; |
|
} |
|
} |
|
|
|
if (emulator->ua) { |
|
uasync_destroy(emulator->ua); |
|
emulator->ua = NULL; |
|
} |
|
|
|
emulator->running = 0; |
|
} |
|
|
|
// Main function |
|
int main(int argc, char *argv[]) { |
|
setbuf(stdout, NULL); |
|
setbuf(stderr, NULL); |
|
|
|
if (argc != 2) { |
|
fprintf(stderr, "Usage: %s <config_file>\n", argv[0]); |
|
fprintf(stderr, "Example: %s net_emulator.conf\n", argv[0]); |
|
return 1; |
|
} |
|
|
|
printf("Network Emulator for utun net_debug mode\n"); |
|
printf("=========================================\n"); |
|
|
|
// Parse configuration |
|
if (parse_emulator_config(argv[1], &g_emulator) < 0) { |
|
fprintf(stderr, "Failed to parse configuration\n"); |
|
return 1; |
|
} |
|
|
|
if (g_emulator.endpoint_count == 0) { |
|
fprintf(stderr, "No endpoints configured\n"); |
|
return 1; |
|
} |
|
|
|
// Initialize emulator |
|
if (net_emulator_init(&g_emulator) < 0) { |
|
fprintf(stderr, "Failed to initialize emulator\n"); |
|
net_emulator_cleanup(&g_emulator); |
|
return 1; |
|
} |
|
|
|
// Run main loop |
|
net_emulator_run(&g_emulator); |
|
|
|
// Cleanup (never reached) |
|
net_emulator_cleanup(&g_emulator); |
|
return 0; |
|
} |