Last active
July 17, 2026 06:44
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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, ®s->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 *)®s->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 = ®s->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 = ®s->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 *)®s->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 *)®s->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, ®s) < 0) { | |
| DBG("ptrace register read failed for pid %ld: %s", (long)pid, | |
| strerror(errno)); | |
| return -1; | |
| } | |
| syscall_no = sp_arch_get_syscall(®s); | |
| #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(®s, 0); | |
| size = sp_arch_get_arg(®s, 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(®s, clone_flags, child_stack, parent_tid, | |
| child_tid, tls); | |
| if (sp_arch_commit_regs(pid, ®s) < 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(®s, err); | |
| if (sp_arch_commit_regs(pid, ®s) < 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