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 20ee23c1c3 fix: use socks5h:// for macOS daemon DNS resolution through proxy
macOS getaddrinfo() uses mDNSResponder via Mach IPC and does NOT fall
back to direct UDP DNS when those services are blocked — it simply
fails with EAI_NONAME. This made DNS resolution fail for all sandboxed
processes in daemon mode.

Switch to setting ALL_PROXY=socks5h:// env var so proxy-aware apps
(curl, git, etc.) resolve hostnames through the SOCKS5 proxy. The "h"
suffix means "resolve hostname at proxy side". Only ALL_PROXY is set
(not HTTP_PROXY) to avoid breaking apps like Bun/Node.js.

Other changes:
- Revert opendirectoryd.libinfo and configd mach service blocks
- Exclude loopback (127.0.0.0/8) from pf TCP route-to to prevent
  double-proxying when ALL_PROXY connects directly to local proxy
- Always create DNS relay with default upstream (127.0.0.1:42053)
- Use always-on logging in DNS relay (not debug-only)
- Force IPv4 (udp4) for DNS relay upstream connections
- Log tunnel cleanup errors instead of silently discarding them
2026-03-02 12:04:36 -06:00

177 lines
5.1 KiB
Go

//go:build darwin || linux
package daemon
import (
"fmt"
"net"
"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 {
Logf("DNS relay listening on %s, forwarding to %s", 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()
Logf("DNS relay stopped")
}
// 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:
Logf("DNS relay: read error: %v", 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()
Logf("DNS relay: query from %s (%d bytes)", clientAddr, len(query))
// Create a dedicated UDP connection to the upstream DNS server.
// Use "udp4" to force IPv4, since the upstream may only listen on 127.0.0.1.
upstreamConn, err := net.Dial("udp4", d.targetAddr)
if err != nil {
Logf("DNS relay: failed to connect to upstream %s: %v", 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 {
Logf("DNS relay: failed to send query to upstream: %v", err)
return
}
// Wait for the response with a timeout.
if err := upstreamConn.SetReadDeadline(time.Now().Add(upstreamTimeout)); err != nil {
Logf("DNS relay: failed to set read deadline: %v", err)
return
}
resp := make([]byte, maxDNSPacketSize)
n, err := upstreamConn.Read(resp)
if err != nil {
Logf("DNS relay: upstream response error from %s: %v", d.targetAddr, 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:
Logf("DNS relay: failed to send response to %s: %v", clientAddr, err)
}
}
Logf("DNS relay: response to %s (%d bytes)", clientAddr, n)
}