diff options
23 files changed, 2462 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1269521 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +# need to make a test command to run them easily +CC = gcc +CFLAGS_BASE = -Wall -Wextra -Wpedantic -Werror -std=c99 -Isrc +CFLAGS_DEBUG = $(CFLAGS_BASE) -g -DDEBUG +CFLAGS_PROD = $(CFLAGS_BASE) -O2 -DNDEBUG +LDFLAGS = + +SRC_DIR = src +TEST_DIR = test +BUILD_DIR = build + +TARGET = hoodsgate +SOURCES := $(shell find $(SRC_DIR) -type f -name '*.c') +OBJECTS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCES)) + +CFLAGS = $(CFLAGS_DEBUG) +CFLAGS += -D_POSIX_C_SOURCE=200809L + +all: $(BUILD_DIR) $(TARGET) + +prod: CFLAGS = $(CFLAGS_PROD) +prod: clean all + @echo "Production build complete" + +debug: CFLAGS = $(CFLAGS_DEBUG) +debug: clean all + @echo "Debug build complete" + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +$(TARGET): $(OBJECTS) + $(CC) $(OBJECTS) $(LDFLAGS) -o $(TARGET) + +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -rf $(BUILD_DIR) $(TARGET) + +rebuild: clean all + +run: $(TARGET) + ./$(TARGET) + +.PHONY: all prod debug clean rebuild run @@ -0,0 +1,165 @@ +/* + * Control Plane Daemon for Tailscale Exit Node Router + * Routes traffic through eth0, Tor, or VPN based on policies + */ + +#include <arpa/inet.h> +#include <errno.h> +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#include <netinet/in.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/un.h> +#include <unistd.h> + +/* Handle control commands from clients */ +void handle_control_command(int client_fd, const char *cmd) { + char response[1024]; + char command[64], arg1[64], arg2[64], arg3[64]; + + log_event(EVENT_COMMAND_RECEIVED, 0, 0, "Command: %s", cmd); + + // Parse command + int n = sscanf(cmd, "%s %s %s %s", command, arg1, arg2, arg3); + + if (strcmp(command, "add-source-route") == 0 && n >= 3) { + // add-source-route <source_ip> <eth0|tor|vpn> [vpn_server] + struct in_addr addr; + if (inet_aton(arg1, &addr) == 0) { + snprintf(response, sizeof(response), "ERROR: Invalid IP address\n"); + send(client_fd, response, strlen(response), 0); + return; + } + + path_type_t path; + const char *vpn_server = NULL; + + if (strcmp(arg2, "eth0") == 0) + path = PATH_ETH0; + else if (strcmp(arg2, "tor") == 0) + path = PATH_TOR; + else if (strcmp(arg2, "vpn") == 0) { + path = PATH_VPN; + if (n < 4) { + snprintf(response, sizeof(response), + "ERROR: VPN server name required\n"); + send(client_fd, response, strlen(response), 0); + return; + } + vpn_server = arg3; + } else { + snprintf(response, sizeof(response), "ERROR: Invalid path type\n"); + send(client_fd, response, strlen(response), 0); + return; + } + + if (add_routing_policy(addr.s_addr, 0, path, vpn_server) == 0) { + snprintf(response, sizeof(response), "OK: Policy added\n"); + } else { + snprintf(response, sizeof(response), "ERROR: Failed to add policy\n"); + } + + } else if (strcmp(command, "add-mark-route") == 0 && n >= 3) { + // add-mark-route <mark_hex> <eth0|tor|vpn> [vpn_server] + uint32_t mark = strtoul(arg1, NULL, 16); + + path_type_t path; + const char *vpn_server = NULL; + + if (strcmp(arg2, "eth0") == 0) + path = PATH_ETH0; + else if (strcmp(arg2, "tor") == 0) + path = PATH_TOR; + else if (strcmp(arg2, "vpn") == 0) { + path = PATH_VPN; + if (n < 4) { + snprintf(response, sizeof(response), + "ERROR: VPN server name required\n"); + send(client_fd, response, strlen(response), 0); + return; + } + vpn_server = arg3; + } else { + snprintf(response, sizeof(response), "ERROR: Invalid path type\n"); + send(client_fd, response, strlen(response), 0); + return; + } + + if (add_routing_policy(0, mark, path, vpn_server) == 0) { + snprintf(response, sizeof(response), "OK: Mark policy added\n"); + } else { + snprintf(response, sizeof(response), + "ERROR: Failed to add mark policy\n"); + } + + } else if (strcmp(command, "list") == 0) { + snprintf(response, sizeof(response), "Active policies: %d\n", + cp_state.num_policies); + send(client_fd, response, strlen(response), 0); + + for (int i = 0; i < cp_state.num_policies; i++) { + routing_policy_t *p = &cp_state.policies[i]; + if (!p->active) + continue; + + snprintf(response, sizeof(response), + "[%d] src=%s mark=0x%x type=%s table=%d\n", i, + p->source_ip ? inet_ntoa((struct in_addr){p->source_ip}) : "any", + p->mark, + p->path_type == PATH_ETH0 ? "eth0" + : p->path_type == PATH_TOR ? "tor" + : p->vpn_server, + p->routing_table_id); + send(client_fd, response, strlen(response), 0); + } + return; + + } else if (strcmp(command, "status") == 0) { + snprintf(response, sizeof(response), + "Control Plane Status\n" + "Policies: %d/%d\n" + "VPN Configs: %d/%d\n" + "Events logged: %d\n", + cp_state.num_policies, MAX_POLICIES, cp_state.num_vpn_configs, + MAX_VPN_CONFIGS, cp_state.journal.count); + + } else if (strcmp(command, "events") == 0) { + // Query event journal + int max_events = 20; + if (n >= 2) { + max_events = atoi(arg1); + if (max_events > EVENT_JOURNAL_SIZE) + max_events = EVENT_JOURNAL_SIZE; + } + + event_entry_t events[EVENT_JOURNAL_SIZE]; + int count; + get_recent_events(events, max_events, &count); + + snprintf(response, sizeof(response), "Recent %d events:\n", count); + send(client_fd, response, strlen(response), 0); + + for (int i = 0; i < count; i++) { + event_entry_t *e = &events[i]; + snprintf(response, sizeof(response), + "[%lu.%06lu] data1=0x%x data2=0x%x %s\n", e->timestamp / 1000000, + e->timestamp % 1000000, e->data1, e->data2, e->message); + send(client_fd, response, strlen(response), 0); + } + return; + + } else { + snprintf(response, sizeof(response), + "ERROR: Unknown command. Available: add-source-route, " + "add-mark-route, list, status, events\n"); + } + + send(client_fd, response, strlen(response), 0); +} diff --git a/src/hoodsgate/control_plane/cp_policy_engine.c b/src/hoodsgate/control_plane/cp_policy_engine.c new file mode 100644 index 0000000..09487fc --- /dev/null +++ b/src/hoodsgate/control_plane/cp_policy_engine.c @@ -0,0 +1,205 @@ +/* +=============================================================================== +Copyright (c) 2026 Wayne Cole + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +=============================================================================== +*/ +// cp_policiy_engine.c -- control plane: policy engine implementation + +#include "cp_policy_engine.h" +#include "../hgcommon.h" +#include <arpa/inet.h> +#include <linux/fib_rules.h> +#include <linux/netfilter.h> +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/un.h> +#include <unistd.h> + +int cp_add_policy_route_int(uint32_t mark, uint32_t source_ip_ibytes, + int table_id) { + int sock; + struct { + struct nlmsghdr n; + struct rtmsg r; + char buf[4096]; + } req; + + sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock < 0) { + perror("netlink socket"); + return -1; + } + + memset(&req, 0, sizeof(req)); + + req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL; + req.n.nlmsg_type = RTM_NEWRULE; + + req.r.rtm_family = AF_INET; + req.r.rtm_table = table_id; + req.r.rtm_protocol = RTPROT_STATIC; + req.r.rtm_scope = RT_SCOPE_UNIVERSE; + req.r.rtm_type = RTN_UNICAST; + + // Add attributes (mark or source IP) + if (mark != 0) { + struct rtattr *rta = + (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.n.nlmsg_len)); + rta->rta_type = FRA_FWMARK; // Short for fowarding mark + rta->rta_len = RTA_LENGTH(sizeof(uint32_t)); + memcpy(RTA_DATA(rta), &mark, sizeof(uint32_t)); + req.n.nlmsg_len = NLMSG_ALIGN(req.n.nlmsg_len) + rta->rta_len; + } + + // Source ip being used to match + if (source_ip_ibytes != 0) { + struct rtattr *rta = + (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.n.nlmsg_len)); + rta->rta_type = FRA_SRC; + rta->rta_len = RTA_LENGTH(sizeof(uint32_t)); + memcpy(RTA_DATA(rta), &source_ip_ibytes, sizeof(uint32_t)); + req.n.nlmsg_len = NLMSG_ALIGN(req.n.nlmsg_len) + rta->rta_len; + } + + if (send(sock, &req, req.n.nlmsg_len, 0) < 0) { + perror("netlink send"); + close(sock); + return -1; + } + + close(sock); + + hgc_log_event_void( + EVENT_POLICY_ADD, mark, source_ip_ibytes, + "Added policy route: mark=0x%x src=%s table=%d", mark, + source_ip_ibytes ? inet_ntoa((struct in_addr){source_ip_ibytes}) : "any", + table_id); + + return 0; +} + +// Might use the lib for json +int cp_setup_nftables_mark_rules_int(uint32_t mark) { + char cmd[512]; + + // Rule to propagate socket mark to conntrack + snprintf(cmd, sizeof(cmd), + "nft add rule inet filter output meta mark 0x%x ct mark set 0x%x " + "2>/dev/null || true", + mark, mark); + system(cmd); + + // Rule to restore mark from conntrack for return traffic + snprintf(cmd, sizeof(cmd), + "nft add rule inet filter prerouting ct mark 0x%x meta mark set " + "0x%x 2>/dev/null || true", + mark, mark); + system(cmd); + + hgc_log_event_void(EVENT_NFTABLES_UPDATE, mark, 0, + "Setup nftables rules for mark 0x%x", mark); + return 0; +} + +int cp_setup_routing_table_int(int table_id, const char *interface, + const char *gateway) { + char cmd[512]; + // Add default route in the specific table + snprintf(cmd, sizeof(cmd), + "ip route add default via %s dev %s table %d 2>/dev/null || " + "ip route replace default via %s dev %s table %d", + gateway, interface, table_id, gateway, interface, table_id); + + if (system(cmd) != 0) { + fprintf(stderr, "Failed to setup routing table %d\n", table_id); + return -1; + } + + printf("Setup routing table %d via %s dev %s\n", table_id, gateway, + interface); + return 0; +} + +int cp_init_nftables() { + // TODO: check for duplicates, because this can mess stuff up. + // Create inet filter table if not exists + // Maybe need to add event logger here? + // sudo nft list ruleset | grep "specific_rule_part" + system("nft add table inet filter 2>/dev/null || true"); + system("nft add chain inet filter output { type filter hook output priority " + "0 \\; } 2>/dev/null || true"); + system("nft add chain inet filter prerouting { type filter hook prerouting " + "priority 0 \\; } 2>/dev/null || true"); + + printf("Initialized nftables\n"); + return 0; +} + +/* + * Setup control socket for receiving commands + * Only want the 'host' machine to have the + * control socket, so that you can't hit any + * device in the cluster and have access to the + * data plane + */ +int cp_setup_control_socket() { + int sock_fd; + struct sockaddr_un addr; + + sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock_fd < 0) { + perror("socket"); + return -1; + } + // Remove old socket file if exists + unlink(CONTROL_SOCKET_PATH); + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, CONTROL_SOCKET_PATH, sizeof(addr.sun_path) - 1); + + if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(sock_fd); + return -1; + } + + if (listen(sock_fd, 5) < 0) { + perror("listen"); + close(sock_fd); + return -1; + } + + // Set permissions; means that all users can read and write but cannot + // execute the file + chmod(CONTROL_SOCKET_PATH, 0666); + + printf("Control socket listening on %s\n", CONTROL_SOCKET_PATH); + return sock_fd; +} diff --git a/src/hoodsgate/control_plane/cp_policy_engine.h b/src/hoodsgate/control_plane/cp_policy_engine.h new file mode 100644 index 0000000..cc63e9f --- /dev/null +++ b/src/hoodsgate/control_plane/cp_policy_engine.h @@ -0,0 +1,34 @@ +#ifndef CP_POLICY_ENGINE_H +#define CP_POLICY_ENGINE_H + +#include "../hoodsgate.h" +#include <stdint.h> + +#define CONTROL_SOCKET_PATH "/var/run/hgated.sock" +#define MAX_POLICIES 256 + +/* ============================================================================ + * ROUTING POLICY - maps source/mark to egress path + * ============================================================================ + * *IMPORTANT!!!* + * uint32_t source_ip_ibytes; // Source IP to match (0 IF USING MARK instead) + * uint32_t mark; // Packet mark to match (0 IF USING SOURCE_IP instead) + */ +typedef struct { + int active; + uint32_t source_ip_ibytes; // Source IP to match (0 if using mark instead) + uint32_t mark; // Packet mark to match (0 if using source_ip instead) + hg_path_type_t path_type; + int routing_table_id; // Kernel routing table ID + char vpn_server[64]; // VPN server name if path_type == PATH_VPN +} cp_routing_policy_t; + +int cp_add_policy_route_int(uint32_t mark, uint32_t source_ip_ibytes, + int table_id); +int cp_setup_nftables_mark_rules_int(uint32_t mark); +int cp_setup_routing_table_int(int table_id, const char *interface, + const char *gateway); +int cp_init_nftables(void); +int cp_setup_control_socket(void); + +#endif diff --git a/src/hoodsgate/control_plane/cp_state_manager.c b/src/hoodsgate/control_plane/cp_state_manager.c new file mode 100644 index 0000000..652de5d --- /dev/null +++ b/src/hoodsgate/control_plane/cp_state_manager.c @@ -0,0 +1,96 @@ +/* +=============================================================================== +Copyright (c) 2026 Wayne Cole + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +=============================================================================== +*/ +// cp_state_manager.c -- control plane: state manager implementation + +#include "cp_state_manager.h" +#include <stdio.h> +#include <string.h> + +static cp_control_plane_t cp_state = { + .num_policies = 0, + .num_vpn_configs = 0, + .next_routing_table_id = 100, + .control_socket = -1, // default errors + .running = 1, + .journal = {.head = 0, .count = 0, .wrapped = 0}}; + +void cp_signal_handler_void(int signum) { + printf("\nReceived signal %d, shutting down...\n", signum); + cp_state.running = 0; +} + +cp_vpn_config_t *cp_get_or_create_vpn_config(const char *server) { + // Check if already exists + for (int i = 0; i < cp_state.num_vpn_configs; i++) { + if (strcmp(cp_state.vpn_configs[i].server_name, server) == 0) { + return &cp_state.vpn_configs[i]; + } + } + + // Create new + if (cp_state.num_vpn_configs >= MAX_VPN_CONFIGS) { + fprintf(stderr, "Max VPN configs reached\n"); + return NULL; + } + + cp_vpn_config_t *cfg = &cp_state.vpn_configs[cp_state.num_vpn_configs++]; + strncpy(cfg->server_name, server, sizeof(cfg->server_name) - 1); + cfg->routing_table_id = cp_state.next_routing_table_id++; + cfg->connected = 0; + + return cfg; +} + +// Need to figure out how I want to handle vpn interfaces???? +int cp_connect_vpn_int(cp_vpn_config_t *cfg) { + if (cfg->connected) { + hgc_log_event_void(EVENT_INFO, 0, 0, "VPN %s already connected", + cfg->server_name); + return 0; + } + + // TODO: Actually connect to VPN + // For now, simulate with a dummy interface + hgc_log_event_void(EVENT_VPN_CONNECT, 0, cfg->routing_table_id, + "Connecting to VPN: %s", cfg->server_name); + + // Simulate connection + snprintf(cfg->interface_name, sizeof(cfg->interface_name), "tun0"); + snprintf(cfg->endpoint, sizeof(cfg->endpoint), "10.8.0.1"); // Dummy gateway + cfg->connected = 1; + + // Setup routing table for this VPN + cp_setup_routing_table_int(cfg->routing_table_id, cfg->interface_name, + cfg->endpoint); + + hgc_log_event_void(EVENT_VPN_CONNECT, 0, cfg->routing_table_id, + "VPN %s connected on %s", cfg->server_name, + cfg->interface_name); + return 0; +} + +// Fallover host logic for the main host at the time to calculate +// and send to all exit nodes in the cluster in case of failure of +// main host +void cp_manage_fallover_hosts_void(cp_fallover_hosts_t *hosts) {}; diff --git a/src/hoodsgate/control_plane/cp_state_manager.h b/src/hoodsgate/control_plane/cp_state_manager.h new file mode 100644 index 0000000..4ecc1d5 --- /dev/null +++ b/src/hoodsgate/control_plane/cp_state_manager.h @@ -0,0 +1,56 @@ +#ifndef CP_STATE_MANAGER_H +#define CP_STATE_MANAGER_H + +#include "../hgcommon.h" +#include "cp_policy_engine.h" +#include <stdint.h> + +#define MAX_VPN_CONFIGS 32 +#define MAX_FALLOVER_HOST 2 + +#include <sys/types.h> + +typedef struct { + char server_name[64]; + char endpoint[128]; // VPN endpoint address + int connected; + int routing_table_id; + char interface_name[16]; // e.g., "tun0" +} cp_vpn_config_t; + +typedef struct { + char name[64]; + char ipv4[4]; + char ipv6[16]; + int online; + uint8_t priority; +} cp_host_t; + +typedef struct { + cp_host_t *hosts[MAX_FALLOVER_HOST]; +} cp_fallover_hosts_t; + +typedef struct { + cp_host_t cur_host; // hosting plane daemon that embedded server talks to + cp_fallover_hosts_t fb_hosts; // fallback + + cp_routing_policy_t policies[MAX_POLICIES]; + int num_policies; + + cp_vpn_config_t vpn_configs[MAX_VPN_CONFIGS]; + int num_vpn_configs; + + int next_routing_table_id; // Track next available routing table ID + int control_socket; + int running; + + hgc_event_journal_t journal; // Event journal +} cp_control_plane_t; + +static cp_control_plane_t cp_state; +void cp_signal_handler_void(int signum); +cp_vpn_config_t *cp_get_or_create_vpn_config(const char *server); +int cp_connect_vpn_int(cp_vpn_config_t *cfg); +void cp_manage_fallover_hosts_void(cp_fallover_hosts_t *hosts); + +#endif diff --git a/src/hoodsgate/control_plane/data_plane/dp_netlink_routes.c b/src/hoodsgate/control_plane/data_plane/dp_netlink_routes.c new file mode 100644 index 0000000..fce6bb8 --- /dev/null +++ b/src/hoodsgate/control_plane/data_plane/dp_netlink_routes.c @@ -0,0 +1 @@ +char *yo = "yo"; diff --git a/src/hoodsgate/control_plane/data_plane/dp_netlink_routes.h b/src/hoodsgate/control_plane/data_plane/dp_netlink_routes.h new file mode 100644 index 0000000..94a82df --- /dev/null +++ b/src/hoodsgate/control_plane/data_plane/dp_netlink_routes.h @@ -0,0 +1,6 @@ +#ifndef DP_NETLINK_ROUTES_H +#define DP_NETLINK_ROUTES_H + +void somefunc2(void); + +#endif diff --git a/src/hoodsgate/control_plane/data_plane/dp_nftables.c b/src/hoodsgate/control_plane/data_plane/dp_nftables.c new file mode 100644 index 0000000..255b5cc --- /dev/null +++ b/src/hoodsgate/control_plane/data_plane/dp_nftables.c @@ -0,0 +1 @@ +char *hello = "What is do?"; diff --git a/src/hoodsgate/control_plane/data_plane/dp_nftables.h b/src/hoodsgate/control_plane/data_plane/dp_nftables.h new file mode 100644 index 0000000..d2a8a32 --- /dev/null +++ b/src/hoodsgate/control_plane/data_plane/dp_nftables.h @@ -0,0 +1,6 @@ +#ifndef DP_NFTABLES_H +#define DP_NFTABLES_H + +void somefunc(void); + +#endif diff --git a/src/hoodsgate/hgcommon.c b/src/hoodsgate/hgcommon.c new file mode 100644 index 0000000..9fbd89f --- /dev/null +++ b/src/hoodsgate/hgcommon.c @@ -0,0 +1,160 @@ +/* +=============================================================================== +Copyright (c) 2026 Wayne Cole + +This file is inspired by the Quake III qcommon architecture. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +=============================================================================== +*/ +// hgcommon.c -- misc functions used in the control and data planes +// +// TODO: Implement event file handling; wipe after successful runs +// but if error or above persist event journal for debugging and +// playback + +#include "hgcommon.h" +#include "control_plane/cp_state_manager.h" +#include <bits/time.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <time.h> +#include <unistd.h> + +/* +=============================================================================== +EVENT JOURNAL - Single point for all logging and event playback +=============================================================================== +*/ + +#ifdef _WIN32 +#include <windows.h> +uint64_t get_timestamp_us() { return (uint64_t)GetTickCount64(); } +#else +uint64_t get_timestamp_us() { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + return (uint64_t)ts.tv_sec * 100000ULL + ts.tv_nsec / 1000; +} +#endif + +void hgc_log_event_void(hgc_event_type_t type, uint32_t data1, uint32_t data2, + const char *fmt, ...) { + hgc_event_journal_t *journal = &cp_state.journal; + hgc_event_entry_t *entry = &journal->entries[journal->head]; + + // filling the entry + entry->timestamp = get_timestamp_us(); + entry->type = type; + entry->data1 = data1; + entry->data2 = data2; + + // formatting message + va_list args; + va_start(args, fmt); + vsnprintf(entry->message, sizeof(entry->message), fmt, args); + va_end(args); + + const char *type_str[] = {"DAEMON_START", "DAEMON_STOP", "POLICY_ADD", + "POLICY_REMOVE", "VPN_CONNECT", "VPN_DISCONNECT", + "VPN_ERROR", "ROUTE_ADD", "ROUTE_REMOVE", + "NFTABLES_UPDATE", "COMMAND", "ERROR", + "WARNING", "INFO"}; + printf("[%s] %s\n", type_str[type], entry->message); + + // advance ring buffer + journal->head = (journal->head + 1) % EVENT_JOURNAL_SIZE; + journal->count++; + if (journal->count >= EVENT_JOURNAL_SIZE) { + journal->wrapped = 1; + } +} + +void hgc_get_recent_events_void(hgc_event_entry_t *out, int max_events, + int *count_actual) { + hgc_event_journal_t *journal = &cp_state.journal; + int available = journal->wrapped ? EVENT_JOURNAL_SIZE : journal->head; + int count = available < max_events ? available : max_events; + + int start = journal->wrapped ? journal->head : 0; + + for (int i = 0; i < count; i++) { + int idx = (start + i) % EVENT_JOURNAL_SIZE; + out[i] = journal->entries[idx]; + } + + *count_actual = count; +} + +bool hgc_save_logs_boolean(const hgc_event_journal_t *ej, const char *path) { + FILE *fs = fopen(path, "wb"); + if (!fs) { + hgc_log_event_void(EVENT_ERROR, 0, 0, "Could not open %s for wb", path); + return false; + } + + int count = ej->count; + if (fwrite(&count, sizeof(count), 1, fs) != 1) { + hgc_log_event_void(EVENT_ERROR, 0, 0, "Could not write to %s", path); + fclose(fs); + return false; + } + int start_index = + (ej->head - ej->count + EVENT_JOURNAL_SIZE) % EVENT_JOURNAL_SIZE; + for (int i = 0; i < ej->count; i++) { + int idx = (start_index + i) % EVENT_JOURNAL_SIZE; + fwrite(&ej, sizeof(cp_state.journal), 1, fs); + } + fclose(fs); + + return true; +} + +bool hgc_load_logs_boolean(hgc_event_journal_t *ej, const char *path) { + FILE *fs = fopen(path, "rb"); + if (!fs) { + hgc_log_event_void(EVENT_ERROR, 0, 0, "Could not open %s for rb", path); + return false; + } + int count = 0; + if (fread(&count, sizeof(count), 1, fs) != 1) { + hgc_log_event_void(EVENT_ERROR, 0, 0, "Could not read from %s", path); + fclose(fs); + return false; + } + + for (int i = 0; i < ej->count; i++) { + hgc_event_entry_t e; + if (fread(&e, sizeof(hgc_event_entry_t), 1, fs) != 1) { + fclose(fs); + return false; + } + + ej->entries[ej->head] = e; + ej->head = (ej->head + 1) % EVENT_JOURNAL_SIZE; + if (ej->count < EVENT_JOURNAL_SIZE) { + ej->count++; + } + } + fclose(fs); + + return true; +} diff --git a/src/hoodsgate/hgcommon.h b/src/hoodsgate/hgcommon.h new file mode 100644 index 0000000..330a416 --- /dev/null +++ b/src/hoodsgate/hgcommon.h @@ -0,0 +1,53 @@ +#ifndef HGCOMMON_H +#define HGCOMMON_H + +#define _POSIX_C_SOURCE 200809L +#include <stdint.h> +#include <time.h> + +#define EVENT_JOURNAL_SIZE 1024 +#define BUFFER_SIZE 4096 + +#include <stdbool.h> + +typedef enum { + EVENT_DAEMON_START = 0, + EVENT_DAEMON_STOP, + EVENT_POLICY_ADD, + EVENT_POLICY_REMOVE, + EVENT_VPN_CONNECT, + EVENT_VPN_DISCONNECT, + EVENT_VPN_ERROR, + EVENT_ROUTE_ADD, + EVENT_ROUTE_REMOVE, + EVENT_NFTABLES_UPDATE, + EVENT_COMMAND_RECEIVED, + EVENT_ERROR, + EVENT_WARNING, + EVENT_INFO +} hgc_event_type_t; + +typedef struct { + uint64_t timestamp; + hgc_event_type_t type; + uint32_t data1; // context dependent data + uint32_t data2; + char message[128]; // short message +} hgc_event_entry_t; + +typedef struct { + hgc_event_entry_t entries[EVENT_JOURNAL_SIZE]; + int head; + int count; + int wrapped; +} hgc_event_journal_t; + +uint64_t hgc_get_timestamp_us_uint64(void); +void hgc_log_event_void(hgc_event_type_t type, uint32_t data1, uint32_t data2, + const char *fmt, ...); +void hgc_get_recent_events_void(hgc_event_entry_t *out, int max_events, + int *count_actual); +bool hgc_save_logs_boolean(const hgc_event_journal_t *ej, const char *path); +bool hgc_load_logs_boolean(hgc_event_journal_t *ej, const char *path); + +#endif diff --git a/src/hoodsgate/hoodsgate.c b/src/hoodsgate/hoodsgate.c new file mode 100644 index 0000000..9482933 --- /dev/null +++ b/src/hoodsgate/hoodsgate.c @@ -0,0 +1,253 @@ +#include "hoodsgate.h" +#include <ctype.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <unistd.h> + +bool hg_is_hex_digit_boolean(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); +} + +char *hg_run_command_w_execvp_pchar(const char *cmd, char *const args[], + char *const input) { + int stdout_pipe[2]; + if (pipe(stdout_pipe) == -1) { + perror("pipe"); + return false; + } + + pid_t cpid = fork(); + if (cpid == -1) { + perror("fork"); + return false; + } + + if (cpid == 0) { + // child proc + close(STDOUT_FILENO); + dup2(stdout_pipe[1], STDOUT_FILENO); + + close(stdout_pipe[0]); + close(stdout_pipe[1]); + + execvp(cmd, args); + + perror("exec"); + exit(1); + } + + if (input) { + write(stdout_pipe[1], input, strlen(input)); + } + + size_t buffer_size = 4096; + char *output = malloc(buffer_size); + + size_t total = 0; + ssize_t bytes; + + while ((bytes = read(stdout_pipe[0], output + total, + buffer_size - total - 1)) > 0) { + total += bytes; + if (total >= buffer_size - 1) { + buffer_size *= 2; + char *new_output = realloc(output, buffer_size); + if (!new_output) { + free(output); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + waitpid(cpid, NULL, 0); + return false; + } + output = new_output; + } + } + + output[total] = '\0'; + close(stdout_pipe[0]); + close(stdout_pipe[1]); + + // wait for child + int status; + waitpid(cpid, &status, 0); + + if (WEXITSTATUS(status) != 0) { + fprintf(stderr, "Command failed with status %d\n", WEXITSTATUS(status)); + } + + if (output) { + return output; + } + + return NULL; +} + +bool hg_validate_ipv4_boolean(char *ipv4) { + /* + * four octets seperated by dots: X.X.X.X + * octet range must be 0-255 + * no leading zeros + * exactly 3 dots + * no whitespace + * digits and dots + * */ + + if (!ipv4) + return false; + + int octet_count = 0; + int num = 0; + int digit_count; + bool has_leading_zero = false; + + for (int i = 0; ipv4[i] != '\0'; i++) { + if (isdigit(ipv4[i])) { + if (digit_count == 0 && ipv4[i] == '0' && isdigit(ipv4[i + 1])) { + has_leading_zero = true; + // don't know if I want leading zero to be invalid or not + // break; + } + + num = num * 10 + (ipv4[i] - '0'); + + // octet too long or too large + digit_count++; + if (digit_count > 3 || num > 255) { + return false; + } + } else if (ipv4[i] == '.') { + // empty octet or leading zero + if (digit_count == 0 || has_leading_zero) { + return false; + } + + // too many octets + if (octet_count >= 4) { + return false; + } + + num = 0; + digit_count = 0; + has_leading_zero = false; + } else { + // invalid character + return false; + } + } + + if (digit_count == 0 || has_leading_zero) { + return false; + } + + return octet_count == 4; +} + +bool hg_validate_ipv6_boolean(char *ipv6) { + /* + * eight groups 1-4 hexadecimal digits separated by colons + * hex digits: 0-9 a-f A-F + * compression :: + * leading zeros; optional + * mixed notation; can end with ipv4 (::ffff:192.168.1.1) + * */ + if (!ipv6 || strlen(ipv6) == 0) + return false; + + int count_group = 0; + int count_digit = 0; + bool saw_double_colon = false; + bool in_ipv4 = false; + int i = 0; + int len = strlen(ipv6); + + if (ipv6[0] == ':') { + if (ipv6[1] != ':') + return false; + saw_double_colon = true; + i = 2; + } + + while (i < len) { + if (isdigit(ipv6[i])) { + int dots = 0; + int j = i; + + while (j < len && (isdigit(ipv6[j]) || ipv6[j] == '.')) { + if (ipv6[j] == '.') + dots++; + j++; + } + + if (dots == 3) { + // IPv4 at the end + char ipv4[16]; + int k = 0; + while (i < len && k < 15) { + ipv4[k++] = ipv6[i++]; + } + ipv4[k] = '\0'; + + if (!hg_validate_ipv4_boolean(ipv4)) { + return false; + } + in_ipv4 = true; + count_group += 2; + break; + } + } + + // process the hex group + if (hg_is_hex_digit_boolean(ipv6[i])) { + count_digit++; + // max of 4 hex digits per group + if (count_digit > 4) + return false; + } else if (ipv6[i] == ':') { + if (count_digit > 0) { + count_group++; + count_digit = 0; + } + + i++; + + // checking for :: + if (i < len && ipv6[i] == ':') { + if (saw_double_colon) { + // only one :: is allowed + return false; + } + saw_double_colon = true; + + i++; + + // :: at the end + if (i >= len) { + count_group++; + break; + } + } + } else { + // invalid char + return false; + } + } + + // count last group; if one exists + if (count_digit > 0 && !in_ipv4) { + count_group++; + } + + // validation + if (saw_double_colon) { + // with ::, we can have fewer than 8 groups + return count_group <= 8; + } else { + // without ::, must have exactly 8 groups (or 6 + IPv4) + return count_group == 8; + } +} diff --git a/src/hoodsgate/hoodsgate.h b/src/hoodsgate/hoodsgate.h new file mode 100644 index 0000000..411dc77 --- /dev/null +++ b/src/hoodsgate/hoodsgate.h @@ -0,0 +1,18 @@ +#ifndef HOODSGATE_H +#define HOODSGATE_H + +#include <stdbool.h> + +#define MASK 0xff000000 +#define VALUEDP 0x01000000 +#define VALUETRP 0x02000000 + +typedef enum { PATH_ETH0 = 0, PATH_TOR = 1, PATH_VPN = 2 } hg_path_type_t; + +bool hg_is_hex_digit_boolean(char c); +char *hg_run_command_w_execvp_pchar(const char *cmd, char *const args[], + char *const input); +bool hg_validate_ipv4_boolean(char *ipv4); +bool hg_validate_ipv6_boolean(char *ipv4); + +#endif diff --git a/src/hoodsgate/kvm_api/ka_interface.c b/src/hoodsgate/kvm_api/ka_interface.c new file mode 100644 index 0000000..75cb93c --- /dev/null +++ b/src/hoodsgate/kvm_api/ka_interface.c @@ -0,0 +1,158 @@ +#include "ka_interface.h" +#include "../hoodsgate.h" +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static ka_domain_t main_domain = {.pid = -1, + .name = VM_NAME, + .status = NOTINITIALIZED, + .opts = + (ka_kvm_opts_t){.vcpus = VCPUS, + .ram = RAM, + .storage = DISK_SIZE, + .ifbridge = BRIDGE_INT, + .isodir = ISO_DIR}}; + +void ka_kvm_install_pid_t(ka_domain_t *cfg_domain) { + ka_kvm_opts_t *opts = &cfg_domain->opts; + char viOpts[300]; + snprintf(viOpts, sizeof(viOpts), "--name '%s' \ + --vcpus '%d' \ + --memory '%d' \ + --disk 'size=$DISK_SIZE,format=qcow2' \ + --os-variant '%s' \ + --location '%s' \ + --extra-args 'console=ttyS0,115200n8 serial' \ + --network 'bridge=%s' \ + --graphics none \ + --console pty,target_type=serial \ + --check all=off \ + --hvm \ + --virt-type kvm \ + --noreboot", + cfg_domain->name, opts->vcpus, opts->ram, opts->osvar, opts->isodir, + opts->ifbridge); + char *const viCmd[] = {PROG_VIRT_INSTALL, viOpts}; + char *viOutput = + hg_run_command_w_execvp_pchar(PROG_VIRT_INSTALL, viCmd, NULL); + if (viOutput == NULL) { + cfg_domain->status = NOTINITIALIZED; + } + + // need to get proper domain status; might be a function itself + char domstate[32] = "domstate "; + char *const viCmdStatus[] = { + PROG_VIRT_INSTALL, strncat(domstate, cfg_domain->name, + sizeof(domstate) - strlen(domstate) - 1)}; + char *domStatus = + hg_run_command_w_execvp_pchar(PROG_VIRT_INSTALL, viCmdStatus, NULL); + if (strcmp(domStatus, "running") == 0) { + cfg_domain->status = RUNNING; + } else if (strcmp(domStatus, "idle") == 0) { + cfg_domain->status = IDLE; + } else if (strcmp(domStatus, "paused") == 0) { + cfg_domain->status = PAUSED; + } else if (strcmp(domStatus, "shutdown") == 0) { + cfg_domain->status = SHUTDOWN; + } else if (strcmp(domStatus, "shut off") == 0) { + cfg_domain->status = SHUTOFF; + } else if (strcmp(domStatus, "crashed") == 0) { + cfg_domain->status = CRASHED; + } else if (strcmp(domStatus, "dying") == 0) { + cfg_domain->status = DYING; + } else if (strcmp(domStatus, "pmsuspended") == 0) { + cfg_domain->status = PMSUSPENDED; + } else { + cfg_domain->status = NOTINITIALIZED; + } + // end + + if (cfg_domain->status == RUNNING) { + char guestName[32]; + snprintf(guestName, sizeof(guestName), "guest=%s", VM_NAME); + char *const pgrepCmd[] = {PROG_PGREP, "-f", guestName, NULL}; + char *pid_HGVM = hg_run_command_w_execvp_pchar(PROG_PGREP, pgrepCmd, NULL); + + cfg_domain->pid = (pid_t)atoi(pid_HGVM); + } +} + +// verify/test that hg_run_command_w_execvp_pchar works here +bool ka_virsh_if_bool(ka_virsh_cmd_t command, ka_domain_t domain) { + char *output = NULL; + // char *const cmd[] = {PROG_VIRSH, "start", domain.name, "--autodestroy", + // NULL}; + char *cmd[5] = {NULL}; + + switch (command) { + case START: + cmd[0] = PROG_VIRSH; + cmd[1] = "start"; + cmd[2] = domain.name; + cmd[3] = "--autodestroy"; + cmd[4] = NULL; + output = hg_run_command_w_execvp_pchar(PROG_VIRSH, cmd, NULL); + break; + case DSHUTDOWN: + cmd[0] = PROG_VIRSH; + cmd[1] = "shutdown"; + cmd[2] = domain.name; + cmd[3] = NULL; + output = hg_run_command_w_execvp_pchar(PROG_VIRSH, cmd, NULL); + break; + case DESTROY: + cmd[0] = PROG_VIRSH; + cmd[1] = "shutdown"; + cmd[2] = domain.name; + cmd[3] = NULL; + output = hg_run_command_w_execvp_pchar(PROG_VIRSH, cmd, NULL); + break; + case REBOOT: + cmd[0] = PROG_VIRSH; + cmd[1] = "destroy"; + cmd[2] = domain.name; + cmd[3] = NULL; + output = hg_run_command_w_execvp_pchar(PROG_VIRSH, cmd, NULL); + break; + case RESET: + cmd[0] = PROG_VIRSH; + cmd[1] = "reset"; + cmd[2] = domain.name; + cmd[3] = NULL; + output = hg_run_command_w_execvp_pchar(PROG_VIRSH, cmd, NULL); + break; + case SUSPEND: + cmd[0] = PROG_VIRSH; + cmd[1] = "suspend"; + cmd[2] = domain.name; + cmd[3] = NULL; + output = hg_run_command_w_execvp_pchar(PROG_VIRSH, cmd, NULL); + break; + case RESUME: + cmd[0] = PROG_VIRSH; + cmd[1] = "resume"; + cmd[2] = domain.name; + cmd[3] = NULL; + output = hg_run_command_w_execvp_pchar(PROG_VIRSH, cmd, NULL); + break; + case UNDEFINE: + cmd[0] = PROG_VIRSH; + cmd[1] = "undefine"; + cmd[2] = domain.name; + cmd[3] = "--remove-all-storage"; + cmd[4] = NULL; + output = hg_run_command_w_execvp_pchar(PROG_VIRSH, cmd, NULL); + break; + default: + break; + } + + if (output != NULL) { + return true; + } + + return false; +} diff --git a/src/hoodsgate/kvm_api/ka_interface.h b/src/hoodsgate/kvm_api/ka_interface.h new file mode 100644 index 0000000..ef6d166 --- /dev/null +++ b/src/hoodsgate/kvm_api/ka_interface.h @@ -0,0 +1,90 @@ +#ifndef KA_INTERFACE_H +#define KA_INTERFACE_H + +#include <stdbool.h> +#include <stdint.h> +#include <sys/types.h> +#include <unistd.h> + +#define PROG_VIRT_INSTALL "virt-install" +#define PROG_VIRSH "virsh" +#define PROG_PGREP "pgrep" + +#define VM_NAME "HGVM" +#define VCPUS 1 +#define RAM 4096 +#define DISK_SIZE 5 // gb +#define BRIDGE_INT "virbr0" +#define ISO_DIR "/var/lib/libvirt/images" +#define OS_VARIANT "alpinelinux3.21" + +typedef enum { + RUNNING = 0, + IDLE, + PAUSED, + SHUTDOWN, + SHUTOFF, + CRASHED, + DYING, + PMSUSPENDED, + NOTINITIALIZED +} ka_state_t; + +typedef struct ka_kvm_opts_t { + int vcpus; + int ram; + uint8_t storage; + char *ifbridge; + char *isodir; + char *osvar; +} ka_kvm_opts_t; + +typedef struct { + pid_t pid; + char *name; + ka_state_t status; + ka_kvm_opts_t opts; +} ka_domain_t; + +void ka_kvm_install_pid_t(ka_domain_t *cfg_domain); +/* ====================================================== + * VIRSH INTERFACE: function to interact with virsh and + * domain state + * ====================================================== + * start domain-name-or-uuid [--console] [--paused] + * [--autodestroy] [--bypass-cache] [--force-boot] + * ------------------------------------------------------ + * shutdown domain [--mode acpi|agent]; graceful + * ------------------------------------------------------ + * destroy domain [--graceful]; gives os no shot + * ------------------------------------------------------ + * reboot domain [--mode acpi|agent] + * ------------------------------------------------------ + * reset domain; emulates the power reset button + * ------------------------------------------------------ + * suspend domain; kept in memory but won't schedule + * ------------------------------------------------------ + * resume domain; moves domain out of suspend state + * ------------------------------------------------------ + * undefine domain [--managed-save] [--snapshots-metadata] + * [ {--storage volumes | --remove-all-storage} --wipe-storage]; + * If the domain is running, this converts it to a + * transient domain, without stopping it. If the domain + * is inactive, the domain configuration is removed + */ + +typedef enum { + START = 0, + DSHUTDOWN, + DESTROY, + REBOOT, + RESET, + SUSPEND, + RESUME, + UNDEFINE +} ka_virsh_cmd_t; + +// maybe add flags in func signature? +bool ka_virsh_if_bool(ka_virsh_cmd_t command, ka_domain_t domain); + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..cd15971 --- /dev/null +++ b/src/main.c @@ -0,0 +1,181 @@ +#include "main.h" +#include "hoodsgate/control_plane/cp_policy_engine.h" +#include "hoodsgate/control_plane/cp_state_manager.h" +#include "hoodsgate/hoodsgate.h" +#include "tailnet.h" +#include <assert.h> +#include <bits/getopt_core.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <unistd.h> + +bool is_directory_boolean(const char *path) { + struct stat path_stat; + if (stat(path, &path_stat) != 0) { + return false; + } + return S_ISDIR(path_stat.st_mode); +} + +bool path_exists_boolean(const char *path) { return access(path, F_OK) == 0; } +bool is_readable_boolean(const char *path) { return access(path, R_OK) == 0; } +bool is_writable_boolean(const char *path) { return access(path, W_OK) == 0; } +bool is_executable_boolean(const char *path) { return access(path, X_OK) == 0; } +bool is_rw_boolean(const char *path) { + return is_readable_boolean(path) && is_writable_boolean(path); +} +bool is_rwx_boolean(const char *path) { + return is_readable_boolean(path) && is_writable_boolean(path) && + is_executable_boolean(path); +} + +bool init_tailscale() { + // determine what progs are missing, if just tailscale is missing then + // install it and move forward + if (!tailnet_check_required_programs_boolean()) { + int missing_count = + sizeof(tailnetMissingProgs) / sizeof(tailnetMissingProgs[0]); + if (missing_count == 1) { + if (strcmp(PROG_TAILSCALE, tailnetMissingProgs[0]) == 0) { + // install tailscale via official curl script + char *const tsGetInstallScript[] = { + PROG_CURL, "-fsSL", "https://tailscale.com/install.sh", NULL}; + char *installScript = + hg_run_command_w_execvp_pchar(PROG_CURL, tsGetInstallScript, NULL); + if (installScript == NULL) { + fprintf(stderr, "Tailscale installation failed"); + return false; + } + // for testing + printf("%s\n", installScript); + char *const tsRunScript[] = {"sh", NULL}; + char *installOutput = hg_run_command_w_execvp_pchar( + PROG_CURL, tsRunScript, installScript); + if (installOutput == NULL) { + fprintf(stderr, "Tailscale installation failed"); + return false; + } + } + } else { + fprintf(stderr, + "Missing one or more required programs to run Hoods Gate"); + return false; + } + } + assert(tailnet_advertise_exit_node_boolean()); + + return true; +} + +// --tailscale, --control-plane +// TODO: Need to implement different logic +// for control-plane+data-plane exit node +// and data-plane only exit nodes +int main(int argc, char *argv[]) { + if (geteuid() != 0) { + fprintf(stderr, + "Failed: this program must be run with sudo or root privileges\n"); + exit(EXIT_FAILURE); + } + + bool _tailscale = false; + bool _isHost = false; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--tailscale") == 0) { + _tailscale = true; + } else if (strcmp(argv[i], "--control-plane") == 0) { + _isHost = true; + } else { + fprintf(stderr, "Usage: %s [--tailscale] [--control-plane]\n", argv[0]); + exit(EXIT_FAILURE); + } + } + + if (_tailscale) { + if (!init_tailscale()) { + exit(EXIT_FAILURE); + } + } + + // Initialize nftables, first run flagging? + if (cp_init_nftables() < 0) { + fprintf(stderr, "Failed to initialize nftables\n"); + exit(EXIT_FAILURE); + } + + printf("Starting Exit Node Control Plane...\n"); + + hgc_log_event_void(EVENT_DAEMON_START, 0, 0, "Control plane daemon starting"); + + if (_isHost) { + cp_state.control_socket = cp_setup_control_socket(); + if (cp_state.control_socket < 0) { + fprintf(stderr, "Failed to setup control socket\n"); + exit(EXIT_FAILURE); + } + } else { + printf("This machine is not the host"); + } + + // Main event loop + while (cp_state.running) { + fd_set readfds; + struct timeval tv = {.tv_sec = 1, .tv_usec = 0}; + + FD_ZERO(&readfds); + FD_SET(cp_state.control_socket, &readfds); + + // watches fd for data; man select + int ret = select(cp_state.control_socket + 1, &readfds, NULL, NULL, &tv); + + if (ret < 0) { + // interruption signal + if (errno == EINTR) + continue; + perror("select"); + break; + } + + if (ret == 0) + continue; // timeout on timeval struct value + + if (FD_ISSET(cp_state.control_socket, &readfds)) { + // Data is available in the socket + int client_fd = accept(cp_state.control_socket, NULL, NULL); + if (client_fd < 0) { + perror("accept"); + continue; + } + + char buffer[BUFFER_SIZE]; + ssize_t n = recv(client_fd, buffer, sizeof(buffer) - 1, 0); + if (n > 0) { + buffer[n] = '\0'; + // Remove trailing newline + if (buffer[n - 1] == '\n') + buffer[n - 1] = '\0'; + + // Not implemented yet. + // handle_control_command(client_fd, buffer); + } else if (n < 0) { + hgc_log_event_void(EVENT_ERROR, errno, 0, "recv error: %s", + strerror(errno)); + } + + close(client_fd); + } + } + + // Cleanup + hgc_log_event_void(EVENT_DAEMON_STOP, 0, 0, + "Control plane daemon shutting down"); + printf("Shutting down...\n"); + close(cp_state.control_socket); + unlink(CONTROL_SOCKET_PATH); + + return 0; +} diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..679f3d3 --- /dev/null +++ b/src/main.h @@ -0,0 +1,17 @@ +#ifndef MAIN_H +#define MAIN_H + +#include <stdbool.h> +#include <sys/stat.h> +#include <unistd.h> + +bool path_exists_boolean(const char *path); +bool is_readable_boolean(const char *path); +bool is_writable_boolean(const char *path); +bool is_executable_boolean(const char *path); +bool is_directory_boolean(const char *path); +bool is_rw_boolean(const char *path); +bool is_rwx_boolean(const char *path); +bool init_tailscale(void); + +#endif diff --git a/src/tailnet.c b/src/tailnet.c new file mode 100644 index 0000000..21a90f6 --- /dev/null +++ b/src/tailnet.c @@ -0,0 +1,173 @@ +#include "tailnet.h" +#include "hoodsgate/hoodsgate.h" +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <unistd.h> + +DEFINE_PROGRAM_LIST(PROG_TAILSCALE, PROG_SYSCTL, PROG_UFW, PROG_CURL, PROG_NFT) + +const char *tailnetMissingProgs[MAX_PROG_COUNT]; +static int _missingProgsCount = 0; +static char *_kernelOpts[] = {"net.ipv4.ip_forward = 1\n", + "net.ipv6.conf.all.forwarding = 1\n"}; +// filename needs to be 99-tailscale.conf +static char *_sysctlConf = "/etc/sysctl.d/100-tailscale.conf"; +static char *_sysctlConfFallback = "/etc/sysctl.conf"; +static char *_ufw_file = "/etc/default/ufw"; + +bool tailnet_is_file_executable_boolean(const char *filepath) { + return access(filepath, X_OK) == 0; +} + +bool tailnet_is_program_installed_boolean(const char *program_name) { + if (!program_name || strlen(program_name) == 0) { + return false; + } + + char *path_env = getenv("PATH"); + if (!path_env) { + return false; + } + + char path_copy[4096]; + strncpy(path_copy, path_env, sizeof(path_copy) - 1); + path_copy[sizeof(path_copy) - 1] = '\0'; + + char *dir = strtok(path_copy, ":"); + while (dir != NULL) { + char fullpath[1024]; + snprintf(fullpath, sizeof(fullpath), "%s/%s", dir, program_name); + + if (tailnet_is_file_executable_boolean(fullpath)) { + return true; + } + dir = strtok(NULL, ":"); + } + return false; +} + +bool tailnet_check_required_programs_boolean(void) { + printf("Checking required programs...\n"); + bool res = true; + for (int i = 0; i < REQUIRED_PROGS_COUNT; i++) { + if (!tailnet_is_program_installed_boolean(REQUIRED_PROGS[i])) { + int pos = _missingProgsCount; + if (pos < MAX_PROG_COUNT) { + tailnetMissingProgs[pos] = REQUIRED_PROGS[i]; + _missingProgsCount++; + } + printf(" ✗ %s (MISSING)\n", REQUIRED_PROGS[i]); + res = false; + } else { + printf(" ✓ %s\n", REQUIRED_PROGS[i]); + } + } + + if (res) { + printf("All required programs found!\n"); + } else { + printf("\nPlease install missing programs. \n"); + } + + return res; +} + +bool tailnet_advertise_exit_node_boolean() { + const char *conf_file = + (access("/etc/sysctl.d/", F_OK) == 0) ? _sysctlConf : _sysctlConfFallback; + + FILE *file = fopen(conf_file, "a"); + if (!file) { + perror("Failed to open file for appending kernel opts forwarding rules"); + return false; + } + + for (int i = 0; i < (int)(sizeof(_kernelOpts) / sizeof(_kernelOpts[0])); + i++) { + fputs(_kernelOpts[i], file); + } + + fclose(file); + printf("Wrote kernel forwarding opts to %s\n", conf_file); + + if (!(access(_ufw_file, F_OK) == 0)) { + perror("Failed to access ufw default file for file ops"); + return false; + } + file = fopen(_ufw_file, "r"); + if (!file) { + perror("Failed to open file to look for ufw foward policy"); + return false; + } + + /* Following is from a comment in /etc/default/ufw + * 'Set the default forward policy to ACCEPT, DROP or REJECT. Please note + * that if you change this you will most likely want to adjust your rules + * DEFAULT_FORWARD_POLICY="DROP"' + */ + char line[512]; + bool found = false; + while (fgets(line, sizeof(line), file)) { + if (strstr(line, "DEFAULT_FORWARD_POLICY") && strstr(line, "DROP")) { + printf("Found: %s\n", line); + found = true; + break; + } else if (strstr(line, "DEFAULT_FORWARD_POLICY") && + strstr(line, "ACCEPT")) { + printf("Found: %s\nChange your policy rule in /etc/default/ufw to " + "DEFAULT_FORWARD_POLICY='DROP'\nand adjust your ufw rules " + "accordingly, then rerun", + line); + exit(1); + } else if (strstr(line, "DEFAULT_FORWARD_POLICY") && + strstr(line, "REJECT")) { + printf("Found: %s\nChange your policy rule in /etc/default/ufw to " + "DEFAULT_FORWARD_POLICY='DROP'\nand adjust your ufw rules " + "accordingly, then rerun", + line); + exit(1); + } + } + + fclose(file); + + if (found != true) { + return false; + } + + // tailscaled exit node ivp4 addr + char *const tsGetIpv4Addr[] = {PROG_TAILSCALE, "ip", "--4", NULL}; + char *exitNodeIpv4 = + hg_run_command_w_execvp_pchar(PROG_TAILSCALE, tsGetIpv4Addr, NULL); + + if (exitNodeIpv4 == NULL || hg_validate_ipv4_boolean(exitNodeIpv4) == false) { + return false; + } + + char *flagExitNode = "--exit-node="; + strcat(flagExitNode, exitNodeIpv4); + char *const tsSetExitNode[] = {PROG_TAILSCALE, "set", flagExitNode, + "--exit-node-allow-lan-access=true", NULL}; + char *setExitNode = + hg_run_command_w_execvp_pchar(PROG_TAILSCALE, tsSetExitNode, NULL); + + // not sure if the setExitNode returns output; will have to test + if (setExitNode == NULL) { + return false; + } + + char *const tsRunExitNode[] = {PROG_TAILSCALE, "up", NULL}; + char *runExitNode = + hg_run_command_w_execvp_pchar(PROG_TAILSCALE, tsRunExitNode, NULL); + + // not sure if the runExitNode returns output; will have to test + if (runExitNode == NULL) { + return false; + } + + return true; +} diff --git a/src/tailnet.h b/src/tailnet.h new file mode 100644 index 0000000..8e7c8a6 --- /dev/null +++ b/src/tailnet.h @@ -0,0 +1,25 @@ +#ifndef TAILNET_H +#define TAILNET_H + +#include <stdbool.h> + +#define DEFINE_PROGRAM_LIST(...) \ + static const char *REQUIRED_PROGS[] = {__VA_ARGS__}; \ + static const int REQUIRED_PROGS_COUNT = \ + sizeof(REQUIRED_PROGS) / sizeof(REQUIRED_PROGS[0]); + +#define PROG_TAILSCALE "tailscale" +#define PROG_SYSCTL "sysctl" +#define PROG_UFW "ufw" +#define PROG_CURL "curl" +#define PROG_NFT "nft" +#define MAX_PROG_COUNT \ + 10 // this is here cause of C99 array sizes compile time constraints + +extern const char *tailnetMissingProgs[MAX_PROG_COUNT]; +bool tailnet_is_program_installed_boolean(const char *program_name); +bool tailnet_is_file_executable_boolean(const char *filepath); +bool tailnet_check_required_programs_boolean(void); +bool tailnet_advertise_exit_node_boolean(void); + +#endif diff --git a/test-env/README.md b/test-env/README.md new file mode 100644 index 0000000..ef2bb0a --- /dev/null +++ b/test-env/README.md @@ -0,0 +1,110 @@ +# Technical Specification: Automated KVM Provisioning for Hoods Gate Testing + +This document outlines the architecture and implementation of a shell-based +automation utility for deploying Kernel-based Virtual Machines (KVM) on Ubuntu +Server using virt-install. This approach is designed for rapid iteration in +kernel development and network stack testing where total environment isolation +is required. + +## Overview + +The goal of this implementation is to provide a reproducible, command-line +driven interface for deploying headless virtual machines. By utilizing a Linux +Bridge and Serial Console redirection, developers can perform destructive +kernel operations while maintaining persistent access to the debug output, +bypassing the overhead of a graphical user interface. + +## System Requirements & Prerequisites + +The host system must support hardware virtualization (Intel VT-x or AMD-V) and +have the following packages installed: + +- qemu-kvm: Backend emulator +- libvirt-daemon-system: Management system +- virinst: CLI Utility for provisioning +- bridge-utils: Necessary for L2 network bridging + +### Storage Persistence + +Virtual disk images created by this script are stored in +/var/lib/libvirt/images/. During kernel testing, if the filesystem becomes +corrupted due to experimental kernel modules, the storage must be manually +purged or overwritten. + +### Network Configuration + +The host must have a bridge interface (e.g., br0) configured via Netplan. +This allows the Guest VM to obtain its own IP address and provides a +transparent environment for testing custom networking protocols or firewall +rules. + +## Implementation + +The script, [provision-lab.sh](./provision-vm.sh), automates the +virt-install process for isolated kernel and network testing. It utilizes local +ISO media and libosinfo detection to ensure hardware-optimized environments +while configuring the serial console for direct TTY access. + +## Operational Procedures + +### VM Creation + +Execute the script with optional parameters for resource allocation: + +```shell +sudo ./provision-vm.sh <name> <vcpus> <ram_in_mb> +``` + +### Interacting with the Guest Kernel + +Since the VM is configured without a graphics card, the standard virsh console +command is used to attach to the guest's serial port. This is essential for +capturing Kernel Panics or Early Printk output that would otherwise be lost. + +Attach to Console: + +```shell +virsh console <vm-name> +``` + +Detach from Console (Press): + +``` +CTRL + ] +``` + +### Modifying Kernel Parameters + +To test specific kernel options (e.g., disabling KASLR or isolating CPUs), +edit the VM configuration directly via the XML descriptor: + +```shell +virsh edit <vm-name> +``` + +Locate the <cmdline> tag within the <os> section to append your required flags. + +### Network Analysis + +Use tcpdump -i br0 on the host to monitor raw packet traffic moving through the +VM's virtual interface. + +```shell +tcpdump -i br0 +``` + +## Lifecycle Management + +Operation|Command|Description +Start|virsh start <name>|Powers on the virtual machine +Stop|virsh shutdown <name>|Sends an ACPI power signal for a graceful exit +Destroy|virsh destroy <name>|Equivalent to pulling the power plug (immediate stop) +Undefine|virsh undefine <name> --remove-all-storage|Completely removes the VM and its disk images + +## Technical Considerations + +When performing kernel modifications, the following behaviors should be expected: + +- _Storage Persistence_: Changes made to the filesystem persist unless the --remove-all-storage flag is used during deletion. +- _Network Isolation_: By using a bridge, the VM possesses its own MAC address. Ensure the host firewall (ufw/iptables) is configured to allow traffic across the bridge. +- _Instruction Set Passthrough_: If the kernel testing requires specific CPU instructions (e.g., AES-NI), append --cpu host-passthrough to the virt-install command. diff --git a/test-env/net-sim/netsim.sh b/test-env/net-sim/netsim.sh new file mode 100755 index 0000000..8b0f68e --- /dev/null +++ b/test-env/net-sim/netsim.sh @@ -0,0 +1,489 @@ +#!/bin/bash +# +# Network Simulator for Hoods Gate Testing +# Isolated network environment with fake internet responses +# + +# Configuration +STATE_DIR="/tmp/netsim" +STATE_FILE="${STATE_DIR}/state" +NETNS_CLIENT="netsim_client" +NETNS_EXIT="netsim_exit" +NETNS_INTERNET="netsim_internet" + +# Network Configuration +BRIDGE_CLIENT="br-client" +BRIDGE_EXIT="br-exit" +BRIDGE_WAN="br-wan" # Bridge for WAN side connection +VETH_CLIENT_HOST="veth-c-host" +VETH_CLIENT_NS="veth-c-ns" +VETH_EXIT_HOST="veth-e-host" +VETH_EXIT_NS="veth-e-ns" +VETH_INET_EXIT="veth-i-exit" +VETH_INET_NS="veth-i-ns" +EXIT_WAN_IFACE="wan0" # Interface name inside exit KVM + +# IP Configuration +IP_CLIENT_BRIDGE="10.10.1.1/24" +IP_CLIENT_VM="10.10.1.10/24" +IP_EXIT_BRIDGE="10.10.2.1/24" +IP_EXIT_VM="10.10.2.10/24" +IP_EXIT_WAN="203.0.113.10/24" # WAN IP for exit node +IP_INET_GATEWAY="203.0.113.1/24" # Gateway in internet namespace + +RED=$'\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC=$'\033[0m' + +check_root() { + if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Error: This script must be run as root${NC}" + exit 1 + fi +} + +fail() { + echo -e "${RED}$2${NC}" >&2 + exit $1 +} + +usage() { + cat << EOF + ▄▄▄ ▄▄ ██ + ███ ██ ██ ▀▀ + ██▀█ ██ ▄████▄ ███████ ▄▄█████▄ ████ ████▄██▄ + ██ ██ ██ ██▄▄▄▄██ ██ ██▄▄▄▄ ▀ ██ ██ ██ ██ + ██ █▄██ ██▀▀▀▀▀▀ ██ ▀▀▀▀██▄ ██ ██ ██ ██ + ██ ███ ▀██▄▄▄▄█ ██▄▄▄ █▄▄▄▄▄██ ▄▄▄██▄▄▄ ██ ██ ██ + ▀▀ ▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀ +*Network Simulator for Hoods Gate Testing${NC} + +Usage: + $0 setup [bandwidth] [latency] + Create network topology for Hoods Gate testing + bandwidth: in mbit (default: 100) + latency: in ms (default: 10) + Example: $0 setup 50 20 + + $0 status + Show current network configuration and statistics + + $0 logs + Show traffic logs from fake internet + + $0 adjust <bandwidth> <latency> + Adjust network conditions on the fly + Example: $0 adjust 10 100 + + $0 teardown + Remove all network configuration + + $0 test + Run connectivity tests + +Network Topology Created: + [Client KVM] ---> [Bridge] ---> [Exit KVM] ---> [Fake Internet] + 10.10.1.10 10.10.1.1 10.10.2.10 192.168.100.2 + +Instructions for KVMs: + Client KVM: Attach to bridge '${BRIDGE_CLIENT}', use IP 10.10.1.10/24, gateway 10.10.1.1 + Exit KVM: Attach to bridge '${BRIDGE_EXIT}', use IP 10.10.2.10/24, gateway 10.10.2.1 + SDN should route traffic to 192.168.100.1 + +EOF + exit 0 +} + +init_state() { + mkdir -p "${STATE_DIR}" + touch "${STATE_FILE}" +} + +# Setup network topology +setup_network() { + local bandwidth=${1:-100} + local latency=${2:-10} + + echo -e "${GREEN}Setting up network simulation...${NC}" + + # Clean up any existing setup + teardown_network 2>/dev/null + + echo -e "${BLUE}[1/7]${NC} Creating network namespaces..." + ip netns add ${NETNS_EXIT} 2>/dev/null + ip netns add ${NETNS_INTERNET} 2>/dev/null + + echo -e "${BLUE}[2/7]${NC} Creating bridges for KVM attachment..." + ip link add ${BRIDGE_CLIENT} type bridge + ip link add ${BRIDGE_EXIT} type bridge + ip link add ${BRIDGE_WAN} type bridge + ip addr add ${IP_CLIENT_BRIDGE} dev ${BRIDGE_CLIENT} + ip addr add ${IP_EXIT_BRIDGE} dev ${BRIDGE_EXIT} + ip link set ${BRIDGE_CLIENT} up + ip link set ${BRIDGE_EXIT} up + ip link set ${BRIDGE_WAN} up + + echo -e "${BLUE}[3/7]${NC} Creating veth pairs..." + # Exit namespace LAN side connection + ip link add ${VETH_EXIT_HOST} type veth peer name ${VETH_EXIT_NS} + ip link set ${VETH_EXIT_NS} netns ${NETNS_EXIT} + ip link set ${VETH_EXIT_HOST} master ${BRIDGE_EXIT} + ip link set ${VETH_EXIT_HOST} up + + # WAN bridge to internet namespace connection + ip link add veth-wan-br type veth peer name veth-wan-inet + ip link set veth-wan-br master ${BRIDGE_WAN} + ip link set veth-wan-inet netns ${NETNS_INTERNET} + ip link set veth-wan-br up + + # Internet namespace connection + ip link add ${VETH_INET_EXIT} type veth peer name ${VETH_INET_NS} + ip link set ${VETH_INET_EXIT} master ${BRIDGE_WAN} + ip link set ${VETH_INET_NS} netns ${NETNS_INTERNET} + ip link set ${VETH_INET_EXIT} up + + echo -e "${BLUE}[4/7]${NC} Configuring IP addresses..." + # Exit namespace LAN IP only (no WAN IP - that's on the KVM side) + ip netns exec ${NETNS_EXIT} ip addr add ${IP_EXIT_BRIDGE} dev ${VETH_EXIT_NS} + ip netns exec ${NETNS_EXIT} ip link set ${VETH_EXIT_NS} up + ip netns exec ${NETNS_EXIT} ip link set lo up + + # Internet namespace IPs (acts as the gateway) + ip netns exec ${NETNS_INTERNET} ip addr add ${IP_INET_GATEWAY} dev veth-wan-inet + ip netns exec ${NETNS_INTERNET} ip addr add 8.8.8.8/32 dev lo # Fake DNS server + ip netns exec ${NETNS_INTERNET} ip addr add 1.1.1.1/32 dev lo # Fake DNS server + ip netns exec ${NETNS_INTERNET} ip link set veth-wan-inet up + ip netns exec ${NETNS_INTERNET} ip link set ${VETH_INET_NS} up + ip netns exec ${NETNS_INTERNET} ip link set lo up + + echo -e "${BLUE}[5/7]${NC} Configuring routing..." + # Host routing for client bridge - NAT to exit bridge + iptables -t nat -A POSTROUTING -s 10.10.1.0/24 -o ${BRIDGE_EXIT} -j MASQUERADE + echo 1 > /proc/sys/net/ipv4/ip_forward + + # Internet namespace routing (responds to everything, blackholes it) + ip netns exec ${NETNS_INTERNET} sysctl -w net.ipv4.ip_forward=1 >/dev/null + ip netns exec ${NETNS_INTERNET} ip route add default dev veth-wan-inet + + # Note: Exit KVM will handle routing from LAN to WAN interface + + echo -e "${BLUE}[6/7]${NC} Applying traffic control (${bandwidth}mbit, ${latency}ms)..." + apply_tc ${bandwidth} ${latency} + + echo -e "${BLUE}[7/7]${NC} Starting fake internet services..." + start_fake_internet + + # Save configuration + echo "bandwidth=${bandwidth}" > "${STATE_FILE}" + echo "latency=${latency}" >> "${STATE_FILE}" + echo "timestamp=$(date +%s)" >> "${STATE_FILE}" + + echo -e "${GREEN}✓ Network simulation setup complete!${NC}" + echo "" + echo -e "${YELLOW}Next steps:${NC}" + echo " 1. Attach Client KVM to bridge: ${BRIDGE_CLIENT}" + echo " Configure with IP: 10.10.1.10/24, Gateway: 10.10.1.1, DNS: 10.10.1.1" + echo "" + echo " 2. Attach Exit KVM with TWO network interfaces:" + echo " - LAN interface to bridge: ${BRIDGE_EXIT}" + echo " Configure with IP: 10.10.2.10/24" + echo " - WAN interface to bridge: ${BRIDGE_WAN}" + echo " This will appear as '${EXIT_WAN_IFACE}' inside the VM" + echo " Configure with IP: 203.0.113.10/24, Gateway: 203.0.113.1" + echo "" + echo " 3. SDN should route traffic OUT of the '${EXIT_WAN_IFACE}' interface" + echo " (Just like routing out eth0 on a normal machine)" + echo "" + echo " 4. Run '$0 test' to verify connectivity" +} + +# Apply traffic control +apply_tc() { + local bandwidth=$1 + local latency=$2 + + # Apply to client->exit connection + tc qdisc del dev ${VETH_EXIT_HOST} root 2>/dev/null + tc qdisc add dev ${VETH_EXIT_HOST} root handle 1: htb default 10 + tc class add dev ${VETH_EXIT_HOST} parent 1: classid 1:10 htb rate ${bandwidth}mbit + tc qdisc add dev ${VETH_EXIT_HOST} parent 1:10 handle 10: netem delay ${latency}ms + + # Apply to WAN bridge (exit->internet) + tc qdisc del dev ${VETH_INET_EXIT} root 2>/dev/null + tc qdisc add dev ${VETH_INET_EXIT} root handle 1: htb default 10 + tc class add dev ${VETH_INET_EXIT} parent 1: classid 1:10 htb rate ${bandwidth}mbit + tc qdisc add dev ${VETH_INET_EXIT} parent 1:10 handle 10: netem delay ${latency}ms +} + +# Start fake internet services +start_fake_internet() { + local log_file="${STATE_DIR}/traffic.log" + > "${log_file}" + + # DNS Server (responds with fake IPs) + ip netns exec ${NETNS_INTERNET} dnsmasq \ + --no-daemon \ + --listen-address=${IP_INET_GATEWAY%/*} \ + --address=/#/93.184.216.34 \ + --log-queries \ + --log-facility=${STATE_DIR}/dns.log \ + --no-resolv \ + --no-poll \ + >/dev/null 2>&1 & + echo $! > "${STATE_DIR}/dns.pid" + + # HTTP/HTTPS responder + ip netns exec ${NETNS_INTERNET} python3 -c " +import socket +import threading +import datetime + +def log_request(proto, ip, port, data): + with open('${log_file}', 'a') as f: + f.write(f'{datetime.datetime.now()} | {proto} | {ip}:{port} | {len(data)} bytes\\n') + +def http_server(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('0.0.0.0', 80)) + sock.listen(5) + while True: + try: + conn, addr = sock.accept() + data = conn.recv(4096) + log_request('HTTP', addr[0], addr[1], data) + response = b'HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n<html><body><h1>Fake Internet Response</h1></body></html>' + conn.sendall(response) + conn.close() + except: pass + +def https_server(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('0.0.0.0', 443)) + sock.listen(5) + while True: + try: + conn, addr = sock.accept() + data = conn.recv(4096) + log_request('HTTPS', addr[0], addr[1], data) + conn.close() + except: pass + +def generic_tcp(): + for port in [22, 21, 25, 3389, 3306, 5432, 6379]: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind(('0.0.0.0', port)) + sock.listen(1) + threading.Thread(target=lambda s=sock, p=port: tcp_handler(s, p), daemon=True).start() + except: pass + +def tcp_handler(sock, port): + while True: + try: + conn, addr = sock.accept() + data = conn.recv(4096) + log_request(f'TCP/{port}', addr[0], addr[1], data) + conn.close() + except: pass + +threading.Thread(target=http_server, daemon=True).start() +threading.Thread(target=https_server, daemon=True).start() +threading.Thread(target=generic_tcp, daemon=True).start() + +import time +while True: time.sleep(1) +" >/dev/null 2>&1 & + echo $! > "${STATE_DIR}/http.pid" + + # ICMP responder (respond to pings) + ip netns exec ${NETNS_INTERNET} bash -c " + while true; do + ping -c 1 8.8.8.8 >/dev/null 2>&1 || true + sleep 1 + done + " >/dev/null 2>&1 & + echo $! > "${STATE_DIR}/icmp.pid" +} + +# Show status +show_status() { + if [ ! -f "${STATE_FILE}" ]; then + echo -e "${YELLOW}No simulation running${NC}" + return + fi + + source "${STATE_FILE}" + + echo -e "${GREEN}Network Simulation Status${NC}" + echo "================================" + echo -e "Bandwidth: ${bandwidth}mbit" + echo -e "Latency: ${latency}ms" + echo -e "Running since: $(date -d @${timestamp})" + echo "" + + echo -e "${GREEN}Network Namespaces:${NC}" + ip netns list | grep netsim && echo -e "${GREEN}✓${NC} Namespaces active" || echo -e "${RED}✗${NC} Namespaces missing" + echo "" + + echo -e "${GREEN}Bridges (for KVM attachment):${NC}" + ip link show ${BRIDGE_CLIENT} >/dev/null 2>&1 && echo -e "${GREEN}✓${NC} ${BRIDGE_CLIENT} - Client KVM (LAN side)" || echo -e "${RED}✗${NC} ${BRIDGE_CLIENT} missing" + ip link show ${BRIDGE_EXIT} >/dev/null 2>&1 && echo -e "${GREEN}✓${NC} ${BRIDGE_EXIT} - Exit KVM (LAN side)" || echo -e "${RED}✗${NC} ${BRIDGE_EXIT} missing" + ip link show ${BRIDGE_WAN} >/dev/null 2>&1 && echo -e "${GREEN}✓${NC} ${BRIDGE_WAN} - Exit KVM (WAN side)" || echo -e "${RED}✗${NC} ${BRIDGE_WAN} missing" + echo "" + + echo -e "${GREEN}Fake Internet Services:${NC}" + [ -f "${STATE_DIR}/dns.pid" ] && kill -0 $(cat "${STATE_DIR}/dns.pid") 2>/dev/null && echo -e "${GREEN}✓${NC} DNS (port 53)" || echo -e "${RED}✗${NC} DNS not running" + [ -f "${STATE_DIR}/http.pid" ] && kill -0 $(cat "${STATE_DIR}/http.pid") 2>/dev/null && echo -e "${GREEN}✓${NC} HTTP/HTTPS (ports 80, 443)" || echo -e "${RED}✗${NC} HTTP not running" + echo "" + + echo -e "${GREEN}Traffic Statistics:${NC}" + if [ -f "${STATE_DIR}/traffic.log" ]; then + local total=$(wc -l < "${STATE_DIR}/traffic.log") + echo " Total requests: ${total}" + if [ ${total} -gt 0 ]; then + echo " Last 5 requests:" + tail -5 "${STATE_DIR}/traffic.log" | sed 's/^/ /' + fi + else + echo " No traffic logged yet" + fi +} + +# Show logs +show_logs() { + echo -e "${GREEN}Traffic Logs${NC}" + echo "================================" + + if [ -f "${STATE_DIR}/traffic.log" ]; then + cat "${STATE_DIR}/traffic.log" + else + echo "No traffic logged" + fi + + echo "" + echo -e "${GREEN}DNS Queries${NC}" + echo "================================" + + if [ -f "${STATE_DIR}/dns.log" ]; then + tail -20 "${STATE_DIR}/dns.log" + else + echo "No DNS queries logged" + fi +} + +# Adjust network conditions +adjust_network() { + local bandwidth=$1 + local latency=$2 + + if [ -z "$bandwidth" ] || [ -z "$latency" ]; then + fail 1 "Usage: $0 adjust <bandwidth> <latency>" + fi + + echo -e "${GREEN}Adjusting network conditions...${NC}" + echo " Bandwidth: ${bandwidth}mbit" + echo " Latency: ${latency}ms" + + apply_tc ${bandwidth} ${latency} + + # Update state file + sed -i "s/^bandwidth=.*/bandwidth=${bandwidth}/" "${STATE_FILE}" + sed -i "s/^latency=.*/latency=${latency}/" "${STATE_FILE}" + + echo -e "${GREEN}✓ Network conditions updated${NC}" +} + +# Run connectivity tests +run_tests() { + echo -e "${GREEN}Running connectivity tests...${NC}" + echo "" + + echo -e "${BLUE}Test 1: Ping fake internet gateway${NC}" + ping -c 3 ${IP_INET_GATEWAY%/*} 2>/dev/null && \ + echo -e "${GREEN}✓ PASS${NC}" || echo -e "${RED}✗ FAIL${NC}" + echo "" + + echo -e "${BLUE}Test 2: HTTP request to fake internet${NC}" + ip netns exec ${NETNS_INTERNET} timeout 5 python3 -m http.server 8080 >/dev/null 2>&1 & + local pid=$! + sleep 1 + curl -s --max-time 5 http://${IP_INET_GATEWAY%/*}:8080 >/dev/null && \ + echo -e "${GREEN}✓ PASS${NC}" || echo -e "${RED}✗ FAIL${NC}" + kill $pid 2>/dev/null + echo "" + + echo -e "${BLUE}Test 3: DNS resolution${NC}" + dig @${IP_INET_GATEWAY%/*} google.com +short +time=2 >/dev/null && \ + echo -e "${GREEN}✓ PASS${NC}" || echo -e "${RED}✗ FAIL${NC}" + echo "" + + echo -e "${YELLOW}Note: For full testing, attach your KVMs and test from there${NC}" +} + +# Teardown network +teardown_network() { + echo -e "${GREEN}Tearing down network simulation...${NC}" + + # Kill services + [ -f "${STATE_DIR}/dns.pid" ] && kill $(cat "${STATE_DIR}/dns.pid") 2>/dev/null + [ -f "${STATE_DIR}/http.pid" ] && kill $(cat "${STATE_DIR}/http.pid") 2>/dev/null + [ -f "${STATE_DIR}/icmp.pid" ] && kill $(cat "${STATE_DIR}/icmp.pid") 2>/dev/null + + # Remove traffic control + tc qdisc del dev ${VETH_EXIT_HOST} root 2>/dev/null || true + + # Delete bridges + ip link set ${BRIDGE_CLIENT} down 2>/dev/null || true + ip link set ${BRIDGE_EXIT} down 2>/dev/null || true + ip link set ${BRIDGE_WAN} down 2>/dev/null || true + ip link del ${BRIDGE_CLIENT} 2>/dev/null || true + ip link del ${BRIDGE_EXIT} 2>/dev/null || true + ip link del ${BRIDGE_WAN} 2>/dev/null || true + + # Delete namespaces (this removes veth pairs automatically) + ip netns del ${NETNS_EXIT} 2>/dev/null || true + ip netns del ${NETNS_INTERNET} 2>/dev/null || true + + # Clean iptables + iptables -t nat -D POSTROUTING -s 10.10.1.0/24 -o ${BRIDGE_EXIT} -j MASQUERADE 2>/dev/null || true + + # Clean state + rm -rf "${STATE_DIR}" + + echo -e "${GREEN}✓ Teardown complete${NC}" +} + +# Main +check_root +init_state + +case "${1:-}" in + setup) + setup_network "${2:-100}" "${3:-10}" + ;; + status) + show_status + ;; + logs) + show_logs + ;; + adjust) + adjust_network "$2" "$3" + ;; + test) + run_tests + ;; + teardown) + teardown_network + ;; + *) + usage + ;; +esac + +exit 0 diff --git a/test-env/provision-vm.sh b/test-env/provision-vm.sh new file mode 100755 index 0000000..97bfb32 --- /dev/null +++ b/test-env/provision-vm.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# Default Configuration +VM_NAME="kernel-test-lab" +VCPUS=2 +RAM=4096 +DISK_SIZE=20 +BRIDGE_INT="virbr0" +ISO_DIR="/var/lib/libvirt/images" +ISO_PATH="" + +#OS_VARIANT=$(osinfo-query os | grep -i "$(basename "$ISO_PATH" | cut -d'-' -f1)" | head -n1 | awk '{print $1}') +# more than likely this will be set to none or "" see: https://linux.die.net/man/1/virt-install +OS_VARIANT="" + +usage() { + cat << EOF +┌──────────────────────────────────────────────────────────┐ +│░█░█░█░█░█▄█░░░█▀█░█▀▄░█▀█░█░█░▀█▀░█▀▀░▀█▀░█▀█░█▀█░█▀▀░█▀▄│ +│░█▀▄░▀▄▀░█░█░░░█▀▀░█▀▄░█░█░▀▄▀░░█░░▀▀█░░█░░█░█░█░█░█▀▀░█▀▄│ +│░▀░▀░░▀░░▀░▀░░░▀░░░▀░▀░▀▀▀░░▀░░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀░▀│ +└──────────────────────────────────────────────────────────┘ +*KVM Provisioning Utility for Isolated Kernel and Network Testing. + +Usage: sudo $0 [OPTIONS] + +Options: + -n NAME Name of the Virtual Machine (Default: kernel-test-lab) + -c CPUS Number of vCPUs to allocate (Default: 2) + -m RAM Memory allocation in MB (Default: 4096) + -d DISK Disk size in GB (Default: 20) + -b BRIDGE Host bridge interface (Default: virbr0) + -i ISO Absolute path to ISO (Bypasses interactive selection) + -ov OSVARIANT Optimize the guest configuration for a specific os variant (Bypasses Interactive selection) + -h Display this technical manual + +Example: + sudo ./provision-lab.sh -n kernel-dev-01 -c 4 -m 8192 -d 40 +EOF + exit 0 +} + +if [[ $EUID -ne 0 ]]; then + echo "Error: Execution requires root privileges." >&2 + exit 1 +fi + +while getopts "n:c:m:d:b:i:ov:h" opt; do + case ${opt} in + n) VM_NAME=$OPTARG ;; + c) VCPUS=$OPTARG ;; + m) RAM=$OPTARG ;; + d) DISK_SIZE=$OPTARG ;; + b) BRIDGE_INT=$OPTARG ;; + i) ISO_PATH=$OPTARG ;; + ov) OS_VARIANT=$OPTARG ;; + h) usage ;; + *) usage ;; + esac +done + +if [[ -z "$ISO_PATH" ]]; then + echo "--- KVM Lab Provisioner ---" + echo "Scanning $ISO_DIR for installation media..." + + mapfile -t ISO_LIST < <(ls "$ISO_DIR"/*.iso 2>/dev/null) + + if [ ${#ISO_LIST[@]} -eq 0 ]; then + echo "Error: No ISO files detected in $ISO_DIR" >&2 + exit 1 + fi + + select opt in "${ISO_LIST[@]}" "Cancel"; do + if [[ "$opt" == "Cancel" ]]; then exit 0; fi + if [[ -f "$opt" ]]; then + ISO_PATH="$opt" + break + else + echo "Invalid selection." + fi + done +fi + +if [[ -z "$OS_VARIANT" ]]; then + echo "Need a OS Variant..." + read OS_VARIANT +fi + +echo "Provisioning Details:" +echo " Name: $VM_NAME" +echo " vCPUs: $VCPUS" +echo " RAM: $RAM MB" +echo " Storage: $DISK_SIZE GB" +echo " Bridge: $BRIDGE_INT" +echo " ISO: $ISO_PATH" +echo " Profile: $OS_VARIANT" + +# error was: ERROR Kernel arguments are only supported with location or kernel installs +# changed from location to cdrom; can't pass kernel args unless you use --location +# --extra-args "console=ttyS0,115200n8 serial" \ +virt-install \ + --name "$VM_NAME" \ + --vcpus "$VCPUS" \ + --memory "$RAM" \ + --disk "size=$DISK_SIZE,format=qcow2" \ + --os-variant "$OS_VARIANT" \ + --location "$ISO_PATH" \ + --extra-args "console=ttyS0,115200n8 serial" \ + --network "bridge=$BRIDGE_INT" \ + --graphics none \ + --console pty,target_type=serial \ + --check all=off \ + --hvm \ + --virt-type kvm \ + --noreboot + +echo "--- Provisioning Sequence Complete ---" +echo "To begin testing: virsh start $VM_NAME --console" |
