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 <stangri@melmac.ca>
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"
}
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<NF; i++) if ($i=="via") {print $(i+1); exit}}')" # use table all in case of netifd where default routes are not present in main table
+ # Fall back in case interfaces do not have a default route
+ [ -z "$gw" ] && gw="$(ip -4 route show dev "$dev" table all 2>/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<NF; i++) if ($i=="via") {print $(i+1); exit}}')" # use table all in case of netifd where default routes are not present in main table
+ # Fall back in case interfaces do not have a default route
+ [ -z "$gw" ] && gw="$(ip -6 route show dev "$dev" table all 2>/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
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"; }
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";;
;;
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
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
;;
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
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
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
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
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
}
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
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=""
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
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=""
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=""
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
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
;;
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)
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:-<empty>}', previous='${prevGwIpv6:-<empty>}'\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
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'
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'
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
}
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'"
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
#!/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<NF; i++) if ($i=="via") {print $(i+1); exit}}')"
+ [ -z "$gw" ] && gw="$(ip -4 route show dev "$dev" table all 2>/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<NF; i++) if ($i=="via") {print $(i+1); exit}}')"
+ [ -z "$gw" ] && gw="$(ip -6 route show dev "$dev" table all 2>/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: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
+ 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: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP>" ;;
+ 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: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
+ 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: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
+ 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: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
+ 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: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP>" ;;
+ 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: <BROADCAST,MULTICAST,UP,LOWER_UP>" ;;
+ 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
--- /dev/null
+#!/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