背景

在很多工厂的实际网络环境中,由于历史原因,经常会出现多个子网IP地址重复(子网重叠)的问题。但由于工业生产的特殊性,我们无法对工厂现有网络进行大规模改造——一旦改动网络,可能导致生产线停机,造成严重损失。

此外,许多工业设备(如PLC、传感器等)自带TCP/IP功能,其IP地址是固定的,无法修改;部分设备端口也固定,且仅支持IPv4。这些限制进一步加剧了网络管理的难度。

当设备出现故障需要诊断时,工程师通常必须亲临现场。如果工厂距离研发中心上千公里,在生产高峰期派人出差,不仅耗时耗力,还会影响生产效率。

目前虽然有一些组网方案,但效果并不理想。之前我们一直使用传统VPN解决设备远程维护的问题,但发现无法让所有子网中的设备同时在线,通信受限,实用性无法达到预期效果。而且很多工程师习惯用原始IP(如192.168.1.3)直接访问设备,不喜欢记虚拟地址或复杂域名,操作体验差。

子网重叠

解决

为了解决这些问题,我花了数月时间研究并实现了一套可行方案,满足以下核心需求:

  • 不改变工厂原有网络结构:无需调整现场设备IP或网络配置,避免影响生产。

  • 全设备在线、互联互通:所有子网设备均可接入,按需通信,也可灵活隔离。

  • 集中数据采集:只需在一台上位机安装采集软件,即可获取所有设备的数据。

  • 使用原IP远程访问:工程师可直接通过设备原有的IP地址(如192.168.1.3 或 127.0.0.1)进行远程连接和程序调试。

  • 支持相同IP设备区分访问:即使两个不同工厂都有IP为192.168.1.8的设备,也能准确指定并连接目标设备,且不影响其他设备的数据采集。

  • 贴合工程师操作习惯:尽量保持现有操作方式,降低学习成本,提升使用效率。

经过反复测试,该方案已在工业网关(带数据采集功能)和用户电脑上成功验证并使用,支持国内与海外设备的组网需求。相比早期的VPN方案,现已升级为基于 WireGuard + 自研技术 的组合方案,有效解决了子网重叠和原IP远程访问的核心痛点。

如您所在企业正面临类似问题,欢迎联系沟通,可提供完整解决方案及技术支持(包括方案购买、部署协助等)。

说明

本文介绍如何使用 headscaleDNS 功能。当用户使用tailscale 客户端登入后,除了可以使用 IP 进行设备互访外,还可以使用 主机名 进行设备通信。
windows 系统中,tailscale客户端登入后会在 C:\Windows\System32\drivers\etc 目录下 hosts 文件中写入 设备IP 以及 主机名,这样用户就可以使用主机名或MagicDNS进行设备互访。

功能开启

config.yaml 中,修改 dns 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
dns:
# Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
magic_dns: true

# Defines the base domain to create the hostnames for MagicDNS.
# This domain _must_ be different from the server_url domain.
# `base_domain` must be a FQDN, without the trailing dot.
# The FQDN of the hosts will be
# `hostname.base_domain` (e.g., _myhost.example.com_).
base_domain: example.com

# Whether to use the local DNS settings of a node (default) or override the
# local DNS settings and force the use of Headscale's DNS configuration.
override_local_dns: false

# List of DNS servers to expose to clients.
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
- 2606:4700:4700::1111
- 2606:4700:4700::1001

# NextDNS (see https://tailscale.com/kb/1218/nextdns/).
# "abc123" is example NextDNS ID, replace with yours.
# - https://dns.nextdns.io/abc123

# Split DNS (see https://tailscale.com/kb/1054/dns/),
# a map of domains and which DNS server to use for each.
split:
{}
# foo.bar.com:
# - 1.1.1.1
# darp.headscale.net:
# - 1.1.1.1
# - 8.8.8.8

# Set custom DNS search domains. With MagicDNS enabled,
# your tailnet base_domain is always the first search domain.
search_domains: []

# Extra DNS records
# so far only A and AAAA records are supported (on the tailscale side)
# See: docs/ref/dns.md
# extra_records: []
# - name: "grafana.myvpn.example.com"
# type: "A"
# value: "100.64.0.3"
#
# # you can also put it in one line
# - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
#
# Alternatively, extra DNS records can be loaded from a JSON file.
# Headscale processes this file on each change.
extra_records_path: /var/lib/headscale/extra-records.json

注释掉 extra_records: [] ,然后取消 extra_records_path 注释,使用 extra-records.json 文件作为扩展方便 headscale 发现 dns 变化。

extra-records.json 内容如下:

1
2
3
4
5
6
7
[
{
"name": "192-168-6-1-via-7",
"type": "AAAA",
"value": "fd7a:115c:a1e0:b1a:0:7:c0a8:601"
}
]

type 支持 AAAAAA 表示 ipv4,AAAA 表示 ipv6。
此项功能在开启 4via6 功能后特别好用,因为不需要去记 ipv6 地址,方便设备访问。

比如我有一台设备的 IP 是 fd7a:115c:a1e0:b1a:0:7:c0a8:601 ,这串 ipv6 地址很长不好记,那么我就可以用它对应的 ipv4 地址 作为 dns 信息。

注意 第一次修改 extra_records 配置时请先停止 headscale ,修改完后再重启。后续只需要修改 extra-records.json 即可,无需重启 headscale

