portmaster
portmaster copied to clipboard
Implement Plugin and Extension System
:rocket: This PR adds support for external Plugins to the Portmaster :rocket:
Notes For Reviewers
The config handling part of the plugin system requires a fix in portbase: https://github.com/safing/portbase/pull/185
Roughly the following packages have been created:
-
plugin
: contains the whole plugin system and portbase module -
plugin/shared/*
: contains code that must be imported by the Portmaster and each plugin instance. It defines the different plugin types and implements the GRPC Server and Client code -
plugin/shared/proto
: Go-Source files inplugin/shared/proto
are auto-generated using the protobuf compiler and it's GRPC plugin. Just review the*.proto
files in there and ignore all the*.pb.go
files. -
plugin/internal
: contains internal stuff that should only be used by the Portmaster and not imported by plugins (to avoid a huge dependency on Portmaster code) -
plugin/loader
: contains the plugin loader and the lifecycle management code for plugins -
plugin/framework
: provides a utility framework for writing plugins without the need to handle a lot of boilerplate code.
Introduction
The plugin system is based on hashicorp/go-plugin and uses a sub-process architecture where the plugin host (the Portmaster) and plugins communicate via gRPC on a localhost/Unix-Socket HTTP/2 connection.
Plugin Types
The package defines a couple of different types that plugin authors may implement depending on their planned feature set:
-
Decider Plugins
"decider"
A decider plugin is used by the firewall system to decide if a connection or DNS request is allowed, blocked or routed through the SPN. -
Reporter Plugins
"reporter"
A reporter plugin is notified whenever the firewall found a decision on a new connection or DNS request. Reporter plugins may be used to store connection history in places not directly supported by the Portmaster. -
Resolver Plugins
"resolver"
A resolver plugin is called when the Portmaster tries to resolve a DNS query. Resolver plugins are called before the actual resolvers configured in the Portmaster are queried.
Capabilities
In addition to the plugin types available plugins also have access to the following Portmaster systems:
-
Config System: Plugins can register custom configuration options that will show up in the Portmaster user interface and can watch for changes to those configuration options. By default, plugins are restricted to only access configuration options they registered themselves (that is, configuration keys are always prefixed with "plugins/
"). If a plugin is marked as privileged in the JSON configuration file the plugin may access all configuration options registered in the Portmaster, even internal configuration options. Plugin authors should be really careful when using or working with Portmaster internal options as there is no guarantee of their availability and might be changed with any Portmaster release. -
Notification System: Plugins may display custom notifications to the user with support for notification actions. Plugins may also "take-over" notifications and can present the to the user different ways (like pushing to a mobile phone). Plugin developers must make sure to not take-over notifications whose defined actions cannot be supported by the plugin implementation. That is, most actions defined in the proto package are meant to be displayed and executed by the User Interface. An exception to this, for example, is the Webhook action which may easily implemented by plugins as well.
-
Plugin Manager: Plugins that are configured as privileged in the JSON configuration file also get access to the plugin management system of Portmaster. That is, privileged plugins may register, start and stop or remove additional plugins at the Portmaster. This feature is mainly designed so the Portmaster development community is capable of implementing third-party plugin registries that automate the installation and management of plugins.
Configuration
The plugin system first needs to be enabled by the user. A new subsystem is created for the plugin system that is visible in Developer Mode
when the Feature Stability is set to Experimental
.
Afterwards, place your plugins (the binaries) into the plugins folder in the Portmaster installation directory. On linux, this is normally somewhere at /opt/safing/portmaster/plugins
. That's the only path the plugin loader searches for those executables right now.
Next, enable your plugin by creating/editing /opt/safing/portmaster/plugins.json
and add an entry for your plugin:
[
{
"name": "my-portmaster-plugin",
"types": [
"decider",
"reporter",
],
"config": null,
}
]
Examples
Custom Decider
The following is a simple example of a decider
plugin:
package main
import (
"context"
"log"
"github.com/safing/portmaster/plugin/framework"
"github.com/safing/portmaster/plugin/shared/proto"
)
func DecideOnConnection(_ context.Context, conn *proto.Connection) (proto.Verdict, string, error) {
if conn.GetProcess().BinaryPath == "/usr/bin/curl" {
switch conn.GetEntity().GetDomain() {
case "example.com.", "safing.io.":
return proto.Verdict_VERDICT_ACCEPT, "allowed", nil
}
}
log.Printf("ignoring decision request for %s: %s", conn.Id, conn.GetEntity().GetDomain())
return proto.Verdict_VERDICT_UNDECIDED, "", nil
}
func main() {
// Register the decider plugin type
framework.RegisterDecider(framework.DeciderFunc(DecideOnConnection))
// Serve the plugin
framework.Serve()
}
Next, build the plugin (go build .
) and place the binary (let's say my-plugin
) into /opt/safing/portmaster/plugins
. Finally, make sure the plugins configuration array in /opt/safing/portmaster/plugins.json
contains the follwing:
[
{
"name": "my-plugin",
"types": [
"decider"
]
}
]
Available Plugins
You can also check out some of the plugins I already created when implementing and testing the Plugin System (this list will be extended in the next days when I start pushing the plugins to github):
DISCLAIMER: none of the following plugins are offical Safing products. Please do not report issues with those plugins in the offical Safing repositories. Rather post the issue in the plugin repositories and I'll try to take care :)
-
ppacher/portmaster-plugin-yaegi: provide "Rules-As-Code" using the Go-Interpreter from
traefik/yaegi
. - ppacher/portmaster-plugin-prometheus: expose connection statistics to a Prometheus server. Supports pull and push mode.
- ppacher/portmaster-plugin-hosts: Add support for resolving DNS queries using /etc/hosts
- ppacher/portmaster-plugin-dnscrypt: add support to resolve DNS queries using a DNSCrypt server
I had a first high-level look at the plugin system. Here are my first thoughts.
- I really like how this can be used to easily extend Portmaster.
- It's great that plugins are a separate processes and do not pose a direct threat to Portmaster integrity.
- Loading the plugins is a bit of a question mark for me:
- It seems that plugin can be loaded at any time and also plugins can load other plugins.
- I feel this is a bit excessive and think we would benefit from a "closer" integration into Portmaster.
- One idea would be to only load plugins on start and do so before starting the module system of Portmaster.
- This way everything blends in nicer with the current managing of modules, eg. configuration.
- Plugins would then be registered as modules themselves and list their dependencies.
- This ensures a proper and fast boot and shutdown of all plugins.
I hope a tighter integration will save us from other problems, as we are already having with the config system, where I feel that current proposed changes open quite some edge cases that will be weird and hard to nicely handle with a good user experience.
In my use case scenarios, plugins are a predominantly permanent configuration, so I would not enable/disable them via a config flag, but would just use the plugins config file, which is the first thing that is loaded. Also, plugins should be either signed by Safing, or dev mode needs to be active.
Loaded plugins themselves could be nicely shown on the dashboard, including some additional information about them. Loading plugins distributed by Safing could be neatly hidden in the quick search bar - metadata about them distributed via the intel-data repo with other metadata files.
Question: How would we handle a plugin that depends on the SPN or other modules that the user can turn on and off?
Plugins would be really nice to have tbh