#Docker #WebDAV #frp #Samba #openclaw #服务器运维

背景

路由器设备更换,局域网网段从 192.168.0.0/24 整体迁移至 192.168.2.0/24。N100 重启后各服务需要恢复,同时处理 openclaw 权限修复遗留的路径问题、WebDAV 公网访问故障及 LLM 连接问题。

本文涉及的 openclaw 权限修复遗留问题,源自 《N100 安全事件响应:OpenClaw SSH 暴力破解与 fail2ban 加固》


一、磁盘检查与挂载恢复

btrfs RAID1 健康验证

1
2
3
# 只读检查(不修改数据),双盘 RAID1 只需指定其中一块
sudo btrfs check --readonly /dev/sdc1
# 结果:no error found ✅

两块盘共享同一 fsid(RAID1 特性),num_devices: 2btrfs check 会自动找到另一块盘,无需手动指定两个设备。

开机挂载验证

重启后确认两个挂载点均正常:

1
2
/dev/sdc1 on /nfs type btrfs (rw,noatime,compress=zstd:3,...)
/dev/sdb2 on /smb type exfat (rw,relatime,uid=1000,gid=1000,...)

Docker 启动顺序修复

原问题:Docker 容器在磁盘挂载完成前启动,导致依赖 /nfs/smb 的容器失败。

/etc/systemd/system/docker.service.d/wait-mounts.conf 添加:

1
2
[Unit]
After=nfs.mount smb.mount

使 Docker 守护进程等待两个挂载点就绪后再启动。


二、openclaw 路径修复

问题:/root/.openclaw 权限拒绝

openclaw 以 user: 1000:1000(node 用户)运行,但 openclaw.json 中所有 agent 的 workspaceagentDir 路径仍指向 /root/.openclaw/,导致:

1
Error: EACCES: permission denied, mkdir '/root/.openclaw/workspace-download'

修复

openclaw.json 中所有路径从 /root/.openclaw 改为 /home/node/.openclaw

1
2
3
4
5
6
7
8
9
10
{
"agents": {
"list": [
{ "workspace": "/home/node/.openclaw/workspace" },
{ "workspace": "/home/node/.openclaw/workspace-download" },
{ "workspace": "/home/node/.openclaw/workspace-heartbeat",
"agentDir": "/home/node/.openclaw/agents/heartbeat" }
]
}
}

exec-approvals socket 路径修复

同样原因,exec-approvals.json 的 socket 路径需指向 node 用户家目录:

1
2
3
4
5
{
"socket": {
"path": "/home/node/.openclaw/exec-approvals.sock"
}
}

三、WebDAV 公网访问恢复

架构说明

WebDAV 公网访问链路:

1
2
3
4
外部客户端
→ VPS:[VPS_IP]:4455(nginx HTTPS,SSL 终结)
→ VPS:127.0.0.1:4456(frps 隧道口)
→ N100:127.0.0.1:4455(Caddy WebDAV 容器)

关键区分:

  • 4455:对外的 nginx HTTPS 端口(Alibaba Cloud 安全组开放)

  • 4456:frp 内部隧道端口(frpc remote_port),VPS 内部使用,安全组无需对外开放

/etc/frp/frpc.ini 配置:

1
2
3
4
5
[webdav_tcp]
type = tcp
local_ip = 127.0.0.1
local_port = 4455 # N100 本地 Caddy 端口
remote_port = 4456 # VPS 上 frps 监听端口(内部)

VPS /etc/nginx/sites-available/port_4455.conf 核心配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 4455 ssl;
ssl_certificate /etc/nginx/ssl/...;

client_max_body_size 0; # 支持大文件上传

location /milin/ {
proxy_pass http://127.0.0.1:4456/milin/;
proxy_set_header Depth $http_depth;
proxy_set_header Destination $http_destination;
proxy_set_header Overwrite $http_overwrite;
}
location /joplin/ {
proxy_pass http://127.0.0.1:4456/joplin/;
# ... 同上
}
}

故障排查过程

检查项 结果 结论
N100 本地 curl http://127.0.0.1:4455/milin/ 401 Caddy 正常
frpc 日志 webdav_tcp start proxy success 隧道建立
VPS ss -tlnp | grep 4456 LISTEN frps 监听正常
外网 curl https://[VPS_IP]:4455/milin/ 503/超时 安全组未生效

根本原因:Alibaba Cloud 安全组的 4455 规则虽已存在(创建于 2025-07-09),但实际未生效。解决方案:在控制台删除旧规则并重新添加,强制规则刷新后立即恢复。

验证

1
2
3
4
5
6
# LAN 直接访问
curl --noproxy '*' http://192.168.2.14:4455/milin/ # 401 ✅

# 公网 HTTPS(通过 nginx + frp 隧道)
curl --noproxy '*' -sk https://[VPS_IP]:4455/milin/ # 401 ✅
curl --noproxy '*' -sk https://[VPS_IP]:4455/joplin/ # 401 ✅

401 表示认证请求被正确处理,WebDAV 服务链路完整。

注意:本地 curl 使用 v2raya HTTP 代理(http_proxy=http://127.0.0.1:1087),测试公网访问必须加 --noproxy '*' 绕过本地代理,否则 503 是代理超时而非实际服务返回。

目录结构

WebDAV 路径 实际目录 内容
/milin/ /smb(exfat 3.6T) 影视、照片、备份等
/joplin/ /nfs/joplin(btrfs 1.8T) Joplin 笔记同步

四、SMB 服务

现状

1
systemctl is-active smbd nmbd   # active active

挂载点 /smb/Movies 目录结构正常(电影、电视剧、动画)。

已知问题:Samba 密码数据库为空

Samba 维护独立于 Linux 系统账号的密码数据库(/var/lib/samba/private/passdb.tdb)。即使 Linux 用户存在,若未通过 smbpasswd 注册,认证会失败,map to guest = Bad User 会将其映射为 guest,而 valid users = @smbfilm 的共享 guest 无权访问。

修复方法(需在 N100 本地执行,须交互式输入密码):

1
2
3
sudo smbpasswd -a milin
sudo smbpasswd -a nll2025
sudo smbpasswd -a filmplayer

SMB 共享配置

1
2
[电影]   path=/smb/Movies  valid users=@smbfilm  read only=no
[nfs_root] path=/smb valid users=milin read only=no

smbfilm 组成员:filmplayernll2025milin


五、局域网网段迁移(192.168.0.x → 192.168.2.x)

路由器更换后,所有设备 IP 变更:

设备 旧 IP 新 IP
N100(MyUbuntu) 192.168.0.155 192.168.2.14
milin_desktop 192.168.0.111 192.168.2.10
milin_win11 192.168.0.195 192.168.2.12

涉及更新的配置文件

文件 变更内容
openclaw/openclaw.json Ollama baseUrl 更新
openclaw/.env OPENCLAW_LLM_API_URL 更新
openclaw/init-ssh.sh SSH HostName 更新
openclaw/data/.ssh/config SSH HostName 更新
openclaw/data/workspace-download/tools/qbit_shell.sh QB_HOST 更新
openclaw/data/workspace-download/AGENTS.md qBittorrent URL 更新
~/.ssh/config(Mac) 已在迁移前更新,无需修改

批量替换命令示例:

1
2
3
sed -i 's|192\.168\.0\.155|192.168.2.14|g' <文件列表>
sed -i 's|192\.168\.0\.111|192.168.2.10|g' <文件列表>
sed -i 's|192\.168\.0\.195|192.168.2.12|g' <文件列表>

六、openclaw LLM 连接问题(已解决)

错误现象

1
2
3
All models failed (2):
deepseek/deepseek-chat: LLM idle timeout (120s)
ollama/qwen3:8b: Connection error (EHOSTUNREACH)

根因分析

DeepSeek(云端 API)

N100 宿主机本身可正常访问外网(curl https://api.deepseek.com 返回 401),但 openclaw Docker 容器完全无法出网(连 1.1.1.1 也超时)。

根本原因是 v2raya nftables 透明代理拦截了 Docker 容器流量

v2raya 使用 table inet v2raya 中的 tp_rule 链,在 PREROUTING 阶段(优先级 dstnat-5,即 -105)对所有 TCP 流量做判断:

1
2
3
4
5
6
chain tp_rule {
ip daddr @whitelist return ← 目标是私有 IP → 直连
ip daddr @interface return ← 目标是本机接口 IP → 直连
...
meta l4proto tcp redirect to :52345 ← 其余全部重定向到 v2ray 代理
}

关键点:白名单判断的是目标地址(daddr),而非源地址。172.16.0.0/12 在白名单中表示"目标是 Docker 子网则直连",而不是"来自 Docker 子网则放行"。因此 Docker 容器(源 IP 172.20.0.x)访问外网 IP 时,目标地址不在白名单,流量被 redirect to :52345——目标改写为 127.0.0.1:52345 后进入 INPUT 链,无法再转发,最终丢失。

诊断确认:

1
2
3
# 容器内测试
docker exec openclaw curl -s --max-time 5 http://192.168.2.1 # 307 ✅(LAN 路由器可达,daddr 在白名单)
docker exec openclaw curl -s --max-time 5 http://1.1.1.1 # 000 ❌(外网不可达,daddr 不在白名单被重定向)

Ollama(本地)

milin_desktop(192.168.2.10)当前不在线,EHOSTUNREACH 属正常情况,机器开机后可恢复。

修复过程

第一步:v2raya nftables 添加源地址放行规则

tp_rule 链最前面插入一条基于源地址的 return 规则,使来自 Docker 子网的流量直接跳过重定向:

1
sudo nft insert rule inet v2raya tp_rule ip saddr 172.16.0.0/12 return

验证插入结果:

1
2
sudo nft list chain inet v2raya tp_rule
# 预期 tp_rule 第一条为:ip saddr 172.16.0.0/12 return

第二步:发现并移除错误的 HTTP_PROXY 环境变量

添加 nft 规则后 DeepSeek 仍然 000。排查发现:前期排查时在 docker-compose.yml 中加入了 HTTP_PROXY=http://172.20.0.1:20175,但对应的转发服务已不存在,导致容器内所有 HTTPS 请求先尝试连接死掉的代理端口,直接超时。

临时绕过验证:

1
2
3
4
docker exec -e HTTPS_PROXY="" -e HTTP_PROXY="" openclaw \
curl -s --max-time 10 https://api.deepseek.com/v1/models \
-o /dev/null -w 'deepseek: %{http_code}\n'
# 返回 401 ✅ — 网络通,认证未通过属预期

修复:删除 docker-compose.yml 中的三行 proxy 环境变量并重建容器:

1
2
sed -i '/HTTP_PROXY\|HTTPS_PROXY\|NO_PROXY/d' docker-compose.yml
docker compose up -d --force-recreate

第三步:持久化 nft 规则

v2raya 重启时会重建 table inet v2raya,手动插入的规则会丢失。通过 systemd drop-in 在 v2raya 启动后自动补规则:

1
2
3
4
5
6
sudo mkdir -p /etc/systemd/system/v2raya.service.d
sudo tee /etc/systemd/system/v2raya.service.d/docker-bypass.conf << 'EOF'
[Service]
ExecStartPost=/bin/bash -c "sleep 3 && nft insert rule inet v2raya tp_rule ip saddr 172.16.0.0/12 return || true"
EOF
sudo systemctl daemon-reload

访问模式说明

修复后 Docker 容器的网络访问方式:

场景 访问路径
DeepSeek、其他直连服务 直连出网(Docker MASQUERADE → 路由器 → 互联网)
GFW 屏蔽的服务(搜索 API 等) HTTP_PROXY → Python 转发代理 → v2raya(20171)→ 出网(见第七节)
Ollama(局域网) 直接 LAN 路由

七、权限审批与代理补充修复

7.1 openclaw exec-approvals 设置

openclaw 的 exec-approvals 默认为 allowlist 模式。问题:download agent 执行的命令含 shell 重定向(2>/dev/null),allowlist 模式不支持对重定向符做 glob 匹配,导致相关命令永远无法加入白名单,UI 的"始终允许"按钮灰显。

修复:将 download agent 单独配置为 security: "full" + ask: "off",其他 agent 保持 allowlist 模式不变:

1
2
3
4
5
6
7
8
python3 -c "
import json
with open('/home/milin/dockerfile/openclaw/data/exec-approvals.json') as f:
d = json.load(f)
d['agents']['download'] = {'security': 'full', 'ask': 'off'}
with open('/home/milin/dockerfile/openclaw/data/exec-approvals.json', 'w') as f:
json.dump(d, f, indent=2)
"

7.2 fail2ban 误封 Mac LAN IP

现象:Mac(192.168.2.13)无法 SSH 到 N100,fail2ban 将其封禁。

原因:Mac ~/.ssh/config 中裸 IP 192.168.2.14 未匹配任何 Host 条目,SSH 使用 Mac 本地用户名 lin(而非 milin)登录,连续认证失败触发 fail2ban。

修复

1
2
3
4
5
6
7
8
9
# 解封 Mac IP
sudo fail2ban-client set sshd unbanip 192.168.2.13

# 添加 LAN 子网到免封白名单
sudo tee /etc/fail2ban/jail.d/local.conf << 'EOF'
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.2.0/24
EOF
sudo systemctl reload fail2ban

根本预防:Mac 的 ~/.ssh/configMyUbuntuLocal 条目同时匹配 Host 名和裸 IP,确保裸 IP 也使用正确的用户名:

1
2
3
4
Host MyUbuntuLocal 192.168.2.14
HostName 192.168.2.14
User milin
IdentityFile ~/.ssh/id_rsa

7.3 Docker 容器 HTTP 代理(web_search 修复)

背景:第六节的 nft saddr bypass 规则让 Docker 容器可以直连出网,但无法访问 GFW 屏蔽的站点。openclaw 的 web_search 工具需要通过 v2raya 代理才能工作。

解决方案:在宿主机 Docker bridge 网关(172.20.0.1)上运行 Python TCP 转发代理,将容器发来的 HTTP 代理请求转发到 v2raya 的 HTTP 代理端口。

v2raya 代理端口:20170 = SOCKS5,20171 = HTTP proxy(curl 验证 DuckDuckGo 返回 200)

脚本保存至 /home/milin/v2raya-docker-proxy.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import socket, threading

def pipe(s1, s2):
try:
while True:
d = s1.recv(65536)
if not d: break
s2.sendall(d)
except: pass
finally:
for s in (s1, s2):
try: s.close()
except: pass

srv = socket.socket(); srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("172.20.0.1", 20170)); srv.listen(100)
while True:
c, _ = srv.accept()
p = socket.socket()
try:
p.connect(("127.0.0.1", 20171))
for a,b in ((c,p),(p,c)): threading.Thread(target=pipe,args=(a,b),daemon=True).start()
except: c.close(); p.close()

UFW 放行规则(按 Docker 子网范围放行,不依赖具体 bridge 名称):

1
sudo ufw allow from 172.16.0.0/12 to any port 20170 proto tcp

注意docker compose up --force-recreate 会重建容器网络,导致 bridge 接口名称(如 br-ceefd7058599)变更。UFW 若绑定具体 bridge 名称则失效,必须改用源 IP 子网匹配。

openclaw .env 追加代理配置:

1
2
3
HTTP_PROXY=http://172.20.0.1:20170
HTTPS_PROXY=http://172.20.0.1:20170
NO_PROXY=127.0.0.1,localhost,192.168.2.0/24,172.20.0.0/16

NO_PROXY 范围:需涵盖整个 LAN 子网(192.168.2.0/24),而非单个 IP。否则同网段的 Ollama(192.168.2.10)、qBittorrent(192.168.2.14:9090)等服务请求会被错误地发往 HTTP 代理,导致 ETIMEDOUT。

持久化:更新 v2raya systemd drop-in,在 nft 规则之后同步启动 Python 代理:

1
2
3
4
# /etc/systemd/system/v2raya.service.d/docker-bypass.conf
[Service]
ExecStartPost=/bin/bash -c "sleep 3 && nft insert rule inet v2raya tp_rule ip saddr 172.16.0.0/12 return || true"
ExecStartPost=/bin/bash -c "sleep 4 && nohup python3 /home/milin/v2raya-docker-proxy.py > /tmp/v2raya-proxy.log 2>&1 &"

验证

1
2
docker exec openclaw curl -s --max-time 15 'https://duckduckgo.com/' -o /dev/null -w 'duckduckgo: %{http_code}\n'
# duckduckgo: 200 ✅

八、N100 重启后的恢复 checklist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 确认磁盘挂载
findmnt /nfs /smb

# 2. 检查所有容器状态
docker ps -a --format 'table {{.Names}}\t{{.Status}}'

# 3. 启动 unless-stopped 容器(若用 docker stop 手动停止过)
docker start openclaw webdav hermes-agent

# 4. 确认 frpc 连通
journalctl -u frpc -n 5 --no-pager | grep 'start proxy success'

# 5. 测试 WebDAV
curl --noproxy '*' -sk https://[VPS_IP]:4455/milin/ -o /dev/null -w '%{http_code}'

# 6. 确认 Docker HTTP 代理进程运行
ss -tlnp | grep 20170 # 应有 172.20.0.1:20170 监听(python3)

# 7. 验证 openclaw 容器出网
docker exec openclaw curl -s --max-time 10 'https://duckduckgo.com/' -o /dev/null -w 'duckduckgo: %{http_code}\n'

遗留问题

问题 状态 说明
Samba 用户密码注册 ⏳ 待执行 需在 N100 本地运行 sudo smbpasswd -a <user>
v2raya 透明代理绕过 Docker ✅ 已解决 nft 源地址规则 + 移除 HTTP_PROXY 环境变量 + systemd drop-in 持久化
Docker 容器访问 GFW 屏蔽站点 ✅ 已解决 Python TCP 代理(172.20.0.1:20170 → 127.0.0.1:20171)+ HTTP_PROXY 环境变量
UFW bridge 名称失效(force-recreate 后) ✅ 已解决 UFW 规则改为按 172.16.0.0/12 子网匹配,不依赖 bridge 名称
NO_PROXY 未覆盖全 LAN 导致 Ollama ETIMEDOUT ✅ 已解决 NO_PROXY 改为 192.168.2.0/24,覆盖所有 LAN 服务
openclaw exec-approvals 过严 ✅ 已解决 download agent 设为 security: full + ask: off
fail2ban 误封 Mac LAN IP ✅ 已解决 ignoreip 添加 192.168.2.0/24 + SSH config 修复裸 IP 匹配
milin_desktop Ollama 连通 ⏳ 待验证 IP 已更新为 192.168.2.10,待机器上线后验证
milin_win11 Ollama 连通 ⏳ 待验证 IP 已更新为 192.168.2.12,待机器上线后验证
Mac SSH 连接 N100 已知密钥更新 ✅ 已处理 ssh-keygen -R 192.168.2.14 && ssh -o StrictHostKeyChecking=accept-new MyUbuntuLocal

延伸阅读