使用

tailscale 客户端登入后,系统路由表中新增相关 headscale 服务端的路由。

路由

使用 192-168-6-1-via-7 访问 设备服务。

dns使用

Headscale官方的镜像大小使用docker images查看时发现只有 80M,如果我们自己使用 Dockerfile 打包镜像,发现会很大。

但是源码中给的Dockerfile文件都没法实现官方镜像的大小(毕竟是开发环境打包的),然后我就去找官方镜像是怎么打包的,在dockerhub里发现ko工具的身影,所以就尝试用ko打包headscale镜像。

ko 是一个用于构建和打包 Go 应用程序的轻量级工具,特别适用于容器化环境(如 DockerKubernetes),目标是简化 Go 程序构建为容器镜像的过程,无需编写 Dockerfile

ko 安装

我使用的 Ubuntu24.04 系统,在普通用户下安装了 go 程序。
使用 go 安装 ko

1
go install github.com/google/ko@latest

安装后,在用户目录下可以找到ko文件,我的电脑在 /home/Users/go/bin/ko 位置。

ko 打包

在源码目录下运行命令:

1
2
3
4
# --local 表示不推送镜像到仓库
/home/djc/go/bin/ko build --local ./cmd/headscale

# 打包完成后会出现 ko.local 开头的镜像
1
2
3
4
5
6
7
8
9
# 打包完成后,使用 docker images 即可查看镜像
djc@jetron-djc:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
headscale v0.26.1-r bf66da388ca1 6 days ago 87.5MB
ko.local/headscale-f40b3d8640713cd381403459ebd67e78 38aefca56cab7d9b11692c61968915fb59fdf1dce134e52fed02ae2fa3a0e871 bf66da388ca1 6 days ago 87.5MB
ko.local/headscale-f40b3d8640713cd381403459ebd67e78 latest bf66da388ca1 6 days ago 87.5MB
ghcr.io/juanfont/headscale v0.26.1 b9e7b75fd3b0 N/A 80.8MB

# 后面使用 docker tag 可重命名镜像名称

镜像运行

程序运行需要config.yaml配置文件,以及/var/run/headscale//var/lib/headscale/ 目录的读取写入权限。

1
2
3
4
5
6
7
8
# 可在 config.yaml 中将
unix_socket: /var/run/headscale/headscale.sock

# 修改成
unix_socket: /var/lib/headscale/headscale.sock

# 然后只需要挂载 /var/lib/headscale/ 目录即可运行
docker run -d -v .headscale/config.yaml:/etc/headscale/config.yaml -v ./doc:/var/lib/headscale --name headscale -p 8080:8080 -p 9090:9090 headscale:v0.26.1-r serve

参考

源码编译:https://ownding.com/2025/07/30/%E4%BD%BF%E7%94%A8Headscale%E6%BA%90%E7%A0%81%E8%87%AA%E8%A1%8C%E7%BC%96%E8%AF%91%E6%89%93%E5%8C%85/

方法介绍

有两种方法能够获取到EMQX的设备是否在线:

  • 使用 EMQX 提供的 REST API,通过API获取设备状态
  • 订阅EMQX的系统主题,当设备上线、下线时通过该主题都会收到设备上下线消息

订阅系统主题

EMQ X提供上下线状态主题:
上线主题:$SYS/brokers/<node>/clients/<clientid>/connected

下线主题:$SYS/brokers/<node>/clients/<clientid>/disconnected

1)其中 <node><clientid> 分别指定具体节点名、设备ClientID

2)支持 +#通配符

通配符模式主题: $SYS/brokers/+/clients/+/+

示例:

  • 1、配置ACL规则,可使用ip也可以用username,使得某个用户或IP能够订阅系统主题。

ACL

ACL1

  • 2、订阅主题。 我使用的是MQTTX工具进行订阅模拟。

sub

说明

本文说明在 Linux 系统下使用 tailscale 客户端 登入 http 协议的 headscale
Tailscale 客户端默认使用 https 协议。

  • Ubuntu 24.04 LTS
  • headscale v0.26.1
  • tailscale 客户端版本 1.86.2

tailscale客户端安装

1
2
3
4
5
6
7
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null

curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list

sudo apt-get update

sudo apt-get install tailscale

问题

linux(ubuntu 24.04)系统中安装了 tailscale 客户端,但是程序启动后,再登入自定的 headscale 服务端(如果是 http,没有ssl证书)会一直卡在登入环节。

