#!/bin/bash # 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