Merge pull request #450 from safing/feature/compat

Add compatibility assistant
This commit is contained in:
Daniel 2021-11-17 16:13:34 +01:00 committed by GitHub
commit 2a9e07a86c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 724 additions and 51 deletions

29
compat/api.go Normal file
View file

@ -0,0 +1,29 @@
package compat
import (
"github.com/safing/portbase/api"
)
func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Path: "compat/self-check",
Read: api.PermitUser,
BelongsTo: module,
ActionFunc: selfcheckViaAPI,
Name: "Run Integration Self-Check",
Description: "Runs a couple integration self-checks in order to see if the system integration works.",
}); err != nil {
return err
}
return nil
}
func selfcheckViaAPI(ar *api.Request) (msg string, err error) {
_, err = selfcheck(ar.Context())
if err != nil {
return "", err
}
return "self-check successful", nil
}

36
compat/callbacks.go Normal file
View file

@ -0,0 +1,36 @@
package compat
import (
"net"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process"
)
func SubmitSystemIntegrationCheckPacket(p packet.Packet) {
select {
case systemIntegrationCheckPackets <- p:
default:
}
}
func SubmitDNSCheckDomain(subdomain string) (respondWith net.IP) {
// Submit queried domain.
select {
case dnsCheckReceivedDomain <- subdomain:
default:
}
// Return the answer.
dnsCheckAnswerLock.Lock()
defer dnsCheckAnswerLock.Unlock()
return dnsCheckAnswer
}
func ReportSecureDNSBypassIssue(p *process.Process) {
secureDNSBypassIssue.notify(p)
}
func ReportMultiPeerUDPTunnelIssue(p *process.Process) {
multiPeerUDPTunnelIssue.notify(p)
}

82
compat/module.go Normal file
View file

@ -0,0 +1,82 @@
package compat
import (
"context"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/netenv"
"github.com/tevino/abool"
)
var (
module *modules.Module
selfcheckTask *modules.Task
selfcheckTaskRetryAfter = 10 * time.Second
selfCheckIsFailing = abool.New()
)
func init() {
module = modules.Register("compat", prep, start, stop, "base", "network", "interception", "netenv", "notifications")
}
func prep() error {
return registerAPIEndpoints()
}
func start() error {
selfcheckTask = module.NewTask("compatibility self-check", selfcheckTaskFunc).
Repeat(1 * time.Minute).
StartASAP()
return module.RegisterEventHook(
netenv.ModuleName,
netenv.NetworkChangedEvent,
"trigger compat self-check",
func(_ context.Context, _ interface{}) error {
selfcheckTask.StartASAP()
return nil
},
)
}
func stop() error {
selfcheckTask.Cancel()
selfcheckTask = nil
return nil
}
func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error {
// Run selfcheck and return if successful.
issue, err := selfcheck(ctx)
if err == nil {
selfCheckIsFailing.UnSet()
resetSystemIssue()
return nil
}
// Log result.
if issue != nil {
selfCheckIsFailing.Set()
log.Errorf("compat: %s", err)
issue.notify(err)
// Retry quicker when failed.
task.Schedule(time.Now().Add(selfcheckTaskRetryAfter))
} else {
selfCheckIsFailing.UnSet()
// Only log internal errors, but don't notify.
log.Warningf("compat: %s", err)
}
return nil
}
func SelfCheckIsFailing() bool {
return selfCheckIsFailing.IsSet()
}

166
compat/notify.go Normal file
View file

