开源VPN-NetBird

NetBird 是一个简单快速的企业级 VPN 替代方案,建立在原生 WireGuard® 之上,可以轻松为您的组织或家庭创建安全的私人网络。它几乎不需要任何配置工作,从而摆脱了开放端口、复杂的防火墙规则、VPN 网关等烦琐任务的困扰。

官网地址:https://netbird.io/
文档地址:https://docs.netbird.io/

介绍

WireGuard相比较,NetBird 更适合作为团队或企业级的 VPN 解决方案。

因为 NetBird 会自行检测客户端网络环境,觉得是由服务端做中继节点还是通过 NAT 穿透,让客户端节点进行点对点通信。

搭建NetBird服务器

若您没有其他的要求,则可以直接通过官方提供的脚本进行一键安装,首先需要安装基础环境,以Ubuntu为例:

#
apt update 
apt install curl jq

其次 NetBird 需要 Docker 环境做支撑,可参考Ubuntu安装Docker文档完成对 Docker 环境的安装。

接下来执行export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started-with-zitadel.sh | bash指令即可完成对 NetBird 的安装。

自定义搭建NetBird服务器

若有较为高阶的定制需求。例如,服务器已拥有 Nginx 代理而不想使用脚本中安装的 Caddy 代理时,则需要对脚本进行自定义。

准备Nginx配置

当前以 Nginx 配置 SSL 进行代理为例介绍,这里需要将生成好的 Caddyfile 翻译为 Nginx 的配置文件,对比如下:

在此声明:需要用到的域名,若您想将 IDP 服务和 NetBird 服务进行区分(给每个服务配上不同的域名)安装,请自行研究,这里不在赘述。
用到的域名:demo.cikaros.top (该域名当前已不可用,别想白嫖我~)

{
  debug
	servers :80,:443 {
    protocols h1 h2c
  }
}

(security_headers) {
    header * {
        # enable HSTS

        Strict-Transport-Security "max-age=3600; includeSubDomains; preload"

        # disable clients from sniffing the media type
        X-Content-Type-Options "nosniff"

        # clickjacking protection
        X-Frame-Options "DENY"

        # xss protection
        X-XSS-Protection "1; mode=block"

        # Remove -Server header, which is an information leak
        # Remove Caddy from Headers
        -Server

        # keep referrer data off of HTTP connections
        Referrer-Policy strict-origin-when-cross-origin
    }
}

