This repository has been archived on 2024-09-06. You can view files and clone it, but cannot push or open issues or pull requests.
clash/dns/util.go
Jiahao Lu 45c10a17a6
Fix: DNS NCACHE TTL and OPT RRs (#2900)
* Fix: DNS NCACHE TTL and OPT RRs

1. DNS NCACHE was not correctly implemented.
2. OPT RRs must not be cached or forwarded.

Closes #2889.
2023-08-24 23:28:04 +08:00

171 lines
3.6 KiB
Go

package dns
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/picker"
"github.com/Dreamacro/clash/log"
D "github.com/miekg/dns"
"github.com/samber/lo"
)
const serverFailureCacheTTL uint32 = 5
func minimalTTL(records []D.RR) uint32 {
rr := lo.MinBy(records, func(r1 D.RR, r2 D.RR) bool {
return r1.Header().Ttl < r2.Header().Ttl
})
if rr == nil {
return 0
}
return rr.Header().Ttl
}
func updateTTL(records []D.RR, ttl uint32) {
if len(records) == 0 {
return
}
delta := minimalTTL(records) - ttl
for i := range records {
records[i].Header().Ttl = lo.Clamp(records[i].Header().Ttl-delta, 1, records[i].Header().Ttl)
}
}
func putMsgToCache(c *cache.LruCache, key string, q D.Question, msg *D.Msg) {
// skip dns cache for acme challenge
if q.Qtype == D.TypeTXT && strings.HasPrefix(q.Name, "_acme-challenge.") {
log.Debugln("[DNS] dns cache ignored because of acme challenge for: %s", q.Name)
return
}
var ttl uint32
if msg.Rcode == D.RcodeServerFailure {
// [...] a resolver MAY cache a server failure response.
// If it does so it MUST NOT cache it for longer than five (5) minutes [...]
ttl = serverFailureCacheTTL
} else {
ttl = minimalTTL(append(append(msg.Answer, msg.Ns...), msg.Extra...))
}
if ttl == 0 {
return
}
c.SetWithExpire(key, msg.Copy(), time.Now().Add(time.Duration(ttl)*time.Second))
}
func setMsgTTL(msg *D.Msg, ttl uint32) {
for _, answer := range msg.Answer {
answer.Header().Ttl = ttl
}
for _, ns := range msg.Ns {
ns.Header().Ttl = ttl
}
for _, extra := range msg.Extra {
extra.Header().Ttl = ttl
}
}
func updateMsgTTL(msg *D.Msg, ttl uint32) {
updateTTL(msg.Answer, ttl)
updateTTL(msg.Ns, ttl)
updateTTL(msg.Extra, ttl)
}
func isIPRequest(q D.Question) bool {
return q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA)
}
func transform(servers []NameServer, resolver *Resolver) []dnsClient {
ret := []dnsClient{}
for _, s := range servers {
switch s.Net {
case "https":
ret = append(ret, newDoHClient(s.Addr, s.Interface, resolver))
continue
case "dhcp":
ret = append(ret, newDHCPClient(s.Addr))
continue
}
host, port, _ := net.SplitHostPort(s.Addr)
ret = append(ret, &client{
Client: &D.Client{
Net: s.Net,
TLSConfig: &tls.Config{
ServerName: host,
},
UDPSize: 4096,
Timeout: 5 * time.Second,
},
port: port,
host: host,
iface: s.Interface,
r: resolver,
})
}
return ret
}
func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg {
msg := &D.Msg{}
msg.Answer = []D.RR{}
msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true
msg.RecursionAvailable = true
return msg
}
func msgToIP(msg *D.Msg) []net.IP {
ips := []net.IP{}
for _, answer := range msg.Answer {
switch ans := answer.(type) {
case *D.AAAA:
ips = append(ips, ans.AAAA)
case *D.A:
ips = append(ips, ans.A)
}
}
return ips
}
func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) {
fast, ctx := picker.WithContext(ctx)
for _, client := range clients {
r := client
fast.Go(func() (any, error) {
m, err := r.ExchangeContext(ctx, m)
if err != nil {
return nil, err
} else if m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused {
return nil, errors.New("server failure")
}
return m, nil
})
}
elm := fast.Wait()
if elm == nil {
err := errors.New("all DNS requests failed")
if fErr := fast.Error(); fErr != nil {
err = fmt.Errorf("%w, first error: %s", err, fErr.Error())
}
return nil, err
}
msg = elm.(*D.Msg)
return
}