@ -0,0 +1,166 @@
package compat
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/safing/portmaster/profile"
"github.com/safing/portbase/log"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/process"
)
type baseIssue struct {
id string
title string
message string
level notifications.Type
}
type systemIssue baseIssue
type appIssue baseIssue
var (
systemIssueNotification *notifications.Notification
systemIssueNotificationLock sync.Mutex
systemIntegrationIssue = &systemIssue{
id: "compat:system-integration-issue",
title: "Detected System Integration Issue",
message: "Portmaster detected a problem with its system integration. You can try to restart or reinstall the Portmaster. If that does not help, please report the issue via [GitHub](https://github.com/safing/portmaster/issues) or send a mail to [support@safing.io](mailto:support@safing.io) so we can help you out.",
level: notifications.Error,
}
systemCompatibilityIssue = &systemIssue{
id: "compat:compatibility-issue",
title: "Detected Compatibility Issue",
message: "Portmaster detected that something is interfering with its operation. This could be a VPN, an Anti-Virus or another network protection software. Please check if you are running an incompatible [VPN client](https://docs.safing.io/portmaster/install/status/vpn-compatibility) or [software](https://docs.safing.io/portmaster/install/status/software-compatibility). Otherwise, please report the issue via [GitHub](https://github.com/safing/portmaster/issues) or send a mail to [support@safing.io](mailto:support@safing.io) so we can help you out.",
level: notifications.Error,
}
secureDNSBypassIssue = &appIssue{
id: "compat:secure-dns-bypass-%s",
title: "Detected %s Bypass Attempt",
message: "Portmaster detected that %s is trying to use a secure DNS resolver. While this is a good thing, the Portmaster already handles secure DNS for your whole device. Please disable the secure DNS resolver within the app.",
// TODO: Add this when the new docs page is finished:
// , or [find out about other options](link to new docs page)
level: notifications.Warning,
}
multiPeerUDPTunnelIssue = &appIssue{
id: "compat:multi-peer-udp-tunnel-%s",
title: "Detected SPN Incompatibility in %s",
message: "Portmaster detected that %s is trying to connect to multiple servers via the SPN using a single UDP connection. This is common for technologies such as torrents. Unfortunately, the SPN does not support this feature currently. You can try to change this behavior within the affected app or you could exempt it from using the SPN.",
level: notifications.Warning,
}
)
func (issue *systemIssue) notify(err error) {
systemIssueNotificationLock.Lock()
defer systemIssueNotificationLock.Unlock()
if systemIssueNotification != nil {
// Ignore duplicate notification.
if issue.id == systemIssueNotification.EventID {
return
}
// Remove old notification.
systemIssueNotification.Delete()
}
// Create new notification.
n := &notifications.Notification{
EventID: issue.id,
Type: issue.level,
Title: issue.title,
Message: issue.message,
ShowOnSystem: true,
}
notifications.Notify(n)
systemIssueNotification = n
n.AttachToModule(module)
// Report the raw error as module error.
module.NewErrorMessage("selfcheck", err).Report()
}
func resetSystemIssue() {
systemIssueNotificationLock.Lock()
defer systemIssueNotificationLock.Unlock()
if systemIssueNotification != nil {
systemIssueNotification.Delete()
}
systemIssueNotification = nil
}
func (issue *appIssue) notify(proc *process.Process) {
// Get profile from process.
p := proc.Profile().LocalProfile()
if p == nil {
return
}
// Ignore notifications for unidentified processes.
if p.ID == profile.UnidentifiedProfileID {
return
}
// Log warning.
log.Warningf(
"compat: detected %s issue with %s",
strings.ReplaceAll(
strings.TrimPrefix(
strings.TrimSuffix(issue.id, "-%d"),
"compat:",
),
"-", " ",
),
proc.Path,
)
// Check if we already have this notification.
eventID := fmt.Sprintf(issue.id, p.ID)
n := notifications.Get(eventID)
if n != nil {
return
}
// Otherwise, create a new one.
n = &notifications.Notification{
EventID: eventID,
Type: issue.level,
Title: fmt.Sprintf(issue.title, p.Name),
Message: fmt.Sprintf(issue.message, p.Name),
ShowOnSystem: true,
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
},
}
notifications.Notify(n)
// Set warning on profile.
module.StartWorker("set app compat warning", func(ctx context.Context) error {
func() {
p.Lock()
defer p.Unlock()
p.Warning = fmt.Sprintf(
"%s \nThis was last detected at %s.",
fmt.Sprintf(issue.message, p.Name),
time.Now().Format("15:04 on 2.1.2006"),
)
p.WarningLastUpdated = time.Now()
}()
return p.Save()
})
}

193
compat/selfcheck.go Normal file
View file

