diff --git a/net/miniupnpd/Makefile b/net/miniupnpd/Makefile index c8900c7050a46..8a2171aa898e1 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,22 +80,28 @@ 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 $(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/firewall3.include b/net/miniupnpd/files/firewall3.include index 4fd483974306c..8fc2144db3193 100644 --- a/net/miniupnpd/files/firewall3.include +++ b/net/miniupnpd/files/firewall3.include @@ -20,39 +20,39 @@ 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.config.external_iface); do - add_extzone_rules $(fw3 -q network "$ext_iface") +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 @@ -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.hotplug b/net/miniupnpd/files/miniupnpd.hotplug index 607a32bdc6885..27b2d0f7369e4 100644 --- a/net/miniupnpd/files/miniupnpd.hotplug +++ b/net/miniupnpd/files/miniupnpd.hotplug @@ -1,41 +1,33 @@ +#!/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.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 - -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 de3504529b600..3d796471d4295 100644 --- a/net/miniupnpd/files/miniupnpd.init +++ b/net/miniupnpd/files/miniupnpd.init @@ -7,173 +7,165 @@ 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 -} - -conf_rule_add() { - local cfg="$1" - local action int_addr - local ext_start ext_end int_start int_end comment +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 - 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 + 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 - # Make a single IP IP/32 so that miniupnpd.conf can use it. - [ "${int_addr%/*}" = "$int_addr" ] && int_addr="$int_addr/32" + 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 - echo "$action $ext_start${ext_end:+-}$ext_end $int_addr $int_start${int_end:+-}$int_end #$comment" + 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_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" +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 - echo "$alt=yes" + 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 } -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 clean_ruleset_threshold clean_ruleset_interval - local ipv6_disable - - local enabled - config_get_bool enabled config enabled 1 - [ "$enabled" -eq 0 ] && return 1 - - 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 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 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 +service_triggers() { + procd_add_reload_trigger "upnpd" "firewall" +} +upnpd_generate_config() { + # Daemon + 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 lease_file settings lease_file /run/miniupnpd.leases + + # 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 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 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 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 - - { - 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 + 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 - 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" - } - - [ -n "$upload" ] && [ -n "$download" ] && { - echo "bitrate_down=$((download * 1024 * 8))" - echo "bitrate_up=$((upload * 1024 * 8))" - } - - [ -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" - - [ -z "$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 + { + 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" + # Alternatively, as allow-filtered detects the public IPv4 required by multiple clients, e.g. PCP/NAT-PMP + [ "$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 + [ "$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" + [ "$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" + + 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.settings.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" @@ -181,46 +173,101 @@ upnpd() { echo "upnp_nat_postrouting_chain=upnp_postrouting" fi - } > "$tmpconf" - fi + echo "# External network interface" + echo "ext_ifname=$ifname" + echo "ext_ifname6=$ifname6" + [ -n "$external_ip" ] && echo "ext_ip=$external_ip" - 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 - iptables -L MINIUPNPD >/dev/null 2>&1 || fw3 reload - fi - else - logger -t "upnp daemon" "external interface not found, not starting" - fi + # Enabled internal networks / access control + config_foreach upnpd_add_int_network_and_preset internal_network precustom + echo "# Custom ACL" + 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" - 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 - 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 - 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 +log() { + logger -s -p "${2:-daemon.notice}" -t "upnpd" "$1" || echo "upnpd: $1" >&2 } -start_service() { - config_load "upnpd" - config_foreach upnpd "upnpd" +xml_encode() { + # Encode required XML entities of text UPnP IGD config options until the daemon does so + echo "$1" | sed "s/&/\&/g; s//\>/g" } -service_triggers() { - procd_add_reload_trigger "upnpd" +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 + 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 + 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 + 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 + 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 + 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 +} + +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" } diff --git a/net/miniupnpd/files/upnpd-migration.uci-defaults b/net/miniupnpd/files/upnpd-migration.uci-defaults new file mode 100644 index 0000000000000..e83aea7a572fe --- /dev/null +++ b/net/miniupnpd/files/upnpd-migration.uci-defaults @@ -0,0 +1,244 @@ +#!/bin/sh + +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) +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 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 + 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" + [ "$access_preset" != "" ] && uci set upnpd.@internal_network[$ifnr].access_preset="$access_preset" + ifnr=$((ifnr + 1)) + done + 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 bd7c3ec4007dc..a370042222f53 100644 --- a/net/miniupnpd/files/upnpd.config +++ b/net/miniupnpd/files/upnpd.config @@ -1,28 +1,37 @@ -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 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" +config upnpd 'settings' + 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' -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" +# 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 +# Action can now be set to accept/reject/ignore + +config acl_entry + option comment 'High ports' + option int_addr '0.0.0.0/0' + option int_port '1024-65535' + option ext_port '1024-65535' + option action 'ignore' + +config acl_entry + option comment 'Low/system ports' + option int_addr '0.0.0.0/0' + option int_port '1-1023' + option ext_port '1-1023' + option action 'ignore' 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 0000000000000..08ef5b61bc4b5 --- /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, 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 0000000000000..9d657d15345f0 --- /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 +- 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 0000000000000..b8a42b2b567f2 --- /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); 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 0a1ba0a2339f8..0000000000000 --- 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 8a569f35f7f7e..0000000000000 --- 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