🍉 Sync 2026-10-06 20:57:57

This commit is contained in:
github-actions[bot] committed 2026-10-06 20:57:57 +08:00
1 parent d42f800eec
commit 6a9b13947b
23 files changed
+991 -507

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-flowproxy
PKG_VERSION:=1.0.3
PKG_RELEASE:=5
PKG_RELEASE:=6
LUCI_TITLE:=LuCI Support for FlowProxy Traffic Diversion
LUCI_DEPENDS:=+luci-base +nftables +ip-full
@@ -189,63 +189,64 @@ return L.view.extend({
s.taboption('settings', form.Flag, 'dns_proxy_enabled', _('Force upstream DNS to proxy server')).rmempty = false;
o = s.taboption('settings', form.Value, 'proxy_server_ip_addr', _('Proxy server IP address'));
o.datatype = 'ip4addr'; o.rmempty = false;
o.datatype = 'ip4addr'; o.rmempty = true;
o.placeholder = '192.168.1.254';
o.validate = function(section_id, value) {
var ipInCidr = function(ipStr, cidrStr) {
if (!cidrStr || cidrStr.indexOf(':') !== -1 || !ipStr || ipStr.indexOf(':') !== -1) return false;
var parts = cidrStr.split('/');
var cidrIp = parts[0];
var maskBits = parseInt(parts[1] || '32', 10);
var ipToNum = function(s) {
var p = s.split('.');
if (p.length !== 4) return 0;
return (((parseInt(p[0], 10) << 24) |
(parseInt(p[1], 10) << 16) |
(parseInt(p[2], 10) << 8) |
parseInt(p[3], 10)) >>> 0);
};
var ipNum = ipToNum(ipStr);
var cidrNum = ipToNum(cidrIp);
if (maskBits === 0) return true;
var mask = (0xffffffff << (32 - maskBits)) >>> 0;
return (ipNum & mask) === (cidrNum & mask);
};
m.save = function() {
var enabled = s.formvalue('global', 'enabled') || uci.get('flowproxy', 'global', 'enabled');
if (enabled === '1') {
var ipVal = s.formvalue('global', 'proxy_server_ip_addr') || uci.get('flowproxy', 'global', 'proxy_server_ip_addr');
var ifaceVal = s.formvalue('global', 'interface') || uci.get('flowproxy', 'global', 'interface');
var selected_interface = s.formvalue(section_id, 'interface');
if (!value || !selected_interface) return true;
if (ipVal && ifaceVal) {
var ipInCidr = function(ipStr, cidrStr) {
if (!cidrStr || cidrStr.indexOf(':') !== -1 || !ipStr || ipStr.indexOf(':') !== -1) return false;
var parts = cidrStr.split('/');
var cidrIp = parts[0];
var maskBits = parseInt(parts[1] || '32', 10);
var ipToNum = function(s) {
var p = s.split('.');
if (p.length !== 4) return 0;
return (((parseInt(p[0], 10) << 24) |
(parseInt(p[1], 10) << 16) |
(parseInt(p[2], 10) << 8) |
parseInt(p[3], 10)) >>> 0);
};
var ipNum = ipToNum(ipStr);
var cidrNum = ipToNum(cidrIp);
if (maskBits === 0) return true;
var mask = (0xffffffff << (32 - maskBits)) >>> 0;
return (ipNum & mask) === (cidrNum & mask);
};
var target_iface = null;
for (var i = 0; i < ifaces.length; i++) {
var dev = ifaces[i].getL3Device() || ifaces[i].getDevice();
if ((dev && dev.getName() === selected_interface) || ifaces[i].getName() === selected_interface) {
target_iface = ifaces[i];
break;
var target_iface = null;
for (var i = 0; i < ifaces.length; i++) {
var dev = ifaces[i].getL3Device() || ifaces[i].getDevice();
if ((dev && dev.getName() === ifaceVal) || ifaces[i].getName() === ifaceVal) {
target_iface = ifaces[i];
break;
}
}
if (target_iface) {
var ips = target_iface.getIPAddrs();
if (ips && ips.length > 0) {
var inSubnet = false;
for (var j = 0; j < ips.length; j++) {
if (ipInCidr(ipVal, ips[j])) {
inSubnet = true;
break;
}
}
if (!inSubnet) {
ui.addNotification(null, E('p', _('The proxy server IP address must be within the subnet of the selected interface (%s).').format(ips.join(', '))), 'danger');
return Promise.reject(new Error('Proxy IP subnet mismatch'));
}
}
}
}
}
if (!target_iface) return true;
var ips = target_iface.getIPAddrs();
if (!ips || ips.length === 0) return true;
var inSubnet = false;
for (var j = 0; j < ips.length; j++) {
if (ipInCidr(value, ips[j])) {
inSubnet = true;
break;
}
}
if (!inSubnet) {
return _('The proxy server IP address must be within the subnet of the selected interface (%s).').format(ips.join(', '));
}
return true;
return form.Map.prototype.save.apply(m, arguments);
};
o = s.taboption('settings', form.Value, 'proxy_server_dns_port', _('Proxy server DNS port'));
+8 -4
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 Lienol <lawlienol@gmail.com>
# Copyright (C) 2018-2026 Lienol <lawlienol@gmail.com>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
@@ -6,13 +6,17 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-ipsec-server
PKG_VERSION:=20260815
PKG_RELEASE:=2
PKG_VERSION:=20260807
PKG_RELEASE:=3
PKG_MAINTAINER:=Lienol <lawlienol@gmail.com>
LUCI_TITLE:=LuCI support for IPSec VPN Server
LUCI_DEPENDS:=+kmod-tun +luci-lib-jsonc +strongswan +strongswan-minimal +strongswan-mod-kernel-libipsec +strongswan-mod-openssl +strongswan-mod-xauth-generic +strongswan-mod-eap-mschapv2 +strongswan-mod-eap-identity +strongswan-mod-pem +strongswan-mod-pkcs1 +strongswan-mod-pkcs8 +strongswan-mod-dhcp +strongswan-mod-farp
LUCI_DEPENDS:=+kmod-tun +luci-lib-jsonc +xl2tpd +strongswan \
+PACKAGE_strongswan:strongswan-mod-kernel-libipsec \
+PACKAGE_strongswan:strongswan-mod-openssl \
+PACKAGE_strongswan:strongswan-mod-xauth-generic \
+(PACKAGE_strongswan-mod-kdf||PACKAGE_strongswan-mod-openssl||PACKAGE_strongswan-mod-wolfssl):strongswan-minimal
LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/conffiles
-26
View File
@@ -1,26 +0,0 @@
conn ikev2-EAP
keyexchange=ikev2
auto=add
fragmentation=yes
forceencaps=yes
left=%defaultroute
leftid=@ns.miaogongzi.cc
leftauth=pubkey
leftcert=full.pem
leftsendcert=always
leftsubnet=0.0.0.0/0
right=%any
rightid=%any
rightauth=eap-mschapv2
eap_identity=%identity
rightsourceip=10.0.20.0/24
rightdns=10.0.0.1
ike=aes256-sha256-modp1024,aes256-sha1-modp1024,aes128-sha256-modp1024,aes128-sha1-modp1024,aes256-sha256-modp2048,aes256-sha1-modp2048
esp=aes256-sha256,aes128-sha256,aes256-sha1,aes128-sha1
dpdaction=clear
dpddelay=30s
rekey=no
compress=no
@@ -11,12 +11,14 @@ function index()
entry({"admin", "vpn", "ipsec-server", "settings"}, cbi("ipsec-server/settings"), _("General Settings"), 10).leaf = true
entry({"admin", "vpn", "ipsec-server", "users"}, cbi("ipsec-server/users"), _("Users Manager"), 20).leaf = true
entry({"admin", "vpn", "ipsec-server", "l2tp_user"}, cbi("ipsec-server/l2tp_user")).leaf = true
entry({"admin", "vpn", "ipsec-server", "online"}, cbi("ipsec-server/online"), _("Online Users"), 30).leaf = true
entry({"admin", "vpn", "ipsec-server", "status"}, call("act_status")).leaf = true
end
function act_status()
local e = {}
e["ipsec_status"] = luci.sys.call("pidof charon >/dev/null 2>&1") == 0
e["ipsec_status"] = luci.sys.call("/usr/bin/pgrep ipsec >/dev/null") == 0
e["l2tp_status"] = luci.sys.call("top -bn1 | grep -v grep | grep '/var/etc/xl2tpd' >/dev/null") == 0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
@@ -1,10 +1,10 @@
local d = require "luci.dispatcher"
local fs = require "nixio.fs"
local sys = require "luci.sys"
m = Map("luci-app-ipsec-server", "L2TP/IPSec PSK " .. translate("Users Manager"))
m.redirect = d.build_url("admin", "vpn", "ipsec-server", "users")
if fs.access("/usr/sbin/xl2tpd") then
if sys.call("command -v xl2tpd > /dev/null") == 0 then
s = m:section(NamedSection, arg[1], "l2tp_users", "")
s.addremove = false
s.anonymous = true
@@ -19,7 +19,6 @@ if fs.access("/usr/sbin/xl2tpd") then
o = s:option(Value, "password", translate("Password"))
o.placeholder = translate("Password")
o.password = true
o.rmempty = false
o = s:option(Value, "ipaddress", translate("IP address"))
@@ -0,0 +1,148 @@
local d = require "luci.dispatcher"
local fs = require "nixio.fs"
local jsonc = require "luci.jsonc"
local sys = require "luci.sys"
local util = require "luci.util"
local uci = require "luci.model.uci".cursor()
f = SimpleForm("processes")
f.reset = false
f.submit = false
local firewall_user_path = "/etc/firewall.user"
if uci:get("luci-app-ipsec-server", "ipsec", "type") ~= "L2TP" then
local sessions = {}
local status_dump = io.popen('ipsec status 2>&1 | grep "ESTABLISHED"')
if status_dump then
local line
for line in status_dump:lines() do
local line = string.split(line, ",")
local s1 = string.split(line[1], " ")
local type_count = s1[1]
local user = line[2]
local user_split = string.split(user, "...")
if #user_split == 2 then
local leftid = string.match(user_split[1], "%[(.-)%]")
local username = string.match(user_split[2], "%[(.-)%]")
local remote_ip = string.split(user_split[2], "[")[1]
local a = {}
for i, v in ipairs(s1) do
if i > 2 then
a[#a + 1] = v
end
end
local login_time = table.concat(a, " ")
sessions[#sessions + 1] = {
username = username,
remote_ip = remote_ip,
login_time = login_time
}
end
end
end
for i, v in ipairs(sessions) do
local ip = util.exec("ipsec leases 2>/dev/null | grep 'online' | grep '%s' | awk '{print $1}'" % { v.username })
v.ip = ip
end
t = f:section(Table, sessions, translate("Online Users"))
t:option(DummyValue, "username", translate("Username") .. "/" .. translate("Identifier"))
t:option(DummyValue, "ip", translate("Client IP"))
t:option(DummyValue, "login_time", translate("Login Time"))
t:option(DummyValue, "remote_ip", translate("IP address"))
add_blacklist = t:option(Button, "add_blacklist", translate("Blacklist"))
function add_blacklist.render(e, t, a)
e.title = translate("Add to Blacklist")
e.inputstyle = "remove"
Button.render(e, t, a)
end
function add_blacklist.write(t, s)
local e = t.map:get(s, "remote_ip")
util.execi("echo 'iptables -I INPUT -s %s -p udp -m multiport --dports 500,4500 -j DROP ## ipsec-blacklist-%s' >> %s" % {e, e, firewall_user_path})
util.execi("iptables -I INPUT -s %s -p udp -m multiport --dports 500,4500 -j DROP" % {e})
luci.http.redirect(d.build_url("admin/vpn/ipsec-server/online"))
end
else
local l2tp_sessions = {}
local l2tp_session_path = "/var/etc/xl2tpd/session"
if fs.access(l2tp_session_path) then
for filename in fs.dir(l2tp_session_path) do
local session_file = l2tp_session_path .. "/" .. filename
local file = io.open(session_file, "r")
local t = jsonc.parse(file:read("*a"))
if t then
t.session_file = session_file
l2tp_sessions[#l2tp_sessions + 1] = t
end
file:close()
end
end
t = f:section(Table, l2tp_sessions, translate("L2TP Online Users"))
t:option(DummyValue, "username", translate("Username"))
t:option(DummyValue, "interface", translate("Interface"))
t:option(DummyValue, "ip", translate("Client IP"))
t:option(DummyValue, "remote_ip", translate("IP address"))
t:option(DummyValue, "login_time", translate("Login Time"))
_blacklist = t:option(Button, "_blacklist", translate("Blacklist"))
function _blacklist.render(e, t, a)
e.title = translate("Add to Blacklist")
e.inputstyle = "remove"
Button.render(e, t, a)
end
function _blacklist.write(t, s)
local e = t.map:get(s, "remote_ip")
util.execi("echo 'iptables -I INPUT -s %s -p udp -m multiport --dports 500,4500,1701 -j DROP ## xl2tpd-blacklist-%s' >> %s" % {e, e, firewall_user_path})
util.execi("iptables -I INPUT -s %s -p udp -m multiport --dports 500,4500,1701 -j DROP" % {e})
util.execi("rm -f " .. t.map:get(s, "session_file"))
null, t.tag_error[s] = sys.process.signal(t.map:get(s, "pid"), 9)
luci.http.redirect(d.build_url("admin/vpn/ipsec-server/online"))
end
_kill = t:option(Button, "_kill", translate("Forced offline"))
_kill.inputstyle = "remove"
function _kill.write(t, s)
util.execi("rm -f " .. t.map:get(s, "session_file"))
null, t.tag_error[t] = sys.process.signal(t.map:get(s, "pid"), 9)
luci.http.redirect(d.build_url("admin/vpn/ipsec-server/online"))
end
end
local blacklist = {}
if fs.access(firewall_user_path) then
for line in io.lines(firewall_user_path) do
local m = line:match('ipsec%-blacklist%-([^\n]+)')
if m then
local t = {}
t.ip = m
blacklist[#blacklist + 1] = t
end
local m = line:match('xl2tpd%-blacklist%-([^\n]+)')
if m then
local t = {}
t.ip = m
blacklist[#blacklist + 1] = t
end
end
end
t = f:section(Table, blacklist, translate("Blacklist"))
t:option(DummyValue, "ip", translate("IP address"))
remove_blacklist = t:option(Button, "remove_blacklist", translate("Blacklist"))
function remove_blacklist.render(e, t, a)
e.title = translate("Remove from Blacklist")
e.inputstyle = "apply"
Button.render(e, t, a)
end
function remove_blacklist.write(t, s)
local e = t.map:get(s, "ip")
util.execi("sed -i -e '/## ipsec-blacklist-%s/d' %s" % {e, firewall_user_path})
util.execi("sed -i -e '/## xl2tpd-blacklist-%s/d' %s" % {e, firewall_user_path})
util.execi("iptables -D INPUT -s %s -p udp -m multiport --dports 500,4500 -j DROP 2>/dev/null" % {e})
util.execi("iptables -D INPUT -s %s -p udp -m multiport --dports 500,4500,1701 -j DROP 2>/dev/null" % {e})
luci.http.redirect(d.build_url("admin/vpn/ipsec-server/online"))
end
return f
@@ -1,3 +1,5 @@
local sys = require "luci.sys"
m = Map("luci-app-ipsec-server", translate("IPSec VPN Server"))
m.template = "ipsec-server/ipsec-server_status"
@@ -7,53 +9,64 @@ s.anonymous = true
o = s:option(DummyValue, "ipsec-server_status", translate("Current Condition"))
o.rawhtml = true
o.cfgvalue = function(t, n)
return '<span class="ipsec-server-status is-loading" role="status" aria-live="polite">' .. translate("Checking...") .. '</span>'
return '<font class="ipsec-server_status"></font>'
end
enabled = s:option(Flag, "enabled", translate("Enable"))
enabled.description = translate("Enable IPsec access for IKEv2 PSK, IKEv1 XAuth PSK, and IKEv2 EAP clients.")
enabled.default = 0
enabled.rmempty = false
o = s:option(Flag, "enabled", translate("Enable"))
o.default = 0
o.rmempty = false
clientip = s:option(Value, "clientip", translate("VPN Client IP"))
clientip.description = translate("VPN client address pool in CIDR notation, such as: 10.0.10.0/24")
clientip.datatype = "ip4addr"
clientip.optional = false
clientip.rmempty = false
clientip.placeholder = "10.0.10.0/24"
type = s:option(ListValue, "type", translate("Type"))
type:value("IKEv2", "IKEv2/IPSec PSK")
type:value("IKEv1", "IPSec Xauth PSK")
type.default = "IKEv2"
serverip = s:option(Value, "serverip", translate("VPN Server IP"))
serverip.description = translate("VPN server address and subnet in CIDR notation, such as: 10.0.0.1/24")
serverip.datatype = "ip4addr"
serverip.optional = false
serverip.rmempty = false
serverip.placeholder = "10.0.0.1/24"
o = s:option(Value, "clientip", translate("VPN Client IP"))
o.description = translate("VPN Client reserved started IP addresses with the same subnet mask, such as: 192.168.100.10/24")
o.datatype = "ip4addr"
o.optional = false
o:depends("type", "IKEv2")
o:depends("type", "IKEv1")
domain = s:option(Value, "domain", translate("VPN Domain"))
domain.description = translate("Domain name used for IKEv2 certificate and Remote ID, such as: ns.miaogongzi.cc")
domain.datatype = "host"
domain.optional = false
domain.rmempty = false
domain.placeholder = "ns.miaogongzi.cc"
domain.default = "ns.miaogongzi.cc"
certs_name = s:option(Value, "certs_name", translate("Certificate File Name"))
certs_name.description = translate("Server certificate file name under /etc/ipsec.d/certs/, such as: server.pem")
certs_name.optional = false
certs_name.rmempty = false
certs_name.placeholder = "server.pem"
certs_name.default = "server.pem"
private_key = s:option(Value, "private_key", translate("Private Key File Name"))
private_key.description = translate("Server private key file name under /etc/ipsec.d/private/, such as: server_key.pem")
private_key.optional = false
private_key.rmempty = false
private_key.placeholder = "server_key.pem"
private_key.default = "server_key.pem"
secret = s:option(Value, "secret", translate("Secret Pre-Shared Key"))
secret.description = translate("Used by IKEv2 PSK and IKEv1 XAuth PSK clients.")
secret = s:option(Value, "secret", translate("Preshared Key"))
secret.default = "ipsec"
secret.password = true
secret.rmempty = false
secret:depends("type", "IKEv1")
if sys.call("command -v xl2tpd > /dev/null") == 0 then
type:value("L2TP", "L2TP/IPSec PSK")
secret:depends("type", "L2TP")
o = s:option(DummyValue, "l2tp_status", "L2TP " .. translate("Current Condition"))
o.rawhtml = true
o.cfgvalue = function(t, n)
return '<font class="l2tp_status"></font>'
end
o:depends("type", "L2TP")
o = s:option(Value, "l2tp_localip", "L2TP " .. translate("Server IP"))
o.description = translate("VPN Server IP address, such as: 192.168.101.1")
o.datatype = "ip4addr"
o.default = "192.168.101.1"
o.placeholder = o.default
o:depends("type", "L2TP")
o = s:option(Value, "l2tp_remoteip", "L2TP " .. translate("Client IP"))
o.description = translate("VPN Client IP address range, such as: 192.168.101.10-20")
o.default = "192.168.101.10-20"
o.placeholder = o.default
o:depends("type", "L2TP")
--[[
if sys.call("ls -L /usr/lib/ipsec/libipsec* 2>/dev/null >/dev/null") == 0 then
o = s:option(DummyValue, "_o", " ")
o.rawhtml = true
o.cfgvalue = function(t, n)
return string.format('<a style="color: red">%s</a>', translate("L2TP/IPSec is not compatible with kernel-libipsec, which will disable this module."))
end
o:depends("type", "L2TP")
end
]]--
end
return m
@@ -1,28 +1,49 @@
local d = require "luci.dispatcher"
local fs = require "nixio.fs"
local sys = require "luci.sys"
m = Map("luci-app-ipsec-server")
s = m:section(TypedSection, "ipsec_users", "IPSec Xauth PSK/EAP " .. translate("Users Manager"))
s.description = translate("Use a client that supports IPSec Xauth PSK or IPSec EAP (iOS, Android, macOS, or Windows) to connect to this server.")
s.addremove = true
s.anonymous = true
s.template = "cbi/tblsection"
local type = m:get("ipsec", "type")
o = s:option(Flag, "enabled", translate("Enabled"))
o.default = 1
o.rmempty = false
if type == "IKEv2" then
s = m:section(TypedSection, "ikev2_users", "IKEv2/IPSec PSK " .. translate("Users Manager"))
s.description = translate("Use a client that supports IKEv2/IPSec PSK (iOS or Android) to connect to this server.")
s.addremove = true
s.anonymous = true
s.template = "cbi/tblsection"
o = s:option(Value, "username", translate("Username"))
o.placeholder = translate("Username")
o.rmempty = false
o = s:option(Flag, "enabled", translate("Enabled"))
o.default = 1
o.rmempty = false
o = s:option(Value, "password", translate("Password"))
o.placeholder = translate("Password")
o.password = true
o.rmempty = false
o = s:option(Value, "identifier", translate("Identifier"))
o.placeholder = translate("Identifier")
o.rmempty = false
if fs.access("/usr/sbin/xl2tpd") then
o = s:option(Value, "secret", translate("Preshared Key"))
o.placeholder = translate("Preshared Key")
o.rmempty = false
end
if type == "IKEv1" then
s = m:section(TypedSection, "ipsec_users", "IPSec Xauth PSK " .. translate("Users Manager"))
s.description = translate("Use a client that supports IPSec Xauth PSK (iOS or Android) to connect to this server.")
s.addremove = true
s.anonymous = true
s.template = "cbi/tblsection"
o = s:option(Flag, "enabled", translate("Enabled"))
o.default = 1
o.rmempty = false
o = s:option(Value, "username", translate("Username"))
o.placeholder = translate("Username")
o.rmempty = false
o = s:option(Value, "password", translate("Password"))
o.placeholder = translate("Password")
o.rmempty = false
end
if type == "L2TP" and sys.call("command -v xl2tpd > /dev/null") == 0 then
s = m:section(TypedSection, "l2tp_users", "L2TP/IPSec PSK " .. translate("Users Manager"))
s.description = translate("Use a client that supports L2TP over IPSec PSK to connect to this server.")
s.addremove = true
@@ -44,7 +65,6 @@ if fs.access("/usr/sbin/xl2tpd") then
o = s:option(Value, "password", translate("Password"))
o.placeholder = translate("Password")
o.password = true
o.rmempty = false
o = s:option(Value, "ipaddress", translate("IP address"))
@@ -1,61 +1,21 @@
<style>
.cbi-map {
padding-bottom: 30px;
}
.ipsec-server-status {
display: inline-flex;
align-items: center;
font-weight: 600;
}
.ipsec-server-status::before {
width: .65em;
height: .65em;
margin-right: .55em;
border-radius: 50%;
background: #6b7280;
content: "";
}
.ipsec-server-status.is-running::before {
background: #198754;
}
.ipsec-server-status.is-stopped::before {
background: #c0392b;
}
</style>
<% include("cbi/map") %>
<script type="text/javascript">//<![CDATA[
(function() {
var status = document.getElementsByClassName('ipsec-server-status')[0];
if (!status) {
return;
}
XHR.poll(5, '<%=luci.dispatcher.build_url("admin", "vpn", "ipsec-server", "status")%>', null,
function(x, result) {
var className;
var label;
if (!result || typeof result.ipsec_status !== 'boolean') {
className = 'ipsec-server-status is-unknown';
label = '<%=translate("Status unavailable")%>';
}
else {
className = 'ipsec-server-status ' + (result.ipsec_status ? 'is-running' : 'is-stopped');
label = result.ipsec_status ? '<%=translate("RUNNING")%>' : '<%=translate("NOT RUNNING")%>';
}
if (status.className !== className) {
status.className = className;
status.textContent = label;
}
XHR.poll(2, '<%=luci.dispatcher.build_url("admin", "vpn", "ipsec-server", "status")%>', null,
function(x, result)
{
var ipsec_status = document.getElementsByClassName('ipsec-server_status')[0];
if (ipsec_status) {
ipsec_status.setAttribute("style","font-weight:bold;");
ipsec_status.setAttribute("color",result.ipsec_status ? "green":"red");
ipsec_status.innerHTML = result.ipsec_status?'<%=translate("RUNNING")%>':'<%=translate("NOT RUNNING")%>';
}
);
})();
var l2tp_status = document.getElementsByClassName('l2tp_status')[0];
if (l2tp_status) {
l2tp_status.setAttribute("style","font-weight:bold;");
l2tp_status.setAttribute("color",result.l2tp_status ? "green":"red");
l2tp_status.innerHTML = result.l2tp_status?'<%=translate("RUNNING")%>':'<%=translate("NOT RUNNING")%>';
}
}
)
//]]>
</script>
+44 -47
View File
@@ -1,60 +1,51 @@
msgid "IPSec VPN Server"
msgstr "IPSec VPN 服务器"
msgid "Enable IPsec access for IKEv2 PSK, IKEv1 XAuth PSK, and IKEv2 EAP clients."
msgstr "启用 IKEv2 PSK、IKEv1 XAuth PSK 和 IKEv2 EAP 客户端接入。"
msgid "Type"
msgstr "类型"
msgid "Use a client that supports IPSec Xauth PSK or IPSec EAP (iOS, Android, macOS, or Windows) to connect to this server."
msgstr "请使用支持 IPSec Xauth PSK 或 IPSec EAP 的客户端(iOS、Android、macOS、Windows)连接到此服务器。"
msgid "Use a client that supports IKEv2/IPSec PSK (iOS or Android) to connect to this server."
msgstr "使用支持 IKEv2/IPSec PSK(iOS 或 Android)的客户端连接到此服务端。"
msgid "Use a client that supports IPSec Xauth PSK (iOS or Android) to connect to this server."
msgstr "使用支持 IPSec Xauth PSK(iOS 或 Android)的客户端连接到此服务端。"
msgid "Use a client that supports L2TP over IPSec PSK to connect to this server."
msgstr "使用支持 L2TP over IPSec PSK 的客户端连接到此服务端。"
msgid "Current Condition"
msgstr "当前状态"
msgid "Checking..."
msgstr "正在检查..."
msgid "Status unavailable"
msgstr "状态不可用"
msgid "General Settings"
msgid "General settings"
msgstr "基本设置"
msgid "Enable"
msgstr "启用"
msgid "Enabled"
msgstr "启用"
msgid "VPN Client IP"
msgstr "VPN 客户端地址池"
msgstr "VPN客户端地址段"
msgid "VPN Server IP"
msgstr "VPN 服务端地址"
msgid "VPN Client reserved started IP addresses with the same subnet mask, such as: 192.168.100.10/24"
msgstr "VPN客户端获取IP的起始地址,例如:192.168.100.10/24"
msgid "VPN client address pool in CIDR notation, such as: 10.0.10.0/24"
msgstr "VPN 客户端地址池(CIDR 格式),例如:10.0.10.0/24"
msgid "Preshared Key"
msgstr "预共享密钥"
msgid "VPN server address and subnet in CIDR notation, such as: 10.0.0.1/24"
msgstr "VPN 服务端地址和子网(CIDR 格式),例如:10.0.0.1/24"
msgid "VPN Server IP address, such as: 192.168.101.1"
msgstr "VPN服务端IP地址,例如:192.168.101.1"
msgid "Domain name used for IKEv2 certificate and Remote ID, such as: ns.miaogongzi.cc"
msgstr "用于 IKEv2 证书和远程 ID 的域名,例如:ns.miaogongzi.cc"
msgid "VPN Client IP address range, such as: 192.168.101.10-20"
msgstr "VPN客户端获取IP范围,例如:192.168.101.10-20"
msgid "Server private key file name under /etc/ipsec.d/private/, such as: server_key.pem"
msgstr "位于 /etc/ipsec.d/private/ 的服务器私钥文件名,例如:server_key.pem"
msgid "Server certificate file name under /etc/ipsec.d/certs/, such as: server.pem"
msgstr "位于 /etc/ipsec.d/certs/ 的服务器证书文件名,例如:server.pem"
msgid "Secret Pre-Shared Key"
msgstr "PSK 密钥"
msgid "Used by IKEv2 PSK and IKEv1 XAuth PSK clients."
msgstr "供 IKEv2 PSK 和 IKEv1 XAuth PSK 客户端使用。"
msgid "L2TP/IPSec is not compatible with kernel-libipsec, which will disable this module."
msgstr "L2TP/IPSec不兼容kernel-libipsec,开启将会禁用此模块。"
msgid "Users Manager"
msgstr "用户管理"
msgid "Identifier"
msgstr "标识符"
msgid "Username"
msgstr "用户名"
@@ -67,23 +58,29 @@ msgstr "IP 地址"
msgid "Automatically"
msgstr "自动分配"
msgid "Static Routes"
msgstr "静态路由"
msgid "Online Users"
msgstr "在线用户"
msgid "Use a client that supports L2TP over IPSec PSK to connect to this server."
msgstr "使用支持 L2TP over IPSec PSK 的客户端连接到此服务器。"
msgid "L2TP Online Users"
msgstr "L2TP 在线用户"
msgid "Login Time"
msgstr "登录时间"
msgid "Blacklist"
msgstr "黑名单"
msgid "Add to Blacklist"
msgstr "加入黑名单"
msgid "Remove from Blacklist"
msgstr "移出黑名单"
msgid "Forced offline"
msgstr "强制下线"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "RUNNING"
msgstr "运行中"
msgid "Certificate File Name"
msgstr "证书文件名"
msgid "Private Key File Name"
msgstr "私钥文件名"
msgid "VPN Domain"
msgstr "VPN 域名"
@@ -1,10 +1,6 @@
config service 'ipsec'
option enabled '0'
option secret 'ipsec'
option clientip '10.0.10.0/24'
option serverip '10.0.0.1/24'
option certs_name 'server.pem'
option private_key 'server_key.pem'
option domain 'ns.miaogongzi.cc'
option type 'IKEv2'
option clientip '192.168.100.10/24'
@@ -12,15 +12,13 @@ L2TP_CONFIG_FILE=${L2TP_PATH}/xl2tpd.conf
L2TP_OPTIONS_FILE=${L2TP_PATH}/options.xl2tpd
L2TP_LOG_FILE=${L2TP_PATH}/xl2tpd.log
vt_clientip=$(uci -q get ${CONFIG}.@service[0].clientip)
vt_serverip=$(uci -q get ${CONFIG}.@service[0].serverip)
certs_name=$(uci -q get ${CONFIG}.@service[0].certs_name)
private_key=$(uci -q get ${CONFIG}.@service[0].private_key)
vpn_domain=$(uci -q get ${CONFIG}.@service[0].domain)
l2tp_enabled=$(uci -q get ${CONFIG}.@service[0].l2tp_enable)
l2tp_localip=$(uci -q get ${CONFIG}.@service[0].l2tp_localip)
ipt_comment="IPSec VPN Server"
ipt_flag="IPSec VPN Server"
init_config() {
vt_type=$(uci -q get ${CONFIG}.@service[0].type)
vt_clientip=$(uci -q get ${CONFIG}.@service[0].clientip)
l2tp_localip=$(uci -q get ${CONFIG}.@service[0].l2tp_localip)
}
get_enabled_anonymous_secs() {
uci -q show "${CONFIG}" | grep "${1}\[.*\.enabled='1'" | cut -d '.' -sf2
@@ -28,29 +26,73 @@ get_enabled_anonymous_secs() {
ipt_rule() {
if [ "$1" = "add" ]; then
iptables -t nat -I POSTROUTING -s ${vt_serverip} -m comment --comment "${ipt_flag}" -j MASQUERADE 2>/dev/null
iptables -I forwarding_rule -s ${vt_serverip} -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
iptables -I forwarding_rule -m policy --dir in --pol ipsec --proto esp -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
iptables -I forwarding_rule -m policy --dir out --pol ipsec --proto esp -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
iptables -I INPUT -p udp -m multiport --dports 500,4500 -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
iptables -t mangle -I OUTPUT -p udp -m multiport --sports 500,4500 -m comment --comment "${ipt_flag}" -j RETURN 2>/dev/null
[ "${l2tp_enabled}" = 1 ] && {
iptables -t nat -I POSTROUTING -s ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_flag}" -j MASQUERADE 2>/dev/null
iptables -I forwarding_rule -s ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
iptables -I INPUT -p udp --dport 1701 -m comment --comment "${ipt_flag}" -j ACCEPT 2>/dev/null
iptables -t mangle -I OUTPUT -p udp --sport 1701 -m comment --comment "${ipt_flag}" -j RETURN 2>/dev/null
}
iptables -w -t mangle -I OUTPUT -p udp -m multiport --sports 500,4500 -m comment --comment "${ipt_comment}" -j RETURN 2>/dev/null
iptables -w -I INPUT -p udp -m multiport --dports 500,4500 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I FORWARD -m policy --dir in --pol ipsec --proto esp -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I FORWARD -m policy --dir out --pol ipsec --proto esp -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
ip6tables -w -t mangle -I OUTPUT -p udp -m multiport --sports 500,4500 -m comment --comment "${ipt_comment}" -j RETURN 2>/dev/null
ip6tables -w -I INPUT -p udp -m multiport --dports 500,4500 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
ip6tables -w -I FORWARD -m policy --dir in --pol ipsec --proto esp -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
ip6tables -w -I FORWARD -m policy --dir out --pol ipsec --proto esp -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
if [ "${vt_type}" = "L2TP" ]; then
iptables -w -t mangle -I OUTPUT -p udp --sport 1701 -m comment --comment "${ipt_comment}" -j RETURN 2>/dev/null
iptables -w -I INPUT -p udp --dport 1701 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I INPUT -s ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I INPUT -d ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I FORWARD -s ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I FORWARD -d ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I OUTPUT -s ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I OUTPUT -d ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -t nat -I POSTROUTING -s ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j FULLCONENAT 2>/dev/null
[ "$?" != "0" ] && iptables -w -t nat -I POSTROUTING -s ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j MASQUERADE 2>/dev/null
iptables -w -t nat -I POSTROUTING -d ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j FULLCONENAT 2>/dev/null
[ "$?" != "0" ] && iptables -w -t nat -I POSTROUTING -d ${l2tp_localip%.*}.0/24 -m comment --comment "${ipt_comment}" -j MASQUERADE 2>/dev/null
else
iptables -w -I INPUT -s ${vt_clientip} -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I INPUT -d ${vt_clientip} -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I FORWARD -s ${vt_clientip} -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I FORWARD -d ${vt_clientip} -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I OUTPUT -s ${vt_clientip} -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -I OUTPUT -d ${vt_clientip} -m comment --comment "${ipt_comment}" -j ACCEPT 2>/dev/null
iptables -w -t nat -I POSTROUTING -s ${vt_clientip} -m comment --comment "${ipt_comment}" -j FULLCONENAT 2>/dev/null
[ "$?" != "0" ] && iptables -w -t nat -I POSTROUTING -s ${vt_clientip} -m comment --comment "${ipt_comment}" -j MASQUERADE 2>/dev/null
iptables -w -t nat -I POSTROUTING -d ${vt_clientip} -m comment --comment "${ipt_comment}" -j FULLCONENAT 2>/dev/null
[ "$?" != "0" ] && iptables -w -t nat -I POSTROUTING -d ${vt_clientip} -m comment --comment "${ipt_comment}" -j MASQUERADE 2>/dev/null
uci -q batch <<-EOF >/dev/null
delete network.ipsec_server
set network.ipsec_server=interface
set network.ipsec_server.device="ipsec0"
set network.ipsec_server.device="ipsec0"
set network.ipsec_server.proto="static"
set network.ipsec_server.ipaddr="${vt_clientip%.*}.1"
set network.ipsec_server.netmask="255.255.255.0"
commit network
EOF
fi
else
ipt_del() {
for i in $(seq 1 $($1 -nL $2 | grep -c "${ipt_flag}")); do
local index=$($1 --line-number -nL $2 | grep "${ipt_flag}" | head -1 | awk '{print $1}')
for i in $(seq 1 $($1 -nL $2 | grep -c "${ipt_comment}")); do
local index=$($1 --line-number -nL $2 | grep "${ipt_comment}" | head -1 | awk '{print $1}')
$1 -w -D $2 $index 2>/dev/null
done
}
ipt_del "iptables" "forwarding_rule"
ipt_del "iptables" "INPUT"
ipt_del "iptables -t nat" "POSTROUTING"
ipt_del "iptables -t mangle" "OUTPUT"
ipt_del "iptables -w" "INPUT"
ipt_del "iptables -w" "FORWARD"
ipt_del "iptables -w" "OUTPUT"
ipt_del "iptables -w -t nat" "POSTROUTING"
ipt_del "iptables -w -t mangle" "OUTPUT"
ipt_del "ip6tables -w" "INPUT"
ipt_del "ip6tables -w" "FORWARD"
ipt_del "ip6tables -w -t mangle" "OUTPUT"
uci -q batch <<-EOF >/dev/null
delete network.ipsec_server
commit network
EOF
ifup ipsec_server > /dev/null 2>&1
fi
}
@@ -58,50 +100,155 @@ gen_include() {
echo '#!/bin/sh' > /var/etc/ipsecvpn.include
extract_rules() {
echo "*$1"
iptables-save -t $1 | grep "${ipt_flag}" | \
iptables-save -t $1 | grep "${ipt_comment}" | \
sed -e "s/^-A \(INPUT\)/-I \1 1/"
echo 'COMMIT'
}
extract_rules6() {
echo "*$1"
ip6tables-save -t $1 | grep "${ipt_comment}" | \
sed -e "s/^-A \(INPUT\)/-I \1 1/"
echo 'COMMIT'
}
cat <<-EOF >> /var/etc/ipsecvpn.include
iptables-save -c | grep -v "${ipt_flag}" | iptables-restore -c
iptables-save -c | grep -v "${ipt_comment}" | iptables-restore -c
iptables-restore -n <<-EOT
$(extract_rules filter)
$(extract_rules nat)
$(extract_rules mangle)
EOT
ip6tables-save -c | grep -v "${ipt_comment}" | ip6tables-restore -c
ip6tables-restore -n <<-EOT
$(extract_rules6 filter)
$(extract_rules6 mangle)
EOT
exit 0
EOF
return 0
}
start() {
init_config
local vt_enabled=$(uci -q get ${CONFIG}.@service[0].enabled)
[ "$vt_enabled" = 1 ] || return 0
local vt_gateway="${vt_serverip%.*}.1"
local vt_secret=$(uci -q get ${CONFIG}.@service[0].secret)
local l2tp_enabled=$(uci -q get ${CONFIG}.@service[0].l2tp_enable)
[ "${l2tp_enabled}" = 1 ] && {
touch ${CHAP_SECRETS}
local vt_remoteip=$(uci -q get ${CONFIG}.@service[0].l2tp_remoteip)
local ipsec_l2tp_config=$(cat <<-EOF
#######################################
# L2TP Connections
#######################################
[ "$vt_enabled" = 0 ] && return 1
conn L2TP-IKEv1-PSK
type=transport
keyexchange=ikev1
authby=secret
leftprotoport=udp/l2tp
left=%any
right=%any
cat > ${IPSEC_CONN_FILE} <<-EOF
# ipsec.conf - strongSwan IPsec configuration file
config setup
uniqueids=no
charondebug="cfg 2, dmn 2, ike 2, net 0"
conn %default
dpdaction=clear
dpddelay=300s
rekey=no
forceencaps=yes
left=%any
leftfirewall=yes
right=%any
ikelifetime=60m
keylife=20m
rekeymargin=3m
keyingtries=1
auto=add
ike=aes128-sha1-modp2048,aes128-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1536
esp=aes128-sha1,3des-sha1
esp=aes128-sha1,aes256-sha256,3des-sha1
EOF
cat > ${IPSEC_SECRETS_FILE} <<-EOF
# /etc/ipsec.secrets - strongSwan IPsec secrets file
EOF
([ "${vt_type}" = "IKEv1" ] || [ "${vt_type}" = "IKEv2" ]) && {
local vt_gateway="${vt_clientip%.*}.1"
cat >> ${IPSEC_CONN_FILE} <<-EOF
conn conn-ipsec
leftsubnet=0.0.0.0/0
rightsubnet=${vt_clientip}
rightsourceip=${vt_clientip}
rightdns=${vt_gateway}
EOF
)
}
[ "${vt_type}" = "IKEv2" ] && {
cat >> ${IPSEC_CONN_FILE} <<-EOF
conn IKEv2-PSK-XAuth
also=conn-ipsec
keyexchange=ikev2
leftauth=psk
rightauth=psk
EOF
local ikev2_users=$(get_enabled_anonymous_secs "@ikev2_users")
[ -n "${ikev2_users}" ] && {
for _user in ${ikev2_users}; do
local u_enabled=$(uci -q get ${CONFIG}.${_user}.enabled)
[ "${u_enabled}" -eq 1 ] || continue
local u_identifier=$(uci -q get ${CONFIG}.${_user}.identifier)
[ -n "${u_identifier}" ] || continue
local u_secret=$(uci -q get ${CONFIG}.${_user}.secret)
[ -n "${u_secret}" ] || continue
echo "${u_identifier} : PSK '${u_secret}'" >> ${IPSEC_SECRETS_FILE}
done
}
}
([ "${vt_type}" = "IKEv1" ] || [ "${vt_type}" = "L2TP" ]) && {
cat >> ${IPSEC_CONN_FILE} <<-EOF
conn IKEv1-PSK-XAuth
keyexchange=ikev1
EOF
local vt_secret=$(uci -q get ${CONFIG}.@service[0].secret)
cat >> ${IPSEC_SECRETS_FILE} <<-EOF
: PSK "$vt_secret"
EOF
}
[ "${vt_type}" = "IKEv1" ] && {
cat >> ${IPSEC_CONN_FILE} <<-EOF
also=conn-ipsec
leftauth=psk
rightauth=psk
rightauth2=xauth
EOF
local ipsec_users=$(get_enabled_anonymous_secs "@ipsec_users")
[ -n "${ipsec_users}" ] && {
for _user in ${ipsec_users}; do
local u_enabled=$(uci -q get ${CONFIG}.${_user}.enabled)
[ "${u_enabled}" -eq 1 ] || continue
local u_username=$(uci -q get ${CONFIG}.${_user}.username)
[ -n "${u_username}" ] || continue
local u_password=$(uci -q get ${CONFIG}.${_user}.password)
[ -n "${u_password}" ] || continue
echo "${u_username} : XAUTH '${u_password}'" >> ${IPSEC_SECRETS_FILE}
done
}
}
[ "${vt_type}" = "L2TP" ] && {
cat >> ${IPSEC_CONN_FILE} <<-EOF
conn L2TP-IKEv1-PSK
type=transport
keyexchange=ikev1
authby=secret
leftprotoport=udp/1701
left=%any
right=%any
rekey=no
forceencaps=yes
EOF
mkdir -p ${L2TP_PATH}
cat > ${L2TP_OPTIONS_FILE} <<-EOF
name "l2tp-server"
@@ -117,6 +264,7 @@ start() {
lcp-echo-interval 60
connect-delay 5000
EOF
local l2tp_remoteip=$(uci -q get ${CONFIG}.@service[0].l2tp_remoteip)
cat > ${L2TP_CONFIG_FILE} <<-EOF
[global]
port = 1701
@@ -125,7 +273,7 @@ start() {
;debug state = yes
;debug tunnel = yes
[lns default]
ip range = ${vt_remoteip}
ip range = ${l2tp_remoteip}
local ip = ${l2tp_localip}
require chap = yes
refuse pap = yes
@@ -135,192 +283,49 @@ start() {
pppoptfile = ${L2TP_OPTIONS_FILE}
length bit = yes
EOF
touch ${CHAP_SECRETS}
local l2tp_users=$(get_enabled_anonymous_secs "@l2tp_users")
[ -n "${l2tp_users}" ] && {
for _user in ${l2tp_users}; do
local u_enabled=$(uci -q get ${CONFIG}.${_user}.enabled)
[ "${u_enabled}" -eq 1 ] || continue
local u_username=$(uci -q get ${CONFIG}.${_user}.username)
[ -n "${u_username}" ] || continue
local u_password=$(uci -q get ${CONFIG}.${_user}.password)
[ -n "${u_password}" ] || continue
local u_ipaddress=$(uci -q get ${CONFIG}.${_user}.ipaddress)
[ -n "${u_ipaddress}" ] || u_ipaddress="*"
echo "${u_username} l2tp-server ${u_password} ${u_ipaddress}" >> ${CHAP_SECRETS}
done
}
unset user
echo "ip-up-script /usr/share/xl2tpd/ip-up" >> ${L2TP_OPTIONS_FILE}
echo "ip-down-script /usr/share/xl2tpd/ip-down" >> ${L2TP_OPTIONS_FILE}
xl2tpd -c ${L2TP_CONFIG_FILE} -C ${L2TP_CONTROL_FILE} -D >${L2TP_LOG_FILE} 2>&1 &
rm -f "/usr/lib/ipsec/libipsec.so.0"
}
cat > ${IPSEC_CONN_FILE} <<-EOF
# ipsec.conf - strongSwan IPsec configuration file
config setup
uniqueids=no
charondebug="cfg 2, dmn 2, ike 2, net 0"
conn %default
dpdaction=clear
dpddelay=300s
rekey=no
left=%defaultroute
leftfirewall=yes
right=%any
ikelifetime=60m
keylife=20m
rekeymargin=3m
keyingtries=1
auto=add
#######################################
# Default non L2TP Connections
#######################################
conn Non-L2TP
leftsubnet=0.0.0.0/0
rightsubnet=${vt_serverip}
rightsourceip=${vt_serverip}
rightdns=${vt_gateway}
ike=aes128-sha1-modp2048,aes128-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1536
esp=aes128-sha1,3des-sha1
# Cisco IPSec
conn IKEv1-PSK-XAuth
also=Non-L2TP
keyexchange=ikev1
leftauth=psk
rightauth=psk
rightauth2=xauth
conn IKEv2-PSK
keyexchange=ikev2
auto=add
left=%defaultroute
leftauth=psk
leftsubnet=0.0.0.0/0
right=%any
rightauth=psk
rightsourceip=${vt_clientip}
ike=aes128-sha1-modp2048,aes128-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1536
esp=aes128-sha1,3des-sha1
dpdaction=clear
dpddelay=30s
rekey=no
compress=no
conn ikev2-EAP
keyexchange=ikev2
auto=add
fragmentation=yes
forceencaps=yes
left=%defaultroute
leftid=@$vpn_domain
leftauth=pubkey
leftcert=${certs_name}
leftsendcert=always
leftsubnet=0.0.0.0/0
right=%any
rightid=%any
rightauth=eap-mschapv2
eap_identity=%identity
rightsourceip=${vt_clientip}
rightdns=10.0.0.1
ike=aes256-sha256-modp1024,aes256-sha1-modp1024,aes128-sha256-modp1024,aes128-sha1-modp1024,aes256-sha256-modp2048,aes256-sha1-modp2048
esp=aes256-sha256,aes128-sha256,aes256-sha1,aes128-sha1
dpdaction=clear
dpddelay=30s
rekey=no
compress=no
$ipsec_l2tp_config
EOF
cat > /etc/ipsec.secrets <<-EOF
# /etc/ipsec.secrets - strongSwan IPsec secrets file
: PSK "$vt_secret"
: RSA /etc/ipsec.d/private/$private_key
EOF
local ipsec_users=$(get_enabled_anonymous_secs "@ipsec_users")
[ -n "${ipsec_users}" ] && {
for _user in ${ipsec_users}; do
local u_enabled=$(uci -q get ${CONFIG}.${_user}.enabled)
[ "${u_enabled}" -eq 1 ] || continue
local u_username=$(uci -q get ${CONFIG}.${_user}.username)
[ -n "${u_username}" ] || continue
local u_password=$(uci -q get ${CONFIG}.${_user}.password)
[ -n "${u_password}" ] || continue
echo "${u_username} : XAUTH '${u_password}'" >> ${IPSEC_SECRETS_FILE}
echo "${u_username} : EAP '${u_password}'" >> ${IPSEC_SECRETS_FILE}
done
}
unset user
ipt_rule add
/usr/lib/ipsec/starter --daemon charon --nofork > /dev/null 2>&1 &
gen_include
uci -q batch <<-EOF >/dev/null
set network.ipsec_server.ipaddr="${vt_serverip%.*}.1"
commit network
EOF
ifup ipsec_server > /dev/null 2>&1
}
stop() {
init_config
ifdown ipsec_server > /dev/null 2>&1
sed -i '/l2tp-server/d' ${CHAP_SECRETS} 2>/dev/null
top -bn1 | grep "${L2TP_PATH}" | grep -v "grep" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
rm -rf ${L2TP_PATH}
ps -w | grep "/usr/lib/ipsec" | grep -v "grep" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
busybox ps -w | grep "/usr/lib/ipsec" | grep -v "grep" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
ipt_rule del
rm -rf /var/etc/ipsecvpn.include
ln -s "libipsec.so.0.0.0" "/usr/lib/ipsec/libipsec.so.0" >/dev/null 2>&1
}
gen_iface_and_firewall() {
uci -q batch <<-EOF >/dev/null
delete network.ipsec_server
set network.ipsec_server=interface
set network.ipsec_server.device="ipsec0"
set network.ipsec_server.device="ipsec0"
set network.ipsec_server.proto="static"
set network.ipsec_server.ipaddr="${vt_serverip%.*}.1"
set network.ipsec_server.netmask="255.255.255.0"
commit network
delete firewall.ipsecserver
set firewall.ipsecserver=zone
set firewall.ipsecserver.name="ipsecserver"
set firewall.ipsecserver.input="ACCEPT"
set firewall.ipsecserver.forward="ACCEPT"
set firewall.ipsecserver.output="ACCEPT"
set firewall.ipsecserver.network="ipsec_server"
commit firewall
EOF
}
if [ -z "$(uci -q get network.ipsec_server)" ] || [ -z "$(uci -q get firewall.ipsecserver)" ]; then
gen_iface_and_firewall
fi
+7 -17
View File
@@ -1,29 +1,19 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-luci-app-ipsec-server.json" ] && {
cat > /usr/share/ucitrack/luci-app-luci-app-ipsec-server.json << EEOF
{
"config": "luci-app-ipsec-server",
"init": "luci-app-ipsec-server"
}
EEOF
}
rm -f /usr/share/ucitrack/luci-app-luci-app-ipsec-server.json
uci -q batch <<-EOF >/dev/null
delete firewall.luci_app_ipsec_server
set firewall.luci_app_ipsec_server=include
set firewall.luci_app_ipsec_server.type=script
set firewall.luci_app_ipsec_server.path=/var/etc/ipsecvpn.include
set firewall.luci_app_ipsec_server.reload=1
commit firewall
EOF
uci -q batch <<-EOF >/dev/null
delete ucitrack.@luci-app-ipsec-server[-1]
add ucitrack luci-app-ipsec-server
set ucitrack.@luci-app-ipsec-server[-1].init=luci-app-ipsec-server
commit ucitrack
EOF
if [ -x "/sbin/fw3" ] && [ ! -L "/sbin/fw3" ]; then
uci -q set firewall.luci_app_ipsec_server.reload='1'
else
uci -q delete firewall.luci_app_ipsec_server.reload
uci -q delete firewall.luci_app_ipsec_server.fw4_compatible
fi
uci commit firewall
/etc/init.d/ipsec disable 2>/dev/null
/etc/init.d/ipsec stop 2>/dev/null
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
_LOGOUT_TIME="$(date "+%Y-%m-%d %H:%M:%S")"
CONFIG="luci-app-ipsec-server"
L2TP_PATH=/var/etc/xl2tpd
L2TP_SESSION_PATH=${L2TP_PATH}/session
_USERNAME=${PEERNAME}
_IFACE=${1}
_TTY=${2}
_SPEED=${3}
_LOCALIP=${4}
_PEERIP=${5}
_REMOTEIP=${6}
_BYTES_SENT=${BYTES_SENT}
_BYTES_RCVD=${BYTES_RCVD}
_CONNECT_TIME=${CONNECT_TIME}
rm -f ${L2TP_SESSION_PATH}/${_USERNAME}.${_IFACE}
rm -f /var/run/${_IFACE}.pid
#可根据退出的账号自定义脚本,如静态路由表,组网等。
SCRIPT="/usr/share/xl2tpd/ip-down.d/${_USERNAME}"
[ -s "$SCRIPT" ] && {
[ ! -x "$SCRIPT" ] && chmod 0755 "$SCRIPT"
"$SCRIPT" "$@"
}
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
_LOGIN_TIME="$(date "+%Y-%m-%d %H:%M:%S")"
CONFIG="luci-app-ipsec-server"
L2TP_PATH=/var/etc/xl2tpd
L2TP_SESSION_PATH=${L2TP_PATH}/session
_USERNAME=${PEERNAME}
_IFACE=${1}
_TTY=${2}
_SPEED=${3}
_LOCALIP=${4}
_PEERIP=${5}
_PID=$(cat /var/run/${_IFACE}.pid 2>/dev/null)
_REMOTEIP=$(cat /var/etc/xl2tpd/xl2tpd.log 2>/dev/null | grep "PID: ${_PID}" | grep -o -E '([0-9]{1,3}[\.]){3}[0-9]{1,3}')
mkdir -p ${L2TP_SESSION_PATH}
cat <<-EOF > ${L2TP_SESSION_PATH}/${_USERNAME}.${_IFACE}
{
"username": "${_USERNAME}",
"interface": "${_IFACE}",
"tty": "${_TTY}",
"speed": "${_SPEED}",
"ip": "${_PEERIP}",
"remote_ip": "${_REMOTEIP}",
"pid": "${_PID}",
"login_time": "${_LOGIN_TIME}"
}
EOF
#只能单用户使用
cfgid=$(uci show ${CONFIG} | grep "@l2tp_users" | grep "\.username='${_USERNAME}'" | cut -d '.' -sf 2)
[ -n "$cfgid" ] && {
HAS_LOGIN=$(ls ${L2TP_SESSION_PATH} | grep "^${_USERNAME}\.ppp" | grep -v "${_IFACE}")
[ -n "$HAS_LOGIN" ] && {
#踢出之前的用户
KO_IFACE=$(echo $HAS_LOGIN | awk -F '.' '{print $2}')
KO_PID=$(cat /var/run/${KO_IFACE}.pid 2>/dev/null)
[ -n "$KO_PID" ] && kill -9 ${KO_PID} >/dev/null 2>&1
rm -f ${L2TP_SESSION_PATH}/${HAS_LOGIN}
rm -f /var/run/${KO_IFACE}.pid
}
routes=$(uci -q get ${CONFIG}.${cfgid}.routes)
[ -n "$routes" ] && {
for router in ${routes}; do
route add -net ${router} dev ${_IFACE} >/dev/null 2>&1
done
}
}
#可根据登录的账号自定义脚本,如组网、日志、限速、权限等特殊待遇。
SCRIPT="/usr/share/xl2tpd/ip-up.d/${_USERNAME}"
[ -s "$SCRIPT" ] && {
[ ! -x "$SCRIPT" ] && chmod 0755 "$SCRIPT"
"$SCRIPT" "$@"
}
+70
View File
@@ -2,6 +2,76 @@
All notable changes to this project will be documented in this file.
## [2.7.2-r8] - Surge / SurgeMac 一并丢弃 Hysteria v1(LEGACY_ISSUES 7.9 j)
第六轮把 Loon 的 Hysteria v1 丢弃了,本轮把同一条证据链延伸到 Surge 家族。
### 修复 — 7.9(j):Surge / SurgeMac 丢弃 Hysteria v1
第六轮新发现 (j):Surge 家族的手册协议清单里写的也是 **"Hysteria 2"**,
`manual.nssurge.com/policies/hysteria.html` 是 404 而同目录的 `hysteria2.html`
存在 —— 与 Loon 同一情形,而本生成器仍会给这两家输出 `hysteria, ...` 行。
`FAMILY_CAPS.surge` / `FAMILY_CAPS.surgemac` 各加 `hysteria = false`,整条丢弃。
被丢的仍然只有上一代 **v1**:Hysteria 2 三家都支持,不受影响。
**Surfboard 不在其中**:`getsurfboard.com` 的 `profile-format/proxy/external-proxy`
一节及其无斜杠形式都返回 404,无法证实它读不懂 v1。既然没有证据,就不替它丢弃 ——
能力表对 Surfboard 保持沉默(`caps[p] == nil` 一律保留)。测试里有一条断言钉住这点,
防止日后有人「顺手统一」。
### 测试
* `tests/protocol_registry_test.lua`:`hysteria` 的丢弃表加 `surge` / `surgemac` 两条。
* `tests/output_formats_test.lua`:(j) 新增 5 条断言(surge 丢/留、组不含 v1、
surgemac 丢、surfboard 留),并删去第六轮那条 `finding j pending` 的占位断言。
* 反向验证:把 `hysteria = false` 从 surge / surgemac 撤掉,确认测试变红
(output_formats 3 条 + registry 1 条),且 surfboard 那条**始终绿**(本就无关)。
* 全套 **59 个测试文件、0 失败**。
## [2.7.2-r7] - Loon 丢弃 Hysteria v1、skip-cert-verify 改写 true(LEGACY_ISSUES 7.9 g/h)
第五轮新发现里经决策要实施的两条。第三条(协议名大小写)决定保持现状。
### 修复 — 7.9(g):Loon 丢弃 Hysteria v1
Loon 的节点类型清单里只有 **Hysteria2**(`nsloon.app/docs/Node/`),本生成器却会把
统一模型里的 hysteria v1 节点也输出成一条 Loon 读不懂的行。现在
`FAMILY_CAPS.loon` 加 `hysteria = false`,整条丢弃 —— 与丢弃 wireguard / ssr
同一约定。**hysteria 2 是各家通用的**,被丢的只有上一代 v1。
顺带把 `surge_config` 的过滤逻辑改成数据驱动:此前是逐协议写死的
`elseif p == "ssr" and not caps.ssr` / `elseif p == "vless" and not caps.vless`,
每加一个维度就要再加一条分支,**漏掉的那条默认是「保留」** —— 这正是本条缺陷的
成因(`hysteria` 从没进过这张表)。现在是 `elseif caps[p] == false`,标一个键即可。
### 修复 — 7.9(h):`skip-cert-verify` 写 `true` 而不是 `1`
Loon 文档的示例就是 `skip-cert-verify=false`,直接证明 `false` 是合法字面量、
`true` 同理;而 Surge 手册的 TLS 页只写「Optional, boolean, default: false」,
两家的文档里都找不到 `1` 这个取值。解析端两种都认,回环不受影响。
### 保持现状 — 7.9(i):协议名大小写
Loon 文档写 `VMess` / `Hysteria2`(首字母大写),本生成器用小写。无证据显示小写会
失败(Loon 定位是兼容 Surge 配置,Surge 手册的语法行就是小写),改动面却覆盖全部
协议 —— 不改。
### 测试
* `tests/protocol_registry_test.lua`:`hysteria` 的丢弃表加 `loon` 一条。
* `tests/output_formats_test.lua`:新增 (g) 5 条 / (h) 4 条断言(含回环)。
* 反向验证:两条改动各自改回旧行为,确认测试变红。
* 全套 **59 个测试文件、0 失败**。
### 本轮新发现(未实施,记录在 LEGACY_ISSUES)
(j) **Surge 家族同样没有 Hysteria v1** —— `manual.nssurge.com/policies/hysteria.html`
返回 404 而 `hysteria2.html` 存在,手册的协议清单写的也是 "Hysteria 2"。也就是
Surge / SurgeMac 的 `hysteria` 行同样读不懂。本轮**没有**顺手改 —— (g) 的决策只
点名了 Loon。测试里有一条断言如实钉住当前行为,实施 (j) 时一并翻转。
Surfboard 未获证据(其 external-proxy 文档页本轮返回 404)。
## [2.7.2-r6] - Loon / Surge 家族写法按官方文档对齐(LEGACY_ISSUES 7.9)
`docs/LEGACY_ISSUES.md` 第七节 2.7.2 审计新发现的**第五轮**修复:(a)(b)(c)(d)
+1 -1
View File
@@ -10,7 +10,7 @@ PKG_NAME:=luci-app-substore
# 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9);
# 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。
PKG_VERSION:=2.7.2
PKG_RELEASE:=21
PKG_RELEASE:=22
LUCI_DEPENDS:=+luci-lua-runtime +luci-compat
+164 -15
View File
@@ -658,13 +658,13 @@ crontab 里,`substore-cron.sh` 会拿着已不存在的 id 反复执行,每
| 7.6 | 修复 | **已修复**(第一轮,见下) |
| 7.7 | A:后端错误串 msgid 化 | **已实施**(第四轮,见下) |
| 7.8 | A:删除 `age.lua` / `age_test.lua` | **已删除**(第一轮) |
| 7.9 | (a)(b)(c)(d) 全部实施;(e) 保持保守(含逗号整条丢弃) | **已实施**(第五轮,见下) |
| 7.9 | (a)(b)(c)(d) 全部实施;(e) 保持保守;(g)(h) 实施;(i) 保持现状;(j) 实施(Surge/SurgeMac) | **已实施**(第五、六、七轮,见下) |
7.5 / 7.6 的修复见本节末尾「7.5 / 7.6 修复记录」,
7.1 / 7.3 / 7.4 的实施见「第二轮修复记录」,
7.2 的实施见「第三轮修复记录」,
7.7 的实施见「第四轮修复记录」,
7.9 的实施见「第五轮修复记录」。
7.9 的 (a)–(e) 见「第五轮修复记录」,(g)(h) 见「第六轮修复记录」,(j) 见「第七轮修复记录」。
| # | 问题 | 位置 | 依据 | 影响 |
|---|---|---|---|---|
@@ -676,7 +676,7 @@ crontab 里,`substore-cron.sh` 会拿着已不存在的 id 反复执行,每
| 7.6 | ~~Loon 的 `transport=ws` 未映射到 `net`~~ **已修复** | `parser_surge.parse_surge_line` 的 `transport=` 分支 | 只认 `ws=true`(Surge 旧写法)与 `obfs=ws`(QX);`nsloon.app/docs/Node/` 用 `transport=ws` + `path=` + `host=` | Loon 的 ws 节点导入后 `net=tcp`,`path` / `host` 全丢 → 导出到任何格式都按 tcp 连,握手失败**且不报错** |
| 7.7 | ~~后端模块仍有 **113 处**硬编码中文字符串字面量(注释外)~~ **已实施** | `core.lua` 47 / `http.lua` 47 / `parser.lua` 9 / `util.lua` 5 / `output_wireguard_conf.lua` 3 / `node.lua` 2 | 扫描脚本(去注释后提取含 CJK / 全角的字符串字面量),见下 | 控制器文案已接入 i18n,但这些来自后端的失败原因经 `?err=` **原样**显示,英文界面下仍是中文。**已按 A 实施**:全部改为语言中立的英文 msgid,组合消息用 `msg.lua` 的分隔符机制,翻译只在显示边界发生(详见「第四轮修复记录」) |
| 7.8 | ~~`root/usr/share/substore/age.lua` 与 `tests/age_test.lua` 未被 git 跟踪,且 `age.lua` 未被任何模块 `require`~~ **已删除** | 仓库根 | `grep -rn require` 无引用 | 未随包发布;留在工作区会被后续审计反复重新评估 |
| 7.9 | ~~Loon 的节点行仍有**多处**与官方文档不一致~~ **已实施** | `output_formats.surge_line` / `parser_surge.parse_surge_line` | `nsloon.app/docs/Node/`;`manual.nssurge.com`;`getsurfboard.com` | 逐条见下「7.9 的明细」。**已按 (a)(b)(c)(d) 全部实施、(e) 保持保守**:Loon 的 TLS 开关改 `over-tls`(Trojan 除外)、UDP 改 `udp`、Surge 家族补 `encrypt-method`、Loon 的 ss/ssr/hysteria2 改位置参数;解析端同步跟进以保住回环(详见「第五轮修复记录」)。实施中新发现三条待决策项,见下「第五轮新发现」 |
| 7.9 | ~~Loon 的节点行仍有**多处**与官方文档不一致~~ **已实施** | `output_formats.surge_line` / `parser_surge.parse_surge_line` | `nsloon.app/docs/Node/`;`manual.nssurge.com`;`getsurfboard.com` | 逐条见下「7.9 的明细」。**已按 (a)(b)(c)(d) 全部实施、(e) 保持保守**:Loon 的 TLS 开关改 `over-tls`(Trojan 除外)、UDP 改 `udp`、Surge 家族补 `encrypt-method`、Loon 的 ss/ssr/hysteria2 改位置参数;解析端同步跟进以保住回环(详见「第五轮修复记录」)。实施中新发现三条待决策项,见下「第五轮新发现」;其中 (g)(h) 已于第六轮实施、(i) 决定保持现状,另发现 (j) Surge 家族同样没有 Hysteria v1,**已于第七轮实施**(Surge / SurgeMac 丢弃,Surfboard 因无证据保留) |
### 7.7 的统计口径与例外(修复前必读)
@@ -790,33 +790,76 @@ trojan 页同样写 `username=` / `password=`。所以 Surge / Surfboard / Surge
就整条丢弃」处理,并在 `tests/output_formats_test.lua` 钉了一条断言防止日后被
无意改成半截输出。
### 第五轮新发现(**均未实施,待决策**)
### 第五轮新发现((g)(h) 第六轮已实施,(i) 保持现状,(j) 第七轮已实施)
第五轮实施 (a)–(d) 时对照 `nsloon.app/docs/Node/` 全文复核,另发现三处不一致。
每条都只陈述**文档写了什么**与**本实现写了什么**,不推测客户端的实际行为。
第六轮按决策实施了 (g)(h),(i) 决定保持现状,实施过程中又发现 (j)(第七轮已实施)。
**(g) Loon 没有 Hysteria v1,本生成器会把 `hysteria` 行输出给它** —— Loon 文档的
节点类型清单里只有 **Hysteria2**,没有 Hysteria(v1)。而 `FAMILY_CAPS` 只按
vless / ssr 分客户端能力,`hysteria` 不在过滤表里,所以统一模型里的 hysteria v1
节点会生成一条 Loon 读不懂的行。修法二选一:(i) 给 `FAMILY_CAPS.loon` 加
`hysteria = false` 整条丢弃(与丢弃 wireguard / ssr 同一约定);(ii) 保持现状。
**未实施** —— 需要先确认「Loon 是否真的不认 `hysteria`」以及 hysteria v1 节点在
用户订阅里的实际占比。
节点会生成一条 Loon 读不懂的行。
**→ 实施(第六轮)**:`FAMILY_CAPS.loon` 加 `hysteria = false`,整条丢弃
(与丢弃 wireguard / ssr 同一约定)。同时把 `surge_config` 的过滤从逐协议写死的
`elseif p == "ssr" and not caps.ssr` 改成数据驱动的 `elseif caps[p] == false` ——
每加一个维度就要再加一条分支,漏掉的那条默认是「保留」,正是本条缺陷的成因。
**注意 hysteria 2 是各家通用的**,被丢的只有上一代 v1。
**(h) `skip-cert-verify` 写成 `1`,两家文档都没写这个取值** —— Loon 文档的示例
一律写 `skip-cert-verify=false`;Surge 手册的 TLS 页只写「Optional, boolean,
default: false」,**没有**列出可接受的字面量。本生成器一律写
`skip-cert-verify=1`。`1` 是否被接受**无依据**。解析端两种都认(`parser_surge`
把非 `0` / 非 `false` 都当 true),所以回环不受影响,改与不改都不会破坏导入。
**未实施** —— 改成 `true` 会动到所有 Surge 系输出的既有断言,收益(若 `1` 本就被
接受则为零)尚不明确。
把非 `0` / 非 `false` 都当 true),所以回环不受影响。
**→ 实施(第六轮)**:改写成 `skip-cert-verify=true`。判据是「Loon 自己的示例写了
`false`,直接证明 `false` 是合法字面量、`true` 同理;而 `1` 在任何一家文档里都
找不到」。既有测试里没有任何一条断言过**生成端**的 `skip-cert-verify=1`
(`anytls_reality_test` 里那一处是把 `=1` 当**输入**喂给解析器,仍然有效)。
**(i) 协议名大小写:Loon 文档一律首字母大写,本生成器一律小写** —— Loon 文档写
`Hysteria2` / `VMess` / `VLESS` / `Shadowsocks` / `AnyTLS`;本生成器输出
`hysteria2` / `vmess` / `vless` / `shadowsocks` / `anytls`。Loon 官方定位是
「兼容 Surge 配置」,而 Surge 手册的语法行用的是小写(`Name = vmess, <host>, …`),
所以小写**很可能**被接受,但**没有文档明说**。**未实施** —— 这是全协议面的写法,
改动面大且依据不足;若日后确认大小写敏感,应改的是 Loon 分支的类型名映射。
所以小写**很可能**被接受,但**没有文档明说**。
**→ 决定:保持现状(第六轮)**。没有任何证据显示小写会失败,而改动面覆盖全部
协议;若日后确认大小写敏感,应改的是 Loon 分支的类型名映射。
### 第六轮新发现((j) **第七轮已实施**)
**(j) Surge 家族同样没有 Hysteria v1,本生成器仍会把 `hysteria` 行输出给它们** ——
第五轮实施 (g) 时顺带复核了 Surge 的协议清单,发现与 Loon 是**同一个缺口**:
* `manual.nssurge.com` 的 Proxy Protocols 一节列的是 **Hysteria 2**,没有 Hysteria;
* 直接的页面探针:`manual.nssurge.com/policies/hysteria.html` → **HTTP 404**,
而 `manual.nssurge.com/policies/hysteria2.html` → **存在**(语法行
`Proxy-Hysteria = hysteria2, 192.168.20.6, 443, password=pwd, download-bandwidth=100`)。
也就是说 Surge / SurgeMac 的 `hysteria`(v1)行同样是一条客户端读不懂的行。
**(g) 的决策只点名了 Loon**,所以第六轮**没有**顺手改 Surge 家族 ——
`tests/output_formats_test.lua` 里有一条断言如实钉住了当前行为
(`surge still keeps hysteria v1 (finding j pending)`),实施 (j) 时要一并翻转。
Surfboard 未获证据:`getsurfboard.com/docs/profile-format/proxy/external-proxy/`
与其无斜杠形式本轮都返回 404,没能取到协议清单。
修法:给 `FAMILY_CAPS` 的 surge / surfboard / surgemac 三行都加 `hysteria = false`
(过滤逻辑第六轮已改成数据驱动,加一个键即可),或把 `hysteria` 与 `wireguard`
一样无条件丢弃。
**→ 实施(第七轮,2.7.2-r8)**:`FAMILY_CAPS.surge` / `FAMILY_CAPS.surgemac` 各加
`hysteria = false`,整条丢弃。被丢的仍然只有上一代 v1 —— Hysteria 2 三家都支持,
两者在 `surge_line` 里共用同一个输出分支,靠 `proto` 区分,容易误伤,测试里专门
断言了「丢 v1、留 v2」。
**没有**给 `surfboard` 加这一条:文档 404、证据不足,能力表对它保持沉默
(`caps[p] == nil` 一律保留)。上面那条「三家都加」的修法**没有**采纳 ——
第七轮的决策是「Surge + SurgeMac 一起丢」,Surfboard 维持现状。
`tests/output_formats_test.lua` 里留了一条 `surfboard keeps hysteria v1
(no evidence to drop)` 钉住这点,防止日后有人「顺手统一」。
第六轮那条 `finding j pending` 的占位断言已按计划翻转成 `surge drops hysteria v1`。
## 7.1 / 7.2 / 7.3 的实测探针
@@ -1308,10 +1351,116 @@ trojan / vmess / vless 一律用具名参数」—— 7.4-A 之后就不成立
全套 **59 个测试文件、0 失败**。
### 本轮新发现(未实施,见上文「第五轮新发现」)
### 本轮新发现(见上文「第五轮新发现」)
(g) Loon 没有 Hysteria v1;(h) `skip-cert-verify=1` 的取值无文档依据;
(i) 协议名大小写。三条都只记录,不动代码。
(i) 协议名大小写。三条当时只记录、不动代码;(g)(h) 已在第六轮实施,
(i) 决定保持现状。
---
## 第六轮修复记录(7.9 的 g / h)
第五轮新发现里经决策要实施的两条。(i) 决定保持现状,不动代码。
### (g) Loon 丢弃 Hysteria v1
```lua
loon = { vless = true, ssr = true, hysteria = false },
```
判据:Loon 的节点类型清单里只有 **Hysteria2**(`nsloon.app/docs/Node/`)。
**hysteria 2 是各家通用的**,被丢的只有上一代 v1 —— 两者共用同一个输出分支
(`elseif proto == "hysteria2" or proto == "hysteria"`),最容易误伤。
**顺带把过滤逻辑改成数据驱动**。此前是逐协议写死的:
```lua
elseif p == "ssr" and not caps.ssr then -- 旧
elseif p == "vless" and not caps.vless then -- 旧
```
```lua
elseif caps[p] == false then -- 新
```
每加一个维度就要再加一条分支,**漏掉的那条默认是「保留」** —— 这正是本条缺陷的
成因(`hysteria` 从没进过这张表)。改成查表后,`FAMILY_CAPS` 里标一个 `false`
即可,不会再有「忘了加分支」这种漏法。`caps[p]` 为 nil(表没表态)一律保留。
### (h) `skip-cert-verify` 改写 `true`
```lua
if n["skip-cert-verify"] then e[#e + 1] = "skip-cert-verify=true" end
```
判据:Loon 文档的示例就是 `skip-cert-verify=false`,直接证明 `false` 是合法
字面量、`true` 同理;而 `1` 在任何一家文档里都找不到(Surge 手册的 TLS 页只写
「Optional, boolean, default: false」)。解析端两种都认,回环不受影响。
**断言面审计**:全仓库只有 `output_formats.lua` 一处写这个串;既有测试里**没有**
任何一条断言过生成端的 `skip-cert-verify=1` —— `anytls_reality_test.lua:151` 那处
是把 `=1` 当**输入**喂给解析器,改写后仍然有效。
### (i) 协议名大小写:保持现状
无证据显示小写会失败(Loon 定位是兼容 Surge 配置,Surge 手册的语法行就是小写),
改动面却覆盖全部协议。不改。
### 测试
* `tests/protocol_registry_test.lua`:`hysteria` 的丢弃表加 `loon` 一条。
* `tests/output_formats_test.lua`:新增 (g) 5 条 / (h) 4 条断言,含回环。
其中一条如实钉住 **Surge 家族当前仍输出 hysteria v1**(发现 (j),待决策),
实施 (j) 时要一并翻转。
* 反向验证:`FAMILY_CAPS.loon.hysteria` 改回不设 / `skip-cert-verify` 改回 `=1`,
确认测试变红。
* 全套 **59 个测试文件、0 失败**。
### 本轮新发现(见上文「第六轮新发现」)
(j) Surge 家族同样没有 Hysteria v1(`manual.nssurge.com/policies/hysteria.html`
是 404、`hysteria2.html` 存在)。本轮**没有**顺手改 —— (g) 的决策只点名了 Loon。
**→ 已于第七轮实施。**
---
## 第七轮修复记录(7.9 的 j)
第六轮新发现 (j):Surge 家族的手册协议清单里写的也是 **"Hysteria 2"**,
`manual.nssurge.com/policies/hysteria.html` 是 404 而同目录的 `hysteria2.html`
存在 —— 与 Loon 同一情形。本轮按决策把丢弃延伸到 Surge / SurgeMac。
### 改动
* `root/usr/share/substore/output_formats.lua`
* `FAMILY_CAPS.surge` / `FAMILY_CAPS.surgemac` 各加 `hysteria = false`。
**Surfboard 不加** —— 文档 404、证据不足(决策是「Surge + SurgeMac 一起丢」)。
* `FAMILY_CAPS` 上方的依据注释同步更新:Surge / SurgeMac 的清单补上「也没有
Hysteria v1」及其探针证据;Surfboard 一段写明「未获证据,故保留」。
* `surge_config` 的过滤注释补上 `hysteria` 一行,说明它同样按 `FAMILY_CAPS`
分客户端、Surfboard 因本表沉默而保留。
* 过滤**逻辑**本身不动:第六轮已改成数据驱动(`elseif caps[p] == false`),
本轮只是往表里加键 —— 这正是第六轮那次重构要买的东西。
### 测试
* `tests/protocol_registry_test.lua`:`hysteria` 的丢弃表加 `surge` / `surgemac`
两条,并注明 Surfboard 不在其中。
* `tests/output_formats_test.lua`:(j) 新增 5 条断言 —— surge 丢 v1 / 留 v2 /
组不含 v1、surgemac 丢 v1、**surfboard 留 v1(无证据丢弃)**;第六轮那条
`surge still keeps hysteria v1 (finding j pending)` 占位断言已删除(被
`surge drops hysteria v1` 取代)。
* 反向验证:把 `hysteria = false` 从 surge / surgemac 两行撤掉 → **4 条 FAIL**
(output_formats 3 条 + registry 1 条),而 `surfboard keeps hysteria v1`
那条**始终绿**(本轮没碰 Surfboard,它本就不该受影响)→ 说明断言确实指向
本轮改动,且没有误伤范围外的行为。还原后全绿。
* 全套 **59 个测试文件、0 失败**。
### 本轮新发现
无。这是 7.9 决策链的最后一条;实施完 (j) 后 7.9 的 (a)–(e)、(g)–(j) 全部结清
((f) 复核确认本来就对,(i) 决定保持现状)。
---
@@ -74,7 +74,9 @@ local REALITY_FLAVORS = { loon = true }
-- 各 Surge 系客户端对「非通用协议」的支持情况。
--
-- 通用协议(shadowsocks / vmess / trojan / socks5 / http / hysteria2 / tuic /
-- anytls)各家都认;各家**不一致**的只有 VLESS 与 SSR 两个,所以只记这两个。
-- anytls)各家都认;各家**不一致**的是 VLESS、SSR 与 Hysteria(v1)三个,
-- 所以只记这三个。注意 Hysteria **2** 是通用的,各家都支持;不一致的是它的
-- 上一代 Hysteria v1。
-- 用一张表而不是给每个调用点传布尔参数:此前是 `supports_ssr` 一个布尔量,
-- 加一个维度就要再加一个参数,调用点一多必然漏传(漏传的默认值还是「支持」,
-- 于是导出里多出客户端读不懂的行,且不报错)。
@@ -83,18 +85,32 @@ local REALITY_FLAVORS = { loon = true }
-- * Surge / SurgeMac:manual.nssurge.com 的 Proxy Protocols 一节只列
-- HTTP and HTTP/2、SOCKS5、Shadowsocks、Snell、VMess、Trojan、TUIC、
-- Hysteria 2、MASQUE、AnyTLS、Trust Tunnel、SSH、WireGuard、Tailscale
-- —— 没有 VLESS,也没有 ShadowsocksR。
-- —— 没有 VLESS、没有 ShadowsocksR,也没有 Hysteria(v1)。
-- Hysteria v1 的证据:清单里写的是 "Hysteria 2",且同目录下
-- manual.nssurge.com/policies/hysteria2.html 存在而 hysteria.html 是 404。
-- * Surfboard:getsurfboard.com 的 external-proxy 清单同样没有 VLESS / SSR。
-- * Loon:nsloon.app/docs/Node/ 有独立的 VLESS 与 ShadowsocksR 两节,都支持。
-- (Surfboard 是否有 Hysteria v1 未获证据:该站文档的
-- profile-format/proxy/external-proxy 一节及其无斜杠形式都返回 404。
-- 既然无法证实「读不懂」,就不替它丢弃 —— 见 LEGACY_ISSUES 的 (j),
-- 该条已实施:Surge / SurgeMac 丢弃 Hysteria v1,Surfboard 按此保留。)
-- * Loon:nsloon.app/docs/Node/ 有独立的 VLESS 与 ShadowsocksR 两节,都支持;
-- 但节点类型清单里**只有 Hysteria2**,没有 Hysteria(v1)—— 与 Surge 家族
-- 相反的两处之一(另一处是 VLESS / SSR)。
--
-- Egern **不在**这张表里:它的配置是 YAML,已由 output_egern.lua 单独实现
-- (7.2 已实施)。它的协议清单与 Surge 家族本就不同(有 VLESS 与 WireGuard,
-- 没有 SSR 与 Hysteria v1),能力判定一并搬到了那个模块里。
--
-- 注意 hysteria 指的是**上一代 v1**:Hysteria 2 是各家通用的,被丢的只有 v1
-- (两者在 surge_line 里共用同一个输出分支,靠 proto 区分)。
local FAMILY_CAPS = {
surge = { vless = false, ssr = false },
surge = { vless = false, ssr = false, hysteria = false },
surfboard = { vless = false, ssr = false },
surgemac = { vless = false, ssr = false },
loon = { vless = true, ssr = true },
surgemac = { vless = false, ssr = false, hysteria = false },
-- Loon 支持 VLESS 与 SSR(与 Surge 家族相反),但没有 Hysteria v1
-- (节点类型清单里只有 Hysteria2)。标 false 即整条丢弃,与丢弃 wireguard
-- 同一约定:不输出客户端读不懂的东西。
loon = { vless = true, ssr = true, hysteria = false },
}
-- 生成 Surge 风格代理行(Surge / Surfboard / SurgeMac / Loon 通用)。
@@ -339,7 +355,12 @@ function M.surge_line(n, flavor)
return nil
end
if n["skip-cert-verify"] then e[#e + 1] = "skip-cert-verify=1" end
-- 取值写 true/false,不写 1:Loon 文档的示例就是 `skip-cert-verify=false`
-- (nsloon.app/docs/Node/ 的 Hysteria2 一行),直接证明 false 是合法字面量、
-- true 同理;而 Surge 手册的 TLS 页只写「Optional, boolean, default: false」,
-- 两家的文档里都找不到 `1` 这个取值。解析端两种都认
-- (parser_surge 把非 "0" / 非 "false" 都当 true),所以回环不受影响。
if n["skip-cert-verify"] then e[#e + 1] = "skip-cert-verify=true" end
-- UDP 转发的参数名同样各家不同:Surge 家族是 udp-relay=true,Loon 是 udp=true
-- (nsloon.app/docs/Node/ 的每个示例都写 udp=true,udp-relay 一次都没出现;
-- 相关的另外两个开关叫 udp-over-tcp / udp-port)。
@@ -402,16 +423,19 @@ local function surge_config(nodes, group_name, flavor)
local list = nodes or {}
-- 过滤 Surge 家族无法用单行 [Proxy] 表达的协议:
-- vless / ssr:按 FAMILY_CAPS 分客户端(Surge/Surfboard/SurgeMac 两个都不认)
-- hysteria:同样按 FAMILY_CAPS(Surge/SurgeMac 只有 Hysteria 2,没有 v1;
-- Surfboard 未获证据,本表没对它表态,故保留)
-- wireguard:Surge 需专用多段 [WireGuard] 配置,单行无法表达,统一丢弃(不输出损坏行)
local kept = {}
for _, n in ipairs(list) do
local p = (n.proto or ""):lower()
if p == "wireguard" then
-- drop
elseif p == "ssr" and not caps.ssr then
-- drop
elseif p == "vless" and not caps.vless then
-- drop
elseif caps[p] == false then
-- drop:能力表显式标了 false 的协议。写成数据驱动而不是逐个协议写
-- `elseif p == "ssr" and not caps.ssr`:每加一个维度就要再加一条分支,
-- 漏掉的那条默认是「保留」,于是导出里多出客户端读不懂的行且不报错。
-- (caps[p] 为 nil 表示「这张表没对它表态」,一律保留。)
else
kept[#kept + 1] = n
end
@@ -262,5 +262,44 @@ local rt_tj = loon_roundtrip(tj_node)
check("loon rt trojan password", rt_tj and rt_tj.password == "p")
check("loon rt trojan security", rt_tj and rt_tj.security == "tls")
-- ---------- 7.9(g)+(j):Loon / Surge / SurgeMac 都没有 Hysteria v1 ----------
-- Loon 的节点类型清单里只有 Hysteria2(nsloon.app/docs/Node/)。注意 hysteria
-- **2** 是各家通用的,被丢的只有上一代 v1 —— 两者共用同一个输出分支,容易误伤。
local hy1_node = { proto = "hysteria", name = "H1", server = "1.1.1.1", port = 443, password = "p" }
local hy2_node = { proto = "hysteria2", name = "H2", server = "1.1.1.1", port = 443, password = "p" }
local loon_hy = fmts.to_loon({ hy1_node, hy2_node }, { name = "P" })
check("loon drops hysteria v1", loon_hy:find("hysteria, 1.1.1.1", 1, true) == nil)
check("loon keeps hysteria2", loon_hy:find('hysteria2, 1.1.1.1, 443, "p"', 1, true) ~= nil)
-- 被丢弃的节点也不能留在 [Proxy Group] 的成员列表里(否则组引用一个不存在的代理)
check("loon group has no hysteria v1", loon_hy:find("H1", 1, true) == nil)
check("loon group keeps hysteria2", loon_hy:find("H2", 1, true) ~= nil)
-- (j):Surge / SurgeMac 的手册协议清单写的也是 "Hysteria 2",
-- manual.nssurge.com/policies/hysteria.html 是 404 而 hysteria2.html 存在。
-- Surfboard 未获证据(其文档 404),**不**跟着丢 —— 这条断言防止有人顺手扩大范围。
local surge_hy = fmts.to_surge({ hy1_node, hy2_node }, { name = "P" })
check("surge drops hysteria v1", surge_hy:find("hysteria, 1.1.1.1", 1, true) == nil)
check("surge keeps hysteria2", surge_hy:find("hysteria2, 1.1.1.1", 1, true) ~= nil)
check("surge group has no hysteria v1", surge_hy:find("H1", 1, true) == nil)
local mac_hy = fmts.to_surgemac({ hy1_node }, { name = "P" })
check("surgemac drops hysteria v1", mac_hy:find("hysteria, 1.1.1.1", 1, true) == nil)
local sb_hy = fmts.to_surfboard({ hy1_node }, { name = "P" })
check("surfboard keeps hysteria v1 (no evidence to drop)",
sb_hy:find("hysteria, 1.1.1.1", 1, true) ~= nil)
-- ---------- 7.9(h):skip-cert-verify 写 true/false ----------
-- Loon 文档的示例是 skip-cert-verify=false;Surge 手册只写 "boolean"。
-- 两家文档里都找不到 `1` 这个取值。
local scv_node = { proto = "hysteria2", name = "S", server = "1.1.1.1", port = 443,
password = "p", ["skip-cert-verify"] = true }
check("surge skip-cert-verify=true",
fmts.surge_line(scv_node, "surge"):find("skip-cert-verify=true", 1, true) ~= nil)
check("loon skip-cert-verify=true",
fmts.surge_line(scv_node, "loon"):find("skip-cert-verify=true", 1, true) ~= nil)
check("no skip-cert-verify=1",
fmts.surge_line(scv_node, "surge"):find("skip-cert-verify=1", 1, true) == nil)
local rt_scv = loon_roundtrip(scv_node)
check("loon rt skip-cert-verify", rt_scv and rt_scv["skip-cert-verify"] == true)
print(string.format("\n%d passed, %d failed", passed, failed))
os.exit(failed == 0 and 0 or 1)
@@ -103,9 +103,17 @@ local DROPPED = {
v2ray = "V2RAY_PROTOS 无 hysteria2", wgconf = "wgconf 只收 wireguard" },
tuic = { clash = "原版 Clash 无 tuic", qx = "to_qx 无 tuic",
v2ray = "V2RAY_PROTOS 无 tuic", wgconf = "wgconf 只收 wireguard" },
-- hysteria v1 的第二、三处:Loon 的节点类型清单里同样只有 Hysteria2
-- (nsloon.app/docs/Node/),Surge / SurgeMac 的手册协议清单写的也是
-- "Hysteria 2"(且 policies/hysteria.html 是 404、hysteria2.html 存在)——
-- 均由 FAMILY_CAPS.<flavor>.hysteria=false 整条丢弃。
-- Surfboard **不在**此列:其文档 404,无法证实它读不懂 v1,故保留。
hysteria = { clash = "原版 Clash 无 hysteria", qx = "to_qx 无 hysteria",
v2ray = "V2RAY_PROTOS 无 hysteria", wgconf = "wgconf 只收 wireguard",
egern = "Egern 只有 Hysteria2,没有 Hysteria v1" },
egern = "Egern 只有 Hysteria2,没有 Hysteria v1",
loon = "Loon 只有 Hysteria2,没有 Hysteria v1",
surge = "Surge 只有 Hysteria2,没有 Hysteria v1",
surgemac = "SurgeMac 只有 Hysteria2,没有 Hysteria v1" },
-- wireguard:Surge 家族需要专用的多段 [WireGuard] 配置,单行 [Proxy] 表达不了,
-- surge_config 统一丢弃;原版 Clash / qx / v2ray 同样不支持。
-- Egern **不在**此列:它的配置是 YAML,WireGuard 有独立的协议块
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=naiveproxy
PKG_VERSION:=154.0.8037.49
PKG_RELEASE:=2
PKG_RELEASE:=3
ARCH_PREBUILT:=$(ARCH_PACKAGES)