From 932db91ff866633e1b0d366dba89c1abc595f421 Mon Sep 17 00:00:00 2001 From: Yoshinobu Date Date: Wed, 13 May 2026 22:00:00 +0900 Subject: [PATCH 1/2] build: gate optional socketd backend bridge The forthcoming C++ droidspaces-socketd daemon needs a narrow private control path into the existing privileged Droidspaces daemon. Keep the native Droidspaces build unchanged by default and compile that bridge only when ENABLE_SOCKETD_BACKEND=1 is requested. This keeps the current static-musl runtime layout intact for existing users while reserving an explicit opt-in build path for the Portainer/Podman compatibility work. --- Makefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Makefile b/Makefile index 01f0f929..287186df 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,11 @@ NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || ech # Verbose control - V=1 shows full commands, V=0 (default) shows kernel-style short logs V ?= 0 + +# Optional private control bridge for the external C++ droidspaces-socketd. +# Keep this off by default so the stock static Droidspaces binary stays unchanged. +ENABLE_SOCKETD_BACKEND ?= 0 + ifeq ($(V),1) Q = msg_cc = @@ -58,6 +63,11 @@ CFLAGS += -fstack-protector-strong LDFLAGS = -static -no-pie -flto=auto -pthread LIBS = -lutil +ifeq ($(ENABLE_SOCKETD_BACKEND),1) + CFLAGS += -DDS_ENABLE_SOCKETD_BACKEND=1 + SRCS += $(SRC_DIR)/socketd_bridge.c +endif + # Auto-detect architecture from compiler ARCH := $(shell $(CC) -dumpmachine 2>/dev/null | cut -d'-' -f1 | \ sed 's/x86_64/x86_64/; s/aarch64/aarch64/; s/i686/x86/; \ @@ -103,6 +113,8 @@ help: @echo "" @echo "Options:" @echo " V=1 - Show full compiler commands" + @echo " ENABLE_SOCKETD_BACKEND=1" + @echo " - Compile the private droidspaces-socketd backend bridge" @echo "" @echo "Other:" @echo " make clean - Remove build artifacts" From 1a82bbd8c26caf2dd20db2256a6cde96aab38cb5 Mon Sep 17 00:00:00 2001 From: Yoshinobu Date Date: Wed, 13 May 2026 22:05:00 +0900 Subject: [PATCH 2/2] daemon: add optional private socketd backend bridge Introduce the first native-C seam for the future C++ droidspaces-socketd daemon. The bridge is intentionally: * private, using a separate Linux abstract AF_UNIX socket; * opt-in, compiled only with DS_ENABLE_SOCKETD_BACKEND; * C-only, keeping C++ out of the existing runtime; * tiny, with a versioned framed protocol and root/same-UID peer gate; * dormant with respect to the existing CLI/app daemon protocol. The initial bridge implements only protocol-level PING and CAPABILITIES requests. Container listing, inspection, and lifecycle RPCs can be added on this stable framing without changing the public Docker-compatible API daemon. --- src/daemon.c | 9 ++ src/socketd_bridge.c | 233 +++++++++++++++++++++++++++++++++++++++++ src/socketd_bridge.h | 19 ++++ src/socketd_protocol.h | 86 +++++++++++++++ 4 files changed, 347 insertions(+) create mode 100644 src/socketd_bridge.c create mode 100644 src/socketd_bridge.h create mode 100644 src/socketd_protocol.h diff --git a/src/daemon.c b/src/daemon.c index 7bdaf0ca..bb4813a8 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -22,6 +22,10 @@ */ #include "droidspace.h" +#ifdef DS_ENABLE_SOCKETD_BACKEND +#include "socketd_bridge.h" +#endif + #include #include #include @@ -815,6 +819,11 @@ int ds_daemon_run(int foreground, char **argv) { /* SIGUSR2: app sends this after a live binary swap as an acknowledgment */ signal(SIGUSR2, sigusr2_handler); +#ifdef DS_ENABLE_SOCKETD_BACKEND + if (ds_socketd_bridge_start() < 0) + ds_warn("Failed to start droidspaces-socketd backend bridge: %s", strerror(errno)); +#endif + /* Write PID file so the Android app can signal us */ { char pid_path[PATH_MAX]; diff --git a/src/socketd_bridge.c b/src/socketd_bridge.c new file mode 100644 index 00000000..1a40f0cd --- /dev/null +++ b/src/socketd_bridge.c @@ -0,0 +1,233 @@ +/* + * Droidspaces v6 - private backend bridge for droidspaces-socketd + * + * Copyright (C) 2026 ravindu644 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "droidspace.h" +#include "socketd_bridge.h" +#include "socketd_protocol.h" + +/* + * The public Docker/Podman-compatible socket belongs to the external C++ + * droidspaces-socketd daemon. This bridge is deliberately narrower: it is a + * local, private, privileged control path that will eventually expose the + * Droidspaces-native operations needed by that compatibility daemon. + */ + +static int socketd_read_exact(int fd, void *buf, size_t len) { + uint8_t *p = (uint8_t *)buf; + while (len > 0) { + ssize_t r = read(fd, p, len); + if (r == 0) + return -1; + if (r < 0) { + if (errno == EINTR) + continue; + return -1; + } + p += (size_t)r; + len -= (size_t)r; + } + return 0; +} + +static socklen_t socketd_backend_addr(struct sockaddr_un *addr) { + memset(addr, 0, sizeof(*addr)); + addr->sun_family = AF_UNIX; + + size_t name_len = strlen(DS_SOCKETD_BACKEND_SOCK_NAME); + if (name_len >= sizeof(addr->sun_path)) + name_len = sizeof(addr->sun_path) - 1; + + memcpy(addr->sun_path + 1, DS_SOCKETD_BACKEND_SOCK_NAME, name_len); + return (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + name_len); +} + +static int socketd_send_response(int fd, enum ds_socketd_status status, + const void *payload, uint32_t payload_len) { + struct ds_socketd_response_header hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.magic_be = htonl(DS_SOCKETD_PROTO_MAGIC); + hdr.version_be = htons(DS_SOCKETD_PROTO_VERSION); + hdr.status_be = htons((uint16_t)status); + hdr.payload_len_be = htonl(payload_len); + + if (write_all(fd, &hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) + return -1; + + if (payload_len > 0 && payload != NULL) { + if (write_all(fd, payload, payload_len) != (ssize_t)payload_len) + return -1; + } + return 0; +} + +static int socketd_peer_authorized(int fd) { +#ifdef SO_PEERCRED + struct ucred cred; + socklen_t cred_len = sizeof(cred); + memset(&cred, 0, sizeof(cred)); + + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &cred_len) < 0) + return 0; + if (cred_len != sizeof(cred)) + return 0; + + /* + * The first socketd implementation is expected to run as root beside the + * Droidspaces daemon. Same-EUID access keeps local developer/test launches + * usable on desktop Linux without opening the bridge to arbitrary users. + */ + return cred.uid == 0 || cred.uid == geteuid(); +#else + (void)fd; + return 0; +#endif +} + +static int socketd_discard_payload(int fd, uint32_t len) { + char buf[4096]; + uint32_t remaining = len; + while (remaining > 0) { + size_t chunk = remaining < sizeof(buf) ? (size_t)remaining : sizeof(buf); + if (socketd_read_exact(fd, buf, chunk) < 0) + return -1; + remaining -= (uint32_t)chunk; + } + return 0; +} + +static void socketd_handle_conn(int conn) { + struct ds_socketd_request_header req; + memset(&req, 0, sizeof(req)); + + if (!socketd_peer_authorized(conn)) { + socketd_send_response(conn, DS_SOCKETD_STATUS_FORBIDDEN, NULL, 0); + return; + } + + if (socketd_read_exact(conn, &req, sizeof(req)) < 0) { + socketd_send_response(conn, DS_SOCKETD_STATUS_BAD_REQUEST, NULL, 0); + return; + } + + uint32_t magic = ntohl(req.magic_be); + uint16_t version = ntohs(req.version_be); + uint16_t opcode = ntohs(req.opcode_be); + uint32_t payload_len = ntohl(req.payload_len_be); + + if (magic != DS_SOCKETD_PROTO_MAGIC || version != DS_SOCKETD_PROTO_VERSION) { + socketd_send_response(conn, DS_SOCKETD_STATUS_BAD_REQUEST, NULL, 0); + return; + } + + if (payload_len > DS_SOCKETD_MAX_PAYLOAD) { + socketd_send_response(conn, DS_SOCKETD_STATUS_BAD_REQUEST, NULL, 0); + return; + } + + /* + * The currently implemented opcodes do not consume payloads, but draining + * a well-sized payload keeps the framing strict and future-proofs callers. + */ + if (payload_len > 0 && socketd_discard_payload(conn, payload_len) < 0) { + socketd_send_response(conn, DS_SOCKETD_STATUS_BAD_REQUEST, NULL, 0); + return; + } + + switch ((enum ds_socketd_opcode)opcode) { + case DS_SOCKETD_OP_PING: { + static const char pong[] = "PONG"; + socketd_send_response(conn, DS_SOCKETD_STATUS_OK, pong, + (uint32_t)(sizeof(pong) - 1)); + return; + } + + case DS_SOCKETD_OP_CAPABILITIES: { + uint32_t caps_be = htonl(DS_SOCKETD_CAP_PROTOCOL_V1 | + DS_SOCKETD_CAP_PING | + DS_SOCKETD_CAP_CAPABILITIES); + socketd_send_response(conn, DS_SOCKETD_STATUS_OK, &caps_be, + (uint32_t)sizeof(caps_be)); + return; + } + + case DS_SOCKETD_OP_INFO: + case DS_SOCKETD_OP_LIST_CONTAINERS: + case DS_SOCKETD_OP_INSPECT_CONTAINER: + case DS_SOCKETD_OP_START_CONTAINER: + case DS_SOCKETD_OP_STOP_CONTAINER: + case DS_SOCKETD_OP_RESTART_CONTAINER: + socketd_send_response(conn, DS_SOCKETD_STATUS_UNSUPPORTED, NULL, 0); + return; + + default: + socketd_send_response(conn, DS_SOCKETD_STATUS_UNSUPPORTED, NULL, 0); + return; + } +} + +static int socketd_bridge_loop(void) { + struct sockaddr_un addr; + int server = socket(AF_UNIX, SOCK_STREAM, 0); + if (server < 0) + return -1; + + fcntl(server, F_SETFD, FD_CLOEXEC); + + socklen_t addr_len = socketd_backend_addr(&addr); + if (bind(server, (struct sockaddr *)&addr, addr_len) < 0) { + close(server); + return -1; + } + + if (listen(server, SOMAXCONN) < 0) { + close(server); + return -1; + } + + ds_log("droidspaces-socketd backend bridge listening on @%s", + DS_SOCKETD_BACKEND_SOCK_NAME); + + for (;;) { + int conn = accept(server, NULL, NULL); + if (conn < 0) { + if (errno == EINTR) + continue; + ds_warn("socketd backend accept failed: %s", strerror(errno)); + continue; + } + + fcntl(conn, F_SETFD, FD_CLOEXEC); + socketd_handle_conn(conn); + close(conn); + } + + return 0; +} + +int ds_socketd_bridge_start(void) { + pid_t child = fork(); + if (child < 0) + return -1; + + if (child > 0) { + ds_log("droidspaces-socketd backend bridge process started (PID %d)", child); + return 0; + } + + prctl(PR_SET_NAME, "[ds-socketd]", 0, 0, 0); + signal(SIGPIPE, SIG_IGN); + +#ifdef PR_SET_PDEATHSIG + prctl(PR_SET_PDEATHSIG, SIGTERM); + if (getppid() == 1) + _exit(0); +#endif + + int rc = socketd_bridge_loop(); + ds_error("droidspaces-socketd backend bridge exited: %s", strerror(errno)); + _exit(rc == 0 ? 0 : 1); +} diff --git a/src/socketd_bridge.h b/src/socketd_bridge.h new file mode 100644 index 00000000..192517da --- /dev/null +++ b/src/socketd_bridge.h @@ -0,0 +1,19 @@ +/* + * Droidspaces v6 - private backend bridge for droidspaces-socketd + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef DROIDSPACES_SOCKETD_BRIDGE_H +#define DROIDSPACES_SOCKETD_BRIDGE_H + +#ifdef __cplusplus +extern "C" { +#endif + +int ds_socketd_bridge_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* DROIDSPACES_SOCKETD_BRIDGE_H */ diff --git a/src/socketd_protocol.h b/src/socketd_protocol.h new file mode 100644 index 00000000..6249f71e --- /dev/null +++ b/src/socketd_protocol.h @@ -0,0 +1,86 @@ +/* + * Droidspaces v6 - private socketd backend wire protocol + * SPDX-License-Identifier: GPL-3.0-or-later + * + * This header is deliberately C/C++ compatible. The existing Droidspaces + * daemon consumes it from C; the future droidspaces-socketd daemon will use it + * from C++ without pulling C++ types into the native runtime. + */ + +#ifndef DROIDSPACES_SOCKETD_PROTOCOL_H +#define DROIDSPACES_SOCKETD_PROTOCOL_H + +#include + +#if defined(__GNUC__) +#define DS_SOCKETD_PACKED __attribute__((packed)) +#else +#define DS_SOCKETD_PACKED +#endif + +#define DS_SOCKETD_BACKEND_SOCK_NAME "droidspaces-socketd-backend" + +/* ASCII "DSAP" in network byte order after htonl(). */ +#define DS_SOCKETD_PROTO_MAGIC 0x44534150u +#define DS_SOCKETD_PROTO_VERSION 1u +#define DS_SOCKETD_MAX_PAYLOAD (1024u * 1024u) + +enum ds_socketd_opcode { + DS_SOCKETD_OP_PING = 1, + DS_SOCKETD_OP_CAPABILITIES = 2, + DS_SOCKETD_OP_INFO = 3, + DS_SOCKETD_OP_LIST_CONTAINERS = 4, + DS_SOCKETD_OP_INSPECT_CONTAINER = 5, + DS_SOCKETD_OP_START_CONTAINER = 6, + DS_SOCKETD_OP_STOP_CONTAINER = 7, + DS_SOCKETD_OP_RESTART_CONTAINER = 8, +}; + +enum ds_socketd_status { + DS_SOCKETD_STATUS_OK = 0, + DS_SOCKETD_STATUS_BAD_REQUEST = 1, + DS_SOCKETD_STATUS_UNSUPPORTED = 2, + DS_SOCKETD_STATUS_NOT_FOUND = 3, + DS_SOCKETD_STATUS_INTERNAL_ERROR = 4, + DS_SOCKETD_STATUS_FORBIDDEN = 5, +}; + +enum ds_socketd_capability { + DS_SOCKETD_CAP_PROTOCOL_V1 = 1u << 0, + DS_SOCKETD_CAP_PING = 1u << 1, + DS_SOCKETD_CAP_CAPABILITIES = 1u << 2, + DS_SOCKETD_CAP_INFO = 1u << 3, + DS_SOCKETD_CAP_LIST_CONTAINERS = 1u << 4, + DS_SOCKETD_CAP_INSPECT_CONTAINER = 1u << 5, + DS_SOCKETD_CAP_LIFECYCLE = 1u << 6, +}; + +/* + * Request frame: + * magic_be DS_SOCKETD_PROTO_MAGIC via htonl() + * version_be DS_SOCKETD_PROTO_VERSION via htons() + * opcode_be enum ds_socketd_opcode via htons() + * payload_len_be number of payload bytes via htonl() + */ +struct DS_SOCKETD_PACKED ds_socketd_request_header { + uint32_t magic_be; + uint16_t version_be; + uint16_t opcode_be; + uint32_t payload_len_be; +}; + +/* + * Response frame: + * magic_be DS_SOCKETD_PROTO_MAGIC via htonl() + * version_be DS_SOCKETD_PROTO_VERSION via htons() + * status_be enum ds_socketd_status via htons() + * payload_len_be number of payload bytes via htonl() + */ +struct DS_SOCKETD_PACKED ds_socketd_response_header { + uint32_t magic_be; + uint16_t version_be; + uint16_t status_be; + uint32_t payload_len_be; +}; + +#endif /* DROIDSPACES_SOCKETD_PROTOCOL_H */