diff --git a/process/process.go b/process/process.go index 652c1118..3bc261bf 100644 --- a/process/process.go +++ b/process/process.go @@ -42,14 +42,18 @@ type Process struct { Cwd string CmdLine string FirstArg string - - // SpecialDetail holds special information, the meaning of which can change - // based on any of the previous attributes. - SpecialDetail string + Env map[string]string // Profile attributes. // Once set, these don't change; safe for concurrent access. + // Tags holds extended information about the (virtual) process, which is used + // to find a profile. + Tags []profile.Tag + // MatchingPath holds an alternative binary path that can be used to find a + // profile. + MatchingPath string + // PrimaryProfileID holds the scoped ID of the primary profile. PrimaryProfileID string // profile holds the layered profile based on the primary profile. @@ -177,7 +181,7 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) { } // Get process information from the system. - pInfo, err := processInfo.NewProcess(int32(pid)) + pInfo, err := processInfo.NewProcessWithContext(ctx, int32(pid)) if err != nil { return nil, err } @@ -186,7 +190,7 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) { // net yet implemented for windows if onLinux { var uids []int32 - uids, err = pInfo.Uids() + uids, err = pInfo.UidsWithContext(ctx) if err != nil { return nil, fmt.Errorf("failed to get UID for p%d: %w", pid, err) } @@ -194,7 +198,7 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) { } // Username - process.UserName, err = pInfo.Username() + process.UserName, err = pInfo.UsernameWithContext(ctx) if err != nil { return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pid, err) } @@ -203,14 +207,14 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) { // new.UserHome, err = // PPID - ppid, err := pInfo.Ppid() + ppid, err := pInfo.PpidWithContext(ctx) if err != nil { return nil, fmt.Errorf("failed to get PPID for p%d: %w", pid, err) } process.ParentPid = int(ppid) // Path - process.Path, err = pInfo.Exe() + process.Path, err = pInfo.ExeWithContext(ctx) if err != nil { return nil, fmt.Errorf("failed to get Path for p%d: %w", pid, err) } @@ -229,13 +233,13 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) { // } // Command line arguments - process.CmdLine, err = pInfo.Cmdline() + process.CmdLine, err = pInfo.CmdlineWithContext(ctx) if err != nil { return nil, fmt.Errorf("failed to get Cmdline for p%d: %w", pid, err) } // Name - process.Name, err = pInfo.Name() + process.Name, err = pInfo.NameWithContext(ctx) if err != nil { return nil, fmt.Errorf("failed to get Name for p%d: %w", pid, err) } @@ -243,9 +247,48 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) { process.Name = process.ExecName } - // OS specifics - process.specialOSInit() + // Get all environment variables + env, err := pInfo.EnvironWithContext(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get the environment for p%d: %w", pid, err) + } + // Split env variables in key and value. + process.Env = make(map[string]string, len(env)) + for _, entry := range env { + splitted := strings.SplitN(entry, "=", 2) + if len(splitted) == 2 { + process.Env[strings.Trim(splitted[0], `'"`)] = strings.Trim(splitted[1], `'"`) + } + } + + // Add process tags. + process.addTags() + if len(process.Tags) > 0 { + log.Tracer(ctx).Debugf("profile: added tags: %+v", process.Tags) + } process.Save() return process, nil } + +// MatchingData returns the matching data for the process. +func (p *Process) MatchingData() *MatchingData { + return &MatchingData{p} +} + +// MatchingData provides a interface compatible view on the process for profile matching. +type MatchingData struct { + p *Process +} + +// Tags returns process.Tags. +func (md *MatchingData) Tags() []profile.Tag { return md.p.Tags } + +// Env returns process.Env. +func (md *MatchingData) Env() map[string]string { return md.p.Env } + +// Path returns process.Path. +func (md *MatchingData) Path() string { return md.p.Path } + +// MatchingPath returns process.MatchingPath. +func (md *MatchingData) MatchingPath() string { return md.p.MatchingPath } diff --git a/process/profile.go b/process/profile.go index 6c8ceac6..aab86996 100644 --- a/process/profile.go +++ b/process/profile.go @@ -2,6 +2,7 @@ package process import ( "context" + "fmt" "os" "runtime" "strings" @@ -29,25 +30,48 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) { // If not, continue with loading the profile. log.Tracer(ctx).Trace("process: loading profile") + // Check if there is a special profile for this process. + localProfile, err := p.loadSpecialProfile(ctx) + if err != nil { + return false, fmt.Errorf("failed to load special profile: %w", err) + } + + // Otherwise, find a regular profile for the process. + if localProfile == nil { + localProfile, err = profile.GetLocalProfile("", 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 +} + +// loadSpecialProfile attempts to load a special profile. +func (p *Process) loadSpecialProfile(_ context.Context) (*profile.Profile, error) { // Check if we need a special profile. - profileID := "" + var specialProfileID string switch p.Pid { case UnidentifiedProcessID: - profileID = profile.UnidentifiedProfileID + specialProfileID = profile.UnidentifiedProfileID case UnsolicitedProcessID: - profileID = profile.UnsolicitedProfileID + specialProfileID = profile.UnsolicitedProfileID case SystemProcessID: - profileID = profile.SystemProfileID + specialProfileID = profile.SystemProfileID case ownPID: - profileID = profile.PortmasterProfileID + specialProfileID = profile.PortmasterProfileID default: // Check if this is another Portmaster component. if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) { switch { case strings.Contains(p.Path, "portmaster-app"): - profileID = profile.PortmasterAppProfileID + specialProfileID = profile.PortmasterAppProfileID case strings.Contains(p.Path, "portmaster-notifier"): - profileID = profile.PortmasterNotifierProfileID + specialProfileID = profile.PortmasterNotifierProfileID default: // Unexpected binary from within the Portmaster updates directpry. log.Warningf("process: unexpected binary in the updates directory: %s", p.Path) @@ -62,10 +86,10 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) { 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. - (strings.Contains(p.SpecialDetail, "Dnscache") || + (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")) { - profileID = profile.SystemResolverProfileID + specialProfileID = profile.SystemResolverProfileID } case "linux": switch p.Path { @@ -77,22 +101,18 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) { "/usr/sbin/nscd", "/usr/bin/dnsmasq", "/usr/sbin/dnsmasq": - profileID = profile.SystemResolverProfileID + specialProfileID = profile.SystemResolverProfileID } } } - // Get the (linked) local profile. - localProfile, err := profile.GetProfile(profile.SourceLocal, profileID, p.Path, false) - if err != nil { - return false, err + // Check if a special profile should be applied. + if specialProfileID == "" { + return nil, nil } - // Assign profile to process. - p.PrimaryProfileID = localProfile.ScopedID() - p.profile = localProfile.LayeredProfile() - - return true, nil + // Return special profile. + return profile.GetSpecialProfile(specialProfileID, p.Path) } // UpdateProfileMetadata updates the metadata of the local profile diff --git a/process/tags.go b/process/tags.go new file mode 100644 index 00000000..0eea7f49 --- /dev/null +++ b/process/tags.go @@ -0,0 +1,80 @@ +package process + +import ( + "errors" + "sync" + + "github.com/safing/portmaster/profile" +) + +var ( + tagRegistry []TagHandler + tagRegistryLock sync.RWMutex +) + +// TagHandler is a collection of process tag related interfaces. +type TagHandler interface { + // Name returns the tag handler name. + Name() string + + // TagDescriptions returns a list of all possible tags and their description + // of this handler. + TagDescriptions() []TagDescription + + // AddTags adds tags to the given process. + AddTags(p *Process) + + // CreateProfile creates a profile based on the tags of the process. + // Returns nil to skip. + CreateProfile(p *Process) *profile.Profile +} + +// TagDescription describes a tag. +type TagDescription struct { + ID string + Name string + Description string +} + +// RegisterTagHandler registers a tag handler. +func RegisterTagHandler(th TagHandler) error { + tagRegistryLock.Lock() + defer tagRegistryLock.Unlock() + + // Check if the handler is already registered. + for _, existingTH := range tagRegistry { + if th.Name() == existingTH.Name() { + return errors.New("already registered") + } + } + + tagRegistry = append(tagRegistry, th) + return nil +} + +func (p *Process) addTags() { + tagRegistryLock.RLock() + defer tagRegistryLock.RUnlock() + + for _, th := range tagRegistry { + th.AddTags(p) + } +} + +// CreateProfileCallback attempts to create a profile on special attributes +// of the process. +func (p *Process) CreateProfileCallback() *profile.Profile { + tagRegistryLock.RLock() + defer tagRegistryLock.RUnlock() + + // Go through handlers and see which one wants to create a profile. + for _, th := range tagRegistry { + newProfile := th.CreateProfile(p) + if newProfile != nil { + return newProfile + } + } + + // No handler wanted to create a profile. + return nil +}