部署headscale和DERP

一、如何判断一个crt证书是不是根证书?

核心判断依据是以下三个条件的 “与(AND)” 关系:

  1. 颁发者(Issuer)== 使用者(Subject)(自签名)。

  2. 基本约束(Basic Constraints) 中标记为 CA:TRUE

  3. 密钥用法(Key Usage) 包含 Certificate Sign

1. 检查“颁发者”和“使用者”

Issuer: C=CN, O=MyOrg, CN=My Root CA
Subject: C=CN, O=MyOrg, CN=My Root CA

如果 Issuer 和 Subject 完全一致,说明该证书是自签名的(根证书必须是自签名的)。

2. 检查“基本约束 (X509v3 Basic Constraints)”

X509v3 Basic Constraints: critical
    CA:TRUE

  • 必须有 CA:TRUE。如果是 CA:FALSE,则说明是终端实体证书(叶子证书),即使自签名也不是根证书。

3. 检查“密钥用法 (X509v3 Key Usage)”

X509v3 Key Usage: critical
    Certificate Sign, CRL Sign

  • 必须包含 Certificate Sign,表示该证书具备签发下级证书的能力。

证书只满足“ Issuer 和 Subject 完全一致”,没有基本约束和秘钥用法字段,是根证书吗?

严格来说,这种证书通常不算作标准的根证书。 虽然它满足了“自签名”(颁发者=使用者)的条件,但缺少了两个关键的“标准根证书”核心特征。

  • 标准要求:根证书必须包含 CA:TRUE 扩展,以明确声明该证书可用于签发其他证书。

  • 标准要求:根证书必须包含 Certificate Sign 密钥用法,表明其私钥可用于签署其他证书。

  • 你证书的状态:没有此字段,私钥的用途不明确。虽然它仍是“自签名”的,但不符合 X.509 标准中关于 CA 证书的约定。你证书的状态:没有此字段,意味着证书没有声明自己的 CA 能力。浏览器或操作系统会因此将其视为“无法确认的 CA”,可能拒绝信任其签发的下级证书。

  • 技术上:它是一个自签名证书(Self-Signed Certificate),但不是一个标准的“根 CA 证书”。功能上:它可以作为自己的信任锚点(如果你手动信任它),但不符合 RFC 5280 对 CA 证书的定义

RFC 5280 明确要求
用于签发证书的 CA 证书 必须包含 basicConstraints 扩展,且 cA 字段必须为 TRUE

这样的证书能用吗?

场景是否可用
作为私有 CA 的根证书(内部使用)⚠️ 可用但不规范。某些工具(如 OpenSSL)默认不要求根证书带 basicConstraints,但严格的应用(如 Java keytool)会拒绝。
作为服务器证书(自签名)✅ 可用。例如用于内部测试的 HTTPS 或 VPN,只要客户端手动信任即可。
提交到公共 CA 证书库❌ 不可用。公共 CA 审核会强制要求符合所有标准字段。

二、生成wildcard根证书

1. 生成私钥(RSA 2048 位)

openssl genrsa -out root.key 2048

2. 证书配置文件root.conf

[ req ]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no

[ req_distinguished_name ]
# 国家代码(2位字母)
C = CN
# 省份/州
ST = Zhejiang
# 城市
L = Hangzhou
# 组织名称
O = Jenet Digital Technology Co., Ltd.
# 组织单位(可选)
OU = IT Department
# 通用名称(这里设置为通配符域名)
CN = *.jenet.com.cn

[ v3_ca ]
# 关键扩展:声明为 CA
basicConstraints = critical, CA:FALSE
# 密钥用法:用于签署证书和 CRL
keyUsage = critical, keyCertSign, cRLSign
# 主题密钥标识符
subjectKeyIdentifier = hash
# 颁发者密钥标识符(自签名时与 subjectKeyIdentifier 相同)
authorityKeyIdentifier = keyid:always,issuer

生成的证书中有黄色感叹号:

这个黄色感叹号是 Windows 证书管理器给出的“不安全”或“配置不完整”的警告提示,并不代表证书本身格式错误或失效。它主要是由以下两个原因触发的:

  1. 证书未加入 Windows 信任存储区
    由于你是在本地生成的自签名根证书,默认情况下 Windows 不会将其自动放入“受信任的根证书颁发机构”存储区,因此系统会通过黄色叹号提示你:“此证书不受当前系统信任”。

  2. 缺少部分可选扩展字段
    虽然你已经设置了 basicConstraints 和 keyUsage,但 Windows 可能期望根证书额外包含 CRL 分发点(CRL Distribution Points)授权信息访问(Authority Information Access) 等扩展。缺少这些字段虽然不是错误,但部分安全策略较严格的系统会给予“警告”提示。

需要注意,不能声明为CA(上面的配置文件不能使用),否则就不能作为服务器证书:

openssl req -x509 -newkey rsa:2048 \
  -keyout wildcard.jenet.com.cn.key \
  -out wildcard.jenet.com.cn.crt \
  -days 10950 -nodes \
  -subj "/C=CN/ST=Zhejiang/L=Hangzhou/O=Jenet/OU=IT/CN=*.jenet.com.cn" \
  -addext "subjectAltName=DNS:*.jenet.com.cn,DNS:jenet.com.cn" \
  -addext "basicConstraints=CA:FALSE" \
  -addext "keyUsage=digitalSignature,keyEncipherment" \
  -addext "extendedKeyUsage=serverAuth"

3. 将证书设置为NMS系统的信任证书

