From 72c44a6325e82e4d4ab868707bc25cbca36f078f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Oct 2026 02:34:41 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Sync=202026-10-13=2002:34:41?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-adguardhome/.official_channel | 2 +- luci-app-passwall/Makefile | 2 +- .../model/cbi/passwall/client/acl_config.lua | 4 +- .../model/cbi/passwall/client/global.lua | 9 +- luci-app-passwall/luasrc/passwall/api.lua | 45 +- .../view/passwall/cbi/nodes_listvalue.htm | 42 +- .../view/passwall/cbi/nodes_listvalue_com.htm | 6 +- .../luasrc/view/passwall/cbi/nodes_value.htm | 42 +- .../view/passwall/cbi/nodes_value_com.htm | 6 +- .../luasrc/view/passwall/global/status.htm | 4 +- luci-app-passwall2/Makefile | 2 +- .../model/cbi/passwall2/client/acl_config.lua | 6 +- luci-app-trafficctl/Makefile | 4 +- .../resources/view/trafficctl/status.js | 18 + .../root/usr/libexec/rpcd/luci.trafficctl | 3 +- .../root/usr/local/bin/trafficctl-block.sh | 9 +- .../root/usr/local/bin/trafficctl-fw.sh | 78 + .../usr/local/bin/trafficctl-ratelimit.sh | 16 +- .../root/usr/local/bin/trafficctl-shape.sh | 13 +- .../resources/view/status/include/80_upnp.js | 2 +- .../luci-static/resources/view/upnp/upnp.js | 4 +- .../root/usr/share/rpcd/ucode/luci.upnp | 26 +- luci-app-vantage/Makefile | 4 +- .../luci-static/resources/vantage/app.css | 282 +- .../resources/view/wizard/initsetup.js | 10 +- luci-theme-vantage/Makefile | 4 +- .../luci-static/resources/menu-vantage.js | 160 +- .../htdocs/luci-static/vantage/vantage.css | 369 ++- .../po/templates/vantage-theme.pot | 126 +- miniupnpd/Makefile | 9 +- miniupnpd/files/miniupnpd.init | 2 +- miniupnpd/files/upnpd.config | 3 +- ...mpat.patch => 010-upnp-igdv2-compat.patch} | 2 +- miniupnpd/patches/020-improve-logging.patch | 2454 +++++++++++++++++ miniupnpd/patches/20-improve-logging.patch | 1379 --------- miniupnpd/test.sh | 93 + my-default-settings/Makefile | 2 +- .../etc/uci-defaults/99-default-settings | 2 +- 38 files changed, 3510 insertions(+), 1734 deletions(-) rename miniupnpd/patches/{10-upnp-igdv2-compat.patch => 010-upnp-igdv2-compat.patch} (98%) create mode 100644 miniupnpd/patches/020-improve-logging.patch delete mode 100644 miniupnpd/patches/20-improve-logging.patch create mode 100755 miniupnpd/test.sh 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