Merge branch 'develop'

This commit is contained in:
Daniel 2024-11-27 16:40:03 +01:00
commit df4106fd70
66 changed files with 8188 additions and 6228 deletions

41
.github/workflows/windows-dll.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Windows Portmaster Core DLL
on:
push:
paths:
- 'windows_core_dll/**'
branches:
- master
- develop
pull_request:
paths:
- 'windows_core_dll/**'
branches:
- master
- develop
workflow_dispatch:
jobs:
build:
name: Build
runs-on: windows-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
- name: Build DLL
run: msbuild windows_core_dll\windows_core_dll.sln -t:rebuild -property:Configuration=Release
- name: Verify DLL
shell: powershell
run: |
if (!(Test-Path "windows_core_dll/x64/Release/portmaster-core.dll")) {
Write-Error "DLL build failed: portmaster-core.dll not found"
exit 1
}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: portmaster-core-dll
path: windows_core_dll/x64/Release/portmaster-core.dll

1
.gitignore vendored
View file

@ -52,3 +52,4 @@ go.work.sum
# Kext releases
windows_kext/release/kext_release_*.zip
windows_core_dll/.vs/windows_core_dll

View file

@ -420,7 +420,7 @@ rust-base:
DO rust+INIT --keep_fingerprints=true
# For now we need tauri-cli 2.0.0 for bulding
DO rust+CARGO --args="install tauri-cli --version ^2.0.0-beta"
DO rust+CARGO --args="install tauri-cli --version 2.1.0"
# Explicitly cache here.
SAVE IMAGE --cache-hint

View file

@ -15,6 +15,7 @@ import (
"strings"
"time"
"github.com/hectane/go-acl"
"github.com/safing/portmaster/base/database/iterator"
"github.com/safing/portmaster/base/database/query"
"github.com/safing/portmaster/base/database/record"
@ -288,10 +289,13 @@ func writeFile(filename string, data []byte, perm os.FileMode) error {
defer t.Cleanup() //nolint:errcheck
// Set permissions before writing data, in case the data is sensitive.
if !onWindows {
if err := t.Chmod(perm); err != nil {
return err
}
if onWindows {
err = acl.Chmod(filename, perm)
} else {
err = t.Chmod(perm)
}
if err != nil {
return err
}
if _, err := t.Write(data); err != nil {

View file

@ -14,6 +14,7 @@ import (
"path/filepath"
"time"
"github.com/hectane/go-acl"
"github.com/safing/jess/filesig"
"github.com/safing/jess/lhash"
"github.com/safing/portmaster/base/log"
@ -136,7 +137,12 @@ func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client,
return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err)
}
// set permissions
if !onWindows {
if onWindows {
err = acl.Chmod(rv.storagePath(), 0o0755)
if err != nil {
log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
}
} else {
// TODO: only set executable files to 0755, set other to 0644
err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above.
if err != nil {

View file

@ -6,6 +6,8 @@ import (
"io/fs"
"os"
"runtime"
"github.com/hectane/go-acl"
)
const isWindows = runtime.GOOS == "windows"
@ -20,8 +22,9 @@ func EnsureDirectory(path string, perm os.FileMode) error {
if f.IsDir() {
// directory exists, check permissions
if isWindows {
// TODO: set correct permission on windows
// acl.Chmod(path, perm)
// Ignore windows permission error. For none admin users it will always fail.
acl.Chmod(path, perm)
return nil
} else if f.Mode().Perm() != perm {
return os.Chmod(path, perm)
}
@ -38,7 +41,13 @@ func EnsureDirectory(path string, perm os.FileMode) error {
if err != nil {
return fmt.Errorf("could not create dir %s: %w", path, err)
}
return os.Chmod(path, perm)
if isWindows {
// Ignore windows permission error. For none admin users it will always fail.
acl.Chmod(path, perm)
return nil
} else {
return os.Chmod(path, perm)
}
}
// other error opening path
return fmt.Errorf("failed to access %s: %w", path, err)

View file

@ -1,6 +1,11 @@
package renameio
import "os"
import (
"os"
"runtime"
"github.com/hectane/go-acl"
)
// WriteFile mirrors os.WriteFile, replacing an existing file with the same
// name atomically.
@ -14,7 +19,12 @@ func WriteFile(filename string, data []byte, perm os.FileMode) error {
}()
// Set permissions before writing data, in case the data is sensitive.
if err := t.Chmod(perm); err != nil {
if runtime.GOOS == "windows" {
err = acl.Chmod(t.path, perm)
} else {
err = t.Chmod(perm)
}
if err != nil {
return err
}

View file

@ -272,7 +272,7 @@ func fixExecPerm(path string) error {
return nil
}
if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec // Set execution rights.
if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec
return fmt.Errorf("failed to chmod %s: %w", path, err)
}

View file

@ -23,13 +23,13 @@
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@tauri-apps/api": ">=2.0.0-rc.1",
"@tauri-apps/plugin-cli": ">=2.0.0-rc.1",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0-rc.1",
"@tauri-apps/plugin-dialog": ">=2.0.0-rc.1",
"@tauri-apps/plugin-notification": ">=2.0.0-rc.1",
"@tauri-apps/plugin-os": ">=2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc",
"@tauri-apps/api": ">=2.1.1",
"@tauri-apps/plugin-cli": ">=2.0.0",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
"@tauri-apps/plugin-dialog": ">=2.0.0",
"@tauri-apps/plugin-notification": ">=2.0.0",
"@tauri-apps/plugin-os": ">=2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"autoprefixer": "^10.4.14",
"d3": "^7.8.4",
"data-urls": "^5.0.0",
@ -4406,9 +4406,9 @@
"peer": true
},
"node_modules/@tauri-apps/api": {
"version": "2.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
"integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.1.1.tgz",
"integrity": "sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@ -4416,57 +4416,57 @@
}
},
"node_modules/@tauri-apps/plugin-cli": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0-rc.1.tgz",
"integrity": "sha512-EcSTRfEU3zzlNbgwVtZVzqB19z3PNjyXD9H+YXuuLpV+Hwuh6Oi1fhUdCI0mp5zr9HSMWE+HzHkpBI7sVP1RyA==",
"license": "MIT or APACHE-2.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0.tgz",
"integrity": "sha512-glQmlL1IiCGEa1FHYa/PTPSeYhfu56omLRgHXWlJECDt6DbJyRuJWVgtkQfUxtqnVdYnnU+DGIGeiInoEqtjLw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-clipboard-manager": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.1.tgz",
"integrity": "sha512-hFgUABMmQuVGKwHb8PR9fuqfk0WRkedbWUt/ZV5sL4Q6kLrsp3JYJvtzVPeMYdeBvMqHl8WXNxAc/zwSld2h9w==",
"license": "MIT or APACHE-2.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
"integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.1.tgz",
"integrity": "sha512-H28gh6BfZtjflHQ+HrmWwunDriBI3AQLAKnMs50GA6zeNUULqbQr7VXbAAKeJL/0CmWcecID4PKXVoSlaWRhEg==",
"license": "MIT or APACHE-2.0",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.1.tgz",
"integrity": "sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-notification": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0-rc.1.tgz",
"integrity": "sha512-ddDj7xM8XR7Zv2vdpofNXlLjcp49p/VjlL0D+/eBcMuyooaLNMor3jz/+H6s23iHerdxMWA50mzy26BRN1BySA==",
"license": "MIT or APACHE-2.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0.tgz",
"integrity": "sha512-6qEDYJS7mgXZWLXA0EFL+DVCJh8sJlzSoyw6B50pxhLPVFjc5Vr5DVzl5W3mUHaYhod5wsC984eQnlCCGqxYDA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-os": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-rc.1.tgz",
"integrity": "sha512-PV8zlSTmYfiN2xzILUmlDSEycS7UYbH2yXk/ZqF+qQU6/s+OVQvmSth4EhllFjcpvPbtqELvpzfjw+2qEouchA==",
"license": "MIT or APACHE-2.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0.tgz",
"integrity": "sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz",
"integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==",
"license": "MIT or APACHE-2.0",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.1.tgz",
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tootallnate/once": {
@ -21067,56 +21067,56 @@
"peer": true
},
"@tauri-apps/api": {
"version": "2.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
"integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg=="
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.1.1.tgz",
"integrity": "sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A=="
},
"@tauri-apps/plugin-cli": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0-rc.1.tgz",
"integrity": "sha512-EcSTRfEU3zzlNbgwVtZVzqB19z3PNjyXD9H+YXuuLpV+Hwuh6Oi1fhUdCI0mp5zr9HSMWE+HzHkpBI7sVP1RyA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0.tgz",
"integrity": "sha512-glQmlL1IiCGEa1FHYa/PTPSeYhfu56omLRgHXWlJECDt6DbJyRuJWVgtkQfUxtqnVdYnnU+DGIGeiInoEqtjLw==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-clipboard-manager": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.1.tgz",
"integrity": "sha512-hFgUABMmQuVGKwHb8PR9fuqfk0WRkedbWUt/ZV5sL4Q6kLrsp3JYJvtzVPeMYdeBvMqHl8WXNxAc/zwSld2h9w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
"integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-dialog": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.1.tgz",
"integrity": "sha512-H28gh6BfZtjflHQ+HrmWwunDriBI3AQLAKnMs50GA6zeNUULqbQr7VXbAAKeJL/0CmWcecID4PKXVoSlaWRhEg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.1.tgz",
"integrity": "sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-notification": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0-rc.1.tgz",
"integrity": "sha512-ddDj7xM8XR7Zv2vdpofNXlLjcp49p/VjlL0D+/eBcMuyooaLNMor3jz/+H6s23iHerdxMWA50mzy26BRN1BySA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0.tgz",
"integrity": "sha512-6qEDYJS7mgXZWLXA0EFL+DVCJh8sJlzSoyw6B50pxhLPVFjc5Vr5DVzl5W3mUHaYhod5wsC984eQnlCCGqxYDA==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-os": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-rc.1.tgz",
"integrity": "sha512-PV8zlSTmYfiN2xzILUmlDSEycS7UYbH2yXk/ZqF+qQU6/s+OVQvmSth4EhllFjcpvPbtqELvpzfjw+2qEouchA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0.tgz",
"integrity": "sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tauri-apps/plugin-shell": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz",
"integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.1.tgz",
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==",
"requires": {
"@tauri-apps/api": "^2.0.0-rc.4"
"@tauri-apps/api": "^2.0.0"
}
},
"@tootallnate/once": {

View file

@ -37,13 +37,13 @@
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@tauri-apps/api": ">=2.0.0-rc.1",
"@tauri-apps/plugin-cli": ">=2.0.0-rc.1",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0-rc.1",
"@tauri-apps/plugin-dialog": ">=2.0.0-rc.1",
"@tauri-apps/plugin-notification": ">=2.0.0-rc.1",
"@tauri-apps/plugin-os": ">=2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc",
"@tauri-apps/api": ">=2.1.1",
"@tauri-apps/plugin-cli": ">=2.0.0",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
"@tauri-apps/plugin-dialog": ">=2.0.0",
"@tauri-apps/plugin-notification": ">=2.0.0",
"@tauri-apps/plugin-os": ">=2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"autoprefixer": "^10.4.14",
"d3": "^7.8.4",
"data-urls": "^5.0.0",

File diff suppressed because it is too large Load diff

View file

@ -12,21 +12,21 @@ rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.0.0-rc.7", features = [] }
tauri-build = { version = "2.0.3", features = [] }
[dependencies]
# Tauri
tauri = { version = "2.0.0-rc.8", features = ["tray-icon", "image-png", "config-json5", "devtools"] }
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-dialog = "2.0.0-rc"
tauri-plugin-clipboard-manager = "2.0.0-rc"
tauri-plugin-os = "2.0.0-rc"
tauri-plugin-single-instance = "2.0.0-rc"
tauri-plugin-notification = "2.0.0-rc"
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-window-state = "2.0.0-rc"
tauri = { version = "2.1.1", features = ["tray-icon", "image-png", "config-json5", "devtools"] }
tauri-plugin-shell = "2.0.2"
tauri-plugin-dialog = "2.0.3"
tauri-plugin-clipboard-manager = "2.0.2"
tauri-plugin-os = "2.0.1"
tauri-plugin-single-instance = "2.0.1"
tauri-plugin-notification = "2.0.1"
tauri-plugin-log = "2.0.2"
tauri-plugin-window-state = "2.0.2"
tauri-cli = "2.0.0-rc.8"
tauri-cli = "2.1.0"
clap = { version = "4" }
# General

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,7 @@ use crate::{
portmaster::PortmasterExt,
window::{create_main_window, may_navigate_to_ui, open_window},
};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
pub type AppIcon = TrayIcon<Wry>;
@ -199,8 +199,10 @@ pub fn setup_tray_menu(
app.dialog()
.message("This does not stop the Portmaster system service")
.title("Do you really want to quit the user interface?")
.ok_button_label("Yes, exit")
.cancel_button_label("No")
.buttons(MessageDialogButtons::OkCancelCustom(
"Yes, exit".to_owned(),
"No".to_owned(),
))
.show(move |answer| {
if answer {
// let _ = handle.emit("exit-requested", "");

View file

@ -139,7 +139,7 @@
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
<File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">

View file

@ -139,7 +139,7 @@
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
<File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">

6
go.mod
View file

@ -32,6 +32,7 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.7.0
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb
github.com/jackc/puddle/v2 v2.2.2
github.com/lmittmann/tint v1.0.5
github.com/maruel/panicparse/v2 v2.3.1
@ -58,6 +59,7 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
github.com/varlink/go v0.4.0
github.com/vincent-petithory/dataurl v1.0.0
go.etcd.io/bbolt v1.3.11
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
@ -120,8 +122,8 @@ require (
golang.org/x/tools v0.27.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.61.0 // indirect
modernc.org/libc v1.61.2 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.33.1 // indirect
modernc.org/sqlite v1.34.1 // indirect
)

21
go.sum
View file

@ -137,6 +137,8 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb h1:PGufWXXDq9yaev6xX1YQauaO1MV90e6Mpoq1I7Lz/VM=
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E=
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -311,6 +313,8 @@ github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
github.com/varlink/go v0.4.0 h1:+/BQoUO9eJK/+MTSHwFcJch7TMsb6N6Dqp6g0qaXXRo=
github.com/varlink/go v0.4.0/go.mod h1:DKg9Y2ctoNkesREGAEak58l+jOC6JU2aqZvUYs5DynU=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
@ -388,6 +392,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -460,16 +465,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8=
modernc.org/cc/v4 v4.23.1/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.22.3 h1:C7AW89Zw3kygesTQWBzApwIn9ldM+cb/plrTIKq41Os=
modernc.org/ccgo/v4 v4.22.3/go.mod h1:Dz7n0/UkBbH3pnYaxgi1mFSfF4REqUOZNziphZASx6k=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
modernc.org/libc v1.61.2 h1:dkO4DlowfClcJYsvf/RiK6fUwvzCQTmB34bJLt0CAGQ=
modernc.org/libc v1.61.2/go.mod h1:4QGjNyX3h+rn7V5oHpJY2yH0QN6frt1X+5BkXzwLPCo=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
@ -478,8 +483,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk=
modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View file

@ -181,4 +181,5 @@ func New(instance instance) (*Compat, error) {
type instance interface {
NetEnv() *netenv.NetEnv
Resolver() *resolver.ResolverModule
}

View file

@ -158,6 +158,12 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) {
// Step 3: Have the nameserver respond with random data in the answer section.
// Check if the resolver is enabled
if module.instance.Resolver().IsDisabled() {
// There is no control over the response, there is nothing more that can be checked.
return nil, nil
}
// Wait for the reply from the resolver.
select {
case err := <-dnsCheckLookupError:

View file

@ -43,8 +43,24 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
return endpoints.NoMatch, "", nil
}
// If Portmaster resolver is disabled allow requests going to system dns resolver.
// And allow all connections out of the System Resolver.
if module.instance.Resolver().IsDisabled() {
// TODO(vladimir): Is there a more specific check that can be done?
if conn.Process().IsSystemResolver() {
return endpoints.NoMatch, "", nil
}
if conn.Entity.Port == 53 && conn.Entity.IPScope.IsLocalhost() {
return endpoints.NoMatch, "", nil
}
}
// Block bypass attempts using an (encrypted) DNS server.
switch {
case looksLikeOutgoingDNSRequest(conn) && module.instance.Resolver().IsDisabled():
// Allow. Packet will be analyzed and blocked if its not a dns request, before sent.
conn.Inspecting = true
return endpoints.NoMatch, "", nil
case conn.Entity.Port == 53:
return endpoints.Denied,
"blocked DNS query, manual dns setup required",
@ -62,3 +78,17 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
return endpoints.NoMatch, "", nil
}
func looksLikeOutgoingDNSRequest(conn *network.Connection) bool {
// Outbound on remote port 53, UDP.
if conn.Inbound {
return false
}
if conn.Entity.Port != 53 {
return false
}
if conn.IPProtocol != packet.UDP {
return false
}
return true
}

View file

@ -287,6 +287,30 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
}
}
// Create new record for this IP.
record := resolver.ResolvedDomain{
Domain: q.FQDN,
Resolver: rrCache.Resolver,
DNSRequestContext: rrCache.ToDNSRequestContext(),
Expires: rrCache.Expires,
}
// Process CNAMEs
record.AddCNAMEs(cnames)
// Link connection with cnames.
if conn.Type == network.DNSRequest {
conn.Entity.CNAME = record.CNAMEs
}
SaveIPsInCache(ips, profileID, record)
}
// formatRR is a friendlier alternative to miekg/dns.RR.String().
func formatRR(rr dns.RR) string {
return strings.ReplaceAll(rr.String(), "\t", " ")
}
// SaveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
func SaveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
// Package IPs and CNAMEs into IPInfo structs.
for _, ip := range ips {
// Never save domain attributions for localhost IPs.
@ -294,31 +318,6 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
continue
}
// Create new record for this IP.
record := resolver.ResolvedDomain{
Domain: q.FQDN,
Resolver: rrCache.Resolver,
DNSRequestContext: rrCache.ToDNSRequestContext(),
Expires: rrCache.Expires,
}
// Resolve all CNAMEs in the correct order and add the to the record - up to max 50 layers.
domain := q.FQDN
for range 50 {
nextDomain, isCNAME := cnames[domain]
if !isCNAME || nextDomain == domain {
break
}
record.CNAMEs = append(record.CNAMEs, nextDomain)
domain = nextDomain
}
// Update the entity to include the CNAMEs of the query response.
conn.Entity.CNAME = record.CNAMEs
// Check if there is an existing record for this DNS response.
// Else create a new one.
ipString := ip.String()
info, err := resolver.GetIPInfo(profileID, ipString)
if err != nil {
@ -341,8 +340,3 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
}
}
}
// formatRR is a friendlier alternative to miekg/dns.RR.String().
func formatRR(rr dns.RR) string {
return strings.ReplaceAll(rr.String(), "\t", " ")
}

View file

@ -0,0 +1,99 @@
//go:build windows
// +build windows
package dnsmonitor
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"github.com/safing/portmaster/service/integration"
"golang.org/x/sys/windows"
)
type ETWSession struct {
i integration.ETWFunctions
shutdownGuard atomic.Bool
shutdownMutex sync.Mutex
state uintptr
}
// NewSession creates new ETW event listener and initilizes it. This is a low level interface, make sure to call DestorySession when you are done using it.
func NewSession(etwInterface integration.ETWFunctions, callback func(domain string, result string)) (*ETWSession, error) {
etwSession := &ETWSession{
i: etwInterface,
}
// Make sure session from previous instances are not running.
_ = etwSession.i.StopOldSession()
// Initialize notification activated callback
win32Callback := windows.NewCallback(func(domain *uint16, result *uint16) uintptr {
callback(windows.UTF16PtrToString(domain), windows.UTF16PtrToString(result))
return 0
})
// The function only allocates memory it will not fail.
etwSession.state = etwSession.i.CreateState(win32Callback)
// Make sure DestroySession is called even if caller forgets to call it.
runtime.SetFinalizer(etwSession, func(s *ETWSession) {
_ = s.i.DestroySession(s.state)
})
// Initialize session.
err := etwSession.i.InitializeSession(etwSession.state)
if err != nil {
return nil, fmt.Errorf("failed to initialzie session: %q", err)
}
return etwSession, nil
}
// StartTrace starts the tracing session of dns events. This is a blocking call. It will not return until the trace is stopped.
func (l *ETWSession) StartTrace() error {
return l.i.StartTrace(l.state)
}
// IsRunning returns true if DestroySession has NOT been called.
func (l *ETWSession) IsRunning() bool {
return !l.shutdownGuard.Load()
}
// FlushTrace flushes the trace buffer.
func (l *ETWSession) FlushTrace() error {
l.shutdownMutex.Lock()
defer l.shutdownMutex.Unlock()
// Make sure session is still running.
if l.shutdownGuard.Load() {
return nil
}
return l.i.FlushTrace(l.state)
}
// StopTrace stopes the trace. This will cause StartTrace to return.
func (l *ETWSession) StopTrace() error {
return l.i.StopTrace(l.state)
}
// DestroySession closes the session and frees the allocated memory. Listener cannot be used after this function is called.
func (l *ETWSession) DestroySession() error {
l.shutdownMutex.Lock()
defer l.shutdownMutex.Unlock()
if l.shutdownGuard.Swap(true) {
return nil
}
err := l.i.DestroySession(l.state)
if err != nil {
return err
}
l.state = 0
return nil
}

View file

@ -0,0 +1,19 @@
//go:build !linux && !windows
// +build !linux,!windows
package dnsmonitor
type Listener struct{}
func newListener(_ *DNSMonitor) (*Listener, error) {
return &Listener{}, nil
}
func (l *Listener) flush() error {
// Nothing to flush
return nil
}
func (l *Listener) stop() error {
return nil
}

View file

@ -0,0 +1,145 @@
//go:build linux
// +build linux
package dnsmonitor
import (
"errors"
"fmt"
"net"
"os"
"github.com/miekg/dns"
"github.com/varlink/go/varlink"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/resolver"
)
type Listener struct {
varlinkConn *varlink.Connection
}
func newListener(module *DNSMonitor) (*Listener, error) {
// Set source of the resolver.
ResolverInfo.Source = resolver.ServerSourceSystemd
// Check if the system has systemd-resolver.
_, err := os.Stat("/run/systemd/resolve/io.systemd.Resolve.Monitor")
if err != nil {
return nil, fmt.Errorf("system does not support systemd resolver monitor")
}
listener := &Listener{}
restartAttempts := 0
module.mgr.Go("systemd-resolver-event-listener", func(w *mgr.WorkerCtx) error {
// Abort initialization if the connection failed after too many tries.
if restartAttempts > 10 {
return nil
}
restartAttempts += 1
// Initialize varlink connection
varlinkConn, err := varlink.NewConnection(module.mgr.Ctx(), "unix:/run/systemd/resolve/io.systemd.Resolve.Monitor")
if err != nil {
return fmt.Errorf("failed to connect to systemd-resolver varlink service: %w", err)
}
defer func() {
if varlinkConn != nil {
err = varlinkConn.Close()
if err != nil {
log.Errorf("dnsmonitor: failed to close varlink connection: %s", err)
}
}
}()
listener.varlinkConn = varlinkConn
// Subscribe to the dns query events
receive, err := listener.varlinkConn.Send(w.Ctx(), "io.systemd.Resolve.Monitor.SubscribeQueryResults", nil, varlink.More)
if err != nil {
var varlinkErr *varlink.Error
if errors.As(err, &varlinkErr) {
return fmt.Errorf("failed to issue Varlink call: %+v", varlinkErr.Parameters)
} else {
return fmt.Errorf("failed to issue Varlink call: %w", err)
}
}
for {
queryResult := QueryResult{}
// Receive the next event from the resolver.
flags, err := receive(w.Ctx(), &queryResult)
if err != nil {
var varlinkErr *varlink.Error
if errors.As(err, &varlinkErr) {
return fmt.Errorf("failed to receive Varlink reply: %+v", varlinkErr.Parameters)
} else {
return fmt.Errorf("failed to receive Varlink reply: %w", err)
}
}
// Check if the reply indicates the end of the stream
if flags&varlink.Continues == 0 {
break
}
// Ignore if there is no question.
if queryResult.Question == nil || len(*queryResult.Question) == 0 {
continue
}
// Protmaster self check
domain := (*queryResult.Question)[0].Name
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
// Not need to process result.
continue
}
if queryResult.Rcode != nil {
continue // Ignore DNS errors
}
listener.processAnswer(domain, &queryResult)
}
return nil
})
return listener, nil
}
func (l *Listener) flush() error {
// Nothing to flush
return nil
}
func (l *Listener) stop() error {
return nil
}
func (l *Listener) processAnswer(domain string, queryResult *QueryResult) {
// Allocated data struct for the parsed result.
cnames := make(map[string]string)
ips := make([]net.IP, 0, 5)
// Check if the query is valid
if queryResult.Answer == nil {
return
}
// Go trough each answer entry.
for _, a := range *queryResult.Answer {
if a.RR.Address != nil {
ip := net.IP(*a.RR.Address)
// Answer contains ip address.
ips = append(ips, ip)
} else if a.RR.Name != nil {
// Answer is a CNAME.
cnames[domain] = *a.RR.Name
}
}
saveDomain(domain, ips, cnames)
}

View file

@ -0,0 +1,103 @@
//go:build windows
// +build windows
package dnsmonitor
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/miekg/dns"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/resolver"
)
type Listener struct {
etw *ETWSession
}
func newListener(module *DNSMonitor) (*Listener, error) {
// Set source of the resolver.
ResolverInfo.Source = resolver.ServerSourceETW
listener := &Listener{}
var err error
// Initialize new dns event session.
listener.etw, err = NewSession(module.instance.OSIntegration().GetETWInterface(), listener.processEvent)
if err != nil {
return nil, err
}
// Start listening for events.
module.mgr.Go("etw-dns-event-listener", func(w *mgr.WorkerCtx) error {
return listener.etw.StartTrace()
})
return listener, nil
}
func (l *Listener) flush() error {
return l.etw.FlushTrace()
}
func (l *Listener) stop() error {
if l == nil {
return fmt.Errorf("listener is nil")
}
if l.etw == nil {
return fmt.Errorf("invalid etw session")
}
// Stop and destroy trace. Destroy should be called even if stop fails for some reason.
err := l.etw.StopTrace()
err2 := l.etw.DestroySession()
if err != nil {
return fmt.Errorf("StopTrace failed: %w", err)
}
if err2 != nil {
return fmt.Errorf("DestroySession failed: %w", err2)
}
return nil
}
func (l *Listener) processEvent(domain string, result string) {
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
// Not need to process result.
return
}
// Ignore empty results
if len(result) == 0 {
return
}
cnames := make(map[string]string)
ips := []net.IP{}
resultArray := strings.Split(result, ";")
for _, r := range resultArray {
// For results other than IP addresses, the string starts with "type:"
if strings.HasPrefix(r, "type:") {
dnsValueArray := strings.Split(r, " ")
if len(dnsValueArray) < 3 {
continue
}
// Ignore everything except CNAME records
if value, err := strconv.ParseInt(dnsValueArray[1], 10, 16); err == nil && value == int64(dns.TypeCNAME) {
cnames[domain] = dnsValueArray[2]
}
} else {
// If the event doesn't start with "type:", it's an IP address
ip := net.ParseIP(r)
if ip != nil {
ips = append(ips, ip)
}
}
}
saveDomain(domain, ips, cnames)
}

View file

@ -0,0 +1,139 @@
package dnsmonitor
import (
"errors"
"net"
"strings"
"github.com/miekg/dns"
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/compat"
"github.com/safing/portmaster/service/integration"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/resolver"
)
var ResolverInfo = resolver.ResolverInfo{
Name: "SystemResolver",
Type: resolver.ServerTypeMonitor,
}
type DNSMonitor struct {
instance instance
mgr *mgr.Manager
listener *Listener
}
// Manager returns the module manager.
func (dl *DNSMonitor) Manager() *mgr.Manager {
return dl.mgr
}
// Start starts the module.
func (dl *DNSMonitor) Start() error {
// Initialize dns event listener
var err error
dl.listener, err = newListener(dl)
if err != nil {
log.Warningf("dnsmonitor: failed to start dns listener: %s", err)
}
return nil
}
// Stop stops the module.
func (dl *DNSMonitor) Stop() error {
if dl.listener != nil {
err := dl.listener.stop()
if err != nil {
log.Errorf("dnsmonitor: failed to close listener: %s", err)
}
}
return nil
}
// Flush flushes the buffer forcing all events to be processed.
func (dl *DNSMonitor) Flush() error {
return dl.listener.flush()
}
func saveDomain(domain string, ips []net.IP, cnames map[string]string) {
fqdn := dns.Fqdn(domain)
// Create new record for this IP.
record := resolver.ResolvedDomain{
Domain: fqdn,
Resolver: &ResolverInfo,
DNSRequestContext: &resolver.DNSRequestContext{},
Expires: 0,
}
// Process cnames
record.AddCNAMEs(cnames)
// Add to cache
saveIPsInCache(ips, resolver.IPInfoProfileScopeGlobal, record)
}
func New(instance instance) (*DNSMonitor, error) {
// Initialize module
m := mgr.New("DNSMonitor")
module := &DNSMonitor{
mgr: m,
instance: instance,
}
return module, nil
}
type instance interface {
OSIntegration() *integration.OSIntegration
}
func processIfSelfCheckDomain(fqdn string) bool {
// Check for compat check dns request.
if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
_ = compat.SubmitDNSCheckDomain(subdomain)
log.Infof("dnsmonitor: self-check domain received")
// No need to parse the answer.
return true
}
return false
}
// saveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
func saveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
// Package IPs and CNAMEs into IPInfo structs.
for _, ip := range ips {
// Never save domain attributions for localhost IPs.
if netutils.GetIPScope(ip) == netutils.HostLocal {
continue
}
ipString := ip.String()
info, err := resolver.GetIPInfo(profileID, ipString)
if err != nil {
if !errors.Is(err, database.ErrNotFound) {
log.Errorf("dnsmonitor: failed to search for IP info record: %s", err)
}
info = &resolver.IPInfo{
IP: ipString,
ProfileID: profileID,
}
}
// Add the new record to the resolved domains for this IP and scope.
info.AddDomain(record)
// Save if the record is new or has been updated.
if err := info.Save(); err != nil {
log.Errorf("dnsmonitor: failed to save IP info record: %s", err)
}
}
}

