Developing Custom Packages for pfSense

Packages extend pfSense functionality without modifying the base system. Each package is a FreeBSD port with a pfSense-specific structure: an XML manifest defines metadata, XML configuration files describe the interface, and PHP files implement the logic. The Package Manager installs packages from the official repository, but developers can build and install custom packages manually.

Package Port Structure

pfSense packages follow the naming convention pfSense-pkg-<Name> and reside in FreeBSD port category directories such as sysutils/, net/, or security/. Development takes place in the devel branch of the pfSense FreeBSD-ports repository.

A typical port directory layout:

sysutils/pfSense-pkg-MyPackage/
├── Makefile                           # Version, dependencies, build rules
├── pkg-descr                          # Plain-text package description
├── pkg-plist                          # List of installed files
└── files/
    ├── pkg-install.in                 # Installation script
    ├── pkg-deinstall.in               # Removal script
    └── usr/local/
        ├── bin/                       # Executable scripts
        ├── pkg/
        │   ├── mypackage.xml          # GUI configuration
        │   └── mypackage.inc          # PHP include with logic
        ├── share/pfSense-pkg-MyPackage/
        │   └── info.xml               # Package manifest
        └── www/packages/mypackage/
            └── mypackage.php          # Web interface pages

Makefile

The Makefile defines the package version, dependencies, and build procedure. The version is set via PORTVERSION, and binary dependencies from FreeBSD are declared in RUN_DEPENDS:

PORTNAME=       pfSense-pkg-MyPackage
PORTVERSION=    0.1.0
CATEGORIES=     sysutils
MASTER_SITES=   # empty for local packages

MAINTAINER=     developer@example.com
COMMENT=        My custom pfSense package

RUN_DEPENDS+=   bash:shells/bash

NO_BUILD=       yes
NO_MTREE=       yes

.include <bsd.port.mk>

Incrementing PORTVERSION is mandatory with every update. Without a version change, the system will not detect the new release.

pkg-plist

The pkg-plist file enumerates all files installed by the package, using paths relative to /usr/local/:

share/pfSense-pkg-MyPackage/info.xml
pkg/mypackage.xml
pkg/mypackage.inc
www/packages/mypackage/mypackage.php

Every time a file is added to the package, pkg-plist must be updated accordingly. Missing entries mean missing files in the final build.

Installation and Removal Scripts

The pkg-install.in and pkg-deinstall.in files are identical across all pfSense packages and handle integration with the Package Manager. Copy them from any existing package without modification.

XML Manifest (info.xml)

The manifest file resides at files/usr/local/share/pfSense-pkg-<Name>/info.xml and holds package metadata:

<?xml version="1.0" encoding="utf-8" ?>
<pfsensepkgs>
  <package>
    <name>mypackage</name>
    <descr>Description of my package</descr>
    <pkginfolink>https://example.com/docs</pkginfolink>
    <version>%%PKGVERSION%%</version>
    <configurationfile>
      https://packages.pfsense.org/packages/config/mypackage/mypackage.xml
    </configurationfile>
  </package>
</pfsensepkgs>

The %%PKGVERSION%% variable is substituted automatically from the Makefile PORTVERSION at build time.

GUI Configuration (Package XML File)

The package XML file (e.g., mypackage.xml) defines how the package integrates with the pfSense web interface: menu entries, settings pages, form fields, and lifecycle hooks.

Menu Integration

The <menu> element registers the package in the pfSense navigation:

<packagegui>
  <menu>
    <name>MyPackage</name>
    <section>Services</section>
    <url>/packages/mypackage/mypackage.php</url>
  </menu>
</packagegui>

After installation, the package will appear under the Services section in the main menu.

Defining Settings Fields

The <fields> section describes form elements on the package settings page:

<fields>
  <field>
    <fielddescr>Enable Service</fielddescr>
    <fieldname>enable</fieldname>
    <type>checkbox</type>
    <description>Enable or disable the service</description>
  </field>
  <field>
    <fielddescr>Listen Port</fielddescr>
    <fieldname>port</fieldname>
    <type>input</type>
    <size>5</size>
    <description>Port number for the service (default: 8080)</description>
  </field>
  <field>
    <fielddescr>Network Interface</fielddescr>
    <fieldname>interface</fieldname>
    <type>interfaces_selection</type>
    <description>Select the interface to bind</description>
  </field>
</fields>

Supported field types include input, password, textarea, checkbox, select, interfaces_selection, info, and button.

Use <combinefields> to group multiple fields into a single row. Use <rowhelper> for table-style multi-entry input. Use <adddeleteeditpagefields> for list-based management with add, edit, and delete operations.

Lifecycle Hooks

PHP functions that execute at specific stages of the package lifecycle:

HookPurpose
custom_php_install_commandActions performed after package installation
custom_php_deinstall_commandCleanup when the package is removed
custom_php_resync_config_commandConfiguration synchronization (triggered on save)
custom_php_validation_commandInput validation before saving

Hooks are specified in XML as names of PHP functions defined in the package .inc file.

PHP Files

Include File (.inc)

The mypackage.inc file in files/usr/local/pkg/ contains the core package logic: validation functions, configuration application, and service configuration file generation.

<?php
require_once("config.inc");
require_once("util.inc");
require_once("service-utils.inc");

function mypackage_validate_input($post, &$input_errors) {
    if (!is_port($post['port'])) {
        $input_errors[] = "Invalid port number.";
    }
}

function mypackage_resync_config() {
    global $config;
    $settings = $config['installedpackages']['mypackage']['config'][0];

    if ($settings['enable'] != "on") {
        mypackage_stop_service();
        return;
    }

    // Generate configuration file
    $conf = "listen_port={$settings['port']}\n";
    file_put_contents("/usr/local/etc/mypackage.conf", $conf);

    mypackage_restart_service();
}

function mypackage_restart_service() {
    mwexec("/usr/local/bin/mypackage restart");
}

function mypackage_stop_service() {
    mwexec("/usr/local/bin/mypackage stop");
}

Web Pages (.php)

PHP files in files/usr/local/www/packages/mypackage/ render the package pages in the web interface. Most packages use a standard template that automatically generates the settings page from the XML configuration.

Package Dependencies

Dependencies on FreeBSD binary packages are declared in the Makefile via RUN_DEPENDS. The package XML configuration does not manage dependencies - that responsibility belongs to the ports system.

RUN_DEPENDS+=   python3:lang/python3 \
                curl:ftp/curl

pfSense maintains its own package repository based on FreeBSD pkg. Not all packages from the standard FreeBSD repository are available - verify that required dependencies exist in the pfSense repository before relying on them.

Compiling Software for pfSense

pfSense intentionally excludes compilation tools (make, header files) for security reasons. To compile custom software for pfSense:

  1. Set up a virtual machine running the FreeBSD version that matches your pfSense release
  2. Compile the software in that environment
  3. Transfer the resulting binaries or packages to the firewall

Alternatively, use pre-compiled FreeBSD packages via pkg install when version compatibility allows.

Building and Testing

Building the Package

Build the package from the port directory using standard FreeBSD tooling:

cd /usr/ports/sysutils/pfSense-pkg-MyPackage
make package

This produces a .pkg (or .txz) archive ready for installation.

Installing on a Test Instance

Install the built package on a test pfSense instance:

pkg add pfSense-pkg-MyPackage-0.1.0.pkg

After installation, verify the following:

  • The menu entry appears in the correct section
  • Settings save and load correctly
  • Lifecycle hooks execute as expected
  • The package uninstalls and reinstalls cleanly

Debugging

When troubleshooting package errors, monitor the system log:

tail -f /var/log/system.log

PHP errors from packages are also displayed under Status > System Logs in the System > General tab.

Submitting to the Official Repository

To include a package in the official pfSense repository:

  1. Ensure the package conforms to the Developer Style Guide
  2. Create an entry in pfSense Redmine describing the package
  3. Submit a pull request to the pfSense FreeBSD-ports repository targeting the devel branch
  4. Await review from Netgate developers

The pull request must reference the Redmine entry, describe the package functionality, and include test results.

Common Pitfalls

ProblemCauseSolution
Package does not updatePORTVERSION not incremented in MakefileBump the version number with every change
Files missing after installpkg-plist not updatedAdd all new files to pkg-plist
PHP errors after pfSense upgradeIncompatibility with PHP 8.xReplace deprecated functions, use elseif instead of else if
Works on 2.7 but fails on 2.8Underlying FreeBSD version changeVerify compatibility with the target FreeBSD release
Spaces instead of tabsCoding style violationUse tabs with 8-stop width for indentation
XSS vulnerabilitiesOutputting user input without encodingUse htmlspecialchars() for all rendered data

Related Sections

Last updated on