summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
18 files changed, 1533 insertions, 0 deletions
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