wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

29
service/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
}

View File

@@ -0,0 +1,40 @@
package compat
import (
"net"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/process"
)
// SubmitSystemIntegrationCheckPacket submit a packet for the system integrity check.
func SubmitSystemIntegrationCheckPacket(p packet.Packet) {
select {
case systemIntegrationCheckPackets <- p:
default:
}
}
// SubmitDNSCheckDomain submits a subdomain for the dns check.
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
}
// ReportSecureDNSBypassIssue reports a DNS bypassing issue for the given process.
func ReportSecureDNSBypassIssue(p *process.Process) {
secureDNSBypassIssue.notify(p)
}
// ReportMultiPeerUDPTunnelIssue reports a multi-peer UDP tunnel for the given process.
func ReportMultiPeerUDPTunnelIssue(p *process.Process) {
multiPeerUDPTunnelIssue.notify(p)
}

View File

@@ -0,0 +1,10 @@
//go:build !windows && !linux
package compat
import "github.com/safing/portbase/utils/debug"
// AddToDebugInfo adds compatibility data to the given debug.Info.
func AddToDebugInfo(di *debug.Info) {
// Not yet implemented on this platform.
}

View File

@@ -0,0 +1,28 @@
package compat
import (
"fmt"
"github.com/safing/portbase/utils/debug"
)
// AddToDebugInfo adds compatibility data to the given debug.Info.
func AddToDebugInfo(di *debug.Info) {
// Get iptables state and add error info if it fails.
chains, err := GetIPTablesChains()
if err != nil {
di.AddSection(
"Compatibility: IPTables Chains (failed)",
debug.UseCodeSection,
err.Error(),
)
return
}
// Add data as section.
di.AddSection(
fmt.Sprintf("Compatibility: IPTables Chains (%d)", len(chains)-10),
debug.UseCodeSection|debug.AddContentLineBreaks,
chains...,
)
}

View File

@@ -0,0 +1,30 @@
package compat
import (
"fmt"
"strings"
"github.com/safing/portbase/utils/debug"
)
// AddToDebugInfo adds compatibility data to the given debug.Info.
func AddToDebugInfo(di *debug.Info) {
// Get WFP state and add error info if it fails.
wfp, err := GetWFPState()
if err != nil {
di.AddSection(
"Compatibility: WFP State (failed)",
debug.UseCodeSection,
err.Error(),
)
return
}
// Add data as section.
wfpTable := wfp.AsTable()
di.AddSection(
fmt.Sprintf("Compatibility: WFP State (%d)", strings.Count(wfpTable, "\n")),
debug.UseCodeSection,
wfpTable,
)
}

View File

@@ -0,0 +1,60 @@
//go:build linux
package compat
import (
"fmt"
"github.com/coreos/go-iptables/iptables"
)
var (
iptProtocols = []iptables.Protocol{
iptables.ProtocolIPv4,
iptables.ProtocolIPv6,
}
iptTables = []string{
"filter",
"nat",
"mangle",
"raw",
}
)
// GetIPTablesChains returns the chain names currently in ip(6)tables.
func GetIPTablesChains() ([]string, error) {
chains := make([]string, 0, 100)
// Iterate over protocols.
for _, protocol := range iptProtocols {
if protocol == iptables.ProtocolIPv4 {
chains = append(chains, "v4")
} else {
chains = append(chains, "v6")
}
// Get iptables access for protocol.
tbls, err := iptables.NewWithProtocol(protocol)
if err != nil {
return nil, err
}
// Iterate over tables.
for _, table := range iptTables {
chains = append(chains, " "+table)
// Get chain names
chainNames, err := tbls.ListChains(table)
if err != nil {
return nil, fmt.Errorf("failed to get chains of table %s: %w", table, err)
}
// Add chain names to list.
for _, name := range chainNames {
chains = append(chains, " "+name)
}
}
}
return chains, nil
}

View File

