diff --git a/luci-app-adguardhome/.official_channel b/luci-app-adguardhome/.official_channel
index 8ab67f2e7..49f33a8c6 100644
--- a/luci-app-adguardhome/.official_channel
+++ b/luci-app-adguardhome/.official_channel
@@ -1 +1 @@
-snapshot
+both
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index 4bb100248..5bf6fa3df 100644
--- a/luci-app-passwall/Makefile
+++ b/luci-app-passwall/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.4
-PKG_RELEASE:=331
+PKG_RELEASE:=332
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua
index 4a2f7ac7f..9f5d2d9d0 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua
@@ -133,10 +133,10 @@ sources.validate = function(self, value, t)
flag = true
end
end
- if flag == false and datatypes.macaddr(v) then
+ if flag == false and api.datatypes.macaddr(v) then
flag = true
end
- if flag == false and datatypes.ip4addr(v) then
+ if flag == false and api.datatypes.ip4addr(v) then
flag = true
end
if flag == false and api.iprange(v) then
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua
index d353dc0b1..9b3530aaf 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua
@@ -3,6 +3,7 @@ datatypes = api.datatypes
local fs = api.fs
has_singbox = api.finded_com("sing-box")
has_xray = api.finded_com("xray")
+local has_smartdns = api.is_finded("smartdns")
local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")
local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist")
local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute")
@@ -180,7 +181,7 @@ o.write = function(self, section, value)
end
return ListValue.write(self, section, value)
end
-if api.is_finded("smartdns") then
+if has_smartdns then
o:value("smartdns", "SmartDNS")
o = s:taboption("DNS", Value, "group_domestic", translate("Domestic group name"))
@@ -284,7 +285,7 @@ o.remove = function(self, section)
end
---- SmartDNS Forward Mode
-if api.is_finded("smartdns") then
+if has_smartdns then
o = s:taboption("DNS", ListValue, "smartdns_dns_mode", translate("Filter Mode"))
o:value("socks", "Socks")
if has_singbox then
@@ -487,14 +488,14 @@ o:value("remote", translate("Remote DNS"))
o:value("direct", translate("Direct DNS"))
o.description = desc .. ""
o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"})
-if api.is_finded("smartdns") then
+if has_smartdns then
o:depends({dns_shunt = "smartdns", tcp_proxy_mode = "proxy", chn_list = "direct"})
end
o = s:taboption("DNS", Flag, "force_https_soa", translate("Force HTTPS SOA"), translate("Force queries with qtype 65 to respond with an SOA record."))
o.default = "0"
o:depends({dns_shunt = "chinadns-ng"})
-if api.is_finded("smartdns") then
+if has_smartdns then
o:depends({dns_shunt = "smartdns"})
end
diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua
index 827856af7..3f30db51e 100644
--- a/luci-app-passwall/luasrc/passwall/api.lua
+++ b/luci-app-passwall/luasrc/passwall/api.lua
@@ -871,19 +871,26 @@ function clone(org)
end
function get_bin_version_cache(file, cmd)
- sys.call("mkdir -p " .. CACHE_PATH)
- if fs.access(file) then
+ if file and file ~= "" and fs.access(file) then
chmod_755(file)
- local md5 = md5_file(file)
- if fs.access(CACHE_PATH .. "/" .. md5) then
- return sys.exec("echo -n $(cat %s)" % { CACHE_PATH .. "/" .. md5 })
- else
- local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd))
- if version and version ~= "" then
- sys.call("echo '%s' > %s" % { version, CACHE_PATH .. "/" .. md5})
- return version
+ local stat = fs.stat(file)
+ local cache_file
+ if stat and stat.dev and stat.ino and stat.size and stat.mtime and stat.ctime then
+ -- Use metadata to avoid hashing the entire binary on every lookup.
+ cache_file = CACHE_PATH .. "/binver_" .. table.concat({stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime}, "_")
+ local cached = fs.readfile(cache_file)
+ if cached and trim(cached) ~= "" then
+ return trim(cached)
end
end
+ local version = sys.exec(string.format("echo -n $(%s %s)", util.shellquote(file), cmd))
+ if version and version ~= "" then
+ if cache_file then
+ fs.mkdirr(CACHE_PATH)
+ fs.writefile(cache_file, version)
+ end
+ return version
+ end
end
return ""
end
@@ -904,7 +911,7 @@ end
local function is_file(path)
if path and #path > 1 then
- if sys.exec('[ -f "%s" ] && echo -n 1' % path) == "1" then
+ if fs.stat(path, "type") == "reg" then
return true
end
end
@@ -913,7 +920,7 @@ end
local function is_dir(path)
if path and #path > 1 then
- if sys.exec('[ -d "%s" ] && echo -n 1' % path) == "1" then
+ if fs.stat(path, "type") == "dir" then
return true
end
end
@@ -1554,7 +1561,7 @@ function to_install_self(id, force)
remove(dir)
return {code = 1}
end
- fs.chmod(script, 755)
+ chmod_755(script)
fs.writefile(status, '{"installing":true}')
sys.call("nohup " .. script .. " " .. dir .. " " .. status .. " " .. manager .. " " ..
(force == "1" and "1" or "0") .. " >/dev/null 2>&1 0 then
@@ -73,19 +98,28 @@ local function find_label_by_key(k)
end
local current_label = find_label_by_key(current_key) or ""
--- 打包数据给 JS
+-- 控件只携带分组引用,重复的候选数据在页面中仅输出一次
local dropdown_data = {
+ appname = api.appname,
current_key = current_key,
current_label = current_label,
ungrouped = ungrouped,
- groups = groups,
+ group_refs = group_refs,
group_order = group_order,
cbid = cbid,
}
%>
diff --git a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_listvalue_com.htm b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_listvalue_com.htm
index a75124cfb..2a066287b 100644
--- a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_listvalue_com.htm
+++ b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_listvalue_com.htm
@@ -577,8 +577,10 @@
}
// 分组项
- data.group_order.forEach(gname => {
- const items = data.groups[gname];
+ const groupPool = window.lv_groups_pool && window.lv_groups_pool[data.appname];
+ data.group_order.forEach((gname, index) => {
+ const items = data.groups ? data.groups[gname] : groupPool && groupPool[data.group_refs[index]];
+ if (!Array.isArray(items)) return;
html += `
diff --git a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value.htm b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value.htm
index ccaaedc12..7fe732073 100644
--- a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value.htm
+++ b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value.htm
@@ -6,6 +6,7 @@
-- Description: Passwall(2) UI template
local json = require "luci.jsonc"
+local api = self.map.api
-- 读取值(keylist/vallist/group)
local values = {{key = "", label = translate("-- Please choose --")}}
@@ -56,6 +57,30 @@ if not current_key_added and current_key and current_key ~= "" then
table.insert(ungrouped, {key = current_key, label = current_key})
end
+-- 仅共享候选分组,各控件的自定义地址仍保留在自己的 ungrouped 中
+local group_cache = self.map._v_group_cache
+if not group_cache then
+ group_cache = {}
+ self.map._v_group_cache = group_cache
+end
+local group_refs = {}
+local new_groups = {}
+for i, g in ipairs(group_order) do
+ -- 组名、节点 key/label 及顺序完全相同才复用,不与 ListValue 缓存混用
+ local signature = {g}
+ for _, item in ipairs(groups[g]) do
+ signature[#signature + 1] = {item.key, item.label}
+ end
+ local cache_key = json.stringify(signature)
+ local ref = group_cache[cache_key]
+ if not ref then
+ ref = cbid .. ".v-group." .. i
+ group_cache[cache_key] = ref
+ new_groups[ref] = groups[g]
+ end
+ group_refs[i] = ref
+end
+
-- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项)
if not current_key then
if #ungrouped > 0 then
@@ -81,19 +106,28 @@ local function find_label_by_key(k)
end
local current_label = find_label_by_key(current_key) or ""
--- 打包数据给 JS
+-- 控件只携带分组引用,重复的候选数据在页面中仅输出一次
local dropdown_data = {
+ appname = api.appname,
current_key = current_key,
current_label = current_label,
ungrouped = ungrouped,
- groups = groups,
+ group_refs = group_refs,
group_order = group_order,
cbid = cbid,
}
%>
diff --git a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value_com.htm b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value_com.htm
index 0b92052c0..0c5f5c02c 100644
--- a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value_com.htm
+++ b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value_com.htm
@@ -603,8 +603,10 @@
}
// 分组项
- data.group_order.forEach(gname => {
- const items = data.groups[gname];
+ const groupPool = window.v_groups_pool && window.v_groups_pool[data.appname];
+ data.group_order.forEach((gname, index) => {
+ const items = data.groups ? data.groups[gname] : groupPool && groupPool[data.group_refs[index]];
+ if (!Array.isArray(items)) return;
html += `
diff --git a/luci-app-passwall/luasrc/view/passwall/global/status.htm b/luci-app-passwall/luasrc/view/passwall/global/status.htm
index 5f79de131..5a29332c3 100644
--- a/luci-app-passwall/luasrc/view/passwall/global/status.htm
+++ b/luci-app-passwall/luasrc/view/passwall/global/status.htm
@@ -139,7 +139,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
-
+
@@ -163,7 +163,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
-
+
diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile
index eaca14a3a..3fb9f8b6a 100644
--- a/luci-app-passwall2/Makefile
+++ b/luci-app-passwall2/Makefile
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.10.1
-PKG_RELEASE:=140
+PKG_RELEASE:=141
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua
index 9493af6e3..d8493dafb 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua
@@ -29,7 +29,7 @@ local doh_validate = function(self, value, t)
for i = 1, #val do
local v = val[i]
if v then
- if not datatypes.ipmask4(v) then
+ if not api.datatypes.ipmask4(v) then
flag = 1
end
end
@@ -113,11 +113,11 @@ sources.validate = function(self, value, t)
end
end
- if flag == false and datatypes.macaddr(v) then
+ if flag == false and api.datatypes.macaddr(v) then
flag = true
end
- if flag == false and datatypes.ip4addr(v) then
+ if flag == false and api.datatypes.ip4addr(v) then
flag = true
end
diff --git a/luci-app-trafficctl/Makefile b/luci-app-trafficctl/Makefile
index 8346e165c..a19704fc7 100644
--- a/luci-app-trafficctl/Makefile
+++ b/luci-app-trafficctl/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-trafficctl
-PKG_VERSION:=1.21.5
-PKG_RELEASE:=9
+PKG_VERSION:=1.21.6
+PKG_RELEASE:=10
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Denis Iusupov
diff --git a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js
index c2668eb51..409b5c715 100644
--- a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js
+++ b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js
@@ -4212,6 +4212,24 @@ return view.extend({
_('Offloads routing to the hardware engine (PPE/NPU). Requires software offload. ' +
'⚠ On many platforms (e.g. Mediatek Filogic) the driver does not report byte counters back to the kernel — real-time speed monitoring will show zero.'));
+ // Qualcomm NSS acceleration (#81). This is NOT one of the
+ // flow-offload modes above and is not controlled by the two
+ // toggles below it: ECM pushes flow rules into the NSS
+ // firmware cores, so an accelerated flow never reaches the
+ // Linux stack at all and neither the nftables policer nor
+ // Linux HTB can act on it. The apply result says so on every
+ // attempt; this is here because the Flow Offload panel is
+ // where somebody asking "why is my limit doing nothing" looks.
+ if (cfg.nss === 'active') {
+ container.appendChild(E('div', {
+ 'style': 'margin-bottom:10px;padding:8px;border-left:3px solid var(--tc-warn);font-size:12px'
+ }, [
+ E('b', {'style':'color:var(--tc-warn)'}, '⚠ ' + _('Qualcomm NSS acceleration is active')),
+ E('div', {'style':'margin-top:4px;color:var(--tc-muted)'},
+ _('Rate limits and traffic shaping cannot be enforced while it is: accelerated flows bypass the Linux network stack, so rules are installed but have no effect. The toggles below do not control this — stop the ECM front end (front_end_ipv4_stop / front_end_ipv6_stop under /sys/kernel/debug/ecm) or set acceleration_engine in /etc/config/ecm, then re-apply the limit.'))
+ ]));
+ }
+
container.appendChild(modeBadge);
container.appendChild(E('div', {'class':'tc-offload-row'}, [swToggleEl, saveStatus]));
container.appendChild(swDesc);
diff --git a/luci-app-trafficctl/root/usr/libexec/rpcd/luci.trafficctl b/luci-app-trafficctl/root/usr/libexec/rpcd/luci.trafficctl
index e26b9cdf6..1c1e13afe 100755
--- a/luci-app-trafficctl/root/usr/libexec/rpcd/luci.trafficctl
+++ b/luci-app-trafficctl/root/usr/libexec/rpcd/luci.trafficctl
@@ -302,10 +302,11 @@ call)
hw=$(uci -q get firewall.@defaults[0].flow_offloading_hw)
poll=$(uci -q get trafficctl.main.poll_interval)
window=$(uci -q get trafficctl.main.avg_window)
- printf '{"enabled":%s,"default_mode":"%s","offload_mode":"%s","sw":%s,"hw":%s,"poll_interval":%d,"avg_window":%d}' \
+ printf '{"enabled":%s,"default_mode":"%s","offload_mode":"%s","nss":"%s","sw":%s,"hw":%s,"poll_interval":%d,"avg_window":%d}' \
"$([ "$enabled" = "1" ] && echo true || echo false)" \
"${mode:-limiter}" \
"$offload" \
+ "$(tctl_nss_status)" \
"$([ "$sw" = "1" ] && echo true || echo false)" \
"$([ "$hw" = "1" ] && echo true || echo false)" \
"${poll:-2}" \
diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-block.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-block.sh
index 71d1d2300..7ee1b7415 100755
--- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-block.sh
+++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-block.sh
@@ -43,6 +43,9 @@ if tctl_block_add "$IP" "$COMMENT"; then
# flow offload those are never re-evaluated against the new rule — the
# device would stay online over v6 indefinitely while the UI reads blocked.
MAC=$(tctl_target_mac "$IP" 2>/dev/null) && tctl_conntrack_flush_v6 "$MAC"
+ # Same problem one layer further out: a flow already accelerated by the
+ # NSS firmware is not re-evaluated against this rule either.
+ tctl_nss_defunct
tctl_persist_enabled && tctl_persist_save "block" "$IP" "$LABEL"
tctl_log "block" "$IP" "$LABEL" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
@@ -58,10 +61,12 @@ if tctl_block_add "$IP" "$COMMENT"; then
else
NOTE=" — IPv4 only: no MAC known for $IP (no DHCP lease, no neighbour entry), so IPv6 is not covered"
fi
+ NOTE="$NOTE$(tctl_nss_note)"
+ NSS="$(tctl_nss_status)"
if [ "$SELF_BLOCK" = "true" ]; then
- echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"msg\":\"internet blocked for $IP (your device — LuCI access preserved)$NOTE\"}"
+ echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"internet blocked for $IP (your device — LuCI access preserved)$NOTE\"}"
else
- echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"msg\":\"internet blocked for $IP$NOTE\"}"
+ echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"internet blocked for $IP$NOTE\"}"
fi
else
echo "{\"ok\":false,\"msg\":\"failed to block $IP\"}"
diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh
index cb8e39539..ade64af2f 100755
--- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh
+++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh
@@ -1149,3 +1149,81 @@ tctl_get_offload_mode() {
echo "none"
fi
}
+
+# ── Qualcomm NSS / ECM Detection ───────────────────────────────────────────
+#
+# On Qualcomm NSS builds (ipq807x/ipq60xx with qca-nss-ecm, qca-nss-drv and
+# qca-nss-clients) the Enhanced Connection Manager installs flow rules into
+# the NSS firmware cores, and an accelerated flow NEVER TRAVERSES THE LINUX
+# NETWORK STACK. Both of this package's enforcement paths are therefore inert
+# on such a router: the nftables netdev policer never sees the packets, and
+# Linux HTB/IFB never gets to queue them. Reported in issue #81, where a user
+# applied WiFi client limits and nothing was limited.
+#
+# Nothing here attempts to fix that — an NSS backend would mean nsshtb/nsstbl
+# and act_nssmirred in place of htb and ifb, which needs hardware to develop
+# against. What these helpers do is let the callers STOP REPORTING SUCCESS for
+# a rule that cannot be enforced, which is the part that can be made correct
+# without hardware and is the actual complaint.
+#
+# Named by absolute path, as it must be on the router; the tests sed this line
+# into their scratch directory (same approach as TCTL_LEASES_FILE above).
+TCTL_ECM_DEBUGFS="/sys/kernel/debug/ecm"
+
+# absent — no ECM on this system; every rule this package installs applies
+# normally and nothing needs qualifying.
+# stopped — ECM is present but its front end has been stopped, so flows are
+# back on the Linux path and our rules DO apply.
+# active — ECM is accelerating. Our rules are installed but not enforced for
+# any accelerated flow.
+#
+# The distinction between "stopped" and "active" is deliberate and is read
+# from the kernel rather than inferred from the packages installed: somebody
+# who has turned acceleration off to get working QoS must not be told their
+# limits do not work.
+tctl_nss_status() {
+ [ -d "$TCTL_ECM_DEBUGFS" ] || { printf 'absent\n'; return 0; }
+ local v4 v6
+ v4=$(cat "$TCTL_ECM_DEBUGFS/front_end_ipv4_stop" 2>/dev/null)
+ v6=$(cat "$TCTL_ECM_DEBUGFS/front_end_ipv6_stop" 2>/dev/null)
+ # Both stopped is the only state in which the Linux datapath is whole. A
+ # missing knob is NOT read as stopped: an ECM build whose debugfs layout we
+ # do not recognise is still accelerating, and guessing "stopped" there
+ # would restore exactly the false success this exists to remove.
+ if [ "$v4" = "1" ] && [ "$v6" = "1" ]; then
+ printf 'stopped\n'
+ else
+ printf 'active\n'
+ fi
+}
+
+tctl_nss_active() { [ "$(tctl_nss_status)" = "active" ]; }
+
+# The suffix every caller appends to its own message. One wording in one place,
+# so the limiter, the shaper and the block path cannot drift into describing
+# the same condition three different ways.
+tctl_nss_note() {
+ tctl_nss_active || return 0
+ printf ' [NOT ENFORCED: Qualcomm NSS acceleration is active — accelerated flows bypass the Linux network stack entirely, so this rule is installed but has no effect. Stop acceleration (echo 1 > %s/front_end_ipv4_stop, same for ipv6) or set acceleration_engine in /etc/config/ecm, then re-apply]' \
+ "$TCTL_ECM_DEBUGFS"
+}
+
+# Defunct the flows ECM has already accelerated.
+#
+# This is the NSS counterpart of the `conntrack -D` every rule change already
+# does, and it is needed for the same reason: a flow that is ALREADY offloaded
+# is not re-evaluated against a rule that appeared afterwards, so without this
+# a new block or limit would only affect connections started later — which
+# presents as "it works sometimes".
+#
+# Best effort by design: absent on every non-NSS router, and a write failure
+# must never fail the operation the caller is performing. UNVERIFIED ON
+# HARDWARE — there is no IPQ device here to test it against, so it is wired in
+# as an addition to the existing flush rather than as something any result
+# depends on.
+tctl_nss_defunct() {
+ local f="$TCTL_ECM_DEBUGFS/ecm_db/defunct_all"
+ [ -w "$f" ] || return 0
+ echo 1 > "$f" 2>/dev/null || true
+ return 0
+}
diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh
index d9551dd78..7a5e63e91 100755
--- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh
+++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh
@@ -96,11 +96,21 @@ else
tctl_persist_enabled && tctl_persist_save "ratelimit" "$IP" "$RATE" "$MODE" \
"$([ "$RATE_UP" != "$RATE" ] && echo "$RATE_UP")"
tctl_log "ratelimit_set" "$IP" "$(rate_text)" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
+ # An already-accelerated flow is not re-evaluated against a rule that
+ # appeared afterwards, exactly as with flow offload — so the NSS flow
+ # table gets the same treatment the conntrack table does.
+ tctl_nss_defunct
+ # On a Qualcomm NSS router the rules above are installed and inert: the
+ # packets never reach the Linux stack. Reporting an unqualified success
+ # there is the defect in #81, so every message below carries the verdict
+ # and the result carries the status as a field.
+ NSSNOTE="$(tctl_nss_note)"
+ NSS="$(tctl_nss_status)"
if [ "$TCTL_RL_DOWNLOAD_FAILED" = "1" ]; then
- echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE\"}"
+ echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE$NSSNOTE\"}"
elif [ "$TCTL_RL_UPLOAD_FAILED" = "1" ]; then
- echo "{\"ok\":true,\"ipv6_upload\":false,\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]\"}"
+ echo "{\"ok\":true,\"ipv6_upload\":false,\"nss\":\"$NSS\",\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]$NSSNOTE\"}"
else
- echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE\"}"
+ echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE$NSSNOTE\"}"
fi
fi
diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-shape.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-shape.sh
index 78c32c8a1..00f5e522a 100755
--- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-shape.sh
+++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-shape.sh
@@ -303,14 +303,21 @@ do_add() {
save_shape "$IP" "$RATE" "$classid" "$RATE_UP"
local rates="${RATE} kbit/s"
[ "$RATE_UP" != "$RATE" ] && rates="${RATE} kbit/s down, ${RATE_UP} kbit/s up"
+ # Linux HTB never gets to queue a flow the NSS firmware is forwarding, so
+ # a shape on such a router is installed and inert (#81). NSS builds ship
+ # their own qdiscs (nsshtb/nsstbl) which this does not yet use; until it
+ # does, say so instead of reporting success.
+ tctl_nss_defunct
+ NSSNOTE="$(tctl_nss_note)"
+ NSS="$(tctl_nss_status)"
if [ "$ul_ok" = "1" ]; then
- echo "{\"ok\":true,\"msg\":\"shape $rates applied to $IP both directions (class $classid)\"}"
+ echo "{\"ok\":true,\"nss\":\"$NSS\",\"msg\":\"shape $rates applied to $IP both directions (class $classid)$NSSNOTE\"}"
elif [ "$RATE_UP" != "$RATE" ]; then
# Saying "applied" here would be a lie about the half the operator
# explicitly asked to differ.
- echo "{\"ok\":true,\"msg\":\"shape ${RATE} kbit/s applied to $IP for download only — the ${RATE_UP} kbit/s upload ceiling was NOT applied, it needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched\"}"
+ echo "{\"ok\":true,\"nss\":\"$NSS\",\"msg\":\"shape ${RATE} kbit/s applied to $IP for download only — the ${RATE_UP} kbit/s upload ceiling was NOT applied, it needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched$NSSNOTE\"}"
else
- echo "{\"ok\":true,\"msg\":\"shape ${RATE} kbit/s applied to $IP (download only — upload needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched)\"}"
+ echo "{\"ok\":true,\"nss\":\"$NSS\",\"msg\":\"shape ${RATE} kbit/s applied to $IP (download only — upload needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched)$NSSNOTE\"}"
fi
}
diff --git a/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js b/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js
index 7d06b2a56..46e101acd 100644
--- a/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js
+++ b/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js
@@ -82,7 +82,7 @@ return baseclass.extend({
];
});
- cbi_update_table(table, rows, E('em', _('There are no active IPv4/IPv6 port maps')));
+ cbi_update_table(table, rows, E('em', _('No active IPv4/IPv6 port maps')));
return table;
}
diff --git a/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js b/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js
index 93e1870be..d9141e0ec 100644
--- a/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js
+++ b/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js
@@ -73,7 +73,7 @@ return view.extend({
];
});
- cbi_update_table(nodes.querySelector('#upnp_status_table'), rows, E('em', _('There are no active IPv4/IPv6 port maps')));
+ cbi_update_table(nodes.querySelector('#upnp_status_table'), rows, E('em', _('No active IPv4/IPv6 port maps')));
},
render: function(data) {
@@ -137,7 +137,7 @@ return view.extend({
];
});
- cbi_update_table(table, rows, E('em', _('There are no active IPv4/IPv6 port maps')));
+ cbi_update_table(table, rows, E('em', _('No active IPv4/IPv6 port maps')));
return E('div', { 'class': 'cbi-section cbi-tblsection' }, [
E('h3', _('Active Port Maps')), table
diff --git a/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp b/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp
index bd6fe158c..cb44e81c7 100644
--- a/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp
+++ b/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp
@@ -20,11 +20,8 @@ const methods = {
if (leasefile) {
for (let line = leasefile.read('line'); length(line); line = leasefile.read('line')) {
const record = split(line, ':', 7);
- if (length(record) == 6 || length(record) == 7) {
- // Daemon version >2.3.11 changes lease file format
- let recshift = 0;
- if (length(record) == 7) recshift = 1;
- let descr = trim(record[5 + recshift]);
+ if (length(record) == 7) {
+ let descr = trim(record[6]);
let m = match(descr, /^PCP [A-Z]+ ([0-9a-f]{24})$/);
if (m) descr = 'PCP (nonce ' + m[1] + ')';
else if (match(descr, /^NAT-PMP \d+ \w+$/)) descr = 'NAT-PMP';
@@ -33,9 +30,9 @@ const methods = {
push(leases, {
proto: uc(record[0]),
extport: +record[1],
- intaddr: arrtoip(iptoarr(record[2 + recshift])),
- intport: +record[3 + recshift],
- expires: record[4 + recshift] - timelocal(localtime()),
+ intaddr: arrtoip(iptoarr(record[3])),
+ intport: +record[4],
+ expires: record[5] - timelocal(localtime()),
descr: descr
});
}
@@ -84,14 +81,14 @@ const methods = {
const nft = popen('nft --handle list chain inet fw4 upnp_prerouting 2>/dev/null');
if (nft) {
for (let line = nft.read('line'), num = 1; length(line); line = nft.read('line')) {
- let m = match(line, /^\t\tiif ".+" @nh,72,8 (0x6|0x11) th dport ([0-9]+).* dnat ip to ([0-9.]+):([0-9]+)/);
+ let m = match(line, /^\t\tiif ".+" ip protocol ([ut].p) .* dport ([0-9]+).* dnat ip to ([0-9.]+):([0-9]+)/);
if (m) {
push(rules, {
num: `${num}`,
intaddr: arrtoip(iptoarr(m[3])),
intport: +m[4],
extport: +m[2],
- proto: (m[1] == '0x6') ? 'TCP' : 'UDP'
+ proto: uc(m[1])
});
num++;
}
@@ -101,17 +98,14 @@ const methods = {
const nftipv6 = popen('nft --handle list chain inet fw4 upnp_forward 2>/dev/null');
if (nftipv6) {
for (let line = nftipv6.read('line'), num = 1; length(line); line = nftipv6.read('line')) {
- let m = match(line, /^\t\tiif ".+" th dport ([0-9]+) @nh,192,128 0x([0-9a-f]+) @nh,48,8 (0x6|0x11) accept/);
+ let m = match(line, /^\t\tiif ".+" th dport ([0-9]+) ip6 daddr ([0-9a-f:]+) ip6 nexthdr ([ut].p) accept/);
if (m) {
push(rules, {
num: '0',
- intaddr: arrtoip(iptoarr(join(':', [substr(m[2], 0, 4),
- substr(m[2], 4, 4), substr(m[2], 8, 4), substr(m[2], 12, 4), substr(m[2], 16, 4),
- substr(m[2], 20, 4), substr(m[2], 24, 4), substr(m[2], 28, 4)
- ]))),
+ intaddr: m[2],
intport: +m[1],
extport: +m[1],
- proto: (m[3] == '0x6') ? 'TCP' : 'UDP'
+ proto: uc(m[3])
});
num++;
}
diff --git a/luci-app-vantage/Makefile b/luci-app-vantage/Makefile
index 0fd292de3..94d96d987 100644
--- a/luci-app-vantage/Makefile
+++ b/luci-app-vantage/Makefile
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-vantage
-PKG_VERSION:=1.0.3
-PKG_RELEASE:=9
+PKG_VERSION:=1.0.4
+PKG_RELEASE:=10
PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage dashboard
diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css b/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css
index 3f5e68aea..ca0bbd347 100644
--- a/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css
+++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css
@@ -37,9 +37,18 @@
--vt-info-soft: var(--v-info-soft, rgba(31, 99, 198, .1));
--vt-font: var(--v-font, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
--vt-mono: var(--v-mono, ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, Consolas, monospace);
+ --vt-fs-micro: var(--v-fs-micro, 10.5px); /* the theme's type scale */
+ --vt-fs-sm: var(--v-fs-sm, 12px);
+ --vt-fs-base: var(--v-fs-base, 13px);
+ --vt-fs-md: var(--v-fs-md, 14px);
+ --vt-fs-lg: var(--v-fs-lg, 16px);
+ --vt-fs-xl: var(--v-fs-xl, 20px);
+ --vt-fs-2xl: var(--v-fs-2xl, 24px);
--vt-r-sm: var(--v-r-sm, 6px);
--vt-r: var(--v-r, 10px);
--vt-r-lg: var(--v-r-lg, 14px);
+ --vt-ctl: var(--v-ctl-h, 36px); /* every control: buttons, fields, chips */
+ --vt-r-ctl: var(--v-r, 6px); /* their corners, as the theme's controls */
--vt-shadow: var(--v-shadow, 0 1px 2px rgba(16, 20, 24, .05), 0 1px 1px rgba(16, 20, 24, .03));
/* derived: readable accent for text/strokes on surfaces (a lime or
@@ -76,6 +85,12 @@
--vt-sheet-shadow: -18px 0 48px rgba(12, 14, 17, .16), -2px 0 8px rgba(12, 14, 17, .06);
}
+/* touch: controls follow the pointer, not the width (the theme sets
+ --v-ctl-h the same way; this is the fallback under other themes) */
+@media (pointer: coarse) {
+ .vt-app { --vt-ctl: var(--v-ctl-h, 44px); }
+}
+
html[data-darkmode="true"] .vt-app {
--vt-bg: var(--v-bg, #0f1113);
--vt-surface: var(--v-surface, #171a1d);
@@ -121,7 +136,7 @@ html[data-darkmode="true"] .vt-app {
.vt-app {
color: var(--vt-text);
font-family: var(--vt-font);
- font-size: 13.5px;
+ font-size: var(--vt-fs-base);
line-height: 1.45;
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
@@ -154,11 +169,11 @@ html[data-darkmode="true"] .vt-app {
.vt-app .vt-mono { font-family: var(--vt-mono); font-size: .92em; letter-spacing: -.01em; }
.vt-app .vt-tn { font-variant-numeric: tabular-nums; }
.vt-app .vt-muted { color: var(--vt-text-3); }
-.vt-app .vt-hint { color: var(--vt-text-3); font-size: 12px; }
+.vt-app .vt-hint { color: var(--vt-text-3); font-size: var(--vt-fs-sm); }
.vt-app .vt-t-warn { color: var(--vt-warn); }
.vt-app .vt-t-err { color: var(--vt-err); }
-.vt-app .vt-label { display: block; color: var(--vt-text-3); font-size: 11.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
-.vt-app .vt-big { font-size: 30px; font-weight: 680; letter-spacing: -.02em; line-height: 1.05; }
+.vt-app .vt-label { display: block; color: var(--vt-text-3); font-size: var(--vt-fs-micro); font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
+.vt-app .vt-big { font-size: var(--vt-fs-2xl); font-weight: 680; letter-spacing: -.02em; line-height: 1.05; }
.vt-app .vt-ico {
width: 18px; height: 18px; flex: none;
@@ -174,10 +189,10 @@ html[data-darkmode="true"] .vt-app {
/* buttons of the app (the theme's global button look is reset here) */
.vt-app .vt-btn {
display: inline-flex; align-items: center; gap: 6px;
- height: 32px; padding: 0 12px;
- border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm);
+ height: var(--vt-ctl); padding: 0 12px;
+ border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl);
background: var(--vt-surface); color: var(--vt-text);
- font-size: 13px; font-weight: 560; line-height: 1;
+ font-size: var(--vt-fs-base); font-weight: 560; line-height: 1;
cursor: pointer; box-shadow: none; text-transform: none; min-width: 0;
transition: background .15s, border-color .15s;
}
@@ -190,7 +205,7 @@ html[data-darkmode="true"] .vt-app {
.vt-app .vt-link {
display: inline-flex; align-items: center; gap: 2px;
- color: var(--vt-accent-text); font-size: 12.5px; font-weight: 600; text-decoration: none; white-space: nowrap;
+ color: var(--vt-accent-text); font-size: var(--vt-fs-sm); font-weight: 600; text-decoration: none; white-space: nowrap;
}
.vt-app .vt-link .vt-ico { width: 14px; height: 14px; transition: transform .15s; }
.vt-app .vt-link:hover { text-decoration: none; color: var(--vt-text); }
@@ -198,7 +213,7 @@ html[data-darkmode="true"] .vt-app {
.vt-app .vt-pill {
display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
- border-radius: 999px; font-size: 10.5px; font-weight: 650; letter-spacing: .02em; white-space: nowrap;
+ border-radius: 999px; font-size: var(--vt-fs-micro); font-weight: 650; letter-spacing: .02em; white-space: nowrap;
background: var(--vt-surface-3); color: var(--vt-text-2);
}
.vt-app .vt-pill-accent { background: var(--vt-accent); color: var(--vt-accent-ink); }
@@ -206,7 +221,7 @@ html[data-darkmode="true"] .vt-app {
.vt-app .vt-band {
display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
- border-radius: 5px; font-size: 10.5px; font-weight: 700; letter-spacing: .01em; white-space: nowrap;
+ border-radius: 5px; font-size: var(--vt-fs-micro); font-weight: 700; letter-spacing: .01em; white-space: nowrap;
--vt-bc: var(--vt-b60);
background: color-mix(in oklab, var(--vt-bc) 14%, transparent);
color: color-mix(in oklab, var(--vt-bc) 78%, var(--vt-text));
@@ -215,11 +230,11 @@ html[data-darkmode="true"] .vt-app {
.vt-app .vt-band-5g { --vt-bc: var(--vt-b5); }
.vt-app .vt-band-6g { --vt-bc: var(--vt-b6); }
-.vt-app .vt-gen { font-size: 11px; color: var(--vt-text-3); white-space: nowrap; }
+.vt-app .vt-gen { font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; }
.vt-app .vt-src {
display: inline-flex; align-items: center; height: 16px; padding: 0 5px;
- border-radius: 4px; font-size: 9.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
+ border-radius: 4px; font-size: var(--vt-fs-micro); font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
background: var(--vt-surface-3); color: var(--vt-text-3); white-space: nowrap;
}
.vt-app .vt-src-alias { background: var(--vt-accent-soft); color: var(--vt-accent-text); }
@@ -263,20 +278,20 @@ html[data-darkmode="true"] .vt-app {
}
.vt-app .vt-sk-max {
position: absolute; right: 0; top: -2px; transform: translateY(-100%);
- font-size: 10px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
+ font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
}
.vt-app .vt-sk-thresh {
position: absolute; transform: translate(-100%, -120%);
- font-size: 10px; line-height: 1; color: var(--vt-warn); white-space: nowrap; pointer-events: none;
+ font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-warn); white-space: nowrap; pointer-events: none;
}
-.vt-app .vt-sk-note { position: absolute; right: 0; bottom: 4px; font-size: 10.5px; color: var(--vt-chart-axis); }
+.vt-app .vt-sk-note { position: absolute; right: 0; bottom: 4px; font-size: var(--vt-fs-micro); color: var(--vt-chart-axis); }
.vt-app .vt-spark-cpu { --vt-sc: var(--vt-chart-5); }
.vt-app .vt-spark-2g { --vt-sc: var(--vt-b2); }
.vt-app .vt-spark-5g { --vt-sc: var(--vt-b5); }
.vt-app .vt-spark-6g { --vt-sc: var(--vt-b6); }
.vt-app .vt-spark-x { --vt-sc: var(--vt-text-3); }
.vt-app .vt-spark-up { --vt-sc: var(--vt-series-up); }
-.vt-app .vt-cap { display: block; margin-top: 3px; font-size: 10px; line-height: 1.1; color: var(--vt-chart-axis); letter-spacing: .02em; white-space: nowrap; }
+.vt-app .vt-cap { display: block; margin-top: 3px; font-size: var(--vt-fs-micro); line-height: 1.1; color: var(--vt-chart-axis); letter-spacing: .02em; white-space: nowrap; }
.vt-app .vt-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* ------------------------------------------------------------ 3. layout */
@@ -299,7 +314,7 @@ html[data-darkmode="true"] .vt-app {
/* ----------------------------------------------- 3b. arranging the tiles */
-.vt-app .vt-layout-btn { flex: none; height: 30px; gap: 6px; font-size: 12px; }
+.vt-app .vt-layout-btn { flex: none; gap: 6px; font-size: var(--vt-fs-sm); }
.vt-app .vt-layout-btn .vt-ico { width: 16px; height: 16px; }
.vt-app .vt-layout-btn[aria-pressed="true"] { background: var(--vt-accent-soft); color: var(--vt-text); }
@container vt (max-width: 699px) { .vt-app .vt-layout-btn span { display: none; } }
@@ -312,7 +327,7 @@ html[data-darkmode="true"] .vt-app {
}
.vt-app .vt-editbar[hidden] { display: none; }
.vt-app .vt-editbar-text { display: flex; flex-direction: column; gap: 2px; flex: 1 1 260px; min-width: 0; }
-.vt-app .vt-editbar-text strong { font-size: 13px; font-weight: 650; }
+.vt-app .vt-editbar-text strong { font-size: var(--vt-fs-base); font-weight: 650; }
.vt-app .vt-edit-hidden { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.vt-app .vt-edit-hidden:empty { display: none; }
.vt-app .vt-edit-hidden .vt-fchip .vt-ico { width: 13px; height: 13px; }
@@ -334,11 +349,11 @@ html[data-darkmode="true"] .vt-app {
margin: 8px; padding: 6px; border-radius: var(--vt-r-sm);
background: var(--vt-surface); border: 1px solid var(--vt-line-2); box-shadow: var(--vt-shadow);
}
-.vt-app .vt-ov-name { flex: 1 1 auto; min-width: 0; font-weight: 650; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.vt-app .vt-ov-name { flex: 1 1 auto; min-width: 0; font-weight: 650; font-size: var(--vt-fs-base); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vt-app .vt-ov-btns { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
.vt-app .vt-ov-grip, .vt-app .vt-ov-btn {
display: inline-flex; align-items: center; justify-content: center; flex: none;
- width: 30px; height: 30px; min-width: 0; margin: 0; padding: 0; border: 0; border-radius: 8px;
+ width: var(--vt-ctl); height: var(--vt-ctl); min-width: 0; margin: 0; padding: 0; border: 0; border-radius: var(--vt-r-ctl);
background: transparent; color: var(--vt-text-2); box-shadow: none; cursor: pointer;
}
.vt-app .vt-ov-grip { cursor: grab; touch-action: none; background: var(--vt-surface-3); color: var(--vt-text); }
@@ -348,7 +363,7 @@ html[data-darkmode="true"] .vt-app {
.vt-app .vt-ov-grip .vt-ico { stroke-width: 3.2; }
.vt-app .vt-ov-flip .vt-ico { transform: scaleX(-1); }
.vt-app .vt-ov-hide { margin-left: 4px; }
-.vt-app .vt-ov-size { min-width: 26px; text-align: center; font-size: 11.5px; font-weight: 650; color: var(--vt-text-2); }
+.vt-app .vt-ov-size { min-width: 26px; text-align: center; font-size: var(--vt-fs-micro); font-weight: 650; color: var(--vt-text-2); }
.vt-app.vt-edit-1col .vt-ov-col { display: none; }
.vt-app.vt-editing .vt-cell { transition: top .2s ease, left .2s ease; }
@@ -371,7 +386,7 @@ html[data-darkmode="true"] .vt-app {
}
.vt-app .vt-tile-title {
display: inline-flex; align-items: center; gap: 8px; min-width: 0;
- font-size: 13px; font-weight: 650; letter-spacing: .005em; color: var(--vt-text);
+ font-size: var(--vt-fs-base); font-weight: 650; letter-spacing: .005em; color: var(--vt-text);
white-space: nowrap;
}
.vt-app .vt-tile-title .vt-ico { color: var(--vt-text-3); width: 17px; height: 17px; }
@@ -382,7 +397,7 @@ html[data-darkmode="true"] .vt-app {
@container vt (max-width: 699px) {
.vt-app .vt-fold-btn {
display: inline-flex; align-items: center; justify-content: center; flex: none;
- width: 32px; height: 32px; margin: -4px -8px -4px 0; padding: 0; border: 0; border-radius: 8px;
+ width: var(--vt-ctl); height: var(--vt-ctl); margin: -4px -8px -4px 0; padding: 0; border: 0; border-radius: var(--vt-r-ctl);
background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
}
.vt-app .vt-fold-btn:hover { background: var(--vt-surface-3); color: var(--vt-text); }
@@ -412,9 +427,9 @@ html[data-darkmode="true"] .vt-app {
}
.vt-app .vt-rsum:hover { background: var(--vt-surface-2); }
.vt-app .vt-rsum .vt-status { margin: 0; }
-.vt-app .vt-rsum-main { font-weight: 650; font-size: 13px; white-space: nowrap; }
-.vt-app .vt-rsum-sub { font-size: 12px; color: var(--vt-text-3); white-space: nowrap; }
-.vt-app .vt-rsum-rate { margin-left: auto; font-size: 12px; color: var(--vt-text-2); white-space: nowrap; }
+.vt-app .vt-rsum-main { font-weight: 650; font-size: var(--vt-fs-base); white-space: nowrap; }
+.vt-app .vt-rsum-sub { font-size: var(--vt-fs-sm); color: var(--vt-text-3); white-space: nowrap; }
+.vt-app .vt-rsum-rate { margin-left: auto; font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; }
.vt-app .vt-rsum > .vt-ico { color: var(--vt-text-3); }
.vt-app .vt-empty {
@@ -422,9 +437,9 @@ html[data-darkmode="true"] .vt-app {
padding: 32px 16px; text-align: center; color: var(--vt-text-3);
}
.vt-app .vt-empty > .vt-ico { width: 28px; height: 28px; stroke-width: 1.4; margin-bottom: 4px; }
-.vt-app .vt-empty-title { color: var(--vt-text); font-weight: 620; font-size: 14px; }
+.vt-app .vt-empty-title { color: var(--vt-text); font-weight: 620; font-size: var(--vt-fs-md); }
.vt-app .vt-empty .vt-btn { margin-top: 8px; }
-.vt-app .vt-empty-inline { color: var(--vt-text-3); font-size: 12.5px; padding: 4px 0; }
+.vt-app .vt-empty-inline { color: var(--vt-text-3); font-size: var(--vt-fs-sm); padding: 4px 0; }
/* ----------------------------------------------------- 4. head, health */
@@ -458,16 +473,16 @@ h2, h3{
.vt-app .vt-head-text { min-width: 0; flex: 1; }
.vt-app .vt-headline {
- font-size: 21px; font-weight: 680; letter-spacing: -.015em; line-height: 1.2; color: var(--vt-text);
+ font-size: var(--vt-fs-xl); font-weight: 680; letter-spacing: -.015em; line-height: 1.2; color: var(--vt-text);
margin: 0; padding: 0; border: 0; text-transform: none;
}
-.vt-app .vt-subline { color: var(--vt-text-3); font-size: 12.5px; margin-top: 2px; }
+.vt-app .vt-subline { color: var(--vt-text-3); font-size: var(--vt-fs-sm); margin-top: 2px; }
.vt-app .vt-livepill {
flex: none; display: inline-flex; align-items: center; gap: 8px;
height: 30px; padding: 0 12px 0 10px; border-radius: 999px;
border: 1px solid var(--vt-line); background: var(--vt-surface);
- font-size: 12px; font-weight: 560; color: var(--vt-text-2); white-space: nowrap;
+ font-size: var(--vt-fs-sm); font-weight: 560; color: var(--vt-text-2); white-space: nowrap;
}
.vt-app .vt-live-beat { width: 8px; height: 8px; border-radius: 50%; background: var(--vt-ok); box-shadow: 0 0 0 0 color-mix(in oklab, var(--vt-ok) 50%, transparent); animation: vt-beat 2s infinite; }
.vt-app .vt-live-paused .vt-live-beat { background: var(--vt-text-3); animation: none; }
@@ -504,10 +519,10 @@ h2, h3{
.vt-app .vt-chip-ico .vt-ico { width: 15px; height: 15px; stroke-width: 2.1; }
.vt-app .vt-chip-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.vt-app .vt-chip-top { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
-.vt-app .vt-chip-title { font-size: 11px; line-height: 1.3; font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; }
-.vt-app .vt-chip-value { font-size: 15px; font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
+.vt-app .vt-chip-title { font-size: var(--vt-fs-micro); line-height: 1.3; font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; }
+.vt-app .vt-chip-value { font-size: var(--vt-fs-md); font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.vt-app .vt-chip-reason {
- font-size: 12px; color: var(--vt-text-2); margin-top: 2px;
+ font-size: var(--vt-fs-sm); color: var(--vt-text-2); margin-top: 2px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
@@ -528,9 +543,9 @@ h2, h3{
.vt-app button.vt-kpi { cursor: pointer; }
.vt-app button.vt-kpi:hover { background: var(--vt-surface-2); }
.vt-app .vt-kpi + .vt-kpi { box-shadow: -1px 0 0 var(--vt-line); border-radius: 0 var(--vt-r-sm) var(--vt-r-sm) 0; }
-.vt-app .vt-kpi-label { font-size: 11px; font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; }
-.vt-app .vt-kpi-value { font-size: 22px; font-weight: 700; letter-spacing: -.02em; line-height: 1.2; white-space: nowrap; }
-.vt-app .vt-kpi-sub { font-size: 12px; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.vt-app .vt-kpi-label { font-size: var(--vt-fs-micro); font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; }
+.vt-app .vt-kpi-value { font-size: var(--vt-fs-xl); font-weight: 700; letter-spacing: -.02em; line-height: 1.2; white-space: nowrap; }
+.vt-app .vt-kpi-sub { font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vt-app .vt-kpi-warn .vt-kpi-value { color: var(--vt-warn); }
@container vthero (max-width: 700px) {
.vt-app .vt-kpis { grid-template-columns: 1fr 1fr; }
@@ -577,9 +592,9 @@ h2, h3{
}
.vt-app .vt-node:hover { border-color: color-mix(in oklab, var(--vt-accent-line) 60%, var(--vt-line)); transform: translateY(-1px); box-shadow: 0 6px 16px rgba(12, 14, 17, .08); background: var(--vt-surface); }
.vt-app .vt-node-body { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
-.vt-app .vt-node-kicker { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; }
-.vt-app .vt-node-title { font-size: 13.5px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.vt-app .vt-node-sub { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.vt-app .vt-node-kicker { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; }
+.vt-app .vt-node-title { font-size: var(--vt-fs-base); font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.vt-app .vt-node-sub { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vt-app .vt-node-ico {
flex: none; width: 32px; height: 32px; border-radius: 9px;
display: inline-flex; align-items: center; justify-content: center;
@@ -589,12 +604,12 @@ h2, h3{
.vt-app .vt-node-gw { justify-self: stretch; align-self: center; }
.vt-app .vt-node-ap { justify-self: stretch; align-self: center; padding: 12px 13px; border-color: color-mix(in oklab, var(--vt-accent-line) 40%, var(--vt-line)); }
.vt-app .vt-node-ap .vt-node-ico { background: var(--vt-accent); color: var(--vt-accent-ink); }
-.vt-app .vt-node-ap .vt-node-title { font-size: 14.5px; }
+.vt-app .vt-node-ap .vt-node-title { font-size: var(--vt-fs-md); }
.vt-app .vt-node-gen { text-transform: none; letter-spacing: 0; font-weight: 560; }
.vt-app .vt-node-radio { padding: 8px 11px; }
.vt-app .vt-node-net, .vt-app .vt-node-net:hover { text-decoration: none; color: var(--vt-text); }
.vt-app .vt-node-ssid { padding: 7px 10px; }
-.vt-app .vt-node-ssid .vt-node-title { display: flex; align-items: center; gap: 6px; font-size: 13px; }
+.vt-app .vt-node-ssid .vt-node-title { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-base); }
.vt-app .vt-node-ssid .vt-node-title .vt-ico { color: var(--vt-text-3); }
.vt-app .vt-ssid-name span { overflow: hidden; text-overflow: ellipsis; }
.vt-app .vt-node-idle { background: var(--vt-surface-2); box-shadow: none; }
@@ -618,14 +633,14 @@ h2, h3{
padding: 0 6px; border-radius: 6px; background: var(--vt-surface);
}
.vt-app .vt-link-rate .vt-ico { width: 13px; height: 13px; align-self: center; stroke-width: 2.2; }
-.vt-app .vt-link-rate b { font-size: 17px; font-weight: 700; letter-spacing: -.01em; }
-.vt-app .vt-link-rate small { font-size: 11px; color: var(--vt-text-3); font-weight: 600; }
+.vt-app .vt-link-rate b { font-size: var(--vt-fs-lg); font-weight: 700; letter-spacing: -.01em; }
+.vt-app .vt-link-rate small { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 600; }
.vt-app .vt-link-down { grid-row: 1; align-self: end; }
.vt-app .vt-link-down .vt-ico { color: var(--vt-accent-text); }
-.vt-app .vt-link-up { font-size: 12px; }
-.vt-app .vt-link-up b { font-size: 13px; font-weight: 650; color: var(--vt-text-2); }
+.vt-app .vt-link-up { font-size: var(--vt-fs-sm); }
+.vt-app .vt-link-up b { font-size: var(--vt-fs-base); font-weight: 650; color: var(--vt-text-2); }
.vt-app .vt-link-up .vt-ico { color: var(--vt-text-3); }
-.vt-app .vt-link-meta { font-size: 11px; color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px; }
+.vt-app .vt-link-meta { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px; }
/* the branches spread over the height the tile gets from its row, so a
tall neighbour leaves no empty band above or below the diagram */
@@ -664,8 +679,8 @@ h2, h3{
.vt-app .vt-sys-mem { display: flex; flex-direction: column; gap: 7px; }
.vt-app .vt-mem-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
-.vt-app .vt-mem-avail { font-size: 17px; font-weight: 680; }
-.vt-app .vt-mem-of { font-size: 12px; color: var(--vt-text-3); }
+.vt-app .vt-mem-avail { font-size: var(--vt-fs-lg); font-weight: 680; }
+.vt-app .vt-mem-of { font-size: var(--vt-fs-sm); color: var(--vt-text-3); }
.vt-app .vt-seg { display: flex; height: 12px; border-radius: 6px; overflow: hidden; background: var(--vt-surface-3); gap: 2px; }
.vt-app .vt-seg-part { height: 100%; min-width: 0; transition: width .7s cubic-bezier(.2, .7, .2, 1); }
.vt-app .vt-seg-used { background: var(--vt-mem); border-radius: 6px 2px 2px 6px; }
@@ -675,24 +690,24 @@ h2, h3{
border-radius: 2px;
}
.vt-app .vt-seg-free { background: transparent; }
-.vt-app .vt-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--vt-text-2); }
+.vt-app .vt-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: var(--vt-fs-sm); color: var(--vt-text-2); }
.vt-app .vt-legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.vt-app .vt-legend-item b { font-weight: 650; color: var(--vt-text); }
.vt-app .vt-key { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.vt-app .vt-key.vt-seg-free { background: var(--vt-surface-3); box-shadow: inset 0 0 0 1px var(--vt-line-2); }
-.vt-app .vt-swap { display: flex; align-items: center; gap: 8px; font-size: 12px; }
+.vt-app .vt-swap { display: flex; align-items: center; gap: 8px; font-size: var(--vt-fs-sm); }
.vt-app .vt-swap:empty { display: none; }
.vt-app .vt-thin { flex: 1; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; }
.vt-app .vt-thin-fill { display: block; height: 100%; background: var(--vt-mem); }
.vt-app .vt-sys-store { display: flex; flex-direction: column; gap: 8px; }
.vt-app .vt-storage { display: flex; flex-direction: column; gap: 8px; }
-.vt-app .vt-mount-top { display: flex; justify-content: space-between; gap: 8px; font-size: 12.5px; margin-bottom: 4px; }
+.vt-app .vt-mount-top { display: flex; justify-content: space-between; gap: 8px; font-size: var(--vt-fs-sm); margin-bottom: 4px; }
.vt-app .vt-bar { height: 6px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; }
.vt-app .vt-bar-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-mem); transition: width .7s ease; }
.vt-app .vt-bg-warn { background: var(--vt-warn); }
.vt-app .vt-bg-err { background: var(--vt-err); }
-.vt-app .vt-fw { display: flex; flex-direction: column; gap: 1px; margin-top: auto; padding-top: 10px; border-top: 1px dashed var(--vt-line); font-size: 12px; color: var(--vt-text-2); }
+.vt-app .vt-fw { display: flex; flex-direction: column; gap: 1px; margin-top: auto; padding-top: 10px; border-top: 1px dashed var(--vt-line); font-size: var(--vt-fs-sm); color: var(--vt-text-2); }
/* ------------------------------------------------------------- 7. chart */
@@ -700,7 +715,7 @@ h2, h3{
a shared time axis and one crosshair across all rows */
.vt-app .vt-chart { display: flex; flex-direction: column; --vt-mh: 136px; --vt-mg: 16px; }
.vt-app .vt-chart .vt-tile-title { flex-wrap: wrap; row-gap: 0; }
-.vt-app .vt-chart-sub { font-size: 12px; font-weight: 500; color: var(--vt-text-3); }
+.vt-app .vt-chart-sub { font-size: var(--vt-fs-sm); font-weight: 500; color: var(--vt-text-3); }
.vt-app .vt-chart-sub::before { content: "·"; margin: 0 6px 0 0; }
.vt-app .vt-lg-key { width: 12px; height: 3px; border-radius: 2px; background: var(--vt-sc); flex: none; }
/* one colour per direction in every row (series, gradients, dots, keys) */
@@ -709,7 +724,7 @@ h2, h3{
.vt-app .vt-arr { font-weight: 700; }
.vt-app .vt-arr-dn { color: color-mix(in oklab, var(--vt-dir-dn) 70%, var(--vt-text)); }
.vt-app .vt-arr-up { color: color-mix(in oklab, var(--vt-dir-up) 70%, var(--vt-text)); }
-.vt-app .vt-dir-legend { font-size: 11.5px; gap: 4px 12px; }
+.vt-app .vt-dir-legend { font-size: var(--vt-fs-micro); gap: 4px 12px; }
.vt-app .vt-mult-wrap { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 300px; border-radius: var(--vt-r-sm); outline-offset: 4px; }
.vt-app .vt-mults { flex: 1; display: grid; grid-auto-rows: minmax(100px, 1fr); }
@@ -719,11 +734,11 @@ h2, h3{
}
.vt-app .vt-mult:first-child { border-top: 0; padding-top: 2px; }
.vt-app .vt-mult-head { display: flex; flex-direction: column; justify-content: center; gap: 1px; min-width: 0; }
-.vt-app .vt-mult-name { font-size: 11.5px; font-weight: 600; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.vt-app .vt-mult-now { display: flex; align-items: center; gap: 6px; font-size: 17px; font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; }
+.vt-app .vt-mult-name { font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.vt-app .vt-mult-now { display: flex; align-items: center; gap: 6px; font-size: var(--vt-fs-lg); font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; }
.vt-app .vt-mult-now b { font-weight: inherit; }
-.vt-app .vt-mult-now-up { font-size: 13px; font-weight: 650; line-height: 1.3; color: var(--vt-text-2); }
-.vt-app .vt-mult-stats { display: flex; flex-wrap: wrap; gap: 0 8px; font-size: 11px; color: var(--vt-text-3); white-space: nowrap; }
+.vt-app .vt-mult-now-up { font-size: var(--vt-fs-base); font-weight: 650; line-height: 1.3; color: var(--vt-text-2); }
+.vt-app .vt-mult-stats { display: flex; flex-wrap: wrap; gap: 0 8px; font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; }
.vt-app .vt-mult-stats b { font-weight: 600; color: var(--vt-text-2); }
.vt-app .vt-mult-plot { position: relative; min-height: 72px; margin: 12px 0; --vt-sc: inherit; }
.vt-app .vt-mult-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
@@ -734,7 +749,7 @@ h2, h3{
.vt-app .vt-chart-mid { fill: none; stroke: color-mix(in oklab, var(--vt-chart-axis) 55%, transparent); stroke-width: 1; vector-effect: non-scaling-stroke; }
.vt-app .vt-mult-max {
position: absolute; right: 0; top: -3px; transform: translateY(-100%);
- font-size: 10.5px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
+ font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none;
}
.vt-app .vt-mult-max-up { top: auto; bottom: -3px; transform: translateY(100%); }
.vt-app .vt-mult-dot {
@@ -743,7 +758,7 @@ h2, h3{
}
.vt-app .vt-mult-over { position: absolute; top: 0; bottom: 20px; left: calc(var(--vt-mh) + var(--vt-mg)); right: 0; pointer-events: none; z-index: 3; }
.vt-app .vt-mult-x { position: relative; height: 20px; margin-left: calc(var(--vt-mh) + var(--vt-mg)); }
-.vt-app .vt-mx { position: absolute; top: 5px; font-size: 10.5px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; transform: translateX(-50%); }
+.vt-app .vt-mx { position: absolute; top: 5px; font-size: var(--vt-fs-micro); line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; transform: translateX(-50%); }
.vt-app .vt-mx-0 { left: 0; transform: none; }
.vt-app .vt-mx-1 { right: 0; transform: none; }
.vt-app .vt-mult-wrap { cursor: crosshair; touch-action: pan-y; }
@@ -753,11 +768,11 @@ h2, h3{
width: max-content; min-width: 180px; padding: 8px 10px 7px; border-radius: var(--vt-r);
background: var(--vt-surface); border: 1px solid var(--vt-line-2);
box-shadow: 0 10px 28px rgba(12, 14, 17, .16), 0 2px 6px rgba(12, 14, 17, .06);
- font-size: 12px; pointer-events: none;
+ font-size: var(--vt-fs-sm); pointer-events: none;
}
-.vt-app .vt-tip-time { display: flex; justify-content: space-between; gap: 12px; color: var(--vt-text-3); font-size: 11px; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid var(--vt-line); }
+.vt-app .vt-tip-time { display: flex; justify-content: space-between; gap: 12px; color: var(--vt-text-3); font-size: var(--vt-fs-micro); margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid var(--vt-line); }
.vt-app .vt-tip-link + .vt-tip-link { margin-top: 5px; }
-.vt-app .vt-tip-label { color: var(--vt-text-3); font-size: 11px; white-space: nowrap; }
+.vt-app .vt-tip-label { color: var(--vt-text-3); font-size: var(--vt-fs-micro); white-space: nowrap; }
.vt-app .vt-tip-row { display: grid; grid-template-columns: 12px auto minmax(0, 1fr); align-items: center; column-gap: 8px; line-height: 1.6; color: var(--vt-text-2); }
.vt-app .vt-tip-row b { font-weight: 700; color: var(--vt-text); white-space: nowrap; text-align: right; }
@@ -772,36 +787,36 @@ h2, h3{
.vt-app .vt-radio::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--vt-bc); opacity: .85; }
.vt-app .vt-radio-off { opacity: .75; }
.vt-app .vt-radio-head { display: flex; align-items: center; gap: 8px; }
-.vt-app .vt-radio-name { font-weight: 650; font-size: 13px; }
-.vt-app .vt-status { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; color: var(--vt-text-2); }
+.vt-app .vt-radio-name { font-weight: 650; font-size: var(--vt-fs-base); }
+.vt-app .vt-status { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-2); }
.vt-app .vt-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--vt-ok); }
.vt-app .vt-status-warn::before { background: var(--vt-warn); }
.vt-app .vt-status-err::before { background: var(--vt-err); }
.vt-app .vt-icon-action {
display: inline-flex; align-items: center; justify-content: center; flex: none;
- width: 26px; height: 26px; padding: 0; margin: 0; border: 0; border-radius: 7px;
+ width: var(--vt-ctl); height: var(--vt-ctl); padding: 0; margin: 0; border: 0; border-radius: var(--vt-r-ctl);
background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
}
.vt-app .vt-icon-action:hover { background: var(--vt-surface-3); color: var(--vt-text); }
.vt-app .vt-radio-chan { display: flex; align-items: baseline; gap: 8px; }
-.vt-app .vt-radio-width { font-size: 14px; font-weight: 600; color: var(--vt-text-2); }
+.vt-app .vt-radio-width { font-size: var(--vt-fs-md); font-weight: 600; color: var(--vt-text-2); }
.vt-app .vt-spectrum { position: relative; height: 8px; border-radius: 4px; background: var(--vt-surface-3); margin-bottom: 12px; }
.vt-app .vt-spectrum-blk { position: absolute; top: -2px; bottom: -2px; border-radius: 3px; background: var(--vt-bc); box-shadow: 0 0 0 2px var(--vt-surface); }
-.vt-app .vt-spectrum-lo, .vt-app .vt-spectrum-hi { position: absolute; top: 11px; font-size: 10px; color: var(--vt-text-3); }
+.vt-app .vt-spectrum-lo, .vt-app .vt-spectrum-hi { position: absolute; top: 11px; font-size: var(--vt-fs-micro); color: var(--vt-text-3); }
.vt-app .vt-spectrum-lo { left: 0; }
.vt-app .vt-spectrum-hi { right: 0; }
.vt-app .vt-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; }
-.vt-app .vt-kv dt { font-size: 11px; color: var(--vt-text-3); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
-.vt-app .vt-kv dd { font-size: 13.5px; font-weight: 600; margin: 1px 0 0; }
-.vt-app .vt-kv dd .vt-muted { font-weight: 500; font-size: 12.5px; }
+.vt-app .vt-kv dt { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
+.vt-app .vt-kv dd { font-size: var(--vt-fs-base); font-weight: 600; margin: 1px 0 0; }
+.vt-app .vt-kv dd .vt-muted { font-weight: 500; font-size: var(--vt-fs-sm); }
.vt-app .vt-radio-live { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; align-items: center; padding: 8px 10px; border-radius: var(--vt-r-sm); background: var(--vt-surface-2); }
.vt-app .vt-radio-live .vt-spark { height: 30px; }
.vt-app .vt-rates { display: flex; flex-direction: column; gap: 1px; }
-.vt-app .vt-rate { display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; font-weight: 650; white-space: nowrap; }
+.vt-app .vt-rate { display: inline-flex; align-items: center; gap: 4px; font-size: var(--vt-fs-sm); font-weight: 650; white-space: nowrap; }
.vt-app .vt-rate .vt-ico { width: 12px; height: 12px; stroke-width: 2.3; color: var(--vt-text-3); }
.vt-app .vt-rate-up { font-weight: 500; color: var(--vt-text-2); }
.vt-app .vt-notes { display: flex; flex-direction: column; gap: 4px; }
-.vt-app .vt-note { display: flex; align-items: flex-start; gap: 6px; font-size: 12px; color: var(--vt-text-2); }
+.vt-app .vt-note { display: flex; align-items: flex-start; gap: 6px; font-size: var(--vt-fs-sm); color: var(--vt-text-2); }
.vt-app .vt-note .vt-ico { margin-top: 2px; color: var(--vt-text-3); }
.vt-app .vt-note-warn .vt-ico { color: var(--vt-warn); }
.vt-app .vt-note-err .vt-ico { color: var(--vt-err); }
@@ -815,38 +830,38 @@ h2, h3{
/* ----------------------------------------------------------- 9. clients */
.vt-app .vt-clients { padding-bottom: 8px; }
-.vt-app .vt-count { font-size: 12px; color: var(--vt-text-3); font-weight: 560; }
+.vt-app .vt-count { font-size: var(--vt-fs-sm); color: var(--vt-text-3); font-weight: 560; }
.vt-app .vt-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 10px; }
.vt-app .vt-filter {
position: relative; display: flex; align-items: center; flex: 1 1 260px; max-width: 380px;
- height: 34px; padding: 0 8px 0 10px; gap: 6px;
- border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm); background: var(--vt-surface);
+ height: var(--vt-ctl); padding: 0 8px 0 10px; gap: 6px;
+ border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl); background: var(--vt-surface);
color: var(--vt-text-3); cursor: text;
}
.vt-app .vt-filter:focus-within { border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
.vt-app .vt-filter .vt-ico { width: 16px; height: 16px; }
.vt-app .vt-filter .vt-filter-input {
- flex: 1; min-width: 0; width: auto; height: 32px; padding: 0; margin: 0;
+ flex: 1; min-width: 0; width: auto; height: calc(var(--vt-ctl) - 2px); padding: 0; margin: 0;
border: 0; background: transparent; box-shadow: none; outline: none;
- color: var(--vt-text); font: inherit; font-size: 13px;
+ color: var(--vt-text); font: inherit; font-size: var(--vt-fs-base);
}
.vt-app .vt-filter .vt-filter-input::placeholder { color: var(--vt-text-3); opacity: 1; }
-.vt-app .vt-key-hint { font: 600 10.5px/1 var(--vt-mono); padding: 3px 5px; border-radius: 4px; border: 1px solid var(--vt-line-2); color: var(--vt-text-3); background: var(--vt-surface-2); }
+.vt-app .vt-key-hint { font: 600 var(--vt-fs-micro)/1 var(--vt-mono); padding: 3px 5px; border-radius: 4px; border: 1px solid var(--vt-line-2); color: var(--vt-text-3); background: var(--vt-surface-2); }
.vt-app .vt-filter:focus-within .vt-key-hint { display: none; }
.vt-app .vt-filter-clear {
- display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; margin: 0;
- border: 0; border-radius: 5px; background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
+ display: inline-flex; align-items: center; justify-content: center; width: calc(var(--vt-ctl) - 10px); height: calc(var(--vt-ctl) - 10px); padding: 0; margin: 0;
+ border: 0; border-radius: var(--vt-r-ctl); background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0;
}
.vt-app .vt-filter-clear:hover { background: var(--vt-surface-3); color: var(--vt-text); }
.vt-app .vt-filter-clear .vt-ico { width: 14px; height: 14px; }
.vt-app .vt-fchips { display: flex; flex-wrap: wrap; gap: 6px; }
.vt-app .vt-fchip {
- display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; margin: 0;
+ display: inline-flex; align-items: center; gap: 6px; height: var(--vt-ctl); padding: 0 12px; margin: 0;
border: 1px solid var(--vt-line); border-radius: 999px; background: var(--vt-surface);
- color: var(--vt-text-2); font-size: 12px; font-weight: 600; cursor: pointer; box-shadow: none; min-width: 0; text-transform: none;
+ color: var(--vt-text-2); font-size: var(--vt-fs-sm); font-weight: 600; cursor: pointer; box-shadow: none; min-width: 0; text-transform: none;
}
.vt-app .vt-fchip:hover { border-color: var(--vt-line-2); color: var(--vt-text); background: var(--vt-surface-2); }
-.vt-app .vt-fchip-n { font-size: 11px; color: var(--vt-text-3); font-weight: 600; }
+.vt-app .vt-fchip-n { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 600; }
.vt-app .vt-fchip-warn .vt-fchip-n { color: var(--vt-warn); }
.vt-app .vt-fchip.vt-fchip-on { background: var(--vt-text); border-color: var(--vt-text); color: var(--vt-surface); }
.vt-app .vt-fchip.vt-fchip-on .vt-fchip-n { color: color-mix(in oklab, var(--vt-surface) 70%, transparent); }
@@ -855,18 +870,18 @@ h2, h3{
.vt-app .vt-ctable { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: auto; min-width: 800px; }
.vt-app .vt-ctable th {
position: sticky; top: 0; z-index: 1;
- padding: 0 10px; height: 34px; text-align: left; white-space: nowrap; letter-spacing: normal; text-transform: none;
+ padding: 0 10px; height: var(--vt-ctl); text-align: left; white-space: nowrap; letter-spacing: normal; text-transform: none;
background: var(--vt-surface); border-bottom: 1px solid var(--vt-line);
font-weight: 400;
}
.vt-app .vt-sort {
- display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 4px; margin: 0 -4px;
- border: 0; border-radius: 5px; background: transparent; box-shadow: none; min-width: 0;
- color: var(--vt-text-3); font-size: 11px; font-weight: 650; letter-spacing: .05em; text-transform: uppercase; cursor: pointer;
+ display: inline-flex; align-items: center; gap: 4px; height: var(--vt-ctl); padding: 0 4px; margin: 0 -4px;
+ border: 0; border-radius: var(--vt-r-ctl); background: transparent; box-shadow: none; min-width: 0;
+ color: var(--vt-text-3); font-size: var(--vt-fs-micro); font-weight: 650; letter-spacing: .05em; text-transform: uppercase; cursor: pointer;
}
.vt-app .vt-sort:hover { color: var(--vt-text); background: var(--vt-surface-2); }
.vt-app .vt-sort-on { color: var(--vt-text); }
-.vt-app .vt-sort-ind { font-size: 11px; width: 8px; }
+.vt-app .vt-sort-ind { font-size: var(--vt-fs-micro); width: 8px; }
.vt-app .vt-ctable td {
height: auto; line-height: 1.4; overflow-wrap: normal; text-align: left;
padding: 9px 8px; vertical-align: middle;
@@ -888,36 +903,36 @@ h2, h3{
.vt-app .vt-who-text { min-width: 0; }
/* names get two lines before they are cut; the full name is the title */
.vt-app .vt-who-label {
- font-weight: 640; font-size: 13.5px; line-height: 1.3; max-width: 280px; overflow-wrap: anywhere;
+ font-weight: 640; font-size: var(--vt-fs-base); line-height: 1.3; max-width: 280px; overflow-wrap: anywhere;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
-.vt-app .vt-who-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 6px; margin-top: 3px; color: var(--vt-text-3); font-size: 11.5px; }
+.vt-app .vt-who-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 6px; margin-top: 3px; color: var(--vt-text-3); font-size: var(--vt-fs-micro); }
.vt-app .vt-who-id { white-space: nowrap; }
.vt-app .vt-td-name { min-width: 250px; }
.vt-app .vt-net { display: flex; flex-direction: column; gap: 3px; }
.vt-app .vt-net-ssid { font-weight: 560; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
.vt-app .vt-net-badges { display: flex; align-items: center; gap: 6px; }
-.vt-app .vt-phy { display: flex; align-items: center; gap: 4px; font-size: 12.5px; white-space: nowrap; }
+.vt-app .vt-phy { display: flex; align-items: center; gap: 4px; font-size: var(--vt-fs-sm); white-space: nowrap; }
.vt-app .vt-phy b { font-weight: 620; }
.vt-app .vt-phy .vt-ico { color: var(--vt-text-3); stroke-width: 2.2; width: 12px; height: 12px; }
-.vt-app .vt-phy-up { color: var(--vt-text-3); font-size: 11.5px; padding-left: 16px; }
+.vt-app .vt-phy-up { color: var(--vt-text-3); font-size: var(--vt-fs-micro); padding-left: 16px; }
.vt-app .vt-live-cell { display: grid; grid-template-columns: auto; gap: 3px; min-width: 96px; }
.vt-app .vt-live-total { font-weight: 650; }
.vt-app .vt-mini { display: block; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; }
.vt-app .vt-mini-fill { display: block; height: 100%; background: var(--vt-info); border-radius: 2px; transition: width .6s ease; }
-.vt-app .vt-live-split { font-size: 11px; color: var(--vt-text-3); white-space: nowrap; }
+.vt-app .vt-live-split { font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; }
.vt-app .vt-exp { display: flex; align-items: center; gap: 9px; min-width: 0; }
.vt-app .vt-score {
flex: none; display: inline-flex; align-items: center; justify-content: center;
- width: 32px; height: 26px; border-radius: 7px; font-size: 12.5px; font-weight: 700;
+ width: 32px; height: 26px; border-radius: 7px; font-size: var(--vt-fs-sm); font-weight: 700;
--vt-xc: var(--vt-ok); background: color-mix(in oklab, var(--vt-xc) 15%, transparent); color: color-mix(in oklab, var(--vt-xc) 85%, var(--vt-text));
}
.vt-app .vt-score-warn { --vt-xc: var(--vt-warn); }
.vt-app .vt-score-err { --vt-xc: var(--vt-err); }
.vt-app .vt-exp-text { display: flex; flex-direction: column; min-width: 0; }
-.vt-app .vt-exp-text b { font-size: 12.5px; font-weight: 650; }
+.vt-app .vt-exp-text b { font-size: var(--vt-fs-sm); font-weight: 650; }
.vt-app .vt-exp-text span {
- font-size: 11.5px; line-height: 1.35; color: var(--vt-text-3); max-width: 170px;
+ font-size: var(--vt-fs-micro); line-height: 1.35; color: var(--vt-text-3); max-width: 170px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.vt-app .vt-td-time { white-space: nowrap; color: var(--vt-text-2); }
@@ -935,15 +950,15 @@ h2, h3{
text-align: left; cursor: pointer; color: var(--vt-text); height: auto; min-width: 0; text-transform: none; font-weight: 400;
}
.vt-app .vt-talk-row:hover, .vt-app .vt-ssid:hover, .vt-app .vt-dlist-row:hover { background: var(--vt-surface-2); }
-.vt-app .vt-talk-rank { width: 14px; font-size: 12px; color: var(--vt-text-3); font-weight: 650; text-align: center; flex: none; }
+.vt-app .vt-talk-rank { width: 14px; font-size: var(--vt-fs-sm); color: var(--vt-text-3); font-weight: 650; text-align: center; flex: none; }
.vt-app .vt-talk-main { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.vt-app .vt-talk-top { display: flex; justify-content: space-between; gap: 10px; }
.vt-app .vt-talk-name { font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
-.vt-app .vt-talk-share { font-size: 12px; font-weight: 650; color: var(--vt-text-2); white-space: nowrap; }
+.vt-app .vt-talk-share { font-size: var(--vt-fs-sm); font-weight: 650; color: var(--vt-text-2); white-space: nowrap; }
.vt-app .vt-talk-sub b { color: var(--vt-text); font-weight: 650; }
.vt-app .vt-talk-bar { height: 5px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; }
.vt-app .vt-talk-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-info); transition: width .6s ease; }
-.vt-app .vt-talk-sub { display: flex; gap: 8px; align-items: baseline; font-size: 11px; color: var(--vt-text-3); white-space: nowrap; overflow: hidden; }
+.vt-app .vt-talk-sub { display: flex; gap: 8px; align-items: baseline; font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; overflow: hidden; }
.vt-app .vt-ssids { display: flex; flex-direction: column; gap: 2px; }
.vt-app .vt-ssid-ico { flex: none; width: 30px; height: 30px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: var(--vt-surface-3); color: var(--vt-text-2); }
@@ -953,9 +968,9 @@ h2, h3{
.vt-app .vt-ssid-top { display: flex; align-items: center; gap: 6px; min-width: 0; }
.vt-app .vt-ssid-name { font-weight: 640; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vt-app .vt-ssid-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
-.vt-app .vt-sec { font-size: 11px; font-weight: 600; color: var(--vt-text-2); }
+.vt-app .vt-sec { font-size: var(--vt-fs-micro); font-weight: 600; color: var(--vt-text-2); }
.vt-app .vt-ssid-side { display: flex; flex-direction: column; align-items: flex-end; flex: none; }
-.vt-app .vt-ssid-n { font-size: 17px; font-weight: 700; line-height: 1.1; }
+.vt-app .vt-ssid-n { font-size: var(--vt-fs-lg); font-weight: 700; line-height: 1.1; }
.vt-app .vt-ssid-idle .vt-ssid-n { color: var(--vt-text-3); }
/* ------------------------------------------------------------ 11. drawer */
@@ -988,11 +1003,11 @@ h2, h3{
.vt-app .vt-drawer-ico { width: 44px; height: 44px; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; background: var(--vt-surface-3); color: var(--vt-text-2); flex: none; }
.vt-app .vt-drawer-ico .vt-ico { width: 22px; height: 22px; }
.vt-app .vt-drawer-titles { min-width: 0; }
-.vt-app .vt-drawer-name { font-size: 17px; font-weight: 680; letter-spacing: -.01em; line-height: 1.25; overflow-wrap: anywhere; }
-.vt-app .vt-drawer-sub { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; color: var(--vt-text-3); font-size: 12px; margin-top: 3px; }
+.vt-app .vt-drawer-name { font-size: var(--vt-fs-lg); font-weight: 680; letter-spacing: -.01em; line-height: 1.25; overflow-wrap: anywhere; }
+.vt-app .vt-drawer-sub { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; color: var(--vt-text-3); font-size: var(--vt-fs-sm); margin-top: 3px; }
.vt-app .vt-drawer-close {
display: inline-flex; align-items: center; justify-content: center; flex: none;
- width: 34px; height: 34px; padding: 0; margin: 0; border: 0; border-radius: 9px;
+ width: var(--vt-ctl); height: var(--vt-ctl); padding: 0; margin: 0; border: 0; border-radius: var(--vt-r-ctl);
background: transparent; color: var(--vt-text-2); cursor: pointer; box-shadow: none; min-width: 0;
}
.vt-app .vt-drawer-close:hover { background: var(--vt-surface-3); color: var(--vt-text); }
@@ -1000,7 +1015,7 @@ h2, h3{
.vt-app .vt-dactions { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.vt-app .vt-dactions:has(> .vt-link:only-child) { justify-content: flex-end; }
.vt-app .vt-rename-slot:empty { display: none; }
-.vt-app .vt-banner { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: var(--vt-r-sm); font-size: 12.5px; }
+.vt-app .vt-banner { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: var(--vt-r-sm); font-size: var(--vt-fs-sm); }
.vt-app .vt-banner-muted { background: var(--vt-surface-3); color: var(--vt-text-2); }
.vt-app .vt-expcard {
@@ -1015,52 +1030,52 @@ h2, h3{
.vt-app .vt-ring-svg { width: 58px; height: 58px; transform: rotate(-90deg); }
.vt-app .vt-ring-track { fill: none; stroke: color-mix(in oklab, var(--vt-xc) 18%, transparent); stroke-width: 3.5; }
.vt-app .vt-ring-arc { fill: none; stroke: var(--vt-xc); stroke-width: 3.5; stroke-linecap: round; }
-.vt-app .vt-ring-n { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 17px; font-weight: 720; }
-.vt-app .vt-expcard-grade { font-weight: 680; font-size: 14px; }
-.vt-app .vt-expcard-reason { font-size: 12.5px; color: var(--vt-text-2); margin-top: 1px; }
-.vt-app .vt-factors { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; font-size: 11.5px; color: var(--vt-text-3); }
+.vt-app .vt-ring-n { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: var(--vt-fs-lg); font-weight: 720; }
+.vt-app .vt-expcard-grade { font-weight: 680; font-size: var(--vt-fs-md); }
+.vt-app .vt-expcard-reason { font-size: var(--vt-fs-sm); color: var(--vt-text-2); margin-top: 1px; }
+.vt-app .vt-factors { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; font-size: var(--vt-fs-micro); color: var(--vt-text-3); }
.vt-app .vt-factors li { display: flex; gap: 6px; }
.vt-app .vt-factors li .vt-tn { width: 26px; flex: none; color: var(--vt-text-2); font-weight: 650; }
.vt-app .vt-dgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.vt-app .vt-dcard { display: flex; flex-direction: column; gap: 1px; padding: 12px; border-radius: var(--vt-r); background: var(--vt-surface-2); border: 1px solid var(--vt-line); min-width: 0; }
-.vt-app .vt-dcard-label { font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: .05em; color: var(--vt-text-3); }
-.vt-app .vt-dcard-value { font-size: 20px; font-weight: 700; letter-spacing: -.01em; white-space: nowrap; }
-.vt-app .vt-dcard-sub { font-size: 11.5px; line-height: 1.35; color: var(--vt-text-3); min-height: 16px; }
+.vt-app .vt-dcard-label { font-size: var(--vt-fs-micro); font-weight: 650; text-transform: uppercase; letter-spacing: .05em; color: var(--vt-text-3); }
+.vt-app .vt-dcard-value { font-size: var(--vt-fs-xl); font-weight: 700; letter-spacing: -.01em; white-space: nowrap; }
+.vt-app .vt-dcard-sub { font-size: var(--vt-fs-micro); line-height: 1.35; color: var(--vt-text-3); min-height: 16px; }
.vt-app .vt-dcard .vt-sk { margin-top: auto; padding-top: 18px; }
.vt-app .vt-dcard .vt-sk-max { top: 16px; }
.vt-app .vt-dcard .vt-spark { height: 36px; }
.vt-app .vt-dspark { padding-top: 14px; }
.vt-app .vt-dspark .vt-spark { height: 48px; }
-.vt-app .vt-dsec-title { font-size: 11.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .06em; color: var(--vt-text-3); margin: 0 0 6px; padding: 0; border: 0; }
+.vt-app .vt-dsec-title { font-size: var(--vt-fs-micro); font-weight: 650; text-transform: uppercase; letter-spacing: .06em; color: var(--vt-text-3); margin: 0 0 6px; padding: 0; border: 0; }
.vt-app .vt-kvs { display: flex; flex-direction: column; }
.vt-app .vt-kvs .vt-kv { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--vt-line); }
.vt-app .vt-kvs .vt-kv:last-child { border-bottom: 0; }
-.vt-app .vt-kvs .vt-kv dt { font-size: 12px; text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--vt-text-3); }
-.vt-app .vt-kvs .vt-kv dd { font-size: 13px; font-weight: 520; margin: 0; white-space: pre-line; overflow-wrap: anywhere; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
-.vt-app .vt-kvs .vt-kv dd small { font-size: 11.5px; color: var(--vt-text-3); font-weight: 400; }
+.vt-app .vt-kvs .vt-kv dt { font-size: var(--vt-fs-sm); text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--vt-text-3); }
+.vt-app .vt-kvs .vt-kv dd { font-size: var(--vt-fs-base); font-weight: 520; margin: 0; white-space: pre-line; overflow-wrap: anywhere; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
+.vt-app .vt-kvs .vt-kv dd small { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 400; }
.vt-app .vt-dlist { display: flex; flex-direction: column; }
.vt-app .vt-dlist-name { flex: 1; min-width: 0; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vt-app .vt-rename { display: flex; flex-direction: column; gap: 10px; padding: 12px; border-radius: var(--vt-r); background: var(--vt-surface-2); border: 1px solid var(--vt-line); }
-.vt-app .vt-rename-label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; font-weight: 600; color: var(--vt-text-2); }
+.vt-app .vt-rename-label { display: flex; flex-direction: column; gap: 5px; font-size: var(--vt-fs-sm); font-weight: 600; color: var(--vt-text-2); }
.vt-app .vt-rename .vt-rename-input {
- height: 36px; width: 100%; padding: 0 10px; margin: 0;
- border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm); background: var(--vt-surface);
- color: var(--vt-text); font: inherit; font-size: 14px; box-shadow: none;
+ height: var(--vt-ctl); width: 100%; padding: 0 10px; margin: 0;
+ border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-ctl); background: var(--vt-surface);
+ color: var(--vt-text); font: inherit; font-size: var(--vt-fs-md); box-shadow: none;
}
.vt-app .vt-rename .vt-rename-input:focus { outline: none; border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); }
.vt-app .vt-icon-picks { display: flex; flex-wrap: wrap; gap: 4px; }
.vt-app .vt-icon-pick {
- display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; margin: 0;
- border: 1px solid var(--vt-line); border-radius: 8px; background: var(--vt-surface); color: var(--vt-text-2);
+ display: inline-flex; align-items: center; justify-content: center; width: var(--vt-ctl); height: var(--vt-ctl); padding: 0; margin: 0;
+ border: 1px solid var(--vt-line); border-radius: var(--vt-r-ctl); background: var(--vt-surface); color: var(--vt-text-2);
cursor: pointer; box-shadow: none; min-width: 0;
}
.vt-app .vt-icon-pick:hover { border-color: var(--vt-line-2); color: var(--vt-text); }
.vt-app .vt-icon-pick[aria-pressed="true"] { background: var(--vt-accent); border-color: var(--vt-accent); color: var(--vt-accent-ink); }
.vt-app .vt-rename-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
-.vt-app .vt-rename-actions .vt-hint { flex-basis: 100%; font-size: 11px; }
-.vt-app .vt-form-err { color: var(--vt-err); font-size: 12px; }
+.vt-app .vt-rename-actions .vt-hint { flex-basis: 100%; font-size: var(--vt-fs-micro); }
+.vt-app .vt-form-err { color: var(--vt-err); font-size: var(--vt-fs-sm); }
/* ------------------------------------------------ 12. small screens, motion */
@@ -1071,7 +1086,7 @@ h2, h3{
.vt-app .vt-fchips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin: 0 -14px; padding: 0 14px; }
.vt-app .vt-fchip { flex: none; }
.vt-app .vt-head { gap: 10px; }
- .vt-app .vt-headline { font-size: 18px; }
+ .vt-app .vt-headline { font-size: var(--vt-fs-lg); }
.vt-app .vt-orb { width: 32px; height: 32px; }
/* phones: a live connection is just the pulsing dot; paused or
reconnecting still says so in words */
@@ -1082,10 +1097,10 @@ h2, h3{
.vt-app .vt-health { grid-template-columns: 1fr 1fr; gap: 8px; }
.vt-app .vt-chip { padding: 9px 10px; gap: 8px; }
.vt-app .vt-chip-ico { width: 22px; height: 22px; }
- .vt-app .vt-chip-reason { -webkit-line-clamp: 2; font-size: 11.5px; }
+ .vt-app .vt-chip-reason { -webkit-line-clamp: 2; font-size: var(--vt-fs-micro); }
.vt-app .vt-sys-cpu { grid-template-columns: auto minmax(0, 1fr); }
.vt-app .vt-chart { --vt-mh: 108px; --vt-mg: 12px; }
- .vt-app .vt-mult-now { font-size: 15px; }
+ .vt-app .vt-mult-now { font-size: var(--vt-fs-md); }
.vt-app .vt-mx-odd { display: none; }
.vt-app .vt-tip { min-width: 160px; }
}
@@ -1119,11 +1134,11 @@ h2, h3{
.vt-app .vt-exp { gap: 8px; }
.vt-app .vt-exp-text { flex-direction: row; align-items: baseline; gap: 6px; min-width: 0; }
.vt-app .vt-exp-text b { flex: none; }
- .vt-app .vt-score { width: 28px; height: 22px; font-size: 12px; }
+ .vt-app .vt-score { width: 28px; height: 22px; font-size: var(--vt-fs-sm); }
.vt-app .vt-td-name { min-width: 0; }
.vt-app .vt-ctable td.vt-td-rate, .vt-app .vt-ctable td.vt-td-time { display: none; }
.vt-app .vt-who-label { max-width: 100%; }
- .vt-app .vt-net-ssid { font-size: 13px; max-width: none; }
+ .vt-app .vt-net-ssid { font-size: var(--vt-fs-base); max-width: none; }
}
@media (max-width: 640px) {
@@ -1141,6 +1156,11 @@ h2, h3{
.vt-app .vt-kvs .vt-kv { grid-template-columns: 112px minmax(0, 1fr); }
}
+/* touch: 16px text in fields (iOS zooms into anything smaller) */
+@media (pointer: coarse) {
+ .vt-app .vt-filter .vt-filter-input, .vt-app .vt-rename .vt-rename-input { font-size: 16px; }
+}
+
@media (prefers-reduced-motion: reduce) {
.vt-app *, .vt-app *::before, .vt-app *::after { animation: none !important; transition: none !important; }
.vt-app .vt-flow-on { opacity: .8; stroke-dasharray: 1.5 6; }
diff --git a/luci-app-wizard/htdocs/luci-static/resources/view/wizard/initsetup.js b/luci-app-wizard/htdocs/luci-static/resources/view/wizard/initsetup.js
index 944795dd5..e0b7f95b2 100644
--- a/luci-app-wizard/htdocs/luci-static/resources/view/wizard/initsetup.js
+++ b/luci-app-wizard/htdocs/luci-static/resources/view/wizard/initsetup.js
@@ -134,7 +134,7 @@ return view.extend({
dhcp: uci.get('dhcp', 'lan', 'ignore') === '1' ? '0' : '1',
ipv6: uci.get('network', 'wan6', 'auto') === '0' ? '0' : '1',
https: uci.get('wizard', 'default', 'https') || '0',
- cookie_p: uci.get('luci', 'main', 'persistent_cookies') || '1',
+ persistent_cookies: uci.get('luci', 'main', 'persistent_cookies') || '1',
landing_page: uci.get('luci', 'main', 'landing_page') || 'auto',
autoupgrade_fm: uci.get('luci', 'main', 'autoupgrade_fm') || '0',
coremark: uci.get('wizard', 'default', 'coremark') || '0',
@@ -192,7 +192,7 @@ return view.extend({
// 固件与系统设置
{ tab: 'firmware', type: form.Flag, id: 'autoupgrade_fm', title: _('Firmware Upgrade Notice'), desc: _('Check and display notices for newer firmware versions.') },
{ tab: 'firmware', type: form.Flag, id: 'coremark', title: _('Run CoreMark on Boot'), desc: _('Run CPU benchmark asynchronously upon router initialization.') },
- { tab: 'firmware', type: form.Flag, id: 'cookie_p', title: _('Persistent Cookie Session'), desc: _('Maintain persistent login sessions in the web browser.') },
+ { tab: 'firmware', type: form.Flag, id: 'persistent_cookies', title: _('Persistent Cookie Session'), desc: _('Maintain persistent login sessions in the web browser.') },
{ tab: 'firmware', type: form.Flag, id: 'https', title: _('Enforce HTTPS Access'), desc: _('Automatically redirect HTTP requests to secure HTTPS.') },
{ tab: 'firmware', type: form.ListValue, id: 'landing_page', title: _('Landing Dashboard Mode'),
choices: { 'auto': _('Auto'), 'overview': _('Overview'), 'istoreos': _('iStoreOS'), 'dashboard': _('Dashboard'), routerdog: _('RouterDog'), nas: _('NAS'), 'next-nas': _('Next-NAS'), router: _('Router') } }
@@ -480,9 +480,9 @@ return view.extend({
uci.set('wizard', 'default', 'https', cur.https);
}
- // I. 页面与会话设置(持久Cookie双向同步至luci系统底层与向导配置包)
- if (has(changed, 'cookie_p')) {
- uci.set('luci', 'main', 'persistent_cookies', cur.cookie_p);
+ // I. 页面与会话设置
+ if (has(changed, 'persistent_cookies')) {
+ uci.set('luci', 'main', 'persistent_cookies', cur.persistent_cookies);
}
if (has(changed, 'landing_page')) {
uci.set('luci', 'main', 'landing_page', cur.landing_page);
diff --git a/luci-theme-vantage/Makefile b/luci-theme-vantage/Makefile
index 6a85de151..b07dfa44c 100644
--- a/luci-theme-vantage/Makefile
+++ b/luci-theme-vantage/Makefile
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-vantage
-PKG_VERSION:=1.0.3
-PKG_RELEASE:=16
+PKG_VERSION:=1.0.4
+PKG_RELEASE:=17
PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage theme
diff --git a/luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js b/luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js
index a6deba98f..f0b651ad9 100644
--- a/luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js
+++ b/luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js
@@ -46,7 +46,8 @@ const ICONS = {
moon: [ [ 'path', { d: 'M19.5 14.5A7.5 7.5 0 0 1 9.5 4.5a7.5 7.5 0 1 0 10 10z' } ] ],
auto: [ [ 'circle', { cx: '12', cy: '12', r: '8' } ], [ 'path', { d: 'M12 4a8 8 0 0 1 0 16z', 'class': 'vt-fill' } ] ],
logout: [ [ 'path', { d: 'M14 4h4.5A1.5 1.5 0 0 1 20 5.5v13a1.5 1.5 0 0 1-1.5 1.5H14' } ], [ 'path', { d: 'M10 8l-4 4 4 4M6 12h10' } ] ],
- pin: [ [ 'path', { d: 'M4 5v14M9 12h11M15 7l5 5-5 5' } ] ]
+ pin: [ [ 'path', { d: 'M4 5v14M9 12h11M15 7l5 5-5 5' } ] ],
+ info: [ [ 'circle', { cx: '12', cy: '12', r: '8.5' } ], [ 'path', { d: 'M12 11v5.5' } ], [ 'circle', { cx: '12', cy: '7.75', r: '.6' } ] ]
};
function iconFor(name) {
@@ -180,6 +181,11 @@ function technical(text) {
return /\d/.test(rest) && /^[\s0-9a-fA-F.:,\/%()+\-x[\]~=]*$/.test(rest);
}
+/* heading text as compared for repeats: case, spacing and a trailing colon don't count */
+function headingKey(text) {
+ return String(text || '').replace(/\s+/g, ' ').replace(/[\s:]+$/, '').trim().toLowerCase();
+}
+
/* ---------------------------------------------------------------------------
* Stock-page enhancers, only where CSS cannot restructure LuCI's markup
* (Status -> Overview: fused memory bar, per-mount storage bars, radio band
@@ -410,11 +416,18 @@ function humpPath(points) {
return 'M' + r(x0) + ',' + r(base) + 'C' + r(x0 + k) + ',' + r(cy) + ' ' + r(x3 - k) + ',' + r(cy) + ' ' + r(x3) + ',' + r(base) + 'Z';
}
+/* "#89cdb4" -> "rgb(137, 205, 180)", the form element.style reads back */
+function hexRgb(hex) {
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(String(hex));
+ return m ? 'rgb(' + [ m[1], m[2], m[3] ].map(h => parseInt(h, 16)).join(', ') + ')' : null;
+}
+
function setupChannelAnalysis() {
const view = document.getElementById('view');
const own = new Set();
const slots = new Map();
let next = 0;
+ let colourOf = null;
/* slot 1 (the accent) is the AP itself; neighbours take 2..6 in the order
they first appear (the view sorts by channel, so neighbours differ) */
const slotFor = (colour, mine) => {
@@ -455,9 +468,17 @@ function setupChannelAnalysis() {
const dot = tr.querySelector('span[style*="color"]');
const cells = tr.querySelectorAll(':scope > td, :scope > .td');
if (!cells.length) return;
- const bssid = cells[cells.length - 1].textContent.trim().toUpperCase();
+ const printed = cells[cells.length - 1].textContent.trim();
+ const bssid = printed.toUpperCase();
const mine = own.has(bssid);
if (tr.classList.contains('v-ca-own') !== mine) tr.classList.toggle('v-ca-own', mine);
+ /* the view prints its own interface's row without the colour
+ dot, so rebuild that colour the way the view made it: from
+ the BSSID as printed (the derivation is case-sensitive) */
+ if (mine && colourOf) {
+ const derived = hexRgb(colourOf(printed));
+ if (derived) mineByColour.set(derived, true);
+ }
if (!dot) return;
const colour = dot.style.color;
if (mine) mineByColour.set(colour, true);
@@ -531,7 +552,15 @@ function setupChannelAnalysis() {
busy = false;
};
- L.resolveDefault(callWirelessDevices(), {}).then(devs => {
+ /* tools.prng is what the view colours networks with (it is loaded
+ already); derive_color reseeds on every call, so calling it here
+ leaves the view's colours as they are */
+ Promise.all([
+ L.resolveDefault(callWirelessDevices(), {}),
+ L.resolveDefault(L.require('tools.prng'), null)
+ ]).then(([ devs, prng ]) => {
+ if (prng && typeof prng.derive_color === 'function')
+ colourOf = b => prng.derive_color(b);
for (const r of Object.values(devs || {}))
for (const i of (r && Array.isArray(r.interfaces)) ? r.interfaces : [])
[ i.iwinfo && i.iwinfo.bssid, i.config && i.config.macaddr ].forEach(b => {
@@ -558,10 +587,12 @@ return baseclass.extend({
this.closeTimer = null;
this.openTimer = null;
this.entries = [];
+ this.trailKeys = new Set();
this.setupTheme();
this.setupIndicators();
this.setupNumerals();
+ this.setupForms();
setupStockEnhancers();
setupCharts();
this.setupKeys();
@@ -605,6 +636,7 @@ return baseclass.extend({
this.renderCrumbs();
this.renderTabs();
this.buildIndex();
+ this.queueForms();
},
isPinned() {
@@ -861,6 +893,7 @@ return baseclass.extend({
});
this.pageTitle = parts[parts.length - 1].title;
+ parts.forEach(p => this.trailKeys.add(headingKey(p.title)));
this.updateTitle();
},
@@ -1194,5 +1227,126 @@ return baseclass.extend({
window.requestAnimationFrame(scan);
}).observe(main, { childList: true, subtree: true, characterData: true });
scan();
+ },
+
+ /* ---------------------------------------------------- form chrome */
+
+ /* LuCI renders forms into #view and the modal, and renders them again
+ after a save: watch the body and decorate what is new. This adds (i)
+ buttons and sets classes and attributes; LuCI's text and the order of
+ LuCI's nodes stay as they are. */
+ setupForms() {
+ this.formsQueued = false;
+ this.helpSeq = 0;
+ this.helpSizes = window.ResizeObserver ? new ResizeObserver(entries => entries.forEach(e => this.fitHelp(e.target))) : null;
+ if (window.MutationObserver)
+ new MutationObserver(() => this.queueForms()).observe(document.body, { childList: true, subtree: true });
+ this.decorateForms();
+ },
+
+ queueForms() {
+ if (this.formsQueued) return;
+ this.formsQueued = true;
+ window.requestAnimationFrame(() => {
+ this.formsQueued = false;
+ this.decorateForms();
+ });
+ },
+
+ decorateForms() {
+ document.querySelectorAll('.cbi-value > .cbi-value-title + .cbi-value-field > .cbi-value-description:not(.vt-help-text)')
+ .forEach(desc => this.addFieldHelp(desc));
+ document.querySelectorAll(':is(.cbi-map-descr, .cbi-section-descr):not(:has(> .vt-help))')
+ .forEach(desc => this.addIntroHelp(desc));
+ this.markRepeats();
+ },
+
+ /* Help text shows its first line; (i) unfolds the rest. LuCI also puts
+ warnings here, so none is hidden whole: its start stays visible.
+ Field help: the button sits in the label column, after the field,
+ because LuCI's label click handler finds the widget through
+ nextElementSibling. */
+ addFieldHelp(desc) {
+ const row = desc.parentNode.parentNode;
+ const title = row.querySelector(':scope > .cbi-value-title');
+ row.appendChild(this.helpToggle(desc, row, _('Show full help: %s').format(title.textContent.trim()), _('Show full help')));
+ },
+
+ /* Page and section intros: the button leads the first line, inside the
+ description, since nothing may stand between LuCI's siblings (ui.tabs
+ finds a tab menu as its pane group's previous sibling). The fold needs
+ the button, so a view that rewrote the text would show it whole until
+ the next pass adds the button again. The cut-off line unfolds on a
+ click too, a bigger target on touch than the icon. */
+ addIntroHelp(desc) {
+ if (desc.closest('.vt-app') || !desc.textContent.trim()) return;
+ desc.insertBefore(this.helpToggle(desc, desc, _('Show full description'), _('Show full description')), desc.firstChild);
+ if (desc._vtIntroClick) return;
+ desc._vtIntroClick = true;
+ desc.addEventListener('click', ev => {
+ const btn = desc.querySelector(':scope > .vt-help');
+ if (!btn || btn.hidden || desc.getAttribute('data-v-help') !== 'closed') return;
+ if (ev.target.closest('a, button') || String(window.getSelection())) return;
+ btn.click();
+ });
+ },
+
+ /* The (i) button for desc. Its state lives on holder (the field row, or
+ the intro itself) as data-v-help, which the CSS folds by. */
+ helpToggle(desc, holder, label, title) {
+ const state = holder.getAttribute('data-v-help') || 'closed';
+ holder.setAttribute('data-v-help', state);
+ desc.classList.add('vt-help-text');
+ if (!desc.id) desc.id = 'vt-help-' + (++this.helpSeq);
+ const btn = E('button', {
+ 'class': 'vt-help', 'type': 'button', 'hidden': '',
+ 'aria-expanded': String(state === 'open'), 'aria-controls': desc.id,
+ 'aria-label': label, 'title': title,
+ 'click': () => {
+ const open = holder.getAttribute('data-v-help') !== 'open';
+ holder.setAttribute('data-v-help', open ? 'open' : 'closed');
+ btn.setAttribute('aria-expanded', String(open));
+ this.fitHelp(desc);
+ }
+ }, [ svg('info', 16) ]);
+ desc._vtHelp = { btn: btn, holder: holder };
+ if (this.helpSizes) this.helpSizes.observe(desc);
+ else btn.hidden = false;
+ return btn;
+ },
+
+ /* (i) shows only while the one-line clamp cuts the text. Text that is
+ not displayed (another tab, an unmet dependency) measures 0 and waits
+ for the next resize notification. */
+ fitHelp(desc) {
+ if (!desc.isConnected) {
+ if (this.helpSizes) this.helpSizes.unobserve(desc);
+ return;
+ }
+ const h = desc._vtHelp;
+ if (!h || !h.btn.isConnected || !desc.clientHeight) return;
+ const cut = desc.scrollHeight > desc.clientHeight + 1;
+ h.btn.hidden = !cut && h.holder.getAttribute('data-v-help') !== 'open';
+ },
+
+ /* A page title or card heading that only repeats the breadcrumb, the
+ map title or its own tab's name is hidden visually; screen readers
+ still get it as a heading. Headings that hold a control (Overview's
+ Hide/Show label) always stay. */
+ markRepeats() {
+ /* template pages (Status -> Overview) print their title before #view */
+ document.querySelectorAll('#maincontent > h2, #view h2, #view .cbi-section > h3:first-child').forEach(h => {
+ if (h.closest('.vt-app') || h.querySelector('a, button, input, select, [data-clickable], [tabindex]')) return;
+ const key = headingKey(h.textContent);
+ let repeat = this.trailKeys.has(key);
+ if (!repeat && h.tagName === 'H3') {
+ const map = h.closest('.cbi-map');
+ const mapTitle = map && map.querySelector(':scope > h2');
+ const pane = h.closest('[data-tab-title]');
+ repeat = (mapTitle && headingKey(mapTitle.textContent) === key) ||
+ (pane && headingKey(pane.getAttribute('data-tab-title')) === key);
+ }
+ h.classList.toggle('vt-repeat', !!(key && repeat));
+ });
}
});
diff --git a/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css b/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css
index 0f0478287..dd700a1e7 100644
--- a/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css
+++ b/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css
@@ -39,7 +39,9 @@
--rail-w: 56px;
--rail-w-pinned: 204px;
--top-h: 52px;
- --ctl-h: 32px;
+ --ctl-h: 36px; /* every control: inputs, selects, buttons, tabs, chips */
+ --label-w: 180px; /* form label column */
+ --field-max: 360px; /* widest single-line field */
--content-max: 1320px;
/* light: cool paper grey, olive accent text */
@@ -72,6 +74,9 @@
--scrim: rgba(20, 23, 26, .38);
--shadow-pop: 0 12px 32px rgba(20, 23, 26, .16), 0 2px 6px rgba(20, 23, 26, .08);
--zone-mix: .22;
+ --seg-bg: var(--s3); /* segmented control: track, selected segment */
+ --seg-on: var(--s2);
+ --seg-on-ring: 0 0 0 1px var(--line), var(--v-shadow);
}
html[data-darkmode="true"] {
@@ -107,6 +112,14 @@ html[data-darkmode="true"] {
--scrim: rgba(0, 0, 0, .58);
--shadow-pop: 0 16px 40px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .02);
--zone-mix: .2;
+ --seg-bg: var(--bg);
+ --seg-on: var(--s3);
+ --seg-on-ring: 0 0 0 1px var(--line-2);
+}
+
+/* touch: controls are sized by the pointer, not the screen width */
+@media (pointer: coarse) {
+ :root { --ctl-h: 44px; }
}
/* Shared Vantage tokens (--v-*): the stable contract other Vantage views
@@ -136,9 +149,17 @@ html[data-darkmode="true"] {
--v-info-soft: var(--info-soft);
--v-font: var(--font);
--v-mono: var(--mono);
+ --v-fs-micro: var(--fs-micro); /* the type scale: 10.5 12 13 14 16 20 24 px */
+ --v-fs-sm: var(--fs-sm);
+ --v-fs-base: var(--fs-base);
+ --v-fs-md: var(--fs-md);
+ --v-fs-lg: var(--fs-lg);
+ --v-fs-xl: var(--fs-xl);
+ --v-fs-2xl: var(--fs-2xl);
--v-r-sm: var(--r-sm);
--v-r: var(--r);
--v-r-lg: var(--r-lg);
+ --v-ctl-h: var(--ctl-h); /* every control: 36px, 44px on touch */
--v-shadow: 0 1px 2px rgba(20, 23, 26, .06), 0 1px 0 rgba(20, 23, 26, .03);
/* where sticky content may start: below the top bar and page tabs */
--v-stick-top: var(--stick-top);
@@ -508,7 +529,7 @@ a.vt-crumb:hover { color: var(--text); }
font-size: var(--fs-sm);
white-space: nowrap;
}
-.vt-host b { color: var(--text); font-weight: 600; font-family: var(--mono); font-size: 11.5px; }
+.vt-host b { color: var(--text); font-weight: 600; font-family: var(--mono); font-size: var(--fs-micro); }
.vt-host span { overflow: hidden; text-overflow: ellipsis; }
.vt-host-sep { color: var(--text-3); }
@@ -555,7 +576,7 @@ a.vt-crumb:hover { color: var(--text); }
min-width: 20px; height: 20px; padding: 0 6px;
border-radius: 999px;
background: var(--warn); color: #1a1200;
- font: 700 11.5px/1 var(--mono);
+ font: 700 var(--fs-micro)/1 var(--mono);
}
#indicators > span[data-indicator="media_error"] { color: var(--err); border-color: var(--err); background: var(--err-soft); }
@@ -575,7 +596,7 @@ a.vt-crumb:hover { color: var(--text); }
.vt-tab {
position: relative;
display: inline-flex; align-items: center;
- height: 40px; padding: 0 var(--sp-3);
+ height: var(--ctl-h); padding: 0 var(--sp-3);
color: var(--text-2);
font-weight: 500;
white-space: nowrap;
@@ -691,7 +712,7 @@ a.vt-crumb:hover { color: var(--text); }
/* page = title block + a stack of cards. Cards carry a full-bleed header
row (h3) and full-bleed tables; nested sections lose their chrome. */
:root { --card-px: 20px; --card-py: 16px; --stick-top: var(--top-h); }
-html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + 41px); }
+html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + var(--ctl-h) + 1px); }
.cbi-map { margin-bottom: var(--sp-6); }
.cbi-map > h2, .vt-main > h2, #view > h2, #view > div > h2:first-child, .cbi-map > h2:first-child {
@@ -704,18 +725,18 @@ html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + 41px); }
max-width: 76ch;
margin: 0 0 var(--sp-6);
color: var(--text-2);
- font-size: 13.5px; line-height: 1.55;
+ font-size: var(--fs-base); line-height: 1.55;
}
.cbi-section-descr { max-width: 80ch; margin: 0 0 var(--sp-4); color: var(--text-2); }
.cbi-map-descr:empty, .cbi-section-descr:empty { display: none; }
-.cbi-map > h2 + .cbi-tabmenu, .cbi-map > h2 + .cbi-map-descr:empty + .cbi-tabmenu { margin-top: var(--sp-4); }
+.cbi-map > h2:not(.vt-repeat) + .cbi-tabmenu, .cbi-map > h2:not(.vt-repeat) + .cbi-map-descr:empty + .cbi-tabmenu { margin-top: var(--sp-4); }
.cbi-section, .cbi-map > fieldset {
margin: 0 0 var(--sp-4);
padding: var(--card-py) var(--card-px);
background: var(--s1);
border: 1px solid var(--line);
- border-radius: 10px;
+ border-radius: var(--r-lg);
box-shadow: var(--v-shadow);
min-width: 0;
background-clip: padding-box;
@@ -754,22 +775,27 @@ html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + 41px); }
}
.cbi-section .cbi-section > h3:first-child { margin: 0 0 var(--sp-3); padding: 0; min-height: 0; border: 0; }
.cbi-section .cbi-section + .cbi-section { margin-top: var(--sp-5); padding-top: var(--sp-5); border-top: 1px solid var(--line); }
+/* a nested table section scrolls its own table, so it takes the card bleed
+ itself (out by --card-px, padded back in): the table's bleed then stays
+ inside the scroll box instead of overflowing it by --card-px */
+.cbi-section .cbi-section.cbi-tblsection {
+ margin-left: calc(-1 * var(--card-px)); margin-right: calc(-1 * var(--card-px));
+ padding-left: var(--card-px); padding-right: var(--card-px);
+}
fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
legend { font-size: var(--fs-md); font-weight: 600; padding: 0; margin-bottom: var(--sp-3); }
-/* option rows: two aligned columns on desktop */
+/* option rows: label column + field, no rules between rows */
.cbi-value {
display: grid;
- grid-template-columns: minmax(160px, 260px) minmax(0, 1fr);
- column-gap: var(--sp-8);
+ grid-template-columns: var(--label-w) minmax(0, 1fr);
+ column-gap: var(--sp-5);
align-items: start;
- padding: 14px 0;
- border-bottom: 1px solid var(--line);
+ padding: 6px 0;
}
-.cbi-value:last-child, .cbi-value.cbi-value-last { border-bottom: 0; }
.cbi-value > .cbi-value-title {
- padding-top: 7px;
+ padding-top: calc((var(--ctl-h) - 1.5em) / 2); /* 1.5em: one line at line-height 1.5 */
color: var(--text);
font-weight: 500;
overflow-wrap: anywhere;
@@ -790,6 +816,64 @@ legend { font-size: var(--fs-md); font-weight: 600; padding: 0; margin-bottom: v
line-height: 1.5;
}
.cbi-value-helpicon img { width: 14px; height: 14px; opacity: .6; }
+
+/* folding help (menu-vantage.js): one line until (i) unfolds it; the
+ button shares the label cell, so both are placed explicitly */
+.cbi-value[data-v-help="closed"] > .cbi-value-field > .vt-help-text {
+ display: -webkit-box; -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1; line-clamp: 1;
+ max-height: 1.5em;
+ overflow: hidden;
+}
+.cbi-value[data-v-help] > .cbi-value-title { grid-row: 1; grid-column: 1; }
+.cbi-value[data-v-help] > .cbi-value-field { grid-row: 1; }
+.cbi-value:has(> .vt-help:not([hidden])) > .cbi-value-title { padding-right: 32px; }
+.cbi-value > .vt-help {
+ grid-row: 1; grid-column: 1;
+ justify-self: end;
+ display: grid; place-items: center;
+ width: 28px; height: var(--ctl-h);
+ padding: 0;
+ background: none; border: 0; border-radius: var(--r);
+ color: var(--text-3);
+ cursor: pointer;
+}
+/* a title that opens with
(Wireless "Operating frequency", lined up
+ with captioned selects) has its text on the second line: so has (i) */
+.cbi-value:has(> .cbi-value-title > br:first-child) > .vt-help { margin-top: 1.5em; }
+
+/* page and section intros fold the same way, (i) leading the first line;
+ the fold needs the button, so without it the text shows whole */
+:is(.cbi-map-descr, .cbi-section-descr)[data-v-help="closed"]:has(> .vt-help) {
+ display: -webkit-box; -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1; line-clamp: 1;
+ overflow: hidden;
+}
+.cbi-map-descr[data-v-help="closed"]:has(> .vt-help) { max-height: 1.55em; } /* its line-height */
+.cbi-section-descr[data-v-help="closed"]:has(> .vt-help) { max-height: 1.5em; }
+:is(.cbi-map-descr, .cbi-section-descr)[data-v-help="closed"]:has(> .vt-help:not([hidden])) { cursor: pointer; }
+:is(.cbi-map-descr, .cbi-section-descr) > .vt-help {
+ display: inline-grid; place-items: center;
+ width: 1.5em; height: 1.5em;
+ margin: 0 6px 0 -3px; padding: 0;
+ vertical-align: top;
+ background: none; border: 0; border-radius: var(--r-sm);
+ color: var(--text-3);
+ cursor: pointer;
+}
+/* both buttons, after their placement rules */
+.vt-help svg { fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
+.vt-help:hover { color: var(--text); background: var(--s3); }
+.vt-help[aria-expanded="true"] { color: var(--accent-text); }
+
+/* a heading that repeats the breadcrumb or its tab (menu-vantage.js):
+ gone from view, still a heading for screen readers */
+.vt-repeat {
+ position: absolute !important;
+ width: 1px; height: 1px; min-height: 0 !important;
+ margin: -1px !important; padding: 0 !important; border: 0 !important;
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap;
+}
.cbi-value.cbi-value-error .cbi-value-title,
.cbi-value-field .cbi-input-invalid + .cbi-value-description { color: var(--err); }
@@ -812,7 +896,7 @@ legend { font-size: var(--fs-md); font-weight: 600; padding: 0; margin-bottom: v
backdrop-filter: saturate(1.3) blur(14px);
-webkit-backdrop-filter: saturate(1.3) blur(14px);
border: 1px solid var(--line-2);
- border-radius: 12px;
+ border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
}
.cbi-page-actions > * { margin: 0 !important; }
@@ -833,7 +917,7 @@ input, select, textarea, button { font: inherit; color: inherit; }
.vt-main input[type="date"], .vt-main input[type="time"], .vt-main input[type="datetime-local"],
.vt-main input:not([type]), .modal input[type="text"], .modal input[type="password"], .modal input:not([type]), .modal select, .modal textarea {
width: 100%;
- max-width: 440px;
+ max-width: var(--field-max);
min-height: var(--ctl-h);
padding: 5px 10px;
background: var(--s2);
@@ -844,7 +928,7 @@ input, select, textarea, button { font: inherit; color: inherit; }
line-height: 20px;
transition: border-color .12s, box-shadow .12s;
}
-.vt-main textarea, .cbi-input-textarea, .modal textarea { max-width: none; min-height: 88px; font-family: var(--mono); font-size: var(--fs-sm); resize: vertical; }
+.vt-main textarea, .cbi-input-textarea, .modal textarea { max-width: calc(2 * var(--field-max)); min-height: 88px; font-family: var(--mono); font-size: var(--fs-sm); resize: vertical; }
.vt-main select, .cbi-input-select, .modal select {
appearance: none; -webkit-appearance: none;
padding-right: 30px;
@@ -853,6 +937,13 @@ input, select, textarea, button { font: inherit; color: inherit; }
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
+/* a native select in a form row cuts a long choice off without an
+ ellipsis: let it grow past --field-max to fit (field-sizing: the chosen
+ value; elsewhere the widest option), never past its column */
+.cbi-value > .cbi-value-field > select:not([multiple]), .cbi-value > .cbi-value-field > div > select:not([multiple]) {
+ field-sizing: content;
+ width: auto; min-width: min(100%, var(--field-max)); max-width: 100%;
+}
.vt-main select[multiple] { background-image: none; padding-right: 10px; }
input::placeholder, textarea::placeholder { color: var(--text-3); opacity: 1; }
input:hover, select:hover, textarea:hover { border-color: var(--text-3); }
@@ -876,7 +967,7 @@ input[readonly] { cursor: default; }
/* password + reveal and other control groups */
.control-group {
display: flex; align-items: stretch; flex-wrap: nowrap;
- max-width: 440px;
+ max-width: var(--field-max);
}
.control-group > * { margin: 0 !important; }
.control-group > :not(:first-child) { margin-left: -1px !important; }
@@ -913,7 +1004,7 @@ input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible { outlin
input[type="checkbox"][disabled]:checked { opacity: .5; }
/* range slider */
-.cbi-range-slider { display: flex; align-items: center; gap: var(--sp-3); max-width: 440px; min-height: var(--ctl-h); flex-wrap: wrap; }
+.cbi-range-slider { display: flex; align-items: center; gap: var(--sp-3); max-width: var(--field-max); min-height: var(--ctl-h); flex-wrap: wrap; }
.cbi-range-slider input[type="range"] { flex: 1 1 160px; accent-color: var(--accent-text); }
.cbi-range-slider-value, .cbi-range-slider-calc { font-family: var(--mono); color: var(--text-2); font-size: var(--fs-sm); }
@@ -942,7 +1033,7 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
opacity: .45; cursor: not-allowed; pointer-events: none;
}
-.cbi-button-up, .cbi-button-down { min-width: 28px; padding: 0 6px; }
+.cbi-button-up, .cbi-button-down { min-width: var(--ctl-h); padding: 0 6px; }
.button-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-4); }
.right > .btn, .right > .cbi-button, .right > button { margin-left: var(--sp-2); }
@@ -951,7 +1042,7 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
.cbi-dropdown {
position: relative;
display: inline-flex; align-items: stretch;
- width: 100%; max-width: 440px;
+ width: 100%; max-width: var(--field-max);
min-height: var(--ctl-h);
padding: 0;
background: var(--s2);
@@ -1044,8 +1135,8 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
}
.cbi-dropdown[open] > ul.dropdown[style*="bottom"] { top: auto; }
.cbi-dropdown[open] > ul.dropdown > li {
- display: flex;
- min-height: 30px;
+ display: flex; align-items: center;
+ min-height: calc(var(--ctl-h) - 4px);
padding: 4px 8px;
border-radius: var(--r-sm);
white-space: normal;
@@ -1062,6 +1153,18 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
.cbi-dropdown[multiple][open] > ul.dropdown > li > form { display: inline-flex; }
.cbi-dropdown[open] > ul.dropdown > li input.create-item-input { max-width: none; min-height: 28px; }
.cbi-dropdown[open] > .open { color: var(--accent-text); }
+/* one arrow for every field: the select's triangle, drawn where LuCI puts
+ its "▾" glyph (ComboButtons keep theirs) */
+.cbi-dropdown:not(.btn, .cbi-button) > .open {
+ --arrow: var(--text-3);
+ width: 30px; padding: 0;
+ font-size: 0;
+ background-image: linear-gradient(45deg, transparent 50%, var(--arrow) 50%), linear-gradient(135deg, var(--arrow) 50%, transparent 50%);
+ background-position: calc(100% - 15px) 50%, calc(100% - 10px) 50%;
+ background-size: 5px 5px, 5px 5px;
+ background-repeat: no-repeat;
+}
+.cbi-dropdown:not(.btn, .cbi-button)[open] > .open { --arrow: var(--accent-text); }
/* ComboButton: dropdown dressed as a split button */
.cbi-dropdown.btn, .cbi-dropdown.cbi-button {
@@ -1139,12 +1242,12 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
:is(.btn, .cbi-button).cbi-button-reset:hover { color: var(--text); }
/* dynamic list */
-.cbi-dynlist { display: flex; flex-direction: column; gap: 6px; max-width: 440px; }
+.cbi-dynlist { display: flex; flex-direction: column; gap: 6px; max-width: var(--field-max); }
.cbi-dynlist > .item {
position: relative;
display: flex; align-items: center;
- min-height: 30px;
- padding: 4px 34px 4px 10px;
+ min-height: var(--ctl-h);
+ padding: 4px calc(var(--ctl-h) + 4px) 4px 10px;
background: var(--s2);
border: 1px solid var(--line);
border-radius: var(--r);
@@ -1155,11 +1258,11 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
.cbi-dynlist > .item::after {
content: "\00d7";
position: absolute; top: 0; right: 0; bottom: 0;
- width: 30px;
+ width: var(--ctl-h);
display: flex; align-items: center; justify-content: center;
border-left: 1px solid var(--line);
color: var(--text-3);
- font: 400 16px/1 var(--font);
+ font: 400 var(--fs-lg)/1 var(--font);
cursor: pointer;
}
.cbi-dynlist > .item:hover::after { color: var(--err); background: var(--err-soft); border-radius: 0 var(--r) var(--r) 0; }
@@ -1168,7 +1271,7 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
.cbi-dynlist[disabled] > .item::after { display: none; }
.cbi-dynlist > .add-item { max-width: none; }
.cbi-dynlist > .add-item > .cbi-dropdown { max-width: none; }
-.cbi-dynlist > .add-item > .cbi-button-add { min-width: 34px; font-family: var(--mono); font-size: var(--fs-md); }
+.cbi-dynlist > .add-item > .cbi-button-add { min-width: var(--ctl-h); font-family: var(--mono); font-size: var(--fs-md); }
/* add rows built without .control-group (e.g. System → SSH-Keys): input and button side by side */
.cbi-dynlist > .add-item:not(.control-group) { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); max-width: 720px; }
.cbi-dynlist > .add-item:not(.control-group) > input { flex: 1 1 320px; min-width: 0; margin: 0; }
@@ -1242,13 +1345,13 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; }
.td:not(.cbi-value-field)[data-title*="Target" i], .td:not(.cbi-value-field)[data-title*="Source" i], .td:not(.cbi-value-field)[data-title*="DUID" i], .td:not(.cbi-value-field)[data-title="PID"],
.td:not(.cbi-value-field)[data-title*="Metric" i], .td:not(.cbi-value-field)[data-title*="Priority" i], .td:not(.cbi-value-field)[data-title*="Remaining" i], .td:not(.cbi-value-field)[data-title*="Usage" i] {
white-space: nowrap;
- font-family: var(--mono); font-size: 12.5px;
+ font-family: var(--mono); font-size: var(--fs-sm);
font-variant-numeric: tabular-nums slashed-zero;
}
.td > span:has(> span + br + span) { display: flex; flex-direction: column; gap: 1px; }
.td > span:has(> span + br + span) > br { display: none; }
-.td > span:has(> span + br + span) > span { white-space: nowrap; font: 12px/1.5 var(--mono); }
-.td > span:has(> span + br + span) > span:not(:first-child) { color: var(--text-3); font-size: 11.5px; }
+.td > span:has(> span + br + span) > span { white-space: nowrap; font: var(--fs-sm)/1.5 var(--mono); }
+.td > span:has(> span + br + span) > span:not(:first-child) { color: var(--text-3); font-size: var(--fs-micro); }
/* badges inside cells are content, not pills: icon + text */
.td .ifacebadge, .td .ifacebadge.large {
@@ -1257,7 +1360,7 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; }
font-size: inherit; white-space: nowrap;
}
.td .ifacebadge img, .td .ifacebadge img[style] { width: 18px !important; height: 18px !important; flex: none; }
-.td .ifacebadge small { color: var(--text-3); font-family: var(--mono); font-size: 11.5px; }
+.td .ifacebadge small { color: var(--text-3); font-family: var(--mono); font-size: var(--fs-micro); }
.td > .ifacebadge + .ifacebadge, .td > span + .ifacebadge { margin-left: 10px; }
.cbi-section-table-descr > .th { font-weight: 400; text-transform: none; letter-spacing: 0; font-size: var(--fs-sm); }
.cbi-section-table-cell, .cbi-section-table-row > .td { vertical-align: middle; }
@@ -1273,7 +1376,7 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; }
.td.cbi-value-field.inactive { visibility: hidden; }
.td.cbi-section-actions, .th.cbi-section-actions { text-align: right; white-space: nowrap; width: 1%; }
.td.cbi-section-actions > div { display: inline-flex; gap: 4px; flex-wrap: nowrap; align-items: center; }
-.td.cbi-section-actions .btn, .td.cbi-section-actions .cbi-button { min-height: 28px; padding: 0 10px; font-size: var(--fs-sm); }
+.td.cbi-section-actions .btn, .td.cbi-section-actions .cbi-button { padding: 0 10px; }
.drag-handle { cursor: grab; color: var(--text-3); padding: 0 6px; font-family: var(--mono); }
.drag-over-above > .td, .drag-over-above > td { box-shadow: inset 0 2px 0 var(--accent-text); }
.drag-over-below > .td, .drag-over-below > td { box-shadow: inset 0 -2px 0 var(--accent-text); }
@@ -1294,38 +1397,120 @@ div.right, div.left { display: block; }
div.right { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-4); }
div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0; }
-/* in-map section tabs (ui.tabs) */
+/* Tabs inside a page (ui.tabs), one look per level:
+ - tabs standing at page level (not inside a form section or another
+ tab) are the page's sub-pages when the menu shows no page tabs: they
+ take the page-tab underline, like .vt-tabs (Interfaces / Devices /
+ ..., DHCP's Leases / dnsmasq, Channel Analysis's radios). Under
+ .vt-tabs they are the first level inside the page instead;
+ - the first level inside a page, form or modal: a segmented control;
+ - tabs inside a tab: a side list where the pane is wide enough
+ (container query on the pane, so modals and pages alike), chips
+ where it is not and at every deeper level.
+ A level is a pane of a section's option tabs (.cbi-section-node-tabbed);
+ map-level panes are not counted, so a page's sub-pages add no level. */
.cbi-tabmenu {
- display: flex; flex-wrap: nowrap; gap: var(--sp-1);
- margin: 0 0 var(--sp-4); padding: 0;
+ display: flex; flex-wrap: nowrap; align-items: center; gap: 2px;
+ width: fit-content; max-width: 100%; min-height: var(--ctl-h);
+ margin: 0 0 var(--sp-5); padding: 2px;
list-style: none;
- border-bottom: 1px solid var(--line);
+ background: var(--seg-bg);
+ border: 1px solid var(--line);
+ border-radius: var(--r-lg);
overflow-x: auto;
scrollbar-width: none;
}
.cbi-tabmenu::-webkit-scrollbar { display: none; }
.cbi-tabmenu > li { position: relative; flex: none; }
.cbi-tabmenu > li > a {
- display: inline-flex; align-items: center; gap: 6px;
- height: 36px; padding: 0 var(--sp-3);
+ display: flex; align-items: center; gap: 6px;
+ height: calc(var(--ctl-h) - 6px); padding: 0 var(--sp-3);
+ border-radius: var(--r);
color: var(--text-2);
font-weight: 500;
white-space: nowrap;
text-decoration: none !important;
}
.cbi-tabmenu > li > a:hover { color: var(--text); }
-.cbi-tabmenu > li.cbi-tab > a { color: var(--text); font-weight: 600; }
-.cbi-tabmenu > li.cbi-tab::after {
- content: ""; position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: -1px;
+.cbi-tabmenu > li.cbi-tab > a { background: var(--seg-on); color: var(--text); font-weight: 600; box-shadow: var(--seg-on-ring); }
+
+html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) {
+ gap: var(--sp-1);
+ width: auto; padding: 0;
+ background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0;
+}
+html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li > a { height: var(--ctl-h); border-radius: 0; }
+html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li.cbi-tab > a { background: none; box-shadow: none; }
+html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li.cbi-tab::after {
+ content: ""; position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: 0;
height: 2px; background: var(--accent-text); border-radius: 2px 2px 0 0;
}
+
+.cbi-section-node-tabbed > [data-tab-title] { container-type: inline-size; }
+.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu {
+ flex-wrap: wrap; gap: var(--sp-2);
+ width: auto; margin-bottom: var(--sp-4); padding: 0;
+ background: none; border: 0; border-radius: 0;
+ overflow: visible;
+}
+.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a {
+ height: var(--ctl-h); padding: 0 14px;
+ border: 1px solid var(--line-2); border-radius: 999px;
+}
+.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a {
+ background: var(--accent-soft); border-color: var(--accent-line);
+ color: var(--accent-text); box-shadow: none;
+}
+@container (min-width: 640px) {
+ .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) {
+ display: grid;
+ grid-template-columns: 168px minmax(0, 1fr);
+ column-gap: var(--sp-6);
+ align-items: start;
+ }
+ .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > * { grid-column: 1 / -1; }
+ .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * { grid-column: 2; }
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu {
+ grid-column: 1;
+ flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: 2px;
+ margin: 0;
+ }
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a {
+ justify-content: space-between;
+ padding: 0 var(--sp-3);
+ border: 0; border-radius: var(--r);
+ white-space: normal; line-height: 1.25;
+ }
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a:hover { background: var(--s3); }
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent-line); }
+ /* beside the list there is no card edge to bleed to: table sections and
+ tables fit the column */
+ .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .cbi-tblsection { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; }
+ .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table { width: 100% !important; margin-left: 0; margin-right: 0; }
+ .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table .tr > :is(.th, .td):first-child { padding-left: 0; }
+ .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table .tr > :is(.th, .td):last-child { padding-right: 0; }
+ /* deeper levels: back to the chips above */
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) { display: block; }
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu {
+ flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
+ margin: 0 0 var(--sp-4);
+ }
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a {
+ justify-content: normal;
+ padding: 0 14px;
+ border: 1px solid var(--line-2); border-radius: 999px;
+ white-space: nowrap; line-height: inherit;
+ }
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a:hover { background: none; }
+ .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a { border-color: var(--accent-line); box-shadow: none; }
+}
.cbi-tabmenu > li[data-errors] > a::after {
content: attr(data-errors);
display: inline-flex; align-items: center; justify-content: center;
min-width: 18px; height: 18px; padding: 0 5px;
border-radius: 999px;
background: var(--err); color: #fff;
- font: 700 10.5px/1 var(--mono);
+ font: 700 var(--fs-micro)/1 var(--mono);
}
.cbi-tabmenu > li.cbi-tab-disabled[data-errors] > a { color: var(--err); }
[data-tab-title]:not([data-tab-active="true"]) { display: none; }
@@ -1386,13 +1571,13 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
min-width: 0;
white-space: normal; overflow-wrap: anywhere;
color: var(--text);
- font: 400 12px/1.55 var(--mono);
+ font: 400 var(--fs-sm)/1.55 var(--mono);
font-variant-numeric: tabular-nums slashed-zero;
}
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > .nowrap:not(:has(> strong)) { display: block; }
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > .nowrap > strong {
color: var(--text-3);
- font: 600 10.5px/1.55 var(--font);
+ font: 600 var(--fs-micro)/1.55 var(--font);
letter-spacing: .06em; text-transform: uppercase;
white-space: nowrap;
}
@@ -1452,7 +1637,7 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
padding: 2px 8px;
border: 1px solid var(--line-2); border-radius: 999px;
color: var(--text-2);
- font: 600 10.5px/1.5 var(--mono); letter-spacing: .02em;
+ font: 600 var(--fs-micro)/1.5 var(--mono); letter-spacing: .02em;
}
.network-status-table .ifacebox-body { display: flex; flex-direction: column; gap: 14px; padding: 8px 16px 16px; }
.network-status-table .ifacebox-body > div:not(:empty) {
@@ -1474,7 +1659,7 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
white-space: normal;
}
.network-status-table .ifacebox .ifacebadge > img { width: 18px; height: 18px; margin-top: 1px; }
-.network-status-table .ifacebox .ifacebadge > span > .nowrap:first-child { display: block; margin-bottom: 4px; font: 600 13px/1.4 var(--font); }
+.network-status-table .ifacebox .ifacebadge > span > .nowrap:first-child { display: block; margin-bottom: 4px; font: 600 var(--fs-base)/1.4 var(--font); }
.network-status-table .ifacebox .ifacebadge > span > .nowrap:first-child > strong { display: none; }
/* zone colours arrive inline (--zone-color-rgb + background-color): tint instead */
@@ -1522,7 +1707,7 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
display: flex; align-items: center;
padding: 0 8px;
color: var(--text);
- font: 500 11.5px/1 var(--mono);
+ font: 500 var(--fs-micro)/1 var(--mono);
font-variant-numeric: tabular-nums;
white-space: nowrap;
z-index: 1;
@@ -1551,7 +1736,6 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
.alert-message.success { border-left-color: var(--ok); background: color-mix(in srgb, var(--ok-soft) 60%, var(--s1)); }
.alert-message.warning { border-left-color: var(--warn); background: color-mix(in srgb, var(--warn-soft) 70%, var(--s1)); }
.alert-message.error, .alert-message.danger { border-left-color: var(--err); background: color-mix(in srgb, var(--err-soft) 70%, var(--s1)); }
-.alert-message .btn { min-height: 28px; }
/* spinners */
.spinning { position: relative; }
@@ -1743,7 +1927,7 @@ body.modal-overlay-active #modal_overlay { display: block; }
margin: 0 0 var(--sp-4);
background: var(--s1);
border: 1px solid var(--line);
- border-radius: 10px;
+ border-radius: var(--r-lg);
box-shadow: var(--v-shadow);
overflow: clip;
}
@@ -1755,7 +1939,8 @@ body.modal-overlay-active #modal_overlay { display: block; }
.includes { margin-top: var(--sp-4); }
#view > .cbi-section:first-child { margin-top: 0; }
.cbi-section > div[style*="display: none"] + div { display: block; }
-.wifi-qr-code-content, .qr-code { background: #fff; padding: var(--sp-2); border-radius: var(--r); }
+/* the QR image gets a white tile; the "Generate QR" button (also .qr-code) stays a button */
+.wifi-qr-code-content, div.qr-code { background: #fff; padding: var(--sp-2); border-radius: var(--r); }
iframe { border: 1px solid var(--line); border-radius: var(--r); }
/* ------------------------------------------------------ 7b. stock charts
@@ -1798,7 +1983,7 @@ iframe { border: 1px solid var(--line); border-radius: var(--r); }
#view div[style*="height:300px"][style*="background"] svg text, #view div[id="channel_graph"] svg text {
fill: var(--v-chart-axis) !important;
text-shadow: none !important;
- font: 500 10.5px/1 var(--v-mono) !important;
+ font: 500 var(--fs-micro)/1 var(--v-mono) !important;
font-variant-numeric: tabular-nums;
letter-spacing: .02em;
paint-order: stroke; /* halo: labels stay legible over areas */
@@ -1821,7 +2006,7 @@ body[data-page^="admin-status-realtime"] #view svg polyline:is(#load05, #tx, #tc
/* "(4 minute window, 3 second interval)" */
body[data-page^="admin-status-realtime"] #view .cbi-section > br { display: none; }
body[data-page^="admin-status-realtime"] #view .cbi-section > div.right:has(> small[id^="scale"]) { margin: 6px 0 0; }
-body[data-page^="admin-status-realtime"] #view small[id^="scale"] { color: var(--v-chart-axis); font: 10.5px/1.4 var(--v-mono); letter-spacing: .02em; }
+body[data-page^="admin-status-realtime"] #view small[id^="scale"] { color: var(--v-chart-axis); font: var(--fs-micro)/1.4 var(--v-mono); letter-spacing: .02em; }
body[data-page^="admin-status-realtime"] #view .cbi-section > .table ~ div[style*="height:300px"] { margin-top: var(--sp-6); }
body[data-page^="admin-status-realtime"] #view .cbi-section-create:empty { display: none; }
@@ -1868,11 +2053,11 @@ body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-
}
body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:is(:nth-child(2), :nth-child(4), :nth-child(6)) {
font-family: var(--v-mono); font-variant-numeric: tabular-nums;
- color: var(--v-text-3); font-size: 11.5px;
+ color: var(--v-text-3); font-size: var(--fs-micro);
white-space: nowrap;
}
-body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:is(:nth-child(2), :nth-child(4), :nth-child(6))::first-line { color: var(--v-text-2); font-size: 12.5px; }
-body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:nth-child(2)::first-line { color: var(--v-text); font-weight: 650; font-size: 14px; }
+body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:is(:nth-child(2), :nth-child(4), :nth-child(6))::first-line { color: var(--v-text-2); font-size: var(--fs-sm); }
+body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:nth-child(2)::first-line { color: var(--v-text); font-weight: 650; font-size: var(--fs-md); }
body[data-page^="admin-status-realtime"] #view .tr:has(:is(#lb_load01_cur, #rx_bw_cur, #rssi_bw_cur, #lb_udp_cur)) { --key: var(--v-chart-1); }
body[data-page^="admin-status-realtime"] #view .tr:has(:is(#lb_load05_cur, #tx_bw_cur, #rate_bw_cur, #lb_tcp_cur)) { --key: var(--v-chart-2); }
body[data-page^="admin-status-realtime"] #view .tr:has(:is(#lb_load15_cur, #noise_bw_cur, #lb_otr_cur)) { --key: var(--v-chart-3); }
@@ -1887,22 +2072,24 @@ body[data-page^="admin-status-realtime"] #view #settings-row input { max-width:
#connections .tr > .th:first-child { padding-left: var(--card-px); }
#connections .tr:not(.table-titles) > .td:is(:nth-child(1), :nth-child(2)) {
color: var(--v-text-2);
- font: 600 10.5px/1.5 var(--v-mono); letter-spacing: .05em;
+ font: 600 var(--fs-micro)/1.5 var(--v-mono); letter-spacing: .05em;
}
-#connections .tr:not(.table-titles) > .td:is(:nth-child(3), :nth-child(4)) { font-family: var(--v-mono); font-size: 12px; overflow-wrap: break-word; }
+#connections .tr:not(.table-titles) > .td:is(:nth-child(3), :nth-child(4)) { font-family: var(--v-mono); font-size: var(--fs-sm); overflow-wrap: break-word; }
#connections .tr > .th:is(:nth-child(1), :nth-child(2)) { width: 9%; }
#connections .tr > .th:is(:nth-child(3), :nth-child(4)) { width: 31%; }
#connections .tr > .th:nth-child(5) { width: 20%; }
-#connections .tr:not(.table-titles) > .td:last-child { font: 12px var(--v-mono); font-variant-numeric: tabular-nums; color: var(--v-text-2); }
+#connections .tr:not(.table-titles) > .td:last-child { font: var(--fs-sm) var(--v-mono); font-variant-numeric: tabular-nums; color: var(--v-text-2); }
/* channel analysis: each tab is one card (graph over table) */
body[data-page^="admin-status-channel_analysis"] #view .cbi-title-section { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-4); }
body[data-page^="admin-status-channel_analysis"] #view .cbi-title-section > h2 { margin: 0; }
+/* the buttons keep the right edge when the title repeats the breadcrumb and is hidden */
+body[data-page^="admin-status-channel_analysis"] #view .cbi-title-section > .cbi-title-buttons { margin-left: auto; }
body[data-page^="admin-status-channel_analysis"] #view [data-tab-title]:not(.cbi-section) {
padding: var(--card-py) var(--card-px) 0;
background: var(--s1);
border: 1px solid var(--line);
- border-radius: 10px;
+ border-radius: var(--r-lg);
box-shadow: var(--v-shadow);
overflow: clip;
}
@@ -1938,7 +2125,7 @@ body[data-page^="admin-status-channel_analysis"] #view [data-tab-title]:not([dat
.v-ca-shape[data-v-own] { stroke-width: 2.25px; }
#view div[id="channel_graph"] svg text.v-ca-label {
fill: var(--v-text-2) !important;
- font: 500 11px/1 var(--v-font) !important;
+ font: 500 var(--fs-micro)/1 var(--v-font) !important;
letter-spacing: 0;
paint-order: stroke;
stroke: var(--s1); stroke-width: 3px; stroke-linejoin: round;
@@ -2068,7 +2255,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
padding: 10px 12px;
}
.cbi-section-table .ifacebox-body > br { display: none; }
-.cbi-section-table .ifacebox-body > small { flex-basis: 100%; color: var(--text-2); font: 12px/1.3 var(--mono); }
+.cbi-section-table .ifacebox-body > small { flex-basis: 100%; color: var(--text-2); font: var(--fs-sm)/1.3 var(--mono); }
/* "( dev dev dev )": drop the parentheses, group the member ports */
.cbi-section-table .ifacebox-body > span:not(.cbi-tooltip-container) {
display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px;
@@ -2087,11 +2274,11 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
[id$="-ifc-description"] > .nowrap {
display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 0 1 auto;
white-space: normal; overflow-wrap: anywhere;
- font: 12.5px/1.45 var(--mono); font-variant-numeric: tabular-nums slashed-zero;
+ font: var(--fs-sm)/1.45 var(--mono); font-variant-numeric: tabular-nums slashed-zero;
}
[id$="-ifc-description"] > .nowrap > strong, .td[data-name="_stat"] .nowrap > strong {
color: var(--text-3);
- font: 600 11.5px/1.5 var(--font); letter-spacing: .06em; text-transform: uppercase;
+ font: 600 var(--fs-micro)/1.5 var(--font); letter-spacing: .06em; text-transform: uppercase;
}
/* Network -> Wireless: radios as group headers, networks as rows */
@@ -2121,7 +2308,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.cbi-progressbar::before {
inset: auto 0 calc(100% + 5px) 0; padding: 0;
color: var(--text-2);
- font: 500 11.5px/1.2 var(--mono);
+ font: 500 var(--fs-micro)/1.2 var(--mono);
}
/* fused memory / storage meters (built by menu-vantage.js) */
@@ -2130,9 +2317,9 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.v-meter + .v-meter { margin-top: var(--sp-3); padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.v-meter-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
.v-meter-title { color: var(--text); font-weight: 600; }
-.v-meter-big { font: 650 22px/1 var(--mono); letter-spacing: -.02em; color: var(--text); }
+.v-meter-big { font: 650 var(--fs-xl)/1 var(--mono); letter-spacing: -.02em; color: var(--text); }
.v-meter-big small { margin-left: 6px; font: 500 var(--fs-sm) var(--font); letter-spacing: 0; color: var(--text-2); }
-.v-meter-sub { margin-left: auto; color: var(--text-2); font: 12px/1.4 var(--mono); font-variant-numeric: tabular-nums; }
+.v-meter-sub { margin-left: auto; color: var(--text-2); font: var(--fs-sm)/1.4 var(--mono); font-variant-numeric: tabular-nums; }
.v-meter-bar {
position: relative;
display: flex;
@@ -2155,16 +2342,16 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.v-seg.is-crit { background: var(--err); }
.v-meter-legend { display: flex; flex-wrap: wrap; gap: 6px 20px; margin: 0; padding: 0; list-style: none; font-size: var(--fs-sm); color: var(--text-2); }
.v-meter-legend li { display: inline-flex; align-items: center; gap: 7px; }
-.v-meter-legend b { color: var(--text); font: 600 12px var(--mono); font-variant-numeric: tabular-nums; }
+.v-meter-legend b { color: var(--text); font: 600 var(--fs-sm) var(--mono); font-variant-numeric: tabular-nums; }
.v-meter-legend em { color: var(--text-3); font-style: normal; font-size: var(--fs-micro); }
.v-key { width: 10px; height: 10px; border-radius: 2px; flex: none; }
.v-key.v-seg-free { background: var(--s3); box-shadow: inset 0 0 0 1px var(--line-2); }
.v-meter-rows { display: grid; grid-template-columns: minmax(120px, max-content) minmax(0, 1fr) max-content; gap: 14px 20px; padding: 4px 0 6px; }
.v-meter-row { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; grid-template-areas: "label bar val"; align-items: center; }
.v-meter-row > .v-meter-label { grid-area: label; color: var(--text-2); overflow-wrap: anywhere; }
-.v-meter-row > .v-meter-label small { display: block; color: var(--text-3); font: 11px var(--mono); }
+.v-meter-row > .v-meter-label small { display: block; color: var(--text-3); font: var(--fs-micro) var(--mono); }
.v-meter-row > .v-meter-bar { grid-area: bar; }
-.v-meter-row > .v-meter-val { grid-area: val; color: var(--text-2); font: 12px var(--mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
+.v-meter-row > .v-meter-val { grid-area: val; color: var(--text-2); font: var(--fs-sm) var(--mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.v-meter-row > .v-meter-val b { color: var(--text); font-weight: 600; }
.v-sr { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@@ -2230,7 +2417,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.vt-rail-link .vt-rail-label, html[data-rail="pinned"] .vt-rail-link .vt-rail-label {
display: block !important;
max-width: 100%;
- font-size: 10.5px; font-weight: 500;
+ font-size: var(--fs-micro); font-weight: 500;
text-align: center;
}
.vt-rail-item.is-active > .vt-rail-link::before { left: 14px; right: 14px; top: -6px; bottom: auto; width: auto; height: 2px; border-radius: 0 0 2px 2px; }
@@ -2286,7 +2473,31 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.cbi-value { grid-template-columns: minmax(0, 1fr); row-gap: 6px; }
.cbi-value > .cbi-value-title { padding-top: 0; }
.cbi-value > .cbi-value-field { grid-column: 1; }
- .vt-main input, .vt-main select, .vt-main textarea, .cbi-dropdown, .modal input, .modal select { font-size: 16px !important; }
+ .cbi-value[data-v-help] > .cbi-value-field { grid-row: 2; }
+ .cbi-value:has(> .vt-help:not([hidden])) > .cbi-value-title { align-self: center; }
+ /* stacked, there is nothing beside the title to line up with */
+ .cbi-value > .cbi-value-title > br:first-child { display: none; }
+ .cbi-value:has(> .cbi-value-title > br:first-child) > .vt-help { margin-top: 0; }
+ /* checkbox rows: box, label and (i) on one line, help below. The field
+ box dissolves so its children join the row grid; LuCI's nodes stay put. */
+ .cbi-value:has(> .cbi-value-field > .cbi-checkbox) {
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ column-gap: var(--sp-3);
+ align-items: center;
+ }
+ .cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field { display: contents; }
+ .cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > .cbi-checkbox { grid-row: 1; grid-column: 1; }
+ .cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-title { grid-row: 1; grid-column: 2; padding: 0; }
+ .cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .vt-help { grid-row: 1; grid-column: 3; }
+ .cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > :not(.cbi-checkbox) { grid-column: 1 / -1; }
+ /* a segmented control wraps its segments instead of hiding them */
+ .cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) { flex-wrap: wrap; width: auto; }
+ .cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) > li { flex: 1 1 auto; }
+ .cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) > li > a { justify-content: center; }
+ /* ... while an underline strip scrolls, like the page tabs */
+ html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) { flex-wrap: nowrap; }
+ html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li { flex: none; }
+ html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li > a { justify-content: normal; }
.cbi-dropdown, .vt-main input, .vt-main select, .control-group, .cbi-dynlist { max-width: none; }
/* tables become cards */
@@ -2345,6 +2556,16 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
.vt-pal-foot > span:not(.vt-pal-count) { display: none; }
}
+/* touch, at any width: 16px text in fields (iOS zooms below that),
+ bigger check marks, a little more air between rows */
+@media (pointer: coarse), (max-width: 720px) {
+ .vt-main input, .vt-main select, .vt-main textarea, .cbi-dropdown:not(.btn, .cbi-button), .modal input, .modal select { font-size: 16px !important; }
+}
+@media (pointer: coarse) {
+ input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
+ .cbi-value { padding: 8px 0; }
+}
+
/* ----------------------------------------------------------- motion */
/* phones: content in cards wraps instead of pushing the card wider */
diff --git a/luci-theme-vantage/po/templates/vantage-theme.pot b/luci-theme-vantage/po/templates/vantage-theme.pot
index 2166f8f95..2f3b41052 100644
--- a/luci-theme-vantage/po/templates/vantage-theme.pot
+++ b/luci-theme-vantage/po/templates/vantage-theme.pot
@@ -1,13 +1,13 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:918
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:951
msgid "Actions"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:914
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:915
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:916
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:947
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:948
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:949
msgid "Appearance"
msgstr ""
@@ -15,7 +15,7 @@ msgstr ""
msgid "Authorization Required"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125
msgid "Auto"
msgstr ""
@@ -23,52 +23,52 @@ msgstr ""
msgid "Breadcrumb"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269
msgid "Buffered"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:284
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294
msgid "Cache"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269
msgid "Cached"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:344
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:350
msgid "Channel"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:687
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:719
msgid "Collapse navigation"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1093
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1126
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:81
msgid "Colour theme"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:934
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:967
msgid "Command palette"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125
msgid "Dark"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:687
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:719
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:54
msgid "Expand navigation"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:916
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:949
msgid "Follow system theme"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:275
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:281
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:295
msgid "Free"
msgstr ""
@@ -76,9 +76,9 @@ msgstr ""
msgid "Home"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:283
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:287
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:293
msgid "In use"
msgstr ""
@@ -86,11 +86,11 @@ msgstr ""
msgid "Invalid username and/or password! Please try again."
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:930
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:963
msgid "Jump to a page or action…"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1092
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125
msgid "Light"
msgstr ""
@@ -99,7 +99,7 @@ msgstr ""
msgid "Log in"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:917
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:84
msgid "Log out"
msgstr ""
@@ -108,12 +108,12 @@ msgstr ""
msgid "Main navigation"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:258
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:273
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:264
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:279
msgid "Memory"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1005
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1038
msgid "No matching pages"
msgstr ""
@@ -125,57 +125,69 @@ msgstr ""
msgid "Password"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:793
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:825
msgid "Press"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:932
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:965
msgid "Results"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:930
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:963
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:74
msgid "Search pages"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:917
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
msgid "Session"
msgstr ""
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1283
+msgid "Show full description"
+msgstr ""
+
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1272
+msgid "Show full help"
+msgstr ""
+
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1272
+msgid "Show full help: %s"
+msgstr ""
+
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:44
msgid "Skip to content"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:310
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:316
msgid "Storage"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:296
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:298
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:304
msgid "Swap"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:263
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269
msgid "Swap free"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:275
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:281
msgid "Total"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:262
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:268
msgid "Total Available"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:914
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:947
msgid "Use dark theme"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:915
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:948
msgid "Use light theme"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:262
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:268
msgid "Used"
msgstr ""
@@ -183,54 +195,54 @@ msgstr ""
msgid "Username"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:337
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:343
msgid "Wireless"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:279
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:285
msgid "available"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:940
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:973
msgid "close"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:325
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:328
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334
msgid "free"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1007
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1040
msgid "items"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:938
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:971
msgid "navigate"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:939
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:972
msgid "open"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:773
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:805
msgid "page"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:773
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:805
msgid "pages"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:274
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:284
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294
msgid "reclaimable"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:793
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:825
msgid "to search all pages"
msgstr ""
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:325
-#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:328
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331
+#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334
msgid "used"
msgstr ""
diff --git a/miniupnpd/Makefile b/miniupnpd/Makefile
index cd55858e4..b51abee0d 100644
--- a/miniupnpd/Makefile
+++ b/miniupnpd/Makefile
@@ -9,13 +9,10 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=miniupnpd
PKG_VERSION:=2.3.11
-PKG_RELEASE:=10
+PKG_RELEASE:=11
-#PKG_SOURCE_URL:=https://github.com/miniupnp/miniupnp/releases/download/miniupnpd_$(subst .,_,$(PKG_VERSION))
-#PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
-#PKG_HASH:=66cb3c3d697ab2bb3a61d3c48628166d6ba328d7c2dbeb95898fdf2a3202af7b
-# Build from GitHub tag archives, as release archive differ in code comments to the public repository
-PKG_SOURCE_VERSION:=63ac33eaa49332cde15fd509b539a260b531da05
+# Build from GitHub archives, as release archives differ from Git in code comments; patches cannot be 1:1 applied
+PKG_SOURCE_VERSION:=f8c4f4c3320820e615b281147b3043f66c195f17
PKG_SOURCE_URL:=https://codeload.github.com/miniupnp/miniupnp/tar.gz/$(PKG_SOURCE_VERSION)?
PKG_SOURCE:=miniupnp-$(PKG_SOURCE_VERSION).tar.gz
PKG_HASH:=skip
diff --git a/miniupnpd/files/miniupnpd.init b/miniupnpd/files/miniupnpd.init
index e9bfcfddf..ed5cac922 100644
--- a/miniupnpd/files/miniupnpd.init
+++ b/miniupnpd/files/miniupnpd.init
@@ -126,7 +126,7 @@ upnpd_generate_config() {
log "IPv6 not ready yet; delay start"
sleep 5
pass=$((pass + 1))
- [ "$pass" = "4" ] && log "IPv6 GUA not yet available, UPnP IGD mapping not possible" && break
+ [ "$pass" = "2" ] && log "IPv6 GUA not yet available, UPnP IGDv2 IPv6 mapping may not work" && break
done
fi
# Only perform an STUN CGNAT test if necessary, with a private/CGNAT-reserved external IPv4
diff --git a/miniupnpd/files/upnpd.config b/miniupnpd/files/upnpd.config
index 70aa5cf8b..bd33fe256 100644
--- a/miniupnpd/files/upnpd.config
+++ b/miniupnpd/files/upnpd.config
@@ -8,8 +8,9 @@ config upnpd 'settings'
# Enable networks, multiple can be specified, separated by a space
option internal_iface 'lan'
option access_defaults 'accept-high-ports' # default none
+ #option accept_ports ''
+ #option reject_ports '21 23 135 137-139 445 3389'
option check_acl '0' # default 1
- #option ipv6_disable '0'
#option allow_third_party_mapping '0'
# Extra logging by setting to info or debug (previously 1)
option log_output 'default'
diff --git a/miniupnpd/patches/10-upnp-igdv2-compat.patch b/miniupnpd/patches/010-upnp-igdv2-compat.patch
similarity index 98%
rename from miniupnpd/patches/10-upnp-igdv2-compat.patch
rename to miniupnpd/patches/010-upnp-igdv2-compat.patch
index 03a73a5bc..ab593ab0a 100644
--- a/miniupnpd/patches/10-upnp-igdv2-compat.patch
+++ b/miniupnpd/patches/010-upnp-igdv2-compat.patch
@@ -20,7 +20,7 @@ Link: https://github.com/Self-Hosting-Group/wiki/wiki/Port-Mapping-Protocols-Ove
--- a/upnphttp.c
+++ b/upnphttp.c
-@@ -319,9 +319,22 @@ ParseHttpHeaders(struct upnphttp * h)
+@@ -321,9 +321,22 @@ ParseHttpHeaders(struct upnphttp * h)
}
else if(strncasecmp(line, "user-agent:", 11) == 0)
{
diff --git a/miniupnpd/patches/020-improve-logging.patch b/miniupnpd/patches/020-improve-logging.patch
new file mode 100644
index 000000000..35728a038
--- /dev/null
+++ b/miniupnpd/patches/020-improve-logging.patch
@@ -0,0 +1,2454 @@
+From 4f5fdc10d22b6a49f38f4d07cc90d691d3a11d92 Mon Sep 17 00:00:00 2001
+From: Self-Hosting-Group
+ <155233284+Self-Hosting-Group@users.noreply.github.com>
+Date: Mon, 12 Oct 2026 00:00:00 +0000
+Subject: [PATCH] miniupnpd: Improve logging and CGNAT use
+
+* Clearer logging of enabled protocols/ports, IPv6 mapping, and UPnP IGD
+ compatibility mode… in start banner
+* Log warnings with an enabled allow third-party mapping option
+* Change the log level (less verbose) of many internal or normally
+ occurring messages, and some error messages, without functional
+ restrictions, to DEBUG, client requests/actions and their errors to
+ INFO, and only daemon-wide messages to NOTICE/WARNING/ERR…
+* Comment out some log-filling messages when internal interfaces lose
+ link, and for e.g. `rule with label '%s' is not a IGD pinhole`, as
+ normally occurring
+* Log IPv4 mapping disabled/enabled messages by default
+* Fix IPv4 mapping re-enable after external interface down/up with a
+ CGNAT option set; handling was previously not entirely implemented for
+ both options, incl. miniupnp/miniupnp@bbdd738, miniupnp/miniupnp#905
+* No IPv4 mapping disabled logging on requests, but more cases
+* Clearer IPv4 mapping disabled/enabled logging
+* Add `default_min_port`/max options for IPv6/IPv4
+* Standardise request/action logging with `LOG_INFO`
+
+Fixes: https://github.com/openwrt/packages/issues/11971
+Fixes: https://github.com/openwrt/packages/issues/17601
+Fixes: https://github.com/openwrt/packages/issues/26483
+Link: https://github.com/miniupnp/miniupnp/pull/764
+---
+ INSTALL | 45 +++---
+ asyncsendto.c | 4 +-
+ getifaddr.c | 4 +-
+ minissdp.c | 24 +--
+ miniupnpd.c | 244 ++++++++++++++++++------------
+ miniupnpd.conf | 258 +++++++++++++++++---------------
+ natpmp.c | 39 ++---
+ netfilter/iptcrdr.c | 13 +-
+ netfilter/iptpinhole.c | 10 +-
+ netfilter_nft/nftnlrdr.c | 4 +-
+ netfilter_nft/nftnlrdr_misc.c | 8 +-
+ netfilter_nft/nftpinhole.c | 16 +-
+ options.c | 7 +-
+ options.h | 6 +-
+ pcpserver.c | 100 ++++++++-----
+ pf/obsdrdr.c | 2 +-
+ pf/pfpinhole.c | 12 +-
+ upnpdescgen.c | 2 +-
+ upnpglobalvars.h | 2 +-
+ upnphttp.c | 6 +-
+ upnppermissions.c | 14 +-
+ upnppermissions.h | 3 +
+ upnppinhole.c | 16 +-
+ upnpredirect.c | 32 ++--
+ upnpsoap.c | 107 +++++++------
+ upnpstun.c | 37 ++---
+ 26 files changed, 554 insertions(+), 461 deletions(-)
+
+--- a/INSTALL
++++ b/INSTALL
+@@ -156,7 +156,7 @@ ext_ifname : WAN network interface (in
+ listening_ip : LAN network interface (network where to supply NAT traversal)
+ enable_pcp_pmp=yes
+ enable_upnp=yes
+-and the permission rules (see below).
++and the access control settings (see below).
+
+ Historically, LAN had to be specified by IP/mask, such as
+ listening_ip=192.168.0.1/24
+@@ -168,42 +168,31 @@ the wrong mask would be picked. You can
+ names :
+ listening_ip=eth0/24
+
+-Some users want to use miniupnpd on a NAT router which is connected to the
+-internet through another NAT router (NAT behind NAT). This kind of setup is
+-strongly discouraged. By default, miniupnpd will refuses to launch when
+-detecting a private (RFC1918) "WAN" IP address. The ext_ip option sould be
+-added :
+-ext_ifname=eth1 # "WAN" network interface, whose IP could be 192.168.1.22
+-ext_ip=80.1.2.3 # Real public IP address
+-listening_ip=eth0
+-Please note that miniupnpd doesn't redirect any port on the other NAT router.
+-UPNP requests forwarding is not implemented.
+-In such NAT behind NAT setups, it is also possible to use STUN :
+-See the ext_perform_stun / ext_stun_host / ext_stun_port options.
+-If you know what you do and want to disable the restriction on the "WAN" IP
+-address, you can use the option :
+-ext_allow_private_ipv4=yes
+-
+ it is also possible to set a different interface for IPv6 WAN
+ ext_ifname=eth0
+ ext_ifname6=sit0
+
++Allow CGNAT use
++To enable IPv4 mapping even when using a CGNAT with a private/CGNAT-reserved
++external IPv4 address, users should refer to the documentation for the
++ext_allow_cgnat option in the sample miniupnpd.conf config file
++
+ miniupnpd supports some kind of security check for allowing or disallowing
+-redirection to be made. The UPnP permission rules are read from the
++port maps to be made. Access control list (ACL) entries are read from the
+ miniupnpd.conf configuration file.
+-When a new redirection is requested, permission rules are evaluated in
+-top-down order and the first permission rule matched gives the response :
+-redirection allowed or denied. If no rule is matching, the redirection is
+-allowed, so it is a good practice to have a "catch all" deny permission
+-rule at the end of your permission ruleset.
+-Sample permission ruleset :
++When a new port map is requested, ACL entries are evaluated in
++top-down order and the first ACL entry matched gives the response :
++port map allowed or denied. If no entry is matching, the port map is
++allowed, so it is a good practice to have a "catch all" deny ACL entry
++at the end of your ACL.
++Sample ACL:
+ allow 4662-4672 192.168.1.34/32 4662-4672
+-deny 0-65535 192.168.1.34/32 0-65535
++deny 1-65535 192.168.1.34 1-65535
+ allow 1024-65535 192.168.1.0/24 1024-65535
+-deny 0-65535 0.0.0.0/0 0-65535
+-With this ruleset, redirections are allowed only for host on the subnet
++deny 1-65535 0.0.0.0/0 1-65535
++With this ACL, port maps are allowed only for host on the subnet
+ 192.168.1.0/255.255.255.0 for the ports 1024 or above. There is an exception
+-for the host 192.168.1.34 for which only redirections from/to port 4662 to
++for the host 192.168.1.34 for which only port maps from/to port 4662 to
+ 4672 are allowed.
+
+ You can generate the uuid for your UPnP device with the uuidgen available
+--- a/asyncsendto.c
++++ b/asyncsendto.c
+@@ -254,9 +254,9 @@ int try_sendto(fd_set * writefds)
+ /* uncatched error */
+ if(sockaddr_to_string(elt->dest_addr, addr_str, sizeof(addr_str)) <= 0)
+ addr_str[0] = '\0';
+- syslog(LOG_ERR, "%s(sock=%d, len=%u, dest=%s): sendto: %m",
++ /*syslog(LOG_DEBUG, "%s(sock=%d, len=%u, dest=%s): sendto: %m",
+ "try_sendto", elt->sockfd, (unsigned)elt->len,
+- addr_str);
++ addr_str);*/
+ ret--;
+ }
+ } else if((int)n != (int)elt->len) {
+--- a/getifaddr.c
++++ b/getifaddr.c
+@@ -74,7 +74,7 @@ getifaddr(const char * ifname, char * bu
+ } else {
+ r = GETIFADDR_IOCTL_ERROR;
+ }
+- syslog(LOG_ERR, "ioctl(s, SIOCGIFADDR, ...): %m");
++ syslog(LOG_DEBUG, "ioctl(s, SIOCGIFADDR, ...): %m");
+ close(s);
+ return r;
+ }
+@@ -149,7 +149,7 @@ getifaddr(const char * ifname, char * bu
+ if(addr) *addr = ((struct sockaddr_in *)candidate->ifa_addr)->sin_addr;
+ if(mask) *mask = ((struct sockaddr_in *)candidate->ifa_netmask)->sin_addr;
+ } else {
+- syslog(LOG_WARNING, "no AF_INET address found for %s", ifname);
++ syslog(LOG_DEBUG, "no AF_INET address found for %s", ifname);
+ freeifaddrs(ifap);
+ return GETIFADDR_NO_ADDRESS;
+ }
+--- a/minissdp.c
++++ b/minissdp.c
+@@ -743,8 +743,8 @@ SendSSDPNotify(int s, const struct socka
+ }
+ n = sendto_or_schedule(s, bufr, l, 0, dest, dest_len);
+ if(n < 0) {
+- syslog(LOG_ERR, "sendto(udp_notify=%d, %s): %m", s,
+- host ? host : "NULL");
++ /*syslog(LOG_DEBUG, "sendto(udp_notify=%d, %s): %m", s,
++ host ? host : "NULL");*/
+ } else if(n != l) {
+ syslog(LOG_NOTICE, "sendto() sent %d out of %d bytes", n, l);
+ }
+@@ -754,8 +754,8 @@ SendSSDPNotify(int s, const struct socka
+ * discovery messages SHOULD NOT be sent more than three times. */
+ n = sendto_schedule(s, bufr, l, 0, dest, dest_len, 250);
+ if(n < 0) {
+- syslog(LOG_ERR, "sendto(udp_notify=%d, %s): %m", s,
+- host ? host : "NULL");
++ /*syslog(LOG_DEBUG, "sendto(udp_notify=%d, %s): %m", s,
++ host ? host : "NULL");*/
+ }
+ }
+
+@@ -1060,7 +1060,7 @@ ProcessSSDPData(int s, const char *bufr,
+ }
+ if(lan_addr == NULL)
+ {
+- syslog(LOG_WARNING, "SSDP packet sender %s (if_index=%d) not from a LAN, ignoring",
++ syslog(LOG_DEBUG, "SSDP packet sender %s (if_index=%d) not from a LAN, ignoring",
+ sender_str, source_if);
+ return;
+ }
+@@ -1175,7 +1175,7 @@ ProcessSSDPData(int s, const char *bufr,
+ sender_str );*/
+ if(st && (st_len > 0))
+ {
+- syslog(LOG_INFO, "SSDP M-SEARCH from %s ST: %.*s",
++ syslog(LOG_DEBUG, "SSDP M-SEARCH from %s ST: %.*s",
+ sender_str, st_len, st);
+ /* find in which sub network the client is */
+ #ifdef ENABLE_IPV6
+@@ -1288,7 +1288,7 @@ ProcessSSDPData(int s, const char *bufr,
+ else
+ snprintf(ver_str, sizeof(ver_str), "%d", known_service_types[i].version);
+ #endif
+- syslog(LOG_INFO, "Single search found");
++ syslog(LOG_DEBUG, "Single search found");
+ #ifdef DELAY_MSEARCH_RESPONSE
+ delay = random() / (1 + RAND_MAX / (1000 * mx_value));
+ #ifdef DEBUG
+@@ -1317,7 +1317,7 @@ ProcessSSDPData(int s, const char *bufr,
+ #ifdef DELAY_MSEARCH_RESPONSE
+ unsigned int delay_increment = (mx_value * 1000) / 15;
+ #endif
+- syslog(LOG_INFO, "ssdp:all found");
++ syslog(LOG_DEBUG, "ssdp:all found");
+ for(i=0; known_service_types[i].s; i++)
+ {
+ #ifdef DELAY_MSEARCH_RESPONSE
+@@ -1375,7 +1375,7 @@ ProcessSSDPData(int s, const char *bufr,
+ #endif
+ if(0 == memcmp(st, uuidvalue_igd, l))
+ {
+- syslog(LOG_INFO, "ssdp:uuid (IGD) found");
++ syslog(LOG_DEBUG, "ssdp:uuid (IGD) found");
+ SendSSDPResponse(s, sender, st, st_len, "",
+ announced_host, http_port,
+ #ifdef ENABLE_HTTPS
+@@ -1385,7 +1385,7 @@ ProcessSSDPData(int s, const char *bufr,
+ }
+ else if(0 == memcmp(st, uuidvalue_wan, l))
+ {
+- syslog(LOG_INFO, "ssdp:uuid (WAN) found");
++ syslog(LOG_DEBUG, "ssdp:uuid (WAN) found");
+ SendSSDPResponse(s, sender, st, st_len, "",
+ announced_host, http_port,
+ #ifdef ENABLE_HTTPS
+@@ -1395,7 +1395,7 @@ ProcessSSDPData(int s, const char *bufr,
+ }
+ else if(0 == memcmp(st, uuidvalue_wcd, l))
+ {
+- syslog(LOG_INFO, "ssdp:uuid (WCD) found");
++ syslog(LOG_DEBUG, "ssdp:uuid (WCD) found");
+ SendSSDPResponse(s, sender, st, st_len, "",
+ announced_host, http_port,
+ #ifdef ENABLE_HTTPS
+@@ -1456,7 +1456,7 @@ SendSSDPbyebye(int s, const struct socka
+ n = sendto_or_schedule(s, bufr, l, 0, dest, destlen);
+ if(n < 0)
+ {
+- syslog(LOG_ERR, "sendto(udp_shutdown=%d) to %s: %m", s, dest_str);
++ syslog(LOG_DEBUG, "sendto(udp_shutdown=%d) to %s: %m", s, dest_str);
+ return -1;
+ }
+ else if(n != l)
+--- a/miniupnpd.c
++++ b/miniupnpd.c
+@@ -495,7 +495,7 @@ ProcessIncomingHTTP(int shttpl, const ch
+ if(get_lan_for_peer((struct sockaddr *)&clientname) == NULL)
+ {
+ /* The peer is not a LAN ! */
+- syslog(LOG_WARNING,
++ syslog(LOG_DEBUG,
+ "%s peer %s is not from a LAN, closing the connection",
+ protocol, addr_str);
+ close(shttp);
+@@ -866,7 +866,7 @@ set_startup_time(void)
+ }
+ else
+ {
+- syslog(LOG_INFO, "system uptime is %lu seconds", uptime);
++ syslog(LOG_DEBUG, "system uptime is %lu seconds", uptime);
+ }
+ fclose(f);
+ startup_time -= uptime;
+@@ -1059,11 +1059,11 @@ parselanaddr(struct lan_addr_s * lan_add
+ return -1;
+ }
+ if(addr_is_reserved(&lan_addr->ext_ip_addr)) {
+- if (GETFLAG(ALLOWPRIVATEIPV4MASK)) {
+- syslog(LOG_WARNING, "IGNORED : option ext_ip address contains reserved / private address : %s", lan_addr->ext_ip_str);
++ if (GETFLAG(REPORTPRIVATEIPV4MASK)) {
++ syslog(LOG_WARNING, "WARNING: IPv4 mapping enabled forcibly, as ext_allow_cgnat=report-private-ipv4 set; check security note if not set");
+ } else {
+ /* error */
+- INIT_PRINT_ERR("Error: option ext_ip address contains reserved / private address : %s\n", lan_addr->ext_ip_str);
++ INIT_PRINT_ERR("Option ext_ip set to private/CGNAT-reserved (%s) IPv4, exiting\n", lan_addr->ext_ip_str);
+ return -1;
+ }
+ }
+@@ -1117,20 +1117,19 @@ parselan_error:
+
+ static char ext_addr_str[INET_ADDRSTRLEN];
+
+-int update_ext_ip_addr_from_stun(int init)
++int update_ext_ip_addr_from_stun(void)
+ {
+ struct in_addr if_addr, ext_addr;
+- int restrictive_nat;
++ int restrictive_nat, reserved;
+ char if_addr_str[INET_ADDRSTRLEN];
+
+- syslog(LOG_INFO, "STUN: Performing with host=%s and port=%u ...", ext_stun_host, (unsigned)ext_stun_port);
+-
+ if (getifaddr(ext_if_name, if_addr_str, INET_ADDRSTRLEN, &if_addr, NULL) < 0) {
+- syslog(LOG_ERR, "STUN: Cannot get IP address for ext interface %s", ext_if_name);
++ syslog(LOG_ERR, "Detected no IPv4 address on external network interface %s", ext_if_name);
+ return 1;
+ }
++ syslog(LOG_INFO, "STUN: Detecting public IPv4 and testing for unrestricted endpoint-independent (1:1) CGNAT via %s:%u...", ext_stun_host, ext_stun_port ? ext_stun_port : 3478);
+ if (perform_stun(ext_if_name, if_addr_str, ext_stun_host, ext_stun_port, &ext_addr, &restrictive_nat) != 0) {
+- syslog(LOG_ERR, "STUN: Performing STUN failed: %s", strerror(errno));
++ syslog(LOG_ERR, "STUN: Performing test failed");
+ return 1;
+ }
+ if (!inet_ntop(AF_INET, &ext_addr, ext_addr_str, sizeof(ext_addr_str))) {
+@@ -1138,32 +1137,29 @@ int update_ext_ip_addr_from_stun(int ini
+ return 1;
+ }
+
+- if ((init || disable_port_forwarding) && !restrictive_nat) {
+- if (addr_is_reserved(&if_addr))
+- syslog(LOG_INFO, "STUN: ext interface %s with IP address %s is now behind unrestricted full-cone NAT 1:1 with public IP address %s and firewall does not block incoming connections set by miniupnpd", ext_if_name, if_addr_str, ext_addr_str);
+- else
+- syslog(LOG_INFO, "STUN: ext interface %s has now public IP address %s and firewall does not block incoming connections set by miniupnpd", ext_if_name, if_addr_str);
+- syslog(LOG_INFO, "Port forwarding is now enabled");
+- } else if ((init || !disable_port_forwarding) && restrictive_nat) {
+- if (addr_is_reserved(&if_addr)) {
+- syslog(LOG_WARNING, "STUN: ext interface %s with private IP address %s is now possibly behind restrictive or symmetric NAT with public IP address %s which does not support port forwarding", ext_if_name, if_addr_str, ext_addr_str);
+- syslog(LOG_WARNING, "NAT on upstream router blocks incoming connections set by miniupnpd");
+- syslog(LOG_WARNING, "Turn off NAT on upstream router or change it to full-cone NAT 1:1 type");
++ reserved = addr_is_reserved(&if_addr);
++ if (!restrictive_nat && !reserved) {
++ syslog(LOG_INFO, "STUN: Detected unrestricted public external IPv4 %s on %s", if_addr_str, ext_if_name);
++ } else if (!restrictive_nat && reserved) {
++ syslog(LOG_INFO, "STUN: Detected unrestricted endpoint-independent (1:1) CGNAT with public IPv4 %s and %s as external on %s", ext_addr_str, if_addr_str, ext_if_name);
++ } else if (restrictive_nat && !reserved) {
++ syslog(LOG_INFO, "STUN: Detected restricted public external IPv4 %s on %s, IPv4 mapping may not work", if_addr_str, ext_if_name);
++ } else if (restrictive_nat && reserved) {
++ syslog(LOG_INFO, "STUN: Detected restricted or address- and port-dependent (symmetric) CGNAT with public IPv4 %s and %s as external on %s, IPv4 mapping may not work", ext_addr_str, if_addr_str, ext_if_name);
++ }
++
++ use_ext_ip_addr = ext_addr_str;
++ if (!restrictive_nat || GETFLAG(ALLOWFILTEREDSTUNMASK)) {
++ if (!restrictive_nat) {
++ syslog(LOG_NOTICE, "IPv4 mapping enabled, and reachability/non-filtering tested via STUN");
+ } else {
+- syslog(LOG_WARNING, "STUN: ext interface %s has now public IP address %s but firewall filters incoming connections set by miniunnpd", ext_if_name, if_addr_str);
+- syslog(LOG_WARNING, "Check configuration of firewall on local machine and also on upstream router");
+- }
+- if (!GETFLAG(ALLOWFILTEREDSTUNMASK)) {
+- syslog(LOG_WARNING, "Port forwarding is now disabled");
+- syslog(LOG_WARNING, "Set ext_perform_stun=allow-filtered if you still want to use port forwarding in current situation");
++ syslog(LOG_WARNING, "WARNING: IPv4 mapping enabled forcibly, as ext_allow_cgnat=allow-filtered set; check security note if not set");
+ }
++ disable_port_forwarding = 0;
+ } else {
+- syslog(LOG_INFO, "STUN: ... done");
++ syslog(LOG_WARNING, "IPv4 mapping disabled, as own/upstream router filters incoming, or there is an address- and port-dependent (symmetric) CGNAT; to workaround current daemon limitation, or enable it anyway, set ext_allow_cgnat=allow-filtered; check security note if not set");
++ disable_port_forwarding = 1;
+ }
+-
+- use_ext_ip_addr = ext_addr_str;
+- if (!GETFLAG(ALLOWFILTEREDSTUNMASK))
+- disable_port_forwarding = restrictive_nat;
+ return 0;
+ }
+
+@@ -1183,32 +1179,41 @@ static void update_disable_port_forwardi
+ syslog(LOG_WARNING, "ext interface %s is down", ext_if_name);
+ break;
+ case GETIFADDR_NO_ADDRESS:
+- syslog(LOG_WARNING, "ext interface %s has no IPv4 address. Network is down", ext_if_name);
++ syslog(LOG_ERR, "Detected no IPv4 address on external network interface %s", ext_if_name);
+ break;
+ default:
+- syslog(LOG_ERR, "Error getting IPv4 address for ext interface %s. Network is down", ext_if_name);
++ syslog(LOG_ERR, "Detected no IPv4 address on external network interface %s", ext_if_name);
+ }
++ syslog(LOG_WARNING, "IPv4 mapping disabled");
+ disable_port_forwarding = 1;
++
+ } else {
+ int reserved = addr_is_reserved(&addr);
+- if (!disable_port_forwarding && reserved) {
+- if (GETFLAG(ALLOWPRIVATEIPV4MASK)) {
+- syslog(LOG_WARNING, "IGNORED : Reserved / private IP address %s on ext interface %s", if_addr, ext_if_name);
+- } else {
+- syslog(LOG_WARNING, "Reserved / private IP address %s on ext interface %s: Port forwarding is impossible", if_addr, ext_if_name);
+- syslog(LOG_INFO, "You are probably behind NAT, enable option ext_perform_stun=yes to detect public IP address");
+- syslog(LOG_INFO, "Or use ext_ip= / -o option to declare public IP address");
+- syslog(LOG_INFO, "In case that miniupnpd is thinking that it's behind symmetric NAT while it actually is full-cone");
+- syslog(LOG_INFO, "You can set option ext_allow_private_ipv4=yes to enable port forwarding");
+- syslog(LOG_INFO, "But you may still need to configure stun server or ext_ip to make it work correctly");
+- syslog(LOG_INFO, "Public IP address is required by UPnP/PCP/PMP protocols and clients do not work without it");
+- disable_port_forwarding = 1;
+- }
+- } else if (disable_port_forwarding &&
+- (!reserved || GETFLAG(ALLOWPRIVATEIPV4MASK))) {
+- syslog(LOG_INFO, "%s IP address %s on ext interface %s: Port forwarding is enabled",
+- reserved ? "Reserved / private" : "Public", if_addr, ext_if_name);
++ if (!reserved) {
++ syslog(LOG_INFO, "Detected public external IPv4 %s on %s", if_addr, ext_if_name);
++ } else {
++ syslog(LOG_INFO, "Detected private/CGNAT-reserved external IPv4 %s on %s, IPv4 mapping may not work", if_addr, ext_if_name);
++ }
++
++ if (!reserved || GETFLAG(REPORTPRIVATEIPV4MASK)) {
++ if (!reserved) {
++ syslog(LOG_NOTICE, "IPv4 mapping enabled");
++ } else {
++ syslog(LOG_WARNING, "WARNING: IPv4 mapping enabled forcibly, as ext_allow_cgnat=report-private-ipv4 set; check compatibility/security notes if not set");
++ }
+ disable_port_forwarding = 0;
++ } else {
++ syslog(LOG_WARNING, "IPv4 mapping disabled by default, as private/CGNAT-reserved external IPv4 detected. Prevented for:");
++ syslog(LOG_WARNING, "- Info, as port maps not reachable via internet without an unrestricted endpoint-independent (1:1) IPv4 CGNAT, but possibly from within");
++ syslog(LOG_WARNING, "- Compatibility, as many UPnP IGD & PCP/NAT-PMP clients require a returned public external IPv4 address");
++ syslog(LOG_WARNING, "- Security, to prevent access via internet if the external/internal network interface has been configured swapped");
++ syslog(LOG_WARNING, "To enable IPv4 mapping in this case, select a priority-listed option:");
++ syslog(LOG_WARNING, "A) Detect public external IPv4 and test for unrestricted endpoint-independent (1:1) CGNAT via STUN server, set ext_allow_cgnat=yes");
++ syslog(LOG_WARNING, "B) As A, but workaround filtered CGNAT result daemon limitation (without extra firewall rule), set ext_allow_cgnat=allow-filtered; check security note");
++ syslog(LOG_WARNING, "C) Manually override reported external IPv4 with public, set ext_ip; check security note");
++ syslog(LOG_WARNING, "D) Report private/CGNAT-reserved external IPv4 to clients, set ext_allow_cgnat=report-private-ipv4; check compatibility/security notes");
++ syslog(LOG_WARNING, "IPv4 mapping disabled");
++ disable_port_forwarding = 1;
+ }
+ }
+ }
+@@ -1458,11 +1463,7 @@ init(int argc, char * * argv, struct run
+ case UPNPEXT_IP:
+ use_ext_ip_addr = ary_options[i].value;
+ break;
+- case UPNPEXT_ALLOW_PRIVATE_IPV4:
+- if(strcmp(ary_options[i].value, "yes") == 0)
+- SETFLAG(ALLOWPRIVATEIPV4MASK);
+- break;
+- case UPNPEXT_PERFORM_STUN:
++ case UPNPEXT_ALLOW_CGNAT:
+ if(strcmp(ary_options[i].value, "yes") == 0)
+ SETFLAG(PERFORMSTUNMASK);
+ else if(strcmp(ary_options[i].value, "allow-filtered") == 0)
+@@ -1470,6 +1471,14 @@ init(int argc, char * * argv, struct run
+ SETFLAG(PERFORMSTUNMASK);
+ SETFLAG(ALLOWFILTEREDSTUNMASK);
+ }
++ else if(strcmp(ary_options[i].value, "report-private-ipv4") == 0)
++ {
++ SETFLAG(REPORTPRIVATEIPV4MASK);
++ }
++ break;
++ case UPNPEXT_REPORT_PRIVATE_IPV4:
++ if(strcmp(ary_options[i].value, "yes") == 0)
++ SETFLAG(REPORTPRIVATEIPV4MASK);
+ break;
+ case UPNPEXT_STUN_HOST:
+ ext_stun_host = ary_options[i].value;
+@@ -1509,6 +1518,12 @@ init(int argc, char * * argv, struct run
+ SETFLAG(IPV6DISABLEDMASK);
+ break;
+ #endif /* ENABLE_IPV6 */
++ case DEFAULT_MIN_PORT:
++ default_min_port = atoi(ary_options[i].value);
++ break;
++ case DEFAULT_MAX_PORT:
++ default_min_port = atoi(ary_options[i].value);
++ break;
+ case UPNPPORT:
+ v->port = atoi(ary_options[i].value);
+ break;
+@@ -1696,7 +1711,7 @@ init(int argc, char * * argv, struct run
+ }
+ #endif /* ENABLE_PCP */
+ if (GETFLAG(PERFORMSTUNMASK) && !ext_stun_host) {
+- INIT_PRINT_ERR("You must specify ext_stun_host= when ext_perform_stun=yes\n");
++ INIT_PRINT_ERR("You must specify ext_stun_host= when ext_allow_cgnat=yes/allow-filtered\n");
+ return 1;
+ }
+ }
+@@ -2083,7 +2098,7 @@ init(int argc, char * * argv, struct run
+ #endif
+
+ if (use_ext_ip_addr && GETFLAG(PERFORMSTUNMASK)) {
+- INIT_PRINT_ERR("Error: options ext_ip= and ext_perform_stun=yes cannot be specified together\n");
++ INIT_PRINT_ERR("Error: options ext_ip= and ext_allow_cgnat=yes/allow-filtered cannot be specified together\n");
+ return 1;
+ }
+
+@@ -2093,10 +2108,10 @@ init(int argc, char * * argv, struct run
+ return 1;
+ }
+ if (addr_is_reserved(&addr)) {
+- if (GETFLAG(ALLOWPRIVATEIPV4MASK)) {
+- syslog(LOG_WARNING, "IGNORED : option ext_ip contains reserved / private address %s, not public routable", use_ext_ip_addr);
++ if (GETFLAG(REPORTPRIVATEIPV4MASK)) {
++ syslog(LOG_WARNING, "WARNING: IPv4 mapping enabled forcibly, as ext_allow_cgnat=report-private-ipv4 set; check compatibility/security notes if not set");
+ } else {
+- INIT_PRINT_ERR("Error: option ext_ip contains reserved / private address %s, not public routable\n", use_ext_ip_addr);
++ INIT_PRINT_ERR("Option ext_ip set to private/CGNAT-reserved (%s) IPv4, exiting\n", use_ext_ip_addr);
+ return 1;
+ }
+ }
+@@ -2229,6 +2244,25 @@ init(int argc, char * * argv, struct run
+ pidfilename = NULL;
+ #endif
+
++ syslog(LOG_NOTICE, "MiniUPnP daemon " MINIUPNPD_VERSION " starting, enabled protocols %s%s%s, ext_ifname=%s BOOTID=%u",
++ GETFLAG(ENABLEUPNPMASK) ? "UPnP IGD" : "",
++#ifdef ENABLE_NATPMP
++ GETFLAG(ENABLEUPNPMASK) && GETFLAG(ENABLENATPMPMASK) ? " & " : "",
++#ifdef ENABLE_PCP
++ GETFLAG(ENABLENATPMPMASK) ? "PCP/NAT-PMP" : "",
++#else
++ GETFLAG(ENABLENATPMPMASK) ? "NAT-PMP" : "",
++#endif
++#else
++ "", "",
++#endif
++ ext_if_name, upnp_bootid);
++ syslog(LOG_INFO, "More information at https://miniupnp.tuxfamily.org or http://miniupnp.free.fr");
++ syslog(LOG_NOTICE, "Extra logging with log level info (-v) or debug (-v -v)");
++
++ syslog(LOG_INFO, "Options default_min/max_port set to %d/%d apply to IPv6 and to IPv4 only, with no matching or default reject ACL entries",
++ default_min_port, default_max_port);
++
+ #ifdef USE_SYSTEMD
+ if (systemd_flag) {
+ int r = sd_notify(0,
+@@ -2240,9 +2274,10 @@ init(int argc, char * * argv, struct run
+ #endif
+
+ #ifdef ENABLE_LEASEFILE
+- /*remove(lease_file);*/
+- syslog(LOG_INFO, "Reloading rules from lease file");
+- reload_from_lease_file();
++ if (lease_file && lease_file[0] != '\0') {
++ syslog(LOG_INFO, "Reloading port maps from lease file %s", lease_file);
++ reload_from_lease_file();
++ }
+ #ifdef ENABLE_UPNPPINHOLE
+ reload_from_lease_file6();
+ #endif
+@@ -2432,28 +2467,16 @@ main(int argc, char * * argv)
+ goto shutdown;
+ }
+
+- syslog(LOG_INFO, "version " MINIUPNPD_VERSION " starting%s%sext if %s BOOTID=%u",
+-#ifdef ENABLE_NATPMP
+-#ifdef ENABLE_PCP
+- GETFLAG(ENABLENATPMPMASK) ? " NAT-PMP/PCP " : " ",
+-#else
+- GETFLAG(ENABLENATPMPMASK) ? " NAT-PMP " : " ",
+-#endif
+-#else
+- " ",
+-#endif
+- GETFLAG(ENABLEUPNPMASK) ? "UPnP-IGD " : "",
+- ext_if_name, upnp_bootid);
+ #ifdef ENABLE_IPV6
+ if (strcmp(ext_if_name6, ext_if_name) != 0) {
+- syslog(LOG_INFO, "specific IPv6 ext if %s", ext_if_name6);
++ syslog(LOG_INFO, "Separate ext_ifname6=%s set", ext_if_name6);
+ }
+ #endif
+
+ if(GETFLAG(PERFORMSTUNMASK))
+ {
+- if (update_ext_ip_addr_from_stun(1) != 0) {
+- syslog(LOG_ERR, "Performing STUN failed. EXITING");
++ if (update_ext_ip_addr_from_stun() != 0) {
++ syslog(LOG_ERR, "STUN: Performing test failed, exiting");
+ return_code = 1;
+ goto shutdown;
+ }
+@@ -2461,6 +2484,8 @@ main(int argc, char * * argv)
+ else if (!use_ext_ip_addr)
+ {
+ update_disable_port_forwarding();
++ } else {
++ syslog(LOG_NOTICE, "IPv4 mapping enabled");
+ }
+
+ #ifdef DYNAMIC_OS_VERSION
+@@ -2487,7 +2512,7 @@ main(int argc, char * * argv)
+ #ifdef ENABLE_NFQUEUE
+ nfqueue_data.http_port = listen_port;
+ #endif /* ENABLE_NFQUEUE */
+- syslog(LOG_NOTICE, "HTTP listening on port %d", v.port);
++ syslog(LOG_NOTICE, "Listening for UPnP IGD (SOAP/HTTP) traffic on port %d/TCP, SSDP 1900/UDP", v.port);
+ #if defined(V6SOCKETS_ARE_V6ONLY) && defined(ENABLE_IPV6)
+ if(!GETFLAG(IPV6DISABLEDMASK))
+ {
+@@ -2518,7 +2543,7 @@ main(int argc, char * * argv)
+ #ifdef ENABLE_NFQUEUE
+ nfqueue_data.https_port = listen_port;
+ #endif /* ENABLE_NFQUEUE */
+- syslog(LOG_NOTICE, "HTTPS listening on port %d", v.https_port);
++ syslog(LOG_NOTICE, "Listening for UPnP IGD (SOAP/HTTPS) traffic on port %d/TCP", v.https_port);
+ #if defined(V6SOCKETS_ARE_V6ONLY) && defined(ENABLE_IPV6)
+ shttpsl_v4 = OpenAndConfHTTPSocket(&listen_port, 0);
+ if(shttpsl_v4 < 0)
+@@ -2533,11 +2558,11 @@ main(int argc, char * * argv)
+ if(!GETFLAG(IPV6DISABLEDMASK)) {
+ if(find_ipv6_addr(lan_addrs.lh_first ? lan_addrs.lh_first->ifname : NULL,
+ ipv6_addr_for_http_with_brackets, sizeof(ipv6_addr_for_http_with_brackets)) > 0) {
+- syslog(LOG_NOTICE, "HTTP IPv6 address given to control points : %s",
++ syslog(LOG_INFO, "Address given to UPnP IGDv2 IPv6 clients: %s",
+ ipv6_addr_for_http_with_brackets);
+ } else {
+ memcpy(ipv6_addr_for_http_with_brackets, "[::1]", 6);
+- syslog(LOG_WARNING, "no HTTP IPv6 address, disabling IPv6");
++ syslog(LOG_DEBUG, "no HTTP IPv6 address, disabling IPv6");
+ SETFLAG(IPV6DISABLEDMASK);
+ }
+ }
+@@ -2594,7 +2619,7 @@ main(int argc, char * * argv)
+ if(SendSSDPGoodbye(snotify, addr_count * 2) < 0)
+ #endif
+ {
+- syslog(LOG_WARNING, "Failed to broadcast good-bye notifications");
++ syslog(LOG_DEBUG, "Failed to broadcast good-bye notifications");
+ }
+ }
+ #endif /* UPNP_STRICT */
+@@ -2619,16 +2644,16 @@ main(int argc, char * * argv)
+ if(OpenAndConfNATPMPSockets(snatpmp) < 0)
+ #ifdef ENABLE_PCP
+ {
+- syslog(LOG_ERR, "Failed to open sockets for NAT-PMP/PCP.");
++ syslog(LOG_ERR, "Failed to open port 5351/UDP for PCP/NAT-PMP");
+ } else {
+- syslog(LOG_NOTICE, "Listening for NAT-PMP/PCP traffic on port %u",
++ syslog(LOG_NOTICE, "Listening for PCP/NAT-PMP traffic on port %u/UDP",
+ NATPMP_PORT);
+ }
+ #else
+ {
+- syslog(LOG_ERR, "Failed to open sockets for NAT PMP.");
++ syslog(LOG_ERR, "Failed to open port 5351/UDP for NAT-PMP");
+ } else {
+- syslog(LOG_NOTICE, "Listening for NAT-PMP traffic on port %u",
++ syslog(LOG_NOTICE, "Listening for NAT-PMP traffic on port %u/UDP",
+ NATPMP_PORT);
+ }
+ #endif
+@@ -2739,6 +2764,30 @@ main(int argc, char * * argv)
+ }
+ #endif /* HAS_LIBCAP_NG */
+
++if (GETFLAG(ENABLEUPNPMASK) && !GETFLAG(SECUREMODEMASK))
++ syslog(LOG_WARNING, "WARNING: Allow adding port maps for non-requesting IP addresses via UPnP IGD, as secure_mode=no set");
++#ifdef ENABLE_PCP
++if (GETFLAG(ENABLENATPMPMASK) && GETFLAG(PCP_ALLOWTHIRDPARTYMASK))
++ syslog(LOG_WARNING, "WARNING: Allow adding port maps for non-requesting IP addresses via PCP, as pcp_allow_thirdparty=yes set");
++#endif
++#ifdef ENABLE_IPV6
++if (GETFLAG(IPV6DISABLEDMASK))
++ syslog(LOG_NOTICE, "IPv6 mapping disabled");
++#else
++syslog(LOG_NOTICE, "IPv6 mapping disabled");
++#endif
++if (GETFLAG(ENABLEUPNPMASK)) {
++#ifdef IGD_V2
++ if (GETFLAG(FORCEIGDDESCV1MASK)) {
++ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv1 (IPv4 only)");
++ } else {
++ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv2");
++ }
++#else
++ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv1 (IPv4 only)");
++#endif
++}
++
+ #ifdef USE_SYSTEMD
+ if (v.systemd_notify) {
+ upnp_update_status();
+@@ -2768,12 +2817,13 @@ main(int argc, char * * argv)
+ /* send public address change notifications if needed */
+ if(should_send_public_address_change_notif)
+ {
+- syslog(LOG_INFO, "should send external iface address change notification(s)");
++ syslog(LOG_NOTICE, "External network interface link/address change detected");
+ if(GETFLAG(PERFORMSTUNMASK))
+ {
+- if (update_ext_ip_addr_from_stun(0) != 0) {
++ if (update_ext_ip_addr_from_stun() != 0) {
+ /* if stun succeed it updates disable_port_forwarding;
+ * if stun failed (non-zero return value) then port forwarding would not work, so disable it */
++ syslog(LOG_WARNING, "IPv4 mapping disabled");
+ disable_port_forwarding = 1;
+ }
+ }
+@@ -3049,7 +3099,7 @@ main(int argc, char * * argv)
+ }
+ i = try_sendto(&writeset);
+ if(i < 0) {
+- syslog(LOG_ERR, "try_sendto failed to send %d packets", -i);
++ /*syslog(LOG_DEBUG, "try_sendto failed to send %d packets", -i);*/
+ }
+ #ifdef USE_MINIUPNPDCTL
+ for(ectl = ctllisthead.lh_first; ectl;)
+@@ -3149,7 +3199,7 @@ main(int argc, char * * argv)
+ if(lan_addr == NULL) {
+ char sender_str[64];
+ sockaddr_to_string((struct sockaddr *)&senderaddr, sender_str, sizeof(sender_str));
+- syslog(LOG_WARNING, "NAT-PMP packet sender %s not from a LAN, ignoring",
++ syslog(LOG_DEBUG, "NAT-PMP packet sender %s not from a LAN, ignoring",
+ sender_str);
+ continue;
+ }
+@@ -3172,7 +3222,7 @@ main(int argc, char * * argv)
+ if(lan_addr == NULL) {
+ char sender_str[64];
+ sockaddr_to_string((struct sockaddr *)&senderaddr, sender_str, sizeof(sender_str));
+- syslog(LOG_WARNING, "NAT-PMP packet sender %s not from a LAN, ignoring",
++ syslog(LOG_DEBUG, "NAT-PMP packet sender %s not from a LAN, ignoring",
+ sender_str);
+ continue;
+ }
+@@ -3206,7 +3256,7 @@ main(int argc, char * * argv)
+ /* process SSDP packets */
+ if(sudp >= 0 && FD_ISSET(sudp, &readset))
+ {
+- /*syslog(LOG_INFO, "Received UDP Packet");*/
++ /*syslog(LOG_DEBUG, "Received UDP Packet");*/
+ #ifdef ENABLE_HTTPS
+ ProcessSSDPRequest(sudp, (unsigned short)v.port, (unsigned short)v.https_port);
+ #else
+@@ -3216,7 +3266,7 @@ main(int argc, char * * argv)
+ #ifdef ENABLE_IPV6
+ if(sudpv6 >= 0 && FD_ISSET(sudpv6, &readset))
+ {
+- syslog(LOG_INFO, "Received UDP Packet (IPv6)");
++ /*syslog(LOG_DEBUG, "Received UDP Packet (IPv6)");*/
+ #ifdef ENABLE_HTTPS
+ ProcessSSDPRequest(sudpv6, (unsigned short)v.port, (unsigned short)v.https_port);
+ #else
+@@ -3318,7 +3368,7 @@ main(int argc, char * * argv)
+
+ shutdown:
+
+- syslog(LOG_NOTICE, "shutting down MiniUPnPd");
++ syslog(LOG_NOTICE, "Shutting down MiniUPnPd");
+ #ifdef USE_SYSTEMD
+ if (v.systemd_notify) {
+ sd_notify(0,
+@@ -3337,7 +3387,7 @@ shutdown:
+ if(SendSSDPGoodbye(snotify, addr_count * 2) < 0)
+ #endif
+ {
+- syslog(LOG_ERR, "Failed to broadcast good-bye notifications");
++ syslog(LOG_DEBUG, "Failed to broadcast good-bye notifications");
+ }
+ }
+ /* try to send pending packets */
+--- a/miniupnpd.conf
++++ b/miniupnpd.conf
+@@ -3,50 +3,49 @@
+
+ # CAUTION: mixing up WAN and LAN interfaces may introduce security risks!
+ # Be sure to assign the correct interfaces to LAN and WAN and consider
+-# implementing UPnP permission rules at the bottom of this configuration file
++# configuring access control settings at the bottom of this file
++
++##### EXTERNAL (WAN) NETWORK INTERFACE #####
+
+-# WAN network interface
+ #ext_ifname=eth1
+
+-# if the WAN network interface for IPv6 is different than for IPv4,
+-# set ext_ifname6
++# If the WAN network interface for IPv6 is different than for IPv4
+ #ext_ifname6=eth2
+
+-# If the WAN interface has several IP addresses, you
+-# can specify the one to use below.
+-# Setting ext_ip is also useful in double NAT setup, you can declare here
+-# the public IP address.
++# Allow CGNAT use (default is no)
++# By default, daemon disables IPv4 mapping if a private/CGNAT-reserved external
++# IPv4 address is detected for the following reasons:
++# - Info, as port maps not reachable via internet without an unrestricted
++# endpoint-independent (1:1) IPv4 CGNAT, but possibly from within
++# - Compatibility, as many UPnP IGD & PCP/NAT-PMP clients require a returned
++# public external IPv4 address
++# - Security, to prevent access via internet if the external/internal network
++# interface has been configured swapped
++# To enable IPv4 mapping in this case, select a priority-listed option:
++# A) Detect public external IPv4 and test for unrestricted endpoint-independent
++# (1:1) CGNAT via STUN server, set ext_allow_cgnat=yes
++# B) As A, but workaround filtered CGNAT result daemon limitation (without extra
++# firewall rule), set ext_allow_cgnat=allow-filtered; check security note
++# C) Manually override reported external IPv4 with public, set ext_ip; check
++# security note
++# D) Report private/CGNAT-reserved external IPv4 to clients, set
++# ext_allow_cgnat=report-private-ipv4; check compatibility/security notes
++#ext_allow_cgnat=yes
++#ext_allow_cgnat=allow-filtered
++#ext_allow_cgnat=report-private-ipv4
++
++# Configure STUN server
++# Some CHANGE-REQUEST's capable public servers: stun.nextcloud.com,
++# stun.sipgate.net, stun.3cx.com, stun.counterpath.com, stun.miwifi.com
++ext_stun_host=stun.nextcloud.com
++#ext_stun_port=3478
++
++# Override external IPv4
++# Report custom public/external (WAN) IPv4 address to clients
++# If external interface has several IPv4 addresses, you can set which to report
+ #ext_ip=
+
+-# Allow private IP address on external interface. When set to yes, program
+-# won't just disable port forwarding when external address is private IP.
+-# This option is disabled by default.
+-#ext_allow_private_ipv4=yes
+-
+-# The WAN interface must have a public IP address. Otherwise it is behind NAT
+-# and port forwarding is impossible. In some cases WAN interface can be
+-# behind unrestricted full-cone NAT 1:1 when all incoming traffic is NAT-ed and
+-# routed to WAN interfaces without any filtering. In this cases miniupnpd
+-# needs to know the public IP address and it can be learnt by asking external
+-# server via STUN protocol. Following option enable retrieving external
+-# public IP address from STUN server and detection of NAT type. You need
+-# to specify also external STUN server in stun_host option below.
+-# STUN CGNAT filter check result would be ignored when deciding whether to
+-# disable port forwarding if setting this option to allow-filtered.
+-# This option is disabled by default.
+-#ext_perform_stun=yes
+-#ext_perform_stun=allow-filtered
+-
+-# Specify STUN server, either hostname or IP address
+-# Some public STUN servers:
+-# stun.nextcloud.com
+-# stun.sipgate.net
+-# stun.3cx.com
+-# stun.counterpath.com
+-# stun.miwifi.com
+-#ext_stun_host=stun.nextcloud.com
+-# Specify STUN UDP port, by default it is standard port 3478.
+-#ext_stun_port=3478
++##### INTERNAL (LAN) NETWORK INTERFACES #####
+
+ # LAN network interfaces IPs / networks
+ # There can be multiple listening IPs for SSDP traffic, in that case
+@@ -64,33 +63,77 @@
+ #listening_ip=10.5.0.0/16
+ #listening_ip=eth0
+
++##### DAEMON CONFIGURATION #####
++
++# Enable UPNP support (default is yes)
++#enable_upnp=no
++
++# Enable NAT-PMP and PCP support (default is no)
++#enable_pcp_pmp=yes
++
++# Lease file location
++#lease_file=/var/log/upnp.leases
++
++# Report system uptime instead of daemon uptime
++system_uptime=yes
++
++# Unused rules cleaning.
++# never remove any rule before this threshold for the number
++# of redirections is exceeded. default to 20
++#clean_ruleset_threshold=10
++# Clean process work interval in seconds. default to 0 (disabled).
++# a 600 seconds (10 minutes) interval makes sense
++clean_ruleset_interval=600
++
++##### UPNP IGD CONFIGURATION #####
++
++# If compiled with IGD_V2 defined, force reporting IGDv1 in rootDesc (default
++# is no)
++#force_igd_desc_v1=no
++
++# Bitrates reported by daemon in bits per second
++# by default miniupnpd tries to get WAN interface speed
++#bitrate_up=1000000
++#bitrate_down=10000000
++
++# Daemon's serial and model number when reporting to clients
++# (in XML description)
++#serial=12345678
++#model_number=1
++
++# Default presentation URL is HTTP address on port 80
++# If set to an empty string, no presentationURL element will appear
++# in the XML description of the device, which prevents MS Windows
++# from displaying an icon in the "Network Connections" panel.
++#presentation_url=http://www.mylan/index.php
++
++# UUID, generate your own UUID with "make genuuid"
++uuid=00000000-0000-0000-0000-000000000000
++
+ # Port for HTTP (descriptions and SOAP) traffic. Set to 0 for autoselect.
+ #http_port=0
+
+ # Port for HTTPS. Set to 0 for autoselect (default)
+ #https_port=0
+
++# Notify interval in seconds. default is 900 seconds.
++# As advised in the standard, announcement have a lifetime double of this value.
++notify_interval=900
++
+ # Path to the UNIX socket used to communicate with MiniSSDPd
+ # If running, MiniSSDPd will manage M-SEARCH answering.
+ # default is /var/run/minissdpd.sock
+ #minissdpdsocket=/var/run/minissdpd.sock
+
+-# Disable IPv6 (default is no : IPv6 enabled if enabled at build time)
+-#ipv6_disable=yes
+-
+-# Enable NAT-PMP and PCP support (default is no)
+-#enable_pcp_pmp=yes
+-
+-# Enable UPNP support (default is yes)
+-#enable_upnp=no
++##### PCP CONFIGURATION #####
+
+ # PCP
+ # Configure the minimum and maximum lifetime of a port mapping in seconds
+ # 120s and 86400s (24h) are suggested values from PCP-base
+ #min_lifetime=120
+ #max_lifetime=86400
+-# allow THIRD_PARTY Option for MAP and PEER Opcodes (default is no)
+-#pcp_allow_thirdparty=yes
++
++##### LINUX NFTABLES/IPTABLES FIREWALL CONFIGURATION #####
+
+ # table names for netfilter nft. Default is "filter" for both
+ #upnp_table_name=
+@@ -103,8 +146,27 @@
+ #upnp_nat_postrouting_chain=UPnP-Postrouting
+ #upnp_nftables_family_split=no
+
+-# Lease file location
+-#lease_file=/var/log/upnp.leases
++##### BSD'S PF FIREWALL CONFIGURATION #####
++
++# Log packets in pf (default is no)
++#packet_log=no
++
++# Anchor name in pf (default is miniupnpd)
++#anchor=miniupnpd
++
++# ALTQ queue in pf
++# Filter rules must be used for this to be used.
++# compile with PF_ENABLE_FILTER_RULES (see config.h file)
++#queue=queue_name1
++
++# Tag name in pf
++#tag=tag_name1
++
++# Make filter rules in pf quick or not. default is yes
++# active when compiled with PF_ENABLE_FILTER_RULES (see config.h file)
++#quickrules=no
++
++##### CUSTOM UPNP IGD MANUFACTURER INFORMATION #####
+
+ # To enable the next few runtime options, see compile time
+ # ENABLE_MANUFACTURER_INFO_CONFIGURATION (config.h)
+@@ -127,83 +189,41 @@
+ # Model URL, default is URL of OS vendor
+ #model_url=https://miniupnp.tuxfamily.org/
+
+-# Bitrates reported by daemon in bits per second
+-# by default miniupnpd tries to get WAN interface speed
+-#bitrate_up=1000000
+-#bitrate_down=10000000
+-
+-# Secure Mode, UPnP clients can only add mappings to their own IP
+-# Enabled by default
+-#secure_mode=no
+-
+-# Default presentation URL is HTTP address on port 80
+-# If set to an empty string, no presentationURL element will appear
+-# in the XML description of the device, which prevents MS Windows
+-# from displaying an icon in the "Network Connections" panel.
+-#presentation_url=http://www.mylan/index.php
+-
+-# Report system uptime instead of daemon uptime
+-system_uptime=yes
++##### ACCESS CONTROL #####
+
+-# Notify interval in seconds. default is 900 seconds.
+-# As advised in the standard, announcement have a lifetime double of this value.
+-notify_interval=900
++# Default minimum/maximum port for IPv4/IPv6 port maps (default is 1)
++# Specify the default min/max accepted internal/external port. Applies with no
++# matching or default reject IPv4 ACL entries, but for all IPv6 devices, as no
++# IPv6 ACL.
++# For private use, min. of 1024 recommended; planned default in >=2.4.0 release.
++# However, for use cases where low ports (e.g. 80/443 for HTTP/HTTPS) need to be
++# made reachable via internet, it's recommended to make min. user configurable
++#default_min_port=1
++#default_max_port=65535
+
+-# Unused rules cleaning.
+-# never remove any rule before this threshold for the number
+-# of redirections is exceeded. default to 20
+-#clean_ruleset_threshold=10
+-# Clean process work interval in seconds. default to 0 (disabled).
+-# a 600 seconds (10 minutes) interval makes sense
+-clean_ruleset_interval=600
+-
+-# Log packets in pf (default is no)
+-#packet_log=no
+-
+-# Anchor name in pf (default is miniupnpd)
+-#anchor=miniupnpd
+-
+-# ALTQ queue in pf
+-# Filter rules must be used for this to be used.
+-# compile with PF_ENABLE_FILTER_RULES (see config.h file)
+-#queue=queue_name1
+-
+-# Tag name in pf
+-#tag=tag_name1
+-
+-# Make filter rules in pf quick or not. default is yes
+-# active when compiled with PF_ENABLE_FILTER_RULES (see config.h file)
+-#quickrules=no
+-
+-# UUID, generate your own UUID with "make genuuid"
+-uuid=00000000-0000-0000-0000-000000000000
+-
+-# Daemon's serial and model number when reporting to clients
+-# (in XML description)
+-#serial=12345678
+-#model_number=1
++# Disable IPv6 mapping (default is no, available if ENABLE_IPV6 set at build)
++#ipv6_disable=yes
+
+-# If compiled with IGD_V2 defined, force reporting IGDv1 in rootDesc (default
+-# is no)
+-#force_igd_desc_v1=no
++# Allow third-party mapping (use with care)
++# Allow adding port maps for non-requesting IP addresses; use with care
++# To allow this via UPnP IGD protocol, set secure_mode=no (default is yes)
++# To allow this via PCP protocol, set pcp_allow_thirdparty=yes (default is no)
++#secure_mode=no
++#pcp_allow_thirdparty=yes
+
+-# UPnP permission rules (also enforced for NAT-PMP and PCP) for IPv4
+-# (allow|deny) (external port range) IP/mask (internal port range) (optional regex filter)
+-# A port range is - or if there is only
+-# one port in the range.
+-# IP/mask format must be nnn.nnn.nnn.nnn/nn
++# Access control list (ACL) entries, for IPv4 only
++# The ACL specifies which IPv4 addresses and ports can be mapped
++# (allow/deny) (ext port/range) IP/mask (int port/range) (optional regex filter)
+ # Regex support must be enabled at build time : ./configure --regex
+-# It is advised to only allow redirection of ports >= 1024
+-# and end the rule set with "deny 0-65535 0.0.0.0/0 0-65535"
++# It is advised to end the rule set with "deny 1-65535 0.0.0.0/0 1-65535"
+ # The following default ruleset allows specific LAN side IP addresses
+-# to request only ephemeral ports. It is recommended that users
++# to request only high ports. It is recommended that users
+ # modify the IP ranges to match their own internal networks, and
+ # also consider implementing network-specific restrictions
+-# CAUTION: failure to enforce any rules may permit insecure requests to be made!
+ allow 1024-65535 192.168.0.0/24 1024-65535
+-# disallow requests whose description string matches the given regex
+-# deny 1024-65535 192.168.1.0/24 1024-65535 "My evil app ver [[:digit:]]*"
++# To disallow requests whose description string matches the given regex
++# deny 1024-65535 192.168.1.0/24 1024-65535 "My evil app ver [[:digit:]]*"
++# CAUTION: failure to enforce any rules may permit insecure requests to be made!
+ allow 1024-65535 192.168.1.0/24 1024-65535
+-allow 1024-65535 192.168.0.0/23 22
+-allow 12345 192.168.7.113/32 54321
+-deny 0-65535 0.0.0.0/0 0-65535
++allow 12345 192.168.7.113 54321
++deny 1-65535 0.0.0.0/0 0-65535
+--- a/natpmp.c
++++ b/natpmp.c
+@@ -106,10 +106,10 @@ static void FillPublicAddressResponse(un
+ resp[3] = 3; /* Network Failure (e.g. NAT box itself
+ * has not obtained a DHCP lease) */
+ } else if(getifaddr(ext_if_name, tmp, INET_ADDRSTRLEN, &addr, NULL) < 0) {
+- syslog(LOG_ERR, "Failed to get IP for interface %s", ext_if_name);
++ syslog(LOG_DEBUG, "Failed to get IP for interface %s", ext_if_name);
+ resp[3] = 3; /* Network Failure (e.g. NAT box itself
+ * has not obtained a DHCP lease) */
+- } else if (!GETFLAG(ALLOWPRIVATEIPV4MASK) && addr_is_reserved(&addr)) {
++ } else if (!GETFLAG(REPORTPRIVATEIPV4MASK) && addr_is_reserved(&addr)) {
+ resp[3] = 3; /* Network Failure, box has not obtained
+ public IP address */
+ } else {
+@@ -231,7 +231,7 @@ void ProcessIncomingNATPMPPacket(int s,
+ syslog(LOG_ERR, "inet_ntop(natpmp): %m");
+ }
+
+- syslog(LOG_INFO, "NAT-PMP request received from %s:%hu %dbytes",
++ syslog(LOG_DEBUG, "NAT-PMP request received from %s:%hu %d bytes",
+ senderaddrstr, ntohs(senderaddr->sin_port), n);
+
+ if(n<2 || ((((req[1]-1)&~1)==0) && n<12)) {
+@@ -260,7 +260,7 @@ void ProcessIncomingNATPMPPacket(int s,
+ resp[3] = 1; /* unsupported version */
+ } else switch(req[1]) {
+ case 0: /* Public address request */
+- syslog(LOG_INFO, "NAT-PMP public address request");
++ syslog(LOG_DEBUG, "NAT-PMP public address request");
+ FillPublicAddressResponse(resp, senderaddr->sin_addr.s_addr);
+ resplen = 12;
+ break;
+@@ -280,10 +280,6 @@ void ProcessIncomingNATPMPPacket(int s,
+ eport = READNU16(req+6);
+ lifetime = READNU32(req+8);
+ proto = (req[1]==1)?IPPROTO_UDP:IPPROTO_TCP;
+- syslog(LOG_INFO, "NAT-PMP port mapping request : "
+- "%hu->%s:%hu %s lifetime=%us",
+- eport, senderaddrstr, iport,
+- (req[1]==1)?"udp":"tcp", lifetime);
+ /* TODO: accept port mapping if iport ok but eport not ok
+ * (and set eport correctly) */
+ if(lifetime == 0) {
+@@ -319,8 +315,8 @@ void ProcessIncomingNATPMPPacket(int s,
+ resp[3] = 2; /* Not Authorized/Refused */
+ break;
+ } else {
+- syslog(LOG_INFO, "NAT-PMP %s port %hu mapping removed",
+- proto2==IPPROTO_TCP?"TCP":"UDP", eport2);
++ syslog(LOG_INFO, "Delete IPv4 port map %hu:%s:%hu/%s via NAT-PMP",
++ eport2, senderaddrstr, iport, proto2 == IPPROTO_TCP ? "TCP" : "UDP");
+ index--;
+ }
+ }
+@@ -340,8 +336,8 @@ void ProcessIncomingNATPMPPacket(int s,
+ eport_first = eport;
+ } else if(eport == eport_first) { /* no eport available */
+ if(any_eport_allowed == 0) { /* all eports rejected by permissions */
+- syslog(LOG_ERR, "No allowed eport for NAT-PMP %hu %s->%s:%hu",
+- eport, proto_itoa(proto), senderaddrstr, iport);
++ syslog(LOG_INFO, "Reject IPv4 port map %hu:%s:%hu/%s via NAT-PMP reason ACL/default-min/max-port",
++ eport, senderaddrstr, iport, proto_itoa(proto));
+ resp[3] = 2; /* Not Authorized/Refused */
+ } else { /* at least one eport allowed (but none available) */
+ syslog(LOG_ERR, "Failed to find available eport for NAT-PMP %hu %s->%s:%hu",
+@@ -358,7 +354,7 @@ void ProcessIncomingNATPMPPacket(int s,
+ any_eport_allowed = 1; /* at lease one eport is allowed */
+ #ifdef CHECK_PORTINUSE
+ if (port_in_use(ext_if_name, eport, proto, senderaddrstr, iport) > 0) {
+- syslog(LOG_INFO, "port %hu protocol %s already in use",
++ syslog(LOG_INFO, "Reject IPv4 port map %hu:?:?/%s via NAT-PMP reason port in use",
+ eport, proto_itoa(proto));
+ eport++;
+ if(eport == 0) eport++; /* skip port zero */
+@@ -373,8 +369,8 @@ void ProcessIncomingNATPMPPacket(int s,
+ if(strcmp(senderaddrstr, iaddr_old)==0
+ && iport==iport_old) {
+ /* redirection already existing */
+- syslog(LOG_INFO, "port %hu %s already redirected to %s:%hu, replacing",
+- eport, (proto==IPPROTO_TCP)?"tcp":"udp", iaddr_old, iport_old);
++ syslog(LOG_INFO, "Renew IPv4 port map %hu:%s:%hu/%s lifetime %u via NAT-PMP",
++ eport, senderaddrstr, iport, proto_itoa(proto), lifetime);
+ /* remove and then add again */
+ if(_upnp_delete_redir(eport, proto) < 0) {
+ syslog(LOG_ERR, "failed to remove port mapping");
+@@ -394,13 +390,17 @@ void ProcessIncomingNATPMPPacket(int s,
+ if(upnp_redirect_internal(NULL, eport, senderaddrstr,
+ iport, proto, desc,
+ timestamp) < 0) {
+- syslog(LOG_ERR, "Failed to add NAT-PMP %hu %s->%s:%hu '%s'",
++ syslog(LOG_DEBUG, "Failed to add NAT-PMP %hu %s->%s:%hu '%s'",
+ eport, proto_itoa(proto), senderaddrstr, iport, desc);
+ resp[3] = 3; /* Failure */
+ }
+ break;
+ }
+ }
++ if (resp[3] == 0 && lifetime) {
++ syslog(LOG_INFO, "Add IPv4 port map %hu:%s:%hu/%s lifetime %u via NAT-PMP",
++ eport, senderaddrstr, iport, req[1] == 1 ? "UDP" : "TCP", lifetime);
++ }
+ WRITENU16(resp+8, iport); /* private port */
+ WRITENU16(resp+10, eport); /* public port */
+ WRITENU32(resp+12, lifetime); /* Port Mapping lifetime */
+@@ -410,6 +410,7 @@ void ProcessIncomingNATPMPPacket(int s,
+ default:
+ resp[3] = 5; /* Unsupported OPCODE */
+ }
++
+ n = sendto_or_schedule(s, resp, resplen, 0,
+ (struct sockaddr *)senderaddr, sizeof(*senderaddr));
+ if(n<0) {
+@@ -443,7 +444,7 @@ void SendNATPMPPublicAddressChangeNotifi
+ FillPublicAddressResponse(notif, 0);
+ if(notif[3])
+ {
+- syslog(LOG_WARNING, "%s: cannot get public IP address, stopping",
++ syslog(LOG_DEBUG, "%s: cannot get public IP address, stopping",
+ "SendNATPMPPublicAddressChangeNotification");
+ return;
+ }
+@@ -471,7 +472,7 @@ void SendNATPMPPublicAddressChangeNotifi
+ (struct sockaddr *)&sockname, sizeof(struct sockaddr_in));
+ if(n < 0)
+ {
+- syslog(LOG_ERR, "%s: sendto(s_udp=%d, port=%d): %m",
++ syslog(LOG_DEBUG, "%s: sendto(s_udp=%d, port=%d): %m",
+ "SendNATPMPPublicAddressChangeNotification", sockets[j], NATPMP_PORT);
+ return;
+ }
+@@ -481,7 +482,7 @@ void SendNATPMPPublicAddressChangeNotifi
+ (struct sockaddr *)&sockname, sizeof(struct sockaddr_in));
+ if(n < 0)
+ {
+- syslog(LOG_ERR, "%s: sendto(s_udp=%d, port=%d): %m",
++ syslog(LOG_DEBUG, "%s: sendto(s_udp=%d, port=%d): %m",
+ "SendNATPMPPublicAddressChangeNotification", sockets[j], NATPMP_NOTIF_PORT);
+ return;
+ }
+--- a/netfilter/iptcrdr.c
++++ b/netfilter/iptcrdr.c
+@@ -715,7 +715,7 @@ delete_filter_rule(const char * ifname,
+ continue;
+ }
+ index = i;
+- /*syslog(LOG_INFO, "Trying to delete filter rule at index %u", index);*/
++ /*syslog(LOG_DEBUG, "Trying to delete filter rule at index %u", index);*/
+ r = delete_rule_and_commit(index, h, miniupnpd_forward_chain, "delete_filter_rule");
+ h = NULL;
+ break;
+@@ -808,7 +808,7 @@ delete_redirect_and_filter_rules(unsigne
+ #endif
+ if(r == 0)
+ {
+- syslog(LOG_INFO, "Trying to delete nat rule at index %u", index);
++ syslog(LOG_DEBUG, "Trying to delete nat rule at index %u", index);
+ /* Now delete both rules */
+ /* first delete the nat rule */
+ h = iptc_init("nat");
+@@ -852,7 +852,7 @@ delete_redirect_and_filter_rules(unsigne
+ if(iaddr != e->ip.dst.s_addr)
+ continue;
+ index = i;
+- syslog(LOG_INFO, "Trying to delete filter rule at index %u", index);
++ syslog(LOG_DEBUG, "Trying to delete filter rule at index %u", index);
+ r = delete_rule_and_commit(index, h, miniupnpd_forward_chain, "delete_filter_rule");
+ h = NULL;
+ break;
+@@ -867,6 +867,9 @@ delete_redirect_and_filter_rules(unsigne
+ iptc_free(&h);
+ #endif
+ }
++ } else {
++ syslog(LOG_INFO, "Non-existing IPv4 port map to delete %hu:?:?/%s via UPnP IGD",
++ eport, proto == IPPROTO_TCP ? "TCP" : "UDP");
+ }
+
+ /*delete PEER rule*/
+@@ -910,7 +913,7 @@ delete_redirect_and_filter_rules(unsigne
+ }
+
+ index = i;
+- syslog(LOG_INFO, "Trying to delete peer rule at index %u", index);
++ syslog(LOG_DEBUG, "Trying to delete peer rule at index %u", index);
+ r2 = delete_rule_and_commit(index, h, miniupnpd_nat_postrouting_chain, "delete_peer_rule");
+ h = NULL;
+ break;
+@@ -962,7 +965,7 @@ delete_redirect_and_filter_rules(unsigne
+ if(iaddr != e->ip.src.s_addr)
+ continue;
+ index = i;
+- syslog(LOG_INFO, "Trying to delete dscp rule at index %u", index);
++ syslog(LOG_DEBUG, "Trying to delete dscp rule at index %u", index);
+ r2 = delete_rule_and_commit(index, h, miniupnpd_nat_chain, "delete_dscp_rule");
+ h = NULL;
+ break;
+--- a/netfilter/iptpinhole.c
++++ b/netfilter/iptpinhole.c
+@@ -232,13 +232,13 @@ int add_pinhole(const char * ifname,
+ strncpy(e->ipv6.iniface, ifname, IFNAMSIZ);
+ if(rem_host && (rem_host[0] != '\0')) {
+ if(inet_pton(AF_INET6, rem_host, &e->ipv6.src) < 1) {
+- syslog(LOG_WARNING, "failed to parse INET6 address \"%s\"", rem_host);
++ syslog(LOG_DEBUG, "Failed to parse remote IPv6 address (%s)", rem_host);
+ } else {
+ memset(&e->ipv6.smsk, 0xff, sizeof(e->ipv6.smsk));
+ }
+ }
+ if (inet_pton(AF_INET6, int_client, &e->ipv6.dst) < 1) {
+- syslog(LOG_WARNING, "failed to parse INET6 address \"%s\"", int_client);
++ syslog(LOG_DEBUG, "Failed to parse IPv6 address (%s)", int_client);
+ } else {
+ memset(&e->ipv6.dmsk, 0xff, sizeof(e->ipv6.dmsk));
+ }
+@@ -295,14 +295,14 @@ find_pinhole(const char * ifname,
+
+ if(rem_host && (rem_host[0] != '\0')) {
+ if (inet_pton(AF_INET6, rem_host, &saddr) < 1) {
+- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", rem_host);
++ syslog(LOG_DEBUG, "Failed to parse remote IPv6 address (%s)", rem_host);
+ memset(&saddr, 0, sizeof(struct in6_addr));
+ }
+ } else {
+ memset(&saddr, 0, sizeof(struct in6_addr));
+ }
+ if (inet_pton(AF_INET6, int_client, &daddr) < 1) {
+- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", int_client);
++ syslog(LOG_DEBUG, "Failed to parse IPv6 address (%s)", int_client);
+ memset(&daddr, 0, sizeof(struct in6_addr));
+ }
+ for(p = pinhole_list.lh_first; p != NULL; p = p->entries.le_next) {
+@@ -483,7 +483,7 @@ clean_pinhole_list(unsigned int * next_t
+ while(p != NULL) {
+ if(p->timestamp <= (unsigned int)current_time) {
+ unsigned short uid = p->uid;
+- syslog(LOG_INFO, "removing expired pinhole with uid=%hu", uid);
++ syslog(LOG_INFO, "Expire IPv6 port map [?]:?/?");
+ p = p->entries.le_next;
+ if(delete_pinhole(uid) == 0)
+ n++;
+--- a/netfilter_nft/nftnlrdr.c
++++ b/netfilter_nft/nftnlrdr.c
+@@ -353,8 +353,8 @@ delete_redirect_and_filter_rules(unsigne
+ }
+ }
+ } else {
+- syslog(LOG_WARNING, "%s: redirect rule with eport=%hu proto %d NOT FOUND",
+- "delete_redirect_and_filter_rules", eport, proto);
++ syslog(LOG_INFO, "Non-existing IPv4 port map to delete %hu:?:?/%s via UPnP IGD",
++ eport, proto == IPPROTO_TCP ? "TCP" : "UDP");
+ }
+
+ iaddr = 0;
+--- a/netfilter_nft/nftnlrdr_misc.c
++++ b/netfilter_nft/nftnlrdr_misc.c
+@@ -110,7 +110,7 @@ nft_mnl_connect(void)
+ return -1;
+ }
+ mnl_portid = mnl_socket_get_portid(mnl_sock);
+- syslog(LOG_INFO, "mnl_socket bound, port_id=%u", mnl_portid);
++ syslog(LOG_DEBUG, "mnl_socket bound, port_id=%u", mnl_portid);
+ return 0;
+ }
+
+@@ -755,7 +755,7 @@ refresh_nft_cache(struct rule_list *head
+ errno = 0;
+ ret = mnl_cb_run(buf, n, mnl_seq, mnl_portid, table_cb, &data);
+ if (ret <= -1 /*== MNL_CB_ERROR*/) {
+- syslog(LOG_ERR, "%s: mnl_cb_run returned %d: %m",
++ syslog(LOG_DEBUG, "%s: mnl_cb_run returned %d: %m",
+ "refresh_nft_cache", ret);
+ return -1;
+ }
+@@ -1331,7 +1331,7 @@ nft_send_rule(struct nftnl_rule * rule,
+
+ result = send_batch(batch);
+ if (result < 0) {
+- syslog(LOG_ERR, "%s(%p, %d, %d) send_batch failed %d",
++ syslog(LOG_DEBUG, "%s(%p, %d, %d) send_batch failed %d",
+ "nft_send_rule", rule, (int)cmd, (int)chain_type, result);
+ }
+ }
+@@ -1517,7 +1517,7 @@ send_batch(struct mnl_nlmsg_batch *batch
+ errno = 0;
+ ret = mnl_cb_run(buf, n, 0, mnl_portid, NULL, NULL);
+ if (ret <= -1 /*== MNL_CB_ERROR*/) {
+- syslog(LOG_ERR, "%s: mnl_cb_run returned %d: %m",
++ syslog(LOG_DEBUG, "%s: mnl_cb_run returned %d: %m",
+ "send_batch", ret);
+ return -4;
+ }
+--- a/netfilter_nft/nftpinhole.c
++++ b/netfilter_nft/nftpinhole.c
+@@ -90,12 +90,12 @@ int add_pinhole(const char * ifname,
+ if (inet_pton(AF_INET6, rem_host, &rhost_addr) == 1) {
+ rhost_addr_p = &rhost_addr;
+ } else {
+- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", rem_host);
++ syslog(LOG_DEBUG, "Failed to parse remote IPv6 address (%s)", rem_host);
+ }
+ }
+
+ if (inet_pton(AF_INET6, int_client, &ihost_addr) != 1) {
+- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", int_client);
++ syslog(LOG_DEBUG, "Failed to parse IPv6 address (%s)", int_client);
+ }
+
+ snprintf(comment, NFT_DESCR_SIZE,
+@@ -133,7 +133,7 @@ find_pinhole(const char * ifname,
+
+ if (rem_host && rem_host[0] != '\0' && rem_host[0] != '*') {
+ if (inet_pton(AF_INET6, rem_host, &saddr) < 1) {
+- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", rem_host);
++ syslog(LOG_DEBUG, "Failed to parse remote IPv6 address (%s)", rem_host);
+ memset(&saddr, 0, sizeof(struct in6_addr));
+ }
+ } else {
+@@ -141,7 +141,7 @@ find_pinhole(const char * ifname,
+ }
+
+ if (inet_pton(AF_INET6, int_client, &daddr) < 1) {
+- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", int_client);
++ syslog(LOG_DEBUG, "Failed to parse IPv6 address (%s)", int_client);
+ memset(&daddr, 0, sizeof(struct in6_addr));
+ }
+
+@@ -163,7 +163,7 @@ find_pinhole(const char * ifname,
+ (0 == memcmp(&daddr, &p->daddr6, sizeof(struct in6_addr)))) {
+
+ if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
+- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);
++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/
+ continue;
+ }
+
+@@ -400,7 +400,7 @@ get_pinhole_info(unsigned short uid,
+ if (timestamp) {
+ int uid_temp;
+ if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid_temp, &ts) != 2) {
+- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);
++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/
+ continue;
+ }
+
+@@ -464,12 +464,12 @@ clean_pinhole_list(unsigned int * next_t
+ continue;
+
+ if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
+- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);
++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/
+ continue;
+ }
+
+ if (ts <= (unsigned int)current_time) {
+- syslog(LOG_INFO, "removing expired pinhole '%s'", p->desc);
++ syslog(LOG_INFO, "Expire IPv6 port map [?]:?/?");
+ r = rule_del_handle(p);
+ nft_send_rule(r, NFT_MSG_DELRULE, RULE_CHAIN_FILTER);
+ n++;
+--- a/options.c
++++ b/options.c
+@@ -35,8 +35,9 @@ static const struct {
+ { UPNPEXT_IFNAME6, "ext_ifname6" },
+ #endif
+ { UPNPEXT_IP, "ext_ip" },
+- { UPNPEXT_ALLOW_PRIVATE_IPV4, "ext_allow_private_ipv4" },
+- { UPNPEXT_PERFORM_STUN, "ext_perform_stun" },
++ { UPNPEXT_ALLOW_CGNAT, "ext_allow_cgnat" },
++ { UPNPEXT_ALLOW_CGNAT, "ext_perform_stun" },
++ { UPNPEXT_REPORT_PRIVATE_IPV4, "ext_allow_private_ipv4" },
+ { UPNPEXT_STUN_HOST, "ext_stun_host" },
+ { UPNPEXT_STUN_PORT, "ext_stun_port" },
+ { UPNPLISTENING_IP, "listening_ip" },
+@@ -44,6 +45,8 @@ static const struct {
+ { UPNPIPV6_LISTENING_IP, "ipv6_listening_ip" },
+ { UPNPIPV6_DISABLE, "ipv6_disable" },
+ #endif /* ENABLE_IPV6 */
++ { DEFAULT_MIN_PORT, "default_min_port" },
++ { DEFAULT_MAX_PORT, "default_max_port" },
+ { UPNPPORT, "port" },
+ { UPNPPORT, "http_port" }, /* "port" and "http_port" are synonims */
+ #ifdef ENABLE_HTTPS
+--- a/options.h
++++ b/options.h
+@@ -25,8 +25,8 @@ enum upnpconfigoptions {
+ UPNPEXT_IFNAME6, /*!< ext_ifname6 */
+ #endif
+ UPNPEXT_IP, /*!< ext_ip */
+- UPNPEXT_ALLOW_PRIVATE_IPV4, /*!< ext_allow_private_ipv4 */
+- UPNPEXT_PERFORM_STUN, /*!< ext_perform_stun */
++ UPNPEXT_ALLOW_CGNAT, /*!< ext_allow_cgnat */
++ UPNPEXT_REPORT_PRIVATE_IPV4, /*!< old ext_allow_private_ipv4 */
+ UPNPEXT_STUN_HOST, /*!< ext_stun_host */
+ UPNPEXT_STUN_PORT, /*!< ext_stun_port */
+ UPNPLISTENING_IP, /*!< listening_ip */
+@@ -34,6 +34,8 @@ enum upnpconfigoptions {
+ UPNPIPV6_LISTENING_IP, /*!< listening address for IPv6 */
+ UPNPIPV6_DISABLE, /*!< ipv6_disable */
+ #endif /* ENABLE_IPV6 */
++ DEFAULT_MIN_PORT, /*!< default_min_port */
++ DEFAULT_MAX_PORT, /*!< default_max_port */
+ UPNPPORT, /*!< "port" / "http_port" */
+ #ifdef ENABLE_HTTPS
+ UPNPHTTPSPORT, /*!< "https_port" */
+--- a/pcpserver.c
++++ b/pcpserver.c
+@@ -631,7 +631,7 @@ static int CheckExternalAddress(pcp_info
+ }
+
+ if (!IN6_ARE_ADDR_EQUAL(pcp_msg_info->ext_ip, &external_addr)) {
+- syslog(LOG_ERR,
++ syslog(LOG_DEBUG,
+ "PCP: External IP in request didn't match interface IP \n");
+ #ifdef DEBUG
+ {
+@@ -943,7 +943,7 @@ static int CreatePCPMap_NAT(pcp_info_t *
+ ((struct in_addr*)pcp_msg_info->mapped_ip->s6_addr)[3],
+ pcp_msg_info->int_port, pcp_msg_info->desc)))) {
+ if (pcp_msg_info->pfailure_present) {
+- return PCP_ERR_CANNOT_PROVIDE_EXTERNAL;
++ return PCP_ERR_NOT_AUTHORIZED;
+ }
+ pcp_msg_info->ext_port++;
+ if (pcp_msg_info->ext_port == 0) { /* skip port zero */
+@@ -955,7 +955,7 @@ static int CreatePCPMap_NAT(pcp_info_t *
+ #ifdef CHECK_PORTINUSE
+ if (port_in_use(ext_if_name, pcp_msg_info->ext_port, pcp_msg_info->protocol,
+ pcp_msg_info->mapped_str, pcp_msg_info->int_port) > 0) {
+- syslog(LOG_INFO, "port %hu protocol %s already in use",
++ syslog(LOG_INFO, "Reject IPv4 port map %hu:?:?/%s via PCP reason port in use",
+ pcp_msg_info->ext_port,
+ proto_itoa(pcp_msg_info->protocol));
+ pcp_msg_info->ext_port++;
+@@ -980,9 +980,9 @@ static int CreatePCPMap_NAT(pcp_info_t *
+ return PCP_ERR_CANNOT_PROVIDE_EXTERNAL;
+ }
+ } else {
+- syslog(LOG_INFO, "port %hu %s already redirected to %s:%hu, replacing",
+- pcp_msg_info->ext_port, proto_itoa(pcp_msg_info->protocol),
+- iaddr_old, iport_old);
++ syslog(LOG_INFO, "Renew IPv4 port map %hu:%s:%hu/%s lifetime %d via PCP",
++ pcp_msg_info->ext_port, iaddr_old, iport_old,
++ proto_itoa(pcp_msg_info->protocol), pcp_msg_info->lifetime);
+ /* remove and then add again */
+ if (_upnp_delete_redir(pcp_msg_info->ext_port,
+ pcp_msg_info->protocol)==0) {
+@@ -1016,6 +1016,14 @@ static int CreatePCPMap_FW(pcp_info_t *p
+ int uid;
+ int r;
+ char desc[64];
++
++ if (!IN6_IS_ADDR_V4MAPPED(pcp_msg_info->mapped_ip) &&
++ (pcp_msg_info->int_port < default_min_port || pcp_msg_info->int_port > default_max_port)) {
++ syslog(LOG_INFO, "Reject IPv6 port map [%s]:%hu/%s via PCP reason default-min/max-port",
++ pcp_msg_info->mapped_str, pcp_msg_info->int_port, proto_itoa(pcp_msg_info->protocol));
++ return PCP_ERR_NOT_AUTHORIZED;
++ }
++
+ /* first check if pinhole already exists */
+ uid = upnp_find_inboundpinhole(NULL, 0,
+ pcp_msg_info->mapped_str,
+@@ -1027,13 +1035,14 @@ static int CreatePCPMap_FW(pcp_info_t *p
+ /* pinhole already exists, updating */
+ if (0 != strcmp(desc, pcp_msg_info->desc)) {
+ /* nonce does not match */
+- syslog(LOG_ERR, "Unauthorized to update pinhole : \"%s\" != \"%s\"",
+- desc, pcp_msg_info->desc);
++ syslog(LOG_INFO, "Reject IPv6 port map [%s]:%hu/%s via PCP nonce %s reason nonce not matching",
++ pcp_msg_info->mapped_str, pcp_msg_info->int_port,
++ proto_itoa(pcp_msg_info->protocol), pcp_msg_info->desc + 8);
+ return PCP_ERR_NOT_AUTHORIZED;
+ }
+- syslog(LOG_INFO, "updating pinhole %d to %s:%hu %s",
+- uid, pcp_msg_info->mapped_str, pcp_msg_info->int_port,
+- proto_itoa(pcp_msg_info->protocol));
++ syslog(LOG_INFO, "Renew IPv6 port map [%s]:%hu/%s lifetime %d via PCP",
++ pcp_msg_info->mapped_str, pcp_msg_info->int_port,
++ proto_itoa(pcp_msg_info->protocol), pcp_msg_info->lifetime);
+ r = upnp_update_inboundpinhole((unsigned short)uid, pcp_msg_info->lifetime);
+ return r >= 0 ? PCP_SUCCESS : PCP_ERR_NO_RESOURCES;
+ } else {
+@@ -1089,19 +1098,25 @@ static void CreatePCPMap(pcp_info_t *pcp
+ {
+ int r;
+
+- if (pcp_msg_info->is_fw)
++ if (pcp_msg_info->is_fw) {
+ r = CreatePCPMap_FW(pcp_msg_info);
+- else
++ syslog(LOG_INFO,
++ "%s IPv6 port map [%s]:%hu/%s remote IP ?/? remote port ? lifetime %d via PCP nonce %s %s",
++ r == PCP_SUCCESS ? "Add" : "Reject",
++ pcp_msg_info->mapped_str, pcp_msg_info->int_port,
++ proto_itoa(pcp_msg_info->protocol), pcp_msg_info->lifetime,
++ pcp_msg_info->desc + 8, r == PCP_ERR_NOT_AUTHORIZED ? "reason default-min/max-port" : "");
++ } else {
+ r = CreatePCPMap_NAT(pcp_msg_info);
++ syslog(LOG_INFO,
++ "%s IPv4 port map %hu:%s:%hu/%s remote IP ?/? remote port ? lifetime %d via PCP nonce %s %s",
++ r == PCP_SUCCESS ? "Add" : "Reject",
++ pcp_msg_info->ext_port,
++ pcp_msg_info->mapped_str, pcp_msg_info->int_port,
++ proto_itoa(pcp_msg_info->protocol), pcp_msg_info->lifetime,
++ pcp_msg_info->desc + 8, r == PCP_ERR_NOT_AUTHORIZED ? "reason ACL/default-min/max-port" : "");
++ }
+ pcp_msg_info->result_code = r;
+- syslog(r == PCP_SUCCESS ? LOG_INFO : LOG_ERR,
+- "PCP MAP: %s mapping %s %hu->%s:%hu '%s'",
+- r == PCP_SUCCESS ? "added" : "failed to add",
+- proto_itoa(pcp_msg_info->protocol),
+- pcp_msg_info->ext_port,
+- pcp_msg_info->mapped_str,
+- pcp_msg_info->int_port,
+- pcp_msg_info->desc);
+ }
+
+ static void DeletePCPMap(pcp_info_t *pcp_msg_info)
+@@ -1136,8 +1151,8 @@ static void DeletePCPMap(pcp_info_t *pcp
+ if(0 != strcmp(desc, pcp_msg_info->desc)) {
+ /* nonce does not match */
+ pcp_msg_info->result_code = PCP_ERR_NOT_AUTHORIZED;
+- syslog(LOG_ERR, "Unauthorized to remove PCP mapping internal port %hu, protocol %s",
+- iport, proto_itoa(pcp_msg_info->protocol));
++ syslog(LOG_INFO, "Reject IPv6 port map [?]:%hu/%s via PCP nonce %s reason nonce not matching",
++ iport, proto_itoa(pcp_msg_info->protocol), pcp_msg_info->desc + 8);
+ return;
+ } else {
+ r = _upnp_delete_redir(eport2, proto2);
+@@ -1154,15 +1169,15 @@ static void DeletePCPMap(pcp_info_t *pcp
+ desc, sizeof(desc),
+ NULL /* lifetime */);
+ if (uid < 0) {
+- syslog(LOG_ERR, "Failed to find mapping to %s:%hu, protocol %s",
+- pcp_msg_info->mapped_str, iport, proto_itoa(pcp_msg_info->protocol));
++ syslog(LOG_INFO, "Non-existing IPv6 port map to delete [%s]:%hu/%s via PCP nonce %s",
++ pcp_msg_info->mapped_str, iport, proto_itoa(pcp_msg_info->protocol), pcp_msg_info->desc + 8);
+ return;
+ } else {
+ if(0 != strcmp(desc, pcp_msg_info->desc)) {
+ /* nonce does not match */
+ pcp_msg_info->result_code = PCP_ERR_NOT_AUTHORIZED;
+- syslog(LOG_ERR, "Unauthorized to remove PCP mapping internal port %hu, protocol %s",
+- iport, proto_itoa(pcp_msg_info->protocol));
++ syslog(LOG_INFO, "Reject IPv6 port map [?]:%hu/%s via PCP nonce %s reason nonce not matching",
++ iport, proto_itoa(pcp_msg_info->protocol), pcp_msg_info->desc + 8);
+ return;
+ } else {
+ r = upnp_delete_inboundpinhole(uid);
+@@ -1173,11 +1188,21 @@ static void DeletePCPMap(pcp_info_t *pcp
+ #endif /* ENABLE_UPNPPINHOLE */
+ }
+ if (r >= 0) {
+- syslog(LOG_INFO, "PCP: %s port %hu mapping removed",
+- proto==IPPROTO_TCP?"TCP":"UDP", (pcp_msg_info->is_fw ? iport : eport2));
++ if (!pcp_msg_info->is_fw) {
++ syslog(LOG_INFO, "Delete IPv4 port map %hu:%s:%hu/%s via PCP nonce %s",
++ iport, pcp_msg_info->mapped_str, iport, proto_itoa(proto), pcp_msg_info->desc + 8);
++ } else {
++ syslog(LOG_INFO, "Delete IPv6 port map [%s]:%hu/%s via PCP nonce %s",
++ pcp_msg_info->mapped_str, iport, proto_itoa(proto), pcp_msg_info->desc + 8);
++ }
+ } else {
+- syslog(LOG_ERR, "Failed to remove PCP mapping to %s:%hu %s",
+- pcp_msg_info->mapped_str, iport, proto_itoa(proto));
++ if (!pcp_msg_info->is_fw) {
++ syslog(LOG_INFO, "Non-existing IPv4 port map to delete ?:%s:%hu/%s via PCP nonce %s",
++ pcp_msg_info->mapped_str, iport, proto_itoa(proto), pcp_msg_info->desc + 8);
++ } else {
++ syslog(LOG_INFO, "Non-existing IPv6 port map to delete %s:%hu/%s via PCP nonce %s",
++ pcp_msg_info->mapped_str, iport, proto_itoa(proto), pcp_msg_info->desc + 8);
++ }
+ pcp_msg_info->result_code = PCP_ERR_NO_RESOURCES;
+ }
+ }
+@@ -1199,6 +1224,7 @@ static int ValidatePCPMsg(pcp_info_t *pc
+
+ if (pcp_msg_info->thirdp_ip) {
+ if (!GETFLAG(PCP_ALLOWTHIRDPARTYMASK)) {
++ syslog(LOG_INFO, "Reject IPv? port map ?:?:?/? via PCP reason for third-party from ?");
+ pcp_msg_info->result_code = PCP_ERR_UNSUPP_OPTION;
+ return 0;
+ }
+@@ -1253,8 +1279,8 @@ static int ValidatePCPMsg(pcp_info_t *pc
+ snprintf(pcp_msg_info->desc, sizeof(pcp_msg_info->desc),
+ "PCP %s %08x%08x%08x",
+ getPCPOpCodeStr(pcp_msg_info->opcode),
+- pcp_msg_info->nonce[0],
+- pcp_msg_info->nonce[1], pcp_msg_info->nonce[2]);
++ ntohl(pcp_msg_info->nonce[0]),
++ ntohl(pcp_msg_info->nonce[1]), ntohl(pcp_msg_info->nonce[2]));
+ break;
+ }
+ return 1;
+@@ -1399,7 +1425,7 @@ static int processPCPRequest(void * req,
+ CreatePCPMap(pcp_msg_info);
+ }
+ } else {
+- syslog(LOG_ERR, "PCP: Invalid PCP v2 MAP message.");
++ syslog(LOG_DEBUG, "PCP: Invalid PCP v2 MAP message.");
+ return pcp_msg_info->result_code;
+ }
+
+@@ -1609,7 +1635,7 @@ int ProcessIncomingPCPPacket(int s, unsi
+ if (!GETFLAG(PCP_ALLOWTHIRDPARTYMASK)) {
+ lan_addr = get_lan_for_peer(senderaddr);
+ if(lan_addr == NULL) {
+- syslog(LOG_WARNING, "PCP packet sender %s not from a LAN, ignoring",
++ syslog(LOG_DEBUG, "PCP packet sender %s not from a LAN, ignoring",
+ addr_str);
+ return 0;
+ }
+@@ -1730,7 +1756,7 @@ void PCPSendUnsolicitedAnnounce(int * so
+ }
+ len = sendto_or_schedule(sockets[i], buff, PCP_MIN_LEN, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
+ if( len < 0 ) {
+- syslog(LOG_ERR, "PCPSendUnsolicitedAnnounce(sockets[%d]) sendto(): %m", i);
++ syslog(LOG_DEBUG, "PCPSendUnsolicitedAnnounce(sockets[%d]) sendto(): %m", i);
+ }
+ }
+ #ifdef ENABLE_IPV6
+@@ -1741,7 +1767,7 @@ void PCPSendUnsolicitedAnnounce(int * so
+ addr6.sin6_port = htons(5350);
+ len = sendto_or_schedule(socket6, buff, PCP_MIN_LEN, 0, (struct sockaddr *)&addr6, sizeof(struct sockaddr_in6));
+ if( len < 0 ) {
+- syslog(LOG_ERR, "PCPSendUnsolicitedAnnounce() IPv6 sendto(): %m");
++ syslog(LOG_DEBUG, "PCPSendUnsolicitedAnnounce() IPv6 sendto(): %m");
+ }
+ }
+ #endif /* ENABLE_IPV6 */
+--- a/pf/obsdrdr.c
++++ b/pf/obsdrdr.c
+@@ -1302,7 +1302,7 @@ priv_delete_redirect_rule_check_desc(con
+ }
+ }
+ if (r == -2)
+- syslog(LOG_NOTICE, "could not find redirect rule to delete eport=%hu", eport);
++ syslog(LOG_DEBUG, "could not find redirect rule to delete eport=%hu", eport);
+ release_ticket(dev, tnum);
+ return r;
+ }
+--- a/pf/pfpinhole.c
++++ b/pf/pfpinhole.c
+@@ -246,12 +246,12 @@ int find_pinhole(const char * ifname,
+ (0 == memcmp(&daddr, &RULE.dst.addr.v.a.addr.v6, sizeof(struct in6_addr)))) {
+ #ifdef USE_LIBPFCTL
+ if(sscanf(RULE.label[0], PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
+- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);
++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);*/
+ continue;
+ }
+ #else /* USE_LIBPFCTL */
+ if(sscanf(RULE.label, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
+- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);
++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);*/
+ continue;
+ }
+ #endif /* USE_LIBPFCTL */
+@@ -565,7 +565,7 @@ int clean_pinhole_list(unsigned int * ne
+ return -1;
+ }
+ if(sscanf(RULE.label[0], PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
+- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);
++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);*/
+ continue;
+ }
+ #else /* USE_LIBPFCTL */
+@@ -576,7 +576,7 @@ int clean_pinhole_list(unsigned int * ne
+ return -1;
+ }
+ if(sscanf(RULE.label, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
+- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);
++ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);*/
+ continue;
+ }
+ #endif /* USE_LIBPFCTL */
+@@ -589,9 +589,9 @@ int clean_pinhole_list(unsigned int * ne
+ pr.ticket = ri.ticket;
+ pr.nr = i;
+ strlcpy(pr.anchor, anchor_name, MAXPATHLEN);
+- syslog(LOG_INFO, "removing expired pinhole '%s'", RULE.label[0]);
++ syslog(LOG_INFO, "Expire IPv6 port map [?]:?/?");
+ #else /* USE_LIBPFCTL */
+- syslog(LOG_INFO, "removing expired pinhole '%s'", RULE.label);
++ syslog(LOG_INFO, "Expire IPv6 port map [?]:?/?");
+ #endif /* USE_LIBPFCTL */
+ pr.action = PF_CHANGE_GET_TICKET;
+ if(ioctl(dev, DIOCCHANGERULE, &pr) < 0) {
+--- a/upnpdescgen.c
++++ b/upnpdescgen.c
+@@ -1311,7 +1311,7 @@ genEventVars(int * len, const struct ser
+ else {
+ struct in_addr addr;
+ char ext_ip_addr[INET_ADDRSTRLEN];
+- if(getifaddr(ext_if_name, ext_ip_addr, INET_ADDRSTRLEN, &addr, NULL) < 0 || (!GETFLAG(ALLOWPRIVATEIPV4MASK) && addr_is_reserved(&addr))) {
++ if(getifaddr(ext_if_name, ext_ip_addr, INET_ADDRSTRLEN, &addr, NULL) < 0 || (!GETFLAG(REPORTPRIVATEIPV4MASK) && addr_is_reserved(&addr))) {
+ str = strcat_str(str, len, &tmplen, "0.0.0.0");
+ } else {
+ str = strcat_str(str, len, &tmplen, ext_ip_addr);
+--- a/upnpglobalvars.h
++++ b/upnpglobalvars.h
+@@ -102,7 +102,7 @@ extern int runtime_flags;
+ #define PERFORMSTUNMASK 0x1000
+
+ /*! \brief Allow the external IPv4 to be private */
+-#define ALLOWPRIVATEIPV4MASK 0x2000
++#define REPORTPRIVATEIPV4MASK 0x2000
+ /*! \brief do not stop on STUN detecting restricive NAT */
+ #define ALLOWFILTEREDSTUNMASK 0x4000
+
+--- a/upnphttp.c
++++ b/upnphttp.c
+@@ -554,7 +554,7 @@ ProcessHTTPPOST_upnphttp(struct upnphttp
+ if(h->req_soapActionOff > 0)
+ {
+ /* we can process the request */
+- syslog(LOG_INFO, "SOAPAction: %.*s",
++ syslog(LOG_DEBUG, "SOAPAction: %.*s",
+ h->req_soapActionLen, h->req_buf + h->req_soapActionOff);
+ ExecuteSoapAction(h,
+ h->req_buf + h->req_soapActionOff,
+@@ -854,7 +854,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt
+ for(i = 0; i<15 && *p != '\r'; i++)
+ HttpVer[i] = *(p++);
+ HttpVer[i] = '\0';
+- syslog(LOG_INFO, "HTTP REQUEST from %s : %s %s (%s)",
++ syslog(LOG_DEBUG, "HTTP REQUEST from %s : %s %s (%s)",
+ h->clientaddr_str, HttpCommand, HttpUrl, HttpVer);
+ ParseHttpHeaders(h);
+ if(h->req_contentlen > CONTENT_LENGTH_LIMIT) {
+@@ -940,7 +940,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt
+ return;
+ }
+ #endif
+- syslog(LOG_NOTICE, "%s not found, responding ERROR 404", HttpUrl);
++ syslog(LOG_DEBUG, "%s not found, responding ERROR 404", HttpUrl);
+ Send404(h);
+ }
+ #ifdef ENABLE_EVENTS
+--- a/upnppermissions.c
++++ b/upnppermissions.c
+@@ -23,6 +23,9 @@
+ #include "upnppermissions.h"
+ #include "upnputils.h"
+
++uint16_t default_min_port = 1;
++uint16_t default_max_port = 65535;
++
+ static int
+ isodigit(char c)
+ {
+@@ -523,14 +526,19 @@ check_upnp_rule_against_permissions(cons
+ if(match_permission(permary + i, eport, address, iport, desc))
+ {
+ syslog(LOG_DEBUG,
+- "UPnP permission rule %d matched : port mapping %s",
++ "ACL entry %d matched, port mapping %s",
+ i, (permary[i].type == UPNPPERM_ALLOW)?"accepted":"rejected"
+ );
+ return (permary[i].type == UPNPPERM_ALLOW);
+ }
+ }
+- syslog(LOG_DEBUG, "no permission rule matched : accept by default (n_perms=%d)", n_perms);
+- return 1; /* Default : accept */
++
++ if (iport < default_min_port || eport < default_min_port || iport > default_max_port || eport > default_max_port) {
++ return 0;
++ } else {
++ syslog(LOG_DEBUG, "No ACL entry matched, ACL accepted by default");
++ return 1; /* Default : accept */
++ }
+ }
+
+ void
+--- a/upnppermissions.h
++++ b/upnppermissions.h
+@@ -18,6 +18,9 @@
+ #include
+ #endif
+
++extern uint16_t default_min_port;
++extern uint16_t default_max_port;
++
+ /* UPnP permission rule samples:
+ * allow 1024-65535 192.168.3.0/24 1024-65535
+ * deny 0-65535 192.168.1.125/32 0-65535 */
+--- a/upnppinhole.c
++++ b/upnppinhole.c
+@@ -110,7 +110,7 @@ lease_file6_add(const char * rem_client,
+
+ fd = fopen( lease_file6, "a");
+ if (fd==NULL) {
+- syslog(LOG_ERR, "could not open lease file: %s", lease_file);
++ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file);
+ return -1;
+ }
+
+@@ -398,7 +398,7 @@ int lease_file6_expire(void)
+ // Internal Host
+ p = strchr(line, ';');
+ if(!p) {
+- syslog(LOG_ERR, "unrecognized data in lease file");
++ syslog(LOG_DEBUG, "unrecognized data in lease file (non-critical bug)");
+ continue;
+ }
+ *(p++) = '\0';
+@@ -489,7 +489,7 @@ int reload_from_lease_file6(void)
+ if(!lease_file6) return -1;
+ fd = fopen( lease_file6, "r");
+ if (fd==NULL) {
+- syslog(LOG_ERR, "could not open lease file: %s", lease_file6);
++ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file6);
+ return -1;
+ }
+ if(unlink(lease_file6) < 0) {
+@@ -560,11 +560,6 @@ int reload_from_lease_file6(void)
+ while(isspace(*p) && (p > desc))
+ *(p--) = '\0';
+
+- /* Ensure port is non-privileged and RemoteHost is valid */
+- if (int_port < 1024) {
+- syslog(LOG_WARNING, "ignoring lease file pinhole with privileged port %hu", int_port);
+- continue;
+- }
+ if (rem_client && rem_client[0] != '\0' && rem_client[0] != '*') {
+ struct in6_addr rem_addr;
+ if (inet_pton(AF_INET6, rem_client, &rem_addr) != 1) {
+@@ -575,7 +570,7 @@ int reload_from_lease_file6(void)
+
+ if(timestamp > 0) {
+ if(timestamp <= (unsigned int)current_unix_time) {
+- syslog(LOG_NOTICE, "already expired lease in lease file");
++ syslog(LOG_INFO, "Expire IPv6 port map ?:[?]:[?]/? reason lease file reload");
+ continue;
+ } else {
+ leaseduration = timestamp - current_unix_time;
+@@ -652,7 +647,8 @@ upnp_add_inboundpinhole(const char * rad
+
+ *uid = upnp_find_inboundpinhole(raddr, rport, iaddr, iport, proto, NULL, 0, NULL);
+ if(*uid >= 0) {
+- syslog(LOG_INFO, "Pinhole for inbound traffic from [%s]:%hu to [%s]:%hu with proto %d found uid=%d. Updating it.", raddr, rport, iaddr, iport, proto, *uid);
++ syslog(LOG_INFO, "Renew IPv6 port map [%s]:%hu/%s remote IP %s remote port %hu lifetime %d via UPnP IGDv2 IPv6 UID %d",
++ iaddr, iport, proto_itoa(proto), raddr[0] != '\0' ? raddr : "any", rport, leasetime, *uid);
+ r = upnp_update_inboundpinhole(*uid, leasetime);
+ #ifdef ENABLE_LEASEFILE
+ if (r >= 0) {
+--- a/upnpredirect.c
++++ b/upnpredirect.c
+@@ -67,7 +67,7 @@ lease_file_add(const char * rhost,
+
+ fd = fopen( lease_file, "a");
+ if (fd==NULL) {
+- syslog(LOG_ERR, "could not open lease file: %s", lease_file);
++ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file);
+ return -1;
+ }
+
+@@ -196,7 +196,7 @@ int reload_from_lease_file(void)
+ if(!lease_file) return -1;
+ fd = fopen( lease_file, "r");
+ if (fd==NULL) {
+- syslog(LOG_ERR, "could not open lease file: %s", lease_file);
++ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file);
+ return -1;
+ }
+ if(unlink(lease_file) < 0) {
+@@ -266,7 +266,7 @@ int reload_from_lease_file(void)
+ timestamp += current_time; /* convert to our time */
+ #else
+ if(timestamp <= (unsigned int)current_unix_time) {
+- syslog(LOG_NOTICE, "already expired lease in lease file (%hu=>%s:%hu %s)",
++ syslog(LOG_INFO, "Expire IPv4 port map %hu:%s:%hu/%s reason lease file reload",
+ eport, iaddr, iport, proto);
+ continue;
+ } else {
+@@ -348,8 +348,8 @@ upnp_redirect(const char * rhost, unsign
+
+ if(!check_upnp_rule_against_permissions(upnppermlist, num_upnpperm,
+ eport, address, iport, desc)) {
+- syslog(LOG_INFO, "redirection permission check failed for "
+- "%hu->%s:%hu %s %s", eport, iaddr, iport, protocol, desc);
++ syslog(LOG_INFO, "Reject IPv4 port map %hu:%s:%hu/%s via UPnP IGD reason ACL/default-min/max-port",
++ eport, iaddr, iport, protocol);
+ return -3;
+ }
+
+@@ -397,8 +397,8 @@ upnp_redirect(const char * rhost, unsign
+ ((rhost == NULL && rhost_old[0]=='\0') ||
+ (rhost && (strcmp(rhost, "*") == 0) && rhost_old[0]=='\0') ||
+ (rhost && (strcmp(rhost, rhost_old) == 0)))) {
+- syslog(LOG_INFO, "updating existing port mapping %hu %s (rhost '%s') => %s:%hu",
+- eport, protocol, rhost_old, iaddr_old, iport_old);
++ syslog(LOG_INFO, "Renew IPv4 port map %hu:%s:%hu/%s remote IP %s lifetime %d via UPnP IGD",
++ eport, iaddr_old, iport_old, protocol, rhost_old[0] != '\0' ? rhost_old : "any", leaseduration);
+ timestamp = (leaseduration > 0) ? upnp_time() + leaseduration : 0;
+ if(iport != iport_old) {
+ r = update_portmapping(ext_if_name, eport, proto, iport, desc, timestamp);
+@@ -413,19 +413,19 @@ upnp_redirect(const char * rhost, unsign
+ #endif /* ENABLE_LEASEFILE */
+ return r;
+ } else {
+- syslog(LOG_INFO, "port %hu %s (rhost '%s') already redirected to %s:%hu",
+- eport, protocol, rhost_old, iaddr_old, iport_old);
++ syslog(LOG_INFO, "Reject IPv4 port map %hu:%s:%hu/%s via UPnP IGD reason port in use",
++ eport, iaddr_old, iport_old, protocol);
+ return -2;
+ }
+ #ifdef CHECK_PORTINUSE
+ } else if (port_in_use(ext_if_name, eport, proto, iaddr, iport) > 0) {
+- syslog(LOG_INFO, "port %hu protocol %s already in use",
+- eport, protocol);
++ syslog(LOG_INFO, "Reject IPv4 port map %hu:%s:%hu/%s via UPnP IGD reason port in use",
++ eport, iaddr, iport, protocol);
+ return -4;
+ #endif /* CHECK_PORTINUSE */
+ } else {
+ timestamp = (leaseduration > 0) ? upnp_time() + leaseduration : 0;
+- syslog(LOG_INFO, "redirecting port %hu to %s:%hu protocol %s for: %s",
++ syslog(LOG_DEBUG, "redirecting port %hu to %s:%hu protocol %s for: %s",
+ eport, iaddr, iport, protocol, desc);
+ return upnp_redirect_internal(rhost, eport, iaddr, iport, proto,
+ desc, timestamp);
+@@ -438,8 +438,6 @@ upnp_redirect_internal(const char * rhos
+ int proto, const char * desc,
+ unsigned int timestamp)
+ {
+- /*syslog(LOG_INFO, "redirecting port %hu to %s:%hu protocol %s for: %s",
+- eport, iaddr, iport, protocol, desc); */
+ if(disable_port_forwarding)
+ return -1;
+ if(add_redirect_rule2(ext_if_name, rhost, eport, iaddr, iport, proto,
+@@ -450,8 +448,6 @@ upnp_redirect_internal(const char * rhos
+ #ifdef ENABLE_LEASEFILE
+ lease_file_add(rhost, eport, iaddr, iport, proto, desc, timestamp);
+ #endif
+-/* syslog(LOG_INFO, "creating pass rule to %s:%hu protocol %s for: %s",
+- iaddr, iport, protocol, desc);*/
+ if(add_filter_rule2(ext_if_name, rhost, iaddr, eport, iport, proto, desc) < 0) {
+ /* clean up the redirect rule */
+ #if !defined(__linux__)
+@@ -571,7 +567,7 @@ _upnp_delete_redir(unsigned short eport,
+ int
+ upnp_delete_redirection(unsigned short eport, const char * protocol)
+ {
+- syslog(LOG_INFO, "removing redirect rule port %hu %s", eport, protocol);
++ syslog(LOG_INFO, "Delete IPv4 port map %hu:?:?/%s via UPnP IGD", eport, protocol);
+ return _upnp_delete_redir(eport, proto_atoi(protocol));
+ }
+
+@@ -675,7 +671,7 @@ get_upnp_rules_state_list(int max_rules_
+ {
+ if(tmp->to_remove)
+ {
+- syslog(LOG_NOTICE, "remove port mapping %hu %s because it has expired",
++ syslog(LOG_INFO, "Expire IPv4 port map %hu:?:?/%s",
+ tmp->eport, proto_itoa(tmp->proto));
+ _upnp_delete_redir(tmp->eport, tmp->proto);
+ *p = tmp->next;
+--- a/upnpsoap.c
++++ b/upnpsoap.c
+@@ -412,18 +412,9 @@ GetExternalIPAddress(struct upnphttp * h
+ else
+ {
+ struct in_addr addr;
+- if(getifaddr(ext_if_name, ext_ip_addr, INET_ADDRSTRLEN, &addr, NULL) < 0)
+- {
+- syslog(LOG_ERR, "Failed to get ip address for interface %s",
+- ext_if_name);
+- ext_ip_addr[0] = '\0';
+- } else if (addr_is_reserved(&addr)) {
+- if (GETFLAG(ALLOWPRIVATEIPV4MASK)) {
+- syslog(LOG_WARNING, "IGNORED : private/reserved address %s is not suitable for external IP", ext_ip_addr);
+- } else {
+- syslog(LOG_NOTICE, "private/reserved address %s is not suitable for external IP", ext_ip_addr);
++ if (getifaddr(ext_if_name, ext_ip_addr, INET_ADDRSTRLEN, &addr, NULL) < 0 ||
++ (addr_is_reserved(&addr) && !GETFLAG(REPORTPRIVATEIPV4MASK))) {
+ ext_ip_addr[0] = '\0';
+- }
+ }
+ }
+ #else
+@@ -545,7 +536,7 @@ AddPortMapping(struct upnphttp * h, cons
+ }
+ else
+ {
+- syslog(LOG_ERR, "Failed to convert hostname '%s' to ip address", int_ip);
++ syslog(LOG_INFO, "Failed to parse or resolve to (%s) IPv4 address via UPnP IGD", int_ip);
+ ClearNameValueList(&data);
+ SoapError(h, 402, "Invalid Args");
+ return;
+@@ -557,8 +548,8 @@ AddPortMapping(struct upnphttp * h, cons
+ {
+ if(h->clientaddr.s_addr != result_ip.s_addr)
+ {
+- syslog(LOG_INFO, "Client %s tried to redirect port to %s",
+- inet_ntoa(h->clientaddr), int_ip);
++ syslog(LOG_INFO, "Reject IPv4 port map ?:%s:?/? via UPnP IGD reason for third-party from %s",
++ int_ip, inet_ntoa(h->clientaddr));
+ ClearNameValueList(&data);
+ #ifdef IGD_V2
+ SoapError(h, 606, "Action not authorized");
+@@ -606,9 +597,9 @@ AddPortMapping(struct upnphttp * h, cons
+ leaseduration = 604800;
+ #endif
+
+- syslog(LOG_INFO, "%s: ext port %hu to %s:%hu protocol %s for: %s leaseduration=%u rhost=%s",
+- action, eport, int_ip, iport, protocol, desc, leaseduration,
+- r_host ? r_host : "NULL");
++ syslog(LOG_INFO, "Add IPv4 port map %hu:%s:%hu/%s remote IP %s lifetime %u via UPnP IGD",
++ eport, int_ip, iport, protocol, r_host[0] != '\0' ? r_host : "any",
++ leaseduration);
+
+ r = upnp_redirect(r_host, eport, int_ip, iport, protocol, desc, leaseduration);
+
+@@ -750,7 +741,7 @@ AddAnyPortMapping(struct upnphttp * h, c
+ }
+ else
+ {
+- syslog(LOG_ERR, "Failed to convert hostname '%s' to ip address", int_ip);
++ syslog(LOG_INFO, "Failed to parse or resolve to (%s) IPv4 address via UPnP IGDv2 AddAny", int_ip);
+ ClearNameValueList(&data);
+ SoapError(h, 402, "Invalid Args");
+ return;
+@@ -762,8 +753,8 @@ AddAnyPortMapping(struct upnphttp * h, c
+ {
+ if(h->clientaddr.s_addr != result_ip.s_addr)
+ {
+- syslog(LOG_INFO, "Client %s tried to redirect port to %s",
+- inet_ntoa(h->clientaddr), int_ip);
++ syslog(LOG_INFO, "Reject IPv4 port map ?:%s:?/? via UPnP IGDv2 AddAny reason for third-party from %s",
++ int_ip, inet_ntoa(h->clientaddr));
+ ClearNameValueList(&data);
+ SoapError(h, 606, "Action not authorized");
+ return;
+@@ -826,6 +817,8 @@ AddAnyPortMapping(struct upnphttp * h, c
+ SoapError(h, 501, "Action Failed");
+ return;
+ }
++ syslog(LOG_INFO, "Add IPv4 port map %hu:%s:%hu/%s remote IP %s lifetime %u via UPnP IGDv2 AddAny",
++ eport, int_ip, iport, protocol, r_host[0] != '\0' ? r_host : "any", leaseduration);
+ BuildSendAndCloseSoapResp(h, body, bodylen);
+ break;
+ case -2: /* already redirected */
+@@ -913,7 +906,7 @@ GetSpecificPortMappingEntry(struct upnph
+ }
+ else
+ {
+- syslog(LOG_INFO, "%s: rhost='%s' %s %s found => %s:%u desc='%s' duration=%u",
++ syslog(LOG_DEBUG, "%s: rhost='%s' %s %s found => %s:%u desc='%s' duration=%u",
+ action,
+ r_host ? r_host : "NULL", ext_port, protocol, int_ip,
+ (unsigned int)iport, desc, leaseduration);
+@@ -1003,7 +996,7 @@ DeletePortMapping(struct upnphttp * h, c
+ return;
+ }
+
+- syslog(LOG_INFO, "%s: external port: %hu, protocol: %s",
++ syslog(LOG_DEBUG, "%s: external port: %hu, protocol: %s",
+ action, eport, protocol);
+
+ /* if in secure mode, check the IP
+@@ -1042,6 +1035,8 @@ DeletePortMapping(struct upnphttp * h, c
+
+ if(r < 0)
+ {
++ syslog(LOG_INFO, "Non-existing IPv4 port map to delete %hu:?:?/%s via UPnP IGD",
++ eport, protocol);
+ SoapError(h, 714, "NoSuchEntryInArray");
+ }
+ else
+@@ -1109,8 +1104,8 @@ DeletePortMappingRange(struct upnphttp *
+ return;
+ }
+
+- syslog(LOG_INFO, "%s: deleting external ports: %hu-%hu, protocol: %s",
+- action, startport, endport, protocol);
++ syslog(LOG_INFO, "Delete IPv4 port map %hu-%hu:?:?/%s via UPnP IGDv2 DeleteRange",
++ startport, endport, protocol);
+
+ port_list = upnp_get_portmappings_in_range(startport, endport,
+ protocol, &number);
+@@ -1125,7 +1120,7 @@ DeletePortMappingRange(struct upnphttp *
+ for(i = 0; i < number; i++)
+ {
+ r = upnp_delete_redirection(port_list[i], protocol);
+- syslog(LOG_INFO, "%s: deleting external port: %hu, protocol: %s: %s",
++ syslog(LOG_DEBUG, "%s: deleting external port: %hu, protocol: %s: %s",
+ action, port_list[i], protocol, r < 0 ? "failed" : "ok");
+ }
+ free(port_list);
+@@ -1196,7 +1191,7 @@ GetGenericPortMappingEntry(struct upnpht
+ return;
+ }
+
+- syslog(LOG_INFO, "%s: index=%d", action, (int)index);
++ syslog(LOG_DEBUG, "%s: index=%d", action, (int)index);
+
+ rhost[0] = '\0';
+ r = upnp_get_redirection_infos_by_index((int)index, &eport, protocol, &iport,
+@@ -1777,8 +1772,8 @@ PinholeVerification(struct upnphttp * h,
+ struct addrinfo hints, *ai, *p;
+ int found = 0;
+
+- syslog(LOG_INFO, "%s: InternalClient %s is not an IPv6, assume hostname and convert",
+- "PinholeVerification", int_ip);
++ syslog(LOG_DEBUG, "Resolve hostname (%s) to IPv6 address",
++ int_ip);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+@@ -1788,8 +1783,8 @@ PinholeVerification(struct upnphttp * h,
+ r = getaddrinfo(int_ip, NULL, &hints, &ai);
+ if (r != 0)
+ {
+- syslog(LOG_WARNING, "%s: Failed to convert hostname '%s' to IP address : %s",
+- "PinholeVerification", int_ip, gai_strerror(r));
++ syslog(LOG_INFO, "Failed to parse or resolve to (%s) IPv6 address via UPnP IGDv2 IPv6",
++ int_ip);
+ SoapError(h, 402, "Invalid Args");
+ return -1;
+ }
+@@ -1803,12 +1798,12 @@ PinholeVerification(struct upnphttp * h,
+
+ if (inet_ntop(AF_INET6, &result_ip, int_ip_resolved, INET6_ADDRSTRLEN) == NULL)
+ {
+- syslog(LOG_WARNING, "%s: inet_ntop(): %m", "PinholeVerification");
++ syslog(LOG_INFO, "%s: inet_ntop(): %m", "PinholeVerification");
+ SoapError(h, 501, "Action Failed");
+ return -1;
+ }
+- syslog(LOG_INFO, "%s: InternalClient \"%s\" resolved as %s",
+- "PinholeVerification", int_ip, int_ip_resolved);
++ syslog(LOG_DEBUG, "Resolve hostname (%s) to IPv6 address (%s)",
++ int_ip, int_ip_resolved);
+ found = 1;
+ }
+ else
+@@ -1823,8 +1818,8 @@ PinholeVerification(struct upnphttp * h,
+ freeaddrinfo(ai);
+ if (!found)
+ {
+- syslog(LOG_NOTICE, "%s: No IPv6 address for hostname '%s'",
+- "PinholeVerification", int_ip);
++ syslog(LOG_INFO, "Failed to parse or resolve to (%s) IPv6 address via UPnP IGDv2 IPv6",
++ int_ip);
+ SoapError(h, 402, "Invalid Args");
+ return -1;
+ }
+@@ -1841,7 +1836,7 @@ PinholeVerification(struct upnphttp * h,
+ /* no need to copy to int_ip_resolved, but we still need to fill result_ip up */
+ if (inet_pton(AF_INET6, int_ip, &result_ip) <= 0)
+ {
+- syslog(LOG_ERR, "inet_pton(%s)", int_ip);
++ syslog(LOG_DEBUG, "inet_pton(%s)", int_ip);
+ SoapError(h, 501, "Action Failed");
+ return -1;
+ }
+@@ -1855,8 +1850,8 @@ PinholeVerification(struct upnphttp * h,
+
+ if(memcmp(&h->clientaddr_v6, &result_ip, sizeof(struct in6_addr)) != 0)
+ {
+- syslog(LOG_INFO, "%s: Client %s tried to access pinhole for internal %s and is not authorized",
+- "PinholeVerification", clientaddr_str, int_ip_resolved == NULL ? int_ip : int_ip_resolved);
++ syslog(LOG_INFO, "Reject IPv6 port map [%s]:?/? via UPnP IGDv2 IPv6 reason for third-party from %s",
++ int_ip_resolved == NULL ? int_ip : int_ip_resolved, clientaddr_str);
+ SoapError(h, 606, "Action not authorized");
+ return 0;
+ }
+@@ -1868,11 +1863,10 @@ PinholeVerification(struct upnphttp * h,
+ }
+ #endif
+
+- /* Pinhole InternalPort must be greater than or equal to 1024 */
+- if (int_port < 1024)
++ if (int_port < default_min_port || int_port > default_max_port)
+ {
+- syslog(LOG_INFO, "Client %s tried to access pinhole with port < 1024 and is not authorized to do it",
+- clientaddr_str);
++ syslog(LOG_INFO, "Reject IPv6 port map [%s]:%hu/? via UPnP IGDv2 IPv6 reason default-min/max-port",
++ clientaddr_str, int_port);
+ SoapError(h, 606, "Action not authorized");
+ return 0;
+ }
+@@ -1974,7 +1968,7 @@ AddPinhole(struct upnphttp * h, const ch
+ inet_ntop(AF_INET6,
+ &(((struct sockaddr_in6 *)p->ai_addr)->sin6_addr),
+ rem_ip, sizeof(rem_ip));
+- syslog(LOG_INFO, "resolved '%s' to '%s'", rem_host, rem_ip);
++ syslog(LOG_DEBUG, "Resolve hostname (%s) to IPv6 address (%s)", rem_host, rem_ip);
+ rem_host = rem_ip;
+ break;
+ }
+@@ -1983,8 +1977,8 @@ AddPinhole(struct upnphttp * h, const ch
+ }
+ else
+ {
+- syslog(LOG_WARNING, "AddPinhole : getaddrinfo(%s) : %s",
+- rem_host, gai_strerror(err));
++ syslog(LOG_INFO, "Failed to parse or resolve to (%s) IPv6 address via UPnP IGDv2 IPv6",
++ rem_host);
+ #if 0
+ SoapError(h, 402, "Invalid Args");
+ goto clear_and_exit;
+@@ -2017,10 +2011,8 @@ AddPinhole(struct upnphttp * h, const ch
+ if(PinholeVerification(h, int_ip, iport, int_ip_resolved) <= 0)
+ goto clear_and_exit;
+
+- syslog(LOG_INFO, "%s: (inbound) from [%s]:%hu to [%s]:%hu with proto %ld during %d sec",
+- action, rem_host?rem_host:"any",
+- rport, int_ip_resolved, iport,
+- proto, ltime);
++ syslog(LOG_INFO, "Add IPv6 port map [%s]:%hu/%s remote IP %s remote port %hu lifetime %d via UPnP IGDv2 IPv6",
++ int_ip_resolved, iport, proto_itoa(proto), rem_host[0] != '\0' ? rem_host : "any", rport, ltime);
+
+ /* In cases where the RemoteHost, RemotePort, InternalPort,
+ * InternalClient and Protocol are the same than an existing pinhole,
+@@ -2124,8 +2116,8 @@ UpdatePinhole(struct upnphttp * h, const
+ return;
+ }
+
+- syslog(LOG_INFO, "%s: (inbound) updating lease duration to %d for pinhole with ID: %d",
+- action, ltime, uid);
++ syslog(LOG_INFO, "Renew IPv6 port map [?]:?/? lifetime %d via UPnP IGDv2 IPv6 Update UID %d",
++ ltime, uid);
+
+ n = upnp_update_inboundpinhole(uid, ltime);
+ if(n == -1)
+@@ -2188,7 +2180,7 @@ GetOutboundPinholeTimeout(struct upnphtt
+ iport = (unsigned short)atoi(int_port);
+ /*proto = atoi(protocol);*/
+
+- syslog(LOG_INFO, "%s: retrieving timeout for outbound pinhole from [%s]:%hu to [%s]:%hu protocol %s", action, int_ip, iport,rem_host, rport, protocol);
++ syslog(LOG_DEBUG, "%s: retrieving timeout for outbound pinhole from [%s]:%hu to [%s]:%hu protocol %s", action, int_ip, iport,rem_host, rport, protocol);
+
+ /* TODO */
+ r = -1;/*upnp_check_outbound_pinhole(proto, &opt);*/
+@@ -2264,11 +2256,14 @@ DeletePinhole(struct upnphttp * h, const
+ &leasetime, NULL);
+ if (n >= 0)
+ {
+- if(PinholeVerification(h, iaddr, iport, NULL) <= 0)
++ if(PinholeVerification(h, iaddr, iport, NULL) <= 0) {
++ syslog(LOG_INFO, "Non-existing IPv6 port map to delete via UPnP IGDv2 IPv2 UID %d", uid);
+ return;
++ }
+ }
+ else if(n == -2)
+ {
++ syslog(LOG_INFO, "Non-existing IPv6 port map to delete via UPnP IGDv2 IPv2 UID %d", uid);
+ SoapError(h, 704, "NoSuchEntry");
+ return;
+ }
+@@ -2286,8 +2281,8 @@ DeletePinhole(struct upnphttp * h, const
+ SoapError(h, 501, "Action Failed");
+ return;
+ }
+- syslog(LOG_INFO, "%s: (inbound) pinhole with ID %d successfully removed",
+- action, uid);
++ syslog(LOG_INFO, "Delete IPv6 port map [?]:?/? via UPnP IGDv2 IPv6 UID %d",
++ uid);
+ bodylen = snprintf(body, sizeof(body), resp,
+ action, ns, action);
+ if (bodylen < 0 || bodylen >= (int)sizeof(body)) {
+@@ -2696,7 +2691,7 @@ SoapError(struct upnphttp * h, int errCo
+ char body[2048];
+ int bodylen;
+
+- syslog(LOG_INFO, "Returning UPnPError %d: %s", errCode, errDesc);
++ syslog(LOG_DEBUG, "Returning UPnPError %d: %s", errCode, errDesc);
+ bodylen = snprintf(body, sizeof(body), resp, errCode, errDesc);
+ if(bodylen < 0) {
+ syslog(LOG_ERR, "snprintf() returned %d", bodylen);
+--- a/upnpstun.c
++++ b/upnpstun.c
+@@ -10,9 +10,10 @@
+ /*! \file upnpstun.c
+ * \brief STUN client implementation
+ *
+- * - https://datatracker.ietf.org/doc/html/rfc3489 (obsolete)
+- * - https://datatracker.ietf.org/doc/html/rfc5389
+- * - https://datatracker.ietf.org/doc/html/rfc5780 (experimental)
++ * - https://www.rfc-editor.org/info/rfc3489/ (classic STUN, obsoleted)
++ * - https://www.rfc-editor.org/info/rfc5389/ (STUN obsoleted)
++ * - https://www.rfc-editor.org/info/rfc5780/ (STUN NAT behavior, experimental)
++ * - https://www.rfc-editor.org/info/rfc8489/ (STUN current, not implemented)
+ */
+ #include
+ #include
+@@ -124,8 +125,8 @@ static int resolve_stun_host(const char
+
+ r = getaddrinfo(stun_host, service, &hints, &result);
+ if (r != 0) {
+- syslog(LOG_ERR, "%s: getaddrinfo(%s, %s, ...) failed : %s",
+- "resolve_stun_host", stun_host, service, gai_strerror(r));
++ syslog(LOG_ERR, "STUN: Failed to resolve hostname (%s) to IPv4 address (%s)",
++ stun_host, gai_strerror(r));
+ errno = EHOSTUNREACH;
+ return -1;
+ }
+@@ -149,8 +150,8 @@ static int resolve_stun_host(const char
+ } else {
+ char addr_str[48];
+ if (sockaddr_to_string((struct sockaddr *)sock_addr, addr_str, sizeof(addr_str)) > 0) {
+- syslog(LOG_DEBUG, "%s: %s:%s => %s",
+- "resolve_stun_host", stun_host, service, addr_str);
++ syslog(LOG_DEBUG, "%s: Resolve hostname (%s) and connect to %s",
++ "resolve_stun_host", stun_host, addr_str);
+ }
+ }
+
+@@ -274,7 +275,7 @@ static int wait_for_stun_responses(int f
+ FD_SET(fds[i], &fdset);
+ }
+
+- syslog(LOG_DEBUG, "%s: waiting %ld secs and %ld usecs", "wait_for_stun_responses", (long)timeout.tv_sec, (long)timeout.tv_usec);
++ syslog(LOG_DEBUG, "%s: waiting %ld.%ld s", "wait_for_stun_responses", (long)timeout.tv_sec, (long)timeout.tv_usec);
+ ret = select(max_fd+1, &fdset, NULL, NULL, &timeout);
+ if (ret < 0) {
+ if (errno == EINTR)
+@@ -484,7 +485,8 @@ static int parse_stun_response(unsigned
+ if (!have_other_address && have_address) {
+ syslog(LOG_ERR, "STUN server not supported, not returning "
+ "OTHER-ADDRESS / support CHANGE-REQUEST's required for "
+- "endpoint-independent (1:1) CGNAT filtering tests per RFC 5780.");
++ "endpoint-independent (1:1) CGNAT filtering tests per RFC 5780, "
++ "see compatible servers in sample config");
+ return -1;
+ }
+ return (have_address && have_other_address) ? 0 : -1;
+@@ -529,13 +531,13 @@ int perform_stun(const char *if_name, co
+
+ /* Determine unrestricted endpoint-independent (1:1) CGNAT in two STUN requests per RFC 5780 4.4 test I/II */
+ /* 1. Connectivity (binding, detect public IPv4), 2. CHANGE-REQUEST with change-IP and change-port set */
+- /* https://datatracker.ietf.org/doc/html/rfc5780#section-4.4 */
++ /* https://www.rfc-editor.org/info/rfc5780/#section-4.4 */
+ fill_request(requests[i], i, i);
+ transaction_ids[i] = requests[i]+8;
+ }
+
+- syslog(LOG_INFO, "%s: local ports %hu %hu %hu %hu",
+- "perform_stun", local_ports[0], local_ports[1],
++ syslog(LOG_INFO, "STUN: Testing with local UDP ports %hu %hu %hu %hu",
++ local_ports[0], local_ports[1],
+ local_ports[2], local_ports[3]);
+
+ /* Unblock local ports */
+@@ -600,16 +602,15 @@ int perform_stun(const char *if_name, co
+ if (mapped_addrs_count < 4) {
+ /* We have not received all four responses,
+ * therefore NAT or firewall is doing some filtering */
+- syslog(LOG_NOTICE, "%s: %d response out of 4 received",
+- "perform_stun", mapped_addrs_count);
++ syslog(LOG_NOTICE, "STUN: Filtering detected as %d/4 responses received (1: IPv4 detection, 2-4: filtering tests)",
++ mapped_addrs_count);
+ *restrictive_nat = 1;
+ }
+
+ if (memcmp(&remote_addr, &peer_addrs[0], sizeof(peer_addrs[0])) != 0) {
+ /* We received STUN response from different address
+ * even we did not asked for it, so some strange NAT is active */
+- syslog(LOG_NOTICE, "%s: address changed",
+- "perform_stun");
++ syslog(LOG_NOTICE, "STUN: Public IPv4 change detected in responses");
+ *restrictive_nat = 1;
+ }
+
+@@ -621,8 +622,8 @@ int perform_stun(const char *if_name, co
+ sockaddr_to_string((struct sockaddr *)&mapped_addrs[i], mapped_addr_str, sizeof(mapped_addr_str));
+ /* External IP address or port was changed,
+ * therefore symmetric NAT is active */
+- syslog(LOG_NOTICE, "%s: #%d external address or port changed : %s:%hu => %s",
+- "perform_stun", i, inet_ntoa(*ext_addr), local_ports[i], mapped_addr_str);
++ syslog(LOG_NOTICE, "STUN: Public IPv4/port change detected in response #%d (%s:%hu => %s)",
++ i+1, inet_ntoa(*ext_addr), local_ports[i], mapped_addr_str);
+ *restrictive_nat = 1;
+ }
+ }
diff --git a/miniupnpd/patches/20-improve-logging.patch b/miniupnpd/patches/20-improve-logging.patch
deleted file mode 100644
index c58d1ff34..000000000
--- a/miniupnpd/patches/20-improve-logging.patch
+++ /dev/null
@@ -1,1379 +0,0 @@
-From 22a056e0c4b7c117bd2e0c06e93771c3961dcb15 Mon Sep 17 00:00:00 2001
-From: Self-Hosting-Group
- <155233284+Self-Hosting-Group@users.noreply.github.com>
-Date: Wed, 9 Sep 2026 00:00:00 +0000
-Subject: [PATCH] miniupnpd: Improve logging and CGNAT use
-
-* Clearer logging of enabled protocols/ports, IPv6 mapping, and UPnP IGD
- compatibility mode… in start banner
-* Log warnings with an enabled allow third-party mapping option
-* Change the log level (less verbose) of many internal or normally
- occurring messages, and some error messages, without functional
- restrictions, to DEBUG, client requests/actions and their errors to
- INFO, and only daemon-wide messages to NOTICE/WARNING/ERR…
-* Comment out some log-filling messages when internal interfaces lose
- link, and for e.g. `rule with label '%s' is not a IGD pinhole`, as
- normally occurring
-* Log IPv4 mapping disabled/enabled messages by default
-* Fix IPv4 mapping re-enable after external interface down/up with a
- CGNAT option set; handling was previously not entirely implemented for
- both options, incl. miniupnp/miniupnp@bbdd738, miniupnp/miniupnp#905
-* No IPv4 mapping disabled logging on requests, but more cases
-* Clearer IPv4 mapping disabled/enabled logging
-
-Fixes: https://github.com/openwrt/packages/issues/11971
-Fixes: https://github.com/openwrt/packages/issues/17601
-Fixes: https://github.com/openwrt/packages/issues/26483
-Link: https://github.com/miniupnp/miniupnp/pull/764
----
- asyncsendto.c | 4 +-
- getifaddr.c | 4 +-
- minissdp.c | 24 ++--
- miniupnpd.c | 208 +++++++++++++++++++-------------
- natpmp.c | 16 +--
- netfilter/iptcrdr.c | 10 +-
- netfilter/iptpinhole.c | 4 +-
- netfilter_nft/nftnlrdr.c | 2 +-
- netfilter_nft/nftnlrdr_misc.c | 8 +-
- netfilter_nft/nftpinhole.c | 10 +-
- pcpserver.c | 18 +--
- pf/obsdrdr.c | 6 +-
- pf/pfpinhole.c | 12 +-
- upnphttp.c | 6 +-
- upnppinhole.c | 8 +-
- upnpredirect.c | 10 +-
- upnpsoap.c | 43 +++----
- upnpstun.c | 37 +++---
- 18 files changed, 229 insertions(+), 201 deletions(-)
-
---- a/asyncsendto.c
-+++ b/asyncsendto.c
-@@ -254,9 +254,9 @@ int try_sendto(fd_set * writefds)
- /* uncatched error */
- if(sockaddr_to_string(elt->dest_addr, addr_str, sizeof(addr_str)) <= 0)
- addr_str[0] = '\0';
-- syslog(LOG_ERR, "%s(sock=%d, len=%u, dest=%s): sendto: %m",
-+ /*syslog(LOG_DEBUG, "%s(sock=%d, len=%u, dest=%s): sendto: %m",
- "try_sendto", elt->sockfd, (unsigned)elt->len,
-- addr_str);
-+ addr_str);*/
- ret--;
- }
- } else if((int)n != (int)elt->len) {
---- a/getifaddr.c
-+++ b/getifaddr.c
-@@ -74,7 +74,7 @@ getifaddr(const char * ifname, char * bu
- } else {
- r = GETIFADDR_IOCTL_ERROR;
- }
-- syslog(LOG_ERR, "ioctl(s, SIOCGIFADDR, ...): %m");
-+ syslog(LOG_DEBUG, "ioctl(s, SIOCGIFADDR, ...): %m");
- close(s);
- return r;
- }
-@@ -149,7 +149,7 @@ getifaddr(const char * ifname, char * bu
- if(addr) *addr = ((struct sockaddr_in *)candidate->ifa_addr)->sin_addr;
- if(mask) *mask = ((struct sockaddr_in *)candidate->ifa_netmask)->sin_addr;
- } else {
-- syslog(LOG_WARNING, "no AF_INET address found for %s", ifname);
-+ syslog(LOG_DEBUG, "no AF_INET address found for %s", ifname);
- freeifaddrs(ifap);
- return GETIFADDR_NO_ADDRESS;
- }
---- a/minissdp.c
-+++ b/minissdp.c
-@@ -743,8 +743,8 @@ SendSSDPNotify(int s, const struct socka
- }
- n = sendto_or_schedule(s, bufr, l, 0, dest, dest_len);
- if(n < 0) {
-- syslog(LOG_ERR, "sendto(udp_notify=%d, %s): %m", s,
-- host ? host : "NULL");
-+ /*syslog(LOG_DEBUG, "sendto(udp_notify=%d, %s): %m", s,
-+ host ? host : "NULL");*/
- } else if(n != l) {
- syslog(LOG_NOTICE, "sendto() sent %d out of %d bytes", n, l);
- }
-@@ -754,8 +754,8 @@ SendSSDPNotify(int s, const struct socka
- * discovery messages SHOULD NOT be sent more than three times. */
- n = sendto_schedule(s, bufr, l, 0, dest, dest_len, 250);
- if(n < 0) {
-- syslog(LOG_ERR, "sendto(udp_notify=%d, %s): %m", s,
-- host ? host : "NULL");
-+ /*syslog(LOG_DEBUG, "sendto(udp_notify=%d, %s): %m", s,
-+ host ? host : "NULL");*/
- }
- }
-
-@@ -1060,7 +1060,7 @@ ProcessSSDPData(int s, const char *bufr,
- }
- if(lan_addr == NULL)
- {
-- syslog(LOG_WARNING, "SSDP packet sender %s (if_index=%d) not from a LAN, ignoring",
-+ syslog(LOG_DEBUG, "SSDP packet sender %s (if_index=%d) not from a LAN, ignoring",
- sender_str, source_if);
- return;
- }
-@@ -1163,7 +1163,7 @@ ProcessSSDPData(int s, const char *bufr,
- sender_str );*/
- if(st && (st_len > 0))
- {
-- syslog(LOG_INFO, "SSDP M-SEARCH from %s ST: %.*s",
-+ syslog(LOG_DEBUG, "SSDP M-SEARCH from %s ST: %.*s",
- sender_str, st_len, st);
- /* find in which sub network the client is */
- #ifdef ENABLE_IPV6
-@@ -1276,7 +1276,7 @@ ProcessSSDPData(int s, const char *bufr,
- else
- snprintf(ver_str, sizeof(ver_str), "%d", known_service_types[i].version);
- #endif
-- syslog(LOG_INFO, "Single search found");
-+ syslog(LOG_DEBUG, "Single search found");
- #ifdef DELAY_MSEARCH_RESPONSE
- delay = random() / (1 + RAND_MAX / (1000 * mx_value));
- #ifdef DEBUG
-@@ -1305,7 +1305,7 @@ ProcessSSDPData(int s, const char *bufr,
- #ifdef DELAY_MSEARCH_RESPONSE
- unsigned int delay_increment = (mx_value * 1000) / 15;
- #endif
-- syslog(LOG_INFO, "ssdp:all found");
-+ syslog(LOG_DEBUG, "ssdp:all found");
- for(i=0; known_service_types[i].s; i++)
- {
- #ifdef DELAY_MSEARCH_RESPONSE
-@@ -1363,7 +1363,7 @@ ProcessSSDPData(int s, const char *bufr,
- #endif
- if(0 == memcmp(st, uuidvalue_igd, l))
- {
-- syslog(LOG_INFO, "ssdp:uuid (IGD) found");
-+ syslog(LOG_DEBUG, "ssdp:uuid (IGD) found");
- SendSSDPResponse(s, sender, st, st_len, "",
- announced_host, http_port,
- #ifdef ENABLE_HTTPS
-@@ -1373,7 +1373,7 @@ ProcessSSDPData(int s, const char *bufr,
- }
- else if(0 == memcmp(st, uuidvalue_wan, l))
- {
-- syslog(LOG_INFO, "ssdp:uuid (WAN) found");
-+ syslog(LOG_DEBUG, "ssdp:uuid (WAN) found");
- SendSSDPResponse(s, sender, st, st_len, "",
- announced_host, http_port,
- #ifdef ENABLE_HTTPS
-@@ -1383,7 +1383,7 @@ ProcessSSDPData(int s, const char *bufr,
- }
- else if(0 == memcmp(st, uuidvalue_wcd, l))
- {
-- syslog(LOG_INFO, "ssdp:uuid (WCD) found");
-+ syslog(LOG_DEBUG, "ssdp:uuid (WCD) found");
- SendSSDPResponse(s, sender, st, st_len, "",
- announced_host, http_port,
- #ifdef ENABLE_HTTPS
-@@ -1444,7 +1444,7 @@ SendSSDPbyebye(int s, const struct socka
- n = sendto_or_schedule(s, bufr, l, 0, dest, destlen);
- if(n < 0)
- {
-- syslog(LOG_ERR, "sendto(udp_shutdown=%d) to %s: %m", s, dest_str);
-+ syslog(LOG_DEBUG, "sendto(udp_shutdown=%d) to %s: %m", s, dest_str);
- return -1;
- }
- else if(n != l)
---- a/miniupnpd.c
-+++ b/miniupnpd.c
-@@ -496,7 +496,7 @@ ProcessIncomingHTTP(int shttpl, const ch
- if(get_lan_for_peer((struct sockaddr *)&clientname) == NULL)
- {
- /* The peer is not a LAN ! */
-- syslog(LOG_WARNING,
-+ syslog(LOG_DEBUG,
- "%s peer %s is not from a LAN, closing the connection",
- protocol, addr_str);
- close(shttp);
-@@ -867,7 +867,7 @@ set_startup_time(void)
- }
- else
- {
-- syslog(LOG_INFO, "system uptime is %lu seconds", uptime);
-+ syslog(LOG_DEBUG, "system uptime is %lu seconds", uptime);
- }
- fclose(f);
- startup_time -= uptime;
-@@ -1061,10 +1061,10 @@ parselanaddr(struct lan_addr_s * lan_add
- }
- if(addr_is_reserved(&lan_addr->ext_ip_addr)) {
- if (GETFLAG(ALLOWPRIVATEIPV4MASK)) {
-- syslog(LOG_WARNING, "IGNORED : option ext_ip address contains reserved / private address : %s", lan_addr->ext_ip_str);
-+ syslog(LOG_WARNING, "WARNING: IPv4 mapping enabled forcibly, as ext_allow_private_ipv4=yes set; check security note if not set");
- } else {
- /* error */
-- INIT_PRINT_ERR("Error: option ext_ip address contains reserved / private address : %s\n", lan_addr->ext_ip_str);
-+ INIT_PRINT_ERR("Option ext_ip set to private/CGNAT-reserved (%s) IPv4, exiting\n", lan_addr->ext_ip_str);
- return -1;
- }
- }
-@@ -1118,20 +1118,19 @@ parselan_error:
-
- static char ext_addr_str[INET_ADDRSTRLEN];
-
--int update_ext_ip_addr_from_stun(int init)
-+int update_ext_ip_addr_from_stun(void)
- {
- struct in_addr if_addr, ext_addr;
-- int restrictive_nat;
-+ int restrictive_nat, reserved;
- char if_addr_str[INET_ADDRSTRLEN];
-
-- syslog(LOG_INFO, "STUN: Performing with host=%s and port=%u ...", ext_stun_host, (unsigned)ext_stun_port);
--
- if (getifaddr(ext_if_name, if_addr_str, INET_ADDRSTRLEN, &if_addr, NULL) < 0) {
-- syslog(LOG_ERR, "STUN: Cannot get IP address for ext interface %s", ext_if_name);
-+ syslog(LOG_ERR, "Detected no IPv4 address on external network interface %s", ext_if_name);
- return 1;
- }
-+ syslog(LOG_INFO, "STUN: Detecting public IPv4 and testing for unrestricted endpoint-independent (1:1) CGNAT via %s:%u...", ext_stun_host, ext_stun_port ? ext_stun_port : 3478);
- if (perform_stun(ext_if_name, if_addr_str, ext_stun_host, ext_stun_port, &ext_addr, &restrictive_nat) != 0) {
-- syslog(LOG_ERR, "STUN: Performing STUN failed: %s", strerror(errno));
-+ syslog(LOG_ERR, "STUN: Performing test failed");
- return 1;
- }
- if (!inet_ntop(AF_INET, &ext_addr, ext_addr_str, sizeof(ext_addr_str))) {
-@@ -1139,32 +1138,29 @@ int update_ext_ip_addr_from_stun(int ini
- return 1;
- }
-
-- if ((init || disable_port_forwarding) && !restrictive_nat) {
-- if (addr_is_reserved(&if_addr))
-- syslog(LOG_INFO, "STUN: ext interface %s with IP address %s is now behind unrestricted full-cone NAT 1:1 with public IP address %s and firewall does not block incoming connections set by miniupnpd", ext_if_name, if_addr_str, ext_addr_str);
-- else
-- syslog(LOG_INFO, "STUN: ext interface %s has now public IP address %s and firewall does not block incoming connections set by miniupnpd", ext_if_name, if_addr_str);
-- syslog(LOG_INFO, "Port forwarding is now enabled");
-- } else if ((init || !disable_port_forwarding) && restrictive_nat) {
-- if (addr_is_reserved(&if_addr)) {
-- syslog(LOG_WARNING, "STUN: ext interface %s with private IP address %s is now possibly behind restrictive or symmetric NAT with public IP address %s which does not support port forwarding", ext_if_name, if_addr_str, ext_addr_str);
-- syslog(LOG_WARNING, "NAT on upstream router blocks incoming connections set by miniupnpd");
-- syslog(LOG_WARNING, "Turn off NAT on upstream router or change it to full-cone NAT 1:1 type");
-+ reserved = addr_is_reserved(&if_addr);
-+ if (!restrictive_nat && !reserved) {
-+ syslog(LOG_INFO, "STUN: Detected unrestricted public external IPv4 %s on %s", if_addr_str, ext_if_name);
-+ } else if (!restrictive_nat && reserved) {
-+ syslog(LOG_INFO, "STUN: Detected unrestricted endpoint-independent (1:1) CGNAT with public IPv4 %s and %s as external on %s", ext_addr_str, if_addr_str, ext_if_name);
-+ } else if (restrictive_nat && !reserved) {
-+ syslog(LOG_INFO, "STUN: Detected restricted public external IPv4 %s on %s, IPv4 mapping may not work", if_addr_str, ext_if_name);
-+ } else if (restrictive_nat && reserved) {
-+ syslog(LOG_INFO, "STUN: Detected restricted or address- and port-dependent (symmetric) CGNAT with public IPv4 %s and %s as external on %s, IPv4 mapping may not work", ext_addr_str, if_addr_str, ext_if_name);
-+ }
-+
-+ use_ext_ip_addr = ext_addr_str;
-+ if (!restrictive_nat || GETFLAG(ALLOWFILTEREDSTUNMASK)) {
-+ if (!restrictive_nat) {
-+ syslog(LOG_NOTICE, "IPv4 mapping enabled, and reachability/non-filtering tested via STUN");
- } else {
-- syslog(LOG_WARNING, "STUN: ext interface %s has now public IP address %s but firewall filters incoming connections set by miniunnpd", ext_if_name, if_addr_str);
-- syslog(LOG_WARNING, "Check configuration of firewall on local machine and also on upstream router");
-- }
-- if (!GETFLAG(ALLOWFILTEREDSTUNMASK)) {
-- syslog(LOG_WARNING, "Port forwarding is now disabled");
-- syslog(LOG_WARNING, "Set ext_perform_stun=allow-filtered if you still want to use port forwarding in current situation");
-+ syslog(LOG_WARNING, "WARNING: IPv4 mapping enabled forcibly, as ext_perform_stun=allow-filtered set; check security note if not set");
- }
-+ disable_port_forwarding = 0;
- } else {
-- syslog(LOG_INFO, "STUN: ... done");
-+ syslog(LOG_WARNING, "IPv4 mapping disabled, as own/upstream router filters incoming, or there is an address- and port-dependent (symmetric) CGNAT; to workaround current daemon limitation, or enable it anyway, set ext_perform_stun=allow-filtered; check security note if not set");
-+ disable_port_forwarding = 1;
- }
--
-- use_ext_ip_addr = ext_addr_str;
-- if (!GETFLAG(ALLOWFILTEREDSTUNMASK))
-- disable_port_forwarding = restrictive_nat;
- return 0;
- }
-
-@@ -1184,32 +1180,41 @@ static void update_disable_port_forwardi
- syslog(LOG_WARNING, "ext interface %s is down", ext_if_name);
- break;
- case GETIFADDR_NO_ADDRESS:
-- syslog(LOG_WARNING, "ext interface %s has no IPv4 address. Network is down", ext_if_name);
-+ syslog(LOG_ERR, "Detected no IPv4 address on external network interface %s", ext_if_name);
- break;
- default:
-- syslog(LOG_ERR, "Error getting IPv4 address for ext interface %s. Network is down", ext_if_name);
-+ syslog(LOG_ERR, "Detected no IPv4 address on external network interface %s", ext_if_name);
- }
-+ syslog(LOG_WARNING, "IPv4 mapping disabled");
- disable_port_forwarding = 1;
-+
- } else {
- int reserved = addr_is_reserved(&addr);
-- if (!disable_port_forwarding && reserved) {
-- if (GETFLAG(ALLOWPRIVATEIPV4MASK)) {
-- syslog(LOG_WARNING, "IGNORED : Reserved / private IP address %s on ext interface %s", if_addr, ext_if_name);
-+ if (!reserved) {
-+ syslog(LOG_INFO, "Detected public external IPv4 %s on %s", if_addr, ext_if_name);
-+ } else {
-+ syslog(LOG_INFO, "Detected private/CGNAT-reserved external IPv4 %s on %s, IPv4 mapping may not work", if_addr, ext_if_name);
-+ }
-+
-+ if (!reserved || GETFLAG(ALLOWPRIVATEIPV4MASK)) {
-+ if (!reserved) {
-+ syslog(LOG_NOTICE, "IPv4 mapping enabled");
- } else {
-- syslog(LOG_WARNING, "Reserved / private IP address %s on ext interface %s: Port forwarding is impossible", if_addr, ext_if_name);
-- syslog(LOG_INFO, "You are probably behind NAT, enable option ext_perform_stun=yes to detect public IP address");
-- syslog(LOG_INFO, "Or use ext_ip= / -o option to declare public IP address");
-- syslog(LOG_INFO, "In case that miniupnpd is thinking that it's behind symmetric NAT while it actually is full-cone");
-- syslog(LOG_INFO, "You can set option ext_allow_private_ipv4=yes to enable port forwarding");
-- syslog(LOG_INFO, "But you may still need to configure stun server or ext_ip to make it work correctly");
-- syslog(LOG_INFO, "Public IP address is required by UPnP/PCP/PMP protocols and clients do not work without it");
-- disable_port_forwarding = 1;
-- }
-- } else if (disable_port_forwarding &&
-- (!reserved || GETFLAG(ALLOWPRIVATEIPV4MASK))) {
-- syslog(LOG_INFO, "%s IP address %s on ext interface %s: Port forwarding is enabled",
-- reserved ? "Reserved / private" : "Public", if_addr, ext_if_name);
-+ syslog(LOG_WARNING, "WARNING: IPv4 mapping enabled forcibly, as ext_allow_private_ipv4=yes set; check compatibility/security notes if not set");
-+ }
- disable_port_forwarding = 0;
-+ } else {
-+ syslog(LOG_WARNING, "IPv4 mapping disabled by default, as private/CGNAT-reserved external IPv4 detected. Prevented for:");
-+ syslog(LOG_WARNING, "- Info, as port maps not reachable via internet without an unrestricted endpoint-independent (1:1) IPv4 CGNAT; possibly from within");
-+ syslog(LOG_WARNING, "- Compatibility, as many UPnP IGD & PCP/NAT-PMP clients require a returned public external IPv4");
-+ syslog(LOG_WARNING, "- Security, to prevent access via internet if the external/internal network interface has been configured swapped");
-+ syslog(LOG_WARNING, "To enable IPv4 mapping anyway, select one of the following options, listed by priority:");
-+ syslog(LOG_WARNING, "A) Detect public external IPv4 and test for unrestricted endpoint-independent (1:1) CGNAT via STUN server, set ext_perform_stun=yes");
-+ syslog(LOG_WARNING, "B) As A, but workaround filtered CGNAT result daemon limitation (without an extra firewall rule), set ext_perform_stun=allow-filtered; check security note");
-+ syslog(LOG_WARNING, "C) Manually override reported external IPv4 with public, set ext_ip; check security note");
-+ syslog(LOG_WARNING, "D) Report private/CGNAT-reserved external IPv4 to clients, set ext_allow_private_ipv4=yes; check compatibility/security notes");
-+ syslog(LOG_WARNING, "IPv4 mapping disabled");
-+ disable_port_forwarding = 1;
- }
- }
- }
-@@ -2095,9 +2100,9 @@ init(int argc, char * * argv, struct run
- }
- if (addr_is_reserved(&addr)) {
- if (GETFLAG(ALLOWPRIVATEIPV4MASK)) {
-- syslog(LOG_WARNING, "IGNORED : option ext_ip contains reserved / private address %s, not public routable", use_ext_ip_addr);
-+ syslog(LOG_WARNING, "WARNING: IPv4 mapping enabled forcibly, as ext_allow_private_ipv4=yes set; check compatibility/security notes if not set");
- } else {
-- INIT_PRINT_ERR("Error: option ext_ip contains reserved / private address %s, not public routable\n", use_ext_ip_addr);
-+ INIT_PRINT_ERR("Option ext_ip set to private/CGNAT-reserved (%s) IPv4, exiting\n", use_ext_ip_addr);
- return 1;
- }
- }
-@@ -2230,6 +2235,22 @@ init(int argc, char * * argv, struct run
- pidfilename = NULL;
- #endif
-
-+ syslog(LOG_NOTICE, "MiniUPnP daemon " MINIUPNPD_VERSION " starting, enabled protocols %s%s%s, ext_ifname=%s BOOTID=%u",
-+ GETFLAG(ENABLEUPNPMASK) ? "UPnP IGD" : "",
-+#ifdef ENABLE_NATPMP
-+ GETFLAG(ENABLEUPNPMASK) && GETFLAG(ENABLENATPMPMASK) ? " & " : "",
-+#ifdef ENABLE_PCP
-+ GETFLAG(ENABLENATPMPMASK) ? "PCP/NAT-PMP" : "",
-+#else
-+ GETFLAG(ENABLENATPMPMASK) ? "NAT-PMP" : "",
-+#endif
-+#else
-+ "", "",
-+#endif
-+ ext_if_name, upnp_bootid);
-+ syslog(LOG_INFO, "More information at https://miniupnp.tuxfamily.org/ or http://miniupnp.free.fr/");
-+ syslog(LOG_NOTICE, "Extra logging with log level info (-v) or debug (-v -v)");
-+
- #ifdef USE_SYSTEMD
- if (systemd_flag) {
- int r = sd_notify(0,
-@@ -2242,7 +2263,7 @@ init(int argc, char * * argv, struct run
-
- #ifdef ENABLE_LEASEFILE
- /*remove(lease_file);*/
-- syslog(LOG_INFO, "Reloading rules from lease file");
-+ syslog(LOG_INFO, "Reloading port maps from lease file");
- reload_from_lease_file();
- #ifdef ENABLE_UPNPPINHOLE
- reload_from_lease_file6();
-@@ -2433,28 +2454,16 @@ main(int argc, char * * argv)
- goto shutdown;
- }
-
-- syslog(LOG_INFO, "version " MINIUPNPD_VERSION " starting%s%sext if %s BOOTID=%u",
--#ifdef ENABLE_NATPMP
--#ifdef ENABLE_PCP
-- GETFLAG(ENABLENATPMPMASK) ? " NAT-PMP/PCP " : " ",
--#else
-- GETFLAG(ENABLENATPMPMASK) ? " NAT-PMP " : " ",
--#endif
--#else
-- " ",
--#endif
-- GETFLAG(ENABLEUPNPMASK) ? "UPnP-IGD " : "",
-- ext_if_name, upnp_bootid);
- #ifdef ENABLE_IPV6
- if (strcmp(ext_if_name6, ext_if_name) != 0) {
-- syslog(LOG_INFO, "specific IPv6 ext if %s", ext_if_name6);
-+ syslog(LOG_INFO, "Separate ext_ifname6=%s set", ext_if_name6);
- }
- #endif
-
- if(GETFLAG(PERFORMSTUNMASK))
- {
-- if (update_ext_ip_addr_from_stun(1) != 0) {
-- syslog(LOG_ERR, "Performing STUN failed. EXITING");
-+ if (update_ext_ip_addr_from_stun() != 0) {
-+ syslog(LOG_ERR, "STUN: Performing test failed, exiting");
- return_code = 1;
- goto shutdown;
- }
-@@ -2462,6 +2471,8 @@ main(int argc, char * * argv)
- else if (!use_ext_ip_addr)
- {
- update_disable_port_forwarding();
-+ } else {
-+ syslog(LOG_NOTICE, "IPv4 mapping enabled");
- }
-
- #ifdef DYNAMIC_OS_VERSION
-@@ -2496,7 +2507,7 @@ main(int argc, char * * argv)
- #ifdef ENABLE_NFQUEUE
- nfqueue_data.http_port = listen_port;
- #endif /* ENABLE_NFQUEUE */
-- syslog(LOG_NOTICE, "HTTP listening on port %d", v.port);
-+ syslog(LOG_NOTICE, "Listening for UPnP IGD (SOAP/HTTP) traffic on port %d/TCP, SSDP 1900/UDP", v.port);
- #if defined(V6SOCKETS_ARE_V6ONLY) && defined(ENABLE_IPV6)
- if(!GETFLAG(IPV6DISABLEDMASK))
- {
-@@ -2527,7 +2538,7 @@ main(int argc, char * * argv)
- #ifdef ENABLE_NFQUEUE
- nfqueue_data.https_port = listen_port;
- #endif /* ENABLE_NFQUEUE */
-- syslog(LOG_NOTICE, "HTTPS listening on port %d", v.https_port);
-+ syslog(LOG_NOTICE, "Listening for UPnP IGD (SOAP/HTTPS) traffic on port %d/TCP", v.https_port);
- #if defined(V6SOCKETS_ARE_V6ONLY) && defined(ENABLE_IPV6)
- shttpsl_v4 = OpenAndConfHTTPSocket(&listen_port, 0);
- if(shttpsl_v4 < 0)
-@@ -2542,11 +2553,11 @@ main(int argc, char * * argv)
- if(!GETFLAG(IPV6DISABLEDMASK)) {
- if(find_ipv6_addr(lan_addrs.lh_first ? lan_addrs.lh_first->ifname : NULL,
- ipv6_addr_for_http_with_brackets, sizeof(ipv6_addr_for_http_with_brackets)) > 0) {
-- syslog(LOG_NOTICE, "HTTP IPv6 address given to control points : %s",
-+ syslog(LOG_INFO, "IPv6 address given to UPnP IGD clients: %s",
- ipv6_addr_for_http_with_brackets);
- } else {
- memcpy(ipv6_addr_for_http_with_brackets, "[::1]", 6);
-- syslog(LOG_WARNING, "no HTTP IPv6 address, disabling IPv6");
-+ syslog(LOG_DEBUG, "no HTTP IPv6 address, disabling IPv6");
- SETFLAG(IPV6DISABLEDMASK);
- }
- }
-@@ -2603,7 +2614,7 @@ main(int argc, char * * argv)
- if(SendSSDPGoodbye(snotify, addr_count * 2) < 0)
- #endif
- {
-- syslog(LOG_WARNING, "Failed to broadcast good-bye notifications");
-+ syslog(LOG_DEBUG, "Failed to broadcast good-bye notifications");
- }
- }
- #endif /* UPNP_STRICT */
-@@ -2628,16 +2639,16 @@ main(int argc, char * * argv)
- if(OpenAndConfNATPMPSockets(snatpmp) < 0)
- #ifdef ENABLE_PCP
- {
-- syslog(LOG_ERR, "Failed to open sockets for NAT-PMP/PCP.");
-+ syslog(LOG_ERR, "Failed to open port 5351/UDP for PCP/NAT-PMP");
- } else {
-- syslog(LOG_NOTICE, "Listening for NAT-PMP/PCP traffic on port %u",
-+ syslog(LOG_NOTICE, "Listening for PCP/NAT-PMP traffic on port %u/UDP",
- NATPMP_PORT);
- }
- #else
- {
-- syslog(LOG_ERR, "Failed to open sockets for NAT PMP.");
-+ syslog(LOG_ERR, "Failed to open port 5351/UDP for NAT-PMP");
- } else {
-- syslog(LOG_NOTICE, "Listening for NAT-PMP traffic on port %u",
-+ syslog(LOG_NOTICE, "Listening for NAT-PMP traffic on port %u/UDP",
- NATPMP_PORT);
- }
- #endif
-@@ -2748,6 +2759,30 @@ main(int argc, char * * argv)
- }
- #endif /* HAS_LIBCAP_NG */
-
-+if (GETFLAG(ENABLEUPNPMASK) && !GETFLAG(SECUREMODEMASK))
-+ syslog(LOG_WARNING, "WARNING: Allow adding port maps for non-requesting IP addresses via UPnP IGD, as secure_mode=no set");
-+#ifdef ENABLE_PCP
-+if (GETFLAG(ENABLENATPMPMASK) && GETFLAG(PCP_ALLOWTHIRDPARTYMASK))
-+ syslog(LOG_WARNING, "WARNING: Allow adding port maps for non-requesting IP addresses via PCP, as pcp_allow_thirdparty=yes set");
-+#endif
-+#ifdef ENABLE_IPV6
-+if (GETFLAG(IPV6DISABLEDMASK))
-+ syslog(LOG_NOTICE, "IPv6 mapping disabled");
-+#else
-+syslog(LOG_NOTICE, "IPv6 mapping disabled");
-+#endif
-+if (GETFLAG(ENABLEUPNPMASK)) {
-+#ifdef IGD_V2
-+ if (GETFLAG(FORCEIGDDESCV1MASK)) {
-+ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv1 (IPv4 only)");
-+ } else {
-+ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv2");
-+ }
-+#else
-+ syslog(LOG_NOTICE, "UPnP IGD compatibility mode set to IGDv1 (IPv4 only)");
-+#endif
-+}
-+
- #ifdef USE_SYSTEMD
- if (v.systemd_notify) {
- upnp_update_status();
-@@ -2777,12 +2812,13 @@ main(int argc, char * * argv)
- /* send public address change notifications if needed */
- if(should_send_public_address_change_notif)
- {
-- syslog(LOG_INFO, "should send external iface address change notification(s)");
-+ syslog(LOG_NOTICE, "External network interface link/address change detected");
- if(GETFLAG(PERFORMSTUNMASK))
- {
-- if (update_ext_ip_addr_from_stun(0) != 0) {
-+ if (update_ext_ip_addr_from_stun() != 0) {
- /* if stun succeed it updates disable_port_forwarding;
- * if stun failed (non-zero return value) then port forwarding would not work, so disable it */
-+ syslog(LOG_WARNING, "IPv4 mapping disabled");
- disable_port_forwarding = 1;
- }
- }
-@@ -3058,7 +3094,7 @@ main(int argc, char * * argv)
- }
- i = try_sendto(&writeset);
- if(i < 0) {
-- syslog(LOG_ERR, "try_sendto failed to send %d packets", -i);
-+ /*syslog(LOG_DEBUG, "try_sendto failed to send %d packets", -i);*/
- }
- #ifdef USE_MINIUPNPDCTL
- for(ectl = ctllisthead.lh_first; ectl;)
-@@ -3158,7 +3194,7 @@ main(int argc, char * * argv)
- if(lan_addr == NULL) {
- char sender_str[64];
- sockaddr_to_string((struct sockaddr *)&senderaddr, sender_str, sizeof(sender_str));
-- syslog(LOG_WARNING, "NAT-PMP packet sender %s not from a LAN, ignoring",
-+ syslog(LOG_DEBUG, "NAT-PMP packet sender %s not from a LAN, ignoring",
- sender_str);
- continue;
- }
-@@ -3181,7 +3217,7 @@ main(int argc, char * * argv)
- if(lan_addr == NULL) {
- char sender_str[64];
- sockaddr_to_string((struct sockaddr *)&senderaddr, sender_str, sizeof(sender_str));
-- syslog(LOG_WARNING, "NAT-PMP packet sender %s not from a LAN, ignoring",
-+ syslog(LOG_DEBUG, "NAT-PMP packet sender %s not from a LAN, ignoring",
- sender_str);
- continue;
- }
-@@ -3215,7 +3251,7 @@ main(int argc, char * * argv)
- /* process SSDP packets */
- if(sudp >= 0 && FD_ISSET(sudp, &readset))
- {
-- /*syslog(LOG_INFO, "Received UDP Packet");*/
-+ /*syslog(LOG_DEBUG, "Received UDP Packet");*/
- #ifdef ENABLE_HTTPS
- ProcessSSDPRequest(sudp, (unsigned short)v.port, (unsigned short)v.https_port);
- #else
-@@ -3225,7 +3261,7 @@ main(int argc, char * * argv)
- #ifdef ENABLE_IPV6
- if(sudpv6 >= 0 && FD_ISSET(sudpv6, &readset))
- {
-- syslog(LOG_INFO, "Received UDP Packet (IPv6)");
-+ /*syslog(LOG_DEBUG, "Received UDP Packet (IPv6)");*/
- #ifdef ENABLE_HTTPS
- ProcessSSDPRequest(sudpv6, (unsigned short)v.port, (unsigned short)v.https_port);
- #else
-@@ -3327,7 +3363,7 @@ main(int argc, char * * argv)
-
- shutdown:
-
-- syslog(LOG_NOTICE, "shutting down MiniUPnPd");
-+ syslog(LOG_NOTICE, "Shutting down MiniUPnPd");
- #ifdef USE_SYSTEMD
- if (v.systemd_notify) {
- sd_notify(0,
-@@ -3346,7 +3382,7 @@ shutdown:
- if(SendSSDPGoodbye(snotify, addr_count * 2) < 0)
- #endif
- {
-- syslog(LOG_ERR, "Failed to broadcast good-bye notifications");
-+ syslog(LOG_DEBUG, "Failed to broadcast good-bye notifications");
- }
- }
- /* try to send pending packets */
---- a/natpmp.c
-+++ b/natpmp.c
-@@ -106,7 +106,7 @@ static void FillPublicAddressResponse(un
- resp[3] = 3; /* Network Failure (e.g. NAT box itself
- * has not obtained a DHCP lease) */
- } else if(getifaddr(ext_if_name, tmp, INET_ADDRSTRLEN, &addr, NULL) < 0) {
-- syslog(LOG_ERR, "Failed to get IP for interface %s", ext_if_name);
-+ syslog(LOG_DEBUG, "Failed to get IP for interface %s", ext_if_name);
- resp[3] = 3; /* Network Failure (e.g. NAT box itself
- * has not obtained a DHCP lease) */
- } else if (!GETFLAG(ALLOWPRIVATEIPV4MASK) && addr_is_reserved(&addr)) {
-@@ -231,7 +231,7 @@ void ProcessIncomingNATPMPPacket(int s,
- syslog(LOG_ERR, "inet_ntop(natpmp): %m");
- }
-
-- syslog(LOG_INFO, "NAT-PMP request received from %s:%hu %dbytes",
-+ syslog(LOG_DEBUG, "NAT-PMP request received from %s:%hu %d bytes",
- senderaddrstr, ntohs(senderaddr->sin_port), n);
-
- if(n<2 || ((((req[1]-1)&~1)==0) && n<12)) {
-@@ -260,7 +260,7 @@ void ProcessIncomingNATPMPPacket(int s,
- resp[3] = 1; /* unsupported version */
- } else switch(req[1]) {
- case 0: /* Public address request */
-- syslog(LOG_INFO, "NAT-PMP public address request");
-+ syslog(LOG_DEBUG, "NAT-PMP public address request");
- FillPublicAddressResponse(resp, senderaddr->sin_addr.s_addr);
- resplen = 12;
- break;
-@@ -319,7 +319,7 @@ void ProcessIncomingNATPMPPacket(int s,
- resp[3] = 2; /* Not Authorized/Refused */
- break;
- } else {
-- syslog(LOG_INFO, "NAT-PMP %s port %hu mapping removed",
-+ syslog(LOG_DEBUG, "NAT-PMP %s port %hu mapping removed",
- proto2==IPPROTO_TCP?"TCP":"UDP", eport2);
- index--;
- }
-@@ -340,7 +340,7 @@ void ProcessIncomingNATPMPPacket(int s,
- eport_first = eport;
- } else if(eport == eport_first) { /* no eport available */
- if(any_eport_allowed == 0) { /* all eports rejected by permissions */
-- syslog(LOG_ERR, "No allowed eport for NAT-PMP %hu %s->%s:%hu",
-+ syslog(LOG_INFO, "No allowed eport for NAT-PMP %hu %s->%s:%hu",
- eport, proto_itoa(proto), senderaddrstr, iport);
- resp[3] = 2; /* Not Authorized/Refused */
- } else { /* at least one eport allowed (but none available) */
-@@ -443,7 +443,7 @@ void SendNATPMPPublicAddressChangeNotifi
- FillPublicAddressResponse(notif, 0);
- if(notif[3])
- {
-- syslog(LOG_WARNING, "%s: cannot get public IP address, stopping",
-+ syslog(LOG_DEBUG, "%s: cannot get public IP address, stopping",
- "SendNATPMPPublicAddressChangeNotification");
- return;
- }
-@@ -471,7 +471,7 @@ void SendNATPMPPublicAddressChangeNotifi
- (struct sockaddr *)&sockname, sizeof(struct sockaddr_in));
- if(n < 0)
- {
-- syslog(LOG_ERR, "%s: sendto(s_udp=%d, port=%d): %m",
-+ syslog(LOG_DEBUG, "%s: sendto(s_udp=%d, port=%d): %m",
- "SendNATPMPPublicAddressChangeNotification", sockets[j], NATPMP_PORT);
- return;
- }
-@@ -481,7 +481,7 @@ void SendNATPMPPublicAddressChangeNotifi
- (struct sockaddr *)&sockname, sizeof(struct sockaddr_in));
- if(n < 0)
- {
-- syslog(LOG_ERR, "%s: sendto(s_udp=%d, port=%d): %m",
-+ syslog(LOG_DEBUG, "%s: sendto(s_udp=%d, port=%d): %m",
- "SendNATPMPPublicAddressChangeNotification", sockets[j], NATPMP_NOTIF_PORT);
- return;
- }
---- a/netfilter/iptcrdr.c
-+++ b/netfilter/iptcrdr.c
-@@ -715,7 +715,7 @@ delete_filter_rule(const char * ifname,
- continue;
- }
- index = i;
-- /*syslog(LOG_INFO, "Trying to delete filter rule at index %u", index);*/
-+ /*syslog(LOG_DEBUG, "Trying to delete filter rule at index %u", index);*/
- r = delete_rule_and_commit(index, h, miniupnpd_forward_chain, "delete_filter_rule");
- h = NULL;
- break;
-@@ -808,7 +808,7 @@ delete_redirect_and_filter_rules(unsigne
- #endif
- if(r == 0)
- {
-- syslog(LOG_INFO, "Trying to delete nat rule at index %u", index);
-+ syslog(LOG_DEBUG, "Trying to delete nat rule at index %u", index);
- /* Now delete both rules */
- /* first delete the nat rule */
- h = iptc_init("nat");
-@@ -852,7 +852,7 @@ delete_redirect_and_filter_rules(unsigne
- if(iaddr != e->ip.dst.s_addr)
- continue;
- index = i;
-- syslog(LOG_INFO, "Trying to delete filter rule at index %u", index);
-+ syslog(LOG_DEBUG, "Trying to delete filter rule at index %u", index);
- r = delete_rule_and_commit(index, h, miniupnpd_forward_chain, "delete_filter_rule");
- h = NULL;
- break;
-@@ -910,7 +910,7 @@ delete_redirect_and_filter_rules(unsigne
- }
-
- index = i;
-- syslog(LOG_INFO, "Trying to delete peer rule at index %u", index);
-+ syslog(LOG_DEBUG, "Trying to delete peer rule at index %u", index);
- r2 = delete_rule_and_commit(index, h, miniupnpd_nat_postrouting_chain, "delete_peer_rule");
- h = NULL;
- break;
-@@ -962,7 +962,7 @@ delete_redirect_and_filter_rules(unsigne
- if(iaddr != e->ip.src.s_addr)
- continue;
- index = i;
-- syslog(LOG_INFO, "Trying to delete dscp rule at index %u", index);
-+ syslog(LOG_DEBUG, "Trying to delete dscp rule at index %u", index);
- r2 = delete_rule_and_commit(index, h, miniupnpd_nat_chain, "delete_dscp_rule");
- h = NULL;
- break;
---- a/netfilter/iptpinhole.c
-+++ b/netfilter/iptpinhole.c
-@@ -295,14 +295,14 @@ find_pinhole(const char * ifname,
-
- if(rem_host && (rem_host[0] != '\0')) {
- if (inet_pton(AF_INET6, rem_host, &saddr) < 1) {
-- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", rem_host);
-+ syslog(LOG_INFO, "Failed to parse INET6 address \"%s\"", rem_host);
- memset(&saddr, 0, sizeof(struct in6_addr));
- }
- } else {
- memset(&saddr, 0, sizeof(struct in6_addr));
- }
- if (inet_pton(AF_INET6, int_client, &daddr) < 1) {
-- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", int_client);
-+ syslog(LOG_INFO, "Failed to parse INET6 address \"%s\"", int_client);
- memset(&daddr, 0, sizeof(struct in6_addr));
- }
- for(p = pinhole_list.lh_first; p != NULL; p = p->entries.le_next) {
---- a/netfilter_nft/nftnlrdr.c
-+++ b/netfilter_nft/nftnlrdr.c
-@@ -353,7 +353,7 @@ delete_redirect_and_filter_rules(unsigne
- }
- }
- } else {
-- syslog(LOG_WARNING, "%s: redirect rule with eport=%hu proto %d NOT FOUND",
-+ syslog(LOG_INFO, "%s: redirect rule with eport=%hu proto %d NOT FOUND",
- "delete_redirect_and_filter_rules", eport, proto);
- }
-
---- a/netfilter_nft/nftnlrdr_misc.c
-+++ b/netfilter_nft/nftnlrdr_misc.c
-@@ -110,7 +110,7 @@ nft_mnl_connect(void)
- return -1;
- }
- mnl_portid = mnl_socket_get_portid(mnl_sock);
-- syslog(LOG_INFO, "mnl_socket bound, port_id=%u", mnl_portid);
-+ syslog(LOG_DEBUG, "mnl_socket bound, port_id=%u", mnl_portid);
- return 0;
- }
-
-@@ -751,7 +751,7 @@ refresh_nft_cache(struct rule_list *head
- errno = 0;
- ret = mnl_cb_run(buf, n, mnl_seq, mnl_portid, table_cb, &data);
- if (ret <= -1 /*== MNL_CB_ERROR*/) {
-- syslog(LOG_ERR, "%s: mnl_cb_run returned %d: %m",
-+ syslog(LOG_DEBUG, "%s: mnl_cb_run returned %d: %m",
- "refresh_nft_cache", ret);
- return -1;
- }
-@@ -1288,7 +1288,7 @@ nft_send_rule(struct nftnl_rule * rule,
-
- result = send_batch(batch);
- if (result < 0) {
-- syslog(LOG_ERR, "%s(%p, %d, %d) send_batch failed %d",
-+ syslog(LOG_DEBUG, "%s(%p, %d, %d) send_batch failed %d",
- "nft_send_rule", rule, (int)cmd, (int)chain_type, result);
- }
- }
-@@ -1474,7 +1474,7 @@ send_batch(struct mnl_nlmsg_batch *batch
- errno = 0;
- ret = mnl_cb_run(buf, n, 0, mnl_portid, NULL, NULL);
- if (ret <= -1 /*== MNL_CB_ERROR*/) {
-- syslog(LOG_ERR, "%s: mnl_cb_run returned %d: %m",
-+ syslog(LOG_DEBUG, "%s: mnl_cb_run returned %d: %m",
- "send_batch", ret);
- return -4;
- }
---- a/netfilter_nft/nftpinhole.c
-+++ b/netfilter_nft/nftpinhole.c
-@@ -133,7 +133,7 @@ find_pinhole(const char * ifname,
-
- if (rem_host && rem_host[0] != '\0' && rem_host[0] != '*') {
- if (inet_pton(AF_INET6, rem_host, &saddr) < 1) {
-- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", rem_host);
-+ syslog(LOG_INFO, "Failed to parse INET6 address \"%s\"", rem_host);
- memset(&saddr, 0, sizeof(struct in6_addr));
- }
- } else {
-@@ -141,7 +141,7 @@ find_pinhole(const char * ifname,
- }
-
- if (inet_pton(AF_INET6, int_client, &daddr) < 1) {
-- syslog(LOG_WARNING, "Failed to parse INET6 address \"%s\"", int_client);
-+ syslog(LOG_INFO, "Failed to parse INET6 address \"%s\"", int_client);
- memset(&daddr, 0, sizeof(struct in6_addr));
- }
-
-@@ -163,7 +163,7 @@ find_pinhole(const char * ifname,
- (0 == memcmp(&daddr, &p->daddr6, sizeof(struct in6_addr)))) {
-
- if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
-- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);
-+ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/
- continue;
- }
-
-@@ -400,7 +400,7 @@ get_pinhole_info(unsigned short uid,
- if (timestamp) {
- int uid_temp;
- if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid_temp, &ts) != 2) {
-- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);
-+ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/
- continue;
- }
-
-@@ -464,7 +464,7 @@ clean_pinhole_list(unsigned int * next_t
- continue;
-
- if (sscanf(p->desc, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
-- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);
-+ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", p->desc);*/
- continue;
- }
-
---- a/pcpserver.c
-+++ b/pcpserver.c
-@@ -1094,7 +1094,7 @@ static void CreatePCPMap(pcp_info_t *pcp
- else
- r = CreatePCPMap_NAT(pcp_msg_info);
- pcp_msg_info->result_code = r;
-- syslog(r == PCP_SUCCESS ? LOG_INFO : LOG_ERR,
-+ syslog(LOG_INFO,
- "PCP MAP: %s mapping %s %hu->%s:%hu '%s'",
- r == PCP_SUCCESS ? "added" : "failed to add",
- proto_itoa(pcp_msg_info->protocol),
-@@ -1136,7 +1136,7 @@ static void DeletePCPMap(pcp_info_t *pcp
- if(0 != strcmp(desc, pcp_msg_info->desc)) {
- /* nonce does not match */
- pcp_msg_info->result_code = PCP_ERR_NOT_AUTHORIZED;
-- syslog(LOG_ERR, "Unauthorized to remove PCP mapping internal port %hu, protocol %s",
-+ syslog(LOG_INFO, "Unauthorized to remove PCP mapping internal port %hu, protocol %s",
- iport, proto_itoa(pcp_msg_info->protocol));
- return;
- } else {
-@@ -1154,14 +1154,14 @@ static void DeletePCPMap(pcp_info_t *pcp
- desc, sizeof(desc),
- NULL /* lifetime */);
- if (uid < 0) {
-- syslog(LOG_ERR, "Failed to find mapping to %s:%hu, protocol %s",
-+ syslog(LOG_INFO, "Failed to find mapping to %s:%hu, protocol %s",
- pcp_msg_info->mapped_str, iport, proto_itoa(pcp_msg_info->protocol));
- return;
- } else {
- if(0 != strcmp(desc, pcp_msg_info->desc)) {
- /* nonce does not match */
- pcp_msg_info->result_code = PCP_ERR_NOT_AUTHORIZED;
-- syslog(LOG_ERR, "Unauthorized to remove PCP mapping internal port %hu, protocol %s",
-+ syslog(LOG_INFO, "Unauthorized to remove PCP mapping internal port %hu, protocol %s",
- iport, proto_itoa(pcp_msg_info->protocol));
- return;
- } else {
-@@ -1176,7 +1176,7 @@ static void DeletePCPMap(pcp_info_t *pcp
- syslog(LOG_INFO, "PCP: %s port %hu mapping removed",
- proto==IPPROTO_TCP?"TCP":"UDP", (pcp_msg_info->is_fw ? iport : eport2));
- } else {
-- syslog(LOG_ERR, "Failed to remove PCP mapping to %s:%hu %s",
-+ syslog(LOG_INFO, "Failed to remove PCP mapping to %s:%hu %s",
- pcp_msg_info->mapped_str, iport, proto_itoa(proto));
- pcp_msg_info->result_code = PCP_ERR_NO_RESOURCES;
- }
-@@ -1399,7 +1399,7 @@ static int processPCPRequest(void * req,
- CreatePCPMap(pcp_msg_info);
- }
- } else {
-- syslog(LOG_ERR, "PCP: Invalid PCP v2 MAP message.");
-+ syslog(LOG_DEBUG, "PCP: Invalid PCP v2 MAP message.");
- return pcp_msg_info->result_code;
- }
-
-@@ -1609,7 +1609,7 @@ int ProcessIncomingPCPPacket(int s, unsi
- if (!GETFLAG(PCP_ALLOWTHIRDPARTYMASK)) {
- lan_addr = get_lan_for_peer(senderaddr);
- if(lan_addr == NULL) {
-- syslog(LOG_WARNING, "PCP packet sender %s not from a LAN, ignoring",
-+ syslog(LOG_DEBUG, "PCP packet sender %s not from a LAN, ignoring",
- addr_str);
- return 0;
- }
-@@ -1730,7 +1730,7 @@ void PCPSendUnsolicitedAnnounce(int * so
- }
- len = sendto_or_schedule(sockets[i], buff, PCP_MIN_LEN, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
- if( len < 0 ) {
-- syslog(LOG_ERR, "PCPSendUnsolicitedAnnounce(sockets[%d]) sendto(): %m", i);
-+ syslog(LOG_DEBUG, "PCPSendUnsolicitedAnnounce(sockets[%d]) sendto(): %m", i);
- }
- }
- #ifdef ENABLE_IPV6
-@@ -1741,7 +1741,7 @@ void PCPSendUnsolicitedAnnounce(int * so
- addr6.sin6_port = htons(5350);
- len = sendto_or_schedule(socket6, buff, PCP_MIN_LEN, 0, (struct sockaddr *)&addr6, sizeof(struct sockaddr_in6));
- if( len < 0 ) {
-- syslog(LOG_ERR, "PCPSendUnsolicitedAnnounce() IPv6 sendto(): %m");
-+ syslog(LOG_DEBUG, "PCPSendUnsolicitedAnnounce() IPv6 sendto(): %m");
- }
- }
- #endif /* ENABLE_IPV6 */
---- a/pf/obsdrdr.c
-+++ b/pf/obsdrdr.c
-@@ -602,7 +602,7 @@ delete_nat_rule(const char * ifname, uns
- pr.nr = i;
- if(ioctl(dev, DIOCCHANGERULE, &pr) < 0)
- {
-- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
-+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
- r = -1;
- }
- else
-@@ -1292,7 +1292,7 @@ priv_delete_redirect_rule_check_desc(con
- pr.nr = i;
- if(ioctl(dev, DIOCCHANGERULE, &pr) < 0)
- {
-- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
-+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
- r = -1;
- break;
- }
-@@ -1377,7 +1377,7 @@ syslog(LOG_DEBUG, "%2d port=%hu proto=%d
- pr.nr = i;
- if(ioctl(dev, DIOCCHANGERULE, &pr) < 0)
- {
-- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
-+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
- r = -1;
- }
- else
---- a/pf/pfpinhole.c
-+++ b/pf/pfpinhole.c
-@@ -246,12 +246,12 @@ int find_pinhole(const char * ifname,
- (0 == memcmp(&daddr, &RULE.dst.addr.v.a.addr.v6, sizeof(struct in6_addr)))) {
- #ifdef USE_LIBPFCTL
- if(sscanf(RULE.label[0], PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
-- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);
-+ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);*/
- continue;
- }
- #else /* USE_LIBPFCTL */
- if(sscanf(RULE.label, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
-- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);
-+ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);*/
- continue;
- }
- #endif /* USE_LIBPFCTL */
-@@ -360,7 +360,7 @@ int delete_pinhole(unsigned short uid)
- pr.action = PF_CHANGE_REMOVE;
- pr.nr = i;
- if(ioctl(dev, DIOCCHANGERULE, &pr) < 0) {
-- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
-+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
- release_ticket(dev, tnum);
- return -1;
- }
-@@ -565,7 +565,7 @@ int clean_pinhole_list(unsigned int * ne
- return -1;
- }
- if(sscanf(RULE.label[0], PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
-- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);
-+ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label[0]);*/
- continue;
- }
- #else /* USE_LIBPFCTL */
-@@ -576,7 +576,7 @@ int clean_pinhole_list(unsigned int * ne
- return -1;
- }
- if(sscanf(RULE.label, PINEHOLE_LABEL_FORMAT_SKIPDESC, &uid, &ts) != 2) {
-- syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);
-+ /*syslog(LOG_DEBUG, "rule with label '%s' is not a IGD pinhole", RULE.label);*/
- continue;
- }
- #endif /* USE_LIBPFCTL */
-@@ -602,7 +602,7 @@ int clean_pinhole_list(unsigned int * ne
- pr.action = PF_CHANGE_REMOVE;
- pr.nr = i;
- if(ioctl(dev, DIOCCHANGERULE, &pr) < 0) {
-- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
-+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
- release_ticket(dev, tnum);
- return -1;
- }
---- a/upnphttp.c
-+++ b/upnphttp.c
-@@ -545,7 +545,7 @@ ProcessHTTPPOST_upnphttp(struct upnphttp
- if(h->req_soapActionOff > 0)
- {
- /* we can process the request */
-- syslog(LOG_INFO, "SOAPAction: %.*s",
-+ syslog(LOG_DEBUG, "SOAPAction: %.*s",
- h->req_soapActionLen, h->req_buf + h->req_soapActionOff);
- ExecuteSoapAction(h,
- h->req_buf + h->req_soapActionOff,
-@@ -845,7 +845,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt
- for(i = 0; i<15 && *p != '\r'; i++)
- HttpVer[i] = *(p++);
- HttpVer[i] = '\0';
-- syslog(LOG_INFO, "HTTP REQUEST from %s : %s %s (%s)",
-+ syslog(LOG_DEBUG, "HTTP REQUEST from %s : %s %s (%s)",
- h->clientaddr_str, HttpCommand, HttpUrl, HttpVer);
- ParseHttpHeaders(h);
- if(h->req_contentlen > CONTENT_LENGTH_LIMIT) {
-@@ -931,7 +931,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt
- return;
- }
- #endif
-- syslog(LOG_NOTICE, "%s not found, responding ERROR 404", HttpUrl);
-+ syslog(LOG_DEBUG, "%s not found, responding ERROR 404", HttpUrl);
- Send404(h);
- }
- #ifdef ENABLE_EVENTS
---- a/upnppinhole.c
-+++ b/upnppinhole.c
-@@ -110,7 +110,7 @@ lease_file6_add(const char * rem_client,
-
- fd = fopen( lease_file6, "a");
- if (fd==NULL) {
-- syslog(LOG_ERR, "could not open lease file: %s", lease_file);
-+ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file);
- return -1;
- }
-
-@@ -398,7 +398,7 @@ int lease_file6_expire(void)
- // Internal Host
- p = strchr(line, ';');
- if(!p) {
-- syslog(LOG_ERR, "unrecognized data in lease file");
-+ syslog(LOG_DEBUG, "unrecognized data in lease file (bug)");
- continue;
- }
- *(p++) = '\0';
-@@ -489,7 +489,7 @@ int reload_from_lease_file6(void)
- if(!lease_file6) return -1;
- fd = fopen( lease_file6, "r");
- if (fd==NULL) {
-- syslog(LOG_ERR, "could not open lease file: %s", lease_file6);
-+ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file6);
- return -1;
- }
- if(unlink(lease_file6) < 0) {
-@@ -575,7 +575,7 @@ int reload_from_lease_file6(void)
-
- if(timestamp > 0) {
- if(timestamp <= (unsigned int)current_unix_time) {
-- syslog(LOG_NOTICE, "already expired lease in lease file");
-+ syslog(LOG_INFO, "already expired lease in lease file");
- continue;
- } else {
- leaseduration = timestamp - current_unix_time;
---- a/upnpredirect.c
-+++ b/upnpredirect.c
-@@ -67,7 +67,7 @@ lease_file_add(const char * rhost,
-
- fd = fopen( lease_file, "a");
- if (fd==NULL) {
-- syslog(LOG_ERR, "could not open lease file: %s", lease_file);
-+ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file);
- return -1;
- }
-
-@@ -196,7 +196,7 @@ int reload_from_lease_file(void)
- if(!lease_file) return -1;
- fd = fopen( lease_file, "r");
- if (fd==NULL) {
-- syslog(LOG_ERR, "could not open lease file: %s", lease_file);
-+ syslog(LOG_DEBUG, "could not open lease file: %s", lease_file);
- return -1;
- }
- if(unlink(lease_file) < 0) {
-@@ -266,7 +266,7 @@ int reload_from_lease_file(void)
- timestamp += current_time; /* convert to our time */
- #else
- if(timestamp <= (unsigned int)current_unix_time) {
-- syslog(LOG_NOTICE, "already expired lease in lease file (%hu=>%s:%hu %s)",
-+ syslog(LOG_INFO, "already expired lease in lease file (%hu=>%s:%hu %s)",
- eport, iaddr, iport, proto);
- continue;
- } else {
-@@ -425,7 +425,7 @@ upnp_redirect(const char * rhost, unsign
- #endif /* CHECK_PORTINUSE */
- } else {
- timestamp = (leaseduration > 0) ? upnp_time() + leaseduration : 0;
-- syslog(LOG_INFO, "redirecting port %hu to %s:%hu protocol %s for: %s",
-+ syslog(LOG_DEBUG, "redirecting port %hu to %s:%hu protocol %s for: %s",
- eport, iaddr, iport, protocol, desc);
- return upnp_redirect_internal(rhost, eport, iaddr, iport, proto,
- desc, timestamp);
-@@ -675,7 +675,7 @@ get_upnp_rules_state_list(int max_rules_
- {
- if(tmp->to_remove)
- {
-- syslog(LOG_NOTICE, "remove port mapping %hu %s because it has expired",
-+ syslog(LOG_INFO, "remove port mapping %hu %s because it has expired",
- tmp->eport, proto_itoa(tmp->proto));
- _upnp_delete_redir(tmp->eport, tmp->proto);
- *p = tmp->next;
---- a/upnpsoap.c
-+++ b/upnpsoap.c
-@@ -412,18 +412,9 @@ GetExternalIPAddress(struct upnphttp * h
- else
- {
- struct in_addr addr;
-- if(getifaddr(ext_if_name, ext_ip_addr, INET_ADDRSTRLEN, &addr, NULL) < 0)
-- {
-- syslog(LOG_ERR, "Failed to get ip address for interface %s",
-- ext_if_name);
-- ext_ip_addr[0] = '\0';
-- } else if (addr_is_reserved(&addr)) {
-- if (GETFLAG(ALLOWPRIVATEIPV4MASK)) {
-- syslog(LOG_WARNING, "IGNORED : private/reserved address %s is not suitable for external IP", ext_ip_addr);
-- } else {
-- syslog(LOG_NOTICE, "private/reserved address %s is not suitable for external IP", ext_ip_addr);
-+ if (getifaddr(ext_if_name, ext_ip_addr, INET_ADDRSTRLEN, &addr, NULL) < 0 ||
-+ (addr_is_reserved(&addr) && !GETFLAG(ALLOWPRIVATEIPV4MASK))) {
- ext_ip_addr[0] = '\0';
-- }
- }
- }
- #else
-@@ -545,7 +536,7 @@ AddPortMapping(struct upnphttp * h, cons
- }
- else
- {
-- syslog(LOG_ERR, "Failed to convert hostname '%s' to ip address", int_ip);
-+ syslog(LOG_INFO, "Failed to convert hostname '%s' to ip address", int_ip);
- ClearNameValueList(&data);
- SoapError(h, 402, "Invalid Args");
- return;
-@@ -750,7 +741,7 @@ AddAnyPortMapping(struct upnphttp * h, c
- }
- else
- {
-- syslog(LOG_ERR, "Failed to convert hostname '%s' to ip address", int_ip);
-+ syslog(LOG_INFO, "Failed to convert hostname '%s' to ip address", int_ip);
- ClearNameValueList(&data);
- SoapError(h, 402, "Invalid Args");
- return;
-@@ -913,7 +904,7 @@ GetSpecificPortMappingEntry(struct upnph
- }
- else
- {
-- syslog(LOG_INFO, "%s: rhost='%s' %s %s found => %s:%u desc='%s' duration=%u",
-+ syslog(LOG_DEBUG, "%s: rhost='%s' %s %s found => %s:%u desc='%s' duration=%u",
- action,
- r_host ? r_host : "NULL", ext_port, protocol, int_ip,
- (unsigned int)iport, desc, leaseduration);
-@@ -1003,7 +994,7 @@ DeletePortMapping(struct upnphttp * h, c
- return;
- }
-
-- syslog(LOG_INFO, "%s: external port: %hu, protocol: %s",
-+ syslog(LOG_DEBUG, "%s: external port: %hu, protocol: %s",
- action, eport, protocol);
-
- /* if in secure mode, check the IP
-@@ -1125,7 +1116,7 @@ DeletePortMappingRange(struct upnphttp *
- for(i = 0; i < number; i++)
- {
- r = upnp_delete_redirection(port_list[i], protocol);
-- syslog(LOG_INFO, "%s: deleting external port: %hu, protocol: %s: %s",
-+ syslog(LOG_DEBUG, "%s: deleting external port: %hu, protocol: %s: %s",
- action, port_list[i], protocol, r < 0 ? "failed" : "ok");
- }
- free(port_list);
-@@ -1196,7 +1187,7 @@ GetGenericPortMappingEntry(struct upnpht
- return;
- }
-
-- syslog(LOG_INFO, "%s: index=%d", action, (int)index);
-+ syslog(LOG_DEBUG, "%s: index=%d", action, (int)index);
-
- rhost[0] = '\0';
- r = upnp_get_redirection_infos_by_index((int)index, &eport, protocol, &iport,
-@@ -1777,7 +1768,7 @@ PinholeVerification(struct upnphttp * h,
- struct addrinfo hints, *ai, *p;
- int found = 0;
-
-- syslog(LOG_INFO, "%s: InternalClient %s is not an IPv6, assume hostname and convert",
-+ syslog(LOG_DEBUG, "%s: InternalClient %s is not an IPv6, assume hostname and convert",
- "PinholeVerification", int_ip);
-
- memset(&hints, 0, sizeof(hints));
-@@ -1788,7 +1779,7 @@ PinholeVerification(struct upnphttp * h,
- r = getaddrinfo(int_ip, NULL, &hints, &ai);
- if (r != 0)
- {
-- syslog(LOG_WARNING, "%s: Failed to convert hostname '%s' to IP address : %s",
-+ syslog(LOG_INFO, "%s: Failed to convert hostname '%s' to IP address : %s",
- "PinholeVerification", int_ip, gai_strerror(r));
- SoapError(h, 402, "Invalid Args");
- return -1;
-@@ -1803,11 +1794,11 @@ PinholeVerification(struct upnphttp * h,
-
- if (inet_ntop(AF_INET6, &result_ip, int_ip_resolved, INET6_ADDRSTRLEN) == NULL)
- {
-- syslog(LOG_WARNING, "%s: inet_ntop(): %m", "PinholeVerification");
-+ syslog(LOG_INFO, "%s: inet_ntop(): %m", "PinholeVerification");
- SoapError(h, 501, "Action Failed");
- return -1;
- }
-- syslog(LOG_INFO, "%s: InternalClient \"%s\" resolved as %s",
-+ syslog(LOG_DEBUG, "%s: InternalClient \"%s\" resolved as %s",
- "PinholeVerification", int_ip, int_ip_resolved);
- found = 1;
- }
-@@ -1823,7 +1814,7 @@ PinholeVerification(struct upnphttp * h,
- freeaddrinfo(ai);
- if (!found)
- {
-- syslog(LOG_NOTICE, "%s: No IPv6 address for hostname '%s'",
-+ syslog(LOG_INFO, "%s: No IPv6 address for hostname '%s'",
- "PinholeVerification", int_ip);
- SoapError(h, 402, "Invalid Args");
- return -1;
-@@ -1841,7 +1832,7 @@ PinholeVerification(struct upnphttp * h,
- /* no need to copy to int_ip_resolved, but we still need to fill result_ip up */
- if (inet_pton(AF_INET6, int_ip, &result_ip) <= 0)
- {
-- syslog(LOG_ERR, "inet_pton(%s)", int_ip);
-+ syslog(LOG_DEBUG, "inet_pton(%s)", int_ip);
- SoapError(h, 501, "Action Failed");
- return -1;
- }
-@@ -1974,7 +1965,7 @@ AddPinhole(struct upnphttp * h, const ch
- inet_ntop(AF_INET6,
- &(((struct sockaddr_in6 *)p->ai_addr)->sin6_addr),
- rem_ip, sizeof(rem_ip));
-- syslog(LOG_INFO, "resolved '%s' to '%s'", rem_host, rem_ip);
-+ syslog(LOG_DEBUG, "resolved '%s' to '%s'", rem_host, rem_ip);
- rem_host = rem_ip;
- break;
- }
-@@ -1983,7 +1974,7 @@ AddPinhole(struct upnphttp * h, const ch
- }
- else
- {
-- syslog(LOG_WARNING, "AddPinhole : getaddrinfo(%s) : %s",
-+ syslog(LOG_INFO, "AddPinhole : getaddrinfo(%s) : %s",
- rem_host, gai_strerror(err));
- #if 0
- SoapError(h, 402, "Invalid Args");
-@@ -2696,7 +2687,7 @@ SoapError(struct upnphttp * h, int errCo
- char body[2048];
- int bodylen;
-
-- syslog(LOG_INFO, "Returning UPnPError %d: %s", errCode, errDesc);
-+ syslog(LOG_DEBUG, "Returning UPnPError %d: %s", errCode, errDesc);
- bodylen = snprintf(body, sizeof(body), resp, errCode, errDesc);
- if(bodylen < 0) {
- syslog(LOG_ERR, "snprintf() returned %d", bodylen);
---- a/upnpstun.c
-+++ b/upnpstun.c
-@@ -10,9 +10,10 @@
- /*! \file upnpstun.c
- * \brief STUN client implementation
- *
-- * - https://datatracker.ietf.org/doc/html/rfc3489 (obsolete)
-- * - https://datatracker.ietf.org/doc/html/rfc5389
-- * - https://datatracker.ietf.org/doc/html/rfc5780 (experimental)
-+ * - https://www.rfc-editor.org/info/rfc3489/ (classic STUN, obsoleted)
-+ * - https://www.rfc-editor.org/info/rfc5389/ (STUN obsoleted)
-+ * - https://www.rfc-editor.org/info/rfc5780/ (STUN NAT behavior, experimental)
-+ * - https://www.rfc-editor.org/info/rfc8489/ (STUN current, not implemented)
- */
- #include
- #include
-@@ -124,8 +125,8 @@ static int resolve_stun_host(const char
-
- r = getaddrinfo(stun_host, service, &hints, &result);
- if (r != 0) {
-- syslog(LOG_ERR, "%s: getaddrinfo(%s, %s, ...) failed : %s",
-- "resolve_stun_host", stun_host, service, gai_strerror(r));
-+ syslog(LOG_ERR, "STUN: Failed to resolve hostname (%s) to IPv4 address (%s)",
-+ stun_host, gai_strerror(r));
- errno = EHOSTUNREACH;
- return -1;
- }
-@@ -149,8 +150,8 @@ static int resolve_stun_host(const char
- } else {
- char addr_str[48];
- if (sockaddr_to_string((struct sockaddr *)sock_addr, addr_str, sizeof(addr_str)) > 0) {
-- syslog(LOG_DEBUG, "%s: %s:%s => %s",
-- "resolve_stun_host", stun_host, service, addr_str);
-+ syslog(LOG_DEBUG, "%s: Resolve hostname (%s) and connect to %s",
-+ "resolve_stun_host", stun_host, addr_str);
- }
- }
-
-@@ -274,7 +275,7 @@ static int wait_for_stun_responses(int f
- FD_SET(fds[i], &fdset);
- }
-
-- syslog(LOG_DEBUG, "%s: waiting %ld secs and %ld usecs", "wait_for_stun_responses", (long)timeout.tv_sec, (long)timeout.tv_usec);
-+ syslog(LOG_DEBUG, "%s: waiting %ld.%ld s", "wait_for_stun_responses", (long)timeout.tv_sec, (long)timeout.tv_usec);
- ret = select(max_fd+1, &fdset, NULL, NULL, &timeout);
- if (ret < 0) {
- if (errno == EINTR)
-@@ -484,7 +485,8 @@ static int parse_stun_response(unsigned
- if (!have_other_address && have_address) {
- syslog(LOG_ERR, "STUN server not supported, not returning "
- "OTHER-ADDRESS / support CHANGE-REQUEST's required for "
-- "endpoint-independent (1:1) CGNAT filtering tests per RFC 5780.");
-+ "endpoint-independent (1:1) CGNAT filtering tests per RFC 5780, "
-+ "see compatible servers in sample config");
- return -1;
- }
- return (have_address && have_other_address) ? 0 : -1;
-@@ -529,13 +531,13 @@ int perform_stun(const char *if_name, co
-
- /* Determine unrestricted endpoint-independent (1:1) CGNAT in two STUN requests per RFC 5780 4.4 test I/II */
- /* 1. Connectivity (binding, detect public IPv4), 2. CHANGE-REQUEST with change-IP and change-port set */
-- /* https://datatracker.ietf.org/doc/html/rfc5780#section-4.4 */
-+ /* https://www.rfc-editor.org/info/rfc5780/#section-4.4 */
- fill_request(requests[i], i, i);
- transaction_ids[i] = requests[i]+8;
- }
-
-- syslog(LOG_INFO, "%s: local ports %hu %hu %hu %hu",
-- "perform_stun", local_ports[0], local_ports[1],
-+ syslog(LOG_INFO, "STUN: Testing with local UDP ports %hu %hu %hu %hu",
-+ local_ports[0], local_ports[1],
- local_ports[2], local_ports[3]);
-
- /* Unblock local ports */
-@@ -600,16 +602,15 @@ int perform_stun(const char *if_name, co
- if (mapped_addrs_count < 4) {
- /* We have not received all four responses,
- * therefore NAT or firewall is doing some filtering */
-- syslog(LOG_NOTICE, "%s: %d response out of 4 received",
-- "perform_stun", mapped_addrs_count);
-+ syslog(LOG_NOTICE, "STUN: Filtering detected as %d/4 responses received (1: IPv4 detection, 2-4: filtering tests)",
-+ mapped_addrs_count);
- *restrictive_nat = 1;
- }
-
- if (memcmp(&remote_addr, &peer_addrs[0], sizeof(peer_addrs[0])) != 0) {
- /* We received STUN response from different address
- * even we did not asked for it, so some strange NAT is active */
-- syslog(LOG_NOTICE, "%s: address changed",
-- "perform_stun");
-+ syslog(LOG_NOTICE, "STUN: Public IPv4 change detected in responses");
- *restrictive_nat = 1;
- }
-
-@@ -621,8 +622,8 @@ int perform_stun(const char *if_name, co
- sockaddr_to_string((struct sockaddr *)&mapped_addrs[i], mapped_addr_str, sizeof(mapped_addr_str));
- /* External IP address or port was changed,
- * therefore symmetric NAT is active */
-- syslog(LOG_NOTICE, "%s: #%d external address or port changed : %s:%hu => %s",
-- "perform_stun", i, inet_ntoa(*ext_addr), local_ports[i], mapped_addr_str);
-+ syslog(LOG_NOTICE, "STUN: Public IPv4/port change detected in response #%d (%s:%hu => %s)",
-+ i+1, inet_ntoa(*ext_addr), local_ports[i], mapped_addr_str);
- *restrictive_nat = 1;
- }
- }
diff --git a/miniupnpd/test.sh b/miniupnpd/test.sh
new file mode 100755
index 000000000..8b977e198
--- /dev/null
+++ b/miniupnpd/test.sh
@@ -0,0 +1,93 @@
+#!/bin/sh
+# Functional tests for the miniupnpd IGD/NAT-PMP daemon variants. The package
+# version is checked by the build framework, so it is not tested here.
+
+bin=/usr/sbin/miniupnpd
+
+# Binary, runtime linking and the OpenWrt integration files common to both
+# firewall variants. "miniupnpd -h" exits non-zero but must print the usage
+# banner, which also proves every shared-library dep resolves at runtime.
+common_checks() {
+ [ -x "$bin" ] || { echo "FAIL: $bin missing or not executable"; exit 1; }
+ "$bin" -h 2>&1 | grep -q "^Usage:" \
+ || { echo "FAIL: 'miniupnpd -h' printed no usage banner"; exit 1; }
+ [ -x /etc/init.d/miniupnpd ] || { echo "FAIL: init script missing"; exit 1; }
+ grep -q "USE_PROCD=1" /etc/init.d/miniupnpd \
+ || { echo "FAIL: init script is not a procd service"; exit 1; }
+ grep -q "config upnpd" /etc/config/upnpd \
+ || { echo "FAIL: /etc/config/upnpd is not valid UCI"; exit 1; }
+ [ -f /etc/hotplug.d/iface/50-miniupnpd ] \
+ || { echo "FAIL: iface hotplug script missing"; exit 1; }
+}
+
+# The config-file parser runs before any firewall or socket setup, so it can be
+# exercised inside the CI container. A bogus option must be rejected non-zero
+# and a minimal valid config must be accepted without a parse error.
+config_checks() {
+ good="/tmp/miniupnpd-good.$$.conf"
+ bad="/tmp/miniupnpd-bad.$$.conf"
+ log="/tmp/miniupnpd.$$.log"
+ cat > "$good" <<-EOF
+ ext_ifname=lo
+ listening_ip=127.0.0.1
+ port=5000
+ enable_natpmp=yes
+ enable_upnp=yes
+ notify_interval=60
+ uuid=00000000-0000-0000-0000-000000000000
+ EOF
+ echo "not_a_real_option=1" > "$bad"
+
+ if "$bin" -f "$bad" -d > "$log" 2>&1; then
+ echo "FAIL: invalid config option was accepted"
+ rm -f "$good" "$bad" "$log"; exit 1
+ fi
+ grep -qiE "invalid option|Error reading configuration" "$log" \
+ || { echo "FAIL: no parse error reported for bad config"
+ rm -f "$good" "$bad" "$log"; exit 1; }
+
+ # A valid config: launch briefly and confirm the parser accepted it. The
+ # daemon may later stop at firewall/socket setup in the container; that is
+ # past the parser and irrelevant to this check.
+ "$bin" -f "$good" -d > "$log" 2>&1 &
+ mpid=$!
+ sleep 2
+ kill "$mpid" 2>/dev/null
+ wait "$mpid" 2>/dev/null
+ if grep -qiE "invalid option|Error reading configuration" "$log"; then
+ echo "FAIL: valid config was rejected by the parser"
+ rm -f "$good" "$bad" "$log"; exit 1
+ fi
+ rm -f "$good" "$bad" "$log"
+}
+
+case "$1" in
+miniupnpd-iptables)
+ # iptables variant ships the fw3 shell include. Its uci-defaults seeder
+ # is not checked here: the postinst sources /etc/uci-defaults/* and
+ # deletes each script that succeeds, so it is gone by now.
+ common_checks
+ [ -f /usr/share/miniupnpd/firewall.include ] || { echo "FAIL: firewall.include missing"; exit 1; }
+ sh -n /usr/share/miniupnpd/firewall.include \
+ || { echo "FAIL: firewall.include has a shell syntax error"; exit 1; }
+ config_checks
+ echo "miniupnpd-iptables: all functional tests passed"
+ ;;
+miniupnpd-nftables)
+ # nftables variant ships the nft rule fragments consumed by fw4.
+ common_checks
+ for f in table-post/20-miniupnpd.nft \
+ chain-post/dstnat/20-miniupnpd.nft \
+ chain-post/forward/20-miniupnpd.nft \
+ chain-post/srcnat/20-miniupnpd.nft; do
+ [ -s "/usr/share/nftables.d/$f" ] \
+ || { echo "FAIL: nft fragment $f missing or empty"; exit 1; }
+ done
+ config_checks
+ echo "miniupnpd-nftables: all functional tests passed"
+ ;;
+*)
+ echo "Untested package: $1" >&2
+ exit 1
+ ;;
+esac
diff --git a/my-default-settings/Makefile b/my-default-settings/Makefile
index 843c3b59b..23e3c1e53 100644
--- a/my-default-settings/Makefile
+++ b/my-default-settings/Makefile
@@ -10,7 +10,7 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=my-default-settings
PKG_VERSION:=2
-PKG_RELEASE:=35
+PKG_RELEASE:=36
include $(INCLUDE_DIR)/package.mk
diff --git a/my-default-settings/files/etc/uci-defaults/99-default-settings b/my-default-settings/files/etc/uci-defaults/99-default-settings
index 60c1e8d33..f87dc90b1 100644
--- a/my-default-settings/files/etc/uci-defaults/99-default-settings
+++ b/my-default-settings/files/etc/uci-defaults/99-default-settings
@@ -74,7 +74,7 @@ if [ "$version" -lt 1 ]; then
EOF
(echo root; echo root) | passwd root >/dev/null 2>&1
- [ -f /usr/share/rpcd/acl.d/luci-theme-vantage.json ] && uci -q set luci.main.mediaurlbase='/luci-static/vantage'
+ [ -d /usr/share/ucode/luci/template/themes/vantage ] && uci -q set luci.main.mediaurlbase='/luci-static/vantage'
grep -q 'log-facility' /etc/dnsmasq.conf 2>/dev/null || echo "log-facility=/dev/null" >> /etc/dnsmasq.conf
mkdir -p /etc/sysctl.d