headscale 压力测试

总结摘要

用法速记:

  1. 解压后 cp .env.example .env,填好 HEADSCALE_URLTS_AUTHKEY
  2. bash scripts/00_check_env.sh
  3. bash scripts/01_bootstrap_up.sh(默认 200 节点)
  4. bash scripts/02_list_nodes.sh → 生成 nodes.tsv
  5. 终端 A:bash scripts/03_churn.sh(每 30s 随机 up/down/restart)
  6. 终端 B:watch -n 20 'bash scripts/04_traffic_round.sh' 持续抽样流量
  7. 结束:bash scripts/99_cleanup.sh

200个客户端同时连接

客户端

headscale服务器资源消耗

服务器资源消耗

tailscale客户端服务器资源消耗-200个客户端

客户端资源消耗


一、推荐机器规格(200 设备规模)

被测 headscale与**发压端(loadgen)**分离,更易定位瓶颈。若你只买一台也能做,但推荐两台。

A. 发压端(运行 200 个 Tailscale 客户端容器)

  • vCPU:16 核

  • 内存:32 GB

  • 磁盘:100 GB NVMe(Docker 层与容器日志)

  • 网卡:≥ 1 Gbps(最好 5–10 Gbps)

  • OS:Ubuntu 22.04 LTS / Debian 12

  • 内核参数(建议):

    • fs.file-max=200000
    • net.core.rmem_max=16777216
    • net.core.wmem_max=16777216
    • net.ipv4.ip_local_port_range=10000 65000
  • Docker 守护进程日志轮转(避免日志撑满磁盘):

    • log-driver: "local", log-opts: {"max-size":"10m","max-file":"3"}

B. 被测 headscale(控制面)

  • vCPU:4 核
  • 内存:8 GB
  • 磁盘:100 GB
  • 网卡:≥ 1 Gbps
  • 数据库:SQLite(WAL 打开)或 PostgreSQL(随你已有环境)
  • 反向代理:如必须放在反代后,请放宽超时/保持连接,保证长轮询不被切。

C.(可选)自建 DERP 中继

  • vCPU:2–4 核
  • 内存:2–4 GB
  • 网卡:≥ 1 Gbps
  • 开放 TCP(derp) 与 UDP 3478(STUN)

二、目录结构(建议)

你可以在任意目录新建如下文件(均在本文给出内容):

1
2
3
4
5
6
7
8
9
10
11
headscale-loadtest/
├─ docker-compose.yml
├─ .env.example
├─ scripts/
│ ├─ 00_check_env.sh
│ ├─ 01_bootstrap_up.sh
│ ├─ 02_list_nodes.sh
│ ├─ 03_churn.sh
│ ├─ 04_traffic_round.sh
│ ├─ 99_cleanup.sh
└─ README.md (可把本文另存)

三、使用说明

  1. 准备:在 headscale 上生成一个 pre-auth key(预授权密钥)。
  2. 编辑 .env(复制 .env.example)填入 HEADSCALE_URLTS_AUTHKEY
  3. bash scripts/00_check_env.sh 检查环境(tun、docker 等)。
  4. bash scripts/01_bootstrap_up.sh 一键起 200 个 tailscale 客户端容器。
  5. bash scripts/02_list_nodes.sh 导出容器名 ↔ Tailscale IP 列表。
  6. 抖动:另开终端运行 bash scripts/03_churn.sh(每 30s 随机 up/down/restart 一批)。
  7. 流量:再开终端循环跑 bash scripts/04_traffic_round.sh(随机挑对做 TCP/UDP 10s 压力)。
  8. 结束后 bash scripts/99_cleanup.sh 清理容器与临时 sidecar。

四、docker-compose 与 .env

说明:我们用 docker compose up --scale tsnode=200 -d 来“一行扩容 200 台设备”。hostname 不必手配,容器会使用唯一的容器 ID 作为主机名,注册到 headscale 时也会保持唯一性。
iperf3 不安装进 tailscale 容器里,而是用短生命的 sidecar 容器通过 --network=container:<tsnode> 共享网络命名空间来发流/收流,避免镜像扩展与 apt 依赖。

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
services:
tsnode:
image: tailscale/tailscale:stable
restart: unless-stopped
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
# 必填:由 .env 注入
- TS_AUTHKEY=${TS_AUTHKEY}
# 建议关闭容器内部 DNS 接管;指向你的 headscale
- TS_EXTRA_ARGS=--login-server=${HEADSCALE_URL} --accept-dns=false
sysctls:
net.ipv6.conf.all.disable_ipv6: "0"
# 可按需开启持久化(不是必须)
# volumes:
# - ts-state:/var/lib/tailscale

# volumes:
# ts-state:

