systemd 用户服务 203/EXEC 错误排查:wstunnel 自启动配置实录
背景
在国内网络环境下,运营商经常对 UDP 流量进行限速或直接封锁,导致 WireGuard 等基于 UDP 的 VPN 连接不稳定甚至完全无法使用。为了解决这个问题,我使用 wstunnel 将 WireGuard 的 UDP 流量封装成 WSS(WebSocket Secure over TLS) 流量。WSS 本质上是建立在 TLS 之上的 WebSocket,端口为 443,看起来和普通的 HTTPS 流量完全一样,运营商难以识别和干扰,从而保证了 VPN 连接的稳定性。
为了方便,我写了一个启动脚本 start-vpn.sh,并使用 systemd 用户服务配置了开机自启,原本一切运行正常。
问题现象:电脑重启后 VPN 无法工作
某天电脑重启后,我发现 VPN 没有正常工作。于是检查 wstunnel 服务的状态:
systemctl --user status wstunnel.service
输出如下:
● wstunnel.service - Wstunnel Client for VPN
Loaded: loaded (/home/wangqiang/.config/systemd/user/wstunnel.service; enabled)
Active: activating (auto-restart) (Result: exit-code) since ...
Process: 12780 ExecStart=/home/wangqiang/start-vpn.sh (code=exited, status=203/EXEC)
Main PID: 12780 (code=exited, status=203/EXEC)

关键信息是 code=exited, status=203/EXEC。这个退出码意味着 systemd 无法执行指定的程序。
排查 203/EXEC 错误
203/EXEC 通常由以下几种原因导致:
- ExecStart 指定的文件不存在
- 文件没有执行权限
- 脚本的 shebang(如
#!/bin/bash)错误或对应的解释器不存在 - 脚本所在文件系统被挂载为
noexec
由于之前服务是正常的,我首先想到是不是文件被移动或删除了。检查原路径:
ls -l /home/wangqiang/start-vpn.sh
结果:
ls: cannot access '/home/wangqiang/start-vpn.sh': No such file or directory
果然,文件不在了。我回想起来,之前整理目录时把脚本移到了 ~/VPN/ 下:
ls -l /home/wangqiang/VPN/start-vpn.sh
输出:
-rwxrwxr-x 1 wangqiang wangqiang 163 May 28 16:41 /home/wangqiang/VPN/start-vpn.sh
问题根源:移动了脚本,但 systemd 服务文件中的 ExecStart 还指向旧路径。重启后 systemd 尝试启动服务时找不到脚本,因此报 203/EXEC 错误并不断自动重启。
修复步骤
1. 修改服务文件中的路径
编辑用户服务文件:
nano ~/.config/systemd/user/wstunnel.service
将 ExecStart 行改为:
ExecStart=/home/wangqiang/VPN/start-vpn.sh

保存后退出(nano 中按 Ctrl+O,回车,再按 Ctrl+X)。
2. 确认脚本具有执行权限(已存在,无需修改)
ls -l /home/wangqiang/VPN/start-vpn.sh
权限中包含 x,无需额外 chmod。
3. 重新加载 systemd 并重启服务
systemctl --user daemon-reload
systemctl --user restart wstunnel.service
4. 检查服务状态
systemctl --user status wstunnel.service
现在输出变为 active (running):
● wstunnel.service - Wstunnel Client for VPN
Loaded: loaded (/home/wangqiang/.config/systemd/user/wstunnel.service; enabled)
Active: active (running) since Mon 2026-06-08 14:01:52 CST; 8s ago
Main PID: 13932 (start-vpn.sh)
Tasks: 6 (limit: 18198)
Memory: 3.7M
CPU: 122ms
CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/wstunnel.service
├─13932 /bin/bash /home/wangqiang/VPN/start-vpn.sh
└─13935 /usr/local/bin/wstunnel client -L "udp://127.0.0.1:51820:127.0.0.1:51820?timeout_sec=0" --tls-sni-override wg.shuijingwanwq.com wss://154.21.196.249:443
并且日志中显示 wstunnel 成功启动、监听 UDP 端口、建立 TLS 连接:
6月 08 14:01:52 ... Started wstunnel.service.
6月 08 14:01:52 ... INFO wstunnel: Starting wstunnel client v10.5.5
6月 08 14:01:52 ... INFO wstunnel::protocols::udp::server: Starting UDP server listening on 127.0.0.1:51820
6月 08 14:02:00 ... INFO wstunnel::protocols::udp::server: New UDP connection from 127.0.0.1:46144
6月 08 14:02:00 ... INFO wstunnel::protocols::tcp::server: Opening TCP connection to 154.21.196.249:443
6月 08 14:02:00 ... INFO wstunnel::protocols::tls::server: Doing TLS handshake using SNI DnsName("wg.shuijingwanwq.com")
5. 验证端口监听
最后确认 UDP 转发端口已经正常监听:
ss -lun | grep 51820
输出:
UNCONN 0 0 127.0.0.1:51820 0.0.0.0:*

一切恢复正常。现在 wstunnel 客户端会随用户登录自动启动,并在意外退出时自动重试(Restart=on-failure)。
为什么用 wstunnel 来加密 WireGuard 流量?
运营商普遍对 UDP 流量实施 QoS 限速或直接丢弃,导致 WireGuard 连接经常超时或断线。wstunnel 将 UDP 数据包封装在 WSS(WebSocket Secure over TLS) 流中——WSS 使用 443 端口和 TLS 加密,从网络特征上看与普通 HTTPS 请求完全一致。运营商的 DPI 设备无法区分这是正常网页访问还是 VPN 隧道,因此不会对流量进行特殊处理,保证了连接的稳定性和带宽。
在启动命令中:
wstunnel client \
-L "udp://127.0.0.1:51820:127.0.0.1:51820?timeout_sec=0" \
--tls-sni-override wg.shuijingwanwq.com \
wss://154.21.196.249:443
wss://表示使用 WebSocket over TLS,服务器端必须配置有效的 TLS 证书。--tls-sni-override用于指定 SNI 名称,使得 TLS 握手时使用wg.shuijingwanwq.com作为服务器名称指示,进一步模拟正常 HTTPS 请求。
通过这种方式,WireGuard 的 UDP 流量被“伪装”成标准的 HTTPS 流量,成功避免了运营商的干扰。
总结与避坑指南
| 常见错误 | 解决方法 |
|---|---|
status=203/EXEC | 检查 ExecStart= 路径是否正确,文件是否存在且有 +x 权限 |
| 脚本手动可运行但 systemd 失败 | 检查脚本 shebang(第一行),避免使用 ~ 相对路径 |
| systemd 修改后配置未生效 | 务必执行 systemctl --user daemon-reload |
服务一直 auto-restart | 查看 journalctl --user -u 服务名 -f 获得详细错误日志 |
特别提醒:如果你移动了脚本的位置,一定记得同步更新 systemd 服务文件中的路径,否则就会出现本文描述的 203/EXEC 错误。修改后需要执行 daemon-reload 才能生效。
另外,如果是用户服务(--user),请确保服务随用户登录启动:systemctl --user enable 服务名。用户服务依赖于用户的 systemd 实例(一般由 systemd --user 在登录时自动启动)。
参考资料
- systemd.exec 手册 – 了解
ExecStart和退出码 journalctl --user -u wstunnel.service -f– 实时查看用户服务日志
通过以上步骤,我快速定位并解决了因移动脚本导致的 203/EXEC 错误,恢复了 wstunnel 的自启动。希望这篇博客能帮助遇到类似问题的朋友少走弯路。
Happy tunneling!