mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
fix(dns): проба udp/53 не теряет второй сервер, doh до смены сети
This commit is contained in:
@@ -429,23 +429,27 @@ func udpDNSDial(ctx context.Context, _ string, _ string) (net.Conn, error) {
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// autoDial выполняет DNS probe по UDP/53 и при недоступности переключается на DoH.
|
||||
func autoDial(r *DohResolver) dialFunc {
|
||||
var (
|
||||
probed sync.Once
|
||||
useDoH atomic.Bool
|
||||
mu sync.Mutex
|
||||
probed *[]string
|
||||
useDoH bool
|
||||
doh = dohForwarderDial(r)
|
||||
)
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
probed.Do(func() {
|
||||
if udpProbe(autoUDPBudget) {
|
||||
Log().Infof("[DNS] UDP/53 probe OK, using UDP")
|
||||
mu.Lock()
|
||||
if servers := udpDNSServersPtr.Load(); servers != probed {
|
||||
probed = servers
|
||||
useDoH = !udpProbe(autoUDPBudget)
|
||||
if useDoH {
|
||||
Log().Warnf("[DNS] UDP/53 unreachable; switching to DoH until network change")
|
||||
} else {
|
||||
Log().Warnf("[DNS] UDP/53 unreachable; sticky-switching to DoH")
|
||||
useDoH.Store(true)
|
||||
Log().Infof("[DNS] UDP/53 probe OK, using UDP")
|
||||
}
|
||||
})
|
||||
if useDoH.Load() {
|
||||
}
|
||||
viaDoH := useDoH
|
||||
mu.Unlock()
|
||||
if viaDoH {
|
||||
return doh(ctx, network, addr)
|
||||
}
|
||||
return udpDNSDial(ctx, network, addr)
|
||||
@@ -466,24 +470,31 @@ func udpProbe(timeout time.Duration) bool {
|
||||
buf := make([]byte, 512)
|
||||
servers := udpDNSServers()
|
||||
limit := min(len(servers), 2)
|
||||
for _, server := range servers[:limit] {
|
||||
for i, server := range servers[:limit] {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
break
|
||||
}
|
||||
// Остаток бюджета делится поровну: молчащий сервер не съедает попытку следующего.
|
||||
attempt := remaining / time.Duration(limit-i)
|
||||
// netctl.Apply обязателен для исключения сокета пробы из VPN-туннеля.
|
||||
d := net.Dialer{Timeout: remaining, Control: netctl.Apply}
|
||||
d := net.Dialer{Timeout: attempt, Control: netctl.Apply}
|
||||
conn, err := d.Dial("udp", server) //nolint:noctx
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_ = conn.SetDeadline(deadline) //nolint:errcheck
|
||||
_ = conn.SetDeadline(time.Now().Add(attempt)) //nolint:errcheck
|
||||
_, _ = conn.Write(wire)
|
||||
n, err := conn.Read(buf)
|
||||
_ = conn.Close()
|
||||
if err == nil && n > 12 {
|
||||
if err == nil && isReply(m, buf[:n]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isReply(query *dns.Msg, raw []byte) bool {
|
||||
var resp dns.Msg
|
||||
return resp.Unpack(raw) == nil && resp.Response && resp.Id == query.Id
|
||||
}
|
||||
|
||||
@@ -12,35 +12,22 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// dohAnswer builds a wire-format DNS reply for a single question with one
|
||||
// answer of the matching type (A or AAAA). TTL is returned as-is.
|
||||
func dohAnswer(t *testing.T, query []byte, ip net.IP, ttl uint32) []byte {
|
||||
func dnsAnswer(t *testing.T, query []byte) []byte {
|
||||
t.Helper()
|
||||
req := new(dns.Msg)
|
||||
if err := req.Unpack(query); err != nil {
|
||||
t.Fatalf("unpack query: %v", err)
|
||||
}
|
||||
reply := new(dns.Msg)
|
||||
reply.SetReply(req)
|
||||
if len(req.Question) != 1 {
|
||||
t.Fatalf("expected 1 question, got %d", len(req.Question))
|
||||
}
|
||||
q := req.Question[0]
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
reply.Answer = append(reply.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: ttl},
|
||||
A: v4,
|
||||
})
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
if ip.To4() == nil {
|
||||
reply.Answer = append(reply.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl},
|
||||
AAAA: ip,
|
||||
})
|
||||
}
|
||||
reply := new(dns.Msg)
|
||||
reply.SetReply(req)
|
||||
if q := req.Question[0]; q.Qtype == dns.TypeA {
|
||||
reply.Answer = append(reply.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.IPv4(9, 9, 9, 9),
|
||||
})
|
||||
}
|
||||
out, err := reply.Pack()
|
||||
if err != nil {
|
||||
@@ -49,55 +36,97 @@ func dohAnswer(t *testing.T, query []byte, ip net.IP, ttl uint32) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
func readWire(t *testing.T, r io.Reader) []byte {
|
||||
func mockDoH(t *testing.T) *DohResolver {
|
||||
t.Helper()
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestAutoDial_StickyAfterUDPFailure(t *testing.T) {
|
||||
// DoH backend: always responds with a valid wire-format reply.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body := readWire(t, r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("read body: %v", err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/dns-message")
|
||||
_, _ = w.Write(dohAnswer(t, body, net.ParseIP("9.9.9.9"), 300)) //nolint:errcheck
|
||||
_, _ = w.Write(dnsAnswer(t, body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
resolver := newDohResolverWithClient(
|
||||
t.Cleanup(srv.Close)
|
||||
return newDohResolverWithClient(
|
||||
[]DohEndpoint{{URL: srv.URL, Hostname: "mock", BootstrapIPs: []string{"127.0.0.1"}}},
|
||||
srv.Client(),
|
||||
)
|
||||
|
||||
dial := autoDial(resolver)
|
||||
|
||||
// Poison udpDNSServers so that udpProbe (real DNS round-trip) fails
|
||||
// immediately — net.DialTimeout rejects the malformed address.
|
||||
old := udpDNSServersPtr.Load()
|
||||
bad := []string{"not-a-valid-host-port"}
|
||||
udpDNSServersPtr.Store(&bad)
|
||||
defer func() { udpDNSServersPtr.Store(old) }()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn1, err := dial(ctx, "udp", "unused")
|
||||
if err != nil {
|
||||
t.Fatalf("first dial: %v", err)
|
||||
}
|
||||
_ = conn1.Close()
|
||||
|
||||
// Second call must skip UDP entirely. We assert this by poisoning
|
||||
// udpDNSServers with a value that would fail parsing — if the dialer
|
||||
// touches UDP again the call errors loudly.
|
||||
bad2 := []string{"still-not-a-valid-host-port"}
|
||||
udpDNSServersPtr.Store(&bad2)
|
||||
conn2, err := dial(ctx, "udp", "unused")
|
||||
if err != nil {
|
||||
t.Fatalf("second dial: %v", err)
|
||||
}
|
||||
_ = conn2.Close()
|
||||
}
|
||||
|
||||
// udpDNSServer поднимает UDP/53-стенд; reply == nil - сервер молчит.
|
||||
func udpDNSServer(t *testing.T, reply func(query []byte) []byte) string {
|
||||
t.Helper()
|
||||
pc, err := (&net.ListenConfig{}).ListenPacket(t.Context(), "udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = pc.Close() })
|
||||
go func() {
|
||||
buf := make([]byte, 512)
|
||||
for {
|
||||
n, addr, err := pc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if reply != nil {
|
||||
_, _ = pc.WriteTo(reply(buf[:n]), addr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return pc.LocalAddr().String()
|
||||
}
|
||||
|
||||
func useUDPServers(t *testing.T, servers []string) {
|
||||
t.Helper()
|
||||
old := udpDNSServersPtr.Load()
|
||||
udpDNSServersPtr.Store(&servers)
|
||||
t.Cleanup(func() { udpDNSServersPtr.Store(old) })
|
||||
}
|
||||
|
||||
func TestAutoDial_DoHStickyUntilServersChange(t *testing.T) {
|
||||
dial := autoDial(mockDoH(t))
|
||||
// Невалидный адрес валит UDP-пробу сразу.
|
||||
servers := []string{"not-a-valid-host-port"}
|
||||
useUDPServers(t, servers)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
dialTo := func() string {
|
||||
t.Helper()
|
||||
conn, err := dial(ctx, "udp", "unused")
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
return conn.RemoteAddr().String()
|
||||
}
|
||||
dialTo()
|
||||
|
||||
good := udpDNSServer(t, func(q []byte) []byte { return dnsAnswer(t, q) })
|
||||
servers[0] = good
|
||||
if dialTo() == good {
|
||||
t.Fatal("re-probed UDP without servers change")
|
||||
}
|
||||
|
||||
SetUDPDNSServers([]string{good})
|
||||
if got := dialTo(); got != good {
|
||||
t.Fatalf("after servers change dial went to %s, want UDP %s", got, good)
|
||||
}
|
||||
}
|
||||
|
||||
// Молчащий первый сервер не должен съедать бюджет второго.
|
||||
func TestUDPProbe_SilentFirstServer(t *testing.T) {
|
||||
useUDPServers(t, []string{udpDNSServer(t, nil), udpDNSServer(t, func(q []byte) []byte { return dnsAnswer(t, q) })})
|
||||
if !udpProbe(300 * time.Millisecond) {
|
||||
t.Fatal("udpProbe = false, want true via second server")
|
||||
}
|
||||
}
|
||||
|
||||
// Эхо запроса - не DNS-ответ, UDP/53 такой сервер не обслуживает.
|
||||
func TestUDPProbe_RejectsNonResponse(t *testing.T) {
|
||||
useUDPServers(t, []string{udpDNSServer(t, func(q []byte) []byte { return append([]byte(nil), q...) })})
|
||||
if udpProbe(300 * time.Millisecond) {
|
||||
t.Fatal("udpProbe = true on echoed query, want false")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user