Merge branch 'develop' into feature/ui-revamp
This commit is contained in:
@@ -32,8 +32,8 @@ var (
|
||||
// `dot://1.0.0.2:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip`,
|
||||
|
||||
// AdGuard (encrypted DNS, default flavor)
|
||||
// `dot://176.103.130.130:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip`,
|
||||
// `dot://176.103.130.131:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip`,
|
||||
// `dot://94.140.14.14:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip`,
|
||||
// `dot://94.140.15.15:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip`,
|
||||
|
||||
// Foundation for Applied Privacy (encrypted DNS)
|
||||
// `dot://94.130.106.88:853?verify=dot1.applied-privacy.net&name=AppliedPrivacy`,
|
||||
@@ -48,8 +48,8 @@ var (
|
||||
// `dns://1.0.0.2:53?name=Cloudflare&blockedif=zeroip`,
|
||||
|
||||
// AdGuard (plain DNS, default flavor)
|
||||
// `dns://176.103.130.130&name=AdGuard&blockedif=zeroip`,
|
||||
// `dns://176.103.130.131&name=AdGuard&blockedif=zeroip`,
|
||||
// `dns://94.140.14.14&name=AdGuard&blockedif=zeroip`,
|
||||
// `dns://94.140.15.15&name=AdGuard&blockedif=zeroip`,
|
||||
}
|
||||
|
||||
CfgOptionNameServersKey = "dns/nameservers"
|
||||
@@ -96,7 +96,7 @@ IP:
|
||||
always use the IP address and _not_ the domain name!
|
||||
|
||||
Port:
|
||||
always add the port!
|
||||
optionally define a custom port
|
||||
|
||||
Parameters:
|
||||
name: give your DNS Server a name that is used for messages and logs
|
||||
|
||||
@@ -28,6 +28,7 @@ type NameRecord struct {
|
||||
|
||||
Domain string
|
||||
Question string
|
||||
RCode int
|
||||
Answer []string
|
||||
Ns []string
|
||||
Extra []string
|
||||
@@ -35,6 +36,7 @@ type NameRecord struct {
|
||||
|
||||
Server string
|
||||
ServerScope int8
|
||||
ServerInfo string
|
||||
}
|
||||
|
||||
func makeNameRecordKey(domain string, question string) string {
|
||||
@@ -85,48 +87,13 @@ func (rec *NameRecord) Save() error {
|
||||
return recordDatabase.PutNew(rec)
|
||||
}
|
||||
|
||||
func clearNameCache(_ context.Context, _ interface{}) error {
|
||||
log.Debugf("resolver: name cache clearing started...")
|
||||
for {
|
||||
done, err := removeNameEntries(10000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeNameEntries(batchSize int) (bool, error) {
|
||||
iter, err := recordDatabase.Query(query.New(nameRecordsKeyPrefix))
|
||||
func clearNameCache(ctx context.Context, _ interface{}) error {
|
||||
log.Debugf("resolver: dns cache clearing started...")
|
||||
n, err := recordDatabase.Purge(ctx, query.New(nameRecordsKeyPrefix))
|
||||
if err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
|
||||
keys := make([]string, 0, batchSize)
|
||||
|
||||
var cnt int
|
||||
for r := range iter.Next {
|
||||
cnt++
|
||||
keys = append(keys, r.Key())
|
||||
|
||||
if cnt == batchSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
iter.Cancel()
|
||||
|
||||
for _, key := range keys {
|
||||
if err := recordDatabase.Delete(key); err != nil {
|
||||
log.Warningf("resolver: failed to remove name cache entry %q: %s", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("resolver: successfully removed %d name cache entries", cnt)
|
||||
|
||||
// if we removed less entries that the batch size we
|
||||
// are done and no more entries exist
|
||||
return cnt < batchSize, nil
|
||||
log.Debugf("resolver: cleared %d entries in dns cache", n)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
minTTL = 60 // 1 Minute
|
||||
minTTL = 60 // 1 Minute
|
||||
refreshTTL = minTTL / 2
|
||||
minMDnsTTL = 60 // 1 Minute
|
||||
maxTTL = 24 * 60 * 60 // 24 hours
|
||||
)
|
||||
@@ -120,6 +121,9 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
|
||||
}
|
||||
|
||||
// log
|
||||
// try adding a context tracer
|
||||
ctx, tracer := log.AddTracer(ctx)
|
||||
defer tracer.Submit()
|
||||
log.Tracer(ctx).Tracef("resolver: resolving %s%s", q.FQDN, q.QType)
|
||||
|
||||
// check query compliance
|
||||
@@ -130,8 +134,7 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
|
||||
// check the cache
|
||||
if !q.NoCaching {
|
||||
rrCache = checkCache(ctx, q)
|
||||
if rrCache != nil {
|
||||
rrCache.MixAnswers()
|
||||
if rrCache != nil && !rrCache.Expired() {
|
||||
return rrCache, nil
|
||||
}
|
||||
|
||||
@@ -140,8 +143,7 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
|
||||
if markRequestFinished == nil {
|
||||
// we waited for another request, recheck the cache!
|
||||
rrCache = checkCache(ctx, q)
|
||||
if rrCache != nil {
|
||||
rrCache.MixAnswers()
|
||||
if rrCache != nil && !rrCache.Expired() {
|
||||
return rrCache, nil
|
||||
}
|
||||
log.Tracer(ctx).Debugf("resolver: waited for another %s%s query, but cache missed!", q.FQDN, q.QType)
|
||||
@@ -149,17 +151,22 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
|
||||
} else {
|
||||
// we are the first!
|
||||
defer markRequestFinished()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return resolveAndCache(ctx, q)
|
||||
return resolveAndCache(ctx, q, rrCache)
|
||||
}
|
||||
|
||||
func checkCache(ctx context.Context, q *Query) *RRCache {
|
||||
// Never ask cache for connectivity domains.
|
||||
if netenv.IsConnectivityDomain(q.FQDN) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get data from cache.
|
||||
rrCache, err := GetRRCache(q.FQDN, q.QType)
|
||||
|
||||
// failed to get from cache
|
||||
// Return if entry is not in cache.
|
||||
if err != nil {
|
||||
if err != database.ErrNotFound {
|
||||
log.Tracer(ctx).Warningf("resolver: getting RRCache %s%s from database failed: %s", q.FQDN, q.QType.String(), err)
|
||||
@@ -167,21 +174,21 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get resolver that rrCache was resolved with
|
||||
// Get the resolver that the rrCache was resolved with.
|
||||
resolver := getActiveResolverByIDWithLocking(rrCache.Server)
|
||||
if resolver == nil {
|
||||
log.Tracer(ctx).Debugf("resolver: ignoring RRCache %s%s because source server %s has been removed", q.FQDN, q.QType.String(), rrCache.Server)
|
||||
return nil
|
||||
}
|
||||
|
||||
// check compliance of resolver
|
||||
// Check compliance of the resolver, return if non-compliant.
|
||||
err = resolver.checkCompliance(ctx, q)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Debugf("resolver: cached entry for %s%s does not comply to query parameters: %s", q.FQDN, q.QType.String(), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if we want to reset the cache
|
||||
// Check if we want to reset the cache for this entry.
|
||||
if shouldResetCache(q) {
|
||||
err := DeleteNameRecord(q.FQDN, q.QType.String())
|
||||
switch {
|
||||
@@ -195,27 +202,39 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if expired
|
||||
// Check if the cache has already expired.
|
||||
// We still return the cache, if it isn't NXDomain, as it will be used if the
|
||||
// new query fails.
|
||||
if rrCache.Expired() {
|
||||
if netenv.IsConnectivityDomain(rrCache.Domain) {
|
||||
// do not use cache, resolve immediately
|
||||
return nil
|
||||
if rrCache.RCode == dns.RcodeSuccess {
|
||||
return rrCache
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the cache will expire soon and start an async request.
|
||||
if rrCache.ExpiresSoon() {
|
||||
// Set flag that we are refreshing this entry.
|
||||
rrCache.Lock()
|
||||
rrCache.requestingNew = true
|
||||
rrCache.Unlock()
|
||||
|
||||
log.Tracer(ctx).Tracef(
|
||||
"resolver: using expired RR from cache (since %s), refreshing async now",
|
||||
time.Since(time.Unix(rrCache.TTL, 0)),
|
||||
"resolver: cache for %s will expire in %s, refreshing async now",
|
||||
q.ID(),
|
||||
time.Until(time.Unix(rrCache.TTL, 0)).Round(time.Second),
|
||||
)
|
||||
|
||||
// resolve async
|
||||
module.StartWorker("resolve async", func(ctx context.Context) error {
|
||||
_, err := resolveAndCache(ctx, q)
|
||||
ctx, tracer := log.AddTracer(ctx)
|
||||
defer tracer.Submit()
|
||||
tracer.Debugf("resolver: resolving %s async", q.ID())
|
||||
_, err := resolveAndCache(ctx, q, nil)
|
||||
if err != nil {
|
||||
log.Warningf("resolver: async query for %s%s failed: %s", q.FQDN, q.QType, err)
|
||||
tracer.Warningf("resolver: async query for %s failed: %s", q.ID(), err)
|
||||
} else {
|
||||
tracer.Debugf("resolver: async query for %s succeeded", q.ID())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -225,7 +244,7 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
|
||||
|
||||
log.Tracer(ctx).Tracef(
|
||||
"resolver: using cached RR (expires in %s)",
|
||||
time.Until(time.Unix(rrCache.TTL, 0)),
|
||||
time.Until(time.Unix(rrCache.TTL, 0)).Round(time.Second),
|
||||
)
|
||||
return rrCache
|
||||
}
|
||||
@@ -290,7 +309,7 @@ retry:
|
||||
}
|
||||
}
|
||||
|
||||
func resolveAndCache(ctx context.Context, q *Query) (rrCache *RRCache, err error) { //nolint:gocognit
|
||||
func resolveAndCache(ctx context.Context, q *Query, oldCache *RRCache) (rrCache *RRCache, err error) { //nolint:gocognit,gocyclo
|
||||
// get resolvers
|
||||
resolvers, tryAll := GetResolversInScope(ctx, q)
|
||||
if len(resolvers) == 0 {
|
||||
@@ -358,31 +377,51 @@ resolveLoop:
|
||||
// Defensive: This should normally not happen.
|
||||
continue
|
||||
}
|
||||
// Check if request succeeded and whether we should try another resolver.
|
||||
if rrCache.RCode != dns.RcodeSuccess && tryAll {
|
||||
continue
|
||||
}
|
||||
break resolveLoop
|
||||
}
|
||||
}
|
||||
|
||||
// check for error
|
||||
// Post-process errors
|
||||
if err != nil {
|
||||
// tried all resolvers, possibly twice
|
||||
if i > 1 {
|
||||
return nil, fmt.Errorf("all %d query-compliant resolvers failed, last error: %s", len(resolvers), err)
|
||||
err = fmt.Errorf("all %d query-compliant resolvers failed, last error: %s", len(resolvers), err)
|
||||
}
|
||||
} else if rrCache == nil /* defensive */ {
|
||||
err = ErrNotFound
|
||||
}
|
||||
|
||||
// Check if we want to use an older cache instead.
|
||||
if oldCache != nil {
|
||||
oldCache.isBackup = true
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
// There was an error during resolving, return the old cache entry instead.
|
||||
log.Tracer(ctx).Debugf("resolver: serving backup cache of %s because query failed: %s", q.ID(), err)
|
||||
return oldCache, nil
|
||||
case !rrCache.Cacheable():
|
||||
// The new result is NXDomain, return the old cache entry instead.
|
||||
log.Tracer(ctx).Debugf("resolver: serving backup cache of %s because fresh response is NXDomain", q.ID())
|
||||
return oldCache, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Return error, if there is one.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check for result
|
||||
if rrCache == nil /* defensive */ {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// cache if enabled
|
||||
if !q.NoCaching {
|
||||
// persist to database
|
||||
// Save the new entry if cache is enabled.
|
||||
if !q.NoCaching && rrCache.Cacheable() {
|
||||
rrCache.Clean(minTTL)
|
||||
err = rrCache.Save()
|
||||
if err != nil {
|
||||
log.Warningf("resolver: failed to cache RR for %s%s: %s", q.FQDN, q.QType.String(), err)
|
||||
log.Tracer(ctx).Warningf("resolver: failed to cache RR for %s: %s", q.ID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ var (
|
||||
Server: ServerSourceEnv,
|
||||
ServerType: ServerTypeEnv,
|
||||
ServerIPScope: netutils.SiteLocal,
|
||||
ServerInfo: "Portmaster environment",
|
||||
Source: ServerSourceEnv,
|
||||
Conn: &envResolverConn{},
|
||||
}
|
||||
@@ -110,10 +111,12 @@ func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache {
|
||||
return &RRCache{
|
||||
Domain: q.FQDN,
|
||||
Question: q.QType,
|
||||
RCode: dns.RcodeSuccess,
|
||||
Answer: answers,
|
||||
Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD.
|
||||
Server: envResolver.Server,
|
||||
ServerScope: envResolver.ServerIPScope,
|
||||
ServerInfo: envResolver.ServerInfo,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ var (
|
||||
Server: ServerSourceMDNS,
|
||||
ServerType: ServerTypeDNS,
|
||||
ServerIPScope: netutils.SiteLocal,
|
||||
ServerInfo: "mDNS resolver",
|
||||
Source: ServerSourceMDNS,
|
||||
Conn: &mDNSResolverConn{},
|
||||
}
|
||||
@@ -201,8 +202,10 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
|
||||
rrCache = &RRCache{
|
||||
Domain: question.Name,
|
||||
Question: dns.Type(question.Qtype),
|
||||
RCode: dns.RcodeSuccess,
|
||||
Server: mDNSResolver.Server,
|
||||
ServerScope: mDNSResolver.ServerIPScope,
|
||||
ServerInfo: mDNSResolver.ServerInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,9 +304,11 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
|
||||
rrCache = &RRCache{
|
||||
Domain: v.Header().Name,
|
||||
Question: dns.Type(v.Header().Class),
|
||||
RCode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{v},
|
||||
Server: mDNSResolver.Server,
|
||||
ServerScope: mDNSResolver.ServerIPScope,
|
||||
ServerInfo: mDNSResolver.ServerInfo,
|
||||
}
|
||||
rrCache.Clean(minMDnsTTL)
|
||||
err := rrCache.Save()
|
||||
@@ -416,7 +421,15 @@ func queryMulticastDNS(ctx context.Context, q *Query) (*RRCache, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrNotFound
|
||||
// Respond with NXDomain.
|
||||
return &RRCache{
|
||||
Domain: q.FQDN,
|
||||
Question: q.QType,
|
||||
RCode: dns.RcodeNameError,
|
||||
Server: mDNSResolver.Server,
|
||||
ServerScope: mDNSResolver.ServerIPScope,
|
||||
ServerInfo: mDNSResolver.ServerInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func cleanSavedQuestions() {
|
||||
|
||||
@@ -81,11 +81,13 @@ func (pr *PlainResolver) Query(ctx context.Context, q *Query) (*RRCache, error)
|
||||
newRecord := &RRCache{
|
||||
Domain: q.FQDN,
|
||||
Question: q.QType,
|
||||
RCode: reply.Rcode,
|
||||
Answer: reply.Answer,
|
||||
Ns: reply.Ns,
|
||||
Extra: reply.Extra,
|
||||
Server: pr.resolver.Server,
|
||||
ServerScope: pr.resolver.ServerIPScope,
|
||||
ServerInfo: pr.resolver.ServerInfo,
|
||||
}
|
||||
|
||||
// TODO: check if reply.Answer is valid
|
||||
|
||||
@@ -3,6 +3,8 @@ package resolver
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -26,6 +28,8 @@ type TCPResolver struct {
|
||||
dnsClient *dns.Client
|
||||
|
||||
clientStarted *abool.AtomicBool
|
||||
clientHeartbeat chan struct{}
|
||||
clientCancel func()
|
||||
connInstanceID *uint32
|
||||
queries chan *dns.Msg
|
||||
inFlightQueries map[uint16]*InFlightQuery
|
||||
@@ -46,11 +50,13 @@ func (ifq *InFlightQuery) MakeCacheRecord(reply *dns.Msg) *RRCache {
|
||||
return &RRCache{
|
||||
Domain: ifq.Query.FQDN,
|
||||
Question: ifq.Query.QType,
|
||||
RCode: reply.Rcode,
|
||||
Answer: reply.Answer,
|
||||
Ns: reply.Ns,
|
||||
Extra: reply.Extra,
|
||||
Server: ifq.Resolver.Server,
|
||||
ServerScope: ifq.Resolver.ServerIPScope,
|
||||
ServerInfo: ifq.Resolver.ServerInfo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +73,12 @@ func NewTCPResolver(resolver *Resolver) *TCPResolver {
|
||||
Timeout: defaultConnectTimeout,
|
||||
WriteTimeout: tcpWriteTimeout,
|
||||
},
|
||||
clientStarted: abool.New(),
|
||||
clientHeartbeat: make(chan struct{}),
|
||||
clientCancel: func() {},
|
||||
connInstanceID: &instanceID,
|
||||
queries: make(chan *dns.Msg, 100),
|
||||
inFlightQueries: make(map[uint16]*InFlightQuery),
|
||||
clientStarted: abool.New(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +153,7 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
|
||||
// submit to client
|
||||
inFlight := tr.submitQuery(ctx, q)
|
||||
if inFlight == nil {
|
||||
tr.checkClientStatus()
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
|
||||
@@ -152,6 +161,7 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
|
||||
select {
|
||||
case reply = <-inFlight.Response:
|
||||
case <-time.After(defaultRequestTimeout):
|
||||
tr.checkClientStatus()
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
|
||||
@@ -167,6 +177,22 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
|
||||
return inFlight.MakeCacheRecord(reply), nil
|
||||
}
|
||||
|
||||
func (tr *TCPResolver) checkClientStatus() {
|
||||
// Get client cancel function before waiting in order to not immediately
|
||||
// cancel a new client.
|
||||
tr.Lock()
|
||||
cancelClient := tr.clientCancel
|
||||
tr.Unlock()
|
||||
|
||||
// Check if the client is alive with the heartbeat, if not shut it down.
|
||||
select {
|
||||
case tr.clientHeartbeat <- struct{}{}:
|
||||
case <-time.After(defaultRequestTimeout):
|
||||
log.Warningf("resolver: heartbeat failed for %s dns client, stopping", tr.resolver.GetName())
|
||||
cancelClient()
|
||||
}
|
||||
}
|
||||
|
||||
type tcpResolverConnMgr struct {
|
||||
tr *TCPResolver
|
||||
responses chan *dns.Msg
|
||||
@@ -184,8 +210,14 @@ func (tr *TCPResolver) startClient() {
|
||||
}
|
||||
|
||||
func (mgr *tcpResolverConnMgr) run(workerCtx context.Context) error {
|
||||
mgr.tr.clientStarted.Set()
|
||||
defer mgr.shutdown()
|
||||
mgr.tr.clientStarted.Set()
|
||||
|
||||
// Create additional cancel function for this worker.
|
||||
workerCtx, cancelWorker := context.WithCancel(workerCtx)
|
||||
mgr.tr.Lock()
|
||||
mgr.tr.clientCancel = cancelWorker
|
||||
mgr.tr.Unlock()
|
||||
|
||||
// connection lifecycle loop
|
||||
for {
|
||||
@@ -208,7 +240,7 @@ func (mgr *tcpResolverConnMgr) run(workerCtx context.Context) error {
|
||||
}
|
||||
|
||||
// create connection
|
||||
conn, connClosing, connCtx, cancelConnCtx := mgr.establishConnection(workerCtx)
|
||||
conn, connClosing, connCtx, cancelConnCtx := mgr.establishConnection()
|
||||
if conn == nil {
|
||||
mgr.failCnt++
|
||||
continue
|
||||
@@ -293,7 +325,7 @@ func (mgr *tcpResolverConnMgr) waitForWork(workerCtx context.Context) (proceed b
|
||||
return true
|
||||
}
|
||||
|
||||
func (mgr *tcpResolverConnMgr) establishConnection(workerCtx context.Context) (
|
||||
func (mgr *tcpResolverConnMgr) establishConnection() (
|
||||
conn *dns.Conn,
|
||||
connClosing *abool.AtomicBool,
|
||||
connCtx context.Context,
|
||||
@@ -313,10 +345,21 @@ func (mgr *tcpResolverConnMgr) establishConnection(workerCtx context.Context) (
|
||||
log.Debugf("resolver: failed to connect to %s (%s)", mgr.tr.resolver.GetName(), mgr.tr.resolver.ServerAddress)
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
connCtx, cancelConnCtx = context.WithCancel(workerCtx)
|
||||
connCtx, cancelConnCtx = context.WithCancel(context.Background())
|
||||
connClosing = abool.New()
|
||||
|
||||
log.Debugf("resolver: connected to %s (%s)", mgr.tr.resolver.GetName(), conn.RemoteAddr())
|
||||
// Get amount of in waiting queries.
|
||||
mgr.tr.Lock()
|
||||
waitingQueries := len(mgr.tr.inFlightQueries)
|
||||
mgr.tr.Unlock()
|
||||
|
||||
// Log that a connection to the resolver was established.
|
||||
log.Debugf(
|
||||
"resolver: connected to %s (%s) with %d queries waiting",
|
||||
mgr.tr.resolver.GetName(),
|
||||
conn.RemoteAddr(),
|
||||
waitingQueries,
|
||||
)
|
||||
|
||||
// start reader
|
||||
module.StartServiceWorker("dns client reader", 10*time.Millisecond, func(workerCtx context.Context) error {
|
||||
@@ -348,6 +391,9 @@ func (mgr *tcpResolverConnMgr) queryHandler( //nolint:golint // context.Context
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mgr.tr.clientHeartbeat:
|
||||
// respond to alive checks
|
||||
|
||||
case <-workerCtx.Done():
|
||||
// module shutdown
|
||||
return false
|
||||
@@ -372,9 +418,7 @@ func (mgr *tcpResolverConnMgr) queryHandler( //nolint:golint // context.Context
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(mgr.tr.dnsClient.WriteTimeout))
|
||||
err := conn.WriteMsg(msg)
|
||||
if err != nil {
|
||||
if connClosing.SetToIf(false, true) {
|
||||
log.Warningf("resolver: write error to %s (%s): %s", mgr.tr.resolver.GetName(), conn.RemoteAddr(), err)
|
||||
}
|
||||
mgr.logConnectionError(err, conn, connClosing)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -434,6 +478,10 @@ func (mgr *tcpResolverConnMgr) handleQueryResponse(conn *dns.Conn, msg *dns.Msg)
|
||||
|
||||
// persist to database
|
||||
rrCache := inFlight.MakeCacheRecord(msg)
|
||||
if !rrCache.Cacheable() {
|
||||
return
|
||||
}
|
||||
|
||||
rrCache.Clean(minTTL)
|
||||
err := rrCache.Save()
|
||||
if err != nil {
|
||||
@@ -455,11 +503,37 @@ func (mgr *tcpResolverConnMgr) msgReader(
|
||||
for {
|
||||
msg, err := conn.ReadMsg()
|
||||
if err != nil {
|
||||
if connClosing.SetToIf(false, true) {
|
||||
log.Warningf("resolver: read error from %s (%s): %s", mgr.tr.resolver.GetName(), conn.RemoteAddr(), err)
|
||||
}
|
||||
mgr.logConnectionError(err, conn, connClosing)
|
||||
return nil
|
||||
}
|
||||
mgr.responses <- msg
|
||||
}
|
||||
}
|
||||
|
||||
func (mgr *tcpResolverConnMgr) logConnectionError(err error, conn *dns.Conn, connClosing *abool.AtomicBool) {
|
||||
// Check if we are the first to see an error.
|
||||
if connClosing.SetToIf(false, true) {
|
||||
// Get amount of in flight queries.
|
||||
mgr.tr.Lock()
|
||||
inFlightQueries := len(mgr.tr.inFlightQueries)
|
||||
mgr.tr.Unlock()
|
||||
|
||||
// Log error.
|
||||
if errors.Is(err, io.EOF) {
|
||||
log.Debugf(
|
||||
"resolver: connection to %s (%s) was closed with %d in-flight queries",
|
||||
mgr.tr.resolver.GetName(),
|
||||
conn.RemoteAddr(),
|
||||
inFlightQueries,
|
||||
)
|
||||
} else {
|
||||
log.Warningf(
|
||||
"resolver: write error to %s (%s) with %d in-flight queries: %s",
|
||||
mgr.tr.resolver.GetName(),
|
||||
conn.RemoteAddr(),
|
||||
inFlightQueries,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ type Resolver struct {
|
||||
ServerIP net.IP
|
||||
ServerIPScope int8
|
||||
ServerPort uint16
|
||||
ServerInfo string
|
||||
|
||||
// Special Options
|
||||
VerifyDomain string
|
||||
|
||||
@@ -90,6 +90,16 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
|
||||
return nil, false, fmt.Errorf("invalid resolver IP")
|
||||
}
|
||||
|
||||
// Add default port for scheme if it is missing.
|
||||
if u.Port() == "" {
|
||||
switch u.Scheme {
|
||||
case ServerTypeDNS, ServerTypeTCP:
|
||||
u.Host += ":53"
|
||||
case ServerTypeDoT:
|
||||
u.Host += ":853"
|
||||
}
|
||||
}
|
||||
|
||||
scope := netutils.ClassifyIP(ip)
|
||||
if scope == netutils.HostLocal {
|
||||
return nil, true, nil // skip
|
||||
@@ -128,6 +138,13 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
|
||||
UpstreamBlockDetection: blockType,
|
||||
}
|
||||
|
||||
u.RawQuery = "" // Remove options from parsed URL
|
||||
if new.Name != "" {
|
||||
new.ServerInfo = fmt.Sprintf("%s (%s, from %s)", new.Name, u, source)
|
||||
} else {
|
||||
new.ServerInfo = fmt.Sprintf("%s (from %s)", u, source)
|
||||
}
|
||||
|
||||
new.Conn = resolverConnFactory(new)
|
||||
return new, false, nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func ResolveIPAndValidate(ctx context.Context, ip string, securityLevel uint8) (
|
||||
// get reversed DNS address
|
||||
reverseIP, err := dns.ReverseAddr(ip)
|
||||
if err != nil {
|
||||
log.Tracef("resolver: failed to get reverse address of %s: %s", ip, err)
|
||||
log.Tracer(ctx).Tracef("resolver: failed to get reverse address of %s: %s", ip, err)
|
||||
return "", ErrInvalid
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/nameserver/nsutil"
|
||||
"github.com/safing/portmaster/netenv"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
@@ -20,17 +22,20 @@ type RRCache struct {
|
||||
|
||||
Domain string // constant
|
||||
Question dns.Type // constant
|
||||
RCode int // constant
|
||||
|
||||
Answer []dns.RR // might be mixed
|
||||
Answer []dns.RR // constant
|
||||
Ns []dns.RR // constant
|
||||
Extra []dns.RR // constant
|
||||
TTL int64 // constant
|
||||
|
||||
Server string // constant
|
||||
ServerScope int8 // constant
|
||||
ServerInfo string // constant
|
||||
|
||||
servedFromCache bool // mutable
|
||||
requestingNew bool // mutable
|
||||
isBackup bool // mutable
|
||||
Filtered bool // mutable
|
||||
FilteredEntries []string // mutable
|
||||
|
||||
@@ -47,19 +52,16 @@ func (rrCache *RRCache) Expired() bool {
|
||||
return rrCache.TTL <= time.Now().Unix()
|
||||
}
|
||||
|
||||
// MixAnswers randomizes the answer records to allow dumb clients (who only look at the first record) to reliably connect.
|
||||
func (rrCache *RRCache) MixAnswers() {
|
||||
rrCache.Lock()
|
||||
defer rrCache.Unlock()
|
||||
|
||||
for i := range rrCache.Answer {
|
||||
j := rand.Intn(i + 1)
|
||||
rrCache.Answer[i], rrCache.Answer[j] = rrCache.Answer[j], rrCache.Answer[i]
|
||||
}
|
||||
// ExpiresSoon returns whether the record will expire soon and should already be refreshed.
|
||||
func (rrCache *RRCache) ExpiresSoon() bool {
|
||||
return rrCache.TTL <= time.Now().Unix()+refreshTTL
|
||||
}
|
||||
|
||||
// Clean sets all TTLs to 17 and sets cache expiry with specified minimum.
|
||||
func (rrCache *RRCache) Clean(minExpires uint32) {
|
||||
rrCache.Lock()
|
||||
defer rrCache.Unlock()
|
||||
|
||||
var lowestTTL uint32 = 0xFFFFFFFF
|
||||
var header *dns.RR_Header
|
||||
|
||||
@@ -83,8 +85,8 @@ func (rrCache *RRCache) Clean(minExpires uint32) {
|
||||
|
||||
// shorten caching
|
||||
switch {
|
||||
case rrCache.IsNXDomain():
|
||||
// NXDomain
|
||||
case rrCache.RCode != dns.RcodeSuccess:
|
||||
// Any sort of error.
|
||||
lowestTTL = 10
|
||||
case netenv.IsConnectivityDomain(rrCache.Domain):
|
||||
// Responses from these domains might change very quickly depending on the environment.
|
||||
@@ -126,9 +128,11 @@ func (rrCache *RRCache) ToNameRecord() *NameRecord {
|
||||
new := &NameRecord{
|
||||
Domain: rrCache.Domain,
|
||||
Question: rrCache.Question.String(),
|
||||
RCode: rrCache.RCode,
|
||||
TTL: rrCache.TTL,
|
||||
Server: rrCache.Server,
|
||||
ServerScope: rrCache.ServerScope,
|
||||
ServerInfo: rrCache.ServerInfo,
|
||||
}
|
||||
|
||||
// stringify RR entries
|
||||
@@ -145,8 +149,27 @@ func (rrCache *RRCache) ToNameRecord() *NameRecord {
|
||||
return new
|
||||
}
|
||||
|
||||
// rcodeIsCacheable returns whether a record with the given RCode should be cached.
|
||||
func rcodeIsCacheable(rCode int) bool {
|
||||
switch rCode {
|
||||
case dns.RcodeSuccess, dns.RcodeNameError, dns.RcodeRefused:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Cacheable returns whether the record should be cached.
|
||||
func (rrCache *RRCache) Cacheable() bool {
|
||||
return rcodeIsCacheable(rrCache.RCode)
|
||||
}
|
||||
|
||||
// Save saves the RRCache to the database as a NameRecord.
|
||||
func (rrCache *RRCache) Save() error {
|
||||
if !rrCache.Cacheable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return rrCache.ToNameRecord().Save()
|
||||
}
|
||||
|
||||
@@ -162,6 +185,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rrCache.RCode = nameRecord.RCode
|
||||
rrCache.TTL = nameRecord.TTL
|
||||
for _, entry := range nameRecord.Answer {
|
||||
rrCache.Answer = parseRR(rrCache.Answer, entry)
|
||||
@@ -175,6 +199,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
|
||||
|
||||
rrCache.Server = nameRecord.Server
|
||||
rrCache.ServerScope = nameRecord.ServerScope
|
||||
rrCache.ServerInfo = nameRecord.ServerInfo
|
||||
rrCache.servedFromCache = true
|
||||
return rrCache, nil
|
||||
}
|
||||
@@ -211,6 +236,9 @@ func (rrCache *RRCache) Flags() string {
|
||||
if rrCache.requestingNew {
|
||||
s += "R"
|
||||
}
|
||||
if rrCache.isBackup {
|
||||
s += "B"
|
||||
}
|
||||
if rrCache.Filtered {
|
||||
s += "F"
|
||||
}
|
||||
@@ -221,16 +249,12 @@ func (rrCache *RRCache) Flags() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsNXDomain returnes whether the result is nxdomain.
|
||||
func (rrCache *RRCache) IsNXDomain() bool {
|
||||
return len(rrCache.Answer) == 0
|
||||
}
|
||||
|
||||
// ShallowCopy returns a shallow copy of the cache. slices are not copied, but referenced.
|
||||
func (rrCache *RRCache) ShallowCopy() *RRCache {
|
||||
return &RRCache{
|
||||
Domain: rrCache.Domain,
|
||||
Question: rrCache.Question,
|
||||
RCode: rrCache.RCode,
|
||||
Answer: rrCache.Answer,
|
||||
Ns: rrCache.Ns,
|
||||
Extra: rrCache.Extra,
|
||||
@@ -238,11 +262,81 @@ func (rrCache *RRCache) ShallowCopy() *RRCache {
|
||||
|
||||
Server: rrCache.Server,
|
||||
ServerScope: rrCache.ServerScope,
|
||||
ServerInfo: rrCache.ServerInfo,
|
||||
|
||||
updated: rrCache.updated,
|
||||
servedFromCache: rrCache.servedFromCache,
|
||||
requestingNew: rrCache.requestingNew,
|
||||
isBackup: rrCache.isBackup,
|
||||
Filtered: rrCache.Filtered,
|
||||
FilteredEntries: rrCache.FilteredEntries,
|
||||
}
|
||||
}
|
||||
|
||||
// ReplyWithDNS creates a new reply to the given query with the data from the RRCache, and additional informational records.
|
||||
func (rrCache *RRCache) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||
// reply to query
|
||||
reply := new(dns.Msg)
|
||||
reply.SetRcode(request, rrCache.RCode)
|
||||
reply.Ns = rrCache.Ns
|
||||
reply.Extra = rrCache.Extra
|
||||
|
||||
if len(rrCache.Answer) > 0 {
|
||||
// Copy answers, as we randomize their order a little.
|
||||
reply.Answer = make([]dns.RR, len(rrCache.Answer))
|
||||
copy(reply.Answer, rrCache.Answer)
|
||||
|
||||
// Randomize the order of the answer records a little to allow dumb clients
|
||||
// (who only look at the first record) to reliably connect.
|
||||
for i := range reply.Answer {
|
||||
j := rand.Intn(i + 1)
|
||||
reply.Answer[i], reply.Answer[j] = reply.Answer[j], reply.Answer[i]
|
||||
}
|
||||
}
|
||||
|
||||
return reply
|
||||
}
|
||||
|
||||
// GetExtraRRs returns a slice of RRs with additional informational records.
|
||||
func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) {
|
||||
// Add cache status and source of data.
|
||||
if rrCache.servedFromCache {
|
||||
extra = addExtra(ctx, extra, "served from cache, resolved by "+rrCache.ServerInfo)
|
||||
} else {
|
||||
extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.ServerInfo)
|
||||
}
|
||||
|
||||
// Add expiry and cache information.
|
||||
if rrCache.Expired() {
|
||||
extra = addExtra(ctx, extra, fmt.Sprintf("record expired since %s", time.Since(time.Unix(rrCache.TTL, 0)).Round(time.Second)))
|
||||
} else {
|
||||
extra = addExtra(ctx, extra, fmt.Sprintf("record valid for %s", time.Until(time.Unix(rrCache.TTL, 0)).Round(time.Second)))
|
||||
}
|
||||
if rrCache.requestingNew {
|
||||
extra = addExtra(ctx, extra, "async request to refresh the cache has been started")
|
||||
}
|
||||
if rrCache.isBackup {
|
||||
extra = addExtra(ctx, extra, "this record is served because a fresh request failed")
|
||||
}
|
||||
|
||||
// Add information about filtered entries.
|
||||
if rrCache.Filtered {
|
||||
if len(rrCache.FilteredEntries) > 1 {
|
||||
extra = addExtra(ctx, extra, fmt.Sprintf("%d records have been filtered", len(rrCache.FilteredEntries)))
|
||||
} else {
|
||||
extra = addExtra(ctx, extra, fmt.Sprintf("%d record has been filtered", len(rrCache.FilteredEntries)))
|
||||
}
|
||||
}
|
||||
|
||||
return extra
|
||||
}
|
||||
|
||||
func addExtra(ctx context.Context, extra []dns.RR, msg string) []dns.RR {
|
||||
rr, err := nsutil.MakeMessageRecord(log.InfoLevel, msg)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Warningf("resolver: failed to add informational record to reply: %s", err)
|
||||
return extra
|
||||
}
|
||||
extra = append(extra, rr)
|
||||
return extra
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ addNextResolver:
|
||||
for _, resolver := range addResolvers {
|
||||
// check for compliance
|
||||
if err := resolver.checkCompliance(ctx, q); err != nil {
|
||||
log.Tracef("skipping non-compliant resolver %s: %s", resolver.GetName(), err)
|
||||
log.Tracer(ctx).Tracef("skipping non-compliant resolver %s: %s", resolver.GetName(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user