Files
v2rayA/core/dns/socket_windows.go
Zakk a6c2d5f1d2 feat(core): bind the DNS module's own sockets to an egress interface
On Linux the module's upstream, bootstrap and health-check sockets carry
SO_MARK 0x80, which the transparent-proxy rules exempt. Windows and macOS
have no mark: once a TUN holds the default route, those sockets would send
the module's own queries into the TUN, where the core's port-53
interception hands them straight back to the module. The module config
gains egress_interface; when set, every dialer created by
newMarkedDnsClient binds to it with IP_UNICAST_IF / IPV6_UNICAST_IF on
Windows and IP_BOUND_IF / IPV6_BOUND_IF on macOS. Unset, the dialers are
unchanged, and Linux keeps the mark.

The darwin and windows tests dial through the dialer and read the option
back; they run on those platforms. The interface lookup itself is tested
everywhere.
2026-09-19 10:34:55 +10:00

73 lines
1.8 KiB
Go

//go:build windows
package dns
import (
"encoding/binary"
"net"
"syscall"
"time"
"unsafe"
"github.com/miekg/dns"
)
// From ws2ipdef.h; the two options share the number.
const (
ipUnicastIf = 31
ipv6UnicastIf = 31
)
// setSocketMark is a no-op on Windows: there is no SO_MARK. Self-exclusion
// from the TUN is done by binding to the physical interface instead.
func setSocketMark(fd uintptr) error {
return nil
}
// markFd binds the socket to the configured egress interface, if any.
// IP_UNICAST_IF wants the index in network byte order; IPV6_UNICAST_IF
// wants host order.
func markFd(network, address string, c syscall.RawConn) error {
idx, ok := egressInterfaceIndex()
if !ok {
return nil
}
var opErr error
err := c.Control(func(fd uintptr) {
if isIPv6Address(network, address) {
opErr = syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, ipv6UnicastIf, idx)
return
}
var be [4]byte
binary.BigEndian.PutUint32(be[:], uint32(idx))
opErr = syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, ipUnicastIf, int(*(*uint32)(unsafe.Pointer(&be[0]))))
})
if err != nil {
return err
}
return opErr
}
func markedDialer() *net.Dialer {
return &net.Dialer{
Timeout: 5 * time.Second,
Control: markFd,
KeepAlive: 30 * time.Second,
}
}
// newMarkedDnsClient creates a *dns.Client whose sockets are bound to the
// egress interface when one is configured. UDPSize=4096 gives queries
// without an EDNS0 OPT record a 4 KiB receive buffer instead of
// miekg/dns's 512-byte default.
func newMarkedDnsClient(network string) *dns.Client {
return &dns.Client{
Net: network,
UDPSize: 4096,
Timeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
Dialer: markedDialer(),
}
}