Author: Peter Haag

# Plugin Writers Guide

Plugins allow to extend NfSen to add additional functionality. There are two type of plugins: backend plugins and frontend plugins.

Fig. Plugin Concept

The backend plugins are loaded into the nfsend background process, while nfsend is started. A backend plugin may provide several functions for adding different functionalities. These include periodic data processing, alerting conditions and alerting actions. The frontend plugins may displays visually any results of the backend processing. Backend plugins are Perl Modules, where as frontend plugins are PHP files. Both plugins may exchange relevant data over the standard nfsend.comm socket.

## Installing Plugins

Plugins are stored in the BACKEND_PLUGINDIR and FRONTEND_PLUGINDIR respectively and are configured in nfsen.conf.
The configuration section of nfsen.conf:

    #
    # Plugins
    # Plugins extend NfSen for the purpose of:
    # Periodic data processing, alerting-condition and alerting-action
    # For data processing a plugin may run for any profile or for a specific profile only.
    #     Syntax: [ 'profile list', 'module' ]
    #            profile list:  ',' separated list of profiles ( 'profilegroup/profilename' ),
    #                           or '*' for any profile, '!' for no profile
    #            module:        Perl Module name equal to plugin name.
    # The profile list '!' make sense for plugins, which only provide alerting functions
    #
    # The module follows the standard Perl module conventions, with at least one
    # function: Init(). See demoplugin.pm for a simple template.
    #
    # A file with the same name in the FRONTEND_PLUGINDIR and .php extension is automatically
    # recognized as frontend plugin.
    #
    # Plugins are installed under
    # $BACKEND_PLUGINDIR and $FRONTEND_PLUGINDIR
    #
    @plugins = (
            # profile # module
            [ '*', 'demoplugin' ],
    );

For each plugin, add an entry in the array @plugins. The entry consist of two values, the profile list an the plugin name ( Perl module name ). Once your plugins are installed and configured, signal the 'nfsend' daemon to integrate the new plugin:

    BASEDIR/bin/nfsen reload

In the example above the file demoplugin.pm is stored in the specified $BACKEND_PLUGINDIR, and the file demoplugin.php in the $FRONTEND_PLUGINDIR.

Check the syslog file for possible errors reported. Successfully loaded modules are reported as shown below.

    Jun  6 10:39:23 prolog nfsen[9087]: Frontend module 'demoplugin.php' found
    Jun  6 10:39:23 prolog nfsen[9087]: demoplugin BEGIN
    Jun  6 10:39:23 prolog nfsen[9086]: nfsend: [9086]
    Jun  6 10:39:23 prolog nfsen[9087]: Loading plugin 'demoplugin': Success
    Jun  6 10:39:23 prolog nfsen[9087]: demoplugin: Init
    Jun  6 10:39:23 prolog nfsen[9087]: Initializing plugin 'demoplugin': Success
    Jun  6 10:39:23 prolog nfsen[9087]: plugin 'demoplugin': Profile plugin: 1, Alert condition plugin: 1, Alert action plugin: 1
    Jun  6 10:39:23 prolog nfsen[9087]: Plugins for profile         : ./live - demoplugin
    Jun  6 10:39:23 prolog nfsen[9087]: Plugins for Alert conditions: demoplugin
    Jun  6 10:39:23 prolog nfsen[9087]: Plugins for Alert actions   : demoplugin

## Writing Backend Plugins

### Init

The backend plugin is written as a Perl module. A plugin must at least contain these definitions:

    #
    #
    # Name of the plugin
    package PluginName;

    # highly recommended for good style Perl programming
    use strict;

    # This string identifies the plugin as a version 1.3.0 plugin.
    our $VERSION = 130;

    #
    # The Init function is called when the plugin is loaded. It's purpose is to give the plugin
    # the possibility to initialize itself. The plugin should return 1 for success or 0 for
    # failure. If the plugin fails to initialize, it's disabled and not used. Therefore, if
    # you want to temporarily disable your plugin return 0 when Init is called.
    #
    sub Init {
            return 1;
    }

    1;