Tailscale 客户端登入信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Aug 06 08:00:42 ding tailscaled[828]: Received error: register request: Post "https://124.232.182.156:8081/machine/register": connection attempts aborted by context: context deadline exceeded 
Aug 06 08:00:43 ding tailscaled[828]: control: doLogin(regen=false, hasUrl=false)
Aug 06 08:00:43 ding tailscaled[828]: control: control server key from http://124.232.182.156:8081: ts2021=[QNt10], legacy=
Aug 06 08:00:43 ding tailscaled[828]: control: RegisterReq: onode= node=[V20sD] fup=false nks=false
Aug 06 08:00:43 ding tailscaled[828]: control: controlhttp: forcing port 443 dial due to recent noise dial
Aug 06 08:00:53 ding tailscaled[828]: Received error: register request: Post "https://124.232.182.156:8081/machine/register": connection attempts aborted by context: context deadline exceeded
Aug 06 08:00:54 ding tailscaled[828]: control: doLogin(regen=false, hasUrl=false)
Aug 06 08:00:54 ding tailscaled[828]: control: control server key from http://124.232.182.156:8081: ts2021=[QNt10], legacy=
Aug 06 08:00:54 ding tailscaled[828]: control: RegisterReq: onode= node=[V20sD] fup=false nks=false
Aug 06 08:00:54 ding tailscaled[828]: control: controlhttp: forcing port 443 dial due to recent noise dial
Aug 06 08:01:04 ding tailscaled[828]: Received error: register request: Post "https://124.232.182.156:8081/machine/register": connection attempts aborted by context: context deadline exceeded
Aug 06 08:01:05 ding tailscaled[828]: control: doLogin(regen=false, hasUrl=false)
Aug 06 08:01:05 ding tailscaled[828]: control: control server key from http://124.232.182.156:8081: ts2021=[QNt10], legacy=
Aug 06 08:01:05 ding tailscaled[828]: control: RegisterReq: onode= node=[V20sD] fup=false nks=false
Aug 06 08:01:05 ding tailscaled[828]: control: controlhttp: forcing port 443 dial due to recent noise dial
Aug 06 08:01:15 ding tailscaled[828]: Received error: register request: Post "https://124.232.182.156:8081/machine/register": connection attempts aborted by context: context deadline exceeded
Aug 06 08:01:16 ding tailscaled[828]: control: doLogin(regen=false, hasUrl=false)
Aug 06 08:01:16 ding tailscaled[828]: control: control server key from http://124.232.182.156:8081: ts2021=[QNt10], legacy=
Aug 06 08:01:16 ding tailscaled[828]: control: RegisterReq: onode= node=[V20sD] fup=false nks=false
Aug 06 08:01:16 ding tailscaled[828]: control: controlhttp: forcing port 443 dial due to recent noise dial
Aug 06 08:01:24 ding kernel: workqueue: drain_vmap_area_work hogged CPU for >10000us 4 times, consider switching to WQ_UNBOUND
Aug 06 08:01:26 ding tailscaled[828]: Received error: register request: Post "https://124.232.182.156:8081/machine/register": connection attempts aborted by context: context deadline exceeded
Aug 06 08:01:28 ding tailscaled[828]: control: doLogin(regen=false, hasUrl=false)
Aug 06 08:01:28 ding tailscaled[828]: control: control server key from http://124.232.182.156:8081: ts2021=[QNt10], legacy=
Aug 06 08:01:28 ding tailscaled[828]: control: RegisterReq: onode= node=[V20sD] fup=false nks=false
Aug 06 08:01:28 ding tailscaled[828]: control: controlhttp: forcing port 443 dial due to recent noise dial
Aug 06 08:01:38 ding tailscaled[828]: Received error: register request: Post "https://124.232.182.156:8081/machine/register": connection attempts aborted by context: context deadline exceeded
Aug 06 08:01:40 ding tailscaled[828]: control: doLogin(regen=false, hasUrl=false)
Aug 06 08:01:40 ding tailscaled[828]: control: control server key from http://124.232.182.156:8081: ts2021=[QNt10], legacy=
Aug 06 08:01:40 ding tailscaled[828]: control: RegisterReq: onode= node=[V20sD] fup=false nks=false
Aug 06 08:01:40 ding tailscaled[828]: control: controlhttp: forcing port 443 dial due to recent noise dial
Aug 06 08:01:50 ding tailscaled[828]: Received error: register request: Post "https://124.232.182.156:8081/machine/register": connection attempts aborted by context: context deadline exceeded
Aug 06 08:01:52 ding tailscaled[828]: control: doLogin(regen=false, hasUrl=false)
Aug 06 08:01:52 ding tailscaled[828]: control: control server key from http://124.232.182.156:8081: ts2021=[QNt10], legacy=
Aug 06 08:01:52 ding tailscaled[828]: control: RegisterReq: onode= node=[V20sD] fup=false nks=false
Aug 06 08:01:52 ding tailscaled[828]: control: controlhttp: forcing port 443 dial due to recent noise dial

可以看到登入日志中出现了 https://124.232.182.156:8081tailscale 客户端会访问 https 的服务器,显然是无法访问成功的,因为服务器就没有配置 ssl

解决

删除 /var/lib/tailscale/tailscaled.state 文件,然后重新登入就可以了。

介绍

最近在做一个Headscale自动批准路由的功能,希望设备宣告路由后服务器端就能自动批准路由。经常一番查找,发现headscaleacl中支持autoApprovers,即当你配置你相关的acl时可以自动批准路由。
不过这还不能满足要求,因为我发现它不支持通配符,为了安全考虑只能每个路由配置一次。
本文会介绍如何在 headscale 源码中修改相关代码,让autoApprovers支持通配符路由批准设置(请确认你确实需要这个功能,可能带来安全问题)。

headscale 中的 autoApprovers 使用

具体配置及使用可参照官网链接:https://headscale.net/development/ref/routes/#automatically-approve-routes-of-a-subnet-router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 示例配置
{
"tagOwners": {
"tag:router": ["alice@"]
},
"autoApprovers": {
"routes": {
"192.168.0.0/24": ["tag:router"]
}
},
"acls": [
// more rules
]
}

