Smail/backend/hooks/plugin_isolation_linux_test.go
smail ceacd99a74
Some checks are pending
Security Checks / security (push) Waiting to run
为隔离插件增加按权限校验的本地出站通道
2026-09-30 22:33:27 +08:00

135 lines
3.8 KiB
Go

//go:build linux
package hooks
import (
"net"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCopyPluginFileRejectsGrowthPastLimit(t *testing.T) {
directory := t.TempDir()
sourcePath := filepath.Join(directory, "source.json")
targetPath := filepath.Join(directory, "target.json")
if err := os.WriteFile(sourcePath, []byte(strings.Repeat("a", int(maxPluginConfigBytes)+1)), 0600); err != nil {
t.Fatal(err)
}
if err := copyPluginFile(sourcePath, targetPath, os.Getuid(), os.Getgid(), 0600); err == nil {
t.Fatal("oversized plugin configuration was copied")
}
if _, err := os.Lstat(targetPath); !os.IsNotExist(err) {
t.Fatalf("oversized target was not removed: %v", err)
}
}
func TestPluginCopiesApplySecurityBeforeClosingTargets(t *testing.T) {
directory := t.TempDir()
configSource := filepath.Join(directory, "source.json")
configTarget := filepath.Join(directory, "target.json")
if err := os.WriteFile(configSource, []byte(`{"enabled":true}`), 0600); err != nil {
t.Fatal(err)
}
if err := copyPluginFile(configSource, configTarget, os.Getuid(), os.Getgid(), 0600); err != nil {
t.Fatalf("copy plugin configuration: %v", err)
}
configInfo, err := os.Stat(configTarget)
if err != nil {
t.Fatal(err)
}
if configInfo.Mode().Perm() != 0600 {
t.Fatalf("configuration mode = %o, want 0600", configInfo.Mode().Perm())
}
executableSource := filepath.Join(directory, "source-plugin")
executableTarget := filepath.Join(directory, "target-plugin")
if err := os.WriteFile(executableSource, []byte("plugin"), 0700); err != nil {
t.Fatal(err)
}
if err := copyPluginExecutable(executableSource, executableTarget, os.Getuid(), os.Getgid()); err != nil {
t.Fatalf("copy plugin executable: %v", err)
}
executableInfo, err := os.Stat(executableTarget)
if err != nil {
t.Fatal(err)
}
if executableInfo.Mode().Perm() != 0700 {
t.Fatalf("executable mode = %o, want 0700", executableInfo.Mode().Perm())
}
}
func TestInheritedAbstractSocketRemainsReachableAcrossListenerCopy(t *testing.T) {
socketName, err := newPluginSocketName(1)
if err != nil {
t.Fatal(err)
}
address := "@" + socketName
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: address, Net: "unix"})
if err != nil {
t.Fatal(err)
}
file, err := listener.File()
if closeErr := listener.Close(); closeErr != nil && err == nil {
err = closeErr
}
if err != nil {
t.Fatal(err)
}
defer file.Close()
inherited, err := net.FileListener(file)
if err != nil {
t.Fatal(err)
}
defer inherited.Close()
accepted := make(chan error, 1)
go func() {
conn, acceptErr := inherited.Accept()
if acceptErr == nil {
_ = conn.Close()
}
accepted <- acceptErr
}()
conn, err := net.Dial("unix", address)
if err != nil {
t.Fatal(err)
}
_ = conn.Close()
if err := <-accepted; err != nil {
t.Fatal(err)
}
}
func TestPluginEgressSocketLifecycle(t *testing.T) {
spec := &pluginLaunchSpec{
runtimeDir: t.TempDir(),
procAttr: &os.ProcAttr{Env: []string{
"SMAIL_PLUGIN_ID=wechat_push",
"SMAIL_PLUGIN_RPC_TOKEN=" + strings.Repeat("b", 64),
}},
}
if err := startPluginEgress(spec, os.Getuid(), os.Getgid()); err != nil {
t.Fatal(err)
}
defer spec.cleanup()
socketPath := filepath.Join(spec.runtimeDir, "egress.socket")
info, err := os.Stat(socketPath)
if err != nil || info.Mode()&os.ModeSocket == 0 || info.Mode().Perm() != 0600 {
t.Fatal("egress socket is missing or permissions are unsafe")
}
token := ""
for _, value := range spec.procAttr.Env {
if strings.HasPrefix(value, "SMAIL_PLUGIN_EGRESS_TOKEN=") {
token = strings.TrimPrefix(value, "SMAIL_PLUGIN_EGRESS_TOKEN=")
}
}
if len(token) != 64 || token == strings.Repeat("b", 64) {
t.Fatal("egress token is missing or reuses the RPC token")
}
spec.egressStop()
if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
t.Fatal("egress socket survived shutdown")
}
}