Although this plugin does nothing useful, it's recognized as a plugin and loaded successfully. As there are no further subroutines defined the plugin has no relevance. However, a plugin like this may be useful to initialize any other systems, when NfSen is starting up.

Depending on the function of the plugin, dedicated subroutines are now added to the module:

- sub Cleanup

- sub run

- sub alert_condition

- sub alert_action

- Communication routines with frontend plugin

### Cleanup

This subroutine cleans up the plugin, when nfsend terminates. The plugin cleanup subroutines are called at last action before nfsend exits.

    #
    # The Cleanup function is called, when nfsend terminates. It's purpose is to give the
    # plugin the possibility to cleanup itself. It's return value is discard.
    sub Cleanup {
            syslog("info", "demoplugin Cleanup");
            # not used here
    }

### run

The run subroutine is required when periodic data processing is done. It's called at every periodic update cycle for each profile specified in the @plugins array in nfsen.conf with the appropriate parameters. This subroutine must exists, if you specify one or more profiles in nfsen.conf for this plugin, otherwise loading this plugin will fail.
For possible future extension, the arguments are passed using a argument hash. The return value is not important and discarded.

    #
    # Periodic data processing function
    #       input:  hash reference including the items:
    #               'profile'       profile name
    #               'profilegroup'  profile group
    #               'timeslot'      time of slot to process: Format yyyymmddHHMM e.g. 200503031200
    sub run {
            my $argref       = shift;

            my $profile      = $$argref{'profile'};
            my $profilegroup = $$argref{'profilegroup'};
            my $timeslot     = $$argref{'timeslot'};

            # Add your code here

    } # End of run

Important: Make sure your plugin completes within reasonable time. The sum of the execution time of all plugins must not exceed 5min, otherwise the execution cycles will pile up. However, nfsend recognizes this event and reports it via syslog.

### Working with a profile within the 'run' subroutine

You may access the data of a profile as well as the profile description data itself, by using other NfSen module's subroutine calls. Assuming the argument decoding as shown above, you may add the following code:

            my $profilepath     = NfProfile::ProfilePath($profile, $profilegroup);
            my $all_sources     = join ':', keys %{$profileinfo{'channel'}};
            my $netflow_sources = "$NfConf::PROFILEDATADIR/$profilepath/$all_sources";

$profilepath results in the proper path of the profile directory under PROFILESTATDIR or PROFILEDATADIR. The resulting $netflow_sources variable for example can then be used in a possible nfdump command:

    system("$NfConf::PREFIX/nfdump -M $netflow_sources ....");

Accessing profile description data:

    my %profileinfo     = NfProfile::ReadProfile($profile, $profilegroup);

The returned hash contains the current state and information of the profile. As of NfSen 1.3 the hash contains the following keys:

    %profileinfo = (
        'description' => [ 'The profile' ],  # Array of comment lines
        'name'        => 'live',             # name of profile
        'group'       => '.',                # name of profile group
        'tbegin'      => 1177711200,         # UNIX time of begin of profile
        'tcreate'     => 1177711200,         # UNIX time of create time of profile
        'tstart'      => 1177711200,         # UNIX time of start time of profile data
        'tend'        => 1177969800,         # UNIX time of end time of profile
        'updated'     => 1177969800,         # UNIX time of last update
        'expire'      => 24,                 # Max lifetime of profile data in hours 0 = no expire time
        'maxsize'     => 0,                  # Max size of profile in bytes  0 = no limit
        'size'        => 12354,              # Current size of profile in bytes
        'type'        => 0,                  # Profile type: 0: life, 1: history profile, 2: continuous profile
        'locked'      => 0,                  # profile locked - 0 or 1
        'status'      => "OK",               # status of profile
        'version'     => 130,                # version of profile.dat 130 for version 1.3.0
        'channel'     => {                   # hash of all channels in this profile
                           'upstream' => {   # channel name as key
                                            'order'   => '1',          # display order in graph
                                            'sign'    => '+',          # display on + or - side of y-axis
                                            'colour'  => '#abcdef'     # channel colour
                                            'sourcelist' => 'upstream' # array of ':' separated list of sources
                                         },
                         }
    );

