From dde3a694f0a15ac00b07bb9982c1558acbf65437 Mon Sep 17 00:00:00 2001 From: Stan Grishin Date: Wed, 25 Feb 2026 02:31:00 +0000 Subject: pbr: update to 1.2.2-r6 Update pbr from 1.2.1-r87 to 1.2.2-r6. This release adds mwan4 (Multi-WAN) integration, a diagnostic `support` command, IPv6 lease-to-nftset handling, improved split-uplink detection, stricter UCI validation, shell variable quoting fixes across 30+ locations, and a comprehensive 126-case test suite with a full mock OpenWrt sysroot. Signed-off-by: Stan Grishin --- - **31 files changed**, +1,745 / -227 lines (net +1,518) - **1 commit**: `61c8923` — `pbr: update to 1.2.2-r6` --- - Version bumped from `1.2.1-r87` to `1.2.2-r6` - URL updated from `github.com/stangri/pbr/` to `github.com/mossdef-org/pbr/` - No dependency changes --- Three options changed from scalar to list type: | Option | Old Type | New Type | |---------------------|----------|----------| | `ignored_interface` | `option` | `list` | | `lan_device` | `option` | `list` | | `resolver_instance` | `option` | `list` | Options reordered: scalars first, then lists, matching UCI convention. No values changed. --- The init script (`/etc/init.d/pbr`) received significant additions and fixes across ~660 lines (+443/-218). Bumped from `24` to `25`. **mwan4 (Multi-WAN) Integration (8 new functions):** - `mwan4_is_installed()` — Detect mwan4 package - `mwan4_is_running()` — Check service status - `mwan4_get_iface_list()` — Get enabled interfaces - `mwan4_get_strategy_list()` — Get strategies - `mwan4_get_iface_mark_chain()` — Get nft mark chain for interface - `mwan4_get_iface_nft_sets()` — Get nftset names - `mwan4_get_strategy_chain()` — Get strategy chain - `mwan4_get_mmx_mask()` — Get Multi-WAN mark mask Enables PBR to coordinate with mwan4 for combined policy routing and multi-WAN failover. **Diagnostic `support` Command:** - New `support()` function generates masked diagnostic output for troubleshooting - `print_config_masked()` redacts sensitive data (passwords, keys, tokens, PSKs, endpoints) while preserving IP addresses and structure **IPv6 Lease Handling:** - New `ipv6_leases_to_nftset()` parses DHCPv6 leases from `/tmp/hosts/odhcpd` - Complements existing `ipv4_leases_to_nftset()` **Split Uplink Detection (3 new functions):** - `is_uplink4()` — Check IPv4 uplink interface - `is_uplink6()` — Check IPv6 uplink interface - `is_uplink()` — Unified check (v4 or v6) - New `ipv6_default_lookup` variable for split IPv4/IPv6 uplink routing table assignment **ubus Integration:** - New `ubus_get_interface()` queries PBR gateway data via ubus **Shell Variable Quoting (30+ locations):** Systematic conversion of bare variable references to brace-quoted syntax throughout the script: - `$2` to `${2}` in string replacements - `$_ret` to `${_ret}` in conditional expansions - `$_mark` to `${_mark}` in nft rule generation - `$nftset6` to `${nftset6}` in dnsmasq rules - `$nft_set_timeout` to `${nft_set_timeout}` - `$xrayIfacePrefix` to `${xrayIfacePrefix}` - And many more across rule generation, output strings, and conditional expressions **Specific Fixes:** - `pbr_get_gateway6()`: Changed `is_wan` to `is_uplink4` for correct IPv4 uplink detection - `is_netifd_interface()`: Now checks both `ip4table` and `ip6table` (was IPv4 only) - `load_environment()`: Fixed inverted flag check (`-z` changed to `-n` for `loadEnvironmentFlag`) - Dnsmasq instance detection: Fixed UCI section lookup with proper variable handling - Help text URL: `#WarningMessagesDetails` changed to `#warning-messages-details` (kebab-case) - `uplink_ip_rules_priority`: Changed from `uinteger` to `range(99,32765)` to enforce valid Linux routing policy DB bounds Three options now use `config_get_list` instead of `config_get` to support multiple values: - `ignored_interface` - `lan_device` - `resolver_instance` **Rule Cleanup Refactored:** - Replaced complex awk-based rule parsing with priority-range approach - Calculates `prio_min = priority - max_ifaces` and `prio_max = priority`, iterates and deletes rules within range - Skips netifd-managed fwmark rules - Added legacy rule cleanup for `suppress_prefixlength` entries **Firewall Sync:** - Added `fw4 -q reload` after successful nft file installation to ensure fw4 state synchronizes with PBR's nftables changes **Resolver Instance Handling:** - Added robustness checks in `_dnsmasq_instance_config()`: file existence check and instance validity check - Better section name resolution with UCI query - Added missing `setup` parameter in resolver instance setup calls - `uci_get_device()` — Replaced with inline call - `uci_get_protocol()` — Replaced with inline call --- In `70-pbr`, fixed shell variable quoting: ```sh ${DEVICE:+ ($DEVICE)} ${DEVICE:+ (${DEVICE})} ``` --- In `pbr.user.netflix`, fixed two instances of bare variable expansion in parameter substitution: ```sh params="${params:+$params, }${p}" params="${params:+${params}, }${p}" ``` --- A full test suite is added in `net/pbr/tests/` (21 new files, ~1,300 lines) using the shunit2 framework with a complete mock OpenWrt sysroot. **Runner (`run_tests.sh`):** - Discovers test files via glob pattern - Supports pattern-based filtering via CLI arg - Executes each test in isolated bash subprocess - Captures output, reports pass/fail with color - Accumulates stats and lists failures at end - Requires `shunit2` package **Setup (`lib/setup.sh`):** - Creates temporary mock sysroot (`$MOCK_ROOT`) - Sets `IPKG_INSTROOT` for OpenWrt path resolution - Installs mock libraries, configs, and binaries - Stubs `rc.common`, procd, logger, resolveip, jsonfilter, pidof, sync - Sources pbr init script with `readonly` keyword stripped (allows test overrides) - Redirects all file paths to temp directories **UCI Config API (`lib/mocks/functions.sh`):** - Full `config_load` parser for UCI syntax - `config_get`, `config_get_bool`, `config_get_list`, `config_foreach`, `config_list_foreach` - `uci_set`, `uci_get`, `uci_add_list`, `uci_remove`, `uci_remove_list`, `uci_commit` - Stores state in associative arrays **Network API (`lib/mocks/network.sh`):** - `network_get_device`, `network_get_physdev`, `network_get_gateway`, `network_get_gateway6`, `network_get_protocol`, `network_get_ipaddr`, `network_get_ip6addr`, `network_get_dnsserver`, `network_flush_cache` - Backed by `MOCK_NET_*` variables that tests override to simulate different network states - Pre-configured: wan (eth0/dhcp/192.168.1.1), wan6 (eth0/dhcpv6/fd00::1), wg0 (wireguard), lan (br-lan/static), loopback (lo/static) **JSON Shell (`lib/mocks/jshn.sh`):** - Minimal JSON-in-shell implementation - `json_init`, `json_add_string/boolean/int`, `json_add_object/array`, `json_close_*`, `json_select`, `json_get_var`, `json_get_keys`, `json_dump`, `json_load` - Associative array backend with path tracking **Mock Binaries:** - `nft` — Returns fw4 table structure with standard chains (input, forward, output, dstnat, mangle_*); passes syntax checks - `dnsmasq` — Reports version with nftset support - `readlink` — Returns `/usr/libexec/ip-full` for `*/sbin/ip` (simulates ip-full installed) **Mock UCI Configs:** - `pbr` — Full config: enabled, policies (vpn_all, vpn_gaming, disabled_policy), dns_policy, nft settings, interface lists - `network` — Interfaces: loopback, lan, wan, wan6, wg0 (wireguard) - `firewall` — Zones: lan (accept all), wan (reject input/forward) - `dhcp` — DHCP server stub - `system` — Hostname and timezone **01_validation — Input Validation (67 cases):** `01_ipv4_validation` (13 cases): - Valid IPs: 192.168.1.1, 10.0.0.1, 172.16.0.1 - Valid CIDR: /8, /24, /32, /0 - Invalid: octets >255, wrong octet count, CIDR >32, IPv6 addresses, domain names `02_ipv6_validation` (21 cases): - Valid: ::1, fe80::1, 2001:db8::1, fd00::1, full addresses, ::/0 - Invalid: IPv4 addrs, plain strings, MACs - Scope detection: global (2001:db8::/32), link-local (fe80::/10), ULA (fd00::/8) `03_domain_validation` (8 cases): - Host: single labels (router, host123) - Hostname: multi-label (example.com, sub.example.com, deep.sub.example.com) - Domain: FQDN or single-label - Invalid: IPs, empty strings, MAC notation `04_misc_validators` (25 cases): - MAC addresses (colon notation, case variants) - Integer validation (positive, not negative) - Negation marker (! prefix detection) - URL schemes (http, https, ftp, file://) - Version comparison (is_greater, is_greater_or_equal) - Family mismatch (IPv4/IPv6 mixing detection) **02_string_utils — String Functions (8 cases):** `01_str_functions`: - `str_contains` — Substring search - `str_contains_word` — Word-boundary search - `str_to_lower` / `str_to_upper` — Case convert - `str_first_word` — Token extraction - `str_replace` — String substitution - `str_extras_to_underscore` — Normalize delims - `str_extras_to_space` — Expand delimiters **03_wan_detection — Interface Detection (13 cases):** `01_wan_types`: - `is_wan4` — Detects wan/wanX, not wan6/lan/wg0 - `is_wan6` — Detects wan6/mwan6 (IPv6-aware) - `is_wan6_disabled` — Disabled when ipv6 off - `is_wan` — Unified v4+v6 detection - `is_uplink4` / `is_uplink6` — Uplink detection - `is_tor` — Case-insensitive tor detection - `is_ignore_target` — Ignore target detection - `is_list` — Comma/space list vs single value **04_config — Configuration Loading (13 cases):** `01_load_config` (7 cases): - Default values from UCI config - Hex value parsing (fw_mask, uplink_mark) - XOR calculation (fw_maskXor = ~fw_mask) - List parsing (ignored_interface, resolver) - nft parameters (auto-merge, flags) - Config-loaded flag tracking `02_disabled_service` (2 cases): - Disabled: enabled option becomes unset - Enabled: enabled option is set `03_config_ipv6` (4 cases): - IPv6 enabled: config and uplink interface set - IPv6 disabled: both unset - Reload behavior verification **05_nft — nftables Integration (14 cases):** `01_nft_file_operations` (8 cases): - File creation with nft shebang - Chain creation (dstnat, forward, output, prerouting) - Jump rules and guard rules - File append, content search, file deletion `02_nft_check_element` (6 cases): - fw4 table existence - Chain existence (input, forward, output, dstnat, mangle_*) - Non-existent chain detection **06_network — Network Functions (11 cases):** `01_gateway_discovery` (4 cases): - IPv4 gateway from mock (192.168.1.1) - IPv4 gateway fallback (ip addr parsing) - IPv6 gateway from mock (fd00::1) - Interface finding for uplinks `02_supported_interfaces` (7 cases): - Ignored: loopback in ignored list - LAN detection vs non-LAN - Uplink support (wan is supported) - LAN/loopback not supported - Wireguard supported (wg0) - Explicit custom interface support --- ```sh cd net/pbr/tests && sh run_tests.sh ``` Requires: `bash`, `shunit2`. Optional filter: `sh run_tests.sh 01_validation` Signed-off-by: Stan Grishin --- net/pbr/Makefile | 6 +- net/pbr/files/etc/config/pbr | 6 +- net/pbr/files/etc/hotplug.d/iface/70-pbr | 2 +- net/pbr/files/etc/init.d/pbr | 661 +++++++++++++++-------- net/pbr/files/usr/share/pbr/pbr.user.netflix | 4 +- net/pbr/tests/01_validation/01_ipv4_validation | 37 ++ net/pbr/tests/01_validation/02_ipv6_validation | 47 ++ net/pbr/tests/01_validation/03_domain_validation | 35 ++ net/pbr/tests/01_validation/04_misc_validators | 71 +++ net/pbr/tests/02_string_utils/01_str_functions | 61 +++ net/pbr/tests/03_wan_detection/01_wan_types | 72 +++ net/pbr/tests/04_config/01_load_config | 58 ++ net/pbr/tests/04_config/02_disabled_service | 28 + net/pbr/tests/04_config/03_config_ipv6 | 31 ++ net/pbr/tests/05_nft/01_nft_file_operations | 64 +++ net/pbr/tests/05_nft/02_nft_check_element | 30 + net/pbr/tests/06_network/01_gateway_discovery | 55 ++ net/pbr/tests/06_network/02_supported_interfaces | 48 ++ net/pbr/tests/lib/mocks/functions.sh | 161 ++++++ net/pbr/tests/lib/mocks/jshn.sh | 138 +++++ net/pbr/tests/lib/mocks/network.sh | 61 +++ net/pbr/tests/lib/setup.sh | 86 +++ net/pbr/tests/mocks/bin/dnsmasq | 11 + net/pbr/tests/mocks/bin/nft | 31 ++ net/pbr/tests/mocks/bin/readlink | 11 + net/pbr/tests/mocks/etc/config/dhcp | 9 + net/pbr/tests/mocks/etc/config/firewall | 20 + net/pbr/tests/mocks/etc/config/network | 21 + net/pbr/tests/mocks/etc/config/pbr | 52 ++ net/pbr/tests/mocks/etc/config/system | 3 + net/pbr/tests/run_tests.sh | 52 ++ 31 files changed, 1745 insertions(+), 227 deletions(-) create mode 100644 net/pbr/tests/01_validation/01_ipv4_validation create mode 100644 net/pbr/tests/01_validation/02_ipv6_validation create mode 100644 net/pbr/tests/01_validation/03_domain_validation create mode 100755 net/pbr/tests/01_validation/04_misc_validators create mode 100755 net/pbr/tests/02_string_utils/01_str_functions create mode 100755 net/pbr/tests/03_wan_detection/01_wan_types create mode 100755 net/pbr/tests/04_config/01_load_config create mode 100755 net/pbr/tests/04_config/02_disabled_service create mode 100755 net/pbr/tests/04_config/03_config_ipv6 create mode 100755 net/pbr/tests/05_nft/01_nft_file_operations create mode 100755 net/pbr/tests/05_nft/02_nft_check_element create mode 100755 net/pbr/tests/06_network/01_gateway_discovery create mode 100755 net/pbr/tests/06_network/02_supported_interfaces create mode 100644 net/pbr/tests/lib/mocks/functions.sh create mode 100644 net/pbr/tests/lib/mocks/jshn.sh create mode 100644 net/pbr/tests/lib/mocks/network.sh create mode 100644 net/pbr/tests/lib/setup.sh create mode 100644 net/pbr/tests/mocks/bin/dnsmasq create mode 100644 net/pbr/tests/mocks/bin/nft create mode 100644 net/pbr/tests/mocks/bin/readlink create mode 100644 net/pbr/tests/mocks/etc/config/dhcp create mode 100644 net/pbr/tests/mocks/etc/config/firewall create mode 100644 net/pbr/tests/mocks/etc/config/network create mode 100644 net/pbr/tests/mocks/etc/config/pbr create mode 100644 net/pbr/tests/mocks/etc/config/system create mode 100644 net/pbr/tests/run_tests.sh diff --git a/net/pbr/Makefile b/net/pbr/Makefile index f2af31b582..53ab64311d 100644 --- a/net/pbr/Makefile +++ b/net/pbr/Makefile @@ -4,8 +4,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=pbr -PKG_VERSION:=1.2.1 -PKG_RELEASE:=87 +PKG_VERSION:=1.2.2 +PKG_RELEASE:=6 PKG_LICENSE:=AGPL-3.0-or-later PKG_MAINTAINER:=Stan Grishin @@ -16,7 +16,7 @@ define Package/pbr CATEGORY:=Network SUBMENU:=Routing and Redirection TITLE:=Policy Based Routing Service with nft/nft set support - URL:=https://github.com/stangri/pbr/ + URL:=https://github.com/mossdef-org/pbr/ PKGARCH:=all DEPENDS:= \ +ip-full \ diff --git a/net/pbr/files/etc/config/pbr b/net/pbr/files/etc/config/pbr index 2c216bb509..f6532e39eb 100644 --- a/net/pbr/files/etc/config/pbr +++ b/net/pbr/files/etc/config/pbr @@ -1,9 +1,7 @@ config pbr 'config' option enabled '0' option fw_mask '00ff0000' - list ignored_interface 'vpnserver' option ipv6_enabled '0' - option lan_device 'br-lan' option nft_rule_counter '0' option nft_set_auto_merge '1' option nft_set_counter '0' @@ -13,7 +11,6 @@ config pbr 'config' option nft_user_set_counter '0' option procd_boot_trigger_delay '5000' option procd_reload_delay '0' - list resolver_instance '*' option resolver_set 'dnsmasq.nftset' option strict_enforcement '1' option uplink_interface 'wan' @@ -21,6 +18,9 @@ config pbr 'config' option uplink_ip_rules_priority '30000' option uplink_mark '00010000' option verbosity '2' + list ignored_interface 'vpnserver' + list lan_device 'br-lan' + list resolver_instance '*' list webui_supported_protocol 'all' list webui_supported_protocol 'tcp' list webui_supported_protocol 'udp' diff --git a/net/pbr/files/etc/hotplug.d/iface/70-pbr b/net/pbr/files/etc/hotplug.d/iface/70-pbr index 5340ba5c03..002ddb7d11 100644 --- a/net/pbr/files/etc/hotplug.d/iface/70-pbr +++ b/net/pbr/files/etc/hotplug.d/iface/70-pbr @@ -1,6 +1,6 @@ #!/bin/sh # shellcheck disable=SC1091,SC3060 if [ -x /etc/init.d/pbr ] && /etc/init.d/pbr enabled; then - logger -t pbr "Sending reload signal to pbr for $INTERFACE due to $ACTION of $INTERFACE${DEVICE:+ ($DEVICE)}" + logger -t pbr "Sending reload signal to pbr for $INTERFACE due to $ACTION of $INTERFACE${DEVICE:+ (${DEVICE})}" /etc/init.d/pbr on_interface_reload "$INTERFACE" "$ACTION" fi diff --git a/net/pbr/files/etc/init.d/pbr b/net/pbr/files/etc/init.d/pbr index c846e73144..f9a9c42301 100755 --- a/net/pbr/files/etc/init.d/pbr +++ b/net/pbr/files/etc/init.d/pbr @@ -18,21 +18,25 @@ if type extra_command >/dev/null 2>&1; then Use '-p' option to automatically upload data under PBR paste.ee account WARNING: while paste.ee uploads are unlisted, they are still publicly available List domain names after options to include their lookup in report" + extra_command 'support' "Show diagnostic info and mask sensitive data" extra_command 'version' "Show version information" else # shellcheck disable=SC2034 - EXTRA_COMMANDS='netifd on_interface_reload status version' + EXTRA_COMMANDS='help netifd on_interface_reload status version' # shellcheck disable=SC2034 - EXTRA_HELP=" status Generates output required to troubleshoot routing issues - Use '-d' option for more detailed output - Use '-p' option to automatically upload data under PBR paste.ee account - WARNING: while paste.ee uploads are unlisted, they are still publicly available - List domain names after options to include their lookup in report" + EXTRA_HELP=" +\tstatus\tGenerates output required to troubleshoot routing issues +\t\tUse '-d' option for more detailed output +\t\tUse '-p' option to automatically upload data under PBR paste.ee account +\t\t\tWARNING: while paste.ee uploads are unlisted, they are still publicly available +\t\tList domain names after options to include their lookup in report +\tsupport\tShow diagnostic info and mask sensitive data +" fi readonly packageName='pbr' readonly PKG_VERSION='dev-test' -readonly packageCompat='24' +readonly packageCompat='25' readonly serviceName="$packageName $PKG_VERSION" readonly packageConfigFile="/etc/config/${packageName}" readonly packageDebugFile="/var/run/${packageName}.debug" @@ -201,8 +205,8 @@ resolverWorkingFlag= # shellcheck disable=SC1091 . "${IPKG_INSTROOT}/usr/share/libubox/jshn.sh" -debug() { local i j; for i in "$@"; do eval "j=\$$i"; logger "${packageName:+-t $packageName}" "${i}: ${j} "; done; } -str_contains() { [ "${1//$2}" != "$1" ]; } +debug() { local i j; for i in "$@"; do eval "j=\$$i"; logger "${packageName:+-t ${packageName}}" "${i}: ${j} "; done; } +str_contains() { [ "${1//${2}}" != "$1" ]; } str_contains_word() { echo "$1" | grep -qw "$2"; } str_extras_to_underscore() { echo "$1" | sed -E 's/[\. ~`!@#$%^&*()+=,<>?;:\/\\-]/_/g; s/_+/_/g'; } str_extras_to_space() { echo "$1" | tr ',;{}' ' '; } @@ -210,7 +214,7 @@ str_first_value_interface() { local i; for i in $1; do is_supported_interface "$ str_first_value_ipv4() { local i; for i in $1; do is_ipv4 "$i" && { echo "$i"; break; }; done; } str_first_value_ipv6() { local i; for i in $1; do is_ipv6 "$i" && { echo "$i"; break; }; done; } str_first_word() { echo "${1%% *}"; } -str_replace() { echo "${1//$2/$3}"; } +str_replace() { echo "${1//${2}/${3}}"; } str_to_dnsmasq_nftset() { echo "$1" | tr ' ' '/'; } str_to_lower() { echo "$1" | tr 'A-Z' 'a-z'; } str_to_upper() { echo "$1" | tr 'a-z' 'A-Z'; } @@ -262,7 +266,7 @@ pbr_get_gateway4() { } pbr_get_gateway6() { local iface="$2" dev="$3" gw - is_wan "$iface" && iface="$uplink_interface6" + 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}')" @@ -277,7 +281,7 @@ filter_options() { opt="${opt/_negative}" fi eval "is_$opt" "${v/\!}" || continue - _ret="${_ret:+$_ret }$v" + _ret="${_ret:+${_ret} }$v" done echo "$_ret" return 0 @@ -285,7 +289,7 @@ filter_options() { inline_set() { local value="$1" inline_set i for i in $value; do - inline_set="${inline_set:+$inline_set, }${i#[@\!]}" + inline_set="${inline_set:+${inline_set}, }${i#[@\!]}" done echo "$inline_set" } @@ -301,13 +305,6 @@ is_config_enabled() { config_foreach _check_config "$cfg" return "$_cfg_enabled" } -uci_get_device() { - local __tmp - __tmp="$(uci_get 'network' "$2" 'device')" - [ -z "$__tmp" ] && unset "$1" && return 1 - eval "$1=$__tmp" -} -uci_get_protocol() { uci_get 'network' "$1" 'proto'; } is_default_dev() { [ "$1" = "$(ip -4 route show default | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1);exit}}')" ]; } is_netifd_interface_default() { is_netifd_interface "$1" || return 1 @@ -339,20 +336,20 @@ is_mac_address() { echo "$1" | grep -qE '^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$' is_mac_address_bad_notation() { echo "$1" | grep -qE '^([0-9A-Fa-f]{2}-){5}([0-9A-Fa-f]{2})$'; } is_negated() { [ "${1:0:1}" = '!' ]; } is_netifd_table() { grep -q "ip.table.*$1" /etc/config/network; } -is_netifd_interface() { local iface="$1"; [ -n "$(uci_get 'network' "$iface" 'ip4table')" ]; } +is_netifd_interface() { local iface="$1"; [ -n "$(uci_get 'network' "$iface" 'ip4table')" ] || [ -n "$(uci_get 'network' "$iface" 'ip6table')" ]; } is_oc() { local p; network_get_protocol p "$1"; [ "${p:0:11}" = "openconnect" ]; } is_ovpn() { local d; uci_get_device d "$1"; [ "${d:0:3}" = "tun" ] || [ "${d:0:3}" = "tap" ] || [ -f "/sys/devices/virtual/net/${d}/tun_flags" ]; } is_ovpn_valid() { local dev_net dev_ovpn; uci_get_device dev_net "$1"; dev_ovpn="$(uci_get 'openvpn' "$1" 'dev')"; [ -n "$dev_net" ] && [ -n "$dev_ovpn" ] && [ "$dev_net" = "$dev_ovpn" ]; } -is_phys_dev(){ [ "${1:0:1}" = "@" ] && [ -L "/sys/class/net/${1#@}" ]; } +is_phys_dev() { [ "${1:0:1}" = "@" ] && [ -L "/sys/class/net/${1#@}" ]; } is_present() { command -v "$1" >/dev/null 2>&1; } is_service_running() { is_service_running_nft; } is_service_running_nft() { [ -x "$nft" ] && [ -n "$(get_mark_nft_chains)" ]; } is_supported_iface_dev() { local n dev; for n in $ifacesSupported; do network_get_device dev "$n"; [ "$1" = "$dev" ] && return 0; done; return 1; } -is_supported_protocol(){ grep -qi "^${1:--}" /etc/protocols;} +is_supported_protocol() { grep -qi "^${1:--}" /etc/protocols;} is_pptp() { local p; network_get_protocol p "$1"; [ "${p:0:4}" = "pptp" ]; } is_softether() { local d; network_get_device d "$1"; [ "${d:0:4}" = "vpn_" ]; } is_split_uplink() { [ -n "$ipv6_enabled" ] && [ "$uplink_interface4" != "$uplink_interface6" ]; } -is_supported_interface() { { is_lan "$1" || is_disabled_interface "$1"; } && return 1; str_contains_word "$supported_interface" "$1" || { ! is_ignored_interface "$1" && { is_wan "$1" || is_wan6 "$1" || is_tunnel "$1"; }; } || is_ignore_target "$1" || is_xray "$1"; } +is_supported_interface() { { is_lan "$1" || is_disabled_interface "$1"; } && return 1; str_contains_word "$supported_interface" "$1" || { ! is_ignored_interface "$1" && { is_uplink "$1" || is_wan "$1" || is_tunnel "$1"; }; } || is_ignore_target "$1" || is_xray "$1"; } is_netbird() { local d; network_get_device d "$1"; [ "${d:0:2}" = "wt" ]; } is_tailscale() { local d; network_get_device d "$1"; [ "${d:0:9}" = "tailscale" ]; } is_tor() { [ "$(str_to_lower "$1")" = "tor" ]; } @@ -364,8 +361,12 @@ is_url_file() { [ "$1" != "${1#file://}" ]; } is_url_ftp() { [ "$1" != "${1#ftp://}" ]; } is_url_http() { [ "$1" != "${1#http://}" ]; } is_url_https() { [ "$1" != "${1#https://}" ]; } -is_wan() { [ "$1" = "$uplink_interface4" ]; } -is_wan6() { [ -n "$ipv6_enabled" ] && [ "$1" = "$uplink_interface6" ]; } +is_uplink4() { [ "$1" = "$uplink_interface4" ]; } +is_uplink6() { [ -n "$ipv6_enabled" ] && [ "$1" = "$uplink_interface6" ]; } +is_uplink() { is_uplink4 "$1" || is_uplink6 "$1"; } +is_wan6() { [ -n "$ipv6_enabled" ] || return 1; case "$1" in wan*6|*wan6) return 0;; *) return 1;; esac; } +is_wan4() { case "$1" in wan*6|*wan6) return 1;; wan*|*wan) return 0;; *) return 1;; esac; } +is_wan() { is_wan4 "$1" || is_wan6 "$1"; } is_wg() { local p lp; network_get_protocol p "$1"; uci_get_listen_port lp "$1"; [ -z "$lp" ] && [ "${p:0:9}" = "wireguard" ]; } is_wg_server() { local p lp; network_get_protocol p "$1"; uci_get_listen_port lp "$1"; [ -n "$lp" ] && [ "${p:0:9}" = "wireguard" ]; } is_xray() { [ -n "$(get_xray_traffic_port "$1")" ]; } @@ -378,7 +379,7 @@ get_ss_traffic_ports() { local i="$(jsonfilter -i "$ssConfigFile" -q -e "@.inbou get_tor_dns_port() { local i="$(grep -m1 DNSPort "$torConfigFile" | awk -F: '{print $2}')"; echo "${i:-9053}"; } # shellcheck disable=SC2155 get_tor_traffic_port() { local i="$(grep -m1 TransPort "$torConfigFile" | awk -F: '{print $2}')"; echo "${i:-9040}"; } -get_xray_traffic_port() { local i="${1//$xrayIfacePrefix}"; [ "$i" = "$1" ] && unset i; echo "$i"; } +get_xray_traffic_port() { local i="${1//${xrayIfacePrefix}}"; [ "$i" = "$1" ] && unset i; echo "$i"; } get_rt_tables_id() { local iface="$1"; grep "${ipTablePrefix}_${iface}\$" "$rtTablesFile" | awk '{print $1;}'; } get_rt_tables_next_id() { echo "$(($(sort -r -n "$rtTablesFile" | grep -o -E -m 1 "^[0-9]+")+1))"; } get_rt_tables_non_pbr_next_id() { echo "$(($(grep -v "${ipTablePrefix}_" "$rtTablesFile" | sort -r -n | grep -o -E -m 1 "^[0-9]+")+1))"; } @@ -387,17 +388,25 @@ resolveip_to_nftset() { resolver 'wait' && resolveip "$@" | sed -n 'H;${x;s/\n/, resolveip_to_nftset4() { resolveip_to_nftset -4 "$@"; } resolveip_to_nftset6() { [ -n "$ipv6_enabled" ] && resolveip_to_nftset -6 "$@"; } # shellcheck disable=SC2016 -ipv4_leases_to_nftset(){ [ -s '/tmp/dhcp.leases' ] && awk -v arg="$1" 'BEGIN{fs=""};$0~arg{printf fs$3;fs=","}' /tmp/dhcp.leases;} +ipv4_leases_to_nftset() { [ -s '/tmp/dhcp.leases' ] && awk -v arg="$1" 'BEGIN{fs=""};$0~arg{printf fs$3;fs=","}' /tmp/dhcp.leases;} # shellcheck disable=SC2016 -ipv6_leases_to_nftset(){ [ -s '/tmp/hosts/odhcpd' ] && awk -v arg="$1" 'BEGIN{fs=""};$0~arg{printf fs$1;fs=","}' /tmp/hosts/odhcpd;} +ipv6_leases_to_nftset() { [ -s '/tmp/hosts/odhcpd' ] && awk -v arg="$1" 'BEGIN{fs=""};$0~arg{printf fs$1;fs=","}' /tmp/hosts/odhcpd;} # shellcheck disable=SC3037 ports_to_nftset() { echo -en "$1"; } get_mark_nft_chains() { "$nft" list table inet "$nftTable" 2>/dev/null | grep chain | grep "${nftPrefix}_mark_" | awk '{ print $2 }'; } get_nft_sets() { "$nft" list table inet "$nftTable" 2>/dev/null | grep 'set' | grep "${nftPrefix}_" | awk '{ print $2 }'; } __ubus_get() { ubus call service list "{ 'name': '$packageName' }" | jsonfilter -e "$1"; } ubus_get_status() { __ubus_get "@.${packageName}.instances.main.data.status.${1}"; } -ubus_get_interface() { __ubus_get "@.${packageName}.instances.main.data.gateways[@.name='${1}']${2:+.$2}"; } +ubus_get_interface() { __ubus_get "@.${packageName}.instances.main.data.gateways[@.name='${1}']${2:+.${2}}"; } ubus_get_gateways() { __ubus_get "@.${packageName}.instances.main.data.gateways"; } +config_get_list() { config_get "$@"; } +uci_get_device() { + local __tmp + __tmp="$(uci_get 'network' "$2" 'device')" + [ -z "$__tmp" ] && unset "$1" && return 1 + eval "$1=$__tmp" +} +uci_get_protocol() { uci_get 'network' "$1" 'proto'; } uci_add_list_if_new() { local PACKAGE="$1" local CONFIG="$2" @@ -415,7 +424,7 @@ uci_changes() { local CONFIG="$2" local OPTION="$3" [ -s "${UCI_CONFIG_DIR:-/etc/config/}${PACKAGE}" ] && \ - [ -n "$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} changes "$PACKAGE${CONFIG:+.$CONFIG}${OPTION:+.$OPTION}")" ] + [ -n "$(/sbin/uci ${UCI_CONFIG_DIR:+-c ${UCI_CONFIG_DIR}} changes "$PACKAGE${CONFIG:+.${CONFIG}}${OPTION:+.${OPTION}}")" ] } uci_get_listen_port() { local __tmp @@ -437,6 +446,115 @@ check_dnsmasq_nftset() { [ -z "$dnsmasq_features" ] && dnsmasq_features="$(dnsmasq --version | grep -m1 'Compile time options:' | cut -d: -f2) " [ "${dnsmasq_features#* nftset }" != "$dnsmasq_features" ] } + +# mwan4 detection and integration functions +mwan4_is_installed() { + [ -x /etc/init.d/mwan4 ] && [ -f /etc/config/mwan4 ] +} + +mwan4_is_running() { + mwan4_is_installed && /etc/init.d/mwan4 running >/dev/null 2>&1 +} + +mwan4_get_iface_list() { + # Get list of enabled mwan4 interfaces from UCI config + # Output: space-separated list of interface names + local iface_list="" + mwan4_is_installed || return 1 + + _mwan4_collect_iface() { + local en + config_get_bool en "$1" 'enabled' '0' + [ "$en" -gt '0' ] && iface_list="${iface_list}${1} " + } + config_load 'mwan4' + config_foreach _mwan4_collect_iface 'interface' + + echo "${iface_list% }" +} + +mwan4_get_strategy_list() { + # Get list of mwan4 strategies from UCI config + # Output: space-separated list of strategy names + local strategy_list="" + mwan4_is_installed || return 1 + + _mwan4_collect_strategy() { strategy_list="${strategy_list}${1} "; } + config_load 'mwan4' + config_foreach _mwan4_collect_strategy 'strategy' + + echo "${strategy_list% }" +} + +mwan4_get_iface_mark_chain() { + # Get the nftables marking chain name for a specific mwan4 interface + # $1 = interface name + # Output: chain name (e.g., "mwan4_iface_in_wan") + local iface="$1" + [ -z "$iface" ] && return 1 + mwan4_is_running || return 1 + + # Check if the chain exists in nftables + if "$nft" list chain inet fw4 "mwan4_iface_in_${iface}" >/dev/null 2>&1; then + echo "mwan4_iface_in_${iface}" + return 0 + fi + return 1 +} + +mwan4_get_iface_nft_sets() { + # Get the nftables set names used by a specific mwan4 interface + # $1 = interface name + # Output: space-separated list of set names + local iface="$1" + local family="" + local sets="" + [ -z "$iface" ] && return 1 + mwan4_is_installed || return 1 + + # Get the family (ipv4/ipv6) for the interface + config_load 'mwan4' + config_get family "$iface" 'family' 'ipv4' + + # The sets used by mwan4 per interface family + for settype in connected custom dynamic; do + if "$nft" list set inet fw4 "mwan4_${settype}_${family}" >/dev/null 2>&1; then + sets="${sets}mwan4_${settype}_${family} " + fi + done + + echo "${sets% }" +} + +mwan4_get_strategy_chain() { + # Get the nftables strategy chain name for a specific mwan4 strategy + # $1 = strategy name + # $2 = family (ipv4 or ipv6, defaults to ipv4) + # Output: chain name (e.g., "mwan4_strategy_balanced_ipv4") + local strategy="$1" + local family="${2:-ipv4}" + [ -z "$strategy" ] && return 1 + mwan4_is_running || return 1 + + # Check if the chain exists in nftables + if "$nft" list chain inet fw4 "mwan4_strategy_${strategy}_${family}" >/dev/null 2>&1; then + echo "mwan4_strategy_${strategy}_${family}" + return 0 + fi + return 1 +} + +mwan4_get_mmx_mask() { + # Get the MMX (Multi-WAN Mark) mask used by mwan4 + # Output: hex mask value (e.g., "0x3F00") + mwan4_is_installed || return 1 + + local mask + config_load 'mwan4' + config_get mask 'globals' 'mmx_mask' '0x3F00' + echo "$mask" +} + print_json_bool() { json_init; json_add_boolean "$1" "$2"; json_dump; json_cleanup; } print_json_string() { json_init; json_add_string "$1" "$2"; json_dump; json_cleanup; } try() { @@ -515,7 +633,7 @@ get_text() { warningOutdatedLuciPackage) printf "The WebUI application is outdated (version %s), please update it" "$1";; warningDnsmasqInstanceNoConfdir) printf "Dnsmasq instance '%s' targeted in settings, but it doesn't have its own confdir" "$1";; warningDhcpLanForce) printf "Please set 'dhcp.%s.force=1' to speed up service start-up" "$1";; - warningSummary) printf "Warnings encountered, please check %s" "$(get_url '#WarningMessagesDetails')";; + warningSummary) printf "Warnings encountered, please check %s" "$(get_url '#warning-messages-details')";; warningIncompatibleDHCPOption6) printf "Incompatible DHCP Option 6 for interface '%s'" "$1";; warningNetifdMissingInterfaceLocal) printf "Netifd setup: option netifd_interface_local is missing, assuming '%s'" "$1";; warningUplinkDown) printf "Uplink/WAN interface is still down, going back to boot mode";; @@ -571,9 +689,9 @@ load_package_config() { config_get_bool enabled 'config' 'enabled' '0' config_get fw_mask 'config' 'fw_mask' '00ff0000' config_get icmp_interface 'config' 'icmp_interface' - config_get ignored_interface 'config' 'ignored_interface' + config_get_list ignored_interface 'config' 'ignored_interface' config_get_bool ipv6_enabled 'config' 'ipv6_enabled' '0' - config_get lan_device 'config' 'lan_device' 'br-lan' + config_get_list lan_device 'config' 'lan_device' 'br-lan' config_get_bool nft_rule_counter 'config' 'nft_rule_counter' '0' config_get_bool nft_set_auto_merge 'config' 'nft_set_auto_merge' '1' config_get_bool nft_set_counter 'config' 'nft_set_counter' '0' @@ -586,10 +704,10 @@ load_package_config() { config_get prefixlength 'config' 'prefixlength' '1' config_get procd_boot_trigger_delay 'config' 'procd_boot_trigger_delay' '5000' config_get procd_reload_delay 'config' 'procd_reload_delay' '0' - config_get resolver_instance 'config' 'resolver_instance' '*' + config_get_list resolver_instance 'config' 'resolver_instance' '*' config_get resolver_set 'config' 'resolver_set' config_get_bool strict_enforcement 'config' 'strict_enforcement' '1' - config_get supported_interface 'config' 'supported_interface' + config_get_list supported_interface 'config' 'supported_interface' config_get uplink_interface 'config' 'uplink_interface' 'wan' config_get uplink_interface6 'config' 'uplink_interface6' 'wan6' config_get uplink_ip_rules_priority 'config' 'uplink_ip_rules_priority' '30000' @@ -620,9 +738,9 @@ load_package_config() { local nft_set_flags case "${nft_set_flags_interval}:${nft_set_flags_timeout}" in - 1:1) nft_set_flags="flags interval, timeout${nft_set_timeout:+; timeout $nft_set_timeout}";; + 1:1) nft_set_flags="flags interval, timeout${nft_set_timeout:+; timeout ${nft_set_timeout}}";; 1:0) nft_set_flags='flags interval';; - 0:1) nft_set_flags="flags timeout${nft_set_timeout:+; timeout $nft_set_timeout}";; + 0:1) nft_set_flags="flags timeout${nft_set_timeout:+; timeout ${nft_set_timeout}}";; 0:0) nft_set_flags='';; esac @@ -639,10 +757,10 @@ load_package_config() { nftSetParams=" \ ${nft_set_auto_merge:+ auto-merge;} \ ${nft_set_counter:+ counter;} \ - ${nft_set_flags:+ $nft_set_flags;} \ - ${nft_set_gc_interval:+ gc_interval "$nft_set_gc_interval";} \ - ${nft_set_policy:+ policy "$nft_set_policy";} \ - ${nft_set_timeout:+ timeout "$nft_set_timeout";} \ + ${nft_set_flags:+ ${nft_set_flags};} \ + ${nft_set_gc_interval:+ gc_interval "${nft_set_gc_interval}";} \ + ${nft_set_policy:+ policy "${nft_set_policy}";} \ + ${nft_set_timeout:+ timeout "${nft_set_timeout}";} \ " if [ -x "$agh" ] && [ ! -s "$aghConfigFile" ]; then @@ -706,7 +824,7 @@ load_environment() { return "$_ret" } local param="$1" validation_result="$2" - [ -z "$loadEnvironmentFlag" ] || return 0 + [ -n "$loadEnvironmentFlag" ] && return 0 case "$param" in on_boot|on_start) output 1 "Loading environment ($param) " @@ -779,7 +897,7 @@ load_network() { [ -n "$uplinkGW6" ] && output 2 "Found uplink IPv6 gateway (${param}): $uplinkGW6 $__OK__\n" ;; esac - uplinkGW="${uplinkGW4:-$uplinkGW6}" + uplinkGW="${uplinkGW4:-${uplinkGW6}}" } is_wan_up() { @@ -799,6 +917,9 @@ is_wan_up() { fi } +# nft() overrides the nft binary: all calls append to the atomic nft file. +# This captures both internal rules and user include script output. +# Use nft_call() for direct nft binary access. nft() { [ -n "$*" ] && nft_file 'add' 'main' "$@"; } nft4() { nft "$@"; } nft6() { [ -n "$ipv6_enabled" ] || return 0; nft "$@"; } @@ -840,7 +961,7 @@ nft_file() { echo "" >> "$nftTempFile" # Insert PBR guards at the top of pbr chains so first PBR match wins, while preserving foreign marks. for chain in $chainsList; do - echo "add rule inet $nftTable ${nftPrefix}_${chain} ${nftRuleParams:+$nftRuleParams }meta mark & $fw_mask != 0 return" >> "$nftTempFile" + echo "add rule inet $nftTable ${nftPrefix}_${chain} ${nftRuleParams:+${nftRuleParams} }meta mark & $fw_mask != 0 return" >> "$nftTempFile" done ;; create:netifd) @@ -874,6 +995,7 @@ nft_file() { if nft_call -c -f "$nftTempFile" && \ cp -f "$nftTempFile" "$nftMainFile"; then output_okn + fw4 -q reload >/dev/null 2>&1 else json add error 'errorNftMainFileInstall' "$nftTempFile" output_failn @@ -915,8 +1037,8 @@ nftset() { local command="$1" iface="$2" target="${3:-dst}" type="${4:-ip}" uid="$5" comment="$6" param="$7" mark="$7" local nftset4 nftset6 i param4 param6 local ipv4_error=1 ipv6_error=1 - nftset4="${nftPrefix}${iface:+_$iface}_4${target:+_$target}${type:+_$type}${uid:+_$uid}" - nftset6="${nftPrefix}${iface:+_$iface}_6${target:+_$target}${type:+_$type}${uid:+_$uid}" + nftset4="${nftPrefix}${iface:+_${iface}}_4${target:+_${target}}${type:+_${type}}${uid:+_${uid}}" + nftset6="${nftPrefix}${iface:+_${iface}}_6${target:+_${target}}${type:+_${type}}${uid:+_${uid}}" if [ "${#nftset4}" -gt '255' ]; then json add error 'errorNftsetNameTooLong' "$nftset4" @@ -949,8 +1071,8 @@ 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 + 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 ;; create) case "$type" in @@ -1044,59 +1166,45 @@ cleanup() { sync ;; main_table) - # Get all rules to delete in one pass (format: "priority suppress_prefixlength_value table_name") - ip -4 rule show | awk ' - /lookup[[:space:]]+main[[:space:]]+suppress_prefixlength[[:space:]]+[0-9]+/ { - sub(":", "", $1) - match($0, /suppress_prefixlength[[:space:]]+([0-9]+)/, arr) - print $1 " " arr[1] " main" - } - /lookup '"$ipTablePrefix"'_/ { - sub(":", "", $1) - match($0, /lookup[[:space:]]+([^[:space:]]+)/, arr) - print $1 " 0 " arr[1] - } - ' | while read -r prio len table; do - if [ "$table" != "main" ] && is_netifd_table "$table"; then - continue # Skip netifd-managed tables - fi - if [ "$len" = "0" ]; then - # pbr table rule - try priority deletion first - ip -4 rule del priority "$prio" 2>/dev/null - else - # suppress_prefixlength rule - try priority first, then full spec - if ! ip -4 rule del priority "$prio" 2>/dev/null; then - ip -4 rule del lookup main suppress_prefixlength "$len" priority "$prio" 2>/dev/null || \ - ip -4 rule del table main suppress_prefixlength "$len" priority "$prio" 2>/dev/null - fi - fi + # Delete rules by priority range instead of parsing table names. + # pbr rules occupy: uplink_ip_rules_priority (down) for interfaces, + # uplink_ip_rules_priority+1 for suppress_prefixlength. + local prio_min prio_max prio table line + # max interfaces = fw_mask / uplink_mark (e.g. 0x00ff0000/0x00010000 = 255) + local max_ifaces="$((fw_mask / uplink_mark))" + prio_max="$((uplink_ip_rules_priority))" + prio_min="$((uplink_ip_rules_priority - max_ifaces))" + [ "$prio_min" -lt 1 ] && prio_min="1" + ip -4 rule show | while IFS= read -r line; do + prio="${line%%:*}" + [ "$prio" -ge "$prio_min" ] 2>/dev/null && [ "$prio" -le "$prio_max" ] 2>/dev/null || continue + # Skip netifd-managed fwmark rules (but not WG sport rules) + case "$line" in + *fwmark*"lookup ${ipTablePrefix}_"*) + table="${line##*lookup }" + table="${table%% *}" + is_netifd_table "$table" && continue + ;; + esac + ip -4 rule del priority "$prio" 2>/dev/null done + # Legacy: remove suppress_prefixlength rules created without explicit priority + ip -4 rule del lookup 'main' suppress_prefixlength "$prefixlength" 2>/dev/null # Always attempt IPv6 cleanup regardless of current ipv6_enabled setting # since rules might exist from when IPv6 was previously enabled - ip -6 rule show 2>/dev/null | awk ' - /lookup[[:space:]]+main[[:space:]]+suppress_prefixlength[[:space:]]+[0-9]+/ { - sub(":", "", $1) - match($0, /suppress_prefixlength[[:space:]]+([0-9]+)/, arr) - print $1 " " arr[1] " main" - } - /lookup '"$ipTablePrefix"'_/ { - sub(":", "", $1) - match($0, /lookup[[:space:]]+([^[:space:]]+)/, arr) - print $1 " 0 " arr[1] - } - ' | while read -r prio len table; do - if [ "$table" != "main" ] && is_netifd_table "$table"; then - continue # Skip netifd-managed tables - fi - if [ "$len" = "0" ]; then - ip -6 rule del priority "$prio" 2>/dev/null - else - if ! ip -6 rule del priority "$prio" 2>/dev/null; then - ip -6 rule del lookup main suppress_prefixlength "$len" priority "$prio" 2>/dev/null || \ - ip -6 rule del table main suppress_prefixlength "$len" priority "$prio" 2>/dev/null - fi - fi + ip -6 rule show 2>/dev/null | while IFS= read -r line; do + prio="${line%%:*}" + [ "$prio" -ge "$prio_min" ] 2>/dev/null && [ "$prio" -le "$prio_max" ] 2>/dev/null || continue + case "$line" in + *fwmark*"lookup ${ipTablePrefix}_"*) + table="${line##*lookup }" + table="${table%% *}" + is_netifd_table "$table" && continue + ;; + esac + ip -6 rule del priority "$prio" 2>/dev/null done + ip -6 rule del lookup 'main' suppress_prefixlength "$prefixlength" 2>/dev/null ;; main_chains) for i in $chainsList dstnat; do @@ -1170,14 +1278,17 @@ json() { resolver() { _dnsmasq_instance_get_confdir() { - local cfg_file + local cfg cfg_file + cfg="$(uci -q show "dhcp.${1}" | awk -F'[.=]' 'NR==1{print $2}')" [ -z "$dnsmasq_ubus" ] && dnsmasq_ubus="$(ubus call service list '{"name":"dnsmasq"}')" - cfg_file="$(echo "$dnsmasq_ubus" | jsonfilter -e "@.dnsmasq.instances.${1}.command" \ + cfg_file="$(echo "$dnsmasq_ubus" | jsonfilter -e "@.dnsmasq.instances.${cfg}.command" \ | awk '{gsub(/\\\//,"/");gsub(/[][",]/,"");for(i=1;i<=NF;i++)if($i=="-C"){print $(i+1);exit}}')" awk -F= '/^conf-dir=/{print $2; exit}' "$cfg_file" } _dnsmasq_instance_config() { local cfg="$1" param="$2" confdir + [ -s "/etc/config/dhcp" ] || return 0 + [ -n "$(uci_get dhcp "$cfg")" ] || return 1 case "$param" in cleanup) # clean up all dnsmasq configs @@ -1271,8 +1382,8 @@ resolver() { else config_foreach _dnsmasq_instance_config 'dnsmasq' 'cleanup' for i in $resolver_instance; do - _dnsmasq_instance_config "@dnsmasq[$i]" \ - || _dnsmasq_instance_config "$i" + _dnsmasq_instance_config "@dnsmasq[$i]" 'setup' \ + || _dnsmasq_instance_config "$i" 'setup' done fi ;; @@ -1373,9 +1484,13 @@ netifd() { uci_set 'network' "${rt_name}_ipv4" 'priority' "${lan_priority}" fi if [ -n "$ipv6_enabled" ] && [ -n "$netifd_interface_default6" ]; then + local ipv6_default_lookup="${ipTablePrefix}_${netifd_interface_default6}" + if is_split_uplink && [ "$netifd_interface_default6" = "$uplink_interface6" ]; then + ipv6_default_lookup="${ipTablePrefix}_${uplink_interface4}" + fi uci_add 'network' 'rule6' "${rt_name}_ipv6" uci_set 'network' "${rt_name}_ipv6" 'in' "${iface}" - uci_set 'network' "${rt_name}_ipv6" 'lookup' "${ipTablePrefix}_${netifd_interface_default6}" + uci_set 'network' "${rt_name}_ipv6" 'lookup' "$ipv6_default_lookup" uci_set 'network' "${rt_name}_ipv6" 'priority' "${lan_priority}" fi lan_priority="$((lan_priority + 1))" @@ -1393,7 +1508,7 @@ netifd() { local splitUplinkSecondIface if is_split_uplink; then - if is_wan "$iface" || is_wan6 "$iface"; then + if is_uplink4 "$iface" || is_uplink6 "$iface"; then if [ -n "$_uplinkMark" ] && [ -n "$_uplinkPriority" ] && [ -n "$_uplinkTableID" ]; then _mark="$_uplinkMark" _priority="$_uplinkPriority" @@ -1415,7 +1530,7 @@ netifd() { case "$action" in install) output 2 "Setting up netifd extensions for $iface... " - if ! is_split_uplink || ! is_wan6 "$iface"; then + if ! is_split_uplink || ! is_uplink6 "$iface"; then uci_set 'network' "${iface}" 'ip4table' "${rt_name}" uci_add 'network' 'rule' "${rt_name}_ipv4" uci_set 'network' "${rt_name}_ipv4" 'priority' "${_priority}" @@ -1423,7 +1538,7 @@ netifd() { uci_set 'network' "${rt_name}_ipv4" 'mark' "${_mark}" uci_set 'network' "${rt_name}_ipv4" 'mask' "${fw_mask}" fi - if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_wan "$iface"; }; then + if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_uplink4 "$iface"; }; then uci_set 'network' "${iface}" 'ip6table' "${rt_name}" uci_add 'network' 'rule6' "${rt_name}_ipv6" uci_set 'network' "${rt_name}_ipv6" 'priority' "${_priority}" @@ -1431,7 +1546,7 @@ netifd() { uci_set 'network' "${rt_name}_ipv6" 'mark' "${_mark}" uci_set 'network' "${rt_name}_ipv6" 'mask' "${fw_mask}" fi - if ! is_split_uplink || ! is_wan6 "$iface"; then + if ! is_split_uplink || ! is_uplink6 "$iface"; then [ "$rt_name" = 'main' ] || sed -i "\#${rt_name}\$#d" "$rtTablesFile" >/dev/null 2>&1 [ "$rt_name" = 'main' ] || echo "${_tid} ${rt_name}" >> "$rtTablesFile" nft_file 'sed' 'temp' "\#${_mark}#d" @@ -1443,18 +1558,18 @@ netifd() { fi local dscp="$(uci_get "$packageName" 'config' "${iface}_dscp")" if [ "${dscp:-0}" -ge '1' ] && [ "${dscp:-0}" -le '63' ]; then - if ! is_split_uplink || ! is_wan6 "$iface"; then + if ! is_split_uplink || ! is_uplink6 "$iface"; then nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv4Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${_mark}" fi - if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_wan "$iface"; }; then + if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_uplink4 "$iface"; }; then nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv6Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${_mark}" fi fi if [ "$iface" = "$icmp_interface" ]; then - if ! is_split_uplink || ! is_wan6 "$iface"; then + if ! is_split_uplink || ! is_uplink6 "$iface"; then nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv4Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${_mark}" fi - if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_wan "$iface"; }; then + if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_uplink4 "$iface"; }; then nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv6Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${_mark}" fi fi @@ -1528,7 +1643,7 @@ netifd() { esac nft_file 'create' 'netifd' - output 1 "Netifd extensions $action ${target_iface:+on $target_iface }" + output 1 "Netifd extensions $action ${target_iface:+on ${target_iface} }" uci_remove 'network' 'rule' "main_ipv4" 2>/dev/null uci_remove 'network' 'rule6' "main_ipv6" 2>/dev/null config_load 'network' @@ -1557,7 +1672,7 @@ netifd() { uci_commit "$packageName" uci_commit 'network' sync - output "Restarting network ${action:+(on_$action) }" + output "Restarting network ${action:+(on_${action}) }" { /etc/init.d/network 'reload'; /etc/init.d/firewall 'reload'; } >/dev/null 2>&1 && output_okbn || output_failn } @@ -1606,11 +1721,11 @@ dns_policy_routing() { value="$src_addr" first_value="$(str_first_word "$value")" if is_phys_dev "$first_value"; then - param4="${param4:+$param4 }iifname ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }iifname ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }iifname ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }iifname ${negation:+${negation} }{ $(inline_set "$value") }" elif is_mac_address "$first_value"; then - param4="${param4:+$param4 }ether saddr ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }ether saddr ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }ether saddr ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }ether saddr ${negation:+${negation} }{ $(inline_set "$value") }" elif is_domain "$first_value"; then local inline_set_ipv4='' inline_set_ipv6='' d='' for d in $value; do @@ -1620,17 +1735,17 @@ dns_policy_routing() { if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then json add error 'errorFailedToResolve' "$d" else - [ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+$inline_set_ipv4, }$resolved_ipv4" - [ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+$inline_set_ipv6, }$resolved_ipv6" + [ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+${inline_set_ipv4}, }$resolved_ipv4" + [ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+${inline_set_ipv6}, }$resolved_ipv6" fi done [ -n "$inline_set_ipv4" ] || inline_set_ipv4_empty_flag='true' [ -n "$inline_set_ipv6" ] || inline_set_ipv6_empty_flag='true' - param4="${param4:+$param4 }${nftIPv4Flag} saddr ${negation:+$negation }{ $inline_set_ipv4 }" - param6="${param6:+$param6 }${nftIPv6Flag} saddr ${negation:+$negation }{ $inline_set_ipv6 }" + param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }{ $inline_set_ipv4 }" + param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }{ $inline_set_ipv6 }" else - param4="${param4:+$param4 }${nftIPv4Flag} saddr ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }${nftIPv6Flag} saddr ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }{ $(inline_set "$value") }" fi fi @@ -1740,17 +1855,17 @@ policy_routing() { fi first_value_src="$(str_first_word "$value")" if is_phys_dev "$first_value_src"; then - param4="${param4:+$param4 }iifname ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }iifname ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }iifname ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }iifname ${negation:+${negation} }{ $(inline_set "$value") }" elif is_mac_address "$first_value_src"; then - param4="${param4:+$param4 }ether saddr ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }ether saddr ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }ether saddr ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }ether saddr ${negation:+${negation} }{ $(inline_set "$value") }" elif is_domain "$first_value_src"; then local target='src' type='ip' if resolver 'create_resolver_set' "$iface" "$target" "$type" "$uid" "$name" && \ resolver 'add_resolver_element' "$iface" "$target" "$type" "$uid" "$name" "$value"; then - param4="${param4:+$param4 }${nftIPv4Flag} saddr ${negation:+$negation }@${nftPrefix}_${iface}_4_${target}_${type}_${uid}${nftset_suffix}" - param6="${param6:+$param6 }${nftIPv6Flag} saddr ${negation:+$negation }@${nftPrefix}_${iface}_6_${target}_${type}_${uid}${nftset_suffix}" + param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }@${nftPrefix}_${iface}_4_${target}_${type}_${uid}${nftset_suffix}" + param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }@${nftPrefix}_${iface}_6_${target}_${type}_${uid}${nftset_suffix}" else local inline_set_ipv4='' inline_set_ipv6='' d='' unset src_inline_set_ipv4_empty_flag @@ -1762,18 +1877,18 @@ policy_routing() { if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then json add error 'errorFailedToResolve' "$d" else - [ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+$inline_set_ipv4, }$resolved_ipv4" - [ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+$inline_set_ipv6, }$resolved_ipv6" + [ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+${inline_set_ipv4}, }$resolved_ipv4" + [ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+${inline_set_ipv6}, }$resolved_ipv6" fi done [ -n "$inline_set_ipv4" ] || src_inline_set_ipv4_empty_flag='true' [ -n "$inline_set_ipv6" ] || src_inline_set_ipv6_empty_flag='true' - param4="${param4:+$param4 }${nftIPv4Flag} saddr ${negation:+$negation }{ $inline_set_ipv4 }" - param6="${param6:+$param6 }${nftIPv6Flag} saddr ${negation:+$negation }{ $inline_set_ipv6 }" + param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }{ $inline_set_ipv4 }" + param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }{ $inline_set_ipv6 }" fi else - param4="${param4:+$param4 }${nftIPv4Flag} saddr ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }${nftIPv6Flag} saddr ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }{ $(inline_set "$value") }" fi fi @@ -1785,14 +1900,14 @@ policy_routing() { fi first_value_dest="$(str_first_word "$value")" if is_phys_dev "$first_value_dest"; then - param4="${param4:+$param4 }oifname ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }oifname ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }oifname ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }oifname ${negation:+${negation} }{ $(inline_set "$value") }" elif is_domain "$first_value_dest"; then local target='dst' type='ip' if resolver 'create_resolver_set' "$iface" "$target" "$type" "$uid" "$name" && \ resolver 'add_resolver_element' "$iface" "$target" "$type" "$uid" "$name" "$value"; then - param4="${param4:+$param4 }${nftIPv4Flag} daddr ${negation:+$negation }@${nftPrefix}_${iface}_4_${target}_${type}_${uid}${nftset_suffix}" - param6="${param6:+$param6 }${nftIPv6Flag} daddr ${negation:+$negation }@${nftPrefix}_${iface}_6_${target}_${type}_${uid}${nftset_suffix}" + param4="${param4:+${param4} }${nftIPv4Flag} daddr ${negation:+${negation} }@${nftPrefix}_${iface}_4_${target}_${type}_${uid}${nftset_suffix}" + param6="${param6:+${param6} }${nftIPv6Flag} daddr ${negation:+${negation} }@${nftPrefix}_${iface}_6_${target}_${type}_${uid}${nftset_suffix}" else local inline_set_ipv4='' inline_set_ipv6='' d='' unset dest_inline_set_ipv4_empty_flag @@ -1804,18 +1919,18 @@ policy_routing() { if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then json add error 'errorFailedToResolve' "$d" else - [ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+$inline_set_ipv4, }$resolved_ipv4" - [ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+$inline_set_ipv6, }$resolved_ipv6" + [ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+${inline_set_ipv4}, }$resolved_ipv4" + [ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+${inline_set_ipv6}, }$resolved_ipv6" fi done [ -n "$inline_set_ipv4" ] || dest_inline_set_ipv4_empty_flag='true' [ -n "$inline_set_ipv6" ] || dest_inline_set_ipv6_empty_flag='true' - param4="${param4:+$param4 }${nftIPv4Flag} daddr ${negation:+$negation }{ $inline_set_ipv4 }" - param6="${param6:+$param6 }${nftIPv6Flag} daddr ${negation:+$negation }{ $inline_set_ipv6 }" + param4="${param4:+${param4} }${nftIPv4Flag} daddr ${negation:+${negation} }{ $inline_set_ipv4 }" + param6="${param6:+${param6} }${nftIPv6Flag} daddr ${negation:+${negation} }{ $inline_set_ipv6 }" fi else - param4="${param4:+$param4 }${nftIPv4Flag} daddr ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }${nftIPv6Flag} daddr ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }${nftIPv4Flag} daddr ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }${nftIPv6Flag} daddr ${negation:+${negation} }{ $(inline_set "$value") }" fi fi @@ -1825,8 +1940,8 @@ policy_routing() { else unset negation; value="$src_port"; fi - param4="${param4:+$param4 }${proto_i:+$proto_i }sport ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }${proto_i:+$proto_i }sport ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }${proto_i:+${proto_i} }sport ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }${proto_i:+${proto_i} }sport ${negation:+${negation} }{ $(inline_set "$value") }" fi if [ -n "$dest_port" ]; then @@ -1835,8 +1950,8 @@ policy_routing() { else unset negation; value="$dest_port"; fi - param4="${param4:+$param4 }${proto_i:+$proto_i }dport ${negation:+$negation }{ $(inline_set "$value") }" - param6="${param6:+$param6 }${proto_i:+$proto_i }dport ${negation:+$negation }{ $(inline_set "$value") }" + param4="${param4:+${param4} }${proto_i:+${proto_i} }dport ${negation:+${negation} }{ $(inline_set "$value") }" + param6="${param6:+${param6} }${proto_i:+${proto_i} }dport ${negation:+${negation} }{ $(inline_set "$value") }" fi if is_tor "$iface"; then @@ -1918,7 +2033,7 @@ dns_policy_process() { if is_url "$i"; then i="$(process_url "$i")" fi - j="${j:+$j }$i" + j="${j:+${j} }$i" done src_addr="$j" @@ -1931,9 +2046,9 @@ dns_policy_process() { 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}" + dest_dns_ipv4="${dest_dns_ipv4:-${d}}" elif is_ipv6 "$d"; then - dest_dns_ipv6="${dest_dns_ipv6:-$d}" + dest_dns_ipv6="${dest_dns_ipv6:-${d}}" fi fi done @@ -2006,7 +2121,7 @@ policy_process() { if is_url "$i"; then i="$(process_url "$i")" fi - j="${j:+$j }$i" + j="${j:+${j} }$i" done src_addr="$j" @@ -2015,7 +2130,7 @@ policy_process() { if is_url "$i"; then i="$(process_url "$i")" fi - j="${j:+$j }$i" + j="${j:+${j} }$i" done dest_addr="$j" @@ -2040,8 +2155,8 @@ policy_process() { continue fi policy_routing "$name" "$interface" "$filtered_value_src_addr" "$src_port" "$filtered_value_dest_addr" "$dest_port" "$proto" "$chain" "$uid" - processed_value_src_addr="${processed_value_src_addr:+$processed_value_src_addr }$filtered_value_src_addr" - processed_value_dest_addr="${processed_value_dest_addr:+$processed_value_dest_addr }$filtered_value_dest_addr" + processed_value_src_addr="${processed_value_src_addr:+${processed_value_src_addr} }$filtered_value_src_addr" + processed_value_dest_addr="${processed_value_dest_addr:+${processed_value_dest_addr} }$filtered_value_dest_addr" fi done fi @@ -2104,17 +2219,17 @@ interface_routing() { fi if ! nft_file 'match' 'temp' "${nftPrefix}_mark_${mark}"; then - try nft add chain inet "$nftTable" "${nftPrefix}_mark_${mark}" || ipv4_error=1 - try nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} ${nftRuleParams} meta mark set (meta mark & ${fw_maskXor}) | ${mark}" || ipv4_error=1 - try nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} return" || ipv4_error=1 + nft add chain inet "$nftTable" "${nftPrefix}_mark_${mark}" + nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} ${nftRuleParams} meta mark set (meta mark & ${fw_maskXor}) | ${mark}" + nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} return" fi dscp="$(uci_get "$packageName" 'config' "${iface}_dscp" '0')" if [ "$dscp" -ge '1' ] && [ "$dscp" -le '63' ]; then - try nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv4Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" || s=1 + nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv4Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" fi if [ "$iface" = "$icmp_interface" ]; then - try nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv4Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" || s=1 + nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv4Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" fi fi @@ -2142,17 +2257,17 @@ interface_routing() { fi if ! nft_file 'match' 'temp' "${nftPrefix}_mark_${mark}"; then - try nft add chain inet "$nftTable" "${nftPrefix}_mark_${mark}" || ipv6_error=1 - try nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} ${nftRuleParams} meta mark set (meta mark & ${fw_maskXor}) | ${mark}" || ipv6_error=1 - try nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} return" || ipv6_error=1 + nft add chain inet "$nftTable" "${nftPrefix}_mark_${mark}" + nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} ${nftRuleParams} meta mark set (meta mark & ${fw_maskXor}) | ${mark}" + nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} return" fi dscp="$(uci_get "$packageName" 'config' "${iface}_dscp" '0')" if [ "$dscp" -ge '1' ] && [ "$dscp" -le '63' ]; then - try nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv6Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" || s=1 + nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv6Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" fi if [ "$iface" = "$icmp_interface" ]; then - try nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv6Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" || s=1 + nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv6Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" fi fi @@ -2171,10 +2286,8 @@ interface_routing() { ;; delete|destroy) is_netifd_interface "$iface" && return 0 - ip -4 rule del table 'main' suppress_prefixlength "$prefixlength" prio "$((priority - 1))" >/dev/null 2>&1 ip -4 rule del table 'main' prio "$((priority - 1000))" >/dev/null 2>&1 ip -4 rule del table "$tid" prio "$priority" >/dev/null 2>&1 - ip -6 rule del table 'main' suppress_prefixlength "$prefixlength" prio "$((priority - 1))" >/dev/null 2>&1 ip -6 rule del table 'main' prio "$((priority - 1000))" >/dev/null 2>&1 ip -6 rule del table "$tid" prio "$priority" >/dev/null 2>&1 ip -4 rule flush table "$tid" >/dev/null 2>&1 @@ -2275,12 +2388,6 @@ process_interface() { return 0 ;; create_global_rules) - ip -4 rule del lookup 'main' suppress_prefixlength "$prefixlength" priority "$ifacePriority" >/dev/null 2>&1 - try ip -4 rule add lookup 'main' suppress_prefixlength "$prefixlength" priority "$ifacePriority" || ipv4_error=1 - if [ -n "$ipv6_enabled" ]; then - ip -6 rule del lookup 'main' suppress_prefixlength "$prefixlength" priority "$ifacePriority" >/dev/null 2>&1 - try ip -6 rule add lookup 'main' suppress_prefixlength "$prefixlength" priority "$ifacePriority" || ipv6_error=1 - fi _wg_server() { local iface="$1" if is_wg_server "$iface" && ! is_ignored_interface "$iface"; then @@ -2289,17 +2396,27 @@ process_interface() { config_get listen_port "$iface" 'listen_port' if [ "$disabled" != '1' ] && [ -n "$listen_port" ]; then if [ -n "$uplink_interface4" ]; then - ip rule del sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" >/dev/null 2>&1 - ip rule add sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" >/dev/null 2>&1 + #ip rule del sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" priority "$ifacePriority" >/dev/null 2>&1 + ip rule add sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" priority "$ifacePriority" >/dev/null 2>&1 if [ -n "$ipv6_enabled" ]; then - ip -6 rule del sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" >/dev/null 2>&1 - ip -6 rule add sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" >/dev/null 2>&1 + #ip -6 rule del sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" priority "$ifacePriority" >/dev/null 2>&1 + ip -6 rule add sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" priority "$ifacePriority" >/dev/null 2>&1 fi + ifacePriority="$((ifacePriority - 1))" fi fi fi } config_foreach _wg_server 'interface' + + #ip -4 rule del priority "$ifacePriority" >/dev/null 2>&1 + #ip -4 rule del lookup 'main' suppress_prefixlength "$prefixlength" >/dev/null 2>&1 + try ip -4 rule add lookup 'main' suppress_prefixlength "$prefixlength" priority "$ifacePriority" || ipv4_error=1 + if [ -n "$ipv6_enabled" ]; then + #ip -6 rule del priority "$ifacePriority" >/dev/null 2>&1 + #ip -6 rule del lookup 'main' suppress_prefixlength "$prefixlength" >/dev/null 2>&1 + try ip -6 rule add lookup 'main' suppress_prefixlength "$prefixlength" priority "$ifacePriority" || ipv6_error=1 + fi return 0 ;; esac @@ -2344,7 +2461,7 @@ process_interface() { network_get_device dev4 "$iface" [ -z "$dev4" ] && network_get_physdev dev4 "$iface" - if is_wan "$iface" && [ -n "$uplink_interface6" ]; then + if is_uplink4 "$iface" && [ -n "$uplink_interface6" ]; then network_get_device dev6 "$uplink_interface6" [ -z "$dev6" ] && network_get_physdev dev6 "$uplink_interface6" fi @@ -2357,7 +2474,7 @@ process_interface() { local splitUplinkSecondIface if is_split_uplink; then - if is_wan "$iface" || is_wan6 "$iface"; then + if is_uplink4 "$iface" || is_uplink6 "$iface"; then if [ -n "$_uplinkMark" ] && [ -n "$_uplinkPriority" ] && [ -n "$_uplinkTableID" ]; then _mark="$_uplinkMark" _priority="$_uplinkPriority" @@ -2380,7 +2497,7 @@ process_interface() { eval "enum_mark_${iface//-/_}"='$_mark' eval "enum_priority_${iface//-/_}"='$_priority' eval "enum_tid_${iface//-/_}"='$_tid' - ifacesTriggers="${ifacesTriggers:+$ifacesTriggers }$iface" + ifacesTriggers="${ifacesTriggers:+${ifacesTriggers} }$iface" ;; create) if [ -z "$splitUplinkSecondIface" ]; then @@ -2395,9 +2512,9 @@ process_interface() { dispGw4="${gw4:-0.0.0.0}" dispGw6="${gw6:-::/0}" if is_split_uplink; then - if is_wan "$iface"; then + if is_uplink4 "$iface"; then gw6=""; dev6="" - elif is_wan6 "$iface"; then + elif is_uplink6 "$iface"; then gw4=""; dev4="" fi fi @@ -2408,11 +2525,11 @@ process_interface() { if is_netifd_interface_default "$iface"; then [ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__" fi - displayText="${iface}/${dispDev:+$dispDev/}${dispGw4}${ipv6_enabled:+/$dispGw6}" + 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" - gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ $dispStatus}\n" + gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n" if is_netifd_interface "$iface"; then output_okb; else output_ok; fi else json add error 'errorFailedSetup' "$displayText" @@ -2428,9 +2545,9 @@ process_interface() { eval "mark_${iface//-/_}"='$_mark' eval "tid_${iface//-/_}"='$_tid' if is_split_uplink; then - if is_wan "$iface"; then + if is_uplink4 "$iface"; then dev6="" - elif is_wan6 "$iface"; then + elif is_uplink6 "$iface"; then dev4="" fi fi @@ -2441,7 +2558,7 @@ process_interface() { if is_netifd_interface_default "$iface"; then [ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__" fi - displayText="${iface}/${dispDev:+$dispDev/}" + displayText="${iface}/${dispDev:+${dispDev}/}" interface_routing 'create_user_set' "$_tid" "$_mark" "$iface" "" "$dev4" "" "$dev6" "$_priority" ;; destroy) @@ -2453,9 +2570,9 @@ process_interface() { eval "mark_${iface//-/_}"='$_mark' eval "tid_${iface//-/_}"='$_tid' if is_split_uplink; then - if is_wan "$iface"; then + if is_uplink4 "$iface"; then dev6="" - elif is_wan6 "$iface"; then + elif is_uplink6 "$iface"; then dev4="" fi fi @@ -2466,7 +2583,7 @@ process_interface() { if is_netifd_interface_default "$iface"; then [ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__" fi - displayText="${iface}/${dispDev:+$dispDev}" + displayText="${iface}/${dispDev:+${dispDev}}" output 2 "Removing routing for '$displayText' " interface_routing 'destroy' "$_tid" "$_mark" "$iface" "" "$dev4" "" "$dev6" "$_priority" if is_netifd_interface "$iface"; then output_okb; else output_ok; fi @@ -2484,9 +2601,9 @@ process_interface() { dispGw4="${gw4:-0.0.0.0}" dispGw6="${gw6:-::/0}" if is_split_uplink; then - if is_wan "$iface"; then + if is_uplink4 "$iface"; then gw6=""; dev6="" - elif is_wan6 "$iface"; then + elif is_uplink6 "$iface"; then gw4=""; dev4="" fi fi @@ -2497,8 +2614,8 @@ process_interface() { if is_netifd_interface_default "$iface"; then [ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__" fi - displayText="${iface}/${dispDev:+$dispDev/}${dispGw4}${ipv6_enabled:+/$dispGw6}" - gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ $dispStatus}\n" + displayText="${iface}/${dispDev:+${dispDev}/}${dispGw4}${ipv6_enabled:+/${dispGw6}}" + gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n" ;; reload_interface) if [ -z "$splitUplinkSecondIface" ]; then @@ -2513,9 +2630,9 @@ process_interface() { dispGw4="${gw4:-0.0.0.0}" dispGw6="${gw6:-::/0}" if is_split_uplink; then - if is_wan "$iface"; then + if is_uplink4 "$iface"; then gw6=""; dev6="" - elif is_wan6 "$iface"; then + elif is_uplink6 "$iface"; then gw4=""; dev4="" fi fi @@ -2526,12 +2643,12 @@ process_interface() { if is_netifd_interface_default "$iface"; then [ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__" fi - displayText="${iface}/${dispDev:+$dispDev/}${dispGw4}${ipv6_enabled:+/$dispGw6}" + displayText="${iface}/${dispDev:+${dispDev}/}${dispGw4}${ipv6_enabled:+/${dispGw6}}" 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" - gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ $dispStatus}\n" + gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n" if is_netifd_interface "$iface"; then output_okb; else output_ok; fi else json add error 'errorFailedReload' "$displayText" @@ -2539,13 +2656,13 @@ process_interface() { fi else json_add_gateway 'skip_interface' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority" "$dispStatus" - gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ $dispStatus}\n" + gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n" fi ;; esac if is_split_uplink && [ -z "$splitUplinkSecondIface" ]; then - if is_wan "$iface" || is_wan6 "$iface"; then + if is_uplink4 "$iface" || is_uplink6 "$iface"; then _uplinkTableID="$_tid" fi fi @@ -2638,6 +2755,10 @@ start_service() { local tid enum_tid tid="$(get_rt_tables_id "$reloadedIface")" enum_tid="$(eval echo "\$enum_tid_${reloadedIface//-/_}")" + if is_split_uplink && is_uplink6 "$reloadedIface" && { [ -z "$tid" ] || [ -z "$enum_tid" ]; }; then + tid="$(get_rt_tables_id "$uplink_interface4")" + enum_tid="$(eval echo "\$enum_tid_${uplink_interface4//-/_}")" + fi if [ "$tid" = "$enum_tid" ]; then serviceStartTrigger='on_interface_reload' else @@ -2730,7 +2851,6 @@ start_service() { output_1_newline fi nft_file 'install' 'main' - resolver 'compare_hash' && resolver 'restart' ;; esac @@ -2761,15 +2881,15 @@ start_service() { procd_close_instance } -service_running() { procd_set_config_changed firewall; } +service_running() { is_service_running; } service_started() { [ -n "$pbrBootFlag" ] && return 0 local error warning c if nft_file 'exists' 'main'; then - procd_set_config_changed firewall - [ -n "$gatewaySummary" ] && output "$serviceName (fw4 nft file mode) started with gateways:\n${gatewaySummary}" + resolver 'compare_hash' && resolver 'restart' + [ -n "$gatewaySummary" ] && output "$serviceName started with gateways:\n${gatewaySummary}" else - output "$serviceName FAILED TO START in fw4 nft file mode!!!\n" + output "$serviceName FAILED TO START!!!\n" output "Check the output of nft -c -f $nftTempFile\n" fi warning="$(json get warning)" @@ -2779,7 +2899,7 @@ service_started() { info="$(json get warning "$c" 'info')" output_warning "$(get_text "$code" "$info")" done - output_warning "$(get_text 'warningSummary' "$(get_url '#WarningMessagesDetails')")" + output_warning "$(get_text 'warningSummary' "$(get_url '#warning-messages-details')")" fi error="$(json get error)" if [ -n "$error" ]; then @@ -2788,7 +2908,7 @@ service_started() { info="$(json get error "$c" 'info')" output_error "$(get_text "$code" "$info")" done - output_error "$(get_text 'errorSummary' "$(get_url '#ErrorMessagesDetails')")" + output_error "$(get_text 'errorSummary' "$(get_url '#error-messages-details')")" fi touch "$packageLockFile" if [ -n "$error" ]; then @@ -2845,7 +2965,7 @@ stop_service() { fi output 'Resetting routing ' if nft_file 'delete' 'main' && \ - cleanup 'main_table' 'rt_tables' 'main_chains' && \ + cleanup 'main_table' 'rt_tables' && \ ip route flush cache; then output_okn else @@ -2886,13 +3006,13 @@ status_service() { if [ -n "$uplink_interface4" ]; then network_get_device dev4 "$uplink_interface4" [ -z "$dev4" ] && network_get_physdev dev4 "$uplink_interface4" - status="${status}Uplink (IPv4): ${uplink_interface4}${dev4:+/$dev4}/${uplinkGW4:-0.0.0.0}.\n" + status="${status}Uplink (IPv4): ${uplink_interface4}${dev4:+/${dev4}}/${uplinkGW4:-0.0.0.0}.\n" fi if [ -n "$uplink_interface6" ]; then network_get_device dev6 "$uplink_interface6" [ -z "$dev6" ] && network_get_physdev dev6 "$uplink_interface6" [ -z "$dev6" ] && dev6="$dev4" - status="${status}Uplink (IPv6): ${uplink_interface6}${dev6:+/$dev6}/${uplinkGW6:-::/0}.\n" + status="${status}Uplink (IPv6): ${uplink_interface6}${dev6:+/${dev6}}/${uplinkGW6:-::/0}.\n" fi echo "$_SEPARATOR_" @@ -2936,9 +3056,9 @@ status_service() { wanTID=$(($(get_rt_tables_next_id)-tableCount)) for tid in main $(seq "$wanTID" $((wanTID + tableCount - 1))); do status_table="$(grep "^${tid}[[:space:]]" "$rtTablesFile" | awk '{print $2}')" - echo "IPv4 table ${tid}${status_table:+ ($status_table)} routes:" + echo "IPv4 table ${tid}${status_table:+ (${status_table})} routes:" ip -4 route show table "$tid" | sed 's/^/ /' - echo "IPv4 table ${tid}${status_table:+ ($status_table)} rules:" + echo "IPv4 table ${tid}${status_table:+ (${status_table})} rules:" ip -4 rule list table "$tid" | sed 's/^/ /' if [ -n "$ipv6_enabled" ]; then echo "$_SEPARATOR_" @@ -2951,9 +3071,114 @@ status_service() { done } +print_config_masked() { + local file="$1" + [ ! -f "/etc/config/$file" ] && return + printf "\n===== %s config =====\n" "$file" + + awk -v sq="'" ' + BEGIN { + masklist = "^(endpoint_host|key|password|preshared_key|private_key|psk|public_key|token|username)$" + } + + /^[ \t]*(option|list)[ \t]+/ { + orig = $0 + + # capture indentation + match(orig, /^[ \t]*/) + indent = substr(orig, RSTART, RLENGTH) + + # capture kind: "option" or "list" + tmp = orig + sub(/^[ \t]*/, "", tmp) + kind = tmp + sub(/[ \t].*$/, "", kind) + + # remove leading indent + kind + line = orig + sub(/^[ \t]*(option|list)[ \t]+/, "", line) + + # extract key name + key = line + sub(/[ \t].*$/, "", key) + + # extract value (rest after key) + sub(/^[^ \t]+[ \t]+/, "", line) + val = line + sub(/^[ \t]+/, "", val) + sub(/[ \t]+$/, "", val) + + # If this key is in masklist, mask the value (preserve dots and length) + if (key ~ masklist) { + q = "" + if (val ~ "^" sq ".*" sq "$") { q = sq; val = substr(val, 2, length(val)-2) } + else if (val ~ /^".*"$/) { q = "\""; val = substr(val, 2, length(val)-2) } + + masked = val + gsub(/[^.]/, "*", masked) + + if (q != "") + print indent kind " " key " " q masked q + else + print indent kind " " key " " masked + next + } + + # Otherwise print unchanged for now + print orig + next + } + + { print } + ' "/etc/config/$file" \ + | awk ' + # Do NOT mask IPs (v4) in allowed_ips (option or list) + /^[ \t]*(option|list)[ \t]+allowed_ips[ \t]+/ { + print + next + } + + { + line = $0 + + # Mask digits inside IPv4-looking tokens, keep dots + while (match(line, /([0-9]{1,3}\.){3}[0-9]{1,3}/)) { + ip = substr(line, RSTART, RLENGTH) + masked = ip + gsub(/[0-9]/, "*", masked) + line = substr(line, 1, RSTART-1) masked substr(line, RSTART+RLENGTH) + } + + print line + } + ' \ + | sed -E 's/([a-fA-F0-9:]{2,}:){1,7}[a-fA-F0-9]{2,}/***/g' +} + +support() { + echo "Setting counters and verbosity for diagnostics..." + uci set pbr.config.nft_rule_counter='1' + uci set pbr.config.nft_set_counter='1' + uci set pbr.config.verbosity='2' + uci commit pbr + + for cfg in dhcp firewall network pbr; do + print_config_masked "$cfg" + done + + printf "\n===== ubus call system board =====\n" + ubus call system board + + printf "\n===== /etc/init.d/pbr restart =====\n" + /etc/init.d/pbr restart + + printf "\n===== /etc/init.d/pbr status (after restart) =====\n" + /etc/init.d/pbr status +} + # shellcheck disable=SC2120 load_validate_config() { - uci_load_validate "$packageName" "$packageName" "$1" "${2}${3:+ $3}" \ + uci_load_validate "$packageName" "$packageName" "$1" "${2}${3:+ ${3}}" \ 'enabled:bool:0' \ 'strict_enforcement:bool:1' \ 'ipv6_enabled:bool:0' \ @@ -2961,7 +3186,7 @@ load_validate_config() { 'resolver_instance:list(or(integer, string)):*' \ 'verbosity:range(0,2):2' \ 'uplink_mark:regex("[A-Fa-f0-9]{8}"):00010000' \ - 'uplink_ip_rules_priority:uinteger:30000' \ + 'uplink_ip_rules_priority:range(99,32765):30000' \ 'fw_mask:regex("[A-Fa-f0-9]{8}"):00ff0000' \ 'icmp_interface:or("", tor, uci("network", "@interface"))' \ 'ignored_interface:list(or(tor, uci("network", "@interface")))' \ @@ -2991,7 +3216,7 @@ load_validate_dns_policy() { local src_addr local dest_dns local dest_dns_port - uci_load_validate "$packageName" 'dns_policy' "$1" "${2}${3:+ $3}" \ + uci_load_validate "$packageName" 'dns_policy' "$1" "${2}${3:+ ${3}}" \ 'name:string:Untitled' \ 'enabled:bool:1' \ 'src_addr:list(neg(or(host,network,macaddr,string)))' \ @@ -3011,7 +3236,7 @@ load_validate_policy() { local src_port local dest_addr local dest_port - uci_load_validate "$packageName" 'policy' "$1" "${2}${3:+ $3}" \ + uci_load_validate "$packageName" 'policy' "$1" "${2}${3:+ ${3}}" \ 'name:string:Untitled' \ 'enabled:bool:1' \ 'interface:or("ignore", "tor", regex("xray_.*"), uci("network", "@interface")):wan' \ @@ -3028,7 +3253,7 @@ load_validate_policy() { load_validate_include() { local path= local enabled= - uci_load_validate "$packageName" 'include' "$1" "${2}${3:+ $3}" \ + uci_load_validate "$packageName" 'include' "$1" "${2}${3:+ ${3}}" \ 'path:file' \ 'enabled:bool:0' \ ; diff --git a/net/pbr/files/usr/share/pbr/pbr.user.netflix b/net/pbr/files/usr/share/pbr/pbr.user.netflix index 3bbe09b830..2c3c8c7630 100644 --- a/net/pbr/files/usr/share/pbr/pbr.user.netflix +++ b/net/pbr/files/usr/share/pbr/pbr.user.netflix @@ -35,7 +35,7 @@ fi if [ -s "$TARGET_DL_FILE_4" ]; then params= - while read -r p; do params="${params:+$params, }${p}"; done < "$TARGET_DL_FILE_4" + while read -r p; do params="${params:+${params}, }${p}"; done < "$TARGET_DL_FILE_4" [ -n "$params" ] && nft "add element $TARGET_TABLE $TARGET_NFTSET_4 { $params }" || _ret=1 fi @@ -47,7 +47,7 @@ if [ -n "$TARGET_DL_FILE_6" ] && [ ! -s "$TARGET_DL_FILE_6" ]; then fi if [ -s "$TARGET_DL_FILE_6" ]; then params= - while read -r p; do params="${params:+$params, }${p}"; done < "$TARGET_DL_FILE_6" + while read -r p; do params="${params:+${params}, }${p}"; done < "$TARGET_DL_FILE_6" [ -n "$params" ] && nft "add element $TARGET_TABLE $TARGET_NFTSET_6 { $params }" || _ret=1 fi diff --git a/net/pbr/tests/01_validation/01_ipv4_validation b/net/pbr/tests/01_validation/01_ipv4_validation new file mode 100644 index 0000000000..5107d75d3c --- /dev/null +++ b/net/pbr/tests/01_validation/01_ipv4_validation @@ -0,0 +1,37 @@ +#!/bin/bash +# Test: IPv4 address validation +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testIpv4ValidStandard() { + assertTrue "Standard private IP" "is_ipv4 '192.168.1.1'" + assertTrue "Class A private" "is_ipv4 '10.0.0.1'" + assertTrue "Class B private" "is_ipv4 '172.16.0.1'" + assertTrue "Google DNS" "is_ipv4 '8.8.8.8'" + assertTrue "All zeros" "is_ipv4 '0.0.0.0'" + assertTrue "All ones" "is_ipv4 '255.255.255.255'" + assertTrue "Simple IP" "is_ipv4 '1.2.3.4'" +} + +testIpv4ValidCIDR() { + assertTrue "CIDR /8" "is_ipv4 '10.0.0.0/8'" + assertTrue "CIDR /24" "is_ipv4 '192.168.1.0/24'" + assertTrue "CIDR /32" "is_ipv4 '10.0.0.1/32'" + assertTrue "Default route" "is_ipv4 '0.0.0.0/0'" +} + +testIpv4Invalid() { + assertFalse "Octet > 255" "is_ipv4 '256.1.1.1'" + assertFalse "Last octet > 255" "is_ipv4 '1.2.3.256'" + assertFalse "Not an IP" "is_ipv4 'not_an_ip'" + assertFalse "Empty string" "is_ipv4 ''" + assertFalse "Only 3 octets" "is_ipv4 '192.168.1'" + assertFalse "5 octets" "is_ipv4 '192.168.1.1.1'" + assertFalse "CIDR > 32" "is_ipv4 '192.168.1.1/33'" + assertFalse "IPv6 loopback" "is_ipv4 '::1'" + assertFalse "IPv6 link-local" "is_ipv4 'fe80::1'" + assertFalse "Domain name" "is_ipv4 'example.com'" +} + +. shunit2 diff --git a/net/pbr/tests/01_validation/02_ipv6_validation b/net/pbr/tests/01_validation/02_ipv6_validation new file mode 100644 index 0000000000..f5bb017c04 --- /dev/null +++ b/net/pbr/tests/01_validation/02_ipv6_validation @@ -0,0 +1,47 @@ +#!/bin/bash +# Test: IPv6 address validation and scope detection +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testIpv6Valid() { + assertTrue "Loopback" "is_ipv6 '::1'" + assertTrue "Link-local" "is_ipv6 'fe80::1'" + assertTrue "Documentation prefix" "is_ipv6 '2001:db8::1'" + assertTrue "Unique local" "is_ipv6 'fd00::1'" + assertTrue "Full address" "is_ipv6 '2001:0db8:85a3::8a2e:0370:7334'" + assertTrue "Default route" "is_ipv6 '::/0'" +} + +testIpv6Invalid() { + assertFalse "IPv4 address" "is_ipv6 '192.168.1.1'" + assertFalse "Plain string" "is_ipv6 'not_ipv6'" + assertFalse "Empty string" "is_ipv6 ''" + assertFalse "MAC address" "is_ipv6 'AA:BB:CC:DD:EE:FF'" +} + +testIpv6GlobalScope() { + assertTrue "Global scope 2001" "is_ipv6_global_scope '2001:db8::1'" + assertFalse "Link-local not global" "is_ipv6_global_scope 'fe80::1'" + assertFalse "ULA not global" "is_ipv6_global_scope 'fd00::1'" +} + +testIpv6LinkLocal() { + assertTrue "Link-local fe80" "is_ipv6_local_link 'fe80::1'" + assertFalse "Global not link-local" "is_ipv6_local_link '2001::1'" +} + +testIpv6UniqueLocal() { + assertTrue "ULA fd" "is_ipv6_local_unique 'fd00::1'" + assertTrue "ULA fc" "is_ipv6_local_unique 'fc00::1'" + assertFalse "Link-local not ULA" "is_ipv6_local_unique 'fe80::1'" + assertFalse "Global not ULA" "is_ipv6_local_unique '2001::1'" +} + +testIpv6LocalScope() { + assertTrue "Link-local is local scope" "is_ipv6_local_scope 'fe80::1'" + assertTrue "ULA is local scope" "is_ipv6_local_scope 'fd00::1'" + assertFalse "Global not local scope" "is_ipv6_local_scope '2001::1'" +} + +. shunit2 diff --git a/net/pbr/tests/01_validation/03_domain_validation b/net/pbr/tests/01_validation/03_domain_validation new file mode 100644 index 0000000000..204a270c41 --- /dev/null +++ b/net/pbr/tests/01_validation/03_domain_validation @@ -0,0 +1,35 @@ +#!/bin/bash +# Test: Domain, host, and hostname validation +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testIsHost() { + assertTrue "Simple hostname" "is_host 'router'" + assertTrue "Hostname with hyphen" "is_host 'my-host'" + assertTrue "Hostname with numbers" "is_host 'host123'" + assertTrue "Single character" "is_host 'A'" + assertFalse "Empty string" "is_host ''" + assertFalse "Starts with hyphen" "is_host '-invalid'" +} + +testIsHostname() { + assertTrue "Simple domain" "is_hostname 'example.com'" + assertTrue "Subdomain" "is_hostname 'sub.example.com'" + assertTrue "Deep subdomain" "is_hostname 'deep.sub.example.com'" + assertTrue "Hyphenated with ccTLD" "is_hostname 'my-site.co.uk'" + assertFalse "Single label" "is_hostname 'localhost'" + assertFalse "Empty string" "is_hostname ''" + assertFalse "IP address" "is_hostname '192.168.1.1'" +} + +testIsDomain() { + assertTrue "Standard domain" "is_domain 'example.com'" + assertTrue "Single-label host" "is_domain 'router'" + assertTrue "Local domain" "is_domain 'my-server.local'" + assertFalse "IPv4 not a domain" "is_domain '192.168.1.1'" + assertFalse "Empty string" "is_domain ''" + assertFalse "Bad MAC notation" "is_domain 'AA-BB-CC-DD-EE-FF'" +} + +. shunit2 diff --git a/net/pbr/tests/01_validation/04_misc_validators b/net/pbr/tests/01_validation/04_misc_validators new file mode 100755 index 0000000000..c8df0b8c9b --- /dev/null +++ b/net/pbr/tests/01_validation/04_misc_validators @@ -0,0 +1,71 @@ +#!/bin/bash +# Test: Miscellaneous validators (MAC, integer, URL, negation, version comparison) +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testMacAddress() { + assertTrue "Uppercase MAC" "is_mac_address 'AA:BB:CC:DD:EE:FF'" + assertTrue "Lowercase MAC" "is_mac_address 'aa:bb:cc:dd:ee:ff'" + assertTrue "Numeric MAC" "is_mac_address '00:11:22:33:44:55'" + assertFalse "Too short" "is_mac_address 'AA:BB:CC:DD:EE'" + assertFalse "Too long" "is_mac_address 'AA:BB:CC:DD:EE:FF:00'" + assertFalse "Dash notation" "is_mac_address 'AA-BB-CC-DD-EE-FF'" + assertFalse "Not a MAC" "is_mac_address 'not_a_mac'" + assertFalse "Empty string" "is_mac_address ''" +} + +testMacAddressBadNotation() { + assertTrue "Dash notation" "is_mac_address_bad_notation 'AA-BB-CC-DD-EE-FF'" + assertFalse "Colon notation" "is_mac_address_bad_notation 'AA:BB:CC:DD:EE:FF'" +} + +testIsInteger() { + assertTrue "Zero" "is_integer '0'" + assertTrue "Positive" "is_integer '123'" + assertTrue "Large number" "is_integer '999999'" + assertFalse "Empty string" "is_integer ''" + assertFalse "Letters" "is_integer 'abc'" + assertFalse "Decimal" "is_integer '12.34'" + assertFalse "Negative" "is_integer '-1'" +} + +testIsNegated() { + assertTrue "Negated IP" "is_negated '!192.168.1.1'" + assertTrue "Negated domain" "is_negated '!example.com'" + assertFalse "Not negated" "is_negated '192.168.1.1'" + assertFalse "Empty string" "is_negated ''" +} + +testUrlValidators() { + assertTrue "HTTP URL" "is_url_http 'http://example.com'" + assertTrue "HTTPS URL" "is_url_https 'https://example.com'" + assertTrue "FTP URL" "is_url_ftp 'ftp://files.example.com'" + assertTrue "File URL" "is_url_file 'file:///tmp/list.txt'" + assertFalse "HTTPS is not HTTP" "is_url_http 'https://example.com'" + assertFalse "HTTP is not HTTPS" "is_url_https 'http://example.com'" + assertTrue "HTTP is URL" "is_url 'http://example.com'" + assertTrue "HTTPS is URL" "is_url 'https://example.com'" + assertTrue "FTP is URL" "is_url 'ftp://example.com'" + assertTrue "File is URL" "is_url 'file:///tmp/x'" + assertFalse "Plain domain not URL" "is_url 'example.com'" +} + +testVersionComparison() { + assertTrue "2.0 > 1.0" "is_greater '2.0' '1.0'" + assertTrue "1.10 > 1.9" "is_greater '1.10' '1.9'" + assertFalse "1.0 not > 2.0" "is_greater '1.0' '2.0'" + assertFalse "Equal not greater" "is_greater '1.0' '1.0'" + assertTrue "Equal is >=" "is_greater_or_equal '1.0' '1.0'" + assertTrue "Greater is >=" "is_greater_or_equal '2.0' '1.0'" + assertFalse "Lesser not >=" "is_greater_or_equal '1.0' '2.0'" +} + +testFamilyMismatch() { + assertTrue "IPv4 src IPv6 dst" "is_family_mismatch '192.168.1.1' '::1'" + assertTrue "IPv6 src IPv4 dst" "is_family_mismatch '::1' '10.0.0.1'" + assertFalse "Both IPv4" "is_family_mismatch '10.0.0.1' '10.0.0.2'" + assertFalse "Both IPv6" "is_family_mismatch '::1' '::2'" +} + +. shunit2 diff --git a/net/pbr/tests/02_string_utils/01_str_functions b/net/pbr/tests/02_string_utils/01_str_functions new file mode 100755 index 0000000000..5fb7b7df32 --- /dev/null +++ b/net/pbr/tests/02_string_utils/01_str_functions @@ -0,0 +1,61 @@ +#!/bin/bash +# Test: String utility functions +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testStrContains() { + assertTrue "Contains word" "str_contains 'hello world' 'world'" + assertTrue "Contains substring" "str_contains 'hello world' 'lo wo'" + assertTrue "Contains middle" "str_contains 'abcdef' 'bcd'" + assertFalse "Does not contain" "str_contains 'hello' 'xyz'" + assertFalse "Empty haystack" "str_contains '' 'test'" + # In bash, ${1//} with empty pattern doesn't remove anything, so returns false + assertFalse "Empty needle returns false" "str_contains 'hello' ''" +} + +testStrContainsWord() { + assertTrue "Contains exact word" "str_contains_word 'one two three' 'two'" + assertFalse "Partial not word match" "str_contains_word 'one twothree' 'two'" + assertTrue "Single word" "str_contains_word 'one' 'one'" + assertFalse "Word not present" "str_contains_word 'one two three' 'four'" +} + +testStrToLower() { + assertEquals "All caps to lower" "hello" "$(str_to_lower 'HELLO')" + assertEquals "Mixed case" "hello" "$(str_to_lower 'Hello')" + assertEquals "Already lowercase" "hello" "$(str_to_lower 'hello')" + assertEquals "With numbers" "123abc" "$(str_to_lower '123ABC')" +} + +testStrToUpper() { + assertEquals "All lower to upper" "HELLO" "$(str_to_upper 'hello')" + assertEquals "Mixed case" "HELLO" "$(str_to_upper 'Hello')" + assertEquals "With numbers" "123ABC" "$(str_to_upper '123abc')" +} + +testStrFirstWord() { + assertEquals "First of two" "hello" "$(str_first_word 'hello world')" + assertEquals "First of three" "one" "$(str_first_word 'one two three')" + assertEquals "Single word" "single" "$(str_first_word 'single')" +} + +testStrReplace() { + assertEquals "Replace word" "hello universe" "$(str_replace 'hello world' 'world' 'universe')" + assertEquals "Replace dots" "aXbXc" "$(str_replace 'a.b.c' '.' 'X')" + assertEquals "No match unchanged" "hello world" "$(str_replace 'hello world' 'xyz' 'abc')" +} + +testStrExtrasToUnderscore() { + assertEquals "Dot to underscore" "hello_world" "$(str_extras_to_underscore 'hello.world')" + assertEquals "Spaces to underscores" "a_b_c" "$(str_extras_to_underscore 'a b c')" + assertEquals "Slash to underscore" "test_path" "$(str_extras_to_underscore 'test/path')" + assertEquals "Multiple dots collapsed" "no_dups" "$(str_extras_to_underscore 'no..dups')" +} + +testStrExtrasToSpace() { + assertEquals "Delimiters to spaces" "a b c d" "$(str_extras_to_space 'a,b;c{d')" + assertEquals "Closing brace to space" "a b" "$(str_extras_to_space 'a}b')" +} + +. shunit2 diff --git a/net/pbr/tests/03_wan_detection/01_wan_types b/net/pbr/tests/03_wan_detection/01_wan_types new file mode 100755 index 0000000000..156ca85c9a --- /dev/null +++ b/net/pbr/tests/03_wan_detection/01_wan_types @@ -0,0 +1,72 @@ +#!/bin/bash +# Test: WAN/interface type detection functions +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testIsWan4() { + assertTrue "Standard wan" "is_wan4 'wan'" + assertTrue "wan prefix" "is_wan4 'wanX'" + assertFalse "wan6 is not wan4" "is_wan4 'wan6'" + assertFalse "Ends with wan6" "is_wan4 'mwan6'" + assertFalse "LAN not wan4" "is_wan4 'lan'" + assertFalse "Wireguard not wan4" "is_wan4 'wg0'" +} + +testIsWan6() { + ipv6_enabled='1' + assertTrue "Standard wan6" "is_wan6 'wan6'" + assertTrue "Ends with wan6" "is_wan6 'mwan6'" + assertFalse "wan is not wan6" "is_wan6 'wan'" + assertFalse "LAN not wan6" "is_wan6 'lan'" +} + +testIsWan6Disabled() { + unset ipv6_enabled + assertFalse "wan6 without ipv6 disabled" "is_wan6 'wan6'" +} + +testIsWan() { + ipv6_enabled='1' + assertTrue "wan matches" "is_wan 'wan'" + assertTrue "wan6 matches" "is_wan 'wan6'" + assertFalse "LAN not wan" "is_wan 'lan'" + assertFalse "Wireguard not wan" "is_wan 'wg0'" +} + +testIsUplink() { + uplink_interface4="wan" + uplink_interface6="wan6" + ipv6_enabled='1' + assertTrue "wan is uplink4" "is_uplink4 'wan'" + assertFalse "wan6 is not uplink4" "is_uplink4 'wan6'" + assertTrue "wan6 is uplink6" "is_uplink6 'wan6'" + assertFalse "wan is not uplink6" "is_uplink6 'wan'" + assertTrue "wan is uplink" "is_uplink 'wan'" + assertTrue "wan6 is uplink" "is_uplink 'wan6'" + assertFalse "wg0 is not uplink" "is_uplink 'wg0'" +} + +testIsTor() { + assertTrue "Lowercase tor" "is_tor 'tor'" + assertTrue "Uppercase TOR" "is_tor 'TOR'" + assertTrue "Mixed case Tor" "is_tor 'Tor'" + assertFalse "Not tor" "is_tor 'vpn'" +} + +testIsIgnoreTarget() { + assertTrue "Lowercase ignore" "is_ignore_target 'ignore'" + assertTrue "Uppercase IGNORE" "is_ignore_target 'IGNORE'" + assertTrue "Mixed case" "is_ignore_target 'Ignore'" + assertFalse "Not ignore" "is_ignore_target 'wan'" +} + +testIsList() { + assertTrue "Comma-separated" "is_list 'a,b'" + assertTrue "Space-separated" "is_list 'a b'" + assertTrue "Multiple commas" "is_list 'a,b,c'" + assertFalse "Single value" "is_list 'single'" + assertFalse "Empty string" "is_list ''" +} + +. shunit2 diff --git a/net/pbr/tests/04_config/01_load_config b/net/pbr/tests/04_config/01_load_config new file mode 100755 index 0000000000..6311cef67d --- /dev/null +++ b/net/pbr/tests/04_config/01_load_config @@ -0,0 +1,58 @@ +#!/bin/bash +# Test: Package config loading +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testLoadBasicConfig() { + load_package_config + assertNotNull "enabled is set" "$enabled" + assertEquals "verbosity" "2" "$verbosity" + assertEquals "uplink_interface4" "wan" "$uplink_interface4" + assertEquals "uplink_ip_rules_priority" "30000" "$uplink_ip_rules_priority" + assertEquals "procd_boot_trigger_delay" "5000" "$procd_boot_trigger_delay" +} + +testLoadHexValues() { + load_package_config + assertEquals "fw_mask hex" "0x00ff0000" "$fw_mask" + assertEquals "uplink_mark hex" "0x00010000" "$uplink_mark" +} + +testFwMaskXor() { + load_package_config + assertNotNull "fw_maskXor computed" "${fw_maskXor:-}" + assertEquals "fw_maskXor value" "0xff00ffff" "$fw_maskXor" +} + +testIpv6DisabledConfig() { + load_package_config + assertNull "ipv6_enabled unset when 0" "${ipv6_enabled:-}" + assertNull "uplink_interface6 unset" "${uplink_interface6:-}" +} + +testStrictEnforcement() { + load_package_config + assertNotNull "strict_enforcement set" "${strict_enforcement:-}" +} + +testNftSetParams() { + load_package_config + echo "$nftSetParams" | grep -q 'auto-merge' + assertTrue "nft auto-merge enabled" $? + echo "$nftSetParams" | grep -q 'flags interval' + assertTrue "nft flags interval enabled" $? +} + +testLoadPackageConfigFlag() { + load_package_config + assertEquals "flag set" "true" "$loadPackageConfigFlag" +} + +testIgnoredInterfaceList() { + load_package_config + echo "$ignored_interface" | grep -qF 'loopback' + assertTrue "loopback in ignored_interface" $? +} + +. shunit2 diff --git a/net/pbr/tests/04_config/02_disabled_service b/net/pbr/tests/04_config/02_disabled_service new file mode 100755 index 0000000000..1abd5f1510 --- /dev/null +++ b/net/pbr/tests/04_config/02_disabled_service @@ -0,0 +1,28 @@ +#!/bin/bash +# Test: Disabled service detection +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testDisabledService() { + cp "$MOCK_ROOT/etc/config/pbr" "$MOCK_ROOT/etc/config/pbr.bak" + sed -i "s/option enabled '1'/option enabled '0'/" "$MOCK_ROOT/etc/config/pbr" + + _CONFIG_LOADED_PKG="" + loadPackageConfigFlag="" + load_package_config + + assertNull "enabled is unset when service disabled" "${enabled:-}" + + cp "$MOCK_ROOT/etc/config/pbr.bak" "$MOCK_ROOT/etc/config/pbr" +} + +testEnabledService() { + _CONFIG_LOADED_PKG="" + loadPackageConfigFlag="" + load_package_config + + assertNotNull "enabled is set when service enabled" "$enabled" +} + +. shunit2 diff --git a/net/pbr/tests/04_config/03_config_ipv6 b/net/pbr/tests/04_config/03_config_ipv6 new file mode 100755 index 0000000000..58ab6b32ee --- /dev/null +++ b/net/pbr/tests/04_config/03_config_ipv6 @@ -0,0 +1,31 @@ +#!/bin/bash +# Test: IPv6 config variations +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +testIpv6Enabled() { + cp "$MOCK_ROOT/etc/config/pbr" "$MOCK_ROOT/etc/config/pbr.bak" + sed -i "s/option ipv6_enabled '0'/option ipv6_enabled '1'/" "$MOCK_ROOT/etc/config/pbr" + + _CONFIG_LOADED_PKG="" + loadPackageConfigFlag="" + load_package_config + + assertNotNull "ipv6_enabled is set" "${ipv6_enabled:-}" + assertEquals "uplink_interface6" "wan6" "${uplink_interface6:-}" + assertTrue "wan6 detected" "is_wan6 'wan6'" + + cp "$MOCK_ROOT/etc/config/pbr.bak" "$MOCK_ROOT/etc/config/pbr" +} + +testIpv6Disabled() { + _CONFIG_LOADED_PKG="" + loadPackageConfigFlag="" + load_package_config + + assertNull "ipv6_enabled unset" "${ipv6_enabled:-}" + assertNull "uplink_interface6 unset" "${uplink_interface6:-}" +} + +. shunit2 diff --git a/net/pbr/tests/05_nft/01_nft_file_operations b/net/pbr/tests/05_nft/01_nft_file_operations new file mode 100755 index 0000000000..82ee480b52 --- /dev/null +++ b/net/pbr/tests/05_nft/01_nft_file_operations @@ -0,0 +1,64 @@ +#!/bin/bash +# Test: nft file operations (create, add, match, delete) +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +setUp() { + mkdir -p "$(dirname "$nftTempFile")" 2>/dev/null || true + mkdir -p "$(dirname "$nftMainFile")" 2>/dev/null || true + rm -f "$nftTempFile" "$nftMainFile" + load_package_config +} + +tearDown() { + rm -f "$nftTempFile" "$nftMainFile" +} + +testNftFileCreate() { + nft_file 'create' 'main' + assertTrue "nft temp file created" "[ -f '$nftTempFile' ]" + assertTrue "Has nft shebang" "grep -q '#!/usr/sbin/nft -f' '$nftTempFile'" +} + +testNftFileChains() { + nft_file 'create' 'main' + assertTrue "dstnat chain" "grep -q 'add chain inet fw4 pbr_dstnat' '$nftTempFile'" + assertTrue "forward chain" "grep -q 'add chain inet fw4 pbr_forward' '$nftTempFile'" + assertTrue "output chain" "grep -q 'add chain inet fw4 pbr_output' '$nftTempFile'" + assertTrue "prerouting chain" "grep -q 'add chain inet fw4 pbr_prerouting' '$nftTempFile'" +} + +testNftFileJumpRules() { + nft_file 'create' 'main' + assertTrue "jump to dstnat" "grep -q 'jump pbr_dstnat' '$nftTempFile'" + assertTrue "jump to prerouting" "grep -q 'jump pbr_prerouting' '$nftTempFile'" + assertTrue "jump to output" "grep -q 'jump pbr_output' '$nftTempFile'" + assertTrue "jump to forward" "grep -q 'jump pbr_forward' '$nftTempFile'" +} + +testNftFileGuardRules() { + nft_file 'create' 'main' + assertTrue "Guard rule" "grep -q 'meta mark & 0x00ff0000 != 0 return' '$nftTempFile'" +} + +testNftFileAdd() { + nft_file 'create' 'main' + nft_file 'add' 'main' 'add rule inet fw4 pbr_prerouting ip saddr 192.168.1.0/24 goto pbr_mark_0x00010000' + assertTrue "Added rule present" "grep -q '192.168.1.0/24' '$nftTempFile'" +} + +testNftFileMatch() { + nft_file 'create' 'main' + assertTrue "Match existing" "nft_file 'match' 'temp' 'pbr_prerouting'" + assertFalse "Match missing" "nft_file 'match' 'temp' 'nonexistent_xyz'" +} + +testNftFileDelete() { + nft_file 'create' 'main' + nft_file 'delete' 'main' + assertFalse "Temp file deleted" "[ -f '$nftTempFile' ]" + assertFalse "Main file deleted" "[ -f '$nftMainFile' ]" +} + +. shunit2 diff --git a/net/pbr/tests/05_nft/02_nft_check_element b/net/pbr/tests/05_nft/02_nft_check_element new file mode 100755 index 0000000000..f188f11e96 --- /dev/null +++ b/net/pbr/tests/05_nft/02_nft_check_element @@ -0,0 +1,30 @@ +#!/bin/bash +# Test: nft_check_element for verifying fw4 table/chain existence +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +setUp() { + nft_fw4_dump="" +} + +testTableExists() { + assertTrue "fw4 table exists" "nft_check_element 'table' 'fw4'" +} + +testChainsExist() { + assertTrue "input chain" "nft_check_element 'chain' 'input'" + assertTrue "forward chain" "nft_check_element 'chain' 'forward'" + assertTrue "output chain" "nft_check_element 'chain' 'output'" + assertTrue "dstnat chain" "nft_check_element 'chain' 'dstnat'" + assertTrue "mangle_prerouting" "nft_check_element 'chain' 'mangle_prerouting'" + assertTrue "mangle_output" "nft_check_element 'chain' 'mangle_output'" + assertTrue "mangle_forward" "nft_check_element 'chain' 'mangle_forward'" +} + +testNonExistentElements() { + assertFalse "Non-existent chain" "nft_check_element 'chain' 'nonexistent_chain'" + assertFalse "srcnat not present" "nft_check_element 'chain' 'srcnat'" +} + +. shunit2 diff --git a/net/pbr/tests/06_network/01_gateway_discovery b/net/pbr/tests/06_network/01_gateway_discovery new file mode 100755 index 0000000000..e0936c7b0e --- /dev/null +++ b/net/pbr/tests/06_network/01_gateway_discovery @@ -0,0 +1,55 @@ +#!/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 diff --git a/net/pbr/tests/06_network/02_supported_interfaces b/net/pbr/tests/06_network/02_supported_interfaces new file mode 100755 index 0000000000..e876bdd7b5 --- /dev/null +++ b/net/pbr/tests/06_network/02_supported_interfaces @@ -0,0 +1,48 @@ +#!/bin/bash +# Test: Interface support detection +. "$(dirname "$0")/../lib/setup.sh" + +oneTimeTearDown() { rm -rf "${MOCK_ROOT:-}"; } + +setUp() { + load_package_config + lan_device="br-lan" + supported_interface="" + ignored_interface="loopback" + uplink_interface4="wan" + uplink_interface6="" +} + +testIgnoredInterface() { + assertTrue "loopback is ignored" "is_ignored_interface 'loopback'" + assertFalse "wan is not ignored" "is_ignored_interface 'wan'" + assertFalse "wg0 is not ignored" "is_ignored_interface 'wg0'" +} + +testIsLan() { + assertTrue "lan is LAN" "is_lan 'lan'" + assertFalse "wan is not LAN" "is_lan 'wan'" +} + +testWanIsSupported() { + assertTrue "wan is supported" "is_supported_interface 'wan'" +} + +testLanNotSupported() { + assertFalse "lan not supported" "is_supported_interface 'lan'" +} + +testLoopbackNotSupported() { + assertFalse "loopback not supported" "is_supported_interface 'loopback'" +} + +testWireguardSupported() { + assertTrue "wg0 supported" "is_supported_interface 'wg0'" +} + +testExplicitlySupportedInterface() { + supported_interface="custom_iface" + assertTrue "Explicitly supported" "is_supported_interface 'custom_iface'" +} + +. shunit2 diff --git a/net/pbr/tests/lib/mocks/functions.sh b/net/pbr/tests/lib/mocks/functions.sh new file mode 100644 index 0000000000..c7e383fb78 --- /dev/null +++ b/net/pbr/tests/lib/mocks/functions.sh @@ -0,0 +1,161 @@ +#!/bin/bash +# Mock /lib/functions.sh for pbr tests +# Implements OpenWrt UCI config shell API backed by UCI-format config files + +# Config state +_CONFIG_LOADED_PKG="" +declare -gA _CONFIG_TYPES # section -> type +declare -gA _CONFIG_OPTS # section.option -> value +declare -gA _CONFIG_LISTS # section.option -> "val1 val2 ..." +_CONFIG_SECTIONS="" + +config_load() { + local package="$1" + local file="${UCI_CONFIG_DIR:-${IPKG_INSTROOT}/etc/config}/${package}" + + # Reset state + _CONFIG_LOADED_PKG="$package" + _CONFIG_TYPES=() + _CONFIG_OPTS=() + _CONFIG_LISTS=() + _CONFIG_SECTIONS="" + + [ -f "$file" ] || return 1 + + local section="" anon_counter=0 + while IFS= read -r line || [ -n "$line" ]; do + # Strip leading whitespace + line="${line#"${line%%[![:space:]]*}"}" + # Skip comments and empty lines + [[ "$line" == \#* || -z "$line" ]] && continue + + if [[ "$line" =~ ^config[[:space:]]+([^[:space:]\'\"]+)[[:space:]]*([\'\"]([^\'\"]*)[\'\"])?(.*)$ ]]; then + local type="${BASH_REMATCH[1]}" + section="${BASH_REMATCH[3]}" + [ -z "$section" ] && section="cfg${anon_counter}" && anon_counter=$((anon_counter + 1)) + _CONFIG_TYPES["$section"]="$type" + _CONFIG_SECTIONS="${_CONFIG_SECTIONS:+$_CONFIG_SECTIONS }$section" + elif [[ "$line" =~ ^option[[:space:]]+([^[:space:]]+)[[:space:]]+[\'\"]([^\'\"]*)[\'\"] ]]; then + local key="${BASH_REMATCH[1]}" + local val="${BASH_REMATCH[2]}" + _CONFIG_OPTS["${section}.${key}"]="$val" + elif [[ "$line" =~ ^option[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+) ]]; then + local key="${BASH_REMATCH[1]}" + local val="${BASH_REMATCH[2]}" + val="${val//\'/}" + val="${val//\"/}" + _CONFIG_OPTS["${section}.${key}"]="$val" + elif [[ "$line" =~ ^list[[:space:]]+([^[:space:]]+)[[:space:]]+[\'\"]([^\'\"]*)[\'\"] ]]; then + local key="${BASH_REMATCH[1]}" + local val="${BASH_REMATCH[2]}" + if [ -n "${_CONFIG_LISTS["${section}.${key}"]:-}" ]; then + _CONFIG_LISTS["${section}.${key}"]="${_CONFIG_LISTS["${section}.${key}"]} $val" + else + _CONFIG_LISTS["${section}.${key}"]="$val" + fi + elif [[ "$line" =~ ^list[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+) ]]; then + local key="${BASH_REMATCH[1]}" + local val="${BASH_REMATCH[2]}" + val="${val//\'/}" + val="${val//\"/}" + if [ -n "${_CONFIG_LISTS["${section}.${key}"]:-}" ]; then + _CONFIG_LISTS["${section}.${key}"]="${_CONFIG_LISTS["${section}.${key}"]} $val" + else + _CONFIG_LISTS["${section}.${key}"]="$val" + fi + fi + done < "$file" +} + +config_get() { + local var="$1" section="$2" option="$3" default="$4" + local key="${section}.${option}" + local val="${_CONFIG_OPTS[$key]:-${_CONFIG_LISTS[$key]:-}}" + [ -z "$val" ] && val="$default" + eval "$var=\"\$val\"" +} + +config_get_bool() { + local var="$1" section="$2" option="$3" default="${4:-0}" + local key="${section}.${option}" + local val="${_CONFIG_OPTS[$key]:-$default}" + case "$val" in + 1|yes|on|true|enabled) val=1;; + *) val=0;; + esac + eval "$var=$val" +} + +config_get_list() { + config_get "$@" +} + +config_foreach() { + local callback="$1" type="$2" + local section + for section in $_CONFIG_SECTIONS; do + [ "${_CONFIG_TYPES[$section]:-}" = "$type" ] && "$callback" "$section" + done +} + +config_list_foreach() { + local section="$1" option="$2" callback="$3" + local key="${section}.${option}" + local val="${_CONFIG_LISTS[$key]:-}" + local item + for item in $val; do + "$callback" "$item" + done +} + +uci_get() { + local package="${1:-}" section="${2:-}" option="${3:-}" default="${4:-}" + [ -z "$package" ] || [ -z "$section" ] && return 1 + # Auto-load if different package + if [ "$_CONFIG_LOADED_PKG" != "$package" ]; then + config_load "$package" + fi + if [ -n "$option" ]; then + local key="${section}.${option}" + echo "${_CONFIG_OPTS[$key]:-${_CONFIG_LISTS[$key]:-$default}}" + else + # Check if section exists + [ -n "${_CONFIG_TYPES[$section]:-}" ] && echo "$section" + fi +} + +uci_add_list() { + local package="$1" section="$2" option="$3" value="$4" + local key="${section}.${option}" + if [ -n "${_CONFIG_LISTS[$key]:-}" ]; then + _CONFIG_LISTS[$key]="${_CONFIG_LISTS[$key]} $value" + else + _CONFIG_LISTS[$key]="$value" + fi +} + +uci_remove() { + local package="$1" section="$2" option="${3:-}" + if [ -n "$option" ]; then + unset "_CONFIG_OPTS[${section}.${option}]" + unset "_CONFIG_LISTS[${section}.${option}]" + fi +} + +uci_remove_list() { + local package="$1" section="$2" option="$3" value="$4" + local key="${section}.${option}" + local old="${_CONFIG_LISTS[$key]:-}" + local new="" item + for item in $old; do + [ "$item" != "$value" ] && new="${new:+$new }$item" + done + _CONFIG_LISTS[$key]="$new" +} + +uci_commit() { :; } + +uci_set() { + local package="$1" section="$2" option="$3" value="$4" + _CONFIG_OPTS["${section}.${option}"]="$value" +} diff --git a/net/pbr/tests/lib/mocks/jshn.sh b/net/pbr/tests/lib/mocks/jshn.sh new file mode 100644 index 0000000000..64e665c31d --- /dev/null +++ b/net/pbr/tests/lib/mocks/jshn.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# Minimal mock /usr/share/libubox/jshn.sh for pbr tests +# Implements enough of the jshn API to support the json() function and procd_open_data + +# Internal state +_JSON_PREFIX="" +_JSON_DEPTH=0 +declare -gA _JSON_DATA +_JSON_CUR_PATH="" +_JSON_KEYS="" +_JSON_NS="" + +json_set_namespace() { + _JSON_NS="${1:-}" +} + +json_init() { + _JSON_DATA=() + _JSON_DEPTH=0 + _JSON_CUR_PATH="" + _JSON_KEYS="" +} + +json_add_string() { + local key="$1" value="$2" + _JSON_DATA["${_JSON_CUR_PATH}${key}"]="$value" +} + +json_add_boolean() { + local key="$1" value="$2" + [ "$value" = "1" ] && value="true" || value="false" + _JSON_DATA["${_JSON_CUR_PATH}${key}"]="$value" +} + +json_add_int() { + local key="$1" value="$2" + _JSON_DATA["${_JSON_CUR_PATH}${key}"]="$value" +} + +json_add_object() { + local key="${1:-}" + if [ -n "$key" ]; then + _JSON_CUR_PATH="${_JSON_CUR_PATH}${key}." + fi + _JSON_DEPTH=$((_JSON_DEPTH + 1)) +} + +json_close_object() { + _JSON_DEPTH=$((_JSON_DEPTH - 1)) + # Pop last path component + if [ -n "$_JSON_CUR_PATH" ]; then + _JSON_CUR_PATH="${_JSON_CUR_PATH%*.}" + _JSON_CUR_PATH="${_JSON_CUR_PATH%.*}" + [ -n "$_JSON_CUR_PATH" ] && _JSON_CUR_PATH="${_JSON_CUR_PATH}." + fi +} + +json_add_array() { + local key="${1:-}" + if [ -n "$key" ]; then + _JSON_CUR_PATH="${_JSON_CUR_PATH}${key}." + _JSON_DATA["${_JSON_CUR_PATH}_type"]="array" + fi + _JSON_DEPTH=$((_JSON_DEPTH + 1)) +} + +json_close_array() { + json_close_object +} + +json_select() { + local key="$1" + if [ "$key" = ".." ]; then + # Go up one level + if [ -n "$_JSON_CUR_PATH" ]; then + _JSON_CUR_PATH="${_JSON_CUR_PATH%*.}" + _JSON_CUR_PATH="${_JSON_CUR_PATH%.*}" + [ -n "$_JSON_CUR_PATH" ] && _JSON_CUR_PATH="${_JSON_CUR_PATH}." + fi + return 0 + fi + # Check if key exists + local prefix="${_JSON_CUR_PATH}${key}." + local found=0 + for k in "${!_JSON_DATA[@]}"; do + if [[ "$k" == "${prefix}"* ]] || [ -n "${_JSON_DATA["${_JSON_CUR_PATH}${key}"]:-}" ]; then + found=1 + break + fi + done + if [ "$found" = "1" ]; then + _JSON_CUR_PATH="$prefix" + return 0 + fi + return 1 +} + +json_get_var() { + local var="$1" key="$2" + local val="${_JSON_DATA["${_JSON_CUR_PATH}${key}"]:-}" + eval "$var=\"\$val\"" +} + +json_get_keys() { + local var="$1" + local prefix="$_JSON_CUR_PATH" + local keys="" k + for k in "${!_JSON_DATA[@]}"; do + if [[ "$k" == "${prefix}"* ]]; then + local rest="${k#"$prefix"}" + local first="${rest%%.*}" + if [ -n "$first" ] && ! echo " $keys " | grep -q " $first "; then + keys="${keys:+$keys }$first" + fi + fi + done + eval "$var=\"\$keys\"" +} + +json_dump() { + # Simple JSON output - enough for testing + echo "{}" +} + +json_load() { + json_init +} + +json_load_file() { + local file="$1" + [ -f "$file" ] || return 1 + json_init + return 0 +} + +json_cleanup() { + json_init +} diff --git a/net/pbr/tests/lib/mocks/network.sh b/net/pbr/tests/lib/mocks/network.sh new file mode 100644 index 0000000000..7778cebada --- /dev/null +++ b/net/pbr/tests/lib/mocks/network.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Mock /lib/functions/network.sh for pbr tests +# Provides configurable network state via MOCK_NET_* variables + +# Default mock network data - tests can override these before calling setup +: "${MOCK_NET_wan_device:=eth0}" +: "${MOCK_NET_wan_gateway:=192.168.1.1}" +: "${MOCK_NET_wan_proto:=dhcp}" +: "${MOCK_NET_wan6_device:=eth0}" +: "${MOCK_NET_wan6_gateway6:=fd00::1}" +: "${MOCK_NET_wan6_proto:=dhcpv6}" +: "${MOCK_NET_wg0_device:=wg0}" +: "${MOCK_NET_wg0_proto:=wireguard}" +: "${MOCK_NET_lan_device:=br-lan}" +: "${MOCK_NET_lan_proto:=static}" +: "${MOCK_NET_loopback_device:=lo}" +: "${MOCK_NET_loopback_proto:=static}" + +_net_get_var() { + local var="$1" iface="$2" field="$3" + local iface_safe="${iface//-/_}" + local val="" + eval "val=\"\${MOCK_NET_${iface_safe}_${field}:-}\"" + eval "$var=\"\$val\"" +} + +network_get_device() { + _net_get_var "$1" "$2" "device" +} + +network_get_physdev() { + _net_get_var "$1" "$2" "device" +} + +network_get_gateway() { + local var="$1" iface="$2" + _net_get_var "$var" "$iface" "gateway" +} + +network_get_gateway6() { + local var="$1" iface="$2" + _net_get_var "$var" "$iface" "gateway6" +} + +network_get_protocol() { + _net_get_var "$1" "$2" "proto" +} + +network_get_ipaddr() { + _net_get_var "$1" "$2" "ipaddr" +} + +network_get_ip6addr() { + _net_get_var "$1" "$2" "ip6addr" +} + +network_flush_cache() { :; } + +network_get_dnsserver() { + _net_get_var "$1" "$2" "dns" +} diff --git a/net/pbr/tests/lib/setup.sh b/net/pbr/tests/lib/setup.sh new file mode 100644 index 0000000000..4531c76e64 --- /dev/null +++ b/net/pbr/tests/lib/setup.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Common test setup for pbr shell tests (shunit2-based) +# Source this at the top of each test file before defining test functions. +# Each test file should end with: . shunit2 + +TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PKG_DIR="$(cd "$TESTS_DIR/.." && pwd)" + +# Create mock sysroot +MOCK_ROOT="$(mktemp -d)" +export IPKG_INSTROOT="$MOCK_ROOT" + +# Install mock libraries into sysroot +mkdir -p "$MOCK_ROOT/lib/functions" +mkdir -p "$MOCK_ROOT/usr/share/libubox" +cp "$TESTS_DIR/lib/mocks/functions.sh" "$MOCK_ROOT/lib/functions.sh" +cp "$TESTS_DIR/lib/mocks/network.sh" "$MOCK_ROOT/lib/functions/network.sh" +cp "$TESTS_DIR/lib/mocks/jshn.sh" "$MOCK_ROOT/usr/share/libubox/jshn.sh" + +# Install mock config files +mkdir -p "$MOCK_ROOT/etc/config" +if [ -d "$TESTS_DIR/mocks/etc/config" ]; then + cp "$TESTS_DIR/mocks/etc/config/"* "$MOCK_ROOT/etc/config/" 2>/dev/null || true +fi + +# Install mock binaries and add to PATH +mkdir -p "$MOCK_ROOT/bin" +if [ -d "$TESTS_DIR/mocks/bin" ]; then + cp "$TESTS_DIR/mocks/bin/"* "$MOCK_ROOT/bin/" 2>/dev/null || true + chmod +x "$MOCK_ROOT/bin/"* +fi +export PATH="$MOCK_ROOT/bin:$PATH" + +# Create required directories +mkdir -p "$MOCK_ROOT/var/run" +mkdir -p "$MOCK_ROOT/dev/shm" +mkdir -p "$MOCK_ROOT/usr/share/nftables.d/ruleset-post" +mkdir -p "$MOCK_ROOT/etc/iproute2" +cat > "$MOCK_ROOT/etc/iproute2/rt_tables" <<'RT' +255 local +254 main +253 default +0 unspec +RT + +# Stub out OpenWrt rc.common / procd functions +extra_command() { :; } +rc_procd() { "$@"; } +service_started() { :; } +procd_open_instance() { :; } +procd_set_param() { :; } +procd_close_instance() { :; } +procd_open_data() { :; } +procd_close_data() { :; } +procd_add_reload_trigger() { :; } +procd_add_interface_trigger() { :; } +procd_open_trigger() { :; } +procd_close_trigger() { :; } + +# Stub external commands +logger() { :; } +resolveip() { echo "127.0.0.1"; } +jsonfilter() { echo ""; } +pidof() { return 1; } +sync() { :; } + +# Prepare a test-friendly copy of the pbr script: +# 1. Strip 'readonly' keyword to avoid collision with shunit2 internals +# (pbr defines readonly _FAIL_, _OK_ etc. that clash with shunit2) +# 2. Redirect file paths to temp directories we control +_PBR_TEST_SCRIPT="$MOCK_ROOT/pbr_test.sh" +sed 's/^readonly //' "$PKG_DIR/files/etc/init.d/pbr" > "$_PBR_TEST_SCRIPT" + +# Source the modified pbr script +. "$_PBR_TEST_SCRIPT" + +# Override file paths to use test-friendly temp locations +nftTempFile="$MOCK_ROOT/var/run/pbr.nft" +nftMainFile="$MOCK_ROOT/usr/share/nftables.d/ruleset-post/30-pbr.nft" +nftNetifdFile="$MOCK_ROOT/usr/share/nftables.d/ruleset-post/20-pbr-netifd.nft" +rtTablesFile="$MOCK_ROOT/etc/iproute2/rt_tables" +runningStatusFile="$MOCK_ROOT/dev/shm/pbr.status.json" +packageLockFile="$MOCK_ROOT/var/run/pbr.lock" +packageDnsmasqFile="$MOCK_ROOT/var/run/pbr.dnsmasq" +packageDebugFile="$MOCK_ROOT/var/run/pbr.debug" +packageConfigFile="$MOCK_ROOT/etc/config/pbr" diff --git a/net/pbr/tests/mocks/bin/dnsmasq b/net/pbr/tests/mocks/bin/dnsmasq new file mode 100644 index 0000000000..de4834101f --- /dev/null +++ b/net/pbr/tests/mocks/bin/dnsmasq @@ -0,0 +1,11 @@ +#!/bin/bash +# Mock dnsmasq for pbr tests +case "$1" in + --version) + echo "Dnsmasq version 2.90" + echo "Compile time options: IPv6 GNU-getopt DBus no-UBus no-i18n IDN2 DHCP DHCPv6 no-Lua TFTP conntrack ipset nftset auth cryptohash DNSSEC loop-detect inotify dumpfile" + ;; + *) + exit 0 + ;; +esac diff --git a/net/pbr/tests/mocks/bin/nft b/net/pbr/tests/mocks/bin/nft new file mode 100644 index 0000000000..52ae75cbcc --- /dev/null +++ b/net/pbr/tests/mocks/bin/nft @@ -0,0 +1,31 @@ +#!/bin/bash +# Mock nft binary for pbr tests +case "$1" in + list) + case "$*" in + "list table inet fw4"|"list table inet fw4 2>&1") + cat <<'EOF' +table inet fw4 { + chain input { } + chain forward { } + chain output { } + chain dstnat { } + chain mangle_prerouting { } + chain mangle_output { } + chain mangle_forward { } +} +EOF + ;; + *) + echo "table inet fw4 {}" + ;; + esac + ;; + -c) + # Syntax check - always succeed + exit 0 + ;; + *) + exit 0 + ;; +esac diff --git a/net/pbr/tests/mocks/bin/readlink b/net/pbr/tests/mocks/bin/readlink new file mode 100644 index 0000000000..50e6ebf69b --- /dev/null +++ b/net/pbr/tests/mocks/bin/readlink @@ -0,0 +1,11 @@ +#!/bin/bash +# Mock readlink for pbr tests +# Returns /usr/libexec/ip-full for /sbin/ip to pass the ip-full check +case "$*" in + */sbin/ip) + echo "/usr/libexec/ip-full" + ;; + *) + command readlink "$@" 2>/dev/null || echo "$1" + ;; +esac diff --git a/net/pbr/tests/mocks/etc/config/dhcp b/net/pbr/tests/mocks/etc/config/dhcp new file mode 100644 index 0000000000..218274af5c --- /dev/null +++ b/net/pbr/tests/mocks/etc/config/dhcp @@ -0,0 +1,9 @@ +config dnsmasq 'cfg01411c' + option domainneeded '1' + +config dhcp 'lan' + option interface 'lan' + option start '100' + option limit '150' + option leasetime '12h' + option force '1' diff --git a/net/pbr/tests/mocks/etc/config/firewall b/net/pbr/tests/mocks/etc/config/firewall new file mode 100644 index 0000000000..248e467772 --- /dev/null +++ b/net/pbr/tests/mocks/etc/config/firewall @@ -0,0 +1,20 @@ +config defaults 'defaults' + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + +config zone 'lan_zone' + option name 'lan' + list network 'lan' + option input 'ACCEPT' + option output 'ACCEPT' + option forward 'ACCEPT' + +config zone 'wan_zone' + option name 'wan' + list network 'wan' + list network 'wan6' + list network 'wg0' + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' diff --git a/net/pbr/tests/mocks/etc/config/network b/net/pbr/tests/mocks/etc/config/network new file mode 100644 index 0000000000..43937f15e9 --- /dev/null +++ b/net/pbr/tests/mocks/etc/config/network @@ -0,0 +1,21 @@ +config interface 'loopback' + option device 'lo' + option proto 'static' + option ipaddr '127.0.0.1' + +config interface 'lan' + option device 'br-lan' + option proto 'static' + option ipaddr '192.168.1.1' + +config interface 'wan' + option device 'eth0' + option proto 'dhcp' + +config interface 'wan6' + option device 'eth0' + option proto 'dhcpv6' + +config interface 'wg0' + option proto 'wireguard' + option device 'wg0' diff --git a/net/pbr/tests/mocks/etc/config/pbr b/net/pbr/tests/mocks/etc/config/pbr new file mode 100644 index 0000000000..f067c68acc --- /dev/null +++ b/net/pbr/tests/mocks/etc/config/pbr @@ -0,0 +1,52 @@ +config pbr 'config' + option enabled '1' + option verbosity '2' + option strict_enforcement '1' + option ipv6_enabled '0' + option fw_mask '00ff0000' + option resolver_set 'none' + option uplink_interface 'wan' + option uplink_interface6 'wan6' + option uplink_mark '00010000' + option uplink_ip_rules_priority '30000' + list ignored_interface 'loopback' + list lan_device 'br-lan' + option procd_boot_trigger_delay '5000' + option procd_reload_delay '0' + option nft_set_policy 'performance' + option nft_set_auto_merge '1' + option nft_set_flags_interval '1' + option nft_set_flags_timeout '0' + option nft_rule_counter '0' + option nft_set_counter '0' + option nft_user_set_counter '0' + option prefixlength '1' + list resolver_instance '*' + option webui_show_ignore_target '0' + +config policy 'vpn_all' + option name 'VPN All Traffic' + option interface 'wg0' + option src_addr '192.168.1.0/24' + option dest_addr '' + option enabled '1' + +config policy 'vpn_gaming' + option name 'VPN Gaming' + option interface 'wg0' + option src_addr '' + option dest_addr '10.0.0.0/8' + option src_port '27015-27030' + option enabled '1' + +config policy 'disabled_policy' + option name 'Disabled Policy' + option interface 'wan' + option src_addr '10.10.10.0/24' + option enabled '0' + +config dns_policy 'dns_vpn' + option name 'DNS via VPN' + option interface 'wg0' + option src_addr '192.168.1.100' + option enabled '1' diff --git a/net/pbr/tests/mocks/etc/config/system b/net/pbr/tests/mocks/etc/config/system new file mode 100644 index 0000000000..302202e1aa --- /dev/null +++ b/net/pbr/tests/mocks/etc/config/system @@ -0,0 +1,3 @@ +config system + option hostname 'OpenWrt' + option timezone 'UTC' diff --git a/net/pbr/tests/run_tests.sh b/net/pbr/tests/run_tests.sh new file mode 100644 index 0000000000..595fc488b0 --- /dev/null +++ b/net/pbr/tests/run_tests.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Test runner for pbr shell tests (shunit2-based) +# Usage: bash tests/run_tests.sh [test_pattern] +set -uo pipefail + +cd "$(dirname "$0")/.." || exit 1 +TESTS_DIR="$(pwd)/tests" +PASS=0 +FAIL=0 +TOTAL=0 +FAILED_TESTS="" + +# Check shunit2 availability +if ! command -v shunit2 >/dev/null 2>&1 && [ ! -f /usr/bin/shunit2 ]; then + echo "ERROR: shunit2 not found. Install with: apt-get install shunit2" >&2 + exit 1 +fi + +pattern="${1:-}" + +for test_dir in "$TESTS_DIR"/[0-9]*/; do + [ -d "$test_dir" ] || continue + for test_script in "$test_dir"[0-9]*; do + [ -f "$test_script" ] || continue + test_name="${test_dir##*tests/}${test_script##*/}" + # Filter by pattern if provided + if [ -n "$pattern" ] && ! echo "$test_name" | grep -q "$pattern"; then + continue + fi + TOTAL=$((TOTAL + 1)) + output_file="$(mktemp)" + if bash "$test_script" >"$output_file" 2>&1; then + printf '\033[0;32mPASS\033[0m: %s\n' "$test_name" + PASS=$((PASS + 1)) + else + printf '\033[0;31mFAIL\033[0m: %s\n' "$test_name" + cat "$output_file" | sed 's/^/ /' + FAIL=$((FAIL + 1)) + FAILED_TESTS="${FAILED_TESTS:+$FAILED_TESTS\n} $test_name" + fi + rm -f "$output_file" + done +done + +echo "" +echo "Results: $PASS/$TOTAL passed, $FAIL failed" +if [ -n "$FAILED_TESTS" ]; then + echo "" + echo "Failed tests:" + printf "%b\n" "$FAILED_TESTS" +fi +[ "$FAIL" -eq 0 ] -- cgit v1.2.3