This repository has been archived on 2026-03-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
greywall/internal/daemon/relay_test.go
Mathieu Virbel cfe29d2c0b feat: switch macOS daemon from user-based to group-based pf routing
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
2026-02-26 09:56:15 -06:00

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")
}
})
}
}