示例配置中 192.168.0.0/24tag:router 均会自动批准。

但是有一些设备或应用场景中希望所有的子路由均自动批准,那么现有的程序或 acl 配置无法满足要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 我期望的配置,但是原版程序中无法实现
{
"tagOwners": {
"tag:router": ["alice@"]
},
"autoApprovers": {
"routes": {
"*": ["*"]
}
},
"acls": [
// more rules
]
}

源码修改

为了实现自动批准所有路由的功能,需要修改源码。
本文只介绍autoApprovers源码修改的一部分内容,代码不会全部公开。

  • 软件版本:v0.26.1

修改源码涉及的文件有 hscontrol/policy/v2/types.gohscontrol/policy/v2/policy.gohscontrol/policy/v1/acls_types.go 等。

其中 acls_types.go 中有 func (autoApprovers *AutoApprovers) GetRouteApprovers 函数,修改的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// GetRouteApprovers returns the list of autoApproving users, groups or tags for a given IPPrefix.
func (autoApprovers *AutoApprovers) GetRouteApprovers(
prefix netip.Prefix,
) ([]string, error) {
if prefix.Bits() == 0 {
return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent
}

approverAliases := make([]string, 0)

// Check for wildcard route approval first
// Wildcard "*" approves all non-exit routes
if wildcardApprovers, exists := autoApprovers.Routes["*"]; exists {
approverAliases = append(approverAliases, wildcardApprovers...)
}

for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes {
// Skip wildcard entry as it's already handled above
if autoApprovedPrefix == "*" {
continue
}

autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix)
if err != nil {
return nil, err
}

if prefix.Bits() >= autoApprovedPrefix.Bits() &&
autoApprovedPrefix.Contains(prefix.Masked().Addr()) {
approverAliases = append(approverAliases, autoApproverAliases...)
}
}

return approverAliases, nil
}

以上只是示例,具体修改还需要结合版本及需求进行。

说明

本文介绍如何自行打包编译 Headscale 源码,以及在编译源码的过程中可能出现的问题以及解决方法。

  • 操作系统: Windows 11 家庭中文版;WSL 2 Ubuntu24.04 LTS 系统。 本次编译源码使用了 wsl 2安装的 Ubutnu 24系统。
  • 源码目录: C:\Users\XXX\Documents\develop\0me\headscale 。 源码放在 Winodws系统中。
  • 源码下载: https://github.com/juanfont/headscale
  • 程序版本: v0.26.1

环境准备

代码下载

推荐使用 git 下载,而不是 下载 压缩包的源码。我使用压缩包的源码进行编译的时候会出现很多错误,最后是通过go build -o headscale ./cmd/headscale 编译了一个可执行文件,但是无法通过 make build 编译。

使用下面的命令下载源码:

1
2
3
4
git clone https://github.com/juanfont/headscale.git

# 使用 v0.26.1 的 tag
git checkout -b release-v0.26.1 v0.26.1

然后复制根目录下的 config-example.yaml 重命名为 config.yaml

WSL

开启 WSL,并且安装 Ubuntu系统。

在当前计算机用户目录下,比如 C:\Users\XXX ,创建一个 .wslconfig 的文件,里面填入:

1
2
3
4
5
6
7
8
9
[wsl2]
nestedVirtualization=true
ipv6=true
[experimental]
autoMemoryReclaim=gradual # gradual | dropcache | disabled
networkingMode=mirrored
dnsTunneling=true
firewall=true
autoProxy=true

然后重启 wsl,这样 Ubuntu 系统就可以使用你本机的VPN网络了。 因为编译时需要下载一堆东西,没有外网VPN无法下载成功。

Nix安装

使用 root 账号安装 nixMulti-user 版本。

1
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon

参考链接:https://nixos.org/download/

编译

编译时请从 root 账号切换到普通账号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 进入目录。windows电脑的目录在 wsl系统中 /mnt/c/ 目录下
cd /mnt/c/Users/XXX/Documents/develop/0me/headscale

# 准备环境。
nix develop

make generate
make test
make build

# 查看结果
ls -la result/

# 检查版本
cd ./result/bin
./headscale version # 会输出版本号

环境准备需要挺久,耐心等待。

make build 执行成功后,目录中会出现一个 result 目录(在windows系统中查看就是一个 0kb 大小的文件,实际在linux中是一个目录。

r
wr

运行

在开发机器上编译的 headscale 文件可直接运行,下面介绍将 headscale 文件复制到其它服务器上运行。

  • 运行服务器: Ubuntu 22.04 Server
1
2
3
4
5
6
7
8
9
10
# 检查文件 使用 ldd 或 file 命令
ubuntu@VM-16-7-ubuntu:~/doc/headscale$ ldd headscale
linux-vdso.so.1 (0x00007ffc991a1000)
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x000079e2a91e5000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x000079e2a91e0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000079e2a8e00000)
/nix/store/vbrdc5wgzn0w1zdp10xd2favkjn5fk7y-glibc-2.40-66/lib/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x000079e2a9201000)
ubuntu@VM-16-7-ubuntu:~/doc/headscale$ file headscale
headscale: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /nix/store/vbrdc5wgzn0w1zdp10xd2favkjn5fk7y-glibc-2.40-66/lib/ld-linux-x86-64.so.2, stripped
ubuntu@VM-16-7-ubuntu:~/doc/headscale$

