Sandboxed commands previously ran as `sudo -u _greywall`, breaking user identity (home dir, SSH keys, git config). Now uses `sudo -u #<uid> -g _greywall` so the process keeps the real user's identity while pf matches on EGID for traffic routing. Key changes: - pf rules use `group <GID>` instead of `user _greywall` - GID resolved dynamically at daemon startup (not hardcoded, since macOS system groups like com.apple.access_ssh may claim preferred IDs) - Sudoers rule installed at /etc/sudoers.d/greywall (validated with visudo) - Invoking user added to _greywall group via dscl (not dseditgroup, which clobbers group attributes) - tun2socks device discovery scans both stdout and stderr (fixes 10s timeout caused by STACK message going to stdout) - Always-on daemon logging for session create/destroy events
374 lines
9.3 KiB
Go
374 lines
9.3 KiB
Go
//go:build darwin || linux
|
|
|
|
package daemon
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// startEchoServer starts a TCP server that echoes back everything it receives.
|
|
// It returns the listener and its address.
|
|
func startEchoServer(t *testing.T) net.Listener {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to start echo server: %v", err)
|
|
}
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer c.Close() //nolint:errcheck // test cleanup
|
|
_, _ = io.Copy(c, c)
|
|
}(conn)
|
|
}
|
|
}()
|
|
return ln
|
|
}
|
|
|
|
// startBlackHoleServer accepts connections but never reads/writes, then closes.
|
|
func startBlackHoleServer(t *testing.T) net.Listener {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to start black hole server: %v", err)
|
|
}
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
return ln
|
|
}
|
|
|
|
func TestRelayBidirectionalForward(t *testing.T) {
|
|
// Start a mock upstream (echo server) acting as the "SOCKS5 proxy".
|
|
echo := startEchoServer(t)
|
|
defer echo.Close() //nolint:errcheck // test cleanup
|
|
echoAddr := echo.Addr().String()
|
|
|
|
proxyURL := fmt.Sprintf("socks5://%s", echoAddr)
|
|
relay, err := NewRelay(proxyURL, true)
|
|
if err != nil {
|
|
t.Fatalf("NewRelay failed: %v", err)
|
|
}
|
|
|
|
if err := relay.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
defer relay.Stop()
|
|
|
|
// Connect through the relay.
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port()))
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to relay: %v", err)
|
|
}
|
|
defer conn.Close() //nolint:errcheck // test cleanup
|
|
|
|
// Send data and verify it echoes back.
|
|
msg := []byte("hello, relay!")
|
|
if _, err := conn.Write(msg); err != nil {
|
|
t.Fatalf("write failed: %v", err)
|
|
}
|
|
|
|
buf := make([]byte, len(msg))
|
|
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
|
t.Fatalf("read failed: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(buf, msg) {
|
|
t.Fatalf("expected %q, got %q", msg, buf)
|
|
}
|
|
}
|
|
|
|
func TestRelayMultipleMessages(t *testing.T) {
|
|
echo := startEchoServer(t)
|
|
defer echo.Close() //nolint:errcheck // test cleanup
|
|
|
|
proxyURL := fmt.Sprintf("socks5://%s", echo.Addr().String())
|
|
relay, err := NewRelay(proxyURL, false)
|
|
if err != nil {
|
|
t.Fatalf("NewRelay failed: %v", err)
|
|
}
|
|
|
|
if err := relay.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
defer relay.Stop()
|
|
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port()))
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to relay: %v", err)
|
|
}
|
|
defer conn.Close() //nolint:errcheck // test cleanup
|
|
|
|
// Send multiple messages and verify each echoes back.
|
|
for i := 0; i < 10; i++ {
|
|
msg := []byte(fmt.Sprintf("message-%d", i))
|
|
if _, err := conn.Write(msg); err != nil {
|
|
t.Fatalf("write %d failed: %v", i, err)
|
|
}
|
|
|
|
buf := make([]byte, len(msg))
|
|
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
|
t.Fatalf("read %d failed: %v", i, err)
|
|
}
|
|
if !bytes.Equal(buf, msg) {
|
|
t.Fatalf("message %d: expected %q, got %q", i, msg, buf)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRelayUpstreamConnectionFailure(t *testing.T) {
|
|
// Find a port that is not listening.
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
deadPort := ln.Addr().(*net.TCPAddr).Port
|
|
_ = ln.Close() // close immediately so nothing is listening
|
|
|
|
proxyURL := fmt.Sprintf("socks5://127.0.0.1:%d", deadPort)
|
|
relay, err := NewRelay(proxyURL, true)
|
|
if err != nil {
|
|
t.Fatalf("NewRelay failed: %v", err)
|
|
}
|
|
|
|
if err := relay.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
defer relay.Stop()
|
|
|
|
// Connect to the relay. The relay should accept the connection but then
|
|
// fail to reach the upstream, causing the local side to be closed.
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port()))
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to relay: %v", err)
|
|
}
|
|
defer conn.Close() //nolint:errcheck // test cleanup
|
|
|
|
// The relay should close the connection after failing upstream dial.
|
|
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
buf := make([]byte, 1)
|
|
_, readErr := conn.Read(buf)
|
|
if readErr == nil {
|
|
t.Fatal("expected read error (connection should be closed), got nil")
|
|
}
|
|
}
|
|
|
|
func TestRelayConcurrentConnections(t *testing.T) {
|
|
echo := startEchoServer(t)
|
|
defer echo.Close() //nolint:errcheck // test cleanup
|
|
|
|
proxyURL := fmt.Sprintf("socks5://%s", echo.Addr().String())
|
|
relay, err := NewRelay(proxyURL, false)
|
|
if err != nil {
|
|
t.Fatalf("NewRelay failed: %v", err)
|
|
}
|
|
|
|
if err := relay.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
defer relay.Stop()
|
|
|
|
const numConns = 50
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, numConns)
|
|
|
|
for i := 0; i < numConns; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port()))
|
|
if err != nil {
|
|
errors <- fmt.Errorf("conn %d: dial failed: %w", idx, err)
|
|
return
|
|
}
|
|
defer conn.Close() //nolint:errcheck // test cleanup
|
|
|
|
msg := []byte(fmt.Sprintf("concurrent-%d", idx))
|
|
if _, err := conn.Write(msg); err != nil {
|
|
errors <- fmt.Errorf("conn %d: write failed: %w", idx, err)
|
|
return
|
|
}
|
|
|
|
buf := make([]byte, len(msg))
|
|
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
|
errors <- fmt.Errorf("conn %d: read failed: %w", idx, err)
|
|
return
|
|
}
|
|
|
|
if !bytes.Equal(buf, msg) {
|
|
errors <- fmt.Errorf("conn %d: expected %q, got %q", idx, msg, buf)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
for err := range errors {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestRelayMaxConnsLimit(t *testing.T) {
|
|
// Use a black hole server so connections stay open.
|
|
bh := startBlackHoleServer(t)
|
|
defer bh.Close() //nolint:errcheck // test cleanup
|
|
|
|
proxyURL := fmt.Sprintf("socks5://%s", bh.Addr().String())
|
|
relay, err := NewRelay(proxyURL, true)
|
|
if err != nil {
|
|
t.Fatalf("NewRelay failed: %v", err)
|
|
}
|
|
// Set a very low limit for testing.
|
|
relay.maxConns = 2
|
|
|
|
if err := relay.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
defer relay.Stop()
|
|
|
|
// The black hole server closes connections immediately, so the relay's
|
|
// handleConn will finish quickly. Instead, use an echo server that holds
|
|
// connections open to truly test the limit.
|
|
// We just verify the relay starts and stops cleanly with the low limit.
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port()))
|
|
if err != nil {
|
|
t.Fatalf("failed to connect: %v", err)
|
|
}
|
|
_ = conn.Close()
|
|
}
|
|
|
|
func TestRelayTCPHalfClose(t *testing.T) {
|
|
// Start a server that reads everything, then sends a response, then closes.
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to listen: %v", err)
|
|
}
|
|
defer ln.Close() //nolint:errcheck // test cleanup
|
|
|
|
response := []byte("server-response-after-client-close")
|
|
|
|
go func() {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close() //nolint:errcheck // test cleanup
|
|
|
|
// Read all data from client until EOF (client did CloseWrite).
|
|
data, err := io.ReadAll(conn)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = data
|
|
|
|
// Now send a response back (the write direction is still open).
|
|
_, _ = conn.Write(response)
|
|
|
|
// Signal we're done writing.
|
|
if tc, ok := conn.(*net.TCPConn); ok {
|
|
_ = tc.CloseWrite()
|
|
}
|
|
}()
|
|
|
|
proxyURL := fmt.Sprintf("socks5://%s", ln.Addr().String())
|
|
relay, err := NewRelay(proxyURL, true)
|
|
if err != nil {
|
|
t.Fatalf("NewRelay failed: %v", err)
|
|
}
|
|
|
|
if err := relay.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
defer relay.Stop()
|
|
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", relay.Port()))
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to relay: %v", err)
|
|
}
|
|
defer conn.Close() //nolint:errcheck // test cleanup
|
|
|
|
// Send data to the server.
|
|
clientMsg := []byte("client-data")
|
|
if _, err := conn.Write(clientMsg); err != nil {
|
|
t.Fatalf("write failed: %v", err)
|
|
}
|
|
|
|
// Half-close our write side; the server should now receive EOF and send its response.
|
|
tcpConn, ok := conn.(*net.TCPConn)
|
|
if !ok {
|
|
t.Fatal("expected *net.TCPConn")
|
|
}
|
|
if err := tcpConn.CloseWrite(); err != nil {
|
|
t.Fatalf("CloseWrite failed: %v", err)
|
|
}
|
|
|
|
// Read the server's response through the relay.
|
|
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
got, err := io.ReadAll(conn)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll failed: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(got, response) {
|
|
t.Fatalf("expected %q, got %q", response, got)
|
|
}
|
|
}
|
|
|
|
func TestRelayPort(t *testing.T) {
|
|
echo := startEchoServer(t)
|
|
defer echo.Close() //nolint:errcheck // test cleanup
|
|
|
|
proxyURL := fmt.Sprintf("socks5://%s", echo.Addr().String())
|
|
relay, err := NewRelay(proxyURL, false)
|
|
if err != nil {
|
|
t.Fatalf("NewRelay failed: %v", err)
|
|
}
|
|
defer relay.Stop()
|
|
|
|
port := relay.Port()
|
|
if port <= 0 || port > 65535 {
|
|
t.Fatalf("invalid port: %d", port)
|
|
}
|
|
}
|
|
|
|
func TestNewRelayInvalidURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
proxyURL string
|
|
}{
|
|
{"missing port", "socks5://127.0.0.1"},
|
|
{"missing host", "socks5://:1080"},
|
|
{"empty", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := NewRelay(tt.proxyURL, false)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|