.env.example(复制为 .env 并修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 你的 headscale 外部可达地址(含 https://,勿带结尾斜杠)
HEADSCALE_URL=https://headscale.example.com
# 从 headscale 预先创建的预授权密钥
TS_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 规模与参数(可改)
REPLICAS=200 # 模拟设备数量
CHURN_INTERVAL=30 # 抖动间隔秒
CHURN_BATCH_PERCENT=10 # 每轮对多少百分比容器做 up/down/restart
TRAFFIC_PAIR_COUNT=30 # 每轮随机压多少对
TRAFFIC_TCP_DURATION=10 # TCP 每对持续秒
TRAFFIC_TCP_PARALLEL=4 # TCP 并发流数 -P
TRAFFIC_UDP_DURATION=10 # UDP 每对持续秒
TRAFFIC_UDP_BW=50M # UDP 目标带宽 -b

五、脚本集合(保存到 scripts/

注意:脚本使用 docker compose(v2),以及 networkstatic/iperf3 作为临时 sidecar 镜像。首次需要联网 docker pull,之后就可离线复用。

1) 00_check_env.sh

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
#!/usr/bin/env bash
set -euo pipefail

# 载入 .env
if [[ -f .env ]]; then source .env; else echo ".env 未找到,请复制 .env.example"; exit 1; fi

# 检查 docker
command -v docker >/dev/null || { echo "docker 未安装"; exit 1; }
docker info >/dev/null || { echo "docker daemon 不可用"; exit 1; }

# 检查 compose
if ! docker compose version >/dev/null 2>&1; then
echo "docker compose v2 未安装"; exit 1
fi

# 检查 tun
if [[ ! -e /dev/net/tun ]]; then
echo "/dev/net/tun 不存在,尝试加载内核模块..."
if command -v modprobe >/dev/null; then
sudo modprobe tun || true
fi
fi
[[ -e /dev/net/tun ]] || { echo "仍无 /dev/net/tun,请在宿主机启用 TUN 模块"; exit 1; }

# 预拉镜像(可离线后使用)
echo "预拉取镜像(可选)..."
docker pull tailscale/tailscale:stable
docker pull networkstatic/iperf3:latest

echo "检查通过。"

2) 01_bootstrap_up.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env bash
set -euo pipefail
source .env

echo "以 $REPLICAS 个副本拉起 tsnode..."
docker compose up -d --scale tsnode="${REPLICAS}"

echo "等待节点启动并完成登录(首次 30~60s)..."
sleep 45

echo "当前副本数量:"
docker compose ps tsnode

echo "可运行 scripts/02_list_nodes.sh 导出节点 IP。"

3) 02_list_nodes.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env bash
set -euo pipefail

OUT="nodes.tsv"
: > "$OUT"

# 列出 compose 管理的 tsnode 容器(名字包含项目前缀)
mapfile -t CTS < <(docker compose ps --format '{{.Name}}' tsnode)

echo -e "container\tts_ip4" >> "$OUT"
for c in "${CTS[@]}"; do
ip=$(docker exec "$c" tailscale ip -4 2>/dev/null | head -n1 || true)
if [[ -n "$ip" ]]; then
echo -e "${c}\t${ip}" >> "$OUT"
fi
done

echo "已导出 $OUT(容器名 ↔ Tailscale IPv4)。"

4) 03_churn.sh(每 30s 随机 up/down/restart 一批)

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
#!/usr/bin/env bash
set -euo pipefail
source .env

SERVICE="tsnode"
INTERVAL="${CHURN_INTERVAL:-30}"
PCT="${CHURN_BATCH_PERCENT:-10}"

echo "每 ${INTERVAL}s 随机对 ${PCT}% 容器执行 up/down/restart 抖动。按 Ctrl-C 结束。"

while true; do
mapfile -t CTS < <(docker compose ps --format '{{.Name}}' "$SERVICE")
TOT=${#CTS[@]}
((TOT>0)) || { echo "未发现容器"; exit 1; }
BATCH=$(( (TOT*PCT + 99)/100 )) # 向上取整

# 随机挑选
mapfile -t PICKED < <(printf "%s\n" "${CTS[@]}" | shuf -n "$BATCH")

echo "=== Churn round: $(date '+%F %T') 目标 $BATCH/$TOT ==="
for c in "${PICKED[@]}"; do
op=$((RANDOM%3)) # 0:down 1:up 2:restart
case "$op" in
0)
echo " - $c : tailscale down"
docker exec "$c" tailscale down || true
;;
1)
echo " - $c : tailscale up"
# 使用容器内命令 up;如果你在 .env 中改了 HEADSCALE_URL/TS_AUTHKEY,这里会重登
docker exec "$c" tailscale up --login-server="${HEADSCALE_URL}" --authkey="${TS_AUTHKEY}" --accept-dns=false || true
;;
2)
echo " - $c : docker restart"
docker restart "$c" >/dev/null || true
;;
esac
done

sleep "$INTERVAL"
done

5) 04_traffic_round.sh(随机起对做 TCP/UDP 流量)

设计为一轮 10s的抽样压测;你可以循环调用或配合 watch -n 20 调度。
服务器与客户端都用 临时 sidecar 容器--network=container:<tsnode> 共享网络命名空间,无需在 tsnode 内安装 iperf3。

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
56
57
58
59
#!/usr/bin/env bash
set -euo pipefail
source .env

PAIRS="${TRAFFIC_PAIR_COUNT:-30}"

# 读取 nodes.tsv(若不存在则生成)
[[ -f nodes.tsv ]] || bash scripts/02_list_nodes.sh

mapfile -t LINES < <(tail -n +2 nodes.tsv)
CNT=${#LINES[@]}
((CNT>=2)) || { echo "可用节点不足 2"; exit 1; }

pick_node() {
# 输出:name ip
local line="${LINES[$((RANDOM % CNT))]}"
local name ip
name=$(awk '{print $1}' <<<"$line")
ip=$(awk '{print $2}' <<<"$line")
echo "$name $ip"
}

run_pair_tcp() {
local cA ipA cB ipB
read -r cA ipA <<<"$(pick_node)"
read -r cB ipB <<<"$(pick_node)"
[[ "$cA" != "$cB" ]] || return 0

# 启动一次性服务器(-1 处理一次并退出)
docker run --rm -d --name "iperf-srv-${cB}" --network="container:${cB}" networkstatic/iperf3 -s -1 >/dev/null

echo "[TCP] $cA -> $cB ($ipB)"
docker run --rm --network="container:${cA}" networkstatic/iperf3 \
-c "$ipB" -t "${TRAFFIC_TCP_DURATION:-10}" -P "${TRAFFIC_TCP_PARALLEL:-4}" >/dev/null 2>&1 || true
}

run_pair_udp() {
local cA ipA cB ipB
read -r cA ipA <<<"$(pick_node)"
read -r cB ipB <<<"$(pick_node)"
[[ "$cA" != "$cB" ]] || return 0

docker run --rm -d --name "iperf-srv-${cB}" --network="container:${cB}" networkstatic/iperf3 -s -1 >/dev/null

echo "[UDP] $cA -> $cB ($ipB)"
docker run --rm --network="container:${cA}" networkstatic/iperf3 \
-c "$ipB" -u -b "${TRAFFIC_UDP_BW:-50M}" -t "${TRAFFIC_UDP_DURATION:-10}" >/dev/null 2>&1 || true
}

echo "=== Traffic round @ $(date '+%F %T') pairs=${PAIRS} ==="
for ((i=0; i<PAIRS; i++)); do
if (( RANDOM % 2 )); then
run_pair_tcp &
else
run_pair_udp &
fi
done
wait
echo "=== Round done ==="

6) 99_cleanup.sh

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash
set -euo pipefail

echo "停止并清理 tsnode 副本与残留 iperf sidecar..."
# 先清理 sidecar
docker ps --format '{{.ID}} {{.Names}}' | awk '/iperf-srv-/{print $1}' | xargs -r docker rm -f

# 再停 tsnode
docker compose down -t 5

六、测试步骤建议

  1. 单节点验证

    • 修改 .envREPLICAS=1bash scripts/01_bootstrap_up.sh
    • docker compose exec tsnode-1 tailscale status 看是否已连上你的 headscale(可用 tailscale netcheck 验证直连/DERP)。
    • bash scripts/02_list_nodes.sh 确认导出了 IP。
  2. 扩到 200 节点

    • .env 改回 REPLICAS=200,再次 01_bootstrap_up.sh
    • 02_list_nodes.sh 导出 nodes.tsv
  3. 控制面抖动

    • 终端 A:bash scripts/03_churn.sh(默认每 30s 对 10% 做 up/down/restart)
    • 观察 headscale /metrics、CPU、内存、日志中 PollNetMap 等请求量与耗时。
  4. 数据面吞吐

    • 终端 B:watch -n 20 'bash scripts/04_traffic_round.sh' 连续抽样回合。
    • 对比直连与 DERP(可在安全组/防火墙层面封掉 UDP,观察 DERP 回退下的吞吐变化)。
  5. 扩大/缩小与对比

    • REPLICAS 改为 300/500 试试 headscale 的上限趋势(若机器足够)。
    • 分别在无抖动强抖动两种状态下记录 headscale 的指标差异。

七、常见问题(FAQ)

  • 为什么用 sidecar 跑 iperf3?
    这样无需在 tailscale 镜像里安装任何包;--network=container:<tsnode> 会共享该 tsnode 的网络命名空间,等价于“在该设备上跑 iperf3”。

  • Compose 扩容 200 个需要唯一主机名吗?
    不需要手工设置。默认容器主机名是容器 ID,Tailscale 设备名会唯一。也可自行加 --hostname 脚本模式(不建议和 --scale 混用)。

  • 抖动脚本中的 tailscale down/updocker restart 区别?
    down/up 更接近“客户端主动注销/重登录”;restart 模拟“设备重启或网络闪断”,三者混合能覆盖更多状态机分支。


问题

现在信创等需求越来越多,很多时候我们需要同时打包 x86arm 架构的程序。
本文主要介绍 如何使用 Docker 部署的 Jenkins 自动打包 x86arm 架构的镜像。
本次介绍 如何打包 Spring Boot 以及 Node 项目,包含了前后端。

环境准备

1、x86 电脑或服务器,部署了 Jenkins 服务
2、X86 电脑或服务器,部署了 Harbor 服务。 打包后,自动推送镜像到 harbor

Jenkins 安装及配置

  • Jenkins 安装
1
2
3
4
5
6
7
8
9
10
11
12
docker run \
-u root \
-d \
-v $(which docker):/usr/bin/docker \
-v $PWD/jenkins-data:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /root/.docker/:/root/.docker/ \
-v /etc/localtime:/etc/localtime \
--network host \
--restart=always \
--privileged \
jenkins/jenkins

注意: 挂载了宿主机的 /root/.docker/ 目录。这样方便 容器内使用 buildx

  • Buildx 安装

1、 在 Github 上下载 Buildx 二进制文件

1
wget https://github.com/docker/buildx/releases/download/v0.28.0/buildx-v0.28.0.linux-amd64

2、 移动到 /root/.docker 目录下

1
mv buildx-v0.10.2.linux-amd64 /root/.docker/cli-plugins/docker-buildx

3、 添加可执行权限

1
chmod +x /root/.docker/cli-plugins/docker-buildx

buildx

  • 检查容器内 buildx 是否可用
1
2
3
4
5
6
# 进入容器
docker exec -it 容器名称 sh
# 查看 buildx 版本
docker buildx version
# 查看 buildx 状态
docker buildx ls

buildx版本

多架构基础镜像准备

如果使用 自有 Harbor 里的镜像,需要仓库里有多个架构的基础镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 能 push 的前提是,docker 已经 登入了 harbor
# 以 alpine 为例,x86 镜像,推送到 harbor
docker pull registry.cn-hangzhou.aliyuncs.com/acs/alpine:3.16.0
docker tag registry.cn-hangzhou.aliyuncs.com/acs/alpine:3.16.0 harbor.xxxx.com/library/alpine:3.16.0
docker push harbor.xxxx.com/library/alpine:3.16.0

# 拉取arm 镜像,推送到 harbor
docker pull --platform=linux/arm64 registry.cn-hangzhou.aliyuncs.com/acs/alpine:3.16.0-arm64 # 带 --platform=linux/arm64 ,表示拉取 arm64 架构的镜像
docker tag registry.cn-hangzhou.aliyuncs.com/acs/alpine:3.16.0-arm64 harbor.xxxx.com/library/alpine:3.16.0-arm64
docker push harbor.xxxx.com/library/alpine:3.16.0-arm64


# 合并 x86 和 arm64 架构的镜像
docker buildx imagetools create -t harbor.xxxx.com/library/alpine:3.16.0 harbor.xxxx.com/library/alpine:3.16.0-arm harbor.xxxx.com/library/alpine:3.16.0

harbor 显示

多架构镜像

后端打包

后端 Spring Boot 打包其实不需要 buildx,因为 Spring Boot 项目使用 jib 插件即可完成多架构镜像的打包及推送。

示例:

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
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.6</version>
<configuration>
<allowInsecureRegistries>true</allowInsecureRegistries>
<from>
<image>harbor.xxxx.com/library/openjdk:8u342-jdk</image>
<platforms>
<platform>
<architecture>arm64</architecture>
<os>linux</os>
</platform>
<!-- 如果想一次推多架构清单(amd64 + arm64),再加一个: -->
<platform>
<architecture>amd64</architecture>
<os>linux</os>
</platform>
</platforms>
</from>
<to>
<image>harbor.xxxx.com/library/spring:${project.version}</image>
</to>
<container>
<jvmFlags>
<jvmFlag>-Xms2g</jvmFlag>
<jvmFlag>-Xmx6g</jvmFlag>
<jvmFlag>-XX:+HeapDumpOnOutOfMemoryError</jvmFlag>
<jvmFlag>-XX:HeapDumpPath=/app/logs</jvmFlag>
<jvmFlag>-Duser.timezone=Asia/Shanghai</jvmFlag>
</jvmFlags>
<ports>
<port>9988</port>
</ports>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
<mainClass>com.xxx.xxx.XxxApplication</mainClass>
</container>
</configuration>
</plugin>

前端打包

前端打包需要使用到 buildx,因为 Node 项目没有类似 jib 的插件。

前端项目 jenkinsfile 代码如下:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env groovy

def version = '1.0.arm64'

pipeline {
agent none
environment {
RELEASE_NUMBER = "${version}.${env.BUILD_NUMBER}"
NPM_CONFIG_REGISTRY = 'http://yournexue.com/repository/npm-group/'
}
stages {
stage("install dependencies and build") {
agent {
docker {
image 'node:14.18.1'
args '-v /root/.yarn/v6:/usr/local/share/.cache/yarn/v6 --add-host=raw.githubusercontent.com:185.199.111.133 --add-host=registry.yarnpkg.com:104.16.30.34 -e NODE_OPTIONS=--max_old_space_size=8192'
}
}
steps {
sh "sed 's/VERSION-NUM/${RELEASE_NUMBER}/g' -i ./.env.production"
sh "sed 's/VERSION-NUM/${RELEASE_NUMBER}/g' -i ./.env.test"
sh "yarn config set registry ${NPM_CONFIG_REGISTRY} && yarn config set ignore-engines true && yarn install"
// sh 'cp -rf dependencies/** /node_modules'
sh 'yarn build'
sh "rm -rf xxx-app && mkdir -p xxx-app && mv -f dist xxx-app"
sh "mv -f conf xxx-app"
sh "mv -f Dockerfile xxx-app"
}
}
stage("setup buildx (multi-arch + HTTP Harbor)") {
agent any
steps {
writeFile file: 'buildkit.toml', text: '''
[registry."harbor.xxxx.com"]
http = true
insecure = true
'''
sh '''
set -e
export DOCKER_HOST="${DOCKER_HOST:-unix:///var/run/docker.sock}"
export DOCKER_CLI_EXPERIMENTAL="${DOCKER_CLI_EXPERIMENTAL:-enabled}"
export HOME="${HOME:-/root}"

echo "[check] docker & buildx versions:"
docker version
docker buildx version

# 1) 启用 binfmt(使 x86 上可构建 arm64;幂等)
docker run --privileged --rm tonistiigi/binfmt --install all || true

# 2) 确保使用 docker-container 驱动的 builder(本 stage 内就地创建/选择,避免跨节点丢失)
BUILDER_NAME=fe-multiarch-builder

# 如已存在则直接 use,不存在则创建
if docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then
docker buildx use "${BUILDER_NAME}"
else
docker buildx create \
--name "${BUILDER_NAME}" \
--driver docker-container \
--config ./buildkit.toml \
--driver-opt network=host \
--driver-opt "env.http_proxy=" \
--driver-opt "env.https_proxy=" \
--use
fi

# 3) 引导 BuildKit
docker buildx inspect --bootstrap
echo "[builders]"
docker buildx ls

# 4) 多架构构建并推送(显式指定 --builder,防止默认切回 docker driver)
echo "[buildx] building & pushing multi-arch image..."
docker buildx build xxx-app \
--builder "${BUILDER_NAME}" \
--platform linux/amd64,linux/arm64 \
-t 172.16.30.52:8894/library/app:${RELEASE_NUMBER} \
--push
'''
}
}
stage("clean") {
agent any
steps {
sh '''
# 可选择保留 builder 以复用缓存,这里演示清理
docker buildx rm fe-multiarch-builder || true
rm -rf app buildkit.toml || true
'''
}
}
}
}

总结

Jenkins 中同时打包 x86 以及 arm 架构的镜像,我们只需要在x86 架构的机器上安装 buildx 即可,不需要用到 arm 架构的机器。

下面对 Tailscale 如何做 NAT 穿透(包括直连建立、保持与回退)的关键原理拆开讲。

1)总体线路:三条路,优先直连

  • 优先直连(UDP over Internet):两端先尝试点对点直连,成功后所有数据都直接走这条路(性能最好、时延最低)。(tailscale.com)
  • 协商/引导与兜底(DERP):在直连之前与失败时,先经就近 DERP 中继建立“控制/引导通道”,用它交换候选地址、发起“请你打洞”等信号;若直连始终失败,则走 DERP 作为加密转发 的备胎。(tailscale.com)
  • 更细的穿透协议(DISCO + STUN):Tailscale 基于 WireGuard 自研了“DISCO”发现/心跳层,配合 STUN 获取公网映射与 NAT 类型,驱动 UDP 打洞。(tailscale.com)

2)打洞的关键“料”:同一 UDP socket、STUN、候选地址、互打

  • 同一 UDP socket:获取公网映射必须用和后续真正传数的同一个 UDP 套接字来发 STUN/接回包,否则你测到的 公网IP:端口 跟实际传输不同,洞就白打了。这是很多实现失败的根源。(tailscale.com)
  • STUN 获取外网映射与 NAT 性质:客户端向 STUN 发请求,得到“我在外面的 IP:port 是啥”,也能侧面推断 NAT 类型(如对称型更难打)。(tailscale.com)
  • 交换候选地址:双方把自己当前可用的候选 端点(endpoints)(含 IPv4/IPv6、多个 IP:port 映射)通过控制面/DERP 互换。(tailscale.com)
  • 双向同时发起(UDP hole punching):A、B 对彼此的候选 IP:port 同步“互打”若干小包(包含 DISCO 探测),在两端 NAT 上同时打开出站映射,让对方进入我的 NAT 表,从而“外部包”也能被 NAT 放行给我。(tailscale.com)

3)DISCO 层干了什么?(比 WireGuard 更懂 NAT 的“前戏”)

  • DISCO ping / 心跳tailscale ping 默认发的是 DISCO 探测,不是 ICMP;它能告诉你“目前是经 DERP 还是直连哪个 IP:port”。(tailscale.com)
  • CallMeMaybe:当 A 已经对 B 的候选端点开火时,会经 DERP 送一个 CallMeMaybe 给 B,意思是“我在打你的这些端口了,你也回打一发,把回程洞打开”。(Go Packages)
  • (新版)CallMeMaybeVia / UDP relay endpoint:还有一种 Via 形式,提示通过中继候选来协助开路(对“UDP 被限”网络更友好)。(Go Packages)
  • 选择 & 优先级:如果已经有可靠直连,端上可以不再开启新的路径;直连优先于“经中继的路径”。(Go Packages)

4)魔法插座:magicsock 把所有这些都“接”在一起

  • magicsock 是 Tailscale 在用户态的“魔法 UDP 套接字”,同一个 socket 同时承载 WireGuard 数据、STUN、DISCO,以及穿透时需要的各种小包和心跳,从而保证“测到的映射=用来传数的映射”。(Go Packages)
  • 这也是为何 Tailscale 不直接用内核态 WireGuard:因为 NAT 穿透的许多动作必须跟数据在同一个用户态 socket里做,方便灵活控制与观测重绑定(rebinding)、心跳、计时器等。(Reddit)

5)失败与回退:DERP 作为发现通道 + 永久兜底

  • 发现阶段:最初几包通常先走最近的 DERP,用于互递 DISCO / 候选地址;一旦直连打通,就切换到直连通道。(tailscale.com)
  • 兜底阶段:若遇到 对称 NAT + 防火墙 等强限制,直连打不通,就全流量走 DERP(加密、稳定,但带宽/时延逊于直连)。(iam.bitbeats.io)
  • 现实世界并不完美:官方也承认 NAT 穿透是“长期拉锯战”,需要持续遥测与改进。(tailscale.com)

6)连通保持与漫游(rebind)处理

  • 保活心跳:穿透成功后,DISCO 会以低频心跳维持 NAT 映射;社区里称过“silent disco”等优化,尽量减少无谓心跳。(GitHub)
  • NAT 重绑定:当一端公网 IP:port 变化(例如路由器重启/蜂窝到 Wi-Fi 切换),magicsock 会通过 DISCO/STUN 迅速探测到并重建可用路径。原理还是“同一 socket 上继续打洞 + 候选更新”。(Go Packages)

7)IPv6 情况

  • 双栈最优:若两端都有可达 IPv6,通常不需要穿透就能直连(无 NAT、路径更稳定);若一端仅 IPv4-NAT,仍按上面流程打洞或回退 DERP。(tailscale.com)

8)你能在现场怎么“看懂它在做什么”

  1. 看当前连接类型与路径
1
2
3
tailscale status --peers
tailscale ping <peer-name> # 默认 DISCO,输出会标明 via DERP(...) 或 via <IP:port>
tailscale ping --until-direct <peer-name> # 持续直到打出直连