从上面控制台输出我们可以发现,headscale 运行时需要 /nix/store/vbrdc5wgzn0w1zdp10xd2favkjn5fk7y-glibc-2.40-66/lib/ld-linux-x86-64.so.2 ,该文件其实是链接的 /lib64/ld-linux-x86-64.so.2 ,要想 程序正常运行,需要手动创建 /nix/store/vbrdc5wgzn0w1zdp10xd2favkjn5fk7y-glibc-2.40-66/lib/ 目录,并且将 ld-linux-x86-64.so.2 文件复制到刚创建的文件夹中。

此外,需要 config.yaml 文件,以及创建 /root/.headscale//varlib/headscale/ 并且需要给这些目录赋予权限。

1
2
3
4
5
6
7
8
9
10
11
12
# 启动命令 headscale serve

ubuntu@VM-16-7-ubuntu:~/doc/headscale$ sudo ./headscale serve
2025-08-05T17:20:01+08:00 INF Opening database database=sqlite3 path=/var/lib/headscale/db.sqlite
2025-08-05T17:20:01+08:00 INF Using policy manager version: 2
2025-08-05T17:20:01+08:00 INF Starting Headscale commit=6b6daf389bd11624c4036de525740a0568d5f72f-dirty version=6b6daf3-dirty
2025-08-05T17:20:01+08:00 INF Clients with a lower minimum version will be rejected minimum_version=v1.62.0
2025-08-05T17:20:01+08:00 INF github.com/juanfont/headscale/hscontrol/derp/server/derp_server.go:106 > DERP region: {RegionID:999 RegionCode:headscale RegionName:Headscale Embedded DERP Latitude:0 Longitude:0 Avoid:false NoMeasureNoHome:false Nodes:[0xc00051a090]}
2025-08-05T17:20:01+08:00 INF github.com/juanfont/headscale/hscontrol/derp/server/derp_server.go:107 > DERP Nodes[0]: &{Name:999 RegionID:999 HostName:124.232.181.156 CertName: IPv4:124.232.181.156 IPv6:2406:da18:d4c:c000:8d2b:1775:f73f:7c2f STUNPort:3478 STUNOnly:false DERPPort:8081 InsecureForTests:false STUNTestIP: CanPort80:false}
2025-08-05T17:20:01+08:00 INF STUN server started at [::]:3478
2025-08-05T17:20:01+08:00 INF listening and serving HTTP on: 0.0.0.0:8080
2025-08-05T17:20:01+08:00 INF listening and serving debug and metrics on: 0.0.0.0:9090

打包成镜像

如果你想将源码打包成 docker 镜像,可以参考下面的链接:

https://ownding.com/2025/08/08/headscale%E7%B3%BB%E5%88%97%EF%BC%9A%E5%A6%82%E4%BD%95%E5%B0%86%E6%BA%90%E7%A0%81%E7%BC%96%E8%AF%91%E7%9A%84headscale%E6%89%93%E5%8C%85%E6%88%90docker%E9%95%9C%E5%83%8F/

编译异常汇总

以下编译的异常情况都是我使用 压缩的源代码编译出现的。使用 git 版本的源码编译没有如下问题。

问题1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 按照官方的编译步骤,使用 nix 编译。 make test 编译成功,但是 make build 出现如下错误:
djc@jetron-djc:/mnt/c/Users/DJC/Documents/develop/0me/headscale-0.26.1$ make build
nix build
error:
while calling the 'derivationStrict' builtin
at <nix/derivation-internal.nix>:37:12:
36|
37| strict = derivationStrict drvAttrs;
| ^
38|

while evaluating the derivation attribute 'name'
at /nix/store/qmm7hgw60vp7vj9lma95hl329d0j3n6n-source/pkgs/stdenv/generic/make-derivation.nix:438:13:
437| // (optionalAttrs (attrs ? name || (attrs ? pname && attrs ? version)) {
438| name =
| ^
439| let

(stack trace truncated; use '--show-trace' to show the full, detailed trace)

error: attribute 'dirtyShortRev' missing
at /nix/store/wrx2gzxp6f5sdha4kswnpn6j8sqmfbnk-source/flake.nix:15:41:
14| }: let
15| headscaleVersion = self.shortRev or self.dirtyShortRev;
| ^
16| commitHash = self.rev or self.dirtyRev;
make: *** [Makefile:20: build] Error 1
  • 解决方法:

编辑 flake.nix 文件,

headscaleVersion = self.shortRev or self.dirtyShortRev; 修改成:

1
2
3
4
5
headscaleVersion = if self ? shortRev 
then self.shortRev
else if self ? dirtyShortRev
then self.dirtyShortRev
else "v0.26.1";

