diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/hoodsgate/control_plane/cp_policy_engine.c | 205 | ||||
| -rw-r--r-- | src/hoodsgate/control_plane/cp_policy_engine.h | 34 | ||||
| -rw-r--r-- | src/hoodsgate/control_plane/cp_state_manager.c | 96 | ||||
| -rw-r--r-- | src/hoodsgate/control_plane/cp_state_manager.h | 56 | ||||
| -rw-r--r-- | src/hoodsgate/control_plane/data_plane/dp_netlink_routes.c | 1 | ||||
| -rw-r--r-- | src/hoodsgate/control_plane/data_plane/dp_netlink_routes.h | 6 | ||||
| -rw-r--r-- | src/hoodsgate/control_plane/data_plane/dp_nftables.c | 1 | ||||
| -rw-r--r-- | src/hoodsgate/control_plane/data_plane/dp_nftables.h | 6 | ||||
| -rw-r--r-- | src/hoodsgate/hgcommon.c | 160 | ||||
| -rw-r--r-- | src/hoodsgate/hgcommon.h | 53 | ||||
| -rw-r--r-- | src/hoodsgate/hoodsgate.c | 253 | ||||
| -rw-r--r-- | src/hoodsgate/hoodsgate.h | 18 | ||||
| -rw-r--r-- | src/hoodsgate/kvm_api/ka_interface.c | 158 | ||||
| -rw-r--r-- | src/hoodsgate/kvm_api/ka_interface.h | 90 | ||||
| -rw-r--r-- | src/main.c | 181 | ||||
| -rw-r--r-- | src/main.h | 17 | ||||
| -rw-r--r-- | src/tailnet.c | 173 | ||||
| -rw-r--r-- | src/tailnet.h | 25 |
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 |
