mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:12:51 +08:00
🎈 Sync 2026-10-04 14:38:18
This commit is contained in:
1 parent
0e6611f767
commit
aba12667c5
88 files changed
+4600
-1458
No files matched your search
+6
-3
@@ -9,19 +9,22 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=brlaser
|
||||
PKG_VERSION:=v3
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=3
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE_URL:=https://github.com/pdewacht/brlaser.git
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_SOURCE_VERSION:=2a49e3287c70c254e7e3ac9dabe9d6a07218c3fa
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(PKG_SOURCE_VERSION).tar.gz
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_FIXUP:=autoreconf
|
||||
|
||||
PKG_INSTALL:=1
|
||||
|
||||
# Add CMAKE_POLICY_VERSION_MINIMUM to handle CMake 3.5+ compatibility
|
||||
CMAKE_OPTIONS += -DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/cmake.mk
|
||||
|
||||
|
||||
+5
-5
@@ -3,17 +3,17 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=honk
|
||||
PKG_VERSION:=2026.10.3_pre2
|
||||
PKG_RELEASE:=6
|
||||
PKG_VERSION:=2026.10.4_pre1
|
||||
PKG_RELEASE:=7
|
||||
|
||||
# Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49)
|
||||
# because that is what the GitHub release asset name uses.
|
||||
HONK_RELEASE_TAG:=debug.2026.10.3.native-api.2
|
||||
HONK_RELEASE_TAG:=debug.2026.10.4.native-api.1
|
||||
HONK_ASSET_TAG:=debug
|
||||
|
||||
# SHA256 of each release tarball, kept in sync by scripts/update_honk_version.sh.
|
||||
HONK_HASH_X86_64:=7ce4585150c46e95829f916f843675ce3471fee48e13a012c0f3652028f137dd
|
||||
HONK_HASH_AARCH64:=ee5ff000688237d1b1bde701f933736dfc7ed479cb84fdf2bddc6d64c95c33ab
|
||||
HONK_HASH_X86_64:=dfeab5ca18744563c0cb672069fc548744360e3f4b2f35d7be3cfbfef96dbf6c
|
||||
HONK_HASH_AARCH64:=a17fd9ee544b7671f57defc4c030a7a3620c1ebfdf329e6e2a22050223ddad9e
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
PKG_LICENSE_FILE:=LICENSE
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-banmac-ipt
|
||||
PKG_VERSION:=20211118
|
||||
PKG_RELEASE:=1
|
||||
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-banmac-ipt
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=LuCI Support for Ban Client
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-banmac-ipt/install
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci
|
||||
cp -pR ./luasrc/* $(1)/usr/lib/lua/luci
|
||||
$(INSTALL_DIR) $(1)/usr/banmac
|
||||
$(INSTALL_BIN) ./files/banmac/* $(1)/usr/banmac
|
||||
$(INSTALL_DIR) $(1)/etc
|
||||
$(INSTALL_CONF) ./files/banmaclog $(1)/etc/banmaclog
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/banmac.conf $(1)/etc/config/banmac
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-banmac-ipt))
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
config banlist
|
||||
|
||||
config banmac 'smartphone'
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
sectionname=$(echo $0 | cut -d '_' -f 2 | sed 's/^OO!%!OO//')
|
||||
getcmac=$(uci get banmac.$sectionname.banlist_mac | tr 'A-Z' 'a-z')
|
||||
iptables -I FORWARD -m mac --mac-source $getcmac -j DROP
|
||||
hostname=$(grep -n $getcmac /tmp/dhcp.leases | cut -d ' ' -f 4)
|
||||
hostip=$(grep -n $getcmac /tmp/dhcp.leases | cut -d ' ' -f 3)
|
||||
echo "★禁网设备:$hostname($hostip) MAC地址:$getcmac 操作日期:$(date +%Y年%m月%d日\ %H点%M分%S秒)" >> /etc/banmaclog
|
||||
|
||||
for i in $(seq 0 1)
|
||||
do
|
||||
for x in $(iw wlan${i} station dump | grep -i station | cut -d ' ' -f 2)
|
||||
do
|
||||
if [ $x = $getcmac ]; then
|
||||
iw dev wlan${i} station del $x
|
||||
fi
|
||||
done
|
||||
done
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
sectionname=$(echo $0 | cut -d '_' -f 2 | sed 's/^OO!%!OO//')
|
||||
getcmac=$(uci get banmac.$sectionname.banlist_mac | tr 'A-Z' 'a-z')
|
||||
iptables -D FORWARD -m mac --mac-source $getcmac -j DROP
|
||||
|
||||
sed -i "s/$(cat /etc/banmaclog | grep $getcmac | head -n 1)//;/^\s*$/d" /etc/banmaclog
|
||||
@@ -1,3 +0,0 @@
|
||||
#点击 One-Click Ban 快速禁止指定客户端。点击 One-Click UnBan 快速解禁。
|
||||
#这里记录了客户端列表
|
||||
----------------------------------------------------------------------
|
||||
@@ -1,7 +0,0 @@
|
||||
module("luci.controller.banmac",package.seeall)
|
||||
|
||||
function index()
|
||||
local page = entry({"admin", "services", "banmac"}, cbi("banmac"), _("BanMac"),5)
|
||||
page.dependent = true
|
||||
end
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
m = Map("banmac", translate("BanMac"))
|
||||
|
||||
local banlist = m:section(TypedSection, "banlist", translate("log"))
|
||||
banlist.anonymous = true
|
||||
|
||||
local bmdetails = "/etc/banmaclog"
|
||||
local BMNXFS = require "nixio.fs"
|
||||
bmd = banlist:option(TextValue, "details")
|
||||
bmd.rows = 6
|
||||
bmd.wrap = "off"
|
||||
bmd.cfgvalue = function(self, section)
|
||||
return BMNXFS.readfile(bmdetails) or ""
|
||||
end
|
||||
bmd.write = function(self, section, value)
|
||||
BMNXFS.writefile(bmdetails, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
|
||||
s = m:section(TypedSection, "banmac", "")
|
||||
s.anonymous = false
|
||||
s.addremove = true
|
||||
|
||||
s:tab("banmactab", translate("BanMac Menu"))
|
||||
|
||||
banlist_mac = s:taboption("banmactab", Value, "banlist_mac", translate("MAC address"))
|
||||
banlist_mac.rmempty = true
|
||||
banlist_mac.datatype = "macaddr"
|
||||
luci.sys.net.mac_hints(function(mac, name)
|
||||
banlist_mac:value(mac, "%s (%s)" %{ mac, name })
|
||||
end)
|
||||
|
||||
ban_mac = s:taboption("banmactab", Button, "ban_mac", translate("One-Click BAN"))
|
||||
ban_mac.rmempty = false
|
||||
ban_mac.inputstyle = "apply"
|
||||
function ban_mac.write(self, section)
|
||||
luci.util.exec("cp /usr/banmac/ban.sh /tmp/ban_OO!%!OO" ..section.. "_.sh >/dev/null 2>&1 &")
|
||||
luci.util.exec("/tmp/ban_OO!%!OO" ..section.. "_.sh >/dev/null 2>&1 &")
|
||||
end
|
||||
|
||||
unban_mac = s:taboption("banmactab", Button, "unban_mac", translate("One-Click UnBAN"))
|
||||
unban_mac.rmempty = false
|
||||
unban_mac.inputstyle = "apply"
|
||||
function unban_mac.write(self, section)
|
||||
luci.util.exec("cp /usr/banmac/unban.sh /tmp/unban_OO!%!OO" ..section.. "_.sh >/dev/null 2>&1 &")
|
||||
luci.util.exec("/tmp/unban_OO!%!OO" ..section.. "_.sh >/dev/null 2>&1 &")
|
||||
end
|
||||
|
||||
return m
|
||||
@@ -1,32 +0,0 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-banmac-nft
|
||||
PKG_VERSION:=20231012
|
||||
PKG_RELEASE:=1
|
||||
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-banmac-nft
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=LuCI Support for Ban Client
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-banmac-nft/install
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci
|
||||
cp -pR ./luasrc/* $(1)/usr/lib/lua/luci
|
||||
$(INSTALL_DIR) $(1)/usr/banmac
|
||||
$(INSTALL_BIN) ./files/banmac/* $(1)/usr/banmac
|
||||
$(INSTALL_DIR) $(1)/etc
|
||||
$(INSTALL_CONF) ./files/banmaclog $(1)/etc/banmaclog
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/banmac.conf $(1)/etc/config/banmac
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-banmac-nft))
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
config banlist
|
||||
|
||||
config banmac 'smartphone'
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
sectionname=$(echo $0 | cut -d '_' -f 2 | sed 's/^OO!%!OO//')
|
||||
getcmac=$(uci get banmac.$sectionname.banlist_mac | tr 'A-Z' 'a-z')
|
||||
iptables -I FORWARD -m mac --mac-source $getcmac -j DROP
|
||||
hostname=$(grep -n $getcmac /tmp/dhcp.leases | cut -d ' ' -f 4)
|
||||
hostip=$(grep -n $getcmac /tmp/dhcp.leases | cut -d ' ' -f 3)
|
||||
echo "★禁网设备:$hostname($hostip) MAC地址:$getcmac 操作日期:$(date +%Y年%m月%d日\ %H点%M分%S秒)" >> /etc/banmaclog
|
||||
|
||||
for i in $(seq 0 1);do
|
||||
for j in $(seq 0 1);do
|
||||
for x in $(iw dev phy${i}-ap${j} station dump | grep -i station | awk '{print$2}');do
|
||||
if [ $x = $getcmac ]; then
|
||||
iw dev phy${i}-ap${j} station del $x
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
sectionname=$(echo $0 | cut -d '_' -f 2 | sed 's/^OO!%!OO//')
|
||||
getcmac=$(uci get banmac.$sectionname.banlist_mac | tr 'A-Z' 'a-z')
|
||||
iptables -D FORWARD -m mac --mac-source $getcmac -j DROP
|
||||
|
||||
sed -i "s/$(cat /etc/banmaclog | grep $getcmac | head -n 1)//;/^\s*$/d" /etc/banmaclog
|
||||
@@ -1,3 +0,0 @@
|
||||
#点击 One-Click Ban 快速禁止指定客户端。点击 One-Click UnBan 快速解禁。
|
||||
#这里记录了客户端列表
|
||||
----------------------------------------------------------------------
|
||||
@@ -1,7 +0,0 @@
|
||||
module("luci.controller.banmac",package.seeall)
|
||||
|
||||
function index()
|
||||
local page = entry({"admin", "services", "banmac"}, cbi("banmac"), _("BanMac"),5)
|
||||
page.dependent = true
|
||||
end
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
m = Map("banmac", translate("BanMac"))
|
||||
|
||||
local banlist = m:section(TypedSection, "banlist", translate("log"))
|
||||
banlist.anonymous = true
|
||||
|
||||
local bmdetails = "/etc/banmaclog"
|
||||
local BMNXFS = require "nixio.fs"
|
||||
bmd = banlist:option(TextValue, "details")
|
||||
bmd.rows = 6
|
||||
bmd.wrap = "off"
|
||||
bmd.cfgvalue = function(self, section)
|
||||
return BMNXFS.readfile(bmdetails) or ""
|
||||
end
|
||||
bmd.write = function(self, section, value)
|
||||
BMNXFS.writefile(bmdetails, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
|
||||
s = m:section(TypedSection, "banmac", "")
|
||||
s.anonymous = false
|
||||
s.addremove = true
|
||||
|
||||
s:tab("banmactab", translate("BanMac Menu"))
|
||||
|
||||
banlist_mac = s:taboption("banmactab", Value, "banlist_mac", translate("MAC address"))
|
||||
banlist_mac.rmempty = true
|
||||
banlist_mac.datatype = "macaddr"
|
||||
luci.sys.net.mac_hints(function(mac, name)
|
||||
banlist_mac:value(mac, "%s (%s)" %{ mac, name })
|
||||
end)
|
||||
|
||||
ban_mac = s:taboption("banmactab", Button, "ban_mac", translate("One-Click BAN"))
|
||||
ban_mac.rmempty = false
|
||||
ban_mac.inputstyle = "apply"
|
||||
function ban_mac.write(self, section)
|
||||
luci.util.exec("cp /usr/banmac/ban.sh /tmp/ban_OO!%!OO" ..section.. "_.sh >/dev/null 2>&1 &")
|
||||
luci.util.exec("/tmp/ban_OO!%!OO" ..section.. "_.sh >/dev/null 2>&1 &")
|
||||
end
|
||||
|
||||
unban_mac = s:taboption("banmactab", Button, "unban_mac", translate("One-Click UnBAN"))
|
||||
unban_mac.rmempty = false
|
||||
unban_mac.inputstyle = "apply"
|
||||
function unban_mac.write(self, section)
|
||||
luci.util.exec("cp /usr/banmac/unban.sh /tmp/unban_OO!%!OO" ..section.. "_.sh >/dev/null 2>&1 &")
|
||||
luci.util.exec("/tmp/unban_OO!%!OO" ..section.. "_.sh >/dev/null 2>&1 &")
|
||||
end
|
||||
|
||||
return m
|
||||
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.51
|
||||
PKG_RELEASE:=58
|
||||
PKG_RELEASE:=59
|
||||
|
||||
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
|
||||
@@ -1143,11 +1143,13 @@ enable_wan_logging() {
|
||||
|
||||
current=$(wan_zone_log_value "$zone")
|
||||
if wan_filter_log_enabled "$current"; then
|
||||
# Already-on with a lost baseline: reconstruct the usual first-enable
|
||||
# snapshot (unset), not the current on-value. Recording "1" would make
|
||||
# uninstall restore logging after disable. Skip-if-exists in
|
||||
# the helper. Failure must not become baseline_snapshot_failed.
|
||||
maybe_snapshot_wan_log_baseline "$zone" "" || true
|
||||
# Already-on with a lost baseline: reconstruct the value before this
|
||||
# package's owned filter-log bit was enabled. Preserve other log-mask
|
||||
# bits so uninstall cannot erase unrelated logging settings. The helper
|
||||
# skips an existing baseline. Failure stays best-effort and must not
|
||||
# become baseline_snapshot_failed.
|
||||
baseline=$(wan_filter_log_clear_value "$current")
|
||||
maybe_snapshot_wan_log_baseline "$zone" "$baseline" || true
|
||||
if ! wan_log_generation_bump >/dev/null; then
|
||||
release_wan_log_lock
|
||||
wan_log_tracking_failed_json "$zone_json"
|
||||
|
||||
@@ -18,7 +18,7 @@ LUCI_DEPENDS:= \
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy-pro
|
||||
PKG_VERSION:=28.10.1.14
|
||||
PKG_RELEASE:=18
|
||||
PKG_RELEASE:=19
|
||||
|
||||
LUCI_BASENAME:=homeproxy-pro
|
||||
|
||||
|
||||
@@ -92,17 +92,16 @@ return baseclass.extend({
|
||||
|
||||
/* Parse sing-box's rule-set download failures out of a log.
|
||||
*
|
||||
* With `ruleset_safe_start` on, a remote rule-set that cannot be
|
||||
* downloaded is no longer a failed start: sing-box reads the `initial_path`
|
||||
* fallback, comes up, and retries in the background, logging
|
||||
* A remote rule-set is fetched during initialization, before the inbounds
|
||||
* bind, so a failure there is a failed start:
|
||||
*
|
||||
* 2026-10-03 01:15:42 ERROR router: fetch rule-set geoip-cn: Get "https://…": dial tcp …
|
||||
* 2026-10-03 01:15:42 ERROR router: fetch rule-set example: Get "https://…": dial tcp …
|
||||
*
|
||||
* That is a good trade, and it is also invisible: the instance is healthy,
|
||||
* the health gate passes, the LAN is proxied - and the rule-set that is
|
||||
* supposed to keep mainland traffic direct matches NOTHING, so all of it
|
||||
* goes to `final`. The status page is the only place a user looks without
|
||||
* ssh, so it has to show this.
|
||||
* That is loud - but only in a log, and only for the rule-sets a user
|
||||
* defined themselves (the built-in China split is local files, nothing to
|
||||
* download). A start that failed leaves no running instance and no status
|
||||
* page, so the reason is readable only over ssh. Surfacing it here is the
|
||||
* whole job.
|
||||
*
|
||||
* Two deliberate choices about the format:
|
||||
*
|
||||
|
||||
@@ -77,25 +77,19 @@ const ensureLogPoll = hp.statusPoller({
|
||||
|
||||
/* --- rule-set download failures ------------------------------------------
|
||||
*
|
||||
* With ruleset_safe_start on, a remote rule-set that cannot be downloaded no
|
||||
* longer stops the service: sing-box reads the empty initial_path fallback,
|
||||
* binds, and retries in the background. The instance is healthy and the LAN is
|
||||
* proxied - and the rule-set that keeps mainland traffic direct matches
|
||||
* nothing, so all of it goes to `final`.
|
||||
*
|
||||
* That is the trade, and it is the reason this block exists: the degradation
|
||||
* has no other symptom. The health gate does not look at rule-sets at all
|
||||
* (there is nothing in health.sh that could), so the service reports itself
|
||||
* healthy while a rule-set is silently empty.
|
||||
* A remote rule-set that cannot be downloaded stops the service: sing-box
|
||||
* fetches it during initialization, before the inbounds bind, so a failure
|
||||
* there is a failed start. That makes it a loud problem, and this block is
|
||||
* here to make it legible rather than to solve it - a start that failed leaves
|
||||
* no instance, so sing-box-c.log is the only place the reason is written down.
|
||||
*
|
||||
* sing-box-c.log is read rather than a new RPC: the ACL already grants the
|
||||
* browser read on that file for the log viewer below, so this costs no
|
||||
* permission and no new method to keep in step with the method table.
|
||||
*
|
||||
* Whether a fallback is actually in play is read from UCI rather than guessed:
|
||||
* with the opt-in OFF, a failing rule-set means the instance would not have
|
||||
* started at all, which is a different and much more visible problem - so the
|
||||
* wording has to differ, not just the colour. */
|
||||
* The built-in China split is not among the rule-sets this can be about: those
|
||||
* are local files the resource updater maintains, generated from the same lists
|
||||
* the nft sets are rendered from, and there is nothing to download. */
|
||||
const ruleSetNode = E('div', { 'id': 'ruleset_status' },
|
||||
E('img', {
|
||||
'src': L.resource('icons/loading.svg'),
|
||||
@@ -112,7 +106,6 @@ function renderRuleSetStatus() {
|
||||
fs.read_direct(`${hp_dir}/sing-box-c.log`, 'text')
|
||||
.then((text) => {
|
||||
const failures = hp.parseRuleSetFetchFailures(text);
|
||||
const safeStart = (uci.get('homeproxy-pro', 'config', 'ruleset_safe_start') || '0') === '1';
|
||||
|
||||
if (!failures.length) {
|
||||
dom.content(ruleSetNode, E('span', { 'style': 'color:green' },
|
||||
@@ -125,9 +118,7 @@ function renderRuleSetStatus() {
|
||||
_('%d rule-set(s) cannot be downloaded.').format(failures.length)
|
||||
]),
|
||||
E('p', { 'style': 'color:gray' }, [
|
||||
safeStart
|
||||
? _('Startup fallback in use: these rule-sets are EMPTY, so the traffic they would have split falls through to the default route. The service is up, but the split is not in effect until the download succeeds.')
|
||||
: _('Startup fallback is OFF, so a rule-set that cannot be downloaded prevents the service from starting at all. The service is running, so the log below is from an earlier attempt or the download has since recovered.')
|
||||
_('A remote rule-set is fetched before the service starts, so a rule-set that cannot be downloaded prevents the start. The service is running, so the log below is from an earlier attempt or the download has since recovered.')
|
||||
]),
|
||||
E('ul', {}, failures.map((f) => E('li', [
|
||||
E('code', [ f.tag ]),
|
||||
@@ -386,6 +377,50 @@ return view.extend({
|
||||
return ruleSetNode;
|
||||
};
|
||||
|
||||
/* A dedicated section rather than a fourth log view: the question this
|
||||
* answers is "collect everything someone would ask me to look at, in
|
||||
* one file", and the answer is a download rather than something to read
|
||||
* on the page. The report is assembled by the backend because the
|
||||
* commands that fill it (nft, ip rule, the package queries) are not
|
||||
* browser-reachable; the ACL grants read on the output path only.
|
||||
*
|
||||
* The title is "Diagnostic report" and not the more natural
|
||||
* "Diagnostics" on purpose: a shipped zh-cn catalog somewhere in the
|
||||
* LuCI stack already renders a bare "Diagnostics" as 网络诊断, which
|
||||
* reads as a network diagnostic tool rather than a generated file.
|
||||
* A msgid this specific cannot collide with another package's. */
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy-pro', _('Diagnostic report'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_debug_report');
|
||||
o.rawhtml = true;
|
||||
o.render = function() {
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('p', {}, _('Collects system, dependency, routing, firewall and configuration state into one file. Credentials are masked by option name; public addresses and LAN topology are not. Read it before posting it anywhere.')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return hp.rpcCall('debug_report', [], { expect: { '': {} } })
|
||||
.then((res) => {
|
||||
if (!res.result)
|
||||
throw new Error(res.error || _('Could not build the report.'));
|
||||
return fs.read_direct(res.path, 'blob');
|
||||
})
|
||||
.then((blob) => {
|
||||
const url = window.URL.createObjectURL(blob, { type: 'text/markdown' });
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'homeproxy-pro-debug.log';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
});
|
||||
})
|
||||
}, [ _('Generate and download') ])
|
||||
]);
|
||||
};
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy-pro');
|
||||
s.anonymous = true;
|
||||
|
||||
|
||||
@@ -441,6 +441,15 @@ msgstr ""
|
||||
msgid "Collecting data..."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:408
|
||||
msgid ""
|
||||
"Collects system, dependency, routing, firewall and configuration "
|
||||
"state into one file. Credentials are masked by option name; public "
|
||||
"addresses and LAN topology are not. Read it before posting it "
|
||||
"anywhere."
|
||||
msgstr ""
|
||||
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/client/routing.js:105
|
||||
msgid "Common ports only (bypass P2P traffic)"
|
||||
msgstr ""
|
||||
@@ -461,6 +470,11 @@ msgstr ""
|
||||
msgid "Conservative"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:415
|
||||
msgid "Could not build the report."
|
||||
msgstr ""
|
||||
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/node.js:823
|
||||
msgid "Could not start subscription update: %s"
|
||||
msgstr ""
|
||||
@@ -569,6 +583,12 @@ msgstr ""
|
||||
msgid "Default timeout per DNS query in seconds (sing-box default: 10)."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:401
|
||||
msgid "Diagnostic report"
|
||||
msgstr "诊断报告"
|
||||
|
||||
|
||||
|
||||
#: htdocs/luci-static/resources/homeproxy-pro.js:217
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/client/common.js:335
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/client/nodes.js:97
|
||||
@@ -1048,6 +1068,11 @@ msgstr ""
|
||||
msgid "Generate"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:429
|
||||
msgid "Generate and download"
|
||||
msgstr ""
|
||||
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:357
|
||||
msgid "GitHub token"
|
||||
msgstr ""
|
||||
|
||||
@@ -454,6 +454,15 @@ msgstr "Cloudflare"
|
||||
msgid "Collecting data..."
|
||||
msgstr "正在收集数据中..."
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:408
|
||||
msgid ""
|
||||
"Collects system, dependency, routing, firewall and configuration "
|
||||
"state into one file. Credentials are masked by option name; public "
|
||||
"addresses and LAN topology are not. Read it before posting it "
|
||||
"anywhere."
|
||||
msgstr "将系统、依赖、路由、防火墙和配置状态汇总到一个文件。凭据按选项名脱敏,但公网地址和内网拓扑不会脱敏。发到任何地方之前请先阅读。"
|
||||
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/client/routing.js:105
|
||||
msgid "Common ports only (bypass P2P traffic)"
|
||||
msgstr "仅常用端口(绕过 P2P 流量)"
|
||||
@@ -474,6 +483,11 @@ msgstr "连接复用"
|
||||
msgid "Conservative"
|
||||
msgstr "保守"
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:415
|
||||
msgid "Could not build the report."
|
||||
msgstr "无法生成诊断报告。"
|
||||
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/node.js:823
|
||||
msgid "Could not start subscription update: %s"
|
||||
msgstr "无法启动订阅更新:%s"
|
||||
@@ -582,6 +596,12 @@ msgstr "默认服务器名称"
|
||||
msgid "Default timeout per DNS query in seconds (sing-box default: 10)."
|
||||
msgstr "每次 DNS 查询的默认超时时间(秒,sing-box 默认为 10)。"
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:401
|
||||
msgid "Diagnostic report"
|
||||
msgstr "诊断报告"
|
||||
|
||||
|
||||
|
||||
#: htdocs/luci-static/resources/homeproxy-pro.js:217
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/client/common.js:335
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/client/nodes.js:97
|
||||
@@ -1077,6 +1097,11 @@ msgstr "Gecko(1.14)"
|
||||
msgid "Generate"
|
||||
msgstr "生成"
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:429
|
||||
msgid "Generate and download"
|
||||
msgstr "生成并下载"
|
||||
|
||||
|
||||
#: htdocs/luci-static/resources/view/homeproxy-pro/status.js:357
|
||||
msgid "GitHub token"
|
||||
msgstr "GitHub 令牌"
|
||||
|
||||
@@ -55,7 +55,6 @@ config homeproxy-pro 'config'
|
||||
# With it off, a cold cache on a fresh install waits for
|
||||
# raw.githubusercontent.com (through the selected node) before the instance
|
||||
# will listen at all, and a failure there fails the whole start.
|
||||
option ruleset_safe_start '0'
|
||||
option tun_dns_mode ''
|
||||
option udp_mapping ''
|
||||
option udp_filtering ''
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"rules":[]}
|
||||
Binary file not shown.
@@ -404,7 +404,6 @@ export const Loader = {
|
||||
* configuration generated cleanly, `sing-box check` passed, and the
|
||||
* feature simply never turned on. Guard 53 now compares the two
|
||||
* lists so that class cannot come back quietly. */
|
||||
ruleset_safe_start: opt(uci, 'main', 'ruleset_safe_start'),
|
||||
main_urltest_nodes: opt(uci, 'main', 'main_urltest_nodes') || [],
|
||||
main_urltest_interval: opt(uci, 'main', 'main_urltest_interval'),
|
||||
main_urltest_tolerance: opt(uci, 'main', 'main_urltest_tolerance'),
|
||||
|
||||
@@ -59,6 +59,49 @@ function resolve_ipv6(str) {
|
||||
return ipv6.addr;
|
||||
}
|
||||
|
||||
/* The node server addresses the generator exported (generate_client.uc's
|
||||
* export_node_addresses()), as an nftables anonymous set - or null when there
|
||||
* is nothing to protect.
|
||||
*
|
||||
* Only the literal-address form is rendered here. A node configured with a
|
||||
* host name reaches the same set at runtime: the dnsmasq snippet runtime/dns.sh
|
||||
* writes resolves the name and back-fills the set through `nftset=`, so one
|
||||
* mechanism covers both without this template needing a resolver of its own.
|
||||
* A literal address has no name to resolve, so it can only come from here.
|
||||
*
|
||||
* readfile() rather than a UCI read on purpose: the generator is the only
|
||||
* place that knows which nodes are actually in use (main_node, main_udp_node,
|
||||
* urltest membership, custom-mode routing_nodes, wireguard endpoints), and a
|
||||
* copy of that selection in this template is a copy that can drift. A missing
|
||||
* file renders null, which is the behaviour before this existed. */
|
||||
function node_addr_to_nftarr(run_dir, family) {
|
||||
let raw = readfile(run_dir + '/node-addr-ips.txt');
|
||||
if (isEmpty(raw))
|
||||
return null;
|
||||
|
||||
let out = [];
|
||||
for (let entry in split(trim(raw), /[\r\n]/)) {
|
||||
let parsed = fw4.parse_subnet(entry)?.[0];
|
||||
if (!parsed || parsed.family !== family)
|
||||
continue;
|
||||
push(out, (parsed.bits > -1) ? `${parsed.addr}/${parsed.bits}` : parsed.addr);
|
||||
}
|
||||
|
||||
return isEmpty(out) ? null : array_to_nftarr(out);
|
||||
}
|
||||
|
||||
/* Whether the node bypass has anything to protect at all.
|
||||
|
||||
Either export file counts. The domains file is the one that matters most:
|
||||
a host-name node - the common case, and the one the device this was written
|
||||
for uses - appears only there, and its set has to exist before dnsmasq can
|
||||
put anything in it. Gating on the literal-address file instead is what left
|
||||
the whole mechanism doing nothing. */
|
||||
function node_addr_protected(run_dir) {
|
||||
return !isEmpty(readfile(run_dir + '/node-addr-ips.txt'))
|
||||
|| !isEmpty(readfile(run_dir + '/node-addr-domains.txt'));
|
||||
}
|
||||
|
||||
/* Normalize a list of IPv6 entries into an nftables anonymous set
|
||||
(or null when empty), so IPv6 ACLs match in a single set lookup
|
||||
instead of one rule per address. */
|
||||
@@ -140,6 +183,26 @@ function dnsmasq_nftset() {
|
||||
|
||||
/* Misc config */
|
||||
const resources_dir = '/etc/homeproxy-pro/resources';
|
||||
const run_dir = '/var/run/homeproxy-pro';
|
||||
|
||||
/* Resolved here, in the ucode block, rather than with `{% set %}` in the
|
||||
* template body below: utpl's `set` tag takes a plain expression and rejects a
|
||||
* call outright - `{% set x = f(); %}` is a parse error ("Unexpected token,
|
||||
* Expecting ';'"). A parse error kills the whole render, fw4_post.nft comes
|
||||
* out empty, and the health gate silently keeps the previous ruleset, so a
|
||||
* template that fails this way does not look broken at all. r41 shipped
|
||||
* `{% set node_addr_v4 = node_addr_to_nftarr(4); %}` and never rendered once.
|
||||
*
|
||||
* Both helpers take run_dir as a parameter rather than closing over it. In
|
||||
* this ucode a closure captures the block variables that already exist where
|
||||
* the function is DEFINED, not where it is called: a function defined above
|
||||
* `const run_dir` cannot see run_dir even when it is called far below, and
|
||||
* the error is "Reference error: access to undeclared variable run_dir" -
|
||||
* which reads as though run_dir were missing, when it is in fact declared 80
|
||||
* lines above the call. Passing it in is the only spelling that works. */
|
||||
const node_protect = node_addr_protected(run_dir);
|
||||
const node_addr_v4 = node_addr_to_nftarr(run_dir, 4);
|
||||
const node_addr_v6 = node_addr_to_nftarr(run_dir, 6);
|
||||
|
||||
/* UCI config start */
|
||||
const cfgname = 'homeproxy-pro';
|
||||
@@ -399,6 +462,42 @@ set homeproxy_routing_port {
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# Node server addresses.
|
||||
Declared when the generator exported any node address at all - and NOT
|
||||
only when there is a literal address to put in it.
|
||||
|
||||
That distinction is the whole point of the set existing. A node configured
|
||||
with a host name contributes no element here; dnsmasq resolves the name and
|
||||
back-fills this set through the nftset= snippet runtime/dns.sh writes. A
|
||||
set that is declared only when it already has elements is therefore never
|
||||
declared for a host-name node, the back-fill has nothing to land in, and the
|
||||
return rules below - which are what actually exclude the address - are not
|
||||
emitted either. r41 gated both on the literal form and was inert for the
|
||||
configuration it was written for.
|
||||
|
||||
An empty set is harmless: nft accepts one with no elements, and the cost is
|
||||
one lookup per packet until the first answer back-fills it. #}
|
||||
{% if (node_protect): %}
|
||||
set homeproxy_node_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (node_addr_v4): %}
|
||||
elements = { {{ node_addr_v4 }} }
|
||||
{% endif %}
|
||||
}
|
||||
{% endif %}
|
||||
{% if (node_protect && v6_handled): %}
|
||||
set homeproxy_node_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (node_addr_v6): %}
|
||||
elements = { {{ node_addr_v6 }} }
|
||||
{% endif %}
|
||||
}
|
||||
{% endif /* node_addr_v6 */ %}
|
||||
|
||||
{# DNS hijack & TCP redirect #}
|
||||
chain dstnat {
|
||||
{% if ((dns_redirect === '1') && (dns_hijacked !== '1')): %}
|
||||
@@ -461,6 +560,13 @@ chain homeproxy_redirect_lanac {
|
||||
chain homeproxy_redirect {
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (node_protect): %}
|
||||
ip daddr @homeproxy_node_addr_v4 counter return comment "!{{ cfgname }}: node address"
|
||||
{% endif %}
|
||||
{% if (node_protect && v6_handled): %}
|
||||
ip6 daddr @homeproxy_node_addr_v6 counter return comment "!{{ cfgname }}: node address"
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port
|
||||
{% if (v6_handled): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port
|
||||
@@ -582,6 +688,13 @@ chain homeproxy_mangle_lanac {
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_prerouting {
|
||||
{% if (node_protect): %}
|
||||
ip daddr @homeproxy_node_addr_v4 counter return comment "!{{ cfgname }}: node address"
|
||||
{% endif %}
|
||||
{% if (node_protect && v6_handled): %}
|
||||
ip6 daddr @homeproxy_node_addr_v6 counter return comment "!{{ cfgname }}: node address"
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port
|
||||
{% if (v6_handled): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port
|
||||
@@ -646,6 +759,13 @@ chain homeproxy_mangle_prerouting {
|
||||
chain homeproxy_mangle_output {
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (node_protect): %}
|
||||
ip daddr @homeproxy_node_addr_v4 counter return comment "!{{ cfgname }}: node address"
|
||||
{% endif %}
|
||||
{% if (node_protect && v6_handled): %}
|
||||
ip6 daddr @homeproxy_node_addr_v6 counter return comment "!{{ cfgname }}: node address"
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark
|
||||
{% if (v6_handled): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark
|
||||
|
||||
@@ -44,5 +44,7 @@ HP_FW4_SETS="
|
||||
homeproxy_wan_proxy_addr_v6
|
||||
homeproxy_wan_direct_addr_v4
|
||||
homeproxy_wan_direct_addr_v6
|
||||
homeproxy_node_addr_v4
|
||||
homeproxy_node_addr_v6
|
||||
homeproxy_routing_port
|
||||
"
|
||||
@@ -29,8 +29,8 @@ import { lstat, mkdtemp, readfile, writefile } from 'fs';
|
||||
|
||||
import { Loader } from './config/loader.uc';
|
||||
import { generate } from './generator/client.uc';
|
||||
import { BUILTIN_REMOTE_RULE_SETS, declaresBuiltinRemoteRuleSets, rule_set_tags } from './generator/common.uc';
|
||||
import { removeBlankAttrs, isEmpty, probeRuleSetFile, ruleSetFormatExtension, ruleSetFormatFromPath, ruleSetInitialFallback, RULESET_EMPTY_BINARY, RULESET_EMPTY_SOURCE, RULESET_INITIAL_DIR, shellQuote, validateRuleSetPath, HP_DIR, RUN_DIR, UCICONFIG_DIR } from './homeproxy-pro.uc';
|
||||
import { rule_set_tags } from './generator/common.uc';
|
||||
import { removeBlankAttrs, isEmpty, isValidCIDR, probeRuleSetFile, ruleSetFormatFromPath, shellQuote, validateRuleSetPath, validation, HP_DIR, RUN_DIR, UCICONFIG_DIR } from './homeproxy-pro.uc';
|
||||
|
||||
/* Resolve the GenerationContext inputs. This is the only impure step on the
|
||||
* client generation path, and it is deliberately here rather than under
|
||||
@@ -46,69 +46,87 @@ import { removeBlankAttrs, isEmpty, probeRuleSetFile, ruleSetFormatExtension, ru
|
||||
*
|
||||
* The extra parentheses around the ubus call keep the ?. chain guarded when
|
||||
* connect() returns null. */
|
||||
/* Write one EMPTY rule-set to <path>, in <format>.
|
||||
|
||||
/* Normalise one server address into `host`, or return null when there is
|
||||
* nothing usable. WireGuard peers and the plain outbounds disagree on the
|
||||
* field name and the IPv6 form, so this is the only place that knows both. */
|
||||
function node_host_of(v) {
|
||||
if (isEmpty(v))
|
||||
return null;
|
||||
|
||||
let host = trim(v);
|
||||
|
||||
/* `[fdfe::1]` is how an IPv6 literal arrives from a UCI value; the set
|
||||
* syntax wants the bare address. */
|
||||
if (length(host) > 2 && substr(host, 0, 1) === '[' && substr(host, -1, 1) === ']')
|
||||
host = substr(host, 1, -2);
|
||||
|
||||
return isEmpty(host) ? null : host;
|
||||
}
|
||||
|
||||
/* Export the node server addresses, split by form, into two files the
|
||||
* intercept layer reads:
|
||||
*
|
||||
* The fallback exists so that a remote rule-set with no initial file of the
|
||||
* user's own does not have to be downloaded before the inbounds bind - and on
|
||||
* a cold cache that is the difference between a router that comes up and one
|
||||
* that waits on raw.githubusercontent.com (through the node) before it will
|
||||
* listen at all.
|
||||
* RUN_DIR/node-addr-ips.txt literal addresses, rendered straight into
|
||||
* the nft set by firewall_post.ut
|
||||
* RUN_DIR/node-addr-domains.txt host names, handed to dnsmasq as
|
||||
* `server=` + `nftset=` so the addresses they
|
||||
* resolve to land in the same set
|
||||
*
|
||||
* binary is produced by compiling, not by writing bytes: a file produced by
|
||||
* the sing-box that is running cannot drift from it, and the compiler is
|
||||
* always there because the very next thing this script does is run it. The
|
||||
* shipped 14-byte constant is the fallback for the fallback - a router where
|
||||
* the compile cannot run - and it is a file in the package rather than a byte
|
||||
* string in the source, so `hexdump -C` on a device can confirm it.
|
||||
* Why the set exists at all: sing-box's own outbound already carries
|
||||
* `routing_mark: self_mark` and every chain returns on that mark, so the
|
||||
* tunnel's connection to the node is never re-intercepted. The LAN's is not.
|
||||
* A client opening a connection to the node's own address - the panel, an SSH
|
||||
* session to the landing box, anything on a port the proxy covers - was
|
||||
* redirected into sing-box, which then dialled the very address it was asked
|
||||
* for through the tunnel to that same node: one extra hairpin hop, and the
|
||||
* node sees the connection arriving from itself.
|
||||
*
|
||||
* Returns true when <path> holds a usable file afterwards. A false return is
|
||||
* not fatal: the caller skips the initial_path, and the rule-set then behaves
|
||||
* exactly as it does today (fetched during initialization, blocking on
|
||||
* failure). That is the point of the whole opt-in - it must never make a
|
||||
* situation worse than not having it. */
|
||||
function writeEmptyRuleSet(path, format) {
|
||||
const ext = ruleSetFormatExtension(format);
|
||||
* The generator is the only place that knows which nodes are actually in use
|
||||
* (main_node, main_udp_node, urltest membership, custom-mode routing_nodes,
|
||||
* wireguard endpoints), so it is the only place that can produce this list
|
||||
* without the firewall template re-deriving that selection and drifting.
|
||||
*
|
||||
* Failure is not fatal. A missing file means the set renders empty, which is
|
||||
* exactly the behaviour before this existed. */
|
||||
function export_node_addresses(config) {
|
||||
const addrs = [];
|
||||
const domains = [];
|
||||
|
||||
if (!ext)
|
||||
return false;
|
||||
const add = (v) => {
|
||||
const host = node_host_of(v);
|
||||
if (host === null)
|
||||
return;
|
||||
if (isValidCIDR(host, 4) || isValidCIDR(host, 6))
|
||||
push(addrs, host);
|
||||
else if (validation('hostname', host))
|
||||
push(domains, host);
|
||||
};
|
||||
|
||||
system('mkdir -p ' + shellQuote(RULESET_INITIAL_DIR));
|
||||
for (let ob in (config.outbounds || []))
|
||||
add(ob.server);
|
||||
|
||||
if (format === 'source') {
|
||||
/* No compile needed, and no temp file: the source form is the JSON
|
||||
* itself. The shipped empty.source.json is the source of record, so
|
||||
* a missing one means the install is incomplete and the copy is the
|
||||
* thing that says so. */
|
||||
if (system('cp -f ' + shellQuote(RULESET_EMPTY_SOURCE) + ' ' + shellQuote(path)) !== 0)
|
||||
return false;
|
||||
} else {
|
||||
/* Compile into the destination directly. A partial output is
|
||||
* possible if the compile dies, so the file is only accepted when the
|
||||
* compile succeeded AND something is there - sing-box opening a
|
||||
* truncated .srs would block startup, which is the outcome this is
|
||||
* all meant to prevent.
|
||||
*
|
||||
* `-o` rather than `--output`, because that is the form
|
||||
* tests/ucode/test_generators.sh already exercises against this
|
||||
* binary, and a flag spelling that only ever runs in production is a
|
||||
* flag spelling nobody has ever seen work. */
|
||||
if (system('sing-box rule-set compile ' + shellQuote(RULESET_EMPTY_SOURCE)
|
||||
+ ' -o ' + shellQuote(path) + ' >/dev/null 2>&1') !== 0) {
|
||||
if (system('cp -f ' + shellQuote(RULESET_EMPTY_BINARY) + ' ' + shellQuote(path)) !== 0)
|
||||
return false;
|
||||
}
|
||||
/* WireGuard moved from `outbounds` to `endpoints` in 1.13, and the peer
|
||||
* address is the node there. */
|
||||
for (let ep in (config.endpoints || []))
|
||||
for (let peer in (ep.peers || []))
|
||||
add(peer.address);
|
||||
|
||||
const st = lstat(path);
|
||||
if (!st || st.type !== 'file' || st.size <= 0)
|
||||
return false;
|
||||
}
|
||||
const dump = (path, list) => {
|
||||
if (isEmpty(list))
|
||||
return;
|
||||
/* ucode's join() takes the separator FIRST: join(sep, list). With
|
||||
* the arguments the other way round it does not raise, it returns
|
||||
* null - and null + '\n' is the four-character string "null\n", so
|
||||
* the file comes out non-empty and every "is it there?" check on
|
||||
* it passes. That is exactly how a released r41 ended up with
|
||||
* dnsmasq resolving a host literally named "null". */
|
||||
if (writefile(path, join('\n', uniq(list)) + '\n') == null)
|
||||
warn(sprintf('homeproxy-pro: could not write %s\n', path));
|
||||
};
|
||||
|
||||
/* The jailed client reads this as the sing-box user. writefile() has no
|
||||
* mode argument and cp preserves the source's, so the mode is set
|
||||
* explicitly rather than inherited from whatever umask was in force. */
|
||||
system('chmod 644 ' + shellQuote(path));
|
||||
|
||||
return true;
|
||||
dump(RUN_DIR + '/node-addr-ips.txt', addrs);
|
||||
dump(RUN_DIR + '/node-addr-domains.txt', domains);
|
||||
}
|
||||
|
||||
function resolve_env(dm) {
|
||||
@@ -147,6 +165,11 @@ function resolve_env(dm) {
|
||||
* argument to get wrong - and because the neighbouring stderr-size
|
||||
* check in homeproxy-pro.uc already reads sizes through it. */
|
||||
china_ip6_ready: lstat(HP_DIR + '/resources/china_ip6.json') !== null,
|
||||
/* The DNS half of the same split: whether china-domain.json is
|
||||
* there. Same reason, same lstat(), same one-sided default - a
|
||||
* missing file has to leave the DNS rule out rather than name a path
|
||||
* that does not exist and fail the whole start. */
|
||||
china_domain_ready: lstat(HP_DIR + '/resources/china-domain.json') !== null,
|
||||
/* Which enabled `type: local` rule-sets have a usable file on disk,
|
||||
* keyed by UCI section name.
|
||||
*
|
||||
@@ -205,7 +228,6 @@ function resolve_env(dm) {
|
||||
/* Which empty startup fallbacks are actually on disk, keyed by rule_set
|
||||
* tag: { '<tag>': true }. Presence, not a path - see the block at the
|
||||
* end of this function. */
|
||||
ruleset_initial: {}
|
||||
};
|
||||
|
||||
if (routing_mode === 'custom') {
|
||||
@@ -302,92 +324,6 @@ function resolve_env(dm) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ruleset_safe_start: write the empty fallbacks.
|
||||
*
|
||||
* Recorded as PRESENCE, not as a path: env.ruleset_initial[tag] is true
|
||||
* only once a file for that tag is on disk and readable. The generator
|
||||
* then emits `initial_path` for exactly the tags it can point at, which
|
||||
* is what keeps the two sides from disagreeing - a file that could not be
|
||||
* written is simply not offered, and that rule-set keeps today's
|
||||
* behaviour (fetched during initialization) instead of pointing at
|
||||
* something that is not there. sing-box ignores a missing initial_path
|
||||
* and blocks startup exactly as if none had been configured, so this
|
||||
* cannot make a broken install worse - but the point of the opt-in is
|
||||
* that it helps, and a pointer to a nonexistent file helps nobody.
|
||||
*
|
||||
* Built-ins first, then the user's own. Both come from the same helper
|
||||
* the generator uses, so the format the file was written in and the
|
||||
* format declared in the config are decided by one question asked once. */
|
||||
if (dm.general.ruleset_safe_start === '1') {
|
||||
if (declaresBuiltinRemoteRuleSets(routing_mode)) {
|
||||
for (let rs in BUILTIN_REMOTE_RULE_SETS) {
|
||||
const path = ruleSetInitialFallback([ rs.tag ], rs.format, rs.url);
|
||||
if (path && writeEmptyRuleSet(path, rs.format))
|
||||
env.ruleset_initial[rs.tag] = true;
|
||||
else
|
||||
warn(sprintf("homeproxy-pro: could not write the empty fallback for '%s'; this rule-set will be fetched during startup, as before.", rs.tag));
|
||||
}
|
||||
}
|
||||
|
||||
/* User rule-sets are custom-mode only - build_user_rulesets() is not
|
||||
* called in any other mode - so writing fallbacks for them anywhere
|
||||
* else would create files nothing ever points at. A rule-set with an
|
||||
* initial file of its own is left completely alone. */
|
||||
if (routing_mode === 'custom') {
|
||||
for (let cfg in (dm.routing.rulesets || [])) {
|
||||
if (!cfg.enabled || cfg.type !== 'remote' || !isEmpty(cfg.initial_path))
|
||||
continue;
|
||||
|
||||
const format = cfg.format || ruleSetFormatFromPath(cfg.url);
|
||||
const tags = rule_set_tags(cfg);
|
||||
const template = ruleSetInitialFallback(tags, format, cfg.url);
|
||||
|
||||
if (!template) {
|
||||
warn(sprintf("homeproxy-pro: rule-set '%s' has no usable format for the startup fallback (format '%s', url '%s'); it will be fetched during startup, as before.", cfg.name, cfg.format || '', cfg.url || ''));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let tag in tags) {
|
||||
if (env.ruleset_initial[tag])
|
||||
continue;
|
||||
|
||||
/* A single-tag rule-set's template IS the path; a
|
||||
* multi-tag one carries {tag} and needs one file each -
|
||||
* sing-box opens every tag's file, so a missing second
|
||||
* one blocks startup as surely as a missing first. */
|
||||
const path = (length(tags) > 1) ? replace(template, '{tag}', tag) : template;
|
||||
|
||||
if (writeEmptyRuleSet(path, format))
|
||||
env.ruleset_initial[tag] = true;
|
||||
else
|
||||
warn(sprintf("homeproxy-pro: could not write the empty fallback for rule-set '%s' (tag %s); it will be fetched during startup, as before.", cfg.name, tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* One line naming what is running on an empty fallback.
|
||||
*
|
||||
* The CLI already warns per rule-set when a fallback could not be written,
|
||||
* but "which rule-sets are currently on an EMPTY one" is the fact a user
|
||||
* needs and nothing else records it: the running configuration is not
|
||||
* readable by the browser, the health gate does not look at rule-sets, and
|
||||
* sing-box's own log only says the download failed. This line is the
|
||||
* durable answer, and it goes to homeproxy-pro.log, which the status page can
|
||||
* read with the permission it already has.
|
||||
*
|
||||
* Emitted only when the opt-in is on AND something actually got a
|
||||
* fallback, so the log does not gain a line on every generation of a
|
||||
* configuration that is not using the feature. */
|
||||
if (dm.general.ruleset_safe_start === '1' && !isEmpty(env.ruleset_initial)) {
|
||||
const tags = [];
|
||||
|
||||
for (let tag in env.ruleset_initial)
|
||||
push(tags, tag);
|
||||
|
||||
warn(sprintf("homeproxy-pro: ruleset_safe_start is on; these rule-sets have an EMPTY initial file and match nothing until their download succeeds: %s.", join(', ', tags)));
|
||||
}
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
const direct_list_raw = readfile(HP_DIR + '/resources/direct_list.txt');
|
||||
env.direct_domain_list = direct_list_raw ? split(trim(direct_list_raw), /[\r\n]/) : [];
|
||||
@@ -432,6 +368,11 @@ if (system('sing-box check --config ' + shellQuote(tmp)) !== 0) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* Only now, with a configuration sing-box has accepted: the firewall and the
|
||||
* dnsmasq snippets read the exported list, and they must never be pointed at
|
||||
* addresses from a configuration that does not start. */
|
||||
export_node_addresses(config);
|
||||
|
||||
if (system('mv -f ' + shellQuote(tmp) + ' ' + shellQuote(RUN_DIR) + '/sing-box-c.json') !== 0) {
|
||||
system('rm -rf ' + shellQuote(work_dir));
|
||||
exit(1);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { isEmpty } from '../homeproxy-pro.uc';
|
||||
import { isEmpty, HP_DIR } from '../homeproxy-pro.uc';
|
||||
|
||||
import { ConfigQuery } from '../config/model.uc';
|
||||
|
||||
@@ -142,43 +142,42 @@ export function rule_set_tags(cfg) {
|
||||
return tags;
|
||||
};
|
||||
|
||||
/* BUILTIN_REMOTE_RULE_SETS - the remote rule-sets the generator declares
|
||||
* itself, in the modes that split on mainland China.
|
||||
/* The routing modes that split on mainland China, and the rule-sets they
|
||||
* declare.
|
||||
*
|
||||
* geoip-cn referenced by the route rule to split destinations by IP
|
||||
* geosite-cn referenced only by the DNS rule (server: china-dns); the
|
||||
* route layer never matches it, but sing-box still loads and
|
||||
* keeps it, so removing it would break the DNS split
|
||||
* The three of them - china-ip, china-ip6, china-domain - are local files the
|
||||
* resource updater generates, one per list, and each is declared only when
|
||||
* something references it and its file is on disk. route.uc owns that
|
||||
* decision because the conditions are per-tag (the address list in both
|
||||
* mainland modes, the v6 half only with IPv6 on, the domain list only in
|
||||
* bypass mode), so there is nothing here for a shared list to keep in step
|
||||
* with.
|
||||
*
|
||||
* This list is shared with generate_client.uc, which has to create the
|
||||
* "do not block startup" fallback file for each of them before the generator
|
||||
* runs. The two used to be independent: route.uc hard-coded the entries and
|
||||
* the CLI knew nothing about them, which is why a cold-cache first start
|
||||
* blocked on downloading them and why a change to either side could not be
|
||||
* checked against the other. One list, two readers.
|
||||
* They were `type: remote` entries pointing at SagerNet's sing-geoip and
|
||||
* sing-geosite repositories, and two things followed from that which the local
|
||||
* arrangement does not have:
|
||||
*
|
||||
* Not a conffile-order dependency - this is the generator's own declaration,
|
||||
* in the order the generated rule_set array must keep so the emitted bytes
|
||||
* stay stable. */
|
||||
export const BUILTIN_REMOTE_RULE_SETS = [
|
||||
{
|
||||
tag: 'geoip-cn',
|
||||
format: 'binary',
|
||||
url: 'https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs',
|
||||
update_interval: '24h'
|
||||
},
|
||||
{
|
||||
tag: 'geosite-cn',
|
||||
format: 'binary',
|
||||
url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs',
|
||||
update_interval: '24h'
|
||||
}
|
||||
];
|
||||
|
||||
/* The routing modes that declare them. Kept beside the list so the CLI can
|
||||
* decide whether it needs to create their fallbacks without repeating the
|
||||
* condition route.uc branches on - and so the two cannot drift. */
|
||||
export function declaresBuiltinRemoteRuleSets(routing_mode) {
|
||||
* - a cold start had to download them before the inbounds bind, over the
|
||||
* node (download_detour), so a first start - or a start after the cache
|
||||
* was cleared - depended on GitHub being reachable through the proxy it
|
||||
* was trying to bring up;
|
||||
* - the two halves of the same split read two different files. The kernel's
|
||||
* came from china_ip4.txt and the resolver's from a .srs, and the route.uc
|
||||
* comment about them disagreeing is the scar of patching that instead of
|
||||
* fixing it.
|
||||
*
|
||||
* Now the nft set and the sing-box rule-set are both generated from the same
|
||||
* list, on the same schedule, with nothing to download.
|
||||
*
|
||||
* Source JSON, not a compiled .srs: the updater writes the canonical form and
|
||||
* sing-box watches the file, so a new list is picked up in place. Compiling
|
||||
* first was measured rather than assumed - 111k domain suffixes cost 3.7 MB of
|
||||
* RSS in source form (44.9 MB baseline vs 48.7 MB loaded), which is not worth
|
||||
* a second artifact and a second failure mode. */
|
||||
/* The routing modes that split on mainland China. The China domain list is
|
||||
* only meaningful where the split is by domain: proxy_mainland_china decides
|
||||
* by address on the route side and leaves the resolver to the mode default. */
|
||||
export function declaresBuiltinRuleSets(routing_mode) {
|
||||
return routing_mode === 'bypass_mainland_china' || routing_mode === 'proxy_mainland_china';
|
||||
};
|
||||
|
||||
|
||||
@@ -138,6 +138,11 @@ export function build_context(dm, env) {
|
||||
* whole config. Default false - an absent env must not turn into an
|
||||
* optimistic "assume the file is there". */
|
||||
china_ip6_ready: (env?.china_ip6_ready === true),
|
||||
/* Whether china-domain.json is on disk. Same one-sided default as
|
||||
* the field above, for the same reason: it is a filesystem fact the
|
||||
* CLI resolves, and an absent env must not be read as "assume the
|
||||
* file is there". */
|
||||
china_domain_ready: (env?.china_domain_ready === true),
|
||||
/* Which enabled `type: local` rule-sets have a usable file on disk,
|
||||
* keyed by UCI section name: { '<section>': true }.
|
||||
*
|
||||
@@ -256,29 +261,6 @@ export function build_context(dm, env) {
|
||||
* Default '0' preserves the
|
||||
* 300ms / default-list behaviour so an upgrade is invisible. */
|
||||
sniffer_advanced_mode: dm.general.sniffer_advanced_mode || '0',
|
||||
/* ruleset_safe_start: when '1' (opt-in), every enabled remote
|
||||
* rule-set that has no initial_path of its own gets one, pointing at
|
||||
* an EMPTY rule-set the generator writes before the config is
|
||||
* checked. A remote rule-set with no initial file is fetched during
|
||||
* initialization, before the inbounds bind, so on a cold cache the
|
||||
* first start depends on the CDN being reachable - and on the node,
|
||||
* because the fetch goes through http_clients.
|
||||
*
|
||||
* The fallback is empty on purpose and that is the trade: a rule-set
|
||||
* with no rules matches nothing, so its traffic falls through to
|
||||
* `final`. That is a routing change, not a robustness tweak, which
|
||||
* is why the default is '0' and why the whole repository stays
|
||||
* fail-loud by default. Guard 52 pins the default, the way guard 39
|
||||
* pins sniffer_advanced_mode's. */
|
||||
ruleset_safe_start: dm.general.ruleset_safe_start || '0',
|
||||
/* Which empty startup fallbacks the CLI actually wrote, keyed by
|
||||
* rule_set tag. Presence, not a path: the generator emits
|
||||
* `initial_path` only for tags it can point at, so a fallback that
|
||||
* could not be written leaves that rule-set on today's behaviour
|
||||
* rather than pointing at a file that is not there. Pessimistic by
|
||||
* construction - an env that forgot the field (the server path) has
|
||||
* no fallbacks, and the server declares no remote rule-sets anyway. */
|
||||
ruleset_initial: (env?.ruleset_initial || {}),
|
||||
main_urltest_nodes: dm.general.main_urltest_nodes || [],
|
||||
main_urltest_interval: dm.general.main_urltest_interval,
|
||||
main_urltest_tolerance: dm.general.main_urltest_tolerance,
|
||||
|
||||
@@ -278,11 +278,22 @@ function append_proxy_dns(config, dm, ctx) {
|
||||
server: 'default-dns'
|
||||
});
|
||||
|
||||
push(config.dns.rules, {
|
||||
rule_set: 'geosite-cn',
|
||||
action: 'route',
|
||||
server: 'china-dns'
|
||||
});
|
||||
/* The China domain list, generated from china_list.txt - the same
|
||||
* schedule and the same updater as the address list the route and
|
||||
* firewall halves read. It replaced geosite-geolocation-cn.srs,
|
||||
* which had to be downloaded before the inbounds could bind.
|
||||
*
|
||||
* Only with the file there: a rule naming a tag nothing declares
|
||||
* fails the config, and a rule-set whose path is missing fails it
|
||||
* too, so both the declaration (route.uc) and this reference are
|
||||
* gated on the same lstat. */
|
||||
if (ctx.china_domain_ready) {
|
||||
push(config.dns.rules, {
|
||||
rule_set: 'china-domain',
|
||||
action: 'route',
|
||||
server: 'china-dns'
|
||||
});
|
||||
}
|
||||
|
||||
/* sing-box 1.14: restore CN-IP fallback via evaluate/match_response (opt-in) */
|
||||
if (ctx.cn_ip_fallback === '1') {
|
||||
@@ -292,18 +303,17 @@ function append_proxy_dns(config, dm, ctx) {
|
||||
tag: 'cn-fallback'
|
||||
});
|
||||
/* The response is matched by the addresses the upstream returned,
|
||||
* and geoip-cn has no IPv6 in it, so with IPv6 support on a
|
||||
* mainland domain that resolved to an AAAA address was never
|
||||
* recognised as mainland and its answer kept coming from the
|
||||
* proxy resolver. Adding the v6 rule-set here is what makes the
|
||||
* DNS half of the split agree with the route half, which
|
||||
* classifies the same destinations through the same list
|
||||
* (route.uc: china-ip6). Both tags have to be declared before
|
||||
* this rule runs, which is why the list is built here rather than
|
||||
* inlined: a rule_set naming an undeclared tag fails the config.
|
||||
* china-ip6 is only declared when its file exists, so it is only
|
||||
* named here under the same condition. */
|
||||
const cn_fallback_sets = ['geoip-cn'];
|
||||
* and the address list has to be the one the route half uses for
|
||||
* the same decision to agree with it - so this names china-ip, not
|
||||
* a list of its own. It is the same reason the declaration lives
|
||||
* in common.uc.
|
||||
*
|
||||
* china-ip6 is added only when it is declared: the rule_set list
|
||||
* has to name declared tags only, or the config is rejected.
|
||||
* Without it a mainland domain that resolved to an AAAA address
|
||||
* is never recognised as mainland and its answer keeps coming
|
||||
* from the proxy resolver. */
|
||||
const cn_fallback_sets = ['china-ip'];
|
||||
if (ctx.ipv6_support === '1' && ctx.china_ip6_ready)
|
||||
push(cn_fallback_sets, 'china-ip6');
|
||||
push(config.dns.rules, {
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { isEmpty, strToInt, strToTime, strToBool, parse_port, ruleSetInitialFallback, HP_DIR } from '../homeproxy-pro.uc';
|
||||
import { isEmpty, strToInt, strToTime, strToBool, parse_port, HP_DIR } from '../homeproxy-pro.uc';
|
||||
|
||||
import { BUILTIN_REMOTE_RULE_SETS, declaresBuiltinRemoteRuleSets, get_outbound, get_resolver, get_ruleset, get_direct_override } from './common.uc';
|
||||
import { declaresBuiltinRuleSets, get_outbound, get_resolver, get_ruleset, get_direct_override } from './common.uc';
|
||||
|
||||
/* --- shared initial block (every routing mode) ------------------------- */
|
||||
|
||||
@@ -93,10 +93,10 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
|
||||
outbound: 'direct-out'
|
||||
});
|
||||
|
||||
/* Split by destination IP, in both directions.
|
||||
/* Split by destination address, in both directions.
|
||||
*
|
||||
* sing-box does not match an IP-based rule set (geoip-cn) against a
|
||||
* domain destination unless it is resolved first, so an explicit resolve
|
||||
* sing-box does not match an address-based rule set against a domain
|
||||
* destination unless it is resolved first, so an explicit resolve
|
||||
* action comes first. Which way the split points is the mode's default
|
||||
* policy (ctx.proxy_fallback):
|
||||
*
|
||||
@@ -105,10 +105,17 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
|
||||
* proxy_mainland_china mainland -> proxy, the rest falls through to
|
||||
* direct-out
|
||||
*
|
||||
* Using geoip-cn rather than the geosite-* domain lists is deliberate:
|
||||
* those mis-classify foreign domains (Google's gvt2.com beacons) as "cn"
|
||||
* and would send them direct to time out. Keep the direct-domain
|
||||
* fast-path above for known direct domains. */
|
||||
* Using china-ip rather than a domain list is deliberate: those
|
||||
* mis-classify foreign domains (Google's gvt2.com beacons) as "cn" and
|
||||
* would send them direct to time out. Keep the direct-domain fast-path
|
||||
* above for known direct domains.
|
||||
*
|
||||
* One list for both halves of the split: china-ip is generated from the
|
||||
* same china_ip4.txt the firewall renders homeproxy_mainland_addr_v4
|
||||
* from, so the kernel and the resolver cannot disagree about what counts
|
||||
* as mainland. It used to be geoip-cn, with china-ip added later as a
|
||||
* correction for the two disagreeing - two rule-sets, one of them
|
||||
* redundant in this mode, neither able to see the other's data. */
|
||||
if (ctx.routing_mode === 'bypass_mainland_china'
|
||||
|| ctx.routing_mode === 'proxy_mainland_china') {
|
||||
push(config.route.rules, {
|
||||
@@ -116,37 +123,20 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
|
||||
strategy: (ctx.ipv6_support !== '1') ? 'prefer_ipv4' : null
|
||||
});
|
||||
push(config.route.rules, {
|
||||
rule_set: 'geoip-cn',
|
||||
rule_set: 'china-ip',
|
||||
action: 'route',
|
||||
outbound: ctx.proxy_fallback ? 'direct-out' : 'main-out'
|
||||
});
|
||||
}
|
||||
if (ctx.routing_mode === 'bypass_mainland_china') {
|
||||
/* geoip-cn.srs is upstream's list; the firewall's mainland set is
|
||||
* built from our own china_ip4.txt (firewall_post.ut reads it
|
||||
* directly). The two disagree - the bundled list has 8.152.0.0/13
|
||||
* (Alibaba Cloud) and geoip-cn.srs does not - so a connection into
|
||||
* that range passes the firewall as "not mainland" and would then be
|
||||
* sent direct by the rule above instead of to the proxy. This local
|
||||
* rule-set is generated from the same china_ip4.txt, which is what
|
||||
* makes both sides decide from one list. In
|
||||
* proxy_mainland_china it is not needed twice: the geoip-cn rule
|
||||
* already selects the mainland side for the proxy. */
|
||||
push(config.route.rules, {
|
||||
rule_set: 'china-ip',
|
||||
action: 'route',
|
||||
outbound: 'direct-out'
|
||||
});
|
||||
|
||||
/* IPv6 half of the same split, and the reason a mainland site opened
|
||||
* over IPv6 used to go through the proxy even with IPv6 support on.
|
||||
* geoip-cn.srs carries no IPv6 and china_ip4.json is generated from an
|
||||
* IPv4 list, so a destination that resolved to an AAAA address matched
|
||||
* neither and fell through to `final` - which in this mode is main-out.
|
||||
* The rule is emitted in both mainland modes (unlike china-ip, which
|
||||
* proxy_mainland_china does not need twice because geoip-cn already
|
||||
* picks the mainland side there): with no IPv6 in geoip-cn there is
|
||||
* nothing left to pick it, so the side has to be named explicitly.
|
||||
/* The IPv6 half of the same split, and the reason a mainland site
|
||||
* opened over IPv6 used to go through the proxy even with IPv6
|
||||
* support on. china_ip4.json is generated from an IPv4 list, so a
|
||||
* destination that resolved to an AAAA address matched nothing and
|
||||
* fell through to `final` - which in bypass mode is main-out. The
|
||||
* rule is emitted in both mainland modes for that reason. Declared
|
||||
* only when china_ip6.json is actually there: a rule_set pointing at
|
||||
* a missing file takes the whole config down with it, and a router
|
||||
* whose v6 list is unusable must still start (the firewall has
|
||||
* already degraded to passing IPv6 through, with a warning).
|
||||
* `prefer_ipv4` on the resolve rule above is what lets both families
|
||||
* through - it biases, it does not filter. */
|
||||
if (ctx.ipv6_support === '1' && ctx.china_ip6_ready) {
|
||||
@@ -211,90 +201,36 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
|
||||
]
|
||||
});
|
||||
|
||||
/* Both mainland modes split on geoip-cn, so both need it declared - it is
|
||||
* what the route rule added above matches. `proxy_mainland_china` used
|
||||
* to reach this point without it and sing-box refused the whole config
|
||||
* with "initialize rule[3]: rule-set not found: geoip-cn", which the
|
||||
* health gate turned into a rollback and an unproxied network. */
|
||||
if (declaresBuiltinRemoteRuleSets(ctx.routing_mode)) {
|
||||
/*
|
||||
* Fetched straight from the upstream SagerNet repositories and
|
||||
* downloaded through the selected node. A direct fetch depends on
|
||||
* the CDN staying reachable from mainland China, where DNS pollution
|
||||
* makes it fail intermittently; the files total ~250 KB per
|
||||
* day, so proxying the download costs almost nothing.
|
||||
*
|
||||
* Both rule_sets are emitted, but they are consumed by different
|
||||
* blocks:
|
||||
*
|
||||
* geoip-cn - referenced by the route rule above to split
|
||||
* destinations by IP: mainland -> direct-out in
|
||||
* bypass_mainland_china, mainland -> main-out in
|
||||
* proxy_mainland_china.
|
||||
* geosite-cn - referenced only by the DNS rule in
|
||||
* generator/dns.uc (rule_set: geosite-cn ->
|
||||
* server: china-dns); the route layer never matches
|
||||
* it. Sing-box still loads and keeps the rule-set in
|
||||
* memory even when only one block references it, so
|
||||
* removing it would also break the DNS split.
|
||||
*
|
||||
* History: a third geosite-noncn used to be declared here as well,
|
||||
* but no rule referenced it and sing-box still loaded and updated
|
||||
* it daily, so it was removed (the comment above was rewritten
|
||||
* during the 2026-09-29 review to clarify that geosite-cn is still
|
||||
* in use - the DNS side - just not from this block).
|
||||
*
|
||||
* The list itself lives in common.uc, because the CLI has to create
|
||||
* an "initial" fallback file for each of these before this generator
|
||||
* runs - see the note on BUILTIN_REMOTE_RULE_SETS. That is the whole
|
||||
* reason this block is a loop: these two are the rule-sets a cold
|
||||
* install blocks on, so a change to them that only reached one of the
|
||||
* two readers would put the fallback out of step with the config
|
||||
* that points at it.
|
||||
*/
|
||||
for (let rs in BUILTIN_REMOTE_RULE_SETS) {
|
||||
push(config.route.rule_set, {
|
||||
type: 'remote',
|
||||
tag: rs.tag,
|
||||
format: rs.format,
|
||||
url: rs.url,
|
||||
update_interval: rs.update_interval,
|
||||
/* Only with the opt-in on, and only for a fallback the CLI
|
||||
* actually wrote. A remote rule-set with no initial_path is
|
||||
* fetched during initialization, before the inbounds bind, so
|
||||
* a cold cache makes the first start depend on the CDN - and
|
||||
* on the node, since these go through http_clients. The
|
||||
* fallback is an EMPTY rule-set, which changes routing for
|
||||
* everything it would have matched, so it stays off unless
|
||||
* the user asks for it.
|
||||
*
|
||||
* These two are the rule-sets a fresh install blocks on, so
|
||||
* they are also the whole point of the feature: an earlier
|
||||
* version of this block only knew how to build the entries,
|
||||
* and the CLI had no idea they existed. */
|
||||
initial_path: (ctx.ruleset_safe_start === '1' && ctx.ruleset_initial[rs.tag])
|
||||
? ruleSetInitialFallback([ rs.tag ], rs.format, rs.url) : null,
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
}
|
||||
/* Local, and not downloaded: it is generated from the same
|
||||
* china_ip4.txt the firewall renders homeproxy_mainland_addr_v4
|
||||
* from, so the two cannot drift apart. `type: local` is watched by
|
||||
* sing-box with fswatch, so when the resource updater replaces the
|
||||
* file the running instance reloads it in place - no restart and,
|
||||
* more importantly, no second copy of the list to keep in sync. */
|
||||
/* Both mainland modes split on the China address list, so both need it
|
||||
* declared - it is what the route rule added above matches.
|
||||
* `proxy_mainland_china` used to reach this point without a declaration and
|
||||
* sing-box refused the whole config with "initialize rule[3]: rule-set not
|
||||
* found: geoip-cn", which the health gate turned into a rollback and an
|
||||
* unproxied network.
|
||||
*
|
||||
* All three are `type: local` and none of them is downloaded. Each is
|
||||
* generated by runtime/{china_ip,domain}_ruleset.uc from a list the resource
|
||||
* updater maintains, and sing-box watches the file with fswatch, so a new
|
||||
* list is picked up in place - no restart, and no second copy of the data to
|
||||
* keep in step with the firewall's nft set.
|
||||
*
|
||||
* Which ones are declared depends on what references them:
|
||||
* china-ip the route rule above, in both mainland modes
|
||||
* china-ip6 the IPv6 route rule, when there is one
|
||||
* china-domain the DNS rule in generator/dns.uc, bypass mode only -
|
||||
* proxy_mainland_china decides by address and leaves the
|
||||
* resolver to the mode default, so declaring it there would
|
||||
* load 111k suffixes for a rule that does not exist. */
|
||||
if (declaresBuiltinRuleSets(ctx.routing_mode)) {
|
||||
push(config.route.rule_set, {
|
||||
type: 'local',
|
||||
tag: 'china-ip',
|
||||
path: HP_DIR + '/resources/china_ip4.json'
|
||||
});
|
||||
|
||||
/* The IPv6 counterpart, from china_ip6.txt - the same list the
|
||||
* firewall renders homeproxy_mainland_addr_v6 from, and the only
|
||||
* IPv6 mainland data either side of the split has. Declared only
|
||||
* when the file is actually there: a rule_set pointing at a missing
|
||||
* file takes the whole config down with it, and a router whose v6
|
||||
* list is unusable must still start (the firewall has already
|
||||
/* Declared only when the file is actually there: a rule_set pointing
|
||||
* at a missing file takes the whole config down with it, and a router
|
||||
* whose v6 list is unusable must still start (the firewall has already
|
||||
* degraded to passing IPv6 through, with a warning). */
|
||||
if (ctx.ipv6_support === '1' && ctx.china_ip6_ready) {
|
||||
push(config.route.rule_set, {
|
||||
@@ -303,6 +239,18 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
|
||||
path: HP_DIR + '/resources/china_ip6.json'
|
||||
});
|
||||
}
|
||||
|
||||
/* The DNS half. Same "only when it is there" rule as the v6 one
|
||||
* above, for the same reason: generate_client.uc's dns rule names this
|
||||
* tag, and a declaration whose file is missing fails the start rather
|
||||
* than degrading. */
|
||||
if (ctx.routing_mode === 'bypass_mainland_china' && ctx.china_domain_ready) {
|
||||
push(config.route.rule_set, {
|
||||
type: 'local',
|
||||
tag: 'china-domain',
|
||||
path: HP_DIR + '/resources/china-domain.json'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmpty(config.route.rule_set))
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { isEmpty, rulePathRootsText, ruleSetFormatFromPath, ruleSetInitialFallback, strToTime, validateRuleSetPath } from '../homeproxy-pro.uc';
|
||||
import { isEmpty, rulePathRootsText, ruleSetFormatFromPath, strToTime, validateRuleSetPath } from '../homeproxy-pro.uc';
|
||||
|
||||
import { get_outbound, isDirectOutboundTag, rule_set_tags } from './common.uc';
|
||||
|
||||
@@ -214,37 +214,6 @@ export function build_user_rulesets(rule_set_array, dm, ctx) {
|
||||
if (!validateRuleSetPath(cfg.initial_path))
|
||||
die(sprintf("homeproxy-pro: rule-set '%s' initial_path '%s' is outside the allowed rule-set roots (%s); move the file into one of them, or clear the field to let sing-box fetch the rule-set on its own.", cfg.name, cfg.initial_path, rulePathRootsText()));
|
||||
ruleset.initial_path = cfg.initial_path;
|
||||
} else if (cfg.type === 'remote' && ctx.ruleset_safe_start === '1') {
|
||||
/* No initial file of the user's own, and the opt-in is on: point at
|
||||
* the empty fallback the CLI wrote before this generator ran.
|
||||
*
|
||||
* ctx.ruleset_initial records PRESENCE, not a path, and every tag
|
||||
* has to be present. sing-box substitutes {tag} and opens every
|
||||
* resulting file, so a multi-tag rule-set whose second file is
|
||||
* missing blocks startup exactly as surely as one whose first is -
|
||||
* measured on 1.14.2, where a literal "{tag}.srs" on disk makes
|
||||
* it fall back to downloading and fail. All-or-nothing is the
|
||||
* only correct answer here.
|
||||
*
|
||||
* Nothing is emitted when the answer is no: the rule-set then
|
||||
* behaves exactly as it does today, which is also what happens if
|
||||
* the CLI could not write the file. */
|
||||
const fallback = ruleSetInitialFallback(all_tags, cfg.format, cfg.url);
|
||||
|
||||
if (fallback) {
|
||||
let ready = true;
|
||||
|
||||
for (let t in all_tags)
|
||||
if (!ctx.ruleset_initial[t]) {
|
||||
ready = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ready)
|
||||
ruleset.initial_path = fallback;
|
||||
else
|
||||
warn(sprintf("homeproxy-pro: rule-set '%s' has no complete empty fallback, so it will be fetched during startup as before.", cfg.name));
|
||||
}
|
||||
}
|
||||
push(rule_set_array, ruleset);
|
||||
}
|
||||
|
||||
@@ -300,116 +300,6 @@ export function ruleSetFormatFromPath(path) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/* --- the "do not block startup on the first download" fallback ---------- *
|
||||
*
|
||||
* A remote rule-set with no `initial_path` is fetched during
|
||||
* initialization, BEFORE the inbounds bind. On a cold cache that means a
|
||||
* fresh install cannot start until raw.githubusercontent.com answers - and if
|
||||
* it does not, the whole instance dies with a FATAL and the health gate turns
|
||||
* that into a rollback or a released intercept layer. The node has to be up
|
||||
* too, because these downloads go through http_clients, so a working
|
||||
* configuration can be kept off the air by an unreachable CDN.
|
||||
*
|
||||
* `initial_path` is the way out: sing-box reads the file first, starts
|
||||
* immediately, and refreshes from the URL in the background. A download that
|
||||
* keeps failing then becomes an ERROR line in the log instead of a failed
|
||||
* start - and the health gate does not look at rule-sets at all, so a
|
||||
* degraded rule-set is promoted like any other healthy start.
|
||||
*
|
||||
* What the file holds is an EMPTY rule-set, and that is the whole trade: a
|
||||
* rule-set with no rules matches nothing, so its traffic falls through to
|
||||
* `final`. That changes routing, so this is opt-in (config.ruleset_safe_start,
|
||||
* default off) and never the default.
|
||||
*
|
||||
* The format has to match the declared one. A binary rule-set fed a source
|
||||
* JSON is not read at all - sing-box ignores the mismatch and blocks exactly
|
||||
* as it does with no initial_path - so the decision of whether a fallback is
|
||||
* even possible is made once, here, from the same inputs on both sides. */
|
||||
|
||||
/* Where the generated fallbacks live. Inside the rule-set archive, so
|
||||
* validateRuleSetPath() admits it and the jail's read-only mount of HP_DIR
|
||||
* already exposes it to the sing-box user. */
|
||||
export const RULESET_INITIAL_DIR = HP_DIR + '/ruleset/initial';
|
||||
|
||||
/* The two files the package ships for this. empty.source.json is what
|
||||
* `sing-box rule-set compile` consumes (24 bytes, readable, and the form
|
||||
* every version agrees on); empty.srs is the compiled result of THAT source
|
||||
* on 1.14.2, kept as the last resort for a router where the compile cannot
|
||||
* run.
|
||||
*
|
||||
* The .srs is committed rather than built at package time on purpose: it is a
|
||||
* 14-byte binary, and having it in the tree means `hexdump -C` on a router
|
||||
* can confirm byte for byte that the shipped constant is the documented one.
|
||||
* It is deliberately NOT a conffile - it is ours, not the user's, and an
|
||||
* upgrade may replace it freely.
|
||||
*
|
||||
* Compatibility note, since a hard-coded binary artifact invites the question:
|
||||
* sing-box only rejects an SRS whose version is NEWER than its own, so a
|
||||
* version-2 file stays readable. The compile path exists anyway, because a
|
||||
* file produced by the kernel that is running it cannot drift from it. */
|
||||
export const RULESET_EMPTY_SOURCE = RULESET_INITIAL_DIR + '/empty.source.json';
|
||||
export const RULESET_EMPTY_BINARY = RULESET_INITIAL_DIR + '/empty.srs';
|
||||
|
||||
/* A tag becomes a file name, so it has to be one. UCI section names are
|
||||
* already restricted, but a rule_set `tag` is a sing-box identifier that UCI
|
||||
* does not fully own, and this value is about to be concatenated into a path
|
||||
* the generator writes as root. Anything outside this set is refused, and
|
||||
* the caller skips the fallback rather than sanitising it into a name that
|
||||
* could collide with another rule-set's. */
|
||||
export function isSafeRuleSetTag(tag) {
|
||||
if (!tag || type(tag) !== 'string')
|
||||
return false;
|
||||
|
||||
return match(tag, /^[A-Za-z0-9][A-Za-z0-9._-]*$/) != null;
|
||||
};
|
||||
|
||||
/* The extension a format is stored under. Null for anything that is not one
|
||||
* of the two formats, which is the caller's signal that no fallback is
|
||||
* possible rather than something to guess at. */
|
||||
export function ruleSetFormatExtension(format) {
|
||||
if (format === 'binary')
|
||||
return 'srs';
|
||||
|
||||
if (format === 'source')
|
||||
return 'json';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/* ruleSetInitialFallback(tags, declared, url) -> the initial_path to emit, or
|
||||
* null when no fallback is possible.
|
||||
*
|
||||
* One function, called from both sides on purpose. The CLI asks it to
|
||||
* decide which files to create and the generator asks it where to point
|
||||
* `initial_path`; if they each worked it out separately they could disagree
|
||||
* about the format, and a mismatch is the failure this whole mechanism exists
|
||||
* to avoid - sing-box ignores a wrong-format initial file and blocks startup
|
||||
* exactly as if there were none.
|
||||
*
|
||||
* `declared` is the rule-set's own `format`, which for a rule-set with no
|
||||
* initial file is also the format the generator emits (nothing to probe, so
|
||||
* nothing is corrected). With no declared format the URL's extension is the
|
||||
* only remaining evidence; with neither, the answer is null and the rule-set
|
||||
* keeps today's behaviour rather than being handed a file of the wrong shape.
|
||||
*
|
||||
* More than one tag gets a `{tag}` placeholder, because that is what sing-box
|
||||
* substitutes and it requires every tag's file to exist - P3 on 1.14.2: a
|
||||
* literal "{tag}.srs" on disk makes it try to download and block. */
|
||||
export function ruleSetInitialFallback(tags, declared, url) {
|
||||
const format = declared || ruleSetFormatFromPath(url);
|
||||
const ext = ruleSetFormatExtension(format);
|
||||
|
||||
if (!ext)
|
||||
return null;
|
||||
|
||||
for (let tag in (tags || []))
|
||||
if (!isSafeRuleSetTag(tag))
|
||||
return null;
|
||||
|
||||
const stem = (length(tags || []) > 1) ? '{tag}' : tags[0];
|
||||
|
||||
return RULESET_INITIAL_DIR + '/' + stem + '.' + ext;
|
||||
};
|
||||
|
||||
/* probeRuleSetFile(path) -> 'binary' | 'source' | null.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*
|
||||
* A single self-contained diagnostic report, built in-process for the
|
||||
* `debug_report` rpc method (see luci.homeproxy-pro). It exists because the
|
||||
* failure modes this plugin has are not visible from the log page: a route
|
||||
* that stopped splitting, an nft rule that is not in the fw4 table, a
|
||||
* rule-set that never downloaded, a mark that no longer matches anything.
|
||||
* Each of those needs a different command to confirm, and collecting them
|
||||
* by hand over ssh is slow and easy to get wrong - the commands are spread
|
||||
* across `ip rule`, `nft`, the procd state and the resource version files.
|
||||
*
|
||||
* Two properties are deliberate and are the reason this is not a shell
|
||||
* script dropped in beside the other runtime helpers:
|
||||
*
|
||||
* 1. REDACTION IS DEFAULT-DENY. A node section is a credential store: it
|
||||
* holds passwords, uuids, private keys, SNI and the server address. A
|
||||
* report that is going to be pasted into an issue tracker has to be safe
|
||||
* by construction, so the rule is "if the key looks like a secret, mask
|
||||
* it" rather than an enumerated list of keys to mask - the enumerated
|
||||
* kind is exactly the one that goes stale when a field is added. The
|
||||
* pattern is applied to the key name, so a value is masked no matter
|
||||
* which section it came from.
|
||||
*
|
||||
* 2. EVERY PROBE DEGRADES. A report that aborts because `nft` is missing,
|
||||
* or that silently omits a section when a command fails, is worse than
|
||||
* no report: the reader cannot tell "not applicable" from "not
|
||||
* collected". Each probe therefore reports one of three states -
|
||||
* the output, "(unavailable)" for a missing command, or
|
||||
* "(no output, exit N)" for one that ran and failed. This is the same
|
||||
* distinction health.sh draws between "probe failed" and "cannot probe
|
||||
* here", and it is why a testbed without netfilter still produces a
|
||||
* usable report.
|
||||
*
|
||||
* The commands below are fixed literals: nothing in this file is built from
|
||||
* configuration, so there is no shell interpolation to get wrong and no
|
||||
* argument that would need shellQuote().
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { popen } from 'fs';
|
||||
|
||||
import { HP_DIR, RUN_DIR, shellQuote } from '../homeproxy-pro.uc';
|
||||
|
||||
export const DEBUG_REPORT_PATH = RUN_DIR + '/debug.log';
|
||||
|
||||
/* Per-section output cap. The nft dump is the one section that is expected
|
||||
* to be large - a populated fw4 table runs to a few thousand lines, and
|
||||
* truncating it is worse than useless because a truncated chain looks like
|
||||
* a chain that ends there. The cap is high enough that a real router never
|
||||
* reaches it, and reaching it is stated in the output rather than hidden. */
|
||||
const SECTION_MAX = 128 * 1024;
|
||||
|
||||
/* A key whose name matches any of these is masked. Deliberately broad: a
|
||||
* false positive costs a line of a report, a false negative ships someone's
|
||||
* private key. `method` and `path` are in the list because obfs parameters
|
||||
* and websocket paths are credentials in practice; `user` catches both
|
||||
* `username` and the access-control user lists; `short` is there for
|
||||
* `tls_reality_short_id`, which matched nothing until a canary run put a
|
||||
* value in it and found it in the report verbatim.
|
||||
*
|
||||
* `url` is the one pattern that is NOT a bare substring, and it has to stay
|
||||
* that way: a plain `url` alternative matches inside `main_urltest_nodes` and
|
||||
* `main_urltest_tolerance` (`main_u-r-l-test`), which silently blanked the
|
||||
* urltest pool out of the report. Those lists are how you tell "this node
|
||||
* is still in the test pool" from "this node was dropped", which is a
|
||||
* question the report exists to answer. Anchoring on a word boundary keeps
|
||||
* `url` / `urls` / `subscription_urls` covered and leaves the pool visible. */
|
||||
const SECRET_KEY_RE = /pass|secret|token|key|uuid|(^|_)urls?$|address|server|host|sni|user|psk|path|method|param|seed|salt|auth|hosts|short/i;
|
||||
|
||||
/* Exported for the rpc behaviour test: the redaction rule is the one part of
|
||||
* this file with no I/O to observe, so it is the one part that can be
|
||||
* asserted directly - a key that stops being recognised is a silent leak. */
|
||||
export function isSecretKey(name) {
|
||||
/* ucode has no RegExp.prototype.test - the module-level match() is the
|
||||
* only way to apply a pattern, and every other file in this tree does it
|
||||
* the same way. */
|
||||
return type(name) === 'string' && match(name, SECRET_KEY_RE) != null;
|
||||
};
|
||||
|
||||
function maskValue(value) {
|
||||
/* A list keeps its length visible: "3 masked entries" tells the reader
|
||||
* the subscription imported something, which is a fact worth having,
|
||||
* without disclosing any of it. */
|
||||
if (type(value) === 'array')
|
||||
return value === null || length(value) === 0
|
||||
? value
|
||||
: sprintf('[%d masked]', length(value));
|
||||
|
||||
return '***';
|
||||
};
|
||||
|
||||
/* Run one collector. Returns { state, text } where state is 'ok',
|
||||
* 'unavailable' or 'failed', so the caller can label it honestly instead of
|
||||
* printing an empty block that reads like "nothing to see". */
|
||||
function probe(command) {
|
||||
if (command == null)
|
||||
return { state: 'unavailable', text: '' };
|
||||
|
||||
const fd = popen(command);
|
||||
if (fd == null)
|
||||
return { state: 'unavailable', text: '' };
|
||||
|
||||
let out = fd.read('all');
|
||||
fd.close();
|
||||
|
||||
if (out == null)
|
||||
return { state: 'failed', text: '' };
|
||||
|
||||
out = trim(out);
|
||||
|
||||
/* popen() gives no exit status, so an empty result is the only signal
|
||||
* that the command produced nothing. Whether that is a problem
|
||||
* depends on the command, and the section header says which. */
|
||||
if (out === '')
|
||||
return { state: 'failed', text: '' };
|
||||
|
||||
if (length(out) > SECTION_MAX)
|
||||
out = substr(out, 0, SECTION_MAX) +
|
||||
sprintf('\n... [truncated at %d bytes; rerun the command by hand for the full output]\n', SECTION_MAX);
|
||||
|
||||
return { state: 'ok', text: out };
|
||||
};
|
||||
|
||||
/* The value form of the same collector: trimmed stdout, or null when the
|
||||
* command produced nothing. Used where the caller wants a value rather
|
||||
* than a rendered block. Deliberately not written with the optional-chaining
|
||||
* shorthand, which no other file in this tree uses. */
|
||||
function runCommand(command) {
|
||||
const fd = popen(command);
|
||||
if (fd == null)
|
||||
return null;
|
||||
|
||||
const out = fd.read('all');
|
||||
fd.close();
|
||||
|
||||
if (out == null)
|
||||
return null;
|
||||
|
||||
const t = trim(out);
|
||||
return t === '' ? null : t;
|
||||
};
|
||||
|
||||
function runDate() {
|
||||
return runCommand('date "+%Y-%m-%d %H:%M:%S %Z"') || 'unknown';
|
||||
};
|
||||
|
||||
/* Render one ```bash fenced block for a command, with the state spelled out
|
||||
* when there is no output to show. */
|
||||
function block(command, unavailable_label) {
|
||||
const r = probe(command);
|
||||
|
||||
if (r.state === 'ok')
|
||||
return sprintf('```shell\n# %s\n%s\n```\n', command, r.text);
|
||||
|
||||
if (r.state === 'unavailable')
|
||||
return sprintf('```\n(unavailable: %s)\n```\n', unavailable_label || command);
|
||||
|
||||
return '```\n(no output)\n```\n';
|
||||
};
|
||||
|
||||
/* Render a two-column table, or a single "(unavailable)" row. */
|
||||
function table(rows, unavailable_label) {
|
||||
if (rows == null)
|
||||
return sprintf('_%s_\n', unavailable_label || 'unavailable');
|
||||
|
||||
let out = '| 项目 | 值 |\n|------|-----|\n';
|
||||
for (let r in rows)
|
||||
out += sprintf('| %s | %s |\n', r[0], r[1]);
|
||||
return out;
|
||||
};
|
||||
|
||||
/* `apk list -I <pkg>` prints one line:
|
||||
*
|
||||
* sing-box-1.14.2-r1 x86_64 {feeds/.../sing-box} (GPL-3.0-or-later) [installed]
|
||||
*
|
||||
* The package name is the argument, so the version is simply what follows
|
||||
* that name's trailing dash up to the first space. A pattern anchored on
|
||||
* a negated class instead - `^[^ -]+-([0-9][^ ]*)` - stops at the FIRST
|
||||
* dash, so it works for `firewall4` and silently fails for every package
|
||||
* whose own name contains one (`sing-box`, `luci-app-homeproxy-pro`), which is
|
||||
* most of them. Splitting on the known prefix has no such case.
|
||||
*
|
||||
* This also avoids `\\s` inside a negated class, whose meaning inside `[...]`
|
||||
* is not what it is outside: `homeproxy-pro.uc`'s `replace(reason, /\s+/g, ' ')`
|
||||
* collapses whitespace correctly, yet the same `\s` inside `[^\\s-]` did not
|
||||
* exclude whitespace, and the version came back with the architecture glued
|
||||
* to it. */
|
||||
function packageVersion(name) {
|
||||
if (system('command -v apk >/dev/null 2>&1') !== 0)
|
||||
return null;
|
||||
|
||||
const fd = popen(sprintf('apk list -I %s 2>/dev/null', shellQuote(name)));
|
||||
if (fd == null)
|
||||
return null;
|
||||
|
||||
const line = trim(fd.read('line'));
|
||||
fd.close();
|
||||
|
||||
if (line == null || line === '')
|
||||
return null;
|
||||
|
||||
const prefix = name + '-';
|
||||
if (index(line, prefix) !== 0)
|
||||
return null;
|
||||
|
||||
const rest = substr(line, length(prefix));
|
||||
const sp = index(rest, ' ');
|
||||
const v = (sp == -1) ? rest : substr(rest, 0, sp);
|
||||
|
||||
return v === '' ? null : v;
|
||||
};
|
||||
|
||||
/* --- UCI summary --------------------------------------------------------- */
|
||||
|
||||
/* A structural, redacted view of the configuration: counts and switch
|
||||
* positions, never a credential. Sections are summarised by type rather
|
||||
* than dumped, because a node section has no field that is safe to print
|
||||
* and a subscription section has exactly one. */
|
||||
function configSummary(uci) {
|
||||
const all = uci.get_all('homeproxy-pro') || {};
|
||||
const byType = {};
|
||||
|
||||
for (let sid in all) {
|
||||
const section = all[sid];
|
||||
const stype = section['.type'] || 'unknown';
|
||||
|
||||
if (byType[stype] == null)
|
||||
byType[stype] = { count: 0, sample: section['.name'] || sid };
|
||||
|
||||
byType[stype].count++;
|
||||
}
|
||||
|
||||
if (byType == null || length(keys(byType)) === 0)
|
||||
return '_configuration is empty_\n';
|
||||
|
||||
let out = '';
|
||||
|
||||
for (let t in keys(byType)) {
|
||||
out += sprintf('- `%s`: %d section(s)\n', t, byType[t].count);
|
||||
}
|
||||
|
||||
/* The switches that decide what the report is FOR: which mode, which
|
||||
* ports, and the mark - these are the first things anyone reading a
|
||||
* routing problem looks for, and none of them is sensitive. Each entry
|
||||
* is [section, option]. */
|
||||
const switches = [
|
||||
['config', 'proxy_mode'],
|
||||
['config', 'routing_mode'],
|
||||
['config', 'main_node'],
|
||||
['config', 'main_udp_node'],
|
||||
['config', 'ipv6_support'],
|
||||
['config', 'log_level'],
|
||||
['config', 'bypass_cn_traffic'],
|
||||
['infra', 'mixed_port'],
|
||||
['infra', 'redirect_port'],
|
||||
['infra', 'tproxy_port'],
|
||||
['infra', 'dns_port'],
|
||||
['infra', 'self_mark'],
|
||||
['infra', 'tproxy_mark'],
|
||||
['infra', 'tun_mark'],
|
||||
['infra', 'table_mark'],
|
||||
['infra', 'tun_name'],
|
||||
['server', 'enabled']
|
||||
];
|
||||
|
||||
out += '\n| 开关 | 值 |\n|------|-----|\n';
|
||||
for (let sw in switches) {
|
||||
const v = uci.get('homeproxy-pro', sw[0], sw[1]);
|
||||
out += sprintf('| `%s.%s` | %s |\n', sw[0], sw[1],
|
||||
(v == null || v === '') ? '_(unset)_' : (isSecretKey(sw[1]) ? maskValue(v) : v));
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
/* Every non-secret option, flattened, with anything that looks like a
|
||||
* credential masked. This is the section that makes the report useful for
|
||||
* "which option did I set wrong" - the summary above cannot show them. */
|
||||
function fullConfig(uci) {
|
||||
const all = uci.get_all('homeproxy-pro') || {};
|
||||
let out = '';
|
||||
|
||||
for (let sid in keys(all)) {
|
||||
const section = all[sid];
|
||||
const stype = section['.type'] || 'unknown';
|
||||
const sname = section['.name'] || sid;
|
||||
|
||||
out += sprintf('\n### %s `%s`\n\n```\n', stype, sname);
|
||||
|
||||
for (let k in keys(section)) {
|
||||
if (substr(k, 0, 1) === '.')
|
||||
continue;
|
||||
|
||||
const v = section[k];
|
||||
|
||||
if (isSecretKey(k)) {
|
||||
out += sprintf('option %s %s\n', k, maskValue(v));
|
||||
} else if (type(v) === 'array') {
|
||||
out += sprintf('list %s %s\n', k,
|
||||
length(v) === 0 ? '' : join(' ', v));
|
||||
} else {
|
||||
out += sprintf('option %s %s\n', k, v);
|
||||
}
|
||||
}
|
||||
|
||||
out += '```\n';
|
||||
}
|
||||
|
||||
return out == '' ? '_configuration is empty_\n' : out;
|
||||
};
|
||||
|
||||
/* --- resource versions --------------------------------------------------- */
|
||||
|
||||
/* The .ver files next to each shipped list. A rule-set that has been
|
||||
* failing to download shows up here as a version that never moves, which is
|
||||
* the cheapest way to tell "the list is stale" from "the list is missing". */
|
||||
function resourceVersions() {
|
||||
const names = ['china_ip4', 'china_ip6', 'china_list', 'gfw_list'];
|
||||
let out = '';
|
||||
|
||||
for (let n in names) {
|
||||
const path = sprintf('%s/resources/%s.ver', HP_DIR, n);
|
||||
const fd = popen(sprintf('cat %s 2>/dev/null', shellQuote(path)));
|
||||
|
||||
if (fd != null) {
|
||||
const v = trim(fd.read('all'));
|
||||
fd.close();
|
||||
out += sprintf('- `%s`: %s\n', n, (v == null || v === '') ? '_(no version file)_' : v);
|
||||
} else {
|
||||
out += sprintf('- `%s`: _(unavailable)_\n', n);
|
||||
}
|
||||
}
|
||||
|
||||
/* The dnsmasq nftset capability marker: init.d writes this at start
|
||||
* and firewall_post.ut gates the gfwlist mode on it, so a device whose
|
||||
* dnsmasq was built without HAVE_NFTSET silently routes everything
|
||||
* direct while DNS still goes through the proxy. */
|
||||
const marker = sprintf('%s/resources/.dnsmasq-nftset', HP_DIR);
|
||||
const fd = popen(sprintf('cat %s 2>/dev/null', shellQuote(marker)));
|
||||
let cap = 'unknown';
|
||||
if (fd != null) {
|
||||
cap = trim(fd.read('all')) || 'unknown';
|
||||
fd.close();
|
||||
}
|
||||
out += sprintf('- `dnsmasq nftset`: %s', cap == '1' ? 'yes' : (cap == '0' ? 'no (gfwlist mode cannot work)' : cap));
|
||||
|
||||
return out + '\n';
|
||||
};
|
||||
|
||||
/* --- the report ---------------------------------------------------------- */
|
||||
|
||||
export function buildDebugReport(uci) {
|
||||
const stamp = trim(runDate());
|
||||
|
||||
let out = '';
|
||||
out += '# HomeProxy 诊断报告\n\n';
|
||||
out += sprintf('> 生成时间: %s\n', stamp == '' ? '(unknown)' : stamp);
|
||||
out += sprintf('> 插件版本: %s\n', packageVersion('luci-app-homeproxy-pro') || 'unknown');
|
||||
out += '> 隐私: 凭据字段已按名称脱敏,但本报告仍含公网 IP、内网拓扑和 LAN 主机地址。\n';
|
||||
out += '> 外发前请自行确认。\n';
|
||||
|
||||
/* 1. System */
|
||||
out += '\n## 1. 系统\n\n';
|
||||
out += block('cat /etc/openwrt_release 2>/dev/null', 'uname -a');
|
||||
out += block('uname -a');
|
||||
out += block('uptime');
|
||||
out += block('df -h / /tmp /etc/homeproxy-pro 2>/dev/null', 'df unavailable');
|
||||
out += block('free -m 2>/dev/null', 'free unavailable');
|
||||
|
||||
/* 2. Dependencies - the table, not a "is it there" yes/no, because the
|
||||
* version is what a reader needs when reporting "it does not work on
|
||||
* this build". */
|
||||
out += '\n## 2. 依赖检查\n\n';
|
||||
const deps = ['sing-box', 'firewall4', 'kmod-nft-tproxy', 'kmod-tun',
|
||||
'ip-full', 'uclient-fetch', 'ucode', 'ucode-mod-digest'];
|
||||
const dep_rows = [];
|
||||
|
||||
for (let d in deps) {
|
||||
const v = packageVersion(d);
|
||||
push(dep_rows, [d, v == null ? '_(not reported by apk; may be built in)_' : v]);
|
||||
}
|
||||
|
||||
out += table(dep_rows, 'apk not available on this system');
|
||||
|
||||
/* 3. Core capabilities */
|
||||
out += '\n## 3. 内核与能力\n\n';
|
||||
out += block('/usr/bin/sing-box version 2>/dev/null', 'sing-box not installed');
|
||||
out += block('lsmod 2>/dev/null | grep -E "tun|nft_tproxy|inet_diag"', 'lsmod unavailable');
|
||||
out += block('command -v nft fw4 utpl ucode uci 2>/dev/null', 'PATH lookup unavailable');
|
||||
|
||||
/* 4. Configuration */
|
||||
out += '\n## 4. 配置摘要(脱敏)\n\n';
|
||||
out += configSummary(uci);
|
||||
|
||||
/* 5. Resources */
|
||||
out += '\n## 5. 资源版本\n\n';
|
||||
out += resourceVersions();
|
||||
|
||||
/* 6. Service */
|
||||
out += '\n## 6. 服务状态\n\n';
|
||||
out += block('/etc/init.d/homeproxy-pro info 2>&1', 'init script unavailable');
|
||||
out += block('ubus call service list \'"name":"homeproxy-pro"\' 2>/dev/null',
|
||||
'ubus unavailable');
|
||||
|
||||
/* 7. Routing - `ip rule` first, because the mark that routes a
|
||||
* connection into the proxy is set there, and a rule that is missing
|
||||
* explains a proxy that binds its ports and then times out. */
|
||||
out += '\n## 7. 路由策略\n\n';
|
||||
out += '### ip rule\n\n';
|
||||
out += block('ip rule list');
|
||||
out += '### ip route\n\n';
|
||||
out += block('ip -4 route list');
|
||||
out += block('ip -6 route list');
|
||||
out += sprintf('\n**策略路由表**(table_mark / tproxy_mark / tun_mark):\n\n```shell\n# ip route list table 100\n%s\n```\n',
|
||||
trim(runCommand('ip route list table 100 2>/dev/null') || '(empty or unavailable)'));
|
||||
|
||||
/* 8. Firewall - the full fw4 table, not just homeproxy-pro's own chains.
|
||||
* The failure this covers is "homeproxy-pro's rules are present and
|
||||
* correct but something in fw4 jumps to another chain first", and
|
||||
* that is only visible with the surrounding rules in view. */
|
||||
out += '\n## 8. 防火墙\n\n';
|
||||
out += '_The full `inet fw4` table. homeproxy-pro\'s chains are the ones named '
|
||||
+ '`homeproxy_*`; everything else is fw4\'s own._\n\n';
|
||||
out += block('nft -a list table inet fw4 2>/dev/null', 'nft unavailable (is firewall4 installed?)');
|
||||
|
||||
/* 9. Log excerpts - the tail of each log, not the whole file: the
|
||||
* interesting part of a failure is the last thing that happened. */
|
||||
out += '\n## 9. 日志尾部\n\n';
|
||||
for (let l in ['homeproxy-pro', 'sing-box-c', 'sing-box-s']) {
|
||||
out += sprintf('### %s.log\n\n', l);
|
||||
out += block(sprintf('tail -n 60 %s/%s.log 2>/dev/null', RUN_DIR, l),
|
||||
sprintf('%s.log not present', l));
|
||||
}
|
||||
|
||||
/* 10. Full redacted configuration */
|
||||
out += '\n## 10. 完整配置(脱敏)\n\n';
|
||||
out += fullConfig(uci);
|
||||
|
||||
return out;
|
||||
};
|
||||
@@ -73,7 +73,7 @@ hp_dnsmasq_has_nftset() {
|
||||
[ "$HP_DNSMASQ_NFTSET" = "1" ]
|
||||
}
|
||||
|
||||
# hp_dnsmasq_render_snippets <stage-dir> <hp-dir> <routing-mode> <dns-port> <ipv6>
|
||||
# hp_dnsmasq_render_snippets <stage-dir> <hp-dir> <routing-mode> <dns-port> <ipv6> [run-dir]
|
||||
# Render the desired snippet set into <stage-dir>. Writes nothing outside it,
|
||||
# restarts nothing: the caller decides whether the result differs from what is
|
||||
# already installed.
|
||||
@@ -89,6 +89,7 @@ hp_dnsmasq_render_snippets() {
|
||||
local routing_mode="$3"
|
||||
local dns_port="$4"
|
||||
local ipv6="$5"
|
||||
local run_dir="${6:-/var/run/homeproxy-pro}"
|
||||
local gfw_nftset_v6="" wan_nftset_v6=""
|
||||
local nftset_ok=1
|
||||
|
||||
@@ -154,6 +155,33 @@ hp_dnsmasq_render_snippets() {
|
||||
"$hp_dir/resources/proxy_list.txt" > "$stage/proxy_list.conf" || return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Node server addresses, host-name form.
|
||||
#
|
||||
# This snippet carries no `server=`: the routing mode's own snippet above
|
||||
# already decides which upstream answers, and in every proxied mode that is
|
||||
# sing-box's dns-in. The generator resolves the node through the WAN
|
||||
# resolver either way (route.default_domain_resolver), so the address that
|
||||
# comes back is the same one sing-box will dial - which is the point. Only
|
||||
# the `nftset=` is added here, and dnsmasq back-fills it from the answer it
|
||||
# relays, forwarded or not.
|
||||
#
|
||||
# Without it, a LAN client opening a connection to the node's own address
|
||||
# was redirected into sing-box, which dialled that same address through the
|
||||
# tunnel to that same node. sing-box's own outbound is already exempt
|
||||
# (routing_mark / self_mark); this covers the LAN, which is not.
|
||||
#
|
||||
# The literal-address form cannot come from here - there is no name to
|
||||
# resolve - so firewall_post.ut renders it into the same set directly.
|
||||
if [ "$nftset_ok" = "1" ] && [ -s "$run_dir/node-addr-domains.txt" ]; then
|
||||
local node_nftset_v6=""
|
||||
[ "$ipv6" -eq "0" ] || node_nftset_v6=",6#inet#fw4#homeproxy_node_addr_v6"
|
||||
sed -r -e '/^\s*$/d' \
|
||||
-e "s/(.*)/nftset=\/.\1\/4#inet#fw4#homeproxy_node_addr_v4$node_nftset_v6/g" \
|
||||
"$run_dir/node-addr-domains.txt" > "$stage/node_addr.conf" || return 1
|
||||
elif [ -s "$run_dir/node-addr-domains.txt" ]; then
|
||||
log "Warning: this dnsmasq has no nftset support; LAN connections to the node's address are not excluded from the proxy."
|
||||
fi
|
||||
}
|
||||
|
||||
# hp_dnsmasq_dir_differs <stage-dir> <live-dir>
|
||||
@@ -179,7 +207,7 @@ hp_dnsmasq_dir_differs() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# hp_dnsmasq_write_snippets <dns-dir> <hp-dir> <routing-mode>
|
||||
# hp_dnsmasq_write_snippets <dns-dir> <hp-dir> <routing-mode> [run-dir]
|
||||
# Write the include file plus the mode-specific snippet, then restart
|
||||
# dnsmasq. Requires config_load (reads ipv6_support and dns_port).
|
||||
#
|
||||
@@ -192,6 +220,7 @@ hp_dnsmasq_write_snippets() {
|
||||
local dnsmasq_dir="$1"
|
||||
local hp_dir="$2"
|
||||
local routing_mode="$3"
|
||||
local run_dir="${4:-/var/run/homeproxy-pro}"
|
||||
local ipv6_support dns_port
|
||||
local include="$dnsmasq_dir/../dnsmasq-homeproxy-pro.conf"
|
||||
local stage changed=0
|
||||
@@ -208,7 +237,7 @@ hp_dnsmasq_write_snippets() {
|
||||
# installed snippet set untouched: the staged directory is discarded, so
|
||||
# the caller keeps the snippets that are already in place instead of
|
||||
# installing a truncated one.
|
||||
if ! hp_dnsmasq_render_snippets "$stage" "$hp_dir" "$routing_mode" "$dns_port" "$ipv6_support"; then
|
||||
if ! hp_dnsmasq_render_snippets "$stage" "$hp_dir" "$routing_mode" "$dns_port" "$ipv6_support" "$run_dir"; then
|
||||
rm -rf "$stage"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* runtime/domain_ruleset.uc <source .txt> <destination .json>
|
||||
*
|
||||
* The domain-list counterpart of china_ip_ruleset.uc: turn a bare host-name
|
||||
* list into a sing-box source rule-set of `domain_suffix` rules, so the DNS
|
||||
* side of the China split reads a local file instead of downloading
|
||||
* geosite-geolocation-cn.srs from GitHub on a schedule.
|
||||
*
|
||||
* Why this exists, in the terms that decided it: the two consumers of the
|
||||
* China split - the nft mainland set and the sing-box rule-set - were reading
|
||||
* two different files. The kernel's came from china_ip4.txt, the resolver's
|
||||
* from a remote .srs, and the remote copy is what a cold start has to wait for
|
||||
* (through the node, when download_detour says so) before the inbounds bind.
|
||||
* With the local copy there is nothing to wait for and nothing to keep in step.
|
||||
*
|
||||
* The entry is a suffix, not an exact domain: china_list.txt holds registrable
|
||||
* domains ("163.com", "taobao.com"), and the DNS rule has to match every name
|
||||
* under them, which is what `domain_suffix` means to sing-box. `domain` would
|
||||
* match only the name itself and quietly stop matching www.<domain> for the
|
||||
* entire list.
|
||||
*
|
||||
* One rule holding the whole list, for the same reason china_ip_ruleset.uc
|
||||
* gives: sing-box matches rule-sets by walking a prefix-compressed structure,
|
||||
* and splitting the list would only add rules to walk per query.
|
||||
*
|
||||
* Entries are validated here rather than trusted. The strict parser on the
|
||||
* other side is the authority for what a valid name is, and one bad entry
|
||||
* makes it reject the *whole* rule-set - which would take the client's start
|
||||
* with it. A malformed line is skipped and reported, exactly like the blank
|
||||
* lines and comments a list may carry.
|
||||
*/
|
||||
'use strict';
|
||||
import { readfile, writefile, rename, unlink } from 'fs';
|
||||
|
||||
if (length(ARGV) < 2 || ARGV[0] == null || ARGV[1] == null) {
|
||||
warn(sprintf('usage: domain_ruleset.uc <source .txt> <destination .json>\n'));
|
||||
exit(1);
|
||||
}
|
||||
const src = ARGV[0];
|
||||
const dst = ARGV[1];
|
||||
const data = readfile(src);
|
||||
if (type(data) !== 'string') {
|
||||
warn(sprintf('cannot read %s\n', src));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const suffixes = [];
|
||||
let skipped = 0;
|
||||
|
||||
/* A structural hostname check, in process.
|
||||
*
|
||||
* validation() from homeproxy-pro.uc is the obvious tool and the wrong one here:
|
||||
* it shells out to /sbin/validate_data once per call, and this list is 111k
|
||||
* lines. That is 111k process spawns. china_ip_ruleset.uc makes the same call
|
||||
* for the same reason - its is_v4/is_v6 are hand-written rather than delegated.
|
||||
*
|
||||
* Deliberately structural, not exhaustive: the goal is to keep a line that
|
||||
* cannot be a domain out of the file, since one bad entry makes sing-box reject
|
||||
* the whole rule-set and take the client's start with it. The strict parser on
|
||||
* the other side stays the authority for what is really valid. */
|
||||
function is_hostname(entry) {
|
||||
if (length(entry) < 1 || length(entry) > 253)
|
||||
return false;
|
||||
|
||||
/* Anything outside this set is a path, a URL, a port, an email, or a
|
||||
* comment marker that survived the updater's filter. */
|
||||
if (!match(entry, /^[A-Za-z0-9.-]+$/))
|
||||
return false;
|
||||
|
||||
/* A leading dot, a doubled dot, or a trailing dot all mean the list is
|
||||
* not in the bare form this emits; sing-box treats them differently from
|
||||
* what the DNS rule expects to match. */
|
||||
if (substr(entry, 0, 1) == '.' || substr(entry, -1, 1) == '.')
|
||||
return false;
|
||||
if (match(entry, /\.\./))
|
||||
return false;
|
||||
|
||||
for (let label in split(entry, '.')) {
|
||||
if (length(label) < 1 || length(label) > 63)
|
||||
return false;
|
||||
/* A label may not start or end with a hyphen (RFC 1035). */
|
||||
if (substr(label, 0, 1) == '-' || substr(label, -1, 1) == '-')
|
||||
return false;
|
||||
}
|
||||
|
||||
/* An all-numeric set of labels is a bare IPv4 address, not a domain:
|
||||
* the structure above accepts "192.168.1.1" quite happily, and a
|
||||
* domain_suffix rule holding an address never matches a query. No TLD
|
||||
* is all digits, so this cannot cost a real name. IPv6 never reaches
|
||||
* here at all - it carries colons and was rejected above. */
|
||||
let all_numeric = true;
|
||||
for (let label in split(entry, '.')) {
|
||||
if (!match(label, /^[0-9]+$/)) {
|
||||
all_numeric = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_numeric)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let line in split(data, /\n/)) {
|
||||
const entry = trim(line);
|
||||
if (entry == '')
|
||||
continue;
|
||||
if (match(entry, /^[#;]/))
|
||||
continue;
|
||||
|
||||
/* The updater already strips `full:` prefixes and anything with a colon
|
||||
* (update_resources.sh's china_list case), so what arrives here is a bare
|
||||
* name. Anything that is not still a hostname is dropped rather than
|
||||
* passed on. */
|
||||
if (!is_hostname(entry)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
push(suffixes, entry);
|
||||
}
|
||||
|
||||
if (skipped > 0)
|
||||
warn(sprintf('skipped %d malformed entries in %s\n', skipped, src));
|
||||
|
||||
/* An empty rule-set is worse than no rule-set: it matches nothing, so every
|
||||
* name the list should have covered falls through to whatever the DNS side
|
||||
* defaults to. Refuse rather than install one - the previous file stays. */
|
||||
if (length(suffixes) == 0) {
|
||||
warn(sprintf('no usable domain entries in %s\n', src));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* The temporary name is a sibling so the rename stays on one filesystem: the
|
||||
* watcher must never read a half-written file. `%.J` is the same serializer
|
||||
* the config generator uses, so the output is the canonical sing-box form. */
|
||||
const rendered = sprintf('%.J\n', {
|
||||
version: 3,
|
||||
rules: [
|
||||
{ domain_suffix: suffixes }
|
||||
]
|
||||
});
|
||||
|
||||
const tmp = dst + '.tmp';
|
||||
if (!writefile(tmp, rendered)) {
|
||||
warn(sprintf('cannot write %s\n', tmp));
|
||||
exit(1);
|
||||
}
|
||||
if (!rename(tmp, dst)) {
|
||||
unlink(tmp);
|
||||
warn(sprintf('cannot install %s\n', dst));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
printf('%d domain suffixes\n', length(suffixes));
|
||||
@@ -87,8 +87,8 @@ hp_require_ucode() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# hp_crontab_drop <crontab>
|
||||
# Remove the auto-update entry from <crontab>.
|
||||
# hp_crontab_drop <crontab> [marker]
|
||||
# Remove the cron entries tagged <marker> from <crontab>.
|
||||
#
|
||||
# Not `sed -i`: the bare `-i` form is a busybox/GNU extension, and on a host with
|
||||
# BSD sed it fails ("invalid command code"), which silently left the stale entry
|
||||
@@ -96,11 +96,16 @@ hp_require_ucode() {
|
||||
# behaves identically on busybox, GNU and BSD sed, and it is what lets this
|
||||
# module be exercised off-target at all
|
||||
# (tests/runtime/test_runtime_extraction.sh stages and drives it).
|
||||
#
|
||||
# The marker is a parameter because there are two independent entries now: the
|
||||
# resource lists and the subscriptions. A shared marker would let dropping one
|
||||
# take the other with it.
|
||||
hp_crontab_drop() {
|
||||
local crontab="$1"
|
||||
local marker="${2:-${CONF}_autosetup}"
|
||||
local tmp="${crontab}.hp-new"
|
||||
|
||||
sed "/#${CONF}_autosetup/d" "$crontab" > "$tmp" 2>"/dev/null" || { rm -f "$tmp"; return 1; }
|
||||
sed "/#${marker}/d" "$crontab" > "$tmp" 2>"/dev/null" || { rm -f "$tmp"; return 1; }
|
||||
mv -f "$tmp" "$crontab" 2>"/dev/null" || { rm -f "$tmp"; return 1; }
|
||||
|
||||
# mv replaces the *file*, not its contents: the temporary was created by the
|
||||
@@ -115,6 +120,36 @@ hp_crontab_drop() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# hp_sync_resource_cron <hour>
|
||||
# Install the resource-list update cron entry. Unconditional.
|
||||
#
|
||||
# The resource lists were previously refreshed by the same cron line as the
|
||||
# subscription update, behind the *subscription's* auto_update switch. Two
|
||||
# unrelated things behind one switch, and the consequence was silent: a router
|
||||
# with subscription auto-update off kept a months-old china_ip4.txt, and with it
|
||||
# a months-old `homeproxy_mainland_addr_v4` nft set - the firewall was deciding
|
||||
# "mainland or not" from a list nobody was maintaining. The subscription switch
|
||||
# says nothing about the user wanting stale routing data, so the two are now
|
||||
# scheduled independently.
|
||||
#
|
||||
# The hour is a constant rather than a setting. This is a maintenance job with
|
||||
# nothing to configure, and adding an option for it is exactly the kind of
|
||||
# switch that ends up defaulted and never touched. Two hours earlier than the
|
||||
# subscription default (2) so the two runs of uclient-fetch do not collide.
|
||||
#
|
||||
# The entry runs update_resources_cron.sh, which reloads the service only when a
|
||||
# list actually changed - so a day where the upstream lists did not move costs
|
||||
# one round of requests and no interruption.
|
||||
HP_RESOURCE_CRON_HOUR=3
|
||||
hp_sync_resource_cron() {
|
||||
hp_crontab_drop "/etc/crontabs/root" "${CONF}_resource_cron" \
|
||||
|| log "Warning: failed to drop the previous resource-update cron entry."
|
||||
printf '0 %s * * * %s/scripts/update_resources_cron.sh #%s_resource_cron\n' \
|
||||
"$HP_RESOURCE_CRON_HOUR" "$HP_DIR" "$CONF" >> "/etc/crontabs/root" \
|
||||
|| log "Warning: failed to install the resource-update cron entry."
|
||||
/etc/init.d/cron restart >"/dev/null" 2>&1 || log "Warning: failed to restart cron."
|
||||
}
|
||||
|
||||
# hp_sync_autoupdate_cron <enabled> <hour>
|
||||
# Install (or drop) the subscription auto-update cron entry. <enabled> is a
|
||||
# config_get_bool result; the entry is only touched when it is "1".
|
||||
@@ -140,7 +175,7 @@ hp_sync_autoupdate_cron() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
hp_crontab_drop "/etc/crontabs/root" \
|
||||
hp_crontab_drop "/etc/crontabs/root" "${CONF}_autosetup" \
|
||||
|| log "Warning: failed to drop the previous auto-update cron entry."
|
||||
printf '0 %s * * * %s/scripts/update_crond.sh #%s_autosetup\n' \
|
||||
"$auto_update_time" "$HP_DIR" "$CONF" >> "/etc/crontabs/root" \
|
||||
@@ -149,10 +184,16 @@ hp_sync_autoupdate_cron() {
|
||||
}
|
||||
|
||||
# hp_clear_autoupdate_cron
|
||||
# Drop the auto-update cron entry and restart cron. Used by stop_service.
|
||||
# Drop both cron entries and restart cron. Used by stop_service.
|
||||
#
|
||||
# Both markers: stop_service has to leave no homeproxy-pro cron entry behind, and
|
||||
# the resource entry is installed unconditionally, so dropping only the
|
||||
# subscription one would leak a daily job on every stop.
|
||||
hp_clear_autoupdate_cron() {
|
||||
hp_crontab_drop "/etc/crontabs/root" \
|
||||
|| log "Warning: failed to drop the auto-update cron entry."
|
||||
hp_crontab_drop "/etc/crontabs/root" "${CONF}_autosetup" \
|
||||
|| log "Warning: failed to drop the previous auto-update cron entry."
|
||||
hp_crontab_drop "/etc/crontabs/root" "${CONF}_resource_cron" \
|
||||
|| log "Warning: failed to drop the previous resource-update cron entry."
|
||||
/etc/init.d/cron restart >"/dev/null" 2>&1 || log "Warning: failed to restart cron."
|
||||
}
|
||||
|
||||
@@ -251,6 +292,29 @@ hp_prepare_runtime_files() {
|
||||
log "WARNING: mainland IPv6 cannot be told apart from foreign IPv6, so IPv6 is passed through UNPROXIED (both sides of the split degrade). Run: ${hp_dir}/scripts/update_resources.sh china_ip6"
|
||||
fi
|
||||
|
||||
# The DNS half of the China split, same arrangement as china_ip4.json above:
|
||||
# the dns rule sends the list's domains to china-dns, and it reads
|
||||
# china-domain.json rather than a remote geosite-geolocation-cn.srs so a
|
||||
# cold start has nothing to download. Generated here for the same reason -
|
||||
# an install that predates the file, or a list replaced while the service
|
||||
# was stopped, must still get one, or the client config names a file that
|
||||
# does not exist and sing-box refuses to start.
|
||||
#
|
||||
# A missing china_list.txt is not an error: the generator then emits no
|
||||
# DNS-side rule for it, and the split degrades to the resolver default
|
||||
# rather than failing the start.
|
||||
if [ "$client_enabled" = "1" ] && [ -f "$hp_dir/resources/china_list.txt" ]; then
|
||||
if ucode -S "$hp_dir/scripts/runtime/domain_ruleset.uc" \
|
||||
"$hp_dir/resources/china_list.txt" "$hp_dir/resources/china-domain.json" >>"$LOG_PATH" 2>&1; then
|
||||
chown sing-box:sing-box "$hp_dir/resources/china-domain.json" 2>"/dev/null" \
|
||||
|| log "Warning: failed to hand ${hp_dir}/resources/china-domain.json to sing-box."
|
||||
else
|
||||
# Same reasoning as the v6 branch above: the helper refuses to
|
||||
# write an empty rule-set, and the previous one is still valid.
|
||||
log "Warning: could not generate ${hp_dir}/resources/china-domain.json; the DNS side keeps the previous list."
|
||||
fi
|
||||
fi
|
||||
|
||||
# cache_file is enabled for bypass_mainland_china and custom alike
|
||||
# (generator/common.uc:attachExperimental). The file has to exist before
|
||||
# the client jail binds it, or sing-box cannot create it in there.
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
# Subscription auto-update, run from the crontab entry that
|
||||
# runtime/service.sh installs when subscription.auto_update is on.
|
||||
#
|
||||
# The resource lists used to be refreshed from this same script. They are not
|
||||
# any more: they have their own entry (update_resources_cron.sh), because a
|
||||
# subscription switch had been deciding whether the firewall's mainland address
|
||||
# set was still current. See hp_sync_resource_cron() in runtime/service.sh.
|
||||
|
||||
SCRIPTS_DIR="/etc/homeproxy-pro/scripts"
|
||||
|
||||
for i in "china_ip4" "china_ip6" "gfw_list" "china_list"; do
|
||||
"$SCRIPTS_DIR"/update_resources.sh "$i"
|
||||
done
|
||||
|
||||
"$SCRIPTS_DIR"/update_subscriptions.uc
|
||||
@@ -236,6 +236,23 @@ check_list_update() {
|
||||
log "[$(to_upper "$listtype")] Warning: could not regenerate $listtype.json (list has no usable CIDR entry?); the route side keeps the previous list."
|
||||
fi
|
||||
;;
|
||||
"china_list")
|
||||
# The DNS half of the same split. It reads this file rather than
|
||||
# a remote geosite-geolocation-cn.srs for the same reasons
|
||||
# china_ip4.json exists: one list with two readers, and nothing for
|
||||
# a cold start to download before the inbounds bind.
|
||||
#
|
||||
# The sed above rewrote the file in place, so this runs after it
|
||||
# and sees the cleaned list. `full:` prefixes and anything with a
|
||||
# colon are gone by now; what is left is what the helper accepts.
|
||||
if ucode -S "$SCRIPT_DIR/runtime/domain_ruleset.uc" \
|
||||
"$RESOURCES_DIR/$listtype.txt" "$RESOURCES_DIR/china-domain.json" >>"$LOG_PATH" 2>&1; then
|
||||
log "[CHINA_LIST] DNS-side rule-set regenerated."
|
||||
chown sing-box:sing-box "$RESOURCES_DIR/china-domain.json" 2>"/dev/null"
|
||||
else
|
||||
log "[CHINA_LIST] Warning: could not regenerate china-domain.json (list has no usable domain entry?); the DNS side keeps the previous list."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
else
|
||||
rm -f "$RUN_DIR/$listname"
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Refresh the four resource lists (china_ip4 / china_ip6 / gfw_list /
|
||||
# china_list) and reload the service only when one of them actually moved.
|
||||
#
|
||||
# Scheduled unconditionally - see hp_sync_resource_cron() in runtime/service.sh
|
||||
# for why this is not behind the subscription auto_update switch.
|
||||
#
|
||||
# Why a reload is needed at all, when the sing-box side reloads itself: the
|
||||
# lists feed two consumers. update_resources.sh regenerates the sing-box
|
||||
# rule-sets from them, and sing-box watches those files and picks a new one up
|
||||
# in place. The firewall reads the same lists directly when firewall_post.ut is
|
||||
# rendered into the nft sets, and that only happens on an fw4 reload - so
|
||||
# without this the kernel kept routing from the previous list while sing-box
|
||||
# had already moved to the new one, and the two halves of the same split
|
||||
# disagreed. That window is invisible: mainland traffic still works, it just
|
||||
# stops being mainland for whatever the new entries added.
|
||||
#
|
||||
# update_resources.sh exits 3 when a list is already current, so a day with no
|
||||
# upstream movement costs one round of requests and no interruption.
|
||||
|
||||
SCRIPTS_DIR="/etc/homeproxy-pro/scripts"
|
||||
NAME="homeproxy-pro"
|
||||
RUN_DIR="/var/run/$NAME"
|
||||
LOG_PATH="$RUN_DIR/$NAME.log"
|
||||
|
||||
mkdir -p "$RUN_DIR" 2>/dev/null
|
||||
|
||||
log() {
|
||||
printf '%s %s\n' "$(date "+%Y-%m-%d %H:%M:%S")" "$*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
changed=0
|
||||
for i in "china_ip4" "china_ip6" "gfw_list" "china_list"; do
|
||||
"$SCRIPTS_DIR"/update_resources.sh "$i"
|
||||
case "$?" in
|
||||
0) changed=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ "$changed" = "1" ] || exit 0
|
||||
|
||||
log "A resource list changed; reloading $NAME so the nft sets catch up."
|
||||
if ! /etc/init.d/$NAME reload >>"$LOG_PATH" 2>&1; then
|
||||
log "Warning: the $NAME reload after a resource update failed; the firewall still uses the previous list."
|
||||
fi
|
||||
@@ -157,7 +157,7 @@ hp_rearm_intercept() {
|
||||
[ -f "$RUN_DIR/intercept-released" ] || return 0
|
||||
rm -f "$RUN_DIR/intercept-released"
|
||||
log "Client is up again; reinstalling the intercept layer."
|
||||
hp_dnsmasq_write_snippets "$DNSMASQ_DIR" "$HP_DIR" "$2"
|
||||
hp_dnsmasq_write_snippets "$DNSMASQ_DIR" "$HP_DIR" "$2" "$RUN_DIR"
|
||||
hp_net_setup "$1" "$2"
|
||||
# hp_firewall_apply already writes $RUN_DIR/failed-firewall on its own
|
||||
# failure, but the helper's caller is service_started, which only reads
|
||||
@@ -265,6 +265,11 @@ start_service() {
|
||||
config_get_bool auto_update "subscription" "auto_update" "0"
|
||||
config_get auto_update_time "subscription" "auto_update_time" "2"
|
||||
hp_sync_autoupdate_cron "$auto_update" "$auto_update_time"
|
||||
# Unconditional, and independent of the switch above: the resource
|
||||
# lists decide the firewall's mainland set, and that going stale is
|
||||
# not something "update my subscriptions" should have been able to
|
||||
# cause.
|
||||
hp_sync_resource_cron
|
||||
|
||||
# The DNS snippets are the other half of the intercept layer: without
|
||||
# them the LAN resolves through the ISP while the nft rules still send
|
||||
@@ -273,7 +278,7 @@ start_service() {
|
||||
# leaving an empty snippet behind; record it the same way a firewall
|
||||
# failure is recorded, so the configuration is not promoted to
|
||||
# known-good and the log names the real problem.
|
||||
if ! hp_dnsmasq_write_snippets "$DNSMASQ_DIR" "$HP_DIR" "$routing_mode"; then
|
||||
if ! hp_dnsmasq_write_snippets "$DNSMASQ_DIR" "$HP_DIR" "$routing_mode" "$RUN_DIR"; then
|
||||
log "Error: the dnsmasq snippets were not installed; LAN DNS keeps using the previous configuration."
|
||||
printf '%s\n' "dns" > "$RUN_DIR/failed-dns"
|
||||
fi
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"file": {
|
||||
"/var/run/homeproxy-pro/homeproxy-pro.log": [ "read" ],
|
||||
"/var/run/homeproxy-pro/sing-box-c.log": [ "read" ],
|
||||
"/var/run/homeproxy-pro/sing-box-s.log": [ "read" ]
|
||||
"/var/run/homeproxy-pro/sing-box-s.log": [ "read" ],
|
||||
"/var/run/homeproxy-pro/debug.log": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ],
|
||||
@@ -31,6 +32,7 @@
|
||||
"luci.homeproxy-pro": [
|
||||
"acllist_write",
|
||||
"certificate_write",
|
||||
"debug_report",
|
||||
"log_clean",
|
||||
"resources_update",
|
||||
"singbox_generator",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { cursor } from 'uci';
|
||||
|
||||
import { fetchBinary, isBinary, isValidECHConfig, isValidPEM, shellQuote, executeCommand } from '/etc/homeproxy-pro/scripts/homeproxy-pro.uc';
|
||||
import { parse_uri } from '/etc/homeproxy-pro/scripts/parser/uri.uc';
|
||||
import { buildDebugReport, DEBUG_REPORT_PATH } from '/etc/homeproxy-pro/scripts/runtime/debug_report.uc';
|
||||
|
||||
/* Kanged from ucode/luci - replaced by the imported shellQuote() so every shell
|
||||
* argument in this file goes through one helper. */
|
||||
@@ -473,6 +474,46 @@ const methods = {
|
||||
|
||||
return { running, log_tail: tail };
|
||||
}
|
||||
},
|
||||
|
||||
/* Build a single self-contained diagnostic report and leave it where the
|
||||
* browser can read it. The report is generated, not fetched: the ACL
|
||||
* grants the browser `read` on the output path and nothing else, so the
|
||||
* commands that produce it (nft, ip rule, the package queries) stay
|
||||
* behind this method rather than becoming browser-reachable ones.
|
||||
*
|
||||
* RUN_DIR is tmpfs, so the report does not survive a reboot and never
|
||||
* occupies flash - the reader downloads it, and the copy they keep is
|
||||
* the one that matters. A fixed filename means the ACL grants exactly
|
||||
* one path and the button always reads the freshest report.
|
||||
*
|
||||
* The collector degrades per probe instead of aborting, so on a machine
|
||||
* without netfilter the report still renders the sections that do work.
|
||||
* The catch is here rather than inside buildDebugReport() so a bug in
|
||||
* the collector surfaces as a toast with the ucode error type instead
|
||||
* of an empty file the reader has to guess about. */
|
||||
debug_report: {
|
||||
call: function() {
|
||||
const uci = cursor();
|
||||
|
||||
let report = null;
|
||||
try {
|
||||
report = buildDebugReport(uci);
|
||||
} catch (e) {
|
||||
return {
|
||||
result: false,
|
||||
error: sprintf('diagnostic report failed (%s): %s', e.type, e.message)
|
||||
};
|
||||
}
|
||||
|
||||
if (type(report) !== 'string' || report === '') {
|
||||
return { result: false, error: 'the diagnostic report came back empty' };
|
||||
}
|
||||
|
||||
writefile(DEBUG_REPORT_PATH, report);
|
||||
|
||||
return { result: true, path: DEBUG_REPORT_PATH, size: length(report) };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ Two further host differences are bridged so the remaining checks still run:
|
||||
| `tests/ucode/test_parser_flatten.uc` | Round-trip invariant for the canonical-Node pipeline: `flatten(normalize(parse_uri(uri)))` must equal the parser's flat UCI dict field-for-field for every supported scheme. This is what stops the parser, the Loader's `PROTOCOL_OPTIONS` (now derived from `parser/mapping.uc`) and the Repository's flatten from drifting apart. |
|
||||
| `tests/ucode/test_subscription_repository.uc` | Integration test for `subscription/repository.uc`: stages a sandboxed UCI file with a user node, a kept subscription node, and a dropped subscription node, then runs `Repository.apply_nodes` and asserts (a) the user node is untouched, (b) the kept node has the new fields and no stale fields, (c) the dropped node is gone, (d) a brand-new node is added under `md5(group+label)`. PR-03 added the `apply_main_node_refs` paths (urltest prune + missing-target switch + reset-to-nil) and the `scrub_stale_urltest_refs` pass to the same test. |
|
||||
| `tests/ucode/test_generators.sh` | Runs `generate_client.uc`/`generate_server.uc` against the UCI fixtures in `tests/fixtures/generators/` inside a scratch directory (the `uci` cursor and `HP_DIR`/`RUN_DIR` are redirected) and validates each result with `sing-box check`. The `redirect` case is the product's default `proxy_mode` and asserts, from the emitted JSON, that every dialling outbound carries the runtime `routing_mark`. |
|
||||
| `tests/ucode/test_dns_proxy_list.sh` | The `proxy_list.txt` → `dns.rules` plumbing and the bootstrap resolver, neither of which `run_case()` in the generator suite can see (it truncates both resource lists before every case and the fixture has no `china_dns_server` option). Patches the client fixture to each of the four proxy modes with both lists populated, and asserts the DNS rule order (`direct-domain` < `proxy-domain` via `main-dns` < the SVCB/HTTPS reject, `geosite-cn` last when the mode emits it) plus the routing half's `"tag": "proxy-domain"` and `route.final: main-out`. Then, separately, the bootstrap resolver: it is derived from `china_dns_server`, so a bare IP there must make `main-dns` resolve its own hostname through `bootstrap-dns` with no `domain_resolver` of its own, and a DoH URL there (a hostname, which cannot resolve a hostname) must emit no `bootstrap-dns` server at all and leave `main-dns` on `default-dns`. |
|
||||
| `tests/ucode/test_dns_proxy_list.sh` | The `proxy_list.txt` → `dns.rules` plumbing and the bootstrap resolver, neither of which `run_case()` in the generator suite can see (it truncates both resource lists before every case and the fixture has no `china_dns_server` option). Patches the client fixture to each of the four proxy modes with both lists populated, and asserts the DNS rule order (`direct-domain` < `proxy-domain` via `main-dns` < the SVCB/HTTPS reject, `china-domain` last when the mode emits it) plus the routing half's `"tag": "proxy-domain"` and `route.final: main-out`. Then, separately, the bootstrap resolver: it is derived from `china_dns_server`, so a bare IP there must make `main-dns` resolve its own hostname through `bootstrap-dns` with no `domain_resolver` of its own, and a DoH URL there (a hostname, which cannot resolve a hostname) must emit no `bootstrap-dns` server at all and leave `main-dns` on `default-dns`. |
|
||||
| `tests/toolchain/build-ucode-linux.sh` | Builds the local ucode toolchain described above; not part of `tests/run.sh`. |
|
||||
| `tests/toolchain/validate-data.sh` | Off-target stand-in for `/sbin/validate_data`, used through `HP_VALIDATE_DATA`; not part of `tests/run.sh`. |
|
||||
| `tests/ucode/test_ucode_grammar.sh` | Pins the ucode dialect: the toolchain must reject `export function ... }` without `;` and object/array destructuring, and must accept the constructs the package uses (`?.`/`??`, object spread, computed keys, template literals). |
|
||||
|
||||
@@ -2223,9 +2223,9 @@ fi
|
||||
# proxy resolver even though the route side would now send it direct.
|
||||
cn_fb_block="$(sed -n '/cn_fallback/,/^ }/p' "$SCRIPTS/generator/dns.uc" || true)"
|
||||
if printf '%s' "$cn_fb_block" | grep -q "china-ip6"; then
|
||||
pass "the cn-fallback response match covers china-ip6 as well as geoip-cn"
|
||||
pass "the cn-fallback response match covers china-ip6 as well as china-ip"
|
||||
else
|
||||
fail "cn-fallback still matches geoip-cn only; the DNS half of the IPv6 split"
|
||||
fail "cn-fallback still matches china-ip only; the DNS half of the IPv6 split"
|
||||
fail "is not in agreement with the route half"
|
||||
fi
|
||||
|
||||
@@ -2626,118 +2626,115 @@ else
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== guard 52: the startup fallback stays opt-in, and the two sides stay in step =="
|
||||
echo "== guard 52: the China split is local, and both halves read one list =="
|
||||
|
||||
# ruleset_safe_start points every remote rule-set that has no initial file of
|
||||
# its own at an EMPTY rule-set, so sing-box starts immediately instead of
|
||||
# fetching during initialization - before the inbounds bind. On a cold cache
|
||||
# that is the difference between a router that comes up and one that waits on
|
||||
# raw.githubusercontent.com, through the node, before it will listen at all.
|
||||
# The split that keeps mainland traffic direct used to be fed by two remote
|
||||
# rule-sets downloaded from SagerNet's repositories: geoip-cn on the route
|
||||
# side, geosite-cn on the DNS side. Three things were wrong with that and all
|
||||
# three are silent when they come back:
|
||||
#
|
||||
# It is opt-in because the fallback is empty on purpose: a rule-set with no
|
||||
# rules matches nothing, so everything it would have split falls through to
|
||||
# `final`. On bypass_mainland_china that means mainland destinations can be
|
||||
# proxied until the download succeeds. That is a routing change, so the
|
||||
# default must stay off, exactly as sniffer_advanced_mode's does.
|
||||
# - a cold start had to fetch them before the inbounds bind, through the
|
||||
# node, so the proxy's own start depended on the CDN being reachable
|
||||
# through the proxy;
|
||||
# - the kernel's half (homeproxy_mainland_addr_v4, from china_ip4.txt) and
|
||||
# the resolver's half came from different files, and route.uc once carried
|
||||
# a corrective rule plus a long comment because they disagreed;
|
||||
# - every day of that, the resource lists behind the kernel's half were only
|
||||
# refreshed if the user happened to have subscription auto-update on.
|
||||
#
|
||||
# The rest of the guard is about the mechanism being one mechanism: the CLI
|
||||
# writes the file and the generator points at it, and a disagreement between
|
||||
# them is silent and fatal (a wrong-format or missing initial file is ignored
|
||||
# by sing-box, which then blocks startup exactly as if none existed).
|
||||
SAFE_START="$(python3 - "$ROOT" "$SCRIPTS" <<'PY'
|
||||
# They are local files now, each generated from a list the resource updater
|
||||
# maintains, and the firewall renders its nft set from those same lists. This
|
||||
# guard pins the arrangement rather than the feature it replaced: the tags the
|
||||
# two halves name have to be the same tags route.uc declares, they have to be
|
||||
# declared local, and nothing may reintroduce a remote download for them.
|
||||
CN_LOCAL="$(python3 - "$ROOT" "$SCRIPTS" <<'PY'
|
||||
import pathlib, re, sys
|
||||
|
||||
root, scripts = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
|
||||
config_uc = (root / 'root/etc/config/homeproxy-pro').read_text(encoding='utf-8')
|
||||
context_uc = (scripts / 'generator/context.uc').read_text(encoding='utf-8')
|
||||
route_uc = (scripts / 'generator/route.uc').read_text(encoding='utf-8')
|
||||
ruleset_uc = (scripts / 'generator/ruleset.uc').read_text(encoding='utf-8')
|
||||
dns_uc = (scripts / 'generator/dns.uc').read_text(encoding='utf-8')
|
||||
common_uc = (scripts / 'generator/common.uc').read_text(encoding='utf-8')
|
||||
homeproxy_uc = (scripts / 'homeproxy-pro.uc').read_text(encoding='utf-8')
|
||||
client_uc = (scripts / 'generate_client.uc').read_text(encoding='utf-8')
|
||||
service_sh = (scripts / 'runtime/service.sh').read_text(encoding='utf-8')
|
||||
update_sh = (scripts / 'update_resources.sh').read_text(encoding='utf-8')
|
||||
fw_ut = (scripts / 'firewall_post.ut').read_text(encoding='utf-8')
|
||||
problems = []
|
||||
|
||||
def strip_comments(src):
|
||||
src = re.sub(r'/\*[\s\S]*?\*/', '', src)
|
||||
return '\n'.join(l for l in src.split('\n') if not l.lstrip().startswith(('#', '//', '*')))
|
||||
|
||||
# (a) The default, in both places it is stated. Same shape as guard 39.
|
||||
if "ruleset_safe_start: dm.general.ruleset_safe_start || '0'" not in context_uc:
|
||||
problems.append("context.uc no longer falls back to '0' for ruleset_safe_start; a missing "
|
||||
"UCI value would have to mean something other than the safe default")
|
||||
if not re.search(r"^\toption ruleset_safe_start '0'$", config_uc, re.M):
|
||||
problems.append("the package-shipped /etc/config/homeproxy-pro no longer ships ruleset_safe_start "
|
||||
"'0'; upgrading users would silently start routing with empty rule-sets")
|
||||
route, dns = strip_comments(route_uc), strip_comments(dns_uc)
|
||||
|
||||
# (b) An initial_path may only be emitted for a fallback that exists. Both
|
||||
# sites, because one without the other is the silent failure.
|
||||
if "ctx.ruleset_initial" not in strip_comments(route_uc):
|
||||
problems.append('route.uc emits initial_path without checking ctx.ruleset_initial; a built-in '
|
||||
'could be pointed at a file the CLI never wrote')
|
||||
if "ctx.ruleset_initial" not in strip_comments(ruleset_uc):
|
||||
problems.append('ruleset.uc emits initial_path without checking ctx.ruleset_initial')
|
||||
if 'ruleset_initial' not in client_uc:
|
||||
problems.append('generate_client.uc no longer writes the fallbacks, so ctx.ruleset_initial is '
|
||||
'always empty and the opt-in does nothing')
|
||||
# (a) None of the three may be a remote rule-set again. A `type: 'remote'`
|
||||
# in the built-in block brings back both the cold-start download and the
|
||||
# second copy of the data.
|
||||
decl = re.search(r'if \(declaresBuiltinRuleSets.*?\n\t\}', route, re.S)
|
||||
if not decl:
|
||||
problems.append('route.uc no longer declares the built-in rule-sets behind '
|
||||
'declaresBuiltinRuleSets(); the China split would have no data source')
|
||||
else:
|
||||
block = decl.group(0)
|
||||
for tag in ('china-ip', 'china-ip6', 'china-domain'):
|
||||
if "tag: '%s'" % tag not in block:
|
||||
problems.append("route.uc no longer declares '%s'; the rule that matches it "
|
||||
"would fail the whole configuration with rule-set not found" % tag)
|
||||
if "type: 'remote'" in block:
|
||||
problems.append("route.uc declares a built-in as type: 'remote' again; a cold start "
|
||||
"would have to download it before the inbounds bind")
|
||||
|
||||
# (c) The built-ins come from the shared list. route.uc hard-coding the tags
|
||||
# is how the two readers drifted in the first place: the CLI had no idea
|
||||
# these rule-sets existed.
|
||||
for tag in ('geoip-cn', 'geosite-cn'):
|
||||
if re.search(r"tag:\s*'%s'" % re.escape(tag), strip_comments(route_uc)):
|
||||
problems.append("route.uc hard-codes tag '%s' again; it must come from "
|
||||
"BUILTIN_REMOTE_RULE_SETS so the CLI can create its fallback" % tag)
|
||||
if 'BUILTIN_REMOTE_RULE_SETS' not in route_uc:
|
||||
problems.append('route.uc no longer reads BUILTIN_REMOTE_RULE_SETS')
|
||||
if 'BUILTIN_REMOTE_RULE_SETS' not in client_uc:
|
||||
problems.append('generate_client.uc no longer reads BUILTIN_REMOTE_RULE_SETS')
|
||||
# (b) The two halves must name the same tags. This is the part that actually
|
||||
# drifted before: the route rule named geoip-cn and the DNS rule named
|
||||
# geosite-cn, and the fix was a second rule rather than one list.
|
||||
if re.search(r"rule_set:\s*'geoip-cn'", route + dns):
|
||||
problems.append("a China rule names 'geoip-cn' again; the address list is china-ip, "
|
||||
"generated from the same china_ip4.txt the nft set is rendered from")
|
||||
if re.search(r"rule_set:\s*'geosite-cn'", route + dns):
|
||||
problems.append("a China rule names 'geosite-cn' again; the domain list is china-domain, "
|
||||
"generated from china_list.txt")
|
||||
if "rule_set: 'china-domain'" not in dns:
|
||||
problems.append('dns.uc no longer routes the China domain list; those domains would fall '
|
||||
'through to the mode default and resolve through the proxy')
|
||||
|
||||
# (d) The shipped constant, byte for byte. It is a binary artifact in the
|
||||
# tree, and a corrupted one fails in the worst possible way: sing-box
|
||||
# cannot parse the initial file, ignores it, and blocks startup - the
|
||||
# exact failure the file exists to prevent. Comparing the bytes here is
|
||||
# the only thing standing between a bad `cp` and that.
|
||||
expected_srs = bytes([0x53, 0x52, 0x53, 0x02, 0x78, 0xDA, 0x62, 0x00,
|
||||
0x0C, 0x00, 0x00, 0x01, 0x00, 0x01])
|
||||
srs = root / 'root/etc/homeproxy-pro/ruleset/initial/empty.srs'
|
||||
src = root / 'root/etc/homeproxy-pro/ruleset/initial/empty.source.json'
|
||||
if not srs.is_file():
|
||||
problems.append('the offline fallback %s is missing from the package payload' % srs)
|
||||
elif srs.read_bytes() != expected_srs:
|
||||
problems.append('%s is %s, not the documented %s; it is the last-resort binary '
|
||||
'fallback and a corrupted one makes sing-box block startup'
|
||||
% (srs.name, srs.read_bytes().hex(' '), expected_srs.hex(' ')))
|
||||
if not src.is_file():
|
||||
problems.append('the empty source %s is missing from the package payload' % src)
|
||||
elif src.read_bytes() != b'{"version":3,"rules":[]}':
|
||||
problems.append('%s is %r, not the documented empty source' % (src.name, src.read_bytes()))
|
||||
# (c) The fallback tags have to be declared under the same condition they are
|
||||
# referenced, or sing-box rejects the config with an undeclared tag.
|
||||
for name, src, tag in (('route', route, 'china-ip6'), ('dns', dns, 'china-ip6')):
|
||||
if re.search(r"rule_set:\s*'china-ip6'", src) and 'china_ip6_ready' not in src:
|
||||
problems.append('%s names china-ip6 without gating on china_ip6_ready; the declaration '
|
||||
'is conditional, so this is an undeclared tag' % name)
|
||||
|
||||
# (e) The fallbacks live inside the rule-set archive, so the existing jail
|
||||
# mount of HP_DIR and the existing path whitelist both already cover them.
|
||||
# A directory outside the archive would need a whitelist change and a new
|
||||
# jail mount, and neither exists.
|
||||
m = re.search(r"RULESET_INITIAL_DIR\s*=\s*HP_DIR \+ '([^']*)'", homeproxy_uc)
|
||||
if not m:
|
||||
problems.append('RULESET_INITIAL_DIR is gone or no longer derived from HP_DIR')
|
||||
elif not m.group(1).startswith('/ruleset/'):
|
||||
problems.append('RULESET_INITIAL_DIR is %r, outside the rule-set archive; the jail mount and '
|
||||
'the path whitelist would not cover it' % m.group(1))
|
||||
if 'RULESET_INITIAL_DIR' not in common_uc and 'BUILTIN_REMOTE_RULE_SETS' not in common_uc:
|
||||
problems.append('common.uc no longer carries the shared built-in list')
|
||||
# (d) Both halves of each list have to be generated by something that runs.
|
||||
# domain_ruleset.uc is the newest of the three and the only one with no
|
||||
# long history pointing at it, so it is the one that can quietly go
|
||||
# missing and leave china-domain.json absent - which degrades the DNS
|
||||
# split with nothing in the log.
|
||||
for name, src in (('runtime/service.sh', service_sh), ('update_resources.sh', update_sh)):
|
||||
if 'domain_ruleset.uc' not in src:
|
||||
problems.append('%s no longer invokes domain_ruleset.uc; china-domain.json is only '
|
||||
'produced there, and a missing file silently drops the DNS half of '
|
||||
'the split' % name)
|
||||
if 'china_ip_ruleset.uc' not in service_sh:
|
||||
problems.append('runtime/service.sh no longer invokes china_ip_ruleset.uc; a cold install '
|
||||
'would have no china_ip4.json and the route half would be missing too')
|
||||
|
||||
# (e) The kernel's half has to keep coming from the same .txt the generator's
|
||||
# half is made from. This is the pair that disagreed once.
|
||||
if "resources_dir + '/china_ip4.txt'" not in fw_ut:
|
||||
problems.append('firewall_post.ut no longer renders the mainland set from china_ip4.txt; '
|
||||
'the kernel and the resolver would decide "mainland" from two lists again')
|
||||
|
||||
print('\n'.join(problems))
|
||||
PY
|
||||
)" || SAFE_START="__SCAN_FAILED__"
|
||||
if [ "$SAFE_START" = "__SCAN_FAILED__" ]; then
|
||||
fail "the safe-start scan could not run - fix the guard before trusting a pass"
|
||||
elif [ -z "$SAFE_START" ]; then
|
||||
pass "the startup fallback is opt-in, both sides read one shared list, and the"
|
||||
pass " shipped constant is byte-for-byte the documented one"
|
||||
)" || CN_LOCAL="__SCAN_FAILED__"
|
||||
if [ "$CN_LOCAL" = "__SCAN_FAILED__" ]; then
|
||||
fail "the China-split scan could not run - fix the guard before trusting a pass"
|
||||
elif [ -z "$CN_LOCAL" ]; then
|
||||
pass "the China split is three local files, the two halves name the same tags, and"
|
||||
pass " each half is generated by something that runs on start and on update"
|
||||
else
|
||||
fail "the startup fallback has been broken or silently enabled:"
|
||||
printf ' %s\n' "$SAFE_START"
|
||||
fail "the China split has drifted off one list:"
|
||||
printf ' %s\n' "$CN_LOCAL"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== guard 53: every dm.general field the generators read is one the Loader lists =="
|
||||
|
||||
@@ -2748,7 +2745,7 @@ echo "== guard 53: every dm.general field the generators read is one the Loader
|
||||
# left the option off.
|
||||
#
|
||||
# That is not a hypothetical. ruleset_safe_start was added to
|
||||
# /etc/config/homeproxy-pro and read in context.uc, and nothing said the key was
|
||||
# /etc/config/homeproxy-pro and read in context.uc (removed in r41), and nothing said the key was
|
||||
# missing: the configuration generated cleanly, `sing-box check` passed, every
|
||||
# test that did not look for the feature's effect was green, and the feature
|
||||
# was simply never on. Only the e2e case that asserts an initial_path exists
|
||||
@@ -2888,6 +2885,127 @@ else
|
||||
printf ' %s\n' "$FETCH_LAYER"
|
||||
fi
|
||||
|
||||
# guard 55: every join() call passes the separator first.
|
||||
#
|
||||
# ucode's join(sep, list) does not validate its arguments. Swapped, it
|
||||
# returns null rather than raising, and null + '\n' is the string "null\n" -
|
||||
# a non-empty file naming nothing. That shipped in r41: the node-address
|
||||
# export wrote "null", dnsmasq rendered nftset=/.null/... and the node
|
||||
# bypass silently did nothing while every existence check stayed green.
|
||||
#
|
||||
# This is cheap to pin because every call is a literal, so an argument that
|
||||
# is a list literal or a call returning one is the tell.
|
||||
join_bad="$(python3 - "$SCRIPTS" "$RPC" <<'PY_JOIN'
|
||||
import re, sys, pathlib
|
||||
|
||||
def strip_comments(src):
|
||||
src = re.sub(r"/\*.*?\*/", " ", src, flags=re.S)
|
||||
return re.sub(r"//[^\n]*", " ", src)
|
||||
|
||||
def top_level_first(args):
|
||||
"""First argument, split on commas that are not inside quotes or parens -
|
||||
the separator itself is routinely ', '."""
|
||||
depth, j = 0, 0
|
||||
while j < len(args):
|
||||
c = args[j]
|
||||
if c in "'\"":
|
||||
k = j + 1
|
||||
while k < len(args) and args[k] != c:
|
||||
k += 1
|
||||
j = k + 1; continue
|
||||
if c in "([{":
|
||||
depth += 1
|
||||
elif c in ")]}":
|
||||
depth -= 1
|
||||
elif c == "," and depth == 0:
|
||||
return args[:j].strip()
|
||||
j += 1
|
||||
return args.strip()
|
||||
|
||||
def join_args(src, i):
|
||||
depth, j, out = 0, i, []
|
||||
while j < len(src):
|
||||
c = src[j]
|
||||
if c in "'\"":
|
||||
k = j + 1
|
||||
while k < len(src) and src[k] != c:
|
||||
k += 1
|
||||
out.append(src[j:k + 1]); j = k + 1; continue
|
||||
if c == "(":
|
||||
depth += 1
|
||||
if depth == 1:
|
||||
j += 1; continue
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return "".join(out), j
|
||||
out.append(c); j += 1
|
||||
return None, j
|
||||
|
||||
bad = []
|
||||
for root in sys.argv[1:]:
|
||||
p = pathlib.Path(root)
|
||||
files = ([p] if p.is_file()
|
||||
else sorted(p.rglob("*.uc")) + sorted(p.rglob("*.ut")))
|
||||
for f in files:
|
||||
src = strip_comments(f.read_text(errors="replace"))
|
||||
for m in re.finditer(r"\bjoin\s*\(", src):
|
||||
args, _ = join_args(src, m.end() - 1)
|
||||
if args is None:
|
||||
continue
|
||||
first = top_level_first(args)
|
||||
# A separator is a string literal. Anything else in that slot -
|
||||
# a call, a subscript, a bare identifier - is the swapped form,
|
||||
# and ucode answers it with null instead of raising.
|
||||
if not re.fullmatch(r"'[^']*'|\"[^\"]*\"", first):
|
||||
bad.append("%s: join(%s, ...)" % (f.name, first))
|
||||
print("\n".join(sorted(set(bad))))
|
||||
PY_JOIN
|
||||
)"
|
||||
if [ -n "$join_bad" ]; then
|
||||
fail "a join() call does not pass a string literal first (ucode takes join(sep, list), and"
|
||||
fail "swapped it returns null silently instead of raising):"
|
||||
printf ' %s\n' "$join_bad"
|
||||
else
|
||||
pass "every join() call passes a string-literal separator first; swapped arguments would"
|
||||
pass " have returned null silently and shipped an empty-of-meaning export"
|
||||
fi
|
||||
|
||||
# guard 56: no `{% set %}` tag assigns the result of a function call.
|
||||
#
|
||||
# utpl's set tag takes a plain expression and rejects a call outright:
|
||||
# `{% set x = f(); %}` is a parse error. The damage is disproportionate to
|
||||
# the typo, because a parse error kills the whole render - firewall_post.ut
|
||||
# produces a zero-byte fw4_post.nft, fw4 has nothing new to load, and the
|
||||
# health gate quietly keeps the previous ruleset. Nothing looks broken: the
|
||||
# service is up, the proxy works, and the template change is simply absent.
|
||||
# r41 shipped such a line and never rendered once; r43 fixed it.
|
||||
#
|
||||
# So the rule is mechanical and worth pinning: inside a set tag, no `(`.
|
||||
set_call="$(python3 - "$SCRIPTS" <<'PY_SET'
|
||||
import re, sys, pathlib
|
||||
bad = []
|
||||
for p in sorted(pathlib.Path(sys.argv[1]).rglob("*.ut")):
|
||||
src = p.read_text(errors="replace")
|
||||
# Comments quote the mistake on purpose; only real tags count.
|
||||
src = re.sub(r"/\*.*?\*/", " ", src, flags=re.S)
|
||||
for m in re.finditer(r"\{%-?\s*set\b([^%]*?)-?%\}", src, flags=re.S):
|
||||
rhs = m.group(1)
|
||||
# A call is any identifier immediately followed by "(".
|
||||
if re.search(r"[A-Za-z_][A-Za-z0-9_.]*\s*\(", rhs):
|
||||
bad.append("%s: {%% set%s%%}" % (p.name, rhs.strip()))
|
||||
print("\n".join(bad))
|
||||
PY_SET
|
||||
)"
|
||||
if [ -n "$set_call" ]; then
|
||||
fail "a {% set %} tag assigns a function call; utpl rejects it, and the parse error"
|
||||
fail "zeroes the whole render instead of failing visibly:"
|
||||
printf ' %s\n' "$set_call"
|
||||
else
|
||||
pass "no {% set %} tag calls a function (utpl would reject it and the whole render"
|
||||
pass " would silently collapse to an empty ruleset)"
|
||||
fi
|
||||
|
||||
echo
|
||||
printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')"
|
||||
if [ "$FAILED" != 0 ]; then
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Regression fixture for config.ruleset_safe_start = '1' (issue #7 batch 2a).
|
||||
#
|
||||
# custom.uci with the opt-in on and two REMOTE rule-sets added, because those
|
||||
# are the only ones the fallback applies to: a local rule-set already has its
|
||||
# file on disk and is never fetched during initialization.
|
||||
#
|
||||
# It is a separate file rather than three sed variations of custom.uci for two
|
||||
# reasons. The opt-in has to be in the `config homeproxy-pro 'routing'` section
|
||||
# (dm.general.ruleset_safe_start), which is a different section from anything
|
||||
# the variations were anchored on; and a multi-expression sed that both inserts
|
||||
# an option and appends a section is a construct this suite had never run -
|
||||
# the one-expression form is proven, the two-expression form is not, and a test
|
||||
# whose own tooling is untested is how a case silently stops testing anything.
|
||||
#
|
||||
# The two rule-sets cover the two shapes the fallback has to get right:
|
||||
#
|
||||
# rs_remote one tag. initial_path is a concrete path and one file is
|
||||
# written for it.
|
||||
# rs_multi two tags. sing-box substitutes {tag} and opens EVERY resulting
|
||||
# file, so initial_path must carry the placeholder and one file
|
||||
# must be written per tag - a rule-set with a literal
|
||||
# "{tag}.srs" on disk falls back to downloading and blocks
|
||||
# startup, which is the exact failure the feature removes.
|
||||
#
|
||||
# __RULESET_DIR__ is substituted by tests/ucode/test_generators.sh with the
|
||||
# per-run archive.
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '0'
|
||||
option log_level 'error'
|
||||
option ruleset_safe_start '1'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
# direct node: exercises the direct_overrides write path
|
||||
config node 'n_direct'
|
||||
option label 'direct-node'
|
||||
option type 'direct'
|
||||
option override_address '1.2.3.4'
|
||||
option override_port '8443'
|
||||
|
||||
# routing node + rule: exercises get_direct_override() reading direct_overrides
|
||||
config routing_node 'rn_direct'
|
||||
option enabled '1'
|
||||
option label 'rn-direct'
|
||||
option node 'n_direct'
|
||||
option domain_resolver 'default-dns'
|
||||
option domain_strategy 'prefer_ipv4'
|
||||
|
||||
config routing_rule 'rr_direct'
|
||||
option enabled '1'
|
||||
option label 'rr-direct'
|
||||
option outbound 'rn_direct'
|
||||
option action 'route'
|
||||
|
||||
# The two remote rule-sets the fallback applies to. Neither has an
|
||||
# initial_path of its own, which is the whole precondition: a rule-set that has
|
||||
# one is left completely alone by the fallback.
|
||||
config ruleset 'rs_remote'
|
||||
option enabled '1'
|
||||
option label 'remote-rs'
|
||||
option type 'remote'
|
||||
option format 'binary'
|
||||
option url 'https://example.invalid/remote.srs'
|
||||
|
||||
config ruleset 'rs_multi'
|
||||
option enabled '1'
|
||||
option label 'multi-rs'
|
||||
option type 'remote'
|
||||
option format 'binary'
|
||||
option url 'https://example.invalid/{tag}.srs'
|
||||
list extra_tags 'alt'
|
||||
|
||||
# A disabled remote rule-set: the fallback must not create a file for it, since
|
||||
# build_user_rulesets() skips it before it can ever reach the generated
|
||||
# configuration.
|
||||
config ruleset 'rs_off'
|
||||
option enabled '0'
|
||||
option label 'off-rs'
|
||||
option type 'remote'
|
||||
option format 'binary'
|
||||
option url 'https://example.invalid/off.srs'
|
||||
+271
-168
@@ -9,6 +9,8 @@ ip route show default
|
||||
ip -6 route show default
|
||||
ifstatus wan
|
||||
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
init.d/dnsmasq restart
|
||||
ip link show singtun0
|
||||
ip link set singtun0 up
|
||||
@@ -42,6 +44,8 @@ procd_open_instance log-cleaner
|
||||
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
|
||||
chown sing-box:sing-box etc/homeproxy-pro/cache.db
|
||||
@@ -74,6 +78,7 @@ sing-box version -n
|
||||
log sing-box 1.14.0 started.
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
ip rule del fwmark 101 table 100
|
||||
@@ -130,6 +135,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -162,6 +171,8 @@ ifstatus wan
|
||||
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
init.d/dnsmasq restart
|
||||
ip rule del fwmark 101 table 100
|
||||
ip rule add fwmark 101 table 100
|
||||
@@ -219,6 +230,8 @@ procd_set_param respawn
|
||||
procd_close_instance
|
||||
log WARNING: IPv6 support is ON but etc/homeproxy-pro/resources/china_ip6.txt has 0 usable prefixes.
|
||||
log WARNING: mainland IPv6 cannot be told apart from foreign IPv6, so IPv6 is passed through UNPROXIED (both sides of the split degrade). Run: etc/homeproxy-pro/scripts/update_resources.sh china_ip6
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/certs
|
||||
chmod 700 etc/homeproxy-pro/certs
|
||||
chown sing-box:sing-box etc/homeproxy-pro/certs/server_privatekey.pem
|
||||
@@ -269,90 +282,6 @@ sing-box version -n
|
||||
log sing-box 1.14.0 started.
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
ip rule del fwmark 101 table 100
|
||||
ip route del local 0.0.0.0/0 dev lo table 100
|
||||
ip -6 rule del fwmark 101 table 100
|
||||
ip -6 route del local ::/0 dev lo table 100
|
||||
ip route del default dev singtun0 table 100
|
||||
ip rule del fwmark 102 table 100
|
||||
ip -6 route del default dev singtun0 table 100
|
||||
ip -6 rule del fwmark 102 table 100
|
||||
nft flush chain inet fw4 homeproxy_output_redir
|
||||
nft delete chain inet fw4 homeproxy_output_redir
|
||||
nft flush chain inet fw4 homeproxy_redirect
|
||||
nft delete chain inet fw4 homeproxy_redirect
|
||||
nft flush chain inet fw4 homeproxy_redirect_proxy
|
||||
nft delete chain inet fw4 homeproxy_redirect_proxy
|
||||
nft flush chain inet fw4 homeproxy_redirect_proxy_port
|
||||
nft delete chain inet fw4 homeproxy_redirect_proxy_port
|
||||
nft flush chain inet fw4 homeproxy_redirect_lanac
|
||||
nft delete chain inet fw4 homeproxy_redirect_lanac
|
||||
nft flush chain inet fw4 homeproxy_mangle_prerouting
|
||||
nft delete chain inet fw4 homeproxy_mangle_prerouting
|
||||
nft flush chain inet fw4 homeproxy_mangle_output
|
||||
nft delete chain inet fw4 homeproxy_mangle_output
|
||||
nft flush chain inet fw4 homeproxy_mangle_tproxy
|
||||
nft delete chain inet fw4 homeproxy_mangle_tproxy
|
||||
nft flush chain inet fw4 homeproxy_mangle_tproxy_port
|
||||
nft delete chain inet fw4 homeproxy_mangle_tproxy_port
|
||||
nft flush chain inet fw4 homeproxy_mangle_lanac
|
||||
nft delete chain inet fw4 homeproxy_mangle_lanac
|
||||
nft flush chain inet fw4 homeproxy_mangle_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_mark
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
nft delete set inet fw4 homeproxy_local_addr_v6
|
||||
nft flush set inet fw4 homeproxy_gfw_list_v4
|
||||
nft delete set inet fw4 homeproxy_gfw_list_v4
|
||||
nft flush set inet fw4 homeproxy_gfw_list_v6
|
||||
nft delete set inet fw4 homeproxy_gfw_list_v6
|
||||
nft flush set inet fw4 homeproxy_mainland_addr_v4
|
||||
nft delete set inet fw4 homeproxy_mainland_addr_v4
|
||||
nft flush set inet fw4 homeproxy_mainland_addr_v6
|
||||
nft delete set inet fw4 homeproxy_mainland_addr_v6
|
||||
nft flush set inet fw4 homeproxy_wan_proxy_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_proxy_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_proxy_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_proxy_addr_v6
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
init.d/miniupnpd enabled
|
||||
init.d/miniupnpd running
|
||||
init.d/miniupnpd restart
|
||||
init.d/dnsmasq restart
|
||||
log Service stopped.
|
||||
config_load homeproxy-pro
|
||||
ip link set singtun0 down
|
||||
ip tuntap del mode tun name singtun0
|
||||
stop rc=0
|
||||
file var/run/homeproxy-pro/fw4_forward.nft:
|
||||
file var/run/homeproxy-pro/fw4_input.nft:
|
||||
file var/run/homeproxy-pro/fw4_post.nft:
|
||||
file var/run/homeproxy-pro/homeproxy-pro.log:
|
||||
file var/run/homeproxy-pro/known-good/sing-box-c.json: {| "log": {},| "inbounds": [| {| "tag": "dns-in",| "listen_port": 5333| },| {| "tag": "mixed-in",| "listen_port": 5330| }| ],| "outbounds": []|}|
|
||||
file var/run/homeproxy-pro/known-good/sing-box-s.json: {| "log": {},| "inbounds": [],| "outbounds": []|}|
|
||||
file var/run/homeproxy-pro/known-good/uci-snapshot.txt: homeproxy-pro.config.main_node=n1|homeproxy-pro.config.main_udp_node=u1|homeproxy-pro.config.proxy_mode=redirect_tproxy|homeproxy-pro.config.routing_mode=gfwlist|homeproxy-pro.config.routing_port=|homeproxy-pro.config.ipv6_support=1|homeproxy-pro.config.china_dns_server=|homeproxy-pro.infra.self_mark=|homeproxy-pro.infra.redirect_port=|homeproxy-pro.infra.tproxy_port=|homeproxy-pro.infra.tproxy_mark=101|homeproxy-pro.infra.tun_mark=102|homeproxy-pro.infra.tun_name=singtun0|homeproxy-pro.infra.tun_addr4=|homeproxy-pro.infra.tun_addr6=|homeproxy-pro.infra.dns_redirect=|homeproxy-pro.infra.dns_port=5333|homeproxy-pro.infra.common_port=|homeproxy-pro.infra.table_mark=100|homeproxy-pro.routing.bypass_cn_traffic=|homeproxy-pro.routing.default_outbound=direct-out|homeproxy-pro.server.enabled=1|homeproxy-pro.control.lan_proxy_mode=|list homeproxy-pro.control.listen_interfaces=|list homeproxy-pro.control.lan_direct_ipv4_ips=|list homeproxy-pro.control.lan_direct_ipv6_ips=|list homeproxy-pro.control.lan_direct_mac_addrs=|list homeproxy-pro.control.lan_proxy_ipv4_ips=|list homeproxy-pro.control.lan_proxy_ipv6_ips=|list homeproxy-pro.control.lan_proxy_mac_addrs=|list homeproxy-pro.control.lan_gaming_mode_ipv4_ips=|list homeproxy-pro.control.lan_gaming_mode_ipv6_ips=|list homeproxy-pro.control.lan_gaming_mode_mac_addrs=|list homeproxy-pro.control.lan_global_proxy_ipv4_ips=|list homeproxy-pro.control.lan_global_proxy_ipv6_ips=|list homeproxy-pro.control.lan_global_proxy_mac_addrs=|list homeproxy-pro.control.wan_proxy_ipv4_ips=|list homeproxy-pro.control.wan_proxy_ipv6_ips=|list homeproxy-pro.control.wan_direct_ipv4_ips=|list homeproxy-pro.control.wan_direct_ipv6_ips=|
|
||||
|
||||
===== scenario: C-nothing-configured (start) =====
|
||||
uci -q show dhcp.@dnsmasq[0]
|
||||
config_load homeproxy-pro
|
||||
ucode -e import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)
|
||||
sing-box version -n
|
||||
log Neither client nor server is configured. Service not started.
|
||||
config_load homeproxy-pro
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
@@ -410,6 +339,100 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
init.d/miniupnpd enabled
|
||||
init.d/miniupnpd running
|
||||
init.d/miniupnpd restart
|
||||
init.d/dnsmasq restart
|
||||
log Service stopped.
|
||||
config_load homeproxy-pro
|
||||
ip link set singtun0 down
|
||||
ip tuntap del mode tun name singtun0
|
||||
stop rc=0
|
||||
file var/run/homeproxy-pro/fw4_forward.nft:
|
||||
file var/run/homeproxy-pro/fw4_input.nft:
|
||||
file var/run/homeproxy-pro/fw4_post.nft:
|
||||
file var/run/homeproxy-pro/homeproxy-pro.log:
|
||||
file var/run/homeproxy-pro/known-good/sing-box-c.json: {| "log": {},| "inbounds": [| {| "tag": "dns-in",| "listen_port": 5333| },| {| "tag": "mixed-in",| "listen_port": 5330| }| ],| "outbounds": []|}|
|
||||
file var/run/homeproxy-pro/known-good/sing-box-s.json: {| "log": {},| "inbounds": [],| "outbounds": []|}|
|
||||
file var/run/homeproxy-pro/known-good/uci-snapshot.txt: homeproxy-pro.config.main_node=n1|homeproxy-pro.config.main_udp_node=u1|homeproxy-pro.config.proxy_mode=redirect_tproxy|homeproxy-pro.config.routing_mode=gfwlist|homeproxy-pro.config.routing_port=|homeproxy-pro.config.ipv6_support=1|homeproxy-pro.config.china_dns_server=|homeproxy-pro.infra.self_mark=|homeproxy-pro.infra.redirect_port=|homeproxy-pro.infra.tproxy_port=|homeproxy-pro.infra.tproxy_mark=101|homeproxy-pro.infra.tun_mark=102|homeproxy-pro.infra.tun_name=singtun0|homeproxy-pro.infra.tun_addr4=|homeproxy-pro.infra.tun_addr6=|homeproxy-pro.infra.dns_redirect=|homeproxy-pro.infra.dns_port=5333|homeproxy-pro.infra.common_port=|homeproxy-pro.infra.table_mark=100|homeproxy-pro.routing.bypass_cn_traffic=|homeproxy-pro.routing.default_outbound=direct-out|homeproxy-pro.server.enabled=1|homeproxy-pro.control.lan_proxy_mode=|list homeproxy-pro.control.listen_interfaces=|list homeproxy-pro.control.lan_direct_ipv4_ips=|list homeproxy-pro.control.lan_direct_ipv6_ips=|list homeproxy-pro.control.lan_direct_mac_addrs=|list homeproxy-pro.control.lan_proxy_ipv4_ips=|list homeproxy-pro.control.lan_proxy_ipv6_ips=|list homeproxy-pro.control.lan_proxy_mac_addrs=|list homeproxy-pro.control.lan_gaming_mode_ipv4_ips=|list homeproxy-pro.control.lan_gaming_mode_ipv6_ips=|list homeproxy-pro.control.lan_gaming_mode_mac_addrs=|list homeproxy-pro.control.lan_global_proxy_ipv4_ips=|list homeproxy-pro.control.lan_global_proxy_ipv6_ips=|list homeproxy-pro.control.lan_global_proxy_mac_addrs=|list homeproxy-pro.control.wan_proxy_ipv4_ips=|list homeproxy-pro.control.wan_proxy_ipv6_ips=|list homeproxy-pro.control.wan_direct_ipv4_ips=|list homeproxy-pro.control.wan_direct_ipv6_ips=|
|
||||
|
||||
===== scenario: C-nothing-configured (start) =====
|
||||
uci -q show dhcp.@dnsmasq[0]
|
||||
config_load homeproxy-pro
|
||||
ucode -e import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)
|
||||
sing-box version -n
|
||||
log Neither client nor server is configured. Service not started.
|
||||
config_load homeproxy-pro
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
ip rule del fwmark 101 table 100
|
||||
ip route del local 0.0.0.0/0 dev lo table 100
|
||||
ip -6 rule del fwmark 101 table 100
|
||||
ip -6 route del local ::/0 dev lo table 100
|
||||
ip route del default dev singtun0 table 100
|
||||
ip rule del fwmark 102 table 100
|
||||
ip -6 route del default dev singtun0 table 100
|
||||
ip -6 rule del fwmark 102 table 100
|
||||
nft flush chain inet fw4 homeproxy_output_redir
|
||||
nft delete chain inet fw4 homeproxy_output_redir
|
||||
nft flush chain inet fw4 homeproxy_redirect
|
||||
nft delete chain inet fw4 homeproxy_redirect
|
||||
nft flush chain inet fw4 homeproxy_redirect_proxy
|
||||
nft delete chain inet fw4 homeproxy_redirect_proxy
|
||||
nft flush chain inet fw4 homeproxy_redirect_proxy_port
|
||||
nft delete chain inet fw4 homeproxy_redirect_proxy_port
|
||||
nft flush chain inet fw4 homeproxy_redirect_lanac
|
||||
nft delete chain inet fw4 homeproxy_redirect_lanac
|
||||
nft flush chain inet fw4 homeproxy_mangle_prerouting
|
||||
nft delete chain inet fw4 homeproxy_mangle_prerouting
|
||||
nft flush chain inet fw4 homeproxy_mangle_output
|
||||
nft delete chain inet fw4 homeproxy_mangle_output
|
||||
nft flush chain inet fw4 homeproxy_mangle_tproxy
|
||||
nft delete chain inet fw4 homeproxy_mangle_tproxy
|
||||
nft flush chain inet fw4 homeproxy_mangle_tproxy_port
|
||||
nft delete chain inet fw4 homeproxy_mangle_tproxy_port
|
||||
nft flush chain inet fw4 homeproxy_mangle_lanac
|
||||
nft delete chain inet fw4 homeproxy_mangle_lanac
|
||||
nft flush chain inet fw4 homeproxy_mangle_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_mark
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
nft delete set inet fw4 homeproxy_local_addr_v6
|
||||
nft flush set inet fw4 homeproxy_gfw_list_v4
|
||||
nft delete set inet fw4 homeproxy_gfw_list_v4
|
||||
nft flush set inet fw4 homeproxy_gfw_list_v6
|
||||
nft delete set inet fw4 homeproxy_gfw_list_v6
|
||||
nft flush set inet fw4 homeproxy_mainland_addr_v4
|
||||
nft delete set inet fw4 homeproxy_mainland_addr_v4
|
||||
nft flush set inet fw4 homeproxy_mainland_addr_v6
|
||||
nft delete set inet fw4 homeproxy_mainland_addr_v6
|
||||
nft flush set inet fw4 homeproxy_wan_proxy_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_proxy_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_proxy_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_proxy_addr_v6
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -435,6 +458,8 @@ ip route show default
|
||||
ip -6 route show default
|
||||
ifstatus wan
|
||||
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
init.d/dnsmasq restart
|
||||
ip link show singtun0
|
||||
ip link set singtun0 up
|
||||
@@ -468,6 +493,8 @@ procd_open_instance log-cleaner
|
||||
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
|
||||
chown sing-box:sing-box etc/homeproxy-pro/cache.db
|
||||
@@ -504,6 +531,7 @@ config_load homeproxy-pro
|
||||
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
|
||||
chmod 600 var/run/homeproxy-pro/candidate/sing-box-c.json
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
nft flush chain inet fw4 homeproxy_output_redir
|
||||
@@ -552,6 +580,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -568,6 +600,8 @@ ip route show default
|
||||
ip -6 route show default
|
||||
ifstatus wan
|
||||
log Activating the client configuration validated by the reload preflight.
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
log dnsmasq snippets unchanged, skipping the restart.
|
||||
ip link show singtun0
|
||||
ip link set singtun0 up
|
||||
@@ -601,6 +635,8 @@ procd_open_instance log-cleaner
|
||||
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
|
||||
chown sing-box:sing-box etc/homeproxy-pro/cache.db
|
||||
@@ -756,6 +792,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -806,6 +846,7 @@ uci delete homeproxy-pro.control.wan_direct_ipv4_ips
|
||||
uci delete homeproxy-pro.control.wan_direct_ipv6_ips
|
||||
uci commit homeproxy-pro
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
nft flush chain inet fw4 homeproxy_output_redir
|
||||
@@ -854,6 +895,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -870,6 +915,8 @@ ip route show default
|
||||
ip -6 route show default
|
||||
ifstatus wan
|
||||
log Starting with the last known-good client configuration.
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
init.d/dnsmasq restart
|
||||
ip link show singtun0
|
||||
ip link set singtun0 up
|
||||
@@ -903,6 +950,8 @@ procd_open_instance log-cleaner
|
||||
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
|
||||
chown sing-box:sing-box etc/homeproxy-pro/cache.db
|
||||
@@ -949,6 +998,7 @@ log sing-box 1.14.0 started.
|
||||
log Rollback completed; the previous configuration is running.
|
||||
reload_service rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
nft flush chain inet fw4 homeproxy_output_redir
|
||||
@@ -997,6 +1047,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -1027,6 +1081,8 @@ ip route show default
|
||||
ip -6 route show default
|
||||
ifstatus wan
|
||||
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
init.d/dnsmasq restart
|
||||
ip link show singtun0
|
||||
ip link set singtun0 up
|
||||
@@ -1060,6 +1116,8 @@ procd_open_instance log-cleaner
|
||||
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown -R sing-box:sing-box etc/homeproxy-pro/ruleset
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
|
||||
@@ -1093,90 +1151,6 @@ sing-box version -n
|
||||
log sing-box 1.14.0 started.
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
ip rule del fwmark 101 table 100
|
||||
ip route del local 0.0.0.0/0 dev lo table 100
|
||||
ip -6 rule del fwmark 101 table 100
|
||||
ip -6 route del local ::/0 dev lo table 100
|
||||
ip route del default dev singtun0 table 100
|
||||
ip rule del fwmark 102 table 100
|
||||
ip -6 route del default dev singtun0 table 100
|
||||
ip -6 rule del fwmark 102 table 100
|
||||
nft flush chain inet fw4 homeproxy_output_redir
|
||||
nft delete chain inet fw4 homeproxy_output_redir
|
||||
nft flush chain inet fw4 homeproxy_redirect
|
||||
nft delete chain inet fw4 homeproxy_redirect
|
||||
nft flush chain inet fw4 homeproxy_redirect_proxy
|
||||
nft delete chain inet fw4 homeproxy_redirect_proxy
|
||||
nft flush chain inet fw4 homeproxy_redirect_proxy_port
|
||||
nft delete chain inet fw4 homeproxy_redirect_proxy_port
|
||||
nft flush chain inet fw4 homeproxy_redirect_lanac
|
||||
nft delete chain inet fw4 homeproxy_redirect_lanac
|
||||
nft flush chain inet fw4 homeproxy_mangle_prerouting
|
||||
nft delete chain inet fw4 homeproxy_mangle_prerouting
|
||||
nft flush chain inet fw4 homeproxy_mangle_output
|
||||
nft delete chain inet fw4 homeproxy_mangle_output
|
||||
nft flush chain inet fw4 homeproxy_mangle_tproxy
|
||||
nft delete chain inet fw4 homeproxy_mangle_tproxy
|
||||
nft flush chain inet fw4 homeproxy_mangle_tproxy_port
|
||||
nft delete chain inet fw4 homeproxy_mangle_tproxy_port
|
||||
nft flush chain inet fw4 homeproxy_mangle_lanac
|
||||
nft delete chain inet fw4 homeproxy_mangle_lanac
|
||||
nft flush chain inet fw4 homeproxy_mangle_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_mark
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
nft delete set inet fw4 homeproxy_local_addr_v6
|
||||
nft flush set inet fw4 homeproxy_gfw_list_v4
|
||||
nft delete set inet fw4 homeproxy_gfw_list_v4
|
||||
nft flush set inet fw4 homeproxy_gfw_list_v6
|
||||
nft delete set inet fw4 homeproxy_gfw_list_v6
|
||||
nft flush set inet fw4 homeproxy_mainland_addr_v4
|
||||
nft delete set inet fw4 homeproxy_mainland_addr_v4
|
||||
nft flush set inet fw4 homeproxy_mainland_addr_v6
|
||||
nft delete set inet fw4 homeproxy_mainland_addr_v6
|
||||
nft flush set inet fw4 homeproxy_wan_proxy_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_proxy_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_proxy_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_proxy_addr_v6
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
init.d/miniupnpd enabled
|
||||
init.d/miniupnpd running
|
||||
init.d/miniupnpd restart
|
||||
init.d/dnsmasq restart
|
||||
log Service stopped.
|
||||
config_load homeproxy-pro
|
||||
ip link set singtun0 down
|
||||
ip tuntap del mode tun name singtun0
|
||||
stop rc=0
|
||||
file var/run/homeproxy-pro/fw4_forward.nft:
|
||||
file var/run/homeproxy-pro/fw4_input.nft:
|
||||
file var/run/homeproxy-pro/fw4_post.nft:
|
||||
file var/run/homeproxy-pro/homeproxy-pro.log:
|
||||
file var/run/homeproxy-pro/known-good/sing-box-c.json: {| "log": {},| "inbounds": [| {| "tag": "dns-in",| "listen_port": 5333| },| {| "tag": "mixed-in",| "listen_port": 5330| }| ],| "outbounds": []|}|
|
||||
file var/run/homeproxy-pro/known-good/uci-snapshot.txt: homeproxy-pro.config.main_node=n1|homeproxy-pro.config.main_udp_node=nil|homeproxy-pro.config.proxy_mode=tun|homeproxy-pro.config.routing_mode=custom|homeproxy-pro.config.routing_port=|homeproxy-pro.config.ipv6_support=0|homeproxy-pro.config.china_dns_server=|homeproxy-pro.infra.self_mark=|homeproxy-pro.infra.redirect_port=|homeproxy-pro.infra.tproxy_port=|homeproxy-pro.infra.tproxy_mark=101|homeproxy-pro.infra.tun_mark=102|homeproxy-pro.infra.tun_name=singtun0|homeproxy-pro.infra.tun_addr4=|homeproxy-pro.infra.tun_addr6=|homeproxy-pro.infra.dns_redirect=|homeproxy-pro.infra.dns_port=5333|homeproxy-pro.infra.common_port=|homeproxy-pro.infra.table_mark=100|homeproxy-pro.routing.bypass_cn_traffic=|homeproxy-pro.routing.default_outbound=direct-out|homeproxy-pro.server.enabled=0|homeproxy-pro.control.lan_proxy_mode=|list homeproxy-pro.control.listen_interfaces=|list homeproxy-pro.control.lan_direct_ipv4_ips=|list homeproxy-pro.control.lan_direct_ipv6_ips=|list homeproxy-pro.control.lan_direct_mac_addrs=|list homeproxy-pro.control.lan_proxy_ipv4_ips=|list homeproxy-pro.control.lan_proxy_ipv6_ips=|list homeproxy-pro.control.lan_proxy_mac_addrs=|list homeproxy-pro.control.lan_gaming_mode_ipv4_ips=|list homeproxy-pro.control.lan_gaming_mode_ipv6_ips=|list homeproxy-pro.control.lan_gaming_mode_mac_addrs=|list homeproxy-pro.control.lan_global_proxy_ipv4_ips=|list homeproxy-pro.control.lan_global_proxy_ipv6_ips=|list homeproxy-pro.control.lan_global_proxy_mac_addrs=|list homeproxy-pro.control.wan_proxy_ipv4_ips=|list homeproxy-pro.control.wan_proxy_ipv6_ips=|list homeproxy-pro.control.wan_direct_ipv4_ips=|list homeproxy-pro.control.wan_direct_ipv6_ips=|
|
||||
file etc/homeproxy-pro/cache.db:
|
||||
|
||||
===== scenario: F-custom-routing-nothing-configured (start) =====
|
||||
uci -q show dhcp.@dnsmasq[0]
|
||||
config_load homeproxy-pro
|
||||
ucode -e import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)
|
||||
sing-box version -n
|
||||
log Neither client nor server is configured. Service not started.
|
||||
config_load homeproxy-pro
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
@@ -1234,6 +1208,100 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
init.d/miniupnpd enabled
|
||||
init.d/miniupnpd running
|
||||
init.d/miniupnpd restart
|
||||
init.d/dnsmasq restart
|
||||
log Service stopped.
|
||||
config_load homeproxy-pro
|
||||
ip link set singtun0 down
|
||||
ip tuntap del mode tun name singtun0
|
||||
stop rc=0
|
||||
file var/run/homeproxy-pro/fw4_forward.nft:
|
||||
file var/run/homeproxy-pro/fw4_input.nft:
|
||||
file var/run/homeproxy-pro/fw4_post.nft:
|
||||
file var/run/homeproxy-pro/homeproxy-pro.log:
|
||||
file var/run/homeproxy-pro/known-good/sing-box-c.json: {| "log": {},| "inbounds": [| {| "tag": "dns-in",| "listen_port": 5333| },| {| "tag": "mixed-in",| "listen_port": 5330| }| ],| "outbounds": []|}|
|
||||
file var/run/homeproxy-pro/known-good/uci-snapshot.txt: homeproxy-pro.config.main_node=n1|homeproxy-pro.config.main_udp_node=nil|homeproxy-pro.config.proxy_mode=tun|homeproxy-pro.config.routing_mode=custom|homeproxy-pro.config.routing_port=|homeproxy-pro.config.ipv6_support=0|homeproxy-pro.config.china_dns_server=|homeproxy-pro.infra.self_mark=|homeproxy-pro.infra.redirect_port=|homeproxy-pro.infra.tproxy_port=|homeproxy-pro.infra.tproxy_mark=101|homeproxy-pro.infra.tun_mark=102|homeproxy-pro.infra.tun_name=singtun0|homeproxy-pro.infra.tun_addr4=|homeproxy-pro.infra.tun_addr6=|homeproxy-pro.infra.dns_redirect=|homeproxy-pro.infra.dns_port=5333|homeproxy-pro.infra.common_port=|homeproxy-pro.infra.table_mark=100|homeproxy-pro.routing.bypass_cn_traffic=|homeproxy-pro.routing.default_outbound=direct-out|homeproxy-pro.server.enabled=0|homeproxy-pro.control.lan_proxy_mode=|list homeproxy-pro.control.listen_interfaces=|list homeproxy-pro.control.lan_direct_ipv4_ips=|list homeproxy-pro.control.lan_direct_ipv6_ips=|list homeproxy-pro.control.lan_direct_mac_addrs=|list homeproxy-pro.control.lan_proxy_ipv4_ips=|list homeproxy-pro.control.lan_proxy_ipv6_ips=|list homeproxy-pro.control.lan_proxy_mac_addrs=|list homeproxy-pro.control.lan_gaming_mode_ipv4_ips=|list homeproxy-pro.control.lan_gaming_mode_ipv6_ips=|list homeproxy-pro.control.lan_gaming_mode_mac_addrs=|list homeproxy-pro.control.lan_global_proxy_ipv4_ips=|list homeproxy-pro.control.lan_global_proxy_ipv6_ips=|list homeproxy-pro.control.lan_global_proxy_mac_addrs=|list homeproxy-pro.control.wan_proxy_ipv4_ips=|list homeproxy-pro.control.wan_proxy_ipv6_ips=|list homeproxy-pro.control.wan_direct_ipv4_ips=|list homeproxy-pro.control.wan_direct_ipv6_ips=|
|
||||
file etc/homeproxy-pro/cache.db:
|
||||
|
||||
===== scenario: F-custom-routing-nothing-configured (start) =====
|
||||
uci -q show dhcp.@dnsmasq[0]
|
||||
config_load homeproxy-pro
|
||||
ucode -e import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)
|
||||
sing-box version -n
|
||||
log Neither client nor server is configured. Service not started.
|
||||
config_load homeproxy-pro
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
ip rule del fwmark 101 table 100
|
||||
ip route del local 0.0.0.0/0 dev lo table 100
|
||||
ip -6 rule del fwmark 101 table 100
|
||||
ip -6 route del local ::/0 dev lo table 100
|
||||
ip route del default dev singtun0 table 100
|
||||
ip rule del fwmark 102 table 100
|
||||
ip -6 route del default dev singtun0 table 100
|
||||
ip -6 rule del fwmark 102 table 100
|
||||
nft flush chain inet fw4 homeproxy_output_redir
|
||||
nft delete chain inet fw4 homeproxy_output_redir
|
||||
nft flush chain inet fw4 homeproxy_redirect
|
||||
nft delete chain inet fw4 homeproxy_redirect
|
||||
nft flush chain inet fw4 homeproxy_redirect_proxy
|
||||
nft delete chain inet fw4 homeproxy_redirect_proxy
|
||||
nft flush chain inet fw4 homeproxy_redirect_proxy_port
|
||||
nft delete chain inet fw4 homeproxy_redirect_proxy_port
|
||||
nft flush chain inet fw4 homeproxy_redirect_lanac
|
||||
nft delete chain inet fw4 homeproxy_redirect_lanac
|
||||
nft flush chain inet fw4 homeproxy_mangle_prerouting
|
||||
nft delete chain inet fw4 homeproxy_mangle_prerouting
|
||||
nft flush chain inet fw4 homeproxy_mangle_output
|
||||
nft delete chain inet fw4 homeproxy_mangle_output
|
||||
nft flush chain inet fw4 homeproxy_mangle_tproxy
|
||||
nft delete chain inet fw4 homeproxy_mangle_tproxy
|
||||
nft flush chain inet fw4 homeproxy_mangle_tproxy_port
|
||||
nft delete chain inet fw4 homeproxy_mangle_tproxy_port
|
||||
nft flush chain inet fw4 homeproxy_mangle_lanac
|
||||
nft delete chain inet fw4 homeproxy_mangle_lanac
|
||||
nft flush chain inet fw4 homeproxy_mangle_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_mark
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
nft delete set inet fw4 homeproxy_local_addr_v6
|
||||
nft flush set inet fw4 homeproxy_gfw_list_v4
|
||||
nft delete set inet fw4 homeproxy_gfw_list_v4
|
||||
nft flush set inet fw4 homeproxy_gfw_list_v6
|
||||
nft delete set inet fw4 homeproxy_gfw_list_v6
|
||||
nft flush set inet fw4 homeproxy_mainland_addr_v4
|
||||
nft delete set inet fw4 homeproxy_mainland_addr_v4
|
||||
nft flush set inet fw4 homeproxy_mainland_addr_v6
|
||||
nft delete set inet fw4 homeproxy_mainland_addr_v6
|
||||
nft flush set inet fw4 homeproxy_wan_proxy_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_proxy_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_proxy_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_proxy_addr_v6
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -1259,6 +1327,8 @@ ip route show default
|
||||
ip -6 route show default
|
||||
ifstatus wan
|
||||
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
init.d/dnsmasq restart
|
||||
procd_open_instance sing-box-c
|
||||
procd_set_param command /usr/bin/sing-box
|
||||
@@ -1287,6 +1357,8 @@ procd_open_instance log-cleaner
|
||||
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
|
||||
chown sing-box:sing-box etc/homeproxy-pro/cache.db
|
||||
@@ -1442,6 +1514,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -1451,6 +1527,7 @@ init.d/miniupnpd restart
|
||||
init.d/dnsmasq restart
|
||||
start rc=1
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
ip rule del fwmark 101 table 100
|
||||
@@ -1507,6 +1584,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -1534,6 +1615,8 @@ ip route show default
|
||||
ip -6 route show default
|
||||
ifstatus wan
|
||||
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
log Warning: infra.dns_port=70000 is out of range; skipping the dnsmasq snippets.
|
||||
log Error: the dnsmasq snippets were not installed; LAN DNS keeps using the previous configuration.
|
||||
ip link show singtun0
|
||||
@@ -1568,6 +1651,8 @@ procd_open_instance log-cleaner
|
||||
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.json
|
||||
chown sing-box:sing-box var/run/homeproxy-pro/sing-box-c.log
|
||||
chown sing-box:sing-box etc/homeproxy-pro/cache.db
|
||||
@@ -1599,6 +1684,7 @@ sing-box version -n
|
||||
log sing-box 1.14.0 started.
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
ip rule del fwmark 101 table 100
|
||||
@@ -1655,6 +1741,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -1682,6 +1772,8 @@ ip route show default
|
||||
ip -6 route show default
|
||||
ifstatus wan
|
||||
ucode -S etc/homeproxy-pro/scripts/generate_client.uc
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
init.d/dnsmasq restart
|
||||
procd_open_instance sing-box-c
|
||||
procd_set_param command /usr/bin/sing-box
|
||||
@@ -1731,6 +1823,8 @@ procd_open_instance log-cleaner
|
||||
procd_set_param command etc/homeproxy-pro/scripts/clean_log.sh
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
ucode -S etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc etc/homeproxy-pro/resources/china_list.txt etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/resources/china-domain.json
|
||||
chown sing-box:sing-box etc/homeproxy-pro/certs
|
||||
chmod 700 etc/homeproxy-pro/certs
|
||||
chown sing-box:sing-box etc/homeproxy-pro/certs/server_privatekey.pem
|
||||
@@ -1839,6 +1933,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
@@ -1904,6 +2002,7 @@ sing-box version -n
|
||||
log sing-box 1.14.0 started.
|
||||
start rc=0
|
||||
chmod 600 etc/crontabs/root
|
||||
chmod 600 etc/crontabs/root
|
||||
init.d/cron restart
|
||||
config_load homeproxy-pro
|
||||
ip rule del fwmark 101 table 100
|
||||
@@ -1960,6 +2059,10 @@ nft flush set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v4
|
||||
nft flush set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft delete set inet fw4 homeproxy_wan_direct_addr_v6
|
||||
nft flush set inet fw4 homeproxy_node_addr_v4
|
||||
nft delete set inet fw4 homeproxy_node_addr_v4
|
||||
nft flush set inet fw4 homeproxy_node_addr_v6
|
||||
nft delete set inet fw4 homeproxy_node_addr_v6
|
||||
nft flush set inet fw4 homeproxy_routing_port
|
||||
nft delete set inet fw4 homeproxy_routing_port
|
||||
fw4 reload
|
||||
|
||||
@@ -178,10 +178,10 @@ function makePoll() {
|
||||
|
||||
/* --- the rule-set degradation report ------------------------------------
|
||||
*
|
||||
* parseRuleSetFetchFailures() is the only thing that tells a user a rule-set
|
||||
* is currently EMPTY, because with ruleset_safe_start on there is no other
|
||||
* symptom: the instance is healthy, the health gate passes (nothing in
|
||||
* health.sh looks at rule-sets), and the LAN is proxied.
|
||||
* A remote rule-set is fetched before the service starts, so a failure there
|
||||
* is a failed start and the running instance has nothing to show for it. The
|
||||
* reason is only ever written to a log, and a start that failed has no status
|
||||
* page - so this parse is the only place it becomes legible without ssh.
|
||||
*
|
||||
* It parses a file sing-box is concurrently writing and that clean_log.sh
|
||||
* truncates at 50 KB, so the cases below are about tolerance and about not
|
||||
|
||||
@@ -362,6 +362,12 @@ echo "== china ip rule-set generator =="
|
||||
# validation and its atomic write are both load-bearing.
|
||||
sh "$ROOT/tests/ucode/test_china_ip_ruleset.sh" "$ROOT" "$WORK/china_ip_ruleset" || FAILED=1
|
||||
|
||||
# The DNS half of the same split. Same failure mode and the same consequence
|
||||
# class - one bad entry in a list that is replaced unattended makes sing-box
|
||||
# reject the whole rule-set - plus one that is its own: the entry has to be a
|
||||
# suffix, or the split quietly stops matching anything under the listed names.
|
||||
sh "$ROOT/tests/ucode/test_domain_ruleset.sh" "$ROOT" "$WORK/domain_ruleset" || FAILED=1
|
||||
|
||||
echo "== test doubles still match production =="
|
||||
# The mocks copy isEmpty/decodeBase64Str/parseURL/redactUrl from production and
|
||||
# their headers say to keep them in sync; nothing enforced it, and the fetcher's
|
||||
|
||||
@@ -83,6 +83,17 @@ stage_case() {
|
||||
printf '{"version":3,"rules":[{"ip_cidr":["192.0.2.0/24"]}]}\n' > "$dir/resources/china_ip4.json"
|
||||
fi
|
||||
|
||||
# The DNS half of the China split. generate_client.uc reports it as
|
||||
# ctx.china_domain_ready, an lstat on this file; absent, the domestic
|
||||
# lookup rule is not emitted and the ordering this file asserts cannot be
|
||||
# observed at all. On a router hp_prepare_runtime_files generates it from
|
||||
# china_list.txt by the same route as china_ip4.json above.
|
||||
if ! ucode -S "$ROOT/root/etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc" \
|
||||
"$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" "$dir/resources/china-domain.json" \
|
||||
>>"$dir/resources/china-domain.log" 2>&1; then
|
||||
printf '{"version":3,"rules":[{"domain_suffix":["example.invalid"]}]}\n' > "$dir/resources/china-domain.json"
|
||||
fi
|
||||
|
||||
VALIDATE_DATA="${HP_VALIDATE_DATA:-/sbin/validate_data}"
|
||||
sed -e "s#^export const HP_DIR = '/etc/homeproxy-pro';#export const HP_DIR = '$dir';#" \
|
||||
-e "s#^export const RUN_DIR = '/var/run/homeproxy-pro';#export const RUN_DIR = '$dir/run';#" \
|
||||
@@ -173,15 +184,15 @@ check_fallback_pair() {
|
||||
|
||||
echo "PASS: $label: route.final=$have_route and dns.final=$have_dns agree"
|
||||
|
||||
# Both mainland modes match destinations against geoip-cn, so both have to
|
||||
# declare it. proxy_mainland_china reached this point without the
|
||||
# declaration once and sing-box refused the whole config with
|
||||
# "initialize rule[3]: rule-set not found: geoip-cn"; the health gate then
|
||||
# Both mainland modes match destinations against the local china-ip
|
||||
# rule-set, so both have to declare it. proxy_mainland_china reached this
|
||||
# point without the declaration once and sing-box refused the whole config
|
||||
# with "initialize rule[3]: rule-set not found"; the health gate then
|
||||
# rolled the intercept layer back and the network went unproxied.
|
||||
case "$label" in
|
||||
bypass_mainland_china|proxy_mainland_china)
|
||||
if ! grep -qF '"rule_set": "geoip-cn"' "$json" || ! grep -qF '"tag": "geoip-cn"' "$json"; then
|
||||
echo "FAIL: $label: the route rule matches geoip-cn but the rule-set is not declared"
|
||||
if ! grep -qF '"rule_set": "china-ip"' "$json" || ! grep -qF '"tag": "china-ip"' "$json"; then
|
||||
echo "FAIL: $label: the route rule matches china-ip but the rule-set is not declared"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
@@ -232,9 +243,9 @@ check_proxy_list() {
|
||||
# In the one mode that has both, the domestic lookup still comes last.
|
||||
case "$label" in
|
||||
bypass_mainland_china*)
|
||||
geosite_rule="$(grep -nF '"rule_set": "geosite-cn"' "$json" | head -1 | cut -d: -f1)"
|
||||
if [ -z "$geosite_rule" ] || [ "$geosite_rule" -lt "$dns_rule" ]; then
|
||||
echo "FAIL: $label: geosite-cn must come after proxy-domain (proxy=$dns_rule geosite=${geosite_rule:-none})"
|
||||
china_domain_rule="$(grep -nF '"rule_set": "china-domain"' "$json" | head -1 | cut -d: -f1)"
|
||||
if [ -z "$china_domain_rule" ] || [ "$china_domain_rule" -lt "$dns_rule" ]; then
|
||||
echo "FAIL: $label: china-domain must come after proxy-domain (proxy=$dns_rule china-domain=${china_domain_rule:-none})"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# runtime/domain_ruleset.uc: the text -> sing-box local rule-set generator for
|
||||
# the DNS half of the China split.
|
||||
#
|
||||
# Why this has its own test: it is the only thing standing between a downloaded
|
||||
# host-name list and a DNS split. sing-box rejects a rule-set containing an
|
||||
# entry it cannot parse, and it rejects the WHOLE file when it does - so one bad
|
||||
# line in a list that is replaced unattended, on a router nobody is watching,
|
||||
# takes the resolver's half of the split down with it. The cases below are the
|
||||
# ones that decide whether that can happen.
|
||||
#
|
||||
# Three properties matter beyond "it produces a file":
|
||||
#
|
||||
# - the entry has to be a SUFFIX. `domain` would match only the name itself
|
||||
# and quietly stop matching www.<domain> for the whole list, which is a
|
||||
# split that looks configured and behaves as if the list were empty.
|
||||
# - the host-name check has to be in-process. validation() in homeproxy-pro.uc
|
||||
# shells out to /sbin/validate_data once per call, and this list is 111k
|
||||
# lines - that is 111k process spawns, which is not a test that passes, it
|
||||
# is a test that never finishes.
|
||||
# - an empty or all-malformed source must fail rather than install a
|
||||
# rule-set that matches nothing.
|
||||
#
|
||||
# Usage: sh tests/ucode/test_domain_ruleset.sh <repo-root> [workdir]
|
||||
|
||||
set -u
|
||||
|
||||
ROOT="$(cd "${1:-$(dirname "$0")/../..}" && pwd)"
|
||||
WORK="${2:-/tmp/hp-domain-test}"
|
||||
|
||||
if ! command -v ucode > "/dev/null" 2>&1; then
|
||||
echo "NOT RUN: ucode is not on PATH."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
HELPER="$ROOT/root/etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc"
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK"
|
||||
|
||||
FAILED=0
|
||||
CHECKS=0
|
||||
FAILURES=0
|
||||
|
||||
expect() {
|
||||
# expect <name> <actual> <expected>
|
||||
CHECKS=$((CHECKS + 1))
|
||||
if [ "$2" = "$3" ]; then
|
||||
echo "PASS: $1"
|
||||
else
|
||||
echo "FAIL: $1 (expected '$3', got '$2')"
|
||||
FAILED=1
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
generate() {
|
||||
# generate <source> <destination>; stdout is "N domain suffixes"
|
||||
ucode -S "$HELPER" "$1" "$2" 2>"/dev/null"
|
||||
}
|
||||
|
||||
# --- fixtures ---------------------------------------------------------------
|
||||
|
||||
# A list shaped like the real one: comments, blank lines, plain two-label
|
||||
# names, the single-label and deep forms that appear in a real suffix list, and
|
||||
# the ways a line can be malformed.
|
||||
#
|
||||
# Every malformed entry below is named so that its own assertion's grep pattern
|
||||
# MATCHES it. That is the point: an assertion whose pattern cannot match the
|
||||
# entry it is about stays green whether or not the entry is rejected, which is
|
||||
# a guard pointing at the wrong side. The first draft of this fixture had
|
||||
# "trailing-hyphen.com" and "trailing.dot.com" - both perfectly valid names -
|
||||
# while the assertions claimed a trailing hyphen and a trailing dot, and both
|
||||
# assertions were vacuous.
|
||||
cat > "$WORK/mixed.txt" <<'EOF'
|
||||
# comment
|
||||
; also a comment
|
||||
|
||||
taobao.com
|
||||
163.com
|
||||
0.zone
|
||||
a.b.c.example.cn
|
||||
xn--fiqs8s.com
|
||||
under_score.com
|
||||
-leading-hyphen.com
|
||||
trailinghyphen-.com
|
||||
double..dot.com
|
||||
.dotted.com
|
||||
trailingdot.com.
|
||||
with space.com
|
||||
http://scheme.com
|
||||
192.168.1.1
|
||||
EOF
|
||||
|
||||
# Five survive: taobao.com, 163.com, 0.zone, a.b.c.example.cn,
|
||||
# xn--fiqs8s.com. Everything else in the fixture is one malformation.
|
||||
expect "mixed list: only the valid entries survive" \
|
||||
"$(generate "$WORK/mixed.txt" "$WORK/mixed.json")" "5 domain suffixes"
|
||||
expect "mixed list: the underscore name is gone" \
|
||||
"$(grep -c 'under_score' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the leading-hyphen label is gone" \
|
||||
"$(grep -c 'leading-hyphen' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the trailing-hyphen label is gone" \
|
||||
"$(grep -c 'trailinghyphen-' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the doubled dot is gone" \
|
||||
"$(grep -c 'double\.\.dot' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the leading dot is gone" \
|
||||
"$(grep -c '"\.dotted\.com"' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the trailing dot is gone" \
|
||||
"$(grep -c '"trailingdot\.com\."' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the name with a space is gone" \
|
||||
"$(grep -c 'with space' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the URL is gone" \
|
||||
"$(grep -c 'scheme.com' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: a bare address is not treated as a domain" \
|
||||
"$(grep -c '192\.168\.1\.1' "$WORK/mixed.json")" "0"
|
||||
expect "mixed list: the plain names stay" \
|
||||
"$(grep -c 'taobao\.com\|163\.com\|0\.zone\|a\.b\.c\.example\.cn\|xn--fiqs8s\.com' "$WORK/mixed.json")" "5"
|
||||
|
||||
# The single most consequential property: suffix, not exact.
|
||||
expect "output is a single domain_suffix rule" \
|
||||
"$(grep -c '"domain_suffix"' "$WORK/mixed.json")" "1"
|
||||
expect "output does not use exact domain matching" \
|
||||
"$(grep -c '"domain":' "$WORK/mixed.json")" "0"
|
||||
expect "output carries a version" \
|
||||
"$(grep -c '"version": 3' "$WORK/mixed.json")" "1"
|
||||
|
||||
# --- the real bundled list --------------------------------------------------
|
||||
|
||||
# This is the list the DNS split runs on. A generator that mangles it would
|
||||
# send every mainland lookup to the proxy resolver while looking healthy.
|
||||
if [ -f "$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" ]; then
|
||||
expect "the bundled china_list.txt generates" \
|
||||
"$(generate "$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" "$WORK/real.json")" \
|
||||
"$(wc -l < "$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" | tr -d ' ') domain suffixes"
|
||||
expect "the bundled list is emitted as suffixes, not exact names" \
|
||||
"$(grep -c '"domain_suffix"' "$WORK/real.json")" "1"
|
||||
expect "the bundled list emits no exact-domain rules" \
|
||||
"$(grep -c '"domain":' "$WORK/real.json")" "0"
|
||||
else
|
||||
echo "SKIP: the bundled china_list.txt is not in the tree."
|
||||
fi
|
||||
|
||||
# --- failure paths ----------------------------------------------------------
|
||||
|
||||
# An empty source must not install an empty rule-set: the DNS split would then
|
||||
# match nothing and every mainland lookup would go to the proxy resolver, with
|
||||
# nothing in the log saying so.
|
||||
: > "$WORK/empty.txt"
|
||||
expect "empty source produces no file" "$(generate "$WORK/empty.txt" "$WORK/empty.json")" ""
|
||||
ucode -S "$HELPER" "$WORK/empty.txt" "$WORK/empty.json" >/dev/null 2>&1
|
||||
expect "empty source exits non-zero" "$?" "1"
|
||||
|
||||
# A source that is entirely malformed is the same case reached differently.
|
||||
printf 'http://a\nhttp://b\nunder_score\n' > "$WORK/junk.txt"
|
||||
expect "all-malformed source produces no file" "$(generate "$WORK/junk.txt" "$WORK/junk.json")" ""
|
||||
ucode -S "$HELPER" "$WORK/junk.txt" "$WORK/junk.json" >/dev/null 2>&1
|
||||
expect "all-malformed source exits non-zero" "$?" "1"
|
||||
|
||||
# A missing source is a caller error, not an empty list.
|
||||
expect "missing source exits non-zero" \
|
||||
"$(ucode -S "$HELPER" "$WORK/no-such-file.txt" "$WORK/none.json" >/dev/null 2>&1; echo $?)" "1"
|
||||
|
||||
# --- atomicity --------------------------------------------------------------
|
||||
|
||||
# sing-box watches this file and reloads it in place, so a partially written
|
||||
# one is read by a running instance. The destination has to appear whole or
|
||||
# not at all.
|
||||
cat > "$WORK/atomic.txt" <<'EOF'
|
||||
one.example
|
||||
two.example
|
||||
three.example
|
||||
EOF
|
||||
generate "$WORK/atomic.txt" "$WORK/atomic.json" >/dev/null
|
||||
expect "the destination is complete after a run" \
|
||||
"$(grep -c 'one\.example\|two\.example\|three\.example' "$WORK/atomic.json")" "3"
|
||||
expect "no temporary file is left behind" \
|
||||
"$(ls "$WORK" | grep -c 'atomic\.json\.tmp')" "0"
|
||||
|
||||
# --- summary ----------------------------------------------------------------
|
||||
|
||||
echo " ($CHECKS checks, $FAILURES failures)"
|
||||
exit "$FAILED"
|
||||
@@ -118,6 +118,86 @@ OUT="$STAGE/rendered.nft"
|
||||
ERR="$STAGE/render.err"
|
||||
trap '[ "$OWN_STAGE" = 1 ] && rm -rf "$STAGE"' EXIT INT TERM
|
||||
|
||||
# --- the node-address bypass is not gated on the literal-address file ------
|
||||
#
|
||||
# This is a source-level check, not a render one, on purpose. The render layer
|
||||
# below is skipped wherever firewall4 is absent - which is every off-target
|
||||
# run - and the failure this pins is one that renders perfectly and does
|
||||
# nothing: with a host-name node there are no literal addresses, so a set and
|
||||
# rules gated on "the literal list is non-empty" simply are not emitted, and
|
||||
# dnsmasq's nftset= back-fill has no set to land in. r41 shipped that.
|
||||
#
|
||||
# The invariant, stated so it can be grepped: `node_addr_v4` may guard the
|
||||
# `elements = { ... }` line and nothing else. Deciding whether the set and the
|
||||
# return rules exist at all belongs to node_protect, which is true when either
|
||||
# export file has content.
|
||||
if ! grep -q 'function node_addr_protected(run_dir)' "$TEMPLATE_SRC"; then
|
||||
echo "FAIL: firewall_post.ut has no node_addr_protected(); the node bypass cannot"
|
||||
echo " distinguish 'nothing to protect' from 'a host name dnsmasq will resolve'"
|
||||
FAILED=1
|
||||
else
|
||||
np_body="$(sed -n '/function node_addr_protected(run_dir)/,/^}/p' "$TEMPLATE_SRC")"
|
||||
for f in node-addr-ips.txt node-addr-domains.txt; do
|
||||
if ! printf '%s' "$np_body" | grep -q "$f"; then
|
||||
echo "FAIL: node_addr_protected() does not look at $f"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
[ "$FAILED" -eq 0 ] && echo "PASS: node_addr_protected() covers both export files"
|
||||
fi
|
||||
|
||||
# Both helpers read run_dir, and they are defined ~80 lines above its
|
||||
# declaration. In this ucode a closure captures the block variables that exist
|
||||
# where the function is DEFINED, so a function defined earlier cannot see a
|
||||
# const declared later - the call fails with "access to undeclared variable
|
||||
# run_dir" while run_dir sits right there in the file. Passing it in is the
|
||||
# only spelling that works, and r43 is what proved it.
|
||||
for fn in node_addr_protected node_addr_to_nftarr; do
|
||||
if ! grep -q "function $fn(run_dir" "$TEMPLATE_SRC"; then
|
||||
echo "FAIL: $fn does not take run_dir as a parameter; in this ucode a closure only"
|
||||
echo " sees block variables declared before the function definition, and both"
|
||||
echo " helpers are defined above run_dir"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
[ "$FAILED" -eq 0 ] && echo "PASS: the node-address helpers take run_dir as a parameter"
|
||||
|
||||
# Every consumer of the v4 set outside its own elements= line must be gated on
|
||||
# node_protect. `node_addr_v4)` as a condition is exactly the mistake.
|
||||
stray_gate="$(awk '
|
||||
/if \(node_addr_v4/ {
|
||||
# The one legitimate use guards the elements= line that follows it.
|
||||
saw = 1; ln = NR; next
|
||||
}
|
||||
saw {
|
||||
if ($0 ~ /elements *= *\{/) { saw = 0; next }
|
||||
print " " ln ": " $0
|
||||
saw = 0
|
||||
}
|
||||
' "$TEMPLATE_SRC" || true)"
|
||||
if [ -n "$stray_gate" ]; then
|
||||
echo "FAIL: the node-address set is gated on node_addr_v4 having elements; a host-name"
|
||||
echo " node has none, and the set plus its return rules then never exist:"
|
||||
printf ' %s\n' "$stray_gate"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# And the return rules themselves must be present in all three chains, each
|
||||
# behind node_protect rather than behind a literal-address list.
|
||||
for chain in redirect mangle_prerouting mangle_output; do
|
||||
if ! grep -q "$chain" "$TEMPLATE_SRC"; then
|
||||
echo "FAIL: chain $chain disappeared from firewall_post.ut"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
v4_rules="$(grep -c 'ip daddr @homeproxy_node_addr_v4 counter return' "$TEMPLATE_SRC" || true)"
|
||||
if [ "$v4_rules" -eq 3 ]; then
|
||||
echo "PASS: all three chains carry the node-address return rule"
|
||||
else
|
||||
echo "FAIL: expected 3 'ip daddr @homeproxy_node_addr_v4 counter return' rules, found $v4_rules"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# Probe with ucode, not utpl: `utpl -e` is not an eval flag, it renders the
|
||||
# argument as template text and always succeeds.
|
||||
if ! ucode -e 'require("fw4");' > "/dev/null" 2>&1; then
|
||||
|
||||
@@ -73,6 +73,19 @@ run_case() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# And the DNS half of the split, staged for the same reason and by the
|
||||
# same route: hp_prepare_runtime_files runs domain_ruleset.uc over
|
||||
# china_list.txt, and generate_client.uc reports the result as
|
||||
# ctx.china_domain_ready (an lstat on this very file). Without it the
|
||||
# DNS rule that carries the domestic lookup is not emitted at all - by
|
||||
# design, because a rule_set naming a tag nothing declared makes
|
||||
# sing-box reject the whole config.
|
||||
if ! ucode -S "$ROOT/root/etc/homeproxy-pro/scripts/runtime/domain_ruleset.uc" \
|
||||
"$ROOT/root/etc/homeproxy-pro/resources/china_list.txt" "$dir/resources/china-domain.json" \
|
||||
>>"$dir/resources/china-domain.log" 2>&1; then
|
||||
printf '{"version":3,"rules":[{"domain_suffix":["example.invalid"]}]}\n' > "$dir/resources/china-domain.json"
|
||||
fi
|
||||
|
||||
if grep -q "__RULESET_DIR__" "$fixture"; then
|
||||
# The fixture needs a real local rule-set on disk. The path has to
|
||||
# satisfy validateRuleSetPath(), whose RULE_PATH_ROOTS was rewritten
|
||||
@@ -127,15 +140,11 @@ run_case() {
|
||||
fi
|
||||
|
||||
|
||||
# Stage the two empty-rule-set files the "do not block startup" fallback
|
||||
# uses. RULESET_INITIAL_DIR is derived from HP_DIR, which was rewritten
|
||||
# to $dir above, so the generator looks for them under $dir/ruleset/initial
|
||||
# and the repository's copy has to be staged there. Copied rather than
|
||||
# referenced so a case that deletes or corrupts one cannot damage the
|
||||
# checkout - the byte-exactness of the constant is arch-guard 52's job.
|
||||
mkdir -p "$dir/ruleset/initial"
|
||||
cp "$ROOT/root/etc/homeproxy-pro/ruleset/initial/empty.srs" "$dir/ruleset/initial/"
|
||||
cp "$ROOT/root/etc/homeproxy-pro/ruleset/initial/empty.source.json" "$dir/ruleset/initial/"
|
||||
# Stage the rule-set archive. A user-defined `type: local` rule-set names
|
||||
# a path under it, and `sing-box check` opens every one of them, so the
|
||||
# directory has to exist before the generated configuration is validated -
|
||||
# the same reason hp_prepare_ruleset_dir() creates it on a real start.
|
||||
mkdir -p "$dir/ruleset"
|
||||
|
||||
# HP_VALIDATE_DATA lets a development host replace /sbin/validate_data
|
||||
# (see tests/README.md); on a target the production path is kept.
|
||||
@@ -464,6 +473,75 @@ EOF
|
||||
esac
|
||||
fi
|
||||
|
||||
# The same fixture with a NON-default self_mark. 250 is deliberately clear
|
||||
# of every other mark in the fixtures (self_mark 100, tproxy_mark 101,
|
||||
# tun_mark 102), so a value that leaked in from the wrong place would show up
|
||||
# as a mismatch rather than as a coincidence.
|
||||
#
|
||||
# Why this case exists at all: the mark is a two-sided contract read from one
|
||||
# UCI option - firewall_post.ut renders `meta mark <self_mark> counter
|
||||
# return`, generator/context.uc turns the same value into every outbound's
|
||||
# routing_mark. Both sides read infra.self_mark, but they normalise it
|
||||
# differently: the nft side goes through mark_value_or(), which falls back to
|
||||
# 100 for anything fw4.parse_mark does not reduce to a bare hex value, while
|
||||
# the generator hands the raw string to strToInt(). Until now every fixture
|
||||
# declared 100, so the two paths never had to agree on anything else and the
|
||||
# divergence was untested.
|
||||
#
|
||||
# A dedicated fixture file would differ from redirect.uci by that one option,
|
||||
# which is what run_case's variation argument is for (see the main_udp_node
|
||||
# case below for the other user of it). The expected value is read back from
|
||||
# the STAGED fixture rather than hardcoded: fixture_self_mark() takes a path,
|
||||
# and the staged copy is what the generator actually saw, so the assertion
|
||||
# cannot drift away from its input the way a literal 250 would.
|
||||
run_case redirect-self-mark "$ROOT/tests/fixtures/generators/redirect.uci" generate_client.uc sing-box-c.json \
|
||||
"s/^\([[:space:]]*\)option self_mark '100'/\1option self_mark '250'/"
|
||||
|
||||
rsm_dir="$WORK/redirect-self-mark"
|
||||
rsm_json="$rsm_dir/run/sing-box-c.json"
|
||||
rsm_mark="$(fixture_self_mark "$rsm_dir/config/homeproxy-pro")"
|
||||
|
||||
if [ "$rsm_mark" != "250" ]; then
|
||||
# Two ways to land here, both a vacuous assertion rather than a product
|
||||
# failure: the variation did not apply, or fixture_self_mark cannot read
|
||||
# what is there. run_case's own cmp guard catches a variation that
|
||||
# matched nothing, so reaching this means the mark is unreadable.
|
||||
echo "FAIL: redirect-self-mark: the staged fixture declares self_mark='$rsm_mark',"
|
||||
echo " not the 250 this case varies to, so the assertion is vacuous"
|
||||
FAILED=1
|
||||
elif [ ! -f "$rsm_json" ]; then
|
||||
echo "FAIL: redirect-self-mark: no config was generated"
|
||||
FAILED=1
|
||||
else
|
||||
mark_result="$(ucode "$WORK/markcheck.uc" "$rsm_mark" "$rsm_json" 2>"/dev/null")"
|
||||
mark_checked="${mark_result%% *}"
|
||||
mark_problems="${mark_result##* }"
|
||||
|
||||
case "$mark_checked" in
|
||||
''|*[!0-9]*)
|
||||
echo "FAIL: redirect-self-mark: could not check the emitted outbounds ($rsm_json)"
|
||||
FAILED=1
|
||||
;;
|
||||
0)
|
||||
echo "FAIL: redirect-self-mark: no dialling outbound was emitted, the assertion is vacuous"
|
||||
FAILED=1
|
||||
;;
|
||||
*)
|
||||
if [ "$mark_problems" -ne 0 ]; then
|
||||
echo "FAIL: redirect-self-mark: $mark_problems of $mark_checked dialling outbounds do not"
|
||||
echo " carry routing_mark=$rsm_mark. With infra.self_mark at a non-default"
|
||||
echo " value the nft side and the generator must still agree: a generator"
|
||||
echo " that fell back to 100 (or dropped the field) while firewall_post.ut"
|
||||
echo " emitted 'meta mark 250 counter return' leaves the loop-prevention"
|
||||
echo " return matching nothing."
|
||||
FAILED=1
|
||||
else
|
||||
echo "PASS: redirect-self-mark: all $mark_checked dialling outbounds carry routing_mark=$rsm_mark"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# The UDP tproxy path: a dedicated UDP node makes context.uc assign
|
||||
# tproxy_port, firewall_post.ut emit the tproxy chain, and inbound.uc emit the
|
||||
# tproxy-in that chain redirects to. No fixture covered this path before -
|
||||
@@ -722,7 +800,7 @@ const config = json(readfile(ARGV[0]));
|
||||
const rules = config.dns?.rules || [];
|
||||
let has_eval_tag = false, has_match_response = false, geoip_ref = false;
|
||||
/* rule_set is emitted as an array because the cn-fallback match names more
|
||||
* than one rule-set when IPv6 support is on (geoip-cn + china-ip6). sing-box
|
||||
* than one rule-set when IPv6 support is on (china-ip + china-ip6). sing-box
|
||||
* accepts a bare string for the single-tag case, so accept both shapes here
|
||||
* rather than pinning the one this build happens to produce. */
|
||||
const names = (v) => (type(v) === 'array' ? v : [v]);
|
||||
@@ -731,7 +809,7 @@ for (let r in rules) {
|
||||
has_eval_tag = true;
|
||||
if (r.match_response === 'cn-fallback') {
|
||||
has_match_response = true;
|
||||
if (index(names(r.rule_set), 'geoip-cn') >= 0)
|
||||
if (index(names(r.rule_set), 'china-ip') >= 0)
|
||||
geoip_ref = true;
|
||||
}
|
||||
}
|
||||
@@ -739,7 +817,7 @@ printf('%d %d %d\n', +has_eval_tag, +has_match_response, +geoip_ref);
|
||||
EOF
|
||||
cn_info="$(ucode "$WORK/cn-fallback.uc" "$client_json" 2>/dev/null)"
|
||||
if [ "$cn_info" = "1 1 1" ]; then
|
||||
echo "PASS: cn-fallback-default-on: evaluate+match_response with geoip-cn is in the proxy config"
|
||||
echo "PASS: cn-fallback-default-on: evaluate+match_response with china-ip is in the proxy config"
|
||||
else
|
||||
echo "FAIL: cn-fallback-default-on: evaluate/match_response flags = '$cn_info', expected '1 1 1'"
|
||||
FAILED=1
|
||||
@@ -957,36 +1035,36 @@ run_case_type_error dangling-resolver "does not exist or is disabled" \
|
||||
# it runs identically on macOS and the testbed. ucode-stdout probes
|
||||
# are out; structural assertions against the emitted bytes are in.
|
||||
|
||||
# 1) P2 #2: geosite-cn is the *DNS*-layer rule_set consumer.
|
||||
# 1) china-domain is the *DNS*-layer rule_set consumer.
|
||||
# The route layer never references it as a rule_set value (route.rule_set
|
||||
# declares it as a tag). Indent cannot disambiguate dns.rules[] from
|
||||
# route.rules[] (both pretty-print at 4 tabs on the testbed; both are
|
||||
# bare-string `"rule_set": "..."`), so use count-based invariants:
|
||||
# - "tag": "geosite-cn" appears at least once (route.rule_set)
|
||||
# - "rule_set": "geosite-cn" appears EXACTLY once (dns.rules only;
|
||||
# - "tag": "china-domain" appears at least once (route.rule_set)
|
||||
# - "rule_set": "china-domain" appears EXACTLY once (dns.rules only;
|
||||
# route.rules must NOT add a second one).
|
||||
# A route regression that adds `"rule_set": "geosite-cn"` to route.rules
|
||||
# A route regression that adds `"rule_set": "china-domain"` to route.rules
|
||||
# bumps the count to 2 and fails loud.
|
||||
run_case route-no-geosite-cn "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
|
||||
run_case route-no-china-domain "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
|
||||
|
||||
rs_json="$WORK/route-no-geosite-cn/run/sing-box-c.json"
|
||||
rs_json="$WORK/route-no-china-domain/run/sing-box-c.json"
|
||||
if [ ! -f "$rs_json" ]; then
|
||||
echo "FAIL: route-no-geosite-cn: no config was generated"
|
||||
echo "FAIL: route-no-china-domain: no config was generated"
|
||||
FAILED=1
|
||||
else
|
||||
rs_tag_count="$(grep -c '"tag": "geosite-cn"' "$rs_json")"
|
||||
rs_ruleset_count="$(grep -c '"rule_set": "geosite-cn"' "$rs_json")"
|
||||
rs_tag_count="$(grep -c '"tag": "china-domain"' "$rs_json")"
|
||||
rs_ruleset_count="$(grep -c '"rule_set": "china-domain"' "$rs_json")"
|
||||
if [ "$rs_tag_count" -lt 1 ]; then
|
||||
echo "FAIL: route-no-geosite-cn: route.rule_set does not declare geosite-cn (tag count=$rs_tag_count, want >=1)"
|
||||
echo "FAIL: route-no-china-domain: route.rule_set does not declare china-domain (tag count=$rs_tag_count, want >=1)"
|
||||
FAILED=1
|
||||
fi
|
||||
if [ "$rs_ruleset_count" -ne 1 ]; then
|
||||
echo "FAIL: route-no-geosite-cn: expected exactly 1 '\"rule_set\": \"geosite-cn\"' line (dns.rules only); got $rs_ruleset_count"
|
||||
grep -n '"rule_set": "geosite-cn"' "$rs_json" | sed 's/^/ /'
|
||||
echo "FAIL: route-no-china-domain: expected exactly 1 '\"rule_set\": \"china-domain\"' line (dns.rules only); got $rs_ruleset_count"
|
||||
grep -n '"rule_set": "china-domain"' "$rs_json" | sed 's/^/ /'
|
||||
FAILED=1
|
||||
fi
|
||||
if [ "$rs_tag_count" -ge 1 ] && [ "$rs_ruleset_count" -eq 1 ]; then
|
||||
echo "PASS: route-no-geosite-cn: route.rule_set has geosite-cn (tag=$rs_tag_count); route.rules never references it (rule_set=$rs_ruleset_count, dns only)"
|
||||
echo "PASS: route-no-china-domain: route.rule_set has china-domain (tag=$rs_tag_count); route.rules never references it (rule_set=$rs_ruleset_count, dns only)"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1355,177 +1433,74 @@ for pair in "ruleset-update-interval:3600s:3600" "ruleset-update-interval-unit:2
|
||||
fi
|
||||
done
|
||||
|
||||
# 8) Batch 2a: the "do not block startup on the first download" fallback.
|
||||
# 8) Node server addresses: the generator exports them for the intercept layer.
|
||||
#
|
||||
# A remote rule-set with no initial_path is fetched during initialization,
|
||||
# BEFORE the inbounds bind. On a cold cache - which is what a fresh install
|
||||
# has, since cache.db ships empty - that means the first start waits on
|
||||
# raw.githubusercontent.com, through the selected node, before the instance
|
||||
# will listen at all; and a failure there is a FATAL that the health gate
|
||||
# turns into a rollback or a released intercept layer.
|
||||
# LAN traffic to the node's own address used to be redirected into sing-box,
|
||||
# which then dialled that same address through the tunnel to that same node.
|
||||
# sing-box's own outbound is exempt (routing_mark / self_mark); the LAN was
|
||||
# not, and nothing in the rule-set kept it out.
|
||||
#
|
||||
# The opt-in points such a rule-set at an EMPTY rule-set, so sing-box reads
|
||||
# a file, starts, and refreshes from the URL in the background. It is
|
||||
# opt-in because an empty rule-set matches nothing and everything it would
|
||||
# have split falls through to `final` - a routing change, not a robustness
|
||||
# tweak.
|
||||
#
|
||||
# The first case is the one that matters most and is the easiest to get
|
||||
# wrong: with the switch off, nothing may change. Not "the feature is off",
|
||||
# but no initial_path, no file written, and a configuration that differs
|
||||
# from the pre-feature one.
|
||||
# The two files are the whole mechanism, and both are checked here because
|
||||
# either one being wrong fails silently: a missing domains file means dnsmasq
|
||||
# writes no nftset= snippet, and a missing ips file means the set renders with
|
||||
# no elements. Neither logs anything at the point of failure.
|
||||
run_case node-addr "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
|
||||
|
||||
# 8a) The default. opt-in off -> no initial_path anywhere, and no fallback
|
||||
# file created. The generated config must be identical to the same
|
||||
# fixture with the option absent, which is the upgrade-invisible property.
|
||||
run_case safe-start-off "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
|
||||
run_case safe-start-off-explicit "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json \
|
||||
"s/option log_level 'error'/option log_level 'error'\n\toption ruleset_safe_start '0'/"
|
||||
|
||||
for name in safe-start-off safe-start-off-explicit; do
|
||||
f="$WORK/$name/run/sing-box-c.json"
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "FAIL: $name: no config was generated"
|
||||
FAILED=1
|
||||
elif grep -q 'initial_path' "$f"; then
|
||||
echo "FAIL: $name: the opt-in is OFF but the config carries an initial_path:"
|
||||
grep -n 'initial_path' "$f" | sed 's/^/ /'
|
||||
FAILED=1
|
||||
elif [ -d "$WORK/$name/ruleset/initial" ] && [ -n "$(ls -A "$WORK/$name/ruleset/initial" 2>/dev/null | grep -v '^empty\.')" ]; then
|
||||
echo "FAIL: $name: the opt-in is OFF but a fallback file was written:"
|
||||
ls -1 "$WORK/$name/ruleset/initial" | sed 's/^/ /'
|
||||
FAILED=1
|
||||
else
|
||||
echo "PASS: $name: opt-in off -> no initial_path, no fallback file"
|
||||
fi
|
||||
done
|
||||
|
||||
# 8b) opt-in on, the built-ins. These two ARE the feature: an earlier version
|
||||
# of this file only knew how to build their entries, and the CLI had no
|
||||
# idea they existed, so a cold install blocked on downloading them.
|
||||
run_case safe-start-on "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json \
|
||||
"s/option log_level 'error'/option log_level 'error'\n\toption ruleset_safe_start '1'/"
|
||||
|
||||
ss_json="$WORK/safe-start-on/run/sing-box-c.json"
|
||||
ss_initial="$WORK/safe-start-on/ruleset/initial"
|
||||
if [ ! -f "$ss_json" ]; then
|
||||
echo "FAIL: safe-start-on: no config was generated"
|
||||
na_dir="$WORK/node-addr/run"
|
||||
if [ ! -f "$na_dir/sing-box-c.json" ]; then
|
||||
echo "FAIL: node-addr: no config was generated"
|
||||
FAILED=1
|
||||
else
|
||||
# grep -F on the fully-constructed path, not an awk range around the entry.
|
||||
# The range form over-captured: it ran past the entry's closing brace into
|
||||
# the next one and then reported success on whatever it found, so a
|
||||
# rule-set with NO initial_path would have passed whenever the next entry
|
||||
# had one. Asserting the exact string also pins that the path is inside
|
||||
# this run's archive, which is the property that makes it readable by the
|
||||
# jailed client and admitted by the path whitelist.
|
||||
for tag in geoip-cn geosite-cn; do
|
||||
want="\"initial_path\": \"$ss_initial/$tag.srs\""
|
||||
if ! grep -qF "$want" "$ss_json"; then
|
||||
echo "FAIL: safe-start-on: the built-in '$tag' does not carry"
|
||||
echo " $want"
|
||||
grep -n 'initial_path' "$ss_json" | sed 's/^/ /'
|
||||
FAILED=1
|
||||
elif [ ! -s "$ss_initial/$tag.srs" ]; then
|
||||
echo "FAIL: safe-start-on: '$tag' points at an initial file that was not written"
|
||||
FAILED=1
|
||||
else
|
||||
echo "PASS: safe-start-on: built-in '$tag' -> initial_path inside the archive, file present"
|
||||
fi
|
||||
done
|
||||
|
||||
# The file has to be a real compiled rule-set, not a copy of the source
|
||||
# JSON and not an empty file: sing-box IGNORES an initial file whose format
|
||||
# does not match the declared one and blocks startup exactly as if there
|
||||
# were none, which would make the whole feature a no-op that looks enabled.
|
||||
# The magic is the check - it is what distinguishes a .srs from source JSON.
|
||||
for tag in geoip-cn geosite-cn; do
|
||||
f="$ss_initial/$tag.srs"
|
||||
[ -s "$f" ] || continue
|
||||
if [ "$(dd if="$f" bs=1 count=3 2>/dev/null)" = "SRS" ]; then
|
||||
echo "PASS: safe-start-on: $tag.srs is a compiled rule-set (SRS magic)"
|
||||
else
|
||||
echo "FAIL: safe-start-on: $tag.srs is not a compiled rule-set - the first three"
|
||||
echo " bytes are not the SRS magic, so sing-box would ignore the initial file"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
# The fixture's nodes are all host names (a.example.com ...), so the
|
||||
# domains file must name them. "The file exists" is NOT the assertion:
|
||||
# join() with its arguments the wrong way round returns null, null + "\n"
|
||||
# is the string "null\n", and the file is then non-empty while naming
|
||||
# nothing. A released r41 shipped exactly that and dnsmasq went on to
|
||||
# resolve a host called "null". So this greps for a name that is
|
||||
# actually in the fixture.
|
||||
if grep -qx 'a\.example\.com' "$na_dir/node-addr-domains.txt" 2>/dev/null; then
|
||||
echo "PASS: node-addr: node-addr-domains.txt names the node ($(wc -l < "$na_dir/node-addr-domains.txt") host names)"
|
||||
elif [ -s "$na_dir/node-addr-domains.txt" ]; then
|
||||
echo "FAIL: node-addr: node-addr-domains.txt is non-empty but does not name a.example.com:"
|
||||
sed 's/^/ /' "$na_dir/node-addr-domains.txt" | head -5
|
||||
FAILED=1
|
||||
else
|
||||
echo "FAIL: node-addr: no node-addr-domains.txt; dnsmasq would get no nftset= snippet and"
|
||||
echo " LAN connections to the node would be redirected into the proxy"
|
||||
FAILED=1
|
||||
fi
|
||||
# The literal "null" is the specific failure above, and it is worth
|
||||
# naming: it is the one value that satisfies a bare existence check.
|
||||
if grep -qx 'null' "$na_dir/node-addr-domains.txt" 2>/dev/null \
|
||||
|| grep -qx 'null' "$na_dir/node-addr-ips.txt" 2>/dev/null; then
|
||||
echo "FAIL: node-addr: an exported file contains the literal 'null' - join() has its"
|
||||
echo " arguments reversed (ucode takes the separator first)"
|
||||
FAILED=1
|
||||
fi
|
||||
# A literal address cannot be back-filled by dnsmasq - there is no name for
|
||||
# it to resolve - so firewall_post.ut renders it from this file instead.
|
||||
if grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$|^[0-9a-fA-F:]+:[0-9a-fA-F:]*$' "$na_dir/node-addr-ips.txt" 2>/dev/null; then
|
||||
echo "PASS: node-addr: node-addr-ips.txt holds literal addresses"
|
||||
elif [ -s "$na_dir/node-addr-domains.txt" ]; then
|
||||
# The fixture's node is a host name, so an empty ips file is the
|
||||
# correct answer here rather than a silent failure.
|
||||
echo "PASS: node-addr: no literal-address node in this fixture, ips file correctly empty"
|
||||
else
|
||||
echo "FAIL: node-addr: node-addr-ips.txt is missing and the domains file is empty too -"
|
||||
echo " the generator did not export anything"
|
||||
FAILED=1
|
||||
fi
|
||||
# An entry that is neither a host name nor an address is what the split is
|
||||
# for; a bracketed IPv6 literal has to arrive unbracketed or the nft set
|
||||
# element is a syntax error and fw4 rejects the whole ruleset.
|
||||
if grep -q '\[' "$na_dir/node-addr-domains.txt" 2>/dev/null || grep -q '\[' "$na_dir/node-addr-ips.txt" 2>/dev/null; then
|
||||
echo "FAIL: node-addr: an address kept its brackets; the nft set element would be invalid"
|
||||
FAILED=1
|
||||
else
|
||||
echo "PASS: node-addr: no bracketed address leaked into either file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 8c/8d) opt-in on, user-defined remote rule-sets: one tag and two tags.
|
||||
# One fixture covers both, plus a disabled one:
|
||||
# tests/fixtures/generators/custom_safe_start.uci. A separate file rather
|
||||
# than sed variations of custom.uci, because the opt-in belongs to the
|
||||
# `config homeproxy-pro 'routing'` section - a different section than anything
|
||||
# the existing variations anchor on - and because a two-expression sed that
|
||||
# both inserts an option and appends a whole section is a construct this
|
||||
# suite has never run. The one-expression form is proven here; the
|
||||
# two-expression form is not, and a test whose own tooling is untested is
|
||||
# how a case quietly stops testing anything.
|
||||
run_case safe-start-user-remote "$ROOT/tests/fixtures/generators/custom_safe_start.uci" \
|
||||
generate_client.uc sing-box-c.json
|
||||
|
||||
sr_json="$WORK/safe-start-user-remote/run/sing-box-c.json"
|
||||
sr_initial="$WORK/safe-start-user-remote/ruleset/initial"
|
||||
if [ ! -f "$sr_json" ]; then
|
||||
echo "FAIL: safe-start-user-remote: no config was generated"
|
||||
FAILED=1
|
||||
else
|
||||
# Single tag: a concrete path, and the file it names has to be there. A
|
||||
# pointer at a file that does not exist is not a degraded feature, it is no
|
||||
# feature - sing-box ignores it and blocks startup exactly as before.
|
||||
# Same reason as 8b: assert the exact string, not a range that can run on
|
||||
# into the next entry.
|
||||
sr_want="\"initial_path\": \"$sr_initial/cfg-rs_remote-rule.srs\""
|
||||
if ! grep -qF "$sr_want" "$sr_json"; then
|
||||
echo "FAIL: safe-start-user-remote: the single-tag rule-set does not carry"
|
||||
echo " $sr_want"
|
||||
grep -n 'initial_path' "$sr_json" | sed 's/^/ /'
|
||||
FAILED=1
|
||||
fi
|
||||
if [ -s "$sr_initial/cfg-rs_remote-rule.srs" ]; then
|
||||
echo "PASS: safe-start-user-remote: single-tag -> concrete initial_path, file present"
|
||||
else
|
||||
echo "FAIL: safe-start-user-remote: no fallback file for cfg-rs_remote-rule.srs"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# Multi tag: {tag} in the path, and one file per tag. The entry is matched
|
||||
# on the multi-tag shape (a tag ARRAY) rather than on the section name, so
|
||||
# the assertion cannot be satisfied by the single-tag entry above.
|
||||
mt_want="\"initial_path\": \"$sr_initial/{tag}.srs\""
|
||||
if ! grep -qF "$mt_want" "$sr_json"; then
|
||||
echo "FAIL: safe-start-user-remote: the multi-tag rule-set does not carry"
|
||||
echo " $mt_want"
|
||||
echo " sing-box substitutes {tag} and opens every resulting file, so a"
|
||||
echo " literal path would make it fetch and block - the failure this removes"
|
||||
grep -n 'initial_path' "$sr_json" | sed 's/^/ /'
|
||||
FAILED=1
|
||||
fi
|
||||
mt_missing=""
|
||||
for tag in cfg-rs_multi-rule cfg-alt-rule; do
|
||||
[ -s "$sr_initial/$tag.srs" ] || mt_missing="$mt_missing $tag"
|
||||
done
|
||||
if [ -n "$mt_missing" ]; then
|
||||
echo "FAIL: safe-start-user-remote: no fallback file for:$mt_missing"
|
||||
echo " sing-box opens every tag's file, so a missing one blocks startup"
|
||||
FAILED=1
|
||||
else
|
||||
echo "PASS: safe-start-user-remote: multi-tag -> {tag} placeholder, one file per tag"
|
||||
fi
|
||||
|
||||
# Disabled: build_user_rulesets() skips it, so it must be absent from the
|
||||
# config AND must not have caused a file to be written for it.
|
||||
if grep -q 'cfg-rs_off-rule' "$sr_json"; then
|
||||
echo "FAIL: safe-start-user-remote: a disabled rule-set reached the generated config"
|
||||
FAILED=1
|
||||
elif [ -e "$sr_initial/cfg-rs_off-rule.srs" ]; then
|
||||
echo "FAIL: safe-start-user-remote: a fallback file was written for a disabled rule-set"
|
||||
FAILED=1
|
||||
else
|
||||
echo "PASS: safe-start-user-remote: a disabled rule-set gets neither path nor file"
|
||||
fi
|
||||
fi
|
||||
# 6) P3 #8 (extra_tags die() on missing {tag}): deferred to
|
||||
# a dedicated 'testbed-dialect' sprint. The multi-line sed to
|
||||
# attach an extra_tags list onto the existing rs_local ruleset
|
||||
|
||||
@@ -224,9 +224,9 @@ check('certificate_write tolerates an empty request', threw == null, threw);
|
||||
* executed. Four of these were referenced by no test at all. */
|
||||
const all_methods = [
|
||||
'acllist_read', 'acllist_write', 'certificate_write', 'connection_check',
|
||||
'log_clean', 'node_parse', 'resources_get_version', 'resources_update',
|
||||
'singbox_generator', 'singbox_get_features', 'update_subscriptions',
|
||||
'update_subscriptions_status'
|
||||
'debug_report', 'log_clean', 'node_parse', 'resources_get_version',
|
||||
'resources_update', 'singbox_generator', 'singbox_get_features',
|
||||
'update_subscriptions', 'update_subscriptions_status'
|
||||
];
|
||||
|
||||
for (let m in all_methods) {
|
||||
@@ -249,6 +249,78 @@ check('connection_check rejects an unknown site',
|
||||
check('log_clean rejects an unknown log type',
|
||||
rpc.log_clean.call({ args: { type: '../etc/passwd' } }).status !== 0);
|
||||
|
||||
/* --- debug_report -------------------------------------------------------
|
||||
*
|
||||
* This one is worth more than a shape check, because the property that
|
||||
* makes it usable is the opposite of "works on my router": it has to
|
||||
* produce a complete, readable report on a machine that has none of the
|
||||
* runtime - no nft, no sing-box, no apk, no /etc/config/homeproxy-pro. That is
|
||||
* exactly this test host, so a regression that turns a failed probe into an
|
||||
* exception, or drops a section when its command is missing, fails here
|
||||
* rather than on a user's router where the report is the thing they are
|
||||
* relying on. */
|
||||
import { isSecretKey } from '@@SCRIPTS@@/runtime/debug_report.uc';
|
||||
|
||||
const dr = rpc.debug_report.call({ args: {} });
|
||||
check('debug_report succeeds where no runtime is installed', dr.result === true,
|
||||
dr.error);
|
||||
check('debug_report names the file it wrote', type(dr.path) === 'string' && length(dr.path) > 0);
|
||||
check('debug_report reports a non-zero size', dr.size > 0);
|
||||
|
||||
const dr_text = readfile(sprintf('%s/run/debug.log', WORK));
|
||||
check('the report was written where the ACL grants read', dr_text != null && length(dr_text) > 0);
|
||||
|
||||
if (dr_text != null && length(dr_text) > 0) {
|
||||
check('the report carries a title', index(dr_text, '# HomeProxy ') != -1);
|
||||
check('the report carries a privacy notice', index(dr_text, '脱敏') != -1);
|
||||
|
||||
/* Every section header, checked as a literal substring rather than a
|
||||
* regex: the headers carry CJK and full-width brackets, and a probe that
|
||||
* throws would take the whole report down rather than leave one section
|
||||
* missing - so "the section is still there" is the property worth pinning. */
|
||||
for (let h in ['## 1. ', '## 2. ', '## 3. ', '## 4. ', '## 5. ',
|
||||
'## 6. ', '## 7. ', '## 8. ', '## 9. ', '## 10. '])
|
||||
check(sprintf('the report still has section %s', h), index(dr_text, h) != -1);
|
||||
|
||||
/* A missing command must be said out loud. A report that silently omits
|
||||
* the firewall dump reads exactly like "the firewall is empty", which is
|
||||
* the opposite of what happened. */
|
||||
check('an unavailable probe is labelled, not omitted',
|
||||
index(dr_text, 'unavailable') != -1 || index(dr_text, '(no output)') != -1);
|
||||
}
|
||||
|
||||
/* The redaction rule has no I/O to observe, so it is asserted directly: a
|
||||
* key that quietly stops being recognised is a credential in an issue
|
||||
* thread, and nothing else in this suite would notice. */
|
||||
for (let k in ['password', 'uuid', 'secret', 'token', 'private_key', 'url',
|
||||
'urls', 'subscription_urls', 'address', 'tls_sni', 'username',
|
||||
'hysteria_obfs_password', 'tls_reality_public_key',
|
||||
'tls_reality_short_id', 'ws_path', 'hysteria_obfs_method',
|
||||
'auth'])
|
||||
check(sprintf('the key %s is redacted', k), isSecretKey(k));
|
||||
|
||||
/* ...and the other direction matters just as much: over-broad matching
|
||||
* would blank out the very switches the report exists to show. */
|
||||
for (let k in ['proxy_mode', 'routing_mode', 'mixed_port', 'redirect_port',
|
||||
'self_mark', 'tproxy_mark', 'log_level', 'ipv6_support',
|
||||
'enabled', 'grouphash'])
|
||||
check(sprintf('the key %s is left readable', k), !isSecretKey(k));
|
||||
|
||||
/* The urltest lists are the reason `url` is anchored rather than a bare
|
||||
* substring: `main_urltest_nodes` contains the letters u-r-l, so an
|
||||
* unanchored pattern blanked the pool out of the report - and "is this node
|
||||
* still in the test pool" is exactly what the report is read for. These
|
||||
* three were found by a canary run, not by inspection. */
|
||||
for (let k in ['main_urltest_nodes', 'main_urltest_tolerance',
|
||||
'main_udp_urltest_nodes'])
|
||||
check(sprintf('the key %s is left readable (urltest, not url)', k), !isSecretKey(k));
|
||||
|
||||
/* grpc_servicename names the gRPC service, it is not a credential: masking
|
||||
* it would hide which transport a node uses, which is worth reporting.
|
||||
* Asserted on the readable side because the obvious guess is that anything
|
||||
* shaped like a service identifier should be hidden. */
|
||||
check('the key grpc_servicename is left readable', !isSecretKey('grpc_servicename'));
|
||||
|
||||
printf('rpc methods: %d checks, %d failures\n', checks, failures);
|
||||
exit(failures ? 1 : 0);
|
||||
DRIVER
|
||||
@@ -259,7 +331,8 @@ DRIVER
|
||||
# `methods` and the driver drives it in the same scope. Nothing else about the
|
||||
# file changes.
|
||||
sed '$d' "$WORK/rpc.uc" > "$WORK/run.uc"
|
||||
sed -e "s#@@WORK@@#$WORK#" -e "s#@@STAGE@@#$WORK/tmp#" "$WORK/driver_body.uc" >> "$WORK/run.uc"
|
||||
sed -e "s#@@WORK@@#$WORK#" -e "s#@@STAGE@@#$WORK/tmp#" -e "s#@@SCRIPTS@@#$WORK/scripts#g" \
|
||||
"$WORK/driver_body.uc" >> "$WORK/run.uc"
|
||||
|
||||
if ucode -L "$WORK/scripts" -L "$WORK" "$WORK/run.uc"; then
|
||||
echo "PASS: rpc method behaviour"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=Network Shares - Ksmbd the SMB kernel fileserver
|
||||
LUCI_DEPENDS:=+luci-base +ksmbd-server +ksmbd-hotplug
|
||||
LUCI_DEPENDS:=+luci-base +ksmbd-server +ksmbd-hotplug +umdns +nbnsd
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Richard Yu <yurichard3839@gmail.com>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for OpenVPN Server
|
||||
LUCI_DEPENDS:=+openvpn-openssl +openvpn-easy-rsa
|
||||
LUCI_DEPENDS:=+openvpn-openssl +openvpn-easy-rsa +ip-full +resolveip
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-openvpn-server-client
|
||||
PKG_VERSION:=6.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
MAINTAINER:=lunatickochiya <125438787@qq.com>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
@@ -27,9 +27,57 @@ function index()
|
||||
entry({"admin", "vpn", "openvpn-server","status"},call("act_status")).leaf=true
|
||||
end
|
||||
|
||||
-- 内核是否提供 DCO (需要 ovpn.ko)
|
||||
function dco_kernel_supported()
|
||||
local fs = require "nixio.fs"
|
||||
local krel = luci.sys.exec("uname -r"):gsub("%s+$", "")
|
||||
return (fs.access("/sys/module/ovpn") or fs.access("/lib/modules/" .. krel .. "/ovpn.ko")) and true or false
|
||||
end
|
||||
|
||||
-- openvpn 是否编译了 DCO 支持
|
||||
function dco_build_supported()
|
||||
local v = luci.sys.exec("openvpn --version 2>/dev/null | head -n1")
|
||||
return v:find("%[DCO%]") ~= nil
|
||||
end
|
||||
|
||||
-- DCO 是否真的在工作: 优先看内核模块引用计数, 退路是查运行日志
|
||||
function dco_in_use()
|
||||
local fs = require "nixio.fs"
|
||||
local rc = fs.readfile("/sys/module/ovpn/refcnt")
|
||||
if rc then
|
||||
-- 注意: gsub 返回两个值, 必须加括号只取第一个
|
||||
local n = tonumber((rc:gsub("%s+$", "")))
|
||||
if n and n > 0 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local logf = uci:get("openvpn", "myvpn", "log") or "/tmp/openvpn.log"
|
||||
local hit = luci.sys.exec("grep -m1 -E 'DCO device .* opened|ovpn-dco device' '" .. logf .. "' 2>/dev/null")
|
||||
return hit ~= nil and hit ~= ""
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local e={}
|
||||
e.running=luci.sys.call("pgrep openvpn >/dev/null")==0
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
local e = {}
|
||||
e.running = luci.sys.call("pgrep openvpn >/dev/null") == 0
|
||||
|
||||
e.dco_kernel = dco_kernel_supported()
|
||||
e.dco_build = dco_build_supported()
|
||||
e.dco_supported = (e.dco_kernel and e.dco_build) and true or false
|
||||
e.dco_active = dco_in_use()
|
||||
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local ac = uci:get("openvpn", "myvpn", "allow_compression")
|
||||
local lzo = uci:get("openvpn", "myvpn", "comp_lzo")
|
||||
local dis = uci:get("openvpn", "myvpn", "disable_dco")
|
||||
|
||||
e.dco_disabled = (dis == "1")
|
||||
e.allow_compression = ac or ""
|
||||
e.comp_lzo = lzo or ""
|
||||
-- 非法的压缩组合: allow-compression=no 与 comp_lzo 同时存在, openvpn 会拒绝启动
|
||||
e.conflict = (ac == "no" and lzo ~= nil and lzo ~= "")
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
@@ -33,6 +33,42 @@ end
|
||||
|
||||
local major, minor, patch, lzo = get_openvpn_info()
|
||||
|
||||
-- DCO 是否真正可用: 需要内核模块 ovpn.ko (只有 openvpn 编译支持还不够)
|
||||
local function dco_supported()
|
||||
local fs = require "nixio.fs"
|
||||
local krel = luci.sys.exec("uname -r"):gsub("%s+$", "")
|
||||
return (fs.access("/sys/module/ovpn") or fs.access("/lib/modules/" .. krel .. "/ovpn.ko")) and true or false
|
||||
end
|
||||
local dco_ok = dco_supported()
|
||||
|
||||
-- 打开页面时顺手修复历史遗留的不一致配置:
|
||||
-- 1) allow-compression=no 与 comp_lzo 不能共存 (openvpn 会拒绝启动)
|
||||
-- 2) 启用 DCO 时不能有压缩 (否则 DCO 静默失效, 不加速)
|
||||
if (major > 2) or (major == 2 and minor >= 5) or dco_ok then
|
||||
local cur = require("luci.model.uci").cursor()
|
||||
cur:foreach("openvpn", "openvpn", function(sec)
|
||||
local name = sec[".name"]
|
||||
local ac, lzo, dis = sec.allow_compression, sec.comp_lzo, sec.disable_dco
|
||||
local need_fix = false
|
||||
|
||||
if dco_ok and dis ~= "1" then
|
||||
-- DCO 已启用: 压缩必须关掉
|
||||
if ac ~= "no" or (lzo ~= nil and lzo ~= "") then
|
||||
need_fix = true
|
||||
end
|
||||
elseif ac == "no" and lzo then
|
||||
-- 致命冲突: allow-compression=no 与 comp_lzo 共存
|
||||
need_fix = true
|
||||
end
|
||||
|
||||
if need_fix then
|
||||
cur:set("openvpn", name, "allow_compression", "no")
|
||||
cur:delete("openvpn", name, "comp_lzo")
|
||||
cur:commit("openvpn")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
mp = Map("openvpn", "OpenVPN Server",translate("An easy config OpenVPN Server Web-UI"))
|
||||
|
||||
mp:section(SimpleSection).template = "openvpn/openvpn_status"
|
||||
@@ -72,7 +108,16 @@ proto.default ="tcp4"
|
||||
|
||||
if (major > 2) or (major == 2 and minor >= 6) then
|
||||
disable_dco = s:taboption("basic",Flag,"disable_dco", translate("disable dco"))
|
||||
disable_dco.description = translate("Disabling DCO provides better compatibility but disables acceleration.")
|
||||
if dco_ok then
|
||||
disable_dco.description = translate("Better compatibility but no acceleration. This device supports DCO: uncheck to enable it (compression will be turned off automatically).")
|
||||
else
|
||||
disable_dco.description = translate("Better compatibility but no acceleration. This device does NOT support DCO (kernel module ovpn.ko is missing); unchecking has no effect.")
|
||||
end
|
||||
|
||||
-- DCO 与压缩互斥: 启用 DCO 时必须关闭压缩, 否则 openvpn 会静默退回用户态,
|
||||
-- DCO 等于没开。
|
||||
-- 注意: Flag 取消勾选时 LuCI 不会调用 write, 所以实际修正放在 on_after_commit
|
||||
-- (那个钩子每次保存都会执行, 已验证)。
|
||||
end
|
||||
|
||||
if (major > 2) or (major == 2 and minor >= 5) then
|
||||
@@ -84,6 +129,26 @@ allow_compression:value("no")
|
||||
allow_compression.default="asym"
|
||||
allow_compression.description = translate("Allow compression, DCO can only be used when set to NO. asym is compatible mode.")
|
||||
|
||||
-- 修复 allow_compression 与 comp_lzo 的冲突:
|
||||
-- openvpn 2.5+ 在两者同时存在时会直接拒绝启动, 报:
|
||||
-- "Options error: Compression is not allowed since allow-compression is set to 'stub-only'"
|
||||
-- CBI 的 depends 只会把 comp_lzo 隐藏, 并不会删除 UCI 里已有的值,
|
||||
-- 于是把 Allow Compression 改成 no 再保存, openvpn 就起不来了。
|
||||
-- 这里在写入 allow_compression=no 时同步删除 comp_lzo。
|
||||
local _allow_compression_write = allow_compression.write
|
||||
allow_compression.write = function(self, section, value)
|
||||
local c = require("luci.model.uci").cursor()
|
||||
-- DCO 已启用时不允许再开压缩 (DCO 与压缩互斥), 直接强制为 no
|
||||
if dco_ok and c:get("openvpn", section, "disable_dco") ~= "1" then
|
||||
value = "no"
|
||||
end
|
||||
_allow_compression_write(self, section, value)
|
||||
if value == "no" then
|
||||
c:delete("openvpn", section, "comp_lzo")
|
||||
c:commit("openvpn")
|
||||
end
|
||||
end
|
||||
|
||||
push_peer_info = s:taboption("basic",Flag,"push_peer_info", translate("push peer info"))
|
||||
push_peer_info.description = translate("This will allow the server to know more info about the client like HWADDR, very useful for managing IoT devices.")
|
||||
|
||||
@@ -107,6 +172,17 @@ comp_lzo:depends("comp_lzo", "adaptive")
|
||||
comp_lzo.default="adaptive"
|
||||
end
|
||||
comp_lzo.description = translate("Using LZO compression, it does not support versions above 2.5.X, does not support DCO; if your version number is greater than this version, select NO to disable it.")
|
||||
-- 反向保护: 只要 allow_compression 已经是 no, 就绝不写入 comp_lzo
|
||||
if (major > 2) or (major == 2 and minor >= 5) then
|
||||
local _comp_lzo_write = comp_lzo.write
|
||||
comp_lzo.write = function(self, section, value)
|
||||
local ac = require("luci.model.uci").cursor():get("openvpn", section, "allow_compression")
|
||||
if ac == "no" then
|
||||
return
|
||||
end
|
||||
_comp_lzo_write(self, section, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
auth_user_pass_verify = s:taboption("basic",Value,"auth_user_pass_verify", translate("user password verify"))
|
||||
@@ -252,6 +328,20 @@ end
|
||||
local comp_lzo_val = uci:get("openvpn", "myvpn", "comp_lzo")
|
||||
|
||||
function mp.on_after_commit(self)
|
||||
-- 保存后统一修正 DCO / 压缩 的互斥关系。
|
||||
-- Flag 取消勾选(启用 DCO)时 LuCI 不会走 write, 所以在这里处理
|
||||
-- (on_after_commit 每次保存都会执行)。
|
||||
local c = require("luci.model.uci").cursor()
|
||||
if dco_ok and c:get("openvpn", "myvpn", "disable_dco") ~= "1" then
|
||||
local ac = c:get("openvpn", "myvpn", "allow_compression")
|
||||
local lzo = c:get("openvpn", "myvpn", "comp_lzo")
|
||||
if ac ~= "no" or (lzo ~= nil and lzo ~= "") then
|
||||
c:set("openvpn", "myvpn", "allow_compression", "no")
|
||||
c:delete("openvpn", "myvpn", "comp_lzo")
|
||||
c:commit("openvpn")
|
||||
end
|
||||
end
|
||||
|
||||
os.execute("uci set firewall.openvpn.dest_port=$(uci get openvpn.myvpn.port) && uci commit firewall && /etc/init.d/firewall restart")
|
||||
os.execute("/etc/init.d/openvpn restart")
|
||||
if comp_lzo_val == "no" then
|
||||
|
||||
+12
-1
@@ -48,6 +48,11 @@ o = sl:taboption("base", Flag, "enabled", translate("Enable Instance"))
|
||||
o.default = o.disabled
|
||||
o.rmempty = false
|
||||
|
||||
+o = sl:taboption("base", Flag, "tcp_cubic", translate("Use CUBIC for outer TCP"))
|
||||
o.default = o.disabled
|
||||
o.rmempty = false
|
||||
o.description = translate("For TCP client profiles only. Uses the current route to each remote endpoint and keeps the system congestion control unchanged.")
|
||||
|
||||
o = sl:taboption("base", Value, "config", translate("Config File Path"))
|
||||
o.readonly = true
|
||||
o.placeholder = translate("Auto-generated based on instance identifier")
|
||||
@@ -60,7 +65,7 @@ end
|
||||
|
||||
o = sl:taboption("config_file", TextValue, "_config_content")
|
||||
o.title = translate("OpenVPN Configuration (.ovpn/.conf)")
|
||||
o.description = translate("Paste the content of the .ovpn or .conf file here. This will be saved to the path shown in 'Base Settings'.")
|
||||
o.description = translate("Paste the content of the .ovpn or .conf file here. This will be saved to the path shown in 'Base Settings'.") .. " " .. translate("If DCO hangs on this device, add disable-dco on its own line before reconnecting.")
|
||||
o.rows = 15
|
||||
o.wrap = "off"
|
||||
o.rmempty = true
|
||||
@@ -171,6 +176,12 @@ sp.on_remove = function(self, section)
|
||||
end
|
||||
|
||||
m2.on_commit = function(self)
|
||||
sys.call("/etc/init.d/openvpn-client-cubic enable >/dev/null 2>&1")
|
||||
local route_result = sys.call("/etc/init.d/openvpn-client-cubic restart >/dev/null 2>&1")
|
||||
if route_result ~= 0 then
|
||||
self.message = translate("Could not prepare the CUBIC route for the OpenVPN client.")
|
||||
return
|
||||
end
|
||||
local result = sys.call("/etc/init.d/openvpn reload")
|
||||
if result ~= 0 then
|
||||
self.message = translate("Failed to reload OpenVPN service.")
|
||||
|
||||
@@ -2,14 +2,44 @@
|
||||
XHR.poll(3, '<%=url([[admin]], [[vpn]], [[openvpn-server]], [[status]])%>', null,
|
||||
function(x, data) {
|
||||
var tb = document.getElementById('openvpn_status');
|
||||
if (data && tb) {
|
||||
if (data.running) {
|
||||
var links = '<em><b><font color=green>OpenVPN <%:RUNNING%></font></b></em>';
|
||||
tb.innerHTML = links;
|
||||
} else {
|
||||
tb.innerHTML = '<em><b><font color=red>OpenVPN <%:NOT RUNNING%></font></b></em>';
|
||||
}
|
||||
if (!(data && tb)) return;
|
||||
|
||||
var s = '';
|
||||
|
||||
/* OpenVPN 运行状态 */
|
||||
s += data.running
|
||||
? '<em><b><font color="#27ae60">OpenVPN <%:RUNNING%></font></b></em>'
|
||||
: '<em><b><font color="#e74c3c">OpenVPN <%:NOT RUNNING%></font></b></em>';
|
||||
|
||||
s += ' · ';
|
||||
|
||||
/* DCO 状态 */
|
||||
if (data.dco_active) {
|
||||
s += '<em><b><font color="#27ae60"><%:DCO ACTIVE%></font></b></em>';
|
||||
} else if (!data.dco_supported) {
|
||||
s += '<em><b><font color="#95a5a6"><%:DCO NOT SUPPORTED%></font></b></em>';
|
||||
} else if (data.dco_disabled) {
|
||||
s += '<em><b><font color="#95a5a6"><%:DCO DISABLED%></font></b></em>';
|
||||
} else {
|
||||
s += '<em><b><font color="#e67e22"><%:DCO INACTIVE (COMPRESSION CONFLICT)%></font></b></em>';
|
||||
}
|
||||
|
||||
/* 压缩配置冲突 */
|
||||
if (data.conflict) {
|
||||
s += ' · <em><b><font color="#e74c3c"><%:COMPRESSION CONFIG CONFLICT%></font></b></em>';
|
||||
}
|
||||
|
||||
/* 明细 */
|
||||
var d = [];
|
||||
d.push('<%:DCO kernel module%>: ' + (data.dco_kernel ? '✔' : '✘'));
|
||||
d.push('<%:OpenVPN built with DCO%>: ' + (data.dco_build ? '✔' : '✘'));
|
||||
d.push('allow-compression: ' + (data.allow_compression || '-'));
|
||||
d.push('comp_lzo: ' + (data.comp_lzo || '-'));
|
||||
if (data.conflict) {
|
||||
d.push('<font color="#e74c3c"><%:allow-compression=no and comp_lzo cannot coexist, openvpn will refuse to start.%></font>');
|
||||
}
|
||||
|
||||
tb.innerHTML = s + '<br><small style="color:#7f8c8d">' + d.join(' | ') + '</small>';
|
||||
}
|
||||
);
|
||||
//]]>
|
||||
@@ -19,4 +49,4 @@ XHR.poll(3, '<%=url([[admin]], [[vpn]], [[openvpn-server]], [[status]])%>', null
|
||||
<p id="openvpn_status">
|
||||
<em><%:Collecting data...%></em>
|
||||
</p>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
@@ -216,3 +216,104 @@ msgstr "这将允许服务器获取更多关于客户端的信息,比如硬件
|
||||
|
||||
msgid "Disabling DCO provides better compatibility but disables acceleration."
|
||||
msgstr "禁用DCO可以提供更好的兼容性,但会关闭加速功能。"
|
||||
|
||||
# ---- DCO / compression related (added) ----
|
||||
|
||||
msgid "Allow Compression"
|
||||
msgstr "允许压缩"
|
||||
|
||||
msgid "Allow compression, DCO can only be used when set to NO. asym is compatible mode."
|
||||
msgstr "允许压缩。只有设为 NO 时才能使用 DCO 加速;asym 为兼容模式。"
|
||||
|
||||
msgid "Base Settings"
|
||||
msgstr "基本设置"
|
||||
|
||||
msgid "Better compatibility but no acceleration. This device does NOT support DCO (kernel module ovpn.ko is missing); unchecking has no effect."
|
||||
msgstr "兼容性更好但不走加速。本机不支持 DCO(缺少内核模块 ovpn.ko),取消勾选也不会生效。"
|
||||
|
||||
msgid "Better compatibility but no acceleration. This device supports DCO: uncheck to enable it (compression will be turned off automatically)."
|
||||
msgstr "兼容性更好但不走加速。本机支持 DCO:取消勾选即可启用(会同时自动关闭压缩)。"
|
||||
|
||||
msgid "COMPRESSION CONFIG CONFLICT"
|
||||
msgstr "压缩配置冲突"
|
||||
|
||||
msgid "Client"
|
||||
msgstr "客户端"
|
||||
|
||||
msgid "Client Config File Content"
|
||||
msgstr "客户端配置内容"
|
||||
|
||||
msgid "Collecting data..."
|
||||
msgstr "正在收集数据…"
|
||||
|
||||
msgid "Config File Path"
|
||||
msgstr "配置文件路径"
|
||||
|
||||
msgid "Configure authentication files for OpenVPN client instances."
|
||||
msgstr "为 OpenVPN 客户端实例配置认证文件。"
|
||||
|
||||
msgid "DCO ACTIVE"
|
||||
msgstr "DCO 加速中"
|
||||
|
||||
msgid "DCO DISABLED"
|
||||
msgstr "DCO 已关闭"
|
||||
|
||||
msgid "DCO INACTIVE (COMPRESSION CONFLICT)"
|
||||
msgstr "DCO 未生效(压缩冲突)"
|
||||
|
||||
msgid "DCO NOT SUPPORTED"
|
||||
msgstr "DCO 不支持"
|
||||
|
||||
msgid "DCO kernel module"
|
||||
msgstr "DCO 内核模块"
|
||||
|
||||
msgid "Failed to reload OpenVPN service."
|
||||
msgstr "OpenVPN 服务重载失败。"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "OpenVPN Cert"
|
||||
msgstr "OpenVPN 证书"
|
||||
|
||||
msgid "OpenVPN Client Authentication"
|
||||
msgstr "OpenVPN 客户端认证"
|
||||
|
||||
msgid "OpenVPN built with DCO"
|
||||
msgstr "openvpn 编译支持 DCO"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "Start"
|
||||
msgstr "启动"
|
||||
|
||||
msgid "Stop"
|
||||
msgstr "停止"
|
||||
|
||||
msgid "allow-compression=no and comp_lzo cannot coexist, openvpn will refuse to start."
|
||||
msgstr "allow-compression=no 与 comp_lzo 不能共存,openvpn 会拒绝启动。"
|
||||
|
||||
msgid "client-to-client"
|
||||
msgstr "客户端互通(client-to-client)"
|
||||
|
||||
msgid "client_cert_not_required"
|
||||
msgstr "客户端免证书(client_cert_not_required)"
|
||||
|
||||
msgid "comp_lzo"
|
||||
msgstr "LZO 压缩(comp_lzo)"
|
||||
|
||||
msgid "disable dco"
|
||||
msgstr "关闭 DCO"
|
||||
|
||||
msgid "duplicate_cn"
|
||||
msgstr "允许重复 CN(duplicate_cn)"
|
||||
|
||||
msgid "proto"
|
||||
msgstr "传输协议"
|
||||
|
||||
msgid "push peer info"
|
||||
msgstr "推送客户端信息(push_peer_info)"
|
||||
|
||||
msgid "username_as_common_name"
|
||||
msgstr "用户名作为 CN(username_as_common_name)"
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
config openvpn 'myvpn'
|
||||
option enabled '0'
|
||||
option proto 'tcp4'
|
||||
option dev 'tun'
|
||||
option topology 'subnet'
|
||||
option server '10.8.0.0 255.255.255.0'
|
||||
option comp_lzo 'adaptive'
|
||||
option ca '/etc/openvpn/pki/ca.crt'
|
||||
option dh '/etc/openvpn/pki/dh.pem'
|
||||
option cert '/etc/openvpn/pki/server.crt'
|
||||
option key '/etc/openvpn/pki/server.key'
|
||||
option persist_key '1'
|
||||
option persist_tun '1'
|
||||
option max_clients '88'
|
||||
option keepalive '10 120'
|
||||
option verb '3'
|
||||
option status '/var/log/openvpn_status.log'
|
||||
option log '/tmp/openvpn.log'
|
||||
option port '1194'
|
||||
option ddns 'myserver.f3322.net'
|
||||
option auth_user_pass_verify '/etc/openvpn/server/checkpsw.sh via-env'
|
||||
option script_security '3'
|
||||
list push 'route 192.168.9.0 255.255.255.0'
|
||||
list push 'comp-lzo adaptive'
|
||||
list push 'redirect-gateway def1 bypass-dhcp'
|
||||
list push 'dhcp-option DNS 192.168.9.1'
|
||||
|
||||
config openvpn 'client1'
|
||||
option enabled '0'
|
||||
option config '/etc/openvpn/client1.conf'
|
||||
|
||||
config openvpn 'client2'
|
||||
option enabled '0'
|
||||
option config '/etc/openvpn/client2.conf'
|
||||
|
||||
config openvpnpassword 'pwd1'
|
||||
option pwdfile '/etc/openvpn/pass.txt'
|
||||
|
||||
config openvpnpassword 'pwd2'
|
||||
option pwdfile '/etc/openvpn/pass2.txt'
|
||||
@@ -1,10 +0,0 @@
|
||||
set_var EASYRSA_OPENSSL "openssl"
|
||||
set_var EASYRSA_KEY_SIZE 2048
|
||||
set_var EASYRSA_REQ_COUNTRY "lunatickochiya"
|
||||
set_var EASYRSA_REQ_PROVINCE "github"
|
||||
set_var EASYRSA_REQ_CITY "github"
|
||||
set_var EASYRSA_REQ_ORG "github"
|
||||
set_var EASYRSA_REQ_EMAIL "lunatickochiya@github.com"
|
||||
set_var EASYRSA_REQ_OU "github"
|
||||
set_var EASYRSA_CA_EXPIRE 3650
|
||||
set_var EASYRSA_CERT_EXPIRE 3650
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
case "$ACTION" in
|
||||
ifup|ifupdate) /usr/libexec/openvpn-client-cubic-route update >/dev/null 2>&1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=89
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
start_service() {
|
||||
local attempt=0
|
||||
/usr/libexec/openvpn-client-cubic-route enabled || return 0
|
||||
|
||||
# OpenVPN starts at 90. Wait for DNS and the uplink before it connects.
|
||||
while ! /usr/libexec/openvpn-client-cubic-route update; do
|
||||
attempt=$((attempt + 1))
|
||||
[ "$attempt" -lt 12 ] || {
|
||||
logger -t openvpn-client-cubic 'Could not install CUBIC route before OpenVPN startup'
|
||||
return 1
|
||||
}
|
||||
sleep 2
|
||||
done
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command /usr/libexec/openvpn-client-cubic-route watch
|
||||
procd_set_param respawn 3600 5 -1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
/usr/libexec/openvpn-client-cubic-route clear
|
||||
}
|
||||
@@ -1,34 +1,52 @@
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
function rand_str() {
|
||||
rand_str() {
|
||||
(base64 /dev/urandom | tr -dc 'A-Za-z' | head -c $1) 2>/dev/null
|
||||
}
|
||||
|
||||
function rand_str_upper() {
|
||||
rand_str_upper() {
|
||||
(rand_str $1 | tr 'a-z' 'A-Z') 2>/dev/null
|
||||
}
|
||||
|
||||
function rand_str_lower() {
|
||||
rand_str_lower() {
|
||||
(rand_str $1 | tr 'A-Z' 'a-z') 2>/dev/null
|
||||
}
|
||||
|
||||
function rand_easy_rsa_vars() {
|
||||
set_easy_rsa_var() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
|
||||
if grep -q "^[[:space:]]*set_var[[:space:]]\+$name[[:space:]]" /etc/easy-rsa/vars; then
|
||||
sed -i "s|^[[:space:]]*set_var[[:space:]]\+$name[[:space:]].*$|set_var $name\t$value|" /etc/easy-rsa/vars
|
||||
else
|
||||
printf 'set_var %s\t%s\n' "$name" "$value" >> /etc/easy-rsa/vars
|
||||
fi
|
||||
}
|
||||
|
||||
init_easy_rsa_vars() {
|
||||
touch /etc/easy-rsa/vars
|
||||
set_easy_rsa_var EASYRSA_OPENSSL '"openssl"'
|
||||
set_easy_rsa_var EASYRSA_KEY_SIZE '2048'
|
||||
set_easy_rsa_var EASYRSA_CA_EXPIRE '3650'
|
||||
set_easy_rsa_var EASYRSA_CERT_EXPIRE '3650'
|
||||
}
|
||||
|
||||
rand_easy_rsa_vars() {
|
||||
local KEY_PROVINCE="$(rand_str_upper 6)"
|
||||
local KEY_CITY="$(rand_str 8)"
|
||||
local KEY_ORG="$(rand_str 8)"
|
||||
local KEY_EMAIL="$(rand_str_lower 8)@$(rand_str_lower 4).$(rand_str_lower 3)"
|
||||
local KEY_OU="$(rand_str 8)"
|
||||
sed -i \
|
||||
-e "s/^[[:space:]]*set_var[[:space:]]\+EASYRSA_REQ_COUNTRY[[:space:]]\+\".*\"$/set_var EASYRSA_REQ_COUNTRY\t\"$KEY_PROVINCE\"/" \
|
||||
-e "s/^[[:space:]]*set_var[[:space:]]\+EASYRSA_REQ_PROVINCE[[:space:]]\+\".*\"$/set_var EASYRSA_REQ_PROVINCE\t\"$KEY_CITY\"/" \
|
||||
-e "s/^[[:space:]]*set_var[[:space:]]\+EASYRSA_REQ_CITY[[:space:]]\+\".*\"$/set_var EASYRSA_REQ_CITY\t\"$KEY_ORG\"/" \
|
||||
-e "s/^[[:space:]]*set_var[[:space:]]\+EASYRSA_REQ_ORG[[:space:]]\+\".*\"$/set_var EASYRSA_REQ_ORG\t\"$KEY_ORG\"/" \
|
||||
-e "s/^[[:space:]]*set_var[[:space:]]\+EASYRSA_REQ_EMAIL[[:space:]]\+\".*\"$/set_var EASYRSA_REQ_EMAIL\t\"$KEY_EMAIL\"/" \
|
||||
-e "s/^[[:space:]]*set_var[[:space:]]\+EASYRSA_REQ_OU[[:space:]]\+\".*\"$/set_var EASYRSA_REQ_OU\t\"$KEY_OU\"/" \
|
||||
/etc/easy-rsa/vars
|
||||
set_easy_rsa_var EASYRSA_REQ_COUNTRY "\"$KEY_PROVINCE\""
|
||||
set_easy_rsa_var EASYRSA_REQ_PROVINCE "\"$KEY_CITY\""
|
||||
set_easy_rsa_var EASYRSA_REQ_CITY "\"$KEY_ORG\""
|
||||
set_easy_rsa_var EASYRSA_REQ_ORG "\"$KEY_ORG\""
|
||||
set_easy_rsa_var EASYRSA_REQ_EMAIL "\"$KEY_EMAIL\""
|
||||
set_easy_rsa_var EASYRSA_REQ_OU "\"$KEY_OU\""
|
||||
}
|
||||
|
||||
init_easy_rsa_vars
|
||||
rand_easy_rsa_vars
|
||||
|
||||
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
/etc/init.d/openvpn-client-cubic enable
|
||||
exit 0
|
||||
@@ -1,6 +1,70 @@
|
||||
#!/bin/sh
|
||||
|
||||
grep -qv "myvpn" /etc/config/openvpn && grep -q "myvpn" /etc/config/openvpn-opkg && cp -f /etc/config/openvpn-opkg /etc/config/openvpn
|
||||
if ! uci -q get openvpn.myvpn >/dev/null; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set openvpn.myvpn=openvpn
|
||||
set openvpn.myvpn.enabled='0'
|
||||
set openvpn.myvpn.proto='tcp4'
|
||||
set openvpn.myvpn.dev='tun'
|
||||
set openvpn.myvpn.topology='subnet'
|
||||
set openvpn.myvpn.server='10.8.0.0 255.255.255.0'
|
||||
set openvpn.myvpn.comp_lzo='adaptive'
|
||||
set openvpn.myvpn.ca='/etc/openvpn/pki/ca.crt'
|
||||
set openvpn.myvpn.dh='/etc/openvpn/pki/dh.pem'
|
||||
set openvpn.myvpn.cert='/etc/openvpn/pki/server.crt'
|
||||
set openvpn.myvpn.key='/etc/openvpn/pki/server.key'
|
||||
set openvpn.myvpn.persist_key='1'
|
||||
set openvpn.myvpn.persist_tun='1'
|
||||
set openvpn.myvpn.max_clients='88'
|
||||
set openvpn.myvpn.keepalive='10 120'
|
||||
set openvpn.myvpn.verb='3'
|
||||
set openvpn.myvpn.status='/var/log/openvpn_status.log'
|
||||
set openvpn.myvpn.log='/tmp/openvpn.log'
|
||||
set openvpn.myvpn.port='1194'
|
||||
set openvpn.myvpn.ddns='myserver.f3322.net'
|
||||
set openvpn.myvpn.auth_user_pass_verify='/etc/openvpn/server/checkpsw.sh via-env'
|
||||
set openvpn.myvpn.script_security='3'
|
||||
add_list openvpn.myvpn.push='route 192.168.9.0 255.255.255.0'
|
||||
add_list openvpn.myvpn.push='comp-lzo adaptive'
|
||||
add_list openvpn.myvpn.push='redirect-gateway def1 bypass-dhcp'
|
||||
add_list openvpn.myvpn.push='dhcp-option DNS 192.168.9.1'
|
||||
commit openvpn
|
||||
EOF
|
||||
fi
|
||||
|
||||
if ! uci -q get openvpn.client1 >/dev/null; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set openvpn.client1=openvpn
|
||||
set openvpn.client1.enabled='0'
|
||||
set openvpn.client1.config='/etc/openvpn/client1.conf'
|
||||
commit openvpn
|
||||
EOF
|
||||
fi
|
||||
|
||||
if ! uci -q get openvpn.client2 >/dev/null; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set openvpn.client2=openvpn
|
||||
set openvpn.client2.enabled='0'
|
||||
set openvpn.client2.config='/etc/openvpn/client2.conf'
|
||||
commit openvpn
|
||||
EOF
|
||||
fi
|
||||
|
||||
if ! uci -q get openvpn.pwd1 >/dev/null; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set openvpn.pwd1=openvpnpassword
|
||||
set openvpn.pwd1.pwdfile='/etc/openvpn/pass.txt'
|
||||
commit openvpn
|
||||
EOF
|
||||
fi
|
||||
|
||||
if ! uci -q get openvpn.pwd2 >/dev/null; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set openvpn.pwd2=openvpnpassword
|
||||
set openvpn.pwd2.pwdfile='/etc/openvpn/pass2.txt'
|
||||
commit openvpn
|
||||
EOF
|
||||
fi
|
||||
|
||||
openvpn_port="$(uci -q get openvpn.myvpn.port)"
|
||||
[ -z "$openvpn_port" ] && openvpn_port=1194
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
#!/bin/sh
|
||||
# Set CUBIC only for opted-in OpenVPN clients' outer TCP destinations.
|
||||
|
||||
umask 077
|
||||
state=${OPENVPN_CLIENT_CUBIC_STATE:-/tmp/openvpn-client-cubic-routes}
|
||||
|
||||
sections() {
|
||||
uci -q show openvpn 2>/dev/null |
|
||||
sed -n 's/^openvpn\.\(client[A-Za-z0-9_-]*\)=openvpn$/\1/p'
|
||||
}
|
||||
|
||||
client_enabled() {
|
||||
[ "$(uci -q get "openvpn.$1.enabled")" = 1 ] &&
|
||||
[ "$(uci -q get "openvpn.$1.tcp_cubic")" = 1 ]
|
||||
}
|
||||
|
||||
has_clients() {
|
||||
for section in $(sections); do
|
||||
client_enabled "$section" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
delete_old_routes() {
|
||||
[ -f "$state" ] || return 0
|
||||
while IFS='|' read -r address via device; do
|
||||
[ -n "$address" ] && [ -n "$device" ] || continue
|
||||
if [ -n "$via" ]; then
|
||||
ip -4 route del "$address/32" via "$via" dev "$device" 2>/dev/null
|
||||
else
|
||||
ip -4 route del "$address/32" dev "$device" 2>/dev/null
|
||||
fi
|
||||
done < "$state"
|
||||
return 0
|
||||
}
|
||||
|
||||
add_route() {
|
||||
local address=$1 route via device
|
||||
route=$(ip -4 route get "$address" 2>/dev/null) || return 1
|
||||
set -- $route
|
||||
while [ "$#" -gt 1 ]; do
|
||||
case "$1" in
|
||||
via) via=$2; shift 2 ;;
|
||||
dev) device=$2; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$device" ] && [ "$device" != lo ] || return 1
|
||||
if [ -n "$via" ]; then
|
||||
ip -4 route replace "$address/32" via "$via" dev "$device" congctl cubic || return 1
|
||||
else
|
||||
ip -4 route replace "$address/32" dev "$device" congctl cubic || return 1
|
||||
fi
|
||||
printf '%s|%s|%s\n' "$address" "$via" "$device" >> "$new_state"
|
||||
}
|
||||
|
||||
update_routes() {
|
||||
local section conf host addresses address failed=0 desired new_state
|
||||
desired=$(mktemp /tmp/openvpn-client-cubic-desired.XXXXXX) || return 1
|
||||
new_state=$(mktemp /tmp/openvpn-client-cubic-state.XXXXXX) || {
|
||||
rm -f "$desired"
|
||||
return 1
|
||||
}
|
||||
|
||||
for section in $(sections); do
|
||||
client_enabled "$section" || continue
|
||||
conf=$(uci -q get "openvpn.$section.config")
|
||||
if [ ! -r "$conf" ]; then
|
||||
logger -t openvpn-client-cubic "Cannot read config for $section"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
# An outer TCP congestion controller has no effect on UDP clients.
|
||||
grep -Eiq '^[[:space:]]*proto[[:space:]]+tcp' "$conf" || continue
|
||||
for host in $(awk '$1 == "remote" { gsub(/["\047]/, "", $2); print $2 }' "$conf"); do
|
||||
case "$host" in
|
||||
''|*[!0-9.]*) addresses=$(resolveip -4 -t 5 "$host" 2>/dev/null) ;;
|
||||
*) addresses=$host ;;
|
||||
esac
|
||||
if [ -z "$addresses" ]; then
|
||||
logger -t openvpn-client-cubic "Cannot resolve $host for $section"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
for address in $addresses; do
|
||||
case "$address" in
|
||||
*[!0-9.]*|'') continue ;;
|
||||
esac
|
||||
printf '%s\n' "$address" >> "$desired"
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
if [ "$failed" -ne 0 ]; then
|
||||
rm -f "$desired" "$new_state"
|
||||
return 1
|
||||
fi
|
||||
|
||||
delete_old_routes
|
||||
sort -u "$desired" | while IFS= read -r address; do
|
||||
[ -n "$address" ] || continue
|
||||
add_route "$address" || exit 1
|
||||
done
|
||||
failed=$?
|
||||
rm -f "$desired"
|
||||
if [ "$failed" -eq 0 ]; then
|
||||
mv -f "$new_state" "$state"
|
||||
else
|
||||
rm -f "$new_state"
|
||||
fi
|
||||
return "$failed"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
enabled) has_clients ;;
|
||||
update) update_routes ;;
|
||||
clear) delete_old_routes; rm -f "$state" ;;
|
||||
watch)
|
||||
while sleep 300; do
|
||||
update_routes || logger -t openvpn-client-cubic 'Could not refresh CUBIC routes'
|
||||
done
|
||||
;;
|
||||
*) echo "Usage: $0 {enabled|update|clear|watch}" >&2; exit 2 ;;
|
||||
esac
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"luci-app-openvpn-server-client": {
|
||||
"description": "Grant UCI access for luci-app-openvpn-server-client",
|
||||
"read": {
|
||||
"uci": [ "openvpn" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "openvpn" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.10.3
|
||||
PKG_RELEASE:=309
|
||||
PKG_RELEASE:=310
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -894,12 +894,14 @@ end
|
||||
function com_update(comname)
|
||||
local json = nil
|
||||
local task = http.formvalue("task")
|
||||
if task == "extract" then
|
||||
if task == "progress" then
|
||||
json = api.to_download_progress(comname, http.formvalue("id"), http.formvalue("total_size"))
|
||||
elseif task == "extract" then
|
||||
json = api.to_extract(comname, http.formvalue("file"), http.formvalue("subfix"))
|
||||
elseif task == "move" then
|
||||
json = api.to_move(comname, http.formvalue("file"))
|
||||
else
|
||||
json = api.to_download(comname, http.formvalue("url"), http.formvalue("size"))
|
||||
json = api.to_download(comname, http.formvalue("url"), http.formvalue("size"), http.formvalue("id"))
|
||||
end
|
||||
|
||||
http_write_json(json)
|
||||
|
||||
@@ -11,8 +11,13 @@ s = m:section(NamedSection, "@global_app[0]", "global_app", translate("App Updat
|
||||
|
||||
s:appendTemplate("/app_update/app_version", {com = com})
|
||||
|
||||
o = s:option(Flag, "github_proxy", translate("GitHub Proxy"), translate("Use gh-proxy instead of proxy nodes for component updates."))
|
||||
o.default = 0
|
||||
o = s:option(ListValue, "gh_proxy_url", translate("GitHub Proxy"), translate("Use GitHub instead of proxy nodes for component updates."))
|
||||
o.default = ""
|
||||
o:value("", translate("Close(Not use)"))
|
||||
o:value("https://gh-proxy.org/", "gh-proxy.org")
|
||||
o:value("https://ghproxy.cc/", "ghproxy.cc")
|
||||
o:value("https://mirror.ghproxy.com/", "ghproxy.com")
|
||||
o:value("https://ghfast.top/", "ghfast.top")
|
||||
|
||||
local k, v
|
||||
for _, k in ipairs(com.order) do
|
||||
|
||||
@@ -1132,10 +1132,10 @@ local default_file_tree = {
|
||||
}
|
||||
|
||||
local function get_api_json(url)
|
||||
local gh_proxy = uci_get_c("@global_app[0]", "github_proxy") or "0"
|
||||
local gh_proxy = uci_get_c("@global_app[0]", "gh_proxy_url") or ""
|
||||
local return_code, content
|
||||
if gh_proxy == "1" then
|
||||
url = "https://gh-proxy.org/" .. url
|
||||
if gh_proxy ~= "" then
|
||||
url = gh_proxy .. url
|
||||
return_code, content = curl_base(url, nil, curl_args)
|
||||
else
|
||||
return_code, content = curl_auto(url, nil, curl_args)
|
||||
@@ -1232,7 +1232,7 @@ function to_check(arch, app_name)
|
||||
}
|
||||
end
|
||||
|
||||
function to_download(app_name, url, size)
|
||||
function to_download(app_name, url, size, task_id)
|
||||
local result = check_path(app_name)
|
||||
if result.code ~= 0 then
|
||||
return result
|
||||
@@ -1244,7 +1244,13 @@ function to_download(app_name, url, size)
|
||||
|
||||
remove("/tmp/" .. app_name .. "_download.*")
|
||||
|
||||
local tmp_file = trim(util.exec("mktemp -u -t " .. app_name .. "_download.XXXXXX"))
|
||||
local tmp_file
|
||||
if task_id and task_id:match("^[%w_-]+$") then
|
||||
tmp_file = "/tmp/" .. app_name .. "_download." .. task_id
|
||||
remove(tmp_file)
|
||||
else
|
||||
tmp_file = trim(util.exec("mktemp -u -t " .. app_name .. "_download.XXXXXX"))
|
||||
end
|
||||
|
||||
if size then
|
||||
local kb1 = get_free_space("/tmp")
|
||||
@@ -1256,10 +1262,10 @@ function to_download(app_name, url, size)
|
||||
local _curl_args = clone(curl_args)
|
||||
table.insert(_curl_args, "--speed-limit 51200 --speed-time 15 --max-time 300")
|
||||
|
||||
local gh_proxy = uci_get_c("@global_app[0]", "github_proxy") or "0"
|
||||
local gh_proxy = uci_get_c("@global_app[0]", "gh_proxy_url") or ""
|
||||
local return_code, result
|
||||
if gh_proxy == "1" then
|
||||
url = "https://gh-proxy.org/" .. url
|
||||
if gh_proxy ~= "" then
|
||||
url = gh_proxy .. url
|
||||
return_code, result = curl_base(url, tmp_file, _curl_args)
|
||||
else
|
||||
return_code, result = curl_auto(url, tmp_file, _curl_args)
|
||||
@@ -1277,6 +1283,23 @@ function to_download(app_name, url, size)
|
||||
return {code = 0, file = tmp_file, zip = com[app_name].zipped }
|
||||
end
|
||||
|
||||
function to_download_progress(app_name, task_id, total_size)
|
||||
if not com[app_name] or type(task_id) ~= "string" or not task_id:match("^[%w_-]+$") then
|
||||
return {code = 1, error = i18n.translate("Invalid download task.")}
|
||||
end
|
||||
|
||||
total_size = tonumber(total_size) or 0
|
||||
local tmp_file = "/tmp/" .. app_name .. "_download." .. task_id
|
||||
local downloaded = tonumber(fs.stat(tmp_file, "size")) or 0
|
||||
local percent
|
||||
if total_size > 0 then
|
||||
-- The download request has not completed yet, so leave 100% for its success callback.
|
||||
percent = math.min(99, math.floor(downloaded * 100 / total_size))
|
||||
end
|
||||
|
||||
return {code = 0, downloaded = downloaded, total = total_size, percent = percent}
|
||||
end
|
||||
|
||||
function to_extract(app_name, file, subfix)
|
||||
local result = check_path(app_name)
|
||||
if result.code ~= 0 then
|
||||
@@ -1425,10 +1448,10 @@ end
|
||||
function to_check_self()
|
||||
local url = "https://raw.githubusercontent.com/Openwrt-Passwall/openwrt-passwall/main/luci-app-passwall/Makefile"
|
||||
local tmp_file = "/tmp/passwall_makefile"
|
||||
local gh_proxy = uci_get_c("@global_app[0]", "github_proxy") or "0"
|
||||
local gh_proxy = uci_get_c("@global_app[0]", "gh_proxy_url") or ""
|
||||
local return_code, result
|
||||
if gh_proxy == "1" then
|
||||
url = "https://gh-proxy.org/" .. url
|
||||
if gh_proxy ~= "" then
|
||||
url = gh_proxy .. url
|
||||
return_code, result = curl_base(url, tmp_file, curl_args)
|
||||
else
|
||||
return_code, result = curl_auto(url, tmp_file, curl_args)
|
||||
|
||||
@@ -129,12 +129,54 @@ local com = self.com
|
||||
|
||||
var appUpdateUrl = '<%=api.url("update_")%>' + app;
|
||||
var appInfo = appInfoList[app];
|
||||
var downloadId = Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
||||
var totalSize = appInfo && appInfo.data ? Number(appInfo.data.size) : 0;
|
||||
if (!isFinite(totalSize) || totalSize <= 0) {
|
||||
totalSize = 0;
|
||||
}
|
||||
var downloadProgressActive = true;
|
||||
var downloadProgressPending = false;
|
||||
var progressTimer = null;
|
||||
|
||||
function stopDownloadProgress() {
|
||||
downloadProgressActive = false;
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer);
|
||||
progressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDownloadProgress() {
|
||||
if (!downloadProgressActive || downloadProgressPending || totalSize <= 0) {
|
||||
return;
|
||||
}
|
||||
downloadProgressPending = true;
|
||||
XHR.get(appUpdateUrl, {
|
||||
token: tokenStr,
|
||||
task: 'progress',
|
||||
id: downloadId,
|
||||
total_size: totalSize
|
||||
}, function (x, json) {
|
||||
downloadProgressPending = false;
|
||||
if (downloadProgressActive && json && !json.code && typeof json.percent === 'number') {
|
||||
btn.value = downloadingText + ' ' + json.percent + '%';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
if (totalSize > 0) {
|
||||
updateDownloadProgress();
|
||||
progressTimer = setInterval(updateDownloadProgress, 1000);
|
||||
}
|
||||
|
||||
// Download file
|
||||
XHR.get(appUpdateUrl, {
|
||||
token: tokenStr,
|
||||
url: appInfo ? appInfo.data.browser_download_url : '',
|
||||
size: appInfo ? appInfo.data.size / 1024 : null
|
||||
size: appInfo ? appInfo.data.size / 1024 : null,
|
||||
id: downloadId
|
||||
}, function (x, json) {
|
||||
stopDownloadProgress();
|
||||
if (json.code) {
|
||||
removePageNotice();
|
||||
onRequestError(btn, json.error);
|
||||
|
||||
@@ -1199,8 +1199,8 @@ msgstr "用于版本检查的 API URL"
|
||||
msgid "GitHub Proxy"
|
||||
msgstr "GitHub 反代"
|
||||
|
||||
msgid "Use gh-proxy instead of proxy nodes for component updates."
|
||||
msgstr "开启后将使用 gh-proxy 反向代理,不通过节点更新组件。"
|
||||
msgid "Use GitHub instead of proxy nodes for component updates."
|
||||
msgstr "开启后将使用 GitHub 反向代理,不通过节点更新组件。"
|
||||
|
||||
msgid "Node Subscribe"
|
||||
msgstr "节点订阅"
|
||||
|
||||
@@ -28,3 +28,6 @@ dot.360.cn
|
||||
#GitHub Proxy
|
||||
gh-proxy.com
|
||||
gh-proxy.org
|
||||
ghproxy.cc
|
||||
ghfast.top
|
||||
ghproxy.com
|
||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-quickactions
|
||||
PKG_VERSION:=3.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=arafatrahmanzami <zamimondol@gmail.com>
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
File diff suppressed because it is too large.
Load diff
@@ -17,12 +17,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wwand
|
||||
PKG_VERSION:=1.6.9
|
||||
PKG_RELEASE:=37
|
||||
PKG_VERSION:=1.6.9_p1
|
||||
PKG_RELEASE:=38
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
|
||||
PKG_SOURCE_VERSION:=214093f765f34f0d99dcf0e3a94581929d81d9f3
|
||||
PKG_SOURCE_VERSION:=91e492e8c33b192b6bfcfcb8690dfcfd380af7d8
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=natflow
|
||||
PKG_VERSION:=20260930
|
||||
PKG_RELEASE:=89
|
||||
PKG_RELEASE:=90
|
||||
|
||||
PKG_SOURCE:=$(PKG_VERSION).tar.xz
|
||||
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
|
||||
|
||||
@@ -17,7 +17,7 @@ config zone
|
||||
config zone
|
||||
option type 'wan'
|
||||
option id '2'
|
||||
option ifname 'pppoe-wan' # можно префикс: 'eth+'
|
||||
option ifname 'wan' # or mask prefix: 'eth+'
|
||||
|
||||
# config vline
|
||||
# option type 'vline' # vline | relay
|
||||
|
||||
@@ -12,6 +12,28 @@ opt() {
|
||||
[ -n "$3" ] && echo "$2=$3" >"$1"
|
||||
}
|
||||
|
||||
# rate_to_bytes <value> -> байт/с
|
||||
# N | NK|NM|NG (KiB/MiB/GiB, x1024) | Nkbit|Nmbit|Ngbit (биты, x1000)
|
||||
rate_to_bytes() {
|
||||
local v="$1" n u
|
||||
|
||||
n="${v%%[!0-9]*}"
|
||||
u="${v#$n}"
|
||||
[ -n "$n" ] || return 1
|
||||
u="$(echo "$u" | tr 'A-Z' 'a-z')"
|
||||
|
||||
case "$u" in
|
||||
"") echo "$n" ;;
|
||||
k|kb) echo $((n * 1024)) ;;
|
||||
m|mb) echo $((n * 1024 * 1024)) ;;
|
||||
g|gb) echo $((n * 1024 * 1024 * 1024)) ;;
|
||||
kbit) echo $((n * 125)) ;;
|
||||
mbit) echo $((n * 125000)) ;;
|
||||
gbit) echo $((n * 125000000)) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
apply_zone() {
|
||||
local cfg="$1" type id ifname
|
||||
|
||||
@@ -62,6 +84,15 @@ apply_qos() {
|
||||
config_get rxbytes "$cfg" rxbytes 0
|
||||
config_get txbytes "$cfg" txbytes 0
|
||||
|
||||
rxbytes="$(rate_to_bytes "$rxbytes")" || {
|
||||
logger -t natflow "qos $cfg: bad rxbytes, rule skipped"
|
||||
return 0
|
||||
}
|
||||
txbytes="$(rate_to_bytes "$txbytes")" || {
|
||||
logger -t natflow "qos $cfg: bad txbytes, rule skipped"
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "add user=$user,user_port=$user_port,remote=$remote,remote_port=$remote_port,proto=$proto,rxbytes=$rxbytes,txbytes=$txbytes" >"$QOS"
|
||||
}
|
||||
|
||||
@@ -123,4 +154,4 @@ stop() {
|
||||
|
||||
reload() {
|
||||
start
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -50,12 +50,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.6.9_p2
|
||||
PKG_RELEASE:=67
|
||||
PKG_VERSION:=1.6.9_p5
|
||||
PKG_RELEASE:=68
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=e82b67689936839a249ae7a69b9a67e070db253d
|
||||
PKG_SOURCE_VERSION:=c52913c970fa31f845dcb464339cc8c8ab1dac5b
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
Reference in new issue
Block a user