mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:52:51 +08:00
🎈 Sync 2026-09-30 10:37:56
This commit is contained in:
1 parent
7bb984c650
commit
214a01499a
17 files changed
+383
-66
No files matched your search
+9
-6
@@ -9,18 +9,20 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=firewall
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL=$(PROJECT_GIT)/project/firewall3.git
|
||||
PKG_SOURCE_DATE:=2022-02-17
|
||||
PKG_SOURCE_VERSION:=4cd7d4f36bea731bf901cb067456f1d460294926
|
||||
PKG_SOURCE_DATE:=2025-10-03
|
||||
PKG_SOURCE_VERSION:=3a65fde5a3e675fdf2a24cb3f5d12d0a426be36e
|
||||
PKG_MIRROR_HASH:=skip
|
||||
PKG_MAINTAINER:=Jo-Philipp Wich <jo@mein.io>
|
||||
PKG_LICENSE:=ISC
|
||||
|
||||
PKG_CONFIG_DEPENDS := CONFIG_IPV6
|
||||
|
||||
PKG_BUILD_FLAGS:=gc-sections lto
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/cmake.mk
|
||||
|
||||
@@ -28,7 +30,10 @@ define Package/firewall
|
||||
SECTION:=net
|
||||
CATEGORY:=Base system
|
||||
TITLE:=OpenWrt C Firewall
|
||||
DEPENDS:=+libubox +libubus +libuci +libip4tc +IPV6:libip6tc +libiptext +IPV6:libiptext6 +libxtables +kmod-ipt-core +kmod-ipt-conntrack +iptables-mod-conntrack-extra +IPV6:kmod-nf-conntrack6 +kmod-ipt-nat +iptables-mod-fullconenat \
|
||||
DEPENDS:=+libubox +libubus +libuci +libip4tc +IPV6:libip6tc +libiptext +IPV6:libiptext6 \
|
||||
+libxtables +kmod-ipt-core +kmod-ipt-conntrack +IPV6:kmod-nf-conntrack6 +kmod-ipt-nat \
|
||||
+iptables-mod-fullconenat \
|
||||
+iptables-mod-fullconenat +ip6tables-mod-fullconenat +iptables-legacy +ip6tables-legacy +kmod-ipt-nat6 +kmod-ipt-offload \
|
||||
+ipset +iptables-mod-conntrack-extra +iptables-mod-iprange +iptables-mod-socket +iptables-mod-tproxy
|
||||
PROVIDES:=uci-firewall
|
||||
endef
|
||||
@@ -42,8 +47,6 @@ define Package/firewall/conffiles
|
||||
/etc/firewall.user
|
||||
endef
|
||||
|
||||
TARGET_CFLAGS += -ffunction-sections -fdata-sections -flto
|
||||
TARGET_LDFLAGS += -Wl,--gc-sections -flto
|
||||
CMAKE_OPTIONS += $(if $(CONFIG_IPV6),,-DDISABLE_IPV6=1)
|
||||
|
||||
define Package/firewall/install
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
config defaults
|
||||
option synflood_protect 1
|
||||
option input ACCEPT
|
||||
option input REJECT
|
||||
option output ACCEPT
|
||||
option forward REJECT
|
||||
option fullcone 0
|
||||
option flow_offloading 1
|
||||
option fullcone 1
|
||||
# Uncomment this line to disable ipv6 rules
|
||||
# option disable_ipv6 1
|
||||
|
||||
@@ -55,13 +56,12 @@ config rule
|
||||
option target ACCEPT
|
||||
|
||||
# Allow DHCPv6 replies
|
||||
# see https://dev.openwrt.org/ticket/10381
|
||||
# see https://github.com/openwrt/openwrt/issues/5066
|
||||
config rule
|
||||
option name Allow-DHCPv6
|
||||
option src wan
|
||||
option proto udp
|
||||
option src_ip fc00::/6
|
||||
option dest_ip fc00::/6
|
||||
option dest_ip fe80::/10
|
||||
option dest_port 546
|
||||
option family ipv6
|
||||
option target ACCEPT
|
||||
@@ -141,7 +141,7 @@ config rule
|
||||
option proto udp
|
||||
option family ipv4
|
||||
option target REJECT
|
||||
option enabled false
|
||||
option enabled 0
|
||||
|
||||
# include a file with users custom iptables rules
|
||||
config include
|
||||
|
||||
@@ -4,17 +4,6 @@ START=19
|
||||
USE_PROCD=1
|
||||
QUIET=""
|
||||
|
||||
LOCK_FILE="/var/lock/procd_firewall.lock"
|
||||
set_lock() {
|
||||
exec 1000>"$LOCK_FILE"
|
||||
flock -xn 1000
|
||||
}
|
||||
|
||||
unset_lock() {
|
||||
flock -u 1000
|
||||
rm -rf "$LOCK_FILE"
|
||||
}
|
||||
|
||||
validate_firewall_redirect()
|
||||
{
|
||||
uci_validate_section firewall redirect "${1}" \
|
||||
@@ -41,39 +30,27 @@ validate_firewall_rule()
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger firewall
|
||||
set_lock
|
||||
|
||||
procd_open_validate
|
||||
validate_firewall_redirect
|
||||
validate_firewall_rule
|
||||
procd_close_validate
|
||||
unset_lock
|
||||
}
|
||||
|
||||
restart() {
|
||||
unset_lock
|
||||
set_lock
|
||||
fw3 restart
|
||||
unset_lock
|
||||
}
|
||||
|
||||
start_service() {
|
||||
set_lock
|
||||
fw3 ${QUIET} start
|
||||
unset_lock
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
fw3 flush
|
||||
unset_lock
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
unset_lock
|
||||
set_lock
|
||||
reload_config firewall
|
||||
fw3 reload
|
||||
unset_lock
|
||||
}
|
||||
|
||||
boot() {
|
||||
|
||||
File renamed without changes.
+7
-5
@@ -1,6 +1,6 @@
|
||||
--- a/defaults.c
|
||||
+++ b/defaults.c
|
||||
@@ -49,7 +49,7 @@ const struct fw3_option fw3_flag_opts[]
|
||||
@@ -49,7 +49,7 @@
|
||||
FW3_OPT("synflood_rate", limit, defaults, syn_flood_rate),
|
||||
FW3_OPT("synflood_burst", int, defaults, syn_flood_rate.burst),
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
|
||||
FW3_OPT("tcp_syncookies", bool, defaults, tcp_syncookies),
|
||||
FW3_OPT("tcp_ecn", int, defaults, tcp_ecn),
|
||||
|
||||
--- a/options.h
|
||||
+++ b/options.h
|
||||
@@ -98,6 +98,13 @@ enum fw3_reject_code
|
||||
@@ -98,6 +98,13 @@
|
||||
__FW3_REJECT_CODE_MAX
|
||||
};
|
||||
|
||||
@@ -25,7 +26,7 @@
|
||||
extern const char *fw3_flag_names[__FW3_FLAG_MAX];
|
||||
|
||||
|
||||
@@ -297,7 +304,7 @@ struct fw3_defaults
|
||||
@@ -297,7 +304,7 @@
|
||||
enum fw3_reject_code any_reject_code;
|
||||
|
||||
bool syn_flood;
|
||||
@@ -34,9 +35,10 @@
|
||||
struct fw3_limit syn_flood_rate;
|
||||
|
||||
bool tcp_syncookies;
|
||||
|
||||
--- a/zones.c
|
||||
+++ b/zones.c
|
||||
@@ -757,7 +757,7 @@ print_zone_rule(struct fw3_ipt_handle *h
|
||||
@@ -756,7 +756,7 @@
|
||||
r = fw3_ipt_rule_new(handle);
|
||||
fw3_ipt_rule_src_dest(r, msrc, mdest);
|
||||
/*FIXME: Workaround for FULLCONE-NAT*/
|
||||
@@ -45,7 +47,7 @@
|
||||
{
|
||||
warn("%s will enable FULLCONE-NAT", zone->name);
|
||||
fw3_ipt_rule_target(r, "FULLCONENAT");
|
||||
@@ -767,6 +767,12 @@ print_zone_rule(struct fw3_ipt_handle *h
|
||||
@@ -766,6 +766,12 @@
|
||||
fw3_ipt_rule_target(r, "FULLCONENAT");
|
||||
fw3_ipt_rule_append(r, "zone_%s_prerouting", zone->name);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ LUCI_DEPENDS:= \
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy-pro
|
||||
PKG_VERSION:=28.9.1.14
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
# Only files the package actually ships belong here. The previous list named
|
||||
# four paths that exist nowhere in the tree - certs/ and ruleset/ are created at
|
||||
|
||||
@@ -26,6 +26,15 @@ config homeproxy-pro 'config'
|
||||
option main_udp_node 'same'
|
||||
option dns_server '8.8.8.8'
|
||||
option china_dns_server '223.5.5.5'
|
||||
# Bootstrap resolver for the DoH/DoT endpoint's own hostname. Without
|
||||
# it `main-dns` borrows the WAN resolver, so the one lookup that decides
|
||||
# where every proxied name goes is answered by the ISP: poisoning that
|
||||
# single answer is enough to break the whole DNS split, and it is the
|
||||
# only query that was still leaving through the ISP after the split
|
||||
# itself was configured correctly. This is a conffile, so upgrading
|
||||
# never rewrites an existing /etc/config/homeproxy-pro - the value below
|
||||
# only reaches fresh installs.
|
||||
list bootstrap_dns '223.5.5.5'
|
||||
option routing_mode 'bypass_mainland_china'
|
||||
option routing_port 'common'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
|
||||
@@ -2064,6 +2064,34 @@ fi
|
||||
rm -rf "$SNAP_TMP"
|
||||
trap - EXIT INT TERM
|
||||
|
||||
echo
|
||||
echo "== guard 45: a fresh install bootstraps main-dns, an untouched config still does not =="
|
||||
|
||||
# E3. With no bootstrap_dns, `main-dns` (the DoH/DoT endpoint) falls back to
|
||||
# the WAN resolver, so the ISP answers the one lookup that decides where every
|
||||
# proxied name goes. The package default below closes that for fresh
|
||||
# installs; this guard exists so neither half drifts silently:
|
||||
# (a) an explicitly empty list must still emit NO bootstrap-dns server, so
|
||||
# a user who clears the field gets the old behaviour back rather than a
|
||||
# resolver the generator invents for them;
|
||||
# (b) the package-shipped default is what makes the fix reach a new install
|
||||
# at all - the field is read from the 'config' section, and the file is
|
||||
# a conffile, so this line only lands on fresh installs.
|
||||
if grep -q "if (isEmpty(ctx.bootstrap_dns))" "$SCRIPTS/generator/dns.uc"; then
|
||||
pass "an empty bootstrap_dns still emits no bootstrap-dns server"
|
||||
else
|
||||
fail "append_bootstrap_dns no longer short-circuits on an empty bootstrap_dns - a"
|
||||
fail "user who cleared the field would get a resolver they did not ask for"
|
||||
fi
|
||||
|
||||
if grep -q "^ list bootstrap_dns '223.5.5.5'" "$ROOT/root/etc/config/homeproxy-pro"; then
|
||||
pass "a fresh install ships a bootstrap_dns, so main-dns never asks the ISP"
|
||||
else
|
||||
fail "the package-shipped /etc/config/homeproxy-pro no longer ships a bootstrap_dns -"
|
||||
fail "a new install would resolve its DoH endpoint's hostname through the WAN"
|
||||
fail "resolver, which is the last query that still leaves via the ISP"
|
||||
fi
|
||||
|
||||
echo
|
||||
printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')"
|
||||
if [ "$FAILED" != 0 ]; then
|
||||
|
||||
@@ -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.6.0
|
||||
PKG_RELEASE:=11
|
||||
PKG_RELEASE:=12
|
||||
|
||||
LUCI_DEPENDS:=+luci-lua-runtime +luci-compat
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
-- 归一为白名单取值后用 json_encode 输出,保证是合法的 JS 字符串字面量
|
||||
if local_mode ~= "form" then local_mode = "text" end
|
||||
local js_mode = util.json_encode(local_mode) or '"text"'
|
||||
-- 协议与字段清单:与 core.merge_form_node 共用 substore/node.lua 的同一份数据
|
||||
local node_mod = require("substore.node")
|
||||
local js_protos = (util.json_encode(node_mod.PROTOS) or "[]"):gsub("</", "<\\/")
|
||||
local js_proto_fields = (util.json_encode(node_mod.PROTO_FIELDS) or "{}"):gsub("</", "<\\/")
|
||||
%>
|
||||
<h2><%=is_edit and luci.i18n.translate("Edit local subscription") or luci.i18n.translate("Add local subscription")%></h2>
|
||||
|
||||
@@ -118,6 +122,7 @@ var FIELD_LABELS = {
|
||||
"obfs": "Obfs",
|
||||
"obfs-param": "Obfs Param",
|
||||
"obfs-password": "Obfs Password",
|
||||
"protocol": "Protocol",
|
||||
"protocol-param": "Protocol Param",
|
||||
"udp": "UDP",
|
||||
"skip-cert-verify": "Skip Cert Verify",
|
||||
@@ -140,8 +145,13 @@ var NODEFORM_I18N = {
|
||||
"node": "<%:Node%>",
|
||||
"defaultOpt": "<%:Default%>"
|
||||
};
|
||||
// 协议与字段清单由服务端注入(唯一来源:substore/node.lua 的 M.PROTOS /
|
||||
// M.PROTO_FIELDS)。core.merge_form_node 用同一份清单决定哪些字段可以被表单
|
||||
// 覆盖,两边必须严格一致,因此不在前端另抄一份。
|
||||
var SUBSTORE_PROTOS = <%=js_protos%>;
|
||||
var SUBSTORE_PROTO_FIELDS = <%=js_proto_fields%>;
|
||||
</script>
|
||||
<script src="/luci-static/resources/substore/nodeform.js?v=2.2.0"></script>
|
||||
<script src="/luci-static/resources/substore/nodeform.js?v=2.6.1"></script>
|
||||
<script>
|
||||
// 切换导入模式:仅切换显示/禁用,不提交表单
|
||||
function substore_local_switch(mode) {
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
local n = nodes[idx]
|
||||
-- 注入 JS 的节点对象:转义 "</" 防止提前闭合 script
|
||||
local js_node = n and ((util.json_encode(n) or "null"):gsub("</", "<\\/")) or "null"
|
||||
-- 协议与字段清单:与 core.merge_form_node 共用 substore/node.lua 的同一份数据
|
||||
local node_mod = require("substore.node")
|
||||
local js_protos = (util.json_encode(node_mod.PROTOS) or "[]"):gsub("</", "<\\/")
|
||||
local js_proto_fields = (util.json_encode(node_mod.PROTO_FIELDS) or "{}"):gsub("</", "<\\/")
|
||||
local back_url = luci.dispatcher.build_url('admin/services/substore/nodes') .. "?id=" .. luci.util.urlencode(id)
|
||||
%>
|
||||
<h2><%:Edit node%> — <%=luci.util.pcdata(meta and meta.name or "")%></h2>
|
||||
@@ -61,6 +65,7 @@ var FIELD_LABELS = {
|
||||
"obfs": "Obfs",
|
||||
"obfs-param": "Obfs Param",
|
||||
"obfs-password": "Obfs Password",
|
||||
"protocol": "Protocol",
|
||||
"protocol-param": "Protocol Param",
|
||||
"udp": "UDP",
|
||||
"skip-cert-verify": "Skip Cert Verify",
|
||||
@@ -83,9 +88,14 @@ var NODEFORM_I18N = {
|
||||
"node": "<%:Node%>",
|
||||
"defaultOpt": "<%:Default%>"
|
||||
};
|
||||
// 协议与字段清单由服务端注入(唯一来源:substore/node.lua 的 M.PROTOS /
|
||||
// M.PROTO_FIELDS)。core.merge_form_node 用同一份清单决定哪些字段可以被表单
|
||||
// 覆盖,两边必须严格一致,因此不在前端另抄一份。
|
||||
var SUBSTORE_PROTOS = <%=js_protos%>;
|
||||
var SUBSTORE_PROTO_FIELDS = <%=js_proto_fields%>;
|
||||
var NODE_EDIT_DATA = <%=js_node%>;
|
||||
</script>
|
||||
<script src="/luci-static/resources/substore/nodeform.js?v=2.2.0"></script>
|
||||
<script src="/luci-static/resources/substore/nodeform.js?v=2.6.1"></script>
|
||||
<script>
|
||||
function node_edit_prepare() {
|
||||
var arr = collectNodes();
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
local group = http.formvalue("group") or q.group or ""
|
||||
local sort_by = http.formvalue("sort") or q.sort or "name"
|
||||
local sort_desc = http.formvalue("desc") == "1" or q.desc == "1"
|
||||
-- id 会进入 <script> 里的 JS 字符串字面量,用 JSON 编码而不是 HTML 转义
|
||||
local js_id = util.json_encode(id) or '""'
|
||||
-- id 会进入 <script> 里的 JS 字符串字面量,用 JSON 编码而不是 HTML 转义。
|
||||
-- 但只有 JSON 编码还不够:json_encode 不转义 "<",id 直接来自查询串,
|
||||
-- 形如 "</script><script>alert(1)</script>" 的值会在 HTML 词法阶段就闭合
|
||||
-- script 元素(与 JS 字符串上下文无关),剩下的部分被当作 HTML 执行。
|
||||
-- 转义 "</" 即可:在 JS 字符串里 "\/" 就是 "/",而 HTML 不再看到 "</script"。
|
||||
local js_id = (util.json_encode(id) or '""'):gsub("</", "<\\/")
|
||||
|
||||
-- 标注原始数组下标(filter 保留表引用、sort 原地排序,下标可穿透筛选排序)
|
||||
for i, nn in ipairs(nodes) do nn.__idx = i end
|
||||
|
||||
@@ -386,11 +386,68 @@ local FORM_KEYS = {
|
||||
username = true,
|
||||
}
|
||||
|
||||
-- 合并表单节点到原节点:表单字段整体替换(可清空),非表单字段保留
|
||||
-- FORM_KEYS 里同一个语义的多种写法。表单只渲染其中的规范写法,别名必须跟随规范
|
||||
-- 写法一起清空:解析器产出的就是别名(parser_clash_yaml 写 skip_cert_verify、
|
||||
-- parser_json_config 与 parser.lua 写 obfs_param / protocol_param),残留的别名
|
||||
-- 仍会被输出模块读到,表现为「关不掉」——例如 build_tls 在 security 为 "none"
|
||||
-- 时还会退回 n.tls,output_formats.surge_line 同理,于是用户把 vmess 的 TLS
|
||||
-- 关掉之后 Surge 输出里依然是 tls=true。
|
||||
local FORM_ALIASES = {
|
||||
["obfs_param"] = "obfs-param",
|
||||
["protocol_param"] = "protocol-param",
|
||||
["skip_cert_verify"] = "skip-cert-verify",
|
||||
["obfs_password"] = "obfs-password",
|
||||
["private_key"] = "private-key",
|
||||
["public_key"] = "public-key",
|
||||
["peer-public-key"] = "public-key",
|
||||
["peer_public_key"] = "public-key",
|
||||
["preshared_key"] = "pre-shared-key",
|
||||
["allowed_ips"] = "allowed-ips",
|
||||
["persistent_keepalive"] = "persistent-keepalive",
|
||||
["listen_port"] = "listen-port",
|
||||
["network"] = "net",
|
||||
-- normalize 把 tls 归一到 security,两者是同一语义的两种写法
|
||||
["tls"] = "security",
|
||||
-- shadowsocks 用 method、vmess/ssr 用 cipher,parse_local 双向互为别名
|
||||
["method"] = "cipher",
|
||||
["cipher"] = "method",
|
||||
}
|
||||
|
||||
-- 合并表单节点到原节点。
|
||||
--
|
||||
-- 只有「该协议的表单确实渲染过」的字段才允许被覆盖(含被清空):表单按
|
||||
-- node.PROTO_FIELDS[proto] 渲染,未渲染的字段根本提交不上来,把它们一并清空
|
||||
-- 就等于用空值覆盖原值 —— vmess 的 security、hysteria2/tuic 的 security、
|
||||
-- WireGuard 的 dns 都是这样被静默抹掉的(详见 node.PROTO_FIELDS 的注释)。
|
||||
-- 别名(FORM_ALIASES)跟随其规范写法一起清空,避免残留值「关不掉」。
|
||||
--
|
||||
-- 协议未知(不在 PROTO_FIELDS 里)时退回「清空全部表单字段」的旧行为:
|
||||
-- 那时无从判断表单渲染了什么,清空虽然可能丢字段,但至少不会留下用户改不动的旧值。
|
||||
function M.merge_form_node(orig, formnode)
|
||||
local node_mod = require("substore.node")
|
||||
local out = {}
|
||||
for k, v in pairs(orig or {}) do out[k] = v end
|
||||
local fields = type(formnode) == "table" and node_mod.PROTO_FIELDS[formnode.proto] or nil
|
||||
local old_fields = type(orig) == "table" and node_mod.PROTO_FIELDS[orig.proto] or nil
|
||||
-- 原协议未知(或原节点没有 proto)时不做「只清渲染字段」的裁剪:那种情况下
|
||||
-- 无从判断原节点里哪些字段是本协议的,退回到清空全部表单字段的旧行为。
|
||||
if fields and (orig == nil or orig.proto == nil or old_fields) then
|
||||
local rendered = {}
|
||||
for _, k in ipairs(fields) do rendered[k] = true end
|
||||
-- 协议被改过时,旧协议的字段也要清掉(vmess 改成 trojan 不该留着 uuid)。
|
||||
-- 协议没变时 old_fields == fields,是同一个集合,无副作用。
|
||||
for _, k in ipairs(old_fields or {}) do rendered[k] = true end
|
||||
for _, k in ipairs(node_mod.FORM_ALWAYS_FIELDS) do rendered[k] = true end
|
||||
for k in pairs(FORM_KEYS) do
|
||||
-- 先看字段本身是否被渲染;只有「别名」才回退到它的规范写法。
|
||||
-- 顺序不能反:method 与 cipher 互为别名,若一律先查别名表,
|
||||
-- shadowsocks 渲染的 method 会被判成「cipher 没渲染」而不清空。
|
||||
local canon = rendered[k] and k or (FORM_ALIASES[k] or k)
|
||||
if rendered[canon] then out[k] = nil end
|
||||
end
|
||||
else
|
||||
for k in pairs(FORM_KEYS) do out[k] = nil end
|
||||
end
|
||||
for k, v in pairs(formnode or {}) do
|
||||
if k ~= "type" then out[k] = v end
|
||||
end
|
||||
|
||||
@@ -7,11 +7,55 @@ M.PROTOS = {
|
||||
"vmess", "vless", "trojan", "shadowsocks", "ssr", "hysteria2", "tuic", "hysteria", "wireguard", "socks",
|
||||
}
|
||||
|
||||
-- 每种协议在「表单导入 / 节点编辑」界面里渲染的字段集合,数组顺序即界面顺序。
|
||||
--
|
||||
-- 这里是**唯一**的字段清单:LuCI 页面把它渲染成 window.SUBSTORE_PROTO_FIELDS
|
||||
-- 交给 nodeform.js 渲染表单,core.merge_form_node 用它决定「哪些字段可以被表单
|
||||
-- 覆盖(含清空)」。两边共用一份数据不是洁癖,而是必须的 —— 表单没渲染的字段
|
||||
-- 提交不上来,合并时若一并清空就等于用空值覆盖原值:
|
||||
-- * vmess 的 TLS 层字段是 security,而表单原先渲染的是 tls,编辑一次就把
|
||||
-- security 抹成 nil,normalize 再补成 "none",一个启用 TLS 的 vmess 节点
|
||||
-- 静默变成明文;
|
||||
-- * hysteria2 / tuic / hysteria 是 TLS-only 协议(parser.lua 补 security="tls"),
|
||||
-- 表单不渲染 security,编辑一次就丢掉 TLS,sing-box 会因 C.ErrTLSRequired
|
||||
-- 直接拒绝启动;
|
||||
-- * WireGuard 的 dns、vmess 的 flow 同理。
|
||||
-- 所以字段清单必须与表单渲染的字段严格一致,不能各维护一份。
|
||||
M.PROTO_FIELDS = {
|
||||
ssr = { "server", "port", "password", "cipher", "protocol", "obfs", "obfs-param", "protocol-param", "udp" },
|
||||
-- vmess 的 TLS 开关是 security(与 vless 一致):节点模型里 TLS 层统一存于
|
||||
-- security,vmess 的加密方式存于 cipher。表单渲染 tls 时读不到 security,
|
||||
-- 保存即丢 TLS。
|
||||
vmess = { "server", "port", "uuid", "alterId", "cipher", "net", "headerType", "path", "host", "sni", "security", "udp", "skip-cert-verify" },
|
||||
vless = { "server", "port", "uuid", "security", "flow", "net", "headerType", "path", "host", "sni", "udp", "skip-cert-verify" },
|
||||
trojan = { "server", "port", "password", "sni", "net", "headerType", "path", "host", "udp", "skip-cert-verify" },
|
||||
shadowsocks = { "server", "port", "password", "method", "headerType", "udp" },
|
||||
hysteria2 = { "server", "port", "password", "sni", "obfs", "obfs-password", "skip-cert-verify" },
|
||||
-- hysteria(v1):obfs 是普通字符串(不是 hysteria2 的 salamander),因此没有 obfs-password。
|
||||
-- 字段集合以各输出模块实际消费的键为准(output_clash_meta / output_singbox / output_uri)。
|
||||
hysteria = { "server", "port", "password", "sni", "obfs", "skip-cert-verify" },
|
||||
tuic = { "server", "port", "uuid", "password", "sni", "udp", "skip-cert-verify" },
|
||||
wireguard = { "server", "port", "private-key", "public-key", "pre-shared-key", "ip", "ipv6", "allowed-ips", "reserved", "persistent-keepalive", "listen-port", "mtu", "amnezia-wg-option" },
|
||||
socks = { "server", "port", "username", "password", "udp" },
|
||||
}
|
||||
|
||||
-- 表单里与协议无关、始终渲染的字段(nodeform.js 的 nodeTemplate 固定输出这三个)
|
||||
M.FORM_ALWAYS_FIELDS = { "name", "group" }
|
||||
|
||||
-- 协议默认值,用于补全缺省字段
|
||||
local DEFAULTS = {
|
||||
vmess = { net = "tcp", security = "none" },
|
||||
vless = { net = "tcp", security = "none" },
|
||||
trojan = { security = "tls" },
|
||||
-- hysteria2 / hysteria / tuic 在 sing-box 与 mihomo 里都是 TLS-only:
|
||||
-- sing-box 的 hysteria2/tuic 出站在 TLS 缺失或未启用时直接返回 C.ErrTLSRequired
|
||||
-- 拒绝启动。URI 解析器(parser.lua)本来就会补 security="tls",但 Clash YAML
|
||||
-- 与 sing-box JSON 导入的 hysteria2/tuic 节点没有这个字段,导出后是一份客户端
|
||||
-- 起不来的配置。TLS-only 是协议本身的约束,属于归一化该保证的不变量,
|
||||
-- 与 trojan 同理。
|
||||
hysteria2 = { security = "tls" },
|
||||
hysteria = { security = "tls" },
|
||||
tuic = { security = "tls" },
|
||||
}
|
||||
|
||||
-- vmess 加密方式(cipher)白名单。sing-box 的 vmess.security、Xray 的
|
||||
|
||||
@@ -2,28 +2,19 @@
|
||||
* 依赖页面在引入本文件前定义:
|
||||
* var FIELD_LABELS = { "name": "...", "group": "...", ... }; // 字段标签(模板用 <%:...%> 渲染)
|
||||
* var NODEFORM_I18N = { node: "...", defaultOpt: "..." }; // 界面文本(同上)
|
||||
* var SUBSTORE_PROTOS = [...]; // 协议下拉框
|
||||
* var SUBSTORE_PROTO_FIELDS = { proto: [...] }; // 各协议渲染的字段
|
||||
* 提供:SUBSTORE_PROTOS / PROTO_FIELDS / renderFields / collectNodes / addNodeRow
|
||||
*
|
||||
* 协议与字段清单**不在本文件里**,由页面从 substore/node.lua 的 M.PROTOS /
|
||||
* M.PROTO_FIELDS 渲染后注入。这不是洁癖:服务端的 core.merge_form_node 用同一份
|
||||
* 清单决定「哪些字段可以被表单覆盖(含清空)」,两边各维护一份必然会漂移 ——
|
||||
* 表单没渲染的字段提交不上来,而合并时一并清空就等于用空值覆盖原值(vmess 与
|
||||
* hysteria2/tuic 的 TLS 层字段就是这样被静默抹掉的)。详见 node.PROTO_FIELDS 的注释。
|
||||
*/
|
||||
var SUBSTORE_PROTOS = window.SUBSTORE_PROTOS || [];
|
||||
|
||||
// 必须与 substore/node.lua 的 M.PROTOS 一致(规格 §25「本地表单协议能力」的协议集合)。
|
||||
// 之前这里少了 hysteria / socks:node.lua、节点页筛选、规则 proto_filter 都认这两个协议,
|
||||
// 唯独表单下拉框选不到,用户无法用「表单导入」录入它们。
|
||||
var SUBSTORE_PROTOS = ["vmess","vless","trojan","shadowsocks","ssr","hysteria2","tuic","hysteria","wireguard","socks"];
|
||||
|
||||
var PROTO_FIELDS = {
|
||||
ssr: ["server","port","password","cipher","protocol","obfs","obfs-param","protocol-param","udp"],
|
||||
vmess: ["server","port","uuid","alterId","cipher","net","headerType","path","host","sni","tls","udp","skip-cert-verify"],
|
||||
vless: ["server","port","uuid","security","flow","net","headerType","path","host","sni","udp","skip-cert-verify"],
|
||||
trojan: ["server","port","password","sni","net","headerType","path","host","udp","skip-cert-verify"],
|
||||
shadowsocks: ["server","port","password","method","headerType","udp"],
|
||||
hysteria2: ["server","port","password","sni","obfs","obfs-password","skip-cert-verify"],
|
||||
// hysteria(v1):obfs 是普通字符串(不是 hysteria2 的 salamander),因此没有 obfs-password。
|
||||
// 字段集合以各输出模块实际消费的键为准(output_clash_meta / output_singbox / output_uri)。
|
||||
hysteria: ["server","port","password","sni","obfs","skip-cert-verify"],
|
||||
tuic: ["server","port","uuid","password","sni","udp","skip-cert-verify"],
|
||||
wireguard: ["server","port","private-key","public-key","pre-shared-key","ip","ipv6","allowed-ips","reserved","persistent-keepalive","listen-port","mtu","amnezia-wg-option"],
|
||||
socks: ["server","port","username","password","udp"]
|
||||
};
|
||||
var PROTO_FIELDS = window.SUBSTORE_PROTO_FIELDS || {};
|
||||
|
||||
// 枚举字段用下拉框;键为 "协议.字段" 优先,退化为通用 "字段"
|
||||
// 选项集合以模型/输出模块实际支持的值为准(见 output_singbox.build_transport / output_uri)
|
||||
|
||||
@@ -53,5 +53,63 @@ local m5 = core.merge_form_node(orig, nil)
|
||||
check("nil form keeps non-form fields", m5.raw == "vmess://xxx")
|
||||
check("nil form clears form fields", m5.name == nil)
|
||||
|
||||
-- ---------- 表单没渲染的字段不得被清空 ----------
|
||||
--
|
||||
-- 表单按 node.PROTO_FIELDS[proto] 渲染,没渲染的字段提交不上来。原先
|
||||
-- merge_form_node 一律清空全部表单字段,等于用空值覆盖原值:vmess 与
|
||||
-- hysteria2/tuic 的 TLS 层字段(security)就是这样被静默抹掉的 —— 前者
|
||||
-- 变成明文,后者让 sing-box 因 C.ErrTLSRequired 拒绝启动。
|
||||
local parser = require("substore.parser")
|
||||
local util = require("substore.util")
|
||||
local node = require("substore.node")
|
||||
|
||||
-- 模拟一次真实的表单提交:只提交本协议渲染出来的字段,再过 parse_local
|
||||
local function submit(proto, vals)
|
||||
local t = {}
|
||||
for _, k in ipairs(node.PROTO_FIELDS[proto] or {}) do
|
||||
if vals[k] ~= nil and vals[k] ~= "" then t[k] = tostring(vals[k]) end
|
||||
end
|
||||
t.type = proto
|
||||
return parser.parse_local(util.json_encode({ t }), "form").nodes[1]
|
||||
end
|
||||
|
||||
local vless_node = { proto = "vmess", name = "n", server = "1.1.1.1", port = 443, uuid = "u", security = "tls" }
|
||||
local kept = core.merge_form_node(vless_node, submit("vmess", { name = "n", server = "1.1.1.1", port = 443, uuid = "u", security = "tls" }))
|
||||
check("vmess security kept through form", kept.security == "tls")
|
||||
|
||||
-- 关掉 TLS 时,别名 tls 必须一起清掉,否则 build_tls / surge_line 会退回旧值
|
||||
local off = core.merge_form_node(
|
||||
{ proto = "vmess", name = "n", server = "1.1.1.1", port = 443, uuid = "u", security = "tls", tls = true },
|
||||
submit("vmess", { name = "n", server = "1.1.1.1", port = 443, uuid = "u", security = "none" }))
|
||||
check("vmess security off honoured", off.security == "none")
|
||||
check("vmess tls alias cleared", off.tls == nil)
|
||||
|
||||
-- hysteria2 / tuic / hysteria 是 TLS-only,表单不渲染 security,不得被清掉
|
||||
for _, p in ipairs({ "hysteria2", "hysteria", "tuic" }) do
|
||||
local o = { proto = p, name = "n", server = "1.1.1.1", port = 443, password = "pw", security = "tls" }
|
||||
local r = core.merge_form_node(o, submit(p, { name = "n", server = "1.1.1.1", port = 443, password = "pw" }))
|
||||
check(p .. " security kept through form", r.security == "tls")
|
||||
end
|
||||
|
||||
-- 未渲染的普通字段同样保留(WireGuard 的 dns、vmess 的 flow)
|
||||
local wg = { proto = "wireguard", name = "w", server = "1.1.1.1", port = 51820, ["private-key"] = "pk", dns = "1.1.1.1" }
|
||||
local wg2 = core.merge_form_node(wg, submit("wireguard", { name = "w", server = "1.1.1.1", port = 51820, ["private-key"] = "pk" }))
|
||||
check("wireguard dns preserved", wg2.dns == "1.1.1.1")
|
||||
local vm = { proto = "vmess", name = "n", server = "1.1.1.1", port = 443, uuid = "u", flow = "xtls-rprx-vision" }
|
||||
local vm2 = core.merge_form_node(vm, submit("vmess", { name = "n", server = "1.1.1.1", port = 443, uuid = "u" }))
|
||||
check("vmess flow preserved", vm2.flow == "xtls-rprx-vision")
|
||||
|
||||
-- 别名跟随规范名一起清空(解析器产出的是下划线写法)
|
||||
local alias = core.merge_form_node(
|
||||
{ proto = "vmess", name = "n", server = "1.1.1.1", port = 443, uuid = "u", ["skip-cert-verify"] = true, skip_cert_verify = true },
|
||||
submit("vmess", { name = "n", server = "1.1.1.1", port = 443, uuid = "u" }))
|
||||
check("skip-cert-verify cleared", alias["skip-cert-verify"] == nil)
|
||||
check("skip_cert_verify alias cleared", alias.skip_cert_verify == nil)
|
||||
|
||||
-- 协议未知时退回旧行为(清空全部表单字段)
|
||||
local unk = core.merge_form_node({ proto = "snell", name = "s", server = "1.1.1.1", port = 443, password = "p" },
|
||||
{ proto = "snell", name = "s", server = "1.1.1.1", port = 443 })
|
||||
check("unknown proto falls back to full wipe", unk.password == nil)
|
||||
|
||||
print(string.format("%d passed, %d failed", passed, failed))
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
@@ -0,0 +1,124 @@
|
||||
-- view_injection_test.lua — 视图层的注入与字段清单一致性检查
|
||||
-- 用法:lua5.1 tests/view_injection_test.lua
|
||||
--
|
||||
-- 这些是静态源码检查(.htm 需要 LuCI 运行时才能渲染,lua5.1 跑不了模板),
|
||||
-- 但检查的正是那类「只在浏览器里才暴露」的缺陷:
|
||||
-- H2 注入到 <script> 里的值必须转义 "</":json_encode 不转义 "<",
|
||||
-- 而 "</script" 会在 HTML 词法阶段就闭合脚本元素(与 JS 字符串上下文无关),
|
||||
-- 查询串里的 id 可以直接构造出 "</script><script>alert(1)</script>"。
|
||||
-- 字段清单必须由服务端注入:core.merge_form_node 用 node.PROTO_FIELDS 决定
|
||||
-- 哪些字段可以被表单覆盖,前端若另抄一份就会漂移(vmess 的 TLS 字段
|
||||
-- 丢失正是这么来的)。
|
||||
|
||||
package.path = "./root/usr/share/?.lua;" .. package.path
|
||||
|
||||
local util = require("substore.util")
|
||||
local node = require("substore.node")
|
||||
|
||||
local passed, failed = 0, 0
|
||||
|
||||
local function check(name, cond)
|
||||
if cond then
|
||||
passed = passed + 1
|
||||
print("PASS " .. name)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("FAIL " .. name)
|
||||
end
|
||||
end
|
||||
|
||||
local VIEWS = {
|
||||
"root/usr/lib/lua/luci/view/substore/nodes.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/node_edit.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/local_form.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/subscriptions.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/form.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/combo.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/output.htm",
|
||||
}
|
||||
|
||||
-- ---------- H2:JSON 编码注入到 JS 的位置必须同时转义 "</" ----------
|
||||
--
|
||||
-- 逐个 json_encode 调用点检查:要么同一行/下一行出现 gsub("</", …),
|
||||
-- 要么该值是白名单归一化后的固定取值(注释里标注了「白名单」)。
|
||||
for _, path in ipairs(VIEWS) do
|
||||
local src = util.read_file(path)
|
||||
if src then
|
||||
local lines = {}
|
||||
for l in (src .. "\n"):gmatch("([^\n]*)\n") do lines[#lines + 1] = l end
|
||||
for i, l in ipairs(lines) do
|
||||
-- 只关心真正注入到模板里的调用,跳过注释
|
||||
if l:find("json_encode") and not l:match("^%s*%-%-") and not l:match("^%s*//") then
|
||||
-- 转义可能写在下一行,白名单说明通常写在紧邻的注释里(上一两行)
|
||||
local window = (lines[i - 2] or "") .. (lines[i - 1] or "") .. l .. (lines[i + 1] or "")
|
||||
local escaped = window:find('gsub("</"', 1, true) ~= nil
|
||||
-- 白名单归一化的取值("text"/"form")不含 "<",无需转义
|
||||
local whitelisted = window:find("白名单", 1, true) ~= nil
|
||||
check(path:match("[^/]+$") .. ":" .. i .. " json_encode escapes </",
|
||||
escaped or whitelisted)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 反向验证:不转义时确实能闭合 script(说明上面的检查不是空转)
|
||||
local payload = "</script><script>alert(1)</script>"
|
||||
check("json_encode does not escape <", util.json_encode(payload):find("</script", 1, true) ~= nil)
|
||||
check("gsub(</) removes the closing tag",
|
||||
(util.json_encode(payload):gsub("</", "<\\/")):find("</script", 1, true) == nil)
|
||||
|
||||
-- ---------- 字段清单:唯一来源在 Lua,页面负责注入 ----------
|
||||
local js = util.read_file("root/www/luci-static/resources/substore/nodeform.js")
|
||||
check("nodeform.js readable", js ~= nil)
|
||||
-- 注释里会举例说明注入方式("var PROTO_FIELDS = { proto: [...] }"),先剥掉注释再查
|
||||
local js_code = js:gsub("/%*.-%*/", ""):gsub("//[^\n]*", "")
|
||||
check("nodeform.js does not hardcode PROTO_FIELDS", js_code:find("PROTO_FIELDS = {") == nil)
|
||||
check("nodeform.js does not hardcode SUBSTORE_PROTOS",
|
||||
js_code:find('SUBSTORE_PROTOS = %[') == nil)
|
||||
check("nodeform.js reads injected PROTO_FIELDS", js:find("window.SUBSTORE_PROTO_FIELDS") ~= nil)
|
||||
check("nodeform.js reads injected SUBSTORE_PROTOS", js:find("window.SUBSTORE_PROTOS") ~= nil)
|
||||
|
||||
for _, path in ipairs({ "root/usr/lib/lua/luci/view/substore/node_edit.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/local_form.htm" }) do
|
||||
local src = util.read_file(path) or ""
|
||||
local name = path:match("[^/]+$")
|
||||
check(name .. " injects SUBSTORE_PROTOS", src:find("var SUBSTORE_PROTOS = <%=js_protos%>", 1, true) ~= nil)
|
||||
check(name .. " injects SUBSTORE_PROTO_FIELDS", src:find("var SUBSTORE_PROTO_FIELDS = <%=js_proto_fields%>", 1, true) ~= nil)
|
||||
check(name .. " loads nodeform.js", src:find("nodeform.js", 1, true) ~= nil)
|
||||
end
|
||||
|
||||
-- 每个协议都要有字段清单,否则表单渲染不出任何输入框
|
||||
for _, p in ipairs(node.PROTOS) do
|
||||
local f = node.PROTO_FIELDS[p]
|
||||
check("PROTO_FIELDS has " .. p, type(f) == "table" and #f > 0)
|
||||
end
|
||||
|
||||
-- 每个渲染出来的字段都要有标签,否则界面上直接显示英文键名
|
||||
local label_src = (util.read_file("root/usr/lib/lua/luci/view/substore/node_edit.htm") or "")
|
||||
local labels = {}
|
||||
for k in label_src:gmatch('"([%w%-]+)"%s*:') do labels[k] = true end
|
||||
local missing = {}
|
||||
for p, fields in pairs(node.PROTO_FIELDS) do
|
||||
for _, k in ipairs(fields) do
|
||||
if not labels[k] then missing[#missing + 1] = p .. "." .. k end
|
||||
end
|
||||
end
|
||||
table.sort(missing)
|
||||
check("every form field has a label" .. (#missing > 0 and (" (missing: " .. table.concat(missing, ",") .. ")") or ""),
|
||||
#missing == 0)
|
||||
|
||||
-- TLS 层字段必须出现在表单里,否则编辑一次就丢 TLS(vmess 的历史缺陷)
|
||||
for _, p in ipairs({ "vmess", "vless" }) do
|
||||
local has = false
|
||||
for _, k in ipairs(node.PROTO_FIELDS[p]) do if k == "security" then has = true end end
|
||||
check(p .. " form renders security", has)
|
||||
end
|
||||
|
||||
-- TLS-only 协议必须由 normalize 保证 security,而不是靠 URI 解析器各自补
|
||||
for _, p in ipairs({ "hysteria2", "hysteria", "tuic", "trojan" }) do
|
||||
check(p .. " normalize forces security=tls",
|
||||
node.normalize({ proto = p, server = "h", port = 1 }).security == "tls")
|
||||
end
|
||||
|
||||
print(string.format("\n%d passed, %d failed", passed, failed))
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
Reference in new issue
Block a user