net/cloudflared: new plugin for Cloudflare Tunnel integration#5429
net/cloudflared: new plugin for Cloudflare Tunnel integration#5429insanityinside wants to merge 4 commits intoopnsense:masterfrom
Conversation
Reworks the original plugin by Alan Martines to address the architectural feedback on PR opnsense#5406: the custom binary installer is replaced with PLUGIN_DEPENDS= cloudflared, delegating binary management entirely to pkg via the FreeBSD ports tree. The plugin is now a pure configuration wrapper. Binary and service: - Remove install_binary.sh and bundled rc.d script; use FreeBSD port - Pass tunnel token via TUNNEL_TOKEN env var (cloudflared_env in rc.subr) so it does not appear in ps aux; /etc/rc.conf.d/cloudflared chmod 600 - Add config.yml template; move options out of rc.conf.d command args - Hardcode no-autoupdate: true (pkg manages the binary; self-update is inappropriate) New features: - Transport protocol selector: Auto (QUIC with HTTP/2 fallback, default), QUIC-only (UDP 7844), HTTP/2-only (TCP 443) - Automatic outbound firewall rule for TCP/UDP 7844 via cloudflared_firewall() hook; UDP active for Auto and QUIC-only modes, TCP for Auto and HTTP/2-only - quic-disable-pmtu-discovery option: workaround for intermittent QUIC stream errors on networks where ICMP is filtered - Log viewer tab with client-side pagination (25/50/100/200 lines/page, Older/Newer navigation) and Follow mode for live tailing - Crash recovery: monitor.sh syshook and cron job restart cloudflared if it exits unexpectedly; sentinel file suppresses watchdog after intentional stop - newwanip/newwanip6 hook to restart on WAN IP change if daemon exits Reliability fixes: - Improve tunnel health detection: cross-check Prometheus metrics against log output to catch stale ha_connections; report accurate down state Other: - Security notice in UI: tunnel traffic bypasses OPNsense firewall rules - Translations for 20 languages in addition to the original pt_BR (machine generated) - BSD license headers on all scripts - README.md entry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new net/cloudflared plugin to integrate Cloudflare Tunnel (cloudflared) into OPNsense as a managed service with a native MVC UI and backend/service wiring, relying on the FreeBSD cloudflared package for the binary.
Changes:
- Introduces MVC pages + API endpoints to configure, control, and display tunnel/log status.
- Adds configd actions, templates, and scripts to generate config, apply tunables, and expose tunnel health.
- Adds firewall hook + watchdog (cron) for protocol-specific outbound rules and crash recovery.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds the new plugin to the repository plugin list. |
| net/cloudflared/Makefile | Declares the plugin and its dependency on cloudflared. |
| net/cloudflared/pkg-descr | Describes the plugin’s purpose and major features. |
| net/cloudflared/LICENSE | Adds plugin license file. |
| net/cloudflared/src/opnsense/service/templates/OPNsense/Cloudflared/+TARGETS | Installs generated templates to rc.conf.d, config.yml, and sysctl.d. |
| net/cloudflared/src/opnsense/service/templates/OPNsense/Cloudflared/rc.conf.d | Generates rc.conf.d service enablement + token env wiring. |
| net/cloudflared/src/opnsense/service/templates/OPNsense/Cloudflared/config.yml | Generates cloudflared config (metrics, protocol, flags). |
| net/cloudflared/src/opnsense/service/templates/OPNsense/Cloudflared/tunables.conf | Generates sysctl tunables for QUIC performance. |
| net/cloudflared/src/opnsense/service/conf/actions.d/actions_cloudflared.conf | Defines configd actions (start/stop/restart/status/reconfigure/log/tunnel_status). |
| net/cloudflared/src/opnsense/scripts/OPNsense/Cloudflared/reconfigure.sh | Reloads templates, applies sysctls, restarts service, reloads firewall. |
| net/cloudflared/src/opnsense/scripts/OPNsense/Cloudflared/tunnel_status.sh | Computes tunnel health via metrics + log cross-check. |
| net/cloudflared/src/opnsense/scripts/OPNsense/Cloudflared/monitor.sh | Watchdog script to restart the service when it dies unexpectedly. |
| net/cloudflared/src/opnsense/mvc/app/views/OPNsense/Cloudflared/index.volt | Adds UI for settings, service controls, health badge, and log viewer. |
| net/cloudflared/src/opnsense/mvc/app/models/OPNsense/Cloudflared/Menu/Menu.xml | Adds Services menu entry for the plugin UI. |
| net/cloudflared/src/opnsense/mvc/app/models/OPNsense/Cloudflared/ACL/ACL.xml | Adds ACL for UI/API endpoints. |
| net/cloudflared/src/opnsense/mvc/app/models/OPNsense/Cloudflared/Cloudflared.xml | Defines plugin configuration model (enabled/token/protocol/tunables). |
| net/cloudflared/src/opnsense/mvc/app/models/OPNsense/Cloudflared/Cloudflared.php | Adds BaseModel class for the plugin model. |
| net/cloudflared/src/opnsense/mvc/app/controllers/OPNsense/Cloudflared/IndexController.php | Serves the main UI page and general form. |
| net/cloudflared/src/opnsense/mvc/app/controllers/OPNsense/Cloudflared/forms/general.xml | Defines UI fields (token, protocol, tunables, flags). |
| net/cloudflared/src/opnsense/mvc/app/controllers/OPNsense/Cloudflared/Api/SettingsController.php | Exposes settings get/set API for the model. |
| net/cloudflared/src/opnsense/mvc/app/controllers/OPNsense/Cloudflared/Api/ServiceController.php | Exposes service actions plus tunnel status/log APIs. |
| net/cloudflared/src/etc/inc/plugins.inc.d/cloudflared.inc | Adds firewall hook, WAN-IP hook behavior, and service registration. |
| net/cloudflared/src/etc/cron.d/cloudflared | Adds a per-minute cron job to run the watchdog script. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # DO NOT EDIT THIS FILE -- OPNsense auto-generated file | ||
| # |
There was a problem hiding this comment.
Follows OPNsense plugin conventions, that's why I left it in - it was flagged it in review.
|
I did not intend to invoke Copilot to review that, automatic Github settings at work. It did identify a couple of useful things (bad links) which I fixed in f99ed31 below that I hadn't caught in my code review, and one comment on the cronjob - followed convention I saw in other plugins, but happy to tidy up the header comments if appropriate. |
| <post_quantum type="BooleanField"> | ||
| <default>0</default> | ||
| </post_quantum> | ||
| <kern_ipc_maxsockbuf type="IntegerField"> |
There was a problem hiding this comment.
I would remove the tunables glue here. Documenting what needs to be changed is better.
| }; | ||
|
|
||
| // Save settings and reconfigure the service | ||
| $("#saveAct").click(function() { |
There was a problem hiding this comment.
Why not use SimpleActionButton, it does this all for you.
There was a problem hiding this comment.
That's what I get for letting Claude do the heavy lifting. Will look into it.
| }); | ||
|
|
||
| // Service control | ||
| $("#startBtn").click(function() { |
There was a problem hiding this comment.
Shouldn't be needed, check out examples here:
In general check out this above plugin. I made it very recently and its a good reference how a lean wrapper plugin should look like.
| updateServiceStatus(i18n); | ||
| setInterval(function() { updateServiceStatus(i18n); }, 5000); | ||
|
|
||
| // Log tab: load on first activation, refresh on demand |
There was a problem hiding this comment.
Just use the log page thats included in core
| 1, | ||
| [ | ||
| 'descr' => 'Allow Cloudflare Tunnel QUIC (autogenerated)', | ||
| 'direction' => 'out', |
There was a problem hiding this comment.
I would remove auto generated firewall rules. Traffic initiated by the firewall itself has a default out allow rule.
There was a problem hiding this comment.
Didn't work in real life without an outbound rule, pf blocked the traffic, I was surprised by the behaviour - I didn't expect this was necessary.
What's the best approach here?
There was a problem hiding this comment.
It depends on your ruleset, if you have rules somewhere that block all traffic in the out direction (e.g. in floating) e.g. in GeoIP or blocklist aliases, this can happen.
But generally there is a "Allow all" rule for firewall initiated traffic, you don't need these rules in the plugin.
Best remove auto generated rules in general and document these rules might be needed to be set manually. A small https://github.com/opnsense/docs manual how to use your plugin is a good choice for that.
E.g. here an example from another plugin I maintain:
https://github.com/opnsense/docs/blob/master/source/manual/how-tos/caddy.rst#prepare-opnsense-for-caddy-after-installation
|
I would suggest you remove as much cruft as possible and turn this into a minimum viable product first. I'm sure it could be 30-40% less code than this when using modern examples (e.g. the plugin I referenced). I work with AI every day, and this output here looks too random and overdesigned at quite a few spots. I don't know why the AI made those decisions, but better prompting and better examples should improve the output by a lot. Also keep in mind that you will be the owner of the end result and you will need to maintain this and give support for it. |
| function cloudflared_configure() | ||
| { | ||
| return [ | ||
| 'newwanip' => ['cloudflared_configure_newwanip:10'], |
There was a problem hiding this comment.
Why would that be needed, cloudflare should bind to the * (any) socket, so it won't crash when some IP address changes.
There was a problem hiding this comment.
It's weird behaviour that came up in real-world testing, if it came started before the WAN was up and it couldn't resolve the Cloudflare router IPs. If it can't resolve them on startup, the process exits. It was a belt and braces approach.
Can't explain the :10 though, that's a Claude special.
There was a problem hiding this comment.
For the code you are using the default number of parameters is ok which is 1 so you can omit the ":10" completely.
| { | ||
| return [ | ||
| 'newwanip' => ['cloudflared_configure_newwanip:10'], | ||
| 'newwanip6' => ['cloudflared_configure_newwanip:10'], |
There was a problem hiding this comment.
Oops. Nice hallucination. 🤦🏻♂️
|
Understood. Leave it with me, I'll rebuild it clean by hand, and do my research properly. While it works, it can be a lot slimmer, and I'll take the comments on-board. Worth closing this PR for now and I'll recreate with a much tidier version? I leant a bit too hard on Claude, and I'm still learning the ropes with LLM code generation vs just writing it myself. Appreciate the feedback, I'm on it. |
|
You can leave this open, it helps to track the review progress and the actual changes you implement to improve it. No rush with anything. |
| if (!cloudflared_enabled()) { | ||
| return; | ||
| } | ||
| if (file_exists('/var/run/cloudflared.stopped')) { |
There was a problem hiding this comment.
usually we get around these special magic files unless we're dealing with difficult scenarios such as CARP which are decoupled in cause and effect
There was a problem hiding this comment.
The reason I put that in there was to stop the cronjob auto-restarting the daemon if a user had manually stopped the service while it was still enabled. Covered the scenario if it'd exited due to the previously mentioned odd behaviour if it couldn't resolve the Cloudflare endpoints. If there's a better way, I'm happy to implement.
There was a problem hiding this comment.
This kinda reminds me of wireguard startup failures for configuration where it needs to resolve a hostname, and it only does so exactly once.
Why cant the Cloudflare tunnel thing be smarter and retry it longer, it's purpose is to be behind whacky networks after all... xD
There was a problem hiding this comment.
Yeah, the behaviour of the daemon is impressively dumb. Why it doesn't just loop round to try and resolve again is beyond me, but it just bails out. Only seems to occur if it starts up before it can resolve DNS though - it's fine if it can resolve but can't actually connect. Appeared when I was testing all failure scenarios, when starting the box on a bench with no connection plugged in, but also occurs when the box starts up before it can connect to external resolvers if the daemon comes up first.
There was a problem hiding this comment.
Here is how core solves this for wireguard
If you add a description to an action, it appears configurable in "System - Settings - Cron".
Affected users can then create a cron job if they want to, which is cheap for you since you need less glue to prevent that startup issue in your code.
There was a problem hiding this comment.
I'm unsure why we need this here in this way... there's no need to kill the current one, posix_kill() is not in our code/modules either and we can trust the rc framework to start it. the dynamic hook is ok. there are other ways but none of them are perfect anyway.
Important notices
Before you submit a pull request, we ask you kindly to acknowledge the following:
If AI was used, please disclose:
Describe the problem
OPNsense has no native interface for managing Cloudflare Tunnel (cloudflared). Users behind CGNAT — where a publicly reachable WAN IP is not available — commonly use Cloudflare Tunnel as a supported, production-grade solution for exposing services without port forwarding. Managing it currently requires manual CLI setup with no OPNsense service integration.
Describe the proposed solution
Adds a new plugin
net/cloudflaredthat integrates Cloudflare Tunnel into OPNsense as a first-class service. This is an extensive rework of #5406 (original authorship and credit: @AlanMartines), which this PR builds on and supersedes. It is revised to address the architectural feedback from that review, and integrates my real-world experience with the net/cloudflared port on native FreeBSD.Key architectural change from #5406:
The original PR bundled a custom binary installer that downloaded cloudflared from a third-party GitHub fork. This plugin instead declares
PLUGIN_DEPENDS= cloudflared, delegating binary management entirely to pkg via the FreeBSD ports tree. The plugin is a pure configuration wrapper, as requested in the review of the original PR.net/cloudflaredwould need adding to the OPNsense port builds as it is not currently present in the repo (I manually installed the pre-built FreeBSD binary package for testing). I'm assuming it's not an automatic process with that PLUGIN_DEPENDS line.Functionality carried from the original PR:
Enhancements added in this rework:
Notes:
Translations:
Machine-assisted translations for 20 languages, including original pt-BR translations from #5406, are prepared and ready to be contributed to the OPNsense lang repo via POEditor as a follow-up to this PR. I'm assuming that's the correct way to handle the translated strings for the plugin, please correct me if not.
Testing:
Tested on OPNsense 26.1.7 / FreeBSD 14.x (amd64).
Related issue
Closes #5070