Files
portmaster/service/process/profile.go
2025-05-22 15:40:06 +03:00

180 lines
5.8 KiB
Go

package process
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/profile"
)
var ownPID = os.Getpid()
// GetProfile finds and assigns a profile set to the process.
func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
p.Lock()
defer p.Unlock()
// Check if profile is already loaded.
if p.profile != nil {
log.Tracer(ctx).Trace("process: profile already loaded")
return
}
// If not, continue with loading the profile.
log.Tracer(ctx).Trace("process: loading profile")
// Get special or regular profile.
localProfile, err := profile.GetLocalProfile(p.getSpecialProfileID(), p.MatchingData(), p.CreateProfileCallback)
if err != nil {
return false, fmt.Errorf("failed to find profile: %w", err)
}
// Assign profile to process.
p.PrimaryProfileID = localProfile.ScopedID()
p.profile = localProfile.LayeredProfile()
return true, nil
}
// RefetchProfile removes the profile and finds and assigns a new profile.
func (p *Process) RefetchProfile(ctx context.Context) error {
p.Lock()
defer p.Unlock()
// Get special or regular profile.
localProfile, err := profile.GetLocalProfile(p.getSpecialProfileID(), p.MatchingData(), p.CreateProfileCallback)
if err != nil {
return fmt.Errorf("failed to find profile: %w", err)
}
// Assign profile to process.
p.PrimaryProfileID = localProfile.ScopedID()
p.profile = localProfile.LayeredProfile()
return nil
}
// getSpecialProfileID returns the special profile ID for the process, if any.
func (p *Process) getSpecialProfileID() (specialProfileID string) {
// Check if we need a special profile.
switch p.Pid {
case UnidentifiedProcessID:
specialProfileID = profile.UnidentifiedProfileID
case UnsolicitedProcessID:
specialProfileID = profile.UnsolicitedProfileID
case SystemProcessID:
specialProfileID = profile.SystemProfileID
case ownPID:
specialProfileID = profile.PortmasterProfileID
default:
// Check if this is another Portmaster component.
if p.IsPortmasterUi(context.Background()) {
specialProfileID = profile.PortmasterAppProfileID
}
// Check if this is the system resolver.
switch runtime.GOOS {
case "windows":
// Depending on the OS version System32 may be capitalized or not.
if (p.Path == `C:\Windows\System32\svchost.exe` ||
p.Path == `C:\Windows\system32\svchost.exe`) &&
// This comes from the windows tasklist command and should be pretty consistent.
(profile.KeyAndValueInTags(p.Tags, "svchost", "Dnscache") ||
// As an alternative in case of failure, we try to match the svchost.exe service parameter.
strings.Contains(p.CmdLine, "-s Dnscache")) {
specialProfileID = profile.SystemResolverProfileID
}
case "linux":
switch p.Path {
case "/lib/systemd/systemd-resolved",
"/usr/lib/systemd/systemd-resolved",
"/lib64/systemd/systemd-resolved",
"/usr/lib64/systemd/systemd-resolved",
"/usr/bin/nscd",
"/usr/sbin/nscd",
"/usr/bin/dnsmasq",
"/usr/sbin/dnsmasq":
specialProfileID = profile.SystemResolverProfileID
}
}
}
return specialProfileID
}
// IsPortmasterUi checks if the process is the Portmaster UI or its child (up to 3 parent levels).
func (p *Process) IsPortmasterUi(ctx context.Context) bool {
if module.portmasterUIPath == "" {
return false
}
// Find parent for up to two levels, if we don't match the path.
const checkLevels = 3
var previousPid int
proc := p
hasPmWebviewEnvVar := false
for i := 0; i < checkLevels; i++ {
if proc.Pid == UnidentifiedProcessID || proc.Pid == SystemProcessID {
break
}
realPath, err := filepath.EvalSymlinks(proc.Path)
if err == nil && realPath == module.portmasterUIPath {
if runtime.GOOS != "windows" {
return true
}
// On Windows, avoid false positive detection of the Portmaster UI.
// For example:
// There may be cases where a system browser is launched from the Portmaster UI,
// making it a child of the Portmaster UI process (e.g., user clicked a link in the UI).
// In this case, the parent process tree may look like this:
// Portmaster.exe
// ├─ WebView (PM UI)
// │ └─ WebView (PM UI child)
// └─ System Web Browser ...
//
// To ensure that 'p' is the actual Portmaster UI process, we check for the presence
// of the 'PORTMASTER_UI_WEBVIEW_PROCESS' environment variable in the process and its parents.
// If the env var is set, we are a child (WebView window) of the Portmaster UI process.
// Otherwise, the process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process.
if i == 0 {
return true // We are the main Portmaster UI process.
}
if hasPmWebviewEnvVar {
return true // We are a WebView window of the Portmaster UI process.
}
// The process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process.
log.Tracer(ctx).Warningf("process: %d %q is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path)
return false
}
// Check if the process has the environment variable set.
//
// It is OK to check for the existence of the environment variable in all
// processes in the parent chain (on all loop iterations). This increases the
// chance of correct detection, even if a child or grandchild WebView process
// did not inherit the environment variable for some reason.
if _, ok := proc.Env["PORTMASTER_UI_WEBVIEW_PROCESS"]; ok {
hasPmWebviewEnvVar = true
}
if i < checkLevels-1 { // no need to check parent if we are at the last level
previousPid = proc.Pid
proc, err = GetOrFindProcess(ctx, proc.ParentPid)
if err != nil || proc.Pid == previousPid {
break
}
}
}
return false
}