This commit is contained in:
Richard Aspden 2026-05-23 06:52:43 +01:00 committed by GitHub
commit 4be7f89c69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 729 additions and 0 deletions

View file

@ -49,6 +49,7 @@ misc/theme-rebellion -- A suitably dark theme
misc/theme-tukan -- The tukan theme - blue/white
misc/theme-vicuna -- The vicuna theme - blue sapphire
net/chrony -- Chrony time synchronisation
net/cloudflared -- Cloudflare Tunnel Service
net/freeradius -- RADIUS Authentication, Authorization and Accounting Server
net/frr -- The FRRouting Protocol Suite
net/ftp-proxy -- Control ftp-proxy processes

8
net/cloudflared/Makefile Normal file
View file

@ -0,0 +1,8 @@
PLUGIN_NAME= cloudflared
PLUGIN_VERSION= 0.1.0
PLUGIN_REVISION= 1
PLUGIN_COMMENT= Cloudflare Tunnel integration
PLUGIN_MAINTAINER= rick+github@insanityinside.net
PLUGIN_DEPENDS= cloudflared
.include "../../Mk/plugins.mk"

View file

@ -0,0 +1,7 @@
Integrates the Cloudflare Tunnel daemon (cloudflared) into OPNsense,
allowing self-hosted services to be exposed through Cloudflare Zero Trust
without requiring inbound firewall rules or a public IP address.
WWW: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/
Inspired by original work by Alan Martines <alancpmartines@hotmail.com>.

View file