root@dd6f5d5515a7:~# keytool -importcert -alias headscale_jenet -file wildcard.jenet.com.cn.crt -keystore /usr/local/openjdk-8/lib/security/cacerts -storepass changeit -noprompt
Certificate was added to keystore

这条命令的作用是:将一个通配符SSL证书(wildcard.jenet.com.cn.crt)作为受信任的根证书,导入到Java运行环境(OpenJDK 8)的默认信任库(cacerts)中,并且全程自动确认、无需人工交互。

三、部署headscale

1. 域名

 nginx需要使用一个子域名,为headscale添加子域名hs01.jenet.com.cn:

2. 网站证书

使用之前生成的wildcard证书,不需要重新生成。

3. Noise私钥

Noise私钥用于加密Headscale与Tailscale客户端之间的通信流量。

你无需手动生成这个私钥。 根据Headscale的官方配置说明,如果配置文件中指定的私钥文件不存在,Headscale会在启动时自动生成一个新的Noise私钥。

你只需要在配置文件中指定私钥的存储路径即可,例如:

noise:
  private_key_path: /var/lib/headscale/noise_private.key

Headscale启动时会检查该路径,如果文件不存在则自动创建。

4. 启用内置DERP

Headscale内置了DERP(Designated Encrypted Relay for Packets)服务器,可以方便地自建中继服务。

以下是一份启用内置DERP服务器的config.yaml配置示例:

derp:
  server:
    enabled: true                      # 启用内置DERP服务器
    region_id: 999                     # 自定义区域ID
    region_code: "headscale"           # 区域代码
    region_name: "Headscale Embedded DERP"  # 区域名称
    stun_listen_addr: "0.0.0.0:3478"  # STUN服务监听地址
    private_key_path: /var/lib/headscale/derp_server_private.key  # DERP私钥路径
  automatically_add_embedded_derp_region: true  # 自动将内置DERP添加到DERP映射中
  urls:
    - https://controlplane.tailscale.com/derpmap/default  # 加载默认的DERP映射
  paths: []                             # 自定义DERP映射文件路径

DERP服务器的私钥路径,与Noise私钥类似,如果文件不存在也会自动生成。

5. 配置文件

# 服务地址 - 客户端连接地址
server_url: https://hs01.jenet.com.cn:8011

# HTTP 监听地址
listen_addr: 0.0.0.0:8080

# Metrics 监控端口
metrics_listen_addr: 0.0.0.0:9090

# gRPC 监听地址
grpc_listen_addr: 0.0.0.0:50443

# 允许 gRPC 使用非加密连接(仅在内网测试环境使用)
grpc_allow_insecure: true

# Noise 协议私钥路径
noise:
  private_key_path: /var/lib/headscale/noise_private.key

# IP 地址分配前缀
prefixes:
  v4: 100.65.0.0/10
  v6: fd7b:115c:a1e0::/48
  allocation: sequential

# DERP 中继服务器配置
derp:
  server:
    enabled: true
    region_id: 998
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key
    automatically_add_embedded_derp_region: true


# 禁用自动检查更新
disable_check_updates: true

# 临时节点自动删除时间
ephemeral_node_inactivity_timeout: 30m


# 数据库配置 - 推荐使用 PostgreSQL
database:
  type: postgres

  gorm:
    prepare_stmt: true
    parameterized_queries: true
    skip_err_record_not_found: true
    slow_threshold: 1000

  # PostgreSQL 配置
  postgres:
    host: 10.206.16.17
    port: 5432
    name: headscale
    user: headscale
    pass: HeadscaleTestB
    max_open_conns: 10
    max_idle_conns: 10
    conn_max_idle_time_secs: 3600
    ssl: false

验证PSQL数据库是否正常:

6. 部署headscale命令

部署headscale的命令为:

docker run -d -v /home/headscale/container-config/config.yaml:/etc/headscale/config.yaml -v /home/headscale/container-data/data:/var/lib/headscale --name headscale-jenet -p 8051:8080 -p 9051:9090 -p 50443:50443 -p 3478:3478/udp wx.nb-jetron.com:5088/fangzhou/headscale:1.0 serve

7. 生成apikey

先进入到headscale docker容器,再创建apikey

root@VM-16-17-ubuntu:/home/headscale/container-config# docker exec -it bc964133e16c /bin/sh
/ #  headscale apikeys create --expiration 10950d
FyZT_r7.WOARWNORSNM53ZclOBWI25up-Ssq9SRD
/ #

8. 状态检查

我们启动docker时,将8080和9090映射了出去,我们可以通过

http://118.195.143.228:8051/health检查headscale的运行状态:

http://118.195.143.228:9051/metrics

9. 启动nginx 容器

# 在 docker-compose.yml 文件所在目录下执行
docker compose up -d
  • up:创建并启动容器。

  • -d:后台运行(detach 模式),与 docker run -d 效果一致。

root@VM-16-17-ubuntu:/home/headscale/nginx# cat docker-compose.yml
version: '3.7'

services:
  headscale-nginx:
    image: 61.130.105.158:5080/library/nginx:1.18.0-alpine
    ports:
      - "8011:8011"
    volumes:
      - /home/headscale/certs/:/home/headscale/cert/
      - /home/headscale/nginx/nginx.conf:/etc/nginx/nginx.conf
    restart: always

Nginx 做 TLS 终止 + 反向代理,这是对的。但当前 proxy_pass http://127.0.0.1:8051 不通(Nginx 容器内 127.0.0.1 指不到宿主机)。

为什么50443端口不需要代理:

gRPC(50443)— 不需要 Nginx