:80, demo.cikaros.top:443 {
    import security_headers
    # Signal
    reverse_proxy /signalexchange.SignalExchange/* h2c://signal:10000
    # Management
    reverse_proxy /api/* management:80
    reverse_proxy /management.ManagementService/* h2c://management:80
    # Zitadel
    reverse_proxy /zitadel.admin.v1.AdminService/* h2c://zitadel:8080
    reverse_proxy /admin/v1/* h2c://zitadel:8080
    reverse_proxy /zitadel.auth.v1.AuthService/* h2c://zitadel:8080
    reverse_proxy /auth/v1/* h2c://zitadel:8080
    reverse_proxy /zitadel.management.v1.ManagementService/* h2c://zitadel:8080
    reverse_proxy /management/v1/* h2c://zitadel:8080
    reverse_proxy /zitadel.system.v1.SystemService/* h2c://zitadel:8080
    reverse_proxy /system/v1/* h2c://zitadel:8080
    reverse_proxy /assets/v1/* h2c://zitadel:8080
    reverse_proxy /ui/* h2c://zitadel:8080
    reverse_proxy /oidc/v1/* h2c://zitadel:8080
    reverse_proxy /saml/v2/* h2c://zitadel:8080
    reverse_proxy /oauth/v2/* h2c://zitadel:8080
    reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080
    reverse_proxy /openapi/* h2c://zitadel:8080
    reverse_proxy /debug/* h2c://zitadel:8080
    reverse_proxy /device/* h2c://zitadel:8080
    reverse_proxy /device h2c://zitadel:8080
    # Dashboard
    reverse_proxy /* dashboard:80
}

构建的 Nginx 配置如下:

注意:需要在服务器内部制定合理的网桥环境,为每一个需要反向代理的容器制定合适的IP地址。
网段:172.10.0.0/24
zitadel172.10.0.2/32
dashboard172.10.0.3/32
signal172.10.0.4/32
management172.10.0.5/32
turn:需要公网开放,因此使用host网络模式

upstream zitadel {
    server 172.10.0.2:8080; 
}
upstream dashboard {
    keepalive 10; 
    server 172.10.0.3; 
}
upstream signal {
    server 172.10.0.4:10000; 
}
upstream management {
    server 172.10.0.5; 
}
server {
    listen 80 ; 
    listen 443 ssl http2 ; 
    server_name demo.cikaros.top; 
    index index.php index.html index.htm default.php default.htm default.html; 
    proxy_set_header Host $host:$server_port; 
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header X-Forwarded-Host $server_name; 
    proxy_set_header X-Real-IP $remote_addr; 
    proxy_http_version 1.1; 
    proxy_set_header Upgrade $http_upgrade; 
    proxy_set_header Connection "upgrade"; 
    access_log /www/sites/demo.cikaros.top/log/access.log; 
    error_log /www/sites/demo.cikaros.top/log/error.log; 
    location ^~ /.well-known/acme-challenge {
        allow all; 
        root /usr/share/nginx/html; 
    }
    client_header_timeout 1d; 
    client_body_timeout 1d; 
    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 X-Forwarded-Proto https; 
    proxy_set_header X-Forwarded-Host $host; 
    grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    add_header Strict-Transport-Security "max-age=3600; includeSubDomains; preload"; 
    add_header X-Content-Type-Options "nosniff"; 
    add_header X-Frame-Options "DENY"; 
    add_header X-XSS-Protection "1; mode=block"; 
    add_header Referrer-Policy "strict-origin-when-cross-origin"; 
    # Signal
    #reverse_proxy /signalexchange.SignalExchange/* h2c://signal:10000
    location /signalexchange.SignalExchange/ {
        grpc_pass grpc://signal; 
        #grpc_ssl_verify off;
        grpc_read_timeout 1d; 
        grpc_send_timeout 1d; 
        grpc_socket_keepalive on; 
    }
    # Management
    #reverse_proxy /api/* management:80
    location /api {
        proxy_pass http://management; 
    }
    #reverse_proxy /management.ManagementService/* h2c://management:80
    location /management.ManagementService {
        grpc_pass grpc://management; 
        #grpc_ssl_verify off;
        grpc_read_timeout 1d; 
        grpc_send_timeout 1d; 
        grpc_socket_keepalive on; 
    }
    # Zitadel
    #reverse_proxy /zitadel.admin.v1.AdminService/* h2c://zitadel:8080
    location /zitadel.admin.v1.AdminService {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /admin/v1/* h2c://zitadel:8080
    location /admin {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /zitadel.auth.v1.AuthService/* h2c://zitadel:8080
    location /zitadel.auth.v1.AuthService {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /auth/v1/* h2c://zitadel:8080
    location /auth {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /zitadel.management.v1.ManagementService/* h2c://zitadel:8080
    location /zitadel.management.v1.ManagementService {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /management/v1/* h2c://zitadel:8080
    location /management {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /zitadel.system.v1.SystemService/* h2c://zitadel:8080
    location /zitadel.system.v1.SystemService {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /system/v1/* h2c://zitadel:8080
    location /system {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /assets/v1/* h2c://zitadel:8080
    location /assets {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /ui/* h2c://zitadel:8080
    location /ui {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /oidc/v1/* h2c://zitadel:8080
    location /oidc {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /saml/v2/* h2c://zitadel:8080
    location /saml {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /oauth/v2/* h2c://zitadel:8080
    location /oauth {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080
    location = /.well-known/openid-configuration {
        proxy_pass http://zitadel; 
    }
    #reverse_proxy /openapi/* h2c://zitadel:8080
    location /openapi {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /debug/* h2c://zitadel:8080
    location /debug {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    #reverse_proxy /device/* h2c://zitadel:8080
    #reverse_proxy /device h2c://zitadel:8080
    location /device {
        grpc_pass grpc://zitadel; 
        grpc_set_header Host $host:$server_port; 
    }
    # Dashboard
    #reverse_proxy /* dashboard:80
    location / {
        proxy_pass http://dashboard; 
    }
    if ($scheme = http) {
        return 301 https://$host$request_uri; 
    }
    ssl_certificate /www/sites/demo.cikaros.top/ssl/fullchain.pem; 
    ssl_certificate_key /www/sites/demo.cikaros.top/ssl/privkey.pem; 
    ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1; 
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; 
    ssl_prefer_server_ciphers on; 
    ssl_session_cache shared:SSL:10m; 
    ssl_session_timeout 10m; 
    #add_header Strict-Transport-Security "max-age=31536000"; 
    error_page 497 https://$host$request_uri; 
    proxy_set_header X-Forwarded-Proto https; 
    ssl_stapling on; 
    ssl_stapling_verify on; 
}

getting-started-with-zitadel.sh脚本进行修订

#!/bin/bash
#函数返回非零,程序立即终止
set -e

handle_request_command_status() {
  PARSED_RESPONSE=$1
  FUNCTION_NAME=$2
  RESPONSE=$3
  if [[ $PARSED_RESPONSE -ne 0 ]]; then
    echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
    exit 1
  fi
}

handle_zitadel_request_response() {
  PARSED_RESPONSE=$1
  FUNCTION_NAME=$2
  RESPONSE=$3
  if [[ $PARSED_RESPONSE == "null" ]]; then
    echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
    exit 1
  fi
  sleep 1
}

#检查Docker Docker Compose是否安装
check_docker_compose() {
  if command -v docker-compose &> /dev/null
  then
      echo "docker-compose"
      return
  fi
  if docker compose --help &> /dev/null
  then
      echo "docker compose"
      return
  fi

  echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr
  exit 1
}

#检查JQ是否安装
check_jq() {
  if ! command -v jq &> /dev/null
  then
    echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr
    exit 1
  fi
}

#等待crdb正常启动
wait_crdb() {
  set +e
  while true; do
    if $DOCKER_COMPOSE_COMMAND exec -T crdb curl -sf -o /dev/null 'http://localhost:8080/health?ready=1'; then
      break
    fi
    echo -n " ."
    sleep 5
  done
  echo " done"
  set -e
}

#初始化crdb
init_crdb() {
  echo -e "\nInitializing Zitadel's CockroachDB\n\n"
  $DOCKER_COMPOSE_COMMAND up -d crdb
  echo ""
  # shellcheck disable=SC2028
  echo -n "Waiting cockroachDB  to become ready "
  wait_crdb
  $DOCKER_COMPOSE_COMMAND exec -T crdb /bin/bash -c "cp /cockroach/certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown -R 1000:1000 /zitadel-certs/"
  handle_request_command_status $? "init_crdb failed" ""
}

#获取主机IP
get_main_ip_address() {
  if [[ "$OSTYPE" == "darwin"* ]]; then
    interface=$(route -n get default | grep 'interface:' | awk '{print $2}')
    ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}')
  else
    interface=$(ip route | grep default | awk '{print $5}' | head -n 1)
    ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1)
  fi

  echo "$ip_address"
}

#等待PAT是否已生成
wait_pat() {
  PAT_PATH=$1
  set +e
  while true; do
    if [[ -f "$PAT_PATH" ]]; then
      break
    fi
    echo -n " ."
    sleep 1
  done
  echo " done"
  set -e
}

#等待API调用
wait_api() {
    INSTANCE_URL=$1
    PAT=$2
    set +e
    while true; do
      curl -s --fail -o /dev/null "$INSTANCE_URL/auth/v1/users/me" -H "Authorization: Bearer $PAT"
      if [[ $? -eq 0 ]]; then
        break
      fi
      echo -n " ."
      sleep 1
    done
    echo " done"
    set -e
}

#创建内部项目
create_new_project() {
  INSTANCE_URL=$1
  PAT=$2
  PROJECT_NAME="NETBIRD"

  RESPONSE=$(
    curl -sS -X POST "$INSTANCE_URL/management/v1/projects" \
      -H "Authorization: Bearer $PAT" \
      -H "Content-Type: application/json" \
      -d '{"name": "'"$PROJECT_NAME"'"}'
  )
  PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.id')
  handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_project" "$RESPONSE"
  echo "$PARSED_RESPONSE"
}

#创建应用
create_new_application() {
  INSTANCE_URL=$1
  PAT=$2
  APPLICATION_NAME=$3
  BASE_REDIRECT_URL1=$4
  BASE_REDIRECT_URL2=$5
  LOGOUT_URL=$6
  ZITADEL_DEV_MODE=$7
  DEVICE_CODE=$8

  if [[ $DEVICE_CODE == "true" ]]; then
    GRANT_TYPES='["OIDC_GRANT_TYPE_AUTHORIZATION_CODE","OIDC_GRANT_TYPE_DEVICE_CODE","OIDC_GRANT_TYPE_REFRESH_TOKEN"]'
  else
    GRANT_TYPES='["OIDC_GRANT_TYPE_AUTHORIZATION_CODE","OIDC_GRANT_TYPE_REFRESH_TOKEN"]'
  fi

  RESPONSE=$(
    curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
      -H "Authorization: Bearer $PAT" \
      -H "Content-Type: application/json" \
      -d '{
    "name": "'"$APPLICATION_NAME"'",
    "redirectUris": [
      "'"$BASE_REDIRECT_URL1"'",
      "'"$BASE_REDIRECT_URL2"'"
    ],
    "postLogoutRedirectUris": [
       "'"$LOGOUT_URL"'"
    ],
    "RESPONSETypes": [
      "OIDC_RESPONSE_TYPE_CODE"
    ],
    "grantTypes": '"$GRANT_TYPES"',
    "appType": "OIDC_APP_TYPE_USER_AGENT",
    "authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
    "version": "OIDC_VERSION_1_0",
    "devMode": '"$ZITADEL_DEV_MODE"',
    "accessTokenType": "OIDC_TOKEN_TYPE_JWT",
    "accessTokenRoleAssertion": true,
    "skipNativeAppSuccessPage": true
  }'
  )

  PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.clientId')
  handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_application" "$RESPONSE"
  echo "$PARSED_RESPONSE"
}

#创建服务用户
create_service_user() {
  INSTANCE_URL=$1
  PAT=$2

  RESPONSE=$(
    curl -sS -X POST "$INSTANCE_URL/management/v1/users/machine" \
      -H "Authorization: Bearer $PAT" \
      -H "Content-Type: application/json" \
      -d '{
            "userName": "netbird-service-account",
            "name": "Netbird Service Account",
            "description": "Netbird Service Account for IDP management",
            "accessTokenType": "ACCESS_TOKEN_TYPE_JWT"
      }'
  )
  PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
  handle_zitadel_request_response "$PARSED_RESPONSE" "create_service_user" "$RESPONSE"
  echo "$PARSED_RESPONSE"
}

#创建服务用户密钥
create_service_user_secret() {
  INSTANCE_URL=$1
  PAT=$2
  USER_ID=$3

  RESPONSE=$(
    curl -sS -X PUT "$INSTANCE_URL/management/v1/users/$USER_ID/secret" \
      -H "Authorization: Bearer $PAT" \
      -H "Content-Type: application/json" \
      -d '{}'
  )
  SERVICE_USER_CLIENT_ID=$(echo "$RESPONSE" | jq -r '.clientId')
  handle_zitadel_request_response "$SERVICE_USER_CLIENT_ID" "create_service_user_secret_id" "$RESPONSE"
  SERVICE_USER_CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.clientSecret')
  handle_zitadel_request_response "$SERVICE_USER_CLIENT_SECRET" "create_service_user_secret" "$RESPONSE"
}

#将服务用户追加至组织管理员
add_organization_user_manager() {
  INSTANCE_URL=$1
  PAT=$2
  USER_ID=$3

  RESPONSE=$(
    curl -sS -X POST "$INSTANCE_URL/management/v1/orgs/me/members" \
      -H "Authorization: Bearer $PAT" \
      -H "Content-Type: application/json" \
      -d '{
            "userId": "'"$USER_ID"'",
            "roles": [
              "ORG_USER_MANAGER"
            ]
      }'
  )
  PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
  handle_zitadel_request_response "$PARSED_RESPONSE" "add_organization_user_manager" "$RESPONSE"
  echo "$PARSED_RESPONSE"
}

#创建管理员用户
create_admin_user() {
    INSTANCE_URL=$1
    PAT=$2
    USERNAME=$3
    PASSWORD=$4
    RESPONSE=$(
        curl -sS -X POST "$INSTANCE_URL/management/v1/users/human/_import" \
          -H "Authorization: Bearer $PAT" \
          -H "Content-Type: application/json" \
          -d '{
                "userName": "'"$USERNAME"'",
                "profile": {
                  "firstName": "Zitadel",
                  "lastName": "Admin"
                },
                "email": {
                  "email": "'"$USERNAME"'",
                  "isEmailVerified": true
                },
                "password": "'"$PASSWORD"'",
                "passwordChangeRequired": true
          }'
      )
      PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
      handle_zitadel_request_response "$PARSED_RESPONSE" "create_admin_user" "$RESPONSE"
      echo "$PARSED_RESPONSE"
}

add_instance_admin() {
  INSTANCE_URL=$1
  PAT=$2
  USER_ID=$3

  RESPONSE=$(
    curl -sS -X POST "$INSTANCE_URL/admin/v1/members" \
      -H "Authorization: Bearer $PAT" \
      -H "Content-Type: application/json" \
      -d '{
            "userId": "'"$USER_ID"'",
            "roles": [
              "IAM_OWNER"
            ]
      }'
  )
  PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
  handle_zitadel_request_response "$PARSED_RESPONSE" "add_instance_admin" "$RESPONSE"
  echo "$PARSED_RESPONSE"
}

delete_auto_service_user() {
  INSTANCE_URL=$1
  PAT=$2

  RESPONSE=$(
    curl -sS -X GET "$INSTANCE_URL/auth/v1/users/me" \
      -H "Authorization: Bearer $PAT" \
      -H "Content-Type: application/json" \
  )
  USER_ID=$(echo "$RESPONSE" | jq -r '.user.id')
  handle_zitadel_request_response "$USER_ID" "delete_auto_service_user_get_user" "$RESPONSE"

  RESPONSE=$(
      curl -sS -X DELETE "$INSTANCE_URL/admin/v1/members/$USER_ID" \
        -H "Authorization: Bearer $PAT" \
        -H "Content-Type: application/json" \
  )
  PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
  handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_instance_permissions" "$RESPONSE"

  RESPONSE=$(
      curl -sS -X DELETE "$INSTANCE_URL/management/v1/orgs/me/members/$USER_ID" \
        -H "Authorization: Bearer $PAT" \
        -H "Content-Type: application/json" \
  )
  PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
  handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_org_permissions" "$RESPONSE"
  echo "$PARSED_RESPONSE"
}

#初始化zitadel服务器
init_zitadel() {
  echo -e "\nInitializing Zitadel with NetBird's applications\n"
  INSTANCE_URL="$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"

  TOKEN_PATH=./machinekey/zitadel-admin-sa.token

  echo -n "Waiting for Zitadel's PAT to be created "
  wait_pat "$TOKEN_PATH"
  echo "Reading Zitadel PAT"
  PAT=$(cat $TOKEN_PATH)
  if [ "$PAT" = "null" ]; then
    echo "Failed requesting getting Zitadel PAT"
    exit 1
  fi

  echo -n "Waiting for Zitadel to become ready "
  wait_api "$INSTANCE_URL" "$PAT"

  #  create the zitadel project
  echo "Creating new zitadel project"
  PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT")

  ZITADEL_DEV_MODE=false
  BASE_REDIRECT_URL=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
  if [[ $NETBIRD_HTTP_PROTOCOL == "http" ]]; then
    ZITADEL_DEV_MODE=true
  fi

  # create zitadel spa applications
  echo "Creating new Zitadel SPA Dashboard application"
  DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Dashboard" "$BASE_REDIRECT_URL/nb-auth" "$BASE_REDIRECT_URL/nb-silent-auth" "$BASE_REDIRECT_URL/" "$ZITADEL_DEV_MODE" "false")

  echo "Creating new Zitadel SPA Cli application"
  CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true" "true")

  MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT")

  SERVICE_USER_CLIENT_ID="null"
  SERVICE_USER_CLIENT_SECRET="null"

  create_service_user_secret "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID"

  DATE=$(add_organization_user_manager "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID")

  ZITADEL_ADMIN_USERNAME="Cikaros@$NETBIRD_DOMAIN"
  ZITADEL_ADMIN_PASSWORD="$(openssl rand -base64 32 | sed 's/=//g')@"

  HUMAN_USER_ID=$(create_admin_user "$INSTANCE_URL" "$PAT" "$ZITADEL_ADMIN_USERNAME" "$ZITADEL_ADMIN_PASSWORD")

  DATE="null"

  DATE=$(add_instance_admin "$INSTANCE_URL" "$PAT" "$HUMAN_USER_ID")

  DATE="null"
  DATE=$(delete_auto_service_user "$INSTANCE_URL" "$PAT")
  if [ "$DATE" = "null" ]; then
      echo "Failed deleting auto service user"
      echo "Please remove it manually"
  fi

  export NETBIRD_AUTH_CLIENT_ID=$DASHBOARD_APPLICATION_CLIENT_ID
  export NETBIRD_AUTH_CLIENT_ID_CLI=$CLI_APPLICATION_CLIENT_ID
  export NETBIRD_IDP_MGMT_CLIENT_ID=$SERVICE_USER_CLIENT_ID
  export NETBIRD_IDP_MGMT_CLIENT_SECRET=$SERVICE_USER_CLIENT_SECRET
  export ZITADEL_ADMIN_USERNAME
  export ZITADEL_ADMIN_PASSWORD
}

#检查域名
check_nb_domain() {
  DOMAIN=$1
  if [ "$DOMAIN-x" == "-x" ]; then
    echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr
    return 1
  fi

  if [ "$DOMAIN" == "netbird.example.com" ]; then
    echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
    return 1
  fi
  return 0
}

read_nb_domain() {
  READ_NETBIRD_DOMAIN=""
  echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr
  read -r READ_NETBIRD_DOMAIN < /dev/tty
  if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then
    read_nb_domain
  fi
  echo "$READ_NETBIRD_DOMAIN"
}

#获取turn外部IP
get_turn_external_ip() {
  TURN_EXTERNAL_IP_CONFIG="#external-ip="
  IP=$(curl -s -4 https://jsonip.com | jq -r '.ip')
  if [[ "x-$IP" != "x-" ]]; then
    TURN_EXTERNAL_IP_CONFIG="external-ip=$IP"
  fi
  echo "$TURN_EXTERNAL_IP_CONFIG"
}

#初始化环境变量 -- 程序入口
initEnvironment() {
  #当前服务器的域名
  NETBIRD_DOMAIN="demo.cikaros.top"
  #搭建完成后每个客户端节点的根域名
  NETBIRD_HOSTED_DOMAIN="netbird.local"
  #抛弃
  #CADDY_SECURE_DOMAIN=""
  #Zidatel服务器相关环境变量
  ZITADEL_EXTERNALSECURE="false"
  ZITADEL_TLS_MODE="disabled"
  ZITADEL_MASTERKEY="$(openssl rand -base64 32 | head -c 32)"
  #Netbird相关环境变量
  NETBIRD_PORT=80
  NETBIRD_HTTP_PROTOCOL="http"
  #Turn服务器相关环境变量
  TURN_USER="self"
  TURN_PASSWORD=$(openssl rand -base64 32 | sed 's/=//g')
  #这里可根据情况进行设定
  TURN_MIN_PORT=49152
  TURN_MAX_PORT=65535
  TURN_EXTERNAL_IP_CONFIG=$(get_turn_external_ip)

  #检查用户提供的域名是否可用
  if ! check_nb_domain "$NETBIRD_DOMAIN"; then
    NETBIRD_DOMAIN=$(read_nb_domain)
  fi

  #检查域名是否为使用IP
  if [ "$NETBIRD_DOMAIN" == "use-ip" ]; then
    NETBIRD_DOMAIN=$(get_main_ip_address)
  else
    ZITADEL_EXTERNALSECURE="true"
    ZITADEL_TLS_MODE="external"
    NETBIRD_PORT=443
    #CADDY_SECURE_DOMAIN=", $NETBIRD_DOMAIN:$NETBIRD_PORT"
    NETBIRD_HTTP_PROTOCOL="https"
  fi

  #生成Zidatel Token过期时间
  if [[ "$OSTYPE" == "darwin"* ]]; then
      ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
  else
      ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ")
  fi

  check_jq

  DOCKER_COMPOSE_COMMAND=$(check_docker_compose)

  #检查Zidatel环境变量是否存在
  if [ -f zitadel.env ]; then
    echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
    echo "You can use the following commands:"
    echo "  $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
    echo "  rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json"
    echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
    exit 1
  fi

  echo 生成初始化文件...
  renderDockerCompose > docker-compose.yml
  #抛弃
  #renderCaddyfile > Caddyfile
  renderZitadelEnv > zitadel.env
  echo "" > dashboard.env
  echo "" > turnserver.conf
  echo "" > management.json

  mkdir -p machinekey
  chmod 777 machinekey

  init_crdb

  echo -e "\nStarting Zidatel IDP for user management\n\n"
  $DOCKER_COMPOSE_COMMAND up -d zitadel
  init_zitadel

  echo -e "\nRendering NetBird files...\n"
  renderTurnServerConf > turnserver.conf
  renderManagementJson > management.json
  renderDashboardEnv > dashboard.env

  echo -e "\nStarting NetBird services\n"
  $DOCKER_COMPOSE_COMMAND up -d
  echo -e "\nDone!\n"
  echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"
  echo "Login with the following credentials:"
  echo "Username: $ZITADEL_ADMIN_USERNAME" | tee .env
  echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
}

#代码已被删除,这里仅做占位
renderCaddyfile() {
}

renderTurnServerConf() {
  cat <<EOF
listening-port=3478
$TURN_EXTERNAL_IP_CONFIG
tls-listening-port=5349
min-port=$TURN_MIN_PORT
max-port=$TURN_MAX_PORT
fingerprint
lt-cred-mech
user=$TURN_USER:$TURN_PASSWORD
realm=wiretrustee.com
cert=/etc/coturn/certs/cert.pem
pkey=/etc/coturn/private/privkey.pem
log-file=stdout
no-software-attribute
pidfile="/var/tmp/turnserver.pid"
no-cli
EOF
}

renderManagementJson() {
  cat <<EOF
{
    "Stuns": [
        {
            "Proto": "udp",
            "URI": "stun:$NETBIRD_DOMAIN:3478"
        }
    ],
    "TURNConfig": {
        "Turns": [
            {
                "Proto": "udp",
                "URI": "turn:$NETBIRD_DOMAIN:3478",
                "Username": "$TURN_USER",
                "Password": "$TURN_PASSWORD"
            }
        ],
        "TimeBasedCredentials": false
    },
    "Signal": {
        "Proto": "$NETBIRD_HTTP_PROTOCOL",
        "URI": "$NETBIRD_DOMAIN:10000"
    },
    "HttpConfig": {
        "AuthIssuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN",
        "AuthAudience": "$NETBIRD_AUTH_CLIENT_ID",
        "OIDCConfigEndpoint":"$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/.well-known/openid-configuration"
    },
    "IdpManagerConfig": {
        "ManagerType": "zitadel",
        "ClientConfig": {
            "Issuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN",
            "TokenEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/oauth/v2/token",
            "ClientID": "$NETBIRD_IDP_MGMT_CLIENT_ID",
            "ClientSecret": "$NETBIRD_IDP_MGMT_CLIENT_SECRET",
            "GrantType": "client_credentials"
        },
        "ExtraConfig": {
            "ManagementEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/management/v1"
        }
     },
   "DeviceAuthorizationFlow": {
       "Provider": "hosted",
       "ProviderConfig": {
           "Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",
           "ClientID": "$NETBIRD_AUTH_CLIENT_ID_CLI",
           "Scope": "openid"
       }
     },
    "PKCEAuthorizationFlow": {
        "ProviderConfig": {
            "Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",
            "ClientID": "$NETBIRD_AUTH_CLIENT_ID_CLI",
            "Scope": "openid profile email offline_access",
            "RedirectURLs": ["http://localhost:53000/","http://localhost:54000/"]
        }
    }
}
EOF
}

renderDashboardEnv() {
  cat <<EOF
# Endpoints
NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
# OIDC
AUTH_AUDIENCE=$NETBIRD_AUTH_CLIENT_ID
AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
AUTH_AUTHORITY=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
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
EOF
}

renderZitadelEnv() {
  cat <<EOF
ZITADEL_LOG_LEVEL=info
ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY
ZITADEL_DATABASE_COCKROACH_HOST=crdb
ZITADEL_DATABASE_COCKROACH_USER_USERNAME=zitadel_user
ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE=verify-full
ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT="/crdb-certs/ca.crt"
ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT="/crdb-certs/client.zitadel_user.crt"
ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY="/crdb-certs/client.zitadel_user.key"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_MODE=verify-full
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_ROOTCERT="/crdb-certs/ca.crt"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_CERT="/crdb-certs/client.root.crt"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_KEY="/crdb-certs/client.root.key"
ZITADEL_EXTERNALSECURE=$ZITADEL_EXTERNALSECURE
ZITADEL_TLS_ENABLED="false"
ZITADEL_EXTERNALPORT=$NETBIRD_PORT
ZITADEL_EXTERNALDOMAIN=$NETBIRD_DOMAIN
ZITADEL_FIRSTINSTANCE_PATPATH=/machinekey/zitadel-admin-sa.token
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_SCOPES=openid
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE=$ZIDATE_TOKEN_EXPIRATION_DATE
EOF
}

renderDockerCompose() {
  cat <<EOF
version: "3.4"
networks:
  netbird:
    ipam:
      config:
       - subnet: 172.10.0.0/24
services:
  #UI dashboard
  dashboard:
    image: netbirdio/dashboard:latest
    restart: unless-stopped
    networks: 
      netbird:
        ipv4_address: 172.10.0.3
    env_file:
      - ./dashboard.env
  # Signal
  signal:
    image: netbirdio/signal:latest
    restart: unless-stopped
    networks: 
      netbird:
        ipv4_address: 172.10.0.4
  # Management
  management:
    image: netbirdio/management:latest
    restart: unless-stopped
    networks: 
      netbird:
        ipv4_address: 172.10.0.5
    volumes:
      - netbird_management:/var/lib/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=false",
      "--single-account-mode-domain=$NETBIRD_HOSTED_DOMAIN",
      "--dns-domain=$NETBIRD_HOSTED_DOMAIN",
      "--idp-sign-key-refresh-enabled",
    ]
  # Coturn, AKA relay server
  coturn:
    image: coturn/coturn
    restart: unless-stopped
    domainname: netbird.relay.selfhosted
    volumes:
      - ./turnserver.conf:/etc/turnserver.conf:ro
    network_mode: host
    command:
      - -c /etc/turnserver.conf
  # Zitadel - identity provider
  zitadel:
    restart: 'always'
    networks: 
      netbird:
        ipv4_address: 172.10.0.2
    image: 'ghcr.io/zitadel/zitadel:v2.31.3' #不建议更换版本,当前 NetBird 存在Bug,并不兼容最新版本的 zitadel
    command: 'start-from-init --masterkeyFromEnv --tlsMode $ZITADEL_TLS_MODE'
    env_file:
      - ./zitadel.env
    depends_on:
      crdb:
        condition: 'service_healthy'
    volumes:
      - ./machinekey:/machinekey
      - netbird_zitadel_certs:/crdb-certs:ro
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
  # CockroachDB for zitadel
  crdb:
    restart: 'always'
    networks: 
      netbird:
        ipv4_address: 172.10.0.6
    image: 'cockroachdb/cockroach:v22.2.2'
    command: 'start-single-node --advertise-addr crdb'
    volumes:
      - netbird_crdb_data:/cockroach/cockroach-data
      - netbird_crdb_certs:/cockroach/certs
      - netbird_zitadel_certs:/zitadel-certs
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ]
      interval: '10s'
      timeout: '30s'
      retries: 5
      start_period: '20s'

volumes:
  netbird_management:
  netbird_crdb_data:
  netbird_crdb_certs:
  netbird_zitadel_certs:
EOF
}

initEnvironment

download-geolite2.sh脚本进行修订

文档中并未说明这里的步骤是必须手动做的,是在 Github 中找到的,用来处理登陆后页面提示 Token 401 的处理方案。

原因是因为,NetBird 依赖 GeoLite2-City.mmdb 数据库,若management(大陆)服务中不存在该数据库,则会造成management服务无法正常启动。

需要下载的文件,请将下载较慢的两个数据库文件,手动进行下载,并修改脚本中的环境变量:DATABASE_FILEDATABASE_FILE
文档地址:https://docs.netbird.io/selfhosted/geo-support

#!/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..."
  DATABASE_FILE='GeoLite2-City_20240305.tar.gz'

  # 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..."
  DATABASE_FILE='GeoLite2-City-CSV_20240305.zip'

  # 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服务即可!

客户端的使用

请自行参照官方文档进行部署配置,没有难点,这里不在赘述~

其他可能遇到的问题

服务已经可以正常访问,但客户端仍然连接不到服务器

建议检查客户端日志来确定错误位置,一般情况下是因为信号服务(signal)部署的问题导致,客户端无法正常连接到该服务。需检查服务配置和 Nginx 代理配置是否正确,必要时可检查 Nginx 错误请求日志来排查和确定。

crdb换成了PostgreSQL

需要按照官方文档进行配置,因PostgreSQL数据库的要求较高,需要针对编码和locale进行相应的配置,作者未过多的进行了解,详情请查看官方文档-数据库配置

没有域名,直接通过IP部署的

将域名环境变量(NETBIRD_DOMAIN)修改为use-ip,运行脚本即可。


开源VPN-NetBird
https://blog.cikaros.top/doc/8a393b1f.html
作者
Cikaros
发布于
2024年6月7日
许可协议