开源VPN-记一次对Netbird的纯手动部署

不知是因为什么原因,在一次版本更新后,Netbird的部分网络功能存在异常。原以为是官方Bug,但经过不屑的努力,最终发现问题出在我自己的服务器内部,备份数据后尝试重新部署…

以我个人真实的业务场景为部署依据,详细介绍部署步骤。

Netbird VPN的构成

Netbird VPN 在架构上主要包括以下几个部分:

  • Management:Netbird的管理后端
  • Signal:节点间交换密钥的信号服务
  • Relay/Turn:用于在无法进行P2P通讯时提供的中继服务
  • Stun:查询节点的公网IP,是检查P2P通讯的前提条件

部署过程

为了完成此次部署,需要一些前提条件:

  1. 拥有一个公网级别可用的域名 example.com
  2. 拥有一台在公网下可访问的服务器 111.123.164.210 240e:433:2103:2e::db
  3. 由于服务是用Docker部署的,因此创建了一个专属的内网netbird-network,网卡配置为172.10.0.0/16
  4. 在服务器内部准备 NginxPostgreSQL,假设PostgreSQLnetbird-network网络内,地址为172.10.0.100

上述域名与IP均为示例值,请在参考时更换为真实域名和IP

身份认证服务

Netbird VPN 自部署依赖第三方认证服务,自身并没有用户管理能力,因此需要部署一个 IdP (身份提供商) ,这里使用 ZITADEL 进行讲解。

首先,部署一个域名为sso.example.com的IdP,本人使用Docker Compose进行部署,源码如下:

networks:
  netbird-network:
    external: true
services:
  zitadel:
    restart: 'always'
    networks: 
      netbird-network:
        ipv4_address: 172.10.0.11
    image: 'ghcr.io/zitadel/zitadel:v2.64.6'
    command: 'start-from-init --masterkey "<密钥,自行生成即可>" --tlsMode external'
    environment:
      ZITADEL_DATABASE_POSTGRES_HOST: 172.10.0.100
      ZITADEL_DATABASE_POSTGRES_PORT: 5432
      ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_USERNAME: <账号>
      ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: <密码>
      ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
      ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: <账号>
      ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: <密码>
      ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
      ZITADEL_EXTERNALSECURE: false
      ZITADEL_EXTERNALDOMAIN: sso.example.com
      ZITADEL_EXTERNALPORT: 443
      # 默认账号
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: <自行定义>
      # 默认密码
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: <自行定义>
      # SMTP
      ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST: <自行定义>
      ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_USER: <自行定义>
      ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_PASSWORD: <自行定义>
      ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_TLS: true
      ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROM: <自行定义>
      ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROMNAME: noreply@sso.example.com
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

出口这边我统一使用Nginx进行,便于同时开启V4和V6访问,主要配置如下:

location ^~ / {
    proxy_pass http://172.10.0.11:8080; 
    proxy_set_header Host $host; 
    proxy_set_header X-Real-IP $remote_addr; 
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header REMOTE-HOST $remote_addr; 
    proxy_set_header Upgrade $http_upgrade; 
    proxy_set_header Connection $http_connection; 
    proxy_set_header X-Forwarded-Proto $scheme; 
    proxy_set_header X-Forwarded-Port $server_port; 
    proxy_http_version 1.1; 
    add_header X-Cache $upstream_cache_status; 
    add_header Cache-Control no-cache; 
    proxy_ssl_server_name off; 
    proxy_ssl_name $proxy_host; 
    add_header Strict-Transport-Security "max-age=31536000"; 
}

接下来的操作就是页面上的操作了,首先通过访问https://sso.example.com登录默认的账号密码,进入IdP系统。

⚠️注意:首次登录后页面是纯英文,如果你看不懂,点击右上角的头像图标,选择【编辑账户】。在【全局】设置里修改【Language】为中文保存即可。

首先选择右上角头像旁边的【默认设置】,选择左侧菜单栏里的【登录行为和安全】,在这里进行最初的安全配置。

⚠️注意:【登录和访问】这块设置真的很有必要!!!

完成后,进入页面最下边的【其他配置】,在安全配置中勾选【允许 iFrame】,并在下面追加需要跨域访问的域名地址。

‼️重要:假设 Netbird VPN 的部署域名为netbird.example.com,这里就需要添加https://netbird.example.com。如果不做这一步,后续部署完成后会出现跨域等问题

