mirror of
https://github.com/safing/portmaster
synced 2025-09-02 10:39:22 +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 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,14 +112,16 @@ 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 {
|
|
||||||
selfCheckIsFailing.UnSet()
|
|
||||||
selfcheckFails = 0
|
|
||||||
|
|
||||||
// Only log internal errors, but don't notify.
|
return nil
|
||||||
tracer.Warningf("compat: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset self-check state.
|
||||||
|
selfcheckNetworkChangedFlag.Refresh()
|
||||||
|
selfCheckIsFailing.UnSet()
|
||||||
|
selfcheckFails = 0
|
||||||
|
resetSystemIssue()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
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/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(¬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 {
|
func stop() error {
|
||||||
stopListenersLock.Lock()
|
stopListenersLock.Lock()
|
||||||
defer stopListenersLock.Unlock()
|
defer stopListenersLock.Unlock()
|
||||||
|
|
|
@ -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):
|
||||||
tracer.Tracef("nameserver: %s", err)
|
// Try alternatives domain names for unofficial domain spaces.
|
||||||
conn.Failed("domain does not exist", "")
|
rrCache = checkAlternativeCaches(ctx, q)
|
||||||
return reply(nsutil.NxDomain("nxdomain: " + err.Error()))
|
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):
|
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,8 +296,12 @@ 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:
|
||||||
// Return now if NXDomain.
|
// Try alternatives domain names for unofficial domain spaces.
|
||||||
return reply(nsutil.NxDomain("no answer found (NXDomain)"))
|
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.
|
// Check with firewall again after resolving.
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
} else {
|
|
||||||
// check for expected response
|
|
||||||
for _, ip := range ips {
|
|
||||||
if ip.Equal(DNSTestExpectedIP) {
|
|
||||||
updateOnlineStatus(StatusOnline, nil, "all checks passed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
return
|
||||||
}
|
}
|
||||||
// check for expected response
|
ips, ok, err := DNSTestQueryFunc(ctx, DNSTestDomain)
|
||||||
for _, ip := range ips {
|
switch {
|
||||||
if ip.Equal(DNSFallbackTestExpectedIP) {
|
case ok && err != nil:
|
||||||
updateOnlineStatus(StatusOnline, nil, "all checks passed")
|
updateOnlineStatus(StatusOnline, nil, fmt.Sprintf(
|
||||||
return
|
"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")
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
// 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,21 +4,38 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cleanDomainRegex = regexp.MustCompile(
|
var (
|
||||||
`^` + // match beginning
|
cleanDomainRegex = regexp.MustCompile(
|
||||||
`(` + // start subdomain group
|
`^` + // match beginning
|
||||||
`(xn--)?` + // idn prefix
|
`(` + // start subdomain group
|
||||||
`[a-z0-9_-]{1,63}` + // main chunk
|
`(xn--)?` + // idn prefix
|
||||||
`\.` + // ending with a dot
|
`[a-z0-9_-]{1,63}` + // main chunk
|
||||||
`)*` + // end subdomain group, allow any number of subdomains
|
`\.` + // ending with a dot
|
||||||
`(xn--)?` + // TLD idn prefix
|
`)*` + // end subdomain group, allow any number of subdomains
|
||||||
`[a-z0-9_-]{2,63}` + // TLD main chunk with at least two characters
|
`(xn--)?` + // TLD idn prefix
|
||||||
`\.` + // ending with a dot
|
`[a-z0-9_-]{2,63}` + // TLD main chunk with at least two characters
|
||||||
`$`, // match end
|
`\.` + // 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.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -224,9 +224,9 @@ The format is: "protocol://ip:port?parameter=value¶meter=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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue