mirror of
https://github.com/safing/portmaster
synced 2025-09-02 02:29:12 +00:00
Merge pull request #146 from safing/feature/ui-revamp
Summary PR for PM v0.6 related changes
This commit is contained in:
commit
8dfcab43c0
98 changed files with 3735 additions and 2160 deletions
|
@ -3,14 +3,16 @@
|
||||||
baseDir="$( cd "$(dirname "$0")" && pwd )"
|
baseDir="$( cd "$(dirname "$0")" && pwd )"
|
||||||
cd "$baseDir"
|
cd "$baseDir"
|
||||||
|
|
||||||
COL_OFF="\033[00m"
|
COL_OFF="\033[0m"
|
||||||
COL_BOLD="\033[01;01m"
|
COL_BOLD="\033[01;01m"
|
||||||
COL_RED="\033[31m"
|
COL_RED="\033[31m"
|
||||||
|
COL_GREEN="\033[32m"
|
||||||
|
COL_YELLOW="\033[33m"
|
||||||
|
|
||||||
destDirPart1="../../dist"
|
destDirPart1="../../dist"
|
||||||
destDirPart2="core"
|
destDirPart2="core"
|
||||||
|
|
||||||
function check {
|
function prep {
|
||||||
# output
|
# output
|
||||||
output="main"
|
output="main"
|
||||||
# get version
|
# get version
|
||||||
|
@ -25,46 +27,47 @@ function check {
|
||||||
fi
|
fi
|
||||||
# build destination path
|
# build destination path
|
||||||
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
|
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
|
||||||
|
}
|
||||||
|
|
||||||
|
function check {
|
||||||
|
prep
|
||||||
|
|
||||||
# check if file exists
|
# check if file exists
|
||||||
if [[ -f $destPath ]]; then
|
if [[ -f $destPath ]]; then
|
||||||
echo "[core] $platform $version already built"
|
echo "[core] $platform v$version already built"
|
||||||
else
|
else
|
||||||
echo -e "${COL_BOLD}[core] $platform $version${COL_OFF}"
|
echo -e "${COL_BOLD}[core] $platform v$version${COL_OFF}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function build {
|
function build {
|
||||||
# output
|
prep
|
||||||
output="main"
|
|
||||||
# get version
|
|
||||||
version=$(grep "info.Set" main.go | cut -d'"' -f4)
|
|
||||||
# build versioned file name
|
|
||||||
filename="portmaster-core_v${version//./-}"
|
|
||||||
# platform
|
|
||||||
platform="${GOOS}_${GOARCH}"
|
|
||||||
if [[ $GOOS == "windows" ]]; then
|
|
||||||
filename="${filename}.exe"
|
|
||||||
output="${output}.exe"
|
|
||||||
fi
|
|
||||||
# build destination path
|
|
||||||
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
|
|
||||||
|
|
||||||
# check if file exists
|
# check if file exists
|
||||||
if [[ -f $destPath ]]; then
|
if [[ -f $destPath ]]; then
|
||||||
echo "[core] $platform already built in version $version, skipping..."
|
echo "[core] $platform already built in v$version, skipping..."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# build
|
# build
|
||||||
./build main.go
|
./build main.go
|
||||||
if [[ $? -ne 0 ]]; then
|
if [[ $? -ne 0 ]]; then
|
||||||
echo -e "\n${COL_BOLD}[core] $platform: ${COL_RED}BUILD FAILED.${COL_OFF}"
|
echo -e "\n${COL_BOLD}[core] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p $(dirname $destPath)
|
mkdir -p $(dirname $destPath)
|
||||||
cp $output $destPath
|
cp $output $destPath
|
||||||
echo -e "\n${COL_BOLD}[core] $platform: successfully built.${COL_OFF}"
|
echo -e "\n${COL_BOLD}[core] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset {
|
||||||
|
prep
|
||||||
|
|
||||||
|
# delete if file exists
|
||||||
|
if [[ -f $destPath ]]; then
|
||||||
|
rm $destPath
|
||||||
|
echo "[core] $platform v$version deleted."
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_all {
|
function check_all {
|
||||||
|
@ -79,6 +82,12 @@ function build_all {
|
||||||
GOOS=darwin GOARCH=amd64 build
|
GOOS=darwin GOARCH=amd64 build
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reset_all {
|
||||||
|
GOOS=linux GOARCH=amd64 reset
|
||||||
|
GOOS=windows GOARCH=amd64 reset
|
||||||
|
GOOS=darwin GOARCH=amd64 reset
|
||||||
|
}
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
"check" )
|
"check" )
|
||||||
check_all
|
check_all
|
||||||
|
@ -86,6 +95,9 @@ case $1 in
|
||||||
"build" )
|
"build" )
|
||||||
build_all
|
build_all
|
||||||
;;
|
;;
|
||||||
|
"reset" )
|
||||||
|
reset_all
|
||||||
|
;;
|
||||||
* )
|
* )
|
||||||
echo ""
|
echo ""
|
||||||
echo "build list:"
|
echo "build list:"
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dataDir string
|
dataDir string
|
||||||
|
staging bool
|
||||||
maxRetries int
|
maxRetries int
|
||||||
dataRoot *utils.DirStructure
|
dataRoot *utils.DirStructure
|
||||||
logsRoot *utils.DirStructure
|
logsRoot *utils.DirStructure
|
||||||
|
@ -41,8 +42,8 @@ var (
|
||||||
Use: "portmaster-start",
|
Use: "portmaster-start",
|
||||||
Short: "Start Portmaster components",
|
Short: "Start Portmaster components",
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||||
mustLoadIndex := cmd == updatesCmd
|
mustLoadIndex := indexRequired(cmd)
|
||||||
if err := configureDataRoot(mustLoadIndex); err != nil {
|
if err := configureRegistry(mustLoadIndex); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,8 +65,9 @@ func init() {
|
||||||
{
|
{
|
||||||
flags.StringVar(&dataDir, "data", "", "Configures the data directory. Alternatively, this can also be set via the environment variable PORTMASTER_DATA.")
|
flags.StringVar(&dataDir, "data", "", "Configures the data directory. Alternatively, this can also be set via the environment variable PORTMASTER_DATA.")
|
||||||
flags.StringVar(®istry.UserAgent, "update-agent", "Start", "Sets the user agent for requests to the update server")
|
flags.StringVar(®istry.UserAgent, "update-agent", "Start", "Sets the user agent for requests to the update server")
|
||||||
|
flags.BoolVar(&staging, "staging", false, "Use staging update channel (for testing only)")
|
||||||
flags.IntVar(&maxRetries, "max-retries", 5, "Maximum number of retries when starting a Portmaster component")
|
flags.IntVar(&maxRetries, "max-retries", 5, "Maximum number of retries when starting a Portmaster component")
|
||||||
flags.BoolVar(&stdinSignals, "input-signals", false, "Emulate signals using stdid.")
|
flags.BoolVar(&stdinSignals, "input-signals", false, "Emulate signals using stdin.")
|
||||||
_ = rootCmd.MarkPersistentFlagDirname("data")
|
_ = rootCmd.MarkPersistentFlagDirname("data")
|
||||||
_ = flags.MarkHidden("input-signals")
|
_ = flags.MarkHidden("input-signals")
|
||||||
}
|
}
|
||||||
|
@ -131,34 +133,32 @@ func initCobra() {
|
||||||
portlog.SetLogLevel(portlog.CriticalLevel)
|
portlog.SetLogLevel(portlog.CriticalLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureDataRoot(mustLoadIndex bool) error {
|
func configureRegistry(mustLoadIndex bool) error {
|
||||||
// The data directory is not
|
// If dataDir is not set, check the environment variable.
|
||||||
// check for environment variable
|
|
||||||
// PORTMASTER_DATA
|
|
||||||
if dataDir == "" {
|
if dataDir == "" {
|
||||||
dataDir = os.Getenv("PORTMASTER_DATA")
|
dataDir = os.Getenv("PORTMASTER_DATA")
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's still empty try to auto-detect it
|
// If it's still empty, try to auto-detect it.
|
||||||
if dataDir == "" {
|
if dataDir == "" {
|
||||||
dataDir = detectInstallationDir()
|
dataDir = detectInstallationDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
// finally, if it's still empty the user must provide it
|
// Finally, if it's still empty, the user must provide it.
|
||||||
if dataDir == "" {
|
if dataDir == "" {
|
||||||
return errors.New("please set the data directory using --data=/path/to/data/dir")
|
return errors.New("please set the data directory using --data=/path/to/data/dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove redundant escape characters and quotes
|
// Remove left over quotes.
|
||||||
dataDir = strings.Trim(dataDir, `\"`)
|
dataDir = strings.Trim(dataDir, `\"`)
|
||||||
// initialize dataroot
|
// Initialize data root.
|
||||||
err := dataroot.Initialize(dataDir, 0755)
|
err := dataroot.Initialize(dataDir, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize data root: %s", err)
|
return fmt.Errorf("failed to initialize data root: %s", err)
|
||||||
}
|
}
|
||||||
dataRoot = dataroot.Root()
|
dataRoot = dataroot.Root()
|
||||||
|
|
||||||
// initialize registry
|
// Initialize registry.
|
||||||
err = registry.Initialize(dataRoot.ChildDir("updates", 0755))
|
err = registry.Initialize(dataRoot.ChildDir("updates", 0755))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -177,6 +177,19 @@ func configureDataRoot(mustLoadIndex bool) error {
|
||||||
// Beta: true,
|
// Beta: true,
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
if stagingActive() {
|
||||||
|
// Set flag no matter how staging was activated.
|
||||||
|
staging = true
|
||||||
|
|
||||||
|
log.Println("WARNING: staging environment is active.")
|
||||||
|
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "staging.json",
|
||||||
|
Stable: true,
|
||||||
|
Beta: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return updateRegistryIndex(mustLoadIndex)
|
return updateRegistryIndex(mustLoadIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,3 +246,14 @@ func detectInstallationDir() string {
|
||||||
|
|
||||||
return parent
|
return parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stagingActive() bool {
|
||||||
|
// Check flag and env variable.
|
||||||
|
if staging || os.Getenv("PORTMASTER_STAGING") == "enabled" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if staging index is present and acessible.
|
||||||
|
_, err := os.Stat(filepath.Join(registry.StorageDir().Path, "staging.json"))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,14 +3,16 @@
|
||||||
baseDir="$( cd "$(dirname "$0")" && pwd )"
|
baseDir="$( cd "$(dirname "$0")" && pwd )"
|
||||||
cd "$baseDir"
|
cd "$baseDir"
|
||||||
|
|
||||||
COL_OFF="\033[00m"
|
COL_OFF="\033[0m"
|
||||||
COL_BOLD="\033[01;01m"
|
COL_BOLD="\033[01;01m"
|
||||||
COL_RED="\033[31m"
|
COL_RED="\033[31m"
|
||||||
|
COL_GREEN="\033[32m"
|
||||||
|
COL_YELLOW="\033[33m"
|
||||||
|
|
||||||
destDirPart1="../../dist"
|
destDirPart1="../../dist"
|
||||||
destDirPart2="start"
|
destDirPart2="start"
|
||||||
|
|
||||||
function check {
|
function prep {
|
||||||
# output
|
# output
|
||||||
output="portmaster-start"
|
output="portmaster-start"
|
||||||
# get version
|
# get version
|
||||||
|
@ -25,46 +27,47 @@ function check {
|
||||||
fi
|
fi
|
||||||
# build destination path
|
# build destination path
|
||||||
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
|
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
|
||||||
|
}
|
||||||
|
|
||||||
|
function check {
|
||||||
|
prep
|
||||||
|
|
||||||
# check if file exists
|
# check if file exists
|
||||||
if [[ -f $destPath ]]; then
|
if [[ -f $destPath ]]; then
|
||||||
echo "[start] $platform $version already built"
|
echo "[start] $platform $version already built"
|
||||||
else
|
else
|
||||||
echo -e "${COL_BOLD}[start] $platform $version${COL_OFF}"
|
echo -e "${COL_BOLD}[start] $platform v$version${COL_OFF}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function build {
|
function build {
|
||||||
# output
|
prep
|
||||||
output="portmaster-start"
|
|
||||||
# get version
|
|
||||||
version=$(grep "info.Set" main.go | cut -d'"' -f4)
|
|
||||||
# build versioned file name
|
|
||||||
filename="portmaster-start_v${version//./-}"
|
|
||||||
# platform
|
|
||||||
platform="${GOOS}_${GOARCH}"
|
|
||||||
if [[ $GOOS == "windows" ]]; then
|
|
||||||
filename="${filename}.exe"
|
|
||||||
output="${output}.exe"
|
|
||||||
fi
|
|
||||||
# build destination path
|
|
||||||
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
|
|
||||||
|
|
||||||
# check if file exists
|
# check if file exists
|
||||||
if [[ -f $destPath ]]; then
|
if [[ -f $destPath ]]; then
|
||||||
echo "[start] $platform already built in version $version, skipping..."
|
echo "[start] $platform already built in v$version, skipping..."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# build
|
# build
|
||||||
./build
|
./build
|
||||||
if [[ $? -ne 0 ]]; then
|
if [[ $? -ne 0 ]]; then
|
||||||
echo -e "\n${COL_BOLD}[start] $platform: ${COL_RED}BUILD FAILED.${COL_OFF}"
|
echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p $(dirname $destPath)
|
mkdir -p $(dirname $destPath)
|
||||||
cp $output $destPath
|
cp $output $destPath
|
||||||
echo -e "\n${COL_BOLD}[start] $platform: successfully built.${COL_OFF}"
|
echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset {
|
||||||
|
prep
|
||||||
|
|
||||||
|
# delete if file exists
|
||||||
|
if [[ -f $destPath ]]; then
|
||||||
|
rm $destPath
|
||||||
|
echo "[start] $platform v$version deleted."
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_all {
|
function check_all {
|
||||||
|
@ -79,6 +82,12 @@ function build_all {
|
||||||
GOOS=darwin GOARCH=amd64 build
|
GOOS=darwin GOARCH=amd64 build
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reset_all {
|
||||||
|
GOOS=linux GOARCH=amd64 reset
|
||||||
|
GOOS=windows GOARCH=amd64 reset
|
||||||
|
GOOS=darwin GOARCH=amd64 reset
|
||||||
|
}
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
"check" )
|
"check" )
|
||||||
check_all
|
check_all
|
||||||
|
@ -86,6 +95,9 @@ case $1 in
|
||||||
"build" )
|
"build" )
|
||||||
build_all
|
build_all
|
||||||
;;
|
;;
|
||||||
|
"reset" )
|
||||||
|
reset_all
|
||||||
|
;;
|
||||||
* )
|
* )
|
||||||
echo ""
|
echo ""
|
||||||
echo "build list:"
|
echo "build list:"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -19,6 +20,9 @@ const (
|
||||||
// RestartExitCode is the exit code that any service started by portmaster-start
|
// RestartExitCode is the exit code that any service started by portmaster-start
|
||||||
// can return in order to trigger a restart after a clean shutdown.
|
// can return in order to trigger a restart after a clean shutdown.
|
||||||
RestartExitCode = 23
|
RestartExitCode = 23
|
||||||
|
|
||||||
|
exeSuffix = ".exe"
|
||||||
|
zipSuffix = ".zip"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -49,7 +53,7 @@ func init() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Portmaster App",
|
Name: "Portmaster App",
|
||||||
Identifier: "app/portmaster-app",
|
Identifier: "app/portmaster-app.zip",
|
||||||
AllowDownload: false,
|
AllowDownload: false,
|
||||||
AllowHidingWindow: false,
|
AllowHidingWindow: false,
|
||||||
},
|
},
|
||||||
|
@ -62,7 +66,6 @@ func init() {
|
||||||
{
|
{
|
||||||
Name: "Safing Privacy Network",
|
Name: "Safing Privacy Network",
|
||||||
Identifier: "hub/spn-hub",
|
Identifier: "hub/spn-hub",
|
||||||
ShortIdentifier: "hub",
|
|
||||||
AllowDownload: true,
|
AllowDownload: true,
|
||||||
AllowHidingWindow: true,
|
AllowHidingWindow: true,
|
||||||
},
|
},
|
||||||
|
@ -147,8 +150,8 @@ func run(opts *Options, cmdArgs []string) (err error) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// adapt identifier
|
// adapt identifier
|
||||||
if onWindows {
|
if onWindows && !strings.HasSuffix(opts.Identifier, zipSuffix) {
|
||||||
opts.Identifier += ".exe"
|
opts.Identifier += exeSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup logging
|
// setup logging
|
||||||
|
@ -275,16 +278,30 @@ func execute(opts *Options, args []string) (cont bool, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, fmt.Errorf("could not get component: %w", err)
|
return true, fmt.Errorf("could not get component: %w", err)
|
||||||
}
|
}
|
||||||
|
binPath := file.Path()
|
||||||
|
|
||||||
|
// Adapt path for packaged software.
|
||||||
|
if strings.HasSuffix(binPath, zipSuffix) {
|
||||||
|
// Remove suffix from binary path.
|
||||||
|
binPath = strings.TrimSuffix(binPath, zipSuffix)
|
||||||
|
// Add binary with the same name to access the unpacked binary.
|
||||||
|
binPath = filepath.Join(binPath, filepath.Base(binPath))
|
||||||
|
|
||||||
|
// Adapt binary path on Windows.
|
||||||
|
if onWindows {
|
||||||
|
binPath += exeSuffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check permission
|
// check permission
|
||||||
if err := fixExecPerm(file.Path()); err != nil {
|
if err := fixExecPerm(binPath); err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("starting %s %s\n", file.Path(), strings.Join(args, " "))
|
log.Printf("starting %s %s\n", binPath, strings.Join(args, " "))
|
||||||
|
|
||||||
// create command
|
// create command
|
||||||
exc := exec.Command(file.Path(), args...) //nolint:gosec // everything is okay
|
exc := exec.Command(binPath, args...) //nolint:gosec // everything is okay
|
||||||
|
|
||||||
if !runningInConsole && opts.AllowHidingWindow {
|
if !runningInConsole && opts.AllowHidingWindow {
|
||||||
// Windows only:
|
// Windows only:
|
||||||
|
|
|
@ -24,6 +24,7 @@ var (
|
||||||
RunE: runAndLogControlError(func(cmd *cobra.Command, args []string) error {
|
RunE: runAndLogControlError(func(cmd *cobra.Command, args []string) error {
|
||||||
return runService(cmd, &Options{
|
return runService(cmd, &Options{
|
||||||
Identifier: "core/portmaster-core",
|
Identifier: "core/portmaster-core",
|
||||||
|
ShortIdentifier: "core",
|
||||||
AllowDownload: true,
|
AllowDownload: true,
|
||||||
AllowHidingWindow: false,
|
AllowHidingWindow: false,
|
||||||
NoOutput: true,
|
NoOutput: true,
|
||||||
|
|
|
@ -15,8 +15,8 @@ func init() {
|
||||||
var showCmd = &cobra.Command{
|
var showCmd = &cobra.Command{
|
||||||
Use: "show",
|
Use: "show",
|
||||||
PersistentPreRunE: func(*cobra.Command, []string) error {
|
PersistentPreRunE: func(*cobra.Command, []string) error {
|
||||||
// all show sub-commands need the data-root but no logging.
|
// All show sub-commands need the registry but no logging.
|
||||||
return configureDataRoot(false)
|
return configureRegistry(false)
|
||||||
},
|
},
|
||||||
Short: "Show the command to run a Portmaster component yourself",
|
Short: "Show the command to run a Portmaster component yourself",
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func show(opts *Options, cmdArgs []string) error {
|
||||||
|
|
||||||
// adapt identifier
|
// adapt identifier
|
||||||
if onWindows {
|
if onWindows {
|
||||||
opts.Identifier += ".exe"
|
opts.Identifier += exeSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := registry.GetFile(platform(opts.Identifier))
|
file, err := registry.GetFile(platform(opts.Identifier))
|
||||||
|
|
|
@ -3,22 +3,49 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var reset bool
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(updatesCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
rootCmd.AddCommand(purgeCmd)
|
||||||
|
|
||||||
|
flags := updateCmd.Flags()
|
||||||
|
flags.BoolVar(&reset, "reset", false, "Delete all resources and re-download the basic set")
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatesCmd = &cobra.Command{
|
var (
|
||||||
Use: "update",
|
updateCmd = &cobra.Command{
|
||||||
Short: "Run a manual update process",
|
Use: "update",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
Short: "Run a manual update process",
|
||||||
return downloadUpdates()
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
},
|
return downloadUpdates()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
purgeCmd = &cobra.Command{
|
||||||
|
Use: "purge",
|
||||||
|
Short: "Remove old resource versions that are superseded by at least three versions",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return purge()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func indexRequired(cmd *cobra.Command) bool {
|
||||||
|
switch cmd {
|
||||||
|
case updateCmd,
|
||||||
|
purgeCmd:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadUpdates() error {
|
func downloadUpdates() error {
|
||||||
|
@ -26,8 +53,9 @@ func downloadUpdates() error {
|
||||||
if onWindows {
|
if onWindows {
|
||||||
registry.MandatoryUpdates = []string{
|
registry.MandatoryUpdates = []string{
|
||||||
platform("core/portmaster-core.exe"),
|
platform("core/portmaster-core.exe"),
|
||||||
|
platform("kext/portmaster-kext.dll"),
|
||||||
|
platform("kext/portmaster-kext.sys"),
|
||||||
platform("start/portmaster-start.exe"),
|
platform("start/portmaster-start.exe"),
|
||||||
platform("app/portmaster-app.exe"),
|
|
||||||
platform("notifier/portmaster-notifier.exe"),
|
platform("notifier/portmaster-notifier.exe"),
|
||||||
platform("notifier/portmaster-snoretoast.exe"),
|
platform("notifier/portmaster-snoretoast.exe"),
|
||||||
}
|
}
|
||||||
|
@ -35,7 +63,6 @@ func downloadUpdates() error {
|
||||||
registry.MandatoryUpdates = []string{
|
registry.MandatoryUpdates = []string{
|
||||||
platform("core/portmaster-core"),
|
platform("core/portmaster-core"),
|
||||||
platform("start/portmaster-start"),
|
platform("start/portmaster-start"),
|
||||||
platform("app/portmaster-app"),
|
|
||||||
platform("notifier/portmaster-notifier"),
|
platform("notifier/portmaster-notifier"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,10 +70,64 @@ func downloadUpdates() error {
|
||||||
// add updates that we require on all platforms.
|
// add updates that we require on all platforms.
|
||||||
registry.MandatoryUpdates = append(
|
registry.MandatoryUpdates = append(
|
||||||
registry.MandatoryUpdates,
|
registry.MandatoryUpdates,
|
||||||
"all/ui/modules/base.zip",
|
platform("app/portmaster-app.zip"),
|
||||||
|
"all/ui/modules/portmaster.zip",
|
||||||
)
|
)
|
||||||
|
|
||||||
log.SetLogLevel(log.InfoLevel)
|
// Add assets that need unpacking.
|
||||||
|
registry.AutoUnpack = []string{
|
||||||
|
platform("app/portmaster-app.zip"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// logging is configured as a persistent pre-run method inherited from
|
||||||
|
// the root command but since we don't use run.Run() we need to start
|
||||||
|
// logging ourself.
|
||||||
|
log.SetLogLevel(log.TraceLevel)
|
||||||
|
err := log.Start()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to start logging: %s\n", err)
|
||||||
|
}
|
||||||
|
defer log.Shutdown()
|
||||||
|
|
||||||
|
if reset {
|
||||||
|
// Delete storage.
|
||||||
|
err = os.RemoveAll(registry.StorageDir().Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to reset update dir: %s", err)
|
||||||
|
}
|
||||||
|
err = registry.StorageDir().Ensure()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create update dir: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset registry state.
|
||||||
|
registry.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all indexes.
|
||||||
|
err = registry.UpdateIndexes(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download all required updates.
|
||||||
|
err = registry.DownloadUpdates(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select versions and unpack the selected.
|
||||||
|
registry.SelectVersions()
|
||||||
|
err = registry.UnpackResources()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unpack resources: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func purge() error {
|
||||||
|
log.SetLogLevel(log.TraceLevel)
|
||||||
|
|
||||||
// logging is configured as a persistent pre-run method inherited from
|
// logging is configured as a persistent pre-run method inherited from
|
||||||
// the root command but since we don't use run.Run() we need to start
|
// the root command but since we don't use run.Run() we need to start
|
||||||
|
@ -57,7 +138,8 @@ func downloadUpdates() error {
|
||||||
}
|
}
|
||||||
defer log.Shutdown()
|
defer log.Shutdown()
|
||||||
|
|
||||||
return registry.DownloadUpdates(context.TODO())
|
registry.Purge(3)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func platform(identifier string) string {
|
func platform(identifier string) string {
|
||||||
|
|
|
@ -20,9 +20,9 @@ var versionCmd = &cobra.Command{
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
PersistentPreRunE: func(*cobra.Command, []string) error {
|
PersistentPreRunE: func(*cobra.Command, []string) error {
|
||||||
if showAllVersions {
|
if showAllVersions {
|
||||||
// if we are going to show all component versions
|
// If we are going to show all component versions,
|
||||||
// we need the dataroot to be configured.
|
// we need the registry to be configured.
|
||||||
if err := configureDataRoot(false); err != nil {
|
if err := configureRegistry(false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
cmds/updatemgr/.gitignore
vendored
Normal file
2
cmds/updatemgr/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
updatemgr
|
||||||
|
updatemgr.exe
|
20
cmds/updatemgr/confirm.go
Normal file
20
cmds/updatemgr/confirm.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func confirm(msg string) bool {
|
||||||
|
fmt.Printf("%s: [y|n] ", msg)
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
ok := scanner.Scan()
|
||||||
|
if ok && strings.TrimSpace(scanner.Text()) == "y" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import (
|
||||||
var registry *updater.ResourceRegistry
|
var registry *updater.ResourceRegistry
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "uptool",
|
Use: "updatemgr",
|
||||||
Short: "A simple tool to assist in the update and release process",
|
Short: "A simple tool to assist in the update and release process",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
58
cmds/updatemgr/purge.go
Normal file
58
cmds/updatemgr/purge.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(purgeCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var purgeCmd = &cobra.Command{
|
||||||
|
Use: "purge",
|
||||||
|
Short: "Remove old resource versions that are superseded by at least three versions",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: purge,
|
||||||
|
}
|
||||||
|
|
||||||
|
func purge(cmd *cobra.Command, args []string) error {
|
||||||
|
log.SetLogLevel(log.TraceLevel)
|
||||||
|
err := log.Start()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to start logging: %s\n", err)
|
||||||
|
}
|
||||||
|
defer log.Shutdown()
|
||||||
|
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "stable.json",
|
||||||
|
Stable: true,
|
||||||
|
Beta: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "beta.json",
|
||||||
|
Stable: false,
|
||||||
|
Beta: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
err = registry.LoadIndexes(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scanStorage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.SelectVersions()
|
||||||
|
registry.Purge(3)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
122
cmds/updatemgr/staging.go
Normal file
122
cmds/updatemgr/staging.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/updater"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
stageReset bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(stageCmd)
|
||||||
|
stageCmd.Flags().BoolVar(&stageReset, "reset", false, "Reset staging assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
var stageCmd = &cobra.Command{
|
||||||
|
Use: "stage",
|
||||||
|
Short: "Stage scans the specified directory and loads the indexes - it then creates a staging index with all files newer than the stable and beta indexes",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: stage,
|
||||||
|
}
|
||||||
|
|
||||||
|
func stage(cmd *cobra.Command, args []string) error {
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "stable.json",
|
||||||
|
Stable: true,
|
||||||
|
Beta: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "beta.json",
|
||||||
|
Stable: false,
|
||||||
|
Beta: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := registry.LoadIndexes(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scanStorage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we want to reset staging instead.
|
||||||
|
if stageReset {
|
||||||
|
for _, stagedPath := range exportStaging(true) {
|
||||||
|
err = os.Remove(stagedPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export all staged versions and format them.
|
||||||
|
stagingData, err := json.MarshalIndent(exportStaging(false), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build destination path.
|
||||||
|
stagingIndexFilePath := filepath.Join(registry.StorageDir().Path, "staging.json")
|
||||||
|
|
||||||
|
// Print preview.
|
||||||
|
fmt.Printf("staging (%s):\n", stagingIndexFilePath)
|
||||||
|
fmt.Println(string(stagingData))
|
||||||
|
|
||||||
|
// Ask for confirmation.
|
||||||
|
if !confirm("\nDo you want to write this index?") {
|
||||||
|
fmt.Println("aborted...")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write new index to disk.
|
||||||
|
err = ioutil.WriteFile(stagingIndexFilePath, stagingData, 0o644) //nolint:gosec // 0644 is intended
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("written %s\n", stagingIndexFilePath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportStaging(storagePath bool) map[string]string {
|
||||||
|
// Sort all versions.
|
||||||
|
registry.SetBeta(false)
|
||||||
|
registry.SelectVersions()
|
||||||
|
export := registry.Export()
|
||||||
|
|
||||||
|
// Go through all versions and save the highest version, if not stable or beta.
|
||||||
|
versions := make(map[string]string)
|
||||||
|
for _, rv := range export {
|
||||||
|
// Get highest version.
|
||||||
|
v := rv.Versions[0]
|
||||||
|
|
||||||
|
// Do not take stable or beta releases into account.
|
||||||
|
if v.StableRelease || v.BetaRelease {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add highest version to staging
|
||||||
|
if storagePath {
|
||||||
|
rv.SelectedVersion = v
|
||||||
|
versions[rv.Identifier] = rv.GetFile().Path()
|
||||||
|
} else {
|
||||||
|
versions[rv.Identifier] = v.VersionNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions
|
||||||
|
}
|
77
cmds/updatemgr/update.go
Normal file
77
cmds/updatemgr/update.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateCmd = &cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Short: "Update scans the specified directory and registry the index and symlink structure",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: update,
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(cmd *cobra.Command, args []string) error {
|
||||||
|
err := scanStorage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export versions.
|
||||||
|
betaData, err := json.MarshalIndent(exportSelected(true), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stableData, err := json.MarshalIndent(exportSelected(false), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build destination paths.
|
||||||
|
betaIndexFilePath := filepath.Join(registry.StorageDir().Path, "beta.json")
|
||||||
|
stableIndexFilePath := filepath.Join(registry.StorageDir().Path, "stable.json")
|
||||||
|
|
||||||
|
// Print previews.
|
||||||
|
fmt.Printf("beta (%s):\n", betaIndexFilePath)
|
||||||
|
fmt.Println(string(betaData))
|
||||||
|
fmt.Printf("\nstable: (%s)\n", stableIndexFilePath)
|
||||||
|
fmt.Println(string(stableData))
|
||||||
|
|
||||||
|
// Ask for confirmation.
|
||||||
|
if !confirm("\nDo you want to write these new indexes (and update latest symlinks)?") {
|
||||||
|
fmt.Println("aborted...")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write indexes.
|
||||||
|
err = ioutil.WriteFile(betaIndexFilePath, betaData, 0o644) //nolint:gosec // 0644 is intended
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("written %s\n", betaIndexFilePath)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(stableIndexFilePath, stableData, 0o644) //nolint:gosec // 0644 is intended
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("written %s\n", stableIndexFilePath)
|
||||||
|
|
||||||
|
// Create symlinks to latest stable versions.
|
||||||
|
symlinksDir := registry.StorageDir().ChildDir("latest", 0o755)
|
||||||
|
err = registry.CreateSymlinks(symlinksDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("updated stable symlinks in %s\n", symlinksDir.Path)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
1
cmds/uptool/.gitignore
vendored
1
cmds/uptool/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
uptool
|
|
|
@ -1,64 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateCmd = &cobra.Command{
|
|
||||||
Use: "update",
|
|
||||||
Short: "Update scans the specified directory and registry the index and symlink structure",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: update,
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(cmd *cobra.Command, args []string) error {
|
|
||||||
err := scanStorage()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// export beta
|
|
||||||
data, err := json.MarshalIndent(exportSelected(true), "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// print
|
|
||||||
fmt.Println("beta:")
|
|
||||||
fmt.Println(string(data))
|
|
||||||
// write index
|
|
||||||
err = ioutil.WriteFile(filepath.Join(registry.StorageDir().Dir, "beta.json"), data, 0o644) //nolint:gosec // 0644 is intended
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// export stable
|
|
||||||
data, err = json.MarshalIndent(exportSelected(false), "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// print
|
|
||||||
fmt.Println("\nstable:")
|
|
||||||
fmt.Println(string(data))
|
|
||||||
// write index
|
|
||||||
err = ioutil.WriteFile(filepath.Join(registry.StorageDir().Dir, "stable.json"), data, 0o644) //nolint:gosec // 0644 is intended
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// create symlinks
|
|
||||||
err = registry.CreateSymlinks(registry.StorageDir().ChildDir("latest", 0o755))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println("\nstable symlinks created")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -9,8 +9,6 @@ import (
|
||||||
"github.com/safing/portbase/dataroot"
|
"github.com/safing/portbase/dataroot"
|
||||||
"github.com/safing/portbase/info"
|
"github.com/safing/portbase/info"
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
"github.com/safing/portbase/modules/subsystems"
|
|
||||||
"github.com/safing/portbase/notifications"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default Values (changeable for testing)
|
// Default Values (changeable for testing)
|
||||||
|
@ -67,11 +65,5 @@ func globalPrep() error {
|
||||||
// set api listen address
|
// set api listen address
|
||||||
api.SetDefaultAPIListenAddress(DefaultAPIListenAddress)
|
api.SetDefaultAPIListenAddress(DefaultAPIListenAddress)
|
||||||
|
|
||||||
// set notification persistence
|
|
||||||
notifications.SetPersistenceBasePath("core:notifications")
|
|
||||||
|
|
||||||
// set subsystem status dir
|
|
||||||
subsystems.SetDatabaseKeySpace("core:status/subsystems")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,26 +29,32 @@ func registerConfig() error {
|
||||||
err := config.Register(&config.Option{
|
err := config.Register(&config.Option{
|
||||||
Name: "Development Mode",
|
Name: "Development Mode",
|
||||||
Key: CfgDevModeKey,
|
Key: CfgDevModeKey,
|
||||||
Description: "In Development Mode security restrictions are lifted/softened to enable easier access to Portmaster for debugging and testing purposes.",
|
Description: "In Development Mode, security restrictions are lifted/softened to enable easier access to Portmaster for debugging and testing purposes.",
|
||||||
Order: 127,
|
|
||||||
OptType: config.OptTypeBool,
|
OptType: config.OptTypeBool,
|
||||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
DefaultValue: defaultDevMode,
|
DefaultValue: defaultDevMode,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: 512,
|
||||||
|
config.CategoryAnnotation: "Development",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Use System Notifications",
|
Name: "Desktop Notifications",
|
||||||
Key: CfgUseSystemNotificationsKey,
|
Key: CfgUseSystemNotificationsKey,
|
||||||
Description: "Send notifications to your operating system's notification system. When this setting is turned off, notifications will only be visible in the Portmaster App. This affects both alerts from the Portmaster and questions from the Privacy Filter.",
|
Description: "In addition to showing notifications in the Portmaster App, also send them to the Desktop. This requires the Portmaster Notifier to be running.",
|
||||||
Order: 32,
|
|
||||||
OptType: config.OptTypeBool,
|
OptType: config.OptTypeBool,
|
||||||
ExpertiseLevel: config.ExpertiseLevelUser,
|
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
DefaultValue: true, // TODO: turn off by default on unsupported systems
|
DefaultValue: true, // TODO: turn off by default on unsupported systems
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: -15,
|
||||||
|
config.CategoryAnnotation: "User Interface",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -8,6 +8,10 @@ import (
|
||||||
"github.com/safing/portmaster/profile/endpoints"
|
"github.com/safing/portmaster/profile/endpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
resolverFilterLists = []string{"17-DNS"}
|
||||||
|
)
|
||||||
|
|
||||||
// PreventBypassing checks if the connection should be denied or permitted
|
// PreventBypassing checks if the connection should be denied or permitted
|
||||||
// based on some bypass protection checks.
|
// based on some bypass protection checks.
|
||||||
func PreventBypassing(conn *network.Connection) (endpoints.EPResult, string, nsutil.Responder) {
|
func PreventBypassing(conn *network.Connection) (endpoints.EPResult, string, nsutil.Responder) {
|
||||||
|
@ -18,5 +22,11 @@ func PreventBypassing(conn *network.Connection) (endpoints.EPResult, string, nsu
|
||||||
nsutil.NxDomain()
|
nsutil.NxDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conn.Entity.MatchLists(resolverFilterLists) {
|
||||||
|
return endpoints.Denied,
|
||||||
|
"blocked rogue connection to DNS resolver",
|
||||||
|
nsutil.ZeroIP()
|
||||||
|
}
|
||||||
|
|
||||||
return endpoints.NoMatch, "", nil
|
return endpoints.NoMatch, "", nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,14 @@ var (
|
||||||
CfgOptionEnableFilterKey = "filter/enable"
|
CfgOptionEnableFilterKey = "filter/enable"
|
||||||
|
|
||||||
CfgOptionAskWithSystemNotificationsKey = "filter/askWithSystemNotifications"
|
CfgOptionAskWithSystemNotificationsKey = "filter/askWithSystemNotifications"
|
||||||
CfgOptionAskWithSystemNotificationsOrder = 2
|
cfgOptionAskWithSystemNotificationsOrder = 2
|
||||||
|
|
||||||
CfgOptionAskTimeoutKey = "filter/askTimeout"
|
CfgOptionAskTimeoutKey = "filter/askTimeout"
|
||||||
CfgOptionAskTimeoutOrder = 3
|
cfgOptionAskTimeoutOrder = 3
|
||||||
askTimeout config.IntOption
|
askTimeout config.IntOption
|
||||||
|
|
||||||
CfgOptionPermanentVerdictsKey = "filter/permanentVerdicts"
|
CfgOptionPermanentVerdictsKey = "filter/permanentVerdicts"
|
||||||
CfgOptionPermanentVerdictsOrder = 128
|
cfgOptionPermanentVerdictsOrder = 96
|
||||||
permanentVerdicts config.BoolOption
|
permanentVerdicts config.BoolOption
|
||||||
|
|
||||||
devMode config.BoolOption
|
devMode config.BoolOption
|
||||||
|
@ -29,12 +29,15 @@ func registerConfig() error {
|
||||||
err := config.Register(&config.Option{
|
err := config.Register(&config.Option{
|
||||||
Name: "Permanent Verdicts",
|
Name: "Permanent Verdicts",
|
||||||
Key: CfgOptionPermanentVerdictsKey,
|
Key: CfgOptionPermanentVerdictsKey,
|
||||||
Description: "With permanent verdicts, control of a connection is fully handed back to the OS after the initial decision in order to drastically increase performance.",
|
Description: "The Portmaster's system integration intercepts every single packet. Usually the first packet is enough for the Portmaster to set the verdict for a connection - ie. to allow or deny it. Making these verdicts permanent means that the Portmaster will tell the system integration that is does not want to see any more packets of that single connection. This brings a major performance increase.",
|
||||||
Order: CfgOptionPermanentVerdictsOrder,
|
|
||||||
OptType: config.OptTypeBool,
|
OptType: config.OptTypeBool,
|
||||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
ReleaseLevel: config.ReleaseLevelExperimental,
|
ReleaseLevel: config.ReleaseLevelExperimental,
|
||||||
DefaultValue: true,
|
DefaultValue: true,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionPermanentVerdictsOrder,
|
||||||
|
config.CategoryAnnotation: "Advanced",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -42,33 +45,42 @@ func registerConfig() error {
|
||||||
permanentVerdicts = config.Concurrent.GetAsBool(CfgOptionPermanentVerdictsKey, true)
|
permanentVerdicts = config.Concurrent.GetAsBool(CfgOptionPermanentVerdictsKey, true)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Ask with System Notifications",
|
Name: "Prompt Desktop Notifications",
|
||||||
Key: CfgOptionAskWithSystemNotificationsKey,
|
Key: CfgOptionAskWithSystemNotificationsKey,
|
||||||
Description: `Ask about connections using your operating system's notification system. For this to be enabled, the setting "Use System Notifications" must enabled too. This only affects questions from the Privacy Filter, and does not affect alerts from the Portmaster.`,
|
Description: `In addition to showing prompt notifications in the Portmaster App, also send them to the Desktop. This requires the Portmaster Notifier to be running. Requires Desktop Notifications to be enabled.`,
|
||||||
Order: CfgOptionAskWithSystemNotificationsOrder,
|
|
||||||
OptType: config.OptTypeBool,
|
OptType: config.OptTypeBool,
|
||||||
ExpertiseLevel: config.ExpertiseLevelUser,
|
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||||
ReleaseLevel: config.ReleaseLevelExperimental,
|
|
||||||
DefaultValue: true,
|
DefaultValue: true,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionAskWithSystemNotificationsOrder,
|
||||||
|
config.CategoryAnnotation: "General",
|
||||||
|
config.RequiresAnnotation: config.ValueRequirement{
|
||||||
|
Key: core.CfgUseSystemNotificationsKey,
|
||||||
|
Value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Timeout for Ask Notifications",
|
Name: "Prompt Timeout",
|
||||||
Key: CfgOptionAskTimeoutKey,
|
Key: CfgOptionAskTimeoutKey,
|
||||||
Description: "Amount of time (in seconds) how long the Portmaster will wait for a response when prompting about a connection via a notification. Please note that system notifications might not respect this or have it's own limits.",
|
Description: "How long the Portmaster will wait for a reply to a prompt notification. Please note that Desktop Notifications might not respect this or have their own limits.",
|
||||||
Order: CfgOptionAskTimeoutOrder,
|
|
||||||
OptType: config.OptTypeInt,
|
OptType: config.OptTypeInt,
|
||||||
ExpertiseLevel: config.ExpertiseLevelUser,
|
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||||
ReleaseLevel: config.ReleaseLevelExperimental,
|
DefaultValue: 20,
|
||||||
DefaultValue: 60,
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionAskTimeoutOrder,
|
||||||
|
config.UnitAnnotation: "seconds",
|
||||||
|
config.CategoryAnnotation: "General",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
askTimeout = config.Concurrent.GetAsInt(CfgOptionAskTimeoutKey, 60)
|
askTimeout = config.Concurrent.GetAsInt(CfgOptionAskTimeoutKey, 15)
|
||||||
|
|
||||||
devMode = config.Concurrent.GetAsBool(core.CfgDevModeKey, false)
|
devMode = config.Concurrent.GetAsBool(core.CfgDevModeKey, false)
|
||||||
apiListenAddress = config.GetAsString(api.CfgDefaultListenAddressKey, "")
|
apiListenAddress = config.GetAsString(api.CfgDefaultListenAddressKey, "")
|
||||||
|
|
|
@ -17,13 +17,14 @@ import (
|
||||||
"github.com/safing/portmaster/resolver"
|
"github.com/safing/portmaster/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ([]dns.RR, []string, int) {
|
func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ([]dns.RR, []string, int, string) {
|
||||||
goodEntries := make([]dns.RR, 0, len(entries))
|
goodEntries := make([]dns.RR, 0, len(entries))
|
||||||
filteredRecords := make([]string, 0, len(entries))
|
filteredRecords := make([]string, 0, len(entries))
|
||||||
|
|
||||||
// keeps track of the number of valid and allowed
|
// keeps track of the number of valid and allowed
|
||||||
// A and AAAA records.
|
// A and AAAA records.
|
||||||
var allowedAddressRecords int
|
var allowedAddressRecords int
|
||||||
|
var interveningOptionKey string
|
||||||
|
|
||||||
for _, rr := range entries {
|
for _, rr := range entries {
|
||||||
// get IP and classification
|
// get IP and classification
|
||||||
|
@ -45,10 +46,12 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
|
||||||
case classification == netutils.HostLocal:
|
case classification == netutils.HostLocal:
|
||||||
// No DNS should return localhost addresses
|
// No DNS should return localhost addresses
|
||||||
filteredRecords = append(filteredRecords, rr.String())
|
filteredRecords = append(filteredRecords, rr.String())
|
||||||
|
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
|
||||||
continue
|
continue
|
||||||
case scope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
|
case scope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
|
||||||
// No global DNS should return LAN addresses
|
// No global DNS should return LAN addresses
|
||||||
filteredRecords = append(filteredRecords, rr.String())
|
filteredRecords = append(filteredRecords, rr.String())
|
||||||
|
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,12 +61,15 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
|
||||||
switch {
|
switch {
|
||||||
case p.BlockScopeInternet() && classification == netutils.Global:
|
case p.BlockScopeInternet() && classification == netutils.Global:
|
||||||
filteredRecords = append(filteredRecords, rr.String())
|
filteredRecords = append(filteredRecords, rr.String())
|
||||||
|
interveningOptionKey = profile.CfgOptionBlockScopeInternetKey
|
||||||
continue
|
continue
|
||||||
case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
|
case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
|
||||||
filteredRecords = append(filteredRecords, rr.String())
|
filteredRecords = append(filteredRecords, rr.String())
|
||||||
|
interveningOptionKey = profile.CfgOptionBlockScopeLANKey
|
||||||
continue
|
continue
|
||||||
case p.BlockScopeLocal() && classification == netutils.HostLocal:
|
case p.BlockScopeLocal() && classification == netutils.HostLocal:
|
||||||
filteredRecords = append(filteredRecords, rr.String())
|
filteredRecords = append(filteredRecords, rr.String())
|
||||||
|
interveningOptionKey = profile.CfgOptionBlockScopeLocalKey
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +81,7 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
|
||||||
goodEntries = append(goodEntries, rr)
|
goodEntries = append(goodEntries, rr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return goodEntries, filteredRecords, allowedAddressRecords
|
return goodEntries, filteredRecords, allowedAddressRecords, interveningOptionKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *resolver.RRCache {
|
func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *resolver.RRCache {
|
||||||
|
@ -97,18 +103,30 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res
|
||||||
|
|
||||||
var filteredRecords []string
|
var filteredRecords []string
|
||||||
var validIPs int
|
var validIPs int
|
||||||
|
var interveningOptionKey string
|
||||||
|
|
||||||
rrCache.Answer, filteredRecords, validIPs = filterDNSSection(rrCache.Answer, p, rrCache.ServerScope)
|
rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(rrCache.Answer, p, rrCache.ServerScope)
|
||||||
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
|
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
|
||||||
|
|
||||||
// we don't count the valid IPs in the extra section
|
// we don't count the valid IPs in the extra section
|
||||||
rrCache.Extra, filteredRecords, _ = filterDNSSection(rrCache.Extra, p, rrCache.ServerScope)
|
rrCache.Extra, filteredRecords, _, _ = filterDNSSection(rrCache.Extra, p, rrCache.ServerScope)
|
||||||
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
|
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
|
||||||
|
|
||||||
if len(rrCache.FilteredEntries) > 0 {
|
if len(rrCache.FilteredEntries) > 0 {
|
||||||
rrCache.Filtered = true
|
rrCache.Filtered = true
|
||||||
if validIPs == 0 {
|
if validIPs == 0 {
|
||||||
conn.Block("no addresses returned for this domain are permitted")
|
switch interveningOptionKey {
|
||||||
|
case profile.CfgOptionBlockScopeInternetKey:
|
||||||
|
conn.Block("Internet access blocked", interveningOptionKey)
|
||||||
|
case profile.CfgOptionBlockScopeLANKey:
|
||||||
|
conn.Block("LAN access blocked", interveningOptionKey)
|
||||||
|
case profile.CfgOptionBlockScopeLocalKey:
|
||||||
|
conn.Block("Localhost access blocked", interveningOptionKey)
|
||||||
|
case profile.CfgOptionRemoveOutOfScopeDNSKey:
|
||||||
|
conn.Block("DNS global/private split-view violation", interveningOptionKey)
|
||||||
|
default:
|
||||||
|
conn.Block("DNS response only contained to-be-blocked IPs", interveningOptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
// If all entries are filtered, this could mean that these are broken/bogus resource records.
|
// If all entries are filtered, this could mean that these are broken/bogus resource records.
|
||||||
if rrCache.Expired() {
|
if rrCache.Expired() {
|
||||||
|
@ -151,12 +169,6 @@ func DecideOnResolvedDNS(
|
||||||
rrCache *resolver.RRCache,
|
rrCache *resolver.RRCache,
|
||||||
) *resolver.RRCache {
|
) *resolver.RRCache {
|
||||||
|
|
||||||
// check profile
|
|
||||||
if checkProfileExists(ctx, conn, nil) {
|
|
||||||
// returns true if check triggered
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// special grant for connectivity domains
|
// special grant for connectivity domains
|
||||||
if checkConnectivityDomain(ctx, conn, nil) {
|
if checkConnectivityDomain(ctx, conn, nil) {
|
||||||
// returns true if check triggered
|
// returns true if check triggered
|
||||||
|
@ -186,14 +198,14 @@ func mayBlockCNAMEs(ctx context.Context, conn *network.Connection) bool {
|
||||||
|
|
||||||
result, reason := conn.Process().Profile().MatchEndpoint(ctx, conn.Entity)
|
result, reason := conn.Process().Profile().MatchEndpoint(ctx, conn.Entity)
|
||||||
if result == endpoints.Denied {
|
if result == endpoints.Denied {
|
||||||
conn.BlockWithContext(reason.String(), reason.Context())
|
conn.BlockWithContext(reason.String(), profile.CfgOptionFilterCNAMEKey, reason.Context())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if result == endpoints.NoMatch {
|
if result == endpoints.NoMatch {
|
||||||
result, reason = conn.Process().Profile().MatchFilterLists(ctx, conn.Entity)
|
result, reason = conn.Process().Profile().MatchFilterLists(ctx, conn.Entity)
|
||||||
if result == endpoints.Denied {
|
if result == endpoints.Denied {
|
||||||
conn.BlockWithContext(reason.String(), reason.Context())
|
conn.BlockWithContext(reason.String(), profile.CfgOptionFilterCNAMEKey, reason.Context())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,16 @@ func init() {
|
||||||
filterModule,
|
filterModule,
|
||||||
"config:filter/",
|
"config:filter/",
|
||||||
&config.Option{
|
&config.Option{
|
||||||
Name: "Enable Privacy Filter",
|
Name: "Privacy Filter",
|
||||||
Key: CfgOptionEnableFilterKey,
|
Key: CfgOptionEnableFilterKey,
|
||||||
Description: "Enable the Privacy Filter Subsystem to filter DNS queries and network requests.",
|
Description: "Enable the DNS and Network Filter.",
|
||||||
OptType: config.OptTypeBool,
|
OptType: config.OptTypeBool,
|
||||||
ExpertiseLevel: config.ExpertiseLevelUser,
|
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||||
ReleaseLevel: config.ReleaseLevelBeta,
|
ReleaseLevel: config.ReleaseLevelBeta,
|
||||||
DefaultValue: true,
|
DefaultValue: true,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.CategoryAnnotation: "General",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,11 +85,11 @@ func RunInspectors(conn *network.Connection, pkt packet.Packet) (network.Verdict
|
||||||
verdict = network.VerdictDrop
|
verdict = network.VerdictDrop
|
||||||
continueInspection = true
|
continueInspection = true
|
||||||
case BLOCK_CONN:
|
case BLOCK_CONN:
|
||||||
conn.SetVerdict(network.VerdictBlock, "", nil)
|
conn.SetVerdict(network.VerdictBlock, "", "", nil)
|
||||||
verdict = conn.Verdict
|
verdict = conn.Verdict
|
||||||
activeInspectors[key] = true
|
activeInspectors[key] = true
|
||||||
case DROP_CONN:
|
case DROP_CONN:
|
||||||
conn.SetVerdict(network.VerdictDrop, "", nil)
|
conn.SetVerdict(network.VerdictDrop, "", "", nil)
|
||||||
verdict = conn.Verdict
|
verdict = conn.Verdict
|
||||||
activeInspectors[key] = true
|
activeInspectors[key] = true
|
||||||
case STOP_INSPECTING:
|
case STOP_INSPECTING:
|
||||||
|
|
|
@ -2,6 +2,7 @@ package firewall
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
@ -29,6 +30,9 @@ var (
|
||||||
packetsBlocked = new(uint64)
|
packetsBlocked = new(uint64)
|
||||||
packetsDropped = new(uint64)
|
packetsDropped = new(uint64)
|
||||||
packetsFailed = new(uint64)
|
packetsFailed = new(uint64)
|
||||||
|
|
||||||
|
blockedIPv4 = net.IPv4(0, 0, 0, 17)
|
||||||
|
blockedIPv6 = net.ParseIP("::17")
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -84,6 +88,11 @@ func handlePacket(ctx context.Context, pkt packet.Packet) {
|
||||||
func fastTrackedPermit(pkt packet.Packet) (handled bool) {
|
func fastTrackedPermit(pkt packet.Packet) (handled bool) {
|
||||||
meta := pkt.Info()
|
meta := pkt.Info()
|
||||||
|
|
||||||
|
// Check for blocked IP
|
||||||
|
if meta.Dst.Equal(blockedIPv4) || meta.Dst.Equal(blockedIPv6) {
|
||||||
|
_ = pkt.PermanentBlock()
|
||||||
|
}
|
||||||
|
|
||||||
switch meta.Protocol {
|
switch meta.Protocol {
|
||||||
case packet.ICMP:
|
case packet.ICMP:
|
||||||
// Always permit ICMP.
|
// Always permit ICMP.
|
||||||
|
@ -171,7 +180,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
|
||||||
ps := getPortStatusAndMarkUsed(pkt.Info().LocalPort())
|
ps := getPortStatusAndMarkUsed(pkt.Info().LocalPort())
|
||||||
if ps.isMe {
|
if ps.isMe {
|
||||||
// approve
|
// approve
|
||||||
conn.Accept("internally approved")
|
conn.Accept("connection by Portmaster", noReasonOptionKey)
|
||||||
conn.Internal = true
|
conn.Internal = true
|
||||||
// finish
|
// finish
|
||||||
conn.StopFirewallHandler()
|
conn.StopFirewallHandler()
|
||||||
|
@ -191,7 +200,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
|
||||||
// check if filtering is enabled
|
// check if filtering is enabled
|
||||||
if !filterEnabled() {
|
if !filterEnabled() {
|
||||||
conn.Inspecting = false
|
conn.Inspecting = false
|
||||||
conn.SetVerdict(network.VerdictAccept, "privacy filter disabled", nil)
|
conn.Accept("privacy filter disabled", noReasonOptionKey)
|
||||||
conn.StopFirewallHandler()
|
conn.StopFirewallHandler()
|
||||||
issueVerdict(conn, pkt, 0, true)
|
issueVerdict(conn, pkt, 0, true)
|
||||||
return
|
return
|
||||||
|
|
|
@ -70,7 +70,7 @@ func handleWindowsDNSCache() {
|
||||||
|
|
||||||
func notifyDisableDNSCache() {
|
func notifyDisableDNSCache() {
|
||||||
(¬ifications.Notification{
|
(¬ifications.Notification{
|
||||||
ID: "windows-disable-dns-cache",
|
EventID: "interception:windows-disable-dns-cache",
|
||||||
Message: "The Portmaster needs the Windows Service \"DNS Client\" (dnscache) to be disabled for best effectiveness.",
|
Message: "The Portmaster needs the Windows Service \"DNS Client\" (dnscache) to be disabled for best effectiveness.",
|
||||||
Type: notifications.Warning,
|
Type: notifications.Warning,
|
||||||
}).Save()
|
}).Save()
|
||||||
|
@ -78,7 +78,7 @@ func notifyDisableDNSCache() {
|
||||||
|
|
||||||
func notifyRebootRequired() {
|
func notifyRebootRequired() {
|
||||||
(¬ifications.Notification{
|
(¬ifications.Notification{
|
||||||
ID: "windows-dnscache-reboot-required",
|
EventID: "interception:windows-dnscache-reboot-required",
|
||||||
Message: "Please restart your system to complete Portmaster integration.",
|
Message: "Please restart your system to complete Portmaster integration.",
|
||||||
Type: notifications.Warning,
|
Type: notifications.Warning,
|
||||||
}).Save()
|
}).Save()
|
||||||
|
|
|
@ -122,6 +122,12 @@ func (pkt *packet) Accept() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pkt *packet) Block() error {
|
func (pkt *packet) Block() error {
|
||||||
|
if pkt.Info().Protocol == pmpacket.ICMP {
|
||||||
|
// ICMP packets attributed to a blocked connection are always allowed, as
|
||||||
|
// rejection ICMP packets will have the same mark as the blocked
|
||||||
|
// connection. This is why we need to drop blocked ICMP packets instead.
|
||||||
|
return pkt.mark(MarkDrop)
|
||||||
|
}
|
||||||
return pkt.mark(MarkBlock)
|
return pkt.mark(MarkBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +140,12 @@ func (pkt *packet) PermanentAccept() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pkt *packet) PermanentBlock() error {
|
func (pkt *packet) PermanentBlock() error {
|
||||||
|
if pkt.Info().Protocol == pmpacket.ICMP {
|
||||||
|
// ICMP packets attributed to a blocked connection are always allowed, as
|
||||||
|
// rejection ICMP packets will have the same mark as the blocked
|
||||||
|
// connection. This is why we need to drop blocked ICMP packets instead.
|
||||||
|
return pkt.mark(MarkDropAlways)
|
||||||
|
}
|
||||||
return pkt.mark(MarkBlockAlways)
|
return pkt.mark(MarkBlockAlways)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,10 +60,18 @@ func init() {
|
||||||
|
|
||||||
"filter C17 -m mark --mark 0 -j DROP",
|
"filter C17 -m mark --mark 0 -j DROP",
|
||||||
"filter C17 -m mark --mark 1700 -j RETURN",
|
"filter C17 -m mark --mark 1700 -j RETURN",
|
||||||
|
// Accepting ICMP packets with mark 1701 is required for rejecting to work,
|
||||||
|
// as the rejection ICMP packet will have the same mark. Blocked ICMP
|
||||||
|
// packets will always result in a drop within the Portmaster.
|
||||||
|
"filter C17 -m mark --mark 1701 -p icmp -j RETURN",
|
||||||
"filter C17 -m mark --mark 1701 -j REJECT --reject-with icmp-host-prohibited",
|
"filter C17 -m mark --mark 1701 -j REJECT --reject-with icmp-host-prohibited",
|
||||||
"filter C17 -m mark --mark 1702 -j DROP",
|
"filter C17 -m mark --mark 1702 -j DROP",
|
||||||
"filter C17 -j CONNMARK --save-mark",
|
"filter C17 -j CONNMARK --save-mark",
|
||||||
"filter C17 -m mark --mark 1710 -j RETURN",
|
"filter C17 -m mark --mark 1710 -j RETURN",
|
||||||
|
// Accepting ICMP packets with mark 1711 is required for rejecting to work,
|
||||||
|
// as the rejection ICMP packet will have the same mark. Blocked ICMP
|
||||||
|
// packets will always result in a drop within the Portmaster.
|
||||||
|
"filter C17 -m mark --mark 1711 -p icmp -j RETURN",
|
||||||
"filter C17 -m mark --mark 1711 -j REJECT --reject-with icmp-host-prohibited",
|
"filter C17 -m mark --mark 1711 -j REJECT --reject-with icmp-host-prohibited",
|
||||||
"filter C17 -m mark --mark 1712 -j DROP",
|
"filter C17 -m mark --mark 1712 -j DROP",
|
||||||
"filter C17 -m mark --mark 1717 -j RETURN",
|
"filter C17 -m mark --mark 1717 -j RETURN",
|
||||||
|
|
|
@ -36,44 +36,83 @@ import (
|
||||||
// 3. DecideOnConnection
|
// 3. DecideOnConnection
|
||||||
// is called with the first packet of a network connection.
|
// is called with the first packet of a network connection.
|
||||||
|
|
||||||
|
const noReasonOptionKey = ""
|
||||||
|
|
||||||
|
type deciderFn func(context.Context, *network.Connection, packet.Packet) bool
|
||||||
|
|
||||||
|
var deciders = []deciderFn{
|
||||||
|
checkPortmasterConnection,
|
||||||
|
checkSelfCommunication,
|
||||||
|
checkConnectionType,
|
||||||
|
checkConnectionScope,
|
||||||
|
checkEndpointLists,
|
||||||
|
checkConnectivityDomain,
|
||||||
|
checkBypassPrevention,
|
||||||
|
checkFilterLists,
|
||||||
|
dropInbound,
|
||||||
|
checkDomainHeuristics,
|
||||||
|
checkAutoPermitRelated,
|
||||||
|
}
|
||||||
|
|
||||||
// DecideOnConnection makes a decision about a connection.
|
// DecideOnConnection makes a decision about a connection.
|
||||||
// When called, the connection and profile is already locked.
|
// When called, the connection and profile is already locked.
|
||||||
func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packet.Packet) {
|
func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packet.Packet) {
|
||||||
// update profiles and check if communication needs reevaluation
|
// Check if we have a process and profile.
|
||||||
if conn.UpdateAndCheck() {
|
layeredProfile := conn.Process().Profile()
|
||||||
|
if layeredProfile == nil {
|
||||||
|
conn.Deny("unknown process or profile", noReasonOptionKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the layered profile needs updating.
|
||||||
|
if layeredProfile.NeedsUpdate() {
|
||||||
|
// Update revision counter in connection.
|
||||||
|
conn.ProfileRevisionCounter = layeredProfile.Update()
|
||||||
|
conn.SaveWhenFinished()
|
||||||
|
|
||||||
|
// Reset verdict for connection.
|
||||||
log.Tracer(ctx).Infof("filter: re-evaluating verdict on %s", conn)
|
log.Tracer(ctx).Infof("filter: re-evaluating verdict on %s", conn)
|
||||||
conn.Verdict = network.VerdictUndecided
|
conn.Verdict = network.VerdictUndecided
|
||||||
|
|
||||||
|
// Reset entity if it exists.
|
||||||
if conn.Entity != nil {
|
if conn.Entity != nil {
|
||||||
conn.Entity.ResetLists()
|
conn.Entity.ResetLists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var deciders = []func(context.Context, *network.Connection, packet.Packet) bool{
|
// Run all deciders and return if they came to a conclusion.
|
||||||
checkPortmasterConnection,
|
done, defaultAction := runDeciders(ctx, conn, pkt)
|
||||||
checkSelfCommunication,
|
if done {
|
||||||
checkProfileExists,
|
return
|
||||||
checkConnectionType,
|
|
||||||
checkConnectivityDomain,
|
|
||||||
checkConnectionScope,
|
|
||||||
checkEndpointLists,
|
|
||||||
checkBypassPrevention,
|
|
||||||
checkFilterLists,
|
|
||||||
checkInbound,
|
|
||||||
checkDomainHeuristics,
|
|
||||||
checkDefaultPermit,
|
|
||||||
checkAutoPermitRelated,
|
|
||||||
checkDefaultAction,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deciders did not conclude, use default action.
|
||||||
|
switch defaultAction {
|
||||||
|
case profile.DefaultActionPermit:
|
||||||
|
conn.Accept("default permit", profile.CfgOptionDefaultActionKey)
|
||||||
|
case profile.DefaultActionAsk:
|
||||||
|
prompt(ctx, conn, pkt)
|
||||||
|
default:
|
||||||
|
conn.Deny("default block", profile.CfgOptionDefaultActionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDeciders(ctx context.Context, conn *network.Connection, pkt packet.Packet) (done bool, defaultAction uint8) {
|
||||||
|
layeredProfile := conn.Process().Profile()
|
||||||
|
|
||||||
|
// Read-lock the all the profiles.
|
||||||
|
layeredProfile.LockForUsage()
|
||||||
|
defer layeredProfile.UnlockForUsage()
|
||||||
|
|
||||||
|
// Go though all deciders, return if one sets an action.
|
||||||
for _, decider := range deciders {
|
for _, decider := range deciders {
|
||||||
if decider(ctx, conn, pkt) {
|
if decider(ctx, conn, pkt) {
|
||||||
return
|
return true, profile.DefaultActionNotSet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultAction == DefaultActionBlock
|
// Return the default action.
|
||||||
conn.Deny("endpoint is not allowed (default=block)")
|
return false, layeredProfile.DefaultAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPortmasterConnection allows all connection that originate from
|
// checkPortmasterConnection allows all connection that originate from
|
||||||
|
@ -82,7 +121,7 @@ func checkPortmasterConnection(ctx context.Context, conn *network.Connection, pk
|
||||||
// grant self
|
// grant self
|
||||||
if conn.Process().Pid == os.Getpid() {
|
if conn.Process().Pid == os.Getpid() {
|
||||||
log.Tracer(ctx).Infof("filter: granting own connection %s", conn)
|
log.Tracer(ctx).Infof("filter: granting own connection %s", conn)
|
||||||
conn.Verdict = network.VerdictAccept
|
conn.Accept("connection by Portmaster", noReasonOptionKey)
|
||||||
conn.Internal = true
|
conn.Internal = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -115,7 +154,7 @@ func checkSelfCommunication(ctx context.Context, conn *network.Connection, pkt p
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Tracer(ctx).Warningf("filter: failed to find load local peer process with PID %d: %s", otherPid, err)
|
log.Tracer(ctx).Warningf("filter: failed to find load local peer process with PID %d: %s", otherPid, err)
|
||||||
} else if otherProcess.Pid == conn.Process().Pid {
|
} else if otherProcess.Pid == conn.Process().Pid {
|
||||||
conn.Accept("connection to self")
|
conn.Accept("process internal connection", noReasonOptionKey)
|
||||||
conn.Internal = true
|
conn.Internal = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -126,14 +165,6 @@ func checkSelfCommunication(ctx context.Context, conn *network.Connection, pkt p
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkProfileExists(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
|
||||||
if conn.Process().Profile() == nil {
|
|
||||||
conn.Block("unknown process or profile")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkEndpointLists(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
|
func checkEndpointLists(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
|
||||||
var result endpoints.EPResult
|
var result endpoints.EPResult
|
||||||
var reason endpoints.Reason
|
var reason endpoints.Reason
|
||||||
|
@ -142,17 +173,20 @@ func checkEndpointLists(ctx context.Context, conn *network.Connection, _ packet.
|
||||||
p := conn.Process().Profile()
|
p := conn.Process().Profile()
|
||||||
|
|
||||||
// check endpoints list
|
// check endpoints list
|
||||||
|
var optionKey string
|
||||||
if conn.Inbound {
|
if conn.Inbound {
|
||||||
result, reason = p.MatchServiceEndpoint(ctx, conn.Entity)
|
result, reason = p.MatchServiceEndpoint(ctx, conn.Entity)
|
||||||
|
optionKey = profile.CfgOptionServiceEndpointsKey
|
||||||
} else {
|
} else {
|
||||||
result, reason = p.MatchEndpoint(ctx, conn.Entity)
|
result, reason = p.MatchEndpoint(ctx, conn.Entity)
|
||||||
|
optionKey = profile.CfgOptionEndpointsKey
|
||||||
}
|
}
|
||||||
switch result {
|
switch result {
|
||||||
case endpoints.Denied:
|
case endpoints.Denied:
|
||||||
conn.DenyWithContext(reason.String(), reason.Context())
|
conn.DenyWithContext(reason.String(), optionKey, reason.Context())
|
||||||
return true
|
return true
|
||||||
case endpoints.Permitted:
|
case endpoints.Permitted:
|
||||||
conn.AcceptWithContext(reason.String(), reason.Context())
|
conn.AcceptWithContext(reason.String(), optionKey, reason.Context())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,16 +201,16 @@ func checkConnectionType(ctx context.Context, conn *network.Connection, _ packet
|
||||||
case network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid:
|
case network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid:
|
||||||
if p.BlockInbound() {
|
if p.BlockInbound() {
|
||||||
if conn.Scope == network.IncomingHost {
|
if conn.Scope == network.IncomingHost {
|
||||||
conn.Block("inbound connections blocked")
|
conn.Block("inbound connections blocked", profile.CfgOptionBlockInboundKey)
|
||||||
} else {
|
} else {
|
||||||
conn.Drop("inbound connections blocked")
|
conn.Drop("inbound connections blocked", profile.CfgOptionBlockInboundKey)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case network.PeerInternet:
|
case network.PeerInternet:
|
||||||
// BlockP2P only applies to connections to the Internet
|
// BlockP2P only applies to connections to the Internet
|
||||||
if p.BlockP2P() {
|
if p.BlockP2P() {
|
||||||
conn.Block("direct connections (P2P) blocked")
|
conn.Block("direct connections (P2P) blocked", profile.CfgOptionBlockP2PKey)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,7 +236,7 @@ func checkConnectivityDomain(_ context.Context, conn *network.Connection, _ pack
|
||||||
|
|
||||||
case netenv.IsConnectivityDomain(conn.Entity.Domain):
|
case netenv.IsConnectivityDomain(conn.Entity.Domain):
|
||||||
// Special grant!
|
// Special grant!
|
||||||
conn.Accept("special grant for connectivity domain during network bootstrap")
|
conn.Accept("special grant for connectivity domain during network bootstrap", noReasonOptionKey)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -221,29 +255,29 @@ func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.
|
||||||
switch classification {
|
switch classification {
|
||||||
case netutils.Global, netutils.GlobalMulticast:
|
case netutils.Global, netutils.GlobalMulticast:
|
||||||
if p.BlockScopeInternet() {
|
if p.BlockScopeInternet() {
|
||||||
conn.Deny("Internet access blocked") // Block Outbound / Drop Inbound
|
conn.Deny("Internet access blocked", profile.CfgOptionBlockScopeInternetKey) // Block Outbound / Drop Inbound
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast:
|
case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast:
|
||||||
if p.BlockScopeLAN() {
|
if p.BlockScopeLAN() {
|
||||||
conn.Block("LAN access blocked") // Block Outbound / Drop Inbound
|
conn.Block("LAN access blocked", profile.CfgOptionBlockScopeLANKey) // Block Outbound / Drop Inbound
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case netutils.HostLocal:
|
case netutils.HostLocal:
|
||||||
if p.BlockScopeLocal() {
|
if p.BlockScopeLocal() {
|
||||||
conn.Block("Localhost access blocked") // Block Outbound / Drop Inbound
|
conn.Block("Localhost access blocked", profile.CfgOptionBlockScopeLocalKey) // Block Outbound / Drop Inbound
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
default: // netutils.Invalid
|
default: // netutils.Invalid
|
||||||
conn.Deny("invalid IP") // Block Outbound / Drop Inbound
|
conn.Deny("invalid IP", noReasonOptionKey) // Block Outbound / Drop Inbound
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else if conn.Entity.Domain != "" {
|
} else if conn.Entity.Domain != "" {
|
||||||
// DNS Query
|
// This is a DNS Request.
|
||||||
// DNS is expected to resolve to LAN or Internet addresses
|
// DNS is expected to resolve to LAN or Internet addresses.
|
||||||
// TODO: handle domains mapped to localhost
|
// Localhost queries are immediately responded to by the nameserver.
|
||||||
if p.BlockScopeInternet() && p.BlockScopeLAN() {
|
if p.BlockScopeInternet() && p.BlockScopeLAN() {
|
||||||
conn.Block("Internet and LAN access blocked")
|
conn.Block("Internet and LAN access blocked", profile.CfgOptionBlockScopeInternetKey)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,10 +290,10 @@ func checkBypassPrevention(_ context.Context, conn *network.Connection, _ packet
|
||||||
result, reason, reasonCtx := PreventBypassing(conn)
|
result, reason, reasonCtx := PreventBypassing(conn)
|
||||||
switch result {
|
switch result {
|
||||||
case endpoints.Denied:
|
case endpoints.Denied:
|
||||||
conn.BlockWithContext("bypass prevention: "+reason, reasonCtx)
|
conn.BlockWithContext("bypass prevention: "+reason, profile.CfgOptionPreventBypassingKey, reasonCtx)
|
||||||
return true
|
return true
|
||||||
case endpoints.Permitted:
|
case endpoints.Permitted:
|
||||||
conn.AcceptWithContext("bypass prevention: "+reason, reasonCtx)
|
conn.AcceptWithContext("bypass prevention: "+reason, profile.CfgOptionPreventBypassingKey, reasonCtx)
|
||||||
return true
|
return true
|
||||||
case endpoints.NoMatch:
|
case endpoints.NoMatch:
|
||||||
}
|
}
|
||||||
|
@ -274,7 +308,7 @@ func checkFilterLists(ctx context.Context, conn *network.Connection, pkt packet.
|
||||||
result, reason := p.MatchFilterLists(ctx, conn.Entity)
|
result, reason := p.MatchFilterLists(ctx, conn.Entity)
|
||||||
switch result {
|
switch result {
|
||||||
case endpoints.Denied:
|
case endpoints.Denied:
|
||||||
conn.DenyWithContext(reason.String(), reason.Context())
|
conn.DenyWithContext(reason.String(), profile.CfgOptionFilterListsKey, reason.Context())
|
||||||
return true
|
return true
|
||||||
case endpoints.NoMatch:
|
case endpoints.NoMatch:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
|
@ -315,7 +349,7 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ pack
|
||||||
domainToCheck,
|
domainToCheck,
|
||||||
score,
|
score,
|
||||||
)
|
)
|
||||||
conn.Block("possible DGA domain commonly used by malware")
|
conn.Block("possible DGA domain commonly used by malware", profile.CfgOptionDomainHeuristicsKey)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
log.Tracer(ctx).Tracef("filter: LMS score of eTLD+1 %s is %.2f", etld1, score)
|
log.Tracer(ctx).Tracef("filter: LMS score of eTLD+1 %s is %.2f", etld1, score)
|
||||||
|
@ -335,7 +369,7 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ pack
|
||||||
domainToCheck,
|
domainToCheck,
|
||||||
score,
|
score,
|
||||||
)
|
)
|
||||||
conn.Block("possible data tunnel for covert communication and protection bypassing")
|
conn.Block("possible data tunnel for covert communication and protection bypassing", profile.CfgOptionDomainHeuristicsKey)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
log.Tracer(ctx).Tracef("filter: LMS score of entire domain is %.2f", score)
|
log.Tracer(ctx).Tracef("filter: LMS score of entire domain is %.2f", score)
|
||||||
|
@ -344,20 +378,10 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ pack
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkInbound(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
func dropInbound(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
||||||
// implicit default=block for inbound
|
// implicit default=block for inbound
|
||||||
if conn.Inbound {
|
if conn.Inbound {
|
||||||
conn.Drop("endpoint is not allowed (incoming is always default=block)")
|
conn.Drop("incoming connection blocked by default", profile.CfgOptionServiceEndpointsKey)
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkDefaultPermit(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
|
||||||
// check default action
|
|
||||||
p := conn.Process().Profile()
|
|
||||||
if p.DefaultAction() == profile.DefaultActionPermit {
|
|
||||||
conn.Accept("endpoint is not blocked (default=permit)")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -365,22 +389,24 @@ func checkDefaultPermit(_ context.Context, conn *network.Connection, _ packet.Pa
|
||||||
|
|
||||||
func checkAutoPermitRelated(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
func checkAutoPermitRelated(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
||||||
p := conn.Process().Profile()
|
p := conn.Process().Profile()
|
||||||
if !p.DisableAutoPermit() {
|
|
||||||
related, reason := checkRelation(conn)
|
|
||||||
if related {
|
|
||||||
conn.Accept(reason)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkDefaultAction(_ context.Context, conn *network.Connection, pkt packet.Packet) bool {
|
// Auto permit is disabled for default action permit.
|
||||||
p := conn.Process().Profile()
|
if p.DefaultAction() == profile.DefaultActionPermit {
|
||||||
if p.DefaultAction() == profile.DefaultActionAsk {
|
return false
|
||||||
prompt(conn, pkt)
|
}
|
||||||
|
|
||||||
|
// Check if auto permit is disabled.
|
||||||
|
if p.DisableAutoPermit() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for relation to auto permit.
|
||||||
|
related, reason := checkRelation(conn)
|
||||||
|
if related {
|
||||||
|
conn.Accept(reason, profile.CfgOptionDisableAutoPermitKey)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,7 +452,7 @@ matchLoop:
|
||||||
}
|
}
|
||||||
|
|
||||||
if related {
|
if related {
|
||||||
reason = fmt.Sprintf("domain is related to process: %s is related to %s", domainElement, processElement)
|
reason = fmt.Sprintf("auto permitted: domain is related to process: %s is related to %s", domainElement, processElement)
|
||||||
}
|
}
|
||||||
return related, reason
|
return related, reason
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,172 +1,266 @@
|
||||||
package firewall
|
package firewall
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portmaster/profile/endpoints"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/notifications"
|
"github.com/safing/portbase/notifications"
|
||||||
|
"github.com/safing/portmaster/intel"
|
||||||
"github.com/safing/portmaster/network"
|
"github.com/safing/portmaster/network"
|
||||||
"github.com/safing/portmaster/network/packet"
|
"github.com/safing/portmaster/network/packet"
|
||||||
|
"github.com/safing/portmaster/profile"
|
||||||
|
"github.com/safing/portmaster/profile/endpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// notification action IDs
|
// notification action IDs
|
||||||
permitDomainAll = "permit-domain-all"
|
allowDomainAll = "allow-domain-all"
|
||||||
permitDomainDistinct = "permit-domain-distinct"
|
allowDomainDistinct = "allow-domain-distinct"
|
||||||
denyDomainAll = "deny-domain-all"
|
blockDomainAll = "block-domain-all"
|
||||||
denyDomainDistinct = "deny-domain-distinct"
|
blockDomainDistinct = "block-domain-distinct"
|
||||||
|
|
||||||
permitIP = "permit-ip"
|
allowIP = "allow-ip"
|
||||||
denyIP = "deny-ip"
|
blockIP = "block-ip"
|
||||||
permitServingIP = "permit-serving-ip"
|
allowServingIP = "allow-serving-ip"
|
||||||
denyServingIP = "deny-serving-ip"
|
blockServingIP = "block-serving-ip"
|
||||||
|
|
||||||
|
cancelPrompt = "cancel"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // TODO
|
var (
|
||||||
nTTL := time.Duration(askTimeout()) * time.Second
|
promptNotificationCreation sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
type promptData struct {
|
||||||
|
Entity *intel.Entity
|
||||||
|
Profile promptProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptProfile struct {
|
||||||
|
Source string
|
||||||
|
ID string
|
||||||
|
LinkedPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func prompt(ctx context.Context, conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // TODO
|
||||||
|
// Create notification.
|
||||||
|
n := createPrompt(ctx, conn, pkt)
|
||||||
|
|
||||||
|
// wait for response/timeout
|
||||||
|
select {
|
||||||
|
case promptResponse := <-n.Response():
|
||||||
|
switch promptResponse {
|
||||||
|
case allowDomainAll, allowDomainDistinct, allowIP, allowServingIP:
|
||||||
|
conn.Accept("permitted via prompt", profile.CfgOptionEndpointsKey)
|
||||||
|
default: // deny
|
||||||
|
conn.Deny("blocked via prompt", profile.CfgOptionEndpointsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
log.Tracer(ctx).Debugf("filter: continuing prompting async")
|
||||||
|
conn.Deny("prompting in progress, please respond to prompt", profile.CfgOptionDefaultActionKey)
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Tracer(ctx).Debugf("filter: aborting prompting because of shutdown")
|
||||||
|
conn.Drop("shutting down", noReasonOptionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// promptIDPrefix is an identifier for privacy filter prompts. This is also use
|
||||||
|
// in the UI, so don't change!
|
||||||
|
const promptIDPrefix = "filter:prompt"
|
||||||
|
|
||||||
|
func createPrompt(ctx context.Context, conn *network.Connection, pkt packet.Packet) (n *notifications.Notification) {
|
||||||
|
expires := time.Now().Add(time.Duration(askTimeout()) * time.Second).Unix()
|
||||||
|
|
||||||
|
// Get local profile.
|
||||||
|
profile := conn.Process().Profile()
|
||||||
|
if profile == nil {
|
||||||
|
log.Tracer(ctx).Warningf("filter: tried creating prompt for connection without profile")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localProfile := profile.LocalProfile()
|
||||||
|
if localProfile == nil {
|
||||||
|
log.Tracer(ctx).Warningf("filter: tried creating prompt for connection without local profile")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// first check if there is an existing notification for this.
|
// first check if there is an existing notification for this.
|
||||||
// build notification ID
|
// build notification ID
|
||||||
var nID string
|
var nID string
|
||||||
switch {
|
switch {
|
||||||
case conn.Inbound, conn.Entity.Domain == "": // connection to/from IP
|
case conn.Inbound, conn.Entity.Domain == "": // connection to/from IP
|
||||||
nID = fmt.Sprintf("filter:prompt-%d-%s-%s", conn.Process().Pid, conn.Scope, pkt.Info().RemoteIP())
|
nID = fmt.Sprintf(
|
||||||
|
"%s-%s-%s-%s",
|
||||||
|
promptIDPrefix,
|
||||||
|
localProfile.ID,
|
||||||
|
conn.Scope,
|
||||||
|
pkt.Info().RemoteIP(),
|
||||||
|
)
|
||||||
default: // connection to domain
|
default: // connection to domain
|
||||||
nID = fmt.Sprintf("filter:prompt-%d-%s", conn.Process().Pid, conn.Scope)
|
nID = fmt.Sprintf(
|
||||||
|
"%s-%s-%s",
|
||||||
|
promptIDPrefix,
|
||||||
|
localProfile.ID,
|
||||||
|
conn.Scope,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
n := notifications.Get(nID)
|
|
||||||
saveResponse := true
|
|
||||||
|
|
||||||
|
// Only handle one notification at a time.
|
||||||
|
promptNotificationCreation.Lock()
|
||||||
|
defer promptNotificationCreation.Unlock()
|
||||||
|
|
||||||
|
n = notifications.Get(nID)
|
||||||
|
|
||||||
|
// If there already is a notification, just update the expiry.
|
||||||
if n != nil {
|
if n != nil {
|
||||||
// update with new expiry
|
n.Update(expires)
|
||||||
n.Update(time.Now().Add(nTTL).Unix())
|
log.Tracer(ctx).Debugf("filter: updated existing prompt notification")
|
||||||
// do not save response to profile
|
return
|
||||||
saveResponse = false
|
|
||||||
} else {
|
|
||||||
// create new notification
|
|
||||||
n = (¬ifications.Notification{
|
|
||||||
ID: nID,
|
|
||||||
Type: notifications.Prompt,
|
|
||||||
Expires: time.Now().Add(nTTL).Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// add message and actions
|
|
||||||
switch {
|
|
||||||
case conn.Inbound:
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
{
|
|
||||||
ID: permitServingIP,
|
|
||||||
Text: "Permit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: denyServingIP,
|
|
||||||
Text: "Deny",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
case conn.Entity.Domain == "": // direct connection
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
{
|
|
||||||
ID: permitIP,
|
|
||||||
Text: "Permit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: denyIP,
|
|
||||||
Text: "Deny",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
default: // connection to domain
|
|
||||||
if pkt != nil {
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", conn.Process(), conn.Entity.Domain, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
|
|
||||||
} else {
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain)
|
|
||||||
}
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
{
|
|
||||||
ID: permitDomainAll,
|
|
||||||
Text: "Permit all",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: permitDomainDistinct,
|
|
||||||
Text: "Permit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: denyDomainDistinct,
|
|
||||||
Text: "Deny",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// save new notification
|
|
||||||
n.Save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for response/timeout
|
// Reference relevant data for save function
|
||||||
select {
|
entity := conn.Entity
|
||||||
case promptResponse := <-n.Response():
|
// Also needed: localProfile
|
||||||
switch promptResponse {
|
|
||||||
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
|
|
||||||
conn.Accept("permitted by user")
|
|
||||||
default: // deny
|
|
||||||
conn.Deny("denied by user")
|
|
||||||
}
|
|
||||||
|
|
||||||
// end here if we won't save the response to the profile
|
// Create new notification.
|
||||||
if !saveResponse {
|
n = ¬ifications.Notification{
|
||||||
return
|
EventID: nID,
|
||||||
}
|
Type: notifications.Prompt,
|
||||||
|
Title: "Connection Prompt",
|
||||||
// get profile
|
Category: "Privacy Filter",
|
||||||
p := conn.Process().Profile()
|
EventData: &promptData{
|
||||||
|
Entity: entity,
|
||||||
var ep endpoints.Endpoint
|
Profile: promptProfile{
|
||||||
switch promptResponse {
|
Source: string(localProfile.Source),
|
||||||
case permitDomainAll:
|
ID: localProfile.ID,
|
||||||
ep = &endpoints.EndpointDomain{
|
LinkedPath: localProfile.LinkedPath,
|
||||||
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
},
|
||||||
Domain: "." + conn.Entity.Domain,
|
},
|
||||||
}
|
Expires: expires,
|
||||||
case permitDomainDistinct:
|
|
||||||
ep = &endpoints.EndpointDomain{
|
|
||||||
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
|
||||||
Domain: conn.Entity.Domain,
|
|
||||||
}
|
|
||||||
case denyDomainAll:
|
|
||||||
ep = &endpoints.EndpointDomain{
|
|
||||||
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
|
||||||
Domain: "." + conn.Entity.Domain,
|
|
||||||
}
|
|
||||||
case denyDomainDistinct:
|
|
||||||
ep = &endpoints.EndpointDomain{
|
|
||||||
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
|
||||||
Domain: conn.Entity.Domain,
|
|
||||||
}
|
|
||||||
case permitIP, permitServingIP:
|
|
||||||
ep = &endpoints.EndpointIP{
|
|
||||||
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
|
||||||
IP: conn.Entity.IP,
|
|
||||||
}
|
|
||||||
case denyIP, denyServingIP:
|
|
||||||
ep = &endpoints.EndpointIP{
|
|
||||||
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
|
||||||
IP: conn.Entity.IP,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Warningf("filter: unknown prompt response: %s", promptResponse)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch promptResponse {
|
|
||||||
case permitServingIP, denyServingIP:
|
|
||||||
p.AddServiceEndpoint(ep.String())
|
|
||||||
default:
|
|
||||||
p.AddEndpoint(ep.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-n.Expired():
|
|
||||||
conn.Deny("no response to prompt")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set action function.
|
||||||
|
n.SetActionFunction(func(_ context.Context, n *notifications.Notification) error {
|
||||||
|
return saveResponse(
|
||||||
|
localProfile,
|
||||||
|
entity,
|
||||||
|
n.SelectedActionID,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get name of profile for notification. The profile is read-locked by the firewall handler.
|
||||||
|
profileName := localProfile.Name
|
||||||
|
|
||||||
|
// add message and actions
|
||||||
|
switch {
|
||||||
|
case conn.Inbound:
|
||||||
|
n.Message = fmt.Sprintf("%s wants to accept connections from %s (%d/%d)", profileName, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
{
|
||||||
|
ID: allowServingIP,
|
||||||
|
Text: "Allow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: blockServingIP,
|
||||||
|
Text: "Block",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case conn.Entity.Domain == "": // direct connection
|
||||||
|
n.Message = fmt.Sprintf("%s wants to connect to %s (%d/%d)", profileName, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
{
|
||||||
|
ID: allowIP,
|
||||||
|
Text: "Allow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: blockIP,
|
||||||
|
Text: "Block",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default: // connection to domain
|
||||||
|
n.Message = fmt.Sprintf("%s wants to connect to %s", profileName, conn.Entity.Domain)
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
{
|
||||||
|
ID: allowDomainAll,
|
||||||
|
Text: "Allow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: blockDomainAll,
|
||||||
|
Text: "Block",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Save()
|
||||||
|
log.Tracer(ctx).Debugf("filter: sent prompt notification")
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveResponse(p *profile.Profile, entity *intel.Entity, promptResponse string) error {
|
||||||
|
if promptResponse == cancelPrompt {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the profile if necessary.
|
||||||
|
if p.IsOutdated() {
|
||||||
|
var err error
|
||||||
|
p, err = profile.GetProfile(p.Source, p.ID, p.LinkedPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ep endpoints.Endpoint
|
||||||
|
switch promptResponse {
|
||||||
|
case allowDomainAll:
|
||||||
|
ep = &endpoints.EndpointDomain{
|
||||||
|
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
||||||
|
OriginalValue: "." + entity.Domain,
|
||||||
|
}
|
||||||
|
case allowDomainDistinct:
|
||||||
|
ep = &endpoints.EndpointDomain{
|
||||||
|
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
||||||
|
OriginalValue: entity.Domain,
|
||||||
|
}
|
||||||
|
case blockDomainAll:
|
||||||
|
ep = &endpoints.EndpointDomain{
|
||||||
|
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
||||||
|
OriginalValue: "." + entity.Domain,
|
||||||
|
}
|
||||||
|
case blockDomainDistinct:
|
||||||
|
ep = &endpoints.EndpointDomain{
|
||||||
|
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
||||||
|
OriginalValue: entity.Domain,
|
||||||
|
}
|
||||||
|
case allowIP, allowServingIP:
|
||||||
|
ep = &endpoints.EndpointIP{
|
||||||
|
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
||||||
|
IP: entity.IP,
|
||||||
|
}
|
||||||
|
case blockIP, blockServingIP:
|
||||||
|
ep = &endpoints.EndpointIP{
|
||||||
|
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
||||||
|
IP: entity.IP,
|
||||||
|
}
|
||||||
|
case cancelPrompt:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown prompt response: %s", promptResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch promptResponse {
|
||||||
|
case allowServingIP, blockServingIP:
|
||||||
|
p.AddServiceEndpoint(ep.String())
|
||||||
|
log.Infof("filter: added incoming rule to profile %s: %q", p, ep.String())
|
||||||
|
default:
|
||||||
|
p.AddEndpoint(ep.String())
|
||||||
|
log.Infof("filter: added outgoing rule to profile %s: %q", p, ep.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,20 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database"
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
"github.com/safing/portbase/database/record"
|
"github.com/safing/portbase/database/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const resetVersion = "v0.6.0"
|
||||||
|
|
||||||
type cacheVersionRecord struct {
|
type cacheVersionRecord struct {
|
||||||
record.Base
|
record.Base
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
||||||
Version string
|
Version string
|
||||||
|
Reset string
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCacheDatabaseVersion reads and returns the cache
|
// getCacheDatabaseVersion reads and returns the cache
|
||||||
|
@ -37,6 +42,10 @@ func getCacheDatabaseVersion() (*version.Version, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if verRecord.Reset != resetVersion {
|
||||||
|
return nil, database.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
ver, err := version.NewSemver(verRecord.Version)
|
ver, err := version.NewSemver(verRecord.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -50,6 +59,7 @@ func getCacheDatabaseVersion() (*version.Version, error) {
|
||||||
func setCacheDatabaseVersion(ver string) error {
|
func setCacheDatabaseVersion(ver string) error {
|
||||||
verRecord := &cacheVersionRecord{
|
verRecord := &cacheVersionRecord{
|
||||||
Version: ver,
|
Version: ver,
|
||||||
|
Reset: resetVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
verRecord.SetKey(filterListCacheVersionKey)
|
verRecord.SetKey(filterListCacheVersionKey)
|
||||||
|
|
|
@ -200,14 +200,14 @@ func processEntry(ctx context.Context, filter *scopedBloom, entry *listEntry, re
|
||||||
normalizeEntry(entry)
|
normalizeEntry(entry)
|
||||||
|
|
||||||
// Only add the entry to the bloom filter if it has any sources.
|
// Only add the entry to the bloom filter if it has any sources.
|
||||||
if len(entry.Sources) > 0 {
|
if len(entry.Resources) > 0 {
|
||||||
filter.add(entry.Type, entry.Entity)
|
filter.add(entry.Type, entry.Entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &entityRecord{
|
r := &entityRecord{
|
||||||
Value: entry.Entity,
|
Value: entry.Entity,
|
||||||
Type: entry.Type,
|
Type: entry.Type,
|
||||||
Sources: entry.Sources,
|
Sources: entry.getSources(),
|
||||||
UpdatedAt: time.Now().Unix(),
|
UpdatedAt: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,31 @@ import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/safing/portbase/formats/dsd"
|
"github.com/safing/portbase/formats/dsd"
|
||||||
|
"github.com/safing/portbase/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type listEntry struct {
|
type listEntry struct {
|
||||||
Entity string `json:"entity"`
|
Type string `json:"type"`
|
||||||
Sources []string `json:"sources"`
|
Entity string `json:"entity"`
|
||||||
Whitelist bool `json:"whitelist"`
|
Whitelist bool `json:"whitelist"`
|
||||||
Type string `json:"type"`
|
Resources []entryResource `json:"resources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type entryResource struct {
|
||||||
|
SourceID string `json:"sourceID"`
|
||||||
|
ResourceID string `json:"resourceID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *listEntry) getSources() (sourceIDs []string) {
|
||||||
|
sourceIDs = make([]string, 0, len(entry.Resources))
|
||||||
|
|
||||||
|
for _, resource := range entry.Resources {
|
||||||
|
if !utils.StringInSlice(sourceIDs, resource.SourceID) {
|
||||||
|
sourceIDs = append(sourceIDs, resource.SourceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeFile decodes a DSDL filterlists file and sends decoded entities to
|
// decodeFile decodes a DSDL filterlists file and sends decoded entities to
|
||||||
|
|
|
@ -166,6 +166,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
|
||||||
|
|
||||||
// Get connection for this request. This identifies the process behind the request.
|
// Get connection for this request. This identifies the process behind the request.
|
||||||
conn := network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, packet.IPv4, remoteAddr.IP, uint16(remoteAddr.Port))
|
conn := network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, packet.IPv4, remoteAddr.IP, uint16(remoteAddr.Port))
|
||||||
|
conn.Lock()
|
||||||
|
defer conn.Unlock()
|
||||||
|
|
||||||
// Once we decided on the connection we might need to save it to the database,
|
// Once we decided on the connection we might need to save it to the database,
|
||||||
// so we defer that check for now.
|
// so we defer that check for now.
|
||||||
|
@ -195,11 +197,11 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
|
||||||
// A reason for this might be that the request is sink-holed to a forced
|
// A reason for this might be that the request is sink-holed to a forced
|
||||||
// IP address in which case we "accept" it, but let the firewall handle
|
// IP address in which case we "accept" it, but let the firewall handle
|
||||||
// the resolving as it wishes.
|
// the resolving as it wishes.
|
||||||
if responder, ok := conn.ReasonContext.(nsutil.Responder); ok {
|
if responder, ok := conn.Reason.Context.(nsutil.Responder); ok {
|
||||||
// Save the request as open, as we don't know if there will be a connection or not.
|
// Save the request as open, as we don't know if there will be a connection or not.
|
||||||
network.SaveOpenDNSRequest(conn)
|
network.SaveOpenDNSRequest(conn)
|
||||||
|
|
||||||
tracer.Infof("nameserver: handing over request for %s to special filter responder: %s", q.ID(), conn.Reason)
|
tracer.Infof("nameserver: handing over request for %s to special filter responder: %s", q.ID(), conn.Reason.Msg)
|
||||||
return reply(responder)
|
return reply(responder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,11 +243,11 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
|
||||||
rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache)
|
rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache)
|
||||||
if rrCache == nil {
|
if rrCache == nil {
|
||||||
// Check again if there is a responder from the firewall.
|
// Check again if there is a responder from the firewall.
|
||||||
if responder, ok := conn.ReasonContext.(nsutil.Responder); ok {
|
if responder, ok := conn.Reason.Context.(nsutil.Responder); ok {
|
||||||
// Save the request as open, as we don't know if there will be a connection or not.
|
// Save the request as open, as we don't know if there will be a connection or not.
|
||||||
network.SaveOpenDNSRequest(conn)
|
network.SaveOpenDNSRequest(conn)
|
||||||
|
|
||||||
tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason)
|
tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason.Msg)
|
||||||
return reply(responder)
|
return reply(responder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,9 +58,9 @@ func ZeroIP(msgs ...string) ResponderFunc {
|
||||||
|
|
||||||
switch question.Qtype {
|
switch question.Qtype {
|
||||||
case dns.TypeA:
|
case dns.TypeA:
|
||||||
rr, err = dns.NewRR(question.Name + " 0 IN A 0.0.0.0")
|
rr, err = dns.NewRR(question.Name + " 1 IN A 0.0.0.17")
|
||||||
case dns.TypeAAAA:
|
case dns.TypeAAAA:
|
||||||
rr, err = dns.NewRR(question.Name + " 0 IN AAAA ::")
|
rr, err = dns.NewRR(question.Name + " 1 IN AAAA ::17")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -100,9 +100,9 @@ func Localhost(msgs ...string) ResponderFunc {
|
||||||
|
|
||||||
switch question.Qtype {
|
switch question.Qtype {
|
||||||
case dns.TypeA:
|
case dns.TypeA:
|
||||||
rr, err = dns.NewRR("localhost. 0 IN A 127.0.0.1")
|
rr, err = dns.NewRR("localhost. 1 IN A 127.0.0.1")
|
||||||
case dns.TypeAAAA:
|
case dns.TypeAAAA:
|
||||||
rr, err = dns.NewRR("localhost. 0 IN AAAA ::1")
|
rr, err = dns.NewRR("localhost. 1 IN AAAA ::1")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|
|
@ -47,11 +47,16 @@ func checkForConflictingService() error {
|
||||||
// wait for a short duration for the other service to shut down
|
// wait for a short duration for the other service to shut down
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
// notify user
|
notifications.Notify(¬ifications.Notification{
|
||||||
(¬ifications.Notification{
|
EventID: "namserver:stopped-conflicting-service",
|
||||||
ID: "nameserver-stopped-conflicting-service",
|
Type: notifications.Info,
|
||||||
Message: fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid),
|
Title: "Conflicting DNS Service",
|
||||||
}).Save()
|
Category: "Secure DNS",
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"The Portmaster stopped a conflicting name service (pid %d) to gain required system integration.",
|
||||||
|
pid,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
// restart via service-worker logic
|
// restart via service-worker logic
|
||||||
return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, pid)
|
return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, pid)
|
||||||
|
|
|
@ -213,16 +213,6 @@ func setCaptivePortal(portalURL *url.URL) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify
|
|
||||||
cleanUpPortalNotification()
|
|
||||||
defer func() {
|
|
||||||
// TODO: add "open" button
|
|
||||||
captivePortalNotification = notifications.NotifyInfo(
|
|
||||||
"netenv:captive-portal:"+captivePortal.Domain,
|
|
||||||
"Portmaster detected a captive portal at "+captivePortal.Domain,
|
|
||||||
)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// set
|
// set
|
||||||
captivePortal = &CaptivePortal{
|
captivePortal = &CaptivePortal{
|
||||||
URL: portalURL.String(),
|
URL: portalURL.String(),
|
||||||
|
@ -234,6 +224,20 @@ func setCaptivePortal(portalURL *url.URL) {
|
||||||
} else {
|
} else {
|
||||||
captivePortal.Domain = portalURL.Hostname()
|
captivePortal.Domain = portalURL.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// notify
|
||||||
|
cleanUpPortalNotification()
|
||||||
|
captivePortalNotification = notifications.Notify(¬ifications.Notification{
|
||||||
|
EventID: "netenv:captive-portal",
|
||||||
|
Type: notifications.Info,
|
||||||
|
Title: "Captive Portal",
|
||||||
|
Category: "Core",
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"Portmaster detected a captive portal at %s",
|
||||||
|
captivePortal.Domain,
|
||||||
|
),
|
||||||
|
EventData: captivePortal,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanUpPortalNotification() {
|
func cleanUpPortalNotification() {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/safing/portmaster/process"
|
"github.com/safing/portmaster/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
cleanerTickDuration = 5 * time.Second
|
cleanerTickDuration = 5 * time.Second
|
||||||
deleteConnsAfterEndedThreshold = 5 * time.Minute
|
deleteConnsAfterEndedThreshold = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
@ -46,15 +46,8 @@ func cleanConnections() (activePIDs map[int]struct{}) {
|
||||||
nowUnix := now.Unix()
|
nowUnix := now.Unix()
|
||||||
deleteOlderThan := now.Add(-deleteConnsAfterEndedThreshold).Unix()
|
deleteOlderThan := now.Add(-deleteConnsAfterEndedThreshold).Unix()
|
||||||
|
|
||||||
// lock both together because we cannot fully guarantee in which map a connection lands
|
|
||||||
// of course every connection should land in the correct map, but this increases resilience
|
|
||||||
connsLock.Lock()
|
|
||||||
defer connsLock.Unlock()
|
|
||||||
dnsConnsLock.Lock()
|
|
||||||
defer dnsConnsLock.Unlock()
|
|
||||||
|
|
||||||
// network connections
|
// network connections
|
||||||
for _, conn := range conns {
|
for _, conn := range conns.clone() {
|
||||||
conn.Lock()
|
conn.Lock()
|
||||||
|
|
||||||
// delete inactive connections
|
// delete inactive connections
|
||||||
|
@ -70,15 +63,13 @@ func cleanConnections() (activePIDs map[int]struct{}) {
|
||||||
Dst: conn.Entity.IP,
|
Dst: conn.Entity.IP,
|
||||||
DstPort: conn.Entity.Port,
|
DstPort: conn.Entity.Port,
|
||||||
}, now)
|
}, now)
|
||||||
|
|
||||||
activePIDs[conn.process.Pid] = struct{}{}
|
activePIDs[conn.process.Pid] = struct{}{}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
// Step 2: mark end
|
// Step 2: mark end
|
||||||
conn.Ended = nowUnix
|
conn.Ended = nowUnix
|
||||||
if conn.KeyIsSet() {
|
conn.Save()
|
||||||
// Be absolutely sure that we have a key set here, else conn.Save() will deadlock.
|
|
||||||
conn.Save()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case conn.Ended < deleteOlderThan:
|
case conn.Ended < deleteOlderThan:
|
||||||
// Step 3: delete
|
// Step 3: delete
|
||||||
|
@ -90,7 +81,7 @@ func cleanConnections() (activePIDs map[int]struct{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// dns requests
|
// dns requests
|
||||||
for _, conn := range dnsConns {
|
for _, conn := range dnsConns.clone() {
|
||||||
conn.Lock()
|
conn.Lock()
|
||||||
|
|
||||||
// delete old dns connections
|
// delete old dns connections
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -19,49 +18,173 @@ import (
|
||||||
"github.com/safing/portmaster/resolver"
|
"github.com/safing/portmaster/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FirewallHandler defines the function signature for a firewall handle function
|
// FirewallHandler defines the function signature for a firewall
|
||||||
|
// handle function. A firewall handler is responsible for finding
|
||||||
|
// a reasonable verdict for the connection conn. The connection is
|
||||||
|
// locked before the firewall handler is called.
|
||||||
type FirewallHandler func(conn *Connection, pkt packet.Packet)
|
type FirewallHandler func(conn *Connection, pkt packet.Packet)
|
||||||
|
|
||||||
// Connection describes a distinct physical network connection identified by the IP/Port pair.
|
// ProcessContext holds additional information about the process
|
||||||
|
// that iniated a connection.
|
||||||
|
type ProcessContext struct {
|
||||||
|
// ProcessName is the name of the process.
|
||||||
|
ProcessName string
|
||||||
|
//ProfileName is the name of the profile.
|
||||||
|
ProfileName string
|
||||||
|
// BinaryPath is the path to the process binary.
|
||||||
|
BinaryPath string
|
||||||
|
// PID i the process identifier.
|
||||||
|
PID int
|
||||||
|
// Profile is the ID of the main profile that
|
||||||
|
// is applied to the process.
|
||||||
|
Profile string
|
||||||
|
// Source is the source of the profile.
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection describes a distinct physical network connection
|
||||||
|
// identified by the IP/Port pair.
|
||||||
type Connection struct { //nolint:maligned // TODO: fix alignment
|
type Connection struct { //nolint:maligned // TODO: fix alignment
|
||||||
record.Base
|
record.Base
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
||||||
ID string
|
// ID may hold unique connection id. It is only set for non-DNS
|
||||||
Scope string
|
// request connections and is considered immutable after a
|
||||||
|
// connection object has been created.
|
||||||
|
ID string
|
||||||
|
// Scope defines the scope of a connection. For DNS requests, the
|
||||||
|
// scope is always set to the domain name. For direct packet
|
||||||
|
// connections the scope consists of the involved network environment
|
||||||
|
// and the packet direction. Once a connection object is created,
|
||||||
|
// Scope is considered immutable.
|
||||||
|
Scope string
|
||||||
|
// IPVersion is set to the packet IP version. It is not set (0) for
|
||||||
|
// connections created from a DNS request.
|
||||||
IPVersion packet.IPVersion
|
IPVersion packet.IPVersion
|
||||||
Inbound bool
|
// Inbound is set to true if the connection is incoming. Inbound is
|
||||||
|
// only set when a connection object is created and is considered
|
||||||
// local endpoint
|
// immutable afterwards.
|
||||||
|
Inbound bool
|
||||||
|
// IPProtocol is set to the transport protocol used by the connection.
|
||||||
|
// Is is considered immutable once a connection object has been
|
||||||
|
// created. IPProtocol is not set for connections that have been
|
||||||
|
// created from a DNS request.
|
||||||
IPProtocol packet.IPProtocol
|
IPProtocol packet.IPProtocol
|
||||||
LocalIP net.IP
|
// LocalIP holds the local IP address of the connection. It is not
|
||||||
LocalPort uint16
|
// set for connections created from DNS requests. LocalIP is
|
||||||
process *process.Process
|
// considered immutable once a connection object has been created.
|
||||||
|
LocalIP net.IP
|
||||||
// remote endpoint
|
// LocalPort holds the local port of the connection. It is not
|
||||||
|
// set for connections created from DNS requests. LocalPort is
|
||||||
|
// considered immutable once a connection object has been created.
|
||||||
|
LocalPort uint16
|
||||||
|
// Entity describes the remote entity that the connection has been
|
||||||
|
// established to. The entity might be changed or information might
|
||||||
|
// be added to it during the livetime of a connection. Access to
|
||||||
|
// entity must be guarded by the connection lock.
|
||||||
Entity *intel.Entity
|
Entity *intel.Entity
|
||||||
|
// Verdict is the final decision that has been made for a connection.
|
||||||
Verdict Verdict
|
// The verdict may change so any access to it must be guarded by the
|
||||||
Reason string
|
// connection lock.
|
||||||
ReasonContext interface{}
|
Verdict Verdict
|
||||||
ReasonID string // format source[:id[:id]] // TODO
|
// Reason holds information justifying the verdict, as well as additional
|
||||||
|
// information about the reason.
|
||||||
Started int64
|
// Access to Reason must be guarded by the connection lock.
|
||||||
Ended int64
|
Reason Reason
|
||||||
Tunneled bool
|
// Started holds the number of seconds in UNIX epoch time at which
|
||||||
|
// the connection has been initated and first seen by the portmaster.
|
||||||
|
// Staretd is only every set when creating a new connection object
|
||||||
|
// and is considered immutable afterwards.
|
||||||
|
Started int64
|
||||||
|
// Ended is set to the number of seconds in UNIX epoch time at which
|
||||||
|
// the connection is considered terminated. Ended may be set at any
|
||||||
|
// time so access must be guarded by the connection lock.
|
||||||
|
Ended int64
|
||||||
|
// VerdictPermanent is set to true if the final verdict is permanent
|
||||||
|
// and the connection has been (or will be) handed back to the kernel.
|
||||||
|
// VerdictPermanent may be changed together with the Verdict and Reason
|
||||||
|
// properties and must be guarded using the connection lock.
|
||||||
VerdictPermanent bool
|
VerdictPermanent bool
|
||||||
Inspecting bool
|
// Inspecting is set to true if the connection is being inspected
|
||||||
Encrypted bool // TODO
|
// by one or more of the registered inspectors. This property may
|
||||||
Internal bool // Portmaster internal connections are marked in order to easily filter these out in the UI
|
// be changed during the lifetime of a connection and must be guarded
|
||||||
|
// using the connection lock.
|
||||||
pktQueue chan packet.Packet
|
Inspecting bool
|
||||||
|
// Tunneled is currently unused and MUST be ignored.
|
||||||
|
Tunneled bool
|
||||||
|
// Encrypted is currently unused and MUST be ignored.
|
||||||
|
Encrypted bool
|
||||||
|
// ProcessContext holds additional information about the process
|
||||||
|
// that iniated the connection. It is set once when the connection
|
||||||
|
// object is created and is considered immutable afterwards.
|
||||||
|
ProcessContext ProcessContext
|
||||||
|
// Internal is set to true if the connection is attributed as an
|
||||||
|
// Portmaster internal connection. Internal may be set at different
|
||||||
|
// points and access to it must be guarded by the connection lock.
|
||||||
|
Internal bool
|
||||||
|
// process holds a reference to the actor process. That is, the
|
||||||
|
// process instance that initated the connection.
|
||||||
|
process *process.Process
|
||||||
|
// pkgQueue is used to serialize packet handling for a single
|
||||||
|
// connection and is served by the connections packetHandler.
|
||||||
|
pktQueue chan packet.Packet
|
||||||
|
// firewallHandler is the firewall handler that is called for
|
||||||
|
// each packet sent to pktQueue.
|
||||||
firewallHandler FirewallHandler
|
firewallHandler FirewallHandler
|
||||||
|
// saveWhenFinished can be set to drue during the life-time of
|
||||||
|
// a connection and signals the firewallHandler that a Save()
|
||||||
|
// should be issued after processing the connection.
|
||||||
|
saveWhenFinished bool
|
||||||
|
// activeInspectors is a slice of booleans where each entry
|
||||||
|
// maps to the index of an available inspector. If the value
|
||||||
|
// is true the inspector is currently active. False indicates
|
||||||
|
// that the inspector has finished and should be skipped.
|
||||||
activeInspectors []bool
|
activeInspectors []bool
|
||||||
inspectorData map[uint8]interface{}
|
// inspectorData holds additional meta data for the inspectors.
|
||||||
|
// using the inspectors index as a map key.
|
||||||
|
inspectorData map[uint8]interface{}
|
||||||
|
// ProfileRevisionCounter is used to track changes to the process
|
||||||
|
// profile and required for correct re-evaluation of a connections
|
||||||
|
// verdict.
|
||||||
|
ProfileRevisionCounter uint64
|
||||||
|
}
|
||||||
|
|
||||||
saveWhenFinished bool
|
// Reason holds information justifying a verdict, as well as additional
|
||||||
profileRevisionCounter uint64
|
// information about the reason.
|
||||||
|
type Reason struct {
|
||||||
|
// Msg is a human readable description of the reason.
|
||||||
|
Msg string
|
||||||
|
// OptionKey is the configuration option key of the setting that
|
||||||
|
// was responsible for the verdict.
|
||||||
|
OptionKey string
|
||||||
|
// Profile is the database key of the profile that held the setting
|
||||||
|
// that was responsible for the verdict.
|
||||||
|
Profile string
|
||||||
|
// ReasonContext may hold additional reason-specific information and
|
||||||
|
// any access must be guarded by the connection lock.
|
||||||
|
Context interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProcessContext(ctx context.Context, proc *process.Process) ProcessContext {
|
||||||
|
// Gather process information.
|
||||||
|
pCtx := ProcessContext{
|
||||||
|
BinaryPath: proc.Path,
|
||||||
|
ProcessName: proc.Name,
|
||||||
|
PID: proc.Pid,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get local profile.
|
||||||
|
localProfile := proc.Profile().LocalProfile()
|
||||||
|
if localProfile == nil {
|
||||||
|
log.Tracer(ctx).Warningf("network: process %s has no profile", proc)
|
||||||
|
return pCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add profile information and return.
|
||||||
|
pCtx.ProfileName = localProfile.Name
|
||||||
|
pCtx.Profile = localProfile.ID
|
||||||
|
pCtx.Source = string(localProfile.Source)
|
||||||
|
return pCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConnectionFromDNSRequest returns a new connection based on the given dns request.
|
// NewConnectionFromDNSRequest returns a new connection based on the given dns request.
|
||||||
|
@ -91,9 +214,10 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []stri
|
||||||
Domain: fqdn,
|
Domain: fqdn,
|
||||||
CNAME: cnames,
|
CNAME: cnames,
|
||||||
},
|
},
|
||||||
process: proc,
|
process: proc,
|
||||||
Started: timestamp,
|
ProcessContext: getProcessContext(ctx, proc),
|
||||||
Ended: timestamp,
|
Started: timestamp,
|
||||||
|
Ended: timestamp,
|
||||||
}
|
}
|
||||||
return dnsConn
|
return dnsConn
|
||||||
}
|
}
|
||||||
|
@ -120,7 +244,10 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
|
||||||
scope = IncomingLAN
|
scope = IncomingLAN
|
||||||
case netutils.Global, netutils.GlobalMulticast:
|
case netutils.Global, netutils.GlobalMulticast:
|
||||||
scope = IncomingInternet
|
scope = IncomingInternet
|
||||||
default: // netutils.Invalid
|
|
||||||
|
case netutils.Invalid:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
scope = IncomingInvalid
|
scope = IncomingInvalid
|
||||||
}
|
}
|
||||||
entity = &intel.Entity{
|
entity = &intel.Entity{
|
||||||
|
@ -169,7 +296,10 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
|
||||||
scope = PeerLAN
|
scope = PeerLAN
|
||||||
case netutils.Global, netutils.GlobalMulticast:
|
case netutils.Global, netutils.GlobalMulticast:
|
||||||
scope = PeerInternet
|
scope = PeerInternet
|
||||||
default: // netutils.Invalid
|
|
||||||
|
case netutils.Invalid:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
scope = PeerInvalid
|
scope = PeerInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,95 +312,96 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
|
||||||
IPVersion: pkt.Info().Version,
|
IPVersion: pkt.Info().Version,
|
||||||
Inbound: inbound,
|
Inbound: inbound,
|
||||||
// local endpoint
|
// local endpoint
|
||||||
IPProtocol: pkt.Info().Protocol,
|
IPProtocol: pkt.Info().Protocol,
|
||||||
LocalIP: pkt.Info().LocalIP(),
|
LocalIP: pkt.Info().LocalIP(),
|
||||||
LocalPort: pkt.Info().LocalPort(),
|
LocalPort: pkt.Info().LocalPort(),
|
||||||
process: proc,
|
ProcessContext: getProcessContext(pkt.Ctx(), proc),
|
||||||
|
process: proc,
|
||||||
// remote endpoint
|
// remote endpoint
|
||||||
Entity: entity,
|
Entity: entity,
|
||||||
// meta
|
// meta
|
||||||
Started: time.Now().Unix(),
|
Started: time.Now().Unix(),
|
||||||
profileRevisionCounter: proc.Profile().RevisionCnt(),
|
ProfileRevisionCounter: proc.Profile().RevisionCnt(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConnection fetches a Connection from the database.
|
// GetConnection fetches a Connection from the database.
|
||||||
func GetConnection(id string) (*Connection, bool) {
|
func GetConnection(id string) (*Connection, bool) {
|
||||||
connsLock.RLock()
|
return conns.get(id)
|
||||||
defer connsLock.RUnlock()
|
|
||||||
|
|
||||||
conn, ok := conns[id]
|
|
||||||
return conn, ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptWithContext accepts the connection.
|
// AcceptWithContext accepts the connection.
|
||||||
func (conn *Connection) AcceptWithContext(reason string, ctx interface{}) {
|
func (conn *Connection) AcceptWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
||||||
if !conn.SetVerdict(VerdictAccept, reason, ctx) {
|
if !conn.SetVerdict(VerdictAccept, reason, reasonOptionKey, ctx) {
|
||||||
log.Warningf("filter: tried to accept %s, but current verdict is %s", conn, conn.Verdict)
|
log.Warningf("filter: tried to accept %s, but current verdict is %s", conn, conn.Verdict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept is like AcceptWithContext but only accepts a reason.
|
// Accept is like AcceptWithContext but only accepts a reason.
|
||||||
func (conn *Connection) Accept(reason string) {
|
func (conn *Connection) Accept(reason, reasonOptionKey string) {
|
||||||
conn.AcceptWithContext(reason, nil)
|
conn.AcceptWithContext(reason, reasonOptionKey, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockWithContext blocks the connection.
|
// BlockWithContext blocks the connection.
|
||||||
func (conn *Connection) BlockWithContext(reason string, ctx interface{}) {
|
func (conn *Connection) BlockWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
||||||
if !conn.SetVerdict(VerdictBlock, reason, ctx) {
|
if !conn.SetVerdict(VerdictBlock, reason, reasonOptionKey, ctx) {
|
||||||
log.Warningf("filter: tried to block %s, but current verdict is %s", conn, conn.Verdict)
|
log.Warningf("filter: tried to block %s, but current verdict is %s", conn, conn.Verdict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block is like BlockWithContext but does only accepts a reason.
|
// Block is like BlockWithContext but does only accepts a reason.
|
||||||
func (conn *Connection) Block(reason string) {
|
func (conn *Connection) Block(reason, reasonOptionKey string) {
|
||||||
conn.BlockWithContext(reason, nil)
|
conn.BlockWithContext(reason, reasonOptionKey, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DropWithContext drops the connection.
|
// DropWithContext drops the connection.
|
||||||
func (conn *Connection) DropWithContext(reason string, ctx interface{}) {
|
func (conn *Connection) DropWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
||||||
if !conn.SetVerdict(VerdictDrop, reason, ctx) {
|
if !conn.SetVerdict(VerdictDrop, reason, reasonOptionKey, ctx) {
|
||||||
log.Warningf("filter: tried to drop %s, but current verdict is %s", conn, conn.Verdict)
|
log.Warningf("filter: tried to drop %s, but current verdict is %s", conn, conn.Verdict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop is like DropWithContext but does only accepts a reason.
|
// Drop is like DropWithContext but does only accepts a reason.
|
||||||
func (conn *Connection) Drop(reason string) {
|
func (conn *Connection) Drop(reason, reasonOptionKey string) {
|
||||||
conn.DropWithContext(reason, nil)
|
conn.DropWithContext(reason, reasonOptionKey, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DenyWithContext blocks or drops the link depending on the connection direction.
|
// DenyWithContext blocks or drops the link depending on the connection direction.
|
||||||
func (conn *Connection) DenyWithContext(reason string, ctx interface{}) {
|
func (conn *Connection) DenyWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
||||||
if conn.Inbound {
|
if conn.Inbound {
|
||||||
conn.DropWithContext(reason, ctx)
|
conn.DropWithContext(reason, reasonOptionKey, ctx)
|
||||||
} else {
|
} else {
|
||||||
conn.BlockWithContext(reason, ctx)
|
conn.BlockWithContext(reason, reasonOptionKey, ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny is like DenyWithContext but only accepts a reason.
|
// Deny is like DenyWithContext but only accepts a reason.
|
||||||
func (conn *Connection) Deny(reason string) {
|
func (conn *Connection) Deny(reason, reasonOptionKey string) {
|
||||||
conn.DenyWithContext(reason, nil)
|
conn.DenyWithContext(reason, reasonOptionKey, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FailedWithContext marks the connection with VerdictFailed and stores the reason.
|
// FailedWithContext marks the connection with VerdictFailed and stores the reason.
|
||||||
func (conn *Connection) FailedWithContext(reason string, ctx interface{}) {
|
func (conn *Connection) FailedWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
||||||
if !conn.SetVerdict(VerdictFailed, reason, ctx) {
|
if !conn.SetVerdict(VerdictFailed, reason, reasonOptionKey, ctx) {
|
||||||
log.Warningf("filter: tried to drop %s due to error but current verdict is %s", conn, conn.Verdict)
|
log.Warningf("filter: tried to drop %s due to error but current verdict is %s", conn, conn.Verdict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Failed is like FailedWithContext but only accepts a string.
|
// Failed is like FailedWithContext but only accepts a string.
|
||||||
func (conn *Connection) Failed(reason string) {
|
func (conn *Connection) Failed(reason, reasonOptionKey string) {
|
||||||
conn.FailedWithContext(reason, nil)
|
conn.FailedWithContext(reason, reasonOptionKey, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetVerdict sets a new verdict for the connection, making sure it does not interfere with previous verdicts.
|
// SetVerdict sets a new verdict for the connection, making sure it does not interfere with previous verdicts.
|
||||||
func (conn *Connection) SetVerdict(newVerdict Verdict, reason string, reasonCtx interface{}) (ok bool) {
|
func (conn *Connection) SetVerdict(newVerdict Verdict, reason, reasonOptionKey string, reasonCtx interface{}) (ok bool) {
|
||||||
if newVerdict >= conn.Verdict {
|
if newVerdict >= conn.Verdict {
|
||||||
conn.Verdict = newVerdict
|
conn.Verdict = newVerdict
|
||||||
conn.Reason = reason
|
conn.Reason.Msg = reason
|
||||||
conn.ReasonContext = reasonCtx
|
conn.Reason.Context = reasonCtx
|
||||||
|
if reasonOptionKey != "" && conn.Process() != nil {
|
||||||
|
conn.Reason.OptionKey = reasonOptionKey
|
||||||
|
conn.Reason.Profile = conn.Process().Profile().GetProfileSource(conn.Reason.OptionKey)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -286,32 +417,24 @@ func (conn *Connection) SaveWhenFinished() {
|
||||||
conn.saveWhenFinished = true
|
conn.saveWhenFinished = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the connection in the storage and propagates the change through the database system.
|
// Save saves the connection in the storage and propagates the change
|
||||||
|
// through the database system. Save may lock dnsConnsLock or connsLock
|
||||||
|
// in if Save() is called the first time.
|
||||||
|
// Callers must make sure to lock the connection itself before calling
|
||||||
|
// Save().
|
||||||
func (conn *Connection) Save() {
|
func (conn *Connection) Save() {
|
||||||
conn.UpdateMeta()
|
conn.UpdateMeta()
|
||||||
|
|
||||||
if !conn.KeyIsSet() {
|
if !conn.KeyIsSet() {
|
||||||
|
// A connection without an ID has been created from
|
||||||
|
// a DNS request rather than a packet. Choose the correct
|
||||||
|
// connection store here.
|
||||||
if conn.ID == "" {
|
if conn.ID == "" {
|
||||||
// dns request
|
|
||||||
|
|
||||||
// set key
|
|
||||||
conn.SetKey(fmt.Sprintf("network:tree/%d/%s", conn.process.Pid, conn.Scope))
|
conn.SetKey(fmt.Sprintf("network:tree/%d/%s", conn.process.Pid, conn.Scope))
|
||||||
mapKey := strconv.Itoa(conn.process.Pid) + "/" + conn.Scope
|
dnsConns.add(conn)
|
||||||
|
|
||||||
// save
|
|
||||||
dnsConnsLock.Lock()
|
|
||||||
dnsConns[mapKey] = conn
|
|
||||||
dnsConnsLock.Unlock()
|
|
||||||
} else {
|
} else {
|
||||||
// network connection
|
|
||||||
|
|
||||||
// set key
|
|
||||||
conn.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", conn.process.Pid, conn.Scope, conn.ID))
|
conn.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", conn.process.Pid, conn.Scope, conn.ID))
|
||||||
|
conns.add(conn)
|
||||||
// save
|
|
||||||
connsLock.Lock()
|
|
||||||
conns[conn.ID] = conn
|
|
||||||
connsLock.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,34 +442,25 @@ func (conn *Connection) Save() {
|
||||||
dbController.PushUpdate(conn)
|
dbController.PushUpdate(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete deletes a link from the storage and propagates the change. Nothing is locked - both the conns map and the connection itself require locking
|
// delete deletes a link from the storage and propagates the change.
|
||||||
|
// delete may lock either the dnsConnsLock or connsLock. Callers
|
||||||
|
// must still make sure to lock the connection itself.
|
||||||
func (conn *Connection) delete() {
|
func (conn *Connection) delete() {
|
||||||
|
// A connection without an ID has been created from
|
||||||
|
// a DNS request rather than a packet. Choose the correct
|
||||||
|
// connection store here.
|
||||||
if conn.ID == "" {
|
if conn.ID == "" {
|
||||||
delete(dnsConns, strconv.Itoa(conn.process.Pid)+"/"+conn.Scope)
|
dnsConns.delete(conn)
|
||||||
} else {
|
} else {
|
||||||
delete(conns, conn.ID)
|
conns.delete(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Meta().Delete()
|
conn.Meta().Delete()
|
||||||
dbController.PushUpdate(conn)
|
dbController.PushUpdate(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAndCheck updates profiles and checks whether a reevaluation is needed.
|
// SetFirewallHandler sets the firewall handler for this link, and starts a
|
||||||
func (conn *Connection) UpdateAndCheck() (needsReevaluation bool) {
|
// worker to handle the packets.
|
||||||
p := conn.process.Profile()
|
|
||||||
if p == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
revCnt := p.Update()
|
|
||||||
|
|
||||||
if conn.profileRevisionCounter != revCnt {
|
|
||||||
conn.profileRevisionCounter = revCnt
|
|
||||||
needsReevaluation = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFirewallHandler sets the firewall handler for this link, and starts a worker to handle the packets.
|
|
||||||
func (conn *Connection) SetFirewallHandler(handler FirewallHandler) {
|
func (conn *Connection) SetFirewallHandler(handler FirewallHandler) {
|
||||||
if conn.firewallHandler == nil {
|
if conn.firewallHandler == nil {
|
||||||
conn.pktQueue = make(chan packet.Packet, 1000)
|
conn.pktQueue = make(chan packet.Packet, 1000)
|
||||||
|
@ -382,13 +496,13 @@ func (conn *Connection) HandlePacket(pkt packet.Packet) {
|
||||||
|
|
||||||
// packetHandler sequentially handles queued packets
|
// packetHandler sequentially handles queued packets
|
||||||
func (conn *Connection) packetHandler() {
|
func (conn *Connection) packetHandler() {
|
||||||
for {
|
for pkt := range conn.pktQueue {
|
||||||
pkt := <-conn.pktQueue
|
|
||||||
if pkt == nil {
|
if pkt == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// get handler
|
// get handler
|
||||||
conn.Lock()
|
conn.Lock()
|
||||||
|
|
||||||
// execute handler or verdict
|
// execute handler or verdict
|
||||||
if conn.firewallHandler != nil {
|
if conn.firewallHandler != nil {
|
||||||
conn.firewallHandler(conn, pkt)
|
conn.firewallHandler(conn, pkt)
|
||||||
|
@ -396,14 +510,16 @@ func (conn *Connection) packetHandler() {
|
||||||
defaultFirewallHandler(conn, pkt)
|
defaultFirewallHandler(conn, pkt)
|
||||||
}
|
}
|
||||||
// log verdict
|
// log verdict
|
||||||
log.Tracer(pkt.Ctx()).Infof("filter: connection %s %s: %s", conn, conn.Verdict.Verb(), conn.Reason)
|
log.Tracer(pkt.Ctx()).Infof("filter: connection %s %s: %s", conn, conn.Verdict.Verb(), conn.Reason.Msg)
|
||||||
conn.Unlock()
|
|
||||||
// save does not touch any changing data
|
// save does not touch any changing data
|
||||||
// must not be locked, will deadlock with cleaner functions
|
// must not be locked, will deadlock with cleaner functions
|
||||||
if conn.saveWhenFinished {
|
if conn.saveWhenFinished {
|
||||||
conn.saveWhenFinished = false
|
conn.saveWhenFinished = false
|
||||||
conn.Save()
|
conn.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn.Unlock()
|
||||||
// submit trace logs
|
// submit trace logs
|
||||||
log.Tracer(pkt.Ctx()).Submit()
|
log.Tracer(pkt.Ctx()).Submit()
|
||||||
}
|
}
|
||||||
|
|
57
network/connection_store.go
Normal file
57
network/connection_store.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type connectionStore struct {
|
||||||
|
rw sync.RWMutex
|
||||||
|
items map[string]*Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConnectionStore() *connectionStore {
|
||||||
|
return &connectionStore{
|
||||||
|
items: make(map[string]*Connection, 100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *connectionStore) getID(conn *Connection) string {
|
||||||
|
if conn.ID != "" {
|
||||||
|
return conn.ID
|
||||||
|
}
|
||||||
|
return strconv.Itoa(conn.process.Pid) + "/" + conn.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *connectionStore) add(conn *Connection) {
|
||||||
|
cs.rw.Lock()
|
||||||
|
defer cs.rw.Unlock()
|
||||||
|
|
||||||
|
cs.items[cs.getID(conn)] = conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *connectionStore) delete(conn *Connection) {
|
||||||
|
cs.rw.Lock()
|
||||||
|
defer cs.rw.Unlock()
|
||||||
|
|
||||||
|
delete(cs.items, cs.getID(conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *connectionStore) get(id string) (*Connection, bool) {
|
||||||
|
cs.rw.RLock()
|
||||||
|
defer cs.rw.RUnlock()
|
||||||
|
|
||||||
|
conn, ok := cs.items[id]
|
||||||
|
return conn, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *connectionStore) clone() map[string]*Connection {
|
||||||
|
cs.rw.RLock()
|
||||||
|
defer cs.rw.RUnlock()
|
||||||
|
|
||||||
|
m := make(map[string]*Connection, len(cs.items))
|
||||||
|
for key, conn := range cs.items {
|
||||||
|
m[key] = conn
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package network
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/network/state"
|
"github.com/safing/portmaster/network/state"
|
||||||
|
|
||||||
|
@ -16,15 +15,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dnsConns = make(map[string]*Connection) // key: <PID>/Scope
|
|
||||||
dnsConnsLock sync.RWMutex
|
|
||||||
conns = make(map[string]*Connection) // key: Connection ID
|
|
||||||
connsLock sync.RWMutex
|
|
||||||
|
|
||||||
dbController *database.Controller
|
dbController *database.Controller
|
||||||
|
|
||||||
|
dnsConns = newConnectionStore()
|
||||||
|
conns = newConnectionStore()
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageInterface provices a storage.Interface to the configuration manager.
|
// StorageInterface provices a storage.Interface to the
|
||||||
|
// configuration manager.
|
||||||
type StorageInterface struct {
|
type StorageInterface struct {
|
||||||
storage.InjectBase
|
storage.InjectBase
|
||||||
}
|
}
|
||||||
|
@ -45,18 +43,12 @@ func (s *StorageInterface) Get(key string) (record.Record, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 3:
|
case 3:
|
||||||
dnsConnsLock.RLock()
|
if r, ok := dnsConns.get(splitted[1] + "/" + splitted[2]); ok {
|
||||||
defer dnsConnsLock.RUnlock()
|
return r, nil
|
||||||
conn, ok := dnsConns[splitted[1]+"/"+splitted[2]]
|
|
||||||
if ok {
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
case 4:
|
case 4:
|
||||||
connsLock.RLock()
|
if r, ok := conns.get(splitted[3]); ok {
|
||||||
defer connsLock.RUnlock()
|
return r, nil
|
||||||
conn, ok := conns[splitted[3]]
|
|
||||||
if ok {
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "system":
|
case "system":
|
||||||
|
@ -97,28 +89,24 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
|
||||||
|
|
||||||
if slashes <= 2 {
|
if slashes <= 2 {
|
||||||
// dns scopes only
|
// dns scopes only
|
||||||
dnsConnsLock.RLock()
|
for _, dnsConn := range dnsConns.clone() {
|
||||||
for _, dnsConn := range dnsConns {
|
|
||||||
dnsConn.Lock()
|
dnsConn.Lock()
|
||||||
if q.Matches(dnsConn) {
|
if q.Matches(dnsConn) {
|
||||||
it.Next <- dnsConn
|
it.Next <- dnsConn
|
||||||
}
|
}
|
||||||
dnsConn.Unlock()
|
dnsConn.Unlock()
|
||||||
}
|
}
|
||||||
dnsConnsLock.RUnlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if slashes <= 3 {
|
if slashes <= 3 {
|
||||||
// connections
|
// connections
|
||||||
connsLock.RLock()
|
for _, conn := range conns.clone() {
|
||||||
for _, conn := range conns {
|
|
||||||
conn.Lock()
|
conn.Lock()
|
||||||
if q.Matches(conn) {
|
if q.Matches(conn) {
|
||||||
it.Next <- conn
|
it.Next <- conn
|
||||||
}
|
}
|
||||||
conn.Unlock()
|
conn.Unlock()
|
||||||
}
|
}
|
||||||
connsLock.RUnlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it.Finish(nil)
|
it.Finish(nil)
|
||||||
|
|
|
@ -17,14 +17,16 @@ var (
|
||||||
openDNSRequests = make(map[string]*Connection) // key: <pid>/fqdn
|
openDNSRequests = make(map[string]*Connection) // key: <pid>/fqdn
|
||||||
openDNSRequestsLock sync.Mutex
|
openDNSRequestsLock sync.Mutex
|
||||||
|
|
||||||
|
// scope prefix
|
||||||
|
unidentifiedProcessScopePrefix = strconv.Itoa(process.UnidentifiedProcessID) + "/"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
// write open dns requests every
|
// write open dns requests every
|
||||||
writeOpenDNSRequestsTickDuration = 5 * time.Second
|
writeOpenDNSRequestsTickDuration = 5 * time.Second
|
||||||
|
|
||||||
// duration after which DNS requests without a following connection are logged
|
// duration after which DNS requests without a following connection are logged
|
||||||
openDNSRequestLimit = 3 * time.Second
|
openDNSRequestLimit = 3 * time.Second
|
||||||
|
|
||||||
// scope prefix
|
|
||||||
unidentifiedProcessScopePrefix = strconv.Itoa(process.UnidentifiedProcessID) + "/"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getDNSRequestCacheKey(pid int, fqdn string) string {
|
func getDNSRequestCacheKey(pid int, fqdn string) string {
|
||||||
|
@ -122,15 +124,15 @@ func (conn *Connection) GetExtraRRs(ctx context.Context, request *dns.Msg) []dns
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create resource record with verdict and reason.
|
// Create resource record with verdict and reason.
|
||||||
rr, err := nsutil.MakeMessageRecord(level, fmt.Sprintf("%s: %s", conn.Verdict.Verb(), conn.Reason))
|
rr, err := nsutil.MakeMessageRecord(level, fmt.Sprintf("%s: %s", conn.Verdict.Verb(), conn.Reason.Msg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Tracer(ctx).Warningf("filter: failed to add informational record to reply: %s", err)
|
log.Tracer(ctx).Warningf("filter: failed to add informational record to reply: %s", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
extra := []dns.RR{rr}
|
extra := []dns.RR{rr}
|
||||||
|
|
||||||
// Add additional records from ReasonContext.
|
// Add additional records from Reason.Context.
|
||||||
if rrProvider, ok := conn.ReasonContext.(nsutil.RRProvider); ok {
|
if rrProvider, ok := conn.Reason.Context.(nsutil.RRProvider); ok {
|
||||||
rrs := rrProvider.GetExtraRRs(ctx, request)
|
rrs := rrProvider.GetExtraRRs(ctx, request)
|
||||||
extra = append(extra, rrs...)
|
extra = append(extra, rrs...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,6 +207,7 @@ func procDelimiter(c rune) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertIPv4(data string) net.IP {
|
func convertIPv4(data string) net.IP {
|
||||||
|
// Decode and bullshit check the data length.
|
||||||
decoded, err := hex.DecodeString(data)
|
decoded, err := hex.DecodeString(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("proc: could not parse IPv4 %s: %s", data, err)
|
log.Warningf("proc: could not parse IPv4 %s: %s", data, err)
|
||||||
|
@ -216,11 +217,14 @@ func convertIPv4(data string) net.IP {
|
||||||
log.Warningf("proc: decoded IPv4 %s has wrong length", decoded)
|
log.Warningf("proc: decoded IPv4 %s has wrong length", decoded)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the IPv4 address with the reversed byte order.
|
||||||
ip := net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0])
|
ip := net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0])
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertIPv6(data string) net.IP {
|
func convertIPv6(data string) net.IP {
|
||||||
|
// Decode and bullshit check the data length.
|
||||||
decoded, err := hex.DecodeString(data)
|
decoded, err := hex.DecodeString(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("proc: could not parse IPv6 %s: %s", data, err)
|
log.Warningf("proc: could not parse IPv6 %s: %s", data, err)
|
||||||
|
@ -230,6 +234,11 @@ func convertIPv6(data string) net.IP {
|
||||||
log.Warningf("proc: decoded IPv6 %s has wrong length", decoded)
|
log.Warningf("proc: decoded IPv6 %s has wrong length", decoded)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the IPv6 address with the translated byte order.
|
||||||
|
for i := 0; i < 16; i += 4 {
|
||||||
|
decoded[i], decoded[i+1], decoded[i+2], decoded[i+3] = decoded[i+3], decoded[i+2], decoded[i+1], decoded[i]
|
||||||
|
}
|
||||||
ip := net.IP(decoded)
|
ip := net.IP(decoded)
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ type BindInfo struct {
|
||||||
PID int
|
PID int
|
||||||
UID int
|
UID int
|
||||||
Inode int
|
Inode int
|
||||||
|
|
||||||
|
ListensAny bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address is an IP + Port pair.
|
// Address is an IP + Port pair.
|
||||||
|
@ -108,3 +110,7 @@ func (i *BindInfo) GetUIDandInode() (int, int) {
|
||||||
|
|
||||||
return i.UID, i.Inode
|
return i.UID, i.Inode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compile time checks
|
||||||
|
var _ Info = new(ConnectionInfo)
|
||||||
|
var _ Info = new(BindInfo)
|
||||||
|
|
|
@ -31,7 +31,7 @@ var (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
baseWaitTime = 3 * time.Millisecond
|
baseWaitTime = 3 * time.Millisecond
|
||||||
lookupRetries = 7
|
lookupRetries = 7 * 2 // Every retry takes two full passes.
|
||||||
)
|
)
|
||||||
|
|
||||||
// Lookup looks for the given connection in the system state tables and returns the PID of the associated process and whether the connection is inbound.
|
// Lookup looks for the given connection in the system state tables and returns the PID of the associated process and whether the connection is inbound.
|
||||||
|
@ -68,97 +68,147 @@ func (table *tcpTable) lookup(pktInfo *packet.Info) (
|
||||||
inbound bool,
|
inbound bool,
|
||||||
err error,
|
err error,
|
||||||
) {
|
) {
|
||||||
|
// Search pattern: search, wait, search, refresh, search, wait, search, refresh, ...
|
||||||
|
|
||||||
localIP := pktInfo.LocalIP()
|
// Search for the socket until found.
|
||||||
localPort := pktInfo.LocalPort()
|
|
||||||
|
|
||||||
// search until we find something
|
|
||||||
for i := 0; i <= lookupRetries; i++ {
|
for i := 0; i <= lookupRetries; i++ {
|
||||||
table.lock.RLock()
|
// Check main table for socket.
|
||||||
|
socketInfo, inbound := table.findSocket(pktInfo)
|
||||||
// always search listeners first
|
if socketInfo == nil && table.dualStack != nil {
|
||||||
for _, socketInfo := range table.listeners {
|
// If there was no match in the main table and we are dual-stack, check
|
||||||
if localPort == socketInfo.Local.Port &&
|
// the dual-stack table for the socket.
|
||||||
(socketInfo.Local.IP[0] == 0 || localIP.Equal(socketInfo.Local.IP)) {
|
socketInfo, inbound = table.dualStack.findSocket(pktInfo)
|
||||||
table.lock.RUnlock()
|
|
||||||
return checkPID(socketInfo, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// search connections
|
// If there's a match, check we have the PID and return.
|
||||||
for _, socketInfo := range table.connections {
|
if socketInfo != nil {
|
||||||
if localPort == socketInfo.Local.Port &&
|
return checkPID(socketInfo, inbound)
|
||||||
localIP.Equal(socketInfo.Local.IP) {
|
|
||||||
table.lock.RUnlock()
|
|
||||||
return checkPID(socketInfo, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.lock.RUnlock()
|
|
||||||
|
|
||||||
// every time, except for the last iteration
|
// every time, except for the last iteration
|
||||||
if i < lookupRetries {
|
if i < lookupRetries {
|
||||||
// we found nothing, we could have been too fast, give the kernel some time to think
|
// Take turns in waiting and refreshing in order to satisfy the search pattern.
|
||||||
// back off timer: with 3ms baseWaitTime: 3, 6, 9, 12, 15, 18, 21ms - 84ms in total
|
if i%2 == 0 {
|
||||||
time.Sleep(time.Duration(i+1) * baseWaitTime)
|
// we found nothing, we could have been too fast, give the kernel some time to think
|
||||||
|
// back off timer: with 3ms baseWaitTime: 3, 6, 9, 12, 15, 18, 21ms - 84ms in total
|
||||||
// refetch lists
|
time.Sleep(time.Duration(i+1) * baseWaitTime)
|
||||||
table.updateTables()
|
} else {
|
||||||
|
// refetch lists
|
||||||
|
table.updateTables()
|
||||||
|
if table.dualStack != nil {
|
||||||
|
table.dualStack.updateTables()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return socket.UnidentifiedProcessID, pktInfo.Inbound, ErrConnectionNotFound
|
return socket.UnidentifiedProcessID, pktInfo.Inbound, ErrConnectionNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (table *tcpTable) findSocket(pktInfo *packet.Info) (
|
||||||
|
socketInfo socket.Info,
|
||||||
|
inbound bool,
|
||||||
|
) {
|
||||||
|
localIP := pktInfo.LocalIP()
|
||||||
|
localPort := pktInfo.LocalPort()
|
||||||
|
|
||||||
|
table.lock.RLock()
|
||||||
|
defer table.lock.RUnlock()
|
||||||
|
|
||||||
|
// always search listeners first
|
||||||
|
for _, socketInfo := range table.listeners {
|
||||||
|
if localPort == socketInfo.Local.Port &&
|
||||||
|
(socketInfo.ListensAny || localIP.Equal(socketInfo.Local.IP)) {
|
||||||
|
return socketInfo, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// search connections
|
||||||
|
for _, socketInfo := range table.connections {
|
||||||
|
if localPort == socketInfo.Local.Port &&
|
||||||
|
localIP.Equal(socketInfo.Local.IP) {
|
||||||
|
return socketInfo, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
func (table *udpTable) lookup(pktInfo *packet.Info) (
|
func (table *udpTable) lookup(pktInfo *packet.Info) (
|
||||||
pid int,
|
pid int,
|
||||||
inbound bool,
|
inbound bool,
|
||||||
err error,
|
err error,
|
||||||
) {
|
) {
|
||||||
localIP := pktInfo.LocalIP()
|
// Search pattern: search, wait, search, refresh, search, wait, search, refresh, ...
|
||||||
localPort := pktInfo.LocalPort()
|
|
||||||
|
|
||||||
isInboundMulticast := pktInfo.Inbound && netutils.ClassifyIP(localIP) == netutils.LocalMulticast
|
|
||||||
// TODO: Currently broadcast/multicast scopes are not checked, so we might
|
// TODO: Currently broadcast/multicast scopes are not checked, so we might
|
||||||
// attribute an incoming broadcast/multicast packet to the wrong process if
|
// attribute an incoming broadcast/multicast packet to the wrong process if
|
||||||
// there are multiple processes listening on the same local port, but
|
// there are multiple processes listening on the same local port, but
|
||||||
// binding to different addresses. This highly unusual for clients.
|
// binding to different addresses. This highly unusual for clients.
|
||||||
|
isInboundMulticast := pktInfo.Inbound && netutils.ClassifyIP(pktInfo.LocalIP()) == netutils.LocalMulticast
|
||||||
|
|
||||||
// search until we find something
|
// Search for the socket until found.
|
||||||
for i := 0; i <= lookupRetries; i++ {
|
for i := 0; i <= lookupRetries; i++ {
|
||||||
table.lock.RLock()
|
// Check main table for socket.
|
||||||
|
socketInfo := table.findSocket(pktInfo, isInboundMulticast)
|
||||||
// search binds
|
if socketInfo == nil && table.dualStack != nil {
|
||||||
for _, socketInfo := range table.binds {
|
// If there was no match in the main table and we are dual-stack, check
|
||||||
if localPort == socketInfo.Local.Port &&
|
// the dual-stack table for the socket.
|
||||||
(socketInfo.Local.IP[0] == 0 || // zero IP
|
socketInfo = table.dualStack.findSocket(pktInfo, isInboundMulticast)
|
||||||
isInboundMulticast || // inbound broadcast, multicast
|
|
||||||
localIP.Equal(socketInfo.Local.IP)) {
|
|
||||||
table.lock.RUnlock()
|
|
||||||
|
|
||||||
// do not check direction if remoteIP/Port is not given
|
|
||||||
if pktInfo.RemotePort() == 0 {
|
|
||||||
return checkPID(socketInfo, pktInfo.Inbound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get direction and return
|
|
||||||
connInbound := table.getDirection(socketInfo, pktInfo)
|
|
||||||
return checkPID(socketInfo, connInbound)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.lock.RUnlock()
|
// If there's a match, get the direction and check we have the PID, then return.
|
||||||
|
if socketInfo != nil {
|
||||||
|
// If there is no remote port, do check for the direction of the
|
||||||
|
// connection. This will be the case for pure checking functions
|
||||||
|
// that do not want to change direction state.
|
||||||
|
if pktInfo.RemotePort() == 0 {
|
||||||
|
return checkPID(socketInfo, pktInfo.Inbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get (and save) the direction of the connection.
|
||||||
|
connInbound := table.getDirection(socketInfo, pktInfo)
|
||||||
|
|
||||||
|
// Check we have the PID and return.
|
||||||
|
return checkPID(socketInfo, connInbound)
|
||||||
|
}
|
||||||
|
|
||||||
// every time, except for the last iteration
|
// every time, except for the last iteration
|
||||||
if i < lookupRetries {
|
if i < lookupRetries {
|
||||||
// we found nothing, we could have been too fast, give the kernel some time to think
|
// Take turns in waiting and refreshing in order to satisfy the search pattern.
|
||||||
// back off timer: with 3ms baseWaitTime: 3, 6, 9, 12, 15, 18, 21ms - 84ms in total
|
if i%2 == 0 {
|
||||||
time.Sleep(time.Duration(i+1) * baseWaitTime)
|
// we found nothing, we could have been too fast, give the kernel some time to think
|
||||||
|
// back off timer: with 3ms baseWaitTime: 3, 6, 9, 12, 15, 18, 21ms - 84ms in total
|
||||||
// refetch lists
|
time.Sleep(time.Duration(i+1) * baseWaitTime)
|
||||||
table.updateTable()
|
} else {
|
||||||
|
// refetch lists
|
||||||
|
table.updateTable()
|
||||||
|
if table.dualStack != nil {
|
||||||
|
table.dualStack.updateTable()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return socket.UnidentifiedProcessID, pktInfo.Inbound, ErrConnectionNotFound
|
return socket.UnidentifiedProcessID, pktInfo.Inbound, ErrConnectionNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (table *udpTable) findSocket(pktInfo *packet.Info, isInboundMulticast bool) (socketInfo *socket.BindInfo) {
|
||||||
|
localIP := pktInfo.LocalIP()
|
||||||
|
localPort := pktInfo.LocalPort()
|
||||||
|
|
||||||
|
table.lock.RLock()
|
||||||
|
defer table.lock.RUnlock()
|
||||||
|
|
||||||
|
// search binds
|
||||||
|
for _, socketInfo := range table.binds {
|
||||||
|
if localPort == socketInfo.Local.Port &&
|
||||||
|
(socketInfo.ListensAny || // zero IP (dual-stack)
|
||||||
|
isInboundMulticast || // inbound broadcast, multicast
|
||||||
|
localIP.Equal(socketInfo.Local.IP)) {
|
||||||
|
return socketInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,6 +17,11 @@ func (table *tcpTable) updateTables() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-check for any listeners.
|
||||||
|
for _, bindInfo := range listeners {
|
||||||
|
bindInfo.ListensAny = bindInfo.Local.IP.Equal(net.IPv4zero) || bindInfo.Local.IP.Equal(net.IPv6zero)
|
||||||
|
}
|
||||||
|
|
||||||
table.connections = connections
|
table.connections = connections
|
||||||
table.listeners = listeners
|
table.listeners = listeners
|
||||||
})
|
})
|
||||||
|
@ -31,6 +38,11 @@ func (table *udpTable) updateTable() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-check for any listeners.
|
||||||
|
for _, bindInfo := range binds {
|
||||||
|
bindInfo.ListensAny = bindInfo.Local.IP.Equal(net.IPv4zero) || bindInfo.Local.IP.Equal(net.IPv6zero)
|
||||||
|
}
|
||||||
|
|
||||||
table.binds = binds
|
table.binds = binds
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,16 +16,19 @@ type tcpTable struct {
|
||||||
|
|
||||||
fetchOnceAgain utils.OnceAgain
|
fetchOnceAgain utils.OnceAgain
|
||||||
fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error)
|
fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error)
|
||||||
|
|
||||||
|
dualStack *tcpTable
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tcp4Table = &tcpTable{
|
|
||||||
version: 4,
|
|
||||||
fetchTable: getTCP4Table,
|
|
||||||
}
|
|
||||||
|
|
||||||
tcp6Table = &tcpTable{
|
tcp6Table = &tcpTable{
|
||||||
version: 6,
|
version: 6,
|
||||||
fetchTable: getTCP6Table,
|
fetchTable: getTCP6Table,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tcp4Table = &tcpTable{
|
||||||
|
version: 4,
|
||||||
|
fetchTable: getTCP4Table,
|
||||||
|
dualStack: tcp6Table,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,6 +22,8 @@ type udpTable struct {
|
||||||
|
|
||||||
states map[string]map[string]*udpState
|
states map[string]map[string]*udpState
|
||||||
statesLock sync.Mutex
|
statesLock sync.Mutex
|
||||||
|
|
||||||
|
dualStack *udpTable
|
||||||
}
|
}
|
||||||
|
|
||||||
type udpState struct {
|
type udpState struct {
|
||||||
|
@ -41,17 +43,18 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
udp4Table = &udpTable{
|
|
||||||
version: 4,
|
|
||||||
fetchTable: getUDP4Table,
|
|
||||||
states: make(map[string]map[string]*udpState),
|
|
||||||
}
|
|
||||||
|
|
||||||
udp6Table = &udpTable{
|
udp6Table = &udpTable{
|
||||||
version: 6,
|
version: 6,
|
||||||
fetchTable: getUDP6Table,
|
fetchTable: getUDP6Table,
|
||||||
states: make(map[string]map[string]*udpState),
|
states: make(map[string]map[string]*udpState),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
udp4Table = &udpTable{
|
||||||
|
version: 4,
|
||||||
|
fetchTable: getUDP4Table,
|
||||||
|
states: make(map[string]map[string]*udpState),
|
||||||
|
dualStack: udp6Table,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// CleanUDPStates cleans the udp connection states which save connection directions.
|
// CleanUDPStates cleans the udp connection states which save connection directions.
|
||||||
|
|
|
@ -3,9 +3,10 @@ package network
|
||||||
// Verdict describes the decision made about a connection or link.
|
// Verdict describes the decision made about a connection or link.
|
||||||
type Verdict int8
|
type Verdict int8
|
||||||
|
|
||||||
// List of values a Status can have
|
// All possible verdicts that can be applied to a network
|
||||||
|
// connection.
|
||||||
const (
|
const (
|
||||||
// UNDECIDED is the default status of new connections
|
// VerdictUndecided is the default status of new connections.
|
||||||
VerdictUndecided Verdict = 0
|
VerdictUndecided Verdict = 0
|
||||||
VerdictUndeterminable Verdict = 1
|
VerdictUndeterminable Verdict = 1
|
||||||
VerdictAccept Verdict = 2
|
VerdictAccept Verdict = 2
|
||||||
|
@ -63,7 +64,7 @@ func (v Verdict) Verb() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Packer Directions
|
// Packet Directions
|
||||||
const (
|
const (
|
||||||
Inbound = true
|
Inbound = true
|
||||||
Outbound = false
|
Outbound = false
|
||||||
|
|
75
pack
75
pack
|
@ -3,33 +3,60 @@
|
||||||
baseDir="$( cd "$(dirname "$0")" && pwd )"
|
baseDir="$( cd "$(dirname "$0")" && pwd )"
|
||||||
cd "$baseDir"
|
cd "$baseDir"
|
||||||
|
|
||||||
# first check what will be built
|
COL_OFF="\033[0m"
|
||||||
|
COL_BOLD="\033[01;01m"
|
||||||
|
COL_RED="\033[31m"
|
||||||
|
COL_GREEN="\033[32m"
|
||||||
|
COL_YELLOW="\033[33m"
|
||||||
|
|
||||||
function packAll() {
|
function safe_execute {
|
||||||
for i in ./cmds/* ; do
|
echo -e "\n[....] $*"
|
||||||
if [ -e $i/pack ]; then
|
$*
|
||||||
$i/pack $1
|
if [[ $? -eq 0 ]]; then
|
||||||
fi
|
echo -e "[${COL_GREEN} OK ${COL_OFF}] $*"
|
||||||
done
|
else
|
||||||
|
echo -e "[${COL_RED}FAIL${COL_OFF}] $*" >/dev/stderr
|
||||||
|
echo -e "[${COL_RED}CRIT${COL_OFF}] ABORTING..." >/dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
echo ""
|
function check {
|
||||||
echo "pack list:"
|
./cmds/portmaster-core/pack check
|
||||||
echo ""
|
./cmds/portmaster-start/pack check
|
||||||
|
}
|
||||||
|
|
||||||
packAll check
|
function build {
|
||||||
|
safe_execute ./cmds/portmaster-core/pack build
|
||||||
|
safe_execute ./cmds/portmaster-start/pack build
|
||||||
|
}
|
||||||
|
|
||||||
# confirm
|
function reset {
|
||||||
|
./cmds/portmaster-core/pack reset
|
||||||
|
./cmds/portmaster-start/pack reset
|
||||||
|
}
|
||||||
|
|
||||||
echo ""
|
case $1 in
|
||||||
read -p "press [Enter] to start packing" x
|
"check" )
|
||||||
echo ""
|
check
|
||||||
|
;;
|
||||||
# build
|
"build" )
|
||||||
|
build
|
||||||
set -e
|
;;
|
||||||
packAll build
|
"reset" )
|
||||||
|
reset
|
||||||
echo ""
|
;;
|
||||||
echo "finished packing."
|
* )
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "build list:"
|
||||||
|
echo ""
|
||||||
|
check
|
||||||
|
echo ""
|
||||||
|
read -p "press [Enter] to start building" x
|
||||||
|
echo ""
|
||||||
|
build
|
||||||
|
echo ""
|
||||||
|
echo "finished building."
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
|
@ -14,13 +14,16 @@ func registerConfiguration() error {
|
||||||
// Enable Process Detection
|
// Enable Process Detection
|
||||||
// This should be always enabled. Provided as an option to disable in case there are severe problems on a system, or for debugging.
|
// This should be always enabled. Provided as an option to disable in case there are severe problems on a system, or for debugging.
|
||||||
err := config.Register(&config.Option{
|
err := config.Register(&config.Option{
|
||||||
Name: "Enable Process Detection",
|
Name: "Process Detection",
|
||||||
Key: CfgOptionEnableProcessDetectionKey,
|
Key: CfgOptionEnableProcessDetectionKey,
|
||||||
Description: "This option enables the attribution of network traffic to processes. This should be always enabled, and effectively disables app profiles if disabled.",
|
Description: "This option enables the attribution of network traffic to processes. This should always be enabled, and effectively disables app profiles if disabled.",
|
||||||
Order: 144,
|
|
||||||
OptType: config.OptTypeBool,
|
OptType: config.OptTypeBool,
|
||||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
DefaultValue: true,
|
DefaultValue: true,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: 528,
|
||||||
|
config.CategoryAnnotation: "Development",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -66,7 +66,7 @@ func (p *Process) Save() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if dbControllerFlag.IsSet() {
|
if dbControllerFlag.IsSet() {
|
||||||
go dbController.PushUpdate(p)
|
dbController.PushUpdate(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ func (p *Process) Delete() {
|
||||||
// propagate delete
|
// propagate delete
|
||||||
p.Meta().Delete()
|
p.Meta().Delete()
|
||||||
if dbControllerFlag.IsSet() {
|
if dbControllerFlag.IsSet() {
|
||||||
go dbController.PushUpdate(p)
|
dbController.PushUpdate(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: maybe mark the assigned profiles as no longer needed?
|
// TODO: maybe mark the assigned profiles as no longer needed?
|
||||||
|
@ -106,32 +106,34 @@ func CleanProcessStorage(activePIDs map[int]struct{}) {
|
||||||
|
|
||||||
// clean primary processes
|
// clean primary processes
|
||||||
for _, p := range processesCopy {
|
for _, p := range processesCopy {
|
||||||
p.Lock()
|
// The PID of a process does not change.
|
||||||
|
|
||||||
_, active := activePIDs[p.Pid]
|
// Check if this is a special process.
|
||||||
switch {
|
if p.Pid == UnidentifiedProcessID || p.Pid == SystemProcessID {
|
||||||
case p.Pid == UnidentifiedProcessID:
|
p.profile.MarkStillActive()
|
||||||
// internal
|
continue
|
||||||
case p.Pid == SystemProcessID:
|
|
||||||
// internal
|
|
||||||
case active:
|
|
||||||
// process in system process table or recently seen on the network
|
|
||||||
default:
|
|
||||||
// delete now or soon
|
|
||||||
switch {
|
|
||||||
case p.LastSeen == 0:
|
|
||||||
// add last
|
|
||||||
p.LastSeen = time.Now().Unix()
|
|
||||||
case p.LastSeen > threshold:
|
|
||||||
// within keep period
|
|
||||||
default:
|
|
||||||
// delete now
|
|
||||||
log.Tracef("process.clean: deleted %s", p.DatabaseKey())
|
|
||||||
go p.Delete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Unlock()
|
// Check if process is active.
|
||||||
|
_, active := activePIDs[p.Pid]
|
||||||
|
if active {
|
||||||
|
p.profile.MarkStillActive()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process is inactive, start deletion process
|
||||||
|
lastSeen := p.GetLastSeen()
|
||||||
|
switch {
|
||||||
|
case lastSeen == 0:
|
||||||
|
// add last seen timestamp
|
||||||
|
p.SetLastSeen(time.Now().Unix())
|
||||||
|
case lastSeen > threshold:
|
||||||
|
// within keep period
|
||||||
|
default:
|
||||||
|
// delete now
|
||||||
|
p.Delete()
|
||||||
|
log.Tracef("process: cleaned %s", p.DatabaseKey())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,10 +30,14 @@ func GetProcessByConnection(ctx context.Context, pktInfo *packet.Info) (process
|
||||||
return nil, connInbound, err
|
return nil, connInbound, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = process.GetProfile(ctx)
|
changed, err := process.GetProfile(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
|
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
process.Save()
|
||||||
|
}
|
||||||
|
|
||||||
return process, connInbound, nil
|
return process, connInbound, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,40 +30,56 @@ type Process struct {
|
||||||
record.Base
|
record.Base
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
||||||
|
// Constant attributes.
|
||||||
|
|
||||||
|
Name string
|
||||||
UserID int
|
UserID int
|
||||||
UserName string
|
UserName string
|
||||||
UserHome string
|
UserHome string
|
||||||
Pid int
|
Pid int
|
||||||
ParentPid int
|
ParentPid int
|
||||||
Path string
|
Path string
|
||||||
|
ExecName string
|
||||||
Cwd string
|
Cwd string
|
||||||
CmdLine string
|
CmdLine string
|
||||||
FirstArg string
|
FirstArg string
|
||||||
|
|
||||||
ExecName string
|
|
||||||
ExecHashes map[string]string
|
|
||||||
// ExecOwner ...
|
|
||||||
// ExecSignature ...
|
|
||||||
|
|
||||||
LocalProfileKey string
|
LocalProfileKey string
|
||||||
profile *profile.LayeredProfile
|
profile *profile.LayeredProfile
|
||||||
Name string
|
|
||||||
Icon string
|
// Mutable attributes.
|
||||||
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for database cache path or "c:"/"a:" for a the icon key to fetch it from a company / authoritative node and cache it in its own cache.
|
|
||||||
|
|
||||||
FirstSeen int64
|
FirstSeen int64
|
||||||
LastSeen int64
|
LastSeen int64
|
||||||
|
Virtual bool // This process is either merged into another process or is not needed.
|
||||||
|
Error string // Cache errors
|
||||||
|
|
||||||
Virtual bool // This process is either merged into another process or is not needed.
|
ExecHashes map[string]string
|
||||||
Error string // Cache errors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profile returns the assigned layered profile.
|
// Profile returns the assigned layered profile.
|
||||||
func (p *Process) Profile() *profile.LayeredProfile {
|
func (p *Process) Profile() *profile.LayeredProfile {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.profile
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastSeen returns the unix timestamp when the process was last seen.
|
||||||
|
func (p *Process) GetLastSeen() int64 {
|
||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
|
|
||||||
return p.profile
|
return p.LastSeen
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLastSeen sets the unix timestamp when the process was last seen.
|
||||||
|
func (p *Process) SetLastSeen(lastSeen int64) {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
p.LastSeen = lastSeen
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strings returns a string representation of process.
|
// Strings returns a string representation of process.
|
||||||
|
@ -72,8 +88,6 @@ func (p *Process) String() string {
|
||||||
return "?"
|
return "?"
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Lock()
|
|
||||||
defer p.Unlock()
|
|
||||||
return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid)
|
return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,165 +232,83 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
||||||
defer markRequestFinished()
|
defer markRequestFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
// create new process
|
// Create new a process object.
|
||||||
new := &Process{
|
new := &Process{
|
||||||
Pid: pid,
|
Pid: pid,
|
||||||
Virtual: true, // caller must decide to actually use the process - we need to save now.
|
Virtual: true, // caller must decide to actually use the process - we need to save now.
|
||||||
FirstSeen: time.Now().Unix(),
|
FirstSeen: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
// Get process information from the system.
|
||||||
case new.IsKernel():
|
pInfo, err := processInfo.NewProcess(int32(pid))
|
||||||
new.UserName = "Kernel"
|
if err != nil {
|
||||||
new.Name = "Operating System"
|
return nil, err
|
||||||
default:
|
|
||||||
|
|
||||||
pInfo, err := processInfo.NewProcess(int32(pid))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UID
|
|
||||||
// net yet implemented for windows
|
|
||||||
if runtime.GOOS == "linux" {
|
|
||||||
var uids []int32
|
|
||||||
uids, err = pInfo.Uids()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get UID for p%d: %s", pid, err)
|
|
||||||
}
|
|
||||||
new.UserID = int(uids[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Username
|
|
||||||
new.UserName, err = pInfo.Username()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("process: failed to get Username for p%d: %s", pid, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: User Home
|
|
||||||
// new.UserHome, err =
|
|
||||||
|
|
||||||
// PPID
|
|
||||||
ppid, err := pInfo.Ppid()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get PPID for p%d: %s", pid, err)
|
|
||||||
}
|
|
||||||
new.ParentPid = int(ppid)
|
|
||||||
|
|
||||||
// Path
|
|
||||||
new.Path, err = pInfo.Exe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get Path for p%d: %s", pid, err)
|
|
||||||
}
|
|
||||||
// remove linux " (deleted)" suffix for deleted files
|
|
||||||
if onLinux {
|
|
||||||
new.Path = strings.TrimSuffix(new.Path, " (deleted)")
|
|
||||||
}
|
|
||||||
// Executable Name
|
|
||||||
_, new.ExecName = filepath.Split(new.Path)
|
|
||||||
|
|
||||||
// Current working directory
|
|
||||||
// net yet implemented for windows
|
|
||||||
// new.Cwd, err = pInfo.Cwd()
|
|
||||||
// if err != nil {
|
|
||||||
// log.Warningf("process: failed to get Cwd: %s", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Command line arguments
|
|
||||||
new.CmdLine, err = pInfo.Cmdline()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get Cmdline for p%d: %s", pid, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name
|
|
||||||
new.Name, err = pInfo.Name()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get Name for p%d: %s", pid, err)
|
|
||||||
}
|
|
||||||
if new.Name == "" {
|
|
||||||
new.Name = new.ExecName
|
|
||||||
}
|
|
||||||
|
|
||||||
// OS specifics
|
|
||||||
new.specialOSInit()
|
|
||||||
|
|
||||||
// TODO: App Icon
|
|
||||||
// new.Icon, err =
|
|
||||||
|
|
||||||
// get Profile
|
|
||||||
// processPath := new.Path
|
|
||||||
// var applyProfile *profiles.Profile
|
|
||||||
// iterations := 0
|
|
||||||
// for applyProfile == nil {
|
|
||||||
//
|
|
||||||
// iterations++
|
|
||||||
// if iterations > 10 {
|
|
||||||
// log.Warningf("process: got into loop while getting profile for %s", new)
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// applyProfile, err = profiles.GetActiveProfileByPath(processPath)
|
|
||||||
// if err == database.ErrNotFound {
|
|
||||||
// applyProfile, err = profiles.FindProfileByPath(processPath, new.UserHome)
|
|
||||||
// }
|
|
||||||
// if err != nil {
|
|
||||||
// log.Warningf("process: could not get profile for %s: %s", new, err)
|
|
||||||
// } else if applyProfile == nil {
|
|
||||||
// log.Warningf("process: no default profile found for %s", new)
|
|
||||||
// } else {
|
|
||||||
//
|
|
||||||
// // TODO: there is a lot of undefined behaviour if chaining framework profiles
|
|
||||||
//
|
|
||||||
// // process framework
|
|
||||||
// if applyProfile.Framework != nil {
|
|
||||||
// if applyProfile.Framework.FindParent > 0 {
|
|
||||||
// var ppid int32
|
|
||||||
// for i := uint8(1); i < applyProfile.Framework.FindParent; i++ {
|
|
||||||
// parent, err := pInfo.Parent()
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// ppid = parent.Pid
|
|
||||||
// }
|
|
||||||
// if applyProfile.Framework.MergeWithParent {
|
|
||||||
// return GetOrFindProcess(int(ppid))
|
|
||||||
// }
|
|
||||||
// // processPath, err = os.Readlink(fmt.Sprintf("/proc/%d/exe", pid))
|
|
||||||
// // if err != nil {
|
|
||||||
// // return nil, fmt.Errorf("could not read /proc/%d/exe: %s", pid, err)
|
|
||||||
// // }
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// newCommand, err := applyProfile.Framework.GetNewPath(new.CmdLine, new.Cwd)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // assign
|
|
||||||
// new.CmdLine = newCommand
|
|
||||||
// new.Path = strings.SplitN(newCommand, " ", 2)[0]
|
|
||||||
// processPath = new.Path
|
|
||||||
//
|
|
||||||
// // make sure we loop
|
|
||||||
// applyProfile = nil
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // apply profile to process
|
|
||||||
// log.Debugf("process: applied profile to %s: %s", new, applyProfile)
|
|
||||||
// new.Profile = applyProfile
|
|
||||||
// new.ProfileKey = applyProfile.GetKey().String()
|
|
||||||
//
|
|
||||||
// // update Profile with Process icon if Profile does not have one
|
|
||||||
// if !new.Profile.Default && new.Icon != "" && new.Profile.Icon == "" {
|
|
||||||
// new.Profile.Icon = new.Icon
|
|
||||||
// new.Profile.Save()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UID
|
||||||
|
// net yet implemented for windows
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
var uids []int32
|
||||||
|
uids, err = pInfo.Uids()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get UID for p%d: %s", pid, err)
|
||||||
|
}
|
||||||
|
new.UserID = int(uids[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Username
|
||||||
|
new.UserName, err = pInfo.Username()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("process: failed to get Username for p%d: %s", pid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: User Home
|
||||||
|
// new.UserHome, err =
|
||||||
|
|
||||||
|
// PPID
|
||||||
|
ppid, err := pInfo.Ppid()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get PPID for p%d: %s", pid, err)
|
||||||
|
}
|
||||||
|
new.ParentPid = int(ppid)
|
||||||
|
|
||||||
|
// Path
|
||||||
|
new.Path, err = pInfo.Exe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get Path for p%d: %s", pid, err)
|
||||||
|
}
|
||||||
|
// remove linux " (deleted)" suffix for deleted files
|
||||||
|
if onLinux {
|
||||||
|
new.Path = strings.TrimSuffix(new.Path, " (deleted)")
|
||||||
|
}
|
||||||
|
// Executable Name
|
||||||
|
_, new.ExecName = filepath.Split(new.Path)
|
||||||
|
|
||||||
|
// Current working directory
|
||||||
|
// net yet implemented for windows
|
||||||
|
// new.Cwd, err = pInfo.Cwd()
|
||||||
|
// if err != nil {
|
||||||
|
// log.Warningf("process: failed to get Cwd: %s", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Command line arguments
|
||||||
|
new.CmdLine, err = pInfo.Cmdline()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get Cmdline for p%d: %s", pid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name
|
||||||
|
new.Name, err = pInfo.Name()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get Name for p%d: %s", pid, err)
|
||||||
|
}
|
||||||
|
if new.Name == "" {
|
||||||
|
new.Name = new.ExecName
|
||||||
|
}
|
||||||
|
|
||||||
|
// OS specifics
|
||||||
|
new.specialOSInit()
|
||||||
|
|
||||||
new.Save()
|
new.Save()
|
||||||
return new, nil
|
return new, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,8 @@
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
// IsKernel returns whether the process is the Kernel.
|
// SystemProcessID is the PID of the System/Kernel itself.
|
||||||
func (p *Process) IsKernel() bool {
|
const SystemProcessID = 0
|
||||||
return p.Pid == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// specialOSInit does special OS specific Process initialization.
|
// specialOSInit does special OS specific Process initialization.
|
||||||
func (p *Process) specialOSInit() {
|
func (p *Process) specialOSInit() {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package process
|
package process
|
||||||
|
|
||||||
// IsKernel returns whether the process is the Kernel.
|
// SystemProcessID is the PID of the System/Kernel itself.
|
||||||
func (p *Process) IsKernel() bool {
|
const SystemProcessID = 0
|
||||||
return p.Pid == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// specialOSInit does special OS specific Process initialization.
|
// specialOSInit does special OS specific Process initialization.
|
||||||
func (p *Process) specialOSInit() {
|
func (p *Process) specialOSInit() {
|
||||||
|
|
|
@ -7,10 +7,8 @@ import (
|
||||||
"github.com/safing/portbase/utils/osdetail"
|
"github.com/safing/portbase/utils/osdetail"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsKernel returns whether the process is the Kernel.
|
// SystemProcessID is the PID of the System/Kernel itself.
|
||||||
func (p *Process) IsKernel() bool {
|
const SystemProcessID = 4
|
||||||
return p.Pid == 4
|
|
||||||
}
|
|
||||||
|
|
||||||
// specialOSInit does special OS specific Process initialization.
|
// specialOSInit does special OS specific Process initialization.
|
||||||
func (p *Process) specialOSInit() {
|
func (p *Process) specialOSInit() {
|
||||||
|
|
|
@ -8,35 +8,51 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetProfile finds and assigns a profile set to the process.
|
// GetProfile finds and assigns a profile set to the process.
|
||||||
func (p *Process) GetProfile(ctx context.Context) error {
|
func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
|
|
||||||
// only find profiles if not already done.
|
// only find profiles if not already done.
|
||||||
if p.profile != nil {
|
if p.profile != nil {
|
||||||
log.Tracer(ctx).Trace("process: profile already loaded")
|
log.Tracer(ctx).Trace("process: profile already loaded")
|
||||||
// mark profile as used
|
// Mark profile as used.
|
||||||
p.profile.MarkUsed()
|
p.profile.MarkUsed()
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
log.Tracer(ctx).Trace("process: loading profile")
|
log.Tracer(ctx).Trace("process: loading profile")
|
||||||
|
|
||||||
// get profile
|
// Check if we need a special profile.
|
||||||
localProfile, new, err := profile.FindOrCreateLocalProfileByPath(p.Path)
|
profileID := ""
|
||||||
|
switch p.Pid {
|
||||||
|
case UnidentifiedProcessID:
|
||||||
|
profileID = profile.UnidentifiedProfileID
|
||||||
|
case SystemProcessID:
|
||||||
|
profileID = profile.SystemProfileID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the (linked) local profile.
|
||||||
|
localProfile, err := profile.GetProfile(profile.SourceLocal, profileID, p.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
|
||||||
// add more information if new
|
|
||||||
if new {
|
|
||||||
localProfile.Name = p.ExecName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mark profile as used
|
// Update metadata of profile.
|
||||||
localProfile.MarkUsed()
|
metadataUpdated := localProfile.UpdateMetadata(p.Name)
|
||||||
|
|
||||||
|
// Mark profile as used.
|
||||||
|
profileChanged := localProfile.MarkUsed()
|
||||||
|
|
||||||
|
// Save the profile if we changed something.
|
||||||
|
if metadataUpdated || profileChanged {
|
||||||
|
err := localProfile.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("process: failed to save profile %s: %s", localProfile.ScopedID(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign profile to process.
|
||||||
p.LocalProfileKey = localProfile.Key()
|
p.LocalProfileKey = localProfile.Key()
|
||||||
p.profile = profile.NewLayeredProfile(localProfile)
|
p.profile = localProfile.LayeredProfile()
|
||||||
|
|
||||||
go p.Save()
|
return true, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,16 @@ package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portmaster/profile"
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Special Process IDs
|
// UnidentifiedProcessID is the PID used for anything that could not be
|
||||||
const (
|
// attributed to a PID for any reason.
|
||||||
UnidentifiedProcessID = -1
|
const UnidentifiedProcessID = -1
|
||||||
SystemProcessID = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// unidentifiedProcess is used when a process cannot be found.
|
// unidentifiedProcess is used when a process cannot be found.
|
||||||
|
@ -32,53 +31,41 @@ var (
|
||||||
ParentPid: SystemProcessID,
|
ParentPid: SystemProcessID,
|
||||||
Name: "Operating System",
|
Name: "Operating System",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSpecialProcessSingleInflight singleflight.Group
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetUnidentifiedProcess returns the special process assigned to unidentified processes.
|
// GetUnidentifiedProcess returns the special process assigned to unidentified processes.
|
||||||
func GetUnidentifiedProcess(ctx context.Context) *Process {
|
func GetUnidentifiedProcess(ctx context.Context) *Process {
|
||||||
return getSpecialProcess(ctx, UnidentifiedProcessID, unidentifiedProcess, profile.GetUnidentifiedProfile)
|
return getSpecialProcess(ctx, unidentifiedProcess)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSystemProcess returns the special process used for the Kernel.
|
// GetSystemProcess returns the special process used for the Kernel.
|
||||||
func GetSystemProcess(ctx context.Context) *Process {
|
func GetSystemProcess(ctx context.Context) *Process {
|
||||||
return getSpecialProcess(ctx, SystemProcessID, systemProcess, profile.GetSystemProfile)
|
return getSpecialProcess(ctx, systemProcess)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSpecialProcess(ctx context.Context, pid int, template *Process, getProfile func() *profile.Profile) *Process {
|
func getSpecialProcess(ctx context.Context, template *Process) *Process {
|
||||||
// check storage
|
p, _, _ := getSpecialProcessSingleInflight.Do(strconv.Itoa(template.Pid), func() (interface{}, error) {
|
||||||
p, ok := GetProcessFromStorage(pid)
|
// Check if we have already loaded the special process.
|
||||||
if ok {
|
process, ok := GetProcessFromStorage(template.Pid)
|
||||||
return p
|
if ok {
|
||||||
}
|
return process, nil
|
||||||
|
}
|
||||||
|
|
||||||
// assign template
|
// Create new process from template
|
||||||
p = template
|
process = template
|
||||||
|
process.FirstSeen = time.Now().Unix()
|
||||||
|
|
||||||
p.Lock()
|
// Get profile.
|
||||||
defer p.Unlock()
|
_, err := process.GetProfile(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
|
||||||
|
}
|
||||||
|
|
||||||
if p.FirstSeen == 0 {
|
// Save process to storage.
|
||||||
p.FirstSeen = time.Now().Unix()
|
process.Save()
|
||||||
}
|
return process, nil
|
||||||
|
})
|
||||||
// only find profiles if not already done.
|
return p.(*Process)
|
||||||
if p.profile != nil {
|
|
||||||
log.Tracer(ctx).Trace("process: special profile already loaded")
|
|
||||||
// mark profile as used
|
|
||||||
p.profile.MarkUsed()
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
log.Tracer(ctx).Trace("process: loading special profile")
|
|
||||||
|
|
||||||
// get profile
|
|
||||||
localProfile := getProfile()
|
|
||||||
|
|
||||||
// mark profile as used
|
|
||||||
localProfile.MarkUsed()
|
|
||||||
|
|
||||||
p.LocalProfileKey = localProfile.Key()
|
|
||||||
p.profile = profile.NewLayeredProfile(localProfile)
|
|
||||||
|
|
||||||
go p.Save()
|
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,46 +7,69 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
activeProfileCleanerTickDuration = 10 * time.Minute
|
activeProfileCleanerTickDuration = 1 * time.Minute
|
||||||
activeProfileCleanerThreshold = 1 * time.Hour
|
activeProfileCleanerThreshold = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// TODO: periodically clean up inactive profiles
|
|
||||||
activeProfiles = make(map[string]*Profile)
|
activeProfiles = make(map[string]*Profile)
|
||||||
activeProfilesLock sync.RWMutex
|
activeProfilesLock sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// getActiveProfile returns a cached copy of an active profile and nil if it isn't found.
|
// getActiveProfile returns a cached copy of an active profile and
|
||||||
|
// nil if it isn't found.
|
||||||
func getActiveProfile(scopedID string) *Profile {
|
func getActiveProfile(scopedID string) *Profile {
|
||||||
activeProfilesLock.Lock()
|
activeProfilesLock.RLock()
|
||||||
defer activeProfilesLock.Unlock()
|
defer activeProfilesLock.RUnlock()
|
||||||
|
|
||||||
profile, ok := activeProfiles[scopedID]
|
return activeProfiles[scopedID]
|
||||||
if ok {
|
}
|
||||||
return profile
|
|
||||||
|
// getAllActiveProfiles returns a slice of active profiles.
|
||||||
|
func getAllActiveProfiles() []*Profile {
|
||||||
|
activeProfilesLock.RLock()
|
||||||
|
defer activeProfilesLock.RUnlock()
|
||||||
|
|
||||||
|
result := make([]*Profile, 0, len(activeProfiles))
|
||||||
|
for _, p := range activeProfiles {
|
||||||
|
result = append(result, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// findActiveProfile searched for an active local profile using the linked path.
|
||||||
|
func findActiveProfile(linkedPath string) *Profile {
|
||||||
|
activeProfilesLock.RLock()
|
||||||
|
defer activeProfilesLock.RUnlock()
|
||||||
|
|
||||||
|
for _, activeProfile := range activeProfiles {
|
||||||
|
if activeProfile.LinkedPath == linkedPath {
|
||||||
|
activeProfile.MarkStillActive()
|
||||||
|
return activeProfile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// markProfileActive registers a profile as active.
|
// addActiveProfile registers a active profile.
|
||||||
func markProfileActive(profile *Profile) {
|
func addActiveProfile(profile *Profile) {
|
||||||
activeProfilesLock.Lock()
|
activeProfilesLock.Lock()
|
||||||
defer activeProfilesLock.Unlock()
|
defer activeProfilesLock.Unlock()
|
||||||
|
|
||||||
|
profile.MarkStillActive()
|
||||||
activeProfiles[profile.ScopedID()] = profile
|
activeProfiles[profile.ScopedID()] = profile
|
||||||
}
|
}
|
||||||
|
|
||||||
// markActiveProfileAsOutdated marks an active profile as outdated, so that it will be refetched from the database.
|
// markActiveProfileAsOutdated marks an active profile as outdated.
|
||||||
func markActiveProfileAsOutdated(scopedID string) {
|
func markActiveProfileAsOutdated(scopedID string) {
|
||||||
activeProfilesLock.Lock()
|
activeProfilesLock.RLock()
|
||||||
defer activeProfilesLock.Unlock()
|
defer activeProfilesLock.RUnlock()
|
||||||
|
|
||||||
profile, ok := activeProfiles[scopedID]
|
profile, ok := activeProfiles[scopedID]
|
||||||
if ok {
|
if ok {
|
||||||
profile.outdated.Set()
|
profile.outdated.Set()
|
||||||
delete(activeProfiles, scopedID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,16 +78,12 @@ func cleanActiveProfiles(ctx context.Context) error {
|
||||||
select {
|
select {
|
||||||
case <-time.After(activeProfileCleanerTickDuration):
|
case <-time.After(activeProfileCleanerTickDuration):
|
||||||
|
|
||||||
threshold := time.Now().Add(-activeProfileCleanerThreshold)
|
threshold := time.Now().Add(-activeProfileCleanerThreshold).Unix()
|
||||||
|
|
||||||
activeProfilesLock.Lock()
|
activeProfilesLock.Lock()
|
||||||
for id, profile := range activeProfiles {
|
for id, profile := range activeProfiles {
|
||||||
// get last used
|
// Remove profile if it hasn't been used for a while.
|
||||||
profile.Lock()
|
if profile.LastActive() < threshold {
|
||||||
lastUsed := profile.lastUsed
|
|
||||||
profile.Unlock()
|
|
||||||
// remove if not used for a while
|
|
||||||
if lastUsed.Before(threshold) {
|
|
||||||
profile.outdated.Set()
|
profile.outdated.Set()
|
||||||
delete(activeProfiles, id)
|
delete(activeProfiles, id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portbase/config"
|
"github.com/safing/portbase/config"
|
||||||
|
"github.com/safing/portbase/modules"
|
||||||
"github.com/safing/portmaster/intel/filterlists"
|
"github.com/safing/portmaster/intel/filterlists"
|
||||||
"github.com/safing/portmaster/profile/endpoints"
|
"github.com/safing/portmaster/profile/endpoints"
|
||||||
)
|
)
|
||||||
|
@ -25,11 +26,15 @@ func registerConfigUpdater() error {
|
||||||
"config",
|
"config",
|
||||||
"config change",
|
"config change",
|
||||||
"update global config profile",
|
"update global config profile",
|
||||||
updateGlobalConfigProfile,
|
func(ctx context.Context, _ interface{}) error {
|
||||||
|
return updateGlobalConfigProfile(ctx, nil)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateGlobalConfigProfile(ctx context.Context, data interface{}) error {
|
const globalConfigProfileErrorID = "profile:global-profile-error"
|
||||||
|
|
||||||
|
func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
|
||||||
cfgLock.Lock()
|
cfgLock.Lock()
|
||||||
defer cfgLock.Unlock()
|
defer cfgLock.Unlock()
|
||||||
|
|
||||||
|
@ -71,13 +76,9 @@ func updateGlobalConfigProfile(ctx context.Context, data interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// build global profile for reference
|
// build global profile for reference
|
||||||
profile := &Profile{
|
profile := New(SourceSpecial, "global-config", "")
|
||||||
ID: "global-config",
|
profile.Name = "Global Configuration"
|
||||||
Source: SourceSpecial,
|
profile.Internal = true
|
||||||
Name: "Global Configuration",
|
|
||||||
Config: make(map[string]interface{}),
|
|
||||||
internalSave: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
newConfig := make(map[string]interface{})
|
newConfig := make(map[string]interface{})
|
||||||
// fill profile config options
|
// fill profile config options
|
||||||
|
@ -104,5 +105,27 @@ func updateGlobalConfigProfile(ctx context.Context, data interface{}) error {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there was any error, try again later until it succeeds.
|
||||||
|
if lastErr == nil {
|
||||||
|
module.Resolve(globalConfigProfileErrorID)
|
||||||
|
} else {
|
||||||
|
// Create task after first failure.
|
||||||
|
if task == nil {
|
||||||
|
task = module.NewTask(
|
||||||
|
"retry updating global config profile",
|
||||||
|
updateGlobalConfigProfile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule task.
|
||||||
|
task.Schedule(time.Now().Add(15 * time.Second))
|
||||||
|
|
||||||
|
// Add module warning to inform user.
|
||||||
|
module.Warning(
|
||||||
|
globalConfigProfileErrorID,
|
||||||
|
fmt.Sprintf("Failed to process global settings: %s", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return lastErr
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package profile
|
package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/safing/portbase/config"
|
"github.com/safing/portbase/config"
|
||||||
|
"github.com/safing/portmaster/profile/endpoints"
|
||||||
"github.com/safing/portmaster/status"
|
"github.com/safing/portmaster/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,13 +15,18 @@ var (
|
||||||
cfgIntOptions = make(map[string]config.IntOption)
|
cfgIntOptions = make(map[string]config.IntOption)
|
||||||
cfgBoolOptions = make(map[string]config.BoolOption)
|
cfgBoolOptions = make(map[string]config.BoolOption)
|
||||||
|
|
||||||
|
// General
|
||||||
|
|
||||||
// Enable Filter Order = 0
|
// Enable Filter Order = 0
|
||||||
|
|
||||||
CfgOptionDefaultActionKey = "filter/defaultAction"
|
CfgOptionDefaultActionKey = "filter/defaultAction"
|
||||||
cfgOptionDefaultAction config.StringOption
|
cfgOptionDefaultAction config.StringOption
|
||||||
cfgOptionDefaultActionOrder = 1
|
cfgOptionDefaultActionOrder = 1
|
||||||
|
|
||||||
// Prompt Timeout Order = 2
|
// Prompt Desktop Notifications Order = 2
|
||||||
|
// Prompt Timeout Order = 3
|
||||||
|
|
||||||
|
// Network Scopes
|
||||||
|
|
||||||
CfgOptionBlockScopeInternetKey = "filter/blockInternet"
|
CfgOptionBlockScopeInternetKey = "filter/blockInternet"
|
||||||
cfgOptionBlockScopeInternet config.IntOption // security level option
|
cfgOptionBlockScopeInternet config.IntOption // security level option
|
||||||
|
@ -32,6 +40,8 @@ var (
|
||||||
cfgOptionBlockScopeLocal config.IntOption // security level option
|
cfgOptionBlockScopeLocal config.IntOption // security level option
|
||||||
cfgOptionBlockScopeLocalOrder = 18
|
cfgOptionBlockScopeLocalOrder = 18
|
||||||
|
|
||||||
|
// Connection Types
|
||||||
|
|
||||||
CfgOptionBlockP2PKey = "filter/blockP2P"
|
CfgOptionBlockP2PKey = "filter/blockP2P"
|
||||||
cfgOptionBlockP2P config.IntOption // security level option
|
cfgOptionBlockP2P config.IntOption // security level option
|
||||||
cfgOptionBlockP2POrder = 19
|
cfgOptionBlockP2POrder = 19
|
||||||
|
@ -40,6 +50,8 @@ var (
|
||||||
cfgOptionBlockInbound config.IntOption // security level option
|
cfgOptionBlockInbound config.IntOption // security level option
|
||||||
cfgOptionBlockInboundOrder = 20
|
cfgOptionBlockInboundOrder = 20
|
||||||
|
|
||||||
|
// Rules
|
||||||
|
|
||||||
CfgOptionEndpointsKey = "filter/endpoints"
|
CfgOptionEndpointsKey = "filter/endpoints"
|
||||||
cfgOptionEndpoints config.StringArrayOption
|
cfgOptionEndpoints config.StringArrayOption
|
||||||
cfgOptionEndpointsOrder = 32
|
cfgOptionEndpointsOrder = 32
|
||||||
|
@ -48,43 +60,47 @@ var (
|
||||||
cfgOptionServiceEndpoints config.StringArrayOption
|
cfgOptionServiceEndpoints config.StringArrayOption
|
||||||
cfgOptionServiceEndpointsOrder = 33
|
cfgOptionServiceEndpointsOrder = 33
|
||||||
|
|
||||||
CfgOptionPreventBypassingKey = "filter/preventBypassing"
|
|
||||||
cfgOptionPreventBypassing config.IntOption // security level option
|
|
||||||
cfgOptionPreventBypassingOrder = 48
|
|
||||||
|
|
||||||
CfgOptionFilterListsKey = "filter/lists"
|
CfgOptionFilterListsKey = "filter/lists"
|
||||||
cfgOptionFilterLists config.StringArrayOption
|
cfgOptionFilterLists config.StringArrayOption
|
||||||
cfgOptionFilterListsOrder = 64
|
cfgOptionFilterListsOrder = 34
|
||||||
|
|
||||||
CfgOptionFilterSubDomainsKey = "filter/includeSubdomains"
|
CfgOptionFilterSubDomainsKey = "filter/includeSubdomains"
|
||||||
cfgOptionFilterSubDomains config.IntOption // security level option
|
cfgOptionFilterSubDomains config.IntOption // security level option
|
||||||
cfgOptionFilterSubDomainsOrder = 65
|
cfgOptionFilterSubDomainsOrder = 35
|
||||||
|
|
||||||
|
// DNS Filtering
|
||||||
|
|
||||||
CfgOptionFilterCNAMEKey = "filter/includeCNAMEs"
|
CfgOptionFilterCNAMEKey = "filter/includeCNAMEs"
|
||||||
cfgOptionFilterCNAME config.IntOption // security level option
|
cfgOptionFilterCNAME config.IntOption // security level option
|
||||||
cfgOptionFilterCNAMEOrder = 66
|
cfgOptionFilterCNAMEOrder = 48
|
||||||
|
|
||||||
CfgOptionDisableAutoPermitKey = "filter/disableAutoPermit"
|
|
||||||
cfgOptionDisableAutoPermit config.IntOption // security level option
|
|
||||||
cfgOptionDisableAutoPermitOrder = 80
|
|
||||||
|
|
||||||
CfgOptionEnforceSPNKey = "filter/enforceSPN"
|
|
||||||
cfgOptionEnforceSPN config.IntOption // security level option
|
|
||||||
cfgOptionEnforceSPNOrder = 96
|
|
||||||
|
|
||||||
CfgOptionRemoveOutOfScopeDNSKey = "filter/removeOutOfScopeDNS"
|
CfgOptionRemoveOutOfScopeDNSKey = "filter/removeOutOfScopeDNS"
|
||||||
cfgOptionRemoveOutOfScopeDNS config.IntOption // security level option
|
cfgOptionRemoveOutOfScopeDNS config.IntOption // security level option
|
||||||
cfgOptionRemoveOutOfScopeDNSOrder = 112
|
cfgOptionRemoveOutOfScopeDNSOrder = 49
|
||||||
|
|
||||||
CfgOptionRemoveBlockedDNSKey = "filter/removeBlockedDNS"
|
CfgOptionRemoveBlockedDNSKey = "filter/removeBlockedDNS"
|
||||||
cfgOptionRemoveBlockedDNS config.IntOption // security level option
|
cfgOptionRemoveBlockedDNS config.IntOption // security level option
|
||||||
cfgOptionRemoveBlockedDNSOrder = 113
|
cfgOptionRemoveBlockedDNSOrder = 50
|
||||||
|
|
||||||
CfgOptionDomainHeuristicsKey = "filter/domainHeuristics"
|
CfgOptionDomainHeuristicsKey = "filter/domainHeuristics"
|
||||||
cfgOptionDomainHeuristics config.IntOption // security level option
|
cfgOptionDomainHeuristics config.IntOption // security level option
|
||||||
cfgOptionDomainHeuristicsOrder = 114
|
cfgOptionDomainHeuristicsOrder = 51
|
||||||
|
|
||||||
// Permanent Verdicts Order = 128
|
// Advanced
|
||||||
|
|
||||||
|
CfgOptionPreventBypassingKey = "filter/preventBypassing"
|
||||||
|
cfgOptionPreventBypassing config.IntOption // security level option
|
||||||
|
cfgOptionPreventBypassingOrder = 64
|
||||||
|
|
||||||
|
CfgOptionDisableAutoPermitKey = "filter/disableAutoPermit"
|
||||||
|
cfgOptionDisableAutoPermit config.IntOption // security level option
|
||||||
|
cfgOptionDisableAutoPermitOrder = 65
|
||||||
|
|
||||||
|
// Permanent Verdicts Order = 96
|
||||||
|
|
||||||
|
CfgOptionUseSPNKey = "spn/useSPN"
|
||||||
|
cfgOptionUseSPN config.BoolOption
|
||||||
|
cfgOptionUseSPNOrder = 129
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerConfiguration() error {
|
func registerConfiguration() error {
|
||||||
|
@ -93,15 +109,33 @@ func registerConfiguration() error {
|
||||||
// ask - ask mode: if not verdict is found, the user is consulted
|
// ask - ask mode: if not verdict is found, the user is consulted
|
||||||
// block - allowlist mode: everything is blocked unless permitted
|
// block - allowlist mode: everything is blocked unless permitted
|
||||||
err := config.Register(&config.Option{
|
err := config.Register(&config.Option{
|
||||||
Name: "Default Filter Action",
|
Name: "Default Action",
|
||||||
Key: CfgOptionDefaultActionKey,
|
Key: CfgOptionDefaultActionKey,
|
||||||
Description: `The default filter action when nothing else permits or blocks a connection.`,
|
Description: `The default action when nothing else permits or blocks an outgoing connection. Incoming connections are always blocked by default.`,
|
||||||
Order: cfgOptionDefaultActionOrder,
|
OptType: config.OptTypeString,
|
||||||
OptType: config.OptTypeString,
|
DefaultValue: "permit",
|
||||||
ReleaseLevel: config.ReleaseLevelExperimental,
|
Annotations: config.Annotations{
|
||||||
DefaultValue: "permit",
|
config.DisplayHintAnnotation: config.DisplayHintOneOf,
|
||||||
ExternalOptType: "string list",
|
config.DisplayOrderAnnotation: cfgOptionDefaultActionOrder,
|
||||||
ValidationRegex: "^(permit|ask|block)$",
|
config.CategoryAnnotation: "General",
|
||||||
|
},
|
||||||
|
PossibleValues: []config.PossibleValue{
|
||||||
|
{
|
||||||
|
Name: "Permit",
|
||||||
|
Value: "permit",
|
||||||
|
Description: "Permit all connections",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Block",
|
||||||
|
Value: "block",
|
||||||
|
Description: "Block all connections",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Prompt",
|
||||||
|
Value: "ask",
|
||||||
|
Description: "Prompt for decisions",
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -111,14 +145,19 @@ func registerConfiguration() error {
|
||||||
|
|
||||||
// Disable Auto Permit
|
// Disable Auto Permit
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Disable Auto Permit",
|
// TODO: Check how to best handle negation here.
|
||||||
Key: CfgOptionDisableAutoPermitKey,
|
Name: "Disable Auto Permit",
|
||||||
Description: "Auto Permit searches for a relation between an app and the destionation of a connection - if there is a correlation, the connection will be permitted. This setting is negated in order to provide a streamlined user experience, where higher settings are better.",
|
Key: CfgOptionDisableAutoPermitKey,
|
||||||
Order: cfgOptionDisableAutoPermitOrder,
|
Description: `Auto Permit searches for a relation between an app and the destination of a connection - if there is a correlation, the connection will be permitted.`,
|
||||||
OptType: config.OptTypeInt,
|
OptType: config.OptTypeInt,
|
||||||
ExternalOptType: "security level",
|
ReleaseLevel: config.ReleaseLevelBeta,
|
||||||
DefaultValue: status.SecurityLevelsAll,
|
DefaultValue: status.SecurityLevelsAll,
|
||||||
ValidationRegex: "^(4|6|7)$",
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionDisableAutoPermitOrder,
|
||||||
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.CategoryAnnotation: "Advanced",
|
||||||
|
},
|
||||||
|
PossibleValues: status.SecurityLevelValues,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -126,42 +165,39 @@ func registerConfiguration() error {
|
||||||
cfgOptionDisableAutoPermit = config.Concurrent.GetAsInt(CfgOptionDisableAutoPermitKey, int64(status.SecurityLevelsAll))
|
cfgOptionDisableAutoPermit = config.Concurrent.GetAsInt(CfgOptionDisableAutoPermitKey, int64(status.SecurityLevelsAll))
|
||||||
cfgIntOptions[CfgOptionDisableAutoPermitKey] = cfgOptionDisableAutoPermit
|
cfgIntOptions[CfgOptionDisableAutoPermitKey] = cfgOptionDisableAutoPermit
|
||||||
|
|
||||||
filterListHelp := `Format:
|
rulesHelp := strings.ReplaceAll(`Rules are checked from top to bottom, stopping after the first match. They can match:
|
||||||
Permission:
|
|
||||||
"+": permit
|
|
||||||
"-": block
|
|
||||||
Host Matching:
|
|
||||||
IP, CIDR, Country Code, ASN, Filterlist, Network Scope, "*" for any
|
|
||||||
Domains:
|
|
||||||
"example.com": exact match
|
|
||||||
".example.com": exact match + subdomains
|
|
||||||
"*xample.com": prefix wildcard
|
|
||||||
"example.*": suffix wildcard
|
|
||||||
"*example*": prefix and suffix wildcard
|
|
||||||
Protocol and Port Matching (optional):
|
|
||||||
<protocol>/<port>
|
|
||||||
|
|
||||||
Examples:
|
- By address: "192.168.0.1"
|
||||||
+ .example.com */HTTP
|
- By network: "192.168.0.1/24"
|
||||||
- .example.com
|
- By domain:
|
||||||
+ 192.168.0.1
|
- Matching a distinct domain: "example.com"
|
||||||
+ 192.168.1.1/24
|
- Matching a domain with subdomains: ".example.com"
|
||||||
+ Localhost,LAN
|
- Matching with a wildcard prefix: "*xample.com"
|
||||||
- AS123456789
|
- Matching with a wildcard suffix: "example.*"
|
||||||
- L:MAL
|
- Matching domains containing text: "*example*"
|
||||||
+ AT
|
- By country (based on IP): "US"
|
||||||
- *`
|
- By filter list - use the filterlist ID prefixed with "L:": "L:MAL"
|
||||||
|
- Match anything: "*"
|
||||||
|
|
||||||
|
Additionally, you may supply a protocol and port just behind that using numbers ("6/80") or names ("TCP/HTTP").
|
||||||
|
In this case the rule is only matched if the protocol and port also match.
|
||||||
|
Example: "192.168.0.1 TCP/HTTP"
|
||||||
|
`, `"`, "`")
|
||||||
|
|
||||||
// Endpoint Filter List
|
// Endpoint Filter List
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Endpoint Filter List",
|
Name: "Outgoing Rules",
|
||||||
Key: CfgOptionEndpointsKey,
|
Key: CfgOptionEndpointsKey,
|
||||||
Description: "Filter outgoing connections by matching the destination endpoint. Network Scope restrictions still apply.",
|
Description: "Rules that apply to outgoing network connections. Cannot overrule Network Scopes and Connection Types (see above).",
|
||||||
Help: filterListHelp,
|
Help: rulesHelp,
|
||||||
Order: cfgOptionEndpointsOrder,
|
OptType: config.OptTypeStringArray,
|
||||||
OptType: config.OptTypeStringArray,
|
DefaultValue: []string{},
|
||||||
DefaultValue: []string{},
|
Annotations: config.Annotations{
|
||||||
ExternalOptType: "endpoint list",
|
config.StackableAnnotation: true,
|
||||||
|
config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionEndpointsOrder,
|
||||||
|
config.CategoryAnnotation: "Rules",
|
||||||
|
},
|
||||||
ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`,
|
ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -172,14 +208,36 @@ Examples:
|
||||||
|
|
||||||
// Service Endpoint Filter List
|
// Service Endpoint Filter List
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Service Endpoint Filter List",
|
Name: "Incoming Rules",
|
||||||
Key: CfgOptionServiceEndpointsKey,
|
Key: CfgOptionServiceEndpointsKey,
|
||||||
Description: "Filter incoming connections by matching the source endpoint. Network Scope restrictions and the inbound permission still apply. Also not that the implicit default action of this list is to always block.",
|
Description: "Rules that apply to incoming network connections. Cannot overrule Network Scopes and Connection Types (see above). Also note that the default action for incoming connections is to always block.",
|
||||||
Help: filterListHelp,
|
Help: rulesHelp,
|
||||||
Order: cfgOptionServiceEndpointsOrder,
|
OptType: config.OptTypeStringArray,
|
||||||
OptType: config.OptTypeStringArray,
|
DefaultValue: []string{"+ Localhost"},
|
||||||
DefaultValue: []string{"+ Localhost"},
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ExternalOptType: "endpoint list",
|
Annotations: config.Annotations{
|
||||||
|
config.StackableAnnotation: true,
|
||||||
|
config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionServiceEndpointsOrder,
|
||||||
|
config.CategoryAnnotation: "Rules",
|
||||||
|
config.QuickSettingsAnnotation: []config.QuickSetting{
|
||||||
|
{
|
||||||
|
Name: "SSH",
|
||||||
|
Action: config.QuickMergeTop,
|
||||||
|
Value: []string{"+ * tcp/22"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "HTTP/s",
|
||||||
|
Action: config.QuickMergeTop,
|
||||||
|
Value: []string{"+ * tcp/80", "+ * tcp/443"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "RDP",
|
||||||
|
Action: config.QuickMergeTop,
|
||||||
|
Value: []string{"+ * */3389"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`,
|
ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -188,15 +246,40 @@ Examples:
|
||||||
cfgOptionServiceEndpoints = config.Concurrent.GetAsStringArray(CfgOptionServiceEndpointsKey, []string{})
|
cfgOptionServiceEndpoints = config.Concurrent.GetAsStringArray(CfgOptionServiceEndpointsKey, []string{})
|
||||||
cfgStringArrayOptions[CfgOptionServiceEndpointsKey] = cfgOptionServiceEndpoints
|
cfgStringArrayOptions[CfgOptionServiceEndpointsKey] = cfgOptionServiceEndpoints
|
||||||
|
|
||||||
|
filterListsHelp := strings.ReplaceAll(`Filter lists contain domains and IP addresses that are known to be used adversarial. The data is collected from many public sources and put into the following categories. In order to active a category, add it's "ID" to the list.
|
||||||
|
|
||||||
|
**Ads & Trackers** - ID: "TRAC"
|
||||||
|
Services that track and profile people online, including as ads, analytics and telemetry.
|
||||||
|
|
||||||
|
**Malware** - ID: "MAL"
|
||||||
|
Services that are (ab)used for attacking devices through technical means.
|
||||||
|
|
||||||
|
**Deception** - ID: "DECEP"
|
||||||
|
Services that trick humans into thinking the service is genuine, while it is not, including phishing, fake news and fraud.
|
||||||
|
|
||||||
|
**Bad Stuff (Mixed)** - ID: "BAD"
|
||||||
|
Miscellaneous services that are believed to be harmful to security or privacy, but their exact use is unknown, not categorized, or lists have mixed categories.
|
||||||
|
|
||||||
|
**NSFW** - ID: "NSFW"
|
||||||
|
Services that are generally not accepted in work environments, including pornography, violence and gambling.
|
||||||
|
|
||||||
|
The lists are automatically updated every hour using incremental updates.
|
||||||
|
[See here](https://github.com/safing/intel-data) for more detail about these lists, their sources and how to help to improve them.
|
||||||
|
`, `"`, "`")
|
||||||
|
|
||||||
// Filter list IDs
|
// Filter list IDs
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Filter List",
|
Name: "Filter Lists",
|
||||||
Key: CfgOptionFilterListsKey,
|
Key: CfgOptionFilterListsKey,
|
||||||
Description: "Filter connections by matching the endpoint against configured filterlists",
|
Description: "Block connections that match enabled filter lists.",
|
||||||
Order: cfgOptionFilterListsOrder,
|
Help: filterListsHelp,
|
||||||
OptType: config.OptTypeStringArray,
|
OptType: config.OptTypeStringArray,
|
||||||
DefaultValue: []string{"TRAC", "MAL"},
|
DefaultValue: []string{"TRAC", "MAL"},
|
||||||
ExternalOptType: "filter list",
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayHintAnnotation: "filter list",
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionFilterListsOrder,
|
||||||
|
config.CategoryAnnotation: "Rules",
|
||||||
|
},
|
||||||
ValidationRegex: `^[a-zA-Z0-9\-]+$`,
|
ValidationRegex: `^[a-zA-Z0-9\-]+$`,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -207,15 +290,18 @@ Examples:
|
||||||
|
|
||||||
// Include CNAMEs
|
// Include CNAMEs
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Filter CNAMEs",
|
Name: "Block Domain Aliases",
|
||||||
Key: CfgOptionFilterCNAMEKey,
|
Key: CfgOptionFilterCNAMEKey,
|
||||||
Description: "Also filter requests where a CNAME would be blocked",
|
Description: "Block a domain if a resolved CNAME (alias) is blocked by a rule or filter list.",
|
||||||
Order: cfgOptionFilterCNAMEOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
DefaultValue: status.SecurityLevelsAll,
|
||||||
ExternalOptType: "security level",
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
DefaultValue: status.SecurityLevelsAll,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
config.DisplayOrderAnnotation: cfgOptionFilterCNAMEOrder,
|
||||||
|
config.CategoryAnnotation: "DNS Filtering",
|
||||||
|
},
|
||||||
|
PossibleValues: status.SecurityLevelValues,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -225,14 +311,17 @@ Examples:
|
||||||
|
|
||||||
// Include subdomains
|
// Include subdomains
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Filter Subdomains",
|
Name: "Block Subdomains of Filter List Entries",
|
||||||
Key: CfgOptionFilterSubDomainsKey,
|
Key: CfgOptionFilterSubDomainsKey,
|
||||||
Description: "Also filter a domain if any parent domain is blocked by a filter list",
|
Description: "Additionally block all subdomains of entries in selected filter lists.",
|
||||||
Order: cfgOptionFilterSubDomainsOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
DefaultValue: status.SecurityLevelsAll,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.SecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelsAll,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionFilterSubDomainsOrder,
|
||||||
|
config.CategoryAnnotation: "Rules",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -242,15 +331,18 @@ Examples:
|
||||||
|
|
||||||
// Block Scope Local
|
// Block Scope Local
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Block Scope Local",
|
Name: "Block Device-Local Connections",
|
||||||
Key: CfgOptionBlockScopeLocalKey,
|
Key: CfgOptionBlockScopeLocalKey,
|
||||||
Description: "Block internal connections on your own device, ie. localhost.",
|
Description: "Block all internal connections on your own device, ie. localhost. Is stronger than Rules (see below).",
|
||||||
Order: cfgOptionBlockScopeLocalOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
DefaultValue: status.SecurityLevelOff,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.AllSecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelOff,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(0|4|6|7)$",
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionBlockScopeLocalOrder,
|
||||||
|
config.CategoryAnnotation: "Network Scope",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -260,14 +352,17 @@ Examples:
|
||||||
|
|
||||||
// Block Scope LAN
|
// Block Scope LAN
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Block Scope LAN",
|
Name: "Block LAN",
|
||||||
Key: CfgOptionBlockScopeLANKey,
|
Key: CfgOptionBlockScopeLANKey,
|
||||||
Description: "Block connections to the Local Area Network.",
|
Description: "Block all connections from and to the Local Area Network. Is stronger than Rules (see below).",
|
||||||
Order: cfgOptionBlockScopeLANOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.AllSecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(0|4|6|7)$",
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionBlockScopeLANOrder,
|
||||||
|
config.CategoryAnnotation: "Network Scope",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -277,14 +372,17 @@ Examples:
|
||||||
|
|
||||||
// Block Scope Internet
|
// Block Scope Internet
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Block Scope Internet",
|
Name: "Block Internet Access",
|
||||||
Key: CfgOptionBlockScopeInternetKey,
|
Key: CfgOptionBlockScopeInternetKey,
|
||||||
Description: "Block connections to the Internet.",
|
Description: "Block connections from and to the Internet. Is stronger than Rules (see below).",
|
||||||
Order: cfgOptionBlockScopeInternetOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
DefaultValue: status.SecurityLevelOff,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.AllSecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelOff,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(0|4|6|7)$",
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionBlockScopeInternetOrder,
|
||||||
|
config.CategoryAnnotation: "Network Scope",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -294,14 +392,17 @@ Examples:
|
||||||
|
|
||||||
// Block Peer to Peer Connections
|
// Block Peer to Peer Connections
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Block Peer to Peer Connections",
|
Name: "Block P2P/Direct Connections",
|
||||||
Key: CfgOptionBlockP2PKey,
|
Key: CfgOptionBlockP2PKey,
|
||||||
Description: "These are connections that are established directly to an IP address on the Internet without resolving a domain name via DNS first.",
|
Description: "These are connections that are established directly to an IP address or peer on the Internet without resolving a domain name via DNS first. Is stronger than Rules (see below).",
|
||||||
Order: cfgOptionBlockP2POrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
DefaultValue: status.SecurityLevelExtreme,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.SecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelExtreme,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionBlockP2POrder,
|
||||||
|
config.CategoryAnnotation: "Connection Types",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -311,14 +412,17 @@ Examples:
|
||||||
|
|
||||||
// Block Inbound Connections
|
// Block Inbound Connections
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Block Inbound Connections",
|
Name: "Block Incoming Connections",
|
||||||
Key: CfgOptionBlockInboundKey,
|
Key: CfgOptionBlockInboundKey,
|
||||||
Description: "Connections initiated towards your device from the LAN or Internet. This will usually only be the case if you are running a network service or are using peer to peer software.",
|
Description: "Connections initiated towards your device from the LAN or Internet. This will usually only be the case if you are running a network service or are using peer to peer software. Is stronger than Rules (see below).",
|
||||||
Order: cfgOptionBlockInboundOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.SecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionBlockInboundOrder,
|
||||||
|
config.CategoryAnnotation: "Connection Types",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -326,36 +430,20 @@ Examples:
|
||||||
cfgOptionBlockInbound = config.Concurrent.GetAsInt(CfgOptionBlockInboundKey, int64(status.SecurityLevelsHighAndExtreme))
|
cfgOptionBlockInbound = config.Concurrent.GetAsInt(CfgOptionBlockInboundKey, int64(status.SecurityLevelsHighAndExtreme))
|
||||||
cfgIntOptions[CfgOptionBlockInboundKey] = cfgOptionBlockInbound
|
cfgIntOptions[CfgOptionBlockInboundKey] = cfgOptionBlockInbound
|
||||||
|
|
||||||
// Enforce SPN
|
|
||||||
err = config.Register(&config.Option{
|
|
||||||
Name: "Enforce SPN",
|
|
||||||
Key: CfgOptionEnforceSPNKey,
|
|
||||||
Description: "This setting enforces connections to be routed over the SPN. If this is not possible for any reason, connections will be blocked.",
|
|
||||||
Order: cfgOptionEnforceSPNOrder,
|
|
||||||
OptType: config.OptTypeInt,
|
|
||||||
ReleaseLevel: config.ReleaseLevelExperimental,
|
|
||||||
ExternalOptType: "security level",
|
|
||||||
DefaultValue: status.SecurityLevelOff,
|
|
||||||
ValidationRegex: "^(0|4|6|7)$",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cfgOptionEnforceSPN = config.Concurrent.GetAsInt(CfgOptionEnforceSPNKey, int64(status.SecurityLevelOff))
|
|
||||||
cfgIntOptions[CfgOptionEnforceSPNKey] = cfgOptionEnforceSPN
|
|
||||||
|
|
||||||
// Filter Out-of-Scope DNS Records
|
// Filter Out-of-Scope DNS Records
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Filter Out-of-Scope DNS Records",
|
Name: "Enforce Global/Private Split-View",
|
||||||
Key: CfgOptionRemoveOutOfScopeDNSKey,
|
Key: CfgOptionRemoveOutOfScopeDNSKey,
|
||||||
Description: "Filter DNS answers that are outside of the scope of the server. A server on the public Internet may not respond with a private LAN address.",
|
Description: "Reject private IP addresses (RFC1918 et al.) from public DNS responses.",
|
||||||
Order: cfgOptionRemoveOutOfScopeDNSOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
DefaultValue: status.SecurityLevelsAll,
|
||||||
ReleaseLevel: config.ReleaseLevelBeta,
|
PossibleValues: status.SecurityLevelValues,
|
||||||
ExternalOptType: "security level",
|
Annotations: config.Annotations{
|
||||||
DefaultValue: status.SecurityLevelsAll,
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayOrderAnnotation: cfgOptionRemoveOutOfScopeDNSOrder,
|
||||||
|
config.CategoryAnnotation: "DNS Filtering",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -365,16 +453,18 @@ Examples:
|
||||||
|
|
||||||
// Filter DNS Records that would be blocked
|
// Filter DNS Records that would be blocked
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Filter DNS Records that would be blocked",
|
Name: "Reject Blocked IPs",
|
||||||
Key: CfgOptionRemoveBlockedDNSKey,
|
Key: CfgOptionRemoveBlockedDNSKey,
|
||||||
Description: "Pre-filter DNS answers that an application would not be allowed to connect to.",
|
Description: "Reject blocked IP addresses directly from the DNS response instead of handing them over to the app and blocking a resulting connection.",
|
||||||
Order: cfgOptionRemoveBlockedDNSOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
DefaultValue: status.SecurityLevelsAll,
|
||||||
ReleaseLevel: config.ReleaseLevelBeta,
|
PossibleValues: status.SecurityLevelValues,
|
||||||
ExternalOptType: "security level",
|
Annotations: config.Annotations{
|
||||||
DefaultValue: status.SecurityLevelsAll,
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayOrderAnnotation: cfgOptionRemoveBlockedDNSOrder,
|
||||||
|
config.CategoryAnnotation: "DNS Filtering",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -384,15 +474,18 @@ Examples:
|
||||||
|
|
||||||
// Domain heuristics
|
// Domain heuristics
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Enable Domain Heuristics",
|
Name: "Enable Domain Heuristics",
|
||||||
Key: CfgOptionDomainHeuristicsKey,
|
Key: CfgOptionDomainHeuristicsKey,
|
||||||
Description: "Domain Heuristics checks for suspicious looking domain names and blocks them. Ths option currently targets domains generated by malware and DNS data tunnels.",
|
Description: "Checks for suspicious domain names and blocks them. This option currently targets domain names generated by malware and DNS data exfiltration channels.",
|
||||||
Order: cfgOptionDomainHeuristicsOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
DefaultValue: status.SecurityLevelsAll,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.AllSecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelsAll,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(0|4|6|7)$",
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionDomainHeuristicsOrder,
|
||||||
|
config.CategoryAnnotation: "DNS Filtering",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -401,16 +494,22 @@ Examples:
|
||||||
|
|
||||||
// Bypass prevention
|
// Bypass prevention
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Prevent Bypassing",
|
Name: "Block Bypassing",
|
||||||
Key: CfgOptionPreventBypassingKey,
|
Key: CfgOptionPreventBypassingKey,
|
||||||
Description: "Prevent apps from bypassing the privacy filter: Firefox by disabling DNS-over-HTTPs",
|
Description: `Prevent apps from bypassing the privacy filter.
|
||||||
Order: cfgOptionPreventBypassingOrder,
|
Current Features:
|
||||||
OptType: config.OptTypeInt,
|
- Disable Firefox' internal DNS-over-HTTPs resolver
|
||||||
ExpertiseLevel: config.ExpertiseLevelUser,
|
- Block direct access to public DNS resolvers`,
|
||||||
ReleaseLevel: config.ReleaseLevelBeta,
|
OptType: config.OptTypeInt,
|
||||||
ExternalOptType: "security level",
|
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||||
DefaultValue: status.SecurityLevelsAll,
|
ReleaseLevel: config.ReleaseLevelBeta,
|
||||||
ValidationRegex: "^(4|6|7)",
|
DefaultValue: status.SecurityLevelsAll,
|
||||||
|
PossibleValues: status.SecurityLevelValues,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionPreventBypassingOrder,
|
||||||
|
config.CategoryAnnotation: "Advanced",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -418,5 +517,23 @@ Examples:
|
||||||
cfgOptionPreventBypassing = config.Concurrent.GetAsInt((CfgOptionPreventBypassingKey), int64(status.SecurityLevelsAll))
|
cfgOptionPreventBypassing = config.Concurrent.GetAsInt((CfgOptionPreventBypassingKey), int64(status.SecurityLevelsAll))
|
||||||
cfgIntOptions[CfgOptionPreventBypassingKey] = cfgOptionPreventBypassing
|
cfgIntOptions[CfgOptionPreventBypassingKey] = cfgOptionPreventBypassing
|
||||||
|
|
||||||
|
// Use SPN
|
||||||
|
err = config.Register(&config.Option{
|
||||||
|
Name: "Use SPN",
|
||||||
|
Key: CfgOptionUseSPNKey,
|
||||||
|
Description: "Route connections through the Safing Privacy Network. If it is disabled or unavailable for any reason, connections will be blocked.",
|
||||||
|
OptType: config.OptTypeBool,
|
||||||
|
DefaultValue: true,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionUseSPNOrder,
|
||||||
|
config.CategoryAnnotation: "General",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfgOptionUseSPN = config.Concurrent.GetAsBool(CfgOptionUseSPNKey, true)
|
||||||
|
cfgBoolOptions[CfgOptionUseSPNKey] = cfgOptionUseSPN
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,12 +27,12 @@ var (
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeScopedID(source, id string) string {
|
func makeScopedID(source profileSource, id string) string {
|
||||||
return source + "/" + id
|
return string(source) + "/" + id
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeProfileKey(source, id string) string {
|
func makeProfileKey(source profileSource, id string) string {
|
||||||
return profilesDBPath + source + "/" + id
|
return profilesDBPath + string(source) + "/" + id
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerValidationDBHook() (err error) {
|
func registerValidationDBHook() (err error) {
|
||||||
|
|
24
profile/endpoints/annotations.go
Normal file
24
profile/endpoints/annotations.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package endpoints
|
||||||
|
|
||||||
|
// DisplayHintEndpointList marks an option as an endpoint
|
||||||
|
// list option. It's meant to be used with DisplayHintAnnotation.
|
||||||
|
const DisplayHintEndpointList = "endpoint list"
|
||||||
|
|
||||||
|
// EndpointListAnnotation is the annotation identifier used in configuration
|
||||||
|
// options to hint the UI on available endpoint list types. If configured, only
|
||||||
|
// the specified set of entities is allowed to be used. The value is expected
|
||||||
|
// to be a single string or []string. If this annotation is missing, all
|
||||||
|
// values are expected to be allowed.
|
||||||
|
const EndpointListAnnotation = "safing/portmaster:ui:endpoint-list"
|
||||||
|
|
||||||
|
// Allowed values for the EndpointListAnnotation.
|
||||||
|
const (
|
||||||
|
EndpointListIP = "ip"
|
||||||
|
EndpointListAsn = "asn"
|
||||||
|
EndpointListCountry = "country"
|
||||||
|
EndpointListDomain = "domain"
|
||||||
|
EndpointListIPRange = "iprange"
|
||||||
|
EndpointListLists = "lists"
|
||||||
|
EndpointListScopes = "scopes"
|
||||||
|
EndpointListProtocolAndPorts = "protocol-port"
|
||||||
|
)
|
|
@ -21,9 +21,9 @@ type reason struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *reason) String() string {
|
func (r *reason) String() string {
|
||||||
prefix := "endpoint in blocklist: "
|
prefix := "denied by rule: "
|
||||||
if r.Permitted {
|
if r.Permitted {
|
||||||
prefix = "endpoint in allowlist: "
|
prefix = "permitted by rule: "
|
||||||
}
|
}
|
||||||
|
|
||||||
return prefix + r.description + " " + r.Value
|
return prefix + r.description + " " + r.Value
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
package profile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/safing/portbase/database/query"
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FindOrCreateLocalProfileByPath returns an existing or new profile for the given application path.
|
|
||||||
func FindOrCreateLocalProfileByPath(fullPath string) (profile *Profile, new bool, err error) {
|
|
||||||
// find local profile
|
|
||||||
it, err := profileDB.Query(
|
|
||||||
query.New(makeProfileKey(SourceLocal, "")).Where(
|
|
||||||
query.Where("LinkedPath", query.SameAs, fullPath),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get first result
|
|
||||||
r := <-it.Next
|
|
||||||
// cancel immediately
|
|
||||||
it.Cancel()
|
|
||||||
|
|
||||||
// return new if none was found
|
|
||||||
if r == nil {
|
|
||||||
profile = New()
|
|
||||||
profile.LinkedPath = fullPath
|
|
||||||
return profile, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure its a profile
|
|
||||||
profile, err = EnsureProfile(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare config
|
|
||||||
err = profile.prepConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse config
|
|
||||||
err = profile.parseConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mark active
|
|
||||||
markProfileActive(profile)
|
|
||||||
|
|
||||||
// return parsed profile
|
|
||||||
return profile, false, nil
|
|
||||||
}
|
|
204
profile/get.go
Normal file
204
profile/get.go
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
package profile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/dataroot"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database/query"
|
||||||
|
"github.com/safing/portbase/database/record"
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnidentifiedProfileID is the profile ID used for unidentified processes.
|
||||||
|
UnidentifiedProfileID = "_unidentified"
|
||||||
|
|
||||||
|
// SystemProfileID is the profile ID used for the system/kernel.
|
||||||
|
SystemProfileID = "_system"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getProfileSingleInflight singleflight.Group
|
||||||
|
|
||||||
|
// GetProfile fetches a profile. This function ensure that the profile loaded
|
||||||
|
// is shared among all callers. You must always supply both the scopedID and
|
||||||
|
// linkedPath parameters whenever available.
|
||||||
|
func GetProfile(source profileSource, id, linkedPath string) ( //nolint:gocognit
|
||||||
|
profile *Profile,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
// Select correct key for single in flight.
|
||||||
|
singleInflightKey := linkedPath
|
||||||
|
if singleInflightKey == "" {
|
||||||
|
singleInflightKey = makeScopedID(source, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err, _ := getProfileSingleInflight.Do(singleInflightKey, func() (interface{}, error) {
|
||||||
|
var previousVersion *Profile
|
||||||
|
|
||||||
|
// Fetch profile depending on the available information.
|
||||||
|
switch {
|
||||||
|
case id != "":
|
||||||
|
scopedID := makeScopedID(source, id)
|
||||||
|
|
||||||
|
// Get profile via the scoped ID.
|
||||||
|
// Check if there already is an active and not outdated profile.
|
||||||
|
profile = getActiveProfile(scopedID)
|
||||||
|
if profile != nil {
|
||||||
|
profile.MarkStillActive()
|
||||||
|
|
||||||
|
if profile.outdated.IsSet() {
|
||||||
|
previousVersion = profile
|
||||||
|
} else {
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Get from database.
|
||||||
|
profile, err = getProfile(scopedID)
|
||||||
|
|
||||||
|
// If we cannot find a profile, check if the request is for a special
|
||||||
|
// profile we can create.
|
||||||
|
if errors.Is(err, database.ErrNotFound) {
|
||||||
|
switch id {
|
||||||
|
case UnidentifiedProfileID:
|
||||||
|
profile = New(SourceLocal, UnidentifiedProfileID, linkedPath)
|
||||||
|
err = nil
|
||||||
|
case SystemProfileID:
|
||||||
|
profile = New(SourceLocal, SystemProfileID, linkedPath)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case linkedPath != "":
|
||||||
|
// Search for profile via a linked path.
|
||||||
|
// Check if there already is an active and not outdated profile for
|
||||||
|
// the linked path.
|
||||||
|
profile = findActiveProfile(linkedPath)
|
||||||
|
if profile != nil {
|
||||||
|
if profile.outdated.IsSet() {
|
||||||
|
previousVersion = profile
|
||||||
|
} else {
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Get from database.
|
||||||
|
profile, err = findProfile(linkedPath)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.New("cannot fetch profile without ID or path")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process profiles coming directly from the database.
|
||||||
|
// As we don't use any caching, these will be new objects.
|
||||||
|
|
||||||
|
// Mark the profile as being saved internally in order to not trigger an
|
||||||
|
// update after saving it to the database.
|
||||||
|
profile.internalSave = true
|
||||||
|
|
||||||
|
// Add a layeredProfile to local profiles.
|
||||||
|
if profile.Source == SourceLocal {
|
||||||
|
// If we are refetching, assign the layered profile from the previous version.
|
||||||
|
if previousVersion != nil {
|
||||||
|
profile.layeredProfile = previousVersion.layeredProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local profiles must have a layered profile, create a new one if it
|
||||||
|
// does not yet exist.
|
||||||
|
if profile.layeredProfile == nil {
|
||||||
|
profile.layeredProfile = NewLayeredProfile(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the profile to the currently active profiles.
|
||||||
|
addActiveProfile(profile)
|
||||||
|
|
||||||
|
return profile, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if p == nil {
|
||||||
|
return nil, errors.New("profile getter returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.(*Profile), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProfile fetches the profile for the given scoped ID.
|
||||||
|
func getProfile(scopedID string) (profile *Profile, err error) {
|
||||||
|
// Get profile from the database.
|
||||||
|
r, err := profileDB.Get(profilesDBPath + scopedID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and prepare the profile, return the result.
|
||||||
|
return prepProfile(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findProfile searches for a profile with the given linked path. If it cannot
|
||||||
|
// find one, it will create a new profile for the given linked path.
|
||||||
|
func findProfile(linkedPath string) (profile *Profile, err error) {
|
||||||
|
// Search the database for a matching profile.
|
||||||
|
it, err := profileDB.Query(
|
||||||
|
query.New(makeProfileKey(SourceLocal, "")).Where(
|
||||||
|
query.Where("LinkedPath", query.SameAs, linkedPath),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only wait for the first result, or until the query ends.
|
||||||
|
r := <-it.Next
|
||||||
|
// Then cancel the query, should it still be running.
|
||||||
|
it.Cancel()
|
||||||
|
|
||||||
|
// Prep and return an existing profile.
|
||||||
|
if r != nil {
|
||||||
|
profile, err = prepProfile(r)
|
||||||
|
return profile, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was no profile in the database, create a new one, and return it.
|
||||||
|
profile = New(SourceLocal, "", linkedPath)
|
||||||
|
|
||||||
|
// Check if the profile should be marked as internal.
|
||||||
|
// This is the case whenever the binary resides within the data root dir.
|
||||||
|
if strings.HasPrefix(linkedPath, dataroot.Root().Dir+string(os.PathSeparator)) {
|
||||||
|
profile.Internal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepProfile(r record.Record) (*Profile, error) {
|
||||||
|
// ensure its a profile
|
||||||
|
profile, err := EnsureProfile(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare config
|
||||||
|
err = profile.prepConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse config
|
||||||
|
err = profile.parseConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return parsed profile
|
||||||
|
return profile, nil
|
||||||
|
}
|
|
@ -38,6 +38,11 @@ func start() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = registerRevisionProvider()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = startProfileUpdateChecker()
|
err = startProfileUpdateChecker()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package profile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/core/pmtesting"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
pmtesting.TestMain(m, module)
|
|
||||||
}
|
|
83
profile/profile-layered-provider.go
Normal file
83
profile/profile-layered-provider.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package profile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database/record"
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
revisionProviderPrefix = "layeredProfile/"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errProfileNotActive = errors.New("profile not active")
|
||||||
|
errNoLayeredProfile = errors.New("profile has no layered profile")
|
||||||
|
pushLayeredProfile runtime.PushFunc = func(...record.Record) {}
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerRevisionProvider() error {
|
||||||
|
push, err := runtime.Register(
|
||||||
|
revisionProviderPrefix,
|
||||||
|
runtime.SimpleValueGetterFunc(getRevisions),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pushLayeredProfile = push
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRevisions(key string) ([]record.Record, error) {
|
||||||
|
key = strings.TrimPrefix(key, revisionProviderPrefix)
|
||||||
|
|
||||||
|
var profiles []*Profile
|
||||||
|
|
||||||
|
if key == "" {
|
||||||
|
profiles = getAllActiveProfiles()
|
||||||
|
} else {
|
||||||
|
// Get active profile.
|
||||||
|
profile := getActiveProfile(key)
|
||||||
|
if profile == nil {
|
||||||
|
return nil, errProfileNotActive
|
||||||
|
}
|
||||||
|
profiles = append(profiles, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
records := make([]record.Record, 0, len(profiles))
|
||||||
|
|
||||||
|
for _, p := range profiles {
|
||||||
|
layered, err := getProfileRevision(p)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed to get layered profile for %s: %s", p.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, layered)
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProfileRevision returns the layered profile for p.
|
||||||
|
// It also updates the layered profile if required.
|
||||||
|
func getProfileRevision(p *Profile) (*LayeredProfile, error) {
|
||||||
|
// Get layered profile.
|
||||||
|
layeredProfile := p.LayeredProfile()
|
||||||
|
if layeredProfile == nil {
|
||||||
|
return nil, errNoLayeredProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update profiles if necessary.
|
||||||
|
if layeredProfile.NeedsUpdate() {
|
||||||
|
layeredProfile.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
return layeredProfile, nil
|
||||||
|
}
|
|
@ -5,48 +5,51 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database/record"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/runtime"
|
||||||
|
|
||||||
"github.com/safing/portmaster/status"
|
"github.com/safing/portmaster/status"
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/config"
|
"github.com/safing/portbase/config"
|
||||||
"github.com/safing/portmaster/intel"
|
"github.com/safing/portmaster/intel"
|
||||||
"github.com/safing/portmaster/profile/endpoints"
|
"github.com/safing/portmaster/profile/endpoints"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
no = abool.NewBool(false)
|
|
||||||
)
|
|
||||||
|
|
||||||
// LayeredProfile combines multiple Profiles.
|
// LayeredProfile combines multiple Profiles.
|
||||||
type LayeredProfile struct {
|
type LayeredProfile struct {
|
||||||
lock sync.Mutex
|
record.Base
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
localProfile *Profile
|
localProfile *Profile
|
||||||
layers []*Profile
|
layers []*Profile
|
||||||
revisionCounter uint64
|
|
||||||
|
|
||||||
validityFlag *abool.AtomicBool
|
LayerIDs []string
|
||||||
validityFlagLock sync.Mutex
|
RevisionCounter uint64
|
||||||
globalValidityFlag *config.ValidityFlag
|
globalValidityFlag *config.ValidityFlag
|
||||||
|
|
||||||
securityLevel *uint32
|
securityLevel *uint32
|
||||||
|
|
||||||
DisableAutoPermit config.BoolOption
|
// These functions give layered access to configuration options and require
|
||||||
BlockScopeLocal config.BoolOption
|
// the layered profile to be read locked.
|
||||||
BlockScopeLAN config.BoolOption
|
|
||||||
BlockScopeInternet config.BoolOption
|
// TODO(ppacher): we need JSON tags here so the layeredProfile can be exposed
|
||||||
BlockP2P config.BoolOption
|
// via the API. If we ever switch away from JSON to something else supported
|
||||||
BlockInbound config.BoolOption
|
// by DSD this WILL BREAK!
|
||||||
EnforceSPN config.BoolOption
|
|
||||||
RemoveOutOfScopeDNS config.BoolOption
|
DisableAutoPermit config.BoolOption `json:"-"`
|
||||||
RemoveBlockedDNS config.BoolOption
|
BlockScopeLocal config.BoolOption `json:"-"`
|
||||||
FilterSubDomains config.BoolOption
|
BlockScopeLAN config.BoolOption `json:"-"`
|
||||||
FilterCNAMEs config.BoolOption
|
BlockScopeInternet config.BoolOption `json:"-"`
|
||||||
PreventBypassing config.BoolOption
|
BlockP2P config.BoolOption `json:"-"`
|
||||||
DomainHeuristics config.BoolOption
|
BlockInbound config.BoolOption `json:"-"`
|
||||||
|
RemoveOutOfScopeDNS config.BoolOption `json:"-"`
|
||||||
|
RemoveBlockedDNS config.BoolOption `json:"-"`
|
||||||
|
FilterSubDomains config.BoolOption `json:"-"`
|
||||||
|
FilterCNAMEs config.BoolOption `json:"-"`
|
||||||
|
PreventBypassing config.BoolOption `json:"-"`
|
||||||
|
DomainHeuristics config.BoolOption `json:"-"`
|
||||||
|
UseSPN config.BoolOption `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLayeredProfile returns a new layered profile based on the given local profile.
|
// NewLayeredProfile returns a new layered profile based on the given local profile.
|
||||||
|
@ -56,8 +59,7 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
||||||
new := &LayeredProfile{
|
new := &LayeredProfile{
|
||||||
localProfile: localProfile,
|
localProfile: localProfile,
|
||||||
layers: make([]*Profile, 0, len(localProfile.LinkedProfiles)+1),
|
layers: make([]*Profile, 0, len(localProfile.LinkedProfiles)+1),
|
||||||
revisionCounter: 0,
|
LayerIDs: make([]string, 0, len(localProfile.LinkedProfiles)+1),
|
||||||
validityFlag: abool.NewBool(true),
|
|
||||||
globalValidityFlag: config.NewValidityFlag(),
|
globalValidityFlag: config.NewValidityFlag(),
|
||||||
securityLevel: &securityLevelVal,
|
securityLevel: &securityLevelVal,
|
||||||
}
|
}
|
||||||
|
@ -86,10 +88,6 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
||||||
CfgOptionBlockInboundKey,
|
CfgOptionBlockInboundKey,
|
||||||
cfgOptionBlockInbound,
|
cfgOptionBlockInbound,
|
||||||
)
|
)
|
||||||
new.EnforceSPN = new.wrapSecurityLevelOption(
|
|
||||||
CfgOptionEnforceSPNKey,
|
|
||||||
cfgOptionEnforceSPN,
|
|
||||||
)
|
|
||||||
new.RemoveOutOfScopeDNS = new.wrapSecurityLevelOption(
|
new.RemoveOutOfScopeDNS = new.wrapSecurityLevelOption(
|
||||||
CfgOptionRemoveOutOfScopeDNSKey,
|
CfgOptionRemoveOutOfScopeDNSKey,
|
||||||
cfgOptionRemoveOutOfScopeDNS,
|
cfgOptionRemoveOutOfScopeDNS,
|
||||||
|
@ -114,22 +112,56 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
||||||
CfgOptionDomainHeuristicsKey,
|
CfgOptionDomainHeuristicsKey,
|
||||||
cfgOptionDomainHeuristics,
|
cfgOptionDomainHeuristics,
|
||||||
)
|
)
|
||||||
|
new.UseSPN = new.wrapBoolOption(
|
||||||
|
CfgOptionUseSPNKey,
|
||||||
|
cfgOptionUseSPN,
|
||||||
|
)
|
||||||
|
|
||||||
// TODO: load linked profiles.
|
new.LayerIDs = append(new.LayerIDs, localProfile.ScopedID())
|
||||||
|
|
||||||
// FUTURE: load forced company profile
|
|
||||||
new.layers = append(new.layers, localProfile)
|
new.layers = append(new.layers, localProfile)
|
||||||
// FUTURE: load company profile
|
|
||||||
// FUTURE: load community profile
|
// TODO: Load additional profiles.
|
||||||
|
|
||||||
new.updateCaches()
|
new.updateCaches()
|
||||||
|
|
||||||
|
new.CreateMeta()
|
||||||
|
new.SetKey(runtime.DefaultRegistry.DatabaseName() + ":" + revisionProviderPrefix + localProfile.ScopedID())
|
||||||
|
|
||||||
|
// Inform database subscribers about the new layered profile.
|
||||||
|
new.Lock()
|
||||||
|
defer new.Unlock()
|
||||||
|
|
||||||
|
pushLayeredProfile(new)
|
||||||
|
|
||||||
return new
|
return new
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lp *LayeredProfile) getValidityFlag() *abool.AtomicBool {
|
// LockForUsage locks the layered profile, including all layers individually.
|
||||||
lp.validityFlagLock.Lock()
|
func (lp *LayeredProfile) LockForUsage() {
|
||||||
defer lp.validityFlagLock.Unlock()
|
lp.RLock()
|
||||||
return lp.validityFlag
|
for _, layer := range lp.layers {
|
||||||
|
layer.RLock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockForUsage unlocks the layered profile, including all layers individually.
|
||||||
|
func (lp *LayeredProfile) UnlockForUsage() {
|
||||||
|
lp.RUnlock()
|
||||||
|
for _, layer := range lp.layers {
|
||||||
|
layer.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalProfile returns the local profile associated with this layered profile.
|
||||||
|
func (lp *LayeredProfile) LocalProfile() *Profile {
|
||||||
|
if lp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.RLock()
|
||||||
|
defer lp.RUnlock()
|
||||||
|
|
||||||
|
return lp.localProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevisionCnt returns the current profile revision counter.
|
// RevisionCnt returns the current profile revision counter.
|
||||||
|
@ -138,23 +170,57 @@ func (lp *LayeredProfile) RevisionCnt() (revisionCounter uint64) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
lp.lock.Lock()
|
lp.RLock()
|
||||||
defer lp.lock.Unlock()
|
defer lp.RUnlock()
|
||||||
|
|
||||||
return lp.revisionCounter
|
return lp.RevisionCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update checks for updated profiles and replaces any outdated profiles.
|
// MarkStillActive marks all the layers as still active.
|
||||||
|
func (lp *LayeredProfile) MarkStillActive() {
|
||||||
|
if lp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.RLock()
|
||||||
|
defer lp.RUnlock()
|
||||||
|
|
||||||
|
for _, layer := range lp.layers {
|
||||||
|
layer.MarkStillActive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsUpdate checks for outdated profiles.
|
||||||
|
func (lp *LayeredProfile) NeedsUpdate() (outdated bool) {
|
||||||
|
lp.RLock()
|
||||||
|
defer lp.RUnlock()
|
||||||
|
|
||||||
|
// Check global config state.
|
||||||
|
if !lp.globalValidityFlag.IsValid() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check config in layers.
|
||||||
|
for _, layer := range lp.layers {
|
||||||
|
if layer.outdated.IsSet() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checks for and replaces any outdated profiles.
|
||||||
func (lp *LayeredProfile) Update() (revisionCounter uint64) {
|
func (lp *LayeredProfile) Update() (revisionCounter uint64) {
|
||||||
lp.lock.Lock()
|
lp.Lock()
|
||||||
defer lp.lock.Unlock()
|
defer lp.Unlock()
|
||||||
|
|
||||||
var changed bool
|
var changed bool
|
||||||
for i, layer := range lp.layers {
|
for i, layer := range lp.layers {
|
||||||
if layer.outdated.IsSet() {
|
if layer.outdated.IsSet() {
|
||||||
changed = true
|
changed = true
|
||||||
// update layer
|
// update layer
|
||||||
newLayer, err := GetProfile(layer.Source, layer.ID)
|
newLayer, err := GetProfile(layer.Source, layer.ID, layer.LinkedPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("profiles: failed to update profile %s", layer.ScopedID())
|
log.Errorf("profiles: failed to update profile %s", layer.ScopedID())
|
||||||
} else {
|
} else {
|
||||||
|
@ -167,11 +233,6 @@ func (lp *LayeredProfile) Update() (revisionCounter uint64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
// reset validity flag
|
|
||||||
lp.validityFlagLock.Lock()
|
|
||||||
lp.validityFlag.SetTo(false)
|
|
||||||
lp.validityFlag = abool.NewBool(true)
|
|
||||||
lp.validityFlagLock.Unlock()
|
|
||||||
// get global config validity flag
|
// get global config validity flag
|
||||||
lp.globalValidityFlag.Refresh()
|
lp.globalValidityFlag.Refresh()
|
||||||
|
|
||||||
|
@ -179,10 +240,12 @@ func (lp *LayeredProfile) Update() (revisionCounter uint64) {
|
||||||
lp.updateCaches()
|
lp.updateCaches()
|
||||||
|
|
||||||
// bump revision counter
|
// bump revision counter
|
||||||
lp.revisionCounter++
|
lp.RevisionCounter++
|
||||||
|
|
||||||
|
pushLayeredProfile(lp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return lp.revisionCounter
|
return lp.RevisionCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lp *LayeredProfile) updateCaches() {
|
func (lp *LayeredProfile) updateCaches() {
|
||||||
|
@ -194,8 +257,6 @@ func (lp *LayeredProfile) updateCaches() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
atomic.StoreUint32(lp.securityLevel, uint32(newLevel))
|
atomic.StoreUint32(lp.securityLevel, uint32(newLevel))
|
||||||
|
|
||||||
// TODO: ignore community profiles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkUsed marks the localProfile as used.
|
// MarkUsed marks the localProfile as used.
|
||||||
|
@ -203,12 +264,12 @@ func (lp *LayeredProfile) MarkUsed() {
|
||||||
lp.localProfile.MarkUsed()
|
lp.localProfile.MarkUsed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecurityLevel returns the highest security level of all layered profiles.
|
// SecurityLevel returns the highest security level of all layered profiles. This function is atomic and does not require any locking.
|
||||||
func (lp *LayeredProfile) SecurityLevel() uint8 {
|
func (lp *LayeredProfile) SecurityLevel() uint8 {
|
||||||
return uint8(atomic.LoadUint32(lp.securityLevel))
|
return uint8(atomic.LoadUint32(lp.securityLevel))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultAction returns the active default action ID.
|
// DefaultAction returns the active default action ID. This functions requires the layered profile to be read locked.
|
||||||
func (lp *LayeredProfile) DefaultAction() uint8 {
|
func (lp *LayeredProfile) DefaultAction() uint8 {
|
||||||
for _, layer := range lp.layers {
|
for _, layer := range lp.layers {
|
||||||
if layer.defaultAction > 0 {
|
if layer.defaultAction > 0 {
|
||||||
|
@ -221,7 +282,7 @@ func (lp *LayeredProfile) DefaultAction() uint8 {
|
||||||
return cfgDefaultAction
|
return cfgDefaultAction
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchEndpoint checks if the given endpoint matches an entry in any of the profiles.
|
// MatchEndpoint checks if the given endpoint matches an entry in any of the profiles. This functions requires the layered profile to be read locked.
|
||||||
func (lp *LayeredProfile) MatchEndpoint(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
|
func (lp *LayeredProfile) MatchEndpoint(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
|
||||||
for _, layer := range lp.layers {
|
for _, layer := range lp.layers {
|
||||||
if layer.endpoints.IsSet() {
|
if layer.endpoints.IsSet() {
|
||||||
|
@ -237,7 +298,7 @@ func (lp *LayeredProfile) MatchEndpoint(ctx context.Context, entity *intel.Entit
|
||||||
return cfgEndpoints.Match(ctx, entity)
|
return cfgEndpoints.Match(ctx, entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchServiceEndpoint checks if the given endpoint of an inbound connection matches an entry in any of the profiles.
|
// MatchServiceEndpoint checks if the given endpoint of an inbound connection matches an entry in any of the profiles. This functions requires the layered profile to be read locked.
|
||||||
func (lp *LayeredProfile) MatchServiceEndpoint(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
|
func (lp *LayeredProfile) MatchServiceEndpoint(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
|
||||||
entity.EnableReverseResolving()
|
entity.EnableReverseResolving()
|
||||||
|
|
||||||
|
@ -256,7 +317,7 @@ func (lp *LayeredProfile) MatchServiceEndpoint(ctx context.Context, entity *inte
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchFilterLists matches the entity against the set of filter
|
// MatchFilterLists matches the entity against the set of filter
|
||||||
// lists.
|
// lists. This functions requires the layered profile to be read locked.
|
||||||
func (lp *LayeredProfile) MatchFilterLists(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
|
func (lp *LayeredProfile) MatchFilterLists(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
|
||||||
entity.ResolveSubDomainLists(ctx, lp.FilterSubDomains())
|
entity.ResolveSubDomainLists(ctx, lp.FilterSubDomains())
|
||||||
entity.EnableCNAMECheck(ctx, lp.FilterCNAMEs())
|
entity.EnableCNAMECheck(ctx, lp.FilterCNAMEs())
|
||||||
|
@ -287,16 +348,6 @@ func (lp *LayeredProfile) MatchFilterLists(ctx context.Context, entity *intel.En
|
||||||
return endpoints.NoMatch, nil
|
return endpoints.NoMatch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddEndpoint adds an endpoint to the local endpoint list, saves the local profile and reloads the configuration.
|
|
||||||
func (lp *LayeredProfile) AddEndpoint(newEntry string) {
|
|
||||||
lp.localProfile.AddEndpoint(newEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddServiceEndpoint adds a service endpoint to the local endpoint list, saves the local profile and reloads the configuration.
|
|
||||||
func (lp *LayeredProfile) AddServiceEndpoint(newEntry string) {
|
|
||||||
lp.localProfile.AddServiceEndpoint(newEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lp *LayeredProfile) wrapSecurityLevelOption(configKey string, globalConfig config.IntOption) config.BoolOption {
|
func (lp *LayeredProfile) wrapSecurityLevelOption(configKey string, globalConfig config.IntOption) config.BoolOption {
|
||||||
activeAtLevels := lp.wrapIntOption(configKey, globalConfig)
|
activeAtLevels := lp.wrapIntOption(configKey, globalConfig)
|
||||||
|
|
||||||
|
@ -308,22 +359,27 @@ func (lp *LayeredProfile) wrapSecurityLevelOption(configKey string, globalConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.IntOption) config.IntOption {
|
func (lp *LayeredProfile) wrapBoolOption(configKey string, globalConfig config.BoolOption) config.BoolOption {
|
||||||
valid := no
|
revCnt := lp.RevisionCounter
|
||||||
var value int64
|
var value bool
|
||||||
|
var refreshLock sync.Mutex
|
||||||
|
|
||||||
return func() int64 {
|
return func() bool {
|
||||||
if !valid.IsSet() {
|
refreshLock.Lock()
|
||||||
valid = lp.getValidityFlag()
|
defer refreshLock.Unlock()
|
||||||
|
|
||||||
|
// Check if we need to refresh the value.
|
||||||
|
if revCnt != lp.RevisionCounter {
|
||||||
|
revCnt = lp.RevisionCounter
|
||||||
|
|
||||||
|
// Go through all layers to find an active value.
|
||||||
found := false
|
found := false
|
||||||
layerLoop:
|
|
||||||
for _, layer := range lp.layers {
|
for _, layer := range lp.layers {
|
||||||
layerValue, ok := layer.configPerspective.GetAsInt(configKey)
|
layerValue, ok := layer.configPerspective.GetAsBool(configKey)
|
||||||
if ok {
|
if ok {
|
||||||
found = true
|
found = true
|
||||||
value = layerValue
|
value = layerValue
|
||||||
break layerLoop
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
|
@ -335,25 +391,76 @@ func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.In
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.IntOption) config.IntOption {
|
||||||
|
revCnt := lp.RevisionCounter
|
||||||
|
var value int64
|
||||||
|
var refreshLock sync.Mutex
|
||||||
|
|
||||||
|
return func() int64 {
|
||||||
|
refreshLock.Lock()
|
||||||
|
defer refreshLock.Unlock()
|
||||||
|
|
||||||
|
// Check if we need to refresh the value.
|
||||||
|
if revCnt != lp.RevisionCounter {
|
||||||
|
revCnt = lp.RevisionCounter
|
||||||
|
|
||||||
|
// Go through all layers to find an active value.
|
||||||
|
found := false
|
||||||
|
for _, layer := range lp.layers {
|
||||||
|
layerValue, ok := layer.configPerspective.GetAsInt(configKey)
|
||||||
|
if ok {
|
||||||
|
found = true
|
||||||
|
value = layerValue
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
value = globalConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProfileSource returns the database key of the first profile in the
|
||||||
|
// layers that has the given configuration key set. If it returns an empty
|
||||||
|
// string, the global profile can be assumed to have been effective.
|
||||||
|
func (lp *LayeredProfile) GetProfileSource(configKey string) string {
|
||||||
|
for _, layer := range lp.layers {
|
||||||
|
if layer.configPerspective.Has(configKey) {
|
||||||
|
return layer.Key()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global Profile
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
For later:
|
For later:
|
||||||
|
|
||||||
func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption {
|
func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption {
|
||||||
valid := no
|
revCnt := lp.RevisionCounter
|
||||||
var value string
|
var value string
|
||||||
|
var refreshLock sync.Mutex
|
||||||
|
|
||||||
return func() string {
|
return func() string {
|
||||||
if !valid.IsSet() {
|
refreshLock.Lock()
|
||||||
valid = lp.getValidityFlag()
|
defer refreshLock.Unlock()
|
||||||
|
|
||||||
|
// Check if we need to refresh the value.
|
||||||
|
if revCnt != lp.RevisionCounter {
|
||||||
|
revCnt = lp.RevisionCounter
|
||||||
|
|
||||||
|
// Go through all layers to find an active value.
|
||||||
found := false
|
found := false
|
||||||
layerLoop:
|
|
||||||
for _, layer := range lp.layers {
|
for _, layer := range lp.layers {
|
||||||
layerValue, ok := layer.configPerspective.GetAsString(configKey)
|
layerValue, ok := layer.configPerspective.GetAsString(configKey)
|
||||||
if ok {
|
if ok {
|
||||||
found = true
|
found = true
|
||||||
value = layerValue
|
value = layerValue
|
||||||
break layerLoop
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
package profile
|
package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
"github.com/tevino/abool"
|
||||||
|
@ -12,6 +16,7 @@ import (
|
||||||
"github.com/safing/portbase/database/record"
|
"github.com/safing/portbase/database/record"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/utils"
|
"github.com/safing/portbase/utils"
|
||||||
|
"github.com/safing/portbase/utils/osdetail"
|
||||||
"github.com/safing/portmaster/intel/filterlists"
|
"github.com/safing/portmaster/intel/filterlists"
|
||||||
"github.com/safing/portmaster/profile/endpoints"
|
"github.com/safing/portmaster/profile/endpoints"
|
||||||
)
|
)
|
||||||
|
@ -20,12 +25,15 @@ var (
|
||||||
lastUsedUpdateThreshold = 24 * time.Hour
|
lastUsedUpdateThreshold = 24 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// profileSource is the source of the profile.
|
||||||
|
type profileSource string
|
||||||
|
|
||||||
// Profile Sources
|
// Profile Sources
|
||||||
const (
|
const (
|
||||||
SourceLocal string = "local" // local, editable
|
SourceLocal profileSource = "local" // local, editable
|
||||||
SourceSpecial string = "special" // specials (read-only)
|
SourceSpecial profileSource = "special" // specials (read-only)
|
||||||
SourceCommunity string = "community"
|
SourceCommunity profileSource = "community"
|
||||||
SourceEnterprise string = "enterprise"
|
SourceEnterprise profileSource = "enterprise"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default Action IDs
|
// Default Action IDs
|
||||||
|
@ -36,35 +44,76 @@ const (
|
||||||
DefaultActionPermit uint8 = 3
|
DefaultActionPermit uint8 = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// iconType describes the type of the Icon property
|
||||||
|
// of a profile.
|
||||||
|
type iconType string
|
||||||
|
|
||||||
|
// Supported icon types.
|
||||||
|
const (
|
||||||
|
IconTypeFile iconType = "path"
|
||||||
|
IconTypeDatabase iconType = "database"
|
||||||
|
IconTypeBlob iconType = "blob"
|
||||||
|
)
|
||||||
|
|
||||||
// Profile is used to predefine a security profile for applications.
|
// Profile is used to predefine a security profile for applications.
|
||||||
type Profile struct { //nolint:maligned // not worth the effort
|
type Profile struct { //nolint:maligned // not worth the effort
|
||||||
record.Base
|
record.Base
|
||||||
sync.Mutex
|
sync.RWMutex
|
||||||
|
|
||||||
// Identity
|
// ID is a unique identifier for the profile.
|
||||||
ID string
|
ID string // constant
|
||||||
Source string
|
// Source describes the source of the profile.
|
||||||
|
Source profileSource // constant
|
||||||
// App Information
|
// Name is a human readable name of the profile. It
|
||||||
Name string
|
// defaults to the basename of the application.
|
||||||
|
Name string
|
||||||
|
// Description may holds an optional description of the
|
||||||
|
// profile or the purpose of the application.
|
||||||
Description string
|
Description string
|
||||||
Homepage string
|
// Homepage may refer the the website of the application
|
||||||
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for a database path or "e:" for the encoded data.
|
// vendor.
|
||||||
|
Homepage string
|
||||||
|
// Icon holds the icon of the application. The value
|
||||||
|
// may either be a filepath, a database key or a blob URL.
|
||||||
|
// See IconType for more information.
|
||||||
Icon string
|
Icon string
|
||||||
|
// IconType describes the type of the Icon property.
|
||||||
// References - local profiles only
|
IconType iconType
|
||||||
// LinkedPath is a filesystem path to the executable this profile was created for.
|
// LinkedPath is a filesystem path to the executable this
|
||||||
LinkedPath string
|
// profile was created for.
|
||||||
|
LinkedPath string // constant
|
||||||
// LinkedProfiles is a list of other profiles
|
// LinkedProfiles is a list of other profiles
|
||||||
LinkedProfiles []string
|
LinkedProfiles []string
|
||||||
|
// SecurityLevel is the mininum security level to apply to
|
||||||
// Fingerprints
|
// connections made with this profile.
|
||||||
// TODO: Fingerprints []*Fingerprint
|
// Note(ppacher): we may deprecate this one as it can easily
|
||||||
|
// be "simulated" by adjusting the settings
|
||||||
// Configuration
|
// directly.
|
||||||
// The mininum security level to apply to connections made with this profile
|
|
||||||
SecurityLevel uint8
|
SecurityLevel uint8
|
||||||
Config map[string]interface{}
|
// Config holds profile specific setttings. It's a nested
|
||||||
|
// object with keys defining the settings database path. All keys
|
||||||
|
// until the actual settings value (which is everything that is not
|
||||||
|
// an object) need to be concatenated for the settings database
|
||||||
|
// path.
|
||||||
|
Config map[string]interface{}
|
||||||
|
// ApproxLastUsed holds a UTC timestamp in seconds of
|
||||||
|
// when this Profile was approximately last used.
|
||||||
|
// For performance reasons not every single usage is saved.
|
||||||
|
ApproxLastUsed int64
|
||||||
|
// Created holds the UTC timestamp in seconds when the
|
||||||
|
// profile has been created.
|
||||||
|
Created int64
|
||||||
|
|
||||||
|
// Internal is set to true if the profile is attributed to a
|
||||||
|
// Portmaster internal process. Internal is set during profile
|
||||||
|
// creation and may be accessed without lock.
|
||||||
|
Internal bool
|
||||||
|
|
||||||
|
// layeredProfile is a link to the layered profile with this profile as the
|
||||||
|
// main profile.
|
||||||
|
// All processes with the same binary should share the same instance of the
|
||||||
|
// local profile and the associated layered profile.
|
||||||
|
layeredProfile *LayeredProfile
|
||||||
|
|
||||||
// Interpreted Data
|
// Interpreted Data
|
||||||
configPerspective *config.Perspective
|
configPerspective *config.Perspective
|
||||||
|
@ -75,17 +124,8 @@ type Profile struct { //nolint:maligned // not worth the effort
|
||||||
filterListIDs []string
|
filterListIDs []string
|
||||||
|
|
||||||
// Lifecycle Management
|
// Lifecycle Management
|
||||||
outdated *abool.AtomicBool
|
outdated *abool.AtomicBool
|
||||||
lastUsed time.Time
|
lastActive *int64
|
||||||
|
|
||||||
// Framework
|
|
||||||
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process/actor must be found
|
|
||||||
// TODO: Framework *Framework
|
|
||||||
|
|
||||||
// When this Profile was approximately last used.
|
|
||||||
// For performance reasons not every single usage is saved.
|
|
||||||
ApproxLastUsed int64
|
|
||||||
Created int64
|
|
||||||
|
|
||||||
internalSave bool
|
internalSave bool
|
||||||
}
|
}
|
||||||
|
@ -94,6 +134,7 @@ func (profile *Profile) prepConfig() (err error) {
|
||||||
// prepare configuration
|
// prepare configuration
|
||||||
profile.configPerspective, err = config.NewPerspective(profile.Config)
|
profile.configPerspective, err = config.NewPerspective(profile.Config)
|
||||||
profile.outdated = abool.New()
|
profile.outdated = abool.New()
|
||||||
|
profile.lastActive = new(int64)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,16 +194,25 @@ func (profile *Profile) parseConfig() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Profile.
|
// New returns a new Profile.
|
||||||
func New() *Profile {
|
func New(source profileSource, id string, linkedPath string) *Profile {
|
||||||
profile := &Profile{
|
profile := &Profile{
|
||||||
ID: utils.RandomUUID("").String(),
|
ID: id,
|
||||||
Source: SourceLocal,
|
Source: source,
|
||||||
|
LinkedPath: linkedPath,
|
||||||
Created: time.Now().Unix(),
|
Created: time.Now().Unix(),
|
||||||
Config: make(map[string]interface{}),
|
Config: make(map[string]interface{}),
|
||||||
internalSave: true,
|
internalSave: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// create placeholders
|
// Generate random ID if none is given.
|
||||||
|
if id == "" {
|
||||||
|
profile.ID = utils.RandomUUID("").String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make key from ID and source.
|
||||||
|
profile.makeKey()
|
||||||
|
|
||||||
|
// Prepare profile to create placeholders.
|
||||||
_ = profile.prepConfig()
|
_ = profile.prepConfig()
|
||||||
_ = profile.parseConfig()
|
_ = profile.parseConfig()
|
||||||
|
|
||||||
|
@ -174,6 +224,11 @@ func (profile *Profile) ScopedID() string {
|
||||||
return makeScopedID(profile.Source, profile.ID)
|
return makeScopedID(profile.Source, profile.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// makeKey derives and sets the record Key from the profile attributes.
|
||||||
|
func (profile *Profile) makeKey() {
|
||||||
|
profile.SetKey(makeProfileKey(profile.Source, profile.ID))
|
||||||
|
}
|
||||||
|
|
||||||
// Save saves the profile to the database
|
// Save saves the profile to the database
|
||||||
func (profile *Profile) Save() error {
|
func (profile *Profile) Save() error {
|
||||||
if profile.ID == "" {
|
if profile.ID == "" {
|
||||||
|
@ -183,38 +238,41 @@ func (profile *Profile) Save() error {
|
||||||
return fmt.Errorf("profile: profile %s does not specify a source", profile.ID)
|
return fmt.Errorf("profile: profile %s does not specify a source", profile.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !profile.KeyIsSet() {
|
|
||||||
profile.SetKey(makeProfileKey(profile.Source, profile.ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
return profileDB.Put(profile)
|
return profileDB.Put(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkUsed marks the profile as used and saves it when it has changed.
|
// MarkStillActive marks the profile as still active.
|
||||||
func (profile *Profile) MarkUsed() {
|
func (profile *Profile) MarkStillActive() {
|
||||||
profile.Lock()
|
atomic.StoreInt64(profile.lastActive, time.Now().Unix())
|
||||||
// lastUsed
|
}
|
||||||
profile.lastUsed = time.Now()
|
|
||||||
|
// LastActive returns the unix timestamp when the profile was last marked as
|
||||||
|
// still active.
|
||||||
|
func (profile *Profile) LastActive() int64 {
|
||||||
|
return atomic.LoadInt64(profile.lastActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkUsed updates ApproxLastUsed when it's been a while and saves the profile if it was changed.
|
||||||
|
func (profile *Profile) MarkUsed() (changed bool) {
|
||||||
|
profile.Lock()
|
||||||
|
defer profile.Unlock()
|
||||||
|
|
||||||
// ApproxLastUsed
|
|
||||||
save := false
|
|
||||||
if time.Now().Add(-lastUsedUpdateThreshold).Unix() > profile.ApproxLastUsed {
|
if time.Now().Add(-lastUsedUpdateThreshold).Unix() > profile.ApproxLastUsed {
|
||||||
profile.ApproxLastUsed = time.Now().Unix()
|
profile.ApproxLastUsed = time.Now().Unix()
|
||||||
save = true
|
return true
|
||||||
}
|
}
|
||||||
profile.Unlock()
|
|
||||||
|
|
||||||
if save {
|
return false
|
||||||
err := profile.Save()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("profiles: failed to save profile %s after marking as used: %s", profile.ScopedID(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a string representation of the Profile.
|
// String returns a string representation of the Profile.
|
||||||
func (profile *Profile) String() string {
|
func (profile *Profile) String() string {
|
||||||
return profile.Name
|
return fmt.Sprintf("<%s %s/%s>", profile.Name, profile.Source, profile.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOutdated returns whether the this instance of the profile is marked as outdated.
|
||||||
|
func (profile *Profile) IsOutdated() bool {
|
||||||
|
return profile.outdated.IsSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddEndpoint adds an endpoint to the endpoint list, saves the profile and reloads the configuration.
|
// AddEndpoint adds an endpoint to the endpoint list, saves the profile and reloads the configuration.
|
||||||
|
@ -228,82 +286,79 @@ func (profile *Profile) AddServiceEndpoint(newEntry string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) {
|
func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) {
|
||||||
|
changed := false
|
||||||
|
|
||||||
|
// When finished, save the profile.
|
||||||
|
defer func() {
|
||||||
|
if !changed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := profile.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("profile: failed to save profile %s after add an endpoint rule: %s", profile.ScopedID(), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// When finished increase the revision counter of the layered profile.
|
||||||
|
defer func() {
|
||||||
|
if !changed || profile.layeredProfile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.layeredProfile.Lock()
|
||||||
|
defer profile.layeredProfile.Unlock()
|
||||||
|
|
||||||
|
profile.layeredProfile.RevisionCounter++
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Lock the profile for editing.
|
||||||
profile.Lock()
|
profile.Lock()
|
||||||
// get, update, save endpoints list
|
defer profile.Unlock()
|
||||||
|
|
||||||
|
// Get the endpoint list configuration value and add the new entry.
|
||||||
endpointList, ok := profile.configPerspective.GetAsStringArray(cfgKey)
|
endpointList, ok := profile.configPerspective.GetAsStringArray(cfgKey)
|
||||||
if !ok {
|
if ok {
|
||||||
endpointList = make([]string, 0, 1)
|
// A list already exists, check for duplicates within the same prefix.
|
||||||
|
newEntryPrefix := strings.Split(newEntry, " ")[0] + " "
|
||||||
|
for _, entry := range endpointList {
|
||||||
|
if !strings.HasPrefix(entry, newEntryPrefix) {
|
||||||
|
// We found an entry with a different prefix than the new entry.
|
||||||
|
// Beyond this entry we cannot possibly know if identical entries will
|
||||||
|
// match, so we will have to add the new entry no matter what the rest
|
||||||
|
// of the list has.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == newEntry {
|
||||||
|
// An identical entry is already in the list, abort.
|
||||||
|
log.Debugf("profile: ingoring new endpoint rule for %s, as identical is already present: %s", profile, newEntry)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpointList = append([]string{newEntry}, endpointList...)
|
||||||
|
} else {
|
||||||
|
endpointList = []string{newEntry}
|
||||||
}
|
}
|
||||||
endpointList = append(endpointList, newEntry)
|
|
||||||
|
// Save new value back to profile.
|
||||||
config.PutValueIntoHierarchicalConfig(profile.Config, cfgKey, endpointList)
|
config.PutValueIntoHierarchicalConfig(profile.Config, cfgKey, endpointList)
|
||||||
|
changed = true
|
||||||
|
|
||||||
profile.Unlock()
|
// Reload the profile manually in order to parse the newly added entry.
|
||||||
err := profile.Save()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("profile: failed to save profile after adding endpoint: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reload manually
|
|
||||||
profile.Lock()
|
|
||||||
profile.dataParsed = false
|
profile.dataParsed = false
|
||||||
err = profile.parseConfig()
|
err := profile.parseConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("profile: failed to parse profile config after adding endpoint: %s", err)
|
log.Warningf("profile: failed to parse %s config after adding endpoint: %s", profile, err)
|
||||||
}
|
}
|
||||||
profile.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProfile loads a profile from the database.
|
// LayeredProfile returns the layered profile associated with this profile.
|
||||||
func GetProfile(source, id string) (*Profile, error) {
|
func (profile *Profile) LayeredProfile() *LayeredProfile {
|
||||||
return GetProfileByScopedID(makeScopedID(source, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProfileByScopedID loads a profile from the database using a scoped ID like "local/id" or "community/id".
|
|
||||||
func GetProfileByScopedID(scopedID string) (*Profile, error) {
|
|
||||||
// check cache
|
|
||||||
profile := getActiveProfile(scopedID)
|
|
||||||
if profile != nil {
|
|
||||||
profile.MarkUsed()
|
|
||||||
return profile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// get from database
|
|
||||||
r, err := profileDB.Get(profilesDBPath + scopedID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert
|
|
||||||
profile, err = EnsureProfile(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// lock for prepping
|
|
||||||
profile.Lock()
|
profile.Lock()
|
||||||
|
defer profile.Unlock()
|
||||||
|
|
||||||
// prepare config
|
return profile.layeredProfile
|
||||||
err = profile.prepConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse config
|
|
||||||
err = profile.parseConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mark as internal
|
|
||||||
profile.internalSave = true
|
|
||||||
|
|
||||||
profile.Unlock()
|
|
||||||
|
|
||||||
// mark active
|
|
||||||
profile.MarkUsed()
|
|
||||||
markProfileActive(profile)
|
|
||||||
|
|
||||||
return profile, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureProfile ensures that the given record is a *Profile, and returns it.
|
// EnsureProfile ensures that the given record is a *Profile, and returns it.
|
||||||
|
@ -326,3 +381,108 @@ func EnsureProfile(r record.Record) (*Profile, error) {
|
||||||
}
|
}
|
||||||
return new, nil
|
return new, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateMetadata updates meta data fields on the profile and returns whether
|
||||||
|
// the profile was changed. If there is data that needs to be fetched from the
|
||||||
|
// operating system, it will start an async worker to fetch that data and save
|
||||||
|
// the profile afterwards.
|
||||||
|
func (profile *Profile) UpdateMetadata(processName string) (changed bool) {
|
||||||
|
// Check if this is a local profile, else warn and return.
|
||||||
|
if profile.Source != SourceLocal {
|
||||||
|
log.Warningf("tried to update metadata for non-local profile %s", profile.ScopedID())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.Lock()
|
||||||
|
defer profile.Unlock()
|
||||||
|
|
||||||
|
// Check if this is a special profile.
|
||||||
|
if profile.LinkedPath == "" {
|
||||||
|
// This is a special profile, just assign the processName, if needed, and
|
||||||
|
// return.
|
||||||
|
if profile.Name != processName {
|
||||||
|
profile.Name = processName
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsUpdateFromSystem bool
|
||||||
|
|
||||||
|
// Check profile name.
|
||||||
|
_, filename := filepath.Split(profile.LinkedPath)
|
||||||
|
|
||||||
|
// Update profile name if it is empty or equals the filename, which is the
|
||||||
|
// case for older profiles.
|
||||||
|
if profile.Name == "" || profile.Name == filename {
|
||||||
|
// Generate a default profile name if does not exist.
|
||||||
|
profile.Name = osdetail.GenerateBinaryNameFromPath(profile.LinkedPath)
|
||||||
|
if profile.Name == filename {
|
||||||
|
// TODO: Theoretically, the generated name could be identical to the
|
||||||
|
// filename.
|
||||||
|
// As a quick fix, append a space to the name.
|
||||||
|
profile.Name += " "
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
needsUpdateFromSystem = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If needed, get more/better data from the operating system.
|
||||||
|
if needsUpdateFromSystem {
|
||||||
|
module.StartWorker("get profile metadata", profile.updateMetadataFromSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateMetadataFromSystem updates the profile metadata with data from the
|
||||||
|
// operating system and saves it afterwards.
|
||||||
|
func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
||||||
|
// This function is only valid for local profiles.
|
||||||
|
if profile.Source != SourceLocal || profile.LinkedPath == "" {
|
||||||
|
return fmt.Errorf("tried to update metadata for non-local / non-linked profile %s", profile.ScopedID())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the profile when finished, if needed.
|
||||||
|
save := false
|
||||||
|
defer func() {
|
||||||
|
if save {
|
||||||
|
err := profile.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("profile: failed to save %s after metadata update: %s", profile.ScopedID(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Get binary name from linked path.
|
||||||
|
newName, err := osdetail.GetBinaryNameFromSystem(profile.LinkedPath)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, osdetail.ErrNotSupported) {
|
||||||
|
log.Warningf("profile: error while getting binary name for %s: %s", profile.LinkedPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filename of linked path for comparison.
|
||||||
|
_, filename := filepath.Split(profile.LinkedPath)
|
||||||
|
|
||||||
|
// TODO: Theoretically, the generated name from the system could be identical
|
||||||
|
// to the filename. This would mean that the worker is triggered every time
|
||||||
|
// the profile is freshly loaded.
|
||||||
|
if newName == filename {
|
||||||
|
// As a quick fix, append a space to the name.
|
||||||
|
newName += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock profile for applying metadata.
|
||||||
|
profile.Lock()
|
||||||
|
defer profile.Unlock()
|
||||||
|
|
||||||
|
// Apply new name if it changed.
|
||||||
|
if profile.Name != newName {
|
||||||
|
profile.Name = newName
|
||||||
|
save = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
package profile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
unidentifiedProfileID = "_unidentified"
|
|
||||||
systemProfileID = "_system"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetUnidentifiedProfile returns the special profile assigned to unidentified processes.
|
|
||||||
func GetUnidentifiedProfile() *Profile {
|
|
||||||
// get profile
|
|
||||||
profile, err := GetProfile(SourceLocal, unidentifiedProfileID)
|
|
||||||
if err == nil {
|
|
||||||
return profile
|
|
||||||
}
|
|
||||||
|
|
||||||
// create if not available (or error)
|
|
||||||
profile = New()
|
|
||||||
profile.Name = "Unidentified Processes"
|
|
||||||
profile.Source = SourceLocal
|
|
||||||
profile.ID = unidentifiedProfileID
|
|
||||||
|
|
||||||
// save to db
|
|
||||||
err = profile.Save()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("profiles: failed to save %s: %s", profile.ScopedID(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return profile
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSystemProfile returns the special profile used for the Kernel.
|
|
||||||
func GetSystemProfile() *Profile {
|
|
||||||
// get profile
|
|
||||||
profile, err := GetProfile(SourceLocal, systemProfileID)
|
|
||||||
if err == nil {
|
|
||||||
return profile
|
|
||||||
}
|
|
||||||
|
|
||||||
// create if not available (or error)
|
|
||||||
profile = New()
|
|
||||||
profile.Name = "Operating System"
|
|
||||||
profile.Source = SourceLocal
|
|
||||||
profile.ID = systemProfileID
|
|
||||||
|
|
||||||
// save to db
|
|
||||||
err = profile.Save()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("profiles: failed to save %s: %s", profile.ScopedID(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return profile
|
|
||||||
}
|
|
|
@ -57,19 +57,19 @@ var (
|
||||||
cfgOptionNameServersOrder = 0
|
cfgOptionNameServersOrder = 0
|
||||||
|
|
||||||
CfgOptionNoAssignedNameserversKey = "dns/noAssignedNameservers"
|
CfgOptionNoAssignedNameserversKey = "dns/noAssignedNameservers"
|
||||||
noAssignedNameservers status.SecurityLevelOption
|
noAssignedNameservers status.SecurityLevelOptionFunc
|
||||||
cfgOptionNoAssignedNameserversOrder = 1
|
cfgOptionNoAssignedNameserversOrder = 1
|
||||||
|
|
||||||
CfgOptionNoMulticastDNSKey = "dns/noMulticastDNS"
|
CfgOptionNoMulticastDNSKey = "dns/noMulticastDNS"
|
||||||
noMulticastDNS status.SecurityLevelOption
|
noMulticastDNS status.SecurityLevelOptionFunc
|
||||||
cfgOptionNoMulticastDNSOrder = 2
|
cfgOptionNoMulticastDNSOrder = 2
|
||||||
|
|
||||||
CfgOptionNoInsecureProtocolsKey = "dns/noInsecureProtocols"
|
CfgOptionNoInsecureProtocolsKey = "dns/noInsecureProtocols"
|
||||||
noInsecureProtocols status.SecurityLevelOption
|
noInsecureProtocols status.SecurityLevelOptionFunc
|
||||||
cfgOptionNoInsecureProtocolsOrder = 3
|
cfgOptionNoInsecureProtocolsOrder = 3
|
||||||
|
|
||||||
CfgOptionDontResolveSpecialDomainsKey = "dns/dontResolveSpecialDomains"
|
CfgOptionDontResolveSpecialDomainsKey = "dns/dontResolveSpecialDomains"
|
||||||
dontResolveSpecialDomains status.SecurityLevelOption
|
dontResolveSpecialDomains status.SecurityLevelOptionFunc
|
||||||
cfgOptionDontResolveSpecialDomainsOrder = 16
|
cfgOptionDontResolveSpecialDomainsOrder = 16
|
||||||
|
|
||||||
CfgOptionNameserverRetryRateKey = "dns/nameserverRetryRate"
|
CfgOptionNameserverRetryRateKey = "dns/nameserverRetryRate"
|
||||||
|
@ -82,36 +82,71 @@ func prepConfig() error {
|
||||||
Name: "DNS Servers",
|
Name: "DNS Servers",
|
||||||
Key: CfgOptionNameServersKey,
|
Key: CfgOptionNameServersKey,
|
||||||
Description: "DNS Servers to use for resolving DNS requests.",
|
Description: "DNS Servers to use for resolving DNS requests.",
|
||||||
Help: `Format:
|
Help: strings.ReplaceAll(`DNS Servers are used in the order as entered. The first one will be used as the primary DNS Server. Only if it fails, will the other servers be used as a fallback - in their respective order. If all fail, or if no DNS Server is configured here, the Portmaster will use the one configured in your system or network.
|
||||||
|
|
||||||
DNS Servers are configured in a URL format. This allows you to specify special settings for a resolver. If you just want to use a resolver at IP 10.2.3.4, please enter: dns://10.2.3.4:53
|
Additionally, if it is more likely that the DNS Server of your system or network has a (better) answer to a request, they will be asked first. This will be the case for special local domains and domain spaces announced on the current network.
|
||||||
The format is: protocol://ip:port?parameter=value¶meter=value
|
|
||||||
|
|
||||||
Protocols:
|
DNS Servers are configured in a URL format. This allows you to specify special settings for a resolver. If you just want to use a resolver at IP 10.2.3.4, please enter: "dns://10.2.3.4"
|
||||||
dot: DNS-over-TLS (recommended)
|
The format is: "protocol://ip:port?parameter=value¶meter=value"
|
||||||
dns: plain old DNS
|
|
||||||
tcp: plain old DNS over TCP
|
|
||||||
|
|
||||||
IP:
|
- Protocol
|
||||||
always use the IP address and _not_ the domain name!
|
- "dot": DNS-over-TLS (recommended)
|
||||||
|
- "dns": plain old DNS
|
||||||
Port:
|
- "tcp": plain old DNS over TCP
|
||||||
optionally define a custom port
|
- IP: always use the IP address and _not_ the domain name!
|
||||||
|
- Port: optionally define a custom port
|
||||||
Parameters:
|
- Parameters:
|
||||||
name: give your DNS Server a name that is used for messages and logs
|
- "name": give your DNS Server a name that is used for messages and logs
|
||||||
verify: domain name to verify for "dot", required and only valid for "dot"
|
- "verify": domain name to verify for "dot", required and only valid for protocol "dot"
|
||||||
blockedif: detect if the name server blocks a query, options:
|
- "blockedif": detect if the name server blocks a query, options:
|
||||||
empty: server replies with NXDomain status, but without any other record in any section
|
- "empty": server replies with NXDomain status, but without any other record in any section
|
||||||
refused: server replies with Refused status
|
- "refused": server replies with Refused status
|
||||||
zeroip: server replies with an IP address, but it is zero
|
- "zeroip": server replies with an IP address, but it is zero
|
||||||
`,
|
`, `"`, "`"),
|
||||||
Order: cfgOptionNameServersOrder,
|
|
||||||
OptType: config.OptTypeStringArray,
|
OptType: config.OptTypeStringArray,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
DefaultValue: defaultNameServers,
|
DefaultValue: defaultNameServers,
|
||||||
ValidationRegex: fmt.Sprintf("^(%s|%s|%s)://.*", ServerTypeDoT, ServerTypeDNS, ServerTypeTCP),
|
ValidationRegex: fmt.Sprintf("^(%s|%s|%s)://.*", ServerTypeDoT, ServerTypeDNS, ServerTypeTCP),
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayHintAnnotation: config.DisplayHintOrdered,
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionNameServersOrder,
|
||||||
|
config.CategoryAnnotation: "Servers",
|
||||||
|
config.QuickSettingsAnnotation: []config.QuickSetting{
|
||||||
|
{
|
||||||
|
Name: "Quad9",
|
||||||
|
Action: config.QuickReplace,
|
||||||
|
Value: []string{
|
||||||
|
"dot://9.9.9.9:853?verify=dns.quad9.net&name=Quad9&blockedif=empty",
|
||||||
|
"dot://149.112.112.112:853?verify=dns.quad9.net&name=Quad9&blockedif=empty",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AdGuard",
|
||||||
|
Action: config.QuickReplace,
|
||||||
|
Value: []string{
|
||||||
|
"dot://94.140.14.14:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip",
|
||||||
|
"dot://94.140.15.15:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Foundation for Applied Privacy",
|
||||||
|
Action: config.QuickReplace,
|
||||||
|
Value: []string{
|
||||||
|
"dot://94.130.106.88:853?verify=dot1.applied-privacy.net&name=AppliedPrivacy",
|
||||||
|
"dot://94.130.106.88:443?verify=dot1.applied-privacy.net&name=AppliedPrivacy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Cloudflare",
|
||||||
|
Action: config.QuickReplace,
|
||||||
|
Value: []string{
|
||||||
|
"dot://1.1.1.2:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip",
|
||||||
|
"dot://1.0.0.2:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -119,14 +154,18 @@ Parameters:
|
||||||
configuredNameServers = config.Concurrent.GetAsStringArray(CfgOptionNameServersKey, defaultNameServers)
|
configuredNameServers = config.Concurrent.GetAsStringArray(CfgOptionNameServersKey, defaultNameServers)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "DNS Server Retry Rate",
|
Name: "Retry Timeout",
|
||||||
Key: CfgOptionNameserverRetryRateKey,
|
Key: CfgOptionNameserverRetryRateKey,
|
||||||
Description: "Rate at which to retry failed DNS Servers, in seconds.",
|
Description: "Timeout between retries when a DNS server fails.",
|
||||||
Order: cfgOptionNameserverRetryRateOrder,
|
|
||||||
OptType: config.OptTypeInt,
|
OptType: config.OptTypeInt,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
DefaultValue: 600,
|
DefaultValue: 300,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionNameserverRetryRateOrder,
|
||||||
|
config.UnitAnnotation: "seconds",
|
||||||
|
config.CategoryAnnotation: "Servers",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -134,72 +173,87 @@ Parameters:
|
||||||
nameserverRetryRate = config.Concurrent.GetAsInt(CfgOptionNameserverRetryRateKey, 600)
|
nameserverRetryRate = config.Concurrent.GetAsInt(CfgOptionNameserverRetryRateKey, 600)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Do not use Multicast DNS",
|
Name: "Ignore System/Network Servers",
|
||||||
Key: CfgOptionNoMulticastDNSKey,
|
Key: CfgOptionNoAssignedNameserversKey,
|
||||||
Description: "Multicast DNS queries other devices in the local network",
|
Description: "Ignore DNS servers configured in your system or network.",
|
||||||
Order: cfgOptionNoMulticastDNSOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.SecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayOrderAnnotation: cfgOptionNoAssignedNameserversOrder,
|
||||||
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.CategoryAnnotation: "Servers",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
noMulticastDNS = status.ConfigIsActiveConcurrent(CfgOptionNoMulticastDNSKey)
|
noAssignedNameservers = status.SecurityLevelOption(CfgOptionNoAssignedNameserversKey)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Do not use assigned Nameservers",
|
Name: "Ignore Multicast DNS",
|
||||||
Key: CfgOptionNoAssignedNameserversKey,
|
Key: CfgOptionNoMulticastDNSKey,
|
||||||
Description: "that were acquired by the network (dhcp) or system",
|
Description: "Do not resolve using Multicast DNS. This may break certain Plug and Play devices or services.",
|
||||||
Order: cfgOptionNoAssignedNameserversOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.SecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayOrderAnnotation: cfgOptionNoMulticastDNSOrder,
|
||||||
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.CategoryAnnotation: "Resolving",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
noAssignedNameservers = status.ConfigIsActiveConcurrent(CfgOptionNoAssignedNameserversKey)
|
noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Do not resolve insecurely",
|
Name: "Enforce Secure DNS",
|
||||||
Key: CfgOptionNoInsecureProtocolsKey,
|
Key: CfgOptionNoInsecureProtocolsKey,
|
||||||
Description: "Do not resolve domains with insecure protocols, ie. plain DNS",
|
Description: "Never resolve using insecure protocols, ie. plain DNS.",
|
||||||
Order: cfgOptionNoInsecureProtocolsOrder,
|
OptType: config.OptTypeInt,
|
||||||
OptType: config.OptTypeInt,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
||||||
ExternalOptType: "security level",
|
PossibleValues: status.SecurityLevelValues,
|
||||||
DefaultValue: status.SecurityLevelsHighAndExtreme,
|
Annotations: config.Annotations{
|
||||||
ValidationRegex: "^(4|6|7)$",
|
config.DisplayOrderAnnotation: cfgOptionNoInsecureProtocolsOrder,
|
||||||
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.CategoryAnnotation: "Resolving",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
noInsecureProtocols = status.ConfigIsActiveConcurrent(CfgOptionNoInsecureProtocolsKey)
|
noInsecureProtocols = status.SecurityLevelOption(CfgOptionNoInsecureProtocolsKey)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Do not resolve special domains",
|
Name: "Block Unofficial TLDs",
|
||||||
Key: CfgOptionDontResolveSpecialDomainsKey,
|
Key: CfgOptionDontResolveSpecialDomainsKey,
|
||||||
Description: fmt.Sprintf("Do not resolve the special top level domains %s", formatScopeList(specialServiceDomains)),
|
Description: fmt.Sprintf(
|
||||||
Order: cfgOptionDontResolveSpecialDomainsOrder,
|
"Block %s. Unofficial domains may pose a security risk. This does not affect .onion domains in the Tor Browser.",
|
||||||
OptType: config.OptTypeInt,
|
formatScopeList(specialServiceDomains),
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
),
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
OptType: config.OptTypeInt,
|
||||||
ExternalOptType: "security level",
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
DefaultValue: status.SecurityLevelsAll,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
ValidationRegex: "^(4|6|7)$",
|
DefaultValue: status.SecurityLevelsAll,
|
||||||
|
PossibleValues: status.AllSecurityLevelValues,
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: cfgOptionDontResolveSpecialDomainsOrder,
|
||||||
|
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
|
||||||
|
config.CategoryAnnotation: "Resolving",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dontResolveSpecialDomains = status.ConfigIsActiveConcurrent(CfgOptionDontResolveSpecialDomainsKey)
|
dontResolveSpecialDomains = status.SecurityLevelOption(CfgOptionDontResolveSpecialDomainsKey)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -207,7 +261,7 @@ Parameters:
|
||||||
func formatScopeList(list []string) string {
|
func formatScopeList(list []string) string {
|
||||||
formatted := make([]string, 0, len(list))
|
formatted := make([]string, 0, len(list))
|
||||||
for _, domain := range list {
|
for _, domain := range list {
|
||||||
formatted = append(formatted, strings.Trim(domain, "."))
|
formatted = append(formatted, strings.TrimRight(domain, "."))
|
||||||
}
|
}
|
||||||
return strings.Join(formatted, ", ")
|
return strings.Join(formatted, ", ")
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,28 +204,32 @@ func getSystemResolvers() (resolvers []*Resolver) {
|
||||||
return resolvers
|
return resolvers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const missingResolversErrorID = "missing-resolvers"
|
||||||
|
|
||||||
func loadResolvers() {
|
func loadResolvers() {
|
||||||
// TODO: what happens when a lot of processes want to reload at once? we do not need to run this multiple times in a short time frame.
|
// TODO: what happens when a lot of processes want to reload at once? we do not need to run this multiple times in a short time frame.
|
||||||
resolversLock.Lock()
|
resolversLock.Lock()
|
||||||
defer resolversLock.Unlock()
|
defer resolversLock.Unlock()
|
||||||
|
|
||||||
|
// Resolve module error about missing resolvers.
|
||||||
|
module.Resolve(missingResolversErrorID)
|
||||||
|
|
||||||
newResolvers := append(
|
newResolvers := append(
|
||||||
getConfiguredResolvers(configuredNameServers()),
|
getConfiguredResolvers(configuredNameServers()),
|
||||||
getSystemResolvers()...,
|
getSystemResolvers()...,
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(newResolvers) == 0 {
|
if len(newResolvers) == 0 {
|
||||||
msg := "no (valid) dns servers found in (user) configuration or system, falling back to defaults"
|
msg := "no (valid) dns servers found in configuration or system, falling back to defaults"
|
||||||
log.Warningf("resolver: %s", msg)
|
log.Warningf("resolver: %s", msg)
|
||||||
module.Warning("no-valid-user-resolvers", msg)
|
module.Warning(missingResolversErrorID, msg)
|
||||||
|
|
||||||
// load defaults directly, overriding config system
|
// load defaults directly, overriding config system
|
||||||
newResolvers = getConfiguredResolvers(defaultNameServers)
|
newResolvers = getConfiguredResolvers(defaultNameServers)
|
||||||
if len(newResolvers) == 0 {
|
if len(newResolvers) == 0 {
|
||||||
msg = "no (valid) dns servers found in configuration or system"
|
msg = "no (valid) dns servers found in configuration or system"
|
||||||
log.Criticalf("resolver: %s", msg)
|
log.Criticalf("resolver: %s", msg)
|
||||||
module.Error("no-valid-default-resolvers", msg)
|
module.Error(missingResolversErrorID, msg)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
36
status/autopilot.go
Normal file
36
status/autopilot.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
var runAutoPilot = make(chan struct{})
|
||||||
|
|
||||||
|
func triggerAutopilot() {
|
||||||
|
select {
|
||||||
|
case runAutoPilot <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoPilot(ctx context.Context) error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case <-runAutoPilot:
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := SelectedSecurityLevel()
|
||||||
|
mitigation := getHighestMitigationLevel()
|
||||||
|
|
||||||
|
active := SecurityLevelNormal
|
||||||
|
if selected != SecurityLevelOff {
|
||||||
|
active = selected
|
||||||
|
} else if mitigation != SecurityLevelOff {
|
||||||
|
active = mitigation
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveLevel(active)
|
||||||
|
|
||||||
|
pushSystemStatus()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
// Definitions of Security and Status Levels.
|
|
||||||
const (
|
|
||||||
SecurityLevelOff uint8 = 0
|
|
||||||
|
|
||||||
SecurityLevelNormal uint8 = 1
|
|
||||||
SecurityLevelHigh uint8 = 2
|
|
||||||
SecurityLevelExtreme uint8 = 4
|
|
||||||
|
|
||||||
SecurityLevelsNormalAndHigh uint8 = SecurityLevelNormal | SecurityLevelHigh
|
|
||||||
SecurityLevelsNormalAndExtreme uint8 = SecurityLevelNormal | SecurityLevelExtreme
|
|
||||||
SecurityLevelsHighAndExtreme uint8 = SecurityLevelHigh | SecurityLevelExtreme
|
|
||||||
SecurityLevelsAll uint8 = SecurityLevelNormal | SecurityLevelHigh | SecurityLevelExtreme
|
|
||||||
|
|
||||||
StatusOff uint8 = 0
|
|
||||||
StatusError uint8 = 1
|
|
||||||
StatusWarning uint8 = 2
|
|
||||||
StatusOk uint8 = 3
|
|
||||||
)
|
|
|
@ -1,62 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/database"
|
|
||||||
"github.com/safing/portbase/database/query"
|
|
||||||
"github.com/safing/portbase/database/record"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
statusDBKey = "core:status/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
statusDB = database.NewInterface(&database.Options{
|
|
||||||
Local: true,
|
|
||||||
Internal: true,
|
|
||||||
})
|
|
||||||
hook *database.RegisteredHook
|
|
||||||
)
|
|
||||||
|
|
||||||
type statusHook struct {
|
|
||||||
database.HookBase
|
|
||||||
}
|
|
||||||
|
|
||||||
// UsesPrePut implements the Hook interface.
|
|
||||||
func (sh *statusHook) UsesPrePut() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrePut implements the Hook interface.
|
|
||||||
func (sh *statusHook) PrePut(r record.Record) (record.Record, error) {
|
|
||||||
// record is already locked!
|
|
||||||
|
|
||||||
newStatus, err := EnsureSystemStatus(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply applicable settings
|
|
||||||
if SelectedSecurityLevel() != newStatus.SelectedSecurityLevel {
|
|
||||||
module.StartWorker("set selected security level", func(_ context.Context) error {
|
|
||||||
setSelectedSecurityLevel(newStatus.SelectedSecurityLevel)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: allow setting of Gate17 status (on/off)
|
|
||||||
|
|
||||||
// return original status
|
|
||||||
return status, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initStatusHook() (err error) {
|
|
||||||
hook, err = database.RegisterHook(query.New(statusDBKey), &statusHook{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopStatusHook() error {
|
|
||||||
return hook.Cancel()
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/safing/portbase/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// SecurityLevelOption defines the returned function by ConfigIsActive.
|
|
||||||
SecurityLevelOption func(minSecurityLevel uint8) bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func max(a, b uint8) uint8 {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigIsActive returns whether the given security level dependent config option is on or off.
|
|
||||||
func ConfigIsActive(name string) SecurityLevelOption {
|
|
||||||
activeAtLevel := config.GetAsInt(name, int64(SecurityLevelsAll))
|
|
||||||
return func(minSecurityLevel uint8) bool {
|
|
||||||
return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigIsActiveConcurrent returns whether the given security level dependent config option is on or off and is concurrency safe.
|
|
||||||
func ConfigIsActiveConcurrent(name string) SecurityLevelOption {
|
|
||||||
activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelsAll))
|
|
||||||
return func(minSecurityLevel uint8) bool {
|
|
||||||
return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
activeSecurityLevel *uint32
|
|
||||||
selectedSecurityLevel *uint32
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var (
|
|
||||||
activeSecurityLevelValue uint32
|
|
||||||
selectedSecurityLevelValue uint32
|
|
||||||
)
|
|
||||||
|
|
||||||
activeSecurityLevel = &activeSecurityLevelValue
|
|
||||||
selectedSecurityLevel = &selectedSecurityLevelValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveSecurityLevel returns the current security level.
|
|
||||||
func ActiveSecurityLevel() uint8 {
|
|
||||||
return uint8(atomic.LoadUint32(activeSecurityLevel))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SelectedSecurityLevel returns the selected security level.
|
|
||||||
func SelectedSecurityLevel() uint8 {
|
|
||||||
return uint8(atomic.LoadUint32(selectedSecurityLevel))
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
|
||||||
|
|
||||||
// only test for panics
|
|
||||||
// TODO: write real tests
|
|
||||||
ActiveSecurityLevel()
|
|
||||||
SelectedSecurityLevel()
|
|
||||||
option := ConfigIsActive("invalid")
|
|
||||||
option(0)
|
|
||||||
option = ConfigIsActiveConcurrent("invalid")
|
|
||||||
option(0)
|
|
||||||
|
|
||||||
}
|
|
60
status/mitigation.go
Normal file
60
status/mitigation.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type knownThreats struct {
|
||||||
|
sync.RWMutex
|
||||||
|
// active threats and their recommended mitigation level
|
||||||
|
list map[string]uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
var threats = &knownThreats{
|
||||||
|
list: make(map[string]uint8),
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMitigationLevel sets the mitigation level for id
|
||||||
|
// to mitigation. If mitigation is SecurityLevelOff the
|
||||||
|
// mitigation record will be removed. If mitigation is
|
||||||
|
// an invalid level the call to SetMitigationLevel is a
|
||||||
|
// no-op.
|
||||||
|
func SetMitigationLevel(id string, mitigation uint8) {
|
||||||
|
if !IsValidSecurityLevel(mitigation) {
|
||||||
|
log.Warningf("tried to set invalid mitigation level %d for threat %s", mitigation, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer triggerAutopilot()
|
||||||
|
|
||||||
|
threats.Lock()
|
||||||
|
defer threats.Unlock()
|
||||||
|
if mitigation == 0 {
|
||||||
|
delete(threats.list, id)
|
||||||
|
} else {
|
||||||
|
threats.list[id] = mitigation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMitigationLevel deletes the mitigation level for id.
|
||||||
|
func DeleteMitigationLevel(id string) {
|
||||||
|
SetMitigationLevel(id, SecurityLevelOff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHighestMitigationLevel returns the highest mitigation
|
||||||
|
// level set on a threat.
|
||||||
|
func getHighestMitigationLevel() uint8 {
|
||||||
|
threats.RLock()
|
||||||
|
defer threats.RUnlock()
|
||||||
|
|
||||||
|
var level uint8 = SecurityLevelNormal
|
||||||
|
for _, lvl := range threats.list {
|
||||||
|
if lvl > level {
|
||||||
|
level = lvl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return level
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/safing/portbase/database"
|
"context"
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -11,56 +12,30 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
module = modules.Register("status", nil, start, stop, "base")
|
module = modules.Register("status", nil, start, nil, "base")
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() error {
|
func start() error {
|
||||||
err := initSystemStatus()
|
if err := setupRuntimeProvider(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = startNetEnvHooking()
|
module.StartWorker("auto-pilot", autoPilot)
|
||||||
|
|
||||||
|
triggerAutopilot()
|
||||||
|
|
||||||
|
err := module.RegisterEventHook(
|
||||||
|
"netenv",
|
||||||
|
netenv.OnlineStatusChangedEvent,
|
||||||
|
"update online status in system status",
|
||||||
|
func(_ context.Context, _ interface{}) error {
|
||||||
|
triggerAutopilot()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
status.Save()
|
|
||||||
|
|
||||||
return initStatusHook()
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSystemStatus() error {
|
|
||||||
// load status from database
|
|
||||||
r, err := statusDB.Get(statusDBKey)
|
|
||||||
switch err {
|
|
||||||
case nil:
|
|
||||||
loadedStatus, err := EnsureSystemStatus(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Criticalf("status: failed to unwrap system status: %s", err)
|
|
||||||
} else {
|
|
||||||
status = loadedStatus
|
|
||||||
}
|
|
||||||
case database.ErrNotFound:
|
|
||||||
// create new status
|
|
||||||
default:
|
|
||||||
log.Criticalf("status: failed to load system status: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
status.Lock()
|
|
||||||
defer status.Unlock()
|
|
||||||
|
|
||||||
// load status into atomic getters
|
|
||||||
atomicUpdateSelectedSecurityLevel(status.SelectedSecurityLevel)
|
|
||||||
|
|
||||||
// update status
|
|
||||||
status.updateThreatMitigationLevel()
|
|
||||||
status.autopilot()
|
|
||||||
status.updateOnlineStatus()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() error {
|
|
||||||
return stopStatusHook()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/netenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// startNetEnvHooking starts the listener for online status changes.
|
|
||||||
func startNetEnvHooking() error {
|
|
||||||
return module.RegisterEventHook(
|
|
||||||
"netenv",
|
|
||||||
netenv.OnlineStatusChangedEvent,
|
|
||||||
"update online status in system status",
|
|
||||||
func(_ context.Context, _ interface{}) error {
|
|
||||||
status.Lock()
|
|
||||||
status.updateOnlineStatus()
|
|
||||||
status.Unlock()
|
|
||||||
status.Save()
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SystemStatus) updateOnlineStatus() {
|
|
||||||
s.OnlineStatus = netenv.GetOnlineStatus()
|
|
||||||
s.CaptivePortal = netenv.GetCaptivePortal()
|
|
||||||
}
|
|
97
status/provider.go
Normal file
97
status/provider.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database/record"
|
||||||
|
"github.com/safing/portbase/runtime"
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pushUpdate runtime.PushFunc
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupRuntimeProvider() (err error) {
|
||||||
|
// register the system status getter
|
||||||
|
//
|
||||||
|
statusProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) {
|
||||||
|
return []record.Record{buildSystemStatus()}, nil
|
||||||
|
})
|
||||||
|
pushUpdate, err = runtime.Register("system/status", statusProvider)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// register the selected security level setter
|
||||||
|
//
|
||||||
|
levelProvider := runtime.SimpleValueSetterFunc(setSelectedSecurityLevel)
|
||||||
|
_, err = runtime.Register("system/security-level", levelProvider)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSelectedSecurityLevel updates the selected security level
|
||||||
|
func setSelectedSecurityLevel(r record.Record) (record.Record, error) {
|
||||||
|
var upd *SelectedSecurityLevelRecord
|
||||||
|
if r.IsWrapped() {
|
||||||
|
upd = new(SelectedSecurityLevelRecord)
|
||||||
|
if err := record.Unwrap(r, upd); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO(ppacher): this can actually never happen
|
||||||
|
// as we're write-only and ValueProvider.Set() should
|
||||||
|
// only ever be called from the HTTP API (so r must be wrapped).
|
||||||
|
// Though, make sure we handle the case as well ...
|
||||||
|
var ok bool
|
||||||
|
upd, ok = r.(*SelectedSecurityLevelRecord)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected *SelectedSecurityLevelRecord but got %T", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidSecurityLevel(upd.SelectedSecurityLevel) {
|
||||||
|
return nil, fmt.Errorf("invalid security level: %d", upd.SelectedSecurityLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if SelectedSecurityLevel() != upd.SelectedSecurityLevel {
|
||||||
|
setSelectedLevel(upd.SelectedSecurityLevel)
|
||||||
|
triggerAutopilot()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSystemStatus build a new system status record.
|
||||||
|
func buildSystemStatus() *SystemStatusRecord {
|
||||||
|
status := &SystemStatusRecord{
|
||||||
|
ActiveSecurityLevel: ActiveSecurityLevel(),
|
||||||
|
SelectedSecurityLevel: SelectedSecurityLevel(),
|
||||||
|
ThreatMitigationLevel: getHighestMitigationLevel(),
|
||||||
|
CaptivePortal: netenv.GetCaptivePortal(),
|
||||||
|
OnlineStatus: netenv.GetOnlineStatus(),
|
||||||
|
}
|
||||||
|
|
||||||
|
status.CreateMeta()
|
||||||
|
status.SetKey("runtime:system/status")
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushSystemStatus pushes a new system status via
|
||||||
|
// the runtime database.
|
||||||
|
func pushSystemStatus() {
|
||||||
|
if pushUpdate == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
record := buildSystemStatus()
|
||||||
|
record.Lock()
|
||||||
|
defer record.Unlock()
|
||||||
|
|
||||||
|
pushUpdate(record)
|
||||||
|
}
|
42
status/records.go
Normal file
42
status/records.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database/record"
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SystemStatusRecord describes the overall status of the Portmaster.
|
||||||
|
// It's a read-only record exposed via runtime:system/status.
|
||||||
|
type SystemStatusRecord struct {
|
||||||
|
record.Base
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
// ActiveSecurityLevel holds the currently
|
||||||
|
// active security level.
|
||||||
|
ActiveSecurityLevel uint8
|
||||||
|
// SelectedSecurityLevel holds the security level
|
||||||
|
// as selected by the user.
|
||||||
|
SelectedSecurityLevel uint8
|
||||||
|
// ThreatMitigationLevel holds the security level
|
||||||
|
// as selected by the auto-pilot.
|
||||||
|
ThreatMitigationLevel uint8
|
||||||
|
// OnlineStatus holds the current online status as
|
||||||
|
// seen by the netenv package.
|
||||||
|
OnlineStatus netenv.OnlineStatus
|
||||||
|
// CaptivePortal holds all information about the captive
|
||||||
|
// portal of the network the portmaster is currently
|
||||||
|
// connected to, if any.
|
||||||
|
CaptivePortal *netenv.CaptivePortal
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectedSecurityLevelRecord is used as a dummy record.Record
|
||||||
|
// to provide a simply runtime-configuration for the user.
|
||||||
|
// It is write-only and exposed at runtime:system/security-level
|
||||||
|
type SelectedSecurityLevelRecord struct {
|
||||||
|
record.Base
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
SelectedSecurityLevel uint8
|
||||||
|
}
|
114
status/security_level.go
Normal file
114
status/security_level.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import "github.com/safing/portbase/config"
|
||||||
|
|
||||||
|
type (
|
||||||
|
// SecurityLevelOptionFunc can be called with a minimum security level
|
||||||
|
// and returns whether or not a given security option is enabled or
|
||||||
|
// not.
|
||||||
|
// Use SecurityLevelOption() to get a SecurityLevelOptionFunc for a
|
||||||
|
// specific option.
|
||||||
|
SecurityLevelOptionFunc func(minSecurityLevel uint8) bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// DisplayHintSecurityLevel is an external option hint for security levels.
|
||||||
|
// It's meant to be used as a value for config.DisplayHintAnnotation.
|
||||||
|
const DisplayHintSecurityLevel string = "security level"
|
||||||
|
|
||||||
|
// Security levels
|
||||||
|
const (
|
||||||
|
SecurityLevelOff uint8 = 0
|
||||||
|
SecurityLevelNormal uint8 = 1
|
||||||
|
SecurityLevelHigh uint8 = 2
|
||||||
|
SecurityLevelExtreme uint8 = 4
|
||||||
|
|
||||||
|
SecurityLevelsNormalAndHigh uint8 = SecurityLevelNormal | SecurityLevelHigh
|
||||||
|
SecurityLevelsNormalAndExtreme uint8 = SecurityLevelNormal | SecurityLevelExtreme
|
||||||
|
SecurityLevelsHighAndExtreme uint8 = SecurityLevelHigh | SecurityLevelExtreme
|
||||||
|
SecurityLevelsAll uint8 = SecurityLevelNormal | SecurityLevelHigh | SecurityLevelExtreme
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecurityLevelValues defines all possible security levels.
|
||||||
|
var SecurityLevelValues = []config.PossibleValue{
|
||||||
|
{
|
||||||
|
Name: "Normal",
|
||||||
|
Value: SecurityLevelsAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "High",
|
||||||
|
Value: SecurityLevelsHighAndExtreme,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Extreme",
|
||||||
|
Value: SecurityLevelExtreme,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllSecurityLevelValues is like SecurityLevelValues but also includes Off.
|
||||||
|
var AllSecurityLevelValues = append([]config.PossibleValue{
|
||||||
|
{
|
||||||
|
Name: "Off",
|
||||||
|
Value: SecurityLevelOff,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SecurityLevelValues...,
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValidSecurityLevel returns true if level is a valid,
|
||||||
|
// single security level. Level is also invalid if it's a
|
||||||
|
// bitmask with more that one security level set.
|
||||||
|
func IsValidSecurityLevel(level uint8) bool {
|
||||||
|
return level == SecurityLevelOff ||
|
||||||
|
level == SecurityLevelNormal ||
|
||||||
|
level == SecurityLevelHigh ||
|
||||||
|
level == SecurityLevelExtreme
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidSecurityLevelMask returns true if level is a valid
|
||||||
|
// security level mask. It's like IsValidSecurityLevel but
|
||||||
|
// also allows bitmask combinations.
|
||||||
|
func IsValidSecurityLevelMask(level uint8) bool {
|
||||||
|
return level <= 7
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b uint8) uint8 {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityLevelOption returns a function to check if the option
|
||||||
|
// identified by name is active at a given minimum security level.
|
||||||
|
// The returned function is safe for concurrent use with configuration
|
||||||
|
// updates.
|
||||||
|
func SecurityLevelOption(name string) SecurityLevelOptionFunc {
|
||||||
|
activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelsAll))
|
||||||
|
return func(minSecurityLevel uint8) bool {
|
||||||
|
return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityLevelString returns the given security level as a string.
|
||||||
|
func SecurityLevelString(level uint8) string {
|
||||||
|
switch level {
|
||||||
|
case SecurityLevelOff:
|
||||||
|
return "Off"
|
||||||
|
case SecurityLevelNormal:
|
||||||
|
return "Normal"
|
||||||
|
case SecurityLevelHigh:
|
||||||
|
return "High"
|
||||||
|
case SecurityLevelExtreme:
|
||||||
|
return "Extreme"
|
||||||
|
case SecurityLevelsNormalAndHigh:
|
||||||
|
return "Normal and High"
|
||||||
|
case SecurityLevelsNormalAndExtreme:
|
||||||
|
return "Normal and Extreme"
|
||||||
|
case SecurityLevelsHighAndExtreme:
|
||||||
|
return "High and Extreme"
|
||||||
|
case SecurityLevelsAll:
|
||||||
|
return "Normal, High and Extreme"
|
||||||
|
default:
|
||||||
|
return "INVALID"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,54 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// autopilot automatically adjusts the security level as needed.
|
|
||||||
func (s *SystemStatus) autopilot() {
|
|
||||||
// check if users is overruling
|
|
||||||
if s.SelectedSecurityLevel > SecurityLevelOff {
|
|
||||||
s.ActiveSecurityLevel = s.SelectedSecurityLevel
|
|
||||||
atomicUpdateActiveSecurityLevel(s.SelectedSecurityLevel)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// update active security level
|
|
||||||
switch s.ThreatMitigationLevel {
|
|
||||||
case SecurityLevelOff:
|
|
||||||
s.ActiveSecurityLevel = SecurityLevelNormal
|
|
||||||
atomicUpdateActiveSecurityLevel(SecurityLevelNormal)
|
|
||||||
case SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme:
|
|
||||||
s.ActiveSecurityLevel = s.ThreatMitigationLevel
|
|
||||||
atomicUpdateActiveSecurityLevel(s.ThreatMitigationLevel)
|
|
||||||
default:
|
|
||||||
log.Errorf("status: threat mitigation level is set to invalid value: %d", s.ThreatMitigationLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setSelectedSecurityLevel sets the selected security level.
|
|
||||||
func setSelectedSecurityLevel(level uint8) {
|
|
||||||
switch level {
|
|
||||||
case SecurityLevelOff, SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme:
|
|
||||||
status.Lock()
|
|
||||||
|
|
||||||
status.SelectedSecurityLevel = level
|
|
||||||
atomicUpdateSelectedSecurityLevel(level)
|
|
||||||
status.autopilot()
|
|
||||||
|
|
||||||
status.Unlock()
|
|
||||||
status.Save()
|
|
||||||
default:
|
|
||||||
log.Errorf("status: tried to set selected security level to invalid value: %d", level)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func atomicUpdateActiveSecurityLevel(level uint8) {
|
|
||||||
atomic.StoreUint32(activeSecurityLevel, uint32(level))
|
|
||||||
}
|
|
||||||
|
|
||||||
func atomicUpdateSelectedSecurityLevel(level uint8) {
|
|
||||||
atomic.StoreUint32(selectedSecurityLevel, uint32(level))
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestSet(t *testing.T) {
|
|
||||||
|
|
||||||
// only test for panics
|
|
||||||
// TODO: write real tests
|
|
||||||
setSelectedSecurityLevel(0)
|
|
||||||
|
|
||||||
}
|
|
30
status/state.go
Normal file
30
status/state.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
activeLevel = new(uint32)
|
||||||
|
selectedLevel = new(uint32)
|
||||||
|
)
|
||||||
|
|
||||||
|
func setActiveLevel(lvl uint8) {
|
||||||
|
atomic.StoreUint32(activeLevel, uint32(lvl))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSelectedLevel(lvl uint8) {
|
||||||
|
atomic.StoreUint32(selectedLevel, uint32(lvl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveSecurityLevel returns the currently active security
|
||||||
|
// level.
|
||||||
|
func ActiveSecurityLevel() uint8 {
|
||||||
|
return uint8(atomic.LoadUint32(activeLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectedSecurityLevel returns the security level as selected
|
||||||
|
// by the user.
|
||||||
|
func SelectedSecurityLevel() uint8 {
|
||||||
|
return uint8(atomic.LoadUint32(selectedLevel))
|
||||||
|
}
|
113
status/status.go
113
status/status.go
|
@ -1,113 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/netenv"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/database/record"
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
status *SystemStatus
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
status = &SystemStatus{
|
|
||||||
Threats: make(map[string]*Threat),
|
|
||||||
}
|
|
||||||
status.SetKey(statusDBKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SystemStatus saves basic information about the current system status.
|
|
||||||
//nolint:maligned // TODO
|
|
||||||
type SystemStatus struct {
|
|
||||||
record.Base
|
|
||||||
sync.Mutex
|
|
||||||
|
|
||||||
ActiveSecurityLevel uint8
|
|
||||||
SelectedSecurityLevel uint8
|
|
||||||
|
|
||||||
OnlineStatus netenv.OnlineStatus
|
|
||||||
CaptivePortal *netenv.CaptivePortal
|
|
||||||
|
|
||||||
ThreatMitigationLevel uint8
|
|
||||||
Threats map[string]*Threat
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAsync saves the SystemStatus to the database asynchronously.
|
|
||||||
func (s *SystemStatus) SaveAsync() {
|
|
||||||
module.StartWorker("save system status", func(_ context.Context) error {
|
|
||||||
s.Save()
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save saves the SystemStatus to the database.
|
|
||||||
func (s *SystemStatus) Save() {
|
|
||||||
err := statusDB.Put(s)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("status: could not save status to database: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnsureSystemStatus ensures that the given record is of type SystemStatus and unwraps it, if needed.
|
|
||||||
func EnsureSystemStatus(r record.Record) (*SystemStatus, error) {
|
|
||||||
// unwrap
|
|
||||||
if r.IsWrapped() {
|
|
||||||
// only allocate a new struct, if we need it
|
|
||||||
new := &SystemStatus{}
|
|
||||||
err := record.Unwrap(r, new)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return new, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// or adjust type
|
|
||||||
new, ok := r.(*SystemStatus)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("record not of type *SystemStatus, but %T", r)
|
|
||||||
}
|
|
||||||
return new, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FmtActiveSecurityLevel returns the current security level as a string.
|
|
||||||
func FmtActiveSecurityLevel() string {
|
|
||||||
status.Lock()
|
|
||||||
mitigationLevel := status.ThreatMitigationLevel
|
|
||||||
status.Unlock()
|
|
||||||
active := ActiveSecurityLevel()
|
|
||||||
s := FmtSecurityLevel(active)
|
|
||||||
if mitigationLevel > 0 && active != mitigationLevel {
|
|
||||||
s += "*"
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// FmtSecurityLevel returns the given security level as a string.
|
|
||||||
func FmtSecurityLevel(level uint8) string {
|
|
||||||
switch level {
|
|
||||||
case SecurityLevelOff:
|
|
||||||
return "Off"
|
|
||||||
case SecurityLevelNormal:
|
|
||||||
return "Normal"
|
|
||||||
case SecurityLevelHigh:
|
|
||||||
return "High"
|
|
||||||
case SecurityLevelExtreme:
|
|
||||||
return "Extreme"
|
|
||||||
case SecurityLevelsNormalAndHigh:
|
|
||||||
return "Normal and High"
|
|
||||||
case SecurityLevelsNormalAndExtreme:
|
|
||||||
return "Normal and Extreme"
|
|
||||||
case SecurityLevelsHighAndExtreme:
|
|
||||||
return "High and Extreme"
|
|
||||||
case SecurityLevelsAll:
|
|
||||||
return "Normal, High and Extreme"
|
|
||||||
default:
|
|
||||||
return "INVALID"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestStatus(t *testing.T) {
|
|
||||||
|
|
||||||
setSelectedSecurityLevel(SecurityLevelOff)
|
|
||||||
if FmtActiveSecurityLevel() != "Normal" {
|
|
||||||
t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedSecurityLevel(SecurityLevelNormal)
|
|
||||||
AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelHigh})
|
|
||||||
if FmtActiveSecurityLevel() != "Normal*" {
|
|
||||||
t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedSecurityLevel(SecurityLevelHigh)
|
|
||||||
if FmtActiveSecurityLevel() != "High" {
|
|
||||||
t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedSecurityLevel(SecurityLevelHigh)
|
|
||||||
AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelExtreme})
|
|
||||||
if FmtActiveSecurityLevel() != "High*" {
|
|
||||||
t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedSecurityLevel(SecurityLevelExtreme)
|
|
||||||
if FmtActiveSecurityLevel() != "Extreme" {
|
|
||||||
t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
172
status/threat.go
172
status/threat.go
|
@ -1,73 +1,133 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"time"
|
||||||
"sync"
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Threat describes a detected threat.
|
// Threat represents a threat to the system.
|
||||||
|
// A threat is basically a notification with strong
|
||||||
|
// typed EventData. Use the methods expored on Threat
|
||||||
|
// to manipulate the EventData field and push updates
|
||||||
|
// of the notification.
|
||||||
|
// Do not use EventData directly!
|
||||||
type Threat struct {
|
type Threat struct {
|
||||||
ID string // A unique ID chosen by reporting module (eg. modulePrefix-incident) to periodically check threat existence
|
*notifications.Notification
|
||||||
Name string // Descriptive (human readable) name for detected threat
|
|
||||||
Description string // Simple description
|
|
||||||
AdditionalData interface{} // Additional data a module wants to make available for the user
|
|
||||||
MitigationLevel uint8 // Recommended Security Level to switch to for mitigation
|
|
||||||
Started int64
|
|
||||||
Ended int64
|
|
||||||
// TODO: add locking
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddOrUpdateThreat adds or updates a new threat in the system status.
|
// ThreatPayload holds threat related information.
|
||||||
func AddOrUpdateThreat(new *Threat) {
|
type ThreatPayload struct {
|
||||||
status.Lock()
|
// MitigationLevel holds the recommended security
|
||||||
defer status.Unlock()
|
// level to mitigate the threat.
|
||||||
|
MitigationLevel uint8
|
||||||
status.Threats[new.ID] = new
|
// Started holds the UNIX epoch timestamp in seconds
|
||||||
status.updateThreatMitigationLevel()
|
// at which the threat has been detected the first time.
|
||||||
status.autopilot()
|
Started int64
|
||||||
|
// Ended holds the UNIX epoch timestamp in seconds
|
||||||
status.SaveAsync()
|
// at which the threat has been detected the last time.
|
||||||
|
Ended int64
|
||||||
|
// Data may holds threat-specific data.
|
||||||
|
Data interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteThreat deletes a threat from the system status.
|
// NewThreat returns a new threat. Note that the
|
||||||
func DeleteThreat(id string) {
|
// threat only gets published once Publish is called.
|
||||||
status.Lock()
|
//
|
||||||
defer status.Unlock()
|
// Example:
|
||||||
|
//
|
||||||
delete(status.Threats, id)
|
// threat := NewThreat("portscan", "Someone is scanning you").
|
||||||
status.updateThreatMitigationLevel()
|
// SetData(portscanResult).
|
||||||
status.autopilot()
|
// SetMitigationLevel(SecurityLevelExtreme).
|
||||||
|
// Publish()
|
||||||
status.SaveAsync()
|
//
|
||||||
}
|
// // Once you're done, delete the threat
|
||||||
|
// threat.Delete().Publish()
|
||||||
// GetThreats returns all threats who's IDs are prefixed by the given string, and also a locker for editing them.
|
//
|
||||||
func GetThreats(idPrefix string) ([]*Threat, sync.Locker) {
|
func NewThreat(id, title, msg string) *Threat {
|
||||||
status.Lock()
|
t := &Threat{
|
||||||
defer status.Unlock()
|
Notification: ¬ifications.Notification{
|
||||||
|
EventID: id,
|
||||||
var exportedThreats []*Threat
|
Type: notifications.Warning,
|
||||||
for id, threat := range status.Threats {
|
Title: title,
|
||||||
if strings.HasPrefix(id, idPrefix) {
|
Category: "Threat",
|
||||||
exportedThreats = append(exportedThreats, threat)
|
Message: msg,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return exportedThreats, &status.Mutex
|
t.threatData().Started = time.Now().Unix()
|
||||||
|
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SystemStatus) updateThreatMitigationLevel() {
|
// SetData sets the data member of the threat payload.
|
||||||
// get highest mitigationLevel
|
func (t *Threat) SetData(data interface{}) *Threat {
|
||||||
var mitigationLevel uint8
|
t.Lock()
|
||||||
for _, threat := range s.Threats {
|
defer t.Unlock()
|
||||||
switch threat.MitigationLevel {
|
|
||||||
case SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme:
|
t.threatData().Data = data
|
||||||
if threat.MitigationLevel > mitigationLevel {
|
return t
|
||||||
mitigationLevel = threat.MitigationLevel
|
}
|
||||||
}
|
|
||||||
}
|
// SetMitigationLevel sets the mitigation level of the
|
||||||
|
// threat data.
|
||||||
|
func (t *Threat) SetMitigationLevel(lvl uint8) *Threat {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
t.threatData().MitigationLevel = lvl
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete sets the ended timestamp of the threat.
|
||||||
|
func (t *Threat) Delete() *Threat {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
t.threatData().Ended = time.Now().Unix()
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload returns a copy of the threat payload.
|
||||||
|
func (t *Threat) Payload() ThreatPayload {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
return *t.threatData() // creates a copy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish publishes the current threat.
|
||||||
|
// Publish should always be called when changes to
|
||||||
|
// the threat are recorded.
|
||||||
|
func (t *Threat) Publish() *Threat {
|
||||||
|
data := t.Payload()
|
||||||
|
if data.Ended > 0 {
|
||||||
|
DeleteMitigationLevel(t.EventID)
|
||||||
|
} else {
|
||||||
|
SetMitigationLevel(t.EventID, data.MitigationLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set new ThreatMitigationLevel
|
t.Save()
|
||||||
s.ThreatMitigationLevel = mitigationLevel
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// threatData returns the threat payload associated with this
|
||||||
|
// threat. If not data has been created yet a new ThreatPayload
|
||||||
|
// is attached to t and returned. The caller must make sure to
|
||||||
|
// hold appropriate locks when working with the returned payload.
|
||||||
|
func (t *Threat) threatData() *ThreatPayload {
|
||||||
|
if t.EventData == nil {
|
||||||
|
t.EventData = new(ThreatPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := t.EventData.(*ThreatPayload)
|
||||||
|
if !ok {
|
||||||
|
log.Warningf("unexpected type %T in thread notification payload", t.EventData)
|
||||||
|
return new(ThreatPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
|
|
23
ui/serve.go
23
ui/serve.go
|
@ -28,7 +28,7 @@ func registerRoutes() error {
|
||||||
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}", redirAddSlash).Methods("GET", "HEAD")
|
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}", redirAddSlash).Methods("GET", "HEAD")
|
||||||
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}/", ServeBundle("")).Methods("GET", "HEAD")
|
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}/", ServeBundle("")).Methods("GET", "HEAD")
|
||||||
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", ServeBundle("")).Methods("GET", "HEAD")
|
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", ServeBundle("")).Methods("GET", "HEAD")
|
||||||
api.RegisterHandleFunc("/", RedirectToBase)
|
api.RegisterHandleFunc("/", redirectToDefault)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -97,13 +97,21 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri
|
||||||
readCloser, err := bundle.Open(path)
|
readCloser, err := bundle.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == resources.ErrNotFound {
|
if err == resources.ErrNotFound {
|
||||||
log.Tracef("ui: requested resource \"%s\" not found in bundle %s: %s", path, bundleName, err)
|
// Check if there is a base index.html file we can serve instead.
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
var indexErr error
|
||||||
|
path = "index.html"
|
||||||
|
readCloser, indexErr = bundle.Open(path)
|
||||||
|
if indexErr != nil {
|
||||||
|
// If we cannot get an index, continue with handling the original error.
|
||||||
|
log.Tracef("ui: requested resource \"%s\" not found in bundle %s: %s", path, bundleName, err)
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Tracef("ui: error opening module %s: %s", bundleName, err)
|
log.Tracef("ui: error opening module %s: %s", bundleName, err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set content type
|
// set content type
|
||||||
|
@ -131,9 +139,9 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri
|
||||||
readCloser.Close()
|
readCloser.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedirectToBase redirects the requests to the control app
|
// redirectToDefault redirects the request to the default UI module.
|
||||||
func RedirectToBase(w http.ResponseWriter, r *http.Request) {
|
func redirectToDefault(w http.ResponseWriter, r *http.Request) {
|
||||||
u, err := url.Parse("/ui/modules/base/")
|
u, err := url.Parse("/ui/modules/portmaster/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -141,6 +149,7 @@ func RedirectToBase(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, r.URL.ResolveReference(u).String(), http.StatusTemporaryRedirect)
|
http.Redirect(w, r, r.URL.ResolveReference(u).String(), http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redirAddSlash redirects the request to the same, but with a trailing slash.
|
||||||
func redirAddSlash(w http.ResponseWriter, r *http.Request) {
|
func redirAddSlash(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusPermanentRedirect)
|
http.Redirect(w, r, r.RequestURI+"/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package updates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/config"
|
"github.com/safing/portbase/config"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
@ -15,42 +14,56 @@ const (
|
||||||
var (
|
var (
|
||||||
releaseChannel config.StringOption
|
releaseChannel config.StringOption
|
||||||
devMode config.BoolOption
|
devMode config.BoolOption
|
||||||
disableUpdates config.BoolOption
|
enableUpdates config.BoolOption
|
||||||
|
|
||||||
previousReleaseChannel string
|
previousReleaseChannel string
|
||||||
updatesCurrentlyDisabled bool
|
updatesCurrentlyEnabled bool
|
||||||
previousDevMode bool
|
previousDevMode bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerConfig() error {
|
func registerConfig() error {
|
||||||
err := config.Register(&config.Option{
|
err := config.Register(&config.Option{
|
||||||
Name: "Release Channel",
|
Name: "Release Channel",
|
||||||
Key: releaseChannelKey,
|
Key: releaseChannelKey,
|
||||||
Description: "The Release Channel changes which updates are applied. When using beta, you will receive new features earlier and Portmaster will update more frequently. Some beta or experimental features are also available in the stable release channel.",
|
Description: "Switch release channel.",
|
||||||
Order: 1,
|
|
||||||
OptType: config.OptTypeString,
|
OptType: config.OptTypeString,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
ReleaseLevel: config.ReleaseLevelBeta,
|
ReleaseLevel: config.ReleaseLevelExperimental,
|
||||||
RequiresRestart: false,
|
RequiresRestart: false,
|
||||||
DefaultValue: releaseChannelStable,
|
DefaultValue: releaseChannelStable,
|
||||||
ExternalOptType: "string list",
|
PossibleValues: []config.PossibleValue{
|
||||||
ValidationRegex: fmt.Sprintf("^(%s|%s)$", releaseChannelStable, releaseChannelBeta),
|
{
|
||||||
|
Name: "Stable",
|
||||||
|
Value: releaseChannelStable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Beta",
|
||||||
|
Value: releaseChannelBeta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: -4,
|
||||||
|
config.DisplayHintAnnotation: config.DisplayHintOneOf,
|
||||||
|
config.CategoryAnnotation: "Updates",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Disable Updates",
|
Name: "Automatic Updates",
|
||||||
Key: disableUpdatesKey,
|
Key: enableUpdatesKey,
|
||||||
Description: "Disable automatic updates.",
|
Description: "Enable automatic checking, downloading and applying of updates. This affects all kinds of updates, including intelligence feeds and broadcast notifications.",
|
||||||
Order: 64,
|
|
||||||
OptType: config.OptTypeBool,
|
OptType: config.OptTypeBool,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
ReleaseLevel: config.ReleaseLevelStable,
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
RequiresRestart: false,
|
RequiresRestart: false,
|
||||||
DefaultValue: false,
|
DefaultValue: true,
|
||||||
ExternalOptType: "disable updates",
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: -12,
|
||||||
|
config.CategoryAnnotation: "Updates",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -63,8 +76,8 @@ func initConfig() {
|
||||||
releaseChannel = config.GetAsString(releaseChannelKey, releaseChannelStable)
|
releaseChannel = config.GetAsString(releaseChannelKey, releaseChannelStable)
|
||||||
previousReleaseChannel = releaseChannel()
|
previousReleaseChannel = releaseChannel()
|
||||||
|
|
||||||
disableUpdates = config.GetAsBool(disableUpdatesKey, false)
|
enableUpdates = config.GetAsBool(enableUpdatesKey, true)
|
||||||
updatesCurrentlyDisabled = disableUpdates()
|
updatesCurrentlyEnabled = enableUpdates()
|
||||||
|
|
||||||
devMode = config.GetAsBool(cfgDevModeKey, false)
|
devMode = config.GetAsBool(cfgDevModeKey, false)
|
||||||
previousDevMode = devMode()
|
previousDevMode = devMode()
|
||||||
|
@ -86,10 +99,10 @@ func updateRegistryConfig(_ context.Context, _ interface{}) error {
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if disableUpdates() != updatesCurrentlyDisabled {
|
if enableUpdates() != updatesCurrentlyEnabled {
|
||||||
updatesCurrentlyDisabled = disableUpdates()
|
updatesCurrentlyEnabled = enableUpdates()
|
||||||
changed = true
|
changed = true
|
||||||
forceUpdate = !updatesCurrentlyDisabled
|
forceUpdate = updatesCurrentlyEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
|
@ -100,7 +113,7 @@ func updateRegistryConfig(_ context.Context, _ interface{}) error {
|
||||||
module.Resolve(updateFailed)
|
module.Resolve(updateFailed)
|
||||||
_ = TriggerUpdate()
|
_ = TriggerUpdate()
|
||||||
log.Infof("updates: automatic updates enabled again.")
|
log.Infof("updates: automatic updates enabled again.")
|
||||||
} else if updatesCurrentlyDisabled {
|
} else if !updatesCurrentlyEnabled {
|
||||||
module.Warning(updateFailed, "Automatic updates are disabled! This also affects security updates and threat intelligence.")
|
module.Warning(updateFailed, "Automatic updates are disabled! This also affects security updates and threat intelligence.")
|
||||||
log.Warningf("updates: automatic updates are now disabled.")
|
log.Warningf("updates: automatic updates are now disabled.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ type versions struct {
|
||||||
|
|
||||||
Core *info.Info
|
Core *info.Info
|
||||||
Resources map[string]*updater.Resource
|
Resources map[string]*updater.Resource
|
||||||
|
Beta bool
|
||||||
|
Staging bool
|
||||||
|
|
||||||
internalSave bool
|
internalSave bool
|
||||||
}
|
}
|
||||||
|
@ -43,6 +45,8 @@ func initVersionExport() (err error) {
|
||||||
// init export struct
|
// init export struct
|
||||||
versionExport = &versions{
|
versionExport = &versions{
|
||||||
internalSave: true,
|
internalSave: true,
|
||||||
|
Beta: registry.Beta,
|
||||||
|
Staging: staging,
|
||||||
}
|
}
|
||||||
versionExport.SetKey(versionsDBKey)
|
versionExport.SetKey(versionsDBKey)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ const (
|
||||||
releaseChannelStable = "stable"
|
releaseChannelStable = "stable"
|
||||||
releaseChannelBeta = "beta"
|
releaseChannelBeta = "beta"
|
||||||
|
|
||||||
disableUpdatesKey = "core/disableUpdates"
|
enableUpdatesKey = "core/automaticUpdates"
|
||||||
|
|
||||||
// ModuleName is the name of the update module
|
// ModuleName is the name of the update module
|
||||||
// and can be used when declaring module dependencies.
|
// and can be used when declaring module dependencies.
|
||||||
|
@ -49,6 +51,7 @@ var (
|
||||||
module *modules.Module
|
module *modules.Module
|
||||||
registry *updater.ResourceRegistry
|
registry *updater.ResourceRegistry
|
||||||
userAgentFromFlag string
|
userAgentFromFlag string
|
||||||
|
staging bool
|
||||||
|
|
||||||
updateTask *modules.Task
|
updateTask *modules.Task
|
||||||
updateASAP bool
|
updateASAP bool
|
||||||
|
@ -76,13 +79,13 @@ func init() {
|
||||||
module.RegisterEvent(ResourceUpdateEvent)
|
module.RegisterEvent(ResourceUpdateEvent)
|
||||||
|
|
||||||
flag.StringVar(&userAgentFromFlag, "update-agent", "", "Sets the user agent for requests to the update server")
|
flag.StringVar(&userAgentFromFlag, "update-agent", "", "Sets the user agent for requests to the update server")
|
||||||
|
flag.BoolVar(&staging, "staging", false, "Use staging update channel (for testing only)")
|
||||||
|
|
||||||
// initialize mandatory updates
|
// initialize mandatory updates
|
||||||
if onWindows {
|
if onWindows {
|
||||||
MandatoryUpdates = []string{
|
MandatoryUpdates = []string{
|
||||||
platform("core/portmaster-core.exe"),
|
platform("core/portmaster-core.exe"),
|
||||||
platform("start/portmaster-start.exe"),
|
platform("start/portmaster-start.exe"),
|
||||||
platform("app/portmaster-app.exe"),
|
|
||||||
platform("notifier/portmaster-notifier.exe"),
|
platform("notifier/portmaster-notifier.exe"),
|
||||||
platform("notifier/portmaster-snoretoast.exe"),
|
platform("notifier/portmaster-snoretoast.exe"),
|
||||||
}
|
}
|
||||||
|
@ -90,10 +93,15 @@ func init() {
|
||||||
MandatoryUpdates = []string{
|
MandatoryUpdates = []string{
|
||||||
platform("core/portmaster-core"),
|
platform("core/portmaster-core"),
|
||||||
platform("start/portmaster-start"),
|
platform("start/portmaster-start"),
|
||||||
platform("app/portmaster-app"),
|
|
||||||
platform("notifier/portmaster-notifier"),
|
platform("notifier/portmaster-notifier"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MandatoryUpdates = append(
|
||||||
|
MandatoryUpdates,
|
||||||
|
platform("app/portmaster-app.zip"),
|
||||||
|
"all/ui/modules/portmaster.zip",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
|
@ -139,9 +147,12 @@ func start() error {
|
||||||
},
|
},
|
||||||
UserAgent: UserAgent,
|
UserAgent: UserAgent,
|
||||||
MandatoryUpdates: MandatoryUpdates,
|
MandatoryUpdates: MandatoryUpdates,
|
||||||
Beta: releaseChannel() == releaseChannelBeta,
|
AutoUnpack: []string{
|
||||||
DevMode: devMode(),
|
platform("app/portmaster-app.zip"),
|
||||||
Online: true,
|
},
|
||||||
|
Beta: releaseChannel() == releaseChannelBeta,
|
||||||
|
DevMode: devMode(),
|
||||||
|
Online: true,
|
||||||
}
|
}
|
||||||
if userAgentFromFlag != "" {
|
if userAgentFromFlag != "" {
|
||||||
// override with flag value
|
// override with flag value
|
||||||
|
@ -159,18 +170,33 @@ func start() error {
|
||||||
Beta: false,
|
Beta: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
registry.AddIndex(updater.Index{
|
if registry.Beta {
|
||||||
Path: "beta.json",
|
registry.AddIndex(updater.Index{
|
||||||
Stable: false,
|
Path: "beta.json",
|
||||||
Beta: true,
|
Stable: false,
|
||||||
})
|
Beta: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
registry.AddIndex(updater.Index{
|
registry.AddIndex(updater.Index{
|
||||||
Path: "all/intel/intel.json",
|
Path: "all/intel/intel.json",
|
||||||
Stable: true,
|
Stable: true,
|
||||||
Beta: false,
|
Beta: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if stagingActive() {
|
||||||
|
// Set flag no matter how staging was activated.
|
||||||
|
staging = true
|
||||||
|
|
||||||
|
log.Warning("updates: staging environment is active")
|
||||||
|
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "staging.json",
|
||||||
|
Stable: true,
|
||||||
|
Beta: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
err = registry.LoadIndexes(module.Ctx)
|
err = registry.LoadIndexes(module.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("updates: failed to load indexes: %s", err)
|
log.Warningf("updates: failed to load indexes: %s", err)
|
||||||
|
@ -184,6 +210,7 @@ func start() error {
|
||||||
registry.SelectVersions()
|
registry.SelectVersions()
|
||||||
module.TriggerEvent(VersionUpdateEvent, nil)
|
module.TriggerEvent(VersionUpdateEvent, nil)
|
||||||
|
|
||||||
|
// Initialize the version export - this requires the registry to be set up.
|
||||||
err = initVersionExport()
|
err = initVersionExport()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -245,7 +272,7 @@ func DisableUpdateSchedule() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkForUpdates(ctx context.Context) (err error) {
|
func checkForUpdates(ctx context.Context) (err error) {
|
||||||
if updatesCurrentlyDisabled {
|
if !updatesCurrentlyEnabled {
|
||||||
log.Debugf("updates: automatic updates are disabled")
|
log.Debugf("updates: automatic updates are disabled")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -257,7 +284,7 @@ func checkForUpdates(ctx context.Context) (err error) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
module.Resolve(updateInProgress)
|
module.Resolve(updateInProgress)
|
||||||
} else {
|
} else {
|
||||||
module.Warning(updateFailed, "Failed to check for updates: "+err.Error())
|
module.Warning(updateFailed, "Failed to update: "+err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -273,6 +300,13 @@ func checkForUpdates(ctx context.Context) (err error) {
|
||||||
|
|
||||||
registry.SelectVersions()
|
registry.SelectVersions()
|
||||||
|
|
||||||
|
// Unpack selected resources.
|
||||||
|
err = registry.UnpackResources()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("failed to update: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
module.TriggerEvent(ResourceUpdateEvent, nil)
|
module.TriggerEvent(ResourceUpdateEvent, nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -288,3 +322,14 @@ func stop() error {
|
||||||
func platform(identifier string) string {
|
func platform(identifier string) string {
|
||||||
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
|
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stagingActive() bool {
|
||||||
|
// Check flag and env variable.
|
||||||
|
if staging || os.Getenv("PORTMASTER_STAGING") == "enabled" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if staging index is present and acessible.
|
||||||
|
_, err := os.Stat(filepath.Join(registry.StorageDir().Path, "staging.json"))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
processInfo "github.com/shirou/gopsutil/process"
|
processInfo "github.com/shirou/gopsutil/process"
|
||||||
"github.com/tevino/abool"
|
"github.com/tevino/abool"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/dataroot"
|
||||||
"github.com/safing/portbase/info"
|
"github.com/safing/portbase/info"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/notifications"
|
"github.com/safing/portbase/notifications"
|
||||||
|
@ -99,18 +100,30 @@ func upgradeCoreNotify() error {
|
||||||
|
|
||||||
// check for new version
|
// check for new version
|
||||||
if info.GetInfo().Version != pmCoreUpdate.Version() {
|
if info.GetInfo().Version != pmCoreUpdate.Version() {
|
||||||
n := notifications.NotifyInfo(
|
n := notifications.Notify(¬ifications.Notification{
|
||||||
"updates:core-update-available",
|
EventID: "updates:core-update-available",
|
||||||
fmt.Sprintf("There is an update available for the Portmaster core (v%s), please restart the Portmaster to apply the update.", pmCoreUpdate.Version()),
|
Type: notifications.Info,
|
||||||
notifications.Action{
|
Title: fmt.Sprintf(
|
||||||
ID: "later",
|
"Portmaster Update v%s",
|
||||||
Text: "Later",
|
pmCoreUpdate.Version(),
|
||||||
|
),
|
||||||
|
Category: "Core",
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
`:tada: Update to **Portmaster v%s** is available!
|
||||||
|
Please restart the Portmaster to apply the update.`,
|
||||||
|
pmCoreUpdate.Version(),
|
||||||
|
),
|
||||||
|
AvailableActions: []*notifications.Action{
|
||||||
|
{
|
||||||
|
ID: "restart",
|
||||||
|
Text: "Restart",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "later",
|
||||||
|
Text: "Not now",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
notifications.Action{
|
})
|
||||||
ID: "restart",
|
|
||||||
Text: "Restart Portmaster Now",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
n.SetActionFunction(upgradeCoreNotifyActionHandler)
|
n.SetActionFunction(upgradeCoreNotifyActionHandler)
|
||||||
|
|
||||||
log.Debugf("updates: new portmaster version available, sending notification to user")
|
log.Debugf("updates: new portmaster version available, sending notification to user")
|
||||||
|
@ -119,7 +132,7 @@ func upgradeCoreNotify() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func upgradeCoreNotifyActionHandler(n *notifications.Notification) {
|
func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error {
|
||||||
switch n.SelectedActionID {
|
switch n.SelectedActionID {
|
||||||
case "restart":
|
case "restart":
|
||||||
// Cannot directly trigger due to import loop.
|
// Cannot directly trigger due to import loop.
|
||||||
|
@ -130,11 +143,13 @@ func upgradeCoreNotifyActionHandler(n *notifications.Notification) {
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("updates: failed to trigger restart via notification: %s", err)
|
return fmt.Errorf("failed to trigger restart via notification: %s", err)
|
||||||
}
|
}
|
||||||
case "later":
|
case "later":
|
||||||
n.Expires = time.Now().Unix() // expire immediately
|
return n.Delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func upgradeHub() error {
|
func upgradeHub() error {
|
||||||
|
@ -192,12 +207,11 @@ func upgradePortmasterStart() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// update portmaster-start in data root
|
// update portmaster-start in data root
|
||||||
rootPmStartPath := filepath.Join(filepath.Dir(registry.StorageDir().Path), filename)
|
rootPmStartPath := filepath.Join(dataroot.Root().Path, filename)
|
||||||
err := upgradeFile(rootPmStartPath, pmCtrlUpdate)
|
err := upgradeFile(rootPmStartPath, pmCtrlUpdate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Infof("updates: upgraded %s", rootPmStartPath)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -244,10 +258,18 @@ func warnOnIncorrectParentPath() {
|
||||||
if !strings.HasPrefix(absPath, root) {
|
if !strings.HasPrefix(absPath, root) {
|
||||||
log.Warningf("detected unexpected path %s for portmaster-start", absPath)
|
log.Warningf("detected unexpected path %s for portmaster-start", absPath)
|
||||||
|
|
||||||
notifications.NotifyWarn(
|
notifications.Notify(¬ifications.Notification{
|
||||||
"updates:unsupported-parent",
|
EventID: "updates:unsupported-parent",
|
||||||
fmt.Sprintf("The portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", expectedFileName, absPath, filepath.Join(root, expectedFileName)),
|
Type: notifications.Warning,
|
||||||
)
|
Title: "Unsupported Launcher",
|
||||||
|
Category: "Core",
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"The portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.",
|
||||||
|
expectedFileName,
|
||||||
|
absPath,
|
||||||
|
filepath.Join(root, expectedFileName),
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,6 +290,7 @@ func upgradeFile(fileToUpgrade string, file *updater.File) error {
|
||||||
// abort if version matches
|
// abort if version matches
|
||||||
currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
|
currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
|
||||||
if currentVersion == file.Version() {
|
if currentVersion == file.Version() {
|
||||||
|
log.Tracef("updates: %s is already v%s", fileToUpgrade, file.Version())
|
||||||
// already up to date!
|
// already up to date!
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -330,6 +353,8 @@ func upgradeFile(fileToUpgrade string, file *updater.File) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue