ncclient – Nebula Commander device client
License: GNU GPLv3 or later. See LICENSE in this directory.
A small client that works like Defined.net's dnclient and dnclientd: enroll once with a code from Nebula Commander, then run as a daemon to pull config and certificates and optionally orchestrate the Nebula process (start/restart it when config changes).
Install
From PyPI (recommended):
pip install nebula-commander
This installs the ncclient command. Requires Python 3.10+.
From source (e.g. repo clone):
pip install -r client/requirements.txt
# then run as: python -m client --server URL enroll --code XXX
Or install the package in development mode from the client/ directory: pip install -e . to get the ncclient command.
Enroll (one-time)
- In Nebula Commander, open Nodes, find your node, and click Enroll.
- Copy the enrollment code and run on the device:
ncclient enroll --server https://YOUR_NEBULA_COMMANDER_URL --code XXXXXXXX
This saves the device token to your OS credential store (e.g. Windows Credential Manager, macOS Keychain, Linux Secret Service) and saves the server URL so you can run ncclient run without --server next time.
Upgrading from an older version: If you previously used a token file, you must re-enroll once after upgrading. You can then delete the old token file if it exists (~/.config/nebula-commander/token or /etc/nebula-commander/token).
Run (daemon)
Poll for config and certs every 60 seconds, write them to /etc/nebula (or another directory), and run Nebula (from your PATH) when config changes. If you have already enrolled, you can omit --server (the URL is saved at enrollment):
ncclient run
# or: ncclient run --server https://YOUR_NEBULA_COMMANDER_URL
ncclient assumes nebula is on your PATH and will start/restart it by default. Options:
--output-dir DIR– where to writeconfig.yaml,ca.crt,host.crt(default:/etc/nebulaon Linux/macOS,~/.nebulaon Windows)--interval N– poll interval in seconds (default: 60)--nebula PATH– path to thenebulabinary only if it's not in PATH (e.g.--nebula /opt/homebrew/bin/nebula). Omit this whennebulais already on your PATH.--restart-service NAME– instead of running nebula directly, restart this systemd service after config updates (e.g.nebula). Use only one of--nebulaor--restart-service.--accept-dns– enable split-horizon DNS: fetch DNS config from the server, writedns-client.json, and apply it so the Nebula domain is resolved via the network’s DNS (e.g. lighthouse). On Linux the client tries, in order: systemd-resolved, dnsmasq, NetworkManager, systemd-networkd (with resolved), then /etc/resolv.conf. The resolv.conf fallback is best-effort only (no guaranteed split-horizon). Install systemd-resolved or dnsmasq for proper split-horizon. On Windows it uses NRPT. Run as root (Linux) or Administrator (Windows) to apply.
Example – nebula in a non-standard location:
ncclient run --server https://nc.example.com --nebula /usr/local/bin/nebula
Example – use systemd to run Nebula; ncclient only restarts the service:
ncclient run --server https://nc.example.com --restart-service nebula
When the certificate was created via the server (Create certificate in the Nebula Commander UI), the bundle includes host.key and no manual copy is needed. For certificates created via Sign (betterkeys, client-generated key), the server does not have the key; place your own host.key in the same directory as the generated certs.
Linux: Creating the Nebula TUN device requires root. Run ncclient as root so the Nebula process can create the interface, e.g. sudo ncclient run --server https://... (or use --output-dir ~/.nebula and run as root so nebula reads from a dir that has host.key).
Split-horizon DNS (Linux)
With --accept-dns, ncclient applies split-horizon DNS so the Nebula domain (e.g. *.nebula.example.com) is resolved by the network’s DNS server while everything else keeps using the host's normal DNS. It detects how the host manages DNS and uses the matching mechanism:
- NetworkManager delegating to systemd-resolved, or systemd-resolved on its own (e.g. systemd-networkd servers) – per-link DNS on the Nebula interface via
resolvectl(~domainrouting, not a default route). - NetworkManager with its dnsmasq plugin – a
server=/domain/ipsnippet in/etc/NetworkManager/dnsmasq.d/. If NetworkManager writes/etc/resolv.confdirectly (dns=default, e.g. Debian) anddnsmasq(Debian:dnsmasq-base) is installed, ncclient switches NetworkManager to its dnsmasq plugin with a drop-in in/etc/NetworkManager/conf.d/. Without dnsmasq or systemd-resolved, split-horizon can't work there and ncclient says so. - A standalone dnsmasq service – snippet under
/etc/dnsmasq.d/. - /etc/resolv.conf – append nameserver and search domain (last resort; not true split-horizon; the first nameserver often gets all queries).
When NetworkManager is running, ncclient also marks the Nebula interface unmanaged (/etc/NetworkManager/conf.d/91-nebula-commander-unmanaged.conf) so NetworkManager never takes over the tun device, and it deletes stale nebula* tun profiles older versions left in /etc/NetworkManager/system-connections/. Every poll it checks the DNS is still in effect and re-applies it if a resolver restart or a recreated interface undid it. Everything is removed again when ncclient stops or DNS is turned off.
If nothing works, ncclient reports why and what to install. For proper split-horizon, use a system with systemd-resolved or dnsmasq. Manual apply/remove scripts (for when ncclient itself isn't run with enough privilege to self-apply): client/contrib/dns-apply-linux.sh and client/contrib/dns-apply-windows.ps1. Note: the Linux fallback script only covers the systemd-resolved, dnsmasq, and resolv.conf backends (not NetworkManager) - on those setups, run ncclient --accept-dns directly instead.
Subnet router / exit node
Being picked as a consumer of a subnet route or exit node (the gateway's "Used by"
list, or this node's own "Use Subnet Router"/"Use Exit Node" dropdown in the admin
UI) makes the route available to this device - it isn't used automatically. The
same way --accept-dns is a separate local opt-in on top of DNS being enabled for
the network, this device has to locally accept a route before Nebula actually
uses it. This is cross-platform (Linux and Windows, since both run through the same
ncclient core):
ncclient routes list # what's offered vs. accepted
ncclient routes accept 192.168.1.0/24 # add --via IP if offered by multiple gateways
ncclient routes reject 192.168.1.0/24
ncclient routes accept-exit-node --via 10.100.0.30
ncclient routes reject-exit-node
Multiple subnet routes can be accepted at once as long as their CIDRs don't
overlap - accept rejects a conflicting one with an explanation of which
already-accepted route it clashes with. At most one exit node is accepted at a
time. A change takes effect within one poll cycle, no restart needed.
On Linux, when a node is set up as a gateway (advertising a subnet or acting
as an exit node itself, not consuming one), ncclient also automatically enables
IP forwarding and installs the nftables rules that make forwarding actually work -
no manual host setup needed there either. See
../docs/unsafe-routes.md for the full picture on both
sides (gateway and consumer), and what to do on Windows/macOS/Docker or a bare
nebula install where the gateway-side automation doesn't run.
Troubleshooting
-
No network device (tun) created / can't ping Nebula IP
On Linux, Nebula needs root to create the TUN interface. Runsudo ncclient run --server ....
If the certificate was created via the server (Create certificate in the UI), the bundle includeshost.keyand no manual copy is needed. If it was created via Sign (betterkeys), put yourhost.keyin the output dir (e.g./etc/nebula). Nebula will exit or fail withouthost.key.
Nebula's errors are printed to the same terminal; look for messages like "failed to get tun device" (permission) or "no such file" (missing host.key). -
Nebula starts then exits
Check the Nebula error lines ncclient prints. Common causes: missinghost.key(for Sign flow; Create flow includes it in the bundle), wrong config path, or (Linux) need to run as root.
Running at startup
Quick install (Linux)
On Linux you can install the systemd service with one command:
sudo ncclient install
This checks that you have already enrolled (token is stored in the OS credential store). If not, it prints the exact ncclient enroll --server URL --code XXXXXXXX command to run first (get the code from the Nebula Commander UI: Nodes → Enroll). Then it prompts for the server URL and optional settings (output directory, poll interval, nebula path, restart-service), writes /etc/default/ncclient and /etc/systemd/system/ncclient.service, enables the service, and optionally starts it. Use --no-start to enable without starting; use --non-interactive with NEBULA_COMMANDER_SERVER (and optional env vars) set for scripting.
Manual setup (all platforms)
Run ncclient run under systemd (or your init system) so config and certs stay up to date. ncclient runs nebula from your PATH by default; use --restart-service if you prefer to have systemd run Nebula and ncclient only restart the service. Example configs are in examples/; see examples/README-startup.md for step-by-step install on macOS and Windows.
macOS
ncclient works on macOS (Intel and Apple Silicon). Use Python 3.10+ and install with pip install nebula-commander.
- Token is stored in the OS credential store (Keychain).
- Default output dir is
/etc/nebula(same as Linux). If you run as a normal user, use--output-dir ~/.nebulaso you don't need sudo to write config/certs. - Nebula: ncclient runs
nebulafrom your PATH by default. Afterbrew install nebula, you usually don't need--nebula. Use--nebula /opt/homebrew/bin/nebula(Apple Silicon) or--nebula /usr/local/bin/nebula(Intel) only if it's not on PATH. Do not use--restart-service; macOS uses launchd, not systemd. - To run ncclient in the background, use launchd (e.g. a LaunchAgent in
~/Library/LaunchAgentsor a LaunchDaemon in/Library/LaunchDaemons).
Windows 11
ncclient works on Windows 11. Use Python 3.10+ and install with pip install nebula-commander.
- Token is stored under
%USERPROFILE%\.config\nebula-commander\token. - Default output dir for config and certs is
%USERPROFILE%\.nebula. Override with--output-dir(e.g.C:\ProgramData\Nebulaif you run as Administrator). - Nebula: ncclient runs
nebulafrom your PATH by default. Ifnebula.exeis not on PATH, use--nebula "C:\Path\To\nebula.exe". Do not use--restart-service; there is no systemd on Windows. - Run ncclient in a terminal or install it as a Windows service (e.g. with NSSM or Task Scheduler) so it keeps running.
Windows GUI app
Nebula Commander (client/windows-app/, WinUI 3) ships alongside the CLI,
talking to the NebulaCommanderService Windows Service over the shared
%ProgramData%\nebula-commander\ state. A proper windowed app with side tabs
(Status, Enrollment, Settings), minimizes to the tray on close instead of
exiting. The Status page shows live server/service/interface/DNS state, lets
you start/stop/restart the service, view config.yaml, and accept/reject
subnet routes and exit nodes interactively. See
client/windows-app/README.md.
(The older Python/Tkinter system-tray-only app that used to ship alongside it
has been removed - see client/windows/README.md for
the NebulaCommanderService it still talks to.)
Release files for nebula-commander 0.6.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nebula_commander-0.6.4.tar.gz | 243.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nebula_commander-0.6.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 322.7 kB
Release files / nebula_commander-0.6.4.tar.gz
| Download URL | nebula_commander-0.6.4.tar.gz |
|---|---|
| Size | 243.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
47e0afedc8d9854f15998c34335c88fcbb4297b14c76d10adf93433656882b31
|
|
BLAKE2b-256 checksum How to use checksums |
cad08f336188a10d878e6c4d3675da8cbae05db5b3d3699cf7bcc9d82a4d3b4e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / nebula_commander-0.6.4-py3-none-any.whl
| Download URL | nebula_commander-0.6.4-py3-none-any.whl |
|---|---|
| Size | 79.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
316a04c199cc7b94095dd5d8c70a15e5ab16865ac2e79433f5d33bd5e5b2477b
|
|
BLAKE2b-256 checksum How to use checksums |
2d94ddc9689da94ce7ed0749215601bc55275884af07077625e3aa4cc13a8acd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log