🚀 Sync 2026-10-06 13:56:25

This commit is contained in:
github-actions[bot] committed 2026-10-06 13:56:25 +08:00
1 parent d63f87a572
commit 3b4244f7de
14 files changed
+1034 -410

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
*.apk
*.ipk
+201
View File
@@ -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.
+9
View File
@@ -1,9 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
#
# Original: ImmortalWrt LuCI applications/luci-app-ipsec-vpnd
# Maintained by: szwjp <szwjp@users.noreply.github.com>
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
+8
View File
@@ -0,0 +1,8 @@
LuCI - Lua Configuration Interface
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
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
+146
View File
@@ -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`。
@@ -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() {
// 用显式 <thead>/<tbody> 渲染:只重建表体,表头固定保留。
// 不依赖 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();
}
});
+67 -18
View File
@@ -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 ""
+71 -24
View File
@@ -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客户端地址段"
@@ -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'
+38 -354
View File
@@ -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 <<EOF
# ipsec.conf - strongSwan IPsec configuration file
@@ -402,18 +80,20 @@ conn xauth_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
prepare_env
[ "$WAIT_FOR_INTF" -eq "1" ] && return 1
umask 022
chmod 600 /etc/ipsec.secrets
procd_open_instance
@@ -428,6 +108,10 @@ EOF
procd_set_param respawn
procd_close_instance
}
echo 1 > /proc/sys/net/ipv4/conf/br-lan/proxy_arp
stop_service() {
# proxy_arp 由 updown 钩子按 ipsec0 接口单独设置,接口在服务停止时随 charon
# 一并移除,无需在此清理。
return 0
}
@@ -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
@@ -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
+249
View File
@@ -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 "$@"
@@ -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" ]
}
}