mirror of
https://github.com/safing/portmaster
synced 2025-09-02 10:39:22 +00:00
Merge pull request #450 from safing/feature/compat
Add compatibility assistant
This commit is contained in:
commit
2a9e07a86c
23 changed files with 724 additions and 51 deletions
29
compat/api.go
Normal file
29
compat/api.go
Normal 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
36
compat/callbacks.go
Normal 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
82
compat/module.go
Normal 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
166
compat/notify.go
Normal 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 := ¬ifications.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 = ¬ifications.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
193
compat/selfcheck.go
Normal 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. -> 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. -> 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. -> 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 -> 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?
|
||||||
|
|
||||||
|
*/
|
|
@ -27,7 +27,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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(
|
subsystems.Register(
|
||||||
"core",
|
"core",
|
||||||
"Core",
|
"Core",
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/compat"
|
||||||
|
|
||||||
"github.com/safing/portmaster/nameserver/nsutil"
|
"github.com/safing/portmaster/nameserver/nsutil"
|
||||||
"github.com/safing/portmaster/network"
|
"github.com/safing/portmaster/network"
|
||||||
"github.com/safing/portmaster/network/packet"
|
"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.
|
// Make an exception for ICMP, as these IPs are also often used for debugging.
|
||||||
default:
|
default:
|
||||||
if conn.Entity.MatchLists(resolverFilterLists) {
|
if conn.Entity.MatchLists(resolverFilterLists) {
|
||||||
|
compat.ReportSecureDNSBypassIssue(conn.Process())
|
||||||
return endpoints.Denied,
|
return endpoints.Denied,
|
||||||
"blocked rogue connection to DNS resolver",
|
"blocked rogue connection to DNS resolver",
|
||||||
nsutil.BlockIP()
|
nsutil.BlockIP()
|
||||||
|
|
|
@ -170,6 +170,11 @@ func FilterResolvedDNS(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't filter env responses.
|
||||||
|
if rrCache.Resolver.Type == resolver.ServerTypeEnv {
|
||||||
|
return rrCache
|
||||||
|
}
|
||||||
|
|
||||||
// special grant for connectivity domains
|
// special grant for connectivity domains
|
||||||
if checkConnectivityDomain(ctx, conn, layeredProfile, nil) {
|
if checkConnectivityDomain(ctx, conn, layeredProfile, nil) {
|
||||||
// returns true if check triggered
|
// returns true if check triggered
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/compat"
|
||||||
|
|
||||||
"github.com/safing/spn/captain"
|
"github.com/safing/spn/captain"
|
||||||
|
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
|
@ -314,6 +316,13 @@ func fastTrackedPermit(pkt packet.Packet) (handled bool) {
|
||||||
_ = pkt.PermanentAccept()
|
_ = pkt.PermanentAccept()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case compat.SystemIntegrationCheckProtocol:
|
||||||
|
if pkt.Info().Dst.Equal(compat.SystemIntegrationCheckDstIP) {
|
||||||
|
compat.SubmitSystemIntegrationCheckPacket(pkt)
|
||||||
|
_ = pkt.Drop()
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
@ -331,11 +340,19 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
|
||||||
// Set tunnel options.
|
// Set tunnel options.
|
||||||
setCustomTunnelOptionsForPortmaster(conn)
|
setCustomTunnelOptionsForPortmaster(conn)
|
||||||
|
|
||||||
|
// Redirect outbound DNS packests,
|
||||||
case pkt.IsOutbound() &&
|
case pkt.IsOutbound() &&
|
||||||
pkt.Info().DstPort == 53 &&
|
pkt.Info().DstPort == 53 &&
|
||||||
conn.Process().Pid != ownPID &&
|
// that don't match the address of our nameserver,
|
||||||
nameserverIPMatcherReady.IsSet() &&
|
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.
|
// Reroute rogue dns queries back to Portmaster.
|
||||||
conn.Verdict = network.VerdictRerouteToNameserver
|
conn.Verdict = network.VerdictRerouteToNameserver
|
||||||
conn.Reason.Msg = "redirecting rogue dns query"
|
conn.Reason.Msg = "redirecting rogue dns query"
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -18,6 +18,7 @@ require (
|
||||||
github.com/klauspost/reedsolomon v1.9.13 // indirect
|
github.com/klauspost/reedsolomon v1.9.13 // indirect
|
||||||
github.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267 // indirect
|
github.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267 // indirect
|
||||||
github.com/miekg/dns v1.1.43
|
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/oschwald/maxminddb-golang v1.8.0
|
||||||
github.com/safing/portbase v0.12.3
|
github.com/safing/portbase v0.12.3
|
||||||
github.com/safing/spn v0.3.6
|
github.com/safing/spn v0.3.6
|
||||||
|
|
|
@ -241,7 +241,7 @@ func addLocation(dl *DeviceLocation) {
|
||||||
func GetApproximateInternetLocation() (net.IP, error) {
|
func GetApproximateInternetLocation() (net.IP, error) {
|
||||||
loc, ok := GetInternetLocation()
|
loc, ok := GetInternetLocation()
|
||||||
if !ok || loc.Best() == nil {
|
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
|
return loc.Best().IP, nil
|
||||||
}
|
}
|
||||||
|
@ -259,7 +259,7 @@ func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
|
||||||
// Get all assigned addresses.
|
// Get all assigned addresses.
|
||||||
v4s, v6s, err := GetAssignedAddresses()
|
v4s, v6s, err := GetAssignedAddresses()
|
||||||
if err != nil {
|
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
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,27 +267,24 @@ func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
|
||||||
v4ok, v6ok := getLocationFromInterfaces()
|
v4ok, v6ok := getLocationFromInterfaces()
|
||||||
|
|
||||||
// Try other methods for missing locations.
|
// Try other methods for missing locations.
|
||||||
if len(v4s) > 0 {
|
if len(v4s) > 0 && !v4ok {
|
||||||
if !v4ok {
|
|
||||||
_, err = getLocationFromTraceroute()
|
_, err = getLocationFromTraceroute()
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
v4ok = true
|
v4ok = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Get location from timezone as final fallback.
|
||||||
if !v4ok {
|
if !v4ok {
|
||||||
v4ok = getLocationFromTimezone(packet.IPv4)
|
getLocationFromTimezone(packet.IPv4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(v6s) > 0 && !v6ok {
|
if len(v6s) > 0 && !v6ok {
|
||||||
// TODO
|
// TODO: Find more ways to get IPv6 device location
|
||||||
log.Warningf("netenv: could not get IPv6 location")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have any locations.
|
// Get location from timezone as final fallback.
|
||||||
if !v4ok && !v6ok {
|
getLocationFromTimezone(packet.IPv6)
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return gathered locations.
|
// Return gathered locations.
|
||||||
|
|
|
@ -145,6 +145,7 @@ var (
|
||||||
onlineStatusInvestigationTrigger = make(chan struct{}, 1)
|
onlineStatusInvestigationTrigger = make(chan struct{}, 1)
|
||||||
onlineStatusInvestigationInProgress = abool.NewBool(false)
|
onlineStatusInvestigationInProgress = abool.NewBool(false)
|
||||||
onlineStatusInvestigationWg sync.WaitGroup
|
onlineStatusInvestigationWg sync.WaitGroup
|
||||||
|
onlineStatusNotification *notifications.Notification
|
||||||
|
|
||||||
captivePortal = &CaptivePortal{}
|
captivePortal = &CaptivePortal{}
|
||||||
captivePortalLock sync.Mutex
|
captivePortalLock sync.Mutex
|
||||||
|
@ -186,7 +187,7 @@ func CheckAndGetOnlineStatus() OnlineStatus {
|
||||||
func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string) {
|
func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string) {
|
||||||
changed := false
|
changed := false
|
||||||
|
|
||||||
// status
|
// Update online status.
|
||||||
currentStatus := atomic.LoadInt32(onlineStatus)
|
currentStatus := atomic.LoadInt32(onlineStatus)
|
||||||
if status != OnlineStatus(currentStatus) && atomic.CompareAndSwapInt32(onlineStatus, currentStatus, int32(status)) {
|
if status != OnlineStatus(currentStatus) && atomic.CompareAndSwapInt32(onlineStatus, currentStatus, int32(status)) {
|
||||||
// status changed!
|
// status changed!
|
||||||
|
@ -196,10 +197,10 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string)
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// captive portal
|
// Update captive portal.
|
||||||
setCaptivePortal(portalURL)
|
setCaptivePortal(portalURL)
|
||||||
|
|
||||||
// trigger event
|
// Trigger events.
|
||||||
if changed {
|
if changed {
|
||||||
module.TriggerEvent(OnlineStatusChangedEvent, status)
|
module.TriggerEvent(OnlineStatusChangedEvent, status)
|
||||||
if status == StatusPortal {
|
if status == StatusPortal {
|
||||||
|
@ -209,6 +210,9 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string)
|
||||||
}
|
}
|
||||||
triggerNetworkChangeCheck()
|
triggerNetworkChangeCheck()
|
||||||
|
|
||||||
|
// Notify user.
|
||||||
|
notifyOnlineStatus(status)
|
||||||
|
|
||||||
// Trigger update check when coming (semi) online.
|
// Trigger update check when coming (semi) online.
|
||||||
if Online() {
|
if Online() {
|
||||||
_ = updates.TriggerUpdate(false)
|
_ = 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(¬ifications.Notification{
|
||||||
|
EventID: eventID,
|
||||||
|
Type: notifications.Info,
|
||||||
|
Title: title,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func setCaptivePortal(portalURL *url.URL) {
|
func setCaptivePortal(portalURL *url.URL) {
|
||||||
captivePortalLock.Lock()
|
captivePortalLock.Lock()
|
||||||
defer captivePortalLock.Unlock()
|
defer captivePortalLock.Unlock()
|
||||||
|
|
||||||
// delete
|
// Delete captive portal if no url is supplied.
|
||||||
if portalURL == nil {
|
if portalURL == nil {
|
||||||
captivePortal = &CaptivePortal{}
|
captivePortal = &CaptivePortal{}
|
||||||
if captivePortalNotification != nil {
|
if captivePortalNotification != nil {
|
||||||
|
@ -230,12 +277,12 @@ func setCaptivePortal(portalURL *url.URL) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// return if unchanged
|
// Only set captive portal once per detection.
|
||||||
if portalURL.String() == captivePortal.URL {
|
if captivePortal.URL != "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// set
|
// Compile captive portal data.
|
||||||
captivePortal = &CaptivePortal{
|
captivePortal = &CaptivePortal{
|
||||||
URL: portalURL.String(),
|
URL: portalURL.String(),
|
||||||
}
|
}
|
||||||
|
@ -247,7 +294,7 @@ func setCaptivePortal(portalURL *url.URL) {
|
||||||
captivePortal.Domain = portalURL.Hostname()
|
captivePortal.Domain = portalURL.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify
|
// Notify user about portal.
|
||||||
captivePortalNotification = notifications.Notify(¬ifications.Notification{
|
captivePortalNotification = notifications.Notify(¬ifications.Notification{
|
||||||
EventID: "netenv:captive-portal",
|
EventID: "netenv:captive-portal",
|
||||||
Type: notifications.Info,
|
Type: notifications.Info,
|
||||||
|
|
|
@ -30,6 +30,8 @@ const (
|
||||||
ICMPv6 = IPProtocol(58)
|
ICMPv6 = IPProtocol(58)
|
||||||
UDPLite = IPProtocol(136)
|
UDPLite = IPProtocol(136)
|
||||||
RAW = IPProtocol(255)
|
RAW = IPProtocol(255)
|
||||||
|
|
||||||
|
AnyHostInternalProtocol61 = IPProtocol(61)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verdicts
|
// Verdicts
|
||||||
|
|
|
@ -94,7 +94,12 @@ func parseIGMP(packet gopacket.Packet, info *Info) error {
|
||||||
return nil
|
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 {
|
if err := packet.ErrorLayer(); err != nil {
|
||||||
return err.Error()
|
return err.Error()
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,9 +68,16 @@ type Profile struct { //nolint:maligned // not worth the effort
|
||||||
// Name is a human readable name of the profile. It
|
// Name is a human readable name of the profile. It
|
||||||
// defaults to the basename of the application.
|
// defaults to the basename of the application.
|
||||||
Name string
|
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.
|
// profile or the purpose of the application.
|
||||||
Description string
|
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
|
// Homepage may refer the the website of the application
|
||||||
// vendor.
|
// vendor.
|
||||||
Homepage string
|
Homepage string
|
||||||
|
|
|
@ -112,7 +112,7 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// Add description to tell users about the quirks of this 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:
|
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 {
|
switch profile.ID {
|
||||||
case SystemResolverProfileID:
|
case SystemResolverProfileID:
|
||||||
return canBeUpgraded(profile, "1.6.2021")
|
return canBeUpgraded(profile, "18.11.2021")
|
||||||
case PortmasterAppProfileID:
|
case PortmasterAppProfileID:
|
||||||
return canBeUpgraded(profile, "8.9.2021")
|
return canBeUpgraded(profile, "8.9.2021")
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -4,11 +4,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
|
"github.com/safing/portbase/notifications"
|
||||||
"github.com/safing/portmaster/intel"
|
"github.com/safing/portmaster/intel"
|
||||||
|
"github.com/tevino/abool"
|
||||||
|
|
||||||
// module dependencies
|
// module dependencies
|
||||||
_ "github.com/safing/portmaster/core/base"
|
_ "github.com/safing/portmaster/core/base"
|
||||||
|
@ -105,3 +108,50 @@ func getLocalAddr(network string) net.Addr {
|
||||||
}
|
}
|
||||||
return nil
|
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 := ¬ifications.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,12 +7,12 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portmaster/netenv"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
"github.com/safing/portbase/database"
|
"github.com/safing/portbase/database"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portmaster/compat"
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -313,13 +313,13 @@ retry:
|
||||||
|
|
||||||
func resolveAndCache(ctx context.Context, q *Query, oldCache *RRCache) (rrCache *RRCache, err error) { //nolint:gocognit,gocyclo
|
func resolveAndCache(ctx context.Context, q *Query, oldCache *RRCache) (rrCache *RRCache, err error) { //nolint:gocognit,gocyclo
|
||||||
// get resolvers
|
// get resolvers
|
||||||
resolvers, tryAll := GetResolversInScope(ctx, q)
|
resolvers, primarySource, tryAll := GetResolversInScope(ctx, q)
|
||||||
if len(resolvers) == 0 {
|
if len(resolvers) == 0 {
|
||||||
return nil, ErrNoCompliance
|
return nil, ErrNoCompliance
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we are online
|
// check if we are online
|
||||||
if netenv.GetOnlineStatus() == netenv.StatusOffline {
|
if primarySource != ServerSourceEnv && netenv.GetOnlineStatus() == netenv.StatusOffline {
|
||||||
if !netenv.IsConnectivityDomain(q.FQDN) {
|
if !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
|
||||||
|
@ -391,6 +391,10 @@ resolveLoop:
|
||||||
|
|
||||||
// Report a successful connection.
|
// Report a successful connection.
|
||||||
resolver.Conn.ResetFailure()
|
resolver.Conn.ResetFailure()
|
||||||
|
// Reset failing resolvers notification, if querying in global scope.
|
||||||
|
if primarySource == ServerSourceConfigured {
|
||||||
|
resetFailingResolversNotification()
|
||||||
|
}
|
||||||
|
|
||||||
break resolveLoop
|
break resolveLoop
|
||||||
}
|
}
|
||||||
|
@ -401,6 +405,13 @@ resolveLoop:
|
||||||
// tried all resolvers, possibly twice
|
// tried all resolvers, possibly twice
|
||||||
if i > 1 {
|
if i > 1 {
|
||||||
err = fmt.Errorf("all %d query-compliant resolvers failed, last error: %s", len(resolvers), err)
|
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 */ {
|
} else if rrCache == nil /* defensive */ {
|
||||||
err = ErrNotFound
|
err = ErrNotFound
|
||||||
|
|
|
@ -4,15 +4,17 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portmaster/compat"
|
||||||
"github.com/safing/portmaster/netenv"
|
"github.com/safing/portmaster/netenv"
|
||||||
"github.com/safing/portmaster/network/netutils"
|
"github.com/safing/portmaster/network/netutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
internalSpecialUseDomain = "17.home.arpa."
|
internalSpecialUseDomain = "portmaster.home.arpa."
|
||||||
|
|
||||||
routerDomain = "router.local." + internalSpecialUseDomain
|
routerDomain = "router.local." + internalSpecialUseDomain
|
||||||
captivePortalDomain = "captiveportal.local." + internalSpecialUseDomain
|
captivePortalDomain = "captiveportal.local." + internalSpecialUseDomain
|
||||||
|
@ -36,6 +38,7 @@ var (
|
||||||
|
|
||||||
func prepEnvResolver() (err error) {
|
func prepEnvResolver() (err error) {
|
||||||
netenv.SpecialCaptivePortalDomain = captivePortalDomain
|
netenv.SpecialCaptivePortalDomain = captivePortalDomain
|
||||||
|
compat.DNSCheckInternalDomainScope = ".self-check." + internalSpecialUseDomain
|
||||||
|
|
||||||
internalSpecialUseSOA, err = dns.NewRR(fmt.Sprintf(
|
internalSpecialUseSOA, err = dns.NewRR(fmt.Sprintf(
|
||||||
"%s 17 IN SOA localhost. none.localhost. 0 0 0 0 0",
|
"%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) {
|
func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
|
||||||
switch uint16(q.QType) {
|
switch uint16(q.QType) {
|
||||||
case dns.TypeA, dns.TypeAAAA: // We respond with all IPv4/6 addresses we can find.
|
case dns.TypeA, dns.TypeAAAA: // We respond with all IPv4/6 addresses we can find.
|
||||||
|
// Check for exact matches.
|
||||||
switch q.FQDN {
|
switch q.FQDN {
|
||||||
case captivePortalDomain:
|
case captivePortalDomain:
|
||||||
// Get IP address of the captive portal.
|
// 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.nxDomain(q), nil
|
||||||
}
|
}
|
||||||
return er.makeRRCache(q, records), 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:
|
case dns.TypeSOA:
|
||||||
// Direct query for the SOA record.
|
// Direct query for the SOA record.
|
||||||
|
|
|
@ -9,11 +9,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portmaster/network/netutils"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portmaster/network/netutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNS Classes
|
// DNS Classes
|
||||||
|
|
|
@ -145,10 +145,16 @@ func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolve
|
||||||
// Connect to server.
|
// Connect to server.
|
||||||
conn, err := tr.dnsClient.Dial(tr.resolver.ServerAddress)
|
conn, err := tr.dnsClient.Dial(tr.resolver.ServerAddress)
|
||||||
if err != nil {
|
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)
|
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 that a connection to the resolver was established.
|
||||||
log.Debugf(
|
log.Debugf(
|
||||||
"resolver: connected to %s",
|
"resolver: connected to %s",
|
||||||
|
@ -174,9 +180,6 @@ func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolve
|
||||||
// Set resolver conn for reuse.
|
// Set resolver conn for reuse.
|
||||||
tr.resolverConn = resolverConn
|
tr.resolverConn = resolverConn
|
||||||
|
|
||||||
// Hint network environment at successful connection.
|
|
||||||
netenv.ReportSuccessfulConnection()
|
|
||||||
|
|
||||||
return resolverConn, nil
|
return resolverConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,8 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
scope := netutils.GetIPScope(ip)
|
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
|
return nil, true, nil // skip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
// 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()
|
resolversLock.RLock()
|
||||||
defer resolversLock.RUnlock()
|
defer resolversLock.RUnlock()
|
||||||
|
|
||||||
// Internal use domains
|
// Internal use domains
|
||||||
if domainInScope(q.dotPrefixedFQDN, internalSpecialUseDomains) {
|
if domainInScope(q.dotPrefixedFQDN, internalSpecialUseDomains) {
|
||||||
return envResolvers, false
|
return envResolvers, ServerSourceEnv, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special connectivity domains
|
// Special connectivity domains
|
||||||
if netenv.IsConnectivityDomain(q.FQDN) && len(systemResolvers) > 0 {
|
if netenv.IsConnectivityDomain(q.FQDN) && len(systemResolvers) > 0 {
|
||||||
// Do not do compliance checks for connectivity domains.
|
// Do not do compliance checks for connectivity domains.
|
||||||
selected = append(selected, systemResolvers...) // dhcp assigned resolvers
|
selected = append(selected, systemResolvers...) // dhcp assigned resolvers
|
||||||
return selected, false
|
return selected, ServerSourceOperatingSystem, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prioritize search scopes
|
// 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, mDNSResolvers)
|
||||||
selected = addResolvers(ctx, q, selected, localResolvers)
|
selected = addResolvers(ctx, q, selected, localResolvers)
|
||||||
selected = addResolvers(ctx, q, selected, systemResolvers)
|
selected = addResolvers(ctx, q, selected, systemResolvers)
|
||||||
return selected, true
|
return selected, ServerSourceMDNS, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special use domains
|
// Special use domains
|
||||||
|
@ -145,12 +145,12 @@ func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver, t
|
||||||
domainInScope(q.dotPrefixedFQDN, specialServiceDomains) {
|
domainInScope(q.dotPrefixedFQDN, specialServiceDomains) {
|
||||||
selected = addResolvers(ctx, q, selected, localResolvers)
|
selected = addResolvers(ctx, q, selected, localResolvers)
|
||||||
selected = addResolvers(ctx, q, selected, systemResolvers)
|
selected = addResolvers(ctx, q, selected, systemResolvers)
|
||||||
return selected, true
|
return selected, "special", true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global domains
|
// Global domains
|
||||||
selected = addResolvers(ctx, q, selected, globalResolvers)
|
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 {
|
func addResolvers(ctx context.Context, q *Query, selected []*Resolver, addResolvers []*Resolver) []*Resolver {
|
||||||
|
|
Loading…
Add table
Reference in a new issue