diff --git a/compat/module.go b/compat/module.go index 1517f2cc..d1a27628 100644 --- a/compat/module.go +++ b/compat/module.go @@ -16,7 +16,7 @@ var ( module *modules.Module selfcheckTask *modules.Task - selfcheckTaskRetryAfter = 10 * time.Second + selfcheckTaskRetryAfter = 5 * time.Second // selfCheckIsFailing holds whether or not the self-check is currently // failing. This helps other failure systems to not make noise when there is @@ -47,6 +47,8 @@ func prep() error { } func start() error { + startNotify() + selfcheckTask = module.NewTask("compatibility self-check", selfcheckTaskFunc). Repeat(5 * time.Minute). MaxDelay(selfcheckTaskRetryAfter). @@ -74,12 +76,18 @@ func stop() error { } func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error { + // Create tracing logger. + ctx, tracer := log.AddTracer(ctx) + defer tracer.Submit() + tracer.Tracef("compat: running self-check") + // Run selfcheck and return if successful. issue, err := selfcheck(ctx) if err == nil { selfCheckIsFailing.UnSet() selfcheckFails = 0 resetSystemIssue() + tracer.Debugf("compat: self-check successful") return nil } @@ -88,7 +96,7 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error { selfCheckIsFailing.Set() selfcheckFails++ - log.Errorf("compat: %s", err) + tracer.Errorf("compat: %s", err) if selfcheckFails >= selfcheckFailThreshold { issue.notify(err) } @@ -100,7 +108,7 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error { selfcheckFails = 0 // Only log internal errors, but don't notify. - log.Warningf("compat: %s", err) + tracer.Warningf("compat: %s", err) } return nil diff --git a/compat/notify.go b/compat/notify.go index 04beeff7..b483ead5 100644 --- a/compat/notify.go +++ b/compat/notify.go @@ -3,10 +3,12 @@ package compat import ( "context" "fmt" + "net" "strings" "sync" "time" + "github.com/safing/portbase/config" "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portbase/notifications" @@ -15,10 +17,11 @@ import ( ) type baseIssue struct { - id string //nolint:structcheck // Inherited. - title string //nolint:structcheck // Inherited. - message string //nolint:structcheck // Inherited. - level notifications.Type //nolint:structcheck // Inherited. + id string //nolint:structcheck // Inherited. + title string //nolint:structcheck // Inherited. + message string //nolint:structcheck // Inherited. + level notifications.Type //nolint:structcheck // Inherited. + actions []*notifications.Action //nolint:structcheck // Inherited. } type systemIssue baseIssue @@ -26,6 +29,10 @@ type systemIssue baseIssue type appIssue baseIssue var ( + // Copy of firewall.CfgOptionDNSQueryInterceptionKey. + cfgOptionDNSQueryInterceptionKey = "filter/dnsQueryInterception" + dnsQueryInterception config.BoolOption + systemIssueNotification *notifications.Notification systemIssueNotificationLock sync.Mutex @@ -41,6 +48,22 @@ var ( 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, } + // manualDNSSetupRequired is additionally initialized in startNotify(). + manualDNSSetupRequired = &systemIssue{ + id: "compat:manual-dns-setup-required", + title: "Manual DNS Setup Required", + level: notifications.Error, + actions: []*notifications.Action{ + { + Text: "Revert", + Type: notifications.ActionTypeOpenSetting, + Payload: ¬ifications.ActionTypeOpenSettingPayload{ + Key: cfgOptionDNSQueryInterceptionKey, + }, + }, + }, + } + manualDNSSetupRequiredMessage = "You have disabled Seamless DNS Integration. As a result, Portmaster can no longer protect you or filter connections reliably. To fix this, you have to manually configure %s as the DNS Server in your system and in any conflicting application. This message will disappear 10 seconds after correct configuration." secureDNSBypassIssue = &appIssue{ id: "compat:secure-dns-bypass-%s", @@ -58,6 +81,37 @@ var ( } ) +func startNotify() { + dnsQueryInterception = config.Concurrent.GetAsBool(cfgOptionDNSQueryInterceptionKey, true) + + systemIssueNotificationLock.Lock() + defer systemIssueNotificationLock.Unlock() + + manualDNSSetupRequired.message = fmt.Sprintf( + manualDNSSetupRequiredMessage, + `"127.0.0.1"`, + ) +} + +// SetNameserverListenIP sets the IP address the nameserver is listening on. +// The IP address is used in compatibility notifications. +func SetNameserverListenIP(ip net.IP) { + systemIssueNotificationLock.Lock() + defer systemIssueNotificationLock.Unlock() + + manualDNSSetupRequired.message = fmt.Sprintf( + manualDNSSetupRequiredMessage, + `"`+ip.String()+`"`, + ) +} + +func systemCompatOrManualDNSIssue() *systemIssue { + if dnsQueryInterception() { + return systemCompatibilityIssue + } + return manualDNSSetupRequired +} + func (issue *systemIssue) notify(err error) { systemIssueNotificationLock.Lock() defer systemIssueNotificationLock.Unlock() @@ -74,11 +128,12 @@ func (issue *systemIssue) notify(err error) { // Create new notification. n := ¬ifications.Notification{ - EventID: issue.id, - Type: issue.level, - Title: issue.title, - Message: issue.message, - ShowOnSystem: true, + EventID: issue.id, + Type: issue.level, + Title: issue.title, + Message: issue.message, + ShowOnSystem: true, + AvailableActions: issue.actions, } notifications.Notify(n) @@ -141,17 +196,20 @@ func (issue *appIssue) notify(proc *process.Process) { // Create a new notification. n = ¬ifications.Notification{ - EventID: eventID, - Type: issue.level, - Title: fmt.Sprintf(issue.title, p.Name), - Message: message, - ShowOnSystem: true, - AvailableActions: []*notifications.Action{ + EventID: eventID, + Type: issue.level, + Title: fmt.Sprintf(issue.title, p.Name), + Message: message, + ShowOnSystem: true, + AvailableActions: issue.actions, + } + if len(n.AvailableActions) == 0 { + n.AvailableActions = []*notifications.Action{ { ID: "ack", Text: "OK", }, - }, + } } notifications.Notify(n) diff --git a/compat/selfcheck.go b/compat/selfcheck.go index 1931b70e..c1508d12 100644 --- a/compat/selfcheck.go +++ b/compat/selfcheck.go @@ -28,12 +28,12 @@ var ( systemIntegrationCheckDialNet = fmt.Sprintf("ip4:%d", uint8(SystemIntegrationCheckProtocol)) systemIntegrationCheckDialIP = SystemIntegrationCheckDstIP.String() systemIntegrationCheckPackets = make(chan packet.Packet, 1) - systemIntegrationCheckWaitDuration = 30 * time.Second + systemIntegrationCheckWaitDuration = 20 * time.Second // DNSCheckInternalDomainScope is the domain scope to use for dns checks. DNSCheckInternalDomainScope = ".self-check." + resolver.InternalSpecialUseDomain dnsCheckReceivedDomain = make(chan string, 1) - dnsCheckWaitDuration = 30 * time.Second + dnsCheckWaitDuration = 20 * time.Second dnsCheckAnswerLock sync.Mutex dnsCheckAnswer net.IP ) @@ -61,7 +61,7 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) { if err != nil { return nil, fmt.Errorf("failed to create system integration conn: %w", err) } - _, err = conn.Write([]byte("SELF-CHECK")) + _, err = conn.Write([]byte("PORTMASTER SELF CHECK")) if err != nil { return nil, fmt.Errorf("failed to send system integration packet: %w", err) } @@ -70,7 +70,7 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) { select { case <-systemIntegrationCheckPackets: // Check passed! - log.Tracef("compat: self-check #1: system integration check passed") + log.Tracer(ctx).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(): @@ -139,12 +139,12 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) { select { case receivedTestDomain := <-dnsCheckReceivedDomain: if receivedTestDomain != randomSubdomain { - return systemCompatibilityIssue, fmt.Errorf("self-check #2: dns integration check failed: received unmatching subdomain %q", receivedTestDomain) + return systemCompatOrManualDNSIssue(), 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) + return systemCompatOrManualDNSIssue(), 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") + log.Tracer(ctx).Tracef("compat: self-check #2: dns integration query check passed") // Step 3: Have the nameserver respond with random data in the answer section. @@ -164,7 +164,7 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) { 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") + log.Tracer(ctx).Tracef("compat: self-check #3: dns integration response check passed") return nil, nil } diff --git a/nameserver/module.go b/nameserver/module.go index 7bc177e4..1c1dc871 100644 --- a/nameserver/module.go +++ b/nameserver/module.go @@ -13,6 +13,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portbase/modules/subsystems" + "github.com/safing/portmaster/compat" "github.com/safing/portmaster/firewall" "github.com/safing/portmaster/netenv" ) @@ -53,6 +54,9 @@ func start() error { return fmt.Errorf("failed to parse nameserver listen address: %w", err) } + // Tell the compat module where we are listening. + compat.SetNameserverListenIP(ip1) + // Get own hostname. hostname, err = os.Hostname() if err != nil { diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 4c5bf114..a92ca8ad 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -168,7 +168,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) } default: - tracer.Warningf("nameserver: external request for %s%s, ignoring", q.FQDN, q.QType) + tracer.Warningf("nameserver: external request from %s for %s%s, ignoring", remoteAddr, q.FQDN, q.QType) return reply(nsutil.Refused("external queries are not permitted")) } conn.Lock()