From: Stan Grishin Date: Wed, 1 Jul 2026 18:13:57 +0000 (+0000) Subject: pbr: update to 1.2.2-18 X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=8ef980be00327e62e6377283826db4cc6c04a6fc;p=openwrt-packages.git pbr: update to 1.2.2-18 Maintainer: me Compile tested: x86_64, Dell EMC Edge620, OpenWrt 25.12.4 Run tested: x86_64, Dell EMC Edge620, OpenWrt 25.12.4 Description: Update to version 1.2.2-18 - Update PKG_RELEASE to 18. files/etc/init.d/pbr: - Update package compatibility level to 26. - Improve IPv4 gateway detection for various scenarios (e.g., netifd, point-to-point links). - Add pbr_get_ipaddr4 function to retrieve an interface's IPv4 address. - Improve IPv6 gateway detection, including link-local router discovery and point-to-point link handling. - Add pbr_get_ipaddr6 function to retrieve an interface's IPv6 address. - Introduce `is_punycode` helper for domain validation, recognizing internationalized domain names. - Expand `is_domain` to include `is_punycode` for comprehensive domain matching. - Add `warningInterfaceRoutingUnknownGateway` text for improved error messages. - Enhance `nftset` dnsmasq element addition logic to properly handle existing entries and append new specifications for dual-stack. - Flush IPv4 and IPv6 routes from custom tables during cleanup to ensure proper state reset. - Optimize `dns_policy_process` to only record the first IPv4 and IPv6 DNS servers to avoid issues with multi-value `src_addr` in family mismatch checks. - Refine IPv4 routing (`interface_routing`) for strict enforcement and point-to-point links. - Refine IPv6 routing (`interface_routing`) for strict enforcement and point-to-point links. - Adjust display of gateway information (`dispGw4`, `dispGw6`) to show IP address if no gateway is found. - Ensure `json_add_gateway` uses `dispGw4` and `dispGw6` for consistent output. - Move `process_interface` calls for `reset_globals` and `enumerate_interface` earlier in `start_service` for consistent trigger registration. - Add logic to skip IPv6 interface reload if the gateway is unchanged to prevent unnecessary restarts. - Standardize `service_triggers` to always register all triggers, ensuring robust recovery from WAN-down states. Signed-off-by: Stan Grishin --- diff --git a/net/pbr/Makefile b/net/pbr/Makefile index 9f534ddf5..e4ef8328e 100644 --- a/net/pbr/Makefile +++ b/net/pbr/Makefile @@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=pbr PKG_VERSION:=1.2.2 -PKG_RELEASE:=14 +PKG_RELEASE:=18 PKG_LICENSE:=AGPL-3.0-or-later PKG_MAINTAINER:=Stan Grishin diff --git a/net/pbr/files/etc/init.d/pbr b/net/pbr/files/etc/init.d/pbr index 240ee15aa..fd03a2f4b 100755 --- a/net/pbr/files/etc/init.d/pbr +++ b/net/pbr/files/etc/init.d/pbr @@ -37,7 +37,7 @@ fi readonly packageName='pbr' readonly PKG_VERSION='dev-test' -readonly packageCompat='25' +readonly packageCompat='26' readonly serviceName="$packageName $PKG_VERSION" readonly packageConfigFile="/etc/config/${packageName}" readonly packageDebugFile="/var/run/${packageName}.debug" @@ -259,22 +259,55 @@ pbr_find_iface() { } pbr_get_gateway4() { local iface="$2" dev="$3" gw + is_uplink6 "$iface" && iface="$uplink_interface4" network_get_gateway gw "$iface" true if [ -z "$gw" ] || [ "$gw" = '0.0.0.0' ]; then -# gw="$(ubus call "network.interface.${iface}" status | jsonfilter -e "@.route[0].nexthop")" - gw="$(ip -4 a list dev "$dev" 2>/dev/null | grep inet | awk '{print $2}' | awk -F "/" '{print $1}')" + gw="$(ip -4 route show dev "$dev" table all 2>/dev/null | awk '$1=="default" {for (i=1; i/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="via") {print $(i+1); exit}}')" + # Fall back to ip route get using "table all" might already work + [ -z "$gw" ] && gw="$(ip -4 route get 1.1.1.1 oif "$dev" 2>/dev/null | awk '/via/ {print $3; exit}')" + # Raise warning if no gw and not point-to-point + { [ -z "$gw" ] && ! ip address show dev "$dev" 2>/dev/null | grep -q "POINTOPOINT"; } && json add warning 'warningInterfaceRoutingUnknownGateway' "$dev" fi eval "$1"='$gw' } +pbr_get_ipaddr4() { + local iface="$2" dev="$3" ipa + is_uplink6 "$iface" && iface="$uplink_interface4" + network_get_ipaddr ipa "$iface" + if [ -z "$ipa" ] || [ "$ipa" = '0.0.0.0' ]; then + [ -n "$dev" ] && ipa="$(ip -4 -o addr show dev "$dev" 2>/dev/null | awk '{print $4; exit}' | cut -d/ -f1)" + fi + eval "$1"='$ipa' +} pbr_get_gateway6() { + [ -z "$ipv6_enabled" ] && return 0 local iface="$2" dev="$3" gw is_uplink4 "$iface" && iface="$uplink_interface6" network_get_gateway6 gw "$iface" true if [ -z "$gw" ] || [ "$gw" = '::/0' ] || [ "$gw" = '::0/0' ] || [ "$gw" = '::' ]; then - gw="$(ip -6 a list dev "$dev" 2>/dev/null | grep inet6 | grep 'scope global' | awk '{print $2}')" + gw="$(ip -6 route show dev "$dev" table all 2>/dev/null | awk '$1=="default" {for (i=1; i/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="via") {print $(i+1); exit}}')" + # Fall back to using ip -6 get route is not working as source routing is used which needs "from" + # Fall back to a link-local neighbor advertised as router. + [ -z "$gw" ] && gw="$(ip -6 neigh show dev "$dev" 2>/dev/null | awk '/^fe80:.*router/ {print $1; exit}')" + # Raise warning if no gw and not point-to-point link + { [ -z "$gw" ] && ! ip address show dev "$dev" 2>/dev/null | grep -q "POINTOPOINT"; } && json add warning 'warningInterfaceRoutingUnknownGateway' "$dev" fi eval "$1"='$gw' } +pbr_get_ipaddr6() { + [ -z "$ipv6_enabled" ] && return 0 + local iface="$2" dev="$3" ipa + is_uplink4 "$iface" && iface="$uplink_interface6" + network_get_ipaddr6 ipa "$iface" + if [ -z "$ipa" ] || [ "$ipa" = '::/0' ] || [ "$ipa" = '::0/0' ] || [ "$ipa" = '::' ]; then + [ -n "$dev" ] && ipa="$(ip -6 -o addr show dev "$dev" scope global 2>/dev/null | awk '{print $4; exit}' | cut -d/ -f1)" + fi + eval "$1"='$ipa' +} filter_options() { local opt="$1" values="$2" v _ret for v in $values; do @@ -317,7 +350,8 @@ is_netifd_interface_default() { is_disabled_interface() { [ "$(uci_get 'network' "$1" 'disabled')" = '1' ]; } is_host() { echo "$1" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9_-]{0,61}[a-zA-Z0-9]$|^[a-zA-Z0-9]$'; } is_hostname() { echo "$1" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9_-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'; } -is_domain() { ! is_ipv4 "$1" && ! is_mac_address_bad_notation "$1" && { is_host "$1" || is_hostname "$1"; }; } +is_punycode() { echo "$1" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$' && echo "$1" | grep -qE '(^|\.)xn--'; } +is_domain() { ! is_ipv4 "$1" && ! is_mac_address_bad_notation "$1" && { is_host "$1" || is_hostname "$1" || is_punycode "$1"; }; } is_dslite() { local p; network_get_protocol p "$1"; [ "${p:0:6}" = "dslite" ]; } is_family_mismatch() { ( is_ipv4 "${1//!}" && is_ipv6 "${2//!}" ) || ( is_ipv6 "${1//!}" && is_ipv4 "${2//!}" ); } is_greater() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } @@ -649,6 +683,7 @@ get_text() { errorNetifdMissingOption) printf "Netifd setup: required option '%s' is missing" "$1";; errorNetifdInvalidGateway4) printf "Netifd setup: invalid value of netifd_interface_default option '%s'" "$1";; errorNetifdInvalidGateway6) printf "Netifd setup: invalid value of netifd_interface_default6 option '%s'" "$1";; + warningInterfaceRoutingUnknownGateway) printf "Unknown Gateway for device '%s'" "$1";; warningInvalidOVPNConfig) printf "Invalid OpenVPN config for '%s' interface" "$1";; warningResolverNotSupported) printf "Resolver set (%s) is not supported on this system" "$resolver_set";; warningPolicyProcessCMD) printf "'%s'" "$1";; @@ -1097,8 +1132,26 @@ nftset() { ;; add_dnsmasq_element) [ -n "$ipv6_enabled" ] || unset nftset6 - grep -qxF "nftset=/${param}/4#inet#${nftTable}#${nftset4}${nftset6:+,6#inet#${nftTable}#${nftset6}} # $comment" "$packageDnsmasqFile" && return 0 - echo "nftset=/${param}/4#inet#${nftTable}#${nftset4}${nftset6:+,6#inet#${nftTable}#${nftset6}} # $comment" >> "$packageDnsmasqFile" && ipv4_error=0 + local new_spec="4#inet#${nftTable}#${nftset4}${nftset6:+,6#inet#${nftTable}#${nftset6}}" + local escaped_param + escaped_param=$(printf '%s' "$param" | sed 's/\./\\./g') + local safe_nftset4 safe_nftset6 + safe_nftset4=$(printf '%s' "$nftset4" | sed 's|[/.^$*\[\\]|\\&|g') + safe_nftset6=$(printf '%s' "${nftset6:-}" | sed 's|[/.^$*\[\\]|\\&|g') + grep -qE "^nftset=/${escaped_param}/.*#${safe_nftset4}(,| |$)" "$packageDnsmasqFile" && return 0 + if grep -q "^nftset=/${escaped_param}/" "$packageDnsmasqFile"; then + local append_spec="$new_spec" + if [ -n "$nftset6" ] && grep -qE "^nftset=/${escaped_param}/.*#${safe_nftset6}(,| |$)" "$packageDnsmasqFile"; then + append_spec="4#inet#${nftTable}#${nftset4}" + fi + local safe_append_spec safe_comment + safe_append_spec=$(printf '%s' "$append_spec" | sed 's/[|&\\]/\\&/g') + safe_comment=$(printf '%s' "$comment" | sed 's/[|&\\]/\\&/g') + sed -i "/^nftset=\/${escaped_param}\// s|\( #.*\)$|,${safe_append_spec}\1, ${safe_comment}|" \ + "$packageDnsmasqFile" && ipv4_error=0 + else + echo "nftset=/${param}/${new_spec} # ${comment}" >> "$packageDnsmasqFile" && ipv4_error=0 + fi ;; create) case "$type" in @@ -1187,7 +1240,11 @@ cleanup() { rt_tables) # shellcheck disable=SC2013 for i in $(grep -oh "${ipTablePrefix}_.*" "$rtTablesFile"); do - ! is_netifd_table "$i" && sed -i "/${i}/d" "$rtTablesFile" + if ! is_netifd_table "$i";then + ip -4 route flush table "$i" >/dev/null 2>&1 + ip -6 route flush table "$i" >/dev/null 2>&1 + sed -i "/${i}/d" "$rtTablesFile" + fi done sync ;; @@ -2095,15 +2152,17 @@ dns_policy_process() { dest_dns_interface="$(str_first_value_interface "$dest_dns")" dest_dns_ipv4="$(str_first_value_ipv4 "$dest_dns")" dest_dns_ipv6="$(str_first_value_ipv6 "$dest_dns")" + # Record first v4 and first v6 DNS server from the interface; the family + # decision is made later by the filter_options loop, which knows the + # per-group family by construction. Avoids passing a multi-value + # src_addr to is_family_mismatch (which expected one address per call). if is_supported_interface "$dest_dns_interface"; then local d for d in $(uci -q get network."$dest_dns_interface".dns); do - if ! is_family_mismatch "$src_addr" "$d"; then - if is_ipv4 "$d"; then - dest_dns_ipv4="${dest_dns_ipv4:-${d}}" - elif is_ipv6 "$d"; then - dest_dns_ipv6="${dest_dns_ipv6:-${d}}" - fi + if is_ipv4 "$d"; then + dest_dns_ipv4="${dest_dns_ipv4:-${d}}" + elif is_ipv6 "$d"; then + dest_dns_ipv6="${dest_dns_ipv6:-${d}}" fi done fi @@ -2281,14 +2340,17 @@ interface_routing() { ip -4 route flush table "$tid" >/dev/null 2>&1 if [ -n "$gw4" ] || [ -n "$strict_enforcement" ]; then - if [ -z "$gw4" ] && ip address show dev "$dev4" 2>/dev/null | grep -q "POINTOPOINT"; then + if [ -n "$gw4" ]; then + try ip -4 route replace default via "$gw4" dev "$dev4" table "$tid" || ipv4_error=1 + elif ip -o link show dev "$dev4" 2>/dev/null | grep -q "POINTOPOINT"; then # might need to set this rule as first to prevent rogue gateway detection for point-to-point links try ip -4 route replace default dev "$dev4" table "$tid" || ipv4_error=1 - elif [ -z "$gw4" ]; then - try ip -4 route replace unreachable default table "$tid" || ipv4_error=1 else - try ip -4 route replace default via "$gw4" dev "$dev4" table "$tid" || ipv4_error=1 + try ip -4 route replace unreachable default table "$tid" || ipv4_error=1 fi try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1 + elif ip -o link show dev "$dev4" 2>/dev/null | grep -q "POINTOPOINT"; then + try ip -4 route replace default dev "$dev4" table "$tid" || ipv4_error=1 + try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1 fi elif [ -n "$strict_enforcement" ] && ! { is_split_uplink && [ "$iface" = "$uplink_interface6" ]; }; then ipv4_error=0 @@ -2303,24 +2365,18 @@ interface_routing() { ip -6 rule flush table "$tid" >/dev/null 2>&1 ip -6 route flush table "$tid" >/dev/null 2>&1 - if { [ -n "$gw6" ] && [ "$gw6" != "::/0" ]; } || [ -n "$strict_enforcement" ]; then - if { [ -z "$gw6" ] || [ "$gw6" = "::/0" ]; } && ip address show dev "$dev6" 2>/dev/null | grep -q "POINTOPOINT"; then + if [ -n "$gw6" ] || [ -n "$strict_enforcement" ]; then + if [ -n "$gw6" ]; then + try ip -6 route replace default via "$gw6" dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 + elif ip -o link show dev "$dev6" 2>/dev/null | grep -q "POINTOPOINT"; then # might need to set this rule as first to prevent rogue gateway detection for point-to-point links try ip -6 route replace default dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 - elif [ -z "$gw6" ] || [ "$gw6" = "::/0" ]; then - try ip -6 route replace unreachable default table "$tid" || ipv6_error=1 - elif ip -6 route list table main | grep -q " dev $dev6 "; then - if ip -6 address show dev "$dev6" | grep -q "BROADCAST"; then - try ip -6 route replace default via "$gw6" dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 - elif ip -6 address show dev "$dev6" | grep -q "POINTOPOINT"; then - try ip -6 route replace default dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 - else - json add error 'errorInterfaceRoutingUnknownDevType' "$dev6" - fi else - try ip -6 route replace "$(ip -6 -o a show "$dev6" | awk '{print $4}')" dev "$dev6" table "$tid" || ipv6_error=1 - try ip -6 route replace default dev "$dev6" table "$tid" || ipv6_error=1 + try ip -6 route replace unreachable default table "$tid" || ipv6_error=1 fi try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1 + elif ip -o link show dev "$dev6" 2>/dev/null | grep -q "POINTOPOINT"; then + try ip -6 route replace default dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 + try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1 fi elif [ -n "$ipv6_enabled" ] && [ -n "$strict_enforcement" ] && ! { is_split_uplink && [ "$iface" = "$uplink_interface4" ]; }; then ipv6_error=0 @@ -2369,15 +2425,19 @@ interface_routing() { ipv4_error=0 ip -4 rule flush fwmark "${mark}/${fw_mask}" table "$tid" >/dev/null 2>&1 ip -4 route flush table "$tid" >/dev/null 2>&1 + if [ -n "$gw4" ] || [ -n "$strict_enforcement" ]; then - if [ -z "$gw4" ] && ip address show dev "$dev4" 2>/dev/null | grep -q "POINTOPOINT"; then + if [ -n "$gw4" ]; then + try ip -4 route replace default via "$gw4" dev "$dev4" table "$tid" || ipv4_error=1 + elif ip -o link show dev "$dev4" 2>/dev/null | grep -q "POINTOPOINT"; then # might need to set this rule as first to prevent rogue gateway detection for point-to-point links try ip -4 route replace default dev "$dev4" table "$tid" || ipv4_error=1 - elif [ -z "$gw4" ]; then - try ip -4 route replace unreachable default table "$tid" || ipv4_error=1 else - try ip -4 route replace default via "$gw4" dev "$dev4" table "$tid" || ipv4_error=1 + try ip -4 route replace unreachable default table "$tid" || ipv4_error=1 fi try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1 + elif ip -o link show dev "$dev4" 2>/dev/null | grep -q "POINTOPOINT"; then + try ip -4 route replace default dev "$dev4" table "$tid" || ipv4_error=1 + try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1 fi elif [ -n "$strict_enforcement" ] && ! { is_split_uplink && [ "$iface" = "$uplink_interface6" ]; }; then ipv4_error=0 @@ -2391,24 +2451,19 @@ interface_routing() { ipv6_error=0 ip -6 rule flush fwmark "${mark}/${fw_mask}" table "$tid" >/dev/null 2>&1 ip -6 route flush table "$tid" >/dev/null 2>&1 - if { [ -n "$gw6" ] && [ "$gw6" != "::/0" ]; } || [ -n "$strict_enforcement" ]; then - if { [ -z "$gw6" ] || [ "$gw6" = "::/0" ]; } && ip address show dev "$dev6" 2>/dev/null | grep -q "POINTOPOINT"; then + + if [ -n "$gw6" ] || [ -n "$strict_enforcement" ]; then + if [ -n "$gw6" ]; then + try ip -6 route replace default via "$gw6" dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 + elif ip -o link show dev "$dev6" 2>/dev/null | grep -q "POINTOPOINT"; then # might need to set this rule as first to prevent rogue gateway detection for point-to-point links try ip -6 route replace default dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 - elif [ -z "$gw6" ] || [ "$gw6" = "::/0" ]; then - try ip -6 route replace unreachable default table "$tid" || ipv6_error=1 - elif ip -6 route list table main | grep -q " dev $dev6 "; then - if ip -6 address show dev "$dev6" | grep -q "BROADCAST"; then - try ip -6 route replace default via "$gw6" dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 - elif ip -6 address show dev "$dev6" | grep -q "POINTOPOINT"; then - try ip -6 route replace default dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 - else - json add error 'errorInterfaceRoutingUnknownDevType' "$dev6" - fi else - try ip -6 route replace "$(ip -6 -o a show "$dev6" | awk '{print $4}')" dev "$dev6" table "$tid" || ipv6_error=1 - try ip -6 route replace default dev "$dev6" table "$tid" || ipv6_error=1 + try ip -6 route replace unreachable default table "$tid" || ipv6_error=1 fi try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1 + elif ip address show dev "$dev6" 2>/dev/null | grep -q "POINTOPOINT"; then + try ip -6 route replace default dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1 + try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1 fi elif [ -n "$ipv6_enabled" ] && [ -n "$strict_enforcement" ] && ! { is_split_uplink && [ "$iface" = "$uplink_interface4" ]; }; then ipv6_error=0 @@ -2449,7 +2504,7 @@ json_add_gateway() { } process_interface() { - local gw4 gw6 dev4 dev6 s=0 dscp iface="$1" action="$2" reloadedIface="$3" + local gw4 gw6 ipa4 ipa6 dev4 dev6 s=0 dscp iface="$1" action="$2" reloadedIface="$3" local displayText dispDev dispGw4 dispGw6 dispStatus if [ "$iface" = 'all' ]; then @@ -2587,8 +2642,10 @@ process_interface() { eval "tid_${iface//-/_}"='$_tid' pbr_get_gateway4 gw4 "$iface" "$dev4" pbr_get_gateway6 gw6 "$iface" "$dev6" - dispGw4="${gw4:-0.0.0.0}" - dispGw6="${gw6:-::/0}" + pbr_get_ipaddr4 ipa4 "$iface" "$dev4" + pbr_get_ipaddr6 ipa6 "$iface" "$dev6" + dispGw4="${gw4:-${ipa4:--}}" + dispGw6="${gw6:-${ipa6:--}}" if is_split_uplink; then if is_uplink4 "$iface"; then gw6=""; dev6="" @@ -2606,7 +2663,7 @@ process_interface() { displayText="${iface}/${dispDev:+${dispDev}/}${dispGw4}${ipv6_enabled:+/${dispGw6}}" output 2 "Setting up routing for '$displayText' " if interface_routing 'create' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority"; then - json_add_gateway 'create' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority" "$dispStatus" + json_add_gateway 'create' "$_tid" "$_mark" "$iface" "$dispGw4" "$dev4" "$dispGw6" "$dev6" "$_priority" "$dispStatus" gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n" if is_netifd_interface "$iface"; then output_okb; else output_ok; fi else @@ -2676,8 +2733,10 @@ process_interface() { eval "tid_${iface//-/_}"='$_tid' pbr_get_gateway4 gw4 "$iface" "$dev4" pbr_get_gateway6 gw6 "$iface" "$dev6" - dispGw4="${gw4:-0.0.0.0}" - dispGw6="${gw6:-::/0}" + pbr_get_ipaddr4 ipa4 "$iface" "$dev4" + pbr_get_ipaddr6 ipa6 "$iface" "$dev6" + dispGw4="${gw4:-${ipa4:--}}" + dispGw6="${gw6:-${ipa6:--}}" if is_split_uplink; then if is_uplink4 "$iface"; then gw6=""; dev6="" @@ -2705,8 +2764,10 @@ process_interface() { eval "tid_${iface//-/_}"='$_tid' pbr_get_gateway4 gw4 "$iface" "$dev4" pbr_get_gateway6 gw6 "$iface" "$dev6" - dispGw4="${gw4:-0.0.0.0}" - dispGw6="${gw6:-::/0}" + pbr_get_ipaddr4 ipa4 "$iface" "$dev4" + pbr_get_ipaddr6 ipa6 "$iface" "$dev6" + dispGw4="${gw4:-${ipa4:--}}" + dispGw6="${gw6:-${ipa6:--}}" if is_split_uplink; then if is_uplink4 "$iface"; then gw6=""; dev6="" @@ -2725,7 +2786,7 @@ process_interface() { if [ "$iface" = "$reloadedIface" ]; then output 2 "Reloading routing for '$displayText' " if interface_routing 'reload_interface' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority"; then - json_add_gateway 'reload_interface' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority" "$dispStatus" + json_add_gateway 'reload_interface' "$_tid" "$_mark" "$iface" "$dispGw4" "$dev4" "$dispGw6" "$dev6" "$_priority" "$dispStatus" gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n" if is_netifd_interface "$iface"; then output_okb; else output_ok; fi else @@ -2733,7 +2794,7 @@ process_interface() { output_fail fi else - json_add_gateway 'skip_interface' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority" "$dispStatus" + json_add_gateway 'skip_interface' "$_tid" "$_mark" "$iface" "$dispGw4" "$dev4" "$dispGw6" "$dev6" "$_priority" "$dispStatus" gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n" fi ;; @@ -2811,19 +2872,26 @@ start_service() { load_package_config "$param" trap 'enable_forward' EXIT - stop_forward load_environment "${param:-on_start}" "$(load_validate_config)" || return 1 output "Processing environment (${param:-on_start}) " + + # Build ifacesTriggers BEFORE is_wan_up — enumerate_interface only + # consults UCI/static state and is safe with WAN down. Doing it here + # means service_triggers() can register the full interface trigger + # set even on a degraded start (pbrBootFlag=1), so interface events + # during a WAN flap continue to fire pbr instead of having all + # triggers replaced by a single boot-retry trigger. + process_interface 'all' 'reset_globals' + config_foreach process_interface 'interface' 'enumerate_interface' + if ! is_wan_up "$param"; then output_failn output_warning "$(get_text 'warningUplinkDown')" pbrBootFlag=1 return 0 fi - - process_interface 'all' 'reset_globals' - config_foreach process_interface 'interface' 'enumerate_interface' + output_okn case "$param" in on_boot) @@ -2873,6 +2941,20 @@ start_service() { serviceStartTrigger="${serviceStartTrigger:-on_start}" fi + # skip on_interface_reload of wan6 in case of renew of DHCP with no change in gateway + if [ "$serviceStartTrigger" = 'on_interface_reload' ] && is_uplink6 "$reloadedIface"; then + local prevGwIpv6 + prevGwIpv6="$(ubus_get_interface "$reloadedIface" 'gateway_ipv6')" + output 2 "on_interface_reload '$reloadedIface': current IPv6 gateway='${uplinkGW6:-}', previous='${prevGwIpv6:-}'\n" + if [ -n "$uplinkGW6" ] && [ "$uplinkGW6" = "$prevGwIpv6" ]; then + output 2 "Skipping reload for '$reloadedIface': IPv6 gateway unchanged\n" + resolver 'store_hash' + return 0 + fi + fi + + stop_forward + procd_open_instance 'main' procd_set_param command /bin/true procd_set_param stdout 1 @@ -2882,7 +2964,6 @@ start_service() { case $serviceStartTrigger in on_interface_reload) resolver 'store_hash' - output_okn output 1 "Reloading Interface: $reloadedIface " json_add_array 'gateways' process_interface 'all' 'reset_globals' @@ -2895,7 +2976,6 @@ start_service() { resolver 'configure' cleanup 'main_table' 'rt_tables' 'main_chains' 'sets' nft_file 'create' 'main' - output_okn output 1 'Processing interfaces ' json_add_array 'gateways' process_interface 'all' 'reset_globals' @@ -3008,40 +3088,49 @@ service_stopped() { procd_set_config_changed firewall; } service_triggers() { local n - if [ -n "$pbrBootFlag" ]; then - output "Setting trigger (on_boot) " - if procd_add_raw_trigger "interface.*.up" "$procd_boot_trigger_delay" "/etc/init.d/${packageName}" start; then - output_okn - else - output_failn + # Always register the full validators + interface/config triggers. + # procd's _procd_close_service unconditionally emits a `triggers` + # field, and procd_update() replaces s->trigger with whatever we + # emit — so emitting fewer triggers in any branch silently wipes + # the rest. Registering the full set in every start_service path + # preserves trigger registration across WAN-down transitions. + PROCD_RELOAD_DELAY=$(( procd_reload_delay * 1000 )) + procd_open_validate + load_validate_config + load_validate_policy + load_validate_include + procd_close_validate + procd_open_trigger + procd_add_config_trigger "config.change" 'openvpn' "/etc/init.d/${packageName}" reload 'on_openvpn_change' + procd_add_config_trigger "config.change" "${packageName}" "/etc/init.d/${packageName}" reload + procd_add_config_trigger "config.change" "network" "/etc/init.d/${packageName}" reload + if [ -n "$ifacesTriggers" ]; then + output 1 "Setting interface triggers " + for n in $ifacesTriggers; do + output 2 "Setting interface trigger for $n " + if procd_add_interface_trigger "interface.*" "$n" "/etc/init.d/${packageName}" on_interface_reload "$n"; then + output_ok + else + output_fail + fi + done + output_1_newline fi - else - PROCD_RELOAD_DELAY=$(( procd_reload_delay * 1000 )) - procd_open_validate - load_validate_config - load_validate_policy - load_validate_include - procd_close_validate - procd_open_trigger - procd_add_config_trigger "config.change" 'openvpn' "/etc/init.d/${packageName}" reload 'on_openvpn_change' - procd_add_config_trigger "config.change" "${packageName}" "/etc/init.d/${packageName}" reload - procd_add_config_trigger "config.change" "network" "/etc/init.d/${packageName}" reload - if [ -n "$ifacesTriggers" ]; then - output 1 "Setting interface triggers " - for n in $ifacesTriggers; do - output 2 "Setting interface trigger for $n " - if procd_add_interface_trigger "interface.*" "$n" "/etc/init.d/${packageName}" on_interface_reload "$n"; then - output_ok - else - output_fail - fi - done - output_1_newline + # Safety net: when WAN is down at start time, also schedule a + # raw boot-retry on iface.*.up. The interface triggers above + # will normally drive recovery; the raw trigger only matters if + # no interfaces were enumerable yet. + if [ -n "$pbrBootFlag" ]; then + output "Setting trigger (on_boot) " + if procd_add_raw_trigger "interface.*.up" "$procd_boot_trigger_delay" "/etc/init.d/${packageName}" start; then + output_okn + else + output_failn fi - procd_close_trigger - if [ "$serviceStartTrigger" = 'on_start' ] && [ -n "$ifacesTriggers" ]; then - output 3 "$serviceName monitoring interfaces: ${ifacesTriggers}\n" fi + procd_close_trigger + if [ "$serviceStartTrigger" = 'on_start' ] && [ -n "$ifacesTriggers" ]; then + output 3 "$serviceName monitoring interfaces: ${ifacesTriggers}\n" fi } diff --git a/net/pbr/tests/01_validation/03_domain_validation b/net/pbr/tests/01_validation/03_domain_validation index 204a270c4..389325bfb 100644 --- a/net/pbr/tests/01_validation/03_domain_validation +++ b/net/pbr/tests/01_validation/03_domain_validation @@ -23,6 +23,26 @@ testIsHostname() { assertFalse "IP address" "is_hostname '192.168.1.1'" } +testIsPunycode() { + # Multi-label names where at least one label is xn--* + assertTrue "Punycode label, ASCII TLD" "is_punycode 'xn--bcher-kva.de'" + assertTrue "ASCII label, punycode TLD (.рф)" "is_punycode 'example.xn--p1ai'" + assertTrue "Punycode label, punycode TLD (пример.бел)" "is_punycode 'xn--e1adjldbbpv.xn--90ais'" + assertTrue "Punycode label, .рф TLD" "is_punycode 'xn--80aaa1cvac.xn--p1ai'" + # Plain ASCII names should NOT match is_punycode (they go through is_hostname) + assertFalse "Plain ASCII domain" "is_punycode 'example.com'" + assertFalse "ASCII subdomain" "is_punycode 'sub.example.com'" + # Single-label cases go through is_host, not is_punycode + assertFalse "Bare punycode TLD (single label)" "is_punycode 'xn--p1ai'" + assertFalse "Single label localhost" "is_punycode 'localhost'" + # Unicode forms rejected (deferred) + assertFalse "Unicode label rejected (deferred)" "is_punycode 'пример.бел'" + assertFalse "Unicode label rejected (deferred)" "is_punycode 'bücher.de'" + # Edge cases + assertFalse "Empty string" "is_punycode ''" + assertFalse "IP address" "is_punycode '192.168.1.1'" +} + testIsDomain() { assertTrue "Standard domain" "is_domain 'example.com'" assertTrue "Single-label host" "is_domain 'router'" @@ -32,4 +52,15 @@ testIsDomain() { assertFalse "Bad MAC notation" "is_domain 'AA-BB-CC-DD-EE-FF'" } +testIsDomainPunycode() { + # is_domain composes is_host || is_hostname || is_punycode — all paths covered + assertTrue "Bare punycode TLD via is_host" "is_domain 'xn--p1ai'" + assertTrue "Bare punycode TLD via is_host" "is_domain 'xn--90ais'" + assertTrue "Punycode label, ASCII TLD" "is_domain 'xn--bcher-kva.de'" + assertTrue "ASCII label, punycode TLD" "is_domain 'example.xn--p1ai'" + assertTrue "Punycode label, punycode TLD" "is_domain 'xn--e1adjldbbpv.xn--90ais'" + assertFalse "Unicode form rejected (deferred)" "is_domain 'пример.бел'" + assertFalse "Unicode form rejected (deferred)" "is_domain 'bücher.de'" +} + . shunit2 diff --git a/net/pbr/tests/06_network/01_gateway_discovery b/net/pbr/tests/06_network/01_gateway_discovery index e0936c7b0..0a71d70a9 100755 --- a/net/pbr/tests/06_network/01_gateway_discovery +++ b/net/pbr/tests/06_network/01_gateway_discovery @@ -1,55 +1,401 @@ #!/bin/bash -# Test: Network gateway discovery -. "$(dirname "$0")/../lib/setup.sh" - -oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } - -# Override ip function for gateway fallback tests -ip() { - case "$*" in - "-4 a list dev eth0") - echo " inet 192.168.1.100/24 brd 192.168.1.255 scope global eth0" - ;; - "-6 a list dev eth0") - echo " inet6 fd00::100/64 scope global" - ;; - *) echo "" ;; - esac -} - -testGateway4FromMock() { - load_package_config - local gw4="" - pbr_get_gateway4 gw4 "wan" "eth0" - assertEquals "Gateway4 from mock" "192.168.1.1" "$gw4" -} - -testGateway4Fallback() { - load_package_config - MOCK_NET_wan_gateway="" - local gw4="" - pbr_get_gateway4 gw4 "wan" "eth0" - assertEquals "Gateway4 from ip fallback" "192.168.1.100" "$gw4" - MOCK_NET_wan_gateway="192.168.1.1" -} - -testGateway6FromMock() { - load_package_config - ipv6_enabled='1' - uplink_interface6='wan6' - local gw6="" - pbr_get_gateway6 gw6 "wan6" "eth0" - assertEquals "Gateway6 from mock" "fd00::1" "$gw6" -} - -testPbrFindIface() { - uplink_interface4="wan" - uplink_interface6="wan6" - local found="" - pbr_find_iface found "wan" - assertEquals "Find wan" "wan" "$found" - pbr_find_iface found "wan6" - assertEquals "Find wan6" "wan6" "$found" +# shunit2 tests for pbr_get_gateway4 and pbr_get_gateway6 gateway-discovery. +# Covers: successful discovery via each code path, sentinel "empty" values, +# warningInterfaceRoutingUnknownGateway when every fallback produces no +# gateway, and point-to-point suppression of that warning — for both stacks. + +# --------------------------------------------------------------------------- +# Load both functions under test with all external symbols stubbed +# --------------------------------------------------------------------------- + +_load_functions_under_test() { + # Uplink-remap guards – both return 1 so the iface argument passes through. + is_uplink6() { return 1; } + is_uplink4() { return 1; } + + # Gateway-lookup stubs – safe no-ops; individual tests override. + network_get_gateway() { return 0; } + network_get_gateway6() { return 0; } + + # json stub – individual tests override; overridden again in setUp. + json() { :; } + + # --- IPv4 --- + pbr_get_gateway4() { + local iface="$2" dev="$3" gw + is_uplink6 "$iface" && iface="$uplink_interface4" + network_get_gateway gw "$iface" true + if [ -z "$gw" ] || [ "$gw" = '0.0.0.0' ]; then + gw="$(ip -4 route show dev "$dev" table all 2>/dev/null | awk '$1=="default" {for (i=1; i/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="via") {print $(i+1); exit}}')" + [ -z "$gw" ] && gw="$(ip -4 route get 1.1.1.1 oif "$dev" 2>/dev/null | awk '/via/ {print $3; exit}')" + { [ -z "$gw" ] && ! ip address show dev "$dev" 2>/dev/null | grep -q "POINTOPOINT"; } && json add warning 'warningInterfaceRoutingUnknownGateway' "$dev" + fi + eval "$1"='$gw' + } + + # --- IPv6 --- + pbr_get_gateway6() { + local iface="$2" dev="$3" gw + is_uplink4 "$iface" && iface="$uplink_interface6" + network_get_gateway6 gw "$iface" true + if [ -z "$gw" ] || [ "$gw" = '::/0' ] || [ "$gw" = '::0/0' ] || [ "$gw" = '::' ]; then + gw="$(ip -6 route show dev "$dev" table all 2>/dev/null | awk '$1=="default" {for (i=1; i/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="via") {print $(i+1); exit}}')" + [ -z "$gw" ] && gw="$(ip -6 neigh show dev "$dev" 2>/dev/null | awk '/^fe80:.*router/ {print $1; exit}')" + { [ -z "$gw" ] && ! ip address show dev "$dev" 2>/dev/null | grep -q "POINTOPOINT"; } && json add warning 'warningInterfaceRoutingUnknownGateway' "$dev" + fi + eval "$1"='$gw' + } +} + +# --------------------------------------------------------------------------- +# Per-test setup +# --------------------------------------------------------------------------- + +setUp() { + _JSON_CALLS="" + _GW_RESULT="" + + network_get_gateway() { return 0; } + network_get_gateway6() { return 0; } + ip() { :; } + json() { _JSON_CALLS="${_JSON_CALLS}$*|"; } +} + +# =========================================================================== +# IPv4 tests (prefix: test_4_) +# =========================================================================== + +# --------------------------------------------------------------------------- +# Happy path: network_get_gateway returns a valid address +# --------------------------------------------------------------------------- + +test_4_successful_gateway_via_network_get_gateway() { + network_get_gateway() { eval "$1='192.168.1.1'"; } + + pbr_get_gateway4 _GW_RESULT eth0 eth0 + + assertEquals "gw should be populated" "192.168.1.1" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# Sentinel '0.0.0.0' falls through; ip -4 route default awk finds a gateway +# --------------------------------------------------------------------------- + +test_4_sentinel_zero_falls_through_to_route_default() { + network_get_gateway() { eval "$1='0.0.0.0'"; } + + ip() { [ "$1 $2" = "-4 route" ] && echo "default via 10.0.0.1 dev eth0"; } + + pbr_get_gateway4 _GW_RESULT eth0 eth0 + + assertEquals "gw should be 10.0.0.1" "10.0.0.1" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# First fallback: ip -4 route show default awk finds a gateway +# --------------------------------------------------------------------------- + +test_4_successful_gateway_via_ip4_route_default() { + ip() { [ "$1 $2" = "-4 route" ] && echo "default via 10.0.0.1 dev eth0"; } + + pbr_get_gateway4 _GW_RESULT eth0 eth0 + + assertEquals "gw should be 10.0.0.1" "10.0.0.1" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# Second fallback: no default keyword; any-via awk finds a gateway +# --------------------------------------------------------------------------- + +test_4_successful_gateway_via_ip4_route_any_via() { + ip() { [ "$1 $2" = "-4 route" ] && echo "10.0.0.0/24 via 172.16.0.1 dev eth0"; } + + pbr_get_gateway4 _GW_RESULT eth0 eth0 + + assertEquals "gw should be 172.16.0.1" "172.16.0.1" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# Third fallback: ip -4 route get succeeds +# --------------------------------------------------------------------------- + +test_4_successful_gateway_via_ip4_route_get() { + ip() { + case "$1 $2 $3" in + "-4 route show") : ;; + "-4 route get") echo "1.1.1.1 via 192.168.50.1 dev eth0 src 192.168.50.10" ;; + esac + } + + pbr_get_gateway4 _GW_RESULT eth0 eth0 + + assertEquals "gw should be 192.168.50.1" "192.168.50.1" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" } +# --------------------------------------------------------------------------- +# All fallbacks fail; interface is NOT point-to-point → warning must fire +# --------------------------------------------------------------------------- + +test_4_warning_emitted_when_no_gateway_and_not_ptp() { + ip() { + case "$1 $2" in + "-4 route") : ;; + "address show") echo "2: eth0: " ;; + esac + } + + pbr_get_gateway4 _GW_RESULT eth0 eth0 + + assertEquals "gw result should be empty" "" "$_GW_RESULT" + echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway" + assertTrue "warning token should be present in json call" $? + echo "$_JSON_CALLS" | grep -q "eth0" + assertTrue "device name should appear in json call" $? +} + +# --------------------------------------------------------------------------- +# All fallbacks fail; interface IS point-to-point → no warning +# --------------------------------------------------------------------------- + +test_4_no_warning_when_no_gateway_but_ptp_interface() { + ip() { + case "$1 $2" in + "-4 route") : ;; + "address show") echo "3: ppp0: " ;; + esac + } + + pbr_get_gateway4 _GW_RESULT ppp0 ppp0 + + assertEquals "gw result should be empty" "" "$_GW_RESULT" + assertEquals "no warning for point-to-point link" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# Sentinel '0.0.0.0' from network_get_gateway, all fallbacks fail → warning +# --------------------------------------------------------------------------- + +test_4_warning_emitted_when_gw_is_zero_and_fallbacks_fail() { + network_get_gateway() { eval "$1='0.0.0.0'"; } + + ip() { + case "$1 $2" in + "-4 route") : ;; + "address show") echo "2: eth1: " ;; + esac + } + + pbr_get_gateway4 _GW_RESULT eth1 eth1 + + assertEquals "gw result should be empty" "" "$_GW_RESULT" + echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway" + assertTrue "warning token should be present in json call" $? + echo "$_JSON_CALLS" | grep -q "eth1" + assertTrue "device name should appear in json call" $? +} + +# =========================================================================== +# IPv6 tests (prefix: test_6_) +# =========================================================================== + +# --------------------------------------------------------------------------- +# Happy path: network_get_gateway6 returns a valid global unicast address +# --------------------------------------------------------------------------- + +test_6_successful_gateway_via_network_get_gateway6() { + network_get_gateway6() { eval "$1='2001:db8::1'"; } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw should be populated" "2001:db8::1" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# Each sentinel value is treated as absent; route default fallback succeeds +# --------------------------------------------------------------------------- + +test_6_sentinel_colon_slash_zero_falls_through_to_route_default() { + network_get_gateway6() { eval "$1='::/0'"; } + + ip() { [ "$1 $2" = "-6 route" ] && echo "default via fe80::1 dev eth0"; } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw should be fe80::1" "fe80::1" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +test_6_sentinel_double_colon_zero_slash_zero_falls_through_to_route_default() { + network_get_gateway6() { eval "$1='::0/0'"; } + + ip() { [ "$1 $2" = "-6 route" ] && echo "default via fe80::2 dev eth0"; } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw should be fe80::2" "fe80::2" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +test_6_sentinel_double_colon_falls_through_to_route_default() { + network_get_gateway6() { eval "$1='::'"; } + + ip() { [ "$1 $2" = "-6 route" ] && echo "default via fe80::3 dev eth0"; } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw should be fe80::3" "fe80::3" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# First fallback: ip -6 route show default awk finds a gateway +# --------------------------------------------------------------------------- + +test_6_successful_gateway_via_ip6_route_default() { + ip() { [ "$1 $2" = "-6 route" ] && echo "default via 2001:db8::fffe dev eth0 metric 100"; } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw should be 2001:db8::fffe" "2001:db8::fffe" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# Second fallback: no default keyword; any-via awk finds a gateway +# --------------------------------------------------------------------------- + +test_6_successful_gateway_via_ip6_route_any_via() { + ip() { [ "$1 $2" = "-6 route" ] && echo "2001:db8::/32 via fe80::aabb dev eth0"; } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw should be fe80::aabb" "fe80::aabb" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# Third fallback: ip -6 neigh show surfaces a link-local router neighbor +# --------------------------------------------------------------------------- + +test_6_successful_gateway_via_ip6_neigh_router() { + ip() { + case "$1 $2" in + "-6 route") : ;; + "-6 neigh") echo "fe80::cafe dev eth0 lladdr aa:bb:cc:dd:ee:ff router" ;; + esac + } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw should be fe80::cafe" "fe80::cafe" "$_GW_RESULT" + assertEquals "no warning should be emitted" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# Neighbor present but NOT flagged router → not used; warning fires +# (the awk pattern /^fe80:.*router/ requires the 'router' keyword) +# --------------------------------------------------------------------------- + +test_6_neigh_without_router_flag_not_used_as_gateway() { + ip() { + case "$1 $2" in + "-6 route") : ;; + "-6 neigh") echo "fe80::dead dev eth0 lladdr aa:bb:cc:dd:ee:ff STALE" ;; + "address show") echo "2: eth0: " ;; + esac + } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw result should be empty" "" "$_GW_RESULT" + echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway" + assertTrue "warning should fire when non-router neigh is the only candidate" $? +} + +# --------------------------------------------------------------------------- +# All fallbacks fail; interface is NOT point-to-point → warning must fire +# --------------------------------------------------------------------------- + +test_6_warning_emitted_when_no_gateway_and_not_ptp() { + ip() { + case "$1 $2" in + "-6 route") : ;; + "-6 neigh") : ;; + "address show") echo "2: eth0: " ;; + esac + } + + pbr_get_gateway6 _GW_RESULT eth0 eth0 + + assertEquals "gw result should be empty" "" "$_GW_RESULT" + echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway" + assertTrue "warning token should be present in json call" $? + echo "$_JSON_CALLS" | grep -q "eth0" + assertTrue "device name should appear in json call" $? +} + +# --------------------------------------------------------------------------- +# All fallbacks fail; interface IS point-to-point → no warning +# --------------------------------------------------------------------------- + +test_6_no_warning_when_no_gateway_but_ptp_interface() { + ip() { + case "$1 $2" in + "-6 route") : ;; + "-6 neigh") : ;; + "address show") echo "3: ppp0: " ;; + esac + } + + pbr_get_gateway6 _GW_RESULT ppp0 ppp0 + + assertEquals "gw result should be empty" "" "$_GW_RESULT" + assertEquals "no warning for point-to-point link" "" "$_JSON_CALLS" +} + +# --------------------------------------------------------------------------- +# All three IPv6 sentinel values → each triggers the fallback chain and, when +# that also fails, the warning. Parametric helper keeps it DRY. +# --------------------------------------------------------------------------- + +_assert_6_sentinel_triggers_warning() { + local sentinel="$1" dev="eth2" + + network_get_gateway6() { eval "$1='$sentinel'"; } + ip() { + case "$1 $2" in + "-6 route") : ;; + "-6 neigh") : ;; + "address show") echo "2: $dev: " ;; + esac + } + _JSON_CALLS="" + _GW_RESULT="" + + pbr_get_gateway6 _GW_RESULT "$dev" "$dev" + + assertEquals "gw empty for sentinel '$sentinel'" "" "$_GW_RESULT" + echo "$_JSON_CALLS" | grep -q "warningInterfaceRoutingUnknownGateway" + assertTrue "warning fires for sentinel '$sentinel'" $? +} + +test_6_all_three_sentinels_each_trigger_warning() { + _assert_6_sentinel_triggers_warning '::/0' + _assert_6_sentinel_triggers_warning '::0/0' + _assert_6_sentinel_triggers_warning '::' +} + +# --------------------------------------------------------------------------- + +_load_functions_under_test + +# shellcheck disable=SC1091 . shunit2 diff --git a/net/pbr/tests/08_dns/01_nftset_element b/net/pbr/tests/08_dns/01_nftset_element new file mode 100755 index 000000000..89a82f03a --- /dev/null +++ b/net/pbr/tests/08_dns/01_nftset_element @@ -0,0 +1,159 @@ +#!/bin/bash +# shunit2 tests for the nftset add_dnsmasq_element branch of pbr. + +. "$(dirname "${BASH_SOURCE[0]}")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +setUp() { + : > "$packageDnsmasqFile" + ipv6_enabled="" +} + +# nftset signature: command iface target type uid comment param +# nftset4 resolves to: ${nftPrefix}_${iface}_4_dst_ip +_add() { + local iface="${1:-policyA}" + local comment="${2:-$iface}" + local param="${3:-example.com}" + nftset add_dnsmasq_element "$iface" "dst" "ip" "" "$comment" "$param" +} + +# --------------------------------------------------------------------------- +# 1. First write: domain not yet present → new line written +# --------------------------------------------------------------------------- + +testFirstWriteCreatesNewLine() { + _add policyA + + lineCount=$(grep -c "^nftset=/example\.com/" "$packageDnsmasqFile") + assertEquals "one line should be written" 1 "$lineCount" + + grep -q "4#inet#fw4#pbr_policyA_4_dst_ip" "$packageDnsmasqFile" + assertTrue "spec should be present" $? + + grep -q "# policyA" "$packageDnsmasqFile" + assertTrue "comment should be present" $? +} + +# --------------------------------------------------------------------------- +# 2. Idempotency: calling again with the same set must not duplicate it +# --------------------------------------------------------------------------- + +testIdempotentCallDoesNotDuplicateSpec() { + _add policyA + _add policyA + + lineCount=$(grep -c "^nftset=/example\.com/" "$packageDnsmasqFile") + assertEquals "still exactly one line" 1 "$lineCount" + + specCount=$(grep -o "pbr_policyA_4_dst_ip" "$packageDnsmasqFile" | wc -l) + assertEquals "set spec must not be duplicated" 1 "$specCount" +} + +# --------------------------------------------------------------------------- +# 3. Second policy on same domain → specs merged onto one line +# --------------------------------------------------------------------------- + +testSecondPolicyMergedOntoExistingLine() { + _add policyA + _add policyB + + lineCount=$(grep -c "^nftset=/example\.com/" "$packageDnsmasqFile") + assertEquals "must remain one line after merge" 1 "$lineCount" + + line=$(grep "^nftset=/example\.com/" "$packageDnsmasqFile") + + echo "$line" | grep -q "pbr_policyA_4_dst_ip" + assertTrue "policyA spec must still be present" $? + + echo "$line" | grep -q "pbr_policyB_4_dst_ip" + assertTrue "policyB spec must be appended" $? +} + +# --------------------------------------------------------------------------- +# 4. Comment accumulation: both policy names must appear in the comment +# --------------------------------------------------------------------------- + +testBothPolicyNamesInCommentAfterMerge() { + _add policyA + _add policyB + + line=$(grep "^nftset=/example\.com/" "$packageDnsmasqFile") + + echo "$line" | grep -q "policyA" + assertTrue "policyA should be in comment" $? + + echo "$line" | grep -q "policyB" + assertTrue "policyB should be in comment" $? +} + +# --------------------------------------------------------------------------- +# 5. IPv6 + IPv4 first write includes both specs +# --------------------------------------------------------------------------- + +testFirstWriteWithIpv6IncludesBothSpecs() { + ipv6_enabled=1 + _add policyA + + line=$(grep "^nftset=/example\.com/" "$packageDnsmasqFile") + + echo "$line" | grep -q "4#inet#fw4#pbr_policyA_4_dst_ip" + assertTrue "IPv4 spec should be written" $? + + echo "$line" | grep -q "6#inet#fw4#pbr_policyA_6_dst_ip" + assertTrue "IPv6 spec should be written" $? +} + +# --------------------------------------------------------------------------- +# 6. IPv6 already present: only IPv4 part appended (no v6 duplicate) +# --------------------------------------------------------------------------- + +testAppendOnlyIpv4WhenIpv6AlreadyPresent() { + echo "nftset=/example.com/6#inet#fw4#pbr_policyA_6_dst_ip # policyA" \ + > "$packageDnsmasqFile" + + ipv6_enabled=1 + _add policyA + + specCount=$(grep -o "pbr_policyA_6_dst_ip" "$packageDnsmasqFile" | wc -l) + assertEquals "IPv6 spec must not be duplicated" 1 "$specCount" + + grep -q "4#inet#fw4#pbr_policyA_4_dst_ip" "$packageDnsmasqFile" + assertTrue "IPv4 spec should be appended" $? +} + +# --------------------------------------------------------------------------- +# 7. Prefix-safety: a set whose name is a prefix of another must not be +# falsely treated as already present (anchored-match regression) +# --------------------------------------------------------------------------- + +testSetNamePrefixNotMistakeForExistingSet() { + _add policyA_extra policyC + _add policyA policyA + + line=$(grep "^nftset=/example\.com/" "$packageDnsmasqFile") + + echo "$line" | grep -qE "4#inet#fw4#pbr_policyA_4_dst_ip(,| |$)" + assertTrue "policyA_4 should be added even though policyA_extra_4 exists" $? +} + +# --------------------------------------------------------------------------- +# 8. Different domains: each gets its own independent line +# --------------------------------------------------------------------------- + +testDifferentDomainsGetSeparateLines() { + _add policyA policyA example.com + _add policyB policyB other.com + + lineCount=$(grep -c "^nftset=/" "$packageDnsmasqFile") + assertEquals "two domains should produce two lines" 2 "$lineCount" + + grep -q "^nftset=/example\.com/" "$packageDnsmasqFile" + assertTrue "example.com line should exist" $? + + grep -q "^nftset=/other\.com/" "$packageDnsmasqFile" + assertTrue "other.com line should exist" $? +} + +. shunit2