mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
* feat: run a guest agent in every session for commands, focus and the Steam client's DevTools * feat: give the agent bridge game, input, UI and experiment commands behind a Setup toggle * feat: droiddeckctl commands for the new bridge, a scenario runner and contract tests * feat: run cleanup steps after every scenario run so a failure cannot leak into the next * feat: restart a session that died between scenario runs with start --reuse * fix: give droiddeckctl stop its own, longer wait for the session's logs * fix: harden debugging tools access and output
367 lines
12 KiB
C
367 lines
12 KiB
C
/*
|
|
* Loopback socket ownership for Steam's IPC peer check.
|
|
*
|
|
* Steam validates a websocket peer by running lsof to find which process owns a 127.0.0.1 port.
|
|
* The Android sandbox denies both /proc/net/tcp and sock_diag, so no tool can answer. Every Steam
|
|
* process is preloaded with this library, so each records the ports it binds and connects, and the
|
|
* lsof Steam spawns is answered from that shared registry instead of being run.
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <arpa/inet.h>
|
|
#include <spawn.h>
|
|
#include <dlfcn.h>
|
|
#include <fcntl.h>
|
|
#include <linux/netlink.h>
|
|
#include <netinet/in.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
static const char *net_dir(void) {
|
|
static char dir[256];
|
|
if (!dir[0]) {
|
|
const char *base = getenv("BL_SYSV_DIR");
|
|
snprintf(dir, sizeof(dir), "%s/net", base && *base ? base : "/dev/shm/wnsysv");
|
|
char parent[256];
|
|
snprintf(parent, sizeof(parent), "%s", dir);
|
|
char *slash = strrchr(parent, '/');
|
|
if (slash) {
|
|
*slash = '\0';
|
|
mkdir(parent, 0777);
|
|
}
|
|
mkdir(dir, 0777);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
static int loopback_family(const struct sockaddr *address, socklen_t length) {
|
|
if (address && address->sa_family == AF_INET && length >= sizeof(struct sockaddr_in)) {
|
|
return ((const struct sockaddr_in *)address)->sin_addr.s_addr == htonl(INADDR_LOOPBACK) ? AF_INET : 0;
|
|
}
|
|
if (address && address->sa_family == AF_INET6 && length >= sizeof(struct sockaddr_in6)) {
|
|
const struct in6_addr *ip = &((const struct sockaddr_in6 *)address)->sin6_addr;
|
|
if (IN6_IS_ADDR_LOOPBACK(ip)) return AF_INET6;
|
|
if (IN6_IS_ADDR_V4MAPPED(ip) && ip->s6_addr[12] == 127 && ip->s6_addr[13] == 0 &&
|
|
ip->s6_addr[14] == 0 && ip->s6_addr[15] == 1) return AF_INET;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Record that this process owns a loopback socket on its local port; peer is the far end's port,
|
|
* 0 for a listening socket. */
|
|
static void record_port(int fd, unsigned peer) {
|
|
struct sockaddr_storage ss;
|
|
socklen_t len = sizeof(ss);
|
|
if (getsockname(fd, (struct sockaddr *)&ss, &len) != 0) {
|
|
return;
|
|
}
|
|
int family = loopback_family((struct sockaddr *)&ss, len);
|
|
if (!family) return;
|
|
unsigned port;
|
|
if (ss.ss_family == AF_INET) {
|
|
struct sockaddr_in *in = (struct sockaddr_in *)&ss;
|
|
port = ntohs(in->sin_port);
|
|
} else if (ss.ss_family == AF_INET6) {
|
|
port = ntohs(((struct sockaddr_in6 *)&ss)->sin6_port);
|
|
} else {
|
|
return;
|
|
}
|
|
if (port == 0) {
|
|
return;
|
|
}
|
|
struct stat socket_stat;
|
|
if (fstat(fd, &socket_stat) != 0) return;
|
|
char path[300];
|
|
snprintf(path, sizeof(path), "%s/p%u", net_dir(), port);
|
|
char staged[360];
|
|
snprintf(staged, sizeof(staged), "%s.tmp.%d.%d", path, (int)getpid(), fd);
|
|
char line[128];
|
|
int n = snprintf(line, sizeof(line), "%d %d %d %u %d %llu %d\n", (int)getpid(), (int)getppid(),
|
|
(int)getuid(), peer, fd, (unsigned long long)socket_stat.st_ino, family);
|
|
int out = open(staged, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600);
|
|
if (out >= 0) {
|
|
int complete = write(out, line, n) == n;
|
|
close(out);
|
|
if (complete) rename(staged, path);
|
|
unlink(staged);
|
|
}
|
|
}
|
|
|
|
static unsigned addr_port(const struct sockaddr *addr, socklen_t len) {
|
|
if (addr && addr->sa_family == AF_INET && len >= sizeof(struct sockaddr_in)) {
|
|
return ntohs(((const struct sockaddr_in *)addr)->sin_port);
|
|
}
|
|
if (addr && addr->sa_family == AF_INET6 && len >= sizeof(struct sockaddr_in6)) {
|
|
return ntohs(((const struct sockaddr_in6 *)addr)->sin6_port);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static unsigned peer_port(int fd) {
|
|
struct sockaddr_storage ps;
|
|
socklen_t plen = sizeof(ps);
|
|
if (getpeername(fd, (struct sockaddr *)&ps, &plen) != 0) {
|
|
return 0;
|
|
}
|
|
return addr_port((struct sockaddr *)&ps, plen);
|
|
}
|
|
|
|
typedef int (*bind_fn)(int, const struct sockaddr *, socklen_t);
|
|
typedef int (*listen_fn)(int, int);
|
|
typedef int (*connect_fn)(int, const struct sockaddr *, socklen_t);
|
|
typedef int (*accept4_fn)(int, struct sockaddr *, socklen_t *, int);
|
|
static unsigned guarded_port(void);
|
|
|
|
/* udevmon.c: a stand-in for the netlink socket the sandbox refuses, bound here without the kernel. */
|
|
__attribute__((visibility("hidden"))) int bl_udevmon_stand_in(int fd);
|
|
|
|
int bind(int fd, const struct sockaddr *addr, socklen_t len) {
|
|
static bind_fn real;
|
|
if (!real) real = (bind_fn)dlsym(RTLD_NEXT, "bind");
|
|
if (bl_udevmon_stand_in(fd)) {
|
|
if (addr == NULL || len < sizeof(struct sockaddr_nl) || addr->sa_family != AF_NETLINK) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
struct sockaddr_storage local;
|
|
unsigned guarded = guarded_port();
|
|
if (guarded && addr_port(addr, len) == guarded && len <= sizeof(local)) {
|
|
memcpy(&local, addr, len);
|
|
if (addr->sa_family == AF_INET) {
|
|
((struct sockaddr_in *)&local)->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
addr = (struct sockaddr *)&local;
|
|
} else if (addr->sa_family == AF_INET6) {
|
|
((struct sockaddr_in6 *)&local)->sin6_addr = in6addr_loopback;
|
|
addr = (struct sockaddr *)&local;
|
|
}
|
|
}
|
|
int ret = real(fd, addr, len);
|
|
if (ret == 0) record_port(fd, 0);
|
|
return ret;
|
|
}
|
|
|
|
int listen(int fd, int backlog) {
|
|
static listen_fn real;
|
|
if (!real) real = (listen_fn)dlsym(RTLD_NEXT, "listen");
|
|
int ret = real(fd, backlog);
|
|
if (ret == 0) record_port(fd, 0);
|
|
return ret;
|
|
}
|
|
|
|
static unsigned local_port(int fd) {
|
|
struct sockaddr_storage ss;
|
|
socklen_t len = sizeof(ss);
|
|
if (getsockname(fd, (struct sockaddr *)&ss, &len) != 0) {
|
|
return 0;
|
|
}
|
|
return addr_port((struct sockaddr *)&ss, len);
|
|
}
|
|
|
|
static void bind_before_loopback_connect(int fd, const struct sockaddr *addr, socklen_t len) {
|
|
if (!loopback_family(addr, len)) return;
|
|
struct sockaddr_storage ss;
|
|
socklen_t slen = sizeof(ss);
|
|
if (getsockname(fd, (struct sockaddr *)&ss, &slen) != 0 || ss.ss_family != addr->sa_family ||
|
|
addr_port((struct sockaddr *)&ss, slen) != 0) {
|
|
return;
|
|
}
|
|
struct sockaddr_storage local;
|
|
if (len > sizeof(local)) return;
|
|
memcpy(&local, addr, len);
|
|
if (addr->sa_family == AF_INET) ((struct sockaddr_in *)&local)->sin_port = 0;
|
|
else ((struct sockaddr_in6 *)&local)->sin6_port = 0;
|
|
int saved = errno;
|
|
bind(fd, (struct sockaddr *)&local, len);
|
|
errno = saved;
|
|
}
|
|
|
|
static unsigned guarded_port(void) {
|
|
static int port = -1;
|
|
if (port < 0) {
|
|
const char *value = getenv("BL_CDP_GUARD");
|
|
port = value && *value ? atoi(value) : 0;
|
|
if (port < 0 || port > 65535) port = 0;
|
|
}
|
|
return (unsigned)port;
|
|
}
|
|
|
|
static int guest_peer(int fd) {
|
|
struct sockaddr_storage address;
|
|
socklen_t length = sizeof(address);
|
|
if (getpeername(fd, (struct sockaddr *)&address, &length) != 0) return 0;
|
|
int family = loopback_family((struct sockaddr *)&address, length);
|
|
if (!family) return 0;
|
|
unsigned peer = peer_port(fd);
|
|
if (peer == 0) {
|
|
return 0;
|
|
}
|
|
char path[300];
|
|
snprintf(path, sizeof(path), "%s/p%u", net_dir(), peer);
|
|
struct stat st;
|
|
if (stat(path, &st) != 0 || time(NULL) - st.st_mtime > 30) {
|
|
return 0;
|
|
}
|
|
int in = open(path, O_RDONLY);
|
|
if (in < 0) {
|
|
return 0;
|
|
}
|
|
char buf[128];
|
|
int n = read(in, buf, sizeof(buf) - 1);
|
|
close(in);
|
|
if (n <= 0) {
|
|
return 0;
|
|
}
|
|
buf[n] = '\0';
|
|
int pid, parent, uid, descriptor, recorded_family;
|
|
unsigned destination;
|
|
unsigned long long inode;
|
|
if (sscanf(buf, "%d %d %d %u %d %llu %d", &pid, &parent, &uid, &destination, &descriptor,
|
|
&inode, &recorded_family) != 7 || pid <= 0 || descriptor < 0 || destination != guarded_port() ||
|
|
recorded_family != family) return 0;
|
|
snprintf(path, sizeof(path), "/proc/%d/fd/%d", pid, descriptor);
|
|
return stat(path, &st) == 0 && S_ISSOCK(st.st_mode) && (unsigned long long)st.st_ino == inode;
|
|
}
|
|
|
|
int connect(int fd, const struct sockaddr *addr, socklen_t len) {
|
|
static connect_fn real;
|
|
if (!real) real = (connect_fn)dlsym(RTLD_NEXT, "connect");
|
|
bind_before_loopback_connect(fd, addr, len);
|
|
unsigned peer = loopback_family(addr, len) ? addr_port(addr, len) : 0;
|
|
if (peer) record_port(fd, peer);
|
|
int ret = real(fd, addr, len);
|
|
if (ret == 0 || errno == EINPROGRESS) {
|
|
/* The caller goes on to test for EINPROGRESS, which the bookkeeping must not disturb. */
|
|
int saved = errno;
|
|
record_port(fd, peer);
|
|
errno = saved;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int accept4(int fd, struct sockaddr *addr, socklen_t *len, int flags) {
|
|
static accept4_fn real;
|
|
if (!real) real = (accept4_fn)dlsym(RTLD_NEXT, "accept4");
|
|
for (;;) {
|
|
int ret = real(fd, addr, len, flags);
|
|
if (ret < 0) return ret;
|
|
unsigned guarded = guarded_port();
|
|
if (guarded && local_port(ret) == guarded && !guest_peer(ret)) {
|
|
close(ret);
|
|
continue;
|
|
}
|
|
record_port(ret, peer_port(ret));
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
int accept(int fd, struct sockaddr *addr, socklen_t *len) {
|
|
return accept4(fd, addr, len, 0);
|
|
}
|
|
|
|
/* lsof would read the denied /proc/net/tcp; answer its -i TCP@host:port query from the registry
|
|
* instead of executing it. Recognised only when invoked as lsof with that query. */
|
|
static int maybe_answer_lsof(const char *path, char *const argv[]) {
|
|
const char *base = strrchr(path, '/');
|
|
base = base ? base + 1 : path;
|
|
if (strcmp(base, "lsof") != 0) {
|
|
return 0;
|
|
}
|
|
const char *port_str = NULL;
|
|
for (int i = 0; argv[i]; i++) {
|
|
const char *at = strstr(argv[i], "TCP@");
|
|
if (at) {
|
|
const char *colon = strrchr(at, ':');
|
|
if (colon) port_str = colon + 1;
|
|
}
|
|
}
|
|
if (!port_str) {
|
|
return 0;
|
|
}
|
|
char path_buf[300];
|
|
snprintf(path_buf, sizeof(path_buf), "%s/p%u", net_dir(), (unsigned)atoi(port_str));
|
|
int in = open(path_buf, O_RDONLY);
|
|
if (in < 0) {
|
|
_exit(1); /* lsof exit code for "nothing found" */
|
|
}
|
|
char buf[64];
|
|
int n = read(in, buf, sizeof(buf) - 1);
|
|
close(in);
|
|
if (n <= 0) {
|
|
_exit(1);
|
|
}
|
|
buf[n] = '\0';
|
|
int pid = 0, ppid = 0, uid = 0;
|
|
unsigned peer = 0;
|
|
sscanf(buf, "%d %d %d %u", &pid, &ppid, &uid, &peer);
|
|
/* Steam splits the address on "->" and wants exactly two halves, taking the port it asked
|
|
* about from the left one; only a listening socket has no far end. */
|
|
char out[160];
|
|
int m = peer ? snprintf(out, sizeof(out), "p%d\nR%d\nu%d\nn127.0.0.1:%u->127.0.0.1:%u\n",
|
|
pid, ppid, uid, (unsigned)atoi(port_str), peer)
|
|
: snprintf(out, sizeof(out), "p%d\nR%d\nu%d\nn127.0.0.1:%u\n",
|
|
pid, ppid, uid, (unsigned)atoi(port_str));
|
|
if (write(1, out, m) != m) { /* best effort */ }
|
|
_exit(0);
|
|
}
|
|
|
|
int execve(const char *path, char *const argv[], char *const envp[]) {
|
|
static int (*real)(const char *, char *const[], char *const[]);
|
|
if (!real) real = (int (*)(const char *, char *const[], char *const[]))dlsym(RTLD_NEXT, "execve");
|
|
maybe_answer_lsof(path, argv);
|
|
return real(path, argv, envp);
|
|
}
|
|
|
|
/* popen(3) and posix_spawn(3) reach lsof without going through the execve symbol above. The
|
|
* check is cheap, so a spawned lsof is answered here too by handling it in a short-lived child. */
|
|
static int answer_lsof_via_spawn(const char *path, char *const argv[], pid_t *pid) {
|
|
pid_t child = fork();
|
|
if (child < 0) {
|
|
return errno;
|
|
}
|
|
if (child == 0) {
|
|
maybe_answer_lsof(path, argv);
|
|
_exit(127);
|
|
}
|
|
if (pid) {
|
|
*pid = child;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int execvp(const char *file, char *const argv[]) {
|
|
static int (*real)(const char *, char *const[]);
|
|
if (!real) real = (int (*)(const char *, char *const[]))dlsym(RTLD_NEXT, "execvp");
|
|
maybe_answer_lsof(file, argv);
|
|
return real(file, argv);
|
|
}
|
|
|
|
int execv(const char *path, char *const argv[]) {
|
|
static int (*real)(const char *, char *const[]);
|
|
if (!real) real = (int (*)(const char *, char *const[]))dlsym(RTLD_NEXT, "execv");
|
|
maybe_answer_lsof(path, argv);
|
|
return real(path, argv);
|
|
}
|
|
|
|
int posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *fa,
|
|
const posix_spawnattr_t *attr, char *const argv[], char *const envp[]) {
|
|
const char *base = strrchr(path, '/');
|
|
base = base ? base + 1 : path;
|
|
if (strcmp(base, "lsof") == 0 && !fa) {
|
|
return answer_lsof_via_spawn(path, argv, pid);
|
|
}
|
|
static int (*real)(pid_t *, const char *, const posix_spawn_file_actions_t *,
|
|
const posix_spawnattr_t *, char *const[], char *const[]);
|
|
if (!real) real = (int (*)(pid_t *, const char *, const posix_spawn_file_actions_t *,
|
|
const posix_spawnattr_t *, char *const[], char *const[]))dlsym(RTLD_NEXT, "posix_spawn");
|
|
return real(pid, path, fa, attr, argv, envp);
|
|
}
|