mirror of
https://github.com/safing/portmaster
synced 2025-09-02 02:29:12 +00:00
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:
commit
02779f8bfd
21 changed files with 662 additions and 272 deletions
|
@ -26,6 +26,10 @@ var (
|
|||
// selfcheckFails counts how often the self check failed successively.
|
||||
// selfcheckFails is not locked as it is only accessed by the self-check task.
|
||||
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
|
||||
|
@ -49,6 +53,7 @@ func prep() error {
|
|||
func start() error {
|
||||
startNotify()
|
||||
|
||||
selfcheckNetworkChangedFlag.Refresh()
|
||||
selfcheckTask = module.NewTask("compatibility self-check", selfcheckTaskFunc).
|
||||
Repeat(5 * time.Minute).
|
||||
MaxDelay(selfcheckTaskRetryAfter).
|
||||
|
@ -83,19 +88,23 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error {
|
|||
|
||||
// Run selfcheck and return if successful.
|
||||
issue, err := selfcheck(ctx)
|
||||
if err == nil {
|
||||
selfCheckIsFailing.UnSet()
|
||||
selfcheckFails = 0
|
||||
resetSystemIssue()
|
||||
switch {
|
||||
case err == nil:
|
||||
// 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.
|
||||
if issue != nil {
|
||||
// Set state and increase counter.
|
||||
selfCheckIsFailing.Set()
|
||||
selfcheckFails++
|
||||
|
||||
// Log and notify.
|
||||
tracer.Errorf("compat: %s", err)
|
||||
if selfcheckFails >= selfcheckFailThreshold {
|
||||
issue.notify(err)
|
||||
|
@ -103,13 +112,15 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error {
|
|||
|
||||
// Retry quicker when failed.
|
||||
task.Schedule(time.Now().Add(selfcheckTaskRetryAfter))
|
||||
} else {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset self-check state.
|
||||
selfcheckNetworkChangedFlag.Refresh()
|
||||
selfCheckIsFailing.UnSet()
|
||||
selfcheckFails = 0
|
||||
|
||||
// Only log internal errors, but don't notify.
|
||||
tracer.Warningf("compat: %s", err)
|
||||
}
|
||||
resetSystemIssue()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ package base
|
|||
|
||||
import (
|
||||
"github.com/safing/portbase/database"
|
||||
|
||||
// Dependencies.
|
||||
_ "github.com/safing/portbase/database/dbmodule"
|
||||
_ "github.com/safing/portbase/database/storage/bbolt"
|
||||
)
|
||||
|
|
|
@ -2,9 +2,11 @@ package firewall
|
|||
|
||||
import (
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
_ "github.com/safing/portmaster/core"
|
||||
"github.com/safing/portmaster/intel/filterlists"
|
||||
"github.com/safing/spn/captain"
|
||||
)
|
||||
|
||||
|
@ -12,10 +14,13 @@ var (
|
|||
filterModule *modules.Module
|
||||
filterEnabled config.BoolOption
|
||||
tunnelEnabled config.BoolOption
|
||||
|
||||
unbreakFilterListIDs = []string{"UNBREAK"}
|
||||
resolvedUnbreakFilterListIDs []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
filterModule = modules.Register("filter", filterPrep, nil, nil, "core", "intel")
|
||||
filterModule = modules.Register("filter", filterPrep, filterStart, nil, "core", "intel")
|
||||
subsystems.Register(
|
||||
"filter",
|
||||
"Privacy Filter",
|
||||
|
@ -47,3 +52,14 @@ func filterPrep() (err error) {
|
|||
tunnelEnabled = config.Concurrent.GetAsBool(captain.CfgOptionEnableSPNKey, false)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package firewall
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/agext/levenshtein"
|
||||
|
@ -41,6 +43,7 @@ type deciderFn func(context.Context, *network.Connection, *profile.LayeredProfil
|
|||
var defaultDeciders = []deciderFn{
|
||||
checkPortmasterConnection,
|
||||
checkSelfCommunication,
|
||||
checkIfBroadcastReply,
|
||||
checkConnectionType,
|
||||
checkConnectionScope,
|
||||
checkEndpointLists,
|
||||
|
@ -182,6 +185,46 @@ func checkSelfCommunication(ctx context.Context, conn *network.Connection, _ *pr
|
|||
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 {
|
||||
// DNS request from the system resolver require a special decision process,
|
||||
// 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)
|
||||
switch result {
|
||||
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())
|
||||
return true
|
||||
case endpoints.NoMatch:
|
||||
|
@ -439,10 +492,7 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, p *pro
|
|||
trimmedDomain := strings.TrimRight(conn.Entity.Domain, ".")
|
||||
etld1, err := publicsuffix.EffectiveTLDPlusOne(trimmedDomain)
|
||||
if err != nil {
|
||||
// we don't apply any checks here and let the request through
|
||||
// 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)
|
||||
// Don't run the check if the domain is a TLD.
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ func checkTunneling(ctx context.Context, conn *network.Connection, pkt packet.Pa
|
|||
case conn.Process().Pid == ownPID:
|
||||
// Bypass tunneling for certain own connections.
|
||||
switch {
|
||||
case captain.ClientBootstrapping():
|
||||
case !captain.ClientReady():
|
||||
return
|
||||
case captain.IsExcepted(conn.Entity.IP):
|
||||
return
|
||||
|
@ -42,10 +42,10 @@ func checkTunneling(ctx context.Context, conn *network.Connection, pkt packet.Pa
|
|||
}
|
||||
|
||||
// Check more extensively for Local/LAN connections.
|
||||
myNet, err := netenv.IsMyNet(conn.Entity.IP)
|
||||
localNet, err := netenv.GetLocalNetwork(conn.Entity.IP)
|
||||
if err != nil {
|
||||
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
|
||||
// likely has a public IPv6 address.
|
||||
// Don't tunnel LAN connections.
|
||||
|
|
75
nameserver/conflict.go
Normal file
75
nameserver/conflict.go
Normal 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
|
||||
}
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/compat"
|
||||
"github.com/safing/portmaster/firewall"
|
||||
"github.com/safing/portmaster/netenv"
|
||||
|
@ -25,6 +26,9 @@ var (
|
|||
stopListener1 func() error
|
||||
stopListener2 func() error
|
||||
stopListenersLock sync.Mutex
|
||||
|
||||
eventIDConflictingService = "nameserver:conflicting-service"
|
||||
eventIDListenerFailed = "nameserver:listener-failed"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -133,24 +137,93 @@ func startListener(ip net.IP, port uint16, first bool) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Resolve generic listener error, if primary listener.
|
||||
if first {
|
||||
module.Resolve(eventIDListenerFailed)
|
||||
}
|
||||
|
||||
// Start listening.
|
||||
log.Infof("nameserver: starting to listen on %s", dnsServer.Addr)
|
||||
err := dnsServer.ListenAndServe()
|
||||
if err != nil {
|
||||
// check if we are shutting down
|
||||
// Stop worker without error if we are shutting down.
|
||||
if module.IsStopping() {
|
||||
return nil
|
||||
}
|
||||
// is something blocking our port?
|
||||
checkErr := checkForConflictingService(ip, port)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
log.Warningf("nameserver: failed to listen on %s: %s", dnsServer.Addr, err)
|
||||
handleListenError(err, ip, port, first)
|
||||
}
|
||||
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(¬ifications.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(¬ifications.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 {
|
||||
stopListenersLock.Lock()
|
||||
defer stopListenersLock.Unlock()
|
||||
|
|
|
@ -106,6 +106,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
|
|||
return reply(nsutil.Refused("invalid domain"))
|
||||
}
|
||||
|
||||
// Get public suffix after validation.
|
||||
q.InitPublicSuffixData()
|
||||
|
||||
// Check if query is failing.
|
||||
// Some software retries failing queries excessively. This might not be a
|
||||
// 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 {
|
||||
switch {
|
||||
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)
|
||||
conn.Failed("domain does not exist", "")
|
||||
return reply(nsutil.NxDomain("nxdomain: " + err.Error()))
|
||||
|
||||
}
|
||||
case errors.Is(err, resolver.ErrBlocked):
|
||||
tracer.Tracef("nameserver: %s", err)
|
||||
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):
|
||||
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", "")
|
||||
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"))
|
||||
return reply(nsutil.ServerFailure("internal error: empty reply"))
|
||||
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 reply(nsutil.NxDomain("no answer found (NXDomain)"))
|
||||
}
|
||||
}
|
||||
|
||||
// Check with firewall again after resolving.
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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(¬ifications.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(¬ifications.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
|
||||
}
|
|
@ -152,11 +152,9 @@ func IsMyIP(ip net.IP) (yes bool, err error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// IsMyNet returns whether the given IP is currently in the host's broadcast
|
||||
// domain - ie. the networks that the host is directly attached to.
|
||||
// Function is optimized with the assumption that is unlikely that the IP is
|
||||
// in the broadcast domain.
|
||||
func IsMyNet(ip net.IP) (yes bool, err error) {
|
||||
// GetLocalNetwork uses the given IP to search for a network configured on the
|
||||
// device and returns it.
|
||||
func GetLocalNetwork(ip net.IP) (myNet *net.IPNet, err error) {
|
||||
myNetworksLock.Lock()
|
||||
defer myNetworksLock.Unlock()
|
||||
|
||||
|
@ -164,16 +162,16 @@ func IsMyNet(ip net.IP) (yes bool, err error) {
|
|||
if myNetworksNetworkChangedFlag.IsSet() {
|
||||
err := refreshMyNetworks()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the IP address is in my networks.
|
||||
for _, myNet := range myNetworks {
|
||||
if myNet.Contains(ip) {
|
||||
return true, nil
|
||||
return myNet, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -37,13 +37,12 @@ var (
|
|||
PortalTestIP = net.IPv4(192, 0, 2, 1)
|
||||
PortalTestURL = fmt.Sprintf("http://%s/", PortalTestIP)
|
||||
|
||||
DNSTestDomain = "one.one.one.one."
|
||||
DNSTestExpectedIP = net.IPv4(1, 1, 1, 1)
|
||||
|
||||
DNSFallbackTestDomain = "dns-check.safing.io."
|
||||
DNSFallbackTestExpectedIP = net.IPv4(0, 65, 67, 75) // Ascii: \0ACK
|
||||
DNSTestDomain = "online-check.safing.io."
|
||||
DNSTestExpectedIP = net.IPv4(0, 65, 67, 75) // Ascii: \0ACK
|
||||
DNSTestQueryFunc func(ctx context.Context, fdqn string) (ips []net.IP, ok bool, err error)
|
||||
|
||||
ConnectedToSPN = abool.New()
|
||||
ConnectedToDNS = abool.New()
|
||||
|
||||
// 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,
|
||||
|
@ -53,8 +52,6 @@ var (
|
|||
// ConnectivityDomains holds all connectivity domains. This slice must not be modified.
|
||||
ConnectivityDomains = []string{
|
||||
SpecialCaptivePortalDomain,
|
||||
DNSTestDomain, // Internal DNS Check
|
||||
DNSFallbackTestDomain, // Internal DNS Check
|
||||
|
||||
// Windows
|
||||
"dns.msftncsi.com.", // DNS Check
|
||||
|
@ -380,20 +377,20 @@ func monitorOnlineStatus(ctx context.Context) error {
|
|||
func getDynamicStatusTrigger() <-chan time.Time {
|
||||
switch GetOnlineStatus() {
|
||||
case StatusOffline:
|
||||
// Will be triggered by network change anyway.
|
||||
return time.After(20 * time.Second)
|
||||
// Will also be triggered by network change.
|
||||
return time.After(10 * time.Second)
|
||||
case StatusLimited, StatusPortal:
|
||||
// Change will not be detected otherwise, but impact is minor.
|
||||
return time.After(5 * time.Second)
|
||||
case StatusSemiOnline:
|
||||
// Very small impact.
|
||||
return time.After(20 * time.Second)
|
||||
return time.After(60 * time.Second)
|
||||
case StatusOnline:
|
||||
// Don't check until resolver reports problems.
|
||||
return nil
|
||||
case StatusUnknown:
|
||||
return time.After(5 * time.Second)
|
||||
default: // other unknown status
|
||||
fallthrough
|
||||
default:
|
||||
return time.After(5 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
@ -407,13 +404,18 @@ func checkOnlineStatus(ctx context.Context) {
|
|||
return StatusUnknown
|
||||
}*/
|
||||
|
||||
// 0) check if connected to SPN
|
||||
// 0) check if connected to SPN and/or DNS.
|
||||
|
||||
if ConnectedToSPN.IsSet() {
|
||||
updateOnlineStatus(StatusOnline, nil, "connected to SPN")
|
||||
return
|
||||
}
|
||||
|
||||
if ConnectedToDNS.IsSet() {
|
||||
updateOnlineStatus(StatusOnline, nil, "connected to DNS")
|
||||
return
|
||||
}
|
||||
|
||||
// 1) check for addresses
|
||||
|
||||
ipv4, ipv6, err := GetAssignedAddresses()
|
||||
|
@ -508,34 +510,28 @@ func checkOnlineStatus(ctx context.Context) {
|
|||
|
||||
// 3) resolve a query
|
||||
|
||||
// Check with primary dns check domain.
|
||||
ips, err := net.LookupIP(DNSTestDomain)
|
||||
if err != nil {
|
||||
log.Warningf("netenv: dns check query failed: %s", err)
|
||||
} else {
|
||||
// check for expected response
|
||||
for _, ip := range ips {
|
||||
if ip.Equal(DNSTestExpectedIP) {
|
||||
// Check if we can resolve the dns check domain.
|
||||
if DNSTestQueryFunc == nil {
|
||||
updateOnlineStatus(StatusOnline, nil, "all checks passed, dns query check disabled")
|
||||
return
|
||||
}
|
||||
ips, ok, err := DNSTestQueryFunc(ctx, DNSTestDomain)
|
||||
switch {
|
||||
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")
|
||||
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
43
network/multicast.go
Normal 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
|
||||
}
|
|
@ -4,11 +4,13 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var cleanDomainRegex = regexp.MustCompile(
|
||||
var (
|
||||
cleanDomainRegex = regexp.MustCompile(
|
||||
`^` + // match beginning
|
||||
`(` + // start subdomain group
|
||||
`(xn--)?` + // idn prefix
|
||||
|
@ -19,6 +21,21 @@ var cleanDomainRegex = regexp.MustCompile(
|
|||
`[a-z0-9_-]{2,63}` + // TLD main chunk with at least two characters
|
||||
`\.` + // ending with a dot
|
||||
`$`, // 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.
|
||||
|
@ -33,15 +50,18 @@ func IsValidFqdn(fqdn string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// check with regex
|
||||
if !cleanDomainRegex.MatchString(fqdn) {
|
||||
// IsFqdn checks if a domain name is fully qualified.
|
||||
if !dns.IsFqdn(fqdn) {
|
||||
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.
|
||||
if !dns.IsFqdn(fqdn) {
|
||||
// check with regex
|
||||
if !cleanDomainRegex.MatchString(fqdn) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,9 @@ func GetIPScope(ip net.IP) IPScope { //nolint:gocognit
|
|||
if ip4 := ip.To4(); ip4 != nil {
|
||||
// IPv4
|
||||
switch {
|
||||
case ip4[0] == 0:
|
||||
// 0.0.0.0/8
|
||||
return Invalid
|
||||
case ip4[0] == 127:
|
||||
// 127.0.0.0/8 (RFC1918)
|
||||
return HostLocal
|
||||
|
@ -79,6 +82,8 @@ func GetIPScope(ip net.IP) IPScope { //nolint:gocognit
|
|||
} else if len(ip) == net.IPv6len {
|
||||
// IPv6
|
||||
switch {
|
||||
case ip.Equal(net.IPv6zero):
|
||||
return Invalid
|
||||
case ip.Equal(net.IPv6loopback):
|
||||
return HostLocal
|
||||
case ip[0]&0xfe == 0xfc:
|
||||
|
@ -124,3 +129,29 @@ func (scope IPScope) IsGlobal() bool {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -67,6 +67,32 @@ func (p *Process) Profile() *profile.LayeredProfile {
|
|||
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
|
||||
// system resolver and needs special handling.
|
||||
func (p *Process) IsSystemResolver() bool {
|
||||
|
|
|
@ -6,8 +6,6 @@ import (
|
|||
"github.com/safing/portbase/database/migration"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
|
||||
// Dependency.
|
||||
_ "github.com/safing/portmaster/core/base"
|
||||
"github.com/safing/portmaster/updates"
|
||||
)
|
||||
|
|
|
@ -224,9 +224,9 @@ The format is: "protocol://ip:port?parameter=value¶meter=value"
|
|||
noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Enforce Secure DNS",
|
||||
Name: "Use Secure Protocols Only",
|
||||
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,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
|
|
|
@ -4,10 +4,13 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/log"
|
||||
|
@ -89,6 +92,11 @@ type Query struct {
|
|||
IgnoreFailing 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
|
||||
dotPrefixedFQDN string
|
||||
}
|
||||
|
@ -98,6 +106,41 @@ func (q *Query) ID() 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.
|
||||
func (q *Query) check() (ok bool) {
|
||||
if q.FQDN == "" {
|
||||
|
@ -318,8 +361,8 @@ func resolveAndCache(ctx context.Context, q *Query, oldCache *RRCache) (rrCache
|
|||
}
|
||||
|
||||
// check if we are online
|
||||
if primarySource != ServerSourceEnv && netenv.GetOnlineStatus() == netenv.StatusOffline {
|
||||
if !netenv.IsConnectivityDomain(q.FQDN) {
|
||||
if netenv.GetOnlineStatus() == netenv.StatusOffline && primarySource != ServerSourceEnv {
|
||||
if q.FQDN != netenv.DNSTestDomain && !netenv.IsConnectivityDomain(q.FQDN) {
|
||||
// we are offline and this is not an online check query
|
||||
return oldCache, ErrOffline
|
||||
}
|
||||
|
@ -358,6 +401,7 @@ resolveLoop:
|
|||
// some resolvers might also block
|
||||
return nil, err
|
||||
case netenv.GetOnlineStatus() == netenv.StatusOffline &&
|
||||
q.FQDN != netenv.DNSTestDomain &&
|
||||
!netenv.IsConnectivityDomain(q.FQDN):
|
||||
// we are offline and this is not an online check query
|
||||
return oldCache, ErrOffline
|
||||
|
@ -478,3 +522,45 @@ func shouldResetCache(q *Query) (reset bool) {
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
q.NoCaching = true
|
||||
|
||||
return &RRCache{
|
||||
rrCache := &RRCache{
|
||||
Domain: q.FQDN,
|
||||
Question: q.QType,
|
||||
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.
|
||||
Resolver: envResolver.Info.Copy(),
|
||||
}
|
||||
if len(rrCache.Answer) == 0 {
|
||||
rrCache.RCode = dns.RcodeNameError
|
||||
}
|
||||
return rrCache
|
||||
}
|
||||
|
||||
func (er *envResolverConn) ReportFailure() {}
|
||||
|
@ -145,3 +149,8 @@ func (er *envResolverConn) IsFailing() bool {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -206,8 +206,8 @@ func (brc *BasicResolverConn) init() {
|
|||
|
||||
// ReportFailure reports that an error occurred with this resolver.
|
||||
func (brc *BasicResolverConn) ReportFailure() {
|
||||
// Don't mark resolver as failed if we are offline.
|
||||
if !netenv.Online() {
|
||||
// don't mark failed if we are offline
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -224,6 +224,11 @@ func (brc *BasicResolverConn) ReportFailure() {
|
|||
// the fail.
|
||||
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.
|
||||
|
@ -255,4 +260,9 @@ func (brc *BasicResolverConn) ResetFailure() {
|
|||
defer brc.failLock.Unlock()
|
||||
brc.fails = 0
|
||||
}
|
||||
|
||||
// Report to netenv that a configured server succeeded.
|
||||
if brc.resolver.Info.Source == ServerSourceConfigured {
|
||||
netenv.ConnectedToDNS.Set()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
@ -103,3 +104,57 @@ func TestBulkResolving(t *testing.T) {
|
|||
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue