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 pagesMakefile
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.phpEvery 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:
| Hook | Purpose |
|---|---|
custom_php_install_command | Actions performed after package installation |
custom_php_deinstall_command | Cleanup when the package is removed |
custom_php_resync_config_command | Configuration synchronization (triggered on save) |
custom_php_validation_command | Input 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:
- Set up a virtual machine running the FreeBSD version that matches your pfSense release
- Compile the software in that environment
- 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 packageThis 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.pkgAfter 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.logPHP 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:
- Ensure the package conforms to the Developer Style Guide
- Create an entry in pfSense Redmine describing the package
- Submit a pull request to the pfSense FreeBSD-ports
repository targeting the
develbranch - Await review from Netgate developers
The pull request must reference the Redmine entry, describe the package functionality, and include test results.
Common Pitfalls
| Problem | Cause | Solution |
|---|---|---|
| Package does not update | PORTVERSION not incremented in Makefile | Bump the version number with every change |
| Files missing after install | pkg-plist not updated | Add all new files to pkg-plist |
| PHP errors after pfSense upgrade | Incompatibility with PHP 8.x | Replace deprecated functions, use elseif instead of else if |
| Works on 2.7 but fails on 2.8 | Underlying FreeBSD version change | Verify compatibility with the target FreeBSD release |
| Spaces instead of tabs | Coding style violation | Use tabs with 8-stop width for indentation |
| XSS vulnerabilities | Outputting user input without encoding | Use htmlspecialchars() for all rendered data |
Related Sections
- Custom Scripts - automation scripts, Shellcmd, Cron, and PHP Shell
- API and Automation - programmatic pfSense management via REST API
- pfSense Packages - installing and managing packages through Package Manager