完成后选择右上角头像旁边的【组织配置】,在【组织】中的【登录和访问】卡片中点击【修改】按钮,进入组织设置。在左侧菜单中点击【已验证的域名】,进入已验证的域名页面,将需要的域名加入。

‼️重要:根据上述假设信息,这里需要加入两个域名。sso.example.com netbird.example.com


接下来要开始配置项目了。一般情况下,系统会自动创建一个名为 ZITADEL 的项目,这里我们创建一个新的项目。假定项目名为 Self-Hosting ,创建完成后会自动进入创建好的项目。

创建应用

在【应用】栏那里点击【创建】,进入创建选项卡。名称为Netbird VPN,类型选择User Agent,点击【继续】;身份验证方式选择推荐的【PKCE】,点击【继续】;重定向地址这里开启【开发模式】,并在【重定向Urls】中添加如下内容:

  • https://netbird.example.com/nb-auth
  • https://netbird.example.com/nb-silent-auth
  • http://localhost:53000/
  • http://localhost:54000/

而后,在【退出登录重定向 URLs】中添加 https://netbird.example.com/ 即可完成创建。

‼️重要:请记住生成的Client ID,后续会使用到


接下来,进入创建好的应用,在左侧菜单栏的【配置】中,修改【OIDC 配置】的【授权类型】为:

  • Authorization Code
  • Device Code
  • Refresh Token

并勾选,右侧的【Refresh Token】选项,点击保存。


点击右侧菜单的【令牌设置】,修改【身份验证令牌类型】为JWT,并勾选【将用户角色添加到访问令牌】。

配置Service User

在创建好的项目中选择【用户】选项卡,在【服务用户】中创建一个名为 netbird-service-account的服务用户。

‼️重要:请记住登录密钥,后续会使用到

完成后返回【组织】选项卡,点击【操作】按钮旁的【+】按钮,将创建好的服务用户以\nOrg User Manager的身份加入管理者清单中。

Netbird VPN

🎉恭喜各位坚持到了这里。我们继续进行部署,按照其依赖关系,我们按照如下顺序对每一个服务依次进行部署:

  1. Stun/Turn
  2. Relay (可忽略)
  3. Signal
  4. Management/Dashboard

Coturn

以下提供Docker Compose部署代码:

networks:
    netbird-network:
        external: true
services:
  coturn:
    image: coturn/coturn:latest
    restart: unless-stopped
    network_mode: host
    volumes:
      - /opt/certs:/certs #存放证书
      - /opt/coturn/turnserver.conf:/etc/coturn/turnserver.conf #存放配置文件
      - coturn_data:/var/lib/coturn
volumes:
  coturn_data:

turnserver.conf配置如下:

为了便于管理,这里也提供一个域名映射服务,假设为`stun.example.com`

# Coturn TURN SERVER configuration file

server-name=stun.example.com
listening-device=<网卡>
listening-port=3478
tls-listening-port=5349
cert=/certs/fullchain.pem
pkey=/certs/privkey.pem

# Listening IP addresses
listening-ip=111.123.164.210
listening-ip=240e:433:2103:2e::db

# External IP addresses
external-ip=111.123.164.210

# Relay configuration
relay-device=<网卡>
relay-ip=111.123.164.210
relay-ip=240e:433:2103:2e::db
relay-threads=4

# Port range for relay endpoints
min-port=49152
max-port=65535

# cli
no-cli
cli-ip=127.0.0.1
cli-port=5766
cli-password=<密钥,自主生成即可>

# Security and logging options
#no-udp
#no-tcp
#no-tls
#no-dtls
#no-udp-relay
#no-tcp-relay
no-stdout-log
no-rfc5780
response-origin-only-with-rfc5780

# Authentication and quotas
realm=stun.example.com
fingerprint
lt-cred-mech
user=netbird:<密钥,自主生成即可>
user-quota=1000  # 增加用户配额
total-quota=10000  # 增加总配额

# Bandwidth and lifetime settings
max-bps=10485760  # 单个会话最大 1MB/s 带宽
bps-capacity=1073741824  # 服务器总带宽上限 2GB/s
stale-nonce=600
max-allocate-lifetime=3600
channel-lifetime=600
permission-lifetime=300

# Address family settings
keep-address-family

Netbird Services

为了方便管理,我将剩下的内容放在同一个Docker Compose中进行管理:

networks:
  netbird-network:
    external: true
