mirror of
https://github.com/google/nsjail.git
synced 2026-08-30 18:41:30 -07:00
nsjail 4.0: monitor thread modernization, nstun/sockproxy/unotify refactoring
Major changes: - Co-developed with gemini-cli (reviewed by a human) - Per-child epoll-based monitor thread with thread_local context - Replaced fixed-size arrays with STL containers (unordered_map, vector) - Zero-copy splice proxy for listen mode (sockproxy) - Seccomp unotify integrated into epoll loop (no dedicated thread) - nstun: SOCKS5/HTTP CONNECT proxy, MemPool allocator, IPv6 support - close_range() for FD cleanup - Test coverage for nstun standalone/listen modes
This commit is contained in:
139
Makefile
139
Makefile
@@ -30,10 +30,19 @@ COMMON_FLAGS += -O2 -c \
|
||||
-Ikafel/include
|
||||
|
||||
CXXFLAGS += $(USER_DEFINES) $(COMMON_FLAGS) $(PROTOBUF_CFLAGS) -I. \
|
||||
-std=c++20 -fno-exceptions -Wno-unused -Wno-unused-parameter -Wno-c99-designator
|
||||
-std=c++20 -fno-exceptions -Wno-unused -Wno-unused-parameter
|
||||
|
||||
ifneq ($(findstring clang,$(CXX)),)
|
||||
CXXFLAGS += -Wno-c99-designator
|
||||
endif
|
||||
|
||||
LDFLAGS += -pie -Wl,-z,noexecstack -lpthread $(PROTOBUF_LIBS)
|
||||
|
||||
ifdef USE_ASAN
|
||||
COMMON_FLAGS += -fsanitize=address
|
||||
LDFLAGS += -fsanitize=address
|
||||
endif
|
||||
|
||||
ifeq ($(NL3_EXISTS), yes)
|
||||
CXXFLAGS += $(shell pkg-config --cflags libnl-route-3.0)
|
||||
LDFLAGS += $(shell pkg-config --libs libnl-route-3.0)
|
||||
@@ -61,7 +70,7 @@ ifneq ($(PASTA_BIN_PATH),)
|
||||
CXXFLAGS += -DPASTA_BIN_PATH='"$(PASTA_BIN_PATH)"'
|
||||
endif
|
||||
|
||||
SRCS_CXX = caps.cc cgroup.cc cgroup2.cc cmdline.cc config.cc contain.cc cpu.cc logs.cc mnt.cc mnt_legacy.cc mnt_newapi.cc net.cc nsjail.cc pid.cc sandbox.cc subproc.cc uts.cc user.cc unotify/unotify.cc unotify/stats.cc unotify/syscall.cc util.cc nstun/nstun.cc nstun/policy.cc nstun/encap.cc nstun/iface.cc nstun/tun.cc nstun/ip.cc nstun/icmp.cc nstun/udp.cc nstun/tcp.cc
|
||||
SRCS_CXX = monitor.cc sockproxy/sockproxy.cc caps.cc cgroup.cc cgroup2.cc cmdline.cc config.cc contain.cc cpu.cc logs.cc mnt.cc mnt_legacy.cc mnt_newapi.cc net.cc nsjail.cc pid.cc sandbox.cc subproc.cc uts.cc user.cc unotify/unotify.cc unotify/stats.cc unotify/syscall.cc util.cc nstun/nstun.cc nstun/policy.cc nstun/encap.cc nstun/iface.cc nstun/tun.cc nstun/ip.cc nstun/icmp.cc nstun/udp.cc nstun/tcp.cc
|
||||
SRCS_PROTO = config.proto unotify/unotify.proto
|
||||
|
||||
SRCS_PB_CXX = $(SRCS_PROTO:.proto=.pb.cc)
|
||||
@@ -185,8 +194,12 @@ test: $(BIN)
|
||||
$(call run_test, ./nsjail --config tests/connect.cfg -q -t 3 -- /bin/bash -c 'host -T dns.google 8.8.8.8 && exit 77', 77)
|
||||
$(call run_test, ./nsjail --config tests/connect.cfg -q -t 3 -- /bin/bash -c 'host -T dns.google 2001:4860:4860::8888 && exit 77', 77)
|
||||
|
||||
# --- Nstun standalone / proxy mode tests ---
|
||||
$(call run_test, ./nsjail --config tests/nstun.cfg -Mo -q -t 2 --seccomp_unotify -- /bin/bash -c 'exit 77', 77)
|
||||
$(call run_test, { ./nsjail --config tests/nstun.cfg -Ml --port 31338 -q -t 5 --seccomp_unotify -- /bin/bash -c "sleep 10" & }; sleep 2; echo -n "GET / HTTP/1.0\r\n\r\n" | nc 127.0.0.1 31338 >/dev/null 2>&1 && exit 77, 77)
|
||||
|
||||
# --- HOST_TO_GUEST TCP inbound proxy test (IPv4 + IPv6) ---
|
||||
$(call run_test, { ./nsjail --config tests/dns_http_host_to_guest.cfg -q -t 3 & }; sleep 1; wget -4 -q -O /dev/null --timeout=5 http://127.0.0.1:8080/ && wget -6 -q -O /dev/null --timeout=5 http://[::1]:8080/ && exit 77, 77)
|
||||
$(call run_test, { ./nsjail --config tests/dns_http_host_to_guest.cfg -q -t 5 & }; sleep 2; wget -4 -q -O /dev/null --timeout=5 http://127.0.0.1:8080/ && wget -6 -q -O /dev/null --timeout=5 http://[::1]:8080/ && exit 77, 77)
|
||||
|
||||
# --- --experimental_mnt=old ---
|
||||
$(call run_test, ./nsjail $(OLD_EF) -q -Mo --rw --chroot / --user 99999 --group 99999 -- /bin/bash -c 'touch $(HOME)/nsjail_test && exit 77', 77)
|
||||
@@ -202,12 +215,12 @@ test: $(BIN)
|
||||
$(call run_test, rm -f /run/user/$(UID)/nsjail_test2, 0)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/bash-with-fake-geteuid.cfg -q -t 1 < /dev/null, 0)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/bash-with-fake-geteuid.json -q -t 1 < /dev/null, 0)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/static-busybox-with-execveat.cfg -q -t 1, 137)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/static-busybox-with-execveat.cfg -q -t 1 < /dev/null, 0)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/home-documents-with-xorg-no-net.cfg -q -- /bin/true, 0)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/home-documents-with-xorg-no-net.cfg -q -- /bin/false, 1)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/firefox-with-net-X11.cfg -q -t 2, 137)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/firefox-with-net-wayland.cfg -q -t 2, 137)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/chromium-with-net-wayland.cfg -q -t 2, 137)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/firefox-with-net-X11.cfg -q -t 3, 137)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/firefox-with-net-wayland.cfg -q -t 3, 137)
|
||||
$(call run_test, ./nsjail $(OLD_EF) --config configs/chromium-with-net-wayland.cfg -q -t 5, 137)
|
||||
|
||||
# --- --experimental_mnt=new ---
|
||||
$(call run_test, ./nsjail $(NEW_EF) -q -Mo --rw --chroot / --user 99999 --group 99999 -- /bin/bash -c 'touch $(HOME)/nsjail_test && exit 77', 77)
|
||||
@@ -223,12 +236,12 @@ test: $(BIN)
|
||||
$(call run_test, rm -f /run/user/$(UID)/nsjail_test2, 0)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/bash-with-fake-geteuid.cfg -q -t 1 < /dev/null, 0)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/bash-with-fake-geteuid.json -q -t 1 < /dev/null, 0)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/static-busybox-with-execveat.cfg -q -t 1, 137)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/static-busybox-with-execveat.cfg -q -t 1 < /dev/null, 0)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/home-documents-with-xorg-no-net.cfg -q -- /bin/true, 0)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/home-documents-with-xorg-no-net.cfg -q -- /bin/false, 1)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/firefox-with-net-X11.cfg -q -t 2, 137)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/firefox-with-net-wayland.cfg -q -t 2, 137)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/chromium-with-net-wayland.cfg -q -t 2, 137)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/firefox-with-net-X11.cfg -q -t 3, 137)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/firefox-with-net-wayland.cfg -q -t 3, 137)
|
||||
$(call run_test, ./nsjail $(NEW_EF) --config configs/chromium-with-net-wayland.cfg -q -t 5, 137)
|
||||
|
||||
@echo ""
|
||||
@echo "========================================"
|
||||
@@ -239,61 +252,75 @@ test: $(BIN)
|
||||
# Dependencies (Generated by makedepend)
|
||||
# DO NOT DELETE THIS LINE -- make depend depends on it.
|
||||
|
||||
caps.o: caps.h nsjail.h config.pb.h logs.h macros.h util.h
|
||||
cgroup.o: cgroup.h nsjail.h config.pb.h logs.h util.h
|
||||
cgroup2.o: cgroup2.h nsjail.h config.pb.h logs.h util.h
|
||||
monitor.o: monitor.h logs.h macros.h missing_defs.h net.h nsjail.h
|
||||
monitor.o: config.pb.h nstun/nstun.h sockproxy/sockproxy.h subproc.h
|
||||
monitor.o: unotify/unotify.h util.h
|
||||
sockproxy/sockproxy.o: sockproxy/sockproxy.h logs.h monitor.h util.h
|
||||
sockproxy/sockproxy.o: missing_defs.h nsjail.h config.pb.h
|
||||
caps.o: caps.h nsjail.h config.pb.h logs.h macros.h missing_defs.h util.h
|
||||
cgroup.o: cgroup.h nsjail.h config.pb.h logs.h util.h missing_defs.h
|
||||
cgroup2.o: cgroup2.h nsjail.h config.pb.h logs.h util.h missing_defs.h
|
||||
cmdline.o: cmdline.h nsjail.h config.pb.h caps.h config.h logs.h macros.h
|
||||
cmdline.o: mnt.h mnt_newapi.h user.h util.h
|
||||
cmdline.o: missing_defs.h mnt.h mnt_newapi.h user.h util.h
|
||||
config.o: config.h nsjail.h config.pb.h caps.h cmdline.h logs.h macros.h
|
||||
config.o: mnt.h user.h util.h
|
||||
config.o: missing_defs.h mnt.h user.h util.h
|
||||
contain.o: contain.h nsjail.h config.pb.h caps.h cgroup.h cgroup2.h config.h
|
||||
contain.o: cpu.h logs.h macros.h mnt.h net.h pid.h user.h util.h uts.h
|
||||
cpu.o: cpu.h nsjail.h config.pb.h logs.h util.h
|
||||
logs.o: logs.h macros.h util.h nsjail.h config.pb.h
|
||||
mnt.o: mnt.h nsjail.h config.pb.h logs.h macros.h mnt_legacy.h mnt_newapi.h
|
||||
mnt.o: subproc.h util.h
|
||||
mnt_legacy.o: mnt_legacy.h mnt.h nsjail.h config.pb.h logs.h macros.h util.h
|
||||
mnt_newapi.o: mnt_newapi.h mnt.h nsjail.h config.pb.h logs.h util.h
|
||||
net.o: net.h nsjail.h config.pb.h logs.h macros.h nstun/nstun.h util.h
|
||||
nsjail.o: nsjail.h config.pb.h cgroup2.h cmdline.h logs.h macros.h net.h
|
||||
nsjail.o: sandbox.h subproc.h unotify/unotify.h util.h
|
||||
pid.o: pid.h nsjail.h config.pb.h logs.h subproc.h
|
||||
sandbox.o: sandbox.h nsjail.h config.pb.h subproc.h kafel/include/kafel.h
|
||||
sandbox.o: logs.h unotify/syscall_defs.h util.h
|
||||
subproc.o: subproc.h nsjail.h config.pb.h cgroup.h cgroup2.h contain.h logs.h
|
||||
subproc.o: macros.h net.h nstun/nstun.h sandbox.h unotify/unotify.h user.h
|
||||
subproc.o: util.h
|
||||
contain.o: cpu.h logs.h macros.h missing_defs.h mnt.h net.h monitor.h pid.h
|
||||
contain.o: user.h util.h uts.h
|
||||
cpu.o: cpu.h nsjail.h config.pb.h logs.h util.h missing_defs.h
|
||||
logs.o: logs.h macros.h util.h missing_defs.h nsjail.h config.pb.h
|
||||
mnt.o: mnt.h missing_defs.h nsjail.h config.pb.h logs.h macros.h mnt_legacy.h
|
||||
mnt.o: mnt_newapi.h subproc.h monitor.h util.h
|
||||
mnt_legacy.o: mnt_legacy.h mnt.h missing_defs.h nsjail.h config.pb.h logs.h
|
||||
mnt_legacy.o: macros.h util.h
|
||||
mnt_newapi.o: mnt_newapi.h mnt.h missing_defs.h nsjail.h config.pb.h logs.h
|
||||
mnt_newapi.o: macros.h util.h
|
||||
net.o: net.h monitor.h nsjail.h config.pb.h logs.h macros.h missing_defs.h
|
||||
net.o: nstun/nstun.h subproc.h util.h
|
||||
nsjail.o: nsjail.h config.pb.h cgroup2.h cmdline.h logs.h macros.h
|
||||
nsjail.o: missing_defs.h monitor.h net.h sandbox.h subproc.h unotify/stats.h
|
||||
nsjail.o: unotify/record.h unotify/unotify.pb.h util.h
|
||||
pid.o: pid.h nsjail.h config.pb.h logs.h subproc.h monitor.h
|
||||
sandbox.o: sandbox.h nsjail.h config.pb.h subproc.h monitor.h
|
||||
sandbox.o: kafel/include/kafel.h logs.h missing_defs.h unotify/syscall_defs.h
|
||||
sandbox.o: missing_defs.h util.h
|
||||
subproc.o: subproc.h monitor.h nsjail.h config.pb.h cgroup.h cgroup2.h
|
||||
subproc.o: contain.h logs.h macros.h missing_defs.h net.h nstun/nstun.h
|
||||
subproc.o: sandbox.h user.h util.h
|
||||
uts.o: uts.h nsjail.h config.pb.h logs.h
|
||||
user.o: user.h nsjail.h config.pb.h logs.h macros.h subproc.h util.h
|
||||
user.o: user.h nsjail.h config.pb.h logs.h macros.h subproc.h monitor.h
|
||||
user.o: util.h missing_defs.h
|
||||
unotify/unotify.o: unotify/unotify.h nsjail.h config.pb.h logs.h
|
||||
unotify/unotify.o: unotify/record.h unotify/unotify.pb.h unotify/stats.h
|
||||
unotify/unotify.o: unotify/syscall.h util.h
|
||||
unotify/unotify.o: missing_defs.h monitor.h unotify/record.h
|
||||
unotify/unotify.o: unotify/unotify.pb.h unotify/stats.h unotify/syscall.h
|
||||
unotify/unotify.o: util.h
|
||||
unotify/stats.o: unotify/stats.h nsjail.h config.pb.h unotify/record.h
|
||||
unotify/stats.o: unotify/unotify.pb.h logs.h util.h
|
||||
unotify/stats.o: unotify/unotify.pb.h logs.h util.h missing_defs.h
|
||||
unotify/syscall.o: unotify/syscall.h unotify/record.h unotify/unotify.pb.h
|
||||
unotify/syscall.o: logs.h macros.h unotify/syscall_defs.h util.h nsjail.h
|
||||
unotify/syscall.o: config.pb.h
|
||||
util.o: util.h nsjail.h config.pb.h logs.h macros.h
|
||||
nstun/nstun.o: nstun/nstun.h nstun/core.h nstun/net_defs.h nstun/icmp.h
|
||||
nstun/nstun.o: nstun/iface.h nstun/ip.h logs.h macros.h nstun/policy.h
|
||||
nstun/nstun.o: nstun/tcp.h nstun/tun.h nstun/udp.h util.h nsjail.h
|
||||
nstun/nstun.o: config.pb.h
|
||||
nstun/policy.o: nstun/policy.h nstun/core.h nstun/net_defs.h nstun/nstun.h
|
||||
nstun/policy.o: logs.h config.pb.h nsjail.h
|
||||
nstun/encap.o: nstun/encap.h nstun/net_defs.h logs.h
|
||||
unotify/syscall.o: logs.h macros.h missing_defs.h unotify/syscall_defs.h
|
||||
unotify/syscall.o: missing_defs.h util.h nsjail.h config.pb.h
|
||||
util.o: util.h missing_defs.h nsjail.h config.pb.h logs.h macros.h
|
||||
nstun/nstun.o: nstun/nstun.h monitor.h nstun/core.h nstun/net_defs.h
|
||||
nstun/nstun.o: nstun/encap.h nstun/icmp.h nstun/iface.h nstun/ip.h logs.h
|
||||
nstun/nstun.o: macros.h nsjail.h config.pb.h nstun/policy.h nstun/tcp.h
|
||||
nstun/nstun.o: nstun/tun.h nstun/udp.h util.h missing_defs.h
|
||||
nstun/policy.o: nstun/policy.h config.pb.h nstun/core.h nstun/net_defs.h
|
||||
nstun/policy.o: nstun/nstun.h monitor.h nstun/encap.h logs.h nsjail.h
|
||||
nstun/encap.o: nstun/encap.h nstun/net_defs.h logs.h macros.h
|
||||
nstun/iface.o: nstun/iface.h logs.h macros.h nstun/net_defs.h nsjail.h
|
||||
nstun/iface.o: config.pb.h nstun/nstun.h
|
||||
nstun/iface.o: config.pb.h nstun/nstun.h monitor.h
|
||||
nstun/tun.o: nstun/tun.h nstun/core.h nstun/net_defs.h nstun/nstun.h
|
||||
nstun/tun.o: nstun/icmp.h nstun/ip.h logs.h
|
||||
nstun/ip.o: nstun/ip.h nstun/core.h nstun/net_defs.h nstun/nstun.h
|
||||
nstun/ip.o: nstun/icmp.h logs.h nstun/tcp.h nstun/udp.h
|
||||
nstun/icmp.o: nstun/icmp.h nstun/core.h nstun/net_defs.h nstun/nstun.h logs.h
|
||||
nstun/icmp.o: macros.h nstun/policy.h nstun/tun.h
|
||||
nstun/tun.o: monitor.h nstun/encap.h nstun/icmp.h nstun/ip.h logs.h
|
||||
nstun/ip.o: nstun/ip.h nstun/core.h nstun/net_defs.h nstun/nstun.h monitor.h
|
||||
nstun/ip.o: nstun/encap.h nstun/icmp.h logs.h nstun/tcp.h nstun/udp.h
|
||||
nstun/icmp.o: nstun/icmp.h nstun/core.h nstun/net_defs.h nstun/nstun.h
|
||||
nstun/icmp.o: monitor.h nstun/encap.h logs.h macros.h nstun/policy.h
|
||||
nstun/icmp.o: config.pb.h nstun/tun.h
|
||||
nstun/udp.o: nstun/udp.h nstun/core.h nstun/net_defs.h nstun/nstun.h
|
||||
nstun/udp.o: nstun/encap.h nstun/icmp.h logs.h macros.h nstun/policy.h
|
||||
nstun/udp.o: nstun/tun.h
|
||||
nstun/udp.o: monitor.h nstun/encap.h nstun/icmp.h logs.h macros.h
|
||||
nstun/udp.o: nstun/policy.h config.pb.h nstun/tun.h
|
||||
nstun/tcp.o: nstun/tcp.h nstun/core.h nstun/net_defs.h nstun/nstun.h
|
||||
nstun/tcp.o: nstun/encap.h logs.h macros.h nstun/policy.h nstun/tun.h util.h
|
||||
nstun/tcp.o: nsjail.h config.pb.h
|
||||
nstun/tcp.o: monitor.h nstun/encap.h logs.h macros.h nstun/policy.h
|
||||
nstun/tcp.o: config.pb.h nstun/tun.h util.h missing_defs.h nsjail.h
|
||||
config.pb.o: config.pb.h
|
||||
unotify/unotify.pb.o: unotify/unotify.pb.h
|
||||
|
||||
19
caps.cc
19
caps.cc
@@ -33,21 +33,9 @@
|
||||
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "util.h"
|
||||
|
||||
#if !defined(CAP_AUDIT_READ)
|
||||
#define CAP_AUDIT_READ 37
|
||||
#endif /* !defined(CAP_AUDIT_READ) */
|
||||
#if !defined(CAP_PERFMON)
|
||||
#define CAP_PERFMON 38
|
||||
#endif /* !defined(CAP_PERFMON) */
|
||||
#if !defined(CAP_BPF)
|
||||
#define CAP_BPF 39
|
||||
#endif /* !defined(CAP_BPF) */
|
||||
#if !defined(CAP_CHECKPOINT_RESTORE)
|
||||
#define CAP_CHECKPOINT_RESTORE 40
|
||||
#endif /* !defined(CAP_CHECKPOINT_RESTORE) */
|
||||
|
||||
namespace caps {
|
||||
|
||||
struct {
|
||||
@@ -174,11 +162,6 @@ static void setInheritable(cap_user_data_t cap_data, unsigned int cap) {
|
||||
cap_data[off_byte].inheritable |= mask;
|
||||
}
|
||||
|
||||
#if !defined(PR_CAP_AMBIENT)
|
||||
#define PR_CAP_AMBIENT 47
|
||||
#define PR_CAP_AMBIENT_RAISE 2
|
||||
#define PR_CAP_AMBIENT_CLEAR_ALL 4
|
||||
#endif /* !defined(PR_CAP_AMBIENT) */
|
||||
static bool initNsKeepCaps(cap_user_data_t cap_data) {
|
||||
/* Copy all permitted caps to the inheritable set */
|
||||
std::string dbgmsg1;
|
||||
|
||||
16
cmdline.cc
16
cmdline.cc
@@ -52,6 +52,7 @@
|
||||
#include "config.h"
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "mnt.h"
|
||||
#include "mnt_newapi.h"
|
||||
#include "user.h"
|
||||
@@ -405,13 +406,6 @@ static bool setupArgv(nsj_t* nsj, int argc, char** argv, int optind) {
|
||||
}
|
||||
|
||||
if (nsj->njc.exec_bin().exec_fd()) {
|
||||
#if !defined(__NR_execveat)
|
||||
LOG_E("Your nsjail is compiled without support for the execveat() "
|
||||
"syscall, "
|
||||
"yet you "
|
||||
"specified the --execute_fd flag");
|
||||
return false;
|
||||
#endif /* !defined(__NR_execveat) */
|
||||
if ((nsj->exec_fd = TEMP_FAILURE_RETRY(open(nsj->njc.exec_bin().path().c_str(),
|
||||
O_RDONLY | O_PATH | O_CLOEXEC))) == -1) {
|
||||
PLOG_W("Couldn't open %s file", QC(nsj->njc.exec_bin().path()));
|
||||
@@ -493,18 +487,18 @@ std::unique_ptr<nsj_t> parseArgs(int argc, char* argv[]) {
|
||||
|
||||
/* Generate options array for getopt_long. */
|
||||
const size_t options_length = ARR_SZ(custom_opts) + 1;
|
||||
struct option opts[options_length];
|
||||
std::vector<struct option> opts(options_length);
|
||||
for (unsigned i = 0; i < ARR_SZ(custom_opts); i++) {
|
||||
opts[i] = custom_opts[i].opt;
|
||||
}
|
||||
/* Lastly, NULL option as a terminator */
|
||||
struct option terminator = {NULL, 0, NULL, 0};
|
||||
memcpy(&opts[options_length - 1].name, &terminator, sizeof(terminator));
|
||||
opts[options_length - 1] = {nullptr, 0, nullptr, 0};
|
||||
|
||||
int opt_index = 0;
|
||||
for (;;) {
|
||||
int c = getopt_long(argc, argv,
|
||||
"x:H:D:C:c:p:i:u:g:l:L:t:M:NdvqQeh?E:R:B:T:m:s:P:I:U:G:", opts, &opt_index);
|
||||
"x:H:D:C:c:p:i:u:g:l:L:t:M:NdvqQeh?E:R:B:T:m:s:P:I:U:G:", opts.data(),
|
||||
&opt_index);
|
||||
if (c == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "config.pb.h"
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "mnt.h"
|
||||
#include "user.h"
|
||||
#include "util.h"
|
||||
@@ -156,7 +157,7 @@ static void logHandler(
|
||||
#endif /* NSJAIL_HAS_PROTOBUF_LOG_HANDLER */
|
||||
|
||||
static void flushLog() {
|
||||
for (auto message : error_messages) {
|
||||
for (const auto& message : error_messages) {
|
||||
LOG_W("ProtoTextFormat: %s", message.c_str());
|
||||
}
|
||||
error_messages.clear();
|
||||
|
||||
@@ -187,4 +187,5 @@ exec_bin {
|
||||
path: "/usr/lib/chromium/chromium"
|
||||
arg: "--ozone-platform=wayland"
|
||||
arg: "--incognito"
|
||||
arg: "https://fast.com"
|
||||
}
|
||||
|
||||
@@ -190,4 +190,5 @@ seccomp_string: "DEFAULT ALLOW"
|
||||
exec_bin {
|
||||
path: "/usr/lib/firefox/firefox"
|
||||
arg: "-private"
|
||||
arg: "https://fast.com"
|
||||
}
|
||||
|
||||
@@ -185,4 +185,5 @@ seccomp_string: "DEFAULT ALLOW"
|
||||
exec_bin {
|
||||
path: "/usr/lib/firefox/firefox"
|
||||
arg: "-private"
|
||||
arg: "https://fast.com"
|
||||
}
|
||||
|
||||
206
contain.cc
206
contain.cc
@@ -26,6 +26,9 @@
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#if __has_include(<linux/close_range.h>)
|
||||
#include <linux/close_range.h>
|
||||
#endif
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
@@ -46,6 +49,7 @@
|
||||
#include "cpu.h"
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "mnt.h"
|
||||
#include "net.h"
|
||||
#include "pid.h"
|
||||
@@ -76,9 +80,6 @@ static bool containInitCgroupNs(void) {
|
||||
}
|
||||
|
||||
static bool containDropPrivs(nsj_t* nsj) {
|
||||
#ifndef PR_SET_NO_NEW_PRIVS
|
||||
#define PR_SET_NO_NEW_PRIVS 38
|
||||
#endif
|
||||
if (!nsj->njc.disable_no_new_privs()) {
|
||||
if (prctl(PR_SET_NO_NEW_PRIVS, 1UL, 0UL, 0UL, 0UL) == -1) {
|
||||
/* Only new kernels support it */
|
||||
@@ -98,24 +99,24 @@ static bool containPrepareEnv(nsj_t* nsj) {
|
||||
PLOG_E("prctl(PR_SET_PDEATHSIG, SIGKILL)");
|
||||
return false;
|
||||
}
|
||||
unsigned long personality = 0;
|
||||
unsigned long pers = 0;
|
||||
if (nsj->njc.persona_addr_compat_layout()) {
|
||||
personality |= ADDR_COMPAT_LAYOUT;
|
||||
pers |= ADDR_COMPAT_LAYOUT;
|
||||
}
|
||||
if (nsj->njc.persona_mmap_page_zero()) {
|
||||
personality |= MMAP_PAGE_ZERO;
|
||||
pers |= MMAP_PAGE_ZERO;
|
||||
}
|
||||
if (nsj->njc.persona_read_implies_exec()) {
|
||||
personality |= READ_IMPLIES_EXEC;
|
||||
pers |= READ_IMPLIES_EXEC;
|
||||
}
|
||||
if (nsj->njc.persona_addr_limit_3gb()) {
|
||||
personality |= ADDR_LIMIT_3GB;
|
||||
pers |= ADDR_LIMIT_3GB;
|
||||
}
|
||||
if (nsj->njc.persona_addr_no_randomize()) {
|
||||
personality |= ADDR_NO_RANDOMIZE;
|
||||
pers |= ADDR_NO_RANDOMIZE;
|
||||
}
|
||||
if (personality && ::personality(personality) == -1) {
|
||||
PLOG_E("personality(%lx)", personality);
|
||||
if (pers && personality(pers) == -1) {
|
||||
PLOG_E("personality(%lx)", pers);
|
||||
return false;
|
||||
}
|
||||
LOG_D("setpriority(%d)", nsj->njc.nice_level());
|
||||
@@ -126,6 +127,7 @@ static bool containPrepareEnv(nsj_t* nsj) {
|
||||
if (!nsj->njc.skip_setsid()) {
|
||||
setsid();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -221,10 +223,10 @@ static bool containSetLimits(nsj_t* nsj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool containPassFd(nsj_t* nsj, int fd) {
|
||||
return (std::find(nsj->openfds.begin(), nsj->openfds.end(), fd) != nsj->openfds.end());
|
||||
}
|
||||
|
||||
/*
|
||||
* Marks FDs for close-on-exec, or clears it for FDs that should be passed.
|
||||
* exec(2) handles the actual closing.
|
||||
*/
|
||||
static bool containMakeFdCOE(int fd, bool pass_fd) {
|
||||
int flags = TEMP_FAILURE_RETRY(fcntl(fd, F_GETFD, 0));
|
||||
if (flags == -1) {
|
||||
@@ -252,100 +254,104 @@ static bool containMakeFdCOE(int fd, bool pass_fd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool containMakeFdsCOECloseRange(nsj_t* nsj) {
|
||||
RETURN_ON_FAILURE(util::makeRangeCOE(0U, ~0U));
|
||||
for (const auto fd : nsj->openfds) {
|
||||
RETURN_ON_FAILURE(containMakeFdCOE(fd, /* pass_fd= */ true));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool containMakeFdsCOENaive(nsj_t* nsj) {
|
||||
/*
|
||||
* Don't use getrlimit(RLIMIT_NOFILE) here, as it can return an artifically small value
|
||||
* (e.g. 32), which could be smaller than a maximum assigned number to file-descriptors
|
||||
* in this process. Just use some reasonably sane value (e.g. 1024)
|
||||
*/
|
||||
for (unsigned fd = 0; fd < 1024; fd++) {
|
||||
RETURN_ON_FAILURE(containMakeFdCOE(fd, containPassFd(nsj, fd)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool containMakeFdsCOEProc(nsj_t* nsj) {
|
||||
int dirfd = open("/proc/self/fd", O_DIRECTORY | O_RDONLY | O_CLOEXEC);
|
||||
if (dirfd == -1) {
|
||||
PLOG_D("open('/proc/self/fd', O_DIRECTORY|O_RDONLY|O_CLOEXEC)");
|
||||
return false;
|
||||
}
|
||||
DIR* dir = fdopendir(dirfd);
|
||||
if (dir == nullptr) {
|
||||
PLOG_W("fdopendir(fd=%d)", dirfd);
|
||||
close(dirfd);
|
||||
return false;
|
||||
}
|
||||
/* Make all fds above stderr close-on-exec */
|
||||
for (;;) {
|
||||
errno = 0;
|
||||
struct dirent* entry = readdir(dir);
|
||||
if (entry == nullptr && errno != 0) {
|
||||
PLOG_D("readdir('/proc/self/fd')");
|
||||
closedir(dir);
|
||||
return false;
|
||||
}
|
||||
if (entry == nullptr) {
|
||||
break;
|
||||
}
|
||||
if (util::StrEq(".", entry->d_name)) {
|
||||
continue;
|
||||
}
|
||||
if (util::StrEq("..", entry->d_name)) {
|
||||
continue;
|
||||
}
|
||||
errno = 0;
|
||||
int fd = strtoimax(entry->d_name, NULL, 10);
|
||||
if (errno != 0) {
|
||||
PLOG_W("Cannot convert /proc/self/fd/%s to a number", entry->d_name);
|
||||
continue;
|
||||
}
|
||||
int flags = TEMP_FAILURE_RETRY(fcntl(fd, F_GETFD, 0));
|
||||
if (flags == -1) {
|
||||
PLOG_D("fcntl(fd=%d, F_GETFD, 0)", fd);
|
||||
closedir(dir);
|
||||
return false;
|
||||
}
|
||||
RETURN_ON_FAILURE(containMakeFdCOE(fd, containPassFd(nsj, fd)));
|
||||
}
|
||||
closedir(dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets the Close-On-Exec (COE) flag on all file descriptors except those explicitly
|
||||
* marked to be passed to the sandbox.
|
||||
*
|
||||
* To optimize performance, we utilize the close_range() syscall. Because we are
|
||||
* single-threaded in the child process just before execve(), we can be _cheeky_ -
|
||||
* we blanket-apply CLOSE_RANGE_CLOEXEC to the entire descriptor table (0 to ~0U),
|
||||
* and then simply clear the flag on the exact FDs we wish to pass.
|
||||
*/
|
||||
static bool containMakeFdsCOE(nsj_t* nsj) {
|
||||
if (containMakeFdsCOECloseRange(nsj)) {
|
||||
return true;
|
||||
if (util::syscall(__NR_close_range, 0, ~0U, CLOSE_RANGE_CLOEXEC) == -1) {
|
||||
PLOG_E("close_range(0, ~0U, CLOSE_RANGE_CLOEXEC)");
|
||||
return false;
|
||||
}
|
||||
if (containMakeFdsCOEProc(nsj)) {
|
||||
return true;
|
||||
|
||||
for (const auto fd : nsj->openfds) {
|
||||
if (fd >= 0) {
|
||||
containMakeFdCOE(fd, /* pass_fd= */ true);
|
||||
}
|
||||
}
|
||||
if (containMakeFdsCOENaive(nsj)) {
|
||||
return true;
|
||||
}
|
||||
LOG_E("Couldn't mark relevant file-descriptors as close-on-exec with any known method");
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setupFD(nsj_t* nsj, int fd_in, int fd_out, int fd_err) {
|
||||
/*
|
||||
* (Violently) closes all file descriptors that are not explicitly required to survive
|
||||
* the containment boundary.
|
||||
*
|
||||
* Unlike COE above, closing an FD is a destructive and irreversible operation.
|
||||
* We cannot "close everything and revert". We must build a sorted list of critical
|
||||
* FDs (standard I/O, IPC sockets, logging, etc.) and safely jump over them by
|
||||
* calling close_range() on the numerical "gaps" between our preserved FDs.
|
||||
*/
|
||||
static bool containCloseFDs(nsj_t* nsj, int ipc_fd) {
|
||||
std::vector<unsigned int> keep_fds;
|
||||
|
||||
/* Core standard I/O */
|
||||
keep_fds.push_back(STDIN_FILENO);
|
||||
keep_fds.push_back(STDOUT_FILENO);
|
||||
keep_fds.push_back(STDERR_FILENO);
|
||||
|
||||
/* Crucial infrastructure */
|
||||
if (logs::logFd() > STDERR_FILENO) keep_fds.push_back(logs::logFd());
|
||||
if (ipc_fd >= 0) keep_fds.push_back(ipc_fd);
|
||||
if (nsj->njc.exec_bin().exec_fd() && nsj->exec_fd >= 0) {
|
||||
keep_fds.push_back(nsj->exec_fd);
|
||||
}
|
||||
|
||||
/* User-requested passthrough FDs */
|
||||
for (const auto fd : nsj->openfds) {
|
||||
if (fd >= 0) keep_fds.push_back(fd);
|
||||
}
|
||||
|
||||
/* Sort and deduplicate to safely iterate through the gaps */
|
||||
std::sort(keep_fds.begin(), keep_fds.end());
|
||||
keep_fds.erase(std::unique(keep_fds.begin(), keep_fds.end()), keep_fds.end());
|
||||
|
||||
unsigned int range_start = 0;
|
||||
for (unsigned int target_fd : keep_fds) {
|
||||
/* If there is a gap between the start of our range and the target FD, close the gap
|
||||
*/
|
||||
if (target_fd > range_start) {
|
||||
if (util::syscall(__NR_close_range, range_start, target_fd - 1, 0) == -1) {
|
||||
PLOG_E("close_range(%u, %u, 0)", range_start, target_fd - 1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Advance the start of the next range to be immediately after our target FD */
|
||||
range_start = target_fd + 1;
|
||||
}
|
||||
|
||||
/* Finally, close all remaining file descriptors from the last target FD up to the system
|
||||
* max */
|
||||
if (range_start < ~0U) {
|
||||
if (util::syscall(__NR_close_range, range_start, ~0U, 0) == -1) {
|
||||
PLOG_E("close_range(%u, ~0U, 0)", range_start);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setupFD(nsj_t* nsj, int fd_in, int fd_out, int fd_err, int ipc_fd) {
|
||||
if (nsj->njc.mode() == nsjail::Mode::LISTEN) {
|
||||
util::detachFromTTY();
|
||||
}
|
||||
if (nsj->njc.stderr_to_null()) {
|
||||
LOG_D("Redirecting fd=2 (STDERR_FILENO) to /dev/null");
|
||||
if ((fd_err = TEMP_FAILURE_RETRY(open("/dev/null", O_RDWR))) == -1) {
|
||||
PLOG_E("open('/dev/null', O_RDWR");
|
||||
if ((fd_err = TEMP_FAILURE_RETRY(open("/dev/null", O_RDWR | O_CLOEXEC))) == -1) {
|
||||
PLOG_E("open('/dev/null', O_RDWR | O_CLOEXEC");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (nsj->njc.silent()) {
|
||||
LOG_D("Redirecting fd=0-2 (STDIN/OUT/ERR_FILENO) to /dev/null");
|
||||
if (TEMP_FAILURE_RETRY(fd_in = fd_out = fd_err = open("/dev/null", O_RDWR)) == -1) {
|
||||
PLOG_E("open('/dev/null', O_RDWR)");
|
||||
if (TEMP_FAILURE_RETRY(
|
||||
fd_in = fd_out = fd_err = open("/dev/null", O_RDWR | O_CLOEXEC)) == -1) {
|
||||
PLOG_E("open('/dev/null', O_RDWR | O_CLOEXEC)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -362,6 +368,9 @@ bool setupFD(nsj_t* nsj, int fd_in, int fd_out, int fd_err) {
|
||||
PLOG_E("dup2(%d, STDERR_FILENO)", fd_err);
|
||||
return false;
|
||||
}
|
||||
if (!contain::containCloseFDs(nsj, ipc_fd)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -373,8 +382,7 @@ bool containProc(nsj_t* nsj) {
|
||||
RETURN_ON_FAILURE(containInitUtsNs(nsj));
|
||||
RETURN_ON_FAILURE(containInitCgroupNs());
|
||||
RETURN_ON_FAILURE(containDropPrivs(nsj));
|
||||
;
|
||||
/* */
|
||||
|
||||
/* As non-root */
|
||||
RETURN_ON_FAILURE(containCPU(nsj));
|
||||
RETURN_ON_FAILURE(containTSC(nsj));
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
namespace contain {
|
||||
|
||||
bool setupFD(nsj_t* nsj, int fd_in, int fd_out, int fd_err);
|
||||
bool setupFD(nsj_t* nsj, int fd_in, int fd_out, int fd_err, int ipc_fd);
|
||||
bool containProc(nsj_t* nsj);
|
||||
|
||||
} // namespace contain
|
||||
|
||||
4
logs.cc
4
logs.cc
@@ -110,9 +110,9 @@ void logMsg(enum llevel_t ll, const char* fn, int ln, bool perr, const char* fmt
|
||||
return;
|
||||
}
|
||||
|
||||
char strerr[512];
|
||||
std::string strerr;
|
||||
if (perr) {
|
||||
snprintf(strerr, sizeof(strerr), "%s", strerror(errno));
|
||||
strerr = strerror(errno);
|
||||
}
|
||||
struct {
|
||||
const char* const descr;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
nsjail - missing definitions used across the code
|
||||
nsjail - missing syscall/constant/struct definitions
|
||||
-----------------------------------------
|
||||
|
||||
Copyright 2026 Google Inc. All Rights Reserved.
|
||||
|
||||
10
mnt.cc
10
mnt.cc
@@ -47,6 +47,7 @@
|
||||
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "mnt_legacy.h"
|
||||
#include "mnt_newapi.h"
|
||||
#include "subproc.h"
|
||||
@@ -362,17 +363,20 @@ bool initNs(nsj_t* nsj) {
|
||||
return initNsInternal(nsj);
|
||||
}
|
||||
|
||||
pid_t pid = subproc::cloneProc(CLONE_FS, SIGCHLD);
|
||||
pid_t pid = subproc::cloneProcNoPidfd(CLONE_FS, SIGCHLD);
|
||||
if (pid == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
exit(initNsInternal(nsj) ? 0 : 0xff);
|
||||
_exit(initNsInternal(nsj) ? 0 : 0xff);
|
||||
}
|
||||
|
||||
int status;
|
||||
while (wait4(pid, &status, 0, NULL) != pid);
|
||||
if (TEMP_FAILURE_RETRY(waitpid(pid, &status, 0)) == -1) {
|
||||
PLOG_W("waitpid(pid=%d)", pid);
|
||||
return false;
|
||||
}
|
||||
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
21
mnt.h
21
mnt.h
@@ -29,28 +29,9 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "missing_defs.h"
|
||||
#include "nsjail.h"
|
||||
|
||||
#if !defined(MS_NOSYMFOLLOW)
|
||||
#define MS_NOSYMFOLLOW 256
|
||||
#endif /* if !defined(MS_NOSYMFOLLOW) */
|
||||
|
||||
#if !defined(MS_LAZYTIME)
|
||||
#define MS_LAZYTIME (1 << 25)
|
||||
#endif /* if !defined(MS_LAZYTIME) */
|
||||
|
||||
#if !defined(MS_ACTIVE)
|
||||
#define MS_ACTIVE (1 << 30)
|
||||
#endif /* if !defined(MS_ACTIVE) */
|
||||
|
||||
#if !defined(MS_NOUSER)
|
||||
#define MS_NOUSER (1 << 31)
|
||||
#endif /* if !defined(MS_NOUSER) */
|
||||
|
||||
#if !defined(ST_NOSYMFOLLOW)
|
||||
#define ST_NOSYMFOLLOW 8192
|
||||
#endif /* if !defined(ST_NOSYMFOLLOW) */
|
||||
|
||||
namespace mnt {
|
||||
|
||||
typedef enum {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "mnt.h"
|
||||
#include "util.h"
|
||||
|
||||
|
||||
@@ -21,55 +21,14 @@
|
||||
|
||||
#include "mnt_newapi.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h>
|
||||
// clang-format off
|
||||
/* https://github.com/google/nsjail/issues/250 */
|
||||
#include <sys/mount.h>
|
||||
// clang-format on
|
||||
#include <linux/mount.h>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#include "logs.h"
|
||||
#include "util.h"
|
||||
|
||||
/*
|
||||
* Compile-time feature detection for the new mount API.
|
||||
* Requires kernel headers with fsopen/fsconfig/fsmount/move_mount support.
|
||||
*/
|
||||
#if defined(__NR_fsopen) && defined(__NR_fsconfig) && defined(__NR_fsmount) && \
|
||||
defined(__NR_move_mount) && defined(__NR_open_tree) && defined(__NR_mount_setattr) && \
|
||||
defined(FSOPEN_CLOEXEC) && defined(FSMOUNT_CLOEXEC) && defined(MOVE_MOUNT_F_EMPTY_PATH) && \
|
||||
defined(MOUNT_ATTR_RDONLY) && defined(MOUNT_ATTR_NOSUID) && defined(MOUNT_ATTR_NODEV) && \
|
||||
defined(MOUNT_ATTR_NOEXEC) && defined(AT_EMPTY_PATH) && defined(AT_RECURSIVE)
|
||||
#define MNT_NEWAPI_SUPPORTED 1
|
||||
#endif
|
||||
|
||||
#if !defined(MNT_NEWAPI_SUPPORTED)
|
||||
|
||||
namespace mnt {
|
||||
namespace newapi {
|
||||
|
||||
bool isAvailable() {
|
||||
LOG_W("New mount API unavailable: missing compile-time support");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool remountPt(mnt::mount_t&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<std::string> buildMountTree(nsj_t*, std::vector<mnt::mount_t>*) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace newapi
|
||||
} // namespace mnt
|
||||
|
||||
#else /* MNT_NEWAPI_SUPPORTED */
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <inttypes.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <unistd.h>
|
||||
@@ -78,8 +37,11 @@ std::unique_ptr<std::string> buildMountTree(nsj_t*, std::vector<mnt::mount_t>*)
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "mnt.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace mnt {
|
||||
namespace newapi {
|
||||
@@ -345,8 +307,8 @@ static bool mountDynamicContentAt(mount_t* mpt, int root_fd, const char* rel_dst
|
||||
return false;
|
||||
}
|
||||
|
||||
int mnt_fd =
|
||||
syscall(__NR_open_tree, root_fd, src_rel.c_str(), OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
|
||||
int mnt_fd = util::syscall(__NR_open_tree, (uintptr_t)root_fd, (uintptr_t)src_rel.c_str(),
|
||||
(uintptr_t)(OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC));
|
||||
if (mnt_fd < 0) {
|
||||
PLOG_W("open_tree('%s')", src_rel.c_str());
|
||||
unlinkat(root_fd, src_rel.c_str(), 0);
|
||||
@@ -370,7 +332,8 @@ static bool mountDynamicContentAt(mount_t* mpt, int root_fd, const char* rel_dst
|
||||
PLOG_W("unlinkat(root_fd, '%s')", src_rel.c_str());
|
||||
}
|
||||
|
||||
mpt->fd = syscall(__NR_open_tree, root_fd, rel_dst, (unsigned int)OPEN_TREE_CLOEXEC);
|
||||
mpt->fd = util::syscall(__NR_open_tree, (uintptr_t)root_fd, (uintptr_t)rel_dst,
|
||||
(uintptr_t)(unsigned int)OPEN_TREE_CLOEXEC);
|
||||
if (mpt->fd < 0) {
|
||||
PLOG_W("open_tree(root_fd, '%s')", rel_dst);
|
||||
return false;
|
||||
@@ -386,7 +349,8 @@ static bool doBindMountAt(mount_t* mpt, int root_fd, const char* rel_dst) {
|
||||
}
|
||||
|
||||
LOG_D("open_tree('%s', flags=0x%x)", mpt->src.c_str(), flags);
|
||||
int mnt_fd = syscall(__NR_open_tree, AT_FDCWD, mpt->src.c_str(), flags);
|
||||
int mnt_fd = util::syscall(
|
||||
__NR_open_tree, (uintptr_t)AT_FDCWD, (uintptr_t)mpt->src.c_str(), (uintptr_t)flags);
|
||||
if (mnt_fd < 0) {
|
||||
PLOG_W("open_tree('%s')", mpt->src.c_str());
|
||||
return false;
|
||||
@@ -644,5 +608,3 @@ std::unique_ptr<std::string> buildMountTree(nsj_t* nsj, std::vector<mnt::mount_t
|
||||
|
||||
} // namespace newapi
|
||||
} // namespace mnt
|
||||
|
||||
#endif /* MNT_NEWAPI_SUPPORTED */
|
||||
|
||||
699
monitor.cc
Normal file
699
monitor.cc
Normal file
@@ -0,0 +1,699 @@
|
||||
/*
|
||||
* nsjail - per-child monitor thread
|
||||
* -----------------------------------------
|
||||
*
|
||||
* Each sandboxed child gets a dedicated monitor thread with its own
|
||||
* epoll-based event loop. The thread multiplexes:
|
||||
*
|
||||
* pidfd -- child death notification
|
||||
* ipc_fd -- parent↔child IPC socketpair
|
||||
* sockproxy -- stdin/stdout splice proxy (listen mode only)
|
||||
* unotify -- seccomp user notifications
|
||||
* nstun -- TUN-based networking
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. Receive setup FDs from child via IPC (tap, unotify, etc.)
|
||||
* 2. Acknowledge child with MSG_TAG_READY_H2J
|
||||
* 3. Enter epoll loop
|
||||
* 4. On child death (pidfd) -> cleanUpAndExit()
|
||||
* 5. On setup error -> killAndExit()
|
||||
*/
|
||||
|
||||
#include "monitor.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "net.h"
|
||||
#include "nsjail.h"
|
||||
#include "nstun/nstun.h"
|
||||
#include "sockproxy/sockproxy.h"
|
||||
#include "subproc.h"
|
||||
#include "unotify/unotify.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace monitor {
|
||||
|
||||
/* --- pipe bundle for listen-mode proxy ----------------- */
|
||||
|
||||
struct ProxyPipes {
|
||||
int child_in;
|
||||
int child_out;
|
||||
int parent_in;
|
||||
int parent_out;
|
||||
int connfd;
|
||||
|
||||
void closeAll() {
|
||||
if (child_in >= 0) {
|
||||
close(child_in);
|
||||
}
|
||||
if (child_out >= 0) {
|
||||
close(child_out);
|
||||
}
|
||||
if (parent_in >= 0) {
|
||||
close(parent_in);
|
||||
}
|
||||
if (parent_out >= 0) {
|
||||
close(parent_out);
|
||||
}
|
||||
if (connfd >= 0) {
|
||||
close(connfd);
|
||||
}
|
||||
child_in = child_out = parent_in = parent_out = connfd = -1;
|
||||
}
|
||||
};
|
||||
|
||||
/* --- epoll handler types ------------------------------- */
|
||||
|
||||
struct fdHandler_t {
|
||||
fdCb_t cb;
|
||||
void* data;
|
||||
};
|
||||
|
||||
/* --- per-thread context -------------------------------- */
|
||||
|
||||
constexpr size_t MAX_EVENTS = 64;
|
||||
|
||||
struct ThreadCtx {
|
||||
/* child identity */
|
||||
nsj_t* nsj = nullptr;
|
||||
pid_t pid = -1;
|
||||
int pidfd = -1;
|
||||
time_t start_time = 0;
|
||||
char remote_txt[128];
|
||||
|
||||
/* proxy pipe FDs (listen mode only, nullptr in standalone) */
|
||||
ProxyPipes* pipes = nullptr;
|
||||
|
||||
/* epoll state */
|
||||
int epoll_fd = -1;
|
||||
std::unordered_map<int, fdHandler_t> fd_handlers;
|
||||
std::vector<periodicCb_t> periodics;
|
||||
bool stop_requested = false;
|
||||
};
|
||||
|
||||
static thread_local ThreadCtx current_ctx;
|
||||
|
||||
bool addFd(int fd, uint32_t events, fdCb_t cb, void* data) {
|
||||
if (fd < 0) {
|
||||
return false;
|
||||
}
|
||||
current_ctx.fd_handlers[fd] = {.cb = cb, .data = data};
|
||||
struct epoll_event ev = {
|
||||
.events = events,
|
||||
.data = {.fd = fd},
|
||||
};
|
||||
if (epoll_ctl(current_ctx.epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
|
||||
PLOG_W("epoll_ctl(EPOLL_CTL_ADD, fd=%d)", fd);
|
||||
current_ctx.fd_handlers.erase(fd);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool removeFd(int fd) {
|
||||
if (fd < 0) {
|
||||
return false;
|
||||
}
|
||||
current_ctx.fd_handlers.erase(fd);
|
||||
if (epoll_ctl(current_ctx.epoll_fd, EPOLL_CTL_DEL, fd, nullptr) == -1) {
|
||||
PLOG_W("epoll_ctl(EPOLL_CTL_DEL, fd=%d)", fd);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool modFd(int fd, uint32_t events) {
|
||||
if (fd < 0) {
|
||||
return false;
|
||||
}
|
||||
struct epoll_event ev = {
|
||||
.events = events,
|
||||
.data = {.fd = fd},
|
||||
};
|
||||
if (epoll_ctl(current_ctx.epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) {
|
||||
PLOG_W("epoll_ctl(EPOLL_CTL_MOD, fd=%d)", fd);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void addPeriodic(periodicCb_t cb) {
|
||||
current_ctx.periodics.push_back(cb);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
current_ctx.stop_requested = true;
|
||||
}
|
||||
|
||||
static void dispatchEvents(struct epoll_event* events, int nfds) {
|
||||
for (int i = 0; i < nfds; ++i) {
|
||||
int fd = events[i].data.fd;
|
||||
auto it = current_ctx.fd_handlers.find(fd);
|
||||
if (it != current_ctx.fd_handlers.end() && it->second.cb) {
|
||||
it->second.cb(fd, events[i].events, it->second.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void run() {
|
||||
struct epoll_event events[MAX_EVENTS];
|
||||
uint64_t last_periodic_ms = util::timeUsec() / 1000;
|
||||
|
||||
for (;;) {
|
||||
uint64_t now_ms = util::timeUsec() / 1000;
|
||||
int timeout_ms = current_ctx.periodics.empty()
|
||||
? -1
|
||||
: std::max(0, (int)(1000 - (now_ms - last_periodic_ms)));
|
||||
|
||||
int nfds = epoll_wait(current_ctx.epoll_fd, events, MAX_EVENTS, timeout_ms);
|
||||
if (nfds == -1) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
PLOG_W("epoll_wait");
|
||||
break;
|
||||
}
|
||||
|
||||
dispatchEvents(events, nfds);
|
||||
|
||||
now_ms = util::timeUsec() / 1000;
|
||||
if (!current_ctx.periodics.empty() && now_ms - last_periodic_ms >= 1000) {
|
||||
last_periodic_ms = now_ms;
|
||||
for (auto cb : current_ctx.periodics) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
if (current_ctx.stop_requested) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- event loop ---------------------------------------- */
|
||||
|
||||
/* --- pipe setup (listen mode) -------------------------- */
|
||||
|
||||
static bool createProxyPipes(int connfd, ProxyPipes* pipes) {
|
||||
int in[2];
|
||||
int out[2];
|
||||
|
||||
if (pipe2(in, O_CLOEXEC) != 0) {
|
||||
PLOG_W("pipe2(in)");
|
||||
return false;
|
||||
}
|
||||
pipes->child_in = in[0];
|
||||
pipes->parent_out = in[1];
|
||||
|
||||
if (pipe2(out, O_CLOEXEC) != 0) {
|
||||
PLOG_W("pipe2(out)");
|
||||
pipes->closeAll();
|
||||
return false;
|
||||
}
|
||||
pipes->parent_in = out[0];
|
||||
pipes->child_out = out[1];
|
||||
|
||||
/* Proxy side must be non-blocking for splice() */
|
||||
if (!util::setNonBlock(pipes->parent_in) || !util::setNonBlock(pipes->parent_out)) {
|
||||
pipes->closeAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
pipes->connfd = connfd;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --- teardown ------------------------------------------ */
|
||||
|
||||
static void timeoutCb() {
|
||||
subproc::checkTimeouts(current_ctx.nsj, current_ctx.pid, current_ctx.start_time,
|
||||
current_ctx.remote_txt, current_ctx.pidfd);
|
||||
nstun_periodic();
|
||||
}
|
||||
|
||||
static void cleanUpAndExit() {
|
||||
nstun_destroy_parent();
|
||||
unotify::stop();
|
||||
sockproxy::stop();
|
||||
|
||||
if (current_ctx.pipes) {
|
||||
current_ctx.pipes->closeAll();
|
||||
}
|
||||
|
||||
if (current_ctx.epoll_fd != -1) {
|
||||
close(current_ctx.epoll_fd);
|
||||
current_ctx.epoll_fd = -1;
|
||||
}
|
||||
|
||||
current_ctx.periodics.clear();
|
||||
current_ctx.fd_handlers.clear();
|
||||
current_ctx.stop_requested = false;
|
||||
}
|
||||
|
||||
static void killAndExit() {
|
||||
if (current_ctx.pidfd >= 0) {
|
||||
util::syscall(__NR_pidfd_send_signal, current_ctx.pidfd, SIGKILL, 0, 0);
|
||||
}
|
||||
cleanUpAndExit();
|
||||
}
|
||||
|
||||
/* --- epoll callbacks ----------------------------------- */
|
||||
|
||||
/*
|
||||
* pidfd became readable -> child is already dead.
|
||||
* Just signal the loop to exit; cleanUpAndExit() skips the kill.
|
||||
*/
|
||||
static void pidfdCb(int /* fd */, uint32_t /* events */, void* /* data */) {
|
||||
LOG_D("Child died (pidfd event), exiting monitor thread");
|
||||
current_ctx.stop_requested = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* IPC socketpair events:
|
||||
* EPOLLIN -> message from child (e.g. execve failure)
|
||||
* EPOLLRDHUP -> child closed its end (normal after execve)
|
||||
*/
|
||||
static void ipcFdCb(int ipc_fd, uint32_t events, void* /* data */) {
|
||||
if (events & EPOLLIN) {
|
||||
uint32_t id = 0;
|
||||
errno = 0;
|
||||
if (util::recvMsg(ipc_fd, &id, nullptr)) {
|
||||
LOG_D("IPC msg 0x%08x from fd=%d", id, ipc_fd);
|
||||
if (id == monitor::MSG_TAG_ERROR) {
|
||||
LOG_W("Child reported error (execve failed)");
|
||||
current_ctx.stop_requested = true;
|
||||
}
|
||||
} else {
|
||||
if (errno != 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
PLOG_W("recvMsg from ipc_fd failed unexpectedly");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
|
||||
LOG_D("ipc_fd=%d closed (child reached execve), unregistering", ipc_fd);
|
||||
removeFd(ipc_fd);
|
||||
close(ipc_fd);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- IPC setup handshake ------------------------------- */
|
||||
|
||||
/*
|
||||
* Receive setup FDs from the child (tap, unotify, etc.) until
|
||||
* MSG_TAG_READY_J2H signals that the child is ready to proceed.
|
||||
*
|
||||
* Returns true on success, false on error (caller should killAndExit).
|
||||
*/
|
||||
static bool receiveChildFds(int ipc_fd, nsj_t* nsj, pid_t pid) {
|
||||
for (;;) {
|
||||
struct pollfd pfd = {
|
||||
.fd = ipc_fd,
|
||||
.events = POLLIN,
|
||||
.revents = 0,
|
||||
};
|
||||
struct timespec ts = {
|
||||
.tv_sec = 10,
|
||||
.tv_nsec = 0,
|
||||
};
|
||||
int res = ppoll(&pfd, 1, &ts, nullptr);
|
||||
if (res == -1) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
PLOG_W("ppoll(ipc_fd)");
|
||||
return false;
|
||||
}
|
||||
if (res == 0) {
|
||||
LOG_W("Timeout waiting for setup FDs from child");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t id = 0;
|
||||
int fd = -1;
|
||||
if (!util::recvMsg(ipc_fd, &id, &fd)) {
|
||||
LOG_W("Failed to receive IPC message from child");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (id) {
|
||||
case monitor::MSG_TAG_READY_J2H:
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
return true;
|
||||
|
||||
case monitor::MSG_TAG_TAP:
|
||||
if (!nstun_init_parent(fd, nsj, pid)) {
|
||||
LOG_W("nstun_init_parent failed");
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case monitor::MSG_TAG_UNOTIFY:
|
||||
LOG_D("Received unotif_fd=%d from child", fd);
|
||||
if (!unotify::start(nsj, fd)) {
|
||||
LOG_W("Failed to start unotify");
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case monitor::MSG_TAG_ERROR:
|
||||
LOG_W("Child failed to launch");
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
LOG_W("Unknown IPC message type 0x%08x from child", id);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void proxyCloseCb(void* /* data */) {
|
||||
ThreadCtx* ctx = ¤t_ctx;
|
||||
LOG_D("Proxy closed, killing child process (pid=%d)", ctx->pid);
|
||||
util::syscall(__NR_pidfd_send_signal, ctx->pidfd, SIGKILL, 0, 0);
|
||||
monitor::stop();
|
||||
}
|
||||
|
||||
/* --- monitor thread ------------------------------------ */
|
||||
|
||||
struct MonitorArgs {
|
||||
nsj_t* nsj;
|
||||
pid_t pid;
|
||||
int ipc_fd;
|
||||
int pidfd;
|
||||
ProxyPipes pipes; /* copied by value; connfd < 0 means no proxy */
|
||||
time_t start_time;
|
||||
char remote_txt[128];
|
||||
};
|
||||
|
||||
static void monitorThread(MonitorArgs args) {
|
||||
/* -- Thread identity -- */
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "nsj-mon-%d", args.pid);
|
||||
prctl(PR_SET_NAME, name, 0, 0, 0);
|
||||
|
||||
sigset_t set;
|
||||
sigemptyset(&set);
|
||||
for (int sig : nssigs) {
|
||||
sigaddset(&set, sig);
|
||||
}
|
||||
pthread_sigmask(SIG_BLOCK, &set, nullptr);
|
||||
|
||||
/* -- Initialize thread-local context -- */
|
||||
current_ctx = ThreadCtx();
|
||||
current_ctx.nsj = args.nsj;
|
||||
current_ctx.pid = args.pid;
|
||||
current_ctx.pidfd = args.pidfd;
|
||||
current_ctx.pipes = (args.pipes.connfd >= 0) ? &args.pipes : nullptr;
|
||||
current_ctx.start_time = args.start_time;
|
||||
snprintf(current_ctx.remote_txt, sizeof(current_ctx.remote_txt), "%s", args.remote_txt);
|
||||
|
||||
LOG_I("Monitor thread started for pid=%d (ipc_fd=%d, pidfd=%d) '%s'", (int)args.pid,
|
||||
args.ipc_fd, args.pidfd, current_ctx.remote_txt);
|
||||
|
||||
/* -- Create epoll instance -- */
|
||||
current_ctx.epoll_fd = epoll_create1(EPOLL_CLOEXEC);
|
||||
if (current_ctx.epoll_fd == -1) {
|
||||
LOG_W("epoll_create1 failed");
|
||||
killAndExit();
|
||||
return;
|
||||
}
|
||||
|
||||
/* -- Phase 1: receive setup FDs from child -- */
|
||||
if (!receiveChildFds(args.ipc_fd, args.nsj, args.pid)) {
|
||||
killAndExit();
|
||||
return;
|
||||
}
|
||||
|
||||
/* -- Phase 2: acknowledge child -- */
|
||||
if (!util::sendMsg(args.ipc_fd, monitor::MSG_TAG_READY_H2J)) {
|
||||
LOG_W("Failed to send READY to child");
|
||||
killAndExit();
|
||||
return;
|
||||
}
|
||||
|
||||
/* -- Phase 3: register epoll sources -- */
|
||||
if (!addFd(args.pidfd, EPOLLIN, pidfdCb, nullptr)) {
|
||||
killAndExit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (current_ctx.pipes) {
|
||||
if (!sockproxy::start(¤t_ctx.pipes->connfd, ¤t_ctx.pipes->parent_out,
|
||||
¤t_ctx.pipes->parent_in, proxyCloseCb, nullptr)) {
|
||||
killAndExit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!util::setNonBlock(args.ipc_fd) ||
|
||||
!addFd(args.ipc_fd, EPOLLIN | EPOLLRDHUP, ipcFdCb, nullptr)) {
|
||||
killAndExit();
|
||||
return;
|
||||
}
|
||||
|
||||
/* -- Phase 4: run event loop -- */
|
||||
addPeriodic(timeoutCb);
|
||||
run();
|
||||
|
||||
/* -- Phase 5: kill child (harmless if already dead) and clean up -- */
|
||||
util::syscall(__NR_pidfd_send_signal, current_ctx.pidfd, SIGKILL, 0, 0);
|
||||
cleanUpAndExit();
|
||||
}
|
||||
|
||||
/* --- thread launcher ----------------------------------- */
|
||||
|
||||
static void startMonitorThread(
|
||||
nsj_t* nsj, pid_t pid, int ipc_fd, int pidfd, ProxyPipes* pipes, std::thread* thread_out) {
|
||||
MonitorArgs args = {
|
||||
.nsj = nsj,
|
||||
.pid = pid,
|
||||
.ipc_fd = ipc_fd,
|
||||
.pidfd = pidfd,
|
||||
.pipes = pipes ? *pipes : ProxyPipes{-1, -1, -1, -1, -1},
|
||||
.start_time = 0,
|
||||
.remote_txt = {},
|
||||
};
|
||||
|
||||
/* Copy child metadata on the main thread to avoid racing with reapAll */
|
||||
auto it = nsj->pids.find(pid);
|
||||
if (it != nsj->pids.end()) {
|
||||
args.start_time = it->second.start;
|
||||
snprintf(args.remote_txt, sizeof(args.remote_txt), "%s",
|
||||
it->second.remote_txt.c_str());
|
||||
}
|
||||
|
||||
*thread_out = std::thread(monitorThread, args);
|
||||
}
|
||||
|
||||
/* --- signal handling helpers --------------------------- */
|
||||
|
||||
/*
|
||||
* Handle a fatal signal during the main-thread poll loop.
|
||||
* First signal: forward to children (or SIGKILL), close listen socket.
|
||||
* After 2s: escalate to SIGKILL.
|
||||
* Returns true if we should break out of the outer loop.
|
||||
*/
|
||||
static bool handleShutdownSignal(nsj_t* nsj, int sig, time_t* shutdown_start, int* listenfd) {
|
||||
if (*shutdown_start == 0) {
|
||||
*shutdown_start = time(nullptr);
|
||||
subproc::killAll(nsj, nsj->njc.forward_signals() ? sig : SIGKILL);
|
||||
logs::logStop(sig);
|
||||
if (listenfd && *listenfd >= 0) {
|
||||
close(*listenfd);
|
||||
*listenfd = -1;
|
||||
}
|
||||
} else if (time(nullptr) - *shutdown_start >= 2) {
|
||||
LOG_W("Processes did not exit after 2s, escalating to SIGKILL");
|
||||
subproc::killAll(nsj, SIGKILL);
|
||||
}
|
||||
|
||||
subproc::reapAll(nsj);
|
||||
return (subproc::countProc(nsj) == 0);
|
||||
}
|
||||
|
||||
/* --- listen mode (-Ml) -------------------------------- */
|
||||
|
||||
int runListenMode(nsj_t* nsj) {
|
||||
int listenfd = net::getRecvSocket(nsj);
|
||||
if (listenfd == -1) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
time_t shutdown_start = 0;
|
||||
for (;;) {
|
||||
struct pollfd pfd = {
|
||||
.fd = listenfd,
|
||||
.events = POLLIN,
|
||||
.revents = 0,
|
||||
};
|
||||
struct timespec ts = {
|
||||
.tv_sec = 1,
|
||||
.tv_nsec = 0,
|
||||
};
|
||||
int res = ppoll(&pfd, 1, &ts, nullptr);
|
||||
if (res == -1 && errno != EINTR) {
|
||||
PLOG_W("ppoll");
|
||||
break;
|
||||
}
|
||||
|
||||
/* -- Shutdown path -- */
|
||||
int sig = nsjail::getSigFatal();
|
||||
if (sig > 0) {
|
||||
if (handleShutdownSignal(nsj, sig, &shutdown_start, &listenfd)) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* -- Accept new connection -- */
|
||||
if (res > 0 && listenfd >= 0 && (pfd.revents & POLLIN)) {
|
||||
int connfd = net::acceptConn(listenfd);
|
||||
if (connfd >= 0) {
|
||||
ProxyPipes pipes = {
|
||||
.child_in = -1,
|
||||
.child_out = -1,
|
||||
.parent_in = -1,
|
||||
.parent_out = -1,
|
||||
.connfd = -1,
|
||||
};
|
||||
if (createProxyPipes(connfd, &pipes)) {
|
||||
int pidfd = -1;
|
||||
int ipc_fd = -1;
|
||||
pid_t pid = subproc::runChild(nsj, connfd, pipes.child_in,
|
||||
pipes.child_out, pipes.child_out, &pidfd, &ipc_fd);
|
||||
if (pid <= 0) {
|
||||
pipes.closeAll();
|
||||
} else {
|
||||
/* Parent doesn't need these */
|
||||
close(pipes.child_in);
|
||||
pipes.child_in = -1;
|
||||
close(pipes.child_out);
|
||||
pipes.child_out = -1;
|
||||
|
||||
startMonitorThread(nsj, pid, ipc_fd, pidfd, &pipes,
|
||||
&nsj->pids[pid].thread);
|
||||
}
|
||||
} else {
|
||||
close(connfd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -- Housekeeping -- */
|
||||
subproc::reapAll(nsj);
|
||||
if (nsjail::shouldShowProc()) {
|
||||
nsjail::clearShowProc();
|
||||
subproc::displayProc(nsj);
|
||||
}
|
||||
}
|
||||
|
||||
if (listenfd >= 0) {
|
||||
close(listenfd);
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* --- standalone mode (-Mo / -Me) ----------------------- */
|
||||
|
||||
int runStandaloneMode(nsj_t* nsj) {
|
||||
for (;;) {
|
||||
int pidfd = -1;
|
||||
int ipc_fd = -1;
|
||||
pid_t pid = subproc::runChild(nsj, /* netfd= */ -1, STDIN_FILENO, STDOUT_FILENO,
|
||||
STDERR_FILENO, &pidfd, &ipc_fd);
|
||||
if (pid == -1) {
|
||||
LOG_E("Couldn't launch the child process");
|
||||
return 0xff;
|
||||
}
|
||||
time_t start_time = nsj->pids[pid].start;
|
||||
|
||||
startMonitorThread(nsj, pid, ipc_fd, pidfd, nullptr, &nsj->pids[pid].thread);
|
||||
|
||||
/* -- Wait for child on main thread -- */
|
||||
time_t shutdown_start = 0;
|
||||
for (;;) {
|
||||
struct pollfd pfd = {
|
||||
.fd = pidfd,
|
||||
.events = POLLIN,
|
||||
.revents = 0,
|
||||
};
|
||||
struct timespec ts = {
|
||||
.tv_sec = 1,
|
||||
.tv_nsec = 0,
|
||||
};
|
||||
int res = ppoll(&pfd, 1, &ts, nullptr);
|
||||
if (res == -1 && errno != EINTR) {
|
||||
PLOG_W("ppoll");
|
||||
break;
|
||||
}
|
||||
|
||||
/* -- Shutdown path -- */
|
||||
int sig = nsjail::getSigFatal();
|
||||
if (sig > 0) {
|
||||
handleShutdownSignal(nsj, sig, &shutdown_start, nullptr);
|
||||
}
|
||||
|
||||
if (res > 0 && (pfd.revents & (POLLIN | POLLERR | POLLHUP))) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (nsjail::shouldShowProc()) {
|
||||
nsjail::clearShowProc();
|
||||
subproc::displayProc(nsj);
|
||||
}
|
||||
}
|
||||
|
||||
/* Main thread reaps the child and joins its monitor thread */
|
||||
subproc::reapProc(nsj, pid, true);
|
||||
|
||||
if (subproc::countProc(nsj) == 0) {
|
||||
if (nsj->njc.mode() == nsjail::Mode::ONCE) {
|
||||
return nsj->exit_status;
|
||||
}
|
||||
|
||||
time_t now = time(nullptr);
|
||||
if (now - start_time < 1) {
|
||||
LOG_I("Child exited too quickly, rate-limiting respawn");
|
||||
struct timespec ts = {.tv_sec = 1, .tv_nsec = 0};
|
||||
ppoll(nullptr, 0, &ts, nullptr);
|
||||
}
|
||||
|
||||
/* Daemon mode: check for pending fatal signals before respawning.
|
||||
* Without this, a SIGINT arriving during reapProc could spawn an
|
||||
* unwanted new child (Infinite Daemon Loop Law, goal.md #4). */
|
||||
if (nsjail::getSigFatal() > 0) {
|
||||
return nsj->exit_status;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* not reached */
|
||||
}
|
||||
|
||||
} // namespace monitor
|
||||
50
monitor.h
Normal file
50
monitor.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* nsjail - epoll-based event loop interface
|
||||
* -----------------------------------------
|
||||
*
|
||||
* Public API for the per-child monitor thread and its single-threaded
|
||||
* event loop. Each child process gets one monitor thread that
|
||||
* multiplexes I/O from pidfd, unotify, sockproxy, and nstun sources.
|
||||
*/
|
||||
|
||||
#ifndef NS_MONITOR_H
|
||||
#define NS_MONITOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct nsj_t;
|
||||
|
||||
namespace monitor {
|
||||
|
||||
/* --- IPC message tags ---------------------------------- */
|
||||
|
||||
constexpr uint32_t quad(char a, char b, char c, char d) {
|
||||
return (static_cast<uint32_t>(a)) | (static_cast<uint32_t>(b) << 8) |
|
||||
(static_cast<uint32_t>(c) << 16) | (static_cast<uint32_t>(d) << 24);
|
||||
}
|
||||
|
||||
constexpr uint32_t MSG_TAG_TAP = quad('T', 'A', 'P', 'F');
|
||||
constexpr uint32_t MSG_TAG_UNOTIFY = quad('S', 'E', 'C', 'F');
|
||||
constexpr uint32_t MSG_TAG_READY_J2H = quad('R', 'D', 'J', 'H');
|
||||
constexpr uint32_t MSG_TAG_READY_H2J = quad('R', 'D', 'H', 'J');
|
||||
constexpr uint32_t MSG_TAG_ERROR = quad('E', 'R', 'R', 'F');
|
||||
|
||||
/* --- epoll loop API ------------------------------------ */
|
||||
|
||||
typedef void (*fdCb_t)(int fd, uint32_t events, void* data);
|
||||
typedef void (*periodicCb_t)();
|
||||
|
||||
bool addFd(int fd, uint32_t events, fdCb_t cb, void* data);
|
||||
bool removeFd(int fd);
|
||||
bool modFd(int fd, uint32_t events);
|
||||
void addPeriodic(periodicCb_t cb);
|
||||
void stop();
|
||||
|
||||
/* --- entry points -------------------------------------- */
|
||||
|
||||
int runListenMode(nsj_t* nsj);
|
||||
int runStandaloneMode(nsj_t* nsj);
|
||||
|
||||
} // namespace monitor
|
||||
|
||||
#endif /* NS_MONITOR_H */
|
||||
78
net.cc
78
net.cc
@@ -30,7 +30,6 @@
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/ip6.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <netlink/route/nexthop.h>
|
||||
#include <netlink/route/route.h>
|
||||
#include <netlink/route/rule.h>
|
||||
#include <stdint.h>
|
||||
@@ -55,7 +54,9 @@
|
||||
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "nstun/nstun.h"
|
||||
#include "subproc.h"
|
||||
#include "util.h"
|
||||
|
||||
#define STR_(x) #x
|
||||
@@ -136,13 +137,15 @@ static bool cloneIface(nsj_t* nsj, struct nl_sock* sk, struct nl_cache* link_cac
|
||||
LOG_E("rtnl_link_macvlan_alloc()");
|
||||
return false;
|
||||
}
|
||||
defer {
|
||||
rtnl_link_put(rmv);
|
||||
};
|
||||
|
||||
int err;
|
||||
int master_index = rtnl_link_name2i(link_cache, nsj->njc.macvlan_iface().c_str());
|
||||
if (!master_index) {
|
||||
LOG_E("rtnl_link_name2i(): Did not find '%s' interface",
|
||||
nsj->njc.macvlan_iface().c_str());
|
||||
rtnl_link_put(rmv);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -170,29 +173,32 @@ static bool cloneIface(nsj_t* nsj, struct nl_sock* sk, struct nl_cache* link_cac
|
||||
if ((err = rtnl_link_add(sk, rmv, NLM_F_CREATE)) < 0) {
|
||||
LOG_E("rtnl_link_add(name:'%s' link:'%s'): %s", IFACE_NAME,
|
||||
nsj->njc.macvlan_iface().c_str(), nl_geterror(err));
|
||||
rtnl_link_put(rmv);
|
||||
return false;
|
||||
}
|
||||
|
||||
rtnl_link_put(rmv);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool moveToNs(
|
||||
const std::string& iface, struct nl_sock* sk, struct nl_cache* link_cache, pid_t pid) {
|
||||
LOG_D("Moving interface '%s' into netns=%d", iface.c_str(), (int)pid);
|
||||
|
||||
struct rtnl_link* orig_link = rtnl_link_get_by_name(link_cache, iface.c_str());
|
||||
if (!orig_link) {
|
||||
LOG_E("Couldn't find interface '%s'", iface.c_str());
|
||||
return false;
|
||||
}
|
||||
defer {
|
||||
rtnl_link_put(orig_link);
|
||||
};
|
||||
|
||||
struct rtnl_link* new_link = rtnl_link_alloc();
|
||||
if (!new_link) {
|
||||
LOG_E("Couldn't allocate new link");
|
||||
rtnl_link_put(orig_link);
|
||||
return false;
|
||||
}
|
||||
defer {
|
||||
rtnl_link_put(new_link);
|
||||
};
|
||||
|
||||
rtnl_link_set_ns_pid(new_link, pid);
|
||||
|
||||
@@ -200,13 +206,9 @@ static bool moveToNs(
|
||||
if (err < 0) {
|
||||
LOG_E("rtnl_link_change(): set NS of interface '%s' to pid=%d: %s", iface.c_str(),
|
||||
(int)pid, nl_geterror(err));
|
||||
rtnl_link_put(new_link);
|
||||
rtnl_link_put(orig_link);
|
||||
return false;
|
||||
}
|
||||
|
||||
rtnl_link_put(new_link);
|
||||
rtnl_link_put(orig_link);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -324,9 +326,9 @@ static void pastaProcess(nsj_t* nsj, int pid, int err_pipe) {
|
||||
argv.push_back(pid_str.c_str());
|
||||
argv.push_back(nullptr);
|
||||
|
||||
int nullfd = TEMP_FAILURE_RETRY(open("/dev/null", O_RDWR));
|
||||
int nullfd = TEMP_FAILURE_RETRY(open("/dev/null", O_RDWR | O_CLOEXEC));
|
||||
if (nullfd == -1) {
|
||||
PLOG_E("Cannot open '/dev/null' - O_RDWR");
|
||||
PLOG_E("Cannot open '/dev/null' - O_RDWR | O_CLOEXEC");
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
if (TEMP_FAILURE_RETRY(dup2(nullfd, STDIN_FILENO)) == -1) {
|
||||
@@ -379,11 +381,10 @@ static bool spawnPasta(nsj_t* nsj, int pid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pid_t ppid = fork();
|
||||
pid_t ppid = subproc::cloneProcNoPidfd(0, SIGCHLD);
|
||||
if (ppid == -1) {
|
||||
close(sv[0]);
|
||||
close(sv[1]);
|
||||
PLOG_E("fork()");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -398,7 +399,7 @@ static bool spawnPasta(nsj_t* nsj, int pid) {
|
||||
if (util::readFromFd(sv[0], &err, sizeof(err)) > 0) {
|
||||
close(sv[0]);
|
||||
LOG_E("Pasta execution failed, error: %s", strerror(err));
|
||||
while (waitpid(ppid, nullptr, 0) == -1 && errno == EINTR);
|
||||
TEMP_FAILURE_RETRY(waitpid(ppid, nullptr, 0));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -408,7 +409,7 @@ static bool spawnPasta(nsj_t* nsj, int pid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool initParent(nsj_t* nsj, pid_t pid, int pipefd) {
|
||||
bool initParent(nsj_t* nsj, pid_t pid, int ipc_fd) {
|
||||
if (nsj->njc.has_user_net()) {
|
||||
if (!nsj->njc.clone_newnet()) {
|
||||
LOG_E("Support for User-Mode Networking requested but CLONE_NEWNET "
|
||||
@@ -416,10 +417,7 @@ bool initParent(nsj_t* nsj, pid_t pid, int pipefd) {
|
||||
return false;
|
||||
}
|
||||
if (nsj->njc.user_net().backend() == nsjail::NsJailConfig_UserNet_Backend_NSTUN) {
|
||||
if (!nstun_init_parent(pipefd, nsj)) {
|
||||
LOG_E("nstun_init_parent() failed");
|
||||
return false;
|
||||
}
|
||||
/* Context creation is delegated to the monitor thread */
|
||||
} else if (nsj->njc.user_net().backend() ==
|
||||
nsjail::NsJailConfig_UserNet_Backend_PASTA &&
|
||||
nsj->njc.user_net().has_pasta()) {
|
||||
@@ -457,12 +455,10 @@ bool initParent(nsj_t* nsj, pid_t pid, int pipefd) {
|
||||
|
||||
for (const auto& iface : nsj->njc.iface_own()) {
|
||||
if (!moveToNs(iface, sk, link_cache, pid)) {
|
||||
nl_cache_free(link_cache);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!nsj->njc.macvlan_iface().empty() && !cloneIface(nsj, sk, link_cache, pid)) {
|
||||
nl_cache_free(link_cache);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -535,18 +531,15 @@ int getRecvSocket(const nsj_t* nsj) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
|
||||
int sockfd = socket(AF_INET6, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (sockfd == -1) {
|
||||
PLOG_E("socket(AF_INET6)");
|
||||
return -1;
|
||||
}
|
||||
if (fcntl(sockfd, F_SETFL, O_NONBLOCK)) {
|
||||
PLOG_E("fcntl(%d, F_SETFL, O_NONBLOCK)", sockfd);
|
||||
return -1;
|
||||
}
|
||||
int so = 1;
|
||||
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &so, sizeof(so)) == -1) {
|
||||
PLOG_E("setsockopt(%d, SO_REUSEADDR)", sockfd);
|
||||
close(sockfd);
|
||||
return -1;
|
||||
}
|
||||
struct sockaddr_in6 addr = {
|
||||
@@ -576,9 +569,10 @@ int getRecvSocket(const nsj_t* nsj) {
|
||||
int acceptConn(int listenfd) {
|
||||
struct sockaddr_in6 cli_addr = {};
|
||||
socklen_t socklen = sizeof(cli_addr);
|
||||
int connfd = accept4(listenfd, (struct sockaddr*)&cli_addr, &socklen, SOCK_NONBLOCK);
|
||||
int connfd = TEMP_FAILURE_RETRY(
|
||||
accept4(listenfd, (struct sockaddr*)&cli_addr, &socklen, SOCK_NONBLOCK | SOCK_CLOEXEC));
|
||||
if (connfd == -1) {
|
||||
if (errno != EINTR) {
|
||||
if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
PLOG_E("accept(%d)", listenfd);
|
||||
}
|
||||
return -1;
|
||||
@@ -630,7 +624,7 @@ const std::string connToText(int fd, bool remote, struct sockaddr_in6* addr_or_n
|
||||
}
|
||||
|
||||
static bool ifaceUp(const char* ifacename) {
|
||||
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
int sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_IP);
|
||||
if (sock == -1) {
|
||||
PLOG_E("socket(AF_INET, SOCK_STREAM, IPPROTO_IP)");
|
||||
return false;
|
||||
@@ -640,7 +634,10 @@ static bool ifaceUp(const char* ifacename) {
|
||||
};
|
||||
|
||||
struct ifreq ifr = {};
|
||||
snprintf(ifr.ifr_name, IF_NAMESIZE, "%s", ifacename);
|
||||
if (snprintf(ifr.ifr_name, IF_NAMESIZE, "%s", ifacename) >= IF_NAMESIZE) {
|
||||
LOG_W("Interface name '%s' is too long", ifacename);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ioctl(sock, SIOCGIFFLAGS, &ifr) == -1) {
|
||||
PLOG_E("ioctl(iface='%s', SIOCGIFFLAGS, IFF_UP)", ifacename);
|
||||
@@ -662,21 +659,22 @@ static bool ifaceUp(const char* ifacename) {
|
||||
|
||||
static bool ifaceConfig(const std::string& iface, const std::string& ip, const std::string& mask,
|
||||
const std::string& gw) {
|
||||
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
int sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_IP);
|
||||
if (sock == -1) {
|
||||
PLOG_E("socket(AF_INET, SOCK_STREAM, IPPROTO_IP)");
|
||||
return false;
|
||||
}
|
||||
defer {
|
||||
close(sock);
|
||||
};
|
||||
|
||||
struct in_addr addr;
|
||||
if (inet_pton(AF_INET, ip.c_str(), &addr) != 1) {
|
||||
PLOG_E("Cannot convert '%s' into an IPv4 address", ip.c_str());
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
if (addr.s_addr == INADDR_ANY) {
|
||||
LOG_D("IPv4 address for interface '%s' not set", iface.c_str());
|
||||
close(sock);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -687,36 +685,30 @@ static bool ifaceConfig(const std::string& iface, const std::string& ip, const s
|
||||
sa->sin_addr = addr;
|
||||
if (ioctl(sock, SIOCSIFADDR, &ifr) == -1) {
|
||||
PLOG_E("ioctl(iface='%s', SIOCSIFADDR, '%s')", iface.c_str(), ip.c_str());
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inet_pton(AF_INET, mask.c_str(), &addr) != 1) {
|
||||
PLOG_E("Cannot convert '%s' into a IPv4 netmask", mask.c_str());
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
sa->sin_family = AF_INET;
|
||||
sa->sin_addr = addr;
|
||||
if (ioctl(sock, SIOCSIFNETMASK, &ifr) == -1) {
|
||||
PLOG_E("ioctl(iface='%s', SIOCSIFNETMASK, '%s')", iface.c_str(), mask.c_str());
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ifaceUp(iface.c_str())) {
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inet_pton(AF_INET, gw.c_str(), &addr) != 1) {
|
||||
PLOG_E("Cannot convert '%s' into a IPv4 GW address", gw.c_str());
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
if (addr.s_addr == INADDR_ANY) {
|
||||
LOG_D("Gateway address for '%s' is not set", iface.c_str());
|
||||
close(sock);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -738,11 +730,9 @@ static bool ifaceConfig(const std::string& iface, const std::string& ip, const s
|
||||
|
||||
if (ioctl(sock, SIOCADDRT, &rt) == -1) {
|
||||
PLOG_E("ioctl(SIOCADDRT, '%s')", gw.c_str());
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
close(sock);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -895,10 +885,10 @@ bool initNsFromChild(nsj_t* nsj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool initChildPreSync(nsj_t* nsj, int pipefd) {
|
||||
bool initChildPreSync(nsj_t* nsj, int ipc_fd) {
|
||||
if (nsj->njc.has_user_net()) {
|
||||
if (nsj->njc.user_net().backend() == nsjail::NsJailConfig_UserNet_Backend_NSTUN) {
|
||||
if (!nstun_init_child(pipefd, nsj)) {
|
||||
if (!nstun_init_child(ipc_fd, nsj)) {
|
||||
LOG_E("nstun_init_child() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
5
net.h
5
net.h
@@ -27,6 +27,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "monitor.h"
|
||||
#include "nsjail.h"
|
||||
|
||||
namespace net {
|
||||
@@ -36,8 +37,8 @@ int getRecvSocket(const nsj_t* nsj);
|
||||
int acceptConn(int listenfd);
|
||||
const std::string connToText(int fd, bool remote, struct sockaddr_in6* addr_or_null);
|
||||
bool initNs(nsj_t* nsj);
|
||||
bool initChildPreSync(nsj_t* nsj, int pipefd);
|
||||
bool initParent(nsj_t* nsj, pid_t pid, int pipefd);
|
||||
bool initChildPreSync(nsj_t* nsj, int ipc_fd);
|
||||
bool initParent(nsj_t* nsj, pid_t pid, int ipc_fd);
|
||||
|
||||
} // namespace net
|
||||
|
||||
|
||||
234
nsjail.cc
234
nsjail.cc
@@ -22,15 +22,19 @@
|
||||
#include "nsjail.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#if __has_include(<linux/close_range.h>)
|
||||
#include <linux/close_range.h>
|
||||
#endif
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/wait.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -38,22 +42,30 @@
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "cgroup2.h"
|
||||
#include "cmdline.h"
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "monitor.h"
|
||||
#include "net.h"
|
||||
#include "sandbox.h"
|
||||
#include "subproc.h"
|
||||
#include "unotify/unotify.h"
|
||||
#include "unotify/stats.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace nsjail {
|
||||
|
||||
static __thread std::atomic<int> sigFatal(0);
|
||||
static __thread std::atomic<bool> showProc(false);
|
||||
/*
|
||||
* Thread-local to the main thread. Signals are only delivered to the main thread
|
||||
* (monitor threads block them), and only the main thread reads these in its poll loops.
|
||||
* See "The Threading Model Comprehension Law" in goal.md.
|
||||
*/
|
||||
static __thread std::atomic<int> sigFatal{0};
|
||||
static __thread std::atomic<bool> showProc{false};
|
||||
|
||||
static void sigHandler(int sig) {
|
||||
if (sig == SIGALRM || sig == SIGCHLD || sig == SIGPIPE) {
|
||||
@@ -88,6 +100,16 @@ static bool setSigHandler(int sig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int getSigFatal() {
|
||||
return sigFatal;
|
||||
}
|
||||
bool shouldShowProc() {
|
||||
return showProc;
|
||||
}
|
||||
void clearShowProc() {
|
||||
showProc = false;
|
||||
}
|
||||
|
||||
static bool setSigHandlers(void) {
|
||||
for (const auto& i : nssigs) {
|
||||
if (!setSigHandler(i)) {
|
||||
@@ -98,7 +120,7 @@ static bool setSigHandlers(void) {
|
||||
}
|
||||
|
||||
static bool setTimer(nsj_t* nsj) {
|
||||
if (nsj->njc.mode() == nsjail::Mode::EXECVE) {
|
||||
if (nsj->njc.mode() == ::nsjail::Mode::EXECVE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -144,185 +166,6 @@ static bool setFDLimit() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool pipeTraffic(nsj_t* nsj, int listenfd) {
|
||||
std::vector<struct pollfd> fds;
|
||||
fds.reserve(nsj->pipes.size() * 3 + 1);
|
||||
for (const auto& p : nsj->pipes) {
|
||||
fds.push_back({
|
||||
.fd = p.sock_fd,
|
||||
.events = POLLIN | POLLOUT,
|
||||
.revents = 0,
|
||||
});
|
||||
fds.push_back({
|
||||
.fd = p.pipe_in,
|
||||
.events = POLLOUT,
|
||||
.revents = 0,
|
||||
});
|
||||
fds.push_back({
|
||||
.fd = p.pipe_out,
|
||||
.events = POLLIN,
|
||||
.revents = 0,
|
||||
});
|
||||
}
|
||||
fds.push_back({
|
||||
.fd = listenfd,
|
||||
.events = POLLIN,
|
||||
.revents = 0,
|
||||
});
|
||||
LOG_D("Waiting for fd activity");
|
||||
while (poll(fds.data(), fds.size(), -1) > 0) {
|
||||
if (sigFatal > 0 || showProc) {
|
||||
return false;
|
||||
}
|
||||
if (fds.back().revents != 0) {
|
||||
LOG_D("New connection ready");
|
||||
return true;
|
||||
}
|
||||
bool cleanup = false;
|
||||
for (size_t i = 0; i < fds.size() - 1; ++i) {
|
||||
if (fds[i].revents & POLLIN) {
|
||||
fds[i].events &= ~POLLIN;
|
||||
}
|
||||
if (fds[i].revents & POLLOUT) {
|
||||
fds[i].events &= ~POLLOUT;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < nsj->pipes.size() * 3; i += 3) {
|
||||
const size_t pipe_no = i / 3;
|
||||
int in, out;
|
||||
const char* direction;
|
||||
bool closed = false;
|
||||
std::tuple<int, int, const char*> direction_map[] = {
|
||||
std::make_tuple(i, i + 1, "in"),
|
||||
std::make_tuple(i + 2, i, "out"),
|
||||
};
|
||||
for (const auto& entry : direction_map) {
|
||||
std::tie(in, out, direction) = entry;
|
||||
bool in_ready = (fds[in].events & POLLIN) == 0 ||
|
||||
(fds[in].revents & POLLIN) == POLLIN;
|
||||
bool out_ready = (fds[out].events & POLLOUT) == 0 ||
|
||||
(fds[out].revents & POLLOUT) == POLLOUT;
|
||||
if (in_ready && out_ready) {
|
||||
LOG_D("#%zu piping data %s", pipe_no, direction);
|
||||
ssize_t rv = splice(fds[in].fd, nullptr, fds[out].fd,
|
||||
nullptr, 4096, SPLICE_F_NONBLOCK);
|
||||
if (rv == -1 && errno != EAGAIN) {
|
||||
PLOG_E("splice fd pair #%zu {%d, %d}\n", pipe_no,
|
||||
fds[in].fd, fds[out].fd);
|
||||
}
|
||||
if (rv == 0) {
|
||||
closed = true;
|
||||
}
|
||||
fds[in].events |= POLLIN;
|
||||
fds[out].events |= POLLOUT;
|
||||
}
|
||||
if ((fds[in].revents & (POLLERR | POLLHUP)) != 0 ||
|
||||
(fds[out].revents & (POLLERR | POLLHUP)) != 0) {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
if (closed) {
|
||||
LOG_D("#%zu connection closed", pipe_no);
|
||||
cleanup = true;
|
||||
close(nsj->pipes[pipe_no].sock_fd);
|
||||
close(nsj->pipes[pipe_no].pipe_in);
|
||||
close(nsj->pipes[pipe_no].pipe_out);
|
||||
if (nsj->pipes[pipe_no].pid > 0) {
|
||||
kill(nsj->pipes[pipe_no].pid, SIGKILL);
|
||||
}
|
||||
nsj->pipes[pipe_no] = {};
|
||||
}
|
||||
}
|
||||
if (cleanup) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
nsj->pipes.erase(
|
||||
std::remove(nsj->pipes.begin(), nsj->pipes.end(), pipemap_t{}), nsj->pipes.end());
|
||||
return false;
|
||||
}
|
||||
|
||||
static int listenMode(nsj_t* nsj) {
|
||||
int listenfd = net::getRecvSocket(nsj);
|
||||
if (listenfd == -1) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
for (;;) {
|
||||
if (sigFatal > 0) {
|
||||
subproc::killAndReapAll(
|
||||
nsj, nsj->njc.forward_signals() ? sigFatal.load() : SIGKILL);
|
||||
logs::logStop(sigFatal);
|
||||
close(listenfd);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
if (showProc) {
|
||||
showProc = false;
|
||||
subproc::displayProc(nsj);
|
||||
}
|
||||
if (pipeTraffic(nsj, listenfd)) {
|
||||
int connfd = net::acceptConn(listenfd);
|
||||
if (connfd >= 0) {
|
||||
int in[2];
|
||||
int out[2];
|
||||
if (pipe(in) != 0 || pipe(out) != 0) {
|
||||
PLOG_E("pipe");
|
||||
continue;
|
||||
}
|
||||
|
||||
pid_t pid = subproc::runChild(nsj, connfd, in[0], out[1], out[1]);
|
||||
|
||||
close(in[0]);
|
||||
close(out[1]);
|
||||
|
||||
if (pid <= 0) {
|
||||
close(in[1]);
|
||||
close(out[0]);
|
||||
close(connfd);
|
||||
} else {
|
||||
nsj->pipes.push_back({
|
||||
.sock_fd = connfd,
|
||||
.pipe_in = in[1],
|
||||
.pipe_out = out[0],
|
||||
.pid = pid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
subproc::reapProc(nsj);
|
||||
}
|
||||
}
|
||||
|
||||
static int standaloneMode(nsj_t* nsj) {
|
||||
for (;;) {
|
||||
if (subproc::runChild(
|
||||
nsj, /* netfd= */ -1, STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO) == -1) {
|
||||
LOG_E("Couldn't launch the child process");
|
||||
return 0xff;
|
||||
}
|
||||
for (;;) {
|
||||
int child_status = subproc::reapProc(nsj);
|
||||
if (subproc::countProc(nsj) == 0) {
|
||||
if (nsj->njc.mode() == nsjail::Mode::ONCE) {
|
||||
return child_status;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (showProc) {
|
||||
showProc = false;
|
||||
subproc::displayProc(nsj);
|
||||
}
|
||||
if (sigFatal > 0) {
|
||||
subproc::killAndReapAll(
|
||||
nsj, nsj->njc.forward_signals() ? sigFatal.load() : SIGKILL);
|
||||
logs::logStop(sigFatal);
|
||||
return (128 + sigFatal);
|
||||
}
|
||||
pause();
|
||||
}
|
||||
}
|
||||
// not reached
|
||||
}
|
||||
|
||||
std::unique_ptr<struct termios> getTC(int fd) {
|
||||
std::unique_ptr<struct termios> trm(new struct termios);
|
||||
|
||||
@@ -351,6 +194,19 @@ void setTC(int fd, const struct termios* trm) {
|
||||
} // namespace nsjail
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
/*
|
||||
* Hard minimum: clone3 (5.3), CLONE_PIDFD (5.4),
|
||||
* CLONE_CLEAR_SIGHAND (5.5), PIDFD_NONBLOCK (5.10),
|
||||
* CLOSE_RANGE_CLOEXEC (5.11).
|
||||
*/
|
||||
if (!util::kernelVersionAtLeast(5, 11, 0)) {
|
||||
LOG_F("This version of nsjail requires Linux >= 5.11. "
|
||||
"Use an earlier version of nsjail for older kernels.");
|
||||
}
|
||||
if (!util::kernelVersionAtLeast(6, 0, 0)) {
|
||||
LOG_D("Running on a kernel older than 6.0. Consider upgrading "
|
||||
"for best compatibility.");
|
||||
}
|
||||
std::unique_ptr<nsj_t> nsj = cmdline::parseArgs(argc, argv);
|
||||
std::unique_ptr<struct termios> trm = nsjail::getTC(STDIN_FILENO);
|
||||
|
||||
@@ -387,15 +243,15 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
int ret = 0;
|
||||
if (nsj->njc.mode() == nsjail::Mode::LISTEN) {
|
||||
ret = nsjail::listenMode(nsj.get());
|
||||
if (nsj->njc.mode() == ::nsjail::Mode::LISTEN) {
|
||||
ret = monitor::runListenMode(nsj.get());
|
||||
} else {
|
||||
ret = nsjail::standaloneMode(nsj.get());
|
||||
ret = monitor::runStandaloneMode(nsj.get());
|
||||
}
|
||||
|
||||
subproc::killAndReapAll(nsj.get(), SIGKILL);
|
||||
subproc::killAll(nsj.get(), SIGKILL);
|
||||
sandbox::closePolicy(nsj.get());
|
||||
unotify::stop(nsj.get());
|
||||
unotify::printStats(nsj.get());
|
||||
/* Try to restore the underlying console's params in case some program has changed it */
|
||||
if (!nsj->njc.daemon()) {
|
||||
nsjail::setTC(STDIN_FILENO, trm.get());
|
||||
|
||||
3
nsjail.h
3
nsjail.h
@@ -34,6 +34,7 @@
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "config.pb.h"
|
||||
@@ -58,7 +59,7 @@ struct pids_t {
|
||||
struct sockaddr_in6 remote_addr;
|
||||
|
||||
pid_t pasta_pid;
|
||||
pthread_t monitor_tid;
|
||||
std::thread thread;
|
||||
};
|
||||
|
||||
struct idmap_t {
|
||||
|
||||
283
nstun/core.h
283
nstun/core.h
@@ -5,29 +5,31 @@
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/uio.h>
|
||||
|
||||
#include <compare>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "net_defs.h"
|
||||
#include "nstun.h"
|
||||
#include "encap.h"
|
||||
|
||||
namespace nstun {
|
||||
|
||||
constexpr size_t NSTUN_MAX_FLOWS = 1024;
|
||||
constexpr size_t NSTUN_MAX_FDS = 2048;
|
||||
constexpr size_t NSTUN_MAX_RULES = 128;
|
||||
constexpr size_t UDP_QUEUE_PACKET_MAX = 1500;
|
||||
constexpr size_t UDP_QUEUE_MAX_PACKETS = 50;
|
||||
constexpr int VLEN = 64;
|
||||
|
||||
// Removed MemcmpLess in favor of C++20 operator<=>
|
||||
/* Removed MemcmpLess in favor of C++20 operator<=> */
|
||||
|
||||
struct __attribute__((packed)) FlowKey4 {
|
||||
uint32_t saddr4;
|
||||
uint32_t daddr4;
|
||||
uint16_t sport;
|
||||
uint16_t dport;
|
||||
|
||||
auto operator<=> (const FlowKey4&) const = default;
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) FlowKey6 {
|
||||
@@ -35,119 +37,128 @@ struct __attribute__((packed)) FlowKey6 {
|
||||
uint8_t daddr6[IPV6_ADDR_LEN];
|
||||
uint16_t sport;
|
||||
uint16_t dport;
|
||||
|
||||
auto operator<=> (const FlowKey6&) const = default;
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) IcmpFlowKey4 {
|
||||
uint32_t saddr4;
|
||||
uint32_t daddr4;
|
||||
uint16_t id;
|
||||
|
||||
auto operator<=> (const IcmpFlowKey4&) const = default;
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) IcmpFlowKey6 {
|
||||
uint8_t saddr6[IPV6_ADDR_LEN];
|
||||
uint8_t daddr6[IPV6_ADDR_LEN];
|
||||
uint16_t id;
|
||||
|
||||
auto operator<=> (const IcmpFlowKey6&) const = default;
|
||||
};
|
||||
|
||||
struct Context;
|
||||
|
||||
class Flow {
|
||||
public:
|
||||
time_t last_active = 0;
|
||||
bool is_ipv6 = false;
|
||||
|
||||
virtual ~Flow() = default;
|
||||
virtual void handle_host_event(Context* ctx, int fd, uint32_t events) = 0;
|
||||
virtual void periodic_check(Context* ctx, time_t now) {}
|
||||
virtual bool is_stale(time_t now) const = 0;
|
||||
virtual void destroy(Context* ctx) = 0;
|
||||
};
|
||||
|
||||
enum class FlowType { TCP, UDP, ICMP };
|
||||
enum class ProxyMode : uint8_t { NONE, SOCKS5, HTTP_CONNECT };
|
||||
|
||||
enum class UdpSocks5State {
|
||||
ESTABLISHED, /* Direct or SOCKS5 ready */
|
||||
SOCKS5_GREETING, /* Sent SOCKS5 greeting, awaiting auth reply */
|
||||
SOCKS5_ASSOCIATE, /* Sent UDP ASSOCIATE, awaiting BND addr */
|
||||
TCP_CONNECTING, /* TCP connect() to SOCKS5 proxy in progress */
|
||||
enum UdpSocks5State {
|
||||
UDP_S5_ESTABLISHED, /* Direct or SOCKS5 ready */
|
||||
UDP_S5_GREETING, /* Sent SOCKS5 greeting, awaiting auth reply */
|
||||
UDP_S5_ASSOCIATE, /* Sent UDP ASSOCIATE, awaiting BND addr */
|
||||
UDP_S5_TCP_CONNECTING, /* TCP connect() to SOCKS5 proxy in progress */
|
||||
};
|
||||
|
||||
struct UdpFlow : public Flow {
|
||||
int host_fd = -1;
|
||||
int tcp_fd = -1; /* For SOCKS5 UDP associate */
|
||||
enum class TcpState {
|
||||
SYN_SENT, /* Host connecting to destination */
|
||||
SOCKS5_INIT, /* Sent SOCKS5 greeting, awaiting auth reply */
|
||||
SOCKS5_CONNECTING, /* Sent SOCKS5 CONNECT request, awaiting response */
|
||||
HTTP_CONNECT_WAIT, /* Sent HTTP CONNECT, awaiting proxy 200 reply */
|
||||
ESTABLISHED,
|
||||
FIN_WAIT_1,
|
||||
FIN_WAIT_2,
|
||||
CLOSING,
|
||||
TIME_WAIT,
|
||||
CLOSE_WAIT,
|
||||
};
|
||||
|
||||
struct FlowHeader {
|
||||
bool active;
|
||||
FlowType type;
|
||||
time_t last_active;
|
||||
bool is_ipv6;
|
||||
int host_fd;
|
||||
union {
|
||||
FlowKey4 key4;
|
||||
FlowKey6 key6;
|
||||
IcmpFlowKey4 icmp_key4;
|
||||
IcmpFlowKey6 icmp_key6;
|
||||
};
|
||||
bool is_redirected = false;
|
||||
/*
|
||||
* Original destination (before redirect/SOCKS5 rewrite).
|
||||
* Used when forwarding host replies back to the guest.
|
||||
*/
|
||||
union {
|
||||
uint32_t orig_dest_ip4;
|
||||
uint8_t orig_dest_ip6[16];
|
||||
};
|
||||
uint16_t orig_dest_port = 0;
|
||||
|
||||
/*
|
||||
* Redirect destination - stored at flow creation so the forwarding
|
||||
* path never needs to re-evaluate the rule. For SOCKS5 flows this
|
||||
* is the proxy address; for plain REDIRECT rules it is the target.
|
||||
*/
|
||||
uint32_t redirect_ip4 = 0;
|
||||
uint8_t redirect_ip6[IPV6_ADDR_LEN] = {};
|
||||
uint16_t redirect_port = 0;
|
||||
|
||||
bool use_socks5 = false;
|
||||
UdpSocks5State state = UdpSocks5State::ESTABLISHED;
|
||||
uint32_t bnd_ip = 0;
|
||||
uint16_t bnd_port = 0;
|
||||
|
||||
bool host_fd_is_listener = false;
|
||||
std::deque<std::vector<uint8_t>> tx_queue;
|
||||
|
||||
~UdpFlow() override {
|
||||
if (host_fd != -1 && !host_fd_is_listener) ::close(host_fd);
|
||||
if (tcp_fd != -1) ::close(tcp_fd);
|
||||
}
|
||||
|
||||
void handle_host_event(Context* ctx, int fd, uint32_t events) override;
|
||||
bool is_stale(time_t now) const override;
|
||||
void destroy(Context* ctx) override;
|
||||
};
|
||||
|
||||
struct TcpFlow;
|
||||
|
||||
struct IcmpFlow : public Flow {
|
||||
int host_fd = -1;
|
||||
union {
|
||||
IcmpFlowKey4 key4;
|
||||
IcmpFlowKey6 key6;
|
||||
};
|
||||
bool is_redirected = false;
|
||||
bool is_redirected;
|
||||
union {
|
||||
uint32_t orig_dest_ip4;
|
||||
uint8_t orig_dest_ip6[IPV6_ADDR_LEN];
|
||||
};
|
||||
|
||||
~IcmpFlow() override {
|
||||
if (host_fd != -1) ::close(host_fd);
|
||||
}
|
||||
|
||||
void handle_host_event(Context* ctx, int fd, uint32_t events) override;
|
||||
bool is_stale(time_t now) const override;
|
||||
void destroy(Context* ctx) override;
|
||||
uint16_t orig_dest_port;
|
||||
uint32_t redirect_ip4;
|
||||
uint8_t redirect_ip6[IPV6_ADDR_LEN];
|
||||
uint16_t redirect_port;
|
||||
};
|
||||
|
||||
struct UdpFlow {
|
||||
struct FlowHeader header;
|
||||
|
||||
/* UDP specific */
|
||||
int tcp_fd;
|
||||
bool use_socks5;
|
||||
UdpSocks5State state;
|
||||
uint32_t bnd_ip;
|
||||
uint16_t bnd_port;
|
||||
bool host_fd_is_listener;
|
||||
|
||||
struct {
|
||||
uint8_t data[UDP_QUEUE_PACKET_MAX];
|
||||
size_t len;
|
||||
} c_tx_queue[UDP_QUEUE_MAX_PACKETS];
|
||||
size_t c_tx_queue_head;
|
||||
size_t c_tx_queue_tail;
|
||||
size_t c_tx_queue_count;
|
||||
};
|
||||
|
||||
struct TcpFlow {
|
||||
struct FlowHeader header;
|
||||
|
||||
/* TCP specific */
|
||||
TcpState tcp_state;
|
||||
ProxyMode proxy_mode;
|
||||
bool host_eof;
|
||||
bool guest_eof;
|
||||
bool fin_sent;
|
||||
bool syn_acked;
|
||||
bool fin_acked;
|
||||
|
||||
uint32_t seq_to_guest;
|
||||
uint32_t ack_from_guest;
|
||||
|
||||
uint32_t seq_from_guest;
|
||||
uint32_t ack_to_guest;
|
||||
|
||||
size_t tx_acked_offset;
|
||||
size_t rx_sent_offset;
|
||||
|
||||
/* C-style buffers for migration */
|
||||
uint8_t c_tcp_tx_buf[4096];
|
||||
size_t c_tcp_tx_len;
|
||||
uint8_t c_proxy_rx_buf[8192];
|
||||
size_t c_proxy_rx_len;
|
||||
uint8_t c_tcp_rx_buf[4096];
|
||||
size_t c_tcp_rx_len;
|
||||
|
||||
bool epoll_out_registered;
|
||||
bool epoll_in_disabled;
|
||||
bool inbound;
|
||||
};
|
||||
|
||||
struct IcmpFlow {
|
||||
struct FlowHeader header;
|
||||
};
|
||||
|
||||
|
||||
struct Context {
|
||||
int epoll_fd;
|
||||
int tap_fd;
|
||||
struct nsj_t* nsj;
|
||||
|
||||
@@ -159,24 +170,51 @@ struct Context {
|
||||
uint8_t guest_ip6[IPV6_ADDR_LEN];
|
||||
uint8_t host_ip6[IPV6_ADDR_LEN];
|
||||
|
||||
std::vector<nstun_rule_t> rules;
|
||||
std::map<FlowKey4, std::unique_ptr<UdpFlow>> ipv4_udp_flows_by_key;
|
||||
std::map<FlowKey4, std::unique_ptr<TcpFlow>> ipv4_tcp_flows_by_key;
|
||||
std::map<IcmpFlowKey4, std::unique_ptr<IcmpFlow>> ipv4_icmp_flows_by_key;
|
||||
|
||||
/* Unified host mapping for all encapsulated flows */
|
||||
std::map<int, Flow*> flows_by_fd; // Observer pointer
|
||||
nstun_rule_t c_rules[NSTUN_MAX_RULES];
|
||||
size_t c_rules_count;
|
||||
|
||||
/* IPv6 maps (Owning) */
|
||||
std::map<FlowKey6, std::unique_ptr<UdpFlow>> ipv6_udp_flows_by_key;
|
||||
std::map<FlowKey6, std::unique_ptr<TcpFlow>> ipv6_tcp_flows_by_key;
|
||||
std::map<IcmpFlowKey6, std::unique_ptr<IcmpFlow>> ipv6_icmp_flows_by_key;
|
||||
TcpFlow c_ipv4_tcp_flows[NSTUN_MAX_FLOWS];
|
||||
size_t num_c_ipv4_tcp_flows;
|
||||
UdpFlow c_ipv4_udp_flows[NSTUN_MAX_FLOWS];
|
||||
size_t num_c_ipv4_udp_flows;
|
||||
UdpFlow c_ipv6_udp_flows[NSTUN_MAX_FLOWS];
|
||||
size_t num_c_ipv6_udp_flows;
|
||||
TcpFlow c_ipv6_tcp_flows[NSTUN_MAX_FLOWS];
|
||||
size_t num_c_ipv6_tcp_flows;
|
||||
IcmpFlow c_ipv4_icmp_flows[NSTUN_MAX_FLOWS];
|
||||
size_t num_c_ipv4_icmp_flows;
|
||||
IcmpFlow c_ipv6_icmp_flows[NSTUN_MAX_FLOWS];
|
||||
size_t num_c_ipv6_icmp_flows;
|
||||
struct {
|
||||
int fd;
|
||||
nstun_rule_t rule;
|
||||
} c_host_listener_rules[NSTUN_MAX_RULES];
|
||||
size_t num_c_host_listener_rules;
|
||||
|
||||
std::map<int, nstun_rule_t> host_listener_fd_to_rule;
|
||||
/* Buffer for TUN frames, moved from TLS to avoid stack/TLS pressure */
|
||||
uint8_t tun_buf[NSTUN_MTU + 4];
|
||||
|
||||
/* Buffers for recvmmsg, moved from TLS to avoid stack/TLS pressure */
|
||||
struct mmsghdr recvmmsg_msgs[VLEN];
|
||||
struct iovec recvmmsg_iovecs[VLEN];
|
||||
uint8_t recvmmsg_bufs[VLEN][NSTUN_MTU];
|
||||
struct sockaddr_storage recvmmsg_addrs[VLEN];
|
||||
bool recvmmsg_initialized;
|
||||
|
||||
/* Buffer for SOCKS5 UDP control channel reads */
|
||||
socks5_max_buf udp_socks5_buf;
|
||||
|
||||
/* Specific lookup tables for type safety */
|
||||
TcpFlow* c_tcp_flows_by_fd[NSTUN_MAX_FDS] = {};
|
||||
UdpFlow* c_udp_flows_by_fd[NSTUN_MAX_FDS] = {};
|
||||
IcmpFlow* c_icmp_flows_by_fd[NSTUN_MAX_FDS] = {};
|
||||
|
||||
~Context();
|
||||
};
|
||||
|
||||
void handle_host_events(Context* ctx, int fd, uint32_t events);
|
||||
void host_callback(int fd, uint32_t events, void* data);
|
||||
|
||||
struct RuleResult {
|
||||
nstun_action_t action;
|
||||
uint32_t redirect_ip4;
|
||||
@@ -185,7 +223,50 @@ struct RuleResult {
|
||||
uint8_t redirect_ip6[IPV6_ADDR_LEN];
|
||||
};
|
||||
|
||||
inline TcpFlow* get_tcp_flow_by_fd(const Context* ctx, int fd) {
|
||||
if (fd < 0 || fd >= (int)NSTUN_MAX_FDS) {
|
||||
return nullptr;
|
||||
}
|
||||
return ctx->c_tcp_flows_by_fd[fd];
|
||||
}
|
||||
|
||||
inline bool set_tcp_flow_by_fd(Context* ctx, int fd, TcpFlow* flow) {
|
||||
if (fd < 0 || fd >= (int)NSTUN_MAX_FDS) {
|
||||
return false;
|
||||
}
|
||||
ctx->c_tcp_flows_by_fd[fd] = flow;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline UdpFlow* get_udp_flow_by_fd(const Context* ctx, int fd) {
|
||||
if (fd < 0 || fd >= (int)NSTUN_MAX_FDS) {
|
||||
return nullptr;
|
||||
}
|
||||
return ctx->c_udp_flows_by_fd[fd];
|
||||
}
|
||||
|
||||
inline bool set_udp_flow_by_fd(Context* ctx, int fd, UdpFlow* flow) {
|
||||
if (fd < 0 || fd >= (int)NSTUN_MAX_FDS) {
|
||||
return false;
|
||||
}
|
||||
ctx->c_udp_flows_by_fd[fd] = flow;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline IcmpFlow* get_icmp_flow_by_fd(const Context* ctx, int fd) {
|
||||
if (fd < 0 || fd >= (int)NSTUN_MAX_FDS) {
|
||||
return nullptr;
|
||||
}
|
||||
return ctx->c_icmp_flows_by_fd[fd];
|
||||
}
|
||||
|
||||
inline bool set_icmp_flow_by_fd(Context* ctx, int fd, IcmpFlow* flow) {
|
||||
if (fd < 0 || fd >= (int)NSTUN_MAX_FDS) {
|
||||
return false;
|
||||
}
|
||||
ctx->c_icmp_flows_by_fd[fd] = flow;
|
||||
return true;
|
||||
}
|
||||
|
||||
} /* namespace nstun */
|
||||
|
||||
|
||||
105
nstun/encap.cc
105
nstun/encap.cc
@@ -5,6 +5,7 @@
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "net_defs.h"
|
||||
|
||||
namespace nstun {
|
||||
@@ -15,14 +16,17 @@ int send_socks5_greeting(int fd) {
|
||||
.num_auth = 1,
|
||||
.auth = {SOCKS5_AUTH_NONE},
|
||||
};
|
||||
if (send(fd, &greeting, sizeof(greeting), MSG_NOSIGNAL) != (ssize_t)sizeof(greeting)) {
|
||||
if (TEMP_FAILURE_RETRY(send(fd, &greeting, sizeof(greeting), MSG_NOSIGNAL)) !=
|
||||
(ssize_t)sizeof(greeting)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool parse_socks5_auth_reply(std::span<const uint8_t> data) {
|
||||
if (data.size() < 2) return false;
|
||||
bool parse_socks5_auth_reply(const uint8_t* data, size_t len) {
|
||||
if (len < 2) {
|
||||
return false;
|
||||
}
|
||||
return data[0] == SOCKS5_VERSION && data[1] == SOCKS5_AUTH_NONE;
|
||||
}
|
||||
|
||||
@@ -37,7 +41,8 @@ int send_socks5_connect(int fd, const uint8_t* addr, uint16_t port_nbo, bool is_
|
||||
.dst_port = port_nbo, /* Already in network byte order */
|
||||
};
|
||||
memcpy(req.dst_ip6, addr, sizeof(req.dst_ip6));
|
||||
if (send(fd, &req, sizeof(req), MSG_NOSIGNAL) != (ssize_t)sizeof(req)) {
|
||||
if (TEMP_FAILURE_RETRY(send(fd, &req, sizeof(req), MSG_NOSIGNAL)) !=
|
||||
(ssize_t)sizeof(req)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -51,7 +56,8 @@ int send_socks5_connect(int fd, const uint8_t* addr, uint16_t port_nbo, bool is_
|
||||
.dst_port = port_nbo,
|
||||
};
|
||||
memcpy(&req.dst_ip4, addr, 4);
|
||||
if (send(fd, &req, sizeof(req), MSG_NOSIGNAL) != (ssize_t)sizeof(req)) {
|
||||
if (TEMP_FAILURE_RETRY(send(fd, &req, sizeof(req), MSG_NOSIGNAL)) !=
|
||||
(ssize_t)sizeof(req)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -67,84 +73,113 @@ int send_socks5_udp_associate(int fd) {
|
||||
.dst_ip4 = 0,
|
||||
.dst_port = 0,
|
||||
};
|
||||
if (send(fd, &req, sizeof(req), MSG_NOSIGNAL) != (ssize_t)sizeof(req)) {
|
||||
if (TEMP_FAILURE_RETRY(send(fd, &req, sizeof(req), MSG_NOSIGNAL)) != (ssize_t)sizeof(req)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool parse_socks5_connect_reply(std::span<const uint8_t> data, Socks5Reply* out) {
|
||||
if (data.size() < 4) return false;
|
||||
bool parse_socks5_connect_reply(const uint8_t* data, size_t len, Socks5Reply* out) {
|
||||
if (len < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* data[0]=ver, data[1]=rep, data[2]=rsv, data[3]=atyp */
|
||||
if (data[0] != SOCKS5_VERSION) return false;
|
||||
if (data[1] != SOCKS5_REP_SUCCESS) return false;
|
||||
if (data[0] != SOCKS5_VERSION) {
|
||||
return false;
|
||||
}
|
||||
if (data[1] != SOCKS5_REP_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out->atyp = data[3];
|
||||
|
||||
if (out->atyp == SOCKS5_ATYP_IPV4) {
|
||||
/* Full reply: 4-byte header + 4-byte IPv4 + 2-byte port */
|
||||
if (data.size() < sizeof(socks5_req)) return false;
|
||||
const auto* reply = reinterpret_cast<const socks5_req*>(data.data());
|
||||
memcpy(&out->bind_ip4, &reply->dst_ip4, sizeof(reply->dst_ip4));
|
||||
memcpy(&out->bind_port, &reply->dst_port, sizeof(reply->dst_port));
|
||||
if (len < sizeof(socks5_req)) {
|
||||
return false;
|
||||
}
|
||||
socks5_req reply;
|
||||
memcpy(&reply, data, sizeof(reply));
|
||||
memcpy(&out->bind_ip4, &reply.dst_ip4, sizeof(reply.dst_ip4));
|
||||
memcpy(&out->bind_port, &reply.dst_port, sizeof(reply.dst_port));
|
||||
} else if (out->atyp == SOCKS5_ATYP_IPV6) {
|
||||
/* Full reply: 4-byte header + 16-byte IPv6 + 2-byte port */
|
||||
if (data.size() < sizeof(socks5_req6)) return false;
|
||||
const auto* reply = reinterpret_cast<const socks5_req6*>(data.data());
|
||||
memcpy(&out->bind_port, &reply->dst_port, sizeof(reply->dst_port));
|
||||
if (len < sizeof(socks5_req6)) {
|
||||
return false;
|
||||
}
|
||||
socks5_req6 reply;
|
||||
memcpy(&reply, data, sizeof(reply));
|
||||
memcpy(&out->bind_port, &reply.dst_port, sizeof(reply.dst_port));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int send_http_connect(int fd, const uint8_t* addr, uint16_t port_nbo, bool is_ipv6) {
|
||||
std::string addr_str =
|
||||
is_ipv6 ? ip6_to_string(addr) : ip4_to_string(*(const uint32_t*)addr);
|
||||
char addr_str[INET6_ADDRSTRLEN];
|
||||
if (is_ipv6) {
|
||||
inet_ntop(AF_INET6, addr, addr_str, sizeof(addr_str));
|
||||
} else {
|
||||
uint32_t ip4;
|
||||
memcpy(&ip4, addr, 4);
|
||||
inet_ntop(AF_INET, &ip4, addr_str, sizeof(addr_str));
|
||||
}
|
||||
uint16_t port = ntohs(port_nbo);
|
||||
|
||||
/* Max: "CONNECT [" + 39-char IPv6 + "]:65535 HTTP/1.1\r\nHost: [" + 39 + "]:65535\r\n\r\n"
|
||||
*/
|
||||
char buf[256];
|
||||
char buf[160];
|
||||
int n;
|
||||
if (is_ipv6) {
|
||||
n = snprintf(buf, sizeof(buf), "CONNECT [%s]:%u HTTP/1.1\r\nHost: [%s]:%u\r\n\r\n",
|
||||
addr_str.c_str(), port, addr_str.c_str(), port);
|
||||
addr_str, port, addr_str, port);
|
||||
} else {
|
||||
n = snprintf(buf, sizeof(buf), "CONNECT %s:%u HTTP/1.1\r\nHost: %s:%u\r\n\r\n",
|
||||
addr_str.c_str(), port, addr_str.c_str(), port);
|
||||
addr_str, port, addr_str, port);
|
||||
}
|
||||
|
||||
if (n <= 0 || n >= (int)sizeof(buf)) return -1;
|
||||
if (send(fd, buf, n, MSG_NOSIGNAL) != (ssize_t)n) return -1;
|
||||
if (n <= 0 || n >= (int)sizeof(buf)) {
|
||||
return -1;
|
||||
}
|
||||
if (TEMP_FAILURE_RETRY(send(fd, buf, n, MSG_NOSIGNAL)) != (ssize_t)n) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t find_end_of_headers(const std::vector<uint8_t>& buf) {
|
||||
for (size_t i = 0; i + 3 < buf.size(); ++i) {
|
||||
if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' &&
|
||||
buf[i + 3] == '\n') {
|
||||
size_t find_end_of_headers(const uint8_t* data, size_t len) {
|
||||
for (size_t i = 0; i + 3 < len; ++i) {
|
||||
if (data[i] == '\r' && data[i + 1] == '\n' && data[i + 2] == '\r' &&
|
||||
data[i + 3] == '\n') {
|
||||
return i + 4;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool parse_http_connect_reply(const std::vector<uint8_t>& buf) {
|
||||
bool parse_http_connect_reply(const uint8_t* data, size_t len) {
|
||||
/* Minimum valid response: "HTTP/1.x 2xx" = 12 chars.
|
||||
* We only check the version prefix and status class (2xx). */
|
||||
static constexpr std::string_view HTTP_VERSION_PREFIX = "HTTP/1.";
|
||||
const char* HTTP_VERSION_PREFIX = "HTTP/1.";
|
||||
/* Offset of the first status digit in "HTTP/1.x NNN": H(0)T(1)T(2)P(3)/(4)1(5).(6)x(7)
|
||||
* (8)N(9) */
|
||||
static constexpr size_t HTTP_STATUS_DIGIT_OFFSET = 9;
|
||||
static constexpr size_t HTTP_RESPONSE_MIN_LEN =
|
||||
constexpr size_t HTTP_STATUS_DIGIT_OFFSET = 9;
|
||||
constexpr size_t HTTP_RESPONSE_MIN_LEN =
|
||||
HTTP_STATUS_DIGIT_OFFSET + 3; /* digit + two more chars of status */
|
||||
|
||||
if (buf.size() < HTTP_RESPONSE_MIN_LEN) return false;
|
||||
if (memcmp(buf.data(), HTTP_VERSION_PREFIX.data(), HTTP_VERSION_PREFIX.size()) != 0)
|
||||
if (len < HTTP_RESPONSE_MIN_LEN) {
|
||||
return false;
|
||||
}
|
||||
if (memcmp(data, HTTP_VERSION_PREFIX, 7) != 0) {
|
||||
return false;
|
||||
}
|
||||
/* Accept any 2xx status code */
|
||||
if (buf[HTTP_STATUS_DIGIT_OFFSET] != '2') return false;
|
||||
if (data[HTTP_STATUS_DIGIT_OFFSET] != '2') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "net_defs.h"
|
||||
|
||||
namespace nstun {
|
||||
@@ -93,15 +90,17 @@ struct Socks5Reply {
|
||||
};
|
||||
|
||||
int send_socks5_greeting(int fd);
|
||||
bool parse_socks5_auth_reply(std::span<const uint8_t> data);
|
||||
bool parse_socks5_auth_reply(const uint8_t* data, size_t len);
|
||||
int send_socks5_connect(int fd, const uint8_t* addr, uint16_t port_nbo, bool is_ipv6);
|
||||
int send_socks5_udp_associate(int fd);
|
||||
bool parse_socks5_connect_reply(std::span<const uint8_t> data, Socks5Reply* out);
|
||||
bool parse_socks5_connect_reply(const uint8_t* data, size_t len, Socks5Reply* out);
|
||||
|
||||
/* HTTP CONNECT helpers */
|
||||
int send_http_connect(int fd, const uint8_t* addr, uint16_t port_nbo, bool is_ipv6);
|
||||
size_t find_end_of_headers(const std::vector<uint8_t>& buf);
|
||||
bool parse_http_connect_reply(const std::vector<uint8_t>& buf);
|
||||
|
||||
size_t find_end_of_headers(const uint8_t* data, size_t len);
|
||||
|
||||
bool parse_http_connect_reply(const uint8_t* data, size_t len);
|
||||
|
||||
} /* namespace nstun */
|
||||
|
||||
|
||||
690
nstun/icmp.cc
690
nstun/icmp.cc
@@ -4,6 +4,7 @@
|
||||
#include <netinet/icmp6.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/ip_icmp.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/socket.h>
|
||||
@@ -17,94 +18,166 @@
|
||||
|
||||
namespace nstun {
|
||||
|
||||
static IcmpFlow* find_ipv4_icmp_flow(Context* ctx, const IcmpFlowKey4& key4) {
|
||||
size_t active_seen = 0;
|
||||
for (size_t i = 0; i < NSTUN_MAX_FLOWS; ++i) {
|
||||
IcmpFlow& flow = ctx->c_ipv4_icmp_flows[i];
|
||||
if (flow.header.active) {
|
||||
if (memcmp(&flow.header.icmp_key4, &key4, sizeof(key4)) == 0) {
|
||||
flow.header.last_active = time(nullptr);
|
||||
return &flow;
|
||||
}
|
||||
active_seen++;
|
||||
if (active_seen >= ctx->num_c_ipv4_icmp_flows) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static IcmpFlow* find_ipv6_icmp_flow(Context* ctx, const IcmpFlowKey6& key6) {
|
||||
size_t active_seen = 0;
|
||||
for (size_t i = 0; i < NSTUN_MAX_FLOWS; ++i) {
|
||||
IcmpFlow& flow = ctx->c_ipv6_icmp_flows[i];
|
||||
if (flow.header.active) {
|
||||
if (memcmp(&flow.header.icmp_key6, &key6, sizeof(key6)) == 0) {
|
||||
flow.header.last_active = time(nullptr);
|
||||
return &flow;
|
||||
}
|
||||
active_seen++;
|
||||
if (active_seen >= ctx->num_c_ipv6_icmp_flows) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static IcmpFlow* alloc_icmp_flow(IcmpFlow* flows, size_t max_flows) {
|
||||
for (size_t i = 0; i < max_flows; ++i) {
|
||||
if (!flows[i].header.active) {
|
||||
return &flows[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static IcmpFlow* alloc_ipv4_icmp_flow(Context* ctx) {
|
||||
return alloc_icmp_flow(ctx->c_ipv4_icmp_flows, NSTUN_MAX_FLOWS);
|
||||
}
|
||||
|
||||
static IcmpFlow* alloc_ipv6_icmp_flow(Context* ctx) {
|
||||
return alloc_icmp_flow(ctx->c_ipv6_icmp_flows, NSTUN_MAX_FLOWS);
|
||||
}
|
||||
|
||||
static void init_icmp_flow_zero(IcmpFlow* flow) {
|
||||
memset(flow, 0, sizeof(IcmpFlow));
|
||||
flow->header.type = FlowType::ICMP;
|
||||
flow->header.host_fd = -1;
|
||||
}
|
||||
|
||||
/* Maximum frame size for an ICMP error packet we will generate.
|
||||
* RFC 792 / RFC 4443 require including the original IP header + 8 bytes
|
||||
* of the triggering datagram. We cap total frame size conservatively. */
|
||||
static constexpr size_t ICMP_ERROR_MAX_FRAME = 128;
|
||||
|
||||
static void icmp_destroy_flow(Context* ctx, IcmpFlow* flow) {
|
||||
if (flow->host_fd != -1) {
|
||||
epoll_ctl(ctx->epoll_fd, EPOLL_CTL_DEL, flow->host_fd, nullptr);
|
||||
ctx->flows_by_fd.erase(flow->host_fd);
|
||||
/* close() is handled by ~IcmpFlow via unique_ptr destruction below */
|
||||
if (!flow->header.active) {
|
||||
return;
|
||||
}
|
||||
if (flow->is_ipv6) {
|
||||
ctx->ipv6_icmp_flows_by_key.erase(flow->key6);
|
||||
|
||||
if (flow->header.host_fd != -1) {
|
||||
monitor::removeFd(flow->header.host_fd);
|
||||
close(flow->header.host_fd);
|
||||
if (flow->header.host_fd >= 0 &&
|
||||
static_cast<size_t>(flow->header.host_fd) < nstun::NSTUN_MAX_FDS) {
|
||||
ctx->c_icmp_flows_by_fd[flow->header.host_fd] = nullptr;
|
||||
}
|
||||
flow->header.host_fd = -1;
|
||||
}
|
||||
flow->header.active = false;
|
||||
if (flow->header.is_ipv6) {
|
||||
ctx->num_c_ipv6_icmp_flows--;
|
||||
} else {
|
||||
ctx->ipv4_icmp_flows_by_key.erase(flow->key4);
|
||||
ctx->num_c_ipv4_icmp_flows--;
|
||||
}
|
||||
/* unique_ptr in the owning map runs ~IcmpFlow() which closes host_fd */
|
||||
}
|
||||
|
||||
static void icmp_send_packet4(Context* ctx, uint32_t saddr, uint32_t daddr, uint8_t type,
|
||||
uint8_t code, uint16_t id, uint16_t seq, const uint8_t* data, size_t len) {
|
||||
static bool icmp_send_packet4(Context* ctx, uint32_t saddr, uint32_t daddr, uint8_t type,
|
||||
uint8_t code, uint16_t id, uint16_t seq, const void* data, size_t len) {
|
||||
if (len > NSTUN_MTU) {
|
||||
LOG_W("icmp_send_packet4: data length too large");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t frame_len = sizeof(ip4_hdr) + sizeof(icmp4_hdr) + len;
|
||||
static thread_local uint8_t header_buf[sizeof(ip4_hdr) + sizeof(icmp4_hdr)];
|
||||
uint8_t header_buf[sizeof(ip4_hdr) + sizeof(icmp4_hdr)];
|
||||
memset(header_buf, 0, sizeof(header_buf));
|
||||
|
||||
ip4_hdr* r_ip = reinterpret_cast<ip4_hdr*>(header_buf);
|
||||
icmp4_hdr* r_icmp = reinterpret_cast<icmp4_hdr*>(header_buf + sizeof(ip4_hdr));
|
||||
ip4_hdr ip = {};
|
||||
icmp4_hdr icmp = {};
|
||||
|
||||
/* IPv4 */
|
||||
ip4_set_ihl_version(r_ip, 4, sizeof(ip4_hdr) / 4);
|
||||
r_ip->tos = 0;
|
||||
r_ip->tot_len = htons(frame_len);
|
||||
r_ip->id = 0;
|
||||
r_ip->frag_off = 0;
|
||||
r_ip->ttl = 64;
|
||||
r_ip->protocol = IPPROTO_ICMP;
|
||||
r_ip->saddr = saddr;
|
||||
r_ip->daddr = daddr;
|
||||
r_ip->check = 0;
|
||||
r_ip->check = compute_checksum(r_ip, sizeof(ip4_hdr));
|
||||
ip4_set_ihl_version(&ip, 4, sizeof(ip4_hdr) / 4);
|
||||
ip.tos = 0;
|
||||
ip.tot_len = htons(frame_len);
|
||||
ip.id = 0;
|
||||
ip.frag_off = 0;
|
||||
ip.ttl = 64;
|
||||
ip.protocol = IPPROTO_ICMP;
|
||||
ip.saddr = saddr;
|
||||
ip.daddr = daddr;
|
||||
ip.check = 0;
|
||||
ip.check = compute_checksum(&ip, sizeof(ip4_hdr));
|
||||
|
||||
/* ICMP */
|
||||
r_icmp->type = type;
|
||||
r_icmp->code = code;
|
||||
r_icmp->id = id;
|
||||
r_icmp->seq = seq;
|
||||
r_icmp->check = 0;
|
||||
icmp.type = type;
|
||||
icmp.code = code;
|
||||
icmp.id = id;
|
||||
icmp.seq = seq;
|
||||
icmp.check = 0;
|
||||
|
||||
uint32_t sum = compute_checksum_part(r_icmp, sizeof(icmp4_hdr), 0);
|
||||
uint32_t sum = compute_checksum_part(&icmp, sizeof(icmp4_hdr), 0);
|
||||
if (data && len > 0) {
|
||||
sum = compute_checksum_part(data, len, sum);
|
||||
}
|
||||
r_icmp->check = finalize_checksum(sum);
|
||||
icmp.check = finalize_checksum(sum);
|
||||
|
||||
send_to_guest_v(ctx, header_buf, sizeof(header_buf), data, len);
|
||||
memcpy(header_buf, &ip, sizeof(ip));
|
||||
memcpy(header_buf + sizeof(ip), &icmp, sizeof(icmp));
|
||||
|
||||
return send_to_guest_v(
|
||||
ctx, header_buf, sizeof(header_buf), static_cast<const uint8_t*>(data), len);
|
||||
}
|
||||
|
||||
static void icmp_send_packet6(Context* ctx, const uint8_t* saddr, const uint8_t* daddr,
|
||||
uint8_t type, uint8_t code, uint16_t id, uint16_t seq, const uint8_t* data, size_t len) {
|
||||
static bool icmp_send_packet6(Context* ctx, const uint8_t* saddr, const uint8_t* daddr,
|
||||
uint8_t type, uint8_t code, uint16_t id, uint16_t seq, const void* data, size_t len) {
|
||||
if (len > NSTUN_MTU) {
|
||||
LOG_W("icmp_send_packet6: data length too large");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
static thread_local uint8_t header_buf[sizeof(ip6_hdr) + sizeof(icmp6_hdr)];
|
||||
uint8_t header_buf[sizeof(ip6_hdr) + sizeof(icmp6_hdr)];
|
||||
memset(header_buf, 0, sizeof(header_buf));
|
||||
|
||||
ip6_hdr* r_ip6 = reinterpret_cast<ip6_hdr*>(header_buf);
|
||||
icmp6_hdr* r_icmp6 = reinterpret_cast<icmp6_hdr*>(header_buf + sizeof(ip6_hdr));
|
||||
ip6_hdr ip6 = {};
|
||||
icmp6_hdr icmp6 = {};
|
||||
|
||||
/* IPv6 */
|
||||
r_ip6->vtf = htonl(0x60000000); /* Version 6 */
|
||||
r_ip6->payload_len = htons(sizeof(icmp6_hdr) + len);
|
||||
r_ip6->next_header = IPPROTO_ICMPV6;
|
||||
r_ip6->hop_limit = 64;
|
||||
memcpy(r_ip6->saddr, saddr, sizeof(r_ip6->saddr));
|
||||
memcpy(r_ip6->daddr, daddr, sizeof(r_ip6->daddr));
|
||||
ip6.vtf = htonl(0x60000000); /* Version 6 */
|
||||
ip6.payload_len = htons(sizeof(icmp6_hdr) + len);
|
||||
ip6.next_header = IPPROTO_ICMPV6;
|
||||
ip6.hop_limit = 64;
|
||||
memcpy(ip6.saddr, saddr, sizeof(ip6.saddr));
|
||||
memcpy(ip6.daddr, daddr, sizeof(ip6.daddr));
|
||||
|
||||
/* ICMPv6 */
|
||||
r_icmp6->type = type;
|
||||
r_icmp6->code = code;
|
||||
r_icmp6->id = id;
|
||||
r_icmp6->seq = seq;
|
||||
r_icmp6->check = 0;
|
||||
icmp6.type = type;
|
||||
icmp6.code = code;
|
||||
icmp6.id = id;
|
||||
icmp6.seq = seq;
|
||||
icmp6.check = 0;
|
||||
|
||||
/* 40-byte IPv6 pseudo header */
|
||||
pseudo_hdr6 phdr = {};
|
||||
@@ -114,13 +187,17 @@ static void icmp_send_packet6(Context* ctx, const uint8_t* saddr, const uint8_t*
|
||||
memcpy(phdr.daddr, daddr, sizeof(phdr.daddr));
|
||||
|
||||
uint32_t sum = compute_checksum_part(&phdr, sizeof(phdr), 0);
|
||||
sum = compute_checksum_part(r_icmp6, sizeof(icmp6_hdr), sum);
|
||||
sum = compute_checksum_part(&icmp6, sizeof(icmp6_hdr), sum);
|
||||
if (data && len > 0) {
|
||||
sum = compute_checksum_part(data, len, sum);
|
||||
}
|
||||
r_icmp6->check = finalize_checksum(sum);
|
||||
icmp6.check = finalize_checksum(sum);
|
||||
|
||||
send_to_guest_v(ctx, header_buf, sizeof(header_buf), data, len);
|
||||
memcpy(header_buf, &ip6, sizeof(ip6));
|
||||
memcpy(header_buf + sizeof(ip6), &icmp6, sizeof(icmp6));
|
||||
|
||||
return send_to_guest_v(
|
||||
ctx, header_buf, sizeof(header_buf), static_cast<const uint8_t*>(data), len);
|
||||
}
|
||||
|
||||
void send_icmp4_error(
|
||||
@@ -129,12 +206,18 @@ void send_icmp4_error(
|
||||
/* RFC 792: include IP header + first 8 bytes of original datagram.
|
||||
* Clamp to actual available data to prevent OOB read. */
|
||||
size_t icmp_data_len = req_ihl + 8;
|
||||
if (icmp_data_len > tot_len) icmp_data_len = tot_len;
|
||||
if (icmp_data_len > tot_len) {
|
||||
icmp_data_len = tot_len;
|
||||
}
|
||||
|
||||
if (sizeof(ip4_hdr) + sizeof(icmp4_hdr) + icmp_data_len > ICMP_ERROR_MAX_FRAME) return;
|
||||
if (sizeof(ip4_hdr) + sizeof(icmp4_hdr) + icmp_data_len > ICMP_ERROR_MAX_FRAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
icmp_send_packet4(ctx, req_ip->daddr, req_ip->saddr, type, code, 0, 0,
|
||||
reinterpret_cast<const uint8_t*>(req_ip), icmp_data_len);
|
||||
if (!icmp_send_packet4(
|
||||
ctx, req_ip->daddr, req_ip->saddr, type, code, 0, 0, req_ip, icmp_data_len)) {
|
||||
LOG_W("send_icmp4_error: failed to send ICMP error to guest");
|
||||
}
|
||||
}
|
||||
|
||||
void send_icmp6_error(
|
||||
@@ -142,29 +225,122 @@ void send_icmp6_error(
|
||||
/* IPv6 header + first 8 bytes of original datagram.
|
||||
* Clamp to actual available data to prevent OOB read. */
|
||||
size_t icmp_data_len = sizeof(ip6_hdr) + 8;
|
||||
if (icmp_data_len > tot_len) icmp_data_len = tot_len;
|
||||
if (icmp_data_len > tot_len) {
|
||||
icmp_data_len = tot_len;
|
||||
}
|
||||
|
||||
if (sizeof(ip6_hdr) + sizeof(icmp6_hdr) + icmp_data_len > ICMP_ERROR_MAX_FRAME) return;
|
||||
|
||||
icmp_send_packet6(ctx, req_ip->daddr, req_ip->saddr, type, code, 0, 0,
|
||||
reinterpret_cast<const uint8_t*>(req_ip), icmp_data_len);
|
||||
}
|
||||
|
||||
void handle_icmp6(Context* ctx, const ip6_hdr* ip, std::span<const uint8_t> payload) {
|
||||
if (payload.size() < sizeof(icmp6_hdr)) {
|
||||
if (sizeof(ip6_hdr) + sizeof(icmp6_hdr) + icmp_data_len > ICMP_ERROR_MAX_FRAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
const icmp6_hdr* icmp = reinterpret_cast<const icmp6_hdr*>(payload.data());
|
||||
if (!icmp_send_packet6(
|
||||
ctx, req_ip->daddr, req_ip->saddr, type, code, 0, 0, req_ip, icmp_data_len)) {
|
||||
LOG_W("send_icmp6_error: failed to send ICMPv6 error to guest");
|
||||
}
|
||||
}
|
||||
|
||||
static void proxy_icmp6(Context* ctx, const ip6_hdr* ip, const icmp6_hdr* icmp,
|
||||
const uint8_t* payload, size_t len, const RuleResult& rule) {
|
||||
/* Attempt to proxy ICMP using unprivileged socket */
|
||||
IcmpFlowKey6 key6 = {};
|
||||
memcpy(key6.saddr6, ip->saddr, sizeof(key6.saddr6));
|
||||
memcpy(key6.daddr6, ip->daddr, sizeof(key6.daddr6));
|
||||
key6.id = icmp->id;
|
||||
|
||||
IcmpFlow* flow = find_ipv6_icmp_flow(ctx, key6);
|
||||
|
||||
if (!flow) {
|
||||
if (ctx->num_c_ipv6_icmp_flows >= NSTUN_MAX_FLOWS) {
|
||||
LOG_W("Maximum number of IPv6 ICMP flows reached, dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
flow = alloc_ipv6_icmp_flow(ctx);
|
||||
if (!flow) {
|
||||
LOG_E("Failed to allocate Flow (table full)");
|
||||
return;
|
||||
}
|
||||
|
||||
int fd =
|
||||
socket(AF_INET6, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_ICMPV6);
|
||||
if (fd == -1) {
|
||||
PLOG_W("socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6) failed.");
|
||||
send_icmp6_error(ctx, ip, sizeof(ip6_hdr) + len, ICMP6_DST_UNREACH,
|
||||
ICMP6_DST_UNREACH_ADDR);
|
||||
return;
|
||||
}
|
||||
if (fd >= (int)nstun::NSTUN_MAX_FDS) {
|
||||
LOG_E("FD limit reached (fd=%d)", fd);
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!monitor::addFd(fd, EPOLLIN, host_callback, ctx)) {
|
||||
PLOG_E("monitor::addFd() for ICMPv6 failed");
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
init_icmp_flow_zero(flow);
|
||||
flow->header.active = true;
|
||||
flow->header.host_fd = fd;
|
||||
flow->header.is_ipv6 = true;
|
||||
flow->header.icmp_key6 = key6;
|
||||
|
||||
flow->header.last_active = time(nullptr);
|
||||
flow->header.is_redirected = rule.has_redirect_ip6;
|
||||
memcpy(flow->header.orig_dest_ip6, ip->daddr, sizeof(flow->header.orig_dest_ip6));
|
||||
if (rule.has_redirect_ip6) {
|
||||
memcpy(flow->header.redirect_ip6, rule.redirect_ip6,
|
||||
sizeof(flow->header.redirect_ip6));
|
||||
}
|
||||
|
||||
ctx->num_c_ipv6_icmp_flows++;
|
||||
if (fd >= 0 && static_cast<size_t>(fd) < nstun::NSTUN_MAX_FDS) {
|
||||
ctx->c_icmp_flows_by_fd[fd] = flow;
|
||||
}
|
||||
|
||||
if (flow->header.is_redirected) {
|
||||
LOG_D("Created IPv6 ICMP flow for ID %u (fd=%d) [redirected]",
|
||||
ntohs(key6.id), fd);
|
||||
} else {
|
||||
LOG_D("Created IPv6 ICMP flow for ID %u (fd=%d)", ntohs(key6.id), fd);
|
||||
}
|
||||
}
|
||||
struct sockaddr_in6 dest_addr = INIT_SOCKADDR_IN6(AF_INET6);
|
||||
if (flow->header.is_redirected) {
|
||||
memcpy(
|
||||
&dest_addr.sin6_addr, flow->header.redirect_ip6, sizeof(dest_addr.sin6_addr));
|
||||
} else {
|
||||
/* ip.cc already rejected loopback/v4-mapped destinations */
|
||||
memcpy(
|
||||
&dest_addr.sin6_addr, flow->header.orig_dest_ip6, sizeof(dest_addr.sin6_addr));
|
||||
}
|
||||
|
||||
ssize_t sent = sendto(flow->header.host_fd, payload, len, MSG_NOSIGNAL,
|
||||
reinterpret_cast<const sockaddr*>(&dest_addr), sizeof(dest_addr));
|
||||
if (sent == -1) {
|
||||
PLOG_E("sendto(fd=%d) ICMPv6 failed", flow->header.host_fd);
|
||||
}
|
||||
}
|
||||
|
||||
void handle_icmp6(Context* ctx, const ip6_hdr* ip, const uint8_t* payload, size_t len) {
|
||||
if (len < sizeof(icmp6_hdr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
icmp6_hdr icmp_storage;
|
||||
memcpy(&icmp_storage, payload, sizeof(icmp_storage));
|
||||
const icmp6_hdr* icmp = &icmp_storage;
|
||||
|
||||
/* Validate ICMPv6 checksum (mandatory per RFC 4443) */
|
||||
pseudo_hdr6 phdr = {};
|
||||
phdr.len = htonl(payload.size());
|
||||
phdr.len = htonl(len);
|
||||
phdr.next_header = IPPROTO_ICMPV6;
|
||||
memcpy(phdr.saddr, ip->saddr, sizeof(phdr.saddr));
|
||||
memcpy(phdr.daddr, ip->daddr, sizeof(phdr.daddr));
|
||||
uint32_t csum = compute_checksum_part(&phdr, sizeof(phdr), 0);
|
||||
csum = compute_checksum_part(payload.data(), payload.size(), csum);
|
||||
csum = compute_checksum_part(payload, len, csum);
|
||||
if (finalize_checksum(csum) != 0) {
|
||||
LOG_D("ICMPv6 checksum invalid, dropping");
|
||||
return;
|
||||
@@ -180,8 +356,7 @@ void handle_icmp6(Context* ctx, const ip6_hdr* ip, std::span<const uint8_t> payl
|
||||
return;
|
||||
} else if (rule.action == NSTUN_ACTION_REJECT) {
|
||||
LOG_D("ICMPv6 rejected by policy");
|
||||
send_icmp6_error(ctx, ip, sizeof(ip6_hdr) + payload.size(),
|
||||
ICMP6_DST_UNREACH,
|
||||
send_icmp6_error(ctx, ip, sizeof(ip6_hdr) + len, ICMP6_DST_UNREACH,
|
||||
ICMP6_DST_UNREACH_NOPORT); /* Dest unreachable, port unreachable */
|
||||
return;
|
||||
}
|
||||
@@ -189,112 +364,120 @@ void handle_icmp6(Context* ctx, const ip6_hdr* ip, std::span<const uint8_t> payl
|
||||
if (memcmp(ip->daddr, ctx->host_ip6, IPV6_ADDR_LEN) == 0 &&
|
||||
rule.action != NSTUN_ACTION_REDIRECT) {
|
||||
/* Construct reply (Type 129, Code 0) */
|
||||
const uint8_t* icmp_payload = payload.data() + sizeof(icmp6_hdr);
|
||||
size_t icmp_payload_len = payload.size() - sizeof(icmp6_hdr);
|
||||
icmp_send_packet6(ctx, ip->daddr, ip->saddr, ICMP6_ECHO_REPLY, 0, icmp->id,
|
||||
icmp->seq, icmp_payload, icmp_payload_len);
|
||||
const uint8_t* icmp_payload = payload + sizeof(icmp6_hdr);
|
||||
size_t icmp_payload_len = len - sizeof(icmp6_hdr);
|
||||
if (!icmp_send_packet6(ctx, ip->daddr, ip->saddr, ICMP6_ECHO_REPLY, 0,
|
||||
icmp->id, icmp->seq, icmp_payload, icmp_payload_len)) {
|
||||
LOG_W("handle_icmp6: failed to send Echo Reply");
|
||||
}
|
||||
} else {
|
||||
/* Attempt to proxy ICMP using unprivileged socket */
|
||||
IcmpFlowKey6 key6 = {};
|
||||
memcpy(key6.saddr6, ip->saddr, sizeof(key6.saddr6));
|
||||
memcpy(key6.daddr6, ip->daddr, sizeof(key6.daddr6));
|
||||
key6.id = icmp->id;
|
||||
|
||||
IcmpFlow* flow = nullptr;
|
||||
auto it = ctx->ipv6_icmp_flows_by_key.find(key6);
|
||||
|
||||
if (it != ctx->ipv6_icmp_flows_by_key.end()) {
|
||||
flow = it->second.get();
|
||||
flow->last_active = time(NULL);
|
||||
} else {
|
||||
if (ctx->ipv6_icmp_flows_by_key.size() >= NSTUN_MAX_FLOWS) {
|
||||
LOG_W("Maximum number of IPv6 ICMP flows (%zu) reached, "
|
||||
"dropping",
|
||||
NSTUN_MAX_FLOWS);
|
||||
return;
|
||||
}
|
||||
|
||||
int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
|
||||
IPPROTO_ICMPV6);
|
||||
if (fd == -1) {
|
||||
PLOG_W("socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6) "
|
||||
"failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
defer {
|
||||
if (!success) {
|
||||
close(fd);
|
||||
}
|
||||
};
|
||||
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = fd}};
|
||||
if (epoll_ctl(ctx->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
|
||||
PLOG_E("epoll_ctl(EPOLL_CTL_ADD) for ICMPv6 failed");
|
||||
return;
|
||||
}
|
||||
|
||||
success = true;
|
||||
|
||||
auto flow_ptr = std::make_unique<IcmpFlow>();
|
||||
flow = flow_ptr.get();
|
||||
flow->host_fd = fd;
|
||||
flow->is_ipv6 = true;
|
||||
flow->key6 = key6;
|
||||
flow->last_active = time(NULL);
|
||||
flow->is_redirected = rule.has_redirect_ip6;
|
||||
memcpy(flow->orig_dest_ip6, ip->daddr, sizeof(flow->orig_dest_ip6));
|
||||
|
||||
ctx->ipv6_icmp_flows_by_key[key6] = std::move(flow_ptr);
|
||||
ctx->flows_by_fd[fd] = flow;
|
||||
|
||||
if (flow->is_redirected) {
|
||||
LOG_D(
|
||||
"Created IPv6 ICMP flow for ID %u (fd=%d) [redirected]",
|
||||
ntohs(key6.id), fd);
|
||||
} else {
|
||||
LOG_D("Created IPv6 ICMP flow for ID %u (fd=%d)",
|
||||
ntohs(key6.id), fd);
|
||||
}
|
||||
}
|
||||
struct sockaddr_in6 dest_addr = INIT_SOCKADDR_IN6(AF_INET6);
|
||||
if (rule.has_redirect_ip6) {
|
||||
memcpy(&dest_addr.sin6_addr, rule.redirect_ip6,
|
||||
sizeof(dest_addr.sin6_addr));
|
||||
} else {
|
||||
/* ip.cc already rejected loopback/v4-mapped destinations */
|
||||
memcpy(
|
||||
&dest_addr.sin6_addr, ip->daddr, sizeof(dest_addr.sin6_addr));
|
||||
}
|
||||
|
||||
ssize_t sent = sendto(flow->host_fd, payload.data(), payload.size(),
|
||||
MSG_NOSIGNAL, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
|
||||
if (sent == -1) {
|
||||
PLOG_E("sendto(fd=%d) ICMPv6 failed", flow->host_fd);
|
||||
}
|
||||
proxy_icmp6(ctx, ip, icmp, payload, len, rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handle_icmp4(Context* ctx, const ip4_hdr* ip, std::span<const uint8_t> payload) {
|
||||
if (payload.size() < sizeof(icmp4_hdr)) {
|
||||
static void proxy_icmp4(Context* ctx, const ip4_hdr* ip, const icmp4_hdr* icmp,
|
||||
const uint8_t* payload, size_t len, const RuleResult& rule) {
|
||||
/* Attempt to proxy ICMP using unprivileged socket */
|
||||
IcmpFlowKey4 key4 = {ip->saddr, ip->daddr, icmp->id};
|
||||
LOG_D("handle_icmp4: looking up flow");
|
||||
IcmpFlow* flow = find_ipv4_icmp_flow(ctx, key4);
|
||||
LOG_D("handle_icmp4: flow lookup done, flow=%p", flow);
|
||||
|
||||
if (!flow) {
|
||||
if (ctx->num_c_ipv4_icmp_flows >= NSTUN_MAX_FLOWS) {
|
||||
LOG_W("Maximum number of ICMP flows reached, dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
flow = alloc_ipv4_icmp_flow(ctx);
|
||||
if (!flow) {
|
||||
LOG_E("Failed to allocate Flow (table full)");
|
||||
return;
|
||||
}
|
||||
|
||||
int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_ICMP);
|
||||
if (fd == -1) {
|
||||
PLOG_W("socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) failed. "
|
||||
"You may need: sysctl -w net.ipv4.ping_group_range='0 2147483647'");
|
||||
send_icmp4_error(ctx, ip, ntohs(ip->tot_len), ICMP_DEST_UNREACH,
|
||||
ICMP_HOST_UNREACH); /* host unreachable */
|
||||
return;
|
||||
}
|
||||
if (fd >= (int)nstun::NSTUN_MAX_FDS) {
|
||||
LOG_E("FD limit reached (fd=%d)", fd);
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!monitor::addFd(fd, EPOLLIN, host_callback, ctx)) {
|
||||
PLOG_E("monitor::addFd() for ICMP failed");
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Initialize flow */
|
||||
init_icmp_flow_zero(flow);
|
||||
flow->header.active = true;
|
||||
flow->header.host_fd = fd;
|
||||
flow->header.is_ipv6 = false;
|
||||
flow->header.icmp_key4 = key4;
|
||||
flow->header.last_active = time(nullptr);
|
||||
|
||||
flow->header.is_redirected = (rule.redirect_ip4 != 0);
|
||||
flow->header.orig_dest_ip4 = ip->daddr;
|
||||
flow->header.redirect_ip4 = rule.redirect_ip4;
|
||||
|
||||
ctx->num_c_ipv4_icmp_flows++;
|
||||
if (fd >= 0 && static_cast<size_t>(fd) < nstun::NSTUN_MAX_FDS) {
|
||||
ctx->c_icmp_flows_by_fd[fd] = flow;
|
||||
}
|
||||
|
||||
if (flow->header.is_redirected) {
|
||||
LOG_D("Created ICMP flow for ID %u (fd=%d) [redirected to %s]",
|
||||
ntohs(key4.id), fd, ip4_to_string(rule.redirect_ip4).c_str());
|
||||
} else {
|
||||
LOG_D("Created ICMP flow for ID %u (fd=%d)", ntohs(key4.id), fd);
|
||||
}
|
||||
}
|
||||
|
||||
struct sockaddr_in dest_addr = INIT_SOCKADDR_IN(AF_INET);
|
||||
dest_addr.sin_addr.s_addr =
|
||||
flow->header.is_redirected ? flow->header.redirect_ip4 : flow->header.orig_dest_ip4;
|
||||
|
||||
ssize_t sent = sendto(flow->header.host_fd, payload, len, MSG_NOSIGNAL,
|
||||
reinterpret_cast<const sockaddr*>(&dest_addr), sizeof(dest_addr));
|
||||
if (sent == -1) {
|
||||
PLOG_E("sendto(fd=%d) ICMP failed", flow->header.host_fd);
|
||||
}
|
||||
}
|
||||
|
||||
void handle_icmp4(Context* ctx, const ip4_hdr* ip, const uint8_t* payload, size_t len) {
|
||||
LOG_D("handle_icmp4: started, payload size=%zu", len);
|
||||
if (len < sizeof(icmp4_hdr)) {
|
||||
LOG_D("handle_icmp4: payload too small");
|
||||
return;
|
||||
}
|
||||
|
||||
const icmp4_hdr* icmp = reinterpret_cast<const icmp4_hdr*>(payload.data());
|
||||
icmp4_hdr icmp_storage;
|
||||
memcpy(&icmp_storage, payload, sizeof(icmp_storage));
|
||||
const icmp4_hdr* icmp = &icmp_storage;
|
||||
|
||||
/* Validate ICMP checksum */
|
||||
if (compute_checksum(payload.data(), payload.size()) != 0) {
|
||||
LOG_D("handle_icmp4: validating checksum");
|
||||
if (compute_checksum(payload, len) != 0) {
|
||||
LOG_D("Invalid ICMP checksum, dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
/* We only handle Echo Request (Type 8, Code 0) */
|
||||
LOG_D("handle_icmp4: type=%u, code=%u", icmp->type, icmp->code);
|
||||
if (icmp->type == ICMP_ECHO && icmp->code == 0) {
|
||||
LOG_D("handle_icmp4: evaluating rules");
|
||||
RuleResult rule = evaluate_rules4(
|
||||
ctx, NSTUN_DIR_GUEST_TO_HOST, NSTUN_PROTO_ICMP, ip->saddr, ip->daddr, 0, 0);
|
||||
|
||||
LOG_D("handle_icmp4: rule action=%d", rule.action);
|
||||
if (rule.action == NSTUN_ACTION_DROP) {
|
||||
LOG_D("ICMP dropped by policy");
|
||||
return;
|
||||
@@ -305,120 +488,34 @@ void handle_icmp4(Context* ctx, const ip4_hdr* ip, std::span<const uint8_t> payl
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_D("handle_icmp4: checking dest IP");
|
||||
if (ip->daddr == ctx->host_ip4 && rule.action != NSTUN_ACTION_REDIRECT) {
|
||||
LOG_D("handle_icmp4: sending echo reply");
|
||||
/* Construct reply (Type 0, Code 0) */
|
||||
const uint8_t* icmp_payload = payload.data() + sizeof(icmp4_hdr);
|
||||
size_t icmp_payload_len = payload.size() - sizeof(icmp4_hdr);
|
||||
icmp_send_packet4(ctx, ip->daddr, ip->saddr, ICMP_ECHOREPLY, 0, icmp->id,
|
||||
icmp->seq, icmp_payload, icmp_payload_len);
|
||||
const uint8_t* icmp_payload = payload + sizeof(icmp4_hdr);
|
||||
size_t icmp_payload_len = len - sizeof(icmp4_hdr);
|
||||
if (!icmp_send_packet4(ctx, ip->daddr, ip->saddr, ICMP_ECHOREPLY, 0,
|
||||
icmp->id, icmp->seq, icmp_payload, icmp_payload_len)) {
|
||||
LOG_W("handle_icmp4: failed to send Echo Reply");
|
||||
}
|
||||
} else {
|
||||
/* Attempt to proxy ICMP using unprivileged socket */
|
||||
IcmpFlowKey4 key4 = {ip->saddr, ip->daddr, icmp->id};
|
||||
IcmpFlow* flow = nullptr;
|
||||
auto it = ctx->ipv4_icmp_flows_by_key.find(key4);
|
||||
|
||||
if (it != ctx->ipv4_icmp_flows_by_key.end()) {
|
||||
flow = it->second.get();
|
||||
flow->last_active = time(NULL);
|
||||
} else {
|
||||
if (ctx->ipv4_icmp_flows_by_key.size() >= NSTUN_MAX_FLOWS) {
|
||||
LOG_W(
|
||||
"Maximum number of ICMP flows (%zu) reached, dropping",
|
||||
NSTUN_MAX_FLOWS);
|
||||
return;
|
||||
}
|
||||
|
||||
int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
|
||||
IPPROTO_ICMP);
|
||||
if (fd == -1) {
|
||||
PLOG_W(
|
||||
"socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) "
|
||||
"failed. "
|
||||
"You may need: sysctl -w net.ipv4.ping_group_range='0 "
|
||||
"2147483647'");
|
||||
send_icmp4_error(ctx, ip, ntohs(ip->tot_len),
|
||||
ICMP_DEST_UNREACH,
|
||||
ICMP_HOST_UNREACH); /* host unreachable */
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
defer {
|
||||
if (!success) {
|
||||
close(fd);
|
||||
}
|
||||
};
|
||||
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = fd}};
|
||||
if (epoll_ctl(ctx->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
|
||||
PLOG_E("epoll_ctl(EPOLL_CTL_ADD) for ICMP failed");
|
||||
return;
|
||||
}
|
||||
|
||||
success = true;
|
||||
|
||||
auto flow_ptr = std::make_unique<IcmpFlow>();
|
||||
flow = flow_ptr.get();
|
||||
flow->host_fd = fd;
|
||||
flow->is_ipv6 = false;
|
||||
flow->key4 = key4;
|
||||
flow->last_active = time(NULL);
|
||||
flow->is_redirected = (rule.redirect_ip4 != 0);
|
||||
flow->orig_dest_ip4 = ip->daddr;
|
||||
|
||||
ctx->ipv4_icmp_flows_by_key[key4] = std::move(flow_ptr);
|
||||
ctx->flows_by_fd[fd] = flow;
|
||||
|
||||
if (flow->is_redirected) {
|
||||
LOG_D("Created ICMP flow for ID %u (fd=%d) [redirected to "
|
||||
"%s]",
|
||||
ntohs(key4.id), fd,
|
||||
ip4_to_string(rule.redirect_ip4).c_str());
|
||||
} else {
|
||||
LOG_D("Created ICMP flow for ID %u (fd=%d)", ntohs(key4.id),
|
||||
fd);
|
||||
}
|
||||
}
|
||||
/* ip.cc already rejected loopback destinations */
|
||||
struct sockaddr_in dest_addr = INIT_SOCKADDR_IN(AF_INET);
|
||||
dest_addr.sin_addr.s_addr =
|
||||
(rule.redirect_ip4 != 0) ? rule.redirect_ip4 : ip->daddr;
|
||||
|
||||
ssize_t sent = sendto(flow->host_fd, payload.data(), payload.size(),
|
||||
MSG_NOSIGNAL, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
|
||||
if (sent == -1) {
|
||||
PLOG_E("sendto(fd=%d) ICMP failed", flow->host_fd);
|
||||
}
|
||||
LOG_D("handle_icmp4: proxying ICMP");
|
||||
proxy_icmp4(ctx, ip, icmp, payload, len, rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_host_icmp(Context* ctx, IcmpFlow* flow) {
|
||||
int fd = flow->host_fd;
|
||||
flow->last_active = time(NULL);
|
||||
|
||||
constexpr int VLEN = 64;
|
||||
struct mmsghdr msgs[VLEN];
|
||||
struct iovec iovecs[VLEN];
|
||||
static thread_local std::unique_ptr<uint8_t[][NSTUN_MTU]> bufs_ptr;
|
||||
if (!bufs_ptr) {
|
||||
bufs_ptr = std::make_unique<uint8_t[][NSTUN_MTU]>(VLEN);
|
||||
}
|
||||
uint8_t (*bufs)[NSTUN_MTU] = bufs_ptr.get();
|
||||
static thread_local struct sockaddr_storage src_addrs[VLEN];
|
||||
int fd = flow->header.host_fd;
|
||||
flow->header.last_active = time(nullptr);
|
||||
|
||||
for (int i = 0; i < VLEN; ++i) {
|
||||
iovecs[i].iov_base = bufs[i];
|
||||
iovecs[i].iov_len = sizeof(bufs[i]);
|
||||
msgs[i].msg_hdr.msg_iov = &iovecs[i];
|
||||
msgs[i].msg_hdr.msg_iovlen = 1;
|
||||
msgs[i].msg_hdr.msg_name = &src_addrs[i];
|
||||
msgs[i].msg_hdr.msg_namelen = sizeof(src_addrs[i]);
|
||||
msgs[i].msg_hdr.msg_control = nullptr;
|
||||
msgs[i].msg_hdr.msg_controllen = 0;
|
||||
ctx->recvmmsg_msgs[i].msg_hdr.msg_namelen = sizeof(ctx->recvmmsg_addrs[i]);
|
||||
ctx->recvmmsg_msgs[i].msg_hdr.msg_controllen = 0;
|
||||
}
|
||||
|
||||
int retval = recvmmsg(fd, msgs, VLEN, MSG_DONTWAIT, nullptr);
|
||||
int retval =
|
||||
TEMP_FAILURE_RETRY(recvmmsg(fd, ctx->recvmmsg_msgs, VLEN, MSG_DONTWAIT, nullptr));
|
||||
if (retval == -1) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return;
|
||||
@@ -427,68 +524,81 @@ static void handle_host_icmp(Context* ctx, IcmpFlow* flow) {
|
||||
icmp_destroy_flow(ctx, flow);
|
||||
return;
|
||||
}
|
||||
if (retval == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < retval; ++i) {
|
||||
uint8_t* data_ptr = bufs[i];
|
||||
size_t recv_len = msgs[i].msg_len;
|
||||
struct sockaddr_storage* src_addr_storage = &src_addrs[i];
|
||||
uint8_t* data_ptr = ctx->recvmmsg_bufs[i];
|
||||
size_t recv_len = ctx->recvmmsg_msgs[i].msg_len;
|
||||
const struct sockaddr_storage* src_addr_storage = &ctx->recvmmsg_addrs[i];
|
||||
|
||||
if (flow->is_ipv6) {
|
||||
if (flow->header.is_ipv6) {
|
||||
if ((size_t)recv_len >= sizeof(icmp6_hdr)) {
|
||||
icmp6_hdr* r_icmp6 = reinterpret_cast<icmp6_hdr*>(data_ptr);
|
||||
icmp6_hdr icmp6;
|
||||
memcpy(&icmp6, data_ptr, sizeof(icmp6));
|
||||
const uint8_t* icmp_payload = data_ptr + sizeof(icmp6_hdr);
|
||||
size_t icmp_payload_len = recv_len - sizeof(icmp6_hdr);
|
||||
|
||||
uint8_t saddr6[IPV6_ADDR_LEN];
|
||||
if (flow->is_redirected) {
|
||||
memcpy(saddr6, flow->orig_dest_ip6, sizeof(saddr6));
|
||||
if (flow->header.is_redirected) {
|
||||
memcpy(saddr6, flow->header.orig_dest_ip6, sizeof(saddr6));
|
||||
} else {
|
||||
struct sockaddr_in6* src6 =
|
||||
reinterpret_cast<struct sockaddr_in6*>(
|
||||
const struct sockaddr_in6* src6 =
|
||||
reinterpret_cast<const struct sockaddr_in6*>(
|
||||
src_addr_storage);
|
||||
memcpy(saddr6, &src6->sin6_addr, sizeof(saddr6));
|
||||
}
|
||||
|
||||
icmp_send_packet6(ctx, saddr6, flow->key6.saddr6, r_icmp6->type,
|
||||
r_icmp6->code, flow->key6.id, r_icmp6->seq, icmp_payload,
|
||||
icmp_payload_len);
|
||||
if (!icmp_send_packet6(ctx, saddr6, flow->header.icmp_key6.saddr6,
|
||||
icmp6.type, icmp6.code, flow->header.icmp_key6.id,
|
||||
icmp6.seq, icmp_payload, icmp_payload_len)) {
|
||||
LOG_W("handle_host_icmp: failed to send ICMPv6 packet to "
|
||||
"guest");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
struct sockaddr_in* src_addr =
|
||||
reinterpret_cast<struct sockaddr_in*>(src_addr_storage);
|
||||
uint32_t saddr =
|
||||
flow->is_redirected ? flow->orig_dest_ip4 : src_addr->sin_addr.s_addr;
|
||||
uint32_t daddr = flow->key4.saddr4;
|
||||
const struct sockaddr_in* src_addr =
|
||||
reinterpret_cast<const struct sockaddr_in*>(src_addr_storage);
|
||||
uint32_t saddr = flow->header.is_redirected ? flow->header.orig_dest_ip4
|
||||
: src_addr->sin_addr.s_addr;
|
||||
uint32_t daddr = flow->header.icmp_key4.saddr4;
|
||||
|
||||
if ((size_t)recv_len >= sizeof(icmp4_hdr)) {
|
||||
icmp4_hdr* r_icmp = reinterpret_cast<icmp4_hdr*>(data_ptr);
|
||||
icmp4_hdr icmp4;
|
||||
memcpy(&icmp4, data_ptr, sizeof(icmp4));
|
||||
const uint8_t* icmp_payload = data_ptr + sizeof(icmp4_hdr);
|
||||
size_t icmp_payload_len = recv_len - sizeof(icmp4_hdr);
|
||||
|
||||
icmp_send_packet4(ctx, saddr, daddr, r_icmp->type, r_icmp->code,
|
||||
flow->key4.id, r_icmp->seq, icmp_payload, icmp_payload_len);
|
||||
if (!icmp_send_packet4(ctx, saddr, daddr, icmp4.type, icmp4.code,
|
||||
flow->header.icmp_key4.id, icmp4.seq, icmp_payload,
|
||||
icmp_payload_len)) {
|
||||
LOG_W("handle_host_icmp: failed to send ICMP packet to "
|
||||
"guest");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IcmpFlow::handle_host_event(Context* ctx, int fd, uint32_t events) {
|
||||
if (fd == this->host_fd) {
|
||||
handle_host_icmp(ctx, this);
|
||||
void icmp_handle_host_event(Context* ctx, IcmpFlow* flow, int fd, uint32_t events) {
|
||||
if (fd == flow->header.host_fd) {
|
||||
handle_host_icmp(ctx, flow);
|
||||
}
|
||||
}
|
||||
|
||||
bool IcmpFlow::is_stale(time_t now) const {
|
||||
return (now - this->last_active) > 10;
|
||||
bool icmp_is_stale(const IcmpFlow* flow, time_t now) {
|
||||
return (now - flow->header.last_active) > 10;
|
||||
}
|
||||
|
||||
void IcmpFlow::destroy(Context* ctx) {
|
||||
if (is_ipv6) {
|
||||
LOG_D("GC: stale ICMP flow (IPv6, id=%u)", ntohs(key6.id));
|
||||
void icmp_destroy(Context* ctx, IcmpFlow* flow) {
|
||||
if (flow->header.is_ipv6) {
|
||||
LOG_D("GC: stale ICMP flow (IPv6, id=%u)", ntohs(flow->header.icmp_key6.id));
|
||||
} else {
|
||||
LOG_D("GC: stale ICMP flow (id=%u)", ntohs(key4.id));
|
||||
LOG_D("GC: stale ICMP flow (id=%u)", ntohs(flow->header.icmp_key4.id));
|
||||
}
|
||||
icmp_destroy_flow(ctx, this);
|
||||
|
||||
icmp_destroy_flow(ctx, flow);
|
||||
}
|
||||
|
||||
} /* namespace nstun */
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "core.h"
|
||||
|
||||
namespace nstun {
|
||||
|
||||
void handle_icmp4(Context* ctx, const ip4_hdr* ip, std::span<const uint8_t> payload);
|
||||
void handle_icmp6(Context* ctx, const ip6_hdr* ip, std::span<const uint8_t> payload);
|
||||
void handle_icmp4(Context* ctx, const ip4_hdr* ip, const uint8_t* payload, size_t len);
|
||||
void handle_icmp6(Context* ctx, const ip6_hdr* ip, const uint8_t* payload, size_t len);
|
||||
|
||||
void send_icmp4_error(
|
||||
Context* ctx, const ip4_hdr* req_ip, size_t tot_len, uint8_t type, uint8_t code);
|
||||
|
||||
@@ -27,7 +27,18 @@ struct in6_ifreq {
|
||||
};
|
||||
|
||||
bool configIface(nsj_t* nsj) {
|
||||
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
struct ifreq ifr = {};
|
||||
struct in_addr addr;
|
||||
struct rtentry rt = {};
|
||||
char rt_dev[IFNAMSIZ];
|
||||
|
||||
if (nsj->njc.user_net().ns_iface().length() >= IFNAMSIZ) {
|
||||
LOG_E("Interface name '%s' is too long (max %d)",
|
||||
nsj->njc.user_net().ns_iface().c_str(), IFNAMSIZ - 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
int sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_IP);
|
||||
if (sock == -1) {
|
||||
PLOG_E("socket(AF_INET, SOCK_STREAM, IPPROTO_IP)");
|
||||
return false;
|
||||
@@ -36,12 +47,7 @@ bool configIface(nsj_t* nsj) {
|
||||
close(sock);
|
||||
};
|
||||
|
||||
struct ifreq ifr = {};
|
||||
snprintf(ifr.ifr_name, IFNAMSIZ, "%s", nsj->njc.user_net().ns_iface().c_str());
|
||||
|
||||
struct in_addr addr;
|
||||
struct sockaddr_in* sa = (struct sockaddr_in*)(&ifr.ifr_addr);
|
||||
|
||||
/* Set IP Address */
|
||||
if (!nsj->njc.user_net().ip4().empty()) {
|
||||
if (inet_pton(AF_INET, nsj->njc.user_net().ip4().c_str(), &addr) != 1) {
|
||||
@@ -49,8 +55,10 @@ bool configIface(nsj_t* nsj) {
|
||||
nsj->njc.user_net().ip4().c_str());
|
||||
return false;
|
||||
}
|
||||
sa->sin_family = AF_INET;
|
||||
sa->sin_addr = addr;
|
||||
struct sockaddr_in sa = {};
|
||||
sa.sin_family = AF_INET;
|
||||
sa.sin_addr = addr;
|
||||
memcpy(&ifr.ifr_addr, &sa, sizeof(sa));
|
||||
if (ioctl(sock, SIOCSIFADDR, &ifr) == -1) {
|
||||
PLOG_E("ioctl(SIOCSIFADDR, '%s')", nsj->njc.user_net().ip4().c_str());
|
||||
return false;
|
||||
@@ -59,14 +67,17 @@ bool configIface(nsj_t* nsj) {
|
||||
|
||||
/* Set Point-to-Point Destination Address (Host/GW) */
|
||||
if (!nsj->njc.user_net().gw4().empty()) {
|
||||
struct sockaddr_in* dst = (struct sockaddr_in*)(&ifr.ifr_dstaddr);
|
||||
ifr = {};
|
||||
snprintf(ifr.ifr_name, IFNAMSIZ, "%s", nsj->njc.user_net().ns_iface().c_str());
|
||||
if (inet_pton(AF_INET, nsj->njc.user_net().gw4().c_str(), &addr) != 1) {
|
||||
LOG_E("Cannot convert '%s' into an IPv4 GW address",
|
||||
nsj->njc.user_net().gw4().c_str());
|
||||
return false;
|
||||
}
|
||||
dst->sin_family = AF_INET;
|
||||
dst->sin_addr = addr;
|
||||
struct sockaddr_in dst_sa = {};
|
||||
dst_sa.sin_family = AF_INET;
|
||||
dst_sa.sin_addr = addr;
|
||||
memcpy(&ifr.ifr_dstaddr, &dst_sa, sizeof(dst_sa));
|
||||
if (ioctl(sock, SIOCSIFDSTADDR, &ifr) == -1) {
|
||||
PLOG_E("ioctl(SIOCSIFDSTADDR, '%s')", nsj->njc.user_net().gw4().c_str());
|
||||
return false;
|
||||
@@ -74,9 +85,12 @@ bool configIface(nsj_t* nsj) {
|
||||
}
|
||||
|
||||
/* Set Netmask to /32 for PtP link */
|
||||
struct sockaddr_in* netmask = (struct sockaddr_in*)(&ifr.ifr_netmask);
|
||||
netmask->sin_family = AF_INET;
|
||||
netmask->sin_addr.s_addr = 0xFFFFFFFF; // 255.255.255.255
|
||||
ifr = {};
|
||||
snprintf(ifr.ifr_name, IFNAMSIZ, "%s", nsj->njc.user_net().ns_iface().c_str());
|
||||
struct sockaddr_in netmask_sa = {};
|
||||
netmask_sa.sin_family = AF_INET;
|
||||
netmask_sa.sin_addr.s_addr = 0xFFFFFFFF; /* 255.255.255.255 */
|
||||
memcpy(&ifr.ifr_netmask, &netmask_sa, sizeof(netmask_sa));
|
||||
if (ioctl(sock, SIOCSIFNETMASK, &ifr) == -1) {
|
||||
PLOG_E("ioctl(SIOCSIFNETMASK, 255.255.255.255)");
|
||||
return false;
|
||||
@@ -93,24 +107,26 @@ bool configIface(nsj_t* nsj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ifr = {};
|
||||
snprintf(ifr.ifr_name, IFNAMSIZ, "%s", nsj->njc.user_net().ns_iface().c_str());
|
||||
ifr.ifr_mtu = NSTUN_MTU;
|
||||
if (ioctl(sock, SIOCSIFMTU, &ifr) == -1) {
|
||||
PLOG_W("ioctl(SIOCSIFMTU, %zu)", NSTUN_MTU);
|
||||
PLOG_E("ioctl(SIOCSIFMTU, %zu)", NSTUN_MTU);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Add default route out of interface */
|
||||
struct rtentry rt = {};
|
||||
struct sockaddr_in* sdest = (struct sockaddr_in*)(&rt.rt_dst);
|
||||
struct sockaddr_in* smask = (struct sockaddr_in*)(&rt.rt_genmask);
|
||||
struct sockaddr_in sdest_sa = {};
|
||||
sdest_sa.sin_family = AF_INET;
|
||||
sdest_sa.sin_addr.s_addr = INADDR_ANY;
|
||||
memcpy(&rt.rt_dst, &sdest_sa, sizeof(sdest_sa));
|
||||
|
||||
sdest->sin_family = AF_INET;
|
||||
sdest->sin_addr.s_addr = INADDR_ANY;
|
||||
smask->sin_family = AF_INET;
|
||||
smask->sin_addr.s_addr = INADDR_ANY;
|
||||
struct sockaddr_in smask_sa = {};
|
||||
smask_sa.sin_family = AF_INET;
|
||||
smask_sa.sin_addr.s_addr = INADDR_ANY;
|
||||
memcpy(&rt.rt_genmask, &smask_sa, sizeof(smask_sa));
|
||||
|
||||
rt.rt_flags = RTF_UP; /* Device route, no gateway necessary for PtP */
|
||||
char rt_dev[IFNAMSIZ];
|
||||
snprintf(rt_dev, sizeof(rt_dev), "%s", nsj->njc.user_net().ns_iface().c_str());
|
||||
rt.rt_dev = rt_dev;
|
||||
|
||||
@@ -123,7 +139,7 @@ bool configIface(nsj_t* nsj) {
|
||||
|
||||
/* Configure IPv6 address and route */
|
||||
if (!nsj->njc.user_net().ip6().empty()) {
|
||||
int sock6 = socket(AF_INET6, SOCK_DGRAM, 0);
|
||||
int sock6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (sock6 == -1) {
|
||||
PLOG_E("socket(AF_INET6, SOCK_DGRAM)");
|
||||
return false;
|
||||
|
||||
215
nstun/ip.cc
215
nstun/ip.cc
@@ -1,6 +1,8 @@
|
||||
#include "ip.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "core.h"
|
||||
@@ -62,42 +64,61 @@ static int skip_ipv6_ext_headers(int next_header, const uint8_t*& ptr, size_t& r
|
||||
return -1; /* Extension header chain too deep */
|
||||
}
|
||||
|
||||
void handle_ip4(Context* ctx, std::span<const uint8_t> payload) {
|
||||
if (payload.size() < sizeof(ip4_hdr)) {
|
||||
static bool extract_l4_ports(
|
||||
int proto, const uint8_t* payload, size_t len, uint16_t* src_port, uint16_t* dest_port) {
|
||||
if (proto == IPPROTO_TCP && len >= sizeof(tcp_hdr)) {
|
||||
tcp_hdr tcp;
|
||||
memcpy(&tcp, payload, sizeof(tcp));
|
||||
*src_port = ntohs(tcp.source);
|
||||
*dest_port = ntohs(tcp.dest);
|
||||
return true;
|
||||
} else if (proto == IPPROTO_UDP && len >= sizeof(udp_hdr)) {
|
||||
udp_hdr udp;
|
||||
memcpy(&udp, payload, sizeof(udp));
|
||||
*src_port = ntohs(udp.source);
|
||||
*dest_port = ntohs(udp.dest);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void handle_ip4(Context* ctx, const uint8_t* payload, size_t len) {
|
||||
if (len < sizeof(ip4_hdr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ip4_hdr* ip = reinterpret_cast<const ip4_hdr*>(payload.data());
|
||||
uint8_t ihl = ip4_ihl(ip) * 4;
|
||||
ip4_hdr ip;
|
||||
memcpy(&ip, payload, sizeof(ip));
|
||||
uint8_t ihl = ip4_ihl(&ip) * 4;
|
||||
|
||||
if (ihl < sizeof(ip4_hdr) || ihl > payload.size()) {
|
||||
if (ihl < sizeof(ip4_hdr) || ihl > len) {
|
||||
LOG_D("Invalid IPv4 IHL");
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t tot_len = ntohs(ip->tot_len);
|
||||
if (tot_len < ihl || tot_len > payload.size()) {
|
||||
uint16_t tot_len = ntohs(ip.tot_len);
|
||||
if (tot_len < ihl || tot_len > len) {
|
||||
LOG_D("Invalid IPv4 tot_len");
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* l4_payload = payload.data() + ihl;
|
||||
const uint8_t* l4_payload = payload + ihl;
|
||||
size_t l4_len = tot_len - ihl;
|
||||
|
||||
/* Drop IP fragments: nstun does not reassemble, and non-first
|
||||
* fragments have no L4 header - parsing them would bypass rules */
|
||||
if (ntohs(ip->frag_off) & 0x3FFF) {
|
||||
if (ntohs(ip.frag_off) & 0x3FFF) {
|
||||
LOG_W("Dropping IPv4 fragment");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Validate IPv4 header checksum */
|
||||
if (compute_checksum(ip, ihl) != 0) {
|
||||
if (compute_checksum(payload, ihl) != 0) {
|
||||
LOG_W("Invalid IPv4 header checksum, dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ip->saddr != ctx->guest_ip4 && ip->saddr != 0) {
|
||||
if (ip.saddr != ctx->guest_ip4 && ip.saddr != 0) {
|
||||
LOG_W("Dropping packet with invalid source IP");
|
||||
return;
|
||||
}
|
||||
@@ -106,72 +127,86 @@ void handle_ip4(Context* ctx, std::span<const uint8_t> payload) {
|
||||
* This is the single authoritative check - L4 handlers rely on this
|
||||
* and do NOT duplicate it. Redirect rules in policy may still target
|
||||
* loopback intentionally (admin-controlled). */
|
||||
if (IN_LOOPBACK(ntohl(ip->daddr)) || ip->daddr == htonl(INADDR_ANY) ||
|
||||
ip->daddr == htonl(INADDR_BROADCAST)) {
|
||||
LOG_W("Dropping packet destined to loopback, ANY, or broadcast: %s",
|
||||
ip4_to_string(ip->daddr).c_str());
|
||||
if (IN_LOOPBACK(ntohl(ip.daddr)) || ip.daddr == htonl(INADDR_ANY) ||
|
||||
ip.daddr == htonl(INADDR_BROADCAST)) {
|
||||
char daddr_str[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &ip.daddr, daddr_str, sizeof(daddr_str));
|
||||
LOG_W("Dropping packet destined to loopback, ANY, or broadcast: %s", daddr_str);
|
||||
return;
|
||||
}
|
||||
uint16_t src_port = 0, dest_port = 0;
|
||||
auto l4_span = payload.subspan(ihl, l4_len);
|
||||
if (ip->protocol == IPPROTO_TCP && l4_span.size() >= sizeof(tcp_hdr)) {
|
||||
const tcp_hdr* tcp = reinterpret_cast<const tcp_hdr*>(l4_span.data());
|
||||
src_port = ntohs(tcp->source);
|
||||
dest_port = ntohs(tcp->dest);
|
||||
} else if (ip->protocol == IPPROTO_UDP && l4_span.size() >= sizeof(udp_hdr)) {
|
||||
const udp_hdr* udp = reinterpret_cast<const udp_hdr*>(l4_span.data());
|
||||
src_port = ntohs(udp->source);
|
||||
dest_port = ntohs(udp->dest);
|
||||
extract_l4_ports(ip.protocol, l4_payload, l4_len, &src_port, &dest_port);
|
||||
|
||||
if (logs::getLogLevel() <= logs::DEBUG) {
|
||||
char saddr_str[INET_ADDRSTRLEN];
|
||||
char daddr_str[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &ip.saddr, saddr_str, sizeof(saddr_str));
|
||||
inet_ntop(AF_INET, &ip.daddr, daddr_str, sizeof(daddr_str));
|
||||
if (src_port != 0 && dest_port != 0) {
|
||||
LOG_D("IP packet: proto=%u, %s:%u -> %s:%u, len=%zu", ip.protocol,
|
||||
saddr_str, src_port, daddr_str, dest_port, l4_len);
|
||||
} else {
|
||||
LOG_D("IP packet: proto=%u, %s -> %s, len=%zu", ip.protocol, saddr_str,
|
||||
daddr_str, l4_len);
|
||||
}
|
||||
}
|
||||
|
||||
if (src_port != 0 && dest_port != 0) {
|
||||
LOG_D("IP packet: proto=%u, %s:%u -> %s:%u, len=%zu", ip->protocol,
|
||||
ip4_to_string(ip->saddr).c_str(), src_port, ip4_to_string(ip->daddr).c_str(),
|
||||
dest_port, l4_len);
|
||||
} else {
|
||||
LOG_D("IP packet: proto=%u, %s -> %s, len=%zu", ip->protocol,
|
||||
ip4_to_string(ip->saddr).c_str(), ip4_to_string(ip->daddr).c_str(), l4_len);
|
||||
}
|
||||
|
||||
switch (ip->protocol) {
|
||||
switch (ip.protocol) {
|
||||
case IPPROTO_ICMP:
|
||||
handle_icmp4(ctx, ip, payload.subspan(ihl, l4_len));
|
||||
LOG_D("Calling handle_icmp4");
|
||||
LOG_D("about to call handle_icmp4: ctx=%p, ip=%p, l4=%p, len=%zu", (void*)ctx,
|
||||
(void*)&ip, (void*)l4_payload, l4_len);
|
||||
handle_icmp4(ctx, &ip, l4_payload, l4_len);
|
||||
break;
|
||||
case IPPROTO_UDP:
|
||||
handle_udp4(ctx, ip, payload.subspan(ihl, l4_len));
|
||||
handle_udp4(ctx, &ip, l4_payload, l4_len);
|
||||
break;
|
||||
case IPPROTO_TCP:
|
||||
handle_tcp4(ctx, ip, payload.subspan(ihl, l4_len));
|
||||
handle_tcp4(ctx, &ip, l4_payload, l4_len);
|
||||
break;
|
||||
default:
|
||||
LOG_D("Unknown IPv4 protocol: %u", ip->protocol);
|
||||
LOG_D("Unknown IPv4 protocol: %u", ip.protocol);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void handle_ip6(Context* ctx, std::span<const uint8_t> payload) {
|
||||
if (payload.size() < sizeof(ip6_hdr)) {
|
||||
void handle_ip6(Context* ctx, const uint8_t* payload, size_t len) {
|
||||
if (len < sizeof(ip6_hdr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ip6_hdr* ip6 = reinterpret_cast<const ip6_hdr*>(payload.data());
|
||||
uint16_t payload_len = ntohs(ip6->payload_len);
|
||||
ip6_hdr ip6;
|
||||
memcpy(&ip6, payload, sizeof(ip6));
|
||||
uint16_t payload_len = ntohs(ip6.payload_len);
|
||||
|
||||
if (payload_len + sizeof(ip6_hdr) > payload.size()) {
|
||||
if (payload_len + sizeof(ip6_hdr) > len) {
|
||||
LOG_D("Invalid IPv6 payload_len");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Source IP filtering */
|
||||
if (memcmp(ip6->saddr, ctx->guest_ip6, IPV6_ADDR_LEN) != 0) {
|
||||
if (IN6_IS_ADDR_LINKLOCAL((const struct in6_addr*)ip6->saddr) ||
|
||||
IN6_IS_ADDR_SITELOCAL((const struct in6_addr*)ip6->saddr)) {
|
||||
LOG_D("Dropping IPv6 packet with link/site-local source address: %s",
|
||||
ip6_to_string(ip6->saddr).c_str());
|
||||
if (memcmp(ip6.saddr, ctx->guest_ip6, IPV6_ADDR_LEN) != 0) {
|
||||
struct in6_addr saddr;
|
||||
memcpy(&saddr, ip6.saddr, sizeof(saddr));
|
||||
if (IN6_IS_ADDR_LINKLOCAL(&saddr)) {
|
||||
/* Allow link-local addresses from guest on the local link */
|
||||
if (logs::getLogLevel() <= logs::DEBUG) {
|
||||
char saddr_str[INET6_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET6, ip6.saddr, saddr_str, sizeof(saddr_str));
|
||||
LOG_D("Allowing link-local source address: %s", saddr_str);
|
||||
}
|
||||
} else if (IN6_IS_ADDR_SITELOCAL(&saddr)) {
|
||||
if (logs::getLogLevel() <= logs::DEBUG) {
|
||||
char saddr_str[INET6_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET6, ip6.saddr, saddr_str, sizeof(saddr_str));
|
||||
LOG_D("Dropping IPv6 packet with site-local source address: %s",
|
||||
saddr_str);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
LOG_W("Dropping IPv6 packet with spoofed source address: %s",
|
||||
ip6_to_string(ip6->saddr).c_str());
|
||||
char saddr_str[INET6_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET6, ip6.saddr, saddr_str, sizeof(saddr_str));
|
||||
LOG_W("Dropping IPv6 packet with spoofed source address: %s", saddr_str);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -180,24 +215,40 @@ void handle_ip6(Context* ctx, std::span<const uint8_t> payload) {
|
||||
* This is the single authoritative check - L4 handlers rely on this
|
||||
* and do NOT duplicate it. Redirect rules in policy may still target
|
||||
* ::1 intentionally (admin-controlled). */
|
||||
if (IN6_IS_ADDR_LOOPBACK((const struct in6_addr*)ip6->daddr)) {
|
||||
LOG_D("Dropping IPv6 packet to loopback: %s", ip6_to_string(ip6->daddr).c_str());
|
||||
struct in6_addr daddr;
|
||||
memcpy(&daddr, ip6.daddr, sizeof(daddr));
|
||||
if (IN6_IS_ADDR_LOOPBACK(&daddr)) {
|
||||
if (logs::getLogLevel() <= logs::DEBUG) {
|
||||
char daddr_str[INET6_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET6, ip6.daddr, daddr_str, sizeof(daddr_str));
|
||||
LOG_D("Dropping IPv6 packet to loopback: %s", daddr_str);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (IN6_IS_ADDR_V4MAPPED((const struct in6_addr*)ip6->daddr)) {
|
||||
LOG_D("Dropping IPv6 packet to v4-mapped address (use IPv4 directly): %s",
|
||||
ip6_to_string(ip6->daddr).c_str());
|
||||
if (IN6_IS_ADDR_V4MAPPED(&daddr)) {
|
||||
if (logs::getLogLevel() <= logs::DEBUG) {
|
||||
char daddr_str[INET6_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET6, ip6.daddr, daddr_str, sizeof(daddr_str));
|
||||
LOG_D("Dropping IPv6 packet to v4-mapped address (use IPv4 directly): %s",
|
||||
daddr_str);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (IN6_IS_ADDR_V4COMPAT((const struct in6_addr*)ip6->daddr)) {
|
||||
LOG_D("Dropping IPv6 packet to v4-compatible address (deprecated): %s",
|
||||
ip6_to_string(ip6->daddr).c_str());
|
||||
if (IN6_IS_ADDR_V4COMPAT(&daddr)) {
|
||||
if (logs::getLogLevel() <= logs::DEBUG) {
|
||||
char daddr_str[INET6_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET6, ip6.daddr, daddr_str, sizeof(daddr_str));
|
||||
LOG_D("Dropping IPv6 packet to v4-compatible address (deprecated): %s",
|
||||
daddr_str);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (IN6_IS_ADDR_LINKLOCAL((const struct in6_addr*)ip6->daddr) ||
|
||||
IN6_IS_ADDR_SITELOCAL((const struct in6_addr*)ip6->daddr)) {
|
||||
LOG_D("Dropping IPv6 packet to link/site-local address: %s",
|
||||
ip6_to_string(ip6->daddr).c_str());
|
||||
if (IN6_IS_ADDR_LINKLOCAL(&daddr) || IN6_IS_ADDR_SITELOCAL(&daddr)) {
|
||||
if (logs::getLogLevel() <= logs::DEBUG) {
|
||||
char daddr_str[INET6_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET6, ip6.daddr, daddr_str, sizeof(daddr_str));
|
||||
LOG_D("Dropping IPv6 packet to link/site-local address: %s", daddr_str);
|
||||
}
|
||||
return;
|
||||
}
|
||||
/*
|
||||
@@ -207,45 +258,41 @@ void handle_ip6(Context* ctx, std::span<const uint8_t> payload) {
|
||||
* Each header's "Next Header" field identifies what follows.
|
||||
* We only need to find the L4 header, not process the extensions.
|
||||
*/
|
||||
const uint8_t* l4_payload = payload.data() + sizeof(ip6_hdr);
|
||||
const uint8_t* l4_payload = payload + sizeof(ip6_hdr);
|
||||
size_t remaining = payload_len;
|
||||
|
||||
int l4_proto = skip_ipv6_ext_headers(ip6->next_header, l4_payload, remaining);
|
||||
int l4_proto = skip_ipv6_ext_headers(ip6.next_header, l4_payload, remaining);
|
||||
if (l4_proto < 0) {
|
||||
LOG_D("Failed to parse IPv6 extension headers");
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t src_port = 0, dest_port = 0;
|
||||
if (l4_proto == IPPROTO_TCP && remaining >= sizeof(tcp_hdr)) {
|
||||
const tcp_hdr* tcp = reinterpret_cast<const tcp_hdr*>(l4_payload);
|
||||
src_port = ntohs(tcp->source);
|
||||
dest_port = ntohs(tcp->dest);
|
||||
} else if (l4_proto == IPPROTO_UDP && remaining >= sizeof(udp_hdr)) {
|
||||
const udp_hdr* udp = reinterpret_cast<const udp_hdr*>(l4_payload);
|
||||
src_port = ntohs(udp->source);
|
||||
dest_port = ntohs(udp->dest);
|
||||
}
|
||||
extract_l4_ports(l4_proto, l4_payload, remaining, &src_port, &dest_port);
|
||||
|
||||
if (src_port != 0 && dest_port != 0) {
|
||||
LOG_D("IPv6 packet: next_header=%u, %s:%u -> %s:%u, len=%u", l4_proto,
|
||||
ip6_to_string(ip6->saddr).c_str(), src_port, ip6_to_string(ip6->daddr).c_str(),
|
||||
dest_port, payload_len);
|
||||
} else {
|
||||
LOG_D("IPv6 packet: next_header=%u, %s -> %s, len=%u", l4_proto,
|
||||
ip6_to_string(ip6->saddr).c_str(), ip6_to_string(ip6->daddr).c_str(),
|
||||
payload_len);
|
||||
if (logs::getLogLevel() <= logs::DEBUG) {
|
||||
char saddr_str[INET6_ADDRSTRLEN];
|
||||
char daddr_str[INET6_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET6, ip6.saddr, saddr_str, sizeof(saddr_str));
|
||||
inet_ntop(AF_INET6, ip6.daddr, daddr_str, sizeof(daddr_str));
|
||||
if (src_port != 0 && dest_port != 0) {
|
||||
LOG_D("IPv6 packet: next_header=%u, %s:%u -> %s:%u, len=%u", l4_proto,
|
||||
saddr_str, src_port, daddr_str, dest_port, payload_len);
|
||||
} else {
|
||||
LOG_D("IPv6 packet: next_header=%u, %s -> %s, len=%u", l4_proto, saddr_str,
|
||||
daddr_str, payload_len);
|
||||
}
|
||||
}
|
||||
|
||||
switch (l4_proto) {
|
||||
case IPPROTO_ICMPV6:
|
||||
handle_icmp6(ctx, ip6, std::span<const uint8_t>(l4_payload, remaining));
|
||||
handle_icmp6(ctx, &ip6, l4_payload, remaining);
|
||||
break;
|
||||
case IPPROTO_UDP:
|
||||
handle_udp6(ctx, ip6, std::span<const uint8_t>(l4_payload, remaining));
|
||||
handle_udp6(ctx, &ip6, l4_payload, remaining);
|
||||
break;
|
||||
case IPPROTO_TCP:
|
||||
handle_tcp6(ctx, ip6, std::span<const uint8_t>(l4_payload, remaining));
|
||||
handle_tcp6(ctx, &ip6, l4_payload, remaining);
|
||||
break;
|
||||
default:
|
||||
LOG_D("Unknown IPv6 next_header: %u", l4_proto);
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "core.h"
|
||||
|
||||
namespace nstun {
|
||||
|
||||
void handle_ip4(Context* ctx, std::span<const uint8_t> payload);
|
||||
void handle_ip6(Context* ctx, std::span<const uint8_t> payload);
|
||||
void handle_ip4(Context* ctx, const uint8_t* payload, size_t len);
|
||||
void handle_ip6(Context* ctx, const uint8_t* payload, size_t len);
|
||||
|
||||
} // namespace nstun
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#ifndef NSTUN_NET_DEFS_H_
|
||||
#define NSTUN_NET_DEFS_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
/* From <linux/in.h>, can't include directly due to conflicts with <netinet/in.h> */
|
||||
#ifndef IN_LOOPBACK
|
||||
#define IN_LOOPBACK(a) ((((long int)(a)) & 0xff000000) == 0x7f000000)
|
||||
@@ -110,7 +110,6 @@ inline uint8_t ip_version(const uint8_t* ptr) {
|
||||
return ptr[0] >> 4;
|
||||
}
|
||||
|
||||
|
||||
inline uint8_t ip4_version(const ip4_hdr* h) {
|
||||
return h->ihl_version >> 4;
|
||||
}
|
||||
|
||||
866
nstun/nstun.cc
866
nstun/nstun.cc
@@ -9,6 +9,7 @@
|
||||
#include <netlink/addr.h>
|
||||
#include <netlink/netlink.h>
|
||||
#include <sched.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -18,6 +19,7 @@
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <new>
|
||||
#include <thread>
|
||||
|
||||
#include "core.h"
|
||||
@@ -26,6 +28,8 @@
|
||||
#include "ip.h"
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "monitor.h"
|
||||
#include "nsjail.h"
|
||||
#include "policy.h"
|
||||
#include "tcp.h"
|
||||
#include "tun.h"
|
||||
@@ -34,52 +38,136 @@
|
||||
|
||||
namespace nstun {
|
||||
|
||||
Context::~Context() {
|
||||
/* Owning maps (ipv4_udp_flows_by_key, etc.) will self-clean via std::unique_ptr */
|
||||
for (auto& [fd, _] : host_listener_fd_to_rule) {
|
||||
::close(fd);
|
||||
bool icmp_is_stale(const IcmpFlow* flow, time_t now);
|
||||
void icmp_destroy(Context* ctx, IcmpFlow* flow);
|
||||
void icmp_handle_host_event(Context* ctx, IcmpFlow* flow, int fd, uint32_t events);
|
||||
|
||||
static void context_cleanup(Context* ctx) {
|
||||
for (size_t i = 0; i < NSTUN_MAX_FLOWS; ++i) {
|
||||
if (ctx->c_ipv4_udp_flows[i].header.active) {
|
||||
udp_destroy_flow(ctx, &ctx->c_ipv4_udp_flows[i]);
|
||||
}
|
||||
if (ctx->c_ipv6_udp_flows[i].header.active) {
|
||||
udp_destroy_flow(ctx, &ctx->c_ipv6_udp_flows[i]);
|
||||
}
|
||||
if (ctx->c_ipv4_tcp_flows[i].header.active) {
|
||||
tcp_destroy_flow(ctx, &ctx->c_ipv4_tcp_flows[i]);
|
||||
}
|
||||
if (ctx->c_ipv6_tcp_flows[i].header.active) {
|
||||
tcp_destroy_flow(ctx, &ctx->c_ipv6_tcp_flows[i]);
|
||||
}
|
||||
if (ctx->c_ipv4_icmp_flows[i].header.active) {
|
||||
icmp_destroy(ctx, &ctx->c_ipv4_icmp_flows[i]);
|
||||
}
|
||||
if (ctx->c_ipv6_icmp_flows[i].header.active) {
|
||||
icmp_destroy(ctx, &ctx->c_ipv6_icmp_flows[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ctx->num_c_host_listener_rules; ++i) {
|
||||
monitor::removeFd(ctx->c_host_listener_rules[i].fd);
|
||||
close(ctx->c_host_listener_rules[i].fd);
|
||||
}
|
||||
ctx->num_c_host_listener_rules = 0;
|
||||
if (ctx->tap_fd != -1) {
|
||||
monitor::removeFd(ctx->tap_fd);
|
||||
close(ctx->tap_fd);
|
||||
}
|
||||
}
|
||||
|
||||
static void gc_destroy_tcp_flow(Context* ctx, TcpFlow* flow) {
|
||||
if (flow->header.is_ipv6) {
|
||||
LOG_D("GC: stale TCP flow (IPv6, sport=%u, state=%d)",
|
||||
ntohs(flow->header.key6.sport), static_cast<int>(flow->tcp_state));
|
||||
} else {
|
||||
LOG_D("GC: stale TCP flow (sport=%u, state=%d)", ntohs(flow->header.key4.sport),
|
||||
static_cast<int>(flow->tcp_state));
|
||||
}
|
||||
tcp_destroy_flow(ctx, flow);
|
||||
}
|
||||
|
||||
static void gc_tcp_flows(Context* ctx, TcpFlow* flows, time_t now) {
|
||||
for (size_t i = 0; i < NSTUN_MAX_FLOWS; ++i) {
|
||||
TcpFlow* flow = &flows[i];
|
||||
if (flow->header.active) {
|
||||
tcp_periodic_check(ctx, flow, now);
|
||||
if (is_stale_tcp(flow, now)) {
|
||||
gc_destroy_tcp_flow(ctx, flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void gc_udp_flows(Context* ctx, UdpFlow* flows, time_t now) {
|
||||
for (size_t i = 0; i < NSTUN_MAX_FLOWS; ++i) {
|
||||
UdpFlow* flow = &flows[i];
|
||||
if (flow->header.active) {
|
||||
if (is_stale_udp(flow, now)) {
|
||||
udp_destroy_flow(ctx, flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void gc_icmp_flows(Context* ctx, IcmpFlow* flows, time_t now) {
|
||||
for (size_t i = 0; i < NSTUN_MAX_FLOWS; ++i) {
|
||||
IcmpFlow* flow = &flows[i];
|
||||
if (flow->header.active) {
|
||||
if (icmp_is_stale(flow, now)) {
|
||||
icmp_destroy(ctx, flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void garbage_collect(Context* ctx) {
|
||||
time_t now = time(NULL);
|
||||
time_t now = time(nullptr);
|
||||
|
||||
auto do_gc = [&](auto& map) {
|
||||
std::vector<Flow*> stale_flows;
|
||||
for (auto const& pair : map) {
|
||||
Flow* flow = pair.second.get();
|
||||
flow->periodic_check(ctx, now);
|
||||
if (flow->is_stale(now)) {
|
||||
stale_flows.push_back(flow);
|
||||
}
|
||||
}
|
||||
for (Flow* flow : stale_flows) {
|
||||
flow->destroy(ctx);
|
||||
}
|
||||
};
|
||||
|
||||
do_gc(ctx->ipv4_tcp_flows_by_key);
|
||||
do_gc(ctx->ipv4_udp_flows_by_key);
|
||||
do_gc(ctx->ipv4_icmp_flows_by_key);
|
||||
do_gc(ctx->ipv6_tcp_flows_by_key);
|
||||
do_gc(ctx->ipv6_udp_flows_by_key);
|
||||
do_gc(ctx->ipv6_icmp_flows_by_key);
|
||||
gc_tcp_flows(ctx, ctx->c_ipv4_tcp_flows, now);
|
||||
gc_tcp_flows(ctx, ctx->c_ipv6_tcp_flows, now);
|
||||
gc_udp_flows(ctx, ctx->c_ipv4_udp_flows, now);
|
||||
gc_udp_flows(ctx, ctx->c_ipv6_udp_flows, now);
|
||||
gc_icmp_flows(ctx, ctx->c_ipv4_icmp_flows, now);
|
||||
gc_icmp_flows(ctx, ctx->c_ipv6_icmp_flows, now);
|
||||
}
|
||||
|
||||
static void handle_host_events(Context* ctx, int fd, uint32_t events) {
|
||||
auto it_listener = ctx->host_listener_fd_to_rule.find(fd);
|
||||
if (it_listener != ctx->host_listener_fd_to_rule.end()) {
|
||||
if (it_listener->second.proto == NSTUN_PROTO_TCP) {
|
||||
handle_host_tcp_accept(ctx, fd, it_listener->second);
|
||||
} else if (it_listener->second.proto == NSTUN_PROTO_UDP) {
|
||||
handle_host_udp_accept(ctx, fd, it_listener->second);
|
||||
void handle_host_events(Context* ctx, int fd, uint32_t events) {
|
||||
LOG_D("handle_host_events: fd=%d, events=0x%x", fd, events);
|
||||
for (size_t i = 0; i < ctx->num_c_host_listener_rules; ++i) {
|
||||
if (ctx->c_host_listener_rules[i].fd == fd) {
|
||||
const nstun_rule_t& rule = ctx->c_host_listener_rules[i].rule;
|
||||
switch (rule.proto) {
|
||||
case NSTUN_PROTO_TCP:
|
||||
handle_host_tcp_accept(ctx, fd, rule);
|
||||
break;
|
||||
case NSTUN_PROTO_UDP:
|
||||
handle_host_udp_accept(ctx, fd, rule);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TcpFlow* tcp_flow = get_tcp_flow_by_fd(ctx, fd);
|
||||
if (tcp_flow) {
|
||||
handle_host_tcp_event(ctx, tcp_flow, fd, events);
|
||||
return;
|
||||
}
|
||||
UdpFlow* udp_flow = get_udp_flow_by_fd(ctx, fd);
|
||||
if (udp_flow) {
|
||||
handle_host_udp_event(ctx, udp_flow, fd, events);
|
||||
return;
|
||||
}
|
||||
IcmpFlow* icmp_flow = get_icmp_flow_by_fd(ctx, fd);
|
||||
if (icmp_flow) {
|
||||
icmp_handle_host_event(ctx, icmp_flow, fd, events);
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = ctx->flows_by_fd.find(fd);
|
||||
if (it != ctx->flows_by_fd.end()) {
|
||||
it->second->handle_host_event(ctx, fd, events);
|
||||
if (fd < 0 || fd >= static_cast<int>(NSTUN_MAX_FDS)) {
|
||||
LOG_W("FD %d is out of bounds for lookup table (max %zu)", fd, NSTUN_MAX_FDS);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,65 +176,15 @@ static void handle_host_events(Context* ctx, int fd, uint32_t events) {
|
||||
LOG_D("Stale epoll event for fd %d (already closed), skipping", fd);
|
||||
}
|
||||
|
||||
static void networkLoop(Context* ctx) {
|
||||
LOG_D("nstun network loop started on tap_fd=%d", ctx->tap_fd);
|
||||
|
||||
defer {
|
||||
close(ctx->tap_fd);
|
||||
close(ctx->epoll_fd);
|
||||
delete ctx;
|
||||
};
|
||||
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = ctx->tap_fd}};
|
||||
if (epoll_ctl(ctx->epoll_fd, EPOLL_CTL_ADD, ctx->tap_fd, &ev) == -1) {
|
||||
PLOG_E("epoll_ctl(EPOLL_CTL_ADD, tap_fd)");
|
||||
return;
|
||||
}
|
||||
|
||||
/* TUN frames: 4-byte header + up to NSTUN_MTU bytes of L3 payload */
|
||||
static constexpr size_t TUN_FRAME_BUF_SIZE = NSTUN_MTU + 4;
|
||||
auto buf = std::make_unique<uint8_t[]>(TUN_FRAME_BUF_SIZE);
|
||||
struct epoll_event events[64];
|
||||
time_t last_gc = time(NULL);
|
||||
|
||||
while (true) {
|
||||
int nfds = epoll_wait(ctx->epoll_fd, events, 64, 1000); /* 1s timeout */
|
||||
if (nfds == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
PLOG_E("epoll_wait");
|
||||
break;
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
if (now - last_gc >= 2) {
|
||||
garbage_collect(ctx);
|
||||
last_gc = now;
|
||||
}
|
||||
|
||||
for (int i = 0; i < nfds; ++i) {
|
||||
int fd = events[i].data.fd;
|
||||
|
||||
if (fd == ctx->tap_fd) {
|
||||
ssize_t n = TEMP_FAILURE_RETRY(
|
||||
read(ctx->tap_fd, buf.get(), TUN_FRAME_BUF_SIZE));
|
||||
if (n <= 0) {
|
||||
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
continue;
|
||||
}
|
||||
PLOG_E("read(tap_fd) failed or EOF");
|
||||
return;
|
||||
}
|
||||
handle_tun_frame(ctx, buf.get(), n);
|
||||
} else {
|
||||
handle_host_events(ctx, fd, events[i].events);
|
||||
}
|
||||
}
|
||||
}
|
||||
void host_callback(int fd, uint32_t events, void* data) {
|
||||
Context* ctx = static_cast<Context*>(data);
|
||||
LOG_D("host_callback: ctx=%p", ctx);
|
||||
handle_host_events(ctx, fd, events);
|
||||
}
|
||||
|
||||
} /* namespace nstun */
|
||||
|
||||
bool nstun_init_child(int sock, nsj_t* nsj) {
|
||||
bool nstun_init_child(int ipc_fd, nsj_t* nsj) {
|
||||
/* Create TUN device. */
|
||||
int tap_fd = open("/dev/net/tun", O_RDWR | O_CLOEXEC | O_NONBLOCK);
|
||||
if (tap_fd < 0) {
|
||||
@@ -154,252 +192,315 @@ bool nstun_init_child(int sock, nsj_t* nsj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
defer {
|
||||
if (!success) {
|
||||
close(tap_fd);
|
||||
}
|
||||
};
|
||||
|
||||
struct ifreq ifr = {};
|
||||
ifr.ifr_flags = IFF_TUN | IFF_NO_PI; /* TUN, no packet info */
|
||||
snprintf(ifr.ifr_name, IFNAMSIZ, "%s", nsj->njc.user_net().ns_iface().c_str());
|
||||
|
||||
if (ioctl(tap_fd, TUNSETIFF, &ifr) < 0) {
|
||||
PLOG_E("ioctl(TUNSETIFF)");
|
||||
close(tap_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Configure IP, MAC, UP, route. */
|
||||
if (!nstun::configIface(nsj)) {
|
||||
LOG_E("nstun::configIface() failed");
|
||||
close(tap_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Send FD to parent */
|
||||
if (!util::sendFd(sock, tap_fd)) {
|
||||
PLOG_E("util::sendFd(tap_fd)");
|
||||
if (!util::sendMsg(ipc_fd, monitor::MSG_TAG_TAP, tap_fd)) {
|
||||
PLOG_E("util::sendMsg(tap_fd)");
|
||||
close(tap_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
success = true;
|
||||
close(tap_fd);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool nstun_init_parent(int sock, nsj_t* nsj) {
|
||||
int tap_fd = util::recvFd(sock);
|
||||
if (tap_fd < 0) {
|
||||
LOG_E("Failed to receive TAP fd from child");
|
||||
static void tapCb(int fd, uint32_t /* events */, void* data) {
|
||||
nstun::Context* ctx = static_cast<nstun::Context*>(data);
|
||||
|
||||
/* Rule 21: Avoid infinite loop to prevent event starvation.
|
||||
* Level-triggered epoll will wake us up again if more data is available. */
|
||||
ssize_t n = TEMP_FAILURE_RETRY(read(fd, ctx->tun_buf, nstun::NSTUN_MTU + 4));
|
||||
if (n <= 0) {
|
||||
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
return;
|
||||
}
|
||||
PLOG_E("read(tap_fd) failed or EOF, removing FD");
|
||||
monitor::removeFd(fd);
|
||||
close(fd);
|
||||
ctx->tap_fd = -1;
|
||||
return;
|
||||
}
|
||||
handle_tun_frame(ctx, ctx->tun_buf, n);
|
||||
}
|
||||
|
||||
static thread_local nstun::Context* tls_nstun_ctx = nullptr;
|
||||
|
||||
void nstun_periodic() {
|
||||
if (tls_nstun_ctx) {
|
||||
garbage_collect(tls_nstun_ctx);
|
||||
}
|
||||
}
|
||||
|
||||
static bool assign_ip(const std::string& str, uint32_t* ip) {
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(str.c_str(), AF_INET, &addr) != 0) {
|
||||
LOG_E("Failed to parse IP string: %s", str.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I("nstun initialized successfully, tap_fd=%d", tap_fd);
|
||||
|
||||
std::unique_ptr<nstun::Context> ctx = std::make_unique<nstun::Context>();
|
||||
ctx->epoll_fd = -1; /* Not used yet */
|
||||
ctx->tap_fd = tap_fd;
|
||||
ctx->nsj = nsj;
|
||||
|
||||
auto assign_ip = [](const std::string& str, uint32_t* ip) {
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(str.c_str(), AF_INET, &addr) == 0) {
|
||||
if (nl_addr_get_len(addr) == 4) {
|
||||
memcpy(ip, nl_addr_get_binary_addr(addr), 4);
|
||||
}
|
||||
nl_addr_put(addr);
|
||||
}
|
||||
};
|
||||
|
||||
if (!nsj->njc.user_net().ip4().empty()) {
|
||||
assign_ip(nsj->njc.user_net().ip4(), &ctx->guest_ip4);
|
||||
}
|
||||
if (!nsj->njc.user_net().gw4().empty()) {
|
||||
assign_ip(nsj->njc.user_net().gw4(), &ctx->host_ip4);
|
||||
}
|
||||
|
||||
if (!nsj->njc.user_net().ip6().empty()) {
|
||||
if (inet_pton(AF_INET6, nsj->njc.user_net().ip6().c_str(), ctx->guest_ip6) != 1) {
|
||||
LOG_E("Cannot convert '%s' into an IPv6 address",
|
||||
nsj->njc.user_net().ip6().c_str());
|
||||
close(tap_fd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!nsj->njc.user_net().gw6().empty()) {
|
||||
if (inet_pton(AF_INET6, nsj->njc.user_net().gw6().c_str(), ctx->host_ip6) != 1) {
|
||||
LOG_E("Cannot convert '%s' into an IPv6 address",
|
||||
nsj->njc.user_net().gw6().c_str());
|
||||
close(tap_fd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto parse_ip = [](const std::string& str, uint32_t* ip, uint32_t* mask) {
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(str.c_str(), AF_INET, &addr) == 0) {
|
||||
if (nl_addr_get_len(addr) == 4) {
|
||||
memcpy(ip, nl_addr_get_binary_addr(addr), 4);
|
||||
}
|
||||
int bits = nl_addr_get_prefixlen(addr);
|
||||
*mask = (bits == 0) ? 0 : htonl(~((1ULL << (32 - bits)) - 1));
|
||||
nl_addr_put(addr);
|
||||
} else {
|
||||
LOG_E("Failed to parse IP/CIDR string: %s", str.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
auto parse_ip6 = [](const std::string& str, uint8_t* ip6, uint8_t* mask6) {
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(str.c_str(), AF_INET6, &addr) == 0) {
|
||||
if (nl_addr_get_len(addr) == nstun::IPV6_ADDR_LEN) {
|
||||
memcpy(ip6, nl_addr_get_binary_addr(addr), nstun::IPV6_ADDR_LEN);
|
||||
}
|
||||
int bits = nl_addr_get_prefixlen(addr);
|
||||
memset(mask6, 0, nstun::IPV6_ADDR_LEN);
|
||||
for (int i = 0; i < (int)nstun::IPV6_ADDR_LEN; i++) {
|
||||
if (bits >= 8) {
|
||||
mask6[i] = 0xFF;
|
||||
bits -= 8;
|
||||
} else if (bits > 0) {
|
||||
mask6[i] = (uint8_t)(0xFF << (8 - bits));
|
||||
bits = 0;
|
||||
} else {
|
||||
mask6[i] = 0;
|
||||
}
|
||||
}
|
||||
nl_addr_put(addr);
|
||||
} else {
|
||||
LOG_E("Failed to parse IPv6/CIDR string: %s", str.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
ctx->epoll_fd = epoll_create1(EPOLL_CLOEXEC);
|
||||
if (ctx->epoll_fd == -1) {
|
||||
PLOG_E("epoll_create1(EPOLL_CLOEXEC)");
|
||||
close(ctx->tap_fd);
|
||||
if (nl_addr_get_len(addr) != 4) {
|
||||
LOG_E("IP string is not IPv4: %s", str.c_str());
|
||||
nl_addr_put(addr);
|
||||
return false;
|
||||
}
|
||||
memcpy(ip, nl_addr_get_binary_addr(addr), 4);
|
||||
nl_addr_put(addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto cleanup_and_fail = [&ctx]() -> bool {
|
||||
for (auto& [fd, _] : ctx->host_listener_fd_to_rule) {
|
||||
static bool parse_ip(const std::string& str, uint32_t* ip, uint32_t* mask) {
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(str.c_str(), AF_INET, &addr) != 0) {
|
||||
LOG_E("Failed to parse IP/CIDR string: %s", str.c_str());
|
||||
return false;
|
||||
}
|
||||
if (nl_addr_get_len(addr) != 4) {
|
||||
LOG_E("IP/CIDR string is not IPv4: %s", str.c_str());
|
||||
nl_addr_put(addr);
|
||||
return false;
|
||||
}
|
||||
memcpy(ip, nl_addr_get_binary_addr(addr), 4);
|
||||
int bits = nl_addr_get_prefixlen(addr);
|
||||
*mask = (bits == 0) ? 0 : htonl(~((1ULL << (32 - bits)) - 1));
|
||||
nl_addr_put(addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_ip6(const std::string& str, uint8_t* ip6, uint8_t* mask6) {
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(str.c_str(), AF_INET6, &addr) != 0) {
|
||||
LOG_E("Failed to parse IPv6/CIDR string: %s", str.c_str());
|
||||
return false;
|
||||
}
|
||||
if (nl_addr_get_len(addr) != nstun::IPV6_ADDR_LEN) {
|
||||
LOG_E("IPv6/CIDR string is not IPv6: %s", str.c_str());
|
||||
nl_addr_put(addr);
|
||||
return false;
|
||||
}
|
||||
memcpy(ip6, nl_addr_get_binary_addr(addr), nstun::IPV6_ADDR_LEN);
|
||||
int bits = nl_addr_get_prefixlen(addr);
|
||||
memset(mask6, 0, nstun::IPV6_ADDR_LEN);
|
||||
for (size_t i = 0; i < nstun::IPV6_ADDR_LEN; i++) {
|
||||
if (bits >= 8) {
|
||||
mask6[i] = 0xFF;
|
||||
bits -= 8;
|
||||
} else if (bits > 0) {
|
||||
mask6[i] = (uint8_t)(0xFF << (8 - bits));
|
||||
bits = 0;
|
||||
} else {
|
||||
mask6[i] = 0;
|
||||
}
|
||||
}
|
||||
nl_addr_put(addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int create_host_listener(int domain, int type, const struct sockaddr* addr,
|
||||
socklen_t addrlen, nstun::Context* ctx, bool is_tcp, uint32_t port) {
|
||||
int fd = socket(domain, type | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd == -1) {
|
||||
PLOG_E("socket() for HOST_TO_GUEST");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int opt = 1;
|
||||
if (is_tcp) {
|
||||
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)) == -1) {
|
||||
PLOG_W("setsockopt(TCP_NODELAY)");
|
||||
}
|
||||
}
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
|
||||
PLOG_E("setsockopt(SO_REUSEADDR)");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
if (domain == AF_INET6) {
|
||||
if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)) == -1) {
|
||||
PLOG_E("setsockopt(IPV6_V6ONLY)");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
close(ctx->epoll_fd);
|
||||
close(ctx->tap_fd);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
if (bind(fd, addr, addrlen) == -1) {
|
||||
PLOG_E("bind() for HOST_TO_GUEST on port %u", port);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (is_tcp) {
|
||||
if (listen(fd, SOMAXCONN) == -1) {
|
||||
PLOG_E("listen() for HOST_TO_GUEST on port %u", port);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!monitor::addFd(fd, EPOLLIN, nstun::host_callback, ctx)) {
|
||||
LOG_E("monitor::addFd(host_listener_fd) failed");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG_D("create_host_listener returning fd=%d", fd);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static bool setup_host_redirect(nstun::Context* ctx, const nstun_rule_t& nr) {
|
||||
if (nr.direction != NSTUN_DIR_HOST_TO_GUEST || nr.action != NSTUN_ACTION_REDIRECT) {
|
||||
return true;
|
||||
}
|
||||
if (nr.proto != NSTUN_PROTO_TCP && nr.proto != NSTUN_PROTO_UDP) {
|
||||
LOG_E("HOST_TO_GUEST REDIRECT only supported for TCP/UDP");
|
||||
return false;
|
||||
}
|
||||
if (nr.dport_start == 0) {
|
||||
LOG_E("HOST_TO_GUEST REDIRECT requires 'dport' to be specified");
|
||||
return false;
|
||||
}
|
||||
if (nr.dport_end < nr.dport_start) {
|
||||
LOG_E("Invalid port range: %u - %u", nr.dport_start, nr.dport_end);
|
||||
return false;
|
||||
}
|
||||
if (nr.dport_end - nr.dport_start >= 1024) {
|
||||
LOG_E("Port range too large (%u - %u). Max range is 1024.", nr.dport_start,
|
||||
nr.dport_end);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t num_ports = nr.dport_end - nr.dport_start + 1;
|
||||
if (ctx->num_c_host_listener_rules + num_ports > nstun::NSTUN_MAX_RULES) {
|
||||
LOG_E("Not enough space for %u host listener rules (current: %zu, max: %zu)",
|
||||
num_ports, ctx->num_c_host_listener_rules, nstun::NSTUN_MAX_RULES);
|
||||
return false;
|
||||
}
|
||||
|
||||
int type = (nr.proto == NSTUN_PROTO_TCP) ? SOCK_STREAM : SOCK_DGRAM;
|
||||
bool is_tcp = (nr.proto == NSTUN_PROTO_TCP);
|
||||
|
||||
for (uint32_t port = nr.dport_start; port <= nr.dport_end; port++) {
|
||||
int fd = -1;
|
||||
if (nr.is_ipv6) {
|
||||
struct sockaddr_in6 addr = INIT_SOCKADDR_IN6(AF_INET6);
|
||||
addr.sin6_port = htons(port);
|
||||
memcpy(addr.sin6_addr.s6_addr, nr.src_ip6, sizeof(nr.src_ip6));
|
||||
fd = create_host_listener(AF_INET6, type, (struct sockaddr*)&addr,
|
||||
sizeof(addr), ctx, is_tcp, port);
|
||||
} else {
|
||||
struct sockaddr_in addr = INIT_SOCKADDR_IN(AF_INET);
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = nr.src_ip4;
|
||||
fd = create_host_listener(AF_INET, type, (struct sockaddr*)&addr,
|
||||
sizeof(addr), ctx, is_tcp, port);
|
||||
}
|
||||
if (fd == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx->c_host_listener_rules[ctx->num_c_host_listener_rules].fd = fd;
|
||||
ctx->c_host_listener_rules[ctx->num_c_host_listener_rules].rule = nr;
|
||||
ctx->num_c_host_listener_rules++;
|
||||
|
||||
LOG_I("Listening on host %s port %u for inbound %s redirection to guest",
|
||||
nr.is_ipv6 ? "IPv6" : "IPv4", port,
|
||||
(nr.proto == NSTUN_PROTO_TCP) ? "TCP" : "UDP");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_rules4(nstun::Context* ctx, nsj_t* nsj) {
|
||||
for (int i = 0; i < nsj->njc.user_net().rule4_size(); i++) {
|
||||
const auto& r = nsj->njc.user_net().rule4(i);
|
||||
|
||||
nstun_rule_t nr = {};
|
||||
nstun::RuleParseStatus status = nstun::fill_rule_common(r, &nr);
|
||||
if (status == nstun::RuleParseStatus::ABORT) return cleanup_and_fail();
|
||||
if (status == nstun::RuleParseStatus::IGNORE) continue;
|
||||
if (status == nstun::RuleParseStatus::ABORT) {
|
||||
return false;
|
||||
}
|
||||
if (status == nstun::RuleParseStatus::IGNORE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.has_src_ip()) {
|
||||
parse_ip(r.src_ip(), &nr.src_ip4, &nr.src_mask4);
|
||||
if (!parse_ip(r.src_ip(), &nr.src_ip4, &nr.src_mask4)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (r.has_dst_ip()) {
|
||||
parse_ip(r.dst_ip(), &nr.dst_ip4, &nr.dst_mask4);
|
||||
if (!parse_ip(r.dst_ip(), &nr.dst_ip4, &nr.dst_mask4)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (r.has_redirect_ip()) {
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(r.redirect_ip().c_str(), AF_INET, &addr) == 0) {
|
||||
if (nl_addr_get_len(addr) == 4) {
|
||||
memcpy(&nr.redirect_ip4, nl_addr_get_binary_addr(addr),
|
||||
sizeof(nr.redirect_ip4));
|
||||
}
|
||||
nl_addr_put(addr);
|
||||
if (nl_addr_parse(r.redirect_ip().c_str(), AF_INET, &addr) != 0) {
|
||||
LOG_E("Failed to parse redirect IP: %s", r.redirect_ip().c_str());
|
||||
return false;
|
||||
}
|
||||
if (nl_addr_get_len(addr) != 4) {
|
||||
LOG_E("Redirect IP is not IPv4: %s", r.redirect_ip().c_str());
|
||||
nl_addr_put(addr);
|
||||
return false;
|
||||
}
|
||||
memcpy(&nr.redirect_ip4, nl_addr_get_binary_addr(addr),
|
||||
sizeof(nr.redirect_ip4));
|
||||
nl_addr_put(addr);
|
||||
}
|
||||
nr.redirect_port = r.has_redirect_port() ? r.redirect_port() : 0;
|
||||
|
||||
ctx->rules.push_back(nr);
|
||||
if (ctx->c_rules_count >= nstun::NSTUN_MAX_RULES) {
|
||||
LOG_E("Too many rules (max %zu)", nstun::NSTUN_MAX_RULES);
|
||||
return false;
|
||||
}
|
||||
ctx->c_rules[ctx->c_rules_count++] = nr;
|
||||
|
||||
if (nr.direction == NSTUN_DIR_HOST_TO_GUEST && nr.action == NSTUN_ACTION_REDIRECT) {
|
||||
if (nr.proto != NSTUN_PROTO_TCP && nr.proto != NSTUN_PROTO_UDP) {
|
||||
LOG_E("HOST_TO_GUEST REDIRECT only supported for TCP/UDP");
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
if (nr.dport_start == 0) {
|
||||
LOG_E("HOST_TO_GUEST REDIRECT requires 'dport' to be specified");
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
for (uint32_t port = nr.dport_start; port <= nr.dport_end; port++) {
|
||||
int type = (nr.proto == NSTUN_PROTO_TCP) ? SOCK_STREAM : SOCK_DGRAM;
|
||||
int fd = socket(AF_INET, type | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd == -1) {
|
||||
PLOG_E("socket(AF_INET) for HOST_TO_GUEST");
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
|
||||
int opt = 1;
|
||||
if (nr.proto == NSTUN_PROTO_TCP) {
|
||||
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt,
|
||||
sizeof(opt)) == -1) {
|
||||
PLOG_W("setsockopt(TCP_NODELAY)");
|
||||
}
|
||||
}
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) ==
|
||||
-1) {
|
||||
PLOG_E("setsockopt(SO_REUSEADDR)");
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
|
||||
struct sockaddr_in addr = INIT_SOCKADDR_IN(AF_INET);
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = nr.src_ip4;
|
||||
|
||||
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
PLOG_E("bind() for HOST_TO_GUEST on port %u", port);
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
|
||||
if (nr.proto == NSTUN_PROTO_TCP) {
|
||||
if (listen(fd, SOMAXCONN) == -1) {
|
||||
PLOG_E(
|
||||
"listen() for HOST_TO_GUEST on port %u", port);
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
}
|
||||
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = fd}};
|
||||
if (epoll_ctl(ctx->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
|
||||
PLOG_E("epoll_ctl(EPOLL_CTL_ADD) for HOST_TO_GUEST");
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
|
||||
ctx->host_listener_fd_to_rule[fd] = nr;
|
||||
LOG_I(
|
||||
"Listening on host port %u for inbound %s redirection to guest",
|
||||
port, (nr.proto == NSTUN_PROTO_TCP) ? "TCP" : "UDP");
|
||||
}
|
||||
if (!setup_host_redirect(ctx, nr)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Process IPv6 rules - same NstunRule message, but src_ip4/dst_ip4 parsed as IPv6 */
|
||||
static bool parse_rules6(nstun::Context* ctx, nsj_t* nsj) {
|
||||
for (int i = 0; i < nsj->njc.user_net().rule6_size(); i++) {
|
||||
const auto& r = nsj->njc.user_net().rule6(i);
|
||||
|
||||
nstun_rule_t nr = {};
|
||||
nr.is_ipv6 = true;
|
||||
nstun::RuleParseStatus status = nstun::fill_rule_common(r, &nr);
|
||||
if (status == nstun::RuleParseStatus::ABORT) return cleanup_and_fail();
|
||||
if (status == nstun::RuleParseStatus::IGNORE) continue;
|
||||
if (status == nstun::RuleParseStatus::ABORT) {
|
||||
return false;
|
||||
}
|
||||
if (status == nstun::RuleParseStatus::IGNORE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.has_src_ip()) {
|
||||
parse_ip6(r.src_ip(), nr.src_ip6, nr.src_mask6);
|
||||
if (!parse_ip6(r.src_ip(), nr.src_ip6, nr.src_mask6)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (r.has_dst_ip()) {
|
||||
parse_ip6(r.dst_ip(), nr.dst_ip6, nr.dst_mask6);
|
||||
if (!parse_ip6(r.dst_ip(), nr.dst_ip6, nr.dst_mask6)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (r.has_redirect_ip()) {
|
||||
@@ -407,105 +508,148 @@ bool nstun_init_parent(int sock, nsj_t* nsj) {
|
||||
nr.action == NSTUN_ACTION_ENCAP_CONNECT) {
|
||||
/* Proxy is always IPv4 */
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(r.redirect_ip().c_str(), AF_INET, &addr) == 0) {
|
||||
if (nl_addr_get_len(addr) == 4) {
|
||||
memcpy(&nr.redirect_ip4,
|
||||
nl_addr_get_binary_addr(addr),
|
||||
sizeof(nr.redirect_ip4));
|
||||
}
|
||||
nl_addr_put(addr);
|
||||
if (nl_addr_parse(r.redirect_ip().c_str(), AF_INET, &addr) != 0) {
|
||||
LOG_E("Failed to parse proxy IP: %s",
|
||||
r.redirect_ip().c_str());
|
||||
return false;
|
||||
}
|
||||
if (nl_addr_get_len(addr) != 4) {
|
||||
LOG_E("Proxy IP is not IPv4: %s", r.redirect_ip().c_str());
|
||||
nl_addr_put(addr);
|
||||
return false;
|
||||
}
|
||||
memcpy(&nr.redirect_ip4, nl_addr_get_binary_addr(addr),
|
||||
sizeof(nr.redirect_ip4));
|
||||
nl_addr_put(addr);
|
||||
} else {
|
||||
/* REDIRECT: target is IPv6 */
|
||||
struct nl_addr* addr;
|
||||
if (nl_addr_parse(r.redirect_ip().c_str(), AF_INET6, &addr) == 0) {
|
||||
if (nl_addr_get_len(addr) == 16) {
|
||||
memcpy(nr.redirect_ip6,
|
||||
nl_addr_get_binary_addr(addr),
|
||||
sizeof(nr.redirect_ip6));
|
||||
}
|
||||
nl_addr_put(addr);
|
||||
if (nl_addr_parse(r.redirect_ip().c_str(), AF_INET6, &addr) != 0) {
|
||||
LOG_E("Failed to parse redirect IPv6: %s",
|
||||
r.redirect_ip().c_str());
|
||||
return false;
|
||||
}
|
||||
if (nl_addr_get_len(addr) != 16) {
|
||||
LOG_E(
|
||||
"Redirect IP is not IPv6: %s", r.redirect_ip().c_str());
|
||||
nl_addr_put(addr);
|
||||
return false;
|
||||
}
|
||||
memcpy(nr.redirect_ip6, nl_addr_get_binary_addr(addr),
|
||||
sizeof(nr.redirect_ip6));
|
||||
nl_addr_put(addr);
|
||||
}
|
||||
}
|
||||
nr.redirect_port = r.has_redirect_port() ? r.redirect_port() : 0;
|
||||
|
||||
ctx->rules.push_back(nr);
|
||||
if (ctx->c_rules_count >= nstun::NSTUN_MAX_RULES) {
|
||||
LOG_E("Too many rules (max %zu)", nstun::NSTUN_MAX_RULES);
|
||||
return false;
|
||||
}
|
||||
ctx->c_rules[ctx->c_rules_count++] = nr;
|
||||
|
||||
if (nr.direction == NSTUN_DIR_HOST_TO_GUEST && nr.action == NSTUN_ACTION_REDIRECT) {
|
||||
if (nr.proto != NSTUN_PROTO_TCP && nr.proto != NSTUN_PROTO_UDP) {
|
||||
LOG_E("HOST_TO_GUEST REDIRECT only supported for TCP/UDP");
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
if (nr.dport_start == 0) {
|
||||
LOG_E("HOST_TO_GUEST REDIRECT requires 'dport' to be specified");
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
for (uint32_t port = nr.dport_start; port <= nr.dport_end; port++) {
|
||||
int type = (nr.proto == NSTUN_PROTO_TCP) ? SOCK_STREAM : SOCK_DGRAM;
|
||||
int fd = socket(AF_INET6, type | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd == -1) {
|
||||
PLOG_E("socket(AF_INET6) for HOST_TO_GUEST");
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
|
||||
int opt = 1;
|
||||
if (nr.proto == NSTUN_PROTO_TCP) {
|
||||
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt,
|
||||
sizeof(opt)) == -1) {
|
||||
PLOG_W("setsockopt(TCP_NODELAY)");
|
||||
}
|
||||
}
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) ==
|
||||
-1) {
|
||||
PLOG_E("setsockopt(SO_REUSEADDR)");
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)) ==
|
||||
-1) {
|
||||
PLOG_E("setsockopt(IPV6_V6ONLY)");
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
|
||||
struct sockaddr_in6 addr = INIT_SOCKADDR_IN6(AF_INET6);
|
||||
addr.sin6_port = htons(port);
|
||||
memcpy(addr.sin6_addr.s6_addr, nr.src_ip6, sizeof(nr.src_ip6));
|
||||
|
||||
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
PLOG_E("bind() for HOST_TO_GUEST IPv6 on port %u", port);
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
|
||||
if (nr.proto == NSTUN_PROTO_TCP) {
|
||||
if (listen(fd, SOMAXCONN) == -1) {
|
||||
PLOG_E("listen() for HOST_TO_GUEST IPv6 on port %u",
|
||||
port);
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
}
|
||||
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = fd}};
|
||||
if (epoll_ctl(ctx->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
|
||||
PLOG_E("epoll_ctl(EPOLL_CTL_ADD) for HOST_TO_GUEST IPv6");
|
||||
close(fd);
|
||||
return cleanup_and_fail();
|
||||
}
|
||||
|
||||
ctx->host_listener_fd_to_rule[fd] = nr;
|
||||
LOG_I("Listening on host IPv6 port %u for inbound %s redirection "
|
||||
"to guest",
|
||||
port, (nr.proto == NSTUN_PROTO_TCP) ? "TCP" : "UDP");
|
||||
}
|
||||
if (!setup_host_redirect(ctx, nr)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Spawn network loop thread */
|
||||
std::thread t(nstun::networkLoop, ctx.release());
|
||||
t.detach();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool setup_ip4(const std::string& config_ip, const char* default_ip, uint32_t* out_ip) {
|
||||
if (!config_ip.empty()) {
|
||||
return assign_ip(config_ip, out_ip);
|
||||
}
|
||||
if (inet_pton(AF_INET, default_ip, out_ip) != 1) {
|
||||
LOG_E("Failed to parse default IP4: %s", default_ip);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool setup_ip6(const std::string& config_ip, const char* default_ip, uint8_t* out_ip6) {
|
||||
if (!config_ip.empty()) {
|
||||
if (inet_pton(AF_INET6, config_ip.c_str(), out_ip6) != 1) {
|
||||
LOG_E("Cannot convert '%s' into an IPv6 address", config_ip.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (inet_pton(AF_INET6, default_ip, out_ip6) != 1) {
|
||||
LOG_E("Failed to parse default IP6: %s", default_ip);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool nstun_init_parent(int tap_fd, nsj_t* nsj, pid_t pid) {
|
||||
LOG_D("nstun initialized successfully, tap_fd=%d", tap_fd);
|
||||
|
||||
nstun::Context* ctx = new (std::nothrow) nstun::Context();
|
||||
if (!ctx) {
|
||||
LOG_E("Failed to allocate Context");
|
||||
return false;
|
||||
}
|
||||
LOG_D("nstun_init_parent: allocated ctx=%p", ctx);
|
||||
ctx->tap_fd = tap_fd;
|
||||
|
||||
bool success = false;
|
||||
defer {
|
||||
if (!success) {
|
||||
/* Caller (monitorThread) closes tap_fd on failure, so detach it to prevent
|
||||
* double-close */
|
||||
ctx->tap_fd = -1;
|
||||
context_cleanup(ctx);
|
||||
delete ctx;
|
||||
}
|
||||
};
|
||||
|
||||
if (!setup_ip4(nsj->njc.user_net().ip4(), "192.168.0.2", &ctx->guest_ip4)) {
|
||||
return false;
|
||||
}
|
||||
if (!setup_ip4(nsj->njc.user_net().gw4(), "192.168.0.1", &ctx->host_ip4)) {
|
||||
return false;
|
||||
}
|
||||
if (!setup_ip6(nsj->njc.user_net().ip6(), "fd00::2", ctx->guest_ip6)) {
|
||||
return false;
|
||||
}
|
||||
if (!setup_ip6(nsj->njc.user_net().gw6(), "fd00::1", ctx->host_ip6)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parse_rules4(ctx, nsj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parse_rules6(ctx, nsj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Register with monitor loop */
|
||||
if (!monitor::addFd(ctx->tap_fd, EPOLLIN, tapCb, ctx)) {
|
||||
LOG_E("monitor::addFd(tap_fd) failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < nstun::VLEN; ++i) {
|
||||
ctx->recvmmsg_iovecs[i].iov_base = ctx->recvmmsg_bufs[i];
|
||||
ctx->recvmmsg_iovecs[i].iov_len = sizeof(ctx->recvmmsg_bufs[i]);
|
||||
ctx->recvmmsg_msgs[i].msg_hdr.msg_iov = &ctx->recvmmsg_iovecs[i];
|
||||
ctx->recvmmsg_msgs[i].msg_hdr.msg_iovlen = 1;
|
||||
ctx->recvmmsg_msgs[i].msg_hdr.msg_name = &ctx->recvmmsg_addrs[i];
|
||||
ctx->recvmmsg_msgs[i].msg_hdr.msg_control = nullptr;
|
||||
ctx->recvmmsg_msgs[i].msg_hdr.msg_controllen = 0;
|
||||
}
|
||||
ctx->recvmmsg_initialized = true;
|
||||
|
||||
tls_nstun_ctx = ctx;
|
||||
success = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void nstun_destroy_parent() {
|
||||
if (tls_nstun_ctx) {
|
||||
context_cleanup(tls_nstun_ctx);
|
||||
delete tls_nstun_ctx;
|
||||
tls_nstun_ctx = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#include <memory>
|
||||
|
||||
#include "monitor.h"
|
||||
|
||||
typedef enum {
|
||||
NSTUN_ACTION_DROP,
|
||||
@@ -50,11 +50,9 @@ typedef struct {
|
||||
} nstun_rule_t;
|
||||
struct nsj_t;
|
||||
|
||||
bool nstun_init_child(int sock, struct nsj_t* nsj);
|
||||
bool nstun_init_parent(int sock, struct nsj_t* nsj);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
bool nstun_init_child(int ipc_fd, struct nsj_t* nsj);
|
||||
bool nstun_init_parent(int tap_fd, struct nsj_t* nsj, pid_t pid);
|
||||
void nstun_destroy_parent();
|
||||
void nstun_periodic();
|
||||
|
||||
#endif /* NSTUN_H_ */
|
||||
161
nstun/policy.cc
161
nstun/policy.cc
@@ -1,5 +1,6 @@
|
||||
#include "policy.h"
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "core.h"
|
||||
@@ -14,20 +15,40 @@ namespace nstun {
|
||||
|
||||
RuleResult evaluate_rules4(Context* ctx, nstun_direction_t dir, nstun_proto_t proto,
|
||||
uint32_t src_ip4, uint32_t dst_ip4, uint16_t sport, uint16_t dport) {
|
||||
for (const auto& r : ctx->rules) {
|
||||
if (r.is_ipv6) continue;
|
||||
if (r.direction != dir) continue;
|
||||
if (r.proto != NSTUN_PROTO_ANY && r.proto != proto) continue;
|
||||
|
||||
if (r.src_ip4 != 0 && (src_ip4 & r.src_mask4) != (r.src_ip4 & r.src_mask4))
|
||||
LOG_D("evaluate_rules4: rules count=%zu", ctx->c_rules_count);
|
||||
for (size_t i = 0; i < ctx->c_rules_count; ++i) {
|
||||
const auto& r = ctx->c_rules[i];
|
||||
if (r.is_ipv6) {
|
||||
continue;
|
||||
if (r.dst_ip4 != 0 && (dst_ip4 & r.dst_mask4) != (r.dst_ip4 & r.dst_mask4))
|
||||
}
|
||||
if (r.direction != dir) {
|
||||
continue;
|
||||
}
|
||||
if (r.proto != NSTUN_PROTO_ANY && r.proto != proto) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.sport_start != 0 && (sport < r.sport_start || sport > r.sport_end)) continue;
|
||||
if (r.dport_start != 0 && (dport < r.dport_start || dport > r.dport_end)) continue;
|
||||
if (r.src_mask4 != 0 && (src_ip4 & r.src_mask4) != (r.src_ip4 & r.src_mask4)) {
|
||||
continue;
|
||||
}
|
||||
if (r.dst_mask4 != 0 && (dst_ip4 & r.dst_mask4) != (r.dst_ip4 & r.dst_mask4)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RuleResult res = {r.action, 0, 0, false, {}};
|
||||
if (r.sport_start != 0 && (sport < r.sport_start || sport > r.sport_end)) {
|
||||
continue;
|
||||
}
|
||||
if (r.dport_start != 0 && (dport < r.dport_start || dport > r.dport_end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RuleResult res = {
|
||||
.action = r.action,
|
||||
.redirect_ip4 = 0,
|
||||
.redirect_port = 0,
|
||||
.has_redirect_ip6 = false,
|
||||
.redirect_ip6 = {},
|
||||
};
|
||||
if (r.action == NSTUN_ACTION_REDIRECT || r.action == NSTUN_ACTION_ENCAP_SOCKS5 ||
|
||||
r.action == NSTUN_ACTION_ENCAP_CONNECT) {
|
||||
res.redirect_ip4 = r.redirect_ip4;
|
||||
@@ -35,39 +56,62 @@ RuleResult evaluate_rules4(Context* ctx, nstun_direction_t dir, nstun_proto_t pr
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return {NSTUN_ACTION_ALLOW, 0, 0, false, {}}; /* Default allow */
|
||||
return {
|
||||
.action = NSTUN_ACTION_ALLOW,
|
||||
.redirect_ip4 = 0,
|
||||
.redirect_port = 0,
|
||||
.has_redirect_ip6 = false,
|
||||
.redirect_ip6 = {},
|
||||
};
|
||||
}
|
||||
|
||||
static bool ip6_masked_eq(const uint8_t* a, const uint8_t* b, const uint8_t* mask) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if ((a[i] & mask[i]) != (b[i] & mask[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ip6_is_zero(const uint8_t* addr) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (addr[i] != 0) return false;
|
||||
if ((a[i] & mask[i]) != (b[i] & mask[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
RuleResult evaluate_rules6(Context* ctx, nstun_direction_t dir, nstun_proto_t proto,
|
||||
const uint8_t* src_ip6, const uint8_t* dst_ip6, uint16_t sport, uint16_t dport) {
|
||||
for (const auto& r : ctx->rules) {
|
||||
if (!r.is_ipv6) continue;
|
||||
if (r.direction != dir) continue;
|
||||
if (r.proto != NSTUN_PROTO_ANY && r.proto != proto) continue;
|
||||
|
||||
if (!ip6_is_zero(r.src_ip6) && !ip6_masked_eq(src_ip6, r.src_ip6, r.src_mask6))
|
||||
static const uint8_t kZeroIp6[16] = {0};
|
||||
for (size_t i = 0; i < ctx->c_rules_count; ++i) {
|
||||
const auto& r = ctx->c_rules[i];
|
||||
if (!r.is_ipv6) {
|
||||
continue;
|
||||
if (!ip6_is_zero(r.dst_ip6) && !ip6_masked_eq(dst_ip6, r.dst_ip6, r.dst_mask6))
|
||||
}
|
||||
if (r.direction != dir) {
|
||||
continue;
|
||||
}
|
||||
if (r.proto != NSTUN_PROTO_ANY && r.proto != proto) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.sport_start != 0 && (sport < r.sport_start || sport > r.sport_end)) continue;
|
||||
if (r.dport_start != 0 && (dport < r.dport_start || dport > r.dport_end)) continue;
|
||||
if (memcmp(r.src_mask6, kZeroIp6, 16) != 0 &&
|
||||
!ip6_masked_eq(src_ip6, r.src_ip6, r.src_mask6)) {
|
||||
continue;
|
||||
}
|
||||
if (memcmp(r.dst_mask6, kZeroIp6, 16) != 0 &&
|
||||
!ip6_masked_eq(dst_ip6, r.dst_ip6, r.dst_mask6)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RuleResult res = {r.action, 0, 0, false, {}};
|
||||
if (r.sport_start != 0 && (sport < r.sport_start || sport > r.sport_end)) {
|
||||
continue;
|
||||
}
|
||||
if (r.dport_start != 0 && (dport < r.dport_start || dport > r.dport_end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RuleResult res = {
|
||||
.action = r.action,
|
||||
.redirect_ip4 = 0,
|
||||
.redirect_port = 0,
|
||||
.has_redirect_ip6 = false,
|
||||
.redirect_ip6 = {},
|
||||
};
|
||||
if (r.action == NSTUN_ACTION_REDIRECT) {
|
||||
res.has_redirect_ip6 = true;
|
||||
memcpy(res.redirect_ip6, r.redirect_ip6, sizeof(res.redirect_ip6));
|
||||
@@ -80,11 +124,17 @@ RuleResult evaluate_rules6(Context* ctx, nstun_direction_t dir, nstun_proto_t pr
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return {NSTUN_ACTION_ALLOW, 0, 0, false, {}}; /* Default allow */
|
||||
return {
|
||||
.action = NSTUN_ACTION_ALLOW,
|
||||
.redirect_ip4 = 0,
|
||||
.redirect_port = 0,
|
||||
.has_redirect_ip6 = false,
|
||||
.redirect_ip6 = {},
|
||||
};
|
||||
}
|
||||
|
||||
template <typename RuleMsg>
|
||||
RuleParseStatus fill_rule_common(const RuleMsg& r, nstun_rule_t* nr) {
|
||||
RuleParseStatus fill_rule_common(
|
||||
const nsjail::NsJailConfig_UserNet_NstunRule& r, nstun_rule_t* nr) {
|
||||
if ((r.action() == nsjail::NsJailConfig_UserNet_NstunRule_Action_ENCAP_SOCKS5 ||
|
||||
r.action() == nsjail::NsJailConfig_UserNet_NstunRule_Action_ENCAP_CONNECT) &&
|
||||
r.proto() == nsjail::NsJailConfig_UserNet_NstunRule_Protocol_ICMP) {
|
||||
@@ -98,30 +148,42 @@ RuleParseStatus fill_rule_common(const RuleMsg& r, nstun_rule_t* nr) {
|
||||
nr->direction = NSTUN_DIR_GUEST_TO_HOST;
|
||||
}
|
||||
|
||||
if (r.action() == nsjail::NsJailConfig_UserNet_NstunRule_Action_DROP) {
|
||||
switch (r.action()) {
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Action_DROP:
|
||||
nr->action = NSTUN_ACTION_DROP;
|
||||
} else if (r.action() == nsjail::NsJailConfig_UserNet_NstunRule_Action_REJECT) {
|
||||
break;
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Action_REJECT:
|
||||
nr->action = NSTUN_ACTION_REJECT;
|
||||
} else if (r.action() == nsjail::NsJailConfig_UserNet_NstunRule_Action_ALLOW) {
|
||||
break;
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Action_ALLOW:
|
||||
nr->action = NSTUN_ACTION_ALLOW;
|
||||
} else if (r.action() == nsjail::NsJailConfig_UserNet_NstunRule_Action_REDIRECT) {
|
||||
break;
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Action_REDIRECT:
|
||||
nr->action = NSTUN_ACTION_REDIRECT;
|
||||
} else if (r.action() == nsjail::NsJailConfig_UserNet_NstunRule_Action_ENCAP_SOCKS5) {
|
||||
break;
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Action_ENCAP_SOCKS5:
|
||||
nr->action = NSTUN_ACTION_ENCAP_SOCKS5;
|
||||
} else if (r.action() == nsjail::NsJailConfig_UserNet_NstunRule_Action_ENCAP_CONNECT) {
|
||||
break;
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Action_ENCAP_CONNECT:
|
||||
nr->action = NSTUN_ACTION_ENCAP_CONNECT;
|
||||
} else {
|
||||
break;
|
||||
default:
|
||||
return RuleParseStatus::IGNORE;
|
||||
}
|
||||
|
||||
if (r.proto() == nsjail::NsJailConfig_UserNet_NstunRule_Protocol_TCP) {
|
||||
switch (r.proto()) {
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Protocol_TCP:
|
||||
nr->proto = NSTUN_PROTO_TCP;
|
||||
} else if (r.proto() == nsjail::NsJailConfig_UserNet_NstunRule_Protocol_UDP) {
|
||||
break;
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Protocol_UDP:
|
||||
nr->proto = NSTUN_PROTO_UDP;
|
||||
} else if (r.proto() == nsjail::NsJailConfig_UserNet_NstunRule_Protocol_ICMP) {
|
||||
break;
|
||||
case nsjail::NsJailConfig_UserNet_NstunRule_Protocol_ICMP:
|
||||
nr->proto = NSTUN_PROTO_ICMP;
|
||||
} else {
|
||||
break;
|
||||
default:
|
||||
nr->proto = NSTUN_PROTO_ANY;
|
||||
break;
|
||||
}
|
||||
|
||||
nr->sport_start = r.has_sport() ? r.sport() : 0;
|
||||
@@ -130,11 +192,16 @@ RuleParseStatus fill_rule_common(const RuleMsg& r, nstun_rule_t* nr) {
|
||||
nr->dport_start = r.has_dport() ? r.dport() : 0;
|
||||
nr->dport_end = r.has_dport_end() ? r.dport_end() : nr->dport_start;
|
||||
|
||||
if (nr->sport_end < nr->sport_start) {
|
||||
LOG_E("Invalid source port range: %u - %u", nr->sport_start, nr->sport_end);
|
||||
return RuleParseStatus::ABORT;
|
||||
}
|
||||
if (nr->dport_end < nr->dport_start) {
|
||||
LOG_E("Invalid destination port range: %u - %u", nr->dport_start, nr->dport_end);
|
||||
return RuleParseStatus::ABORT;
|
||||
}
|
||||
|
||||
return RuleParseStatus::OK;
|
||||
}
|
||||
|
||||
/* Explicit template instantiation for the protobuf rule message type */
|
||||
template RuleParseStatus fill_rule_common<nsjail::NsJailConfig_UserNet_NstunRule>(
|
||||
const nsjail::NsJailConfig_UserNet_NstunRule& r, nstun_rule_t* nr);
|
||||
|
||||
} /* namespace nstun */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef NSTUN_POLICY_H_
|
||||
#define NSTUN_POLICY_H_
|
||||
|
||||
#include "config.pb.h"
|
||||
#include "core.h"
|
||||
#include "nstun.h"
|
||||
|
||||
@@ -18,8 +19,7 @@ RuleResult evaluate_rules4(Context* ctx, nstun_direction_t dir, nstun_proto_t pr
|
||||
RuleResult evaluate_rules6(Context* ctx, nstun_direction_t dir, nstun_proto_t proto,
|
||||
const uint8_t* src_ip6, const uint8_t* dst_ip6, uint16_t sport, uint16_t dport);
|
||||
|
||||
template <typename RuleMsg>
|
||||
RuleParseStatus fill_rule_common(const RuleMsg& r, nstun_rule_t* nr);
|
||||
RuleParseStatus fill_rule_common(const nsjail::NsJailConfig_UserNet_NstunRule& r, nstun_rule_t* nr);
|
||||
|
||||
} /* namespace nstun */
|
||||
|
||||
|
||||
2200
nstun/tcp.cc
2200
nstun/tcp.cc
File diff suppressed because it is too large
Load Diff
78
nstun/tcp.h
78
nstun/tcp.h
@@ -1,83 +1,23 @@
|
||||
#ifndef NSTUN_TCP_H_
|
||||
#define NSTUN_TCP_H_
|
||||
|
||||
#include <deque>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "core.h"
|
||||
|
||||
namespace nstun {
|
||||
|
||||
enum class TcpState {
|
||||
SYN_SENT, /* Host connecting to destination */
|
||||
SOCKS5_INIT, /* Sent SOCKS5 greeting, awaiting auth reply */
|
||||
SOCKS5_CONNECTING, /* Sent SOCKS5 CONNECT request, awaiting response */
|
||||
HTTP_CONNECT_WAIT, /* Sent HTTP CONNECT, awaiting proxy 200 reply */
|
||||
ESTABLISHED,
|
||||
FIN_WAIT_1,
|
||||
FIN_WAIT_2,
|
||||
CLOSING,
|
||||
TIME_WAIT,
|
||||
CLOSE_WAIT,
|
||||
};
|
||||
void handle_host_tcp_event(Context* ctx, TcpFlow* flow, int fd, uint32_t events);
|
||||
void tcp_periodic_check(Context* ctx, TcpFlow* flow, time_t now);
|
||||
bool is_stale_tcp(const TcpFlow* flow, time_t now);
|
||||
|
||||
struct TcpFlow : public Flow {
|
||||
int host_fd = -1;
|
||||
union {
|
||||
FlowKey4 key4;
|
||||
FlowKey6 key6;
|
||||
};
|
||||
|
||||
TcpState state = TcpState::SYN_SENT;
|
||||
ProxyMode proxy_mode = ProxyMode::NONE;
|
||||
bool host_eof = false;
|
||||
bool guest_eof = false;
|
||||
bool fin_sent = false;
|
||||
bool syn_acked = false;
|
||||
bool fin_acked = false;
|
||||
|
||||
uint32_t seq_to_guest = 0;
|
||||
uint32_t ack_from_guest = 0;
|
||||
|
||||
uint32_t seq_from_guest = 0;
|
||||
uint32_t ack_to_guest = 0;
|
||||
|
||||
/* Buffer for data from host to guest (not yet ACKed) */
|
||||
/* In a real TCP stack, this would handle retransmissions. */
|
||||
/* Here, we just queue it to send. */
|
||||
std::vector<uint8_t> tx_buffer;
|
||||
size_t tx_acked_offset = 0;
|
||||
|
||||
/* Buffer for accumulating proxy handshake responses (SOCKS5/HTTP CONNECT) */
|
||||
std::vector<uint8_t> proxy_rx_buffer;
|
||||
|
||||
/* Buffer for data from guest to host to avoid dropping packets on EAGAIN */
|
||||
std::vector<uint8_t> rx_buffer;
|
||||
size_t rx_sent_offset = 0;
|
||||
|
||||
bool epoll_out_registered = false;
|
||||
bool epoll_in_disabled = false;
|
||||
bool inbound = false; /* true if flow is HOST_TO_GUEST */
|
||||
~TcpFlow() override {
|
||||
if (host_fd != -1) ::close(host_fd);
|
||||
}
|
||||
|
||||
void handle_host_event(Context* ctx, int fd, uint32_t events) override;
|
||||
void periodic_check(Context* ctx, time_t now) override;
|
||||
bool is_stale(time_t now) const override;
|
||||
void destroy(Context* ctx) override;
|
||||
};
|
||||
|
||||
void tcp_send_packet4(
|
||||
Context* ctx, TcpFlow* flow, uint8_t flags, const uint8_t* data = nullptr, size_t len = 0);
|
||||
void tcp_send_packet6(
|
||||
Context* ctx, TcpFlow* flow, uint8_t flags, const uint8_t* data = nullptr, size_t len = 0);
|
||||
bool tcp_send_packet4(Context* ctx, const TcpFlow* flow, uint8_t flags,
|
||||
const uint8_t* data = nullptr, size_t len = 0);
|
||||
bool tcp_send_packet6(Context* ctx, const TcpFlow* flow, uint8_t flags,
|
||||
const uint8_t* data = nullptr, size_t len = 0);
|
||||
void tcp_destroy_flow(Context* ctx, TcpFlow* flow);
|
||||
void push_to_guest(Context* ctx, TcpFlow* flow);
|
||||
|
||||
void handle_tcp4(Context* ctx, const ip4_hdr* ip, std::span<const uint8_t> payload);
|
||||
void handle_tcp6(Context* ctx, const ip6_hdr* ip, std::span<const uint8_t> payload);
|
||||
void handle_tcp4(Context* ctx, const ip4_hdr* ip, const uint8_t* data, size_t len);
|
||||
void handle_tcp6(Context* ctx, const ip6_hdr* ip, const uint8_t* data, size_t len);
|
||||
void handle_host_tcp(Context* ctx, TcpFlow* flow, uint32_t events);
|
||||
void handle_host_tcp_accept(Context* ctx, int listen_fd, const nstun_rule_t& rule);
|
||||
|
||||
|
||||
10
nstun/tun.cc
10
nstun/tun.cc
@@ -6,8 +6,6 @@
|
||||
#include <sys/uio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "core.h"
|
||||
#include "icmp.h"
|
||||
#include "ip.h"
|
||||
@@ -23,9 +21,9 @@ bool send_to_guest_v(
|
||||
}
|
||||
|
||||
struct iovec iov[2];
|
||||
iov[0].iov_base = (void*)header;
|
||||
iov[0].iov_base = const_cast<void*>(header);
|
||||
iov[0].iov_len = header_len;
|
||||
iov[1].iov_base = (void*)payload;
|
||||
iov[1].iov_base = const_cast<void*>(payload);
|
||||
iov[1].iov_len = payload_len;
|
||||
|
||||
size_t total_len = header_len + payload_len;
|
||||
@@ -54,10 +52,10 @@ void handle_tun_frame(Context* ctx, const uint8_t* buf, size_t len) {
|
||||
|
||||
switch (version) {
|
||||
case 4:
|
||||
handle_ip4(ctx, std::span(buf, len));
|
||||
handle_ip4(ctx, buf, len);
|
||||
break;
|
||||
case 6:
|
||||
handle_ip6(ctx, std::span(buf, len));
|
||||
handle_ip6(ctx, buf, len);
|
||||
break;
|
||||
default:
|
||||
LOG_D("Unknown IP version: %u", version);
|
||||
|
||||
1478
nstun/udp.cc
1478
nstun/udp.cc
File diff suppressed because it is too large
Load Diff
@@ -4,15 +4,16 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "core.h"
|
||||
|
||||
namespace nstun {
|
||||
|
||||
void handle_udp4(Context* ctx, const ip4_hdr* ip, std::span<const uint8_t> payload);
|
||||
void handle_udp6(Context* ctx, const ip6_hdr* ip, std::span<const uint8_t> payload);
|
||||
void handle_udp4(Context* ctx, const ip4_hdr* ip, const uint8_t* data, size_t len);
|
||||
void handle_udp6(Context* ctx, const ip6_hdr* ip, const uint8_t* data, size_t len);
|
||||
void handle_host_udp_accept(Context* ctx, int listen_fd, const nstun_rule_t& rule);
|
||||
void handle_host_udp_event(Context* ctx, UdpFlow* flow, int fd, uint32_t events);
|
||||
bool is_stale_udp(const UdpFlow* flow, time_t now);
|
||||
void udp_destroy_flow(Context* ctx, UdpFlow* flow);
|
||||
|
||||
} // namespace nstun
|
||||
|
||||
|
||||
2
pid.cc
2
pid.cc
@@ -48,7 +48,7 @@ bool initNs(nsj_t* nsj) {
|
||||
* first clone/fork will work, and the rest will fail with ENOMEM (see 'man pid_namespaces'
|
||||
* for details on this behavior)
|
||||
*/
|
||||
pid_t pid = subproc::cloneProc(CLONE_FS, 0);
|
||||
pid_t pid = subproc::cloneProcNoPidfd(CLONE_FS, 0);
|
||||
if (pid == -1) {
|
||||
PLOG_E("Couldn't create a dummy init process");
|
||||
return false;
|
||||
|
||||
35
sandbox.cc
35
sandbox.cc
@@ -36,28 +36,14 @@ extern "C" {
|
||||
#include "kafel.h"
|
||||
}
|
||||
#include "logs.h"
|
||||
#include "missing_defs.h"
|
||||
#include "monitor.h"
|
||||
#include "unotify/syscall_defs.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace sandbox {
|
||||
|
||||
#ifndef PR_SET_NO_NEW_PRIVS /* in prctl.h since Linux 3.5 */
|
||||
#define PR_SET_NO_NEW_PRIVS 38
|
||||
#endif /* PR_SET_NO_NEW_PRIVS */
|
||||
|
||||
#ifndef SECCOMP_FILTER_FLAG_TSYNC
|
||||
#define SECCOMP_FILTER_FLAG_TSYNC (1UL << 0)
|
||||
#endif /* SECCOMP_FILTER_FLAG_TSYNC */
|
||||
|
||||
#ifndef SECCOMP_FILTER_FLAG_LOG
|
||||
#define SECCOMP_FILTER_FLAG_LOG (1UL << 1)
|
||||
#endif /* SECCOMP_FILTER_FLAG_LOG */
|
||||
|
||||
#ifndef SECCOMP_FILTER_FLAG_NEW_LISTENER
|
||||
#define SECCOMP_FILTER_FLAG_NEW_LISTENER (1UL << 3)
|
||||
#endif /* SECCOMP_FILTER_FLAG_NEW_LISTENER */
|
||||
|
||||
bool installUnotifyFilter(nsj_t* nsj, int pipefd) {
|
||||
bool installUnotifyFilter(nsj_t* nsj, int ipc_fd) {
|
||||
if (!nsj->njc.seccomp_unotify()) {
|
||||
return true;
|
||||
}
|
||||
@@ -85,8 +71,8 @@ bool installUnotifyFilter(nsj_t* nsj, int pipefd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!util::sendFd(pipefd, unotif_fd)) {
|
||||
PLOG_E("sendFd(unotif_fd) to parent failed");
|
||||
if (!util::sendMsg(ipc_fd, monitor::MSG_TAG_UNOTIFY, unotif_fd)) {
|
||||
PLOG_E("sendMsg(unotif_fd) to parent failed");
|
||||
close(unotif_fd);
|
||||
return false;
|
||||
}
|
||||
@@ -127,9 +113,9 @@ static bool prepareAndCommit(nsj_t* nsj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool applyPolicy(nsj_t* nsj, int pipefd) {
|
||||
if (pipefd != -1 && nsj->njc.seccomp_unotify()) {
|
||||
if (!installUnotifyFilter(nsj, pipefd)) {
|
||||
bool applyPolicy(nsj_t* nsj, int ipc_fd) {
|
||||
if (ipc_fd != -1 && nsj->njc.seccomp_unotify()) {
|
||||
if (!installUnotifyFilter(nsj, ipc_fd)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -171,8 +157,9 @@ bool preparePolicy(nsj_t* nsj) {
|
||||
|
||||
kafel_ctxt_t ctxt = kafel_ctxt_create();
|
||||
std::string combined_seccomp_policy;
|
||||
FILE* f = nullptr;
|
||||
if (!nsj->njc.seccomp_policy_file().empty()) {
|
||||
FILE* f = fopen(nsj->njc.seccomp_policy_file().c_str(), "r");
|
||||
f = fopen(nsj->njc.seccomp_policy_file().c_str(), "re");
|
||||
if (!f) {
|
||||
PLOG_W("Couldn't open the kafel seccomp policy file '%s'",
|
||||
nsj->njc.seccomp_policy_file().c_str());
|
||||
@@ -197,9 +184,11 @@ bool preparePolicy(nsj_t* nsj) {
|
||||
if (kafel_compile(ctxt, &nsj->seccomp_fprog) != 0) {
|
||||
LOG_E("Could not compile policy: %s", kafel_error_msg(ctxt));
|
||||
kafel_ctxt_destroy(&ctxt);
|
||||
if (f) fclose(f);
|
||||
return false;
|
||||
}
|
||||
kafel_ctxt_destroy(&ctxt);
|
||||
if (f) fclose(f);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
|
||||
namespace sandbox {
|
||||
|
||||
bool installUnotifyFilter(nsj_t* nsj, int pipefd);
|
||||
bool applyPolicy(nsj_t* nsj, int pipefd);
|
||||
bool installUnotifyFilter(nsj_t* nsj, int ipc_fd);
|
||||
bool applyPolicy(nsj_t* nsj, int ipc_fd);
|
||||
bool preparePolicy(nsj_t* nsj);
|
||||
void closePolicy(nsj_t* nsj);
|
||||
|
||||
|
||||
375
sockproxy/sockproxy.cc
Normal file
375
sockproxy/sockproxy.cc
Normal file
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* nsjail - socket proxy for listen mode (-Ml)
|
||||
* -----------------------------------------
|
||||
*
|
||||
* Proxies data between an external TCP connection (sock_fd) and the
|
||||
* child process's stdin/stdout pipes using splice(2) for zero-copy
|
||||
* transfer.
|
||||
*
|
||||
* Two independent half-duplex channels:
|
||||
* sock_fd -> pipe_in (network to child stdin)
|
||||
* pipe_out -> sock_fd (child stdout to network)
|
||||
*
|
||||
* Each channel drains independently with half-close semantics.
|
||||
* When both channels are done, the proxy tears itself down and
|
||||
* signals the monitor to stop the event loop.
|
||||
*/
|
||||
|
||||
#include "sockproxy.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "logs.h"
|
||||
#include "monitor.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace sockproxy {
|
||||
|
||||
static constexpr size_t kSpliceChunkSize = 65536;
|
||||
static constexpr int kMaxSpliceLoops = 16;
|
||||
|
||||
/* --- connection state ---------------------------------- */
|
||||
|
||||
struct channel_t {
|
||||
int pipe_fd = -1;
|
||||
bool blocked = false;
|
||||
bool registered = false;
|
||||
};
|
||||
|
||||
struct conn_t {
|
||||
int sock_fd = -1;
|
||||
bool sock_registered = false;
|
||||
channel_t sock_to_pipe;
|
||||
channel_t pipe_to_sock;
|
||||
on_close_cb_t close_cb = nullptr;
|
||||
void* cb_data = nullptr;
|
||||
};
|
||||
|
||||
static thread_local conn_t current_conn;
|
||||
|
||||
/* --- helpers ------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Remove an FD from epoll, close it, and invalidate the slot.
|
||||
* Safe to call with *fd == -1 (no-op).
|
||||
*/
|
||||
static void closeAndUnregister(channel_t* chan) {
|
||||
if (chan->pipe_fd >= 0) {
|
||||
if (chan->registered) {
|
||||
monitor::removeFd(chan->pipe_fd);
|
||||
chan->registered = false;
|
||||
}
|
||||
close(chan->pipe_fd);
|
||||
chan->pipe_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Tear down an entire connection: unregister all FDs from epoll,
|
||||
* close everything, and clear the state.
|
||||
*/
|
||||
static void teardownConn(conn_t* conn) {
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
closeAndUnregister(&conn->sock_to_pipe);
|
||||
closeAndUnregister(&conn->pipe_to_sock);
|
||||
if (conn->sock_fd >= 0) {
|
||||
if (conn->sock_registered) {
|
||||
monitor::removeFd(conn->sock_fd);
|
||||
conn->sock_registered = false;
|
||||
}
|
||||
close(conn->sock_fd);
|
||||
conn->sock_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Tear down connection and notify monitor.
|
||||
*/
|
||||
static void conclude(conn_t* conn) {
|
||||
teardownConn(conn);
|
||||
if (conn->close_cb) {
|
||||
conn->close_cb(conn->cb_data);
|
||||
} else {
|
||||
monitor::stop();
|
||||
}
|
||||
}
|
||||
|
||||
/* --- splice pump --------------------------------------- */
|
||||
|
||||
enum class PumpResult {
|
||||
kMoved, /* data transferred, loop again */
|
||||
kEof, /* EOF or hard error -- close channel */
|
||||
kBlocked, /* dest full, need EPOLLOUT on dest */
|
||||
kDrained, /* source empty, need EPOLLIN on src */
|
||||
};
|
||||
|
||||
/*
|
||||
* One non-blocking splice(src -> dst).
|
||||
* Exactly one of src/dst must be a pipe end (splice(2) requirement).
|
||||
* On EAGAIN, FIONREAD on check_fd distinguishes "source empty"
|
||||
* from "destination full".
|
||||
*/
|
||||
static PumpResult splicePump(int src, int dst, int check_fd) {
|
||||
ssize_t r = TEMP_FAILURE_RETRY(
|
||||
splice(src, nullptr, dst, nullptr, kSpliceChunkSize, SPLICE_F_NONBLOCK));
|
||||
|
||||
if (r > 0) {
|
||||
return PumpResult::kMoved;
|
||||
}
|
||||
if (r == 0) {
|
||||
return PumpResult::kEof;
|
||||
}
|
||||
if (r == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
PLOG_D("splice(src=%d, dst=%d) error", src, dst);
|
||||
return PumpResult::kEof;
|
||||
}
|
||||
|
||||
/* EAGAIN: source empty or dest full? */
|
||||
int n = 0;
|
||||
if (ioctl(check_fd, FIONREAD, &n) == -1) {
|
||||
PLOG_W("ioctl(FIONREAD, fd=%d)", check_fd);
|
||||
return PumpResult::kEof;
|
||||
}
|
||||
return (n == 0) ? PumpResult::kDrained : PumpResult::kBlocked;
|
||||
}
|
||||
|
||||
/*
|
||||
* Drain one direction until it can't proceed.
|
||||
* On EOF/error, *pipe_fd is removed from epoll and closed.
|
||||
* Returns false if the channel hit EOF (*pipe_fd is now -1).
|
||||
*/
|
||||
static bool drainChannel(int src, int dst, int check_fd, channel_t* chan) {
|
||||
int loops = 0;
|
||||
while (!chan->blocked && chan->pipe_fd >= 0) {
|
||||
switch (splicePump(src, dst, check_fd)) {
|
||||
case PumpResult::kMoved:
|
||||
if (++loops > kMaxSpliceLoops) {
|
||||
return true; /* Yield to event loop for fairness */
|
||||
}
|
||||
continue;
|
||||
case PumpResult::kEof:
|
||||
closeAndUnregister(chan);
|
||||
return false;
|
||||
case PumpResult::kBlocked:
|
||||
chan->blocked = true;
|
||||
return true;
|
||||
case PumpResult::kDrained:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return (chan->pipe_fd >= 0);
|
||||
}
|
||||
|
||||
static void proxyPumpCb(int fd, uint32_t events, void* data);
|
||||
|
||||
static void updateFdMask(int fd, uint32_t events, bool* registered, conn_t* conn) {
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (events != 0) {
|
||||
if (!*registered) {
|
||||
if (monitor::addFd(fd, events, proxyPumpCb, conn)) {
|
||||
*registered = true;
|
||||
}
|
||||
} else {
|
||||
monitor::modFd(fd, events);
|
||||
}
|
||||
} else {
|
||||
if (*registered) {
|
||||
monitor::removeFd(fd);
|
||||
*registered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Recompute epoll event masks for all three FDs.
|
||||
*
|
||||
* Per-direction logic:
|
||||
* not blocked -> watch source for EPOLLIN (data available)
|
||||
* blocked -> watch dest for EPOLLOUT (space available)
|
||||
*/
|
||||
static void updateMasks(conn_t* conn) {
|
||||
uint32_t sock_ev = 0;
|
||||
uint32_t pipe_in_ev = 0;
|
||||
uint32_t pipe_out_ev = 0;
|
||||
|
||||
if (conn->sock_fd >= 0 && conn->sock_to_pipe.pipe_fd >= 0) {
|
||||
sock_ev |= EPOLLRDHUP;
|
||||
}
|
||||
|
||||
/* sock -> pipe_in */
|
||||
if (!conn->sock_to_pipe.blocked && conn->sock_to_pipe.pipe_fd >= 0) {
|
||||
sock_ev |= EPOLLIN;
|
||||
}
|
||||
if (conn->sock_to_pipe.blocked && conn->sock_fd >= 0) {
|
||||
pipe_in_ev |= EPOLLOUT;
|
||||
}
|
||||
|
||||
/* pipe_out -> sock */
|
||||
if (!conn->pipe_to_sock.blocked && conn->pipe_to_sock.pipe_fd >= 0) {
|
||||
pipe_out_ev |= EPOLLIN;
|
||||
}
|
||||
if (conn->pipe_to_sock.blocked && conn->pipe_to_sock.pipe_fd >= 0) {
|
||||
sock_ev |= EPOLLOUT;
|
||||
}
|
||||
|
||||
updateFdMask(conn->sock_fd, sock_ev, &conn->sock_registered, conn);
|
||||
updateFdMask(conn->sock_to_pipe.pipe_fd, pipe_in_ev, &conn->sock_to_pipe.registered, conn);
|
||||
updateFdMask(conn->pipe_to_sock.pipe_fd, pipe_out_ev, &conn->pipe_to_sock.registered, conn);
|
||||
}
|
||||
|
||||
/* --- epoll callback ------------------------------------ */
|
||||
|
||||
static void proxyPumpCb(int fd, uint32_t events, void* data) {
|
||||
conn_t* conn = static_cast<conn_t*>(data);
|
||||
|
||||
LOG_D("proxyPumpCb fd=%d events=0x%x", fd, events);
|
||||
|
||||
if (events & EPOLLERR) {
|
||||
LOG_D("EPOLLERR on fd=%d, tearing down connection", fd);
|
||||
conclude(conn);
|
||||
return;
|
||||
}
|
||||
|
||||
bool handled = false;
|
||||
|
||||
/* -- Direction 1: sock_fd -> pipe_in (network -> child stdin) -- */
|
||||
if (conn->sock_to_pipe.pipe_fd >= 0 && conn->sock_fd >= 0) {
|
||||
bool ready = false;
|
||||
if (fd == conn->sock_fd &&
|
||||
(events & (EPOLLIN | EPOLLRDHUP | EPOLLHUP | EPOLLERR))) {
|
||||
ready = true;
|
||||
}
|
||||
if (fd == conn->sock_to_pipe.pipe_fd &&
|
||||
(events & (EPOLLOUT | EPOLLHUP | EPOLLERR))) {
|
||||
ready = true;
|
||||
}
|
||||
|
||||
if (ready) {
|
||||
conn->sock_to_pipe.blocked = false;
|
||||
if (!drainChannel(conn->sock_fd, conn->sock_to_pipe.pipe_fd, conn->sock_fd,
|
||||
&conn->sock_to_pipe)) {
|
||||
LOG_D("sock->pipe EOF (client half-closed), closing child stdin");
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* -- Direction 2: pipe_out -> sock_fd (child stdout -> network) -- */
|
||||
if (conn->pipe_to_sock.pipe_fd >= 0 && conn->sock_fd >= 0) {
|
||||
bool ready = false;
|
||||
if (fd == conn->pipe_to_sock.pipe_fd &&
|
||||
(events & (EPOLLIN | EPOLLRDHUP | EPOLLHUP | EPOLLERR))) {
|
||||
ready = true;
|
||||
}
|
||||
if (fd == conn->sock_fd && (events & EPOLLOUT)) {
|
||||
ready = true;
|
||||
}
|
||||
|
||||
if (ready) {
|
||||
conn->pipe_to_sock.blocked = false;
|
||||
if (!drainChannel(conn->pipe_to_sock.pipe_fd, conn->sock_fd,
|
||||
conn->pipe_to_sock.pipe_fd, &conn->pipe_to_sock)) {
|
||||
LOG_D("pipe->sock EOF, half-closing socket write side");
|
||||
shutdown(conn->sock_fd, SHUT_WR);
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Socket broken (EPOLLHUP/EPOLLERR): stop feeding the child.
|
||||
* pipe_out may still have buffered data but splice will fail
|
||||
* writing to a dead socket, so both channels converge to -1.
|
||||
*/
|
||||
if (fd == conn->sock_fd && (events & (EPOLLHUP | EPOLLERR))) {
|
||||
LOG_D("Socket error/hup on sock_fd=%d, tearing down", fd);
|
||||
conclude(conn);
|
||||
return;
|
||||
}
|
||||
|
||||
/* -- Both done: tear down proxy and signal monitor to stop -- */
|
||||
if (conn->sock_to_pipe.pipe_fd == -1 && conn->pipe_to_sock.pipe_fd == -1) {
|
||||
LOG_D("Proxy fully drained, tearing down");
|
||||
conclude(conn);
|
||||
} else if (handled) {
|
||||
updateMasks(conn);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- public API ---------------------------------------- */
|
||||
|
||||
bool start(int* connfd, int* pipe_in, int* pipe_out, on_close_cb_t cb, void* data) {
|
||||
conn_t* conn = ¤t_conn;
|
||||
conn->sock_fd = *connfd;
|
||||
conn->sock_to_pipe.pipe_fd = *pipe_in;
|
||||
conn->pipe_to_sock.pipe_fd = *pipe_out;
|
||||
conn->close_cb = cb;
|
||||
conn->cb_data = data;
|
||||
|
||||
/* Belt-and-suspenders: make sock_fd non-blocking in addition to
|
||||
* SPLICE_F_NONBLOCK, so FIONREAD probes and future read()/write()
|
||||
* calls never block the event loop. */
|
||||
if (!util::setNonBlock(conn->sock_fd)) {
|
||||
conn->sock_fd = -1;
|
||||
conn->sock_to_pipe.pipe_fd = -1;
|
||||
conn->pipe_to_sock.pipe_fd = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!monitor::addFd(conn->sock_fd, EPOLLRDHUP, proxyPumpCb, conn)) {
|
||||
PLOG_E("addFd(sock_fd=%d)", conn->sock_fd);
|
||||
conn->sock_fd = -1;
|
||||
conn->sock_to_pipe.pipe_fd = -1;
|
||||
conn->pipe_to_sock.pipe_fd = -1;
|
||||
return false;
|
||||
}
|
||||
conn->sock_registered = true;
|
||||
|
||||
if (!monitor::addFd(conn->sock_to_pipe.pipe_fd, EPOLLRDHUP, proxyPumpCb, conn)) {
|
||||
PLOG_E("addFd(pipe_in=%d)", conn->sock_to_pipe.pipe_fd);
|
||||
monitor::removeFd(conn->sock_fd);
|
||||
conn->sock_registered = false;
|
||||
conn->sock_fd = -1;
|
||||
conn->sock_to_pipe.pipe_fd = -1;
|
||||
conn->pipe_to_sock.pipe_fd = -1;
|
||||
return false;
|
||||
}
|
||||
conn->sock_to_pipe.registered = true;
|
||||
|
||||
if (!monitor::addFd(conn->pipe_to_sock.pipe_fd, EPOLLRDHUP, proxyPumpCb, conn)) {
|
||||
PLOG_E("addFd(pipe_out=%d)", conn->pipe_to_sock.pipe_fd);
|
||||
monitor::removeFd(conn->sock_to_pipe.pipe_fd);
|
||||
conn->sock_to_pipe.registered = false;
|
||||
monitor::removeFd(conn->sock_fd);
|
||||
conn->sock_registered = false;
|
||||
conn->sock_fd = -1;
|
||||
conn->sock_to_pipe.pipe_fd = -1;
|
||||
conn->pipe_to_sock.pipe_fd = -1;
|
||||
return false;
|
||||
}
|
||||
conn->pipe_to_sock.registered = true;
|
||||
|
||||
/* Success: proxy owns the FDs now, invalidate caller's copies */
|
||||
*connfd = -1;
|
||||
*pipe_in = -1;
|
||||
*pipe_out = -1;
|
||||
|
||||
updateMasks(conn);
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
teardownConn(¤t_conn);
|
||||
}
|
||||
|
||||
} // namespace sockproxy
|
||||
23
sockproxy/sockproxy.h
Normal file
23
sockproxy/sockproxy.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef NS_SOCKPROXY_H
|
||||
#define NS_SOCKPROXY_H
|
||||
|
||||
namespace sockproxy {
|
||||
|
||||
/*
|
||||
* Start a socket proxy between an external TCP connection and the
|
||||
* child process's stdin/stdout pipes.
|
||||
*
|
||||
* Takes FD pointers: on success, the proxy owns the FDs and sets
|
||||
* them to -1 in the caller. On failure, FDs are untouched and
|
||||
* the caller remains responsible for closing them.
|
||||
*
|
||||
* When both channels finish, the proxy tears itself down and
|
||||
* calls monitor::stop() to exit the event loop.
|
||||
*/
|
||||
typedef void (*on_close_cb_t)(void* data);
|
||||
bool start(int* connfd, int* pipe_in, int* pipe_out, on_close_cb_t cb, void* data);
|
||||
void stop();
|
||||
|
||||
} // namespace sockproxy
|
||||
|
||||
#endif /* NS_SOCKPROXY_H */
|
||||
527
subproc.cc
527
subproc.cc
@@ -26,7 +26,6 @@
|
||||
#include <limits.h>
|
||||
#include <linux/sched.h>
|
||||
#include <sched.h>
|
||||
#include <setjmp.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
@@ -34,6 +33,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "cgroup.h"
|
||||
@@ -49,23 +50,17 @@
|
||||
#include "contain.h"
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "monitor.h"
|
||||
#include "net.h"
|
||||
#include "nstun/nstun.h"
|
||||
#include "sandbox.h"
|
||||
#include "unotify/unotify.h"
|
||||
#include "user.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace subproc {
|
||||
|
||||
#if !defined(CLONE_NEWCGROUP)
|
||||
#define CLONE_NEWCGROUP 0x02000000
|
||||
#endif /* !defined(CLONE_NEWCGROUP) */
|
||||
#if !defined(CLONE_NEWTIME)
|
||||
#define CLONE_NEWTIME 0x00000080
|
||||
#endif /* !defined(CLONE_NEWTIME) */
|
||||
|
||||
static const std::string cloneFlagsToStr(uint64_t flags) {
|
||||
static std::string cloneFlagsToStr(uint64_t flags) {
|
||||
std::string res;
|
||||
|
||||
struct {
|
||||
@@ -77,9 +72,7 @@ static const std::string cloneFlagsToStr(uint64_t flags) {
|
||||
NS_VALSTR_STRUCT(CLONE_FS),
|
||||
NS_VALSTR_STRUCT(CLONE_FILES),
|
||||
NS_VALSTR_STRUCT(CLONE_SIGHAND),
|
||||
#if !defined(CLONE_PIDFD)
|
||||
#define CLONE_PIDFD 0x00001000
|
||||
#endif
|
||||
|
||||
NS_VALSTR_STRUCT(CLONE_PIDFD),
|
||||
NS_VALSTR_STRUCT(CLONE_PTRACE),
|
||||
NS_VALSTR_STRUCT(CLONE_VFORK),
|
||||
@@ -100,13 +93,9 @@ static const std::string cloneFlagsToStr(uint64_t flags) {
|
||||
NS_VALSTR_STRUCT(CLONE_NEWPID),
|
||||
NS_VALSTR_STRUCT(CLONE_NEWNET),
|
||||
NS_VALSTR_STRUCT(CLONE_IO),
|
||||
#if !defined(CLONE_CLEAR_SIGHAND)
|
||||
#define CLONE_CLEAR_SIGHAND 0x100000000ULL
|
||||
#endif /* !defined(CLONE_CLEAR_SIGHAND) */
|
||||
|
||||
NS_VALSTR_STRUCT(CLONE_CLEAR_SIGHAND),
|
||||
#if !defined(CLONE_INTO_CGROUP)
|
||||
#define CLONE_INTO_CGROUP 0x200000000ULL
|
||||
#endif /* !defined(CLONE_INTO_CGROUP) */
|
||||
|
||||
NS_VALSTR_STRUCT(CLONE_INTO_CGROUP),
|
||||
};
|
||||
|
||||
@@ -139,16 +128,14 @@ static bool resetEnv(void) {
|
||||
/* Unblock all signals */
|
||||
sigset_t sset;
|
||||
sigemptyset(&sset);
|
||||
if (sigprocmask(SIG_SETMASK, &sset, NULL) == -1) {
|
||||
if (sigprocmask(SIG_SETMASK, &sset, nullptr) == -1) {
|
||||
PLOG_W("sigprocmask(SIG_SET, empty)");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char kSubprocErrorChar = 'E';
|
||||
|
||||
static const std::string concatArgs(const std::vector<const char*>& argv) {
|
||||
static std::string concatArgs(const std::vector<const char*>& argv) {
|
||||
std::string ret;
|
||||
for (const auto& s : argv) {
|
||||
if (s) {
|
||||
@@ -161,7 +148,17 @@ static const std::string concatArgs(const std::vector<const char*>& argv) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void newProc(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err, int pipefd) {
|
||||
static void newProc(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err, int ipc_fd) {
|
||||
auto connstr = net::connToText(netfd, /* remote= */ true, nullptr);
|
||||
|
||||
/*
|
||||
* Set parent death signal early to prevent the child from hanging
|
||||
* if the parent dies during the IPC handshake or setup.
|
||||
*/
|
||||
if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) == -1) {
|
||||
PLOG_W("prctl(PR_SET_PDEATHSIG, SIGKILL) failed");
|
||||
}
|
||||
|
||||
if (nsj->njc.has_oom_score_adj()) {
|
||||
std::string score = std::to_string(nsj->njc.oom_score_adj());
|
||||
if (!util::writeBufToFile(
|
||||
@@ -170,45 +167,40 @@ static void newProc(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err, in
|
||||
}
|
||||
}
|
||||
|
||||
if (!contain::setupFD(nsj, fd_in, fd_out, fd_err)) {
|
||||
if (!contain::setupFD(nsj, fd_in, fd_out, fd_err, ipc_fd)) {
|
||||
return;
|
||||
}
|
||||
if (!resetEnv()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pipefd == -1) {
|
||||
if (ipc_fd == -1) {
|
||||
if (!user::initNsFromParent(nsj, getpid())) {
|
||||
LOG_E("Couldn't initialize net user namespace");
|
||||
LOG_E("Couldn't initialize user namespace");
|
||||
return;
|
||||
}
|
||||
if (nsj->njc.use_cgroupv2()) {
|
||||
if (!cgroup2::initNsFromParent(nsj, getpid())) {
|
||||
LOG_E("Couldn't initialize net user namespace");
|
||||
LOG_E("Couldn't initialize cgroup v2 namespace");
|
||||
return;
|
||||
}
|
||||
} else if (!cgroup::initNsFromParent(nsj, getpid())) {
|
||||
LOG_E("Couldn't initialize net user namespace");
|
||||
LOG_E("Couldn't initialize cgroup namespace");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (pipefd != -1) {
|
||||
if (!net::initChildPreSync(nsj, pipefd)) {
|
||||
return;
|
||||
}
|
||||
char doneChar;
|
||||
if (util::readFromFd(pipefd, &doneChar, sizeof(doneChar)) !=
|
||||
sizeof(doneChar)) {
|
||||
return;
|
||||
}
|
||||
if (doneChar != kSubprocDoneChar) {
|
||||
return;
|
||||
}
|
||||
if (!net::initChildPreSync(nsj, ipc_fd)) {
|
||||
return;
|
||||
}
|
||||
uint32_t doneMsg;
|
||||
if (!util::recvMsg(ipc_fd, &doneMsg) || doneMsg != monitor::MSG_TAG_READY_H2J) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!contain::containProc(nsj)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nsj->njc.keep_env()) {
|
||||
clearenv();
|
||||
}
|
||||
@@ -216,7 +208,6 @@ static void newProc(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err, in
|
||||
putenv(const_cast<char*>(nsj->njc.envar(i).c_str()));
|
||||
}
|
||||
|
||||
auto connstr = net::connToText(netfd, /* remote= */ true, NULL);
|
||||
LOG_I("Executing %s for '%s'", QC(nsj->njc.exec_bin().path()), connstr.c_str());
|
||||
|
||||
std::vector<const char*> argv;
|
||||
@@ -228,28 +219,23 @@ static void newProc(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err, in
|
||||
LOG_D("Exec: %s, Args: [%s]", QC(nsj->njc.exec_bin().path()), concatArgs(argv).c_str());
|
||||
|
||||
/* Should be the last one in the sequence */
|
||||
if (!sandbox::applyPolicy(nsj, pipefd)) {
|
||||
if (!sandbox::applyPolicy(nsj, ipc_fd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pipefd != -1 && nsj->njc.seccomp_unotify()) {
|
||||
char doneChar;
|
||||
if (util::readFromFd(pipefd, &doneChar, sizeof(doneChar)) != sizeof(doneChar)) {
|
||||
if (ipc_fd != -1) {
|
||||
if (!util::sendMsg(ipc_fd, monitor::MSG_TAG_READY_J2H)) {
|
||||
return;
|
||||
}
|
||||
if (doneChar != kSubprocDoneChar) {
|
||||
uint32_t doneMsg;
|
||||
if (!util::recvMsg(ipc_fd, &doneMsg) || doneMsg != monitor::MSG_TAG_READY_H2J) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (nsj->njc.exec_bin().exec_fd()) {
|
||||
#if defined(__NR_execveat)
|
||||
util::syscall(__NR_execveat, nsj->exec_fd, (uintptr_t)"", (uintptr_t)argv.data(),
|
||||
(uintptr_t)environ, AT_EMPTY_PATH);
|
||||
#else /* defined(__NR_execveat) */
|
||||
LOG_E("Your system doesn't support execveat() syscall");
|
||||
return;
|
||||
#endif /* defined(__NR_execveat) */
|
||||
} else {
|
||||
execv(nsj->njc.exec_bin().path().c_str(), (char* const*)argv.data());
|
||||
}
|
||||
@@ -257,24 +243,24 @@ static void newProc(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err, in
|
||||
PLOG_E("execve(%s) failed", QC(nsj->njc.exec_bin().path()));
|
||||
}
|
||||
|
||||
static void addProc(nsj_t* nsj, pid_t pid, int sock) {
|
||||
static void addProc(nsj_t* nsj, pid_t pid, int sock, int pidfd) {
|
||||
pids_t p;
|
||||
|
||||
p.start = time(NULL);
|
||||
p.pid = pid;
|
||||
p.start = time(nullptr);
|
||||
|
||||
p.remote_txt = net::connToText(sock, /* remote= */ true, &p.remote_addr);
|
||||
p.pasta_pid = -1;
|
||||
|
||||
char fname[PATH_MAX];
|
||||
snprintf(fname, sizeof(fname), "/proc/%d/syscall", (int)pid);
|
||||
p.pid_syscall_fd = TEMP_FAILURE_RETRY(open(fname, O_RDONLY | O_CLOEXEC));
|
||||
p.pidfd = pidfd;
|
||||
|
||||
if (nsj->pids.find(pid) != nsj->pids.end()) {
|
||||
LOG_F("pid=%d already exists", pid);
|
||||
}
|
||||
nsj->pids.insert(std::make_pair(pid, p));
|
||||
|
||||
LOG_D("Added pid=%d with start time %u to the queue for IP: '%s'", pid,
|
||||
(unsigned int)p.start, p.remote_txt.c_str());
|
||||
|
||||
nsj->pids.emplace(pid, std::move(p));
|
||||
}
|
||||
|
||||
static void removeProc(nsj_t* nsj, pid_t pid) {
|
||||
@@ -283,16 +269,28 @@ static void removeProc(nsj_t* nsj, pid_t pid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& p = nsj->pids[pid];
|
||||
auto& p = nsj->pids[pid];
|
||||
if (p.pasta_pid > 0) {
|
||||
LOG_D("Killing pasta pid=%d", p.pasta_pid);
|
||||
kill(p.pasta_pid, SIGKILL);
|
||||
while (waitpid(p.pasta_pid, nullptr, 0) == -1 && errno == EINTR);
|
||||
TEMP_FAILURE_RETRY(waitpid(p.pasta_pid, nullptr, 0));
|
||||
}
|
||||
LOG_D("Removed pid=%d from the queue (IP:'%s', start time:'%s')", pid, p.remote_txt.c_str(),
|
||||
util::timeToStr(p.start).c_str());
|
||||
|
||||
close(p.pid_syscall_fd);
|
||||
if (p.thread.joinable()) {
|
||||
if (std::this_thread::get_id() == p.thread.get_id()) {
|
||||
p.thread.detach();
|
||||
} else {
|
||||
p.thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
/* pidfd is owned by pids_t - close it here on removal AFTER joining thread */
|
||||
if (p.pidfd >= 0) {
|
||||
close(p.pidfd);
|
||||
}
|
||||
|
||||
nsj->pids.erase(pid);
|
||||
}
|
||||
|
||||
@@ -302,214 +300,192 @@ int countProc(nsj_t* nsj) {
|
||||
|
||||
void displayProc(nsj_t* nsj) {
|
||||
LOG_I("Total number of spawned namespaces: %d", countProc(nsj));
|
||||
time_t now = time(NULL);
|
||||
time_t now;
|
||||
now = time(nullptr);
|
||||
|
||||
for (const auto& pid : nsj->pids) {
|
||||
time_t diff = now - pid.second.start;
|
||||
uint64_t left = nsj->njc.time_limit() ? nsj->njc.time_limit() - (uint64_t)diff : 0;
|
||||
LOG_I("pid=%d, Remote host: %s, Run time: %ld sec. (time left: %s s.)", pid.first,
|
||||
pid.second.remote_txt.c_str(), (long)diff,
|
||||
nsj->njc.time_limit() ? std::to_string(left).c_str() : "unlimited");
|
||||
}
|
||||
}
|
||||
|
||||
static void seccompViolation(nsj_t* nsj, siginfo_t* si) {
|
||||
LOG_W("pid=%d committed a syscall/seccomp violation and exited with SIGSYS", si->si_pid);
|
||||
|
||||
const auto& p = nsj->pids.find(si->si_pid);
|
||||
if (p == nsj->pids.end()) {
|
||||
LOG_W("pid=%d SiStatus:%d SiUid:%d SiUtime:%ld SiStime:%ld (If "
|
||||
"SiStatus==31 (SIGSYS), then see 'dmesg' or 'journalctl -ek' for possible "
|
||||
"auditd report with more data)",
|
||||
(int)si->si_pid, si->si_status, si->si_uid, (long)si->si_utime,
|
||||
(long)si->si_stime);
|
||||
LOG_E("Couldn't find pid element in the subproc list for pid=%d", (int)si->si_pid);
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[4096];
|
||||
ssize_t rdsize = util::readFromFd(p->second.pid_syscall_fd, buf, sizeof(buf) - 1);
|
||||
if (rdsize < 1) {
|
||||
LOG_W("pid=%d SiStatus:%d SiUid:%d SiUtime:%ld SiStime:%ld (If "
|
||||
"SiStatus==31 (SIGSYS), then see 'dmesg' or 'journalctl -ek' for possible "
|
||||
"auditd report with more data)",
|
||||
(int)si->si_pid, si->si_status, si->si_uid, (long)si->si_utime,
|
||||
(long)si->si_stime);
|
||||
return;
|
||||
}
|
||||
buf[rdsize - 1] = '\0';
|
||||
|
||||
uintptr_t arg1, arg2, arg3, arg4, arg5, arg6, sp, pc;
|
||||
ptrdiff_t sc;
|
||||
int ret = sscanf(buf, "%td %tx %tx %tx %tx %tx %tx %tx %tx", &sc, &arg1, &arg2, &arg3,
|
||||
&arg4, &arg5, &arg6, &sp, &pc);
|
||||
if (ret == 9) {
|
||||
LOG_W("pid=%d, Syscall number:%td, Arguments:%#tx, %#tx, %#tx, %#tx, %#tx, %#tx, "
|
||||
"SP:%#tx, PC:%#tx, si_status:%d",
|
||||
(int)si->si_pid, sc, arg1, arg2, arg3, arg4, arg5, arg6, sp, pc, si->si_status);
|
||||
} else if (ret == 3) {
|
||||
LOG_W("pid=%d SiStatus:%d SiUid:%d SiUtime:%ld SiStime:%ld SP:%#tx, PC:%#tx (If "
|
||||
"SiStatus==31 (SIGSYS), then see 'dmesg' or 'journalctl -ek' for possible "
|
||||
"auditd report with more data)",
|
||||
(int)si->si_pid, si->si_status, si->si_uid, (long)si->si_utime,
|
||||
(long)si->si_stime, arg1, arg2);
|
||||
return;
|
||||
} else {
|
||||
LOG_W("pid=%d SiStatus:%d SiUid:%d SiUtime:%ld SiStime:%ld (If "
|
||||
"SiStatus==31 (SIGSYS), then see 'dmesg' or 'journalctl -ek' for possible "
|
||||
"auditd report with more data)",
|
||||
(int)si->si_pid, si->si_status, si->si_uid, (long)si->si_utime,
|
||||
(long)si->si_stime);
|
||||
}
|
||||
}
|
||||
|
||||
static int reapProc(nsj_t* nsj, pid_t pid, bool should_wait = false) {
|
||||
int status;
|
||||
|
||||
if (wait4(pid, &status, should_wait ? 0 : WNOHANG, NULL) == pid) {
|
||||
if (nsj->njc.use_cgroupv2()) {
|
||||
cgroup2::finishFromParent(nsj, pid);
|
||||
if (nsj->njc.time_limit() && (uint64_t)diff < nsj->njc.time_limit()) {
|
||||
uint64_t left = nsj->njc.time_limit() - (uint64_t)diff;
|
||||
LOG_I("pid=%d, Remote host: %s, Run time: %ld sec. (time left: %" PRIu64
|
||||
" s.)",
|
||||
pid.first, pid.second.remote_txt.c_str(), (long)diff, left);
|
||||
} else {
|
||||
cgroup::finishFromParent(nsj, pid);
|
||||
LOG_I("pid=%d, Remote host: %s, Run time: %ld sec. (time left: %s)",
|
||||
pid.first, pid.second.remote_txt.c_str(), (long)diff,
|
||||
nsj->njc.time_limit() ? "expired" : "unlimited");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& pid_entry : nsj->pids) {
|
||||
if (pid_entry.second.pasta_pid > 0 && pid == pid_entry.second.pasta_pid) {
|
||||
int reapProc(nsj_t* nsj, pid_t pid, bool should_wait) {
|
||||
siginfo_t si;
|
||||
memset(&si, 0, sizeof(si));
|
||||
if (TEMP_FAILURE_RETRY(waitid(P_PID, pid, &si, WEXITED | (should_wait ? 0 : WNOHANG))) ==
|
||||
-1) {
|
||||
if (errno != ECHILD) {
|
||||
PLOG_W("waitid(P_PID, id=%d)", pid);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (si.si_pid == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nsj->njc.use_cgroupv2()) {
|
||||
cgroup2::finishFromParent(nsj, pid);
|
||||
} else {
|
||||
cgroup::finishFromParent(nsj, pid);
|
||||
}
|
||||
|
||||
for (auto& pid_entry : nsj->pids) {
|
||||
if (pid_entry.second.pasta_pid > 0 && pid == pid_entry.second.pasta_pid) {
|
||||
int status = si.si_status;
|
||||
if (si.si_code == CLD_EXITED) {
|
||||
LOG_W("Pasta process %d exited unexpectedly with status %d. "
|
||||
"Killing the jail.",
|
||||
pid, WEXITSTATUS(status));
|
||||
kill(pid_entry.first, SIGKILL);
|
||||
pid_entry.second.pasta_pid = -1;
|
||||
return WEXITSTATUS(status);
|
||||
pid, status);
|
||||
} else {
|
||||
LOG_W("Pasta process %d terminated by signal %d. Killing the jail.",
|
||||
pid, status);
|
||||
status = 128 + status;
|
||||
}
|
||||
}
|
||||
|
||||
std::string remote_txt = "[UNKNOWN]";
|
||||
const auto& p = nsj->pids.find(pid);
|
||||
if (p != nsj->pids.end()) {
|
||||
remote_txt = p->second.remote_txt;
|
||||
}
|
||||
|
||||
if (WIFEXITED(status)) {
|
||||
LOG_I("pid=%d (%s) exited with status: %d, (PIDs left: %d)", pid,
|
||||
remote_txt.c_str(), WEXITSTATUS(status), countProc(nsj) - 1);
|
||||
removeProc(nsj, pid);
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
if (WIFSIGNALED(status)) {
|
||||
LOG_I("pid=%d (%s) terminated with signal: %s (%d), (PIDs left: %d)", pid,
|
||||
remote_txt.c_str(), util::sigName(WTERMSIG(status)).c_str(),
|
||||
WTERMSIG(status), countProc(nsj) - 1);
|
||||
removeProc(nsj, pid);
|
||||
return 128 + WTERMSIG(status);
|
||||
util::syscall(
|
||||
__NR_pidfd_send_signal, pid_entry.second.pidfd, SIGKILL, 0, 0);
|
||||
pid_entry.second.pasta_pid = -1;
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
std::string remote_txt = "[UNKNOWN]";
|
||||
const auto& p = nsj->pids.find(pid);
|
||||
if (p != nsj->pids.end()) {
|
||||
remote_txt = p->second.remote_txt;
|
||||
}
|
||||
|
||||
if (si.si_code == CLD_EXITED) {
|
||||
nsj->exit_status = si.si_status;
|
||||
LOG_I("pid=%d (%s) exited with status: %d, (PIDs left: %d)", pid,
|
||||
remote_txt.c_str(), si.si_status, countProc(nsj) - 1);
|
||||
removeProc(nsj, pid);
|
||||
return si.si_status;
|
||||
}
|
||||
if (si.si_code == CLD_KILLED || si.si_code == CLD_DUMPED) {
|
||||
nsj->exit_status = 128 + si.si_status;
|
||||
LOG_I("pid=%d (%s) terminated with signal: %s (%d), (PIDs left: %d)", pid,
|
||||
remote_txt.c_str(), util::sigName(si.si_status).c_str(), si.si_status,
|
||||
countProc(nsj) - 1);
|
||||
removeProc(nsj, pid);
|
||||
return 128 + si.si_status;
|
||||
}
|
||||
|
||||
LOG_W("pid=%d exited with unexpected si_code=%d, cleaning up", pid, si.si_code);
|
||||
nsj->exit_status = 1;
|
||||
removeProc(nsj, pid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int reapProc(nsj_t* nsj) {
|
||||
uint64_t checkTimeouts(
|
||||
nsj_t* nsj, pid_t target_pid, time_t start_time, const std::string& remote_txt, int pidfd) {
|
||||
if (nsj->njc.time_limit() == 0) {
|
||||
return -1; /* no timeout */
|
||||
}
|
||||
|
||||
time_t now = time(nullptr);
|
||||
|
||||
time_t diff = now - start_time;
|
||||
if ((uint64_t)diff >= nsj->njc.time_limit()) {
|
||||
LOG_I("pid=%d run time >= time limit (%ld >= %" PRIu64 ") (%s). Killing it",
|
||||
target_pid, (long)diff, (uint64_t)nsj->njc.time_limit(), remote_txt.c_str());
|
||||
|
||||
if (pidfd >= 0) {
|
||||
util::syscall(__NR_pidfd_send_signal, pidfd, SIGCONT, 0, 0);
|
||||
util::syscall(__NR_pidfd_send_signal, pidfd, SIGKILL, 0, 0);
|
||||
} else {
|
||||
kill(target_pid, SIGCONT);
|
||||
kill(target_pid, SIGKILL);
|
||||
}
|
||||
return -1; /* Wait for pidfd to trigger */
|
||||
}
|
||||
uint64_t remaining = nsj->njc.time_limit() - diff;
|
||||
return remaining * 1000; /* in milliseconds */
|
||||
}
|
||||
|
||||
int reapAll(nsj_t* nsj) {
|
||||
int rv = 0;
|
||||
siginfo_t si;
|
||||
|
||||
for (;;) {
|
||||
si.si_pid = 0;
|
||||
if (waitid(P_ALL, 0, &si, WNOHANG | WNOWAIT | WEXITED) == -1) {
|
||||
if (TEMP_FAILURE_RETRY(waitid(P_ALL, 0, &si, WNOHANG | WNOWAIT | WEXITED)) == -1) {
|
||||
break;
|
||||
}
|
||||
if (si.si_pid == 0) {
|
||||
break;
|
||||
}
|
||||
if (si.si_code == CLD_KILLED && si.si_status == SIGSYS) {
|
||||
seccompViolation(nsj, &si);
|
||||
}
|
||||
rv = reapProc(nsj, si.si_pid);
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
for (const auto& p : nsj->pids) {
|
||||
if (nsj->njc.time_limit() == 0) {
|
||||
continue;
|
||||
}
|
||||
pid_t pid = p.first;
|
||||
time_t diff = now - p.second.start;
|
||||
if ((uint64_t)diff >= nsj->njc.time_limit()) {
|
||||
LOG_I("pid=%d run time >= time limit (%ld >= %" PRIu64 ") (%s). Killing it",
|
||||
pid, (long)diff, (uint64_t)nsj->njc.time_limit(),
|
||||
p.second.remote_txt.c_str());
|
||||
/*
|
||||
* Probably a kernel bug - some processes cannot be killed with KILL if
|
||||
* they're namespaced, and in a stopped state
|
||||
*/
|
||||
kill(pid, SIGCONT);
|
||||
LOG_D("Sent SIGCONT to pid=%d", pid);
|
||||
kill(pid, SIGKILL);
|
||||
LOG_D("Sent SIGKILL to pid=%d", pid);
|
||||
}
|
||||
rv = reapProc(nsj, si.si_pid);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
void killAndReapAll(nsj_t* nsj, int signal) {
|
||||
while (!nsj->pids.empty()) {
|
||||
pid_t pid = nsj->pids.begin()->first;
|
||||
if (nsj->pids.begin()->second.pasta_pid > 0) {
|
||||
kill(nsj->pids.begin()->second.pasta_pid, SIGKILL);
|
||||
void killAll(nsj_t* nsj, int signal) {
|
||||
for (auto& pid_entry : nsj->pids) {
|
||||
pid_t pid = pid_entry.first;
|
||||
auto& p = pid_entry.second;
|
||||
|
||||
if (p.pasta_pid > 0) {
|
||||
if (kill(p.pasta_pid, SIGKILL) == -1 && errno != ESRCH) {
|
||||
PLOG_W("kill(pasta_pid)");
|
||||
}
|
||||
}
|
||||
if (kill(pid, signal) == 0) {
|
||||
reapProc(nsj, pid, true);
|
||||
|
||||
if (p.pidfd >= 0) {
|
||||
if (util::syscall(__NR_pidfd_send_signal, p.pidfd, signal, 0, 0) == -1 &&
|
||||
errno != ESRCH) {
|
||||
PLOG_W("pidfd_send_signal(pidfd=%d, sig=%d)", p.pidfd, signal);
|
||||
}
|
||||
} else {
|
||||
removeProc(nsj, pid);
|
||||
kill(pid, signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool initParent(nsj_t* nsj, pid_t pid, int pipefd) {
|
||||
if (!net::initParent(nsj, pid, pipefd)) {
|
||||
LOG_E("Couldn't initialize net namespace for pid=%d", pid);
|
||||
static bool initParent(nsj_t* nsj, pid_t pid, int ipc_fd) {
|
||||
if (!net::initParent(nsj, pid, ipc_fd)) {
|
||||
LOG_W("Couldn't initialize net namespace for pid=%d", pid);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nsj->njc.use_cgroupv2()) {
|
||||
if (!cgroup2::initNsFromParent(nsj, pid)) {
|
||||
LOG_E("Couldn't initialize cgroup 2 user namespace for pid=%d", pid);
|
||||
exit(0xff);
|
||||
return false;
|
||||
}
|
||||
} else if (!cgroup::initNsFromParent(nsj, pid)) {
|
||||
LOG_E("Couldn't initialize cgroup user namespace for pid=%d", pid);
|
||||
exit(0xff);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user::initNsFromParent(nsj, pid)) {
|
||||
LOG_E("Couldn't initialize user namespace for pid=%d", pid);
|
||||
LOG_W("Couldn't initialize user namespace for pid=%d", pid);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!util::writeToFd(pipefd, &kSubprocDoneChar, sizeof(kSubprocDoneChar))) {
|
||||
LOG_E("Couldn't signal the new process via a socketpair");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nsj->njc.seccomp_unotify()) {
|
||||
int unotif_fd = util::recvFd(pipefd);
|
||||
if (unotif_fd == -1) {
|
||||
LOG_E("Failed to receive unotif_fd from child");
|
||||
return false;
|
||||
}
|
||||
LOG_D("Received unotif_fd=%d from child", unotif_fd);
|
||||
if (!unotify::start(nsj, unotif_fd)) {
|
||||
LOG_E("Failed to start unotify thread");
|
||||
close(unotif_fd);
|
||||
return false;
|
||||
}
|
||||
if (!util::writeToFd(pipefd, &kSubprocDoneChar, sizeof(kSubprocDoneChar))) {
|
||||
LOG_E("Couldn't signal the new process via a socketpair");
|
||||
if (ipc_fd != -1) {
|
||||
if (!util::sendMsg(ipc_fd, monitor::MSG_TAG_READY_H2J)) {
|
||||
LOG_W("Couldn't signal the new process via a socketpair");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* unotify setup and second signaling are handled by the monitor thread */
|
||||
return true;
|
||||
}
|
||||
|
||||
pid_t runChild(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err) {
|
||||
pid_t runChild(
|
||||
nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err, int* pidfd_out, int* ipc_fd_out) {
|
||||
if (!net::limitConns(nsj, netfd)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -537,18 +513,21 @@ pid_t runChild(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err) {
|
||||
|
||||
int sv[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv) == -1) {
|
||||
PLOG_E("socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC) failed");
|
||||
PLOG_W("socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC) failed");
|
||||
return -1;
|
||||
}
|
||||
int child_fd = sv[0];
|
||||
int parent_fd = sv[1];
|
||||
|
||||
pid_t pid = cloneProc(flags, SIGCHLD);
|
||||
pid_t pid = cloneProc(flags, SIGCHLD, pidfd_out);
|
||||
if (pid == 0) {
|
||||
close(parent_fd);
|
||||
|
||||
newProc(nsj, netfd, fd_in, fd_out, fd_err, child_fd);
|
||||
util::writeToFd(child_fd, &kSubprocErrorChar, sizeof(kSubprocErrorChar));
|
||||
LOG_F("Launching child process failed");
|
||||
util::sendMsg(child_fd, monitor::MSG_TAG_ERROR);
|
||||
LOG_E("Launching child process failed");
|
||||
pause();
|
||||
_exit(0xff);
|
||||
}
|
||||
close(child_fd);
|
||||
if (pid == -1) {
|
||||
@@ -558,46 +537,33 @@ pid_t runChild(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err) {
|
||||
errno = saved_errno;
|
||||
return pid;
|
||||
}
|
||||
addProc(nsj, pid, netfd);
|
||||
addProc(nsj, pid, netfd, *pidfd_out);
|
||||
|
||||
if (!initParent(nsj, pid, parent_fd)) {
|
||||
close(parent_fd);
|
||||
LOG_W("initParent failed, killing child pid=%d", pid);
|
||||
if (*pidfd_out >= 0) {
|
||||
util::syscall(__NR_pidfd_send_signal, *pidfd_out, SIGKILL, 0, 0);
|
||||
} else {
|
||||
kill(pid, SIGKILL);
|
||||
}
|
||||
reapProc(nsj, pid, true /* should_wait */);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char rcvChar;
|
||||
while (util::readFromFd(parent_fd, &rcvChar, sizeof(rcvChar)) == sizeof(rcvChar)) {
|
||||
if (rcvChar == kSubprocErrorChar) {
|
||||
LOG_W("Received error message from the child process before it has been "
|
||||
"executed");
|
||||
close(parent_fd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
close(parent_fd);
|
||||
*ipc_fd_out = parent_fd;
|
||||
return pid;
|
||||
}
|
||||
|
||||
/*
|
||||
* Will be used inside the child process only, so it's safe to have it in BSS.
|
||||
* Some CPU archs (e.g. aarch64) must have it aligned. Size: 128 KiB (/2)
|
||||
* Creates a new process via clone3(2) with CLONE_PIDFD.
|
||||
*
|
||||
* clone3() is mandatory - we require pidfd for process lifecycle management.
|
||||
*
|
||||
* Returns child pid in the parent, 0 in the child, -1 on error.
|
||||
* On success, *pidfd receives a file descriptor referring to the child.
|
||||
*/
|
||||
static uint8_t cloneStack[128 * 1024] __attribute__((aligned(__BIGGEST_ALIGNMENT__)));
|
||||
/* Cannot be on the stack, as the child's stack pointer will change after clone() */
|
||||
static __thread jmp_buf env;
|
||||
|
||||
[[noreturn]] static int cloneFunc([[maybe_unused]] void* arg) {
|
||||
longjmp(env, 1);
|
||||
LOG_F("Execution past longjmp");
|
||||
}
|
||||
|
||||
/*
|
||||
* Avoid problems with caching of PID/TID in glibc - when using syscall(__NR_clone) glibc doesn't
|
||||
* update the internal PID/TID caches, what can lead to invalid values being returned by getpid()
|
||||
* or incorrect PID/TIDs used in raise()/abort() functions
|
||||
*/
|
||||
pid_t cloneProc(uint64_t flags, int exit_signal) {
|
||||
pid_t cloneProc(uint64_t flags, int exit_signal, int* pidfd) {
|
||||
exit_signal &= CSIGNAL;
|
||||
|
||||
if (flags & CLONE_VM) {
|
||||
@@ -607,50 +573,39 @@ pid_t cloneProc(uint64_t flags, int exit_signal) {
|
||||
}
|
||||
|
||||
if (flags & CLONE_NEWTIME) {
|
||||
LOG_W("CLONE_NEWTIME reuqested, but it's only supported with the unshare() mode "
|
||||
LOG_W("CLONE_NEWTIME requested, but it's only supported with the unshare() mode "
|
||||
"(-Me)");
|
||||
}
|
||||
|
||||
#if defined(__NR_clone3)
|
||||
struct clone_args ca = {};
|
||||
ca.exit_signal = (uint64_t)exit_signal;
|
||||
ca.flags = flags | CLONE_PIDFD | CLONE_CLEAR_SIGHAND;
|
||||
ca.pidfd = (uint64_t)pidfd;
|
||||
|
||||
ca.flags = flags | CLONE_CLEAR_SIGHAND;
|
||||
pid_t ret = util::syscall(__NR_clone3, (uintptr_t)&ca, sizeof(ca));
|
||||
if (ret != -1) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Now try without CLONE_CLEAR_SIGHAND as it's supported since Linux 5.5, while clone3
|
||||
* appeared in Linux 5.3
|
||||
*/
|
||||
ca.flags = flags;
|
||||
ret = util::syscall(__NR_clone3, (uintptr_t)&ca, sizeof(ca));
|
||||
if (ret != -1 || errno != ENOSYS) {
|
||||
return ret;
|
||||
}
|
||||
#endif /* defined(__NR_clone3) */
|
||||
|
||||
if (flags & CLONE_NEWTIME) {
|
||||
LOG_E("CLONE_NEWTIME was requested but clone3() is not supported");
|
||||
errno = 0;
|
||||
if (ret == -1) {
|
||||
PLOG_W("clone3(flags=%s|CLONE_PIDFD) failed", cloneFlagsToStr(flags).c_str());
|
||||
return -1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (setjmp(env) == 0) {
|
||||
LOG_D("Cloning process with flags:%s", cloneFlagsToStr(flags).c_str());
|
||||
/*
|
||||
* Avoid the problem of the stack growing up/down under different CPU architectures,
|
||||
* by using middle of the static stack buffer (which is temporary, and used only
|
||||
* inside of the cloneFunc()
|
||||
*/
|
||||
void* stack = &cloneStack[sizeof(cloneStack) / 2];
|
||||
/* Parent */
|
||||
return clone(cloneFunc, stack, (int)flags | exit_signal, NULL, NULL, NULL);
|
||||
/*
|
||||
* Lightweight clone3 wrapper for internal helpers (mnt, pid) that need
|
||||
* specific flags (e.g. CLONE_FS) but no pidfd tracking.
|
||||
*/
|
||||
pid_t cloneProcNoPidfd(uint64_t flags, int exit_signal) {
|
||||
exit_signal &= CSIGNAL;
|
||||
|
||||
struct clone_args ca = {};
|
||||
ca.exit_signal = (uint64_t)exit_signal;
|
||||
ca.flags = flags;
|
||||
|
||||
pid_t ret = util::syscall(__NR_clone3, (uintptr_t)&ca, sizeof(ca));
|
||||
if (ret == -1) {
|
||||
PLOG_W("clone3(flags=%s) failed", cloneFlagsToStr(flags).c_str());
|
||||
}
|
||||
/* Child */
|
||||
return 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int systemExe(const std::vector<std::string>& args, char** env) {
|
||||
@@ -668,9 +623,8 @@ int systemExe(const std::vector<std::string>& args, char** env) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
pid_t pid = cloneProcNoPidfd(0, SIGCHLD);
|
||||
if (pid == -1) {
|
||||
PLOG_W("fork()");
|
||||
close(sv[0]);
|
||||
close(sv[1]);
|
||||
return -1;
|
||||
@@ -681,7 +635,7 @@ int systemExe(const std::vector<std::string>& args, char** env) {
|
||||
execve(argv[0], (char* const*)argv.data(), (char* const*)env);
|
||||
PLOG_W("execve('%s')", argv[0]);
|
||||
util::writeToFd(sv[1], "A", 1);
|
||||
exit(0);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
close(sv[1]);
|
||||
@@ -694,12 +648,9 @@ int systemExe(const std::vector<std::string>& args, char** env) {
|
||||
|
||||
for (;;) {
|
||||
int status;
|
||||
int ret = wait4(pid, &status, __WALL, NULL);
|
||||
if (ret == -1 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
int ret = TEMP_FAILURE_RETRY(waitpid(pid, &status, __WALL));
|
||||
if (ret == -1) {
|
||||
PLOG_W("wait4(pid=%d)", pid);
|
||||
PLOG_W("waitpid(pid=%d)", pid);
|
||||
return -1;
|
||||
}
|
||||
if (WIFEXITED(status)) {
|
||||
|
||||
17
subproc.h
17
subproc.h
@@ -29,20 +29,25 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "monitor.h"
|
||||
#include "nsjail.h"
|
||||
|
||||
namespace subproc {
|
||||
constexpr char kSubprocDoneChar = 'D';
|
||||
|
||||
/* 0 - network connection limit reached, -1 - error */
|
||||
pid_t runChild(nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err);
|
||||
pid_t runChild(
|
||||
nsj_t* nsj, int netfd, int fd_in, int fd_out, int fd_err, int* pidfd_out, int* ipc_fd_out);
|
||||
int countProc(nsj_t* nsj);
|
||||
void displayProc(nsj_t* nsj);
|
||||
void killAndReapAll(nsj_t* nsj, int signal);
|
||||
void killAll(nsj_t* nsj, int signal);
|
||||
/* Returns the exit code of the first failing subprocess, or 0 if none fail */
|
||||
int reapProc(nsj_t* nsj);
|
||||
int reapAll(nsj_t* nsj);
|
||||
int reapProc(nsj_t* nsj, pid_t pid, bool should_wait = false);
|
||||
uint64_t checkTimeouts(
|
||||
nsj_t* nsj, pid_t target_pid, time_t start_time, const std::string& remote_txt, int pidfd);
|
||||
int systemExe(const std::vector<std::string>& args, char** env);
|
||||
pid_t cloneProc(uint64_t flags, int exit_signal);
|
||||
pid_t cloneProc(uint64_t flags, int exit_signal, int* pidfd);
|
||||
|
||||
pid_t cloneProcNoPidfd(uint64_t flags, int exit_signal);
|
||||
|
||||
} // namespace subproc
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "unotify/unotify.pb.h"
|
||||
|
||||
@@ -40,20 +39,21 @@ struct ResourceRecord {
|
||||
|
||||
/* Lexicographical comparison to support std::map usage in stats.cc */
|
||||
bool operator<(const ResourceRecord& o) const {
|
||||
return std::tie(has_path1, path1, has_path2, path2, has_net, net_type, net_endpoint, has_net_port, net_port, has_net_path, net_path) <
|
||||
std::tie(o.has_path1, o.path1, o.has_path2, o.path2, o.has_net, o.net_type, o.net_endpoint, o.has_net_port, o.net_port, o.has_net_path, o.net_path);
|
||||
return std::tie(has_path1, path1, has_path2, path2, has_net, net_type, net_endpoint,
|
||||
has_net_port, net_port, has_net_path, net_path) <
|
||||
std::tie(o.has_path1, o.path1, o.has_path2, o.path2, o.has_net, o.net_type,
|
||||
o.net_endpoint, o.has_net_port, o.net_port, o.has_net_path, o.net_path);
|
||||
}
|
||||
};
|
||||
|
||||
struct SyscallRecord {
|
||||
std::string name;
|
||||
std::vector<std::string> args;
|
||||
std::string args_str;
|
||||
ResourceRecord res;
|
||||
|
||||
/* Lexicographical comparison to support std::map usage in stats.cc */
|
||||
bool operator<(const SyscallRecord& other) const {
|
||||
return std::tie(name, args, res) <
|
||||
std::tie(other.name, other.args, other.res);
|
||||
return std::tie(name, args_str, res) < std::tie(other.name, other.args_str, other.res);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
200
unotify/stats.cc
200
unotify/stats.cc
@@ -4,7 +4,6 @@
|
||||
#include <google/protobuf/text_format.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
#include "logs.h"
|
||||
@@ -13,13 +12,31 @@
|
||||
namespace unotify {
|
||||
|
||||
static std::mutex stats_mu;
|
||||
/* Aggregates syscall statistics on the fly.
|
||||
* Uses std::map which requires SyscallRecord to have operator<. */
|
||||
static std::map<SyscallRecord, size_t> stats;
|
||||
|
||||
struct StatEntry {
|
||||
SyscallRecord rec;
|
||||
size_t count;
|
||||
};
|
||||
|
||||
constexpr size_t MAX_STATS = 1024;
|
||||
static StatEntry stats_array[MAX_STATS];
|
||||
static size_t stats_count = 0;
|
||||
|
||||
void addStat(const SyscallRecord& rec) {
|
||||
std::lock_guard<std::mutex> lock(stats_mu);
|
||||
stats[rec]++;
|
||||
for (size_t i = 0; i < stats_count; ++i) {
|
||||
if (!(stats_array[i].rec < rec) && !(rec < stats_array[i].rec)) {
|
||||
stats_array[i].count++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (stats_count < MAX_STATS) {
|
||||
stats_array[stats_count].rec = rec;
|
||||
stats_array[stats_count].count = 1;
|
||||
stats_count++;
|
||||
} else {
|
||||
LOG_W("Max stats reached, dropping record");
|
||||
}
|
||||
}
|
||||
|
||||
static void fillPathInfoPb(Stat_Path* pb, const PathInfoRecord& rec) {
|
||||
@@ -61,56 +78,150 @@ struct NetInfoRecord {
|
||||
|
||||
struct SyscallKey {
|
||||
std::string name;
|
||||
std::vector<std::string> args;
|
||||
std::string args_str;
|
||||
bool operator<(const SyscallKey& o) const {
|
||||
if (name != o.name) return name < o.name;
|
||||
return args < o.args;
|
||||
return args_str < o.args_str;
|
||||
}
|
||||
};
|
||||
|
||||
constexpr size_t MAX_SYSCALLS_PER_RESOURCE = 64;
|
||||
|
||||
struct SyscallKeyEntry {
|
||||
SyscallKey key;
|
||||
uint64_t count;
|
||||
};
|
||||
|
||||
struct FsStats {
|
||||
uint64_t count = 0;
|
||||
std::map<SyscallKey, uint64_t> syscalls;
|
||||
SyscallKeyEntry syscalls[MAX_SYSCALLS_PER_RESOURCE];
|
||||
size_t syscalls_count = 0;
|
||||
};
|
||||
|
||||
struct NetStats {
|
||||
uint64_t count = 0;
|
||||
std::map<SyscallKey, uint64_t> syscalls;
|
||||
SyscallKeyEntry syscalls[MAX_SYSCALLS_PER_RESOURCE];
|
||||
size_t syscalls_count = 0;
|
||||
};
|
||||
|
||||
struct FsStatsEntry {
|
||||
PathInfoRecord path_rec;
|
||||
FsStats stats;
|
||||
};
|
||||
|
||||
struct NetStatsEntry {
|
||||
NetInfoRecord net_rec;
|
||||
NetStats stats;
|
||||
};
|
||||
|
||||
constexpr size_t MAX_FS_STATS = 256;
|
||||
constexpr size_t MAX_NET_STATS = 256;
|
||||
|
||||
static FsStats* find_or_insert_fs(FsStatsEntry* array, size_t* count, const PathInfoRecord& key) {
|
||||
for (size_t i = 0; i < *count; ++i) {
|
||||
if (!(array[i].path_rec < key) && !(key < array[i].path_rec)) {
|
||||
return &array[i].stats;
|
||||
}
|
||||
}
|
||||
if (*count < MAX_FS_STATS) {
|
||||
array[*count].path_rec = key;
|
||||
FsStats* stats = &array[*count].stats;
|
||||
stats->count = 0;
|
||||
stats->syscalls_count = 0;
|
||||
(*count)++;
|
||||
return stats;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static NetStats* find_or_insert_net(NetStatsEntry* array, size_t* count, const NetInfoRecord& key) {
|
||||
for (size_t i = 0; i < *count; ++i) {
|
||||
if (!(array[i].net_rec < key) && !(key < array[i].net_rec)) {
|
||||
return &array[i].stats;
|
||||
}
|
||||
}
|
||||
if (*count < MAX_NET_STATS) {
|
||||
array[*count].net_rec = key;
|
||||
NetStats* stats = &array[*count].stats;
|
||||
stats->count = 0;
|
||||
stats->syscalls_count = 0;
|
||||
(*count)++;
|
||||
return stats;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static SyscallKeyEntry* find_or_insert_syscall(
|
||||
SyscallKeyEntry* array, size_t* count, const SyscallKey& key) {
|
||||
for (size_t i = 0; i < *count; ++i) {
|
||||
if (!(array[i].key < key) && !(key < array[i].key)) {
|
||||
return &array[i];
|
||||
}
|
||||
}
|
||||
if (*count < MAX_SYSCALLS_PER_RESOURCE) {
|
||||
array[*count].key = key;
|
||||
array[*count].count = 0;
|
||||
SyscallKeyEntry* entry = &array[*count];
|
||||
(*count)++;
|
||||
return entry;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void printStats(nsj_t* nsj) {
|
||||
if (!nsj->njc.seccomp_unotify()) {
|
||||
return;
|
||||
}
|
||||
if (stats.empty()) {
|
||||
return; // Do not emit if empty
|
||||
}
|
||||
|
||||
std::map<PathInfoRecord, FsStats> fs_stats;
|
||||
std::map<NetInfoRecord, NetStats> net_stats;
|
||||
static thread_local FsStatsEntry fs_stats_array[MAX_FS_STATS];
|
||||
size_t fs_stats_count = 0;
|
||||
|
||||
static thread_local NetStatsEntry net_stats_array[MAX_NET_STATS];
|
||||
size_t net_stats_count = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stats_mu);
|
||||
for (const auto& [rec, count] : stats) {
|
||||
SyscallKey sys_key{rec.name, rec.args};
|
||||
if (stats_count == 0) {
|
||||
return; // Do not emit if empty
|
||||
}
|
||||
for (size_t i = 0; i < stats_count; ++i) {
|
||||
const auto& rec = stats_array[i].rec;
|
||||
size_t count = stats_array[i].count;
|
||||
SyscallKey sys_key{rec.name, rec.args_str};
|
||||
|
||||
if (rec.res.has_path1) {
|
||||
SyscallKey p1_key = sys_key;
|
||||
if (!rec.res.path1.mode_extra.empty()) {
|
||||
p1_key.args.push_back(
|
||||
"mode_extra=" + rec.res.path1.mode_extra);
|
||||
p1_key.args_str +=
|
||||
"mode_extra=" + rec.res.path1.mode_extra + " ";
|
||||
}
|
||||
FsStats* fs_stat = find_or_insert_fs(
|
||||
fs_stats_array, &fs_stats_count, rec.res.path1);
|
||||
if (fs_stat) {
|
||||
fs_stat->count += count;
|
||||
SyscallKeyEntry* sys_entry = find_or_insert_syscall(
|
||||
fs_stat->syscalls, &fs_stat->syscalls_count, p1_key);
|
||||
if (sys_entry) {
|
||||
sys_entry->count += count;
|
||||
}
|
||||
}
|
||||
fs_stats[rec.res.path1].count += count;
|
||||
fs_stats[rec.res.path1].syscalls[p1_key] += count;
|
||||
}
|
||||
if (rec.res.has_path2) {
|
||||
SyscallKey p2_key = sys_key;
|
||||
if (!rec.res.path2.mode_extra.empty()) {
|
||||
p2_key.args.push_back(
|
||||
"mode_extra=" + rec.res.path2.mode_extra);
|
||||
p2_key.args_str +=
|
||||
"mode_extra=" + rec.res.path2.mode_extra + " ";
|
||||
}
|
||||
FsStats* fs_stat = find_or_insert_fs(
|
||||
fs_stats_array, &fs_stats_count, rec.res.path2);
|
||||
if (fs_stat) {
|
||||
fs_stat->count += count;
|
||||
SyscallKeyEntry* sys_entry = find_or_insert_syscall(
|
||||
fs_stat->syscalls, &fs_stat->syscalls_count, p2_key);
|
||||
if (sys_entry) {
|
||||
sys_entry->count += count;
|
||||
}
|
||||
}
|
||||
fs_stats[rec.res.path2].count += count;
|
||||
fs_stats[rec.res.path2].syscalls[p2_key] += count;
|
||||
}
|
||||
if (rec.res.has_net) {
|
||||
NetInfoRecord net_rec;
|
||||
@@ -121,33 +232,42 @@ void printStats(nsj_t* nsj) {
|
||||
net_rec.has_path = rec.res.has_net_path;
|
||||
net_rec.path = rec.res.net_path;
|
||||
|
||||
net_stats[net_rec].count += count;
|
||||
net_stats[net_rec].syscalls[sys_key] += count;
|
||||
NetStats* net_stat =
|
||||
find_or_insert_net(net_stats_array, &net_stats_count, net_rec);
|
||||
if (net_stat) {
|
||||
net_stat->count += count;
|
||||
SyscallKeyEntry* sys_entry = find_or_insert_syscall(
|
||||
net_stat->syscalls, &net_stat->syscalls_count, sys_key);
|
||||
if (sys_entry) {
|
||||
sys_entry->count += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stat report_pb;
|
||||
|
||||
for (const auto& [path_rec, fs_stat] : fs_stats) {
|
||||
for (size_t i = 0; i < fs_stats_count; ++i) {
|
||||
const auto& path_rec = fs_stats_array[i].path_rec;
|
||||
const auto& fs_stat = fs_stats_array[i].stats;
|
||||
Stat_Path* fs_pb = report_pb.add_fs_access();
|
||||
fs_pb->set_count(fs_stat.count);
|
||||
fillPathInfoPb(fs_pb, path_rec);
|
||||
|
||||
for (const auto& [sys_key, sys_count] : fs_stat.syscalls) {
|
||||
for (size_t j = 0; j < fs_stat.syscalls_count; ++j) {
|
||||
Stat_Syscall* sys_pb = fs_pb->add_syscall();
|
||||
sys_pb->set_name(sys_key.name);
|
||||
sys_pb->set_count(sys_count);
|
||||
for (const auto& arg : sys_key.args) {
|
||||
sys_pb->add_args(arg);
|
||||
}
|
||||
sys_pb->set_name(fs_stat.syscalls[j].key.name);
|
||||
sys_pb->set_count(fs_stat.syscalls[j].count);
|
||||
sys_pb->add_args(fs_stat.syscalls[j].key.args_str);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [net_rec, net_stat] : net_stats) {
|
||||
for (size_t i = 0; i < net_stats_count; ++i) {
|
||||
const auto& net_rec = net_stats_array[i].net_rec;
|
||||
const auto& net_stat = net_stats_array[i].stats;
|
||||
Stat_NetResource* net_pb = report_pb.add_net_access();
|
||||
net_pb->set_count(net_stat.count);
|
||||
|
||||
net_pb->set_type(net_rec.type);
|
||||
if (!net_rec.endpoint.empty()) {
|
||||
net_pb->set_endpoint(net_rec.endpoint);
|
||||
@@ -159,13 +279,11 @@ void printStats(nsj_t* nsj) {
|
||||
fillPathInfoPb(net_pb->mutable_socket_path(), net_rec.path);
|
||||
}
|
||||
|
||||
for (const auto& [sys_key, sys_count] : net_stat.syscalls) {
|
||||
for (size_t j = 0; j < net_stat.syscalls_count; ++j) {
|
||||
Stat_Syscall* sys_pb = net_pb->add_syscall();
|
||||
sys_pb->set_name(sys_key.name);
|
||||
sys_pb->set_count(sys_count);
|
||||
for (const auto& arg : sys_key.args) {
|
||||
sys_pb->add_args(arg);
|
||||
}
|
||||
sys_pb->set_name(net_stat.syscalls[j].key.name);
|
||||
sys_pb->set_count(net_stat.syscalls[j].count);
|
||||
sys_pb->add_args(net_stat.syscalls[j].key.args_str);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "logs.h"
|
||||
#include "macros.h"
|
||||
#include "missing_defs.h"
|
||||
#include "unotify/syscall_defs.h"
|
||||
#include "util.h"
|
||||
|
||||
@@ -26,15 +27,18 @@ constexpr size_t kMaxPathLen = 4096;
|
||||
/* Helper functions string/memory reading, path resolution, etc. */
|
||||
|
||||
static std::string getSocketType(pid_t pid, int target_fd) {
|
||||
int pidfd = syscall(__NR_pidfd_open, pid, 0);
|
||||
int pidfd = util::syscall(__NR_pidfd_open, pid, 0);
|
||||
if (pidfd < 0) {
|
||||
return "";
|
||||
}
|
||||
int local_fd = syscall(__NR_pidfd_getfd, pidfd, target_fd, 0);
|
||||
fcntl(pidfd, F_SETFD, FD_CLOEXEC);
|
||||
|
||||
int local_fd = util::syscall(__NR_pidfd_getfd, pidfd, target_fd, 0);
|
||||
close(pidfd);
|
||||
if (local_fd < 0) {
|
||||
return "";
|
||||
}
|
||||
fcntl(local_fd, F_SETFD, FD_CLOEXEC);
|
||||
|
||||
int type = 0, proto = 0;
|
||||
socklen_t len = sizeof(type);
|
||||
@@ -57,7 +61,9 @@ static std::string getSocketType(pid_t pid, int target_fd) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) type_str = "SOCK_TYPE_" + std::to_string(type);
|
||||
if (!found) {
|
||||
type_str = "SOCK_TYPE_" + std::to_string(type);
|
||||
}
|
||||
}
|
||||
len = sizeof(proto);
|
||||
if (getsockopt(local_fd, SOL_SOCKET, SO_PROTOCOL, &proto, &len) == 0) {
|
||||
@@ -80,62 +86,88 @@ static std::string getSocketType(pid_t pid, int target_fd) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found && proto != 0) type_str += " (proto_" + std::to_string(proto) + ")";
|
||||
if (!found && proto != 0) {
|
||||
type_str += " (proto_" + std::to_string(proto) + ")";
|
||||
}
|
||||
}
|
||||
close(local_fd);
|
||||
return type_str;
|
||||
}
|
||||
|
||||
static std::string readStringFromMem(pid_t pid, uint64_t addr) {
|
||||
if (addr == 0) return "NULL";
|
||||
char buf[kMaxPathLen];
|
||||
struct iovec local = {buf, sizeof(buf) - 1};
|
||||
struct iovec remote = {(void*)addr, sizeof(buf) - 1};
|
||||
if (addr == 0) {
|
||||
return "NULL";
|
||||
}
|
||||
/* Heap-allocate to avoid stack pressure, but avoid static buffer reuse
|
||||
* in case it causes subtle issues with event interleaving */
|
||||
std::string buf(kMaxPathLen - 1, '\0');
|
||||
struct iovec local = {.iov_base = buf.data(), .iov_len = buf.size()};
|
||||
struct iovec remote = {.iov_base = (void*)addr, .iov_len = buf.size()};
|
||||
|
||||
ssize_t ret = process_vm_readv(pid, &local, 1, &remote, 1, 0);
|
||||
if (ret <= 0) {
|
||||
return "<invalid_ptr>";
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
return std::string(buf);
|
||||
buf.resize(strnlen(buf.data(), ret));
|
||||
return buf;
|
||||
}
|
||||
|
||||
static std::vector<std::string> readStringArrayFromMem(pid_t pid, uint64_t addr, bool is_32bit) {
|
||||
std::vector<std::string> arr;
|
||||
if (addr == 0) return arr;
|
||||
static void appendStringArrayFromMem(
|
||||
pid_t pid, uint64_t addr, bool is_32bit, const char* prefix, std::string& out) {
|
||||
if (addr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Heap-allocate pointer arrays to avoid stack pressure */
|
||||
if (is_32bit) {
|
||||
uint32_t ptrs[kMaxArgs];
|
||||
struct iovec local = {ptrs, sizeof(ptrs)};
|
||||
struct iovec remote = {(void*)addr, sizeof(ptrs)};
|
||||
std::vector<uint32_t> ptrs(kMaxArgs);
|
||||
struct iovec local = {
|
||||
.iov_base = ptrs.data(), .iov_len = ptrs.size() * sizeof(uint32_t)};
|
||||
struct iovec remote = {
|
||||
.iov_base = (void*)addr, .iov_len = ptrs.size() * sizeof(uint32_t)};
|
||||
|
||||
ssize_t ret = process_vm_readv(pid, &local, 1, &remote, 1, 0);
|
||||
if (ret <= 0) return arr;
|
||||
if (ret <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int num_ptrs = ret / sizeof(uint32_t);
|
||||
for (int i = 0; i < num_ptrs; i++) {
|
||||
if (ptrs[i] == 0) break;
|
||||
arr.push_back(readStringFromMem(pid, ptrs[i]));
|
||||
if (ptrs[i] == 0) {
|
||||
break;
|
||||
}
|
||||
out += prefix;
|
||||
out +=
|
||||
"[" + std::to_string(i) + "]=" + readStringFromMem(pid, ptrs[i]) + " ";
|
||||
}
|
||||
} else {
|
||||
uint64_t ptrs[kMaxArgs];
|
||||
struct iovec local = {ptrs, sizeof(ptrs)};
|
||||
struct iovec remote = {(void*)addr, sizeof(ptrs)};
|
||||
std::vector<uint64_t> ptrs(kMaxArgs);
|
||||
struct iovec local = {
|
||||
.iov_base = ptrs.data(), .iov_len = ptrs.size() * sizeof(uint64_t)};
|
||||
struct iovec remote = {
|
||||
.iov_base = (void*)addr, .iov_len = ptrs.size() * sizeof(uint64_t)};
|
||||
|
||||
ssize_t ret = process_vm_readv(pid, &local, 1, &remote, 1, 0);
|
||||
if (ret <= 0) return arr;
|
||||
if (ret <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int num_ptrs = ret / sizeof(uint64_t);
|
||||
for (int i = 0; i < num_ptrs; i++) {
|
||||
if (ptrs[i] == 0) break;
|
||||
arr.push_back(readStringFromMem(pid, ptrs[i]));
|
||||
if (ptrs[i] == 0) {
|
||||
break;
|
||||
}
|
||||
out += prefix;
|
||||
out +=
|
||||
"[" + std::to_string(i) + "]=" + readStringFromMem(pid, ptrs[i]) + " ";
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
static std::string getAbsPath(pid_t pid, int dirfd, const std::string& raw_path) {
|
||||
if (raw_path.empty() || raw_path[0] == '/') return raw_path;
|
||||
if (raw_path.empty() || raw_path[0] == '/') {
|
||||
return raw_path;
|
||||
}
|
||||
|
||||
std::string link_path;
|
||||
if (dirfd == AT_FDCWD || dirfd == -100) {
|
||||
@@ -144,26 +176,31 @@ static std::string getAbsPath(pid_t pid, int dirfd, const std::string& raw_path)
|
||||
link_path = "/proc/" + std::to_string(pid) + "/fd/" + std::to_string(dirfd);
|
||||
}
|
||||
|
||||
char buf[PATH_MAX];
|
||||
ssize_t len = readlink(link_path.c_str(), buf, sizeof(buf) - 1);
|
||||
if (len <= 0) return raw_path; // fallback
|
||||
buf[len] = '\0';
|
||||
/* Heap-allocate to avoid stack pressure */
|
||||
std::string buf(PATH_MAX - 1, '\0');
|
||||
ssize_t len = readlink(link_path.c_str(), buf.data(), buf.size());
|
||||
if (len <= 0) {
|
||||
return raw_path; // fallback
|
||||
}
|
||||
|
||||
std::string abs_path = buf;
|
||||
if (abs_path.back() != '/') abs_path += '/';
|
||||
std::string abs_path(buf.data(), len);
|
||||
if (abs_path.back() != '/') {
|
||||
abs_path += '/';
|
||||
}
|
||||
abs_path += raw_path;
|
||||
return abs_path;
|
||||
}
|
||||
|
||||
static void getFileMode(int flags, PathInfoRecord* out) {
|
||||
if ((flags & O_ACCMODE) == O_RDONLY)
|
||||
if ((flags & O_ACCMODE) == O_RDONLY) {
|
||||
out->mode = Stat_Path_Mode_RDONLY;
|
||||
else if ((flags & O_ACCMODE) == O_WRONLY)
|
||||
} else if ((flags & O_ACCMODE) == O_WRONLY) {
|
||||
out->mode = Stat_Path_Mode_WRONLY;
|
||||
else if ((flags & O_ACCMODE) == O_RDWR)
|
||||
} else if ((flags & O_ACCMODE) == O_RDWR) {
|
||||
out->mode = Stat_Path_Mode_RDWR;
|
||||
else
|
||||
} else {
|
||||
out->mode = Stat_Path_Mode_UNSPECIFIED;
|
||||
}
|
||||
|
||||
std::string mode_extra;
|
||||
struct {
|
||||
@@ -200,12 +237,22 @@ static void getFileMode(int flags, PathInfoRecord* out) {
|
||||
}
|
||||
|
||||
static std::string getAccessMode(int mode) {
|
||||
if (mode == F_OK) return "F_OK";
|
||||
if (mode == F_OK) {
|
||||
return "F_OK";
|
||||
}
|
||||
std::string acc;
|
||||
if (mode & R_OK) acc += "R_OK|";
|
||||
if (mode & W_OK) acc += "W_OK|";
|
||||
if (mode & X_OK) acc += "X_OK|";
|
||||
if (!acc.empty()) acc.pop_back();
|
||||
if (mode & R_OK) {
|
||||
acc += "R_OK|";
|
||||
}
|
||||
if (mode & W_OK) {
|
||||
acc += "W_OK|";
|
||||
}
|
||||
if (mode & X_OK) {
|
||||
acc += "X_OK|";
|
||||
}
|
||||
if (!acc.empty()) {
|
||||
acc.pop_back();
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
@@ -214,13 +261,27 @@ static Stat_Path_Type getStatInfo(const std::string& path) {
|
||||
if (lstat(path.c_str(), &st) == -1) {
|
||||
return Stat_Path_Type_NONEXISTENT;
|
||||
}
|
||||
if (S_ISREG(st.st_mode)) return Stat_Path_Type_REGULAR;
|
||||
if (S_ISDIR(st.st_mode)) return Stat_Path_Type_DIR;
|
||||
if (S_ISCHR(st.st_mode)) return Stat_Path_Type_CHR;
|
||||
if (S_ISBLK(st.st_mode)) return Stat_Path_Type_BLK;
|
||||
if (S_ISFIFO(st.st_mode)) return Stat_Path_Type_FIFO;
|
||||
if (S_ISLNK(st.st_mode)) return Stat_Path_Type_LINK;
|
||||
if (S_ISSOCK(st.st_mode)) return Stat_Path_Type_SOCK;
|
||||
if (S_ISREG(st.st_mode)) {
|
||||
return Stat_Path_Type_REGULAR;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
return Stat_Path_Type_DIR;
|
||||
}
|
||||
if (S_ISCHR(st.st_mode)) {
|
||||
return Stat_Path_Type_CHR;
|
||||
}
|
||||
if (S_ISBLK(st.st_mode)) {
|
||||
return Stat_Path_Type_BLK;
|
||||
}
|
||||
if (S_ISFIFO(st.st_mode)) {
|
||||
return Stat_Path_Type_FIFO;
|
||||
}
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
return Stat_Path_Type_LINK;
|
||||
}
|
||||
if (S_ISSOCK(st.st_mode)) {
|
||||
return Stat_Path_Type_SOCK;
|
||||
}
|
||||
return Stat_Path_Type_UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -235,11 +296,13 @@ static void populatePathInfo(pid_t pid, int dirfd, uint64_t addr, PathInfoRecord
|
||||
|
||||
static void parseSockaddr(struct seccomp_notif* req, SyscallRecord* rec, uint64_t addr,
|
||||
socklen_t addrlen, const std::string& socket_type_str) {
|
||||
if (addrlen > sizeof(struct sockaddr_storage) || addr == 0) return;
|
||||
if (addrlen > sizeof(struct sockaddr_storage) || addr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sockaddr_storage ss = {};
|
||||
struct iovec local = {&ss, addrlen};
|
||||
struct iovec remote = {(void*)addr, addrlen};
|
||||
struct iovec local = {.iov_base = &ss, .iov_len = addrlen};
|
||||
struct iovec remote = {.iov_base = (void*)addr, .iov_len = addrlen};
|
||||
ssize_t read_bytes = process_vm_readv(req->pid, &local, 1, &remote, 1, 0);
|
||||
if (read_bytes >= (ssize_t)sizeof(sa_family_t)) {
|
||||
char host[INET6_ADDRSTRLEN] = "unknown";
|
||||
@@ -334,7 +397,9 @@ static std::string getDomainStr(int domain) {
|
||||
NS_VALSTR_STRUCT(AF_VSOCK),
|
||||
};
|
||||
for (const auto& i : domains) {
|
||||
if (domain == i.val) return i.name;
|
||||
if (domain == i.val) {
|
||||
return i.name;
|
||||
}
|
||||
}
|
||||
return std::to_string(domain);
|
||||
}
|
||||
@@ -362,14 +427,22 @@ static std::string getTypeStr(int type) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) res = std::to_string(base_type);
|
||||
if (type & SOCK_CLOEXEC) res += "|SOCK_CLOEXEC";
|
||||
if (type & SOCK_NONBLOCK) res += "|SOCK_NONBLOCK";
|
||||
if (!found) {
|
||||
res = std::to_string(base_type);
|
||||
}
|
||||
if (type & SOCK_CLOEXEC) {
|
||||
res += "|SOCK_CLOEXEC";
|
||||
}
|
||||
if (type & SOCK_NONBLOCK) {
|
||||
res += "|SOCK_NONBLOCK";
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static std::string getProtocolStr(int proto) {
|
||||
if (proto == 0) return "0";
|
||||
if (proto == 0) {
|
||||
return "0";
|
||||
}
|
||||
struct {
|
||||
const int val;
|
||||
const char* const name;
|
||||
@@ -402,7 +475,9 @@ static std::string getProtocolStr(int proto) {
|
||||
NS_VALSTR_STRUCT(IPPROTO_ICMPV6),
|
||||
};
|
||||
for (const auto& i : protos) {
|
||||
if (proto == i.val) return i.name;
|
||||
if (proto == i.val) {
|
||||
return i.name;
|
||||
}
|
||||
}
|
||||
return std::to_string(proto);
|
||||
}
|
||||
@@ -418,10 +493,14 @@ static void decodeSyscallArgs(
|
||||
|
||||
bool is_32bit = false;
|
||||
#ifdef AUDIT_ARCH_I386
|
||||
if (req->data.arch == AUDIT_ARCH_I386) is_32bit = true;
|
||||
if (req->data.arch == AUDIT_ARCH_I386) {
|
||||
is_32bit = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef AUDIT_ARCH_ARM
|
||||
if (req->data.arch == AUDIT_ARCH_ARM) is_32bit = true;
|
||||
if (req->data.arch == AUDIT_ARCH_ARM) {
|
||||
is_32bit = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
@@ -447,9 +526,10 @@ static void decodeSyscallArgs(
|
||||
|
||||
case ArgRole::DIRFD: {
|
||||
int dirfd = (int)arg;
|
||||
rec->args.push_back("dirfd=" + (dirfd == AT_FDCWD || dirfd == -100
|
||||
? std::string("AT_FDCWD")
|
||||
: std::to_string(dirfd)));
|
||||
std::string arg_str =
|
||||
"dirfd=" + (dirfd == AT_FDCWD || dirfd == -100 ? std::string("AT_FDCWD")
|
||||
: std::to_string(dirfd));
|
||||
rec->args_str += arg_str + " ";
|
||||
current_dirfd = dirfd;
|
||||
break;
|
||||
}
|
||||
@@ -463,49 +543,54 @@ static void decodeSyscallArgs(
|
||||
case ArgRole::OCTAL: {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "0%o", (unsigned int)arg);
|
||||
rec->args.push_back("mode=" + std::string(buf));
|
||||
std::string arg_str = "mode=" + std::string(buf);
|
||||
rec->args_str += arg_str + " ";
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::ACCESS:
|
||||
rec->args.push_back("mode=" + getAccessMode((int)arg));
|
||||
case ArgRole::ACCESS: {
|
||||
std::string arg_str = "mode=" + getAccessMode((int)arg);
|
||||
rec->args_str += arg_str + " ";
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::UID:
|
||||
rec->args.push_back("owner=" + std::to_string((int)arg));
|
||||
case ArgRole::UID: {
|
||||
std::string arg_str = "owner=" + std::to_string((int)arg);
|
||||
rec->args_str += arg_str + " ";
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::GID:
|
||||
rec->args.push_back("group=" + std::to_string((int)arg));
|
||||
case ArgRole::GID: {
|
||||
std::string arg_str = "group=" + std::to_string((int)arg);
|
||||
rec->args_str += arg_str + " ";
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::ARGV: {
|
||||
auto argv = readStringArrayFromMem(req->pid, arg, is_32bit);
|
||||
for (size_t j = 0; j < argv.size(); j++) {
|
||||
rec->args.push_back("argv[" + std::to_string(j) + "]=" + argv[j]);
|
||||
}
|
||||
appendStringArrayFromMem(req->pid, arg, is_32bit, "argv", rec->args_str);
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::ENVP: {
|
||||
auto envp = readStringArrayFromMem(req->pid, arg, is_32bit);
|
||||
for (size_t j = 0; j < envp.size(); j++) {
|
||||
rec->args.push_back("envp[" + std::to_string(j) + "]=" + envp[j]);
|
||||
}
|
||||
appendStringArrayFromMem(req->pid, arg, is_32bit, "envp", rec->args_str);
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::FD: {
|
||||
rec->args.push_back("fd=" + std::to_string((int)arg));
|
||||
std::string arg_str = "fd=" + std::to_string((int)arg);
|
||||
rec->args_str += arg_str + " ";
|
||||
last_socket_type = getSocketType(req->pid, (int)arg);
|
||||
if (!last_socket_type.empty()) {
|
||||
rec->args.push_back("type=" + last_socket_type);
|
||||
std::string type_str = "type=" + last_socket_type;
|
||||
rec->args_str += type_str + " ";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::SADDR: {
|
||||
if (arg == 0) break;
|
||||
if (arg == 0) {
|
||||
break;
|
||||
}
|
||||
socklen_t addrlen = (i + 1 < 6) ? (socklen_t)args[i + 1] : 0;
|
||||
parseSockaddr(req, rec, arg, addrlen, last_socket_type);
|
||||
break;
|
||||
@@ -514,21 +599,29 @@ static void decodeSyscallArgs(
|
||||
case ArgRole::ALEN:
|
||||
break; /* consumed by preceding SADDR */
|
||||
|
||||
case ArgRole::IFLAGS:
|
||||
rec->args.push_back("flags=" + std::to_string((int)arg));
|
||||
case ArgRole::IFLAGS: {
|
||||
std::string arg_str = "flags=" + std::to_string((int)arg);
|
||||
rec->args_str += arg_str + " ";
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::DOMAIN:
|
||||
rec->args.push_back("domain=" + getDomainStr((int)arg));
|
||||
case ArgRole::DOMAIN: {
|
||||
std::string arg_str = "domain=" + getDomainStr((int)arg);
|
||||
rec->args_str += arg_str + " ";
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::STYPE:
|
||||
rec->args.push_back("type=" + getTypeStr((int)arg));
|
||||
case ArgRole::STYPE: {
|
||||
std::string arg_str = "type=" + getTypeStr((int)arg);
|
||||
rec->args_str += arg_str + " ";
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::PROTO:
|
||||
rec->args.push_back("protocol=" + getProtocolStr((int)arg));
|
||||
case ArgRole::PROTO: {
|
||||
std::string arg_str = "protocol=" + getProtocolStr((int)arg);
|
||||
rec->args_str += arg_str + " ";
|
||||
break;
|
||||
}
|
||||
|
||||
case ArgRole::OHOW: {
|
||||
struct {
|
||||
@@ -536,16 +629,17 @@ static void decodeSyscallArgs(
|
||||
__u64 mode;
|
||||
__u64 resolve;
|
||||
} how = {};
|
||||
struct iovec local = {&how, sizeof(how)};
|
||||
struct iovec remote = {(void*)arg, sizeof(how)};
|
||||
struct iovec local = {.iov_base = &how, .iov_len = sizeof(how)};
|
||||
struct iovec remote = {.iov_base = (void*)arg, .iov_len = sizeof(how)};
|
||||
if (process_vm_readv(req->pid, &local, 1, &remote, 1, 0) >=
|
||||
(ssize_t)sizeof(how.flags)) {
|
||||
if (last_path) {
|
||||
getFileMode((int)how.flags, last_path);
|
||||
}
|
||||
if (how.resolve != 0) {
|
||||
rec->args.push_back(
|
||||
"resolve=" + std::to_string(how.resolve));
|
||||
std::string arg_str =
|
||||
"resolve=" + std::to_string(how.resolve);
|
||||
rec->args_str += arg_str + " ";
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#ifndef NSJAIL_UNOTIFY_SYSCALL_DEFS_H
|
||||
#define NSJAIL_UNOTIFY_SYSCALL_DEFS_H
|
||||
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#include "../missing_defs.h"
|
||||
|
||||
namespace unotify {
|
||||
|
||||
|
||||
|
||||
enum class SyscallCategory : uint8_t {
|
||||
FS,
|
||||
NET,
|
||||
@@ -24,33 +25,33 @@ enum class SyscallCategory : uint8_t {
|
||||
* last_path - set by PATH, consumed by FLAGS/OHOW
|
||||
*/
|
||||
enum class ArgRole : uint8_t {
|
||||
SKIP, /* Don't decode this argument */
|
||||
PATH, /* String ptr -> populatePathInfo (alternates path1/path2) */
|
||||
DIRFD, /* Directory fd; sets context for next PATH */
|
||||
FLAGS, /* Open flags -> getFileMode on last populated path */
|
||||
OCTAL, /* Permission mode -> "mode=0NNN" */
|
||||
ACCESS, /* Access mode -> "mode=R_OK|W_OK|X_OK" */
|
||||
UID, /* User id -> "owner=N" */
|
||||
GID, /* Group id -> "group=N" */
|
||||
ARGV, /* String array ptr -> "argv[i]=..." */
|
||||
ENVP, /* String array ptr -> "envp[i]=..." */
|
||||
FD, /* Socket fd -> "fd=N" + getSocketType */
|
||||
SADDR, /* sockaddr ptr; next arg must be ALEN */
|
||||
ALEN, /* sockaddr length; consumed by preceding SADDR */
|
||||
IFLAGS, /* Integer flags -> "flags=N" */
|
||||
SKIP, /* Don't decode this argument */
|
||||
PATH, /* String ptr -> populatePathInfo (alternates path1/path2) */
|
||||
DIRFD, /* Directory fd; sets context for next PATH */
|
||||
FLAGS, /* Open flags -> getFileMode on last populated path */
|
||||
OCTAL, /* Permission mode -> "mode=0NNN" */
|
||||
ACCESS, /* Access mode -> "mode=R_OK|W_OK|X_OK" */
|
||||
UID, /* User id -> "owner=N" */
|
||||
GID, /* Group id -> "group=N" */
|
||||
ARGV, /* String array ptr -> "argv[i]=..." */
|
||||
ENVP, /* String array ptr -> "envp[i]=..." */
|
||||
FD, /* Socket fd -> "fd=N" + getSocketType */
|
||||
SADDR, /* sockaddr ptr; next arg must be ALEN */
|
||||
ALEN, /* sockaddr length; consumed by preceding SADDR */
|
||||
IFLAGS, /* Integer flags -> "flags=N" */
|
||||
|
||||
DOMAIN, /* Socket domain -> getDomainStr */
|
||||
STYPE, /* Socket type -> getTypeStr */
|
||||
PROTO, /* Protocol -> getProtocolStr */
|
||||
OHOW, /* struct open_how ptr -> decode flags + resolve */
|
||||
DOMAIN, /* Socket domain -> getDomainStr */
|
||||
STYPE, /* Socket type -> getTypeStr */
|
||||
PROTO, /* Protocol -> getProtocolStr */
|
||||
OHOW, /* struct open_how ptr -> decode flags + resolve */
|
||||
};
|
||||
|
||||
struct SyscallDef {
|
||||
int nr; /* __NR_xxx */
|
||||
const char* kafel_name; /* name for kafel policy ("newstat", "sendmsg") */
|
||||
const char* display_name; /* name for stats output ("stat", "sendmsg") */
|
||||
int nr; /* __NR_xxx */
|
||||
const char* kafel_name; /* name for kafel policy ("newstat", "sendmsg") */
|
||||
const char* display_name; /* name for stats output ("stat", "sendmsg") */
|
||||
SyscallCategory category;
|
||||
ArgRole args[6]; /* role of each syscall argument */
|
||||
ArgRole args[6]; /* role of each syscall argument */
|
||||
};
|
||||
|
||||
/* Shorthand for table readability */
|
||||
@@ -63,166 +64,166 @@ using A = ArgRole;
|
||||
* The kafel BPF policy, name lookup, and argument decoding all derive from this table.
|
||||
*/
|
||||
static constexpr SyscallDef kTracedSyscalls[] = {
|
||||
/* FS - arg0 = path (AT_FDCWD implied) */
|
||||
/* FS - arg0 = path (AT_FDCWD implied) */
|
||||
#ifdef __NR_open
|
||||
{__NR_open, "open", "open", SyscallCategory::FS,
|
||||
{A::PATH, A::FLAGS, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_open, "open", "open", SyscallCategory::FS,
|
||||
{A::PATH, A::FLAGS, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_open */
|
||||
#ifdef __NR_stat
|
||||
{__NR_stat, "newstat", "stat", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_stat, "newstat", "stat", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_stat */
|
||||
#ifdef __NR_lstat
|
||||
{__NR_lstat, "newlstat", "lstat", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_lstat, "newlstat", "lstat", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_lstat */
|
||||
#ifdef __NR_access
|
||||
{__NR_access, "access", "access", SyscallCategory::FS,
|
||||
{A::PATH, A::ACCESS, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_access, "access", "access", SyscallCategory::FS,
|
||||
{A::PATH, A::ACCESS, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_access */
|
||||
#ifdef __NR_chmod
|
||||
{__NR_chmod, "chmod", "chmod", SyscallCategory::FS,
|
||||
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_chmod, "chmod", "chmod", SyscallCategory::FS,
|
||||
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_chmod */
|
||||
#ifdef __NR_chown
|
||||
{__NR_chown, "chown", "chown", SyscallCategory::FS,
|
||||
{A::PATH, A::UID, A::GID, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_chown, "chown", "chown", SyscallCategory::FS,
|
||||
{A::PATH, A::UID, A::GID, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_chown */
|
||||
#ifdef __NR_lchown
|
||||
{__NR_lchown, "lchown", "lchown", SyscallCategory::FS,
|
||||
{A::PATH, A::UID, A::GID, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_lchown, "lchown", "lchown", SyscallCategory::FS,
|
||||
{A::PATH, A::UID, A::GID, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_lchown */
|
||||
#ifdef __NR_readlink
|
||||
{__NR_readlink, "readlink", "readlink", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_readlink, "readlink", "readlink", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_readlink */
|
||||
#ifdef __NR_mkdir
|
||||
{__NR_mkdir, "mkdir", "mkdir", SyscallCategory::FS,
|
||||
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_mkdir, "mkdir", "mkdir", SyscallCategory::FS,
|
||||
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_mkdir */
|
||||
#ifdef __NR_mknod
|
||||
{__NR_mknod, "mknod", "mknod", SyscallCategory::FS,
|
||||
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_mknod, "mknod", "mknod", SyscallCategory::FS,
|
||||
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_mknod */
|
||||
#ifdef __NR_rmdir
|
||||
{__NR_rmdir, "rmdir", "rmdir", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_rmdir, "rmdir", "rmdir", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_rmdir */
|
||||
#ifdef __NR_unlink
|
||||
{__NR_unlink, "unlink", "unlink", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_unlink, "unlink", "unlink", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_unlink */
|
||||
#ifdef __NR_execve
|
||||
{__NR_execve, "execve", "execve", SyscallCategory::FS,
|
||||
{A::PATH, A::ARGV, A::ENVP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_execve, "execve", "execve", SyscallCategory::FS,
|
||||
{A::PATH, A::ARGV, A::ENVP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_execve */
|
||||
#ifdef __NR_chdir
|
||||
{__NR_chdir, "chdir", "chdir", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_chdir, "chdir", "chdir", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_chdir */
|
||||
#ifdef __NR_chroot
|
||||
{__NR_chroot, "chroot", "chroot", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_chroot, "chroot", "chroot", SyscallCategory::FS,
|
||||
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_chroot */
|
||||
|
||||
/* FS - arg0 = dirfd, arg1 = path */
|
||||
/* FS - arg0 = dirfd, arg1 = path */
|
||||
#ifdef __NR_openat
|
||||
{__NR_openat, "openat", "openat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::FLAGS, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_openat, "openat", "openat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::FLAGS, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_openat */
|
||||
#ifdef __NR_openat2
|
||||
{__NR_openat2, "openat2", "openat2", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::OHOW, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_openat2, "openat2", "openat2", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::OHOW, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_openat2 */
|
||||
#ifdef __NR_newfstatat
|
||||
{__NR_newfstatat, "newfstatat", "newfstatat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_newfstatat, "newfstatat", "newfstatat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_newfstatat */
|
||||
#ifdef __NR_faccessat
|
||||
{__NR_faccessat, "faccessat", "faccessat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::ACCESS, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_faccessat, "faccessat", "faccessat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::ACCESS, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_faccessat */
|
||||
#ifdef __NR_fchmodat
|
||||
{__NR_fchmodat, "fchmodat", "fchmodat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_fchmodat, "fchmodat", "fchmodat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_fchmodat */
|
||||
#ifdef __NR_fchownat
|
||||
{__NR_fchownat, "fchownat", "fchownat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::UID, A::GID, A::SKIP, A::SKIP}},
|
||||
{__NR_fchownat, "fchownat", "fchownat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::UID, A::GID, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_fchownat */
|
||||
#ifdef __NR_readlinkat
|
||||
{__NR_readlinkat, "readlinkat", "readlinkat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_readlinkat, "readlinkat", "readlinkat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_readlinkat */
|
||||
#ifdef __NR_mkdirat
|
||||
{__NR_mkdirat, "mkdirat", "mkdirat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_mkdirat, "mkdirat", "mkdirat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_mkdirat */
|
||||
#ifdef __NR_mknodat
|
||||
{__NR_mknodat, "mknodat", "mknodat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_mknodat, "mknodat", "mknodat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_mknodat */
|
||||
#ifdef __NR_unlinkat
|
||||
{__NR_unlinkat, "unlinkat", "unlinkat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_unlinkat, "unlinkat", "unlinkat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_unlinkat */
|
||||
#ifdef __NR_execveat
|
||||
{__NR_execveat, "execveat", "execveat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::ARGV, A::ENVP, A::SKIP, A::SKIP}},
|
||||
{__NR_execveat, "execveat", "execveat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::ARGV, A::ENVP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_execveat */
|
||||
|
||||
/* FS - two paths */
|
||||
/* FS - two paths */
|
||||
#ifdef __NR_rename
|
||||
{__NR_rename, "rename", "rename", SyscallCategory::FS,
|
||||
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_rename, "rename", "rename", SyscallCategory::FS,
|
||||
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_rename */
|
||||
#ifdef __NR_link
|
||||
{__NR_link, "link", "link", SyscallCategory::FS,
|
||||
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_link, "link", "link", SyscallCategory::FS,
|
||||
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_link */
|
||||
#ifdef __NR_symlink
|
||||
{__NR_symlink, "symlink", "symlink", SyscallCategory::FS,
|
||||
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_symlink, "symlink", "symlink", SyscallCategory::FS,
|
||||
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_symlink */
|
||||
|
||||
/* FS - two dirfd+path pairs */
|
||||
/* FS - two dirfd+path pairs */
|
||||
#ifdef __NR_renameat
|
||||
{__NR_renameat, "renameat", "renameat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
|
||||
{__NR_renameat, "renameat", "renameat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_renameat */
|
||||
#ifdef __NR_renameat2
|
||||
{__NR_renameat2, "renameat2", "renameat2", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
|
||||
{__NR_renameat2, "renameat2", "renameat2", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_renameat2 */
|
||||
#ifdef __NR_linkat
|
||||
{__NR_linkat, "linkat", "linkat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
|
||||
{__NR_linkat, "linkat", "linkat", SyscallCategory::FS,
|
||||
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_linkat */
|
||||
|
||||
/* symlinkat: arg0=target(CWD), arg1=newdirfd, arg2=newpath */
|
||||
/* symlinkat: arg0=target(CWD), arg1=newdirfd, arg2=newpath */
|
||||
#ifdef __NR_symlinkat
|
||||
{__NR_symlinkat, "symlinkat", "symlinkat", SyscallCategory::FS,
|
||||
{A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_symlinkat, "symlinkat", "symlinkat", SyscallCategory::FS,
|
||||
{A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_symlinkat */
|
||||
|
||||
/* NET - sockaddr-based */
|
||||
/* NET - sockaddr-based */
|
||||
#ifdef __NR_connect
|
||||
{__NR_connect, "connect", "connect", SyscallCategory::NET,
|
||||
{A::FD, A::SADDR, A::ALEN, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_connect, "connect", "connect", SyscallCategory::NET,
|
||||
{A::FD, A::SADDR, A::ALEN, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_connect */
|
||||
#ifdef __NR_bind
|
||||
{__NR_bind, "bind", "bind", SyscallCategory::NET,
|
||||
{A::FD, A::SADDR, A::ALEN, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_bind, "bind", "bind", SyscallCategory::NET,
|
||||
{A::FD, A::SADDR, A::ALEN, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_bind */
|
||||
#ifdef __NR_sendto
|
||||
{__NR_sendto, "sendto", "sendto", SyscallCategory::NET,
|
||||
{A::FD, A::SKIP, A::SKIP, A::IFLAGS, A::SADDR, A::ALEN}},
|
||||
{__NR_sendto, "sendto", "sendto", SyscallCategory::NET,
|
||||
{A::FD, A::SKIP, A::SKIP, A::IFLAGS, A::SADDR, A::ALEN}},
|
||||
#endif /* __NR_sendto */
|
||||
|
||||
/* NET - socket creation */
|
||||
/* NET - socket creation */
|
||||
#ifdef __NR_socket
|
||||
{__NR_socket, "socket", "socket", SyscallCategory::NET,
|
||||
{A::DOMAIN, A::STYPE, A::PROTO, A::SKIP, A::SKIP, A::SKIP}},
|
||||
{__NR_socket, "socket", "socket", SyscallCategory::NET,
|
||||
{A::DOMAIN, A::STYPE, A::PROTO, A::SKIP, A::SKIP, A::SKIP}},
|
||||
#endif /* __NR_socket */
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
/*
|
||||
* Seccomp Unotify subsystem for nsjail.
|
||||
* This module allows nsjail to observe and log syscalls made by the sandboxed
|
||||
* process using the SECCOMP_USER_NOTIF feature. It runs a background thread
|
||||
* that reads notifications, decodes arguments, and aggregates statistics.
|
||||
* -----------------------------------------
|
||||
*
|
||||
* This module allows nsjail to observe and log syscalls made by the
|
||||
* sandboxed process using the SECCOMP_USER_NOTIF feature. It registers
|
||||
* the unotify fd with the per-child epoll loop and processes
|
||||
* notifications in-line (no dedicated thread).
|
||||
*
|
||||
* Ownership: start() only takes ownership of the passed fd on success.
|
||||
* On success, the fd is managed by the event loop (freed via unotify::stop()).
|
||||
* On failure, the fd is untouched and remains the caller's responsibility to close.
|
||||
*/
|
||||
|
||||
#include "unotify.h"
|
||||
#include "unotify/unotify.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/seccomp.h>
|
||||
#include <poll.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "logs.h"
|
||||
#include "missing_defs.h"
|
||||
#include "monitor.h"
|
||||
#include "unotify/record.h"
|
||||
#include "unotify/stats.h"
|
||||
#include "unotify/syscall.h"
|
||||
@@ -23,104 +34,172 @@
|
||||
|
||||
namespace unotify {
|
||||
|
||||
#ifndef SECCOMP_IOCTL_NOTIF_RECV
|
||||
#define SECCOMP_IOCTL_NOTIF_RECV SECCOMP_IOWR(0, struct seccomp_notif)
|
||||
#define SECCOMP_IOCTL_NOTIF_SEND SECCOMP_IOWR(1, struct seccomp_notif_resp)
|
||||
#define SECCOMP_IOCTL_NOTIF_ID_VALID SECCOMP_IOWR(2, __u64)
|
||||
#endif
|
||||
|
||||
#ifndef SECCOMP_USER_NOTIF_FLAG_CONTINUE
|
||||
#define SECCOMP_USER_NOTIF_FLAG_CONTINUE (1UL << 0)
|
||||
#endif
|
||||
|
||||
static int unotif_fd = -1;
|
||||
static std::thread* worker_thread = nullptr;
|
||||
|
||||
/*
|
||||
* Checks if the target process is still alive and the notification ID is valid.
|
||||
* Returns true if valid, false otherwise.
|
||||
*/
|
||||
static bool isTargetAlive(int fd, __u64 last_id) {
|
||||
return ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID, &last_id) == 0;
|
||||
}
|
||||
|
||||
static void threadMain() {
|
||||
LOG_I("Started unotify loop");
|
||||
/*
|
||||
* When last_id is 0 (no notification received yet), NOTIF_ID_VALID
|
||||
* will always fail since ID 0 is never valid. This correctly exits
|
||||
* the loop when NOTIF_RECV has never succeeded (child died before
|
||||
* making any traced syscall).
|
||||
*/
|
||||
__u64 last_id = 0;
|
||||
|
||||
while (true) {
|
||||
struct pollfd pfd = {.fd = unotif_fd, .events = POLLIN, .revents = 0};
|
||||
|
||||
int ret = poll(&pfd, 1, -1);
|
||||
if (ret == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
PLOG_E("poll failed");
|
||||
if (!isTargetAlive(unotif_fd, last_id)) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
struct seccomp_notif req = {};
|
||||
if (ioctl(unotif_fd, SECCOMP_IOCTL_NOTIF_RECV, &req) == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
PLOG_D("SECCOMP_IOCTL_NOTIF_RECV");
|
||||
if (!isTargetAlive(unotif_fd, last_id)) break;
|
||||
continue;
|
||||
}
|
||||
last_id = req.id;
|
||||
|
||||
LOG_D("Received seccomp notification for syscall %d", req.data.nr);
|
||||
|
||||
SyscallRecord rec;
|
||||
parseSyscall(&req, &rec);
|
||||
addStat(rec);
|
||||
|
||||
if (!isTargetAlive(unotif_fd, req.id)) {
|
||||
break;
|
||||
}
|
||||
|
||||
struct seccomp_notif_resp resp = {};
|
||||
resp.id = req.id;
|
||||
resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
|
||||
|
||||
if (ioctl(unotif_fd, SECCOMP_IOCTL_NOTIF_SEND, &resp) == -1) {
|
||||
if (errno != ENOENT) {
|
||||
PLOG_E("SECCOMP_IOCTL_NOTIF_SEND failed");
|
||||
}
|
||||
if (!isTargetAlive(unotif_fd, req.id)) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool start(nsj_t* nsj, int fd) {
|
||||
if (worker_thread) {
|
||||
LOG_W("unotify::start() called while already running. "
|
||||
"Concurrent tracing in LISTEN mode is not yet supported. "
|
||||
"Closing notification fd for this process.");
|
||||
close(fd);
|
||||
return true;
|
||||
}
|
||||
unotif_fd = fd;
|
||||
worker_thread = new std::thread(threadMain);
|
||||
return true;
|
||||
return TEMP_FAILURE_RETRY(ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID, &last_id)) == 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Called after killAndReapAll(). The worker thread exits its loop when
|
||||
* isTargetAlive() (SECCOMP_IOCTL_NOTIF_ID_VALID) reports the target is dead.
|
||||
* Per-fd context holding the notification fd and kernel-reported structure
|
||||
* sizes for seccomp_notif / seccomp_notif_resp.
|
||||
*/
|
||||
void stop(nsj_t* nsj) {
|
||||
if (worker_thread) {
|
||||
worker_thread->join();
|
||||
delete worker_thread;
|
||||
worker_thread = nullptr;
|
||||
struct unotifyCtx_t {
|
||||
int fd = -1;
|
||||
uint16_t req_size;
|
||||
uint16_t resp_size;
|
||||
uint8_t* req_buf = nullptr;
|
||||
uint8_t* resp_buf = nullptr;
|
||||
};
|
||||
|
||||
static thread_local unotifyCtx_t current_ctx;
|
||||
|
||||
static void closeAndUnregister(int fd) {
|
||||
if (fd >= 0) {
|
||||
monitor::removeFd(fd);
|
||||
close(fd);
|
||||
current_ctx.fd = -1;
|
||||
}
|
||||
if (unotif_fd != -1) {
|
||||
close(unotif_fd);
|
||||
unotif_fd = -1;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
closeAndUnregister(current_ctx.fd);
|
||||
free(current_ctx.req_buf);
|
||||
free(current_ctx.resp_buf);
|
||||
current_ctx.req_buf = nullptr;
|
||||
current_ctx.resp_buf = nullptr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Epoll callback that processes pending seccomp notifications from the
|
||||
* kernel. For each notification, it records statistics and sends a CONTINUE
|
||||
* response so the traced syscall proceeds in the child.
|
||||
*
|
||||
* We process one notification per callback to avoid event starvation,
|
||||
* yielding control back to the event loop.
|
||||
*/
|
||||
static void unotifyCb(int fd, uint32_t events, void* /* data */) {
|
||||
if (events & (EPOLLHUP | EPOLLERR)) {
|
||||
LOG_D("unotif_fd=%d hung up or error, removing from epoll", fd);
|
||||
closeAndUnregister(fd);
|
||||
return;
|
||||
}
|
||||
printStats(nsj);
|
||||
|
||||
struct seccomp_notif* req = reinterpret_cast<struct seccomp_notif*>(current_ctx.req_buf);
|
||||
struct seccomp_notif_resp* resp =
|
||||
reinterpret_cast<struct seccomp_notif_resp*>(current_ctx.resp_buf);
|
||||
|
||||
memset(req, 0, current_ctx.req_size);
|
||||
if (TEMP_FAILURE_RETRY(ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV, req)) == -1) {
|
||||
/* EAGAIN/EWOULDBLOCK = no more pending */
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return;
|
||||
}
|
||||
if (errno == ENOENT) {
|
||||
LOG_D("unotif_fd=%d returned ENOENT, child likely gone. Removing "
|
||||
"from epoll.",
|
||||
fd);
|
||||
closeAndUnregister(fd);
|
||||
return;
|
||||
}
|
||||
PLOG_W("SECCOMP_IOCTL_NOTIF_RECV failed unexpectedly");
|
||||
closeAndUnregister(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
SyscallRecord rec;
|
||||
LOG_D("unotifyCb: before parseSyscall, nr=%d", req->data.nr);
|
||||
parseSyscall(req, &rec);
|
||||
LOG_D("unotifyCb: after parseSyscall");
|
||||
LOG_D("unotify: syscall=%s id=%llx", rec.name.c_str(), (unsigned long long)req->id);
|
||||
addStat(rec);
|
||||
|
||||
if (!isTargetAlive(fd, req->id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(resp, 0, current_ctx.resp_size);
|
||||
resp->id = req->id;
|
||||
|
||||
if (rec.name == "connect" && rec.res.has_net &&
|
||||
(rec.res.net_type == Stat_NetResource_Type_IPV4 ||
|
||||
rec.res.net_type == Stat_NetResource_Type_IPV6)) {
|
||||
resp->flags = 0;
|
||||
resp->error = -ECONNREFUSED;
|
||||
resp->val = -1;
|
||||
LOG_D("unotify: failing network connect with ECONNREFUSED");
|
||||
} else {
|
||||
resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
|
||||
}
|
||||
|
||||
if (TEMP_FAILURE_RETRY(ioctl(fd, SECCOMP_IOCTL_NOTIF_SEND, resp)) == -1) {
|
||||
if (errno != ENOENT) {
|
||||
PLOG_W("SECCOMP_IOCTL_NOTIF_SEND");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Initializes the unotify monitoring for one child process.
|
||||
*
|
||||
* Queries SECCOMP_GET_NOTIF_SIZES from the kernel to learn the correct
|
||||
* allocation sizes, sets the fd non-blocking, and registers it with the
|
||||
* epoll loop.
|
||||
*
|
||||
* Returns true if the fd was successfully absorbed by the loop. On failure,
|
||||
* the fd is untouched and remains the caller's responsibility to close.
|
||||
*/
|
||||
bool start(nsj_t* nsj, int fd) {
|
||||
thread_local struct seccomp_notif_sizes sizes = {0, 0, 0};
|
||||
if (sizes.seccomp_notif == 0) {
|
||||
if (util::syscall(__NR_seccomp, SECCOMP_GET_NOTIF_SIZES, 0, (uintptr_t)&sizes) ==
|
||||
-1) {
|
||||
PLOG_W("seccomp(SECCOMP_GET_NOTIF_SIZES)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
current_ctx.fd = fd;
|
||||
current_ctx.req_size = sizes.seccomp_notif;
|
||||
current_ctx.resp_size = sizes.seccomp_notif_resp;
|
||||
|
||||
current_ctx.req_buf = static_cast<uint8_t*>(malloc(sizes.seccomp_notif));
|
||||
current_ctx.resp_buf = static_cast<uint8_t*>(malloc(sizes.seccomp_notif_resp));
|
||||
|
||||
if (!current_ctx.req_buf || !current_ctx.resp_buf) {
|
||||
LOG_E("Failed to allocate unotify buffers");
|
||||
free(current_ctx.req_buf);
|
||||
free(current_ctx.resp_buf);
|
||||
current_ctx.req_buf = nullptr;
|
||||
current_ctx.resp_buf = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!util::setNonBlock(fd)) {
|
||||
free(current_ctx.req_buf);
|
||||
free(current_ctx.resp_buf);
|
||||
current_ctx.req_buf = nullptr;
|
||||
current_ctx.resp_buf = nullptr;
|
||||
current_ctx.fd = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!monitor::addFd(fd, EPOLLIN, unotifyCb, nullptr)) {
|
||||
PLOG_W("monitor::addFd for unotify failed");
|
||||
free(current_ctx.req_buf);
|
||||
free(current_ctx.resp_buf);
|
||||
current_ctx.req_buf = nullptr;
|
||||
current_ctx.resp_buf = nullptr;
|
||||
current_ctx.fd = -1;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace unotify
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
namespace unotify {
|
||||
|
||||
bool start(nsj_t* nsj, int unotif_fd);
|
||||
void stop(nsj_t* nsj);
|
||||
/* Note: printStats is in unotify/stats.h, exposed to nsjail.cc */
|
||||
void stop(void);
|
||||
|
||||
} // namespace unotify
|
||||
|
||||
|
||||
27
user.cc
27
user.cc
@@ -73,7 +73,7 @@ static bool setResGid(gid_t gid) {
|
||||
PLOG_W("setresgid(%d)", gid);
|
||||
return false;
|
||||
}
|
||||
#endif /* defined(__NR_setresuid32) */
|
||||
#endif /* defined(__NR_setresgid32) */
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -111,11 +111,10 @@ static bool setGroupsDeny(nsj_t* nsj, pid_t pid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
char fname[PATH_MAX];
|
||||
snprintf(fname, sizeof(fname), "/proc/%d/setgroups", pid);
|
||||
std::string fname = "/proc/" + std::to_string(pid) + "/setgroups";
|
||||
const char* const denystr = "deny";
|
||||
if (!util::writeBufToFile(fname, denystr, strlen(denystr), O_WRONLY | O_CLOEXEC)) {
|
||||
LOG_E("util::writeBufToFile('%s', '%s') failed", fname, denystr);
|
||||
if (!util::writeBufToFile(fname.c_str(), denystr, strlen(denystr), O_WRONLY | O_CLOEXEC)) {
|
||||
LOG_E("util::writeBufToFile('%s', '%s') failed", fname.c_str(), denystr);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -138,11 +137,10 @@ static bool uidMapSelf(nsj_t* nsj, pid_t pid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
char fname[PATH_MAX];
|
||||
snprintf(fname, sizeof(fname), "/proc/%d/uid_map", pid);
|
||||
LOG_D("Writing '%s' to '%s'", map.c_str(), fname);
|
||||
if (!util::writeBufToFile(fname, map.data(), map.length(), O_WRONLY | O_CLOEXEC)) {
|
||||
LOG_E("util::writeBufToFile('%s', '%s') failed", fname, map.c_str());
|
||||
std::string fname = "/proc/" + std::to_string(pid) + "/uid_map";
|
||||
LOG_D("Writing '%s' to '%s'", map.c_str(), fname.c_str());
|
||||
if (!util::writeBufToFile(fname.c_str(), map.data(), map.length(), O_WRONLY | O_CLOEXEC)) {
|
||||
LOG_E("util::writeBufToFile('%s', '%s') failed", fname.c_str(), map.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -166,11 +164,10 @@ static bool gidMapSelf(nsj_t* nsj, pid_t pid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
char fname[PATH_MAX];
|
||||
snprintf(fname, sizeof(fname), "/proc/%d/gid_map", pid);
|
||||
LOG_D("Writing '%s' to '%s'", map.c_str(), fname);
|
||||
if (!util::writeBufToFile(fname, map.data(), map.length(), O_WRONLY | O_CLOEXEC)) {
|
||||
LOG_E("util::writeBufToFile('%s', '%s') failed", fname, map.c_str());
|
||||
std::string fname = "/proc/" + std::to_string(pid) + "/gid_map";
|
||||
LOG_D("Writing '%s' to '%s'", map.c_str(), fname.c_str());
|
||||
if (!util::writeBufToFile(fname.c_str(), map.data(), map.length(), O_WRONLY | O_CLOEXEC)) {
|
||||
LOG_E("util::writeBufToFile('%s', '%s') failed", fname.c_str(), map.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
12
util.cc
12
util.cc
@@ -165,18 +165,6 @@ bool recvMsg(int sock, uint32_t* msg_val, int* fd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sendFd(int sock, int fd) {
|
||||
return sendMsg(sock, 0, fd);
|
||||
}
|
||||
|
||||
int recvFd(int sock) {
|
||||
int fd = -1;
|
||||
if (recvMsg(sock, nullptr, &fd)) {
|
||||
return fd;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool readFromFileToStr(const char* fname, std::string* str) {
|
||||
int fd = TEMP_FAILURE_RETRY(open(fname, O_RDONLY | O_CLOEXEC));
|
||||
if (fd == -1) {
|
||||
|
||||
5
util.h
5
util.h
@@ -33,6 +33,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "missing_defs.h"
|
||||
#include "nsjail.h"
|
||||
|
||||
#define RETURN_ON_FAILURE(expr) \
|
||||
@@ -53,10 +54,6 @@ bool writeToFd(int fd, const void* buf, size_t len);
|
||||
|
||||
bool sendMsg(int sock, uint32_t msg, int fd = -1);
|
||||
bool recvMsg(int sock, uint32_t* msg, int* fd = nullptr);
|
||||
|
||||
bool sendFd(int sock, int fd);
|
||||
int recvFd(int sock);
|
||||
|
||||
bool writeBufToFile(
|
||||
const char* filename, const void* buf, size_t len, int open_flags, bool log_errors = true);
|
||||
bool createDirRecursively(const char* dir);
|
||||
|
||||
Reference in New Issue
Block a user