gRPC 配置中 grpcTls=false,是明文直连,不需要 Nginx 做 TLS 终止。而且 Nginx 代理 gRPC 需要 grpc_pass 指令(只有 HTTP/2 才支持),配置复杂。

最简单的做法:gRPC 直连,改端口号

你的 docker 映射是 -p 50443:50443,宿主机端口是 50453,所以:

10. NMS数据库配置

目前NMS上关于headscale的配置如下:

#headscale
# 全局开关: 设为 false 可完全禁用 Headscale/SDWAN 功能
# 禁用后,所有 gRPC/REST 连接将被跳过,SDWAN 用户自动回退到 OpenVPN
headscale.server.enabled=true

#此配置项是下发到网关的,网关会通过这个网址访问headscale服务器
headscale.server.url=https://hs01.jenet.com.cn:8011
headscale.server.apiKey=NaZs_K9.1DUv8WoqKP84kEL4hsRDUr6xhvwxC-80

headscale.server.timeout=30000
headscale.server.retryAttempts=3

# Headscale gRPC configuration
headscale.server.grpcHost=hs01.jenet.com.cn
headscale.server.grpcPort=50443
headscale.server.grpcTls=false
headscale.server.grpcTimeout=30000

# Headscale DB cleanup (PostgreSQL)
# - 默认复用 jdbc.database.* 的连接信息,并将数据库名替换为 headscale(可通过 headscale.db.* 覆盖)。
# - 以 nodes.expiry 不为空作为“设备已退出”的判断依据。
headscale.cleanup.enabled=true
headscale.cleanup.cron=0 0/1 * * * ?
headscale.cleanup.lockSeconds=3600
headscale.cleanup.expiryGraceDays=0
headscale.db.name=headscale
headscale.db.url=jdbc:postgresql://118.195.143.228:5432/headscale
headscale.db.username=headscale
headscale.db.password=HeadscaleTestB
# Warn when ACL payload size approaches the common 4 MiB gRPC default receive limit
headscale.server.aclWarnBytes=3145728

重启nms服务

root@VM-16-17-ubuntu:/home/jetrun_nms_local# docker-compose restart nms_local
[+] Restarting 2/2
 ✔ Container jetrun_nms_local-nms_local-2  Started                                                                                                                                                       11.1s
 ✔ Container jetrun_nms_local-nms_local-1  Started                                                                                                                                                       11.3s
root@VM-16-17-ubuntu:/home/jetrun_nms_local#
 

11. 与NMS联调测试

1)REST API 测试

HeadscaleService.testConnection() 实际上就是调用 GET /api/v1/user

curl -v -X GET \
  "https://hs01.jenet.com.cn:8011/api/v1/user" \
  -H "Authorization: Bearer NaZs_K9.1DUv8WoqKP84kEL4hsRDUr6xhvwxC-80"

成功返回用户列表(JSON),失败则会抛异常。

测试结果如下:

root@VM-16-17-ubuntu:~# curl -v -X GET \
  "https://hs01.jenet.com.cn:8011/api/v1/user" \
  -H "Authorization: Bearer NaZs_K9.1DUv8WoqKP84kEL4hsRDUr6xhvwxC-80"
Note: Unnecessary use of -X or --request, GET is already inferred.
* Host hs01.jenet.com.cn:8011 was resolved.
* IPv6: (none)
* IPv4: 118.195.143.228
*   Trying 118.195.143.228:8011...
* Connected to hs01.jenet.com.cn (118.195.143.228) port 8011
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self-signed certificate
* Closing connection
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
 

Headscale 服务器的 HTTPS 证书是自签名证书(self-signed certificate),curl 默认会验证证书链,所以被拦截了。

curl 输出中关键信息:

* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self-signed certificate

 curl 手动测试:加 -k 跳过证书验证