@@ -0,0 +1,24 @@
//go:build linux
package compat
import (
"testing"
)
func TestIPTablesChains(t *testing.T) {
// Skip in CI.
if testing.Short() {
t.Skip()
}
t.Parallel()
chain, err := GetIPTablesChains()
if err != nil {
t.Fatal(err)
}
if len(chain) < 35 {
t.Errorf("Expected at least 35 output lines, not %d", len(chain))
}
}

137
service/compat/module.go Normal file
View File

@@ -0,0 +1,137 @@
package compat
import (
"context"
"errors"
"time"
"github.com/tevino/abool"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/resolver"
)
var (
module *modules.Module
selfcheckTask *modules.Task
selfcheckTaskRetryAfter = 15 * 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
// an underlying failure.
selfCheckIsFailing = abool.New()
// selfcheckFails counts how often the self check failed successively.
// selfcheckFails is not locked as it is only accessed by the self-check task.
selfcheckFails int
// selfcheckNetworkChangedFlag is used to track changed to the network for
// the self-check.
selfcheckNetworkChangedFlag = netenv.GetNetworkChangedFlag()
)
// selfcheckFailThreshold holds the threshold of how many times the selfcheck
// must fail before it is reported.
const selfcheckFailThreshold = 10
func init() {
module = modules.Register("compat", prep, start, stop, "base", "network", "interception", "netenv", "notifications")
// Workaround resolver integration.
// See resolver/compat.go for details.
resolver.CompatDNSCheckInternalDomainScope = DNSCheckInternalDomainScope
resolver.CompatSelfCheckIsFailing = SelfCheckIsFailing
resolver.CompatSubmitDNSCheckDomain = SubmitDNSCheckDomain
}
func prep() error {
return registerAPIEndpoints()
}
func start() error {
startNotify()
selfcheckNetworkChangedFlag.Refresh()
selfcheckTask = module.NewTask("compatibility self-check", selfcheckTaskFunc).
Repeat(5 * time.Minute).
MaxDelay(selfcheckTaskRetryAfter).
Schedule(time.Now().Add(selfcheckTaskRetryAfter))
module.NewTask("clean notify thresholds", cleanNotifyThreshold).
Repeat(1 * time.Hour)
return module.RegisterEventHook(
netenv.ModuleName,
netenv.NetworkChangedEvent,
"trigger compat self-check",
func(_ context.Context, _ interface{}) error {
selfcheckTask.Schedule(time.Now().Add(selfcheckTaskRetryAfter))
return nil
},
)
}
func stop() error {
selfcheckTask.Cancel()
selfcheckTask = nil
return nil
}
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)
switch {
case err == nil:
// Successful.
tracer.Debugf("compat: self-check successful")
case errors.Is(err, errSelfcheckSkipped):
// Skipped.
tracer.Debugf("compat: %s", err)
case issue == nil:
// Internal error.
tracer.Warningf("compat: %s", err)
case selfcheckNetworkChangedFlag.IsSet():
// The network changed, ignore the issue.
default:
// The self-check failed.
// Set state and increase counter.
selfCheckIsFailing.Set()
selfcheckFails++
// Log and notify.
tracer.Errorf("compat: %s", err)
if selfcheckFails >= selfcheckFailThreshold {
issue.notify(err)
}
// Retry quicker when failed.
task.Schedule(time.Now().Add(selfcheckTaskRetryAfter))
return nil
}
// Reset self-check state.
selfcheckNetworkChangedFlag.Refresh()
selfCheckIsFailing.UnSet()
selfcheckFails = 0
resetSystemIssue()
return nil
}
// SelfCheckIsFailing returns whether the self check is currently failing.
// This returns true after the first check fails, and does not wait for the
// failing threshold to be met.
func SelfCheckIsFailing() bool {
return selfCheckIsFailing.IsSet()
}

287
service/compat/notify.go Normal file
View File

