Merge pull request #662 from safing/fix/qol-fixes-in-compat-netenv-dns-broadcasting

QOL Fixes in compat, netenv, dns and broadcasting
This commit is contained in:
Daniel 2022-06-01 13:48:10 +02:00 committed by GitHub
commit 02779f8bfd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 662 additions and 272 deletions

View file

@ -26,6 +26,10 @@ var (
// selfcheckFails counts how often the self check failed successively. // selfcheckFails counts how often the self check failed successively.
// selfcheckFails is not locked as it is only accessed by the self-check task. // selfcheckFails is not locked as it is only accessed by the self-check task.
selfcheckFails int selfcheckFails int
// selfcheckNetworkChangedFlag is used to track changed to the network for
// the self-check.
selfcheckNetworkChangedFlag = netenv.GetNetworkChangedFlag()
) )
// selfcheckFailThreshold holds the threshold of how many times the selfcheck // selfcheckFailThreshold holds the threshold of how many times the selfcheck
@ -49,6 +53,7 @@ func prep() error {
func start() error { func start() error {
startNotify() startNotify()
selfcheckNetworkChangedFlag.Refresh()
selfcheckTask = module.NewTask("compatibility self-check", selfcheckTaskFunc). selfcheckTask = module.NewTask("compatibility self-check", selfcheckTaskFunc).
Repeat(5 * time.Minute). Repeat(5 * time.Minute).
MaxDelay(selfcheckTaskRetryAfter). MaxDelay(selfcheckTaskRetryAfter).
@ -83,19 +88,23 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error {
// Run selfcheck and return if successful. // Run selfcheck and return if successful.
issue, err := selfcheck(ctx) issue, err := selfcheck(ctx)
if err == nil { switch {
selfCheckIsFailing.UnSet() case err == nil:
selfcheckFails = 0 // Successful.
resetSystemIssue()
tracer.Debugf("compat: self-check successful") tracer.Debugf("compat: self-check successful")
return nil case issue == nil:
} // Internal error.
tracer.Warningf("compat: %s", err)
case selfcheckNetworkChangedFlag.IsSet():
// The network changed, ignore the issue.
default:
// The self-check failed.
// Log result. // Set state and increase counter.
if issue != nil {
selfCheckIsFailing.Set() selfCheckIsFailing.Set()
selfcheckFails++ selfcheckFails++
// Log and notify.
tracer.Errorf("compat: %s", err) tracer.Errorf("compat: %s", err)
if selfcheckFails >= selfcheckFailThreshold { if selfcheckFails >= selfcheckFailThreshold {
issue.notify(err) issue.notify(err)
@ -103,13 +112,15 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error {
// Retry quicker when failed. // Retry quicker when failed.
task.Schedule(time.Now().Add(selfcheckTaskRetryAfter)) task.Schedule(time.Now().Add(selfcheckTaskRetryAfter))
} else {
return nil
}
// Reset self-check state.
selfcheckNetworkChangedFlag.Refresh()
selfCheckIsFailing.UnSet() selfCheckIsFailing.UnSet()
selfcheckFails = 0 selfcheckFails = 0
resetSystemIssue()
// Only log internal errors, but don't notify.
tracer.Warningf("compat: %s", err)
}
return nil return nil
} }

View file

@ -2,8 +2,6 @@ package base
import ( import (
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
// Dependencies.
_ "github.com/safing/portbase/database/dbmodule" _ "github.com/safing/portbase/database/dbmodule"
_ "github.com/safing/portbase/database/storage/bbolt" _ "github.com/safing/portbase/database/storage/bbolt"
) )

View file

@ -2,9 +2,11 @@ package firewall
import ( import (
"github.com/safing/portbase/config" "github.com/safing/portbase/config"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"github.com/safing/portbase/modules/subsystems" "github.com/safing/portbase/modules/subsystems"
_ "github.com/safing/portmaster/core" _ "github.com/safing/portmaster/core"
"github.com/safing/portmaster/intel/filterlists"
"github.com/safing/spn/captain" "github.com/safing/spn/captain"
) )
@ -12,10 +14,13 @@ var (
filterModule *modules.Module filterModule *modules.Module
filterEnabled config.BoolOption filterEnabled config.BoolOption
tunnelEnabled config.BoolOption tunnelEnabled config.BoolOption
unbreakFilterListIDs = []string{"UNBREAK"}
resolvedUnbreakFilterListIDs []string
) )
func init() { func init() {
filterModule = modules.Register("filter", filterPrep, nil, nil, "core", "intel") filterModule = modules.Register("filter", filterPrep, filterStart, nil, "core", "intel")
subsystems.Register( subsystems.Register(
"filter", "filter",
"Privacy Filter", "Privacy Filter",
@ -47,3 +52,14 @@ func filterPrep() (err error) {
tunnelEnabled = config.Concurrent.GetAsBool(captain.CfgOptionEnableSPNKey, false) tunnelEnabled = config.Concurrent.GetAsBool(captain.CfgOptionEnableSPNKey, false)
return nil return nil
} }
func filterStart() error {
// TODO: Re-resolve IDs when filterlist index changes.
resolvedIDs, err := filterlists.ResolveListIDs(unbreakFilterListIDs)
if err != nil {
log.Warningf("filter: failed to resolve unbreak filter list IDs: %s", err)
} else {
resolvedUnbreakFilterListIDs = resolvedIDs
}
return nil
}

View file

@ -3,7 +3,9 @@ package firewall
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/agext/levenshtein" "github.com/agext/levenshtein"
@ -41,6 +43,7 @@ type deciderFn func(context.Context, *network.Connection, *profile.LayeredProfil
var defaultDeciders = []deciderFn{ var defaultDeciders = []deciderFn{
checkPortmasterConnection, checkPortmasterConnection,
checkSelfCommunication, checkSelfCommunication,
checkIfBroadcastReply,
checkConnectionType, checkConnectionType,
checkConnectionScope, checkConnectionScope,
checkEndpointLists, checkEndpointLists,
@ -182,6 +185,46 @@ func checkSelfCommunication(ctx context.Context, conn *network.Connection, _ *pr
return false return false
} }
func checkIfBroadcastReply(ctx context.Context, conn *network.Connection, _ *profile.LayeredProfile, _ packet.Packet) bool {
// Only check inbound connections.
if !conn.Inbound {
return false
}
// Only check if the process has been identified.
if !conn.Process().IsIdentified() {
return false
}
// Check if the remote IP is part of a local network.
localNet, err := netenv.GetLocalNetwork(conn.Entity.IP)
if err != nil {
log.Tracer(ctx).Warningf("filter: failed to get local network: %s", err)
return false
}
if localNet == nil {
return false
}
// Search for a matching requesting connection.
requestingConn := network.GetMulticastRequestConn(conn, localNet)
if requestingConn == nil {
return false
}
conn.Accept(
fmt.Sprintf(
"response to multi/broadcast query to %s/%s",
packet.IPProtocol(requestingConn.Entity.Protocol),
net.JoinHostPort(
requestingConn.Entity.IP.String(),
strconv.Itoa(int(requestingConn.Entity.Port)),
),
),
"",
)
return true
}
func checkEndpointLists(ctx context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool { func checkEndpointLists(ctx context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
// DNS request from the system resolver require a special decision process, // DNS request from the system resolver require a special decision process,
// because the original requesting process is not known. Here, we only check // because the original requesting process is not known. Here, we only check
@ -389,6 +432,16 @@ func checkFilterLists(ctx context.Context, conn *network.Connection, p *profile.
result, reason := p.MatchFilterLists(ctx, conn.Entity) result, reason := p.MatchFilterLists(ctx, conn.Entity)
switch result { switch result {
case endpoints.Denied: case endpoints.Denied:
// If the connection matches a filter list, check if the "unbreak" list matches too and abort blocking.
for _, blockedListID := range conn.Entity.BlockedByLists {
for _, unbreakListID := range resolvedUnbreakFilterListIDs {
if blockedListID == unbreakListID {
log.Tracer(ctx).Debugf("filter: unbreak filter %s matched, ignoring other filter list matches", unbreakListID)
return false
}
}
}
// Otherwise, continue with blocking.
conn.DenyWithContext(reason.String(), profile.CfgOptionFilterListsKey, reason.Context()) conn.DenyWithContext(reason.String(), profile.CfgOptionFilterListsKey, reason.Context())
return true return true
case endpoints.NoMatch: case endpoints.NoMatch:
@ -439,10 +492,7 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, p *pro
trimmedDomain := strings.TrimRight(conn.Entity.Domain, ".") trimmedDomain := strings.TrimRight(conn.Entity.Domain, ".")
etld1, err := publicsuffix.EffectiveTLDPlusOne(trimmedDomain) etld1, err := publicsuffix.EffectiveTLDPlusOne(trimmedDomain)
if err != nil { if err != nil {
// we don't apply any checks here and let the request through // Don't run the check if the domain is a TLD.
// because a malformed domain-name will likely be dropped by
// checks better suited for that.
log.Tracer(ctx).Warningf("filter: failed to get eTLD+1: %s", err)
return false return false
} }

View file

@ -34,7 +34,7 @@ func checkTunneling(ctx context.Context, conn *network.Connection, pkt packet.Pa
case conn.Process().Pid == ownPID: case conn.Process().Pid == ownPID:
// Bypass tunneling for certain own connections. // Bypass tunneling for certain own connections.
switch { switch {
case captain.ClientBootstrapping(): case !captain.ClientReady():
return return
case captain.IsExcepted(conn.Entity.IP): case captain.IsExcepted(conn.Entity.IP):
return return
@ -42,10 +42,10 @@ func checkTunneling(ctx context.Context, conn *network.Connection, pkt packet.Pa
} }
// Check more extensively for Local/LAN connections. // Check more extensively for Local/LAN connections.
myNet, err := netenv.IsMyNet(conn.Entity.IP) localNet, err := netenv.GetLocalNetwork(conn.Entity.IP)
if err != nil { if err != nil {
log.Warningf("firewall: failed to check if %s is in my net: %s", conn.Entity.IP, err) log.Warningf("firewall: failed to check if %s is in my net: %s", conn.Entity.IP, err)
} else if myNet { } else if localNet != nil {
// With IPv6, just checking the IP scope is not enough, as the host very // With IPv6, just checking the IP scope is not enough, as the host very
// likely has a public IPv6 address. // likely has a public IPv6 address.
// Don't tunnel LAN connections. // Don't tunnel LAN connections.

75
nameserver/conflict.go Normal file
View file

@ -0,0 +1,75 @@
package nameserver
import (
"net"
"os"
processInfo "github.com/shirou/gopsutil/process"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/network/state"
)
var commonResolverIPs = []net.IP{
net.IPv4zero,
net.IPv4(127, 0, 0, 1), // default
net.IPv4(127, 0, 0, 53), // some resolvers on Linux
net.IPv6zero,
net.IPv6loopback,
}
func findConflictingProcess(ip net.IP, port uint16) (conflictingProcess *processInfo.Process) {
// Evaluate which IPs to check.
var ipsToCheck []net.IP
if ip.Equal(net.IPv4zero) || ip.Equal(net.IPv6zero) {
ipsToCheck = commonResolverIPs
} else {
ipsToCheck = []net.IP{ip}
}
// Find the conflicting process.
var err error
for _, resolverIP := range ipsToCheck {
conflictingProcess, err = getListeningProcess(resolverIP, port)
switch {
case err != nil:
// Log the error and let the worker try again.
log.Warningf("nameserver: failed to find conflicting service: %s", err)
case conflictingProcess != nil:
// Conflicting service found.
return conflictingProcess
}
}
return nil
}
func getListeningProcess(resolverIP net.IP, resolverPort uint16) (*processInfo.Process, error) {
pid, _, err := state.Lookup(&packet.Info{
Inbound: true,
Version: 0, // auto-detect
Protocol: packet.UDP,
Src: nil, // do not record direction
SrcPort: 0, // do not record direction
Dst: resolverIP,
DstPort: resolverPort,
}, true)
if err != nil {
// there may be nothing listening on :53
return nil, nil //nolint:nilerr // Treat lookup error as "not found".
}
// Ignore if it's us for some reason.
if pid == os.Getpid() {
return nil, nil
}
proc, err := processInfo.NewProcess(int32(pid))
if err != nil {
// Process may have disappeared already.
return nil, err
}
return proc, nil
}

View file

@ -13,6 +13,7 @@ import (
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"github.com/safing/portbase/modules/subsystems" "github.com/safing/portbase/modules/subsystems"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/compat" "github.com/safing/portmaster/compat"
"github.com/safing/portmaster/firewall" "github.com/safing/portmaster/firewall"
"github.com/safing/portmaster/netenv" "github.com/safing/portmaster/netenv"
@ -25,6 +26,9 @@ var (
stopListener1 func() error stopListener1 func() error
stopListener2 func() error stopListener2 func() error
stopListenersLock sync.Mutex stopListenersLock sync.Mutex
eventIDConflictingService = "nameserver:conflicting-service"
eventIDListenerFailed = "nameserver:listener-failed"
) )
func init() { func init() {
@ -133,24 +137,93 @@ func startListener(ip net.IP, port uint16, first bool) {
return nil return nil
} }
// Resolve generic listener error, if primary listener.
if first {
module.Resolve(eventIDListenerFailed)
}
// Start listening. // Start listening.
log.Infof("nameserver: starting to listen on %s", dnsServer.Addr) log.Infof("nameserver: starting to listen on %s", dnsServer.Addr)
err := dnsServer.ListenAndServe() err := dnsServer.ListenAndServe()
if err != nil { if err != nil {
// check if we are shutting down // Stop worker without error if we are shutting down.
if module.IsStopping() { if module.IsStopping() {
return nil return nil
} }
// is something blocking our port? log.Warningf("nameserver: failed to listen on %s: %s", dnsServer.Addr, err)
checkErr := checkForConflictingService(ip, port) handleListenError(err, ip, port, first)
if checkErr != nil {
return checkErr
}
} }
return err return err
}) })
} }
func handleListenError(err error, ip net.IP, port uint16, primaryListener bool) {
var n *notifications.Notification
// Create suffix for secondary listener
var secondaryEventIDSuffix string
if !primaryListener {
secondaryEventIDSuffix = "-secondary"
}
// Find a conflicting service.
cfProcess := findConflictingProcess(ip, port)
if cfProcess != nil {
// Report the conflicting process.
// Build conflicting process description.
var cfDescription string
cfName, err := cfProcess.Name()
if err == nil && cfName != "" {
cfDescription = cfName
}
cfExe, err := cfProcess.Exe()
if err == nil && cfDescription != "" {
if cfDescription != "" {
cfDescription += " (" + cfExe + ")"
} else {
cfDescription = cfName
}
}
// Notify user about conflicting service.
n = notifications.Notify(&notifications.Notification{
EventID: eventIDConflictingService + secondaryEventIDSuffix,
Type: notifications.Error,
Title: "Conflicting DNS Software",
Message: fmt.Sprintf(
"Restart Portmaster after you have deactivated or properly configured the conflicting software: %s",
cfDescription,
),
ShowOnSystem: true,
AvailableActions: []*notifications.Action{
{
Text: "Open Docs",
Type: notifications.ActionTypeOpenURL,
Payload: "https://docs.safing.io/portmaster/install/status/software-compatibility",
},
},
})
} else {
// If no conflict is found, report the error directly.
n = notifications.Notify(&notifications.Notification{
EventID: eventIDListenerFailed + secondaryEventIDSuffix,
Type: notifications.Error,
Title: "Secure DNS Error",
Message: fmt.Sprintf(
"The internal DNS server failed. Restart Portmaster to try again. Error: %s",
err,
),
ShowOnSystem: true,
})
}
// Attach error to module, if primary listener.
if primaryListener {
n.AttachToModule(module)
}
}
func stop() error { func stop() error {
stopListenersLock.Lock() stopListenersLock.Lock()
defer stopListenersLock.Unlock() defer stopListenersLock.Unlock()

View file

@ -106,6 +106,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
return reply(nsutil.Refused("invalid domain")) return reply(nsutil.Refused("invalid domain"))
} }
// Get public suffix after validation.
q.InitPublicSuffixData()
// Check if query is failing. // Check if query is failing.
// Some software retries failing queries excessively. This might not be a // Some software retries failing queries excessively. This might not be a
// problem normally, but handling a request is pretty expensive for the // problem normally, but handling a request is pretty expensive for the
@ -252,10 +255,13 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, resolver.ErrNotFound): case errors.Is(err, resolver.ErrNotFound):
// Try alternatives domain names for unofficial domain spaces.
rrCache = checkAlternativeCaches(ctx, q)
if rrCache == nil {
tracer.Tracef("nameserver: %s", err) tracer.Tracef("nameserver: %s", err)
conn.Failed("domain does not exist", "") conn.Failed("domain does not exist", "")
return reply(nsutil.NxDomain("nxdomain: " + err.Error())) return reply(nsutil.NxDomain("nxdomain: " + err.Error()))
}
case errors.Is(err, resolver.ErrBlocked): case errors.Is(err, resolver.ErrBlocked):
tracer.Tracef("nameserver: %s", err) tracer.Tracef("nameserver: %s", err)
conn.Block(err.Error(), "") conn.Block(err.Error(), "")
@ -268,7 +274,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
case errors.Is(err, resolver.ErrOffline): case errors.Is(err, resolver.ErrOffline):
if rrCache == nil { if rrCache == nil {
log.Tracer(ctx).Debugf("nameserver: not resolving %s, device is offline", q.ID()) tracer.Debugf("nameserver: not resolving %s, device is offline", q.ID())
conn.Failed("not resolving, device is offline", "") conn.Failed("not resolving, device is offline", "")
return reply(nsutil.ServerFailure(err.Error())) return reply(nsutil.ServerFailure(err.Error()))
} }
@ -290,9 +296,13 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
addFailingQuery(q, errors.New("emptry reply from resolver")) addFailingQuery(q, errors.New("emptry reply from resolver"))
return reply(nsutil.ServerFailure("internal error: empty reply")) return reply(nsutil.ServerFailure("internal error: empty reply"))
case rrCache.RCode == dns.RcodeNameError: case rrCache.RCode == dns.RcodeNameError:
// Try alternatives domain names for unofficial domain spaces.
rrCache = checkAlternativeCaches(ctx, q)
if rrCache == nil {
// Return now if NXDomain. // Return now if NXDomain.
return reply(nsutil.NxDomain("no answer found (NXDomain)")) return reply(nsutil.NxDomain("no answer found (NXDomain)"))
} }
}
// Check with firewall again after resolving. // Check with firewall again after resolving.
tracer.Trace("nameserver: deciding on resolved dns") tracer.Trace("nameserver: deciding on resolved dns")
@ -336,3 +346,52 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
) )
return reply(rrCache, conn, rrCache) return reply(rrCache, conn, rrCache)
} }
func checkAlternativeCaches(ctx context.Context, q *resolver.Query) *resolver.RRCache {
// Do not try alternatives when the query is in a public suffix.
// This also includes arpa. and local.
if q.ICANNSpace {
return nil
}
// Check if the env resolver has something.
pmEnvQ := &resolver.Query{
FQDN: q.FQDN + "local." + resolver.InternalSpecialUseDomain,
QType: q.QType,
}
rrCache, err := resolver.QueryPortmasterEnv(ctx, pmEnvQ)
if err == nil && rrCache != nil && rrCache.RCode == dns.RcodeSuccess {
makeAlternativeRecord(ctx, q, rrCache, pmEnvQ.FQDN)
return rrCache
}
// Check if we have anything in cache
localFQDN := q.FQDN + "local."
rrCache, err = resolver.GetRRCache(localFQDN, q.QType)
if err == nil && rrCache != nil && rrCache.RCode == dns.RcodeSuccess {
makeAlternativeRecord(ctx, q, rrCache, localFQDN)
return rrCache
}
return nil
}
func makeAlternativeRecord(ctx context.Context, q *resolver.Query, rrCache *resolver.RRCache, altName string) {
log.Tracer(ctx).Debugf("using %s to answer query", altName)
// Duplicate answers so they match the query.
copied := make([]dns.RR, 0, len(rrCache.Answer))
for _, answer := range rrCache.Answer {
if strings.ToLower(answer.Header().Name) == altName {
copiedAnswer := dns.Copy(answer)
copiedAnswer.Header().Name = q.FQDN
copied = append(copied, copiedAnswer)
}
}
if len(copied) > 0 {
rrCache.Answer = append(rrCache.Answer, copied...)
}
// Update the question.
rrCache.Domain = q.FQDN
}

View file

@ -1,164 +0,0 @@
package nameserver
import (
"fmt"
"net"
"os"
"strconv"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/network/state"
)
var (
commonResolverIPs = []net.IP{
net.IPv4zero,
net.IPv4(127, 0, 0, 1), // default
net.IPv4(127, 0, 0, 53), // some resolvers on Linux
net.IPv6zero,
net.IPv6loopback,
}
// lastKilledPID holds the PID of the last killed conflicting service.
// It is only accessed by checkForConflictingService, which is only called by
// the nameserver worker.
lastKilledPID int
)
func checkForConflictingService(ip net.IP, port uint16) error {
// Evaluate which IPs to check.
var ipsToCheck []net.IP
if ip.Equal(net.IPv4zero) || ip.Equal(net.IPv6zero) {
ipsToCheck = commonResolverIPs
} else {
ipsToCheck = []net.IP{ip}
}
// Check if there is another resolver when need to take over.
var killed int
var killingFailed bool
ipsToCheckLoop:
for _, resolverIP := range ipsToCheck {
pid, err := takeover(resolverIP, port)
switch {
case err != nil:
// Log the error and let the worker try again.
log.Infof("nameserver: failed to stop conflicting service: %s", err)
killingFailed = true
break ipsToCheckLoop
case pid != 0:
// Conflicting service identified and killed!
killed = pid
break ipsToCheckLoop
}
}
// Notify user of failed killing or repeated kill.
if killingFailed || (killed != 0 && killed == lastKilledPID) {
// Notify the user that we failed to kill something.
notifications.Notify(&notifications.Notification{
EventID: "namserver:failed-to-kill-conflicting-service",
Type: notifications.Error,
Title: "Failed to Stop Conflicting DNS Client",
Message: "The Portmaster failed to stop a conflicting DNS client to gain required system integration. If there is another DNS Client (Nameserver; Resolver) on this device, please disable it.",
ShowOnSystem: true,
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
{
Text: "Open Docs",
Type: notifications.ActionTypeOpenURL,
Payload: "https://docs.safing.io/portmaster/install/status/software-compatibility",
},
},
})
return nil
}
// Check if something was killed.
if killed == 0 {
return nil
}
lastKilledPID = killed
// Notify the user that we killed something.
notifications.Notify(&notifications.Notification{
EventID: "namserver:stopped-conflicting-service",
Type: notifications.Info,
Title: "Stopped Conflicting DNS Client",
Message: fmt.Sprintf(
"The Portmaster stopped a conflicting DNS client (pid %d) to gain required system integration. If you are running another DNS client on this device on purpose, you can the check the documentation if it is compatible with the Portmaster.",
killed,
),
ShowOnSystem: true,
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
{
Text: "Open Docs",
Type: notifications.ActionTypeOpenURL,
Payload: "https://docs.safing.io/portmaster/install/status/software-compatibility",
},
},
})
// Restart nameserver via service-worker logic.
// Wait shortly so that the other process can shut down.
time.Sleep(10 * time.Millisecond)
return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, killed)
}
func takeover(resolverIP net.IP, resolverPort uint16) (int, error) {
pid, _, err := state.Lookup(&packet.Info{
Inbound: true,
Version: 0, // auto-detect
Protocol: packet.UDP,
Src: nil, // do not record direction
SrcPort: 0, // do not record direction
Dst: resolverIP,
DstPort: resolverPort,
}, true)
if err != nil {
// there may be nothing listening on :53
return 0, nil //nolint:nilerr // Treat lookup error as "not found".
}
// Just don't, uh, kill ourselves...
if pid == os.Getpid() {
return 0, nil
}
proc, err := os.FindProcess(pid)
if err != nil {
// huh. gone already? I guess we'll wait then...
return 0, err
}
err = proc.Signal(os.Interrupt)
if err != nil {
err = proc.Kill()
if err != nil {
log.Errorf("nameserver: failed to stop conflicting service (pid %d): %s", pid, err)
return 0, err
}
}
log.Warningf(
"nameserver: killed conflicting service with PID %d over %s",
pid,
net.JoinHostPort(
resolverIP.String(),
strconv.Itoa(int(resolverPort)),
),
)
return pid, nil
}

View file

@ -152,11 +152,9 @@ func IsMyIP(ip net.IP) (yes bool, err error) {
return false, nil return false, nil
} }
// IsMyNet returns whether the given IP is currently in the host's broadcast // GetLocalNetwork uses the given IP to search for a network configured on the
// domain - ie. the networks that the host is directly attached to. // device and returns it.
// Function is optimized with the assumption that is unlikely that the IP is func GetLocalNetwork(ip net.IP) (myNet *net.IPNet, err error) {
// in the broadcast domain.
func IsMyNet(ip net.IP) (yes bool, err error) {
myNetworksLock.Lock() myNetworksLock.Lock()
defer myNetworksLock.Unlock() defer myNetworksLock.Unlock()
@ -164,16 +162,16 @@ func IsMyNet(ip net.IP) (yes bool, err error) {
if myNetworksNetworkChangedFlag.IsSet() { if myNetworksNetworkChangedFlag.IsSet() {
err := refreshMyNetworks() err := refreshMyNetworks()
if err != nil { if err != nil {
return false, err return nil, err
} }
} }
// Check if the IP address is in my networks. // Check if the IP address is in my networks.
for _, myNet := range myNetworks { for _, myNet := range myNetworks {
if myNet.Contains(ip) { if myNet.Contains(ip) {
return true, nil return myNet, nil
} }
} }
return false, nil return nil, nil
} }

View file

@ -37,13 +37,12 @@ var (
PortalTestIP = net.IPv4(192, 0, 2, 1) PortalTestIP = net.IPv4(192, 0, 2, 1)
PortalTestURL = fmt.Sprintf("http://%s/", PortalTestIP) PortalTestURL = fmt.Sprintf("http://%s/", PortalTestIP)
DNSTestDomain = "one.one.one.one." DNSTestDomain = "online-check.safing.io."
DNSTestExpectedIP = net.IPv4(1, 1, 1, 1) DNSTestExpectedIP = net.IPv4(0, 65, 67, 75) // Ascii: \0ACK
DNSTestQueryFunc func(ctx context.Context, fdqn string) (ips []net.IP, ok bool, err error)
DNSFallbackTestDomain = "dns-check.safing.io."
DNSFallbackTestExpectedIP = net.IPv4(0, 65, 67, 75) // Ascii: \0ACK
ConnectedToSPN = abool.New() ConnectedToSPN = abool.New()
ConnectedToDNS = abool.New()
// SpecialCaptivePortalDomain is the domain name used to point to the detected captive portal IP // SpecialCaptivePortalDomain is the domain name used to point to the detected captive portal IP
// or the captive portal test IP. The default value should be overridden by the resolver package, // or the captive portal test IP. The default value should be overridden by the resolver package,
@ -53,8 +52,6 @@ var (
// ConnectivityDomains holds all connectivity domains. This slice must not be modified. // ConnectivityDomains holds all connectivity domains. This slice must not be modified.
ConnectivityDomains = []string{ ConnectivityDomains = []string{
SpecialCaptivePortalDomain, SpecialCaptivePortalDomain,
DNSTestDomain, // Internal DNS Check
DNSFallbackTestDomain, // Internal DNS Check
// Windows // Windows
"dns.msftncsi.com.", // DNS Check "dns.msftncsi.com.", // DNS Check
@ -380,20 +377,20 @@ func monitorOnlineStatus(ctx context.Context) error {
func getDynamicStatusTrigger() <-chan time.Time { func getDynamicStatusTrigger() <-chan time.Time {
switch GetOnlineStatus() { switch GetOnlineStatus() {
case StatusOffline: case StatusOffline:
// Will be triggered by network change anyway. // Will also be triggered by network change.
return time.After(20 * time.Second) return time.After(10 * time.Second)
case StatusLimited, StatusPortal: case StatusLimited, StatusPortal:
// Change will not be detected otherwise, but impact is minor. // Change will not be detected otherwise, but impact is minor.
return time.After(5 * time.Second) return time.After(5 * time.Second)
case StatusSemiOnline: case StatusSemiOnline:
// Very small impact. // Very small impact.
return time.After(20 * time.Second) return time.After(60 * time.Second)
case StatusOnline: case StatusOnline:
// Don't check until resolver reports problems. // Don't check until resolver reports problems.
return nil return nil
case StatusUnknown: case StatusUnknown:
return time.After(5 * time.Second) fallthrough
default: // other unknown status default:
return time.After(5 * time.Minute) return time.After(5 * time.Minute)
} }
} }
@ -407,13 +404,18 @@ func checkOnlineStatus(ctx context.Context) {
return StatusUnknown return StatusUnknown
}*/ }*/
// 0) check if connected to SPN // 0) check if connected to SPN and/or DNS.
if ConnectedToSPN.IsSet() { if ConnectedToSPN.IsSet() {
updateOnlineStatus(StatusOnline, nil, "connected to SPN") updateOnlineStatus(StatusOnline, nil, "connected to SPN")
return return
} }
if ConnectedToDNS.IsSet() {
updateOnlineStatus(StatusOnline, nil, "connected to DNS")
return
}
// 1) check for addresses // 1) check for addresses
ipv4, ipv6, err := GetAssignedAddresses() ipv4, ipv6, err := GetAssignedAddresses()
@ -508,34 +510,28 @@ func checkOnlineStatus(ctx context.Context) {
// 3) resolve a query // 3) resolve a query
// Check with primary dns check domain. // Check if we can resolve the dns check domain.
ips, err := net.LookupIP(DNSTestDomain) if DNSTestQueryFunc == nil {
if err != nil { updateOnlineStatus(StatusOnline, nil, "all checks passed, dns query check disabled")
log.Warningf("netenv: dns check query failed: %s", err) return
} else { }
// check for expected response ips, ok, err := DNSTestQueryFunc(ctx, DNSTestDomain)
for _, ip := range ips { switch {
if ip.Equal(DNSTestExpectedIP) { case ok && err != nil:
updateOnlineStatus(StatusOnline, nil, fmt.Sprintf(
"all checks passed, acceptable result for dns query check: %s",
err,
))
case ok && len(ips) >= 1 && ips[0].Equal(DNSTestExpectedIP):
updateOnlineStatus(StatusOnline, nil, "all checks passed") updateOnlineStatus(StatusOnline, nil, "all checks passed")
return case ok && len(ips) >= 1:
log.Warningf("netenv: dns query check response mismatched: got %s", ips[0])
updateOnlineStatus(StatusOnline, nil, "all checks passed, dns query check response mismatched")
case ok:
log.Warningf("netenv: dns query check response mismatched: empty response")
updateOnlineStatus(StatusOnline, nil, "all checks passed, dns query check response was empty")
default:
log.Warningf("netenv: dns query check failed: %s", err)
updateOnlineStatus(StatusOffline, nil, "dns query check failed")
} }
}
}
// If that did not work, check with fallback dns check domain.
ips, err = net.LookupIP(DNSFallbackTestDomain)
if err != nil {
log.Warningf("netenv: dns fallback check query failed: %s", err)
updateOnlineStatus(StatusLimited, nil, "dns fallback check query failed")
return
}
// check for expected response
for _, ip := range ips {
if ip.Equal(DNSFallbackTestExpectedIP) {
updateOnlineStatus(StatusOnline, nil, "all checks passed")
return
}
}
// unexpected response
updateOnlineStatus(StatusSemiOnline, nil, "dns check query response mismatched")
} }

43
network/multicast.go Normal file
View file

@ -0,0 +1,43 @@
package network
import (
"net"
"github.com/safing/portmaster/network/netutils"
)
// GetMulticastRequestConn searches for and returns the requesting connnection
// of a possible multicast/broadcast response.
func GetMulticastRequestConn(responseConn *Connection, responseFromNet *net.IPNet) *Connection {
// Calculate the broadcast address the query would have gone to.
responseNetBroadcastIP := netutils.GetBroadcastAddress(responseFromNet.IP, responseFromNet.Mask)
// Find requesting multicast/broadcast connection.
for _, conn := range conns.clone() {
switch {
case conn.Inbound:
// Ignore incoming connections.
case conn.Ended != 0:
// Ignore ended connections.
case conn.Entity.Protocol != responseConn.Entity.Protocol:
// Ignore on protocol mismatch.
case conn.LocalPort != responseConn.LocalPort:
// Ignore on local port mismatch.
case !conn.LocalIP.Equal(responseConn.LocalIP):
// Ignore on local IP mismatch.
case !conn.Process().Equal(responseConn.Process()):
// Ignore if processes mismatch.
case conn.Entity.IPScope == netutils.LocalMulticast &&
(responseConn.Entity.IPScope == netutils.LinkLocal ||
responseConn.Entity.IPScope == netutils.SiteLocal):
// We found a (possibly routed) multicast request that matches the response!
return conn
case conn.Entity.IP.Equal(responseNetBroadcastIP) &&
responseFromNet.Contains(conn.LocalIP):
// We found a (link local) broadcast request that matches the response!
return conn
}
}
return nil
}

View file

@ -4,11 +4,13 @@ import (
"fmt" "fmt"
"net" "net"
"regexp" "regexp"
"strings"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
var cleanDomainRegex = regexp.MustCompile( var (
cleanDomainRegex = regexp.MustCompile(
`^` + // match beginning `^` + // match beginning
`(` + // start subdomain group `(` + // start subdomain group
`(xn--)?` + // idn prefix `(xn--)?` + // idn prefix
@ -19,6 +21,21 @@ var cleanDomainRegex = regexp.MustCompile(
`[a-z0-9_-]{2,63}` + // TLD main chunk with at least two characters `[a-z0-9_-]{2,63}` + // TLD main chunk with at least two characters
`\.` + // ending with a dot `\.` + // ending with a dot
`$`, // match end `$`, // match end
)
// dnsSDDomainRegex is a lot more lax to better suit the allowed characters in DNS-SD.
// Not all characters have been allowed - some special characters were
// removed to reduce the general attack surface.
dnsSDDomainRegex = regexp.MustCompile(
// Start of charset selection.
`^[` +
// Printable ASCII (character code 32-127), excluding some special characters.
` !#$%&()*+,\-\./0-9:;=?@A-Z[\\\]^_\a-z{|}~` +
// Only latin characters from extended ASCII (character code 128-255).
`ŠŒŽšœžŸ¡¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ` +
// End of charset selection.
`]*$`,
)
) )
// IsValidFqdn returns whether the given string is a valid fqdn. // IsValidFqdn returns whether the given string is a valid fqdn.
@ -33,15 +50,18 @@ func IsValidFqdn(fqdn string) bool {
return false return false
} }
// check with regex // IsFqdn checks if a domain name is fully qualified.
if !cleanDomainRegex.MatchString(fqdn) { if !dns.IsFqdn(fqdn) {
return false return false
} }
// check with miegk/dns // Use special check for .local domains to support DNS-SD.
if strings.HasSuffix(fqdn, ".local.") {
return dnsSDDomainRegex.MatchString(fqdn)
}
// IsFqdn checks if a domain name is fully qualified. // check with regex
if !dns.IsFqdn(fqdn) { if !cleanDomainRegex.MatchString(fqdn) {
return false return false
} }

View file

@ -28,6 +28,9 @@ func GetIPScope(ip net.IP) IPScope { //nolint:gocognit
if ip4 := ip.To4(); ip4 != nil { if ip4 := ip.To4(); ip4 != nil {
// IPv4 // IPv4
switch { switch {
case ip4[0] == 0:
// 0.0.0.0/8
return Invalid
case ip4[0] == 127: case ip4[0] == 127:
// 127.0.0.0/8 (RFC1918) // 127.0.0.0/8 (RFC1918)
return HostLocal return HostLocal
@ -79,6 +82,8 @@ func GetIPScope(ip net.IP) IPScope { //nolint:gocognit
} else if len(ip) == net.IPv6len { } else if len(ip) == net.IPv6len {
// IPv6 // IPv6
switch { switch {
case ip.Equal(net.IPv6zero):
return Invalid
case ip.Equal(net.IPv6loopback): case ip.Equal(net.IPv6loopback):
return HostLocal return HostLocal
case ip[0]&0xfe == 0xfc: case ip[0]&0xfe == 0xfc:
@ -124,3 +129,29 @@ func (scope IPScope) IsGlobal() bool {
return false return false
} }
} }
// GetBroadcastAddress returns the broadcast address of the given IP and network mask.
// If a mixed IPv4/IPv6 input is given, it returns nil.
func GetBroadcastAddress(ip net.IP, netMask net.IPMask) net.IP {
// Convert to standard v4.
if ip4 := ip.To4(); ip4 != nil {
ip = ip4
}
mask := net.IP(netMask)
if ip4Mask := mask.To4(); ip4Mask != nil {
mask = ip4Mask
}
// Check for mixed v4/v6 input.
if len(ip) != len(mask) {
return nil
}
// Merge to broadcast address
n := len(ip)
broadcastAddress := make(net.IP, n)
for i := 0; i < n; i++ {
broadcastAddress[i] = ip[i] | ^mask[i]
}
return broadcastAddress
}

View file

@ -67,6 +67,32 @@ func (p *Process) Profile() *profile.LayeredProfile {
return p.profile return p.profile
} }
// IsIdentified returns whether the process has been identified or if it
// represents some kind of unidentified process.
func (p *Process) IsIdentified() bool {
// Check if process exists.
if p == nil {
return false
}
// Check for special PIDs.
switch p.Pid {
case UndefinedProcessID:
return false
case UnidentifiedProcessID:
return false
case UnsolicitedProcessID:
return false
default:
return true
}
}
// Equal returns if the two processes are both identified and have the same PID.
func (p *Process) Equal(other *Process) bool {
return p.IsIdentified() && other.IsIdentified() && p.Pid == other.Pid
}
// IsSystemResolver is a shortcut to check if the process is or belongs to the // IsSystemResolver is a shortcut to check if the process is or belongs to the
// system resolver and needs special handling. // system resolver and needs special handling.
func (p *Process) IsSystemResolver() bool { func (p *Process) IsSystemResolver() bool {

View file

@ -6,8 +6,6 @@ import (
"github.com/safing/portbase/database/migration" "github.com/safing/portbase/database/migration"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
// Dependency.
_ "github.com/safing/portmaster/core/base" _ "github.com/safing/portmaster/core/base"
"github.com/safing/portmaster/updates" "github.com/safing/portmaster/updates"
) )

View file

@ -224,9 +224,9 @@ The format is: "protocol://ip:port?parameter=value&parameter=value"
noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey) noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey)
err = config.Register(&config.Option{ err = config.Register(&config.Option{
Name: "Enforce Secure DNS", Name: "Use Secure Protocols Only",
Key: CfgOptionNoInsecureProtocolsKey, Key: CfgOptionNoInsecureProtocolsKey,
Description: "Never resolve using insecure protocols, ie. plain DNS.", Description: "Never resolve using insecure protocols, ie. plain DNS. This may break certain local DNS services, which always use plain DNS.",
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert, ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable, ReleaseLevel: config.ReleaseLevelStable,

View file

@ -4,10 +4,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net"
"strings"
"sync" "sync"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/publicsuffix"
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
@ -89,6 +92,11 @@ type Query struct {
IgnoreFailing bool IgnoreFailing bool
LocalResolversOnly bool LocalResolversOnly bool
// ICANNSpace signifies if the domain is within ICANN managed domain space.
ICANNSpace bool
// Domain root is the effective TLD +1.
DomainRoot string
// internal // internal
dotPrefixedFQDN string dotPrefixedFQDN string
} }
@ -98,6 +106,41 @@ func (q *Query) ID() string {
return q.FQDN + q.QType.String() return q.FQDN + q.QType.String()
} }
// InitPublicSuffixData initializes the public suffix data.
func (q *Query) InitPublicSuffixData() {
// Get public suffix and derive if domain is in ICANN space.
suffix, icann := publicsuffix.PublicSuffix(strings.TrimSuffix(q.FQDN, "."))
if icann || strings.Contains(suffix, ".") {
q.ICANNSpace = true
}
// Override some cases.
switch suffix {
case "example":
q.ICANNSpace = true // Defined by ICANN.
case "invalid":
q.ICANNSpace = true // Defined by ICANN.
case "local":
q.ICANNSpace = true // Defined by ICANN.
case "localhost":
q.ICANNSpace = true // Defined by ICANN.
case "onion":
q.ICANNSpace = false // Defined by ICANN, but special.
case "test":
q.ICANNSpace = true // Defined by ICANN.
}
// Add suffix to adhere to FQDN format.
suffix += "."
switch {
case len(q.FQDN) == len(suffix):
// We are at or below the domain root, reset.
q.DomainRoot = ""
case len(q.FQDN) > len(suffix):
domainRootStart := strings.LastIndex(q.FQDN[:len(q.FQDN)-len(suffix)-1], ".") + 1
q.DomainRoot = q.FQDN[domainRootStart:]
}
}
// check runs sanity checks and does some initialization. Returns whether the query passed the basic checks. // check runs sanity checks and does some initialization. Returns whether the query passed the basic checks.
func (q *Query) check() (ok bool) { func (q *Query) check() (ok bool) {
if q.FQDN == "" { if q.FQDN == "" {
@ -318,8 +361,8 @@ func resolveAndCache(ctx context.Context, q *Query, oldCache *RRCache) (rrCache
} }
// check if we are online // check if we are online
if primarySource != ServerSourceEnv && netenv.GetOnlineStatus() == netenv.StatusOffline { if netenv.GetOnlineStatus() == netenv.StatusOffline && primarySource != ServerSourceEnv {
if !netenv.IsConnectivityDomain(q.FQDN) { if q.FQDN != netenv.DNSTestDomain && !netenv.IsConnectivityDomain(q.FQDN) {
// we are offline and this is not an online check query // we are offline and this is not an online check query
return oldCache, ErrOffline return oldCache, ErrOffline
} }
@ -358,6 +401,7 @@ resolveLoop:
// some resolvers might also block // some resolvers might also block
return nil, err return nil, err
case netenv.GetOnlineStatus() == netenv.StatusOffline && case netenv.GetOnlineStatus() == netenv.StatusOffline &&
q.FQDN != netenv.DNSTestDomain &&
!netenv.IsConnectivityDomain(q.FQDN): !netenv.IsConnectivityDomain(q.FQDN):
// we are offline and this is not an online check query // we are offline and this is not an online check query
return oldCache, ErrOffline return oldCache, ErrOffline
@ -478,3 +522,45 @@ func shouldResetCache(q *Query) (reset bool) {
return false return false
} }
func init() {
netenv.DNSTestQueryFunc = testConnectivity
}
// testConnectivity test if resolving a query succeeds and returns whether the
// query itself succeeded, separate from interpreting the result.
func testConnectivity(ctx context.Context, fdqn string) (ips []net.IP, ok bool, err error) {
q := &Query{
FQDN: fdqn,
QType: dns.Type(dns.TypeA),
NoCaching: true,
}
if !q.check() {
return nil, false, ErrInvalid
}
rrCache, err := resolveAndCache(ctx, q, nil)
switch {
case err == nil:
switch rrCache.RCode {
case dns.RcodeNameError:
return nil, true, ErrNotFound
case dns.RcodeRefused:
return nil, true, errors.New("refused")
default:
ips := rrCache.ExportAllARecords()
if len(ips) > 0 {
return ips, true, nil
}
return nil, true, ErrNotFound
}
case errors.Is(err, ErrNotFound):
return nil, true, err
case errors.Is(err, ErrBlocked):
return nil, true, err
case errors.Is(err, ErrNoCompliance):
return nil, true, err
default:
return nil, false, err
}
}

View file

@ -128,7 +128,7 @@ func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache {
// Disable caching, as the env always has the raw data available. // Disable caching, as the env always has the raw data available.
q.NoCaching = true q.NoCaching = true
return &RRCache{ rrCache := &RRCache{
Domain: q.FQDN, Domain: q.FQDN,
Question: q.QType, Question: q.QType,
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
@ -136,6 +136,10 @@ func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache {
Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD. Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD.
Resolver: envResolver.Info.Copy(), Resolver: envResolver.Info.Copy(),
} }
if len(rrCache.Answer) == 0 {
rrCache.RCode = dns.RcodeNameError
}
return rrCache
} }
func (er *envResolverConn) ReportFailure() {} func (er *envResolverConn) ReportFailure() {}
@ -145,3 +149,8 @@ func (er *envResolverConn) IsFailing() bool {
} }
func (er *envResolverConn) ResetFailure() {} func (er *envResolverConn) ResetFailure() {}
// QueryPortmasterEnv queries the environment resolver directly.
func QueryPortmasterEnv(ctx context.Context, q *Query) (*RRCache, error) {
return envResolver.Conn.Query(ctx, q)
}

View file

@ -206,8 +206,8 @@ func (brc *BasicResolverConn) init() {
// ReportFailure reports that an error occurred with this resolver. // ReportFailure reports that an error occurred with this resolver.
func (brc *BasicResolverConn) ReportFailure() { func (brc *BasicResolverConn) ReportFailure() {
// Don't mark resolver as failed if we are offline.
if !netenv.Online() { if !netenv.Online() {
// don't mark failed if we are offline
return return
} }
@ -224,6 +224,11 @@ func (brc *BasicResolverConn) ReportFailure() {
// the fail. // the fail.
brc.networkChangedFlag.Refresh() brc.networkChangedFlag.Refresh()
} }
// Report to netenv that a configured server failed.
if brc.resolver.Info.Source == ServerSourceConfigured {
netenv.ConnectedToDNS.UnSet()
}
} }
// IsFailing returns if this resolver is currently failing. // IsFailing returns if this resolver is currently failing.
@ -255,4 +260,9 @@ func (brc *BasicResolverConn) ResetFailure() {
defer brc.failLock.Unlock() defer brc.failLock.Unlock()
brc.fails = 0 brc.fails = 0
} }
// Report to netenv that a configured server succeeded.
if brc.resolver.Info.Source == ServerSourceConfigured {
netenv.ConnectedToDNS.Set()
}
} }

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
) )
@ -103,3 +104,57 @@ func TestBulkResolving(t *testing.T) {
t.Logf("total time taken: %s", time.Since(started)) t.Logf("total time taken: %s", time.Since(started))
} }
func TestPublicSuffix(t *testing.T) {
t.Parallel()
testSuffix(t, "co.uk.", "", true)
testSuffix(t, "amazon.co.uk.", "amazon.co.uk.", true)
testSuffix(t, "books.amazon.co.uk.", "amazon.co.uk.", true)
testSuffix(t, "www.books.amazon.co.uk.", "amazon.co.uk.", true)
testSuffix(t, "com.", "", true)
testSuffix(t, "amazon.com.", "amazon.com.", true)
testSuffix(t, "example0.debian.net.", "example0.debian.net.", true)
testSuffix(t, "example1.debian.org.", "debian.org.", true)
testSuffix(t, "golang.dev.", "golang.dev.", true)
testSuffix(t, "golang.net.", "golang.net.", true)
testSuffix(t, "play.golang.org.", "golang.org.", true)
testSuffix(t, "gophers.in.space.museum.", "in.space.museum.", true)
testSuffix(t, "0emm.com.", "0emm.com.", true)
testSuffix(t, "a.0emm.com.", "", true)
testSuffix(t, "b.c.d.0emm.com.", "c.d.0emm.com.", true)
testSuffix(t, "org.", "", true)
testSuffix(t, "foo.org.", "foo.org.", true)
testSuffix(t, "foo.co.uk.", "foo.co.uk.", true)
testSuffix(t, "foo.dyndns.org.", "foo.dyndns.org.", true)
testSuffix(t, "foo.blogspot.co.uk.", "foo.blogspot.co.uk.", true)
testSuffix(t, "there.is.no.such-tld.", "no.such-tld.", false)
testSuffix(t, "www.some.bit.", "some.bit.", false)
testSuffix(t, "cromulent.", "", false)
testSuffix(t, "arpa.", "", true)
testSuffix(t, "in-addr.arpa.", "", true)
testSuffix(t, "1.in-addr.arpa.", "1.in-addr.arpa.", true)
testSuffix(t, "ip6.arpa.", "", true)
testSuffix(t, "1.ip6.arpa.", "1.ip6.arpa.", true)
testSuffix(t, "www.some.arpa.", "some.arpa.", true)
testSuffix(t, "www.some.home.arpa.", "home.arpa.", true)
testSuffix(t, ".", "", false)
testSuffix(t, "", "", false)
// Test edge case domains.
testSuffix(t, "www.some.example.", "some.example.", true)
testSuffix(t, "www.some.invalid.", "some.invalid.", true)
testSuffix(t, "www.some.local.", "some.local.", true)
testSuffix(t, "www.some.localhost.", "some.localhost.", true)
testSuffix(t, "www.some.onion.", "some.onion.", false)
testSuffix(t, "www.some.test.", "some.test.", true)
}
func testSuffix(t *testing.T, fqdn, domainRoot string, icannSpace bool) {
t.Helper()
q := &Query{FQDN: fqdn}
q.InitPublicSuffixData()
assert.Equal(t, domainRoot, q.DomainRoot)
assert.Equal(t, icannSpace, q.ICANNSpace)
}