openwrt

嗨,各位工业界的“键盘侠”、PLC驯兽师、设备救火队长们:

你们有没有遇到过这种尴尬场面?

👉 你想远程调试一台远在千里之外的设备,IP是 192.168.1.2 —— 很标准,很熟悉,闭着眼都能敲出来。

结果一连,连上了…隔壁老王的设备???

因为那边厂里有十台设备,清一色都是 192.168.1.2 —— 没错,工业设备出厂默认IP,就是这么“团结”。

这时候你只能:

  • 打电话问现场:“喂,老张,你那台192.168.1.2的PLC是第几台来着?”
  • 查文档:“哦对,虚拟IP是10.8.3.47…”(谁记得住啊!)
  • 改IP?不敢动,一动全厂报警,老板电话马上打过来:“你动我产线IP干嘛?!”

🤯 痛点爆炸!

别慌,救星来了!

我花了N个通宵、喝了N箱咖啡、掉了N撮头发,终于搞出了一个神器 —— 基于 WireGuide 的工业设备物联 & 远程运维系统!

它不炫技,不画饼,专治各种“IP撞车”、“远程卡壳”、“虚拟地址记不住”的工业疑难杂症。

🎯 核心功能一句话总结:

👉 你输入 192.168.1.2,我就让你连上你心里想的那台 192.168.1.2 —— 哪怕全世界有100台设备都叫这个名字!

是不是听着像魔法?其实是黑科技。

🔧 它怎么做到的?

想象一下,你走进一家超大超市,货架上摆着100瓶“可乐”,全都叫“可乐”,包装一模一样。

传统方案(比如SD-WAN/VPN)会说:“不行,重名了!我给每瓶可乐贴个编号:可乐#001、可乐#002…你得记住编号才能买。”

—— 工程师:我只想喝可乐,不想背编号!

而我的方案说:“没问题!你喊‘我要可乐’,我问你‘你要A区第三排那瓶,还是B区收银台旁边那瓶?’ —— 你指一下,我直接递给你。”

我们用 WireGuide 在后台悄悄“打标签+智能路由”,但前端完全不打扰你 —— 你看到的,还是那个熟悉的 192.168.1.2。

💡 三大爽点,专为工程师打造:

✅【零改造组网】
不用动现有设备、不用改IP、不用加交换机、不用培训网络管理员 —— 插上就能用,成本低到老板笑出声。

✅【真·原生IP访问】
PLC软件里直接输 192.168.1.2,点连接 —— 嗖,连上了!不是虚拟IP,不是映射端口,就是原汁原味的设备IP,和你在现场插网线一模一样!

✅【子网重叠?不存在的】
十台设备都是192.168.1.x?没问题!百台设备跨厂区IP全撞车?小意思!我们后台自动隔离+精准路由,互不干扰,所有设备永远在线,永不掉线。

🛠️ 举个栗子:

张工在上海总部,想调试深圳分厂的“包装机PLC”,IP是192.168.1.2。

同时,成都分厂的“注塑机PLC”也是192.168.1.2。

传统方式:张工得查表,找到“深圳包装机”的虚拟IP是10.12.5.8,再打开软件,输入这个“陌生号码”,祈祷别输错。

WireGuide方式:张工打开软件 → 选择“深圳分厂-包装机” → 自动连上 → 地址栏赫然显示:192.168.1.2 ✅

—— 就像你手机里存了两个“李伟”,一个备注“北京李伟”,一个“上海李伟”,你点“北京李伟”,打的就是北京那位。

🚀 为什么现有方案搞不定?

市面上的SD-WAN、工业VPN,本质是“换IP方案” —— 把冲突的IP换成不冲突的虚拟IP。这就像给每个人发个工牌号,不让叫名字了。

问题是:工程师不是网管!他们熟悉的是设备本体IP,不是你分配的“虚拟工号”。每次调试都要查表、换IP、重配软件 —— 效率拉胯,体验稀碎。

我的方案:不换IP,不改习惯,只改“背后怎么送信” —— 让网络自己变聪明,用户无感。

📈 效果如何?

  • 远程调试时间缩短 60%+
  • 工程师培训成本归零(因为不用学新IP)
  • 现场断网/误操作风险下降 90%
  • 老板满意度 ↑↑↑(因为不用买新设备、不用请外援、不用停产)

🎁 彩蛋功能:

  • 支持多工程师同时访问不同“192.168.1.2”,互不干扰
  • 流量加密,安全合规,符合等保要求
  • Web控制台一键切换设备,像换电视频道一样简单
  • 支持老旧设备、非智能设备、串口转网口设备 —— 老古董也能上高速!

📣 最后说句掏心窝子的:

工业不是互联网,不能总想着“颠覆”、“重构”。工程师要的是“顺手”、“靠谱”、“别添乱”。

我的产品不追求花里胡哨,只专注一件事:

让远程运维,回归它本该有的样子 —— 简单、直接、高效。

你敲 192.168.1.2,我就给你 192.168.1.2。

不多不少,不偏不倚。

如果你受够了“IP撞车”、“虚拟地址背诵大赛”、“远程调试像解谜游戏”…

欢迎来找我聊聊 👋 可以通过左侧导航栏Github链接找到我。