@ -0,0 +1,89 @@
<?php
/*
* Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
function cloudflared_enabled()
{
return (string)(new \OPNsense\Cloudflared\Cloudflared())->general->enabled == '1';
}
function cloudflared_configure()
{
return [
'newwanip' => ['cloudflared_configure_newwanip'],
];
}
function cloudflared_configure_newwanip($verbose = false)
{
if (!cloudflared_enabled()) {
return;
}
if (file_exists('/var/run/cloudflared.pid')) {
return;
}
(new \OPNsense\Core\Backend())->configdRun('cloudflared start');
}
function cloudflared_services()
{
global $config;
$services = [];
if (
isset($config['OPNsense']['Cloudflared']['general']['enabled']) &&
$config['OPNsense']['Cloudflared']['general']['enabled'] == '1'
) {
$services[] = [
'description' => gettext('Cloudflare Tunnel'),
'configd' => [
'start' => ['cloudflared start'],
'restart' => ['cloudflared restart'],
'stop' => ['cloudflared stop'],
],
'name' => 'cloudflared',
'pidfile' => '/var/run/cloudflared.pid',
];
}
return $services;
}
function cloudflared_xmlrpc_sync()
{
$result = [];
$result[] = [
'description' => gettext('Cloudflare Tunnel'),
'section' => 'OPNsense.Cloudflared',
'id' => 'cloudflared',
'services' => ['cloudflared'],
];
return $result;
}

View file

@ -0,0 +1,49 @@
<?php
/*
* Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Cloudflared\Api;
use OPNsense\Base\ApiMutableServiceControllerBase;
use OPNsense\Core\Backend;
class ServiceController extends ApiMutableServiceControllerBase
{
protected static $internalServiceClass = '\OPNsense\Cloudflared\Cloudflared';
protected static $internalServiceTemplate = 'OPNsense/Cloudflared';
protected static $internalServiceEnabled = 'general.enabled';
protected static $internalServiceName = 'cloudflared';
public function reconfigureAction()
{
if ($this->request->isPost()) {
(new Backend())->configdRun('cloudflared reconfigure');
return ['status' => 'ok'];
}
return ['status' => 'failed'];
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Cloudflared\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
class SettingsController extends ApiMutableModelControllerBase
{
protected static $internalModelClass = '\OPNsense\Cloudflared\Cloudflared';
protected static $internalModelName = 'cloudflared';
}

View file

@ -0,0 +1,38 @@
<?php
/*
* Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Cloudflared;
class IndexController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->general = $this->getForm("general");
$this->view->pick('OPNsense/Cloudflared/index');
}
}

View file

@ -0,0 +1,42 @@
<form>
<field>
<type>header</type>
<label>General Settings</label>
</field>
<field>
<id>cloudflared.general.enabled</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the Cloudflare Tunnel service.</help>
</field>
<field>
<id>cloudflared.general.token</id>
<label>Tunnel Token</label>
<type>text</type>
<help>Tunnel token from the Cloudflare Zero Trust dashboard (Networks &gt; Tunnels). The token is stored in a root-readable file and passed to cloudflared via an environment variable.</help>
</field>
<field>
<id>cloudflared.general.protocol</id>
<label>Protocol</label>
<type>dropdown</type>
<help>Transport protocol. Leave on auto unless you have a specific reason to force QUIC or HTTP/2.</help>
</field>
<field>
<id>cloudflared.general.post_quantum</id>
<label>Post-Quantum Encryption</label>
<type>checkbox</type>
<help>Enable post-quantum cryptography for the tunnel connection (requires QUIC).</help>
</field>
<field>
<id>cloudflared.general.quic_disable_pmtu_discovery</id>
<label>Disable QUIC PMTU Discovery</label>
<type>checkbox</type>
<help>Disable Path MTU Discovery for QUIC connections. May help in environments where ICMP is blocked.</help>
</field>
<field>
<id>cloudflared.general.log_level</id>
<label>Log Level</label>
<type>dropdown</type>
<help>Verbosity of cloudflared logging. Defaults to info. Use debug for troubleshooting; warn or error for quieter operation.</help>
</field>
</form>

View file

@ -0,0 +1,56 @@
<?php
/*
* Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\System\Status;
use OPNsense\System\AbstractStatus;
use OPNsense\System\SystemStatusCode;
class CloudflaredOverrideStatus extends AbstractStatus
{
public function __construct()
{
$this->internalPriority = 2;
$this->internalPersistent = true;
$this->internalIsBanner = true;
$this->internalTitle = gettext('Cloudflare Tunnel');
$this->internalScope = [
'/ui/cloudflared/'
];
}
public function collectStatus()
{
$this->internalMessage = gettext(
'Cloudflare Tunnel traffic bypasses OPNsense firewall rules; access control must be enforced in ' .
'Cloudflare Access. For optimal QUIC performance, set the recommended kernel tunables. ' .
'See the plugin documentation for details.'
);
$this->internalStatus = SystemStatusCode::NOTICE;
}
}

View file

@ -0,0 +1,16 @@
<acl>
<page-cloudflared-general>
<name>Services: Cloudflare Tunnel</name>
<patterns>
<pattern>ui/cloudflared/*</pattern>
<pattern>api/cloudflared/*</pattern>
</patterns>
</page-cloudflared-general>
<page-cloudflared-log>
<name>Services: Cloudflare Tunnel: Log File</name>
<patterns>
<pattern>ui/diagnostics/log/core/cloudflared/*</pattern>
<pattern>api/diagnostics/log/core/cloudflared/*</pattern>
</patterns>
</page-cloudflared-log>
</acl>

View file

@ -0,0 +1,35 @@
<?php
/*
* Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Cloudflared;
use OPNsense\Base\BaseModel;
class Cloudflared extends BaseModel
{
}

View file

@ -0,0 +1,46 @@
<model>
<mount>//OPNsense/Cloudflared</mount>
<version>1.0.0</version>
<description>Cloudflare Tunnel settings</description>
<items>
<general>
<enabled type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</enabled>
<token type="TextField">
<Required>Y</Required>
<Mask>/^[a-zA-Z0-9\._\-]+$/</Mask>
<ValidationMessage>Token may only contain letters, digits, dots, underscores and hyphens.</ValidationMessage>
</token>
<protocol type="OptionField">
<Default>auto</Default>
<Required>Y</Required>
<OptionValues>
<auto>auto</auto>
<quic>quic</quic>
<http2>http2</http2>
</OptionValues>
</protocol>
<post_quantum type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</post_quantum>
<quic_disable_pmtu_discovery type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</quic_disable_pmtu_discovery>
<log_level type="OptionField">
<Default>info</Default>
<Required>Y</Required>
<OptionValues>
<info>info</info>
<debug>debug</debug>
<warn>warn</warn>
<error>error</error>
<fatal>fatal</fatal>
</OptionValues>
</log_level>
</general>
</items>
</model>

View file

@ -0,0 +1,8 @@
<menu>
<Services>
<Cloudflared VisibleName="Cloudflare Tunnel" cssClass="fa fa-cloud fa-fw">
<Settings order="10" url="/ui/cloudflared/"/>
<Log VisibleName="Log File" order="20" url="/ui/diagnostics/log/core/cloudflared"/>
</Cloudflared>
</Services>
</menu>

View file

@ -0,0 +1,52 @@
{#
# Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#}
<script>
$(document).ready(function() {
mapDataToFormUI({'frm_GeneralSettings': "/api/cloudflared/settings/get"}).done(function() {
$('.selectpicker').selectpicker('refresh');
});
updateServiceControlUI('cloudflared');
$("#reconfigureAct").SimpleActionButton({
onPreAction: function() {
const dfObj = $.Deferred();
saveFormToEndpoint("/api/cloudflared/settings/set", 'frm_GeneralSettings', dfObj.resolve, true, dfObj.reject);
return dfObj;
},
onAction: function() {
updateServiceControlUI('cloudflared');
},
});
});
</script>
<div class="content-box">
{{ partial('layout_partials/base_form', ['fields': general, 'id': 'frm_GeneralSettings']) }}
</div>
{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/cloudflared/service/reconfigure', 'data_service_widget': 'cloudflared'}) }}

View file

@ -0,0 +1,34 @@
#!/bin/sh
# Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
configctl template reload OPNsense/Cloudflared
chmod 600 /etc/rc.conf.d/cloudflared
if grep -q 'cloudflared_enable="YES"' /etc/rc.conf.d/cloudflared 2>/dev/null; then
service cloudflared restart
else
service cloudflared stop 2>/dev/null || true
fi

View file

@ -0,0 +1,38 @@
#!/bin/sh
# Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# Start cloudflared only if it is enabled but not currently running.
# Used by the cron recovery job so healthy tunnels are not disrupted.
if ! grep -q 'cloudflared_enable="YES"' /etc/rc.conf.d/cloudflared 2>/dev/null; then
exit 0
fi
if service cloudflared status > /dev/null 2>&1; then
exit 0
fi
service cloudflared start

View file

@ -0,0 +1,104 @@
"""
Copyright (C) 2026 Richard Aspden <rick+github@insanityinside.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""
import re
from . import NewBaseLogFormat
# zerolog 3-letter level codes → syslog numeric severity
_SEVERITY = {
'DBG': 7,
'INF': 6,
'WRN': 4,
'ERR': 3,
'FTL': 2,
'PNC': 1,
}
# zerolog: 2026-05-12T11:51:06Z INF message
_RE_ZEROLOG = re.compile(
r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2}))\s+([A-Z]{3})\s+(.*)'
)
# Go stdlib log (e.g. quic-go): 2026/05/12 13:35:44 message — no level field
_RE_GOSTDLIB = re.compile(
r'^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})\s+(.*)'
)
class CloudflaredLogFormat(NewBaseLogFormat):
def __init__(self, filename):
super().__init__(filename)
self._timestamp = None
self._severity = None
self._message = None
def match(self, line):
if 'cloudflared' not in self._filename:
return False
return bool(_RE_ZEROLOG.match(line) or _RE_GOSTDLIB.match(line))
def set_line(self, line):
m = _RE_ZEROLOG.match(line)
if m:
ts_raw = m.group(1)
# Normalise Z suffix so fromisoformat() accepts it (Python < 3.11)
self._timestamp = ts_raw.replace('Z', '+00:00')
self._severity = _SEVERITY.get(m.group(2), 6)
self._message = m.group(3)
return
m = _RE_GOSTDLIB.match(line)
if m:
# Go stdlib format has no level; default to Informational
self._timestamp = m.group(1).replace('/', '-', 2).replace(' ', 'T')
self._severity = 6
self._message = m.group(2)
return
self._timestamp = None
self._severity = 6
self._message = line
@property
def timestamp(self):
return self._timestamp
@property
def process_name(self):
# zerolog format has no process field; hardcode so the column isn't blank
return 'cloudflared'
@property
def pid(self):
return None
@property
def severity(self):
return self._severity
@property
def line(self):
return self._message

View file

@ -0,0 +1,37 @@
[start]
command:service cloudflared start
parameters:
type:script
message:Starting Cloudflare Tunnel service
[stop]
command:service cloudflared stop
parameters:
type:script
message:Stopping Cloudflare Tunnel service
[restart]
command:service cloudflared restart
parameters:
type:script
message:Restarting Cloudflare Tunnel service
description:Restart Cloudflare Tunnel
[status]
command:service cloudflared status; exit 0
parameters:
type:script_output
message:Requesting Cloudflare Tunnel status
[recover]
command:/usr/local/opnsense/scripts/OPNsense/Cloudflared/recover.sh
parameters:
type:script
message:Recovering Cloudflare Tunnel
description:Recover Cloudflare Tunnel (start if not running)
[reconfigure]
command:/usr/local/opnsense/scripts/OPNsense/Cloudflared/reconfigure.sh
parameters:
type:script
message:Reconfiguring Cloudflare Tunnel

View file

@ -0,0 +1,2 @@
rc.conf.d:/etc/rc.conf.d/cloudflared
config.yml:/usr/local/etc/cloudflared/config.yml

View file

@ -0,0 +1,14 @@
metrics: localhost:2000
no-autoupdate: true
{% if helpers.exists('OPNsense.Cloudflared.general.post_quantum') and OPNsense.Cloudflared.general.post_quantum == '1' %}
post-quantum: true
{% endif %}
{% if helpers.exists('OPNsense.Cloudflared.general.quic_disable_pmtu_discovery') and OPNsense.Cloudflared.general.quic_disable_pmtu_discovery == '1' %}
quic-disable-pmtu-discovery: true
{% endif %}
{% if helpers.exists('OPNsense.Cloudflared.general.protocol') and OPNsense.Cloudflared.general.protocol != 'auto' %}
protocol: {{ OPNsense.Cloudflared.general.protocol }}
{% endif %}
{% if helpers.exists('OPNsense.Cloudflared.general.log_level') and OPNsense.Cloudflared.general.log_level != 'info' %}
loglevel: {{ OPNsense.Cloudflared.general.log_level }}
{% endif %}

View file

@ -0,0 +1,10 @@
{% if helpers.exists('OPNsense.Cloudflared.general.enabled') and OPNsense.Cloudflared.general.enabled == '1' %}
cloudflared_enable="YES"
{% else %}
cloudflared_enable="NO"
{% endif %}
cloudflared_conf="/usr/local/etc/cloudflared/config.yml"
{% if helpers.exists('OPNsense.Cloudflared.general.token') %}
cloudflared_env="TUNNEL_TOKEN={{ OPNsense.Cloudflared.general.token|trim }}"
{% endif %}
cloudflared_mode_options="run"

View file

@ -0,0 +1,6 @@
###################################################################
# Local syslog-ng configuration [cloudflared].
###################################################################
filter f_local_cloudflared {
program("cloudflared");
};