linkd

Control plane daemon for unos
git clone git://git.finwo.net/app/linkd
Log | Files | Refs | README

commit 58e0844b7c2805cb42e7a6d896693f8e2584158a
parent 6d253db83e32a58928c3cf15a2daa1ef638e9a9b
Author: finwo <finwo@pm.me>
Date:   Thu, 17 Sep 2026 22:38:03 +0200

Add basic ifupdown functionality to unosd

Diffstat:
Asrc/cli/unosc.c | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/config/ifaces.c | 332+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/config/ifaces.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Msrc/config/ports.c | 3++-
Asrc/ipc.c | 446+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ipc.h | 13+++++++++++++
Msrc/main.c | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/netlink/netlink.c | 20++++++++++++++++++--
Msrc/util/config.c | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/util/config.h | 11++++++++---
Mtarget/common/Makefile | 4++--
11 files changed, 1215 insertions(+), 40 deletions(-)

diff --git a/src/cli/unosc.c b/src/cli/unosc.c @@ -0,0 +1,97 @@ +#define _GNU_SOURCE +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <libgen.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <unistd.h> + +#define UNOS_IPC_PATH "/run/unosd.sock" + +int main(int argc, char *argv[]) { + const char *prog = basename(argv[0]); + const char *cmd = NULL; + int cmd_argc = 0; + char **cmd_argv = NULL; + int sock; + struct sockaddr_un addr; + char buf[4096]; + + // Determine command from argv[0] or argv[1] + if (!strcmp(prog, "unosc")) { + if (argc < 2) { + fprintf(stderr, "usage: %s <ifup|ifdown|ifquery|ifreload> [args...]\n", prog); + fprintf(stderr, " or: ifup/ifdown/ifquery/ifreload as symlinks to unosc\n"); + return 1; + } + cmd = argv[1]; + cmd_argc = argc - 2; + cmd_argv = argv + 2; + } else { + // Called as ifup, ifdown, etc. via symlink + cmd = prog; + cmd_argc = argc - 1; + cmd_argv = argv + 1; + } + + sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sock < 0) { + perror("socket"); + return 1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, UNOS_IPC_PATH, sizeof(addr.sun_path)-1); + + if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + fprintf(stderr, "unosc: connect %s failed: %s\n", UNOS_IPC_PATH, strerror(errno)); + fprintf(stderr, "Is unosd running?\n"); + close(sock); + return 1; + } + + // Send command: "cmd [args...]\n" + { + char out[4096]; + int len = 0; + len += snprintf(out+len, sizeof(out)-len, "%s", cmd); + for (int i = 0; i < cmd_argc; i++) { + len += snprintf(out+len, sizeof(out)-len, " %s", cmd_argv[i]); + } + len += snprintf(out+len, sizeof(out)-len, "\n"); + if (write(sock, out, len) != len) { + perror("write"); + close(sock); + return 1; + } + } + + // Read response and pipe to stdout + // Protocol: server sends lines, first line is OK/ERR, but for ifquery the dump is the response + // We just pipe everything to stdout, and exit 0 on OK, 1 on ERR + int saw_ok = 0; + int saw_err = 0; + FILE *f = fdopen(sock, "r"); + if (!f) { + perror("fdopen"); + close(sock); + return 1; + } + while (fgets(buf, sizeof(buf), f)) { + // Check for OK/ERR prefix on first line? Just print and detect + if (!saw_ok && !saw_err) { + if (!strncmp(buf, "OK", 2)) saw_ok = 1; + else if (!strncmp(buf, "ERR", 3)) saw_err = 1; + } + fputs(buf, stdout); + // For non-ifquery, the response is just OK/ERR line + // For ifquery, there may be multiple lines plus OK + } + fclose(f); + // If we saw ERR, exit 1 + if (saw_err) return 1; + return 0; +} diff --git a/src/config/ifaces.c b/src/config/ifaces.c @@ -0,0 +1,332 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> + +#include "finwo/cnfparse.h" + +#include "util/config.h" + +#include "ifaces.h" + +static struct unos_iface *ifaces = NULL; +static struct unos_iface *ifaces_tail = NULL; + +struct unos_iface * ifaces_list(void) { + return ifaces; +} + +struct unos_iface * ifaces_find(const char *name) { + struct unos_iface *cur; + for (cur = ifaces; cur; cur = cur->next) { + if (!strcmp(cur->name, name)) return cur; + } + return NULL; +} + +void ifaces_set_auto(const char *name, int flag) { + struct unos_iface *cur = ifaces_find(name); + if (cur) { + cur->auto_flag = flag; + return; + } + // Create placeholder for auto without iface block yet (common in Debian: auto comes before iface) + cur = calloc(1, sizeof(*cur)); + cur->name = strdup(name); + cur->vlan_id = -1; + cur->auto_flag = flag; + if (ifaces_tail) { + ifaces_tail->next = cur; + } else { + ifaces = cur; + } + ifaces_tail = cur; +} + +void ifaces_free(void) { + struct unos_iface *cur = ifaces; + struct unos_iface *next; + struct iface_addr *a, *anext; + while (cur) { + next = cur->next; + free(cur->name); + free(cur->method); + free(cur->family); + free(cur->gateway); + free(cur->broadcast); + free(cur->hwaddress); + free(cur->vlan_raw_device); + free(cur->pre_up); + free(cur->post_up); + free(cur->pre_down); + free(cur->post_down); + for (a = cur->addrs; a; a = anext) { + anext = a->next; + free(a->address); + free(a->netmask); + free(a); + } + free(cur); + cur = next; + } + ifaces = NULL; + ifaces_tail = NULL; +} + +static void iface_add_addr(struct unos_iface *iface, const char *addr, const char *netmask) { + struct iface_addr *a = calloc(1, sizeof(*a)); + a->address = strdup(addr); + if (netmask) a->netmask = strdup(netmask); + a->next = NULL; + // append + if (!iface->addrs) { + iface->addrs = a; + } else { + struct iface_addr *cur = iface->addrs; + while (cur->next) cur = cur->next; + cur->next = a; + } +} + +// auto <if> [<if> ...] +struct cnf_directive * cfg_parse_auto(FILE *fd, struct cnf_directive *dir, void *user) { + (void)fd; (void)user; + int i; + for (i = 0; i < (int)dir->argc; i++) { + ifaces_set_auto(dir->argv[i], 1); + } + cnf_directive_free(dir); + return NULL; +} + +// iface <name> [inet|inet6 <method>] -- cumulus style `iface <name>` preferred (no inet) +struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void *user) { + struct unos_iface *iface; + (void)user; + + if (dir->argc < 1 || dir->argc > 3) { + fprintf(stderr, "`iface` directive: expected `iface <name>` or `iface <name> inet <method>`\n"); + exit(1); + } + + const char *name = dir->argv[0]; + const char *family = NULL; + const char *method = NULL; + + if (dir->argc == 1) { + // Cumulus style: iface <name> -- defaults to static, no family + method = "static"; + } else if (dir->argc == 3) { + // Traditional: iface <name> inet static + family = dir->argv[1]; + method = dir->argv[2]; + if (strcasecmp(family, "inet") && strcasecmp(family, "inet6")) { + fprintf(stderr, "`iface %s` unknown family: %s (expected inet/inet6)\n", name, family); + exit(1); + } + } else { + fprintf(stderr, "`iface` directive: expected `iface <name>` or `iface <name> inet <method>`\n"); + exit(1); + } + + // Find existing placeholder from auto, or create new + iface = ifaces_find(name); + int was_placeholder = 0; + if (iface && !iface->method && !iface->family && !iface->addrs) { + // placeholder from auto -- reuse + was_placeholder = 1; + } else if (iface) { + // Duplicate iface block -- for now we merge? Cumulus allows multiple iface stanzas for same if + // but we treat as error to keep simple, unless it's same name with different family + // Allow merging: if family/method differ, we keep as separate? For now, error on exact duplicate + // Check if same iface already has addrs/method -- if so, create new entry with same name but as separate node + // To keep diff-apply simple, we allow multiple entries with same name as separate list nodes + // So we will create a new node even if name exists, unless it was placeholder + was_placeholder = 0; + } + + if (iface && was_placeholder) { + // reuse placeholder + free(iface->method); + free(iface->family); + iface->method = method ? strdup(method) : NULL; + iface->family = family ? strdup(family) : NULL; + // keep auto_flag + } else { + // create new + iface = calloc(1, sizeof(*iface)); + iface->name = strdup(name); + iface->method = method ? strdup(method) : NULL; + iface->family = family ? strdup(family) : NULL; + iface->vlan_id = -1; + // check if auto was previously set for this name via placeholder? Already handled, but if not placeholder, check if any placeholder had auto + // Actually ifaces_set_auto may have created placeholder with auto, we already handled. + // If no placeholder, auto_flag stays 0 until auto directive is parsed (order may be auto before or after iface) + // So we need to check if there was a prior auto for this name that wasn't yet linked -- but our placeholder logic already covers. + if (ifaces_tail) { + ifaces_tail->next = iface; + } else { + ifaces = iface; + } + ifaces_tail = iface; + } + + // Handle vlan dot notation: swp1.100 -> raw_device swp1, vlan_id 100 + char *dot = strrchr(iface->name, '.'); + if (dot && !iface->vlan_raw_device) { + char *end = NULL; + long vid = strtol(dot+1, &end, 10); + if (end != dot+1 && *end == '\0' && vid >= 1 && vid <= 4094) { + iface->vlan_id = (int)vid; + // Only set raw_device if not already set via explicit vlan-raw-device + // Keep as is, will be resolved later if needed + } + } + + for(;;) { + if (dir) cnf_directive_free(dir); + dir = cnf_directive_read(fd); + if (!dir) break; + + if(0) {} + else if (!strcasecmp("address", dir->name)) { + if (dir->argc < 1 || dir->argc > 2) { + fprintf(stderr, "`address` expects 1 or 2 args (address + optional netmask)\n"); + exit(1); + } + const char *addr = dir->argv[0]; + const char *nm = dir->argc > 1 ? dir->argv[1] : NULL; + iface_add_addr(iface, addr, nm); + } + else if (!strcasecmp("netmask", dir->name)) { + if (dir->argc != 1) { + fprintf(stderr, "`netmask` expects 1 arg\n"); + exit(1); + } + // Attach netmask to last address without netmask + struct iface_addr *a = iface->addrs; + if (!a) { + fprintf(stderr, "`netmask` without preceding `address`\n"); + exit(1); + } + while (a->next) a = a->next; + free(a->netmask); + a->netmask = strdup(dir->argv[0]); + } + else if (!strcasecmp("broadcast", dir->name)) { + if (dir->argc != 1) { fprintf(stderr, "`broadcast` expects 1 arg\n"); exit(1); } + free(iface->broadcast); + iface->broadcast = strdup(dir->argv[0]); + } + else if (!strcasecmp("gateway", dir->name)) { + if (dir->argc != 1) { fprintf(stderr, "`gateway` expects 1 arg\n"); exit(1); } + free(iface->gateway); + iface->gateway = strdup(dir->argv[0]); + } + else if (!strcasecmp("mtu", dir->name)) { + if (dir->argc != 1) { fprintf(stderr, "`mtu` expects 1 arg\n"); exit(1); } + iface->mtu = atoi(dir->argv[0]); + } + else if (!strcasecmp("hwaddress", dir->name) || !strcasecmp("hw-address", dir->name)) { + if (dir->argc != 1) { fprintf(stderr, "`hwaddress` expects 1 arg\n"); exit(1); } + free(iface->hwaddress); + iface->hwaddress = strdup(dir->argv[0]); + } + else if (!strcasecmp("vlan-raw-device", dir->name) || !strcasecmp("vlan_raw_device", dir->name)) { + if (dir->argc != 1) { fprintf(stderr, "`vlan-raw-device` expects 1 arg\n"); exit(1); } + free(iface->vlan_raw_device); + iface->vlan_raw_device = strdup(dir->argv[0]); + } + else if (!strcasecmp("vlan-id", dir->name) || !strcasecmp("vlan_id", dir->name)) { + if (dir->argc != 1) { fprintf(stderr, "`vlan-id` expects 1 arg\n"); exit(1); } + iface->vlan_id = atoi(dir->argv[0]); + } + else if (!strcasecmp("pre-up", dir->name) || !strcasecmp("pre_up", dir->name)) { + if (dir->argc < 1) { fprintf(stderr, "`pre-up` expects at least 1 arg\n"); exit(1); } + // Join argv with spaces + size_t len = 0; + for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1; + char *cmd = calloc(1, len+1); + for (int i=0;i<(int)dir->argc;i++) { + strcat(cmd, dir->argv[i]); + if (i+1 < (int)dir->argc) strcat(cmd, " "); + } + free(iface->pre_up); + iface->pre_up = cmd; + } + else if (!strcasecmp("post-up", dir->name) || !strcasecmp("post_up", dir->name)) { + if (dir->argc < 1) { fprintf(stderr, "`post-up` expects at least 1 arg\n"); exit(1); } + size_t len = 0; + for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1; + char *cmd = calloc(1, len+1); + for (int i=0;i<(int)dir->argc;i++) { + strcat(cmd, dir->argv[i]); + if (i+1 < (int)dir->argc) strcat(cmd, " "); + } + free(iface->post_up); + iface->post_up = cmd; + } + else if (!strcasecmp("pre-down", dir->name) || !strcasecmp("pre_down", dir->name)) { + if (dir->argc < 1) { fprintf(stderr, "`pre-down` expects at least 1 arg\n"); exit(1); } + size_t len = 0; + for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1; + char *cmd = calloc(1, len+1); + for (int i=0;i<(int)dir->argc;i++) { + strcat(cmd, dir->argv[i]); + if (i+1 < (int)dir->argc) strcat(cmd, " "); + } + free(iface->pre_down); + iface->pre_down = cmd; + } + else if (!strcasecmp("post-down", dir->name) || !strcasecmp("post_down", dir->name)) { + if (dir->argc < 1) { fprintf(stderr, "`post-down` expects at least 1 arg\n"); exit(1); } + size_t len = 0; + for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1; + char *cmd = calloc(1, len+1); + for (int i=0;i<(int)dir->argc;i++) { + strcat(cmd, dir->argv[i]); + if (i+1 < (int)dir->argc) strcat(cmd, " "); + } + free(iface->post_down); + iface->post_down = cmd; + } + else if (!strcasecmp("up", dir->name)) { + // alias for post-up + if (dir->argc < 1) { fprintf(stderr, "`up` expects at least 1 arg\n"); exit(1); } + size_t len = 0; + for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1; + char *cmd = calloc(1, len+1); + for (int i=0;i<(int)dir->argc;i++) { + strcat(cmd, dir->argv[i]); + if (i+1 < (int)dir->argc) strcat(cmd, " "); + } + free(iface->post_up); + iface->post_up = cmd; + } + else if (!strcasecmp("down", dir->name)) { + if (dir->argc < 1) { fprintf(stderr, "`down` expects at least 1 arg\n"); exit(1); } + size_t len = 0; + for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1; + char *cmd = calloc(1, len+1); + for (int i=0;i<(int)dir->argc;i++) { + strcat(cmd, dir->argv[i]); + if (i+1 < (int)dir->argc) strcat(cmd, " "); + } + free(iface->pre_down); + iface->pre_down = cmd; + } + else { + break; + } + } + + return dir; +} + +void ifaces_register(void) { + struct cfg_ns *ns = cfg_ns_get("interfaces"); + cfg_register_directive(ns, "auto", cfg_parse_auto); + cfg_register_directive(ns, "allow-auto", cfg_parse_auto); + cfg_register_directive(ns, "allow-hotplug", cfg_parse_auto); + cfg_register_directive(ns, "iface", cfg_parse_iface); +} diff --git a/src/config/ifaces.h b/src/config/ifaces.h @@ -0,0 +1,47 @@ +#ifndef __UNOS_CONFIG_IFACES_H__ +#define __UNOS_CONFIG_IFACES_H__ + +#include <stdint.h> +#include <stdio.h> + +#include "finwo/cnfparse.h" + +// Network interface configuration from /etc/network/interfaces +// IPv6 is first-class: address may be v4 or v6, gateway may be v4 or v6. + +struct iface_addr { + char *address; // "192.168.1.1/24" or "2001:db8::1/64" + char *netmask; // v4 only, optional + struct iface_addr *next; +}; + +struct unos_iface { + char *name; // e.g., "eth0", "swp1.100" -- cumulus style `iface <name>` preferred + char *method; // "static", "dhcp", "loopback", "manual" -- optional, defaults to "static" when `iface <name>` used without `inet` + char *family; // "inet", "inet6" -- optional, NULL when cumulus style + struct iface_addr *addrs; + char *gateway; + char *broadcast; + int mtu; // 0 = unset + char *hwaddress; + char *vlan_raw_device; + int vlan_id; // -1 = unset, 1..4094 + char *pre_up; + char *post_up; + char *pre_down; + char *post_down; + int auto_flag; // 1 if listed in auto + struct unos_iface *next; +}; + +void ifaces_register(void); + +struct cnf_directive * cfg_parse_auto(FILE *fd, struct cnf_directive *dir, void *user); +struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void *user); + +struct unos_iface * ifaces_list(void); +struct unos_iface * ifaces_find(const char *name); +void ifaces_free(void); +void ifaces_set_auto(const char *name, int flag); + +#endif // __UNOS_CONFIG_IFACES_H__ diff --git a/src/config/ports.c b/src/config/ports.c @@ -166,5 +166,6 @@ struct cnf_directive * cfg_parse_port(FILE *fd, struct cnf_directive *dir, void } void ports_register(void) { - cfg_register_directive("port", cfg_parse_port); + struct cfg_ns *ns = cfg_ns_get("ports"); + cfg_register_directive(ns, "port", cfg_parse_port); } diff --git a/src/ipc.c b/src/ipc.c @@ -0,0 +1,446 @@ +#define _GNU_SOURCE +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/un.h> +#include <unistd.h> +#include <sys/types.h> + +#include "rxi/log.h" + +#include "util/config.h" +#include "config/ifaces.h" +#include "ipc.h" +#include "netlink/netlink.h" + +static int ipc_sock = -1; + +int ipc_fd(void) { + return ipc_sock; +} + +int ipc_init(void) { + struct sockaddr_un addr; + // Ensure /run exists (tmpfs mount in rcS, but early boot may not have it) + mkdir("/run", 0755); + unlink(UNOS_IPC_PATH); + + ipc_sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (ipc_sock < 0) { + log_error("ipc: socket failed: %s", strerror(errno)); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, UNOS_IPC_PATH, sizeof(addr.sun_path)-1); + + if (bind(ipc_sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + log_error("ipc: bind %s failed: %s", UNOS_IPC_PATH, strerror(errno)); + close(ipc_sock); + ipc_sock = -1; + return -1; + } + + if (chmod(UNOS_IPC_PATH, 0600) != 0) { + log_warn("ipc: chmod %s failed: %s", UNOS_IPC_PATH, strerror(errno)); + } + + if (listen(ipc_sock, 8) != 0) { + log_error("ipc: listen failed: %s", strerror(errno)); + close(ipc_sock); + ipc_sock = -1; + unlink(UNOS_IPC_PATH); + return -1; + } + + log_info("ipc: listening on %s", UNOS_IPC_PATH); + return 0; +} + +void ipc_fini(void) { + if (ipc_sock >= 0) { + close(ipc_sock); + ipc_sock = -1; + } + unlink(UNOS_IPC_PATH); +} + +// Helpers to send response +static void ipc_send_ok(FILE *out, const char *msg) { + if (msg) fprintf(out, "OK %s\n", msg); + else fprintf(out, "OK\n"); +} + +static void ipc_send_err(FILE *out, const char *msg) { + fprintf(out, "ERR %s\n", msg ? msg : "unknown error"); +} + +// Forward declarations for handlers (implemented below, using netlink helpers) +static int handle_ifup(FILE *out, const char *ifname, uid_t uid); +static int handle_ifdown(FILE *out, const char *ifname, uid_t uid); +static int handle_ifreload(FILE *out, uid_t uid); +static int handle_ifquery(FILE *out, const char *ifname); + +// IPC namespace handlers -- these are registered with cfg_ns_get("ipc") +// They are invoked via cfg_parse on the connected socket's FILE* +static struct cnf_directive *cfg_handle_ifup(FILE *fd, struct cnf_directive *dir, void *user) { + FILE *out = (FILE*)user; + (void)fd; + log_info("ipc: cfg_handle_ifup argc=%d", dir->argc); + uid_t uid = 0; + extern uid_t ipc_current_uid; + uid = ipc_current_uid; + if (uid != 0) { + ipc_send_err(out, "permission denied: ifup requires root"); + cnf_directive_free(dir); + return NULL; + } + if (dir->argc != 1) { + ipc_send_err(out, "ifup expects 1 arg: <ifname>"); + cnf_directive_free(dir); + return NULL; + } + log_info("ipc: cfg_handle_ifup %s uid=%d", dir->argv[0], uid); + if (handle_ifup(out, dir->argv[0], uid) == 0) ipc_send_ok(out, NULL); + else log_info("ipc: handle_ifup %s failed", dir->argv[0]); + cnf_directive_free(dir); + return NULL; +} + +static struct cnf_directive *cfg_handle_ifdown(FILE *fd, struct cnf_directive *dir, void *user) { + FILE *out = (FILE*)user; + (void)fd; + extern uid_t ipc_current_uid; + uid_t uid = ipc_current_uid; + if (uid != 0) { + ipc_send_err(out, "permission denied: ifdown requires root"); + cnf_directive_free(dir); + return NULL; + } + if (dir->argc != 1) { + ipc_send_err(out, "ifdown expects 1 arg: <ifname>"); + cnf_directive_free(dir); + return NULL; + } + if (handle_ifdown(out, dir->argv[0], uid) == 0) ipc_send_ok(out, NULL); + cnf_directive_free(dir); + return NULL; +} + +static struct cnf_directive *cfg_handle_ifreload(FILE *fd, struct cnf_directive *dir, void *user) { + FILE *out = (FILE*)user; + (void)fd; + extern uid_t ipc_current_uid; + uid_t uid = ipc_current_uid; + if (uid != 0) { + ipc_send_err(out, "permission denied: ifreload requires root"); + cnf_directive_free(dir); + return NULL; + } + if (dir->argc != 0) { + ipc_send_err(out, "ifreload expects no args"); + cnf_directive_free(dir); + return NULL; + } + if (handle_ifreload(out, uid) == 0) ipc_send_ok(out, NULL); + cnf_directive_free(dir); + return NULL; +} + +static struct cnf_directive *cfg_handle_ifquery(FILE *fd, struct cnf_directive *dir, void *user) { + FILE *out = (FILE*)user; + (void)fd; + log_info("ipc: ifquery argc=%d", dir->argc); + const char *ifname = NULL; + if (dir->argc > 1) { + ipc_send_err(out, "ifquery expects 0 or 1 arg"); + cnf_directive_free(dir); + return NULL; + } + if (dir->argc == 1) ifname = dir->argv[0]; + log_info("ipc: ifquery %s", ifname ? ifname : "(all)"); + handle_ifquery(out, ifname); + cnf_directive_free(dir); + return NULL; +} + +// Global for current uid (single-threaded, one client at a time) +uid_t ipc_current_uid = 0; + +int ipc_handle(void) { + log_info("ipc: handle called"); + int cfd; + struct sockaddr_un addr; + socklen_t alen = sizeof(addr); + struct ucred cred; + socklen_t clen = sizeof(cred); + FILE *fin = NULL; + FILE *fout = NULL; + int fd_dup; + + cfd = accept4(ipc_sock, (struct sockaddr*)&addr, &alen, SOCK_CLOEXEC | SOCK_NONBLOCK); + if (cfd < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; + log_error("ipc: accept failed: %s", strerror(errno)); + return -1; + } + + // Get peer credentials + if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &clen) == 0) { + ipc_current_uid = cred.uid; + } else { + ipc_current_uid = 99999; + } + + // Use fdopen for reading and writing -- need separate FILE* for read and write to handle duplex? + // We'll use one FILE* for reading commands, and the fd directly for writing responses. + // Simpler: use fdopen for reading, and use fdopen dup for writing. + fd_dup = dup(cfd); + if (fd_dup < 0) { + close(cfd); + return -1; + } + fin = fdopen(cfd, "r"); + fout = fdopen(fd_dup, "w"); + if (!fin || !fout) { + if (fin) fclose(fin); else close(cfd); + if (fout) fclose(fout); else close(fd_dup); + return -1; + } + + // Set line buffering + setvbuf(fout, NULL, _IONBF, 0); + + // Parse one or more commands from this connection (each line is a command) + // Use cfg_parse on the ipc namespace + struct cfg_ns *ns = cfg_ns_get("ipc"); + // cfg_parse will read directives from fin and dispatch to handlers, which write to fout + // It handles multiple lines until EOF. + cfg_parse(ns, NULL, fin, fout); + + // Ensure OK is sent if handler didn't already? Our handlers send OK/ERR themselves. + // Flush and close + fflush(fout); + fclose(fin); // also closes cfd + fclose(fout); + ipc_current_uid = 0; + return 0; +} + +// Register IPC commands +void ipc_register(void) { + struct cfg_ns *ns = cfg_ns_get("ipc"); + cfg_register_directive(ns, "ifup", cfg_handle_ifup); + cfg_register_directive(ns, "ifdown", cfg_handle_ifdown); + cfg_register_directive(ns, "ifreload", cfg_handle_ifreload); + cfg_register_directive(ns, "ifquery", cfg_handle_ifquery); +} + +// Stubs for netlink helpers (to be implemented in netlink.c or here) +// For now, provide minimal implementations that just log and return OK +// The real implementation will be wired in the next commit (Wire netlink apply) + +#include <sys/wait.h> + +static int run_hook(const char *cmd) { + if (!cmd || !*cmd) return 0; + log_info("ipc: running hook: %s", cmd); + int rc = system(cmd); + if (rc != 0) { + log_warn("hook `%s` failed with %d", cmd, rc); + return -1; + } + return 0; +} + +static int handle_ifup(FILE *out, const char *ifname, uid_t uid) { + (void)uid; + struct unos_iface *iface = ifaces_find(ifname); + if (!iface) { + log_info("ipc: ifup %s (not in interfaces, just bringing link up)", ifname); + char cmd[512]; + snprintf(cmd, sizeof(cmd), "ip link set %s up 2>&1", ifname); + int rc = system(cmd); + if (rc != 0) { + fprintf(out, "ERR ifup %s failed\n", ifname); + return -1; + } + return 0; + } + log_info("ipc: ifup %s", ifname); + if (iface->pre_up && run_hook(iface->pre_up) != 0) { + fprintf(out, "ERR pre-up hook failed for %s\n", ifname); + return -1; + } + // VLAN handling: if iface is vlan, ensure raw device exists and create vlan link + // For now, just bring link up and add addresses via netlink helpers (to be implemented) + // Placeholder: use system ip commands via netlink abstraction + // TODO: wire real netlink RTM_NEWLINK/NEWADDR + char cmd[512]; + int rc; + // Bring link up + snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s up", ifname); + rc = system(cmd); + log_info("ipc: ifup %s link up rc=%d cmd='%s'", ifname, rc, cmd); + if (rc != 0) { fprintf(out, "ERR ifup %s: link up failed (%d)\n", ifname, rc); return -1; } + // Add addresses + struct iface_addr *a; + for (a = iface->addrs; a; a = a->next) { + snprintf(cmd, sizeof(cmd), "/sbin/ip addr add %s dev %s 2>&1", a->address, ifname); + rc = system(cmd); + log_info("ipc: ifup %s addr %s rc=%d", ifname, a->address, rc); + // File exists is not fatal (addr already present) + if (rc != 0 && rc != 256) { /* 256 = File exists from ip */ } + } + if (iface->gateway) { + snprintf(cmd, sizeof(cmd), "/sbin/ip route add default via %s 2>&1 || true", iface->gateway); + rc = system(cmd); + log_info("ipc: ifup %s gateway %s rc=%d", ifname, iface->gateway, rc); + } + if (iface->mtu) { + snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s mtu %d", ifname, iface->mtu); + rc = system(cmd); + log_info("ipc: ifup %s mtu %d rc=%d", ifname, iface->mtu, rc); + if (rc != 0) { fprintf(out, "ERR ifup %s: mtu failed\n", ifname); return -1; } + } + if (iface->hwaddress) { + snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s address %s", ifname, iface->hwaddress); + rc = system(cmd); + log_info("ipc: ifup %s hwaddress %s rc=%d", ifname, iface->hwaddress, rc); + if (rc != 0) { fprintf(out, "ERR ifup %s: hwaddress failed\n", ifname); return -1; } + } + if (iface->post_up && run_hook(iface->post_up) != 0) { + fprintf(out, "ERR post-up hook failed for %s\n", ifname); + return -1; + } + return 0; +} + +static int handle_ifdown(FILE *out, const char *ifname, uid_t uid) { + (void)uid; + struct unos_iface *iface = ifaces_find(ifname); + if (!iface) { + log_info("ipc: ifdown %s (not in interfaces, just bringing link down)", ifname); + char cmd[512]; + snprintf(cmd, sizeof(cmd), "ip link set %s down 2>&1", ifname); + int rc = system(cmd); + if (rc != 0) { + fprintf(out, "ERR ifdown %s failed\n", ifname); + return -1; + } + return 0; + } + log_info("ipc: ifdown %s", ifname); + if (iface->pre_down && run_hook(iface->pre_down) != 0) { + fprintf(out, "ERR pre-down hook failed for %s\n", ifname); + return -1; + } + char cmd[512]; + int rc2; + snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s down", ifname); + rc2 = system(cmd); + log_info("ipc: ifdown %s rc=%d", ifname, rc2); + if (rc2 != 0) { fprintf(out, "ERR ifdown %s failed (%d)\n", ifname, rc2); return -1; } + // Flush addresses (keep for now) + if (iface->post_down && run_hook(iface->post_down) != 0) { + fprintf(out, "ERR post-down hook failed for %s\n", ifname); + return -1; + } + return 0; +} + +static int handle_ifreload(FILE *out, uid_t uid) { + (void)uid; + log_info("ipc: ifreload (diff-apply)"); + // For now, just re-parse and apply all auto interfaces + // TODO: implement diff-apply: compare old vs new lists + // Placeholder: reload config files + // We need to free old and re-parse + ifaces_free(); + // Re-register is idempotent, but we need to reload + // Use same logic as main.c load_interfaces + char *dir = "/etc/network"; + char path[1024]; + char stub[1024]; + FILE *fd; + snprintf(path, sizeof(path), "%s/interfaces", dir); + if (access(path, R_OK) == 0) { + fd = fopen(path, "r"); + if (fd) { + cfg_parse(cfg_ns_get("interfaces"), dir, fd, NULL); + fclose(fd); + } + } else { + snprintf(stub, sizeof(stub), "source interfaces.d/*.cnf\n"); + fd = fmemopen(stub, strlen(stub), "r"); + if (fd) { + cfg_parse(cfg_ns_get("interfaces"), dir, fd, NULL); + fclose(fd); + } + } + // Also reload ports? + snprintf(path, sizeof(path), "%s/ports", dir); + if (access(path, R_OK) == 0) { + fd = fopen(path, "r"); + if (fd) { + cfg_parse(cfg_ns_get("ports"), dir, fd, NULL); + fclose(fd); + } + } else { + snprintf(stub, sizeof(stub), "source ports.d/*.cnf\n"); + fd = fmemopen(stub, strlen(stub), "r"); + if (fd) { + cfg_parse(cfg_ns_get("ports"), dir, fd, NULL); + fclose(fd); + } + } + return 0; +} + +static int handle_ifquery(FILE *out, const char *ifname) { + struct unos_iface *cur; + int n = 0; + for (cur = ifaces_list(); cur; cur = cur->next) n++; + log_info("ipc: ifquery %s (list has %d)", ifname ? ifname : "(all)", n); + if (ifname) { + cur = ifaces_find(ifname); + log_info("ipc: ifquery find %s -> %s", ifname, cur ? "found" : "not found"); + if (!cur) { + fprintf(out, "ERR no such interface: %s\n", ifname); + return -1; + } + // Dump this iface in cnf style + fprintf(out, "iface %s\n", cur->name); + for (struct iface_addr *a = cur->addrs; a; a = a->next) { + fprintf(out, " address %s\n", a->address); + if (a->netmask) fprintf(out, " netmask %s\n", a->netmask); + } + if (cur->gateway) fprintf(out, " gateway %s\n", cur->gateway); + if (cur->mtu) fprintf(out, " mtu %d\n", cur->mtu); + if (cur->hwaddress) fprintf(out, " hwaddress %s\n", cur->hwaddress); + if (cur->vlan_raw_device) fprintf(out, " vlan-raw-device %s\n", cur->vlan_raw_device); + if (cur->vlan_id >= 0) fprintf(out, " vlan-id %d\n", cur->vlan_id); + if (cur->pre_up) fprintf(out, " pre-up %s\n", cur->pre_up); + if (cur->post_up) fprintf(out, " post-up %s\n", cur->post_up); + if (cur->pre_down) fprintf(out, " pre-down %s\n", cur->pre_down); + if (cur->post_down) fprintf(out, " post-down %s\n", cur->post_down); + fprintf(out, "OK\n"); + } else { + for (cur = ifaces_list(); cur; cur = cur->next) { + fprintf(out, "iface %s", cur->name); + if (cur->method) fprintf(out, " %s", cur->method); + fprintf(out, "\n"); + for (struct iface_addr *a = cur->addrs; a; a = a->next) { + fprintf(out, " address %s\n", a->address); + } + if (cur->gateway) fprintf(out, " gateway %s\n", cur->gateway); + } + fprintf(out, "OK\n"); + } + return 0; +} diff --git a/src/ipc.h b/src/ipc.h @@ -0,0 +1,13 @@ +#ifndef __UNOS_IPC_H__ +#define __UNOS_IPC_H__ + +#define UNOS_IPC_PATH "/run/unosd.sock" + +// Server side (unosd) +int ipc_init(void); +int ipc_fd(void); +int ipc_handle(void); // accept and handle one client, returns 0 on handled, -1 on error +void ipc_fini(void); +void ipc_register(void); + +#endif // __UNOS_IPC_H__ diff --git a/src/main.c b/src/main.c @@ -16,8 +16,10 @@ extern "C" { #include "rxi/log.h" #include "config/ports.h" +#include "config/ifaces.h" #include "dataplane.h" #include "dataplane/registry.h" +#include "ipc.h" #include "netlink/netlink.h" #include "util/config.h" @@ -59,20 +61,19 @@ static void stop_handler(int sig) { nl_request_stop(); } -// Port configuration lives in either: -// <dir>/ports.cnf a single file, or -// <dir>/ports.d/*.cnf a drop-in directory +// Port and interface configuration live in either: +// <dir>/ports, <dir>/interfaces single files, or +// <dir>/ports.d/*.cnf, <dir>/interfaces.d/*.cnf drop-ins // -// Rather than growing the config API with a glob entry point, the drop-in case -// is handed to the parser as an in-memory config consisting of nothing but an -// `include` -- the parser already knows how to expand those. +// Rather than growing the config API, the drop-in case is handed to the parser +// as an in-memory config consisting of `source` -- the parser expands it. static int load_ports(const char *dir) { char path[1024]; char stub[1024]; FILE *fd; int rc; - snprintf(path, sizeof(path), "%s/ports.cnf", dir); + snprintf(path, sizeof(path), "%s/ports", dir); if (access(path, R_OK) == 0) { fd = fopen(path, "r"); @@ -81,7 +82,7 @@ static int load_ports(const char *dir) { return CFG_RET_ERROR; } } else { - snprintf(stub, sizeof(stub), "include ports.d/*.cnf\n"); + snprintf(stub, sizeof(stub), "source ports.d/*.cnf\n"); fd = fmemopen(stub, strlen(stub), "r"); if (!fd) { perror("Could not open in-memory config"); @@ -89,7 +90,7 @@ static int load_ports(const char *dir) { } } - rc = cfg_parse(dir, fd, NULL); + rc = cfg_parse(cfg_ns_get("ports"), dir, fd, NULL); if (rc < 0) { log_error("Error during reading port configuration from %s", dir); } @@ -98,6 +99,38 @@ static int load_ports(const char *dir) { return rc; } +static int load_interfaces(const char *dir) { + char path[1024]; + char stub[1024]; + FILE *fd; + int rc; + + snprintf(path, sizeof(path), "%s/interfaces", dir); + + if (access(path, R_OK) == 0) { + fd = fopen(path, "r"); + if (!fd) { + perror("Could not open config file"); + return CFG_RET_ERROR; + } + } else { + snprintf(stub, sizeof(stub), "source interfaces.d/*.cnf\n"); + fd = fmemopen(stub, strlen(stub), "r"); + if (!fd) { + perror("Could not open in-memory config"); + return CFG_RET_ERROR; + } + } + + rc = cfg_parse(cfg_ns_get("interfaces"), dir, fd, NULL); + if (rc < 0) { + log_error("Error during reading interfaces configuration from %s", dir); + } + + fclose(fd); + return rc; +} + static int apply_ports(void) { const struct dp_ops *dp = dp_active(); struct unos_port *cur; @@ -120,6 +153,78 @@ static int apply_ports(void) { return 0; } +static int apply_interfaces(void) { + struct unos_iface *cur; + for (cur = ifaces_list(); cur; cur = cur->next) { + if (!cur->auto_flag) continue; + // For now, use ipc's handle logic via system ip -- will be replaced with netlink + // We call the same helper as ipc ifup would, but directly + char cmd[512]; + // pre-up hook + if (cur->pre_up) { + log_info("apply_interfaces: pre-up %s: %s", cur->name, cur->pre_up); + if (system(cur->pre_up) != 0) { + log_warn("pre-up for %s failed", cur->name); + continue; + } + } + // VLAN creation if needed (dot notation) + if (cur->vlan_id >= 0) { + const char *raw = cur->vlan_raw_device; + if (!raw) { + // Infer from dot notation + char *dot = strrchr(cur->name, '.'); + if (dot) { + size_t len = dot - cur->name; + char *tmp = malloc(len+1); + strncpy(tmp, cur->name, len); + tmp[len] = '\0'; + raw = tmp; + snprintf(cmd, sizeof(cmd), "/sbin/ip link add link %s name %s type vlan id %d 2>&1 || true", raw, cur->name, cur->vlan_id); + log_info("apply_interfaces: %s", cmd); + system(cmd); + free(tmp); + } + } else { + snprintf(cmd, sizeof(cmd), "/sbin/ip link add link %s name %s type vlan id %d 2>&1 || true", raw, cur->name, cur->vlan_id); + log_info("apply_interfaces: %s", cmd); + system(cmd); + } + } + // Bring link up + snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s up 2>&1 || true", cur->name); + log_info("apply_interfaces: %s", cmd); + system(cmd); + // Add addresses (v4 and v6) + struct iface_addr *a; + for (a = cur->addrs; a; a = a->next) { + snprintf(cmd, sizeof(cmd), "/sbin/ip addr add %s dev %s 2>&1 || true", a->address, cur->name); + log_info("apply_interfaces: %s", cmd); + system(cmd); + } + if (cur->gateway) { + snprintf(cmd, sizeof(cmd), "/sbin/ip route add default via %s 2>&1 || true", cur->gateway); + log_info("apply_interfaces: %s", cmd); + system(cmd); + } + if (cur->mtu) { + snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s mtu %d 2>&1 || true", cur->name, cur->mtu); + log_info("apply_interfaces: %s", cmd); + system(cmd); + } + if (cur->hwaddress) { + snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s address %s 2>&1 || true", cur->name, cur->hwaddress); + log_info("apply_interfaces: %s", cmd); + system(cmd); + } + if (cur->post_up) { + log_info("apply_interfaces: post-up %s: %s", cur->name, cur->post_up); + system(cur->post_up); + } + } + return 0; +} + // Atomic publish: write tmp + rename so frr never observes a half file. static int ready_publish(const char *path) { char tmp[1024]; @@ -148,7 +253,7 @@ static void ready_remove(const char *path) { } int main(int argc, const char **argv) { - char *config_dir = "/etc/unos"; + char *config_dir = "/etc/network"; char *dataplane = NULL; char *loglevel = "info"; char *logfile_path = NULL; @@ -170,6 +275,8 @@ int main(int argc, const char **argv) { // Initialize components ports_register(); + ifaces_register(); + ipc_register(); struct argparse argparse; argparse_init(&argparse, options, usage, 0); @@ -231,6 +338,17 @@ int main(int argc, const char **argv) { if (load_ports(config_dir) < 0) { return 1; } + if (load_interfaces(config_dir) < 0) { + return 1; + } + { + int n = 0; + for (struct unos_iface *c = ifaces_list(); c; c = c->next) n++; + log_info("main: loaded %d interfaces from %s", n, config_dir); + for (struct unos_iface *c = ifaces_list(); c; c = c->next) { + log_info("main: iface %s method %s auto %d addrs %s", c->name, c->method ? c->method : "(null)", c->auto_flag, c->addrs ? c->addrs->address : "(none)"); + } + } // Startup order is load-bearing: ports -> backend select -> init -> // port_apply -> nl_open -> initial resync -> ready -> event loop. frr must @@ -250,29 +368,44 @@ int main(int argc, const char **argv) { return 1; } + if (ipc_init() != 0) { + log_warn("ipc: init failed, ifup/ifquery will not be available"); + } + apply_ports(); + // Interfaces are applied via netlink/system ip after ports, before resync + // so that the subsequent nl_resync sees them. nl_fd = nl_open(); if (nl_fd < 0) { dp_fini(); ports_free(); + ifaces_free(); + ipc_fini(); return 1; } + // Apply auto interfaces (needs netlink to be open for link creation, but we use system ip for now) + apply_interfaces(); + // Fail loud: never publish readiness on partial state; runit retries. if (nl_resync(nl_fd) != 0) { log_fatal("netlink: initial resync failed"); close(nl_fd); + ipc_fini(); dp_fini(); ports_free(); + ifaces_free(); return 1; } if (ready_file && *ready_file) { if (ready_publish(ready_file) != 0) { close(nl_fd); + ipc_fini(); dp_fini(); ports_free(); + ifaces_free(); return 1; } log_info("ready: published %s", ready_file); @@ -280,10 +413,12 @@ int main(int argc, const char **argv) { rc = nl_run(nl_fd, resync_interval); close(nl_fd); + ipc_fini(); ready_remove(ready_file); dp_fini(); ports_free(); + ifaces_free(); if (log_file) fclose(log_file); free(log_path); diff --git a/src/netlink/netlink.c b/src/netlink/netlink.c @@ -28,6 +28,7 @@ #include "filter.h" #include "netlink.h" +#include "../ipc.h" #define NL_BUFSZ (64 * 1024) #define NL_MAX_NH 16 @@ -606,11 +607,21 @@ int nl_run(int fd, int resync_interval_s) { clock_gettime(CLOCK_MONOTONIC, &last_resync); while (!nl_stop) { - struct pollfd pfd = { .fd = fd, .events = POLLIN }; + struct pollfd pfds[2]; + int nfds = 1; struct timespec now; long wait_ms = 30000; int pr; + pfds[0].fd = fd; + pfds[0].events = POLLIN; + int ipcfd = ipc_fd(); + if (ipcfd >= 0) { + pfds[1].fd = ipcfd; + pfds[1].events = POLLIN; + nfds = 2; + } + clock_gettime(CLOCK_MONOTONIC, &now); if (resync_interval_s > 0) { long remain = resync_interval_s * 1000L - elapsed_ms(&last_resync, &now); @@ -627,7 +638,7 @@ int nl_run(int fd, int resync_interval_s) { if (remain < wait_ms) wait_ms = remain; } - pr = poll(&pfd, 1, (int)wait_ms); + pr = poll(pfds, nfds, (int)wait_ms); if (pr < 0) { if (errno == EINTR) continue; log_error("netlink: poll failed: %s", strerror(errno)); @@ -635,6 +646,11 @@ int nl_run(int fd, int resync_interval_s) { } if (!pr) continue; + if (nfds > 1 && (pfds[1].revents & POLLIN)) { + ipc_handle(); + } + if (!(pfds[0].revents & POLLIN)) continue; + for (;;) { struct nlmsghdr *h; struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) }; diff --git a/src/util/config.c b/src/util/config.c @@ -8,23 +8,87 @@ #include "config.h" +#define CFG_MAX_NS 8 #define CFG_MAX_REG 256 -static int handler_count = 0; -static const char *handler_name[CFG_MAX_REG]; -static cfg_directive_fn handler_fn[CFG_MAX_REG]; +struct cfg_ns { + char name[32]; + int count; + const char *h_name[CFG_MAX_REG]; + cfg_directive_fn h_fn[CFG_MAX_REG]; +}; -void cfg_register_directive(const char *name, cfg_directive_fn fn) { - if (handler_count >= CFG_MAX_REG) { - fprintf(stderr, "config: handler table full, dropping `%s`\n", name); +static struct cfg_ns ns_tab[CFG_MAX_NS]; +static int ns_count = 0; + +static struct cnf_directive *cfg_handle_source_directory(FILE *fd, struct cnf_directive *dir, void *user); + +struct cfg_ns *cfg_ns_get(const char *name) { + int i; + for (i = 0; i < ns_count; i++) { + if (!strcmp(ns_tab[i].name, name)) return &ns_tab[i]; + } + if (ns_count >= CFG_MAX_NS) { + fprintf(stderr, "config: namespace table full, dropping `%s`\n", name); + return NULL; + } + snprintf(ns_tab[ns_count].name, sizeof(ns_tab[ns_count].name), "%s", name); + ns_tab[ns_count].count = 0; + struct cfg_ns *ns = &ns_tab[ns_count++]; + // Every namespace supports source-directory -> source <dir>/* expansion + cfg_register_directive(ns, "source-directory", cfg_handle_source_directory); + return ns; +} + +void cfg_register_directive(struct cfg_ns *ns, const char *name, cfg_directive_fn fn) { + if (!ns) return; + if (ns->count >= CFG_MAX_REG) { + fprintf(stderr, "config: handler table full for ns `%s`, dropping `%s`\n", ns->name, name); return; } - handler_name[handler_count] = name; - handler_fn [handler_count] = fn; - handler_count++; + ns->h_name[ns->count] = name; + ns->h_fn[ns->count] = fn; + ns->count++; } -int cfg_parse(const char *wd, FILE *fd, void *user) { +// source-directory handler: transforms `source-directory <dir>` into `source <dir>/*` +static struct cnf_directive *cfg_handle_source_directory(FILE *fd, struct cnf_directive *dir, void *user) { + (void)fd; (void)user; + struct cnf_directive *ndir = NULL; + char *pattern = NULL; + + if (dir->argc != 1) { + fprintf(stderr, "`source-directory` directive accepts only 1 argument\n"); + // Return as unknown to let caller handle error? For now treat as error via exit-style? + // Keep consistent with other handlers: print and return dir as unknown for outer to error. + // But we want to synthesize source, so we need at least 1 arg. + // If argc !=1, free and return NULL to signal error via cfg_parse's unknown path? + cnf_directive_free(dir); + return NULL; + } + + // Synthesize a `source <dir>/*` directive + // Allocate new directive manually + ndir = calloc(1, sizeof(*ndir)); + ndir->name = strdup("source"); + ndir->argc = 1; + ndir->argv = calloc(1, sizeof(char*)); + // Append /* if not already ending with /* + if (dir->argv[0][strlen(dir->argv[0])-1] == '/') { + pattern = malloc(strlen(dir->argv[0]) + 2); // "/" + "*" + sprintf(pattern, "%s*", dir->argv[0]); + } else { + // if dir is like "/etc/network/interfaces.d", we want "/etc/network/interfaces.d/*" + pattern = malloc(strlen(dir->argv[0]) + 3); + sprintf(pattern, "%s/*", dir->argv[0]); + } + ndir->argv[0] = pattern; + // we own pattern, will be freed via cnf_directive_free later + cnf_directive_free(dir); + return ndir; +} + +int cfg_parse(struct cfg_ns *ns, const char *wd, FILE *fd, void *user) { struct cnf_directive *dir = NULL; glob_t globbuf; int globflags; @@ -33,63 +97,80 @@ int cfg_parse(const char *wd, FILE *fd, void *user) { char *strtmp = NULL; FILE *nfd; + if (!ns) { + fprintf(stderr, "config: NULL namespace\n"); + return CFG_RET_ERROR; + } + for(;;) { if (dir) cnf_directive_free(dir); dir = cnf_directive_read(fd); if (!dir) break; // EOF = done -cfg_parse_reparse: + cfg_parse_reparse: - // Core handlers - if (!strcasecmp("include", dir->name)) { + // Core handlers: source and source-directory (source-directory is via handler table, but we also handle source here as built-in) + if (!strcasecmp("source", dir->name)) { globflags = GLOB_ERR; for ( i = 0 ; i < (int)dir->argc ; i++ ) { if (dir->argv[i][0] == '/') { strtmp = calloc(strlen(dir->argv[i])+1, 1); strcpy(strtmp, dir->argv[i]); } else { - strtmp = malloc(snprintf(NULL, 0, "%s/%s", wd, dir->argv[i])+1); - sprintf(strtmp, "%s/%s", wd, dir->argv[i]); + // wd may be NULL for IPC (no filesystem context), then treat as absolute or fail + if (wd) { + strtmp = malloc(snprintf(NULL, 0, "%s/%s", wd, dir->argv[i])+1); + sprintf(strtmp, "%s/%s", wd, dir->argv[i]); + } else { + strtmp = calloc(strlen(dir->argv[i])+1, 1); + strcpy(strtmp, dir->argv[i]); + } } glob(strtmp, globflags, NULL, &globbuf); free(strtmp); globflags = GLOB_ERR | GLOB_APPEND; } - for( i = 0 ; i < (int)globbuf.gl_pathc ; i++ ) { + for ( i = 0 ; i < (int)globbuf.gl_pathc ; i++ ) { strtmp = calloc(strlen(globbuf.gl_pathv[i])+1, 1); strcpy(strtmp, globbuf.gl_pathv[i]); - dirname(strtmp); + char *dname = strdup(strtmp); + dirname(dname); + // Use wd as dirname for nested relative sources? Keep dname as wd for nested + // But for simplicity, use dname nfd = fopen(globbuf.gl_pathv[i], "r"); if (!nfd) { perror("Could not open config file for reading"); free(strtmp); + free(dname); globfree(&globbuf); return CFG_RET_ERROR; } - if (cfg_parse(strtmp, nfd, user) < 0) { + if (cfg_parse(ns, dname, nfd, user) < 0) { fprintf(stderr, "Error during reading configuration from %s\n", globbuf.gl_pathv[i]); fclose(nfd); free(strtmp); + free(dname); globfree(&globbuf); return CFG_RET_ERROR; } fclose(nfd); free(strtmp); + free(dname); } globfree(&globbuf); continue; } // Dynamic handlers - for(i = 0 ; i < handler_count ; i++) { - if (!strcasecmp(handler_name[i], dir->name)) { - dir = handler_fn[i](fd, dir, user); + for(i = 0 ; i < ns->count ; i++) { + if (!strcasecmp(ns->h_name[i], dir->name)) { + dir = ns->h_fn[i](fd, dir, user); if (dir) goto cfg_parse_reparse; break; } } - if (i == handler_count) { + if (i == ns->count) { // Here = not found fprintf(stderr, "Unknown directive: %s\n", dir->name); return CFG_RET_ERROR; @@ -101,3 +182,5 @@ cfg_parse_reparse: return CFG_RET_OK; } + + diff --git a/src/util/config.h b/src/util/config.h @@ -13,9 +13,14 @@ // re-dispatch it. Returning NULL means end-of-input was reached. typedef struct cnf_directive * (*cfg_directive_fn)(FILE *fd, struct cnf_directive *dir, void *user); -void cfg_register_directive(const char *name, cfg_directive_fn fn); +struct cfg_ns; -// `wd` is the directory relative `include` patterns resolve against. -int cfg_parse(const char *wd, FILE *fd, void *user); +// Create+get interned namespace (idempotent) +struct cfg_ns *cfg_ns_get(const char *name); + +void cfg_register_directive(struct cfg_ns *ns, const char *name, cfg_directive_fn fn); + +// `wd` is the directory relative `source` patterns resolve against. +int cfg_parse(struct cfg_ns *ns, const char *wd, FILE *fd, void *user); #endif // __UNOS_UTIL_CONFIG_H__ diff --git a/target/common/Makefile b/target/common/Makefile @@ -4,8 +4,8 @@ BIN=unosd SRC:= SRC+=$(wildcard src/*.c) -SRC+=$(wildcard src/*/*.c) -SRC+=$(wildcard src/*/*/*.c) +SRC+=$(filter-out src/cli/%,$(wildcard src/*/*.c)) +SRC+=$(filter-out src/cli/%,$(wildcard src/*/*/*.c)) CFLAGS?=-Wall -Wextra -O2 LDFLAGS?=