@@ -0,0 +1,287 @@
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"
"github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/profile"
)
type baseIssue struct {
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
type appIssue baseIssue
var (
// Copy of firewall.CfgOptionDNSQueryInterceptionKey.
cfgOptionDNSQueryInterceptionKey = "filter/dnsQueryInterception"
dnsQueryInterception config.BoolOption
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, [get support here](https://safing.io/support/).",
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) and disable it. If that does not help, [get support here](https://safing.io/support/).",
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: &notifications.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 some time after correct configuration."
secureDNSBypassIssue = &appIssue{
id: "compat:secure-dns-bypass-%s",
title: "Blocked Bypass Attempt by %s",
message: `[APPNAME] is using its own Secure DNS resolver, which would bypass Portmaster's firewall protections. If [APPNAME] experiences problems, disable Secure DNS within [APPNAME] to restore functionality. Rest assured that Portmaster handles Secure DNS for your whole device, including [APPNAME].`,
// 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 [APPNAME] 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 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()
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,
AvailableActions: issue.actions,
}
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, "-%s"),
"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
}
// Check if we reach the threshold to actually send a notification.
if !isOverThreshold(eventID) {
return
}
// Build message.
message := strings.ReplaceAll(issue.message, "[APPNAME]", p.Name)
// Create a new notification.
n = &notifications.Notification{
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)
// Set warning on profile.
module.StartWorker("set app compat warning", func(ctx context.Context) error {
var changed bool
func() {
p.Lock()
defer p.Unlock()
if p.Warning != message || time.Now().Add(-1*time.Hour).After(p.WarningLastUpdated) {
p.Warning = message
p.WarningLastUpdated = time.Now()
changed = true
}
}()
if changed {
return p.Save()
}
return nil
})
}
const (
notifyThresholdMinIncidents = 10
notifyThresholdResetAfter = 2 * time.Minute
)
var (
notifyThresholds = make(map[string]*notifyThreshold)
notifyThresholdsLock sync.Mutex
)
type notifyThreshold struct {
FirstSeen time.Time
Incidents uint
}
func (nt *notifyThreshold) expired() bool {
return time.Now().Add(-notifyThresholdResetAfter).After(nt.FirstSeen)
}
func isOverThreshold(id string) bool {
notifyThresholdsLock.Lock()
defer notifyThresholdsLock.Unlock()
// Get notify threshold and check if we reach the minimum incidents.
nt, ok := notifyThresholds[id]
if ok && !nt.expired() {
nt.Incidents++
return nt.Incidents >= notifyThresholdMinIncidents
}
// Add new entry.
notifyThresholds[id] = &notifyThreshold{
FirstSeen: time.Now(),
Incidents: 1,
}
return false
}
func cleanNotifyThreshold(ctx context.Context, task *modules.Task) error {
notifyThresholdsLock.Lock()
defer notifyThresholdsLock.Unlock()
for id, nt := range notifyThresholds {
if nt.expired() {
delete(notifyThresholds, id)
}
}
return nil
}

205
service/compat/selfcheck.go Normal file
View File

@@ -0,0 +1,205 @@
package compat
import (
"context"
"encoding/hex"
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/rng"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/resolver"
)
var (
selfcheckLock sync.Mutex
// SystemIntegrationCheckDstIP is the IP address to send a packet to for the
// system integration test.
SystemIntegrationCheckDstIP = net.IPv4(127, 65, 67, 75)
// SystemIntegrationCheckProtocol is the IP protocol to use for the system
// integration test.
SystemIntegrationCheckProtocol = packet.AnyHostInternalProtocol61
systemIntegrationCheckDialNet = fmt.Sprintf("ip4:%d", uint8(SystemIntegrationCheckProtocol))
systemIntegrationCheckDialIP = SystemIntegrationCheckDstIP.String()
systemIntegrationCheckPackets = make(chan packet.Packet, 1)
systemIntegrationCheckWaitDuration = 45 * time.Second
// DNSCheckInternalDomainScope is the domain scope to use for dns checks.
DNSCheckInternalDomainScope = ".self-check." + resolver.InternalSpecialUseDomain
dnsCheckReceivedDomain = make(chan string, 1)
dnsCheckWaitDuration = 45 * time.Second
dnsCheckAnswerLock sync.Mutex
dnsCheckAnswer net.IP
errSelfcheckSkipped = errors.New("self-check skipped")
)
func selfcheck(ctx context.Context) (issue *systemIssue, err error) {
selfcheckLock.Lock()
defer selfcheckLock.Unlock()
// Step 0: Check if self-check makes sense.
if !netenv.Online() {
return nil, fmt.Errorf("%w: device is offline or in limited network", errSelfcheckSkipped)
}
// 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("PORTMASTER 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.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():
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 systemCompatOrManualDNSIssue(), fmt.Errorf("self-check #2: dns integration check failed: received unmatching subdomain %q", receivedTestDomain)
}
case <-time.After(dnsCheckWaitDuration):
return systemCompatOrManualDNSIssue(), fmt.Errorf("self-check #2: dns integration check failed: did not receive test query after %s", dnsCheckWaitDuration)
}
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.
// 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.Tracer(ctx).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?
*/