View file

@ -0,0 +1,83 @@
//go:build linux
// +build linux
package dnsmonitor
// List of struct that define the systemd-resolver varlink dns event protocol.
// Source: `sudo varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve.Monitor io.systemd.Resolve.Monitor`
type ResourceKey struct {
Class int `json:"class"`
Type int `json:"type"`
Name string `json:"name"`
}
type ResourceRecord struct {
Key ResourceKey `json:"key"`
Name *string `json:"name,omitempty"`
Address *[]byte `json:"address,omitempty"`
// Rest of the fields are not used.
// Priority *int `json:"priority,omitempty"`
// Weight *int `json:"weight,omitempty"`
// Port *int `json:"port,omitempty"`
// CPU *string `json:"cpu,omitempty"`
// OS *string `json:"os,omitempty"`
// Items *[]string `json:"items,omitempty"`
// MName *string `json:"mname,omitempty"`
// RName *string `json:"rname,omitempty"`
// Serial *int `json:"serial,omitempty"`
// Refresh *int `json:"refresh,omitempty"`
// Expire *int `json:"expire,omitempty"`
// Minimum *int `json:"minimum,omitempty"`
// Exchange *string `json:"exchange,omitempty"`
// Version *int `json:"version,omitempty"`
// Size *int `json:"size,omitempty"`
// HorizPre *int `json:"horiz_pre,omitempty"`
// VertPre *int `json:"vert_pre,omitempty"`
// Latitude *int `json:"latitude,omitempty"`
// Longitude *int `json:"longitude,omitempty"`
// Altitude *int `json:"altitude,omitempty"`
// KeyTag *int `json:"key_tag,omitempty"`
// Algorithm *int `json:"algorithm,omitempty"`
// DigestType *int `json:"digest_type,omitempty"`
// Digest *string `json:"digest,omitempty"`
// FPType *int `json:"fptype,omitempty"`
// Fingerprint *string `json:"fingerprint,omitempty"`
// Flags *int `json:"flags,omitempty"`
// Protocol *int `json:"protocol,omitempty"`
// DNSKey *string `json:"dnskey,omitempty"`
// Signer *string `json:"signer,omitempty"`
// TypeCovered *int `json:"type_covered,omitempty"`
// Labels *int `json:"labels,omitempty"`
// OriginalTTL *int `json:"original_ttl,omitempty"`
// Expiration *int `json:"expiration,omitempty"`
// Inception *int `json:"inception,omitempty"`
// Signature *string `json:"signature,omitempty"`
// NextDomain *string `json:"next_domain,omitempty"`
// Types *[]int `json:"types,omitempty"`
// Iterations *int `json:"iterations,omitempty"`
// Salt *string `json:"salt,omitempty"`
// Hash *string `json:"hash,omitempty"`
// CertUsage *int `json:"cert_usage,omitempty"`
// Selector *int `json:"selector,omitempty"`
// MatchingType *int `json:"matching_type,omitempty"`
// Data *string `json:"data,omitempty"`
// Tag *string `json:"tag,omitempty"`
// Value *string `json:"value,omitempty"`
}
type Answer struct {
RR *ResourceRecord `json:"rr,omitempty"`
Raw string `json:"raw"`
IfIndex *int `json:"ifindex,omitempty"`
}
type QueryResult struct {
Ready *bool `json:"ready,omitempty"`
State *string `json:"state,omitempty"`
Rcode *int `json:"rcode,omitempty"`
Errno *int `json:"errno,omitempty"`
Question *[]ResourceKey `json:"question,omitempty"`
CollectedQuestions *[]ResourceKey `json:"collectedQuestions,omitempty"`
Answer *[]Answer `json:"answer,omitempty"`
}