我们的产品让工业远程运维,像在家上路由器后台一样轻松。

#工业物联网 #远程运维 #PLC调试 #工业4.0 #WireGuide #子网冲突终结者 #工程师友好型产品 #别再背虚拟IP了

网站介绍

国内要充值 国外一些网站的会员服务,一般都挺麻烦的。

我自己有 wise ,但是基本上也很少用它来充值,主要是价格比较贵(基本上都是原价)。而且我都在 新加坡 区域充值,价格好像比其它区域更贵一点。

23年的时候有充值过 OpenAI 的 API,但是用的少,后面就没有充值了。 之后主要使用免费的比如 GeminiQwen,最近想使用下 Codex 看下效果。无意间看到有拼团的充值网站,价格比较优惠,比原价便宜 2美元。

想着就试下,发现能正常使用。 所以写出来分享下。整个操作基本没难度,直接使用 支付宝 进行付款就行。

网站是FamilyPro

FamilyPro 助您节省 ChatGPT、Netflix、Spotify 等服务费用。 分享您的订阅或加入群组——简单、安全、实惠。

链接:https://familypro.io?invite=GHb1a30b

注册、购买

推荐使用 Google 账号,我使用 hotmail 和 outlook 后缀的邮箱都不行。hotmail后缀的邮箱在注册时就提示无法使用,outlook的邮箱一直收不到 激活邮件。

该网站支持 拼团共享账号,我买的是独享,使用的是自己的账号。

如果购买独享账号,需要提供给 网站 session,然后等几分钟就能成功。我后面看了下,应该是从 iOS 端购买的。

ChatGPT登入后,session 获取地址:https://chatgpt.com/api/auth/session

主页提供多种账号充值购买!

主页

购买界面,支持支付宝!

支付宝

订单界面!

订单

OpenAI ChatGPT显示 Plus订阅成功!

订阅成功

在 iOS 端查看订阅信息!

iOS

问题

使用jenkins对一个前端项目进行打包,每次打包都出现 file-saver 超时(TIMEOUT)问题。即使将yarn依赖源改成国内都解决不了。

1
2
3
error An unexpected error occurred: "https://raw.githubusercontent.com/eligrey/FileSaver.js/e865e37af9f9947ddcced76b549e27dc45c1cb2e/package.json: ETIMEDOUT".
info If you think this is a bug, please open a bug report with the information provided in "/var/jenkins_home/workspace/frontend_k8s/yarn-error.log".
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.

鉴于国内对GITHUB访问时断时续的情况,如果打包时还是从GITHUB进行下载,那么这个问题就会一直出现。

那么,在已经将yran源改成国内的情况下,为何还会出现从 GITHUB 下载的情况呢? 而且项目中 FileSaver 使用的 代码形式的引入,没有进行依赖配置。

排查

为了排查问题,我先在本机电脑上使用 VPN,然后执行 yarn install 看下 yarn.lock 情况。

在命令成功执行后,打开 yarn.lock 文件,搜索 file-saver 找到了原因。

原来是程序中使用了 jspdf,而这个组件依赖 file-saver ,而且指定从 GITHUB 下载。

至此总算找到原因了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jspdf@^1.5.3:
version "1.5.3"
resolved "https://registry.npmmirror.com/jspdf/-/jspdf-1.5.3.tgz#..."
integrity sha512-J9X76xnncMw+wIqb15HeWfPMqPwYxSpPY8yWPJ7rAZN/ZDzFkjCSZObryCyUe8zbrVRNiuCnIeQteCzMn7GnWw==
dependencies:
canvg "1.5.3"
file-saver eligrey/FileSaver.js#1.3.8 # ← 这里是问题所在
html2canvas "1.0.0-alpha.12"
omggif "1.0.7"
promise-polyfill "8.1.0"
stackblur-canvas "2.2.0"


file-saver@1.3.8, "file-saver@github:eligrey/FileSaver.js#1.3.8":
version "1.3.8"
resolved "https://codeload.github.com/eligrey/FileSaver.js/tar.gz/e865e37af9f9947ddcced76b549e27dc45c1cb2e"

jspdf

filesaver

解决

package.json 文件中,添加 resolutions:

1
2
3
4
5
{
"resolutions": {
"**/file-saver": "^2.0.5"
}
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "xxx",
"version": "1.0.2",
"private": true,
"author": "8080@gmail.com",
"scripts": {
// ... 你现有的 scripts
},
"dependencies": {
// ... 你现有的 dependencies
},
"devDependencies": {
// ... 你现有的 devDependencies
},
"resolutions": {
"**/file-saver": "^2.0.5"
},
// ... 其他配置
}

然后删除 yarn.lock 后,重新执行:yarn install.

背景

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

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

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

目前虽然有一些组网方案,比如使用NAT等,但效果并不算理想,因为工程师需要记住各种对应的虚拟地址。之前我们一直使用传统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远程访问的核心痛点。

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

视频演示
说明:重叠的子网是 192.168.6.0/24。 为了能够访问,需要告诉程序你当前要访问的是哪个子网的设备,并且不能妨碍已构建设备网络的通信。

说明

本文介绍如何使用 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
0%