问题2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
djc@jetron-djc:/mnt/c/Users/DJC/Documents/develop/0me/headscale-0.26.1$ make build
nix build
warning: Git tree '/mnt/c/Users/DJC/Documents/develop/0me/headscale-0.26.1' is dirty
error: Cannot build '/nix/store/x0151wjd71c3icbvqrjrhmah4039cxrc-headscale-1d8abba-dirty.drv'.
Reason: builder failed with exit code 1.
Output paths:
/nix/store/w1kqv2j36dsixf5pm4v5lwf2wiwkrkhz-headscale-1d8abba-dirty
Last 25 log lines:
> Running phase: buildPhase
> Building subPackage ./cmd/headscale
> buildPhase completed in 52 seconds
> Running phase: checkPhase
>
> ----------------------------------------------------------------------
> FAIL: headscale_test.go:29: Suite.TestConfigFileLoading
>
> headscale_test.go:54:
> c.Assert(err, check.IsNil)
> ... value *fmt.wrapError = &fmt.wrapError{msg:"fatal error reading config file: open /build/headscale246087243/config.yaml: no such file or directory", err:(*fs.PathError)(0xc0003dfcb0)} ("fatal error reading config file: open /build/headscale246087243/config.yaml: no such file or directory")
>
>
> ----------------------------------------------------------------------
> FAIL: headscale_test.go:73: Suite.TestConfigLoading
>
> headscale_test.go:96:
> c.Assert(err, check.IsNil)
> ... value *fmt.wrapError = &fmt.wrapError{msg:"fatal error reading config file: Config File \"config\" Not Found in \"[/build/headscale4241716010]\"", err:viper.ConfigFileNotFoundError{name:"config", locations:"[/build/headscale4241716010]"}} ("fatal error reading config file: Config File \"config\" Not Found in \"[/build/headscale4241716010]\"")
>
> OOPS: 0 passed, 2 FAILED
> --- FAIL: Test (0.00s)
> FAIL
> FAIL github.com/juanfont/headscale/cmd/headscale 0.020s
> FAIL
For full logs, run:
nix log /nix/store/x0151wjd71c3icbvqrjrhmah4039cxrc-headscale-1d8abba-dirty.drv
make: *** [Makefile:20: build] Error 1

这个错误是因为测试阶段失败导致构建中断。测试失败的原因是找不到配置文件。

直接构建二进制文件:

1
2
3
cp config-example.yaml config.yaml
go mod tidy
go build -o headscale ./cmd/headscale

编译成功后,在项目目录下有个 headscale 文件,大概80多M。

问题3

1
2
root@jetron-djc:~# nix develop
error: experimental Nix feature 'nix-command' is disabled; add '--extra-experimental-features nix-command' to enable it

这个错误是因为 Nix 的 nix-command 功能是实验性的,默认被禁用了。

1
2
# 编辑配置文件
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf

使用 root 用户执行上面的命令即可。

问题4

1
2
3
root@jetron-djc:~# nix develop
path '/root' does not contain a 'flake.nix', searching up
error: could not find a flake.nix file

这个错误是因为 nix develop 命令需要在包含 flake.nix 文件的目录中运行,请 cd 到你项目的目录执行 nix develop

问题5

1
2
3
4
=== Failed
=== FAIL: hscontrol/db TestConstraints/no-duplicate-username-if-no-oidc-postgres (0.05s)
db_test.go:404: start postgres: initdb: initdb: error: cannot be run as root
initdb: hint: Please log in (using, e.g., "su") as the (unprivileged) user that will own the server process.

这个错误是因为 PostgreSQLinitdb 命令不能以 root 用户身份运行,这是出于安全考虑。测试代码试图以 root 用户初始化数据库,但被拒绝了。请使用普通用户 执行make build 命令。

手动安装相关依赖

1
2
3
4
5
6
# 安装 Buf
go install github.com/bufbuild/buf/cmd/buf@v1.55.1

# 安装 Protobuf
sudo apt update
sudo apt install protobuf-compiler

说明

本文介绍 使用EMQX作为 MQTT Server时,如何连接多个数据源(认证链)进行设备认证。

为了使用方便,均使用 docker 启动 EMQX ,并且在启动时配置参数自动进行数据连接。

介绍了 EMQX 中各种环境变量在 docker compose 中的写法。

MySQL认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: '3'
services:
emqx:
image: emqx/emqx:4.3.5
container_name: docker_emqx
ports:
- "18083:18083"
- "1883:1883"
- "4369:4369"
- "8883:8883"
- "8085:8084"
- "8081:8081"
- "8083:8083"
volumes:
- /home/yxin/Documents/emqx/emqx-cert:/opt/emqx/etc/certs
- /home/yxin/Documents/emqx/emqx.conf:/opt/emqx/etc/emqx.conf
restart: always
environment:
- "EMQX_ALLOW_ANONYMOUS=false"
- "EMQX_ACL_NOMATCH=deny"
- "EMQX_AUTH__MYSQL__SERVER=172.16.33.51:3306"
- "EMQX_AUTH__MYSQL__POOL=8"
- "EMQX_AUTH__MYSQL__USERNAME=root"
- "EMQX_AUTH__MYSQL__PASSWORD=MeRootSSSS"
- "EMQX_AUTH__MYSQL__DATABASE=mlic"
- "EMQX_AUTH__MYSQL__AUTH__MYSQL__QUERY_TIMEOUT=5"
- "EMQX_AUTH__MYSQL__AUTH_QUERY=select password from mqtt_user where username = '%u' union all select password from mqtt_user2 where username = '%u' limit 1"
- "EMQX_LOADED_PLUGINS=emqx_recon | emqx_retainer | emqx_rule_engine | emqx_management | emqx_dashboard | emqx_auth_mysql"

其中