services:
  relay:
    image: netbirdio/relay:latest
    restart: unless-stopped
    env_file:
      - /opt/netbird/relay.env
    networks: 
      netbird-network:
        ipv4_address: 172.10.0.12
  signal:
    image: netbirdio/signal:latest
    restart: unless-stopped
    networks: 
      netbird-network:
        ipv4_address: 172.10.0.13
    # entrypoint: [ "/go/bin/netbird-signal","run" ]
    # command: ["--log-file", "console", "--log-level", "DEBUG"]
  dashboard:
    image: netbirdio/dashboard:latest
    restart: unless-stopped
    networks: 
      netbird-network:
        ipv4_address: 172.10.0.14
    env_file:
      - /opt/netbird/dashboard.env
  management:
    image: netbirdio/management:latest
    restart: unless-stopped
    networks: 
      netbird-network:
        ipv4_address: 172.10.0.15
    volumes:
      - /opt/netbird:/var/lib/netbird
      - /opt/netbird/management.json:/etc/netbird/management.json
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    command: [
      "--port", "80",
      "--log-file", "console",
      "--log-level", "info",
      "--disable-anonymous-metrics=true",
      "--single-account-mode-domain=netbird.local",
      "--dns-domain=netbird.local",
      "--idp-sign-key-refresh-enabled",
    ]

内部依赖的环境变量文件内容如下:

# ####
# dashboard.env
# ####
NETBIRD_MGMT_API_ENDPOINT=https://netbird.example.com
NETBIRD_MGMT_GRPC_API_ENDPOINT=https://netbird.example.com
# OIDC
# $NETBIRD_AUTH_CLIENT_ID 是创建应用是得到的Client ID
AUTH_AUDIENCE=<$NETBIRD_AUTH_CLIENT_ID>
AUTH_CLIENT_ID=<$NETBIRD_AUTH_CLIENT_ID>
AUTH_AUTHORITY=https://sso.example.com
USE_AUTH0=false
AUTH_SUPPORTED_SCOPES="openid profile email offline_access"
AUTH_REDIRECT_URI=/nb-auth
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
# SSL
NGINX_SSL_PORT=443
# Letsencrypt
LETSENCRYPT_DOMAIN=none

# ####
# relay.env
# ####

NB_LOG_LEVEL=info
NB_LISTEN_ADDRESS=:33080
NB_QUIC_LISTEN_ADDRESS=:33080
NB_EXPOSED_ADDRESS=rels://relay.example.com:443
NB_AUTH_SECRET=<Relay服务密钥,自主定义>

{
    "Stuns": [
        {
            "Proto": "udp",
            "URI": "stuns:stun.example.com:5349"
        }
    ],
    "TURNConfig": {
        "Turns": [
            {
                "Proto": "udp",
                "URI": "turns:stun.example.com:5349",
                "Username": "netbird",
                "Password": "<turn的用户连接密钥>"
            }
        ],
        "TimeBasedCredentials": false,
        "CredentialsTTL": "1h",
        "Secret": "<turn的认证密钥>"
    },
    "Relay": {
        "Addresses": ["rels://relay.example.com:443"],
        "CredentialsTTL": "24h",
        "Secret": "<Relay服务密钥>"
    },
    "Signal": {
        "Proto": "https",
        "URI": "signal.example.com:443"
    },
    "HttpConfig": {
        "AuthIssuer": "https://netbird.example.com",
        "AuthAudience": "<$NETBIRD_AUTH_CLIENT_ID>",
        "OIDCConfigEndpoint":"https://sso.example.com/.well-known/openid-configuration"
    },
    "IdpManagerConfig": {
        "ManagerType": "zitadel",
        "ClientConfig": {
            "Issuer": "https://netbird.example.com",
            "TokenEndpoint": "https://sso.example.com/oauth/v2/token",
            "ClientID": "<$NETBIRD_IDP_MGMT_CLIENT_ID>",
            "ClientSecret": "<$NETBIRD_IDP_MGMT_CLIENT_SECRET>",
            "GrantType": "client_credentials"
        },
        "ExtraConfig": {
            "ManagementEndpoint": "https://netbird.example.com/management/v1"
        }
    },
  "DeviceAuthorizationFlow": {
      "Provider": "hosted",
      "ProviderConfig": {
          "Audience": "<$NETBIRD_AUTH_CLIENT_ID>",
          "ClientID": "<$NETBIRD_AUTH_CLIENT_ID>",
          "Scope": "openid"
      }
    },
    "PKCEAuthorizationFlow": {
        "ProviderConfig": {
            "Audience": "<$NETBIRD_AUTH_CLIENT_ID>",
            "ClientID": "<$NETBIRD_AUTH_CLIENT_ID>",
            "Scope": "openid profile email offline_access",
            "RedirectURLs": ["http://localhost:53000/","http://localhost:54000/"]
        }
    }
}

