OPNsense Plugin: os-rmm-agent mit WebGUI-Settings, rc.d Service, configd-Integration, Build-Script

This commit is contained in:
cynfo3000 2026-02-28 13:53:24 +01:00
parent d8bc99873f
commit a7d3971b5d
18 changed files with 541 additions and 2 deletions

2
.gitignore vendored
View File

@ -7,3 +7,5 @@ rmm-backend
config.yaml
beispielbilder/
certs/
opnsense-plugin/stage/
opnsense-plugin/*.pkg

View File

@ -1,4 +1,4 @@
.PHONY: all backend agent clean certs deploy-backend deploy-agent
.PHONY: all backend agent plugin clean certs deploy-backend deploy-agent
BACKEND_BIN = build/rmm-backend
AGENT_BIN = build/rmm-agent
@ -19,8 +19,13 @@ agent:
cd agent && GOOS=freebsd GOARCH=amd64 CGO_ENABLED=0 go build -o ../$(AGENT_BIN) .
@echo "==> $(AGENT_BIN) erstellt"
plugin:
@echo "==> Building OPNsense Plugin Package..."
cd opnsense-plugin && ./build.sh $(VERSION)
@echo "==> Plugin Package erstellt in opnsense-plugin/"
clean:
rm -rf build/
rm -rf build/ opnsense-plugin/stage/
certs:
@echo "==> Generiere self-signed TLS-Zertifikate..."

110
opnsense-plugin/build.sh Executable file
View File

@ -0,0 +1,110 @@
#!/bin/sh
#
# Build OPNsense RMM Agent Plugin Package
#
# Usage: ./build.sh [version]
# Example: ./build.sh 1.0.0
#
# Prerequisites:
# - Go 1.24+ (for cross-compilation)
# - FreeBSD pkg tools (runs on the target OPNsense, or use pkg-create on FreeBSD)
#
# This script:
# 1. Cross-compiles the agent binary (freebsd/amd64)
# 2. Assembles the staging directory
# 3. Creates a +MANIFEST for pkg
# 4. Outputs a .tar that can be installed with `pkg add`
set -e
VERSION="${1:-1.0.0}"
NAME="os-rmm-agent"
STAGEDIR="$(pwd)/stage"
BUILDDIR="$(pwd)/../build"
PKGFILE="$(pwd)/${NAME}-${VERSION}.pkg"
echo "==> Building RMM Agent Plugin v${VERSION}"
# 1. Build agent binary
echo "==> Compiling agent (freebsd/amd64)..."
cd ../agent
GOOS=freebsd GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o "${BUILDDIR}/rmm-agent" .
cd ../opnsense-plugin
echo " Binary: ${BUILDDIR}/rmm-agent ($(ls -lh ${BUILDDIR}/rmm-agent | awk '{print $5}'))"
# 2. Assemble staging directory
echo "==> Assembling staging directory..."
rm -rf "${STAGEDIR}"
mkdir -p "${STAGEDIR}/usr/local"
# Plugin files
cp -R src/etc "${STAGEDIR}/usr/local/"
cp -R src/opnsense "${STAGEDIR}/usr/local/"
cp -R src/usr/local/etc "${STAGEDIR}/usr/local/"
mkdir -p "${STAGEDIR}/usr/local/rmm"
cp "${BUILDDIR}/rmm-agent" "${STAGEDIR}/usr/local/rmm/rmm-agent"
# Set permissions
chmod +x "${STAGEDIR}/usr/local/rmm/rmm-agent"
chmod +x "${STAGEDIR}/usr/local/etc/rc.d/rmm_agent"
chmod +x "${STAGEDIR}/usr/local/opnsense/scripts/rmmagent/setup.sh"
chmod +x "${STAGEDIR}/usr/local/opnsense/scripts/OPNsense/RMMAgent/generate_config.php"
# 3. Create +MANIFEST
echo "==> Creating package manifest..."
cat > "${STAGEDIR}/+MANIFEST" << EOF
name: ${NAME}
version: ${VERSION}
origin: sysutils/${NAME}
comment: RMM Agent for OPNsense
maintainer: cm@cynfo.com
www: https://git.cynfo.net/christian/rmm
prefix: /usr/local
desc: <<EOD
RMM Agent for OPNsense - Remote Monitoring & Management
Collects system data and reports to central RMM backend.
Configurable via OPNsense WebGUI under Services > RMM Agent.
EOD
categories: [sysutils]
deps:
opnsense: {origin: opnsense/opnsense, version: "24.7"}
scripts:
post-install: |
/usr/local/etc/rc.d/opnsense-configd restart
echo "RMM Agent Plugin installed."
echo "Configure via: Services > RMM Agent"
pre-deinstall: |
/usr/local/etc/rc.d/rmm_agent stop 2>/dev/null || true
/usr/sbin/sysrc -x rmm_agent_enable 2>/dev/null || true
post-deinstall: |
/usr/local/etc/rc.d/opnsense-configd restart
echo "RMM Agent Plugin removed."
EOF
# 4. Create package (tar format compatible with `pkg add`)
echo "==> Creating package..."
cd "${STAGEDIR}"
# Generate file list
find usr -type f | sort > /tmp/rmm-pkg-files.txt
# Create the pkg using tar (FreeBSD pkg format)
tar -czf "${PKGFILE}" +MANIFEST -C "${STAGEDIR}" $(cat /tmp/rmm-pkg-files.txt)
cd ..
rm -rf "${STAGEDIR}"
rm -f /tmp/rmm-pkg-files.txt
echo ""
echo "==> Package erstellt: ${PKGFILE}"
echo " Version: ${VERSION}"
echo " Groesse: $(ls -lh ${PKGFILE} | awk '{print $5}')"
echo ""
echo "==> Installation auf OPNsense:"
echo " scp ${PKGFILE} root@opnsense:/tmp/"
echo " ssh root@opnsense 'pkg add /tmp/${NAME}-${VERSION}.pkg'"
echo ""
echo "==> Danach:"
echo " - WebGUI oeffnen → Services → RMM Agent"
echo " - Backend URL und API Key eintragen"
echo " - Aktiviert ankreuzen → Speichern"

16
opnsense-plugin/pkg-descr Normal file
View File

@ -0,0 +1,16 @@
RMM Agent for OPNsense
Remote Monitoring & Management Agent for OPNsense firewalls.
Collects system data (CPU, memory, disks, network, services, WireGuard,
DHCP, routing, gateways, certificates, plugins, updates) and reports
to a central RMM backend via HTTPS/WebSocket.
Features:
- Automatic system data collection (configurable interval)
- WebSocket-based remote command execution
- Session-based TCP tunnels (SSH, WebGUI access)
- Config backup and restore
- WireGuard peer management
- OPNsense update management (normal + major upgrades)
WWW: https://git.cynfo.net/christian/rmm

14
opnsense-plugin/pkg-plist Normal file
View File

@ -0,0 +1,14 @@
etc/inc/plugins.inc.d/rmmagent.inc
opnsense/mvc/app/controllers/OPNsense/RMMAgent/Api/ServiceController.php
opnsense/mvc/app/controllers/OPNsense/RMMAgent/Api/SettingsController.php
opnsense/mvc/app/controllers/OPNsense/RMMAgent/GeneralController.php
opnsense/mvc/app/controllers/OPNsense/RMMAgent/forms/general.xml
opnsense/mvc/app/models/OPNsense/RMMAgent/Menu/Menu.xml
opnsense/mvc/app/models/OPNsense/RMMAgent/RMMAgent.php
opnsense/mvc/app/models/OPNsense/RMMAgent/RMMAgent.xml
opnsense/mvc/app/views/OPNsense/RMMAgent/general.volt
opnsense/scripts/OPNsense/RMMAgent/generate_config.php
opnsense/scripts/rmmagent/setup.sh
opnsense/service/conf/actions.d/actions_rmmagent.conf
usr/local/etc/rc.d/rmm_agent
usr/local/rmm/rmm-agent

View File

@ -0,0 +1,40 @@
<?php
/**
* RMM Agent plugin OPNsense integration hooks
*/
use OPNsense\RMMAgent\RMMAgent;
function rmmagent_services()
{
$services = array();
$mdl = new RMMAgent();
if ((string)$mdl->general->Enabled == '1') {
$services[] = array(
'description' => gettext('RMM Agent'),
'configd' => array(
'restart' => array('rmmagent restart'),
'start' => array('rmmagent start'),
'stop' => array('rmmagent stop'),
),
'name' => 'rmm_agent',
'pidfile' => '/var/run/rmm_agent.pid',
);
}
return $services;
}
function rmmagent_xmlrpc_sync()
{
$result = array();
$result[] = array(
'description' => gettext('RMM Agent'),
'section' => 'OPNsense.rmmagent',
'id' => 'rmmagent',
'services' => array('rmm_agent'),
);
return $result;
}