root@VM-16-17-ubuntu:~# curl -v -k -X GET   "https://hs01.jenet.com.cn:8011/api/v1/user"   -H "Authorization: Bearer NaZs_K9.1DUv8WoqKP84kEL4hsRDUr6xhvwxC-80"
Note: Unnecessary use of -X or --request, GET is already inferred.
* Host hs01.jenet.com.cn:8011 was resolved.
* IPv6: (none)
* IPv4: 118.195.143.228
*   Trying 118.195.143.228:8011...
* Connected to hs01.jenet.com.cn (118.195.143.228) port 8011
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: C=CN; ST=Zhejiang; L=Hangzhou; O=Jenet Digital Technology Co., Ltd.; OU=IT Department; CN=*.jenet.com.cn
*  start date: Jul  6 05:59:45 2026 GMT
*  expire date: Jun 28 05:59:45 2056 GMT
*  issuer: C=CN; ST=Zhejiang; L=Hangzhou; O=Jenet Digital Technology Co., Ltd.; OU=IT Department; CN=*.jenet.com.cn
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/1.x
> GET /api/v1/user HTTP/1.1
> Host: hs01.jenet.com.cn:8011
> User-Agent: curl/8.5.0
> Accept: */*
> Authorization: Bearer NaZs_K9.1DUv8WoqKP84kEL4hsRDUr6xhvwxC-80
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/1.1 404 Not Found
< Server: nginx
< Date: Tue, 07 Jul 2026 06:02:38 GMT
< Content-Type: text/html
< Content-Length: 146
< Connection: keep-alive
<
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

导入通配符证书后还是报错:

进入容器中执行:keytool -list -keystore /usr/local/openjdk-8/lib/security/cacerts  -storepass changeit | grep headscale

root@dd6f5d5515a7:/#   keytool -list -keystore /usr/local/openjdk-8/lib/security/cacerts \
  -storepass changeit | grep headscale
headscale, Jul 23, 2025, trustedCertEntry,
headscale01, Jul 7, 2026, trustedCertEntry,
headscale_jenet, Jul 6, 2026, trustedCertEntry,
root@dd6f5d5515a7:/#

查看连接相关日志:

docker logs -f dd6f5d5515a7 2>&1 | grep -iE "(headscale|gRPC|ssl|SSL|PKIX)"

2026-07-07 14:45:42.640 ERROR 7 --- [io-9080-exec-10] c.j.nb.biz.service.HeadscaleService      : Failed to test Headscale connection
javax.net.ssl.SSLPeerUnverifiedException: Hostname hs01.jenet.com.cn not verified:
        at com.jetron.nb.biz.service.HeadscaleService.getUsers(HeadscaleService.java:173)
        at com.jetron.nb.biz.service.HeadscaleService.testConnection(HeadscaleService.java:854)
        at com.jetron.nb.biz.service.HeadscaleHybridService.getConnectionStatus(HeadscaleHybridService.java:189)
        at com.jetron.nb.web.controller.headscale.HeadscaleController.testConnection(HeadscaleController.java:70)
        at com.jetron.nb.web.controller.headscale.HeadscaleController$$FastClassBySpringCGLIB$$c255843b.invoke(<generated>)
        at com.jetron.nb.web.controller.headscale.HeadscaleController$$EnhancerBySpringCGLIB$$f71823a0.testConnection(<generated>)
        at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:678)
2026-07-07 14:45:42.640  INFO 7 --- [io-9080-exec-10] c.j.nb.biz.service.HeadscaleGrpcService  : Testing gRPC connection to hs01.jenet.com.cn:50443 (TLS: false)
2026-07-07 14:45:42.640  INFO 7 --- [io-9080-exec-10] c.j.nb.biz.service.HeadscaleGrpcService  : Channel state: TRANSIENT_FAILURE
2026-07-07 14:45:42.640  INFO 7 --- [io-9080-exec-10] c.j.nb.biz.service.HeadscaleGrpcService  : Attempting to call listUsers for connectivity test...
2026-07-07 14:45:42.640  INFO 7 --- [io-9080-exec-10] c.j.nb.biz.service.HeadscaleGrpcService  : Request details:
2026-07-07 14:45:42.641  WARN 7 --- [io-9080-exec-10] c.j.nb.biz.service.HeadscaleGrpcService  : gRPC connection test failed with status: UNAVAILABLE - io exception (Cause: finishConnect(..) failed: Connection refused: hs01.jenet.com.cn/118.195.143.228:50443)
2026-07-07 14:45:42.641  WARN 7 --- [io-9080-exec-10] c.j.nb.biz.service.HeadscaleGrpcService  : Service unavailable - check if Headscale gRPC server is running and accessible
2026-07-07 14:46:10.187  INFO 7 --- [ool-10-thread-1] c.j.n.b.s.HeadscaleDbCleanupService      : [HeadscaleDbCleanupService] Cleanup finished. nodesTable=public.nodes, nodesDeleted=0, relatedDeleted=0, graceDays=0, threshold=2026-07-07T06:46:10.093Z
 

javax.net.ssl.SSLPeerUnverifiedException: Hostname hs01.jenet.com.cn not verified

注意:这不是证书信任问题(不是 PKIX),而是 Hostname 验证失败。证书已经被信任了,但 Java 的 HostnameVerifier 认为 hs01.jenet.com.cn 与证书不匹配。

根因:Java 的 HTTPS 主机名验证规则是——如果证书中有 SAN(Subject Alternative Name)扩展,则完全忽略 CN,只检查 SAN。你的证书 CN 是 *.jenet.com.cn,但很可能 SAN 里没有包含 *.jenet.com.cn

这个证书是作为 CA 根证书 生成的,有三个致命缺陷:

  1. CA:TRUE — 标记为 CA 证书
  2. 没有 serverAuth 扩展密钥用法
  3. 没有 SAN

Java 的 hostname 验证器(OkHttp 的 OkHostnameVerifier)对这种情况会直接拒绝,即使 CN 通配符匹配。这就是 SSLPeerUnverifiedException 的根本原因。

重新生成证书后:

root@f767decf8615:/# keytool -list -keystore /usr/local/openjdk-8/lib/security/cacerts -storepass changeit | grep headscale
headscale, Jul 23, 2025, trustedCertEntry,
root@f767decf8615:/# keytool -importcert \
  -alias headscale01 \
  -file /tmp/headscale_new.crt \
  -keystore /usr/local/openjdk-8/lib/security/cacerts \
  -storepass changeit -noprompt
Certificate was added to keystore
root@f767decf8615:/# keytool -list -keystore /usr/local/openjdk-8/lib/security/cacerts -storepass changeit | grep headscale
headscale, Jul 23, 2025, trustedCertEntry,
headscale01, Jul 7, 2026, trustedCertEntry,
 

2) gRPC 测试

HeadscaleGrpcService.testGrpcConnection() 会:

  1. 检查 gRPC channel 状态
  2. 调用 headscale.v1.HeadscaleService/listUsers 这个 gRPC 方法

等效于使用 grpcurl 工具:

grpcurl -plaintext \
  -H "authorization: Bearer NaZs_K9.1DUv8WoqKP84kEL4hsRDUr6xhvwxC-80" \
  hs01.jenet.com.cn:50443 \
  headscale.v1.HeadscaleService/ListUsers

3) 连接OK页签报错

测试连接OK,但是点击headscale页签时报错:

原因是:

headscale 刚部署,还没有配置 ACL 策略,请求 /api/v1/policy 时 headscale 内部报 500。iot-platform 把这个 500 转成了「服务器内部错误」显示在前端。

初始化一个 ACL 策略即可:

# 给 headscale 设置一个基础的 ACL 策略(允许所有流量)
curl -X PUT "http://127.0.0.1:8051/api/v1/policy" \
  -H "Authorization: Bearer NaZs_K9.1DUv8WoqKP84kEL4hsRDUr6xhvwxC-80" \
  -H "Content-Type: application/json" \
  -d '{
    "acls": [
      {
        "action": "accept",
        "src": ["*"],
        "dst": ["*:*"]
      }
    ]
  }'

curl -X PUT "http://127.0.0.1:8051/api/v1/policy" -H "Authorization: Bearer FyZT_r7.WOARWNORSNM53ZclOBWI25up-Ssq9SRD" -H "Content-Type: application/json" -d '{"acls":[{"action":"accept","src":["*"],"dst":["*:*"]}]}'

curl -X PUT "http://127.0.0.1:8051/api/v1/policy" -H "Authorization: Bearer FyZT_r7.WOARWNORSNM53ZclOBWI25up-Ssq9SRD" -H "Content-Type: application/json" -d '{"acls":[{"action":"accept","src":["*"],"dst":["*:*"]}],"autoApprovers":{"routes":{"*":["*"]}}}'

4) 写入默认规则

没有任何用户时,默认规则为:

{
    "acls":[

    ],
    "autoApprovers":{
        "routes":{
            "*":[
                "*"
            ]
        }
    },
    "groups":{

    },
    "hosts":{},
    "tagOwners":{},
    "tests":[]
}

插入默认规则脚本:

curl -X PUT "http://127.0.0.1:8051/api/v1/policy" -H "Authorization: Bearer FyZT_r7.WOARWNORSNM53ZclOBWI25up-Ssq9SRD" -H "Content-Type: application/json" -d '{"policy":"{\"acls\":[],\"autoApprovers\":{\"routes\":{\"*\":[\"*\"]}},\"groups\":{},\"hosts\":{},\"tagOwners\":{},\"tests\":[]}"}'

查看规则命令:

curl -s "http://127.0.0.1:8051/api/v1/policy" -H "Authorization: Bearer FyZT_r7.WOARWNORSNM53ZclOBWI25up-Ssq9SRD" | python3 -m json.tool

5) 客户端获取的IP地址不对

现在headscale和iot-platform的reset和grpc接口已经调试通过,tailscale客户端登录也成功了,headscale侧打印日志为:

2026-07-08T02:02:20Z INF github.com/juanfont/headscale/hscontrol/poll.go:602 > node has connected, mapSession: 0xc0005c0600, chan: 0xc000294a80 node=JN7302603050015D node.id=3 omitPeers=false readOnly=false stream=true

但是客户端拿到的地址是100.64.0.5,我的headscale的配置文件中设置的地址为100.65.0.0/16网段的:

数据库

指定用户登录默认数据库

psql -U headscale

查看当前数据库

SELECT current_database();

删除所有的数据后

ACL策略中默认需要写入下面的值:

{
    "acls": [
        {
            "action": "accept",
            "src": ["*"],
            "dst": ["*:*"]
        }
    ],
    "autoApprovers": {
        "routes": {
            "192.168.1.0/24": ["juan111@"]
        }
    }
}

6)调试路由审批

curl -s "http://127.0.0.1:8051/headscale/routes" -H "Authorization: Bearer FyZT_r7.WOARWNORSNM53ZclOBWI25up-Ssq9SRD" | python3 -m json.tool

headscale的显示页面为:

页面上的可用路由字段显示的是available_routes,节点信息的available_routes和approved_routes的内容是一样的,所以表示是已经审批过的路由。

                "approved_routes": [
                        "fd7a:115c:a1e0:b1a:0:5:c0a8:400/120"
                ],
                "available_routes": [
                        "fd7a:115c:a1e0:b1a:0:5:c0a8:400/120"
                ]

 headscale nodes list -o json

 headscale nodes list -o json
[
        {
                "id": 8,
                "machine_key": "mkey:2024dd06096393257f5269d0484de7ca1c40ea99da48cc78555a2700d9fef87d",
                "node_key": "nodekey:91310dfa2fdf6fb85f5203882ffc6a158833ef3e9cc5039e9319bcf3afcdb15a",
                "disco_key": "discokey:6b0aaa695c8d1771df530fcccc41d655ae252cb1562f5e80a33090111b6c830c",
                "ip_addresses": [
                        "100.65.0.1",
                        "fd7a:115c:a1e0::1"
                ],
                "name": "JN7302603050015G",
                "user": {
                        "id": 6,
                        "name": "juanbing1",
                        "created_at": {
                                "seconds": 1783495919,
                                "nanos": 141445000
                        },
                        "display_name": "e8369a307a9e11f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783573888,
                        "nanos": 656668000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 6,
                                "name": "juanbing1",
                                "created_at": {
                                        "seconds": 1783495919,
                                        "nanos": 141445000
                                },
                                "display_name": "e8369a307a9e11f19afb21f67909a124"
                        },
                        "id": 6,
                        "key": "d4703ea3a5cd47956ff80089510b8dcd07b6161f1b9d54da",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783611119,
                                "nanos": 293000000
                        },
                        "created_at": {
                                "seconds": 1783495919,
                                "nanos": 372700000
                        }
                },
                "created_at": {
                        "seconds": -62135596800
                },
                "register_method": 1,
                "given_name": "jn7302603050015g",
                "online": true,
                "approved_routes": [
                        "fd7a:115c:a1e0:b1a:0:4:c0a8:100/120"
                ],
                "available_routes": [
                        "fd7a:115c:a1e0:b1a:0:4:c0a8:100/120"
                ],
                "subnet_routes": [
                        "fd7a:115c:a1e0:b1a:0:4:c0a8:100/120"
                ]
        },
        {
                "id": 9,
                "machine_key": "mkey:33b01c331d571c8b63bb4d5368df4c36479a49d9aa50bce27e54f17a92cf992e",
                "node_key": "nodekey:2f8d878c032db82771fc8fe8467f4f6b788e772d2128007795ce9c3fa4cf0960",
                "disco_key": "discokey:203c4c7ec2f3fe624f6a628002ffd116791aca52f38281ba691b94fc12923c4c",
                "ip_addresses": [
                        "100.65.0.2",
                        "fd7a:115c:a1e0::2"
                ],
                "name": "CHINAMI-IT429FT",
                "user": {
                        "id": 6,
                        "name": "juanbing1",
                        "created_at": {
                                "seconds": 1783495919,
                                "nanos": 141445000
                        },
                        "display_name": "e8369a307a9e11f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783573981,
                        "nanos": 393164000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 6,
                                "name": "juanbing1",
                                "created_at": {
                                        "seconds": 1783495919,
                                        "nanos": 141445000
                                },
                                "display_name": "e8369a307a9e11f19afb21f67909a124"
                        },
                        "id": 6,
                        "key": "d4703ea3a5cd47956ff80089510b8dcd07b6161f1b9d54da",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783611119,
                                "nanos": 293000000
                        },
                        "created_at": {
                                "seconds": 1783495919,
                                "nanos": 372700000
                        }
                },
                "created_at": {
                        "seconds": 1783496523,
                        "nanos": 602383000
                },
                "register_method": 1,
                "given_name": "chinami-it429ft",
                "online": true
        },
        {
                "id": 15,
                "machine_key": "mkey:0313d7d2b61ab6a3d39e900c62ffb50a4d6da36e8a682ca0ffb63e7515ed5b7a",
                "node_key": "nodekey:08f852ed5e6ad42c9cbf7ba8d5d116d5642f2d922cb91df8857b0cfe37769f4e",
                "disco_key": "discokey:5e258f14da22c8b58e9b232fc54fd8789da69d97c98d83cfce0f46eda8c7867a",
                "ip_addresses": [
                        "100.65.0.9",
                        "fd7a:115c:a1e0::9"
                ],
                "name": "JN002026S0011006",
                "user": {
                        "id": 8,
                        "name": "SDtest0011",
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 719719000
                        },
                        "display_name": "d7f27e807b3611f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783564798,
                        "nanos": 417949000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 8,
                                "name": "SDtest0011",
                                "created_at": {
                                        "seconds": 1783561734,
                                        "nanos": 719719000
                                },
                                "display_name": "d7f27e807b3611f19afb21f67909a124"
                        },
                        "id": 8,
                        "key": "e769e10ee1fbfe9d3288bfba75ba17e8a0e8a5900ec4504c",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783676934,
                                "nanos": 867000000
                        },
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 945894000
                        }
                },
                "created_at": {
                        "seconds": 1783564693,
                        "nanos": 843417000
                },
                "register_method": 1,
                "given_name": "jn002026s0011006",
                "approved_routes": [
                        "fd7a:115c:a1e0:b1a:0:b:c0a8:400/120"
                ],
                "available_routes": [
                        "fd7a:115c:a1e0:b1a:0:b:c0a8:400/120"
                ],
                "subnet_routes": [
                        "fd7a:115c:a1e0:b1a:0:b:c0a8:400/120"
                ]
        },
        {
                "id": 16,
                "machine_key": "mkey:6c4f53c9ce7c4875be14d05617af948a5da5c45b8ad7eb542d83885b4175351c",
                "node_key": "nodekey:6cfcf14a6bfb10cc6f35a914f481d8430df16b91dc70404037d50b42be1a835e",
                "disco_key": "discokey:65cbc9f5f06c6ead3f80caaa2e50f76ef1f5cd2f449cb97cf7ed38ad613de96c",
                "ip_addresses": [
                        "100.65.0.10",
                        "fd7a:115c:a1e0::a"
                ],
                "name": "JN002026S0011005",
                "user": {
                        "id": 8,
                        "name": "SDtest0011",
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 719719000
                        },
                        "display_name": "d7f27e807b3611f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783564798,
                        "nanos": 374742000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 8,
                                "name": "SDtest0011",
                                "created_at": {
                                        "seconds": 1783561734,
                                        "nanos": 719719000
                                },
                                "display_name": "d7f27e807b3611f19afb21f67909a124"
                        },
                        "id": 8,
                        "key": "e769e10ee1fbfe9d3288bfba75ba17e8a0e8a5900ec4504c",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783676934,
                                "nanos": 867000000
                        },
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 945894000
                        }
                },
                "created_at": {
                        "seconds": 1783564693,
                        "nanos": 843694000
                },
                "register_method": 1,
                "given_name": "jn002026s0011005",
                "approved_routes": [
                        "fd7a:115c:a1e0:b1a:0:a:c0a8:400/120"
                ],
                "available_routes": [
                        "fd7a:115c:a1e0:b1a:0:a:c0a8:400/120"
                ],
                "subnet_routes": [
                        "fd7a:115c:a1e0:b1a:0:a:c0a8:400/120"
                ]
        },
        {
                "id": 17,
                "machine_key": "mkey:c5f4cdfce267825c17498b7830b2d0348f2abe225e5c417e4e4f6c12837ae359",
                "node_key": "nodekey:7492f1d9fa931763017cc03d93416a0d5c577bbcab75a80267a77a318a77721c",
                "disco_key": "discokey:80368327c06ad3b3444ac45624da4bc69255ebffbdebb5e1f7e07b3466028c71",
                "ip_addresses": [
                        "100.65.0.11",
                        "fd7a:115c:a1e0::b"
                ],
                "name": "JN002026S0011008",
                "user": {
                        "id": 8,
                        "name": "SDtest0011",
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 719719000
                        },
                        "display_name": "d7f27e807b3611f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783564798,
                        "nanos": 418109000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 8,
                                "name": "SDtest0011",
                                "created_at": {
                                        "seconds": 1783561734,
                                        "nanos": 719719000
                                },
                                "display_name": "d7f27e807b3611f19afb21f67909a124"
                        },
                        "id": 8,
                        "key": "e769e10ee1fbfe9d3288bfba75ba17e8a0e8a5900ec4504c",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783676934,
                                "nanos": 867000000
                        },
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 945894000
                        }
                },
                "created_at": {
                        "seconds": 1783564694,
                        "nanos": 97607000
                },
                "register_method": 1,
                "given_name": "jn002026s0011008",
                "approved_routes": [
                        "fd7a:115c:a1e0:b1a:0:d:c0a8:400/120"
                ],
                "available_routes": [
                        "fd7a:115c:a1e0:b1a:0:d:c0a8:400/120"
                ],
                "subnet_routes": [
                        "fd7a:115c:a1e0:b1a:0:d:c0a8:400/120"
                ]
        },
        {
                "id": 18,
                "machine_key": "mkey:352cb581247b8f501ae2a34d262af8eee3c5eebe8183c3e9b7ea4c4ef0b9dc4c",
                "node_key": "nodekey:4b8d5a7dcb887b32b6f487e04331d70a613f988b46f4e92cc89954491381e00d",
                "disco_key": "discokey:a7ca8f2f6e379a295f50574c690122ad475ddc75137ac706ed1a4cbe6e8b3963",
                "ip_addresses": [
                        "100.65.0.12",
                        "fd7a:115c:a1e0::c"
                ],
                "name": "JN002026S0011009",
                "user": {
                        "id": 8,
                        "name": "SDtest0011",
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 719719000
                        },
                        "display_name": "d7f27e807b3611f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783564798,
                        "nanos": 418455000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 8,
                                "name": "SDtest0011",
                                "created_at": {
                                        "seconds": 1783561734,
                                        "nanos": 719719000
                                },
                                "display_name": "d7f27e807b3611f19afb21f67909a124"
                        },
                        "id": 8,
                        "key": "e769e10ee1fbfe9d3288bfba75ba17e8a0e8a5900ec4504c",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783676934,
                                "nanos": 867000000
                        },
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 945894000
                        }
                },
                "created_at": {
                        "seconds": 1783564694,
                        "nanos": 310589000
                },
                "register_method": 1,
                "given_name": "jn002026s0011009",
                "approved_routes": [
                        "fd7a:115c:a1e0:b1a:0:e:c0a8:400/120"
                ],
                "available_routes": [
                        "fd7a:115c:a1e0:b1a:0:e:c0a8:400/120"
                ],
                "subnet_routes": [
                        "fd7a:115c:a1e0:b1a:0:e:c0a8:400/120"
                ]
        },
        {
                "id": 19,
                "machine_key": "mkey:8a9331699d71ff9cd3b9a8cd217d3ae05c75de402a2cdffac4d7173d37aacf04",
                "node_key": "nodekey:f8244c97725fd658f303efe0d129d7d913b97682a1a1dfb71cc4ce1f05cf0c55",
                "disco_key": "discokey:0e34944cf059be2452b26b148733e90b0fb142dc817b982d0eb605cd31c14069",
                "ip_addresses": [
                        "100.65.0.13",
                        "fd7a:115c:a1e0::d"
                ],
                "name": "JN002026S0011007",
                "user": {
                        "id": 8,
                        "name": "SDtest0011",
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 719719000
                        },
                        "display_name": "d7f27e807b3611f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783564798,
                        "nanos": 416773000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 8,
                                "name": "SDtest0011",
                                "created_at": {
                                        "seconds": 1783561734,
                                        "nanos": 719719000
                                },
                                "display_name": "d7f27e807b3611f19afb21f67909a124"
                        },
                        "id": 8,
                        "key": "e769e10ee1fbfe9d3288bfba75ba17e8a0e8a5900ec4504c",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783676934,
                                "nanos": 867000000
                        },
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 945894000
                        }
                },
                "created_at": {
                        "seconds": 1783564697,
                        "nanos": 428502000
                },
                "register_method": 1,
                "given_name": "jn002026s0011007",
                "approved_routes": [
                        "fd7a:115c:a1e0:b1a:0:c:c0a8:400/120"
                ],
                "available_routes": [
                        "fd7a:115c:a1e0:b1a:0:c:c0a8:400/120"
                ],
                "subnet_routes": [
                        "fd7a:115c:a1e0:b1a:0:c:c0a8:400/120"
                ]
        },
        {
                "id": 29,
                "machine_key": "mkey:e37f3d64a5624b683f568d6b2b387279fc132dbda4734f9ed941d6e7b7c89c5f",
                "node_key": "nodekey:eaabed663f5c4ebe2f789c88b387d2190b429cb997ebc4c06d495063a047643c",
                "disco_key": "discokey:d49351e0c79a6a5642d2ed83af783d7417bad930fe4ef438e71984c342a0de21",
                "ip_addresses": [
                        "100.65.0.31",
                        "fd7a:115c:a1e0::1f"
                ],
                "name": "JN002026S0011000",
                "user": {
                        "id": 8,
                        "name": "SDtest0011",
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 719719000
                        },
                        "display_name": "d7f27e807b3611f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783572912,
                        "nanos": 777442000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 8,
                                "name": "SDtest0011",
                                "created_at": {
                                        "seconds": 1783561734,
                                        "nanos": 719719000
                                },
                                "display_name": "d7f27e807b3611f19afb21f67909a124"
                        },
                        "id": 8,
                        "key": "e769e10ee1fbfe9d3288bfba75ba17e8a0e8a5900ec4504c",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783676934,
                                "nanos": 867000000
                        },
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 945894000
                        }
                },
                "created_at": {
                        "seconds": 1783571730,
                        "nanos": 520388000
                },
                "register_method": 1,
                "given_name": "jn002026s0011000",
                "approved_routes": [
                        "fd7a:115c:a1e0:b1a:0:5:c0a8:400/120"
                ],
                "available_routes": [
                        "fd7a:115c:a1e0:b1a:0:5:c0a8:400/120"
                ]
        },
        {
                "id": 31,
                "machine_key": "mkey:1a4297cee3e3edb589fa7dd363be0cd9ced9a86effbed805c213b6d44fdcfe56",
                "node_key": "nodekey:1c6a8060217fd7ff9c1478644fbf71f72a04b4f88b5c75834a14b3f521ca3a36",
                "disco_key": "discokey:68034249bd2dd9a4f49d92141ead69a55c7febc25db18904c9998d578b5d3274",
                "ip_addresses": [
                        "100.65.0.33",
                        "fd7a:115c:a1e0::21"
                ],
                "name": "DESKTOP-QRCDS29",
                "user": {
                        "id": 8,
                        "name": "SDtest0011",
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 719719000
                        },
                        "display_name": "d7f27e807b3611f19afb21f67909a124"
                },
                "last_seen": {
                        "seconds": 1783574023,
                        "nanos": 481309000
                },
                "pre_auth_key": {
                        "user": {
                                "id": 8,
                                "name": "SDtest0011",
                                "created_at": {
                                        "seconds": 1783561734,
                                        "nanos": 719719000
                                },
                                "display_name": "d7f27e807b3611f19afb21f67909a124"
                        },
                        "id": 8,
                        "key": "e769e10ee1fbfe9d3288bfba75ba17e8a0e8a5900ec4504c",
                        "reusable": true,
                        "expiration": {
                                "seconds": 1783676934,
                                "nanos": 867000000
                        },
                        "created_at": {
                                "seconds": 1783561734,
                                "nanos": 945894000
                        }
                },
                "created_at": {
                        "seconds": 1783572157,
                        "nanos": 52441000
                },
                "register_method": 1,
                "given_name": "desktop-qrcds29",
                "online": true
        }
]
/ #
 

7)MQTT消息202和203

以下是 cmd=202(ENABLE_HEADSCALE_ROUTER)的完整处理流程:

处理链路

详细处理步骤

HeadscaleRouterProcessor.java 的 process() 方法:

1. 提取消息字段

  • 从 message.getRouter() 获取路由字符串(如 192.168.1.0/24)
  • 从 message.getSn() 获取设备 SN

2. 校验设备与服务

  • 根据 SN 查询设备信息 AcsUserDevice
  • 根据设备 userId 查询 AcsHeadscaleUser

3. 校验 siteId

  • 从设备获取 siteId,必须非空且有效(≥1 且 ≤4095)

4. 校验 IPv4 CIDR 格式

  • TailscaleIPv6Utils.isValidIPv4CIDR(router) 验证路由是否为合法 CIDR

5. IPv4 → IPv6 转换

  • TailscaleIPv6Utils.convertToIPv6Subnet(router, siteId) 将 IPv4 子网转为 Tailscale 4via6 格式

        例如:192.168.1.0/24 + siteId=4 → fd7a:115c:a1e0:b1a:0:4:c0a8:100/120
6. 更新 ACL(核心) updateACLWithIPv6Route(ipv6Subnet, displayName):

  • 获取 Redis 分布式锁(防止并发)
  • 拉取当前 ACL Policy
  • 检查是否已存在规则:src=["group:<displayName>"] + dst=["<ipv6Subnet>:*"]
  • 不存在则追加:{"action":"accept", "src":["group:<displayName>"], "dst":["<ipv6Subnet>:*"]}
  • 写回 Headscale

7. 发送响应

  • 成功 → sendRouterResponse(sn, router, 0)
  • 失败 → sendRouterResponse(sn, router, 1)

四、DERP服务器部署

docker run --restart always --name derper -p 12345:12345 -p 3478:3478/udp -v /home/headscale/cert/:/app/certs -e DERP_CERT_MODE=manual -e DERP_ADDR=:12345 -e DERP_DOMAIN=bnest.jetrun.com.cn -d ghcr.io/yangchuansheng/derper:latest

搭建脚本

# 创建 Headscale 部署目录

sudo mkdir -p /home/headscale/container-config

sudo mkdir -p /home/headscale/container-data/data

sudo mkdir -p /home/headscale/certs

sudo mkdir -p /home/headscale/nginx

# 设置目录权限

sudo chmod -R 755 /home/headscale

网关上放的证书是wildcard证书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李小白20200202

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值