Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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/; \
Expand Down Expand Up @@ -104,6 +114,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"
Expand Down
9 changes: 9 additions & 0 deletions src/daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
*/

#include "droidspace.h"
#ifdef DS_ENABLE_SOCKETD_BACKEND
#include "socketd_bridge.h"
#endif

#include <arpa/inet.h>
#include <grp.h>
#include <poll.h>
Expand Down Expand Up @@ -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];
Expand Down
233 changes: 233 additions & 0 deletions src/socketd_bridge.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
* Droidspaces v6 - private backend bridge for droidspaces-socketd
*
* Copyright (C) 2026 ravindu644 <droidcasts@protonmail.com>
* 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);
}
19 changes: 19 additions & 0 deletions src/socketd_bridge.h
Original file line number Diff line number Diff line change
@@ -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 */
86 changes: 86 additions & 0 deletions src/socketd_protocol.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

#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 */
Loading