mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
168 lines
4.9 KiB
Go
168 lines
4.9 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
//go:build linux
|
|
|
|
package sandbox
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"quad4/reticulum-go/pkg/common"
|
|
)
|
|
|
|
func TestSeccompPolicyBuilds(t *testing.T) {
|
|
arch, denied, err := seccompPolicy()
|
|
if err != nil {
|
|
t.Fatalf("seccompPolicy: %v", err)
|
|
}
|
|
if arch == 0 {
|
|
t.Fatal("expected non-zero audit arch")
|
|
}
|
|
if len(denied) == 0 {
|
|
t.Fatal("expected denied syscall list")
|
|
}
|
|
filter := make([]unix.SockFilter, 0, 8+2*len(denied))
|
|
filter = append(filter,
|
|
bpfStmt(unix.BPF_LD|unix.BPF_W|unix.BPF_ABS, seccompDataArchOffset),
|
|
bpfJump(unix.BPF_JMP|unix.BPF_JEQ|unix.BPF_K, arch, 1, 0),
|
|
bpfStmt(unix.BPF_RET|unix.BPF_K, unix.SECCOMP_RET_KILL_THREAD),
|
|
bpfStmt(unix.BPF_LD|unix.BPF_W|unix.BPF_ABS, seccompDataNrOffset),
|
|
)
|
|
for _, nr := range denied {
|
|
filter = append(filter,
|
|
bpfJump(unix.BPF_JMP|unix.BPF_JEQ|unix.BPF_K, uint32(nr), 0, 1),
|
|
bpfStmt(unix.BPF_RET|unix.BPF_K, seccompRetErrnoEPERM),
|
|
)
|
|
}
|
|
filter = append(filter, bpfStmt(unix.BPF_RET|unix.BPF_K, unix.SECCOMP_RET_ALLOW))
|
|
if len(filter) < 5 {
|
|
t.Fatalf("filter too short: %d", len(filter))
|
|
}
|
|
}
|
|
|
|
func TestSeccompEnabledConfig(t *testing.T) {
|
|
if !seccompEnabled(nil) {
|
|
t.Fatal("nil config should enable seccomp")
|
|
}
|
|
if seccompEnabled(&common.ReticulumConfig{EnableSandbox: false, EnableSeccomp: true}) {
|
|
t.Fatal("seccomp should be off when sandbox is off")
|
|
}
|
|
if !seccompEnabled(&common.ReticulumConfig{EnableSandbox: true, EnableSeccomp: true}) {
|
|
t.Fatal("expected seccomp enabled")
|
|
}
|
|
if seccompEnabled(&common.ReticulumConfig{EnableSandbox: true, EnableSeccomp: false}) {
|
|
t.Fatal("explicit enable_seccomp=no should disable")
|
|
}
|
|
}
|
|
|
|
func TestSeccompFunctional(t *testing.T) {
|
|
if os.Getenv("RETICULUM_QEMU_USER") == "1" {
|
|
t.Skip("seccomp filter install is unreliable under qemu-user")
|
|
}
|
|
switch runtime.GOARCH {
|
|
case "amd64", "arm64", "386", "arm", "riscv64", "ppc64", "ppc64le":
|
|
default:
|
|
t.Skip("seccomp policy not defined for " + runtime.GOARCH)
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestSeccompHelper", "-test.v")
|
|
cmd.Env = append(os.Environ(), "SANDBOX_SECCOMP_TEST=1")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
if strings.Contains(string(out), "not supported") ||
|
|
strings.Contains(string(out), "operation not permitted") ||
|
|
strings.Contains(string(out), "seccomp install") {
|
|
t.Skip("seccomp not available in test environment")
|
|
}
|
|
t.Fatalf("seccomp helper failed:\n%s", out)
|
|
}
|
|
if !strings.Contains(string(out), "PASS") {
|
|
t.Fatalf("seccomp helper did not report PASS:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestSeccompHelper runs in a subprocess under the seccomp denylist.
|
|
func TestSeccompHelper(t *testing.T) {
|
|
if os.Getenv("SANDBOX_SECCOMP_TEST") != "1" {
|
|
t.Skip("helper, run via TestSeccompFunctional")
|
|
}
|
|
|
|
if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
|
|
t.Fatalf("PR_SET_NO_NEW_PRIVS failed: %v", err)
|
|
}
|
|
mode, err := installSeccompFilter()
|
|
if err != nil {
|
|
t.Skipf("seccomp install: %v", err)
|
|
}
|
|
switch mode {
|
|
case "tsync", "all_threads", "prctl":
|
|
default:
|
|
t.Fatalf("unexpected install mode %q", mode)
|
|
}
|
|
|
|
// Allowed: ordinary process queries must still succeed.
|
|
if pid := os.Getpid(); pid <= 0 {
|
|
t.Fatalf("getpid after seccomp returned %d", pid)
|
|
}
|
|
|
|
// Denied: ptrace should return EPERM.
|
|
_, _, errno := unix.Syscall6(unix.SYS_PTRACE, uintptr(unix.PTRACE_TRACEME), 0, 0, 0, 0, 0)
|
|
if errno != unix.EPERM {
|
|
t.Fatalf("ptrace expected EPERM, got %v", errno)
|
|
}
|
|
|
|
// Denied: mount should return EPERM (unprivileged already fails, but
|
|
// seccomp must still intercept before capability checks where possible).
|
|
err = unix.Mount("none", "/", "", unix.MS_RDONLY, "")
|
|
if err == nil {
|
|
t.Fatal("mount unexpectedly succeeded")
|
|
}
|
|
if errno, ok := err.(syscall.Errno); ok && errno != unix.EPERM && errno != unix.EACCES {
|
|
t.Fatalf("mount expected EPERM/EACCES, got %v", err)
|
|
}
|
|
|
|
_, _, errno = unix.Syscall(unix.SYS_UNSHARE, uintptr(unix.CLONE_NEWNS), 0, 0)
|
|
if errno != unix.EPERM {
|
|
t.Fatalf("unshare expected EPERM, got %v", errno)
|
|
}
|
|
|
|
c, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("udp after seccomp: %v", err)
|
|
}
|
|
_ = c.Close()
|
|
|
|
done := make(chan struct{})
|
|
go func() { close(done) }()
|
|
<-done
|
|
}
|
|
|
|
func TestSeccompBuildProg(t *testing.T) {
|
|
prog, err := buildSeccompProg()
|
|
if err != nil {
|
|
t.Fatalf("buildSeccompProg: %v", err)
|
|
}
|
|
if prog == nil || prog.Len == 0 || prog.Filter == nil {
|
|
t.Fatal("expected non-empty sock_fprog")
|
|
}
|
|
}
|
|
|
|
func TestSeccompInstallSoftFailUnsupportedArch(t *testing.T) {
|
|
// installSeccompFilter soft-fails via applySeccomp; policy error path is covered
|
|
// by seccompPolicy on supported arches returning a usable denylist.
|
|
arch, denied, err := seccompPolicy()
|
|
if err != nil {
|
|
t.Fatalf("seccompPolicy: %v", err)
|
|
}
|
|
if arch == 0 || len(denied) == 0 {
|
|
t.Fatal("expected usable policy on this arch")
|
|
}
|
|
}
|