这些命令能直接看到“是否直连、走哪个 IP:port、DERP 节点名”等。(tailscale.com)

  1. 网络自检
1
tailscale netcheck

用于判断 UDP 是否可出、就近 DERP、是否存在对称 NAT 等(输出因版本而异)。文档对“连接类型”、“DERP 角色”有解释。(tailscale.com)

9)把整个流程串起来(顺序图式说明)

  1. 两端启动 → 通过控制面注册/换公钥,并各自连上就近 DERP。(tailscale.com)
  2. STUN 获取自己的公网 IP:port 与 NAT 线索;把“候选端点”发给对端(经控制面/DERP)。(tailscale.com)
  3. A/B 同时对彼此候选端点发 DISCO 探测(互打)→ NAT 表里建立出站/入站映射。必要时 A 还会经 DERP 发 CallMeMaybe 让 B 主动回打。(Go Packages)
  4. 任一条直连能收发 DISCO/数据包 → 选定最佳直连路径并切换数据面到直连。(tailscale.com)
  5. 若所有直连失败 → 全流量走 DERP,同时周期性再尝试直连以便条件改变时“脱离中继”。(iam.bitbeats.io)

进一步阅读(权威/源码级)

  • 官方长文:How NAT traversal works(强烈推荐通读,解释了为何“必须同一个 UDP socket”)。(tailscale.com)
  • DERP 服务器与角色(发现与兜底)。(tailscale.com)
  • DISCO 协议与 CallMeMaybe / CallMeMaybeVia 类型(Go 包文档)。(Go Packages)
  • magicsock 源码包(用户态“魔法 socket”,NAT 打洞的执行场)。(Go Packages)
  • Ping 类型说明(DISCO/TSMP/ICMP 的区别与用途)。(tailscale.com)

大家好,我是那个曾经天真地以为“买个摄像头=万事大吉”的倒霉蛋。

你有没有经历过这样的场景?
花几百块买了个“智能家用摄像头”,画质清晰、夜视给力、手机远程查看so easy。结果用了三个月,厂商发来一条温柔又冰冷的消息:

“亲爱的用户,您的免费云存储已到期,请支付29元/月开启录像回看功能。”

什么?!一个月29?!我这是在养摄像头还是在供儿子上私立国际学校?

更离谱的是,有些品牌还玩起了“套餐制”——7天循环录像要30,30天要60,要是想永久保存?不好意思,得加钱,还得绑定他们的会员体系……仿佛你不交钱,家里的猫偷吃零食的画面就永远消失了。

于是,我含泪删掉了那段拍到我家狗子半夜翻垃圾桶的珍贵视频,并立下誓言:

我要自己当“云服务商”!


一、市面上的摄像头,不是太贵就是太坑

现在的家用摄像头,说白了就两种:

  1. 便宜的:只能实时查看,不能回放,等于“睁眼瞎”。
  2. 贵的:必须订阅云服务才能录像,每月扣你几十块,像极了爱情里的“续费关系”。

而且你还不能断订——一旦停了,之前的录像全清空,比前任删聊天记录还彻底。

这不是科技,这是数字勒索

所以,我决定反向操作:买硬件不买服务,数据本地存,自由掌握在自己手里


二、我的省钱方案:海康+录像机+Mini主机 = 家庭版“天网系统”

经过一番研究(和踩坑),我最终选择了这套组合拳:

  • 设备清单
    • 海康威视 WiFi/有线双模摄像头 ×2(约230元/台)
    • 海康威视 WiFi录像机(NVR)×1(2路约70元左右,4路约140元左右)
    • Mini主机一台(小钢炮式Windows迷你电脑,300~600元)

总价不到1000,能用五年起步,平均每天才几毛钱,连一杯奶茶都不到!

✅ 方案优势一览:

项目 传统云摄像头 我的DIY方案
是否需要月租 是(29~60元/月) 否!一次性投入
录像是否本地存储 否(全靠云) 是!硬盘说了算
手机能否远程查看 能(通过“海康互联”APP)
数据隐私安全性 厂商掌握你的生活 自己掌握一切
视频保留时间 取决于付费等级 想存多久存多久

三、怎么搭?超简单三步走!

第一步:基础搭建 —— 海康全家桶上线

  1. 把两个海康摄像头装好(一个放客厅盯娃,一个放门口防快递刺客)。
  2. 连上WiFi录像机(支持无线连接摄像头,不用拉网线,懒人福音)。
  3. 插上电源,开机自动配对。

这时候你就可以用 “海康互联”APP 在手机上看实时画面啦!是不是很爽?

⚠️ 重点提醒:
设置时一定要记住你创建的那个 admin账号和密码!这不仅是登录APP的钥匙,更是进入摄像头后台的“万能通行证”。忘了它?恭喜你,可能要重置设备从头再来……

家里网络示意图

网络

录像机

录像机

室内摄像头

室内摄像头

mini主机

mini主机


第二步:进阶玩法 —— 把录像搬到自己的“云”里

你以为这就完了?No no no,真正的自由才刚开始!

我们目标是:不让任何厂商赚我一分钱,所有录像我说了算!

怎么做?

👉 把摄像头的数据导出来,存在自己的电脑或百度网盘!