@ -0,0 +1,193 @@
package compat
import (
"context"
"encoding/hex"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/rng"
"github.com/safing/portmaster/network/packet"
)
var (
selfcheckLock sync.Mutex
SystemIntegrationCheckDstIP = net.IPv4(127, 65, 67, 75)
SystemIntegrationCheckProtocol = packet.AnyHostInternalProtocol61
systemIntegrationCheckDialNet = fmt.Sprintf("ip4:%d", uint8(SystemIntegrationCheckProtocol))
systemIntegrationCheckDialIP = SystemIntegrationCheckDstIP.String()
systemIntegrationCheckPackets = make(chan packet.Packet, 1)
systemIntegrationCheckWaitDuration = 3 * time.Second
DNSCheckInternalDomainScope string
dnsCheckReceivedDomain = make(chan string, 1)
dnsCheckWaitDuration = 3 * time.Second
dnsCheckAnswerLock sync.Mutex
dnsCheckAnswer net.IP
DNSTestDomain = "one.one.one.one."
DNSTestExpectedIP = net.IPv4(1, 1, 1, 1)
)
func selfcheck(ctx context.Context) (issue *systemIssue, err error) {
selfcheckLock.Lock()
defer selfcheckLock.Unlock()
// Step 1: Check if the system integration sees a packet.
// Empty recv channel.
select {
case <-systemIntegrationCheckPackets:
case <-ctx.Done():
return nil, context.Canceled
default:
}
// Send packet.
conn, err := net.DialTimeout(
systemIntegrationCheckDialNet,
systemIntegrationCheckDialIP,
time.Second,
)
if err != nil {
return nil, fmt.Errorf("failed to create system integration conn: %w", err)
}
_, err = conn.Write([]byte("SELF-CHECK"))
if err != nil {
return nil, fmt.Errorf("failed to send system integration packet: %w", err)
}
// Wait for packet.
select {
case <-systemIntegrationCheckPackets:
// Check passed!
log.Tracef("compat: self-check #1: system integration check passed")
case <-time.After(systemIntegrationCheckWaitDuration):
return systemIntegrationIssue, fmt.Errorf("self-check #1: system integration check failed: did not receive test packet after %s", systemIntegrationCheckWaitDuration)
case <-ctx.Done():
return nil, context.Canceled
}
// Step 2: Check if a DNS request arrives at the nameserver
// This step necessary also includes some setup for step 3.
// Generate random subdomain.
randomSubdomainBytes, err := rng.Bytes(16)
if err != nil {
return nil, fmt.Errorf("self-check #2: failed to get random bytes for subdomain check: %w", err)
}
randomSubdomain := "a" + strings.ToLower(hex.EncodeToString(randomSubdomainBytes)) + "b"
// Generate random answer.
var B, C, D uint64
B, err = rng.Number(255)
if err == nil {
C, err = rng.Number(255)
}
if err == nil {
D, err = rng.Number(255)
}
if err != nil {
return nil, fmt.Errorf("self-check #2: failed to get random number for subdomain check response: %w", err)
}
randomAnswer := net.IPv4(127, byte(B), byte(C), byte(D))
func() {
dnsCheckAnswerLock.Lock()
defer dnsCheckAnswerLock.Unlock()
dnsCheckAnswer = randomAnswer
}()
// Setup variables for lookup worker.
var (
dnsCheckReturnedIP net.IP
dnsCheckLookupError = make(chan error)
)
// Empty recv channel.
select {
case <-dnsCheckReceivedDomain:
case <-ctx.Done():
return nil, context.Canceled
default:
}
// Start worker for the DNS lookup.
module.StartWorker("dns check lookup", func(_ context.Context) error {
ips, err := net.LookupIP(randomSubdomain + DNSCheckInternalDomainScope)
if err == nil && len(ips) > 0 {
dnsCheckReturnedIP = ips[0]
}
select {
case dnsCheckLookupError <- err:
case <-time.After(dnsCheckWaitDuration * 2):
case <-ctx.Done():
}
return nil
})
// Wait for the resolver to receive the query.
select {
case receivedTestDomain := <-dnsCheckReceivedDomain:
if receivedTestDomain != randomSubdomain {
return systemCompatibilityIssue, fmt.Errorf("self-check #2: dns integration check failed: received unmatching subdomain %q", receivedTestDomain)
}
case <-time.After(dnsCheckWaitDuration):
return systemCompatibilityIssue, fmt.Errorf("self-check #2: dns integration check failed: did not receive test query after %s", dnsCheckWaitDuration)
}
log.Tracef("compat: self-check #2: dns integration query check passed")
// Step 3: Have the nameserver respond with random data in the answer section.
// Wait for the reply from the resolver.
select {
case err := <-dnsCheckLookupError:
if err != nil {
return systemCompatibilityIssue, fmt.Errorf("self-check #3: dns integration check failed: failed to receive test response: %w", err)
}
case <-time.After(dnsCheckWaitDuration):
return systemCompatibilityIssue, fmt.Errorf("self-check #3: dns integration check failed: did not receive test response after %s", dnsCheckWaitDuration)
case <-ctx.Done():
return nil, context.Canceled
}
// Check response.
if !dnsCheckReturnedIP.Equal(randomAnswer) {
return systemCompatibilityIssue, fmt.Errorf("self-check #3: dns integration check failed: received unmatching response %q", dnsCheckReturnedIP)
}
log.Tracef("compat: self-check #3: dns integration response check passed")
return nil, nil
}
/*
* Check if the system integration sees a packet:
* Send raw IP packet with random content and protocol, report finding to compat module.
* use `Dial("ip4:61", "127.65.67.75")`.
* Firewall reports back the data seen on `ip4:61` to IP `127.65.67.75`.
* If this fails, the system integration is broken. -&gt; Integration Issue
* Check if a DNS request arrives at the nameserver:
* Send A question for `[random-subdomain].self-check.portmaster.home.arpa.`.
* Nameserver reports back the data seen.
* If this fails, redirection to the nameserver fails.
* This means there is another software interfering with DNS. -&gt; Compatibility Issue
* Have the nameserver respond with random data in the answer section.
* Compat provides nameserver with random response data.
* Compat module checks if the received data matches.
* If this fails, redirection to the nameserver fails.
* This means there is another software interfering with DNS on the return path. -&gt; Compatibility Issue
* DROPPED: If resolvers are reported failing, but we are online:
* Send out plain DNS requests to one.one.one.one. and dns.quad9.net via the Go standard lookup and check if the responses are correct.
* If not, something is blocking the Portmaster -&gt; Secure DNS Issue
* Discuss if this is necessary:
* Does this improve from only having a failed TCP connection to the resolver?
* Could another program block port 853, but fully leave requests for one.one.one.one. to port 53 alone?
*/