1
select password from mqtt_user where username = '%u' union all select password from mqtt_user2 where username = '%u' limit 1"

经测试可用,可在多表中进行设备认证。

PostgreSQL认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: '3'
services:
emqx:
image: emqx/emqx:4.4.15
container_name: docker_emqx
ports:
- "18083:18083"
- "1888:1883"
- "4369:4369"
- "8883:8883"
restart: always
environment:
- "EMQX_ALLOW_ANONYMOUS=false"
- "EMQX_ACL_NOMATCH=deny"
- "EMQX_AUTH__PGSQL__SERVER=172.16.33.58:5432"
- "EMQX_AUTH__PGSQL__POOL=8"
- "EMQX_AUTH__PGSQL__USERNAME=postgres"
- "EMQX_AUTH__PGSQL__PASSWORD=PgSQLmmmmmm"
- "EMQX_AUTH__PGSQL__DATABASE=timescale"
# - "EMQX_AUTH__PGSQL__AUTH__PGSQL__QUERY_TIMEOUT=5"
- "EMQX_LOADED_PLUGINS=emqx_recon | emqx_retainer | emqx_rule_engine | emqx_management | emqx_dashboard | emqx_auth_pgsql"

认证链

可同时使用 MySQL以及Postgresql认证。其中有一个认证通过即认证通过,不过如果两个数据库中有相同用户名,密码不同,当一个认证失败后就会返回认证失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
version: '3'
services:
emqx:
image: emqx/emqx:4.4.15
container_name: docker_emqx_auth
ports:
- "18085:18083"
- "1889:1883"
# - "4369:4369"
# - "8883:8883"
restart: always
# 使用host模式会导致emqx启动失败
# network_mode: bridge
# external_links:
# - mysql
environment:
- "EMQX_ALLOW_ANONYMOUS=false"
- "EMQX_ACL_NOMATCH=deny"
- "EMQX_AUTH__PGSQL__SERVER=172.16.33.58:5432"
- "EMQX_AUTH__PGSQL__POOL=8"
- "EMQX_AUTH__PGSQL__USERNAME=postgres"
- "EMQX_AUTH__PGSQL__PASSWORD=JDDDDDDDDt"
- "EMQX_AUTH__PGSQL__DATABASE=timescale"
# - "EMQX_AUTH__PGSQL__AUTH__PGSQL__QUERY_TIMEOUT=5"
- "EMQX_AUTH__MYSQL__SERVER=172.16.33.62:3306"
- "EMQX_AUTH__MYSQL__POOL=8"
- "EMQX_AUTH__MYSQL__USERNAME=root"
- "EMQX_AUTH__MYSQL__PASSWORD=MVVVVVVVVne"
- "EMQX_AUTH__MYSQL__DATABASE=iform"
- "EMQX_AUTH__MYSQL__AUTH__MYSQL__QUERY_TIMEOUT=5"
- "EMQX_LOADED_PLUGINS=emqx_recon | emqx_retainer | emqx_rule_engine | emqx_management | emqx_dashboard | emqx_auth_pgsql | emqx_auth_mysql"
# networks:
# default:
# external:
# name: net-5g

Headscale使用自定义证书

在一些嵌入式设备中没有安装根证书,并且更新根证书比较麻烦,此时可以使用长时间的自定义证书。

生成自定义证书(特别是自签名证书)虽然技术上可行,但需要注意安全性和信任问题。自签名证书不会被浏览器或操作系统信任,会导致安全警告。以下是生成30年有效期证书的步骤和注意事项:


1. 生成证书的步骤

使用 OpenSSL 工具生成自签名证书(支持 SAN 扩展,兼容现代浏览器):

步骤 1:安装 OpenSSL

1
2
3
4
5
# Ubuntu/Debian
sudo apt-get install openssl

# CentOS/RHEL
sudo yum install openssl

步骤 2:创建配置文件 openssl.cnf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[ req ]
default_bits = 4096
default_keyfile = example.com.key
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_ca
prompt = no

[ req_distinguished_name ]
C = CN # 国家代码
ST = Shanghai # 省份
L = Shanghai # 城市
O = example.com Inc # 组织名称
OU = IT # 组织单位
CN = www.example.com

[ req_ext ]
subjectAltName = @alt_names

