Skip to content

Instantly share code, notes, and snippets.

@rrampage
Last active July 17, 2026 06:44
Show Gist options
  • Select an option

  • Save rrampage/92f0eb6bf56d7bb403aff069cc8f1d6b to your computer and use it in GitHub Desktop.

Select an option

Save rrampage/92f0eb6bf56d7bb403aff069cc8f1d6b to your computer and use it in GitHub Desktop.
A userspace sandbox which uses SOCKS proxy to restrict network access (inspired by oniux)
#define _GNU_SOURCE
/*
* sockpuppet.c - single-file Linux sandbox + userspace network broker
*
* Quick build:
* gcc -O2 -g -Wall -Wextra -Wformat -Wformat=2 -Wconversion \
* -Wimplicit-fallthrough -Werror=format-security \
* -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS \
* -fstack-clash-protection -fstack-protector-strong \
* -Wl,-z,relro -Wl,-z,now -Wl,--as-needed \
* -Wl,--no-copy-dt-needed-entries sockpuppet.c -o sockpuppet
*
* Common usage:
* ./sockpuppet /bin/sh
* Run a command with non-interactive stdio: stdin from /dev/null and
* stdout/stderr relayed through the parent.
*
* ./sockpuppet --interactive /bin/sh
* Run with a private PTY for shells, REPLs, and full-screen terminal apps.
*
* ./sockpuppet --allow-host=127.0.0.1:8080/tcp curl http://10.0.1.1:8080
* Allow the sandbox to reach a host-local service through the 10.0.1.x
* gateway mapping.
*
* ./sockpuppet --allow-direct=169.254.169.254/32:80/tcp curl ...
* Permit a direct-mode connection to a normally blocked special-use range.
*
* ./sockpuppet --publish=127.0.0.2:8080:8080/tcp python3 -m http.server 8080
* Publish a sandbox TCP service on an explicit host loopback alias. UDP
* uses the same grammar with /udp. 127.0.0.1 is intentionally rejected.
*
* ./sockpuppet --egress=none --publish=127.0.0.2:8080:8080/tcp ...
* Block child-initiated TCP/UDP egress while still permitting replies for
* active published inbound flows.
*
* ./sockpuppet --socks socks5://127.0.0.1:1080 curl https://example.com
* Route outbound traffic through a SOCKS5 proxy.
*
* ./sockpuppet --unsafe-share-cwd ...
* Allow running from /, /root, or /home/... when you intentionally want
* the current working directory exposed inside the sandbox.
*
* To run under delegated cgroup containment from an external wrapper:
* systemd-run --user --scope --quiet --same-dir --collect \
* --property=Delegate=yes --property=MemoryMax=... \
* --property=MemoryHigh=... --property=TasksMax=... \
* --property=CPUQuota=... -- /path/to/sockpuppet ...
* Keep launch policy in the wrapper; this binary only checks whether it is
* already inside a delegated subtree.
*/
#include <arpa/inet.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/audit.h>
#include <linux/capability.h>
#include <linux/filter.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/sched.h>
#include <linux/seccomp.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <stddef.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <limits.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/random.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/un.h>
#include <sys/uio.h>
#include <sys/xattr.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#if defined(__x86_64__)
#include <sys/user.h>
#elif defined(__aarch64__)
#include <elf.h>
#include <asm/ptrace.h>
#endif
#ifndef __WALL
#define __WALL 0x40000000
#endif
#ifndef AF_ALG
#define AF_ALG 38
#endif
#ifndef NT_ARM_SYSTEM_CALL
#define NT_ARM_SYSTEM_CALL 0x404
#endif
#if defined(__GNUC__)
#define SP_UNUSED __attribute__((unused))
#else
#define SP_UNUSED
#endif
#define MAX_TCP 128
#define MAX_UDP 64
#define MAX_PUBLISH_RULES 64
#define MAX_PUBLISH_TCP 128
#define MAX_PUBLISH_UDP 128
#define MAX_EVENTS 64
#define EPOLL_TIMEOUT_MS 100
#define TCP_PENDING_WRITE_CAP 262144
#define PUBLISH_TCP_PENDING_CAP 262144
#define TUN_QUEUE_PACKETS 32
#define TUN_QUEUE_PACKET_MAX 65535
#define RELAY_QUEUE_CAP 262144
#define RELAY_DRAIN_BUDGET 65536
#define RELAY_BATCH_BUDGET 262144
#define PUBLISH_SYNTH_PORT_MIN 40000U
#define PUBLISH_SYNTH_PORT_MAX 60999U
#define BROKER_MEMORY_LOW "67108864"
#define BROKER_MEMORY_HIGH "134217728"
#define BROKER_MEMORY_MAX "201326592"
#define BROKER_PIDS_MAX "32"
#define BROKER_CPU_WEIGHT "200"
#define PAYLOAD_MEMORY_HIGH "805306368"
#define PAYLOAD_MEMORY_MAX "1073741824"
#define PAYLOAD_PIDS_MAX "128"
#define PAYLOAD_CPU_WEIGHT "100"
#define PAYLOAD_CPU_MAX "200000 100000"
#define RLIMIT_PARENT_NOFILE 4096
#define RLIMIT_CHILD_NOFILE 1024
/* Epoll registrations carry immutable value tokens rather than pointers into
* reusable flow slots. A queued event is accepted only if its type, slot,
* generation, and registered fd still match current state. */
enum fd_type {
FD_TUN = 1,
FD_STDOUT_RELAY,
FD_STDERR_RELAY,
FD_STDOUT_DEST,
FD_STDERR_DEST,
FD_INTERACTIVE_TTY,
FD_INTERACTIVE_PTY,
FD_TCP,
FD_UDP_RELAY,
FD_UDP_CTRL,
FD_PUBLISH_TCP_LISTENER,
FD_PUBLISH_TCP_HOST,
FD_PUBLISH_UDP_SOCKET,
FD_CHILD_EXIT
};
struct epoll_key {
enum fd_type type;
uint8_t index;
uint16_t fd;
uint32_t generation;
};
static uint64_t epoll_token_encode(enum fd_type type, uint8_t index,
uint16_t fd, uint32_t generation) {
return ((uint64_t)(uint8_t)type << 56) | ((uint64_t)index << 48) |
((uint64_t)fd << 32) | (uint64_t)generation;
}
static int epoll_token_decode(uint64_t token, struct epoll_key *key) {
uint8_t type = (uint8_t)(token >> 56);
if (key == NULL || type < (uint8_t)FD_TUN ||
type > (uint8_t)FD_CHILD_EXIT)
return -1;
key->type = (enum fd_type)type;
key->index = (uint8_t)(token >> 48);
key->fd = (uint16_t)(token >> 32);
key->generation = (uint32_t)token;
if (key->generation == 0)
return -1;
return 0;
}
static uint32_t epoll_next_generation(uint32_t generation) {
generation++;
return generation == 0 ? 1 : generation;
}
static int epoll_key_matches(const struct epoll_key *key, enum fd_type type,
uint8_t index, int fd, uint32_t generation) {
return key != NULL && fd >= 0 && fd <= UINT16_MAX && generation != 0 &&
key->type == type && key->index == index &&
key->fd == (uint16_t)fd && key->generation == generation;
}
static int g_epfd = -1; /* Global epoll fd */
static int g_tun_registered_fd = -1;
/* The relay state is deliberately Sans-I/O: it owns bytes and transitions,
* but no descriptors, errno values, clocks, or syscalls. The broker runtime
* below translates readiness and syscall results into these operations. */
struct sp_relay_state {
uint8_t *storage;
size_t capacity;
size_t head;
size_t length;
int source_eof;
int destination_failed;
int source_paused;
uint64_t bytes_in;
uint64_t bytes_out;
uint64_t bytes_discarded;
uint64_t pause_count;
uint64_t resume_count;
size_t high_water;
};
static uint8_t g_stdout_relay_storage[RELAY_QUEUE_CAP];
static uint8_t g_stderr_relay_storage[RELAY_QUEUE_CAP];
static uint8_t g_tty_to_pty_storage[RELAY_QUEUE_CAP];
static uint8_t g_pty_to_tty_storage[RELAY_QUEUE_CAP];
static void sp_relay_init(struct sp_relay_state *relay, uint8_t *storage,
size_t capacity) {
memset(relay, 0, sizeof(*relay));
relay->storage = storage;
relay->capacity = capacity;
}
static int SP_UNUSED sp_relay_invariant(const struct sp_relay_state *relay) {
return relay != NULL && relay->storage != NULL && relay->capacity > 0 &&
relay->head < relay->capacity && relay->length <= relay->capacity &&
(!relay->source_paused || relay->length > relay->capacity / 2);
}
static size_t sp_relay_space(const struct sp_relay_state *relay) {
return relay->capacity - relay->length;
}
static size_t sp_relay_accept(struct sp_relay_state *relay,
const uint8_t *data, size_t length) {
size_t accepted = length < sp_relay_space(relay) ? length
: sp_relay_space(relay);
size_t tail = (relay->head + relay->length) % relay->capacity;
size_t first = accepted < relay->capacity - tail ? accepted
: relay->capacity - tail;
if (first > 0)
memcpy(relay->storage + tail, data, first);
if (accepted > first)
memcpy(relay->storage, data + first, accepted - first);
relay->length += accepted;
relay->bytes_in += accepted;
if (relay->length > relay->high_water)
relay->high_water = relay->length;
if (relay->length == relay->capacity && !relay->source_paused) {
relay->source_paused = 1;
relay->pause_count++;
}
return accepted;
}
static const uint8_t *sp_relay_output(const struct sp_relay_state *relay,
size_t *length) {
size_t contiguous = relay->length;
if (contiguous > relay->capacity - relay->head)
contiguous = relay->capacity - relay->head;
*length = contiguous;
return relay->storage + relay->head;
}
static void sp_relay_consume(struct sp_relay_state *relay, size_t length) {
if (length > relay->length)
length = relay->length;
relay->head = (relay->head + length) % relay->capacity;
relay->length -= length;
relay->bytes_out += length;
if (relay->source_paused && relay->length <= relay->capacity / 2) {
relay->source_paused = 0;
relay->resume_count++;
}
}
static void sp_relay_source_eof(struct sp_relay_state *relay) {
relay->source_eof = 1;
relay->source_paused = 0;
}
static void sp_relay_destination_failed(struct sp_relay_state *relay) {
relay->destination_failed = 1;
relay->bytes_discarded += relay->length;
relay->head = 0;
relay->length = 0;
relay->source_paused = 0;
}
static int sp_relay_wants_read(const struct sp_relay_state *relay) {
return !relay->source_eof && !relay->destination_failed &&
!relay->source_paused && relay->length < relay->capacity;
}
static int sp_relay_wants_write(const struct sp_relay_state *relay) {
return !relay->destination_failed && relay->length > 0;
}
static int sp_relay_complete(const struct sp_relay_state *relay) {
return relay->destination_failed || (relay->source_eof && relay->length == 0);
}
enum sp_relay_event_type {
SP_RELAY_SOURCE_BYTES = 1,
SP_RELAY_SOURCE_EOF,
SP_RELAY_DESTINATION_WRITTEN,
SP_RELAY_DESTINATION_FAILED
};
struct sp_relay_event {
enum sp_relay_event_type type;
const uint8_t *bytes;
size_t length;
};
enum sp_relay_action {
SP_RELAY_ACTION_READ_SOURCE = 1U << 0,
SP_RELAY_ACTION_WRITE_DESTINATION = 1U << 1,
SP_RELAY_ACTION_COMPLETE = 1U << 2,
SP_RELAY_ACTION_INVALID = 1U << 3
};
static unsigned int sp_relay_step(struct sp_relay_state *relay,
const struct sp_relay_event *event) {
switch (event->type) {
case SP_RELAY_SOURCE_BYTES:
if ((event->length > 0 && event->bytes == NULL) ||
event->length > sp_relay_space(relay))
return SP_RELAY_ACTION_INVALID;
(void)sp_relay_accept(relay, event->bytes, event->length);
break;
case SP_RELAY_SOURCE_EOF:
sp_relay_source_eof(relay);
break;
case SP_RELAY_DESTINATION_WRITTEN:
if (event->length > relay->length)
return SP_RELAY_ACTION_INVALID;
sp_relay_consume(relay, event->length);
break;
case SP_RELAY_DESTINATION_FAILED:
sp_relay_destination_failed(relay);
break;
default:
return SP_RELAY_ACTION_INVALID;
}
unsigned int actions = 0;
if (sp_relay_wants_read(relay))
actions |= SP_RELAY_ACTION_READ_SOURCE;
if (sp_relay_wants_write(relay))
actions |= SP_RELAY_ACTION_WRITE_DESTINATION;
if (sp_relay_complete(relay))
actions |= SP_RELAY_ACTION_COMPLETE;
return actions;
}
static uint64_t SP_UNUSED sp_relay_digest(const struct sp_relay_state *relay) {
uint64_t digest = UINT64_C(1469598103934665603);
size_t first_length = 0;
const uint8_t *first = sp_relay_output(relay, &first_length);
for (size_t i = 0; i < first_length; ++i) {
digest ^= first[i];
digest *= UINT64_C(1099511628211);
}
for (size_t i = first_length; i < relay->length; ++i) {
digest ^= relay->storage[i - first_length];
digest *= UINT64_C(1099511628211);
}
digest ^= relay->length;
digest *= UINT64_C(1099511628211);
digest ^= (uint64_t)(unsigned int)relay->source_eof;
digest *= UINT64_C(1099511628211);
digest ^= (uint64_t)(unsigned int)relay->destination_failed;
digest *= UINT64_C(1099511628211);
digest ^= relay->bytes_discarded;
return digest;
}
/* The kernel TUN write queue is finite. Keep a small, fixed broker-side
* queue so a non-reading child cannot block the event loop indefinitely.
* Packets are never reordered: once there is queued output, later packets
* join the queue even if the fd becomes writable before the next EPOLLOUT. */
struct tun_packet {
size_t len;
uint8_t data[TUN_QUEUE_PACKET_MAX];
};
struct tun_queue {
struct tun_packet packets[TUN_QUEUE_PACKETS];
size_t head;
size_t count;
unsigned long dropped_full;
};
static struct tun_queue g_tun_queue;
/* SOCKS5 proxy configuration */
struct socks_config {
char host[256];
int port;
char username[256];
char password[256];
int enabled;
int remote_dns;
int addr_valid;
struct sockaddr_in addr;
};
enum socks_io_state {
SOCKS_IO_NONE = 0,
SOCKS_IO_CONNECTING,
SOCKS_IO_METHOD,
SOCKS_IO_AUTH,
SOCKS_IO_REQUEST,
SOCKS_IO_READY,
SOCKS_IO_FAILED
};
struct socks_io {
int active;
int is_udp;
int connect_pending;
enum socks_io_state state;
uint32_t target_ip;
uint16_t target_port;
char target_domain[256];
uint8_t txbuf[512];
size_t tx_off;
size_t tx_len;
uint8_t rxbuf[512];
size_t rx_len;
};
static struct socks_config socks_proxy = {0};
static int unsafe_share_cwd = 0;
static int require_writable_cwd = 0;
static int interactive_stdio = 0;
static int clone3_ptrace_compat = 0;
static int verbose = 0; /* Verbose debug output */
static int tcp_flow_limit = MAX_TCP;
static int udp_flow_limit = MAX_UDP;
static volatile sig_atomic_t interactive_resize_pending = 0;
struct interactive_session {
int active;
int host_tty_fd;
int pty_master_fd;
int pty_slave_fd;
struct termios host_termios;
struct winsize host_winsize;
int host_termios_saved;
int host_winsize_saved;
};
static struct interactive_session interactive_session = {
.host_tty_fd = -1,
.pty_master_fd = -1,
.pty_slave_fd = -1,
};
/* Debug macro - only prints if verbose mode enabled */
#define DBG(fmt, ...) \
do { \
if (verbose) \
fprintf(stderr, "[sockpuppet] " fmt "\n", ##__VA_ARGS__); \
} while (0)
/* Host gateway configuration - map 10.0.1.x to 127.0.0.x */
#define HOST_PING_IP 0x0100000a /* 10.0.0.1 - only for ping */
#define DNS_PROXY_IP HOST_PING_IP /* 10.0.0.1 - broker DNS for socks5h */
#define HOST_GATEWAY_BASE 0x0001000a /* 10.0.1.0 network byte order */
#define HOST_GATEWAY_MASK 0x00ffffff /* /24 mask for 10.0.1.x */
#define LOCALHOST_BASE 0x0000007f /* 127.0.0.0 network byte order */
#define SANDBOX_IP 0x0200000a /* 10.0.0.2 network byte order */
#define PUBLISH_SYNTHETIC_IP 0x0102000a /* 10.0.2.1 network byte order */
#define MAX_HOST_RULES 64
#define MAX_DIRECT_RULES 64
#define MAX_DNS_MAPPINGS 1024
#define DNS_SYNTHETIC_BASE 0x0a3f0000U /* 10.63.0.0/16, host byte order */
enum egress_mode {
EGRESS_DIRECT = 0,
EGRESS_SOCKS,
EGRESS_NONE,
};
enum parent_harden_profile {
PARENT_HARDEN_DIRECT = 0,
PARENT_HARDEN_SOCKS,
PARENT_HARDEN_EGRESS_NONE,
};
struct parent_harden_config {
enum parent_harden_profile profile;
int outbound_sockets_allowed;
int interactive;
/* Fatal-teardown signaling targets, pinned into the parent seccomp
* filter: signaling is allowed only as SIGKILL to exactly these. */
pid_t child_pid;
int child_pidfd;
};
struct parent_harden_status {
int setup_fds_closed;
int scratch_cleaned;
int landlock_active;
int seccomp_active;
};
static enum egress_mode egress_mode = EGRESS_DIRECT;
static int egress_mode_explicit = 0;
struct host_rule {
uint8_t last_octet; /* x in 127.0.0.x (1-255), 0 = wildcard */
uint16_t port; /* port number, 0 = all ports */
int proto; /* IPPROTO_TCP, IPPROTO_UDP, or 0 for both */
int wildcard_ip; /* match all 127.0.0.x */
int wildcard_port; /* match all ports */
};
static struct host_rule host_rules[MAX_HOST_RULES];
static int host_rule_count = 0;
static int host_allow_all = 0; /* --host=* */
struct direct_rule {
uint32_t network; /* host byte order */
uint32_t mask; /* host byte order */
uint16_t port;
int proto; /* IPPROTO_TCP, IPPROTO_UDP, or 0 for both */
};
static struct direct_rule direct_rules[MAX_DIRECT_RULES];
static int direct_rule_count = 0;
struct publish_rule {
uint32_t host_ip; /* network byte order */
uint16_t host_port;
uint16_t container_port;
int proto; /* IPPROTO_TCP or IPPROTO_UDP */
int listen_fd;
int rule_index;
};
static struct publish_rule publish_rules[MAX_PUBLISH_RULES];
static int publish_rule_count = 0;
enum publish_tcp_state {
PUBLISH_TCP_CLOSED = 0,
PUBLISH_TCP_SYN_SENT,
PUBLISH_TCP_ESTABLISHED,
PUBLISH_TCP_HOST_FIN,
PUBLISH_TCP_CHILD_FIN,
PUBLISH_TCP_CLOSING,
};
struct publish_tcp_flow {
uint32_t generation;
int used;
int host_fd;
int rule_index;
uint32_t synthetic_ip;
uint16_t synthetic_port;
uint32_t child_ip;
uint16_t child_port;
uint32_t child_isn;
uint32_t child_next;
uint32_t broker_isn;
uint32_t broker_next;
uint8_t pending_child_to_host[PUBLISH_TCP_PENDING_CAP];
size_t pending_child_len;
size_t pending_child_off;
time_t last_active;
int host_fin_sent;
int child_fin_seen;
int host_write_shutdown;
enum publish_tcp_state state;
};
struct publish_udp_flow {
int used;
int rule_index;
struct sockaddr_in host_peer;
uint16_t synthetic_port;
uint16_t child_port;
time_t last_active;
};
static struct publish_tcp_flow publish_tcp_flows[MAX_PUBLISH_TCP];
static struct publish_udp_flow publish_udp_flows[MAX_PUBLISH_UDP];
static uint16_t next_publish_synth_port = PUBLISH_SYNTH_PORT_MIN;
struct dns_mapping {
int used;
uint32_t synthetic_ip; /* network byte order */
time_t last_used;
char name[256];
};
static struct dns_mapping dns_mappings[MAX_DNS_MAPPINGS];
static uint32_t next_dns_synthetic_host = 1;
/* Check if IP is in gateway range (10.0.1.0/24) */
static int is_gateway_ip(uint32_t ip) {
return (ip & HOST_GATEWAY_MASK) == HOST_GATEWAY_BASE;
}
/* Extract last octet from gateway IP (10.0.1.x -> x) */
static uint8_t gateway_last_octet(uint32_t ip) {
return (uint8_t)((ip >> 24) & 0xff);
}
/* Convert gateway IP to localhost (10.0.1.x -> 127.0.0.x) */
static uint32_t gateway_to_localhost(uint32_t gw_ip) {
uint8_t last = gateway_last_octet(gw_ip);
return LOCALHOST_BASE | ((uint32_t)last << 24);
}
/* Check if gateway access is allowed for given IP, port, and protocol */
static int is_gateway_allowed(uint32_t gw_ip, uint16_t port, int proto) {
if (!is_gateway_ip(gw_ip))
return 0;
if (host_allow_all)
return 1;
uint8_t last = gateway_last_octet(gw_ip);
for (int i = 0; i < host_rule_count; i++) {
struct host_rule *r = &host_rules[i];
int ip_match = r->wildcard_ip || (r->last_octet == last);
int port_match = r->wildcard_port || (r->port == port);
int proto_match = (r->proto == 0) || (r->proto == proto);
if (ip_match && port_match && proto_match)
return 1;
}
return 0;
}
static uint32_t cidr_mask_from_prefix(unsigned int prefix) {
if (prefix == 0)
return 0;
return 0xffffffffU << (32U - prefix);
}
static int host_ip_in_cidr(uint32_t ip, uint32_t network, uint32_t mask) {
return (ip & mask) == network;
}
static int direct_ip_blocked_by_default(uint32_t ip_be) {
static const struct {
uint32_t network;
uint32_t mask;
} blocked[] = {
{0x00000000U, 0xff000000U}, /* 0.0.0.0/8 */
{0x0a000000U, 0xff000000U}, /* 10.0.0.0/8 */
{0x64400000U, 0xffc00000U}, /* 100.64.0.0/10 */
{0x7f000000U, 0xff000000U}, /* 127.0.0.0/8 */
{0xa9fe0000U, 0xffff0000U}, /* 169.254.0.0/16 */
{0xac100000U, 0xfff00000U}, /* 172.16.0.0/12 */
{0xc0000000U, 0xffffff00U}, /* 192.0.0.0/24 */
{0xc0000200U, 0xffffff00U}, /* 192.0.2.0/24 */
{0xc0a80000U, 0xffff0000U}, /* 192.168.0.0/16 */
{0xc6120000U, 0xfffe0000U}, /* 198.18.0.0/15 */
{0xc6336400U, 0xffffff00U}, /* 198.51.100.0/24 */
{0xcb007100U, 0xffffff00U}, /* 203.0.113.0/24 */
{0xe0000000U, 0xf0000000U}, /* 224.0.0.0/4 */
{0xf0000000U, 0xf0000000U}, /* 240.0.0.0/4 */
};
uint32_t ip = ntohl(ip_be);
for (size_t i = 0; i < sizeof(blocked) / sizeof(blocked[0]); ++i) {
if (host_ip_in_cidr(ip, blocked[i].network, blocked[i].mask))
return 1;
}
return 0;
}
static int direct_rule_matches(const struct direct_rule *rule, uint32_t ip_be,
uint16_t port, int proto) {
uint32_t ip = ntohl(ip_be);
return host_ip_in_cidr(ip, rule->network, rule->mask) &&
rule->port == port && (rule->proto == 0 || rule->proto == proto);
}
static int is_direct_egress_allowed(uint32_t ip_be, uint16_t port, int proto) {
if (!direct_ip_blocked_by_default(ip_be))
return 1;
for (int i = 0; i < direct_rule_count; ++i) {
if (direct_rule_matches(&direct_rules[i], ip_be, port, proto))
return 1;
}
return 0;
}
static void log_direct_egress_block(uint32_t ip_be, uint16_t port, int proto) {
char addr[INET_ADDRSTRLEN];
struct in_addr in = {.s_addr = ip_be};
const char *proto_name = proto == IPPROTO_TCP ? "tcp"
: proto == IPPROTO_UDP ? "udp"
: "ip";
if (inet_ntop(AF_INET, &in, addr, sizeof(addr)) == NULL)
snprintf(addr, sizeof(addr), "unknown");
DBG("[parent] direct %s egress blocked: %s:%u", proto_name, addr, port);
}
static enum parent_harden_profile
parent_harden_profile_for_egress(enum egress_mode mode) {
if (mode == EGRESS_SOCKS)
return PARENT_HARDEN_SOCKS;
if (mode == EGRESS_NONE)
return PARENT_HARDEN_EGRESS_NONE;
return PARENT_HARDEN_DIRECT;
}
static const char *
parent_harden_profile_name(enum parent_harden_profile profile) {
switch (profile) {
case PARENT_HARDEN_SOCKS:
return "socks";
case PARENT_HARDEN_EGRESS_NONE:
return "egress-none";
case PARENT_HARDEN_DIRECT:
default:
return "direct";
}
}
static struct parent_harden_config parent_harden_config_from_runtime(void) {
struct parent_harden_config cfg = {
.profile = parent_harden_profile_for_egress(egress_mode),
.outbound_sockets_allowed = egress_mode != EGRESS_NONE,
.interactive = interactive_stdio,
.child_pid = 0,
.child_pidfd = -1,
};
return cfg;
}
static void
parent_harden_log_prepared(const struct parent_harden_config *cfg,
const struct parent_harden_status *status) {
DBG("parent hardening prepared: profile=%s outbound_sockets=%s "
"interactive=%s setup_fds_closed=%s scratch_cleaned=%s "
"landlock=%s seccomp=%s",
parent_harden_profile_name(cfg->profile),
cfg->outbound_sockets_allowed ? "yes" : "no",
cfg->interactive ? "yes" : "no",
status->setup_fds_closed ? "yes" : "no",
status->scratch_cleaned ? "yes" : "no",
status->landlock_active ? "active" : "pending",
status->seccomp_active ? "active" : "pending");
}
static void parent_harden_log_active(const struct parent_harden_config *cfg) {
DBG("parent hardening active: profile=%s outbound_sockets=%s "
"interactive=%s",
parent_harden_profile_name(cfg->profile),
cfg->outbound_sockets_allowed ? "yes" : "no",
cfg->interactive ? "yes" : "no");
}
/* Rate limiting */
#define MAX_CONNECTS_PER_SEC 50
#define TCP_HALF_OPEN_TIMEOUT_SEC 10
#define TCP_IDLE_TIMEOUT_SEC 120
static struct timespec rate_limit_last = {0};
static double rate_limit_tokens = (double)MAX_CONNECTS_PER_SEC;
static double monotonic_elapsed_seconds(struct timespec now,
struct timespec then) {
time_t sec = now.tv_sec - then.tv_sec;
long nsec = now.tv_nsec - then.tv_nsec;
return (double)sec + ((double)nsec / 1000000000.0);
}
static int check_rate_limit(void) {
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) < 0)
return 0;
if (rate_limit_last.tv_sec == 0 && rate_limit_last.tv_nsec == 0) {
rate_limit_last = now;
} else {
double elapsed = monotonic_elapsed_seconds(now, rate_limit_last);
if (elapsed > 0.0) {
rate_limit_tokens += elapsed * (double)MAX_CONNECTS_PER_SEC;
if (rate_limit_tokens > (double)MAX_CONNECTS_PER_SEC)
rate_limit_tokens = (double)MAX_CONNECTS_PER_SEC;
rate_limit_last = now;
}
}
if (rate_limit_tokens < 1.0)
return 0;
rate_limit_tokens -= 1.0;
return 1;
}
/* TCP connection states */
enum tcp_state {
SP_TCP_CLOSED = 0,
SP_TCP_SYN_RECEIVED,
SP_TCP_ESTABLISHED,
SP_TCP_FIN_WAIT_1,
SP_TCP_FIN_WAIT_2,
SP_TCP_CLOSE_WAIT,
SP_TCP_CLOSING,
SP_TCP_LAST_ACK,
SP_TCP_TIME_WAIT
};
struct tcp_flow {
uint32_t generation;
uint32_t cli_ip;
uint16_t cli_port;
uint32_t srv_ip;
uint16_t srv_port;
uint32_t cli_isn;
uint32_t srv_isn;
uint32_t cli_next;
uint32_t srv_next;
int sock;
enum tcp_state state;
time_t last_active;
/* TCP timestamp option (RFC 7323) */
int ts_ok; /* Timestamps negotiated */
uint32_t ts_recent; /* Last TSval received from client */
uint8_t pending_write[TCP_PENDING_WRITE_CAP];
size_t pending_write_off;
size_t pending_write_len;
int pending_fin;
uint32_t pending_fin_seq;
int backend_ready;
struct socks_io socks;
};
static struct tcp_flow tcp_flows[MAX_TCP];
struct udp_flow {
uint32_t generation;
uint32_t cli_ip;
uint16_t cli_port;
uint32_t srv_ip;
uint16_t srv_port;
int tcp_ctrl; /* SOCKS5 TCP control connection (must stay open) */
int udp_relay; /* UDP socket to SOCKS relay */
int udp_staging; /* UDP socket bound before SOCKS UDP ASSOCIATE completes */
time_t last_used; /* Last activity timestamp */
struct sockaddr_in relay_addr; /* Expected relay source for validation */
struct socks_io socks;
uint8_t pending_data[65535];
size_t pending_len;
int pending_set;
unsigned long dropped_backpressure;
};
static struct udp_flow udp_flows[MAX_UDP];
struct cgroup_ctx {
int active;
int cpu_enabled;
int memory_enabled;
int pids_enabled;
int payload_swap_enabled;
int payload_cpu_quota_enabled;
char root[PATH_MAX];
char broker[PATH_MAX];
char payload[PATH_MAX];
};
static struct cgroup_ctx g_cgroup = {0};
/* I/O buffer for event loop reads - safe as static since writes are via
opaque syscalls (read/recv) that act as compiler barriers. */
static uint8_t g_io_buf[65536];
/* ---------- utilities ---------- */
static void die(const char *msg) {
perror(msg);
exit(1);
}
static void kill_and_reap_child(pid_t pid) {
int saved = errno;
if (pid > 0) {
(void)kill(pid, SIGKILL);
while (waitpid(pid, NULL, 0) < 0 && errno == EINTR) {
}
}
errno = saved;
}
static void parent_die_with_child(pid_t pid, const char *msg) {
kill_and_reap_child(pid);
die(msg);
}
static int cleanup_overlay_base(const char *overlay_base) {
if (rmdir(overlay_base) == 0 || errno == ENOENT)
return 0;
perror("rmdir overlay base");
return -1;
}
static ssize_t write_all(int fd, const void *buf, size_t len) {
const uint8_t *p = buf;
size_t off = 0;
while (off < len) {
ssize_t w = write(fd, p + off, len - off);
if (w > 0) {
off += (size_t)w;
continue;
}
if (w < 0 && errno == EINTR)
continue;
if (w == 0)
errno = EIO;
return -1;
}
return (ssize_t)off;
}
static ssize_t read_all(int fd, void *buf, size_t len) {
uint8_t *p = buf;
size_t off = 0;
while (off < len) {
ssize_t r = read(fd, p + off, len - off);
if (r > 0) {
off += (size_t)r;
continue;
}
if (r < 0 && errno == EINTR)
continue;
if (r == 0)
errno = EPIPE;
return -1;
}
return (ssize_t)off;
}
static void tun_update_events(int tunfd) {
struct epoll_event ev = {
.events = EPOLLIN | (g_tun_queue.count > 0 ? EPOLLOUT : 0),
.data.u64 = epoll_token_encode(FD_TUN, 0, (uint16_t)tunfd, 1),
};
if (g_epfd >= 0 && g_tun_registered_fd == tunfd &&
epoll_ctl(g_epfd, EPOLL_CTL_MOD, tunfd, &ev) < 0)
DBG("epoll_ctl mod TUN: %s", strerror(errno));
}
static int tun_queue_packet(int tunfd, const uint8_t *buf, size_t len,
const char *what) {
size_t tail;
if (len > TUN_QUEUE_PACKET_MAX || g_tun_queue.count == TUN_QUEUE_PACKETS) {
g_tun_queue.dropped_full++;
DBG("TUN output queue full (%s), dropping packet", what);
return -1;
}
tail = (g_tun_queue.head + g_tun_queue.count) % TUN_QUEUE_PACKETS;
g_tun_queue.packets[tail].len = len;
memcpy(g_tun_queue.packets[tail].data, buf, len);
g_tun_queue.count++;
tun_update_events(tunfd);
return 0;
}
/* Returns zero only when the packet is accepted by the kernel or the bounded
* queue. Callers that advance protocol state must use this as their commit
* point: a packet rejected because the queue is full was never emitted. */
static int tun_write_packet(int tunfd, const uint8_t *buf, size_t len,
const char *what) {
if (len == 0 || len > TUN_QUEUE_PACKET_MAX) {
DBG("invalid TUN packet length (%s): %zu", what, len);
return -1;
}
if (g_tun_queue.count > 0)
return tun_queue_packet(tunfd, buf, len, what);
for (;;) {
ssize_t n = write(tunfd, buf, len);
if (n == (ssize_t)len)
return 0;
if (n > 0) {
DBG("TUN write short (%s): %zd/%zu, dropping packet", what, n, len);
return -1;
}
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return tun_queue_packet(tunfd, buf, len, what);
DBG("TUN write failed (%s): %s", what, strerror(errno));
return -1;
}
}
/* Drain queued packets on EPOLLOUT. TUN packet writes must be atomic; a
* short write cannot be resumed without corrupting the packet boundary. */
static int tun_flush_packets(int tunfd) {
while (g_tun_queue.count > 0) {
struct tun_packet *packet = &g_tun_queue.packets[g_tun_queue.head];
ssize_t n = write(tunfd, packet->data, packet->len);
if (n == (ssize_t)packet->len) {
g_tun_queue.head = (g_tun_queue.head + 1) % TUN_QUEUE_PACKETS;
g_tun_queue.count--;
continue;
}
if (n < 0 && errno == EINTR)
continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
break;
if (n > 0)
DBG("TUN queued write short: %zd/%zu, dropping packet", n,
packet->len);
else
DBG("TUN queued write failed: %s", strerror(errno));
return -1;
}
tun_update_events(tunfd);
return 0;
}
static void interactive_handle_sigwinch(int signo) {
(void)signo;
interactive_resize_pending = 1;
}
static int parse_long_strict(const char *s, long min, long max, long *out) {
char *end = NULL;
long value;
if (s == NULL || *s == '\0')
return -1;
errno = 0;
value = strtol(s, &end, 10);
if (errno != 0 || end == NULL || *end != '\0' || value < min || value > max)
return -1;
*out = value;
return 0;
}
/* Helper to suppress unused result warnings from FORTIFY_SOURCE */
#define IGNORE_RESULT(x) \
do { \
if (x) { \
} \
} while (0)
static int write_file_checked(const char *path, const char *data) {
int fd = open(path, O_WRONLY);
size_t len = strlen(data);
if (fd < 0)
return -1;
if (write_all(fd, data, len) != (ssize_t)len) {
int saved = errno;
close(fd);
errno = saved;
return -1;
}
if (close(fd) < 0)
return -1;
return 0;
}
static void set_rlimit_or_die(int resource, rlim_t soft, rlim_t hard,
const char *name) {
struct rlimit lim = {.rlim_cur = soft, .rlim_max = hard};
if (setrlimit(resource, &lim) < 0) {
fprintf(stderr, "setrlimit(%s) failed: %s\n", name, strerror(errno));
exit(1);
}
DBG("RLIMIT %s set to soft=%llu hard=%llu", name,
(unsigned long long)soft, (unsigned long long)hard);
}
static void apply_parent_rlimits(void) {
set_rlimit_or_die(RLIMIT_CORE, 0, 0, "CORE");
set_rlimit_or_die(RLIMIT_MEMLOCK, 0, 0, "MEMLOCK");
set_rlimit_or_die(RLIMIT_NOFILE, RLIMIT_PARENT_NOFILE, RLIMIT_PARENT_NOFILE,
"NOFILE");
}
static void apply_child_rlimits(void) {
set_rlimit_or_die(RLIMIT_CORE, 0, 0, "CORE");
set_rlimit_or_die(RLIMIT_MEMLOCK, 0, 0, "MEMLOCK");
set_rlimit_or_die(RLIMIT_NOFILE, RLIMIT_CHILD_NOFILE, RLIMIT_CHILD_NOFILE,
"NOFILE");
}
static int detect_delegated_cgroup_root(struct cgroup_ctx *ctx);
static void startup_warn(const char *fmt, ...) {
va_list ap;
fprintf(stderr, "[sockpuppet] Warning: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
}
enum bench_trace_role {
BENCH_ROLE_UNKNOWN = 0,
BENCH_ROLE_PARENT,
BENCH_ROLE_OUTER_CHILD,
BENCH_ROLE_PID1,
BENCH_ROLE_PAYLOAD,
};
struct bench_trace_record {
long long ts_ns;
unsigned long long seq;
pid_t pid;
pid_t ppid;
enum bench_trace_role role;
const char *event;
const char *phase;
const char *status;
const char *detail;
};
static int bench_trace_fd = -1;
static enum bench_trace_role bench_trace_role = BENCH_ROLE_UNKNOWN;
static unsigned long long bench_trace_seq = 0;
static pid_t bench_trace_pid = -1;
static pid_t bench_trace_ppid = -1;
static const char *bench_trace_role_name(enum bench_trace_role role) {
switch (role) {
case BENCH_ROLE_PARENT:
return "parent";
case BENCH_ROLE_OUTER_CHILD:
return "outer_child";
case BENCH_ROLE_PID1:
return "pid1";
case BENCH_ROLE_PAYLOAD:
return "payload";
case BENCH_ROLE_UNKNOWN:
default:
return "unknown";
}
}
static long long bench_trace_now_ns(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
return 0;
return (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec;
}
static void bench_trace_init_from_env(void) {
const char *path = getenv("SOCKPUPPET_BENCH_TRACE");
if (path == NULL || path[0] == '\0')
return;
bench_trace_fd =
open(path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0600);
if (bench_trace_fd < 0) {
startup_warn("SOCKPUPPET_BENCH_TRACE disabled: cannot open %s: %s", path,
strerror(errno));
return;
}
}
static void bench_trace_set_role(enum bench_trace_role role) {
bench_trace_role = role;
bench_trace_pid = getpid();
bench_trace_ppid = getppid();
}
static int bench_trace_preserved_fd(void) {
return bench_trace_fd;
}
static size_t bench_json_append_char(char *buf, size_t pos, size_t cap,
char ch) {
if (pos + 1 < cap)
buf[pos++] = ch;
return pos;
}
static size_t bench_json_append_raw(char *buf, size_t pos, size_t cap,
const char *s) {
while (*s != '\0')
pos = bench_json_append_char(buf, pos, cap, *s++);
return pos;
}
static size_t bench_json_append_escaped(char *buf, size_t pos, size_t cap,
const char *s) {
static const char hex[] = "0123456789abcdef";
pos = bench_json_append_char(buf, pos, cap, '"');
if (s == NULL)
s = "";
for (const unsigned char *p = (const unsigned char *)s; *p != '\0'; ++p) {
switch (*p) {
case '"':
pos = bench_json_append_raw(buf, pos, cap, "\\\"");
break;
case '\\':
pos = bench_json_append_raw(buf, pos, cap, "\\\\");
break;
case '\b':
pos = bench_json_append_raw(buf, pos, cap, "\\b");
break;
case '\f':
pos = bench_json_append_raw(buf, pos, cap, "\\f");
break;
case '\n':
pos = bench_json_append_raw(buf, pos, cap, "\\n");
break;
case '\r':
pos = bench_json_append_raw(buf, pos, cap, "\\r");
break;
case '\t':
pos = bench_json_append_raw(buf, pos, cap, "\\t");
break;
default:
if (*p < 0x20) {
pos = bench_json_append_raw(buf, pos, cap, "\\u00");
pos = bench_json_append_char(buf, pos, cap, hex[*p >> 4]);
pos = bench_json_append_char(buf, pos, cap, hex[*p & 0xf]);
} else {
pos = bench_json_append_char(buf, pos, cap, (char)*p);
}
break;
}
}
pos = bench_json_append_char(buf, pos, cap, '"');
return pos;
}
static void bench_trace_emit(const char *event, const char *phase,
const char *status, const char *detail) {
char buf[8192];
size_t pos = 0;
int n;
struct bench_trace_record rec;
if (bench_trace_fd < 0)
return;
rec.ts_ns = bench_trace_now_ns();
rec.seq = ++bench_trace_seq;
rec.pid = bench_trace_pid;
rec.ppid = bench_trace_ppid;
rec.role = bench_trace_role;
rec.event = event;
rec.phase = phase;
rec.status = status;
rec.detail = detail;
n = snprintf(buf, sizeof(buf),
"{\"ts_ns\":%lld,\"seq\":%llu,\"pid\":%ld,\"ppid\":%ld,",
rec.ts_ns, rec.seq, (long)rec.pid, (long)rec.ppid);
if (n < 0)
return;
pos = (size_t)n < sizeof(buf) ? (size_t)n : sizeof(buf) - 1;
pos = bench_json_append_raw(buf, pos, sizeof(buf), "\"role\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf),
bench_trace_role_name(rec.role));
pos = bench_json_append_raw(buf, pos, sizeof(buf), ",\"event\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf), rec.event);
pos = bench_json_append_raw(buf, pos, sizeof(buf), ",\"phase\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf), rec.phase);
pos = bench_json_append_raw(buf, pos, sizeof(buf), ",\"status\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf), rec.status);
pos = bench_json_append_raw(buf, pos, sizeof(buf), ",\"detail\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf), rec.detail);
pos = bench_json_append_raw(buf, pos, sizeof(buf), "}\n");
if (pos >= sizeof(buf) - 1) {
n = snprintf(buf, sizeof(buf),
"{\"ts_ns\":%lld,\"seq\":%llu,\"pid\":%ld,\"ppid\":%ld,"
"\"role\":\"%s\",\"event\":\"trace_truncated\","
"\"phase\":\"trace\",\"status\":\"truncated\","
"\"detail\":\"record exceeded buffer\"}\n",
rec.ts_ns, rec.seq, (long)rec.pid, (long)rec.ppid,
bench_trace_role_name(rec.role));
if (n < 0)
return;
pos = (size_t)n < sizeof(buf) ? (size_t)n : sizeof(buf) - 1;
}
if (write_all(bench_trace_fd, buf, pos) != (ssize_t)pos) {
int saved = errno;
close(bench_trace_fd);
bench_trace_fd = -1;
errno = saved;
}
}
static void bench_phase_begin(const char *phase) {
bench_trace_emit("phase_begin", phase, "start", NULL);
}
static void bench_phase_end(const char *phase, const char *status) {
bench_trace_emit("phase_end", phase, status, NULL);
}
static void bench_mark(const char *event, const char *phase,
const char *status, const char *detail) {
bench_trace_emit(event, phase, status, detail);
}
static void pre_scan_verbose_flag(int argc, char **argv) {
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--verbose") == 0 || strcmp(argv[i], "-v") == 0) {
verbose = 1;
continue;
}
if (strcmp(argv[i], "--socks") == 0) {
++i;
continue;
}
if (strncmp(argv[i], "--socks-auth-file=", 18) == 0 ||
strncmp(argv[i], "--allow-host=", 13) == 0 ||
strncmp(argv[i], "--allow-direct=", 15) == 0 ||
strncmp(argv[i], "--max-tcp-flows=", 16) == 0 ||
strncmp(argv[i], "--max-udp-flows=", 16) == 0 ||
strcmp(argv[i], "--unsafe-share-cwd") == 0 ||
strcmp(argv[i], "--compat-ptrace-clone3") == 0 ||
strcmp(argv[i], "--interactive") == 0) {
continue;
}
if (argv[i][0] == '-')
break;
break;
}
}
static int read_self_cgroup_path(char *out, size_t out_sz) {
FILE *fp = fopen("/proc/self/cgroup", "r");
char line[1024];
if (!fp)
return -1;
while (fgets(line, sizeof(line), fp) != NULL) {
if (strncmp(line, "0::", 3) != 0)
continue;
char *path = line + 3;
char *nl = strchr(path, '\n');
if (nl)
*nl = '\0';
if (*path == '\0')
path = "/";
if (snprintf(out, out_sz, "%s", path) >= (int)out_sz) {
fclose(fp);
return -1;
}
fclose(fp);
return 0;
}
fclose(fp);
return -1;
}
static int read_text_file_trimmed(const char *path, char *out, size_t out_sz) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
ssize_t n;
if (fd < 0)
return -1;
if (out_sz == 0) {
close(fd);
errno = EINVAL;
return -1;
}
n = read(fd, out, out_sz - 1);
if (n < 0) {
int saved = errno;
close(fd);
errno = saved;
return -1;
}
if (close(fd) < 0)
return -1;
out[n] = '\0';
while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r' ||
out[n - 1] == ' ' || out[n - 1] == '\t')) {
out[--n] = '\0';
}
return 0;
}
static void report_userns_diagnostics(int err) {
char unprivileged_userns_clone[64] = "unavailable";
char max_user_namespaces[64] = "unavailable";
char apparmor_restrict[64] = "unavailable";
char lsm_stack[256] = "unavailable";
char lsm_context[256] = "unavailable";
(void)read_text_file_trimmed("/proc/sys/kernel/unprivileged_userns_clone",
unprivileged_userns_clone,
sizeof(unprivileged_userns_clone));
(void)read_text_file_trimmed("/proc/sys/user/max_user_namespaces",
max_user_namespaces,
sizeof(max_user_namespaces));
(void)read_text_file_trimmed(
"/proc/sys/kernel/apparmor_restrict_unprivileged_userns",
apparmor_restrict, sizeof(apparmor_restrict));
(void)read_text_file_trimmed("/sys/kernel/security/lsm", lsm_stack,
sizeof(lsm_stack));
(void)read_text_file_trimmed("/proc/self/attr/current", lsm_context,
sizeof(lsm_context));
fprintf(stderr,
"[sockpuppet] Error: unshare(CLONE_NEWUSER|...) failed: %s\n",
strerror(err));
startup_warn("unprivileged user namespaces are required; this host did not "
"allow sandbox setup");
startup_warn("sysctl kernel.unprivileged_userns_clone=%s",
unprivileged_userns_clone);
startup_warn("sysctl user.max_user_namespaces=%s", max_user_namespaces);
startup_warn("LSM stack: %s", lsm_stack);
if (strstr(lsm_stack, "apparmor") != NULL) {
startup_warn("sysctl kernel.apparmor_restrict_unprivileged_userns=%s",
apparmor_restrict);
startup_warn("current AppArmor context: %s", lsm_context);
} else {
startup_warn("AppArmor is not active on this host; skipping AppArmor-"
"specific userns checks");
startup_warn("current LSM context: %s", lsm_context);
}
startup_warn("supported hosts need user namespaces enabled; see the docs "
"for the supported environment matrix");
}
static int cgroup_write_file(const char *dir, const char *name,
const char *value) {
char path[PATH_MAX];
int fd;
if (snprintf(path, sizeof(path), "%s/%s", dir, name) >= (int)sizeof(path))
return -1;
fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -1;
size_t len = strlen(value);
ssize_t w = write(fd, value, len);
int saved = errno;
close(fd);
if (w != (ssize_t)len) {
errno = saved ? saved : EIO;
return -1;
}
return 0;
}
static int cgroup_mkdir_leaf(const char *path) {
if (mkdir(path, 0755) < 0 && errno != EEXIST)
return -1;
return 0;
}
static int cgroup_move_pid(const char *leaf, pid_t pid) {
char pidbuf[32];
if (snprintf(pidbuf, sizeof(pidbuf), "%ld", (long)pid) >= (int)sizeof(pidbuf))
return -1;
return cgroup_write_file(leaf, "cgroup.procs", pidbuf);
}
static void cgroup_warn(const char *fmt, ...) {
va_list ap;
fprintf(stderr, "[sockpuppet] Warning: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
}
static int cgroup_has_controller(const char *controllers, const char *needle) {
size_t nlen = strlen(needle);
const char *p = controllers;
while (*p) {
while (*p == ' ' || *p == '\t' || *p == '\n')
++p;
if (!*p)
break;
const char *start = p;
while (*p && *p != ' ' && *p != '\t' && *p != '\n')
++p;
size_t len = (size_t)(p - start);
if (len == nlen && memcmp(start, needle, nlen) == 0)
return 1;
}
return 0;
}
static int cgroup_enable_controllers(struct cgroup_ctx *ctx, int want_cpu,
int want_memory, int want_pids) {
char path[PATH_MAX];
char controllers[1024];
FILE *fp;
char enable[128] = {0};
size_t off = 0;
if (snprintf(path, sizeof(path), "%s/cgroup.controllers", ctx->root) >=
(int)sizeof(path))
return -1;
fp = fopen(path, "r");
if (!fp)
return -1;
if (!fgets(controllers, sizeof(controllers), fp)) {
fclose(fp);
return -1;
}
fclose(fp);
ctx->cpu_enabled = want_cpu && cgroup_has_controller(controllers, "cpu");
ctx->memory_enabled =
want_memory && cgroup_has_controller(controllers, "memory");
ctx->pids_enabled = want_pids && cgroup_has_controller(controllers, "pids");
if (ctx->cpu_enabled)
off += (size_t)snprintf(enable + off, sizeof(enable) - off, "%s+cpu",
off > 0 ? " " : "");
if (ctx->memory_enabled)
off += (size_t)snprintf(enable + off, sizeof(enable) - off, "%s+memory",
off > 0 ? " " : "");
if (ctx->pids_enabled)
off += (size_t)snprintf(enable + off, sizeof(enable) - off, "%s+pids",
off > 0 ? " " : "");
if (off == 0)
return 0;
if (off >= sizeof(enable))
return -1;
if (cgroup_write_file(ctx->root, "cgroup.subtree_control", enable) < 0)
return -1;
DBG("cgroup controllers enabled:%s%s%s", ctx->cpu_enabled ? " cpu" : "",
ctx->memory_enabled ? " memory" : "", ctx->pids_enabled ? " pids" : "");
return 0;
}
static int detect_delegated_cgroup_root(struct cgroup_ctx *ctx) {
char rel[PATH_MAX];
char ctrl_path[PATH_MAX];
char subtree_path[PATH_MAX];
char delegate[8] = {0};
ssize_t xrc;
memset(ctx, 0, sizeof(*ctx));
if (read_self_cgroup_path(rel, sizeof(rel)) < 0)
return 0;
if (strcmp(rel, "/") == 0) {
if (snprintf(ctx->root, sizeof(ctx->root), "/sys/fs/cgroup") >=
(int)sizeof(ctx->root))
return 0;
} else {
if (snprintf(ctx->root, sizeof(ctx->root), "/sys/fs/cgroup%s", rel) >=
(int)sizeof(ctx->root))
return 0;
}
if (snprintf(ctrl_path, sizeof(ctrl_path), "%s/cgroup.controllers", ctx->root) >=
(int)sizeof(ctrl_path) ||
snprintf(subtree_path, sizeof(subtree_path), "%s/cgroup.subtree_control",
ctx->root) >= (int)sizeof(subtree_path))
return 0;
if (access(ctrl_path, R_OK) < 0 || access(subtree_path, W_OK) < 0)
return 0;
xrc = getxattr(ctx->root, "user.delegate", delegate, sizeof(delegate) - 1);
if (xrc > 0) {
delegate[xrc] = '\0';
DBG("cgroup user.delegate=%s", delegate);
}
if (snprintf(ctx->broker, sizeof(ctx->broker), "%s/broker", ctx->root) >=
(int)sizeof(ctx->broker) ||
snprintf(ctx->payload, sizeof(ctx->payload), "%s/payload", ctx->root) >=
(int)sizeof(ctx->payload))
return 0;
ctx->active = 1;
return 1;
}
static int cgroup_root_has_foreign_procs(const struct cgroup_ctx *ctx) {
char path[PATH_MAX];
FILE *fp;
long pid;
pid_t self = getpid();
if (snprintf(path, sizeof(path), "%s/cgroup.procs", ctx->root) >=
(int)sizeof(path))
return 1;
fp = fopen(path, "r");
if (!fp)
return 1;
while (fscanf(fp, "%ld", &pid) == 1) {
if ((pid_t)pid != self) {
fclose(fp);
return 1;
}
}
fclose(fp);
return 0;
}
static void cgroup_rollback_setup(const struct cgroup_ctx *ctx) {
if (cgroup_move_pid(ctx->root, getpid()) < 0) {
DBG("cgroup containment rollback: failed to move self back to root (%s)",
strerror(errno));
}
if (rmdir(ctx->broker) < 0 && errno != ENOENT && errno != ENOTEMPTY) {
DBG("cgroup containment rollback: failed to remove broker leaf (%s)",
strerror(errno));
}
if (rmdir(ctx->payload) < 0 && errno != ENOENT && errno != ENOTEMPTY) {
DBG("cgroup containment rollback: failed to remove payload leaf (%s)",
strerror(errno));
}
}
static void cgroup_clear_controller_state(struct cgroup_ctx *ctx) {
ctx->cpu_enabled = 0;
ctx->memory_enabled = 0;
ctx->pids_enabled = 0;
ctx->payload_swap_enabled = 0;
ctx->payload_cpu_quota_enabled = 0;
}
static int cgroup_controller_failure_is_partial(int err) {
return err == EBUSY || err == EACCES || err == EPERM || err == EROFS ||
err == EOPNOTSUPP;
}
static void cgroup_apply_broker_limits(const struct cgroup_ctx *ctx) {
if (ctx->memory_enabled) {
if (cgroup_write_file(ctx->broker, "memory.low", BROKER_MEMORY_LOW) < 0 ||
cgroup_write_file(ctx->broker, "memory.high", BROKER_MEMORY_HIGH) < 0 ||
cgroup_write_file(ctx->broker, "memory.max", BROKER_MEMORY_MAX) < 0 ||
cgroup_write_file(ctx->broker, "memory.oom.group", "1") < 0)
die("cgroup broker memory limits");
}
if (ctx->pids_enabled &&
cgroup_write_file(ctx->broker, "pids.max", BROKER_PIDS_MAX) < 0)
die("cgroup broker pids.max");
if (ctx->cpu_enabled &&
cgroup_write_file(ctx->broker, "cpu.weight", BROKER_CPU_WEIGHT) < 0)
die("cgroup broker cpu.weight");
}
static void cgroup_apply_payload_limits(struct cgroup_ctx *ctx) {
char path[PATH_MAX];
if (ctx->memory_enabled) {
if (cgroup_write_file(ctx->payload, "memory.high", PAYLOAD_MEMORY_HIGH) < 0 ||
cgroup_write_file(ctx->payload, "memory.max", PAYLOAD_MEMORY_MAX) < 0 ||
cgroup_write_file(ctx->payload, "memory.oom.group", "1") < 0)
die("cgroup payload memory limits");
if (snprintf(path, sizeof(path), "%s/memory.swap.max", ctx->payload) <
(int)sizeof(path) &&
access(path, F_OK) == 0) {
if (cgroup_write_file(ctx->payload, "memory.swap.max", "0") < 0)
die("cgroup payload memory.swap.max");
ctx->payload_swap_enabled = 1;
} else {
DBG("cgroup payload memory.swap.max not available, skipping");
}
}
if (ctx->pids_enabled &&
cgroup_write_file(ctx->payload, "pids.max", PAYLOAD_PIDS_MAX) < 0)
die("cgroup payload pids.max");
if (ctx->cpu_enabled) {
if (cgroup_write_file(ctx->payload, "cpu.weight", PAYLOAD_CPU_WEIGHT) < 0)
die("cgroup payload cpu.weight");
if (snprintf(path, sizeof(path), "%s/cpu.max", ctx->payload) <
(int)sizeof(path) &&
access(path, F_OK) == 0) {
if (cgroup_write_file(ctx->payload, "cpu.max", PAYLOAD_CPU_MAX) < 0)
die("cgroup payload cpu.max");
ctx->payload_cpu_quota_enabled = 1;
} else {
DBG("cgroup payload cpu.max not available, skipping");
}
}
}
static void cgroup_report_active_summary(const struct cgroup_ctx *ctx) {
int partial = !ctx->cpu_enabled || !ctx->memory_enabled ||
!ctx->pids_enabled ||
(ctx->memory_enabled && !ctx->payload_swap_enabled) ||
(ctx->cpu_enabled && !ctx->payload_cpu_quota_enabled);
DBG("cgroup containment summary: root=%s controllers cpu=%s memory=%s "
"pids=%s payload-swap=%s payload-cpu-quota=%s",
ctx->root, ctx->cpu_enabled ? "yes" : "no",
ctx->memory_enabled ? "yes" : "no", ctx->pids_enabled ? "yes" : "no",
ctx->payload_swap_enabled ? "yes" : "no",
ctx->payload_cpu_quota_enabled ? "yes" : "no");
if (partial) {
cgroup_warn("cgroup containment partial: cpu=%s memory=%s pids=%s "
"payload-swap=%s payload-cpu-quota=%s",
ctx->cpu_enabled ? "yes" : "no",
ctx->memory_enabled ? "yes" : "no",
ctx->pids_enabled ? "yes" : "no",
ctx->payload_swap_enabled ? "yes" : "no",
ctx->payload_cpu_quota_enabled ? "yes" : "no");
}
}
static void cgroup_setup_containment(void) {
struct cgroup_ctx ctx;
int root_has_foreign_procs;
if (!detect_delegated_cgroup_root(&ctx)) {
DBG("cgroup containment inactive: no delegated writable subtree");
cgroup_warn("cgroup containment inactive: no delegated writable cgroup v2 "
"subtree");
return;
}
DBG("cgroup delegated root writable: %s", ctx.root);
root_has_foreign_procs = cgroup_root_has_foreign_procs(&ctx);
if (root_has_foreign_procs)
DBG("cgroup delegated root contains foreign pids; attempting leaf "
"placement before controller setup");
if (cgroup_mkdir_leaf(ctx.broker) < 0) {
if (errno == EACCES || errno == EPERM || errno == EROFS) {
DBG("cgroup containment inactive: cannot create broker leaf (%s)",
strerror(errno));
cgroup_warn("cgroup containment inactive: cannot create broker leaf "
"(%s)",
strerror(errno));
return;
}
die("cgroup mkdir broker");
}
if (cgroup_move_pid(ctx.broker, getpid()) < 0)
die("cgroup move self to broker");
if (cgroup_enable_controllers(&ctx, 1, 1, 1) < 0) {
int saved = errno;
if (cgroup_controller_failure_is_partial(saved)) {
DBG("cgroup containment partial: cannot enable delegated controllers "
"(%s)",
strerror(saved));
cgroup_warn("cgroup containment partial: broker/payload cgroup "
"placement active but controller limits unavailable (%s%s)",
root_has_foreign_procs ? "delegated root contains foreign "
"processes; "
: "",
strerror(saved));
cgroup_clear_controller_state(&ctx);
} else {
errno = saved;
cgroup_rollback_setup(&ctx);
die("cgroup enable controllers");
}
}
if (cgroup_mkdir_leaf(ctx.payload) < 0)
die("cgroup mkdir payload");
cgroup_apply_broker_limits(&ctx);
cgroup_apply_payload_limits(&ctx);
cgroup_report_active_summary(&ctx);
DBG("cgroup containment active under %s", ctx.root);
DBG("cgroup broker leaf: %s", ctx.broker);
DBG("cgroup payload leaf: %s", ctx.payload);
g_cgroup = ctx;
}
static int cgroup_move_child_to_payload(pid_t pid) {
if (!g_cgroup.active)
return 0;
return cgroup_move_pid(g_cgroup.payload, pid);
}
static int parse_flow_limit_option(const char *value, int max_value,
const char *name) {
long parsed;
if (parse_long_strict(value, 1, max_value, &parsed) < 0) {
fprintf(stderr, "Invalid %s: %s (expected 1..%d)\n", name, value,
max_value);
exit(1);
}
return (int)parsed;
}
static void close_middle_supervisor_fds(int ctl_fd, int sync_fd,
const int stdout_pipe[2],
const int stderr_pipe[2],
struct interactive_session *session) {
close(ctl_fd);
close(sync_fd);
if (!interactive_stdio) {
close(stdout_pipe[0]);
close(stdout_pipe[1]);
close(stderr_pipe[0]);
close(stderr_pipe[1]);
}
if (session->pty_slave_fd >= 0) {
close(session->pty_slave_fd);
session->pty_slave_fd = -1;
}
}
static void set_parent_death_signal(void) {
if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0)
die("PR_SET_PDEATHSIG");
}
static void arm_parent_death_signal(pid_t expected_parent) {
set_parent_death_signal();
if (getppid() != expected_parent)
_exit(1);
}
/* Inner PID 1 cannot use arm_parent_death_signal(): its parent (the middle
* supervisor) lives outside the new PID namespace, so getppid() returns 0
* whether or not the supervisor is alive. Instead the supervisor keeps the
* write end of a liveness pipe open for its lifetime; POLLHUP on the read
* end after arming PR_SET_PDEATHSIG means the supervisor died inside the
* fork-to-arm window and the death signal will never fire. */
static void arm_pid1_parent_death_signal(int liveness_rd) {
set_parent_death_signal();
struct pollfd pfd = {.fd = liveness_rd, .events = 0, .revents = 0};
int rc;
do {
rc = poll(&pfd, 1, 0);
} while (rc < 0 && errno == EINTR);
if (rc < 0)
die("poll pid1 parent liveness");
if (rc > 0) {
fprintf(stderr, "[sockpuppet] Error: middle supervisor died before "
"PID 1 armed parent-death signal\n");
_exit(1);
}
close(liveness_rd);
}
static void mkdir_if_missing(const char *path, mode_t mode) {
if (mkdir(path, mode) < 0 && errno != EEXIST)
die(path);
}
#if defined(__has_include)
#if __has_include(<linux/landlock.h>)
#include <linux/landlock.h>
#define SP_HAVE_LANDLOCK 1
#endif
#endif
#ifndef SP_HAVE_LANDLOCK
#define SP_HAVE_LANDLOCK 0
#endif
struct fs_sandbox {
char resolved_cwd[PATH_MAX];
char final_root[PATH_MAX];
int cwd_writable;
int cwd_readonly_fallback;
int cwd_readonly_mount;
int readonly_binds_enforced;
};
static void write_text_file(const char *path, const char *data) {
int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0)
die(path);
size_t len = strlen(data);
if (write(fd, data, len) != (ssize_t)len)
die("write");
close(fd);
}
static int path_exists(const char *path) {
struct stat st;
return stat(path, &st) == 0;
}
static void mkdir_parents(const char *path, mode_t mode) {
char tmp[PATH_MAX];
size_t len = strlen(path);
if (len >= sizeof(tmp))
die("mkdir_parents");
memcpy(tmp, path, len + 1);
for (char *p = tmp + 1; *p; ++p) {
if (*p != '/')
continue;
*p = '\0';
mkdir_if_missing(tmp, mode);
*p = '/';
}
mkdir_if_missing(tmp, mode);
}
static void ensure_parent_dir(const char *path, mode_t mode) {
char tmp[PATH_MAX];
char *slash;
if (strlen(path) >= sizeof(tmp))
die("ensure_parent_dir");
strcpy(tmp, path);
slash = strrchr(tmp, '/');
if (!slash)
return;
if (slash == tmp) {
mkdir_if_missing("/", mode);
return;
}
*slash = '\0';
mkdir_parents(tmp, mode);
}
static int path_contains(const char *base, const char *path) {
size_t len = strlen(base);
if (strcmp(base, "/") == 0)
return 1;
if (strncmp(base, path, len) != 0)
return 0;
return path[len] == '\0' || path[len] == '/';
}
static void path_append(char *dst, size_t dst_size, const char *root,
const char *suffix) {
if (snprintf(dst, dst_size, "%s%s", root, suffix) >= (int)dst_size)
die("path too long");
}
static void stage_path(char *dst, size_t dst_size, const char *base,
const char *suffix) {
path_append(dst, dst_size, base, suffix);
}
static void normalize_absolute_path(const char *path, char *out,
size_t out_size) {
const char *segments[PATH_MAX / 2];
size_t count = 0;
char tmp[PATH_MAX];
char *save = NULL;
char *tok;
if (strlen(path) >= sizeof(tmp))
die("normalize path");
strcpy(tmp, path);
for (tok = strtok_r(tmp, "/", &save); tok; tok = strtok_r(NULL, "/", &save)) {
if (strcmp(tok, ".") == 0 || *tok == '\0')
continue;
if (strcmp(tok, "..") == 0) {
if (count > 0)
--count;
continue;
}
segments[count++] = tok;
}
if (snprintf(out, out_size, "/") >= (int)out_size)
die("normalize path");
for (size_t i = 0; i < count; ++i) {
size_t used = strlen(out);
if (snprintf(out + used, out_size - used, "%s%s", i == 0 ? "" : "/",
segments[i]) >= (int)(out_size - used))
die("normalize path");
}
}
static void resolve_bind_target(const char *path, char *resolved,
size_t resolved_size) {
struct stat st;
char link[PATH_MAX];
char base[PATH_MAX];
char combined[PATH_MAX];
ssize_t len;
if (lstat(path, &st) < 0 || !S_ISLNK(st.st_mode)) {
if (strlen(path) >= resolved_size)
die("resolve bind target");
strcpy(resolved, path);
return;
}
len = readlink(path, link, sizeof(link) - 1);
if (len < 0)
die("readlink");
link[len] = '\0';
if (link[0] == '/') {
normalize_absolute_path(link, resolved, resolved_size);
return;
}
if (strlen(path) >= sizeof(base))
die("resolve bind target");
strcpy(base, path);
char *slash = strrchr(base, '/');
if (!slash)
die("resolve bind target");
if (slash == base) {
base[1] = '\0';
} else {
*slash = '\0';
}
if (snprintf(combined, sizeof(combined), "%s/%s", base, link) >=
(int)sizeof(combined))
die("resolve bind target");
normalize_absolute_path(combined, resolved, resolved_size);
}
static unsigned long bind_remount_flags(int readonly, int nosuid) {
unsigned long flags = MS_BIND | MS_REMOUNT | MS_REC;
if (readonly)
flags |= MS_RDONLY;
if (nosuid)
flags |= MS_NOSUID;
return flags;
}
static int remount_bind_with_flags(const char *dst, int readonly, int nosuid) {
if (mount(NULL, dst, NULL, bind_remount_flags(readonly, nosuid), NULL) == 0)
return 0;
if (readonly && nosuid) {
int saved = errno;
if (mount(NULL, dst, NULL, bind_remount_flags(1, 0), NULL) == 0) {
errno = saved;
DBG("nosuid remount skipped for %s (%s)", dst, strerror(errno));
return 0;
}
if ((errno == EPERM || errno == EINVAL || errno == EACCES) &&
mount(NULL, dst, NULL, bind_remount_flags(0, 1), NULL) == 0) {
int readonly_errno = errno;
DBG("readonly remount skipped for %s (%s)", dst,
strerror(readonly_errno));
return 0;
}
errno = saved;
return -1;
}
if (nosuid && !readonly &&
(errno == EPERM || errno == EINVAL || errno == EACCES)) {
DBG("nosuid remount skipped for %s (%s)", dst, strerror(errno));
return 0;
}
return -1;
}
static int remount_bind_readonly_best_effort(const char *dst, int nosuid) {
int saved;
if (mount(NULL, dst, NULL, bind_remount_flags(1, nosuid), NULL) == 0)
return 1;
saved = errno;
if (nosuid && mount(NULL, dst, NULL, bind_remount_flags(1, 0), NULL) == 0) {
DBG("nosuid remount skipped for %s (%s)", dst, strerror(saved));
return 1;
}
if (nosuid && mount(NULL, dst, NULL, bind_remount_flags(0, 1), NULL) == 0)
DBG("readonly remount skipped for %s (%s)", dst, strerror(saved));
else
DBG("readonly/nosuid remount skipped for %s (%s)", dst, strerror(saved));
return 0;
}
static void bind_mount_dir(const char *src, const char *dst, int readonly,
int nosuid) {
if (!path_exists(src))
return;
mkdir_parents(dst, 0755);
if (mount(src, dst, NULL, MS_BIND | MS_REC, NULL) < 0)
die(src);
if (!readonly && !nosuid)
return;
if (remount_bind_with_flags(dst, readonly, nosuid) < 0)
die(dst);
}
static int bind_mount_dir_readonly_fallback(const char *src, const char *dst,
int nosuid) {
if (!path_exists(src))
die(src);
mkdir_parents(dst, 0755);
if (mount(src, dst, NULL, MS_BIND | MS_REC, NULL) < 0)
die(src);
return remount_bind_readonly_best_effort(dst, nosuid);
}
static void bind_mount_file(const char *src, const char *dst, int nosuid) {
if (!path_exists(src))
return;
ensure_parent_dir(dst, 0755);
if (!path_exists(dst)) {
int fd = open(dst, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0)
die(dst);
close(fd);
}
if (mount(src, dst, NULL, MS_BIND, NULL) < 0)
die(src);
if (nosuid &&
mount(NULL, dst, NULL, MS_BIND | MS_REMOUNT | MS_NOSUID, NULL) < 0) {
if (errno == EPERM || errno == EINVAL || errno == EACCES) {
DBG("nosuid remount skipped for %s (%s)", dst, strerror(errno));
} else {
die(dst);
}
}
}
static void mount_procfs(const char *root) {
char path[PATH_MAX];
path_append(path, sizeof(path), root, "/proc");
mkdir_parents(path, 0555);
if (mount("proc", path, "proc", MS_NOSUID | MS_NODEV | MS_NOEXEC, "hidepid=2") < 0)
die("mount proc");
}
static void mount_private_tmp(const char *root) {
char path[PATH_MAX];
path_append(path, sizeof(path), root, "/tmp");
mkdir_parents(path, 01777);
if (mount("tmpfs", path, "tmpfs", MS_NODEV | MS_NOSUID,
"mode=1777,size=64m") < 0)
die("mount tmpfs /tmp");
}
static void mount_minimal_dev(const char *root) {
static const char *const dev_files[] = {
"/dev/null", "/dev/zero", "/dev/full", "/dev/random", "/dev/urandom",
};
char dev_root[PATH_MAX];
path_append(dev_root, sizeof(dev_root), root, "/dev");
mkdir_parents("/dev-min", 0755);
for (size_t i = 0; i < sizeof(dev_files) / sizeof(dev_files[0]); ++i) {
char src[PATH_MAX];
char dst[PATH_MAX];
path_append(src, sizeof(src), "/oldroot", dev_files[i]);
path_append(dst, sizeof(dst), "/dev-min", dev_files[i] + 4);
bind_mount_file(src, dst, 1);
}
/* /dev/tty resolves to the caller's controlling terminal. Only project it
* for interactive sessions, where the payload's ctty is the sandbox PTY.
* A noninteractive payload has no ctty and must not be able to reach the
* launcher's terminal through this node. */
if (interactive_stdio)
bind_mount_file("/oldroot/dev/tty", "/dev-min/tty", 1);
mkdir_parents("/dev-min/pts", 0755);
if (mount("devpts", "/dev-min/pts", "devpts",
MS_NOSUID | MS_NOEXEC, "newinstance,ptmxmode=0666,mode=0620") < 0)
die("mount devpts");
unlink("/dev-min/ptmx");
if (symlink("pts/ptmx", "/dev-min/ptmx") < 0)
die("symlink /dev/ptmx");
mkdir_parents(dev_root, 0755);
if (mount("/dev-min", dev_root, NULL, MS_BIND | MS_REC, NULL) < 0)
die("mount /dev");
if (mount(NULL, dev_root, NULL, MS_BIND | MS_REMOUNT | MS_REC | MS_NOSUID,
NULL) < 0)
die("mount /dev nosuid");
}
static void mount_runtime_tree(struct fs_sandbox *sandbox) {
static const char *const runtime_dirs[] = {
"/bin", "/sbin", "/usr", "/lib", "/lib64", "/etc", "/nix",
};
for (size_t i = 0; i < sizeof(runtime_dirs) / sizeof(runtime_dirs[0]); ++i) {
char src[PATH_MAX];
char dst[PATH_MAX];
const char *path = runtime_dirs[i];
if (path_contains(sandbox->resolved_cwd, path))
continue;
path_append(src, sizeof(src), "/oldroot", path);
path_append(dst, sizeof(dst), sandbox->final_root, path);
if (!path_exists(src))
continue;
if (!bind_mount_dir_readonly_fallback(src, dst, 1))
sandbox->readonly_binds_enforced = 0;
}
}
static void mount_resolv_conf(const char *root) {
char resolv_dst[PATH_MAX];
char mount_dst[PATH_MAX];
path_append(resolv_dst, sizeof(resolv_dst), root, "/etc/resolv.conf");
resolve_bind_target(resolv_dst, mount_dst, sizeof(mount_dst));
if (egress_mode == EGRESS_SOCKS && socks_proxy.remote_dns)
write_text_file("/resolv.conf.tmp", "nameserver 10.0.0.1\n");
else
write_text_file("/resolv.conf.tmp", "nameserver 8.8.8.8\n");
bind_mount_file("/resolv.conf.tmp", mount_dst, 1);
}
static int overlay_fallback_allowed_errno(int err) {
return err == EINVAL || err == ENODEV || err == EOPNOTSUPP ||
err == EPERM || err == EACCES;
}
static void die_writable_cwd_overlay_required(const struct fs_sandbox *sandbox,
int err) {
fprintf(stderr,
"Writable cwd overlay required but overlayfs is unavailable for %s "
"(%s)\n",
sandbox->resolved_cwd, strerror(err));
exit(1);
}
static void mount_overlay_cwd(struct fs_sandbox *sandbox) {
char lower_src[PATH_MAX];
char opts[4096];
int force_readonly_fallback =
getenv("SOCKPUPPET_TEST_FORCE_CWD_FALLBACK") != NULL;
path_append(lower_src, sizeof(lower_src), "/oldroot", sandbox->resolved_cwd);
bind_mount_dir(lower_src, "/lower", 0, 0);
mkdir_parents("/upper", 0700);
mkdir_parents("/work", 0700);
mkdir_parents("/merged", 0755);
if (snprintf(opts, sizeof(opts),
"lowerdir=/lower,upperdir=/upper,workdir=/work,userxattr") >=
(int)sizeof(opts))
die("overlay options too long");
if (!force_readonly_fallback &&
mount("overlay", "/merged", "overlay", 0, opts) == 0) {
if (mount(NULL, "/merged", NULL, MS_REMOUNT | MS_NOSUID, NULL) < 0)
die("mount overlay nosuid");
sandbox->cwd_writable = 1;
return;
}
int overlay_errno = force_readonly_fallback ? EOPNOTSUPP : errno;
if (!overlay_fallback_allowed_errno(overlay_errno)) {
errno = overlay_errno;
die("mount overlay");
}
if (require_writable_cwd)
die_writable_cwd_overlay_required(sandbox, overlay_errno);
fprintf(stderr,
"[sockpuppet] Warning: cwd overlay unavailable for %s (%s); using "
"read-only bind fallback\n",
sandbox->resolved_cwd, strerror(overlay_errno));
sandbox->cwd_readonly_mount =
bind_mount_dir_readonly_fallback("/lower", "/merged", 1);
if (!sandbox->cwd_readonly_mount)
sandbox->readonly_binds_enforced = 0;
sandbox->cwd_writable = 0;
sandbox->cwd_readonly_fallback = 1;
}
static void setup_final_root(struct fs_sandbox *sandbox) {
char dst[PATH_MAX];
if (strcmp(sandbox->resolved_cwd, "/") == 0) {
strcpy(sandbox->final_root, "/merged");
} else {
strcpy(sandbox->final_root, "/sandbox");
mkdir_parents(sandbox->final_root, 0755);
mount_runtime_tree(sandbox);
if (!path_contains(sandbox->resolved_cwd, "/tmp"))
mount_private_tmp(sandbox->final_root);
path_append(dst, sizeof(dst), sandbox->final_root, sandbox->resolved_cwd);
mkdir_parents(dst, 0755);
if (mount("/merged", dst, NULL, MS_BIND | MS_REC, NULL) < 0)
die("mount cwd overlay");
if (sandbox->cwd_readonly_fallback) {
int dst_readonly = remount_bind_readonly_best_effort(dst, 1);
sandbox->cwd_readonly_mount =
sandbox->cwd_readonly_mount && dst_readonly;
if (!dst_readonly)
sandbox->readonly_binds_enforced = 0;
} else if (mount(NULL, dst, NULL,
MS_BIND | MS_REMOUNT | MS_REC | MS_NOSUID, NULL) < 0) {
die("mount cwd overlay nosuid");
}
}
if (strcmp(sandbox->resolved_cwd, "/") == 0 &&
!path_contains(sandbox->resolved_cwd, "/tmp"))
mount_private_tmp(sandbox->final_root);
mount_procfs(sandbox->final_root);
mount_minimal_dev(sandbox->final_root);
mount_resolv_conf(sandbox->final_root);
}
static void detach_oldroot(void) {
if (umount2("/oldroot", MNT_DETACH) < 0)
die("umount oldroot");
if (rmdir("/oldroot") < 0 && errno != ENOENT)
die("rmdir oldroot");
}
static void prepare_fs_sandbox(struct fs_sandbox *sandbox, const char *cwd,
const char *base) {
char path[PATH_MAX];
if (!unsafe_share_cwd && (strcmp(cwd, "/") == 0 || strncmp(cwd, "/home/", 6) == 0 || strcmp(cwd, "/root") == 0)) {
fprintf(stderr, "Unsafe working directory %s. Use --unsafe-share-cwd\n", cwd);
exit(1);
}
if (!realpath(cwd, sandbox->resolved_cwd))
die("realpath cwd");
sandbox->cwd_writable = 0;
sandbox->cwd_readonly_fallback = 0;
sandbox->cwd_readonly_mount = 0;
sandbox->readonly_binds_enforced = 1;
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
die("mount MS_PRIVATE");
if (mount("tmpfs", base, "tmpfs", MS_NODEV | MS_NOSUID,
"mode=0700,size=128m") < 0)
die("mount tmpfs overlay base");
stage_path(path, sizeof(path), base, "/oldroot");
mkdir_parents(path, 0755);
stage_path(path, sizeof(path), base, "/dev/net");
mkdir_parents(path, 0755);
stage_path(path, sizeof(path), base, "/lower");
mkdir_parents(path, 0755);
stage_path(path, sizeof(path), base, "/upper");
mkdir_parents(path, 0700);
stage_path(path, sizeof(path), base, "/work");
mkdir_parents(path, 0700);
stage_path(path, sizeof(path), base, "/merged");
mkdir_parents(path, 0755);
stage_path(path, sizeof(path), base, "/sandbox");
mkdir_parents(path, 0755);
stage_path(path, sizeof(path), base, "/dev-min");
mkdir_parents(path, 0755);
stage_path(path, sizeof(path), base, "/resolv.conf.tmp");
ensure_parent_dir(path, 0755);
if (chdir(base) < 0)
die("chdir overlay base");
if (syscall(SYS_pivot_root, ".", "oldroot") < 0)
die("pivot_root");
if (chdir("/") < 0)
die("chdir /");
mkdir_parents("/dev", 0755);
mkdir_parents("/dev/net", 0755);
if (path_exists("/oldroot/dev/net")) {
if (mount("/oldroot/dev/net", "/dev/net", NULL, MS_BIND | MS_REC, NULL) < 0)
die("bind /dev/net");
if (remount_bind_with_flags("/dev/net", 0, 1) < 0)
die("bind /dev/net nosuid");
}
path_append(path, sizeof(path), "/oldroot", base);
if (path_exists(path) &&
mount("tmpfs", path, "tmpfs", MS_NODEV | MS_NOSUID,
"mode=0000,size=4k") < 0)
die("hide overlay base");
mount_overlay_cwd(sandbox);
setup_final_root(sandbox);
detach_oldroot();
}
static void enter_fs_sandbox(const struct fs_sandbox *sandbox) {
if (chroot(sandbox->final_root) < 0)
die("chroot sandbox");
if (chdir(sandbox->resolved_cwd) < 0)
die("chdir sandbox cwd");
}
static int ensure_no_new_privs(void) {
return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
}
static void require_no_new_privs_for_exec(void) {
if (ensure_no_new_privs() < 0)
die("PR_SET_NO_NEW_PRIVS");
}
static uint64_t landlock_read_exec_rights(void) {
#if SP_HAVE_LANDLOCK
return LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR;
#else
return 0;
#endif
}
static uint64_t landlock_write_rights_for_abi(int abi) {
uint64_t rights = 0;
#if SP_HAVE_LANDLOCK
(void)abi;
rights = LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_DIR |
LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_CHAR |
LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO |
LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM;
#ifdef LANDLOCK_ACCESS_FS_REFER
if (abi >= 2)
rights |= LANDLOCK_ACCESS_FS_REFER;
#endif
#ifdef LANDLOCK_ACCESS_FS_TRUNCATE
if (abi >= 3)
rights |= LANDLOCK_ACCESS_FS_TRUNCATE;
#endif
#else
(void)abi;
#endif
return rights;
}
static int add_landlock_rule(int ruleset_fd, const char *path,
uint64_t allowed_access) {
#if SP_HAVE_LANDLOCK && defined(__NR_landlock_add_rule)
int dirfd = open(path, O_PATH | O_CLOEXEC);
if (dirfd < 0)
return -1;
struct landlock_path_beneath_attr rule = {
.allowed_access = allowed_access,
.parent_fd = dirfd,
};
int rc = (int)syscall(__NR_landlock_add_rule, ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH, &rule, 0);
close(dirfd);
return rc;
#else
(void)ruleset_fd;
(void)path;
(void)allowed_access;
errno = ENOSYS;
return -1;
#endif
}
static int landlock_unavailable(const struct fs_sandbox *sandbox,
const char *reason) {
if (!sandbox->readonly_binds_enforced) {
fprintf(stderr,
"[sockpuppet] Warning: read-only bind fallback requires Landlock "
"because one or more read-only remounts were unavailable (%s)\n",
reason);
errno = EOPNOTSUPP;
return -1;
}
fprintf(stderr,
"[sockpuppet] Warning: Landlock not supported by kernel, "
"continuing without filesystem sandbox\n");
return 0;
}
static int apply_landlock_policy(const struct fs_sandbox *sandbox) {
#if SP_HAVE_LANDLOCK && defined(__NR_landlock_create_ruleset) && \
defined(__NR_landlock_restrict_self)
int abi = (int)syscall(__NR_landlock_create_ruleset, NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 0) {
if (errno == ENOSYS || errno == EOPNOTSUPP)
return landlock_unavailable(sandbox, strerror(errno));
return -1;
}
uint64_t read_exec = landlock_read_exec_rights();
uint64_t write_rights = landlock_write_rights_for_abi(abi);
struct landlock_ruleset_attr ruleset = {
.handled_access_fs = read_exec | write_rights,
};
int ruleset_fd = (int)syscall(__NR_landlock_create_ruleset, &ruleset,
sizeof(ruleset), 0);
if (ruleset_fd < 0) {
if (errno == ENOSYS || errno == EOPNOTSUPP)
return landlock_unavailable(sandbox, strerror(errno));
return -1;
}
if (add_landlock_rule(ruleset_fd, "/", read_exec) < 0 ||
add_landlock_rule(ruleset_fd, sandbox->resolved_cwd,
read_exec |
(sandbox->cwd_writable ? write_rights : 0)) < 0 ||
add_landlock_rule(ruleset_fd, "/tmp", read_exec | write_rights) < 0 ||
add_landlock_rule(ruleset_fd, "/dev", LANDLOCK_ACCESS_FS_READ_DIR) < 0 ||
add_landlock_rule(ruleset_fd, "/dev/pts",
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_WRITE_FILE) < 0 ||
add_landlock_rule(ruleset_fd, "/dev/null",
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_WRITE_FILE) < 0 ||
/* /dev/tty only exists in interactive sandboxes (mount_minimal_dev). */
(interactive_stdio &&
add_landlock_rule(ruleset_fd, "/dev/tty",
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_WRITE_FILE) < 0) ||
add_landlock_rule(ruleset_fd, "/dev/ptmx",
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_WRITE_FILE) < 0 ||
add_landlock_rule(ruleset_fd, "/dev/zero",
LANDLOCK_ACCESS_FS_READ_FILE) < 0 ||
add_landlock_rule(ruleset_fd, "/dev/full",
LANDLOCK_ACCESS_FS_READ_FILE) < 0 ||
add_landlock_rule(ruleset_fd, "/dev/random",
LANDLOCK_ACCESS_FS_READ_FILE) < 0 ||
add_landlock_rule(ruleset_fd, "/dev/urandom",
LANDLOCK_ACCESS_FS_READ_FILE) < 0) {
close(ruleset_fd);
return -1;
}
if (ensure_no_new_privs() < 0) {
close(ruleset_fd);
return -1;
}
if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) < 0) {
close(ruleset_fd);
return -1;
}
close(ruleset_fd);
return 0;
#else
return landlock_unavailable(sandbox, "not compiled in");
#endif
}
static int parent_landlock_unavailable(int err, const char *reason) {
fprintf(stderr, "sockpuppet: parent Landlock unavailable (%s)\n", reason);
errno = err;
return -1;
}
static int apply_parent_landlock_policy(void) {
if (getenv("SOCKPUPPET_TEST_PARENT_LANDLOCK_UNAVAILABLE") != NULL)
return parent_landlock_unavailable(ENOSYS, "test override");
#if SP_HAVE_LANDLOCK && defined(__NR_landlock_create_ruleset) && \
defined(__NR_landlock_restrict_self)
int abi = (int)syscall(__NR_landlock_create_ruleset, NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 0) {
int saved = errno;
if (saved == ENOSYS || saved == EOPNOTSUPP)
return parent_landlock_unavailable(saved, strerror(saved));
return -1;
}
struct landlock_ruleset_attr ruleset = {
.handled_access_fs = landlock_read_exec_rights() |
landlock_write_rights_for_abi(abi),
};
int ruleset_fd = (int)syscall(__NR_landlock_create_ruleset, &ruleset,
sizeof(ruleset), 0);
if (ruleset_fd < 0) {
int saved = errno;
if (saved == ENOSYS || saved == EOPNOTSUPP)
return parent_landlock_unavailable(saved, strerror(saved));
return -1;
}
if (ensure_no_new_privs() < 0) {
close(ruleset_fd);
return -1;
}
if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) < 0) {
close(ruleset_fd);
return -1;
}
close(ruleset_fd);
return 0;
#else
return parent_landlock_unavailable(ENOSYS, "not compiled in");
#endif
}
static int parent_harden_probe_enabled(void) {
return getenv("SOCKPUPPET_TEST_PARENT_HARDEN_PROBE") != NULL;
}
static int parent_harden_probe_landlock(void) {
if (!parent_harden_probe_enabled())
return 0;
errno = 0;
int fd = open("/etc/passwd", O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
close(fd);
fprintf(stderr,
"sockpuppet: parent hardening probe failed: Landlock allowed "
"open(/etc/passwd)\n");
errno = EPERM;
return -1;
}
if (errno != EACCES && errno != EPERM) {
int saved = errno;
fprintf(stderr,
"sockpuppet: parent hardening probe failed: open(/etc/passwd) "
"returned %s\n",
strerror(saved));
errno = saved;
return -1;
}
fprintf(stderr,
"sockpuppet: parent hardening probe: Landlock denied open\n");
return 0;
}
static int parent_harden_probe_seccomp(const struct parent_harden_config *cfg) {
if (!parent_harden_probe_enabled())
return 0;
#ifdef __NR_clone3
errno = 0;
if (syscall(__NR_clone3, NULL, 0) != -1 || errno != EPERM) {
int saved = errno;
fprintf(stderr,
"sockpuppet: parent hardening probe failed: clone3 denial was "
"%s\n",
strerror(saved));
errno = saved ? saved : EPERM;
return -1;
}
fprintf(stderr,
"sockpuppet: parent hardening probe: seccomp denied clone3\n");
#else
fprintf(stderr,
"sockpuppet: parent hardening probe: clone3 unavailable at build "
"time\n");
#endif
#ifdef __NR_kill
if (cfg->child_pid > 0) {
errno = 0;
if (kill(cfg->child_pid, 0) != -1 || errno != EPERM) {
int saved = errno;
fprintf(stderr,
"sockpuppet: parent hardening probe failed: non-SIGKILL child "
"kill denial was %s\n",
strerror(saved));
errno = saved ? saved : EPERM;
return -1;
}
fprintf(stderr,
"sockpuppet: parent hardening probe: seccomp denied non-SIGKILL "
"child kill\n");
}
#endif
if (!cfg->outbound_sockets_allowed) {
errno = 0;
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd >= 0) {
close(fd);
fprintf(stderr,
"sockpuppet: parent hardening probe failed: egress-none parent "
"socket() succeeded\n");
errno = EPERM;
return -1;
}
if (errno != EPERM) {
int saved = errno;
fprintf(stderr,
"sockpuppet: parent hardening probe failed: egress-none "
"socket() denial was %s\n",
strerror(saved));
errno = saved;
return -1;
}
fprintf(stderr,
"sockpuppet: parent hardening probe: egress-none socket denied\n");
}
fprintf(stderr, "sockpuppet: parent hardening probe passed\n");
return 0;
}
#ifndef SECCOMP_RET_KILL_PROCESS
#define SECCOMP_RET_KILL_PROCESS SECCOMP_RET_KILL
#endif
#ifndef __X32_SYSCALL_BIT
#define __X32_SYSCALL_BIT 0x40000000U
#endif
#define SP_CLONE_NAMESPACE_FLAGS_BASE \
(CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWIPC | \
CLONE_NEWUTS | CLONE_NEWPID)
#ifdef CLONE_NEWCGROUP
#define SP_CLONE_NAMESPACE_FLAGS_CGROUP | CLONE_NEWCGROUP
#else
#define SP_CLONE_NAMESPACE_FLAGS_CGROUP
#endif
#ifdef CLONE_NEWTIME
#define SP_CLONE_NAMESPACE_FLAGS_TIME | CLONE_NEWTIME
#else
#define SP_CLONE_NAMESPACE_FLAGS_TIME
#endif
#define SP_CLONE_NAMESPACE_FLAGS \
(SP_CLONE_NAMESPACE_FLAGS_BASE SP_CLONE_NAMESPACE_FLAGS_CGROUP \
SP_CLONE_NAMESPACE_FLAGS_TIME)
#ifdef CLONE_UNTRACED
#define SP_CLONE_UNTRACED_FLAG CLONE_UNTRACED
#else
#define SP_CLONE_UNTRACED_FLAG 0
#endif
#define SP_CLONE_BLOCKED_FLAGS \
(SP_CLONE_NAMESPACE_FLAGS | SP_CLONE_UNTRACED_FLAG)
#ifdef CLONE_INTO_CGROUP
#define SP_CLONE_INTO_CGROUP_FLAG CLONE_INTO_CGROUP
#else
#define SP_CLONE_INTO_CGROUP_FLAG 0
#endif
#ifndef CLONE_ARGS_SIZE_VER0
#define CLONE_ARGS_SIZE_VER0 64
#endif
#define SP_CLONE_SIGNAL_MASK 0xffULL
#define SP_CLONE3_MAX_USER_SIZE 4096ULL
#define SP_CLONE3_UNSAFE_FLAGS \
((unsigned long long)SP_CLONE_BLOCKED_FLAGS | \
(unsigned long long)SP_CLONE_INTO_CGROUP_FLAG)
#define SP_CLONE3_SUPPORTED_FLAGS \
((unsigned long long)CLONE_VM | (unsigned long long)CLONE_FS | \
(unsigned long long)CLONE_FILES | (unsigned long long)CLONE_SIGHAND | \
(unsigned long long)CLONE_THREAD | (unsigned long long)CLONE_SYSVSEM | \
(unsigned long long)CLONE_SETTLS | \
(unsigned long long)CLONE_PARENT_SETTID | \
(unsigned long long)CLONE_CHILD_SETTID | \
(unsigned long long)CLONE_CHILD_CLEARTID | \
(unsigned long long)CLONE_PARENT | (unsigned long long)CLONE_VFORK)
enum sp_clone3_seccomp_mode {
SP_CLONE3_SECCOMP_TRACE_TRANSLATE,
SP_CLONE3_SECCOMP_ENOSYS,
};
#define SP_CLONE3_TRACE_MODE_TRANSLATE 'T'
#define SP_CLONE3_TRACE_MODE_ENOSYS 'E'
#define SP_SECCOMP_KILL SECCOMP_RET_KILL_PROCESS
#define SP_SECCOMP_DENY_NR(nr) \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)(nr), 0, 1), \
BPF_STMT(BPF_RET | BPF_K, SP_SECCOMP_KILL)
#define SP_SECCOMP_ERRNO_NR(nr, err) \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)(nr), 0, 1), \
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ((err) & SECCOMP_RET_DATA))
#define SP_SECCOMP_TRACE_NR(nr, token) \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)(nr), 0, 1), \
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE | \
((token) & SECCOMP_RET_DATA))
#define SP_SECCOMP_ALLOW() BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
#define SP_SECCOMP_CLONE3_TRACE_TOKEN 0x0c13U
#ifndef AUDIT_ARCH_AARCH64
#define AUDIT_ARCH_AARCH64 \
(__AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE | 183)
#endif
#ifndef AUDIT_ARCH_RISCV64
#define AUDIT_ARCH_RISCV64 \
(__AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE | 243)
#endif
#if defined(__x86_64__)
#define SP_AUDIT_ARCH AUDIT_ARCH_X86_64
#elif defined(__aarch64__)
#define SP_AUDIT_ARCH AUDIT_ARCH_AARCH64
#elif defined(__riscv) && __riscv_xlen == 64
#define SP_AUDIT_ARCH AUDIT_ARCH_RISCV64
#else
#define SP_AUDIT_ARCH 0
#endif
enum {
SP_PARENT_SECCOMP_MAX_FILTER = 192,
SP_PARENT_SECCOMP_ERRNO = SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA),
/* Linux termios2 ioctl values. glibc may use these instead of the legacy
* TCSETS family when the saved termios carries explicit input/output
* speeds. They have the same UAPI values on the supported 64-bit Linux
* architectures (x86_64, aarch64, and riscv64). */
SP_IOCTL_TCSETS2 = 0x402c542bU,
SP_IOCTL_TCSETSW2 = 0x402c542cU,
SP_IOCTL_TCSETSF2 = 0x402c542dU,
};
static int sp_bpf_append(struct sock_filter *filter, size_t filter_cap,
size_t *filter_len, struct sock_filter insn) {
if (*filter_len >= filter_cap) {
errno = E2BIG;
return -1;
}
filter[*filter_len] = insn;
(*filter_len)++;
return 0;
}
static int sp_bpf_set_jump(unsigned char *field, size_t from, size_t to) {
size_t offset;
if (to <= from || to - from - 1U > UINT8_MAX) {
errno = E2BIG;
return -1;
}
offset = to - from - 1U;
*field = (unsigned char)offset;
return 0;
}
static int parent_seccomp_append_allow_nr(struct sock_filter *filter,
size_t filter_cap,
size_t *filter_len,
unsigned int nr) {
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
nr, 0, 1)) < 0)
return -1;
return sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)SP_SECCOMP_ALLOW());
}
/* Allow nr(arg0, SIGKILL, ...) and deny nr with any other arguments. Only
* the low argument words are compared, which matches how the kernel
* truncates pid/fd/signal arguments to int. Used to pin fatal-teardown
* signaling to exactly the namespace child. */
static int parent_seccomp_append_child_signal_allow(struct sock_filter *filter,
size_t filter_cap,
size_t *filter_len,
unsigned int nr,
unsigned int arg0) {
size_t nr_idx;
size_t arg0_idx;
size_t sig_idx;
size_t deny_idx;
size_t allow_idx;
size_t end_idx;
nr_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
nr, 0, 0)) < 0)
return -1;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(
BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data,
args[0]))) < 0)
return -1;
arg0_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
arg0, 0, 0)) < 0)
return -1;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(
BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data,
args[1]))) < 0)
return -1;
sig_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
(unsigned int)SIGKILL, 0,
0)) < 0)
return -1;
deny_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(BPF_RET | BPF_K,
SP_PARENT_SECCOMP_ERRNO)) <
0)
return -1;
allow_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)SP_SECCOMP_ALLOW()) < 0)
return -1;
end_idx = *filter_len;
if (sp_bpf_set_jump(&filter[nr_idx].jf, nr_idx, end_idx) < 0 ||
sp_bpf_set_jump(&filter[arg0_idx].jf, arg0_idx, deny_idx) < 0 ||
sp_bpf_set_jump(&filter[sig_idx].jt, sig_idx, allow_idx) < 0)
return -1;
return 0;
}
static int parent_seccomp_append_socket_allow(struct sock_filter *filter,
size_t filter_cap,
size_t *filter_len) {
#ifdef __NR_socket
size_t nr_idx = *filter_len;
size_t domain_idx;
size_t stream_idx;
size_t dgram_idx;
size_t deny_idx;
size_t allow_idx;
size_t end_idx;
unsigned int type_mask =
~((unsigned int)SOCK_CLOEXEC | (unsigned int)SOCK_NONBLOCK);
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
(unsigned int)__NR_socket, 0,
0)) < 0)
return -1;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(
BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data,
args[0]))) < 0)
return -1;
domain_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
(unsigned int)AF_INET, 0,
0)) < 0)
return -1;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(
BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data,
args[1]))) < 0)
return -1;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(BPF_ALU | BPF_AND | BPF_K,
type_mask)) < 0)
return -1;
stream_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
(unsigned int)SOCK_STREAM,
0, 0)) < 0)
return -1;
dgram_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
(unsigned int)SOCK_DGRAM, 0,
0)) < 0)
return -1;
deny_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(BPF_RET | BPF_K,
SP_PARENT_SECCOMP_ERRNO)) <
0)
return -1;
allow_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)SP_SECCOMP_ALLOW()) < 0)
return -1;
end_idx = *filter_len;
if (sp_bpf_set_jump(&filter[nr_idx].jf, nr_idx, end_idx) < 0 ||
sp_bpf_set_jump(&filter[domain_idx].jf, domain_idx, deny_idx) < 0 ||
sp_bpf_set_jump(&filter[stream_idx].jt, stream_idx, allow_idx) < 0 ||
sp_bpf_set_jump(&filter[dgram_idx].jt, dgram_idx, allow_idx) < 0)
return -1;
#else
(void)filter;
(void)filter_cap;
(void)filter_len;
#endif
return 0;
}
static int parent_seccomp_append_ioctl_allow(struct sock_filter *filter,
size_t filter_cap,
size_t *filter_len) {
#ifdef __NR_ioctl
unsigned int allowed_cmds[8];
size_t cmd_count = 0;
size_t cmd_idxs[8];
size_t nr_idx;
size_t allow_idx;
size_t end_idx;
#ifdef TIOCGWINSZ
allowed_cmds[cmd_count++] = (unsigned int)TIOCGWINSZ;
#endif
#ifdef TIOCSWINSZ
allowed_cmds[cmd_count++] = (unsigned int)TIOCSWINSZ;
#endif
#ifdef TCSETS
allowed_cmds[cmd_count++] = (unsigned int)TCSETS;
#endif
#ifdef TCSETSW
allowed_cmds[cmd_count++] = (unsigned int)TCSETSW;
#endif
#ifdef TCSETSF
allowed_cmds[cmd_count++] = (unsigned int)TCSETSF;
#endif
allowed_cmds[cmd_count++] = SP_IOCTL_TCSETS2;
allowed_cmds[cmd_count++] = SP_IOCTL_TCSETSW2;
allowed_cmds[cmd_count++] = SP_IOCTL_TCSETSF2;
if (cmd_count == 0)
return 0;
nr_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
(unsigned int)__NR_ioctl, 0,
0)) < 0)
return -1;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(
BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data,
args[1]))) < 0)
return -1;
for (size_t i = 0; i < cmd_count; ++i) {
cmd_idxs[i] = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
allowed_cmds[i], 0, 0)) <
0)
return -1;
}
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)BPF_STMT(BPF_RET | BPF_K,
SP_PARENT_SECCOMP_ERRNO)) <
0)
return -1;
allow_idx = *filter_len;
if (sp_bpf_append(filter, filter_cap, filter_len,
(struct sock_filter)SP_SECCOMP_ALLOW()) < 0)
return -1;
end_idx = *filter_len;
if (sp_bpf_set_jump(&filter[nr_idx].jf, nr_idx, end_idx) < 0)
return -1;
for (size_t i = 0; i < cmd_count; ++i) {
if (sp_bpf_set_jump(&filter[cmd_idxs[i]].jt, cmd_idxs[i], allow_idx) < 0)
return -1;
}
#else
(void)filter;
(void)filter_cap;
(void)filter_len;
#endif
return 0;
}
static int apply_parent_seccomp(const struct parent_harden_config *cfg) {
if (getenv("SOCKPUPPET_TEST_PARENT_SECCOMP_UNAVAILABLE") != NULL) {
fprintf(stderr,
"sockpuppet: parent seccomp unavailable (test override)\n");
errno = ENOSYS;
return -1;
}
#if SP_AUDIT_ARCH != 0
struct sock_filter filter[SP_PARENT_SECCOMP_MAX_FILTER];
size_t filter_len = 0;
if (sp_bpf_append(filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(struct sock_filter)BPF_STMT(
BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, arch))) <
0)
return -1;
if (sp_bpf_append(filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
SP_AUDIT_ARCH, 1, 0)) < 0)
return -1;
if (sp_bpf_append(filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(struct sock_filter)BPF_STMT(BPF_RET | BPF_K,
SP_SECCOMP_KILL)) < 0)
return -1;
if (sp_bpf_append(filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(struct sock_filter)BPF_STMT(
BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, nr))) < 0)
return -1;
#if defined(__x86_64__)
if (sp_bpf_append(filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K,
__X32_SYSCALL_BIT, 0, 1)) <
0)
return -1;
if (sp_bpf_append(filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(struct sock_filter)BPF_STMT(BPF_RET | BPF_K,
SP_SECCOMP_KILL)) < 0)
return -1;
#endif
#ifdef __NR_read
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len, (unsigned int)__NR_read) < 0)
return -1;
#endif
#ifdef __NR_write
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_write) < 0)
return -1;
#endif
#ifdef __NR_close
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_close) < 0)
return -1;
#endif
#ifdef __NR_exit
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len, (unsigned int)__NR_exit) < 0)
return -1;
#endif
#ifdef __NR_exit_group
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_exit_group) < 0)
return -1;
#endif
#ifdef __NR_wait4
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len, (unsigned int)__NR_wait4) <
0)
return -1;
#endif
#ifdef __NR_epoll_ctl
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_epoll_ctl) < 0)
return -1;
#endif
#ifdef __NR_epoll_wait
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_epoll_wait) < 0)
return -1;
#endif
#ifdef __NR_epoll_pwait
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_epoll_pwait) < 0)
return -1;
#endif
#ifdef __NR_epoll_pwait2
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_epoll_pwait2) < 0)
return -1;
#endif
#ifdef __NR_fcntl
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_fcntl) < 0)
return -1;
#endif
#ifdef __NR_getrandom
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_getrandom) < 0)
return -1;
#endif
#ifdef __NR_clock_gettime
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_clock_gettime) < 0)
return -1;
#endif
#ifdef __NR_time
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len, (unsigned int)__NR_time) < 0)
return -1;
#endif
#ifdef __NR_rt_sigreturn
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_rt_sigreturn) < 0)
return -1;
#endif
#ifdef __NR_restart_syscall
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_restart_syscall) < 0)
return -1;
#endif
#ifdef __NR_accept4
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_accept4) < 0)
return -1;
#endif
#ifdef __NR_getpeername
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_getpeername) < 0)
return -1;
#endif
#ifdef __NR_getsockopt
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_getsockopt) < 0)
return -1;
#endif
#ifdef __NR_recvfrom
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_recvfrom) < 0)
return -1;
#endif
#ifdef __NR_recvmsg
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_recvmsg) < 0)
return -1;
#endif
#ifdef __NR_recv
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len, (unsigned int)__NR_recv) < 0)
return -1;
#endif
#ifdef __NR_sendto
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_sendto) < 0)
return -1;
#endif
#ifdef __NR_sendmsg
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_sendmsg) < 0)
return -1;
#endif
#ifdef __NR_send
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len, (unsigned int)__NR_send) < 0)
return -1;
#endif
#ifdef __NR_shutdown
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_shutdown) < 0)
return -1;
#endif
/* Fatal-teardown signaling: SIGKILL to exactly the namespace child,
* via its pidfd or its (never-reused-while-unreaped) pid. Every other
* target or signal stays on the errno-deny default. */
#ifdef __NR_pidfd_send_signal
if (cfg->child_pidfd >= 0 &&
parent_seccomp_append_child_signal_allow(
filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(unsigned int)__NR_pidfd_send_signal,
(unsigned int)cfg->child_pidfd) < 0)
return -1;
#endif
#ifdef __NR_kill
if (cfg->child_pid > 0 &&
parent_seccomp_append_child_signal_allow(
filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(unsigned int)__NR_kill, (unsigned int)cfg->child_pid) < 0)
return -1;
#endif
if (cfg->interactive &&
parent_seccomp_append_ioctl_allow(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len) < 0)
return -1;
if (cfg->outbound_sockets_allowed) {
if (parent_seccomp_append_socket_allow(filter,
SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len) < 0)
return -1;
#ifdef __NR_connect
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len,
(unsigned int)__NR_connect) < 0)
return -1;
#endif
#ifdef __NR_bind
if (parent_seccomp_append_allow_nr(filter, SP_PARENT_SECCOMP_MAX_FILTER,
&filter_len, (unsigned int)__NR_bind) <
0)
return -1;
#endif
#ifdef __NR_getsockname
if (parent_seccomp_append_allow_nr(
filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(unsigned int)__NR_getsockname) < 0)
return -1;
#endif
}
if (sp_bpf_append(filter, SP_PARENT_SECCOMP_MAX_FILTER, &filter_len,
(struct sock_filter)BPF_STMT(BPF_RET | BPF_K,
SP_PARENT_SECCOMP_ERRNO)) <
0)
return -1;
if (filter_len > USHRT_MAX) {
errno = E2BIG;
return -1;
}
struct sock_fprog prog = {
.len = (unsigned short)filter_len,
.filter = filter,
};
if (ensure_no_new_privs() < 0)
return -1;
return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
#else
(void)cfg;
fprintf(stderr, "sockpuppet: parent seccomp unsupported on this architecture\n");
errno = ENOSYS;
return -1;
#endif
}
static int apply_child_seccomp(enum sp_clone3_seccomp_mode clone3_mode) {
#if SP_AUDIT_ARCH != 0
static const struct sock_filter filter_clone3_trace[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SP_AUDIT_ARCH, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SP_SECCOMP_KILL),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, nr)),
#if defined(__x86_64__)
BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, __X32_SYSCALL_BIT, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SP_SECCOMP_KILL),
#endif
#ifdef __NR_unshare
SP_SECCOMP_DENY_NR(__NR_unshare),
#endif
#ifdef __NR_setns
SP_SECCOMP_DENY_NR(__NR_setns),
#endif
#ifdef __NR_mount
SP_SECCOMP_DENY_NR(__NR_mount),
#endif
#ifdef __NR_umount2
SP_SECCOMP_DENY_NR(__NR_umount2),
#endif
#ifdef __NR_pivot_root
SP_SECCOMP_DENY_NR(__NR_pivot_root),
#endif
#ifdef __NR_open_tree
SP_SECCOMP_DENY_NR(__NR_open_tree),
#endif
#ifdef __NR_move_mount
SP_SECCOMP_DENY_NR(__NR_move_mount),
#endif
#ifdef __NR_fsopen
SP_SECCOMP_DENY_NR(__NR_fsopen),
#endif
#ifdef __NR_fsconfig
SP_SECCOMP_DENY_NR(__NR_fsconfig),
#endif
#ifdef __NR_fsmount
SP_SECCOMP_DENY_NR(__NR_fsmount),
#endif
#ifdef __NR_fspick
SP_SECCOMP_DENY_NR(__NR_fspick),
#endif
#ifdef __NR_mount_setattr
SP_SECCOMP_DENY_NR(__NR_mount_setattr),
#endif
#ifdef __NR_bpf
SP_SECCOMP_DENY_NR(__NR_bpf),
#endif
#ifdef __NR_perf_event_open
SP_SECCOMP_DENY_NR(__NR_perf_event_open),
#endif
#ifdef __NR_userfaultfd
SP_SECCOMP_DENY_NR(__NR_userfaultfd),
#endif
#ifdef __NR_ptrace
SP_SECCOMP_DENY_NR(__NR_ptrace),
#endif
#ifdef __NR_init_module
SP_SECCOMP_DENY_NR(__NR_init_module),
#endif
#ifdef __NR_finit_module
SP_SECCOMP_DENY_NR(__NR_finit_module),
#endif
#ifdef __NR_delete_module
SP_SECCOMP_DENY_NR(__NR_delete_module),
#endif
#ifdef __NR_kexec_load
SP_SECCOMP_DENY_NR(__NR_kexec_load),
#endif
#ifdef __NR_kexec_file_load
SP_SECCOMP_DENY_NR(__NR_kexec_file_load),
#endif
#ifdef __NR_io_uring_setup
SP_SECCOMP_ERRNO_NR(__NR_io_uring_setup, EPERM),
#endif
#ifdef __NR_io_uring_enter
SP_SECCOMP_ERRNO_NR(__NR_io_uring_enter, EPERM),
#endif
#ifdef __NR_io_uring_register
SP_SECCOMP_ERRNO_NR(__NR_io_uring_register, EPERM),
#endif
#ifdef __NR_process_vm_readv
SP_SECCOMP_DENY_NR(__NR_process_vm_readv),
#endif
#ifdef __NR_process_vm_writev
SP_SECCOMP_DENY_NR(__NR_process_vm_writev),
#endif
#ifdef __NR_keyctl
SP_SECCOMP_DENY_NR(__NR_keyctl),
#endif
#ifdef __NR_add_key
SP_SECCOMP_DENY_NR(__NR_add_key),
#endif
#ifdef __NR_request_key
SP_SECCOMP_DENY_NR(__NR_request_key),
#endif
#ifdef __NR_seccomp
SP_SECCOMP_ERRNO_NR(__NR_seccomp, EPERM),
#endif
#ifdef __NR_prctl
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)__NR_prctl, 0, 3),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, args[0])),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)PR_SET_SECCOMP, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
#endif
#ifdef __NR_socket
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)__NR_socket, 0, 3),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, args[0])),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)AF_ALG, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
#endif
#ifdef __NR_clone3
SP_SECCOMP_TRACE_NR(__NR_clone3, SP_SECCOMP_CLONE3_TRACE_TOKEN),
#endif
#ifdef __NR_ioctl
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)__NR_ioctl, 0, 6),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, args[1])),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)TIOCSTI, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)TIOCLINUX, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
SP_SECCOMP_ALLOW(),
#endif
#ifdef __NR_clone
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)__NR_clone, 0, 4),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, args[0])),
BPF_STMT(BPF_ALU | BPF_AND | BPF_K, (unsigned int)SP_CLONE_BLOCKED_FLAGS),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SP_SECCOMP_KILL),
#endif
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
static const struct sock_filter filter_clone3_enosys[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SP_AUDIT_ARCH, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SP_SECCOMP_KILL),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, nr)),
#if defined(__x86_64__)
BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, __X32_SYSCALL_BIT, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SP_SECCOMP_KILL),
#endif
#ifdef __NR_unshare
SP_SECCOMP_DENY_NR(__NR_unshare),
#endif
#ifdef __NR_setns
SP_SECCOMP_DENY_NR(__NR_setns),
#endif
#ifdef __NR_mount
SP_SECCOMP_DENY_NR(__NR_mount),
#endif
#ifdef __NR_umount2
SP_SECCOMP_DENY_NR(__NR_umount2),
#endif
#ifdef __NR_pivot_root
SP_SECCOMP_DENY_NR(__NR_pivot_root),
#endif
#ifdef __NR_open_tree
SP_SECCOMP_DENY_NR(__NR_open_tree),
#endif
#ifdef __NR_move_mount
SP_SECCOMP_DENY_NR(__NR_move_mount),
#endif
#ifdef __NR_fsopen
SP_SECCOMP_DENY_NR(__NR_fsopen),
#endif
#ifdef __NR_fsconfig
SP_SECCOMP_DENY_NR(__NR_fsconfig),
#endif
#ifdef __NR_fsmount
SP_SECCOMP_DENY_NR(__NR_fsmount),
#endif
#ifdef __NR_fspick
SP_SECCOMP_DENY_NR(__NR_fspick),
#endif
#ifdef __NR_mount_setattr
SP_SECCOMP_DENY_NR(__NR_mount_setattr),
#endif
#ifdef __NR_bpf
SP_SECCOMP_DENY_NR(__NR_bpf),
#endif
#ifdef __NR_perf_event_open
SP_SECCOMP_DENY_NR(__NR_perf_event_open),
#endif
#ifdef __NR_userfaultfd
SP_SECCOMP_DENY_NR(__NR_userfaultfd),
#endif
#ifdef __NR_ptrace
SP_SECCOMP_DENY_NR(__NR_ptrace),
#endif
#ifdef __NR_init_module
SP_SECCOMP_DENY_NR(__NR_init_module),
#endif
#ifdef __NR_finit_module
SP_SECCOMP_DENY_NR(__NR_finit_module),
#endif
#ifdef __NR_delete_module
SP_SECCOMP_DENY_NR(__NR_delete_module),
#endif
#ifdef __NR_kexec_load
SP_SECCOMP_DENY_NR(__NR_kexec_load),
#endif
#ifdef __NR_kexec_file_load
SP_SECCOMP_DENY_NR(__NR_kexec_file_load),
#endif
#ifdef __NR_io_uring_setup
SP_SECCOMP_ERRNO_NR(__NR_io_uring_setup, EPERM),
#endif
#ifdef __NR_io_uring_enter
SP_SECCOMP_ERRNO_NR(__NR_io_uring_enter, EPERM),
#endif
#ifdef __NR_io_uring_register
SP_SECCOMP_ERRNO_NR(__NR_io_uring_register, EPERM),
#endif
#ifdef __NR_process_vm_readv
SP_SECCOMP_DENY_NR(__NR_process_vm_readv),
#endif
#ifdef __NR_process_vm_writev
SP_SECCOMP_DENY_NR(__NR_process_vm_writev),
#endif
#ifdef __NR_keyctl
SP_SECCOMP_DENY_NR(__NR_keyctl),
#endif
#ifdef __NR_add_key
SP_SECCOMP_DENY_NR(__NR_add_key),
#endif
#ifdef __NR_request_key
SP_SECCOMP_DENY_NR(__NR_request_key),
#endif
#ifdef __NR_seccomp
SP_SECCOMP_ERRNO_NR(__NR_seccomp, EPERM),
#endif
#ifdef __NR_prctl
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)__NR_prctl, 0, 3),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, args[0])),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)PR_SET_SECCOMP, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
#endif
#ifdef __NR_socket
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)__NR_socket, 0, 3),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, args[0])),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)AF_ALG, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
#endif
#ifdef __NR_clone3
SP_SECCOMP_ERRNO_NR(__NR_clone3, ENOSYS),
#endif
#ifdef __NR_ioctl
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)__NR_ioctl, 0, 6),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, args[1])),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)TIOCSTI, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)TIOCLINUX, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
SP_SECCOMP_ALLOW(),
#endif
#ifdef __NR_clone
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (unsigned int)__NR_clone, 0, 4),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(unsigned int)offsetof(struct seccomp_data, args[0])),
BPF_STMT(BPF_ALU | BPF_AND | BPF_K, (unsigned int)SP_CLONE_BLOCKED_FLAGS),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SP_SECCOMP_KILL),
#endif
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
const struct sock_filter *filter = filter_clone3_enosys;
unsigned short filter_len =
(unsigned short)(sizeof(filter_clone3_enosys) /
sizeof(filter_clone3_enosys[0]));
if (clone3_mode == SP_CLONE3_SECCOMP_TRACE_TRANSLATE) {
filter = filter_clone3_trace;
filter_len = (unsigned short)(sizeof(filter_clone3_trace) /
sizeof(filter_clone3_trace[0]));
}
struct sock_fprog prog = {
.len = filter_len,
.filter = (struct sock_filter *)filter,
};
if (ensure_no_new_privs() < 0)
return -1;
return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
#else
(void)clone3_mode;
fprintf(stderr, "[sockpuppet] Warning: Seccomp not supported on this architecture, continuing without syscall filtering\n");
return 0;
#endif
}
static int sp_wait_status_to_exit_code(int status) {
if (WIFEXITED(status))
return WEXITSTATUS(status);
if (WIFSIGNALED(status))
return 128 + WTERMSIG(status);
return 1;
}
static int command_is_shell_without_args(int argc, char *const argv[]) {
static const char *const shell_names[] = {
"sh", "bash", "dash", "ash", "zsh", "ksh", "mksh", "fish",
};
const char *name;
const char *slash;
if (argc != 1 || argv == NULL || argv[0] == NULL || argv[0][0] == '\0')
return 0;
slash = strrchr(argv[0], '/');
name = slash != NULL ? slash + 1 : argv[0];
for (size_t i = 0; i < sizeof(shell_names) / sizeof(shell_names[0]); ++i) {
if (strcmp(name, shell_names[i]) == 0)
return 1;
}
return 0;
}
static void maybe_print_noninteractive_shell_hint(int argc, char *const argv[],
int status) {
if (interactive_stdio || !WIFEXITED(status) || WEXITSTATUS(status) != 0 ||
!command_is_shell_without_args(argc, argv))
return;
fprintf(stderr,
"[sockpuppet] Hint: %s exited because noninteractive mode gives the "
"payload /dev/null as stdin. Re-run with --interactive for a shell.\n",
argv[0]);
}
static int sp_write_clone3_trace_mode(int fd, char mode) {
return write_all(fd, &mode, sizeof(mode)) == (ssize_t)sizeof(mode) ? 0 : -1;
}
static int sp_set_ptrace_supervisor_options(pid_t pid) {
long options = PTRACE_O_TRACESECCOMP | PTRACE_O_TRACECLONE |
PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK |
PTRACE_O_TRACEEXEC | PTRACE_O_EXITKILL;
errno = 0;
if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)(uintptr_t)options) < 0)
return -1;
return 0;
}
static int sp_ptrace_continue(pid_t pid, int signo) {
errno = 0;
if (ptrace(PTRACE_CONT, pid, NULL, (void *)(uintptr_t)(unsigned int)signo) <
0)
return -1;
return 0;
}
static void sp_dbg_yama_ptrace_scope(void) {
char buf[32];
int fd;
ssize_t nread;
if (!verbose)
return;
fd = open("/proc/sys/kernel/yama/ptrace_scope", O_RDONLY | O_CLOEXEC);
if (fd < 0)
return;
do {
nread = read(fd, buf, sizeof(buf) - 1);
} while (nread < 0 && errno == EINTR);
close(fd);
if (nread <= 0)
return;
buf[nread] = '\0';
char *newline = strchr(buf, '\n');
if (newline)
*newline = '\0';
DBG("Yama ptrace_scope=%s", buf);
}
#if defined(__x86_64__) && defined(__NR_clone3) && defined(__NR_clone)
#define SP_ARCH_CLONE3_TRACE_SUPPORTED 1
struct sp_arch_regs {
struct user_regs_struct gpr;
};
static int sp_arch_read_regs(pid_t pid, struct sp_arch_regs *regs) {
errno = 0;
if (ptrace(PTRACE_GETREGS, pid, NULL, &regs->gpr) < 0)
return -1;
return 0;
}
static long long sp_arch_get_syscall(const struct sp_arch_regs *regs) {
return (long long)regs->gpr.orig_rax;
}
static unsigned long long sp_arch_get_arg(const struct sp_arch_regs *regs,
unsigned int index) {
switch (index) {
case 0:
return regs->gpr.rdi;
case 1:
return regs->gpr.rsi;
case 2:
return regs->gpr.rdx;
case 3:
return regs->gpr.r10;
case 4:
return regs->gpr.r8;
case 5:
return regs->gpr.r9;
default:
return 0;
}
}
static void sp_arch_set_syscall(struct sp_arch_regs *regs, long long nr) {
regs->gpr.orig_rax = (unsigned long long)nr;
}
static void SP_UNUSED sp_arch_set_clone_args(struct sp_arch_regs *regs,
unsigned long long clone_flags,
unsigned long long child_stack,
unsigned long long parent_tid,
unsigned long long child_tid,
unsigned long long tls) {
sp_arch_set_syscall(regs, __NR_clone);
regs->gpr.rdi = clone_flags;
regs->gpr.rsi = child_stack;
regs->gpr.rdx = parent_tid;
regs->gpr.r10 = child_tid;
regs->gpr.r8 = tls;
}
static void sp_arch_set_synthetic_return(struct sp_arch_regs *regs, int err) {
sp_arch_set_syscall(regs, -1);
regs->gpr.rax = (unsigned long long)-err;
}
static int sp_arch_commit_regs(pid_t pid, const struct sp_arch_regs *regs) {
errno = 0;
if (ptrace(PTRACE_SETREGS, pid, NULL, (void *)&regs->gpr) < 0)
return -1;
return 0;
}
#elif defined(__aarch64__) && defined(__NR_clone3) && defined(__NR_clone)
#define SP_ARCH_CLONE3_TRACE_SUPPORTED 1
struct sp_arch_regs {
struct user_pt_regs gpr;
int syscall_no;
};
static int sp_arch_read_regs(pid_t pid, struct sp_arch_regs *regs) {
struct iovec iov = {
.iov_base = &regs->gpr,
.iov_len = sizeof(regs->gpr),
};
errno = 0;
if (ptrace(PTRACE_GETREGSET, pid, (void *)(long)NT_PRSTATUS, &iov) < 0)
return -1;
if (iov.iov_len != sizeof(regs->gpr)) {
errno = EIO;
return -1;
}
iov.iov_base = &regs->syscall_no;
iov.iov_len = sizeof(regs->syscall_no);
errno = 0;
if (ptrace(PTRACE_GETREGSET, pid, (void *)(long)NT_ARM_SYSTEM_CALL, &iov) <
0)
return -1;
if (iov.iov_len != sizeof(regs->syscall_no)) {
errno = EIO;
return -1;
}
return 0;
}
static long long sp_arch_get_syscall(const struct sp_arch_regs *regs) {
return regs->syscall_no;
}
static unsigned long long sp_arch_get_arg(const struct sp_arch_regs *regs,
unsigned int index) {
return index < 6 ? regs->gpr.regs[index] : 0;
}
static void sp_arch_set_syscall(struct sp_arch_regs *regs, long long nr) {
regs->syscall_no = (int)nr;
}
static void SP_UNUSED sp_arch_set_clone_args(struct sp_arch_regs *regs,
unsigned long long clone_flags,
unsigned long long child_stack,
unsigned long long parent_tid,
unsigned long long child_tid,
unsigned long long tls) {
sp_arch_set_syscall(regs, __NR_clone);
regs->gpr.regs[0] = clone_flags;
regs->gpr.regs[1] = child_stack;
regs->gpr.regs[2] = parent_tid;
regs->gpr.regs[3] = tls;
regs->gpr.regs[4] = child_tid;
}
static void sp_arch_set_synthetic_return(struct sp_arch_regs *regs, int err) {
sp_arch_set_syscall(regs, -1);
regs->gpr.regs[0] = (unsigned long long)-err;
}
static int sp_arch_commit_regs(pid_t pid, const struct sp_arch_regs *regs) {
struct iovec iov = {
.iov_base = (void *)&regs->gpr,
.iov_len = sizeof(regs->gpr),
};
errno = 0;
if (ptrace(PTRACE_SETREGSET, pid, (void *)(long)NT_PRSTATUS, &iov) < 0)
return -1;
iov.iov_base = (void *)&regs->syscall_no;
iov.iov_len = sizeof(regs->syscall_no);
errno = 0;
if (ptrace(PTRACE_SETREGSET, pid, (void *)(long)NT_ARM_SYSTEM_CALL, &iov) <
0)
return -1;
return 0;
}
#else
#define SP_ARCH_CLONE3_TRACE_SUPPORTED 0
struct sp_arch_regs {
int unused;
};
static int sp_arch_read_regs(pid_t pid, struct sp_arch_regs *regs) {
(void)pid;
(void)regs;
errno = ENOSYS;
return -1;
}
static long long sp_arch_get_syscall(const struct sp_arch_regs *regs) {
(void)regs;
return -1;
}
static unsigned long long sp_arch_get_arg(const struct sp_arch_regs *regs,
unsigned int index) {
(void)regs;
(void)index;
return 0;
}
static void sp_arch_set_syscall(struct sp_arch_regs *regs, long long nr) {
(void)regs;
(void)nr;
}
static void SP_UNUSED sp_arch_set_clone_args(struct sp_arch_regs *regs,
unsigned long long clone_flags,
unsigned long long child_stack,
unsigned long long parent_tid,
unsigned long long child_tid,
unsigned long long tls) {
(void)regs;
(void)clone_flags;
(void)child_stack;
(void)parent_tid;
(void)child_tid;
(void)tls;
}
static void sp_arch_set_synthetic_return(struct sp_arch_regs *regs, int err) {
(void)regs;
(void)err;
}
static int sp_arch_commit_regs(pid_t pid, const struct sp_arch_regs *regs) {
(void)pid;
(void)regs;
errno = ENOSYS;
return -1;
}
#endif
static int sp_clone3_trace_supported(void) {
return SP_ARCH_CLONE3_TRACE_SUPPORTED;
}
static int sp_force_clone3_enosys(void) {
const char *value = getenv("SOCKPUPPET_TEST_FORCE_CLONE3_ENOSYS");
return value != NULL && strcmp(value, "1") == 0;
}
static ssize_t sp_tracee_read_ptrace(pid_t pid, unsigned long long addr,
void *dst, size_t len) {
unsigned char *out = dst;
size_t off = 0;
while (off < len) {
errno = 0;
long word = ptrace(PTRACE_PEEKDATA, pid, (void *)(uintptr_t)(addr + off),
NULL);
if (word == -1 && errno != 0)
return -1;
size_t chunk = sizeof(word);
if (len - off < chunk)
chunk = len - off;
memcpy(out + off, &word, chunk);
off += chunk;
}
return (ssize_t)len;
}
static ssize_t sp_tracee_read(pid_t pid, unsigned long long addr, void *dst,
size_t len) {
struct iovec local = {
.iov_base = dst,
.iov_len = len,
};
struct iovec remote = {
.iov_base = (void *)(uintptr_t)addr,
.iov_len = len,
};
if (len == 0)
return 0;
errno = 0;
ssize_t nread = process_vm_readv(pid, &local, 1, &remote, 1, 0);
if (nread == (ssize_t)len)
return nread;
return sp_tracee_read_ptrace(pid, addr, dst, len);
}
static int sp_clone3_extra_fields_are_zero(pid_t pid, unsigned long long uargs,
unsigned long long size) {
unsigned long long off = sizeof(struct clone_args);
unsigned char buf[128];
while (off < size) {
size_t chunk = sizeof(buf);
if (size - off < chunk)
chunk = (size_t)(size - off);
ssize_t nread = sp_tracee_read(pid, uargs + off, buf, chunk);
if (nread != (ssize_t)chunk)
return -1;
for (size_t i = 0; i < chunk; i++) {
if (buf[i] != 0)
return 1;
}
off += chunk;
}
return 0;
}
static int sp_clone3_compute_child_stack(const struct clone_args *args,
unsigned long long *child_stack) {
if (args->stack == 0 && args->stack_size == 0) {
*child_stack = 0;
return 0;
}
if (args->stack == 0 || args->stack_size == 0)
return -1;
if (UINT64_MAX - args->stack < args->stack_size)
return -1;
*child_stack = args->stack + args->stack_size;
return 0;
}
static int sp_clone3_read_args(pid_t pid, unsigned long long uargs,
unsigned long long size,
struct clone_args *args, int *err,
const char **reason) {
size_t copy_len;
ssize_t nread;
if (uargs == 0) {
*err = EPERM;
*reason = "null clone_args pointer";
return -1;
}
if (size < CLONE_ARGS_SIZE_VER0) {
*err = ENOSYS;
*reason = "short clone_args";
return -1;
}
if (size > SP_CLONE3_MAX_USER_SIZE) {
*err = ENOSYS;
*reason = "oversized clone_args";
return -1;
}
memset(args, 0, sizeof(*args));
copy_len = size < sizeof(*args) ? (size_t)size : sizeof(*args);
nread = sp_tracee_read(pid, uargs, args, copy_len);
if (nread != (ssize_t)copy_len) {
*err = EPERM;
*reason = "failed to read clone_args";
return -1;
}
if (size > sizeof(*args)) {
int extra = sp_clone3_extra_fields_are_zero(pid, uargs, size);
if (extra < 0) {
*err = EPERM;
*reason = "failed to read future clone_args fields";
return -1;
}
if (extra > 0) {
*err = ENOSYS;
*reason = "nonzero future clone_args fields";
return -1;
}
}
return 0;
}
static int sp_clone3_prepare_translation(pid_t pid, unsigned long long uargs,
unsigned long long size,
unsigned long long *clone_flags,
unsigned long long *child_stack,
unsigned long long *parent_tid,
unsigned long long *child_tid,
unsigned long long *tls, int *err,
const char **reason) {
struct clone_args args;
if (sp_clone3_read_args(pid, uargs, size, &args, err, reason) < 0)
return -1;
if ((args.flags & SP_CLONE3_UNSAFE_FLAGS) != 0) {
*err = EPERM;
*reason = "unsafe clone3 flags";
return -1;
}
if (args.set_tid != 0 || args.set_tid_size != 0) {
*err = EPERM;
*reason = "set_tid is blocked";
return -1;
}
if (args.cgroup != 0) {
*err = EPERM;
*reason = "cgroup field is blocked";
return -1;
}
if (args.exit_signal >= (unsigned long long)NSIG) {
*err = EPERM;
*reason = "invalid exit_signal";
return -1;
}
if ((args.flags & ~SP_CLONE3_SUPPORTED_FLAGS) != 0) {
*err = ENOSYS;
*reason = "unsupported clone3 flags";
return -1;
}
if ((args.flags & SP_CLONE_SIGNAL_MASK) != 0) {
*err = ENOSYS;
*reason = "signal bits in clone3 flags";
return -1;
}
if (args.pidfd != 0) {
*err = ENOSYS;
*reason = "pidfd translation is unsupported";
return -1;
}
if (sp_clone3_compute_child_stack(&args, child_stack) < 0) {
*err = EPERM;
*reason = "invalid clone3 stack";
return -1;
}
if ((args.flags & CLONE_VM) != 0 && *child_stack == 0) {
*err = EPERM;
*reason = "CLONE_VM without explicit child stack";
return -1;
}
*clone_flags = args.flags | args.exit_signal;
*parent_tid = args.parent_tid;
*child_tid = args.child_tid;
*tls = args.tls;
*reason = "translated";
return 0;
}
static int sp_handle_seccomp_stop(pid_t pid) {
unsigned long msg = 0;
struct sp_arch_regs regs;
long long syscall_no;
unsigned long long clone_flags = 0;
unsigned long long child_stack = 0;
unsigned long long parent_tid = 0;
unsigned long long child_tid = 0;
unsigned long long tls = 0;
unsigned long long uargs;
unsigned long long size;
const char *reason = "unhandled";
int err = ENOSYS;
errno = 0;
if (ptrace(PTRACE_GETEVENTMSG, pid, NULL, &msg) < 0) {
DBG("ptrace seccomp eventmsg read failed for pid %ld: %s", (long)pid,
strerror(errno));
return -1;
}
if (msg != SP_SECCOMP_CLONE3_TRACE_TOKEN) {
DBG("unexpected seccomp trace token=0x%lx for pid %ld", msg, (long)pid);
return -1;
}
if (sp_arch_read_regs(pid, &regs) < 0) {
DBG("ptrace register read failed for pid %ld: %s", (long)pid,
strerror(errno));
return -1;
}
syscall_no = sp_arch_get_syscall(&regs);
#ifdef __NR_clone3
if (syscall_no != __NR_clone3) {
DBG("unexpected traced syscall %lld for pid %ld", syscall_no, (long)pid);
return -1;
}
#else
DBG("clone3 trace stop on build without __NR_clone3 for pid %ld", (long)pid);
return -1;
#endif
uargs = sp_arch_get_arg(&regs, 0);
size = sp_arch_get_arg(&regs, 1);
DBG("clone3 trace stop pid=%ld uargs=0x%llx size=%llu", (long)pid, uargs,
size);
if (sp_clone3_prepare_translation(pid, uargs, size, &clone_flags,
&child_stack, &parent_tid, &child_tid, &tls,
&err, &reason) == 0) {
sp_arch_set_clone_args(&regs, clone_flags, child_stack, parent_tid,
child_tid, tls);
if (sp_arch_commit_regs(pid, &regs) < 0) {
DBG("ptrace clone3 translation commit failed for pid %ld: %s", (long)pid,
strerror(errno));
return -1;
}
DBG("clone3 translated for pid %ld flags=0x%llx stack=0x%llx", (long)pid,
clone_flags, child_stack);
return 0;
}
sp_arch_set_synthetic_return(&regs, err);
if (sp_arch_commit_regs(pid, &regs) < 0) {
DBG("ptrace synthetic clone3 error commit failed for pid %ld: %s",
(long)pid, strerror(errno));
return -1;
}
if (err == EPERM) {
DBG("clone3 denied for pid %ld with EPERM: %s", (long)pid, reason);
} else {
DBG("clone3 unsupported for pid %ld with ENOSYS: %s", (long)pid, reason);
}
return 0;
}
static enum sp_clone3_seccomp_mode
sp_inner_clone3_ptrace_handshake(int trace_fd) {
char mode = SP_CLONE3_TRACE_MODE_ENOSYS;
ssize_t r;
errno = 0;
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) {
DBG("clone3 ptrace supervision unavailable: PTRACE_TRACEME failed: %s",
strerror(errno));
sp_dbg_yama_ptrace_scope();
IGNORE_RESULT(sp_write_clone3_trace_mode(trace_fd, mode));
return SP_CLONE3_SECCOMP_ENOSYS;
}
if (raise(SIGSTOP) != 0) {
DBG("clone3 ptrace supervision unavailable: SIGSTOP failed: %s",
strerror(errno));
return SP_CLONE3_SECCOMP_ENOSYS;
}
do {
r = read(trace_fd, &mode, sizeof(mode));
} while (r < 0 && errno == EINTR);
if (r != (ssize_t)sizeof(mode)) {
DBG("clone3 ptrace supervision unavailable: mode read failed");
return SP_CLONE3_SECCOMP_ENOSYS;
}
if (mode == SP_CLONE3_TRACE_MODE_TRANSLATE)
return SP_CLONE3_SECCOMP_TRACE_TRANSLATE;
return SP_CLONE3_SECCOMP_ENOSYS;
}
static int sp_wait_for_inner_child(pid_t inner_pid) {
int status = 0;
for (;;) {
pid_t got = waitpid(inner_pid, &status, 0);
if (got == inner_pid)
return sp_wait_status_to_exit_code(status);
if (got < 0 && errno == EINTR)
continue;
return 1;
}
}
static int sp_trace_supervisor_loop(pid_t root_pid) {
for (;;) {
int status = 0;
pid_t got = waitpid(-1, &status, __WALL);
if (got < 0) {
if (errno == EINTR)
continue;
if (errno == ECHILD)
return 1;
DBG("ptrace supervisor wait failed: %s", strerror(errno));
return 1;
}
if (WIFEXITED(status) || WIFSIGNALED(status)) {
if (got == root_pid)
return sp_wait_status_to_exit_code(status);
continue;
}
if (!WIFSTOPPED(status))
continue;
unsigned int event = (unsigned int)status >> 16;
int signo = WSTOPSIG(status);
if (event == PTRACE_EVENT_SECCOMP) {
if (sp_handle_seccomp_stop(got) < 0) {
errno = 0;
if (ptrace(PTRACE_KILL, got, NULL, NULL) < 0 && errno != ESRCH)
DBG("ptrace kill failed for pid %ld: %s", (long)got, strerror(errno));
return 1;
}
}
if (event == PTRACE_EVENT_CLONE || event == PTRACE_EVENT_FORK ||
event == PTRACE_EVENT_VFORK) {
unsigned long new_pid = 0;
errno = 0;
if (ptrace(PTRACE_GETEVENTMSG, got, NULL, &new_pid) < 0) {
DBG("ptrace child eventmsg read failed for pid %ld: %s", (long)got,
strerror(errno));
} else {
DBG("ptrace supervisor observed new tracee pid %lu", new_pid);
}
} else if (event == PTRACE_EVENT_EXEC) {
DBG("ptrace supervisor observed exec in pid %ld", (long)got);
}
if (event == 0 && signo == SIGSTOP) {
if (sp_set_ptrace_supervisor_options(got) < 0 && errno != ESRCH)
DBG("ptrace option setup failed for stopped pid %ld: %s", (long)got,
strerror(errno));
}
int deliver =
(event == 0 && signo != SIGTRAP && signo != SIGSTOP) ? signo : 0;
if (sp_ptrace_continue(got, deliver) < 0 && errno != ESRCH) {
DBG("ptrace continue failed for pid %ld: %s", (long)got, strerror(errno));
return 1;
}
}
}
static int sp_supervise_inner_child(pid_t inner_pid, int trace_fd) {
int status = 0;
for (;;) {
pid_t got = waitpid(inner_pid, &status, __WALL);
if (got == inner_pid)
break;
if (got < 0 && errno == EINTR)
continue;
DBG("ptrace supervisor initial wait failed: %s", strerror(errno));
return 1;
}
if (WIFEXITED(status) || WIFSIGNALED(status))
return sp_wait_status_to_exit_code(status);
if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) {
DBG("ptrace supervisor saw unexpected initial child status 0x%x", status);
return 1;
}
if (sp_set_ptrace_supervisor_options(inner_pid) < 0) {
DBG("clone3 ptrace setup failed, falling back to clone3 ENOSYS: %s",
strerror(errno));
sp_dbg_yama_ptrace_scope();
if (sp_write_clone3_trace_mode(trace_fd, SP_CLONE3_TRACE_MODE_ENOSYS) < 0)
DBG("failed to send clone3 ENOSYS fallback mode to child: %s",
strerror(errno));
errno = 0;
if (ptrace(PTRACE_DETACH, inner_pid, NULL, NULL) < 0 && errno != ESRCH)
DBG("ptrace detach after fallback failed: %s", strerror(errno));
return sp_wait_for_inner_child(inner_pid);
}
char clone3_mode = (!sp_force_clone3_enosys() && sp_clone3_trace_supported())
? SP_CLONE3_TRACE_MODE_TRANSLATE
: SP_CLONE3_TRACE_MODE_ENOSYS;
DBG("clone3 ptrace supervisor active; using %s mode",
clone3_mode == SP_CLONE3_TRACE_MODE_TRANSLATE ? "trace" : "ENOSYS");
if (sp_write_clone3_trace_mode(trace_fd, clone3_mode) < 0) {
DBG("failed to send clone3 seccomp mode to child: %s", strerror(errno));
errno = 0;
if (ptrace(PTRACE_KILL, inner_pid, NULL, NULL) < 0 && errno != ESRCH)
DBG("ptrace kill after mode-send failure failed: %s", strerror(errno));
return 1;
}
if (sp_ptrace_continue(inner_pid, 0) < 0) {
DBG("ptrace continue after setup failed: %s", strerror(errno));
return 1;
}
return sp_trace_supervisor_loop(inner_pid);
}
/* ---------- epoll helpers ---------- */
static uint64_t epoll_registration_token(enum fd_type type, size_t index,
int fd, uint32_t generation) {
if (index > UINT8_MAX || fd < 0 || fd > UINT16_MAX || generation == 0)
die("epoll registration token");
return epoll_token_encode(type, (uint8_t)index, (uint16_t)fd, generation);
}
static void epoll_add_tcp(struct tcp_flow *f) {
if (f->sock < 0 || g_epfd < 0)
return;
size_t index = (size_t)(f - tcp_flows);
struct epoll_event ev = {
.events = EPOLLIN,
.data.u64 = epoll_registration_token(FD_TCP, index, f->sock,
f->generation)};
if (epoll_ctl(g_epfd, EPOLL_CTL_ADD, f->sock, &ev) < 0)
die("epoll_ctl add tcp");
}
static void epoll_mod_tcp(struct tcp_flow *f, uint32_t events) {
if (f->sock < 0 || g_epfd < 0)
return;
size_t index = (size_t)(f - tcp_flows);
struct epoll_event ev = {
.events = events,
.data.u64 = epoll_registration_token(FD_TCP, index, f->sock,
f->generation)};
if (epoll_ctl(g_epfd, EPOLL_CTL_MOD, f->sock, &ev) < 0)
perror("epoll_ctl mod tcp");
}
static void epoll_add_udp(struct udp_flow *f) {
if (f->udp_relay < 0 || g_epfd < 0)
return;
size_t index = (size_t)(f - udp_flows);
struct epoll_event ev = {.events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP,
.data.u64 = epoll_registration_token(
FD_UDP_RELAY, index, f->udp_relay,
f->generation)};
if (epoll_ctl(g_epfd, EPOLL_CTL_ADD, f->udp_relay, &ev) < 0)
die("epoll_ctl add udp");
}
static void epoll_mod_udp(struct udp_flow *f, uint32_t events) {
if (f->udp_relay < 0 || g_epfd < 0)
return;
size_t index = (size_t)(f - udp_flows);
struct epoll_event ev = {
.events = events,
.data.u64 = epoll_registration_token(FD_UDP_RELAY, index, f->udp_relay,
f->generation)};
if (epoll_ctl(g_epfd, EPOLL_CTL_MOD, f->udp_relay, &ev) < 0)
perror("epoll_ctl mod udp");
}
static void epoll_add_udp_ctrl(struct udp_flow *f, uint32_t events) {
if (f->tcp_ctrl < 0 || g_epfd < 0)
return;
size_t index = (size_t)(f - udp_flows);
struct epoll_event ev = {
.events = events,
.data.u64 = epoll_registration_token(FD_UDP_CTRL, index, f->tcp_ctrl,
f->generation)};
if (epoll_ctl(g_epfd, EPOLL_CTL_ADD, f->tcp_ctrl, &ev) < 0)
die("epoll_ctl add udp ctrl");
}
static void epoll_mod_udp_ctrl(struct udp_flow *f, uint32_t events) {
if (f->tcp_ctrl < 0 || g_epfd < 0)
return;
size_t index = (size_t)(f - udp_flows);
struct epoll_event ev = {
.events = events,
.data.u64 = epoll_registration_token(FD_UDP_CTRL, index, f->tcp_ctrl,
f->generation)};
if (epoll_ctl(g_epfd, EPOLL_CTL_MOD, f->tcp_ctrl, &ev) < 0)
perror("epoll_ctl mod udp ctrl");
}
static void epoll_add_publish_rule(struct publish_rule *rule) {
if (rule->listen_fd < 0 || g_epfd < 0)
return;
enum fd_type type = rule->proto == IPPROTO_TCP ? FD_PUBLISH_TCP_LISTENER
: FD_PUBLISH_UDP_SOCKET;
struct epoll_event ev = {
.events = EPOLLIN | EPOLLERR | EPOLLHUP,
.data.u64 = epoll_registration_token(type, (size_t)rule->rule_index,
rule->listen_fd, 1),
};
if (epoll_ctl(g_epfd, EPOLL_CTL_ADD, rule->listen_fd, &ev) < 0)
die("epoll_ctl add publish listener");
}
static void epoll_add_publish_tcp_host(struct publish_tcp_flow *f) {
if (f->host_fd < 0 || g_epfd < 0)
return;
size_t index = (size_t)(f - publish_tcp_flows);
struct epoll_event ev = {
.events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP,
.data.u64 = epoll_registration_token(
FD_PUBLISH_TCP_HOST, index, f->host_fd, f->generation),
};
if (epoll_ctl(g_epfd, EPOLL_CTL_ADD, f->host_fd, &ev) < 0)
die("epoll_ctl add publish tcp host");
}
static void epoll_mod_publish_tcp_host(struct publish_tcp_flow *f,
uint32_t events) {
if (f->host_fd < 0 || g_epfd < 0)
return;
size_t index = (size_t)(f - publish_tcp_flows);
struct epoll_event ev = {
.events = events,
.data.u64 = epoll_registration_token(
FD_PUBLISH_TCP_HOST, index, f->host_fd, f->generation)};
if (epoll_ctl(g_epfd, EPOLL_CTL_MOD, f->host_fd, &ev) < 0)
perror("epoll_ctl mod publish tcp host");
}
static void epoll_del(int fd) {
if (fd >= 0 && g_epfd >= 0)
epoll_ctl(g_epfd, EPOLL_CTL_DEL, fd, NULL);
}
static int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
enum relay_destination_mode {
RELAY_DEST_NONBLOCKING_WRITE = 1,
RELAY_DEST_NONBLOCKING_SEND,
RELAY_DEST_REGULAR_FILE
};
struct relay_destination {
int fd;
enum relay_destination_mode mode;
int owned;
int epollable;
};
struct relay_runtime {
struct sp_relay_state state;
int source_fd;
struct relay_destination destination;
enum fd_type source_type;
enum fd_type destination_type;
int source_registered;
int destination_registered;
int close_source_on_destination_failure;
};
#ifdef SOCKPUPPET_RELAY_TESTING
static int g_relay_test_read_errno;
static int g_relay_test_write_errno;
static size_t g_relay_test_write_limit;
#endif
static ssize_t relay_os_read(int fd, void *buffer, size_t length) {
#ifdef SOCKPUPPET_RELAY_TESTING
if (g_relay_test_read_errno != 0) {
errno = g_relay_test_read_errno;
g_relay_test_read_errno = 0;
return -1;
}
#endif
return read(fd, buffer, length);
}
static ssize_t relay_os_write(struct relay_runtime *runtime,
const uint8_t *data, size_t length) {
#ifdef SOCKPUPPET_RELAY_TESTING
if (g_relay_test_write_errno != 0) {
errno = g_relay_test_write_errno;
g_relay_test_write_errno = 0;
return -1;
}
if (g_relay_test_write_limit > 0 && length > g_relay_test_write_limit)
length = g_relay_test_write_limit;
#endif
if (runtime->destination.mode == RELAY_DEST_NONBLOCKING_SEND)
return send(runtime->destination.fd, data, length,
MSG_DONTWAIT | MSG_NOSIGNAL);
return write(runtime->destination.fd, data, length);
}
static int relay_probe_epollable(int fd) {
struct epoll_event event = {.events = EPOLLOUT};
if (epoll_ctl(g_epfd, EPOLL_CTL_ADD, fd, &event) == 0) {
epoll_del(fd);
return 1;
}
if (errno == EPERM)
return 0;
return -1;
}
static int relay_prepare_destination(int inherited_fd,
struct relay_destination *destination) {
struct stat st;
int flags;
memset(destination, 0, sizeof(*destination));
destination->fd = -1;
if (fstat(inherited_fd, &st) < 0)
return -1;
if (S_ISREG(st.st_mode)) {
destination->fd = inherited_fd;
destination->mode = RELAY_DEST_REGULAR_FILE;
return 0;
}
if (S_ISSOCK(st.st_mode)) {
destination->fd = fcntl(inherited_fd, F_DUPFD_CLOEXEC, 3);
if (destination->fd < 0)
return -1;
destination->mode = RELAY_DEST_NONBLOCKING_SEND;
destination->owned = 1;
destination->epollable = relay_probe_epollable(destination->fd);
if (destination->epollable < 0) {
close(destination->fd);
destination->fd = -1;
destination->owned = 0;
return -1;
}
return 0;
}
char path[64];
int path_len = snprintf(path, sizeof(path), "/proc/self/fd/%d", inherited_fd);
if (path_len < 0 || (size_t)path_len >= sizeof(path)) {
errno = ENAMETOOLONG;
return -1;
}
destination->fd = open(path, O_WRONLY | O_NONBLOCK | O_CLOEXEC);
if (destination->fd < 0) {
flags = fcntl(inherited_fd, F_GETFL, 0);
if (flags < 0 || (flags & O_NONBLOCK) == 0)
return -1;
destination->fd = fcntl(inherited_fd, F_DUPFD_CLOEXEC, 3);
if (destination->fd < 0)
return -1;
}
destination->mode = RELAY_DEST_NONBLOCKING_WRITE;
destination->owned = 1;
destination->epollable = relay_probe_epollable(destination->fd);
if (destination->epollable < 0) {
close(destination->fd);
destination->fd = -1;
destination->owned = 0;
return -1;
}
return 0;
}
static void relay_close_destination(struct relay_destination *destination) {
if (destination->owned && destination->fd >= 0)
close(destination->fd);
destination->fd = -1;
destination->owned = 0;
destination->epollable = 0;
}
static void relay_runtime_init(struct relay_runtime *runtime, int source_fd,
const struct relay_destination *destination,
enum fd_type source_type,
enum fd_type destination_type, uint8_t *storage) {
memset(runtime, 0, sizeof(*runtime));
sp_relay_init(&runtime->state, storage, RELAY_QUEUE_CAP);
runtime->source_fd = source_fd;
runtime->destination = *destination;
runtime->source_type = source_type;
runtime->destination_type = destination_type;
runtime->close_source_on_destination_failure = 1;
}
static void relay_close_source(struct relay_runtime *runtime) {
if (runtime->source_fd < 0)
return;
epoll_del(runtime->source_fd);
close(runtime->source_fd);
runtime->source_fd = -1;
runtime->source_registered = 0;
(void)sp_relay_step(
&runtime->state,
&(struct sp_relay_event){.type = SP_RELAY_SOURCE_EOF});
}
static void relay_drain(struct relay_runtime *runtime, size_t budget) {
while (sp_relay_wants_write(&runtime->state) && budget > 0) {
size_t length;
const uint8_t *data = sp_relay_output(&runtime->state, &length);
if (length > budget)
length = budget;
ssize_t written = relay_os_write(runtime, data, length);
if (written > 0) {
(void)sp_relay_step(
&runtime->state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_WRITTEN,
.length = (size_t)written});
budget -= (size_t)written;
continue;
}
if (written < 0 && errno == EINTR)
continue;
if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
return;
(void)sp_relay_step(
&runtime->state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_FAILED});
if (runtime->close_source_on_destination_failure)
relay_close_source(runtime);
return;
}
}
static void relay_drain_from_batch(struct relay_runtime *runtime,
size_t *batch_budget) {
size_t budget = *batch_budget;
if (budget > RELAY_DRAIN_BUDGET)
budget = RELAY_DRAIN_BUDGET;
uint64_t before = runtime->state.bytes_out;
relay_drain(runtime, budget);
uint64_t drained = runtime->state.bytes_out - before;
if (drained > *batch_budget)
*batch_budget = 0;
else
*batch_budget -= (size_t)drained;
}
static void relay_read_source(struct relay_runtime *runtime) {
size_t space = sp_relay_space(&runtime->state);
if (space == 0)
return;
if (space > sizeof(g_io_buf))
space = sizeof(g_io_buf);
ssize_t count = relay_os_read(runtime->source_fd, g_io_buf, space);
if (count > 0) {
(void)sp_relay_step(
&runtime->state,
&(struct sp_relay_event){.type = SP_RELAY_SOURCE_BYTES,
.bytes = g_io_buf,
.length = (size_t)count});
return;
}
if (count == 0) {
relay_close_source(runtime);
return;
}
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
return;
relay_close_source(runtime);
}
static int interactive_open_pty_master(void) {
return posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC);
}
static int interactive_sync_winsize(struct interactive_session *session) {
if (!session || !session->active || session->host_tty_fd < 0 ||
session->pty_master_fd < 0)
return 0;
if (ioctl(session->host_tty_fd, TIOCGWINSZ, &session->host_winsize) < 0) {
if (errno == ENOTTY)
return 0;
return -1;
}
session->host_winsize_saved = 1;
if (ioctl(session->pty_master_fd, TIOCSWINSZ, &session->host_winsize) < 0)
return -1;
return 0;
}
static int interactive_restore_terminal(struct interactive_session *session) {
if (!session || session->host_tty_fd < 0 || !session->host_termios_saved)
return 0;
for (;;) {
if (tcsetattr(session->host_tty_fd, TCSAFLUSH, &session->host_termios) == 0)
return 0;
if (errno != EINTR)
return -1;
}
}
static int interactive_close_session(struct interactive_session *session) {
int restore_failed = 0;
if (!session)
return 0;
if (interactive_restore_terminal(session) < 0) {
restore_failed = 1;
fprintf(stderr, "[sockpuppet] Error: could not restore parent terminal: %s\n",
strerror(errno));
}
if (session->pty_master_fd >= 0) {
close(session->pty_master_fd);
session->pty_master_fd = -1;
}
if (session->pty_slave_fd >= 0) {
close(session->pty_slave_fd);
session->pty_slave_fd = -1;
}
if (session->host_tty_fd >= 0) {
close(session->host_tty_fd);
session->host_tty_fd = -1;
}
session->active = 0;
return restore_failed ? -1 : 0;
}
static void interactive_atexit_cleanup(void) {
(void)interactive_close_session(&interactive_session);
}
static int interactive_parent_setup(struct interactive_session *session) {
struct termios raw;
memset(session, 0, sizeof(*session));
session->host_tty_fd = -1;
session->pty_master_fd = -1;
session->pty_slave_fd = -1;
session->host_tty_fd = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
if (session->host_tty_fd < 0)
return -1;
if (!isatty(session->host_tty_fd)) {
errno = ENOTTY;
return -1;
}
session->pty_master_fd = interactive_open_pty_master();
if (session->pty_master_fd < 0)
return -1;
if (grantpt(session->pty_master_fd) < 0)
return -1;
if (unlockpt(session->pty_master_fd) < 0)
return -1;
#ifdef TIOCGPTPEER
session->pty_slave_fd =
ioctl(session->pty_master_fd, TIOCGPTPEER, O_RDWR | O_NOCTTY | O_CLOEXEC);
if (session->pty_slave_fd < 0)
return -1;
#else
errno = ENOTSUP;
return -1;
#endif
if (tcgetattr(session->host_tty_fd, &session->host_termios) < 0)
return -1;
session->host_termios_saved = 1;
raw = session->host_termios;
cfmakeraw(&raw);
if (tcsetattr(session->host_tty_fd, TCSAFLUSH, &raw) < 0)
return -1;
session->active = 1;
if (interactive_sync_winsize(session) < 0)
return -1;
return 0;
}
static void interactive_child_setup(const struct interactive_session *session) {
if (!session || session->pty_slave_fd < 0) {
errno = EBADF;
die("interactive child setup");
}
if (setsid() < 0)
die("setsid");
if (ioctl(session->pty_slave_fd, TIOCSCTTY, 0) < 0)
die("TIOCSCTTY");
if (dup2(session->pty_slave_fd, STDIN_FILENO) < 0)
die("dup2 stdin");
if (dup2(session->pty_slave_fd, STDOUT_FILENO) < 0)
die("dup2 stdout");
if (dup2(session->pty_slave_fd, STDERR_FILENO) < 0)
die("dup2 stderr");
}
static int start_nonblocking_connect(int fd, const struct sockaddr_in *addr) {
if (set_nonblocking(fd) < 0)
return -1;
if (connect(fd, (const struct sockaddr *)addr, sizeof(*addr)) == 0)
return 0;
if (errno == EINPROGRESS)
return 1;
return -1;
}
static int socket_connect_complete(int fd) {
int so_error = 0;
socklen_t len = sizeof(so_error);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &len) < 0)
return -1;
if (so_error != 0) {
errno = so_error;
return -1;
}
return 0;
}
static void socks_io_reset(struct socks_io *io) { memset(io, 0, sizeof(*io)); }
static int socks_has_pending_tx(const struct socks_io *io) {
return io->tx_off < io->tx_len;
}
static size_t socks_response_need(const struct socks_io *io) {
if (io->state == SOCKS_IO_METHOD || io->state == SOCKS_IO_AUTH)
return 2;
if (io->state != SOCKS_IO_REQUEST)
return 0;
if (io->rx_len < 4)
return 4;
if (io->rxbuf[3] == 0x01)
return 10;
if (io->rxbuf[3] == 0x04)
return 22;
if (io->rxbuf[3] == 0x03) {
if (io->rx_len < 5)
return 5;
size_t total = (size_t)(4 + 1 + io->rxbuf[4] + 2);
if (total > sizeof(io->rxbuf))
return sizeof(io->rxbuf) + 1; /* Trigger failure check */
return total;
}
return 4;
}
static void socks_consume_rx(struct socks_io *io, size_t used) {
if (used >= io->rx_len) {
io->rx_len = 0;
return;
}
memmove(io->rxbuf, io->rxbuf + used, io->rx_len - used);
io->rx_len -= used;
}
static int socks_queue_send(struct socks_io *io, const uint8_t *data, size_t len) {
if (len > sizeof(io->txbuf))
return -1;
memcpy(io->txbuf, data, len);
io->tx_off = 0;
io->tx_len = len;
return 0;
}
static int socks_queue_greeting(struct socks_io *io, const struct socks_config *cfg) {
uint8_t greeting[4];
if (cfg->username[0] != '\0') {
greeting[0] = 0x05;
greeting[1] = 0x02;
greeting[2] = 0x00;
greeting[3] = 0x02;
return socks_queue_send(io, greeting, 4);
}
greeting[0] = 0x05;
greeting[1] = 0x01;
greeting[2] = 0x00;
return socks_queue_send(io, greeting, 3);
}
static int socks_queue_auth(struct socks_io *io, const struct socks_config *cfg) {
size_t ulen = strlen(cfg->username);
size_t plen = strlen(cfg->password);
uint8_t auth[513];
size_t off = 0;
if (ulen > 255 || plen > 255)
return -1;
auth[off++] = 0x01;
auth[off++] = (uint8_t)ulen;
memcpy(auth + off, cfg->username, ulen);
off += ulen;
auth[off++] = (uint8_t)plen;
memcpy(auth + off, cfg->password, plen);
off += plen;
return socks_queue_send(io, auth, off);
}
static int socks_queue_request(struct socks_io *io) {
uint8_t req[4 + 1 + 255 + 2];
uint16_t port_be = htons(io->target_port);
size_t off = 0;
size_t domain_len = strlen(io->target_domain);
if (domain_len > 255)
return -1;
req[off++] = 0x05;
req[off++] = io->is_udp ? 0x03 : 0x01;
req[off++] = 0x00;
if (!io->is_udp && domain_len > 0) {
req[off++] = 0x03;
req[off++] = (uint8_t)domain_len;
memcpy(req + off, io->target_domain, domain_len);
off += domain_len;
} else {
req[off++] = 0x01;
memcpy(req + off, &io->target_ip, 4);
off += 4;
}
memcpy(req + off, &port_be, 2);
off += 2;
return socks_queue_send(io, req, off);
}
static int socks_begin_handshake(struct socks_io *io,
const struct socks_config *cfg) {
if (socks_queue_greeting(io, cfg) < 0)
return -1;
io->state = SOCKS_IO_METHOD;
return 0;
}
static int socks_flush_tx(int fd, struct socks_io *io) {
while (socks_has_pending_tx(io)) {
ssize_t sent =
send(fd, io->txbuf + io->tx_off, io->tx_len - io->tx_off, MSG_NOSIGNAL);
if (sent > 0) {
io->tx_off += (size_t)sent;
continue;
}
if (sent < 0 && errno == EINTR)
continue;
if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
return 0;
return -1;
}
io->tx_off = 0;
io->tx_len = 0;
return 0;
}
static int socks_process_rx(struct socks_io *io, const struct socks_config *cfg,
struct sockaddr_in *relay_addr) {
for (;;) {
size_t need = socks_response_need(io);
if (io->state == SOCKS_IO_READY)
return 1;
if (need > sizeof(io->rxbuf)) {
io->state = SOCKS_IO_FAILED;
return -1;
}
if (need == 0 || io->rx_len < need)
return 0;
if (io->state == SOCKS_IO_METHOD) {
if (io->rxbuf[0] != 0x05) {
io->state = SOCKS_IO_FAILED;
return -1;
}
if (io->rxbuf[1] == 0x00) {
socks_consume_rx(io, 2);
if (socks_queue_request(io) < 0) {
io->state = SOCKS_IO_FAILED;
return -1;
}
io->state = SOCKS_IO_REQUEST;
continue;
}
if (io->rxbuf[1] == 0x02 && cfg->username[0] != '\0') {
socks_consume_rx(io, 2);
if (socks_queue_auth(io, cfg) < 0) {
io->state = SOCKS_IO_FAILED;
return -1;
}
io->state = SOCKS_IO_AUTH;
continue;
}
io->state = SOCKS_IO_FAILED;
return -1;
}
if (io->state == SOCKS_IO_AUTH) {
if (io->rxbuf[0] != 0x01 || io->rxbuf[1] != 0x00) {
io->state = SOCKS_IO_FAILED;
return -1;
}
socks_consume_rx(io, 2);
if (socks_queue_request(io) < 0) {
io->state = SOCKS_IO_FAILED;
return -1;
}
io->state = SOCKS_IO_REQUEST;
continue;
}
if (io->state == SOCKS_IO_REQUEST) {
uint8_t atyp;
size_t used = need;
if (io->rxbuf[0] != 0x05 || io->rxbuf[1] != 0x00) {
io->state = SOCKS_IO_FAILED;
return -1;
}
atyp = io->rxbuf[3];
if (relay_addr) {
memset(relay_addr, 0, sizeof(*relay_addr));
relay_addr->sin_family = AF_INET;
if (atyp == 0x01 && need >= 10) {
memcpy(&relay_addr->sin_addr.s_addr, io->rxbuf + 4, 4);
relay_addr->sin_port = htons(
(uint16_t)((io->rxbuf[8] << 8) | io->rxbuf[9]));
} else if (atyp == 0x03 && need >= 7) {
relay_addr->sin_addr = cfg->addr.sin_addr;
relay_addr->sin_port = htons(
(uint16_t)((io->rxbuf[need - 2] << 8) | io->rxbuf[need - 1]));
} else if (atyp == 0x04) {
io->state = SOCKS_IO_FAILED;
return -1;
} else {
io->state = SOCKS_IO_FAILED;
return -1;
}
if (relay_addr->sin_addr.s_addr == 0 ||
relay_addr->sin_addr.s_addr == htonl(0x7f000001)) {
relay_addr->sin_addr = cfg->addr.sin_addr;
}
DBG("SOCKS UDP relay ready at %s:%d", inet_ntoa(relay_addr->sin_addr),
ntohs(relay_addr->sin_port));
}
socks_consume_rx(io, used);
io->state = SOCKS_IO_READY;
return 1;
}
}
}
static int socks_recv_and_process(int fd, struct socks_io *io,
const struct socks_config *cfg,
struct sockaddr_in *relay_addr) {
for (;;) {
ssize_t r = recv(fd, io->rxbuf + io->rx_len, sizeof(io->rxbuf) - io->rx_len,
0);
if (r > 0) {
io->rx_len += (size_t)r;
if (socks_process_rx(io, cfg, relay_addr) != 0)
return (io->state == SOCKS_IO_READY) ? 1 : -1;
continue;
}
if (r == 0)
return -1;
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return 0;
return -1;
}
}
/* Drop all capabilities (for rootless mode) */
static void drop_caps(void) {
struct __user_cap_header_struct hdr = {
.version = _LINUX_CAPABILITY_VERSION_3,
.pid = 0,
};
struct __user_cap_data_struct data[2] = {{0}};
struct __user_cap_data_struct verify[2] = {{0}};
/* Drop bounding set first while we still have CAP_SETPCAP.
* EINVAL is expected for capability numbers the kernel doesn't know. */
for (int cap = 0; cap <= CAP_LAST_CAP; cap++) {
if (prctl(PR_CAPBSET_DROP, cap, 0, 0, 0) < 0 && errno != EINVAL) {
perror("PR_CAPBSET_DROP");
exit(1);
}
}
/* Clear ambient capabilities - EINVAL expected if not supported */
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) < 0 &&
errno != EINVAL) {
perror("PR_CAP_AMBIENT_CLEAR_ALL");
exit(1);
}
/* Verify bounding set is empty */
for (int cap = 0; cap <= CAP_LAST_CAP; cap++) {
int rc = prctl(PR_CAPBSET_READ, cap, 0, 0, 0);
if (rc > 0) {
fprintf(stderr, "Capability %d survived drop\n", cap);
exit(1);
}
}
/* Now clear all capability sets */
if (syscall(SYS_capset, &hdr, data) < 0) {
perror("capset");
exit(1);
}
if (syscall(SYS_capget, &hdr, verify) < 0) {
perror("capget verify");
exit(1);
}
if (verify[0].effective || verify[0].permitted || verify[0].inheritable ||
verify[1].effective || verify[1].permitted || verify[1].inheritable) {
fprintf(stderr, "capabilities survived drop\n");
exit(1);
}
/* Disable core dumps (prevents leaking sensitive data) */
if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0) {
perror("PR_SET_DUMPABLE");
exit(1);
}
}
/* Parse SOCKS URL: [socks5://|socks5h://]host:port */
static void parse_socks_auth_file(const char *path) {
FILE *f = fopen(path, "r");
if (!f) die("fopen socks auth file");
char buf[256];
if (!fgets(buf, sizeof(buf), f)) die("read socks auth file");
fclose(f);
char *newline = strchr(buf, '\n');
if (newline) *newline = '\0';
char *colon = strchr(buf, ':');
if (!colon) die("invalid auth file format");
*colon = '\0';
snprintf(socks_proxy.username, sizeof(socks_proxy.username), "%s", buf);
snprintf(socks_proxy.password, sizeof(socks_proxy.password), "%s", colon + 1);
}
static int parse_socks_url(const char *url, struct socks_config *cfg) {
const char *p = url;
const char *colon;
long port;
memset(cfg, 0, sizeof(*cfg));
cfg->port = 1080; /* default SOCKS port */
cfg->remote_dns = 1; /* bare host:port defaults to socks5h */
/* Skip protocol prefix if present */
if (strncmp(p, "socks5h://", 10) == 0) {
cfg->remote_dns = 1;
p += 10;
} else if (strncmp(p, "socks5://", 9) == 0) {
cfg->remote_dns = 0;
p += 9;
} else if (strncmp(p, "socks://", 8) == 0) {
cfg->remote_dns = 0;
p += 8;
}
if (strchr(p, '@')) {
fprintf(stderr, "Credentials in SOCKS URL are not allowed for security reasons.\n");
exit(1);
}
/* Parse host:port */
colon = strrchr(p, ':');
if (colon) {
size_t hlen = (size_t)(colon - p);
if (hlen == 0 || hlen >= sizeof(cfg->host) ||
parse_long_strict(colon + 1, 1, 65535, &port) < 0) {
fprintf(stderr, "Invalid SOCKS proxy: %s\n", url);
exit(1);
}
memcpy(cfg->host, p, hlen);
cfg->host[hlen] = '\0';
cfg->port = (int)port;
} else {
size_t hlen = strlen(p);
if (hlen == 0 || hlen >= sizeof(cfg->host)) {
fprintf(stderr, "Invalid SOCKS proxy: %s\n", url);
exit(1);
}
memcpy(cfg->host, p, hlen);
cfg->host[hlen] = '\0';
}
cfg->enabled = (cfg->host[0] != '\0');
return cfg->enabled;
}
static int parse_direct_allow_spec(const char *spec, char *err,
size_t err_len) {
char buf[128];
char *colon;
char *proto_slash;
char *prefix_slash;
const char *addr_str;
const char *port_str;
long port;
long prefix = 32;
struct in_addr addr;
struct direct_rule rule = {0};
if (direct_rule_count >= MAX_DIRECT_RULES) {
snprintf(err, err_len, "too many --allow-direct rules");
return -1;
}
if (snprintf(buf, sizeof(buf), "%s", spec) >= (int)sizeof(buf)) {
snprintf(err, err_len, "--allow-direct rule too long");
return -1;
}
colon = strrchr(buf, ':');
if (colon == NULL) {
snprintf(err, err_len,
"--allow-direct must be ADDR[/PREFIX]:PORT[/tcp|/udp]");
return -1;
}
proto_slash = strchr(colon + 1, '/');
if (proto_slash != NULL) {
*proto_slash = '\0';
if (strcmp(proto_slash + 1, "tcp") == 0)
rule.proto = IPPROTO_TCP;
else if (strcmp(proto_slash + 1, "udp") == 0)
rule.proto = IPPROTO_UDP;
else {
snprintf(err, err_len, "invalid --allow-direct protocol");
return -1;
}
}
*colon = '\0';
addr_str = buf;
port_str = colon + 1;
if (parse_long_strict(port_str, 1, 65535, &port) < 0) {
snprintf(err, err_len, "invalid --allow-direct port");
return -1;
}
rule.port = (uint16_t)port;
prefix_slash = strchr(buf, '/');
if (prefix_slash != NULL) {
*prefix_slash = '\0';
if (parse_long_strict(prefix_slash + 1, 0, 32, &prefix) < 0) {
snprintf(err, err_len, "invalid --allow-direct prefix");
return -1;
}
}
if (inet_pton(AF_INET, addr_str, &addr) != 1) {
snprintf(err, err_len, "invalid --allow-direct IPv4 address");
return -1;
}
rule.mask = cidr_mask_from_prefix((unsigned int)prefix);
rule.network = ntohl(addr.s_addr) & rule.mask;
direct_rules[direct_rule_count++] = rule;
return 0;
}
static const char *proto_name(int proto) {
if (proto == IPPROTO_TCP)
return "tcp";
if (proto == IPPROTO_UDP)
return "udp";
return "ip";
}
static int is_loopback_publish_addr(uint32_t ip_be) {
uint32_t ip = ntohl(ip_be);
if ((ip & 0xff000000U) != 0x7f000000U)
return 0;
if (ip == 0x7f000000U || ip == 0x7f000001U || ip == 0x7fffffffU)
return 0;
return 1;
}
static int publish_rule_conflicts(const struct publish_rule *rule) {
for (int i = 0; i < publish_rule_count; ++i) {
const struct publish_rule *existing = &publish_rules[i];
if (existing->host_ip == rule->host_ip &&
existing->host_port == rule->host_port &&
existing->proto == rule->proto)
return 1;
}
return 0;
}
static void publish_rule_debug_string(const struct publish_rule *rule,
char *buf, size_t buf_len) {
char host[INET_ADDRSTRLEN];
char child[INET_ADDRSTRLEN];
struct in_addr host_addr = {.s_addr = rule->host_ip};
struct in_addr child_addr = {.s_addr = SANDBOX_IP};
if (inet_ntop(AF_INET, &host_addr, host, sizeof(host)) == NULL)
snprintf(host, sizeof(host), "unknown");
if (inet_ntop(AF_INET, &child_addr, child, sizeof(child)) == NULL)
snprintf(child, sizeof(child), "unknown");
snprintf(buf, buf_len, "%s %s:%u -> %s:%u", proto_name(rule->proto), host,
rule->host_port, child, rule->container_port);
}
static int parse_publish_spec(const char *spec, struct publish_rule *out,
char *err, size_t err_len) {
char buf[160];
char *slash;
char *first_colon;
char *second_colon;
const char *host_str;
const char *host_port_str;
const char *container_port_str;
long host_port;
long container_port;
struct in_addr host_addr;
memset(out, 0, sizeof(*out));
out->listen_fd = -1;
if (spec == NULL || *spec == '\0') {
snprintf(err, err_len, "empty publish rule");
return -1;
}
if (snprintf(buf, sizeof(buf), "%s", spec) >= (int)sizeof(buf)) {
snprintf(err, err_len, "publish rule too long");
return -1;
}
slash = strchr(buf, '/');
if (slash == NULL) {
snprintf(err, err_len, "publish rule requires /tcp or /udp");
return -1;
}
if (strchr(slash + 1, '/') != NULL) {
snprintf(err, err_len, "publish rule has multiple protocol separators");
return -1;
}
*slash = '\0';
if (strcmp(slash + 1, "tcp") == 0) {
out->proto = IPPROTO_TCP;
} else if (strcmp(slash + 1, "udp") == 0) {
out->proto = IPPROTO_UDP;
} else {
snprintf(err, err_len, "invalid publish protocol");
return -1;
}
first_colon = strchr(buf, ':');
if (first_colon == NULL) {
snprintf(err, err_len,
"--publish must be HOST_LOOPBACK_IP:HOST_PORT:CONTAINER_PORT/PROTO");
return -1;
}
second_colon = strchr(first_colon + 1, ':');
if (second_colon == NULL || strchr(second_colon + 1, ':') != NULL) {
snprintf(err, err_len,
"--publish must contain exactly host IP, host port, and container port");
return -1;
}
*first_colon = '\0';
*second_colon = '\0';
host_str = buf;
host_port_str = first_colon + 1;
container_port_str = second_colon + 1;
if (*host_str == '\0' || *host_port_str == '\0' ||
*container_port_str == '\0') {
snprintf(err, err_len, "publish rule contains an empty field");
return -1;
}
if (inet_pton(AF_INET, host_str, &host_addr) != 1) {
snprintf(err, err_len, "publish host must be an IPv4 address");
return -1;
}
if (!is_loopback_publish_addr(host_addr.s_addr)) {
snprintf(err, err_len,
"publish host must be a non-default 127.0.0.0/8 loopback alias");
return -1;
}
if (parse_long_strict(host_port_str, 1, 65535, &host_port) < 0) {
snprintf(err, err_len, "invalid publish host port");
return -1;
}
if (parse_long_strict(container_port_str, 1, 65535, &container_port) < 0) {
snprintf(err, err_len, "invalid publish container port");
return -1;
}
out->host_ip = host_addr.s_addr;
out->host_port = (uint16_t)host_port;
out->container_port = (uint16_t)container_port;
return 0;
}
static int add_publish_rule_from_spec(const char *spec, char *err,
size_t err_len) {
struct publish_rule rule;
if (publish_rule_count >= MAX_PUBLISH_RULES) {
snprintf(err, err_len, "too many publish rules");
return -1;
}
if (parse_publish_spec(spec, &rule, err, err_len) < 0)
return -1;
if (publish_rule_conflicts(&rule)) {
snprintf(err, err_len, "duplicate published host IP, port, and protocol");
return -1;
}
rule.rule_index = publish_rule_count;
publish_rules[publish_rule_count++] = rule;
return 0;
}
static int parse_egress_mode(const char *value, char *err, size_t err_len) {
if (strcmp(value, "direct") == 0) {
egress_mode = EGRESS_DIRECT;
} else if (strcmp(value, "socks") == 0) {
egress_mode = EGRESS_SOCKS;
} else if (strcmp(value, "none") == 0) {
egress_mode = EGRESS_NONE;
} else {
snprintf(err, err_len, "--egress must be direct, socks, or none");
return -1;
}
egress_mode_explicit = 1;
return 0;
}
static int setup_one_publish_listener(struct publish_rule *rule) {
int type = rule->proto == IPPROTO_TCP ? SOCK_STREAM : SOCK_DGRAM;
int fd = socket(AF_INET, type | SOCK_CLOEXEC, 0);
int one = 1;
struct sockaddr_in addr;
char desc[128];
publish_rule_debug_string(rule, desc, sizeof(desc));
if (fd < 0) {
fprintf(stderr, "sockpuppet: failed to publish %s: %s\n", desc,
strerror(errno));
return -1;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) {
fprintf(stderr, "sockpuppet: failed to publish %s: SO_REUSEADDR: %s\n",
desc, strerror(errno));
close(fd);
return -1;
}
if (rule->proto == IPPROTO_UDP &&
setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &one, sizeof(one)) < 0) {
fprintf(stderr, "sockpuppet: failed to publish %s: IP_PKTINFO: %s\n",
desc, strerror(errno));
close(fd);
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(rule->host_port);
addr.sin_addr.s_addr = rule->host_ip;
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
fprintf(stderr, "sockpuppet: failed to publish %s: %s\n", desc,
strerror(errno));
close(fd);
return -1;
}
if (rule->proto == IPPROTO_TCP && listen(fd, SOMAXCONN) < 0) {
fprintf(stderr, "sockpuppet: failed to publish %s: listen: %s\n", desc,
strerror(errno));
close(fd);
return -1;
}
if (set_nonblocking(fd) < 0) {
fprintf(stderr, "sockpuppet: failed to publish %s: nonblocking: %s\n",
desc, strerror(errno));
close(fd);
return -1;
}
rule->listen_fd = fd;
DBG("publish %s", desc);
return 0;
}
static int setup_publish_listeners(void) {
for (int i = 0; i < publish_rule_count; ++i) {
if (setup_one_publish_listener(&publish_rules[i]) < 0)
return -1;
}
return 0;
}
static void close_publish_listeners(void) {
for (int i = 0; i < publish_rule_count; ++i) {
if (publish_rules[i].listen_fd >= 0) {
epoll_del(publish_rules[i].listen_fd);
close(publish_rules[i].listen_fd);
publish_rules[i].listen_fd = -1;
}
}
}
static int resolve_socks_proxy(struct socks_config *cfg) {
struct sockaddr_in proxy_addr = {
.sin_family = AF_INET,
.sin_port = htons((uint16_t)cfg->port),
};
if (!cfg->enabled)
return 0;
if (cfg->addr_valid)
return 0;
if (inet_pton(AF_INET, cfg->host, &proxy_addr.sin_addr) <= 0) {
struct addrinfo hints = {.ai_family = AF_INET, .ai_socktype = SOCK_STREAM};
struct addrinfo *res;
if (getaddrinfo(cfg->host, NULL, &hints, &res) != 0)
return -1;
proxy_addr.sin_addr = ((struct sockaddr_in *)res->ai_addr)->sin_addr;
freeaddrinfo(res);
}
cfg->addr = proxy_addr;
cfg->addr_valid = 1;
return 0;
}
extern char **environ;
static void close_extra_fds_for_exec(void) {
int preserve_fd = bench_trace_preserved_fd();
#ifdef __NR_close_range
if (preserve_fd < 3) {
if (syscall(__NR_close_range, 3U, ~0U, 0U) == 0)
return;
if (errno != ENOSYS && errno != EINVAL)
die("close_range");
} else {
int need_fallback = 0;
if (preserve_fd > 3 &&
syscall(__NR_close_range, 3U, (unsigned int)preserve_fd - 1U, 0U) < 0) {
if (errno == ENOSYS || errno == EINVAL)
need_fallback = 1;
else
die("close_range");
}
if (syscall(__NR_close_range, (unsigned int)preserve_fd + 1U, ~0U, 0U) <
0) {
if (errno == ENOSYS || errno == EINVAL)
need_fallback = 1;
else
die("close_range");
}
if (!need_fallback)
return;
}
#endif
DIR *dir = opendir("/proc/self/fd");
if (dir != NULL) {
int scan_fd = dirfd(dir);
struct dirent *ent;
int *fds = NULL;
size_t fds_len = 0;
size_t fds_cap = 0;
while ((ent = readdir(dir)) != NULL) {
char *end = NULL;
long fd = strtol(ent->d_name, &end, 10);
if (end == NULL || *end != '\0')
continue;
if (fd <= 2 || fd == scan_fd)
continue;
if ((int)fd == preserve_fd)
continue;
if (fds_len == fds_cap) {
size_t new_cap = (fds_cap == 0) ? 16 : fds_cap * 2;
int *new_fds = realloc(fds, new_cap * sizeof(*new_fds));
if (new_fds == NULL) {
free(fds);
closedir(dir);
die("realloc close fd list");
}
fds = new_fds;
fds_cap = new_cap;
}
fds[fds_len++] = (int)fd;
}
closedir(dir);
for (size_t i = 0; i < fds_len; ++i)
close(fds[i]);
free(fds);
return;
}
long maxfd = sysconf(_SC_OPEN_MAX);
if (maxfd < 0)
maxfd = 256;
for (int fd = 3; fd < maxfd; ++fd)
if (fd != preserve_fd)
close(fd);
}
static int env_name_matches(const char *entry, const char *name) {
const char *eq = strchr(entry, '=');
size_t len = eq != NULL ? (size_t)(eq - entry) : strlen(entry);
return strlen(name) == len && strncmp(entry, name, len) == 0;
}
static int should_keep_env(const char *entry) {
static const char *const keep[] = {
"PATH", "HOME", "USER", "LOGNAME", "SHELL",
"TERM", "LANG", "TZ", NULL,
};
if (strncmp(entry, "LC_", 3) == 0 && strchr(entry, '=') != NULL)
return 1;
for (size_t i = 0; keep[i] != NULL; ++i) {
if (env_name_matches(entry, keep[i]))
return 1;
}
return 0;
}
static char *make_env_entry(const char *name, const char *value) {
size_t name_len = strlen(name);
size_t value_len = strlen(value);
char *entry = malloc(name_len + 1 + value_len + 1);
if (entry == NULL)
die("malloc env");
memcpy(entry, name, name_len);
entry[name_len] = '=';
memcpy(entry + name_len + 1, value, value_len + 1);
return entry;
}
static char **build_sanitized_envp(const char *cwd) {
static const char default_path[] =
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
size_t keep_count = 0;
int have_path = 0;
for (char **env = environ; env != NULL && *env != NULL; ++env) {
if (!should_keep_env(*env))
continue;
if (env_name_matches(*env, "PWD") || env_name_matches(*env, "TMPDIR"))
continue;
if (env_name_matches(*env, "PATH"))
have_path = 1;
++keep_count;
}
char **envp = calloc(keep_count + (have_path ? 0U : 1U) + 4U, sizeof(*envp));
size_t idx = 0;
if (envp == NULL)
die("calloc envp");
for (char **env = environ; env != NULL && *env != NULL; ++env) {
if (!should_keep_env(*env))
continue;
if (env_name_matches(*env, "PWD") || env_name_matches(*env, "TMPDIR"))
continue;
envp[idx] = strdup(*env);
if (envp[idx] == NULL)
die("strdup env");
++idx;
}
if (!have_path) {
envp[idx++] = make_env_entry("PATH", default_path);
}
envp[idx++] = make_env_entry("PWD", cwd);
envp[idx++] = make_env_entry("TMPDIR", "/tmp");
envp[idx++] = make_env_entry("HOME", "/tmp/home");
envp[idx] = NULL;
mkdir("/tmp/home", 0700);
return envp;
}
static volatile sig_atomic_t sp_init_pending_signal = 0;
static const int sp_init_forwarded_signals[] = {
SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2, SIGALRM, SIGCONT,
#ifdef SIGWINCH
SIGWINCH,
#endif
#ifdef SIGTSTP
SIGTSTP,
#endif
0,
};
static void sp_init_signal_handler(int signo) {
sp_init_pending_signal = signo;
}
static void sp_init_set_signal_handlers(void (*handler)(int)) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_handler = handler;
for (size_t i = 0; sp_init_forwarded_signals[i] != 0; ++i) {
if (sigaction(sp_init_forwarded_signals[i], &sa, NULL) < 0)
die("sigaction init");
}
}
static void sp_init_install_signal_handlers(void) {
sp_init_set_signal_handlers(sp_init_signal_handler);
}
static void sp_init_reset_signal_handlers(void) {
sp_init_set_signal_handlers(SIG_DFL);
}
static void sp_init_forward_pending_signal(pid_t payload_pid) {
int signo = sp_init_pending_signal;
if (signo == 0 || payload_pid <= 0)
return;
sp_init_pending_signal = 0;
if (kill(-payload_pid, signo) < 0 && errno == ESRCH)
(void)kill(payload_pid, signo);
}
static void close_payload_setup_fds(const int stdout_pipe[2],
const int stderr_pipe[2],
struct interactive_session *session) {
if (!interactive_stdio) {
close(stdout_pipe[0]);
close(stdout_pipe[1]);
close(stderr_pipe[0]);
close(stderr_pipe[1]);
}
if (session->pty_slave_fd >= 0) {
close(session->pty_slave_fd);
session->pty_slave_fd = -1;
}
}
static void exec_payload_child(char **argv, char **envp,
struct interactive_session *session,
const int stdout_pipe[2],
const int stderr_pipe[2]) {
bench_trace_set_role(BENCH_ROLE_PAYLOAD);
bench_mark("process_start", "payload_child", "ok", NULL);
sp_init_reset_signal_handlers();
bench_phase_begin("payload_stdio_setup");
if (interactive_stdio) {
interactive_child_setup(session);
if (session->pty_slave_fd > STDERR_FILENO)
close(session->pty_slave_fd);
} else {
/* Leave the launcher's terminal session entirely: after setsid() the
* payload has no controlling terminal, so it cannot reach the host tty
* via /dev/tty or steal the foreground process group with tcsetpgrp().
* setsid() also makes the payload a process-group leader, which PID 1's
* kill(-payload_pid, ...) signal forwarding relies on. */
if (setsid() < 0)
die("setsid payload");
if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0)
die("dup2 stdout");
if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)
die("dup2 stderr");
int null_fd = open("/dev/null", O_RDONLY);
if (null_fd >= 0) {
if (dup2(null_fd, STDIN_FILENO) < 0)
die("dup2 stdin");
close(null_fd);
}
close(stdout_pipe[0]);
close(stdout_pipe[1]);
close(stderr_pipe[0]);
close(stderr_pipe[1]);
}
bench_phase_end("payload_stdio_setup", "ok");
bench_phase_begin("payload_exec_prep");
close_extra_fds_for_exec();
apply_child_rlimits();
require_no_new_privs_for_exec();
bench_phase_end("payload_exec_prep", "ok");
bench_mark("exec_ready", "payload_exec", "ok", argv[0]);
DBG("Executing: %s", argv[0]);
execvpe(argv[0], argv, envp);
die("exec");
}
static int run_payload_init(char **argv, char **envp,
struct interactive_session *session,
const int stdout_pipe[2],
const int stderr_pipe[2]) {
sp_init_install_signal_handlers();
bench_phase_begin("payload_fork");
pid_t payload_pid = fork();
if (payload_pid < 0)
die("fork payload");
if (payload_pid == 0) {
bench_trace_set_role(BENCH_ROLE_PAYLOAD);
bench_mark("handoff", "payload_fork", "payload", NULL);
exec_payload_child(argv, envp, session, stdout_pipe, stderr_pipe);
}
bench_phase_end("payload_fork", "pid1");
close_payload_setup_fds(stdout_pipe, stderr_pipe, session);
close_extra_fds_for_exec();
for (;;) {
int status = 0;
sp_init_forward_pending_signal(payload_pid);
pid_t got = waitpid(-1, &status, 0);
if (got < 0) {
if (errno == EINTR)
continue;
if (errno == ECHILD)
return 1;
DBG("init waitpid failed: %s", strerror(errno));
return 1;
}
if (got == payload_pid) {
int payload_status = status;
for (;;) {
got = waitpid(-1, &status, WNOHANG);
if (got > 0)
continue;
if (got < 0 && errno == EINTR)
continue;
break;
}
return sp_wait_status_to_exit_code(payload_status);
}
}
}
static void copy_ifname(char dst[IFNAMSIZ], const char *src) {
int rc = snprintf(dst, IFNAMSIZ, "%s", src);
if (rc < 0 || rc >= IFNAMSIZ) {
errno = ENAMETOOLONG;
die("ifname too long");
}
}
/* ---------- FD passing ---------- */
static void send_fd(int sock, int fd) {
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
char byte = 'X';
struct iovec iov = {&byte, 1};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
char cbuf[CMSG_SPACE(sizeof(int))];
msg.msg_control = cbuf;
msg.msg_controllen = sizeof(cbuf);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
if (sendmsg(sock, &msg, 0) < 0)
die("sendmsg");
}
static int recv_fd_checked(int sock) {
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
char byte;
struct iovec iov = {&byte, 1};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
char cbuf[CMSG_SPACE(sizeof(int))];
msg.msg_control = cbuf;
msg.msg_controllen = sizeof(cbuf);
ssize_t n = recvmsg(sock, &msg, MSG_CMSG_CLOEXEC);
if (n < 0)
return -1;
if (n == 0 || (msg.msg_flags & (MSG_TRUNC | MSG_CTRUNC)) != 0) {
errno = EIO;
return -1;
}
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
int fd;
if (cmsg == NULL || cmsg->cmsg_level != SOL_SOCKET ||
cmsg->cmsg_type != SCM_RIGHTS ||
cmsg->cmsg_len < CMSG_LEN(sizeof(int))) {
errno = EPROTO;
return -1;
}
memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
return fd;
}
/* ---------- TUN and interface helpers for child process ---------- */
static int tun_create(const char *name) {
int fd = open("/dev/net/tun", O_RDWR | O_CLOEXEC | O_NONBLOCK);
if (fd < 0)
die("open /dev/net/tun");
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
copy_ifname(ifr.ifr_name, name);
if (ioctl(fd, TUNSETIFF, &ifr) < 0)
die("TUNSETIFF");
return fd;
}
static void if_up(const char *ifname) {
int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0)
die("socket");
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
copy_ifname(ifr.ifr_name, ifname);
if (ioctl(s, SIOCGIFFLAGS, &ifr) < 0)
die("SIOCGIFFLAGS");
ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0)
die("SIOCSIFFLAGS");
close(s);
}
/* point-to-point address */
static void if_addr_ptp(const char *ifname, const char *local,
const char *peer) {
int s = socket(AF_INET, SOCK_DGRAM, 0);
struct ifreq ifr = {0};
struct sockaddr_in addr = {.sin_family = AF_INET};
if (s < 0)
die("socket");
copy_ifname(ifr.ifr_name, ifname);
if (inet_pton(AF_INET, local, &addr.sin_addr) != 1)
die("inet_pton local");
memcpy(&ifr.ifr_addr, &addr, sizeof(addr));
if (ioctl(s, SIOCSIFADDR, &ifr) < 0)
die("SIOCSIFADDR");
if (inet_pton(AF_INET, peer, &addr.sin_addr) != 1)
die("inet_pton peer");
memcpy(&ifr.ifr_dstaddr, &addr, sizeof(addr));
if (ioctl(s, SIOCSIFDSTADDR, &ifr) < 0)
die("SIOCSIFDSTADDR");
close(s);
}
static int if_index(const char *ifname) {
int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0)
die("socket");
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
copy_ifname(ifr.ifr_name, ifname);
if (ioctl(s, SIOCGIFINDEX, &ifr) < 0)
die("SIOCGIFINDEX");
close(s);
return ifr.ifr_ifindex;
}
static void add_default_route(const char *ifname, const char *gw) {
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
struct {
struct nlmsghdr nlh;
struct rtmsg rtm;
char buf[256];
} req = {0};
if (fd < 0)
die("socket");
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
req.nlh.nlmsg_type = RTM_NEWROUTE;
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE;
req.rtm.rtm_family = AF_INET;
req.rtm.rtm_table = RT_TABLE_MAIN;
req.rtm.rtm_protocol = RTPROT_BOOT;
req.rtm.rtm_scope = RT_SCOPE_UNIVERSE;
req.rtm.rtm_type = RTN_UNICAST;
struct rtattr *rta;
rta = (void *)req.buf;
rta->rta_type = RTA_GATEWAY;
rta->rta_len = RTA_LENGTH(4);
if (inet_pton(AF_INET, gw, RTA_DATA(rta)) != 1)
die("inet_pton gateway");
req.nlh.nlmsg_len += rta->rta_len;
rta = (void *)((char *)rta + rta->rta_len);
rta->rta_type = RTA_OIF;
rta->rta_len = RTA_LENGTH(4);
*(int *)RTA_DATA(rta) = if_index(ifname);
req.nlh.nlmsg_len += rta->rta_len;
if (send(fd, &req, req.nlh.nlmsg_len, 0) != (ssize_t)req.nlh.nlmsg_len)
die("send netlink route");
close(fd);
}
static void if_up_netlink(const char *ifname) {
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0)
die("socket");
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifi;
} req = {0};
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nlh.nlmsg_type = RTM_NEWLINK;
req.nlh.nlmsg_flags = NLM_F_REQUEST;
req.ifi.ifi_family = AF_UNSPEC;
req.ifi.ifi_index = if_index(ifname);
req.ifi.ifi_flags = IFF_UP | IFF_RUNNING;
req.ifi.ifi_change = IFF_UP | IFF_RUNNING;
if (send(fd, &req, req.nlh.nlmsg_len, 0) < 0)
die("send netlink");
close(fd);
}
/* ----------- TCP/IP helpers ------------------ */
static uint32_t csum16_partial(const void *buf, size_t len);
static const struct dns_mapping *dns_mapping_find_ip(uint32_t synthetic_ip);
static int dns_is_synthetic_ip(uint32_t ip_be);
static uint16_t csum16_fold(uint32_t sum) {
while (sum >> 16)
sum = (sum & 0xffffU) + (sum >> 16);
return (uint16_t)~sum;
}
static uint16_t csum16(const void *buf, size_t len) {
return csum16_fold(csum16_partial(buf, len));
}
/* ---------- Persistent UDP Flow Management ---------- */
static void udp_close_flow(struct udp_flow *f);
static void udp_flow_begin(struct udp_flow *f) {
uint32_t generation = epoll_next_generation(f->generation);
memset(f, 0, sizeof(*f));
f->generation = generation;
f->tcp_ctrl = -1;
f->udp_relay = -1;
f->udp_staging = -1;
}
static struct udp_flow *udp_find(uint32_t cip, uint16_t cport, uint32_t sip,
uint16_t sport) {
for (int i = 0; i < udp_flow_limit; i++) {
struct udp_flow *f = &udp_flows[i];
/* Full 4-tuple match for proper flow isolation */
if ((f->udp_relay >= 0 || f->udp_staging >= 0 || f->tcp_ctrl >= 0) &&
f->cli_ip == cip &&
f->cli_port == cport &&
f->srv_ip == sip && f->srv_port == sport)
return f;
}
return NULL;
}
static struct udp_flow *udp_alloc(void) {
/* First try to find an empty slot */
for (int i = 0; i < udp_flow_limit; i++) {
if (udp_flows[i].udp_relay < 0 && udp_flows[i].udp_staging < 0 &&
udp_flows[i].tcp_ctrl < 0)
return &udp_flows[i];
}
/* Otherwise evict oldest */
struct udp_flow *oldest = &udp_flows[0];
for (int i = 1; i < udp_flow_limit; i++) {
if (udp_flows[i].last_used < oldest->last_used)
oldest = &udp_flows[i];
}
DBG("[parent] UDP flow table full (%d slots); evicting oldest flow",
udp_flow_limit);
if (oldest->tcp_ctrl >= 0)
epoll_del(oldest->tcp_ctrl);
if (oldest->tcp_ctrl >= 0)
close(oldest->tcp_ctrl);
if (oldest->udp_relay >= 0) {
epoll_del(oldest->udp_relay);
close(oldest->udp_relay);
}
if (oldest->udp_staging >= 0)
close(oldest->udp_staging);
return oldest;
}
static uint32_t udp_ctrl_events(const struct udp_flow *f) {
uint32_t events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP;
if (f->socks.connect_pending || socks_has_pending_tx(&f->socks))
events |= EPOLLOUT;
return events;
}
static void udp_update_events(struct udp_flow *f) {
uint32_t events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP;
if (f->udp_relay >= 0 && f->pending_set)
events |= EPOLLOUT;
epoll_mod_udp(f, events);
}
static int udp_queue_pending(struct udp_flow *f, const uint8_t *data, size_t len) {
if (len > sizeof(f->pending_data))
return -1;
if (f->pending_set) {
f->dropped_backpressure++;
DBG("UDP pending queue full; dropping newest datagram (drops=%lu)",
f->dropped_backpressure);
return 1;
}
memcpy(f->pending_data, data, len);
f->pending_len = len;
f->pending_set = 1;
f->last_used = time(NULL);
udp_update_events(f);
return 0;
}
static int udp_socks_setup(struct udp_flow *f, struct socks_config *cfg) {
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
int udp_sock = -1;
struct sockaddr_in local = {.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
.sin_port = 0};
socklen_t locallen = sizeof(local);
int rc;
if (tcp_sock < 0)
return -1;
udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_sock < 0) {
close(tcp_sock);
return -1;
}
if (bind(udp_sock, (struct sockaddr *)&local, sizeof(local)) < 0) {
close(udp_sock);
close(tcp_sock);
return -1;
}
if (getsockname(udp_sock, (struct sockaddr *)&local, &locallen) < 0) {
close(udp_sock);
close(tcp_sock);
return -1;
}
if (set_nonblocking(udp_sock) < 0) {
close(udp_sock);
close(tcp_sock);
return -1;
}
socks_io_reset(&f->socks);
f->socks.active = 1;
f->socks.is_udp = 1;
f->socks.target_ip = 0;
f->socks.target_port = ntohs(local.sin_port);
rc = start_nonblocking_connect(tcp_sock, &cfg->addr);
if (rc < 0) {
close(udp_sock);
close(tcp_sock);
return -1;
}
f->tcp_ctrl = tcp_sock;
f->udp_relay = -1;
f->udp_staging = udp_sock;
f->socks.connect_pending = (rc > 0);
f->socks.state = f->socks.connect_pending ? SOCKS_IO_CONNECTING
: SOCKS_IO_METHOD;
if (!f->socks.connect_pending && socks_begin_handshake(&f->socks, cfg) < 0) {
close(udp_sock);
close(tcp_sock);
f->tcp_ctrl = -1;
f->udp_staging = -1;
return -1;
}
epoll_add_udp_ctrl(f, udp_ctrl_events(f));
return 0;
}
static int udp_direct_setup(struct udp_flow *f, uint32_t dst_ip,
uint16_t dst_port) {
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_sock < 0)
return -1;
struct sockaddr_in dst = {
.sin_family = AF_INET,
.sin_port = htons(dst_port),
.sin_addr.s_addr = dst_ip,
};
if (connect(udp_sock, (struct sockaddr *)&dst, sizeof(dst)) < 0) {
close(udp_sock);
return -1;
}
if (set_nonblocking(udp_sock) < 0) {
close(udp_sock);
return -1;
}
f->tcp_ctrl = -1;
f->udp_relay = udp_sock;
f->relay_addr = dst;
epoll_add_udp(f);
udp_update_events(f);
return 0;
}
static int udp_try_send_socks(struct udp_flow *f, const uint8_t *payload,
size_t payload_len) {
uint8_t pkt[65536];
struct in_addr dst_addr = {.s_addr = f->srv_ip};
const struct dns_mapping *dns_target = NULL;
size_t domain_len = 0;
size_t hdr_len;
size_t off = 0;
if (socks_proxy.remote_dns)
dns_target = dns_mapping_find_ip(f->srv_ip);
if (dns_target == NULL && socks_proxy.remote_dns &&
dns_is_synthetic_ip(f->srv_ip))
return -1;
if (dns_target != NULL)
domain_len = strlen(dns_target->name);
if (domain_len > 255)
return -1;
hdr_len = dns_target != NULL ? 4 + 1 + domain_len + 2 : 10;
if (payload_len > sizeof(pkt) - hdr_len)
return -1;
pkt[off++] = 0;
pkt[off++] = 0;
pkt[off++] = 0; /* RSV, FRAG */
if (dns_target != NULL) {
pkt[off++] = 0x03;
pkt[off++] = (uint8_t)domain_len;
memcpy(pkt + off, dns_target->name, domain_len);
off += domain_len;
} else {
pkt[off++] = 0x01;
memcpy(pkt + off, &f->srv_ip, 4);
off += 4;
}
pkt[off++] = (uint8_t)(f->srv_port >> 8);
pkt[off++] = (uint8_t)(f->srv_port & 0xff);
memcpy(pkt + off, payload, payload_len);
DBG("UDP via SOCKS relay %s:%d -> %s:%d len=%zu%s%s",
inet_ntoa(f->relay_addr.sin_addr), ntohs(f->relay_addr.sin_port),
inet_ntoa(dst_addr), f->srv_port, payload_len,
dns_target != NULL ? " domain=" : "",
dns_target != NULL ? dns_target->name : "");
for (;;) {
ssize_t n = sendto(f->udp_relay, pkt, hdr_len + payload_len, 0,
(struct sockaddr *)&f->relay_addr,
sizeof(f->relay_addr));
if (n == (ssize_t)(hdr_len + payload_len)) {
f->last_used = time(NULL);
return 0;
}
if (n >= 0) {
DBG("SOCKS UDP short send: %zd/%zu, closing flow", n,
hdr_len + payload_len);
return -1;
}
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
int qrc = udp_queue_pending(f, payload, payload_len);
if (qrc < 0)
return -1;
return 0;
}
if (errno == ENOBUFS || errno == ENOMEM) {
f->dropped_backpressure++;
DBG("SOCKS UDP send drop due to %s (drops=%lu)", strerror(errno),
f->dropped_backpressure);
return 0;
}
return -1;
}
}
static int udp_try_send_direct(struct udp_flow *f, const uint8_t *payload,
size_t payload_len) {
for (;;) {
ssize_t n = send(f->udp_relay, payload, payload_len, MSG_NOSIGNAL);
if (n == (ssize_t)payload_len) {
f->last_used = time(NULL);
return 0;
}
if (n >= 0) {
DBG("UDP short send: %zd/%zu, closing flow", n, payload_len);
return -1;
}
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
int qrc = udp_queue_pending(f, payload, payload_len);
if (qrc < 0)
return -1;
return 0;
}
if (errno == ENOBUFS || errno == ENOMEM) {
f->dropped_backpressure++;
DBG("UDP send drop due to %s (drops=%lu)", strerror(errno),
f->dropped_backpressure);
return 0;
}
return -1;
}
}
static int udp_open_relay_socket(struct udp_flow *f,
const struct sockaddr_in *relay_addr) {
int udp_sock = f->udp_staging;
if (udp_sock < 0)
return -1;
f->udp_relay = udp_sock;
f->udp_staging = -1;
f->relay_addr = *relay_addr;
epoll_add_udp(f);
udp_update_events(f);
return 0;
}
static int udp_flush_pending(struct udp_flow *f) {
if (f->udp_relay < 0 || !f->pending_set)
return 0;
size_t len = f->pending_len;
uint8_t payload[65535];
int rc;
memcpy(payload, f->pending_data, len);
f->pending_set = 0;
f->pending_len = 0;
DBG("Flushing pending UDP datagram len=%zu", len);
if (f->tcp_ctrl >= 0)
rc = udp_try_send_socks(f, payload, len);
else
rc = udp_try_send_direct(f, payload, len);
if (rc < 0)
return -1;
udp_update_events(f);
return 0;
}
/* Inject UDP packet into TUN (response from server to client) */
static void udp_inject_tun(int tunfd, struct udp_flow *f, const uint8_t *data,
size_t len) {
uint8_t out[65536];
if (len > sizeof(out) - sizeof(struct iphdr) - sizeof(struct udphdr))
return;
struct iphdr *ip = (struct iphdr *)out;
struct udphdr *udp = (struct udphdr *)(out + sizeof(*ip));
memset(ip, 0, sizeof(*ip));
ip->version = 4;
ip->ihl = 5;
ip->ttl = 64;
ip->protocol = IPPROTO_UDP;
ip->saddr = f->srv_ip;
ip->daddr = f->cli_ip;
ip->tot_len = htons((uint16_t)(sizeof(*ip) + sizeof(*udp) + len));
ip->check = csum16(ip, sizeof(*ip));
udp->source = htons(f->srv_port);
udp->dest = htons(f->cli_port);
udp->len = htons((uint16_t)(sizeof(*udp) + len));
udp->check = 0;
memcpy(out + sizeof(*ip) + sizeof(*udp), data, len);
IGNORE_RESULT(tun_write_packet(tunfd, out, sizeof(*ip) + sizeof(*udp) + len,
"UDP inject"));
}
static uint16_t dns_get_u16(const uint8_t *p) {
return (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
}
static void dns_put_u16(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)(v & 0xff);
}
static void dns_put_u32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)((v >> 16) & 0xff);
p[2] = (uint8_t)((v >> 8) & 0xff);
p[3] = (uint8_t)(v & 0xff);
}
static int dns_parse_question(const uint8_t *query, size_t len, char *name,
size_t name_len, uint16_t *qtype,
uint16_t *qclass, size_t *question_end) {
size_t off = 12;
size_t out = 0;
int labels = 0;
if (len < 12 || dns_get_u16(query + 4) != 1)
return -1;
if ((dns_get_u16(query + 2) & 0x8000U) != 0)
return -1;
for (;;) {
uint8_t label_len;
if (off >= len)
return -1;
label_len = query[off++];
if ((label_len & 0xc0U) != 0)
return -1;
if (label_len == 0)
break;
if (label_len > 63 || off + label_len > len)
return -1;
if (labels > 0) {
if (out + 1 >= name_len)
return -1;
name[out++] = '.';
}
if (out + label_len >= name_len)
return -1;
for (uint8_t i = 0; i < label_len; ++i) {
unsigned char ch = query[off + i];
if (ch <= 0x20 || ch >= 0x7f)
return -1;
name[out++] = (char)tolower(ch);
}
off += label_len;
++labels;
}
if (out == 0 || off + 4 > len)
return -1;
name[out] = '\0';
*qtype = dns_get_u16(query + off);
*qclass = dns_get_u16(query + off + 2);
*question_end = off + 4;
return 0;
}
static struct dns_mapping *dns_mapping_find_name(const char *name) {
for (int i = 0; i < MAX_DNS_MAPPINGS; ++i) {
if (dns_mappings[i].used && strcmp(dns_mappings[i].name, name) == 0)
return &dns_mappings[i];
}
return NULL;
}
static const struct dns_mapping *dns_mapping_find_ip(uint32_t synthetic_ip) {
for (int i = 0; i < MAX_DNS_MAPPINGS; ++i) {
if (dns_mappings[i].used &&
dns_mappings[i].synthetic_ip == synthetic_ip) {
dns_mappings[i].last_used = time(NULL);
return &dns_mappings[i];
}
}
return NULL;
}
static int dns_is_synthetic_ip(uint32_t ip_be) {
return (ntohl(ip_be) & 0xffff0000U) == DNS_SYNTHETIC_BASE;
}
static uint32_t dns_next_synthetic_ip(void) {
uint32_t host_part = next_dns_synthetic_host++;
if (next_dns_synthetic_host > 0xfffeU)
next_dns_synthetic_host = 1;
return htonl(DNS_SYNTHETIC_BASE | host_part);
}
static struct dns_mapping *dns_mapping_get_or_create(const char *name) {
struct dns_mapping *mapping = dns_mapping_find_name(name);
if (mapping != NULL) {
mapping->last_used = time(NULL);
return mapping;
}
for (int i = 0; i < MAX_DNS_MAPPINGS; ++i) {
if (!dns_mappings[i].used) {
dns_mappings[i].used = 1;
dns_mappings[i].synthetic_ip = dns_next_synthetic_ip();
dns_mappings[i].last_used = time(NULL);
snprintf(dns_mappings[i].name, sizeof(dns_mappings[i].name), "%s", name);
DBG("DNS synthetic mapping: %s -> %s", name,
inet_ntoa((struct in_addr){.s_addr = dns_mappings[i].synthetic_ip}));
return &dns_mappings[i];
}
}
DBG("DNS synthetic mapping table full; refusing %s", name);
return NULL;
}
static int dns_build_response(const uint8_t *query, size_t query_len,
uint8_t *response, size_t response_cap,
size_t *response_len) {
char name[256];
uint16_t qtype;
uint16_t qclass;
uint16_t req_flags;
uint16_t flags;
size_t question_end;
size_t question_len;
size_t off;
struct dns_mapping *mapping = NULL;
int answer_a = 0;
if (dns_parse_question(query, query_len, name, sizeof(name), &qtype, &qclass,
&question_end) < 0)
return -1;
if (question_end > query_len)
return -1;
if (qclass == 1 && qtype == 1) {
mapping = dns_mapping_get_or_create(name);
answer_a = mapping != NULL;
} else if (qclass == 1 && qtype == 28) {
DBG("DNS proxy returning empty AAAA answer for %s", name);
} else {
DBG("DNS proxy returning empty answer for %s type=%u class=%u", name,
(unsigned)qtype, (unsigned)qclass);
}
question_len = question_end - 12;
if (12 + question_len + (answer_a ? 16U : 0U) > response_cap)
return -1;
req_flags = dns_get_u16(query + 2);
flags = (uint16_t)(0x8000U | 0x0080U | (req_flags & 0x0100U));
memcpy(response, query, 2);
dns_put_u16(response + 2, flags);
dns_put_u16(response + 4, 1);
dns_put_u16(response + 6, answer_a ? 1 : 0);
dns_put_u16(response + 8, 0);
dns_put_u16(response + 10, 0);
memcpy(response + 12, query + 12, question_len);
off = 12 + question_len;
if (answer_a) {
response[off++] = 0xc0;
response[off++] = 0x0c;
dns_put_u16(response + off, 1);
off += 2;
dns_put_u16(response + off, 1);
off += 2;
dns_put_u32(response + off, 60);
off += 4;
dns_put_u16(response + off, 4);
off += 2;
memcpy(response + off, &mapping->synthetic_ip, 4);
off += 4;
}
*response_len = off;
return 0;
}
static struct publish_udp_flow *publish_udp_find(uint16_t synthetic_port,
uint16_t child_port);
static void handle_dns_proxy(int tunfd, const struct iphdr *ip,
const struct udphdr *udp, const uint8_t *payload,
size_t payload_len) {
uint8_t response[512];
size_t response_len = 0;
struct udp_flow reply_flow;
if (dns_build_response(payload, payload_len, response, sizeof(response),
&response_len) < 0) {
DBG("DNS proxy dropped malformed or unsupported query");
return;
}
memset(&reply_flow, 0, sizeof(reply_flow));
reply_flow.cli_ip = ip->saddr;
reply_flow.cli_port = ntohs(udp->source);
reply_flow.srv_ip = ip->daddr;
reply_flow.srv_port = ntohs(udp->dest);
udp_inject_tun(tunfd, &reply_flow, response, response_len);
}
static int handle_publish_udp_from_child(const struct iphdr *ip,
const struct udphdr *udp,
const uint8_t *payload,
size_t payload_len) {
uint16_t sport = ntohs(udp->source);
uint16_t dport = ntohs(udp->dest);
struct publish_udp_flow *f;
struct publish_rule *rule;
if (ip->daddr != PUBLISH_SYNTHETIC_IP)
return 0;
if (ip->saddr != SANDBOX_IP)
return 1;
f = publish_udp_find(dport, sport);
if (f == NULL)
return 1;
if (f->rule_index < 0 || f->rule_index >= publish_rule_count)
return 1;
rule = &publish_rules[f->rule_index];
if (rule->proto != IPPROTO_UDP || rule->listen_fd < 0 ||
rule->container_port != sport)
return 1;
for (;;) {
ssize_t n = sendto(rule->listen_fd, payload, payload_len, MSG_NOSIGNAL,
(struct sockaddr *)&f->host_peer,
sizeof(f->host_peer));
if (n == (ssize_t)payload_len) {
f->last_active = time(NULL);
return 1;
}
if (n >= 0)
return 1;
if (errno == EINTR)
continue;
return 1;
}
}
static void handle_udp(int tunfd, uint8_t *pkt, ssize_t len) {
if (len <= 0)
return;
size_t ulen = (size_t)len;
struct iphdr *ip = (struct iphdr *)pkt;
size_t iphl = ip->ihl * 4;
if (ip->version != 4)
return;
if (iphl < sizeof(struct iphdr) || iphl > 60 || iphl > ulen)
return;
size_t ip_total_len = (size_t)ntohs(ip->tot_len);
if (ip_total_len < iphl || ip_total_len > ulen)
return;
/* Reject IP fragments */
if (ntohs(ip->frag_off) & (IP_MF | IP_OFFMASK)) {
DBG("UDP: dropping IP fragment (frag_off=0x%04x)", ntohs(ip->frag_off));
return;
}
if (ulen < iphl + sizeof(struct udphdr))
return;
struct udphdr *udp = (struct udphdr *)(pkt + iphl);
size_t udp_len = (size_t)ntohs(udp->len);
if (udp_len < sizeof(struct udphdr) || udp_len > ip_total_len - iphl)
return;
uint16_t dport = ntohs(udp->dest);
uint16_t sport = ntohs(udp->source);
uint8_t *payload = pkt + iphl + sizeof(struct udphdr);
size_t plen = udp_len - sizeof(struct udphdr);
if (handle_publish_udp_from_child(ip, udp, payload, plen))
return;
if (egress_mode == EGRESS_SOCKS && socks_proxy.remote_dns &&
ip->daddr == DNS_PROXY_IP && dport == 53) {
handle_dns_proxy(tunfd, ip, udp, payload, plen);
return;
}
/* Check for host gateway access (10.0.1.x -> 127.0.0.x) */
uint32_t target_ip = ip->daddr;
int is_gateway = is_gateway_ip(target_ip);
if (egress_mode == EGRESS_NONE) {
DBG("[parent] UDP egress blocked by --egress=none: %s:%u",
inet_ntoa((struct in_addr){.s_addr = target_ip}), dport);
return;
}
if (is_gateway) {
if (!is_gateway_allowed(target_ip, dport, IPPROTO_UDP)) {
DBG("[parent] UDP to 10.0.1.%d:%d blocked", gateway_last_octet(target_ip),
dport);
return;
}
target_ip = gateway_to_localhost(ip->daddr);
DBG("[parent] UDP gateway: 10.0.1.%d:%d -> 127.0.0.%d:%d",
gateway_last_octet(ip->daddr), dport, gateway_last_octet(ip->daddr),
dport);
} else if (egress_mode == EGRESS_DIRECT &&
!is_direct_egress_allowed(target_ip, dport, IPPROTO_UDP)) {
log_direct_egress_block(target_ip, dport, IPPROTO_UDP);
return;
}
if (egress_mode == EGRESS_SOCKS && !is_gateway) {
/* Find or create persistent UDP flow */
struct udp_flow *f = udp_find(ip->saddr, sport, ip->daddr, dport);
if (!f) {
f = udp_alloc();
udp_flow_begin(f);
f->cli_ip = ip->saddr;
f->cli_port = sport;
f->srv_ip = ip->daddr;
f->srv_port = dport;
f->tcp_ctrl = -1;
f->udp_relay = -1;
f->udp_staging = -1;
f->pending_set = 0;
f->pending_len = 0;
socks_io_reset(&f->socks);
if (udp_socks_setup(f, &socks_proxy) < 0) {
f->udp_relay = -1;
return;
}
}
/* Update source port for response routing (may differ on reused flow) */
f->cli_port = sport;
if (f->udp_relay >= 0) {
if (udp_try_send_socks(f, payload, plen) < 0) {
udp_close_flow(f);
return;
}
udp_update_events(f);
} else {
int qrc = udp_queue_pending(f, payload, plen);
if (qrc < 0) {
udp_close_flow(f);
return;
}
}
} else {
/* Direct UDP now uses persistent non-blocking flows too. */
struct udp_flow *f = udp_find(ip->saddr, sport, ip->daddr, dport);
if (!f) {
f = udp_alloc();
udp_flow_begin(f);
f->cli_ip = ip->saddr;
f->cli_port = sport;
f->srv_ip = ip->daddr;
f->srv_port = dport;
f->tcp_ctrl = -1;
f->udp_relay = -1;
f->udp_staging = -1;
if (udp_direct_setup(f, target_ip, dport) < 0) {
f->udp_relay = -1;
return;
}
}
f->cli_port = sport;
if (udp_try_send_direct(f, payload, plen) < 0) {
udp_close_flow(f);
return;
}
udp_update_events(f);
DBG("UDP queued %zu bytes to %s:%d", plen,
is_gateway ? "127.0.0.1" : "remote", dport);
}
}
/* Handle ICMP echo request (ping) to gateway - responds directly */
static void handle_icmp(int tunfd, uint8_t *pkt, ssize_t len) {
if (len <= 0)
return;
size_t ulen = (size_t)len;
struct iphdr *ip = (struct iphdr *)pkt;
size_t iphl = ip->ihl * 4;
if (ip->version != 4)
return;
if (iphl < sizeof(struct iphdr) || iphl > 60 || iphl > ulen)
return;
size_t ip_total_len = (size_t)ntohs(ip->tot_len);
if (ip_total_len < iphl || ip_total_len > ulen)
return;
/* Reject IP fragments */
if (ntohs(ip->frag_off) & (IP_MF | IP_OFFMASK)) {
DBG("ICMP: dropping IP fragment (frag_off=0x%04x)", ntohs(ip->frag_off));
return;
}
if (ip_total_len < iphl + 8) /* ICMP header is 8 bytes minimum */
return;
/* Only respond to ping on 10.0.0.1 (always allowed) */
if (ip->daddr != HOST_PING_IP)
return;
uint8_t *icmp = pkt + iphl;
uint8_t type = icmp[0];
/* Only respond to echo request (type 8) */
if (type != 8)
return;
DBG("ICMP echo request to gateway - sending reply");
/* Build echo reply */
uint8_t out[65536];
size_t icmp_len = ip_total_len - iphl;
struct iphdr *rip = (struct iphdr *)out;
memset(rip, 0, sizeof(*rip));
rip->version = 4;
rip->ihl = 5;
rip->ttl = 64;
rip->protocol = IPPROTO_ICMP;
rip->saddr = ip->daddr; /* Gateway IP */
rip->daddr = ip->saddr; /* Client IP */
rip->tot_len = htons((uint16_t)(sizeof(*rip) + icmp_len));
rip->check = csum16(rip, sizeof(*rip));
/* Copy ICMP data and change type to echo reply (0) */
memcpy(out + sizeof(*rip), icmp, icmp_len);
out[sizeof(*rip)] = 0; /* Type = echo reply */
/* Recalculate ICMP checksum */
uint8_t *ricmp = out + sizeof(*rip);
ricmp[2] = 0;
ricmp[3] = 0;
uint16_t icmp_csum = csum16(ricmp, icmp_len);
ricmp[2] = (uint8_t)(icmp_csum & 0xff);
ricmp[3] = (uint8_t)(icmp_csum >> 8);
IGNORE_RESULT(
tun_write_packet(tunfd, out, sizeof(*rip) + icmp_len, "ICMP reply"));
}
static struct tcp_flow *tcp_find(uint32_t cip, uint16_t cport, uint32_t sip,
uint16_t sport) {
for (int i = 0; i < tcp_flow_limit; i++) {
struct tcp_flow *f = &tcp_flows[i];
if (f->sock >= 0 && f->cli_ip == cip && f->cli_port == cport &&
f->srv_ip == sip && f->srv_port == sport)
return f;
}
return NULL;
}
static void tcp_flow_begin(struct tcp_flow *f) {
uint32_t generation = epoll_next_generation(f->generation);
memset(f, 0, sizeof(*f));
f->generation = generation;
f->sock = -1;
}
static struct tcp_flow *tcp_alloc(void) {
for (int i = 0; i < tcp_flow_limit; i++) {
if (tcp_flows[i].sock < 0)
return &tcp_flows[i];
}
struct tcp_flow *oldest = NULL;
for (int i = 0; i < tcp_flow_limit; i++) {
if (tcp_flows[i].state == SP_TCP_ESTABLISHED)
continue;
if (oldest == NULL || tcp_flows[i].last_active < oldest->last_active)
oldest = &tcp_flows[i];
}
if (oldest == NULL)
return NULL;
DBG("[parent] TCP flow table full (%d slots); evicting oldest non-established flow",
tcp_flow_limit);
if (oldest->sock >= 0) {
if (g_epfd >= 0)
epoll_ctl(g_epfd, EPOLL_CTL_DEL, oldest->sock, NULL);
close(oldest->sock);
}
oldest->sock = -1;
oldest->state = SP_TCP_CLOSED;
oldest->pending_write_len = 0;
oldest->pending_fin = 0;
oldest->backend_ready = 0;
memset(&oldest->socks, 0, sizeof(oldest->socks));
return oldest;
}
static uint32_t csum16_partial(const void *buf, size_t len) {
const uint8_t *p = buf;
uint32_t sum = 0;
while (len > 1) {
/* Little-endian integer construction from bytes */
sum += (uint32_t)p[0] | ((uint32_t)p[1] << 8);
p += 2;
len -= 2;
}
if (len)
sum += (uint32_t)p[0];
return sum;
}
static uint32_t checksum_add_bytes(const void *buf, size_t len) {
const uint8_t *p = buf;
uint32_t sum = 0;
while (len > 1) {
sum += ((uint32_t)p[0] << 8) | (uint32_t)p[1];
p += 2;
len -= 2;
}
if (len > 0)
sum += (uint32_t)p[0] << 8;
return sum;
}
static uint32_t checksum_add_ipv4_pseudo(uint32_t saddr, uint32_t daddr,
uint8_t proto, uint16_t len) {
uint8_t pseudo[12];
pseudo[0] = (uint8_t)((ntohl(saddr) >> 24) & 0xff);
pseudo[1] = (uint8_t)((ntohl(saddr) >> 16) & 0xff);
pseudo[2] = (uint8_t)((ntohl(saddr) >> 8) & 0xff);
pseudo[3] = (uint8_t)(ntohl(saddr) & 0xff);
pseudo[4] = (uint8_t)((ntohl(daddr) >> 24) & 0xff);
pseudo[5] = (uint8_t)((ntohl(daddr) >> 16) & 0xff);
pseudo[6] = (uint8_t)((ntohl(daddr) >> 8) & 0xff);
pseudo[7] = (uint8_t)(ntohl(daddr) & 0xff);
pseudo[8] = 0;
pseudo[9] = proto;
pseudo[10] = (uint8_t)(len >> 8);
pseudo[11] = (uint8_t)(len & 0xff);
return checksum_add_bytes(pseudo, sizeof(pseudo));
}
static uint16_t checksum_finish(uint32_t sum) {
while (sum >> 16)
sum = (sum & 0xffffU) + (sum >> 16);
return (uint16_t)~sum;
}
static int tcp_checksum_valid(const struct iphdr *ip, const struct tcphdr *tcp,
size_t tcp_len) {
uint32_t sum;
if (tcp_len < sizeof(struct tcphdr))
return 0;
sum = checksum_add_ipv4_pseudo(ip->saddr, ip->daddr, IPPROTO_TCP,
(uint16_t)tcp_len);
sum += checksum_add_bytes(tcp, tcp_len);
return checksum_finish(sum) == 0;
}
static int udp_checksum_valid(const struct iphdr *ip, const struct udphdr *udp,
size_t udp_len) {
uint32_t sum;
if (udp->check == 0)
return 1;
if (udp_len < sizeof(struct udphdr))
return 0;
sum = checksum_add_ipv4_pseudo(ip->saddr, ip->daddr, IPPROTO_UDP,
(uint16_t)udp_len);
sum += checksum_add_bytes(udp, udp_len);
return checksum_finish(sum) == 0;
}
static int icmp_checksum_valid(const uint8_t *icmp, size_t icmp_len) {
uint32_t sum;
if (icmp_len == 0)
return 0;
sum = checksum_add_bytes(icmp, icmp_len);
return checksum_finish(sum) == 0;
}
static uint16_t tcp_checksum(struct iphdr *ip, struct tcphdr *tcp,
size_t tcp_len, const uint8_t *payload,
size_t plen) {
struct {
uint32_t src;
uint32_t dst;
uint8_t zero;
uint8_t proto;
uint16_t len;
} __attribute__((packed)) pseudo;
memset(&pseudo, 0, sizeof(pseudo));
pseudo.src = ip->saddr;
pseudo.dst = ip->daddr;
pseudo.zero = 0;
pseudo.proto = IPPROTO_TCP;
pseudo.len = htons((uint16_t)(tcp_len + plen));
uint32_t sum = 0;
uint32_t p1 = csum16_partial(&pseudo, sizeof(pseudo));
uint32_t p2 = csum16_partial(tcp, tcp_len);
uint32_t p3 = plen ? csum16_partial(payload, plen) : 0;
sum = p1 + p2 + p3;
while (sum >> 16)
sum = (sum & 0xffff) + (sum >> 16);
return (uint16_t)~sum;
}
static uint16_t ip_checksum(const void *buf, size_t len) {
const uint16_t *p = buf;
uint32_t sum = 0;
while (len > 1) {
sum += *p++;
len -= 2;
}
if (len)
sum += *(const uint8_t *)p;
while (sum >> 16)
sum = (sum & 0xffff) + (sum >> 16);
return (uint16_t)~sum;
}
static uint16_t tcp_advertised_window(const struct tcp_flow *f) {
size_t free_bytes = 0;
if (f->pending_write_len < TCP_PENDING_WRITE_CAP)
free_bytes = TCP_PENDING_WRITE_CAP - f->pending_write_len;
if (free_bytes > 65535)
free_bytes = 65535;
return (uint16_t)free_bytes;
}
static inline int tcp_seq_before(uint32_t a, uint32_t b) {
return (int32_t)(a - b) < 0;
}
static inline int tcp_seq_after(uint32_t a, uint32_t b) {
return tcp_seq_before(b, a);
}
static inline int tcp_seq_before_or_equal(uint32_t a, uint32_t b) {
return a == b || tcp_seq_before(a, b);
}
static inline int tcp_seq_after_or_equal(uint32_t a, uint32_t b) {
return a == b || tcp_seq_after(a, b);
}
static inline uint32_t tcp_seq_add_len(uint32_t seq, size_t len) {
return seq + (uint32_t)len;
}
static int ipv4_is_loopback(uint32_t ip_be) {
return (ntohl(ip_be) & 0xff000000U) == 0x7f000000U;
}
static struct publish_tcp_flow *publish_tcp_find(uint32_t child_ip,
uint16_t child_port,
uint32_t synthetic_ip,
uint16_t synthetic_port) {
for (int i = 0; i < MAX_PUBLISH_TCP; ++i) {
struct publish_tcp_flow *f = &publish_tcp_flows[i];
if (f->used && f->child_ip == child_ip && f->child_port == child_port &&
f->synthetic_ip == synthetic_ip &&
f->synthetic_port == synthetic_port)
return f;
}
return NULL;
}
static struct publish_tcp_flow *publish_tcp_alloc(void) {
for (int i = 0; i < MAX_PUBLISH_TCP; ++i) {
if (!publish_tcp_flows[i].used)
return &publish_tcp_flows[i];
}
return NULL;
}
static void publish_tcp_flow_begin(struct publish_tcp_flow *f) {
uint32_t generation = epoll_next_generation(f->generation);
memset(f, 0, sizeof(*f));
f->generation = generation;
f->host_fd = -1;
f->state = PUBLISH_TCP_CLOSED;
}
static struct publish_udp_flow *publish_udp_find(uint16_t synthetic_port,
uint16_t child_port) {
for (int i = 0; i < MAX_PUBLISH_UDP; ++i) {
struct publish_udp_flow *f = &publish_udp_flows[i];
if (f->used && f->synthetic_port == synthetic_port &&
f->child_port == child_port)
return f;
}
return NULL;
}
static struct publish_udp_flow *publish_udp_find_host_peer(
int rule_index, const struct sockaddr_in *host_peer) {
for (int i = 0; i < MAX_PUBLISH_UDP; ++i) {
struct publish_udp_flow *f = &publish_udp_flows[i];
if (f->used && f->rule_index == rule_index &&
f->host_peer.sin_addr.s_addr == host_peer->sin_addr.s_addr &&
f->host_peer.sin_port == host_peer->sin_port)
return f;
}
return NULL;
}
static struct publish_udp_flow *publish_udp_alloc(void) {
struct publish_udp_flow *oldest = NULL;
for (int i = 0; i < MAX_PUBLISH_UDP; ++i) {
if (!publish_udp_flows[i].used)
return &publish_udp_flows[i];
if (oldest == NULL ||
publish_udp_flows[i].last_active < oldest->last_active)
oldest = &publish_udp_flows[i];
}
if (oldest != NULL)
memset(oldest, 0, sizeof(*oldest));
return oldest;
}
static uint16_t publish_alloc_synthetic_port(int proto) {
unsigned int span = PUBLISH_SYNTH_PORT_MAX - PUBLISH_SYNTH_PORT_MIN + 1U;
for (unsigned int attempt = 0; attempt < span; ++attempt) {
uint16_t port = next_publish_synth_port++;
int used = 0;
if (next_publish_synth_port > PUBLISH_SYNTH_PORT_MAX)
next_publish_synth_port = PUBLISH_SYNTH_PORT_MIN;
if (proto == IPPROTO_TCP) {
for (int i = 0; i < MAX_PUBLISH_TCP; ++i) {
if (publish_tcp_flows[i].used &&
publish_tcp_flows[i].synthetic_port == port) {
used = 1;
break;
}
}
} else {
for (int i = 0; i < MAX_PUBLISH_UDP; ++i) {
if (publish_udp_flows[i].used &&
publish_udp_flows[i].synthetic_port == port) {
used = 1;
break;
}
}
}
if (!used)
return port;
}
return 0;
}
static uint16_t publish_tcp_advertised_window(const struct publish_tcp_flow *f) {
size_t free_bytes = 0;
if (f->pending_child_len < PUBLISH_TCP_PENDING_CAP)
free_bytes = PUBLISH_TCP_PENDING_CAP - f->pending_child_len;
if (free_bytes > 65535)
free_bytes = 65535;
return (uint16_t)free_bytes;
}
static int publish_tcp_send_packet(int tunfd, struct publish_tcp_flow *f,
uint8_t flags, const uint8_t *payload,
size_t plen) {
enum {
TCPF_FIN = 0x01,
TCPF_SYN = 0x02,
TCPF_RST = 0x04,
TCPF_PSH = 0x08,
TCPF_ACK = 0x10,
};
uint8_t out[65536];
struct iphdr *ip = (struct iphdr *)out;
struct tcphdr *tcp = (struct tcphdr *)(out + sizeof(*ip));
size_t tcp_hdr_len = sizeof(*tcp);
size_t total_len = sizeof(*ip) + tcp_hdr_len + plen;
if (plen > sizeof(out) - sizeof(*ip) - tcp_hdr_len)
return -1;
memset(ip, 0, sizeof(*ip));
ip->version = 4;
ip->ihl = 5;
ip->ttl = 64;
ip->protocol = IPPROTO_TCP;
ip->saddr = f->synthetic_ip;
ip->daddr = f->child_ip;
ip->tot_len = htons((uint16_t)total_len);
memset(tcp, 0, sizeof(*tcp));
tcp->source = htons(f->synthetic_port);
tcp->dest = htons(f->child_port);
tcp->seq = htonl(f->broker_next);
tcp->ack_seq = htonl(f->child_next);
tcp->doff = 5;
tcp->window = htons(publish_tcp_advertised_window(f));
tcp->fin = (flags & TCPF_FIN) != 0;
tcp->syn = (flags & TCPF_SYN) != 0;
tcp->rst = (flags & TCPF_RST) != 0;
tcp->psh = (flags & TCPF_PSH) != 0;
tcp->ack = (flags & TCPF_ACK) != 0;
if (plen > 0)
memcpy(out + sizeof(*ip) + tcp_hdr_len, payload, plen);
tcp->check = tcp_checksum(ip, tcp, tcp_hdr_len, payload, plen);
ip->check = ip_checksum(ip, sizeof(*ip));
if (tun_write_packet(tunfd, out, total_len, "publish TCP packet") < 0)
return -1;
if ((flags & TCPF_RST) == 0) {
if ((flags & TCPF_SYN) != 0)
f->broker_next = tcp_seq_add_len(f->broker_next, 1);
if (plen > 0)
f->broker_next = tcp_seq_add_len(f->broker_next, plen);
if ((flags & TCPF_FIN) != 0)
f->broker_next = tcp_seq_add_len(f->broker_next, 1);
}
return 0;
}
static void publish_tcp_update_events(struct publish_tcp_flow *f) {
uint32_t events = EPOLLRDHUP | EPOLLERR | EPOLLHUP;
if (f->state == PUBLISH_TCP_ESTABLISHED ||
f->state == PUBLISH_TCP_HOST_FIN ||
f->state == PUBLISH_TCP_CHILD_FIN)
events |= EPOLLIN;
if (f->pending_child_len > 0)
events |= EPOLLOUT;
epoll_mod_publish_tcp_host(f, events);
}
static void publish_tcp_close_flow(struct publish_tcp_flow *f) {
uint32_t generation = f->generation;
if (f->host_fd >= 0) {
epoll_del(f->host_fd);
close(f->host_fd);
}
memset(f, 0, sizeof(*f));
f->generation = generation;
f->host_fd = -1;
f->state = PUBLISH_TCP_CLOSED;
}
static void publish_tcp_close_with_rst(int tunfd, struct publish_tcp_flow *f) {
if (f->used && f->state != PUBLISH_TCP_CLOSED)
publish_tcp_send_packet(tunfd, f, 0x14, NULL, 0);
publish_tcp_close_flow(f);
}
static int publish_tcp_queue_child_bytes(struct publish_tcp_flow *f,
const uint8_t *data, size_t len) {
if (len == 0)
return 0;
if (f->pending_child_off != 0 && f->pending_child_len > 0) {
memmove(f->pending_child_to_host,
f->pending_child_to_host + f->pending_child_off,
f->pending_child_len);
f->pending_child_off = 0;
}
if (f->pending_child_len + len > sizeof(f->pending_child_to_host))
return -1;
memcpy(f->pending_child_to_host + f->pending_child_len, data, len);
f->pending_child_len += len;
return 0;
}
static int publish_tcp_maybe_shutdown_host_write(struct publish_tcp_flow *f) {
if (!f->child_fin_seen || f->host_write_shutdown || f->pending_child_len > 0)
return 0;
if (shutdown(f->host_fd, SHUT_WR) < 0 && errno != ENOTCONN &&
errno != EPIPE)
return -1;
f->host_write_shutdown = 1;
return 0;
}
static int publish_tcp_flush_child_to_host(int tunfd,
struct publish_tcp_flow *f) {
while (f->pending_child_len > 0) {
ssize_t n = send(f->host_fd,
f->pending_child_to_host + f->pending_child_off,
f->pending_child_len, MSG_NOSIGNAL);
if (n > 0) {
f->pending_child_off += (size_t)n;
f->pending_child_len -= (size_t)n;
f->last_active = time(NULL);
continue;
}
if (n < 0 && errno == EINTR)
continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
break;
publish_tcp_close_with_rst(tunfd, f);
return -1;
}
if (f->pending_child_len == 0)
f->pending_child_off = 0;
if (f->host_fd >= 0 && publish_tcp_maybe_shutdown_host_write(f) < 0) {
publish_tcp_close_with_rst(tunfd, f);
return -1;
}
if (f->host_fd >= 0) {
if (f->host_fin_sent && f->child_fin_seen && f->pending_child_len == 0) {
publish_tcp_close_flow(f);
return 0;
}
publish_tcp_update_events(f);
}
return 0;
}
static void publish_udp_inject_tun(int tunfd, const struct publish_udp_flow *f,
const uint8_t *data, size_t len) {
uint8_t out[65536];
struct iphdr *ip = (struct iphdr *)out;
struct udphdr *udp = (struct udphdr *)(out + sizeof(*ip));
size_t total_len = sizeof(*ip) + sizeof(*udp) + len;
if (len > sizeof(out) - sizeof(*ip) - sizeof(*udp))
return;
memset(ip, 0, sizeof(*ip));
ip->version = 4;
ip->ihl = 5;
ip->ttl = 64;
ip->protocol = IPPROTO_UDP;
ip->saddr = PUBLISH_SYNTHETIC_IP;
ip->daddr = SANDBOX_IP;
ip->tot_len = htons((uint16_t)total_len);
ip->check = csum16(ip, sizeof(*ip));
memset(udp, 0, sizeof(*udp));
udp->source = htons(f->synthetic_port);
udp->dest = htons(f->child_port);
udp->len = htons((uint16_t)(sizeof(*udp) + len));
udp->check = 0;
memcpy(out + sizeof(*ip) + sizeof(*udp), data, len);
IGNORE_RESULT(
tun_write_packet(tunfd, out, total_len, "publish UDP inject"));
}
/* Send a TCP packet from server to client */
static int send_tcp_packet(int tunfd, struct tcp_flow *f, uint8_t flags,
const uint8_t *payload, size_t plen) {
uint8_t out[65536];
struct iphdr *ip = (struct iphdr *)out;
struct tcphdr *tcp = (struct tcphdr *)(out + sizeof(*ip));
size_t tcp_hdr_len = sizeof(*tcp);
size_t total_len = sizeof(*ip) + tcp_hdr_len + plen;
memset(ip, 0, sizeof(*ip));
ip->version = 4;
ip->ihl = 5;
ip->ttl = 64;
ip->protocol = IPPROTO_TCP;
ip->saddr = f->srv_ip;
ip->daddr = f->cli_ip;
ip->tot_len = htons((uint16_t)total_len);
memset(tcp, 0, sizeof(*tcp));
tcp->source = htons(f->srv_port);
tcp->dest = htons(f->cli_port);
tcp->seq = htonl(f->srv_next);
tcp->ack_seq = htonl(f->cli_next);
tcp->doff = (tcp_hdr_len / 4) & 0xF;
tcp->ack = 1;
if (flags & 0x08)
tcp->psh = 1; /* PSH */
if (flags & 0x01)
tcp->fin = 1; /* FIN */
tcp->window = htons(tcp_advertised_window(f));
if (plen > 0)
memcpy(out + sizeof(*ip) + tcp_hdr_len, payload, plen);
tcp->check = tcp_checksum(ip, tcp, tcp_hdr_len, payload, plen);
ip->check = ip_checksum(ip, sizeof(*ip));
if (tun_write_packet(tunfd, out, total_len, "TCP packet") < 0)
return -1;
/* Update sequence number for data sent */
if (plen > 0)
f->srv_next = tcp_seq_add_len(f->srv_next, plen);
if ((flags & 0x01) != 0)
f->srv_next = tcp_seq_add_len(f->srv_next, 1);
return 0;
}
/* ---------- TCP Option Parsing ---------- */
struct tcp_options {
uint16_t mss;
uint8_t wscale;
uint32_t tsval;
uint32_t tsecr;
int ts_present;
int sack_permitted;
};
static uint32_t decode_be32(const uint8_t *p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
/* Parse TCP options from the options portion of TCP header.
Returns 0 on success, -1 if options are malformed. */
static int parse_tcp_options(const uint8_t *opts, size_t len,
struct tcp_options *out) {
memset(out, 0, sizeof(*out));
out->mss = 536; /* Default MSS per RFC 879 */
size_t i = 0;
while (i < len) {
uint8_t kind = opts[i];
if (kind == 0) /* End of option list */
break;
if (kind == 1) { /* NOP */
i++;
continue;
}
/* All other options have length field */
if (i + 1 >= len)
return -1;
uint8_t optlen = opts[i + 1];
if (optlen < 2 || i + optlen > len)
return -1;
switch (kind) {
case 2: /* MSS */
if (optlen == 4) {
out->mss = (uint16_t)((opts[i + 2] << 8) | opts[i + 3]);
}
break;
case 3: /* Window Scale */
if (optlen == 3) {
out->wscale = opts[i + 2];
}
break;
case 4: /* SACK Permitted */
if (optlen == 2) {
out->sack_permitted = 1;
}
break;
case 8: /* Timestamp */
if (optlen == 10) {
out->ts_present = 1;
out->tsval = decode_be32(opts + i + 2);
out->tsecr = decode_be32(opts + i + 6);
}
break;
}
i += optlen;
}
return 0;
}
/* Build TCP options for SYN-ACK response.
Returns the number of bytes written to buf. */
static size_t build_synack_options(const struct tcp_options *client_opts,
uint8_t *buf, uint32_t our_tsval) {
size_t off = 0;
/* MSS option (kind=2, len=4) - always include */
buf[off++] = 2;
buf[off++] = 4;
buf[off++] = 0x05; /* MSS = 1460 */
buf[off++] = 0xb4;
/* Window scale option if client requested (kind=3, len=3) */
if (client_opts->wscale > 0) {
buf[off++] = 1; /* NOP for alignment */
buf[off++] = 3; /* kind = Window Scale */
buf[off++] = 3; /* length = 3 */
buf[off++] = 0; /* our shift count = 0 (advertise x1 scaling) */
}
/* Timestamp option if client requested (kind=8, len=10) */
if (client_opts->ts_present) {
buf[off++] = 8;
buf[off++] = 10;
buf[off++] = (uint8_t)(our_tsval >> 24);
buf[off++] = (uint8_t)(our_tsval >> 16);
buf[off++] = (uint8_t)(our_tsval >> 8);
buf[off++] = (uint8_t)(our_tsval);
buf[off++] = (uint8_t)(client_opts->tsval >> 24);
buf[off++] = (uint8_t)(client_opts->tsval >> 16);
buf[off++] = (uint8_t)(client_opts->tsval >> 8);
buf[off++] = (uint8_t)(client_opts->tsval);
}
/* Pad to 4-byte boundary with NOPs */
while (off % 4 != 0)
buf[off++] = 1; /* NOP */
return off;
}
/* Send a TCP RST packet */
static void send_tcp_rst(int tunfd, uint32_t saddr, uint32_t daddr,
uint16_t sport, uint16_t dport, uint32_t seq,
uint32_t ack_seq) {
uint8_t out[64];
struct iphdr *ip = (struct iphdr *)out;
struct tcphdr *tcp = (struct tcphdr *)(out + sizeof(*ip));
memset(out, 0, sizeof(out));
ip->version = 4;
ip->ihl = 5;
ip->ttl = 64;
ip->protocol = IPPROTO_TCP;
ip->saddr = saddr;
ip->daddr = daddr;
ip->tot_len = htons(sizeof(*ip) + sizeof(*tcp));
tcp->source = htons(sport);
tcp->dest = htons(dport);
tcp->seq = htonl(seq);
tcp->ack_seq = htonl(ack_seq);
tcp->doff = 5;
tcp->rst = 1;
tcp->ack = 1;
tcp->window = 0;
tcp->check = tcp_checksum(ip, tcp, sizeof(*tcp), NULL, 0);
ip->check = ip_checksum(ip, sizeof(*ip));
IGNORE_RESULT(
tun_write_packet(tunfd, out, sizeof(*ip) + sizeof(*tcp), "TCP RST"));
}
/* Send RST for a given flow and clean it up */
static void tcp_flow_rst(int tunfd, struct tcp_flow *f) {
if (f->sock >= 0) {
epoll_del(f->sock);
close(f->sock);
}
f->pending_write_off = 0;
f->pending_write_len = 0;
f->pending_fin = 0;
f->pending_fin_seq = 0;
f->backend_ready = 0;
socks_io_reset(&f->socks);
send_tcp_rst(tunfd, f->srv_ip, f->cli_ip, f->srv_port, f->cli_port,
f->srv_next, f->cli_next);
f->sock = -1;
f->state = SP_TCP_CLOSED;
}
static void tcp_update_events(struct tcp_flow *f) {
uint32_t events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP;
if (!f->backend_ready || f->pending_write_len > 0 ||
socks_has_pending_tx(&f->socks))
events |= EPOLLOUT;
epoll_mod_tcp(f, events);
}
static void tcp_drain_socks_residual(int tunfd, struct tcp_flow *f) {
if (!f->socks.active || !f->backend_ready || f->socks.rx_len == 0)
return;
if (f->state != SP_TCP_ESTABLISHED && f->state != SP_TCP_CLOSE_WAIT)
return;
DBG("forwarding %zu residual SOCKS TCP bytes", f->socks.rx_len);
send_tcp_packet(tunfd, f, 0x08, f->socks.rxbuf, f->socks.rx_len);
f->socks.rx_len = 0;
f->last_active = time(NULL);
}
static int tcp_finish_pending_fin(int tunfd, struct tcp_flow *f) {
if (!f->backend_ready || f->pending_write_len > 0 || !f->pending_fin ||
f->pending_fin_seq != f->cli_next)
return 0;
f->pending_fin = 0;
f->pending_fin_seq = 0;
f->cli_next = tcp_seq_add_len(f->cli_next, 1);
send_tcp_packet(tunfd, f, 0, NULL, 0);
if (shutdown(f->sock, SHUT_WR) < 0 && errno != ENOTCONN && errno != EPIPE) {
tcp_flow_rst(tunfd, f);
return -1;
}
f->state = SP_TCP_CLOSE_WAIT;
f->last_active = time(NULL);
return 1;
}
static int tcp_queue_pending_write(struct tcp_flow *f, const uint8_t *data,
size_t len) {
if (f->pending_write_off != 0 && f->pending_write_len > 0) {
memmove(f->pending_write, f->pending_write + f->pending_write_off,
f->pending_write_len);
f->pending_write_off = 0;
}
if (f->pending_write_len + len > sizeof(f->pending_write))
return -1;
memcpy(f->pending_write + f->pending_write_len, data, len);
f->pending_write_len += len;
return 0;
}
static int tcp_flush_pending_write(int tunfd, struct tcp_flow *f) {
size_t total_sent = 0;
while (f->pending_write_len > 0) {
ssize_t sent = send(f->sock, f->pending_write + f->pending_write_off,
f->pending_write_len, MSG_NOSIGNAL);
if (sent > 0) {
f->pending_write_off += (size_t)sent;
f->pending_write_len -= (size_t)sent;
total_sent += (size_t)sent;
continue;
}
if (sent == 0) {
tcp_flow_rst(tunfd, f);
return -1;
}
if (sent < 0 && errno == EINTR)
continue;
if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
break;
tcp_flow_rst(tunfd, f);
return -1;
}
if (total_sent > 0) {
f->cli_next = tcp_seq_add_len(f->cli_next, total_sent);
if (tcp_finish_pending_fin(tunfd, f) < 0)
return -1;
if (f->sock >= 0 && f->pending_fin == 0)
send_tcp_packet(tunfd, f, 0, NULL, 0);
}
if (f->pending_write_len == 0)
f->pending_write_off = 0;
if (f->sock >= 0)
tcp_update_events(f);
return 0;
}
static int handle_publish_tcp_from_child(int tunfd, const struct iphdr *ip,
const struct tcphdr *tcp,
size_t tcp_hdr_len,
const uint8_t *payload,
size_t payload_len) {
uint16_t sport = ntohs(tcp->source);
uint16_t dport = ntohs(tcp->dest);
uint32_t seq = ntohl(tcp->seq);
uint32_t ack = ntohl(tcp->ack_seq);
uint32_t segment_payload_end = tcp_seq_add_len(seq, payload_len);
struct publish_tcp_flow *f;
(void)tcp_hdr_len;
if (ip->daddr != PUBLISH_SYNTHETIC_IP)
return 0;
if (ip->saddr != SANDBOX_IP)
return 1;
f = publish_tcp_find(ip->saddr, sport, ip->daddr, dport);
if (f == NULL)
return 1;
f->last_active = time(NULL);
if (tcp->rst) {
publish_tcp_close_flow(f);
return 1;
}
if (f->state == PUBLISH_TCP_SYN_SENT) {
if (tcp->syn && tcp->ack && ack == f->broker_next) {
f->child_isn = seq;
f->child_next = tcp_seq_add_len(seq, 1);
publish_tcp_send_packet(tunfd, f, 0x10, NULL, 0);
f->state = PUBLISH_TCP_ESTABLISHED;
publish_tcp_update_events(f);
}
return 1;
}
if (ack != 0 && tcp_seq_after(ack, f->broker_next)) {
publish_tcp_close_with_rst(tunfd, f);
return 1;
}
if (payload_len > 0) {
const uint8_t *data = payload;
size_t data_len = payload_len;
if (tcp_seq_before(seq, f->child_next)) {
if (tcp_seq_before_or_equal(segment_payload_end, f->child_next)) {
publish_tcp_send_packet(tunfd, f, 0x10, NULL, 0);
if (!tcp->fin)
return 1;
data_len = 0;
seq = f->child_next;
} else {
data += (size_t)(f->child_next - seq);
data_len -= (size_t)(f->child_next - seq);
seq = f->child_next;
}
}
if (seq != f->child_next) {
publish_tcp_send_packet(tunfd, f, 0x10, NULL, 0);
return 1;
}
if (publish_tcp_queue_child_bytes(f, data, data_len) < 0) {
publish_tcp_close_with_rst(tunfd, f);
return 1;
}
f->child_next = tcp_seq_add_len(f->child_next, data_len);
publish_tcp_send_packet(tunfd, f, 0x10, NULL, 0);
if (publish_tcp_flush_child_to_host(tunfd, f) < 0)
return 1;
}
if (tcp->fin && f->host_fd >= 0) {
uint32_t fin_seq = segment_payload_end;
if (fin_seq == f->child_next && !f->child_fin_seen) {
f->child_next = tcp_seq_add_len(f->child_next, 1);
f->child_fin_seen = 1;
f->state = f->host_fin_sent ? PUBLISH_TCP_CLOSING : PUBLISH_TCP_CHILD_FIN;
}
publish_tcp_send_packet(tunfd, f, 0x10, NULL, 0);
if (f->host_fd >= 0 && publish_tcp_flush_child_to_host(tunfd, f) < 0)
return 1;
} else if (payload_len == 0 && f->host_fd >= 0) {
publish_tcp_update_events(f);
}
return 1;
}
static void handle_tcp(int tunfd, uint8_t *pkt, ssize_t len) {
if (len <= 0)
return;
size_t ulen = (size_t)len;
struct iphdr *ip = (struct iphdr *)pkt;
size_t iphl = ip->ihl * 4;
if (ip->version != 4)
return;
if (iphl < sizeof(struct iphdr) || iphl > 60 || iphl > ulen)
return;
size_t ip_total_len = (size_t)ntohs(ip->tot_len);
if (ip_total_len < iphl || ip_total_len > ulen)
return;
/* Reject IP fragments */
if (ntohs(ip->frag_off) & (IP_MF | IP_OFFMASK)) {
DBG("TCP: dropping IP fragment (frag_off=0x%04x)", ntohs(ip->frag_off));
return;
}
if (ip_total_len < iphl + sizeof(struct tcphdr))
return;
struct tcphdr *tcp = (struct tcphdr *)(pkt + iphl);
size_t tcp_hdr_len = (size_t)tcp->doff * 4;
if (tcp_hdr_len < sizeof(struct tcphdr) || tcp_hdr_len > 60 ||
ip_total_len < iphl + tcp_hdr_len)
return;
uint32_t cip = ip->saddr;
uint32_t sip = ip->daddr;
uint16_t cport = ntohs(tcp->source);
uint16_t sport = ntohs(tcp->dest);
size_t payload_off = iphl + tcp_hdr_len;
size_t payload_len = 0;
if (ip_total_len > payload_off)
payload_len = ip_total_len - payload_off;
if (handle_publish_tcp_from_child(tunfd, ip, tcp, tcp_hdr_len,
pkt + payload_off, payload_len))
return;
/* ---------- RST ---------- */
if (tcp->rst) {
struct tcp_flow *f = tcp_find(cip, cport, sip, sport);
if (f && f->sock >= 0) {
epoll_del(f->sock);
close(f->sock);
f->sock = -1;
f->state = SP_TCP_CLOSED;
}
return;
}
/* ---------- SYN ---------- */
if (tcp->syn && !tcp->ack) {
struct tcp_flow *f = tcp_find(cip, cport, sip, sport);
/* Parse TCP options from SYN */
struct tcp_options cli_opts;
if (tcp_hdr_len > sizeof(struct tcphdr)) {
const uint8_t *opt_start = (const uint8_t *)tcp + sizeof(struct tcphdr);
size_t opt_len = tcp_hdr_len - sizeof(struct tcphdr);
if (parse_tcp_options(opt_start, opt_len, &cli_opts) < 0)
return;
} else {
memset(&cli_opts, 0, sizeof(cli_opts));
}
int use_gateway = 0;
int use_socks = 0;
const struct dns_mapping *dns_target = NULL;
if (egress_mode == EGRESS_NONE) {
DBG("[parent] TCP egress blocked by --egress=none: %s:%u",
inet_ntoa((struct in_addr){.s_addr = sip}), sport);
send_tcp_rst(tunfd, sip, cip, sport, cport, 0,
tcp_seq_add_len(ntohl(tcp->seq), 1));
return;
}
if (is_gateway_ip(sip)) {
if (!is_gateway_allowed(sip, sport, IPPROTO_TCP)) {
DBG("[parent] TCP to 10.0.1.%d:%d blocked",
gateway_last_octet(sip), sport);
send_tcp_rst(tunfd, sip, cip, sport, cport, 0,
tcp_seq_add_len(ntohl(tcp->seq), 1));
return;
}
use_gateway = 1;
} else if (egress_mode == EGRESS_SOCKS) {
use_socks = 1;
if (socks_proxy.remote_dns) {
dns_target = dns_mapping_find_ip(sip);
if (dns_target == NULL && dns_is_synthetic_ip(sip)) {
DBG("[parent] TCP to unknown synthetic DNS address blocked");
send_tcp_rst(tunfd, sip, cip, sport, cport, 0,
tcp_seq_add_len(ntohl(tcp->seq), 1));
return;
}
}
} else if (!is_direct_egress_allowed(sip, sport, IPPROTO_TCP)) {
log_direct_egress_block(sip, sport, IPPROTO_TCP);
send_tcp_rst(tunfd, sip, cip, sport, cport, 0,
tcp_seq_add_len(ntohl(tcp->seq), 1));
return;
}
if (!f) {
/* Rate limit new connections */
if (!check_rate_limit()) {
return; /* Too many connections - drop SYN silently */
}
/* first SYN */
f = tcp_alloc();
if (!f) {
DBG("[parent] TCP flow table full (%d slots); refusing new SYN",
tcp_flow_limit);
send_tcp_rst(tunfd, sip, cip, sport, cport, 0,
tcp_seq_add_len(ntohl(tcp->seq), 1));
return;
}
tcp_flow_begin(f);
f->cli_ip = cip;
f->cli_port = cport;
f->srv_ip = sip;
f->srv_port = sport;
f->cli_isn = ntohl(tcp->seq);
f->cli_next = f->cli_isn + 1;
uint32_t isn;
if (getrandom(&isn, sizeof(isn), 0) != (ssize_t)sizeof(isn))
die("getrandom");
f->srv_isn = isn;
f->srv_next = f->srv_isn;
/* Store timestamp negotiation state */
f->ts_ok = cli_opts.ts_present;
if (cli_opts.ts_present) {
f->ts_recent = cli_opts.tsval;
}
int s;
int connect_rc;
struct sockaddr_in dst;
if (use_gateway) {
/* Gateway access - connect to localhost (10.0.1.x -> 127.0.0.x) */
uint32_t local_ip = gateway_to_localhost(sip);
DBG("[parent] TCP gateway: 10.0.1.%d:%d -> 127.0.0.%d:%d",
gateway_last_octet(sip), sport, gateway_last_octet(sip), sport);
s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0)
return;
memset(&dst, 0, sizeof(dst));
dst.sin_family = AF_INET;
dst.sin_port = htons(sport);
dst.sin_addr.s_addr = local_ip;
} else if (use_socks) {
/* Connect via SOCKS5 proxy */
s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0)
return;
dst = socks_proxy.addr;
} else {
/* Direct connection */
s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0)
return;
memset(&dst, 0, sizeof(dst));
dst.sin_family = AF_INET;
dst.sin_port = htons(sport);
dst.sin_addr.s_addr = sip;
}
connect_rc = start_nonblocking_connect(s, &dst);
if (connect_rc < 0) {
close(s);
f->sock = -1;
send_tcp_rst(tunfd, sip, cip, sport, cport, 0, f->cli_next);
return;
}
f->sock = s;
f->backend_ready = 0;
socks_io_reset(&f->socks);
if (use_socks) {
f->socks.active = 1;
f->socks.is_udp = 0;
f->socks.target_ip = sip;
f->socks.target_port = sport;
if (dns_target != NULL)
snprintf(f->socks.target_domain, sizeof(f->socks.target_domain),
"%s", dns_target->name);
f->socks.connect_pending = (connect_rc > 0);
f->socks.state = f->socks.connect_pending ? SOCKS_IO_CONNECTING
: SOCKS_IO_METHOD;
if (!f->socks.connect_pending &&
socks_begin_handshake(&f->socks, &socks_proxy) < 0) {
close(s);
f->sock = -1;
send_tcp_rst(tunfd, sip, cip, sport, cport, 0, f->cli_next);
return;
}
} else if (connect_rc == 0) {
f->backend_ready = 1;
}
f->state = SP_TCP_SYN_RECEIVED;
f->last_active = time(NULL);
epoll_add_tcp(f);
tcp_update_events(f);
}
/* Build SYN-ACK with mirrored options */
uint8_t out[128];
struct iphdr *rip = (struct iphdr *)out;
struct tcphdr *rtcp = (struct tcphdr *)(out + sizeof(*rip));
/* Build TCP options mirroring client's capabilities */
uint8_t opts[24];
uint32_t our_tsval = (uint32_t)time(NULL);
size_t opts_len = build_synack_options(&cli_opts, opts, our_tsval);
memset(rip, 0, sizeof(*rip));
rip->version = 4;
rip->ihl = 5;
rip->ttl = 64;
rip->protocol = IPPROTO_TCP;
rip->saddr = sip;
rip->daddr = cip;
memset(rtcp, 0, sizeof(*rtcp));
rtcp->source = htons(sport);
rtcp->dest = htons(cport);
rtcp->seq = htonl(f->srv_isn);
rtcp->ack_seq = htonl(f->cli_next);
rtcp->syn = 1;
rtcp->ack = 1;
size_t full_tcp_len = sizeof(struct tcphdr) + opts_len;
rtcp->doff = (full_tcp_len / 4) & 0xF;
rtcp->window = htons(tcp_advertised_window(f));
memcpy((uint8_t *)rtcp + sizeof(*rtcp), opts, opts_len);
rip->tot_len = htons((uint16_t)(sizeof(*rip) + full_tcp_len));
rtcp->check = tcp_checksum(rip, rtcp, full_tcp_len, NULL, 0);
rip->check = ip_checksum(rip, sizeof(*rip));
if (tun_write_packet(tunfd, out, sizeof(*rip) + full_tcp_len,
"TCP SYN-ACK") == 0)
f->srv_next = tcp_seq_add_len(f->srv_next, 1);
return;
}
/* ---------- ACK / DATA ---------- */
if (tcp->ack && !tcp->syn) {
struct tcp_flow *f = tcp_find(cip, cport, sip, sport);
if (!f || f->sock < 0)
return;
uint32_t seq = ntohl(tcp->seq);
/* Calculate payload */
if (f->state == SP_TCP_SYN_RECEIVED) {
if (ntohl(tcp->ack_seq) != f->srv_next) {
DBG("TCP: dropping invalid handshake ACK (%u != %u)",
ntohl(tcp->ack_seq), f->srv_next);
return;
}
f->state = SP_TCP_ESTABLISHED;
tcp_drain_socks_residual(tunfd, f);
tcp_update_events(f);
}
/* Update activity time */
f->last_active = time(NULL);
uint32_t pending_end = tcp_seq_add_len(f->cli_next, f->pending_write_len);
if (!f->backend_ready || f->pending_write_len > 0) {
uint8_t *payload = pkt + payload_off;
size_t append_off = 0;
size_t append_len = 0;
if (payload_len > 0) {
uint32_t seg_end = tcp_seq_add_len(seq, payload_len);
if (tcp_seq_before_or_equal(seq, pending_end) &&
tcp_seq_after(seg_end, pending_end)) {
append_off = (size_t)(pending_end - seq);
if (append_off <= payload_len)
append_len = payload_len - append_off;
} else if (seq == pending_end) {
append_len = payload_len;
}
}
if (append_len > 0 &&
tcp_queue_pending_write(f, payload + append_off, append_len) < 0) {
tcp_flow_rst(tunfd, f);
return;
}
if (tcp->fin) {
f->pending_fin = 1;
f->pending_fin_seq = tcp_seq_add_len(seq, payload_len);
}
if (f->backend_ready && (payload_len > 0 || tcp->fin))
send_tcp_packet(tunfd, f, 0, NULL, 0);
return;
}
/* Forward payload to real server and ACK only bytes the backend accepted. */
if (payload_len > 0) {
uint8_t *payload = pkt + payload_off;
size_t payload_off_trim = 0;
if (tcp_seq_before(seq, f->cli_next)) {
uint32_t seg_end = tcp_seq_add_len(seq, payload_len);
if (tcp_seq_before_or_equal(seg_end, f->cli_next)) {
send_tcp_packet(tunfd, f, 0, NULL, 0);
return;
}
payload_off_trim = (size_t)(f->cli_next - seq);
seq = f->cli_next;
payload += payload_off_trim;
payload_len -= payload_off_trim;
}
if (seq != f->cli_next) {
send_tcp_packet(tunfd, f, 0, NULL, 0);
return;
}
size_t total_sent = 0;
while (total_sent < payload_len) {
ssize_t sent = send(f->sock, payload + total_sent,
payload_len - total_sent, MSG_NOSIGNAL);
if (sent == 0) {
tcp_flow_rst(tunfd, f);
return;
}
if (sent < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
/* Connection error - send RST and clean up */
tcp_flow_rst(tunfd, f);
return;
}
total_sent += (size_t)sent;
}
if (total_sent > 0) {
f->cli_next = tcp_seq_add_len(f->cli_next, total_sent);
/* Send ACK back to client */
send_tcp_packet(tunfd, f, 0, NULL, 0);
}
if (total_sent < payload_len) {
if (tcp_queue_pending_write(f, payload + total_sent,
payload_len - total_sent) < 0) {
tcp_flow_rst(tunfd, f);
return;
}
tcp_update_events(f);
}
}
/* Handle FIN from client */
if (tcp->fin) {
if (f->pending_write_len > 0 || seq != f->cli_next) {
f->pending_fin = 1;
f->pending_fin_seq = tcp_seq_add_len(seq, payload_len);
send_tcp_packet(tunfd, f, 0, NULL, 0);
return;
}
f->cli_next = tcp_seq_add_len(f->cli_next, 1);
/* ACK the client FIN but keep the backend open for reads. */
send_tcp_packet(tunfd, f, 0, NULL, 0);
if (shutdown(f->sock, SHUT_WR) < 0 && errno != ENOTCONN &&
errno != EPIPE) {
tcp_flow_rst(tunfd, f);
return;
}
f->state = SP_TCP_CLOSE_WAIT;
f->last_active = time(NULL);
}
return;
}
}
static void handle_publish_tcp_listener(int tunfd, struct publish_rule *rule) {
for (;;) {
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int fd = accept4(rule->listen_fd, (struct sockaddr *)&peer, &peer_len,
SOCK_CLOEXEC | SOCK_NONBLOCK);
struct publish_tcp_flow *f;
uint16_t synthetic_port;
uint32_t isn;
if (fd < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return;
return;
}
peer_len = sizeof(peer);
if (getpeername(fd, (struct sockaddr *)&peer, &peer_len) < 0 ||
peer.sin_family != AF_INET ||
!ipv4_is_loopback(peer.sin_addr.s_addr)) {
close(fd);
continue;
}
f = publish_tcp_alloc();
synthetic_port = publish_alloc_synthetic_port(IPPROTO_TCP);
if (f == NULL || synthetic_port == 0) {
close(fd);
return;
}
publish_tcp_flow_begin(f);
f->used = 1;
f->host_fd = fd;
f->rule_index = rule->rule_index;
f->synthetic_ip = PUBLISH_SYNTHETIC_IP;
f->synthetic_port = synthetic_port;
f->child_ip = SANDBOX_IP;
f->child_port = rule->container_port;
f->state = PUBLISH_TCP_SYN_SENT;
f->last_active = time(NULL);
if (getrandom(&isn, sizeof(isn), 0) != (ssize_t)sizeof(isn)) {
publish_tcp_close_flow(f);
return;
}
f->broker_isn = isn;
f->broker_next = f->broker_isn;
epoll_add_publish_tcp_host(f);
publish_tcp_update_events(f);
publish_tcp_send_packet(tunfd, f, 0x02, NULL, 0);
}
}
static void handle_publish_tcp_host_event(int tunfd, struct publish_tcp_flow *f,
uint32_t events) {
if (f == NULL || f->host_fd < 0)
return;
if ((events & EPOLLOUT) != 0 && f->pending_child_len > 0) {
if (publish_tcp_flush_child_to_host(tunfd, f) < 0)
return;
}
if (f->host_fd < 0)
return;
if ((events & (EPOLLERR | EPOLLHUP)) != 0 && (events & EPOLLIN) == 0) {
publish_tcp_close_with_rst(tunfd, f);
return;
}
if ((events & (EPOLLIN | EPOLLRDHUP)) == 0) {
publish_tcp_update_events(f);
return;
}
if (f->state == PUBLISH_TCP_SYN_SENT) {
publish_tcp_update_events(f);
return;
}
for (;;) {
ssize_t n = recv(f->host_fd, g_io_buf, 60000, 0);
if (n > 0) {
f->last_active = time(NULL);
publish_tcp_send_packet(tunfd, f, 0x18, g_io_buf, (size_t)n);
continue;
}
if (n == 0) {
if (!f->host_fin_sent) {
f->host_fin_sent = 1;
f->state =
f->child_fin_seen ? PUBLISH_TCP_CLOSING : PUBLISH_TCP_HOST_FIN;
publish_tcp_send_packet(tunfd, f, 0x11, NULL, 0);
}
if (f->child_fin_seen && f->pending_child_len == 0)
publish_tcp_close_flow(f);
else if (f->host_fd >= 0)
publish_tcp_update_events(f);
return;
}
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
publish_tcp_update_events(f);
return;
}
publish_tcp_close_with_rst(tunfd, f);
return;
}
}
static void handle_publish_udp_socket(int tunfd, struct publish_rule *rule) {
for (;;) {
struct sockaddr_in peer;
struct iovec iov = {.iov_base = g_io_buf, .iov_len = sizeof(g_io_buf)};
char control[CMSG_SPACE(sizeof(struct in_pktinfo))];
struct msghdr msg;
struct in_pktinfo *pktinfo = NULL;
ssize_t n;
memset(&peer, 0, sizeof(peer));
memset(control, 0, sizeof(control));
memset(&msg, 0, sizeof(msg));
msg.msg_name = &peer;
msg.msg_namelen = sizeof(peer);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
n = recvmsg(rule->listen_fd, &msg, 0);
if (n < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return;
return;
}
for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL;
cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) {
pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsg);
break;
}
}
if ((msg.msg_flags & MSG_TRUNC) != 0)
continue;
if (peer.sin_family != AF_INET ||
!ipv4_is_loopback(peer.sin_addr.s_addr))
continue;
if (pktinfo == NULL || pktinfo->ipi_addr.s_addr != rule->host_ip)
continue;
struct publish_udp_flow *f =
publish_udp_find_host_peer(rule->rule_index, &peer);
if (f == NULL) {
uint16_t synthetic_port = publish_alloc_synthetic_port(IPPROTO_UDP);
if (synthetic_port == 0)
continue;
f = publish_udp_alloc();
if (f == NULL)
continue;
memset(f, 0, sizeof(*f));
f->used = 1;
f->rule_index = rule->rule_index;
f->host_peer = peer;
f->synthetic_port = synthetic_port;
f->child_port = rule->container_port;
}
f->last_active = time(NULL);
publish_udp_inject_tun(tunfd, f, g_io_buf, (size_t)n);
}
}
static void dispatch_tun_ipv4_packet(int tunfd, uint8_t *pkt, size_t len) {
struct iphdr *ip;
size_t iphl;
size_t ip_total_len;
if (len < sizeof(struct iphdr))
return;
ip = (struct iphdr *)pkt;
if (ip->version != 4)
return;
iphl = (size_t)ip->ihl * 4;
if (iphl < sizeof(struct iphdr) || iphl > 60 || iphl > len) {
DBG("Dropping packet: bad IHL=%zu", iphl);
return;
}
if (ip_checksum(ip, iphl) != 0) {
DBG("Dropping packet: bad IP checksum");
return;
}
ip_total_len = (size_t)ntohs(ip->tot_len);
if (ip_total_len < iphl || ip_total_len > len)
return;
if ((ntohs(ip->frag_off) & (IP_MF | IP_OFFMASK)) != 0) {
DBG("Dropping packet: dropping IP fragment (frag_off=0x%04x)",
ntohs(ip->frag_off));
return;
}
if (ip->protocol == IPPROTO_TCP) {
if (ip_total_len < iphl + sizeof(struct tcphdr))
return;
const struct tcphdr *tcp = (const struct tcphdr *)(pkt + iphl);
size_t tcp_len = ip_total_len - iphl;
size_t tcp_hdr_len = (size_t)tcp->doff * 4;
if (tcp_hdr_len < sizeof(struct tcphdr) || tcp_hdr_len > tcp_len)
return;
if (!tcp_checksum_valid(ip, tcp, tcp_len)) {
DBG("Dropping packet: bad TCP checksum");
return;
}
handle_tcp(tunfd, pkt, (ssize_t)ip_total_len);
return;
}
if (ip->protocol == IPPROTO_UDP) {
if (ip_total_len < iphl + sizeof(struct udphdr))
return;
const struct udphdr *udp = (const struct udphdr *)(pkt + iphl);
size_t udp_len = (size_t)ntohs(udp->len);
if (udp_len < sizeof(struct udphdr) || udp_len > ip_total_len - iphl)
return;
if (!udp_checksum_valid(ip, udp, udp_len)) {
DBG("Dropping packet: bad UDP checksum");
return;
}
handle_udp(tunfd, pkt, (ssize_t)ip_total_len);
return;
}
if (ip->protocol == IPPROTO_ICMP) {
const uint8_t *icmp = pkt + iphl;
size_t icmp_len = ip_total_len - iphl;
if (!icmp_checksum_valid(icmp, icmp_len)) {
DBG("Dropping packet: bad ICMP checksum");
return;
}
handle_icmp(tunfd, pkt, (ssize_t)ip_total_len);
}
}
static void udp_close_flow(struct udp_flow *f) {
if (f->udp_relay >= 0) {
epoll_del(f->udp_relay);
close(f->udp_relay);
f->udp_relay = -1;
}
if (f->udp_staging >= 0) {
close(f->udp_staging);
f->udp_staging = -1;
}
if (f->tcp_ctrl >= 0) {
epoll_del(f->tcp_ctrl);
close(f->tcp_ctrl);
f->tcp_ctrl = -1;
}
f->pending_set = 0;
f->pending_len = 0;
socks_io_reset(&f->socks);
}
static int interactive_maybe_sync_winsize(struct interactive_session *session) {
if (!session || !session->active)
return 0;
if (!interactive_resize_pending)
return 0;
interactive_resize_pending = 0;
return interactive_sync_winsize(session);
}
static int open_child_pidfd(pid_t pid) {
#ifdef __NR_pidfd_open
int pidfd = (int)syscall(__NR_pidfd_open, pid, 0);
if (pidfd < 0)
return -1;
if (fcntl(pidfd, F_SETFD, FD_CLOEXEC) < 0) {
int saved = errno;
close(pidfd);
errno = saved;
return -1;
}
return pidfd;
#else
errno = ENOSYS;
return -1;
#endif
}
static int broker_try_reap_child(pid_t pid, int *child_status,
time_t *child_exited_at) {
if (child_status == NULL || child_exited_at == NULL) {
errno = EINVAL;
return -1;
}
if (*child_status != -1)
return 1;
for (;;) {
int status = 0;
pid_t got = waitpid(pid, &status, WNOHANG);
if (got == 0)
return 0;
if (got == pid) {
*child_status = status;
*child_exited_at = time(NULL);
return 1;
}
if (got < 0 && errno == EINTR)
continue;
DBG("waitpid(%ld, WNOHANG) failed while polling child exit: %s",
(long)pid, strerror(errno));
return -1;
}
}
static void broker_close_child_pidfd(int *child_pidfd,
int *child_pidfd_active) {
if (child_pidfd == NULL || *child_pidfd < 0)
return;
if (child_pidfd_active != NULL && *child_pidfd_active) {
if (epoll_ctl(g_epfd, EPOLL_CTL_DEL, *child_pidfd, NULL) < 0 &&
errno != EBADF && errno != ENOENT)
DBG("epoll_ctl DEL child pidfd failed: %s", strerror(errno));
*child_pidfd_active = 0;
}
if (close(*child_pidfd) < 0)
DBG("close child pidfd failed: %s", strerror(errno));
*child_pidfd = -1;
}
/* SP-11: a broker-fatal event-loop exit can leave the namespace child
* running and possibly hostile. Kill it and reap with bounded semantics:
* SIGKILL through the child pidfd when available, falling back to kill()
* on the pid (safe against reuse because an unreaped direct child keeps
* its pid reserved), and block in waitpid only once a kill has succeeded.
* A TERM grace stage is deliberately absent: no process in the child tree
* catches SIGTERM, and the payload's death is namespace-teardown SIGKILL
* either way. */
static void broker_fatal_kill_child(pid_t pid, int child_pidfd) {
int killed = 0;
fprintf(stderr,
"[sockpuppet] Error: fatal broker error; killing sandbox child\n");
#ifdef __NR_pidfd_send_signal
if (child_pidfd >= 0 &&
syscall(__NR_pidfd_send_signal, child_pidfd, SIGKILL, NULL, 0) == 0)
killed = 1;
#else
(void)child_pidfd;
#endif
if (!killed && (kill(pid, SIGKILL) == 0 || errno == ESRCH))
killed = 1;
if (!killed) {
fprintf(stderr,
"[sockpuppet] Warning: could not kill sandbox child: %s; "
"leaving it unreaped\n",
strerror(errno));
return;
}
for (;;) {
if (waitpid(pid, NULL, 0) == pid || errno != EINTR)
break;
}
}
static int relay_update_epoll(struct relay_runtime *runtime) {
uint32_t source_events = EPOLLERR | EPOLLHUP | EPOLLRDHUP;
if (sp_relay_wants_read(&runtime->state))
source_events |= EPOLLIN;
if (runtime->source_fd >= 0 && !sp_relay_wants_read(&runtime->state) &&
runtime->source_registered) {
epoll_del(runtime->source_fd);
runtime->source_registered = 0;
} else if (runtime->source_fd >= 0 &&
sp_relay_wants_read(&runtime->state)) {
struct epoll_event source_event = {
.events = source_events,
.data.u64 = epoll_registration_token(runtime->source_type, 0,
runtime->source_fd, 1)};
int operation = runtime->source_registered ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;
if (epoll_ctl(g_epfd, operation, runtime->source_fd, &source_event) < 0)
return -1;
runtime->source_registered = 1;
}
if (runtime->destination_registered &&
sp_relay_complete(&runtime->state)) {
epoll_del(runtime->destination.fd);
runtime->destination_registered = 0;
} else if (runtime->destination.epollable && runtime->destination.fd >= 0 &&
!sp_relay_complete(&runtime->state)) {
uint32_t destination_events = EPOLLERR | EPOLLHUP | EPOLLRDHUP;
if (sp_relay_wants_write(&runtime->state))
destination_events |= EPOLLOUT;
struct epoll_event destination_event = {
.events = destination_events,
.data.u64 = epoll_registration_token(runtime->destination_type, 0,
runtime->destination.fd, 1)};
int operation = runtime->destination_registered ? EPOLL_CTL_MOD
: EPOLL_CTL_ADD;
if (epoll_ctl(g_epfd, operation, runtime->destination.fd,
&destination_event) < 0)
return -1;
runtime->destination_registered = 1;
}
return 0;
}
static int relay_matches_source(const struct relay_runtime *runtime,
const struct epoll_key *key) {
return epoll_key_matches(key, runtime->source_type, 0, runtime->source_fd, 1);
}
static int relay_matches_destination(const struct relay_runtime *runtime,
const struct epoll_key *key) {
return epoll_key_matches(key, runtime->destination_type, 0,
runtime->destination.fd, 1);
}
static int relay_active(const struct relay_runtime *runtime) {
return runtime->state.storage != NULL &&
!sp_relay_complete(&runtime->state);
}
static void interactive_relay_read(struct relay_runtime *runtime) {
size_t space = sp_relay_space(&runtime->state);
if (space == 0)
return;
if (space > sizeof(g_io_buf))
space = sizeof(g_io_buf);
ssize_t count = relay_os_read(runtime->source_fd, g_io_buf, space);
if (count > 0) {
(void)sp_relay_step(
&runtime->state,
&(struct sp_relay_event){.type = SP_RELAY_SOURCE_BYTES,
.bytes = g_io_buf,
.length = (size_t)count});
} else if (count == 0 ||
(count < 0 && errno != EINTR && errno != EAGAIN &&
errno != EWOULDBLOCK)) {
(void)sp_relay_step(
&runtime->state,
&(struct sp_relay_event){.type = SP_RELAY_SOURCE_EOF});
}
}
static int interactive_update_epoll(struct relay_runtime *tty_to_pty,
struct relay_runtime *pty_to_tty) {
uint32_t tty_events = EPOLLERR | EPOLLHUP | EPOLLRDHUP;
uint32_t pty_events = EPOLLERR | EPOLLHUP | EPOLLRDHUP;
if (sp_relay_wants_read(&tty_to_pty->state))
tty_events |= EPOLLIN;
if (sp_relay_wants_write(&pty_to_tty->state))
tty_events |= EPOLLOUT;
if (sp_relay_wants_read(&pty_to_tty->state))
pty_events |= EPOLLIN;
if (sp_relay_wants_write(&tty_to_pty->state))
pty_events |= EPOLLOUT;
struct epoll_event tty_event = {
.events = tty_events,
.data.u64 = epoll_registration_token(FD_INTERACTIVE_TTY, 0,
tty_to_pty->source_fd, 1)};
struct epoll_event pty_event = {
.events = pty_events,
.data.u64 = epoll_registration_token(FD_INTERACTIVE_PTY, 0,
pty_to_tty->source_fd, 1)};
int tty_operation = tty_to_pty->source_registered ? EPOLL_CTL_MOD
: EPOLL_CTL_ADD;
int pty_operation = pty_to_tty->source_registered ? EPOLL_CTL_MOD
: EPOLL_CTL_ADD;
if (epoll_ctl(g_epfd, tty_operation, tty_to_pty->source_fd, &tty_event) < 0)
return -1;
tty_to_pty->source_registered = 1;
if (epoll_ctl(g_epfd, pty_operation, pty_to_tty->source_fd, &pty_event) < 0)
return -1;
pty_to_tty->source_registered = 1;
return 0;
}
static int event_loop(int tunfd, pid_t pid, int child_pidfd, int stdout_fd,
int stderr_fd,
const struct relay_destination *stdout_destination,
const struct relay_destination *stderr_destination,
struct interactive_session *session) {
bench_mark("broker_ready", "event_loop_enter", "ok", NULL);
struct epoll_event events[MAX_EVENTS];
int child_status = -1;
int child_pidfd_active = 0;
int interactive_pty_active = 0;
time_t child_exited_at = 0;
struct relay_runtime stdout_relay;
struct relay_runtime stderr_relay;
struct relay_runtime tty_to_pty;
struct relay_runtime pty_to_tty;
memset(&stdout_relay, 0, sizeof(stdout_relay));
memset(&stderr_relay, 0, sizeof(stderr_relay));
memset(&tty_to_pty, 0, sizeof(tty_to_pty));
memset(&pty_to_tty, 0, sizeof(pty_to_tty));
stdout_relay.source_fd = -1;
stderr_relay.source_fd = -1;
tty_to_pty.source_fd = -1;
pty_to_tty.source_fd = -1;
/* Register TUN input and enable EPOLLOUT only while bounded output is
* queued. tun_write_packet() updates this value-token registration. */
g_tun_registered_fd = tunfd;
struct epoll_event tun_ev = {
.events = EPOLLIN,
.data.u64 = epoll_registration_token(FD_TUN, 0, tunfd, 1)};
if (epoll_ctl(g_epfd, EPOLL_CTL_ADD, tunfd, &tun_ev) < 0)
die("epoll_ctl TUN");
for (int i = 0; i < publish_rule_count; ++i)
epoll_add_publish_rule(&publish_rules[i]);
if (stdout_fd >= 0) {
relay_runtime_init(&stdout_relay, stdout_fd, stdout_destination,
FD_STDOUT_RELAY, FD_STDOUT_DEST,
g_stdout_relay_storage);
if (relay_update_epoll(&stdout_relay) < 0)
die("epoll stdout relay");
}
if (stderr_fd >= 0) {
relay_runtime_init(&stderr_relay, stderr_fd, stderr_destination,
FD_STDERR_RELAY, FD_STDERR_DEST,
g_stderr_relay_storage);
if (relay_update_epoll(&stderr_relay) < 0)
die("epoll stderr relay");
}
if (session && session->active) {
if (set_nonblocking(session->host_tty_fd) < 0 ||
set_nonblocking(session->pty_master_fd) < 0)
die("nonblocking interactive relay");
struct relay_destination pty_destination = {
.fd = session->pty_master_fd,
.mode = RELAY_DEST_NONBLOCKING_WRITE,
.epollable = 1};
struct relay_destination tty_destination = {
.fd = session->host_tty_fd,
.mode = RELAY_DEST_NONBLOCKING_WRITE,
.epollable = 1};
relay_runtime_init(&tty_to_pty, session->host_tty_fd, &pty_destination,
FD_INTERACTIVE_TTY, FD_INTERACTIVE_PTY,
g_tty_to_pty_storage);
relay_runtime_init(&pty_to_tty, session->pty_master_fd, &tty_destination,
FD_INTERACTIVE_PTY, FD_INTERACTIVE_TTY,
g_pty_to_tty_storage);
tty_to_pty.close_source_on_destination_failure = 0;
pty_to_tty.close_source_on_destination_failure = 0;
if (interactive_update_epoll(&tty_to_pty, &pty_to_tty) < 0)
die("epoll interactive relay");
interactive_pty_active = 1;
}
if (child_pidfd >= 0) {
struct epoll_event child_exit_ev = {
.events = EPOLLIN | EPOLLHUP | EPOLLERR,
.data.u64 = epoll_registration_token(FD_CHILD_EXIT, 0, child_pidfd, 1)};
if (epoll_ctl(g_epfd, EPOLL_CTL_ADD, child_pidfd, &child_exit_ev) == 0) {
child_pidfd_active = 1;
} else {
DBG("epoll_ctl add child pidfd failed: %s; falling back to timeout polling",
strerror(errno));
broker_close_child_pidfd(&child_pidfd, &child_pidfd_active);
}
}
int noninteractive_relays_complete = 0;
for (;;) {
if (getenv("SOCKPUPPET_TEST_BROKER_FATAL") != NULL) {
fprintf(stderr,
"[sockpuppet] test: injected fatal broker-loop error\n");
break;
}
if (interactive_maybe_sync_winsize(session) < 0)
break;
if (stdout_relay.state.storage != NULL &&
!stdout_relay.destination.epollable) {
relay_drain(&stdout_relay, RELAY_DRAIN_BUDGET);
if (relay_update_epoll(&stdout_relay) < 0)
break;
}
if (stderr_relay.state.storage != NULL &&
!stderr_relay.destination.epollable) {
relay_drain(&stderr_relay, RELAY_DRAIN_BUDGET);
if (relay_update_epoll(&stderr_relay) < 0)
break;
}
if (interactive_pty_active > 0) {
if (interactive_update_epoll(&tty_to_pty, &pty_to_tty) < 0)
break;
}
noninteractive_relays_complete =
!relay_active(&stdout_relay) && !relay_active(&stderr_relay);
if (interactive_pty_active > 0 &&
sp_relay_complete(&tty_to_pty.state) &&
sp_relay_complete(&pty_to_tty.state))
interactive_pty_active = 0;
/* Check child status */
if (child_status == -1 &&
broker_try_reap_child(pid, &child_status, &child_exited_at) < 0)
break;
if (child_status != -1)
broker_close_child_pidfd(&child_pidfd, &child_pidfd_active);
if (child_status != -1 && noninteractive_relays_complete &&
interactive_pty_active <= 0)
break;
if (child_status != -1 && child_exited_at != 0 &&
(time(NULL) - child_exited_at) >= 5) {
if (stdout_relay.state.storage != NULL &&
!sp_relay_complete(&stdout_relay.state)) {
(void)sp_relay_step(
&stdout_relay.state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_FAILED});
relay_close_source(&stdout_relay);
}
if (stderr_relay.state.storage != NULL &&
!sp_relay_complete(&stderr_relay.state)) {
(void)sp_relay_step(
&stderr_relay.state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_FAILED});
relay_close_source(&stderr_relay);
}
}
if (child_status != -1 && interactive_pty_active > 0 &&
child_exited_at != 0 && session && session->pty_master_fd >= 0 &&
(time(NULL) - child_exited_at) >= 5) {
(void)sp_relay_step(
&tty_to_pty.state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_FAILED});
(void)sp_relay_step(
&pty_to_tty.state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_FAILED});
epoll_ctl(g_epfd, EPOLL_CTL_DEL, session->pty_master_fd, NULL);
close(session->pty_master_fd);
session->pty_master_fd = -1;
interactive_pty_active = 0;
if (noninteractive_relays_complete)
break;
}
int n = epoll_wait(g_epfd, events, MAX_EVENTS, EPOLL_TIMEOUT_MS);
if (n < 0) {
if (errno == EINTR)
continue;
break;
}
if (interactive_maybe_sync_winsize(session) < 0)
break;
size_t relay_batch_budget = RELAY_BATCH_BUDGET;
for (int i = 0; i < n; i++) {
struct epoll_key key;
if (epoll_token_decode(events[i].data.u64, &key) < 0)
continue;
switch (key.type) {
case FD_STDOUT_RELAY:
case FD_STDERR_RELAY: {
struct relay_runtime *runtime =
key.type == FD_STDOUT_RELAY ? &stdout_relay : &stderr_relay;
if (!relay_matches_source(runtime, &key))
break;
if ((events[i].events & EPOLLIN) != 0 &&
sp_relay_wants_read(&runtime->state))
relay_read_source(runtime);
if ((events[i].events & (EPOLLHUP | EPOLLRDHUP | EPOLLERR)) != 0 &&
(events[i].events & EPOLLIN) == 0 &&
sp_relay_wants_read(&runtime->state))
relay_read_source(runtime);
relay_drain_from_batch(runtime, &relay_batch_budget);
if (relay_update_epoll(runtime) < 0)
goto loop_end;
break;
}
case FD_STDOUT_DEST:
case FD_STDERR_DEST: {
struct relay_runtime *runtime =
key.type == FD_STDOUT_DEST ? &stdout_relay : &stderr_relay;
if (!relay_matches_destination(runtime, &key))
break;
if ((events[i].events & EPOLLOUT) != 0)
relay_drain_from_batch(runtime, &relay_batch_budget);
if ((events[i].events & (EPOLLHUP | EPOLLRDHUP | EPOLLERR)) != 0) {
(void)sp_relay_step(
&runtime->state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_FAILED});
relay_close_source(runtime);
}
if (relay_update_epoll(runtime) < 0)
goto loop_end;
break;
}
case FD_INTERACTIVE_TTY:
case FD_INTERACTIVE_PTY: {
if (!session || !session->active)
break;
int source_fd = key.type == FD_INTERACTIVE_TTY
? tty_to_pty.source_fd
: pty_to_tty.source_fd;
if (!epoll_key_matches(&key, key.type, 0, source_fd, 1))
break;
struct relay_runtime *source = key.type == FD_INTERACTIVE_TTY
? &tty_to_pty
: &pty_to_tty;
struct relay_runtime *destination = key.type == FD_INTERACTIVE_TTY
? &pty_to_tty
: &tty_to_pty;
if ((events[i].events & EPOLLIN) != 0 &&
sp_relay_wants_read(&source->state))
interactive_relay_read(source);
if ((events[i].events & EPOLLIN) != 0 &&
sp_relay_wants_write(&source->state))
relay_drain_from_batch(source, &relay_batch_budget);
if ((events[i].events & EPOLLOUT) != 0)
relay_drain_from_batch(destination, &relay_batch_budget);
if ((events[i].events & (EPOLLHUP | EPOLLRDHUP | EPOLLERR)) != 0) {
(void)sp_relay_step(
&source->state,
&(struct sp_relay_event){.type = SP_RELAY_SOURCE_EOF});
(void)sp_relay_step(
&destination->state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_FAILED});
}
if (interactive_update_epoll(&tty_to_pty, &pty_to_tty) < 0)
goto loop_end;
break;
}
case FD_CHILD_EXIT:
if (!epoll_key_matches(&key, FD_CHILD_EXIT, 0, child_pidfd, 1))
break;
if (broker_try_reap_child(pid, &child_status, &child_exited_at) < 0)
break;
if (child_status != -1)
broker_close_child_pidfd(&child_pidfd, &child_pidfd_active);
if (child_status != -1 && noninteractive_relays_complete &&
interactive_pty_active <= 0)
goto loop_end;
break;
case FD_TUN: {
if (!epoll_key_matches(&key, FD_TUN, 0, tunfd, 1) ||
g_tun_registered_fd != tunfd)
break;
if ((events[i].events & EPOLLOUT) != 0 && tun_flush_packets(tunfd) < 0)
goto loop_end;
if ((events[i].events & EPOLLIN) != 0) {
/* Handle TUN packets (outgoing from child) */
ssize_t r = read(tunfd, g_io_buf, sizeof(g_io_buf));
if (r > 0)
dispatch_tun_ipv4_packet(tunfd, g_io_buf, (size_t)r);
}
break;
}
case FD_TCP: {
/* Handle TCP server socket responses */
struct tcp_flow *f;
if (key.index >= (uint8_t)tcp_flow_limit)
break;
f = &tcp_flows[key.index];
if (!epoll_key_matches(&key, FD_TCP, key.index, f->sock,
f->generation))
break;
if (!f->backend_ready) {
if ((events[i].events & (EPOLLOUT | EPOLLERR | EPOLLHUP |
EPOLLRDHUP)) &&
((!f->socks.active) || f->socks.connect_pending)) {
if (socket_connect_complete(f->sock) < 0) {
tcp_flow_rst(tunfd, f);
break;
}
if (f->socks.active) {
f->socks.connect_pending = 0;
if (socks_begin_handshake(&f->socks, &socks_proxy) < 0) {
tcp_flow_rst(tunfd, f);
break;
}
} else {
f->backend_ready = 1;
}
tcp_update_events(f);
}
if (f->sock < 0)
break;
if (f->socks.active && !f->backend_ready) {
if ((events[i].events & EPOLLOUT) && socks_has_pending_tx(&f->socks)) {
if (socks_flush_tx(f->sock, &f->socks) < 0) {
tcp_flow_rst(tunfd, f);
break;
}
tcp_update_events(f);
}
if ((events[i].events & EPOLLIN) != 0) {
int rc = socks_recv_and_process(f->sock, &f->socks, &socks_proxy,
NULL);
if (rc < 0) {
tcp_flow_rst(tunfd, f);
break;
}
if (rc > 0) {
f->backend_ready = 1;
f->last_active = time(NULL);
tcp_drain_socks_residual(tunfd, f);
if (f->pending_write_len > 0 &&
tcp_flush_pending_write(tunfd, f) < 0)
break;
if (f->sock >= 0 && tcp_finish_pending_fin(tunfd, f) < 0)
break;
if (f->sock >= 0)
tcp_update_events(f);
} else if (f->sock >= 0) {
tcp_update_events(f);
}
}
if (!f->backend_ready)
break;
}
}
if (f->sock >= 0 && (events[i].events & EPOLLOUT) &&
f->pending_write_len > 0) {
if (tcp_flush_pending_write(tunfd, f) < 0)
break;
}
if (f->sock >= 0 && tcp_finish_pending_fin(tunfd, f) < 0)
break;
if (f->sock >= 0)
tcp_drain_socks_residual(tunfd, f);
if (f->sock >= 0 &&
(f->state == SP_TCP_ESTABLISHED ||
f->state == SP_TCP_CLOSE_WAIT) && (events[i].events & EPOLLIN)) {
ssize_t r = recv(f->sock, g_io_buf, sizeof(g_io_buf) - 64, 0);
if (r > 0) {
/* Forward data to client */
send_tcp_packet(tunfd, f, 0x08, g_io_buf, (size_t)r);
f->last_active = time(NULL);
} else if (r == 0) {
if (f->pending_write_len > 0) {
tcp_flow_rst(tunfd, f);
break;
}
/* Server closed connection - send FIN to client */
send_tcp_packet(tunfd, f, 0x01, NULL, 0);
epoll_del(f->sock);
close(f->sock);
f->sock = -1;
f->state = SP_TCP_CLOSED;
} else if (errno != EAGAIN && errno != EWOULDBLOCK &&
errno != EINTR) {
tcp_flow_rst(tunfd, f);
}
}
break;
}
case FD_UDP_CTRL: {
struct udp_flow *f;
if (key.index >= (uint8_t)udp_flow_limit)
break;
f = &udp_flows[key.index];
if (!epoll_key_matches(&key, FD_UDP_CTRL, key.index, f->tcp_ctrl,
f->generation))
break;
if ((events[i].events & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) &&
!f->socks.connect_pending && f->udp_relay >= 0) {
udp_close_flow(f);
break;
}
if (f->socks.connect_pending &&
(events[i].events & (EPOLLOUT | EPOLLERR | EPOLLHUP |
EPOLLRDHUP))) {
if (socket_connect_complete(f->tcp_ctrl) < 0) {
udp_close_flow(f);
break;
}
f->socks.connect_pending = 0;
if (socks_begin_handshake(&f->socks, &socks_proxy) < 0) {
udp_close_flow(f);
break;
}
epoll_mod_udp_ctrl(f, udp_ctrl_events(f));
}
if (f->tcp_ctrl < 0)
break;
if ((events[i].events & EPOLLOUT) && socks_has_pending_tx(&f->socks)) {
if (socks_flush_tx(f->tcp_ctrl, &f->socks) < 0) {
udp_close_flow(f);
break;
}
epoll_mod_udp_ctrl(f, udp_ctrl_events(f));
}
if ((events[i].events & EPOLLIN) != 0) {
struct sockaddr_in relay_addr;
int rc = socks_recv_and_process(f->tcp_ctrl, &f->socks, &socks_proxy,
&relay_addr);
if (rc < 0) {
udp_close_flow(f);
break;
}
if (rc > 0) {
if (udp_open_relay_socket(f, &relay_addr) < 0) {
udp_close_flow(f);
break;
}
epoll_mod_udp_ctrl(f, EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP);
f->last_used = time(NULL);
if (udp_flush_pending(f) < 0) {
udp_close_flow(f);
break;
}
udp_update_events(f);
} else {
epoll_mod_udp_ctrl(f, udp_ctrl_events(f));
}
}
break;
}
case FD_UDP_RELAY: {
/* Handle UDP relay responses (incoming from SOCKS proxy) */
struct udp_flow *f;
if (key.index >= (uint8_t)udp_flow_limit)
break;
f = &udp_flows[key.index];
if (epoll_key_matches(&key, FD_UDP_RELAY, key.index, f->udp_relay,
f->generation)) {
if ((events[i].events & EPOLLOUT) && f->pending_set) {
if (udp_flush_pending(f) < 0) {
udp_close_flow(f);
break;
}
}
struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
ssize_t r = recvfrom(f->udp_relay, g_io_buf, sizeof(g_io_buf), 0,
(struct sockaddr *)&from, &fromlen);
/* Direct and SOCKS relay traffic must return from the exact source
* address and port the broker bound to for the flow. */
if (r > 0 &&
(from.sin_addr.s_addr != f->relay_addr.sin_addr.s_addr ||
from.sin_port != f->relay_addr.sin_port)) {
DBG("[parent] UDP source mismatch: got %s:%d, expected %s:%d",
inet_ntoa(from.sin_addr), ntohs(from.sin_port),
inet_ntoa(f->relay_addr.sin_addr),
ntohs(f->relay_addr.sin_port));
break; /* Packet from unexpected source */
}
if (r > 0 && f->tcp_ctrl < 0) {
udp_inject_tun(tunfd, f, g_io_buf, (size_t)r);
f->last_used = time(NULL);
} else if (r > 10) {
DBG("UDP relay received %zd bytes from %s:%d", r,
inet_ntoa(from.sin_addr), ntohs(from.sin_port));
/* Validate FRAG field (byte 2) - we don't support fragmentation */
if (g_io_buf[2] != 0)
break;
/* Strip SOCKS5 UDP header */
size_t hdr_len = 10;
if (g_io_buf[3] == 0x03)
hdr_len = 4 + 1 + g_io_buf[4] + 2;
else if (g_io_buf[3] == 0x04)
hdr_len = 4 + 16 + 2;
if ((size_t)r > hdr_len) {
udp_inject_tun(tunfd, f, g_io_buf + hdr_len, (size_t)r - hdr_len);
f->last_used = time(NULL);
}
} else if (r == 0) {
/* Relay closed */
udp_close_flow(f);
break;
}
udp_update_events(f);
}
break;
}
case FD_PUBLISH_TCP_LISTENER: {
struct publish_rule *rule;
if (key.index >= (uint8_t)publish_rule_count)
break;
rule = &publish_rules[key.index];
if (rule->proto == IPPROTO_TCP &&
epoll_key_matches(&key, FD_PUBLISH_TCP_LISTENER, key.index,
rule->listen_fd, 1))
handle_publish_tcp_listener(tunfd, rule);
break;
}
case FD_PUBLISH_TCP_HOST: {
struct publish_tcp_flow *f;
if (key.index >= MAX_PUBLISH_TCP)
break;
f = &publish_tcp_flows[key.index];
if (f->used && epoll_key_matches(&key, FD_PUBLISH_TCP_HOST, key.index,
f->host_fd, f->generation))
handle_publish_tcp_host_event(tunfd, f, events[i].events);
break;
}
case FD_PUBLISH_UDP_SOCKET: {
struct publish_rule *rule;
if (key.index >= (uint8_t)publish_rule_count)
break;
rule = &publish_rules[key.index];
if (rule->proto == IPPROTO_UDP &&
epoll_key_matches(&key, FD_PUBLISH_UDP_SOCKET, key.index,
rule->listen_fd, 1))
handle_publish_udp_socket(tunfd, rule);
break;
}
}
}
/* Cleanup stale TCP flows */
time_t now = time(NULL);
for (int i = 0; i < tcp_flow_limit; i++) {
struct tcp_flow *f = &tcp_flows[i];
if (f->sock < 0) continue;
int timeout = TCP_IDLE_TIMEOUT_SEC;
if (f->state == SP_TCP_SYN_RECEIVED) timeout = TCP_HALF_OPEN_TIMEOUT_SEC;
else if (f->state != SP_TCP_ESTABLISHED && f->state != SP_TCP_CLOSE_WAIT)
timeout = 10; /* Quick cleanup for FIN_WAIT / CLOSING / LAST_ACK / TIME_WAIT */
if ((now - f->last_active) > timeout) {
if (f->state == SP_TCP_ESTABLISHED || f->state == SP_TCP_CLOSE_WAIT) {
send_tcp_packet(tunfd, f, 0x01, NULL, 0); /* FIN */
f->state = SP_TCP_FIN_WAIT_1;
f->last_active = now;
shutdown(f->sock, SHUT_WR);
tcp_update_events(f);
} else {
/* Force close for other states or if already closing and timed out */
epoll_del(f->sock);
close(f->sock);
f->sock = -1;
f->state = SP_TCP_CLOSED;
}
}
}
/* Cleanup stale UDP flows (idle for >30 seconds) */
for (int i = 0; i < udp_flow_limit; i++) {
if ((udp_flows[i].udp_relay >= 0 || udp_flows[i].tcp_ctrl >= 0) &&
(now - udp_flows[i].last_used) > 30) {
udp_close_flow(&udp_flows[i]);
}
}
for (int i = 0; i < MAX_PUBLISH_UDP; ++i) {
if (publish_udp_flows[i].used &&
(now - publish_udp_flows[i].last_active) > 30)
memset(&publish_udp_flows[i], 0, sizeof(publish_udp_flows[i]));
}
for (int i = 0; i < MAX_PUBLISH_TCP; ++i) {
struct publish_tcp_flow *f = &publish_tcp_flows[i];
int timeout = TCP_IDLE_TIMEOUT_SEC;
if (!f->used)
continue;
if (f->state == PUBLISH_TCP_SYN_SENT)
timeout = TCP_HALF_OPEN_TIMEOUT_SEC;
if ((now - f->last_active) > timeout)
publish_tcp_close_with_rst(tunfd, f);
}
loop_end:
if (child_status != -1 && noninteractive_relays_complete &&
interactive_pty_active <= 0)
break;
}
if (child_pidfd >= 0)
broker_close_child_pidfd(&child_pidfd, &child_pidfd_active);
g_tun_registered_fd = -1;
return child_status;
}
/* ---------- main ---------- */
int main(int argc, char **argv) {
int cmd_start = 1;
pre_scan_verbose_flag(argc, argv);
bench_trace_init_from_env();
bench_trace_set_role(BENCH_ROLE_PARENT);
bench_mark("process_start", "main", "ok", "sockpuppet_legacy");
bench_phase_begin("arg_parse");
interactive_session.host_tty_fd = -1;
interactive_session.pty_master_fd = -1;
interactive_session.pty_slave_fd = -1;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--socks") == 0 && i + 1 < argc) {
parse_socks_url(argv[i + 1], &socks_proxy);
if (socks_proxy.enabled) {
fprintf(stderr, "Using SOCKS5%s proxy: %s:%d%s\n",
socks_proxy.remote_dns ? "h" : "", socks_proxy.host,
socks_proxy.port,
socks_proxy.username[0] ? " (with auth)" : "");
}
i++; /* skip next arg (the proxy URL) */
cmd_start = i + 1;
} else if (strcmp(argv[i], "--unsafe-share-cwd") == 0) {
unsafe_share_cwd = 1;
cmd_start = i + 1;
} else if (strcmp(argv[i], "--require-writable-cwd") == 0) {
require_writable_cwd = 1;
cmd_start = i + 1;
} else if (strcmp(argv[i], "--interactive") == 0) {
interactive_stdio = 1;
cmd_start = i + 1;
} else if (strcmp(argv[i], "--compat-ptrace-clone3") == 0) {
clone3_ptrace_compat = 1;
cmd_start = i + 1;
} else if (strncmp(argv[i], "--max-tcp-flows=", 16) == 0) {
tcp_flow_limit =
parse_flow_limit_option(argv[i] + 16, MAX_TCP, "--max-tcp-flows");
cmd_start = i + 1;
} else if (strncmp(argv[i], "--max-udp-flows=", 16) == 0) {
udp_flow_limit =
parse_flow_limit_option(argv[i] + 16, MAX_UDP, "--max-udp-flows");
cmd_start = i + 1;
} else if (strncmp(argv[i], "--socks-auth-file=", 18) == 0) {
parse_socks_auth_file(argv[i] + 18);
cmd_start = i + 1;
} else if (strncmp(argv[i], "--egress=", 9) == 0) {
char err[128];
if (parse_egress_mode(argv[i] + 9, err, sizeof(err)) < 0) {
fprintf(stderr, "Invalid --egress: %s\n", err);
return 1;
}
cmd_start = i + 1;
} else if (strncmp(argv[i], "--publish=", 10) == 0) {
char err[160];
if (add_publish_rule_from_spec(argv[i] + 10, err, sizeof(err)) < 0) {
fprintf(stderr, "Invalid --publish: %s\n", err);
return 1;
}
cmd_start = i + 1;
} else if (strcmp(argv[i], "-p") == 0) {
char err[160];
if (i + 1 >= argc) {
fprintf(stderr, "Invalid -p: missing publish rule\n");
return 1;
}
if (add_publish_rule_from_spec(argv[i + 1], err, sizeof(err)) < 0) {
fprintf(stderr, "Invalid -p: %s\n", err);
return 1;
}
i++;
cmd_start = i + 1;
} else if (strcmp(argv[i], "--verbose") == 0 ||
strcmp(argv[i], "-v") == 0) {
verbose = 1;
cmd_start = i + 1;
} else if (strncmp(argv[i], "--allow-host=", 13) == 0) {
const char *spec = argv[i] + 13;
if (host_rule_count < MAX_HOST_RULES) {
struct host_rule *r = &host_rules[host_rule_count];
memset(r, 0, sizeof(*r));
/* Make a mutable copy for parsing */
char buf[128];
strncpy(buf, spec, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
/* Parse protocol suffix /tcp or /udp */
char *slash = strchr(buf, '/');
if (slash) {
*slash = '\0';
if (strcmp(slash + 1, "tcp") == 0)
r->proto = IPPROTO_TCP;
else if (strcmp(slash + 1, "udp") == 0)
r->proto = IPPROTO_UDP;
else {
fprintf(stderr, "Invalid protocol: %s (use /tcp or /udp)\n",
slash + 1);
cmd_start = i + 1;
continue;
}
}
/* Parse 127.0.0.X:PORT format */
if (strncmp(buf, "127.0.0.", 8) == 0) {
char *colon = strchr(buf + 8, ':');
if (colon) {
long last_octet;
long port_val;
*colon = '\0';
const char *port_str = colon + 1;
if (parse_long_strict(buf + 8, 0, 255, &last_octet) < 0) {
fprintf(stderr, "Invalid IP: %s (must be 127.0.0.X)\n", spec);
cmd_start = i + 1;
continue;
}
r->last_octet = (uint8_t)last_octet;
if (strcmp(port_str, "*") == 0) {
fprintf(stderr, "Wildcard port not allowed\n");
cmd_start = i + 1;
continue;
} else {
if (parse_long_strict(port_str, 1, 65535, &port_val) < 0) {
fprintf(stderr, "Invalid port: %s\n", port_str);
cmd_start = i + 1;
continue;
}
r->port = (uint16_t)port_val;
}
host_rule_count++;
const char *proto_str = r->proto == IPPROTO_TCP ? "/tcp"
: r->proto == IPPROTO_UDP ? "/udp"
: "";
DBG("Host gateway: 127.0.0.%d:%d%s", r->last_octet, r->port, proto_str);
} else {
fprintf(stderr, "Invalid format: %s (expected 127.0.0.X:PORT)\n",
spec);
}
} else {
fprintf(stderr, "Invalid IP: %s (must be 127.0.0.X)\n", spec);
}
}
cmd_start = i + 1;
} else if (strncmp(argv[i], "--allow-direct=", 15) == 0) {
char err[128];
if (parse_direct_allow_spec(argv[i] + 15, err, sizeof(err)) < 0) {
fprintf(stderr, "Invalid --allow-direct: %s\n", err);
return 1;
}
cmd_start = i + 1;
} else {
/* First non-flag argument is the command */
cmd_start = i;
break;
}
}
if (cmd_start >= argc) {
fprintf(stderr, "usage: %s [OPTIONS] <cmd> [args...]\n\n", argv[0]);
fprintf(stderr, "Options:\n");
fprintf(stderr,
" --socks <proxy> SOCKS5 proxy; bare host:port defaults to proxy DNS\n");
fprintf(stderr, " --socks-auth-file= File with proxy credentials\n");
fprintf(stderr, " --egress=direct|socks|none\n");
fprintf(stderr,
" Outbound child egress policy (default direct, socks inferred with --socks)\n");
fprintf(stderr,
" --publish=127.X.Y.Z:HOST:CONTAINER/PROTO\n");
fprintf(stderr,
" -p 127.X.Y.Z:HOST:CONTAINER/PROTO\n");
fprintf(stderr,
" Publish sandbox TCP/UDP service on host loopback alias\n");
fprintf(stderr, " --unsafe-share-cwd Allow unsafe sandbox source paths\n");
fprintf(stderr, " --require-writable-cwd\n");
fprintf(stderr, " Fail if cwd overlayfs is unavailable\n");
fprintf(stderr, " --interactive Attach child to a private PTY\n");
fprintf(stderr,
" --compat-ptrace-clone3\n"
" Opt into legacy ptrace clone3 translation\n");
fprintf(stderr, " --max-tcp-flows=N Active TCP flow slots (1-%d, default %d)\n",
MAX_TCP, MAX_TCP);
fprintf(stderr, " --max-udp-flows=N Active UDP flow slots (1-%d, default %d)\n",
MAX_UDP, MAX_UDP);
fprintf(stderr, " -v, --verbose Print debug info\n");
fprintf(stderr,
"\nHost gateway (child accesses 10.0.1.x -> host 127.0.0.x):\n");
fprintf(stderr, " --allow-host=127.0.0.X:PORT/PROTO Allow IP:PORT\n");
fprintf(stderr,
"\nDirect egress exceptions for blocked special/private IPv4 ranges:\n");
fprintf(stderr,
" --allow-direct=ADDR[/PREFIX]:PORT[/PROTO] Allow direct IP:PORT\n");
fprintf(stderr, "\nExamples:\n");
fprintf(stderr, " %s --allow-host=127.0.0.1:8080/tcp curl 10.0.1.1:8080\n", argv[0]);
fprintf(stderr,
" %s --publish=127.0.0.2:8080:8080/tcp python3 -m http.server 8080\n",
argv[0]);
fprintf(stderr,
" %s --allow-direct=169.254.169.254/32:80/tcp curl http://169.254.169.254/\n",
argv[0]);
return 1;
}
bench_phase_end("arg_parse", "ok");
if (!egress_mode_explicit && socks_proxy.enabled)
egress_mode = EGRESS_SOCKS;
if (egress_mode == EGRESS_NONE && socks_proxy.enabled) {
fprintf(stderr, "--egress=none cannot be combined with --socks\n");
return 1;
}
if (egress_mode == EGRESS_SOCKS && !socks_proxy.enabled) {
fprintf(stderr, "--egress=socks requires --socks\n");
return 1;
}
if (egress_mode == EGRESS_DIRECT && socks_proxy.enabled &&
egress_mode_explicit) {
fprintf(stderr, "--egress=direct cannot be combined with --socks\n");
return 1;
}
bench_phase_begin("socks_resolve");
if (egress_mode == EGRESS_SOCKS && resolve_socks_proxy(&socks_proxy) < 0) {
fprintf(stderr, "Failed to resolve SOCKS proxy %s:%d\n", socks_proxy.host,
socks_proxy.port);
return 1;
}
bench_phase_end("socks_resolve",
egress_mode == EGRESS_SOCKS ? "ok" : "skipped");
bench_phase_begin("publish_listener_setup");
if (setup_publish_listeners() < 0)
return 1;
bench_phase_end("publish_listener_setup", "ok");
/* Shift argv to command */
argv = &argv[cmd_start];
argc -= cmd_start;
bench_phase_begin("ipc_setup");
int ctl[2], sync[2];
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, ctl) < 0)
die("socketpair ctl");
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sync) < 0)
die("socketpair sync");
bench_phase_end("ipc_setup", "ok");
char overlay_base[] = "/tmp/.sockpuppet-overlay-XXXXXX";
bench_phase_begin("scratch_setup");
if (!mkdtemp(overlay_base))
die("mkdtemp overlay base");
bench_phase_end("scratch_setup", "ok");
int overlay_base_cleaned = 0;
uid_t uid = getuid();
gid_t gid = getgid();
for (int i = 0; i < MAX_TCP; i++)
tcp_flows[i].sock = -1;
for (int i = 0; i < MAX_UDP; i++) {
udp_flows[i].udp_relay = -1;
udp_flows[i].udp_staging = -1;
udp_flows[i].tcp_ctrl = -1;
}
/* Create epoll instance for event loop */
bench_phase_begin("event_epoll_setup");
g_epfd = epoll_create1(EPOLL_CLOEXEC);
if (g_epfd < 0)
die("epoll_create1");
bench_phase_end("event_epoll_setup", "ok");
bench_phase_begin("stdio_setup");
if (interactive_stdio) {
struct sigaction sa;
if (interactive_parent_setup(&interactive_session) < 0) {
fprintf(stderr, "Interactive mode requires a usable parent tty and PTY support (%s)\n",
strerror(errno));
(void)interactive_close_session(&interactive_session);
return 1;
}
if (atexit(interactive_atexit_cleanup) != 0) {
(void)interactive_close_session(&interactive_session);
fprintf(stderr, "Could not register interactive terminal cleanup\n");
return 1;
}
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_handler = interactive_handle_sigwinch;
if (sigaction(SIGWINCH, &sa, NULL) < 0) {
(void)interactive_close_session(&interactive_session);
die("sigaction SIGWINCH");
}
}
int stdout_pipe[2] = {-1, -1};
int stderr_pipe[2] = {-1, -1};
struct relay_destination stdout_destination = {.fd = -1};
struct relay_destination stderr_destination = {.fd = -1};
if (!interactive_stdio) {
if (pipe(stdout_pipe) < 0 || pipe(stderr_pipe) < 0) die("pipe");
fcntl(stdout_pipe[0], F_SETFL, fcntl(stdout_pipe[0], F_GETFL) | O_NONBLOCK);
fcntl(stderr_pipe[0], F_SETFL, fcntl(stderr_pipe[0], F_GETFL) | O_NONBLOCK);
if (relay_prepare_destination(STDOUT_FILENO, &stdout_destination) < 0)
die("prepare nonblocking stdout relay");
if (relay_prepare_destination(STDERR_FILENO, &stderr_destination) < 0)
die("prepare nonblocking stderr relay");
}
bench_phase_end("stdio_setup", interactive_stdio ? "interactive" : "pipes");
bench_phase_begin("parent_rlimits");
apply_parent_rlimits();
bench_phase_end("parent_rlimits", "ok");
bench_phase_begin("cgroup_setup");
cgroup_setup_containment();
bench_phase_end("cgroup_setup", g_cgroup.active ? "active" : "inactive");
pid_t broker_pid = getpid();
bench_phase_begin("outer_fork");
pid_t pid = fork();
if (pid < 0) {
(void)interactive_close_session(&interactive_session);
die("fork");
}
if (pid == 0) {
/* ---------- child ---------- */
relay_close_destination(&stdout_destination);
relay_close_destination(&stderr_destination);
if (interactive_session.host_tty_fd >= 0) {
close(interactive_session.host_tty_fd);
interactive_session.host_tty_fd = -1;
}
bench_trace_set_role(BENCH_ROLE_OUTER_CHILD);
bench_mark("handoff", "outer_fork", "outer_child", NULL);
bench_mark("process_start", "outer_child", "ok", NULL);
bench_phase_begin("outer_child_setup");
arm_parent_death_signal(broker_pid);
close(ctl[0]);
close(sync[0]);
close(g_epfd);
g_epfd = -1;
close_publish_listeners();
if (interactive_session.pty_master_fd >= 0)
close(interactive_session.pty_master_fd);
bench_phase_end("outer_child_setup", "ok");
/* Cgroup placement barrier: the parent moves this process into the
* payload leaf via cgroup.procs, which does not follow descendants.
* Block until that move has happened so inner PID 1 and the payload
* fork after it and inherit the payload leaf instead of racing into
* the broker leaf. */
bench_phase_begin("cgroup_placement_barrier");
char cgroup_barrier = 0;
if (read_all(sync[1], &cgroup_barrier, 1) != 1 || cgroup_barrier != 'C')
die("cgroup placement barrier");
bench_phase_end("cgroup_placement_barrier", "ok");
bench_phase_begin("namespace_unshare");
if (unshare(CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWIPC |
CLONE_NEWUTS | CLONE_NEWPID) < 0) {
int saved = errno;
if (saved == EPERM || saved == EACCES || saved == EINVAL ||
saved == ENOSPC)
report_userns_diagnostics(saved);
else
fprintf(stderr, "[sockpuppet] Error: unshare failed: %s\n",
strerror(saved));
errno = saved;
_exit(1);
}
bench_phase_end("namespace_unshare", "ok");
DBG("Created namespaces: user, net, mnt, ipc, uts, pid");
int trace_ctl[2] = {-1, -1};
if (clone3_ptrace_compat &&
socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, trace_ctl) < 0)
die("socketpair trace_ctl");
/* Parent-death liveness pipe for inner PID 1; see
* arm_pid1_parent_death_signal(). */
int pdeath_liveness[2];
if (pipe2(pdeath_liveness, O_CLOEXEC) < 0)
die("pipe2 pdeath liveness");
/* CLONE_NEWPID requires a second fork - the child becomes PID 1 */
bench_phase_begin("pid_namespace_fork");
pid_t inner_pid = fork();
if (inner_pid < 0)
die("fork (inner)");
if (inner_pid > 0) {
int exit_code;
bench_phase_end("pid_namespace_fork", "outer_child");
if (getenv("SOCKPUPPET_TEST_PID1_PDEATH_RACE") != NULL)
_exit(42);
close(pdeath_liveness[0]);
/* Keep pdeath_liveness[1] open for this supervisor's lifetime so
* inner PID 1 can detect a supervisor death that predates its
* PR_SET_PDEATHSIG arming. */
if (trace_ctl[1] >= 0)
close(trace_ctl[1]);
close_middle_supervisor_fds(ctl[1], sync[1], stdout_pipe, stderr_pipe,
&interactive_session);
bench_phase_begin("pid1_supervision_wait");
if (clone3_ptrace_compat) {
exit_code = sp_supervise_inner_child(inner_pid, trace_ctl[0]);
close(trace_ctl[0]);
} else {
exit_code = sp_wait_for_inner_child(inner_pid);
}
bench_phase_end("pid1_supervision_wait", "ok");
_exit(exit_code);
}
/* ---------- inner child (PID 1 in new namespace) ---------- */
bench_trace_set_role(BENCH_ROLE_PID1);
bench_mark("handoff", "pid_namespace_fork", "pid1", NULL);
bench_mark("process_start", "pid1", "ok", NULL);
if (getenv("SOCKPUPPET_TEST_PID1_PDEATH_RACE") != NULL)
(void)poll(NULL, 0, 200); /* widen the fork-to-arm race window */
close(pdeath_liveness[1]);
arm_pid1_parent_death_signal(pdeath_liveness[0]);
if (trace_ctl[0] >= 0)
close(trace_ctl[0]);
bench_phase_begin("uid_gid_sync");
if (write_all(sync[1], "1", 1) != 1)
die("sync ready");
if (read_all(sync[1], &uid, sizeof(uid)) != (ssize_t)sizeof(uid))
die("sync uid");
if (read_all(sync[1], &gid, sizeof(gid)) != (ssize_t)sizeof(gid))
die("sync gid");
close(sync[1]);
bench_phase_end("uid_gid_sync", "ok");
enum sp_clone3_seccomp_mode clone3_seccomp_mode =
SP_CLONE3_SECCOMP_ENOSYS;
bench_phase_begin("clone3_compat_handshake");
if (clone3_ptrace_compat) {
clone3_seccomp_mode = sp_inner_clone3_ptrace_handshake(trace_ctl[1]);
close(trace_ctl[1]);
} else {
DBG("clone3 ptrace compatibility disabled; using ENOSYS seccomp mode");
}
bench_phase_end("clone3_compat_handshake",
clone3_ptrace_compat ? "ok" : "skipped");
char cwd[PATH_MAX];
struct fs_sandbox fs_sandbox;
if (!getcwd(cwd, sizeof(cwd)))
die("getcwd");
bench_phase_begin("filesystem_setup");
prepare_fs_sandbox(&fs_sandbox, cwd, overlay_base);
bench_phase_end("filesystem_setup",
fs_sandbox.cwd_readonly_fallback ? "fallback" : "ok");
bench_phase_begin("tun_create");
int tunfd = tun_create("tun0");
bench_phase_end("tun_create", "ok");
/* network config inside child netns */
bench_phase_begin("network_setup");
if_up_netlink("lo");
if_addr_ptp("tun0", "10.0.0.2", "10.0.0.1");
if_up("tun0");
add_default_route("tun0", "10.0.0.1");
DBG("Network setup: tun0 (10.0.0.2 -> 10.0.0.1), lo up");
bench_phase_end("network_setup", "ok");
fcntl(tunfd, F_SETFD, FD_CLOEXEC);
bench_phase_begin("tun_fd_handoff");
send_fd(ctl[1], tunfd);
close(tunfd);
close(ctl[1]);
bench_phase_end("tun_fd_handoff", "ok");
bench_phase_begin("fs_enter");
enter_fs_sandbox(&fs_sandbox);
bench_phase_end("fs_enter", "ok");
/* Privilege dropping is mandatory */
bench_phase_begin("capability_drop");
drop_caps();
DBG("Dropped capabilities");
bench_phase_end("capability_drop", "ok");
bench_phase_begin("child_landlock");
if (apply_landlock_policy(&fs_sandbox) < 0)
die("landlock_restrict_self");
bench_phase_end("child_landlock", "ok");
bench_phase_begin("child_seccomp");
if (apply_child_seccomp(clone3_seccomp_mode) < 0)
die("seccomp");
bench_phase_end("child_seccomp", "ok");
char **envp = build_sanitized_envp(fs_sandbox.resolved_cwd);
bench_phase_begin("payload_init");
int payload_exit =
run_payload_init(argv, envp, &interactive_session, stdout_pipe,
stderr_pipe);
bench_phase_end("payload_init", "ok");
bench_mark("process_exit", "pid1", "ok", NULL);
_exit(payload_exit);
}
bench_phase_end("outer_fork", "parent");
/* ---------- parent ---------- */
struct sigaction sigpipe_ignore;
memset(&sigpipe_ignore, 0, sizeof(sigpipe_ignore));
sigemptyset(&sigpipe_ignore.sa_mask);
sigpipe_ignore.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &sigpipe_ignore, NULL) < 0)
parent_die_with_child(pid, "sigaction SIGPIPE");
struct parent_harden_config parent_harden =
parent_harden_config_from_runtime();
struct parent_harden_status parent_harden_status = {0};
bench_phase_begin("cgroup_move_child");
if (cgroup_move_child_to_payload(pid) < 0)
parent_die_with_child(pid, "cgroup move child to payload");
/* Release the child's cgroup placement barrier only after the move so
* every descendant it creates inherits the payload leaf. */
if (write_all(sync[0], "C", 1) != 1)
parent_die_with_child(pid, "cgroup placement barrier");
bench_phase_end("cgroup_move_child", g_cgroup.active ? "ok" : "skipped");
close(ctl[1]);
close(sync[1]);
if (interactive_session.pty_slave_fd >= 0) {
close(interactive_session.pty_slave_fd);
interactive_session.pty_slave_fd = -1;
}
bench_phase_begin("uid_gid_map");
char tmp;
if (read_all(sync[0], &tmp, 1) != 1)
parent_die_with_child(pid, "sync ready");
char path[128], map[64];
snprintf(path, sizeof(path), "/proc/%d/setgroups", pid);
if (write_file_checked(path, "deny") < 0)
parent_die_with_child(pid, path);
snprintf(path, sizeof(path), "/proc/%d/uid_map", pid);
snprintf(map, sizeof(map), "%d %d 1\n", uid, uid);
if (write_file_checked(path, map) < 0)
parent_die_with_child(pid, path);
snprintf(path, sizeof(path), "/proc/%d/gid_map", pid);
snprintf(map, sizeof(map), "%d %d 1\n", gid, gid);
if (write_file_checked(path, map) < 0)
parent_die_with_child(pid, path);
if (write_all(sync[0], &uid, sizeof(uid)) != (ssize_t)sizeof(uid))
parent_die_with_child(pid, "sync uid");
if (write_all(sync[0], &gid, sizeof(gid)) != (ssize_t)sizeof(gid))
parent_die_with_child(pid, "sync gid");
(void)close(sync[0]);
sync[0] = -1;
bench_phase_end("uid_gid_map", "ok");
if (!interactive_stdio) {
close(stdout_pipe[1]);
close(stderr_pipe[1]);
}
bench_phase_begin("tun_fd_recv");
int tunfd = recv_fd_checked(ctl[0]);
if (tunfd < 0)
parent_die_with_child(pid, "recv_fd");
(void)close(ctl[0]);
ctl[0] = -1;
bench_phase_end("tun_fd_recv", "ok");
parent_harden_status.setup_fds_closed = 1;
bench_phase_begin("parent_cleanup");
if (cleanup_overlay_base(overlay_base) < 0)
parent_die_with_child(pid, "parent overlay cleanup");
overlay_base_cleaned = 1;
bench_phase_end("parent_cleanup", "ok");
parent_harden_status.scratch_cleaned = 1;
int child_pidfd = open_child_pidfd(pid);
if (child_pidfd < 0) {
DBG("child pidfd unavailable: %s; falling back to timeout polling",
strerror(errno));
}
parent_harden.child_pid = pid;
parent_harden.child_pidfd = child_pidfd;
parent_harden_log_prepared(&parent_harden, &parent_harden_status);
bench_phase_begin("parent_landlock");
if (apply_parent_landlock_policy() < 0)
parent_die_with_child(pid, "parent landlock");
parent_harden_status.landlock_active = 1;
if (parent_harden_probe_landlock() < 0)
parent_die_with_child(pid, "parent hardening Landlock probe");
bench_phase_end("parent_landlock", "ok");
bench_phase_begin("parent_seccomp");
if (apply_parent_seccomp(&parent_harden) < 0)
parent_die_with_child(pid, "parent seccomp");
parent_harden_status.seccomp_active = 1;
parent_harden_log_active(&parent_harden);
if (parent_harden_probe_seccomp(&parent_harden) < 0)
return 1;
bench_phase_end("parent_seccomp", "ok");
bench_mark("broker_ready", "event_loop", "ok", NULL);
int status =
event_loop(tunfd, pid, child_pidfd, stdout_pipe[0], stderr_pipe[0],
&stdout_destination, &stderr_destination,
&interactive_session);
relay_close_destination(&stdout_destination);
relay_close_destination(&stderr_destination);
close_publish_listeners();
int terminal_restore_failed =
interactive_close_session(&interactive_session) < 0;
if (status < 0)
broker_fatal_kill_child(pid, child_pidfd);
if (!overlay_base_cleaned)
(void)cleanup_overlay_base(overlay_base);
bench_mark("process_exit", "parent", status < 0 ? "error" : "ok", NULL);
/* A broker-fatal exit reports failure regardless of how the killed
* child's status decodes; only a normally reaped child exit propagates. */
if (status < 0)
return 1;
if (terminal_restore_failed)
return 1;
maybe_print_noninteractive_shell_hint(argc, argv, status);
if (WIFEXITED(status))
return WEXITSTATUS(status);
return 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment