From 1c4f74462413f9d714f8a0266af69eb8a9b1851b Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 01/10] miniupnpd: update to 2.3.9 to fix issues, refresh building - Update daemon to 2.3.9 to fix removal of nftables rules in `upnp_forward` and return the correct client port. This also resulted in the excessive opening of new ports - Build from GitHub releases to get a reliable HTTPS server, as the HTTP-only/HTTPS mirror were only available ~85%/77% over 3 months https://redirect.github.com/miniupnp/miniupnp/issues/770 https://stats.uptimerobot.com/DwGDxUB914 - Build daemon with `--disable-pppconn` to remove the old/IGDv1-only extra WANPPPConnection SSDP announcements workaround not included in other implementations since >15y - Build daemon with `--vendorcfg` to allow customisation of the router/friendly name (+5 potential options) displayed in Windows Explorer, 384 bytes extra required on ARMv7 (binary) - Remove old (iptables variant only) patches, as no longer needed - Remove `clean_ruleset_interval/threshold` UCI config options as not standard/working since OpenWrt 22.03, as nftables not supported Fix: https://redirect.github.com/openwrt/openwrt/issues/18011 Fix: https://redirect.github.com/openwrt/luci/issues/7759 Signed-off-by: Self-Hosting-Group --- net/miniupnpd/Makefile | 10 +++++--- net/miniupnpd/files/miniupnpd.init | 7 +----- .../patches/200-remove-default-cflags.patch | 24 ------------------- net/miniupnpd/patches/300-macos-compat.patch | 22 ----------------- 4 files changed, 8 insertions(+), 55 deletions(-) delete mode 100644 net/miniupnpd/patches/200-remove-default-cflags.patch delete mode 100644 net/miniupnpd/patches/300-macos-compat.patch diff --git a/net/miniupnpd/Makefile b/net/miniupnpd/Makefile index c8900c7050a46e..df63883d4008b5 100644 --- a/net/miniupnpd/Makefile +++ b/net/miniupnpd/Makefile @@ -8,12 +8,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=miniupnpd -PKG_VERSION:=2.3.7 +PKG_VERSION:=2.3.9 PKG_RELEASE:=1 -PKG_SOURCE_URL:=https://miniupnp.tuxfamily.org/files +PKG_SOURCE_URL:=https://github.com/miniupnp/miniupnp/releases/download/miniupnpd_$(subst .,_,$(PKG_VERSION)) PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz -PKG_HASH:=fbdd5501039730f04a8420ea2f8f54b7df63f9f04cde2dc67fa7371e80477bbe +PKG_HASH:=66cb3c3d697ab2bb3a61d3c48628166d6ba328d7c2dbeb95898fdf2a3202af7b PKG_MAINTAINER:= PKG_LICENSE:=BSD-3-Clause @@ -80,14 +80,18 @@ CONFIGURE_ARGS = \ --portinuse \ --firewall=$(BUILD_VARIANT) \ --disable-fork \ + --disable-pppconn \ + --vendorcfg \ --regex TARGET_CFLAGS += $(FPIC) TARGET_LDFLAGS += -Wl,--gc-sections,--as-needed ifeq ($(BUILD_VARIANT),iptables) +ifeq ($(filter $(ARCH),mips mipsel),) TARGET_CFLAGS += -flto endif +endif define Package/miniupnpd/install/Default $(INSTALL_DIR) $(1)/usr/sbin diff --git a/net/miniupnpd/files/miniupnpd.init b/net/miniupnpd/files/miniupnpd.init index de3504529b600c..15bd2e0f9d45b8 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -61,8 +61,7 @@ upnpd() { local external_iface external_iface6 external_zone external_ip internal_iface local upload download log_output port config_file serial_number model_number local use_stun stun_host stun_port uuid notify_interval presentation_url - local upnp_lease_file clean_ruleset_threshold clean_ruleset_interval - local ipv6_disable + local upnp_lease_file ipv6_disable local enabled config_get_bool enabled config enabled 1 @@ -87,8 +86,6 @@ upnpd() { config_get notify_interval config notify_interval config_get presentation_url config presentation_url config_get upnp_lease_file config upnp_lease_file - config_get clean_ruleset_threshold config clean_ruleset_threshold - config_get clean_ruleset_interval config clean_ruleset_interval config_get ipv6_disable config ipv6_disable 0 local conf ifname ifname6 @@ -156,8 +153,6 @@ upnpd() { [ -n "$upnp_lease_file" ] && touch "$upnp_lease_file" && echo "lease_file=$upnp_lease_file" [ -n "$presentation_url" ] && echo "presentation_url=$presentation_url" [ -n "$notify_interval" ] && echo "notify_interval=$notify_interval" - [ -n "$clean_ruleset_threshold" ] && echo "clean_ruleset_threshold=$clean_ruleset_threshold" - [ -n "$clean_ruleset_interval" ] && echo "clean_ruleset_interval=$clean_ruleset_interval" [ -n "$serial_number" ] && echo "serial=$serial_number" [ -n "$model_number" ] && echo "model_number=$model_number" [ -n "$port" ] && echo "port=$port" diff --git a/net/miniupnpd/patches/200-remove-default-cflags.patch b/net/miniupnpd/patches/200-remove-default-cflags.patch deleted file mode 100644 index 0a1ba0a2339f89..00000000000000 --- a/net/miniupnpd/patches/200-remove-default-cflags.patch +++ /dev/null @@ -1,24 +0,0 @@ ---- a/Makefile.linux -+++ b/Makefile.linux -@@ -25,16 +25,16 @@ CONFIG_OPTIONS ?= $(cat .configure.cache - CONFIG_OPTIONS += --firewall=iptables - #CFLAGS = -O -g -DDEBUG - CFLAGS ?= -Os --CFLAGS += -fno-strict-aliasing --CFLAGS += -fno-common --CFLAGS += -fstack-protector -fPIE --CFLAGS += -D_FORTIFY_SOURCE=2 -+#CFLAGS += -fno-strict-aliasing -+#CFLAGS += -fno-common -+#CFLAGS += -fstack-protector -fPIE -+#CFLAGS += -D_FORTIFY_SOURCE=2 - CPPFLAGS += -D_GNU_SOURCE - CFLAGS += -Wall - CFLAGS += -Wextra -Wstrict-prototypes -Wdeclaration-after-statement - #CFLAGS += -Wno-missing-field-initializers - #CFLAGS += -ansi # iptables headers does use typeof which is a gcc extension --LDFLAGS += -Wl,-z,now -Wl,-z,relro -pie -+LDFLAGS ?= -Wl,-z,now -Wl,-z,relro -pie - CC ?= gcc - RM = rm -f - INSTALL = install diff --git a/net/miniupnpd/patches/300-macos-compat.patch b/net/miniupnpd/patches/300-macos-compat.patch deleted file mode 100644 index 8a569f35f7f7ef..00000000000000 --- a/net/miniupnpd/patches/300-macos-compat.patch +++ /dev/null @@ -1,22 +0,0 @@ ---- a/Makefile.linux -+++ b/Makefile.linux -@@ -96,13 +96,13 @@ endif # ($(TEST),1) - endif # ($(TARGET_OPENWRT,) - - ifneq ($(shell ldd --version | grep GLIBC),) --GLIBC_VERSION := $(shell ldd --version | head -n 1 | sed 's/^.* //') --GLIBC_VERSION_MAJOR = $(shell echo $(GLIBC_VERSION) | cut -f 1 -d . ) --GLIBC_VERSION_MINOR = $(shell echo $(GLIBC_VERSION) | cut -f 2 -d . ) -+#GLIBC_VERSION := $(shell ldd --version | head -n 1 | sed 's/^.* //') -+#GLIBC_VERSION_MAJOR = $(shell echo $(GLIBC_VERSION) | cut -f 1 -d . ) -+#GLIBC_VERSION_MINOR = $(shell echo $(GLIBC_VERSION) | cut -f 2 -d . ) - # clock_gettime() needs -lrt when glibc version < 2.17 --LDLIBS += $(shell if [ $(GLIBC_VERSION_MAJOR) -lt 2 ] \ -- || [ \( $(GLIBC_VERSION_MAJOR) -eq 2 \) -a \( $(GLIBC_VERSION_MINOR) -lt 17 \) ] ; \ -- then echo "-lrt" ; fi ) -+#LDLIBS += $(shell if [ $(GLIBC_VERSION_MAJOR) -lt 2 ] \ -+# || [ \( $(GLIBC_VERSION_MAJOR) -eq 2 \) -a \( $(GLIBC_VERSION_MINOR) -lt 17 \) ] ; \ -+# then echo "-lrt" ; fi ) - endif - - TESTUPNPDESCGENOBJS = testupnpdescgen.o upnpdescgen.o From 2065458a142ade850e2bf8be767b4a7b2911caea Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 02/10] miniupnpd: patch for UPnP IGDv2 Microsoft/Apple compat - Add workaround to list port maps with the Windows IGDv2-incompatible client by returning an infinite (0) lease duration. To fix listing and editing via GUI (Explorer/Network), if daemon was compiled with IGDv2 - Extend detection to older versions of Windows and add Xbox - Detect Apple IGDv2-incompatible clients and apply existing workaround, that only caused problems if PCP/NAT-PMP (prioritised) was disabled (to merge with prior) Link: https://github.com/Self-Hosting-Group/miniupnp/tree/upnp-igdv2-compat Signed-off-by: Self-Hosting-Group --- .../patches/010-upnp-igdv2-compat.patch | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 net/miniupnpd/patches/010-upnp-igdv2-compat.patch diff --git a/net/miniupnpd/patches/010-upnp-igdv2-compat.patch b/net/miniupnpd/patches/010-upnp-igdv2-compat.patch new file mode 100644 index 00000000000000..08ef5b61bc4b50 --- /dev/null +++ b/net/miniupnpd/patches/010-upnp-igdv2-compat.patch @@ -0,0 +1,75 @@ +From edd7e856c09b5ba2e2c5774768e3ed752d33feff Mon Sep 17 00:00:00 2001 +From: Self-Hosting-Group + <155233284+Self-Hosting-Group@users.noreply.github.com> +Date: Thu, 27 Nov 2025 00:00:00 +0000 +Subject: [PATCH] miniupnpd: UPnP IGDv2 Microsoft/Apple compatibility + +- Add workaround to list port maps with the Windows IGDv2-incompatible + client by returning an infinite (0) lease duration. To fix listing and + editing via GUI (Explorer/Network), if daemon was compiled with IGDv2 +- Extend detection to older versions of Windows and add Xbox +- Detect Apple IGDv2-incompatible clients and apply existing workaround, + that only caused problems if PCP/NAT-PMP (prioritised) was disabled + +Link: https://github.com/Self-Hosting-Group/miniupnp/tree/upnp-igdv2-compat + +--- a/upnphttp.c ++++ b/upnphttp.c +@@ -303,9 +303,22 @@ ParseHttpHeaders(struct upnphttp * h) + } + else if(strncasecmp(line, "user-agent:", 11) == 0) + { +- /* - User-Agent: Microsoft-Windows/10.0 UPnP/1.0 +- * - User-Agent: FDSSDP */ +- if(strcasestr(line + 11, "microsoft") != NULL || strstr(line + 11, "FDSSDP") != NULL) { ++ /* Detect Microsoft UPnP IGDv2-incompatible clients that only support IGDv1 routers, ++ * and Windows requires extra UDA 1.x (Win XP 1.0), via User-Agent SOAP/HTTP header: ++ * - FDSSDP (Win >=Vista for GET) ++ * - Microsoft-Windows/10.0 UPnP/1.0 (Win >=10 for POST) ++ * - Microsoft-Windows/6.1 UPnP/1.0 (Win 7 for GET/POST) ++ * - Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1) (Win XP/Vista for GET) ++ * - Mozilla/4.0 (compatible; UPnP/1.0; Windows 9x) (Win XP/Vista for POST) ++ * - Xbox/2.0.17559.0 UPnP/1.0 Xbox/2.0.17559.0 (for GET/POST) ++ * Detect Apple UPnP IGDv2-incompatible clients that only support IGDv1 routers: ++ * - Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1) (for GET) ++ * - Mozilla/4.0 (compatible; UPnP/1.0; Windows 9x) (for POST) */ ++ if (((strstr(line + 11, "Microsoft-Windows/") != NULL || ++ strstr(line + 11, "Xbox/") != NULL) && ++ strstr(line + 11, " UPnP/1.0") != NULL) || ++ strstr(line + 11, "FDSSDP") != NULL || ++ strstr(line + 11, "Mozilla/4.0 (compatible; UPnP/1.0; Windows") != NULL) { + h->respflags |= FLAG_MS_CLIENT; + } + } +--- a/upnpsoap.c ++++ b/upnpsoap.c +@@ -854,6 +854,14 @@ GetSpecificPortMappingEntry(struct upnph + #ifdef ENABLE_PCP + hide_pcp_nonce(desc); + #endif ++#ifdef IGD_V2 ++ /* Workaround to list port maps with the Windows IGDv2-incompatible client ++ * by returning an infinite (0) lease duration. To fix listing and editing ++ * via GUI (Explorer/Network), if daemon was compiled with IGDv2 */ ++ if (h->respflags & FLAG_MS_CLIENT) { ++ leaseduration = 0; ++ } ++#endif + bodylen = snprintf(body, sizeof(body), resp, + action, ns/*SERVICE_TYPE_WANIPC*/, + (unsigned int)iport, int_ip, desc, leaseduration, +@@ -1124,6 +1132,14 @@ GetGenericPortMappingEntry(struct upnpht + #ifdef ENABLE_PCP + hide_pcp_nonce(desc); + #endif ++#ifdef IGD_V2 ++ /* Workaround to list port maps with the Windows IGDv2-incompatible client ++ * by returning an infinite (0) lease duration. To fix listing and editing ++ * via GUI (Explorer/Network), if daemon was compiled with IGDv2 */ ++ if (h->respflags & FLAG_MS_CLIENT) { ++ leaseduration = 0; ++ } ++#endif + bodylen = snprintf(body, sizeof(body), resp, + action, ns, /*SERVICE_TYPE_WANIPC,*/ rhost, + (unsigned int)eport, protocol, (unsigned int)iport, iaddr, desc, From c6a2aabd3d1fde141e84cd8f25d5fef9cb1cbc86 Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 03/10] miniupnpd: patch to fix description filter option To fix the non-working description regex filter option (to merge with prior) Link: https://redirect.github.com/miniupnp/miniupnp/pull/853 Signed-off-by: Self-Hosting-Group --- .../patches/020-descr-filter-fix.patch | 511 ++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 net/miniupnpd/patches/020-descr-filter-fix.patch diff --git a/net/miniupnpd/patches/020-descr-filter-fix.patch b/net/miniupnpd/patches/020-descr-filter-fix.patch new file mode 100644 index 00000000000000..9d657d15345f00 --- /dev/null +++ b/net/miniupnpd/patches/020-descr-filter-fix.patch @@ -0,0 +1,511 @@ +From 54ecff0ae4452598773a020e8798ff61ccf3c966 Mon Sep 17 00:00:00 2001 +From: yangfl +Date: Thu, 31 Jul 2025 10:35:50 +0800 +Subject: [PATCH] miniupnpd: Rewrite permission line parser + +Permission line parser rewrites input buffer in-place for parsing, +which causes several problems: + +* It unnecessarily invalidates input buffer for caller. Actually, you + might see the following error message when parsing fails: + miniupnpd[1234]: parsing error file /etc/miniupnpd.conf line 16 : allow 1024 + since the hyphen after 1024 is erased by a '\0'. + +* It fails to validate token separators. For example, the following + line will be accepted: + allow 1024-65535X0.0.0.0/0 1024-65535 all + ^ could be any character + and even a potential buffer over-read if the character is '\0', since + the parser blindlessly skips it. + +* The fifth token is never parsed since it gets a previously written + '\0'. + +Instead of fixing them case-by-case, we rewrite it with DFS in mind, +which is much simpler and less error-prone. + +--- a/upnppermissions.c ++++ b/upnppermissions.c +@@ -29,6 +29,12 @@ isodigit(char c) + return '0' <= c && c >= '7'; + } + ++static int ++iseol(char c) ++{ ++ return c == '\0' || c == '\n' || c == '\r'; ++} ++ + static char + hex2chr(char c) + { +@@ -91,6 +97,84 @@ unescape_char(const char * s, int * seql + return c; + } + ++/* greedy parser: try to match the longest sequence and do not ++ * check for terminators */ ++ ++static char * ++get_sep(const char * s) ++{ ++ if(!isspace(*s)) ++ return NULL; ++ do ++ s++; ++ while(isspace(*s)); ++ return (char *) s; ++} ++ ++static char * ++get_ushort(const char * s, u_short * val) ++{ ++ char * end; ++ unsigned long val_ul; ++ ++ if(!isdigit(*s)) ++ return NULL; ++ val_ul = strtoul(s, &end, 10); ++ if(val_ul > 65535) ++ return NULL; ++ *val = (u_short)val_ul; ++ ++ return end; ++} ++ ++static char * ++get_range(const char * s, u_short * begin, u_short * end) ++{ ++ s = get_ushort(s, begin); ++ if(!s) ++ return NULL; ++ ++ if(*s!='-') ++ *end = *begin; ++ else ++ { ++ s++; ++ s = get_ushort(s, end); ++ if(!s) ++ return NULL; ++ if(*begin > *end) ++ return NULL; ++ } ++ return (char *) s; ++} ++ ++static char * ++get_addr(const char * s, struct in_addr * addr, unsigned int * dot_cnt) ++{ ++ size_t i; ++ char buf[64]; ++ ++ if(!isdigit(*s)) ++ return NULL; ++ ++ *dot_cnt = 0; ++ for(i = 0; isdigit(s[i]) || s[i] == '.';) ++ { ++ if(s[i] == '.') ++ (*dot_cnt)++; ++ buf[i] = s[i]; ++ i++; ++ if (i > sizeof(buf) - 1) ++ return NULL; ++ } ++ ++ buf[i] = '\0'; ++ if(!inet_aton(buf, addr)) ++ return NULL; ++ ++ return (char *) s + i; ++} ++ + /* get_next_token(s, &token, raw) + * put the unquoted/unescaped token in token and returns + * a pointer to the begining of the next token +@@ -99,18 +183,8 @@ static char * + get_next_token(const char * s, char ** token, int raw) + { + char deli; +- const char * end; ++ size_t len; + +- /* skip any whitespace */ +- for(; isspace(*s); s++) +- if(*s == '\0' || *s == '\n') +- { +- if(token) +- *token = NULL; +- return (char *) s; +- } +- +- /* find the start */ + if(*s == '"' || *s == '\'') + { + deli = *s; +@@ -119,85 +193,90 @@ get_next_token(const char * s, char ** t + else + deli = 0; + /* find the end */ +- end = s; +- for(; *end != '\0' && *end != '\n' && (deli ? *end != deli : !isspace(*end)); +- end++) +- if(*end == '\\') ++ for(len = 0; !iseol(s[len]) && (deli ? s[len] != deli : !isspace(s[len])); ++ len++) ++ if(s[len] == '\\') + { +- end++; +- if(*end == '\0') ++ len++; ++ if(iseol(s[len])) + break; + } + + /* save the token */ + if(token) + { +- unsigned int token_len; +- unsigned int i; +- +- token_len = end - s; +- *token = strndup(s, token_len); +- if(!*token) +- return NULL; +- +- for(i = 0; (*token)[i] != '\0'; i++) +- { +- int sequence_len; +- +- if((*token)[i] != '\\') +- continue; +- +- if(raw && deli && (*token)[i + 1] != deli) +- continue; +- (*token)[i] = unescape_char(*token + i, &sequence_len); +- memmove(*token + i + 1, *token + i + sequence_len, +- token_len - i - sequence_len); +- } +- if (i == 0) +- { +- /* behavior of realloc(p, 0) is implementation-defined, so better set it to NULL. +- * https://github.com/miniupnp/miniupnp/issues/652#issuecomment-1518922139 */ +- free(*token); ++ if(len == 0) + *token = NULL; +- } + else + { +- char * tmp = realloc(*token, i); +- if (tmp != NULL) +- *token = tmp; ++ unsigned int i; ++ unsigned int j; ++ ++ char * tmp; ++ char * t; ++ ++ t = malloc(len + 1); ++ if(!t) ++ return NULL; ++ ++ if (raw) ++ { ++ memcpy(t, s, len); ++ j = len; ++ } + else +- syslog(LOG_ERR, "%s: failed to reallocate to %u bytes", +- "get_next_token()", i); ++ { ++ for(i = 0, j = 0; i < len; j++) ++ if(s[i] != '\\') ++ { ++ t[j] = s[i]; ++ i++; ++ } ++ else ++ { ++ int seqlen; ++ t[j] = unescape_char(s + i, &seqlen); ++ i += seqlen; ++ if (i > len) ++ break; ++ } ++ ++ tmp = realloc(*token, j + 1); ++ if (tmp != NULL) ++ t = tmp; ++ else ++ syslog(LOG_ERR, "%s: failed to reallocate to %u bytes", ++ "get_next_token()", j + 1); ++ } ++ t[j] = '\0'; ++ *token = t; + } + } + +- /* return the beginning of the next token */ +- if(deli && *end == deli) +- end++; +- while(isspace(*end)) +- end++; +- return (char *) end; ++ s += len; ++ if(deli && *s == deli) ++ s++; ++ return (char *) s; + } + + /* read_permission_line() + * parse the a permission line which format is : +- * (deny|allow) [0-9]+(-[0-9]+) ip/mask [0-9]+(-[0-9]+) regex ++ * (deny|allow) [0-9]+(-[0-9]+)? ip(/mask)? [0-9]+(-[0-9]+)? (regex)? + * ip/mask is either 192.168.1.1/24 or 192.168.1.1/255.255.255.0 + */ + int + read_permission_line(struct upnpperm * perm, +- char * p) ++ const char * p) + { +- char * q; +- int n_bits; +- int i; ++ unsigned int dot_cnt; + + /* zero memory : see https://github.com/miniupnp/miniupnp/issues/652 */ + memset(perm, 0, sizeof(struct upnpperm)); + +- /* first token: (allow|deny) */ + while(isspace(*p)) + p++; ++ ++ /* first token: (allow|deny) */ + if(0 == memcmp(p, "allow", 5)) + { + perm->type = UPNPPERM_ALLOW; +@@ -212,133 +291,61 @@ read_permission_line(struct upnpperm * p + { + return -1; + } +- while(isspace(*p)) +- p++; ++ ++ p = get_sep(p); ++ if(!p) ++ return -1; + + /* second token: eport or eport_min-eport_max */ +- if(!isdigit(*p)) ++ p = get_range(p, &perm->eport_min, &perm->eport_max); ++ if(!p) + return -1; +- for(q = p; isdigit(*q); q++); +- if(*q=='-') +- { +- *q = '\0'; +- i = atoi(p); +- if(i > 65535) +- return -1; +- perm->eport_min = (u_short)i; +- q++; +- p = q; +- while(isdigit(*q)) +- q++; +- *q = '\0'; +- i = atoi(p); +- if(i > 65535) +- return -1; +- perm->eport_max = (u_short)i; +- if(perm->eport_min > perm->eport_max) +- return -1; +- } +- else if(isspace(*q)) +- { +- *q = '\0'; +- i = atoi(p); +- if(i > 65535) +- return -1; +- perm->eport_min = perm->eport_max = (u_short)i; +- } +- else +- { ++ ++ p = get_sep(p); ++ if(!p) + return -1; +- } +- p = q + 1; +- while(isspace(*p)) +- p++; + +- /* third token: ip/mask */ +- if(!isdigit(*p)) ++ /* third token: ip/mask */ ++ p = get_addr(p, &perm->address, &dot_cnt); ++ if(!p) + return -1; +- for(q = p; isdigit(*q) || (*q == '.'); q++); +- if(*q=='/') ++ ++ if(*p!='/') ++ perm->mask.s_addr = 0xffffffffu; ++ else + { +- *q = '\0'; +- if(!inet_aton(p, &perm->address)) +- return -1; +- q++; +- p = q; +- while(isdigit(*q)) +- q++; +- if(*q == '.') +- { +- while(*q == '.' || isdigit(*q)) +- q++; +- if(!isspace(*q)) +- return -1; +- *q = '\0'; +- if(!inet_aton(p, &perm->mask)) +- return -1; +- } +- else if(!isspace(*q)) ++ p++; ++ p = get_addr(p, &perm->mask, &dot_cnt); ++ if(!p) + return -1; +- else ++ /* inet_aton(): When only one part is given, the value is stored ++ * directly in the network address without any byte ++ * rearrangement. */ ++ if(!dot_cnt) + { +- *q = '\0'; +- n_bits = atoi(p); ++ unsigned int n_bits = ntohl(perm->mask.s_addr); + if(n_bits > 32) + return -1; +- perm->mask.s_addr = htonl(n_bits ? (0xffffffffu << (32 - n_bits)) : 0); ++ perm->mask.s_addr = !n_bits ? 0 : htonl(0xffffffffu << (32 - n_bits)); + } + } +- else if(isspace(*q)) +- { +- *q = '\0'; +- if(!inet_aton(p, &perm->address)) +- return -1; +- perm->mask.s_addr = 0xffffffffu; +- } +- else +- { ++ ++ p = get_sep(p); ++ if(!p) + return -1; +- } +- p = q + 1; + + /* fourth token: iport or iport_min-iport_max */ +- while(isspace(*p)) +- p++; +- if(!isdigit(*p)) ++ p = get_range(p, &perm->iport_min, &perm->iport_max); ++ if(!p) + return -1; +- for(q = p; isdigit(*q); q++); +- if(*q=='-') +- { +- *q = '\0'; +- i = atoi(p); +- if(i > 65535) +- return -1; +- perm->iport_min = (u_short)i; +- q++; +- p = q; +- while(isdigit(*q)) +- q++; +- *q = '\0'; +- i = atoi(p); +- if(i > 65535) +- return -1; +- perm->iport_max = (u_short)i; +- if(perm->iport_min > perm->iport_max) +- return -1; +- } +- else if(isspace(*q) || *q == '\0') +- { +- *q = '\0'; +- i = atoi(p); +- if(i > 65535) +- return -1; +- perm->iport_min = perm->iport_max = (u_short)i; +- } +- else +- { ++ ++ if(iseol(*p) || *p == '#') ++ goto end; ++ p = get_sep(p); ++ if(!p) + return -1; +- } +- p = q; ++ if(iseol(*p) || *p == '#') ++ goto end; + + /* fifth token: (optional) regex */ + p = get_next_token(p, &perm->re, 1); +@@ -381,6 +388,7 @@ read_permission_line(struct upnpperm * p + } + } + ++end: + #ifdef DEBUG + printf("perm rule added : %s %hu-%hu %08x/%08x %hu-%hu %s\n", + (perm->type==UPNPPERM_ALLOW) ? "allow" : "deny", +@@ -427,8 +435,26 @@ write_permlist(int fd, const struct upnp + write(fd, buf, l); + if(perm->re) + { +- write(fd, " ", 1); +- write(fd, perm->re, strlen(perm->re)); ++ const char * p; ++ write(fd, " \"", 2); ++ for(p = perm->re; *p != '\0'; p++) ++ { ++ if(*p == '"') ++ { ++ write(fd, "\\\"", 2); ++ continue; ++ } ++ ++ if(*p == '\\') ++ { ++ write(fd, p, 1); ++ p++; ++ if(*p == '\0') ++ break; ++ } ++ write(fd, p, 1); ++ } ++ write(fd, "\"", 1); + } + write(fd, "\n", 1); + } +--- a/upnppermissions.h ++++ b/upnppermissions.h +@@ -43,7 +43,7 @@ struct upnpperm { + * deny 0-65535 0.0.0.0/0 0-65535 */ + int + read_permission_line(struct upnpperm * perm, +- char * p); ++ const char * p); + + void + free_permission_line(struct upnpperm * perm); +@@ -72,4 +72,3 @@ write_permlist(int fd, const struct upnp + #endif + + #endif +- From 3efe766d5891dc242e942fe890ee5140cb41d939 Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 04/10] miniupnpd: package revision and new/updated UCI options The following settings UCI options been added or changed, and the previous options are migrated on updating: upnpd.config UCI options | Change | Previous name -----------------------------|---------------------------|-------------- enabled | Match default (1) | enabled_protocols=upnp-igd | Combined option | enable_upnp=1 enabled_protocols=pcp+nat-pmp| Combined option | enable_natpmp=1 allow_cgnat | Allow allow-filtered (2) | use_stun stun_host | Allow port inclusion (3) | stun_port | Removed, included in host | allow_third_party_mapping=0 | Inverted/extended to PCP | secure_mode=1 log_output | Allow info log level | lease_file | Set by default + IPv6 (4) | upnp_lease_file upnp_igd_compat=igdv1 | Renamed/match default (1) | igdv1=1 download_kbps | In kbit/s and renamed (5) | download upload_kbps | In kbit/s and renamed (5) | upload friendly_name | New option, router name | http_port | Renamed, rem. if default | port notify_interval | Removed if <900s, minimum | internal_iface | Migrated, new section | internal_network UCI options | Change | Previous name -----------------------------|---------------------------|-------------- interface | New option | access_preset | New option (6) | accept_ports | New option (6) | reject_ports | New option (7) | custom_acl_before | New option (6) | Notes: 1. Init UCI default now matches LuCI and initial config file defaults for: enabled=0 and upnp_igd_compat=igdv1 2. Allow allow-filtered for IPv4 CGNAT use and migrate option from X-Wrt and only use STUN when necessary with a private/CGNAT external IPv4 3. Remove known incompatible STUN servers and set compatible by default 4. Configure undocumented daemon option `lease_file6=${lease_file}-ipv6` so that active IPv6 port maps are not lost when service restarts, e.g. by deleting an active port map. Use /run path, symlinked and appeared in FHS 3.0 in 2015 and remove option if UCI default is set 5. Gets converted, config file now defaults to interface link speed instead of 8/4 Mbit/s, which is removed on migration 6. New options added to select a preset for all devices on the network and decide if the custom ACL should be checked before the preset. Extra ports can also be set that are accepted/rejected. Presets: accept-high-ports/accept-high-ports+web[+dns]/accept-all-ports or 0 7. Reject ports regardless of other settings. By default reject unsafe: 21 (FTP), 23 (Telnet), DCE/NetBIOS/SMB (135/137-139/445), RDP (3389) Code refactoring: - Add a function for logging and output to stderr, and extend logging - Revise daemon init/config-gen slightly by declare all UCI options (incl. booleans) according to the same principle and remove `upnpd_write_bool` - Document and reformat default `/etc/config/upnpd` UCI config file Depends on: https://redirect.github.com/openwrt/luci/pull/7822 Fix: https://redirect.github.com/openwrt/packages/issues/17413 Signed-off-by: Self-Hosting-Group --- net/miniupnpd/Makefile | 2 + net/miniupnpd/files/miniupnpd.init | 206 ++++++++++++------ .../files/upnpd-migration.uci-defaults | 155 +++++++++++++ net/miniupnpd/files/upnpd.config | 56 +++-- 4 files changed, 331 insertions(+), 88 deletions(-) create mode 100644 net/miniupnpd/files/upnpd-migration.uci-defaults diff --git a/net/miniupnpd/Makefile b/net/miniupnpd/Makefile index df63883d4008b5..8a2171aa898e14 100644 --- a/net/miniupnpd/Makefile +++ b/net/miniupnpd/Makefile @@ -98,8 +98,10 @@ define Package/miniupnpd/install/Default $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_DIR) $(1)/etc/config $(INSTALL_DIR) $(1)/etc/hotplug.d/iface + $(INSTALL_DIR) $(1)/etc/uci-defaults $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/miniupnpd $(1)/usr/sbin/miniupnpd $(INSTALL_BIN) ./files/miniupnpd.init $(1)/etc/init.d/miniupnpd + $(INSTALL_BIN) ./files/upnpd-migration.uci-defaults $(1)/etc/uci-defaults/99-miniupnpd-upnpd-migration $(INSTALL_CONF) ./files/upnpd.config $(1)/etc/config/upnpd $(INSTALL_DATA) ./files/miniupnpd.hotplug $(1)/etc/hotplug.d/iface/50-miniupnpd endef diff --git a/net/miniupnpd/files/miniupnpd.init b/net/miniupnpd/files/miniupnpd.init index 15bd2e0f9d45b8..44afdacd0c5303 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -42,51 +42,44 @@ conf_rule_add() { echo "$action $ext_start${ext_end:+-}$ext_end $int_addr $int_start${int_end:+-}$int_end #$comment" } -upnpd_write_bool() { - local opt="$1" - local def="${2:-0}" - local alt="${3:-$opt}" - local val - - config_get_bool val config "$opt" "$def" - if [ "$val" -eq 0 ]; then - echo "$alt=no" - else - echo "$alt=yes" - fi -} - upnpd() { config_load "upnpd" - local external_iface external_iface6 external_zone external_ip internal_iface - local upload download log_output port config_file serial_number model_number - local use_stun stun_host stun_port uuid notify_interval presentation_url - local upnp_lease_file ipv6_disable + local external_iface external_iface6 external_zone external_ip + local upload_kbps download_kbps log_output http_port config_file serial_number model_number + local allow_cgnat stun_host uuid notify_interval presentation_url + local lease_file ipv6_disable + local enabled_protocols allow_third_party_mapping system_uptime upnp_igd_compat + local friendly_name local enabled - config_get_bool enabled config enabled 1 - [ "$enabled" -eq 0 ] && return 1 - + config_get enabled config enabled 0 + if [ "$enabled" != "1" ]; then + log "Service disabled, UCI enabled is not set" + return 1 + fi config_get external_iface config external_iface config_get external_iface6 config external_iface6 config_get external_zone config external_zone config_get external_ip config external_ip - config_get internal_iface config internal_iface - config_get port config port 5000 - config_get upload config upload - config_get download config download - config_get_bool log_output config log_output 0 + config_get http_port config http_port 5000 + config_get upload_kbps config upload_kbps + config_get download_kbps config download_kbps + config_get log_output config log_output config_get config_file config config_file config_get serial_number config serial_number config_get model_number config model_number config_get uuid config uuid - config_get use_stun config use_stun 0 - config_get stun_host config stun_host - config_get stun_port config stun_port + config_get allow_cgnat config allow_cgnat 0 + config_get stun_host config stun_host stun.nextcloud.com config_get notify_interval config notify_interval config_get presentation_url config presentation_url - config_get upnp_lease_file config upnp_lease_file + config_get lease_file config lease_file /run/miniupnpd.leases config_get ipv6_disable config ipv6_disable 0 + config_get enabled_protocols config enabled_protocols all + config_get allow_third_party_mapping config allow_third_party_mapping 0 + config_get system_uptime config system_uptime 1 + config_get upnp_igd_compat config upnp_igd_compat igdv1 + config_get friendly_name config friendly_name "OpenWrt UPnP IGD & PCP" local conf ifname ifname6 @@ -119,53 +112,64 @@ upnpd() { local tmpconf="/var/etc/miniupnpd.conf" conf="$tmpconf" mkdir -p /var/etc - + if [ "$ifname" = "" ]; then + log "No external network interface found, not starting" daemon.err + return 1 + fi + if ! uci -q get upnpd.@internal_network[0].interface >/dev/null; then + log "No internal networks configured, not starting" daemon.err + return 1 + fi { echo "ext_ifname=$ifname" echo "ext_ifname6=$ifname6" [ -n "$external_ip" ] && echo "ext_ip=$external_ip" - local iface - for iface in ${internal_iface:-lan}; do - local device - network_get_device device "$iface" && echo "listening_ip=$device" - done - - config_load "upnpd" - upnpd_write_bool enable_natpmp 1 - upnpd_write_bool enable_upnp 1 - upnpd_write_bool secure_mode 1 - upnpd_write_bool system_uptime 1 - upnpd_write_bool igdv1 0 force_igd_desc_v1 - upnpd_write_bool use_stun 0 ext_perform_stun - upnpd_write_bool ipv6_disable $ipv6_disable - - [ "$use_stun" -eq 0 ] || { - [ -n "$stun_host" ] && echo "ext_stun_host=$stun_host" - [ -n "$stun_port" ] && echo "ext_stun_port=$stun_port" - } + [ "$enabled_protocols" = "all" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=yes" + [ "$enabled_protocols" = "upnp-igd" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=no" + [ "$enabled_protocols" = "pcp+nat-pmp" ] && echo "enable_upnp=no" && echo "enable_pcp_pmp=yes" + [ "$allow_third_party_mapping" = "0" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=no" + [ "$allow_third_party_mapping" = "1" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=yes" + [ "$allow_third_party_mapping" = "upnp-igd" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=no" + [ "$allow_third_party_mapping" = "pcp" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=yes" + [ "$system_uptime" = "0" ] && echo "system_uptime=no" || echo "system_uptime=yes" + [ "$upnp_igd_compat" = "igdv1" ] && echo "force_igd_desc_v1=yes" || echo "force_igd_desc_v1=no" + # Only perform an STUN CGNAT test if necessary, with a private/CGNAT external IPv4 + local extipv4 extipv4private + network_get_ipaddr extipv4 "$external_iface" + case "$extipv4" in + 10.* | 172.1[6-9].* | 172.2[0-9].* | 172.3[0-1].* | 192.168.* | 100.6[4-9].* | 100.[7-9][0-9].* | 100.1[0-1][0-9].* | 100.12[0-7].*) extipv4private=1 ;; + esac + if [ "$extipv4private" = "1" ] && [ "$allow_cgnat" != "0" ] && [ "$external_ip" = "" ]; then + [ "$allow_cgnat" = "1" ] && echo "ext_perform_stun=yes" + [ "$allow_cgnat" = "allow-filtered" ] && echo "ext_perform_stun=allow-filtered" + # Alternatively, as allow-filtered detects the public IPv4 required by multiple clients, e.g. PCP/NAT-PMP + # [ "$allow_cgnat" = "allow-private-ext-ipv4" ] && external_ip=203.1.2.3 + echo "ext_stun_host=${stun_host%%:*}" + [ "${stun_host%%:*}" != "${stun_host##*:}" ] && echo "ext_stun_port=${stun_host##*:}" + fi + [ "$ipv6_disable" = "0" ] && echo "ipv6_disable=no" || echo "ipv6_disable=yes" - [ -n "$upload" ] && [ -n "$download" ] && { - echo "bitrate_down=$((download * 1024 * 8))" - echo "bitrate_up=$((upload * 1024 * 8))" - } + [ -n "$download_kbps" ] && echo "bitrate_down=$((download_kbps * 1000))" + [ -n "$upload_kbps" ] && echo "bitrate_up=$((upload_kbps * 1000))" - [ -n "$upnp_lease_file" ] && touch "$upnp_lease_file" && echo "lease_file=$upnp_lease_file" + touch "$lease_file" && echo "lease_file=$lease_file" + [ "$ipv6_disable" = "0" ] && touch "${lease_file}-ipv6" && echo "lease_file6=${lease_file}-ipv6" + [ -n "$friendly_name" ] && echo "friendly_name=$friendly_name" [ -n "$presentation_url" ] && echo "presentation_url=$presentation_url" [ -n "$notify_interval" ] && echo "notify_interval=$notify_interval" - [ -n "$serial_number" ] && echo "serial=$serial_number" - [ -n "$model_number" ] && echo "model_number=$model_number" - [ -n "$port" ] && echo "port=$port" + echo "serial=$serial_number" + echo "model_number=$model_number" + echo "http_port=$http_port" [ -z "$uuid" ] && { + log "Generate UPnP IGD UUID" uuid="$(cat /proc/sys/kernel/random/uuid)" uci set upnpd.config.uuid="$uuid" uci commit upnpd } - [ "$uuid" = "nocli" ] || echo "uuid=$uuid" - - config_foreach conf_rule_add perm_rule + [ "$uuid" != "nocli" ] && echo "uuid=$uuid" || log "Deprecated. Set uuid to 00000000-0000-0000-0000-000000000000 instead" if [ "$FW" = "fw4" ]; then #When using nftables configure miniupnpd to use its own table and chains @@ -176,6 +180,12 @@ upnpd() { echo "upnp_nat_postrouting_chain=upnp_postrouting" fi + # Enabled internal networks / access control + config_foreach upnpd_add_int_network_and_preset internal_network precustom + echo "# Custom ACL" + config_foreach conf_rule_add perm_rule + config_foreach upnpd_add_int_network_and_preset internal_network postcustom + } > "$tmpconf" fi @@ -186,15 +196,14 @@ upnpd() { else iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload fi - else - logger -t "upnp daemon" "external interface not found, not starting" fi procd_open_instance procd_set_param file "$conf" "/etc/config/firewall" procd_set_param command "$PROG" procd_append_param command -f "$conf" - [ "$log_output" = "1" ] && procd_append_param command -d + [ "$log_output" = "info" ] && procd_append_param command -v + [ "$log_output" = "debug" ] && procd_append_param command -d procd_close_instance } @@ -219,3 +228,72 @@ start_service() { service_triggers() { procd_add_reload_trigger "upnpd" } + +log() { + logger -s -p "${2:-daemon.notice}" -t "upnpd" "$1" || echo "upnpd: $1" >&2 +} + +upnpd_add_int_network_and_preset() { + local cfg="$1" + local interface access_preset accept_ports reject_ports custom_acl_before + config_get interface "$cfg" interface + config_get access_preset "$cfg" access_preset accept-high-ports + config_get accept_ports "$cfg" accept_ports + config_get reject_ports "$cfg" reject_ports "21 23 135 137-139 445 3389" + config_get custom_acl_before "$cfg" custom_acl_before + local device subnet rejectport acceptpresetports acceptport + network_get_device device "$interface" + network_get_subnet subnet "$interface" + if [ "$2" = "precustom" ]; then + echo "# Enable internal network $interface ($device) with preset $access_preset${custom_acl_before:+ and check custom ACL before}" + echo "listening_ip=$device" + fi + [ "$subnet" = "" ] && log "Cannot get IPv4 subnet for network $interface, access_preset ignored" daemon.warn && return 0 + if [ "$2" = "precustom" ]; then + for rejectport in $reject_ports; do + if [ "$rejectport" -ge "1" ] 2>/dev/null && [ "$rejectport" -le "65535" ] 2>/dev/null || + { + [ "${rejectport%%-*}" -ge "1" ] 2>/dev/null && + [ "${rejectport%%-*}" -le "65535" ] 2>/dev/null && + [ "${rejectport##*-}" -ge "1" ] 2>/dev/null && + [ "${rejectport##*-}" -le "65535" ] 2>/dev/null && + [ "${rejectport##*-}" -ge "${rejectport%%-*}" ] 2>/dev/null + }; then + echo "deny $rejectport $subnet $rejectport # Reject port $rejectport on $interface" + else + log "Invalid port or port range ($rejectport) in reject_ports ignored" daemon.warn + fi + done + fi + if { [ "$2" = "postcustom" ] && [ "$custom_acl_before" = "1" ]; } || + { [ "$2" = "precustom" ] && [ "$custom_acl_before" != "1" ]; }; then + if [ "$access_preset" = "accept-high-ports" ]; then + acceptpresetports="1024-65535" + elif [ "$access_preset" = "accept-high-ports+web" ]; then + acceptpresetports="1024-65535 80 443" + elif [ "$access_preset" = "accept-high-ports+web+dns" ]; then + acceptpresetports="1024-65535 80 443 53 853" + elif [ "$access_preset" = "accept-all-ports" ]; then + acceptpresetports="1-65535" + elif [ "$access_preset" != "0" ] && [ "$access_preset" != "accept-listed-ports" ]; then + log "Invalid access_preset ($access_preset) ignored" daemon.warn + fi + for acceptport in $acceptpresetports $accept_ports; do + if [ "$acceptport" -ge "1" ] 2>/dev/null && [ "$acceptport" -le "65535" ] 2>/dev/null || + { + [ "${acceptport%%-*}" -ge "1" ] 2>/dev/null && + [ "${acceptport%%-*}" -le "65535" ] 2>/dev/null && + [ "${acceptport##*-}" -ge "1" ] 2>/dev/null && + [ "${acceptport##*-}" -le "65535" ] 2>/dev/null && + [ "${acceptport##*-}" -ge "${acceptport%%-*}" ] 2>/dev/null + }; then + echo "allow $acceptport $subnet $acceptport # Accept port $acceptport on $interface" + else + log "Invalid port or port range ($acceptport) in accept_ports ignored" daemon.warn + fi + done + fi + if [ "$2" = "precustom" ] && [ "$custom_acl_before" != "1" ] && [ "$access_preset" != "0" ]; then + echo "deny 1-65535 $subnet 1-65535 # Reject ACL by default on $interface" + fi +} diff --git a/net/miniupnpd/files/upnpd-migration.uci-defaults b/net/miniupnpd/files/upnpd-migration.uci-defaults new file mode 100644 index 00000000000000..6904b42fe82268 --- /dev/null +++ b/net/miniupnpd/files/upnpd-migration.uci-defaults @@ -0,0 +1,155 @@ +#!/bin/sh + +log() { + logger -s -p "${2:-daemon.notice}" -t "upnpd" "$1" || echo "upnpd: $1" >&2 +} + +log "Check UCI options in /etc/config/upnpd to be migrated to v2.0" + +# Set missing enabled option to fix previously different defaults in LuCI/config (0) and init UCI (1) +if ! uci -q get upnpd.config.enabled >/dev/null; then + uci -q set upnpd.config.enabled="1" +fi + +# Migrate boolean options to only use 0/1 for LuCI support +for option in enabled ipv6_disable system_uptime; do + if uci -q get upnpd.config.$option >/dev/null; then + uci get upnpd.config.$option | grep -q -E -x "0|off|false|no|disabled" && uci set upnpd.config.$option="0" + uci get upnpd.config.$option | grep -q -E -x "1|on|true|yes|enabled" && uci set upnpd.config.$option="1" + fi +done + +# Migrate enable_upnp/enable_natpmp -> enabled_protocols: Combined option +if uci -q get upnpd.config.enable_upnp >/dev/null || uci -q get upnpd.config.enable_natpmp >/dev/null; then + log "enable_upnp/enable_natpmp -> enabled_protocols: Combined option" + if ! uci -q get upnpd.config.enable_upnp | grep -q -E -x "0|off|false|no|disabled"; then + uci -q get upnpd.config.enable_natpmp | grep -q -E -x "0|off|false|no|disabled" && + uci set upnpd.config.enabled_protocols="upnp-igd" || + uci set upnpd.config.enabled_protocols="all" + elif ! uci -q get upnpd.config.enable_natpmp | grep -q -E -x "0|off|false|no|disabled"; then + uci set upnpd.config.enabled_protocols="pcp+nat-pmp" + else + uci set upnpd.config.enabled_protocols="all" + uci set upnpd.config.enabled="0" + fi + uci -q delete upnpd.config.enable_upnp + uci -q delete upnpd.config.enable_natpmp +fi + +# Rename use_stun -> allow_cgnat +if uci -q get upnpd.config.use_stun >/dev/null; then + log "use_stun -> allow_cgnat" + uci rename upnpd.config.use_stun="allow_cgnat" +fi + +# Migrate force_forwarding=1 -> allow_cgnat=allow-filtered: +# Option from X-Wrt (since 2021) gets migrated to new similar daemon option for cross-project upgrades +if uci -q get upnpd.config.force_forwarding >/dev/null; then + log "force_forwarding=1 -> allow_cgnat=allow-filtered: Migrate to new daemon option" + uci get upnpd.config.force_forwarding | grep -q -E -x "1|on|true|yes|enabled" && + uci set upnpd.config.allow_cgnat="allow-filtered" + uci delete upnpd.config.force_forwarding +fi + +# Remove known incompatible (not CGNAT filtering test capable) STUN servers and include stun_port in stun_host +if uci -q get upnpd.config.stun_host | grep -q -E "stun[0-9]?.l.google.com|stun.cloudflare.com"; then + log "stun_host: Incompatible STUN server ($(uci -q get upnpd.config.stun_host)) found, remove to set default" + uci delete upnpd.config.stun_host + uci -q delete upnpd.config.stun_port +elif uci -q get upnpd.config.stun_port >/dev/null; then + uci -q get upnpd.config.stun_host >/dev/null && [ "$(uci -q get upnpd.config.stun_port)" != "3478" ] && + log "stun_port: Include stun_port in stun_host, and remove option" && + uci set upnpd.config.stun_host="$(uci -q get upnpd.config.stun_host | cut -d ":" -f 1):$(uci -q get upnpd.config.stun_port)" + uci delete upnpd.config.stun_port +fi + +# Migrate secure_mode=1/0 -> allow_third_party_mapping=0/upnp-igd: Invert/extend to PCP +if uci -q get upnpd.config.secure_mode >/dev/null; then + log "secure_mode=1/0 -> allow_third_party_mapping=0/upnp-igd: Invert/extend to PCP" + uci get upnpd.config.secure_mode | grep -q -E -x "0|off|false|no|disabled" && + uci set upnpd.config.allow_third_party_mapping="upnp-igd" || + uci set upnpd.config.allow_third_party_mapping="0" + uci delete upnpd.config.secure_mode +fi + +# Migrate log_output=1/0 -> log_output=debug/default: Now info also allowed +if uci -q get upnpd.config.log_output >/dev/null; then + uci get upnpd.config.log_output | grep -q -E -x "1|on|true|yes|enabled" && + log "log_output=1 -> log_output=debug: Now info also allowed" && + uci set upnpd.config.log_output="debug" + uci get upnpd.config.log_output | grep -q -E -x "0|off|false|no|disabled" && + uci set upnpd.config.log_output="default" +fi + +# Rename upnp_lease_file -> lease_file: To original daemon name, and remove if UCI default +if uci -q get upnpd.config.upnp_lease_file >/dev/null; then + if [ "$(uci -q get upnpd.config.upnp_lease_file)" = "/var/run/miniupnpd.leases" ]; then + log "upnp_lease_file -> lease_file: Remove option as UCI default is set" + uci delete upnpd.config.upnp_lease_file + else + log "upnp_lease_file -> lease_file" + uci rename upnpd.config.upnp_lease_file="lease_file" + fi +fi + +# Migrate igdv1=1/0 -> upnp_igd_compat=igdv1/igdv2: Extensible/clearer +if uci -q get upnpd.config.igdv1 >/dev/null; then + log "igdv1=1/0 -> upnp_igd_compat=igdv1/igdv2" + uci get upnpd.config.igdv1 | grep -q -E -x "1|on|true|yes|enabled" && + uci set upnpd.config.upnp_igd_compat="igdv1" || + uci set upnpd.config.upnp_igd_compat="igdv2" + uci delete upnpd.config.igdv1 +fi + +# Migrate download/upload -> download_kbps/upload_kbps: Convert to kbit/s +if uci -q get upnpd.config.download >/dev/null; then + download="$(uci -q get upnpd.config.download)" + if [ "$download" != "1024" ] && [ "$download" -ge "1" ] 2>/dev/null; then + log "download -> download_kbps: Convert to kbit/s" + download_kbps="$((download * 8 * 1000 / 1024))" + uci set upnpd.config.download_kbps="$download_kbps" + fi + uci delete upnpd.config.download +fi +if uci -q get upnpd.config.upload >/dev/null; then + upload="$(uci -q get upnpd.config.upload)" + if [ "$upload" != "512" ] && [ "$upload" -ge "1" ] 2>/dev/null; then + log "upload -> upload_kbps: Convert to kbit/s" + upload_kbps="$((upload * 8 * 1000 / 1024))" + uci set upnpd.config.upload_kbps="$upload_kbps" + fi + uci delete upnpd.config.upload +fi + +# Rename port -> http_port: Remove if UCI default +if uci -q get upnpd.config.port >/dev/null; then + if [ "$(uci -q get upnpd.config.port)" = "5000" ]; then + log "port -> http_port: Remove option as UCI default is set" + uci delete upnpd.config.port + else + log "port -> http_port" + uci rename upnpd.config.port="http_port" + fi +fi + +# Migrate notify_interval <=900s: Remove to set minimum of 900 (default) +if [ "$(uci -q get upnpd.config.notify_interval)" -le "900" ] 2>/dev/null; then + log "notify_interval <=900s: Remove to set minimum of 900 (default)" + uci delete upnpd.config.notify_interval +fi + +# Migrate internal_iface option to new internal_network section +if ! uci -q get upnpd.@internal_network[0] >/dev/null; then + ifnr=0 + for interface in $(uci -q get upnpd.config.internal_iface || echo lan); do + log "Create new internal_network section for $interface" + uci add upnpd internal_network >/dev/null + uci set upnpd.@internal_network[$ifnr].interface="$interface" + ifnr=$((ifnr + 1)) + done + uci -q delete upnpd.config.internal_iface +fi + +uci commit upnpd >/dev/null + +exit 0 diff --git a/net/miniupnpd/files/upnpd.config b/net/miniupnpd/files/upnpd.config index bd7c3ec4007dce..2af50e8e9f36f8 100644 --- a/net/miniupnpd/files/upnpd.config +++ b/net/miniupnpd/files/upnpd.config @@ -1,28 +1,36 @@ -config upnpd config - option enabled 0 - option enable_natpmp 1 - option enable_upnp 1 - option secure_mode 1 - option log_output 0 - option download 1024 - option upload 512 -#by default, looked up dynamically from ubus -# option external_iface wan - option internal_iface lan - option port 5000 - option upnp_lease_file /var/run/miniupnpd.leases - option igdv1 1 +# UPnP IGD & PCP/NAT-PMP Service Settings (v2.0) + +config upnpd 'config' + option enabled '0' + # Can be set to all/upnp-igd/pcp+nat-pmp + option enabled_protocols 'all' + option allow_third_party_mapping '0' + # Extra logging by setting to info or debug (previously 1) + option log_output 'default' + option upnp_igd_compat 'igdv1' + +# Enabled Networks / Access Control +# To check the custom ACL, set access_preset to 0 or custom_acl_before to 1 + +config internal_network + option interface 'lan' + option access_preset 'accept-high-ports' + #option accept_ports + #option reject_ports '21 23 135 137-139 445 3389' + +# Custom Access Control List +# Empty list is rejected alone, IPv6 is always accepted unless disabled config perm_rule - option action allow - option ext_ports 1024-65535 - option int_addr 0.0.0.0/0 # Does not override secure_mode - option int_ports 1024-65535 - option comment "Allow high ports" + option comment 'Allow high ports' + option int_addr '0.0.0.0/0' + option int_ports '1024-65535' + option ext_ports '1024-65535' + option action 'allow' config perm_rule - option action deny - option ext_ports 0-65535 - option int_addr 0.0.0.0/0 - option int_ports 0-65535 - option comment "Default deny" + option comment 'Default deny' + option int_addr '0.0.0.0/0' + option int_ports '1-65535' + option ext_ports '1-65535' + option action 'deny' From 4c867eac9e8aaed73ee720d34e451c3295a0cfbd Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 05/10] miniupnpd: group/rearrange config-gen and refactoring - Group and rearrange UCI option declaration and config-gen by function/LuCI UI, and comment - Encode required XML entities of text UPnP IGD config options until the daemon does so using the created function `xml_encode` - Only generate UPnP IGD config if the protocol is enabled (to merge with prior) Signed-off-by: Self-Hosting-Group --- net/miniupnpd/files/miniupnpd.init | 125 +++++++++++++++-------------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/net/miniupnpd/files/miniupnpd.init b/net/miniupnpd/files/miniupnpd.init index 44afdacd0c5303..c9091209d18253 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -29,7 +29,6 @@ conf_rule_add() { local cfg="$1" local action int_addr local ext_start ext_end int_start int_end comment - config_get action "$cfg" action "deny" # allow or deny upnpd_get_port_range "ext" "$cfg" ext_ports "0-65535" # external ports: x, x-y, x:y config_get int_addr "$cfg" int_addr "0.0.0.0/0" # ip or network and subnet mask (internal) @@ -44,45 +43,45 @@ conf_rule_add() { upnpd() { config_load "upnpd" - local external_iface external_iface6 external_zone external_ip - local upload_kbps download_kbps log_output http_port config_file serial_number model_number - local allow_cgnat stun_host uuid notify_interval presentation_url - local lease_file ipv6_disable - local enabled_protocols allow_third_party_mapping system_uptime upnp_igd_compat - local friendly_name - local enabled config_get enabled config enabled 0 if [ "$enabled" != "1" ]; then log "Service disabled, UCI enabled is not set" return 1 fi - config_get external_iface config external_iface - config_get external_iface6 config external_iface6 - config_get external_zone config external_zone - config_get external_ip config external_ip - config_get http_port config http_port 5000 - config_get upload_kbps config upload_kbps - config_get download_kbps config download_kbps - config_get log_output config log_output - config_get config_file config config_file - config_get serial_number config serial_number - config_get model_number config model_number - config_get uuid config uuid + # Daemon + local enabled_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime log_output lease_file config_file + config_get enabled_protocols config enabled_protocols all config_get allow_cgnat config allow_cgnat 0 config_get stun_host config stun_host stun.nextcloud.com - config_get notify_interval config notify_interval - config_get presentation_url config presentation_url - config_get lease_file config lease_file /run/miniupnpd.leases - config_get ipv6_disable config ipv6_disable 0 - config_get enabled_protocols config enabled_protocols all config_get allow_third_party_mapping config allow_third_party_mapping 0 + config_get ipv6_disable config ipv6_disable 0 config_get system_uptime config system_uptime 1 + config_get log_output config log_output + config_get lease_file config lease_file /run/miniupnpd.leases + config_get config_file config config_file + + # UPnP IGD + local upnp_igd_compat download_kbps upload_kbps friendly_name model_number serial_number presentation_url uuid http_port notify_interval config_get upnp_igd_compat config upnp_igd_compat igdv1 + config_get download_kbps config download_kbps + config_get upload_kbps config upload_kbps config_get friendly_name config friendly_name "OpenWrt UPnP IGD & PCP" + config_get model_number config model_number + config_get serial_number config serial_number + config_get presentation_url config presentation_url + config_get uuid config uuid + config_get http_port config http_port 5000 + config_get notify_interval config notify_interval - local conf ifname ifname6 + # External network interface + local external_iface external_iface6 external_zone external_ip + config_get external_iface config external_iface + config_get external_iface6 config external_iface6 + config_get external_zone config external_zone + config_get external_ip config external_ip + local conf ifname ifname6 . /lib/functions/network.sh if [ -n "$external_iface" ] ; then @@ -120,26 +119,17 @@ upnpd() { log "No internal networks configured, not starting" daemon.err return 1 fi - { - echo "ext_ifname=$ifname" - echo "ext_ifname6=$ifname6" - [ -n "$external_ip" ] && echo "ext_ip=$external_ip" - - [ "$enabled_protocols" = "all" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=yes" - [ "$enabled_protocols" = "upnp-igd" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=no" - [ "$enabled_protocols" = "pcp+nat-pmp" ] && echo "enable_upnp=no" && echo "enable_pcp_pmp=yes" - [ "$allow_third_party_mapping" = "0" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=no" - [ "$allow_third_party_mapping" = "1" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=yes" - [ "$allow_third_party_mapping" = "upnp-igd" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=no" - [ "$allow_third_party_mapping" = "pcp" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=yes" - [ "$system_uptime" = "0" ] && echo "system_uptime=no" || echo "system_uptime=yes" - [ "$upnp_igd_compat" = "igdv1" ] && echo "force_igd_desc_v1=yes" || echo "force_igd_desc_v1=no" # Only perform an STUN CGNAT test if necessary, with a private/CGNAT external IPv4 local extipv4 extipv4private network_get_ipaddr extipv4 "$external_iface" case "$extipv4" in 10.* | 172.1[6-9].* | 172.2[0-9].* | 172.3[0-1].* | 192.168.* | 100.6[4-9].* | 100.[7-9][0-9].* | 100.1[0-1][0-9].* | 100.12[0-7].*) extipv4private=1 ;; esac + { + echo "# Daemon" + [ "$enabled_protocols" = "all" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=yes" + [ "$enabled_protocols" = "upnp-igd" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=no" + [ "$enabled_protocols" = "pcp+nat-pmp" ] && echo "enable_upnp=no" && echo "enable_pcp_pmp=yes" if [ "$extipv4private" = "1" ] && [ "$allow_cgnat" != "0" ] && [ "$external_ip" = "" ]; then [ "$allow_cgnat" = "1" ] && echo "ext_perform_stun=yes" [ "$allow_cgnat" = "allow-filtered" ] && echo "ext_perform_stun=allow-filtered" @@ -148,31 +138,37 @@ upnpd() { echo "ext_stun_host=${stun_host%%:*}" [ "${stun_host%%:*}" != "${stun_host##*:}" ] && echo "ext_stun_port=${stun_host##*:}" fi + [ "$allow_third_party_mapping" = "0" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=no" + [ "$allow_third_party_mapping" = "1" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=yes" + [ "$allow_third_party_mapping" = "upnp-igd" ] && echo "secure_mode=no" && echo "pcp_allow_thirdparty=no" + [ "$allow_third_party_mapping" = "pcp" ] && echo "secure_mode=yes" && echo "pcp_allow_thirdparty=yes" [ "$ipv6_disable" = "0" ] && echo "ipv6_disable=no" || echo "ipv6_disable=yes" - - [ -n "$download_kbps" ] && echo "bitrate_down=$((download_kbps * 1000))" - [ -n "$upload_kbps" ] && echo "bitrate_up=$((upload_kbps * 1000))" - + [ "$system_uptime" = "0" ] && echo "system_uptime=no" || echo "system_uptime=yes" touch "$lease_file" && echo "lease_file=$lease_file" [ "$ipv6_disable" = "0" ] && touch "${lease_file}-ipv6" && echo "lease_file6=${lease_file}-ipv6" - [ -n "$friendly_name" ] && echo "friendly_name=$friendly_name" - [ -n "$presentation_url" ] && echo "presentation_url=$presentation_url" - [ -n "$notify_interval" ] && echo "notify_interval=$notify_interval" - echo "serial=$serial_number" - echo "model_number=$model_number" - echo "http_port=$http_port" - - [ -z "$uuid" ] && { - log "Generate UPnP IGD UUID" - uuid="$(cat /proc/sys/kernel/random/uuid)" - uci set upnpd.config.uuid="$uuid" - uci commit upnpd - } - [ "$uuid" != "nocli" ] && echo "uuid=$uuid" || log "Deprecated. Set uuid to 00000000-0000-0000-0000-000000000000 instead" + if [ "$enabled_protocols" = "upnp-igd" ] || [ "$enabled_protocols" = "all" ]; then + echo "# UPnP IGD" + [ "$upnp_igd_compat" = "igdv1" ] && echo "force_igd_desc_v1=yes" || echo "force_igd_desc_v1=no" + [ -n "$download_kbps" ] && echo "bitrate_down=$((download_kbps * 1000))" + [ -n "$upload_kbps" ] && echo "bitrate_up=$((upload_kbps * 1000))" + [ -n "$friendly_name" ] && echo "friendly_name=$(xml_encode "$friendly_name")" + [ -n "$model_number" ] && echo "model_number=$(xml_encode "$model_number")" || echo "model_number=" + [ -n "$serial_number" ] && echo "serial=$(xml_encode "$serial_number")" || echo "serial=" + [ -n "$presentation_url" ] && echo "presentation_url=$presentation_url" + [ -z "$uuid" ] && { + log "Generate UPnP IGD UUID" + uuid="$(cat /proc/sys/kernel/random/uuid)" + uci set upnpd.config.uuid="$uuid" + uci commit upnpd + } + [ "$uuid" != "nocli" ] && echo "uuid=$uuid" || log "Deprecated. Set uuid to 00000000-0000-0000-0000-000000000000 instead" + echo "http_port=$http_port" + [ -n "$notify_interval" ] && echo "notify_interval=$notify_interval" + fi if [ "$FW" = "fw4" ]; then - #When using nftables configure miniupnpd to use its own table and chains + echo "# Firewall backend" echo "upnp_table_name=fw4" echo "upnp_nat_table_name=fw4" echo "upnp_forward_chain=upnp_forward" @@ -180,6 +176,11 @@ upnpd() { echo "upnp_nat_postrouting_chain=upnp_postrouting" fi + echo "# External network interface" + echo "ext_ifname=$ifname" + echo "ext_ifname6=$ifname6" + [ -n "$external_ip" ] && echo "ext_ip=$external_ip" + # Enabled internal networks / access control config_foreach upnpd_add_int_network_and_preset internal_network precustom echo "# Custom ACL" @@ -190,7 +191,6 @@ upnpd() { fi if [ -n "$ifname" ]; then - # start firewall if [ "$FW" = "fw4" ]; then nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload else @@ -233,6 +233,11 @@ log() { logger -s -p "${2:-daemon.notice}" -t "upnpd" "$1" || echo "upnpd: $1" >&2 } +xml_encode() { + # Encode required XML entities of text UPnP IGD config options until the daemon does so + echo "$1" | sed "s/&/\&/g; s//\>/g" +} + upnpd_add_int_network_and_preset() { local cfg="$1" local interface access_preset accept_ports reject_ports custom_acl_before From db8068aa5af2de067049ce7272553f21e6ac5c4a Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 06/10] miniupnpd: rename UCI section name to `settings` (v2.0) Inspired/address copilot's PR review for a clearer config by rename UCI section name `config` (v1.0) -> `settings` (v2.0), helps on migration and to distinguish the updated config from the previous one easily (to merge with prior) Signed-off-by: Self-Hosting-Group --- net/miniupnpd/files/firewall3.include | 4 +- net/miniupnpd/files/miniupnpd.hotplug | 6 +-- net/miniupnpd/files/miniupnpd.init | 50 +++++++++---------- .../files/upnpd-migration.uci-defaults | 7 +++ net/miniupnpd/files/upnpd.config | 2 +- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/net/miniupnpd/files/firewall3.include b/net/miniupnpd/files/firewall3.include index 4fd483974306c1..53ca8dc716cc2f 100644 --- a/net/miniupnpd/files/firewall3.include +++ b/net/miniupnpd/files/firewall3.include @@ -48,11 +48,11 @@ add_extzone_rules() { # By default, user configuration is king. -for ext_iface in $(uci -q get upnpd.config.external_iface); do +for ext_iface in $(uci -q get upnpd.settings.external_iface); do add_extzone_rules $(fw3 -q network "$ext_iface") done -add_extzone_rules $(uci -q get upnpd.config.external_zone) +add_extzone_rules $(uci -q get upnpd.settings.external_zone) [ "$ADDED" -ne 0 ] && exit 0 diff --git a/net/miniupnpd/files/miniupnpd.hotplug b/net/miniupnpd/files/miniupnpd.hotplug index 607a32bdc6885b..dfd3f41c26f71a 100644 --- a/net/miniupnpd/files/miniupnpd.hotplug +++ b/net/miniupnpd/files/miniupnpd.hotplug @@ -10,9 +10,9 @@ [ "$ACTION" != "ifup" ] && /etc/init.d/miniupnpd running && exit 0 tmpconf="/var/etc/miniupnpd.conf" -external_iface=$(uci -q get upnpd.config.external_iface) -external_iface6=$(uci -q get upnpd.config.external_iface6) -external_zone=$(uci -q get upnpd.config.external_zone) +external_iface=$(uci -q get upnpd.settings.external_iface) +external_iface6=$(uci -q get upnpd.settings.external_iface6) +external_zone=$(uci -q get upnpd.settings.external_zone) [ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3" . /lib/functions/network.sh diff --git a/net/miniupnpd/files/miniupnpd.init b/net/miniupnpd/files/miniupnpd.init index c9091209d18253..935ef8604b8ac1 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -44,42 +44,42 @@ conf_rule_add() { upnpd() { config_load "upnpd" local enabled - config_get enabled config enabled 0 + config_get enabled settings enabled 0 if [ "$enabled" != "1" ]; then log "Service disabled, UCI enabled is not set" return 1 fi # Daemon local enabled_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime log_output lease_file config_file - config_get enabled_protocols config enabled_protocols all - config_get allow_cgnat config allow_cgnat 0 - config_get stun_host config stun_host stun.nextcloud.com - config_get allow_third_party_mapping config allow_third_party_mapping 0 - config_get ipv6_disable config ipv6_disable 0 - config_get system_uptime config system_uptime 1 - config_get log_output config log_output - config_get lease_file config lease_file /run/miniupnpd.leases - config_get config_file config config_file + config_get enabled_protocols settings enabled_protocols all + config_get allow_cgnat settings allow_cgnat 0 + config_get stun_host settings stun_host stun.nextcloud.com + config_get allow_third_party_mapping settings allow_third_party_mapping 0 + config_get ipv6_disable settings ipv6_disable 0 + config_get system_uptime settings system_uptime 1 + config_get log_output settings log_output + config_get lease_file settings lease_file /run/miniupnpd.leases + config_get config_file settings config_file # UPnP IGD local upnp_igd_compat download_kbps upload_kbps friendly_name model_number serial_number presentation_url uuid http_port notify_interval - config_get upnp_igd_compat config upnp_igd_compat igdv1 - config_get download_kbps config download_kbps - config_get upload_kbps config upload_kbps - config_get friendly_name config friendly_name "OpenWrt UPnP IGD & PCP" - config_get model_number config model_number - config_get serial_number config serial_number - config_get presentation_url config presentation_url - config_get uuid config uuid - config_get http_port config http_port 5000 - config_get notify_interval config notify_interval + config_get upnp_igd_compat settings upnp_igd_compat igdv1 + config_get download_kbps settings download_kbps + config_get upload_kbps settings upload_kbps + config_get friendly_name settings friendly_name "OpenWrt UPnP IGD & PCP" + config_get model_number settings model_number + config_get serial_number settings serial_number + config_get presentation_url settings presentation_url + config_get uuid settings uuid + config_get http_port settings http_port 5000 + config_get notify_interval settings notify_interval # External network interface local external_iface external_iface6 external_zone external_ip - config_get external_iface config external_iface - config_get external_iface6 config external_iface6 - config_get external_zone config external_zone - config_get external_ip config external_ip + config_get external_iface settings external_iface + config_get external_iface6 settings external_iface6 + config_get external_zone settings external_zone + config_get external_ip settings external_ip local conf ifname ifname6 . /lib/functions/network.sh @@ -159,7 +159,7 @@ upnpd() { [ -z "$uuid" ] && { log "Generate UPnP IGD UUID" uuid="$(cat /proc/sys/kernel/random/uuid)" - uci set upnpd.config.uuid="$uuid" + uci set upnpd.settings.uuid="$uuid" uci commit upnpd } [ "$uuid" != "nocli" ] && echo "uuid=$uuid" || log "Deprecated. Set uuid to 00000000-0000-0000-0000-000000000000 instead" diff --git a/net/miniupnpd/files/upnpd-migration.uci-defaults b/net/miniupnpd/files/upnpd-migration.uci-defaults index 6904b42fe82268..649022d1bad1e3 100644 --- a/net/miniupnpd/files/upnpd-migration.uci-defaults +++ b/net/miniupnpd/files/upnpd-migration.uci-defaults @@ -4,6 +4,7 @@ log() { logger -s -p "${2:-daemon.notice}" -t "upnpd" "$1" || echo "upnpd: $1" >&2 } +{ uci -q get upnpd.settings >/dev/null || ! uci -q get upnpd.config >/dev/null; } && exit 0 log "Check UCI options in /etc/config/upnpd to be migrated to v2.0" # Set missing enabled option to fix previously different defaults in LuCI/config (0) and init UCI (1) @@ -150,6 +151,12 @@ if ! uci -q get upnpd.@internal_network[0] >/dev/null; then uci -q delete upnpd.config.internal_iface fi +# Rename section config -> settings (v2.0) +if uci -q get upnpd.config >/dev/null; then + log "Rename section config -> settings (v2.0)" && uci rename upnpd.config="settings" || + log "Error renaming the UCI section" daemon.err +fi + uci commit upnpd >/dev/null exit 0 diff --git a/net/miniupnpd/files/upnpd.config b/net/miniupnpd/files/upnpd.config index 2af50e8e9f36f8..6467f9b259d2aa 100644 --- a/net/miniupnpd/files/upnpd.config +++ b/net/miniupnpd/files/upnpd.config @@ -1,6 +1,6 @@ # UPnP IGD & PCP/NAT-PMP Service Settings (v2.0) -config upnpd 'config' +config upnpd 'settings' option enabled '0' # Can be set to all/upnp-igd/pcp+nat-pmp option enabled_protocols 'all' From 90bd31de7efd37a54ed0aa849b01af7ca719ced3 Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 07/10] miniupnpd: add second CGNAT UCI option Alternative option to STUN allow-filtered. As requested by AquanJSW, to test with Tailscale. Also adds the required daemon fix. No public IPv4 address detection; issues with multiple clients, e.g. PCP/NAT-PMP (proposed for inclusion, to merge with prior) Signed-off-by: Self-Hosting-Group --- net/miniupnpd/files/miniupnpd.init | 2 +- .../patches/030-allow-private-fix.patch | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 net/miniupnpd/patches/030-allow-private-fix.patch diff --git a/net/miniupnpd/files/miniupnpd.init b/net/miniupnpd/files/miniupnpd.init index 935ef8604b8ac1..8b53f280734652 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -134,7 +134,7 @@ upnpd() { [ "$allow_cgnat" = "1" ] && echo "ext_perform_stun=yes" [ "$allow_cgnat" = "allow-filtered" ] && echo "ext_perform_stun=allow-filtered" # Alternatively, as allow-filtered detects the public IPv4 required by multiple clients, e.g. PCP/NAT-PMP - # [ "$allow_cgnat" = "allow-private-ext-ipv4" ] && external_ip=203.1.2.3 + [ "$allow_cgnat" = "allow-private-ext-ipv4" ] && echo "ext_allow_private_ipv4=yes" echo "ext_stun_host=${stun_host%%:*}" [ "${stun_host%%:*}" != "${stun_host##*:}" ] && echo "ext_stun_port=${stun_host##*:}" fi diff --git a/net/miniupnpd/patches/030-allow-private-fix.patch b/net/miniupnpd/patches/030-allow-private-fix.patch new file mode 100644 index 00000000000000..b8a42b2b567f2c --- /dev/null +++ b/net/miniupnpd/patches/030-allow-private-fix.patch @@ -0,0 +1,18 @@ +From 9ff3c71bfb6c7f7bf525d6bda447387de7257aff Mon Sep 17 00:00:00 2001 +From: Vlad Starodubtsev +Date: Wed, 6 Aug 2025 19:04:18 +0300 +Subject: [PATCH] miniupnpd: fix ALLOWPRIVATEIPV4MASK condition + +Fixed usage of private/reserved WAN addresses if they are allowed in the configuration. + +--- a/upnpsoap.c ++++ b/upnpsoap.c +@@ -370,7 +370,7 @@ GetExternalIPAddress(struct upnphttp * h + ext_if_name); + ext_ip_addr[0] = '\0'; + } else if (addr_is_reserved(&addr)) { +- if (!GETFLAG(ALLOWPRIVATEIPV4MASK)) { ++ 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); From 184fa9bdbf95edd181adec9eef21983b4857b37a Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 08/10] miniupnpd: update custom ACL options, migrate section - Note that the custom ACL is now rejected by default, if it is alone used, with no preset or listed accepted ports. Add (ignored) custom ACL template entries on migration - Migrate custom ACL entries to the new section name `acl_entry` - The following custom ACL UCI options been added or changed, and the previous options are migrated on updating: acl_entry UCI options | Change | Previous name ----------------------------|----------------------------|-------------- action | New/updated values (1) | int_port | Remove colon separator (2) | int_ports ext_port | Remove colon separator (2) | ext_ports descr_filter | New option (3) | 1. Allow ignore, and update action option to use the nftables terms (allow/deny -> accept/reject). To avoid adding inverted actions when changing via LuCI, ensure any missing are set, as LuCI and UCI had not matching action defaults. Missing actions are now ignored/logged 2. Ensure that the hyphen (-) is only used as a port range separator by migration, as the colon (:) is not valid in LuCI 3. Add missing UCI option to set a regular expression to check for a UPnP IGD IPv4 port map description, and fix the current collision with the comment field which was not noticed due to a daemon bug https://redirect.github.com/openwrt/packages/pull/24495 https://redirect.github.com/miniupnp/miniupnp/pull/853 Code refactoring: - Add a more universal usable `is_port_or_range` function instead of `upnpd_get_port_range` and check if it has a valid range, and removes a shellcheck warning - Rename `conf_rule_add` function to `upnpd_add_custom_acl_entry` (to merge with prior) Signed-off-by: Self-Hosting-Group --- net/miniupnpd/files/miniupnpd.init | 81 +++++++----------- .../files/upnpd-migration.uci-defaults | 82 +++++++++++++++++++ net/miniupnpd/files/upnpd.config | 21 ++--- 3 files changed, 122 insertions(+), 62 deletions(-) diff --git a/net/miniupnpd/files/miniupnpd.init b/net/miniupnpd/files/miniupnpd.init index 8b53f280734652..4c1d9507adb556 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -7,38 +7,34 @@ USE_PROCD=1 PROG=/usr/sbin/miniupnpd [ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3" -upnpd_get_port_range() { - local var="$1"; shift - local val - - config_get val "$@" - - case "$val" in - [0-9]*[:-][0-9]*) - export -n -- "${var}_start=${val%%[:-]*}" - export -n -- "${var}_end=${val##*[:-]}" - ;; - [0-9]*) - export -n -- "${var}_start=$val" - export -n -- "${var}_end=" - ;; - esac +is_port_or_range() { + [ "$1" = "0" ] && return 1 + [ "$1" -ge "1" ] 2>/dev/null && [ "$1" -le "65535" ] 2>/dev/null && return 0 + [ "$2" = "port0inrange" ] && local minport=0 || local minport=1 + [ "${1%%-*}" -ge "$minport" ] 2>/dev/null && [ "${1%%-*}" -le "65535" ] 2>/dev/null && + [ "${1##*-}" -ge "$minport" ] 2>/dev/null && [ "${1##*-}" -le "65535" ] 2>/dev/null && + [ "${1##*-}" -ge "${1%%-*}" ] 2>/dev/null && return 0 || return 1 } -conf_rule_add() { +upnpd_add_custom_acl_entry() { local cfg="$1" - local action int_addr - local ext_start ext_end int_start int_end comment - config_get action "$cfg" action "deny" # allow or deny - upnpd_get_port_range "ext" "$cfg" ext_ports "0-65535" # external ports: x, x-y, x:y - config_get int_addr "$cfg" int_addr "0.0.0.0/0" # ip or network and subnet mask (internal) - upnpd_get_port_range "int" "$cfg" int_ports "0-65535" # internal ports: x, x-y, x:y or range - config_get comment "$cfg" comment "ACL" # comment - - # Make a single IP IP/32 so that miniupnpd.conf can use it. - [ "${int_addr%/*}" = "$int_addr" ] && int_addr="$int_addr/32" - - echo "$action $ext_start${ext_end:+-}$ext_end $int_addr $int_start${int_end:+-}$int_end #$comment" + local comment int_addr int_port ext_port descr_filter action + config_get comment "$cfg" comment "unspecified" # comment + config_get int_addr "$cfg" int_addr "0.0.0.0/0" # IPv4 or network and subnet mask (internal) + config_get int_port "$cfg" int_port "1-65535" # internal port/range: x or x-y + config_get ext_port "$cfg" ext_port "1-65535" # external port/range: x or x-y + config_get descr_filter "$cfg" descr_filter # description regex filter (must be built in) + config_get action "$cfg" action # accept/reject/ignore + ! is_port_or_range "$int_port" port0inrange && + log "Custom ACL: Entry with invalid port or port range ($int_port) in int_port ignored" daemon.warn && int_port=1-65535 + ! is_port_or_range "$ext_port" port0inrange && + log "Custom ACL: Entry with invalid port or port range ($ext_port) in ext_port ignored" daemon.warn && ext_port=1-65535 + [ "$descr_filter" != "" ] && descr_filter=" \"$descr_filter\"" + [ "$action" = "accept" ] && action=allow + [ "$action" = "reject" ] && action=deny + [ "$action" = "ignore" ] && return 0 + [ "$action" = "" ] && log "Custom ACL: Entry with no action ignored" daemon.warn && return 0 + echo "$action $ext_port $int_addr $int_port${descr_filter} # $comment" } upnpd() { @@ -184,8 +180,9 @@ upnpd() { # Enabled internal networks / access control config_foreach upnpd_add_int_network_and_preset internal_network precustom echo "# Custom ACL" - config_foreach conf_rule_add perm_rule + config_foreach upnpd_add_custom_acl_entry acl_entry config_foreach upnpd_add_int_network_and_preset internal_network postcustom + echo "deny 1-65535 0.0.0.0/0 1-65535 # Reject ACL by default" } > "$tmpconf" fi @@ -256,18 +253,8 @@ upnpd_add_int_network_and_preset() { [ "$subnet" = "" ] && log "Cannot get IPv4 subnet for network $interface, access_preset ignored" daemon.warn && return 0 if [ "$2" = "precustom" ]; then for rejectport in $reject_ports; do - if [ "$rejectport" -ge "1" ] 2>/dev/null && [ "$rejectport" -le "65535" ] 2>/dev/null || - { - [ "${rejectport%%-*}" -ge "1" ] 2>/dev/null && - [ "${rejectport%%-*}" -le "65535" ] 2>/dev/null && - [ "${rejectport##*-}" -ge "1" ] 2>/dev/null && - [ "${rejectport##*-}" -le "65535" ] 2>/dev/null && - [ "${rejectport##*-}" -ge "${rejectport%%-*}" ] 2>/dev/null - }; then - echo "deny $rejectport $subnet $rejectport # Reject port $rejectport on $interface" - else + is_port_or_range "$rejectport" && echo "deny $rejectport $subnet $rejectport # Reject port $rejectport on $interface" || log "Invalid port or port range ($rejectport) in reject_ports ignored" daemon.warn - fi done fi if { [ "$2" = "postcustom" ] && [ "$custom_acl_before" = "1" ]; } || @@ -284,18 +271,8 @@ upnpd_add_int_network_and_preset() { log "Invalid access_preset ($access_preset) ignored" daemon.warn fi for acceptport in $acceptpresetports $accept_ports; do - if [ "$acceptport" -ge "1" ] 2>/dev/null && [ "$acceptport" -le "65535" ] 2>/dev/null || - { - [ "${acceptport%%-*}" -ge "1" ] 2>/dev/null && - [ "${acceptport%%-*}" -le "65535" ] 2>/dev/null && - [ "${acceptport##*-}" -ge "1" ] 2>/dev/null && - [ "${acceptport##*-}" -le "65535" ] 2>/dev/null && - [ "${acceptport##*-}" -ge "${acceptport%%-*}" ] 2>/dev/null - }; then - echo "allow $acceptport $subnet $acceptport # Accept port $acceptport on $interface" - else + is_port_or_range "$acceptport" && echo "allow $acceptport $subnet $acceptport # Accept port $acceptport on $interface" || log "Invalid port or port range ($acceptport) in accept_ports ignored" daemon.warn - fi done fi if [ "$2" = "precustom" ] && [ "$custom_acl_before" != "1" ] && [ "$access_preset" != "0" ]; then diff --git a/net/miniupnpd/files/upnpd-migration.uci-defaults b/net/miniupnpd/files/upnpd-migration.uci-defaults index 649022d1bad1e3..e83aea7a572fea 100644 --- a/net/miniupnpd/files/upnpd-migration.uci-defaults +++ b/net/miniupnpd/files/upnpd-migration.uci-defaults @@ -139,6 +139,87 @@ if [ "$(uci -q get upnpd.config.notify_interval)" -le "900" ] 2>/dev/null; then uci delete upnpd.config.notify_interval fi +# Migrate custom ACL to new section, note that an empty ACL is now rejected alone +# a) Empty/unmodified ACL: Enable appropriate preset, add/update template entries +# b) Modified ACL: +# - Add missing entry action to avoid adding inverted actions when changing via LuCI +# - Update entry action allow/deny -> accept/reject +# - Update entry port options to only use the LuCI (and daemon) supported hyphen (-) as port range separator +# - Not using an preset, add template entries +if uci -q get upnpd.@acl_entry[0] >/dev/null; then + log "Error migrating the custom ACL, as the UCI section already exists" daemon.err +elif ! uci -q get upnpd.@perm_rule[0] >/dev/null; then + log "Empty ACL: Enable preset, add templates, empty ACL now rejected alone" + access_preset=accept-all-ports + addtemplateentries=1 +elif ! uci -q get upnpd.@perm_rule[2] >/dev/null && + [ "$(uci -q get upnpd.@perm_rule[0].int_addr)" = "0.0.0.0/0" ] && + [ "$(uci -q get upnpd.@perm_rule[0].int_ports)" = "1024-65535" ] && + [ "$(uci -q get upnpd.@perm_rule[0].ext_ports)" = "1024-65535" ] && + [ "$(uci -q get upnpd.@perm_rule[0].action)" = "allow" ] && + [ "$(uci -q get upnpd.@perm_rule[1].int_addr)" = "0.0.0.0/0" ] && + [ "$(uci -q get upnpd.@perm_rule[1].int_ports)" = "0-65535" ] && + [ "$(uci -q get upnpd.@perm_rule[1].ext_ports)" = "0-65535" ] && + [ "$(uci -q get upnpd.@perm_rule[1].action)" = "deny" ]; then + log "Unmodified ACL: Enable preset, update templates, empty ACL rejected" + access_preset=accept-high-ports + addtemplateentries=1 + uci delete upnpd.@perm_rule[-1] + uci delete upnpd.@perm_rule[-1] +else + log "Modified ACL: Update ACL entry action/section, empty ACL now rejected" + access_preset=0 + addtemplateentries=1 + entrynr=0 + while uci -q get upnpd.@perm_rule[$entrynr] >/dev/null; do + comment="$(uci -q get upnpd.@perm_rule[$entrynr].comment)" + int_addr="$(uci -q get upnpd.@perm_rule[$entrynr].int_addr)" + int_port="$(uci -q get upnpd.@perm_rule[$entrynr].int_ports)" + ext_port="$(uci -q get upnpd.@perm_rule[$entrynr].ext_ports)" + action="$(uci -q get upnpd.@perm_rule[$entrynr].action)" + echo "$int_port" | grep -q ":" && + log "Modified ACL: Update entry int_port to only use a hyphen (-) as port range separator" && + int_port="$(echo "$int_port" | tr ":" "-")" + echo "$ext_port" | grep -q ":" && + log "Modified ACL: Update entry ext_port to only use a hyphen (-) as port range separator" && + ext_port="$(echo "$ext_port" | tr ":" "-")" + [ "$action" = "" ] && log "Modified ACL: Add missing entry action" && action=reject + [ "$action" = "allow" ] && action=accept + [ "$action" = "deny" ] && action=reject + uci batch >/dev/null <<-EOF + add upnpd acl_entry + set upnpd.@acl_entry[-1].comment="${comment:-unspecified}" + set upnpd.@acl_entry[-1].int_addr="${int_addr:-0.0.0.0/0}" + set upnpd.@acl_entry[-1].int_port="$int_port" + set upnpd.@acl_entry[-1].ext_port="$ext_port" + set upnpd.@acl_entry[-1].action="$action" + EOF + entrynr=$((entrynr + 1)) + done + [ "$int_addr" = "0.0.0.0/0" ] && [ "$int_port" = "0-65535" ] && + [ "$ext_port" = "0-65535" ] && [ "$action" = "reject" ] && + uci delete upnpd.@acl_entry[-1] + while uci -q delete upnpd.@perm_rule[-1]; do :; done +fi +if [ "$addtemplateentries" = "1" ]; then + uci batch >/dev/null <<-EOF + add upnpd acl_entry + add upnpd acl_entry + set upnpd.@acl_entry[-2].comment="High ports" + set upnpd.@acl_entry[-2].int_addr="0.0.0.0/0" + set upnpd.@acl_entry[-2].int_port="1024-65535" + set upnpd.@acl_entry[-2].ext_port="1024-65535" + set upnpd.@acl_entry[-2].action="ignore" + set upnpd.@acl_entry[-1].comment="Low/system ports" + set upnpd.@acl_entry[-1].int_addr="0.0.0.0/0" + set upnpd.@acl_entry[-1].int_port="1-1023" + set upnpd.@acl_entry[-1].ext_port="1-1023" + set upnpd.@acl_entry[-1].action="ignore" + EOF + uci -q get upnpd.@acl_entry[-3] >/dev/null && + uci reorder upnpd.@acl_entry[-2]=0 && uci reorder upnpd.@acl_entry[-1]=1 +fi + # Migrate internal_iface option to new internal_network section if ! uci -q get upnpd.@internal_network[0] >/dev/null; then ifnr=0 @@ -146,6 +227,7 @@ if ! uci -q get upnpd.@internal_network[0] >/dev/null; then log "Create new internal_network section for $interface" uci add upnpd internal_network >/dev/null uci set upnpd.@internal_network[$ifnr].interface="$interface" + [ "$access_preset" != "" ] && uci set upnpd.@internal_network[$ifnr].access_preset="$access_preset" ifnr=$((ifnr + 1)) done uci -q delete upnpd.config.internal_iface diff --git a/net/miniupnpd/files/upnpd.config b/net/miniupnpd/files/upnpd.config index 6467f9b259d2aa..a370042222f537 100644 --- a/net/miniupnpd/files/upnpd.config +++ b/net/miniupnpd/files/upnpd.config @@ -20,17 +20,18 @@ config internal_network # Custom Access Control List # Empty list is rejected alone, IPv6 is always accepted unless disabled +# Action can now be set to accept/reject/ignore -config perm_rule - option comment 'Allow high ports' +config acl_entry + option comment 'High ports' option int_addr '0.0.0.0/0' - option int_ports '1024-65535' - option ext_ports '1024-65535' - option action 'allow' + option int_port '1024-65535' + option ext_port '1024-65535' + option action 'ignore' -config perm_rule - option comment 'Default deny' +config acl_entry + option comment 'Low/system ports' option int_addr '0.0.0.0/0' - option int_ports '1-65535' - option ext_ports '1-65535' - option action 'deny' + option int_port '1-1023' + option ext_port '1-1023' + option action 'ignore' From 53bde2f500d0344dce933aace09c9e5c02cc6062 Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 09/10] miniupnpd: separate service start and config-gen - Remove `config_foreach upnpd "upnpd"` and replace it with regular function call, as init was not designed for a multi-instance setup, as the same `tmpconf` will be used/overwritten, and non-anonymous section - Move code to make the custom vs. config file generation decision earlier, and only perform external interface detection with the second one, and rename function `upnpd` to `upnpd_generate_config` - Replace unnecessary `if` cases with `elif` in init/hotplug - Exit with 1 on errors to get an inactive service status - Do not restart daemon in hotplug when using a custom config file, as then this file will not be regenerated on restarts - Use `procd_add_reload_trigger "firewall"` instead of listening `/etc/config/firewall` (to merge with prior) Signed-off-by: Self-Hosting-Group --- net/miniupnpd/files/miniupnpd.hotplug | 34 +++---- net/miniupnpd/files/miniupnpd.init | 132 ++++++++++++-------------- 2 files changed, 75 insertions(+), 91 deletions(-) diff --git a/net/miniupnpd/files/miniupnpd.hotplug b/net/miniupnpd/files/miniupnpd.hotplug index dfd3f41c26f71a..27b2d0f7369e46 100644 --- a/net/miniupnpd/files/miniupnpd.hotplug +++ b/net/miniupnpd/files/miniupnpd.hotplug @@ -1,13 +1,12 @@ +#!/bin/sh /etc/init.d/miniupnpd enabled || exit 0 -# If miniupnpd is not running: -# - check on _any_ event (event updates may contribute to network_find_wan*) - -# If miniupnpd _is_ running: -# - check only on ifup (otherwise lease updates etc would cause -# miniupnpd state loss) +# If daemon is: +# - not running: check on any event (event updates may contribute to network_find_wan*) +# - running: check only on ifup (otherwise lease updates etc. would cause daemon state loss) [ "$ACTION" != "ifup" ] && /etc/init.d/miniupnpd running && exit 0 +uci -q get upnpd.settings.config_file >/dev/null && exit 0 tmpconf="/var/etc/miniupnpd.conf" external_iface=$(uci -q get upnpd.settings.external_iface) @@ -16,26 +15,19 @@ external_zone=$(uci -q get upnpd.settings.external_zone) [ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3" . /lib/functions/network.sh - -if [ -n "$external_iface" ] ; then +if [ -n "$external_iface" ]; then network_get_device ifname "$external_iface" +elif [ -n "$external_zone" ]; then + ifname=$($FW -q zone "$external_zone" 2>/dev/null | head -1) else - if [ -n "$external_zone" ] ; then - ifname=$($FW -q zone "$external_zone" 2>/dev/null | head -1) - else - network_find_wan external_iface && \ - network_get_device ifname "$external_iface" - fi + network_find_wan external_iface && network_get_device ifname "$external_iface" fi -if [ -n "$external_iface6" ] ; then +if [ -n "$external_iface6" ]; then network_get_device ifname6 "$external_iface6" +elif [ -n "$external_zone" ]; then + ifname6=$($FW -q zone "$external_zone" 2>/dev/null | head -1) else - if [ -n "$external_zone" ] ; then - ifname6=$($FW -q zone "$external_zone" 2>/dev/null | head -1) - else - network_find_wan6 external_iface6 && \ - network_get_device ifname6 "$external_iface6" - fi + network_find_wan6 external_iface6 && network_get_device ifname6 "$external_iface6" fi [ "$DEVICE" != "$ifname" ] && [ "$DEVICE" != "$ifname6" ] && exit 0 diff --git a/net/miniupnpd/files/miniupnpd.init b/net/miniupnpd/files/miniupnpd.init index 4c1d9507adb556..2c525e095812d9 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -37,25 +37,16 @@ upnpd_add_custom_acl_entry() { echo "$action $ext_port $int_addr $int_port${descr_filter} # $comment" } -upnpd() { - config_load "upnpd" - local enabled - config_get enabled settings enabled 0 - if [ "$enabled" != "1" ]; then - log "Service disabled, UCI enabled is not set" - return 1 - fi +upnpd_generate_config() { # Daemon - local enabled_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime log_output lease_file config_file + local enabled_protocols allow_cgnat stun_host allow_third_party_mapping ipv6_disable system_uptime lease_file config_get enabled_protocols settings enabled_protocols all config_get allow_cgnat settings allow_cgnat 0 config_get stun_host settings stun_host stun.nextcloud.com config_get allow_third_party_mapping settings allow_third_party_mapping 0 config_get ipv6_disable settings ipv6_disable 0 config_get system_uptime settings system_uptime 1 - config_get log_output settings log_output config_get lease_file settings lease_file /run/miniupnpd.leases - config_get config_file settings config_file # UPnP IGD local upnp_igd_compat download_kbps upload_kbps friendly_name model_number serial_number presentation_url uuid http_port notify_interval @@ -77,51 +68,39 @@ upnpd() { config_get external_zone settings external_zone config_get external_ip settings external_ip - local conf ifname ifname6 + local ifname ifname6 . /lib/functions/network.sh - - if [ -n "$external_iface" ] ; then + if [ -n "$external_iface" ]; then network_get_device ifname "$external_iface" + elif [ -n "$external_zone" ]; then + ifname=$($FW -q zone "$external_zone" 2>/dev/null | head -1) else - if [ -n "$external_zone" ] ; then - ifname=$($FW -q zone "$external_zone" 2>/dev/null | head -1) - else - network_find_wan external_iface && \ - network_get_device ifname "$external_iface" - fi + network_find_wan external_iface && network_get_device ifname "$external_iface" fi - if [ -n "$external_iface6" ] ; then + if [ -n "$external_iface6" ]; then network_get_device ifname6 "$external_iface6" + elif [ -n "$external_zone" ]; then + ifname6=$($FW -q zone "$external_zone" 2>/dev/null | head -1) else - if [ -n "$external_zone" ] ; then - ifname6=$($FW -q zone "$external_zone" 2>/dev/null | head -1) - else - network_find_wan6 external_iface6 && \ - network_get_device ifname6 "$external_iface6" - fi + network_find_wan6 external_iface6 && network_get_device ifname6 "$external_iface6" fi - if [ -n "$config_file" ]; then - conf="$config_file" - else - local tmpconf="/var/etc/miniupnpd.conf" - conf="$tmpconf" - mkdir -p /var/etc - if [ "$ifname" = "" ]; then - log "No external network interface found, not starting" daemon.err - return 1 - fi - if ! uci -q get upnpd.@internal_network[0].interface >/dev/null; then - log "No internal networks configured, not starting" daemon.err - return 1 - fi - # Only perform an STUN CGNAT test if necessary, with a private/CGNAT external IPv4 - local extipv4 extipv4private - network_get_ipaddr extipv4 "$external_iface" - case "$extipv4" in - 10.* | 172.1[6-9].* | 172.2[0-9].* | 172.3[0-1].* | 192.168.* | 100.6[4-9].* | 100.[7-9][0-9].* | 100.1[0-1][0-9].* | 100.12[0-7].*) extipv4private=1 ;; - esac - { + if [ "$ifname" = "" ]; then + log "No external network interface found, not starting" daemon.err + return 1 + fi + if ! uci -q get upnpd.@internal_network[0].interface >/dev/null; then + log "No internal networks configured, not starting" daemon.err + return 1 + fi + # Only perform an STUN CGNAT test if necessary, with a private/CGNAT external IPv4 + local extipv4 extipv4private + network_get_ipaddr extipv4 "$external_iface" + case "$extipv4" in + 10.* | 172.1[6-9].* | 172.2[0-9].* | 172.3[0-1].* | 192.168.* | 100.6[4-9].* | 100.[7-9][0-9].* | 100.1[0-1][0-9].* | 100.12[0-7].*) extipv4private=1 ;; + esac + + { echo "# Daemon" [ "$enabled_protocols" = "all" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=yes" [ "$enabled_protocols" = "upnp-igd" ] && echo "enable_upnp=yes" && echo "enable_pcp_pmp=no" @@ -184,32 +163,15 @@ upnpd() { config_foreach upnpd_add_int_network_and_preset internal_network postcustom echo "deny 1-65535 0.0.0.0/0 1-65535 # Reject ACL by default" - } > "$tmpconf" - fi - - if [ -n "$ifname" ]; then - if [ "$FW" = "fw4" ]; then - nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload - else - iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload - fi - fi - - procd_open_instance - procd_set_param file "$conf" "/etc/config/firewall" - procd_set_param command "$PROG" - procd_append_param command -f "$conf" - [ "$log_output" = "info" ] && procd_append_param command -v - [ "$log_output" = "debug" ] && procd_append_param command -d - procd_close_instance + } >"$1" } stop_service() { if [ "$FW" = "fw3" ]; then - iptables -t nat -F MINIUPNPD 2>/dev/null - iptables -t nat -F MINIUPNPD-POSTROUTING 2>/dev/null iptables -t filter -F MINIUPNPD 2>/dev/null [ -x /usr/sbin/ip6tables ] && ip6tables -t filter -F MINIUPNPD 2>/dev/null + iptables -t nat -F MINIUPNPD 2>/dev/null + iptables -t nat -F MINIUPNPD-POSTROUTING 2>/dev/null else nft flush chain inet fw4 upnp_forward 2>/dev/null nft flush chain inet fw4 upnp_prerouting 2>/dev/null @@ -219,11 +181,41 @@ stop_service() { start_service() { config_load "upnpd" - config_foreach upnpd "upnpd" + local enabled config_file log_output conf + config_get enabled settings enabled 0 + config_get config_file settings config_file + config_get log_output settings log_output + if [ "$enabled" != "1" ]; then + log "Service disabled, UCI enabled is not set" + return 1 + fi + + if [ -n "$config_file" ]; then + conf="$config_file" + else + local tmpconf="/var/etc/miniupnpd.conf" + conf="$tmpconf" + mkdir -p /var/etc + upnpd_generate_config "$tmpconf" || return 1 + fi + + if [ "$FW" = "fw4" ]; then + nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload + else + iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload + fi + + procd_open_instance + procd_set_param file "$conf" + procd_set_param command "$PROG" + procd_append_param command -f "$conf" + [ "$log_output" = "info" ] && procd_append_param command -v + [ "$log_output" = "debug" ] && procd_append_param command -d + procd_close_instance } service_triggers() { - procd_add_reload_trigger "upnpd" + procd_add_reload_trigger "upnpd" "firewall" } log() { From 2305e473375ad9e23361b3aae9029516bffe2751 Mon Sep 17 00:00:00 2001 From: Self-Hosting-Group Date: Thu, 27 Nov 2025 00:00:00 +0000 Subject: [PATCH 10/10] miniupnpd: rearrange init and format `firewall3.include` - Arrange `start_service` and main init functions first - Format `firewall3.include` using shfmt (to merge with prior) Signed-off-by: Self-Hosting-Group --- net/miniupnpd/files/firewall3.include | 44 ++++---- net/miniupnpd/files/miniupnpd.init | 156 +++++++++++++------------- 2 files changed, 100 insertions(+), 100 deletions(-) diff --git a/net/miniupnpd/files/firewall3.include b/net/miniupnpd/files/firewall3.include index 53ca8dc716cc2f..8fc2144db31932 100644 --- a/net/miniupnpd/files/firewall3.include +++ b/net/miniupnpd/files/firewall3.include @@ -20,36 +20,36 @@ iptables_prepend_rule() { local chain="$3" local target="$4" - $iptables "$IPTARGS" -t "$table" -I "$chain" $($iptables "$IPTARGS" -t "$table" --line-numbers -nL "$chain" | \ + $iptables "$IPTARGS" -t "$table" -I "$chain" $($iptables "$IPTARGS" -t "$table" --line-numbers -nL "$chain" | sed -ne '$s/[^0-9].*//p') -j "$target" } ADDED=0 add_extzone_rules() { - local ext_zone="$1" - - [ -z "$ext_zone" ] && return - - # IPv4 - due to NAT, need to add both to nat and filter table - # need to insert as penultimate rule for input & forward & postrouting since final rule might be a fw3 REJECT - iptables_prepend_rule "$IPTABLES" filter "zone_${ext_zone}_input" MINIUPNPD - iptables_prepend_rule "$IPTABLES" filter "zone_${ext_zone}_forward" MINIUPNPD - $IPTABLES -t nat -A "zone_${ext_zone}_prerouting" -j MINIUPNPD - iptables_prepend_rule "$IPTABLES" nat "zone_${ext_zone}_postrouting" MINIUPNPD-POSTROUTING - - # IPv6 if available - filter only - [ -x $IP6TABLES ] && { - iptables_prepend_rule "$IP6TABLES" filter "zone_${ext_zone}_input" MINIUPNPD - iptables_prepend_rule "$IP6TABLES" filter "zone_${ext_zone}_forward" MINIUPNPD - } - ADDED=$(($ADDED + 1)) + local ext_zone="$1" + + [ -z "$ext_zone" ] && return + + # IPv4 - due to NAT, need to add both to nat and filter table + # need to insert as penultimate rule for input & forward & postrouting since final rule might be a fw3 REJECT + iptables_prepend_rule "$IPTABLES" filter "zone_${ext_zone}_input" MINIUPNPD + iptables_prepend_rule "$IPTABLES" filter "zone_${ext_zone}_forward" MINIUPNPD + $IPTABLES -t nat -A "zone_${ext_zone}_prerouting" -j MINIUPNPD + iptables_prepend_rule "$IPTABLES" nat "zone_${ext_zone}_postrouting" MINIUPNPD-POSTROUTING + + # IPv6 if available - filter only + [ -x $IP6TABLES ] && { + iptables_prepend_rule "$IP6TABLES" filter "zone_${ext_zone}_input" MINIUPNPD + iptables_prepend_rule "$IP6TABLES" filter "zone_${ext_zone}_forward" MINIUPNPD + } + ADDED=$(($ADDED + 1)) } # By default, user configuration is king. for ext_iface in $(uci -q get upnpd.settings.external_iface); do - add_extzone_rules $(fw3 -q network "$ext_iface") + add_extzone_rules $(fw3 -q network "$ext_iface") done add_extzone_rules $(uci -q get upnpd.settings.external_zone) @@ -66,7 +66,7 @@ network_find_wan wan_iface network_find_wan6 wan6_iface for ext_iface in $wan_iface $wan6_iface; do - # fw3 -q network fails on sub-interfaces => map to device first - network_get_device ext_device $ext_iface - add_extzone_rules $(fw3 -q device "$ext_device") + # fw3 -q network fails on sub-interfaces => map to device first + network_get_device ext_device $ext_iface + add_extzone_rules $(fw3 -q device "$ext_device") done diff --git a/net/miniupnpd/files/miniupnpd.init b/net/miniupnpd/files/miniupnpd.init index 2c525e095812d9..3d796471d4295c 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -7,34 +7,56 @@ USE_PROCD=1 PROG=/usr/sbin/miniupnpd [ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3" -is_port_or_range() { - [ "$1" = "0" ] && return 1 - [ "$1" -ge "1" ] 2>/dev/null && [ "$1" -le "65535" ] 2>/dev/null && return 0 - [ "$2" = "port0inrange" ] && local minport=0 || local minport=1 - [ "${1%%-*}" -ge "$minport" ] 2>/dev/null && [ "${1%%-*}" -le "65535" ] 2>/dev/null && - [ "${1##*-}" -ge "$minport" ] 2>/dev/null && [ "${1##*-}" -le "65535" ] 2>/dev/null && - [ "${1##*-}" -ge "${1%%-*}" ] 2>/dev/null && return 0 || return 1 +start_service() { + config_load "upnpd" + local enabled config_file log_output conf + config_get enabled settings enabled 0 + config_get config_file settings config_file + config_get log_output settings log_output + if [ "$enabled" != "1" ]; then + log "Service disabled, UCI enabled is not set" + return 1 + fi + + if [ -n "$config_file" ]; then + conf="$config_file" + else + local tmpconf="/var/etc/miniupnpd.conf" + conf="$tmpconf" + mkdir -p /var/etc + upnpd_generate_config "$tmpconf" || return 1 + fi + + if [ "$FW" = "fw4" ]; then + nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload + else + iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload + fi + + procd_open_instance + procd_set_param file "$conf" + procd_set_param command "$PROG" + procd_append_param command -f "$conf" + [ "$log_output" = "info" ] && procd_append_param command -v + [ "$log_output" = "debug" ] && procd_append_param command -d + procd_close_instance } -upnpd_add_custom_acl_entry() { - local cfg="$1" - local comment int_addr int_port ext_port descr_filter action - config_get comment "$cfg" comment "unspecified" # comment - config_get int_addr "$cfg" int_addr "0.0.0.0/0" # IPv4 or network and subnet mask (internal) - config_get int_port "$cfg" int_port "1-65535" # internal port/range: x or x-y - config_get ext_port "$cfg" ext_port "1-65535" # external port/range: x or x-y - config_get descr_filter "$cfg" descr_filter # description regex filter (must be built in) - config_get action "$cfg" action # accept/reject/ignore - ! is_port_or_range "$int_port" port0inrange && - log "Custom ACL: Entry with invalid port or port range ($int_port) in int_port ignored" daemon.warn && int_port=1-65535 - ! is_port_or_range "$ext_port" port0inrange && - log "Custom ACL: Entry with invalid port or port range ($ext_port) in ext_port ignored" daemon.warn && ext_port=1-65535 - [ "$descr_filter" != "" ] && descr_filter=" \"$descr_filter\"" - [ "$action" = "accept" ] && action=allow - [ "$action" = "reject" ] && action=deny - [ "$action" = "ignore" ] && return 0 - [ "$action" = "" ] && log "Custom ACL: Entry with no action ignored" daemon.warn && return 0 - echo "$action $ext_port $int_addr $int_port${descr_filter} # $comment" +stop_service() { + if [ "$FW" = "fw3" ]; then + iptables -t filter -F MINIUPNPD 2>/dev/null + [ -x /usr/sbin/ip6tables ] && ip6tables -t filter -F MINIUPNPD 2>/dev/null + iptables -t nat -F MINIUPNPD 2>/dev/null + iptables -t nat -F MINIUPNPD-POSTROUTING 2>/dev/null + else + nft flush chain inet fw4 upnp_forward 2>/dev/null + nft flush chain inet fw4 upnp_prerouting 2>/dev/null + nft flush chain inet fw4 upnp_postrouting 2>/dev/null + fi +} + +service_triggers() { + procd_add_reload_trigger "upnpd" "firewall" } upnpd_generate_config() { @@ -166,58 +188,6 @@ upnpd_generate_config() { } >"$1" } -stop_service() { - if [ "$FW" = "fw3" ]; then - iptables -t filter -F MINIUPNPD 2>/dev/null - [ -x /usr/sbin/ip6tables ] && ip6tables -t filter -F MINIUPNPD 2>/dev/null - iptables -t nat -F MINIUPNPD 2>/dev/null - iptables -t nat -F MINIUPNPD-POSTROUTING 2>/dev/null - else - nft flush chain inet fw4 upnp_forward 2>/dev/null - nft flush chain inet fw4 upnp_prerouting 2>/dev/null - nft flush chain inet fw4 upnp_postrouting 2>/dev/null - fi -} - -start_service() { - config_load "upnpd" - local enabled config_file log_output conf - config_get enabled settings enabled 0 - config_get config_file settings config_file - config_get log_output settings log_output - if [ "$enabled" != "1" ]; then - log "Service disabled, UCI enabled is not set" - return 1 - fi - - if [ -n "$config_file" ]; then - conf="$config_file" - else - local tmpconf="/var/etc/miniupnpd.conf" - conf="$tmpconf" - mkdir -p /var/etc - upnpd_generate_config "$tmpconf" || return 1 - fi - - if [ "$FW" = "fw4" ]; then - nft -s -t -n list chain inet fw4 upnp_forward >/dev/null 2>&1 || fw4 reload - else - iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload - fi - - procd_open_instance - procd_set_param file "$conf" - procd_set_param command "$PROG" - procd_append_param command -f "$conf" - [ "$log_output" = "info" ] && procd_append_param command -v - [ "$log_output" = "debug" ] && procd_append_param command -d - procd_close_instance -} - -service_triggers() { - procd_add_reload_trigger "upnpd" "firewall" -} - log() { logger -s -p "${2:-daemon.notice}" -t "upnpd" "$1" || echo "upnpd: $1" >&2 } @@ -227,6 +197,15 @@ xml_encode() { echo "$1" | sed "s/&/\&/g; s//\>/g" } +is_port_or_range() { + [ "$1" = "0" ] && return 1 + [ "$1" -ge "1" ] 2>/dev/null && [ "$1" -le "65535" ] 2>/dev/null && return 0 + [ "$2" = "port0inrange" ] && local minport=0 || local minport=1 + [ "${1%%-*}" -ge "$minport" ] 2>/dev/null && [ "${1%%-*}" -le "65535" ] 2>/dev/null && + [ "${1##*-}" -ge "$minport" ] 2>/dev/null && [ "${1##*-}" -le "65535" ] 2>/dev/null && + [ "${1##*-}" -ge "${1%%-*}" ] 2>/dev/null && return 0 || return 1 +} + upnpd_add_int_network_and_preset() { local cfg="$1" local interface access_preset accept_ports reject_ports custom_acl_before @@ -271,3 +250,24 @@ upnpd_add_int_network_and_preset() { echo "deny 1-65535 $subnet 1-65535 # Reject ACL by default on $interface" fi } + +upnpd_add_custom_acl_entry() { + local cfg="$1" + local comment int_addr int_port ext_port descr_filter action + config_get comment "$cfg" comment "unspecified" # comment + config_get int_addr "$cfg" int_addr "0.0.0.0/0" # IPv4 or network and subnet mask (internal) + config_get int_port "$cfg" int_port "1-65535" # internal port/range: x or x-y + config_get ext_port "$cfg" ext_port "1-65535" # external port/range: x or x-y + config_get descr_filter "$cfg" descr_filter # description regex filter (must be built in) + config_get action "$cfg" action # accept/reject/ignore + ! is_port_or_range "$int_port" port0inrange && + log "Custom ACL: Entry with invalid port or port range ($int_port) in int_port ignored" daemon.warn && int_port=1-65535 + ! is_port_or_range "$ext_port" port0inrange && + log "Custom ACL: Entry with invalid port or port range ($ext_port) in ext_port ignored" daemon.warn && ext_port=1-65535 + [ "$descr_filter" != "" ] && descr_filter=" \"$descr_filter\"" + [ "$action" = "accept" ] && action=allow + [ "$action" = "reject" ] && action=deny + [ "$action" = "ignore" ] && return 0 + [ "$action" = "" ] && log "Custom ACL: Entry with no action ignored" daemon.warn && return 0 + echo "$action $ext_port $int_addr $int_port${descr_filter} # $comment" +}