Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Plugin and Extension System #834

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from

Conversation

ppacher
Copy link
Contributor

@ppacher ppacher commented Sep 5, 2022

🚀 This PR adds support for external Plugins to the Portmaster 🚀

Notes For Reviewers

The config handling part of the plugin system requires a fix in portbase: safing/portbase#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 in plugin/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 ppacher changed the title Feature/plugin system Implement Plugin and Extension System (aka. Portmaster as your Firewall Framework) Sep 5, 2022
@ppacher ppacher changed the title Implement Plugin and Extension System (aka. Portmaster as your Firewall Framework) Implement Plugin and Extension System Sep 5, 2022
@ppacher ppacher requested review from dhaavi and vlabo and removed request for dhaavi September 5, 2022 13:48
@ppacher ppacher mentioned this pull request Sep 12, 2022
@dhaavi
Copy link
Member

dhaavi commented Oct 11, 2023

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?

@nyabinary
Copy link

Plugins would be really nice to have tbh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants