这是 Debian 服务器搭建指南 的 OpenWrt 移植版。配置思路、证书方案、客户端步骤完全一致,差异只在"OpenWrt 怎么落地"这一层。
设计原则:
- 改动最小:不动 LAN/WAN/DHCP/DNS,只追加 fw4 规则和 strongSwan 配置
- 全程可逆:§0.3 做整盘 UCI 备份,§10 一键回滚
- 每步可验证:每节末尾给一条
cmd → 期望输出,不过就回头查- 轻量优先:strongSwan 按需装模块(总 ~2MB),不用
strongswan-full
目标(与 Debian 完全一致)
- [OK] Apple / Windows / Android 用各自系统自带 VPN 客户端(Android 也可装 strongSwan App)
- [OK] 分流:只有
10.0.0.0/16走 VPN,其他流量直连 - [OK] ZeroSSL 证书,DNS-01,90 天自动续期
- [OK] 用户名 + 密码(EAP-MSCHAPv2)登录,加用户改一行
与 Debian 教程的根本区别
| 项 | Debian 教程 | OpenWrt 本教程 | |
|---|---|---|---|
| 角色 | 内网服务器,前级路由器转发端口 | 路由器本身(主路由模式)/ 内网服务器(旁路由模式) | |
| 防火墙 | iptables-persistent | fw4 / nftables + UCI(/etc/config/firewall) | |
| 启动 | systemd (strongswan-starter.service) | procd (/etc/init.d/swanctl) | |
| 配置格式 | 老 stroke:/etc/ipsec.conf + /etc/ipsec.secrets | 新 vici/swanctl:/etc/swanctl/swanctl.conf(snapshot 已弃 stroke) | |
| 证书路径 | /etc/ipsec.d/{private,certs,cacerts}/ | /etc/swanctl/{private,x509,x509ca}/ | |
| 重载配置 | systemctl restart strongswan-starter | swanctl --load-all(不用重启 charon) | |
| 查状态 | ipsec statusall / ipsec listall | swanctl --list-sas / --list-conns / --list-certs | |
| 包管理 | apt | apk(snapshot 已切 apk;老版 opkg 命令一一对应) | |
| IP 转发 | 默认关,要手工开 | 默认已开,不动 | |
| ACME | `curl get.acme.sh \ | sh` | acme-acmesh 包(等价 acme.sh)/ luci-app-acme(有 LuCI 的话) |
| 日志 | journalctl -u ... | logread -e charon -f | |
| VPN 子网集成 | iptables MASQUERADE + FORWARD ACCEPT | XFRM interface (xfrm0):VPN 流量经虚拟接口进入 lan zone,架构上等于 LAN 设备,不需要专门的 vpn zone(详见 §5) |
§0 准备工作
0.1 环境自检
cat /etc/openwrt_release # DISTRIB_RELEASE 应是 SNAPSHOT 或 25.x
uname -r # 完整内核版本, snapshot kmod 必须严格匹配
df -h /overlay # 剩余 > 10MB
free -m # 推荐 128MB+ RAM
date # 时间必须正确, 见 §8.5
uci show network.lan # 看 LAN 全部字段
ip -4 addr show br-lan # 内核里真实下发的 IP/掩码 (最权威)
# 期望: ipaddr=10.0.0.1 且实际是 /16
# 新版可能是 ipaddr='10.0.0.1/16' 一条 CIDR 写法, 没有独立 netmask 字段, 那是正常的
ls /sbin/fw4 && echo "fw4 OK" # 必须有, 没 fw4 是老固件, 本教程不适用
ls /usr/sbin/nft && echo "nft OK" # 必须有
apk --version || opkg --version # 包管理器, snapshot 已是 apk记下:
- 内核版本
KVER=...(后面排错用) - 包管理器:本教程默认
apk(snapshot 已切)。若你跑的是 24.10 老稳定版还是 opkg,把apk add换成opkg install、apk del换成opkg remove即可
验证:
uci get network.lan.ipaddr输出10.0.0.1或10.0.0.1/16都行ip -4 addr show br-lan看到inet 10.0.0.1/16 ...才算真正生效
如果实际不是 /16,先去 LuCI → 网络 → 接口 → LAN 把 IPv4 地址改成 10.0.0.1/16,或者命令行:
uci set network.lan.ipaddr='10.0.0.1/16'
uci delete network.lan.netmask 2>/dev/null
uci commit network
/etc/init.d/network restart客户端断开 LAN 重连一次拿新 IP,再回来继续。改 LAN 是一次性大动作,改完再开始本教程,中途别再改。
service sysntpd restart 强制同步一次。
0.2 部署模式确认(影响 §1.2 / §5.2)
[A] 主路由模式 (本教程默认路径)
公网 ───→ OpenWrt(WAN=公网IP)───→ LAN 10.0.0.0/16
↑ strongSwan 直接听 UDP 500/4500
[B] 旁路由模式 (本教程在每节用 ⓑ 标差异)
公网 ───→ 前级路由器(公网IP)───→ OpenWrt(LAN 内, 10.0.0.X)
↑ 前级要做端口转发 UDP 500/4500 → OpenWrt后续凡是出现 ⓐ 主路由 / ⓑ 旁路由 标签的,按你所在模式选一条做。
0.3 全量备份(可逆的前提)
TS=$(date +%Y%m%d-%H%M)
mkdir -p /root/ikev2-backup/$TS
cp -a /etc/config /root/ikev2-backup/$TS/config
apk list -I > /root/ikev2-backup/$TS/pkglist.before 2>/dev/null \
|| opkg list-installed > /root/ikev2-backup/$TS/pkglist.before
nft list ruleset > /root/ikev2-backup/$TS/nft.before
echo $TS > /root/ikev2-backup/LAST验证:
ls -la /root/ikev2-backup/$TS/
# 期望: config/ pkglist.before nft.before
wc -l /root/ikev2-backup/$TS/pkglist.before # > 50应急回滚(任意阶段失败都可用):
TS=$(cat /root/ikev2-backup/LAST)
cp -a /root/ikev2-backup/$TS/config/. /etc/config/
reboot0.4 拿 DNSPod API Token
照搬 Debian §0.2。
记下:
DP_Id = 123456
DP_Key = abcdef1234567890abcdef1234567890§1 域名 + 公网到达性
1.1 加 A 记录
DNSPod 控制台:vpn → A → 你的公网 IP。
# 看你的 WAN IP
ifstatus wan | grep -i '"address"' | head -1
# 看 DNS 解析
nslookup vpn.example.com 8.8.8.8验证:两者必须一致(差几分钟是 DNS 缓存,过会再查)。下文所有 vpn.example.com 替换成你的实际域名。
1.2 端口可达性
ⓐ 主路由:不需要任何端口转发,UDP 500/4500 由 OpenWrt 自己听。§5.2 会在 fw4 上加 input 放行。
ⓑ 旁路由:在前级路由器上加两条转发:
| 协议 | 外部端口 | 内部 IP(OpenWrt LAN) | 内部端口 |
|---|---|---|---|
| UDP | 500 | 10.0.0.X | 500 |
| UDP | 4500 | 10.0.0.X | 4500 |
不需要 80 端口(DNS-01)。
§2 装包(最轻量)
2.1 更新源(snapshot 必须立刻接装包)
apk updateapk update 完之后当场装包。snapshot 源每天重建,隔天就有可能 404。中途中断要重新 apk update。
2.2 装 strongSwan(模块化,不用 strongswan-full)
strongswan-full 约 5MB,把不需要的插件全拉上。本教程只装 5 个包,基础模块由 strongswan-default 元包自动拉依赖:
apk add \
strongswan-default \
strongswan-mod-eap-mschapv2 \
strongswan-mod-eap-identity \
strongswan-mod-eap-dynamic \
strongswan-mod-openssl| 包 | 作用 | 为什么不能省 |
|---|---|---|
strongswan-default | 主程序 + 基础密码学 + x509/pem/pkcs1/pubkey + kernel-netlink + socket-default | 元包,自动拉一串依赖,这些子模块不用单独列 |
mod-eap-mschapv2 | 用户名密码认证 | strongswan-default 不带 EAP |
mod-eap-identity | 解析 eap_identity=%identity | 同上 |
mod-eap-dynamic | 客户端/服务端 EAP 方法协商兜底 | 同上 |
mod-openssl | TLS / 哈希 / RSA(ZeroSSL 证书校验) | 防止 strongswan-default 默认走 mod-gmp 时缺 openssl |
不装的几个对比:
mod-curl:OCSP/CRL 在线吊销查询,本教程不启用,EAP-MSCHAPv2 也用不到 → 跳过mod-updown:依赖老 iptables(snapshot 已删),会装失败;本教程不设leftupdown=,路由由 §5 的 xfrm0 interface 接管 → 跳过- 如果你看到
iptables/ip6tables提示 "Not available",这是预期的,本教程不需要
§5 还需要 2 个网络相关的包(放在那里装):
kmod-xfrm-interface # XFRM interface 内核模块, VPN 流量通过 xfrm0 进入 lan zone
luci-proto-xfrm # UCI/LuCI 管 xfrm 接口的 proto handler
snapshot 里 strongswan 模块名偶尔会改。某个包报
unknown package / not found 时:apk search 'strongswan-mod-*' | grep -i <关键词>找到新名换上。
验证:
swanctl --version
# 期望: strongSwan 5.x.x / 6.x.x
ls /etc/init.d/swanctl && echo "init OK"
ls -d /etc/swanctl/ && echo "config dir OK"
ls /etc/swanctl/private /etc/swanctl/x509 /etc/swanctl/x509ca 2>/dev/null && echo "subdirs OK"
# 子目录若缺, §3-C.1 会创建ipsec 命令(stroke 接口)在 snapshot 已不打包,整套用 swanctl。教程后续涉及 ipsec.conf / ipsec listall / /etc/init.d/ipsec 的地方已全部改成 swanctl 等价命令。
2.3 装 ACME(两选一)
先看有没有 LuCI:
[ -d /usr/lib/lua/luci ] && echo "有 LuCI → 走 2.3-A" || echo "无 LuCI → 走 2.3-B"2.3-A 有 LuCI:用 luci-app-acme(GUI)
apk add luci-app-acme acme-acmesh acme-acmesh-dnsapi包关系:
acme-acmesh— acme.sh 脚本本体(/usr/lib/acme/acme.sh/)acme-acmesh-dnsapi— DNS-01 插件,含dns_dp.sh(DNSPod)luci-app-acme— LuCI 上的"ACME 证书"页面 + UCI 包装
验证:
ls /usr/lib/acme/acme.sh/acme.sh
ls /usr/lib/acme/acme.sh/dnsapi/dns_dp.sh
# 两个文件都在刷新 LuCI:服务 → ACME 证书 出现。
2.3-B 无 LuCI:命令行 acme.sh
apk add acme-acmesh acme-acmesh-dnsapi验证:
/usr/lib/acme/acme.sh/acme.sh --version
# 期望: v3.x.x
ls /usr/lib/acme/acme.sh/dnsapi/dns_dp.sh && echo "DNSPod 插件 OK"2.4 必备杂项
apk add ca-certificates ca-bundle curl
# ca-* 用于 acme.sh 校验 ZeroSSL 服务端证书验证:
curl -s -o /dev/null -w "%{http_code}\n" https://acme.zerossl.com
# 期望: 200 或 405 (说明 TLS 连接成功, 不是 0/000)§3 申请 ZeroSSL 证书
YR1/YR2/YE1/YE2 挂在 Root YR/YE 下,安卓 / 国产 ROM 信任库没有,客户端会报 no trusted public key found + AUTH_FAILED。用 ZeroSSL,挂在 USERTrust RSA CA(所有设备都信任)。
3-A 路径(LuCI 用户)
3-A.1 在 LuCI → 服务 → ACME 证书
General Settings:
- State directory:
/etc/acme - Account email:
your-email@example.com(必须英文,否则注册失败,见 §8.10)
Add → 新增一个 cert:
- Enabled: [勾选]
- Use staging server: [不勾选]
- Domain names:
vpn.example.com - Key size:
2048 - Validation method:
dns - DNS API:
dns_dp Credentials(每行一对 KEY=VALUE):
DP_Id=你的ID DP_Key=你的Token- ACME server:
https://acme.zerossl.com/v2/DV90(关键,不写就用默认 LE)
Save & Apply。
3-A.2 触发签发
/etc/init.d/acme start
logread -e acme -f # 等 "Cert success", 约 30-60 秒, Ctrl+C证书产出在 /etc/acme/vpn.example.com/。
3-B 路径(命令行用户)
3-B.1 切默认 CA 到 ZeroSSL
ACME=/usr/lib/acme/acme.sh/acme.sh
$ACME --set-default-ca --server zerossl
$ACME --register-account -m your-email@example.com # [!] 英文邮箱3-B.2 喂 DNSPod 凭据
export DP_Id="123456"
export DP_Key="abcdef1234567890..."acme.sh 第一次签完会自动把这俩写进 ~/.acme.sh/account.conf,以后续期自动读,不用每次 export。3-B.3 签证书
$ACME --issue --dns dns_dp -d vpn.example.com --keylength 2048acme.sh 自动加 TXT → 等 ZeroSSL 验 → 自动删 TXT。不需要 80 端口。
3-C 公共部分:摆到 strongSwan 路径 + 续期 hook
3-C.1 创建目标目录(swanctl 路径)
mkdir -p /etc/swanctl/private /etc/swanctl/x509 /etc/swanctl/x509ca| 目录 | 放什么 | swanctl.conf 怎么引用 |
|---|---|---|
/etc/swanctl/private/ | 服务器私钥 privkey.pem | secrets.private-*.file = privkey.pem |
/etc/swanctl/x509/ | 末端 + 中间证书 fullchain.pem | connections.*.local.certs = fullchain.pem |
/etc/swanctl/x509ca/ | 中间证书 chain.pem(独立 CA 副本,供链验证) | swanctl 自动扫描加载,无需显式引用 |
3-C.2 装证书 + 写续期 reloadcmd
LuCI 用户(3-A 路径):LuCI ACME 的产出目录是 /etc/acme/<domain>/,写一个自动检测域名的同步脚本,挂 cron 每天跑一次:
mkdir -p /etc/acme/hooks
cat > /etc/acme/hooks/sync-to-swanctl.sh <<'EOF'
#!/bin/sh
# 优先级: 命令行参数 > swanctl.conf 的 local.id > /etc/acme/ 自动识别
# 这样改 swanctl.conf 的域名时, 续期自动跟着走, 不用改 cron
detect_from_swanctl() {
[ -f /etc/swanctl/swanctl.conf ] || return
awk '
/^[[:space:]]*local[[:space:]]*\{/ { in_local=1; next }
in_local && /\}/ { in_local=0; next }
in_local && /^[[:space:]]*id[[:space:]]*=/ {
sub(/^[[:space:]]*id[[:space:]]*=[[:space:]]*/, "")
sub(/[[:space:]]*$/, "")
sub(/^@/, ""); gsub(/"/, "")
print; exit
}
' /etc/swanctl/swanctl.conf
}
detect_from_acme() {
cd /etc/acme/ 2>/dev/null || return
for d in */; do
d="${d%/}"
case "$d" in
ca|hooks|account|http.*|_*) continue ;;
esac
[ -f "$d/$d.key" ] && echo "$d"
done
}
if [ -n "$1" ]; then
D="$1"
else
D="$(detect_from_swanctl)"
if [ -n "$D" ] && [ ! -d "/etc/acme/$D" ]; then
echo "WARN: swanctl.conf local.id='$D' 但 /etc/acme/$D 不存在, 改用 acme 扫描" >&2
D=""
fi
if [ -z "$D" ]; then
cand="$(detect_from_acme)"
n=$(printf '%s\n' "$cand" | grep -c .)
if [ "$n" -eq 1 ]; then
D="$cand"
elif [ "$n" -gt 1 ]; then
echo "ERR: 多个 acme 域名,但 swanctl.conf 没指明 local.id:" >&2
printf ' %s\n' $cand >&2
echo "用法: $0 <domain>" >&2
exit 1
else
echo "ERR: 没找到任何有效域名" >&2
exit 1
fi
fi
fi
[ -d "/etc/acme/$D" ] || { echo "ERR: /etc/acme/$D 不存在" >&2; exit 1; }
SRC=/etc/acme/$D
DST_KEY=/etc/swanctl/private/privkey.pem
DST_FC=/etc/swanctl/x509/fullchain.pem
DST_CA=/etc/swanctl/x509ca/chain.pem
mkdir -p /etc/swanctl/private /etc/swanctl/x509 /etc/swanctl/x509ca
KEY=$(ls $SRC/${D}.key 2>/dev/null | head -1)
FC=$(ls $SRC/fullchain.cer 2>/dev/null || ls $SRC/fullchain.pem 2>/dev/null | head -1)
CA=$(ls $SRC/ca.cer 2>/dev/null || ls $SRC/ca.pem 2>/dev/null | head -1)
for pair in "KEY:$KEY" "FC:$FC" "CA:$CA"; do
name="${pair%%:*}"; val="${pair#*:}"
if [ -z "$val" ] || [ ! -f "$val" ]; then
echo "ERR: $name 在 $SRC/ 里找不到" >&2
ls -la $SRC/ >&2
exit 1
fi
done
# 用 cmp 比内容, 不靠 mtime, 第一次跑也能正确触发
if ! cmp -s "$KEY" "$DST_KEY" 2>/dev/null || ! cmp -s "$FC" "$DST_FC" 2>/dev/null; then
cp "$KEY" "$DST_KEY"
cp "$FC" "$DST_FC"
cp "$CA" "$DST_CA"
chmod 600 "$DST_KEY"
echo "synced $D -> /etc/swanctl/"
if pgrep -f charon >/dev/null 2>&1; then
swanctl --load-all
else
echo "charon 未运行, 跳过 swanctl --load-all (首次部署正常)"
fi
logger -t acme-sync "synced certs ($D) and reloaded swanctl"
else
echo "no change: certs already in sync"
fi
EOF
chmod +x /etc/acme/hooks/sync-to-swanctl.sh
# 立即手动跑一次, 把当前证书摆到位
/etc/acme/hooks/sync-to-swanctl.sh
# 期望: synced <你的域名> -> /etc/swanctl/ (然后 charon 未运行提示)
# 验证文件就位
ls -la /etc/swanctl/private/privkey.pem /etc/swanctl/x509/fullchain.pem
# 挂 cron, 不带参数 — 脚本会自己从 swanctl.conf 读 local.id
grep -q sync-to-swanctl /etc/crontabs/root 2>/dev/null \
|| echo '15 4 * * * /etc/acme/hooks/sync-to-swanctl.sh' >> /etc/crontabs/root
/etc/init.d/cron enable
/etc/init.d/cron restart改域名时:只需改
swanctl.conf的local.id,cron 自动跟随,不用动 crontab。这是"单一可信源(swanctl.conf)" 的好处。多 acme 域 + swanctl.conf 还没建好的过渡期:脚本会大声报错并列出候选,强制你显式传参,避免拷错证书。
命令行用户(3-B 路径):用 acme.sh 自己的 --install-cert --reloadcmd,只是目标路径和 reload 命令换 swanctl:
$ACME --install-cert -d vpn.example.com \
--key-file /etc/swanctl/private/privkey.pem \
--fullchain-file /etc/swanctl/x509/fullchain.pem \
--ca-file /etc/swanctl/x509ca/chain.pem \
--reloadcmd "swanctl --load-all"
chmod 600 /etc/swanctl/private/privkey.pem
# 装一次 cron, 让 acme.sh 自动续期
grep -q 'acme.sh --cron' /etc/crontabs/root 2>/dev/null \
|| echo '0 4 * * * /usr/lib/acme/acme.sh/acme.sh --cron --home /root/.acme.sh > /dev/null 2>&1' \
>> /etc/crontabs/root
/etc/init.d/cron enable
/etc/init.d/cron restart3-C.3 验证 issuer 是 ZeroSSL
openssl x509 -in /etc/swanctl/x509/fullchain.pem -noout -issuer -dates- [OK] 期望:
issuer=C=AT, O=ZeroSSL GmbH, CN=ZeroSSL RSA DV SSL CA 2 - [X] 出现
Let's Encrypt/YR1/YR2/YE1→ 3-A 的 ACME server URL 没填,或 3-B 没--set-default-ca,回去重做。
§4 strongSwan 配置(swanctl 格式,语义与 Debian §4 一一对应)
swanctl 用大括号嵌套的 VICI 语法,一个文件就把连接定义 + IP 池 + 用户密码全装下,不再分ipsec.conf/ipsec.secrets。
4.1 主配置 /etc/swanctl/swanctl.conf
[ -f /etc/swanctl/swanctl.conf ] && cp /etc/swanctl/swanctl.conf /etc/swanctl/swanctl.conf.bak
vi /etc/swanctl/swanctl.conf粘贴(替换 vpn.example.com 和密码)。注意保留 OpenWrt 默认包里已有的两行 include(在 /etc/swanctl/conf.d/ 或 /var/swanctl/ 有内容时拉取,默认空,留着无害且向前兼容),把下面这段追加在 include 之后:
# Include config snippets (OpenWrt 默认就有, 保留)
include conf.d/*.conf
include /var/swanctl/swanctl.conf
# ↓↓↓ 下面是 IKEv2 VPN 配置 ↓↓↓
connections {
ikev2-vpn {
version = 2
# 宽松套件, 不要在末尾加 !, 兼容老安卓 / 苹果 / Win
proposals = aes256gcm16-prfsha384-ecp384,aes256gcm16-prfsha256-modp2048,aes256-sha256-modp2048,aes256-sha384-modp2048,aes256-sha1-modp2048,aes128-sha256-modp2048,aes128-sha1-modp1024
rekey_time = 0
pools = ikev2-pool
fragmentation = yes
encap = yes # 等同 forceencaps=yes
dpd_delay = 300s
send_certreq = no
unique = never # 等同 uniqueids=no
local {
auth = pubkey
certs = fullchain.pem
id = vpn.example.com
}
remote {
auth = eap-mschapv2
eap_id = %any
}
children {
net {
# 分流核心: 只有这个网段走 VPN (等同 leftsubnet)
local_ts = 10.0.0.0/16
# ↓↓↓ XFRM interface 绑定 (§5 用) ↓↓↓
if_id_in = 42 # 数字任意, 必须和 §5.4 创建 xfrm0 时的 ifid 一致
if_id_out = 42
# ↑↑↑ 这两行让解密包出现在 xfrm0 接口, 自然进入 lan zone ↑↑↑
rekey_time = 0
dpd_action = clear
esp_proposals = aes256gcm16,aes256-sha384,aes256-sha256,aes256-sha1,aes128-sha256,aes128-sha1
}
}
}
}
pools {
ikev2-pool {
# VPN 客户端 IP 池 (等同 rightsourceip)
addrs = 10.10.10.0/24
# OpenWrt 自己当 DNS, 让 VPN 客户端能解析 .lan 域名
dns = 10.0.0.1
}
}
secrets {
# 服务器证书私钥, swanctl 自动从 /etc/swanctl/private/ 读
private-server {
file = privkey.pem
}
# VPN 用户(密码 ≥ 12 位, 大小写+数字+符号), 每人一段:
eap-zhangsan {
id = zhangsan
secret = "ZsKe92!QvLm8rWp3"
}
eap-lisi {
id = lisi
secret = "X9$mP4kRtY8nB2Lv"
}
}chmod 600 /etc/swanctl/swanctl.conf语法对照表(以后看 Debian 教程不晕):
Debian ipsec.conf swanctl.conf 路径 leftid=@vpn.example.comlocal.id = vpn.example.com(swanctl 不要@)leftcert=fullchain.pemlocal.certs = fullchain.pemleftsubnet=10.0.0.0/16children.net.local_ts = 10.0.0.0/16rightauth=eap-mschapv2remote.auth = eap-mschapv2rightsourceip=10.10.10.0/24pools.ikev2-pool.addrs = ...rightdns=10.0.0.1pools.ikev2-pool.dns = 10.0.0.1eap_identity=%identityremote.eap_id = %anyforceencaps=yesencap = yesuniqueids=nounique = neverrekey=norekey_time = 0(连接 + child 各一次)ike=...connections.*.proposalsesp=...children.*.esp_proposals: RSA "privkey.pem"secrets.private-*.file = privkey.pemuser : EAP "pwd"secrets.eap-*.{ id, secret }
生成强密码:
head -c 12 /dev/urandom | base64验证:
# 语法检查 (charon 未起也能跑)
swanctl --load-conns 2>&1 | head
# 期望: 看到 "loaded connection 'ikev2-vpn'" 或类似, 不报 syntax error--load-conns 在 charon 没起来时会报 "Connecting to ... failed",这是正常的,只是说明 vici socket 还没起。语法错的话会先打印 parsing 错误,优先看那一行。先不管,等 §6 启动了再统一检验。
§5 防火墙 + 路由(XFRM interface 方案)
[推荐] 本节是本教程踩坑后总结的最优解,采用 Linux XFRM interface(
xfrm0虚拟接口) 让 VPN 流量自然进入 lan zone,等同于 LAN 设备,不需要专门的 vpn zone / 不需要 masquerade / 不需要复杂的 forwarding 规则。早期方案(独立 vpn zone + subnet 绑定)在 immortalwrt snapshot 上踩到 fw4 规则优先级 bug — wan zone 的 iifname 匹配抢先于 vpn zone 的 subnet 匹配,VPN 流量会被错误路由到 input_wan 并拒绝(只 ping 通,TCP 全拒)。XFRM interface 方案彻底绕开这个问题。
5.1 确认 IP 转发已开
sysctl net.ipv4.ip_forward
# 期望: net.ipv4.ip_forward = 1 (OpenWrt 默认)不用动。
5.2 装 XFRM interface 内核模块
apk update
apk add kmod-xfrm-interface luci-proto-xfrm| 包 | 作用 |
|---|---|
kmod-xfrm-interface | 内核模块,提供 ip link add ... type xfrm 能力 |
luci-proto-xfrm | LuCI / netifd 的 xfrm proto handler,让 UCI 能管 xfrm 接口 |
验证:
modprobe xfrm_interface
lsmod | grep xfrm_interface
# 期望: 看到 xfrm_interface
ls /lib/netifd/proto/xfrm.sh
# 期望: 文件存在[提示]
kmod-xfrm-interface安装时自动建了/etc/modules.d/xfrm-interface(内容就是模块名),开机自动加载已经搞定,不用再手工创建别的文件。验证:cat /etc/modules.d/xfrm-interface # 期望: xfrm_interface
5.3 放行 UDP 500/4500/ESP 入口
无论主路由还是旁路由,只要 OpenWrt 自己跑 strongSwan,就要让对应端口能进 charon。
ⓐ 主路由模式:在 wan 区域加 input 规则。
uci batch <<EOF
add firewall rule
set firewall.@rule[-1].name='Allow-IKEv2-IKE'
set firewall.@rule[-1].src='wan'
set firewall.@rule[-1].proto='udp'
set firewall.@rule[-1].dest_port='500'
set firewall.@rule[-1].target='ACCEPT'
add firewall rule
set firewall.@rule[-1].name='Allow-IKEv2-NATT'
set firewall.@rule[-1].src='wan'
set firewall.@rule[-1].proto='udp'
set firewall.@rule[-1].dest_port='4500'
set firewall.@rule[-1].target='ACCEPT'
add firewall rule
set firewall.@rule[-1].name='Allow-IKEv2-ESP'
set firewall.@rule[-1].src='wan'
set firewall.@rule[-1].proto='esp'
set firewall.@rule[-1].target='ACCEPT'
commit firewall
EOFⓑ 旁路由模式:LAN zone 默认 input=ACCEPT,UDP 500/4500 经前级路由器 NAT 转发后已经从 LAN 进来,通常不用加规则。
5.4 创建 xfrm0 接口(UCI)
uci set network.xfrm0=interface
uci set network.xfrm0.proto='xfrm'
uci set network.xfrm0.tunlink='lan' # 底层接口
uci set network.xfrm0.ifid='42' # 必须和 swanctl.conf 的 if_id_in/out 一致
uci set network.xfrm0.defaultroute='0' # 不当默认网关
uci set network.xfrm0.zone='lan' # 加入 lan zone (核心!)
uci commit network
ifup xfrm0
sleep 2LuCI 法(等价):
- 网络 → 接口 → 添加新接口
- 名称:
xfrm0,协议:XFRM - 常规设置:Tunnel Link =
lan,IF ID =42 - 高级设置:取消勾选"使用该接口的网关作为默认网关"
- 防火墙设置:分配到现有
lanzone(不要新建!)
验证:
ip link show xfrm0
# 期望: xfrm0@br-lan: <MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 ...
nft list table inet fw4 | grep xfrm0 | head -4
# 期望: iifname { "xfrm0", "br-lan" } jump input_lan ...
# iifname { "xfrm0", "br-lan" } jump forward_lan ...
# xfrm0 和 br-lan 平起平坐, fw4 视为 lan 接口5.5 加静态路由 10.10.10.0/24 → xfrm0
这是 XFRM interface 模式必须手动加的一步 —— 内核需要知道"VPN 客户端 IP 段要从 xfrm0 出去"才能把响应包送回隧道。没这条路由,响应包会走默认路由出 wan,客户端永远收不到。
uci add network route
uci set network.@route[-1].interface='xfrm0'
uci set network.@route[-1].target='10.10.10.0/24'
uci commit network
service network reloadLuCI 法:
- 网络 → 接口 → 顶部 静态路由 标签
- 添加 IPv4 路由:接口
xfrm0,目标10.10.10.0/24,网关留空,跃点留空
验证:
ip route get 10.10.10.1
# 期望: 10.10.10.1 dev xfrm0 src 10.0.0.1 ...
# 不能是 "via xxx dev pppoe-wan" — 那样响应包就丢了5.6 MSS clamp(通常不需要,踩 MTU 坑时再加)
默认不用做这一步:xfrm0 接口 MTU 已经是 1280(LuCI 设置或 proto handler 默认),Linux 内核会:
- 路由表里
10.10.10.0/24 dev xfrm0自动继承 1280 MTU - TCP 建立时按 path MTU 自动算 MSS(= 1280 - 40 = 1240),双向都对
- 转发的超大 TCP 包靠 PMTU discovery 兜底(ICMP "Frag needed")
实测在现代 Linux + 正常运营商网络下 xfrm0 MTU=1280 已经够用,不需要显式 MSS clamp。
什么时候才需要(踩坑后再加)
如果你看到这些征兆 → 再加显式 MSS clamp:
- 浏览器开内网页面卡在加载某些大文件(JS / 图片)
- 内网大文件下载到 N% 不动
- SSH 连内网设备能连上,但敲长命令 / 粘贴大段卡死
ping -c 3 -s 1240 10.0.0.1不通(应该通才对)
修法(加显式 MSS clamp 文件):
mkdir -p /etc/nftables.d
cat > /etc/nftables.d/99-vpn-mss.nft <<'EOF'
chain mangle_vpn_mss {
type filter hook forward priority mangle; policy accept;
iifname "xfrm0" tcp flags syn tcp option maxseg size set 1280
oifname "xfrm0" tcp flags syn tcp option maxseg size set 1280
}
EOF
service firewall restart
# 验证
nft list chain inet fw4 mangle_vpn_mss
# 期望: 看到 2 条 maxseg 规则/etc/nftables.d/*.nft 是 fw4 的 include 目录,fw4 重启自动加载。
备注:本教程编写期间最初默认加了这条 MSS clamp,后来用户反馈"删了也能稳跑",所以改成"按需"。这是 Linux 6.x 内核 + 现代 ISP 的进步,老内核或 ICMP 被严格过滤的网络可能还是需要。
5.7 模块开机自动加载(包安装时自动处理)
apk add kmod-xfrm-interface 时包脚本自动建了 /etc/modules.d/xfrm-interface,开机时 kmodloader 自动加载,不需要手工做任何事。
验证:
cat /etc/modules.d/xfrm-interface
# 期望: xfrm_interface
# 看 kmodloader 确实在管理它
ls /etc/modules-boot.d/ 2>/dev/null | grep xfrm如果你装包失败 / 文件意外丢失,可以手动建一份:
echo 'xfrm_interface' > /etc/modules.d/xfrm-interface通常用不到。
5.8 完整验证(全过才进 §6)
echo '==== A. xfrm0 接口 UP + 在 lan zone ===='
ip link show xfrm0 | head -1
nft list table inet fw4 | grep xfrm0 | head -2
echo '==== B. 路由 10.10.10.0/24 走 xfrm0 ===='
ip route get 10.10.10.1
ip route | grep 10.10.10
echo '==== C. MSS clamp 规则在 ===='
nft list chain inet fw4 mangle_vpn_mss 2>&1 | grep maxseg
echo '==== D. wan 入口放行 ===='
nft list ruleset | grep -E 'udp dport (500|4500)' | head -2
echo '==== E. 模块加载持久化 ===='
cat /etc/modules.d/xfrm-interface 2>/dev/null
# 期望: xfrm_interface (包装时自动建)5 节都有期望输出后才进 §6。
失败立刻回滚:
TS=$(cat /root/ikev2-backup/LAST)
cp /root/ikev2-backup/$TS/config/firewall /etc/config/firewall
cp /root/ikev2-backup/$TS/config/network /etc/config/network
service firewall restart
service network restart§6 启动 + 验证
6.1 启动 swanctl
/etc/init.d/swanctl enable
/etc/init.d/swanctl start
# 等 3 秒看 charon 起来
sleep 3
pgrep -fa charon
# 期望: 看到 /usr/lib/ipsec/charon 或 charon-systemd 进程6.2 加载配置
swanctl --load-all
# 期望输出 (按行):
# loaded certificate from '/etc/swanctl/x509/fullchain.pem'
# loaded RSA key from '/etc/swanctl/private/privkey.pem'
# loaded eap secret 'eap-zhangsan'
# loaded eap secret 'eap-lisi'
# loaded pool 'ikev2-pool'
# loaded connection 'ikev2-vpn'
# successfully loaded 1 connections, 0 unloaded/etc/init.d/swanctl start 通常会自动跑一次 --load-all,但改完 swanctl.conf 后手动跑一次最稳。日后改密码 / 加用户也只需 swanctl --load-all,不用重启 charon。
6.3 关键自检
# 1. 证书加载
swanctl --list-certs --pretty | head -20
# 期望: subject "CN=vpn.example.com"
# issuer "C=AT, O=ZeroSSL GmbH, ..." ← 必须 ZeroSSL
# has private key# 2. 连接定义加载
swanctl --list-conns
# 期望: ikev2-vpn: IKEv2 ... local: ... remote: ... children: net# 3. IP 池加载
swanctl --list-pools
# 期望: ikev2-pool: 10.10.10.0/24, 254 online 0 / usage 0任一缺失 → §8 排查。
6.4 实时日志
logread -e charon -f
# 或 logread -f | grep charon打开手机连一次,日志里期望看到:
... received cert request for "..."
... authentication of 'vpn.example.com' with RSA_EMSA_PKCS1_SHA2_256 successful
... IKE_SA ikev2-vpn[N] established
... peer requested virtual IP %any
... assigning virtual IP 10.10.10.1 to peer看在线客户端:
swanctl --list-sas
# 期望: ikev2-vpn: #1, ESTABLISHED, IKEv2 ...
# net: #1, ..., reqid 1 ...6.5 验证 XFRM interface 流量真的走 xfrm0
手机连上 VPN 后,在路由器抓 xfrm0 接口的明文流量:
tcpdump -ni xfrm0 -c 20让 tcpdump 跑着,手机端 termux 跑 ping 10.0.0.1 或浏览器开内网页面。
期望看到:
IP 10.10.10.1.xxxxx > 10.0.0.1.xxx: ICMP echo request ...
IP 10.0.0.1.xxx > 10.10.10.1.xxxxx: ICMP echo reply ...双向都有 → XFRM 路径 100% 通了,VPN 部署完工。
只有单向(只有 client → router 没有 router → client):§5.5 静态路由没装好或 swanctl.conf 的 if_id_in/out 跟 xfrm0 的 ifid 不一致。回查 §8.5b。
6.6 验证证书自动续期
LuCI 用户:
grep -q sync-to-swanctl /etc/crontabs/root && echo "cron OK"
/etc/acme/hooks/sync-to-swanctl.sh # 手动跑一次, 不报错说明脚本正常命令行用户:
crontab -l | grep acme.sh
# 期望: 看到 acme.sh --cron 行
# 强制续期一次预演(不会真换证):
/usr/lib/acme/acme.sh/acme.sh --renew -d vpn.example.com --force
# 期望: Cert success + Run reload cmd: swanctl --load-all§7 客户端配置
完全照搬 Debian §7。服务器域名、用户名密码、分流网段都和 Debian 教程一致,iOS / Android / Windows 步骤一字不差。这里不重复。
要点速记:
- iOS:
IKEv2类型,服务器 = 远程 ID =vpn.example.com(不带 @),自动分流 - Android:装 strongSwan App,VPN Type
IKEv2 EAP,Split tunneling 仅10.0.0.0/16 - Windows:
IKEv2,装完后管理员 PowerShell 跑Set-VpnConnection -SplitTunneling $True+Add-VpnConnectionRoute -DestinationPrefix 10.0.0.0/16
§8 OpenWrt / snapshot 特有踩坑
Debian 教程 §8 的所有坑(LE 链不被信任 / ipsec.secrets 第一行格式 / 安卓"较不安全"标签 / 移动网络分片 / hairpin 等)在 OpenWrt 上同样适用,直接对照 Debian §8。下面只列 OpenWrt / snapshot 独有的。8.1 snapshot kmod 装不上
ERROR: <pkg>: unable to select packages:
kernel-<X.Y.Z>: providers not available(老 opkg 报错:incompatible with the architectures configured)
原因:snapshot 每天重建,kmod-* 包必须严格匹配 running kernel 的小版本。
修法:
apk update && apk add <pkg> # 一气呵成, 别隔夜如果 24h 之内没装上,先 sysupgrade -F 拉最新固件,再装包。
8.2 装到一半 Out of memory / No space left
ERROR: ... No space left on device修法(优先级排序):
df -h /overlay确认确实满了apk list -I | grep luci-app-删用不到的 LuCI 主题/插件- 把模块清单削更狠:本教程已是最小集,再减就跑不起来
- 最终方案:扩展 USB 存储做 overlay,或者换 32MB+ 闪存的硬件
8.3 service firewall restart 后自定义 nft 规则消失
OpenWrt 重启 fw4 会重新生成 nftables 整张表,你用 nft add rule 直接写的规则会丢。
只能走 UCI(本教程 §5 全程 UCI),或者写 include 文件到 /etc/nftables.d/*.nft。
8.4 sysupgrade 后 /etc/swanctl/ 证书丢失
OpenWrt 升级默认保留 /etc/config/*,但 /etc/swanctl/ 是否在保留名单要看 strongswan 包本身。保险做法:
cat >> /etc/sysupgrade.conf <<EOF
/etc/swanctl/
/etc/acme/
/root/.acme.sh/
EOFsysupgrade -l 2>/dev/null | grep -E 'swanctl|acme' 看下里面有没有,有就稳了。
8.5 时间不准 → 证书校验失败
certificate ... is not valid yetOpenWrt 部分硬件没 RTC,断电后时间归零,会出现"未来才生效"的奇怪错误。
service sysntpd status # 必须 running
service sysntpd restart
date # 重新看时间snapshot 里没 sysntpd?装 chrony:
apk add chrony && /etc/init.d/chronyd enable && /etc/init.d/chronyd start8.5 crypto-safexcel 硬件加速驱动崩溃(IPsec 触发)
症状:客户端一连 VPN 几秒内整个路由器重启,WAN 重新拨号,所有 LAN 设备掉线。uptime 归零。
pstore (/sys/fs/pstore/dmesg-ramoops-0)证据:
Unable to handle kernel read from unreadable memory at virtual address 0x20
CPU: x Comm: irq/76-15600000 ← crypto-safexcel 硬件加速 IRQ 线程
Tainted: ... 6.18.31
pc : 0xffffffc079XXXXXX ← 模块代码段
Call trace: irq_thread_fn ...
Kernel panic - not syncing: Oops: Fatal exception真凶:crypto_safexcel 驱动在 ESP 解密完成的 IRQ 处理路径上踩 NULL 指针。OpenWrt 25 snapshot 内核(6.18.x)的回归 bug,不能通过配置修。
误判警告:崩溃 dump 里 Modules linked in: ... nft_fullcone(O) ... 看似指向 fullcone,但关掉 fullcone 后仍然崩(我们实测过)。fullcone 只是恰好在已加载模块列表里。
修法:卸掉硬件加速驱动,strongSwan 自动回退软件 crypto:
# 1. 找包名
apk list -I | grep -iE 'safexcel|crypto-hw'
lsmod | grep safexcel
# 2. 停 strongSwan, 卸包, 重启
/etc/init.d/swanctl stop
apk del kmod-crypto-hw-safexcel # 实际包名按第 1 步结果
reboot
# 3. 重启回来验证
lsmod | grep safexcel # 期望: 空
dmesg | grep safexcel # 期望: 空性能影响:软件 AES-GCM 在带 ARMv8 AES 指令的 SoC 上 200-400 Mbps,家用 VPN 远程访问完全够用。
如果卸了还崩:那不是 safexcel 单独的问题,是 snapshot 内核 IPsec 子系统更深的 bug,走架构层方案:
- 把 strongSwan 搬到内网另一台 Linux 设备(NAS / Pi / 迷你 PC),OpenWrt 只做端口转发 — 见 Debian 服务器搭建指南,最干净
- 或者改用 WireGuard(不走 xfrm / safexcel 路径)
- 或者降级 OpenWrt 24.10 稳定版(代价:你硬件可能没适配,查 ToH)
8.5a 连上 VPN 路由器就重启 / WAN 重拨号
症状:客户端一连 VPN,几秒内整个路由器重启,WAN 重新拨号拿新 IP,所有 LAN 设备掉线。uptime 回到几分钟。
真凶:kmod-nft-fullcone 模块(OpenWrt 社区 fullcone NAT 补丁)在 IPsec 流量 + NAT 路径上踩内核空指针 panic。
证据来源:/sys/fs/pstore/dmesg-ramoops-0 里能看到:
Unable to handle kernel read from unreadable memory at virtual address 0x20
Modules linked in: ... nft_fullcone(O) ...
Tainted: [O]=OOT_MODULE
Comm: irq/76-15600000 ← crypto-safexcel(IPsec 硬件加速)中断
Kernel panic - not syncing: Oops: Fatal exception根因:kmod-nft-fullcone 是社区补丁,2023 年的代码塞进 2026 内核(6.18),代码没跟上 API,在 IPsec + xfrm 流量这种非常规路径下崩。OpenWrt 主线没采用这个模块就是因为代码质量。
修法(按损失递增):
# 方案 A (推荐先试): 保留 wan 的 fullcone, 只关 vpn zone 的
# 90% 概率有效, 内网设备仍享受 NAT1
VPN_IDX=$(uci show firewall | grep -E "name='vpn'" | sed -E "s/.*\[([0-9]+)\].*/\1/")
uci set firewall.@defaults[0].fullcone='1' # 保证全局开
uci set firewall.@zone[$VPN_IDX].fullcone='0' # 单独关 vpn zone
uci commit firewall
service firewall restart
nft list ruleset | grep -i fullcone
# 期望: 只剩 "Handle wan IPv4 fullcone..." 两条, 没有 vpn
# 方案 B: 全局关 fullcone
uci set firewall.@defaults[0].fullcone='0'
uci commit firewall
service firewall restart
nft list ruleset | grep -c fullcone # 应 = 0
# 方案 C: 直接卸了模块 (最彻底)
apk del kmod-nft-fullcone
service firewall restart为什么方案 A 多半够用:崩溃路径是 crypto IRQ → IPsec 解密 → NAT 链 → vpn zone fullcone hook → 空指针。VPN 客户端流量主要被 vpn zone 的 NAT 规则匹配,不走 wan 的。fullcone 模块的 bug 似乎触发在 "按 subnet 绑定的非接口 zone"(我们的 vpn zone 就是这种),wan zone 因为绑物理接口,代码路径不一样,通常不崩。
方案 A 失败时(新崩溃日志里仍有 nft_fullcone(O))再退到 B 或 C。
影响对比:
| 方案 | NAT1(LAN 设备) | NAT1(VPN 客户端) | 不崩 |
|---|---|---|---|
| A | [OK] | [X] | 通常 [OK] |
| B/C | [X] | [X] | [OK] |
家用 + VPN 服务器场景,方案 A 是最优解。即使退到 B/C,损失也只是 P2P 游戏(怪猎/魂)联机偶尔慢一点,刷视频 / 网页 / 上班完全无感。
kmod-nft-fullcone 安装且开启时出现。snapshot 默认不带,但很多第三方编译的 OpenWrt 固件(Tenda、小米、Redmi 等品牌的"科学上网增强版"固件)预装 + 默认开。firewall restart 时如果看到 IPv4 fullcone enabled for zone '...' 就是埋了雷。
8.5b XFRM interface 方案的 3 个易错点(本教程 §5 必读)
xfrm0 方案省事且架构干净,但 3 个细节漏一个就死:
a. swanctl.conf 必须有 if_id_in/out
children {
net {
local_ts = 10.0.0.0/16
if_id_in = 42 # 必填
if_id_out = 42 # 必填
...数字必须和 ip link add xfrm0 type xfrm ... if_id 42 的 ifid 完全一致。漏了或不一致 → SA 建立成功但解密包不进 xfrm0,VPN 客户端发啥都没反应。
b. 必须显式加静态路由 10.10.10.0/24 dev xfrm0
XFRM interface 模式不像传统 policy-based XFRM 那样自动安装 kernel 路由,响应包没路由就走默认网关出 wan,客户端永远收不到。征兆:
- ping 客户端 → router 通(因为客户端发包能进 xfrm0)
- router → ping 客户端不通,curl 连客户端任何 TCP 拒绝
ip route get 10.10.10.1显示via xxx dev pppoe-wan← 错的ip route get 10.10.10.1应该显示dev xfrm0 src 10.0.0.1← 对的
修法见 §5.5。
c. xfrm0 必须在 lan zone(不是新建 vpn zone)
xfrm0 加到 lan zone 等于"VPN 客户端 = LAN 设备",fw4 input_lan / forward_lan 自动放行。如果新建一个 vpn zone 单独管 xfrm0,你又会回到本教程一开始踩到的"vpn zone 入站不被识别"的坑(因为 IPsec 解密后 iifname 不一定准确,subnet 匹配优先级被 wan iifname 抢)。
LuCI 创建 xfrm0 接口时,"防火墙设置"那一步选 现有的 lan,不是创建新区域。
8.5c TCP 通但浏览器卡(MTU/MSS 问题)
征兆:
ping 10.0.0.1 -s 1000通,ping -s 1400不通curl http://10.0.0.1连接建立后卡住没数据(SYN 通,后续大包丢)- 浏览器开了页面但加载不出图片/CSS
原因:IPsec 加上 ~50-70 字节 overhead,中间某段路径 MTU 不够;Linux PMTU discovery 没生效(ICMP 被某网络设备屏蔽 / 客户端不支持 PMTUD)。
先检查:xfrm0 接口 MTU 是不是 1280
ip link show xfrm0 | grep mtu
# 期望: mtu 1280如果 MTU 对的,现代 Linux 99% 情况自动处理就够了。出现上面征兆才说明 PMTU 兜底失败,才需要加 §5.6 的显式 MSS clamp(/etc/nftables.d/99-vpn-mss.nft 写死 1280)。
为什么 xfrm0 MTU 通常够用:
- 路由器自己起 TCP:内核知道 path MTU 自动算 MSS
- 转发 TCP:大包到 xfrm0 → ICMP "Frag needed" 给发送方 → 降 MSS 重发
为什么有时不够:
- 中间网络设备过滤 ICMP type 3 code 4(Frag needed)
- 老旧客户端不支持 PMTU discovery
- 容器 / WSL 等隔离环境的网卡不响应 ICMP
碰到上面任何一条 → §5.6 的 MSS clamp 文件加上,问题消失。
想要榨干性能?用 ping -M do 测真实无分片 MTU
-M do 强制设置 DF (Don't Fragment) 位,禁止 IP 分片,内核会拒绝超过 path MTU 的包并报告真实 MTU。
# 手机 termux 连着 VPN, 从小到大试
ping -c 3 -s 1240 -M do 10.0.0.1
ping -c 3 -s 1280 -M do 10.0.0.1
ping -c 3 -s 1320 -M do 10.0.0.1
ping -c 3 -s 1360 -M do 10.0.0.1期望某个尺寸开始报:
ping: local error: Message too long, mtu=XXXXmtu=XXXX 就是这条路径的真实 inner MTU(手机 strongSwan App 给 tun 接口设的 MTU)。把 xfrm0 设比这个小 5-10 字节做余量。
8.6a 安卓原生 VPN no matching peer config found
服务端 swanctl --log 看到:
looking for peer configs matching <WAN_IP>[<WAN_IP>]...<client_IP>[<your_domain>]
no matching peer config found
sending AUTH_FAILED原因:某些安卓 ROM(尤其 MIUI/HyperOS)的原生 VPN 把 IKE 的 IDi/IDr 装反了 — 把"服务器域名"塞进自己的身份(IDi),把解析出的 IP 当作期望的服务器身份(IDr)发过来。
我们 local.id = jp.vpn.wuzuxi.com,客户端期望 <WAN_IP>,严格匹配失败。
修法:swanctl.conf 把 local.id 改成 %any:
local {
auth = pubkey
certs = fullchain.pem
id = %any # 接受客户端要求的任何 IDr
}swanctl --load-all。
安全性几乎不变 — 真正的服务端身份验证还在 cert 上(ZeroSSL 真证书),用户认证还在 EAP 密码上。local.id这一道只是配置整洁度,不是安全边界。strongSwan App / iOS / Windows 一直正常发 IDr=域名,%any都包含。
8.6b 安卓原生 VPN 握手到 IKE_AUTH 后无响应
服务端 swanctl --log 看到 peer config 匹配成功、发出 IKE_AUTH response,然后没有任何后续:
generating IKE_AUTH response 1 [ IDr AUTH EAP/REQ/ID ] ← 注意缺 CERT
sending packet: ...
(然后没了)原因:安卓原生 VPN 不发 CERTREQ(请求服务端证书),strongSwan 默认 send_cert = ifasked → 不主动发 CERT → 安卓收到 IDr AUTH 没法验证服务端签名 → 默默丢弃。
对比 strongSwan App / iOS 的请求里都有 CERTREQ,服务端响应就带 IDr CERT CERT AUTH EAP/REQ/ID,客户端能验。
修法:swanctl.conf 在 connections.ikev2-vpn 顶层加一行强制送证:
connections {
ikev2-vpn {
version = 2
...
send_cert = always # 强制每次都送 CERT, 不管对方有没要
...
}
}swanctl --load-all 重载。所有客户端都受益,没副作用(就是每次响应大几 KB)。
8.6 acme.sh 在 busybox ash 下偶发语法错
OpenWrt /bin/sh 是 busybox ash,acme.sh 主要为 bash 写的,某些边角语法可能炸(常见症状:syntax error: bad substitution)。
修法:装 bash 并改 shebang:
apk add bash
sed -i '1s|.*|#!/bin/bash|' /usr/lib/acme/acme.sh/acme.sh8.7 snapshot 包名漂移找不到
snapshot 长期演进,部分包会改名 / 拆分 / 合并(典型:strongswan-mod-* 模块拆分变动)。
通用查找:
apk search <关键词>
apk search 'strongswan-mod-*'strongswan-mod-xxx 找不到时,看元包依赖找当前真实子包名:
apk info strongswan-full | sed -n '/depends/,/provides/p'8.8 默认 LAN 是 192.168.1.0/24
OpenWrt 出厂默认 192.168.1.0/24。本教程要求 10.0.0.0/16(§0.1 已检查)。如果你保留默认网段,全文做替换:
10.0.0.0/16→192.168.1.0/2410.0.0.1→192.168.1.110.10.10.0/24→ 改成不冲突的段(例如192.168.99.0/24),否则 VPN 客户端 IP 池和 LAN 冲撞
强烈建议改 LAN 到 10.0.0.0/16 一次性对齐,后面所有配置直接抄。
8.9 LuCI ACME 页面提交后没动静
LuCI ACME 提交后只是写 /etc/config/acme,真正触发签发要手动:
/etc/init.d/acme start
logread -e acme -f不要傻等 UI 自动刷新。
8.10 acme 账号注册 invalidContact
"type": "urn:ietf:params:acme:error:invalidContact"
"detail": "contact email contains non-ASCII characters"原因:邮箱含中文 / 全角。
修法:用纯英文邮箱,重新跑:
/usr/lib/acme/acme.sh/acme.sh --register-account -m real-english@example.com --server zerosslLuCI 用户去页面把 Account email 改成英文,Save & Apply,再 /etc/init.d/acme start。
§9 教程没办法替你解决的"环境坑"
完全同 Debian §9:
- [X] CGNAT / 双层 NAT(WAN IP 是
100.64.0.0/10) - [X] ISP 封 UDP 500/4500
- [X] GFW 干扰
- [X] 公司/学校防火墙拦截 IPSec
跟你 OpenWrt 配置无关,改不了。
§10 日常运维 + 完全卸载
10.1 日常运维
| 操作 | 命令 |
|---|---|
| **修改 VPN 池(改 pool 后 swanctl --load-all 报 "online leases") | 先踢:swanctl --terminate --ike ikev2-vpn,再 --load-all |
| 加用户 | 在 /etc/swanctl/swanctl.conf 的 secrets {} 段加一个 eap-<name> { id=...; secret="..."; } → swanctl --load-all |
| 删用户 | 删对应 eap-* 段 → swanctl --load-all |
| 看在线 | swanctl --list-sas |
| 实时日志 | logread -e charon -f |
| 看证书 | openssl x509 -in /etc/swanctl/x509/fullchain.pem -noout -dates -issuer |
| 手动续期 | (3-A) /etc/init.d/acme start / (3-B) /usr/lib/acme/acme.sh/acme.sh --renew -d vpn.example.com --force |
| 改完配置重载 | swanctl --load-all(不重启 charon,在线会话不断) |
| 全停 | /etc/init.d/swanctl stop |
| 重启 | /etc/init.d/swanctl restart(会断所有在线 VPN) |
10.2 完全卸载 / 回滚到装之前
按反向顺序,全部基于 §0.3 的备份:
# 1. 停服务
/etc/init.d/swanctl stop ; /etc/init.d/swanctl disable
/etc/init.d/acme stop ; /etc/init.d/acme disable 2>/dev/null
# 2. 删 swanctl / acme 配置与证书
rm -rf /etc/swanctl/swanctl.conf /etc/swanctl/swanctl.conf.bak \
/etc/swanctl/private/privkey.pem \
/etc/swanctl/x509/fullchain.pem \
/etc/swanctl/x509ca/chain.pem \
/etc/acme /etc/acme.sh /root/.acme.sh
# 留 /etc/swanctl/ 目录骨架(后续若再装 strongswan 不用重建)
# 3. 删 cron 任务
sed -i '/sync-to-swanctl/d;/acme.sh.*--cron/d' /etc/crontabs/root
/etc/init.d/cron restart
# 4. 删 sysupgrade.conf 里我们加的行
sed -i '/\/etc\/swanctl/d;/\/etc\/acme/d;/\.acme\.sh/d' /etc/sysupgrade.conf
# 5. 删 xfrm0 接口 + 静态路由 (UCI)
uci -q delete network.xfrm0
# 删 10.10.10.0/24 静态路由 (找到对应 index)
for r in $(uci show network 2>/dev/null | grep -E '@route\[' | sed -E "s/.*\[([0-9]+)\].*/\1/" | sort -ru); do
tgt=$(uci -q get network.@route[$r].target)
[ "$tgt" = "10.10.10.0/24" ] && uci delete network.@route[$r]
done
uci commit network
service network reload
# 6. 删模块自动加载 (apk del kmod-xfrm-interface 会自动清掉,无需手动)
# /etc/modules.d/xfrm-interface 跟着包一起卸,无需 rm
# 7. 删 MSS clamp 文件
rm -f /etc/nftables.d/99-vpn-mss.nft
# 8. 还原 firewall (整文件回退到备份, 顺带清掉 wan 入口 IKEv2 三条规则)
TS=$(cat /root/ikev2-backup/LAST)
cp /root/ikev2-backup/$TS/config/firewall /etc/config/firewall
service firewall restart
# 9. 卸载包 (只列我们显式装的, strongswan-default 拉进来的依赖会随它一起被清掉)
apk del \
luci-app-acme acme-acmesh acme-acmesh-dnsapi \
strongswan-default \
strongswan-mod-eap-mschapv2 strongswan-mod-eap-identity strongswan-mod-eap-dynamic \
strongswan-mod-openssl \
kmod-xfrm-interface luci-proto-xfrm \
2>/dev/null
# 残留依赖看一下, 不想要的话再单独 apk del
apk list -I | grep -E 'strongswan|xfrm'
# 10. 验证清理干净
apk list -I | grep -iE 'strongswan|acme|xfrm-interface' # 期望: 空
nft list ruleset | grep -E '500|4500|10\.10\.10|xfrm0' # 期望: 空
ip link show xfrm0 2>/dev/null # 期望: 不存在
ls /etc/swanctl/swanctl.conf 2>/dev/null # 期望: 不存在
ls /etc/swanctl/private/privkey.pem 2>/dev/null # 期望: 不存在完全卸载验证:重启路由器,确认上网、LuCI、原有所有服务正常。
10.3 如果只是临时停用 VPN,不卸载
/etc/init.d/swanctl stop
/etc/init.d/swanctl disablefw4 规则留着不影响其他功能,真要清理再走 §10.2。
§11 安全说明
完全同 Debian §11:
- 用 EAP-MSCHAPv2 而不是 PSK(PSK 没有服务器身份验证)
- 密码 ≥ 12 位,
head -c 12 /dev/urandom | base64生成 - MSCHAPv2 包在 AES-256-GCM 隧道里,理论弱点摸不到
§12 关键文件位置速查
| 文件 | 作用 | 谁维护 | 在 §10 卸载里怎么处理 |
|---|---|---|---|
/etc/swanctl/swanctl.conf | 连接 + IP 池 + 用户密码 + 私钥引用 + if_id_in/out | 你 | rm |
/etc/swanctl/private/privkey.pem | 服务器私钥 | acme(同步脚本) | rm |
/etc/swanctl/x509/fullchain.pem | 末端 + 中间证书 | acme(同步脚本) | rm |
/etc/swanctl/x509ca/chain.pem | 中间证书(供链验证) | acme(同步脚本) | rm |
/etc/acme/<domain>/ (LuCI) | acme 工作目录 | acme | rm -rf |
/root/.acme.sh/ (CLI) | acme.sh 工作目录 | acme.sh | rm -rf |
/etc/config/network | xfrm0 interface + 10.10.10.0/24 静态路由 | uci(§5.4 / §5.5 加) | uci delete + restore |
/etc/config/firewall | fw4 规则(wan 入口 + xfrm0 加入 lan zone) | uci(§5.3) | 还原备份 |
/etc/nftables.d/99-vpn-mss.nft | MSS clamp 1280 防大包丢(§5.6) | 你 | rm |
/etc/modules.d/xfrm-interface | 开机自动加载 xfrm_interface 模块 | kmod-xfrm-interface 包(装包自动建) | 卸包自动清,无需手动 |
/etc/config/acme (LuCI) | LuCI ACME 配置 | uci | 删 acme 包时会清 |
/etc/crontabs/root | 续期 cron | 你 | sed -i 删两行 |
/etc/sysupgrade.conf | 升级保留名单 | 你 | sed -i 删几行 |
/root/ikev2-backup/ | §0.3 备份(config/network + config/firewall 都在) | 你 | 全卸载验证后再删 |
§13 完工检查清单
按顺序,任一不过回到对应章节:
- [ ]
cat /etc/openwrt_release显示 SNAPSHOT / 25.x (§0.1) - [ ]
ls /sbin/fw4存在 (§0.1) - [ ]
uci show network.lan.ipaddr是10.0.0.1(§0.1) - [ ]
/root/ikev2-backup/$(cat /root/ikev2-backup/LAST)/config备份完整 (§0.3) - [ ]
nslookup vpn.example.com 8.8.8.8返回你的 WAN IP (§1.1) - [ ] (旁路由) 前级路由器已转发 UDP 500/4500 (§1.2)
- [ ]
swanctl --version输出 strongSwan 5.x/6.x (§2.2) - [ ]
ls /usr/lib/acme/acme.sh/dnsapi/dns_dp.sh存在 (§2.3) - [ ]
openssl x509 -in /etc/swanctl/x509/fullchain.pem -noout -issuer显示 ZeroSSL (§3-C.3) - [ ]
/etc/swanctl/swanctl.conf里有secrets.private-server.file = privkey.pem段 (§4.1) - [ ]
/etc/swanctl/swanctl.conf里children.net有if_id_in = 42和if_id_out = 42(§4.1) - [ ]
sysctl net.ipv4.ip_forward显示 1 (§5.1) - [ ]
lsmod | grep xfrm_interface看到模块加载 (§5.2) - [ ]
nft list ruleset | grep -E 'udp dport (500|4500)'有规则 (§5.3) - [ ]
ip link show xfrm0显示 UP + LOWER_UP + MTU 1280 (§5.4) - [ ]
nft list table inet fw4 | grep xfrm0看到iifname { "xfrm0", "br-lan" } jump input_lan / forward_lan(§5.4) - [ ]
ip route get 10.10.10.1输出dev xfrm0(不是via pppoe-wan)(§5.5) - [ ]
nft list chain inet fw4 mangle_vpn_mss看到 MSS 1280 规则 (§5.6) - [ ]
cat /etc/modules.d/xfrm-interface显示xfrm_interface(包装时自动建)(§5.2) - [ ]
/etc/init.d/swanctl status显示 running (§6.1) - [ ]
swanctl --load-all输出loaded RSA key+loaded connection 'ikev2-vpn'(§6.2) - [ ]
swanctl --list-certs --pretty看到 RSA 私钥 + ZeroSSL 证书 (§6.3) - [ ] 手机连上后
swanctl --list-sas显示ikev2-vpn: ... ESTABLISHED(§6.4) - [ ] 手机连上后
tcpdump -ni xfrm0能看到明文流量(证明走对了 xfrm0) - [ ] 手机连上后能
curl http://10.0.0.1返回 HTTP 200/301/302(TCP 不再 Connection refused) - [ ] 手机连上后
ping youtube.com解析得到 IP 且能通(DNS 验证) - [ ] 手机连上后
ping -s 1240 10.0.0.1大包能通(MSS clamp 验证) - [ ] 手机连上后访问
ip.sb显示手机原本运营商 IP(分流验证,不应显示路由器 IP) - [ ] (可选)
crontab -l | grep -E 'acme|sync-to-swanctl'看到续期 cron (§6.4)
全部 [OK] 即完成。
附录:与 Debian 教程的章节映射
| OpenWrt §X | Debian §X | 是否一致 |
|---|---|---|
| §0.1 环境自检 | §0.1 | 命令换 OpenWrt 版 |
| §0.2 部署模式 | (无,Debian 隐含旁路由) | 新增 |
| §0.3 全量备份 | (无) | 新增(可逆要求) |
| §0.4 DNSPod token | §0.2 | 完全一致 |
| §1 域名 + 端口 | §1 | ⓐ/ⓑ 分支 |
| §2 装包 | §2 | 包名/命令换 OpenWrt |
| §3 ZeroSSL 证书 | §3 | A/B 两路径 |
| §4 strongSwan 配置 | §4 | 完全一致 |
| §5 防火墙 + 路由 | §5(iptables 直配) | 完全重新设计:XFRM interface(xfrm0)+ 加入 lan zone + 静态路由,本质上"VPN 客户端 = LAN 设备" |
| §6 启动验证 | §6 | 服务名 / 日志命令换 OpenWrt |
| §7 客户端 | §7 | 完全一致,直接引用 |
| §8 踩坑 | §8 | OpenWrt 特有的 10 条 |
| §9 环境坑 | §9 | 完全一致 |
| §10 运维 + 卸载 | §10 | 加"卸载/回滚"全流程 |
| §11 安全 | §11 | 完全一致 |
| §12 文件速查 | §12 | OpenWrt 路径 |
| §13 检查清单 | §13 | OpenWrt 版命令 |