527
service/compat/wfpstate.go Normal file
View File

@@ -0,0 +1,527 @@
package compat
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"text/tabwriter"
"github.com/safing/portbase/utils/osdetail"
)
// GetWFPState queries the system for the WFP state and returns a simplified
// and cleaned version.
func GetWFPState() (*SimplifiedWFPState, error) {
// Use a file to get the wfp state, as the terminal isn't able to return the
// data encoded in UTF-8.
tmpDir, err := os.MkdirTemp("", "portmaster-debug-data-wfpstate")
if err != nil {
return nil, fmt.Errorf("failed to create tmp dir for wfpstate: %w", err)
}
defer func() {
_ = os.RemoveAll(tmpDir)
}()
tmpFile := filepath.Join(tmpDir, "wfpstate.xml")
// Get wfp state and write it to the tmp file.
_, err = osdetail.RunCmd(
"netsh.exe",
"wfp",
"show",
"state",
tmpFile,
)
if err != nil {
return nil, fmt.Errorf("failed to write wfp state to tmp file: %w", err)
}
// Get tmp file contents.
output, err := os.ReadFile(tmpFile)
if err != nil {
return nil, fmt.Errorf("failed to read wfp state to tmp file: %w", err)
}
if len(output) == 0 {
return nil, errors.New("wfp state tmp file was empty")
}
// Parse wfp state.
parsedState, err := parseWFPState(output)
if err != nil {
return nil, fmt.Errorf("failed to parse wfpstate: %w", err)
}
// Return simplified and cleaned state.
return parsedState.simplified(), nil
}
/*
Interesting data is found at:
providers->item[]
->displayData->name
->displayData->description
->providerKey
subLayers->item[]
->displayData->name
->displayData->description
->subLayerKey
layers->item[]->callouts->item[]
->displayData->name
->displayData->description
->calloutKey
->providerKey
->applicableLayer
layers->item[]->filters->item[]
->displayData->name
->displayData->description
->filterKey
->providerKey
->layerKey
->subLayerKey
*/
// SimplifiedWFPState is a simplified version of the full WFP state.
type SimplifiedWFPState struct {
Providers []*WFPProvider
SubLayers []*WFPSubLayer
Callouts []*WFPCallout
Filters []*WFPFilter
}
// WFPProvider represents a WFP Provider.
type WFPProvider struct {
Name string
Description string
ProviderKey string
}
// WFPSubLayer represents a WFP SubLayer.
type WFPSubLayer struct {
Name string
Description string
SubLayerKey string
}
// WFPCallout represents a WFP Callout.
type WFPCallout struct {
Name string
Description string
CalloutKey string
ProviderKey string
ApplicableLayer string
}
// WFPFilter represents a WFP Filter.
type WFPFilter struct {
Name string
Description string
FilterKey string
ProviderKey string
LayerKey string
SubLayerKey string
}
// Keys returns all keys found in the WFP state.
func (sw *SimplifiedWFPState) Keys() map[string]struct{} {
lookupMap := make(map[string]struct{}, len(sw.Providers)+len(sw.SubLayers)+len(sw.Callouts)+len(sw.Filters))
// Collect keys.
for _, provider := range sw.Providers {
lookupMap[provider.ProviderKey] = struct{}{}
}
for _, subLayer := range sw.SubLayers {
lookupMap[subLayer.SubLayerKey] = struct{}{}
}
for _, callout := range sw.Callouts {
lookupMap[callout.CalloutKey] = struct{}{}
}
for _, filter := range sw.Filters {
lookupMap[filter.FilterKey] = struct{}{}
}
return lookupMap
}
// AsTable formats the simplified WFP state as a table.
func (sw *SimplifiedWFPState) AsTable() string {
rows := make([]string, 0, len(sw.Providers)+len(sw.SubLayers)+len(sw.Callouts)+len(sw.Filters))
// Collect data and put it into rows.
for _, provider := range sw.Providers {
rows = append(rows, strings.Join([]string{
provider.Name,
"Provider",
provider.Description,
provider.ProviderKey,
}, "\t"))
}
for _, subLayer := range sw.SubLayers {
rows = append(rows, strings.Join([]string{
subLayer.Name,
"SubLayer",
subLayer.Description,
subLayer.SubLayerKey,
}, "\t"))
}
for _, callout := range sw.Callouts {
rows = append(rows, strings.Join([]string{
callout.Name,
"Callout",
callout.Description,
callout.CalloutKey,
callout.ProviderKey,
callout.ApplicableLayer,
}, "\t"))
}
for _, filter := range sw.Filters {
rows = append(rows, strings.Join([]string{
filter.Name,
"Filter",
filter.Description,
filter.FilterKey,
filter.ProviderKey,
filter.LayerKey,
filter.SubLayerKey,
}, "\t"))
}
// Sort and build table.
sort.Strings(rows)
buf := bytes.NewBuffer(nil)
tabWriter := tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0)
for _, row := range rows {
fmt.Fprint(tabWriter, row)
fmt.Fprint(tabWriter, "\n")
}
_ = tabWriter.Flush()
return buf.String()
}
// wfpState is the WFP state as returned by `netsh.exe wfp show state -`.
type wfpState struct {
XMLName xml.Name `xml:"wfpstate"`
Text string `xml:",chardata"`
TimeStamp string `xml:"timeStamp"`
Providers struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
ProviderKey string `xml:"providerKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item string `xml:"item"`
} `xml:"flags"`
ProviderData string `xml:"providerData"`
ServiceName string `xml:"serviceName"`
} `xml:"item"`
} `xml:"providers"`
SubLayers struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
SubLayerKey string `xml:"subLayerKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item string `xml:"item"`
} `xml:"flags"`
ProviderKey string `xml:"providerKey"`
ProviderData string `xml:"providerData"`
Weight string `xml:"weight"`
} `xml:"item"`
} `xml:"subLayers"`
Layers struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
Layer struct {
Text string `xml:",chardata"`
LayerKey string `xml:"layerKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []string `xml:"item"`
} `xml:"flags"`
Field struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
FieldKey string `xml:"fieldKey"`
Type string `xml:"type"`
DataType string `xml:"dataType"`
} `xml:"item"`
} `xml:"field"`
DefaultSubLayerKey string `xml:"defaultSubLayerKey"`
LayerID string `xml:"layerId"`
} `xml:"layer"`
Callouts struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
CalloutKey string `xml:"calloutKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []string `xml:"item"`
} `xml:"flags"`
ProviderKey string `xml:"providerKey"`
ProviderData string `xml:"providerData"`
ApplicableLayer string `xml:"applicableLayer"`
CalloutID string `xml:"calloutId"`
} `xml:"item"`
} `xml:"callouts"`
Filters struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
FilterKey string `xml:"filterKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []string `xml:"item"`
} `xml:"flags"`
ProviderKey string `xml:"providerKey"`
ProviderData struct {
Text string `xml:",chardata"`
Data string `xml:"data"`
AsString string `xml:"asString"`
} `xml:"providerData"`
LayerKey string `xml:"layerKey"`
SubLayerKey string `xml:"subLayerKey"`
Weight struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint8 string `xml:"uint8"`
Uint64 string `xml:"uint64"`
} `xml:"weight"`
FilterCondition struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
FieldKey string `xml:"fieldKey"`
MatchType string `xml:"matchType"`
ConditionValue struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint32 string `xml:"uint32"`
Uint16 string `xml:"uint16"`
RangeValue struct {
Text string `xml:",chardata"`
ValueLow struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint16 string `xml:"uint16"`
Uint32 string `xml:"uint32"`
ByteArray16 string `xml:"byteArray16"`
} `xml:"valueLow"`
ValueHigh struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint16 string `xml:"uint16"`
Uint32 string `xml:"uint32"`
ByteArray16 string `xml:"byteArray16"`
} `xml:"valueHigh"`
} `xml:"rangeValue"`
Uint8 string `xml:"uint8"`
ByteBlob struct {
Text string `xml:",chardata"`
Data string `xml:"data"`
AsString string `xml:"asString"`
} `xml:"byteBlob"`
Sd string `xml:"sd"`
Sid string `xml:"sid"`
Uint64 string `xml:"uint64"`
} `xml:"conditionValue"`
} `xml:"item"`
} `xml:"filterCondition"`
Action struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
FilterType string `xml:"filterType"`
} `xml:"action"`
RawContext string `xml:"rawContext"`
Reserved string `xml:"reserved"`
FilterID string `xml:"filterId"`
EffectiveWeight struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint64 string `xml:"uint64"`
} `xml:"effectiveWeight"`
ProviderContextKey string `xml:"providerContextKey"`
} `xml:"item"`
} `xml:"filters"`
} `xml:"item"`
} `xml:"layers"`
}
func parseWFPState(data []byte) (*wfpState, error) {
w := &wfpState{}
err := xml.Unmarshal(data, w)
if err != nil {
return nil, err
}
return w, nil
}
func (w *wfpState) simplified() *SimplifiedWFPState {
sw := &SimplifiedWFPState{
Providers: make([]*WFPProvider, 0, len(w.Providers.Item)),
SubLayers: make([]*WFPSubLayer, 0, len(w.SubLayers.Item)),
Callouts: make([]*WFPCallout, 0, len(w.Layers.Item)),
Filters: make([]*WFPFilter, 0, len(w.Layers.Item)),
}
// Collect data.
for _, provider := range w.Providers.Item {
if isIgnoredProvider(provider.DisplayData.Name, provider.ProviderKey) {
continue
}
sw.Providers = append(sw.Providers, &WFPProvider{
Name: defaultTo(provider.DisplayData.Name, "[no name]"),
Description: defaultTo(provider.DisplayData.Description, "[no description]"),
ProviderKey: defaultTo(provider.ProviderKey, "[no provider key]"),
})
}
for _, subLayer := range w.SubLayers.Item {
if isIgnoredProvider(subLayer.DisplayData.Name, "") {
continue
}
sw.SubLayers = append(sw.SubLayers, &WFPSubLayer{
Name: defaultTo(subLayer.DisplayData.Name, "[no name]"),
Description: defaultTo(subLayer.DisplayData.Description, "[no description]"),
SubLayerKey: defaultTo(subLayer.SubLayerKey, "[no sublayer key]"),
})
}
for _, layer := range w.Layers.Item {
for _, callout := range layer.Callouts.Item {
if isIgnoredProvider(callout.DisplayData.Name, callout.ProviderKey) {
continue
}
sw.Callouts = append(sw.Callouts, &WFPCallout{
Name: defaultTo(callout.DisplayData.Name, "[no name]"),
Description: defaultTo(callout.DisplayData.Description, "[no description]"),
CalloutKey: defaultTo(callout.CalloutKey, "[no callout key]"),
ProviderKey: defaultTo(callout.ProviderKey, "[no provider key]"),
ApplicableLayer: defaultTo(callout.ApplicableLayer, "[no applicable layer]"),
})
}
for _, filter := range layer.Filters.Item {
if isIgnoredProvider(filter.DisplayData.Name, filter.ProviderKey) {
continue
}
sw.Filters = append(sw.Filters, &WFPFilter{
Name: defaultTo(filter.DisplayData.Name, "[no name]"),
Description: defaultTo(filter.DisplayData.Description, "[no description]"),
FilterKey: defaultTo(filter.FilterKey, "[no filter key]"),
ProviderKey: defaultTo(filter.ProviderKey, "[no provider key]"),
LayerKey: defaultTo(filter.LayerKey, "[no layer key]"),
SubLayerKey: defaultTo(filter.SubLayerKey, "[no sublayer key]"),
})
}
}
return sw
}
func isIgnoredProvider(name, key string) bool {
// Check provider key.
if key != "" {
matched := true
switch key {
case "{1bebc969-61a5-4732-a177-847a0817862a}": // Microsoft Windows Defender Firewall IPsec Provider.
case "{4b153735-1049-4480-aab4-d1b9bdc03710}": // Microsoft Windows Defender Firewall Provider.
case "{893a4f22-9bba-49b7-8c66-3d40929c8fd5}": // Microsoft Windows Teredo firewall provider.
case "{8e44982a-f477-11df-85ce-78e7d1810190}": // Windows Network Data Usage (NDU) Provider.
case "{9c2532b4-0314-434f-8274-0cbaebdbda56}": // Microsoft Windows edge traversal socket option authorization provider.
case "{aa6a7d87-7f8f-4d2a-be53-fda555cd5fe3}": // Microsoft Windows Defender Firewall IPsec Provider.
case "{c698301d-9129-450c-937c-f4b834bfb374}": // Microsoft Windows edge traversal socket option authorization provider.
case "{decc16ca-3f33-4346-be1e-8fb4ae0f3d62}": // Microsoft Windows Defender Firewall Provider.
case "FWPM_PROVIDER_IKEEXT": // Microsoft Windows WFP Built-in IKEEXT provider used to identify filters added by IKE/AuthIP.
case "FWPM_PROVIDER_IPSEC_DOSP_CONFIG": // Microsoft Windows WFP Built-in IPsec DoS Protection configuration provider used to identify filters added by IPsec Denial of Service Protection.
case "FWPM_PROVIDER_MPSSVC_APP_ISOLATION": // Microsoft Windows WFP Built-in MPSSVC App Isolation provider.
case "FWPM_PROVIDER_MPSSVC_EDP": // Microsoft Windows WFP Built-in MPSSVC Enterprise Data Protection provider.
case "FWPM_PROVIDER_MPSSVC_TENANT_RESTRICTIONS": // Microsoft Windows WFP Built-in MPSSVC Tenant Restrictions provider.
case "FWPM_PROVIDER_MPSSVC_WF": // Microsoft Windows WFP Built-in MPSSVC Windows Firewall provider.
case "FWPM_PROVIDER_MPSSVC_WSH": // Microsoft Windows WFP Built-in MPSSVC Windows Service Hardening and Quarantine provider.
case "FWPM_PROVIDER_TCP_CHIMNEY_OFFLOAD": // Microsoft Windows WFP Built-in TCP Chimney Offload provider used to identify filters added by TCP Chimney Offload.
case "FWPM_PROVIDER_TCP_TEMPLATES": // Microsoft Windows WFP Built-in TCP Templates provider used to identify filters added by TCP Template based configuration.
default:
matched = false
}
if matched {
return true
}
}
// Some entries don't have a provider key (set).
// These are pretty generic, but the output strings are localized.
if name != "" {
switch {
case strings.Contains(name, "Microsoft Corporation"):
return true
case strings.Contains(name, "windefend"):
return true
case strings.Contains(name, "WFP"):
return true
case strings.Contains(name, "RPC"):
return true
case strings.Contains(name, "NDU"):
return true
}
}
return false
}
func defaultTo(a, b string) string {
if a != "" {
return a
}
return b
}

File diff suppressed because it is too large Load Diff