Note: This hash is used for read-only access to the profile description data. If you need to change any of the values for any reason, you must call LockProfile instead of ReadProfile to arbitrate concurrent access followed by WriteProfile. It's the plugins responsibility to make sure that the profile description data is consistent and valid!

    my %profileinfo     = NfProfile::LockProfile($profile, $profilegroup);
    %profileinfo{'maxsize'} = 0;
    if ( !WriteProfile(\%profileinfo) ) {    # note: hash passed by reference!
                  syslog('err', "Error writing profile '$name': $Log::ERROR");
                  ...
    }

Proper data exchange between backend and frontend plugins should be done using the provided communication channels described in the Communication routines with frontend plugin section below.

### alert_condition

A plugin may also be used by the alerting module of NfSen. If a special condition can not be crafted by the alert condition controls provided by the alerting dialogue, a plugin may be called which will evaluate the proper alert condition required. If the subroutine alert_condition is detected while loading the plugin, its name will be automatically be available in the drop down menu when defining the alert.

Fig. Defining an alert using a plugin

The subroutine alert_condition is called after the the selected filter is applied to the flows. The resulting file is stored in the alert data directory and its filename is passed as a parameter to the subroutine.

    #
    # Alert condition function.
    # if defined it will be automatically listed as available plugin, when defining an alert.
    # Called after flow filter is applied. Resulting flows stored in $alertflows file
    # Should return 0 or 1 if condition is met or not
    sub alert_condition {
            my $argref = shift;

            my $alert      = $$argref{'alert'};
            my $alertflows = $$argref{'alertfile'};
            my $timeslot   = $$argref{'timeslot'};

            syslog('info', "Alert condition called: alert: $alert, alertfile: $alertflows, timeslot: $timeslot");
            # Add your code here

            return 1;
    }

It's now up to the subroutine, to return 0 or 1 depending on the condition result.

### alert_action

If an alert triggers a plugin subroutine may be called as one of the actions. If the subroutine alert_action is detected while loading the plugin, its name will be automatically be available in the drop down menu when defining the alert.

Fig. Defining an alert action using a plugin

The subroutine alert_action is called, when the trigger of the alert is fired.

    #
    # Alert action function.
    # if defined it will be automatically listed as available plugin, when defining an alert.
    # Called when the trigger of an alert fires.
    # Return value ignored
    sub alert_action {
            my $argref = shift;

            my $alert    = $$argref{'alert'};
            my $timeslot = $$argref{'timeslot'};

            syslog('info', "Alert action function called: alert: $alert, timeslot: $timeslot");
            # Add your code here

            return 1;
    }

The return value of the subroutine is ignored.

### Configuration Parameters for backend plugins

Plugins may require some configuration parameters, which can be defined in nfsen.conf. For each plugin an entry in %PluginConf specifies the parameters.

    %PluginConf = (
            # For plugin demoplugin
            demoplugin => {
                    # scalar
                    param1 = 42,
                    # hash
                    param2 = { 'key' => 'value' },
            },
            # for plugin otherplugin
            otherplugin => [
                    # array
                    'mary had a little lamb'
            ],
    );

The plugin May access its parameters as:

    use NfConf;

    my $conf   = $NfConf::Pluginconf{demoplugin};
    my $param1 = $$conf{'param1'};    # $param1 => 42
    my $param2 = $$conf{'param2'};    # $$param2{'key'} => 'value'

## Writing Frontend Plugins

A frontend plugin is defined as a PHP script with the same name as the backend plugin incl. .php extension and stored in the appropriate directory $FRONTEND_PLUGINDIR specified in nfsen.conf. A frontend plugin is optional and must not exist. However, a frontend plugin is recognized only if a corresponding backend plugin exists. At least two functions must exist in the frontend plugin: <pluginame>_ParseInput and <pluginname>_Run.

