brainstate

lixmal/caddy-netbird · source

Snapshot 0 · 2026-11-04T12:47:28Z · Go ·

Content fingerprint44f39b7b9b78095d725af5baa85448ca82f62bd07c773040f8ca2d48859b0210

GitHub description at capture

Caddy plugin that embeds a NetBird client for proxying HTTP and TCP/UDP traffic through NetBird networks

README at capture

# caddy-netbird

A [Caddy](https://caddyserver.com) plugin that embeds a [NetBird](https://netbird.io) 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](https://github.com/mholt/caddy-l4).

## Prerequisites

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

Build from source (includes L4 support):

```bash
go build -o caddy ./cmd/caddy
```

### xcaddy

The embedded NetBird client needs one `replace` directive, for a forked dependency of its Rosenpass support. Since xcaddy does not propagate replace directives from plugin modules, you must pass it explicitly:

```bash
xcaddy build \
    --with github.com/lixmal/caddy-netbird \
    --replace github.com/cloudflare/circl=codeberg.org/cunicu/circl@v0.0.0-20230801113412-fec58fc7b5f6
```

The value must match the `replace` in this repo's `go.mod`, which in turn mirrors NetBird's own `go.mod` for the pinned NetBird version.

## Container

```bash
podman 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:latest
```

The image runs as a non-root user. `NET_BIND_SERVICE` is required to bind ports 80 and 443.

## Caddyfile

See [examples/](examples/) for complete Caddyfile configurations. Below is a quick overview.

### Ingress (internet to NetBird)

Expose a NetBird peer to the public internet:

```caddyfile
{
    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
    }
}
```

### Egress (NetBird to external)

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:

```caddyfile
{
    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.

### Internal (NetBird to NetBird)

Reverse proxy between peers, without leaving the NetBird network. Caddy binds its listener on the NetBird interface (like egress) and dials the upstream through the tunnel (like ingress), so both the client and the backend are peers. Combine the `netbird/<node>` bind address with the `netbird` transport:

```caddyfile
{
    netbird {
        management_url https://api.netbird.io:443
        setup_key {$NB_SETUP_KEY}

        node internal {
            hostname caddy-internal
            wireguard_port 0
            block_inbound false
        }
    }
}

# HTTP: peers reach caddy-internal's NetBird IP on :8080
:8080 {
    bind netbird/internal
    reverse_proxy backend.netbird.cloud:8000 {
        transport netbird internal
    }
}
```

Peers connect to `<caddy-internal-ip>:8080`; Caddy proxies to `backend.netbird.cloud:8000` over the tunnel. As with egress, `block_inbound false` is required so peers can reach the listener. For raw TCP/UDP between peers, bind a `layer4` listener on `netbird/<node>` and route with the `netbird` handler. See [examples/internal.caddyfile](examples/internal.caddyfile).

For serving HTTPS on internal-only sites, the default ACME challenges cannot reach a NetBird-only listener. See [examples/internal-tls.caddyfile](examples/internal-tls.caddyfile) for certificate provisioning options: ACME DNS-01 (requires a DNS provider module in the build), Caddy's internal CA, or an existing internal PKI.

### Layer 4 (TCP/UDP)

Requires [caddy-l4](https://github.com/mholt/caddy-l4). The `layer4` block goes inside the global options.

```caddyfile
{
    layer4 {
        :2222 {
            route {
                netbird backend.netbird.cloud:22 ingress
            }
        }
    }
}
```

See [examples/](examples/) for more L4 configurations (UDP, SNI routing, mixed HTTP+L4).

## Admin API

The plugin registers endpoints on Caddy's [admin API](https://caddyserver.com/docs/api) (default: `localhost:2019`) for debugging and runtime control.

### Status

```bash
# 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  -        -              -        -          -
```

### Ping

Test connectivity to a host through the NetBird network:

```bash
# 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:

```json
{"reachable": true, "latency": 1234567}
```

The `node` field defaults to `"default"` if omitted. Latency is in nanoseconds.

### Log level

Change the NetBird client log level at runtime:

```bash
curl -X PUT localhost:2019/netbird/log-level -d '{"level": "debug"}'
```

Valid levels: `panic`, `fatal`, `error`, `warn`, `info`, `debug`, `trace`.

## Architecture

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.

### Client sharing

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.

### Global options

| Option | Description |
|--------|-------------|
| `management_url` | Default management server URL |
| `setup_key` | Default setup key for authentication |
| `log_level` | NetBird client log level (default: `info`) |

### Node options

| Option | Description |
|--------|-------------|
| `management_url` | Override app-level management URL |
| `setup_key` | Override app-level setup key |
| `jwt_token` | JWT-based authentication instead of a setup key |
| `private_key` | Direct private key authentication instead of a setup key |
| `hostname` | Device name in the NetBird network (default: `caddy-<node>`) |
| `pre_shared_key` | Pre-shared key for the network interface |
| `log_level` | Log level for this client (e.g. `debug`, `info`, `warn`) |
| `config_path` | Path to the NetBird config file (in-memory if unset) |
| `state_path` | Path to the NetBird state file |
| `wireguard_port` | Port for the network interface (default: 51820 via NetBird) |
| `mtu` | MTU for the network interface (valid range 576..8192) |
| `no_userspace` | Disable userspace networking mode (`true`/`false`, needs root) |
| `disable_client_routes` | Disable client routes (`true`/`false`) |
| `disable_ipv6` | Disable IPv6 overlay addressing (`true`/`false`) |
| `block_inbound` | Block inbound connections from peers (default: `true`). Set to `false` for egress nodes |
| `block_lan_access` | Block the peer from reaching the host's LAN when used as a routing peer (`true`/`false`) |
| `dns_labels` | Additional DNS labels for the peer (space-separated list) |
| `preallocated_buffers_per_pool` | Cap the per-tunnel buffer pool (process-global tuning) |
| `max_batch_size` | Packets read/written per syscall (process-global tuning) |

> **Authentication:** one of `setup_key` (node or app level), `jwt_token`, or `private_key` is required per node.

> **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.

### Multiple nodes

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:

```caddyfile
{
    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:

```caddyfile
{
    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_port` values to avoid conflicts.

### Upstream TLS

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 |