A headless CLI for connecting to Netmaker remote-access networks — what Netmaker Desktop does, without a GUI. Runs on Linux and macOS.
Netmaker Desktop (the Remote Access Client) is a graphical app. On a server, a container, or anything over SSH there is nothing to click. nmrac drives the same server API from a shell: SSO login in your browser, pick a network, and it renders a WireGuard config and brings the tunnel up with wg-quick.
$ sudo nmrac connect
Networks (user: you@example.com)
1) [off] office-split-tunnel gw gateway-a
2) [on ] lab-network gw gateway-b 10.20.30.8/32
3) [off] build-network gw gateway-c
Select network [1-3], or q to quit: 1netclient is Netmaker's official headless agent, and if it works for you, use it. But netclient join -s <server> registers the machine as a host, and on a Netmaker Pro server that path is restricted to admin users. A regular user gets:
Server SSO Error
Error reason: only admin users can register using SSO
That check is server-side, in the OAuth register callback — no client version or flag gets around it:
// pro/auth/register_callback.go
if user.PlatformRoleID != schema.AdminRole && user.PlatformRoleID != schema.SuperAdminRole {
response := returnErrTemplate(userClaims.getUserName(), "only admin users can register using SSO", state, reqKeyIf)
w.WriteHeader(http.StatusForbidden)Remote access is a different thing entirely. A remote-access client is an external client of a gateway, not a meshed host, and regular users are expected to have them. nmrac uses those endpoints, so it needs no admin role — the same access your Netmaker Desktop login already gives you.
bash(3.2 is fine, so stock macOSbashworks),curl,jqwireguard-tools—wgandwg-quicknode>= 22, for the browser SSO login — it uses Node's built-inWebSocket, no npm packages. Not needed withnmrac login --paste.- root, for
/etc/wireguardandwg-quick - A Netmaker Pro server with OAuth/SSO configured, and a user attached to at least one remote-access gateway
On Debian/Ubuntu:
sudo apt install curl jq wireguard-toolsOn macOS:
brew install jq wireguard-tools nodeTested against Netmaker server v1.5.1. See Server compatibility.
git clone https://github.com/meta-boy/nmrac
sudo install -m 0755 nmrac/nmrac /usr/local/bin/nmracsudo nmrac server https://netmaker.example.com # once
sudo nmrac login # opens a URL, sign in with your IdP
sudo nmrac list # what you can reach
sudo nmrac connect # pick from a menu
sudo nmrac disconnectlogin prints a URL and waits. Open it, authenticate, and the token comes back over the same socket — nothing to copy or paste.
| Command | What it does |
|---|---|
nmrac server <url> |
Save which Netmaker server to talk to. No argument prints the current one. |
nmrac login [--paste] |
SSO login via browser. --paste falls back to pasting a JWT by hand. |
nmrac logout |
Forget the stored token. Leaves tunnels running. |
nmrac list |
Networks, their gateways, and whether each tunnel is up. |
nmrac connect [--dns|--no-dns] [network] |
Bring a tunnel up. Prompts if no network is given. |
nmrac disconnect [network] |
Take a tunnel down, or all of them. |
nmrac status |
WireGuard peer state for active tunnels. |
nmrac conf <network> |
Print the config the server hands out, without applying it. |
nmrac remove <network> |
Delete this machine's client from a network, server-side. |
connect is idempotent. Each machine gets its own external client, keyed on a stable client id ($(hostname)-cli, override with NMRAC_CLIENT_ID), so reconnecting reuses the same client and IP rather than allocating a new one on every call. It will not touch a client belonging to another device.
| Variable | Default | Purpose |
|---|---|---|
NMRAC_API |
from /etc/nmrac/server |
Server URL; overrides the saved one |
NMRAC_CLIENT_ID |
$(hostname)-cli |
This machine's client identity |
NMRAC_STATE_DIR |
/etc/nmrac |
Token, server URL, interface map |
NMRAC_WG_DIR |
/etc/wireguard |
Where .conf files are written |
State on disk:
/etc/nmrac/server— server URL (0644)/etc/nmrac/token— your JWT (0600)/etc/nmrac/ifaces— interface-to-network map/etc/wireguard/nm-*.conf— generated configs (0600)
| Step | Endpoint |
|---|---|
| Login | ws /api/oauth/headless → /api/oauth/register/<state> |
| List networks and gateways | GET /api/users/{user}/remote_access_gw |
| Does this machine have a client? | GET /api/extclients/{net}/{clientid} |
| Create one | POST /api/extclients/{net}/{gwid} |
| Fetch the WireGuard config | GET /api/extclients/{net}/{clientid}/file |
| Remove it | DELETE /api/extclients/{net}/{clientid} |
The config is rendered by the server, the same one the dashboard's View Config gives you. nmrac does not build it, it only strips the DNS = line unless you pass --dns (see below).
Netmaker's OAuth callback dispatches on the length of the state parameter:
// pro/auth/auth.go
node_signin_length = 64
headless_signin_length = 32
switch len(state) {
case node_signin_length: HandleHostSSOCallback(w, r) // admin-gated
case headless_signin_length: HandleHeadlessSSOCallback(w, r) // no admin check
}Both arrive at the same /api/oauth/register/{regKey} URL, but a 64-character state is host registration and admin-only, while a 32-character state is a headless user login — commented in the source as "headless logins such as Netmaker CLI". nmrac opens a websocket to /api/oauth/headless, which mints a 32-character state, and reads the JWT back off that socket.
- Tokens expire. When one does, any command says so; run
nmrac loginagain. Existing tunnels keep working — WireGuard does not care about the API. - No boot persistence. The tunnel does not come back after a reboot.
wg-quick@nm-*.servicewill bring up an existing config, but the token may have expired by then, so a truly durable setup needs a re-login. - DNS is decided per platform. The server always emits a
DNS =line. macOSwg-quickapplies it natively viascutil, so it is kept. Linuxwg-quickshells out toresolvconf, which many minimal images lack, so the line is kept only whenresolvconfis installed and stripped otherwise — the tunnel comes up either way and your existing resolver is left alone. Force it with--dnsor--no-dns. - Interface names. Linux caps them at 15 characters and Netmaker network names are often longer, so configs are named
nm-<first 12 chars>, falling back tonm-<hash>if two networks collide./etc/nmrac/ifaceskeeps the mapping, andlist/statusshow real network names. On macOS the kernel assignsutunNregardless, sostatusshows both (nm-lab-networ -> utun4). - Split vs full tunnel is the gateway's decision, not this tool's.
nmracinstalls exactly theAllowedIPsthe server sends.
| Linux | macOS | |
|---|---|---|
| Interface naming | as configured (nm-*) |
kernel-assigned utunN, mapped via /var/run/wireguard/<name>.name |
| DNS from the gateway | needs resolvconf |
applied natively by wg-quick |
| Machine name | hostname -s |
scutil --get LocalHostName, then ComputerName |
| Address lookup | ip -4 -o addr |
ifconfig |
Interface liveness is read from wg show interfaces, which behaves the same on both.
macOS can report a hostname like Unknown_aa:bb:cc:dd:ee:ff when no name is set. Those colons are not valid in a Netmaker client id, so the derived id is lowercased and stripped to [A-Za-z0-9._-] — that example becomes unknown_aabbccddeeff-cli, which is ugly but stable. Set NMRAC_CLIENT_ID if you want something friendlier.
Developed and tested against Netmaker server v1.5.1 (Pro, self-hosted).
Two API families that look like the right answer are deliberately unused:
/api/v1/rac/*exists on 1.5.1 and is correctly non-admin, but its authorization requires aTARGET_RSRCrequest header. Headers with underscores are dropped by common reverse-proxy defaults (nginx'sunderscores_in_headers off), so behind a proxy every call returns403 target rsrc is missing./api/users/{user}/remote_access_gwis wrapped inContinueIfUserMatchand needs no such header, sonmracuses that instead./api/v1/device/*is the newer desktop-client API, with a proxy-safeX-Host-IDheader and an explicit exemption from those header checks. It does not exist on 1.5.1 (404). On a newer server it would be the better path.
If you run a different version and something breaks, an issue with your server version and the failing response would be useful.
403 target rsrc is missing — see above; it means a request went to /api/v1/rac/*.
no gateway for '<network>' — your user is not attached to a remote-access gateway on that network. An admin attaches users to gateways; nmrac cannot.
--dns needs resolvconf — Linux only: apt install openresolv, or drop the flag and let DNS be skipped.
access denied on an extclient path — on v1.5.1 a client that does not exist answers 403 access denied rather than 404. nmrac treats any non-200 on the existence probe as "not present" and creates the client, so you should not see this; if you do, it is a genuine permission problem on the gateway.
Tunnel up but nothing reachable — check nmrac status for a recent handshake and non-zero transfer. If those look healthy, the crypto path is fine and you are looking at ACLs or filtering. Note that many gateways drop ICMP, so a failed ping proves nothing; try a real port.
MIT — see LICENSE.