Skip to content

net/nebula: new plugin — Slack Nebula mesh overlay VPN#5504

Closed
hstern wants to merge 17 commits into
opnsense:masterfrom
hstern:net-nebula
Closed

net/nebula: new plugin — Slack Nebula mesh overlay VPN#5504
hstern wants to merge 17 commits into
opnsense:masterfrom
hstern:net-nebula

Conversation

@hstern

@hstern hstern commented Jun 16, 2026

Copy link
Copy Markdown

Important notices

Before you submit a pull request, we ask you kindly to acknowledge the following:

If AI was used, please disclose:

  • Model used: Claude (Anthropic) — Claude Code, Opus- and Sonnet-class 4.x models.
  • Extent of AI involvement: Substantial. The plugin was authored primarily with Claude Code under my direction, using a workflow where each change was implemented and then reviewed in a separate pass. I directed the architecture and scope and reviewed every change, and I built, deployed and tested the result end-to-end on a live OPNsense 26.1 VM (instance create → CA → cert sign → Apply → tunnel up and assignable). The design decisions, scoping and verification are mine; the code generation and much of the review assistance were AI.

Note: I originally replaced this template with a free-form writeup, which dropped the AI disclosure above — restored here. Apologies; that was not intentional.


Describe the problem

OPNsense has no native interface for Slack Nebula, a certificate-authenticated mesh overlay VPN. Unlike the mesh VPNs already packaged (netbird, zerotier, tailscale), Nebula has no coordination/control-plane service: identity is pure offline PKI, and the only infrastructure is a "lighthouse" that does host discovery and NAT punching — it brokers no trust and holds no config. It is the one option where OPNsense can be both the CA and the lighthouse — a fully self-hosted, no-account overlay — but today that requires hand-managing certificates, YAML and the daemon from the CLI.


Describe the proposed solution

A new plugin net/nebula that manages Nebula from the OPNsense UI. It declares PLUGIN_DEPENDS = nebulasecurity/nebula (already in opnsense/ports at 1.10.3), so binary management stays with pkg.

Scope note (per review feedback): as submitted this PR contains the complete plugin, which is too much to review in one drop. I'm restructuring this into a series of small, individually-reviewable PRs, starting with a sub-1000-LOC MVP (single instance + config generation + service lifecycle + imported cert material), then adding CA/cert management, first-class interface registration, and overlay firewall / routing / monitoring one PR at a time. See the discussion below.


Related issue

N/A — I should have opened an issue first for a change this size; happy to open one and move the design discussion there if preferred.

hstern added 17 commits June 7, 2026 16:42
Removing the master toggle field from the model and pointing the start /
restart / apply paths at per-instance enabled. Matches WireGuard's model
where each instance is its own daemon and there is no global subsystem
enable.