View file

@ -188,7 +188,7 @@ func (q *Queue) packetHandler(ctx context.Context) func(nfqueue.Attribute) int {
return 0
}
if err := pmpacket.Parse(*attrs.Payload, &pkt.Base); err != nil {
if err := pmpacket.ParseLayer3(*attrs.Payload, &pkt.Base); err != nil {
log.Warningf("nfqueue: failed to parse payload: %s", err)
_ = pkt.Drop()
return 0

View file

@ -59,7 +59,7 @@ func (pkt *Packet) LoadPacketData() error {
return packet.ErrFailedToLoadPayload
}
err = packet.Parse(payload, &pkt.Base)
err = packet.ParseLayer3(payload, &pkt.Base)
if err != nil {
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)
return packet.ErrFailedToLoadPayload

View file

@ -55,6 +55,7 @@ func Handler(ctx context.Context, packets chan packet.Packet, bandwidthUpdate ch
newPacket := &Packet{
verdictRequest: conn.ID,
payload: conn.Payload,
payloadLayer: conn.PayloadLayer,
verdictSet: abool.NewBool(false),
}
info := newPacket.Info()

View file

@ -4,6 +4,7 @@
package windowskext
import (
"fmt"
"sync"
"github.com/tevino/abool"
@ -19,6 +20,7 @@ type Packet struct {
verdictRequest uint64
payload []byte
payloadLayer uint8
verdictSet *abool.AtomicBool
payloadLoaded bool
@ -51,7 +53,15 @@ func (pkt *Packet) LoadPacketData() error {
pkt.payloadLoaded = true
if len(pkt.payload) > 0 {
err := packet.Parse(pkt.payload, &pkt.Base)
var err error
switch pkt.payloadLayer {
case 3:
err = packet.ParseLayer3(pkt.payload, &pkt.Base)
case 4:
err = packet.ParseLayer4(pkt.payload, &pkt.Base)
default:
err = fmt.Errorf("unsupported payload layer: %d", pkt.payloadLayer)
}
if err != nil {
log.Tracef("payload: %#v", pkt.payload)
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)

View file

@ -16,6 +16,7 @@ import (
"github.com/safing/portmaster/service/netquery"
"github.com/safing/portmaster/service/network"
"github.com/safing/portmaster/service/profile"
"github.com/safing/portmaster/service/resolver"
"github.com/safing/portmaster/spn/access"
"github.com/safing/portmaster/spn/captain"
)
@ -34,8 +35,7 @@ func (ss *stringSliceFlag) Set(value string) error {
var allowedClients stringSliceFlag
type Firewall struct {
mgr *mgr.Manager
mgr *mgr.Manager
instance instance
}
@ -165,4 +165,5 @@ type instance interface {
Access() *access.Access
Network() *network.Network
NetQuery() *netquery.NetQuery
Resolver() *resolver.ResolverModule
}

View file

@ -6,10 +6,12 @@ import (
"fmt"
"net"
"os"
"strings"
"sync/atomic"
"time"
"github.com/google/gopacket/layers"
"github.com/miekg/dns"
"github.com/tevino/abool"
"github.com/safing/portmaster/base/log"
@ -23,6 +25,7 @@ import (
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/resolver"
"github.com/safing/portmaster/spn/access"
)
@ -444,8 +447,9 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
filterConnection = false
log.Tracer(pkt.Ctx()).Infof("filter: granting own pre-authenticated connection %s", conn)
// Redirect outbound DNS packets if enabled,
// Redirect outbound DNS packets if enabled,
case dnsQueryInterception() &&
!module.instance.Resolver().IsDisabled() &&
pkt.IsOutbound() &&
pkt.Info().DstPort == 53 &&
// that don't match the address of our nameserver,
@ -478,11 +482,13 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
// Decide how to continue handling connection.
switch {
case conn.Inspecting && looksLikeOutgoingDNSRequest(conn):
inspectDNSPacket(conn, pkt)
conn.UpdateFirewallHandler(inspectDNSPacket)
case conn.Inspecting:
log.Tracer(pkt.Ctx()).Trace("filter: start inspecting")
conn.UpdateFirewallHandler(inspectAndVerdictHandler)
inspectAndVerdictHandler(conn, pkt)
default:
conn.StopFirewallHandler()
verdictHandler(conn, pkt)
@ -506,7 +512,7 @@ func FilterConnection(ctx context.Context, conn *network.Connection, pkt packet.
}
// TODO: Enable inspection framework again.
conn.Inspecting = false
// conn.Inspecting = false
// TODO: Quick fix for the SPN.
// Use inspection framework for proper encryption detection.
@ -580,6 +586,98 @@ func inspectAndVerdictHandler(conn *network.Connection, pkt packet.Packet) {
issueVerdict(conn, pkt, 0, true)
}
func inspectDNSPacket(conn *network.Connection, pkt packet.Packet) {
// Ignore info-only packets in this handler.
if pkt.InfoOnly() {
return
}
dnsPacket := new(dns.Msg)
err := pkt.LoadPacketData()
if err != nil {
_ = pkt.Block()
log.Errorf("filter: failed to load packet payload: %s", err)
return
}
// Parse and block invalid packets.
err = dnsPacket.Unpack(pkt.Payload())
if err != nil {
err = pkt.PermanentBlock()
if err != nil {
log.Errorf("filter: failed to block packet: %s", err)
}
_ = conn.SetVerdict(network.VerdictBlock, "none DNS data on DNS port", "", nil)
conn.VerdictPermanent = true
conn.Save()
return
}
// Packet was parsed.
// Allow it but only after the answer was added to the cache.
defer func() {
err = pkt.Accept()
if err != nil {
log.Errorf("filter: failed to accept dns packet: %s", err)
}
}()
// Check if packet has a question.
if len(dnsPacket.Question) == 0 {
return
}
// Read create structs with the needed data.
question := dnsPacket.Question[0]
fqdn := dns.Fqdn(question.Name)
// Check for compat check dns request.
if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
_ = compat.SubmitDNSCheckDomain(subdomain)
log.Infof("packet_handler: self-check domain received")
// No need to parse the answer.
return
}
// Check if there is an answer.
if len(dnsPacket.Answer) == 0 {
return
}
resolverInfo := &resolver.ResolverInfo{
Name: "DNSRequestObserver",
Type: resolver.ServerTypeFirewall,
Source: resolver.ServerSourceFirewall,
IP: conn.Entity.IP,
Domain: conn.Entity.Domain,
IPScope: conn.Entity.IPScope,
}
rrCache := &resolver.RRCache{
Domain: fqdn,
Question: dns.Type(question.Qtype),
RCode: dnsPacket.Rcode,
Answer: dnsPacket.Answer,
Ns: dnsPacket.Ns,
Extra: dnsPacket.Extra,
Resolver: resolverInfo,
}
query := &resolver.Query{
FQDN: fqdn,
QType: dns.Type(question.Qtype),
NoCaching: false,
IgnoreFailing: false,
LocalResolversOnly: false,
ICANNSpace: false,
DomainRoot: "",
}
// Save to cache
UpdateIPsAndCNAMEs(query, rrCache, conn)
}
func icmpFilterHandler(conn *network.Connection, pkt packet.Packet) {
// Load packet data.
err := pkt.LoadPacketData()

View file

@ -19,6 +19,8 @@ import (
"github.com/safing/portmaster/service/core/base"
"github.com/safing/portmaster/service/firewall"
"github.com/safing/portmaster/service/firewall/interception"
"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
"github.com/safing/portmaster/service/integration"
"github.com/safing/portmaster/service/intel/customlists"
"github.com/safing/portmaster/service/intel/filterlists"
"github.com/safing/portmaster/service/intel/geoip"
@ -65,6 +67,7 @@ type Instance struct {
core *core.Core
updates *updates.Updates
integration *integration.OSIntegration
geoip *geoip.GeoIP
netenv *netenv.NetEnv
ui *ui.UI
@ -74,6 +77,7 @@ type Instance struct {
firewall *firewall.Firewall
filterLists *filterlists.FilterLists
interception *interception.Interception
dnsmonitor *dnsmonitor.DNSMonitor
customlist *customlists.CustomList
status *status.Status
broadcasts *broadcasts.Broadcasts
@ -107,7 +111,6 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
var err error
// Base modules
instance.base, err = base.New(instance)
if err != nil {
@ -151,6 +154,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
if err != nil {
return instance, fmt.Errorf("create updates module: %w", err)
}
instance.integration, err = integration.New(instance)
if err != nil {
return instance, fmt.Errorf("create integration module: %w", err)
}
instance.geoip, err = geoip.New(instance)
if err != nil {
return instance, fmt.Errorf("create customlist module: %w", err)
@ -187,6 +194,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
if err != nil {
return instance, fmt.Errorf("create interception module: %w", err)
}
instance.dnsmonitor, err = dnsmonitor.New(instance)
if err != nil {
return instance, fmt.Errorf("create dns-listener module: %w", err)
}
instance.customlist, err = customlists.New(instance)
if err != nil {
return instance, fmt.Errorf("create customlist module: %w", err)
@ -275,6 +286,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.core,
instance.updates,
instance.integration,
instance.geoip,
instance.netenv,
@ -288,6 +300,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.filterLists,
instance.customlist,
instance.interception,
instance.dnsmonitor,
instance.compat,
instance.status,
@ -378,6 +391,11 @@ func (i *Instance) Updates() *updates.Updates {
return i.updates
}
// OSIntegration returns the integration module.
func (i *Instance) OSIntegration() *integration.OSIntegration {
return i.integration
}
// GeoIP returns the geoip module.
func (i *Instance) GeoIP() *geoip.GeoIP {
return i.geoip
@ -463,6 +481,11 @@ func (i *Instance) Interception() *interception.Interception {
return i.interception
}
// DNSMonitor returns the dns-listener module.
func (i *Instance) DNSMonitor() *dnsmonitor.DNSMonitor {
return i.dnsmonitor
}
// CustomList returns the customlist module.
func (i *Instance) CustomList() *customlists.CustomList {
return i.customlist

View file

@ -0,0 +1,114 @@
//go:build windows
// +build windows
package integration
import (
"fmt"
"golang.org/x/sys/windows"
)
type ETWFunctions struct {
createState *windows.Proc
initializeSession *windows.Proc
startTrace *windows.Proc
flushTrace *windows.Proc
stopTrace *windows.Proc
destroySession *windows.Proc
stopOldSession *windows.Proc
}
func initializeETW(dll *windows.DLL) (ETWFunctions, error) {
var functions ETWFunctions
var err error
functions.createState, err = dll.FindProc("PM_ETWCreateState")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWCreateState: %q", err)
}
functions.initializeSession, err = dll.FindProc("PM_ETWInitializeSession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWInitializeSession: %q", err)
}
functions.startTrace, err = dll.FindProc("PM_ETWStartTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWStartTrace: %q", err)
}
functions.flushTrace, err = dll.FindProc("PM_ETWFlushTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWFlushTrace: %q", err)
}
functions.stopTrace, err = dll.FindProc("PM_ETWStopTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWStopTrace: %q", err)
}
functions.destroySession, err = dll.FindProc("PM_ETWDestroySession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
}
functions.stopOldSession, err = dll.FindProc("PM_ETWStopOldSession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
}
return functions, nil
}
// CreateState calls the dll createState C function.
func (etw ETWFunctions) CreateState(callback uintptr) uintptr {
state, _, _ := etw.createState.Call(callback)
return state
}
// InitializeSession calls the dll initializeSession C function.
func (etw ETWFunctions) InitializeSession(state uintptr) error {
rc, _, _ := etw.initializeSession.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StartTrace calls the dll startTrace C function.
func (etw ETWFunctions) StartTrace(state uintptr) error {
rc, _, _ := etw.startTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// FlushTrace calls the dll flushTrace C function.
func (etw ETWFunctions) FlushTrace(state uintptr) error {
rc, _, _ := etw.flushTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StopTrace calls the dll stopTrace C function.
func (etw ETWFunctions) StopTrace(state uintptr) error {
rc, _, _ := etw.stopTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// DestroySession calls the dll destroySession C function.
func (etw ETWFunctions) DestroySession(state uintptr) error {
rc, _, _ := etw.destroySession.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StopOldSession calls the dll stopOldSession C function.
func (etw ETWFunctions) StopOldSession() error {
rc, _, _ := etw.stopOldSession.Call()
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}

View file

@ -0,0 +1,16 @@
//go:build !windows
// +build !windows
package integration
type OSSpecific struct{}
// Initialize is empty on any OS different then Windows.
func (i *OSIntegration) Initialize() error {
return nil
}
// CleanUp releases any resourses allocated during initializaion.
func (i *OSIntegration) CleanUp() error {
return nil
}

View file

@ -0,0 +1,52 @@
//go:build windows
// +build windows
package integration
import (
"fmt"
"github.com/safing/portmaster/service/updates"
"golang.org/x/sys/windows"
)
type OSSpecific struct {
dll *windows.DLL
etwFunctions ETWFunctions
}
// Initialize loads the dll and finds all the needed functions from it.
func (i *OSIntegration) Initialize() error {
// Find path to the dll.
file, err := updates.GetFile("portmaster-core.dll")
if err != nil {
return err
}
// Load the DLL.
i.os.dll, err = windows.LoadDLL(file.Path())
if err != nil {
return fmt.Errorf("failed to load dll: %q", err)
}
// Enumerate all needed dll functions.
i.os.etwFunctions, err = initializeETW(i.os.dll)
if err != nil {
return err
}
return nil
}
// CleanUp releases any resourses allocated during initializaion.
func (i *OSIntegration) CleanUp() error {
if i.os.dll != nil {
return i.os.dll.Release()
}
return nil
}
// GetETWInterface return struct containing all the ETW related functions.
func (i *OSIntegration) GetETWInterface() ETWFunctions {
return i.os.etwFunctions
}

View file

@ -0,0 +1,49 @@
package integration
import (
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
)
// OSIntegration module provides special integration with the OS.
type OSIntegration struct {
m *mgr.Manager
states *mgr.StateMgr
//nolint:unused
os OSSpecific
instance instance
}
// New returns a new OSIntegration module.
func New(instance instance) (*OSIntegration, error) {
m := mgr.New("OSIntegration")
module := &OSIntegration{
m: m,
states: m.NewStateMgr(),
instance: instance,
}
return module, nil
}
// Manager returns the module manager.
func (i *OSIntegration) Manager() *mgr.Manager {
return i.m
}
// Start starts the module.
func (i *OSIntegration) Start() error {
return i.Initialize()
}
// Stop stops the module.
func (i *OSIntegration) Stop() error {
return i.CleanUp()
}
type instance interface {
Updates() *updates.Updates
}

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net"
"runtime"
"sync"
"sync/atomic"
"time"
@ -18,6 +19,7 @@ import (
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/network/reference"
"github.com/safing/portmaster/service/process"
_ "github.com/safing/portmaster/service/process/tags"
"github.com/safing/portmaster/service/resolver"
@ -542,6 +544,23 @@ func (conn *Connection) GatherConnectionInfo(pkt packet.Packet) (err error) {
// Try again with the global scope, in case DNS went through the system resolver.
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
}
if runtime.GOOS == "windows" && err != nil {
// On windows domains may come with delay.
if module.instance.Resolver().IsDisabled() && conn.shouldWaitForDomain() {
// Flush the dns listener buffer and try again.
for i := range 4 {
_ = module.instance.DNSMonitor().Flush()
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
if err == nil {
log.Tracer(pkt.Ctx()).Debugf("network: found domain from dnsmonitor after %d tries", i+1)
break
}
time.Sleep(5 * time.Millisecond)
}
}
}
if err == nil {
lastResolvedDomain := ipinfo.MostRecentDomain()
if lastResolvedDomain != nil {
@ -869,3 +888,17 @@ func (conn *Connection) String() string {
return fmt.Sprintf("%s -> %s", conn.process, conn.Entity.IP)
}
}
func (conn *Connection) shouldWaitForDomain() bool {
// Should wait for Global Unicast, outgoing and not ICMP connections
switch {
case conn.Entity.IPScope != netutils.Global:
return false
case conn.Inbound:
return false
case reference.IsICMP(conn.Entity.Protocol):
return false
}
return true
}

View file

@ -9,10 +9,12 @@ import (
"sync/atomic"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/state"
"github.com/safing/portmaster/service/profile"
"github.com/safing/portmaster/service/resolver"
)
// Events.
@ -188,4 +190,6 @@ func New(instance instance) (*Network, error) {
type instance interface {
Profile() *profile.ProfileModule
Resolver() *resolver.ResolverModule
DNSMonitor() *dnsmonitor.DNSMonitor
}

View file

@ -106,11 +106,12 @@ func checkError(packet gopacket.Packet, info *Info) error {
return nil
}
// Parse parses an IP packet and saves the information in the given packet object.
func Parse(packetData []byte, pktBase *Base) (err error) {
// ParseLayer3 parses an IP packet and saves the information in the given packet object.
func ParseLayer3(packetData []byte, pktBase *Base) (err error) {
if len(packetData) == 0 {
return errors.New("empty packet")
}
pktBase.layer3Data = packetData
ipVersion := packetData[0] >> 4
@ -155,6 +156,62 @@ func Parse(packetData []byte, pktBase *Base) (err error) {
return nil
}
// ParseLayer4 parses an layer 4 packet and saves the information in the given packet object.
func ParseLayer4(packetData []byte, pktBase *Base) (err error) {
if len(packetData) == 0 {
return errors.New("empty packet")
}
var layer gopacket.LayerType
switch pktBase.info.Protocol {
case ICMP:
layer = layers.LayerTypeICMPv4
case IGMP:
layer = layers.LayerTypeIGMP
case TCP:
layer = layers.LayerTypeTCP
case UDP:
layer = layers.LayerTypeUDP
case ICMPv6:
layer = layers.LayerTypeICMPv6
case UDPLite:
return fmt.Errorf("UDPLite not supported")
case RAW:
return fmt.Errorf("RAW protocol not supported")
case AnyHostInternalProtocol61:
return fmt.Errorf("AnyHostInternalProtocol61 protocol not supported")
default:
return fmt.Errorf("protocol not supported")
}
packet := gopacket.NewPacket(packetData, layer, gopacket.DecodeOptions{
Lazy: true,
NoCopy: true,
})
availableDecoders := []func(gopacket.Packet, *Info) error{
parseTCP,
parseUDP,
// parseUDPLite, // We don't yet support udplite.
parseICMPv4,
parseICMPv6,
parseIGMP,
checkError,
}
for _, dec := range availableDecoders {
if err := dec(packet, pktBase.Info()); err != nil {
return err
}
}
pktBase.layers = packet
if transport := packet.TransportLayer(); transport != nil {
pktBase.layer5Data = transport.LayerPayload()
}
return nil
}
func init() {
genIPProtocolFromLayerType()
}

View file

@ -52,6 +52,27 @@ type ResolvedDomain struct {
Expires int64
}
// AddCNAMEs adds all cnames from the map related to its set Domain.
func (resolved *ResolvedDomain) AddCNAMEs(cnames map[string]string) {
// Resolve all CNAMEs in the correct order and add the to the record - up to max 50 layers.
domain := resolved.Domain
domainLoop:
for range 50 {
nextDomain, isCNAME := cnames[domain]
switch {
case !isCNAME:
break domainLoop
case nextDomain == resolved.Domain:
break domainLoop
case nextDomain == domain:
break domainLoop
}
resolved.CNAMEs = append(resolved.CNAMEs, nextDomain)
domain = nextDomain
}
}
// String returns a string representation of ResolvedDomain including
// the CNAME chain. It implements fmt.Stringer.
func (resolved *ResolvedDomain) String() string {

View file

@ -29,6 +29,8 @@ type ResolverModule struct { //nolint
failingResolverWorkerMgr *mgr.WorkerMgr
suggestUsingStaleCacheTask *mgr.WorkerMgr
isDisabled atomic.Bool
states *mgr.StateMgr
}
@ -52,6 +54,10 @@ func (rm *ResolverModule) Stop() error {
return nil
}
func (rm *ResolverModule) IsDisabled() bool {
return rm.isDisabled.Load()
}
func prep() error {
// Set DNS test connectivity function for the online status check
netenv.DNSTestQueryFunc = func(ctx context.Context, fdqn string) (ips []net.IP, ok bool, err error) {

View file

@ -17,17 +17,22 @@ import (
// DNS Resolver Attributes.
const (
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerTypeMDNS = "mdns"
ServerTypeEnv = "env"
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerTypeMDNS = "mdns"
ServerTypeEnv = "env"
ServerTypeMonitor = "monitor"
ServerTypeFirewall = "firewall"
ServerSourceConfigured = "config"
ServerSourceOperatingSystem = "system"
ServerSourceMDNS = "mdns"
ServerSourceEnv = "env"
ServerSourceETW = "etw"
ServerSourceSystemd = "systemd"
ServerSourceFirewall = "firewall"
)
// DNS resolver scheme aliases.
@ -82,11 +87,11 @@ type ResolverInfo struct { //nolint:golint,maligned // TODO
Name string
// Type describes the type of the resolver.
// Possible values include dns, tcp, dot, doh, mdns, env.
// Possible values include dns, tcp, dot, doh, mdns, env, monitor, firewall.
Type string
// Source describes where the resolver configuration came from.
// Possible values include config, system, mdns, env.
// Possible values include config, system, mdns, env, etw, systemd, firewall.
Source string
// IP is the IP address of the resolver

View file

@ -388,7 +388,6 @@ func loadResolvers() {
// Resolve module error about missing resolvers.
module.states.Remove(missingResolversErrorID)
// Check if settings were changed and clear name cache when they did.
newResolverConfig := configuredNameServers()
if len(currentResolverConfig) > 0 &&
@ -399,6 +398,14 @@ func loadResolvers() {
return err
})
}
// If no resolvers are configure set the disabled state. So other modules knows that the users does not want to use Portmaster resolver.
if len(newResolverConfig) == 0 {
module.isDisabled.Store(true)
} else {
module.isDisabled.Store(false)
}
currentResolverConfig = newResolverConfig
newResolvers := append(
@ -431,7 +438,7 @@ func loadResolvers() {
// save resolvers
globalResolvers = newResolvers
// assing resolvers to scopes
// assign resolvers to scopes
setScopedResolvers(globalResolvers)
// set active resolvers (for cache validation)

View file

@ -48,9 +48,9 @@ func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error {
sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox")
if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil {
log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile)
return fmt.Errorf("failed to chmod: %w", err)
}
log.Debugf("updates: fixed SUID permission for chrome-sandbox")
return nil

View file

@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/hectane/go-acl"
processInfo "github.com/shirou/gopsutil/process"
"github.com/tevino/abool"
@ -349,7 +350,12 @@ func upgradeBinary(fileToUpgrade string, file *updater.File) error {
}
// check permissions
if !onWindows {
if onWindows {
err = acl.Chmod(fileToUpgrade, 0o0755)
if err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err)
}
} else {
info, err := os.Stat(fileToUpgrade)
if err != nil {
return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err)

View file

@ -0,0 +1,2 @@
msbuild .\windows_core_dll.sln /p:Configuration=Release
ls .\x64\Release\portmaster-core.dll

View file

@ -0,0 +1,197 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#pragma comment(lib, "tdh.lib")
// GUID of the DNS log provider
static const GUID DNS_CLIENT_PROVIDER_GUID = {
0x1C95126E,
0x7EEA,
0x49A9,
{0xA3, 0xFE, 0xA3, 0x78, 0xB0, 0x3D, 0xDB, 0x4D} };
// GUID of the event session. This should be unique for the application.
static const GUID PORTMASTER_ETW_SESSION_GUID = {
0x0211d070,
0xc3b2,
0x4609,
{0x92, 0xf5, 0x28, 0xe7, 0x18, 0xb2, 0x3b, 0x18} };
// Name of the session. This is visble when user queries all ETW sessions.
// (example `logman query -ets`)
#define LOGSESSION_NAME L"PortmasterDNSEventListener"
// Fuction type of the callback that will be called on each event.
typedef uint64_t(*GoEventRecordCallback)(wchar_t* domain, wchar_t* result);
// Holds the state of the ETW Session.
struct ETWSessionState {
TRACEHANDLE SessionTraceHandle;
EVENT_TRACE_PROPERTIES* SessionProperties;
TRACEHANDLE sessionHandle;
GoEventRecordCallback callback;
};
// getPropertyValue reads a property from the event.
static bool getPropertyValue(PEVENT_RECORD evt, LPWSTR prop, PBYTE* pData) {
// Describe the data that needs to be retrieved from the event.
PROPERTY_DATA_DESCRIPTOR DataDescriptor;
ZeroMemory(&DataDescriptor, sizeof(DataDescriptor));
DataDescriptor.PropertyName = (ULONGLONG)(prop);
DataDescriptor.ArrayIndex = 0;
DWORD PropertySize = 0;
// Check if the data is avaliable and what is the size of it.
DWORD status =
TdhGetPropertySize(evt, 0, NULL, 1, &DataDescriptor, &PropertySize);
if (ERROR_SUCCESS != status) {
return false;
}
// Allocate memory for the data.
*pData = (PBYTE)malloc(PropertySize);
if (NULL == *pData) {
return false;
}
// Get the data.
status =
TdhGetProperty(evt, 0, NULL, 1, &DataDescriptor, PropertySize, *pData);
if (ERROR_SUCCESS != status) {
if (*pData) {
free(*pData);
*pData = NULL;
}
return false;
}
return true;
}
// EventRecordCallback is a callback called on each event.
static void WINAPI EventRecordCallback(PEVENT_RECORD eventRecord) {
PBYTE resultValue = NULL;
PBYTE domainValue = NULL;
getPropertyValue(eventRecord, (LPWSTR)L"QueryResults", &resultValue);
getPropertyValue(eventRecord, (LPWSTR)L"QueryName", &domainValue);
ETWSessionState* state = (ETWSessionState*)eventRecord->UserContext;
if (resultValue != NULL && domainValue != NULL) {
state->callback((wchar_t*)domainValue, (wchar_t*)resultValue);
}
free(resultValue);
free(domainValue);
}
extern "C" {
// PM_ETWCreateState allocates memory for the state and initializes the config for the session. PM_ETWDestroySession must be called to avoid leaks.
// callback must be set to a valid function pointer.
__declspec(dllexport) ETWSessionState* PM_ETWCreateState(GoEventRecordCallback callback) {
// Create trace session properties.
ULONG BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME);
EVENT_TRACE_PROPERTIES* SessionProperties =
(EVENT_TRACE_PROPERTIES*)calloc(1, BufferSize);
SessionProperties->Wnode.BufferSize = BufferSize;
SessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
SessionProperties->Wnode.ClientContext = 1; // QPC clock resolution
SessionProperties->Wnode.Guid = PORTMASTER_ETW_SESSION_GUID;
SessionProperties->LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
SessionProperties->MaximumFileSize = 1; // MB
SessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
// Create state
ETWSessionState* state = (ETWSessionState*)calloc(1, sizeof(ETWSessionState));
state->SessionProperties = SessionProperties;
state->callback = callback;
return state;
}
// PM_ETWInitializeSession initializes the session.
__declspec(dllexport) uint32_t PM_ETWInitializeSession(ETWSessionState* state) {
return StartTrace(&state->SessionTraceHandle, LOGSESSION_NAME,
state->SessionProperties);
}
// PM_ETWStartTrace subscribes to the dns events and start listening. The function blocks while the listener is running.
// Call PM_ETWStopTrace to stop the listener.
__declspec(dllexport) uint32_t PM_ETWStartTrace(ETWSessionState* state) {
ULONG status =
EnableTraceEx2(state->SessionTraceHandle, (LPCGUID)&DNS_CLIENT_PROVIDER_GUID,
EVENT_CONTROL_CODE_ENABLE_PROVIDER,
TRACE_LEVEL_INFORMATION, 0, 0, 0, NULL);
if (status != ERROR_SUCCESS) {
return status;
}
EVENT_TRACE_LOGFILE trace = { 0 };
trace.LoggerName = (LPWSTR)(LOGSESSION_NAME);
trace.ProcessTraceMode =
PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD;
trace.EventRecordCallback = EventRecordCallback;
trace.Context = state;
state->sessionHandle = OpenTrace(&trace);
if (state->sessionHandle == INVALID_PROCESSTRACE_HANDLE) {
return 1;
}
status = ProcessTrace(&state->sessionHandle, 1, NULL, NULL);
if (status != ERROR_SUCCESS) {
return 1;
}
return ERROR_SUCCESS;
}
// PM_ETWFlushTrace flushes the event buffer.
__declspec(dllexport) uint32_t PM_ETWFlushTrace(ETWSessionState* state) {
return ControlTrace(state->SessionTraceHandle, LOGSESSION_NAME,
state->SessionProperties, EVENT_TRACE_CONTROL_FLUSH);
}
// PM_ETWFlushTrace stops the listener.
__declspec(dllexport) uint32_t PM_ETWStopTrace(ETWSessionState* state) {
return ControlTrace(state->SessionTraceHandle, LOGSESSION_NAME, state->SessionProperties,
EVENT_TRACE_CONTROL_STOP);
}
// PM_ETWFlushTrace Closes the session and frees resourses.
__declspec(dllexport) uint32_t PM_ETWDestroySession(ETWSessionState* state) {
if (state == NULL) {
return 1;
}
uint32_t status = CloseTrace(state->sessionHandle);
// Free memory.
free(state->SessionProperties);
free(state);
return status;
}
// PM_ETWStopOldSession removes old session with the same name if they exist.
// It returns success(0) only if its able to delete the old session.
__declspec(dllexport) ULONG PM_ETWStopOldSession() {
ULONG status = ERROR_SUCCESS;
TRACEHANDLE sessionHandle = 0;
// Create trace session properties
size_t bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME);
EVENT_TRACE_PROPERTIES* sessionProperties = (EVENT_TRACE_PROPERTIES*)calloc(1, bufferSize);
sessionProperties->Wnode.BufferSize = (ULONG)bufferSize;
sessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
sessionProperties->Wnode.ClientContext = 1; // QPC clock resolution
sessionProperties->Wnode.Guid = PORTMASTER_ETW_SESSION_GUID;
sessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
// Use Control trace will stop the session which will trigger a delete.
status = ControlTrace(NULL, LOGSESSION_NAME, sessionProperties, EVENT_TRACE_CONTROL_STOP);
free(sessionProperties);
return status;
}
}

View file

@ -0,0 +1,5 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>

5
windows_core_dll/pch.cpp Normal file
View file

@ -0,0 +1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header
#include "pch.h"
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.

22
windows_core_dll/pch.h Normal file
View file

@ -0,0 +1,22 @@
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.
#ifndef PCH_H
#define PCH_H
// add headers that you want to pre-compile here
#include "framework.h"
#include <evntrace.h>
#include <tdh.h>
#include <iostream>
#include <string>
#include <evntcons.h>
#include <codecvt>
#include <thread>
#endif //PCH_H

View file

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35222.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "windows_core_dll", "windows_core_dll.vcxproj", "{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x64.ActiveCfg = Debug|x64
{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x64.Build.0 = Debug|x64
{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x86.ActiveCfg = Debug|Win32
{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x86.Build.0 = Debug|Win32
{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x64.ActiveCfg = Release|x64
{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x64.Build.0 = Release|x64
{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x86.ActiveCfg = Release|Win32
{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8E60106D-49DF-49C7-AC08-02775342FEAE}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{6f3c7eaf-8511-4822-aaf0-1086d27e4da9}</ProjectGuid>
<RootNamespace>windowscoredll</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>portmaster-core</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="framework.h" />
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="framework.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

View file

@ -105,6 +105,9 @@ pub fn ale_layer_connect_v6(data: CalloutData) {
}
fn ale_layer_auth(mut data: CalloutData, ale_data: AleLayerData) {
// Make the default path as drop.
data.block_and_absorb();
let Some(device) = crate::entry::get_device() else {
return;
};

View file

@ -110,9 +110,12 @@ fn ip_packet_layer(
interface_index: u32,
sub_interface_index: u32,
) {
// Make the default path as drop.
data.block_and_absorb();
// Block all fragment data. No easy way to keep track of the origin and they are rarely used.
if data.is_fragment_data() {
data.action_block();
data.block_and_absorb();
crate::err!("blocked fragment packet");
return;
}
@ -147,7 +150,7 @@ fn ip_packet_layer(
} {
Ok(key) => key,
Err(err) => {
crate::dbg!("failed to get key from nbl: {}", err);
crate::err!("failed to get key from nbl: {}", err);
return;
}
};

View file

@ -1 +1 @@
[2, 0, 5, 0]
[2, 0, 6, 0]

View file

@ -161,24 +161,28 @@ impl<'a> CalloutData<'a> {
pub fn action_permit(&mut self) {
unsafe {
(*self.classify_out).action_permit();
(*self.classify_out).clear_absorb_flag();
}
}
pub fn action_continue(&mut self) {
unsafe {
(*self.classify_out).action_continue();
(*self.classify_out).clear_absorb_flag();
}
}
pub fn action_block(&mut self) {
unsafe {
(*self.classify_out).action_block();
(*self.classify_out).clear_absorb_flag();
}
}
pub fn action_none(&mut self) {
unsafe {
(*self.classify_out).set_none();
(*self.classify_out).clear_absorb_flag();
}
}
@ -198,13 +202,6 @@ impl<'a> CalloutData<'a> {
self.get_value_u32(flags_index) & FWP_CONDITION_FLAG_IS_REAUTHORIZE > 0
}
pub fn parmit_and_absorb(&mut self) {
unsafe {
(*self.classify_out).action_permit();
(*self.classify_out).set_absorb();
}
}
pub fn get_callout_id(&self) -> usize {
self.callout_id
}

View file

@ -80,6 +80,11 @@ impl ClassifyOut {
self.flags |= FWPS_CLASSIFY_OUT_FLAG_ABSORB;
}
// Removes the absorb flag.
pub fn clear_absorb_flag(&mut self) {
self.flags &= !FWPS_CLASSIFY_OUT_FLAG_ABSORB;
}
// Clear the write flag permission. Next filter in the chain will not change the action.
pub fn clear_write_flag(&mut self) {
self.rights &= !FWPS_RIGHT_ACTION_WRITE;

View file

@ -62,7 +62,7 @@ pub(crate) fn register_sublayer(
sublayer.displayData.name = name.as_ptr() as _;
sublayer.displayData.description = description.as_ptr() as _;
sublayer.flags = 0;
sublayer.weight = 0xFFFF;
sublayer.weight = 0xFFFF; // Set to Max value. Weight compared to other sublayers.
let status = FwpmSubLayerAdd0(filter_engine_handle, &sublayer, core::ptr::null_mut());
check_ntstatus(status as i32)?;