Fig. Frontend plugins - flow

    <?php

    /*
     * Frontend plugin: demoplugin
     *
     * Required functions: demoplugin_ParseInput and demoplugin_Run
     *
     */

    /*
     * demoplugin_ParseInput is called prior to any output to the web browser
     * and is intended for the plugin to parse possible form data. This
     * function is called only, if this plugin is selected in the plugins tab.
     * If required, this function may set any number of messages as a result
     * of the argument parsing.
     * The return value is ignored.
     */
    function demoplugin_ParseInput( $plugin_id ) {

            SetMessage('error', "Error set by demo plugin!");
            SetMessage('warning', "Warning set by demo plugin!");
            SetMessage('alert', "Alert set by demo plugin!");
            SetMessage('info', "Info set by demo plugin!");

    } // End of demoplugin_ParseInput

    /*
     * This function is called after the header and the navigation bar have
     * been sent to the browser. It's now up to this function what to display.
     * This function is called only, if this plugin is selected in the plugins tab
     * Its return value is ignored.
     */
    function demoplugin_Run( $plugin_id ) {

            // your code here

    } // End of demoplugin_Run

    ?>

### Conventions

To prevent collisions with other plugins or NfSen internal functions, it's recommended that each plugin follows the following conventions:

- Each plugin also gets a unique ID passed as a parameter, for plugin internal purpose.

- Function names in a plugin should start with <pluginname>_

- Do not store any variables directly in the session array $_SESSION. Use a sub array $_SESSION['plugin'][$plugin_id] instead.

- Use the provided parameter checking routine described in the section below, to parse any form input variables.

### Form parsing in NfSen

NfSen provides a form parsing and validation function ParseForm. This function can and should be used to parse any form data provided in the $_POST array of PHP. It's called with the option array as parameter, and returns a list, with the parsed form data and an error variable.

    list ($process_form, $has_errors) = ParseForm($parse_opts);

