A Caddy plugin that embeds a NetBird client, allowing Caddy to proxy traffic through a NetBird network. Supports HTTP reverse proxying (Layer 7) and raw TCP/UDP proxying (Layer 4) via caddy-l4.
This plugin only handles the Caddy side. It joins the NetBird network as a peer using the provided setup key and dials upstream addresses through the tunnel. All NetBird management configuration must be done beforehand:
- The setup key must be created in the NetBird management dashboard.
- Access control policies must allow traffic from the Caddy peer to the upstream peers/services.
- Networks (routes) must be configured if the upstream is behind a routed network (e.g.
192.168.1.0/24). - The upstream peers must be online and reachable within the NetBird network.
This plugin does not create, modify, or manage any NetBird resources. It is a consumer of the network, not an administrator.
Build from source (includes L4 support):
go build -o caddy ./cmd/caddyNetBird uses forked dependencies that require replace directives. Since xcaddy does not propagate replace directives from plugin modules, you must pass them explicitly:
xcaddy build \
--with github.com/lixmal/caddy-netbird \
--replace github.com/cloudflare/circl=github.com/cunicu/circl@v0.0.0-20230801113412-fec58fc7b5f6 \
--replace github.com/dexidp/dex=github.com/netbirdio/dex@v0.244.0 \
--replace github.com/getlantern/systray=github.com/netbirdio/systray@v0.0.0-20231030152038-ef1ed2a27949 \
--replace github.com/kardianos/service=github.com/netbirdio/service@v0.0.0-20240911161631-f62744f42502 \
--replace github.com/libp2p/go-netroute=github.com/netbirdio/go-netroute@v0.0.0-20240611143515-f59b0e1d3944 \
--replace github.com/pion/ice/v4=github.com/netbirdio/ice/v4@v4.0.0-20250908184934-6202be846b51 \
--replace golang.zx2c4.com/wireguard=github.com/netbirdio/wireguard-go@v0.0.0-20260107100953-33b7c9d03db0podman run -d \
--cap-add NET_BIND_SERVICE \
-v /path/to/Caddyfile:/config/Caddyfile:ro \
-p 80:80 -p 443:443 \
ghcr.io/lixmal/caddy-netbird:latestThe image runs as a non-root user. NET_BIND_SERVICE is required to bind ports 80 and 443.
See examples/ for complete Caddyfile configurations. Below is a quick overview.
Expose a NetBird peer to the public internet:
{
netbird {
management_url https://api.netbird.io:443
setup_key {$NB_SETUP_KEY}
node ingress {
hostname caddy-ingress
}
}
}
app.example.com {
reverse_proxy backend.netbird.cloud:8080 {
transport netbird ingress
}
}Make external services available to NetBird peers. Uses netbird/<node>:<port> (TCP) or netbird-udp/<node>:<port> (UDP) listener address formats to bind on the NetBird virtual interface:
{
netbird {
management_url https://api.netbird.io:443
setup_key {$NB_SETUP_KEY}
node egress {
hostname caddy-egress
wireguard_port 0
block_inbound false
}
}
layer4 {
# Forward TCP port to an external service
netbird/egress:9080 {
route {
proxy external-host:8080
}
}
# SOCKS5 proxy for dynamic destinations
netbird/egress:1080 {
route {
socks5
}
}
# Forward UDP to an internal DNS server
netbird-udp/egress:5353 {
route {
proxy udp/dns-server.internal:53
}
}
}
}NetBird peers can then connect to <caddy-egress-ip>:9080 for the forwarded service, or use <caddy-egress-ip>:1080 as a SOCKS5 proxy.
The block_inbound false option is required for egress nodes: it allows incoming connections from NetBird peers to reach the listener.
Requires caddy-l4. The layer4 block goes inside the global options.
{
layer4 {
:2222 {
route {
netbird backend.netbird.cloud:22 ingress
}
}
}
}See examples/ for more L4 configurations (UDP, SNI routing, mixed HTTP+L4).
The plugin registers endpoints on Caddy's admin API (default: localhost:2019) for debugging and runtime control.
# Human-readable output (default)
curl localhost:2019/netbird/status
# JSON output
curl 'localhost:2019/netbird/status?format=json'Example text output:
Node: ingress
NetBird IP: 100.0.50.187/16
FQDN: caddy-ingress.netbird.cloud
Management: https://api.netbird.io:443 Connected
Signal: https://signal.netbird.io Connected
Relay: rel://relay.netbird.io Available
Peers (3):
FQDN IP Status Latency Transfer Conn Handshake Routes
---- -- ------ ------- -------- ---- --------- ------
backend.netbird.cloud 100.0.1.10 Connected 1.2ms 1.5 KiB/2 KiB P2P 13s ago 192.168.1.0/24
api-backend.netbird.cloud 100.0.1.20 Connected 3.4ms 800 B/1.2 KiB Relayed 45s ago -
web-backend.netbird.cloud 100.0.1.30 Connecting - - - - -
Test connectivity to a host through the NetBird network:
# TCP ping (default)
curl -X POST localhost:2019/netbird/ping \
-d '{"node": "ingress", "address": "backend.netbird.cloud:8080"}'
# ICMP ping
curl -X POST localhost:2019/netbird/ping \
-d '{"node": "ingress", "address": "backend.netbird.cloud", "network": "ping"}'
# UDP ping
curl -X POST localhost:2019/netbird/ping \
-d '{"node": "ingress", "address": "dns-server.netbird.cloud:53", "network": "udp"}'Response:
{"reachable": true, "latency": 1234567}The node field defaults to "default" if omitted. Latency is in nanoseconds.
Change the NetBird client log level at runtime:
curl -X PUT localhost:2019/netbird/log-level -d '{"level": "debug"}'Valid levels: panic, fatal, error, warn, info, debug, trace.
The plugin registers five Caddy modules:
| Module | Caddy ID | Purpose |
|---|---|---|
App |
netbird |
Manages NetBird client lifecycle, config, and usage pool |
Transport |
http.reverse_proxy.transport.netbird |
Dials HTTP upstreams through the NetBird network |
Handler |
layer4.handlers.netbird |
Proxies raw TCP/UDP through the NetBird network (requires caddy-l4) |
Listener |
netbird (network) |
Binds listeners on the NetBird virtual interface for egress |
Admin API |
admin.api.netbird |
Exposes status, ping, and log level endpoints on the admin API |
The embedded NetBird client (embed.Client) runs entirely in userspace without requiring a TUN device or root privileges. Upstream traffic is dialed through the tunnel while Caddy handles TLS termination, load balancing, health checks, retries, and all other reverse proxy features.
Multiple sites can share the same NetBird client by referencing the same node name. Clients are ref-counted via caddy.UsagePool and survive config reloads without reconnecting.
| Option | Description |
|---|---|
management_url |
Default management server URL |
setup_key |
Default setup key for authentication |
log_level |
NetBird client log level (default: info) |
| Option | Description |
|---|---|
management_url |
Override app-level management URL |
setup_key |
Override app-level setup key |
hostname |
Device name in the NetBird network (default: caddy-<node>) |
pre_shared_key |
Pre-shared key for the network interface |
wireguard_port |
Port for the network interface (default: 51820 via NetBird) |
block_inbound |
Block inbound connections from peers (default: true). Set to false for egress nodes |
Note on
wireguard_port: For reliable peer-to-peer connectivity, the configured port (or the default random port) should be exposed via port forwarding on the host's firewall/NAT. Without it, connections may fall back to relayed traffic which adds latency.
Each node creates a separate NetBird peer identity. This is useful when connecting to different networks or management servers from a single Caddy instance.
Different management servers. No shared defaults needed, each node specifies its own management URL and setup key:
{
netbird {
node corp {
management_url https://netbird.corp.example.com:443
setup_key {$NB_CORP_KEY}
hostname caddy-corp
wireguard_port 0
}
node staging {
management_url https://netbird.staging.example.com:443
setup_key {$NB_STAGING_KEY}
hostname caddy-staging
wireguard_port 0
}
}
}
corp-app.example.com {
reverse_proxy backend.corp.internal:8080 {
transport netbird corp
}
}
staging-app.example.com {
reverse_proxy backend.staging.internal:8080 {
transport netbird staging
}
}Same management, different setup keys. Useful for separate peer identities with different access policies on the same network:
{
netbird {
management_url https://api.netbird.io:443
node web {
setup_key {$NB_WEB_KEY}
hostname caddy-web
wireguard_port 0
}
node api {
setup_key {$NB_API_KEY}
hostname caddy-api
wireguard_port 0
}
}
}
web.example.com {
reverse_proxy web-backend.netbird.cloud:8080 {
transport netbird web
}
}
api.example.com {
reverse_proxy api-backend.netbird.cloud:3000 {
transport netbird api
}
}Note: Each node binds its own network interface port. When running multiple nodes, set distinct
wireguard_portvalues to avoid conflicts.
The NetBird network encryption and upstream TLS are independent concerns. The upstream behind the tunnel may be a plain HTTP service on a peer, or it could be an HTTPS endpoint reached via a NetBird route to an external network.
TLS to the upstream is automatically enabled when using https:// upstream addresses. You can also configure it explicitly in the transport block:
| Option | Description |
|---|---|
tls |
Enable TLS to upstream with default settings |
tls_insecure_skip_verify |
Skip TLS certificate verification (testing only) |
tls_server_name |
Override the server name for TLS verification |