From c2198f9956be8b39e51593a2682be9082496cc3a Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Mon, 12 Oct 2026 14:49:04 +0200 Subject: [PATCH] Document nblink (#1013) * Document nblink nblink forwards local ports into a NetBird network from an unprivileged process, so it covers the cases where the agent cannot be installed: managed laptops without administrator rights, rootless containers, and short-lived CI jobs. Covers the download, the forward syntax, the container environment variables, and the access control and userspace limits that decide whether it fits. * Add use cases to the nblink page The page explained what nblink does and how to run it, but not when to reach for it, which is the first thing a reader needs to decide. Covers the three cases it fits best: an MCP server or local AI agent calling an internal API, Agent Network access from a machine where the client cannot be installed, and a sidecar for an application that ignores proxy settings. * Note what the mounted nblink setup key requires The container example referenced a secret path it never mounted, so it could not have worked as written. The mount also needs calling out, because the container runs as an arbitrary UID while a bind mount keeps the host file's owner and mode, and a key written with the usual private mode is unreadable to it. * Document the nblink Host and Origin checks A loopback forward answers only where Host names the loopback interface and refuses a cross-site Origin, because a page the reader visits can point its own hostname at 127.0.0.1 and would otherwise reach the upstream through the listener under their identity. * Describe the nblink cross-site check and its residual The page described only the Host and Origin checks. A page can also embed the loopback address directly, where Sec-Fetch-Site is the signal that stops it, and a browser too old to send that header still gets through. * Publish the nblink container port on loopback only The container example published port 8080 on every host interface while the forward inside listened on 0.0.0.0 with public bind enabled, so anything that could reach the Docker host could use the forward under the peer's identity. The paragraph beneath it justified that by saying the container boundary restricts access, which it does not once the port is published. The example now publishes on 127.0.0.1, with the reason the listener still has to bind 0.0.0.0 inside the container. The same section had drifted from the binary in two other places. The key-file note described what -v does with a missing source path, while the example uses --mount, which refuses to start. And a public forward now answers 421 to a name it was not told about, which the page never mentioned: --allowed-host is added to the container section, the configuration table and the access-control section, including that a listed name is trusted and applies to every forward. --- src/components/NavigationDocs.jsx | 1 + src/pages/client/nblink.mdx | 247 ++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 src/pages/client/nblink.mdx diff --git a/src/components/NavigationDocs.jsx b/src/components/NavigationDocs.jsx index 5a70bc3e..1b87ec11 100644 --- a/src/components/NavigationDocs.jsx +++ b/src/components/NavigationDocs.jsx @@ -904,6 +904,7 @@ export const docsNavigation = [ title: 'CLIENT', links: [ { title: 'Desktop App', href: '/client/desktop-app' }, + { title: 'nblink', href: '/client/nblink' }, { title: 'Profiles', href: '/client/profiles' }, { title: 'gRPC Daemon Socket', href: '/client/grpc-socket' }, { title: 'HTTP/JSON Daemon Socket', href: '/client/json-socket' }, diff --git a/src/pages/client/nblink.mdx b/src/pages/client/nblink.mdx new file mode 100644 index 00000000..00e85764 --- /dev/null +++ b/src/pages/client/nblink.mdx @@ -0,0 +1,247 @@ +import { Note, Warning } from '@/components/mdx' + +export const description = + 'Forward local ports into your NetBird network from an unprivileged process, without installing the agent or granting root.' + +# nblink + +`nblink` forwards local ports into your NetBird network from an ordinary, +unprivileged process. It runs the client in userspace mode, so there is no TUN +device to create and no root to grant. + +That makes it useful where the regular agent cannot run: + +- Managed laptops where you cannot install software that needs administrator rights. +- Rootless containers, including OpenShift and Podman. +- Short-lived environments such as CI jobs, where installing an agent is not worth it. + +Names on the upstream side of a forward are resolved inside the tunnel, so +`/etc/resolv.conf` is never modified. + + + `nblink` forwards HTTP. Support for raw TCP, UDP and SOCKS5 is planned, and + the way a forward is written will not change when they arrive. + + +## Use cases + +### An MCP server or local AI agent reaching an internal API + +AI agents increasingly run in containers or restricted sandboxes where +installing a client is not an option, and the consumer is code rather than a +person, so opening a browser is not a substitute. + +```shell +nblink --forward 'http://3000=https://tickets.internal' --state-dir ~/.nblink +``` + +Point the MCP server at `http://localhost:3000`. The agent carries a real peer +identity, so what it can reach is decided by your policies rather than by +whatever the agent was told to do. + +### Agent Network access without installing a client + +An [Agent Network](/agent-network) endpoint only answers when you are connected +to your network and authenticated through your IdP, which normally means +installing the client and therefore having root. + +```shell +nblink --forward 'http://8080=https://mirror.netbird.ai' +``` + +Set the agent's base URL to `http://localhost:8080`. It still gets identity, +spending limits and the models you approved, and no API key is handed to the +agent itself. + +### A sidecar for an app you cannot modify + +A vendor or legacy application hardcodes an HTTP endpoint and ignores proxy +settings, so pointing it somewhere else is not possible. Running `nblink` +beside it gives it `localhost:8080` without touching the application, on +platforms that forbid privileged containers and assign an arbitrary UID at run +time. + +## Install + +Download the archive for your platform from the +[releases page](https://github.com/netbirdio/netbird/releases), extract it, and +run the binary. There is no installer and no service to register. + +```shell +tar -xzf nblink__linux_amd64.tar.gz +./nblink --help +``` + +## Forward a port + +A forward is a single argument, and the flag repeats: + +```shell +nblink --forward 'http://8080=https://grafana.internal' +``` + +The scheme on the left selects the listener type. The right side is the +upstream reached over your network. Giving only a port binds loopback, so the +command above listens on `127.0.0.1:8080`. + +With no setup key, `nblink` opens your browser to log in. It always prints the +login URL as well, so the same command works over SSH. + +Forward several services from one session and one login: + +```shell +nblink --forward 'http://8080=https://grafana.internal' \ + --forward 'http://9090=https://prometheus.internal' +``` + +Use `0` as the port to let the operating system pick a free one. The address +actually bound is printed when the forward starts. + +Check a configuration without connecting to anything: + +```shell +nblink --forward 'http://8080=https://grafana.internal' --check +``` + +## Run without a browser + +Pass a [setup key](/manage/peers/register-machines-using-setup-keys) to log in +without any interaction: + +```shell +nblink --forward 'http://8080=https://grafana.internal' --setup-key "$NB_SETUP_KEY" +``` + +## Run in a container + +Every flag has a matching `NB_`-prefixed environment variable, which is how the +image is configured. `NB_FORWARD` accepts a comma separated list. + +```shell +docker run --rm -p 127.0.0.1:8080:8080 \ + --mount type=bind,src="$(pwd)/nb_setup_key",dst=/run/secrets/nb_setup_key,readonly \ + -e NB_SETUP_KEY=file:/run/secrets/nb_setup_key \ + -e NB_FORWARD='http://0.0.0.0:8080=https://grafana.internal' \ + -e NB_ALLOW_PUBLIC_BIND=true \ + netbirdio/nblink +``` + +The image needs no `--privileged`, no `--cap-add NET_ADMIN`, no +`--device /dev/net/tun` and no host networking. It runs as a non-root user and +works on platforms that assign an arbitrary UID at run time. + +Inside a container the listener has to bind `0.0.0.0`, because a published +port arrives on the container's own interface rather than its loopback. That +still requires `NB_ALLOW_PUBLIC_BIND`, so exposing a forward is always +something you wrote down. + + + Publish the port on the host's loopback, as above. A bare `-p 8080:8080` + publishes it on every host interface, and anything that can reach the host + can then use the forward under the peer's identity. Publish on another + address only when the network around the host restricts who can reach it. + + +A forward on a public address accepts requests addressed to `localhost` or to +an IP address. A name that other machines use to reach it, such as a DNS name +or a Kubernetes Service name, has to be listed with `--allowed-host` +(`NB_ALLOWED_HOST`); an unlisted name is answered with `421`. See +[Access control](#access-control) for what listing a name trusts. + + + The key file must exist before the run and be readable by the container user, + UID 65532 in group 0. A bind mount keeps the host file's owner and mode, so a + root-owned key written with the usual `chmod 600` is not readable and startup + fails with `permission denied`. Either give the file to that UID + (`chown 65532`) or keep it mode `640` in group 0 (`chgrp 0`). If the source + path does not exist, `--mount` refuses to start the container; the older `-v` + form creates a directory there instead. + + +The image keeps its state in memory, so it runs on a read-only root filesystem +and each start registers a new peer. To keep one identity across restarts, set +`NB_STATE_DIR` and mount a writable volume there: + +```shell +docker run --rm --read-only -p 127.0.0.1:8080:8080 \ + -v nblink-state:/var/lib/nblink -e NB_STATE_DIR=/var/lib/nblink \ + ... +``` + +Without a state directory, use an ephemeral setup key so the peers left behind +by earlier starts are removed once they go offline. + +A kept identity is not yet a kept login. Without a setup key every start runs +the browser flow, even when the peer's session is still valid. Give a restart +that has to be unattended a setup key. + + + A value given as `file:/path` is read from that file. Prefer it for setup + keys in containers, so the secret is not visible to anything that can read + the process environment. + + +## Configuration + +| Flag | Environment variable | Default | Description | +| --- | --- | --- | --- | +| `--forward` | `NB_FORWARD` | | Forward spec, repeatable. Comma separated in the environment. | +| `--setup-key` | `NB_SETUP_KEY` | | Log in without a browser. Accepts a `file:` prefix. | +| `--management-url` | `NB_MANAGEMENT_URL` | NetBird cloud | Management server URL. | +| `--hostname` | `NB_HOSTNAME` | host name | Peer name in your network. | +| `--state-dir` | `NB_STATE_DIR` | memory | Directory for identity and state. | +| `--log-level` | `NB_LOG_LEVEL` | `info` | Log level. | +| `--allow-public-bind` | `NB_ALLOW_PUBLIC_BIND` | `false` | Required to bind an address other than loopback. | +| `--allowed-host` | `NB_ALLOWED_HOST` | | Name a request may arrive under, beyond loopback names and addresses. Repeatable. Comma separated in the environment. | +| `--no-browser` | `NB_NO_BROWSER` | `false` | Print the login URL instead of opening a browser. | +| `--check` | `NB_CHECK` | `false` | Validate the configuration, print the forwards and exit. | + +## Access control + +A peer created by `nblink` is a peer like any other. It appears in your peer +list and reaches only what your +[access control policies](/manage/access-control) allow. + +`nblink` only dials out. It refuses inbound connections and does not reach the +network of the machine it runs on, so the peer cannot be used as a way into +that machine. + +A loopback forward answers only requests whose `Host` names the loopback +interface or a name listed with `--allowed-host`, and refuses any request a +browser marks as belonging to another page, through either a cross-origin +`Origin` or a cross-site `Sec-Fetch-Site`. +A web page you visit can point its own hostname at `127.0.0.1` or embed the +address directly, and without those checks your browser could reach the +upstream through the listener under your identity. Ordinary callers send +neither header and are unaffected. + +A browser too old to send `Sec-Fetch-Site`, which browsers have sent since +2020, can still reach a loopback forward with an embedded no-cors GET. + +A forward on a public address applies the same checks and also accepts any IP +address in `Host`, since a page cannot rebind an address it does not control. + +A name listed with `--allowed-host` is trusted rather than merely permitted, +and the list applies to every forward in the process, loopback ones included. +`Host` is the one signal a rebound page cannot forge, and listing a name gives +that up for it: a page served on that name can resolve it to the listener and +send a matching `Host` and `Origin`. List only names you control, and prefer a +fully qualified one, since a single-label name can be answered by anything on +the local network. + + + Forwards bind `127.0.0.1` unless you pass `--allow-public-bind`. A forward on + a public address lets anything that can reach the port use your network, so + enable it only when the surrounding environment restricts access. + + +## Limits + +Userspace mode carries TCP, UDP and ping only. An `nblink` peer cannot: + +- act as a [routing peer](/manage/networks/how-routing-peers-work) or an exit node, +- take over system DNS for the machine, +- bind privileged ports below 1024. + +If you need any of those, install the +[NetBird client](/get-started/install) instead.