diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 635c2b94..167e668d 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -73,13 +73,6 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) return nil } - // Return with server failure if offline. - if netenv.GetOnlineStatus() == netenv.StatusOffline && - !netenv.IsConnectivityDomain(q.FQDN) { - tracer.Debugf("nameserver: not resolving %s, device is offline", q.FQDN) - return reply(nsutil.ServerFailure("resolving disabled, device is offline")) - } - // Check the Query Class. if originalQuestion.Qclass != dns.ClassINET { // we only serve IN records, return nxdomain @@ -186,6 +179,14 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) case errors.Is(err, resolver.ErrLocalhost): tracer.Tracef("nameserver: returning localhost records") return reply(nsutil.Localhost()) + case errors.Is(err, resolver.ErrOffline): + if rrCache == nil { + log.Tracer(ctx).Debugf("nameserver: not resolving %s, device is offline", q.ID()) + return reply(nsutil.ServerFailure(err.Error())) + } + // If an rrCache was returned, it's usable a backup. + rrCache.IsBackup = true + log.Tracer(ctx).Debugf("nameserver: device is offline, using backup cache for %s", q.ID()) default: tracer.Warningf("nameserver: failed to resolve %s: %s", q.ID(), err) return reply(nsutil.ServerFailure("internal error: " + err.Error())) diff --git a/netenv/online-status.go b/netenv/online-status.go index 882c56c3..db1d868e 100644 --- a/netenv/online-status.go +++ b/netenv/online-status.go @@ -321,11 +321,11 @@ func monitorOnlineStatus(ctx context.Context) error { func getDynamicStatusTrigger() <-chan time.Time { switch GetOnlineStatus() { case StatusOffline: - return time.After(5 * time.Second) + return time.After(1 * time.Second) case StatusLimited, StatusPortal: - return time.After(10 * time.Second) + return time.After(5 * time.Second) case StatusSemiOnline: - return time.After(1 * time.Minute) + return time.After(20 * time.Second) case StatusOnline: return nil case StatusUnknown: diff --git a/resolver/api.go b/resolver/api.go new file mode 100644 index 00000000..69700c48 --- /dev/null +++ b/resolver/api.go @@ -0,0 +1,49 @@ +package resolver + +import ( + "github.com/safing/portbase/api" +) + +func registerAPI() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "dns/clear", + Read: api.PermitUser, + ActionFunc: clearNameCache, + Name: "Clear cached DNS records", + Description: "Deletes all saved DNS records from the database.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "dns/resolvers", + Read: api.PermitAnyone, + StructFunc: exportDNSResolvers, + Name: "List DNS Resolvers", + Description: "List currently configured DNS resolvers and their status.", + }); err != nil { + return err + } + + return nil +} + +type resolverExport struct { + *Resolver + Failing bool +} + +func exportDNSResolvers(*api.Request) (interface{}, error) { + resolversLock.RLock() + defer resolversLock.RUnlock() + + export := make([]*resolverExport, 0, len(globalResolvers)) + for _, r := range globalResolvers { + export = append(export, &resolverExport{ + Resolver: r, + Failing: r.Conn.IsFailing(), + }) + } + + return export, nil +} diff --git a/resolver/main.go b/resolver/main.go index 343d5a50..bb1825c6 100644 --- a/resolver/main.go +++ b/resolver/main.go @@ -6,8 +6,6 @@ import ( "strings" "time" - "github.com/safing/portbase/api" - "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portmaster/intel" @@ -31,6 +29,10 @@ func init() { func prep() error { intel.SetReverseResolver(ResolveIPAndValidate) + if err := registerAPI(); err != nil { + return err + } + if err := prepEnvResolver(); err != nil { return err } @@ -78,15 +80,6 @@ func start() error { return err } - // Register api endpoint to clear DNS cache. - if err := api.RegisterEndpoint(api.Endpoint{ - Path: "dns/clear/namecache", - Read: api.PermitUser, - ActionFunc: clearNameCache, - }); err != nil { - return err - } - // DEPRECATED: remove in v0.7 // cache clearing err = module.RegisterEventHook( diff --git a/resolver/namerecord.go b/resolver/namerecord.go index 6c04d7be..08319353 100644 --- a/resolver/namerecord.go +++ b/resolver/namerecord.go @@ -88,6 +88,10 @@ func GetNameRecord(domain, question string) (*NameRecord, error) { // DeleteNameRecord deletes a NameRecord from the database. func DeleteNameRecord(domain, question string) error { + // In order to properly delete an entry, we must also clear the caches. + recordDatabase.FlushCache() + recordDatabase.ClearCache() + key := makeNameRecordKey(domain, question) return recordDatabase.Delete(key) } @@ -107,24 +111,30 @@ func (rec *NameRecord) Save() error { // clearNameCache clears all dns caches from the database. func clearNameCache(ar *api.Request) (msg string, err error) { - log.Warning("resolver: user requested dns cache clearing via action") + log.Info("resolver: user requested dns cache clearing via action") + recordDatabase.FlushCache() + recordDatabase.ClearCache() n, err := recordDatabase.Purge(ar.Context(), query.New(nameRecordsKeyPrefix)) if err != nil { return "", err } + log.Debugf("resolver: cleared %d entries from dns cache", n) return fmt.Sprintf("cleared %d dns cache entries", n), nil } // DEPRECATED: remove in v0.7 func clearNameCacheEventHandler(ctx context.Context, _ interface{}) error { log.Debugf("resolver: dns cache clearing started...") + + recordDatabase.FlushCache() + recordDatabase.ClearCache() n, err := recordDatabase.Purge(ctx, query.New(nameRecordsKeyPrefix)) if err != nil { return err } - log.Debugf("resolver: cleared %d entries in dns cache", n) + log.Debugf("resolver: cleared %d entries from dns cache", n) return nil } diff --git a/resolver/resolve.go b/resolver/resolve.go index 22f2fe23..631bcb03 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -317,9 +317,8 @@ func resolveAndCache(ctx context.Context, q *Query, oldCache *RRCache) (rrCache // check if we are online if netenv.GetOnlineStatus() == netenv.StatusOffline { if !netenv.IsConnectivityDomain(q.FQDN) { - log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN) // we are offline and this is not an online check query - return nil, ErrOffline + return oldCache, ErrOffline } log.Tracer(ctx).Debugf("resolver: permitting online status test domain %s to resolve even though offline", q.FQDN) } @@ -356,9 +355,8 @@ resolveLoop: return nil, err case netenv.GetOnlineStatus() == netenv.StatusOffline && !netenv.IsConnectivityDomain(q.FQDN): - log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN) // we are offline and this is not an online check query - return nil, ErrOffline + return oldCache, ErrOffline case errors.Is(err, ErrContinue): continue case errors.Is(err, ErrTimeout): diff --git a/resolver/resolver.go b/resolver/resolver.go index d7f21555..ad7f5741 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -41,6 +41,9 @@ type Resolver struct { // - `zeroip`: Answer only contains zeroip Server string + // Source describes from where the resolver configuration originated. + Source string + // Name is the name of the resolver as passed via // ?name=. Name string @@ -65,12 +68,9 @@ type Resolver struct { // Special Options VerifyDomain string Search []string - SkipFQDN string - - Source string // logic interface - Conn ResolverConn + Conn ResolverConn `json:"-"` } // IsBlockedUpstream returns true if the request has been blocked diff --git a/resolver/rrcache.go b/resolver/rrcache.go index a2af8f27..50e51545 100644 --- a/resolver/rrcache.go +++ b/resolver/rrcache.go @@ -324,7 +324,7 @@ func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra 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") + extra = addExtra(ctx, extra, "this record is served because a fresh request was unsuccessful") } // Add information about filtered entries. diff --git a/resolver/scopes.go b/resolver/scopes.go index 0943cf2b..f0102b17 100644 --- a/resolver/scopes.go +++ b/resolver/scopes.go @@ -208,10 +208,6 @@ func (q *Query) checkCompliance() error { } func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error { - if q.FQDN == resolver.SkipFQDN { - return errSkip - } - if noInsecureProtocols(q.SecurityLevel) { switch resolver.ServerType { case ServerTypeDNS: diff --git a/resolver/test/resolving.bash b/resolver/test/resolving.bash new file mode 100644 index 00000000..bddf936a --- /dev/null +++ b/resolver/test/resolving.bash @@ -0,0 +1,22 @@ +#!/bin/bash + +DOMAINS="twitter.com news.ycombinator.com" + +while true; do + for domain in $DOMAINS; do + # query domain + Q=$(dig $domain | tr '\n' '§') + # check result + if [[ $(echo $Q | grep NOERROR | wc -l) -gt 0 ]]; then + echo "$(date "+%y%m%d %H:%M:%S") [OK] $domain ($(echo $Q | grep -aoE 'valid for [a-z0-9]+'))" + else + echo "" + echo "$(date "+%y%m%d %H:%M:%S") [FAILED] $domain" + echo $Q | tr '§' '\n' + echo "#####" + echo "" + fi + # wait + sleep 5 + done +done