一、如何判断一个crt证书是不是根证书?
核心判断依据是以下三个条件的 “与(AND)” 关系:
-
颁发者(Issuer)== 使用者(Subject)(自签名)。
-
基本约束(Basic Constraints) 中标记为
CA:TRUE。 -
密钥用法(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 证书管理器给出的“不安全”或“配置不完整”的警告提示,并不代表证书本身格式错误或失效。它主要是由以下两个原因触发的:
-
证书未加入 Windows 信任存储区
由于你是在本地生成的自签名根证书,默认情况下 Windows 不会将其自动放入“受信任的根证书颁发机构”存储区,因此系统会通过黄色叹号提示你:“此证书不受当前系统信任”。 -
缺少部分可选扩展字段
虽然你已经设置了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: postgresgorm:
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.htmlcurl 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 根证书 生成的,有三个致命缺陷:
CA:TRUE— 标记为 CA 证书- 没有
serverAuth扩展密钥用法 - 没有 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() 会:
- 检查 gRPC channel 状态
- 调用
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证书


913

被折叠的 条评论
为什么被折叠?



