What is a Plugin?

All the core functionality of CoPilot (censorship and networking) is provided plugins that use the CoPilot Plugin Interface. This plugin system allows developers to easily add new censorship, surveillance, or networking functionality to CoPilot without having in-depth knowledge of how CoPilot’s back-end works or having had experience working with its underlying code-base.

Why Plugins?

The CoPilot team faced continuous requests from developers during CoPilots initial research and development to add different types of functionality to CoPilot. Just as the ability to create tailored censorship profiles was critical to the long-term adoption and use of CoPilot for trainers, developers saw the ability for outsiders to extend the CoPilot software as vital to its long-term viability as a core digital security training tool. In response to this CoPilot was re-engineered to include a plugin system at its core.

What makes up a plugin?

Plugins are made up of 2 configuration files, one installation script, on start-up script, and a small script to allow the plugin to write, and save, its own configuration files. An example plugin can be found in the docs repository.

Required package files

  • plugin.conf

The configuration file for the plugin. REQUIRED

  • supervisor.conf

The supervisor program configuration that you want added to the final supervisor.conf

  • config.py

The python file that contains the ConfigWriter object used to write and validate configs. REQUIRED

  • install

The executable script that is used to install and configure the package.

  • start

The executable script that is used to start and clean up after the package. REQUIRED

  • _init_.py

An empty file that allows CoPilot to use this folder as a package. REQUIRED

How do I write a plugin?

In order to support developers using the plugin system the CoPilot team also created an example plugin and a plugin management menu that allows developers to monitor and restart a plugins from the CoPilot web interface.

Naming your plugin

A plugin must have a unique name. A plugin name should also be descriptive of its functionality so that trainers and other developers can identify the service to restart when profiles are behaving incorrectly.

Adding the required files for your plugin

  • Fork the CoPilot repository.
  • Add a folder in the plugins directory named similarly to the name of your plugin.
  • Add the following blank files to this repository.
    • package.conf
    • supervisor.conf
    • config.py
    • install
    • start
    • _init_.py

Creating a package config file

The package.conf file is a configuration file that CoPilot uses to load the package and properly integrate its functionality into the CoPilot user interface.

The info header

A configuration file must start with an info header that looks like the following:

Valid Values

name: The name of your plugin.

config_file: The name of the configuration file that CoPilot should write to.

target: The target(s) you would like trainers to be able to choose from.

actions:The action(s) you would like trainers to be able to choose from.

directory: The directory that CoPilot should store your config files in

Required Values

The following values are required.

  • name
  • config_file
  • directory
Example Config
name = my_config
config_file = my_plugin.conf
target = "website"
actions = fake
directory = /tmp/copilot/

Creating an install script

Creating a supervisor script

CoPilot uses Supervisor to control starting, stopping, and restarting its plugins. If you create a file named supervisor.conf in your plugin directory CoPilot will append it to the existing supervisor.conf. This file should be a valid supervisor program configuration section.

Example Supervisor Script
command = /home/www/copilot/copilot/plugins/myplugin/start
directory = /home/www/copilot/copilot/plugins/myplugin
user = root

Creating a start script


## Setting up the config file for my plugin
readonly config_file="/tmp/copilot/my_plugin.conf"

## In this simple case we just have to specify that we are using a config file when one exists.
## You can look at the existing plugins for how they read configuration file values.
start_create_ap() {
    if [[ -e $(config_file) ]]; then
        echo "Starting with a custom config."
        /usr/bin/my_plugin --config $(config_file)
        echo "Starting with the default config."

## If you have any cleanup of process' that may need to be reset, killed, etc. you will want to clean them up in a trap function
cleanup() {
    killall my_plugin_children
    exit 0

## In this case, this just calls one program
## More complex programs may need multiple process' run to set them up.
main() {

## The trap function for killing all process' missed by supervisor gets set.
trap 'cleanup' EXIT

## The main function that runs your plugin is called.

Creating a configuration file writer

Your plugin will have different configuration file needs than any other program. To handle this we have created a Python based “configuration writer” class that can be customized to write all manner of configuration files. You can see an example of a customized config writer in the DNS plugin

Minimal Configuration Example

Below is the most minimal example of a config writer plugin. This plugin will take rules that are passed to it and write them directly to the file specified in its profile without modifying them.

from copilot.models.config import Config

class ConfigWriter(Config):
    def __init__(self):
        super(ConfigWriter, self).__init__()
        self.config_type = "MY_PLUGIN_NAME"
The structure and API for ConfigWriter

This function takes a single rule, checks if that rule is valid, transforms and formats the rule, and adds that rule to the self._rules list in a way that can be processed by the writer.

####### Args: * rule: A list containing the action, target, and sub-target of a rule as three strings. action, target, sub_target = rule[0], rule[1], rule[2]

####### Example This example is pulled directly from the DNS plugin.

def add_rule(self, rule):
    action, target, sub = rule[0], rule[1], rule[2]
    _rule, _domain, _address = "", "", ""
    log.debug("Adding rule {0} {1} {2}".format(action, target, sub))
        _domain = self.get_dns(sub)
    except ValueError as err:
        log.warning("Could not add rule. Invalid Target.")
        raise ValueError(err)
    if action == "block":
        log.debug("Found a blocking role. Setting address to localhost (")
        _address = ""
    elif action == "redirect":
        log.debug("Found a redirection role. Setting address to default AP address (")
        _address = ""
    _rule = "{0} = {1}\n".format(_domain, _address)

Opens, and clears, the specified config file and writes the header and then all of the rules for this config. You will only want to modify this if you wish to change the fundamental way that your configuration file writes configs.

####### Example In this example the write function has been re-written to only write rules, and not write the header text to the config file. def write(self): self.prepare() log.debug("Opening config file {0} for writing.".format(self.config_file)) with open(self.config_file, 'w+') as config_file: for rule in self._rules: self.write_rule(config_file, rule)

write_rule(self, config_file, rule):

desc ####### Args: * config_file: A file handler for a config file to write to. * rule: A string that should be written to the file.

####### Example An example of an alternative write rule function that shows how it can be modified to fit different use cases.

write_header(self, config_file):


####### Args:

  • config_file: A file handler for a config file to write to.

####### Example

An example of an alternative write header function that shows how it can be modified to fit different use cases.

How data is passed to a Plugin

Adding a watchdog rule

Currently adding a watchdog rule is the only place where you will need to edit an existing piece of code. This will be streamlined in the future.

To edit this you will first need to open the watchdog script.

You will then need to add your config file to the configs that it watches. You do this by adding the following line into the main configuration block.

config_handle.add_config("MYCONFFILE", "MYSUPERVISORNAME")

Replace MYSUPERVISORNAME with the name you gave your program in your supervisor.conf file.

Replace MYCONFFILE with the config_file value from your configuration file.

How do I install a plugin?

  • Fork the CoPilot github repository
  • Add your plugin to the plugins folder.
  • Follow the Installation Guide replacing the URL of the core copilot repository with the URL of your repository.

How do I troubleshoot my plugin?

Restarting a plugin using the copilot user interface

In the copilot user interface, in the info menu you can restart a plugin by clicking on the circular arrow icon next to your plugins name.

plugins page

Checking your plugins logs

Error log location. /var/log/supervisor/PLUGIN_NAME-stderr---supervisor-*

Standard output log location. /var/log/supervisor/PLUGIN_NAME-stdout---supervisor-*