Skip to content

Instantly share code, notes, and snippets.

@rrampage
Last active October 5, 2026 16:09
Show Gist options
  • Select an option

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

Select an option

Save rrampage/92f0eb6bf56d7bb403aff069cc8f1d6b to your computer and use it in GitHub Desktop.
A userspace sandbox which uses SOCKS proxy to restrict network access (inspired by oniux)
#define _GNU_SOURCE
/*
* sockpuppet.c - single-file Linux sandbox + userspace network broker
*
* Quick build:
* gcc -O2 -g -Wall -Wextra -Wformat -Wformat=2 -Wconversion \
* -Wimplicit-fallthrough -Werror=format-security \
* -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS \
* -fstack-clash-protection -fstack-protector-strong \
* -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer \
* -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.
*
* 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>
/* Assertions are always on (STYLEGUIDE.md): a broken invariant crashes the
* broker, which tears the sandbox down, instead of continuing. */
#undef NDEBUG
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <linux/audit.h>
#include <linux/capability.h>
#include <linux/filter.h>
#include <linux/if.h>
#include <linux/if_link.h>
#include <linux/if_tun.h>
#include <linux/mount.h>
#include <linux/netlink.h>
#include <linux/openat2.h>
#include <linux/rtnetlink.h>
#include <linux/sched.h>
#include <linux/securebits.h>
#include <linux/seccomp.h>
#include <linux/virtio_net.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 <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/mman.h>
#include <sys/prctl.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>
/* Native x86-64, arm64 and riscv64 share this number (Linux 6.15).
* Older build headers must still deny it on newer running kernels. */
#if !defined(__NR_open_tree_attr) && \
(defined(__x86_64__) || defined(__aarch64__) || \
(defined(__riscv) && __riscv_xlen == 64))
#define __NR_open_tree_attr 467
#endif
#ifndef SOL_UDP
#define SOL_UDP IPPROTO_UDP
#endif
#ifndef UDP_GRO
#define UDP_GRO 104
#endif
#ifndef SOCKPUPPET_TCP_CORE_ONLY
typedef ssize_t (*sp_random_source_fn)(void *, size_t, unsigned int);
typedef int (*sp_recvmmsg_fn)(int, struct mmsghdr *, unsigned int, int,
struct timespec *);
typedef int (*sp_sendmmsg_fn)(int, struct mmsghdr *, unsigned int, int);
/* Steady-state authority minting must never wait for entropy. The indirection
* is intentionally narrow: focused tests replace only this syscall boundary
* to prove EAGAIN/short-read handling without depending on host entropy state. */
static sp_random_source_fn g_sp_random_source = getrandom;
static int sp_system_recvmmsg(int fd, struct mmsghdr *messages,
unsigned int count, int flags,
struct timespec *timeout) {
#ifdef __GLIBC__
return recvmmsg(fd, messages, count, flags, timeout);
#else
return recvmmsg(fd, messages, count, (unsigned int)flags, timeout);
#endif
}
static sp_recvmmsg_fn g_sp_recvmmsg = sp_system_recvmmsg;
static int sp_system_sendmmsg(int fd, struct mmsghdr *messages,
unsigned int count, int flags) {
#ifdef __GLIBC__
return sendmmsg(fd, messages, count, flags);
#else
return sendmmsg(fd, messages, count, (unsigned int)flags);
#endif
}
static sp_sendmmsg_fn g_sp_sendmmsg = sp_system_sendmmsg;
static int sp_random_exact_nonblocking(void *out, size_t len) {
ssize_t n;
if (len > (size_t)SSIZE_MAX) {
errno = EOVERFLOW;
return -1;
}
n = g_sp_random_source(out, len, GRND_NONBLOCK);
if (n == (ssize_t)len)
return 0;
if (n >= 0)
errno = EIO;
return -1;
}
#endif
#ifdef SOCKPUPPET_TCP_CORE_ONLY
/*
* The focused compile contract includes this translation-unit prefix and
* stops immediately after the typed TCP core. Poison authority-bearing APIs
* after the system headers have declared them: any later core reference is a
* compile error, while the ordinary single-file build remains unchanged.
*/
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC poison accept accept4 bind clock_gettime close connect epoll_ctl
#pragma GCC poison epoll_wait epoll_pwait epoll_pwait2 fcntl getrandom ioctl
#pragma GCC poison listen open openat pidfd_open poll ppoll read readv recv
#pragma GCC poison recvfrom recvmsg recvmmsg select send sendmmsg sendmsg sendto
#pragma GCC poison shutdown socket syscall time timerfd_create timerfd_settime
#pragma GCC poison write writev
#pragma GCC poison sp_counter_add sp_counter_inc
#else
#error "SOCKPUPPET_TCP_CORE_ONLY requires compiler identifier poisoning"
#endif
#endif
enum {
SP_ACCEPTED_PROFILE_VERSION = 1,
SP_TCP_CORE_MIN_MSS = 48,
SP_TCP_CORE_DEFAULT_MSS = 536,
SP_TCP_CORE_MAX_MSS = 1460,
SP_TCP_CORE_RCV_WSCALE = 3,
SP_TCP_CORE_PENDING_CAPACITY = 262144,
SP_TCP_CORE_MAX_EFFECTS = 8,
SP_TCP_CORE_MAX_SEGMENTS = 32,
SP_TCP_CORE_MAX_RTO_RETRIES = 8,
SP_TCP_CORE_K_RX = 6,
};
#define SP_TCP_CORE_RCV_QUANTUM \
(UINT64_C(1) << SP_TCP_CORE_RCV_WSCALE)
#define SP_TCP_CORE_ADVERTISABLE_CAPACITY \
(((uint64_t)SP_TCP_CORE_PENDING_CAPACITY / SP_TCP_CORE_RCV_QUANTUM) * \
SP_TCP_CORE_RCV_QUANTUM)
#define SP_TCP_CORE_UNADVERTISED_TAIL \
((uint64_t)SP_TCP_CORE_PENDING_CAPACITY - \
SP_TCP_CORE_ADVERTISABLE_CAPACITY)
_Static_assert(SP_TCP_CORE_RCV_WSCALE <= 14,
"receive-window shift exceeds RFC 7323 maximum");
_Static_assert(SP_TCP_CORE_ADVERTISABLE_CAPACITY >= SP_TCP_CORE_RCV_QUANTUM,
"receive window must contain a complete scale quantum");
_Static_assert(SP_TCP_CORE_ADVERTISABLE_CAPACITY / SP_TCP_CORE_RCV_QUANTUM <=
UINT16_MAX,
"scaled receive window does not fit the wire field");
_Static_assert(SP_TCP_CORE_UNADVERTISED_TAIL < SP_TCP_CORE_RCV_QUANTUM,
"receive-window floor may hide at most one partial quantum");
enum sp_effect_type {
SP_FX_OPEN_STREAM = 1,
SP_FX_TCP_EMIT_BATCH,
SP_FX_TCP_EMIT_CONTROL,
SP_FX_STREAM_SEND,
SP_FX_STREAM_CONSUME,
SP_FX_INGRESS_DISPOSE,
SP_FX_STREAM_SHUTDOWN,
SP_FX_CLOSE_FLOW,
};
typedef struct { uint64_t value; } sp_flow_id;
typedef struct { uint64_t value; } sp_generation_id;
typedef struct { uint64_t value; } sp_effect_id;
typedef struct { uint64_t value; } sp_ingress_id;
typedef struct { uint64_t value; } sp_capability_id;
typedef struct { uint64_t value; } sp_time_ns;
typedef struct { uint64_t value; } sp_stream_offset;
typedef struct { uint64_t value; } sp_byte_count;
typedef struct { uint64_t value; } sp_core_sequence;
typedef struct { uint64_t value; } sp_segment_count;
struct sp_flow_key {
sp_flow_id flow;
sp_generation_id generation;
};
enum sp_construct_result {
SP_CONSTRUCT_OK = 0,
SP_CONSTRUCT_INTERNAL_INVARIANT = 3,
};
/* Named annotations keep intentional test-build, oracle, and platform helper
* reachability reviewable. `SP_TEST_API` means a helper is deliberately
* visible to focused single-file tests and may be unused in such a translation
* unit; it does not mean that the shipping program cannot call it. */
#define SP_TEST_API __attribute__((unused))
#define SP_ORACLE_API __attribute__((unused))
static inline struct sp_flow_key sp_flow_key_make(sp_flow_id flow,
sp_generation_id generation) {
return (struct sp_flow_key){.flow = flow, .generation = generation};
}
enum sp_tcp_policy_role {
SP_TCP_ROLE_DIRECT = 1,
SP_TCP_ROLE_SOCKS,
SP_TCP_ROLE_PUBLISH,
};
enum sp_tcp_open_role {
SP_TCP_OPEN_PASSIVE = 1,
SP_TCP_OPEN_ACTIVE,
};
enum sp_tcp_core_state {
SP_TCP_CORE_CLOSED = 0,
SP_TCP_CORE_CONNECTING_PASSIVE,
SP_TCP_CORE_CONNECTING_ACTIVE,
SP_TCP_CORE_SYN_RECEIVED,
SP_TCP_CORE_SYN_SENT,
SP_TCP_CORE_ESTABLISHED,
SP_TCP_CORE_FIN_WAIT_1,
SP_TCP_CORE_FIN_WAIT_2,
SP_TCP_CORE_CLOSE_WAIT,
SP_TCP_CORE_CLOSING,
SP_TCP_CORE_LAST_ACK,
SP_TCP_CORE_TIME_WAIT,
};
/* Independent deadlines. Ordering within this enum is not the firing
* precedence; sp_tcp_timer_priority() defines that ladder. */
enum sp_tcp_timer_kind {
SP_TCP_TIMER_HANDSHAKE = 1,
SP_TCP_TIMER_RTO,
SP_TCP_TIMER_PERSIST,
SP_TCP_TIMER_SEND_AGE,
SP_TCP_TIMER_RECEIVE_AGE,
SP_TCP_TIMER_CLOSE,
SP_TCP_TIMER_TIME_WAIT,
};
enum sp_tcp_core_result {
SP_TCP_CORE_OK = 0,
SP_TCP_CORE_REJECTED,
SP_TCP_CORE_BAD_EVENT,
SP_TCP_CORE_EFFECT_OVERFLOW,
};
enum sp_tcp_core_flag {
SP_TCP_CORE_FIN = 0x01,
SP_TCP_CORE_SYN = 0x02,
SP_TCP_CORE_RST = 0x04,
SP_TCP_CORE_PSH = 0x08,
SP_TCP_CORE_ACK = 0x10,
};
struct sp_tcp_tuple {
uint32_t local_addr;
uint32_t peer_addr;
uint16_t local_port;
uint16_t peer_port;
};
struct sp_tcp_peer_options {
uint16_t mss;
uint8_t mss_present;
uint8_t ws_present;
uint8_t ws_raw;
};
/* Products remain deliberately distinct even when their current payloads have
* the same shape. Exact product identity makes cross-family substitution
* mechanically visible to the compiler. */
struct sp_typed_event_context {
struct sp_flow_key key;
sp_time_ns now;
sp_core_sequence control_sequence;
sp_core_sequence causal_fence;
};
struct sp_ev_flow_admitted {
struct sp_flow_key key;
sp_time_ns now;
struct sp_tcp_tuple tuple;
enum sp_tcp_policy_role policy_role;
enum sp_tcp_open_role open_role;
uint32_t local_iss;
uint32_t peer_isn;
uint16_t peer_window;
uint16_t peer_mss;
uint8_t needs_connect;
uint8_t local_rcv_wscale;
uint8_t passive_syn_present;
uint8_t peer_mss_present;
uint8_t peer_ws_present;
uint8_t peer_ws;
};
struct sp_ev_tcp_segment {
struct sp_typed_event_context context;
sp_ingress_id ingress;
sp_byte_count payload_length;
uint32_t seq;
uint32_t ack;
uint16_t window;
uint16_t mss;
uint8_t flags;
uint8_t mss_present;
uint8_t ws_present;
uint8_t ws;
};
enum sp_connect_outcome {
SP_CONNECT_SUCCEEDED = 1,
SP_CONNECT_FAILED,
};
struct sp_ev_connect_result {
struct sp_typed_event_context context;
sp_effect_id effect;
enum sp_connect_outcome outcome;
};
enum sp_send_outcome {
SP_SEND_PROGRESS = 1,
SP_SEND_RETRY,
SP_SEND_FAILED,
};
struct sp_ev_stream_send_result {
struct sp_typed_event_context context;
sp_effect_id effect;
sp_byte_count accepted;
sp_byte_count retained;
enum sp_send_outcome outcome;
};
struct sp_ev_stream_consumed {
struct sp_typed_event_context context;
sp_effect_id effect;
sp_byte_count accepted;
};
struct sp_ev_stream_eof {
struct sp_typed_event_context context;
sp_stream_offset stream_tail;
};
enum sp_emit_outcome {
SP_EMIT_ACCEPTED = 1,
SP_EMIT_RETAINED,
SP_EMIT_FAILED,
};
struct sp_ev_tun_emit_result {
struct sp_typed_event_context context;
sp_effect_id effect;
sp_byte_count accepted;
enum sp_emit_outcome outcome;
};
struct sp_ev_credit {
struct sp_typed_event_context context;
sp_stream_offset stream_tail;
sp_byte_count pending_capacity;
sp_segment_count tun_segments;
};
struct sp_ev_timer_fired {
struct sp_typed_event_context context;
enum sp_tcp_timer_kind kind;
sp_time_ns deadline;
sp_generation_id timer_generation;
};
struct sp_ev_ingress_disposed {
struct sp_typed_event_context context;
sp_effect_id effect;
};
struct sp_typed_effect_context {
struct sp_flow_key key;
sp_effect_id effect;
};
struct sp_fx_open_stream {
struct sp_typed_effect_context context;
};
struct sp_fx_tcp_emit_batch {
struct sp_typed_effect_context context;
sp_stream_offset start;
sp_stream_offset end;
sp_core_sequence seq;
sp_core_sequence ack;
uint32_t flags;
uint32_t attempt;
uint16_t window;
uint16_t mss;
uint8_t terminal;
};
struct sp_fx_tcp_emit_control {
struct sp_typed_effect_context context;
sp_core_sequence seq;
sp_core_sequence ack;
uint32_t flags;
uint32_t attempt;
uint16_t window;
uint16_t mss;
uint8_t ws_present;
uint8_t ws;
uint8_t terminal;
};
struct sp_fx_stream_send {
struct sp_typed_effect_context context;
sp_capability_id capability;
sp_stream_offset start;
sp_stream_offset end;
};
struct sp_fx_stream_consume {
struct sp_typed_effect_context context;
sp_stream_offset start;
sp_stream_offset end;
};
struct sp_fx_ingress_dispose {
struct sp_typed_effect_context context;
sp_ingress_id ingress;
};
struct sp_fx_stream_shutdown {
struct sp_typed_effect_context context;
};
struct sp_fx_close_flow {
struct sp_typed_effect_context context;
};
struct sp_typed_effect {
enum sp_effect_type type;
union {
struct sp_fx_open_stream open_stream;
struct sp_fx_tcp_emit_batch tcp_emit_batch;
struct sp_fx_tcp_emit_control tcp_emit_control;
struct sp_fx_stream_send stream_send;
struct sp_fx_stream_consume stream_consume;
struct sp_fx_ingress_dispose ingress_dispose;
struct sp_fx_stream_shutdown stream_shutdown;
struct sp_fx_close_flow close_flow;
} value;
};
struct sp_typed_effect_list {
struct sp_typed_effect items[SP_TCP_CORE_MAX_EFFECTS];
size_t count;
};
static const struct sp_typed_effect_context *
sp_typed_effect_context(const struct sp_typed_effect *effect) {
switch (effect->type) {
case SP_FX_OPEN_STREAM:
return &effect->value.open_stream.context;
case SP_FX_TCP_EMIT_BATCH:
return &effect->value.tcp_emit_batch.context;
case SP_FX_TCP_EMIT_CONTROL:
return &effect->value.tcp_emit_control.context;
case SP_FX_STREAM_SEND:
return &effect->value.stream_send.context;
case SP_FX_STREAM_CONSUME:
return &effect->value.stream_consume.context;
case SP_FX_INGRESS_DISPOSE:
return &effect->value.ingress_dispose.context;
case SP_FX_STREAM_SHUTDOWN:
return &effect->value.stream_shutdown.context;
case SP_FX_CLOSE_FLOW:
return &effect->value.close_flow.context;
}
return NULL;
}
_Static_assert(sizeof(sp_flow_id) == sizeof(uint64_t),
"semantic IDs must remain zero-overhead wrappers");
_Static_assert(sizeof(struct sp_typed_event_context) <= 48,
"hot typed event context exceeded its reviewed ceiling");
_Static_assert(sizeof(struct sp_ev_tcp_segment) <= 96,
"hot scalar segment event exceeded its reviewed ceiling");
_Static_assert(sizeof(struct sp_fx_tcp_emit_batch) <= 96,
"hot TCP emit effect exceeded its reviewed ceiling");
struct sp_tcp_live_effect {
uint64_t id;
uint64_t start;
uint64_t end;
uint64_t ingress_id;
uint8_t retransmission;
};
struct sp_tcp_core {
enum sp_tcp_core_state state;
uint64_t flow;
uint64_t generation;
enum sp_tcp_policy_role policy_role;
enum sp_tcp_open_role open_role;
uint64_t next_effect_id;
uint64_t last_now_ns;
uint64_t applied_control_seq;
uint64_t iss_abs;
uint64_t irs_abs;
uint64_t data_seq0_abs;
uint64_t snd_una_abs;
uint64_t snd_nxt_high_abs;
uint64_t snd_wl1_abs;
uint64_t snd_wl2_abs;
uint64_t snd_wnd;
uint64_t stream_consumed;
uint64_t consume_target;
uint64_t stream_acked;
uint64_t stream_injected;
uint64_t stream_tail;
uint64_t final_stream_length;
uint8_t stream_eof;
uint8_t fin_injected;
uint8_t fin_acked;
uint64_t rcv_acked_abs;
uint64_t rx_pending_end_abs;
uint64_t rx_authorized_end_abs;
uint64_t rcv_adv_right_abs;
uint64_t pending_capacity;
uint8_t peer_fin_seen;
uint64_t peer_fin_abs;
uint16_t effective_send_mss;
uint8_t snd_wind_shift;
uint8_t rcv_wind_shift;
uint8_t snd_ws_ok;
uint64_t tun_segment_credit;
struct sp_tcp_live_effect connect_effect;
struct sp_tcp_live_effect send_caps[SP_TCP_CORE_K_RX];
unsigned send_cap_count;
struct sp_tcp_live_effect stream_consume_effect;
struct sp_tcp_live_effect tun_emit_effect;
struct sp_tcp_live_effect ingress_dispose_effect;
uint64_t handshake_deadline_ns;
uint64_t rto_deadline_ns;
uint64_t persist_deadline_ns;
uint64_t send_age_deadline_ns;
uint64_t receive_age_deadline_ns;
uint64_t close_deadline_ns;
uint64_t timewait_deadline_ns;
uint64_t timer_generation;
uint64_t timer_gen[SP_TCP_TIMER_TIME_WAIT + 1];
uint32_t persist_probes;
uint32_t rto_retries;
uint64_t rto_first_ns;
/* RTO recovery invariant: backoff history collapses only once new data
* beyond this recovery point is unambiguously acknowledged. */
uint64_t rto_recovery_point_abs;
/* Fast-retransmit invariant: one bounded episode per recovery point. */
uint64_t dup_ack_ref_abs;
uint32_t dup_ack_count;
uint64_t fast_retransmit_point_abs;
uint8_t fast_retransmit_used;
uint64_t rtt_sample_end_abs;
uint64_t rtt_sample_sent_ns;
uint64_t srtt_ns;
uint64_t rttvar_ns;
uint64_t rtt_samples;
uint8_t rtt_sample_ambiguous;
};
void sp_tcp_core_init(struct sp_tcp_core *core);
int sp_tcp_assert_invariants(const struct sp_tcp_core *core);
uint64_t sp_tcp_unwrap_u32(uint32_t wire, uint64_t checkpoint);
uint16_t sp_tcp_encode_window(const struct sp_tcp_core *core,
uint64_t desired_right,
uint64_t physical_capacity,
uint64_t *encoded_right);
uint64_t sp_tcp_next_deadline_ns(const struct sp_tcp_core *core,
enum sp_tcp_timer_kind *kind_out);
uint64_t sp_tcp_ns_until(uint64_t deadline_ns, uint64_t now_ns);
/* Frozen timer profile. The 200 ms minimum RTO is a deliberate
* nonconformance to RFC 6298's one-second minimum, and the 5 s maximum is a
* deliberate nonconformance to its 60-second minimum permitted maximum; both
* are accepted only for the private synthetic child link and are recorded in
* the deviation register. The one-second initial value is retained. */
enum {
SP_TCP_HANDSHAKE_NS = 10000000000ULL,
SP_TCP_RTO_NS = 1000000000ULL,
SP_TCP_RTO_MIN_NS = 200000000ULL,
SP_TCP_RTO_MAX_NS = 5000000000ULL,
SP_TCP_CLOSE_NS = 120000000000ULL,
SP_TCP_SEND_AGE_NS = 120000000000ULL,
SP_TCP_RECEIVE_AGE_NS = 120000000000ULL,
SP_TCP_TIME_WAIT_NS = 240000000000ULL,
SP_TCP_RTO_MAX_AGE_NS = 300000000000ULL,
SP_TCP_DUP_ACK_THRESHOLD = 3,
};
/* Persist probe schedule: 1, 2, 4, 8, 16, then 30 s capped. */
static const uint64_t sp_persist_schedule_ns[] = {
1000000000ULL, 2000000000ULL, 4000000000ULL,
8000000000ULL, 16000000000ULL, 30000000000ULL,
};
static uint64_t sp_min_u64(uint64_t a, uint64_t b) { return a < b ? a : b; }
static uint64_t sp_persist_delay_ns(uint32_t probes) {
size_t last = (sizeof(sp_persist_schedule_ns) /
sizeof(sp_persist_schedule_ns[0])) - 1;
return sp_persist_schedule_ns[probes < last ? probes : last];
}
/* Unmeasured flows use the one-second initial RTO; once RTT samples exist the
* base is the RFC 6298 estimator clamped into [200 ms, 5 s]. Backoff doubles
* the base and saturates at the same 5 s ceiling, so the eight permitted
* expiries of an unmeasured flow fall at 1, 2, 4, 5, 5, 5, 5, and 5 seconds
* and terminate the flow at 32 seconds absent ACK progress. */
static uint64_t sp_rto_base_ns(const struct sp_tcp_core *core) {
uint64_t base;
if (core->rtt_samples == 0)
return SP_TCP_RTO_NS;
base = core->srtt_ns + 4 * core->rttvar_ns;
if (base < SP_TCP_RTO_MIN_NS)
base = SP_TCP_RTO_MIN_NS;
return sp_min_u64(base, SP_TCP_RTO_MAX_NS);
}
static uint64_t sp_rto_delay_ns_from(uint64_t base, uint32_t retries) {
uint32_t shift = retries > 6 ? 6 : retries;
if (base >= SP_TCP_RTO_MAX_NS >> shift)
return SP_TCP_RTO_MAX_NS;
return sp_min_u64(base << shift, SP_TCP_RTO_MAX_NS);
}
static uint64_t sp_rto_delay_ns(const struct sp_tcp_core *core,
uint32_t retries) {
return sp_rto_delay_ns_from(sp_rto_base_ns(core), retries);
}
/* Timer firing precedence. When several deadlines for one flow are due at the
* same instant, terminal absolute deadlines run before RTO and persist so no
* extra packet is emitted after expiry:
*
* handshake/connect absolute deadline
* > send/receive/close resource deadline
* > RTO retry-limit terminal transition
* > retransmission or persist probe
* > TIME-WAIT expiry/other maintenance
*
* The RTO timer occupies one rank; whether a given expiry is the retry-limit
* terminal transition or an ordinary retransmission is decided in sp_timer(). */
static unsigned sp_tcp_timer_priority(enum sp_tcp_timer_kind kind) {
switch (kind) {
case SP_TCP_TIMER_HANDSHAKE:
return 1;
case SP_TCP_TIMER_SEND_AGE:
case SP_TCP_TIMER_RECEIVE_AGE:
case SP_TCP_TIMER_CLOSE:
return 2;
case SP_TCP_TIMER_RTO:
return 3;
case SP_TCP_TIMER_PERSIST:
return 4;
case SP_TCP_TIMER_TIME_WAIT:
return 5;
}
return 6;
}
static uint64_t *sp_timer_slot(struct sp_tcp_core *core,
enum sp_tcp_timer_kind kind) {
switch (kind) {
case SP_TCP_TIMER_HANDSHAKE:
return &core->handshake_deadline_ns;
case SP_TCP_TIMER_RTO:
return &core->rto_deadline_ns;
case SP_TCP_TIMER_PERSIST:
return &core->persist_deadline_ns;
case SP_TCP_TIMER_SEND_AGE:
return &core->send_age_deadline_ns;
case SP_TCP_TIMER_RECEIVE_AGE:
return &core->receive_age_deadline_ns;
case SP_TCP_TIMER_CLOSE:
return &core->close_deadline_ns;
case SP_TCP_TIMER_TIME_WAIT:
return &core->timewait_deadline_ns;
}
return NULL;
}
/* Arms or cancels one independent deadline. Every arm takes a fresh
* generation from the flow's monotonic counter, so a cancel/rearm that lands
* on the identical absolute deadline still invalidates the in-flight event. */
static void sp_arm_timer(struct sp_tcp_core *core, enum sp_tcp_timer_kind kind,
uint64_t deadline_ns) {
uint64_t *slot = sp_timer_slot(core, kind);
if (slot == NULL)
return;
*slot = deadline_ns;
core->timer_gen[kind] = deadline_ns != 0 ? ++core->timer_generation : 0;
}
/* True when a fired timer event still refers to the currently armed deadline.
* A zero event generation is accepted on deadline equality alone so producers
* that predate generation stamping stay compatible. */
static int sp_timer_current(const struct sp_tcp_core *core,
enum sp_tcp_timer_kind kind, uint64_t deadline_ns,
uint64_t generation) {
uint64_t armed = *sp_timer_slot((struct sp_tcp_core *)core, kind);
if (armed == 0 || deadline_ns != armed)
return 0;
return generation == 0 || generation == core->timer_gen[kind];
}
/* Resource deadlines. Each bounds how long one direction may hold an
* unfinished obligation. `progress` distinguishes a genuine advance of the
* acknowledged edge, which restarts the deadline, from merely acquiring new
* work, which only arms an idle timer. Without that distinction a sender
* injecting into a black hole would refresh its own deadline forever, and the
* deadline could move later for reasons this invariant forbids. */
static void sp_update_send_age(struct sp_tcp_core *core, uint64_t now_ns,
int progress) {
int outstanding = core->stream_acked < core->stream_injected ||
(core->fin_injected && !core->fin_acked);
if (!outstanding) {
sp_arm_timer(core, SP_TCP_TIMER_SEND_AGE, 0);
return;
}
if (progress || core->send_age_deadline_ns == 0)
sp_arm_timer(core, SP_TCP_TIMER_SEND_AGE, now_ns + SP_TCP_SEND_AGE_NS);
}
static void sp_update_receive_age(struct sp_tcp_core *core, uint64_t now_ns,
int progress) {
int outstanding = core->rx_authorized_end_abs > core->rcv_acked_abs;
if (!outstanding) {
sp_arm_timer(core, SP_TCP_TIMER_RECEIVE_AGE, 0);
return;
}
if (progress || core->receive_age_deadline_ns == 0)
sp_arm_timer(core, SP_TCP_TIMER_RECEIVE_AGE,
now_ns + SP_TCP_RECEIVE_AGE_NS);
}
/* Returns the flow's next deadline, or 0 when no timer is armed. Earliest
* deadline wins; equal deadlines break by the precedence ladder above. This is
* a pure query so the scheduler can recompute the epoll_pwait2 timeout from
* the minimum across flows without reading the clock. */
uint64_t sp_tcp_next_deadline_ns(const struct sp_tcp_core *core,
enum sp_tcp_timer_kind *kind_out) {
static const enum sp_tcp_timer_kind kinds[] = {
SP_TCP_TIMER_HANDSHAKE, SP_TCP_TIMER_SEND_AGE,
SP_TCP_TIMER_RECEIVE_AGE, SP_TCP_TIMER_CLOSE,
SP_TCP_TIMER_RTO, SP_TCP_TIMER_PERSIST,
SP_TCP_TIMER_TIME_WAIT,
};
const uint64_t deadlines[] = {
core->handshake_deadline_ns, core->send_age_deadline_ns,
core->receive_age_deadline_ns, core->close_deadline_ns,
core->rto_deadline_ns, core->persist_deadline_ns,
core->timewait_deadline_ns,
};
uint64_t best = 0;
unsigned best_priority = 0;
size_t i;
for (i = 0; i < sizeof(kinds) / sizeof(kinds[0]); ++i) {
unsigned priority;
if (deadlines[i] == 0)
continue;
priority = sp_tcp_timer_priority(kinds[i]);
if (best != 0 && (deadlines[i] > best ||
(deadlines[i] == best && priority >= best_priority)))
continue;
best = deadlines[i];
best_priority = priority;
if (kind_out != NULL)
*kind_out = kinds[i];
}
return best;
}
/* Nanoseconds remaining until an absolute monotonic deadline, saturating at
* zero. Callers pass the clock in so fake-clock tests can drive every
* boundary; the wait adapter converts the result to the epoll_pwait2 timespec. */
uint64_t sp_tcp_ns_until(uint64_t deadline_ns, uint64_t now_ns) {
if (deadline_ns == 0 || deadline_ns <= now_ns)
return 0;
return deadline_ns - now_ns;
}
uint64_t sp_tcp_unwrap_u32(uint32_t wire, uint64_t checkpoint) {
uint64_t candidate = (checkpoint & ~UINT64_C(0xffffffff)) | wire;
if (candidate + UINT64_C(0x80000000) < checkpoint)
candidate += UINT64_C(0x100000000);
else if (candidate > checkpoint + UINT64_C(0x80000000) &&
candidate >= UINT64_C(0x100000000))
candidate -= UINT64_C(0x100000000);
return candidate;
}
static uint32_t sp_tcp_wire(uint64_t absolute) {
return (uint32_t)absolute;
}
void sp_tcp_core_init(struct sp_tcp_core *core) {
memset(core, 0, sizeof(*core));
core->state = SP_TCP_CORE_CLOSED;
core->next_effect_id = 1;
core->pending_capacity = SP_TCP_CORE_PENDING_CAPACITY;
}
static int sp_effect_reserve(struct sp_tcp_core *core,
struct sp_typed_effect_list *list,
struct sp_typed_effect **effect) {
if (list->count >= SP_TCP_CORE_MAX_EFFECTS ||
core->next_effect_id == UINT64_MAX)
return -1;
*effect = &list->items[list->count++];
memset(*effect, 0, sizeof(**effect));
core->next_effect_id++;
return 0;
}
#define SP_DEFINE_EFFECT_ADD(name_, tag_, member_, product_) \
static int name_(struct sp_tcp_core *core, \
struct sp_typed_effect_list *list, product_ **out) { \
struct sp_typed_effect *item; \
struct sp_typed_effect_context context; \
if (sp_effect_reserve(core, list, &item) < 0) \
return -1; \
context.key = sp_flow_key_make((sp_flow_id){.value = core->flow}, \
(sp_generation_id){.value = \
core->generation}); \
context.effect.value = core->next_effect_id - 1; \
item->type = tag_; \
item->value.member_.context = context; \
*out = &item->value.member_; \
return 0; \
}
SP_DEFINE_EFFECT_ADD(sp_effect_add_open_stream, SP_FX_OPEN_STREAM, open_stream,
struct sp_fx_open_stream)
SP_DEFINE_EFFECT_ADD(sp_effect_add_tcp_emit_batch, SP_FX_TCP_EMIT_BATCH,
tcp_emit_batch, struct sp_fx_tcp_emit_batch)
SP_DEFINE_EFFECT_ADD(sp_effect_add_tcp_emit_control, SP_FX_TCP_EMIT_CONTROL,
tcp_emit_control, struct sp_fx_tcp_emit_control)
SP_DEFINE_EFFECT_ADD(sp_effect_add_stream_send, SP_FX_STREAM_SEND, stream_send,
struct sp_fx_stream_send)
SP_DEFINE_EFFECT_ADD(sp_effect_add_stream_consume, SP_FX_STREAM_CONSUME,
stream_consume, struct sp_fx_stream_consume)
SP_DEFINE_EFFECT_ADD(sp_effect_add_ingress_dispose, SP_FX_INGRESS_DISPOSE,
ingress_dispose, struct sp_fx_ingress_dispose)
SP_DEFINE_EFFECT_ADD(sp_effect_add_stream_shutdown, SP_FX_STREAM_SHUTDOWN,
stream_shutdown, struct sp_fx_stream_shutdown)
SP_DEFINE_EFFECT_ADD(sp_effect_add_close_flow, SP_FX_CLOSE_FLOW, close_flow,
struct sp_fx_close_flow)
#undef SP_DEFINE_EFFECT_ADD
uint16_t sp_tcp_encode_window(const struct sp_tcp_core *core,
uint64_t desired_right,
uint64_t physical_capacity,
uint64_t *encoded_right) {
uint64_t scale;
uint64_t advertisable_capacity;
uint64_t distance;
uint64_t field;
if (core == NULL || encoded_right == NULL || core->rcv_wind_shift > 14 ||
desired_right < core->rcv_acked_abs)
return 0;
scale = core->snd_ws_ok ? UINT64_C(1) << core->rcv_wind_shift : 1;
advertisable_capacity = (physical_capacity / scale) * scale;
distance = desired_right - core->rcv_acked_abs;
field = distance / scale + (distance % scale != 0);
if (field > UINT16_MAX || field * scale > advertisable_capacity)
return 0;
*encoded_right = core->rcv_acked_abs + field * scale;
return (uint16_t)field;
}
static uint16_t sp_current_window(struct sp_tcp_core *core) {
uint64_t scale = core->snd_ws_ok ? UINT64_C(1) << core->rcv_wind_shift : 1;
uint64_t advertisable_capacity =
(core->pending_capacity / scale) * scale;
uint64_t maximum;
uint64_t encoded_right = core->rcv_adv_right_abs;
uint16_t field;
if (UINT64_MAX - core->rcv_acked_abs < advertisable_capacity)
return 0;
maximum = core->rcv_acked_abs + advertisable_capacity;
if (maximum > core->rcv_adv_right_abs)
core->rcv_adv_right_abs = maximum;
field = sp_tcp_encode_window(core, core->rcv_adv_right_abs,
core->pending_capacity, &encoded_right);
if (field != 0 || core->rcv_adv_right_abs == core->rcv_acked_abs)
core->rcv_adv_right_abs = encoded_right;
return field;
}
static int sp_emit_control(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects, uint8_t flags,
uint64_t seq_abs, uint64_t ack_abs, int syn_options) {
struct sp_fx_tcp_emit_control *effect;
if (sp_effect_add_tcp_emit_control(core, effects, &effect) < 0)
return -1;
effect->flags = flags;
effect->seq.value = seq_abs;
effect->ack.value = ack_abs;
if ((flags & SP_TCP_CORE_SYN) != 0) {
uint64_t window = core->pending_capacity;
if (window > UINT16_MAX)
window = UINT16_MAX;
effect->window = (uint16_t)window;
} else {
effect->window = sp_current_window(core);
}
if (syn_options) {
effect->mss = SP_TCP_CORE_MAX_MSS;
effect->ws_present = core->open_role == SP_TCP_OPEN_ACTIVE ||
core->snd_ws_ok;
effect->ws = core->rcv_wind_shift;
}
return 0;
}
static int sp_emit_current_ack(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects) {
return sp_emit_control(core, effects, SP_TCP_CORE_ACK,
core->snd_nxt_high_abs, core->rcv_acked_abs, 0);
}
static int sp_emit_ingress_dispose(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects,
uint64_t ingress_id) {
struct sp_fx_ingress_dispose *effect;
if (ingress_id == 0 || core->ingress_dispose_effect.id != 0)
return 0;
if (sp_effect_add_ingress_dispose(core, effects, &effect) < 0)
return -1;
effect->ingress.value = ingress_id;
core->ingress_dispose_effect.id = effect->context.effect.value;
core->ingress_dispose_effect.ingress_id = ingress_id;
return 0;
}
static int sp_peer_options_apply(struct sp_tcp_core *core,
const struct sp_tcp_peer_options *options,
int passive) {
uint16_t peer_mss;
if (!options->ws_present && options->ws_raw != 0)
return -1;
if (options->mss_present) {
if (options->mss < SP_TCP_CORE_MIN_MSS)
return -1;
peer_mss = options->mss;
} else {
if (options->mss != 0)
return -1;
peer_mss = SP_TCP_CORE_DEFAULT_MSS;
}
core->effective_send_mss =
peer_mss < SP_TCP_CORE_MAX_MSS ? peer_mss : SP_TCP_CORE_MAX_MSS;
if (options->ws_present) {
core->snd_wind_shift = options->ws_raw > 14 ? 14 : options->ws_raw;
core->snd_ws_ok = 1;
if (passive)
core->rcv_wind_shift = SP_TCP_CORE_RCV_WSCALE;
} else {
if (options->ws_raw != 0)
return -1;
core->snd_wind_shift = 0;
core->rcv_wind_shift = 0;
core->snd_ws_ok = 0;
}
return 0;
}
static int sp_admission_valid(const struct sp_ev_flow_admitted *admission) {
if (admission->needs_connect > 1 || admission->key.flow.value == 0 ||
admission->key.generation.value == 0 ||
admission->tuple.local_port == 0 || admission->tuple.peer_port == 0 ||
admission->local_rcv_wscale > 14 ||
admission->local_rcv_wscale != SP_TCP_CORE_RCV_WSCALE ||
admission->policy_role < SP_TCP_ROLE_DIRECT ||
admission->policy_role > SP_TCP_ROLE_PUBLISH ||
admission->open_role < SP_TCP_OPEN_PASSIVE ||
admission->open_role > SP_TCP_OPEN_ACTIVE)
return 0;
if ((admission->policy_role == SP_TCP_ROLE_DIRECT ||
admission->policy_role == SP_TCP_ROLE_SOCKS) &&
admission->needs_connect == 0)
return 0;
if (admission->open_role == SP_TCP_OPEN_PASSIVE)
return admission->passive_syn_present != 0;
return admission->passive_syn_present == 0 && admission->peer_isn == 0 &&
admission->peer_window == 0 &&
admission->peer_mss_present == 0 &&
admission->peer_mss == 0 &&
admission->peer_ws_present == 0 &&
admission->peer_ws == 0;
}
static int sp_open_stream(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects) {
struct sp_fx_open_stream *effect;
if (sp_effect_add_open_stream(core, effects, &effect) < 0)
return -1;
core->connect_effect.id = effect->context.effect.value;
return 0;
}
static int sp_begin_handshake_emit(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects) {
if (core->open_role == SP_TCP_OPEN_PASSIVE) {
core->state = SP_TCP_CORE_SYN_RECEIVED;
core->snd_nxt_high_abs = core->iss_abs + 1;
return sp_emit_control(core, effects,
SP_TCP_CORE_SYN | SP_TCP_CORE_ACK, core->iss_abs,
core->rcv_acked_abs, 1);
}
core->state = SP_TCP_CORE_SYN_SENT;
core->snd_nxt_high_abs = core->iss_abs + 1;
return sp_emit_control(core, effects, SP_TCP_CORE_SYN, core->iss_abs, 0, 1);
}
static enum sp_tcp_core_result
sp_admit(struct sp_tcp_core *core, const struct sp_ev_flow_admitted *admission,
uint64_t now_ns,
struct sp_typed_effect_list *effects) {
if (core->state != SP_TCP_CORE_CLOSED || !sp_admission_valid(admission))
return SP_TCP_CORE_REJECTED;
if (admission->open_role == SP_TCP_OPEN_PASSIVE &&
((admission->peer_mss_present
? admission->peer_mss < SP_TCP_CORE_MIN_MSS
: admission->peer_mss != 0) ||
(!admission->peer_ws_present && admission->peer_ws != 0)))
return SP_TCP_CORE_REJECTED;
core->flow = admission->key.flow.value;
core->generation = admission->key.generation.value;
core->policy_role = admission->policy_role;
core->open_role = admission->open_role;
core->iss_abs = admission->local_iss;
core->data_seq0_abs = core->iss_abs + 1;
core->snd_una_abs = core->iss_abs;
core->snd_nxt_high_abs = core->iss_abs;
core->rcv_wind_shift = admission->local_rcv_wscale;
core->pending_capacity = SP_TCP_CORE_PENDING_CAPACITY;
sp_arm_timer(core, SP_TCP_TIMER_HANDSHAKE,
now_ns + SP_TCP_HANDSHAKE_NS);
if (admission->open_role == SP_TCP_OPEN_PASSIVE) {
if (sp_peer_options_apply(
core, &(struct sp_tcp_peer_options){
.mss = admission->peer_mss,
.mss_present = admission->peer_mss_present,
.ws_present = admission->peer_ws_present,
.ws_raw = admission->peer_ws}, 1) < 0)
return SP_TCP_CORE_REJECTED;
core->irs_abs = admission->peer_isn;
core->rcv_acked_abs = core->irs_abs + 1;
core->rx_pending_end_abs = core->rcv_acked_abs;
core->rx_authorized_end_abs = core->rcv_acked_abs;
core->rcv_adv_right_abs = core->rcv_acked_abs + UINT16_MAX;
core->snd_wnd = admission->peer_window;
}
if (admission->needs_connect != 0) {
core->state = admission->open_role == SP_TCP_OPEN_PASSIVE
? SP_TCP_CORE_CONNECTING_PASSIVE
: SP_TCP_CORE_CONNECTING_ACTIVE;
if (sp_open_stream(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
if (sp_begin_handshake_emit(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
static int sp_effect_matches(const struct sp_tcp_core *core,
struct sp_flow_key key, sp_effect_id effect,
const struct sp_tcp_live_effect *live) {
return key.flow.value == core->flow &&
key.generation.value == core->generation && live->id != 0 &&
effect.value == live->id;
}
static int sp_close_flow(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects) {
struct sp_fx_close_flow *effect;
memset(&core->connect_effect, 0, sizeof(core->connect_effect));
memset(&core->send_caps, 0, sizeof(core->send_caps));
core->send_cap_count = 0;
memset(&core->stream_consume_effect, 0, sizeof(core->stream_consume_effect));
memset(&core->tun_emit_effect, 0, sizeof(core->tun_emit_effect));
memset(&core->ingress_dispose_effect, 0,
sizeof(core->ingress_dispose_effect));
core->state = SP_TCP_CORE_CLOSED;
/* A terminal transition cancels every deadline so no retransmission or
* probe can be emitted after expiry, including one whose timer event was
* already in flight when the flow closed. */
sp_arm_timer(core, SP_TCP_TIMER_HANDSHAKE, 0);
sp_arm_timer(core, SP_TCP_TIMER_RTO, 0);
sp_arm_timer(core, SP_TCP_TIMER_PERSIST, 0);
sp_arm_timer(core, SP_TCP_TIMER_SEND_AGE, 0);
sp_arm_timer(core, SP_TCP_TIMER_RECEIVE_AGE, 0);
sp_arm_timer(core, SP_TCP_TIMER_CLOSE, 0);
sp_arm_timer(core, SP_TCP_TIMER_TIME_WAIT, 0);
if (sp_effect_add_close_flow(core, effects, &effect) < 0)
return -1;
return 0;
}
static int sp_enter_time_wait(struct sp_tcp_core *core, uint64_t now_ns) {
core->state = SP_TCP_CORE_TIME_WAIT;
sp_arm_timer(core, SP_TCP_TIMER_CLOSE, 0);
sp_arm_timer(core, SP_TCP_TIMER_TIME_WAIT, now_ns + SP_TCP_TIME_WAIT_NS);
return 0;
}
static int sp_maybe_emit_stream_consume(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects) {
struct sp_fx_stream_consume *effect;
if (core->stream_consume_effect.id != 0 ||
core->stream_consumed >= core->stream_acked)
return 0;
if (sp_effect_add_stream_consume(core, effects, &effect) < 0)
return -1;
effect->start.value = core->stream_consumed;
effect->end.value = core->stream_acked;
core->consume_target = effect->end.value;
core->stream_consume_effect.id = effect->context.effect.value;
core->stream_consume_effect.start = effect->start.value;
core->stream_consume_effect.end = effect->end.value;
return 0;
}
static int sp_maybe_emit_to_child(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects) {
struct sp_fx_tcp_emit_batch *effect;
uint64_t allowed;
uint64_t length;
if (core->tun_emit_effect.id != 0 || core->tun_segment_credit == 0 ||
(core->state != SP_TCP_CORE_ESTABLISHED &&
core->state != SP_TCP_CORE_CLOSE_WAIT))
return 0;
if (core->stream_injected < core->stream_tail) {
uint64_t flight_end = core->data_seq0_abs + core->stream_injected;
uint64_t right = core->snd_una_abs + core->snd_wnd;
uint64_t segment_credit;
uint64_t batch_cap;
if (right <= flight_end) {
if (core->persist_deadline_ns == 0)
sp_arm_timer(core, SP_TCP_TIMER_PERSIST,
core->last_now_ns +
sp_persist_delay_ns(core->persist_probes));
return 0;
}
allowed = right - flight_end;
length = sp_min_u64(core->stream_tail - core->stream_injected, allowed);
segment_credit = sp_min_u64(core->tun_segment_credit,
SP_TCP_CORE_MAX_SEGMENTS);
batch_cap = segment_credit * core->effective_send_mss;
length = sp_min_u64(length, batch_cap);
if (length == 0)
return 0;
if (sp_effect_add_tcp_emit_batch(core, effects, &effect) < 0)
return -1;
effect->start.value = core->stream_injected;
effect->end.value = core->stream_injected + length;
effect->seq.value = core->data_seq0_abs + effect->start.value;
effect->ack.value = core->rcv_acked_abs;
effect->flags = SP_TCP_CORE_ACK | SP_TCP_CORE_PSH;
effect->window = sp_current_window(core);
effect->mss = core->effective_send_mss;
effect->terminal = core->stream_eof &&
effect->end.value == core->final_stream_length;
if (effect->terminal)
effect->flags |= SP_TCP_CORE_FIN;
effect->attempt = 1;
core->tun_emit_effect.id = effect->context.effect.value;
core->tun_emit_effect.start = effect->start.value;
core->tun_emit_effect.end = effect->end.value;
return 0;
}
if (core->stream_eof && core->final_stream_length == core->stream_injected &&
core->fin_injected == 0) {
if (sp_emit_control(core, effects, SP_TCP_CORE_FIN | SP_TCP_CORE_ACK,
core->data_seq0_abs + core->stream_injected,
core->rcv_acked_abs, 0) < 0)
return -1;
{
struct sp_fx_tcp_emit_control *control =
&effects->items[effects->count - 1].value.tcp_emit_control;
control->terminal = 1;
core->tun_emit_effect.id = control->context.effect.value;
}
core->tun_emit_effect.start = core->stream_injected;
core->tun_emit_effect.end = core->stream_injected;
}
return 0;
}
/* Rebuilds a retransmission batch from the oldest unacknowledged byte, bounded
* by cap_bytes. The RTO path passes a full authorized batch; the bounded
* fast retransmit passes exactly one MSS. Returns 1 when an effect was
* emitted, 0 when there was nothing to retransmit, and -1 on effect overflow. */
static int sp_emit_retransmit(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects,
uint64_t cap_bytes, uint32_t attempt) {
struct sp_fx_tcp_emit_batch *effect;
if (core->tun_emit_effect.id != 0 ||
core->stream_acked >= core->stream_injected)
return 0;
if (sp_effect_add_tcp_emit_batch(core, effects, &effect) < 0)
return -1;
effect->start.value = core->stream_acked;
effect->end.value =
sp_min_u64(core->stream_injected, core->stream_acked + cap_bytes);
effect->seq.value = core->data_seq0_abs + effect->start.value;
effect->ack.value = core->rcv_acked_abs;
effect->flags = SP_TCP_CORE_ACK | SP_TCP_CORE_PSH;
effect->window = sp_current_window(core);
effect->mss = core->effective_send_mss;
effect->terminal =
core->fin_injected && effect->end.value == core->final_stream_length;
if (effect->terminal)
effect->flags |= SP_TCP_CORE_FIN;
effect->attempt = attempt;
core->tun_emit_effect.id = effect->context.effect.value;
core->tun_emit_effect.start = effect->start.value;
core->tun_emit_effect.end = effect->end.value;
core->tun_emit_effect.retransmission = 1;
return 1;
}
static int sp_ack_update(struct sp_tcp_core *core,
const struct sp_ev_tcp_segment *segment,
uint64_t seq_abs,
struct sp_typed_effect_list *effects) {
uint64_t ack_abs;
uint64_t offered_window;
int window_changed;
if ((segment->flags & SP_TCP_CORE_ACK) == 0)
return 0;
ack_abs = sp_tcp_unwrap_u32(segment->ack, core->snd_una_abs);
if (ack_abs > core->snd_nxt_high_abs) {
if (sp_emit_current_ack(core, effects) < 0)
return -1;
return 1;
}
offered_window = segment->window;
if (core->snd_ws_ok)
offered_window <<= core->snd_wind_shift;
window_changed = offered_window != core->snd_wnd;
if (ack_abs >= core->snd_una_abs) {
if (ack_abs > core->snd_una_abs) {
if (core->rtt_sample_end_abs != 0 &&
ack_abs >= core->rtt_sample_end_abs &&
!core->rtt_sample_ambiguous &&
core->last_now_ns >= core->rtt_sample_sent_ns) {
uint64_t sample = core->last_now_ns - core->rtt_sample_sent_ns;
if (core->rtt_samples == 0) {
core->srtt_ns = sample;
core->rttvar_ns = sample / 2;
} else {
uint64_t delta = core->srtt_ns > sample
? core->srtt_ns - sample
: sample - core->srtt_ns;
core->rttvar_ns = (3 * core->rttvar_ns + delta) / 4;
core->srtt_ns = (7 * core->srtt_ns + sample) / 8;
}
core->rtt_samples++;
core->rtt_sample_end_abs = 0;
core->rtt_sample_sent_ns = 0;
core->rtt_sample_ambiguous = 0;
}
core->snd_una_abs = ack_abs;
/* A valid cumulative ACK restarts the RTO for the new oldest
* obligation, but the backed-off interval and consecutive-expiry count
* collapse only once unambiguously transmitted new data beyond the
* recovery point is acknowledged. A partial ACK of retransmitted data
* leaves the backoff history intact. */
if (core->rto_recovery_point_abs == 0 ||
ack_abs >= core->rto_recovery_point_abs) {
core->rto_retries = 0;
core->rto_first_ns = 0;
core->rto_recovery_point_abs = 0;
}
/* Cumulative progress ends the current fast-retransmit episode. */
core->dup_ack_count = 0;
core->dup_ack_ref_abs = ack_abs;
if (core->fast_retransmit_point_abs != 0 &&
ack_abs >= core->fast_retransmit_point_abs) {
core->fast_retransmit_point_abs = 0;
core->fast_retransmit_used = 0;
}
if (ack_abs > core->data_seq0_abs) {
uint64_t data_ack = ack_abs - core->data_seq0_abs;
if (data_ack > core->stream_injected)
data_ack = core->stream_injected;
if (data_ack > core->stream_acked)
core->stream_acked = data_ack;
}
if (core->fin_injected &&
ack_abs >= core->data_seq0_abs + core->stream_injected + 1)
core->fin_acked = 1;
sp_arm_timer(core, SP_TCP_TIMER_RTO,
core->snd_una_abs < core->snd_nxt_high_abs
? core->last_now_ns +
sp_rto_delay_ns(core, core->rto_retries)
: 0);
sp_update_send_age(core, core->last_now_ns, 1);
if (sp_maybe_emit_stream_consume(core, effects) < 0)
return -1;
} else if (segment->payload_length.value == 0 && !window_changed &&
(segment->flags & (SP_TCP_CORE_SYN | SP_TCP_CORE_FIN)) == 0 &&
core->snd_una_abs < core->snd_nxt_high_abs) {
/* Bounded one-shot fast retransmit. Only a pure, window-neutral
* duplicate of the current SND.UNA qualifies; data-bearing duplicates,
* window-only updates, and challenge/unacceptable traffic are excluded
* by the guards above and by the acceptability check upstream. Three
* qualifying duplicates retransmit exactly the oldest MSS once per
* recovery point. The RTO stays armed and no congestion window is
* maintained on the private child link. */
if (core->dup_ack_ref_abs != ack_abs) {
core->dup_ack_ref_abs = ack_abs;
core->dup_ack_count = 0;
}
if (core->dup_ack_count < UINT32_MAX)
core->dup_ack_count++;
if (core->dup_ack_count == SP_TCP_DUP_ACK_THRESHOLD &&
!core->fast_retransmit_used) {
int emitted =
sp_emit_retransmit(core, effects, core->effective_send_mss, 1);
if (emitted < 0)
return -1;
if (emitted == 1) {
core->fast_retransmit_used = 1;
core->fast_retransmit_point_abs = core->snd_nxt_high_abs;
core->rtt_sample_ambiguous = 1;
}
}
}
if (seq_abs > core->snd_wl1_abs ||
(seq_abs == core->snd_wl1_abs && ack_abs >= core->snd_wl2_abs)) {
uint64_t window = segment->window;
if (core->snd_ws_ok)
window <<= core->snd_wind_shift;
core->snd_wnd = window;
core->snd_wl1_abs = seq_abs;
core->snd_wl2_abs = ack_abs;
if (window != 0) {
sp_arm_timer(core, SP_TCP_TIMER_PERSIST, 0);
core->persist_probes = 0;
}
}
}
return 0;
}
static int sp_segment_acceptable(const struct sp_tcp_core *core,
uint64_t seq_abs, uint64_t length) {
if (length == 0) {
if (core->rcv_adv_right_abs == core->rcv_acked_abs)
return seq_abs == core->rcv_acked_abs;
return seq_abs >= core->rcv_acked_abs &&
seq_abs <= core->rcv_adv_right_abs;
}
return seq_abs < core->rcv_adv_right_abs &&
seq_abs + length > core->rcv_acked_abs;
}
/* Append one exact-slice STREAM_SEND capability to the per-flow FIFO. The FIFO
* holds up to K_rx live capabilities tiling [A, Q) in ascending sequence order:
* fresh authorizations are appended at [Q, end) and, once the FIFO is empty, a
* durable-reservation re-flush is appended at [A, P). Returns 1 when the FIFO
* is full, 0 on success, -1 on effect
* overflow. */
static int sp_stream_send_effect(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects,
uint64_t start, uint64_t end,
uint64_t ingress_id) {
struct sp_fx_stream_send *effect;
struct sp_tcp_live_effect *cap;
if (core->send_cap_count >= SP_TCP_CORE_K_RX)
return 1;
if (sp_effect_add_stream_send(core, effects, &effect) < 0)
return -1;
effect->start.value = start;
effect->end.value = end;
effect->capability.value = ingress_id;
cap = &core->send_caps[core->send_cap_count++];
memset(cap, 0, sizeof(*cap));
cap->id = effect->context.effect.value;
cap->start = start;
cap->end = end;
cap->ingress_id = ingress_id;
return 0;
}
static int sp_handle_peer_fin(struct sp_tcp_core *core,
struct sp_typed_effect_list *effects,
uint64_t fin_abs, uint64_t now_ns) {
struct sp_fx_stream_shutdown *effect;
if (fin_abs != core->rcv_acked_abs ||
core->rx_pending_end_abs != core->rcv_acked_abs ||
core->rx_authorized_end_abs != core->rcv_acked_abs) {
core->peer_fin_seen = 1;
core->peer_fin_abs = fin_abs;
return sp_emit_current_ack(core, effects);
}
core->peer_fin_seen = 1;
core->peer_fin_abs = fin_abs;
core->rcv_acked_abs++;
core->rx_pending_end_abs++;
core->rx_authorized_end_abs++;
if (core->rcv_adv_right_abs < core->rcv_acked_abs)
core->rcv_adv_right_abs = core->rcv_acked_abs;
if (sp_emit_current_ack(core, effects) < 0)
return -1;
if (sp_effect_add_stream_shutdown(core, effects, &effect) < 0)
return -1;
if (core->state == SP_TCP_CORE_ESTABLISHED)
core->state = SP_TCP_CORE_CLOSE_WAIT;
else if (core->state == SP_TCP_CORE_FIN_WAIT_1)
core->state = core->fin_acked ? SP_TCP_CORE_TIME_WAIT : SP_TCP_CORE_CLOSING;
else if (core->state == SP_TCP_CORE_FIN_WAIT_2)
sp_enter_time_wait(core, now_ns);
return 0;
}
static enum sp_tcp_core_result
sp_handle_segment(struct sp_tcp_core *core,
const struct sp_ev_tcp_segment *segment, uint64_t now_ns,
struct sp_typed_effect_list *effects) {
uint64_t seq_abs;
uint64_t sequence_len = segment->payload_length.value;
uint64_t data_start;
uint64_t data_end;
int ack_result;
if ((segment->flags & SP_TCP_CORE_SYN) != 0)
sequence_len++;
if ((segment->flags & SP_TCP_CORE_FIN) != 0)
sequence_len++;
if (core->state == SP_TCP_CORE_SYN_RECEIVED) {
if ((segment->flags & SP_TCP_CORE_RST) != 0) {
if (sp_close_flow(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
if ((segment->flags & SP_TCP_CORE_SYN) != 0 &&
segment->seq == sp_tcp_wire(core->irs_abs)) {
if (sp_emit_control(core, effects,
SP_TCP_CORE_SYN | SP_TCP_CORE_ACK, core->iss_abs,
core->rcv_acked_abs, 1) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
if ((segment->flags & SP_TCP_CORE_ACK) != 0 &&
segment->ack == sp_tcp_wire(core->iss_abs + 1) &&
segment->seq == sp_tcp_wire(core->rcv_acked_abs)) {
core->snd_una_abs = core->iss_abs + 1;
core->snd_wl1_abs = core->rcv_acked_abs;
core->snd_wl2_abs = core->snd_una_abs;
core->snd_wnd = (uint64_t)segment->window << core->snd_wind_shift;
core->state = SP_TCP_CORE_ESTABLISHED;
sp_arm_timer(core, SP_TCP_TIMER_HANDSHAKE, 0);
return SP_TCP_CORE_OK;
}
return SP_TCP_CORE_REJECTED;
}
if (core->state == SP_TCP_CORE_SYN_SENT) {
if ((segment->flags & (SP_TCP_CORE_SYN | SP_TCP_CORE_ACK)) ==
(SP_TCP_CORE_SYN | SP_TCP_CORE_ACK) &&
segment->ack == sp_tcp_wire(core->iss_abs + 1) &&
sp_peer_options_apply(
core, &(struct sp_tcp_peer_options){
.mss = segment->mss,
.mss_present = segment->mss_present,
.ws_present = segment->ws_present,
.ws_raw = segment->ws}, 0) == 0) {
core->irs_abs = segment->seq;
core->rcv_acked_abs = core->irs_abs + 1;
core->rx_pending_end_abs = core->rcv_acked_abs;
core->rx_authorized_end_abs = core->rcv_acked_abs;
core->rcv_adv_right_abs = core->rcv_acked_abs + UINT16_MAX;
core->snd_una_abs = core->iss_abs + 1;
core->snd_wnd = segment->window;
core->state = SP_TCP_CORE_ESTABLISHED;
sp_arm_timer(core, SP_TCP_TIMER_HANDSHAKE, 0);
if (sp_emit_current_ack(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
if ((segment->flags & SP_TCP_CORE_SYN) != 0 &&
(segment->flags & SP_TCP_CORE_ACK) == 0) {
if (sp_emit_control(core, effects, SP_TCP_CORE_RST, core->iss_abs, 0, 0) <
0 ||
sp_close_flow(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
return SP_TCP_CORE_REJECTED;
}
if (core->state < SP_TCP_CORE_ESTABLISHED ||
core->state > SP_TCP_CORE_TIME_WAIT)
return SP_TCP_CORE_REJECTED;
seq_abs = sp_tcp_unwrap_u32(segment->seq, core->rcv_acked_abs);
if (!sp_segment_acceptable(core, seq_abs, sequence_len)) {
if ((segment->flags & SP_TCP_CORE_RST) == 0 &&
sp_emit_current_ack(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (sp_emit_ingress_dispose(core, effects, segment->ingress.value) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
if ((segment->flags & SP_TCP_CORE_RST) != 0) {
if (seq_abs == core->rcv_acked_abs) {
if (sp_close_flow(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
} else if (sp_emit_current_ack(core, effects) < 0) {
return SP_TCP_CORE_EFFECT_OVERFLOW;
}
return SP_TCP_CORE_OK;
}
if ((segment->flags & SP_TCP_CORE_SYN) != 0) {
if (sp_emit_current_ack(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
ack_result = sp_ack_update(core, segment, seq_abs, effects);
if (ack_result < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (ack_result > 0)
return SP_TCP_CORE_OK;
data_start = seq_abs;
data_end = seq_abs + segment->payload_length.value;
if (segment->payload_length.value != 0) {
if (data_end <= core->rx_authorized_end_abs) {
if (sp_emit_current_ack(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
} else if (data_start > core->rx_authorized_end_abs) {
if (sp_emit_current_ack(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
} else if (core->send_cap_count < SP_TCP_CORE_K_RX) {
if (data_start < core->rx_authorized_end_abs)
data_start = core->rx_authorized_end_abs;
if (data_end > core->rcv_adv_right_abs)
data_end = core->rcv_adv_right_abs;
if (data_end > data_start) {
core->rx_authorized_end_abs = data_end;
sp_update_receive_age(core, now_ns, 0);
if (sp_stream_send_effect(core, effects, data_start, data_end,
segment->ingress.value) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
}
}
}
if ((segment->flags & SP_TCP_CORE_FIN) != 0) {
uint64_t fin_abs = seq_abs + segment->payload_length.value;
if (fin_abs < core->rcv_adv_right_abs &&
sp_handle_peer_fin(core, effects, fin_abs, now_ns) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
}
if (segment->payload_length.value == 0 &&
(segment->flags & SP_TCP_CORE_FIN) == 0 &&
sp_emit_ingress_dispose(core, effects, segment->ingress.value) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (core->fin_acked) {
if (core->state == SP_TCP_CORE_FIN_WAIT_1)
core->state = SP_TCP_CORE_FIN_WAIT_2;
else if (core->state == SP_TCP_CORE_CLOSING)
sp_enter_time_wait(core, now_ns);
else if (core->state == SP_TCP_CORE_LAST_ACK &&
sp_close_flow(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
}
if (sp_maybe_emit_to_child(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
static enum sp_tcp_core_result
sp_connect_result(struct sp_tcp_core *core,
const struct sp_ev_connect_result *input,
struct sp_typed_effect_list *effects) {
if (!sp_effect_matches(core, input->context.key, input->effect,
&core->connect_effect))
return SP_TCP_CORE_OK;
memset(&core->connect_effect, 0, sizeof(core->connect_effect));
if (input->outcome == SP_CONNECT_FAILED) {
if (core->open_role == SP_TCP_OPEN_PASSIVE &&
sp_emit_control(core, effects, SP_TCP_CORE_RST, core->iss_abs, 0, 0) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (sp_close_flow(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
if (sp_begin_handshake_emit(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
static enum sp_tcp_core_result
sp_stream_send_result(struct sp_tcp_core *core,
const struct sp_ev_stream_send_result *input,
struct sp_typed_effect_list *effects) {
struct sp_tcp_live_effect *head;
uint64_t length;
uint64_t accepted;
uint64_t retained;
uint64_t ingress_id;
/* Completions linearize FIFO: only the oldest capability (the head, whose
* start is the ACK frontier A for a re-flush or the durable frontier P for a
* fresh slice) may commit. The executor processes a flow's capabilities in
* strict order, so a result naming a still-live non-head capability is an
* ordering violation and fails loud; any id no longer in the FIFO is a stale
* completion and is ignored. */
if (input->context.key.flow.value != core->flow ||
input->context.key.generation.value != core->generation ||
core->send_cap_count == 0)
return SP_TCP_CORE_OK;
head = &core->send_caps[0];
if (input->effect.value != head->id) {
for (unsigned i = 1; i < core->send_cap_count; ++i)
if (core->send_caps[i].id == input->effect.value)
return SP_TCP_CORE_BAD_EVENT;
return SP_TCP_CORE_OK;
}
length = head->end - head->start;
accepted = input->accepted.value;
retained = input->retained.value;
/* accepted may advance A only through a prefix contiguous with A; the executor
* never accepts bytes past a durable gap, so accepted > 0 requires the head to
* start exactly at A. The whole slice must be accounted (accepted+retained). */
if (input->outcome == SP_SEND_FAILED || accepted > length ||
retained > length - accepted || accepted + retained != length ||
(accepted != 0 && head->start != core->rcv_acked_abs))
return SP_TCP_CORE_BAD_EVENT;
ingress_id = head->ingress_id;
core->rcv_acked_abs += accepted;
core->rx_pending_end_abs = head->start + accepted + retained;
sp_update_receive_age(core, input->context.now.value, accepted != 0);
core->send_cap_count--;
if (core->send_cap_count > 0)
memmove(&core->send_caps[0], &core->send_caps[1],
(size_t)core->send_cap_count * sizeof(core->send_caps[0]));
memset(&core->send_caps[core->send_cap_count], 0,
sizeof(core->send_caps[0]));
if (core->send_cap_count == 0)
core->rx_authorized_end_abs = core->rx_pending_end_abs;
if (accepted != 0 && sp_emit_current_ack(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (ingress_id != 0 &&
sp_emit_ingress_dispose(core, effects, ingress_id) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (core->peer_fin_seen && core->peer_fin_abs == core->rcv_acked_abs &&
core->rx_pending_end_abs == core->rcv_acked_abs &&
sp_handle_peer_fin(core, effects, core->peer_fin_abs,
input->context.now.value) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
static enum sp_tcp_core_result
sp_stream_consumed(struct sp_tcp_core *core,
const struct sp_ev_stream_consumed *input,
struct sp_typed_effect_list *effects) {
uint64_t length;
uint64_t accepted;
if (!sp_effect_matches(core, input->context.key, input->effect,
&core->stream_consume_effect))
return SP_TCP_CORE_OK;
length = core->stream_consume_effect.end - core->stream_consume_effect.start;
accepted = input->accepted.value;
if (accepted > length ||
core->stream_consume_effect.start != core->stream_consumed)
return SP_TCP_CORE_BAD_EVENT;
if (accepted == 0) {
if (sp_close_flow(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
core->stream_consumed += accepted;
core->consume_target = core->stream_consumed;
memset(&core->stream_consume_effect, 0, sizeof(core->stream_consume_effect));
if (sp_maybe_emit_stream_consume(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
static enum sp_tcp_core_result
sp_tun_emit_result(struct sp_tcp_core *core,
const struct sp_ev_tun_emit_result *input,
struct sp_typed_effect_list *effects) {
uint64_t length;
uint64_t accepted;
uint64_t segments;
if (!sp_effect_matches(core, input->context.key, input->effect,
&core->tun_emit_effect))
return SP_TCP_CORE_OK;
length = core->tun_emit_effect.end - core->tun_emit_effect.start;
accepted = input->accepted.value;
if (input->outcome == SP_EMIT_FAILED || accepted > length ||
(length != 0 && accepted == 0) ||
(accepted < length &&
accepted % core->effective_send_mss != 0))
return SP_TCP_CORE_BAD_EVENT;
if (core->tun_emit_effect.retransmission) {
memset(&core->tun_emit_effect, 0, sizeof(core->tun_emit_effect));
return SP_TCP_CORE_OK;
}
if (core->tun_emit_effect.start != core->stream_injected)
return SP_TCP_CORE_BAD_EVENT;
if (length != 0 &&
(accepted + core->effective_send_mss - 1) / core->effective_send_mss >
core->tun_segment_credit)
return SP_TCP_CORE_BAD_EVENT;
if (length == 0) {
core->fin_injected = 1;
core->snd_nxt_high_abs++;
core->state = core->state == SP_TCP_CORE_CLOSE_WAIT
? SP_TCP_CORE_LAST_ACK
: SP_TCP_CORE_FIN_WAIT_1;
} else {
segments = (accepted + core->effective_send_mss - 1) /
core->effective_send_mss;
core->stream_injected += accepted;
if (accepted == length && core->stream_eof &&
core->stream_injected == core->final_stream_length) {
core->fin_injected = 1;
core->state = core->state == SP_TCP_CORE_CLOSE_WAIT
? SP_TCP_CORE_LAST_ACK
: SP_TCP_CORE_FIN_WAIT_1;
}
core->snd_nxt_high_abs = core->data_seq0_abs + core->stream_injected +
(core->fin_injected ? 1 : 0);
core->tun_segment_credit -= segments;
sp_arm_timer(core, SP_TCP_TIMER_RTO,
input->context.now.value +
sp_rto_delay_ns(core, core->rto_retries));
sp_update_send_age(core, input->context.now.value, 0);
if (core->rtt_sample_end_abs == 0) {
core->rtt_sample_end_abs = core->data_seq0_abs + core->stream_injected;
core->rtt_sample_sent_ns = input->context.now.value;
core->rtt_sample_ambiguous = 0;
}
}
memset(&core->tun_emit_effect, 0, sizeof(core->tun_emit_effect));
if (sp_maybe_emit_to_child(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
static enum sp_tcp_core_result
sp_credit(struct sp_tcp_core *core, const struct sp_ev_credit *input,
struct sp_typed_effect_list *effects) {
uint64_t requested_capacity = input->pending_capacity.value;
if (input->stream_tail.value < core->stream_tail ||
input->stream_tail.value - core->stream_consumed > SP_TCP_CORE_PENDING_CAPACITY ||
(core->stream_eof && input->stream_tail.value != core->final_stream_length) ||
input->stream_tail.value > UINT64_MAX - core->data_seq0_abs - 1 ||
requested_capacity > SP_TCP_CORE_PENDING_CAPACITY)
return SP_TCP_CORE_BAD_EVENT;
if (requested_capacity != 0) {
uint64_t scale =
core->snd_ws_ok ? UINT64_C(1) << core->rcv_wind_shift : 1;
uint64_t advertisable_capacity =
(requested_capacity / scale) * scale;
if (core->rcv_adv_right_abs < core->rcv_acked_abs ||
core->rcv_adv_right_abs - core->rcv_acked_abs >
advertisable_capacity)
return SP_TCP_CORE_BAD_EVENT;
}
core->stream_tail = input->stream_tail.value;
if (requested_capacity != 0)
core->pending_capacity = requested_capacity;
if (UINT64_MAX - core->tun_segment_credit < input->tun_segments.value)
core->tun_segment_credit = UINT64_MAX;
else
core->tun_segment_credit += input->tun_segments.value;
if (core->rx_pending_end_abs > core->rcv_acked_abs &&
core->send_cap_count == 0 &&
sp_stream_send_effect(core, effects, core->rcv_acked_abs,
core->rx_pending_end_abs, 0) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (sp_maybe_emit_to_child(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
static enum sp_tcp_core_result
sp_stream_eof(struct sp_tcp_core *core, const struct sp_ev_stream_eof *input,
struct sp_typed_effect_list *effects) {
if (input->stream_tail.value < core->stream_tail || core->stream_eof ||
input->stream_tail.value - core->stream_consumed > SP_TCP_CORE_PENDING_CAPACITY ||
input->stream_tail.value > UINT64_MAX - core->data_seq0_abs - 1)
return SP_TCP_CORE_BAD_EVENT;
core->stream_tail = input->stream_tail.value;
core->final_stream_length = core->stream_tail;
core->stream_eof = 1;
sp_arm_timer(core, SP_TCP_TIMER_CLOSE,
input->context.now.value + SP_TCP_CLOSE_NS);
if (sp_maybe_emit_to_child(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
static enum sp_tcp_core_result
sp_timer(struct sp_tcp_core *core, const struct sp_ev_timer_fired *input,
struct sp_typed_effect_list *effects) {
if (input->deadline.value > input->context.now.value)
return SP_TCP_CORE_OK;
switch (input->kind) {
case SP_TCP_TIMER_HANDSHAKE:
case SP_TCP_TIMER_SEND_AGE:
case SP_TCP_TIMER_RECEIVE_AGE:
case SP_TCP_TIMER_CLOSE:
case SP_TCP_TIMER_TIME_WAIT:
if (!sp_timer_current(core, input->kind, input->deadline.value,
input->timer_generation.value))
return SP_TCP_CORE_OK;
if (sp_close_flow(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
case SP_TCP_TIMER_RTO:
if (!sp_timer_current(core, input->kind, input->deadline.value,
input->timer_generation.value))
return SP_TCP_CORE_OK;
if (core->rto_first_ns == 0)
core->rto_first_ns = input->context.now.value;
/* The recovery policy permits at most eight RTO expiries for one unadvanced
* SND.UNA and
* closes on the eighth. rto_retries counts expiries already processed, so
* this expiry is the (rto_retries + 1)th. */
if (core->rto_retries + 1 >= SP_TCP_CORE_MAX_RTO_RETRIES ||
input->context.now.value - core->rto_first_ns >=
SP_TCP_RTO_MAX_AGE_NS) {
if (sp_close_flow(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
core->rto_retries++;
/* The recovery point is the highest sequence transmitted when loss
* was first detected in this episode. Backoff history survives until an
* ACK passes it. */
if (core->rto_recovery_point_abs == 0)
core->rto_recovery_point_abs = core->snd_nxt_high_abs;
core->rtt_sample_ambiguous = 1;
sp_arm_timer(core, SP_TCP_TIMER_RTO,
input->context.now.value +
sp_rto_delay_ns(core, core->rto_retries));
{
int emitted = sp_emit_retransmit(
core, effects,
(uint64_t)SP_TCP_CORE_MAX_SEGMENTS * core->effective_send_mss,
core->rto_retries + 1);
if (emitted < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (emitted == 1)
return SP_TCP_CORE_OK;
}
if (core->tun_emit_effect.id == 0 && core->fin_injected &&
!core->fin_acked) {
struct sp_fx_tcp_emit_control *effect;
if (sp_emit_control(core, effects, SP_TCP_CORE_FIN | SP_TCP_CORE_ACK,
core->data_seq0_abs + core->stream_injected,
core->rcv_acked_abs, 0) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
effect = &effects->items[effects->count - 1].value.tcp_emit_control;
effect->terminal = 1;
effect->attempt = core->rto_retries + 1;
core->tun_emit_effect.id = effect->context.effect.value;
core->tun_emit_effect.start = core->stream_injected;
core->tun_emit_effect.end = core->stream_injected;
core->tun_emit_effect.retransmission = 1;
}
return SP_TCP_CORE_OK;
case SP_TCP_TIMER_PERSIST:
if (!sp_timer_current(core, input->kind, input->deadline.value,
input->timer_generation.value))
return SP_TCP_CORE_OK;
if (core->persist_probes < UINT32_MAX)
core->persist_probes++;
sp_arm_timer(core, SP_TCP_TIMER_PERSIST,
input->context.now.value +
sp_persist_delay_ns(core->persist_probes));
if (sp_emit_current_ack(core, effects) < 0)
return SP_TCP_CORE_EFFECT_OVERFLOW;
return SP_TCP_CORE_OK;
}
return SP_TCP_CORE_BAD_EVENT;
}
int sp_tcp_assert_invariants(const struct sp_tcp_core *core) {
const struct sp_tcp_live_effect *live[] = {
&core->connect_effect,
&core->stream_consume_effect,
&core->tun_emit_effect,
&core->ingress_dispose_effect,
};
if (core->send_cap_count > SP_TCP_CORE_K_RX)
return 0;
if (core->state == SP_TCP_CORE_CLOSED) {
if (core->send_cap_count != 0)
return 0;
for (size_t i = 0; i < sizeof(live) / sizeof(live[0]); ++i)
if (live[i]->id != 0)
return 0;
return 1;
}
if (core->flow == 0 || core->generation == 0 ||
core->policy_role < SP_TCP_ROLE_DIRECT ||
core->policy_role > SP_TCP_ROLE_PUBLISH ||
core->open_role < SP_TCP_OPEN_PASSIVE ||
core->open_role > SP_TCP_OPEN_ACTIVE || core->snd_wind_shift > 14 ||
core->rcv_wind_shift > 14)
return 0;
if (core->state >= SP_TCP_CORE_SYN_RECEIVED &&
core->state != SP_TCP_CORE_SYN_SENT &&
(core->effective_send_mss < SP_TCP_CORE_MIN_MSS ||
core->effective_send_mss > SP_TCP_CORE_MAX_MSS))
return 0;
if (!(core->stream_consumed <= core->consume_target &&
core->consume_target <= core->stream_acked &&
core->stream_acked <= core->stream_injected &&
core->stream_injected <= core->stream_tail))
return 0;
if (core->stream_tail - core->stream_consumed >
SP_TCP_CORE_PENDING_CAPACITY)
return 0;
if ((core->stream_eof && core->final_stream_length != core->stream_tail) ||
(!core->stream_eof && core->final_stream_length != 0))
return 0;
if (!(core->rcv_acked_abs <= core->rx_pending_end_abs &&
core->rx_pending_end_abs <= core->rx_authorized_end_abs &&
core->rx_authorized_end_abs <= core->rcv_adv_right_abs))
return 0;
{
uint64_t scale =
core->snd_ws_ok ? UINT64_C(1) << core->rcv_wind_shift : 1;
uint64_t advertisable_capacity =
(core->pending_capacity / scale) * scale;
if (advertisable_capacity == 0 ||
UINT64_MAX - core->rcv_acked_abs < advertisable_capacity ||
core->rcv_adv_right_abs >
core->rcv_acked_abs + advertisable_capacity)
return 0;
}
if (core->snd_una_abs > core->snd_nxt_high_abs)
return 0;
if (core->state >= SP_TCP_CORE_ESTABLISHED &&
core->snd_nxt_high_abs !=
core->data_seq0_abs + core->stream_injected +
(core->fin_injected ? 1U : 0U))
return 0;
if (core->fin_acked && !core->fin_injected)
return 0;
/* The STREAM_SEND capability FIFO tiles [caps[0].start, Q) contiguously in
* ascending sequence, with A <= caps[0].start <= P and caps[last].end == Q;
* an empty FIFO means no authorization outstanding (Q == P). */
if (core->send_cap_count == 0) {
if (core->rx_authorized_end_abs != core->rx_pending_end_abs)
return 0;
} else {
const struct sp_tcp_live_effect *first = &core->send_caps[0];
const struct sp_tcp_live_effect *last =
&core->send_caps[core->send_cap_count - 1];
if (first->start < core->rcv_acked_abs ||
first->start > core->rx_pending_end_abs ||
last->end != core->rx_authorized_end_abs)
return 0;
for (unsigned i = 0; i < core->send_cap_count; ++i) {
const struct sp_tcp_live_effect *cap = &core->send_caps[i];
if (cap->id == 0 || cap->start >= cap->end)
return 0;
if (i + 1 < core->send_cap_count &&
cap->end != core->send_caps[i + 1].start)
return 0;
}
}
/* Live-effect and capability ids are pairwise distinct. */
for (size_t i = 0; i < sizeof(live) / sizeof(live[0]); ++i) {
if (live[i]->id == 0)
continue;
for (size_t j = i + 1; j < sizeof(live) / sizeof(live[0]); ++j)
if (live[i]->id == live[j]->id)
return 0;
for (unsigned k = 0; k < core->send_cap_count; ++k)
if (live[i]->id == core->send_caps[k].id)
return 0;
}
for (unsigned i = 0; i < core->send_cap_count; ++i)
for (unsigned j = i + 1; j < core->send_cap_count; ++j)
if (core->send_caps[i].id == core->send_caps[j].id)
return 0;
return 1;
}
SP_TEST_API static enum sp_construct_result
sp_typed_effect_construct_check(const struct sp_typed_effect *effect) {
const struct sp_typed_effect_context *context =
sp_typed_effect_context(effect);
if (context == NULL || context->effect.value == 0 ||
context->key.flow.value == 0 || context->key.generation.value == 0)
return SP_CONSTRUCT_INTERNAL_INVARIANT;
switch (effect->type) {
case SP_FX_OPEN_STREAM:
return SP_CONSTRUCT_OK;
case SP_FX_TCP_EMIT_BATCH: {
const struct sp_fx_tcp_emit_batch *v = &effect->value.tcp_emit_batch;
return v->end.value >= v->start.value && v->terminal <= 1
? SP_CONSTRUCT_OK
: SP_CONSTRUCT_INTERNAL_INVARIANT;
}
case SP_FX_TCP_EMIT_CONTROL: {
const struct sp_fx_tcp_emit_control *v = &effect->value.tcp_emit_control;
return v->ws_present <= 1 && v->ws <= 14 && v->terminal <= 1
? SP_CONSTRUCT_OK
: SP_CONSTRUCT_INTERNAL_INVARIANT;
}
case SP_FX_STREAM_SEND:
return effect->value.stream_send.end.value >=
effect->value.stream_send.start.value
? SP_CONSTRUCT_OK
: SP_CONSTRUCT_INTERNAL_INVARIANT;
case SP_FX_STREAM_CONSUME:
return effect->value.stream_consume.end.value >=
effect->value.stream_consume.start.value
? SP_CONSTRUCT_OK
: SP_CONSTRUCT_INTERNAL_INVARIANT;
case SP_FX_INGRESS_DISPOSE:
return effect->value.ingress_dispose.ingress.value != 0
? SP_CONSTRUCT_OK
: SP_CONSTRUCT_INTERNAL_INVARIANT;
case SP_FX_STREAM_SHUTDOWN:
case SP_FX_CLOSE_FLOW:
return SP_CONSTRUCT_OK;
}
return SP_CONSTRUCT_INTERNAL_INVARIANT;
}
/* Family-specific pure-core entry points are the semantic API. Every caller
* supplies one exact event product and receives exact effect products. */
/* Family handlers reject invalid inputs before semantic mutation. The only
* tentative common metadata is the injected time and legacy fixture sequence.
* No whole-flow snapshot is taken on the packet path. */
struct sp_core_transaction {
uint64_t now;
uint64_t control_sequence;
};
static enum sp_tcp_core_result sp_core_transaction_begin(
struct sp_tcp_core *core, const struct sp_typed_event_context *context,
struct sp_typed_effect_list *effects, struct sp_core_transaction *transaction,
int *apply) {
*apply = 0;
if (context->key.flow.value == 0 ||
context->key.generation.value == 0)
return SP_TCP_CORE_BAD_EVENT;
transaction->now = core->last_now_ns;
transaction->control_sequence = core->applied_control_seq;
/* Only items below count are output; sp_effect_reserve() clears each slot
* as it is produced, so unused slots are never written (O9). */
effects->count = 0;
if (context->key.flow.value != core->flow ||
context->key.generation.value != core->generation)
return SP_TCP_CORE_OK;
if (context->now.value < core->last_now_ns)
return SP_TCP_CORE_REJECTED;
/* At most eight effects can be produced by a typed turn: ACK/consume,
* send, peer-FIN ACK/shutdown, disposal/close and one new emit. Reserve
* the complete fixed output batch and ID range before any mutation. */
if (core->next_effect_id > UINT64_MAX - SP_TCP_CORE_MAX_EFFECTS)
return SP_TCP_CORE_EFFECT_OVERFLOW;
if (context->causal_fence.value > core->applied_control_seq)
return SP_TCP_CORE_BAD_EVENT;
if (context->control_sequence.value != 0) {
if (core->applied_control_seq == UINT64_MAX ||
context->control_sequence.value != core->applied_control_seq + 1)
return SP_TCP_CORE_BAD_EVENT;
core->applied_control_seq = context->control_sequence.value;
}
core->last_now_ns = context->now.value;
*apply = 1;
return SP_TCP_CORE_OK;
}
static enum sp_tcp_core_result sp_core_transaction_finish(
struct sp_tcp_core *core, struct sp_typed_effect_list *effects,
const struct sp_core_transaction *transaction,
enum sp_tcp_core_result result) {
if (result != SP_TCP_CORE_OK) {
core->last_now_ns = transaction->now;
core->applied_control_seq = transaction->control_sequence;
effects->count = 0;
}
#ifdef SOCKPUPPET_CORE_CHECKS
assert(sp_tcp_assert_invariants(core));
for (size_t i = 0; i < effects->count; ++i)
assert(sp_typed_effect_construct_check(&effects->items[i]) == SP_CONSTRUCT_OK);
#endif
return result;
}
enum sp_tcp_core_result
sp_tcp_core_admit(struct sp_tcp_core *core,
const struct sp_ev_flow_admitted *input,
struct sp_typed_effect_list *effects) {
{
struct sp_core_transaction transaction = {
.now = core->last_now_ns, .control_sequence = core->applied_control_seq};
enum sp_tcp_core_result result;
effects->count = 0; /* see sp_core_transaction_begin() */
if (core->next_effect_id > UINT64_MAX - SP_TCP_CORE_MAX_EFFECTS)
return SP_TCP_CORE_EFFECT_OVERFLOW;
core->last_now_ns = input->now.value;
result = sp_admit(core, input, input->now.value, effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_segment(struct sp_tcp_core *core,
const struct sp_ev_tcp_segment *input,
struct sp_typed_effect_list *effects) {
if (input->payload_length.value > UINT32_MAX)
return SP_TCP_CORE_BAD_EVENT;
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
result = sp_handle_segment(core, input, input->context.now.value,
effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_connect_result(struct sp_tcp_core *core,
const struct sp_ev_connect_result *input,
struct sp_typed_effect_list *effects) {
if (input->outcome != SP_CONNECT_SUCCEEDED && input->outcome != SP_CONNECT_FAILED)
return SP_TCP_CORE_BAD_EVENT;
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
result = sp_connect_result(core, input, effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_stream_send_result(struct sp_tcp_core *core,
const struct sp_ev_stream_send_result *input,
struct sp_typed_effect_list *effects) {
if (input->outcome < SP_SEND_PROGRESS || input->outcome > SP_SEND_FAILED)
return SP_TCP_CORE_BAD_EVENT;
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
result = sp_stream_send_result(core, input, effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_stream_consumed(struct sp_tcp_core *core,
const struct sp_ev_stream_consumed *input,
struct sp_typed_effect_list *effects) {
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
result = sp_stream_consumed(core, input, effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_stream_eof(struct sp_tcp_core *core,
const struct sp_ev_stream_eof *input,
struct sp_typed_effect_list *effects) {
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
result = sp_stream_eof(core, input, effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_tun_emit_result(struct sp_tcp_core *core,
const struct sp_ev_tun_emit_result *input,
struct sp_typed_effect_list *effects) {
if (input->outcome < SP_EMIT_ACCEPTED || input->outcome > SP_EMIT_FAILED)
return SP_TCP_CORE_BAD_EVENT;
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
result = sp_tun_emit_result(core, input, effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_credit(struct sp_tcp_core *core, const struct sp_ev_credit *input,
struct sp_typed_effect_list *effects) {
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
result = sp_credit(core, input, effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_timer_fired(struct sp_tcp_core *core,
const struct sp_ev_timer_fired *input,
struct sp_typed_effect_list *effects) {
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
result = sp_timer(core, input, effects);
return sp_core_transaction_finish(core, effects, &transaction, result);
}
}
enum sp_tcp_core_result
sp_tcp_core_ingress_disposed(struct sp_tcp_core *core,
const struct sp_ev_ingress_disposed *input,
struct sp_typed_effect_list *effects) {
{
struct sp_core_transaction transaction;
enum sp_tcp_core_result result;
int apply;
result = sp_core_transaction_begin(core, &input->context, effects,
&transaction, &apply);
if (!apply)
return result;
if (sp_effect_matches(core, input->context.key, input->effect,
&core->ingress_dispose_effect))
memset(&core->ingress_dispose_effect, 0,
sizeof(core->ingress_dispose_effect));
return sp_core_transaction_finish(core, effects, &transaction,
SP_TCP_CORE_OK);
}
}
#ifndef SOCKPUPPET_TCP_CORE_ONLY
#ifndef AF_ALG
#define AF_ALG 38
#endif
#define MAX_TCP 128
#define MAX_UDP 64
#define MAX_PUBLISH_RULES 32
#define MAX_PUBLISH_UDP 128
#define MAX_EVENTS 64
#define EPOLL_TIMEOUT_MS 100
/* After the broker reaps its child, payload output still in flight gets this
* long (CLOCK_MONOTONIC) to reach its destinations; then the remaining
* relays are abandoned. */
#define SP_CHILD_EXIT_DRAIN_NS (5ULL * 1000000000ULL)
#define TCP_PENDING_WRITE_CAP 262144
#define SP_INGRESS_RESERVATION_SLOT_BYTES TCP_PENDING_WRITE_CAP
#define SP_INGRESS_RESERVATION_SLOTS 16
#define SP_INGRESS_PUBLISH_RESERVATION_SLOTS 8
/* Overridable only to let the retention regression test shrink the FIFO
* below a single flow's flight-window frame count (which the production size of
* 32 exceeds, making the FIFO-full path otherwise unreachable by one flow). */
#ifndef TUN_QUEUE_PACKETS
#define TUN_QUEUE_PACKETS 32
#endif
#define TUN_QUEUE_PACKET_MAX SP_TUN_FRAME_MAX_BYTES
#define RELAY_QUEUE_CAP 262144
#define RELAY_DRAIN_BUDGET 65536
#define RELAY_BATCH_BUDGET 262144
#define PUBLISH_SYNTH_PORT_MIN 32768U
#define PUBLISH_SYNTH_PORT_MAX 60999U
#define SP_BROKER_MEMORY_MAX_BYTES ((size_t)402653184)
#define SP_RESOURCE_MEMORY_MARGIN_BYTES ((size_t)(32 * 1024 * 1024))
#define RLIMIT_PARENT_NOFILE 4096
#define RLIMIT_CHILD_NOFILE 1024
/* Resource geometry is frozen before packet representation and TCP state
* transitions are selected. Configuration may select smaller active limits,
* but these compile-time ceilings remain authority and allocation boundaries. */
enum sp_resource_limit {
SP_VNET_HDR_BYTES = 10,
SP_TUN_BODY_MAX_BYTES = 65535,
SP_TUN_FRAME_MAX_BYTES = SP_VNET_HDR_BYTES + SP_TUN_BODY_MAX_BYTES,
SP_IPV4_HEADER_BYTES = 20,
SP_UDP_HEADER_BYTES = 8,
SP_UDP_PAYLOAD_MAX = 1500 - SP_IPV4_HEADER_BYTES - SP_UDP_HEADER_BYTES,
SP_TCP_MIN_PEER_MSS = 48,
SP_TCP_DEFAULT_PEER_MSS = 536,
SP_TCP_MTU1500_MAX_MSS = 1460,
SP_TCP_GSO_POLICY_SEGS = 32,
SP_TCP_PENDING_PER_FLOW = TCP_PENDING_WRITE_CAP,
SP_TCP_PENDING_ALL =
SP_INGRESS_RESERVATION_SLOTS * SP_INGRESS_RESERVATION_SLOT_BYTES,
SP_TCP_FLIGHT_PER_FLOW = 256 * 1024,
SP_TCP_FLIGHT_ALL = 32 * 1024 * 1024,
SP_TCP_RCV_WSCALE = 3,
SP_UDP_MMSG_MAX = 32,
SP_UDP_PENDING_PER_FLOW = 4,
SP_UDP_PENDING_ALL = 512,
SP_TUN_DATA_MAX_PKTS = 32,
SP_TUN_DATA_MAX_BYTES = 2 * 1024 * 1024,
SP_TUN_CONTROL_MAX_PKTS = 256,
SP_TUN_CONTROL_MAX_BYTES = 512 * 1024,
SP_TUN_CONTROL_FRAME_MAX = SP_VNET_HDR_BYTES + 1500,
SP_TUN_TERMINAL_RESERVE = 16,
SP_TUN_RESPONSE_RESERVE = 8,
SP_INGRESS_STAGE_SLOTS = 12,
SP_EFFECT_BYTES = 256 * 1024,
SP_CHALLENGE_FLOW_PER_S = 10,
SP_CHALLENGE_FLOW_BURST = 20,
SP_CHALLENGE_ALL_PER_S = 100,
SP_CHALLENGE_ALL_BURST = 200,
SP_CLOSED_RST_ALL_PER_S = 100,
SP_CLOSED_RST_ALL_BURST = 200,
SP_ICMP_FLOW_PER_S = 5,
SP_ICMP_FLOW_BURST = 10,
SP_ICMP_ALL_PER_S = 50,
SP_ICMP_ALL_BURST = 100,
SP_TCP_DIRECT_MAX = 96,
SP_TCP_SOCKS_MAX = 64,
SP_TCP_OUTBOUND_MAX = MAX_TCP,
SP_TCP_PUBLISH_MAX = 96,
/* Direct, SOCKS, and published TCP share g_ns_flows. Role ceilings are
* admission policy within this one live-table ceiling, not additive pools. */
SP_TCP_LIVE_ALL_MAX = MAX_TCP,
SP_TCP_HALF_DIRECT_MAX = 32,
SP_TCP_HALF_SOCKS_MAX = 24,
SP_TCP_HALF_OUT_MAX = 48,
SP_TCP_HALF_PUBLISH_MAX = 32,
SP_CONNECT_PUBLISH_MAX = 16,
SP_CONNECT_INFLIGHT_MAX = 32,
SP_TIMEWAIT_OUTBOUND_MAX = 12288,
SP_TIMEWAIT_PUBLISH_MAX = 3072,
SP_TIMEWAIT_ALL_MAX = 15360,
SP_UDP_DIRECT_MAX = 48,
SP_UDP_SOCKS_MAX = 32,
SP_UDP_OUTBOUND_MAX = MAX_UDP,
SP_UDP_PUBLISH_MAX = MAX_PUBLISH_UDP,
SP_UDP_LIVE_ALL_MAX = MAX_UDP + MAX_PUBLISH_UDP,
SP_TCP_TABLE_SLOTS = 256,
SP_UDP_KEY_MAX = MAX_UDP + 2 * MAX_PUBLISH_UDP,
SP_UDP_TABLE_SLOTS = 1024,
SP_TIMEWAIT_TABLE_SLOTS = 32768,
SP_ADMISSION_KEY_MAX = 512,
SP_ADMISSION_TABLE_SLOTS = 1024,
SP_PORT_ALLOC_PROBES = 64,
SP_SYNTH_PORT_FIRST = 32768,
SP_SYNTH_PORT_LAST = 60999,
SP_UDP_ADMIT_ALL_PER_S = 128,
SP_UDP_ADMIT_ALL_BURST = 256,
SP_UDP_ADMIT_SOURCE_S = 32,
SP_UDP_ADMIT_SOURCE_B = 64,
SP_UDP_ADMIT_RULE_S = 64,
SP_UDP_ADMIT_RULE_B = 128,
SP_UDP_SOCKS_HANDSHAKE_SECONDS = 10,
SP_UDP_IDLE_SECONDS = 30,
SP_HOST_TCP_SNDBUF = 128 * 1024,
SP_HOST_TCP_RCVBUF = 128 * 1024,
/* Accepted effective-buffer band. SO_SNDBUF/SO_RCVBUF have no
* deterministic effective value: Linux doubles the request for bookkeeping
* and then clamps to net.core.wmem_max/rmem_max. A host with the common
* 212992 default therefore reports neither the request nor twice it, and a
* tuned-down host legitimately reports less than the request. This band is
* deliberately loose: it catches a set that was ignored outright or a
* nonsensical readback, not lawful kernel clamping. Do not tighten it toward
* SP_HOST_TCP_*BUF without evidence from the hosts in the release lane. */
SP_HOST_TCP_BUF_MIN = 4 * 1024,
SP_HOST_TCP_BUF_MAX = 32 * 1024 * 1024,
SP_HOST_UDP_SNDBUF = 64 * 1024,
SP_HOST_UDP_RCVBUF = 64 * 1024,
SP_CALLBACK_BUDGET_NS = 250 * 1000,
SP_SCHED_ROUND_NS = 2 * 1000 * 1000,
/* These two limits charge expanded logical work in the TUN egress
* service. Keep "egress" in the name: publish ingress has independent
* syscall/item/byte limits below, and calling these generic dispatch limits
* previously hid that the publish callbacks had no bound at all. */
SP_TUN_EGRESS_PKTS_TURN = 64,
SP_TUN_EGRESS_BYTES_TURN = 2 * 1024 * 1024,
SP_PUBLISH_ACCEPTS_TURN = 64,
SP_PUBLISH_DATAGRAMS_TURN = 64,
SP_PUBLISH_BYTES_TURN = 2 * 1024 * 1024,
SP_EFFECTS_TURN = 64,
SP_TIMER_EVENTS_TURN = 64,
/* Descriptors the broker holds while the event loop serves flows, in every
* mode (host tree, rootfs, setup, interactive). Startup-only descriptors
* are closed before the loop and the kernel reuses the lowest free number,
* so they do not add to the peak. test_tun_closer_contract checks a live
* broker against this total. */
SP_FIXED_STDIO_FDS = 3,
SP_FIXED_TUN_FDS = 1,
SP_FIXED_EPOLL_FDS = 1,
/* PID 1's pidfd, its exit-report socket and the liveness pipe write end. */
SP_FIXED_PID1_FDS = 3,
SP_FIXED_TUN_CLOSER_FDS = 1,
/* The stdout/stderr relay pipes plus, when the broker's stdout/stderr is
* not a regular file, a nonblocking destination descriptor for each
* (relay_prepare_destination); or the host tty and PTY master. */
SP_FIXED_PAYLOAD_IO_FDS = 4,
SP_FIXED_SCRATCH_PARENT_FDS = 1,
SP_FIXED_BENCH_TRACE_FDS = 1,
SP_FIXED_AUTHORITY_FDS = SP_FIXED_STDIO_FDS + SP_FIXED_TUN_FDS +
SP_FIXED_EPOLL_FDS + SP_FIXED_PID1_FDS +
SP_FIXED_TUN_CLOSER_FDS + SP_FIXED_PAYLOAD_IO_FDS +
SP_FIXED_SCRATCH_PARENT_FDS +
SP_FIXED_BENCH_TRACE_FDS,
};
#define SP_UDP_PENDING_BYTES \
((size_t)SP_UDP_PENDING_ALL * (size_t)SP_UDP_PAYLOAD_MAX)
#define SP_INGRESS_STAGE_BYTES \
((size_t)SP_INGRESS_STAGE_SLOTS * (size_t)SP_TUN_FRAME_MAX_BYTES)
_Static_assert(SP_VNET_HDR_BYTES == 10,
"SP-07 uses the non-mergeable VNET header");
_Static_assert(SP_ACCEPTED_PROFILE_VERSION == 1,
"accepted profile changes require explicit review");
_Static_assert(SP_TCP_OUTBOUND_MAX <= SP_TCP_LIVE_ALL_MAX &&
SP_TCP_PUBLISH_MAX <= SP_TCP_LIVE_ALL_MAX,
"each TCP role ceiling must fit the shared live table");
_Static_assert(SP_UDP_LIVE_ALL_MAX ==
SP_UDP_OUTBOUND_MAX + SP_UDP_PUBLISH_MAX,
"UDP role ceilings must cover the global ceiling");
_Static_assert(SP_INGRESS_RESERVATION_SLOT_BYTES >=
SP_TCP_GSO_POLICY_SEGS * SP_TCP_MTU1500_MAX_MSS,
"one reservation slot must retain a maximum ingress effect");
_Static_assert(SP_INGRESS_RESERVATION_SLOTS >= SP_INGRESS_STAGE_SLOTS,
"reservation pool must preserve multi-flow progress");
_Static_assert(SP_INGRESS_PUBLISH_RESERVATION_SLOTS <
SP_INGRESS_RESERVATION_SLOTS,
"published peers must not consume every reservation slot");
_Static_assert(SP_TCP_PUBLISH_MAX < SP_TCP_LIVE_ALL_MAX,
"published peers must not consume every default TCP slot");
_Static_assert(SP_TCP_DIRECT_MAX <= SP_TCP_OUTBOUND_MAX &&
SP_TCP_SOCKS_MAX <= SP_TCP_OUTBOUND_MAX,
"outbound TCP role ceilings exceed their table");
_Static_assert(SP_UDP_DIRECT_MAX <= SP_UDP_OUTBOUND_MAX &&
SP_UDP_SOCKS_MAX <= SP_UDP_OUTBOUND_MAX,
"outbound UDP role ceilings exceed their table");
_Static_assert(SP_TCP_LIVE_ALL_MAX * 2 <= SP_TCP_TABLE_SLOTS,
"TCP table load exceeds one half");
_Static_assert(SP_UDP_KEY_MAX * 2 <= SP_UDP_TABLE_SLOTS,
"UDP table load exceeds one half");
_Static_assert(SP_TIMEWAIT_ALL_MAX * 2 <= SP_TIMEWAIT_TABLE_SLOTS,
"TIME-WAIT table load exceeds one half");
_Static_assert(SP_TIMEWAIT_ALL_MAX ==
SP_TIMEWAIT_OUTBOUND_MAX + SP_TIMEWAIT_PUBLISH_MAX,
"TIME-WAIT role ceilings do not cover the global ceiling");
_Static_assert(SP_ADMISSION_KEY_MAX * 2 <= SP_ADMISSION_TABLE_SLOTS,
"admission table load exceeds one half");
_Static_assert(SP_SYNTH_PORT_FIRST > 0 &&
SP_SYNTH_PORT_FIRST <= SP_SYNTH_PORT_LAST,
"synthetic port range is invalid");
_Static_assert(SP_FIXED_AUTHORITY_FDS == 15,
"fixed authority fd worksheet changed");
_Static_assert(SP_UDP_PAYLOAD_MAX == 1472,
"MTU-1500 IHL-5 UDP payload contract");
_Static_assert(SP_TCP_MTU1500_MAX_MSS == 1460,
"MTU-1500 IHL-5 TCP MSS contract");
_Static_assert((int)SP_TCP_CORE_MIN_MSS == (int)SP_TCP_MIN_PEER_MSS &&
(int)SP_TCP_CORE_DEFAULT_MSS ==
(int)SP_TCP_DEFAULT_PEER_MSS &&
(int)SP_TCP_CORE_MAX_MSS ==
(int)SP_TCP_MTU1500_MAX_MSS &&
(int)SP_TCP_CORE_RCV_WSCALE == (int)SP_TCP_RCV_WSCALE &&
(int)SP_TCP_CORE_PENDING_CAPACITY ==
(int)SP_TCP_PENDING_PER_FLOW,
"core limits must remain tied to resource geometry");
_Static_assert(SP_TCP_PENDING_PER_FLOW >= (1U << SP_TCP_RCV_WSCALE),
"receive capacity must cover one scale quantum");
_Static_assert((SP_TCP_PENDING_PER_FLOW >> SP_TCP_RCV_WSCALE) <= UINT16_MAX,
"scaled receive capacity must fit the wire field");
_Static_assert(SP_TCP_PENDING_PER_FLOW -
((SP_TCP_PENDING_PER_FLOW >> SP_TCP_RCV_WSCALE)
<< SP_TCP_RCV_WSCALE) <
(1U << SP_TCP_RCV_WSCALE),
"receive-window floor tail must stay below one quantum");
_Static_assert(SP_TUN_TERMINAL_RESERVE + SP_TUN_RESPONSE_RESERVE <=
SP_TUN_CONTROL_MAX_PKTS,
"control reserves exceed the control queue");
_Static_assert(SP_TUN_CONTROL_MAX_BYTES >=
SP_TUN_CONTROL_MAX_PKTS * SP_TUN_CONTROL_FRAME_MAX,
"control bytes do not cover every control slot");
/* 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_UDP_RELAY,
FD_UDP_CTRL,
FD_PUBLISH_TCP_LISTENER,
FD_PUBLISH_UDP_SOCKET,
/* Live core-driven TCP adapter backend socket. */
FD_TCP_CORE,
/* Namespace child exit (pidfd). */
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;
static int g_tun_registered_fd = -1;
static struct epoll_event g_tun_interest;
/* Tokens include fd, role, slot and generation; cache only installed state. */
static int sp_epoll_update(int operation, int fd, struct epoll_event *event,
struct epoll_event *installed) {
if (operation == EPOLL_CTL_MOD && installed->events == event->events &&
installed->data.u64 == event->data.u64)
return 0;
if (epoll_ctl(g_epfd, operation, fd, event) < 0)
return -1;
*installed = *event;
return 0;
}
/* 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_out;
};
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 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;
if (relay->length == relay->capacity)
relay->source_paused = 1;
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;
}
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->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;
}
/* 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;
};
static struct tun_queue g_tun_queue;
/* One authoritative schema keeps the runtime counter names, storage, and
* report enumeration synchronized. Each entry names one stable scalar
* uint64_t field; update sites may use saturating increments or direct gauge
* assignment. Array histograms and fixed history records remain separate
* evidence products because they have different output contracts. */
#define SP_RESOURCE_COUNTER_SCHEMA(X) \
X(tun_queue_drops) \
X(tun_vnet_scalar_egress) \
X(host_datagram_read_calls) \
X(host_datagram_read_bytes) \
X(host_datagram_write_bytes) \
X(tcp_logical_segments_host_to_child) \
X(tcp_gso_frames_child_to_host) \
X(tcp_gso_expand_rejects) \
X(tcp_gso_frames_host_to_child) \
X(tcp_gso_segments_host_to_child) \
X(tcp_payload_bytes_host_to_child) \
X(parser_rejects) \
X(pump_reentry_failures) \
X(reservation_current) \
X(reservation_high_water) \
X(reservation_acquire_failures) \
X(tcp_flow_rejections) \
X(relay_drain_abandoned) \
X(epoll_update_failures) \
X(udp_flow_rejections) \
X(udp_queue_slot_exhaustions) \
X(udp_queue_byte_exhaustions) \
X(udp_terminal_drops) \
X(udp_recvmmsg_calls) \
X(udp_recvmmsg_datagrams) \
X(udp_recvmmsg_zero_results) \
X(udp_recvmmsg_eintr_yields) \
X(udp_sendmmsg_datagrams) \
X(udp_sendmmsg_partial_prefixes) \
X(udp_sendmmsg_zero_results) \
X(udp_sendmmsg_eintr_yields) \
X(ipv4_fragment_drops) \
X(scheduler_stale_index_rejects) \
X(icmp_drops)
#define SP_RESOURCE_COUNTER_FIELD(name) uint64_t name;
struct sp_resource_counters {
SP_RESOURCE_COUNTER_SCHEMA(SP_RESOURCE_COUNTER_FIELD)
uint64_t tun_queue_high_water;
};
#undef SP_RESOURCE_COUNTER_FIELD
static struct sp_resource_counters g_resource_counters;
static void sp_resource_print_baseline(const char *point);
static int g_ns_sched_tun_pending;
static int g_resource_metrics_enabled;
static pid_t g_runtime_role_pid;
static int g_vnet_profile_validated;
/* Counters and gauges are maintained only when SOCKPUPPET_METRICS asked for
* the exit dump; otherwise every update is skipped. No production decision
* reads them. */
static void sp_counter_add(uint64_t *counter, uint64_t amount) {
if (!g_resource_metrics_enabled)
return;
if (UINT64_MAX - *counter < amount)
*counter = UINT64_MAX;
else
*counter += amount;
}
static void sp_counter_inc(uint64_t *counter) {
sp_counter_add(counter, 1);
}
static void sp_resource_count_host_io(int datagram, int write_direction,
ssize_t result) {
if (!g_resource_metrics_enabled || !datagram)
return;
if (!write_direction)
sp_counter_inc(&g_resource_counters.host_datagram_read_calls);
if (result > 0)
sp_counter_add(write_direction
? &g_resource_counters.host_datagram_write_bytes
: &g_resource_counters.host_datagram_read_bytes,
(uint64_t)result);
}
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 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 require_writable_cwd = 0;
static int interactive_stdio = 0;
static int verbose = 0;
static int tcp_flow_limit = MAX_TCP;
static int udp_flow_limit = MAX_UDP;
#define SP_BIND_NAME_MAX 32
#define SP_SETUP_SCRIPT_MAX (1024U * 1024U)
#define SP_SETUP_TIMEOUT_DEFAULT_MS 30000U
#define SP_SETUP_TIMEOUT_MAX_MS 300000U
enum sp_bind_mode {
SP_BIND_RO,
SP_BIND_RW,
SP_BIND_COW,
};
enum sp_view_mode {
SP_VIEW_READONLY,
SP_VIEW_COW_DISPOSABLE,
SP_VIEW_COW_RETAINED,
};
enum sp_scope_kind {
SP_SCOPE_ROOTFS,
SP_SCOPE_WORKSPACE,
SP_SCOPE_BIND,
};
enum sp_upper_mode {
SP_UPPER_NONE,
SP_UPPER_DISPOSABLE,
SP_UPPER_RETAINED,
};
struct sp_bind_spec {
char name[SP_BIND_NAME_MAX + 1];
const char *source;
const char *target;
enum sp_bind_mode mode;
};
struct sp_fs_config {
const char *rootfs_path;
const char *retained_directory;
enum sp_view_mode rootfs_view;
enum sp_view_mode workspace_view;
struct sp_bind_spec bind;
int bind_enabled;
};
struct sp_setup_config {
const char *host_script;
const char *guest_script;
unsigned int timeout_ms;
uid_t payload_uid;
gid_t payload_gid;
};
struct sp_launch_config {
struct sp_fs_config filesystem;
struct sp_setup_config setup;
int setup_enabled;
};
struct sp_prepared_fs {
int retained_lock_fd;
int scratch_parent_fd;
char scratch_name[64];
int scratch_owned;
};
struct sp_mount_scope {
const struct sp_bind_spec *bind;
enum sp_scope_kind kind;
enum sp_bind_mode mode;
enum sp_upper_mode upper_mode;
int source_fd;
int target_fd;
int upper_fd;
int work_fd;
int attached;
};
/* The CLI-selected TCP limit is the shared direct/SOCKS/publish live-table
* boundary. Startup rejects values outside this range; returning zero here
* also makes focused tests and any future accidental pre-validation use fail
* closed instead of silently falling back to MAX_TCP. */
static size_t sp_ns_tcp_slot_limit(void) {
return tcp_flow_limit >= 1 && tcp_flow_limit <= MAX_TCP
? (size_t)tcp_flow_limit
: 0;
}
static size_t sp_publish_limit_for_shared(size_t shared) {
size_t child_reserved;
size_t publish;
if (shared == 0)
return 0;
child_reserved = shared / 4 + (shared % 4 != 0);
publish = shared - child_reserved;
return publish < SP_TCP_PUBLISH_MAX ? publish : SP_TCP_PUBLISH_MAX;
}
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;
};
static struct interactive_session interactive_session = {
.host_tty_fd = -1,
.pty_master_fd = -1,
.pty_slave_fd = -1,
};
/*
* Keep post-seccomp diagnostics deterministic on write(2). The parent also
* admits readv(2)/writev(2) for libc and ordinary vectored-I/O compatibility,
* but this fixed buffer avoids depending on a particular stdio backend or
* introducing allocation into the hardened path. stderr is an intentionally
* synchronous, potentially blocking sink; steady-state packet diagnostics
* must therefore use counters or rate-limited samples.
* Submit each record in one write (retry only EINTR with no bytes written).
* A short write is an error, not a second fragment that could interleave.
*/
/* Set while stderr is the host terminal in --interactive raw mode, where a
* bare newline would not return the cursor to column 0. Forked sandbox
* processes inherit it. */
static int g_stderr_raw_tty;
__attribute__((format(printf, 1, 2))) static int
sp_stderr_printf(const char *fmt, ...) {
char buffer[2048];
char crlf[2 * sizeof(buffer)];
const char *output = buffer;
va_list ap;
int length;
ssize_t written;
_Static_assert(sizeof(buffer) <= PIPE_BUF, "stderr record must fit a pipe write");
va_start(ap, fmt);
length = vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);
if (length < 0)
return -1;
if ((size_t)length >= sizeof(buffer)) {
errno = EOVERFLOW;
return -1;
}
if (g_stderr_raw_tty) {
int expanded = 0;
for (int i = 0; i < length; ++i) {
if (buffer[i] == '\n')
crlf[expanded++] = '\r';
crlf[expanded++] = buffer[i];
}
output = crlf;
length = expanded;
}
do {
written = write(STDERR_FILENO, output, (size_t)length);
} while (written < 0 && errno == EINTR);
if (written < 0)
return -1;
if (written != length) {
errno = EIO;
return -1;
}
return 0;
}
/* Debug macro - only prints if verbose mode enabled. */
#define DBG(fmt, ...) \
do { \
if (verbose) \
(void)sp_stderr_printf("[sockpuppet] " fmt "\n", ##__VA_ARGS__); \
} while (0)
/* Host gateway configuration - map 10.0.1.x to 127.0.0.x */
#define SANDBOX_GATEWAY_IP 0x0100000a /* 10.0.0.1: the sandbox's next hop */
#define DNS_PROXY_IP SANDBOX_GATEWAY_IP /* 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 */
#define SP_DNS_SLOT_INVALID UINT16_MAX
#define SP_DNS_NAME_MAX 255
#define SP_DNS_QUESTION_MAX (SP_DNS_NAME_MAX + 6)
#define SP_DNS_QUERY_MAX 512
#define SP_DNS_RESPONSE_MAX 512
#define SP_DNS_TTL_SECONDS 60U
#define SP_DNS_TTL_NS \
((uint64_t)SP_DNS_TTL_SECONDS * UINT64_C(1000000000))
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;
};
static enum egress_mode egress_mode = EGRESS_DIRECT;
static int egress_mode_explicit = 0;
struct host_rule {
uint8_t last_octet; /* exact x in 127.0.0.x (0-255) */
uint16_t port; /* exact port number (1-65535) */
int proto; /* IPPROTO_TCP, IPPROTO_UDP, or 0 for both */
};
static struct host_rule host_rules[MAX_HOST_RULES];
static int host_rule_count = 0;
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;
int effective_sndbuf;
int effective_rcvbuf;
struct epoll_event interest;
};
static struct publish_rule publish_rules[MAX_PUBLISH_RULES];
static int publish_rule_count = 0;
/* Descriptor-free, fixed-size policy product sealed after CLI validation.
* Credentials, resolved proxy sockaddr values, listener descriptors, socket
* buffers, and counters are executor state and intentionally absent. */
struct sp_net_policy_snapshot {
uint8_t initialized;
uint8_t egress;
uint8_t socks_remote_dns;
uint16_t host_rule_count;
uint16_t direct_rule_count;
struct host_rule host_rules[MAX_HOST_RULES];
struct direct_rule direct_rules[MAX_DIRECT_RULES];
};
static struct sp_net_policy_snapshot g_net_policy;
static int sp_policy_proto_valid(int proto, int allow_both) {
return proto == IPPROTO_TCP || proto == IPPROTO_UDP ||
(allow_both && proto == 0);
}
static int sp_policy_mask_valid(uint32_t mask) {
uint32_t inverse = ~mask;
return (inverse & (inverse + 1U)) == 0;
}
static int sp_net_policy_build(
struct sp_net_policy_snapshot *out, enum egress_mode selected_egress,
int selected_socks_enabled, int selected_socks_remote_dns,
const struct host_rule *selected_host_rules,
int selected_host_rule_count,
const struct direct_rule *selected_direct_rules,
int selected_direct_rule_count,
const struct publish_rule *selected_publish_rules,
int selected_publish_rule_count) {
struct sp_net_policy_snapshot candidate;
if (out == NULL || selected_egress < EGRESS_DIRECT ||
selected_egress > EGRESS_NONE ||
(selected_socks_enabled != 0 && selected_socks_enabled != 1) ||
(selected_socks_remote_dns != 0 && selected_socks_remote_dns != 1) ||
selected_host_rule_count < 0 ||
selected_host_rule_count > MAX_HOST_RULES ||
selected_direct_rule_count < 0 ||
selected_direct_rule_count > MAX_DIRECT_RULES ||
selected_publish_rule_count < 0 ||
selected_publish_rule_count > MAX_PUBLISH_RULES ||
(selected_host_rule_count != 0 && selected_host_rules == NULL) ||
(selected_direct_rule_count != 0 && selected_direct_rules == NULL) ||
(selected_publish_rule_count != 0 && selected_publish_rules == NULL))
return -1;
if ((selected_egress == EGRESS_SOCKS) != selected_socks_enabled ||
(selected_socks_remote_dns && !selected_socks_enabled))
return -1;
memset(&candidate, 0, sizeof(candidate));
candidate.egress = (uint8_t)selected_egress;
candidate.socks_remote_dns = (uint8_t)selected_socks_remote_dns;
candidate.host_rule_count = (uint16_t)selected_host_rule_count;
candidate.direct_rule_count = (uint16_t)selected_direct_rule_count;
for (int i = 0; i < selected_host_rule_count; ++i) {
const struct host_rule *rule = &selected_host_rules[i];
if (rule->port == 0 || !sp_policy_proto_valid(rule->proto, 1))
return -1;
candidate.host_rules[i] = *rule;
}
for (int i = 0; i < selected_direct_rule_count; ++i) {
const struct direct_rule *rule = &selected_direct_rules[i];
if (rule->port == 0 || !sp_policy_proto_valid(rule->proto, 1) ||
!sp_policy_mask_valid(rule->mask) ||
(rule->network & ~rule->mask) != 0)
return -1;
candidate.direct_rules[i] = *rule;
}
for (int i = 0; i < selected_publish_rule_count; ++i) {
const struct publish_rule *rule = &selected_publish_rules[i];
uint32_t host = ntohl(rule->host_ip);
if ((host & 0xff000000U) != 0x7f000000U || host == 0x7f000000U ||
host == 0x7f000001U || host == 0x7fffffffU ||
rule->host_port == 0 || rule->container_port == 0 ||
!sp_policy_proto_valid(rule->proto, 0) || rule->rule_index != i)
return -1;
}
candidate.initialized = 1;
*out = candidate;
return 0;
}
/* Private fixed-capacity index. SipHash-1-3 filters; adapters compare typed
* keys. Probes never escape a synchronous operation. Only bucket metadata moves
* on erase; object slots and their timer/reference identities remain stable.
* Allocation and entropy belong to broker startup, outside these mechanics. */
#if !defined(__linux__)
#error "hash table requires Linux"
#endif
#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__
#error "hash table requires little-endian"
#endif
#if !defined(__x86_64__) && !defined(__aarch64__) && \
!(defined(__riscv) && __riscv_xlen == 64)
#error "supported targets are x86-64, AArch64 and RV64"
#endif
_Static_assert(CHAR_BIT == 8 && sizeof(void *) == 8,
"64-bit byte-addressed target required");
struct ht_hash_key {
uint64_t k0;
uint64_t k1;
};
static inline uint64_t
ht_rotl64(uint64_t x, unsigned n)
{
return (x << n) | (x >> (64 - n));
}
#define HT_SIPROUND() do { \
v0 += v1; v1 = ht_rotl64(v1, 13); \
v1 ^= v0; v0 = ht_rotl64(v0, 32); \
v2 += v3; v3 = ht_rotl64(v3, 16); \
v3 ^= v2; \
v0 += v3; v3 = ht_rotl64(v3, 21); \
v3 ^= v0; \
v2 += v1; v1 = ht_rotl64(v1, 17); \
v1 ^= v2; v2 = ht_rotl64(v2, 32); \
} while (0)
static inline uint64_t
ht_load64(const void *p)
{
uint64_t x;
memcpy(&x, p, sizeof x);
return x;
}
static inline uint64_t
ht_siphash13(const void *data, size_t len,
const struct ht_hash_key *key)
{
const uint8_t *p = data;
const uint8_t *end = p + (len & ~(size_t)7);
uint64_t v0 = UINT64_C(0x736f6d6570736575) ^ key->k0;
uint64_t v1 = UINT64_C(0x646f72616e646f6d) ^ key->k1;
uint64_t v2 = UINT64_C(0x6c7967656e657261) ^ key->k0;
uint64_t v3 = UINT64_C(0x7465646279746573) ^ key->k1;
uint64_t b = (uint64_t)len << 56;
while (p != end) {
uint64_t m = ht_load64(p);
v3 ^= m;
HT_SIPROUND();
v0 ^= m;
p += 8;
}
switch (len & 7) {
case 7: b |= (uint64_t)p[6] << 48; /* fall through */
case 6: b |= (uint64_t)p[5] << 40; /* fall through */
case 5: b |= (uint64_t)p[4] << 32; /* fall through */
case 4: b |= (uint64_t)p[3] << 24; /* fall through */
case 3: b |= (uint64_t)p[2] << 16; /* fall through */
case 2: b |= (uint64_t)p[1] << 8; /* fall through */
case 1: b |= (uint64_t)p[0]; /* fall through */
default:
break;
}
v3 ^= b;
HT_SIPROUND();
v0 ^= b;
v2 ^= UINT64_C(0xff);
HT_SIPROUND();
HT_SIPROUND();
HT_SIPROUND();
return v0 ^ v1 ^ v2 ^ v3;
}
static inline bool
ht_hash_key_random(struct ht_hash_key *key)
{
uint8_t *p;
size_t left;
if (!key)
return false;
p = (uint8_t *)key;
left = sizeof *key;
while (left) {
ssize_t n = getrandom(p, left, 0);
if (n > 0) {
p += (size_t)n;
left -= (size_t)n;
continue;
}
if (n < 0 && errno == EINTR)
continue;
return false;
}
return true;
}
#undef HT_SIPROUND
#define HT_SLOT_EMPTY UINT32_MAX
#define HT_SLOT_ERROR (UINT32_MAX - 1U)
struct ht_bucket {
uint32_t hash;
uint32_t slot;
};
_Static_assert(sizeof(struct ht_bucket) == 8,
"ht_bucket must stay 8 bytes");
struct ht_index {
struct ht_bucket *b;
uint32_t capacity;
uint32_t slot_count;
uint32_t count;
uint32_t limit;
};
struct ht_probe {
uint32_t pos;
uint32_t left;
uint32_t hash;
};
enum ht_step {
HT_CANDIDATE,
HT_VACANT,
HT_EXHAUSTED,
HT_CORRUPT
};
enum ht_insert_result {
HT_INSERTED,
HT_FULL = 2,
HT_INVALID
};
static inline bool
ht_init(struct ht_index *t,
struct ht_bucket *bucket_storage,
uint32_t capacity,
uint32_t slot_count,
uint32_t limit)
{
if (!t || !bucket_storage ||
capacity < 2 ||
(capacity & (capacity - 1U)) ||
!slot_count ||
slot_count >= HT_SLOT_ERROR ||
!limit ||
limit > capacity / 2U)
return false;
for (uint32_t i = 0; i < capacity; ++i)
bucket_storage[i].slot = HT_SLOT_EMPTY;
*t = (struct ht_index) {
.b = bucket_storage,
.capacity = capacity,
.slot_count = slot_count,
.count = 0,
.limit = limit,
};
return true;
}
static inline struct ht_probe
ht_probe_begin(const struct ht_index *t, uint64_t hash)
{
uint32_t h = (uint32_t)hash;
return (struct ht_probe) {
.pos = h & (t->capacity - 1U),
.left = t->capacity,
.hash = h,
};
}
static inline enum ht_step
ht_probe_next(const struct ht_index *t,
struct ht_probe *p,
uint32_t *slot,
uint32_t *bucket)
{
if (!t || !p || !slot || !bucket || !t->b || !t->capacity)
return HT_CORRUPT;
const uint32_t mask = t->capacity - 1U;
while (p->left) {
--p->left;
const uint32_t i = p->pos;
const struct ht_bucket *b = &t->b[i];
p->pos = (i + 1U) & mask;
*bucket = i;
if (b->slot == HT_SLOT_EMPTY) {
*slot = HT_SLOT_EMPTY;
return HT_VACANT;
}
if (b->slot >= t->slot_count) {
*slot = HT_SLOT_ERROR;
return HT_CORRUPT;
}
if (b->hash == p->hash) {
*slot = b->slot;
return HT_CANDIDATE;
}
}
*slot = HT_SLOT_EMPTY;
*bucket = t->capacity;
return HT_EXHAUSTED;
}
static inline enum ht_insert_result
ht_insert_at(struct ht_index *t,
const struct ht_probe *p,
uint32_t bucket,
uint32_t slot)
{
if (!t || !p || !t->b ||
bucket >= t->capacity ||
slot >= t->slot_count ||
t->b[bucket].slot != HT_SLOT_EMPTY)
return HT_INVALID;
if (t->count >= t->limit)
return HT_FULL;
t->b[bucket] = (struct ht_bucket) {
.hash = p->hash,
.slot = slot,
};
++t->count;
return HT_INSERTED;
}
static inline uint32_t
ht_erase_at(struct ht_index *t, uint32_t hole)
{
if (!t || !t->b ||
hole >= t->capacity ||
!t->count ||
t->b[hole].slot == HT_SLOT_EMPTY ||
t->b[hole].slot >= t->slot_count)
return HT_SLOT_ERROR;
const uint32_t mask = t->capacity - 1U;
uint32_t i = (hole + 1U) & mask;
bool found_empty = false;
for (uint32_t n = 0;
n < t->capacity;
++n, i = (i + 1U) & mask) {
uint32_t slot = t->b[i].slot;
if (slot == HT_SLOT_EMPTY) {
found_empty = true;
break;
}
if (slot >= t->slot_count)
return HT_SLOT_ERROR;
}
if (!found_empty)
return HT_SLOT_ERROR;
const uint32_t removed = t->b[hole].slot;
i = (hole + 1U) & mask;
for (;;) {
struct ht_bucket b = t->b[i];
if (b.slot == HT_SLOT_EMPTY)
break;
const uint32_t home = b.hash & mask;
if (((hole - home) & mask) <
((i - home) & mask)) {
t->b[hole] = b;
hole = i;
}
i = (i + 1U) & mask;
}
t->b[hole].slot = HT_SLOT_EMPTY;
--t->count;
return removed;
}
struct sp_admission_key { uint8_t bytes[5]; };
struct sp_admission_entry;
static void sp_resource_admission_source_key(struct sp_admission_key *key, uint32_t cli_ip,
uint8_t role);
static struct sp_admission_entry *
sp_resource_admission_acquire(const struct sp_admission_key *key);
static int sp_resource_admission_take(struct sp_admission_entry *entry,
uint64_t now_ns);
static void sp_resource_admission_release_source(uint32_t cli_ip, uint8_t role);
static int sp_ns_accept_publish(int tunfd, struct publish_rule *rule, int fd,
const struct sockaddr_in *peer);
enum sp_udp_role {
SP_UDP_ROLE_NONE = 0,
SP_UDP_ROLE_DIRECT,
SP_UDP_ROLE_SOCKS,
SP_UDP_ROLE_PUBLISH,
};
enum sp_udp_mapping_state {
SP_UDP_MAPPING_INACTIVE = 0,
SP_UDP_MAPPING_ALLOCATED,
SP_UDP_MAPPING_SOCKS_NEGOTIATING,
SP_UDP_MAPPING_READY,
SP_UDP_MAPPING_DRAINING,
};
/* One indexed deadline heap. Owner IDs are stable table slots; teardown
* removes the slot before reuse. Position zero means absent, so BSS is ready
* without touching payload arenas or accumulating stale timer entries. */
enum {
SP_DEADLINE_TCP = 0,
SP_DEADLINE_UDP = MAX_TCP,
SP_DEADLINE_PUBLISH = MAX_TCP + MAX_UDP,
SP_DEADLINE_TIMEWAIT = MAX_TCP + MAX_UDP + MAX_PUBLISH_UDP,
SP_DEADLINE_COUNT = SP_DEADLINE_TIMEWAIT + SP_TIMEWAIT_ALL_MAX,
};
struct sp_deadline_heap {
uint64_t deadlines[SP_DEADLINE_COUNT];
uint16_t owners[SP_DEADLINE_COUNT];
uint16_t positions[SP_DEADLINE_COUNT]; /* heap index plus one */
uint16_t count;
};
_Static_assert(SP_DEADLINE_COUNT < UINT16_MAX, "deadline indices fit");
static struct sp_deadline_heap g_net_deadlines;
static int sp_deadline_less(const struct sp_deadline_heap *h, unsigned a, unsigned b) {
uint16_t x = h->owners[a], y = h->owners[b];
return h->deadlines[x] < h->deadlines[y] ||
(h->deadlines[x] == h->deadlines[y] && x < y);
}
static void sp_deadline_swap(struct sp_deadline_heap *h, unsigned a, unsigned b) {
uint16_t x = h->owners[a];
h->owners[a] = h->owners[b];
h->owners[b] = x;
h->positions[h->owners[a]] = (uint16_t)(a + 1);
h->positions[x] = (uint16_t)(b + 1);
}
static void sp_deadline_set(struct sp_deadline_heap *h, unsigned owner, uint64_t deadline) {
unsigned pos = h->positions[owner];
h->deadlines[owner] = deadline;
if (pos == 0) {
if (deadline == 0) return;
pos = h->count++;
h->owners[pos] = (uint16_t)owner;
h->positions[owner] = (uint16_t)(pos + 1);
} else {
--pos;
if (deadline == 0) {
h->positions[owner] = 0;
--h->count;
if (pos == h->count) return;
h->owners[pos] = h->owners[h->count];
h->positions[h->owners[pos]] = (uint16_t)(pos + 1);
}
}
while (pos > 0 && sp_deadline_less(h, pos, (pos - 1) / 2)) {
unsigned parent = (pos - 1) / 2;
sp_deadline_swap(h, pos, parent);
pos = parent;
}
for (;;) {
unsigned child = 2 * pos + 1;
if (child >= h->count) break;
if (child + 1 < h->count && sp_deadline_less(h, child + 1, child)) ++child;
if (!sp_deadline_less(h, child, pos)) break;
sp_deadline_swap(h, pos, child);
pos = child;
}
}
static uint64_t sp_deadline_next(const struct sp_deadline_heap *h) {
return h->count ? h->deadlines[h->owners[0]] : 0;
}
/* Sans-I/O UDP: shared lifecycle and datagram-handle queue for all roles.
* Payload arenas, tuple lookup, descriptors and admission accounting are adapter-owned. */
struct sp_udp_core {
uint64_t idle_deadline_ns;
uint64_t handshake_deadline_ns;
uint16_t pending_slots[SP_UDP_PENDING_PER_FLOW];
uint8_t role;
uint8_t state;
uint8_t pending_head;
uint8_t pending_count;
};
enum sp_udp_event { SP_UDP_BEGIN, SP_UDP_NEGOTIATE, SP_UDP_READY,
SP_UDP_PROGRESS, SP_UDP_EXPIRE, SP_UDP_CLOSE };
static uint64_t sp_udp_deadline(uint64_t now, unsigned seconds) {
uint64_t duration = (uint64_t)seconds * UINT64_C(1000000000);
return UINT64_MAX - now < duration ? UINT64_MAX : now + duration;
}
/* Returns one close command on expiry; the adapter releases sockets and slots.
* Queue credit is independent of state, so a blocked send never refreshes idle time. */
static int sp_udp_step(struct sp_udp_core *core, enum sp_udp_event event,
uint64_t now, uint8_t role) {
switch (event) {
case SP_UDP_BEGIN:
memset(core, 0, sizeof(*core));
core->role = role;
core->state = SP_UDP_MAPPING_ALLOCATED;
for (size_t i = 0; i < SP_UDP_PENDING_PER_FLOW; ++i)
core->pending_slots[i] = UINT16_MAX;
break;
case SP_UDP_NEGOTIATE:
core->state = SP_UDP_MAPPING_SOCKS_NEGOTIATING;
core->handshake_deadline_ns = sp_udp_deadline(now, SP_UDP_SOCKS_HANDSHAKE_SECONDS);
break;
case SP_UDP_READY:
core->state = SP_UDP_MAPPING_READY;
core->handshake_deadline_ns = 0;
core->idle_deadline_ns = sp_udp_deadline(now, SP_UDP_IDLE_SECONDS);
break;
case SP_UDP_PROGRESS:
core->idle_deadline_ns = sp_udp_deadline(now, SP_UDP_IDLE_SECONDS);
break;
case SP_UDP_EXPIRE:
if ((core->handshake_deadline_ns == 0 || now < core->handshake_deadline_ns) &&
(core->idle_deadline_ns == 0 || now < core->idle_deadline_ns))
return 0;
/* fall through */
case SP_UDP_CLOSE:
core->state = SP_UDP_MAPPING_DRAINING;
return 1;
}
return 0;
}
static int sp_udp_queue(struct sp_udp_core *core, uint16_t slot) {
if (core->pending_count == SP_UDP_PENDING_PER_FLOW)
return 0;
size_t tail = (core->pending_head + core->pending_count) % SP_UDP_PENDING_PER_FLOW;
core->pending_slots[tail] = slot;
core->pending_count++;
return 1;
}
/* Retire only accepted datagrams; zero-length payloads are still one handle. */
static void sp_udp_pop(struct sp_udp_core *core) {
core->pending_slots[core->pending_head] = UINT16_MAX;
core->pending_head = (uint8_t)((core->pending_head + 1U) % SP_UDP_PENDING_PER_FLOW);
core->pending_count--;
}
/* End Sans-I/O UDP. */
struct publish_udp_flow {
struct sp_udp_core core;
uint32_t generation;
uint8_t used;
uint8_t admission_held;
uint8_t global_admission_held;
uint8_t rule_admission_held;
int rule_index;
struct sockaddr_in host_peer;
uint32_t published_local_ip;
uint16_t synthetic_port;
uint16_t child_port;
};
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 {
uint8_t used;
uint8_t reserved[3];
uint32_t generation;
uint32_t synthetic_ip; /* network byte order */
uint64_t last_used_ns;
uint64_t expires_ns;
uint32_t active_refs;
uint16_t name_len;
char name[SP_DNS_NAME_MAX + 1];
};
struct sp_dns_core {
struct dns_mapping mappings[MAX_DNS_MAPPINGS];
uint32_t next_host;
uint32_t next_generation;
uint64_t ttl_ns;
};
enum sp_dns_output_credit {
SP_DNS_CREDIT_AVAILABLE = 0,
SP_DNS_CREDIT_NO_OUTPUT,
};
enum sp_dns_effect_kind {
SP_DNS_EFFECT_NONE = 0,
SP_DNS_EFFECT_RESPONSE,
SP_DNS_EFFECT_DROP,
SP_DNS_EFFECT_RETRY,
};
/* The query owns only the bounded question needed to reproduce the response.
* Additional bytes (for example an EDNS section) are deliberately not carried
* into the transition because this proxy has always answered one question. */
struct sp_dns_query {
uint16_t identifier;
uint16_t request_flags;
uint16_t qtype;
uint16_t qclass;
uint16_t question_len;
uint16_t name_len;
char name[SP_DNS_NAME_MAX + 1];
uint8_t question[SP_DNS_QUERY_MAX - 12];
};
struct sp_dns_response {
uint16_t len;
uint8_t bytes[SP_DNS_RESPONSE_MAX];
};
/* A mapping identity is deliberately smaller than its value. The slot is
* stable while the mapping is live; generation prevents a released handle
* from addressing a later occupant of the same slot. */
struct sp_dns_ref {
uint16_t slot;
uint32_t generation;
};
/* Borrowed while ref remains held. No copy is made for synchronous SOCKS
* encoding; asynchronous handshakes copy the bounded name into their owner.
* sp_dns_core_mapping_name rejects an otherwise valid but unpinned ref. */
struct sp_dns_name_view {
uint32_t synthetic_ip;
uint16_t name_len;
const char *name;
};
enum sp_dns_lookup_kind {
SP_DNS_LOOKUP_EXPIRE = 0,
SP_DNS_LOOKUP_NAME,
SP_DNS_LOOKUP_IP,
};
struct sp_dns_lookup_key {
enum sp_dns_lookup_kind kind;
const char *name;
uint16_t name_len;
uint32_t synthetic_ip;
};
struct sp_dns_lookup_result {
uint16_t match_slot;
uint16_t free_slot;
uint16_t oldest_unpinned_slot;
};
struct sp_dns_core_input {
struct sp_dns_query query;
uint64_t now_ns;
enum sp_dns_output_credit credit;
};
struct sp_dns_core_output {
uint8_t mapping_exhausted;
struct sp_dns_response response;
};
SP_TEST_API static int sp_dns_decode_query(const uint8_t *query, size_t len,
struct sp_dns_query *out);
SP_TEST_API static enum sp_dns_effect_kind
sp_dns_core_step(struct sp_dns_core *core,
const struct sp_dns_core_input *input,
struct sp_dns_core_output *out);
SP_TEST_API static int sp_dns_core_acquire_ip(
struct sp_dns_core *core, uint32_t synthetic_ip, uint64_t now_ns,
struct sp_dns_ref *out);
SP_TEST_API static int sp_dns_core_release(
struct sp_dns_core *core, struct sp_dns_ref ref, uint64_t now_ns);
SP_TEST_API static int sp_dns_core_mapping_name(
const struct sp_dns_core *core, struct sp_dns_ref ref,
struct sp_dns_name_view *out);
static struct sp_dns_core g_dns_core;
/* 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 sp_policy_gateway_allowed(const struct sp_net_policy_snapshot *policy,
uint32_t gw_ip, uint16_t port,
int proto) {
if (policy == NULL || !policy->initialized)
return 0;
if (!is_gateway_ip(gw_ip))
return 0;
uint8_t last = gateway_last_octet(gw_ip);
for (uint16_t i = 0; i < policy->host_rule_count; i++) {
const struct host_rule *r = &policy->host_rules[i];
if (r->last_octet == last && r->port == port && (r->proto == 0 || r->proto == proto))
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
sp_policy_direct_egress_allowed(const struct sp_net_policy_snapshot *policy,
uint32_t ip_be, uint16_t port, int proto) {
if (policy == NULL || !policy->initialized)
return 0;
if (!direct_ip_blocked_by_default(ip_be))
return 1;
for (uint16_t i = 0; i < policy->direct_rule_count; ++i) {
if (direct_rule_matches(&policy->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) {
DBG("parent hardening prepared: profile=%s outbound_sockets=%s "
"interactive=%s setup_fds_closed=yes scratch_cleaned=yes "
"landlock=pending seccomp=pending",
parent_harden_profile_name(cfg->profile),
cfg->outbound_sockets_allowed ? "yes" : "no",
cfg->interactive ? "yes" : "no");
}
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");
}
#define MAX_CONNECTS_PER_SEC 50
#define TCP_HALF_OPEN_TIMEOUT_SEC 10
#define TCP_IDLE_TIMEOUT_SEC 120
struct udp_flow {
struct sp_udp_core core;
uint32_t generation;
uint8_t used;
uint8_t epoll_failed; /* an interest update failed; closed by the loop */
uint8_t admission_held;
uint8_t global_admission_held;
uint32_t cli_ip;
uint16_t cli_port;
uint32_t srv_ip;
uint16_t srv_port;
struct sockaddr_in connected_remote;
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 */
struct epoll_event relay_interest, ctrl_interest;
int effective_sndbuf;
int effective_rcvbuf;
struct sockaddr_in relay_addr; /* Expected relay source for validation */
struct socks_io socks;
uint8_t has_last_quote;
uint8_t last_quote[SP_IPV4_HEADER_BYTES + SP_UDP_HEADER_BYTES];
struct sp_dns_ref dns_mapping_ref;
unsigned long dropped_backpressure;
};
static struct udp_flow udp_flows[MAX_UDP];
static int sp_udp_runtime_step(struct sp_udp_core *core, enum sp_udp_event event,
uint64_t now, uint8_t role) {
int result = sp_udp_step(core, event, now, role);
uintptr_t address = (uintptr_t)core;
unsigned owner;
uint64_t deadline = core->handshake_deadline_ns;
if (address >= (uintptr_t)udp_flows && address < (uintptr_t)(udp_flows + MAX_UDP))
owner = SP_DEADLINE_UDP + (unsigned)((address - (uintptr_t)udp_flows) / sizeof(*udp_flows));
else if (address >= (uintptr_t)publish_udp_flows &&
address < (uintptr_t)(publish_udp_flows + MAX_PUBLISH_UDP))
owner = SP_DEADLINE_PUBLISH + (unsigned)((address - (uintptr_t)publish_udp_flows) / sizeof(*publish_udp_flows));
else
return result;
if (deadline == 0 || (core->idle_deadline_ns && core->idle_deadline_ns < deadline))
deadline = core->idle_deadline_ns;
sp_deadline_set(&g_net_deadlines, owner, result ? 0 : deadline);
return result;
}
struct sp_udp_pending_slot {
uint32_t generation;
uint16_t length;
uint16_t owner_index;
uint8_t used;
uint8_t owner_role;
uint8_t owner_publish;
uint8_t has_quote;
uint8_t quote[SP_IPV4_HEADER_BYTES + SP_UDP_HEADER_BYTES];
};
static struct sp_udp_pending_slot
g_udp_pending_slots[SP_UDP_PENDING_ALL];
enum {
SP_UDP_MMSG_RX_BYTES = 2048,
SP_UDP_MMSG_CONTROL_BYTES = 2 * CMSG_SPACE(sizeof(struct in_pktinfo)),
};
/* One broker thread owns these reusable parallel vectors. mmsghdr must be a
* genuinely contiguous array for the syscall ABI. Every submitted index is
* fully reset immediately before recvmmsg(), so a short vector cannot expose
* name, control, flag, length, or payload metadata from an earlier turn. */
static struct mmsghdr g_udp_mmsg_messages[SP_UDP_MMSG_MAX];
static struct iovec g_udp_mmsg_iov[SP_UDP_MMSG_MAX];
static struct sockaddr_in g_udp_mmsg_sources[SP_UDP_MMSG_MAX];
static unsigned char
g_udp_mmsg_control[SP_UDP_MMSG_MAX][SP_UDP_MMSG_CONTROL_BYTES];
static uint8_t g_udp_mmsg_payload[SP_UDP_MMSG_MAX][SP_UDP_MMSG_RX_BYTES];
enum {
SP_UDP_MMSG_TX_BYTES =
SP_UDP_PENDING_PER_FLOW * SP_UDP_PAYLOAD_MAX + 256,
};
static struct mmsghdr g_udp_mmsg_tx_messages[SP_UDP_MMSG_MAX];
static struct iovec g_udp_mmsg_tx_iov[SP_UDP_MMSG_MAX];
static struct sockaddr_in g_udp_mmsg_tx_names[SP_UDP_MMSG_MAX];
static unsigned char
g_udp_mmsg_tx_control[SP_UDP_MMSG_MAX][SP_UDP_MMSG_CONTROL_BYTES];
static uint8_t g_udp_mmsg_tx_payload[SP_UDP_MMSG_MAX][SP_UDP_MMSG_TX_BYTES];
static size_t g_udp_pending_live_bytes;
static uint64_t g_ns_aux_hash_key;
static uint64_t sp_ns_now_ns(void);
/* 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];
struct sp_resource_geometry {
size_t current_static_arena_bytes;
size_t declared_table_bytes;
size_t declared_userspace_pool_bytes;
size_t requested_kernel_socket_bytes;
size_t declared_total_envelope_bytes;
size_t selected_fd_ceiling;
size_t k_rx;
};
struct sp_tcp_key {
uint32_t cli_ip, srv_ip;
uint16_t cli_port, srv_port;
};
_Static_assert(sizeof(struct sp_tcp_key) == 12, "TCP tuple has no padding");
struct sp_timewait_key {
uint32_t cli_ip, srv_ip;
uint16_t cli_port, srv_port;
};
_Static_assert(sizeof(struct sp_timewait_key) == 12, "TIME-WAIT tuple has no padding");
struct sp_udp_key { uint8_t bytes[24]; };
struct sp_udp_table_value { uint16_t index; uint8_t role; };
struct sp_udp_entry {
struct sp_udp_key key;
uint32_t generation;
struct sp_udp_table_value value;
};
struct sp_resource_admission_value { uint64_t allowance, last_ns; };
struct sp_admission_entry {
struct sp_admission_key key;
bool live;
uint64_t refs;
struct sp_resource_admission_value value;
};
enum sp_ns_timewait_slot_state { SP_NS_TIMEWAIT_EMPTY, SP_NS_TIMEWAIT_LIVE };
#define SP_NS_TIMEWAIT_ADMISSION_REF UINT16_C(1)
struct sp_ns_timewait_entry {
uint64_t expiry_ns, snd_nxt_abs, rcv_acked_abs, rcv_adv_right_abs;
uint32_t cli_ip, srv_ip;
uint16_t cli_port, srv_port;
uint8_t state, role;
uint16_t reserved;
uint32_t admission_source_ip;
};
struct sp_tcp_map {
struct ht_index index;
struct ht_hash_key secret;
struct sp_ns_flow *flows;
};
struct sp_udp_map {
struct ht_index index;
struct ht_hash_key secret;
struct sp_udp_entry *entries;
uint32_t *free_slots, free_count;
};
struct sp_timewait_map {
struct ht_index index;
struct ht_hash_key secret;
struct sp_ns_timewait_entry *entries;
uint32_t *free_slots, free_count;
};
struct sp_admission_map {
struct ht_index index;
struct ht_hash_key secret;
struct sp_admission_entry *entries;
uint32_t *free_slots, free_count;
};
/* One compile-time arena layout: mmap supplies page alignment, C supplies typed
* offsets. No general allocator, resizing or runtime allocation interface. */
struct sp_map_storage {
_Alignas(64) struct ht_bucket tcp[SP_TCP_TABLE_SLOTS];
_Alignas(64) struct ht_bucket udp[SP_UDP_TABLE_SLOTS];
_Alignas(64) struct ht_bucket timewait[SP_TIMEWAIT_TABLE_SLOTS];
_Alignas(64) struct ht_bucket admission[SP_ADMISSION_TABLE_SLOTS];
struct sp_udp_entry udp_entries[SP_UDP_KEY_MAX];
struct sp_ns_timewait_entry timewait_entries[SP_TIMEWAIT_ALL_MAX];
struct sp_admission_entry admission_entries[SP_ADMISSION_KEY_MAX];
uint32_t udp_free[SP_UDP_KEY_MAX];
uint32_t timewait_free[SP_TIMEWAIT_ALL_MAX];
uint32_t admission_free[SP_ADMISSION_KEY_MAX];
};
static struct sp_map_storage *g_map_storage;
static struct sp_tcp_map g_tcp_map;
static struct sp_udp_map g_udp_map;
static struct sp_timewait_map g_timewait_map;
static struct sp_admission_map g_admission_map;
/* Fixed packet arenas stay in BSS and become resident only when their owning
* path writes them. Startup initializes queue metadata, not every payload
* page: a configured upper bound is not a reason to charge idle RSS. */
static uint8_t g_resource_udp_pending_arena[SP_UDP_PENDING_BYTES];
static uint8_t g_resource_tun_data_arena[SP_TUN_DATA_MAX_BYTES];
static uint8_t g_resource_tun_control_arena[SP_TUN_CONTROL_MAX_BYTES];
enum { SP_NS_QUEUE_MAX = 16, SP_NS_PUMP_ITERS = SP_EFFECTS_TURN };
static int g_ns_tunfd_for_emit = -1;
enum sp_pump_item_kind {
SP_PUMP_ITEM_ADMISSION = 1,
SP_PUMP_ITEM_SEGMENT,
SP_PUMP_ITEM_CONNECT_RESULT,
SP_PUMP_ITEM_STREAM_SEND_RESULT,
SP_PUMP_ITEM_STREAM_CONSUMED,
SP_PUMP_ITEM_STREAM_EOF,
SP_PUMP_ITEM_TUN_EMIT_RESULT,
SP_PUMP_ITEM_CREDIT,
SP_PUMP_ITEM_TIMER_FIRED,
SP_PUMP_ITEM_INGRESS_DISPOSED,
};
struct sp_pump_item {
enum sp_pump_item_kind kind;
uint64_t now; /* copied from the event's context when the item is built */
union {
struct sp_ev_flow_admitted admission;
struct sp_ev_tcp_segment segment;
struct sp_ev_connect_result connect_result;
struct sp_ev_stream_send_result stream_send_result;
struct sp_ev_stream_consumed stream_consumed;
struct sp_ev_stream_eof stream_eof;
struct sp_ev_tun_emit_result tun_emit_result;
struct sp_ev_credit credit;
struct sp_ev_timer_fired timer_fired;
struct sp_ev_ingress_disposed ingress_disposed;
} value;
};
static enum sp_tcp_core_result sp_pump_item_dispatch(
struct sp_tcp_core *core, const struct sp_pump_item *item,
struct sp_typed_effect_list *effects);
_Static_assert(sizeof(struct sp_pump_item) <= 104,
"private pump item exceeded its reviewed D3 ceiling");
/* One bounded workspace; no pump continuation survives a call. */
struct sp_ns_pump {
struct sp_pump_item queue[SP_NS_QUEUE_MAX];
size_t count;
struct sp_ns_flow *owner;
int tunfd;
};
static struct sp_ns_pump g_ns_pump;
static uint32_t g_ns_timewait_heap_count;
static int sp_checked_add_size(size_t left, size_t right, size_t *out) {
if (out == NULL || left > SIZE_MAX - right)
return -1;
*out = left + right;
return 0;
}
static int sp_checked_mul_size(size_t left, size_t right, size_t *out) {
if (out == NULL || (right != 0 && left > SIZE_MAX / right))
return -1;
*out = left * right;
return 0;
}
static int sp_checked_ceil_div_size(size_t numerator, size_t denominator,
size_t *out) {
size_t adjusted;
if (out == NULL || denominator == 0)
return -1;
if (numerator == 0) {
*out = 0;
return 0;
}
if (sp_checked_add_size(numerator, denominator - 1, &adjusted) < 0)
return -1;
*out = adjusted / denominator;
return 0;
}
static int sp_resource_compute_geometry(int selected_tcp, int selected_udp,
int selected_publish_rules,
struct sp_resource_geometry *geometry) {
size_t total = 0;
size_t value;
size_t stream_fds;
size_t datagram_fds;
size_t socks_control_fds = SP_UDP_SOCKS_MAX;
size_t publish_listener_fds;
size_t fixed_fds = SP_FIXED_AUTHORITY_FDS + SP_CONNECT_INFLIGHT_MAX;
extern char __executable_start;
extern char _end;
if (geometry == NULL || selected_tcp < 1 || selected_tcp > MAX_TCP ||
selected_udp < 1 || selected_udp > MAX_UDP ||
selected_publish_rules < 0 ||
selected_publish_rules > MAX_PUBLISH_RULES)
return -1;
memset(geometry, 0, sizeof(*geometry));
if ((uintptr_t)&_end < (uintptr_t)&__executable_start)
return -1;
/* Conservative ELF image span: includes code/rodata and alignment as well
* as writable pools. __data_start is a glibc CRT symbol absent on musl. */
geometry->current_static_arena_bytes =
(size_t)((uintptr_t)&_end - (uintptr_t)&__executable_start);
geometry->declared_table_bytes =
sizeof(struct sp_map_storage);
geometry->declared_userspace_pool_bytes =
geometry->current_static_arena_bytes + geometry->declared_table_bytes;
if (sp_checked_ceil_div_size(SP_TCP_PENDING_PER_FLOW,
SP_TCP_GSO_POLICY_SEGS *
SP_TCP_MTU1500_MAX_MSS,
&geometry->k_rx) < 0 ||
geometry->k_rx != 6 || SP_INGRESS_STAGE_SLOTS < 2 * geometry->k_rx)
return -1;
total = 0;
stream_fds = (size_t)selected_tcp;
if (sp_checked_mul_size((size_t)selected_publish_rules, 2, &value) < 0 ||
sp_checked_add_size((size_t)selected_udp, socks_control_fds,
&datagram_fds) < 0 ||
sp_checked_add_size(stream_fds, datagram_fds, &total) < 0 ||
sp_checked_add_size(total, value, &total) < 0 ||
sp_checked_add_size(total, fixed_fds, &total) < 0)
return -1;
geometry->selected_fd_ceiling = total;
publish_listener_fds = (size_t)selected_publish_rules;
if (sp_checked_add_size(stream_fds, socks_control_fds, &stream_fds) < 0 ||
sp_checked_add_size(stream_fds, publish_listener_fds, &stream_fds) < 0 ||
sp_checked_add_size((size_t)selected_udp, publish_listener_fds,
&datagram_fds) < 0)
return -1;
total = 0;
if (sp_checked_mul_size(stream_fds,
2U * (SP_HOST_TCP_SNDBUF + SP_HOST_TCP_RCVBUF),
&total) < 0 ||
sp_checked_mul_size(datagram_fds,
2U * (SP_HOST_UDP_SNDBUF + SP_HOST_UDP_RCVBUF),
&value) < 0 ||
sp_checked_add_size(total, value, &total) < 0)
return -1;
geometry->requested_kernel_socket_bytes = total;
value = geometry->declared_userspace_pool_bytes;
if (sp_checked_add_size(value, geometry->requested_kernel_socket_bytes,
&value) < 0 ||
sp_checked_add_size(value, SP_RESOURCE_MEMORY_MARGIN_BYTES, &value) < 0)
return -1;
geometry->declared_total_envelope_bytes = value;
return 0;
}
static int sp_resource_validate_startup(void) {
struct sp_resource_geometry geometry;
struct rlimit nofile;
if (sp_resource_compute_geometry(tcp_flow_limit, udp_flow_limit,
publish_rule_count, &geometry) < 0) {
fprintf(stderr, "[sockpuppet] invalid resource geometry\n");
return -1;
}
if (getrlimit(RLIMIT_NOFILE, &nofile) < 0)
return -1;
if (geometry.declared_total_envelope_bytes > SP_BROKER_MEMORY_MAX_BYTES) {
fprintf(stderr,
"[sockpuppet] declared memory envelope=%zu exceeds broker "
"MemoryMax=%zu\n",
geometry.declared_total_envelope_bytes,
SP_BROKER_MEMORY_MAX_BYTES);
errno = ENOMEM;
return -1;
}
if (nofile.rlim_cur != RLIM_INFINITY &&
geometry.selected_fd_ceiling > (size_t)nofile.rlim_cur) {
fprintf(stderr,
"[sockpuppet] RLIMIT_NOFILE=%llu cannot cover fd ceiling=%zu\n",
(unsigned long long)nofile.rlim_cur,
geometry.selected_fd_ceiling);
errno = EMFILE;
return -1;
}
DBG("resource geometry: static=%zu pools=%zu kernel_socket_request=%zu fds=%zu "
"k_rx=%zu",
geometry.current_static_arena_bytes,
geometry.declared_userspace_pool_bytes,
geometry.requested_kernel_socket_bytes, geometry.selected_fd_ceiling,
geometry.k_rx);
return 0;
}
static void sp_resource_print_counters(void) {
(void)sp_stderr_printf("sp_runtime_schema\tcounters-v2\n");
(void)sp_stderr_printf(
"sp_runtime_config\tvnet_profile_validated\t%d\t"
"header_bytes\t%d\trequested_offloads\tCSUM|TSO4\t"
"gso_max_size\t%d\tgso_max_segs\t%d\n",
g_vnet_profile_validated,
SP_VNET_HDR_BYTES, SP_TUN_BODY_MAX_BYTES, SP_TCP_GSO_POLICY_SEGS);
#define SP_RESOURCE_COUNTER_PRINT(name) \
(void)sp_stderr_printf("sp_runtime_counter\t" #name "\t%llu\n", \
(unsigned long long)g_resource_counters.name);
SP_RESOURCE_COUNTER_SCHEMA(SP_RESOURCE_COUNTER_PRINT)
#undef SP_RESOURCE_COUNTER_PRINT
}
/* ---------- utilities ---------- */
static int sp_is_namespace_child;
static void die(const char *msg) {
perror(msg);
if (sp_is_namespace_child)
_exit(1);
exit(1);
}
struct sp_child_owner {
pid_t pid;
int pidfd;
};
static int sp_take_fd(int *fd) {
int taken;
if (fd == NULL)
return -1;
taken = *fd;
*fd = -1;
return taken;
}
static int sp_close_owned_fd(int *fd) {
int owned;
if (fd == NULL || *fd < 0)
return 0;
owned = sp_take_fd(fd);
return close(owned);
}
static void sp_close_owned_fd_nointr(int *fd) {
(void)sp_close_owned_fd(fd);
}
static void sp_child_owner_init(struct sp_child_owner *child) {
child->pid = -1;
child->pidfd = -1;
}
static int sp_open_directory_nosymlinks(const char *path) {
struct open_how how = {
.flags = O_PATH | O_DIRECTORY | O_CLOEXEC,
.resolve = RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS,
};
return (int)syscall(SYS_openat2, AT_FDCWD, path, &how, sizeof(how));
}
static int sp_open_directory_for_lock(const char *path) {
struct open_how how = {
.flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC,
.resolve = RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS,
};
return (int)syscall(SYS_openat2, AT_FDCWD, path, &how, sizeof(how));
}
static int sp_open_directory_beneath(int root_fd,
const char *absolute_path) {
struct open_how how = {
.flags = O_PATH | O_DIRECTORY | O_CLOEXEC,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS |
RESOLVE_NO_SYMLINKS,
};
return (int)syscall(SYS_openat2, root_fd, absolute_path + 1, &how,
sizeof(how));
}
static void sp_prepared_fs_init(struct sp_prepared_fs *prepared) {
memset(prepared, 0, sizeof(*prepared));
prepared->retained_lock_fd = -1;
prepared->scratch_parent_fd = -1;
}
static int sp_prepared_fs_cleanup_scratch(struct sp_prepared_fs *prepared) {
if (!prepared->scratch_owned)
return 0;
if (unlinkat(prepared->scratch_parent_fd, prepared->scratch_name,
AT_REMOVEDIR) < 0 && errno != ENOENT)
return -1;
prepared->scratch_owned = 0;
return 0;
}
static void sp_prepared_fs_close_all(struct sp_prepared_fs *prepared) {
(void)sp_prepared_fs_cleanup_scratch(prepared);
sp_close_owned_fd_nointr(&prepared->retained_lock_fd);
sp_close_owned_fd_nointr(&prepared->scratch_parent_fd);
}
static void
sp_prepared_fs_close_child_scratch(struct sp_prepared_fs *prepared) {
prepared->scratch_owned = 0;
sp_close_owned_fd_nointr(&prepared->scratch_parent_fd);
}
static int sp_prepare_fs_storage(struct sp_prepared_fs *prepared,
const struct sp_fs_config *config,
char *overlay_base, size_t overlay_base_size,
char *error, size_t error_size) {
char scratch[] = "/tmp/.sockpuppet-overlay-XXXXXX";
const char *name;
if (config->retained_directory != NULL) {
prepared->retained_lock_fd =
sp_open_directory_for_lock(config->retained_directory);
if (prepared->retained_lock_fd < 0) {
snprintf(error, error_size, "open retained directory");
return -1;
}
if (flock(prepared->retained_lock_fd, LOCK_EX | LOCK_NB) < 0) {
snprintf(error, error_size, "lock retained directory");
sp_prepared_fs_close_all(prepared);
return -1;
}
}
if (mkdtemp(scratch) == NULL) {
snprintf(error, error_size, "create overlay scratch directory");
sp_prepared_fs_close_all(prepared);
return -1;
}
name = strrchr(scratch, '/');
name = name == NULL ? scratch : name + 1;
if (snprintf(prepared->scratch_name, sizeof(prepared->scratch_name), "%s",
name) >= (int)sizeof(prepared->scratch_name) ||
snprintf(overlay_base, overlay_base_size, "%s", scratch) >=
(int)overlay_base_size) {
(void)rmdir(scratch);
errno = ENAMETOOLONG;
snprintf(error, error_size, "overlay scratch path too long");
sp_prepared_fs_close_all(prepared);
return -1;
}
prepared->scratch_parent_fd =
open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (prepared->scratch_parent_fd < 0) {
int saved = errno;
(void)rmdir(scratch);
errno = saved;
snprintf(error, error_size, "open overlay scratch parent");
sp_prepared_fs_close_all(prepared);
return -1;
}
prepared->scratch_owned = 1;
return 0;
}
static void sp_kill_and_reap_child(struct sp_child_owner *child,
int report_fatal) {
int saved = errno;
int killed = 0;
if (child->pid <= 0) {
sp_close_owned_fd_nointr(&child->pidfd);
errno = saved;
return;
}
if (report_fatal)
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;
#endif
if (!killed && (kill(child->pid, SIGKILL) == 0 || errno == ESRCH))
killed = 1;
if (!killed) {
if (report_fatal)
fprintf(stderr,
"[sockpuppet] Warning: could not kill sandbox child: %s; "
"leaving it unreaped\n",
strerror(errno));
errno = saved;
return;
}
for (;;) {
pid_t reaped = waitpid(child->pid, NULL, 0);
if (reaped == child->pid || (reaped < 0 && errno == ECHILD)) {
child->pid = -1;
break;
}
if (reaped < 0 && errno == EINTR)
continue;
break;
}
(void)sp_close_owned_fd(&child->pidfd);
errno = saved;
}
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;
}
/* Failed epoll interest updates (F2). An update that fails leaves the kernel
* watching a different set of events than the broker intends, which can
* stall a flow or spin on stale level-triggered readiness. The owner of the
* registration decides: a TCP flow is reset, a UDP flow is marked and closed
* by the broker loop before its next wait (its update helpers have callers
* that keep using the flow), and a broker-owned registration (TUN, publish
* listener) makes the broker loop fail. Fatal scheduler TUN writes use the
* same broker failure latch; they never become retryable backpressure. */
static int g_broker_io_failed;
static int g_udp_epoll_failed;
static void sp_epoll_update_failed(const char *what) {
sp_counter_inc(&g_resource_counters.epoll_update_failures);
DBG("epoll_ctl mod %s: %s", what, strerror(errno));
}
static void tun_update_events(int tunfd) {
struct epoll_event ev = {
.events = EPOLLIN |
((g_tun_queue.count > 0 || g_ns_sched_tun_pending) ? EPOLLOUT
: 0),
.data.u64 = epoll_token_encode(FD_TUN, 0, (uint16_t)tunfd, 1),
};
if (g_epfd >= 0 && g_tun_registered_fd == tunfd &&
sp_epoll_update(EPOLL_CTL_MOD, tunfd, &ev, &g_tun_interest) < 0) {
sp_epoll_update_failed("TUN");
g_broker_io_failed = 1;
}
}
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) {
sp_counter_inc(&g_resource_counters.tun_queue_drops);
DBG("TUN output queue full (%s), dropping packet", what);
errno = len > TUN_QUEUE_PACKET_MAX ? EMSGSIZE : EAGAIN;
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++;
if (g_resource_metrics_enabled &&
g_tun_queue.count > g_resource_counters.tun_queue_high_water)
g_resource_counters.tun_queue_high_water = g_tun_queue.count;
tun_update_events(tunfd);
return 0;
}
static int sp_vnet_output_envelope_valid(const uint8_t *record, size_t len);
static ssize_t tun_kernel_write(int tunfd, const uint8_t *record, size_t len) {
return write(tunfd, record, len);
}
static int tun_write_frame(int tunfd, const uint8_t *frame, size_t len,
const char *what) {
if (!sp_vnet_output_envelope_valid(frame, len)) {
DBG("invalid VNET output envelope (%s): %zu", what, len);
errno = EPROTO;
return -1;
}
if (g_tun_queue.count > 0)
return tun_queue_packet(tunfd, frame, len, what);
for (;;) {
ssize_t n = tun_kernel_write(tunfd, frame, len);
if (n == (ssize_t)len)
return 0;
if (n >= 0) {
DBG("TUN write short (%s): %zd/%zu, dropping frame", what, n, len);
errno = EIO;
return -1;
}
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return tun_queue_packet(tunfd, frame, len, what);
int saved = errno;
DBG("TUN write failed (%s): %s", what, strerror(saved));
errno = saved;
return -1;
}
}
/* 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) {
uint8_t frame[SP_VNET_HDR_BYTES + 1500];
if (len == 0 || len > 1500) {
DBG("invalid TUN packet length (%s): %zu", what, len);
return -1;
}
memset(frame, 0, SP_VNET_HDR_BYTES);
memcpy(frame + SP_VNET_HDR_BYTES, buf, len);
int result =
tun_write_frame(tunfd, frame, SP_VNET_HDR_BYTES + len, what);
if (result == 0)
sp_counter_inc(&g_resource_counters.tun_vnet_scalar_egress);
return result;
}
/* 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 = tun_kernel_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;
}
static int sp_validate_absolute_path(const char *path, const char *what,
char *error, size_t error_size) {
const char *component;
if (path == NULL || path[0] != '/') {
snprintf(error, error_size, "%s must be an absolute path", what);
return -1;
}
if (path[1] == '\0')
return 0;
if (path[strlen(path) - 1] == '/') {
snprintf(error, error_size, "%s must not end with '/'", what);
return -1;
}
component = path + 1;
while (*component != '\0') {
const char *end = strchr(component, '/');
size_t length = end == NULL ? strlen(component) : (size_t)(end - component);
if (length == 0 || (length == 1 && component[0] == '.') ||
(length == 2 && component[0] == '.' && component[1] == '.')) {
snprintf(error, error_size,
"%s must not contain empty, '.' or '..' components", what);
return -1;
}
if (end == NULL)
break;
component = end + 1;
}
return 0;
}
static int sp_path_contains(const char *parent, const char *child) {
size_t length = strlen(parent);
if (strcmp(parent, child) == 0)
return 1;
if (strcmp(parent, "/") == 0)
return 1;
return strncmp(parent, child, length) == 0 && child[length] == '/';
}
static int sp_paths_overlap(const char *left, const char *right) {
return sp_path_contains(left, right) || sp_path_contains(right, left);
}
static int sp_bind_target_reserved(const char *target) {
static const char *const reserved[] = {
"/usr", "/lib", "/proc", "/dev", "/sys",
"/run", "/tmp", "/workspace", "/.oldroot",
};
if (strcmp(target, "/") == 0)
return 1;
for (size_t i = 0; i < sizeof(reserved) / sizeof(reserved[0]); ++i) {
if (sp_path_contains(reserved[i], target))
return 1;
}
return 0;
}
static int sp_parse_bind_spec(struct sp_bind_spec *out, const char *name,
const char *source, const char *target,
const char *mode, char *error,
size_t error_size) {
size_t name_length = strlen(name);
if (name_length == 0 || name_length > SP_BIND_NAME_MAX) {
snprintf(error, error_size, "bind name must contain 1-%d characters",
SP_BIND_NAME_MAX);
return -1;
}
for (size_t i = 0; i < name_length; ++i) {
unsigned char ch = (unsigned char)name[i];
if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '_' || ch == '-')) {
snprintf(error, error_size,
"bind name must use ASCII letters, digits, '_' or '-'");
return -1;
}
}
if (sp_validate_absolute_path(source, "bind source", error, error_size) < 0 ||
sp_validate_absolute_path(target, "bind target", error, error_size) < 0)
return -1;
if (strcmp(mode, "ro") == 0)
out->mode = SP_BIND_RO;
else if (strcmp(mode, "rw") == 0)
out->mode = SP_BIND_RW;
else if (strcmp(mode, "cow") == 0)
out->mode = SP_BIND_COW;
else {
snprintf(error, error_size, "bind mode must be ro, rw or cow");
return -1;
}
memcpy(out->name, name, name_length + 1);
out->source = source;
out->target = target;
return 0;
}
static int sp_validate_bind_spec(const struct sp_bind_spec *bind,
const char *rootfs_path,
const char *retained_directory, char *error,
size_t error_size) {
if (sp_bind_target_reserved(bind->target)) {
snprintf(error, error_size, "bind target is in a reserved tree");
return -1;
}
if (rootfs_path != NULL && sp_paths_overlap(bind->target, rootfs_path)) {
snprintf(error, error_size, "bind target overlaps the rootfs path");
return -1;
}
if (retained_directory != NULL &&
sp_paths_overlap(bind->target, retained_directory)) {
snprintf(error, error_size, "bind target overlaps the retained directory");
return -1;
}
return 0;
}
static int sp_validate_launch_config(const struct sp_launch_config *config,
char *error, size_t error_size) {
const struct sp_fs_config *filesystem = &config->filesystem;
const struct sp_setup_config *setup = &config->setup;
int retained_view =
filesystem->rootfs_view == SP_VIEW_COW_RETAINED ||
filesystem->workspace_view == SP_VIEW_COW_RETAINED;
if (filesystem->rootfs_path != NULL &&
sp_validate_absolute_path(filesystem->rootfs_path, "rootfs", error,
error_size) < 0)
return -1;
if (filesystem->retained_directory != NULL &&
sp_validate_absolute_path(filesystem->retained_directory,
"overlay directory", error, error_size) < 0)
return -1;
if (retained_view != (filesystem->retained_directory != NULL)) {
snprintf(error, error_size,
"--overlay-dir is required exactly for retained views");
return -1;
}
if (filesystem->rootfs_path != NULL &&
filesystem->retained_directory != NULL &&
sp_paths_overlap(filesystem->rootfs_path,
filesystem->retained_directory)) {
snprintf(error, error_size, "rootfs overlaps the retained directory");
return -1;
}
if (filesystem->bind_enabled &&
sp_validate_bind_spec(&filesystem->bind, filesystem->rootfs_path,
filesystem->retained_directory, error,
error_size) < 0)
return -1;
if (setup->host_script != NULL && setup->guest_script != NULL) {
snprintf(error, error_size, "--setup and --setup-guest are mutually exclusive");
return -1;
}
if (setup->host_script != NULL &&
sp_validate_absolute_path(setup->host_script, "setup script", error,
error_size) < 0)
return -1;
if (setup->guest_script != NULL &&
sp_validate_absolute_path(setup->guest_script, "guest setup script", error,
error_size) < 0)
return -1;
if (setup->guest_script != NULL && filesystem->rootfs_path == NULL) {
snprintf(error, error_size, "--setup-guest requires --rootfs");
return -1;
}
if (setup->timeout_ms == 0 ||
setup->timeout_ms > SP_SETUP_TIMEOUT_MAX_MS) {
snprintf(error, error_size, "setup timeout must be between 1 and 300 seconds");
return -1;
}
if (setup->payload_uid == 0 || setup->payload_uid == (uid_t)-1 ||
setup->payload_gid == 0 || setup->payload_gid == (gid_t)-1) {
snprintf(error, error_size, "payload UID and GID must be nonzero");
return -1;
}
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 int set_rlimit_checked(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)
return -1;
DBG("RLIMIT %s set to soft=%llu hard=%llu", name,
(unsigned long long)soft, (unsigned long long)hard);
return 0;
}
static int set_nofile_rlimit_checked(rlim_t requested) {
struct rlimit inherited;
rlim_t applied = requested;
if (getrlimit(RLIMIT_NOFILE, &inherited) < 0)
return -1;
if (inherited.rlim_max != RLIM_INFINITY && applied > inherited.rlim_max)
applied = inherited.rlim_max;
return set_rlimit_checked(RLIMIT_NOFILE, applied, applied, "NOFILE");
}
static int apply_parent_rlimits(void) {
return set_rlimit_checked(RLIMIT_CORE, 0, 0, "CORE") < 0 ||
set_rlimit_checked(RLIMIT_MEMLOCK, 0, 0, "MEMLOCK") < 0 ||
set_nofile_rlimit_checked(RLIMIT_PARENT_NOFILE) < 0
? -1
: 0;
}
__attribute__((format(printf, 1, 2))) 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_PID1,
BENCH_ROLE_PAYLOAD,
};
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_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;
long long ts_ns;
unsigned long long seq;
/* BENCH_TRACE is an opt-in startup evidence file. Its bounded records use
* the existing synchronous sink; a stalled filesystem may stall startup. */
if (bench_trace_fd < 0)
return;
ts_ns = bench_trace_now_ns();
seq = ++bench_trace_seq;
n = snprintf(buf, sizeof(buf),
"{\"ts_ns\":%lld,\"seq\":%llu,\"pid\":%ld,\"ppid\":%ld,",
ts_ns, seq, (long)bench_trace_pid, (long)bench_trace_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(bench_trace_role));
pos = bench_json_append_raw(buf, pos, sizeof(buf), ",\"event\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf), event);
pos = bench_json_append_raw(buf, pos, sizeof(buf), ",\"phase\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf), phase);
pos = bench_json_append_raw(buf, pos, sizeof(buf), ",\"status\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf), status);
pos = bench_json_append_raw(buf, pos, sizeof(buf), ",\"detail\":");
pos = bench_json_append_escaped(buf, pos, sizeof(buf), 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",
ts_ns, seq, (long)bench_trace_pid, (long)bench_trace_ppid,
bench_trace_role_name(bench_trace_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 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: clone(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_self_root(char *out, size_t out_sz) {
char rel[PATH_MAX];
if (read_self_cgroup_path(rel, sizeof(rel)) < 0)
return -1;
if (snprintf(out, out_sz, "/sys/fs/cgroup%s",
strcmp(rel, "/") == 0 ? "" : rel) >= (int)out_sz)
return -1;
return 0;
}
static const char *cgroup_delegation_state(const char *root) {
char ctrl_path[PATH_MAX];
char subtree_path[PATH_MAX];
if (root == NULL)
return "unknown";
if (snprintf(ctrl_path, sizeof(ctrl_path), "%s/cgroup.controllers", root) >=
(int)sizeof(ctrl_path) ||
snprintf(subtree_path, sizeof(subtree_path), "%s/cgroup.subtree_control",
root) >= (int)sizeof(subtree_path))
return "unknown";
if (access(ctrl_path, R_OK) < 0 || access(subtree_path, W_OK) < 0)
return "absent";
return "delegated";
}
/* Report the limit that is actually in force, not whether something could set
* one. "max" (the kernel default) and an unreadable file both mean unlimited. */
static void cgroup_effective_limit(const char *root, const char *name,
char *out, size_t out_sz) {
char path[PATH_MAX];
if (snprintf(out, out_sz, "unknown") >= (int)out_sz)
return;
if (root == NULL ||
snprintf(path, sizeof(path), "%s/%s", root, name) >= (int)sizeof(path))
return;
if (read_text_file_trimmed(path, out, out_sz) < 0)
(void)snprintf(out, out_sz, "unreadable");
}
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 set_parent_death_signal(void) {
if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0)
die("PR_SET_PDEATHSIG");
}
/* Fork-style clone: the child gets a private copy of memory (its stack
* included) and of the file table, returns 0 like fork(), and is reaped with
* SIGCHLD. glibc and musl do not refresh their cached thread ID in a child
* created this way; PID 1 never calls raise(), abort() or pthread_kill(),
* which are the functions that read it. Every supported architecture takes
* (flags, stack, parent_tid, child_tid or tls, ...); a NULL stack means the
* child continues on its copy of the current one. */
static pid_t sp_clone_namespace_init(unsigned long namespace_flags) {
return (pid_t)syscall(SYS_clone, namespace_flags | SIGCHLD, 0UL, 0UL, 0UL,
0UL);
}
static void arm_parent_death_signal(pid_t expected_parent) {
set_parent_death_signal();
if (getppid() != expected_parent)
_exit(1);
}
/* PID 1 cannot use arm_parent_death_signal(): its parent (the broker) lives
* outside the new PID namespace, so getppid() returns 0 whether or not the
* broker is alive. Instead the broker keeps the write end of a liveness pipe
* open for its lifetime; POLLHUP on the read end after arming
* PR_SET_PDEATHSIG means the broker died inside the clone-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: broker 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);
}
#include <linux/landlock.h>
#define SP_HAVE_LANDLOCK 1
struct fs_sandbox {
char resolved_cwd[PATH_MAX];
/* Canonical host path of the staging tmpfs. The sandbox tree is assembled
* under it in the host view, then a single pivot_root into final_root
* detaches both the host root and the staging tree (M7, 2026-10-03). */
char staging[PATH_MAX];
char final_root[PATH_MAX];
int staging_root_fd;
struct sp_mount_scope scopes[3];
size_t scope_count;
int cwd_readonly_fallback;
int readonly_binds_enforced;
/* --rootfs without a setup script: add a generated /etc/resolv.conf when
* the image has none (a setup script may want to write its own). */
int provide_rootfs_resolver;
};
/* out = sandbox->staging + name, where name starts with '/'. */
static void sp_staging_path(const struct fs_sandbox *sandbox, const char *name,
char *out, size_t size) {
if (snprintf(out, size, "%s%s", sandbox->staging, name) >= (int)size)
die("staging path");
}
static void sp_mount_scope_init(struct sp_mount_scope *scope) {
memset(scope, 0, sizeof(*scope));
scope->source_fd = -1;
scope->target_fd = -1;
scope->upper_fd = -1;
scope->work_fd = -1;
}
static void sp_fs_sandbox_init(struct fs_sandbox *sandbox) {
memset(sandbox, 0, sizeof(*sandbox));
sandbox->staging_root_fd = -1;
for (size_t i = 0; i < sizeof(sandbox->scopes) / sizeof(sandbox->scopes[0]);
++i)
sp_mount_scope_init(&sandbox->scopes[i]);
}
static void sp_close_fs_scopes(struct fs_sandbox *sandbox) {
sp_close_owned_fd_nointr(&sandbox->staging_root_fd);
for (size_t i = 0; i < sandbox->scope_count; ++i) {
struct sp_mount_scope *scope = &sandbox->scopes[i];
sp_close_owned_fd_nointr(&scope->source_fd);
sp_close_owned_fd_nointr(&scope->target_fd);
sp_close_owned_fd_nointr(&scope->upper_fd);
sp_close_owned_fd_nointr(&scope->work_fd);
}
}
static enum sp_upper_mode sp_view_upper_mode(enum sp_view_mode mode) {
if (mode == SP_VIEW_COW_DISPOSABLE)
return SP_UPPER_DISPOSABLE;
if (mode == SP_VIEW_COW_RETAINED)
return SP_UPPER_RETAINED;
return SP_UPPER_NONE;
}
static int sp_add_fs_scope(struct fs_sandbox *sandbox,
enum sp_scope_kind kind,
const struct sp_bind_spec *bind,
enum sp_bind_mode mode,
enum sp_upper_mode upper_mode,
const char *source, char *error,
size_t error_size) {
struct sp_mount_scope *scope = &sandbox->scopes[sandbox->scope_count++];
scope->bind = bind;
scope->kind = kind;
scope->mode = mode;
scope->upper_mode = upper_mode;
scope->source_fd = sp_open_directory_nosymlinks(source);
if (scope->source_fd < 0) {
snprintf(error, error_size, "open %s source",
kind == SP_SCOPE_ROOTFS ? "rootfs"
: kind == SP_SCOPE_BIND ? "bind"
: "workspace");
return -1;
}
return 0;
}
static int sp_open_scope_storage(int storage_fd, const char *name,
struct sp_mount_scope *scope, char *error,
size_t error_size) {
char upper[64];
char work[64];
struct stat source_stat;
int upper_chmod_fd = -1;
if (snprintf(upper, sizeof(upper), "/%s-upper", name) >=
(int)sizeof(upper) ||
snprintf(work, sizeof(work), "/%s-work", name) >= (int)sizeof(work)) {
errno = ENAMETOOLONG;
snprintf(error, error_size, "scope storage name too long");
return -1;
}
if ((mkdirat(storage_fd, upper + 1, 0700) < 0 && errno != EEXIST) ||
(mkdirat(storage_fd, work + 1, 0700) < 0 && errno != EEXIST)) {
snprintf(error, error_size, "create %s overlay storage", name);
return -1;
}
scope->upper_fd = sp_open_directory_beneath(storage_fd, upper);
if (scope->upper_fd < 0) {
snprintf(error, error_size, "open %s upper directory", name);
return -1;
}
/* Overlayfs exposes the upper directory inode as the merged root. Keep the
* backing storage private through its mode-0700 parent, but make the visible
* overlay root honor the source root's traversal policy. */
if (fstat(scope->source_fd, &source_stat) < 0 ||
(upper_chmod_fd = openat(scope->upper_fd, ".",
O_RDONLY | O_DIRECTORY | O_CLOEXEC)) < 0 ||
fchmod(upper_chmod_fd, source_stat.st_mode & 07777) < 0) {
int saved = errno;
sp_close_owned_fd_nointr(&upper_chmod_fd);
errno = saved;
snprintf(error, error_size, "set %s overlay root mode", name);
return -1;
}
sp_close_owned_fd_nointr(&upper_chmod_fd);
scope->work_fd = sp_open_directory_beneath(storage_fd, work);
if (scope->work_fd < 0) {
snprintf(error, error_size, "open %s work directory", name);
return -1;
}
return 0;
}
static int sp_acquire_fs_scopes(struct fs_sandbox *sandbox,
const struct sp_fs_config *config,
const char *cwd, const char *scratch,
int approved_retained_fd,
char *error, size_t error_size) {
int disposable_fd = -1;
int retained_fd = -1;
int need_retained = 0;
if (config->rootfs_path != NULL &&
sp_add_fs_scope(sandbox, SP_SCOPE_ROOTFS, NULL,
config->rootfs_view == SP_VIEW_READONLY ? SP_BIND_RO
: SP_BIND_COW,
sp_view_upper_mode(config->rootfs_view),
config->rootfs_path, error, error_size) < 0)
goto fail;
if (sp_add_fs_scope(
sandbox, SP_SCOPE_WORKSPACE, NULL,
config->workspace_view == SP_VIEW_READONLY ? SP_BIND_RO
: SP_BIND_COW,
sp_view_upper_mode(config->workspace_view), cwd, error,
error_size) < 0)
goto fail;
if (config->bind_enabled &&
sp_add_fs_scope(sandbox, SP_SCOPE_BIND, &config->bind,
config->bind.mode,
config->bind.mode == SP_BIND_COW ? SP_UPPER_DISPOSABLE
: SP_UPPER_NONE,
config->bind.source, error, error_size) < 0)
goto fail;
for (size_t i = 0; i < sandbox->scope_count; ++i) {
need_retained |= sandbox->scopes[i].upper_mode == SP_UPPER_RETAINED;
}
/* The namespace child made every mount private right after unshare(). */
if (mount("tmpfs", scratch, "tmpfs", MS_NODEV | MS_NOSUID,
"mode=0700,size=128m") < 0) {
snprintf(error, error_size, "mount filesystem staging storage");
goto fail;
}
disposable_fd = sp_open_directory_nosymlinks(scratch);
if (disposable_fd < 0) {
snprintf(error, error_size, "open filesystem staging storage");
goto fail;
}
if (need_retained) {
struct stat approved;
struct stat reopened;
retained_fd =
sp_open_directory_nosymlinks(config->retained_directory);
if (retained_fd < 0) {
snprintf(error, error_size, "open retained overlay storage");
goto fail;
}
if (fstat(approved_retained_fd, &approved) < 0 ||
fstat(retained_fd, &reopened) < 0) {
snprintf(error, error_size, "retained directory changed after locking");
goto fail;
}
if (approved.st_dev != reopened.st_dev || approved.st_ino != reopened.st_ino) {
errno = ESTALE;
snprintf(error, error_size, "retained directory changed after locking");
goto fail;
}
}
for (size_t i = 0; i < sandbox->scope_count; ++i) {
struct sp_mount_scope *scope = &sandbox->scopes[i];
char name[48];
int storage_fd;
if (scope->upper_mode == SP_UPPER_NONE)
continue;
if (scope->kind == SP_SCOPE_ROOTFS)
snprintf(name, sizeof(name), "rootfs");
else if (scope->kind == SP_SCOPE_WORKSPACE)
snprintf(name, sizeof(name), "workspace");
else
snprintf(name, sizeof(name), "bind-%s", scope->bind->name);
storage_fd = scope->upper_mode == SP_UPPER_RETAINED ? retained_fd
: disposable_fd;
if (sp_open_scope_storage(storage_fd, name, scope, error, error_size) < 0)
goto fail;
}
sp_close_owned_fd_nointr(&disposable_fd);
sp_close_owned_fd_nointr(&retained_fd);
return 0;
fail: {
int saved = errno;
sp_close_owned_fd_nointr(&disposable_fd);
sp_close_owned_fd_nointr(&retained_fd);
sp_close_fs_scopes(sandbox);
errno = saved;
return -1;
}
}
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 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 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;
size_t len = strlen(path);
if (len >= sizeof(tmp))
die("normalize path");
memcpy(tmp, path, len + 1);
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");
}
}
/* Clone dirfd/path (flags: AT_EMPTY_PATH for a descriptor, AT_RECURSIVE for
* submounts), set attr on the detached clone and attach it at target_fd. The
* attributes are recursive exactly when the clone is, so every cloned submount
* is protected before it becomes visible. Returns 0, or -1 with errno set. */
static int sp_attach_clone(int dirfd, const char *path, unsigned int flags,
const struct mount_attr *attr, int target_fd) {
int tree_fd = (int)syscall(SYS_open_tree, dirfd, path,
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | flags);
int rc = tree_fd < 0 ||
syscall(SYS_mount_setattr, tree_fd, "",
AT_EMPTY_PATH | (flags & AT_RECURSIVE), attr,
sizeof(*attr)) < 0 ||
syscall(SYS_move_mount, tree_fd, "", target_fd, "",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH) < 0
? -1 : 0;
int saved = errno;
if (tree_fd >= 0)
close(tree_fd);
errno = saved;
return rc;
}
/* The caller has already checked that src exists; a source that disappears
* since still fails below. Protect all cloned submounts before attachment;
* MS_REC on a legacy bind-remount only changes the top mount. */
static void bind_mount_dir_readonly(const char *src, const char *dst) {
struct mount_attr attr = {
.attr_set = MOUNT_ATTR_RDONLY | MOUNT_ATTR_NOSUID,
};
mkdir_parents(dst, 0755);
int target_fd = sp_open_directory_nosymlinks(dst);
if (target_fd < 0 ||
sp_attach_clone(AT_FDCWD, src, AT_RECURSIVE, &attr, target_fd) < 0)
die(src);
close(target_fd);
}
/* dst's parent directory must already exist. */
static void bind_mount_file_in_dir(const char *src, const char *dst,
int nosuid) {
if (!path_exists(src))
return;
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);
}
}
}
/* Mountpoints directly beneath the final root. That root can be a
* caller-supplied rootfs or an overlay of the host root, so a component may be
* a symlink; mount(2) would follow it out of the final root. Create the
* directory if missing, then re-open it with RESOLVE_BENEATH|NO_SYMLINKS
* (mkdirat returns EEXIST for an existing symlink too) and mount onto the fd. */
static int sp_open_final_mountpoint(const struct fs_sandbox *sandbox,
const char *path, mode_t mode) {
int fd;
if (mkdirat(sandbox->staging_root_fd, path + 1, mode) < 0 && errno != EEXIST)
die(path);
fd = sp_open_directory_beneath(sandbox->staging_root_fd, path);
if (fd < 0)
die(path);
return fd;
}
/* options holds key/value pairs, NULL-terminated. */
static void sp_mount_new_fs_at(int target_fd, const char *fstype,
const char *const *options,
unsigned int attrs, const char *what) {
int fs_fd = (int)syscall(SYS_fsopen, fstype, FSOPEN_CLOEXEC);
int mnt_fd = -1;
if (fs_fd < 0)
die(what);
for (size_t i = 0; options[i] != NULL; i += 2) {
if (syscall(SYS_fsconfig, fs_fd, FSCONFIG_SET_STRING, options[i],
options[i + 1], 0) < 0)
die(what);
}
if (syscall(SYS_fsconfig, fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0)
die(what);
mnt_fd = (int)syscall(SYS_fsmount, fs_fd, FSMOUNT_CLOEXEC, attrs);
if (mnt_fd < 0)
die(what);
if (syscall(SYS_move_mount, mnt_fd, "", target_fd, "",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH) < 0)
die(what);
close(mnt_fd);
close(fs_fd);
}
static int move_staging_proc(const struct fs_sandbox *sandbox) {
char staging_proc[PATH_MAX];
sp_staging_path(sandbox, "/proc", staging_proc, sizeof(staging_proc));
int target_fd = sp_open_final_mountpoint(sandbox, "/proc", 0555);
int tree_fd = (int)syscall(SYS_open_tree, AT_FDCWD, staging_proc,
OPEN_TREE_CLOEXEC);
if (tree_fd < 0 ||
syscall(SYS_move_mount, tree_fd, "", target_fd, "",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH) < 0)
die("mount proc");
close(target_fd);
return tree_fd;
}
static void mount_private_tmp(const struct fs_sandbox *sandbox) {
static const char *const options[] = {"mode", "1777", "size", "64m", NULL};
int target_fd = sp_open_final_mountpoint(sandbox, "/tmp", 01777);
sp_mount_new_fs_at(target_fd, "tmpfs", options,
MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV, "mount tmpfs /tmp");
close(target_fd);
}
static void mount_minimal_dev(const struct fs_sandbox *sandbox) {
/* /dev/random is a symlink to urandom below: since Linux 5.6 they differ
* only before the CRNG is initialized, long before a sandbox starts. Each
* bind is a mount on the startup path; /dev/full is not provided. */
static const char *const dev_files[] = {
"/dev/null", "/dev/zero", "/dev/urandom",
};
struct mount_attr attr = {.attr_set = MOUNT_ATTR_NOSUID};
char dev_min[PATH_MAX];
sp_staging_path(sandbox, "/dev-min", dev_min, sizeof(dev_min));
int target_fd = sp_open_final_mountpoint(sandbox, "/dev", 0755);
int tree_fd = (int)syscall(SYS_open_tree, AT_FDCWD, dev_min,
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
char dev_root[64], path[PATH_MAX];
if (tree_fd < 0)
die("mount /dev");
if (syscall(SYS_move_mount, tree_fd, "", target_fd, "",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH) < 0)
die("mount /dev");
close(target_fd);
snprintf(dev_root, sizeof(dev_root), "/proc/self/fd/%d", tree_fd);
/* Populate only the attached view; /dev-min has no child mounts to clone.
* Sources are the host's device nodes. */
for (size_t i = 0; i < sizeof(dev_files) / sizeof(dev_files[0]); ++i) {
char dst[PATH_MAX];
path_append(dst, sizeof(dst), dev_root, dev_files[i] + 4);
bind_mount_file_in_dir(dev_files[i], dst, 0);
}
/* /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) {
path_append(path, sizeof(path), dev_root, "/tty");
bind_mount_file_in_dir("/dev/tty", path, 0);
}
path_append(path, sizeof(path), dev_root, "/pts");
mkdir_if_missing(path, 0755);
if (mount("devpts", path, "devpts",
MS_NOSUID | MS_NOEXEC, "newinstance,ptmxmode=0666,mode=0620") < 0)
die("mount devpts");
unlinkat(tree_fd, "ptmx", 0);
if (symlinkat("pts/ptmx", tree_fd, "ptmx") < 0)
die("symlink /dev/ptmx");
if (symlinkat("urandom", tree_fd, "random") < 0)
die("symlink /dev/random");
if (syscall(SYS_mount_setattr, tree_fd, "", AT_EMPTY_PATH | AT_RECURSIVE,
&attr, sizeof(attr)) < 0)
die("mount /dev nosuid");
close(tree_fd);
}
static int sp_try_runtime_alias(const struct fs_sandbox *sandbox,
const char *path, const char *src,
const char *alias) {
char link[PATH_MAX];
struct stat source, target;
if (path_contains(sandbox->resolved_cwd, "/usr") ||
sp_paths_overlap(sandbox->resolved_cwd, path) ||
sp_paths_overlap(sandbox->resolved_cwd, alias))
return 0;
for (size_t i = 0; i < sandbox->scope_count; ++i) {
const struct sp_mount_scope *scope = &sandbox->scopes[i];
if (scope->kind == SP_SCOPE_BIND &&
(sp_paths_overlap(scope->bind->target, path) ||
sp_paths_overlap(scope->bind->target, alias)))
return 0;
}
ssize_t length = readlink(src, link, sizeof(link) - 1);
if (length < 0)
return 0;
link[length] = '\0';
if (strcmp(link, alias) != 0 && strcmp(link, alias + 1) != 0)
return 0;
int fd = sp_open_directory_beneath(sandbox->staging_root_fd, alias);
if (fd < 0)
return 0;
int matches = stat(src, &source) == 0 && fstat(fd, &target) == 0 &&
source.st_dev == target.st_dev && source.st_ino == target.st_ino;
close(fd);
if (!matches)
return 0;
if (symlinkat(alias + 1, sandbox->staging_root_fd, path + 1) < 0)
die(path);
return 1;
}
static void mount_runtime_tree(struct fs_sandbox *sandbox) {
static const struct {
const char *path;
const char *alias;
} runtime_dirs[] = {
{"/usr", NULL}, {"/bin", "/usr/bin"},
{"/sbin", "/usr/sbin"}, {"/lib", "/usr/lib"},
{"/lib64", "/usr/lib64"}, {"/etc", NULL}, {"/nix", NULL},
};
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].path;
if (path_contains(sandbox->resolved_cwd, path))
continue;
path_append(src, sizeof(src), "", path);
path_append(dst, sizeof(dst), sandbox->final_root, path);
if (!path_exists(src))
continue;
if (runtime_dirs[i].alias != NULL &&
sp_try_runtime_alias(sandbox, path, src, runtime_dirs[i].alias))
continue;
bind_mount_dir_readonly(src, dst);
}
}
static const char *generated_resolv_conf(void) {
return egress_mode == EGRESS_SOCKS && socks_proxy.remote_dns
? "nameserver 10.0.0.1\n"
: "nameserver 8.8.8.8\n";
}
/* --rootfs: give an image without /etc/resolv.conf the generated resolver.
* An existing entry (file or symlink) is the image's choice and is left alone.
* The file is only created in a disposable overlay: a read-only rootfs cannot
* take it, and a retained upper would keep a stale copy for later launches. */
static void provide_rootfs_resolv_conf(const struct fs_sandbox *sandbox,
int disposable_upper) {
struct open_how probe = {
.flags = O_PATH | O_NOFOLLOW | O_CLOEXEC,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
};
struct open_how create = {
.flags = O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC,
.mode = 0644,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
};
const char *text = generated_resolv_conf();
size_t len = strlen(text);
int fd = (int)syscall(SYS_openat2, sandbox->staging_root_fd,
"etc/resolv.conf", &probe, sizeof(probe));
if (fd >= 0) {
close(fd);
return;
}
if (errno != ENOENT) {
startup_warn("rootfs /etc/resolv.conf not inspected (%s); DNS is left "
"to the image", strerror(errno));
return;
}
if (!disposable_upper) {
startup_warn("rootfs has no /etc/resolv.conf and its root is not a "
"disposable overlay; DNS is not configured (use "
"--overlay=tmpfs to provide one)");
return;
}
/* Creating under a lower /etc copies it up, which fails (EACCES, EOVERFLOW)
* when the image's ids are not mapped here; such an image launched before
* this feature existed, so treat it like a read-only root. */
if ((mkdirat(sandbox->staging_root_fd, "etc", 0755) < 0 && errno != EEXIST) ||
(fd = (int)syscall(SYS_openat2, sandbox->staging_root_fd,
"etc/resolv.conf", &create, sizeof(create))) < 0) {
startup_warn("cannot create rootfs /etc/resolv.conf (%s); DNS is not "
"configured", strerror(errno));
return;
}
if (write(fd, text, len) != (ssize_t)len)
die("write rootfs /etc/resolv.conf");
close(fd);
}
/* Bind the generated resolver over the final root's /etc/resolv.conf, or
* over the target of its symlink. The link is resolved inside the final root
* (an absolute target is taken relative to it, and ".." cannot climb out),
* and every component is created or opened beneath the root without
* following symlinks, so nothing is created or mounted outside the sandbox
* tree, which is still assembled in the host view. A target that cannot be
* reached that way leaves DNS unconfigured with a warning. */
static void mount_resolv_conf(const struct fs_sandbox *sandbox) {
struct mount_attr attr = {.attr_set = MOUNT_ATTR_NOSUID};
struct open_how dir_how = {
.flags = O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
};
struct open_how file_how = {
.flags = O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC,
.mode = 0644,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
};
char tmp_path[PATH_MAX], link[PATH_MAX], virt[PATH_MAX], joined[PATH_MAX];
int dir_fd, file_fd;
ssize_t len;
sp_staging_path(sandbox, "/resolv.conf.tmp", tmp_path, sizeof(tmp_path));
write_text_file(tmp_path, generated_resolv_conf());
len = readlinkat(sandbox->staging_root_fd, "etc/resolv.conf", link,
sizeof(link) - 1);
if (len < 0) {
memcpy(virt, "/etc/resolv.conf", sizeof("/etc/resolv.conf"));
} else {
link[len] = '\0';
if (snprintf(joined, sizeof(joined), "%s%s", link[0] == '/' ? "" : "/etc/",
link) >= (int)sizeof(joined))
die("resolv.conf link");
normalize_absolute_path(joined, virt, sizeof(virt));
}
char *name = strrchr(virt, '/');
if (name == NULL || name[1] == '\0')
die("resolv.conf target");
*name++ = '\0';
dir_fd = (int)syscall(SYS_openat2, sandbox->staging_root_fd, ".", &dir_how,
sizeof(dir_how));
for (char *save = NULL, *part = strtok_r(virt, "/", &save);
dir_fd >= 0 && part != NULL; part = strtok_r(NULL, "/", &save)) {
int next;
if (mkdirat(dir_fd, part, 0755) < 0 && errno != EEXIST) {
close(dir_fd);
dir_fd = -1;
break;
}
next = (int)syscall(SYS_openat2, dir_fd, part, &dir_how, sizeof(dir_how));
close(dir_fd);
dir_fd = next;
}
file_fd = dir_fd < 0 ? -1
: (int)syscall(SYS_openat2, dir_fd, name, &file_how,
sizeof(file_how));
if (file_fd < 0 && dir_fd >= 0 && (errno == EROFS || errno == EACCES)) {
struct open_how existing = dir_how;
existing.flags = O_PATH | O_NOFOLLOW | O_CLOEXEC;
file_fd = (int)syscall(SYS_openat2, dir_fd, name, &existing,
sizeof(existing));
}
if (dir_fd >= 0)
close(dir_fd);
if (file_fd < 0) {
startup_warn("cannot place /etc/resolv.conf in the sandbox (%s); DNS is "
"not configured", strerror(errno));
return;
}
if (sp_attach_clone(AT_FDCWD, tmp_path, 0, &attr, file_fd) < 0)
die("mount resolv.conf");
close(file_fd);
}
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 struct sp_mount_scope *sp_find_fs_scope(struct fs_sandbox *sandbox,
enum sp_scope_kind kind) {
for (size_t i = 0; i < sandbox->scope_count; ++i) {
if (sandbox->scopes[i].kind == kind)
return &sandbox->scopes[i];
}
return NULL;
}
static int sp_attach_detached(struct sp_mount_scope *scope) {
struct mount_attr attr = {
.attr_set = MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV |
(scope->mode == SP_BIND_RO ? MOUNT_ATTR_RDONLY : 0),
};
if (sp_attach_clone(scope->source_fd, "", AT_EMPTY_PATH, &attr,
scope->target_fd) < 0) {
int saved = errno;
DBG("detached scope attach failed: %s", strerror(saved));
errno = saved;
return -1;
}
scope->attached = 1;
return 0;
}
static int sp_attach_readonly_fallback(const struct fs_sandbox *sandbox,
struct sp_mount_scope *scope,
const char *target) {
struct mount_attr attr = {
.attr_set = MOUNT_ATTR_RDONLY | MOUNT_ATTR_NOSUID,
};
char source[64];
if (snprintf(source, sizeof(source), "/proc/self/fd/%d",
scope->source_fd) >= (int)sizeof(source)) {
errno = ENAMETOOLONG;
return -1;
}
if (mount(source, target, NULL, MS_BIND | MS_REC, NULL) < 0)
return -1;
/* The staging tmpfs is mounted in the host view while the tree is built.
* If the workspace is its parent directory (cwd /tmp), the recursive bind
* copied it: detach that copy so no staging content is visible. */
{
const char *leaf = strrchr(sandbox->staging, '/');
size_t parent_len = leaf == NULL ? 0 : (size_t)(leaf - sandbox->staging);
char copy[PATH_MAX];
if (leaf != NULL && strlen(sandbox->resolved_cwd) == parent_len &&
strncmp(sandbox->resolved_cwd, sandbox->staging, parent_len) == 0) {
if (snprintf(copy, sizeof(copy), "%s%s", target, leaf) >=
(int)sizeof(copy) ||
umount2(copy, MNT_DETACH) < 0) {
int saved = errno;
(void)umount2(target, MNT_DETACH);
errno = saved;
return -1;
}
}
}
int tree_fd = (int)syscall(SYS_open_tree, AT_FDCWD, target,
OPEN_TREE_CLOEXEC);
int rc = tree_fd < 0 ? -1 :
(int)syscall(SYS_mount_setattr, tree_fd, "", AT_EMPTY_PATH | AT_RECURSIVE,
&attr, sizeof(attr));
int saved = errno;
if (tree_fd >= 0)
close(tree_fd);
if (rc < 0) {
(void)umount2(target, MNT_DETACH);
errno = saved;
return -1;
}
scope->attached = 1;
return 1;
}
static int sp_attach_overlay(const struct fs_sandbox *sandbox,
struct sp_mount_scope *scope) {
char options[256];
char staging[PATH_MAX];
int tree_fd;
sp_staging_path(sandbox,
scope->kind == SP_SCOPE_ROOTFS ? "/overlay-rootfs"
: scope->kind == SP_SCOPE_WORKSPACE ? "/overlay-workspace"
: "/overlay-bind",
staging, sizeof(staging));
if (snprintf(options, sizeof(options),
"lowerdir=/proc/self/fd/%d,upperdir=/proc/self/fd/%d,"
"workdir=/proc/self/fd/%d,userxattr",
scope->source_fd, scope->upper_fd, scope->work_fd) >=
(int)sizeof(options)) {
errno = ENAMETOOLONG;
return -1;
}
mkdir_parents(staging, 0755);
/* MS_NOSUID | MS_NODEV here are the mount's final attributes; the mount is
* then moved, not cloned, so they carry over unchanged. */
if (mount("overlay", staging, "overlay", MS_NOSUID | MS_NODEV, options) < 0) {
DBG("mount overlay scope failed: %s", strerror(errno));
return -1;
}
tree_fd = (int)syscall(SYS_open_tree, AT_FDCWD, staging, OPEN_TREE_CLOEXEC);
if (tree_fd < 0) {
DBG("open_tree overlay failed: %s", strerror(errno));
return -1;
}
if (syscall(SYS_move_mount, tree_fd, "", scope->target_fd, "",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH) < 0) {
int saved = errno;
DBG("move_mount overlay failed: %s", strerror(saved));
close(tree_fd);
errno = saved;
return -1;
}
close(tree_fd);
scope->attached = 1;
return 0;
}
static int sp_attach_fs_scope(struct fs_sandbox *sandbox,
struct sp_mount_scope *scope,
const char *target_path,
int allow_readonly_fallback) {
scope->target_fd = scope->kind == SP_SCOPE_ROOTFS
? sp_open_directory_nosymlinks(target_path)
: sp_open_directory_beneath(sandbox->staging_root_fd,
target_path + strlen(sandbox->final_root));
if (scope->target_fd < 0)
return -1;
if (scope->upper_mode != SP_UPPER_NONE &&
sp_attach_overlay(sandbox, scope) == 0)
return 0;
if (scope->upper_mode == SP_UPPER_NONE)
return sp_attach_detached(scope);
int overlay_errno = errno;
if (!allow_readonly_fallback ||
!overlay_fallback_allowed_errno(overlay_errno)) {
errno = overlay_errno;
return -1;
}
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));
scope->mode = SP_BIND_RO;
if (sp_attach_detached(scope) < 0) {
int readonly = sp_attach_readonly_fallback(sandbox, scope, target_path);
if (readonly < 0)
return -1;
sandbox->readonly_binds_enforced = readonly;
}
sandbox->cwd_readonly_fallback = 1;
return 0;
}
static void setup_final_root(struct fs_sandbox *sandbox,
int allow_workspace_fallback) {
struct sp_mount_scope *rootfs =
sp_find_fs_scope(sandbox, SP_SCOPE_ROOTFS);
struct sp_mount_scope *workspace =
sp_find_fs_scope(sandbox, SP_SCOPE_WORKSPACE);
char target[PATH_MAX];
if (rootfs != NULL) {
sp_staging_path(sandbox, "/rootfs", sandbox->final_root,
sizeof(sandbox->final_root));
if (sp_attach_fs_scope(sandbox, rootfs, sandbox->final_root, 0) < 0)
die("attach rootfs");
sandbox->staging_root_fd =
sp_open_directory_nosymlinks(sandbox->final_root);
if (sandbox->staging_root_fd < 0)
die("open staged rootfs");
path_append(target, sizeof(target), sandbox->final_root, "/workspace");
if (mkdirat(sandbox->staging_root_fd, "workspace", 0755) < 0 &&
errno != EEXIST)
die("mkdir workspace");
if (sp_attach_fs_scope(sandbox, workspace, target, 0) < 0)
die("attach workspace");
memcpy(sandbox->resolved_cwd, "/workspace", sizeof("/workspace"));
mount_private_tmp(sandbox);
} else {
/* prepare_fs_sandbox() refuses cwd "/", so the workspace is always a
* subtree placed at its own path inside the synthetic /sandbox. */
sp_staging_path(sandbox, "/sandbox", sandbox->final_root,
sizeof(sandbox->final_root));
if (mount(sandbox->final_root, sandbox->final_root, NULL, MS_BIND, NULL) < 0)
die("mount final host root");
sandbox->staging_root_fd =
sp_open_directory_nosymlinks(sandbox->final_root);
if (sandbox->staging_root_fd < 0)
die("open staged root");
mount_runtime_tree(sandbox);
if (!path_contains(sandbox->resolved_cwd, "/tmp"))
mount_private_tmp(sandbox);
path_append(target, sizeof(target), sandbox->final_root,
sandbox->resolved_cwd);
mkdir_parents(target, 0755);
if (sp_attach_fs_scope(sandbox, workspace, target,
allow_workspace_fallback) < 0)
die("attach cwd");
}
struct sp_mount_scope *bind = sp_find_fs_scope(sandbox, SP_SCOPE_BIND);
if (bind != NULL) {
if (rootfs == NULL &&
sp_paths_overlap(bind->bind->target, sandbox->resolved_cwd)) {
errno = EINVAL;
die("bind target overlaps workspace");
}
path_append(target, sizeof(target), sandbox->final_root, bind->bind->target);
if (sp_attach_fs_scope(sandbox, bind, target, 0) < 0)
die("attach bind volume");
}
mount_minimal_dev(sandbox);
if (rootfs == NULL)
mount_resolv_conf(sandbox);
else if (sandbox->provide_rootfs_resolver)
provide_rootfs_resolv_conf(sandbox,
rootfs->upper_mode == SP_UPPER_DISPOSABLE);
}
static int sp_copy_sealed_script(int source, int proc_fd) {
struct stat before, after;
char buffer[4096];
size_t total = 0;
int sealed = -1;
int readonly = -1;
const int seals = F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL;
if (fstat(source, &before) < 0)
return -1;
if (!S_ISREG(before.st_mode)) {
errno = EINVAL;
return -1;
}
if (before.st_size < 0 || before.st_size > SP_SETUP_SCRIPT_MAX) {
errno = EFBIG;
return -1;
}
sealed = memfd_create("sockpuppet-setup", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (sealed < 0)
return -1;
for (;;) {
ssize_t count = read(source, buffer, sizeof(buffer));
if (count < 0) {
if (errno == EINTR)
continue;
goto out;
}
if (count == 0)
break;
total += (size_t)count;
if (total > SP_SETUP_SCRIPT_MAX) {
errno = EFBIG;
goto out;
}
if (write_all(sealed, buffer, (size_t)count) != count)
goto out;
}
if (fstat(source, &after) < 0)
goto out;
if ((off_t)total != before.st_size || before.st_size != after.st_size ||
before.st_mtim.tv_sec != after.st_mtim.tv_sec ||
before.st_mtim.tv_nsec != after.st_mtim.tv_nsec ||
before.st_ctim.tv_sec != after.st_ctim.tv_sec ||
before.st_ctim.tv_nsec != after.st_ctim.tv_nsec) {
errno = ESTALE;
goto out;
}
if (fcntl(sealed, F_ADD_SEALS, seals) < 0)
goto out;
char path[64];
snprintf(path, sizeof(path), "%sself/fd/%d",
proc_fd == AT_FDCWD ? "/proc/" : "", sealed);
readonly = openat(proc_fd, path, O_RDONLY | O_CLOEXEC);
if (readonly >= 0 && fcntl(readonly, F_GET_SEALS) != seals) {
close(readonly);
readonly = -1;
errno = EPERM;
}
out: {
int saved = errno;
close(sealed);
errno = saved;
return readonly;
}
}
static int sp_open_setup_script(int root_fd, const char *absolute_path,
int proc_fd) {
struct open_how how = {
.flags = O_RDONLY | O_NONBLOCK | O_CLOEXEC,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
};
int source = (int)syscall(SYS_openat2, root_fd, absolute_path + 1,
&how, sizeof(how));
if (source < 0)
return -1;
int sealed = sp_copy_sealed_script(source, proc_fd);
int saved = errno;
close(source);
errno = saved;
return sealed;
}
static int sp_enter_final_root_empty(const struct fs_sandbox *sandbox);
static void prepare_fs_sandbox(struct fs_sandbox *sandbox,
const char *cwd, const char *base,
int allow_workspace_fallback,
const char *guest_script, int *script_fd) {
if (strcmp(cwd, "/") == 0) {
fprintf(stderr, "Refusing to share / as the sandbox workspace; run from a "
"project directory\n");
exit(1);
}
memcpy(sandbox->resolved_cwd, cwd, strlen(cwd) + 1);
sandbox->cwd_readonly_fallback = 0;
sandbox->readonly_binds_enforced = 1;
/* Create each staging directory once, relative to the staging tmpfs, instead
* of re-walking every prefix of base from "/". Parents precede children. */
static const struct {
const char *name;
mode_t mode;
} staging_dirs[] = {
{"proc", 0555}, {"rootfs", 0755},
{"sandbox", 0755}, {"dev-min", 0755},
};
if (realpath(base, sandbox->staging) == NULL)
die("resolve overlay base");
int base_fd = sp_open_directory_nosymlinks(sandbox->staging);
if (base_fd < 0)
die("open overlay base");
for (size_t i = 0; i < sizeof(staging_dirs) / sizeof(staging_dirs[0]); ++i) {
if (mkdirat(base_fd, staging_dirs[i].name, staging_dirs[i].mode) < 0 &&
errno != EEXIST)
die(staging_dirs[i].name);
}
close(base_fd);
{
char staging_proc[PATH_MAX];
sp_staging_path(sandbox, "/proc", staging_proc, sizeof(staging_proc));
if (mount("proc", staging_proc, "proc", MS_NOSUID | MS_NODEV | MS_NOEXEC,
"hidepid=2") < 0)
die("mount staging proc");
}
setup_final_root(sandbox, allow_workspace_fallback);
int proc_fd = move_staging_proc(sandbox);
if (guest_script != NULL) {
*script_fd = sp_open_setup_script(sandbox->staging_root_fd, guest_script,
proc_fd);
if (*script_fd < 0)
die("acquire guest setup script");
}
close(proc_fd);
sp_close_fs_scopes(sandbox);
/* The one root switch: the final root becomes the namespace root, and the
* old root, which is the host tree with the staging tmpfs inside it, is
* detached as a whole. Nothing above the final root stays reachable, so a
* chroot escape from setup or the payload ends at the final root (C11 F1).
* This happens before the TUN handoff so the broker can remove the host
* scratch directory that the staging tmpfs was mounted on. */
if (sp_enter_final_root_empty(sandbox) == 0)
return;
if (chdir(sandbox->final_root) < 0 ||
syscall(SYS_pivot_root, ".", ".") < 0)
die("pivot final root");
if (umount2(".", MNT_DETACH) < 0)
die("detach host and staging trees");
if (chdir("/") < 0)
die("chdir final root");
}
#ifndef UNSHARE_EMPTY_MNTNS
#define UNSHARE_EMPTY_MNTNS 0x00100000 /* Linux 7.1 */
#endif
/* Root switch without pivot_root (Linux 7.1+). Clone the finished final tree
* into an anonymous mount tree, move into a new empty mount namespace (its
* root is an empty nullfs), attach the tree there and make it the root. Only
* the caller's root and cwd are reset, so there is no global task walk; the
* copied namespace holding the host tree and the staging tmpfs is released
* with PID 1's last reference to it. Returns -1 with nothing changed if the
* kernel or an outer policy rejects the empty namespace; the caller then
* uses pivot_root. */
static int sp_enter_final_root_empty(const struct fs_sandbox *sandbox) {
int tree_fd = (int)syscall(SYS_open_tree, AT_FDCWD, sandbox->final_root,
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC |
AT_RECURSIVE);
int root_fd;
if (tree_fd < 0)
die("clone final root");
if (unshare(UNSHARE_EMPTY_MNTNS) < 0) {
int saved = errno;
close(tree_fd);
if (saved == EINVAL || saved == EPERM || saved == EACCES ||
saved == ENOSYS) {
DBG("empty mount namespace unavailable (%s); using pivot_root",
strerror(saved));
return -1;
}
errno = saved;
die("unshare empty mount namespace");
}
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
die("make empty root private");
root_fd = open("/", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (root_fd < 0 ||
syscall(SYS_move_mount, tree_fd, "", root_fd, "",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH) < 0 ||
fchdir(tree_fd) < 0 || chroot(".") < 0 || chdir("/") < 0)
die("enter final root");
close(root_fd);
close(tree_fd);
return 0;
}
static void enter_fs_sandbox(const struct fs_sandbox *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 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) {
int saved = errno;
fprintf(stderr, "[sockpuppet] Landlock open %s: %s\n", path,
strerror(saved));
errno = saved;
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);
if (rc < 0) {
int saved = errno;
fprintf(stderr, "[sockpuppet] Landlock add rule %s: %s\n", path,
strerror(saved));
close(dirfd);
errno = saved;
return rc;
}
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 != NULL && !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;
}
(void)sp_stderr_printf("[sockpuppet] Warning: %s Landlock unavailable (%s); %s\n",
sandbox != NULL ? "child" : "parent", reason,
sandbox != NULL ? "mount-based filesystem confinement remains enforced"
: "broker filesystem-denial layer is absent");
return 0;
}
/* NULL selects the parent's deny-all policy; otherwise add the child's paths.
* Return 1 for installed, 0 for unavailable, -1 for a fatal policy error.
* Only the ABI query may classify kernel absence/disablement as unavailable;
* errors constructing or installing a supported policy are fatal. */
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)
return -1;
/* Read access below "/" covers /dev and its read-only devices; the rules
* below only add write access. */
if (sandbox != NULL &&
(add_landlock_rule(ruleset_fd, "/", read_exec) < 0 ||
add_landlock_rule(ruleset_fd, "/tmp", read_exec | write_rights) < 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)) {
close(ruleset_fd);
return -1;
}
if (sandbox != NULL) {
for (size_t i = 0; i < sandbox->scope_count; ++i) {
const struct sp_mount_scope *scope = &sandbox->scopes[i];
if (!scope->attached || scope->mode == SP_BIND_RO)
continue;
const char *path = scope->kind == SP_SCOPE_ROOTFS ? "/"
: scope->kind == SP_SCOPE_BIND ? scope->bind->target
: sandbox->resolved_cwd;
if (add_landlock_rule(ruleset_fd, path, read_exec | write_rights) < 0) {
close(ruleset_fd);
return -1;
}
}
}
if (sandbox == NULL) {
/* Verify the denial probe's target is readable before this policy. This
* avoids treating a missing file or an inherited restriction as evidence. */
int probe_fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (probe_fd < 0) {
close(ruleset_fd);
return -1;
}
close(probe_fd);
}
if (ensure_no_new_privs() < 0 ||
syscall(__NR_landlock_restrict_self, ruleset_fd, 0) < 0) {
int saved = errno;
close(ruleset_fd);
errno = saved;
return -1;
}
close(ruleset_fd);
return 1;
#else
return landlock_unavailable(sandbox, "not compiled in");
#endif
}
/* Called only after this role's successful confinement/probe sequence. The
* parent's record does not attest to child setup or inherited capability sets. */
/* A caller's payload profile (--seccomp-file): raw struct sock_filter records
* for this architecture, read on the host before the sandbox clone, so PID 1
* gets it in its copy of memory and no descriptor crosses over. It replaces the
* default and binds PID 1 too. The floor goes first and keeps its prologue,
* which kills foreign-ABI and x32 calls whatever the profile returns. */
static struct sock_filter sp_profile[BPF_MAXINSNS];
static unsigned short sp_profile_len; /* 0: no profile, install the default */
static void startup_confinement_assurance(const struct fs_sandbox *sandbox,
int landlock) {
int child = sandbox != NULL;
#ifdef __NR_clone3
const char *clone3_probe = "passed";
#else
const char *clone3_probe = "unavailable-build";
#endif
(void)sp_stderr_printf(
"sp_startup_assurance\trole\t%s\tphase\tconfined\tuserns\t%s\t"
"mounts\t%s\treadonly\t%s\tlandlock\t%s\tlandlock_probe\t%s\t"
"seccomp\t%s\tseccomp_probe\t%s\tclone3_probe\t%s\t"
"caps_dropped\t%s\tno_new_privs\tset\tvnet_gso\tprofile-validated\t"
"tun_offload\tCSUM|TSO4\n",
child ? "child" : "parent", child ? "created" : "not-created",
child ? "enforced" : "not-applicable",
child ? (sandbox->readonly_binds_enforced ? "mounts" : "landlock-fallback")
: "not-applicable",
landlock > 0 ? "installed" : "unavailable",
!child && landlock > 0 ? "passed" : "not-probed",
child && sp_profile_len != 0 ? "profile" : "installed", child ? "not-probed" : "passed", child ? "not-probed" : clone3_probe,
child ? "cleared-verified" : "not-performed");
}
static int parent_harden_probe_landlock(void) {
errno = 0;
int fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0) {
close(fd);
fprintf(stderr,
"sockpuppet: parent hardening probe failed: Landlock allowed "
"open(/)\n");
errno = EPERM;
return -1;
}
if (errno != EACCES && errno != EPERM) {
int saved = errno;
fprintf(stderr,
"sockpuppet: parent hardening probe failed: open(/) "
"returned %s\n",
strerror(saved));
errno = saved;
return -1;
}
(void)sp_stderr_printf(
"sockpuppet: parent hardening probe: Landlock denied open\n");
return 0;
}
static int parent_harden_expect_denied(const char *name, long result) {
int saved = errno;
if (result == -1 && saved == EPERM)
return 0;
(void)sp_stderr_printf(
"sockpuppet: parent hardening probe failed: %s denial was %s (rc=%ld)\n",
name, strerror(saved), result);
errno = saved ? saved : EPERM;
return -1;
}
/* Always-on post-seccomp probes. The extended argument-level matrix lives in
* tests/tcp_refactor/seccomp_policy.c, which installs the same built filter
* through apply_parent_seccomp() in a forked process. */
static int parent_harden_probe_seccomp(void) {
/* Invalid arguments make these safe even if the filter is broken: no child
* can be created and no socket can be changed. Filter denial must be EPERM,
* not the syscall's own EBADF/EINVAL/ENOSYS validation result. */
#ifdef __NR_clone3
errno = 0;
if (parent_harden_expect_denied("clone3",
syscall(__NR_clone3, NULL, 0)) < 0)
return -1;
(void)sp_stderr_printf(
"sockpuppet: parent hardening probe: seccomp denied clone3\n");
#else
(void)sp_stderr_printf(
"sockpuppet: parent hardening probe: clone3 unavailable at build time\n");
#endif
int one = 1;
errno = 0;
if (parent_harden_expect_denied(
"SO_KEEPALIVE", setsockopt(-1, SOL_SOCKET, SO_KEEPALIVE,
&one, sizeof(one))) < 0)
return -1;
(void)sp_stderr_printf(
"sockpuppet: parent hardening probe: seccomp denied SO_KEEPALIVE\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)
#define SP_SECCOMP_KILL SECCOMP_RET_KILL_PROCESS
#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
/* Instructions for built-in filter templates. The prologue kills a call from a
* foreign ABI (on x86_64 also an x32 number) and leaves the syscall number in
* the accumulator; each rule returns `action` for syscall `nr` and otherwise
* falls through to the next rule, so rules never jump across one another. */
#define SP_BPF_LOAD(field) BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, field))
#define SP_BPF_RET(action) BPF_STMT(BPF_RET | BPF_K, (action))
#if defined(__x86_64__)
#define SP_BPF_ARCH_PROLOGUE \
SP_BPF_LOAD(arch), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SP_AUDIT_ARCH, 1, 0), \
SP_BPF_RET(SP_SECCOMP_KILL), SP_BPF_LOAD(nr), \
BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, __X32_SYSCALL_BIT, 0, 1), SP_BPF_RET(SP_SECCOMP_KILL)
#else
#define SP_BPF_ARCH_PROLOGUE \
SP_BPF_LOAD(arch), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SP_AUDIT_ARCH, 1, 0), \
SP_BPF_RET(SP_SECCOMP_KILL), SP_BPF_LOAD(nr)
#endif
#define SP_BPF_NR_RET(nr, action) \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (nr), 0, 1), SP_BPF_RET(action)
/* Loads the high 32 bits of a 64-bit argument (all supported ABIs are LE). */
#define SP_BPF_LOAD_HIGH(field) \
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, field) + sizeof(uint32_t))
/* Returns `action` when `syscall`'s argument `arg` has low 32 bits equal to
* `value`; otherwise reloads the syscall number and falls through. */
#define SP_BPF_ARG_EQ_RET(syscall, arg, value, action) \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (syscall), 0, 3), SP_BPF_LOAD(args[arg]), \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (value), 0, 1), SP_BPF_RET(action), SP_BPF_LOAD(nr)
/* The kernel copies a program at install; it never writes through `filter`. */
#define SP_BPF_PROGRAM(template) \
((struct sock_fprog){.len = sizeof(template) / sizeof((template)[0]), \
.filter = (struct sock_filter *)(template)})
enum {
SP_PARENT_SECCOMP_MAX_FILTER = 256,
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,
};
#define SP_BPF_JEQ(value, jt, jf) BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (value), (jt), (jf))
#define SP_PARENT_DENY SP_BPF_RET(SP_PARENT_SECCOMP_ERRNO)
#define SP_PARENT_ALLOW(nr) SP_BPF_NR_RET(nr, SECCOMP_RET_ALLOW)
/* Allows the call when argument a1 equals first and a2 equals second (low
* words); otherwise falls through to the next pair. */
#define SP_BPF_ALLOW_PAIR(a1, first, a2, second) \
SP_BPF_LOAD(args[a1]), SP_BPF_JEQ(first, 0, 3), SP_BPF_LOAD(args[a2]), \
SP_BPF_JEQ(second, 0, 1), SP_BPF_RET(SECCOMP_RET_ALLOW)
#if SP_AUDIT_ARCH != 0
/* Broker policy: allow a named set, deny the rest with EPERM. The filter is
* assembled at install from fixed segments, in this order; a disabled option
* contributes no segment, and runtime values are written into named slots.
* Each segment starts with "not this syscall: skip the segment", and every
* other jump stays inside its segment. */
static const struct sock_filter sp_parent_head[] = {
SP_BPF_ARCH_PROLOGUE,
SP_PARENT_ALLOW(__NR_read), SP_PARENT_ALLOW(__NR_write),
SP_PARENT_ALLOW(__NR_readv), SP_PARENT_ALLOW(__NR_writev),
SP_PARENT_ALLOW(__NR_close), SP_PARENT_ALLOW(__NR_exit),
SP_PARENT_ALLOW(__NR_exit_group), SP_PARENT_ALLOW(__NR_wait4),
SP_PARENT_ALLOW(__NR_epoll_ctl), SP_PARENT_ALLOW(__NR_epoll_pwait),
SP_PARENT_ALLOW(__NR_fcntl), SP_PARENT_ALLOW(__NR_getrandom),
SP_PARENT_ALLOW(__NR_clock_gettime), SP_PARENT_ALLOW(__NR_rt_sigreturn),
SP_PARENT_ALLOW(__NR_restart_syscall), SP_PARENT_ALLOW(__NR_accept4),
SP_PARENT_ALLOW(__NR_getpeername), SP_PARENT_ALLOW(__NR_recvfrom),
SP_PARENT_ALLOW(__NR_recvmsg), SP_PARENT_ALLOW(__NR_sendto),
SP_PARENT_ALLOW(__NR_sendmsg), SP_PARENT_ALLOW(__NR_shutdown),
/* x86_64 has no recv/send; the generic ABI (aarch64, riscv64) has no
* epoll_wait or time; epoll_pwait2 needs 5.11 headers. */
#ifdef __NR_epoll_wait
SP_PARENT_ALLOW(__NR_epoll_wait),
#endif
#ifdef __NR_epoll_pwait2
SP_PARENT_ALLOW(__NR_epoll_pwait2),
#endif
#ifdef __NR_time
SP_PARENT_ALLOW(__NR_time),
#endif
#ifdef __NR_recv
SP_PARENT_ALLOW(__NR_recv),
#endif
#ifdef __NR_send
SP_PARENT_ALLOW(__NR_send),
#endif
};
/* Shutdown may release exactly the startup map arena; mmap/mremap and any
* other unmap stay denied. Its address is written into the two slots. */
enum { SP_PARENT_MUNMAP_LOW = 2, SP_PARENT_MUNMAP_HIGH = 5 };
static const struct sock_filter sp_parent_munmap[] = {
SP_BPF_JEQ(__NR_munmap, 0, 13),
SP_BPF_LOAD(args[0]), SP_BPF_JEQ(0, 1, 0), SP_PARENT_DENY,
SP_BPF_LOAD_HIGH(args[0]), SP_BPF_JEQ(0, 1, 0), SP_PARENT_DENY,
SP_BPF_LOAD(args[1]), SP_BPF_JEQ(sizeof(struct sp_map_storage), 1, 0), SP_PARENT_DENY,
SP_BPF_LOAD_HIGH(args[1]), SP_BPF_JEQ(0, 1, 0), SP_PARENT_DENY,
SP_BPF_RET(SECCOMP_RET_ALLOW),
};
/* Fatal-teardown signalling: SIGKILL to exactly the namespace child, by
* pidfd_send_signal() or kill(). The syscall and target go into the slots. */
enum { SP_PARENT_SIGNAL_NR = 0, SP_PARENT_SIGNAL_TARGET = 2 };
static const struct sock_filter sp_parent_signal[] = {
SP_BPF_JEQ(0, 0, 6),
SP_BPF_LOAD(args[0]), SP_BPF_JEQ(0, 0, 2),
SP_BPF_LOAD(args[1]), SP_BPF_JEQ(SIGKILL, 1, 0),
SP_PARENT_DENY, SP_BPF_RET(SECCOMP_RET_ALLOW),
};
/* Interactive sessions: window size and terminal mode ioctls only. The
* TCSETS*2 values are glibc's termios2 requests (same on all supported ABIs). */
static const struct sock_filter sp_parent_ioctl[] = {
SP_BPF_JEQ(__NR_ioctl, 0, 11),
SP_BPF_LOAD(args[1]),
SP_BPF_JEQ(TIOCGWINSZ, 8, 0), SP_BPF_JEQ(TIOCSWINSZ, 7, 0),
SP_BPF_JEQ(TCSETS, 6, 0), SP_BPF_JEQ(TCSETSW, 5, 0),
SP_BPF_JEQ(TCSETSF, 4, 0), SP_BPF_JEQ(SP_IOCTL_TCSETS2, 3, 0),
SP_BPF_JEQ(SP_IOCTL_TCSETSW2, 2, 0), SP_BPF_JEQ(SP_IOCTL_TCSETSF2, 1, 0),
SP_PARENT_DENY, SP_BPF_RET(SECCOMP_RET_ALLOW),
};
/* Host sockets: the option, mmsg and readback calls the broker makes itself. */
enum { SP_SETSOCKOPT_PAIRS = 5, SP_GETSOCKOPT_PAIRS = 10 };
static const struct sock_filter sp_parent_sockets[] = {
/* setsockopt: int-sized values only, for these (level, option) pairs. */
SP_BPF_JEQ(__NR_setsockopt, 0, 3 + 5 * SP_SETSOCKOPT_PAIRS),
SP_BPF_LOAD(args[4]), SP_BPF_JEQ(sizeof(int), 0, 5 * SP_SETSOCKOPT_PAIRS),
SP_BPF_ALLOW_PAIR(1, IPPROTO_TCP, 2, TCP_NODELAY),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_SNDBUF),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_RCVBUF),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_RCVLOWAT),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_PEEK_OFF),
SP_PARENT_DENY,
SP_BPF_JEQ(__NR_getsockopt, 0, 1 + 5 * SP_GETSOCKOPT_PAIRS),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_ERROR),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_DOMAIN),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_TYPE),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_PROTOCOL),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_SNDBUF),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_RCVBUF),
SP_BPF_ALLOW_PAIR(1, IPPROTO_TCP, 2, TCP_NODELAY),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_RCVLOWAT),
SP_BPF_ALLOW_PAIR(1, SOL_SOCKET, 2, SO_PEEK_OFF),
SP_BPF_ALLOW_PAIR(1, IPPROTO_IP, 2, IP_MTU),
SP_PARENT_DENY,
/* recvmmsg: at most SP_UDP_MMSG_MAX messages, these flags, no timeout. */
SP_BPF_JEQ(__NR_recvmmsg, 0, 10),
SP_BPF_LOAD(args[2]), BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, SP_UDP_MMSG_MAX, 6, 0),
SP_BPF_LOAD(args[3]), SP_BPF_JEQ(MSG_DONTWAIT | MSG_CMSG_CLOEXEC, 0, 4),
SP_BPF_LOAD(args[4]), SP_BPF_JEQ(0, 0, 2),
SP_BPF_LOAD_HIGH(args[4]), SP_BPF_JEQ(0, 1, 0),
SP_PARENT_DENY, SP_BPF_RET(SECCOMP_RET_ALLOW),
/* sendmmsg: at most SP_UDP_MMSG_MAX messages, these flags. */
SP_BPF_JEQ(__NR_sendmmsg, 0, 6),
SP_BPF_LOAD(args[2]), BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, SP_UDP_MMSG_MAX, 2, 0),
SP_BPF_LOAD(args[3]), SP_BPF_JEQ(MSG_DONTWAIT | MSG_NOSIGNAL, 1, 0),
SP_PARENT_DENY, SP_BPF_RET(SECCOMP_RET_ALLOW),
};
_Static_assert(sizeof(sp_parent_sockets) / sizeof(sp_parent_sockets[0]) ==
4 + 5 * SP_SETSOCKOPT_PAIRS + 2 + 5 * SP_GETSOCKOPT_PAIRS + 11 + 7,
"socket option pair counts must match the segment");
/* Direct egress: IPv4 stream/datagram sockets (CLOEXEC/NONBLOCK allowed),
* then connect, bind and getsockname. */
static const struct sock_filter sp_parent_outbound[] = {
SP_BPF_JEQ(__NR_socket, 0, 8),
SP_BPF_LOAD(args[0]), SP_BPF_JEQ(AF_INET, 0, 4),
SP_BPF_LOAD(args[1]),
BPF_STMT(BPF_ALU | BPF_AND | BPF_K, ~((unsigned int)SOCK_CLOEXEC | (unsigned int)SOCK_NONBLOCK)),
SP_BPF_JEQ(SOCK_STREAM, 2, 0), SP_BPF_JEQ(SOCK_DGRAM, 1, 0),
SP_PARENT_DENY, SP_BPF_RET(SECCOMP_RET_ALLOW),
SP_PARENT_ALLOW(__NR_connect), SP_PARENT_ALLOW(__NR_bind), SP_PARENT_ALLOW(__NR_getsockname),
};
static size_t sp_bpf_append(struct sock_filter *out, size_t used, const struct sock_filter *segment,
size_t count) {
assert(count <= SP_PARENT_SECCOMP_MAX_FILTER - used);
memcpy(out + used, segment, count * sizeof(*segment));
return used + count;
}
/* Writes a runtime value into a comparison slot of a just-copied segment. */
static void sp_bpf_set_slot(struct sock_filter *insn, uint32_t value) {
assert(insn->code == (BPF_JMP | BPF_JEQ | BPF_K));
insn->k = value;
}
#define SP_BPF_APPEND(out, used, segment) \
sp_bpf_append(out, used, segment, sizeof(segment) / sizeof((segment)[0]))
static size_t sp_parent_append_signal(struct sock_filter *out, size_t used, unsigned int nr,
unsigned int target) {
size_t next = SP_BPF_APPEND(out, used, sp_parent_signal);
sp_bpf_set_slot(&out[used + SP_PARENT_SIGNAL_NR], nr);
sp_bpf_set_slot(&out[used + SP_PARENT_SIGNAL_TARGET], target);
return next;
}
/* Assembles the broker filter for cfg into out[SP_PARENT_SECCOMP_MAX_FILTER]
* and returns its length. Targets compare low words, as the kernel's
* seccomp_data does for int arguments. */
static unsigned short sp_parent_seccomp_filter(const struct parent_harden_config *cfg,
struct sock_filter *out) {
size_t used = SP_BPF_APPEND(out, 0, sp_parent_head);
if (g_map_storage != NULL) {
uint64_t address = (uint64_t)(uintptr_t)g_map_storage;
size_t at = used;
used = SP_BPF_APPEND(out, used, sp_parent_munmap);
sp_bpf_set_slot(&out[at + SP_PARENT_MUNMAP_LOW], (uint32_t)address);
sp_bpf_set_slot(&out[at + SP_PARENT_MUNMAP_HIGH], (uint32_t)(address >> 32));
}
#ifdef __NR_pidfd_send_signal
if (cfg->child_pidfd >= 0)
used = sp_parent_append_signal(out, used, __NR_pidfd_send_signal,
(unsigned int)cfg->child_pidfd);
#endif
if (cfg->child_pid > 0)
used = sp_parent_append_signal(out, used, __NR_kill, (unsigned int)cfg->child_pid);
if (cfg->interactive)
used = SP_BPF_APPEND(out, used, sp_parent_ioctl);
used = SP_BPF_APPEND(out, used, sp_parent_sockets);
if (cfg->outbound_sockets_allowed)
used = SP_BPF_APPEND(out, used, sp_parent_outbound);
out[used++] = (struct sock_filter)SP_PARENT_DENY;
return (unsigned short)used;
}
#endif
#ifndef SECCOMP_FILTER_FLAG_LOG
#define SECCOMP_FILTER_FLAG_LOG (1UL << 1)
#endif
/* Filters go through seccomp(2), not prctl: only it takes FLAG_LOG, which puts
* ERRNO denials in the kernel log beside kills (type=1326, with the syscall
* number), so a syscall a newer libc starts using is easy to find. */
static int sp_seccomp_install(const struct sock_fprog *program, unsigned long flags) {
return (int)syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, flags, program);
}
static int apply_parent_seccomp(const struct parent_harden_config *cfg) {
#if SP_AUDIT_ARCH != 0
struct sock_filter filter[SP_PARENT_SECCOMP_MAX_FILTER];
struct sock_fprog program = {.len = sp_parent_seccomp_filter(cfg, filter), .filter = filter};
if (ensure_no_new_privs() < 0)
return -1;
/* Not logged: parent_harden_probe_seccomp() is denied on every launch. */
return sp_seccomp_install(&program, 0);
#else
(void)cfg;
fprintf(stderr, "sockpuppet: parent seccomp unsupported on this architecture\n");
errno = ENOSYS;
return -1;
#endif
}
/* Payload policy: deny a named set, allow everything else, as two installed
* filters. The floor holds the rules no payload profile (plan S2) may relax;
* the default holds the rest, which a profile would replace. The kernel runs
* both and keeps the stricter action; their rules name disjoint syscalls, so
* the pair decides exactly as one combined filter would. */
#define SP_CHILD_KILL SP_SECCOMP_KILL
#define SP_CHILD_EPERM (SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA))
#if SP_AUDIT_ARCH != 0
static const struct sock_filter sp_payload_floor_filter[] = {
SP_BPF_ARCH_PROLOGUE,
/* Kernel code and tracing, keyrings, cross-process memory and mounts. */
SP_BPF_NR_RET(__NR_init_module, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_finit_module, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_delete_module, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_kexec_load, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_kexec_file_load, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_bpf, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_perf_event_open, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_userfaultfd, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_keyctl, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_add_key, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_request_key, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_process_vm_readv, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_process_vm_writev, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_mount, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_umount2, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_pivot_root, SP_CHILD_KILL),
/* fspick, open_tree_attr and io_uring may be absent from older headers;
* the rest of the new mount API is required by the mount code. */
SP_BPF_NR_RET(__NR_open_tree, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_move_mount, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_fsopen, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_fsconfig, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_fsmount, SP_CHILD_KILL),
#ifdef __NR_fspick
SP_BPF_NR_RET(__NR_fspick, SP_CHILD_KILL),
#endif
SP_BPF_NR_RET(__NR_mount_setattr, SP_CHILD_KILL),
#ifdef __NR_open_tree_attr
SP_BPF_NR_RET(__NR_open_tree_attr, SP_CHILD_KILL),
#endif
#ifdef __NR_io_uring_setup
SP_BPF_NR_RET(__NR_io_uring_setup, SP_CHILD_EPERM),
#endif
#ifdef __NR_io_uring_enter
SP_BPF_NR_RET(__NR_io_uring_enter, SP_CHILD_EPERM),
#endif
#ifdef __NR_io_uring_register
SP_BPF_NR_RET(__NR_io_uring_register, SP_CHILD_EPERM),
#endif
SP_BPF_RET(SECCOMP_RET_ALLOW),
};
/* Namespaces, tracing, filter installation and tty injection. Installed after
* the floor: it denies prctl(PR_SET_SECCOMP), so nothing can follow it. */
static const struct sock_filter sp_payload_default_filter[] = {
SP_BPF_ARCH_PROLOGUE,
SP_BPF_NR_RET(__NR_unshare, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_setns, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_ptrace, SP_CHILD_KILL),
SP_BPF_NR_RET(__NR_seccomp, SP_CHILD_EPERM),
#ifdef __NR_clone3
/* ENOSYS, not EPERM: glibc falls back to clone(2) on ENOSYS. */
SP_BPF_NR_RET(__NR_clone3, SECCOMP_RET_ERRNO | (ENOSYS & SECCOMP_RET_DATA)),
#endif
SP_BPF_ARG_EQ_RET(__NR_prctl, 0, PR_SET_SECCOMP, SP_CHILD_EPERM),
SP_BPF_ARG_EQ_RET(__NR_socket, 0, AF_ALG, SP_CHILD_EPERM),
/* Deny the two tty-push ioctls, allow other ioctls; one argument load. */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_ioctl, 0, 6), SP_BPF_LOAD(args[1]),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, TIOCSTI, 0, 1), SP_BPF_RET(SP_CHILD_EPERM),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, TIOCLINUX, 0, 1), SP_BPF_RET(SP_CHILD_EPERM),
SP_BPF_RET(SECCOMP_RET_ALLOW), SP_BPF_LOAD(nr),
/* Legacy clone(2): a nonzero high flags word, or any forbidden flag in the
* low word, is fatal. The high word is checked first so a 64-bit flag set
* cannot smuggle a blocked bit past a 32-bit mask. */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone, 0, 7), SP_BPF_LOAD_HIGH(args[0]),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 0), SP_BPF_RET(SP_SECCOMP_KILL),
SP_BPF_LOAD(args[0]), BPF_STMT(BPF_ALU | BPF_AND | BPF_K, SP_CLONE_BLOCKED_FLAGS),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 0), SP_BPF_RET(SP_SECCOMP_KILL),
SP_BPF_RET(SECCOMP_RET_ALLOW),
};
_Static_assert(sizeof(sp_payload_default_filter) / sizeof(sp_payload_default_filter[0]) <=
BPF_MAXINSNS,
"payload default seccomp template exceeds BPF_MAXINSNS");
#endif
/* Parses n records in sp_profile. A profile must open with this
* architecture's check (one built for another ABI numbers syscalls
* differently) and return only constant actions sockpuppet can honour: it has
* no tracer, listener or SIGSYS handler for TRACE, USER_NOTIF or TRAP. The
* kernel checks the rest at install. Returns NULL or the reason. */
static const char *sp_profile_parse(size_t n) {
const struct sock_filter *f = sp_profile;
if (n < 2 || f[0].code != (BPF_LD | BPF_W | BPF_ABS) ||
f[0].k != offsetof(struct seccomp_data, arch) || f[1].code != (BPF_JMP | BPF_JEQ | BPF_K) ||
f[1].k != SP_AUDIT_ARCH)
return "does not start with this architecture's check (ld arch; jeq AUDIT_ARCH)";
for (size_t i = 0; i < n; ++i) {
uint32_t action = f[i].k & SECCOMP_RET_ACTION_FULL;
if (BPF_CLASS(f[i].code) != BPF_RET)
continue;
if (BPF_RVAL(f[i].code) != BPF_K)
return "returns a computed action";
if (action != SECCOMP_RET_ALLOW && action != SECCOMP_RET_ERRNO && action != SECCOMP_RET_LOG &&
action != SECCOMP_RET_KILL_PROCESS && action != SECCOMP_RET_KILL_THREAD)
return "returns an action other than ALLOW, ERRNO, LOG or KILL";
}
sp_profile_len = (unsigned short)n;
return NULL;
}
/* Reads --seccomp-file into sp_profile. Returns NULL or the reason. */
static const char *sp_profile_load(const char *path) {
const char *error;
struct stat st;
ssize_t got;
int fd;
if (SP_AUDIT_ARCH == 0)
return "seccomp is unsupported on this architecture";
if (path[0] != '/')
return "must be an absolute path";
fd = open(path, O_RDONLY | O_CLOEXEC | O_NOCTTY | O_NONBLOCK);
if (fd < 0)
return strerror(errno);
if (fstat(fd, &st) < 0)
error = strerror(errno);
else if (!S_ISREG(st.st_mode))
error = "not a regular file";
else if (st.st_size <= 0 || (uint64_t)st.st_size % sizeof(struct sock_filter) != 0)
error = "size is not a whole number of struct sock_filter records";
else if ((uint64_t)st.st_size > sizeof(sp_profile))
error = "longer than BPF_MAXINSNS instructions";
else if ((got = read(fd, sp_profile, (size_t)st.st_size)) != st.st_size)
error = got < 0 ? strerror(errno) : "short read";
else
error = sp_profile_parse((size_t)st.st_size / sizeof(struct sock_filter));
close(fd);
return error;
}
static int apply_child_seccomp(void) {
#if SP_AUDIT_ARCH != 0
struct sock_fprog floor = SP_BPF_PROGRAM(sp_payload_floor_filter);
struct sock_fprog def = SP_BPF_PROGRAM(sp_payload_default_filter);
struct sock_fprog profile = {.len = sp_profile_len, .filter = sp_profile};
if (ensure_no_new_privs() < 0 || sp_seccomp_install(&floor, SECCOMP_FILTER_FLAG_LOG) < 0)
return -1;
if (sp_profile_len != 0)
return sp_seccomp_install(&profile, SECCOMP_FILTER_FLAG_LOG);
/* Not logged: glibc tries clone3 (ENOSYS here) once in every process that
* starts a thread or calls posix_spawn, so a build would log per process. */
return sp_seccomp_install(&def, 0);
#else
fprintf(stderr, "[sockpuppet] Warning: Seccomp not supported on this "
"architecture, continuing without syscall filtering\n");
return 0;
#endif
}
/* Setup is a namespace-root program selected by the user. Keep its policy
* broad enough for package managers and ordinary services; deny only the
* operations that would change the fixed sandbox topology or add a second
* kernel-control path. The final payload policy remains separate. */
#define SP_SETUP_SECCOMP_ERRNO \
(SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA))
#if SP_AUDIT_ARCH != 0
static const struct sock_filter sp_setup_seccomp_filter[] = {
SP_BPF_ARCH_PROLOGUE,
/* fspick and open_tree_attr may be absent from older headers; the rest of
* the new mount API is required by the mount code. */
SP_BPF_NR_RET(__NR_mount, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_umount2, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_pivot_root, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_unshare, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_setns, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_ptrace, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_bpf, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_open_tree, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_move_mount, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_fsopen, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_fsconfig, SP_SETUP_SECCOMP_ERRNO),
SP_BPF_NR_RET(__NR_fsmount, SP_SETUP_SECCOMP_ERRNO),
#ifdef __NR_fspick
SP_BPF_NR_RET(__NR_fspick, SP_SETUP_SECCOMP_ERRNO),
#endif
SP_BPF_NR_RET(__NR_mount_setattr, SP_SETUP_SECCOMP_ERRNO),
#ifdef __NR_open_tree_attr
SP_BPF_NR_RET(__NR_open_tree_attr, SP_SETUP_SECCOMP_ERRNO),
#endif
SP_BPF_RET(SECCOMP_RET_ALLOW),
};
_Static_assert(sizeof(sp_setup_seccomp_filter) / sizeof(sp_setup_seccomp_filter[0]) <=
BPF_MAXINSNS,
"setup seccomp template exceeds BPF_MAXINSNS");
#endif
static int apply_setup_seccomp(void) {
#if SP_AUDIT_ARCH != 0
struct sock_fprog program = SP_BPF_PROGRAM(sp_setup_seccomp_filter);
if (ensure_no_new_privs() < 0)
return -1;
return sp_seccomp_install(&program, SECCOMP_FILTER_FLAG_LOG);
#else
errno = ENOSYS;
return -1;
#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;
}
enum sp_pid1_phase {
SP_PID1_PHASE_NONE,
SP_PID1_PHASE_PREPARED,
SP_PID1_PHASE_BROKER_WAIT,
SP_PID1_PHASE_SETUP_RUNNING,
SP_PID1_PHASE_SETUP_REAPED,
SP_PID1_PHASE_FINAL_IDENTITY,
SP_PID1_PHASE_PAYLOAD_RUNNING,
SP_PID1_PHASE_TEARDOWN,
};
static int sp_pid1_phase_enter(enum sp_pid1_phase *current,
enum sp_pid1_phase next) {
static const char *const names[] = {
[SP_PID1_PHASE_PREPARED] = "prepared",
[SP_PID1_PHASE_BROKER_WAIT] = "broker_wait",
[SP_PID1_PHASE_SETUP_RUNNING] = "setup_running",
[SP_PID1_PHASE_SETUP_REAPED] = "setup_reaped",
[SP_PID1_PHASE_FINAL_IDENTITY] = "final_identity",
[SP_PID1_PHASE_PAYLOAD_RUNNING] = "payload_running",
[SP_PID1_PHASE_TEARDOWN] = "teardown",
};
int allowed = 0;
if (current == NULL || *current < SP_PID1_PHASE_NONE ||
*current > SP_PID1_PHASE_TEARDOWN || next <= SP_PID1_PHASE_NONE ||
next > SP_PID1_PHASE_TEARDOWN) {
errno = EPROTO;
return -1;
}
switch (*current) {
case SP_PID1_PHASE_NONE:
allowed = next == SP_PID1_PHASE_PREPARED;
break;
case SP_PID1_PHASE_PREPARED:
allowed = next == SP_PID1_PHASE_BROKER_WAIT ||
next == SP_PID1_PHASE_FINAL_IDENTITY;
break;
case SP_PID1_PHASE_BROKER_WAIT:
allowed = next == SP_PID1_PHASE_SETUP_RUNNING;
break;
case SP_PID1_PHASE_SETUP_RUNNING:
allowed = next == SP_PID1_PHASE_SETUP_REAPED;
break;
case SP_PID1_PHASE_SETUP_REAPED:
allowed = next == SP_PID1_PHASE_FINAL_IDENTITY;
break;
case SP_PID1_PHASE_FINAL_IDENTITY:
allowed = next == SP_PID1_PHASE_PAYLOAD_RUNNING;
break;
case SP_PID1_PHASE_PAYLOAD_RUNNING:
allowed = next == SP_PID1_PHASE_TEARDOWN;
break;
case SP_PID1_PHASE_TEARDOWN:
break;
}
if (!allowed) {
errno = EPROTO;
return -1;
}
*current = next;
bench_mark("pid1_phase", names[next], "entered", NULL);
return 0;
}
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 void sp_setup_exec_child(int script_fd, int stdin_fd, int stdout_fd,
int stderr_fd, pid_t expected_parent) {
static char path_env[] = "PATH=/usr/sbin:/usr/bin:/sbin:/bin";
static char home_env[] = "HOME=/";
static char locale_env[] = "LC_ALL=C";
char script_path[64];
char *const envp[] = {path_env, home_env, locale_env, NULL};
char *const argv[] = {"/bin/sh", script_path, NULL};
arm_parent_death_signal(expected_parent);
if (dup2(stdin_fd, STDIN_FILENO) < 0 ||
dup2(stdout_fd, STDOUT_FILENO) < 0 ||
dup2(stderr_fd, STDERR_FILENO) < 0) {
dprintf(STDERR_FILENO, "[sockpuppet] setup stdio: %s\n", strerror(errno));
_exit(126);
}
if (stdin_fd > STDERR_FILENO)
close(stdin_fd);
if (stdout_fd > STDERR_FILENO && stdout_fd != stdin_fd)
close(stdout_fd);
if (stderr_fd > STDERR_FILENO && stderr_fd != stdin_fd &&
stderr_fd != stdout_fd)
close(stderr_fd);
if (script_fd != 3) {
if (dup3(script_fd, 3, 0) < 0) {
dprintf(STDERR_FILENO, "[sockpuppet] setup script fd: %s\n",
strerror(errno));
_exit(126);
}
close(script_fd);
}
if (fcntl(3, F_SETFD, 0) < 0) {
dprintf(STDERR_FILENO, "[sockpuppet] setup script flags: %s\n",
strerror(errno));
_exit(126);
}
#ifdef __NR_close_range
if (syscall(__NR_close_range, 4U, ~0U, 0U) < 0) {
dprintf(STDERR_FILENO, "[sockpuppet] setup descriptor sweep: %s\n",
strerror(errno));
_exit(126);
}
#else
dprintf(STDERR_FILENO, "[sockpuppet] setup descriptor sweep unavailable\n");
_exit(126);
#endif
if (apply_setup_seccomp() < 0) {
dprintf(STDERR_FILENO, "[sockpuppet] setup seccomp: %s\n",
strerror(errno));
_exit(126);
}
snprintf(script_path, sizeof(script_path), "/proc/self/fd/3");
execve("/bin/sh", argv, envp);
dprintf(STDERR_FILENO, "[sockpuppet] setup exec: %s\n", strerror(errno));
_exit(126);
}
static int sp_run_setup_phase(int *script_fd, int stdout_fd, int stderr_fd,
unsigned int timeout_ms) {
int stdin_pipe[2];
pid_t parent = getpid();
pid_t child;
int status = 0;
int timed_out = 0;
uint64_t deadline;
if (script_fd == NULL || *script_fd < 0 || stdout_fd < 0 ||
stderr_fd < 0 || timeout_ms == 0) {
errno = EINVAL;
return 1;
}
if (pipe2(stdin_pipe, O_CLOEXEC) < 0)
return 1;
child = fork();
if (child < 0) {
close(stdin_pipe[0]);
close(stdin_pipe[1]);
return 1;
}
if (child == 0) {
close(stdin_pipe[1]);
sp_setup_exec_child(*script_fd, stdin_pipe[0], stdout_fd, stderr_fd,
parent);
}
close(stdin_pipe[0]);
close(stdin_pipe[1]);
deadline = sp_ns_now_ns() + (uint64_t)timeout_ms * 1000000ULL;
for (;;) {
pid_t got = waitpid(child, &status, WNOHANG);
if (got == child)
break;
if (got < 0) {
if (errno == EINTR)
continue;
(void)kill(child, SIGKILL);
while (waitpid(child, &status, 0) < 0 && errno == EINTR)
;
status = 0;
break;
}
if (sp_ns_now_ns() >= deadline) {
if (kill(child, SIGKILL) < 0 && errno != ESRCH)
status = 0;
while (waitpid(child, &status, 0) < 0 && errno == EINTR)
;
timed_out = 1;
break;
}
(void)poll(NULL, 0, 10);
}
sp_close_owned_fd_nointr(script_fd);
if (kill(-1, SIGKILL) < 0 && errno != ESRCH)
return 1;
for (;;) {
pid_t got = waitpid(-1, NULL, 0);
if (got > 0)
continue;
if (got < 0 && errno == EINTR)
continue;
if (got < 0 && errno == ECHILD)
break;
return 1;
}
if (timed_out)
return 124;
return sp_wait_status_to_exit_code(status);
}
/* ---------- 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);
}
/* Registers (EPOLL_CTL_ADD) or updates (EPOLL_CTL_MOD) one of a UDP flow's
* two descriptors: its relay socket (FD_UDP_RELAY) or SOCKS control
* connection (FD_UDP_CTRL). A failed ADD is fatal; a failed MOD marks the
* flow and the UDP table for teardown. */
static void epoll_set_udp(struct udp_flow *f, int op, enum fd_type kind, uint32_t events) {
int fd = kind == FD_UDP_CTRL ? f->tcp_ctrl : f->udp_relay;
struct epoll_event *interest = kind == FD_UDP_CTRL ? &f->ctrl_interest : &f->relay_interest;
if (fd < 0 || g_epfd < 0)
return;
struct epoll_event ev = {
.events = events,
.data.u64 = epoll_registration_token(kind, (size_t)(f - udp_flows), fd, f->generation)};
if (sp_epoll_update(op, fd, &ev, interest) == 0)
return;
if (op == EPOLL_CTL_ADD)
die(kind == FD_UDP_CTRL ? "epoll_ctl add udp ctrl" : "epoll_ctl add udp");
sp_epoll_update_failed(kind == FD_UDP_CTRL ? "UDP control" : "UDP relay");
f->epoll_failed = 1;
g_udp_epoll_failed = 1;
}
static int epoll_add_publish_rule(struct publish_rule *rule) {
if (rule->listen_fd < 0 || g_epfd < 0)
return 0;
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),
};
return sp_epoll_update(EPOLL_CTL_ADD, rule->listen_fd, &ev, &rule->interest);
}
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;
struct epoll_event source_interest, destination_interest;
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) {
int fd = destination->owned ? destination->fd : -1;
destination->fd = -1;
destination->owned = 0;
destination->epollable = 0;
if (fd >= 0)
(void)close(fd);
}
static void relay_destination_failed(struct relay_runtime *runtime) {
(void)sp_relay_step(
&runtime->state,
&(struct sp_relay_event){.type = SP_RELAY_DESTINATION_FAILED});
}
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) {
int fd;
if (runtime->source_fd < 0)
return;
fd = runtime->source_fd;
runtime->source_fd = -1;
runtime->source_registered = 0;
runtime->source_interest = (struct epoll_event){0};
epoll_del(fd);
(void)close(fd);
(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;
relay_destination_failed(runtime);
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;
}
/* Reads what fits into the relay. At end of input or on a read error the
* batch relay closes its source; the interactive relay keeps the descriptor
* and only records EOF. */
static void relay_read_source(struct relay_runtime *runtime, int close_at_eof) {
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 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK))
return;
if (close_at_eof)
relay_close_source(runtime);
else
(void)sp_relay_step(&runtime->state, &(struct sp_relay_event){.type = SP_RELAY_SOURCE_EOF});
}
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;
}
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) {
g_stderr_raw_tty = 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));
}
sp_close_owned_fd_nointr(&session->pty_master_fd);
sp_close_owned_fd_nointr(&session->pty_slave_fd);
sp_close_owned_fd_nointr(&session->host_tty_fd);
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;
}
/* Names the first way fd differs from an AF_INET socket of `type` and
* `protocol` (SOCK_STREAM/IPPROTO_TCP or SOCK_DGRAM/IPPROTO_UDP), or NULL.
* Checked before any option call, so post-hardening SOL_SOCKET authority
* cannot be redirected to another descriptor kind. */
static const char *sp_host_socket_kind_error(int fd, int type, int protocol) {
int value = 0;
socklen_t length = sizeof(value);
int tcp = type == SOCK_STREAM;
if (getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &value, &length) < 0 || value != AF_INET)
return "not an AF_INET socket";
length = sizeof(value);
if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &value, &length) < 0 || value != type)
return tcp ? "not a SOCK_STREAM socket" : "not a SOCK_DGRAM socket";
length = sizeof(value);
if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &value, &length) < 0 || value != protocol)
return tcp ? "not an IPPROTO_TCP socket" : "not an IPPROTO_UDP socket";
return NULL;
}
/* Host TCP socket-option ownership.
*
* Every established direct, SOCKS-ready, and accepted-publish host TCP socket
* is configured here and nowhere else. Concentrating the calls is what lets
* the parent seccomp filter permit exactly three (level, option, length)
* triples instead of granting steady-state socket-option authority; see
* parent_seccomp_append_host_tcp_option_allow().
*
* seccomp cannot inspect what kind of file a descriptor refers to, so the fd
* kind is validated here in userspace before any option is set.
*
* Timing: callers on a connect path must invoke this before connect(), because
* SO_RCVBUF has to be in effect before the SYN for window scale to be
* negotiated from the configured buffer rather than the default. The publish
* path is the unavoidable exception โ€” its fd arrives already established from
* accept4(), so the resize cannot influence the scale that was negotiated.
*
* Returns 0 on success, -1 with *reason set to a static string on failure. */
static int sp_scheduler_configure_host_tcp_socket(int fd, const char **reason) {
static const struct {
int level;
int option;
int value;
const char *name;
} options[] = {
{IPPROTO_TCP, TCP_NODELAY, 1, "TCP_NODELAY"},
{SOL_SOCKET, SO_SNDBUF, SP_HOST_TCP_SNDBUF, "SO_SNDBUF"},
{SOL_SOCKET, SO_RCVBUF, SP_HOST_TCP_RCVBUF, "SO_RCVBUF"},
};
socklen_t length;
size_t i;
*reason = sp_host_socket_kind_error(fd, SOCK_STREAM, IPPROTO_TCP);
if (*reason != NULL)
return -1;
for (i = 0; i < sizeof(options) / sizeof(options[0]); ++i) {
int value = options[i].value;
int effective = 0;
if (setsockopt(fd, options[i].level, options[i].option, &value,
sizeof(value)) < 0) {
*reason = options[i].name;
return -1;
}
length = sizeof(effective);
if (getsockopt(fd, options[i].level, options[i].option, &effective,
&length) < 0 || length != sizeof(effective)) {
*reason = options[i].name;
return -1;
}
if (options[i].option == TCP_NODELAY) {
/* Nagle silently left on would batch every small response, so this is a
* strict gate rather than a logged observation. */
if (effective == 0) {
*reason = "TCP_NODELAY readback was 0";
return -1;
}
continue;
}
if (effective < SP_HOST_TCP_BUF_MIN || effective > SP_HOST_TCP_BUF_MAX) {
*reason = options[i].name;
return -1;
}
DBG("host TCP %s: requested %d effective %d", options[i].name, value,
effective);
}
return 0;
}
/* UDP sockets use the same frozen Linux buffer readback band as TCP, after
* the same descriptor kind check. */
static int sp_udp_configure_host_socket(int fd, int *effective_sndbuf,
int *effective_rcvbuf,
const char **reason) {
int request;
int effective;
socklen_t length;
*reason = sp_host_socket_kind_error(fd, SOCK_DGRAM, IPPROTO_UDP);
if (*reason != NULL)
return -1;
request = SP_HOST_UDP_SNDBUF;
if (setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &request, sizeof(request)) < 0) {
*reason = "SO_SNDBUF";
return -1;
}
length = sizeof(effective);
if (getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &effective, &length) < 0 ||
length != sizeof(effective) || effective < SP_HOST_TCP_BUF_MIN ||
effective > SP_HOST_TCP_BUF_MAX) {
*reason = "SO_SNDBUF readback";
return -1;
}
*effective_sndbuf = effective;
request = SP_HOST_UDP_RCVBUF;
if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &request, sizeof(request)) < 0) {
*reason = "SO_RCVBUF";
return -1;
}
length = sizeof(effective);
if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &effective, &length) < 0 ||
length != sizeof(effective) || effective < SP_HOST_TCP_BUF_MIN ||
effective > SP_HOST_TCP_BUF_MAX) {
*reason = "SO_RCVBUF readback";
return -1;
}
*effective_rcvbuf = effective;
return 0;
}
static int sp_ns_segment_needs_challenge_ack(
const struct sp_tcp_core *core, const struct sp_ev_tcp_segment *segment) {
uint64_t seq_abs;
uint64_t sequence_len;
if (segment == NULL || core->state < SP_TCP_CORE_ESTABLISHED ||
core->state > SP_TCP_CORE_TIME_WAIT)
return 0;
if ((segment->flags & SP_TCP_CORE_SYN) != 0)
return 1;
if ((segment->flags & SP_TCP_CORE_RST) == 0)
return 0;
sequence_len = segment->payload_length.value;
if ((segment->flags & SP_TCP_CORE_FIN) != 0)
sequence_len++;
seq_abs = sp_tcp_unwrap_u32(segment->seq, core->rcv_acked_abs);
return sp_segment_acceptable(core, seq_abs, sequence_len) &&
seq_abs != core->rcv_acked_abs;
}
/* Project the core into what the executor is allowed to see. The engine
* wrapper computes the challenge-ACK decision before the transition, then
* calls this after commit or rollback to capture everything else. */
/* The engine wrapper is the sole owner of observations that need pre-event
* core state but are not protocol effects. RFC 5961 challenge classification
* happens before dispatch; status is captured only after commit or rollback.
* Direct, loopback, and split execution all enter here. */
static int start_nonblocking_connect(int fd, const struct sockaddr_in *addr) {
const char *reason = "unknown";
/* Before connect(): SO_RCVBUF must be in effect for window-scale
* negotiation. */
if (sp_scheduler_configure_host_tcp_socket(fd, &reason) < 0) {
DBG("host TCP option setup failed: %s (%s)", reason, strerror(errno));
return -1;
}
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;
}
/* Sans-I/O SOCKS: all inputs are values, with owned bounded rx/tx storage.
* Socket readiness, errno, clocks, addresses and logging belong to the adapter. */
struct sp_socks_config {
const char *username;
const char *password;
uint32_t proxy_ip; /* network byte order */
};
struct sp_socks_endpoint {
uint32_t ip; /* network byte order */
uint16_t port; /* host byte order */
};
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 sizeof(io->rxbuf) + 1; /* Reject an unknown address type. */
}
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 sp_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 sp_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 sp_socks_begin_handshake(struct socks_io *io,
const struct sp_socks_config *cfg) {
if (socks_queue_greeting(io, cfg) < 0)
return -1;
io->state = SOCKS_IO_METHOD;
return 0;
}
static int sp_socks_process_rx(struct socks_io *io, const struct sp_socks_config *cfg,
struct sp_socks_endpoint *relay_addr) {
for (;;) {
size_t need = socks_response_need(io);
if (io->state == SOCKS_IO_READY)
return 1;
if (socks_has_pending_tx(io))
return 0;
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->rxbuf[2] != 0x00) {
io->state = SOCKS_IO_FAILED;
return -1;
}
atyp = io->rxbuf[3];
if (atyp != 0x01 && atyp != 0x03 && atyp != 0x04) {
io->state = SOCKS_IO_FAILED;
return -1;
}
if (relay_addr) {
memset(relay_addr, 0, sizeof(*relay_addr));
if (atyp == 0x01 && need >= 10) {
memcpy(&relay_addr->ip, io->rxbuf + 4, 4);
relay_addr->port = (uint16_t)((io->rxbuf[8] << 8) | io->rxbuf[9]);
} else if (atyp == 0x03 && need >= 7) {
relay_addr->ip = cfg->proxy_ip;
relay_addr->port = (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->ip == 0 ||
relay_addr->ip == htonl(0x7f000001)) {
relay_addr->ip = cfg->proxy_ip;
}
if (relay_addr->port == 0) {
io->state = SOCKS_IO_FAILED;
return -1;
}
}
socks_consume_rx(io, used);
if (io->is_udp && io->rx_len != 0) {
io->state = SOCKS_IO_FAILED;
return -1;
}
io->state = SOCKS_IO_READY;
return 1;
}
}
}
static int sp_socks_tx_accepted(struct socks_io *io, size_t accepted) {
if (accepted > io->tx_len - io->tx_off)
return -1;
io->tx_off += accepted;
if (io->tx_off == io->tx_len)
io->tx_off = io->tx_len = 0;
return 0;
}
static int sp_socks_receive(struct socks_io *io, const uint8_t *bytes,
size_t length, const struct sp_socks_config *cfg,
struct sp_socks_endpoint *relay) {
if (length > sizeof(io->rxbuf) - io->rx_len)
return -1;
memcpy(io->rxbuf + io->rx_len, bytes, length);
io->rx_len += length;
return sp_socks_process_rx(io, cfg, relay);
}
static int sp_socks_udp_encode(struct sp_socks_endpoint endpoint,
const char *domain, size_t domain_len, const uint8_t *payload,
size_t payload_len, uint8_t *packet, size_t capacity, size_t *packet_len) {
size_t header_len, offset = 0;
if (domain_len > 255)
return -1;
header_len = domain_len != 0 ? 7U + domain_len : 10U;
if (payload_len > capacity || header_len > capacity - payload_len)
return -1;
packet[offset++] = 0;
packet[offset++] = 0;
packet[offset++] = 0;
if (domain_len != 0) {
packet[offset++] = 0x03;
packet[offset++] = (uint8_t)domain_len;
memcpy(packet + offset, domain, domain_len);
offset += domain_len;
} else {
packet[offset++] = 0x01;
memcpy(packet + offset, &endpoint.ip, 4);
offset += 4;
}
packet[offset++] = (uint8_t)(endpoint.port >> 8);
packet[offset++] = (uint8_t)(endpoint.port & 0xff);
memcpy(packet + offset, payload, payload_len);
*packet_len = offset + payload_len;
return 0;
}
static int sp_socks_udp_decode(struct sp_socks_endpoint endpoint,
const char *domain, size_t domain_len,
const uint8_t *packet, size_t packet_len,
const uint8_t **payload,
size_t *payload_len) {
size_t header_len;
uint16_t port;
if (packet_len < 4 || packet[0] != 0 || packet[1] != 0 ||
packet[2] != 0)
return -1;
if (packet[3] == 0x01) {
uint32_t address;
header_len = 10;
if (packet_len < header_len)
return -1;
memcpy(&address, packet + 4, sizeof(address));
port = (uint16_t)(((uint16_t)packet[8] << 8) | packet[9]);
if (address != endpoint.ip || port != endpoint.port)
return -1;
} else if (packet[3] == 0x03) {
size_t name_len;
if (packet_len < 5)
return -1;
name_len = packet[4];
header_len = 7U + name_len;
if (name_len == 0 || packet_len < header_len || domain_len == 0 ||
domain_len != name_len ||
memcmp(packet + 5, domain, name_len) != 0)
return -1;
port = (uint16_t)(((uint16_t)packet[5 + name_len] << 8) |
packet[6 + name_len]);
if (port != endpoint.port)
return -1;
} else {
return -1;
}
*payload = packet + header_len;
*payload_len = packet_len - header_len;
return 0;
}
/* End Sans-I/O SOCKS. */
static struct sp_socks_config socks_core_config(const struct socks_config *cfg) {
return (struct sp_socks_config){cfg->username, cfg->password,
cfg->addr.sin_addr.s_addr};
}
static int socks_begin_handshake(struct socks_io *io,
const struct socks_config *cfg) {
struct sp_socks_config config = socks_core_config(cfg);
return sp_socks_begin_handshake(io, &config);
}
static int socks_process_rx(struct socks_io *io, const struct socks_config *cfg,
struct sockaddr_in *relay_addr) {
struct sp_socks_config config = socks_core_config(cfg);
struct sp_socks_endpoint relay = {0};
int result = sp_socks_process_rx(io, &config, relay_addr ? &relay : NULL);
if (result == 1 && relay_addr) {
*relay_addr = (struct sockaddr_in){.sin_family = AF_INET,
.sin_addr.s_addr = relay.ip, .sin_port = htons(relay.port)};
}
return result;
}
static int socks_flush_tx(int fd, struct socks_io *io) {
uint64_t started_ns = sp_ns_now_ns();
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) {
if (sp_socks_tx_accepted(io, (size_t)sent) < 0)
return -1;
if (sp_ns_now_ns() - started_ns >= SP_CALLBACK_BUDGET_NS)
return 0;
continue;
}
if (sent < 0 && errno == EINTR &&
sp_ns_now_ns() - started_ns < SP_CALLBACK_BUDGET_NS)
continue;
if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
return 0;
return -1;
}
return 0;
}
static int socks_recv_and_process(int fd, struct socks_io *io,
const struct socks_config *cfg,
struct sockaddr_in *relay_addr) {
uint64_t started_ns = sp_ns_now_ns();
for (;;) {
uint8_t bytes[sizeof(io->rxbuf)];
int parsed = socks_process_rx(io, cfg, relay_addr);
if (parsed != 0)
return parsed;
if (socks_has_pending_tx(io))
return 0;
ssize_t r = recv(fd, bytes, sizeof(bytes) - io->rx_len, 0);
if (r > 0) {
struct sp_socks_config config = socks_core_config(cfg);
struct sp_socks_endpoint relay = {0};
int result = sp_socks_receive(io, bytes, (size_t)r, &config,
relay_addr ? &relay : NULL);
if (result == 1 && relay_addr)
*relay_addr = (struct sockaddr_in){.sin_family = AF_INET,
.sin_addr.s_addr = relay.ip, .sin_port = htons(relay.port)};
if (result != 0)
return result;
if (socks_has_pending_tx(io))
return 0;
if (sp_ns_now_ns() - started_ns >= SP_CALLBACK_BUDGET_NS)
return 0;
continue;
}
if (r == 0)
return -1;
if (errno == EINTR &&
sp_ns_now_ns() - started_ns < SP_CALLBACK_BUDGET_NS)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return 0;
return -1;
}
}
enum {
SP_CAPABILITY_V3_MAX = _LINUX_CAPABILITY_U32S_3 * 32 - 1,
};
static int sp_runtime_last_capability(int *last_capability) {
char value[32];
long parsed;
if (read_text_file_trimmed("/proc/sys/kernel/cap_last_cap", value,
sizeof(value)) == 0) {
if (parse_long_strict(value, 0, LONG_MAX, &parsed) < 0) {
if (errno == 0)
errno = EINVAL;
return -1;
}
if (parsed > SP_CAPABILITY_V3_MAX) {
errno = EOVERFLOW;
return -1;
}
*last_capability = (int)parsed;
return 0;
}
for (int cap = 0; cap <= SP_CAPABILITY_V3_MAX; ++cap) {
int rc = prctl(PR_CAPBSET_READ, cap, 0, 0, 0);
if (rc >= 0)
continue;
if (errno == EINVAL && cap > 0) {
*last_capability = cap - 1;
return 0;
}
return -1;
}
{
int rc = prctl(PR_CAPBSET_READ, SP_CAPABILITY_V3_MAX + 1, 0, 0, 0);
if (rc >= 0) {
errno = EOVERFLOW;
return -1;
}
if (errno != EINVAL)
return -1;
}
*last_capability = SP_CAPABILITY_V3_MAX;
return 0;
}
static int sp_drop_capability_bounding(void) {
int last_capability;
if (sp_runtime_last_capability(&last_capability) < 0)
return -1;
for (int cap = 0; cap <= last_capability; cap++) {
if (prctl(PR_CAPBSET_DROP, cap, 0, 0, 0) < 0)
return -1;
}
return 0;
}
static int sp_clear_capability_sets(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}};
if (syscall(SYS_capset, &hdr, data) < 0) {
return -1;
}
if (syscall(SYS_capget, &hdr, verify) < 0) {
return -1;
}
if (verify[0].effective || verify[0].permitted || verify[0].inheritable ||
verify[1].effective || verify[1].permitted || verify[1].inheritable) {
errno = EPERM;
return -1;
}
return 0;
}
static int sp_clear_ambient_capabilities(void) {
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) < 0 &&
errno != EINVAL)
return -1;
return 0;
}
/* Drop all capabilities (for rootless mode). */
static void drop_caps(void) {
if (sp_drop_capability_bounding() < 0 ||
sp_clear_ambient_capabilities() < 0 ||
sp_clear_capability_sets() < 0)
die("drop capabilities");
/* Disable core dumps (prevents leaking sensitive data) */
if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0) {
die("PR_SET_DUMPABLE");
}
}
static int sp_install_payload_identity(uid_t uid, gid_t gid) {
const unsigned int securebits =
SECBIT_NOROOT | SECBIT_NOROOT_LOCKED | SECBIT_NO_SETUID_FIXUP |
SECBIT_NO_SETUID_FIXUP_LOCKED | SECBIT_KEEP_CAPS_LOCKED |
SECBIT_NO_CAP_AMBIENT_RAISE | SECBIT_NO_CAP_AMBIENT_RAISE_LOCKED;
uid_t real_uid, effective_uid, saved_uid;
gid_t real_gid, effective_gid, saved_gid;
if (sp_drop_capability_bounding() < 0 ||
sp_clear_ambient_capabilities() < 0 ||
prctl(PR_SET_SECUREBITS, securebits, 0, 0, 0) < 0 ||
/* Set this before the credential drop so the following unprivileged
* Landlock installation cannot depend on a post-drop prctl. */
ensure_no_new_privs() < 0 || setgroups(0, NULL) < 0 ||
setresgid(gid, gid, gid) < 0 ||
setresuid(uid, uid, uid) < 0 || sp_clear_capability_sets() < 0 ||
prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0)
return -1;
if (getresuid(&real_uid, &effective_uid, &saved_uid) < 0 ||
getresgid(&real_gid, &effective_gid, &saved_gid) < 0 ||
real_uid != uid || effective_uid != uid || saved_uid != uid ||
real_gid != gid || effective_gid != gid || saved_gid != gid ||
getgroups(0, NULL) != 0 ||
prctl(PR_GET_SECUREBITS, 0, 0, 0, 0) != (int)securebits ||
prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) != 1) {
errno = EPERM;
return -1;
}
return 0;
}
static int socks_hex_digit(int c) {
if (c >= '0' && c <= '9')
return c - '0';
c |= 0x20;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
return -1;
}
/* Percent-decode one URL userinfo field (RFC 3986). RFC 1929 fields are 1-255
* bytes; a decoded NUL is rejected because the fields are C strings. */
static int socks_decode_userinfo(const char *in, size_t len, char *out,
size_t out_size) {
size_t used = 0;
for (size_t i = 0; i < len; ++i) {
int c = (unsigned char)in[i];
if (c == '%') {
int high = len - i >= 3 ? socks_hex_digit(in[i + 1]) : -1;
int low = high >= 0 ? socks_hex_digit(in[i + 2]) : -1;
if (low < 0)
return -1;
c = high * 16 + low;
i += 2;
}
if (c == 0 || used + 1 >= out_size)
return -1;
out[used++] = (char)c;
}
out[used] = '\0';
return used == 0 ? -1 : 0;
}
/* Parse SOCKS URL: [socks5://|socks5h://][user:password@]host[:port].
* Error messages never echo the URL because it may carry credentials. */
static int parse_socks_url(const char *url, struct socks_config *cfg,
char *err, size_t err_len) {
const char *p = url;
const char *colon;
const char *at;
size_t hlen;
long port = 1080; /* default SOCKS port */
memset(cfg, 0, sizeof(*cfg));
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;
}
/* The last '@' ends the userinfo, so a raw '@' may appear in the password;
* the first ':' separates the fields, so a username needs %3A. */
at = strrchr(p, '@');
if (at != NULL) {
colon = memchr(p, ':', (size_t)(at - p));
if (colon == NULL ||
socks_decode_userinfo(p, (size_t)(colon - p), cfg->username,
sizeof(cfg->username)) < 0 ||
socks_decode_userinfo(colon + 1, (size_t)(at - colon - 1),
cfg->password, sizeof(cfg->password)) < 0) {
memset(cfg, 0, sizeof(*cfg));
snprintf(err, err_len, "credentials must be user:password, each 1-255 "
"bytes after percent-decoding");
return -1;
}
p = at + 1;
}
colon = strrchr(p, ':');
hlen = colon != NULL ? (size_t)(colon - p) : strlen(p);
if (hlen == 0 || hlen >= sizeof(cfg->host) ||
(colon != NULL && parse_long_strict(colon + 1, 1, 65535, &port) < 0)) {
memset(cfg, 0, sizeof(*cfg));
snprintf(err, err_len, "expected host[:port] with port 1-65535");
return -1;
}
memcpy(cfg->host, p, hlen);
cfg->host[hlen] = '\0';
cfg->port = (int)port;
cfg->enabled = 1;
return 0;
}
static int add_host_rule_from_spec(const char *spec, char *err,
size_t err_len) {
char buf[128];
char *colon;
char *slash;
long last_octet;
long port;
struct host_rule rule = {0};
int copied;
if (host_rule_count >= MAX_HOST_RULES) {
snprintf(err, err_len, "too many --allow-host rules");
return -1;
}
copied = snprintf(buf, sizeof(buf), "%s", spec);
if (copied < 0 || copied >= (int)sizeof(buf)) {
snprintf(err, err_len, "--allow-host rule too long");
return -1;
}
slash = strchr(buf, '/');
if (slash != NULL) {
*slash = '\0';
if (strcmp(slash + 1, "tcp") == 0)
rule.proto = IPPROTO_TCP;
else if (strcmp(slash + 1, "udp") == 0)
rule.proto = IPPROTO_UDP;
else {
snprintf(err, err_len, "invalid --allow-host protocol");
return -1;
}
}
if (strncmp(buf, "127.0.0.", 8) != 0) {
snprintf(err, err_len, "--allow-host address must be 127.0.0.X");
return -1;
}
colon = strchr(buf + 8, ':');
if (colon == NULL) {
snprintf(err, err_len, "--allow-host must be 127.0.0.X:PORT[/tcp|/udp]");
return -1;
}
*colon = '\0';
if (parse_long_strict(buf + 8, 0, 255, &last_octet) < 0) {
snprintf(err, err_len, "invalid --allow-host address");
return -1;
}
if (strcmp(colon + 1, "*") == 0) {
snprintf(err, err_len, "--allow-host does not allow wildcard ports");
return -1;
}
if (parse_long_strict(colon + 1, 1, 65535, &port) < 0) {
snprintf(err, err_len, "invalid --allow-host port");
return -1;
}
rule.last_octet = (uint8_t)last_octet;
rule.port = (uint16_t)port;
host_rules[host_rule_count++] = rule;
return 0;
}
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];
const char *option_reason = "unknown";
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) {
int pktinfo = 0;
socklen_t pktinfo_len = sizeof(pktinfo);
if (sp_udp_configure_host_socket(
fd, &rule->effective_sndbuf, &rule->effective_rcvbuf,
&option_reason) < 0 ||
setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &one, sizeof(one)) < 0 ||
getsockopt(fd, IPPROTO_IP, IP_PKTINFO, &pktinfo, &pktinfo_len) < 0 ||
pktinfo_len != sizeof(pktinfo) || pktinfo != 1) {
fprintf(stderr,
"sockpuppet: failed to publish %s: UDP options (%s): %s\n",
desc, option_reason, 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 void close_publish_listeners(void);
static int setup_publish_listeners(void) {
for (int i = 0; i < publish_rule_count; ++i) {
if (setup_one_publish_listener(&publish_rules[i]) < 0) {
int saved = errno;
close_publish_listeners();
errno = saved;
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) {
int fd = sp_take_fd(&publish_rules[i].listen_fd);
epoll_del(fd);
(void)close(fd);
}
}
}
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;
/* close_range is required: the payload spawn and the setup helpers use it
* without a fallback, so a scan of /proc/self/fd could never help a launch. */
/* Close every fd from 3 up except the bench trace fd and also_keep (-1:
* none). */
static void close_extra_fds_for_exec(int also_keep) {
int keep[2] = {bench_trace_preserved_fd(), also_keep};
unsigned int next = 3;
if (keep[0] > keep[1]) {
keep[0] = keep[1];
keep[1] = bench_trace_preserved_fd();
}
for (int i = 0; i < 2; ++i) {
if (keep[i] < (int)next)
continue;
if (keep[i] > (int)next &&
syscall(SYS_close_range, next, (unsigned int)keep[i] - 1U, 0U) < 0)
die("close_range");
next = (unsigned int)keep[i] + 1U;
}
if (syscall(SYS_close_range, next, ~0U, 0U) < 0)
die("close_range");
}
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";
char **envp = calloc(6, sizeof(*envp));
if (envp == NULL)
die("calloc envp");
envp[0] = make_env_entry("PATH", default_path);
envp[1] = make_env_entry("HOME", "/tmp/home");
envp[2] = make_env_entry("TMPDIR", "/tmp");
envp[3] = make_env_entry("PWD", cwd);
envp[4] = make_env_entry("LC_ALL", "C");
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_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;
}
}
/* Payload spawn: clone(CLONE_VM|CLONE_VFORK) with a callback on its own
* stack. Contract: pid1 is single-threaded; the child has its own fd and
* signal tables (no CLONE_FILES or CLONE_SIGHAND) but shares pid1's memory
* until execve or _exit, so it makes only system calls and lock-free libc
* calls. The stack has a guard page. See experiments/clone_spawn.c. */
#if SP_AUDIT_ARCH == 0
#error "payload spawn: the raw sigaction layout is checked only on x86_64, aarch64 and riscv64"
#endif
struct sp_spawn_req {
char *const *argv;
char *const *envp;
int stdio[3]; /* all above fd 2 */
int ctty_fd;
uint64_t mask; /* the payload's mask: pid1's mask before the spawn */
/* Written by the child on failure: the die() message and errno. */
const char *volatile fail_what;
volatile int fail_errno;
};
static int sp_spawn_sigmask(const uint64_t *set, uint64_t *old) {
return (int)syscall(SYS_rt_sigprocmask, SIG_SETMASK, set, old,
sizeof(uint64_t));
}
static void sp_spawn_fail(struct sp_spawn_req *req, const char *what) {
req->fail_errno = errno;
req->fail_what = what;
_exit(127);
}
static int sp_spawn_child(void *arg) {
struct sp_spawn_req *req = arg;
/* The kernel's struct starts with the handler on the supported ABIs; an
* all-zero struct is SIG_DFL without flags. */
const unsigned long kernel_dfl[4] = {0, 0, 0, 0};
/* Reset every caught handler: it would run in shared memory once the mask
* is restored. SIG_IGN runs no code and stays, as with fork. Query and
* reset use the system call, not sigaction(): musl's wrapper takes its
* abort lock for SIGABRT, and glibc's refuses its reserved signals. */
for (int sig = 1; sig < NSIG; ++sig) {
unsigned long cur[4] = {0, 0, 0, 0};
if (sig == SIGKILL || sig == SIGSTOP)
continue;
if (syscall(SYS_rt_sigaction, sig, NULL, cur, sizeof(uint64_t)) < 0)
sp_spawn_fail(req, "sigaction init");
if (cur[0] == (uintptr_t)SIG_DFL || cur[0] == (uintptr_t)SIG_IGN)
continue;
if (syscall(SYS_rt_sigaction, sig, kernel_dfl, NULL, sizeof(uint64_t)) < 0)
sp_spawn_fail(req, "sigaction init");
}
/* A new session: in batch mode the payload has no controlling terminal,
* so it cannot reach the host tty through /dev/tty or tcsetpgrp(). It is
* also a process-group leader, which pid1's kill(-pid, sig) forwarding
* needs. */
if (setsid() < 0)
sp_spawn_fail(req, "setsid payload");
if (req->ctty_fd >= 0 && ioctl(req->ctty_fd, TIOCSCTTY, 0) < 0)
sp_spawn_fail(req, "TIOCSCTTY");
for (int fd = 0; fd < 3; ++fd)
if (dup2(req->stdio[fd], fd) < 0)
sp_spawn_fail(req, "dup2 payload stdio");
/* Close, not CLOEXEC: no inherited fd may be reachable through
* /proc/self/fd while execvpe() resolves the program. */
if (syscall(SYS_close_range, 3U, ~0U, 0U) < 0)
sp_spawn_fail(req, "close_range");
/* Limits and no_new_privs are inherited from pid1. */
if (sp_spawn_sigmask(&req->mask, NULL) < 0)
sp_spawn_fail(req, "sigprocmask");
/* execvpe reads PATH from environ, which pid1 pointed at envp before
* the clone. Its stack use is bounded except glibc's /bin/sh fallback,
* which pid1 sized the stack for. */
execvpe(req->argv[0], req->argv, req->envp);
sp_spawn_fail(req, "exec");
return 0;
}
static pid_t sp_clone_spawn_payload(char **argv, char **envp,
struct interactive_session *session,
const int stdout_pipe[2],
const int stderr_pipe[2]) {
const size_t guard = (size_t)sysconf(_SC_PAGESIZE);
size_t argc = 0, stack_size;
char **saved_environ = environ;
const uint64_t all = ~(uint64_t)0;
struct sp_spawn_req req = {
.argv = argv, .envp = envp, .stdio = {-1, -1, -1}, .ctty_fd = -1};
int null_fd = -1;
pid_t pid;
char *stack;
if (interactive_stdio) {
if (!session || session->pty_slave_fd < 0) {
errno = EBADF;
die("interactive child setup");
}
req.ctty_fd = session->pty_slave_fd;
req.stdio[0] = req.stdio[1] = req.stdio[2] = session->pty_slave_fd;
} else {
null_fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
if (null_fd < 0)
die("open /dev/null for payload stdin");
req.stdio[0] = null_fd;
req.stdio[1] = stdout_pipe[1];
req.stdio[2] = stderr_pipe[1];
}
/* main() keeps fds 0-2 open, so every source is above 2, and no dup2()
* in the child can replace a later source. */
for (int fd = 0; fd < 3; ++fd)
if (req.stdio[fd] <= 2) {
errno = EBADF;
die("payload stdio source in fd 0-2");
}
/* The child inherits pid1's limits. The broker already set no core dumps
* and no locked memory, hard limits included, so only the open-file
* limit changes here, and pid1 uses few fds. */
if (set_nofile_rlimit_checked(RLIMIT_CHILD_NOFILE) < 0)
die("child rlimit");
/* glibc's execvpe() runs a file without an executable header through
* /bin/sh, with argc + 2 pointers on this stack (posix/execvpe.c); 64 KiB
* covers the rest. argv is a suffix of main()'s argv, so the kernel's
* argument limit bounds argc. 16-byte top. */
while (argv[argc] != NULL)
++argc;
stack_size = ((argc + 3) * sizeof(char *) + 64 * 1024 + 15) & ~(size_t)15;
/* The child's execvpe() searches the PATH in environ. pid1 makes this
* write itself, so the child does not change shared memory, and restores
* environ once the child has stopped sharing memory. */
environ = envp;
stack = mmap(NULL, stack_size + guard, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
if (stack == MAP_FAILED)
die("mmap spawn stack");
if (mprotect(stack, guard, PROT_NONE) < 0)
die("mprotect spawn stack");
if (sp_spawn_sigmask(&all, &req.mask) < 0)
die("block signals for spawn");
pid = clone(sp_spawn_child, stack + guard + stack_size,
CLONE_VM | CLONE_VFORK | SIGCHLD, &req);
if (pid < 0)
die("fork payload");
environ = saved_environ;
if (sp_spawn_sigmask(&req.mask, NULL) < 0)
die("restore signal mask after spawn");
munmap(stack, stack_size + guard);
if (null_fd >= 0)
close(null_fd);
if (req.fail_what != NULL) {
while (waitpid(pid, NULL, 0) < 0 && errno == EINTR)
;
errno = req.fail_errno;
die(req.fail_what);
}
return pid;
}
static int run_payload_init(char **argv, char **envp,
struct interactive_session *session,
const int stdout_pipe[2],
const int stderr_pipe[2], int report_fd) {
sp_init_install_signal_handlers();
bench_phase_begin("payload_fork");
pid_t payload_pid = sp_clone_spawn_payload(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(report_fd);
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) != 1)
die("sendmsg");
}
static int sp_vnet_tun_profile_values_valid(mode_t mode, int fd_flags,
int status_flags,
unsigned int interface_flags,
const char *actual_name,
const char *expected_name,
int header_size,
int little_endian) {
unsigned int required = IFF_TUN | IFF_NO_PI | IFF_VNET_HDR;
return S_ISCHR(mode) && (fd_flags & FD_CLOEXEC) != 0 &&
(status_flags & O_NONBLOCK) != 0 &&
(interface_flags &
(IFF_TUN | IFF_TAP | IFF_NO_PI | IFF_VNET_HDR | IFF_MULTI_QUEUE)) ==
required &&
strncmp(actual_name, expected_name, IFNAMSIZ) == 0 &&
header_size == SP_VNET_HDR_BYTES && little_endian == 1;
}
static int tun_fd_profile_valid(int fd, const char *name) {
struct stat st;
struct ifreq ifr;
int fd_flags;
int status_flags;
int hdrsz = 0;
int little_endian = 0;
fd_flags = fcntl(fd, F_GETFD);
status_flags = fcntl(fd, F_GETFL);
if (fd_flags < 0 || status_flags < 0 || fstat(fd, &st) < 0)
return 0;
memset(&ifr, 0, sizeof(ifr));
if (ioctl(fd, TUNGETIFF, &ifr) < 0 ||
ioctl(fd, TUNGETVNETHDRSZ, &hdrsz) < 0 ||
ioctl(fd, TUNGETVNETLE, &little_endian) < 0)
return 0;
return sp_vnet_tun_profile_values_valid(
st.st_mode, fd_flags, status_flags, (unsigned int)ifr.ifr_flags,
ifr.ifr_name, name, hdrsz, little_endian);
}
/* musl defines CMSG_NXTHDR as a macro that compares signed with unsigned
* values, which clang reports (-Wsign-compare) at every use; glibc's is a
* function. One wrapper keeps that suppression in one place. */
static struct cmsghdr *sp_cmsg_next(struct msghdr *msg, struct cmsghdr *cmsg) {
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wsign-compare"
#endif
return CMSG_NXTHDR(msg, cmsg);
#ifdef __clang__
#pragma clang diagnostic pop
#endif
}
static int recv_fd_checked(int sock, const char *expected_name) {
struct msghdr msg;
int received[2] = {-1, -1};
size_t received_count = 0;
int invalid = 0;
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(received))];
memset(cbuf, 0, sizeof(cbuf));
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 != 1 || byte != 'X' ||
(msg.msg_flags & (MSG_TRUNC | MSG_CTRUNC)) != 0)
invalid = 1;
for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL;
cmsg = sp_cmsg_next(&msg, cmsg)) {
size_t payload_len;
size_t fd_count;
if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS ||
cmsg->cmsg_len < CMSG_LEN(0)) {
invalid = 1;
continue;
}
payload_len = cmsg->cmsg_len - CMSG_LEN(0);
if (payload_len == 0 || payload_len % sizeof(int) != 0) {
invalid = 1;
continue;
}
fd_count = payload_len / sizeof(int);
if (fd_count > sizeof(received) / sizeof(received[0]) - received_count) {
invalid = 1;
fd_count = sizeof(received) / sizeof(received[0]) - received_count;
}
memcpy(received + received_count, CMSG_DATA(cmsg),
fd_count * sizeof(int));
received_count += fd_count;
}
if (received_count != 1)
invalid = 1;
if (!invalid && !tun_fd_profile_valid(received[0], expected_name))
invalid = 1;
if (invalid) {
for (size_t i = 0; i < received_count; ++i)
if (received[i] >= 0)
close(received[i]);
errno = EPROTO;
return -1;
}
return received[0];
}
/* ---------- TUN and interface helpers for child process ---------- */
static unsigned int sp_vnet_tun_offload_request(void) {
return TUN_F_CSUM | TUN_F_TSO4;
}
/* Opened by PID 1 before pivot_root, while the host /dev/net/tun is still
* reachable: the fd is bound to the opener's (already private) network
* namespace at open time, so no /dev/net projection is needed later. */
static int tun_open(void) {
int fd = open("/dev/net/tun", O_RDWR | O_CLOEXEC | O_NONBLOCK);
if (fd < 0)
die("open /dev/net/tun");
return fd;
}
static int tun_create(int fd, const char *name) {
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI | IFF_VNET_HDR;
copy_ifname(ifr.ifr_name, name);
if (ioctl(fd, TUNSETIFF, &ifr) < 0)
die("TUNSETIFF");
int hdrsz = SP_VNET_HDR_BYTES;
int little_endian = 1;
unsigned int offloads = sp_vnet_tun_offload_request();
if (ioctl(fd, TUNSETVNETHDRSZ, &hdrsz) < 0)
die("TUNSETVNETHDRSZ");
if (ioctl(fd, TUNSETVNETLE, &little_endian) < 0)
die("TUNSETVNETLE");
if (ioctl(fd, TUNSETOFFLOAD, offloads) < 0)
die("TUNSETOFFLOAD CSUM|TSO4");
if (!tun_fd_profile_valid(fd, name)) {
errno = EPROTO;
die("TUN VNET profile validation");
}
DBG("TUN fd profile validated: name=%s header=%d endian=little "
"cloexec=yes nonblocking=yes",
name, SP_VNET_HDR_BYTES);
return fd;
}
/* PID 1's interface setup shares one socket of each kind instead of opening a
* fresh socket per step. Both are closed once setup finishes. */
struct sp_net_setup {
int inet_fd;
int rtnl_fd;
uint32_t rtnl_seq;
};
static void sp_net_setup_open(struct sp_net_setup *net) {
struct sockaddr_nl address = {.nl_family = AF_NETLINK};
net->inet_fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (net->inet_fd < 0)
die("socket");
net->rtnl_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
if (net->rtnl_fd < 0 ||
bind(net->rtnl_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
die("rtnetlink socket");
net->rtnl_seq = 0;
}
static void sp_net_setup_close(struct sp_net_setup *net) {
sp_close_owned_fd_nointr(&net->inet_fd);
sp_close_owned_fd_nointr(&net->rtnl_fd);
}
static void if_up(struct sp_net_setup *net, const char *ifname) {
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
copy_ifname(ifr.ifr_name, ifname);
if (ioctl(net->inet_fd, SIOCGIFFLAGS, &ifr) < 0)
die("SIOCGIFFLAGS");
ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
if (ioctl(net->inet_fd, SIOCSIFFLAGS, &ifr) < 0)
die("SIOCSIFFLAGS");
}
/* point-to-point address */
static void if_addr_ptp(struct sp_net_setup *net, const char *ifname,
const char *local, const char *peer) {
struct ifreq ifr = {0};
struct sockaddr_in addr = {.sin_family = AF_INET};
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(net->inet_fd, 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(net->inet_fd, SIOCSIFDSTADDR, &ifr) < 0)
die("SIOCSIFDSTADDR");
}
static int if_index(struct sp_net_setup *net, const char *ifname) {
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
copy_ifname(ifr.ifr_name, ifname);
if (ioctl(net->inet_fd, SIOCGIFINDEX, &ifr) < 0)
die("SIOCGIFINDEX");
return ifr.ifr_ifindex;
}
static int sp_rtnl_add_u32(struct nlmsghdr *nlh, size_t capacity,
unsigned short type, uint32_t value) {
size_t offset = NLMSG_ALIGN(nlh->nlmsg_len);
size_t length = RTA_LENGTH(sizeof(value));
struct rtattr *attr;
if (offset > capacity || length > capacity - offset)
return -1;
attr = (struct rtattr *)((uint8_t *)nlh + offset);
attr->rta_type = type;
attr->rta_len = (unsigned short)length;
memcpy(RTA_DATA(attr), &value, sizeof(value));
nlh->nlmsg_len = (uint32_t)(offset + RTA_ALIGN(length));
return 0;
}
static int sp_rtnl_expect_ack(int fd, uint32_t sequence) {
uint8_t response[4096];
ssize_t length;
do {
length = recv(fd, response, sizeof(response), 0);
} while (length < 0 && errno == EINTR);
if (length <= 0)
return -1;
for (struct nlmsghdr *nlh = (struct nlmsghdr *)response;
NLMSG_OK(nlh, length); nlh = NLMSG_NEXT(nlh, length)) {
struct nlmsgerr *error;
if (nlh->nlmsg_seq != sequence || nlh->nlmsg_type != NLMSG_ERROR ||
nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*error)))
continue;
error = NLMSG_DATA(nlh);
if (error->error == 0)
return 0;
errno = -error->error;
return -1;
}
errno = EPROTO;
return -1;
}
static int sp_link_gso_readback(int fd, int index, uint32_t sequence,
uint32_t expected_size,
uint32_t expected_segs) {
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifi;
} request = {0};
uint8_t response[8192];
ssize_t length;
request.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(request.ifi));
request.nlh.nlmsg_type = RTM_GETLINK;
request.nlh.nlmsg_flags = NLM_F_REQUEST;
request.nlh.nlmsg_seq = sequence;
request.ifi.ifi_family = AF_UNSPEC;
request.ifi.ifi_index = index;
if (send(fd, &request, request.nlh.nlmsg_len, 0) !=
(ssize_t)request.nlh.nlmsg_len)
return -1;
do {
length = recv(fd, response, sizeof(response), 0);
} while (length < 0 && errno == EINTR);
if (length <= 0)
return -1;
for (struct nlmsghdr *nlh = (struct nlmsghdr *)response;
NLMSG_OK(nlh, length); nlh = NLMSG_NEXT(nlh, length)) {
struct ifinfomsg *ifi;
struct rtattr *attr;
int attr_len;
uint32_t size = 0;
uint32_t segs = 0;
if (nlh->nlmsg_seq != sequence || nlh->nlmsg_type != RTM_NEWLINK ||
nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*ifi)))
continue;
ifi = NLMSG_DATA(nlh);
if (ifi->ifi_index != index)
continue;
attr = IFLA_RTA(ifi);
if (IFLA_PAYLOAD(nlh) > INT_MAX) {
errno = EOVERFLOW;
return -1;
}
attr_len = (int)IFLA_PAYLOAD(nlh);
while (attr_len >= (int)sizeof(*attr) &&
attr->rta_len >= sizeof(*attr) && attr->rta_len <= attr_len) {
size_t step = RTA_ALIGN(attr->rta_len);
if (RTA_PAYLOAD(attr) == sizeof(uint32_t)) {
if (attr->rta_type == IFLA_GSO_MAX_SIZE)
memcpy(&size, RTA_DATA(attr), sizeof(size));
else if (attr->rta_type == IFLA_GSO_MAX_SEGS)
memcpy(&segs, RTA_DATA(attr), sizeof(segs));
}
if (step > (size_t)attr_len)
break;
attr_len -= (int)step;
attr = (struct rtattr *)((uint8_t *)attr + step);
}
if (size == expected_size && segs == expected_segs)
return 0;
errno = EPROTO;
return -1;
}
errno = EPROTO;
return -1;
}
/* Send one rtnetlink change and require its acknowledgement: a successful
* send() only means the kernel queued the request, not that it applied it. */
static int sp_rtnl_request_ack(struct sp_net_setup *net, struct nlmsghdr *nlh) {
nlh->nlmsg_flags |= NLM_F_ACK;
nlh->nlmsg_seq = ++net->rtnl_seq;
if (send(net->rtnl_fd, nlh, nlh->nlmsg_len, 0) != (ssize_t)nlh->nlmsg_len)
return -1;
return sp_rtnl_expect_ack(net->rtnl_fd, nlh->nlmsg_seq);
}
static int configure_link_gso(struct sp_net_setup *net, const char *ifname,
int index, uint32_t max_size,
uint32_t max_segs) {
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifi;
uint8_t attributes[64];
} request = {0};
request.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(request.ifi));
request.nlh.nlmsg_type = RTM_NEWLINK;
request.nlh.nlmsg_flags = NLM_F_REQUEST;
request.ifi.ifi_family = AF_UNSPEC;
request.ifi.ifi_index = index;
if (sp_rtnl_add_u32(&request.nlh, sizeof(request), IFLA_GSO_MAX_SIZE,
max_size) < 0 ||
sp_rtnl_add_u32(&request.nlh, sizeof(request), IFLA_GSO_MAX_SEGS,
max_segs) < 0 ||
sp_rtnl_request_ack(net, &request.nlh) < 0 ||
sp_link_gso_readback(net->rtnl_fd, index, ++net->rtnl_seq, max_size,
max_segs) < 0)
return -1;
DBG("TUN GSO limits validated by rtnetlink readback: ifname=%s "
"gso_max_size=%u gso_max_segs=%u",
ifname, max_size, max_segs);
return 0;
}
static void if_up_netlink(struct sp_net_setup *net, int index) {
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;
/* Test hook: name a nonexistent interface so the kernel rejects the
* request and the acknowledgement path sees a real error. */
req.ifi.ifi_index = index;
req.ifi.ifi_flags = IFF_UP | IFF_RUNNING;
req.ifi.ifi_change = IFF_UP | IFF_RUNNING;
if (sp_rtnl_request_ack(net, &req.nlh) < 0)
die("netlink link up");
}
static void add_default_route(struct sp_net_setup *net, int index,
const char *gw) {
struct {
struct nlmsghdr nlh;
struct rtmsg rtm;
uint8_t attributes[64];
} req = {0};
struct in_addr gateway;
if (inet_pton(AF_INET, gw, &gateway) != 1)
die("inet_pton gateway");
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;
if (sp_rtnl_add_u32(&req.nlh, sizeof(req), RTA_GATEWAY, gateway.s_addr) < 0 ||
sp_rtnl_add_u32(&req.nlh, sizeof(req), RTA_OIF, (uint32_t)index) < 0 ||
sp_rtnl_request_ack(net, &req.nlh) < 0)
die("netlink default route");
}
/* ----------- TCP/IP helpers ------------------ */
static int dns_is_synthetic_ip(uint32_t ip_be);
/* ---------- Persistent UDP Flow Management ---------- */
static void udp_close_flow(struct udp_flow *f);
static void sp_udp_outbound_key(struct sp_udp_key *key, uint8_t role, uint32_t cip,
uint16_t cport, uint32_t sip,
uint16_t sport) {
memset(key, 0, sizeof(*key));
key->bytes[0] = role;
key->bytes[1] = IPPROTO_UDP;
memcpy(key->bytes + 4, &cip, sizeof(cip));
memcpy(key->bytes + 8, &sip, sizeof(sip));
memcpy(key->bytes + 12, &cport, sizeof(cport));
memcpy(key->bytes + 14, &sport, sizeof(sport));
}
static void sp_udp_publish_key(struct sp_udp_key *key, int rule_index,
uint32_t local_ip, uint16_t local_port,
const struct sockaddr_in *peer) {
memset(key, 0, sizeof(*key));
key->bytes[0] = SP_UDP_ROLE_PUBLISH;
key->bytes[1] = IPPROTO_UDP;
memcpy(key->bytes + 4, &rule_index, sizeof(rule_index));
memcpy(key->bytes + 8, &local_ip, sizeof(local_ip));
memcpy(key->bytes + 12, &local_port, sizeof(local_port));
memcpy(key->bytes + 16, &peer->sin_addr.s_addr, sizeof(peer->sin_addr.s_addr));
memcpy(key->bytes + 20, &peer->sin_port, sizeof(peer->sin_port));
}
static void sp_udp_publish_child_key(struct sp_udp_key *key, uint16_t synthetic_port,
uint16_t child_port) {
memset(key, 0, sizeof(*key));
key->bytes[0] = SP_UDP_ROLE_PUBLISH;
key->bytes[1] = IPPROTO_UDP;
key->bytes[2] = 1;
memcpy(key->bytes + 4, &synthetic_port, sizeof(synthetic_port));
memcpy(key->bytes + 6, &child_port, sizeof(child_port));
memcpy(key->bytes + 8, &(uint32_t){PUBLISH_SYNTHETIC_IP}, sizeof(uint32_t));
memcpy(key->bytes + 12, &(uint32_t){SANDBOX_IP}, sizeof(uint32_t));
}
/* A probe is private to the typed operation; exhaustion/corruption indicates
* an internal invariant failure, not an absent network tuple. */
static enum ht_step sp_udp_map_probe(const struct sp_udp_map *m,
const struct sp_udp_key *key, struct ht_probe *p, uint32_t *slot,
uint32_t *bucket) {
*p = ht_probe_begin(&m->index, ht_siphash13(key->bytes, sizeof(key->bytes), &m->secret));
enum ht_step step;
while ((step = ht_probe_next(&m->index, p, slot, bucket)) == HT_CANDIDATE)
if (memcmp(m->entries[*slot].key.bytes, key->bytes, sizeof(key->bytes)) == 0)
break;
if (step == HT_CORRUPT || step == HT_EXHAUSTED) abort();
return step;
}
static struct sp_udp_entry *sp_udp_table_lookup_entry(const struct sp_udp_key *key) {
struct ht_probe p;
uint32_t slot, bucket;
return sp_udp_map_probe(&g_udp_map, key, &p, &slot, &bucket) == HT_CANDIDATE
? &g_udp_map.entries[slot] : NULL;
}
static int sp_udp_table_insert(const struct sp_udp_key *key, uint32_t generation,
uint16_t index, uint8_t role) {
struct sp_udp_map *m = &g_udp_map;
struct ht_probe p;
uint32_t slot, bucket;
assert(role == SP_UDP_ROLE_DIRECT || role == SP_UDP_ROLE_SOCKS || role == SP_UDP_ROLE_PUBLISH);
if (!generation || (role == SP_UDP_ROLE_PUBLISH ? index >= MAX_PUBLISH_UDP : index >= MAX_UDP))
return -1;
if (sp_udp_map_probe(m, key, &p, &slot, &bucket) == HT_CANDIDATE || !m->free_count)
return -1;
slot = m->free_slots[m->free_count - 1];
m->entries[slot] = (struct sp_udp_entry){.key = *key, .generation = generation,
.value = {.index = index, .role = role}};
if (ht_insert_at(&m->index, &p, bucket, slot) != HT_INSERTED) abort();
--m->free_count;
return 0;
}
static void sp_udp_table_remove(const struct sp_udp_key *key, uint32_t generation) {
struct sp_udp_map *m = &g_udp_map;
struct ht_probe p;
uint32_t slot, bucket;
if (sp_udp_map_probe(m, key, &p, &slot, &bucket) == HT_CANDIDATE &&
m->entries[slot].generation == generation) {
if (ht_erase_at(&m->index, bucket) != slot) abort();
m->free_slots[m->free_count++] = slot;
}
}
static void udp_mark_progress(struct udp_flow *f, uint64_t now_ns) {
(void)sp_udp_runtime_step(&f->core, SP_UDP_PROGRESS, now_ns, 0);
}
static void udp_arm_idle(struct udp_flow *f, uint64_t now_ns) {
(void)sp_udp_runtime_step(&f->core, SP_UDP_READY, now_ns, 0);
}
static uint8_t sp_udp_admission_role(uint8_t role) {
return (uint8_t)(0x80U | role);
}
enum {
SP_UDP_ADMISSION_RULE = 0xc3,
SP_UDP_ADMISSION_GLOBAL = 0xfe,
};
static int sp_udp_admit_key(uint32_t value, uint8_t admission_role) {
struct sp_admission_key key;
struct sp_admission_entry *entry;
sp_resource_admission_source_key(&key, value, admission_role);
entry = sp_resource_admission_acquire(&key);
if (entry == NULL)
return -1;
if (!sp_resource_admission_take(entry, sp_ns_now_ns())) {
sp_resource_admission_release_source(value, admission_role);
return -1;
}
return 0;
}
static int sp_udp_admit_source(uint32_t source_ip, uint8_t role) {
return sp_udp_admit_key(source_ip, sp_udp_admission_role(role));
}
static int sp_udp_admit_global(void) {
return sp_udp_admit_key(0, SP_UDP_ADMISSION_GLOBAL);
}
static int sp_udp_admit_rule(int rule_index) {
return sp_udp_admit_key((uint32_t)rule_index, SP_UDP_ADMISSION_RULE);
}
static void udp_flow_begin(struct udp_flow *f, uint8_t role) {
uint32_t generation = epoll_next_generation(f->generation);
memset(f, 0, sizeof(*f));
f->generation = generation;
f->used = 1;
(void)sp_udp_runtime_step(&f->core, SP_UDP_BEGIN, 0, role);
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, uint8_t role) {
struct sp_udp_key key;
struct sp_udp_entry *entry;
struct sp_udp_table_value value;
struct udp_flow *f;
sp_udp_outbound_key(&key, role, cip, cport, sip, sport);
entry = sp_udp_table_lookup_entry(&key);
if (entry == NULL)
return NULL;
value = entry->value;
if (value.role != role || value.index >= (uint16_t)MAX_UDP)
return NULL;
f = &udp_flows[value.index];
if (!f->used || f->generation != entry->generation || f->core.role != role ||
f->cli_ip != cip || f->cli_port != cport || f->srv_ip != sip ||
f->srv_port != sport)
return NULL;
return f;
}
static struct udp_flow *udp_alloc(int socks_role) {
int role_count = 0;
int role_limit = socks_role ? SP_UDP_SOCKS_MAX : SP_UDP_DIRECT_MAX;
for (int i = 0; i < udp_flow_limit; ++i) {
int active = udp_flows[i].used;
if (active &&
udp_flows[i].core.role ==
(socks_role ? SP_UDP_ROLE_SOCKS : SP_UDP_ROLE_DIRECT))
role_count++;
}
if (role_count >= role_limit) {
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
DBG("[parent] UDP %s role cap reached (%d); rejecting new mapping",
socks_role ? "SOCKS" : "direct", role_limit);
return NULL;
}
for (int i = 0; i < udp_flow_limit; i++) {
if (!udp_flows[i].used)
return &udp_flows[i];
}
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
DBG("[parent] UDP flow table full (%d slots); rejecting new mapping",
udp_flow_limit);
return NULL;
}
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->core.pending_count != 0)
events |= EPOLLOUT;
epoll_set_udp(f, EPOLL_CTL_MOD, FD_UDP_RELAY, events);
}
static uint8_t *udp_pending_data(uint16_t slot) {
if (slot >= SP_UDP_PENDING_ALL)
return NULL;
return g_resource_udp_pending_arena + (size_t)slot * SP_UDP_PAYLOAD_MAX;
}
static void udp_pending_release(uint16_t slot) {
struct sp_udp_pending_slot *pending;
if (slot >= SP_UDP_PENDING_ALL)
return;
pending = &g_udp_pending_slots[slot];
if (!pending->used)
return;
if (g_udp_pending_live_bytes >= pending->length)
g_udp_pending_live_bytes -= pending->length;
else
g_udp_pending_live_bytes = 0;
memset(pending, 0, sizeof(*pending));
}
/* Store one datagram in the shared pending arena and append it to its owner's
* queue. Returns 0 when queued, -1 for an oversized datagram, 1 for a counted
* terminal drop at the global byte or slot limit, and 2 for a counted terminal
* drop at the per-flow limit. The owner is recorded for flush validation. */
static int sp_udp_pending_store(struct sp_udp_core *core, uint32_t generation,
uint16_t owner_index, uint8_t owner_publish,
const uint8_t *quote, const uint8_t *data,
size_t len) {
uint16_t slot = UINT16_MAX;
if (len > SP_UDP_PAYLOAD_MAX)
return -1;
if (core->pending_count >= SP_UDP_PENDING_PER_FLOW) {
sp_counter_inc(&g_resource_counters.udp_queue_slot_exhaustions);
sp_counter_inc(&g_resource_counters.udp_terminal_drops);
return 2;
}
if (g_udp_pending_live_bytes > SP_UDP_PENDING_BYTES - len) {
sp_counter_inc(&g_resource_counters.udp_queue_byte_exhaustions);
sp_counter_inc(&g_resource_counters.udp_terminal_drops);
return 1;
}
for (uint16_t i = 0; i < SP_UDP_PENDING_ALL; ++i) {
if (!g_udp_pending_slots[i].used) {
slot = i;
break;
}
}
if (slot == UINT16_MAX) {
sp_counter_inc(&g_resource_counters.udp_queue_slot_exhaustions);
sp_counter_inc(&g_resource_counters.udp_terminal_drops);
return 1;
}
g_udp_pending_slots[slot].used = 1;
g_udp_pending_slots[slot].generation = generation;
g_udp_pending_slots[slot].length = (uint16_t)len;
g_udp_pending_slots[slot].owner_index = owner_index;
g_udp_pending_slots[slot].owner_role = core->role;
g_udp_pending_slots[slot].owner_publish = owner_publish;
if (quote != NULL) {
memcpy(g_udp_pending_slots[slot].quote, quote,
sizeof(g_udp_pending_slots[slot].quote));
g_udp_pending_slots[slot].has_quote = 1;
}
memcpy(udp_pending_data(slot), data, len);
g_udp_pending_live_bytes += len;
(void)sp_udp_queue(core, slot);
return 0;
}
static int udp_queue_pending(struct udp_flow *f, const uint8_t *data, size_t len,
const uint8_t *canonical_quote) {
int rc = sp_udp_pending_store(&f->core, f->generation,
(uint16_t)(f - udp_flows), 0, canonical_quote,
data, len);
if (rc == 2) {
f->dropped_backpressure++;
DBG("UDP pending queue full; dropping newest datagram (drops=%lu)",
f->dropped_backpressure);
return 1;
}
if (rc == 0)
udp_update_events(f);
return rc;
}
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;
const char *option_reason = "unknown";
if (tcp_sock < 0)
return -1;
udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_sock < 0) {
close(tcp_sock);
return -1;
}
if (sp_udp_configure_host_socket(
udp_sock, &f->effective_sndbuf, &f->effective_rcvbuf,
&option_reason) < 0 ||
bind(udp_sock, (struct sockaddr *)&local, sizeof(local)) < 0) {
DBG("SOCKS UDP option/bind setup failed: %s", option_reason);
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.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;
(void)sp_udp_runtime_step(&f->core, SP_UDP_NEGOTIATE, sp_ns_now_ns(), 0);
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;
}
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);
const char *option_reason = "unknown";
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 (sp_udp_configure_host_socket(
udp_sock, &f->effective_sndbuf, &f->effective_rcvbuf,
&option_reason) < 0 ||
connect(udp_sock, (struct sockaddr *)&dst, sizeof(dst)) < 0) {
DBG("direct UDP option/connect setup failed: %s", option_reason);
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;
f->connected_remote = dst;
udp_arm_idle(f, sp_ns_now_ns());
return 0;
}
enum sp_udp_send_result {
SP_UDP_SEND_FATAL = -1,
SP_UDP_SEND_DONE = 0,
SP_UDP_SEND_RETAIN = 1,
SP_UDP_SEND_EMSGSIZE = 2,
SP_UDP_SEND_REFUSED = 3,
SP_UDP_SEND_DROPPED = 4,
};
/* A positive result retains ownership only for RETAIN. MTU/refusal results
* are terminal for the datagram and let the caller synthesize the narrowly
* permitted ICMP result from its canonical quote. */
static int udp_socks_mapping(const struct udp_flow *f,
struct sp_dns_name_view *mapping) {
/* A synthetic destination must stay tied to the mapping acquired at flow
* admission. Peeking an unpinned slot here could silently encode a new
* name after expiry/reuse; numeric destinations simply use their address. */
if (f == NULL || mapping == NULL || f->dns_mapping_ref.generation == 0)
return 0;
if (sp_dns_core_mapping_name(&g_dns_core, f->dns_mapping_ref, mapping) < 0)
return 0;
return mapping->synthetic_ip == f->srv_ip;
}
static int udp_socks_encode_packet(const struct sp_net_policy_snapshot *policy,
const struct udp_flow *f, const uint8_t *payload, size_t payload_len,
uint8_t *packet, size_t capacity, size_t *packet_len) {
struct sp_dns_name_view mapping;
int mapped = policy->socks_remote_dns && udp_socks_mapping(f, &mapping);
if (!mapped && policy->socks_remote_dns && dns_is_synthetic_ip(f->srv_ip))
return -1;
return sp_socks_udp_encode((struct sp_socks_endpoint){f->srv_ip, f->srv_port},
mapped ? mapping.name : NULL, mapped ? mapping.name_len : 0,
payload, payload_len, packet, capacity, packet_len);
}
static int udp_send_socks_now(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};
size_t packet_len;
unsigned int interrupted = 0;
if (udp_socks_encode_packet(&g_net_policy, f, payload, payload_len, pkt,
sizeof(pkt), &packet_len) < 0)
return -1;
if (verbose) {
/* Separate buffers: inet_ntoa() returns one shared static buffer. */
char relay_text[INET_ADDRSTRLEN], dst_text[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &f->relay_addr.sin_addr, relay_text, sizeof(relay_text));
inet_ntop(AF_INET, &dst_addr, dst_text, sizeof(dst_text));
DBG("UDP via SOCKS relay %s:%d -> %s:%d len=%zu", relay_text,
ntohs(f->relay_addr.sin_port), dst_text, f->srv_port, payload_len);
}
for (;;) {
ssize_t n = sendto(f->udp_relay, pkt, packet_len, 0,
(struct sockaddr *)&f->relay_addr,
sizeof(f->relay_addr));
sp_resource_count_host_io(1, 1, n);
if (n == (ssize_t)packet_len) {
udp_mark_progress(f, sp_ns_now_ns());
return 0;
}
if (n >= 0) {
DBG("SOCKS UDP short send: %zd/%zu, closing flow", n, packet_len);
return -1;
}
if (errno == EINTR && interrupted++ < 4)
continue;
if (errno == EINTR)
return SP_UDP_SEND_RETAIN;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return SP_UDP_SEND_RETAIN;
if (errno == EMSGSIZE)
return SP_UDP_SEND_EMSGSIZE;
if (errno == ECONNREFUSED)
return SP_UDP_SEND_REFUSED;
if (errno == ENOBUFS || errno == ENOMEM) {
f->dropped_backpressure++;
sp_counter_inc(&g_resource_counters.udp_terminal_drops);
DBG("SOCKS UDP send drop due to %s (drops=%lu)", strerror(errno),
f->dropped_backpressure);
return SP_UDP_SEND_DROPPED;
}
return -1;
}
}
static int udp_socks_decode_payload(const struct udp_flow *f,
const uint8_t *packet, size_t packet_len, const uint8_t **payload,
size_t *payload_len) {
struct sp_dns_name_view mapping;
int mapped = packet_len >= 4 && packet[3] == 3 && udp_socks_mapping(f, &mapping);
return sp_socks_udp_decode((struct sp_socks_endpoint){f->srv_ip, f->srv_port},
mapped ? mapping.name : NULL, mapped ? mapping.name_len : 0,
packet, packet_len, payload, payload_len);
}
static int udp_source_matches(const struct udp_flow *f,
const struct sockaddr_in *source,
socklen_t source_len) {
return f != NULL && source != NULL && source_len == sizeof(*source) &&
source->sin_family == AF_INET &&
source->sin_addr.s_addr == f->connected_remote.sin_addr.s_addr &&
source->sin_port == f->connected_remote.sin_port;
}
enum sp_udp_mmsg_receive_result {
SP_UDP_MMSG_RECEIVE_ERROR = -1,
};
static int sp_udp_emit_error(int tunfd, const uint8_t *quote,
const struct udp_flow *f,
enum sp_udp_send_result result);
struct sp_packet_value;
static int sp_udp_dispatch_values(const uint8_t *packet, size_t packet_len,
const struct sp_packet_value *parsed,
uint32_t *src_ip, uint32_t *dst_ip,
uint16_t *src_port, uint16_t *dst_port,
const uint8_t **payload,
size_t *payload_len);
static void sp_udp_emit_tun(int tunfd, uint32_t src_ip, uint32_t dst_ip,
uint16_t src_port, uint16_t dst_port,
const uint8_t *payload, size_t payload_len,
const char *context);
static unsigned int sp_udp_mmsg_prepare(unsigned int requested) {
unsigned int count =
requested > SP_UDP_MMSG_MAX ? SP_UDP_MMSG_MAX : requested;
for (unsigned int i = 0; i < count; ++i) {
memset(&g_udp_mmsg_messages[i], 0, sizeof(g_udp_mmsg_messages[i]));
memset(&g_udp_mmsg_iov[i], 0, sizeof(g_udp_mmsg_iov[i]));
memset(&g_udp_mmsg_sources[i], 0, sizeof(g_udp_mmsg_sources[i]));
memset(g_udp_mmsg_control[i], 0, sizeof(g_udp_mmsg_control[i]));
memset(g_udp_mmsg_payload[i], 0, sizeof(g_udp_mmsg_payload[i]));
g_udp_mmsg_iov[i].iov_base = g_udp_mmsg_payload[i];
g_udp_mmsg_iov[i].iov_len = sizeof(g_udp_mmsg_payload[i]);
g_udp_mmsg_messages[i].msg_hdr.msg_name = &g_udp_mmsg_sources[i];
g_udp_mmsg_messages[i].msg_hdr.msg_namelen =
sizeof(g_udp_mmsg_sources[i]);
g_udp_mmsg_messages[i].msg_hdr.msg_iov = &g_udp_mmsg_iov[i];
g_udp_mmsg_messages[i].msg_hdr.msg_iovlen = 1;
g_udp_mmsg_messages[i].msg_hdr.msg_control = g_udp_mmsg_control[i];
g_udp_mmsg_messages[i].msg_hdr.msg_controllen =
sizeof(g_udp_mmsg_control[i]);
}
return count;
}
/* Submit one fd and no timeout. EINTR has no ownership effect and is retried
* only while both the fixed retry count and callback-time budget permit. */
static int sp_udp_recvmmsg_one_fd(int fd, unsigned int requested,
uint64_t started_ns) {
unsigned int count = sp_udp_mmsg_prepare(requested);
unsigned int interrupted = 0;
if (count == 0)
return 0;
for (;;) {
int result;
sp_counter_inc(&g_resource_counters.udp_recvmmsg_calls);
sp_counter_inc(&g_resource_counters.host_datagram_read_calls);
result = g_sp_recvmmsg(fd, g_udp_mmsg_messages, count,
MSG_DONTWAIT | MSG_CMSG_CLOEXEC, NULL);
if (result >= 0) {
uint64_t bytes = 0;
if (result == 0)
sp_counter_inc(&g_resource_counters.udp_recvmmsg_zero_results);
else
sp_counter_add(&g_resource_counters.udp_recvmmsg_datagrams,
(uint64_t)(unsigned int)result);
for (int i = 0; i < result; ++i)
bytes += g_udp_mmsg_messages[i].msg_len;
sp_counter_add(&g_resource_counters.host_datagram_read_bytes, bytes);
return result;
}
if (errno != EINTR)
return SP_UDP_MMSG_RECEIVE_ERROR;
interrupted++;
if (interrupted >= 4 ||
(started_ns != 0 && sp_ns_now_ns() >= started_ns &&
sp_ns_now_ns() - started_ns >= (uint64_t)SP_CALLBACK_BUDGET_NS)) {
sp_counter_inc(&g_resource_counters.udp_recvmmsg_eintr_yields);
errno = EINTR;
return SP_UDP_MMSG_RECEIVE_ERROR;
}
sp_udp_mmsg_prepare(count);
}
}
static int sp_udp_mmsg_slot_basic_valid(unsigned int index) {
const struct mmsghdr *slot;
const struct msghdr *message;
if (index >= SP_UDP_MMSG_MAX)
return 0;
slot = &g_udp_mmsg_messages[index];
message = &slot->msg_hdr;
return slot->msg_len <= sizeof(g_udp_mmsg_payload[index]) &&
(message->msg_flags & (MSG_TRUNC | MSG_CTRUNC)) == 0 &&
message->msg_namelen == sizeof(g_udp_mmsg_sources[index]) &&
g_udp_mmsg_sources[index].sin_family == AF_INET &&
message->msg_controllen <= sizeof(g_udp_mmsg_control[index]);
}
enum sp_udp_mmsg_send_result {
SP_UDP_MMSG_SEND_ERROR = -1,
};
static unsigned int sp_udp_mmsg_tx_reset(unsigned int requested) {
unsigned int count =
requested > SP_UDP_MMSG_MAX ? SP_UDP_MMSG_MAX : requested;
for (unsigned int i = 0; i < count; ++i) {
memset(&g_udp_mmsg_tx_messages[i], 0,
sizeof(g_udp_mmsg_tx_messages[i]));
memset(&g_udp_mmsg_tx_iov[i], 0, sizeof(g_udp_mmsg_tx_iov[i]));
memset(&g_udp_mmsg_tx_names[i], 0, sizeof(g_udp_mmsg_tx_names[i]));
memset(g_udp_mmsg_tx_control[i], 0,
sizeof(g_udp_mmsg_tx_control[i]));
memset(g_udp_mmsg_tx_payload[i], 0, sizeof(g_udp_mmsg_tx_payload[i]));
g_udp_mmsg_tx_messages[i].msg_hdr.msg_iov = &g_udp_mmsg_tx_iov[i];
g_udp_mmsg_tx_messages[i].msg_hdr.msg_iovlen = 1;
}
return count;
}
static int sp_udp_sendmmsg_one_fd(int fd, unsigned int count,
uint64_t started_ns) {
unsigned int interrupted = 0;
if (count == 0 || count > SP_UDP_MMSG_MAX) {
errno = EINVAL;
return count == 0 ? 0 : SP_UDP_MMSG_SEND_ERROR;
}
for (;;) {
int result;
for (unsigned int i = 0; i < count; ++i)
g_udp_mmsg_tx_messages[i].msg_len = 0;
result = g_sp_sendmmsg(fd, g_udp_mmsg_tx_messages, count,
MSG_DONTWAIT | MSG_NOSIGNAL);
if (result >= 0) {
uint64_t bytes = 0;
if (result == 0)
sp_counter_inc(&g_resource_counters.udp_sendmmsg_zero_results);
else
sp_counter_add(&g_resource_counters.udp_sendmmsg_datagrams,
(uint64_t)(unsigned int)result);
if (result > 0 && (unsigned int)result < count)
sp_counter_inc(&g_resource_counters.udp_sendmmsg_partial_prefixes);
for (int i = 0; i < result; ++i)
bytes += g_udp_mmsg_tx_messages[i].msg_len;
sp_counter_add(&g_resource_counters.host_datagram_write_bytes, bytes);
return result;
}
if (errno != EINTR)
return SP_UDP_MMSG_SEND_ERROR;
interrupted++;
if (interrupted >= 4 ||
(started_ns != 0 && sp_ns_now_ns() >= started_ns &&
sp_ns_now_ns() - started_ns >= (uint64_t)SP_CALLBACK_BUDGET_NS)) {
sp_counter_inc(&g_resource_counters.udp_sendmmsg_eintr_yields);
errno = EINTR;
return SP_UDP_MMSG_SEND_ERROR;
}
}
}
/* UDP I/O uses only recvmmsg/sendmmsg (Linux >= 3.0), and our own seccomp
* policies allow them. An outer runtime policy can still deny them; detect
* that before any flow depends on UDP. With an invalid fd, an available
* syscall fails with EBADF. The probe passes the runtime vector length and
* flags so a policy that pins those arguments (ours does) still accepts it. */
static int sp_udp_mmsg_available(void) {
uint8_t byte = 0;
struct iovec iov = {.iov_base = &byte, .iov_len = 0};
struct mmsghdr message = {.msg_hdr = {.msg_iov = &iov, .msg_iovlen = 1}};
if (g_sp_recvmmsg(-1, &message, 1, MSG_DONTWAIT | MSG_CMSG_CLOEXEC, NULL) >=
0 ||
errno != EBADF)
return -1;
if (g_sp_sendmmsg(-1, &message, 1, MSG_DONTWAIT | MSG_NOSIGNAL) >= 0 ||
errno != EBADF)
return -1;
return 0;
}
static int udp_receive_mmsg(int tunfd, struct udp_flow *f) {
uint64_t started_ns = sp_ns_now_ns();
int received =
sp_udp_recvmmsg_one_fd(f->udp_relay, SP_UDP_MMSG_MAX, started_ns);
if (received < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
return 0;
if (errno == ECONNREFUSED && f->has_last_quote)
(void)sp_udp_emit_error(tunfd, f->last_quote, f, SP_UDP_SEND_REFUSED);
return -1;
}
for (int i = 0; i < received; ++i) {
const uint8_t *payload = g_udp_mmsg_payload[i];
size_t payload_len = g_udp_mmsg_messages[i].msg_len;
if (!sp_udp_mmsg_slot_basic_valid((unsigned int)i) ||
g_udp_mmsg_messages[i].msg_hdr.msg_controllen != 0) {
continue;
}
if (!udp_source_matches(f, &g_udp_mmsg_sources[i],
g_udp_mmsg_messages[i].msg_hdr.msg_namelen)) {
continue;
}
if (f->core.role == SP_UDP_ROLE_SOCKS) {
if (udp_socks_decode_payload(f, payload, payload_len, &payload,
&payload_len) < 0) {
continue;
}
}
if (payload_len > SP_UDP_PAYLOAD_MAX) {
continue;
}
sp_udp_emit_tun(tunfd, ntohl(f->srv_ip), ntohl(f->cli_ip), f->srv_port,
f->cli_port, payload, payload_len, "UDP inject");
udp_mark_progress(f, sp_ns_now_ns());
}
return 0;
}
static uint16_t udp_effective_path_mtu(const struct udp_flow *f,
uint16_t rejected_ip_len) {
int mtu = 0;
socklen_t length = sizeof(mtu);
if (f == NULL || !f->used || f->core.state != SP_UDP_MAPPING_READY ||
f->udp_relay < 0 ||
getsockopt(f->udp_relay, IPPROTO_IP, IP_MTU, &mtu, &length) < 0 ||
length != sizeof(mtu) || mtu < 68 || mtu > 1499)
return 0;
if (f->core.role == SP_UDP_ROLE_SOCKS) {
struct sp_dns_name_view mapping;
int have_mapping = udp_socks_mapping(f, &mapping);
if (dns_is_synthetic_ip(f->srv_ip) && !have_mapping)
return 0;
size_t header_len = have_mapping ? 7U + mapping.name_len : 10U;
if ((size_t)mtu <= header_len)
return 0;
mtu -= (int)header_len;
}
if (mtu < 68 || mtu >= rejected_ip_len)
return 0;
return (uint16_t)mtu;
}
static int udp_send_direct_now(struct udp_flow *f, const uint8_t *payload,
size_t payload_len) {
unsigned int interrupted = 0;
for (;;) {
ssize_t n = send(f->udp_relay, payload, payload_len, MSG_NOSIGNAL);
sp_resource_count_host_io(1, 1, n);
if (n == (ssize_t)payload_len) {
udp_mark_progress(f, sp_ns_now_ns());
return 0;
}
if (n >= 0) {
DBG("UDP short send: %zd/%zu, closing flow", n, payload_len);
return -1;
}
if (errno == EINTR && interrupted++ < 4)
continue;
if (errno == EINTR)
return SP_UDP_SEND_RETAIN;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return SP_UDP_SEND_RETAIN;
if (errno == EMSGSIZE)
return SP_UDP_SEND_EMSGSIZE;
if (errno == ECONNREFUSED)
return SP_UDP_SEND_REFUSED;
if (errno == ENOBUFS || errno == ENOMEM) {
f->dropped_backpressure++;
sp_counter_inc(&g_resource_counters.udp_terminal_drops);
DBG("UDP send drop due to %s (drops=%lu)", strerror(errno),
f->dropped_backpressure);
return SP_UDP_SEND_DROPPED;
}
return -1;
}
}
static int sp_udp_emit_error(int tunfd, const uint8_t *quote,
const struct udp_flow *f,
enum sp_udp_send_result result);
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;
f->connected_remote = *relay_addr;
udp_arm_idle(f, sp_ns_now_ns());
epoll_set_udp(f, EPOLL_CTL_ADD, FD_UDP_RELAY, EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP);
udp_update_events(f);
return 0;
}
static int udp_flush_pending(int tunfd, struct udp_flow *f) {
uint16_t submitted_slots[SP_UDP_MMSG_MAX];
unsigned int count;
int result;
int invariant_failed = 0;
if (f->udp_relay < 0 || f->core.pending_count == 0)
return 0;
count = sp_udp_mmsg_tx_reset(f->core.pending_count);
for (unsigned int i = 0; i < count; ++i) {
size_t queue_index =
(f->core.pending_head + i) % SP_UDP_PENDING_PER_FLOW;
uint16_t slot = f->core.pending_slots[queue_index];
struct sp_udp_pending_slot *pending;
uint8_t *payload;
size_t wire_len;
if (slot >= SP_UDP_PENDING_ALL)
return -1;
pending = &g_udp_pending_slots[slot];
if (!pending->used || pending->generation != f->generation ||
pending->owner_index != (uint16_t)(f - udp_flows) ||
pending->owner_role != f->core.role || pending->owner_publish)
return -1;
payload = udp_pending_data(slot);
submitted_slots[i] = slot;
if (f->core.role == SP_UDP_ROLE_SOCKS) {
if (udp_socks_encode_packet(&g_net_policy, f, payload, pending->length,
g_udp_mmsg_tx_payload[i],
sizeof(g_udp_mmsg_tx_payload[i]),
&wire_len) < 0)
return -1;
g_udp_mmsg_tx_iov[i].iov_base = g_udp_mmsg_tx_payload[i];
g_udp_mmsg_tx_iov[i].iov_len = wire_len;
g_udp_mmsg_tx_names[i] = f->relay_addr;
g_udp_mmsg_tx_messages[i].msg_hdr.msg_name =
&g_udp_mmsg_tx_names[i];
g_udp_mmsg_tx_messages[i].msg_hdr.msg_namelen =
sizeof(g_udp_mmsg_tx_names[i]);
} else {
g_udp_mmsg_tx_iov[i].iov_base = payload;
g_udp_mmsg_tx_iov[i].iov_len = pending->length;
}
}
result = sp_udp_sendmmsg_one_fd(f->udp_relay, count, sp_ns_now_ns());
if (result == 0) {
errno = EIO;
return -1;
}
if (result < 0) {
uint16_t slot = submitted_slots[0];
struct sp_udp_pending_slot *pending = &g_udp_pending_slots[slot];
int send_result;
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
udp_update_events(f);
return 0;
}
if (errno == EMSGSIZE)
send_result = SP_UDP_SEND_EMSGSIZE;
else if (errno == ECONNREFUSED)
send_result = SP_UDP_SEND_REFUSED;
else if (errno == ENOBUFS || errno == ENOMEM)
send_result = SP_UDP_SEND_DROPPED;
else
return -1;
if ((send_result == SP_UDP_SEND_EMSGSIZE ||
send_result == SP_UDP_SEND_REFUSED) &&
pending->has_quote)
(void)sp_udp_emit_error(tunfd, pending->quote, f,
(enum sp_udp_send_result)send_result);
if (send_result == SP_UDP_SEND_DROPPED)
sp_counter_inc(&g_resource_counters.udp_terminal_drops);
udp_pending_release(slot);
sp_udp_pop(&f->core);
udp_update_events(f);
return 0;
}
for (int i = 0; i < result; ++i) {
uint16_t slot = submitted_slots[i];
if (g_udp_mmsg_tx_messages[i].msg_len !=
g_udp_mmsg_tx_iov[i].iov_len)
invariant_failed = 1;
if (f->core.pending_count == 0 ||
f->core.pending_slots[f->core.pending_head] != slot)
return -1;
udp_pending_release(slot);
sp_udp_pop(&f->core);
udp_mark_progress(f, sp_ns_now_ns());
}
udp_update_events(f);
return invariant_failed ? -1 : 0;
}
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);
}
SP_TEST_API static int sp_dns_decode_query(const uint8_t *query, size_t len,
struct sp_dns_query *out) {
struct sp_dns_query candidate;
size_t off = 12;
size_t name_len = 0;
size_t labels = 0;
size_t question_end;
if (query == NULL || out == NULL || len < 12)
return -1;
if (dns_get_u16(query + 4) != 1 || (dns_get_u16(query + 2) & 0x8000U) != 0)
return -1;
memset(&candidate, 0, sizeof(candidate));
candidate.identifier = dns_get_u16(query);
candidate.request_flags = dns_get_u16(query + 2);
for (;;) {
uint8_t label_len;
if (off >= len)
return -1;
label_len = query[off++];
if (label_len == 0)
break;
if ((label_len & 0xc0U) != 0 || label_len > 63 ||
(size_t)label_len > len - off)
return -1;
if (labels != 0) {
if (name_len + 1 >= sizeof(candidate.name))
return -1;
candidate.name[name_len++] = '.';
}
if ((size_t)label_len > sizeof(candidate.name) - 1U - 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;
if (ch >= 'A' && ch <= 'Z')
ch = (unsigned char)(ch - 'A' + 'a');
candidate.name[name_len++] = (char)ch;
}
off += label_len;
++labels;
}
if (name_len == 0 || off > len - 4U)
return -1;
question_end = off + 4U;
if (question_end - 12U > sizeof(candidate.question) ||
question_end - 12U > SP_DNS_QUESTION_MAX)
return -1;
candidate.name_len = (uint16_t)name_len;
candidate.qtype = dns_get_u16(query + off);
candidate.qclass = dns_get_u16(query + off + 2);
candidate.question_len = (uint16_t)(question_end - 12U);
memcpy(candidate.question, query + 12, candidate.question_len);
*out = candidate;
return 0;
}
static uint64_t sp_dns_core_ttl(const struct sp_dns_core *core) {
return core != NULL && core->ttl_ns != 0 ? core->ttl_ns : SP_DNS_TTL_NS;
}
static uint64_t sp_dns_deadline_ns(uint64_t now_ns, uint64_t ttl_ns) {
return UINT64_MAX - now_ns < ttl_ns ? UINT64_MAX : now_ns + ttl_ns;
}
static struct sp_dns_ref sp_dns_ref_invalid(void) {
return (struct sp_dns_ref){.slot = SP_DNS_SLOT_INVALID, .generation = 0};
}
static int sp_dns_mapping_shape_valid(const struct dns_mapping *mapping) {
return mapping != NULL && mapping->used && mapping->generation != 0 &&
mapping->synthetic_ip != 0 && mapping->name_len != 0 &&
mapping->name_len <= SP_DNS_NAME_MAX &&
mapping->name[mapping->name_len] == '\0';
}
static int sp_dns_lookup_key_valid(const struct sp_dns_lookup_key *key) {
switch (key->kind) {
case SP_DNS_LOOKUP_EXPIRE:
return 1;
case SP_DNS_LOOKUP_NAME:
return key->name != NULL && key->name_len != 0 &&
key->name_len <= SP_DNS_NAME_MAX &&
key->name[key->name_len] == '\0';
case SP_DNS_LOOKUP_IP:
return key->synthetic_ip != 0;
}
return 0;
}
/* Perform the table's only ordinary lookup/selection walk. Expiry is folded
* into the walk so a query does not scan once to expire, again to find its
* key, and again to select a replacement. The walk is bounded by the fixed
* table size and returns indices only; callers decide whether to refresh,
* acquire or replace after validation and output-credit checks. */
static int sp_dns_core_lookup(struct sp_dns_core *core,
const struct sp_dns_lookup_key *key,
uint64_t now_ns,
struct sp_dns_lookup_result *out) {
uint64_t oldest_ns = UINT64_MAX;
if (!sp_dns_lookup_key_valid(key))
return -1;
*out = (struct sp_dns_lookup_result){
.match_slot = SP_DNS_SLOT_INVALID,
.free_slot = SP_DNS_SLOT_INVALID,
.oldest_unpinned_slot = SP_DNS_SLOT_INVALID,
};
for (uint16_t i = 0; i < MAX_DNS_MAPPINGS; ++i) {
struct dns_mapping *mapping = &core->mappings[i];
if (!mapping->used) {
if (out->free_slot == SP_DNS_SLOT_INVALID)
out->free_slot = i;
continue;
}
if (!sp_dns_mapping_shape_valid(mapping))
return -1;
if (mapping->active_refs == 0 && mapping->expires_ns != 0 &&
now_ns >= mapping->expires_ns) {
memset(mapping, 0, sizeof(*mapping));
if (out->free_slot == SP_DNS_SLOT_INVALID)
out->free_slot = i;
continue;
}
if (key->kind == SP_DNS_LOOKUP_NAME &&
mapping->name_len == key->name_len &&
memcmp(mapping->name, key->name, key->name_len) == 0) {
if (out->match_slot == SP_DNS_SLOT_INVALID)
out->match_slot = i;
} else if (key->kind == SP_DNS_LOOKUP_IP &&
mapping->synthetic_ip == key->synthetic_ip) {
if (out->match_slot == SP_DNS_SLOT_INVALID)
out->match_slot = i;
}
if (key->kind == SP_DNS_LOOKUP_NAME && mapping->active_refs == 0 &&
(out->oldest_unpinned_slot == SP_DNS_SLOT_INVALID ||
mapping->last_used_ns < oldest_ns)) {
out->oldest_unpinned_slot = i;
oldest_ns = mapping->last_used_ns;
}
}
return 0;
}
static int sp_dns_core_ip_in_use(const struct sp_dns_core *core,
uint32_t synthetic_ip) {
if (synthetic_ip == 0)
return 0;
for (uint16_t i = 0; i < MAX_DNS_MAPPINGS; ++i)
if (core->mappings[i].used &&
core->mappings[i].synthetic_ip == synthetic_ip)
return 1;
return 0;
}
static uint32_t sp_dns_core_next_ip(struct sp_dns_core *core) {
uint32_t cursor;
cursor = core->next_host;
if (cursor == 0 || cursor > 0xfffeU)
cursor = 1;
for (uint32_t attempt = 0; attempt < 0xfffeU; ++attempt) {
uint32_t host = cursor;
uint32_t ip;
cursor = host == 0xfffeU ? 1U : host + 1U;
ip = htonl(DNS_SYNTHETIC_BASE | host);
if (!sp_dns_core_ip_in_use(core, ip)) {
core->next_host = cursor;
return ip;
}
}
core->next_host = cursor;
return 0;
}
static uint32_t sp_dns_core_next_generation(struct sp_dns_core *core) {
uint32_t generation;
generation = core->next_generation;
if (generation == 0)
generation = 1;
core->next_generation = generation == UINT32_MAX ? 1U : generation + 1U;
return generation;
}
static int sp_dns_core_get_or_create(
struct sp_dns_core *core, const char *name, uint16_t name_len,
uint64_t now_ns, uint32_t *synthetic_ip) {
struct sp_dns_lookup_key key;
struct sp_dns_lookup_result lookup;
struct dns_mapping *mapping;
uint16_t slot;
uint32_t candidate_ip;
uint32_t next_generation;
*synthetic_ip = 0;
key = (struct sp_dns_lookup_key){
.kind = SP_DNS_LOOKUP_NAME, .name = name, .name_len = name_len};
if (sp_dns_core_lookup(core, &key, now_ns, &lookup) < 0)
return -1;
if (lookup.match_slot != SP_DNS_SLOT_INVALID) {
mapping = &core->mappings[lookup.match_slot];
mapping->last_used_ns = now_ns;
mapping->expires_ns = sp_dns_deadline_ns(now_ns, sp_dns_core_ttl(core));
*synthetic_ip = mapping->synthetic_ip;
return 0;
}
slot = lookup.free_slot != SP_DNS_SLOT_INVALID
? lookup.free_slot
: lookup.oldest_unpinned_slot;
if (slot == SP_DNS_SLOT_INVALID)
return -1;
mapping = &core->mappings[slot];
candidate_ip = sp_dns_core_next_ip(core);
if (candidate_ip == 0)
return -1;
next_generation = sp_dns_core_next_generation(core);
memset(mapping, 0, sizeof(*mapping));
mapping->used = 1;
mapping->generation = next_generation;
mapping->synthetic_ip = candidate_ip;
mapping->last_used_ns = now_ns;
mapping->expires_ns = sp_dns_deadline_ns(now_ns, sp_dns_core_ttl(core));
mapping->name_len = name_len;
memcpy(mapping->name, name, name_len);
mapping->name[name_len] = '\0';
*synthetic_ip = candidate_ip;
return 0;
}
static const struct dns_mapping *sp_dns_core_ref_get(
const struct sp_dns_core *core, struct sp_dns_ref ref) {
const struct dns_mapping *mapping;
if (ref.slot == SP_DNS_SLOT_INVALID || ref.slot >= MAX_DNS_MAPPINGS || ref.generation == 0)
return NULL;
mapping = &core->mappings[ref.slot];
return sp_dns_mapping_shape_valid(mapping) &&
mapping->generation == ref.generation
? mapping
: NULL;
}
static struct dns_mapping *sp_dns_core_ref_get_mut(struct sp_dns_core *core,
struct sp_dns_ref ref) {
return (struct dns_mapping *)sp_dns_core_ref_get(core, ref);
}
SP_TEST_API static int sp_dns_core_mapping_name(
const struct sp_dns_core *core, struct sp_dns_ref ref,
struct sp_dns_name_view *out) {
const struct dns_mapping *mapping;
*out = (struct sp_dns_name_view){0};
mapping = sp_dns_core_ref_get(core, ref);
if (mapping == NULL || mapping->active_refs == 0)
return -1;
out->synthetic_ip = mapping->synthetic_ip;
out->name_len = mapping->name_len;
out->name = mapping->name;
return 0;
}
SP_TEST_API static int sp_dns_core_acquire_ip(
struct sp_dns_core *core, uint32_t synthetic_ip, uint64_t now_ns,
struct sp_dns_ref *out) {
struct sp_dns_lookup_key key;
struct sp_dns_lookup_result lookup;
struct dns_mapping *mapping;
if (synthetic_ip == 0)
return -1;
*out = sp_dns_ref_invalid();
key = (struct sp_dns_lookup_key){
.kind = SP_DNS_LOOKUP_IP, .synthetic_ip = synthetic_ip};
if (sp_dns_core_lookup(core, &key, now_ns, &lookup) < 0 ||
lookup.match_slot == SP_DNS_SLOT_INVALID)
return -1;
mapping = &core->mappings[lookup.match_slot];
if (mapping->active_refs == UINT32_MAX)
return -1;
mapping->active_refs++;
mapping->last_used_ns = now_ns;
mapping->expires_ns = sp_dns_deadline_ns(now_ns, sp_dns_core_ttl(core));
out->slot = lookup.match_slot;
out->generation = mapping->generation;
return 0;
}
SP_TEST_API static int sp_dns_core_release(
struct sp_dns_core *core, struct sp_dns_ref ref, uint64_t now_ns) {
struct dns_mapping *mapping;
mapping = sp_dns_core_ref_get_mut(core, ref);
if (mapping == NULL || mapping->active_refs == 0)
return -1;
mapping->active_refs--;
if (mapping->active_refs == 0 && mapping->expires_ns != 0 &&
now_ns >= mapping->expires_ns)
memset(mapping, 0, sizeof(*mapping));
return 0;
}
_Static_assert(12U + SP_DNS_QUESTION_MAX + 16U <= SP_DNS_RESPONSE_MAX,
"a decoded DNS question and A answer must fit the response");
_Static_assert(SP_DNS_QUESTION_MAX <= sizeof(((struct sp_dns_query *)0)->question),
"a decoded DNS question must fit its storage");
static void sp_dns_encode_response_value(
const struct sp_dns_query *query, uint32_t answer_ip, int answer_a,
struct sp_dns_response *out) {
struct sp_dns_response candidate;
size_t length;
uint16_t flags;
if (answer_a)
assert(answer_ip != 0);
length = 12U + query->question_len + (answer_a ? 16U : 0U);
memset(&candidate, 0, sizeof(candidate));
flags = (uint16_t)(0x8000U | 0x0080U |
(query->request_flags & 0x0100U));
dns_put_u16(candidate.bytes, query->identifier);
dns_put_u16(candidate.bytes + 2, flags);
dns_put_u16(candidate.bytes + 4, 1);
dns_put_u16(candidate.bytes + 6, answer_a ? 1 : 0);
memcpy(candidate.bytes + 12, query->question, query->question_len);
if (answer_a) {
size_t off = 12U + query->question_len;
candidate.bytes[off++] = 0xc0;
candidate.bytes[off++] = 0x0c;
dns_put_u16(candidate.bytes + off, 1);
off += 2;
dns_put_u16(candidate.bytes + off, 1);
off += 2;
dns_put_u32(candidate.bytes + off, SP_DNS_TTL_SECONDS);
off += 4;
dns_put_u16(candidate.bytes + off, 4);
off += 2;
dns_put_u32(candidate.bytes + off, ntohl(answer_ip));
}
candidate.len = (uint16_t)length;
*out = candidate;
}
SP_TEST_API static enum sp_dns_effect_kind
sp_dns_core_step(struct sp_dns_core *core,
const struct sp_dns_core_input *input,
struct sp_dns_core_output *out) {
struct sp_dns_core_output candidate = {0};
enum sp_dns_effect_kind kind = SP_DNS_EFFECT_DROP;
int answer_a = 0;
uint32_t answer_ip = 0;
/* Only successfully decoded queries cross this internal boundary. */
assert(input->query.name_len != 0);
assert(input->query.name_len <= SP_DNS_NAME_MAX);
assert(input->query.name[input->query.name_len] == '\0');
assert(input->query.question_len >= 4);
assert(input->query.question_len <= SP_DNS_QUESTION_MAX);
assert((input->query.request_flags & 0x8000U) == 0);
if (input->credit == SP_DNS_CREDIT_NO_OUTPUT) {
kind = SP_DNS_EFFECT_RETRY;
goto commit;
}
if (input->credit != SP_DNS_CREDIT_AVAILABLE)
goto commit;
if (input->query.qclass == 1 && input->query.qtype == 1) {
/* Preserve the legacy NODATA response on table exhaustion. */
if (sp_dns_core_get_or_create(
core, input->query.name, input->query.name_len, input->now_ns,
&answer_ip) < 0)
candidate.mapping_exhausted = 1;
else
answer_a = 1;
}
sp_dns_encode_response_value(&input->query, answer_ip, answer_a, &candidate.response);
kind = SP_DNS_EFFECT_RESPONSE;
commit:
*out = candidate;
return kind;
}
static int dns_is_synthetic_ip(uint32_t ip_be) {
return (ntohl(ip_be) & 0xffff0000U) == DNS_SYNTHETIC_BASE;
}
static struct publish_udp_flow *publish_udp_find(uint16_t synthetic_port,
uint16_t child_port);
static void publish_udp_close_flow(struct publish_udp_flow *f);
static void publish_udp_update_rule_events(int rule_index);
static int publish_udp_send_now(const struct publish_rule *rule,
const struct publish_udp_flow *f,
const uint8_t *payload, size_t payload_len) {
struct iovec iov = {.iov_base = (void *)payload, .iov_len = payload_len};
char control[CMSG_SPACE(sizeof(struct in_pktinfo))];
struct msghdr msg;
struct cmsghdr *cmsg;
struct in_pktinfo pktinfo;
unsigned int interrupted = 0;
memset(control, 0, sizeof(control));
memset(&msg, 0, sizeof(msg));
memset(&pktinfo, 0, sizeof(pktinfo));
msg.msg_name = (void *)&f->host_peer;
msg.msg_namelen = sizeof(f->host_peer);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = IPPROTO_IP;
cmsg->cmsg_type = IP_PKTINFO;
cmsg->cmsg_len = CMSG_LEN(sizeof(pktinfo));
pktinfo.ipi_spec_dst.s_addr = f->published_local_ip;
pktinfo.ipi_addr.s_addr = f->published_local_ip;
memcpy(CMSG_DATA(cmsg), &pktinfo, sizeof(pktinfo));
for (;;) {
ssize_t n = sendmsg(rule->listen_fd, &msg, MSG_NOSIGNAL);
sp_resource_count_host_io(1, 1, n);
if (n == (ssize_t)payload_len)
return 0;
if (n >= 0)
return -1;
if (errno == EINTR && interrupted++ < 4)
continue;
if (errno == EINTR)
return 1;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return 1;
if (errno == ENOBUFS || errno == ENOMEM) {
sp_counter_inc(&g_resource_counters.udp_terminal_drops);
return SP_UDP_SEND_DROPPED;
}
return -1;
}
}
static int publish_udp_queue_pending(struct publish_udp_flow *f,
const uint8_t *data, size_t len) {
int rc = sp_udp_pending_store(&f->core, f->generation,
(uint16_t)(f - publish_udp_flows), 1, NULL,
data, len);
if (rc == 0)
publish_udp_update_rule_events(f->rule_index);
return rc == 2 ? 1 : rc;
}
static void handle_dns_proxy(int tunfd, uint32_t src_ip, uint32_t dst_ip,
uint16_t src_port, uint16_t dst_port,
const uint8_t *payload, size_t payload_len) {
struct sp_dns_core_input input;
struct sp_dns_core_output output;
if (sp_dns_decode_query(payload, payload_len, &input.query) < 0) {
DBG("DNS proxy dropped malformed query or response");
return;
}
input.now_ns = sp_ns_now_ns();
input.credit = SP_DNS_CREDIT_AVAILABLE;
if (sp_dns_core_step(&g_dns_core, &input, &output) !=
SP_DNS_EFFECT_RESPONSE) {
DBG("DNS proxy dropped malformed query or response");
return;
}
if (output.mapping_exhausted)
DBG("DNS synthetic mapping table full; returning NODATA");
else if (input.query.qclass == 1 && input.query.qtype == 28)
DBG("DNS proxy returning empty AAAA answer for %s", input.query.name);
else if (input.query.qclass != 1 || input.query.qtype != 1)
DBG("DNS proxy returning empty answer for %s type=%u class=%u",
input.query.name, (unsigned)input.query.qtype,
(unsigned)input.query.qclass);
sp_udp_emit_tun(tunfd, ntohl(dst_ip), ntohl(src_ip), dst_port, src_port,
output.response.bytes, output.response.len, "DNS inject");
}
static int handle_publish_udp_from_child(uint32_t src_ip, uint32_t dst_ip,
uint16_t src_port, uint16_t dst_port,
const uint8_t *payload,
size_t payload_len) {
struct publish_udp_flow *f;
struct publish_rule *rule;
if (dst_ip != PUBLISH_SYNTHETIC_IP)
return 0;
if (src_ip != SANDBOX_IP)
return 1;
f = publish_udp_find(dst_port, src_port);
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 != src_port)
return 1;
{
int rc = publish_udp_send_now(rule, f, payload, payload_len);
if (rc < 0) {
publish_udp_close_flow(f);
return 1;
}
if (rc == SP_UDP_SEND_RETAIN &&
publish_udp_queue_pending(f, payload, payload_len) < 0) {
publish_udp_close_flow(f);
return 1;
}
if (rc == 0) {
uint64_t now_ns = sp_ns_now_ns();
(void)sp_udp_runtime_step(&f->core, SP_UDP_PROGRESS, now_ns, 0);
}
return 1;
}
}
static void handle_udp(int tunfd, const uint8_t *pkt, size_t len,
const struct sp_packet_value *parsed) {
uint32_t src_ip;
uint32_t dst_ip;
uint16_t sport;
uint16_t dport;
const uint8_t *payload;
size_t plen;
if (sp_udp_dispatch_values(pkt, len, parsed, &src_ip, &dst_ip, &sport,
&dport, &payload, &plen) < 0)
return;
if (handle_publish_udp_from_child(src_ip, dst_ip, sport, dport, payload,
plen))
return;
if (g_net_policy.egress == EGRESS_SOCKS &&
g_net_policy.socks_remote_dns &&
dst_ip == DNS_PROXY_IP && dport == 53) {
handle_dns_proxy(tunfd, src_ip, dst_ip, sport, dport, payload, plen);
return;
}
/* Check for host gateway access (10.0.1.x -> 127.0.0.x) */
uint32_t target_ip = dst_ip;
int is_gateway = is_gateway_ip(target_ip);
if (g_net_policy.egress == 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 (!sp_policy_gateway_allowed(&g_net_policy, 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(dst_ip);
DBG("[parent] UDP gateway: 10.0.1.%d:%d -> 127.0.0.%d:%d",
gateway_last_octet(dst_ip), dport, gateway_last_octet(dst_ip),
dport);
} else if (g_net_policy.egress == EGRESS_DIRECT &&
!sp_policy_direct_egress_allowed(&g_net_policy, target_ip, dport,
IPPROTO_UDP)) {
log_direct_egress_block(target_ip, dport, IPPROTO_UDP);
return;
}
if (g_net_policy.egress == EGRESS_SOCKS && !is_gateway) {
/* Find or create persistent UDP flow */
struct udp_flow *f = udp_find(src_ip, sport, dst_ip, dport,
SP_UDP_ROLE_SOCKS);
if (!f) {
struct sp_udp_key key;
if (sp_udp_admit_source(src_ip, SP_UDP_ROLE_SOCKS) < 0) {
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
return;
}
if (sp_udp_admit_global() < 0) {
sp_resource_admission_release_source(
src_ip, sp_udp_admission_role(SP_UDP_ROLE_SOCKS));
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
return;
}
f = udp_alloc(1);
if (f == NULL) {
sp_resource_admission_release_source(
src_ip, sp_udp_admission_role(SP_UDP_ROLE_SOCKS));
sp_resource_admission_release_source(0, SP_UDP_ADMISSION_GLOBAL);
return;
}
udp_flow_begin(f, SP_UDP_ROLE_SOCKS);
f->admission_held = 1;
f->global_admission_held = 1;
f->cli_ip = src_ip;
f->cli_port = sport;
f->srv_ip = dst_ip;
f->srv_port = dport;
if (g_net_policy.socks_remote_dns && dns_is_synthetic_ip(f->srv_ip)) {
struct sp_dns_ref mapping;
if (sp_dns_core_acquire_ip(&g_dns_core, f->srv_ip,
sp_ns_now_ns(), &mapping) < 0) {
udp_close_flow(f);
return;
}
f->dns_mapping_ref = mapping;
}
if (udp_socks_setup(f, &socks_proxy) < 0) {
udp_close_flow(f);
return;
}
sp_udp_outbound_key(&key, f->core.role, f->cli_ip, f->cli_port, f->srv_ip,
f->srv_port);
if (sp_udp_table_insert(&key, f->generation,
(uint16_t)(f - udp_flows), f->core.role) < 0) {
udp_close_flow(f);
return;
}
epoll_set_udp(f, EPOLL_CTL_ADD, FD_UDP_CTRL, udp_ctrl_events(f));
}
memcpy(f->last_quote, pkt, sizeof(f->last_quote));
f->has_last_quote = 1;
if (f->udp_relay >= 0) {
int src = udp_send_socks_now(f, payload, plen);
if (src < 0) {
udp_close_flow(f);
return;
}
if (src == SP_UDP_SEND_RETAIN &&
udp_queue_pending(f, payload, plen, pkt) < 0) {
udp_close_flow(f);
return;
}
if (src == SP_UDP_SEND_EMSGSIZE || src == SP_UDP_SEND_REFUSED)
(void)sp_udp_emit_error(tunfd, pkt, f,
(enum sp_udp_send_result)src);
udp_update_events(f);
} else {
int qrc = udp_queue_pending(f, payload, plen, pkt);
if (qrc < 0) {
udp_close_flow(f);
return;
}
}
} else {
struct udp_flow *f = udp_find(src_ip, sport, dst_ip, dport,
SP_UDP_ROLE_DIRECT);
if (!f) {
struct sp_udp_key key;
if (sp_udp_admit_source(src_ip, SP_UDP_ROLE_DIRECT) < 0) {
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
return;
}
if (sp_udp_admit_global() < 0) {
sp_resource_admission_release_source(
src_ip, sp_udp_admission_role(SP_UDP_ROLE_DIRECT));
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
return;
}
f = udp_alloc(0);
if (f == NULL) {
sp_resource_admission_release_source(
src_ip, sp_udp_admission_role(SP_UDP_ROLE_DIRECT));
sp_resource_admission_release_source(0, SP_UDP_ADMISSION_GLOBAL);
return;
}
udp_flow_begin(f, SP_UDP_ROLE_DIRECT);
f->admission_held = 1;
f->global_admission_held = 1;
f->cli_ip = src_ip;
f->cli_port = sport;
f->srv_ip = dst_ip;
f->srv_port = dport;
if (udp_direct_setup(f, target_ip, dport) < 0) {
udp_close_flow(f);
return;
}
sp_udp_outbound_key(&key, f->core.role, f->cli_ip, f->cli_port, f->srv_ip,
f->srv_port);
if (sp_udp_table_insert(&key, f->generation,
(uint16_t)(f - udp_flows), f->core.role) < 0) {
udp_close_flow(f);
return;
}
epoll_set_udp(f, EPOLL_CTL_ADD, FD_UDP_RELAY, EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP);
udp_update_events(f);
}
memcpy(f->last_quote, pkt, sizeof(f->last_quote));
f->has_last_quote = 1;
int src = udp_send_direct_now(f, payload, plen);
if (src < 0) {
udp_close_flow(f);
return;
}
if (src == SP_UDP_SEND_RETAIN &&
udp_queue_pending(f, payload, plen, pkt) < 0) {
udp_close_flow(f);
return;
}
if (src == SP_UDP_SEND_EMSGSIZE || src == SP_UDP_SEND_REFUSED)
(void)sp_udp_emit_error(tunfd, pkt, f,
(enum sp_udp_send_result)src);
udp_update_events(f);
DBG("UDP queued %zu bytes to %s:%d", plen,
is_gateway ? "127.0.0.1" : "remote", dport);
}
}
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 uint16_t checksum_finish(uint32_t sum) {
while (sum >> 16)
sum = (sum & 0xffffU) + (sum >> 16);
return (uint16_t)~sum;
}
/* Bytewise packet codec. Parsed values contain only scalars and
* offsets: no object below retains a pointer into mutable TUN storage. */
enum sp_codec_status {
SP_CODEC_OK = 0,
SP_CODEC_SHORT,
SP_CODEC_VERSION,
SP_CODEC_LENGTH,
SP_CODEC_IHL,
SP_CODEC_FRAGMENT,
SP_CODEC_TTL,
SP_CODEC_CHECKSUM,
SP_CODEC_PROTOCOL,
SP_CODEC_PORT,
SP_CODEC_TCP_OFFSET,
SP_CODEC_TCP_FLAGS,
SP_CODEC_TCP_OPTIONS,
SP_CODEC_MSS,
SP_CODEC_SOURCE,
SP_CODEC_VNET_FLAGS,
SP_CODEC_VNET_TYPE,
SP_CODEC_VNET_FIELDS,
SP_CODEC_GSO_LIMIT,
SP_CODEC_OUTPUT,
};
enum {
SP_VNET_F_NEEDS_CSUM = 1,
SP_VNET_F_DATA_VALID = 2,
SP_VNET_GSO_NONE = 0,
SP_VNET_GSO_TCPV4 = 1,
SP_TCP_FLAG_FIN = 0x01,
SP_TCP_FLAG_SYN = 0x02,
SP_TCP_FLAG_RST = 0x04,
SP_TCP_FLAG_PSH = 0x08,
SP_TCP_FLAG_ACK = 0x10,
SP_TCP_FLAG_URG = 0x20,
SP_TCP_FLAG_ECE = 0x40,
SP_TCP_FLAG_CWR = 0x80,
};
struct sp_vnet_value {
uint8_t flags;
uint8_t gso_type;
uint16_t hdr_len;
uint16_t gso_size;
uint16_t csum_start;
uint16_t csum_offset;
};
struct sp_ipv4_value {
uint8_t ihl;
uint8_t tos;
uint16_t total_len;
uint16_t id;
uint16_t frag_off;
uint8_t ttl;
uint8_t protocol;
uint32_t src;
uint32_t dst;
size_t transport_offset;
size_t transport_len;
};
struct sp_tcp_value {
uint16_t src_port;
uint16_t dst_port;
uint32_t seq;
uint32_t ack;
uint8_t data_offset;
uint8_t flags;
uint16_t window;
uint16_t urgent;
size_t payload_offset;
size_t payload_len;
uint16_t mss;
uint8_t window_scale;
uint8_t has_mss;
uint8_t has_window_scale;
};
struct sp_udp_value {
uint16_t src_port;
uint16_t dst_port;
uint16_t length;
uint16_t checksum;
size_t payload_offset;
size_t payload_len;
};
struct sp_packet_value {
struct sp_vnet_value vnet;
struct sp_ipv4_value ip;
struct sp_tcp_value tcp;
struct sp_udp_value udp;
size_t packet_offset;
};
struct sp_scalar_packet {
size_t len;
uint8_t bytes[1500];
};
/* A control response quotes the original IPv4/UDP headers. The quote is an
* internal, already validated product; the pure builder copies it into the
* canonical ICMP packet and never parses caller-owned bytes. */
struct sp_icmp_control_request {
uint8_t code;
uint16_t mtu;
uint8_t quote[SP_IPV4_HEADER_BYTES + SP_UDP_HEADER_BYTES];
};
_Static_assert(sizeof(struct virtio_net_hdr) == SP_VNET_HDR_BYTES,
"virtio-net header must match the negotiated 10-byte form");
static uint16_t sp_get_be16(const uint8_t *p) {
return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]);
}
static uint32_t sp_get_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];
}
static uint16_t sp_get_le16(const uint8_t *p) {
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static void sp_put_be16(uint8_t *p, uint16_t value) {
p[0] = (uint8_t)(value >> 8);
p[1] = (uint8_t)value;
}
static void sp_put_be32(uint8_t *p, uint32_t value) {
p[0] = (uint8_t)(value >> 24);
p[1] = (uint8_t)(value >> 16);
p[2] = (uint8_t)(value >> 8);
p[3] = (uint8_t)value;
}
static void sp_put_le16(uint8_t *p, uint16_t value) {
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
}
static int sp_slice_ok(size_t len, size_t offset, size_t need) {
return offset <= len && need <= len - offset;
}
static uint32_t sp_checksum_pseudo(uint32_t src, uint32_t dst, uint8_t proto,
uint16_t length) {
uint8_t pseudo[12];
sp_put_be32(pseudo, src);
sp_put_be32(pseudo + 4, dst);
pseudo[8] = 0;
pseudo[9] = proto;
sp_put_be16(pseudo + 10, length);
return checksum_add_bytes(pseudo, sizeof(pseudo));
}
static uint16_t sp_checksum_complete(const uint8_t *bytes, size_t len) {
return checksum_finish(checksum_add_bytes(bytes, len));
}
static uint16_t sp_checksum_transport(uint32_t src, uint32_t dst,
uint8_t proto, const uint8_t *segment,
size_t length) {
uint32_t sum;
if (length > UINT16_MAX)
return 0;
sum = sp_checksum_pseudo(src, dst, proto, (uint16_t)length);
sum += checksum_add_bytes(segment, length);
return checksum_finish(sum);
}
static uint16_t sp_checksum_partial_seed(uint32_t src, uint32_t dst,
uint8_t proto, uint16_t length) {
uint32_t sum = sp_checksum_pseudo(src, dst, proto, length);
while (sum >> 16)
sum = (sum & 0xffffU) + (sum >> 16);
return (uint16_t)sum;
}
static void sp_write_vnet(uint8_t out[SP_VNET_HDR_BYTES],
const struct sp_vnet_value *vnet) {
out[0] = vnet->flags;
out[1] = vnet->gso_type;
sp_put_le16(out + 2, vnet->hdr_len);
sp_put_le16(out + 4, vnet->gso_size);
sp_put_le16(out + 6, vnet->csum_start);
sp_put_le16(out + 8, vnet->csum_offset);
}
static enum sp_codec_status
sp_parse_vnet(const uint8_t *record, size_t len, struct sp_vnet_value *out) {
if (len < SP_VNET_HDR_BYTES)
return SP_CODEC_SHORT;
out->flags = record[0];
out->gso_type = record[1];
out->hdr_len = sp_get_le16(record + 2);
out->gso_size = sp_get_le16(record + 4);
out->csum_start = sp_get_le16(record + 6);
out->csum_offset = sp_get_le16(record + 8);
if ((out->flags & (uint8_t)~(SP_VNET_F_NEEDS_CSUM |
SP_VNET_F_DATA_VALID)) != 0 ||
out->flags == (SP_VNET_F_NEEDS_CSUM | SP_VNET_F_DATA_VALID))
return SP_CODEC_VNET_FLAGS;
if (out->gso_type != SP_VNET_GSO_NONE &&
out->gso_type != SP_VNET_GSO_TCPV4)
return SP_CODEC_VNET_TYPE;
return SP_CODEC_OK;
}
static enum sp_codec_status sp_parse_ipv4(const uint8_t *packet, size_t len,
int scalar,
struct sp_ipv4_value *out) {
uint8_t version;
size_t ihl_bytes;
if (len < SP_IPV4_HEADER_BYTES)
return SP_CODEC_SHORT;
version = packet[0] >> 4;
out->ihl = packet[0] & 0x0fU;
if (version != 4)
return SP_CODEC_VERSION;
if (out->ihl < 5)
return SP_CODEC_IHL;
ihl_bytes = (size_t)out->ihl * 4U;
if (!sp_slice_ok(len, 0, ihl_bytes))
return SP_CODEC_SHORT;
if (out->ihl != 5)
return SP_CODEC_IHL;
out->tos = packet[1];
out->total_len = sp_get_be16(packet + 2);
out->id = sp_get_be16(packet + 4);
out->frag_off = sp_get_be16(packet + 6);
out->ttl = packet[8];
out->protocol = packet[9];
out->src = sp_get_be32(packet + 12);
out->dst = sp_get_be32(packet + 16);
if ((size_t)out->total_len != len || out->total_len < ihl_bytes)
return SP_CODEC_LENGTH;
if (scalar && out->total_len > 1500)
return SP_CODEC_LENGTH;
if (out->ttl == 0)
return SP_CODEC_TTL;
if ((out->frag_off & (IP_MF | IP_OFFMASK)) != 0)
return SP_CODEC_FRAGMENT;
if (sp_checksum_complete(packet, ihl_bytes) != 0)
return SP_CODEC_CHECKSUM;
out->transport_offset = ihl_bytes;
out->transport_len = len - ihl_bytes;
return SP_CODEC_OK;
}
static enum sp_codec_status sp_parse_tcp_options(const uint8_t *tcp,
size_t header_len,
uint8_t flags,
struct sp_tcp_value *out) {
size_t pos = 20;
int after_eol = 0;
while (pos < header_len) {
uint8_t kind = tcp[pos];
if (after_eol) {
if (kind != 0)
return SP_CODEC_TCP_OPTIONS;
pos++;
continue;
}
if (kind == 0) {
after_eol = 1;
pos++;
continue;
}
if (kind == 1) {
pos++;
continue;
}
if (!sp_slice_ok(header_len, pos, 2) || tcp[pos + 1] < 2 ||
!sp_slice_ok(header_len, pos, tcp[pos + 1]))
return SP_CODEC_TCP_OPTIONS;
if (kind == 2) {
if ((flags & SP_TCP_FLAG_SYN) == 0 || tcp[pos + 1] != 4 || out->has_mss)
return SP_CODEC_TCP_OPTIONS;
out->mss = sp_get_be16(tcp + pos + 2);
if (out->mss < SP_TCP_MIN_PEER_MSS)
return SP_CODEC_MSS;
out->has_mss = 1;
} else if (kind == 3) {
if ((flags & SP_TCP_FLAG_SYN) == 0 || tcp[pos + 1] != 3 ||
out->has_window_scale)
return SP_CODEC_TCP_OPTIONS;
out->window_scale = tcp[pos + 2];
out->has_window_scale = 1;
}
pos += tcp[pos + 1];
}
return SP_CODEC_OK;
}
static enum sp_codec_status sp_parse_tcp(const uint8_t *packet, size_t len,
const struct sp_ipv4_value *ip,
struct sp_tcp_value *out) {
const uint8_t *tcp;
size_t header_len;
enum sp_codec_status status;
if (!sp_slice_ok(len, ip->transport_offset, ip->transport_len) ||
ip->transport_len < 20)
return SP_CODEC_SHORT;
tcp = packet + ip->transport_offset;
memset(out, 0, sizeof(*out));
out->src_port = sp_get_be16(tcp);
out->dst_port = sp_get_be16(tcp + 2);
out->seq = sp_get_be32(tcp + 4);
out->ack = sp_get_be32(tcp + 8);
out->data_offset = tcp[12] >> 4;
out->flags = tcp[13];
out->window = sp_get_be16(tcp + 14);
out->urgent = sp_get_be16(tcp + 18);
if (out->src_port == 0 || out->dst_port == 0)
return SP_CODEC_PORT;
if (out->data_offset < 5 || out->data_offset > 15)
return SP_CODEC_TCP_OFFSET;
header_len = (size_t)out->data_offset * 4U;
if (header_len > ip->transport_len)
return SP_CODEC_TCP_OFFSET;
if ((tcp[12] & 0x0fU) != 0 ||
(out->flags & (SP_TCP_FLAG_ECE | SP_TCP_FLAG_CWR)) != 0)
return SP_CODEC_TCP_FLAGS;
if (((out->flags & (SP_TCP_FLAG_SYN | SP_TCP_FLAG_FIN)) ==
(SP_TCP_FLAG_SYN | SP_TCP_FLAG_FIN)) ||
((out->flags & (SP_TCP_FLAG_SYN | SP_TCP_FLAG_RST)) ==
(SP_TCP_FLAG_SYN | SP_TCP_FLAG_RST)) ||
((out->flags & (SP_TCP_FLAG_FIN | SP_TCP_FLAG_RST)) ==
(SP_TCP_FLAG_FIN | SP_TCP_FLAG_RST)))
return SP_CODEC_TCP_FLAGS;
out->payload_offset = ip->transport_offset + header_len;
out->payload_len = ip->transport_len - header_len;
if ((out->flags & SP_TCP_FLAG_SYN) != 0 && out->payload_len != 0)
return SP_CODEC_TCP_FLAGS;
if ((out->flags & SP_TCP_FLAG_RST) != 0 && out->payload_len != 0)
return SP_CODEC_TCP_FLAGS;
status = sp_parse_tcp_options(tcp, header_len, out->flags, out);
if (status != SP_CODEC_OK)
return status;
if (sp_checksum_transport(ip->src, ip->dst, IPPROTO_TCP, tcp,
ip->transport_len) != 0)
return SP_CODEC_CHECKSUM;
return SP_CODEC_OK;
}
static enum sp_codec_status sp_parse_udp(const uint8_t *packet, size_t len,
const struct sp_ipv4_value *ip,
struct sp_udp_value *out) {
const uint8_t *udp;
if (!sp_slice_ok(len, ip->transport_offset, ip->transport_len) ||
ip->transport_len < SP_UDP_HEADER_BYTES)
return SP_CODEC_SHORT;
udp = packet + ip->transport_offset;
out->src_port = sp_get_be16(udp);
out->dst_port = sp_get_be16(udp + 2);
out->length = sp_get_be16(udp + 4);
out->checksum = sp_get_be16(udp + 6);
if (out->src_port == 0 || out->dst_port == 0)
return SP_CODEC_PORT;
if (out->length != ip->transport_len || out->length < SP_UDP_HEADER_BYTES)
return SP_CODEC_LENGTH;
out->payload_offset = ip->transport_offset + SP_UDP_HEADER_BYTES;
out->payload_len = out->length - SP_UDP_HEADER_BYTES;
if (out->payload_len > SP_UDP_PAYLOAD_MAX)
return SP_CODEC_LENGTH;
if (out->checksum != 0 &&
sp_checksum_transport(ip->src, ip->dst, IPPROTO_UDP, udp,
out->length) != 0)
return SP_CODEC_CHECKSUM;
return SP_CODEC_OK;
}
static enum sp_codec_status
sp_parse_ipv4_packet(const uint8_t *packet, size_t len, int scalar,
int require_child_source, struct sp_packet_value *out) {
enum sp_codec_status status;
memset(out, 0, sizeof(*out));
status = sp_parse_ipv4(packet, len, scalar, &out->ip);
if (status != SP_CODEC_OK)
return status;
if (require_child_source && out->ip.src != ntohl(SANDBOX_IP))
return SP_CODEC_SOURCE;
if (out->ip.protocol == IPPROTO_TCP)
return sp_parse_tcp(packet, len, &out->ip, &out->tcp);
if (out->ip.protocol == IPPROTO_UDP)
return sp_parse_udp(packet, len, &out->ip, &out->udp);
return SP_CODEC_PROTOCOL;
}
static enum sp_codec_status
sp_parse_tun_frame_source(const uint8_t *record, size_t len,
int require_child_source,
struct sp_packet_value *out) {
enum sp_codec_status status;
const uint8_t *packet;
size_t packet_len;
size_t header_end;
size_t segments;
uint16_t checksum;
memset(out, 0, sizeof(*out));
status = sp_parse_vnet(record, len, &out->vnet);
if (status != SP_CODEC_OK)
return status;
packet = record + SP_VNET_HDR_BYTES;
packet_len = len - SP_VNET_HDR_BYTES;
out->packet_offset = SP_VNET_HDR_BYTES;
status = sp_parse_ipv4(packet, packet_len,
out->vnet.gso_type == SP_VNET_GSO_NONE, &out->ip);
if (status != SP_CODEC_OK)
return status;
if (require_child_source && out->ip.src != ntohl(SANDBOX_IP))
return SP_CODEC_SOURCE;
if (out->ip.protocol == IPPROTO_TCP)
status = sp_parse_tcp(packet, packet_len, &out->ip, &out->tcp);
else if (out->ip.protocol == IPPROTO_UDP)
status = sp_parse_udp(packet, packet_len, &out->ip, &out->udp);
else
status = SP_CODEC_PROTOCOL;
if (status != SP_CODEC_OK && status != SP_CODEC_CHECKSUM)
return status;
header_end = out->ip.transport_offset;
if (out->ip.protocol == IPPROTO_TCP)
header_end = out->tcp.payload_offset;
else if (out->ip.protocol == IPPROTO_UDP)
header_end = out->udp.payload_offset;
if (out->vnet.hdr_len > packet_len)
return SP_CODEC_VNET_FIELDS;
if (out->vnet.gso_type == SP_VNET_GSO_NONE) {
if (out->vnet.gso_size != 0 || out->vnet.hdr_len != 0)
return SP_CODEC_VNET_FIELDS;
if (out->vnet.flags == 0 || out->vnet.flags == SP_VNET_F_DATA_VALID) {
if (out->vnet.csum_start != 0 || out->vnet.csum_offset != 0)
return SP_CODEC_VNET_FIELDS;
return status;
}
if (out->vnet.flags != SP_VNET_F_NEEDS_CSUM ||
(out->ip.protocol != IPPROTO_TCP &&
(out->ip.protocol != IPPROTO_UDP || out->udp.checksum == 0)))
return SP_CODEC_VNET_FLAGS;
} else {
if (out->vnet.flags != SP_VNET_F_NEEDS_CSUM || out->vnet.gso_size == 0)
return SP_CODEC_VNET_FIELDS;
if (out->vnet.gso_type == SP_VNET_GSO_TCPV4 &&
out->ip.protocol != IPPROTO_TCP)
return SP_CODEC_PROTOCOL;
if (out->vnet.hdr_len != header_end &&
out->vnet.hdr_len != header_end + out->vnet.gso_size)
return SP_CODEC_VNET_FIELDS;
segments = (packet_len - header_end + out->vnet.gso_size - 1U) /
out->vnet.gso_size;
if (segments == 0 || segments > SP_TCP_GSO_POLICY_SEGS)
return SP_CODEC_GSO_LIMIT;
if (out->vnet.gso_type == SP_VNET_GSO_TCPV4 &&
((out->tcp.flags & (SP_TCP_FLAG_SYN | SP_TCP_FLAG_RST |
SP_TCP_FLAG_URG | SP_TCP_FLAG_ECE |
SP_TCP_FLAG_CWR)) != 0 ||
out->vnet.gso_size > 1500U - header_end))
return SP_CODEC_TCP_FLAGS;
}
if (out->vnet.csum_start != out->ip.transport_offset)
return SP_CODEC_VNET_FIELDS;
if (out->ip.protocol == IPPROTO_TCP) {
if (out->vnet.csum_offset != 16)
return SP_CODEC_VNET_FIELDS;
checksum = sp_get_be16(packet + out->ip.transport_offset + 16);
} else if (out->ip.protocol == IPPROTO_UDP) {
if (out->vnet.csum_offset != 6)
return SP_CODEC_VNET_FIELDS;
checksum = sp_get_be16(packet + out->ip.transport_offset + 6);
} else {
return SP_CODEC_VNET_FIELDS;
}
if (checksum != sp_checksum_partial_seed(
out->ip.src, out->ip.dst, out->ip.protocol,
(uint16_t)out->ip.transport_len))
return SP_CODEC_CHECKSUM;
return SP_CODEC_OK;
}
static enum sp_codec_status
sp_parse_tun_frame(const uint8_t *record, size_t len,
struct sp_packet_value *out) {
return sp_parse_tun_frame_source(record, len, 1, out);
}
static int sp_vnet_output_envelope_valid(const uint8_t *record, size_t len) {
struct sp_vnet_value vnet;
struct sp_ipv4_value ip;
struct sp_packet_value parsed;
const uint8_t *packet;
size_t packet_len;
if (sp_parse_vnet(record, len, &vnet) != SP_CODEC_OK)
return 0;
packet = record + SP_VNET_HDR_BYTES;
packet_len = len - SP_VNET_HDR_BYTES;
if (sp_parse_ipv4(packet, packet_len, vnet.gso_type == SP_VNET_GSO_NONE,
&ip) != SP_CODEC_OK)
return 0;
if (vnet.gso_type == SP_VNET_GSO_NONE)
return vnet.flags == 0 && vnet.hdr_len == 0 && vnet.gso_size == 0 &&
vnet.csum_start == 0 && vnet.csum_offset == 0;
if (vnet.flags != SP_VNET_F_NEEDS_CSUM || vnet.gso_size == 0 ||
vnet.csum_start != ip.transport_offset)
return 0;
if (vnet.gso_type == SP_VNET_GSO_TCPV4) {
if (ip.protocol != IPPROTO_TCP ||
vnet.gso_size > SP_TCP_MTU1500_MAX_MSS ||
vnet.csum_offset != 16)
return 0;
return sp_parse_tun_frame_source(record, len, 0, &parsed) ==
SP_CODEC_OK;
}
return 0;
}
static void sp_rechecksum_ipv4(uint8_t *packet) {
sp_put_be16(packet + 10, 0);
sp_put_be16(packet + 10, sp_checksum_complete(packet, 20));
}
static void sp_rechecksum_tcp(uint8_t *packet, size_t packet_len) {
size_t tcp_len = packet_len - 20;
uint16_t checksum;
sp_put_be16(packet + 20 + 16, 0);
checksum = sp_checksum_transport(sp_get_be32(packet + 12),
sp_get_be32(packet + 16), IPPROTO_TCP,
packet + 20, tcp_len);
sp_put_be16(packet + 20 + 16, checksum);
}
static void sp_rechecksum_udp(uint8_t *packet, size_t packet_len) {
size_t udp_len = packet_len - SP_IPV4_HEADER_BYTES;
uint16_t checksum;
sp_put_be16(packet + SP_IPV4_HEADER_BYTES + 6, 0);
checksum = sp_checksum_transport(sp_get_be32(packet + 12),
sp_get_be32(packet + 16), IPPROTO_UDP,
packet + SP_IPV4_HEADER_BYTES, udp_len);
sp_put_be16(packet + SP_IPV4_HEADER_BYTES + 6,
checksum == 0 ? UINT16_MAX : checksum);
}
static int sp_udp_dispatch_values(const uint8_t *packet, size_t packet_len,
const struct sp_packet_value *parsed,
uint32_t *src_ip, uint32_t *dst_ip,
uint16_t *src_port, uint16_t *dst_port,
const uint8_t **payload,
size_t *payload_len) {
if (packet == NULL || parsed == NULL || src_ip == NULL || dst_ip == NULL ||
src_port == NULL || dst_port == NULL || payload == NULL ||
payload_len == NULL || parsed->ip.protocol != IPPROTO_UDP ||
parsed->ip.total_len != packet_len ||
parsed->ip.transport_offset != SP_IPV4_HEADER_BYTES ||
parsed->udp.length < SP_UDP_HEADER_BYTES ||
parsed->udp.length != parsed->ip.transport_len ||
parsed->udp.payload_offset !=
parsed->ip.transport_offset + SP_UDP_HEADER_BYTES ||
parsed->udp.payload_len !=
(size_t)parsed->udp.length - SP_UDP_HEADER_BYTES ||
!sp_slice_ok(packet_len, parsed->udp.payload_offset,
parsed->udp.payload_len))
return -1;
*src_ip = htonl(parsed->ip.src);
*dst_ip = htonl(parsed->ip.dst);
*src_port = parsed->udp.src_port;
*dst_port = parsed->udp.dst_port;
*payload = packet + parsed->udp.payload_offset;
*payload_len = parsed->udp.payload_len;
return 0;
}
SP_TEST_API static enum sp_codec_status
sp_build_udp_packet_value(uint32_t src_ip, uint32_t dst_ip, uint16_t src_port,
uint16_t dst_port, const uint8_t *payload,
size_t payload_len, struct sp_scalar_packet *out) {
size_t total_len = SP_IPV4_HEADER_BYTES + SP_UDP_HEADER_BYTES + payload_len;
if (out == NULL || src_port == 0 || dst_port == 0 ||
(payload_len != 0 && payload == NULL))
return SP_CODEC_OUTPUT;
if (payload_len > SP_UDP_PAYLOAD_MAX || total_len > sizeof(out->bytes))
return SP_CODEC_LENGTH;
memset(out, 0, sizeof(*out));
out->len = total_len;
out->bytes[0] = 0x45;
sp_put_be16(out->bytes + 2, (uint16_t)total_len);
out->bytes[8] = 64;
out->bytes[9] = IPPROTO_UDP;
sp_put_be32(out->bytes + 12, src_ip);
sp_put_be32(out->bytes + 16, dst_ip);
sp_put_be16(out->bytes + SP_IPV4_HEADER_BYTES, src_port);
sp_put_be16(out->bytes + SP_IPV4_HEADER_BYTES + 2, dst_port);
sp_put_be16(out->bytes + SP_IPV4_HEADER_BYTES + 4,
(uint16_t)(SP_UDP_HEADER_BYTES + payload_len));
if (payload_len != 0)
memcpy(out->bytes + SP_IPV4_HEADER_BYTES + SP_UDP_HEADER_BYTES, payload,
payload_len);
sp_rechecksum_ipv4(out->bytes);
return SP_CODEC_OK;
}
static void sp_udp_emit_tun(int tunfd, uint32_t src_ip, uint32_t dst_ip,
uint16_t src_port, uint16_t dst_port,
const uint8_t *payload, size_t payload_len,
const char *context) {
struct sp_scalar_packet packet;
if (sp_build_udp_packet_value(src_ip, dst_ip, src_port, dst_port, payload,
payload_len, &packet) != SP_CODEC_OK)
return;
IGNORE_RESULT(
tun_write_packet(tunfd, packet.bytes, packet.len, context));
}
static int sp_normalize_vnet_scalar_checksum(
uint8_t *packet, const struct sp_packet_value *parsed) {
if (parsed->vnet.gso_type != SP_VNET_GSO_NONE)
return -1;
if (parsed->vnet.flags != SP_VNET_F_NEEDS_CSUM)
return 0;
if (parsed->ip.protocol == IPPROTO_TCP) {
sp_rechecksum_tcp(packet, parsed->ip.total_len);
return 0;
}
if (parsed->ip.protocol == IPPROTO_UDP) {
sp_rechecksum_udp(packet, parsed->ip.total_len);
return 0;
}
return -1;
}
static enum sp_codec_status
sp_gso_expand_oracle_source(const uint8_t *record, size_t record_len,
struct sp_scalar_packet *out, size_t out_cap,
size_t *out_count, int require_child_source) {
struct sp_packet_value parsed;
enum sp_codec_status status;
const uint8_t *packet;
size_t header_len;
size_t payload_len;
size_t count;
size_t consumed = 0;
*out_count = 0;
status = sp_parse_tun_frame_source(record, record_len,
require_child_source, &parsed);
if (status != SP_CODEC_OK)
return status;
if (parsed.vnet.gso_type != SP_VNET_GSO_TCPV4)
return SP_CODEC_VNET_TYPE;
packet = record + SP_VNET_HDR_BYTES;
header_len = parsed.tcp.payload_offset;
payload_len = parsed.ip.total_len - header_len;
count = (payload_len + parsed.vnet.gso_size - 1U) / parsed.vnet.gso_size;
if (count == 0 || count > out_cap || count > SP_TCP_GSO_POLICY_SEGS)
return SP_CODEC_OUTPUT;
for (size_t i = 0; i < count; ++i) {
size_t part = payload_len - consumed;
uint8_t flags = parsed.tcp.flags;
if (part > parsed.vnet.gso_size)
part = parsed.vnet.gso_size;
out[i].len = header_len + part;
memcpy(out[i].bytes, packet, header_len);
memcpy(out[i].bytes + header_len, packet + header_len + consumed, part);
sp_put_be16(out[i].bytes + 2, (uint16_t)out[i].len);
sp_put_be16(out[i].bytes + 4, (uint16_t)(parsed.ip.id + i));
sp_put_be32(out[i].bytes + 20 + 4,
parsed.tcp.seq + (uint32_t)consumed);
if (i + 1U != count)
flags &= (uint8_t)~(SP_TCP_FLAG_FIN | SP_TCP_FLAG_PSH);
out[i].bytes[20 + 13] = flags;
sp_rechecksum_ipv4(out[i].bytes);
sp_rechecksum_tcp(out[i].bytes, out[i].len);
consumed += part;
}
if (consumed != payload_len)
return SP_CODEC_OUTPUT;
*out_count = count;
return SP_CODEC_OK;
}
SP_ORACLE_API static enum sp_codec_status
sp_gso_expand_oracle(const uint8_t *record, size_t record_len,
struct sp_scalar_packet *out, size_t out_cap,
size_t *out_count) {
return sp_gso_expand_oracle_source(record, record_len, out, out_cap,
out_count, 1);
}
static size_t sp_vnet_build_tcp_record(uint8_t *record, size_t capacity,
uint32_t src, uint32_t dst,
uint16_t src_port, uint16_t dst_port,
uint32_t seq, uint32_t ack, uint8_t flags,
const uint8_t *payload,
size_t payload_len, uint16_t gso_size) {
size_t packet_len = 40 + payload_len;
size_t record_len = SP_VNET_HDR_BYTES + packet_len;
uint8_t *packet;
if (record_len > capacity || packet_len > UINT16_MAX ||
(gso_size != 0 && payload_len <= gso_size))
return 0;
memset(record, 0, record_len);
packet = record + SP_VNET_HDR_BYTES;
packet[0] = 0x45;
sp_put_be16(packet + 2, (uint16_t)packet_len);
sp_put_be16(packet + 4, 1);
packet[8] = 64;
packet[9] = IPPROTO_TCP;
sp_put_be32(packet + 12, src);
sp_put_be32(packet + 16, dst);
sp_put_be16(packet + 20, src_port);
sp_put_be16(packet + 22, dst_port);
sp_put_be32(packet + 24, seq);
sp_put_be32(packet + 28, ack);
packet[32] = 0x50;
packet[33] = flags;
sp_put_be16(packet + 34, UINT16_MAX);
if (payload_len != 0)
memcpy(packet + 40, payload, payload_len);
sp_rechecksum_ipv4(packet);
if (gso_size == 0) {
sp_rechecksum_tcp(packet, packet_len);
} else {
struct sp_vnet_value vnet = {
.flags = SP_VNET_F_NEEDS_CSUM,
.gso_type = SP_VNET_GSO_TCPV4,
.hdr_len = 40,
.gso_size = gso_size,
.csum_start = 20,
.csum_offset = 16,
};
sp_write_vnet(record, &vnet);
sp_put_be16(packet + 36,
sp_checksum_partial_seed(src, dst, IPPROTO_TCP,
(uint16_t)(20 + payload_len)));
}
return record_len;
}
/* A scalar emit is a bounded view over one immutable effect and payload. */
struct sp_tcp_emit_cursor {
size_t payload_offset;
size_t segment_index;
size_t segment_count;
};
static int sp_tcp_emit_cursor_init(const struct sp_fx_tcp_emit_batch *effect,
size_t payload_len,
struct sp_tcp_emit_cursor *cursor) {
if (effect == NULL || cursor == NULL || effect->mss == 0 ||
effect->end.value < effect->start.value ||
payload_len != effect->end.value - effect->start.value ||
payload_len == 0 ||
payload_len > SIZE_MAX - (size_t)(effect->mss - 1U))
return -1;
cursor->payload_offset = 0;
cursor->segment_index = 0;
cursor->segment_count =
(payload_len + (size_t)effect->mss - 1U) / (size_t)effect->mss;
if (cursor->segment_count == 0 ||
cursor->segment_count > SP_TCP_CORE_MAX_SEGMENTS)
return -1;
return 0;
}
/* Format one scalar record; the caller commits the cursor only after the sink
* accepts the returned record. */
SP_TEST_API static size_t sp_tcp_emit_format_scalar(
const struct sp_fx_tcp_emit_batch *effect, uint32_t src, uint32_t dst,
uint16_t src_port, uint16_t dst_port, const uint8_t *payload,
size_t payload_len, struct sp_tcp_emit_cursor *cursor, uint8_t *record,
size_t capacity, size_t *segment_payload_len) {
size_t part;
if (effect == NULL || cursor == NULL || record == NULL ||
segment_payload_len == NULL ||
(payload == NULL && payload_len != 0) ||
cursor->segment_count == 0 ||
cursor->segment_count > SP_TCP_CORE_MAX_SEGMENTS ||
cursor->segment_index >= cursor->segment_count ||
cursor->payload_offset >= payload_len ||
effect->mss == 0 || effect->end.value < effect->start.value ||
payload_len != effect->end.value - effect->start.value)
return 0;
part = payload_len - cursor->payload_offset;
if (part > (size_t)effect->mss)
part = (size_t)effect->mss;
if (part == 0)
return 0;
uint8_t flags = (uint8_t)effect->flags;
if (cursor->segment_index + 1U != cursor->segment_count)
flags &= (uint8_t)~(SP_TCP_CORE_FIN | SP_TCP_CORE_PSH);
size_t record_len = sp_vnet_build_tcp_record(
record, capacity, src, dst, src_port, dst_port,
(uint32_t)(effect->seq.value + cursor->payload_offset),
(uint32_t)effect->ack.value, flags, payload + cursor->payload_offset,
part, 0);
if (record_len == 0)
return 0;
sp_put_be16(record + SP_VNET_HDR_BYTES + 4,
(uint16_t)(1U + cursor->segment_index));
sp_rechecksum_ipv4(record + SP_VNET_HDR_BYTES);
cursor->payload_offset += part;
cursor->segment_index++;
*segment_payload_len = part;
return record_len;
}
enum sp_egress_io_status {
SP_EGRESS_IO_PROGRESS = 0,
SP_EGRESS_IO_BLOCKED,
SP_EGRESS_IO_EOF,
SP_EGRESS_IO_FATAL,
};
struct sp_egress_peek_result {
enum sp_egress_io_status status;
size_t observed;
};
struct sp_egress_consume_result {
enum sp_egress_io_status status;
size_t consumed;
};
enum {
SP_EGRESS_PEEK_OFF_UNKNOWN = 0,
SP_EGRESS_PEEK_OFF_SUPPORTED,
SP_EGRESS_PEEK_OFF_UNAVAILABLE,
};
/*
* Plain TCP SO_PEEK_OFF support is a kernel capability, not a per-flow
* property. Linux 6.1 rejects it with EOPNOTSUPP, so remember that first
* result instead of repeating the rejected setsockopt for every observation
* and materialization. Forced fallback paths remain independent of the cache.
*/
static uint8_t g_egress_peek_off_capability;
#ifdef SOCKPUPPET_EGRESS_SYSCALL_HOOKS
typedef int (*sp_egress_setsockopt_fn)(int, int, int, const void *, socklen_t);
typedef ssize_t (*sp_egress_recvmsg_fn)(int, struct msghdr *, int);
static sp_egress_setsockopt_fn g_egress_setsockopt_fn;
static sp_egress_recvmsg_fn g_egress_recvmsg_fn;
static int sp_egress_setsockopt(int fd, int level, int option, const void *value,
socklen_t length) {
if (g_egress_setsockopt_fn != NULL)
return g_egress_setsockopt_fn(fd, level, option, value, length);
return setsockopt(fd, level, option, value, length);
}
static ssize_t sp_egress_recvmsg(int fd, struct msghdr *message, int flags) {
if (g_egress_recvmsg_fn != NULL)
return g_egress_recvmsg_fn(fd, message, flags);
return recvmsg(fd, message, flags);
}
#define SP_EGRESS_SETSOCKOPT(fd, level, option, value, length) \
sp_egress_setsockopt((fd), (level), (option), (value), (length))
#define SP_EGRESS_RECVMSG(fd, message, flags) \
sp_egress_recvmsg((fd), (message), (flags))
#else
#define SP_EGRESS_SETSOCKOPT(fd, level, option, value, length) \
setsockopt((fd), (level), (option), (value), (length))
#define SP_EGRESS_RECVMSG(fd, message, flags) recvmsg((fd), (message), (flags))
#endif
SP_TEST_API static struct sp_egress_peek_result
sp_egress_peek_backend(int fd, uint64_t stream_consumed, uint64_t stream_tail,
int read_half_closed, int force_fallback,
uint8_t *discard, size_t discard_capacity, uint8_t *target,
size_t target_capacity) {
struct sp_egress_peek_result result = {.status = SP_EGRESS_IO_FATAL};
uint64_t relative_u64;
size_t relative;
if (stream_tail < stream_consumed || target == NULL ||
target_capacity == 0) {
errno = EINVAL;
return result;
}
relative_u64 = stream_tail - stream_consumed;
if (relative_u64 > SIZE_MAX || relative_u64 > INT_MAX) {
errno = EOVERFLOW;
return result;
}
relative = (size_t)relative_u64;
#ifdef SO_PEEK_OFF
if (!force_fallback &&
g_egress_peek_off_capability != SP_EGRESS_PEEK_OFF_UNAVAILABLE) {
int offset = (int)relative;
if (SP_EGRESS_SETSOCKOPT(fd, SOL_SOCKET, SO_PEEK_OFF, &offset,
sizeof(offset)) == 0) {
g_egress_peek_off_capability = SP_EGRESS_PEEK_OFF_SUPPORTED;
ssize_t count = recv(fd, target, target_capacity,
MSG_PEEK | MSG_DONTWAIT);
int saved = errno;
int reset = -1;
if (SP_EGRESS_SETSOCKOPT(fd, SOL_SOCKET, SO_PEEK_OFF, &reset,
sizeof(reset)) < 0)
return result;
if (count > 0) {
result.status = SP_EGRESS_IO_PROGRESS;
result.observed = (size_t)count;
} else if (count == 0) {
result.status = SP_EGRESS_IO_EOF;
} else if (saved == EAGAIN || saved == EWOULDBLOCK) {
result.status = SP_EGRESS_IO_BLOCKED;
} else {
errno = saved;
}
return result;
}
if (errno != ENOPROTOOPT && errno != EOPNOTSUPP && errno != EINVAL)
return result;
g_egress_peek_off_capability = SP_EGRESS_PEEK_OFF_UNAVAILABLE;
DBG("A4 SO_PEEK_OFF unavailable; caching bounded fallback");
}
#else
(void)force_fallback;
#endif
if (relative > discard_capacity || (relative != 0 && discard == NULL)) {
errno = ENOBUFS;
return result;
}
struct iovec iov[2];
size_t iov_count = 0;
if (relative != 0) {
iov[iov_count].iov_base = discard;
iov[iov_count].iov_len = relative;
iov_count++;
}
iov[iov_count].iov_base = target;
iov[iov_count].iov_len = target_capacity;
iov_count++;
struct msghdr message = {.msg_iov = iov, .msg_iovlen = iov_count};
ssize_t count = SP_EGRESS_RECVMSG(fd, &message, MSG_PEEK | MSG_DONTWAIT);
if (count < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
result.status = SP_EGRESS_IO_BLOCKED;
return result;
}
if ((uint64_t)count < relative_u64) {
errno = EPROTO;
return result;
}
if ((uint64_t)count > relative_u64) {
result.status = SP_EGRESS_IO_PROGRESS;
result.observed = (size_t)((uint64_t)count - relative_u64);
} else if (relative == 0 || read_half_closed) {
result.status = SP_EGRESS_IO_EOF;
} else {
result.status = SP_EGRESS_IO_BLOCKED;
}
return result;
}
SP_TEST_API static struct sp_egress_consume_result
sp_egress_consume_backend(int fd, size_t authorized, int force_scratch,
uint8_t *scratch, size_t scratch_capacity) {
struct sp_egress_consume_result result = {.status = SP_EGRESS_IO_FATAL};
ssize_t count;
if (authorized == 0) {
errno = EINVAL;
return result;
}
if (!force_scratch) {
count = recv(fd, NULL, authorized, MSG_TRUNC | MSG_DONTWAIT);
if (count >= 0) {
if (count == 0)
result.status = SP_EGRESS_IO_EOF;
else {
result.status = SP_EGRESS_IO_PROGRESS;
result.consumed = (size_t)count;
}
return result;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
result.status = SP_EGRESS_IO_BLOCKED;
return result;
}
if (errno != EFAULT && errno != EINVAL && errno != EOPNOTSUPP)
return result;
}
if (scratch == NULL || scratch_capacity == 0) {
errno = ENOBUFS;
return result;
}
count = recv(fd, scratch,
authorized < scratch_capacity ? authorized : scratch_capacity,
MSG_DONTWAIT);
if (count > 0) {
result.status = SP_EGRESS_IO_PROGRESS;
result.consumed = (size_t)count;
} else if (count == 0) {
result.status = SP_EGRESS_IO_EOF;
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
result.status = SP_EGRESS_IO_BLOCKED;
}
return result;
}
typedef ssize_t (*sp_tcp_write_fn)(void *context, const uint8_t *buf,
size_t length);
/* Kept as a named adapter type for the existing typed egress entry point and
* fixture callers; both directions use the same callback ABI. */
typedef sp_tcp_write_fn sp_egress_write_fn;
/* Classify one nonblocking stream write. The caller chooses whether a short
* positive prefix is valid: backend stream sends accept it, while a TUN frame
* must be all-or-nothing. No adapter owns or retains the caller's bytes. */
static enum sp_egress_io_status
sp_tcp_write_slice_with(sp_tcp_write_fn write_fn, void *context,
const uint8_t *buf, size_t length, int exact,
size_t *accepted) {
if (accepted == NULL) {
errno = EINVAL;
return SP_EGRESS_IO_FATAL;
}
*accepted = 0;
if (write_fn == NULL || buf == NULL || length == 0) {
errno = EINVAL;
return SP_EGRESS_IO_FATAL;
}
for (unsigned interrupts = 0; interrupts < 8; ++interrupts) {
ssize_t count = write_fn(context, buf, length);
if (count > 0) {
if ((uint64_t)count > length ||
(exact && (size_t)count != length)) {
errno = EIO;
return SP_EGRESS_IO_FATAL;
}
*accepted = (size_t)count;
return SP_EGRESS_IO_PROGRESS;
}
if (count == 0) {
errno = EIO;
return SP_EGRESS_IO_FATAL;
}
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return SP_EGRESS_IO_BLOCKED;
return SP_EGRESS_IO_FATAL;
}
errno = EINTR;
return SP_EGRESS_IO_BLOCKED;
}
static enum sp_egress_io_status
sp_egress_write_tun_record_with(sp_egress_write_fn write_fn, void *context,
const uint8_t *record, size_t length) {
size_t accepted;
if (!sp_vnet_output_envelope_valid(record, length)) {
errno = EPROTO;
return SP_EGRESS_IO_FATAL;
}
return sp_tcp_write_slice_with(write_fn, context, record, length, 1,
&accepted);
}
SP_TEST_API static size_t sp_egress_build_tcp_emit_record(
uint8_t *record, size_t capacity, uint32_t src, uint32_t dst,
uint16_t src_port, uint16_t dst_port,
const struct sp_fx_tcp_emit_batch *effect,
const uint8_t *payload, size_t payload_len, size_t *logical_segments) {
struct sp_tcp_emit_cursor cursor;
uint16_t gso_size;
size_t record_len;
if (logical_segments == NULL ||
(payload == NULL && payload_len != 0) ||
sp_tcp_emit_cursor_init(effect, payload_len, &cursor) < 0)
return 0;
*logical_segments = cursor.segment_count;
gso_size = *logical_segments > 1 ? effect->mss : 0;
record_len = sp_vnet_build_tcp_record(
record, capacity, src, dst, src_port, dst_port,
(uint32_t)effect->seq.value, (uint32_t)effect->ack.value,
(uint8_t)effect->flags, payload, payload_len, gso_size);
return record_len;
}
struct sp_egress_executor_flow {
uint64_t flow;
uint64_t generation;
uint64_t stream_consumed;
uint64_t stream_tail;
uint64_t next_control_seq;
uint8_t tcp_backend;
uint8_t eof_reported;
};
static inline void sp_tcp_event_context_init(
struct sp_typed_event_context *context, uint64_t flow,
uint64_t generation, uint64_t now_ns) {
*context = (struct sp_typed_event_context){
.key = sp_flow_key_make((sp_flow_id){.value = flow},
(sp_generation_id){.value = generation}),
.now = {.value = now_ns},
};
}
static uint64_t sp_egress_min_u64(uint64_t left, uint64_t right) {
return left < right ? left : right;
}
static int sp_egress_arm_backend_lowat(int fd,
const struct sp_egress_executor_flow *flow) {
uint64_t relative;
int lowat;
if (flow->stream_tail < flow->stream_consumed)
return -1;
if (!flow->tcp_backend)
return 0;
relative = flow->stream_tail - flow->stream_consumed;
if (relative >= INT_MAX)
lowat = INT_MAX;
else
lowat = (int)relative + 1;
return setsockopt(fd, SOL_SOCKET, SO_RCVLOWAT, &lowat, sizeof(lowat));
}
enum sp_egress_observation_kind {
SP_EGRESS_OBSERVATION_NONE = 0,
SP_EGRESS_OBSERVATION_CREDIT = 1,
SP_EGRESS_OBSERVATION_EOF = 2,
};
SP_TEST_API static int sp_egress_observe_backend_typed(
struct sp_egress_executor_flow *flow, int backend_fd, uint64_t now_ns,
int read_half_closed, int force_fallback, uint64_t global_flight_credit,
uint64_t effect_byte_credit, uint8_t *discard, size_t discard_capacity,
uint8_t *advisory, size_t advisory_capacity, struct sp_ev_credit *credit,
struct sp_ev_stream_eof *eof) {
uint64_t represented;
uint64_t per_flow_credit;
uint64_t cap;
if (flow == NULL || credit == NULL || eof == NULL ||
flow->stream_tail < flow->stream_consumed)
return -1;
represented = flow->stream_tail - flow->stream_consumed;
if (represented > SP_TCP_FLIGHT_PER_FLOW)
return -1;
per_flow_credit = SP_TCP_FLIGHT_PER_FLOW - represented;
cap = sp_egress_min_u64(per_flow_credit, global_flight_credit);
cap = sp_egress_min_u64(cap, effect_byte_credit);
cap = sp_egress_min_u64(cap,
(uint64_t)SP_TCP_CORE_MAX_SEGMENTS *
SP_TCP_CORE_MAX_MSS);
cap = sp_egress_min_u64(cap, advisory_capacity);
if (cap == 0)
return 0;
struct sp_egress_peek_result peek = sp_egress_peek_backend(
backend_fd, flow->stream_consumed, flow->stream_tail, read_half_closed,
force_fallback, discard, discard_capacity, advisory, (size_t)cap);
if (peek.status == SP_EGRESS_IO_BLOCKED) {
if (sp_egress_arm_backend_lowat(backend_fd, flow) < 0)
return -1;
return 0;
}
if (peek.status == SP_EGRESS_IO_FATAL)
return -1;
if (peek.status == SP_EGRESS_IO_EOF) {
if (flow->eof_reported)
return 0;
flow->eof_reported = 1;
memset(eof, 0, sizeof(*eof));
sp_tcp_event_context_init(&eof->context, flow->flow, flow->generation,
now_ns);
eof->stream_tail.value = flow->stream_tail;
return SP_EGRESS_OBSERVATION_EOF;
}
if (peek.observed > cap || UINT64_MAX - flow->stream_tail < peek.observed)
return -1;
struct sp_egress_executor_flow advanced = *flow;
advanced.stream_tail += peek.observed;
if (sp_egress_arm_backend_lowat(backend_fd, &advanced) < 0)
return -1;
flow->stream_tail = advanced.stream_tail;
memset(credit, 0, sizeof(*credit));
sp_tcp_event_context_init(&credit->context, flow->flow, flow->generation,
now_ns);
credit->stream_tail.value = flow->stream_tail;
return SP_EGRESS_OBSERVATION_CREDIT;
}
SP_TEST_API static int sp_egress_materialize_emit(
struct sp_egress_executor_flow *flow, int backend_fd,
const struct sp_fx_tcp_emit_batch *effect, int force_fallback,
uint8_t *discard,
size_t discard_capacity, uint8_t *payload, size_t payload_capacity) {
uint64_t length;
if (flow == NULL || effect == NULL ||
effect->context.key.flow.value != flow->flow ||
effect->context.key.generation.value != flow->generation ||
effect->end.value < effect->start.value ||
effect->start.value < flow->stream_consumed ||
effect->end.value > flow->stream_tail)
return -1;
length = effect->end.value - effect->start.value;
if (length == 0 || length > payload_capacity ||
length > (uint64_t)SP_TCP_CORE_MAX_SEGMENTS * SP_TCP_CORE_MAX_MSS)
return -1;
struct sp_egress_peek_result peek = sp_egress_peek_backend(
backend_fd, flow->stream_consumed, effect->start.value, 0, force_fallback,
discard, discard_capacity, payload, (size_t)length);
if (peek.status == SP_EGRESS_IO_BLOCKED)
return 0;
if (peek.status != SP_EGRESS_IO_PROGRESS || peek.observed != length) {
errno = EPROTO;
return -1;
}
return 1;
}
SP_TEST_API static int sp_egress_execute_consume_typed(
struct sp_egress_executor_flow *flow, int backend_fd,
const struct sp_fx_stream_consume *effect, uint64_t now_ns,
int force_scratch,
uint8_t *scratch, size_t scratch_capacity,
struct sp_ev_stream_consumed *result) {
uint64_t authorized;
if (flow == NULL || effect == NULL || result == NULL ||
effect->context.key.flow.value != flow->flow ||
effect->context.key.generation.value != flow->generation ||
effect->end.value <= effect->start.value ||
effect->start.value != flow->stream_consumed)
return -1;
authorized = effect->end.value - effect->start.value;
if (authorized > SIZE_MAX)
return -1;
struct sp_egress_consume_result consume = sp_egress_consume_backend(
backend_fd, (size_t)authorized, force_scratch, scratch, scratch_capacity);
if (consume.status == SP_EGRESS_IO_BLOCKED)
return 0;
if (consume.status == SP_EGRESS_IO_FATAL)
return -1;
memset(result, 0, sizeof(*result));
sp_tcp_event_context_init(&result->context, flow->flow, flow->generation,
now_ns);
result->effect = effect->context.effect;
if (consume.status == SP_EGRESS_IO_EOF) {
return 1;
}
if (consume.consumed == 0 || consume.consumed > authorized ||
UINT64_MAX - flow->stream_consumed < consume.consumed)
return -1;
flow->stream_consumed += consume.consumed;
if (sp_egress_arm_backend_lowat(backend_fd, flow) < 0)
return -1;
result->accepted.value = consume.consumed;
return 1;
}
SP_TEST_API static int sp_egress_execute_emit_with_typed(
struct sp_egress_executor_flow *flow, sp_egress_write_fn write_fn, void *context,
const struct sp_fx_tcp_emit_batch *effect, uint32_t src, uint32_t dst,
uint16_t src_port, uint16_t dst_port, const uint8_t *payload,
size_t payload_len, int force_software, uint64_t now_ns,
struct sp_ev_tun_emit_result *result) {
size_t logical_segments;
size_t emitted_records = 0;
size_t accepted_segments = 0;
uint64_t accepted = 0;
if (flow == NULL || effect == NULL || result == NULL ||
effect->context.key.flow.value != flow->flow ||
effect->context.key.generation.value != flow->generation ||
flow->next_control_seq == UINT64_MAX)
return -1;
if (force_software) {
struct sp_tcp_emit_cursor cursor;
struct sp_tcp_emit_cursor next_cursor;
uint8_t scalar_record[SP_TUN_CONTROL_FRAME_MAX];
if (sp_tcp_emit_cursor_init(effect, payload_len, &cursor) < 0)
return -1;
logical_segments = cursor.segment_count;
for (;;) {
size_t segment_payload_len;
next_cursor = cursor;
size_t scalar_len = sp_tcp_emit_format_scalar(
effect, src, dst, src_port, dst_port, payload, payload_len,
&next_cursor,
scalar_record, sizeof(scalar_record), &segment_payload_len);
if (scalar_len == 0 || segment_payload_len == 0)
return -1;
enum sp_egress_io_status status = sp_egress_write_tun_record_with(
write_fn, context, scalar_record, scalar_len);
if (status == SP_EGRESS_IO_BLOCKED) {
if (accepted == 0)
return 0;
break;
}
if (status != SP_EGRESS_IO_PROGRESS)
return -1;
cursor = next_cursor;
accepted += segment_payload_len;
emitted_records++;
accepted_segments++;
if (cursor.segment_index == cursor.segment_count)
break;
}
} else {
uint8_t record[SP_TUN_FRAME_MAX_BYTES];
size_t record_len;
record_len = sp_egress_build_tcp_emit_record(
record, sizeof(record), src, dst, src_port, dst_port, effect, payload,
payload_len, &logical_segments);
if (record_len == 0)
return -1;
enum sp_egress_io_status status =
sp_egress_write_tun_record_with(write_fn, context, record, record_len);
if (status == SP_EGRESS_IO_BLOCKED)
return 0;
if (status != SP_EGRESS_IO_PROGRESS)
return -1;
accepted = payload_len;
emitted_records = 1;
accepted_segments = logical_segments;
}
if (accepted == 0 || accepted > payload_len)
return -1;
sp_counter_add(&g_resource_counters.tcp_logical_segments_host_to_child,
accepted_segments);
sp_counter_add(&g_resource_counters.tcp_payload_bytes_host_to_child,
accepted);
if (!force_software && logical_segments > 1) {
sp_counter_inc(&g_resource_counters.tcp_gso_frames_host_to_child);
sp_counter_add(&g_resource_counters.tcp_gso_segments_host_to_child,
accepted_segments);
} else {
sp_counter_add(&g_resource_counters.tun_vnet_scalar_egress,
emitted_records);
}
memset(result, 0, sizeof(*result));
sp_tcp_event_context_init(&result->context,
effect->context.key.flow.value,
effect->context.key.generation.value, now_ns);
result->context.control_sequence.value = ++flow->next_control_seq;
result->effect = effect->context.effect;
result->accepted.value = accepted;
result->outcome = SP_EMIT_ACCEPTED;
return 1;
}
/* Child-to-backend K_rx staging, durable reservation, and STREAM_SEND
* execution. The pure core authorizes an exact ingress slice against the
* optimistic frontier Q (rx_authorized_end_abs); this executor stages up to
* K_rx immutable frames per flow, submits the exact authorized slice to the
* backend stream socket, and reports the positive backend-accepted prefix
* (advancing A) and the durably-retained suffix (advancing P) back to the core
* as an sp_ev_stream_send_result. Frame bytes never enter the engine. A short
* write copies the unaccepted suffix into the per-flow durable reservation
* before releasing transient staging; EAGAIN copies the complete slice; the
* core re-emits STREAM_SEND with capability 0 to re-flush the reservation once
* the socket is writable. */
#define SP_INGRESS_K_RX_COMPUTED \
((SP_TCP_PENDING_PER_FLOW + \
(SP_TCP_GSO_POLICY_SEGS * SP_TCP_MTU1500_MAX_MSS) - 1) / \
(SP_TCP_GSO_POLICY_SEGS * SP_TCP_MTU1500_MAX_MSS))
enum {
SP_INGRESS_K_RX = SP_INGRESS_K_RX_COMPUTED,
};
_Static_assert(SP_INGRESS_K_RX == 6, "A5 per-flow K_rx geometry changed");
_Static_assert(SP_INGRESS_STAGE_SLOTS >= 2 * SP_INGRESS_K_RX,
"global staging slots must cover 2*K_rx");
enum sp_ingress_stage_state {
SP_INGRESS_STAGE_FREE = 0,
SP_INGRESS_STAGE_STAGED,
};
struct sp_ingress_stage_slot {
enum sp_ingress_stage_state state;
uint64_t flow;
uint64_t generation;
uint64_t ingress_id;
uint64_t interval_start;
uint64_t interval_end;
uint8_t *payload;
};
struct sp_ingress_stage_pool {
struct sp_ingress_stage_slot slots[SP_INGRESS_STAGE_SLOTS];
uint8_t *arena;
size_t global_reserved;
uint64_t next_ingress_id;
};
struct sp_ingress_flow {
uint64_t flow;
uint64_t generation;
uint64_t reserved_base;
size_t pending_bytes;
unsigned staged;
uint16_t reservation_slot;
};
SP_TEST_API static int sp_ingress_stage_pool_init(struct sp_ingress_stage_pool *pool,
uint8_t *arena,
size_t arena_capacity) {
if (pool == NULL || arena == NULL ||
arena_capacity <
(size_t)SP_INGRESS_STAGE_SLOTS * (size_t)SP_TUN_FRAME_MAX_BYTES)
return -1;
memset(pool, 0, sizeof(*pool));
pool->arena = arena;
return 0;
}
SP_TEST_API static void sp_ingress_flow_init(struct sp_ingress_flow *flow,
uint64_t flow_id,
uint64_t generation) {
memset(flow, 0, sizeof(*flow));
flow->flow = flow_id;
flow->generation = generation;
flow->reservation_slot = UINT16_MAX;
}
static struct sp_ingress_stage_slot *sp_ingress_find_slot(struct sp_ingress_stage_pool *pool,
uint64_t flow,
uint64_t generation,
uint64_t ingress_id) {
if (ingress_id == 0)
return NULL;
for (size_t i = 0; i < SP_INGRESS_STAGE_SLOTS; ++i) {
struct sp_ingress_stage_slot *slot = &pool->slots[i];
if (slot->state != SP_INGRESS_STAGE_FREE && slot->ingress_id == ingress_id &&
slot->flow == flow && slot->generation == generation)
return slot;
}
return NULL;
}
static void sp_ingress_stage_free(struct sp_ingress_flow *flow,
struct sp_ingress_stage_slot *slot) {
if (slot->state == SP_INGRESS_STAGE_FREE)
return;
if (flow != NULL && flow->staged > 0)
flow->staged--;
memset(slot, 0, sizeof(*slot));
slot->state = SP_INGRESS_STAGE_FREE;
}
/* Admit one immutable child frame. Returns 1 with *ingress_id_out set, 0 for a
* typed STAGE_BLOCKED (per-flow K_rx cap or global slot exhaustion), or -1 on
* invalid input. */
static int sp_ingress_stage_store(
struct sp_ingress_stage_pool *pool, struct sp_ingress_flow *flow,
uint64_t interval_start, const uint8_t *payload, size_t payload_len,
uint64_t *ingress_id_out) {
size_t free_index = SP_INGRESS_STAGE_SLOTS;
struct sp_ingress_stage_slot *slot;
if (pool == NULL || flow == NULL || ingress_id_out == NULL ||
payload == NULL || payload_len == 0 ||
payload_len > SP_TUN_FRAME_MAX_BYTES ||
UINT64_MAX - interval_start < payload_len ||
pool->next_ingress_id == UINT64_MAX)
return -1;
for (size_t i = 0; i < SP_INGRESS_STAGE_SLOTS; ++i) {
if (pool->slots[i].state == SP_INGRESS_STAGE_FREE) {
free_index = i;
break;
}
}
if (free_index == SP_INGRESS_STAGE_SLOTS)
return 0;
slot = &pool->slots[free_index];
slot->payload = pool->arena + free_index * (size_t)SP_TUN_FRAME_MAX_BYTES;
memcpy(slot->payload, payload, payload_len);
slot->state = SP_INGRESS_STAGE_STAGED;
slot->flow = flow->flow;
slot->generation = flow->generation;
/* IDs are a per-pool sequence: never zero and never reused, so a stale
* completion cannot match a slot that was freed and staged again. */
slot->ingress_id = ++pool->next_ingress_id;
slot->interval_start = interval_start;
slot->interval_end = interval_start + payload_len;
flow->staged++;
*ingress_id_out = slot->ingress_id;
return 1;
}
SP_TEST_API static int sp_ingress_stage_admit(struct sp_ingress_stage_pool *pool,
struct sp_ingress_flow *flow,
uint64_t interval_start,
const uint8_t *payload,
size_t payload_len,
uint64_t *ingress_id_out) {
if (flow->staged >= SP_INGRESS_K_RX)
return 0;
return sp_ingress_stage_store(pool, flow, interval_start, payload,
payload_len, ingress_id_out);
}
/* Dispose every live capability for a flow in finite bounded work. */
SP_TEST_API static size_t sp_ingress_stage_cancel_flow(struct sp_ingress_stage_pool *pool,
struct sp_ingress_flow *flow) {
size_t disposed = 0;
for (size_t i = 0; i < SP_INGRESS_STAGE_SLOTS; ++i) {
struct sp_ingress_stage_slot *slot = &pool->slots[i];
if (slot->state != SP_INGRESS_STAGE_FREE && slot->flow == flow->flow &&
slot->generation == flow->generation) {
sp_ingress_stage_free(flow, slot);
disposed++;
}
}
return disposed;
}
static int sp_ingress_reservation_capacity(struct sp_ingress_stage_pool *pool,
struct sp_ingress_flow *flow,
size_t len,
size_t reservation_capacity) {
if (flow->pending_bytes > reservation_capacity ||
flow->pending_bytes > (size_t)SP_TCP_PENDING_PER_FLOW ||
pool->global_reserved > (size_t)SP_TCP_PENDING_ALL ||
flow->pending_bytes > pool->global_reserved ||
(flow->pending_bytes != 0 &&
flow->reserved_base > UINT64_MAX - flow->pending_bytes))
return -1;
if (len > reservation_capacity - flow->pending_bytes ||
len > (size_t)SP_TCP_PENDING_PER_FLOW - flow->pending_bytes ||
len > (size_t)SP_TCP_PENDING_ALL - pool->global_reserved) {
return 0;
}
return 1;
}
/* Append durably-retained bytes to the per-flow reservation head. Returns 1 on
* success, 0 for STAGE_BLOCKED (byte cap or buffer exhaustion, counted), -1 on
* an invalid reservation invariant or non-contiguous append. */
static int sp_ingress_reserve_append(struct sp_ingress_stage_pool *pool,
struct sp_ingress_flow *flow,
uint64_t abs_start, const uint8_t *bytes,
size_t len, uint8_t *reservation,
size_t reservation_capacity) {
if (len == 0)
return 1;
if (flow->pending_bytes != 0 &&
(flow->reserved_base > UINT64_MAX - flow->pending_bytes ||
abs_start != flow->reserved_base + flow->pending_bytes))
return -1;
{
int capacity = sp_ingress_reservation_capacity(
pool, flow, len, reservation_capacity);
if (capacity <= 0)
return capacity;
}
if (flow->pending_bytes == 0)
flow->reserved_base = abs_start;
memcpy(reservation + flow->pending_bytes, bytes, len);
flow->pending_bytes += len;
pool->global_reserved += len;
return 1;
}
/* Retire the positive backend-accepted prefix from the reservation head. */
static void sp_ingress_reserve_consume(struct sp_ingress_stage_pool *pool,
struct sp_ingress_flow *flow,
uint8_t *reservation, size_t len) {
if (len == 0 || len > flow->pending_bytes || len > pool->global_reserved)
return;
memmove(reservation, reservation + len, flow->pending_bytes - len);
flow->pending_bytes -= len;
flow->reserved_base += len;
pool->global_reserved -= len;
}
typedef sp_tcp_write_fn sp_ingress_send_fn;
/* Classify one nonblocking backend send through the shared write adapter. On
* PROGRESS *accepted holds the positive prefix (full or partial); BLOCKED is
* EAGAIN with nothing accepted; FATAL includes zero and over-reported writes. */
static enum sp_egress_io_status
sp_ingress_send_slice_with(sp_ingress_send_fn send_fn, void *context, const uint8_t *buf,
size_t len, size_t *accepted) {
return sp_tcp_write_slice_with(send_fn, context, buf, len, 0, accepted);
}
SP_TEST_API static ssize_t sp_ingress_kernel_send(void *context, const uint8_t *buf,
size_t len) {
int fd = (int)(intptr_t)context;
return send(fd, buf, len, MSG_DONTWAIT | MSG_NOSIGNAL);
}
/* Execute one SP_FX_STREAM_SEND effect. A nonzero capability authorizes a fresh
* staged frame; a zero capability re-flushes the durable reservation. Returns
* 1 with an sp_ev_stream_send_result (accepted/retained), 0 for a typed
* STAGE_BLOCKED that leaves the effect live for retry, or -1 flow-fatal. */
SP_TEST_API static int sp_ingress_execute_stream_send_typed(
struct sp_ingress_stage_pool *pool, struct sp_ingress_flow *flow,
sp_ingress_send_fn send_fn, void *context,
const struct sp_fx_stream_send *effect,
uint64_t now_ns, uint8_t *reservation, size_t reservation_capacity,
struct sp_ev_stream_send_result *result) {
struct sp_ingress_stage_slot *slot = NULL;
const uint8_t *src;
uint64_t len64;
size_t len;
size_t accepted = 0;
size_t retained;
int queued_behind_reservation = 0;
enum sp_egress_io_status status;
if (pool == NULL || flow == NULL || effect == NULL || result == NULL ||
reservation == NULL || effect->context.key.flow.value != flow->flow ||
effect->context.key.generation.value != flow->generation ||
effect->end.value <= effect->start.value)
return -1;
len64 = effect->end.value - effect->start.value;
if (len64 > SIZE_MAX || len64 > reservation_capacity)
return -1;
len = (size_t)len64;
/* Both fresh sends and capability-zero reflushes rely on the retained-byte
* counters before they can touch reservation storage. */
if (sp_ingress_reservation_capacity(pool, flow, 0,
reservation_capacity) != 1)
return -1;
if (effect->capability.value != 0) {
slot = sp_ingress_find_slot(pool, flow->flow, flow->generation,
effect->capability.value);
if (slot == NULL || effect->start.value < slot->interval_start ||
effect->end.value > slot->interval_end)
return -1;
src = slot->payload +
(size_t)(effect->start.value - slot->interval_start);
/* Reserve the exact slice against both byte caps before the first send. */
{
int capacity = sp_ingress_reservation_capacity(
pool, flow, len, reservation_capacity);
if (capacity <= 0)
return capacity;
}
} else {
if (flow->pending_bytes == 0 ||
effect->start.value != flow->reserved_base ||
len > flow->pending_bytes)
return -1;
src = reservation;
}
/* A durable prefix is already waiting at [A,P). A later fresh capability
* starts at P, so sending it directly would put bytes after the gap on the
* backend socket and then report a non-head accepted prefix to the core.
* Preserve wire order mechanically: append the whole fresh slice to the
* existing reservation and report it retained without issuing send(2).
* EPOLLOUT later re-flushes the one contiguous [A,P) reservation. */
if (effect->capability.value != 0 && flow->pending_bytes != 0) {
int reserved = sp_ingress_reserve_append(
pool, flow, effect->start.value, src, len, reservation,
reservation_capacity);
if (reserved <= 0)
return reserved;
retained = len;
queued_behind_reservation = 1;
} else {
status = sp_ingress_send_slice_with(send_fn, context, src, len, &accepted);
if (status == SP_EGRESS_IO_FATAL || accepted > len)
return -1;
retained = len - accepted;
}
if (effect->capability.value != 0) {
if (retained > 0 && !queued_behind_reservation) {
int reserved =
sp_ingress_reserve_append(pool, flow, effect->start.value + accepted,
src + accepted, retained, reservation,
reservation_capacity);
if (reserved <= 0)
return reserved;
}
sp_ingress_stage_free(flow, slot);
} else if (accepted > 0) {
sp_ingress_reserve_consume(pool, flow, reservation, accepted);
}
memset(result, 0, sizeof(*result));
sp_tcp_event_context_init(&result->context, flow->flow, flow->generation,
now_ns);
result->effect = effect->context.effect;
result->accepted.value = accepted;
result->retained.value = retained;
result->outcome = retained != 0 ? SP_SEND_RETRY : SP_SEND_PROGRESS;
return 1;
}
/* Release a staged capability on the core's SP_FX_INGRESS_DISPOSE and
* acknowledge with an sp_ev_ingress_disposed. Idempotent: a capability already
* released by a completed STREAM_SEND still acknowledges. */
SP_TEST_API static int sp_ingress_execute_dispose_typed(struct sp_ingress_stage_pool *pool,
struct sp_ingress_flow *flow,
const struct sp_fx_ingress_dispose *effect,
uint64_t now_ns,
struct sp_ev_ingress_disposed *result) {
struct sp_ingress_stage_slot *slot;
if (pool == NULL || flow == NULL || effect == NULL || result == NULL ||
effect->context.key.flow.value != flow->flow ||
effect->context.key.generation.value != flow->generation)
return -1;
slot = sp_ingress_find_slot(pool, flow->flow, flow->generation,
effect->ingress.value);
if (slot != NULL) {
sp_ingress_stage_free(flow, slot);
}
memset(result, 0, sizeof(*result));
sp_tcp_event_context_init(&result->context, flow->flow, flow->generation,
now_ns);
result->effect = effect->context.effect;
return 1;
}
/* TUN egress scheduler: dual control/data FIFOs, per-flow
* byte deficit round robin, and credit epochs.
*
* This is upstream of, not a replacement for, `g_tun_queue`. The scheduler
* owns typed flow/class/cost records before emission. `tun_write_frame()` may
* then retain an already-scheduled opaque frame in the final kernel-EAGAIN
* FIFO; scalar UDP/publish producers also use that final FIFO. Collapsing the
* two requires one common final-emission ownership contract, not merely a size
* saving. One opaque FIFO alone cannot implement class bypass or byte DRR.
*
* Records are admitted with an explicit class and cost, and serviced against an
* injected write sink so the whole package is testable without a TUN fd. */
enum sp_scheduler_class {
SP_SCHEDULER_CLASS_DATA = 0,
SP_SCHEDULER_CLASS_CONTROL,
};
/* Control records carry a reserve tier. Terminal SYN/FIN/RST effects and
* ordinary response ACKs have named slot reserves that ordinary control traffic
* may not consume. */
enum sp_scheduler_ctrl_tier {
SP_SCHEDULER_CTRL_ORDINARY = 0,
SP_SCHEDULER_CTRL_RESPONSE,
SP_SCHEDULER_CTRL_TERMINAL,
};
/* Dimensions a producer can be blocked on. A blocked producer records every
* responsible dimension and does not retry until a greater credit epoch reports
* growth in one of them. */
enum sp_scheduler_credit_dim {
SP_SCHEDULER_DIM_DATA_SLOTS = 1U << 0,
SP_SCHEDULER_DIM_DATA_BYTES = 1U << 1,
SP_SCHEDULER_DIM_CONTROL_SLOTS = 1U << 2,
SP_SCHEDULER_DIM_CONTROL_BYTES = 1U << 3,
};
enum {
/* DRR metadata must cover every concurrently admissible outbound TCP owner.
* This is a small control structure, not per-flow payload storage. */
SP_SCHEDULER_MAX_FLOWS = MAX_TCP,
/* Per-logical-packet cost charged on top of bytes, so a flow sending many
* tiny packets is not serviced as cheaply as its byte count suggests. Never
* charged per GSO container: a 32-segment frame is charged as 32 logical
* packets, not as one. */
SP_SCHEDULER_PACKET_COST = 64,
/* One DRR round grants this many bytes of deficit to each active flow. */
SP_SCHEDULER_DRR_QUANTUM = 16 * 1024,
/* Carry is capped at the charge of one maximum authorized batch, so an idle
* flow cannot accumulate unbounded credit and then monopolise a round. This
* must account for the per-logical-packet cost of every segment in that
* batch, not one: a cap below the maximum charge would make the largest
* legal record permanently unaffordable. */
SP_SCHEDULER_DRR_CARRY_MAX = SP_TCP_GSO_POLICY_SEGS *
(SP_TCP_MTU1500_MAX_MSS + SP_SCHEDULER_PACKET_COST),
/* A flow inactive for this many rounds has its deficit reset. */
SP_SCHEDULER_DRR_IDLE_ROUNDS = 4,
/* Hard bound on round advances per service call. The clock is supplied by
* the caller and a caller may legitimately hold it still, so termination may
* not depend on time passing. */
SP_SCHEDULER_ROUNDS_PER_CALL = 64,
/* Per-slot arena stride. Resource geometry sizes the data arena as 32 x
* 64 KiB, which is
* nine bytes short of 32 full SP_TUN_FRAME_MAX_BYTES frames, so the data lane
* admits frames up to the stride rather than the absolute frame ceiling.
* That is not a practical restriction: the largest authorized batch is 32
* segments x 1460 bytes plus headers, far below the stride. */
SP_SCHEDULER_CONTROL_STRIDE = SP_TUN_CONTROL_FRAME_MAX,
SP_SCHEDULER_DATA_STRIDE = SP_TUN_DATA_MAX_BYTES / SP_TUN_DATA_MAX_PKTS,
};
_Static_assert(SP_SCHEDULER_CONTROL_STRIDE * SP_TUN_CONTROL_MAX_PKTS <=
SP_TUN_CONTROL_MAX_BYTES,
"scheduler control slots must fit the resource control arena");
_Static_assert(SP_SCHEDULER_DATA_STRIDE * SP_TUN_DATA_MAX_PKTS <=
SP_TUN_DATA_MAX_BYTES,
"scheduler data slots must fit the resource data arena");
_Static_assert(SP_SCHEDULER_DATA_STRIDE > SP_VNET_HDR_BYTES + SP_IPV4_HEADER_BYTES +
SP_TCP_GSO_POLICY_SEGS *
SP_TCP_MTU1500_MAX_MSS,
"scheduler data stride must hold a maximum authorized GSO batch");
/* The largest admissible record must be affordable, or DRR could never issue
* it and the service loop would spin advancing rounds forever. */
_Static_assert(SP_SCHEDULER_DRR_CARRY_MAX >= SP_TCP_GSO_POLICY_SEGS *
SP_TCP_MTU1500_MAX_MSS +
SP_TCP_GSO_POLICY_SEGS *
SP_SCHEDULER_PACKET_COST,
"scheduler carry cap must cover the maximum authorized batch charge");
struct sp_scheduler_record {
uint64_t flow;
uint64_t generation;
/* UINT16_MAX means that the record has no live executor owner (for example
* an auxiliary CLOSED response). Live producers carry the checked TCP
* table index so the final sink never reverses an identity through a table
* scan. */
uint16_t flow_index;
enum sp_scheduler_class klass;
enum sp_scheduler_ctrl_tier tier;
/* Absolute sequence this record occupies, and for control records the
* sequence that must already be committed before it may be serviced. */
uint64_t seq_abs;
uint64_t depends_on_seq;
uint64_t logical_bytes;
unsigned logical_packets;
uint8_t terminal;
size_t frame_len;
size_t frame_offset;
uint8_t in_use;
};
/* Each slot owns a fixed stride of the arena, so a frame never moves and
* out-of-order removal cannot fragment the arena. FIFO order is kept in a
* separate small array of slot indices; removing a record shifts those indices
* rather than any frame bytes. */
struct sp_scheduler_queue {
struct sp_scheduler_record records[SP_TUN_CONTROL_MAX_PKTS];
uint16_t order[SP_TUN_CONTROL_MAX_PKTS];
size_t capacity_slots;
size_t capacity_bytes;
size_t stride;
size_t count;
size_t bytes;
uint8_t *arena;
};
struct sp_scheduler_flow {
uint64_t flow;
int64_t deficit;
uint64_t committed_seq;
uint64_t last_round;
size_t queued_records;
uint8_t active;
};
struct sp_scheduler_credit {
uint64_t epoch;
size_t data_slots;
size_t data_bytes;
size_t control_slots;
size_t control_bytes;
};
struct sp_scheduler_sched {
struct sp_scheduler_queue control;
struct sp_scheduler_queue data;
struct sp_scheduler_flow flows[SP_SCHEDULER_MAX_FLOWS];
size_t flow_count;
uint64_t round;
uint64_t epoch;
};
_Static_assert(MAX_TCP < UINT16_MAX,
"scheduler flow index sentinel must not alias a TCP slot");
struct sp_scheduler_turn {
size_t packets;
size_t bytes;
size_t records;
uint64_t started_ns;
uint8_t write_blocked;
};
/* Sink verdict for one offered record. FAILED stops service for broker cleanup;
* RETIRED removes the record without counting or charging its flow's deficit. */
enum {
SP_SCHEDULER_WRITE_FAILED = -2,
SP_SCHEDULER_WRITE_BLOCKED = -1,
SP_SCHEDULER_WRITE_DONE = 0,
SP_SCHEDULER_WRITE_RETIRED = 1,
};
/* The record and its frame are valid only for the call; the sink must not
* retain them or mutate the scheduler. */
typedef int (*sp_scheduler_write_fn)(void *context,
const struct sp_scheduler_record *record,
const uint8_t *frame);
/* Supplies the current monotonic time. Passing NULL disables the nanosecond
* budgets and leaves the count budgets and the round bound as the termination
* guarantee. */
typedef uint64_t (*sp_scheduler_clock_fn)(void *context);
static void sp_scheduler_queue_init(struct sp_scheduler_queue *queue, uint8_t *arena,
size_t capacity_slots, size_t capacity_bytes,
size_t stride) {
queue->arena = arena;
queue->capacity_slots = capacity_slots;
queue->capacity_bytes = capacity_bytes;
queue->stride = stride;
}
static size_t sp_scheduler_queue_free_slot(const struct sp_scheduler_queue *queue) {
size_t i;
for (i = 0; i < queue->capacity_slots; ++i)
if (!queue->records[i].in_use)
return i;
return SIZE_MAX;
}
/* `sched` must be zero-filled: the broker's static instance is, and clearing
* it again would fault in its 56 KB just to write zeros (callers that reuse
* storage, such as tests, clear it themselves). */
SP_TEST_API static void sp_scheduler_init(struct sp_scheduler_sched *sched,
uint8_t *control_arena,
size_t control_arena_len,
uint8_t *data_arena,
size_t data_arena_len) {
(void)control_arena_len;
(void)data_arena_len;
sp_scheduler_queue_init(&sched->control, control_arena, SP_TUN_CONTROL_MAX_PKTS,
SP_TUN_CONTROL_MAX_BYTES, SP_SCHEDULER_CONTROL_STRIDE);
sp_scheduler_queue_init(&sched->data, data_arena, SP_TUN_DATA_MAX_PKTS,
SP_TUN_DATA_MAX_BYTES, SP_SCHEDULER_DATA_STRIDE);
sched->epoch = 1;
}
static struct sp_scheduler_flow *sp_scheduler_flow_lookup(struct sp_scheduler_sched *sched,
uint64_t flow, int create) {
size_t i;
for (i = 0; i < sched->flow_count; ++i)
if (sched->flows[i].flow == flow)
return &sched->flows[i];
if (!create || sched->flow_count == SP_SCHEDULER_MAX_FLOWS)
return NULL;
sched->flows[sched->flow_count].flow = flow;
sched->flows[sched->flow_count].deficit = 0;
sched->flows[sched->flow_count].committed_seq = 0;
sched->flows[sched->flow_count].last_round = sched->round;
sched->flows[sched->flow_count].queued_records = 0;
sched->flows[sched->flow_count].active = 0;
return &sched->flows[sched->flow_count++];
}
/* Charge is bytes plus a fixed cost per *logical* packet. A GSO container is
* charged for every segment it expands into, so a 64 KiB super-frame cannot
* receive the same service as a 64-byte control packet. */
static uint64_t sp_scheduler_charge(const struct sp_scheduler_record *record) {
unsigned packets = record->logical_packets == 0 ? 1 : record->logical_packets;
return record->logical_bytes + (uint64_t)packets * SP_SCHEDULER_PACKET_COST;
}
/* Slots a class must leave free for its reserve tiers. */
static size_t sp_scheduler_control_reserve_for(enum sp_scheduler_ctrl_tier tier) {
switch (tier) {
case SP_SCHEDULER_CTRL_TERMINAL:
return 0;
case SP_SCHEDULER_CTRL_RESPONSE:
return SP_TUN_TERMINAL_RESERVE;
case SP_SCHEDULER_CTRL_ORDINARY:
default:
return (size_t)SP_TUN_TERMINAL_RESERVE + SP_TUN_RESPONSE_RESERVE;
}
}
static void sp_scheduler_publish_epoch(struct sp_scheduler_sched *sched) {
sched->epoch++;
}
SP_TEST_API static struct sp_scheduler_credit sp_scheduler_credit_now(
const struct sp_scheduler_sched *sched) {
struct sp_scheduler_credit credit;
credit.epoch = sched->epoch;
credit.data_slots = sched->data.capacity_slots - sched->data.count;
credit.data_bytes = sched->data.capacity_bytes - sched->data.bytes;
credit.control_slots = sched->control.capacity_slots - sched->control.count;
credit.control_bytes = sched->control.capacity_bytes - sched->control.bytes;
return credit;
}
/* True when `later` reports growth, versus `earlier`, in at least one of the
* dimensions the producer was blocked on. Readiness alone is not progress: a
* greater epoch with no growth in a blocked dimension must not trigger a retry. */
SP_TEST_API static int sp_scheduler_credit_permits_retry(
const struct sp_scheduler_credit *earlier, const struct sp_scheduler_credit *later,
unsigned blocked_dims) {
if (later->epoch <= earlier->epoch)
return 0;
if ((blocked_dims & SP_SCHEDULER_DIM_DATA_SLOTS) != 0 &&
later->data_slots > earlier->data_slots)
return 1;
if ((blocked_dims & SP_SCHEDULER_DIM_DATA_BYTES) != 0 &&
later->data_bytes > earlier->data_bytes)
return 1;
if ((blocked_dims & SP_SCHEDULER_DIM_CONTROL_SLOTS) != 0 &&
later->control_slots > earlier->control_slots)
return 1;
if ((blocked_dims & SP_SCHEDULER_DIM_CONTROL_BYTES) != 0 &&
later->control_bytes > earlier->control_bytes)
return 1;
return 0;
}
/* Admits one framed record. Returns 0 on success; -1 with *blocked_dims set to
* every responsible dimension when the record does not fit. Data may consume
* neither control slots nor control bytes, and never falsely reports a commit. */
SP_TEST_API static int sp_scheduler_admit_with_flow_index(
struct sp_scheduler_sched *sched, const struct sp_scheduler_record *record,
const uint8_t *frame, size_t frame_len, unsigned *blocked_dims,
uint16_t flow_index) {
struct sp_scheduler_queue *queue;
struct sp_scheduler_flow *flow = NULL;
struct sp_scheduler_record *slot;
size_t reserve;
size_t tail;
unsigned blocked = 0;
*blocked_dims = 0;
if (flow_index != UINT16_MAX &&
((size_t)flow_index >= sp_ns_tcp_slot_limit() || flow_index >= MAX_TCP))
return -1;
if (frame_len == 0 || record->generation == 0)
return -1;
/* A record whose charge exceeds the carry cap could never be issued by DRR.
* Refusing it at admission keeps "every admitted record is eventually
* affordable" an enforced invariant rather than an assumption. */
if (record->klass == SP_SCHEDULER_CLASS_DATA &&
sp_scheduler_charge(record) > (uint64_t)SP_SCHEDULER_DRR_CARRY_MAX)
return -1;
if (record->klass == SP_SCHEDULER_CLASS_CONTROL) {
queue = &sched->control;
/* Control/response records are scalar and never use GSO, so the byte proof
* cannot be bypassed by a super-frame on the control lane. */
if (frame_len > SP_TUN_CONTROL_FRAME_MAX)
return -1;
reserve = sp_scheduler_control_reserve_for(record->tier);
if (queue->count + reserve >= queue->capacity_slots &&
!(record->tier == SP_SCHEDULER_CTRL_TERMINAL &&
queue->count < queue->capacity_slots))
blocked |= SP_SCHEDULER_DIM_CONTROL_SLOTS;
if (queue->bytes + frame_len > queue->capacity_bytes)
blocked |= SP_SCHEDULER_DIM_CONTROL_BYTES;
} else {
queue = &sched->data;
if (queue->count >= queue->capacity_slots)
blocked |= SP_SCHEDULER_DIM_DATA_SLOTS;
if (queue->bytes + frame_len > queue->capacity_bytes)
blocked |= SP_SCHEDULER_DIM_DATA_BYTES;
}
if (frame_len > queue->stride)
return -1;
if (blocked != 0) {
*blocked_dims = blocked;
return -1;
}
/* Control-only/auxiliary responses need no DRR or committed-data state.
* Allocating a flow slot for every CLOSED RST would let historical hostile
* tuples exhaust metadata needed by live data owners. */
if (record->klass == SP_SCHEDULER_CLASS_DATA) {
flow = sp_scheduler_flow_lookup(sched, record->flow, 1);
if (flow == NULL) {
*blocked_dims = SP_SCHEDULER_DIM_DATA_SLOTS;
return -1;
}
} else if (flow_index != UINT16_MAX) {
flow = sp_scheduler_flow_lookup(sched, record->flow, 0);
}
if (flow != NULL && flow->queued_records == SIZE_MAX)
return -1;
tail = sp_scheduler_queue_free_slot(queue);
if (tail == SIZE_MAX) {
*blocked_dims = record->klass == SP_SCHEDULER_CLASS_CONTROL
? SP_SCHEDULER_DIM_CONTROL_SLOTS
: SP_SCHEDULER_DIM_DATA_SLOTS;
return -1;
}
slot = &queue->records[tail];
*slot = *record;
slot->flow_index = flow_index;
slot->frame_len = frame_len;
slot->frame_offset = tail * queue->stride;
slot->in_use = 1;
memcpy(queue->arena + slot->frame_offset, frame, frame_len);
queue->order[queue->count] = (uint16_t)tail;
queue->count++;
queue->bytes += frame_len;
if (flow != NULL) {
flow->queued_records++;
}
if (flow != NULL && !flow->active) {
flow->active = 1;
flow->last_round = sched->round;
}
return 0;
}
/* Records the sequence a flow has committed, satisfying control dependencies. */
SP_TEST_API static void sp_scheduler_commit_seq(struct sp_scheduler_sched *sched, uint64_t flow,
uint64_t seq_abs) {
struct sp_scheduler_flow *entry = sp_scheduler_flow_lookup(sched, flow, 1);
if (entry != NULL && seq_abs > entry->committed_seq)
entry->committed_seq = seq_abs;
}
static int sp_scheduler_control_eligible(struct sp_scheduler_sched *sched,
const struct sp_scheduler_record *record) {
struct sp_scheduler_flow *flow;
if (record->depends_on_seq == 0)
return 1;
flow = sp_scheduler_flow_lookup(sched, record->flow, 0);
return flow != NULL && flow->committed_seq >= record->depends_on_seq;
}
/* Selects the next eligible control record, or SIZE_MAX when none is ready. */
static size_t sp_scheduler_next_control(struct sp_scheduler_sched *sched) {
size_t i;
for (i = 0; i < sched->control.count; ++i)
if (sp_scheduler_control_eligible(
sched, &sched->control.records[sched->control.order[i]]))
return i;
return SIZE_MAX;
}
/* Removes the record at FIFO position `position`, preserving the order of the
* rest. Only the small index array moves; frame bytes never do. */
static void sp_scheduler_queue_remove_at(struct sp_scheduler_sched *sched,
struct sp_scheduler_queue *queue,
size_t position) {
size_t slot = queue->order[position];
size_t i;
struct sp_scheduler_flow *flow = NULL;
if (queue->records[slot].klass == SP_SCHEDULER_CLASS_DATA ||
queue->records[slot].flow_index != UINT16_MAX)
flow = sp_scheduler_flow_lookup(sched, queue->records[slot].flow, 0);
queue->bytes -= queue->records[slot].frame_len;
if (flow != NULL && flow->queued_records > 0)
flow->queued_records--;
queue->records[slot].in_use = 0;
for (i = position + 1; i < queue->count; ++i)
queue->order[i - 1] = queue->order[i];
queue->count--;
}
/* A flow id is process-unique, but the scheduler's metadata arena is bounded
* to the maximum *concurrent* population. Retire both its queued records and
* DRR state when the live owner closes; otherwise sequential connections fill
* the arena permanently after SP_SCHEDULER_MAX_FLOWS admissions. */
static void sp_scheduler_retire_flow(struct sp_scheduler_sched *sched, uint64_t flow,
uint64_t generation) {
struct sp_scheduler_queue *queues[2] = {&sched->control, &sched->data};
int released_credit = 0;
size_t q;
size_t i;
if (flow == 0 || generation == 0)
return;
for (q = 0; q < 2; ++q) {
struct sp_scheduler_queue *queue = queues[q];
for (i = 0; i < queue->count;) {
const struct sp_scheduler_record *record =
&queue->records[queue->order[i]];
if (record->flow == flow && record->generation == generation) {
sp_scheduler_queue_remove_at(sched, queue, i);
released_credit = 1;
} else {
i++;
}
}
}
for (i = 0; i < sched->flow_count; ++i) {
if (sched->flows[i].flow != flow)
continue;
if (i + 1 < sched->flow_count)
memmove(&sched->flows[i], &sched->flows[i + 1],
(sched->flow_count - i - 1) * sizeof(sched->flows[0]));
memset(&sched->flows[--sched->flow_count], 0, sizeof(sched->flows[0]));
break;
}
if (released_credit)
sp_scheduler_publish_epoch(sched);
}
/* Grants one quantum to every active flow and advances the round. Carry is
* capped so an idle flow cannot bank unbounded credit, and a flow inactive for
* SP_SCHEDULER_DRR_IDLE_ROUNDS has its deficit reset outright. */
static void sp_scheduler_advance_round(struct sp_scheduler_sched *sched) {
size_t i;
sched->round++;
for (i = 0; i < sched->flow_count; ++i) {
struct sp_scheduler_flow *flow = &sched->flows[i];
if (!flow->active) {
if (sched->round - flow->last_round >= SP_SCHEDULER_DRR_IDLE_ROUNDS)
flow->deficit = 0;
continue;
}
flow->deficit += SP_SCHEDULER_DRR_QUANTUM;
if (flow->deficit > (int64_t)SP_SCHEDULER_DRR_CARRY_MAX)
flow->deficit = (int64_t)SP_SCHEDULER_DRR_CARRY_MAX;
flow->last_round = sched->round;
}
}
static int sp_scheduler_turn_exhausted(const struct sp_scheduler_turn *turn, uint64_t now_ns) {
if (turn->packets >= (size_t)SP_TUN_EGRESS_PKTS_TURN ||
turn->bytes >= (size_t)SP_TUN_EGRESS_BYTES_TURN ||
turn->records >= (size_t)SP_EFFECTS_TURN)
return 1;
return turn->started_ns != 0 && now_ns >= turn->started_ns &&
now_ns - turn->started_ns >= (uint64_t)SP_CALLBACK_BUDGET_NS;
}
static int sp_scheduler_write_record(struct sp_scheduler_queue *queue, size_t position,
sp_scheduler_write_fn write_fn, void *context,
struct sp_scheduler_turn *turn) {
const struct sp_scheduler_record *record = &queue->records[queue->order[position]];
unsigned packets = record->logical_packets == 0 ? 1 : record->logical_packets;
int result = write_fn(context, record, queue->arena + record->frame_offset);
/* A sink must return a verdict, rather than a frame byte count. */
assert(result >= SP_SCHEDULER_WRITE_FAILED);
assert(result <= SP_SCHEDULER_WRITE_RETIRED);
if (result <= SP_SCHEDULER_WRITE_BLOCKED) {
/* Never commit a blocked or failed write. The caller retries backpressure
* but propagates fatal failure without offering another record. */
turn->write_blocked = 1;
return result;
}
if (result == SP_SCHEDULER_WRITE_RETIRED)
return 1;
turn->packets += packets;
turn->bytes += record->frame_len;
turn->records++;
return 0;
}
/* Services one scheduler turn against a writable sink.
*
* Control is selected before bulk on every writability service, so a control
* record cannot be stuck behind data already queued. Data is then served by
* per-flow byte DRR: a whole record must fit the flow's current deficit, except
* for one explicitly charged atomic-frame exception per round for a record that
* can never fit a quantum. An issued batch never spans a future round.
*
* Returns the number of records written (excluding retirement), or SIZE_MAX
* on a fatal sink failure. */
SP_TEST_API static size_t sp_scheduler_service(struct sp_scheduler_sched *sched,
sp_scheduler_write_fn write_fn, void *context,
sp_scheduler_clock_fn clock_fn,
void *clock_context) {
struct sp_scheduler_turn turn;
uint64_t now_ns = clock_fn != NULL ? clock_fn(clock_context) : 0;
uint64_t round_started = now_ns;
size_t rounds = 0;
size_t written = 0;
memset(&turn, 0, sizeof(turn));
turn.started_ns = now_ns;
for (;;) {
size_t control_index;
size_t i;
int progressed = 0;
/* Re-read the clock each iteration; a by-value start time can never expire
* its own budget. */
if (clock_fn != NULL)
now_ns = clock_fn(clock_context);
if (sp_scheduler_turn_exhausted(&turn, now_ns))
break;
/* 1. Control and terminal responses first. */
control_index = sp_scheduler_next_control(sched);
if (control_index != SIZE_MAX) {
int write_rc = sp_scheduler_write_record(&sched->control, control_index,
write_fn, context, &turn);
if (write_rc == SP_SCHEDULER_WRITE_FAILED)
return SIZE_MAX;
if (write_rc < 0)
break;
sp_scheduler_queue_remove_at(sched, &sched->control, control_index);
sp_scheduler_publish_epoch(sched);
written += write_rc == 0;
continue;
}
/* 2. Bulk data under per-flow deficit. */
if (sched->data.count == 0)
break;
for (i = 0; i < sched->data.count; ++i) {
struct sp_scheduler_record *record = &sched->data.records[sched->data.order[i]];
struct sp_scheduler_flow *flow = sp_scheduler_flow_lookup(sched, record->flow, 0);
uint64_t charge;
if (flow == NULL)
continue;
charge = sp_scheduler_charge(record);
if ((uint64_t)flow->deficit < charge) {
/* One atomic-frame exception: a record that can never fit a full
* quantum is issued once and explicitly charged, so it cannot deadlock
* the flow. It is not a licence to borrow from a future round. */
if (charge > (uint64_t)SP_SCHEDULER_DRR_CARRY_MAX ||
charge <= (uint64_t)SP_SCHEDULER_DRR_QUANTUM ||
flow->deficit < (int64_t)SP_SCHEDULER_DRR_QUANTUM)
continue;
}
int write_rc = sp_scheduler_write_record(&sched->data, i, write_fn,
context, &turn);
if (write_rc == SP_SCHEDULER_WRITE_FAILED)
return SIZE_MAX;
if (write_rc < 0)
break;
if (write_rc == 0)
flow->deficit -= (int64_t)charge;
sp_scheduler_queue_remove_at(sched, &sched->data, i);
sp_scheduler_publish_epoch(sched);
written += write_rc == 0;
progressed = 1;
break;
}
if (turn.write_blocked)
break;
if (progressed)
continue;
/* No flow could afford its head record: advance the round. One logical
* round stops at SP_SCHED_ROUND_NS but resumes with ready ages unchanged. */
if (clock_fn != NULL && now_ns >= round_started &&
now_ns - round_started >= (uint64_t)SP_SCHED_ROUND_NS)
break;
if (sched->data.count == 0)
break;
/* Bounded independently of the clock: a caller holding time still must
* still see this call return. */
if (rounds >= (size_t)SP_SCHEDULER_ROUNDS_PER_CALL)
break;
rounds++;
sp_scheduler_advance_round(sched);
}
/* `queued_records` is maintained at admission/removal, so this state update
* is O(number of live scheduler flows), not a queue reconstruction scan. */
{
size_t i;
for (i = 0; i < sched->flow_count; ++i) {
struct sp_scheduler_flow *flow = &sched->flows[i];
flow->active = flow->queued_records != 0 ? 1 : 0;
}
}
return written;
}
/* Frozen overload response behavior.
*
* This is a classify-and-shed decision layer in front of the scheduler's
* control admission. It rate-limits response classes against the resource token
* buckets, coalesces equivalent current ACKs, refuses to answer a RST with a
* RST, sheds challenge/ICMP/new-admission work before ordinary responses when
* capacity runs out, and treats a terminal record that cannot be enqueued as a
* global-fatal invariant violation rather than backpressure. It is coupled to
* the scheduler on purpose: "retain the newest current ACK, never drop a
* terminal" is expressed through the scheduler's response and terminal slot
* reserves, and coalescing replaces an already-queued current ACK in place.
*
* The live adapter wires this in front of the live control lane; it is also exercised
* only on synthetic records. */
/* One token is NS_PER_S of allowance, so refill is exact integer arithmetic
* against an injected monotonic clock with no floating point. */
#define SP_SCHEDULER_TOKEN_NS 1000000000ULL
struct sp_scheduler_token_bucket {
uint64_t allowance;
uint64_t max_allowance;
uint64_t rate_per_s;
uint64_t last_ns;
};
static void sp_scheduler_bucket_init(struct sp_scheduler_token_bucket *bucket,
uint64_t rate_per_s, uint64_t burst,
uint64_t now_ns) {
bucket->rate_per_s = rate_per_s;
bucket->max_allowance = burst * SP_SCHEDULER_TOKEN_NS;
bucket->allowance = bucket->max_allowance;
bucket->last_ns = now_ns;
}
static void sp_scheduler_bucket_refill(struct sp_scheduler_token_bucket *bucket,
uint64_t now_ns) {
uint64_t elapsed;
uint64_t add;
uint64_t max_useful;
if (now_ns <= bucket->last_ns) {
/* A non-advancing or backward clock adds nothing and never leaks tokens. */
bucket->last_ns = now_ns;
return;
}
elapsed = now_ns - bucket->last_ns;
bucket->last_ns = now_ns;
if (bucket->rate_per_s == 0)
return;
/* Cap elapsed so the multiply cannot overflow; anything beyond this refills
* to the ceiling anyway. */
max_useful = bucket->max_allowance / bucket->rate_per_s + 1;
if (elapsed > max_useful)
elapsed = max_useful;
add = elapsed * bucket->rate_per_s;
if (bucket->max_allowance - bucket->allowance <= add)
bucket->allowance = bucket->max_allowance;
else
bucket->allowance += add;
}
/* Consumes one token if available. Returns 1 on success, 0 when the bucket is
* empty. */
static int sp_scheduler_bucket_take(struct sp_scheduler_token_bucket *bucket,
uint64_t now_ns) {
sp_scheduler_bucket_refill(bucket, now_ns);
if (bucket->allowance < SP_SCHEDULER_TOKEN_NS)
return 0;
bucket->allowance -= SP_SCHEDULER_TOKEN_NS;
return 1;
}
enum sp_scheduler_response_kind {
SP_SCHEDULER_RESP_CURRENT_ACK = 0,
SP_SCHEDULER_RESP_CHALLENGE_ACK,
SP_SCHEDULER_RESP_CLOSED_RST,
SP_SCHEDULER_RESP_ICMP,
SP_SCHEDULER_RESP_TERMINAL,
};
enum sp_scheduler_shed_decision {
SP_SCHEDULER_SHED_ADMIT = 0, /* enqueued for transmission */
SP_SCHEDULER_SHED_COALESCED, /* replaced an equivalent current ACK in place */
SP_SCHEDULER_SHED_DROPPED, /* rate/capacity shed, counted, non-fatal */
SP_SCHEDULER_SHED_RST_TO_RST, /* CLOSED reset suppressed: never answer RST with RST */
SP_SCHEDULER_SHED_FATAL, /* a terminal record could not be enqueued */
};
enum { SP_SCHEDULER_OVERLOAD_MAX_FLOWS = SP_SCHEDULER_MAX_FLOWS };
struct sp_scheduler_overload_flow {
uint64_t flow;
struct sp_scheduler_token_bucket challenge;
struct sp_scheduler_token_bucket icmp;
uint8_t in_use;
};
struct sp_scheduler_overload {
struct sp_scheduler_token_bucket challenge_all;
struct sp_scheduler_token_bucket closed_rst_all;
struct sp_scheduler_token_bucket icmp_all;
struct sp_scheduler_overload_flow flows[SP_SCHEDULER_OVERLOAD_MAX_FLOWS];
/* Saturating counters with fixed reason labels. */
uint64_t coalesced_current_ack;
uint64_t shed_challenge;
uint64_t shed_closed_rst;
uint64_t shed_icmp;
uint64_t shed_current_ack;
uint64_t suppressed_rst_to_rst;
uint64_t fatal_terminal;
};
static void sp_scheduler_sat_inc(uint64_t *counter) {
if (*counter != UINT64_MAX)
(*counter)++;
}
/* `ov` must be zero-filled, as for sp_scheduler_init(). */
SP_TEST_API static void sp_scheduler_overload_init(struct sp_scheduler_overload *ov,
uint64_t now_ns) {
sp_scheduler_bucket_init(&ov->challenge_all, SP_CHALLENGE_ALL_PER_S,
SP_CHALLENGE_ALL_BURST, now_ns);
sp_scheduler_bucket_init(&ov->closed_rst_all, SP_CLOSED_RST_ALL_PER_S,
SP_CLOSED_RST_ALL_BURST, now_ns);
sp_scheduler_bucket_init(&ov->icmp_all, SP_ICMP_ALL_PER_S, SP_ICMP_ALL_BURST,
now_ns);
}
static struct sp_scheduler_overload_flow *
sp_scheduler_overload_flow(struct sp_scheduler_overload *ov, uint64_t flow, uint64_t now_ns) {
size_t i;
size_t free_slot = SP_SCHEDULER_OVERLOAD_MAX_FLOWS;
for (i = 0; i < SP_SCHEDULER_OVERLOAD_MAX_FLOWS; ++i) {
if (ov->flows[i].in_use && ov->flows[i].flow == flow)
return &ov->flows[i];
if (!ov->flows[i].in_use && free_slot == SP_SCHEDULER_OVERLOAD_MAX_FLOWS)
free_slot = i;
}
if (free_slot == SP_SCHEDULER_OVERLOAD_MAX_FLOWS)
return NULL;
ov->flows[free_slot].flow = flow;
ov->flows[free_slot].in_use = 1;
sp_scheduler_bucket_init(&ov->flows[free_slot].challenge, SP_CHALLENGE_FLOW_PER_S,
SP_CHALLENGE_FLOW_BURST, now_ns);
sp_scheduler_bucket_init(&ov->flows[free_slot].icmp, SP_ICMP_FLOW_PER_S,
SP_ICMP_FLOW_BURST, now_ns);
return &ov->flows[free_slot];
}
static void sp_scheduler_overload_retire_flow(struct sp_scheduler_overload *ov,
uint64_t flow) {
size_t i;
if (flow == 0)
return;
for (i = 0; i < SP_SCHEDULER_OVERLOAD_MAX_FLOWS; ++i) {
if (ov->flows[i].in_use && ov->flows[i].flow == flow) {
memset(&ov->flows[i], 0, sizeof(ov->flows[i]));
return;
}
}
}
/* Finds a queued current ACK for a flow so an equivalent one can coalesce onto
* it, retaining the newest. Returns the control FIFO position or SIZE_MAX. */
static size_t sp_scheduler_find_current_ack(struct sp_scheduler_sched *sched, uint64_t flow) {
size_t i;
for (i = 0; i < sched->control.count; ++i) {
struct sp_scheduler_record *record =
&sched->control.records[sched->control.order[i]];
if (record->flow == flow && record->tier == SP_SCHEDULER_CTRL_RESPONSE &&
!record->terminal)
return i;
}
return SIZE_MAX;
}
static enum sp_scheduler_ctrl_tier sp_scheduler_response_tier(enum sp_scheduler_response_kind kind) {
switch (kind) {
case SP_SCHEDULER_RESP_TERMINAL:
return SP_SCHEDULER_CTRL_TERMINAL;
case SP_SCHEDULER_RESP_CURRENT_ACK:
return SP_SCHEDULER_CTRL_RESPONSE;
case SP_SCHEDULER_RESP_CHALLENGE_ACK:
case SP_SCHEDULER_RESP_CLOSED_RST:
case SP_SCHEDULER_RESP_ICMP:
default:
return SP_SCHEDULER_CTRL_ORDINARY;
}
}
/* Classifies one outbound control response and either admits it to the
* scheduler, coalesces it, sheds it, or reports a terminal-enqueue fatal.
*
* `in_reply_to_rst` matters only for CLOSED resets: a reset is never emitted in
* answer to an incoming RST. On ADMIT the record is placed on the scheduler's
* control lane at the class-appropriate reserve tier. */
static enum sp_scheduler_shed_decision
sp_scheduler_overload_response_with_dependency_index(
struct sp_scheduler_overload *ov, struct sp_scheduler_sched *sched,
enum sp_scheduler_response_kind kind, uint64_t flow, uint64_t generation,
int in_reply_to_rst, const uint8_t *frame, size_t frame_len,
uint64_t seq_abs, uint64_t depends_on_seq, uint64_t now_ns,
uint16_t flow_index) {
struct sp_scheduler_overload_flow *ovflow;
struct sp_scheduler_record record;
unsigned blocked = 0;
enum sp_scheduler_ctrl_tier tier = sp_scheduler_response_tier(kind);
/* Terminals are never rate-limited or shed. Failure to enqueue one means
* ownership can never be released, which is a fatal invariant violation. */
if (kind == SP_SCHEDULER_RESP_TERMINAL) {
memset(&record, 0, sizeof(record));
record.flow = flow;
record.generation = generation;
record.flow_index = flow_index;
record.klass = SP_SCHEDULER_CLASS_CONTROL;
record.tier = SP_SCHEDULER_CTRL_TERMINAL;
record.terminal = 1;
record.seq_abs = seq_abs;
record.depends_on_seq = depends_on_seq;
record.logical_bytes = frame_len;
record.logical_packets = 1;
if (sp_scheduler_admit_with_flow_index(sched, &record, frame, frame_len,
&blocked, flow_index) < 0) {
sp_scheduler_sat_inc(&ov->fatal_terminal);
return SP_SCHEDULER_SHED_FATAL;
}
return SP_SCHEDULER_SHED_ADMIT;
}
/* A CLOSED reset is never a reply to a RST. */
if (kind == SP_SCHEDULER_RESP_CLOSED_RST && in_reply_to_rst) {
sp_scheduler_sat_inc(&ov->suppressed_rst_to_rst);
return SP_SCHEDULER_SHED_RST_TO_RST;
}
/* Equivalent current ACKs coalesce to one per flow per input batch and are
* not charged to the challenge bucket. */
if (kind == SP_SCHEDULER_RESP_CURRENT_ACK) {
size_t existing = sp_scheduler_find_current_ack(sched, flow);
if (existing != SIZE_MAX) {
struct sp_scheduler_record *slot =
&sched->control.records[sched->control.order[existing]];
/* Retain the newest: overwrite the queued frame in place. */
if (frame_len <= sched->control.stride) {
memcpy(sched->control.arena + slot->frame_offset, frame, frame_len);
sched->control.bytes -= slot->frame_len;
sched->control.bytes += frame_len;
slot->frame_len = frame_len;
slot->seq_abs = seq_abs;
}
sp_scheduler_sat_inc(&ov->coalesced_current_ack);
return SP_SCHEDULER_SHED_COALESCED;
}
}
/* Only challenge ACK and ICMP policy has a per-flow bucket. Current ACK and
* CLOSED RST traffic must not retain historical auxiliary tuple ids. */
ovflow = NULL;
if (kind == SP_SCHEDULER_RESP_CHALLENGE_ACK) {
ovflow = sp_scheduler_overload_flow(ov, flow, now_ns);
if (ovflow == NULL || !sp_scheduler_bucket_take(&ovflow->challenge, now_ns) ||
!sp_scheduler_bucket_take(&ov->challenge_all, now_ns)) {
sp_scheduler_sat_inc(&ov->shed_challenge);
return SP_SCHEDULER_SHED_DROPPED;
}
} else if (kind == SP_SCHEDULER_RESP_CLOSED_RST) {
if (!sp_scheduler_bucket_take(&ov->closed_rst_all, now_ns)) {
sp_scheduler_sat_inc(&ov->shed_closed_rst);
return SP_SCHEDULER_SHED_DROPPED;
}
} else if (kind == SP_SCHEDULER_RESP_ICMP) {
ovflow = sp_scheduler_overload_flow(ov, flow, now_ns);
if (ovflow == NULL || !sp_scheduler_bucket_take(&ovflow->icmp, now_ns) ||
!sp_scheduler_bucket_take(&ov->icmp_all, now_ns)) {
sp_scheduler_sat_inc(&ov->shed_icmp);
return SP_SCHEDULER_SHED_DROPPED;
}
}
memset(&record, 0, sizeof(record));
record.flow = flow;
record.generation = generation;
record.flow_index = flow_index;
record.klass = SP_SCHEDULER_CLASS_CONTROL;
record.tier = tier;
record.seq_abs = seq_abs;
record.logical_bytes = frame_len;
record.logical_packets = 1;
if (sp_scheduler_admit_with_flow_index(sched, &record, frame, frame_len, &blocked,
flow_index) < 0) {
/* Capacity exhausted: current ACKs are retained via their reserve where
* possible; everything else is shed. A shed here is never fatal. */
if (kind == SP_SCHEDULER_RESP_CURRENT_ACK)
sp_scheduler_sat_inc(&ov->shed_current_ack);
else if (kind == SP_SCHEDULER_RESP_CHALLENGE_ACK)
sp_scheduler_sat_inc(&ov->shed_challenge);
else if (kind == SP_SCHEDULER_RESP_CLOSED_RST)
sp_scheduler_sat_inc(&ov->shed_closed_rst);
else
sp_scheduler_sat_inc(&ov->shed_icmp);
return SP_SCHEDULER_SHED_DROPPED;
}
return SP_SCHEDULER_SHED_ADMIT;
}
static enum sp_scheduler_shed_decision sp_scheduler_overload_response_with_dependency(
struct sp_scheduler_overload *ov, struct sp_scheduler_sched *sched,
enum sp_scheduler_response_kind kind, uint64_t flow, uint64_t generation,
int in_reply_to_rst, const uint8_t *frame, size_t frame_len,
uint64_t seq_abs, uint64_t depends_on_seq, uint64_t now_ns) {
return sp_scheduler_overload_response_with_dependency_index(
ov, sched, kind, flow, generation, in_reply_to_rst, frame, frame_len,
seq_abs, depends_on_seq, now_ns, UINT16_MAX);
}
static uint16_t sp_icmp_reply_id;
static uint64_t sp_ns_now_ns(void);
/* Build the bounded ICMP destination-unreachable product used by UDP refusal
* and PMTU reporting. The quote is copied from an executor-validated value;
* no transition code parses mutable packet storage. */
SP_TEST_API static enum sp_codec_status
sp_build_icmp_control_reply(const struct sp_icmp_control_request *request,
uint16_t ipv4_id,
struct sp_scalar_packet *reply) {
struct sp_scalar_packet candidate;
if (request == NULL || reply == NULL)
return SP_CODEC_SHORT;
if (request->code != 3 && request->code != 4)
return SP_CODEC_PROTOCOL;
if ((request->code == 3 && request->mtu != 0) ||
(request->code == 4 && request->mtu == 0))
return SP_CODEC_LENGTH;
if (request->quote[0] != 0x45 || request->quote[9] != IPPROTO_UDP ||
sp_get_be16(request->quote + 2) <
SP_IPV4_HEADER_BYTES + SP_UDP_HEADER_BYTES ||
(sp_get_be16(request->quote + 6) & (IP_MF | IP_OFFMASK)) != 0)
return SP_CODEC_PROTOCOL;
memset(&candidate, 0, sizeof(candidate));
candidate.bytes[0] = 0x45;
sp_put_be16(candidate.bytes + 2, 56);
sp_put_be16(candidate.bytes + 4, ipv4_id);
candidate.bytes[8] = 64;
candidate.bytes[9] = IPPROTO_ICMP;
memcpy(candidate.bytes + 12, request->quote + 16, 4);
memcpy(candidate.bytes + 16, request->quote + 12, 4);
candidate.bytes[20] = 3;
candidate.bytes[21] = request->code;
if (request->code == 4)
sp_put_be16(candidate.bytes + 26, request->mtu);
memcpy(candidate.bytes + 28, request->quote,
SP_IPV4_HEADER_BYTES + SP_UDP_HEADER_BYTES);
sp_put_be16(candidate.bytes + 22,
sp_checksum_complete(candidate.bytes + 20, 36));
sp_rechecksum_ipv4(candidate.bytes);
candidate.len = 56;
*reply = candidate;
return SP_CODEC_OK;
}
/* Role classification consumes only the sealed policy value. Exact live-flow
* and DNS-mapping checks remain in their role handlers, but no CLI table or
* mutable runtime object is consulted here. */
static int sp_policy_child_destination_allowed(
const struct sp_net_policy_snapshot *policy,
const struct sp_packet_value *parsed) {
uint16_t port;
uint32_t destination_be = htonl(parsed->ip.dst);
if (policy == NULL || !policy->initialized)
return 0;
if (parsed->ip.protocol == IPPROTO_TCP)
port = parsed->tcp.dst_port;
else if (parsed->ip.protocol == IPPROTO_UDP)
port = parsed->udp.dst_port;
else
return 0;
if (destination_be == PUBLISH_SYNTHETIC_IP)
return 1;
if (is_gateway_ip(destination_be))
return sp_policy_gateway_allowed(policy, destination_be, port,
parsed->ip.protocol);
if (parsed->ip.protocol == IPPROTO_UDP && destination_be == DNS_PROXY_IP)
return policy->egress == EGRESS_SOCKS && policy->socks_remote_dns &&
port == 53;
if (policy->egress == EGRESS_NONE)
return 0;
if (policy->egress == EGRESS_DIRECT)
return sp_policy_direct_egress_allowed(policy, destination_be, port,
parsed->ip.protocol);
return parsed->ip.dst != 0 && parsed->ip.dst != UINT32_MAX &&
(parsed->ip.dst >> 24) != 0 && (parsed->ip.dst >> 24) != 127 &&
(parsed->ip.dst >> 28) != 0xe && (parsed->ip.dst >> 28) != 0xf;
}
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 int sp_publish_synthetic_port_in_use(uint16_t port);
/* Resolves a publish entry of the UDP table to its live flow, or NULL. */
static struct publish_udp_flow *publish_udp_flow_for_key(const struct sp_udp_key *key) {
struct sp_udp_entry *entry = sp_udp_table_lookup_entry(key);
struct publish_udp_flow *f;
if (entry == NULL || entry->value.role != SP_UDP_ROLE_PUBLISH ||
entry->value.index >= MAX_PUBLISH_UDP)
return NULL;
f = &publish_udp_flows[entry->value.index];
return f->used && f->generation == entry->generation ? f : NULL;
}
static struct publish_udp_flow *publish_udp_find(uint16_t synthetic_port,
uint16_t child_port) {
struct sp_udp_key key;
struct publish_udp_flow *f;
sp_udp_publish_child_key(&key, synthetic_port, child_port);
f = publish_udp_flow_for_key(&key);
if (f == NULL || f->core.role != SP_UDP_ROLE_PUBLISH ||
f->synthetic_port != synthetic_port || f->child_port != child_port)
return NULL;
return f;
}
static struct publish_udp_flow *publish_udp_find_host_peer(
int rule_index, uint32_t local_ip, uint16_t local_port,
const struct sockaddr_in *host_peer) {
struct sp_udp_key key;
struct publish_udp_flow *f;
sp_udp_publish_key(&key, rule_index, local_ip, local_port, host_peer);
f = publish_udp_flow_for_key(&key);
if (f == NULL || f->rule_index != rule_index || f->published_local_ip != local_ip ||
f->host_peer.sin_addr.s_addr != host_peer->sin_addr.s_addr ||
f->host_peer.sin_port != host_peer->sin_port)
return NULL;
return f;
}
static struct publish_udp_flow *publish_udp_alloc(void) {
for (int i = 0; i < MAX_PUBLISH_UDP; ++i) {
if (!publish_udp_flows[i].used) {
uint32_t generation =
epoll_next_generation(publish_udp_flows[i].generation);
memset(&publish_udp_flows[i], 0, sizeof(publish_udp_flows[i]));
publish_udp_flows[i].generation = generation;
publish_udp_flows[i].used = 1;
(void)sp_udp_runtime_step(&publish_udp_flows[i].core, SP_UDP_BEGIN, 0,
SP_UDP_ROLE_PUBLISH);
return &publish_udp_flows[i];
}
}
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
return NULL;
}
static void publish_udp_update_rule_events(int rule_index) {
struct publish_rule *rule;
uint32_t events = EPOLLIN | EPOLLERR | EPOLLHUP;
if (rule_index < 0 || rule_index >= publish_rule_count || g_epfd < 0)
return;
rule = &publish_rules[rule_index];
if (rule->proto != IPPROTO_UDP || rule->listen_fd < 0)
return;
for (size_t i = 0; i < MAX_PUBLISH_UDP; ++i) {
if (publish_udp_flows[i].used &&
publish_udp_flows[i].rule_index == rule_index &&
publish_udp_flows[i].core.pending_count != 0) {
events |= EPOLLOUT;
break;
}
}
{
struct epoll_event ev = {
.events = events,
.data.u64 = epoll_registration_token(
FD_PUBLISH_UDP_SOCKET, (size_t)rule_index, rule->listen_fd, 1)};
if (sp_epoll_update(EPOLL_CTL_MOD, rule->listen_fd, &ev,
&rule->interest) < 0) {
sp_epoll_update_failed("publish UDP listener");
g_broker_io_failed = 1;
}
}
}
static void publish_udp_close_flow(struct publish_udp_flow *f) {
struct sp_udp_key external_key;
struct sp_udp_key child_key;
uint32_t generation;
uint16_t local_port = 0;
if (f == NULL)
return;
generation = f->generation;
if (f->rule_index >= 0 && f->rule_index < publish_rule_count)
local_port = publish_rules[f->rule_index].host_port;
if (f->used) {
sp_udp_publish_key(&external_key, f->rule_index, f->published_local_ip,
local_port, &f->host_peer);
sp_udp_publish_child_key(&child_key, f->synthetic_port, f->child_port);
sp_udp_table_remove(&external_key, f->generation);
sp_udp_table_remove(&child_key, f->generation);
}
if (f->admission_held)
sp_resource_admission_release_source(
f->host_peer.sin_addr.s_addr,
sp_udp_admission_role(SP_UDP_ROLE_PUBLISH));
if (f->rule_admission_held)
sp_resource_admission_release_source((uint32_t)f->rule_index,
SP_UDP_ADMISSION_RULE);
if (f->global_admission_held)
sp_resource_admission_release_source(0, SP_UDP_ADMISSION_GLOBAL);
(void)sp_udp_runtime_step(&f->core, SP_UDP_CLOSE, 0, 0);
while (f->core.pending_count != 0) {
uint16_t slot = f->core.pending_slots[f->core.pending_head];
udp_pending_release(slot);
sp_udp_pop(&f->core);
}
{
int rule_index = f->rule_index;
memset(f, 0, sizeof(*f));
f->generation = generation;
f->rule_index = -1;
for (size_t i = 0; i < SP_UDP_PENDING_PER_FLOW; ++i)
f->core.pending_slots[i] = UINT16_MAX;
publish_udp_update_rule_events(rule_index);
}
}
/* Synthetic source port for a published flow (TCP and UDP share the pool). */
static uint16_t sp_ns_publish_port_alloc(void) {
unsigned int span = PUBLISH_SYNTH_PORT_MAX - PUBLISH_SYNTH_PORT_MIN + 1U;
for (unsigned int attempt = 0; attempt < SP_PORT_ALLOC_PROBES; ++attempt) {
uint64_t mixed =
(uint64_t)next_publish_synth_port++ +
(uint64_t)attempt * UINT64_C(0x9e3779b97f4a7c15) +
g_ns_aux_hash_key;
uint16_t port;
if (next_publish_synth_port > PUBLISH_SYNTH_PORT_MAX)
next_publish_synth_port = PUBLISH_SYNTH_PORT_MIN;
mixed ^= mixed >> 30;
mixed *= UINT64_C(0xbf58476d1ce4e5b9);
mixed ^= mixed >> 27;
mixed *= UINT64_C(0x94d049bb133111eb);
mixed ^= mixed >> 31;
port = (uint16_t)(PUBLISH_SYNTH_PORT_MIN + mixed % span);
if (!sp_publish_synthetic_port_in_use(port))
return port;
}
return 0;
}
/* 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 record[SP_VNET_HDR_BYTES + 40];
size_t record_len = sp_vnet_build_tcp_record(
record, sizeof(record), ntohl(saddr), ntohl(daddr), sport, dport, seq,
ack_seq, SP_TCP_FLAG_RST | SP_TCP_FLAG_ACK, NULL, 0, 0);
if (record_len == 0) {
DBG("failed to construct TCP RST");
return;
}
IGNORE_RESULT(tun_write_frame(tunfd, record, record_len, "TCP RST"));
}
struct sp_publish_turn {
size_t items;
size_t bytes;
uint64_t started_ns;
};
/* Published sockets are level-triggered. Returning at any one of these
* limits leaves readiness asserted, so epoll resumes the source in a later
* turn while TUN, timers, and other published rules get service. */
static int sp_publish_turn_exhausted(const struct sp_publish_turn *turn,
size_t item_limit, size_t byte_limit,
uint64_t now_ns) {
if (turn->items >= item_limit ||
(byte_limit != 0 && turn->bytes >= byte_limit))
return 1;
return turn->started_ns != 0 && now_ns >= turn->started_ns &&
now_ns - turn->started_ns >= (uint64_t)SP_CALLBACK_BUDGET_NS;
}
static void sp_publish_turn_account(struct sp_publish_turn *turn,
size_t bytes) {
if (turn->items != SIZE_MAX)
turn->items++;
if (SIZE_MAX - turn->bytes < bytes)
turn->bytes = SIZE_MAX;
else
turn->bytes += bytes;
}
static void handle_publish_tcp_listener(int tunfd, struct publish_rule *rule) {
struct sp_publish_turn turn = {.started_ns = sp_ns_now_ns()};
while (!sp_publish_turn_exhausted(
&turn, SP_PUBLISH_ACCEPTS_TURN, 0, sp_ns_now_ns())) {
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);
if (fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
return;
return;
}
sp_publish_turn_account(&turn, 0);
const char *option_reason = "unknown";
/* Already established from accept4(), so unlike the connect paths the
* SO_RCVBUF resize cannot influence the negotiated window scale. */
if (sp_scheduler_configure_host_tcp_socket(fd, &option_reason) < 0) {
DBG("accepted publish socket rejected: %s (%s)", option_reason,
strerror(errno));
close(fd);
continue;
}
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;
}
if (sp_ns_accept_publish(tunfd, rule, fd, &peer) < 0) {
close(fd);
return;
}
}
}
static int sp_publish_udp_mmsg_entry(unsigned int vector_index,
const struct publish_udp_flow *flow,
const struct sp_udp_pending_slot *pending,
uint16_t slot) {
struct msghdr *message = &g_udp_mmsg_tx_messages[vector_index].msg_hdr;
struct cmsghdr *cmsg;
struct in_pktinfo pktinfo;
if (vector_index >= SP_UDP_MMSG_MAX || slot >= SP_UDP_PENDING_ALL)
return -1;
g_udp_mmsg_tx_iov[vector_index].iov_base = udp_pending_data(slot);
g_udp_mmsg_tx_iov[vector_index].iov_len = pending->length;
g_udp_mmsg_tx_names[vector_index] = flow->host_peer;
message->msg_name = &g_udp_mmsg_tx_names[vector_index];
message->msg_namelen = sizeof(g_udp_mmsg_tx_names[vector_index]);
message->msg_control = g_udp_mmsg_tx_control[vector_index];
message->msg_controllen = CMSG_SPACE(sizeof(pktinfo));
memset(&pktinfo, 0, sizeof(pktinfo));
pktinfo.ipi_spec_dst.s_addr = flow->published_local_ip;
pktinfo.ipi_addr.s_addr = flow->published_local_ip;
cmsg = CMSG_FIRSTHDR(message);
if (cmsg == NULL)
return -1;
cmsg->cmsg_level = IPPROTO_IP;
cmsg->cmsg_type = IP_PKTINFO;
cmsg->cmsg_len = CMSG_LEN(sizeof(pktinfo));
memcpy(CMSG_DATA(cmsg), &pktinfo, sizeof(pktinfo));
return 0;
}
static void sp_publish_udp_mark_progress(struct publish_udp_flow *flow) {
uint64_t now_ns = sp_ns_now_ns();
(void)sp_udp_runtime_step(&flow->core, SP_UDP_PROGRESS, now_ns, 0);
}
static void flush_publish_udp_rule(struct publish_rule *rule) {
struct sp_publish_turn turn = {.started_ns = sp_ns_now_ns()};
while (!sp_publish_turn_exhausted(
&turn, SP_PUBLISH_DATAGRAMS_TURN, SP_PUBLISH_BYTES_TURN,
sp_ns_now_ns())) {
struct publish_udp_flow *owners[SP_UDP_MMSG_MAX];
uint16_t slots[SP_UDP_MMSG_MAX];
unsigned int count = 0;
size_t remaining = SP_PUBLISH_DATAGRAMS_TURN - turn.items;
unsigned int limit =
remaining > SP_UDP_MMSG_MAX ? SP_UDP_MMSG_MAX : (unsigned int)remaining;
int result;
int invariant_failed = 0;
sp_udp_mmsg_tx_reset(limit);
for (size_t i = 0; i < MAX_PUBLISH_UDP && count < limit; ++i) {
struct publish_udp_flow *flow = &publish_udp_flows[i];
/* Closing a flow releases its slots, so entries already gathered for
* it must leave the vector too. */
unsigned int flow_first = count;
if (!flow->used || flow->rule_index != rule->rule_index)
continue;
for (unsigned int q = 0; q < flow->core.pending_count && count < limit; ++q) {
size_t queue_index =
(flow->core.pending_head + q) % SP_UDP_PENDING_PER_FLOW;
uint16_t slot = flow->core.pending_slots[queue_index];
struct sp_udp_pending_slot *pending;
if (slot >= SP_UDP_PENDING_ALL) {
publish_udp_close_flow(flow);
count = flow_first;
break;
}
pending = &g_udp_pending_slots[slot];
if (!pending->used || !pending->owner_publish ||
pending->generation != flow->generation ||
pending->owner_index != (uint16_t)i ||
sp_publish_udp_mmsg_entry(count, flow, pending, slot) < 0) {
publish_udp_close_flow(flow);
count = flow_first;
break;
}
owners[count] = flow;
slots[count] = slot;
count++;
}
}
if (count == 0) {
publish_udp_update_rule_events(rule->rule_index);
return;
}
result =
sp_udp_sendmmsg_one_fd(rule->listen_fd, count, turn.started_ns);
if (result == 0) {
publish_udp_close_flow(owners[0]);
publish_udp_update_rule_events(rule->rule_index);
return;
}
if (result < 0) {
struct publish_udp_flow *flow = owners[0];
uint16_t slot = slots[0];
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
publish_udp_update_rule_events(rule->rule_index);
return;
}
if (errno == EMSGSIZE || errno == ECONNREFUSED || errno == ENOBUFS ||
errno == ENOMEM) {
if (flow->core.pending_count == 0 ||
flow->core.pending_slots[flow->core.pending_head] != slot) {
publish_udp_close_flow(flow);
return;
}
sp_counter_inc(&g_resource_counters.udp_terminal_drops);
udp_pending_release(slot);
sp_udp_pop(&flow->core);
continue;
}
publish_udp_close_flow(flow);
publish_udp_update_rule_events(rule->rule_index);
return;
}
for (int i = 0; i < result; ++i) {
struct publish_udp_flow *flow = owners[i];
uint16_t slot = slots[i];
size_t length = g_udp_pending_slots[slot].length;
if (!flow->used || flow->core.pending_count == 0 ||
flow->core.pending_slots[flow->core.pending_head] != slot) {
if (flow->used)
publish_udp_close_flow(flow);
invariant_failed = 1;
continue;
}
if (g_udp_mmsg_tx_messages[i].msg_len !=
g_udp_mmsg_tx_iov[i].iov_len)
invariant_failed = 1;
udp_pending_release(slot);
sp_udp_pop(&flow->core);
sp_publish_turn_account(&turn, length);
sp_publish_udp_mark_progress(flow);
}
if (invariant_failed || (unsigned int)result < count) {
publish_udp_update_rule_events(rule->rule_index);
return;
}
}
publish_udp_update_rule_events(rule->rule_index);
}
static int sp_publish_udp_pktinfo_valid(const struct msghdr *msg,
uint32_t expected_local_ip) {
unsigned int count = 0;
if (msg == NULL || (msg->msg_flags & (MSG_TRUNC | MSG_CTRUNC)) != 0)
return 0;
for (struct cmsghdr *cmsg = CMSG_FIRSTHDR((struct msghdr *)msg);
cmsg != NULL; cmsg = sp_cmsg_next((struct msghdr *)msg, cmsg)) {
struct in_pktinfo pktinfo;
if (cmsg->cmsg_level != IPPROTO_IP || cmsg->cmsg_type != IP_PKTINFO ||
cmsg->cmsg_len != CMSG_LEN(sizeof(pktinfo)))
return 0;
memcpy(&pktinfo, CMSG_DATA(cmsg), sizeof(pktinfo));
if (pktinfo.ipi_addr.s_addr != expected_local_ip)
return 0;
count++;
}
return count == 1;
}
static void handle_publish_udp_datagram(int tunfd, struct publish_rule *rule,
const struct sockaddr_in *peer,
const uint8_t *payload,
size_t payload_len) {
struct publish_udp_flow *f = publish_udp_find_host_peer(
rule->rule_index, rule->host_ip, rule->host_port, peer);
if (f == NULL) {
uint16_t synthetic_port = sp_ns_publish_port_alloc();
struct sp_udp_key external_key;
struct sp_udp_key child_key;
if (synthetic_port == 0)
return;
if (sp_udp_admit_source(peer->sin_addr.s_addr, SP_UDP_ROLE_PUBLISH) < 0) {
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
return;
}
if (sp_udp_admit_rule(rule->rule_index) < 0) {
sp_resource_admission_release_source(
peer->sin_addr.s_addr, sp_udp_admission_role(SP_UDP_ROLE_PUBLISH));
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
return;
}
if (sp_udp_admit_global() < 0) {
sp_resource_admission_release_source(
peer->sin_addr.s_addr, sp_udp_admission_role(SP_UDP_ROLE_PUBLISH));
sp_resource_admission_release_source((uint32_t)rule->rule_index,
SP_UDP_ADMISSION_RULE);
sp_counter_inc(&g_resource_counters.udp_flow_rejections);
return;
}
f = publish_udp_alloc();
if (f == NULL) {
sp_resource_admission_release_source(
peer->sin_addr.s_addr, sp_udp_admission_role(SP_UDP_ROLE_PUBLISH));
sp_resource_admission_release_source((uint32_t)rule->rule_index,
SP_UDP_ADMISSION_RULE);
sp_resource_admission_release_source(0, SP_UDP_ADMISSION_GLOBAL);
return;
}
f->admission_held = 1;
f->rule_admission_held = 1;
f->global_admission_held = 1;
f->rule_index = rule->rule_index;
f->host_peer = *peer;
f->published_local_ip = rule->host_ip;
f->synthetic_port = synthetic_port;
f->child_port = rule->container_port;
(void)sp_udp_runtime_step(&f->core, SP_UDP_READY, sp_ns_now_ns(), 0);
sp_udp_publish_key(&external_key, f->rule_index, f->published_local_ip,
rule->host_port, &f->host_peer);
sp_udp_publish_child_key(&child_key, f->synthetic_port, f->child_port);
if (sp_udp_table_insert(&external_key, f->generation,
(uint16_t)(f - publish_udp_flows), f->core.role) <
0 ||
sp_udp_table_insert(&child_key, f->generation,
(uint16_t)(f - publish_udp_flows), f->core.role) <
0) {
sp_udp_table_remove(&external_key, f->generation);
publish_udp_close_flow(f);
return;
}
}
{
uint64_t now_ns = sp_ns_now_ns();
(void)sp_udp_runtime_step(&f->core, SP_UDP_PROGRESS, now_ns, 0);
}
sp_udp_emit_tun(tunfd, ntohl(PUBLISH_SYNTHETIC_IP), ntohl(SANDBOX_IP),
f->synthetic_port, f->child_port, payload, payload_len,
"publish UDP inject");
}
static void handle_publish_udp_socket(int tunfd, struct publish_rule *rule) {
struct sp_publish_turn turn = {.started_ns = sp_ns_now_ns()};
while (!sp_publish_turn_exhausted(
&turn, SP_PUBLISH_DATAGRAMS_TURN, SP_PUBLISH_BYTES_TURN,
sp_ns_now_ns())) {
size_t remaining = SP_PUBLISH_DATAGRAMS_TURN - turn.items;
unsigned int requested =
remaining > SP_UDP_MMSG_MAX ? SP_UDP_MMSG_MAX : (unsigned int)remaining;
int received =
sp_udp_recvmmsg_one_fd(rule->listen_fd, requested, turn.started_ns);
if (received <= 0)
return;
for (int i = 0; i < received; ++i) {
struct msghdr *message = &g_udp_mmsg_messages[i].msg_hdr;
size_t payload_len = g_udp_mmsg_messages[i].msg_len;
sp_publish_turn_account(&turn, payload_len);
if (sp_udp_mmsg_slot_basic_valid((unsigned int)i) &&
payload_len <= SP_UDP_PAYLOAD_MAX &&
ipv4_is_loopback(g_udp_mmsg_sources[i].sin_addr.s_addr) &&
sp_publish_udp_pktinfo_valid(message, rule->host_ip))
handle_publish_udp_datagram(tunfd, rule, &g_udp_mmsg_sources[i],
g_udp_mmsg_payload[i], payload_len);
if (sp_publish_turn_exhausted(
&turn, SP_PUBLISH_DATAGRAMS_TURN, SP_PUBLISH_BYTES_TURN,
sp_ns_now_ns()))
return;
}
if ((unsigned int)received < requested)
return;
}
}
/* ==================================================================
* Core-driven TCP adapter (direct/SOCKS/gateway/publish lifecycle)
*
* This is the sole live TCP path. A direct-TCP connection's handshake runs
* through the pure sp_tcp_core: child SYN -> FLOW_ADMITTED (passive) -> OPEN_STREAM ->
* real backend connect() -> connect result -> SYN-ACK emitted through the scheduler ->
* child ACK -> ESTABLISHED. The egress/ingress executors execute the
* bidirectional data effects and the scheduler schedules child-facing
* control/data. SOCKS uses the same ownership; the
* published-listener adapter delegates child segments to its bounded publish
* table before ordinary tuple admission.
* ================================================================== */
struct sp_ns_cold_flow {
char socks_target_domain[256];
struct socks_io socks;
uint8_t socks_residual[sizeof(((struct socks_io *)0)->rxbuf)];
size_t socks_residual_len;
};
static struct sp_ns_cold_flow g_ns_cold_flows[MAX_TCP];
struct sp_ns_flow {
uint8_t in_use;
uint8_t established;
uint8_t has_pending_emit; /* a TUN emit was FIFO-blocked and awaits re-drive */
uint8_t has_sched_emit_result; /* executor result waits for scheduler service */
uint8_t sched_emit_written;
uint8_t sched_restart_software;
uint8_t egress_force_software;
uint8_t waits_sched_credit;
uint8_t use_socks;
uint8_t socks_residual_active;
uint8_t admission_role;
uint8_t admission_source_held;
uint8_t backend_epoll_registered;
struct epoll_event backend_interest;
uint8_t backend_read_idle; /* last observe made no progress; suppress EPOLLIN
* while flight is unconsumed to avoid a 100% CPU
* spin when the child stalls (dup-ACKs, no ACK
* advance). Cleared by a child ACK that consumes. */
uint32_t generation; /* epoll token generation, nonzero when live */
uint32_t admission_source_ip;
int backend_fd;
struct sp_ns_cold_flow *cold;
struct sp_tcp_core local_core;
int pump_close_graceful;
uint8_t challenge_ack;
struct sp_egress_executor_flow egress; /* backend -> child */
struct sp_ingress_flow ingress; /* child -> backend */
/* The exact TCP_EMIT_BATCH effect retained when the TUN output FIFO
* was full. The core holds its matching tun_emit_effect and will neither
* re-emit nor retransmit until this effect's result arrives, so the adapter
* must re-execute *this* effect on TUN writability rather than re-derive. */
struct sp_fx_tcp_emit_batch pending_emit;
struct sp_ev_tun_emit_result sched_emit_result;
uint64_t sched_next_seq;
uint64_t sched_last_data_seq;
uint64_t sched_terminal_seq;
unsigned sched_emit_outstanding;
unsigned sched_blocked_dims;
struct sp_scheduler_credit sched_blocked_credit;
uint64_t flow_id;
uint64_t core_gen;
uint64_t last_active_ns;
uint64_t connect_effect_id; /* OPEN_STREAM effect awaiting its result */
/* Child-facing addressing, network byte order as read from the TUN packet. */
uint32_t cli_ip;
uint32_t srv_ip;
uint16_t cli_port;
uint16_t srv_port;
struct sp_dns_ref dns_mapping_ref;
};
static struct sp_ns_flow g_ns_flows[MAX_TCP];
static uint64_t g_ns_output_work[(MAX_TCP + 63) / 64];
static void sp_ns_output_work_refresh(const struct sp_ns_flow *f) {
size_t slot = (size_t)(f - g_ns_flows);
uint64_t bit = UINT64_C(1) << (slot % 64);
if (f->in_use && (f->has_pending_emit || f->has_sched_emit_result))
g_ns_output_work[slot / 64] |= bit;
else
g_ns_output_work[slot / 64] &= ~bit;
}
/* Find the next occupied output-work slot without touching idle flow state. */
static size_t sp_ns_output_work_next(size_t start) {
size_t limit = sp_ns_tcp_slot_limit();
while (start < limit) {
uint64_t bits = g_ns_output_work[start / 64] &
(UINT64_MAX << (start % 64));
if (bits) {
size_t slot = (start / 64) * 64 + (size_t)__builtin_ctzll(bits);
return slot < limit ? slot : limit;
}
start = (start / 64 + 1) * 64;
}
return limit;
}
static uint16_t g_ns_free_next[MAX_TCP];
static uint16_t g_ns_free_head;
static void sp_ns_slots_init(void) {
memset(g_ns_output_work, 0, sizeof(g_ns_output_work));
size_t count = sp_ns_tcp_slot_limit();
g_ns_free_head = count ? 0 : UINT16_MAX;
for (size_t i = 0; i < count; ++i)
g_ns_free_next[i] = i + 1 < count ? (uint16_t)(i + 1) : UINT16_MAX;
}
/* storage must be freshly zero-filled (sp_maps_startup() maps it
* MAP_ANONYMOUS): the entry arrays are left untouched, so their pages are only
* faulted in when first used. */
static int sp_maps_init(struct sp_map_storage *storage,
const struct ht_hash_key secrets[4]) {
g_tcp_map = (struct sp_tcp_map){.secret = secrets[0], .flows = g_ns_flows};
g_udp_map = (struct sp_udp_map){.secret = secrets[1],
.entries = storage->udp_entries, .free_slots = storage->udp_free,
.free_count = SP_UDP_KEY_MAX};
for (uint32_t i = 0; i < SP_UDP_KEY_MAX; ++i)
storage->udp_free[i] = SP_UDP_KEY_MAX - 1U - i;
g_timewait_map = (struct sp_timewait_map){.secret = secrets[2],
.entries = storage->timewait_entries, .free_slots = storage->timewait_free,
.free_count = SP_TIMEWAIT_ALL_MAX};
for (uint32_t i = 0; i < SP_TIMEWAIT_ALL_MAX; ++i)
storage->timewait_free[i] = SP_TIMEWAIT_ALL_MAX - 1U - i;
g_admission_map = (struct sp_admission_map){.secret = secrets[3],
.entries = storage->admission_entries, .free_slots = storage->admission_free,
.free_count = SP_ADMISSION_KEY_MAX};
for (uint32_t i = 0; i < SP_ADMISSION_KEY_MAX; ++i)
storage->admission_free[i] = SP_ADMISSION_KEY_MAX - 1U - i;
g_ns_timewait_heap_count = 0;
return ht_init(&g_tcp_map.index, storage->tcp, SP_TCP_TABLE_SLOTS, MAX_TCP, MAX_TCP) &&
ht_init(&g_udp_map.index, storage->udp, SP_UDP_TABLE_SLOTS, SP_UDP_KEY_MAX, SP_UDP_KEY_MAX) &&
ht_init(&g_timewait_map.index, storage->timewait, SP_TIMEWAIT_TABLE_SLOTS, SP_TIMEWAIT_ALL_MAX, SP_TIMEWAIT_ALL_MAX) &&
ht_init(&g_admission_map.index, storage->admission, SP_ADMISSION_TABLE_SLOTS, SP_ADMISSION_KEY_MAX, SP_ADMISSION_KEY_MAX) ? 0 : -1;
}
static int sp_maps_startup(void) {
struct ht_hash_key secrets[4];
for (size_t i = 0; i < 4; ++i)
if (!ht_hash_key_random(&secrets[i])) return -1;
g_map_storage = mmap(NULL, sizeof(*g_map_storage), PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (g_map_storage == MAP_FAILED) { g_map_storage = NULL; return -1; }
if (sp_maps_init(g_map_storage, secrets) < 0) {
munmap(g_map_storage, sizeof(*g_map_storage));
g_map_storage = NULL;
return -1;
}
return 0;
}
static void sp_ns_timer_refresh(struct sp_ns_flow *f) {
unsigned owner = (unsigned)(f - g_ns_flows);
uint64_t deadline = sp_tcp_next_deadline_ns(&f->local_core, NULL);
if (!deadline && f->in_use)
deadline = sp_udp_deadline(f->last_active_ns,
(unsigned)(f->established ? TCP_IDLE_TIMEOUT_SEC : TCP_HALF_OPEN_TIMEOUT_SEC));
sp_deadline_set(&g_net_deadlines, owner, f->in_use ? deadline : 0);
}
static uint64_t g_ns_next_flow_id = 1;
static uint32_t g_ns_epoll_generation = 1;
/* Lets the effect executor reach the TUN writer without threading tunfd through
* every effect. Set for the duration of one dispatched packet or backend
* event. */
static struct sp_scheduler_sched g_ns_tun_sched;
static struct sp_scheduler_overload g_ns_overload;
/* Explicit authority for precisely one synchronous TCP_EMIT_BATCH callback. */
struct sp_ns_emit_transaction {
struct sp_ns_flow *flow;
const struct sp_fx_tcp_emit_batch *effect;
int tunfd;
};
static int g_ns_sched_servicing;
/* Process-local secret for scheduler-only auxiliary tuple identities. These
* ids are never TCP flow ids and are used only by the bounded per-source
* accounting/coalescing tables, so a child must not be able to precompute
* collisions that evict another tuple's bucket state. */
static void sp_resource_print_baseline(const char *point) {
uint64_t admission_refs = 0;
size_t tcp_flows = 0;
for (size_t i = 0; i < sp_ns_tcp_slot_limit(); ++i)
if (g_ns_flows[i].in_use)
tcp_flows++;
for (size_t i = 0; i < SP_ADMISSION_KEY_MAX; ++i) {
const struct sp_admission_entry *entry =
&g_admission_map.entries[i];
if (entry->live)
admission_refs += entry->refs;
}
(void)sp_stderr_printf(
"sp_runtime_baseline\t%s\tpid\t%ld\t"
"tcp_flows_current\t%zu\ttimewait_current\t%u\t"
"admission_refs_current\t%llu\treservation_current\t%llu\t"
"scheduler_control_current\t%zu\tscheduler_data_current\t%zu\t"
"tun_queue_current\t%zu\ttun_queue_high_water\t%llu\n",
point, (long)g_runtime_role_pid, tcp_flows,
g_ns_timewait_heap_count,
(unsigned long long)admission_refs,
(unsigned long long)g_resource_counters.reservation_current,
g_ns_tun_sched.control.count, g_ns_tun_sched.data.count,
g_tun_queue.count,
(unsigned long long)g_resource_counters.tun_queue_high_water);
}
/* Scheduler-owned responses without a live TCP flow (CLOSED RSTs and ICMP
* errors for UDP) need a stable identity for the overload policy's buckets/coalescing, but must
* never be mistaken for a reusable live-flow id. */
#define SP_NS_AUX_FLOW_TAG (UINT64_C(1) << 63)
static uint64_t sp_ns_aux_flow_id(uint32_t cli_ip, uint32_t srv_ip,
uint16_t cli_port, uint16_t srv_port,
uint8_t protocol) {
uint64_t hash = g_ns_aux_hash_key ^ UINT64_C(1469598103934665603);
const uint8_t bytes[] = {
(uint8_t)(cli_ip >> 24), (uint8_t)(cli_ip >> 16),
(uint8_t)(cli_ip >> 8), (uint8_t)cli_ip,
(uint8_t)(srv_ip >> 24), (uint8_t)(srv_ip >> 16),
(uint8_t)(srv_ip >> 8), (uint8_t)srv_ip,
(uint8_t)(cli_port >> 8), (uint8_t)cli_port,
(uint8_t)(srv_port >> 8), (uint8_t)srv_port, protocol,
};
size_t i;
for (i = 0; i < sizeof(bytes); ++i) {
hash ^= bytes[i];
hash *= UINT64_C(1099511628211);
}
return SP_NS_AUX_FLOW_TAG | (hash & ~SP_NS_AUX_FLOW_TAG);
}
static int sp_ns_is_aux_flow(uint64_t flow) {
return (flow & SP_NS_AUX_FLOW_TAG) != 0;
}
static uint64_t sp_ns_timewait_hash(uint32_t cli_ip, uint32_t srv_ip,
uint16_t cli_port, uint16_t srv_port) {
return sp_ns_aux_flow_id(ntohl(cli_ip), ntohl(srv_ip), cli_port, srv_port,
IPPROTO_TCP);
}
static int sp_ns_timewait_key_equal(const struct sp_ns_timewait_entry *entry,
uint32_t cli_ip, uint32_t srv_ip,
uint16_t cli_port, uint16_t srv_port) {
return entry->cli_ip == cli_ip && entry->srv_ip == srv_ip &&
entry->cli_port == cli_port && entry->srv_port == srv_port;
}
static void sp_resource_admission_release_source(uint32_t cli_ip, uint8_t role);
static void sp_ns_timewait_heap_add(struct sp_ns_timewait_entry *entry) {
unsigned owner = SP_DEADLINE_TIMEWAIT + (unsigned)(entry - g_timewait_map.entries);
if (!g_net_deadlines.positions[owner]) ++g_ns_timewait_heap_count;
sp_deadline_set(&g_net_deadlines, owner, entry->expiry_ns);
}
static void sp_ns_timewait_heap_detach(struct sp_ns_timewait_entry *entry) {
unsigned owner = SP_DEADLINE_TIMEWAIT + (unsigned)(entry - g_timewait_map.entries);
if (g_net_deadlines.positions[owner]) --g_ns_timewait_heap_count;
sp_deadline_set(&g_net_deadlines, owner, 0);
}
static void sp_ns_timewait_heap_refresh(struct sp_ns_timewait_entry *entry,
uint64_t expiry_ns) {
if (entry == NULL || entry->state != SP_NS_TIMEWAIT_LIVE)
return;
sp_ns_timewait_heap_detach(entry);
entry->expiry_ns = expiry_ns;
sp_ns_timewait_heap_add(entry);
}
static enum ht_step sp_timewait_map_probe(const struct sp_timewait_map *m,
const struct sp_timewait_key *key, struct ht_probe *p, uint32_t *slot,
uint32_t *bucket) {
*p = ht_probe_begin(&m->index, ht_siphash13(key, sizeof(*key), &m->secret));
enum ht_step step;
while ((step = ht_probe_next(&m->index, p, slot, bucket)) == HT_CANDIDATE)
if (sp_ns_timewait_key_equal(&m->entries[*slot], key->cli_ip, key->srv_ip,
key->cli_port, key->srv_port)) break;
if (step == HT_CORRUPT || step == HT_EXHAUSTED) abort();
return step;
}
static struct sp_ns_timewait_entry *
sp_ns_timewait_lookup(uint32_t cli_ip, uint32_t srv_ip, uint16_t cli_port,
uint16_t srv_port) {
const struct sp_timewait_key key = {cli_ip, srv_ip, cli_port, srv_port};
struct ht_probe p;
uint32_t slot, bucket;
return sp_timewait_map_probe(&g_timewait_map, &key, &p, &slot, &bucket) == HT_CANDIDATE
? &g_timewait_map.entries[slot] : NULL;
}
static int sp_ns_timewait_insert(uint32_t cli_ip, uint32_t srv_ip,
uint16_t cli_port, uint16_t srv_port,
uint64_t snd_nxt_abs, uint64_t rcv_acked_abs,
uint64_t rcv_adv_right_abs, uint64_t expiry_ns,
uint8_t role,
uint32_t admission_source_ip) {
struct sp_timewait_map *m = &g_timewait_map;
const struct sp_timewait_key key = {cli_ip, srv_ip, cli_port, srv_port};
struct ht_probe p;
uint32_t slot, bucket;
bool exists = sp_timewait_map_probe(m, &key, &p, &slot, &bucket) == HT_CANDIDATE;
if (!exists) {
if (!m->free_count) return -1;
slot = m->free_slots[m->free_count - 1];
}
struct sp_ns_timewait_entry *entry = &m->entries[slot];
uint16_t flags = exists ? entry->reserved : 0;
/* A refresh cannot transfer an admission reference to a different source. */
if (exists && (flags & SP_NS_TIMEWAIT_ADMISSION_REF) &&
(entry->role != role || entry->admission_source_ip != admission_source_ip))
return -1;
*entry = (struct sp_ns_timewait_entry){
.cli_ip = cli_ip, .srv_ip = srv_ip, .cli_port = cli_port, .srv_port = srv_port,
.snd_nxt_abs = snd_nxt_abs, .rcv_acked_abs = rcv_acked_abs,
.rcv_adv_right_abs = rcv_adv_right_abs, .expiry_ns = expiry_ns,
.state = SP_NS_TIMEWAIT_LIVE, .role = role, .reserved = flags,
.admission_source_ip = admission_source_ip};
if (!exists) {
if (ht_insert_at(&m->index, &p, bucket, slot) != HT_INSERTED) abort();
--m->free_count;
}
sp_ns_timewait_heap_add(entry);
return 0;
}
static void sp_ns_timewait_remove(struct sp_ns_timewait_entry *entry) {
if (entry == NULL || entry->state != SP_NS_TIMEWAIT_LIVE) return;
struct sp_timewait_map *m = &g_timewait_map;
const struct sp_timewait_key key = {entry->cli_ip, entry->srv_ip, entry->cli_port, entry->srv_port};
struct ht_probe p;
uint32_t slot, bucket;
if (sp_timewait_map_probe(m, &key, &p, &slot, &bucket) != HT_CANDIDATE ||
&m->entries[slot] != entry || ht_erase_at(&m->index, bucket) != slot) abort();
sp_ns_timewait_heap_detach(entry);
if (entry->reserved & SP_NS_TIMEWAIT_ADMISSION_REF)
sp_resource_admission_release_source(entry->admission_source_ip, entry->role);
entry->state = SP_NS_TIMEWAIT_EMPTY;
m->free_slots[m->free_count++] = slot;
}
static void sp_resource_admission_limits(const struct sp_admission_key *key, uint64_t *rate,
uint64_t *burst) {
if (key->bytes[4] == SP_UDP_ADMISSION_GLOBAL) {
*rate = SP_UDP_ADMIT_ALL_PER_S;
*burst = SP_UDP_ADMIT_ALL_BURST;
} else if (key->bytes[4] == SP_UDP_ADMISSION_RULE) {
*rate = SP_UDP_ADMIT_RULE_S;
*burst = SP_UDP_ADMIT_RULE_B;
} else if ((key->bytes[4] & 0x80U) != 0) {
*rate = SP_UDP_ADMIT_SOURCE_S;
*burst = SP_UDP_ADMIT_SOURCE_B;
} else {
*rate = MAX_CONNECTS_PER_SEC;
*burst = MAX_CONNECTS_PER_SEC;
}
}
static void sp_resource_admission_refill(struct sp_resource_admission_value *value,
uint64_t now_ns, uint64_t rate,
uint64_t burst) {
uint64_t max_allowance = burst * SP_SCHEDULER_TOKEN_NS;
uint64_t elapsed;
uint64_t add;
if (now_ns <= value->last_ns) {
value->last_ns = now_ns;
return;
}
elapsed = now_ns - value->last_ns;
value->last_ns = now_ns;
if (elapsed > max_allowance / rate + 1)
elapsed = max_allowance / rate + 1;
add = elapsed * rate;
if (max_allowance - value->allowance <= add)
value->allowance = max_allowance;
else
value->allowance += add;
}
static int sp_resource_admission_take(struct sp_admission_entry *entry,
uint64_t now_ns) {
uint64_t rate, burst;
if (entry == NULL || !entry->live) return 0;
sp_resource_admission_limits(&entry->key, &rate, &burst);
sp_resource_admission_refill(&entry->value, now_ns, rate, burst);
if (entry->value.allowance < SP_SCHEDULER_TOKEN_NS) return 0;
entry->value.allowance -= SP_SCHEDULER_TOKEN_NS;
return 1;
}
static enum ht_step sp_admission_map_probe(const struct sp_admission_map *m,
const struct sp_admission_key *key, struct ht_probe *p, uint32_t *slot,
uint32_t *bucket) {
*p = ht_probe_begin(&m->index, ht_siphash13(key->bytes, sizeof(key->bytes), &m->secret));
enum ht_step step;
while ((step = ht_probe_next(&m->index, p, slot, bucket)) == HT_CANDIDATE)
if (memcmp(m->entries[*slot].key.bytes, key->bytes, sizeof(key->bytes)) == 0) break;
if (step == HT_CORRUPT || step == HT_EXHAUSTED) abort();
return step;
}
static struct sp_admission_entry *sp_resource_admission_lookup(const struct sp_admission_key *key) {
struct ht_probe p;
uint32_t slot, bucket;
return sp_admission_map_probe(&g_admission_map, key, &p, &slot, &bucket) == HT_CANDIDATE
? &g_admission_map.entries[slot] : NULL;
}
static void sp_admission_reclaim(struct sp_admission_entry *entry) {
struct sp_admission_map *m = &g_admission_map;
struct ht_probe p;
uint32_t slot, bucket;
if (sp_admission_map_probe(m, &entry->key, &p, &slot, &bucket) != HT_CANDIDATE ||
&m->entries[slot] != entry || ht_erase_at(&m->index, bucket) != slot) abort();
entry->live = false;
m->free_slots[m->free_count++] = slot;
}
static struct sp_admission_entry *sp_resource_admission_acquire(const struct sp_admission_key *key) {
struct sp_admission_map *m = &g_admission_map;
struct ht_probe p;
uint32_t slot, bucket;
struct sp_admission_entry *entry;
if (sp_admission_map_probe(m, key, &p, &slot, &bucket) == HT_CANDIDATE) {
entry = &m->entries[slot];
if (entry->refs == UINT64_MAX) return NULL;
++entry->refs;
return entry;
}
uint64_t now_ns = sp_ns_now_ns(), rate, burst;
/* Reclaim only under pool pressure, before starting the insertion probe.
* Depleted zero-owner buckets retain their tokens: reconnect cannot reset a
* source limit. This bounded scan is at most 512 cold records. */
if (!m->free_count) {
for (uint32_t i = 0; i < SP_ADMISSION_KEY_MAX; ++i) {
entry = &m->entries[i];
if (!entry->live || entry->refs) continue;
sp_resource_admission_limits(&entry->key, &rate, &burst);
sp_resource_admission_refill(&entry->value, now_ns, rate, burst);
if (entry->value.allowance == burst * SP_SCHEDULER_TOKEN_NS) {
sp_admission_reclaim(entry);
break;
}
}
if (!m->free_count) return NULL;
/* Backshift deletion invalidated the original insertion position. */
if (sp_admission_map_probe(m, key, &p, &slot, &bucket) != HT_VACANT) abort();
}
slot = m->free_slots[m->free_count - 1];
entry = &m->entries[slot];
sp_resource_admission_limits(key, &rate, &burst);
*entry = (struct sp_admission_entry){.key = *key, .live = true, .refs = 1,
.value = {.allowance = burst * SP_SCHEDULER_TOKEN_NS, .last_ns = now_ns}};
if (ht_insert_at(&m->index, &p, bucket, slot) != HT_INSERTED) abort();
--m->free_count;
return entry;
}
static void sp_resource_admission_release(struct sp_admission_entry *entry) {
if (entry == NULL) return;
if (!entry->live || !entry->refs) abort();
if (--entry->refs) return;
uint64_t rate, burst;
sp_resource_admission_limits(&entry->key, &rate, &burst);
sp_resource_admission_refill(&entry->value, sp_ns_now_ns(), rate, burst);
if (entry->value.allowance == burst * SP_SCHEDULER_TOKEN_NS)
sp_admission_reclaim(entry);
}
static void sp_resource_admission_source_key(struct sp_admission_key *key,
uint32_t cli_ip, uint8_t role) {
memcpy(key->bytes, &cli_ip, sizeof(cli_ip));
key->bytes[4] = role;
}
static void sp_resource_admission_release_source(uint32_t cli_ip, uint8_t role) {
struct sp_admission_key key;
sp_resource_admission_source_key(&key, cli_ip, role);
sp_resource_admission_release(sp_resource_admission_lookup(&key));
}
/* One global child-to-backend staging pool (design: bounded global pool; the
* per-flow K_rx cap is enforced by ingress.staged) and its arena. */
static uint8_t g_ns_stage_arena[(size_t)SP_INGRESS_STAGE_SLOTS * SP_TUN_FRAME_MAX_BYTES];
static struct sp_ingress_stage_pool g_ns_stage_pool;
/* A reservation slot is assigned only while a flow owns retained child bytes.
* The global pool is one eighth of the former 128 x 256 KiB matrix. A flow is
* assigned at most one slot and only while it owns retained bytes, so one
* blocked flow cannot consume the pool. Published flows can acquire only the
* high eight slots; child-originated direct and SOCKS flows prefer the low
* eight and can acquire all sixteen. Slot pages remain demand-resident. */
static uint8_t
g_ns_reservation[SP_INGRESS_RESERVATION_SLOTS][SP_INGRESS_RESERVATION_SLOT_BYTES];
static uint64_t g_ns_reservation_owner[SP_INGRESS_RESERVATION_SLOTS];
static uint64_t g_ns_reservation_generation[SP_INGRESS_RESERVATION_SLOTS];
static uint8_t *sp_ns_reservation_acquire(struct sp_ingress_flow *flow,
uint8_t role) {
size_t i;
size_t first = role == SP_TCP_ROLE_PUBLISH
? SP_INGRESS_RESERVATION_SLOTS -
SP_INGRESS_PUBLISH_RESERVATION_SLOTS
: 0;
if (flow->reservation_slot != UINT16_MAX) {
i = flow->reservation_slot;
if (i < SP_INGRESS_RESERVATION_SLOTS &&
g_ns_reservation_owner[i] == flow->flow &&
g_ns_reservation_generation[i] == flow->generation)
return g_ns_reservation[i];
return NULL;
}
for (i = first; i < SP_INGRESS_RESERVATION_SLOTS; ++i) {
if (g_ns_reservation_owner[i] == 0) {
g_ns_reservation_owner[i] = flow->flow;
g_ns_reservation_generation[i] = flow->generation;
flow->reservation_slot = (uint16_t)i;
sp_counter_inc(&g_resource_counters.reservation_current);
if (g_resource_metrics_enabled &&
g_resource_counters.reservation_current >
g_resource_counters.reservation_high_water)
g_resource_counters.reservation_high_water =
g_resource_counters.reservation_current;
return g_ns_reservation[i];
}
}
sp_counter_inc(&g_resource_counters.reservation_acquire_failures);
return NULL;
}
static void sp_ns_reservation_release(struct sp_ingress_flow *flow) {
size_t slot;
if (flow->reservation_slot == UINT16_MAX) {
if (flow->pending_bytes != 0)
abort();
return;
}
slot = flow->reservation_slot;
if (flow->flow == 0 || flow->generation == 0 ||
slot >= SP_INGRESS_RESERVATION_SLOTS ||
g_ns_reservation_owner[slot] != flow->flow ||
g_ns_reservation_generation[slot] != flow->generation) {
if (flow->pending_bytes != 0)
abort();
flow->reservation_slot = UINT16_MAX;
return;
}
if (flow->pending_bytes > g_ns_stage_pool.global_reserved)
abort();
g_ns_stage_pool.global_reserved -= flow->pending_bytes;
flow->pending_bytes = 0;
flow->reserved_base = 0;
g_ns_reservation_owner[slot] = 0;
g_ns_reservation_generation[slot] = 0;
if (g_resource_metrics_enabled && g_resource_counters.reservation_current > 0)
g_resource_counters.reservation_current--;
flow->reservation_slot = UINT16_MAX;
}
/* Shared non-reentrant scratch for the egress peek/materialize/consume paths. */
static uint8_t g_ns_discard[SP_TCP_FLIGHT_PER_FLOW];
static uint8_t g_ns_payload[(size_t)SP_TCP_CORE_MAX_SEGMENTS * SP_TCP_CORE_MAX_MSS];
/* The egress write callback admits the exact framed batch to the scheduler. The executor result
* is held until scheduler service reaches the final TUN sink, so core send
* state never advances merely because a record was copied into a scheduler arena. */
static ssize_t sp_ns_tun_write(void *context, const uint8_t *record,
size_t len) {
struct sp_ns_emit_transaction *txn = context;
struct sp_ns_flow *flow;
const struct sp_fx_tcp_emit_batch *effect;
struct sp_scheduler_record queued;
unsigned blocked_dims;
size_t flow_index;
if (txn == NULL || txn->tunfd < 0 || txn->flow == NULL ||
txn->effect == NULL)
return -1;
flow = txn->flow;
effect = txn->effect;
flow_index = (size_t)(flow - g_ns_flows);
if (flow_index >= sp_ns_tcp_slot_limit() || flow_index > UINT16_MAX ||
!flow->in_use || flow->flow_id == 0 || flow->core_gen == 0)
return -1;
memset(&queued, 0, sizeof(queued));
queued.flow = flow->flow_id;
queued.generation = flow->core_gen;
queued.klass = SP_SCHEDULER_CLASS_DATA;
queued.tier = SP_SCHEDULER_CTRL_ORDINARY;
queued.seq_abs = flow->sched_next_seq + 1;
if (len <= SP_VNET_HDR_BYTES + 1500U) {
queued.logical_bytes = len > SP_VNET_HDR_BYTES + 40U
? len - SP_VNET_HDR_BYTES - 40U
: len;
queued.logical_packets = 1;
} else {
queued.logical_bytes = effect->end.value - effect->start.value;
queued.logical_packets =
(unsigned)((queued.logical_bytes + effect->mss - 1) / effect->mss);
}
if (sp_scheduler_admit_with_flow_index(
&g_ns_tun_sched, &queued, record, len, &blocked_dims,
(uint16_t)flow_index) < 0) {
if (blocked_dims == 0) {
errno = EINVAL;
return -1;
}
flow->sched_blocked_credit = sp_scheduler_credit_now(&g_ns_tun_sched);
flow->sched_blocked_dims = blocked_dims;
flow->waits_sched_credit = 1;
errno = EAGAIN;
return -1;
}
flow->waits_sched_credit = 0;
flow->sched_blocked_dims = 0;
flow->sched_next_seq = queued.seq_abs;
flow->sched_last_data_seq = queued.seq_abs;
flow->pending_emit = *effect;
flow->sched_emit_outstanding++;
g_ns_sched_tun_pending = 1;
tun_update_events(txn->tunfd);
return (ssize_t)len;
}
static uint64_t sp_ns_now_ns(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
return 0;
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
}
static uint64_t sp_ns_scheduler_clock(void *context) {
(void)context;
return sp_ns_now_ns();
}
static enum ht_step sp_tcp_map_probe(const struct sp_tcp_map *m,
const struct sp_tcp_key *key, struct ht_probe *p, uint32_t *slot, uint32_t *bucket) {
*p = ht_probe_begin(&m->index, ht_siphash13(key, sizeof(*key), &m->secret));
enum ht_step step;
while ((step = ht_probe_next(&m->index, p, slot, bucket)) == HT_CANDIDATE) {
const struct sp_ns_flow *f = &m->flows[*slot];
if (f->cli_ip == key->cli_ip && f->srv_ip == key->srv_ip &&
f->cli_port == key->cli_port && f->srv_port == key->srv_port) break;
}
if (step == HT_CORRUPT || step == HT_EXHAUSTED) abort();
return step;
}
static int sp_ns_tcp_table_insert(struct sp_ns_flow *f) {
const struct sp_tcp_key key = {f->cli_ip, f->srv_ip, f->cli_port, f->srv_port};
struct ht_probe p;
uint32_t slot, bucket;
if (sp_tcp_map_probe(&g_tcp_map, &key, &p, &slot, &bucket) == HT_CANDIDATE)
return -1;
return ht_insert_at(&g_tcp_map.index, &p, bucket, (uint32_t)(f - g_tcp_map.flows)) == HT_INSERTED ? 0 : -1;
}
static void sp_ns_tcp_table_remove(const struct sp_ns_flow *f) {
const struct sp_tcp_key key = {f->cli_ip, f->srv_ip, f->cli_port, f->srv_port};
struct ht_probe p;
uint32_t slot, bucket;
if (sp_tcp_map_probe(&g_tcp_map, &key, &p, &slot, &bucket) == HT_CANDIDATE &&
&g_tcp_map.flows[slot] == f)
if (ht_erase_at(&g_tcp_map.index, bucket) != slot) abort();
}
static struct sp_ns_flow *sp_ns_find(uint32_t cli_ip, uint16_t cli_port,
uint32_t srv_ip, uint16_t srv_port) {
const struct sp_tcp_key key = {cli_ip, srv_ip, cli_port, srv_port};
struct ht_probe p;
uint32_t slot, bucket;
return sp_tcp_map_probe(&g_tcp_map, &key, &p, &slot, &bucket) == HT_CANDIDATE
? &g_tcp_map.flows[slot] : NULL;
}
static struct sp_ns_timewait_entry *
sp_ns_timewait_find_live(uint32_t cli_ip, uint32_t srv_ip, uint16_t cli_port,
uint16_t srv_port, uint64_t now_ns) {
struct sp_ns_timewait_entry *entry =
sp_ns_timewait_lookup(cli_ip, srv_ip, cli_port, srv_port);
if (entry != NULL && entry->expiry_ns <= now_ns) {
sp_ns_timewait_remove(entry);
entry = NULL;
}
return entry;
}
static struct sp_ns_flow *sp_ns_alloc(void) {
uint16_t slot = g_ns_free_head;
if (slot == UINT16_MAX) return NULL;
g_ns_free_head = g_ns_free_next[slot];
return &g_ns_flows[slot];
}
static int sp_ns_tcp_admission_capacity(uint8_t role) {
size_t live = 0;
size_t role_live = 0;
size_t half_open = 0;
size_t role_half_open = 0;
size_t connecting = 0;
for (size_t i = 0; i < sp_ns_tcp_slot_limit(); ++i) {
const struct sp_ns_flow *flow = &g_ns_flows[i];
if (!flow->in_use)
continue;
live++;
if (flow->admission_role == role)
role_live++;
if (flow->local_core.state < SP_TCP_CORE_ESTABLISHED) {
half_open++;
if (flow->admission_role == role)
role_half_open++;
}
if (!flow->established)
connecting++;
}
if (live >= sp_ns_tcp_slot_limit() || half_open >= SP_TCP_HALF_OUT_MAX ||
connecting >= SP_CONNECT_INFLIGHT_MAX)
return 0;
if (role == SP_TCP_ROLE_DIRECT)
return role_live < SP_TCP_DIRECT_MAX &&
role_half_open < SP_TCP_HALF_DIRECT_MAX;
if (role == SP_TCP_ROLE_SOCKS)
return role_live < SP_TCP_SOCKS_MAX &&
role_half_open < SP_TCP_HALF_SOCKS_MAX;
return 0;
}
static int sp_ns_pump_start(struct sp_ns_flow *f, struct sp_pump_item initial,
uint64_t staged_id, int insert_on_complete);
static void sp_ns_flow_release(struct sp_ns_flow *f) {
unsigned slot = (unsigned)(f - g_ns_flows);
sp_deadline_set(&g_net_deadlines, slot, 0);
if (f->in_use) {
g_ns_free_next[slot] = g_ns_free_head;
g_ns_free_head = (uint16_t)slot;
}
sp_scheduler_retire_flow(&g_ns_tun_sched, f->flow_id, f->core_gen);
sp_scheduler_overload_retire_flow(&g_ns_overload, f->flow_id);
/* Dispose any child frames still staged for this flow before the generation
* is cleared, or they leak the global staging pool. */
(void)sp_ingress_stage_cancel_flow(&g_ns_stage_pool, &f->ingress);
sp_ns_reservation_release(&f->ingress);
if (f->in_use)
sp_ns_tcp_table_remove(f);
if (f->backend_fd >= 0) {
if (f->backend_epoll_registered)
epoll_del(f->backend_fd);
close(f->backend_fd);
}
if (f->in_use && f->admission_source_held)
sp_resource_admission_release_source(f->admission_source_ip,
f->admission_role);
if (f->dns_mapping_ref.generation != 0)
(void)sp_dns_core_release(&g_dns_core, f->dns_mapping_ref,
sp_ns_now_ns());
memset(f, 0, sizeof(*f));
f->backend_fd = -1;
sp_ns_output_work_refresh(f);
}
static void sp_ns_release_all_flows(void) {
for (size_t i = 0; i < sp_ns_tcp_slot_limit(); ++i)
if (g_ns_flows[i].in_use)
sp_ns_flow_release(&g_ns_flows[i]);
}
/* Move a fully closed active-side core into the no-fd TIME-WAIT table only
* after its final control effects have reached the scheduler sink. If the
* bounded table cannot admit this tuple, retain the live flow: capacity pressure
* may reduce new admissions but must never weaken tuple quarantine. */
static int sp_ns_park_timewait(struct sp_ns_flow *f) {
if (f == NULL || !f->in_use || f->local_core.state != SP_TCP_CORE_TIME_WAIT ||
f->local_core.timewait_deadline_ns == 0)
return 0;
if (f->has_pending_emit || f->has_sched_emit_result ||
f->sched_emit_outstanding != 0)
return 0;
if (sp_ns_timewait_insert(
f->cli_ip, f->srv_ip, f->cli_port, f->srv_port,
f->local_core.snd_nxt_high_abs, f->local_core.rcv_acked_abs,
f->local_core.rcv_adv_right_abs, f->local_core.timewait_deadline_ns,
f->admission_role,
f->admission_source_ip) < 0)
return -1;
{
struct sp_admission_key key;
sp_resource_admission_source_key(&key, f->admission_source_ip,
f->admission_role);
if (sp_resource_admission_acquire(&key) == NULL) {
sp_ns_timewait_remove(
sp_ns_timewait_lookup(f->cli_ip, f->srv_ip, f->cli_port,
f->srv_port));
return -1;
}
{
struct sp_ns_timewait_entry *entry =
sp_ns_timewait_lookup(f->cli_ip, f->srv_ip, f->cli_port,
f->srv_port);
if (entry == NULL) {
sp_resource_admission_release_source(f->admission_source_ip,
f->admission_role);
return -1;
}
entry->reserved |= SP_NS_TIMEWAIT_ADMISSION_REF;
}
}
sp_ns_flow_release(f);
return 1;
}
static void sp_ns_maybe_park_timewait(struct sp_ns_flow *f) {
if (f != NULL && f->in_use && f->local_core.state == SP_TCP_CORE_TIME_WAIT)
(void)sp_ns_park_timewait(f);
}
/* Records the capability of the last STREAM_SEND the pump authorized this call,
* so the segment path can free a staged frame the core did not authorize (a
* duplicate or out-of-window retransmit) rather than orphan its slot. */
/* Count of staged child frames freed because the core did not authorize them
* (duplicate/out-of-window retransmits); a nonzero value under load is evidence
* the K_rx orphan-disposal path is exercised and not leaking. */
static uint64_t g_ns_orphans_freed = 0;
/* Set by the pump when the CLOSE_FLOW it processed was a graceful mutual-FIN
* close (so the teardown releases quietly instead of resetting the child). */
static void sp_ns_reset_child(struct sp_ns_flow *f);
static void sp_ns_teardown(struct sp_ns_flow *f);
static void sp_ns_service_tun_scheduler(int tunfd);
static void sp_ns_retry_pending_emit(int tunfd, struct sp_ns_flow *f);
static int sp_ns_handle_timewait(int tunfd,
struct sp_ns_timewait_entry *entry,
uint32_t seq, uint8_t flags,
size_t payload_len, uint64_t now_ns);
static int sp_ns_admit_aux_response(enum sp_scheduler_response_kind kind,
uint64_t flow, int in_reply_to_rst,
const uint8_t *record, size_t record_len,
uint64_t now_ns) {
enum sp_scheduler_shed_decision decision;
decision = sp_scheduler_overload_response_with_dependency(
&g_ns_overload, &g_ns_tun_sched, kind, flow, 1, in_reply_to_rst, record,
record_len, 1, 0, now_ns);
if (decision == SP_SCHEDULER_SHED_FATAL)
return -1;
if (decision == SP_SCHEDULER_SHED_ADMIT || decision == SP_SCHEDULER_SHED_COALESCED) {
g_ns_sched_tun_pending = 1;
tun_update_events(g_ns_tunfd_for_emit);
}
return 0;
}
static int sp_ns_emit_child_record(struct sp_ns_flow *f,
const struct sp_fx_tcp_emit_control *effect,
int challenge_ack) {
uint8_t record[SP_VNET_HDR_BYTES + 40];
enum sp_scheduler_response_kind kind;
enum sp_scheduler_shed_decision decision;
uint64_t seq_abs = f->sched_next_seq + 1;
uint64_t depends_on_seq;
uint64_t now_ns;
size_t flow_index;
size_t record_len = sp_vnet_build_tcp_record(
record, sizeof(record), ntohl(f->srv_ip), ntohl(f->cli_ip), f->srv_port,
f->cli_port, (uint32_t)effect->seq.value, (uint32_t)effect->ack.value,
(uint8_t)effect->flags, NULL, 0, 0);
if (record_len == 0) {
DBG("failed to build child TCP control record");
return -1;
}
flow_index = (size_t)(f - g_ns_flows);
if (flow_index >= sp_ns_tcp_slot_limit() || flow_index > UINT16_MAX ||
!f->in_use || f->flow_id == 0 || f->core_gen == 0)
return -1;
if (effect->terminal &&
(f->has_sched_emit_result || f->sched_terminal_seq != 0 ||
f->egress.next_control_seq == UINT64_MAX)) {
DBG("terminal control completion already pending");
return -1;
}
kind = ((effect->flags &
(SP_TCP_CORE_SYN | SP_TCP_CORE_FIN | SP_TCP_CORE_RST)) != 0 ||
effect->terminal)
? SP_SCHEDULER_RESP_TERMINAL
: SP_SCHEDULER_RESP_CURRENT_ACK;
if (kind == SP_SCHEDULER_RESP_CURRENT_ACK && challenge_ack)
kind = SP_SCHEDULER_RESP_CHALLENGE_ACK;
depends_on_seq = kind == SP_SCHEDULER_RESP_TERMINAL ? f->sched_last_data_seq : 0;
now_ns = sp_ns_now_ns();
decision = sp_scheduler_overload_response_with_dependency_index(
&g_ns_overload, &g_ns_tun_sched, kind, f->flow_id, f->core_gen, 0,
record, record_len, seq_abs, depends_on_seq, now_ns,
(uint16_t)flow_index);
if (decision == SP_SCHEDULER_SHED_FATAL) {
DBG("terminal control scheduler admission failed");
return -1;
}
if (decision == SP_SCHEDULER_SHED_ADMIT || decision == SP_SCHEDULER_SHED_COALESCED) {
f->sched_next_seq = seq_abs;
/* A FIN effect occupies the core's one live TUN-emission slot just like a
* data batch. Hold its zero-byte success until the scheduler reaches the final TUN
* sink; otherwise the slot is never retired and every later RTO is
* suppressed behind the permanently-live FIN. SYN/RST/current-ACK
* controls do not own tun_emit_effect and therefore need no core result. */
if (effect->terminal) {
memset(&f->sched_emit_result, 0, sizeof(f->sched_emit_result));
f->sched_emit_result.context.key = sp_flow_key_make(
(sp_flow_id){.value = f->flow_id},
(sp_generation_id){.value = f->core_gen});
f->sched_emit_result.context.now.value = now_ns;
f->sched_emit_result.context.control_sequence.value =
++f->egress.next_control_seq;
f->sched_emit_result.effect = effect->context.effect;
f->sched_emit_result.outcome = SP_EMIT_ACCEPTED;
f->sched_terminal_seq = seq_abs;
f->has_sched_emit_result = 1;
sp_ns_output_work_refresh(f);
}
g_ns_sched_tun_pending = 1;
tun_update_events(g_ns_tunfd_for_emit);
}
return 0;
}
static int sp_ns_open_backend(struct sp_ns_flow *f,
const struct sp_fx_open_stream *effect) {
struct sockaddr_in addr;
int fd;
int rc;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
if (is_gateway_ip(f->srv_ip)) {
addr.sin_addr.s_addr = gateway_to_localhost(f->srv_ip);
addr.sin_port = htons(f->srv_port);
} else if (f->use_socks) {
addr = socks_proxy.addr;
} else {
addr.sin_addr.s_addr = f->srv_ip;
addr.sin_port = htons(f->srv_port);
}
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0)
return -1;
{
const char *option_reason = NULL;
if (sp_scheduler_configure_host_tcp_socket(fd, &option_reason) < 0) {
close(fd);
return -1;
}
}
rc = start_nonblocking_connect(fd, &addr);
if (rc < 0) {
close(fd);
return -1;
}
f->backend_fd = fd;
f->connect_effect_id = effect->context.effect.value;
if (f->use_socks) {
socks_io_reset(&f->cold->socks);
f->cold->socks.is_udp = 0;
f->cold->socks.target_ip = f->srv_ip;
f->cold->socks.target_port = f->srv_port;
snprintf(f->cold->socks.target_domain, sizeof(f->cold->socks.target_domain), "%s",
f->cold->socks_target_domain);
f->cold->socks.connect_pending = rc > 0;
f->cold->socks.state = f->cold->socks.connect_pending ? SOCKS_IO_CONNECTING
: SOCKS_IO_METHOD;
if (!f->cold->socks.connect_pending &&
socks_begin_handshake(&f->cold->socks, &socks_proxy) < 0) {
close(fd);
f->backend_fd = -1;
return -1;
}
}
f->generation = g_ns_epoll_generation;
g_ns_epoll_generation = epoll_next_generation(g_ns_epoll_generation);
{
size_t index = (size_t)(f - g_ns_flows);
struct epoll_event ev = {
.events = EPOLLOUT | EPOLLRDHUP | EPOLLERR | EPOLLHUP,
.data.u64 = epoll_registration_token(FD_TCP_CORE, index, fd,
f->generation)};
if (g_epfd >= 0) {
if (sp_epoll_update(EPOLL_CTL_ADD, fd, &ev, &f->backend_interest) < 0) {
close(fd);
f->backend_fd = -1;
return -1;
}
f->backend_epoll_registered = 1;
}
}
return 0;
}
SP_TEST_API static int sp_ns_backend_wants_read(const struct sp_ns_flow *f) {
uint64_t represented;
if (f->use_socks && !f->established)
return 1;
if (f->socks_residual_active || f->egress.eof_reported)
return 0;
represented = f->egress.stream_tail >= f->egress.stream_consumed
? f->egress.stream_tail - f->egress.stream_consumed
: 0;
return !(f->backend_read_idle && represented > 0);
}
SP_TEST_API static uint32_t
sp_ns_backend_observation_events(const struct sp_ns_flow *f) {
uint32_t events = EPOLLERR;
if (f == NULL || !f->egress.eof_reported)
events |= EPOLLRDHUP | EPOLLHUP;
return events;
}
SP_TEST_API static int
sp_ns_backend_should_unregister(const struct sp_ns_flow *f) {
return f != NULL && f->egress.eof_reported;
}
/* Updates backend epoll interest: readable while new stream input is possible,
* writable only while an ingress reservation is pending re-flush. Returns -1
* when the kernel registration could not be updated; the caller must then
* reset the flow (F2). */
static int sp_ns_update_backend_interest(struct sp_ns_flow *f) {
uint32_t want = sp_ns_backend_observation_events(f);
struct epoll_event ev;
size_t index = (size_t)(f - g_ns_flows);
if (f->backend_fd < 0 || g_epfd < 0)
return 0;
/* EPOLLHUP and EPOLLERR are reported even when omitted from the requested
* mask. After EOF reaches the core, leave the fd open for authorized
* consume effects but remove it from epoll so terminal readiness cannot
* spin the level-triggered loop. */
if (sp_ns_backend_should_unregister(f)) {
if (f->backend_epoll_registered) {
epoll_del(f->backend_fd);
f->backend_epoll_registered = 0;
f->backend_interest = (struct epoll_event){0};
}
return 0;
}
if (!f->backend_epoll_registered)
return 0;
/* Watch backend readability unless a prior observe made no progress *and* we
* are still holding unconsumed flight (peeked bytes the child has not ACKed).
* In that state level-triggered EPOLLIN re-fires on the same peeked data with
* nothing new to read, spinning the loop at 100% CPU; a child ACK that
* consumes flight clears backend_read_idle and re-arms EPOLLIN through the
* pump. When represented is 0 (nothing in flight) EPOLLIN stays armed until
* EOF is reported; a closed backend must never be re-armed because its
* level-triggered EOF readiness would otherwise spin the broker forever. */
if (sp_ns_backend_wants_read(f))
want |= EPOLLIN;
if (f->connect_effect_id != 0 || f->ingress.pending_bytes > 0 ||
(f->use_socks && socks_has_pending_tx(&f->cold->socks)))
want |= EPOLLOUT;
ev.events = want;
ev.data.u64 =
epoll_registration_token(FD_TCP_CORE, index, f->backend_fd, f->generation);
if (sp_epoll_update(EPOLL_CTL_MOD, f->backend_fd, &ev,
&f->backend_interest) < 0) {
sp_epoll_update_failed("TCP backend");
return -1;
}
return 0;
}
/* The live-adapter effect pump. Runs the core on an event, executes each emitted
* effect through the egress/ingress executors, and feeds every executor-produced result
* back as a NEW event on a bounded work queue, iterating to fixpoint. Executor
* results are internal events: a core rejection of one is a lockstep bug and is
* fatal, whereas rejection of an external child segment is a normal drop.
* Returns 0 to keep the flow, -1 to release it. */
static struct sp_pump_item
sp_pump_item_admission(const struct sp_ev_flow_admitted *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_ADMISSION, .now = input->now.value,
.value.admission = *input};
}
static struct sp_pump_item
sp_pump_item_segment(const struct sp_ev_tcp_segment *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_SEGMENT, .now = input->context.now.value,
.value.segment = *input};
}
static struct sp_pump_item
sp_pump_item_connect_result(const struct sp_ev_connect_result *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_CONNECT_RESULT, .now = input->context.now.value,
.value.connect_result = *input};
}
static struct sp_pump_item
sp_pump_item_stream_send_result(const struct sp_ev_stream_send_result *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_STREAM_SEND_RESULT, .now = input->context.now.value,
.value.stream_send_result = *input};
}
static struct sp_pump_item
sp_pump_item_stream_consumed(const struct sp_ev_stream_consumed *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_STREAM_CONSUMED, .now = input->context.now.value,
.value.stream_consumed = *input};
}
static struct sp_pump_item
sp_pump_item_stream_eof(const struct sp_ev_stream_eof *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_STREAM_EOF, .now = input->context.now.value,
.value.stream_eof = *input};
}
static struct sp_pump_item
sp_pump_item_tun_emit_result(const struct sp_ev_tun_emit_result *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_TUN_EMIT_RESULT, .now = input->context.now.value,
.value.tun_emit_result = *input};
}
static struct sp_pump_item
sp_pump_item_credit(const struct sp_ev_credit *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_CREDIT, .now = input->context.now.value,
.value.credit = *input};
}
static struct sp_pump_item
sp_pump_item_timer_fired(const struct sp_ev_timer_fired *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_TIMER_FIRED, .now = input->context.now.value,
.value.timer_fired = *input};
}
static struct sp_pump_item
sp_pump_item_ingress_disposed(const struct sp_ev_ingress_disposed *input) {
return (struct sp_pump_item){.kind = SP_PUMP_ITEM_INGRESS_DISPOSED, .now = input->context.now.value,
.value.ingress_disposed = *input};
}
/* Exhaustiveness is a local security property for the only trusted runtime
* event discriminant. Make enum growth a build error here without imposing
* -Wswitch-enum on unrelated legacy protocol switches. */
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic error "-Wswitch"
#pragma GCC diagnostic error "-Wswitch-enum"
#endif
static enum sp_tcp_core_result sp_pump_item_dispatch(
struct sp_tcp_core *core, const struct sp_pump_item *item,
struct sp_typed_effect_list *effects) {
switch (item->kind) {
case SP_PUMP_ITEM_ADMISSION:
return sp_tcp_core_admit(core, &item->value.admission, effects);
case SP_PUMP_ITEM_SEGMENT:
return sp_tcp_core_segment(core, &item->value.segment, effects);
case SP_PUMP_ITEM_CONNECT_RESULT:
return sp_tcp_core_connect_result(core, &item->value.connect_result,
effects);
case SP_PUMP_ITEM_STREAM_SEND_RESULT:
return sp_tcp_core_stream_send_result(
core, &item->value.stream_send_result, effects);
case SP_PUMP_ITEM_STREAM_CONSUMED:
return sp_tcp_core_stream_consumed(core, &item->value.stream_consumed,
effects);
case SP_PUMP_ITEM_STREAM_EOF:
return sp_tcp_core_stream_eof(core, &item->value.stream_eof, effects);
case SP_PUMP_ITEM_TUN_EMIT_RESULT:
return sp_tcp_core_tun_emit_result(core, &item->value.tun_emit_result,
effects);
case SP_PUMP_ITEM_CREDIT:
return sp_tcp_core_credit(core, &item->value.credit, effects);
case SP_PUMP_ITEM_TIMER_FIRED:
return sp_tcp_core_timer_fired(core, &item->value.timer_fired, effects);
case SP_PUMP_ITEM_INGRESS_DISPOSED:
return sp_tcp_core_ingress_disposed(core, &item->value.ingress_disposed,
effects);
}
return SP_TCP_CORE_BAD_EVENT;
}
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
static enum sp_tcp_core_result sp_ns_core_process(
struct sp_ns_flow *f, const struct sp_pump_item *item,
struct sp_typed_effect_list *effects) {
enum sp_tcp_core_result result;
f->challenge_ack = (uint8_t)(item->kind == SP_PUMP_ITEM_SEGMENT &&
sp_ns_segment_needs_challenge_ack(&f->local_core, &item->value.segment));
result = sp_pump_item_dispatch(&f->local_core, item, effects);
sp_ns_timer_refresh(f);
return result;
}
static void sp_ns_local_core_init(struct sp_ns_flow *f) {
sp_tcp_core_init(&f->local_core);
}
/* Publish and child-originated TCP flows share the same local state-machine
* and executor ownership. Admission role, tuple, and SOCKS negotiation stay
* at their call sites; this helper only establishes the common adapters after
* flow_id/core_gen have been assigned. */
static void sp_ns_tcp_adapters_init(struct sp_ns_flow *f) {
sp_ns_local_core_init(f);
memset(&f->egress, 0, sizeof(f->egress));
f->egress.flow = f->flow_id;
f->egress.generation = f->core_gen;
f->egress.tcp_backend = 1;
sp_ingress_flow_init(&f->ingress, f->flow_id, f->core_gen);
}
static int sp_ns_pump_run(struct sp_ns_flow *f, struct sp_ns_pump *ctx,
uint64_t initial_staged_id);
static int sp_ns_publish_socks_residual(struct sp_ns_flow *f,
uint64_t now_ns);
enum {
SP_NS_PUMP_FAILED = -1,
SP_NS_PUMP_COMPLETE = 0,
};
static int sp_ns_pump_start(struct sp_ns_flow *f, struct sp_pump_item initial,
uint64_t staged_id, int insert_on_complete) {
struct sp_ns_pump *ctx = &g_ns_pump;
int tunfd = g_ns_tunfd_for_emit;
int rc;
if (ctx->owner != NULL) {
sp_counter_inc(&g_resource_counters.pump_reentry_failures);
return SP_NS_PUMP_FAILED;
}
ctx->owner = f;
ctx->tunfd = tunfd;
ctx->queue[0] = initial;
ctx->count = 1;
f->pump_close_graceful = 0;
rc = sp_ns_pump_run(f, ctx, staged_id);
ctx->owner = NULL;
ctx->count = 0;
ctx->tunfd = -1;
if (rc == SP_NS_PUMP_COMPLETE && insert_on_complete &&
sp_ns_tcp_table_insert(f) < 0)
return SP_NS_PUMP_FAILED;
if (rc == SP_NS_PUMP_COMPLETE && !g_ns_sched_servicing && tunfd >= 0)
sp_ns_service_tun_scheduler(tunfd);
if (rc == SP_NS_PUMP_COMPLETE &&
sp_ns_publish_socks_residual(f, sp_ns_now_ns()) < 0)
return SP_NS_PUMP_FAILED;
return rc;
}
static int sp_ns_pump_run(struct sp_ns_flow *f, struct sp_ns_pump *ctx,
uint64_t initial_staged_id) {
size_t qhead = 0;
/* Capture the TUN execution descriptor for this bounded drain. */
g_ns_tunfd_for_emit = ctx->tunfd;
int iters = 0;
int release = 0;
while (ctx->count > 0 &&
iters < SP_NS_PUMP_ITERS) {
struct sp_pump_item item;
int external;
struct sp_typed_effect_list effects;
struct sp_pump_item produced_items[SP_TCP_CORE_MAX_EFFECTS];
size_t produced_count = 0;
enum sp_tcp_core_result core_rc = SP_TCP_CORE_OK;
uint64_t staged_id;
int authorized_staged = 0;
size_t i;
item = ctx->queue[qhead];
memset(&ctx->queue[qhead], 0, sizeof(ctx->queue[qhead]));
staged_id = initial_staged_id;
initial_staged_id = 0;
external = item.kind == SP_PUMP_ITEM_ADMISSION ||
item.kind == SP_PUMP_ITEM_SEGMENT;
qhead = (qhead + 1) % SP_NS_QUEUE_MAX;
ctx->count--;
/* Bound the immediate completion chain for this input. */
iters++;
core_rc = sp_ns_core_process(f, &item, &effects);
if (core_rc != SP_TCP_CORE_OK) {
if (external) {
if (staged_id != 0) {
struct sp_ingress_stage_slot *slot = sp_ingress_find_slot(
&g_ns_stage_pool, f->flow_id, f->core_gen, staged_id);
if (slot != NULL)
sp_ingress_stage_free(&f->ingress, slot);
}
continue; /* a rejected child segment is a normal drop */
}
/* The broker built this item itself; the core finding it malformed
* means broker and core state disagree (a lockstep bug). */
assert(core_rc != SP_TCP_CORE_BAD_EVENT);
DBG("internal item %d rejected", (int)item.kind);
return SP_NS_PUMP_FAILED;
}
for (i = 0; i < effects.count; ++i) {
const struct sp_typed_effect *effect = &effects.items[i];
struct sp_ev_stream_send_result send_result;
struct sp_ev_stream_consumed consumed_result;
struct sp_ev_tun_emit_result emit_result;
struct sp_ev_ingress_disposed disposed_result;
int produced = 0;
int emit_blocked = 0;
int rc;
switch (effect->type) {
case SP_FX_OPEN_STREAM:
if (sp_ns_open_backend(f, &effect->value.open_stream) < 0) {
DBG("backend open failed: %s", strerror(errno));
return SP_NS_PUMP_FAILED;
}
break;
case SP_FX_TCP_EMIT_CONTROL:
/* Challenge-ACK classification uses pre-event receive state. */
if (sp_ns_emit_child_record(f, &effect->value.tcp_emit_control,
f->challenge_ack) < 0)
return SP_NS_PUMP_FAILED;
break;
case SP_FX_TCP_EMIT_BATCH: {
const struct sp_fx_tcp_emit_batch *emit =
&effect->value.tcp_emit_batch;
struct sp_ns_emit_transaction emit_txn = {
.flow = f, .effect = emit, .tunfd = ctx->tunfd};
/* The preceding CREDIT confirmed these bytes are in the backend socket,
* so a materialize (peek) BLOCKED here is not expected; treat it as
* loud-fatal rather than a silent stall. */
/* The live trace shows the first batch materialized after socket
* consumption can be lost while its later successor reaches the child:
* the child remains at that batch's ACK boundary and dup-ACKs every
* later range. Keep SO_PEEK_OFF only while the socket head is still
* unchanged. Once any prefix was consumed (and for retransmission),
* use the bounded discard-iovec path, whose relative offset is explicit
* and cannot inherit kernel-specific peek-cursor state. */
if (f->socks_residual_active &&
emit->end.value <= f->cold->socks_residual_len) {
uint64_t residual_length = emit->end.value - emit->start.value;
if (emit->start.value > SIZE_MAX || residual_length > SIZE_MAX ||
emit->end.value > sizeof(f->cold->socks_residual)) {
errno = EOVERFLOW;
return SP_NS_PUMP_FAILED;
}
memcpy(g_ns_payload,
f->cold->socks_residual + (size_t)emit->start.value,
(size_t)residual_length);
rc = 1;
} else {
rc = sp_egress_materialize_emit(
&f->egress, f->backend_fd, emit,
f->egress.stream_consumed != 0 ||
f->local_core.tun_emit_effect.retransmission != 0,
g_ns_discard, sizeof(g_ns_discard), g_ns_payload,
sizeof(g_ns_payload));
}
if (rc <= 0) {
DBG("materialize_emit rc=%d (%s)", rc, strerror(errno));
return SP_NS_PUMP_FAILED;
}
rc = sp_egress_execute_emit_with_typed(
&f->egress, sp_ns_tun_write, &emit_txn, emit, ntohl(f->srv_ip),
ntohl(f->cli_ip), f->srv_port, f->cli_port, g_ns_payload,
(size_t)(emit->end.value - emit->start.value),
f->egress_force_software,
item.now, &emit_result);
if (rc < 0) {
DBG("emit_with fatal (%s)", strerror(errno));
return SP_NS_PUMP_FAILED;
}
if (rc == 0) {
/* The TUN output FIFO is full. Retain this exact effect and
* defer; the core holds its matching tun_emit_effect and will neither
* re-emit nor retransmit until the result arrives, so we re-execute
* *this* effect on TUN writability (sp_ns_retry_pending_emit). No
* result is produced, so snd-state does not advance and no lockstep
* control_seq is consumed. Stop processing further effects this pass;
* whatever the core queued behind the emit is re-derived on re-drive. */
f->pending_emit = *emit;
f->has_pending_emit = 1;
sp_ns_output_work_refresh(f);
emit_blocked = 1;
break;
}
if (f->has_sched_emit_result) {
DBG("multiple scheduler completions for flow %llu",
(unsigned long long)f->flow_id);
return SP_NS_PUMP_FAILED;
}
f->sched_emit_result = emit_result;
f->has_sched_emit_result = 1;
sp_ns_output_work_refresh(f);
break;
}
case SP_FX_STREAM_SEND: {
const struct sp_fx_stream_send *send = &effect->value.stream_send;
{
uint8_t *reservation =
sp_ns_reservation_acquire(&f->ingress, f->admission_role);
if (reservation == NULL) {
errno = ENOBUFS;
return SP_NS_PUMP_FAILED;
}
if (send->capability.value != 0) {
if (send->capability.value == staged_id)
authorized_staged = 1;
}
rc = sp_ingress_execute_stream_send_typed(
&g_ns_stage_pool, &f->ingress, sp_ingress_kernel_send,
(void *)(intptr_t)f->backend_fd, send, item.now,
reservation, SP_INGRESS_RESERVATION_SLOT_BYTES, &send_result);
if (rc < 0) {
DBG("stream_send fatal (%s)", strerror(errno));
return SP_NS_PUMP_FAILED;
}
if (rc == 0) {
/* STAGE_BLOCKED: bytes are reserved; re-flush on backend writability. */
break;
}
if (f->ingress.pending_bytes == 0)
sp_ns_reservation_release(&f->ingress);
produced_items[produced_count] =
sp_pump_item_stream_send_result(&send_result);
produced = 1;
break;
}
}
case SP_FX_STREAM_CONSUME: {
const struct sp_fx_stream_consume *consume =
&effect->value.stream_consume;
if (f->socks_residual_active &&
consume->start.value == f->egress.stream_consumed &&
consume->end.value <= f->cold->socks_residual_len) {
uint64_t accepted =
consume->end.value - consume->start.value;
if (accepted == 0 ||
UINT64_MAX - f->egress.stream_consumed < accepted)
return SP_NS_PUMP_FAILED;
f->egress.stream_consumed += accepted;
memset(&consumed_result, 0, sizeof(consumed_result));
sp_tcp_event_context_init(&consumed_result.context, f->flow_id,
f->core_gen, item.now);
consumed_result.effect = consume->context.effect;
consumed_result.accepted.value = accepted;
if (f->egress.stream_consumed == f->cold->socks_residual_len)
f->socks_residual_active = 0;
rc = 1;
} else {
rc = sp_egress_execute_consume_typed(
&f->egress, f->backend_fd, consume, item.now, 0,
g_ns_discard, sizeof(g_ns_discard), &consumed_result);
}
if (rc < 0) {
DBG("consume fatal (%s)", strerror(errno));
return SP_NS_PUMP_FAILED;
}
if (rc == 0)
break; /* BLOCKED: the authorized bytes will be drained on retry */
/* A child ACK freed flight: backend reads can make progress again, so
* re-arm EPOLLIN (cleared here; the post-pump interest recompute applies
* it). */
f->backend_read_idle = 0;
produced_items[produced_count] =
sp_pump_item_stream_consumed(&consumed_result);
produced = 1;
break;
}
case SP_FX_INGRESS_DISPOSE:
rc = sp_ingress_execute_dispose_typed(
&g_ns_stage_pool, &f->ingress, &effect->value.ingress_dispose,
item.now, &disposed_result);
if (rc < 0)
return SP_NS_PUMP_FAILED;
produced_items[produced_count] =
sp_pump_item_ingress_disposed(&disposed_result);
produced = 1;
break;
case SP_FX_STREAM_SHUTDOWN:
/* Backend-facing FIN; the child-facing close handshake is increment 3. */
(void)shutdown(f->backend_fd, SHUT_WR);
break;
case SP_FX_CLOSE_FLOW:
/* A close that follows a mutual FIN exchange is graceful; anything else
* (handshake/RTO/resource deadline, RST, connect or consume failure) is
* an abort that resets the child. */
if (f->local_core.peer_fin_seen && f->local_core.fin_acked)
f->pump_close_graceful = 1;
release = 1;
break;
}
if (produced) {
if (ctx->count + produced_count == SP_NS_QUEUE_MAX) {
DBG("pump work queue overflow");
return SP_NS_PUMP_FAILED;
}
produced_count++;
}
if (emit_blocked)
break; /* emit retained; skip effects queued behind it (re-derived) */
}
/* Prepend immediate completions in effect order before older work. */
while (produced_count > 0) {
qhead = (qhead + SP_NS_QUEUE_MAX - 1) % SP_NS_QUEUE_MAX;
ctx->queue[qhead] = produced_items[--produced_count];
ctx->count++;
}
/* Release child payloads not authorized by this event. */
if (staged_id != 0 && !authorized_staged) {
struct sp_ingress_stage_slot *slot = sp_ingress_find_slot(
&g_ns_stage_pool, f->flow_id, f->core_gen, staged_id);
if (slot != NULL) {
uint64_t interval_start = slot->interval_start;
uint64_t interval_end = slot->interval_end;
sp_ingress_stage_free(&f->ingress, slot);
g_ns_orphans_freed++;
DBG("freed unauthorized staged frame (flow %llu, "
"range=%llu..%llu event_seq=%u acked=%llu authorized=%llu, "
"orphans=%llu)",
(unsigned long long)f->flow_id,
(unsigned long long)interval_start,
(unsigned long long)interval_end,
item.kind == SP_PUMP_ITEM_SEGMENT ? item.value.segment.seq : 0,
(unsigned long long)f->local_core.rcv_acked_abs,
(unsigned long long)f->local_core.rx_authorized_end_abs,
(unsigned long long)g_ns_orphans_freed);
}
}
}
if (ctx->count != 0) {
DBG("pump iteration bound hit (runaway effect chain)");
return SP_NS_PUMP_FAILED;
}
if (release)
return SP_NS_PUMP_FAILED;
if (sp_ns_update_backend_interest(f) < 0)
return SP_NS_PUMP_FAILED;
return SP_NS_PUMP_COMPLETE;
}
/* The passive child handshake may still be in SYN-RECEIVED when the SOCKS
* proxy returns CONNECT success and coalesced application bytes. Keep those
* bytes private until the child's final ACK establishes the core, then publish
* their logical range exactly once. */
static int sp_ns_publish_socks_residual(struct sp_ns_flow *f,
uint64_t now_ns) {
struct sp_ev_credit event;
if (!f->socks_residual_active ||
f->local_core.state != SP_TCP_CORE_ESTABLISHED)
return 0;
if (f->egress.stream_tail == f->cold->socks_residual_len &&
f->local_core.stream_tail == f->cold->socks_residual_len)
return 0; /* already published; an ACK will retire the retained bytes */
if (f->egress.stream_tail != 0 || f->egress.stream_consumed != 0 ||
f->cold->socks_residual_len == 0)
return -1;
f->egress.stream_tail = f->cold->socks_residual_len;
memset(&event, 0, sizeof(event));
event.context.key = sp_flow_key_make(
(sp_flow_id){.value = f->flow_id},
(sp_generation_id){.value = f->core_gen});
event.context.now.value = now_ns;
event.stream_tail.value = f->cold->socks_residual_len;
event.tun_segments.value = SP_TCP_CORE_MAX_SEGMENTS;
return sp_ns_pump_start(f, sp_pump_item_credit(&event), 0, 0);
}
static int sp_publish_synthetic_port_in_use(uint16_t port) {
for (size_t i = 0; i < sp_ns_tcp_slot_limit(); ++i)
if (g_ns_flows[i].in_use && g_ns_flows[i].srv_ip == PUBLISH_SYNTHETIC_IP &&
g_ns_flows[i].srv_port == port)
return 1;
for (size_t i = 0; i < MAX_PUBLISH_UDP; ++i)
if (publish_udp_flows[i].used && publish_udp_flows[i].synthetic_port == port)
return 1;
for (size_t i = 0; i < SP_TIMEWAIT_ALL_MAX; ++i)
if (g_timewait_map.entries[i].state == SP_NS_TIMEWAIT_LIVE &&
g_timewait_map.entries[i].srv_ip == PUBLISH_SYNTHETIC_IP &&
g_timewait_map.entries[i].srv_port == port)
return 1;
return 0;
}
static size_t sp_ns_publish_live_limit(void) {
return sp_publish_limit_for_shared(sp_ns_tcp_slot_limit());
}
static int sp_ns_publish_connect_capacity(void) {
size_t live = 0;
size_t half_open = 0;
for (size_t i = 0; i < sp_ns_tcp_slot_limit(); ++i) {
const struct sp_ns_flow *flow = &g_ns_flows[i];
if (!flow->in_use || flow->admission_role != SP_TCP_ROLE_PUBLISH)
continue;
live++;
if (flow->local_core.state < SP_TCP_CORE_ESTABLISHED)
half_open++;
}
return live < sp_ns_publish_live_limit() &&
half_open < SP_TCP_HALF_PUBLISH_MAX &&
half_open < SP_CONNECT_PUBLISH_MAX;
}
static int sp_ns_accept_publish(int tunfd, struct publish_rule *rule, int fd,
const struct sockaddr_in *peer) {
struct sp_ns_flow *f;
struct sp_ev_flow_admitted event;
struct sp_admission_key source_key;
uint16_t synthetic_port;
uint32_t iss;
if (rule == NULL || peer == NULL || fd < 0)
return -1;
if (!sp_ns_publish_connect_capacity()) {
sp_counter_inc(&g_resource_counters.tcp_flow_rejections);
return -1;
}
synthetic_port = sp_ns_publish_port_alloc();
if (synthetic_port == 0 ||
sp_random_exact_nonblocking(&iss, sizeof(iss)) < 0) {
sp_counter_inc(&g_resource_counters.tcp_flow_rejections);
return -1;
}
f = sp_ns_alloc();
if (f == NULL) {
sp_counter_inc(&g_resource_counters.tcp_flow_rejections);
return -1;
}
memset(f, 0, sizeof(*f));
f->cold = &g_ns_cold_flows[f - g_ns_flows];
memset(f->cold, 0, sizeof(*f->cold));
f->in_use = 1;
f->backend_fd = fd;
f->cli_ip = SANDBOX_IP;
f->srv_ip = PUBLISH_SYNTHETIC_IP;
f->cli_port = rule->container_port;
f->srv_port = synthetic_port;
f->admission_role = SP_TCP_ROLE_PUBLISH;
f->admission_source_ip = peer->sin_addr.s_addr;
f->flow_id = g_ns_next_flow_id++;
f->core_gen = 1;
f->established = 1; /* accepted host socket needs no connect completion */
f->last_active_ns = sp_ns_now_ns();
sp_ns_timer_refresh(f);
f->generation = g_ns_epoll_generation;
g_ns_epoll_generation = epoll_next_generation(g_ns_epoll_generation);
sp_ns_tcp_adapters_init(f);
sp_resource_admission_source_key(&source_key, f->admission_source_ip,
f->admission_role);
{
struct sp_admission_entry *source =
sp_resource_admission_acquire(&source_key);
if (source == NULL ||
!sp_resource_admission_take(source, sp_ns_now_ns())) {
if (source != NULL)
sp_resource_admission_release(source);
sp_ns_flow_release(f);
return 0;
}
}
f->admission_source_held = 1;
{
size_t index = (size_t)(f - g_ns_flows);
struct epoll_event ev = {
.events = EPOLLRDHUP | EPOLLERR | EPOLLHUP,
.data.u64 = epoll_registration_token(FD_TCP_CORE, index, fd,
f->generation)};
if (g_epfd >= 0) {
if (sp_epoll_update(EPOLL_CTL_ADD, fd, &ev, &f->backend_interest) < 0) {
sp_ns_flow_release(f);
return 0;
}
f->backend_epoll_registered = 1;
}
}
memset(&event, 0, sizeof(event));
event.key = sp_flow_key_make((sp_flow_id){.value = f->flow_id},
(sp_generation_id){.value = f->core_gen});
event.now.value = sp_ns_now_ns();
event.tuple.local_addr = ntohl(f->srv_ip);
event.tuple.peer_addr = ntohl(f->cli_ip);
event.tuple.local_port = f->srv_port;
event.tuple.peer_port = f->cli_port;
event.policy_role = SP_TCP_ROLE_PUBLISH;
event.open_role = SP_TCP_OPEN_ACTIVE;
event.local_iss = iss;
event.local_rcv_wscale = SP_TCP_RCV_WSCALE;
g_ns_tunfd_for_emit = tunfd;
if (sp_ns_pump_start(f, sp_pump_item_admission(&event), 0, 1) < 0) {
sp_ns_flow_release(f);
g_ns_tunfd_for_emit = -1;
return 0;
}
if (sp_ns_update_backend_interest(f) < 0)
sp_ns_reset_child(f);
g_ns_tunfd_for_emit = -1;
return 0;
}
/* Resolve a carried scheduler owner index without reversing a flow id through
* the live table. The index is merely a hint until all three identity fields
* agree; this is the generation-reuse/type-confusion guard at the final sink. */
static struct sp_ns_flow *
sp_ns_sched_flow_for_record(const struct sp_scheduler_record *record) {
uint16_t index;
if (record == NULL || record->flow_index == UINT16_MAX)
return NULL;
index = record->flow_index;
if ((size_t)index >= sp_ns_tcp_slot_limit()) {
sp_counter_inc(&g_resource_counters.scheduler_stale_index_rejects);
return NULL;
}
if (!g_ns_flows[index].in_use ||
g_ns_flows[index].flow_id != record->flow ||
g_ns_flows[index].core_gen != record->generation) {
sp_counter_inc(&g_resource_counters.scheduler_stale_index_rejects);
return NULL;
}
return &g_ns_flows[index];
}
/* Pure: decides from the record, its live owner `f` (NULL when there is
* none) and FIFO fullness alone, before anything is written. Returns 1 when
* the frame should be written; otherwise *verdict is the outcome. A record
* whose live owner was reset or reused is dropped as written; CLOSED/ICMP-error
* responses are auxiliary control records with no live flow by design. */
static int sp_ns_sched_decide(const struct sp_scheduler_record *record,
const struct sp_ns_flow *f, int fifo_full,
int *verdict) {
int aux = record->klass == SP_SCHEDULER_CLASS_CONTROL &&
sp_ns_is_aux_flow(record->flow);
*verdict = SP_SCHEDULER_WRITE_BLOCKED;
if (f == NULL && !aux) {
*verdict = SP_SCHEDULER_WRITE_DONE;
return 0;
}
if (fifo_full) {
/* A GSO container is retired and re-executed as scalars instead. */
if (record->klass == SP_SCHEDULER_CLASS_DATA && f->sched_emit_outstanding == 1)
*verdict = SP_SCHEDULER_WRITE_RETIRED;
return 0;
}
if (f == NULL)
return 1;
/* Every queued data record is counted outstanding, and a terminal record's
* core result is held until it is written. */
if (record->klass == SP_SCHEDULER_CLASS_DATA)
assert(f->sched_emit_outstanding > 0);
else if (f->sched_terminal_seq == record->seq_abs)
assert(f->has_sched_emit_result);
return 1;
}
static int sp_ns_sched_write(void *context,
const struct sp_scheduler_record *record,
const uint8_t *frame) {
int tunfd = *(int *)context;
struct sp_ns_flow *f = sp_ns_sched_flow_for_record(record);
int verdict;
if (!sp_ns_sched_decide(record, f, g_tun_queue.count == TUN_QUEUE_PACKETS,
&verdict)) {
if (verdict == SP_SCHEDULER_WRITE_RETIRED) {
/* Retire without completing the core effect; the exact effect is
* re-executed through the ordered scalar path after the final FIFO
* drains, producing independently scheduled scalar records. */
f->sched_emit_outstanding = 0;
f->sched_restart_software = 1;
f->egress_force_software = 1;
f->has_pending_emit = 1;
sp_ns_output_work_refresh(f);
f->sched_emit_written = 0;
}
return verdict;
}
if (tun_write_frame(tunfd, frame, record->frame_len,
record->klass == SP_SCHEDULER_CLASS_CONTROL ? "scheduler control"
: "scheduler data") < 0)
return errno == EAGAIN || errno == EWOULDBLOCK ? SP_SCHEDULER_WRITE_BLOCKED
: SP_SCHEDULER_WRITE_FAILED;
if (record->klass == SP_SCHEDULER_CLASS_DATA) {
sp_scheduler_commit_seq(&g_ns_tun_sched, record->flow, record->seq_abs);
f->sched_emit_outstanding--;
if (f->sched_emit_outstanding == 0)
f->sched_emit_written = 1;
} else if (f != NULL && f->sched_terminal_seq == record->seq_abs) {
f->sched_terminal_seq = 0;
f->sched_emit_written = 1;
}
return SP_SCHEDULER_WRITE_DONE;
}
static void sp_ns_service_tun_scheduler(int tunfd) {
size_t i;
if (g_ns_sched_servicing || g_broker_io_failed)
return;
g_ns_sched_servicing = 1;
if (sp_scheduler_service(&g_ns_tun_sched, sp_ns_sched_write, &tunfd,
sp_ns_scheduler_clock, NULL) == SIZE_MAX) {
g_broker_io_failed = 1;
g_ns_sched_servicing = 0;
return;
}
/* A final-sink-blocked GSO container was retired without a core completion.
* Re-execute its exact effect through the scalar fallback, then service the
* independently admitted scalar records. */
for (i = sp_ns_output_work_next(0); i < sp_ns_tcp_slot_limit();
i = sp_ns_output_work_next(i + 1)) {
struct sp_ns_flow *f = &g_ns_flows[i];
if (!f->in_use || !f->sched_restart_software)
continue;
f->sched_restart_software = 0;
f->sched_emit_written = 0;
sp_ns_retry_pending_emit(tunfd, f);
}
/* Credit epochs: an admission-blocked producer is not re-driven merely
* because the TUN fd remained writable. Retry only after scheduler service
* published a greater epoch with growth in a dimension that actually
* blocked this record. The scan happens after service returns, so admission
* cannot re-enter a mutating scheduler callback. */
for (i = sp_ns_output_work_next(0); i < sp_ns_tcp_slot_limit();
i = sp_ns_output_work_next(i + 1)) {
struct sp_ns_flow *f = &g_ns_flows[i];
struct sp_scheduler_credit credit;
if (!f->in_use || !f->has_pending_emit || !f->waits_sched_credit)
continue;
credit = sp_scheduler_credit_now(&g_ns_tun_sched);
if (!sp_scheduler_credit_permits_retry(&f->sched_blocked_credit, &credit,
f->sched_blocked_dims))
continue;
f->waits_sched_credit = 0;
f->sched_blocked_dims = 0;
sp_ns_retry_pending_emit(tunfd, f);
}
/* Only now is the executor result visible to the core: the scheduler write sink
* accepted the exact record, so send-state can safely advance. */
for (i = sp_ns_output_work_next(0); i < sp_ns_tcp_slot_limit();
i = sp_ns_output_work_next(i + 1)) {
struct sp_ns_flow *f = &g_ns_flows[i];
int saved_tunfd;
if (!f->in_use || !f->has_sched_emit_result || !f->sched_emit_written)
continue;
f->has_sched_emit_result = 0;
f->sched_emit_written = 0;
/* Timestamp final-sink acceptance, not the original queued emission. */
f->sched_emit_result.context.now.value = sp_ns_now_ns();
saved_tunfd = g_ns_tunfd_for_emit;
g_ns_tunfd_for_emit = tunfd;
if (sp_ns_pump_start(f, sp_pump_item_tun_emit_result(&f->sched_emit_result),
0, 0) < 0)
sp_ns_teardown(f);
else if (f->in_use)
sp_ns_maybe_park_timewait(f);
g_ns_tunfd_for_emit = saved_tunfd;
sp_ns_output_work_refresh(f);
}
g_ns_sched_servicing = 0;
g_ns_sched_tun_pending =
g_ns_tun_sched.control.count != 0 || g_ns_tun_sched.data.count != 0;
tun_update_events(tunfd);
}
/* CLOSED dispatch remains outside the TCP core: there
* is no TCB to mutate. It constructs the RFC CLOSED response canonically, then
* the overload policy applies the closed-RST bucket and the never-RST-to-RST rule before
* the scheduler owns final TUN delivery. */
static int sp_ns_emit_closed_rst(int tunfd, uint32_t cli_ip, uint32_t srv_ip,
uint16_t cli_port, uint16_t srv_port,
uint32_t seq, uint32_t ack, uint8_t flags,
size_t payload_len, uint64_t now_ns) {
uint8_t record[SP_VNET_HDR_BYTES + 40];
uint8_t response_flags;
uint32_t response_seq;
uint32_t response_ack;
uint64_t flow = sp_ns_aux_flow_id(ntohl(cli_ip), ntohl(srv_ip), cli_port,
srv_port, IPPROTO_TCP);
size_t record_len;
if ((flags & SP_TCP_CORE_RST) != 0)
return sp_ns_admit_aux_response(SP_SCHEDULER_RESP_CLOSED_RST, flow, 1, NULL, 0,
now_ns);
if ((flags & SP_TCP_CORE_ACK) != 0) {
response_flags = SP_TCP_CORE_RST;
response_seq = ack;
response_ack = 0;
} else {
size_t sequence_len = payload_len;
if ((flags & SP_TCP_CORE_SYN) != 0)
sequence_len++;
if ((flags & SP_TCP_CORE_FIN) != 0)
sequence_len++;
response_flags = SP_TCP_CORE_RST | SP_TCP_CORE_ACK;
response_seq = 0;
response_ack = tcp_seq_add_len(seq, sequence_len);
}
record_len = sp_vnet_build_tcp_record(
record, sizeof(record), ntohl(srv_ip), ntohl(cli_ip), srv_port,
cli_port, response_seq, response_ack, response_flags, NULL, 0, 0);
if (record_len == 0)
return -1;
if (sp_ns_admit_aux_response(SP_SCHEDULER_RESP_CLOSED_RST, flow, 0, record,
record_len, now_ns) < 0)
return -1;
sp_ns_service_tun_scheduler(tunfd);
return 0;
}
static int sp_udp_emit_error(int tunfd, const uint8_t *quote,
const struct udp_flow *f,
enum sp_udp_send_result result) {
uint8_t record[SP_VNET_HDR_BYTES + 56];
uint16_t rejected_len;
uint16_t mtu = 0;
uint16_t quoted_sport;
uint16_t quoted_dport;
uint64_t flow;
int saved_tunfd;
struct sp_icmp_control_request request;
struct sp_scalar_packet reply;
if (quote == NULL || f == NULL ||
(result != SP_UDP_SEND_EMSGSIZE && result != SP_UDP_SEND_REFUSED) ||
quote[0] != 0x45 || quote[9] != IPPROTO_UDP ||
(sp_get_be16(quote + 6) & (IP_MF | IP_OFFMASK)) != 0)
return -1;
rejected_len = sp_get_be16(quote + 2);
quoted_sport = sp_get_be16(quote + SP_IPV4_HEADER_BYTES);
quoted_dport = sp_get_be16(quote + SP_IPV4_HEADER_BYTES + 2);
if (sp_get_be32(quote + 12) != ntohl(f->cli_ip) ||
sp_get_be32(quote + 16) != ntohl(f->srv_ip) ||
quoted_sport != f->cli_port || quoted_dport != f->srv_port)
return -1;
if (result == SP_UDP_SEND_EMSGSIZE) {
mtu = udp_effective_path_mtu(f, rejected_len);
if (mtu == 0) {
sp_counter_inc(&g_resource_counters.icmp_drops);
return 0;
}
}
memset(&request, 0, sizeof(request));
request.code = result == SP_UDP_SEND_EMSGSIZE ? 4 : 3;
request.mtu = mtu;
memcpy(request.quote, quote, SP_IPV4_HEADER_BYTES + SP_UDP_HEADER_BYTES);
if (sp_build_icmp_control_reply(&request, sp_icmp_reply_id++, &reply) !=
SP_CODEC_OK) {
sp_counter_inc(&g_resource_counters.icmp_drops);
return -1;
}
memset(record, 0, sizeof(record));
memcpy(record + SP_VNET_HDR_BYTES, reply.bytes, reply.len);
flow = sp_ns_aux_flow_id(ntohl(f->cli_ip), ntohl(f->srv_ip), f->cli_port,
f->srv_port, IPPROTO_UDP);
saved_tunfd = g_ns_tunfd_for_emit;
g_ns_tunfd_for_emit = tunfd;
if (sp_ns_admit_aux_response(SP_SCHEDULER_RESP_ICMP, flow, 0, record,
SP_VNET_HDR_BYTES + reply.len,
sp_ns_now_ns()) < 0) {
g_ns_tunfd_for_emit = saved_tunfd;
sp_counter_inc(&g_resource_counters.icmp_drops);
return -1;
}
sp_ns_service_tun_scheduler(tunfd);
g_ns_tunfd_for_emit = saved_tunfd;
return 0;
}
static int sp_ns_timewait_segment_acceptable(
const struct sp_ns_timewait_entry *entry, uint64_t seq_abs,
uint64_t sequence_len) {
if (sequence_len == 0) {
if (entry->rcv_adv_right_abs == entry->rcv_acked_abs)
return seq_abs == entry->rcv_acked_abs;
return seq_abs >= entry->rcv_acked_abs &&
seq_abs <= entry->rcv_adv_right_abs;
}
return seq_abs < entry->rcv_adv_right_abs &&
seq_abs + sequence_len > entry->rcv_acked_abs;
}
/* A TIME-WAIT tombstone is not a reduced live flow. It cannot reopen a
* backend or accept stream bytes; it only preserves the quarantined tuple's
* required ACK/challenge/RST disposition until its absolute expiry. */
static int sp_ns_handle_timewait(int tunfd, struct sp_ns_timewait_entry *entry,
uint32_t seq, uint8_t flags,
size_t payload_len, uint64_t now_ns) {
uint64_t sequence_len = payload_len;
uint64_t seq_abs;
int acceptable;
uint8_t record[SP_VNET_HDR_BYTES + 40];
size_t record_len;
enum sp_scheduler_response_kind kind = SP_SCHEDULER_RESP_CURRENT_ACK;
if (entry == NULL || entry->state != SP_NS_TIMEWAIT_LIVE)
return -1;
if ((flags & SP_TCP_CORE_SYN) != 0)
sequence_len++;
if ((flags & SP_TCP_CORE_FIN) != 0)
sequence_len++;
seq_abs = sp_tcp_unwrap_u32(seq, entry->rcv_acked_abs);
acceptable = sp_ns_timewait_segment_acceptable(entry, seq_abs, sequence_len);
if ((flags & SP_TCP_CORE_RST) != 0) {
if (acceptable && seq_abs == entry->rcv_acked_abs)
sp_ns_timewait_remove(entry);
else if (acceptable)
kind = SP_SCHEDULER_RESP_CHALLENGE_ACK;
else
return 0; /* unacceptable RST is silent */
} else if ((flags & SP_TCP_CORE_SYN) != 0 && acceptable) {
kind = SP_SCHEDULER_RESP_CHALLENGE_ACK;
}
/* RFC TIME-WAIT restart applies only to an acceptable retransmitted FIN. */
if ((flags & SP_TCP_CORE_FIN) != 0 && acceptable)
sp_ns_timewait_heap_refresh(entry, now_ns + SP_TCP_TIME_WAIT_NS);
if ((flags & SP_TCP_CORE_RST) != 0 && kind == SP_SCHEDULER_RESP_CURRENT_ACK)
return 0; /* exact RST removed the tombstone and has no response */
record_len = sp_vnet_build_tcp_record(
record, sizeof(record), ntohl(entry->srv_ip), ntohl(entry->cli_ip),
entry->srv_port, entry->cli_port, (uint32_t)entry->snd_nxt_abs,
(uint32_t)entry->rcv_acked_abs, SP_TCP_CORE_ACK, NULL, 0, 0);
if (record_len == 0)
return -1;
if (sp_ns_admit_aux_response(
kind, sp_ns_timewait_hash(entry->cli_ip, entry->srv_ip,
entry->cli_port, entry->srv_port),
0, record, record_len, now_ns) < 0)
return -1;
sp_ns_service_tun_scheduler(tunfd);
return 0;
}
/* Coalesce contiguous scalar child packets into the same immutable ingress slot
* and one core event. This is the live counterpart of a TCPv4 GSO frame:
* bytes remain executor-owned, while the engine sees one contiguous sequence
* interval and can authorize it with one capability/effect round trip. */
static void
sp_ns_drop_unstaged_payload(struct sp_ev_tcp_segment *segment) {
segment->ingress.value = 0;
segment->payload_length.value = 0;
segment->flags &= (uint8_t)~SP_TCP_CORE_FIN;
}
/* Handles a validated child->broker TCP packet under the core adapter. The
* immutable parser product and its backing bytes are consumed synchronously;
* neither pointer is retained. Returns 1 if the packet was consumed by this
* path, 0 when the handoff contract is not satisfied. */
static int handle_core_tcp(int tunfd, const uint8_t *pkt, size_t len,
const struct sp_packet_value *parsed) {
const struct sp_tcp_value *tcp;
uint32_t cli_ip;
uint32_t srv_ip;
uint16_t cli_port;
uint16_t srv_port;
uint8_t flags;
size_t payload_off;
size_t payload_len;
struct sp_ns_flow *f;
uint64_t now = sp_ns_now_ns();
if (pkt == NULL || parsed == NULL ||
parsed->ip.protocol != IPPROTO_TCP ||
parsed->ip.src != ntohl(SANDBOX_IP) ||
(size_t)parsed->ip.total_len != len ||
parsed->ip.transport_offset > len ||
parsed->ip.transport_len != len - parsed->ip.transport_offset ||
parsed->tcp.data_offset < 5 || parsed->tcp.data_offset > 15 ||
parsed->tcp.payload_offset !=
parsed->ip.transport_offset +
(size_t)parsed->tcp.data_offset * 4U ||
!sp_slice_ok(len, parsed->tcp.payload_offset,
parsed->tcp.payload_len) ||
parsed->tcp.payload_len != len - parsed->tcp.payload_offset)
return 0;
tcp = &parsed->tcp;
cli_ip = htonl(parsed->ip.src);
srv_ip = htonl(parsed->ip.dst);
cli_port = tcp->src_port;
srv_port = tcp->dst_port;
flags = (uint8_t)(tcp->flags & 0x3fU);
payload_off = tcp->payload_offset;
payload_len = tcp->payload_len;
g_ns_tunfd_for_emit = tunfd;
f = sp_ns_find(cli_ip, cli_port, srv_ip, srv_port);
/* Published tuples are allocated only by an accepted host connection. A
* child packet cannot create or rebind one, regardless of global egress
* mode. */
if (srv_ip == PUBLISH_SYNTHETIC_IP && f == NULL) {
g_ns_tunfd_for_emit = -1;
return 1;
}
if (srv_ip != PUBLISH_SYNTHETIC_IP &&
g_net_policy.egress != EGRESS_DIRECT &&
g_net_policy.egress != EGRESS_SOCKS) {
g_ns_tunfd_for_emit = -1;
return 0;
}
/* TIME-WAIT is a separate bounded tombstone, not a reusable live-flow
* slot. It is consulted in the same dispatch turn as the live lookup so a
* tuple cannot be allocated between the two ownership decisions. */
if (f == NULL) {
struct sp_ns_timewait_entry *timewait =
sp_ns_timewait_find_live(cli_ip, srv_ip, cli_port, srv_port, now);
if (timewait != NULL) {
int rc = sp_ns_handle_timewait(
tunfd, timewait, tcp->seq, flags, payload_len, now);
g_ns_tunfd_for_emit = -1;
return rc < 0 ? -1 : 1;
}
}
if (f == NULL) {
struct sp_ev_flow_admitted admission;
uint8_t admission_role;
if ((flags & SP_TCP_FLAG_SYN) == 0 ||
(flags & (SP_TCP_FLAG_ACK | SP_TCP_FLAG_RST)) != 0) {
if (sp_ns_emit_closed_rst(tunfd, cli_ip, srv_ip, cli_port, srv_port,
tcp->seq, tcp->ack, flags, payload_len,
now) < 0)
{
g_ns_tunfd_for_emit = -1;
return -1;
}
g_ns_tunfd_for_emit = -1;
return 1;
}
if ((is_gateway_ip(srv_ip) &&
!sp_policy_gateway_allowed(&g_net_policy, srv_ip, srv_port,
IPPROTO_TCP)) ||
(!is_gateway_ip(srv_ip) && g_net_policy.egress == EGRESS_DIRECT &&
!sp_policy_direct_egress_allowed(&g_net_policy, srv_ip, srv_port,
IPPROTO_TCP))) {
log_direct_egress_block(srv_ip, srv_port, IPPROTO_TCP);
if (sp_ns_emit_closed_rst(tunfd, cli_ip, srv_ip, cli_port, srv_port,
tcp->seq, tcp->ack, flags, payload_len,
now) < 0)
{
g_ns_tunfd_for_emit = -1;
return -1;
}
g_ns_tunfd_for_emit = -1;
return 1;
}
admission_role = (uint8_t)(g_net_policy.egress == EGRESS_SOCKS &&
!is_gateway_ip(srv_ip)
? SP_TCP_ROLE_SOCKS
: SP_TCP_ROLE_DIRECT);
if (!sp_ns_tcp_admission_capacity(admission_role)) {
sp_counter_inc(&g_resource_counters.tcp_flow_rejections);
(void)sp_ns_emit_closed_rst(
tunfd, cli_ip, srv_ip, cli_port, srv_port, tcp->seq, tcp->ack,
flags, payload_len, now);
g_ns_tunfd_for_emit = -1;
return 1;
}
f = sp_ns_alloc();
if (f == NULL) {
if (sp_ns_emit_closed_rst(tunfd, cli_ip, srv_ip, cli_port, srv_port,
tcp->seq, tcp->ack, flags, payload_len,
now) < 0)
{
g_ns_tunfd_for_emit = -1;
return -1;
}
g_ns_tunfd_for_emit = -1;
return 1;
}
memset(f, 0, sizeof(*f));
f->cold = &g_ns_cold_flows[f - g_ns_flows];
memset(f->cold, 0, sizeof(*f->cold));
f->in_use = 1;
f->backend_fd = -1;
f->cli_ip = cli_ip;
f->srv_ip = srv_ip;
f->cli_port = cli_port;
f->srv_port = srv_port;
f->use_socks = g_net_policy.egress == EGRESS_SOCKS &&
!is_gateway_ip(srv_ip);
f->admission_role = admission_role;
f->admission_source_ip = cli_ip;
{
struct sp_admission_key source_key;
sp_resource_admission_source_key(&source_key, cli_ip, f->admission_role);
struct sp_admission_entry *source =
sp_resource_admission_acquire(&source_key);
if (source == NULL || !sp_resource_admission_take(source, now)) {
if (source != NULL)
sp_resource_admission_release(source);
sp_ns_flow_release(f);
(void)sp_ns_emit_closed_rst(
tunfd, cli_ip, srv_ip, cli_port, srv_port, tcp->seq, tcp->ack,
flags, payload_len, now);
g_ns_tunfd_for_emit = -1;
return 1;
}
f->admission_source_held = 1;
}
if (f->use_socks && g_net_policy.socks_remote_dns) {
struct sp_dns_ref mapping;
struct sp_dns_name_view name;
if (sp_dns_core_acquire_ip(&g_dns_core, srv_ip, now, &mapping) == 0) {
f->dns_mapping_ref = mapping;
if (sp_dns_core_mapping_name(&g_dns_core, mapping, &name) < 0 ||
name.name_len >= sizeof(f->cold->socks_target_domain)) {
sp_ns_flow_release(f);
g_ns_tunfd_for_emit = -1;
return 1;
}
memcpy(f->cold->socks_target_domain, name.name, name.name_len);
f->cold->socks_target_domain[name.name_len] = '\0';
} else if (dns_is_synthetic_ip(srv_ip)) {
sp_ns_flow_release(f);
g_ns_tunfd_for_emit = -1;
return 1;
}
}
f->flow_id = g_ns_next_flow_id++;
f->core_gen = 1;
f->last_active_ns = sp_ns_now_ns();
sp_ns_timer_refresh(f);
sp_ns_tcp_adapters_init(f);
memset(&admission, 0, sizeof(admission));
admission.key.flow.value = f->flow_id;
admission.key.generation.value = f->core_gen;
admission.now.value = now;
admission.tuple.local_addr = ntohl(srv_ip);
admission.tuple.peer_addr = ntohl(cli_ip);
admission.tuple.local_port = srv_port;
admission.tuple.peer_port = cli_port;
admission.policy_role = f->use_socks ? SP_TCP_ROLE_SOCKS
: SP_TCP_ROLE_DIRECT;
admission.open_role = SP_TCP_OPEN_PASSIVE;
{
/* TCP ISNs remain unpredictable; backend creation needs no IPC token. */
uint32_t iss = 0;
if (sp_random_exact_nonblocking(&iss, sizeof(iss)) < 0) {
sp_counter_inc(&g_resource_counters.tcp_flow_rejections);
send_tcp_rst(tunfd, srv_ip, cli_ip, srv_port, cli_port, 0,
tcp_seq_add_len(tcp->seq, 1));
sp_ns_flow_release(f);
g_ns_tunfd_for_emit = -1;
return 1;
}
admission.local_iss = iss;
}
admission.local_rcv_wscale = SP_TCP_RCV_WSCALE;
admission.needs_connect = 1;
admission.passive_syn_present = 1;
admission.peer_isn = tcp->seq;
admission.peer_window = tcp->window;
admission.peer_mss = tcp->mss;
admission.peer_mss_present = tcp->has_mss;
admission.peer_ws_present = tcp->has_window_scale;
admission.peer_ws = tcp->window_scale;
if (sp_ns_pump_start(f, sp_pump_item_admission(&admission), 0, 1) < 0) {
send_tcp_rst(tunfd, srv_ip, cli_ip, srv_port, cli_port, 0,
tcp_seq_add_len(tcp->seq, 1));
sp_ns_flow_release(f);
}
g_ns_tunfd_for_emit = -1;
return 1;
}
/* Existing flow: feed the segment to the core. Child payload is staged into
* the global ingress pool first so the core can authorize an exact slice against
* it; if the core does not authorize it (a duplicate or out-of-window
* retransmit produces a current-ACK, not a STREAM_SEND), the orphaned slot is
* freed immediately after the pump so a burst of retransmits cannot exhaust
* K_rx and wedge the flow. */
f->last_active_ns = sp_ns_now_ns();
sp_ns_timer_refresh(f);
struct sp_ev_tcp_segment segment = {0};
segment.context.key.flow.value = f->flow_id;
segment.context.key.generation.value = f->core_gen;
segment.context.now.value = now;
segment.context.causal_fence.value = f->egress.next_control_seq;
segment.seq = tcp->seq;
segment.ack = tcp->ack;
segment.window = tcp->window;
segment.flags = flags;
segment.payload_length.value = (uint32_t)payload_len;
uint64_t staged_id = 0;
if (payload_len > 0) {
/* A queued asynchronous packet retains its exact TCP interval; the
* completion-priority pump ordering prevents it from overtaking the event
* that advanced the authorization frontier. */
int admit = sp_ingress_stage_admit(&g_ns_stage_pool, &f->ingress,
sp_tcp_unwrap_u32(tcp->seq,
f->local_core.rcv_acked_abs),
pkt + payload_off, payload_len,
&staged_id);
if (admit == 1)
segment.ingress.value = staged_id;
else if (admit == 0) {
/* Global/K_rx staging exhausted: drop the segment; the child retransmits.
* Pump its ACK/window/RST metadata without granting the unstaged bytes.
* FIN is sequenced after the payload and must be retried with it. */
staged_id = 0;
sp_ns_drop_unstaged_payload(&segment);
}
}
if (sp_ns_pump_start(f, sp_pump_item_segment(&segment), staged_id, 0) < 0) {
sp_ns_teardown(f);
g_ns_tunfd_for_emit = -1;
return 1;
}
sp_ns_maybe_park_timewait(f);
g_ns_tunfd_for_emit = -1;
return 1;
}
/* Backend socket readiness for a core-adapter flow. */
/* Resets the child side of a core flow and releases it, so a backend failure
* during or after connect promptly closes the child rather than leaving its
* connect()/socket to time out. */
static void sp_ns_reset_child(struct sp_ns_flow *f) {
uint32_t ack = f->local_core.rcv_acked_abs != 0 ? (uint32_t)f->local_core.rcv_acked_abs
: 0;
/* RST sequence differs by peer state. A synchronized (established) child
* accepts a RST only when its sequence equals the peer's rcv.nxt โ€” our
* highest sent sequence, snd_nxt, per RFC 5961 (snd_una was silently
* challenged and ignored). A child still in SYN-SENT has no rcv.nxt and
* accepts on a valid ACK alone; the proven refusal form (matching the
* transitional path) uses sequence 0, which resets it immediately. */
if (g_ns_tunfd_for_emit >= 0) {
uint32_t seq = f->established ? (uint32_t)f->local_core.snd_nxt_high_abs : 0;
send_tcp_rst(g_ns_tunfd_for_emit, f->srv_ip, f->cli_ip, f->srv_port,
f->cli_port, seq, ack);
}
sp_ns_flow_release(f);
}
/* Tears down a flow after the pump returned -1. A graceful close (both FINs
* exchanged โ€” the core reached a mutual-FIN terminal) is released quietly; every
* other termination (abort, connect failure, handshake/RTO/resource deadline,
* fatal) resets the child. The pump records which in f->pump_close_graceful. */
static void sp_ns_teardown(struct sp_ns_flow *f) {
if (f->pump_close_graceful) {
if (f->local_core.state == SP_TCP_CORE_TIME_WAIT) {
/* Preserve the quarantine even if the table is temporarily full. */
if (sp_ns_park_timewait(f) != 1)
return;
}
sp_ns_flow_release(f);
} else {
sp_ns_reset_child(f);
}
}
static void sp_ns_backend_event(struct sp_ns_flow *f, uint32_t events) {
uint64_t now = sp_ns_now_ns();
f->last_active_ns = sp_ns_now_ns();
sp_ns_timer_refresh(f);
if (((events & EPOLLERR) != 0 ||
(!f->established && (events & EPOLLHUP) != 0)) &&
(events & EPOLLIN) == 0) {
sp_ns_reset_child(f);
return;
}
/* A SOCKS proxy has a second, bounded connect phase: the proxy TCP connect
* completes first, then the SOCKS greeting/request must reach READY before
* the core sees STREAM_CONNECT_RESULT. */
if (f->use_socks && !f->established) {
if (f->cold->socks.connect_pending && (events & EPOLLOUT) != 0) {
int err = 0;
socklen_t elen = sizeof(err);
if (getsockopt(f->backend_fd, SOL_SOCKET, SO_ERROR, &err, &elen) < 0 ||
err != 0) {
sp_ns_reset_child(f);
return;
}
f->cold->socks.connect_pending = 0;
if (socks_begin_handshake(&f->cold->socks, &socks_proxy) < 0) {
sp_ns_reset_child(f);
return;
}
}
if ((events & EPOLLOUT) != 0 && socks_has_pending_tx(&f->cold->socks) &&
socks_flush_tx(f->backend_fd, &f->cold->socks) < 0) {
sp_ns_reset_child(f);
return;
}
if ((events & EPOLLIN) != 0) {
int rc = socks_recv_and_process(f->backend_fd, &f->cold->socks, &socks_proxy,
NULL);
if (rc < 0) {
sp_ns_reset_child(f);
return;
}
if (rc == 1 && f->cold->socks.state == SOCKS_IO_READY) {
struct sp_ev_connect_result result = {0};
sp_tcp_event_context_init(&result.context, f->flow_id, f->core_gen,
now);
result.effect.value = f->connect_effect_id;
result.outcome = SP_CONNECT_SUCCEEDED;
f->connect_effect_id = 0;
f->established = 1;
if (sp_ns_pump_start(f, sp_pump_item_connect_result(&result), 0, 0) < 0) {
sp_ns_teardown(f);
return;
}
/*
* A single recvmsg may contain the end of the SOCKS CONNECT reply and
* the first application bytes. The parser deliberately leaves those
* bytes in rxbuf. Publish them as the first logical backend-stream
* range before rearming EPOLLIN; otherwise they are stranded forever
* and later socket bytes would be assigned the wrong stream offsets.
*/
if (f->cold->socks.rx_len != 0) {
size_t residual_len = f->cold->socks.rx_len;
if (residual_len > sizeof(f->cold->socks_residual) ||
f->egress.stream_tail != 0 ||
f->egress.stream_consumed != 0) {
sp_ns_reset_child(f);
return;
}
memcpy(f->cold->socks_residual, f->cold->socks.rxbuf, residual_len);
f->cold->socks_residual_len = residual_len;
f->socks_residual_active = 1;
f->cold->socks.rx_len = 0;
}
}
}
if (sp_ns_update_backend_interest(f) < 0)
sp_ns_reset_child(f);
return;
}
/* Direct connect completion: the backend became writable. */
if (f->connect_effect_id != 0 && (events & EPOLLOUT) != 0) {
struct sp_ev_connect_result result = {0};
int err = 0;
int pump_rc;
socklen_t elen = sizeof(err);
if (getsockopt(f->backend_fd, SOL_SOCKET, SO_ERROR, &err, &elen) < 0 ||
err != 0) {
sp_ns_reset_child(f);
return;
}
sp_tcp_event_context_init(&result.context, f->flow_id, f->core_gen, now);
result.effect.value = f->connect_effect_id;
result.outcome = SP_CONNECT_SUCCEEDED;
f->connect_effect_id = 0;
f->established = 1;
pump_rc = sp_ns_pump_start(f, sp_pump_item_connect_result(&result), 0, 0);
if (pump_rc < 0) {
sp_ns_teardown(f);
return;
}
sp_ns_maybe_park_timewait(f);
if (f->in_use && sp_ns_update_backend_interest(f) < 0)
sp_ns_reset_child(f);
return;
}
if (!f->established)
return;
/* Backend readable: observe newly available bytes and hand the core a CREDIT
* (or STREAM_EOF). The observe path leaves authoritative bytes in the socket;
* the core will authorize a TCP_EMIT_BATCH that the pump materializes. */
if ((events & (EPOLLIN | EPOLLRDHUP | EPOLLHUP)) != 0) {
struct sp_ev_credit credit;
struct sp_ev_stream_eof eof;
int rc = sp_egress_observe_backend_typed(
&f->egress, f->backend_fd, now,
(events & (EPOLLRDHUP | EPOLLHUP)) != 0, 0,
SP_TCP_FLIGHT_ALL, SP_EFFECT_BYTES, g_ns_discard, sizeof(g_ns_discard),
g_ns_payload, sizeof(g_ns_payload), &credit, &eof);
if (rc < 0) {
DBG("observe_backend fatal (%s)", strerror(errno));
sp_ns_reset_child(f);
return;
}
/* No progress (no new backend bytes) marks the flow idle so a subsequent
* interest recompute can suppress a fruitless EPOLLIN spin; any progress
* clears it. */
f->backend_read_idle = (rc == 0);
if (rc == SP_EGRESS_OBSERVATION_CREDIT || rc == SP_EGRESS_OBSERVATION_EOF) {
int pump_rc;
/* Observe only fills stream_tail; grant the core's logical emit credit
* here, while live scheduler admission and DRR meter the physical records. */
if (rc == SP_EGRESS_OBSERVATION_CREDIT) {
credit.tun_segments.value = SP_TCP_CORE_MAX_SEGMENTS;
pump_rc = sp_ns_pump_start(f, sp_pump_item_credit(&credit), 0, 0);
} else {
pump_rc = sp_ns_pump_start(f, sp_pump_item_stream_eof(&eof), 0, 0);
}
if (pump_rc < 0) {
sp_ns_teardown(f);
return;
}
sp_ns_maybe_park_timewait(f);
}
}
/* Backend writable: re-flush any durable ingress reservation. A CREDIT with an
* unchanged stream_tail re-drives the core's [A,P) reservation reflush once
* the FIFO has drained. */
if ((events & EPOLLOUT) != 0 && f->ingress.pending_bytes > 0) {
struct sp_ev_credit credit = {0};
int pump_rc;
credit.context.key.flow.value = f->flow_id;
credit.context.key.generation.value = f->core_gen;
credit.context.now.value = now;
credit.stream_tail.value = f->egress.stream_tail;
pump_rc = sp_ns_pump_start(f, sp_pump_item_credit(&credit), 0, 0);
if (pump_rc < 0) {
sp_ns_teardown(f);
return;
}
sp_ns_maybe_park_timewait(f);
}
if (f->in_use && sp_ns_update_backend_interest(f) < 0)
sp_ns_reset_child(f);
}
/* Re-execute a flow's retained TUN emit once the output FIFO drained.
*
* A blocked emit produced no sp_ev_tun_emit_result, so the core still holds the
* matching tun_emit_effect and advanced no send-state. We therefore re-run the
* *same* effect (not a re-derived one): re-materialize the identical byte range
* (still peekable โ€” the child has not ACKed, so nothing was consumed) and write
* it. On success the result is fed back, which advances snd-state, clears the
* core's pending effect, and may emit the next batch (itself possibly deferred
* again). On a still-full FIFO the effect stays retained for the next drain. */
static void sp_ns_retry_pending_emit(int tunfd, struct sp_ns_flow *f) {
struct sp_fx_tcp_emit_batch effect = f->pending_emit;
struct sp_ns_emit_transaction emit_txn = {
.flow = f, .effect = &effect, .tunfd = tunfd};
struct sp_ev_tun_emit_result emit_result;
uint64_t now = sp_ns_now_ns();
uint64_t saved_next_control_seq = f->egress.next_control_seq;
int preserve_scheduler_result = f->has_sched_emit_result;
int rc;
if (f->waits_sched_credit) {
struct sp_scheduler_credit credit = sp_scheduler_credit_now(&g_ns_tun_sched);
if (!sp_scheduler_credit_permits_retry(&f->sched_blocked_credit, &credit,
f->sched_blocked_dims))
return;
f->waits_sched_credit = 0;
f->sched_blocked_dims = 0;
}
g_ns_tunfd_for_emit = tunfd;
rc = sp_egress_materialize_emit(
/* A retained effect is re-materialized after a local TUN-backpressure
* interval, not at the original tail-observation point. Always take the
* explicit bounded path: retrying the third GSO batch through SO_PEEK_OFF
* reproduced the same missing-range symptom as post-consume emission. */
&f->egress, f->backend_fd, &effect, 1,
g_ns_discard, sizeof(g_ns_discard), g_ns_payload,
sizeof(g_ns_payload));
if (rc <= 0) {
if (rc < 0) {
DBG("retained-effect materialize fatal (%s)", strerror(errno));
sp_ns_teardown(f);
}
g_ns_tunfd_for_emit = -1;
return;
}
rc = sp_egress_execute_emit_with_typed(
&f->egress, sp_ns_tun_write, &emit_txn, &effect, ntohl(f->srv_ip),
ntohl(f->cli_ip), f->srv_port, f->cli_port, g_ns_payload,
(size_t)(effect.end.value - effect.start.value), 1, now, &emit_result);
if (preserve_scheduler_result) {
/* The original GSO admission already produced the one completion sequence
* held for this core effect. Scalar re-execution is write-only: preserve
* that event and roll back the executor sequence consumed by the duplicate
* completion, or the core observes a causal-sequence gap. */
f->egress.next_control_seq = saved_next_control_seq;
}
if (rc < 0) {
DBG("retained-effect emit fatal (%s)", strerror(errno));
sp_ns_teardown(f);
g_ns_tunfd_for_emit = -1;
return;
}
if (rc == 0) {
g_ns_tunfd_for_emit = -1; /* FIFO still full; keep the effect retained */
return;
}
f->has_pending_emit = 0;
memset(&f->pending_emit, 0, sizeof(f->pending_emit));
if (!preserve_scheduler_result) {
f->sched_emit_result = emit_result;
f->has_sched_emit_result = 1;
}
sp_ns_output_work_refresh(f);
g_ns_tunfd_for_emit = -1;
}
/* Re-drive every flow whose emit was FIFO-blocked, after the TUN FIFO drained.
* Each retry may itself release the flow, so re-check in_use each iteration. */
static void sp_ns_drain_pending_emits(int tunfd) {
static size_t cursor;
size_t limit = sp_ns_tcp_slot_limit();
size_t start = cursor < limit ? cursor : 0;
for (unsigned pass = 0; pass < 2; ++pass) {
size_t end = pass == 0 ? limit : start;
for (size_t slot = sp_ns_output_work_next(pass == 0 ? start : 0);
slot < end; slot = sp_ns_output_work_next(slot + 1)) {
if (g_tun_queue.count == TUN_QUEUE_PACKETS)
return;
cursor = (slot + 1) % limit;
if (g_ns_flows[slot].in_use && g_ns_flows[slot].has_pending_emit)
sp_ns_retry_pending_emit(tunfd, &g_ns_flows[slot]);
}
}
}
/* The broker's next wake-up: the earliest netstack deadline, or no deadline
* at all (UINT64_MAX) when the loop is event-driven. bounded_poll adds the
* EPOLL_TIMEOUT_MS cap for work no fd will announce: a child without a pidfd,
* or output queued for a destination that cannot be polled (O6). */
static uint64_t sp_ns_wait_deadline_ns(uint64_t now_ns, int bounded_poll) {
uint64_t deadline = UINT64_MAX;
uint64_t core_deadline = sp_deadline_next(&g_net_deadlines);
if (bounded_poll) {
uint64_t poll_ns = (uint64_t)EPOLL_TIMEOUT_MS * 1000000ULL;
deadline = UINT64_MAX - now_ns < poll_ns ? UINT64_MAX : now_ns + poll_ns;
}
if (core_deadline != 0 && core_deadline < deadline)
deadline = core_deadline;
return deadline;
}
/* Signal mask in effect only while the broker waits: the broker keeps
* SIGWINCH blocked otherwise, so a resize that arrives after the loop checked
* its flag interrupts the wait instead of being missed (O6). */
static sigset_t g_broker_wait_mask;
static int g_broker_wait_mask_set;
/* deadline_ns UINT64_MAX waits without a timeout. */
static int sp_epoll_wait_until(int epfd, struct epoll_event *events,
int maxevents, uint64_t deadline_ns,
uint64_t now_ns) {
uint64_t relative_ns = sp_tcp_ns_until(deadline_ns, now_ns);
const sigset_t *mask = g_broker_wait_mask_set ? &g_broker_wait_mask : NULL;
#ifdef __NR_epoll_pwait2
{
struct timespec timeout = {
.tv_sec = (time_t)(relative_ns / 1000000000ULL),
.tv_nsec = (long)(relative_ns % 1000000000ULL),
};
int result = (int)syscall(__NR_epoll_pwait2, epfd, events, maxevents,
deadline_ns == UINT64_MAX ? NULL : &timeout,
mask, mask != NULL ? (size_t)(_NSIG / 8) : 0);
if (result >= 0 || errno != ENOSYS)
return result;
}
#endif
{
uint64_t timeout_ms = relative_ns / 1000000ULL;
if (relative_ns % 1000000ULL != 0)
timeout_ms++;
if (timeout_ms > (uint64_t)INT_MAX)
timeout_ms = INT_MAX;
return epoll_pwait(epfd, events, maxevents,
deadline_ns == UINT64_MAX ? -1 : (int)timeout_ms, mask);
}
}
/* Fragment rejection is a packet-path diagnostic, so keep one bounded sample
* policy for both scalar and VNET records. The aggregate counter remains the
* complete evidence product; stderr is an intentionally synchronous sink. */
static uint64_t g_fragment_log_next_ns;
static void sp_log_fragment_drop(uint16_t frag_off) {
const uint64_t interval_ns = UINT64_C(1000000000);
uint64_t now_ns = sp_ns_now_ns();
if (g_fragment_log_next_ns != 0 && now_ns < g_fragment_log_next_ns)
return;
(void)sp_stderr_printf(
"[sockpuppet] IP: dropping IP fragment (frag_off=0x%04x)\n", frag_off);
g_fragment_log_next_ns =
UINT64_MAX - now_ns < interval_ns ? UINT64_MAX : now_ns + interval_ns;
}
static void dispatch_tun_ipv4_packet(int tunfd, uint8_t *pkt, size_t len) {
struct sp_packet_value parsed;
enum sp_codec_status status;
#define SP_TUN_REJECT() \
do { \
sp_counter_inc(&g_resource_counters.parser_rejects); \
return; \
} while (0)
status = sp_parse_ipv4_packet(pkt, len, 1, 1, &parsed);
if (status != SP_CODEC_OK) {
/* Keep the long-standing diagnostic for valid IPv4 fragments even though
* the shared parser rejects them before protocol dispatch. */
if (status == SP_CODEC_FRAGMENT) {
sp_counter_inc(&g_resource_counters.ipv4_fragment_drops);
sp_log_fragment_drop(parsed.ip.frag_off);
}
SP_TUN_REJECT();
}
if (!sp_policy_child_destination_allowed(&g_net_policy, &parsed))
SP_TUN_REJECT();
if (parsed.ip.protocol == IPPROTO_TCP) {
(void)handle_core_tcp(tunfd, pkt, len, &parsed);
return;
}
if (parsed.ip.protocol == IPPROTO_UDP) {
handle_udp(tunfd, pkt, len, &parsed);
return;
}
SP_TUN_REJECT();
#undef SP_TUN_REJECT
}
static void dispatch_tun_record(int tunfd, uint8_t *record, size_t len) {
struct sp_packet_value parsed;
struct sp_scalar_packet expanded[SP_TCP_GSO_POLICY_SEGS];
size_t expanded_count = 0;
enum sp_codec_status status;
status = sp_parse_tun_frame(record, len, &parsed);
if (status != SP_CODEC_OK) {
if (status == SP_CODEC_FRAGMENT) {
sp_counter_inc(&g_resource_counters.ipv4_fragment_drops);
sp_log_fragment_drop(parsed.ip.frag_off);
}
sp_counter_inc(&g_resource_counters.parser_rejects);
return;
}
if (parsed.vnet.gso_type == SP_VNET_GSO_TCPV4) {
if (sp_gso_expand_oracle(record, len, expanded,
SP_TCP_GSO_POLICY_SEGS,
&expanded_count) != SP_CODEC_OK) {
sp_counter_inc(&g_resource_counters.parser_rejects);
sp_counter_inc(&g_resource_counters.tcp_gso_expand_rejects);
return;
}
sp_counter_inc(&g_resource_counters.tcp_gso_frames_child_to_host);
for (size_t i = 0; i < expanded_count; ++i)
dispatch_tun_ipv4_packet(tunfd, expanded[i].bytes, expanded[i].len);
return;
}
uint8_t *packet = record + parsed.packet_offset;
if (sp_normalize_vnet_scalar_checksum(packet, &parsed) < 0) {
sp_counter_inc(&g_resource_counters.parser_rejects);
return;
}
dispatch_tun_ipv4_packet(tunfd, packet, parsed.ip.total_len);
}
/* Close every UDP flow whose epoll interest update failed (F2). */
static void udp_close_epoll_failed_flows(void) {
g_udp_epoll_failed = 0;
for (int i = 0; i < udp_flow_limit; ++i)
if (udp_flows[i].used && udp_flows[i].epoll_failed)
udp_close_flow(&udp_flows[i]);
}
static void udp_close_flow(struct udp_flow *f) {
struct sp_udp_key key;
uint32_t generation;
if (f == NULL)
return;
generation = f->generation;
if (f->used) {
sp_udp_outbound_key(&key, f->core.role, f->cli_ip, f->cli_port, f->srv_ip,
f->srv_port);
sp_udp_table_remove(&key, f->generation);
}
if (f->admission_held)
sp_resource_admission_release_source(f->cli_ip,
sp_udp_admission_role(f->core.role));
if (f->global_admission_held)
sp_resource_admission_release_source(0, SP_UDP_ADMISSION_GLOBAL);
(void)sp_udp_runtime_step(&f->core, SP_UDP_CLOSE, 0, 0);
while (f->core.pending_count != 0) {
uint16_t slot = f->core.pending_slots[f->core.pending_head];
udp_pending_release(slot);
sp_udp_pop(&f->core);
}
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;
}
if (f->dns_mapping_ref.generation != 0)
(void)sp_dns_core_release(&g_dns_core, f->dns_mapping_ref,
sp_ns_now_ns());
memset(f, 0, sizeof(*f));
f->generation = generation;
f->tcp_ctrl = -1;
f->udp_relay = -1;
f->udp_staging = -1;
for (size_t i = 0; i < SP_UDP_PENDING_PER_FLOW; ++i)
f->core.pending_slots[i] = UINT16_MAX;
}
/* Retire bounded due work before taking another data budget. Removing the
* owner before dispatch lets a transition reschedule itself in place. */
static void sp_net_expire_due(int tunfd, uint64_t now_ns) {
for (unsigned served = 0; served < SP_TIMER_EVENTS_TURN; ++served) {
uint64_t deadline = sp_deadline_next(&g_net_deadlines);
if (!deadline || deadline > now_ns) break;
unsigned owner = g_net_deadlines.owners[0];
if (owner >= SP_DEADLINE_TIMEWAIT) {
sp_ns_timewait_remove(&g_timewait_map.entries[owner - SP_DEADLINE_TIMEWAIT]);
continue;
}
sp_deadline_set(&g_net_deadlines, owner, 0);
if (owner >= SP_DEADLINE_PUBLISH) {
struct publish_udp_flow *f = &publish_udp_flows[owner - SP_DEADLINE_PUBLISH];
if (f->used && sp_udp_runtime_step(&f->core, SP_UDP_EXPIRE, now_ns, 0))
publish_udp_close_flow(f);
} else if (owner >= SP_DEADLINE_UDP) {
struct udp_flow *f = &udp_flows[owner - SP_DEADLINE_UDP];
if (f->used && sp_udp_runtime_step(&f->core, SP_UDP_EXPIRE, now_ns, 0))
udp_close_flow(f);
} else {
struct sp_ns_flow *f = &g_ns_flows[owner];
enum sp_tcp_timer_kind kind = SP_TCP_TIMER_HANDSHAKE;
if (!f->in_use) continue;
deadline = sp_tcp_next_deadline_ns(&f->local_core, &kind);
g_ns_tunfd_for_emit = tunfd;
if (deadline && deadline <= now_ns) {
struct sp_ev_timer_fired event = {
.context = {.key = {.flow = {f->flow_id}, .generation = {f->core_gen}},
.now = {now_ns}},
.kind = kind, .deadline = {deadline},
.timer_generation = {f->local_core.timer_gen[kind]}};
if (sp_ns_pump_start(f, sp_pump_item_timer_fired(&event), 0, 0) < 0) sp_ns_teardown(f);
} else if (!deadline) {
sp_ns_reset_child(f);
}
g_ns_tunfd_for_emit = -1;
if (f->in_use) sp_ns_timer_refresh(f);
}
}
}
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);
/* pidfd_open() always sets close-on-exec. */
return pidfd;
#else
errno = ENOSYS;
return -1;
#endif
}
/* On reaping, sets *drain_deadline_ns: the CLOCK_MONOTONIC instant after
* which undelivered payload output is abandoned. */
static int broker_try_reap_child(pid_t pid, int *child_status,
uint64_t *drain_deadline_ns) {
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;
*drain_deadline_ns = sp_ns_now_ns() + SP_CHILD_EXIT_DRAIN_NS;
bench_mark("child_reaped", "event_loop", "ok", 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) {
int owned;
if (child_pidfd == NULL || *child_pidfd < 0)
return;
owned = sp_take_fd(child_pidfd);
if (child_pidfd_active != NULL && *child_pidfd_active) {
if (epoll_ctl(g_epfd, EPOLL_CTL_DEL, owned, NULL) < 0 &&
errno != EBADF && errno != ENOENT)
DBG("epoll_ctl DEL child pidfd failed: %s", strerror(errno));
*child_pidfd_active = 0;
}
if (close(owned) < 0)
DBG("close child pidfd failed: %s", strerror(errno));
}
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;
runtime->source_interest = (struct epoll_event){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 (sp_epoll_update(operation, runtime->source_fd, &source_event,
&runtime->source_interest) < 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;
runtime->destination_interest = (struct epoll_event){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 (sp_epoll_update(operation, runtime->destination.fd,
&destination_event, &runtime->destination_interest) < 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 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;