summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile47
-rw-r--r--main.c165
-rw-r--r--src/hoodsgate/control_plane/cp_policy_engine.c205
-rw-r--r--src/hoodsgate/control_plane/cp_policy_engine.h34
-rw-r--r--src/hoodsgate/control_plane/cp_state_manager.c96
-rw-r--r--src/hoodsgate/control_plane/cp_state_manager.h56
-rw-r--r--src/hoodsgate/control_plane/data_plane/dp_netlink_routes.c1
-rw-r--r--src/hoodsgate/control_plane/data_plane/dp_netlink_routes.h6
-rw-r--r--src/hoodsgate/control_plane/data_plane/dp_nftables.c1
-rw-r--r--src/hoodsgate/control_plane/data_plane/dp_nftables.h6
-rw-r--r--src/hoodsgate/hgcommon.c160
-rw-r--r--src/hoodsgate/hgcommon.h53
-rw-r--r--src/hoodsgate/hoodsgate.c253
-rw-r--r--src/hoodsgate/hoodsgate.h18
-rw-r--r--src/hoodsgate/kvm_api/ka_interface.c158
-rw-r--r--src/hoodsgate/kvm_api/ka_interface.h90
-rw-r--r--src/main.c181
-rw-r--r--src/main.h17
-rw-r--r--src/tailnet.c173
-rw-r--r--src/tailnet.h25
-rw-r--r--test-env/README.md110
-rwxr-xr-xtest-env/net-sim/netsim.sh489
-rwxr-xr-xtest-env/provision-vm.sh118
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
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..39c8d47
--- /dev/null
+++ b/main.c
@@ -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"