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/dns.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

187 lines
5.4 KiB
Go

//go:build darwin || linux
package daemon
import (
"fmt"
"net"
"os"
"sync"
"time"
)
const (
// maxDNSPacketSize is the maximum UDP packet size we accept.
// DNS can theoretically be up to 65535 bytes, but practically much smaller.
maxDNSPacketSize = 4096
// upstreamTimeout is the time we wait for a response from the upstream DNS server.
upstreamTimeout = 5 * time.Second
)
// DNSRelay is a UDP DNS relay that forwards DNS queries from sandboxed processes
// to a configured upstream DNS server. It operates as a simple packet relay without
// parsing DNS protocol contents.
type DNSRelay struct {
udpConn *net.UDPConn
targetAddr string // upstream DNS server address (host:port)
listenAddr string // address we're listening on
wg sync.WaitGroup
done chan struct{}
debug bool
}
// NewDNSRelay creates a new DNS relay that listens on listenAddr and forwards
// queries to dnsAddr. The listenAddr will typically be "127.0.0.2:53" (loopback alias).
// The dnsAddr must be in "host:port" format (e.g. "1.1.1.1:53").
func NewDNSRelay(listenAddr, dnsAddr string, debug bool) (*DNSRelay, error) {
// Validate the upstream DNS address is parseable as host:port.
targetHost, targetPort, err := net.SplitHostPort(dnsAddr)
if err != nil {
return nil, fmt.Errorf("invalid DNS address %q: %w", dnsAddr, err)
}
if targetHost == "" {
return nil, fmt.Errorf("invalid DNS address %q: empty host", dnsAddr)
}
if targetPort == "" {
return nil, fmt.Errorf("invalid DNS address %q: empty port", dnsAddr)
}
// Resolve and bind the listen address.
udpAddr, err := net.ResolveUDPAddr("udp", listenAddr)
if err != nil {
return nil, fmt.Errorf("failed to resolve listen address %q: %w", listenAddr, err)
}
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
return nil, fmt.Errorf("failed to bind UDP socket on %q: %w", listenAddr, err)
}
return &DNSRelay{
udpConn: conn,
targetAddr: dnsAddr,
listenAddr: conn.LocalAddr().String(),
done: make(chan struct{}),
debug: debug,
}, nil
}
// ListenAddr returns the actual address the relay is listening on.
// This is useful when port 0 was used to get an ephemeral port.
func (d *DNSRelay) ListenAddr() string {
return d.listenAddr
}
// Start begins the DNS relay loop. It reads incoming UDP packets from the
// listening socket and spawns a goroutine per query to forward it to the
// upstream DNS server and relay the response back.
func (d *DNSRelay) Start() error {
if d.debug {
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Listening on %s, forwarding to %s\n", d.listenAddr, d.targetAddr)
}
d.wg.Add(1)
go d.readLoop()
return nil
}
// Stop shuts down the DNS relay. It signals the read loop to stop, closes the
// listening socket, and waits for all in-flight queries to complete.
func (d *DNSRelay) Stop() {
close(d.done)
_ = d.udpConn.Close()
d.wg.Wait()
if d.debug {
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Stopped\n")
}
}
// readLoop is the main loop that reads incoming DNS queries from the listening socket.
func (d *DNSRelay) readLoop() {
defer d.wg.Done()
buf := make([]byte, maxDNSPacketSize)
for {
n, clientAddr, err := d.udpConn.ReadFromUDP(buf)
if err != nil {
select {
case <-d.done:
// Shutting down, expected error from closed socket.
return
default:
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Read error: %v\n", err)
continue
}
}
if n == 0 {
continue
}
// Copy the packet data so the buffer can be reused immediately.
query := make([]byte, n)
copy(query, buf[:n])
d.wg.Add(1)
go d.handleQuery(query, clientAddr)
}
}
// handleQuery forwards a single DNS query to the upstream server and relays
// the response back to the original client. It creates a dedicated UDP connection
// to the upstream server to avoid multiplexing complexity.
func (d *DNSRelay) handleQuery(query []byte, clientAddr *net.UDPAddr) {
defer d.wg.Done()
if d.debug {
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Query from %s (%d bytes)\n", clientAddr, len(query))
}
// Create a dedicated UDP connection to the upstream DNS server.
upstreamConn, err := net.Dial("udp", d.targetAddr)
if err != nil {
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Failed to connect to upstream %s: %v\n", d.targetAddr, err)
return
}
defer upstreamConn.Close() //nolint:errcheck // best-effort cleanup of per-query UDP connection
// Send the query to the upstream server.
if _, err := upstreamConn.Write(query); err != nil {
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Failed to send query to upstream: %v\n", err)
return
}
// Wait for the response with a timeout.
if err := upstreamConn.SetReadDeadline(time.Now().Add(upstreamTimeout)); err != nil {
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Failed to set read deadline: %v\n", err)
return
}
resp := make([]byte, maxDNSPacketSize)
n, err := upstreamConn.Read(resp)
if err != nil {
if d.debug {
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Upstream response error: %v\n", err)
}
return
}
// Send the response back to the original client.
if _, err := d.udpConn.WriteToUDP(resp[:n], clientAddr); err != nil {
// The listening socket may have been closed during shutdown.
select {
case <-d.done:
return
default:
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Failed to send response to %s: %v\n", clientAddr, err)
}
}
if d.debug {
fmt.Fprintf(os.Stderr, "[greywall:dns-relay] Response to %s (%d bytes)\n", clientAddr, n)
}
}