# Developing Custom Packages for pfSense

> pfSense package structure, XML manifest, PHP files, menu integration, settings pages, FreeBSD ports, package testing, and submission process

Source: https://opennix.org/en/docs/pfsense/development/pfsense-package-development/


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](https://github.com/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`:

```makefile
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
<?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:

```xml
<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:

```xml
<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
<?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.

```makefile
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:

```bash
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:

```bash
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:

```bash
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](https://docs.netgate.com/pfsense/en/latest/references/developer-style-guide.html)
2. Create an entry in [pfSense Redmine](https://redmine.pfsense.org) describing the package
3. Submit a pull request to the [pfSense FreeBSD-ports](https://github.com/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

| 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](/docs/pfsense/development/pfsense-custom-scripts/) - automation scripts, Shellcmd, Cron, and PHP Shell
- [API and Automation](/docs/pfsense/development/pfsense-api-automation/) - programmatic pfSense management via REST API
- [pfSense Packages](/docs/pfsense/packages/) - installing and managing packages through Package Manager

