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
177 lines
5.1 KiB
Go
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)
|
|
}
|