context
- 迷你主机上container跑 网心云docker
- 只能插到网关,不能登录网关和路由器后台
Pre-Requirements
- NAT相关知识
TLDR
现象:外网能连通 TCP 18888,但平台仍显示 Port-Restricted
真因:平台评级更看重 UDP/STUN 行为 + 网络栈是否多一层 NAT(docker bridge);你只打通了 TCP,可达性≠NAT 评级
OEA Lite 为何能“插上就映射公网”:它自己做了 UPnP IGD discovery + AddPortMapping + 续租
复刻方案:host 网络(减少 docker NAT)+ UPnP 同时映射 TCP/UDP + 定时续租 + 外网验证
如果你做完仍不变:那是 上游 NAT 策略/平台口径导致,UPnP 只能改善可达性,未必改变“行为评级”
# Beszel 访问问题处理记录
日期:2026-01-10
## 目标
- 保持 Dokploy 动态路由管理(不写死静态路由)。
- 解决 Traefik 回源 504(宿主机无法访问 overlay 容器 IP)。
## 核心结论
- Traefik 以宿主机 systemd 服务运行时,Dokploy 动态路由回源地址是 overlay IP(如 10.0.1.2:8090),宿主机无法访问,导致 504。
- 需要把 Traefik 切回容器方式加入 `dokploy-network`,与 Dokploy 的动态路由机制一致。
## 已做代码改动(本地)
文件:`modules/nixos/vps/dokploy-server/default.nix`
1) 保持 `/etc/dokploy` 目录结构,ACME 文件由容器写入:
- `acme.json` owner 改为 `root:root 0600`(容器 root 可写)。
- 中文注释说明 Traefik 容器写入方式。
2) 移除 systemd 原生 Traefik 服务配置:
- 删除 `services.traefik` 与 `ReadWritePaths` 等配置。
3) 新增 Traefik 容器服务:
- `systemd.services.dokploy-traefik`
- 使用 `docker run` 加入 `dokploy-network`
- 挂载 `/etc/dokploy/traefik/traefik.yml` 与 `dynamic/`
- 暴露 80/443(tcp/udp)
- 采用 `traefik:v3.6.1`
## 远端已验证事实(排障证据)
- Traefik API 显示回源为 overlay IP(示例):`http://10.0.1.2:8090`
- 宿主机无法访问该 IP:`curl http://10.0.1.2:8090` 超时
## 待执行步骤(生效/验证)
1) 通过 colmena / nixos-rebuild 应用配置。
2) 在服务器执行:
- `sudo systemctl stop traefik`(确保旧 systemd Traefik 不占用端口)
- `sudo systemctl start dokploy-traefik`
3) 验证:
- `curl -I -H "Host: beszel.lucc.dev" http://127.0.0.1`
- `curl -vk --resolve beszel.lucc.dev:443:127.0.0.1 https://beszel.lucc.dev --max-time 10`
- `curl -s http://127.0.0.1:8080/api/http/services | jq '.[] | {name:.name, servers:.loadBalancer.servers}'`
期望回源地址为可访问的容器内部网络地址(overlay 可达,因为 Traefik 也在 overlay 内)。
## 备注
- 本次方案严格依赖 Dokploy 动态路由,不新增静态 file provider 路由。
- ddd: ddd
- zzz: ddd
tailscale/netbird
作为 host=nixos-vps,所有VPS就既是 tailscale 的 client (=node) 和 Derp Relay Server
需要注意的是,对于 client来说,部署是很简单的。
但是对于 Derp 来说,要求必须TLS证书,所以使用ACME的DNS-01签发。这里的问题在于,URL的 subdomain 需要动态生成(不可能多台VPS去抢注同一个域名,如果是同一个固定域名,那么只有第一个VPS可以抢注成功,后面的都会失败,也不符合我们的需求)。
这里有两台路线,
一条是直接使用为colmena写的targets metadata作为hosts数据源,让我们可以动态生成 config.networking.hostName。但是这里的问题在于,如果使用colmena targets作为数据源,有两个需求无法满足:1、我希望不只是 hosts/nixos-vps,其他hosts也可以直接实现类似效果。2、不只是colmena,其他部署方式也可以动态生成hostName。总之因为实现复杂性,所以放弃该方案。
另一个方案则简单得多,维护两套 hosts metadata确实麻烦,但是从实现来说,却容易得多。直接复用了之前在 vars/networking.nix 里给 singbox 维护的 vpsNodes作为数据源。然后添加了 lib/node-id.nix 用来通过IP匹配node,又在colmena里实现了动态生成 hostName(而非直接修改 hosts/nixos-vps 的 hostName,注意这点有天壤之别)
zzz
```markdown
你看下相应config.json是否真的有问题,对比一下本地的
sudo cat /run/secrets/rendered/singbox-client.json
---
排查到这个remote host的问题之后,看看是否我们的 singbox的nix配置是否真的有问题
• Ran sshpass -p '159357' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/
│ null 100.81.204.63 'sudo cat /run/secrets/rendered/singbox-client.json'
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
sudo: a terminal is required to read the password; either use ssh's -t option or
configure an askpass helper
sudo: a password is required
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S cat /run/secrets/rendered/
│ singbox-client.json'
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
[sudo] password for luck: cat: /run/secrets/rendered/singbox-client.json: No such
file or directory
Connection to 100.81.204.63 closed.
• Ran sshpass -p '159357' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/
│ null 100.81.204.63 'sudo ls -la /run/secrets /run/secrets/rendered 2>/dev/null;
│ sudo find /run/secrets -maxdepth 3 -type f | head -n 50'
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
sudo: a terminal is required to read the password; either use ssh's -t option or
configure an askpass helper
sudo: a password is required
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S sh -lc "ls -la /run/
│ secrets /run/secrets/rendered 2>/dev/null; echo ---; find /run/secrets
│ … +1 lines
└ [sudo] password for luck: lrwxrwxrwx 1 root root 17 Jan 12 17:51 /run/secrets
-> /run/secrets.d/39
---
Connection to 100.81.204.63 closed.
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S sh -lc "ls -la /run/
│ secrets.d/39; echo ---; find /run/secrets.d/39 -maxdepth 2 -type f"'
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
[sudo] password for luck: total 76
… +40 lines
/run/secrets.d/39/API_context7
Connection to 100.81.204.63 closed.
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S sh -lc "ls -la /var/lib/
│ sing-box /var/cache/sing-box 2>/dev/null; echo ---; ls -la /var/lib/sing-box
│ … +2 lines
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
[sudo] password for luck: /var/lib/sing-box:
… +20 lines
"inet4_range": "198.18.0.0/15",
Connection to 100.81.204.63 closed.
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S systemctl cat
│ sing-box.service'
└ [sudo] password for luck: # /etc/systemd/system/sing-box.service -> /nix/store/
njxzwabfv6a9p0dqcb3jfdjpbdvlbhdy-sing-box-1.12.14/lib/systemd/system/
… +39 lines
WantedBy=multi-user.target
Connection to 100.81.204.63 closed.
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S ls -la /dev/net/tun; printf
│ "\n"; printf "159357\n" | sudo -S sysctl net.ipv4.ip_forward
│ … +1 lines
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
[sudo] password for luck: crw-rw-rw- 1 root root 10, 200 Jan 5 22:03 /dev/net/
… +3 lines
net.ipv6.conf.all.disable_ipv6 = 1
Connection to 100.81.204.63 closed.
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S systemctl show
│ sing-box.service -p DevicePolicy -p DeviceAllow -p PrivateDevices -p
│ … +3 lines
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
[sudo] password for luck: DevicePolicy=auto
… +11 lines
RestrictAddressFamilies=~
Connection to 100.81.204.63 closed.
• I’m trying to figure out why the tun interface setup is failing with permission
denied, considering capabilities like CAP_NET_ADMIN might not be correctly applied
or systemd is denying access despite settings. I’m focusing on the configuration
for the tun inbound, looking for settings like auto_route or interface_name that
might cause this, and planning to check the config directly by grepping relevant
sections.
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S sh -lc "jq '.inbounds[] |
│ select(.type==\"tun\")' /var/lib/sing-box/config.json"'
└ zsh:1: no matches found: select(.type=="tun") /var/lib/sing-box/config.json"
zsh:1: no matches found: printf "159357\n" | sudo -S sh -lc "jq .inbounds[]
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S bash -lc "jq \".inbounds[]
│ | select(.type==\\\"tun\\\")\" /var/lib/sing-box/config.json"'
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
[sudo] password for luck: bash: line 1: jq: command not found
Connection to 100.81.204.63 closed.
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S python - <<"PY"
│ import json
│ … +7 lines
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
[sudo] password for luck: {
… +9 lines
}
Connection to 100.81.204.63 closed.
• I'm thinking the permission denied error when configuring the tun interface likely
stems from having IPv6 disabled system-wide, while singbox still tries to assign an
IPv6 address to the tun device. I'm exploring whether re-enabling IPv6 or modifying
singbox's config generator to omit IPv6 addresses when IPv6 is disabled would be
the best fix. I also want to find a workaround so that Nix operations can prefer
IPv4 without globally disabling IPv6, possibly by using networking.extraHosts to
force IPv4 for specific domains like cache.nixos.org. Next, I plan to examine
singbox’s config generation in lib/singbox/client-config.nix to check for IPv6
address inclusion.
• Ran sshpass -p '159357' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/
│ null 100.81.204.63 'sed -n "1,200p" /home/luck/Desktop/dotfiles/lib/singbox/
│ client-config.nix'
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
{
… +59 lines
inherit configJson clientConfigPath templatesContent;
}
• Ran sshpass -p '159357' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/
│ null 100.81.204.63 'sed -n "1,200p" /home/luck/Desktop/dotfiles/lib/singbox/
│ config.nix'
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
{
… +107 lines
services = [ ];
}
• I'm checking how disabling IPv6 causes permission issues when adding IPv6 addresses
to tun interfaces. It looks like re-enabling IPv6 via sysctl and restarting sing-
box should fix the immediate problem, though TLS errors might still appear. For a
more robust fix, I’m considering configuring Nix to use IPv4 for caching while
keeping IPv6 enabled system-wide.
• Ran sshpass -p '159357' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/
│ dev/null 100.81.204.63 'printf "159357\n" | sudo -S sysctl -w
│ net.ipv6.conf.all.disable_ipv6=0 net.ipv6.conf.default.disable_ipv6=0; printf
│ … +2 lines
└ Warning: Permanently added '100.81.204.63' (ED25519) to the list of known hosts.
[sudo] password for luck: net.ipv6.conf.all.disable_ipv6 = 0
… +20 lines
Jan 12 18:08:05 nixos-homelab systemd[1]: Started sing-box service.
Connection to 100.81.204.63 closed.
• I’m reviewing the sing-box config with IPv6 re-enabled since disabling it caused
permission errors. The config includes an IPv6 address in the tun inbound, so it’s
incompatible with IPv6 disabled. The nix TLS fetch error over IPv6 suggests forcing
IPv4 for cache.nixos.org may help. I’ll explain that the NixOS sing-box config is
generated at /var/lib/sing-box/config.json, not from the missing secret file, and
propose options: keep IPv6 enabled and fix Nix via host overrides, or adjust sing-
box config to omit IPv6 address and keep IPv6 disabled, possibly with persistent
sysctl changes. I want to check with the user about committing those changes.
─ Worked for 12m 40s ────────────────────────────────────────────────────────────────
• 我看了,结论是 sing-box 配置本身没坏,是 IPv6 被你我临时关闭后导致 tun 配置失败。
关键发现
- /run/secrets/rendered/singbox-client.json 在 NixOS 上并不存在(Darwin 才会
render),NixOS 直接用 /var/lib/sing-box/config.json
- /var/lib/sing-box/config.json 的 tun inbound 里包含 IPv6 地址:
- "fdfe:dcba:9876::1/126"
- 你我之前用 sysctl 关了 IPv6(为了解决 cache.nixos.org 的 TLS 失败),结果 sing-
box 起 tun 时配置 IPv6 地址就报 permission denied
我已经在远端临时 恢复 IPv6 并重启 sing-box,服务已正常运行:
- net.ipv6.conf.all.disable_ipv6 = 0
- sing-box.service 现在是 active (running)
接下来你想怎么处理,我给两种方向:
1. 保留 IPv6 开启(推荐),然后我帮你让 Nix 固定走 IPv4 访问 cache.nixos.org(例如
通过 /etc/hosts 绑 IPv4,或者 Nix 层面强制 IPv4)
2. 继续禁用 IPv6,我把 sing-box 配置改成“IPv6 关闭时不写 v6 地址”,避免 tun 报错
你选一个,我继续改。
如果要我对比本地 config,请告诉我你本地指的是哪一份文件路径(例如本机某个
config.json 或 Nix 生成文件),我再做 diff。
1、帮我梳理一下他的这个排查流程
2、为什么会出现上面的问题?以及目前是怎么解决的?
3、是否通常来说 linux kernel 推荐prefer IPv4 基本上就能避免这种问题?
下面按你 3 个问题拆开说(尽量把“他做了什么 → 为什么这么做 → 得到什么结论”串起来)。
## 1)他的排查流程梳理(按时间线)
### A. 先验证“你让他看的那个文件”到底存不存在
1. 目标:读取远端的 `/run/secrets/rendered/singbox-client.json` 来确认渲染出来的 config 是否异常。
2. 过程:
* 直接 `sudo cat ...` 失败:因为远端 `sudo` 需要 TTY/密码(`sudo: a terminal is required...`)。
* 加 `-t` 并用 `sudo -S` 喂密码后能执行,但返回 **No such file or directory**:说明这个路径压根不存在。
> 这一步的意义:先把“是不是 config.json 内容坏了”这个假设落到实处:文件都不存在,就别在这里纠结内容对不对了。
### B. 转而定位“在 NixOS 上 secrets 实际在哪里”
3. 目标:看看 `/run/secrets` 实际指向哪里、有啥内容。
4. 过程:
* `ls -la /run/secrets /run/secrets/rendered`
* 发现 `/run/secrets -> /run/secrets.d/39`(符号链接),于是继续列目录、find 文件。
> 结论:NixOS 这台机的 secrets 组织方式不是你预期的 `rendered/...` 那套(至少这台机上没有)。
### C. 找 sing-box 真正使用的配置来源(而不是你猜的那个 secret)
5. 目标:找 sing-box 在远端实际用的配置文件、以及服务启动方式。
6. 过程:
* 去看 `/var/lib/sing-box`、`/var/cache/sing-box`,并读到了 **`/var/lib/sing-box/config.json`**(这里基本就坐实了“真实配置在这”)。
* `systemctl cat sing-box.service`:确认 systemd unit 来自 Nix store 的 sing-box 包,进一步能推断服务启动参数/权限模型是 systemd 控的。
### D. 围绕“tun permission denied”去验证内核/系统能力限制
7. 目标:解释 tun 为啥会 permission denied(常见方向:`/dev/net/tun`、CAP_NET_ADMIN、systemd sandbox 限制)。
8. 过程:
* 检查 `/dev/net/tun` 是否存在、权限是否正常。
* 检查 `ip_forward`、以及 `net.ipv6.conf.all.disable_ipv6`(这里发现 IPv6 被禁用了)。
* `systemctl show` 看 DevicePolicy/DeviceAllow/PrivateDevices/CapabilityBoundingSet 等限制项。
> 这一步的意义:先排除“tun 设备不存在 / systemd 把设备权限封死 / 没 CAP_NET_ADMIN”这类经典问题。
### E. 直接检查 tun inbound 配置(但遇到工具缺失)
9. 目标:从 config.json 抽出 `.inbounds[] | select(type=="tun")` 看 tun 配了啥。
10. 过程:
* `jq` 用不了(远端没装 jq)。
* 改用 `python` 读 JSON 并打印 tun inbound,成功拿到 tun 的具体配置内容。
### F. 得到关键因果:tun inbound 里含 IPv6 地址,而系统 IPv6 被关闭
11. 观察:tun inbound 配置里包含类似 `fdfe:dcba:9876::1/126` 的 IPv6 地址。
12. 对照:系统层面 `net.ipv6.conf.all.disable_ipv6 = 1`(之前为了绕过 Nix 拉取 cache 的 TLS 问题临时关的)。
13. 推断:sing-box 启 tun 时会尝试给 tun 口加 IPv6 地址;但内核层 IPv6 被禁用时,给接口配置 IPv6 地址会失败,表现成你看到的 permission denied(很多程序不会把这个报错表达得很精确)。
### G. 临时修复验证假设
14. 操作:把 IPv6 打开:
* `sysctl -w net.ipv6.conf.all.disable_ipv6=0 net.ipv6.conf.default.disable_ipv6=0`
15. 重启 sing-box:服务变为 active(running),tun 起来了。
> 这一步非常关键:属于“做最小改动验证因果”的标准排查手法——一改就好,说明根因八九不离十。
---
## 2)为什么会出现这个问题?以及目前怎么解决的?
### 为什么出现
核心是 **“系统层禁 IPv6”** 和 **“sing-box tun 配置里明确要配置 IPv6”** 这两件事撞车了:
* 你们为了处理 `cache.nixos.org` 走 IPv6 时的 TLS/连通性问题,做了 **sysctl 级别的 disable_ipv6**(这是“硬禁”,内核层就不支持了)。
* 但 sing-box 的 tun inbound **配置里写了 IPv6 地址段**(比如 `fd..../126`)。tun 启动时它会执行类似 “给 tun 口加 IPv6 地址/路由” 的动作。
* 内核 IPv6 被硬禁时,这一步必失败 → sing-box 报 tun setup permission denied(或类似的低信息量错误)。
另外一个“误导点”是你最开始关注的 `/run/secrets/rendered/singbox-client.json`:
* 这条路径在这台 NixOS 上并不存在(更像是 Darwin/macOS 那套渲染路径),所以一开始“对比 rendered config”其实对不上真实配置源。
* 真正在跑的是 `/var/lib/sing-box/config.json`。
### 目前怎么解决(现状)
* **临时解法**:把 IPv6 重新打开(sysctl 改回 0)并重启 sing-box → tun 正常起来,服务恢复。
* 但你们最初关 IPv6 的动机(Nix 拉 cache 走 IPv6 有 TLS/网络问题)可能还在,只是暂时不触发或还没重新验证。
### 下一步更稳的两条路线(你们原文里也提到了)
1. **推荐路线:保留 IPv6 开启**(让 tun 这类组件正常工作),再“定向”让 Nix/某些域名走 IPv4(避免 IPv6 链路/运营商/路由导致的 TLS 问题)。
2. 如果你们铁了心要 **系统层继续禁 IPv6**,那就必须同步改 sing-box 的配置生成:在 IPv6 disabled 时不要给 tun 配 IPv6 地址/路由(否则必炸)。
---
## 3)“通常 prefer IPv4 就能避免这种问题吗?”
多数情况下:**是的,比“直接禁 IPv6”更不容易踩坑**;但要理解它的边界。
### 先澄清:prefer IPv4 ≠ 禁用 IPv6
* “prefer IPv4”通常是 **解析/选路偏好**(用户态 resolver/库决定优先用 A 记录),IPv6 依然可用、内核也依然支持给接口配置 IPv6。
* “disable_ipv6=1”是 **内核层硬禁**,结果是:
* 任何程序给接口配 IPv6 地址都会失败(你这次就是这个)
* 依赖 IPv6 的服务/隧道/容器网络也可能出各种奇怪问题
### prefer IPv4 能避免什么
它主要能减少这类问题:
* 某些网络环境 **IPv6 表面可达但质量差**(丢包/MTU/中间盒干扰),导致 HTTPS/TLS/下载偶发失败。
* 应用默认“Happy Eyeballs”挑到 IPv6 路径,但该路径更差。
通过 prefer IPv4,可以让像 Nix 这种“拉取外部 HTTPS 资源”的行为更倾向 IPv4,从而绕开烂 IPv6 链路,同时又不破坏系统对 IPv6 的基本支持(tun/路由/本地地址照样能配)。
### prefer IPv4 解决不了什么
* **如果配置文件里写死了 IPv6 地址**(比如 sing-box tun inbound 就是),你即使 prefer IPv4,程序仍然会去配置 IPv6 地址——只要内核没禁 IPv6,它就没事;但如果你禁了 IPv6,它照样会挂。
* **如果你确实需要 IPv6 端到端能力**(某些内网、隧道、对端只给 AAAA),prefer IPv4 也不该当成“永久方案”,只能算策略性绕行。
### 实操建议(贴合你们这次事故)
* **不要用 sysctl 全局禁 IPv6** 来解决 “某个域名走 IPv6 会出问题”。
* 更稳的是:
* 让系统 IPv6 保持开启(避免 tun/网络组件炸)
* 对 Nix 或 `cache.nixos.org` 做“定向 IPv4”(域名级别或应用级别),把问题缩到最小影响面
如果你愿意,我也可以把“定向让 Nix 访问 cache.nixos.org 走 IPv4”的常见做法整理成你们 NixOS 配置片段(偏保守、可回滚那种),以及对应怎么验证(dig/curl/nix-store/nix build 的验证步骤)。