$parse_opts is an array of form element descriptors, describing the parameters and boundaries of the form elements to parse. Each key in $parse_opts is equal to the name of the form element to be parsed. The key points to the list of parameters.

    Example:

    $parse_opts = array(
            // var
            profile => array( "required"   => 0,
                              "default"    => “defaultname”,
                              "allow_null" => 0,
                              "match"      => "/^[A-Za-z0-9][A-Za-z0-9|\-+_]+$/" ,
                              "validate"   => NULL),
            another => array( ....

    );

Each parameter array contains the following variables:

required | Defines if this field must exists in $_POST. Possible values: 0, 1
allow_null | Defines if the value NULL is allowed. Possible values: 0, 1
default | If var not required or is NULL ( if NULL is allowed ), set var to this default. Possible values: NULL ( default does not apply ) or any desired default value.
match | Check value of var against this match. Match may be either a valid regex or an array or list of items which are allowed. Possible values: NULL ( match does not apply ), "/regex/", array ( "item1", "item2") Examples: "match" => "/[^A-Za-z0-9\-+_]+/" "match" => array("red", "green", "blue"), "match" => range(0, 10), "match" => NULL,
validate | Any additional validation function, if more detailed check required. Possible values: NULL ( no additional validate function ) name of validate function. Example: "validate" => 'function_validate' The validate function is defined as follows: function function_validate (&$var, $opts) &$var is a pointer to the value to be checked. $opts is the parameter array for this value. $var may be changed as appropriate. The function should return 1 on success, or 0 on failure. The special value 2 can also be returned in case of an error, but in that case the default value does not overwrite the content of var.

For each form variable an appropriate array has to be defined. After the form parsing the resulting values are store in the array $process_form. $has_errors is set to 1 in case of an error, otherwise 0.

Example:

HTML form field:

    <input type='text' name='colour' id="colour" value='<?php echo $process_form['colour']; ?>'

            $parse_opts = array(
                            // channel colour
                            "colour" => array( "required"   => 1,
                                               "default"    => '#aabbcc”,
                                               "allow_null" => 0,
                                               "match"      => "/^#[0-9a-f]{6}/i" ,
                                               "validate"   => NULL)
            );

            list ($process_form, $has_errors) = ParseForm($parse_opts);

$proccess_form['colour'] contains now the parsed value.

## Frontend <-> backend plugin communication

Frontend plugins may display any king of data, resulting from backend plugin processing. Even if this data could be displayed by direct access through PHP, this is not the recommended way. Future versions of NfSen may separate NfSen frontend from backend, putting them onto different hosts. This will require a dedicated communication channel between the two type of plugins. This communication channel is provided by NfSen using the subroutines provided in Nfcomm.pm and nfsenutil.php.

Over this communication channel any number of scalar and array values can be exchanged.

Fig. Plugins communication

Backend:

In order to execute a command from the frontend plugin, the command subroutine need to be registered in the backend plugin.

    our %cmd_lookup = (
            'try'   => \&RunProc,
    );

The name of the array must be %cmd_lookup. Each key defines the command name recognized by the command server . The value points to the subroutine being called, when this command is executed. The function implementing the command must follow the calling conventions below:

    sub RunProc {
        my $socket  = shift;
        my $opts    = shift;

    } # End of RunProc

It should accept two parameters: $socket and $opts. $socket is a reference to the communication socket of the current connection, which will be passed to other routines. $opts is a hash and contains scalar and array values passed from the frontend.

Frontend:

The frontend may call the backend command by calling the nfsend_query function.

    $out_list = nfsend_query("Pluginname::try", $opts);

The first argument defines the command to be called, which is composed by the name of the plugin and the command name and the command name itself separated by a double colon. The second argument is a hash of arguments passed to the command in $opts. The names of the scalar or array values in $opts of the frontend correspond to the names in the hash $opts in the backend hash. In the event of an error, or if the backend sends an error as a result of the query, $out_list is FALSE.

Sending back data to the frontend follows the same rules:: The backend makes use of two predefined functions Nfcomm::socket_send_error and Nfcomm::socket_send_ok to send the response.

    Nfcomm::socket_send_ok($socket, \%args);

The first argument is the socket variable passed to the RunProc. The second parameter is a reference to a hash, containing all scalar and array variables passing back to the frontend. Alternatively the backend can signal a failure, by calling Nfcomm::socket_send_error.

    Nfcomm::socket_send_error($socket, "Missing value");

The function takes two parameters. The first one is the socket as in Nfcomm::socket_send_ok, the second one is the error message the backend send to the frontend.

Example:

Frontend: | Backend:
    // prepare arguments
    $opts = array();

    // two scalar values
    $opts['colour1'] = '#72e3fa';
    $opts['colour2'] = '#2a6f99';

    // one array
    $opts['colours'] = array ( '#12af7d', '#56fc7b');

    // call command in backened plugin
    $out_list = nfsend_query("Pluginname::try", $opts);

    // get result
    // if $out_list == FALSE – it's an error
    if ( !is_array($out_list) ) {
        SetMessage('error', "Error calling plugin");
        return FALSE;
    }
    $string       = $out_list['string'];
    $othercolours = $out_list['othercolours'];

    print "Backend reported: <b>$string</b><br>\n";
    ...
    our %cmd_lookup = (
        'try'   => \&RunProc,
    );

    sub RunProc {
        my $socket  = shift;    # scalar
        my $opts    = shift;    # reference to a hash

        # error checking example
        if ( !exists $$opts{'colours'} ) {
            Nfcomm::socket_send_error($socket, "Missing value");
            return;
        }

        # retrieve values passed by frontend
        # two scalars
        my $colour1 = $$opts{'colour1'};
        my $colour2 = $$opts{'colour2'};

        # one array as arrayref
        my $colours = $$opts{'colours'};

        my @othercolours = ( 'red', 'blue' );

        # Prepare answer
        my %args;
        $args{'string'}       = "Greetings from backend plugin."
        $args{'othercolours'} = \@othercolours;

        Nfcomm::socket_send_ok($socket, \%args);

    } # End of RunProc

### Dynamic generated pictures.

The backend plugin may create any dynamic pictures, depending on the data processing. These dynamically created pictures should also be sent to the frontend plugin using the standard communication socket. In order to do so, the frontend plugin has to call pic.php which does all necessary steps:

    <IMG src='pic.php?picture=smily.jpg' border='0' alt='Smily'>

The call for pic.php requires the parameter picture=<path/to/picture>. For security reason the path is relative to the backend plugin directory $BACKEND_PLUGINDIR. This behaviour can be changed by setting the variable $PICDIR in nfsen.conf if required for some reason.

For a complete summary of the code shown in the example, see the demoplugin, coming with NfSen.