[ v3_ca ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = www.example.com
DNS.2 = example.com # 可选:添加其他域名

步骤 3:生成私钥和证书

1
2
openssl req -x509 -new -nodes -keyout example.com.key -sha256 \
-days 10950 -out example.com.crt -config openssl.cnf
  • -days 10950:30年(30×365)
  • 输出文件:example.com.key(私钥)、example.com.crt(证书)

3. 验证证书

1
openssl x509 -in example.com.crt -text -noout

检查有效期(Validity)和域名(Subject Alternative Name)。


4. 部署证书

  • Web 服务器:将 example.com.keyexample.com.crt 配置到 Nginx/Apache。
  • 强制 HTTPS:确保服务器配置了 HTTPS 重定向。

5. 客户端信任证书

Headscale 使用自定义证书时,Tailscale 连接登入会出现如下错误:Received error: fetch control key: Get "https://www.example.com/key?v=116": x509: certificate signed by unknown authority。这是因为Tailscale 客户端默认要求服务器证书由受信任的证书颁发机构(CA)签发,例如 Let’s Encrypt。如果使用自签名证书或非 Let’s Encrypt 的证书,客户端会因无法验证证书链而拒绝连接。

步骤 1:确认证书安装位置

  1. 以管理员身份运行证书管理器

    • Win + R,输入 mmc,回车。
    • 点击 文件 → 添加/删除管理单元
    • 添加 证书 管理单元,选择 计算机账户 → 本地计算机
  2. 检查证书是否已安装到 受信任的根证书颁发机构

    • 在左侧导航栏展开 证书(本地计算机)受信任的根证书颁发机构证书
    • 确认列表中存在 www.example.com 的证书(颁发者和使用者均为 example.com Inc)。
  3. 若未找到证书,重新安装

    • 右键 证书 → 所有任务 → 导入。
    • 使用向导导入 example.com.crt,**在向导最后一步选择存储位置为 受信任的根证书颁发机构**。

注意事项

  1. 证书安装位置错误
    Windows 系统有多个证书存储区(如 受信任的根证书颁发机构本地计算机当前用户)。

    • 必须 将自签名证书安装到 受信任的根证书颁发机构(Trusted Root Certification Authorities)
    • 若安装到其他位置(如 个人证书当前用户 存储),系统级服务(如 Tailscale 客户端)将无法信任该证书。
  2. 证书未正确应用于系统级
    Windows 的证书管理分为 本地计算机当前用户

    • 如果以普通用户身份安装证书,仅当前用户有效,系统服务(如 Tailscale)可能无法访问。
    • 需要以管理员权限运行证书管理工具,将证书安装到 本地计算机受信任的根证书颁发机构
  3. 证书文件内容问题

    • 证书文件可能包含多个证书(如中间证书链),导致解析失败。
    • 证书文件可能不是 PEM 格式(需确保内容以 -----BEGIN CERTIFICATE----- 开头)。

介绍

介绍如何使用 GOWVP 以及 EasyGBD 进行 GB28181服务器的搭建和测试。

由于要将本地萤石NVR的视频信息推送到云端服务器,再查看了该NVR支持的协议后,决定采用GB28181。为了省钱,在网上找了很多开源的GB28181服务器以及模拟客户端,比如 wvp-gb28181-pro 等,但是折腾半天就是看不到画面。没办法只好换个软件,最后选定 GOWVPEasyGBDGOWVP 是服务端,EasyGBD模拟的客户端。

安装

EasyGBD

下载地址:https://github.com/EasyDarwin/EasyGBD

下载后,win/bin 目录下的 EasyGBD_Demo.exe 可直接运行使用。

EasyGBD

GOWVP

推荐使用 dokcer 进行安装,比较方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
services:
gowvp:
# 如果拉不到 docker hub 镜像,也可以尝试
# registry.cn-shanghai.aliyuncs.com/ixugo/homenvr:latest
image: registry.cn-shanghai.aliyuncs.com/ixugo/homenvr:latest
# linux 解开下行注释,并将 ports 全部注释
# network_mode: host
ports:
# gb28181
- 15123:15123 # 管理平台 http 端口
- 15060:15060 # gb28181 sip tcp 端口
- 15060:15060/udp # gb28181 sip udp 端口
# zlm
- 1935:1935 # rtmp
- 554:554 # rtsp
- 8080:80 # http
- 8443:443 # https
- 10000:10000
- 8000:8000/udp
- 9000:9000/udp
- 20000-20100:20000-20100 # gb28181 收流端口
- 20000-20100:20000-20100/udp # gb28181 收流端口udp
volumes:
- ./data:/opt/media/bin/configs

使用 docker-compose 启动程序后,会在 当前 docker-compose.yaml 文件夹中自动生成 data 文件夹,里面放着zlmeidakit等配置文件。

文件夹

然后进入到 data 目录,找到 zlm.ini ,修改里面 rtp_proxy.port_range20000-20100 ,跟 docker-compose.yaml 里暴露的端口一致。

端口

注意 如果是内网安装的话,为了测试方便可以把防火墙关掉。 如果是公网部署,那么需要开放 20000-20100/udp20000-20100/tcp15060/udp/tcp 方便设备注册推流。

设置

当程序启动后,访问服务器 15123 端口,登入到系统。

ZLMediakit 设置

登入界面后,需要查看 zlmediakit 是否正常,如果是绿色闪烁圆点,那么表示系统正常;如果是红色圆点表示不正常。

同时需要修改 zlmediakit 的配置,点击右上角齿轮图表,修改 国标收流默认地址 ,同时填上 secret keysecret keydata 目录下 zlm.ini 文件里。

setting

setting

设置完成后,重启gowvp容器即可。

连接测试

1、打开 EasyGBD_Demo.exe 程序

2、访问系统,国标通道–接入信息 查看 国标ID、国标域、端口号、密码等。

gbsetting

3、修改 EasyGBD_Demo.exeIP,点击 启动。 注册成功后,就可以在系统界面看到注册的客户端信息。

注册

4、点击播放会推送视频

播放

EasyGBD_Demo.exe 界面会显示 推送关键帧

推送

其中,gowvp系统播放界面中有 rtmprtsp等地址,可以使用vlc播放器进行串流播放。

注意 客户端的对应端口需要开通,比如 EasyGBD_Demo.exe 客户端是 15090,这个端口要在防火墙开放,不然可能导致视频无法播放。

0%