Fixes a UX dead-end on a fresh install: the model had a general.enabled
field with no UI surface, so the instances page Apply / Start widgets did
nothing (serviceEnabled() returned false, the service-control widget did
not render, setup.php apply silently no-op'd) until the field was poked
via the API or config.xml. The plugin's installed PLUGIN_DEPENDS=nebula
binary alone meant a user could not enable the daemon at all without
already knowing the internal model layout.

Changes:
- src/opnsense/mvc/app/models/OPNsense/Nebula/Nebula.xml: drop the
  <general><enabled> field (the only field under <general>).
- src/opnsense/mvc/app/controllers/OPNsense/Nebula/Api/ServiceController.php:
  drop \$internalServiceEnabled; override serviceEnabled() to return true
  when at least one instance is enabled, so the standard reconfigure /
  status / widget plumbing works.
- src/etc/inc/plugins.inc.d/nebula.inc: nebula_enabled() now iterates
  instances (same semantic for the services / devices / interface hooks).
- src/opnsense/scripts/OPNsense/Nebula/setup.php: strip \$general_enabled
  from nebula_apply(), nebula_restart_instance(), and the CLI dispatcher;
  per-instance enabled is the only gate.
- tools/reseed_demo.php: drop the now-unused general.enabled seed.
hstern added a commit to hstern/plugins that referenced this pull request Jun 16, 2026
… drop PR sequencing gate

Adds docs/README (index), overview.md, clients.md, widget.md, and architecture.md following Diataxis (how-to / reference / explanation). architecture.md covers the multi-controller model, token cache, read-only posture + rationale, the ARP/DHCP enrichment join, out-of-scope (VPN/hosting), and the pagination/v1 API quirks. Drops the net/nebula (opnsense#5504) sequencing gate from AGENTS.md — the two plugins are independent and net/omada's PR need not wait.
@Monviech

Monviech commented Jun 25, 2026

Copy link
Copy Markdown
Member

There are already multiple mesh vpn solutions in the plugins (netbird, zerotier, tailscale). There are no lack of alternatives, what tricks can net/nebula do that make it interesting enough to become a permanent support burden?

Also this is a huge amount of code and other files. If you want to see a recent PR for a plugin that got a chance, check the Cloudflared PR. Thats a scope that's reasonable.

I checked some of the files and for an initial contribution the scope is way out of hands here.

@Monviech Monviech added the support Community support or awaiting triage label Jun 25, 2026
@hstern

hstern commented Jun 26, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look, and fair questions on both counts.

On "why Nebula when we already have netbird/zerotier/tailscale": they're all mesh VPNs, but Nebula sits at a different point architecturally. Tailscale, NetBird and ZeroTier are all built around a coordination/control plane — a SaaS by default (or a self-hosted Headscale / management+signal server / network controller if you do the extra work). Trust and config are brokered through that service.

Nebula has no such control plane. Identity is pure offline PKI: you run a CA you control, sign host certificates offline, and the only piece of infrastructure is a "lighthouse" that does host discovery and NAT punching — it brokers no trust and holds no config. So you can stand up a fully self-hosted, no-account, no-phone-home overlay where OPNsense is both the CA and the lighthouse. For a self-hosting/sovereignty-minded firewall audience that's a meaningfully different proposition from the three SaaS-coordinated options, not just a fourth flavour of the same thing. It's also already in opnsense/ports (security/nebula 1.10.3), so the runtime dependency is in-tree.

On scope: you're right, this is too much for a first drop. I'd like to cut it down to a minimal viable plugin along the lines of the Cloudflared PR — roughly: instance config + PKI (CA + host certs) + first-class interface registration + Apply/lifecycle — and defer the overlay firewall, routing, live monitoring and dashboard widget to follow-up PRs once the core has landed and been reviewed.

If that phasing sounds right to you, I'll re-roll this PR down to that core. Happy to adjust where the line is drawn — if there's a particular subset you'd rather see first, tell me and I'll scope to it.

@Monviech

Monviech commented Jun 26, 2026

Copy link
Copy Markdown
Member

Thanks for explaining. You also deleted the PR template, I want to know how much of this is AI.

You have probably seen in the Cloudflared PR that we are generally fine with AI, but it's very hard and time consuming to review since it can be hundreds of smaller or bigger things that are wrong.

It's a lot of effort on the review side, while it is less effort to just regenerate plugins via AI.

If you want to pitch this I want an MVP that is way below 1000 LOCs (code and comments combined) as a hard limit. Below 1000 LOC would be ideal, but if you think that is unrealistic, please explain why and where the complexity is unavoidable.

All that said: no promises it will actually pass the bar and make it in.

@hstern

hstern commented Jun 26, 2026

Copy link
Copy Markdown
Author

Thanks — fair on all counts, and let me answer the direct question first.

Disclosure: this was built substantially with AI. Model: Claude (Anthropic), via Claude Code — Opus/Sonnet 4.x. Extent: I directed the architecture and scope and reviewed every change, using a workflow where each change is implemented and then reviewed in a separate pass; I built, deployed and tested it end-to-end on a live OPNsense 26.1 VM. The design decisions and verification are mine; the code generation and much of the review assistance were AI. I've restored the PR template in the description with this filled in.

Process, two apologies: I overwrote the PR template, which dropped that disclosure — not intentional, now fixed. And I didn't open an issue first for what is plainly a non-trivial change. If you'd prefer, I'm happy to open an issue, move the design discussion there, and hold this PR pending that.

Scope / the <1000 LOC limit: you're right, 21k LOC in one drop is unreasonable to review, and I accept the limit. My mistake was treating this as "one and done for 1.10.3" — a whole plugin in a single PR. I'd much rather do it as a series of small, individually-reviewable PRs. Concretely:

  • PR 1 — the MVP, genuinely under 1000 LOC: single instance, config generation + service lifecycle (Apply / start / stop, status badge), cert material imported as text. Roughly the cloudflared footprint.
  • Then, one small PR at a time: CA / cert management (the self-hosted-PKI piece that is the whole point of choosing Nebula), first-class interface registration, and after that overlay firewall / routing / live monitoring.
  • To avoid shipping a half-built plugin to users mid-series, I'm glad to target a staging branch if that's easier for you, or to sequence them so each PR is independently shippable to master — whichever fits how you'd rather review. If a different ordering or cut works better, I'll scope to it.

For context: I've been building my own routers and firewalls since 1997 and run this on my own network. I'd genuinely like to become an active member here and do this the right way, rather than drop a wall of code and walk off. No expectation it makes the bar — I'd just like the chance to earn it.

@Monviech

Monviech commented Jun 26, 2026

Copy link
Copy Markdown
Member

Thanks for explaining things.

I understand your ambitions, yet currently I don't find any user issues who were actively asking for this.

Since there are already plenty of alternatives available there doesn't seem to be immediate need for more.

VPN related plugins are notorious for complexity, support and maintanance in the long run.

I'm just not sure it's worth the effort right now to dive into this. If your plugin will turn out that large eventually, it might make more sense to run it in a container behind the firewall, e.g like alternatives (Netmaker comes to mind).

Just trying to set expectations, it's not about your work, more about the scope and long term support of complex vpn solutions.

@hstern

hstern commented Jun 26, 2026

Copy link
Copy Markdown
Author

I've built the minimal MVP we discussed, on a separate branch so you can look at it without the noise of this PR:

https://github.com/hstern/plugins/tree/net-nebula-mvp

It's 749 LOC across 14 files (code + comments combined — a good chunk of that is the BSD header on each file), well under the 1000 hard limit. Scope is a single Nebula node:

  • One settings page under VPN → Nebula: enable, listen port, lighthouse toggle + hosts, static host map, and the trusted CA / host certificate / host key pasted as text.
  • config.yml generation, validated with nebula -test before launch.
  • Service lifecycle (start/stop/restart/status) via configd with a status badge; daemon output routes to the nebula syslog tag.

Deliberately deferred to follow-up PRs (everything else in this PR): CA/certificate management and signing, first-class interface registration, the overlay firewall, routing, live monitoring, and the dashboard widget.

Built, installed and smoke-tested on a live OPNsense 26.1 VM (FreeBSD 14.3): the page loads and saves, the model round-trips, and the generator produces a valid config.

If the scope and size look right, I'll open it as a proper PR and we can drive the rest as a small series (closing this one in its favour). Happy to move the line wherever you'd prefer to see the first cut.

@AdSchellevis

Copy link
Copy Markdown
Member

I think the main problem with these AI generated plugins is that it's extremely simple to generate something, but maintaining code in the long run in a structured way (without constant movements of large chunks of code) is just something else.

My main concern is adding a lot of plugins in a short time and having no real maintainers a couple of months later... Then the next AI enabled user finds a security concern and starts dropping these on our team....

Not stating we shouldn't add new plugins, but we should think about the rules and how to explain to people what to expect. (e.g. [active] maintainer gone, drop plugin?), also try to avoid being stuck with a degraded code-base because someone wanted a feature which nobody else needs.

At the end of the day, the core team has no interest in taking ownership of these plugins, which means if the maintainer is gone, we risk people being disappointed and left without options.
We also try to avoid being flooded with AI generated content (PR's, issues, security reports,...) and loosing focus on our actual priorities.

Since we can't really assess how likely it is a new contributor will actively support their work in the long run, we risk being stuck between a rock and a hard place after publishing the initial version of a plugin.

There are various possible models to cope with these new challenges, all with their own pro's and cons in terms of security and flexibility I'm afraid.

@hstern

hstern commented Jul 6, 2026

Copy link
Copy Markdown
Author

Thank you @Monviech and @AdSchellevis for the review and the candid, thoughtful feedback. The points about scope, review burden, and — especially — the long-term maintenance model for AI-assisted plugins are fair and well-put. I appreciate you engaging with it seriously and setting expectations clearly rather than leaving it in limbo.

Given all of that, I've decided the right home for this is a self-hosted repository rather than the official tree. That keeps the ongoing support and security-response burden with me instead of the core team — which is exactly the concern raised — while letting me maintain the full feature set. Closing this PR accordingly.

For anyone who finds this later and wants the plugin, it lives here:

Install on OPNsense (root shell):

fetch -o /usr/local/etc/pkg/keys/stern.pub   https://hstern.github.io/opnsense-repo/repo.pub
fetch -o /usr/local/etc/pkg/repos/stern.conf https://hstern.github.io/opnsense-repo/repo.conf
pkg update && pkg install os-nebula

It then appears under System → Firmware → Plugins and updates like any other package.

Thanks again for your time and attention — it's genuinely appreciated.

@hstern hstern closed this Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

support Community support or awaiting triage

Development

Successfully merging this pull request may close these issues.

3 participants