$NETBIRD_AUTH_CLIENT_ID 是创建应用是得到的Client ID

$NETBIRD_IDP_MGMT_CLIENT_ID 是创建的服务用户账号

$NETBIRD_IDP_MGMT_CLIENT_SECRET 是创建的服务用户密钥


剩下的则是Nginx相关的关键配置:

# signal.example.com
location /signalexchange.SignalExchange/ {
    grpc_pass grpc://172.10.0.13:10000; 
    #grpc_ssl_verify off;
    grpc_read_timeout 1d; 
    grpc_send_timeout 1d; 
    grpc_socket_keepalive on;
}

# relay.example.com
location ^~ / {
    proxy_pass http://172.10.0.12:33080; 
    # WebSocket support
    proxy_http_version 1.1; 
    proxy_set_header Upgrade $http_upgrade; 
    proxy_set_header Connection "Upgrade"; 
    # Forward headers
    proxy_set_header Host $host; 
    proxy_set_header X-Real-IP $remote_addr; 
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header X-Forwarded-Proto $scheme; 
    # Timeout settings
    proxy_read_timeout 3600s; 
    proxy_send_timeout 3600s; 
    proxy_connect_timeout 60s; 
    # Handle upstream errors
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; 
    add_header Strict-Transport-Security "max-age=31536000"; 
}

# netbird.example.com

location / {
    proxy_pass http://172.10.0.14; 
    proxy_set_header Host $host; 
    proxy_set_header X-Real-IP $remote_addr; 
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header REMOTE-HOST $remote_addr; 
    proxy_set_header Upgrade $http_upgrade; 
    proxy_set_header Connection $http_connection; 
    proxy_set_header X-Forwarded-Proto $scheme; 
    proxy_set_header X-Forwarded-Port $server_port; 
    proxy_http_version 1.1; 
    add_header X-Cache $upstream_cache_status; 
    add_header Cache-Control no-cache; 
    proxy_ssl_server_name off; 
    proxy_ssl_name $proxy_host; 
    add_header Strict-Transport-Security "max-age=31536000"; 
}

location /api {
    proxy_pass http://172.10.0.15; 
    proxy_set_header Host $host; 
    proxy_set_header X-Real-IP $remote_addr; 
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header X-Scheme $scheme;
    proxy_set_header REMOTE-HOST $remote_addr; 
    proxy_set_header Upgrade $http_upgrade; 
    proxy_set_header Connection $http_connection; 
    proxy_set_header X-Forwarded-Proto $scheme; 
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_set_header X-Forwarded-Host $host;
    proxy_http_version 1.1; 
    add_header X-Cache $upstream_cache_status; 
    add_header Cache-Control no-cache; 
    proxy_ssl_server_name off; 
    proxy_ssl_name $proxy_host; 
}

