mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
🐤 Sync 2026-11-15 01:20:25
This commit is contained in:
1 parent
518b4c9f03
commit
de207ac4ea
22 files changed
+1068
-195
No files matched your search
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=filebrowser-q
|
||||
PKG_VERSION:=2.1.5-beta
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -3,4 +3,4 @@ config filebrowser 'config'
|
||||
option listen_port '8787'
|
||||
option root_path '/'
|
||||
option cacheDir '/tmp'
|
||||
option cacheDirMaxSizeMB '500'
|
||||
option cacheDirMaxSizeMB '300'
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gately
|
||||
PKG_VERSION:=0.6.6.2
|
||||
PKG_RELEASE:=35
|
||||
PKG_VERSION:=0.6.6.4
|
||||
PKG_RELEASE:=36
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-gately
|
||||
PKG_VERSION:=0.6.6.2
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=0.6.6.4
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_MAINTAINER:=wangwanjie <https://github.com/wangwanjie/gately>
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
*.apk
|
||||
*.ipk
|
||||
.DS_Store
|
||||
docs/
|
||||
@@ -6,12 +6,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for IPSec VPN Server (IKEv1 with PSK and Xauth)
|
||||
LUCI_DEPENDS:=+strongswan +strongswan-minimal +strongswan-mod-xauth-generic +strongswan-mod-kernel-libipsec +kmod-tun
|
||||
LUCI_DEPENDS:=+strongswan +strongswan-minimal +strongswan-mod-xauth-generic +strongswan-mod-kernel-libipsec +strongswan-mod-des +kmod-tun
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-ipsec-vpnd
|
||||
PKG_VERSION:=27.906.25.12
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
|
||||
define Package/luci-app-ipsec-vpnd/conffiles
|
||||
/etc/config/ipsec-vpnd
|
||||
|
||||
+141
-35
@@ -10,11 +10,12 @@ IKEv1 + PSK + Xauth(账号密码),目标用户是家用 / 小公司路由
|
||||
|
||||
## 功能
|
||||
|
||||
- **VPN 服务启停**(uhttpd → LuCI → procd 守护进程控制 strongSwan 的 `starter`)
|
||||
- **账号 / 密码 / PSK 配置** 写在 `/etc/config/ipsec-vpnd`,运行时被 init 脚本翻译成 strongSwan 的 `ipsec.conf` + `ipsec.secrets`
|
||||
- **VPN 服务启停**(uhttpd → LuCI → 控制 strongSwan 原生的 `ipsec` 服务,即 `procd` 托管的 `starter` + `charon`);「启用」是总开关,关掉时服务会停、conn 会从 `ipsec.conf` 里移除、凭据文件也会清掉
|
||||
- **账号 / 密码 / PSK 配置** 写在 `/etc/config/ipsec-vpnd`,由 `luci-ipsec-vpnd-render` 翻译成 strongSwan 的 `ipsec.conf` + `ipsec.secrets`
|
||||
- **首次安装自动初始化**:建 `network.VPN` 设备(绑 `ipsec0`)+ `firewall.ike/ipsec/ah/esp` 入站规则 + `firewall.VPN` zone + 端口转发;只跑一次,`/etc/config/ipsec-vpnd` 里的 `initialized` 标记防止升级时覆盖用户后续自定义
|
||||
- **和系统自带的 `ipsec` 服务冲突处理**:每次启动前都 `stop && disable` 系统那个,防止 charon 抢 UDP/500、4500
|
||||
- **不再自建服务实例**:本包不提供自己的 `ipsec-vpnd` 服务,charon 统一交给 strongSwan 自带的 `ipsec` 服务托管(`strongswan-ipsec` 包提供),从根本上避免两个服务抢 UDP/500、4500——抢端口时后启动的那个会**静默退出**,LuCI 上只显示「未运行」却查不到原因
|
||||
- **菜单位置**:`VPN → IPSec VPN Server`(order 30)
|
||||
- **依赖自检与诊断**:服务启动前检查 strongSwan 运行时是否完整,缺失时拒绝启动并写明确日志;LuCI 页面顶部直接列出缺哪一项、给出对应安装命令
|
||||
|
||||
## 界面与配置项
|
||||
|
||||
@@ -22,56 +23,114 @@ UCI 路径 `ipsec-vpnd.@ipsec[0]`,字段:
|
||||
|
||||
| 字段 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `enabled` | `0` | 总开关 |
|
||||
| `enabled` | `0` | 总开关。关掉 = 服务停 + 不加载 conn + 清除 `ipsec.secrets` |
|
||||
| `clientip` | `10.9.8.10/24` | 客户端地址池(CIDR),用 strongSwan 的 `rightsourceip` 推给拨号端 |
|
||||
| `clientdns` | `10.9.8.1` | 推给客户端的 DNS(即 VPN 网关 IP) |
|
||||
| `account` | `vpnuser` | Xauth 用户名 |
|
||||
| `password` | `changeme` | Xauth 密码 |
|
||||
| `secret` | `changeme` | IKE PSK 预共享密钥 |
|
||||
|
||||
VPN 客户端拨号后会拿到 `clientip` 池里一个地址,`clientdns` 指向 VPN 网关(路由器上的 `ipsec0` IP,初始化脚本里按 LAN 前两段、第三段 +1 自动算出来)。
|
||||
VPN 客户端拨号后会拿到 `clientip` 池里一个地址,`clientdns` 指向 VPN 网关(路由器上的 `ipsec0` IP,固定为 `10.9.8.1`)。
|
||||
|
||||
> `clientip` 直接传给 strongSwan 的 `rightsourceip`,格式是 `<起始地址>/<掩码>`,表示一个**地址池**。
|
||||
> 默认的 `10.9.8.10/24` 即 `10.9.8.10`–`10.9.8.254`(245 个地址,起点避开网关 `10.9.8.1`),
|
||||
> 多个客户端可以同时在线,各自拿到池里的一个地址(实测两个客户端分别拿到 `10.9.8.10` / `10.9.8.11`)。
|
||||
> 注意 `account` 目前只支持**一个** XAuth 账号,不同用户名无法登录。
|
||||
|
||||
## 客户端配置示例(已实测通过)
|
||||
|
||||
Linux 客户端(strongSwan 5.9/6.x):
|
||||
|
||||
```conf
|
||||
# /etc/ipsec.conf
|
||||
config setup
|
||||
uniqueids=never
|
||||
|
||||
conn client
|
||||
keyexchange=ikev1
|
||||
ike=aes128-sha1-modp2048,aes128-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1536
|
||||
esp=aes128-sha1,3des-sha1
|
||||
right=192.168.1.139 # 服务端地址
|
||||
rightauth=psk
|
||||
left=%defaultroute
|
||||
leftid=testuser # IKE ID,同时作为 XAuth 用户名
|
||||
leftauth=psk
|
||||
leftauth2=xauth # IKEv1 第二轮 XAuth 认证
|
||||
leftsourceip=%config # 向服务端申请虚拟地址
|
||||
auto=start
|
||||
```
|
||||
|
||||
```conf
|
||||
# /etc/ipsec.secrets(客户端侧也要写 XAuth 密码)
|
||||
: PSK "testpsk"
|
||||
testuser : XAUTH "testpass"
|
||||
```
|
||||
|
||||
**关键点**:客户端必须用 `leftsourceip=%config`(或与服务端 `clientip` 完全一致的 TS)。
|
||||
写死 `leftsubnet=10.9.8.0/24` 或 `leftsourceip=<自己的地址>` 会与服务端下发的
|
||||
`10.9.8.10/32` 冲突,服务端日志出现
|
||||
`no matching CHILD_SA config found for 10.9.8.0/24 === <服务端IP>/32`,
|
||||
最终报 `INVALID_ID_INFORMATION`,IKE 起来了但 CHILD SA 装不上。
|
||||
|
||||
## 依赖
|
||||
|
||||
`Makefile` 里 `LUCI_DEPENDS` 列的运行时依赖:
|
||||
本包是纯 LuCI 前端 + 脚本,运行时依赖 strongSwan。**安装本包时会自动拉取下面这些依赖**(apk / ipk 的 `depends` 已声明):
|
||||
|
||||
```
|
||||
+strongswan-minimal
|
||||
+strongswan-mod-xauth-generic
|
||||
+strongswan-mod-kernel-libipsec
|
||||
+strongswan-mod-des
|
||||
+kmod-tun
|
||||
| 包 | 作用 |
|
||||
|---|---|
|
||||
| `strongswan-minimal` | strongSwan 最小集,含 charon、starter(`/usr/lib/ipsec/starter`)、stroke 插件 |
|
||||
| `strongswan-mod-xauth-generic` | XAuth 账号认证插件,缺它 XAuth 账号无法登录 |
|
||||
| `strongswan-mod-kernel-libipsec` | 内核态 IPsec |
|
||||
| `strongswan-mod-des` | 3DES 加密插件,缺它本包声明的 3DES 提案不可用(老版本 iOS/Android 客户端需要) |
|
||||
| `kmod-tun` | TUN/TAP 设备 |
|
||||
|
||||
> `strongswan-mod-stroke` 会连带装上 `strongswan-ipsec`(提供 `/usr/sbin/ipsec` 命令与系统自带的 `ipsec` 服务)。
|
||||
> 本包正是复用这个服务来托管 charon:LuCI 服务列表里跑起来的是 `ipsec`,本包不再有独立的 `ipsec-vpnd` 服务实例。
|
||||
|
||||
### 如果提示找不到依赖
|
||||
|
||||
先更新索引再装:
|
||||
|
||||
```sh
|
||||
apk update
|
||||
apk add luci-app-ipsec-vpnd-*.apk
|
||||
```
|
||||
|
||||
**这是 IPsec VPN 服务能起来的前提。**安装本包前请确认这些包在固件里都有,否则 LuCI 界面能开,但服务起不来。
|
||||
索引缓存损坏时 `apk search` 会返回空,看起来像"源里没有 strongswan",`apk update` 即可恢复。
|
||||
|
||||
### 关于 ImmortalWrt 25.12 系列(重要)
|
||||
### 关于 ImmortalWrt 25.12 系列
|
||||
|
||||
ImmortalWrt **25.12.0 / 25.12.1 的官方软件源在全架构上都没带 strongswan**(OpenWrt 上游 25.12 源有,ImmortalWrt 24.10 源也有,唯独 ImmortalWrt 25.12 系列缺)。如果你的固件就是 ImmortalWrt 25.12 系列且只挂着官方源,`opkg install luci-app-ipsec-vpnd` 会卡在依赖解析。
|
||||
实测 **ImmortalWrt 25.12.2 官方源已经带 strongswan**(`apk search strongswan*` 有 100+ 个包),直接装本包即可,不需要混用 OpenWrt 源。
|
||||
|
||||
可行方案(任选):
|
||||
25.12.0 / 25.12.1 是否同样带 strongswan **未验证**(这些源的镜像不提供历史索引,无法回溯确认)。如果你手上的版本确实报"找不到依赖",可以自行确认:
|
||||
|
||||
- **换 OpenWrt 上游源**:`/etc/opkg/distfeeds.conf` 里加 `src/gz openwrt_core https://downloads.openwrt.org/releases/25.12.1/targets/.../packages/`
|
||||
- **换同版本但带 strongswan 的 ImmortalWrt fork 源**
|
||||
- **用 szwjp/luci 这种社区源**,里头把 strongswan 也带上了
|
||||
```sh
|
||||
apk update && apk search strongswan\*
|
||||
```
|
||||
|
||||
若确实没有,可用**同版本**的 OpenWrt 源(注意内核与 libc 版本必须一致,否则会出现不可预期的依赖错配),或在固件里预装 strongswan 后再装本包。
|
||||
|
||||
## 安装
|
||||
|
||||
### 普通用户(装现成的 .apk)
|
||||
|
||||
从本仓库的 `Actions → Build → 最新一次成功 run → Artifacts → ipsec-vpnd-packages` 下载 `luci-app-ipsec-vpnd_*.apk` 和 `luci-i18n-ipsec-vpnd-zh-cn_*.apk`(中文界面,可选),用 scp / WinSCP 推到路由器:
|
||||
从本仓库的 `Actions → Build → 最新一次成功 run → Artifacts → ipsec-vpnd-packages` 下载 `luci-app-ipsec-vpnd-*.apk` 和 `luci-i18n-ipsec-vpnd-zh-cn-*.apk`(中文界面,可选),用 scp / WinSCP 推到路由器:
|
||||
|
||||
```sh
|
||||
opkg install luci-app-ipsec-vpnd_*.apk
|
||||
opkg install luci-i18n-ipsec-vpnd-zh-cn_*.apk # 可选
|
||||
apk update
|
||||
apk add --allow-untrusted luci-app-ipsec-vpnd-*.apk
|
||||
apk add --allow-untrusted luci-i18n-ipsec-vpnd-zh-cn-*.apk # 可选
|
||||
```
|
||||
|
||||
第一次安装会跑 `uci-defaults/luci-ipsec-vpnd` 一次性初始化(建 network/firewall/zone + 删系统 ipsec 服务),然后 LuCI 刷新一下就能在 `VPN → IPSec VPN Server` 看到。
|
||||
> 25.12 用 `apk`,更老的版本用 `opkg install`。
|
||||
> `--allow-untrusted` 是因为 CI 产出的包没有签名;正式分发建议自行签名。
|
||||
|
||||
或者从 release 装(GitHub Actions 自动把每次 main 提交 publish 到 `IPSec VPND Latest Build` release):
|
||||
第一次安装会跑 `uci-defaults/luci-ipsec-vpnd` 一次性初始化(建 network/firewall/zone + 启用系统 `ipsec` 服务 + 渲染初始 strongSwan 配置 + 拉起 VPN 网关接口),然后 LuCI 刷新一下就能在 `VPN → IPSec VPN Server` 看到。
|
||||
|
||||
或者从 release 装(GitHub Actions 自动把每次 main 提交 publish 到 `IPSec VPND Latest Build` release,注意该 release 每次推送会重建,文件名里的版本号会变):
|
||||
|
||||
```sh
|
||||
opkg install https://github.com/szwjp/luci-app-ipsec-vpnd/releases/download/luci-app-ipsec-vpnd/luci-app-ipsec-vpnd_27.906.25.12-r1_all.apk
|
||||
apk add --allow-untrusted https://github.com/szwjp/luci-app-ipsec-vpnd/releases/download/luci-app-ipsec-vpnd/luci-app-ipsec-vpnd-27.906.25.12-r12.apk
|
||||
```
|
||||
|
||||
### 自己构建(社区分发 / 自己做 firmware)
|
||||
@@ -83,13 +142,44 @@ opkg install https://github.com/szwjp/luci-app-ipsec-vpnd/releases/download/luci
|
||||
sudo apt-get install -y build-essential meson ninja-build fakeroot
|
||||
|
||||
# 用仓库自带的 .github/build-pkg.sh
|
||||
fakeroot bash .github/build-pkg.sh apk 1 # 出 luci-app-ipsec-vpnd_*.apk + luci-i18n-ipsec-vpnd-zh-cn-*.apk
|
||||
fakeroot bash .github/build-pkg.sh apk 1 # 出 luci-app-ipsec-vpnd-*.apk + luci-i18n-ipsec-vpnd-zh-cn-*.apk
|
||||
fakeroot bash .github/build-pkg.sh ipk 1 # 出同名 .ipk
|
||||
ls -l *.apk *.ipk
|
||||
```
|
||||
|
||||
> `fakeroot` 不能省:apk v3 的 `mkpkg` 直接按文件属主写包,没有 fakeroot 就会把构建者的 uid 打进包里,装出来的文件属主不是 root。
|
||||
> macOS 上脚本本身能跑(已把 `mktemp -p` / `find -printf` / `sha256sum` 换成可移植写法),但产物属主仍是本地用户,仅适合调试,正式发布请走 CI。
|
||||
>
|
||||
> **`po2lmo` 必须是真的那个**(CI 里从 luci 源码编译)。LuCI 的 `.lmo` 不是 GNU MO 格式,
|
||||
> 用 `msgfmt` 转出来的 MO 文件能被 `msgunfmt` 读,但 LuCI 的加载器读不了 —— 装上去的表现是
|
||||
> **页面所有文案退回英文**。本地要构建的话:
|
||||
>
|
||||
> ```sh
|
||||
> git clone --depth 1 --filter=blob:none --sparse https://github.com/openwrt/luci.git
|
||||
> cd luci/modules/luci-base/src && make po2lmo # 产物在当前目录
|
||||
> export PATH="$PWD:$PATH"
|
||||
> ```
|
||||
|
||||
`build-pkg.sh` 用了仓库里那个**专为本包定制的最小工具链**(apk-tools 自编译 + po2lmo 自编译 + ipkg-build),不依赖完整 OpenWrt SDK 编译环境;这就是为什么 `build.yml` 跑得很快(~40s)。所有 actions 都用 Node 24 版(`actions/checkout@v5` / `actions/upload-artifact@v6` / `softprops/action-gh-release@v3`)。
|
||||
|
||||
## 测试
|
||||
|
||||
```sh
|
||||
sh tests/run-all.sh
|
||||
```
|
||||
|
||||
三组离线测试,不依赖路由器:
|
||||
|
||||
- `render-test.sh` —— UCI → `ipsec.conf`/`ipsec.secrets` 的映射、secrets 权限 0600、
|
||||
停用时不留 conn、依赖缺失时不生成配置
|
||||
- `sessions-parse-test.sh` —— 客户端列表解析:控制字符清洗、旧版脏行丢弃、
|
||||
在线但无地址时打 `vip_pending`、历史上限 200 条
|
||||
- `sanitize-test.sh` —— 控制字符清洗工具
|
||||
|
||||
脚本用的是 POSIX sh + awk,**在 OpenWrt 的 busybox 上也能跑**(CI 里跑的是
|
||||
GNU 环境,所以改完记得也在设备上跑一遍:busybox 的 `tr` 不认 `[:cntrl:]`
|
||||
字符类,会把字面字符删掉——这个坑就是这么漏出去的)。
|
||||
|
||||
## CI
|
||||
|
||||
`.github/workflows/` 下两个 workflow:
|
||||
@@ -102,23 +192,37 @@ ls -l *.apk *.ipk
|
||||
```
|
||||
.
|
||||
├── Makefile # OpenWrt 包定义:LUCI_TITLE / LUCI_DEPENDS / PKG_*
|
||||
├── tests/ # 离线回归测试(不依赖路由器,CI 里也会跑)
|
||||
│ ├── run-all.sh # 跑全部测试
|
||||
│ ├── render-test.sh # UCI → ipsec.conf / ipsec.secrets 渲染
|
||||
│ ├── sessions-parse-test.sh # 客户端列表解析(控制字符、脏行、历史上限)
|
||||
│ └── sanitize-test.sh # 控制字符清洗
|
||||
├── htdocs/luci-static/resources/view/ipsec-vpnd.js
|
||||
│ # LuCI web UI(cbid 表格 + 启停按钮)
|
||||
│ # LuCI web UI(配置表单 + 在线客户端表 + 依赖诊断)
|
||||
├── po/
|
||||
│ ├── templates/ipsec-vpnd.pot # gettext 模板
|
||||
│ └── zh_Hans/ipsec-vpnd.po # 简体中文翻译
|
||||
├── root/
|
||||
│ ├── etc/config/ipsec-vpnd # UCI 默认配置(服务默认 enabled=0)
|
||||
│ ├── etc/init.d/ipsec-vpnd # procd init:写 ipsec.conf/secrets、启 starter
|
||||
│ ├── usr/lib/ipsec/luci-ipsec-vpnd-render # UCI → ipsec.conf/ipsec.secrets 渲染器(含依赖自检)
|
||||
│ ├── etc/uci-defaults/luci-ipsec-vpnd
|
||||
│ │ # 首次安装一次性:建 network.VPN + firewall
|
||||
│ ├── usr/lib/ipsec/luci-ipsec-vpnd-deps
|
||||
│ │ # 依赖自检:init 启动拦截 + LuCI 诊断面板共用
|
||||
│ ├── usr/lib/ipsec/luci-ipsec-vpnd-updown
|
||||
│ │ # strongSwan updown 钩子:记录客户端上下线
|
||||
│ ├── usr/lib/ipsec/luci-ipsec-vpnd-sanitize
|
||||
│ │ # 剥掉用户名里的控制字符(busybox 的 tr 不认字符类)
|
||||
│ ├── usr/libexec/rpcd/ipsec-vpnd # rpcd 后端:sessions / clear_history / deps / apply
|
||||
│ ├── usr/share/luci/menu.d/luci-app-ipsec-vpnd.json
|
||||
│ │ # LuCI 菜单注册(VPN → IPSec VPN Server)
|
||||
│ └── usr/share/rpcd/acl.d/luci-app-ipsec-vpnd.json
|
||||
│ # rpcd ACL:放行 luci-app-ipsec-vpnd 读写 /etc/config/ipsec-vpnd
|
||||
├── .github/workflows/
|
||||
│ ├── build.yml
|
||||
│ └── cleanup-old-workflow.yml
|
||||
├── .github/
|
||||
│ ├── build-pkg.sh # 绕过 SDK 的 apk / ipk 直接打包脚本
|
||||
│ └── workflows/
|
||||
│ ├── build.yml
|
||||
│ └── cleanup-old-workflow.yml
|
||||
├── LICENSE # Apache-2.0
|
||||
└── README.md # 本文件
|
||||
```
|
||||
@@ -135,11 +239,13 @@ ls -l *.apk *.ipk
|
||||
| 现象 | 排查 |
|
||||
|---|---|
|
||||
| LuCI 菜单看不到 IPSec VPN Server | LuCI → 系统 → 软件包,确认 `luci-app-ipsec-vpnd` 已装;刷新 LuCI 缓存(`rm -f /tmp/luci-indexcache`) |
|
||||
| 服务起不来,日志说 "starter: no such file" | `strongswan-minimal` 没装;装上 |
|
||||
| 装完服务起不来,日志说 "starter: no such file" | strongSwan 运行时没装上。先 `apk update`(索引缓存坏了会搜不到包),再 `apk add strongswan-minimal strongswan-mod-xauth-generic strongswan-mod-kernel-libipsec strongswan-mod-des` |
|
||||
| LuCI 页面顶部出现红色「缺少依赖」 | 页面会直接列出缺哪一项以及对应的安装命令,照着执行即可 |
|
||||
| 客户端拨不上,日志说 "no IKE config" | 看 `/etc/config/ipsec-vpnd` 的 `enabled` 是不是 `1`;看 init 日志(`logread \| grep ipsec`) |
|
||||
| 系统自带 ipsec 又被启起来抢端口 | 升级 strongswan 包后可能要再跑一次 `uci-defaults/luci-ipsec-vpnd`——本包 init 脚本每次启动前都会重新 stop+disable 系统那个,但只在服务 enabled 时跑 |
|
||||
| ImmortalWrt 25.12 装不上(缺依赖) | 见上面“关于 ImmortalWrt 25.12 系列” |
|
||||
| GitHub Actions 出包失败 | 看 `Build` workflow 的 step 日志;常见坑在 `actions/cache` 没命中、host 工具链 apt 装失败这些 |
|
||||
| 客户端拨不上,日志说 "no acceptable proposal" | 多半缺 3DES 插件:装 `strongswan-mod-des` |
|
||||
| 服务显示「未运行」但 charon 明明在跑 | 说明有第二个服务实例抢占了 UDP/500、4500。本包不再自建服务,正常情况下只有系统 `ipsec` 一个实例;`ls /etc/rc.d/*ipsec*` 检查是否残留旧版 `ipsec-vpnd` 链接 |
|
||||
| 装完 `ipsec0` 上没有 VPN 网关地址 | 执行 `ifup VPN`;本包在安装与服务启动时都会自动执行一次 |
|
||||
| GitHub Actions 出包失败 | 看 `Build` workflow 的 step 日志;常见是 host 工具链 apt 装失败、或 apk-tools/po2lmo 编译报错 |
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ const callServiceList = rpc.declare({
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callServiceInit = rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: ['name', 'action']
|
||||
const callIpsecApply = rpc.declare({
|
||||
object: 'ipsec-vpnd',
|
||||
method: 'apply',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callIpsecSessions = rpc.declare({
|
||||
@@ -35,25 +35,36 @@ const callClearHistory = rpc.declare({
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callIpsecDeps = rpc.declare({
|
||||
object: 'ipsec-vpnd',
|
||||
method: 'deps',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callIpsecStatus = rpc.declare({
|
||||
object: 'ipsec-vpnd',
|
||||
method: 'status',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('ipsec-vpnd'), {}).then(function(res) {
|
||||
let isRunning = false;
|
||||
try {
|
||||
// procd_open_instance 未指定实例名时,procd 按 init 脚本 basename
|
||||
// 命名实例为 'ipsec-vpnd';遍历全部实例,任一 running 即视为运行
|
||||
let instances = res['ipsec-vpnd']['instances'] || {};
|
||||
for (let name in instances)
|
||||
if (instances[name]['running'])
|
||||
isRunning = true;
|
||||
} catch (e) { }
|
||||
return isRunning;
|
||||
// 后端直接报两件事:UCI 里的 enabled 总开关,以及系统 ipsec 服务是否在跑。
|
||||
// 只看服务状态会出现"开关是关的、服务却在跑"这种自相矛盾的情况。
|
||||
return L.resolveDefault(callIpsecStatus(), {}).then(function(res) {
|
||||
return {
|
||||
enabled: (res && res.enabled === '1'),
|
||||
running: !!(res && res.running)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(isRunning) {
|
||||
function renderStatus(st) {
|
||||
let spanTemp = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
|
||||
let renderHTML;
|
||||
if (isRunning)
|
||||
|
||||
if (!st.enabled)
|
||||
renderHTML = spanTemp.format('orange', _('IPSec VPN'), _('DISABLED'));
|
||||
else if (st.running)
|
||||
renderHTML = spanTemp.format('green', _('IPSec VPN'), _('RUNNING'));
|
||||
else
|
||||
renderHTML = spanTemp.format('red', _('IPSec VPN'), _('NOT RUNNING'));
|
||||
@@ -65,6 +76,15 @@ return view.extend({
|
||||
render() {
|
||||
let m, s, o;
|
||||
|
||||
// 配置落盘成功后,渲染 ipsec.conf 并按 enabled 启停服务。
|
||||
// 挂在 "uci-applied" 上是为了保证读到的是**已经落盘**的配置。
|
||||
if (!document.ipsecVpndApplyHook) {
|
||||
document.ipsecVpndApplyHook = true;
|
||||
document.addEventListener('uci-applied', function() {
|
||||
L.resolveDefault(callIpsecApply(), {});
|
||||
});
|
||||
}
|
||||
|
||||
m = new form.Map('ipsec-vpnd', _('IPSec VPN Server'),
|
||||
_('IPSec VPN connectivity using the native built-in VPN Client on iOS or Android (IKEv1 with PSK and Xauth)'));
|
||||
|
||||
@@ -83,6 +103,43 @@ return view.extend({
|
||||
]);
|
||||
}
|
||||
|
||||
// 依赖诊断:strongSwan 运行时不全时,页面直接说明缺什么、怎么装。
|
||||
// 没有这块的话,用户只会看到 NOT RUNNING,无从判断是缺包还是配置问题。
|
||||
s = m.section(form.TypedSection);
|
||||
s.anonymous = true;
|
||||
s.render = function() {
|
||||
let box = E('div', { class: 'cbi-section-error' });
|
||||
let wrap = E('div', { id: 'ipsec_vpnd_deps_wrap', style: 'display:none' }, [ box ]);
|
||||
|
||||
let update = function() {
|
||||
return L.resolveDefault(callIpsecDeps(), {}).then(function(res) {
|
||||
let files = (res && Array.isArray(res.files)) ? res.files : [];
|
||||
let missing = files.filter(function(d) { return d.status !== 'ok'; });
|
||||
if (missing.length === 0) {
|
||||
wrap.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
wrap.style.display = '';
|
||||
box.textContent = '';
|
||||
box.appendChild(E('h3', {}, _('Missing dependencies')));
|
||||
missing.forEach(function(d) {
|
||||
box.appendChild(E('p', {}, d.detail || d.key));
|
||||
});
|
||||
box.appendChild(E('p', {}, _('Install the missing packages with:')));
|
||||
let mgr = (res && res.pkg_mgr === 'opkg') ? 'opkg' : 'apk';
|
||||
let cmd = (mgr === 'opkg')
|
||||
? 'opkg update && opkg install strongswan-minimal strongswan-mod-xauth-generic strongswan-mod-kernel-libipsec strongswan-mod-des'
|
||||
: 'apk update && apk add strongswan-minimal strongswan-mod-xauth-generic strongswan-mod-kernel-libipsec strongswan-mod-des';
|
||||
box.appendChild(E('pre', {}, cmd));
|
||||
});
|
||||
};
|
||||
|
||||
poll.add(update, 15);
|
||||
update();
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'ipsec', 'service');
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
@@ -135,10 +192,15 @@ return view.extend({
|
||||
let renderRows = function(sessions) {
|
||||
clientBody.textContent = '';
|
||||
sessions.forEach(function(s) {
|
||||
// 在线但还没拿到虚拟地址:CHILD_SA 尚未装好,或解析不到 /32
|
||||
// 选择器。给一句明确提示,别让用户对着 "-" 猜。
|
||||
let vipText = s.vip;
|
||||
if (!vipText)
|
||||
vipText = s.vip_pending ? _('Assigned IP pending') : '-';
|
||||
clientBody.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, s.user || '-'),
|
||||
E('td', { 'class': 'td' }, s.remote || '-'),
|
||||
E('td', { 'class': 'td' }, s.vip || '-'),
|
||||
E('td', { 'class': 'td' }, vipText),
|
||||
E('td', { 'class': 'td' }, s.age || '-'),
|
||||
E('td', { 'class': 'td' }, s.online ? _('Connected') : _('Disconnected'))
|
||||
]));
|
||||
@@ -186,13 +248,14 @@ return view.extend({
|
||||
]);
|
||||
};
|
||||
|
||||
// 保存后显式 restart 服务:LuCI 保存走 uci commit,不会触发 procd 的
|
||||
// config.change reload trigger(实测 commit/reload_config 均不重启服务),
|
||||
// 必须显式 rc.init restart 才能让启用/停用即时生效。
|
||||
// 保存只负责把改动交给 LuCI 暂存;真正落盘(apply)由页面在所有 map
|
||||
// 保存完之后统一触发,落盘成功时 LuCI 会在 document 上派发 "uci-applied"。
|
||||
//
|
||||
// 渲染配置、启停服务必须放在那个事件里:早一步的话渲染器读到的是
|
||||
// 落盘前的旧值 —— 结果就是"启用"开关点了没反应(配置存下去了,
|
||||
// ipsec.conf 还是按旧的 enabled 生成的)。
|
||||
m.save = function(ev) {
|
||||
return form.Map.prototype.save.call(this).then(function() {
|
||||
return L.resolveDefault(callServiceInit('ipsec-vpnd', 'restart'));
|
||||
});
|
||||
return form.Map.prototype.save.call(this);
|
||||
};
|
||||
|
||||
return m.render();
|
||||
|
||||
@@ -9,6 +9,10 @@ msgstr ""
|
||||
msgid "Assigned IP"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:186
|
||||
msgid "Assigned IP pending"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:162
|
||||
msgid "Clear all client history records?"
|
||||
msgstr ""
|
||||
@@ -78,6 +82,10 @@ msgstr ""
|
||||
msgid "NOT RUNNING"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:53
|
||||
msgid "DISABLED"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:99
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
@@ -113,3 +121,11 @@ msgstr ""
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:86
|
||||
msgid "VPN Client IP"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:120
|
||||
msgid "Install the missing packages with:"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:119
|
||||
msgid "Missing dependencies"
|
||||
msgstr ""
|
||||
@@ -16,6 +16,10 @@ msgstr "账户"
|
||||
msgid "Assigned IP"
|
||||
msgstr "分配的 IP"
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:186
|
||||
msgid "Assigned IP pending"
|
||||
msgstr "分配地址未就绪"
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:162
|
||||
msgid "Clear all client history records?"
|
||||
msgstr "确定要清除所有客户端历史记录吗?"
|
||||
@@ -87,6 +91,10 @@ msgstr "当前没有 VPN 客户端在线"
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:53
|
||||
msgid "DISABLED"
|
||||
msgstr "已停用"
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:99
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
@@ -122,3 +130,11 @@ msgstr "VPN客户端DNS服务器"
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:86
|
||||
msgid "VPN Client IP"
|
||||
msgstr "VPN客户端地址段"
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:120
|
||||
msgid "Install the missing packages with:"
|
||||
msgstr "请先安装缺失的软件包:"
|
||||
|
||||
#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:119
|
||||
msgid "Missing dependencies"
|
||||
msgstr "缺少依赖"
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=90
|
||||
STOP=10
|
||||
|
||||
USE_PROCD=1
|
||||
PROG=/usr/lib/ipsec/starter
|
||||
|
||||
. $IPKG_INSTROOT/lib/functions.sh
|
||||
. $IPKG_INSTROOT/lib/functions/network.sh
|
||||
|
||||
IPSEC_SECRETS_FILE=/etc/ipsec.secrets
|
||||
IPSEC_CONN_FILE=/etc/ipsec.conf
|
||||
STRONGSWAN_CONF_FILE=/etc/strongswan.conf
|
||||
|
||||
reload_service() {
|
||||
config_load ipsec-vpnd
|
||||
|
||||
local vt_enabled
|
||||
config_get_bool vt_enabled "ipsec" "enabled" "0"
|
||||
[ "$vt_enabled" -eq "0" ] && {
|
||||
stop
|
||||
return 1
|
||||
}
|
||||
|
||||
# 配置变更时重启 procd 实例以重新加载。
|
||||
# 不要使用 strongswan 的 ipsec rereadall/reload 命令:
|
||||
# 它们通过 starter pidfile 操作 charon,会误杀本服务
|
||||
# 自己管理的 starter/charon 实例
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "ipsec-vpnd"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load ipsec-vpnd
|
||||
|
||||
local vt_enabled
|
||||
config_get_bool vt_enabled "ipsec" "enabled" "0"
|
||||
[ "$vt_enabled" -eq "0" ] && return 1
|
||||
|
||||
# 系统 strongswan 的 ipsec 服务与本服务共用 charon 及 UDP 500/4500,
|
||||
# 必须停掉并禁用来避免抢端口。每次启动都重新执行,防止系统 ipsec
|
||||
# 被升级或手动重新启用后再次抢占。
|
||||
[ -x /etc/init.d/ipsec ] && {
|
||||
/etc/init.d/ipsec stop >/dev/null 2>&1
|
||||
/etc/init.d/ipsec disable >/dev/null 2>&1
|
||||
}
|
||||
|
||||
local vt_clientip vt_clientdns vt_account vt_password vt_secret
|
||||
config_get vt_clientip "ipsec" "clientip" "10.9.8.10/24"
|
||||
config_get vt_clientdns "ipsec" "clientdns" "10.9.8.1"
|
||||
config_get vt_account "ipsec" "account" "vpnuser"
|
||||
config_get vt_password "ipsec" "password" "changeme"
|
||||
config_get vt_secret "ipsec" "secret" "changeme"
|
||||
|
||||
cat > /etc/ipsec.conf <<EOF
|
||||
# ipsec.conf - strongSwan IPsec configuration file
|
||||
|
||||
# basic configuration
|
||||
|
||||
config setup
|
||||
# strictcrlpolicy=yes
|
||||
uniqueids=never
|
||||
|
||||
# Add connections here.
|
||||
|
||||
conn xauth_psk
|
||||
keyexchange=ikev1
|
||||
ike=aes128-sha1-modp2048,aes128-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1536
|
||||
esp=aes128-sha1,3des-sha1
|
||||
left=%defaultroute
|
||||
leftauth=psk
|
||||
leftsubnet=0.0.0.0/0
|
||||
right=%any
|
||||
rightauth=psk
|
||||
rightauth2=xauth
|
||||
rightsourceip=$vt_clientip
|
||||
rightdns=$vt_clientdns
|
||||
leftupdown=/usr/lib/ipsec/luci-ipsec-vpnd-updown
|
||||
auto=add
|
||||
EOF
|
||||
|
||||
# secrets 含 PSK 与 XAUTH 密码,用 177 umask 创建并收紧权限,
|
||||
# 避免本地用户读取
|
||||
umask 177
|
||||
cat > /etc/ipsec.secrets <<EOF
|
||||
# /etc/ipsec.secrets - strongSwan IPsec secrets file
|
||||
: PSK "$vt_secret"
|
||||
$vt_account : XAUTH "$vt_password"
|
||||
EOF
|
||||
umask 022
|
||||
chmod 600 /etc/ipsec.secrets
|
||||
|
||||
procd_open_instance
|
||||
|
||||
procd_set_param command $PROG --daemon charon --nofork
|
||||
|
||||
procd_set_param file $IPSEC_CONN_FILE
|
||||
procd_append_param file $IPSEC_SECRETS_FILE
|
||||
procd_append_param file $STRONGSWAN_CONF_FILE
|
||||
procd_append_param file /etc/strongswan.d/*.conf
|
||||
procd_append_param file /etc/strongswan.d/charon/*.conf
|
||||
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
# proxy_arp 由 updown 钩子按 ipsec0 接口单独设置,接口在服务停止时随 charon
|
||||
# 一并移除,无需在此清理。
|
||||
return 0
|
||||
}
|
||||
@@ -7,8 +7,15 @@
|
||||
# 在 conffiles 中,包升级和 sysupgrade 都会保留,标记不会丢失。
|
||||
[ "$(uci -q get ipsec-vpnd.ipsec.initialized)" = "1" ] && exit 0
|
||||
|
||||
# 禁用 strongswan 自带的 ipsec 服务,避免与 ipsec-vpnd 共用 charon 冲突
|
||||
[ -x /etc/init.d/ipsec ] && /etc/init.d/ipsec disable
|
||||
# charon 交由 strongSwan 原生的 `ipsec` 服务托管(strongswan-ipsec 包提供),
|
||||
# 本包不再自建服务实例 —— 两个服务同时跑会抢 UDP 500/4500,后启动的那个
|
||||
# 会静默退出,LuCI 上显示成"未运行"却查不到原因。这里按 enabled 决定
|
||||
# 它是否开机自启(enabled 是总开关,关掉时服务也不该跑)。
|
||||
if [ "$(uci -q get ipsec-vpnd.ipsec.enabled)" = "1" ]; then
|
||||
[ -x /etc/init.d/ipsec ] && /etc/init.d/ipsec enable
|
||||
else
|
||||
[ -x /etc/init.d/ipsec ] && { /etc/init.d/ipsec disable; /etc/init.d/ipsec stop; }
|
||||
fi
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete firewall.ipsecd
|
||||
@@ -94,5 +101,21 @@ EOF
|
||||
uci -q set ipsec-vpnd.ipsec.initialized='1'
|
||||
uci -q commit ipsec-vpnd
|
||||
|
||||
# UCI 配置里明文存着 XAuth 密码与 PSK,收紧到 0600
|
||||
chmod 600 /etc/config/ipsec-vpnd 2>/dev/null
|
||||
|
||||
# 清理旧版本自建的 ipsec-vpnd 服务残留(升级路径)。新的版本不再提供服务实例,
|
||||
# charon 统一由 strongSwan 的 ipsec 服务托管。
|
||||
/etc/init.d/ipsec-vpnd stop >/dev/null 2>&1
|
||||
/etc/init.d/ipsec-vpnd disable >/dev/null 2>&1
|
||||
rm -f /etc/rc.d/S*ipsec-vpnd /etc/rc.d/K*ipsec-vpnd
|
||||
|
||||
# 生成初始 strongSwan 配置
|
||||
/usr/lib/ipsec/luci-ipsec-vpnd-render >/dev/null 2>&1
|
||||
|
||||
# 立即拉起 VPN 网关接口。ipsec0 由 charon 启动后才出现,netifd 会等设备
|
||||
# 就绪后重试,这样装完就能用,不必手动 ifup VPN。
|
||||
/sbin/ifup VPN >/dev/null 2>&1
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# luci-app-ipsec-vpnd 运行时依赖自检。
|
||||
#
|
||||
# 由两处共用:
|
||||
# - /etc/init.d/ipsec-vpnd:缺依赖时拒绝启动并写明确日志(避免只看到 NOT RUNNING)
|
||||
# - /usr/libexec/rpcd/ipsec-vpnd:LuCI 页面的依赖诊断面板
|
||||
#
|
||||
# 用法:
|
||||
# luci-ipsec-vpnd-deps files 文件类依赖检查(starter / charon / 插件)
|
||||
# luci-ipsec-vpnd-deps pkgs 软件包安装状态
|
||||
#
|
||||
# 输出格式(每行一条,'|' 分隔):
|
||||
# <key>|<status>|<detail>
|
||||
# status: ok | missing
|
||||
# detail: ok 时为路径或版本号;missing 时为处理建议
|
||||
#
|
||||
# 退出码:全部就绪 0;存在缺失 1。
|
||||
|
||||
PKGS="strongswan-minimal strongswan-charon strongswan-mod-stroke strongswan-mod-xauth-generic strongswan-mod-kernel-libipsec strongswan-mod-des kmod-tun"
|
||||
|
||||
MISSING=0
|
||||
|
||||
check_file() {
|
||||
# $1=key $2=路径 $3=缺失时的处理建议
|
||||
if [ -e "$2" ]; then
|
||||
printf '%s|ok|%s\n' "$1" "$2"
|
||||
else
|
||||
MISSING=1
|
||||
printf '%s|missing|%s\n' "$1" "$3"
|
||||
fi
|
||||
}
|
||||
|
||||
files() {
|
||||
check_file starter /usr/lib/ipsec/starter \
|
||||
"缺少 strongSwan 启动器 /usr/lib/ipsec/starter(由 strongswan-mod-stroke 提供,随 strongswan-minimal 一并安装)"
|
||||
check_file charon /usr/lib/ipsec/charon \
|
||||
"缺少 IKE 守护进程 /usr/lib/ipsec/charon(需安装 strongswan-charon)"
|
||||
check_file xauth /etc/strongswan.d/charon/xauth-generic.conf \
|
||||
"缺少 xauth-generic 插件(需安装 strongswan-mod-xauth-generic,否则 XAuth 账号无法认证)"
|
||||
check_file libipsec /etc/strongswan.d/charon/kernel-libipsec.conf \
|
||||
"缺少 kernel-libipsec 插件(需安装 strongswan-mod-kernel-libipsec)"
|
||||
check_file des /etc/strongswan.d/charon/des.conf \
|
||||
"缺少 des 插件(需安装 strongswan-mod-des,否则本包声明的 3DES 提案不可用)"
|
||||
}
|
||||
|
||||
# 取已安装包的版本号;apk 与 opkg 两种包管理器都支持
|
||||
pkg_version() {
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk list --installed "$1" 2>/dev/null | awk 'NR==1{print $1}'
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
opkg list-installed "$1" 2>/dev/null | awk 'NR==1{print $3}'
|
||||
fi
|
||||
}
|
||||
|
||||
pkgs() {
|
||||
for p in $PKGS; do
|
||||
v="$(pkg_version "$p")"
|
||||
if [ -n "$v" ]; then
|
||||
printf '%s|ok|%s\n' "$p" "$v"
|
||||
else
|
||||
MISSING=1
|
||||
printf '%s|missing|not installed\n' "$p"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
case "${1:-files}" in
|
||||
files) files ;;
|
||||
pkgs) pkgs ;;
|
||||
*) files ;;
|
||||
esac
|
||||
|
||||
exit $MISSING
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 从 /etc/config/ipsec-vpnd 渲染 strongSwan 的 ipsec.conf / ipsec.secrets。
|
||||
#
|
||||
# 本包不自己启动 charon —— charon 由 strongSwan 原生的 `ipsec` 服务托管
|
||||
# (/etc/init.d/ipsec,来自 strongswan-ipsec 包),避免两个服务抢 UDP 500/4500
|
||||
# 导致其中一个无声退出。本脚本只负责把 UCI 配置翻译成 strongSwan 配置。
|
||||
#
|
||||
# 调用方:rpcd 的 apply 方法、uci-defaults、post-install/post-upgrade。
|
||||
# 未启用服务(enabled=0)时不写文件,保留用户已有配置。
|
||||
|
||||
. "${IPKG_INSTROOT:-}/lib/functions.sh"
|
||||
|
||||
# 依赖自检:strongSwan 运行时不全时不要生成一份"看着能起、实际连不上"的配置,
|
||||
# 直接报错,由调用方(rpcd apply / postinst)把原因透出来。
|
||||
if [ -x /usr/lib/ipsec/luci-ipsec-vpnd-deps ]; then
|
||||
deps_out="$(/usr/lib/ipsec/luci-ipsec-vpnd-deps files)"
|
||||
if [ $? -ne 0 ]; then
|
||||
oldifs="$IFS"
|
||||
IFS='
|
||||
'
|
||||
for line in $deps_out; do
|
||||
case "$line" in
|
||||
*'|missing|'*) logger -t ipsec-vpnd "依赖缺失: ${line#*|missing|}" ;;
|
||||
esac
|
||||
done
|
||||
IFS="$oldifs"
|
||||
logger -t ipsec-vpnd "strongSwan 运行时不完整,未生成 ipsec.conf。请先 apk update,再安装 strongswan-minimal strongswan-mod-xauth-generic strongswan-mod-kernel-libipsec strongswan-mod-des"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
config_load ipsec-vpnd
|
||||
|
||||
vt_enabled=0
|
||||
config_get vt_enabled "ipsec" "enabled" "0"
|
||||
|
||||
vt_clientip="10.9.8.10/24"
|
||||
vt_clientdns="10.9.8.1"
|
||||
vt_account="vpnuser"
|
||||
vt_password="changeme"
|
||||
vt_secret="changeme"
|
||||
config_get vt_clientip "ipsec" "clientip" "$vt_clientip"
|
||||
config_get vt_clientdns "ipsec" "clientdns" "$vt_clientdns"
|
||||
config_get vt_account "ipsec" "account" "$vt_account"
|
||||
config_get vt_password "ipsec" "password" "$vt_password"
|
||||
config_get vt_secret "ipsec" "secret" "$vt_secret"
|
||||
|
||||
# 先写到临时文件,再和现有文件比对:内容没变就不动文件。
|
||||
# 这样 rpcd 的 apply 才知道要不要重启服务 —— 别的页面触发一次 apply
|
||||
# 不应该把正在跑的 VPN 连接踢掉。
|
||||
TMP_CONF="$(mktemp /tmp/.ipsec.conf.XXXXXX)"
|
||||
TMP_SECRETS="$(mktemp /tmp/.ipsec.secrets.XXXXXX)"
|
||||
|
||||
cat > "$TMP_CONF" <<EOF
|
||||
# ipsec.conf - strongSwan IPsec configuration file
|
||||
#
|
||||
# 由 luci-app-ipsec-vpnd 自动生成,请勿手工修改:
|
||||
# 改动请在 LuCI 的 "VPN → IPSec VPN Server" 里做。
|
||||
|
||||
# basic configuration
|
||||
|
||||
config setup
|
||||
# strictcrlpolicy=yes
|
||||
uniqueids=never
|
||||
|
||||
# Add connections here.
|
||||
EOF
|
||||
|
||||
if [ "$vt_enabled" = "1" ]; then
|
||||
cat >> "$TMP_CONF" <<EOF
|
||||
|
||||
conn xauth_psk
|
||||
keyexchange=ikev1
|
||||
ike=aes128-sha1-modp2048,aes128-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1536
|
||||
esp=aes128-sha1,3des-sha1
|
||||
left=%defaultroute
|
||||
leftauth=psk
|
||||
leftsubnet=0.0.0.0/0
|
||||
right=%any
|
||||
rightauth=psk
|
||||
rightauth2=xauth
|
||||
rightsourceip=$vt_clientip
|
||||
rightdns=$vt_clientdns
|
||||
leftupdown=/usr/lib/ipsec/luci-ipsec-vpnd-updown
|
||||
auto=add
|
||||
EOF
|
||||
else
|
||||
# 服务停用时也要把 conn 拿掉:charon 由系统 ipsec 服务托管,配置是全局的,
|
||||
# 残留的 conn 会让服务在"已停用"状态下依然接受连接。
|
||||
logger -t ipsec-vpnd "服务未启用,生成的 ipsec.conf 不含 conn"
|
||||
fi
|
||||
|
||||
if [ "$vt_enabled" = "1" ]; then
|
||||
# secrets 含 PSK 与 XAUTH 密码,用 177 umask 创建并收紧权限,避免本地用户读取
|
||||
umask 177
|
||||
cat > "$TMP_SECRETS" <<EOF
|
||||
# /etc/ipsec.secrets - strongSwan IPsec secrets file
|
||||
# 由 luci-app-ipsec-vpnd 自动生成,请勿手工修改。
|
||||
: PSK "$vt_secret"
|
||||
$vt_account : XAUTH "$vt_password"
|
||||
EOF
|
||||
umask 022
|
||||
else
|
||||
# 停用时清掉遗留的凭据,避免旧口令继续留在磁盘上
|
||||
: > "$TMP_SECRETS"
|
||||
fi
|
||||
|
||||
changed=0
|
||||
if ! cmp -s "$TMP_CONF" /etc/ipsec.conf; then
|
||||
changed=1
|
||||
fi
|
||||
if [ "$vt_enabled" = "1" ]; then
|
||||
if ! cmp -s "$TMP_SECRETS" /etc/ipsec.secrets; then
|
||||
changed=1
|
||||
fi
|
||||
elif [ -e /etc/ipsec.secrets ]; then
|
||||
# 停用状态下要把遗留的凭据文件清掉
|
||||
changed=1
|
||||
fi
|
||||
|
||||
if [ "$changed" = "1" ]; then
|
||||
cat "$TMP_CONF" > /etc/ipsec.conf
|
||||
if [ "$vt_enabled" = "1" ]; then
|
||||
cat "$TMP_SECRETS" > /etc/ipsec.secrets
|
||||
chmod 600 /etc/ipsec.secrets
|
||||
else
|
||||
rm -f /etc/ipsec.secrets
|
||||
fi
|
||||
echo "changed"
|
||||
else
|
||||
echo "unchanged"
|
||||
fi
|
||||
|
||||
rm -f "$TMP_CONF" "$TMP_SECRETS"
|
||||
exit 0
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 去掉输入里的所有控制字符(\t \n \r 等)。
|
||||
#
|
||||
# 为什么需要:XAuth 用户名由客户端完全可控(任何能连到 UDP 500 的人都能影响它),
|
||||
# 控制字符会把 TSV 记录撕开、字段错位并被持久化到历史文件里。
|
||||
#
|
||||
# 实现说明:**不要用 `tr -d '[:cntrl:]'`** —— busybox 的 tr 不认字符类,
|
||||
# 会把字面字符 : [ c n t r o l ] 全部删掉(实测 "testuser" 被删成 "esuse")。
|
||||
# busybox 的 sed / awk 认 POSIX 字符类,用 sed 最稳。
|
||||
|
||||
sed 's/[[:cntrl:]]//g'
|
||||
@@ -47,6 +47,17 @@ now="$(date +%s)"
|
||||
user="${PLUTO_XAUTH_ID:-}"
|
||||
remote="${PLUTO_PEER:-}"
|
||||
|
||||
# 用户名与对端地址都来自网络侧(任何能连到 UDP 500 的人都能影响它们)。
|
||||
# 控制字符会把 TSV 行撕开,导致历史文件字段错位并被持久化,这里统一剔除。
|
||||
SANITIZE=/usr/lib/ipsec/luci-ipsec-vpnd-sanitize
|
||||
if [ -x "$SANITIZE" ]; then
|
||||
user="$(printf '%s' "$user" | "$SANITIZE")"
|
||||
remote="$(printf '%s' "$remote" | "$SANITIZE")"
|
||||
else
|
||||
user="$(printf '%s' "$user" | sed 's/[[:cntrl:]]//g')"
|
||||
remote="$(printf '%s' "$remote" | sed 's/[[:cntrl:]]//g')"
|
||||
fi
|
||||
|
||||
# assigned client IP: the responder-sent virtual IP, then the /32 selector
|
||||
vip="${PLUTO_PEER_SOURCEIP%%/*}"
|
||||
[ -n "$vip" ] || vip="${PLUTO_MY_SOURCEIP%%/*}"
|
||||
@@ -58,7 +69,9 @@ vip="${PLUTO_PEER_SOURCEIP%%/*}"
|
||||
|
||||
key="$user|$remote"
|
||||
|
||||
[ -f "$STATE_FILE" ] || : > "$STATE_FILE"
|
||||
[ -f "$STATE_FILE" ] || ( umask 077; : > "$STATE_FILE" )
|
||||
# 状态文件里有客户端用户名与公网 IP,只允许 root 读
|
||||
chmod 600 "$STATE_FILE" 2>/dev/null
|
||||
|
||||
# serialize read/merge/write with the rpcd backend
|
||||
flock_lock
|
||||
@@ -82,5 +95,7 @@ END {
|
||||
|
||||
[ -s "$UP_TMP" ] && mv "$UP_TMP" "$STATE_FILE"
|
||||
rm -f "$UP_TMP"
|
||||
# mv 会把临时文件的权限带过来,这里再收紧一次
|
||||
chmod 600 "$STATE_FILE" 2>/dev/null
|
||||
flock_unlock
|
||||
exit 0
|
||||
@@ -37,7 +37,7 @@ flock_unlock() {
|
||||
}
|
||||
|
||||
list_methods() {
|
||||
printf '%s\n' '{"sessions":{},"clear_history":{}}'
|
||||
printf '%s\n' '{"sessions":{},"clear_history":{},"deps":{},"apply":{},"status":{}}'
|
||||
}
|
||||
|
||||
json_err() {
|
||||
@@ -92,6 +92,18 @@ BEGIN { n = 0; last = "" }
|
||||
ip = rest
|
||||
}
|
||||
remote[sa] = ip
|
||||
|
||||
# 远端 IKE ID:形如 192.168.1.100[testuser]。
|
||||
# strongSwan 只有在 IKE ID 与 XAuth 用户名不同时才打印
|
||||
# "Remote XAuth identity:",所以拿不到该行时可以用 IKE ID 兜底,
|
||||
# 两者一致 —— 这样与 updown 钩子记录的 key 也能对上,
|
||||
# 不会在客户端列表里出现同一客户端的两行。
|
||||
if (match(rest, /\[[^][]*\]/)) {
|
||||
id = substr(rest, RSTART + 1, RLENGTH - 2)
|
||||
gsub(/^[ \t"'"'"']+|[ \t"'"'"']+$/, "", id)
|
||||
gsub(/[[:cntrl:]]/, "", id)
|
||||
if (id != "") remoteid[sa] = id
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
@@ -101,6 +113,9 @@ BEGIN { n = 0; last = "" }
|
||||
if (sa != "" && (sa in est) && !(sa in user)) {
|
||||
v = substr(line, index(line, "identity:") + 9)
|
||||
gsub(/^[ \t"'"'"']+|[ \t"'"'"']+$/, "", v)
|
||||
# 用户名完全由客户端可控(任何能连到 UDP 500 的人)。控制字符
|
||||
# 会把 TSV 行撕开、字段错位并被持久化到历史文件里,这里统一剔除。
|
||||
gsub(/[[:cntrl:]]/, "", v)
|
||||
if (v != "") user[sa] = v
|
||||
}
|
||||
next
|
||||
@@ -127,6 +142,7 @@ END {
|
||||
for (k = 1; k <= n; k++) {
|
||||
sa = order[k]
|
||||
u = user[sa]; r = remote[sa]; v = vip[sa]; a = age[sa]
|
||||
if (u == "") u = remoteid[sa]
|
||||
if (u == "") u = "-"
|
||||
if (r == "") r = "-"
|
||||
printf "%s|%s\t%s\t%s\t%s\t%s\n", u, r, u, r, v, a
|
||||
@@ -139,7 +155,11 @@ sessions() {
|
||||
local online_tmp="/tmp/.luci-app-ipsec-vpnd-online.tsv"
|
||||
|
||||
# ensure the history file exists so awk can always read it as first input
|
||||
[ -f "$STATE_FILE" ] || : > "$STATE_FILE"
|
||||
# 文件里有客户端用户名与公网 IP,一开始就建成 0600
|
||||
if [ ! -f "$STATE_FILE" ]; then
|
||||
( umask 077; : > "$STATE_FILE" )
|
||||
fi
|
||||
chmod 600 "$STATE_FILE" 2>/dev/null
|
||||
|
||||
# current online clients as TSV
|
||||
/usr/sbin/ipsec statusall 2>/dev/null | parse_statusall > "$online_tmp"
|
||||
@@ -147,7 +167,7 @@ sessions() {
|
||||
# serialize read/merge/write with the updown hook
|
||||
flock_lock
|
||||
|
||||
awk -F'\t' -v now="$now" -v state_tmp="$SESSIONS_TMP" -v state="$STATE_FILE" '
|
||||
awk -F'\t' -v now="$now" -v MAXROWS=200 -v state_tmp="$SESSIONS_TMP" -v state="$STATE_FILE" '
|
||||
function jesc(s) {
|
||||
gsub(/\\/, "\\\\", s)
|
||||
gsub(/"/, "\\\"", s)
|
||||
@@ -156,19 +176,28 @@ function jesc(s) {
|
||||
BEGIN { n = 0 }
|
||||
FILENAME == state {
|
||||
# history rows: key user remote vip age last_seen
|
||||
# 字段数不对的行是旧版本被恶意用户名污染过的脏行,直接丢掉
|
||||
if (NF != 6) next
|
||||
key = $1
|
||||
if (key == "") next
|
||||
gsub(/[[:cntrl:]]/, "", key)
|
||||
if (!(key in seen)) { seen[key] = 1; order[++n] = key }
|
||||
hu[key] = $2; hr[key] = $3; hv[key] = $4; ha[key] = $5; hl[key] = $6
|
||||
gsub(/[[:cntrl:]]/, "", hu[key]); gsub(/[[:cntrl:]]/, "", hr[key])
|
||||
gsub(/[[:cntrl:]]/, "", hv[key]); gsub(/[[:cntrl:]]/, "", ha[key])
|
||||
next
|
||||
}
|
||||
{
|
||||
# online rows: key user remote vip age
|
||||
if (NF != 5) next
|
||||
key = $1
|
||||
if (key == "") next
|
||||
gsub(/[[:cntrl:]]/, "", key)
|
||||
active[key] = 1
|
||||
if (!(key in seen)) { seen[key] = 1; order[++n] = key }
|
||||
hu[key] = $2; hr[key] = $3; hv[key] = $4; ha[key] = $5; hl[key] = now
|
||||
gsub(/[[:cntrl:]]/, "", hu[key]); gsub(/[[:cntrl:]]/, "", hr[key])
|
||||
gsub(/[[:cntrl:]]/, "", hv[key]); gsub(/[[:cntrl:]]/, "", ha[key])
|
||||
}
|
||||
END {
|
||||
# build output order: online first (kept), then offline by last_seen desc
|
||||
@@ -185,6 +214,10 @@ END {
|
||||
}
|
||||
for (i = 1; i <= oc; i++) out[++nc] = off[i]
|
||||
|
||||
# 历史上限:在线的永远保留,离线的按最近使用排序后只留前 MAX 条。
|
||||
# 避免状态文件无限膨胀、每次轮询都把全部历史塞进 JSON。
|
||||
if (nc > MAXROWS) nc = MAXROWS
|
||||
|
||||
# JSON response
|
||||
printf "{\"code\":0,\"sessions\":["
|
||||
first = 1
|
||||
@@ -192,15 +225,21 @@ END {
|
||||
k = out[i]
|
||||
if (!first) printf ","
|
||||
first = 0
|
||||
printf "{\"user\":\"%s\",\"remote\":\"%s\",\"vip\":\"%s\",\"age\":\"%s\",\"online\":%s}",
|
||||
printf "{\"user\":\"%s\",\"remote\":\"%s\",\"vip\":\"%s\",\"age\":\"%s\",\"online\":%s",
|
||||
jesc(hu[k]), jesc(hr[k]), jesc(hv[k]), jesc(ha[k]),
|
||||
(active[k] ? "true" : "false")
|
||||
# 在线但没拿到地址:CHILD_SA 还没装好,或解析不到 /32 选择器。
|
||||
# 给前端一个明确标记,避免和"没有地址"混为一谈。
|
||||
if (active[k] && hv[k] == "")
|
||||
printf ",\"vip_pending\":true"
|
||||
printf "}"
|
||||
}
|
||||
printf "]}\n"
|
||||
|
||||
# rewrite persisted history (drop online flag; recomputed each call)
|
||||
for (i = 1; i <= n; i++) {
|
||||
k = order[i]
|
||||
# 只写保留下来的行,超出的旧记录就此淘汰。
|
||||
for (i = 1; i <= nc; i++) {
|
||||
k = out[i]
|
||||
printf "%s\t%s\t%s\t%s\t%s\t%s\n", k, hu[k], hr[k], hv[k], ha[k], hl[k] > state_tmp
|
||||
}
|
||||
}' "$STATE_FILE" "$online_tmp"
|
||||
@@ -213,10 +252,123 @@ END {
|
||||
mv "$SESSIONS_TMP" "$STATE_FILE"
|
||||
fi
|
||||
rm -f "$SESSIONS_TMP"
|
||||
# 状态文件里有客户端用户名与公网 IP,权限收紧到 0600
|
||||
chmod 600 "$STATE_FILE" 2>/dev/null
|
||||
flock_unlock
|
||||
return $rc
|
||||
}
|
||||
|
||||
apply() {
|
||||
# 把 UCI 配置渲染成 strongSwan 配置,然后按 enabled 启停服务。
|
||||
# charon 由 strongSwan 原生的 `ipsec` 服务托管,本包不再自建服务实例。
|
||||
local render_out render_rc enabled
|
||||
render_out="$(/usr/lib/ipsec/luci-ipsec-vpnd-render 2>&1)"
|
||||
render_rc=$?
|
||||
if [ "$render_rc" -ne 0 ]; then
|
||||
json_err "failed to render ipsec.conf: $render_out"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -x /etc/init.d/ipsec ]; then
|
||||
json_err "strongSwan 的 ipsec 服务不存在(缺 strongswan-ipsec 包)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# enabled 是总开关:关掉时连服务一起停,否则页面上开关是"关"、
|
||||
# 服务却还在跑,charon 也不会因为配置里没有 conn 就自己退出。
|
||||
enabled="$(uci -q get ipsec-vpnd.ipsec.enabled)"
|
||||
if [ "$enabled" = "1" ]; then
|
||||
/etc/init.d/ipsec enable >/dev/null 2>&1
|
||||
# 只有配置真的变了才重启服务:别的页面触发一次 apply 不应该
|
||||
# 把正在跑的 VPN 连接踢掉。
|
||||
if [ "$render_out" = "changed" ] || ! /etc/init.d/ipsec status >/dev/null 2>&1; then
|
||||
/etc/init.d/ipsec restart >/dev/null 2>&1
|
||||
fi
|
||||
else
|
||||
/etc/init.d/ipsec disable >/dev/null 2>&1
|
||||
/etc/init.d/ipsec stop >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_string message "applied"
|
||||
json_dump
|
||||
}
|
||||
|
||||
status() {
|
||||
# 页面状态用:enabled 来自 UCI,running 来自系统 ipsec 服务。
|
||||
# 两者分开报,前端才能区分"没启用"和"启用了但没跑起来"。
|
||||
local enabled running=0
|
||||
enabled="$(uci -q get ipsec-vpnd.ipsec.enabled)"
|
||||
[ "$enabled" = "1" ] || enabled="0"
|
||||
|
||||
if [ -x /etc/init.d/ipsec ]; then
|
||||
/etc/init.d/ipsec status >/dev/null 2>&1 && running=1
|
||||
fi
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_string enabled "$enabled"
|
||||
json_add_int running "$running"
|
||||
json_dump
|
||||
}
|
||||
|
||||
deps() {
|
||||
# 运行时依赖自检:文件类依赖 + 软件包安装状态。
|
||||
# 结果给 LuCI 的依赖诊断面板用;缺失时 init 脚本也会拒绝启动。
|
||||
local files_out files_rc pkgs_out pkgs_rc
|
||||
files_out="$(/usr/lib/ipsec/luci-ipsec-vpnd-deps files)"
|
||||
files_rc=$?
|
||||
pkgs_out="$(/usr/lib/ipsec/luci-ipsec-vpnd-deps pkgs)"
|
||||
pkgs_rc=$?
|
||||
|
||||
local ok=1
|
||||
[ "$files_rc" -eq 0 ] || ok=0
|
||||
[ "$pkgs_rc" -eq 0 ] || ok=0
|
||||
|
||||
json_init
|
||||
json_add_int code 0
|
||||
json_add_int ok "$ok"
|
||||
|
||||
# 包管理器名:安装命令要用对应的那个(apk / opkg)
|
||||
local pkg_mgr="apk"
|
||||
if ! command -v apk >/dev/null 2>&1 && command -v opkg >/dev/null 2>&1; then
|
||||
pkg_mgr="opkg"
|
||||
fi
|
||||
json_add_string pkg_mgr "$pkg_mgr"
|
||||
|
||||
local oldifs="$IFS"
|
||||
local line key rest st det
|
||||
IFS='
|
||||
'
|
||||
json_add_array files
|
||||
for line in $files_out; do
|
||||
[ -z "$line" ] && continue
|
||||
key="${line%%|*}"; rest="${line#*|}"; st="${rest%%|*}"; det="${rest#*|}"
|
||||
json_add_object ""
|
||||
json_add_string key "$key"
|
||||
json_add_string status "$st"
|
||||
json_add_string detail "$det"
|
||||
json_close_object
|
||||
done
|
||||
json_close_array
|
||||
|
||||
json_add_array packages
|
||||
for line in $pkgs_out; do
|
||||
[ -z "$line" ] && continue
|
||||
key="${line%%|*}"; rest="${line#*|}"; st="${rest%%|*}"; det="${rest#*|}"
|
||||
json_add_object ""
|
||||
json_add_string key "$key"
|
||||
json_add_string status "$st"
|
||||
json_add_string detail "$det"
|
||||
json_close_object
|
||||
done
|
||||
json_close_array
|
||||
IFS="$oldifs"
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
clear_history() {
|
||||
flock_lock
|
||||
rm -f "$STATE_FILE" "$SESSIONS_TMP"
|
||||
@@ -236,6 +388,9 @@ main() {
|
||||
case "$2" in
|
||||
sessions) sessions ;;
|
||||
clear_history) clear_history ;;
|
||||
deps) deps ;;
|
||||
apply) apply ;;
|
||||
status) status ;;
|
||||
*) json_err "unknown method" ;;
|
||||
esac
|
||||
;;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Grant access to ipsec configuration",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"ipsec-vpnd": [ "sessions", "clear_history" ],
|
||||
"ipsec-vpnd": [ "sessions", "deps", "status" ],
|
||||
"rc": [ "init" ],
|
||||
"service": [ "list" ]
|
||||
},
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"rc": [ "init" ]
|
||||
"rc": [ "init" ],
|
||||
"ipsec-vpnd": [ "apply", "clear_history" ]
|
||||
},
|
||||
"uci": [ "ipsec-vpnd" ]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# luci-ipsec-vpnd-render 的离线回归测试。
|
||||
#
|
||||
# 渲染器把 /etc/config/ipsec-vpnd 翻译成 strongSwan 的 ipsec.conf / ipsec.secrets,
|
||||
# 这里用一个临时根目录 + 假的 lib/functions.sh 桩件来跑它,不碰真实配置。
|
||||
#
|
||||
# 覆盖:
|
||||
# 1. enabled=1 时生成 conn,rightsourceip/rightdns 与 UCI 一致
|
||||
# 2. ipsec.secrets 权限 0600,且含 PSK 与 XAuth 两行
|
||||
# 3. enabled=0 时不生成 conn,并清掉遗留的 ipsec.secrets
|
||||
# 4. 依赖自检不通过时不生成任何文件、退出码非 0
|
||||
#
|
||||
# 用法:sh tests/render-test.sh [渲染器路径]
|
||||
|
||||
set -u
|
||||
|
||||
RENDER="${1:-root/usr/lib/ipsec/luci-ipsec-vpnd-render}"
|
||||
# 在设备上跑时仓库路径不存在,退回安装后的路径
|
||||
[ -f "$RENDER" ] || RENDER="/usr/lib/ipsec/luci-ipsec-vpnd-render"
|
||||
[ -f "$RENDER" ] || { echo "找不到渲染器: $RENDER"; exit 1; }
|
||||
|
||||
TMP="$(mktemp -d "${TMPDIR:-/tmp}/render-test.XXXXXX")"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
fail=0
|
||||
check() {
|
||||
# check <名称> <期望> <实际>
|
||||
if [ "$2" = "$3" ]; then
|
||||
echo "ok - $1"
|
||||
else
|
||||
echo "FAIL - $1"
|
||||
echo " 期望: [$2]"
|
||||
echo " 实际: [$3]"
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
count_matches() {
|
||||
# count_matches <正则> <文件>:匹配行数,文件不存在或无匹配都返回 0
|
||||
if [ -f "$2" ]; then
|
||||
grep -c "$1" "$2" 2>/dev/null | head -1
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
file_mode() {
|
||||
# 跨平台取权限(macOS / Linux / busybox 的 stat 参数都不一样,统一走 ls -l)
|
||||
ls -l "$1" 2>/dev/null | awk '{
|
||||
p = substr($1, 2, 9)
|
||||
v = 0
|
||||
for (i = 1; i <= 3; i++) {
|
||||
d = 0
|
||||
c = substr(p, i * 3 - 2, 1); if (c == "r") d += 4
|
||||
c = substr(p, i * 3 - 1, 1); if (c == "w") d += 2
|
||||
c = substr(p, i * 3, 1); if (c == "x") d += 1
|
||||
v = v * 10 + d
|
||||
}
|
||||
print v
|
||||
}'
|
||||
}
|
||||
|
||||
# --- 桩件环境 ---------------------------------------------------------------
|
||||
mkdir -p "$TMP/lib" "$TMP/etc" "$TMP/bin" "$TMP/usr/lib/ipsec"
|
||||
|
||||
cat > "$TMP/lib/functions.sh" <<'EOF'
|
||||
config_load() { :; }
|
||||
config_get() {
|
||||
# config_get <var> <section> <option> <default>
|
||||
_v="$(sed -n "s/^$3=//p" "$TEST_UCI" 2>/dev/null | head -n1)"
|
||||
[ -n "$_v" ] || _v="$4"
|
||||
eval "$1=\$_v"
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$TMP/bin/logger" <<'EOF'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
EOF
|
||||
chmod 0755 "$TMP/bin/logger"
|
||||
PATH="$TMP/bin:$PATH"
|
||||
export PATH
|
||||
|
||||
# 渲染器里的绝对路径统统换成临时目录
|
||||
sed -e "s|/etc/ipsec.conf|$TMP/etc/ipsec.conf|g" \
|
||||
-e "s|/etc/ipsec.secrets|$TMP/etc/ipsec.secrets|g" \
|
||||
-e "s|/usr/lib/ipsec/luci-ipsec-vpnd-deps|$TMP/usr/lib/ipsec/luci-ipsec-vpnd-deps|g" \
|
||||
"$RENDER" > "$TMP/render"
|
||||
chmod 0755 "$TMP/render"
|
||||
|
||||
run_render() {
|
||||
TEST_UCI="$1" IPKG_INSTROOT="$TMP" "$TMP/render" >"$TMP/out" 2>&1
|
||||
echo $?
|
||||
}
|
||||
|
||||
# --- 1. enabled=1 ----------------------------------------------------------
|
||||
cat > "$TMP/uci" <<'EOF'
|
||||
enabled=1
|
||||
clientip=10.9.8.10/24
|
||||
clientdns=10.9.8.1
|
||||
account=acc1
|
||||
password=pw1
|
||||
secret=psk1
|
||||
EOF
|
||||
|
||||
rc="$(run_render "$TMP/uci")"
|
||||
check "enabled=1 时渲染成功" "0" "$rc"
|
||||
check "生成 conn xauth_psk" "1" "$(count_matches '^conn xauth_psk$' "$TMP/etc/ipsec.conf")"
|
||||
check "rightsourceip 取自 UCI" "1" "$(count_matches 'rightsourceip=10.9.8.10/24' "$TMP/etc/ipsec.conf")"
|
||||
check "rightdns 取自 UCI" "1" "$(count_matches 'rightdns=10.9.8.1' "$TMP/etc/ipsec.conf")"
|
||||
check "secrets 含 PSK" "1" "$(count_matches '^: PSK "psk1"$' "$TMP/etc/ipsec.secrets")"
|
||||
check "secrets 含 XAuth" "1" "$(count_matches '^acc1 : XAUTH "pw1"$' "$TMP/etc/ipsec.secrets")"
|
||||
check "secrets 权限 0600" "600" "$(file_mode "$TMP/etc/ipsec.secrets")"
|
||||
check "首次渲染报告 changed" "changed" "$(cat "$TMP/out")"
|
||||
|
||||
# 幂等:同样的配置再渲染一次,报告 unchanged 且不改动文件
|
||||
before="$(cksum "$TMP/etc/ipsec.conf")"
|
||||
rc="$(run_render "$TMP/uci")"
|
||||
check "重复渲染退出码 0" "0" "$rc"
|
||||
check "重复渲染报告 unchanged" "unchanged" "$(cat "$TMP/out")"
|
||||
check "重复渲染不改文件" "$before" "$(cksum "$TMP/etc/ipsec.conf")"
|
||||
|
||||
# --- 2. enabled=0 ----------------------------------------------------------
|
||||
cat > "$TMP/uci" <<'EOF'
|
||||
enabled=0
|
||||
clientip=10.9.8.10/24
|
||||
clientdns=10.9.8.1
|
||||
account=acc1
|
||||
password=pw1
|
||||
secret=psk1
|
||||
EOF
|
||||
|
||||
rc="$(run_render "$TMP/uci")"
|
||||
check "enabled=0 时渲染成功" "0" "$rc"
|
||||
check "enabled=0 时不生成 conn" "0" "$(count_matches '^conn xauth_psk$' "$TMP/etc/ipsec.conf")"
|
||||
check "enabled=0 时清掉遗留 secrets" "0" "$([ -e "$TMP/etc/ipsec.secrets" ] && echo 1 || echo 0)"
|
||||
check "enabled=0 时报告 changed" "changed" "$(cat "$TMP/out")"
|
||||
|
||||
# --- 3. 依赖自检不通过 -----------------------------------------------------
|
||||
cat > "$TMP/uci" <<'EOF'
|
||||
enabled=1
|
||||
clientip=10.9.8.10/24
|
||||
clientdns=10.9.8.1
|
||||
account=acc1
|
||||
password=pw1
|
||||
secret=psk1
|
||||
EOF
|
||||
|
||||
cat > "$TMP/usr/lib/ipsec/luci-ipsec-vpnd-deps" <<'EOF'
|
||||
#!/bin/sh
|
||||
[ "$1" = "files" ] && echo "starter|missing|/usr/lib/ipsec/starter"
|
||||
exit 1
|
||||
EOF
|
||||
chmod 0755 "$TMP/usr/lib/ipsec/luci-ipsec-vpnd-deps"
|
||||
|
||||
rm -f "$TMP/etc/ipsec.conf" "$TMP/etc/ipsec.secrets"
|
||||
rc="$(run_render "$TMP/uci")"
|
||||
check "依赖缺失时退出码非 0" "1" "$([ "$rc" != "0" ] && echo 1 || echo 0)"
|
||||
check "依赖缺失时不生成 ipsec.conf" "0" "$([ -e "$TMP/etc/ipsec.conf" ] && echo 1 || echo 0)"
|
||||
check "依赖缺失时不生成 ipsec.secrets" "0" "$([ -e "$TMP/etc/ipsec.secrets" ] && echo 1 || echo 0)"
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
echo "render-test: 全部通过"
|
||||
exit 0
|
||||
fi
|
||||
echo "render-test: 有失败项"
|
||||
exit 1
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 跑全部离线测试。可在开发机(macOS/Linux)或目标设备(OpenWrt/busybox)上执行:
|
||||
# sh tests/run-all.sh
|
||||
|
||||
set -u
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
|
||||
rc=0
|
||||
for t in tests/sanitize-test.sh tests/render-test.sh tests/sessions-parse-test.sh; do
|
||||
echo "=== $t ==="
|
||||
sh "$t" || rc=1
|
||||
echo
|
||||
done
|
||||
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo "全部测试通过"
|
||||
else
|
||||
echo "有测试失败"
|
||||
fi
|
||||
exit "$rc"
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# luci-ipsec-vpnd-sanitize 的回归测试。
|
||||
#
|
||||
# 这个小工具负责剥掉用户名里的控制字符。它必须在 busybox(OpenWrt 自带)
|
||||
# 上正确工作:busybox 的 `tr` 不认 `[:cntrl:]` 字符类,会把字面字符删掉
|
||||
# (实测 "testuser" 被删成 "esuse"),所以这里逐项验证。
|
||||
#
|
||||
# 用法:sh tests/sanitize-test.sh [脚本路径]
|
||||
|
||||
set -u
|
||||
|
||||
SAN="${1:-root/usr/lib/ipsec/luci-ipsec-vpnd-sanitize}"
|
||||
# 在设备上跑时仓库路径不存在,退回安装后的路径
|
||||
[ -f "$SAN" ] || SAN="/usr/lib/ipsec/luci-ipsec-vpnd-sanitize"
|
||||
[ -f "$SAN" ] || { echo "找不到脚本: $SAN"; exit 1; }
|
||||
|
||||
fail=0
|
||||
check() {
|
||||
if [ "$2" = "$3" ]; then
|
||||
echo "ok - $1"
|
||||
else
|
||||
echo "FAIL - $1"
|
||||
echo " 期望: [$2]"
|
||||
echo " 实际: [$3]"
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
TAB="$(printf '\t')"
|
||||
NL="$(printf '\n')"
|
||||
CR="$(printf '\r')"
|
||||
|
||||
check "普通用户名原样保留" "testuser" "$(printf '%s' 'testuser' | sh "$SAN")"
|
||||
check "制表符被剥掉" "evil" "$(printf '%s' "ev${TAB}il" | sh "$SAN")"
|
||||
check "换行被剥掉" "evil" "$(printf '%s' "ev${NL}il" | sh "$SAN")"
|
||||
check "回车被剥掉" "evil" "$(printf '%s' "ev${CR}il" | sh "$SAN")"
|
||||
check "不能误删普通字母" "testuser" "$(printf '%s' 'testuser' | sh "$SAN")"
|
||||
check "不能误删含 n/t/r/o/l 的名字" "notroll" "$(printf '%s' 'notroll' | sh "$SAN")"
|
||||
check "多个控制字符一起剥掉" "abc" "$(printf '%s' "a${TAB}b${NL}c${CR}" | sh "$SAN")"
|
||||
check "空输入输出为空" "" "$(printf '%s' '' | sh "$SAN")"
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
echo "sanitize-test: 全部通过"
|
||||
exit 0
|
||||
fi
|
||||
echo "sanitize-test: 有失败项"
|
||||
exit 1
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# rpcd 后端 ipsec-vpnd 的解析逻辑回归测试。
|
||||
#
|
||||
# 后端里有两段 awk:
|
||||
# parse_statusall() —— 把 `ipsec statusall` 解析成 TSV
|
||||
# sessions() 里的合并 awk —— 合并历史与在线行、输出 JSON、裁剪历史
|
||||
# 这里把这两段从脚本里抽出来,喂合成数据,验证:
|
||||
# 1. P1-1 控制字符用户名被清洗(不会把 TSV 行撕开、字段错位)
|
||||
# 2. P1-1 旧版本被污染的脏行(字段数不对)被丢弃
|
||||
# 3. P1-3 在线但没有地址时输出 vip_pending 标记
|
||||
# 4. P1-6 历史上限 200 条,超出的被淘汰
|
||||
#
|
||||
# 用法:sh tests/sessions-parse-test.sh [rpcd 脚本路径]
|
||||
|
||||
set -u
|
||||
|
||||
RPCD="${1:-root/usr/libexec/rpcd/ipsec-vpnd}"
|
||||
# 在设备上跑时仓库路径不存在,退回安装后的路径
|
||||
[ -f "$RPCD" ] || RPCD="/usr/libexec/rpcd/ipsec-vpnd"
|
||||
[ -f "$RPCD" ] || { echo "找不到 rpcd 脚本: $RPCD"; exit 1; }
|
||||
|
||||
TMP="$(mktemp -d "${TMPDIR:-/tmp}/sessions-test.XXXXXX")"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
fail=0
|
||||
check() {
|
||||
# check <名称> <期望> <实际>
|
||||
if [ "$2" = "$3" ]; then
|
||||
echo "ok - $1"
|
||||
else
|
||||
echo "FAIL - $1"
|
||||
echo " 期望: [$2]"
|
||||
echo " 实际: [$3]"
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 抽出 parse_statusall() ------------------------------------------------
|
||||
# 用 grep 定位行号再 sed 取区间,避免在 awk 里写带引号的正则
|
||||
fn_start="$(grep -n '^parse_statusall() {' "$RPCD" | head -1 | cut -d: -f1)"
|
||||
fn_end="$(grep -nFx "}'" "$RPCD" | cut -d: -f1 | awk -v s="$fn_start" '$1 > s { print $1; exit }')"
|
||||
sed -n "${fn_start},$((fn_end + 1))p" "$RPCD" > "$TMP/fn.sh"
|
||||
if ! grep -q '^parse_statusall() {' "$TMP/fn.sh"; then
|
||||
echo "FAIL - 没能从 rpcd 脚本里抽出 parse_statusall()"
|
||||
exit 1
|
||||
fi
|
||||
. "$TMP/fn.sh"
|
||||
|
||||
# --- 抽出 sessions() 里的合并 awk -------------------------------------------
|
||||
awk '
|
||||
/-v state="/ && /awk -F/ {g=1}
|
||||
g{print}
|
||||
g && /\$online_tmp"/ {exit}
|
||||
' "$RPCD" > "$TMP/merge.sh"
|
||||
if ! grep -q "awk -F" "$TMP/merge.sh"; then
|
||||
echo "FAIL - 没能从 rpcd 脚本里抽出合并用的 awk"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. 控制字符用户名被清洗 ------------------------------------------------
|
||||
# 用户名 "ev<TAB>il" 由客户端完全可控,不清洗的话 TSV 行会被撕成两段。
|
||||
TAB="$(printf '\t')"
|
||||
STATUS="xauth_psk[3]: ESTABLISHED 10 seconds ago, 192.168.1.139[192.168.1.139]...203.0.113.9[203.0.113.9]
|
||||
xauth_psk[3]: Remote XAuth identity: ev${TAB}il
|
||||
xauth_psk{1}: 192.168.1.139/32 === 10.9.8.10/32"
|
||||
|
||||
out="$(printf '%s\n' "$STATUS" | parse_statusall)"
|
||||
check "控制字符用户名被清洗" "evil|203.0.113.9 evil 203.0.113.9 10.9.8.10 10 seconds" "$out"
|
||||
check "清洗后仍是 5 个字段(key/user/remote/vip/age)" "5" "$(printf '%s\n' "$out" | awk -F'\t' '{print NF}')"
|
||||
|
||||
# --- 2. IKE ID 与 XAuth 用户名相同(无 identity 行)时用 IKE ID 兜底 -------
|
||||
STATUS2="xauth_psk[3]: ESTABLISHED 5 seconds ago, 192.168.1.139[192.168.1.139]...203.0.113.9[alice]
|
||||
xauth_psk{1}: 192.168.1.139/32 === 10.9.8.10/32"
|
||||
out2="$(printf '%s\n' "$STATUS2" | parse_statusall)"
|
||||
check "缺 identity 行时用远端 IKE ID 兜底" "alice|203.0.113.9 alice 203.0.113.9 10.9.8.10 5 seconds" "$out2"
|
||||
|
||||
# --- 3. 合并 awk:脏行丢弃 + vip_pending + 历史上限 ------------------------
|
||||
now=1000000
|
||||
STATE_FILE="$TMP/state"
|
||||
SESSIONS_TMP="$TMP/state.new"
|
||||
online_tmp="$TMP/online"
|
||||
|
||||
# 历史文件里塞一行被污染的脏行(字段数 7,不是一行合法记录)
|
||||
{
|
||||
printf 'good|10.0.0.1\tgood\t10.0.0.1\t10.9.8.10\t10 seconds\t100\n'
|
||||
printf 'ev\til|10.0.0.2\tev\til\t10.0.0.2\t100\t100\n'
|
||||
printf 'bad|10.0.0.3\tbad\t10.0.0.3\t10.9.8.11\t20 seconds\t200\n'
|
||||
} > "$STATE_FILE"
|
||||
|
||||
# 在线行:一个有地址,一个在线但没地址(CHILD_SA 还没装上)
|
||||
{
|
||||
printf 'good|10.0.0.1\tgood\t10.0.0.1\t10.9.8.10\t10 seconds\n'
|
||||
printf 'pending|10.0.0.9\tpending\t10.0.0.9\t\t3 seconds\n'
|
||||
} > "$online_tmp"
|
||||
|
||||
now=2000000
|
||||
STATE_FILE="$STATE_FILE" SESSIONS_TMP="$SESSIONS_TMP" online_tmp="$online_tmp" now="$now" \
|
||||
. "$TMP/merge.sh" > "$TMP/json"
|
||||
|
||||
json="$(cat "$TMP/json")"
|
||||
check "脏行被丢弃" "0" "$(printf '%s\n' "$json" | grep -c '"user":"ev"')"
|
||||
check "合法历史行仍在" "1" "$(printf '%s\n' "$json" | grep -c '"user":"bad"')"
|
||||
check "vip_pending 标记存在" "1" "$(printf '%s\n' "$json" | grep -c '"vip_pending":true')"
|
||||
check "有地址的在线行不带 vip_pending" "1" "$(printf '%s\n' "$json" | grep -c '"vip":"10.9.8.10"')"
|
||||
|
||||
# --- 4. 历史上限 200 --------------------------------------------------------
|
||||
: > "$STATE_FILE"
|
||||
i=1
|
||||
while [ "$i" -le 250 ]; do
|
||||
printf 'u%s|10.1.0.1\tu%s\t10.1.0.1\t10.9.8.20\t1 seconds\t%s\n' "$i" "$i" "$i" >> "$STATE_FILE"
|
||||
i=$((i + 1))
|
||||
done
|
||||
: > "$online_tmp"
|
||||
now=3000000
|
||||
STATE_FILE="$STATE_FILE" SESSIONS_TMP="$SESSIONS_TMP" online_tmp="$online_tmp" now="$now" \
|
||||
. "$TMP/merge.sh" > "$TMP/json"
|
||||
|
||||
check "返回条数被限制到 200" "200" "$(printf '%s\n' "$(cat "$TMP/json")" | grep -o '"user"' | wc -l | tr -d ' ')"
|
||||
check "落盘的历史也被限制到 200" "200" "$(wc -l < "$SESSIONS_TMP" | tr -d ' ')"
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
echo "sessions-parse-test: 全部通过"
|
||||
exit 0
|
||||
fi
|
||||
echo "sessions-parse-test: 有失败项"
|
||||
exit 1
|
||||
Reference in new issue
Block a user