View file

@ -27,7 +27,7 @@ var (
)
func init() {
module = modules.Register("core", prep, start, nil, "base", "subsystems", "status", "updates", "api", "notifications", "ui", "netenv", "network", "interception")
module = modules.Register("core", prep, start, nil, "base", "subsystems", "status", "updates", "api", "notifications", "ui", "netenv", "network", "interception", "compat")
subsystems.Register(
"core",
"Core",

View file

@ -4,6 +4,8 @@ import (
"context"
"strings"
"github.com/safing/portmaster/compat"
"github.com/safing/portmaster/nameserver/nsutil"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/packet"
@ -30,6 +32,7 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
// Make an exception for ICMP, as these IPs are also often used for debugging.
default:
if conn.Entity.MatchLists(resolverFilterLists) {
compat.ReportSecureDNSBypassIssue(conn.Process())
return endpoints.Denied,
"blocked rogue connection to DNS resolver",
nsutil.BlockIP()

View file

@ -170,6 +170,11 @@ func FilterResolvedDNS(
return nil
}
// Don't filter env responses.
if rrCache.Resolver.Type == resolver.ServerTypeEnv {
return rrCache
}
// special grant for connectivity domains
if checkConnectivityDomain(ctx, conn, layeredProfile, nil) {
// returns true if check triggered

View file

@ -9,6 +9,8 @@ import (
"sync/atomic"
"time"
"github.com/safing/portmaster/compat"
"github.com/safing/spn/captain"
"github.com/google/gopacket/layers"
@ -314,6 +316,13 @@ func fastTrackedPermit(pkt packet.Packet) (handled bool) {
_ = pkt.PermanentAccept()
return true
}
case compat.SystemIntegrationCheckProtocol:
if pkt.Info().Dst.Equal(compat.SystemIntegrationCheckDstIP) {
compat.SubmitSystemIntegrationCheckPacket(pkt)
_ = pkt.Drop()
}
return true
}
return false
@ -331,11 +340,19 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
// Set tunnel options.
setCustomTunnelOptionsForPortmaster(conn)
// Redirect outbound DNS packests,
case pkt.IsOutbound() &&
pkt.Info().DstPort == 53 &&
conn.Process().Pid != ownPID &&
// that don't match the address of our nameserver,
nameserverIPMatcherReady.IsSet() &&
!nameserverIPMatcher(pkt.Info().Dst):
!nameserverIPMatcher(pkt.Info().Dst) &&
// and are not broadcast queries by us.
// Context:
// - Unicast queries by the resolver are pre-authenticated.
// - Unicast qeries by the compat self-check should be redirected.
!(conn.Process().Pid == ownPID &&
conn.Entity.IPScope == netutils.LocalMulticast):
// Reroute rogue dns queries back to Portmaster.
conn.Verdict = network.VerdictRerouteToNameserver
conn.Reason.Msg = "redirecting rogue dns query"

1
go.mod
View file

@ -18,6 +18,7 @@ require (
github.com/klauspost/reedsolomon v1.9.13 // indirect
github.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267 // indirect
github.com/miekg/dns v1.1.43
github.com/mr-tron/base58 v1.2.0
github.com/oschwald/maxminddb-golang v1.8.0
github.com/safing/portbase v0.12.3
github.com/safing/spn v0.3.6

View file

@ -241,7 +241,7 @@ func addLocation(dl *DeviceLocation) {
func GetApproximateInternetLocation() (net.IP, error) {
loc, ok := GetInternetLocation()
if !ok || loc.Best() == nil {
return nil, errors.New("no location data available")
return nil, errors.New("no device location data available")
}
return loc.Best().IP, nil
}
@ -259,7 +259,7 @@ func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
// Get all assigned addresses.
v4s, v6s, err := GetAssignedAddresses()
if err != nil {
log.Warningf("netenv: failed to get assigned addresses: %s", err)
log.Warningf("netenv: failed to get assigned addresses for device location: %s", err)
return nil, false
}
@ -267,27 +267,24 @@ func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
v4ok, v6ok := getLocationFromInterfaces()
// Try other methods for missing locations.
if len(v4s) > 0 {
if !v4ok {
if len(v4s) > 0 && !v4ok {
_, err = getLocationFromTraceroute()
if err != nil {
log.Warningf("netenv: failed to get IPv4 from traceroute: %s", err)
log.Warningf("netenv: failed to get IPv4 device location from traceroute: %s", err)
} else {
v4ok = true
}
}
// Get location from timezone as final fallback.
if !v4ok {
v4ok = getLocationFromTimezone(packet.IPv4)
getLocationFromTimezone(packet.IPv4)
}
}
if len(v6s) > 0 && !v6ok {
// TODO
log.Warningf("netenv: could not get IPv6 location")
}
// TODO: Find more ways to get IPv6 device location
// Check if we have any locations.
if !v4ok && !v6ok {
return nil, false
// Get location from timezone as final fallback.
getLocationFromTimezone(packet.IPv6)
}
// Return gathered locations.

View file

@ -145,6 +145,7 @@ var (
onlineStatusInvestigationTrigger = make(chan struct{}, 1)
onlineStatusInvestigationInProgress = abool.NewBool(false)
onlineStatusInvestigationWg sync.WaitGroup
onlineStatusNotification *notifications.Notification
captivePortal = &CaptivePortal{}
captivePortalLock sync.Mutex
@ -186,7 +187,7 @@ func CheckAndGetOnlineStatus() OnlineStatus {
func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string) {
changed := false
// status
// Update online status.
currentStatus := atomic.LoadInt32(onlineStatus)
if status != OnlineStatus(currentStatus) && atomic.CompareAndSwapInt32(onlineStatus, currentStatus, int32(status)) {
// status changed!
@ -196,10 +197,10 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string)
changed = true
}
// captive portal
// Update captive portal.
setCaptivePortal(portalURL)
// trigger event
// Trigger events.
if changed {
module.TriggerEvent(OnlineStatusChangedEvent, status)
if status == StatusPortal {
@ -209,6 +210,9 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string)
}
triggerNetworkChangeCheck()
// Notify user.
notifyOnlineStatus(status)
// Trigger update check when coming (semi) online.
if Online() {
_ = updates.TriggerUpdate(false)
@ -216,11 +220,54 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string)
}
}
func notifyOnlineStatus(status OnlineStatus) {
var eventID, title, message string
// Check if status is worth notifying.
switch status {
case StatusOffline:
eventID = "netenv:online-status:offline"
title = "Device is Offline"
message = "Portmaster did not detect any network connectivity."
case StatusLimited:
eventID = "netenv:online-status:limited"
title = "Limited network connectivity."
message = "Portmaster did detect local network connectivity, but could not detect connectivity to the Internet."
default:
// Delete notification, if present.
if onlineStatusNotification != nil {
onlineStatusNotification.Delete()
onlineStatusNotification = nil
}
return
}
// Update notification if not present or online status changed.
switch {
case onlineStatusNotification == nil:
// Continue creating new notification.
case onlineStatusNotification.EventID == eventID:
// Notification stays the same, stick with the old one.
return
default:
// Delete old notification before triggering updated one.
onlineStatusNotification.Delete()
}
// Create update status notification.
onlineStatusNotification = notifications.Notify(&notifications.Notification{
EventID: eventID,
Type: notifications.Info,
Title: title,
Message: message,
})
}
func setCaptivePortal(portalURL *url.URL) {
captivePortalLock.Lock()
defer captivePortalLock.Unlock()
// delete
// Delete captive portal if no url is supplied.
if portalURL == nil {
captivePortal = &CaptivePortal{}
if captivePortalNotification != nil {
@ -230,12 +277,12 @@ func setCaptivePortal(portalURL *url.URL) {
return
}
// return if unchanged
if portalURL.String() == captivePortal.URL {
// Only set captive portal once per detection.
if captivePortal.URL != "" {
return
}
// set
// Compile captive portal data.
captivePortal = &CaptivePortal{
URL: portalURL.String(),
}
@ -247,7 +294,7 @@ func setCaptivePortal(portalURL *url.URL) {
captivePortal.Domain = portalURL.Hostname()
}
// notify
// Notify user about portal.
captivePortalNotification = notifications.Notify(&notifications.Notification{
EventID: "netenv:captive-portal",
Type: notifications.Info,

View file

@ -30,6 +30,8 @@ const (
ICMPv6 = IPProtocol(58)
UDPLite = IPProtocol(136)
RAW = IPProtocol(255)
AnyHostInternalProtocol61 = IPProtocol(61)
)
// Verdicts

View file

@ -94,7 +94,12 @@ func parseIGMP(packet gopacket.Packet, info *Info) error {
return nil
}
func checkError(packet gopacket.Packet, _ *Info) error {
func checkError(packet gopacket.Packet, info *Info) error {
// Check for known unparseable before checking the error layer.
if info.Protocol == AnyHostInternalProtocol61 {
return nil
}
if err := packet.ErrorLayer(); err != nil {
return err.Error()
}

View file

@ -68,9 +68,16 @@ type Profile struct { //nolint:maligned // not worth the effort
// Name is a human readable name of the profile. It
// defaults to the basename of the application.
Name string
// Description may holds an optional description of the
// Description may hold an optional description of the
// profile or the purpose of the application.
Description string
// Warning may hold an optional warning about this application.
// It may be static or be added later on when the Portmaster detected an
// issue with the application.
Warning string
// WarningLastUpdated holds the timestamp when the Warning field was last
// updated.
WarningLastUpdated time.Time
// Homepage may refer the the website of the application
// vendor.
Homepage string

View file

@ -112,7 +112,7 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
},
)
// Add description to tell users about the quirks of this profile.
systemResolverProfile.Description = `The System DNS Client is a system service that requires special handling. For regular network connections, the configured settings will apply as usual, but DNS requests coming from the System DNS Client are handled in a special way, as they could actually be coming from any other application on the system.
systemResolverProfile.Warning = `The System DNS Client is a system service that requires special handling. For regular network connections, the configured settings will apply as usual, but DNS requests coming from the System DNS Client are handled in a special way, as they could actually be coming from any other application on the system.
In order to respect the app settings of the actual application, DNS requests from the System DNS Client are only subject to the following settings:
@ -179,7 +179,7 @@ func specialProfileNeedsReset(profile *Profile) bool {
switch profile.ID {
case SystemResolverProfileID:
return canBeUpgraded(profile, "1.6.2021")
return canBeUpgraded(profile, "18.11.2021")
case PortmasterAppProfileID:
return canBeUpgraded(profile, "8.9.2021")
default:

View file

@ -4,11 +4,14 @@ import (
"context"
"net"
"strings"
"sync"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/intel"
"github.com/tevino/abool"
// module dependencies
_ "github.com/safing/portmaster/core/base"
@ -105,3 +108,50 @@ func getLocalAddr(network string) net.Addr {
}
return nil
}
var (
failingResolverNotification *notifications.Notification
failingResolverNotificationSet = abool.New()
failingResolverNotificationLock sync.Mutex
)
func notifyAboutFailingResolvers(err error) {
failingResolverNotificationLock.Lock()
defer failingResolverNotificationLock.Unlock()
failingResolverNotificationSet.Set()
// Check if already set.
if failingResolverNotification != nil {
return
}
// Create new notification.
n := &notifications.Notification{
EventID: "resolver:all-configured-resolvers-failed",
Type: notifications.Error,
Title: "Detected DNS Compatibility Issue",
Message: "Portmaster detected that something is interfering with its Secure DNS resolver. This could be a firewall or another secure DNS resolver software. Please check if you are running incompatible [software](https://docs.safing.io/portmaster/install/status/software-compatibility). Otherwise, please report the issue via [GitHub](https://github.com/safing/portmaster/issues) or send a mail to [support@safing.io](mailto:support@safing.io) so we can help you out.",
ShowOnSystem: true,
}
notifications.Notify(n)
failingResolverNotification = n
n.AttachToModule(module)
// Report the raw error as module error.
module.NewErrorMessage("resolving", err).Report()
}
func resetFailingResolversNotification() {
if failingResolverNotificationSet.IsNotSet() {
return
}
failingResolverNotificationLock.Lock()
defer failingResolverNotificationLock.Unlock()
if failingResolverNotification != nil {
failingResolverNotification.Delete()
failingResolverNotification = nil
}
}

View file

@ -7,12 +7,12 @@ import (
"sync"
"time"
"github.com/safing/portmaster/netenv"
"github.com/miekg/dns"
"github.com/safing/portbase/database"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/compat"
"github.com/safing/portmaster/netenv"
)
var (
@ -313,13 +313,13 @@ retry:
func resolveAndCache(ctx context.Context, q *Query, oldCache *RRCache) (rrCache *RRCache, err error) { //nolint:gocognit,gocyclo
// get resolvers
resolvers, tryAll := GetResolversInScope(ctx, q)
resolvers, primarySource, tryAll := GetResolversInScope(ctx, q)
if len(resolvers) == 0 {
return nil, ErrNoCompliance
}
// check if we are online
if netenv.GetOnlineStatus() == netenv.StatusOffline {
if primarySource != ServerSourceEnv && netenv.GetOnlineStatus() == netenv.StatusOffline {
if !netenv.IsConnectivityDomain(q.FQDN) {
// we are offline and this is not an online check query
return oldCache, ErrOffline
@ -391,6 +391,10 @@ resolveLoop:
// Report a successful connection.
resolver.Conn.ResetFailure()
// Reset failing resolvers notification, if querying in global scope.
if primarySource == ServerSourceConfigured {
resetFailingResolversNotification()
}
break resolveLoop
}
@ -401,6 +405,13 @@ resolveLoop:
// tried all resolvers, possibly twice
if i > 1 {
err = fmt.Errorf("all %d query-compliant resolvers failed, last error: %s", len(resolvers), err)
if primarySource == ServerSourceConfigured &&
netenv.Online() && compat.SelfCheckIsFailing() {
notifyAboutFailingResolvers(err)
} else {
resetFailingResolversNotification()
}
}
} else if rrCache == nil /* defensive */ {
err = ErrNotFound

View file

@ -4,15 +4,17 @@ import (
"context"
"fmt"
"net"
"strings"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/compat"
"github.com/safing/portmaster/netenv"
"github.com/safing/portmaster/network/netutils"
)
const (
internalSpecialUseDomain = "17.home.arpa."
internalSpecialUseDomain = "portmaster.home.arpa."
routerDomain = "router.local." + internalSpecialUseDomain
captivePortalDomain = "captiveportal.local." + internalSpecialUseDomain
@ -36,6 +38,7 @@ var (
func prepEnvResolver() (err error) {
netenv.SpecialCaptivePortalDomain = captivePortalDomain
compat.DNSCheckInternalDomainScope = ".self-check." + internalSpecialUseDomain
internalSpecialUseSOA, err = dns.NewRR(fmt.Sprintf(
"%s 17 IN SOA localhost. none.localhost. 0 0 0 0 0",
@ -57,6 +60,7 @@ type envResolverConn struct{}
func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
switch uint16(q.QType) {
case dns.TypeA, dns.TypeAAAA: // We respond with all IPv4/6 addresses we can find.
// Check for exact matches.
switch q.FQDN {
case captivePortalDomain:
// Get IP address of the captive portal.
@ -86,7 +90,23 @@ func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error
return er.nxDomain(q), nil
}
return er.makeRRCache(q, records), nil
}
// Check for suffix matches.
switch {
case strings.HasSuffix(q.FQDN, compat.DNSCheckInternalDomainScope):
subdomain := strings.TrimSuffix(q.FQDN, compat.DNSCheckInternalDomainScope)
respondWith := compat.SubmitDNSCheckDomain(subdomain)
// We'll get an A record. Only respond if it's an A question.
if respondWith != nil && uint16(q.QType) == dns.TypeA {
records, err := netutils.IPsToRRs(q.FQDN, []net.IP{respondWith})
if err != nil {
log.Warningf("nameserver: failed to create dns check response to %s: %s", q.FQDN, err)
return er.nxDomain(q), nil
}
return er.makeRRCache(q, records), nil
}
}
case dns.TypeSOA:
// Direct query for the SOA record.

View file

@ -9,11 +9,9 @@ import (
"sync"
"time"
"github.com/safing/portmaster/network/netutils"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/netutils"
)
// DNS Classes

View file

@ -145,10 +145,16 @@ func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolve
// Connect to server.
conn, err := tr.dnsClient.Dial(tr.resolver.ServerAddress)
if err != nil {
log.Debugf("resolver: failed to connect to %s", tr.resolver.Info.DescriptiveName())
// Hint network environment at failed connection.
netenv.ReportFailedConnection()
log.Debugf("resolver: failed to connect to %s: %s", tr.resolver.Info.DescriptiveName(), err)
return nil, fmt.Errorf("%w: failed to connect to %s: %s", ErrFailure, tr.resolver.Info.DescriptiveName(), err)
}
// Hint network environment at successful connection.
netenv.ReportSuccessfulConnection()
// Log that a connection to the resolver was established.
log.Debugf(
"resolver: connected to %s",
@ -174,9 +180,6 @@ func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolve
// Set resolver conn for reuse.
tr.resolverConn = resolverConn
// Hint network environment at successful connection.
netenv.ReportSuccessfulConnection()
return resolverConn, nil
}

View file

@ -112,7 +112,8 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
}
scope := netutils.GetIPScope(ip)
if scope.IsLocalhost() {
// Skip localhost resolvers from the OS, but not if configured.
if scope.IsLocalhost() && source == ServerSourceOperatingSystem {
return nil, true, nil // skip
}

View file

@ -109,20 +109,20 @@ func domainInScope(dotPrefixedFQDN string, scopeList []string) bool {
}
// GetResolversInScope returns all resolvers that are in scope the resolve the given query and options.
func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver, tryAll bool) { //nolint:gocognit // TODO
func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver, primarySource string, tryAll bool) { //nolint:gocognit // TODO
resolversLock.RLock()
defer resolversLock.RUnlock()
// Internal use domains
if domainInScope(q.dotPrefixedFQDN, internalSpecialUseDomains) {
return envResolvers, false
return envResolvers, ServerSourceEnv, false
}
// Special connectivity domains
if netenv.IsConnectivityDomain(q.FQDN) && len(systemResolvers) > 0 {
// Do not do compliance checks for connectivity domains.
selected = append(selected, systemResolvers...) // dhcp assigned resolvers
return selected, false
return selected, ServerSourceOperatingSystem, false
}
// Prioritize search scopes
@ -137,7 +137,7 @@ func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver, t
selected = addResolvers(ctx, q, selected, mDNSResolvers)
selected = addResolvers(ctx, q, selected, localResolvers)
selected = addResolvers(ctx, q, selected, systemResolvers)
return selected, true
return selected, ServerSourceMDNS, true
}
// Special use domains
@ -145,12 +145,12 @@ func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver, t
domainInScope(q.dotPrefixedFQDN, specialServiceDomains) {
selected = addResolvers(ctx, q, selected, localResolvers)
selected = addResolvers(ctx, q, selected, systemResolvers)
return selected, true
return selected, "special", true
}
// Global domains
selected = addResolvers(ctx, q, selected, globalResolvers)
return selected, false
return selected, ServerSourceConfigured, false
}
func addResolvers(ctx context.Context, q *Query, selected []*Resolver, addResolvers []*Resolver) []*Resolver {