具体操作如下:

  1. 准备一台 Mini主机(比如Intel N100的小盒子,性能够用还省电)。
  2. 让它连上家里同一个网络(可以让摄像头直接接路由器,也可以直接连录像机的WiFi热点)。录像机的WIFI热点以及密码可以在海康互联APP里看到。
  3. 登入摄像头后台,获取RTSP流地址(后面细说)。
  4. 在Mini主机上跑一个叫 ZLMediaKit 的开源工具(GitHub上有,免费!),它可以接收并录制摄像头的视频流。
  5. 录下来的文件自动同步到百度网盘、阿里云盘或者NAS,实现“私有云备份”。

这样一来,哪怕你家停电、设备损坏,视频还在云端躺着笑你:“主人,我活得比你还久。”

摄像头使用ZLMediaKit参考链接:https://ownding.com/2025/06/06/%E6%B5%B7%E5%BA%B7%E6%91%84%E5%83%8F%E6%9C%BA%E5%BD%95%E5%83%8F%E6%9C%BA%E5%AF%B9%E6%8E%A5%E6%96%B9%E5%BC%8F%E8%AE%B0%E5%BD%95/


四、如何找到摄像头IP并登入后台?

很多人卡在这一步,其实很简单,只需要一点点“黑客精神”(别怕,合法的那种)。

方法一:通过录像机找IP

  1. 用手机连上录像机发出的WiFi(初始默认名通常是 NVRxxxxxx)。
  2. 打开命令行工具(Windows按 Win+R → 输入 cmd)。
  3. 输入这个神奇咒语(需要先安装 nmap客户端):
1
nmap -sn 192.168.254.0/24

(注意:不同型号IP段可能不同,常见的是 192.168.1.x 或 192.168.254.x)

执行后你会看到一堆设备列表,其中就有你的摄像头IP,长得像这样:192.168.254.101

此步的目的就是找到摄像头的IP

  1. 打开浏览器,输入 https://[IP地址],比如:
1
https://192.168.254.101

然后输入你在“海康互联”里设的 admin 账号密码,Boom!进入后台!

在这里你可以:

  • 查看RTSP推流地址(一般是 rtsp://admin:12345@192.0.0.64:554/h264/ch1/main/av_stream 这种格式)
  • 调整分辨率、帧率
  • 设置移动侦测报警

摄像头web界面

摄像头web界面


五、终极自由:ZLMediaKit + 百度网盘 = 永不丢失的录像库

接下来就是技术流时间(放心,不写代码):

  1. 在Mini主机安装 ZLMediaKit(搜索 GitHub 就能找到)。
  2. 配置它去拉取每个摄像头的RTSP流。
  3. 设置录制规则:比如全天录制 or 只录有人动的时候。
  4. 百度网盘客户端CloudSync 类工具,把录好的视频文件夹自动上传。

完成之后,效果是这样的:

当你出门旅游时,突然收到消息:“阳台有只野猫跳进来啃拖鞋。”
你打开手机百度网盘,翻出昨天下午三点的录像,果然看见一只橘猫正在表演“拖鞋刺身”。
你一边笑出声,一边庆幸:我没给任何公司交过一分钱会员费。

摄像头使用ZLMediaKit参考链接:https://ownding.com/2025/06/06/%E6%B5%B7%E5%BA%B7%E6%91%84%E5%83%8F%E6%9C%BA%E5%BD%95%E5%83%8F%E6%9C%BA%E5%AF%B9%E6%8E%A5%E6%96%B9%E5%BC%8F%E8%AE%B0%E5%BD%95/


六、总结:自己动手,丰衣足食

这套系统的好处总结一下:

✅ 不依赖云服务,没有月租
✅ 数据完全自主掌控,不怕泄露
✅ 支持长期存储 + 自动备份
✅ 手机随时查看,体验不输商业产品
✅ 关键是——省钱!省钱!省钱!

虽然前期稍微动了点脑子,但比起每个月被割韭菜,这点学习成本简直微不足道。


最后一句忠告:

科技应该是为你服务的,而不是让你沦为它的付费奴隶。

与其把钱交给那些天天弹窗催续费的App,不如投资一套真正属于自己的家庭安防系统。

毕竟,谁不想做一个既安全、又清醒、还不花冤枉钱的智慧家长呢?


📌 附:装备采购建议清单

名称 推荐型号 价格参考
海康摄像头 HK-Q3S5M-W / DS-IPC-K44H-LWPT ¥180~220
海康WiFi录像机 7802N-S1/W ¥60~140左右
Mini主机 Intel N100小主机 ¥300~600
开源工具 ZLMediaKit(GitHub) 免费

注意:mini主机自带几百G硬盘,够使用了。如果把录像同步到网盘后,可以清理mini主机硬盘中的视频文件。


如果你也厌倦了“云存储绑架”,不妨试试这个方案。
说不定哪天你还能靠这段拍到邻居偷摘你葡萄的视频,在小区业主群里一战成名!


#家用摄像头 #海康威视 #DIY监控 #零月租方案 #拒绝云绑架 #自己动手丰衣足食

Tailscale 4via6 在 Windows 子网路由上的 ICMP 行为说明

(为什么能用“合成 IPv6”访问 Web,却 ping -6 不通)

1. 场景与现象

  • 拓扑:
    A、B 两台 Windows 电脑安装 Tailscale;B 同时作为子网路由器(Subnet Router)。
    C 是 OpenWrt 设备,挂在 B 所在现场的内网。

  • 配置:已开启 4via6,B 宣告(advertise)一个 IPv6 子网,并在 Headscale/后台审批通过。

  • 现象:

    • 从 A 能用 “合成的 IPv6 地址” 打开 C 的 Web 服务(HTTP/HTTPS 正常)。
    • 但从 A 执行 ping -6 <C-的IPv6> 不通。抓包能看到 Echo Request 发出,但收不到回包;在 B 上用 Wireshark 看,没有 ICMP 包进入

2. 关键概念:4via6 与“合成 IPv6”

Tailscale 的 4via6 会把内网 IPv4 目标编码进一个保留的 ULA 段(如 fd7a:115c:a1e0::/48),形成合成 IPv6 地址
例:c0a8:0601 对应 192.168.6.1,于是会看到类似
fd7a:115c:a1e0:b1a0:7:c0a8:0601 这样的目的地址。

  • TCP/UDP 流量:Tailscale 会把发往这些“合成 IPv6”的包转换并送到真实的 IPv4 终端,所以浏览器/SSH/Modbus-TCP 等能正常工作。
  • ICMPv6:在 Windows 客户端 上,当前路径不会把 ICMPv6 Echo 转成 ICMPv4 Echo,因此 ping -6 不会得到回应——包也不会被转发到 B 或 C。

3. 根因(简洁版)

  1. Windows + 4via6 的协议支持面
    只对 TCP/UDP 做 IPv6→IPv4 转换;ICMPv6 不在转换范围内,所以 ping -6 失败,而 Web 正常。

  2. Windows 子网路由对“原生 IPv6 子网”的转发能力有限
    即便 B 宣告了 IPv6 子网,Windows 作为子网路由在 IPv6 转发与邻居发现处理上并不如 Linux 完整稳定,导致跨站点的 ICMPv6 转发经常不可用

    换成 Linux/OpenWrt 当子网路由后,同样配置下 ping -6 通常即可恢复正常。

4. 结论一句话

在 Windows 子网路由 + 4via6 的组合中,HTTP/UDP 能走,但 ICMPv6 不会被转换/转发,所以 ping -6 不通是预期行为;Linux 子网路由可以正常转发 ICMPv6。

5. 可选解决/替代方案

方案 A(最省事):用 IPv4 做连通性检查

  • 改用 ping 192.168.6.x(ICMPv4),或
    Test-NetConnection 192.168.6.x -Port 80/22 验证端口。
  • 适合“只想知道在线与否”的场景;无需改动现场。

方案 B(推荐):把 子网路由器 换成 Linux/OpenWrt

在 B 侧放一台小 Linux/OpenWrt 盒子(可以是现有 OpenWrt 路由或小主机/容器)作为 Tailscale 子网路由:

1
2
3
4
5
6
7
8
9
# 1) 开启 IPv6 转发
sysctl -w net.ipv6.conf.all.forwarding=1
# 持久化:/etc/sysctl.conf 里设置 net.ipv6.conf.all.forwarding=1

# 2) tailscaled 启动并宣告 IPv6 前缀
tailscale up --advertise-routes=<你的IPv6前缀>/64 --accept-dns=false
# 按需添加其它前缀,或配合 --accept-routes

# 3) 在 Headscale/管理端批准这些 routes

完成后,从 A 直接 ping -6 <C 的**原生** IPv6>,ICMPv6 会被正常转发;B 上能抓到往返的 ICMPv6。

方案 C(最干净):让 C 直接加入 Tailnet

在 OpenWrt 上安装 Tailscale,让 C 成为独立节点(拥有自己的 fd7a:... 节点地址):

1
2
3
4
5
opkg update
opkg install tailscale
/etc/init.d/tailscaled enable
/etc/init.d/tailscaled start
tailscale up

A 直接 ping -6 <C 的 Tailscale IPv6>,端到端走 Tailscale,绕开子网路由的限制。

方案 D(保留 Windows,但想要 ping -6

在 B 侧增加小型 Linux NAT64/CLAT(例如 Jool EAMT),专门把发往 fd7a:115c:a1e0::/96ICMPv6⇄ICMPv4 做转换;并在 B/网关上加静态路由把该前缀指向这台转换器。

该方案能打通 ping -6,但复杂度与维护成本较高,一般不如方案 B/C。

6. 快速排障自检

  • 能开 Web,ping -6 不通:90% 是 ICMPv6 未转换/未转发的已知限制。

  • 在 A 执行:

    • ping 192.168.6.x(C 的 IPv4):若可达,证明 4via6 通路正常,只是 ICMPv6 不支持。
    • route print -6Get-NetRoute -AddressFamily IPv6:若看到目的合成地址不断触发 Neighbor Solicitation 而无回应,这是主机把目标当“链路内”,但 Tailscale 不会为其应答/转发 ICMPv6 的典型表现。
  • 在 B 抓包(Tailscale/Wintun 接口):若看不到来自 A 的 ICMPv6,则说明包没被交给子网路由(仍印证“Windows 不处理 ICMPv6 转换”)。

7. 常见问答

  • 为什么浏览器能打开?
    因为 TCP(和大多数 UDP)会被 4via6 转换并转发到 IPv4 终端;ICMPv6 不在这个转换面里。
  • UDP 是否也能走?
    能(取决于具体协议/端口与策略),多数 UDP 通过 4via6 没问题;问题集中在 ICMPv6。
  • 以后 Windows 会支持吗?
    以当前版本看没有现成开关可启用。若对 ICMPv6 必须要,建议按方案 B/C 处理。

推荐落地选型

  • 想保留 Windows 当子网路由:接受“不能 ping -6”,用 IPv4 ping/端口探测替代。
  • 需要完整 IPv6(含 ping -6:把子网路由迁到 Linux/OpenWrt;或让终端设备直接跑 Tailscale。

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使用

0%