location /management.ManagementService/ {
    grpc_pass grpc://172.10.0.15:33073; 
    #grpc_ssl_verify off;
    grpc_read_timeout 1d; 
    grpc_send_timeout 1d; 
    grpc_socket_keepalive on;
    grpc_set_header Host $host:$server_port;
    grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location /management {
    grpc_pass grpc://172.10.0.11:8080; 
    grpc_set_header Host $host:$server_port;
    grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

遇到的问题

你可能会遇到因为网络问题导致 Management 服务无法完成GEO库的下载,这里我准备了一个脚本(来自官方改版),可以解决这个问题。

#!/bin/bash

# to install sha256sum on mac: brew install coreutils
if ! command -v sha256sum &> /dev/null
then
    echo "sha256sum is not installed or not in PATH, please install with your package manager. e.g. sudo apt install sha256sum" > /dev/stderr
    exit 1
fi

if ! command -v sqlite3 &> /dev/null
then
    echo "sqlite3 is not installed or not in PATH, please install with your package manager. e.g. sudo apt install sqlite3" > /dev/stderr
    exit 1
fi

if ! command -v unzip &> /dev/null
then
    echo "unzip is not installed or not in PATH, please install with your package manager. e.g. sudo apt install unzip" > /dev/stderr
    exit 1
fi

download_geolite_mmdb() {
  DATABASE_URL="https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City/download?suffix=tar.gz"
  SIGNATURE_URL="https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City/download?suffix=tar.gz.sha256"
  # Download the database and signature files
  echo "Downloading mmdb signature file..."
  SIGNATURE_FILE=$(curl -s  -L -O -J "$SIGNATURE_URL" -w "%{filename_effective}")
  echo "Downloading mmdb database file..."
  # DATE_STR=$(date +%Y%m%d)
  # DATABASE_FILE="GeoLite2-City_${DATE_STR}.tar.gz"
  DATABASE_FILE=$(curl -s  -L -O -J "$DATABASE_URL" -w "%{filename_effective}")
  # Verify the signature
  echo "Verifying signature..."
  if sha256sum -c --status "$SIGNATURE_FILE"; then
      echo "Signature is valid."
  else
      echo "Signature is invalid. Aborting."
      exit 1
  fi

  # Unpack the database file
  EXTRACTION_DIR=$(basename "$DATABASE_FILE" .tar.gz)
  echo "Unpacking $DATABASE_FILE..."
  mkdir -p "$EXTRACTION_DIR"
  tar -xzvf "$DATABASE_FILE" > /dev/null 2>&1

  MMDB_FILE="GeoLite2-City.mmdb"
  cp "$EXTRACTION_DIR"/"$MMDB_FILE" $MMDB_FILE

  # Remove downloaded files
  rm -r "$EXTRACTION_DIR"
  rm "$DATABASE_FILE" "$SIGNATURE_FILE"

  # Done. Print next steps
  echo ""
  echo "Process completed successfully."
  echo "Now you can place $MMDB_FILE to 'datadir' of management service."
  echo -e "Example:\n\tdocker compose cp $MMDB_FILE management:/var/lib/netbird/"
}


download_geolite_csv_and_create_sqlite_db() {
  DATABASE_URL="https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City-CSV/download?suffix=zip"
  SIGNATURE_URL="https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City-CSV/download?suffix=zip.sha256"

  # Download the database file
  echo "Downloading csv signature file..."
  SIGNATURE_FILE=$(curl -s  -L -O -J "$SIGNATURE_URL" -w "%{filename_effective}")
  echo "Downloading csv database file..."
  # DATE_STR=$(date +%Y%m%d)
  # DATABASE_FILE="GeoLite2-City-CSV_${DATE_STR}.zip"
  DATABASE_FILE=$(curl -s  -L -O -J "$DATABASE_URL" -w "%{filename_effective}")
  # Verify the signature
  echo "Verifying signature..."
  if sha256sum -c --status "$SIGNATURE_FILE"; then
      echo "Signature is valid."
  else
      echo "Signature is invalid. Aborting."
      exit 1
  fi

  # Unpack the database file
  EXTRACTION_DIR=$(basename "$DATABASE_FILE" .zip)
  DB_NAME="geonames.db"

  echo "Unpacking $DATABASE_FILE..."
  unzip "$DATABASE_FILE" > /dev/null 2>&1

# Create SQLite database and import data from CSV
sqlite3 "$DB_NAME" <<EOF
.mode csv
.import "$EXTRACTION_DIR/GeoLite2-City-Locations-en.csv" geonames
EOF


  # Remove downloaded and extracted files
  rm -r -r "$EXTRACTION_DIR"
  rm  "$DATABASE_FILE" "$SIGNATURE_FILE"
  echo ""
  echo "SQLite database '$DB_NAME' created successfully."
  echo "Now you can place $DB_NAME to 'datadir' of management service."
  echo -e "Example:\n\tdocker compose cp $DB_NAME management:/var/lib/netbird/"
}

download_geolite_mmdb
echo -e "\n\n"
download_geolite_csv_and_create_sqlite_db
echo -e "\n\n"
echo "After copying the database files to the management service. You can restart the management service with:"
echo -e "Example:\n\tdocker compose restart management"

下载后的文件映射到 management 服务中即可,新版本 Netbird 可能会对下载的GEO文件名称有所要求,请查看日志后根据报错信息自行调整。


开源VPN-记一次对Netbird的纯手动部署
https://blog.cikaros.top/doc/860ffcf0.html
作者
Cikaros
发布于
2025年6月22日
许可协议