View File

@ -0,0 +1,13 @@
<?php
namespace OPNsense\RMMAgent\Api;
use OPNsense\Base\ApiMutableServiceControllerBase;
class ServiceController extends ApiMutableServiceControllerBase
{
protected static $internalServiceClass = '\OPNsense\RMMAgent\RMMAgent';
protected static $internalServiceTemplate = 'OPNsense/RMMAgent/config.yaml';
protected static $internalServiceEnabled = 'general.Enabled';
protected static $internalServiceName = 'rmmagent';
}

View File

@ -0,0 +1,21 @@
<?php
namespace OPNsense\RMMAgent\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
class SettingsController extends ApiMutableModelControllerBase
{
protected static $internalModelName = 'rmmagent';
protected static $internalModelClass = 'OPNsense\RMMAgent\RMMAgent';
public function getAction()
{
return $this->getBase('general', 'general');
}
public function setAction()
{
return $this->setBase('general', 'general');
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace OPNsense\RMMAgent;
use OPNsense\Base\IndexController;
class GeneralController extends IndexController
{
public function indexAction()
{
$this->view->generalForm = $this->getForm('general');
$this->view->pick('OPNsense/RMMAgent/general');
}
}

View File

@ -0,0 +1,38 @@
<form>
<field>
<id>rmmagent.general.Enabled</id>
<label>Aktiviert</label>
<type>checkbox</type>
<help>RMM Agent Dienst aktivieren/deaktivieren</help>
</field>
<field>
<id>rmmagent.general.BackendUrl</id>
<label>Backend URL</label>
<type>text</type>
<help>URL des RMM Backends (z.B. https://192.168.85.13:8443)</help>
</field>
<field>
<id>rmmagent.general.ApiKey</id>
<label>API Key</label>
<type>password</type>
<help>API-Schluessel fuer die Authentifizierung am Backend</help>
</field>
<field>
<id>rmmagent.general.AgentName</id>
<label>Agent Name</label>
<type>text</type>
<help>Optionaler Name (Standard: Hostname der Firewall)</help>
</field>
<field>
<id>rmmagent.general.Interval</id>
<label>Heartbeat Intervall (Sek.)</label>
<type>text</type>
<help>Intervall fuer Heartbeat/Datensammlung in Sekunden (Standard: 60)</help>
</field>
<field>
<id>rmmagent.general.Insecure</id>
<label>Self-Signed Zertifikate</label>
<type>checkbox</type>
<help>TLS-Zertifikats-Pruefung deaktivieren (fuer self-signed Backend-Zertifikate)</help>
</field>
</form>

View File

@ -0,0 +1,6 @@
<menu>
<Services>
<RMMAgent VisibleName="RMM Agent" cssClass="fa fa-shield" url="/ui/rmmagent/general" order="99">
</RMMAgent>
</Services>
</menu>

View File

@ -0,0 +1,9 @@
<?php
namespace OPNsense\RMMAgent;
use OPNsense\Base\BaseModel;
class RMMAgent extends BaseModel
{
}

View File

@ -0,0 +1,37 @@
<model>
<mount>//OPNsense/rmmagent</mount>
<version>1.0.0</version>
<description>RMM Agent Settings</description>
<items>
<general>
<Enabled type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</Enabled>
<BackendUrl type="TextField">
<Required>Y</Required>
<Mask>/^https?:\/\/.+/</Mask>
<ValidationMessage>Gueltige URL erforderlich (https://host:port)</ValidationMessage>
</BackendUrl>
<ApiKey type="TextField">
<Required>Y</Required>
<Mask>/^.{8,}$/</Mask>
<ValidationMessage>API-Key muss mindestens 8 Zeichen lang sein</ValidationMessage>
</ApiKey>
<AgentName type="TextField">
<Required>N</Required>
<ValidationMessage>Optionaler Agent-Name</ValidationMessage>
</AgentName>
<Interval type="IntegerField">
<Default>60</Default>
<MinimumValue>10</MinimumValue>
<MaximumValue>3600</MaximumValue>
<ValidationMessage>Intervall zwischen 10 und 3600 Sekunden</ValidationMessage>
</Interval>
<Insecure type="BooleanField">
<Default>1</Default>
<Required>Y</Required>
</Insecure>
</general>
</items>
</model>

View File

@ -0,0 +1,51 @@
{#
# Copyright (c) 2026 cynfo.com
# RMM Agent Settings Page
#}
<script>
$(document).ready(function () {
var data_get_map = {'frm_GeneralSettings': '/api/rmmagent/settings/get'};
mapDataToFormUI(data_get_map).done(function () {
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
updateServiceControlUI('rmmagent');
});
$('#saveAct').unbind('click').click(function () {
saveFormToEndpoint('/api/rmmagent/settings/set', 'frm_GeneralSettings', function () {
$('#saveAct_progress').addClass('fa fa-spinner fa-pulse');
ajaxCall('/api/rmmagent/service/reconfigure', {}, function (data, status) {
updateServiceControlUI('rmmagent');
$('#saveAct_progress').removeClass('fa fa-spinner fa-pulse');
});
});
});
});
</script>
<div class="content-box" style="padding: 10px;">
<div class="content-box-header">
<h3>{{ lang._('RMM Agent Einstellungen') }}</h3>
</div>
<div class="content-box-main">
<div class="table-responsive">
{{ partial("layout_partials/base_form", ['fields': generalForm, 'id': 'frm_GeneralSettings']) }}
</div>
</div>
<div class="col-md-12">
<hr/>
<button class="btn btn-primary" id="saveAct" type="button">
<b>{{ lang._('Speichern') }}</b> <i id="saveAct_progress"></i>
</button>
</div>
</div>
<div class="content-box" style="padding: 10px; margin-top: 20px;">
<div class="content-box-header">
<h3>{{ lang._('Dienst-Status') }}</h3>
</div>
<div class="content-box-main">
{{ partial("layout_partials/service_control", ['serviceName': 'rmmagent']) }}
</div>
</div>

View File

@ -0,0 +1,31 @@
#!/usr/local/bin/php
<?php
require_once('config.inc');
require_once('util.inc');
use OPNsense\RMMAgent\RMMAgent;
$mdl = new RMMAgent();
$backendUrl = (string)$mdl->general->BackendUrl;
$apiKey = (string)$mdl->general->ApiKey;
$interval = (string)$mdl->general->Interval;
$agentName = (string)$mdl->general->AgentName;
$insecure = (string)$mdl->general->Insecure;
if (empty($interval)) {
$interval = "60";
}
echo "# RMM Agent Configuration — auto-generated\n";
echo "# Do not edit — changes via OPNsense WebGUI\n\n";
echo "backend_url: \"{$backendUrl}\"\n";
echo "api_key: \"{$apiKey}\"\n";
echo "interval_seconds: {$interval}\n";
if (!empty($agentName)) {
echo "agent_name: \"{$agentName}\"\n";
}
// insecure is handled via command-line flag, not config

View File

@ -0,0 +1,29 @@
#!/bin/sh
# RMM Agent — generate config and manage service
CONFIGDIR="/usr/local/rmm"
CONFIGFILE="${CONFIGDIR}/config.yaml"
# Generate config from model
mkdir -p "${CONFIGDIR}"
/usr/local/bin/php /usr/local/opnsense/scripts/OPNsense/RMMAgent/generate_config.php > "${CONFIGFILE}"
# Check if enabled
ENABLED=$(/usr/local/sbin/pluginctl -g OPNsense.rmmagent.general.Enabled 2>/dev/null)
# Check insecure flag
INSECURE=$(/usr/local/sbin/pluginctl -g OPNsense.rmmagent.general.Insecure 2>/dev/null)
if [ "${INSECURE}" = "1" ]; then
/usr/sbin/sysrc rmm_agent_insecure="YES" > /dev/null 2>&1
else
/usr/sbin/sysrc rmm_agent_insecure="NO" > /dev/null 2>&1
fi
if [ "${ENABLED}" = "1" ]; then
/usr/sbin/sysrc rmm_agent_enable="YES" > /dev/null 2>&1
/usr/local/etc/rc.d/rmm_agent restart
echo "RMM Agent started"
else
/usr/local/etc/rc.d/rmm_agent stop 2>/dev/null
/usr/sbin/sysrc rmm_agent_enable="NO" > /dev/null 2>&1
echo "RMM Agent stopped"
fi

View File

@ -0,0 +1,29 @@
[start]
command:/usr/local/etc/rc.d/rmm_agent start
parameters:
type:script
message:Starting RMM Agent
[stop]
command:/usr/local/etc/rc.d/rmm_agent stop
parameters:
type:script
message:Stopping RMM Agent
[restart]
command:/usr/local/etc/rc.d/rmm_agent restart
parameters:
type:script
message:Restarting RMM Agent
[status]
command:/usr/local/etc/rc.d/rmm_agent status; exit 0
parameters:
type:script_output
message:Request RMM Agent status
[reconfigure]
command:/usr/local/opnsense/scripts/rmmagent/setup.sh
parameters:
type:script
message:Reconfiguring RMM Agent

View File

@ -0,0 +1,74 @@
#!/bin/sh
#
# PROVIDE: rmm_agent
# REQUIRE: NETWORKING
# KEYWORD: shutdown
#
# Add the following to /etc/rc.conf to enable:
# rmm_agent_enable="YES"
. /etc/rc.subr
name="rmm_agent"
rcvar="rmm_agent_enable"
load_rc_config ${name}
: ${rmm_agent_enable:="NO"}
: ${rmm_agent_config:="/usr/local/rmm/config.yaml"}
: ${rmm_agent_logfile:="/var/log/rmm-agent.log"}
: ${rmm_agent_insecure:="YES"}
pidfile="/var/run/${name}.pid"
command="/usr/local/rmm/rmm-agent"
start_cmd="${name}_start"
stop_cmd="${name}_stop"
status_cmd="${name}_status"
rmm_agent_start()
{
if [ ! -f "${command}" ]; then
echo "rmm-agent binary not found: ${command}"
return 1
fi
if [ ! -f "${rmm_agent_config}" ]; then
echo "Config not found: ${rmm_agent_config}"
return 1
fi
echo "Starting ${name}."
INSECURE_FLAG=""
if checkyesno rmm_agent_insecure; then
INSECURE_FLAG="--insecure"
fi
/usr/sbin/daemon -f -p ${pidfile} -o ${rmm_agent_logfile} \
${command} --config ${rmm_agent_config} ${INSECURE_FLAG}
}
rmm_agent_stop()
{
if [ -f "${pidfile}" ]; then
echo "Stopping ${name}."
kill $(cat ${pidfile}) 2>/dev/null
rm -f ${pidfile}
else
echo "${name} is not running."
fi
}
rmm_agent_status()
{
if [ -f "${pidfile}" ] && kill -0 $(cat ${pidfile}) 2>/dev/null; then
PID=$(cat ${pidfile})
echo "${name} is running as pid ${PID}."
else
echo "${name} is not running."
return 1
fi
}
run_rc_command "$1"