开源VPN-记一次对Netbird的纯手动部署
不知是因为什么原因,在一次版本更新后,Netbird的部分网络功能存在异常。原以为是官方Bug,但经过不屑的努力,最终发现问题出在我自己的服务器内部,备份数据后尝试重新部署…
以我个人真实的业务场景为部署依据,详细介绍部署步骤。
Netbird VPN的构成
Netbird VPN 在架构上主要包括以下几个部分:
- Management:Netbird的管理后端
- Signal:节点间交换密钥的信号服务
- Relay/Turn:用于在无法进行P2P通讯时提供的中继服务
- Stun:查询节点的公网IP,是检查P2P通讯的前提条件
部署过程
为了完成此次部署,需要一些前提条件:
- 拥有一个公网级别可用的域名
example.com
- 拥有一台在公网下可访问的服务器
111.123.164.210
240e:433:2103:2e::db
- 由于服务是用Docker部署的,因此创建了一个专属的内网
netbird-network
,网卡配置为172.10.0.0/16
- 在服务器内部准备
Nginx
和PostgreSQL
,假设PostgreSQL
在netbird-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
🎉恭喜各位坚持到了这里。我们继续进行部署,按照其依赖关系,我们按照如下顺序对每一个服务依次进行部署:
- Stun/Turn
- Relay (可忽略)
- Signal
- 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文件名称有所要求,请查看日志后根据报错信息自行调整。