fix: use raw IPv6 socket for DNS responses in macOS intercept mode

macOS rejects sendmsg from [::1] to global unicast IPv6 (EINVAL), and
nat on lo0 doesn't fire for route-to'd packets (pf skips translation
on the second interface pass). ULA addresses on lo0 also fail (EHOSTUNREACH
- kernel segregates lo0 routing).

Solution: wrap the [::1] UDP listener's ResponseWriter with rawIPv6Writer
that sends responses via SOCK_RAW (IPPROTO_UDP) on lo0, bypassing the
kernel's routing validation. pf's rdr state reverses the address
translation on the response path.

Changes:
- Add rawipv6_darwin.go: rawIPv6Writer wraps dns.ResponseWriter, sends
  UDP responses via raw IPv6 socket with proper checksum calculation
- Add rawipv6_other.go: no-op wrapIPv6Handler for non-darwin platforms
- Remove nat rules from pf anchor (no longer needed)
- Block IPv6 TCP DNS (block return) - falls back to IPv4 (~1s, rare)
- Remove IPv6 TCP rdr/route-to/pass rules (only UDP intercepted)
This commit is contained in:
Codescribe
2026-03-30 13:55:52 -04:00
committed by Cuong Manh Le
parent 95dd871e2d
commit 22a796f673
5 changed files with 227 additions and 78 deletions
+5 -1
View File
@@ -211,7 +211,11 @@ func (p *prog) serveDNS(listenerNum string) error {
proto := proto
if needLocalIPv6Listener(p.cfg.Service.InterceptMode) {
g.Go(func() error {
s, errCh := runDNSServer(net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), proto, handler)
ipv6Handler := handler
if proto == "udp" {
ipv6Handler = wrapIPv6Handler(handler)
}
s, errCh := runDNSServer(net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), proto, ipv6Handler)
defer s.Shutdown()
select {
case <-p.stopCh: