From 3b4244f7de6f51663bb3fcc37cbd6819986b326f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 13:56:25 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Sync=202026-10-06=2013:56:25?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-ipsec-vpnd/.gitignore | 2 + luci-app-ipsec-vpnd/LICENSE | 201 +++++++++ luci-app-ipsec-vpnd/Makefile | 9 + luci-app-ipsec-vpnd/NOTICE | 8 + luci-app-ipsec-vpnd/README.md | 146 +++++++ .../luci-static/resources/view/ipsec-vpnd.js | 116 +++++- .../po/templates/ipsec-vpnd.pot | 85 +++- luci-app-ipsec-vpnd/po/zh_Hans/ipsec-vpnd.po | 95 +++-- .../root/etc/config/ipsec-vpnd | 13 +- .../root/etc/init.d/ipsec-vpnd | 392 ++---------------- .../root/etc/uci-defaults/luci-ipsec-vpnd | 37 +- .../root/usr/lib/ipsec/luci-ipsec-vpnd-updown | 86 ++++ .../root/usr/libexec/rpcd/ipsec-vpnd | 249 +++++++++++ .../share/rpcd/acl.d/luci-app-ipsec-vpnd.json | 5 + 14 files changed, 1034 insertions(+), 410 deletions(-) create mode 100644 luci-app-ipsec-vpnd/.gitignore create mode 100644 luci-app-ipsec-vpnd/LICENSE create mode 100644 luci-app-ipsec-vpnd/NOTICE create mode 100644 luci-app-ipsec-vpnd/README.md create mode 100755 luci-app-ipsec-vpnd/root/usr/lib/ipsec/luci-ipsec-vpnd-updown create mode 100755 luci-app-ipsec-vpnd/root/usr/libexec/rpcd/ipsec-vpnd diff --git a/luci-app-ipsec-vpnd/.gitignore b/luci-app-ipsec-vpnd/.gitignore new file mode 100644 index 000000000..d6e885d97 --- /dev/null +++ b/luci-app-ipsec-vpnd/.gitignore @@ -0,0 +1,2 @@ +*.apk +*.ipk diff --git a/luci-app-ipsec-vpnd/LICENSE b/luci-app-ipsec-vpnd/LICENSE new file mode 100644 index 000000000..f49a4e16e --- /dev/null +++ b/luci-app-ipsec-vpnd/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/luci-app-ipsec-vpnd/Makefile b/luci-app-ipsec-vpnd/Makefile index ce080e8d5..197c24459 100644 --- a/luci-app-ipsec-vpnd/Makefile +++ b/luci-app-ipsec-vpnd/Makefile @@ -1,9 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Original: ImmortalWrt LuCI applications/luci-app-ipsec-vpnd +# Maintained by: szwjp + 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_PKGARCH:=all +PKG_NAME:=luci-app-ipsec-vpnd +PKG_VERSION:=27.906.25.12 +PKG_RELEASE:=2 + define Package/luci-app-ipsec-vpnd/conffiles /etc/config/ipsec-vpnd endef diff --git a/luci-app-ipsec-vpnd/NOTICE b/luci-app-ipsec-vpnd/NOTICE new file mode 100644 index 000000000..2136dbbf0 --- /dev/null +++ b/luci-app-ipsec-vpnd/NOTICE @@ -0,0 +1,8 @@ +LuCI - Lua Configuration Interface +Copyright 2008 Steven Barth +Copyright 2008 Jo-Philipp Wich +Licensed under the Apache License, Version 2.0. + +Contains code from: +coxpcall - Copyright 2005 - Kepler Project (www.keplerproject.org) +ltn12/luasocket - Copyright 2004-2007 Diego Nehab diff --git a/luci-app-ipsec-vpnd/README.md b/luci-app-ipsec-vpnd/README.md new file mode 100644 index 000000000..55a6808a8 --- /dev/null +++ b/luci-app-ipsec-vpnd/README.md @@ -0,0 +1,146 @@ +# luci-app-ipsec-vpnd + +LuCI 界面,给运行在 OpenWrt / ImmortalWrt 上的 strongSwan IPsec VPN 服务做配置管理。 + +IKEv1 + PSK + Xauth(账号密码),目标用户是家用 / 小公司路由器场景——LAN 后面的客户端用用户名密码拨号,拿到一个独立虚拟网段的 IP。 + +> **维护者说明**:本项目维护者本人既在自家路由器上跑,也把编译好的 .apk/.ipk 同步发到 community / 朋友分发渠道。下面“构建”与“安装”分别对应这两种使用方式。 + +--- + +## 功能 + +- **VPN 服务启停**(uhttpd → LuCI → procd 守护进程控制 strongSwan 的 `starter`) +- **账号 / 密码 / PSK 配置** 写在 `/etc/config/ipsec-vpnd`,运行时被 init 脚本翻译成 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 +- **菜单位置**:`VPN → IPSec VPN Server`(order 30) + +## 界面与配置项 + +UCI 路径 `ipsec-vpnd.@ipsec[0]`,字段: + +| 字段 | 默认 | 说明 | +|---|---|---| +| `enabled` | `0` | 总开关 | +| `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 自动算出来)。 + +## 依赖 + +`Makefile` 里 `LUCI_DEPENDS` 列的运行时依赖: + +``` ++strongswan-minimal ++strongswan-mod-xauth-generic ++strongswan-mod-kernel-libipsec ++strongswan-mod-des ++kmod-tun +``` + +**这是 IPsec VPN 服务能起来的前提。**安装本包前请确认这些包在固件里都有,否则 LuCI 界面能开,但服务起不来。 + +### 关于 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` 会卡在依赖解析。 + +可行方案(任选): + +- **换 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 也带上了 + +## 安装 + +### 普通用户(装现成的 .apk) + +从本仓库的 `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 # 可选 +``` + +第一次安装会跑 `uci-defaults/luci-ipsec-vpnd` 一次性初始化(建 network/firewall/zone + 删系统 ipsec 服务),然后 LuCI 刷新一下就能在 `VPN → IPSec VPN Server` 看到。 + +或者从 release 装(GitHub Actions 自动把每次 main 提交 publish 到 `IPSec VPND Latest Build` 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 +``` + +### 自己构建(社区分发 / 自己做 firmware) + +走仓库的 GitHub Actions **Build** workflow:push 到 `main` 触发(或手动 dispatch),会在 GitHub Actions 里产 `luci-app-ipsec-vpnd-*.apk` + `luci-i18n-ipsec-vpnd-zh-cn-*.apk` 两个 apk 和对应的 ipk,并通过 `softprops/action-gh-release` publish 到 latest release。本地要复现的话: + +```sh +# Ubuntu 20.04+,装上 apk-tools / meson / ninja / fakeroot +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 ipk 1 # 出同名 .ipk +ls -l *.apk *.ipk +``` + +`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`)。 + +## CI + +`.github/workflows/` 下两个 workflow: + +- **`build.yml`**(生产 workflow):push 到 main / PR / 手动 dispatch 触发。9 步全绿、零 annotation,~40s 出包并 publish 到 release。是这个项目的命脉。 +- **`cleanup-old-workflow.yml`**(工具 workflow):手动 dispatch,按你给的天数阈值删 `actions/runs` 里的 completed run,防止 Actions 列表太长。文件名带 `.yml` 后缀(之前没有后缀,GitHub Actions 静默忽略——这种坑踩过一次记到 memory 里了)。 + +## 目录结构 + +``` +. +├── Makefile # OpenWrt 包定义:LUCI_TITLE / LUCI_DEPENDS / PKG_* +├── htdocs/luci-static/resources/view/ipsec-vpnd.js +│ # LuCI web UI(cbid 表格 + 启停按钮) +├── 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 +│ ├── etc/uci-defaults/luci-ipsec-vpnd +│ │ # 首次安装一次性:建 network.VPN + firewall +│ ├── 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 +├── LICENSE # Apache-2.0 +└── README.md # 本文件 +``` + +## 维护 + +- 上游:fork 自 ImmortalWrt LuCI `applications/luci-app-ipsec-vpnd` +- 当前 main:自维护分支,修了上游一些配置假设(详见 commit 历史) +- 同步:fix 在 main 上验证后通过 PR 回上游 +- 不要把生成的 `.apk` / `.ipk` 提交到仓库(`.gitignore` 已过滤) + +## 故障排查 + +| 现象 | 排查 | +|---|---| +| LuCI 菜单看不到 IPSec VPN Server | LuCI → 系统 → 软件包,确认 `luci-app-ipsec-vpnd` 已装;刷新 LuCI 缓存(`rm -f /tmp/luci-indexcache`) | +| 服务起不来,日志说 "starter: no such file" | `strongswan-minimal` 没装;装上 | +| 客户端拨不上,日志说 "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 装失败这些 | + +## 许可证 + +Apache-2.0。详见 `LICENSE`。 diff --git a/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js b/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js index 45f0606cc..f55b1294f 100644 --- a/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js +++ b/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js @@ -17,11 +17,34 @@ const callServiceList = rpc.declare({ expect: { '': {} } }); +const callServiceInit = rpc.declare({ + object: 'rc', + method: 'init', + params: ['name', 'action'] +}); + +const callIpsecSessions = rpc.declare({ + object: 'ipsec-vpnd', + method: 'sessions', + expect: { '': {} } +}); + +const callClearHistory = rpc.declare({ + object: 'ipsec-vpnd', + method: 'clear_history', + expect: { '': {} } +}); + function getServiceStatus() { return L.resolveDefault(callServiceList('ipsec-vpnd'), {}).then(function(res) { let isRunning = false; try { - isRunning = res['ipsec-vpnd']['instances']['instance1']['running']; + // 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; }); @@ -43,7 +66,7 @@ return view.extend({ let m, s, o; m = new form.Map('ipsec-vpnd', _('IPSec VPN Server'), - _('IPSec VPN connectivity using the native built-in VPN Client on iOS or Andriod (IKEv1 with PSK and Xauth)')); + _('IPSec VPN connectivity using the native built-in VPN Client on iOS or Android (IKEv1 with PSK and Xauth)')); s = m.section(form.TypedSection); s.anonymous = true; @@ -67,12 +90,12 @@ return view.extend({ o.rmempty = false; o = s.option(form.Value, 'clientip', _('VPN Client IP'), - _('LAN DHCP reserved started IP addresses with the same subnet mask')); + _('Starting IP of the VPN client address pool (CIDR notation). Use a private subnet that does not overlap the LAN, e.g. 10.9.8.10/24')); o.datatype = 'ip4addr'; o.rmempty = false; o = s.option(form.Value, 'clientdns', _('VPN Client DNS'), - _('DNS using in VPN tunnel.Set to the router\'s LAN IP is recommended')); + _('DNS server assigned to VPN clients. Use the VPN gateway (ipsec0) address, e.g. 10.9.8.1')); o.datatype = 'ip4addr'; o.rmempty = false; @@ -87,6 +110,91 @@ return view.extend({ o.password = true; o.rmempty = false; + // 在线客户端列表:页面最底部,5s 轮询刷新。数据来自 rpcd 后端 + // /usr/libexec/rpcd/ipsec-vpnd(解析 `ipsec statusall` 的 stroke 输出)。 + s = m.section(form.TypedSection); + s.anonymous = true; + s.render = function() { + // 用显式 / 渲染:只重建表体,表头固定保留。 + // 不依赖 cbi_update_table 对表头行(tr.table-titles)的处理,更稳。 + let clientBody = E('tbody', { 'id': 'ipsec_clients_body' }); + let clientTable = E('table', { 'class': 'table cbi-section-table', 'id': 'ipsec_clients_table' }, [ + E('thead', {}, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('User')), + E('th', { 'class': 'th' }, _('Remote Address')), + E('th', { 'class': 'th' }, _('Assigned IP')), + E('th', { 'class': 'th' }, _('Duration')), + E('th', { 'class': 'th' }, _('Status')) + ]) + ]), + clientBody + ]); + let hint = E('p', { 'id': 'ipsec_clients_hint' }, _('Collecting data...')); + + let renderRows = function(sessions) { + clientBody.textContent = ''; + sessions.forEach(function(s) { + 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' }, s.age || '-'), + E('td', { 'class': 'td' }, s.online ? _('Connected') : _('Disconnected')) + ])); + }); + }; + + let update = function() { + return L.resolveDefault(callIpsecSessions(), {}).then(function(res) { + let sessions = (res && Array.isArray(res.sessions)) ? res.sessions : null; + let hintEl = document.getElementById('ipsec_clients_hint'); + if (sessions === null) { + if (hintEl) hintEl.textContent = _('Failed to retrieve VPN client information.'); + clientBody.textContent = ''; + return; + } + if (sessions.length === 0) { + if (hintEl) hintEl.textContent = _('No VPN clients are connected.'); + clientBody.textContent = ''; + return; + } + if (hintEl) hintEl.textContent = ''; + renderRows(sessions); + }); + }; + + poll.add(update, 5); + update(); + + let clearBtn = E('button', { + 'class': 'btn cbi-button-remove', + 'click': function(ev) { + ev.preventDefault(); + if (!window.confirm(_('Clear all client history records?'))) + return; + L.resolveDefault(callClearHistory(), {}).then(function() { + return update(); + }); + } + }, [ _('Clear history') ]); + + return E('div', { 'class': 'cbi-section cbi-tblsection' }, [ + E('div', {}, [ E('h3', _('IPSec VPN Clients')), clearBtn ]), + hint, + clientTable + ]); + }; + + // 保存后显式 restart 服务:LuCI 保存走 uci commit,不会触发 procd 的 + // config.change reload trigger(实测 commit/reload_config 均不重启服务), + // 必须显式 rc.init restart 才能让启用/停用即时生效。 + m.save = function(ev) { + return form.Map.prototype.save.call(this).then(function() { + return L.resolveDefault(callServiceInit('ipsec-vpnd', 'restart')); + }); + }; + return m.render(); } }); diff --git a/luci-app-ipsec-vpnd/po/templates/ipsec-vpnd.pot b/luci-app-ipsec-vpnd/po/templates/ipsec-vpnd.pot index c53472de0..4067c94e4 100644 --- a/luci-app-ipsec-vpnd/po/templates/ipsec-vpnd.pot +++ b/luci-app-ipsec-vpnd/po/templates/ipsec-vpnd.pot @@ -1,66 +1,115 @@ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:79 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:96 msgid "Account" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:59 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:116 +msgid "Assigned IP" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:162 +msgid "Clear all client history records?" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:168 +msgid "Clear history" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:76 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:121 msgid "Collecting data..." msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:75 -msgid "DNS using in VPN tunnel.Set to the router's LAN IP is recommended" +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:190 +msgid "Connected" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:65 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:92 +msgid "DNS server assigned to VPN clients. Use the VPN gateway (ipsec0) address, e.g. 10.9.8.1" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:190 +msgid "Disconnected" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:117 +msgid "Duration" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:82 msgid "Enable" msgstr "" +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:145 +msgid "Failed to retrieve VPN client information." +msgstr "" + #: applications/luci-app-ipsec-vpnd/root/usr/share/rpcd/acl.d/luci-app-ipsec-vpnd.json:3 msgid "Grant access to ipsec configuration" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:34 -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:36 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:51 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:53 msgid "IPSec VPN" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:45 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:171 +msgid "IPSec VPN Clients" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:62 #: applications/luci-app-ipsec-vpnd/root/usr/share/luci/menu.d/luci-app-ipsec-vpnd.json:3 msgid "IPSec VPN Server" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:46 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:63 msgid "" "IPSec VPN connectivity using the native built-in VPN Client on iOS or " -"Andriod (IKEv1 with PSK and Xauth)" +"Android (IKEv1 with PSK and Xauth)" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:70 -msgid "LAN DHCP reserved started IP addresses with the same subnet mask" +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:171 +msgid "No VPN clients are connected." msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:36 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:53 msgid "NOT RUNNING" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:82 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:99 msgid "Password" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:34 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:115 +msgid "Remote Address" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:51 msgid "RUNNING" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:86 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:103 msgid "Secret Pre-Shared Key" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:74 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:87 +msgid "Starting IP of the VPN client address pool (CIDR notation). Use a private subnet that does not overlap the LAN, e.g. 10.9.8.10/24" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:118 +msgid "Status" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:114 +msgid "User" +msgstr "" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:91 msgid "VPN Client DNS" msgstr "" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:69 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:86 msgid "VPN Client IP" msgstr "" diff --git a/luci-app-ipsec-vpnd/po/zh_Hans/ipsec-vpnd.po b/luci-app-ipsec-vpnd/po/zh_Hans/ipsec-vpnd.po index 9a0095039..fb0c044c0 100644 --- a/luci-app-ipsec-vpnd/po/zh_Hans/ipsec-vpnd.po +++ b/luci-app-ipsec-vpnd/po/zh_Hans/ipsec-vpnd.po @@ -8,70 +8,117 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Transfer-Encoding: 8bit\n" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:79 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:96 msgid "Account" msgstr "账户" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:59 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:116 +msgid "Assigned IP" +msgstr "分配的 IP" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:162 +msgid "Clear all client history records?" +msgstr "确定要清除所有客户端历史记录吗?" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:168 +msgid "Clear history" +msgstr "清除历史" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:76 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:121 msgid "Collecting data..." msgstr "正在收集数据中..." -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:75 -msgid "DNS using in VPN tunnel.Set to the router's LAN IP is recommended" -msgstr "指定VPN客户端的DNS地址。推荐设置为路由器的LAN IP,例如 192.168.0.1" +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:190 +msgid "Connected" +msgstr "已连接" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:65 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:92 +msgid "DNS server assigned to VPN clients. Use the VPN gateway (ipsec0) address, e.g. 10.9.8.1" +msgstr "分配给 VPN 客户端的 DNS 服务器。使用 VPN 网关(ipsec0)地址,例如 10.9.8.1" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:190 +msgid "Disconnected" +msgstr "已断开" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:117 +msgid "Duration" +msgstr "连接时长" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:82 msgid "Enable" msgstr "启用" +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:145 +msgid "Failed to retrieve VPN client information." +msgstr "获取 VPN 客户端信息失败" + #: applications/luci-app-ipsec-vpnd/root/usr/share/rpcd/acl.d/luci-app-ipsec-vpnd.json:3 msgid "Grant access to ipsec configuration" msgstr "授予访问 ipsec 配置的权限" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:34 -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:36 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:51 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:53 msgid "IPSec VPN" -msgstr "" +msgstr "IPSec VPN" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:45 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:171 +msgid "IPSec VPN Clients" +msgstr "IPSec VPN 在线客户端" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:62 #: applications/luci-app-ipsec-vpnd/root/usr/share/luci/menu.d/luci-app-ipsec-vpnd.json:3 msgid "IPSec VPN Server" msgstr "IPSec VPN 服务器" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:46 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:63 msgid "" "IPSec VPN connectivity using the native built-in VPN Client on iOS or " -"Andriod (IKEv1 with PSK and Xauth)" +"Android (IKEv1 with PSK and Xauth)" msgstr "" -"使用iOS 或者 Andriod (IKEv1 with PSK and Xauth) 原生内置 IPSec VPN 客户端进行" +"使用iOS 或者 Android (IKEv1 with PSK and Xauth) 原生内置 IPSec VPN 客户端进行" "连接" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:70 -msgid "LAN DHCP reserved started IP addresses with the same subnet mask" -msgstr "" -"VPN客户端使用DHCP保留空余IP的起始地址,必须和路由器LAN同一个子网掩码,例如 " -"192.168.0.10/24" +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:171 +msgid "No VPN clients are connected." +msgstr "当前没有 VPN 客户端在线" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:36 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:53 msgid "NOT RUNNING" msgstr "未运行" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:82 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:99 msgid "Password" msgstr "密码" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:34 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:115 +msgid "Remote Address" +msgstr "来源地址" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:51 msgid "RUNNING" msgstr "运行中" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:86 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:103 msgid "Secret Pre-Shared Key" msgstr "PSK密钥" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:74 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:87 +msgid "Starting IP of the VPN client address pool (CIDR notation). Use a private subnet that does not overlap the LAN, e.g. 10.9.8.10/24" +msgstr "VPN 客户端地址池的起始地址(CIDR 记法)。使用与 LAN 不重叠的私网网段,例如 10.9.8.10/24" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:118 +msgid "Status" +msgstr "状态" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:114 +msgid "User" +msgstr "用户" + +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:91 msgid "VPN Client DNS" msgstr "VPN客户端DNS服务器" -#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:69 +#: applications/luci-app-ipsec-vpnd/htdocs/luci-static/resources/view/ipsec-vpnd.js:86 msgid "VPN Client IP" msgstr "VPN客户端地址段" diff --git a/luci-app-ipsec-vpnd/root/etc/config/ipsec-vpnd b/luci-app-ipsec-vpnd/root/etc/config/ipsec-vpnd index 93c21f278..cba1d91f3 100644 --- a/luci-app-ipsec-vpnd/root/etc/config/ipsec-vpnd +++ b/luci-app-ipsec-vpnd/root/etc/config/ipsec-vpnd @@ -1,9 +1,12 @@ config service 'ipsec' - option clientdns '192.168.0.1' - option account 'lean' - option secret 'myopenwrt' +# VPN 网段固定为 10.9.8.0/24(首次安装的 uci-defaults 也按此写入,不再按 LAN 推导)。 +# 若 LAN 恰在 10.9.8.0/24,请手动改 clientip/clientdns。 +# 下面的账号密码仅为占位值,启用服务前请务必修改。 + option clientdns '10.9.8.1' + option account 'vpnuser' + option secret 'changeme' option enabled '0' - option password '12345678' - option clientip '192.168.0.10/24' + option password 'changeme' + option clientip '10.9.8.10/24' diff --git a/luci-app-ipsec-vpnd/root/etc/init.d/ipsec-vpnd b/luci-app-ipsec-vpnd/root/etc/init.d/ipsec-vpnd index 494556363..07c27425a 100755 --- a/luci-app-ipsec-vpnd/root/etc/init.d/ipsec-vpnd +++ b/luci-app-ipsec-vpnd/root/etc/init.d/ipsec-vpnd @@ -13,356 +13,26 @@ IPSEC_SECRETS_FILE=/etc/ipsec.secrets IPSEC_CONN_FILE=/etc/ipsec.conf STRONGSWAN_CONF_FILE=/etc/strongswan.conf -IPSEC_VAR_SECRETS_FILE=/var/ipsec/ipsec.secrets -IPSEC_VAR_CONN_FILE=/var/ipsec/ipsec.conf -STRONGSWAN_VAR_CONF_FILE=/var/ipsec/strongswan.conf - -WAIT_FOR_INTF=0 - -file_reset() { - : > "$1" -} - -xappend() { - local file="$1" - shift - - echo "${@}" >> "${file}" -} - -remove_include() { - local file="$1" - local include="$2" - - sed -i "\_${include}_d" "${file}" -} - -remove_includes() { - remove_include "${IPSEC_CONN_FILE}" "${IPSEC_VAR_CONN_FILE}" - remove_include "${IPSEC_SECRETS_FILE}" "${IPSEC_VAR_SECRETS_FILE}" - remove_include "${STRONGSWAN_CONF_FILE}" "${STRONGSWAN_VAR_CONF_FILE}" -} - -do_include() { - local conf="$1" - local uciconf="$2" - local backup=`mktemp -t -p /tmp/ ipsec-init-XXXXXX` - - [ ! -f "${conf}" ] && rm -rf "${conf}" - touch "${conf}" - - cat "${conf}" | grep -v "${uciconf}" > "${backup}" - mv "${backup}" "${conf}" - xappend "${conf}" "include ${uciconf}" - file_reset "${uciconf}" -} - -ipsec_reset() { - do_include "${IPSEC_CONN_FILE}" "${IPSEC_VAR_CONN_FILE}" -} - -ipsec_xappend() { - xappend "${IPSEC_VAR_CONN_FILE}" "$@" -} - -swan_reset() { - do_include "${STRONGSWAN_CONF_FILE}" "${STRONGSWAN_VAR_CONF_FILE}" -} - -swan_xappend() { - xappend "${STRONGSWAN_VAR_CONF_FILE}" "$@" -} - -secret_reset() { - do_include "${IPSEC_SECRETS_FILE}" "${IPSEC_VAR_SECRETS_FILE}" -} - -secret_xappend() { - xappend "${IPSEC_VAR_SECRETS_FILE}" "$@" -} - -warning() { - echo "WARNING: $@" >&2 -} - -add_crypto_proposal() { - local encryption_algorithm - local hash_algorithm - local dh_group - - config_get encryption_algorithm "$1" encryption_algorithm - config_get hash_algorithm "$1" hash_algorithm - config_get dh_group "$1" dh_group - - [ -n "${encryption_algorithm}" ] && \ - crypto="${crypto:+${crypto},}${encryption_algorithm}${hash_algorithm:+-${hash_algorithm}}${dh_group:+-${dh_group}}" -} - -set_crypto_proposal() { - local conf="$1" - local proposal - - crypto="" - - config_get crypto_proposal "$conf" crypto_proposal "" - for proposal in $crypto_proposal; do - add_crypto_proposal "$proposal" - done - - [ -n "${crypto}" ] && { - local force_crypto_proposal - - config_get_bool force_crypto_proposal "$conf" force_crypto_proposal - - [ "${force_crypto_proposal}" = "1" ] && crypto="${crypto}!" - } - - crypto_proposal="${crypto}" -} - -config_conn() { - # Generic ipsec conn section shared by tunnel and transport - local mode - local local_subnet - local local_nat - local local_sourceip - local local_updown - local local_firewall - local remote_subnet - local remote_sourceip - local remote_updown - local remote_firewall - local ikelifetime - local lifetime - local margintime - local keyingtries - local dpdaction - local dpddelay - local inactivity - local keyexchange - - config_get mode "$1" mode "route" - config_get local_subnet "$1" local_subnet "" - config_get local_nat "$1" local_nat "" - config_get local_sourceip "$1" local_sourceip "" - config_get local_updown "$1" local_updown "" - config_get local_firewall "$1" local_firewall "" - config_get remote_subnet "$1" remote_subnet "" - config_get remote_sourceip "$1" remote_sourceip "" - config_get remote_updown "$1" remote_updown "" - config_get remote_firewall "$1" remote_firewall "" - config_get ikelifetime "$1" ikelifetime "3h" - config_get lifetime "$1" lifetime "1h" - config_get margintime "$1" margintime "9m" - config_get keyingtries "$1" keyingtries "3" - config_get dpdaction "$1" dpdaction "none" - config_get dpddelay "$1" dpddelay "30s" - config_get inactivity "$1" inactivity - config_get keyexchange "$1" keyexchange "ikev2" - - [ -n "$local_nat" ] && local_subnet=$local_nat - - ipsec_xappend "conn $config_name-$1" - ipsec_xappend " left=%any" - ipsec_xappend " right=$remote_gateway" - - [ -n "$local_sourceip" ] && ipsec_xappend " leftsourceip=$local_sourceip" - [ -n "$local_subnet" ] && ipsec_xappend " leftsubnet=$local_subnet" - - [ -n "$local_firewall" ] && ipsec_xappend " leftfirewall=$local_firewall" - [ -n "$remote_firewall" ] && ipsec_xappend " rightfirewall=$remote_firewall" - - ipsec_xappend " ikelifetime=$ikelifetime" - ipsec_xappend " lifetime=$lifetime" - ipsec_xappend " margintime=$margintime" - ipsec_xappend " keyingtries=$keyingtries" - ipsec_xappend " dpdaction=$dpdaction" - ipsec_xappend " dpddelay=$dpddelay" - - [ -n "$inactivity" ] && ipsec_xappend " inactivity=$inactivity" - - if [ "$auth_method" = "psk" ]; then - ipsec_xappend " leftauth=psk" - ipsec_xappend " rightauth=psk" - - [ "$remote_sourceip" != "" ] && ipsec_xappend " rightsourceip=$remote_sourceip" - [ "$remote_subnet" != "" ] && ipsec_xappend " rightsubnet=$remote_subnet" - - ipsec_xappend " auto=$mode" - else - warning "AuthenticationMethod $auth_method not supported" - fi - - [ -n "$local_identifier" ] && ipsec_xappend " leftid=$local_identifier" - [ -n "$remote_identifier" ] && ipsec_xappend " rightid=$remote_identifier" - [ -n "$local_updown" ] && ipsec_xappend " leftupdown=$local_updown" - [ -n "$remote_updown" ] && ipsec_xappend " rightupdown=$remote_updown" - ipsec_xappend " keyexchange=$keyexchange" - - set_crypto_proposal "$1" - [ -n "${crypto_proposal}" ] && ipsec_xappend " esp=$crypto_proposal" - [ -n "${ike_proposal}" ] && ipsec_xappend " ike=$ike_proposal" -} - -config_tunnel() { - config_conn "$1" - - # Specific for the tunnel part - ipsec_xappend " type=tunnel" -} - -config_transport() { - config_conn "$1" - - # Specific for the transport part - ipsec_xappend " type=transport" -} - -config_remote() { - local enabled - local gateway - local pre_shared_key - local auth_method - - config_name=$1 - - config_get_bool enabled "$1" enabled 0 - [ $enabled -eq 0 ] && return - - config_get gateway "$1" gateway - config_get pre_shared_key "$1" pre_shared_key - config_get auth_method "$1" authentication_method - config_get local_identifier "$1" local_identifier "" - config_get remote_identifier "$1" remote_identifier "" - - [ "$gateway" = "any" ] && remote_gateway="%any" || remote_gateway="$gateway" - - [ -z "$local_identifier" ] && { - local ipdest - - [ "$remote_gateway" = "%any" ] && ipdest="1.1.1.1" || ipdest="$remote_gateway" - local_gateway=`ip route get $ipdest | awk -F"src" '/src/{gsub(/ /,"");print $2}'` - } - - [ -n "$local_identifier" ] && secret_xappend -n "$local_identifier " || secret_xappend -n "$local_gateway " - [ -n "$remote_identifier" ] && secret_xappend -n "$remote_identifier " || secret_xappend -n "$remote_gateway " - - secret_xappend ": PSK \"$pre_shared_key\"" - - set_crypto_proposal "$1" - ike_proposal="$crypto_proposal" - - config_list_foreach "$1" tunnel config_tunnel - - config_list_foreach "$1" transport config_transport - - ipsec_xappend "" -} - -config_ipsec() { - local debug - local rtinstall_enabled - local routing_tables_ignored - local routing_table - local routing_table_id - local interface - local device_list - - ipsec_reset - secret_reset - swan_reset - - ipsec_xappend "# generated by /etc/init.d/ipsec-vpnd" - ipsec_xappend "version 2" - ipsec_xappend "" - - secret_xappend "# generated by /etc/init.d/ipsec-vpnd" - - config_get debug "$1" debug 0 - config_get_bool rtinstall_enabled "$1" rtinstall_enabled 1 - [ $rtinstall_enabled -eq 1 ] && install_routes=yes || install_routes=no - - # prepare extra charon config option ignore_routing_tables - for routing_table in $(config_get "$1" "ignore_routing_tables"); do - if [ "$routing_table" -ge 0 ] 2>/dev/null; then - routing_table_id=$routing_table - else - routing_table_id=$(sed -n '/[ \t]*[0-9]\+[ \t]\+'$routing_table'[ \t]*$/s/[ \t]*\([0-9]\+\).*/\1/p' /etc/iproute2/rt_tables) - fi - - [ -n "$routing_table_id" ] && append routing_tables_ignored "$routing_table_id" - done - - local interface_list=$(config_get "$1" "interface") - if [ -z "$interface_list" ]; then - WAIT_FOR_INTF=0 - else - for interface in $interface_list; do - network_get_device device $interface - [ -n "$device" ] && append device_list "$device" "," - done - [ -n "$device_list" ] && WAIT_FOR_INTF=0 || WAIT_FOR_INTF=1 - fi - - swan_xappend "# generated by /etc/init.d/ipsec-vpnd" - swan_xappend "charon {" - swan_xappend " load_modular = yes" - swan_xappend " install_routes = $install_routes" - [ -n "$routing_tables_ignored" ] && swan_xappend " ignore_routing_tables = $routing_tables_ignored" - [ -n "$device_list" ] && swan_xappend " interfaces_use = $device_list" - swan_xappend " plugins {" - swan_xappend " include /etc/strongswan.d/charon/*.conf" - swan_xappend " }" - swan_xappend " syslog {" - swan_xappend " identifier = ipsec" - swan_xappend " daemon {" - swan_xappend " default = $debug" - swan_xappend " }" - swan_xappend " auth {" - swan_xappend " default = $debug" - swan_xappend " }" - swan_xappend " }" - swan_xappend "}" -} - -prepare_env() { - mkdir -p /var/ipsec - remove_includes - config_load ipsec-vpnd - config_foreach config_ipsec ipsec - config_foreach config_remote remote -} - -service_running() { - ipsec status > /dev/null 2>&1 -} - reload_service() { - local bool vt_enabled=`uci get ipsec-vpnd@service[0].enabled 2>/dev/null` - [ "$vt_enabled" = 0 ] && /etc/init.d/ipsec-vpnd stop && return - running && { - prepare_env - [ $WAIT_FOR_INTF -eq 0 ] && { - ipsec rereadall - ipsec reload - return - } + config_load ipsec-vpnd + + local vt_enabled + config_get_bool vt_enabled "ipsec" "enabled" "0" + [ "$vt_enabled" -eq "0" ] && { + stop + return 1 } - [ "$vt_enabled" = 1 ] && start -} -check_ipsec_interface() { - local intf - - for intf in $(config_get "$1" interface); do - procd_add_interface_trigger "interface.*" "$intf" /etc/init.d/ipsec-vpnd reload - done + # 配置变更时重启 procd 实例以重新加载。 + # 不要使用 strongswan 的 ipsec rereadall/reload 命令: + # 它们通过 starter pidfile 操作 charon,会误杀本服务 + # 自己管理的 starter/charon 实例 + stop + start } service_triggers() { procd_add_reload_trigger "ipsec-vpnd" - config_load "ipsec-vpnd" - config_foreach check_ipsec_interface ipsec } start_service() { @@ -370,14 +40,22 @@ start_service() { local vt_enabled config_get_bool vt_enabled "ipsec" "enabled" "0" - [ "$vt_enabled" -eq "0" ] && stop && return 1 + [ "$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" - config_get vt_clientdns "ipsec" "clientdns" - config_get vt_account "ipsec" "account" - config_get vt_password "ipsec" "password" - config_get vt_secret "ipsec" "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 < /etc/ipsec.secrets < /proc/sys/net/ipv4/conf/br-lan/proxy_arp +stop_service() { + # proxy_arp 由 updown 钩子按 ipsec0 接口单独设置,接口在服务停止时随 charon + # 一并移除,无需在此清理。 + return 0 } diff --git a/luci-app-ipsec-vpnd/root/etc/uci-defaults/luci-ipsec-vpnd b/luci-app-ipsec-vpnd/root/etc/uci-defaults/luci-ipsec-vpnd index 274835eb4..bf182a78d 100755 --- a/luci-app-ipsec-vpnd/root/etc/uci-defaults/luci-ipsec-vpnd +++ b/luci-app-ipsec-vpnd/root/etc/uci-defaults/luci-ipsec-vpnd @@ -1,16 +1,33 @@ #!/bin/sh +# 仅首次安装时执行初始化(停用系统 ipsec、重建 network/firewall、删除 +# strongswan 占位配置)。升级或重复执行时直接跳过,避免覆盖用户对网络、 +# 防火墙及 strongswan 的自定义配置。 +# 初始化标记记录在 /etc/config/ipsec-vpnd 的 initialized 选项里:该文件 +# 在 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 + uci -q batch <<-EOF >/dev/null delete firewall.ipsecd commit firewall EOF +# VPN 专用 /24 网段固定为 10.9.8.0/24(与出厂默认配置一致),不再按 LAN 推导。 +# 若你的 LAN 恰好在 10.9.8.0/24,请在初始化后手动改 network.VPN.ipaddr 与 +# ipsec-vpnd.ipsec 的 clientip/clientdns,避免与 LAN 重叠。 +VPN_NET="10.9.8" +VPN_GW="$VPN_NET.1" +VPN_POOL="$VPN_NET.10/24" + uci -q batch <<-EOF >/dev/null delete network.VPN set network.VPN=interface set network.VPN.device="ipsec0" set network.VPN.proto="static" - set network.VPN.ipaddr="192.168.0.1" + set network.VPN.ipaddr="$VPN_GW" set network.VPN.netmask="255.255.255.0" commit network @@ -50,7 +67,7 @@ uci -q batch <<-EOF >/dev/null delete firewall.VPN set firewall.VPN=zone - set firewall.VPN.name="VPN" + set firewall.VPN.name="ipsec-v" set firewall.VPN.input="ACCEPT" set firewall.VPN.forward="ACCEPT" set firewall.VPN.output="ACCEPT" @@ -58,14 +75,24 @@ uci -q batch <<-EOF >/dev/null delete firewall.vpn set firewall.vpn=forwarding - set firewall.vpn.name="vpn" + set firewall.vpn.name="ipsec-v" set firewall.vpn.dest="wan" - set firewall.vpn.src="VPN" + set firewall.vpn.src="ipsec-v" commit firewall + + set ipsec-vpnd.ipsec.clientip="$VPN_POOL" + set ipsec-vpnd.ipsec.clientdns="$VPN_GW" + commit ipsec-vpnd EOF -[ -f "/etc/config/ipsec" ] && mv "/etc/config/ipsec" "/etc/config/ipsec-vpnd" +# strongswan 的 /etc/config/ipsec 只是占位配置,直接删除, +# 避免改名后覆盖本包自带配置(默认 enabled=0) +[ -f "/etc/config/ipsec" ] && rm -f "/etc/config/ipsec" + +# 记录初始化标记,避免升级时重复执行破坏性操作 +uci -q set ipsec-vpnd.ipsec.initialized='1' +uci -q commit ipsec-vpnd rm -f /tmp/luci-indexcache exit 0 diff --git a/luci-app-ipsec-vpnd/root/usr/lib/ipsec/luci-ipsec-vpnd-updown b/luci-app-ipsec-vpnd/root/usr/lib/ipsec/luci-ipsec-vpnd-updown new file mode 100755 index 000000000..933104494 --- /dev/null +++ b/luci-app-ipsec-vpnd/root/usr/lib/ipsec/luci-ipsec-vpnd-updown @@ -0,0 +1,86 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# +# strongSwan updown hook for luci-app-ipsec-vpnd. +# Invoked by strongSwan on every IKEv1 SA up/down event; records the client in +# the router-side history file so the LuCI client list shows it even when the +# page is never opened. Online state is still determined live from +# `ipsec statusall` by the rpcd backend; this hook only guarantees presence. +# +# Documented strongSwan variables used: +# PLUTO_VERB up-client / down-client / up-host / down-host [...] +# PLUTO_XAUTH_ID XAuth username +# PLUTO_PEER peer (client) IP address +# PLUTO_PEER_SOURCEIP virtual IP sent to the client (responder side) +# PLUTO_PEER_CLIENT peer traffic selector (may carry the /32 VIP) +# PLUTO_MY_SOURCEIP fallback virtual IP +# +# State file format (matches the rpcd backend): +# key\tuser\tremote\tvip\tage\tlast_seen (key = "user|remote") + +STATE_FILE="/etc/luci-app-ipsec-vpnd-sessions.state" +# Working copy for this hook; never share this tmp path with the rpcd backend. +UP_TMP="/tmp/.luci-app-ipsec-vpnd-updown.tmp" +LOCK_FILE="/tmp/.luci-app-ipsec-vpnd-sessions.lock" + +# Best-effort advisory lock (same rationale as the rpcd backend). +flock_lock() { + command -v flock >/dev/null 2>&1 || return 0 + exec 9>"$LOCK_FILE" 2>/dev/null || return 0 + flock -w 5 9 2>/dev/null || return 0 +} +flock_unlock() { + command -v flock >/dev/null 2>&1 || return 0 + flock -u 9 2>/dev/null + exec 9>&- 2>/dev/null +} + +[ -n "$PLUTO_VERB" ] || exit 0 + +# 只对 VPN 的 ipsec0 接口开 proxy_arp,不开到所有接口(避免影响 LAN/WAN)。 +# 该接口在 strongSwan 建 SA 时才存在,所以放在这里;服务停止时接口随 charon 移除, +# proxy_arp 也随之消失,无需在此清理。 +[ -d /proc/sys/net/ipv4/conf/ipsec0 ] && echo 1 > /proc/sys/net/ipv4/conf/ipsec0/proxy_arp 2>/dev/null + +now="$(date +%s)" + +user="${PLUTO_XAUTH_ID:-}" +remote="${PLUTO_PEER:-}" + +# assigned client IP: the responder-sent virtual IP, then the /32 selector +vip="${PLUTO_PEER_SOURCEIP%%/*}" +[ -n "$vip" ] || vip="${PLUTO_MY_SOURCEIP%%/*}" +[ -n "$vip" ] || vip="$(printf '%s' "${PLUTO_PEER_CLIENT%%/*}" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1)" +[ -n "$vip" ] || vip="" + +[ -z "$remote" ] && exit 0 +[ -z "$user" ] && user="-" + +key="$user|$remote" + +[ -f "$STATE_FILE" ] || : > "$STATE_FILE" + +# serialize read/merge/write with the rpcd backend +flock_lock + +awk -F'\t' -v key="$key" -v user="$user" -v remote="$remote" -v vip="$vip" -v now="$now" ' +{ + k = $1 + if (k == key) { + # refresh record, keep old vip/age if none supplied + printf "%s\t%s\t%s\t%s\t%s\t%s\n", key, user, remote, + (vip != "" ? vip : $4), $5, now + done = 1 + } else { + print $0 + } +} +END { + if (!done) + printf "%s\t%s\t%s\t%s\t%s\t%s\n", key, user, remote, vip, "", now +}' "$STATE_FILE" > "$UP_TMP" + +[ -s "$UP_TMP" ] && mv "$UP_TMP" "$STATE_FILE" +rm -f "$UP_TMP" +flock_unlock +exit 0 diff --git a/luci-app-ipsec-vpnd/root/usr/libexec/rpcd/ipsec-vpnd b/luci-app-ipsec-vpnd/root/usr/libexec/rpcd/ipsec-vpnd new file mode 100755 index 000000000..957b19ae6 --- /dev/null +++ b/luci-app-ipsec-vpnd/root/usr/libexec/rpcd/ipsec-vpnd @@ -0,0 +1,249 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# +# rpcd backend for luci-app-ipsec-vpnd. +# Exposes one read-only ubus object method "sessions": parse `ipsec statusall` +# (stroke), merge the currently-online IKEv1 SA set with a persisted history of +# seen clients (router-side, survives browser/reboot), and return the combined +# list with an "online" flag per entry. +# +# rpcd plugin protocol: argv[1] = "list" | "call", argv[2] = method name when +# calling; the request JSON arrives on stdin. stdout carries one JSON document. +# +# Persisted state: one line per known client, TAB separated: +# key\tuser\tremote\tvip\tage\tlast_seen (key = "user|remote") + +. /usr/share/libubox/jshn.sh + +STATE_FILE="/etc/luci-app-ipsec-vpnd-sessions.state" +# Working copy for sessions(); kept in /tmp so an unchanged 5s poll doesn't keep +# rewriting the persistent state file on flash. Never share this path with the +# updown hook. +SESSIONS_TMP="/tmp/.luci-app-ipsec-vpnd-sessions.tmp" +LOCK_FILE="/tmp/.luci-app-ipsec-vpnd-sessions.lock" + +# Best-effort advisory lock: serialises the read/merge/write against the updown +# hook. If flock is unavailable each writer still uses its own tmp, so the worst +# case is a lost update in a tiny race window, never a corrupted file. +flock_lock() { + command -v flock >/dev/null 2>&1 || return 0 + exec 9>"$LOCK_FILE" 2>/dev/null || return 0 + flock -w 5 9 2>/dev/null || return 0 +} +flock_unlock() { + command -v flock >/dev/null 2>&1 || return 0 + flock -u 9 2>/dev/null + exec 9>&- 2>/dev/null +} + +list_methods() { + printf '%s\n' '{"sessions":{},"clear_history":{}}' +} + +json_err() { + json_init + json_add_int code 1 + json_add_string error "$1" + json_dump +} + +# Parse `ipsec statusall` from stdin into TAB-separated online rows: +# key\tuser\tremote\tvip\tage +parse_statusall() { + awk ' +function getsa(l) { + if (match(l, /^[ \t]*[A-Za-z0-9_.-]+\[[0-9]+\]:/)) { + s = substr(l, RSTART, RLENGTH - 1) + sub(/^[ \t]*/, "", s) + return s + } + return "" +} +function getts(s) { + if (match(s, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+/)) + return substr(s, RSTART, RLENGTH) + return "" +} +BEGIN { n = 0; last = "" } +{ + line = $0 + + if (match(line, /^[ \t]*[A-Za-z0-9_.-]+\[[0-9]+\]: ESTABLISHED /)) { + sa = getsa(line) + if (!(sa in est)) { + est[sa] = 1 + order[++n] = sa + last = sa + + s = index(line, "ESTABLISHED ") + 12 + e = index(line, " ago,") + age[sa] = (e > s) ? substr(line, s, e - s) : "" + + rest = "" + i = index(line, "...") + if (i > 0) rest = substr(line, i + 3) + ip = "" + if (match(rest, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) + ip = substr(rest, RSTART, RLENGTH) + if (ip == "") { + sub(/^[ \t]*/, "", rest) + j = index(rest, "[") + if (j > 0) rest = substr(rest, 1, j - 1) + ip = rest + } + remote[sa] = ip + } + next + } + + if (line ~ /[Xx][Aa]uth/ && index(line, "identity:") > 0) { + sa = getsa(line) + if (sa != "" && (sa in est) && !(sa in user)) { + v = substr(line, index(line, "identity:") + 9) + gsub(/^[ \t"'"'"']+|[ \t"'"'"']+$/, "", v) + if (v != "") user[sa] = v + } + next + } + + # CHILD_SA traffic selector carrying the assigned /32 client address; + # strongSwan 6.x puts the pool /32 on the RIGHT (remote) side here. + if (index(line, "===") > 0 && line ~ /\/[0-9]+/) { + i = index(line, "===") + l = getts(substr(line, 1, i - 1)) + r = getts(substr(line, i + 3)) + ip = "" + if (r ~ /\/32$/) ip = substr(r, 1, length(r) - 3) + else if (l ~ /\/32$/) ip = substr(l, 1, length(l) - 3) + if (ip != "") { + sa = getsa(line) + if (!(sa in est)) sa = last + if ((sa in est) && !(sa in vip)) vip[sa] = ip + } + next + } +} +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 = "-" + if (r == "") r = "-" + printf "%s|%s\t%s\t%s\t%s\t%s\n", u, r, u, r, v, a + } +}' +} + +sessions() { + local now="$(date +%s)" + 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" + + # current online clients as TSV + /usr/sbin/ipsec statusall 2>/dev/null | parse_statusall > "$online_tmp" + + # 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" ' +function jesc(s) { + gsub(/\\/, "\\\\", s) + gsub(/"/, "\\\"", s) + return s +} +BEGIN { n = 0 } +FILENAME == state { + # history rows: key user remote vip age last_seen + key = $1 + if (key == "") next + 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 + next +} +{ + # online rows: key user remote vip age + key = $1 + if (key == "") next + 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 +} +END { + # build output order: online first (kept), then offline by last_seen desc + nc = 0 + for (i = 1; i <= n; i++) + if (active[order[i]]) out[++nc] = order[i] + oc = 0 + for (i = 1; i <= n; i++) + if (!active[order[i]]) off[++oc] = order[i] + for (i = 1; i <= oc; i++) + for (j = i + 1; j <= oc; j++) + if ((hl[off[j]] + 0) > (hl[off[i]] + 0)) { + t = off[i]; off[i] = off[j]; off[j] = t + } + for (i = 1; i <= oc; i++) out[++nc] = off[i] + + # JSON response + printf "{\"code\":0,\"sessions\":[" + first = 1 + for (i = 1; i <= nc; i++) { + k = out[i] + if (!first) printf "," + first = 0 + 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") + } + printf "]}\n" + + # rewrite persisted history (drop online flag; recomputed each call) + for (i = 1; i <= n; i++) { + k = order[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" + + rc=$? + rm -f "$online_tmp" + # rewrite the persisted history only when it actually changed, so an idle + # 5s poll doesn't keep re-writing the state file on flash. + if [ -s "$SESSIONS_TMP" ] && ! cmp -s "$SESSIONS_TMP" "$STATE_FILE"; then + mv "$SESSIONS_TMP" "$STATE_FILE" + fi + rm -f "$SESSIONS_TMP" + flock_unlock + return $rc +} + +clear_history() { + flock_lock + rm -f "$STATE_FILE" "$SESSIONS_TMP" + flock_unlock + json_init + json_add_int code 0 + json_add_string message "cleared" + json_dump +} + +main() { + case "$1" in + list) + list_methods + ;; + call) + case "$2" in + sessions) sessions ;; + clear_history) clear_history ;; + *) json_err "unknown method" ;; + esac + ;; + *) + printf '%s\n' '{}' + ;; + esac +} + +# source-able for offline parser testing +[ -n "$IPSEC_VPND_SKIP_MAIN" ] || main "$@" diff --git a/luci-app-ipsec-vpnd/root/usr/share/rpcd/acl.d/luci-app-ipsec-vpnd.json b/luci-app-ipsec-vpnd/root/usr/share/rpcd/acl.d/luci-app-ipsec-vpnd.json index efb9c865c..193d5711f 100644 --- a/luci-app-ipsec-vpnd/root/usr/share/rpcd/acl.d/luci-app-ipsec-vpnd.json +++ b/luci-app-ipsec-vpnd/root/usr/share/rpcd/acl.d/luci-app-ipsec-vpnd.json @@ -3,11 +3,16 @@ "description": "Grant access to ipsec configuration", "read": { "ubus": { + "ipsec-vpnd": [ "sessions", "clear_history" ], + "rc": [ "init" ], "service": [ "list" ] }, "uci": [ "ipsec-vpnd" ] }, "write": { + "ubus": { + "rc": [ "init" ] + }, "uci": [ "ipsec-vpnd" ] } }