From f5381d849dc64147e01058f09a74c2e2cdf5a2f6 Mon Sep 17 00:00:00 2001 From: Stan Grishin Date: Fri, 1 Aug 2025 01:04:35 +0000 Subject: [PATCH] pbr: update to 1.1.8-32 Makefile: - add SPDX-Identifier-License - update Copyright README: - add basic README with the link to full documentation Config: - add debug_dnsmasq - add procd_boot_trigger_delay Init Script: - move extra_command calls high up for visibility - bump packageCompat to sync with luci app - implement support for debug_dnsmasq to dump dnsmasq debug into $packageDebugFile - create $runningStatusFile json-file allowing more verbose errors/warnings messages - replaced `state add` calls with json add calls to store errors/warnings messages - remove no longer needed errorSummary, warningSummary - ensure environment is only loaded once per run via $load_environment_flag - bugfix: update is_{host,hostname,domain,ipv4,mac_address} functions to properly sort policy entries - bugfix: change references to melmac.net to melmac.ca - add some new error/warning messages - add delay before service is started on boot via procd_boot_trigger_delay - bugfix: add logic to identify unknown policy entries instead of silently failing on them - store error/warning messages as json objects in ubus data for luci app - update load_validate_config with debug_dnsmasq and procd_boot_trigger_delay entries Signed-off-by: Stan Grishin --- net/pbr/Makefile | 6 +- net/pbr/README.md | 18 +- net/pbr/files/etc/config/pbr | 2 + net/pbr/files/etc/init.d/pbr | 399 ++++++++++++++++++++--------------- 4 files changed, 251 insertions(+), 174 deletions(-) diff --git a/net/pbr/Makefile b/net/pbr/Makefile index 3ac7c54eac..856d83ded4 100644 --- a/net/pbr/Makefile +++ b/net/pbr/Makefile @@ -1,11 +1,11 @@ -# Copyright 2017-2024 MOSSDeF, Stan Grishin (stangri@melmac.ca). -# This is free software, licensed under AGPL-3.0-or-later. +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright 2017-2025 MOSSDeF, Stan Grishin (stangri@melmac.ca). include $(TOPDIR)/rules.mk PKG_NAME:=pbr PKG_VERSION:=1.1.8 -PKG_RELEASE:=16 +PKG_RELEASE:=32 PKG_LICENSE:=AGPL-3.0-or-later PKG_MAINTAINER:=Stan Grishin diff --git a/net/pbr/README.md b/net/pbr/README.md index 98feae65df..3a65c6b225 100644 --- a/net/pbr/README.md +++ b/net/pbr/README.md @@ -1,4 +1,18 @@ -# README +# Policy-Based Routing (pbr) -Documentation for this project is available at [https://docs.openwrt.melmac.net/pbr/](https://docs.openwrt.melmac.net/pbr/). +[![OpenWrt](https://img.shields.io/badge/OpenWrt-Compatible-blueviolet)](https://openwrt.org) +[![Web UI](https://img.shields.io/badge/Web_UI-Available-blue)](https://docs.openwrt.melmac.ca/pbr/) +[![License](https://img.shields.io/badge/License-GPL--3.0-lightgrey)](https://github.com/stangri/pbr/blob/master/LICENSE) +Flexible policy-based routing (PBR) framework for OpenWrt. +Allows routing specific traffic (by IP, MAC, port, protocol, or domain) through a specific WAN, VPN, or tunnel. + +## Features + +- Route by IP, MAC, port, or domain name +- Works with WAN, VPNs (WireGuard, OpenVPN), or tunnels +- Lightweight shell-based implementation +- Optional Web UI integration via LuCI + +**Full documentation:** +[https://docs.openwrt.melmac.ca/pbr/](https://docs.openwrt.melmac.ca/pbr/) diff --git a/net/pbr/files/etc/config/pbr b/net/pbr/files/etc/config/pbr index f6d50ebd4f..8bf686f063 100644 --- a/net/pbr/files/etc/config/pbr +++ b/net/pbr/files/etc/config/pbr @@ -1,4 +1,5 @@ config pbr 'config' + option debug_dnsmasq '0' option enabled '0' option verbosity '2' option strict_enforcement '1' @@ -8,6 +9,7 @@ config pbr 'config' list ignored_interface 'vpnserver' option boot_timeout '30' option rule_create_option 'add' + option procd_boot_trigger_delay '5000' option procd_reload_delay '1' option webui_show_ignore_target '0' option nft_rule_counter '0' diff --git a/net/pbr/files/etc/init.d/pbr b/net/pbr/files/etc/init.d/pbr index 4efae5cb3f..dcf64d1483 100755 --- a/net/pbr/files/etc/init.d/pbr +++ b/net/pbr/files/etc/init.d/pbr @@ -12,21 +12,44 @@ USE_PROCD=1 [ -n "${IPKG_INSTROOT}" ] && return 0 +if type extra_command >/dev/null 2>&1; then + extra_command '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_command 'version' 'Show version information' + extra_command 'on_firewall_reload' ' Run service on firewall reload' + extra_command 'on_interface_reload' ' Run service on indicated interface reload' +else +# shellcheck disable=SC2034 + EXTRA_COMMANDS='on_firewall_reload 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" +fi + readonly packageName='pbr' readonly PKG_VERSION='dev-test' -readonly packageCompat='11' +readonly packageCompat='14' readonly serviceName="$packageName $PKG_VERSION" readonly packageConfigFile="/etc/config/${packageName}" +readonly packageDebugFile="/var/run/${packageName}.debug" readonly packageLockFile="/var/run/${packageName}.lock" readonly dnsmasqFileDefault="/var/run/${packageName}.dnsmasq" +readonly runningStatusFile="/dev/shm/${packageName}.status.json" +readonly runningStatusFileLock="/var/lock/${packageName}.lock" readonly _OK_='\033[0;32m\xe2\x9c\x93\033[0m' readonly __OK__='\033[0;32m[\xe2\x9c\x93]\033[0m' readonly _OKB_='\033[1;34m\xe2\x9c\x93\033[0m' readonly __OKB__='\033[1;34m[\xe2\x9c\x93]\033[0m' readonly _FAIL_='\033[0;31m\xe2\x9c\x97\033[0m' readonly __FAIL__='\033[0;31m[\xe2\x9c\x97]\033[0m' -readonly _ERROR_='\033[0;31mERROR\033[0m' -readonly _WARNING_='\033[0;33mWARNING\033[0m' +readonly _ERROR_='\033[0;31mERROR:\033[0m' +readonly _WARNING_='\033[0;33mWARNING:\033[0m' readonly ip_full='/usr/libexec/ip-full' # shellcheck disable=SC2155 readonly ipTablePrefix="$packageName" @@ -47,6 +70,7 @@ readonly xrayIfacePrefix='xray_' readonly rtTablesFile='/etc/iproute2/rt_tables' # package config options +debug_dnsmasq= enabled= fw_mask= icmp_interface= @@ -54,6 +78,7 @@ ignored_interface= ipv6_enabled= nft_user_set_policy= nft_user_set_counter= +procd_boot_trigger_delay= procd_reload_delay= procd_lan_device= procd_wan_interface= @@ -76,10 +101,9 @@ nft_set_policy= nft_set_timeout= # run-time +load_environment_flag= aghConfigFile='/etc/AdGuardHome/AdGuardHome.yaml' gatewaySummary= -errorSummary= -warningSummary= wanIface4= wanIface6= dnsmasqFileList= @@ -143,6 +167,8 @@ output_okb() { output 1 "$_OKB_"; output 2 "$__OKB__\n"; } output_okbn() { output 1 "$_OKB_\n"; output 2 "$__OKB__\n"; } output_fail() { output 1 "$_FAIL_"; output 2 "$__FAIL__\n"; } output_failn() { output 1 "$_FAIL_\n"; output 2 "$__FAIL__\n"; } +output_error() { output "${_ERROR_} $*!\n"; } +output_warning() { output "${_WARNING_} $*!\n"; } quiet_mode() { case "$1" in on) verbosity=0;; @@ -215,7 +241,9 @@ uci_get_device() { uci_get_protocol() { uci_get 'network' "$1" 'proto'; } is_default_dev() { [ "$1" = "$(ip -4 r | grep -m1 'dev' | grep -Eso 'dev [^ ]*' | awk '{print $2}')" ]; } is_disabled_interface() { [ "$(uci_get 'network' "$1" 'disabled')" = '1' ]; } -is_domain(){ echo "$1" | grep -qE '^([a-zA-Z0-9][a-zA-Z0-9-]{0,61}\.)*[a-zA-Z]{2,}$'; } +is_host() { echo "$1" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9_-]{0,61}[a-zA-Z0-9]$|^[a-zA-Z0-9]$'; } +is_hostname() { echo "$1" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9_-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'; } +is_domain() { ! is_ipv4 "$1" && ! is_mac_address_bad_notation "$1" && { is_host "$1" || is_hostname "$1"; }; } is_dslite() { local p; network_get_protocol p "$1"; [ "${p:0:6}" = "dslite" ]; } is_family_mismatch() { ( is_ipv4 "${1//!}" && is_ipv6 "${2//!}" ) || ( is_ipv6 "${1//!}" && is_ipv4 "${2//!}" ); } is_greater() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } @@ -223,7 +251,7 @@ is_greater_or_equal() { test "$(printf '%s\n' "$@" | sort -V | head -n '1')" = " is_ignored_interface() { str_contains_word "$ignored_interface" "$1"; } is_ignore_target() { [ "$(str_to_lower "$1")" = 'ignore' ]; } is_integer() { case "$1" in ''|*[!0-9]*) return 1;; esac; } -is_ipv4() { expr "${1%/*}" : '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' >/dev/null; } +is_ipv4() { echo "$1" | grep -qE '^((25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(/(3[0-2]|[12]?[0-9]))?$'; } is_ipv6() { ! is_mac_address "$1" && str_contains "$1" ':'; } is_ipv6_global_scope() { [ "${1:0:4}" = '2001' ]; } is_ipv6_local_scope() { is_ipv6_local_link "$1" || is_ipv6_local_unique "$1"; } @@ -232,7 +260,8 @@ is_ipv6_local_unique() { [ "${1:0:2}" = 'fc' ] || [ "${1:0:2}" = 'fd' ]; } is_list() { str_contains "$1" ',' || str_contains "$1" ' '; } is_lan() { local d; network_get_device d "$1"; str_contains "$procd_lan_device" "$d"; } is_l2tp() { local p; network_get_protocol p "$1"; [ "${p:0:4}" = "l2tp" ]; } -is_mac_address() { expr "$1" : '[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]$' >/dev/null; } +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_table_interface() { local iface="$1"; [ "$(uci_get 'network' "$iface" 'ip4table')" = "${packageName}_${iface%6}" ]; } @@ -311,47 +340,33 @@ check_dnsmasq_nftset() { check_nft || return 1 check_dnsmasq || return 1 o="$(dnsmasq -v 2>/dev/null)" + [ -n "$debug_dnsmasq" ] && { + echo " $(date) dnsmasq output dump:"; +# shellcheck disable=SC3003 + echo "${o%$'\n'$'\n'This*}"; + echo '-------------------------'; + } >> "$packageDebugFile" ! echo "$o" | grep -q 'no-nftset' && echo "$o" | grep -q 'nftset' } 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() { if ! "$@" >/dev/null 2>&1; then - state add 'errorSummary' 'errorTryFailed' "$*" + json add error 'errorTryFailed' "$*" return 1 fi } -if type extra_command >/dev/null 2>&1; then - extra_command 'status' "Generates output required to troubleshoot routing issues - Use '-d' option for more detailed output - Use '-p' option to automatically upload data under VPR 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 'version' 'Show version information' - extra_command 'on_firewall_reload' ' Run service on firewall reload' - extra_command 'on_interface_reload' ' Run service on indicated interface reload' -else -# shellcheck disable=SC2034 - EXTRA_COMMANDS='on_firewall_reload 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 VPR 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" -fi - get_text() { local r case "$1" in errorConfigValidation) r="Config ($packageConfigFile) validation failure!";; errorNoNft) r="Resolver set support (${resolver_set}) requires nftables, but nft binary cannot be found!";; errorResolverNotSupported) r="Resolver set (${resolver_set}) is not supported on this system!";; - errorServiceDisabled) r="The ${packageName} service is currently disabled!";; + errorServiceDisabled) r="The ${packageName} service is currently disabled";; errorNoWanGateway) r="The ${serviceName} service failed to discover WAN gateway!";; errorNoWanInterface) r="The %s interface not found, you need to set the 'pbr.config.procd_wan_interface' option!";; - errorNoWanInterfaceHint) r="Refer to https://docs.openwrt.melmac.net/pbr/#procd_wan_interface.";; + errorNoWanInterfaceHint) r="Refer to https://docs.openwrt.melmac.ca/pbr/#procd_wan_interface.";; errorNftsetNameTooLong) r="The nft set name '%s' is longer than allowed 255 characters!";; errorUnexpectedExit) r="Unexpected exit or service termination: '%s'!";; errorPolicyNoSrcDest) r="Policy '%s' has no source/destination parameters!";; @@ -374,6 +389,7 @@ get_text() { errorPolicyProcessUnknownProtocol) r="Unknown protocol in policy '%s'!";; errorPolicyProcessInsertionFailed) r="Insertion failed for both IPv4 and IPv6 for policy '%s'!";; errorPolicyProcessInsertionFailedIpv4) r="Insertion failed for IPv4 for policy '%s'!";; + errorPolicyProcessUnknownEntry) r="Unknown entry in policy '%s'!";; errorInterfaceRoutingEmptyValues) r="Received empty tid/mark or interface name when setting up routing!";; errorFailedToResolve) r="Failed to resolve '%s'!";; errorTryFailed) r="Command failed: %s";; @@ -387,6 +403,7 @@ get_text() { errorDefaultFw4ChainMissing) r="Default fw4 chain '%s' is missing!";; errorRequiredBinaryMissing) r="Required binary '%s' is missing!";; errorInterfaceRoutingUnknownDevType) r="Unknown IPv6 Link type for device '%s'!";; + errorUplinkDown) r="Uplink/WAN interface is still down, increase value of 'procd_boot_trigger_delay' option";; warningInvalidOVPNConfig) r="Invalid OpenVPN config for '%s' interface.";; warningResolverNotSupported) r="Resolver set (${resolver_set}) is not supported on this system.";; warningPolicyProcessCMD) r="'%s'";; @@ -397,6 +414,7 @@ get_text() { warningBadNftCallsInUserFile) r="Incompatible nft calls detected in user include file, disabling fw4 nft file support.";; warningDnsmasqInstanceNoConfdir) r="Dnsmasq instance '%s' targeted in settings, but it doesn't have its own confdir.";; warningDhcpLanForce) r="Please set 'dhcp.%s.force=1' to speed up service start-up.";; + *) r="Unknown error '%s'!";; esac echo "$r" } @@ -428,13 +446,13 @@ process_url() { dl_temp_file="$(mktemp -u -q -t "${packageName}_tmp.XXXXXXXX")" done if is_url_file "$url" && ! is_present 'curl'; then - state add 'errorSummary' 'errorFileSchemaRequiresCurl' "$url" + json add error 'errorFileSchemaRequiresCurl' "$url" elif is_url_https "$url" && [ -z "$dl_https_supported" ]; then - state add 'errorSummary' 'errorDownloadUrlNoHttps' "$url" + json add error 'errorDownloadUrlNoHttps' "$url" elif $dl_command "$url" "$dl_flag" "$dl_temp_file" 2>/dev/null; then sed 'N;s/\n/ /;s/\s\+/ /g;' "$dl_temp_file" else - state add 'errorSummary' 'errorDownloadUrl' "$url" + json add error 'errorDownloadUrl' "$url" fi rm -f "$dl_temp_file" } @@ -443,6 +461,7 @@ load_package_config() { local param="$1" local user_file_check_result i config_load "$packageName" + config_get_bool debug_dnsmasq 'config' 'debug_dnsmasq' '0' config_get_bool enabled 'config' 'enabled' '0' config_get fw_mask 'config' 'fw_mask' 'ff0000' config_get icmp_interface 'config' 'icmp_interface' @@ -461,12 +480,14 @@ load_package_config() { config_get_bool strict_enforcement 'config' 'strict_enforcement' '1' config_get supported_interface 'config' 'supported_interface' config_get verbosity 'config' 'verbosity' '2' - config_get procd_reload_delay 'config' 'procd_reload_delay' '0' + config_get procd_boot_trigger_delay 'config' 'procd_boot_trigger_delay' '5000' config_get procd_lan_device 'config' 'procd_lan_device' 'br-lan' + config_get procd_reload_delay 'config' 'procd_reload_delay' '0' config_get procd_wan_interface 'config' 'procd_wan_interface' 'wan' config_get procd_wan6_interface 'config' 'procd_wan6_interface' 'wan6' config_get wan_ip_rules_priority 'config' 'wan_ip_rules_priority' '30000' config_get wan_mark 'config' 'wan_mark' '010000' + fw_mask="0x${fw_mask}" wan_mark="0x${wan_mark}" if [ -x "$agh" ] && [ ! -s "$aghConfigFile" ]; then @@ -477,6 +498,10 @@ load_package_config() { fw_maskXor="$(printf '%#x' "$((fw_mask ^ 0xffffffff))")" fw_maskXor="${fw_maskXor:-0xff00ffff}" + is_integer "$procd_boot_trigger_delay" || procd_boot_trigger_delay='5000' + [ "$procd_boot_trigger_delay" -lt '1000' ] && procd_boot_trigger_delay='1000' + + [ "$debug_dnsmasq" != '1' ] && unset debug_dnsmasq [ "$nft_rule_counter" != '1' ] && unset nft_rule_counter [ "$nft_set_auto_merge" != '1' ] && unset nft_set_auto_merge [ "$nft_set_counter" != '1' ] && unset nft_set_counter @@ -513,7 +538,7 @@ load_environment() { _check_dhcp_force() { is_lan "$1" || return 0 if [ "$(uci_get dhcp "$1" force 0)" = '0' ]; then - state add 'warningSummary' 'warningDhcpLanForce' "$1" + json add warning 'warningDhcpLanForce' "$1" fi } local i _ret=0 @@ -522,22 +547,22 @@ load_environment() { uci_commit firewall fi if [ "$(readlink /sbin/ip)" != "$ip_full" ]; then - state add 'errorSummary' 'errorRequiredBinaryMissing' 'ip-full' + json add error 'errorRequiredBinaryMissing' 'ip-full' _ret='1' fi if ! nft_call list table inet fw4; then - state add 'errorSummary' 'errorDefaultFw4TableMissing' 'fw4' + json add error 'errorDefaultFw4TableMissing' 'fw4' _ret='1' fi if is_config_enabled 'dns_policy' || is_tor_running; then if ! nft_call list chain inet fw4 dstnat; then - state add 'errorSummary' 'errorDefaultFw4ChainMissing' 'dstnat' + json add error 'errorDefaultFw4ChainMissing' 'dstnat' _ret='1' fi fi for i in $chainsList; do if ! nft_call list chain inet fw4 "mangle_${i}"; then - state add 'errorSummary' 'errorDefaultFw4ChainMissing' "mangle_${i}" + json add error 'errorDefaultFw4ChainMissing' "mangle_${i}" _ret='1' fi done @@ -546,20 +571,25 @@ load_environment() { return "$_ret" } local param="$1" validation_result="$2" + [ -z "$load_environment_flag" ] || return 0 + json init case "$param" in on_boot|on_start) output 1 "Loading environment ($param) " load_package_config "$param" if [ "$enabled" -eq '0' ]; then output 1 "$_FAIL_\n" - state add 'errorSummary' 'errorServiceDisabled' + json add error 'errorServiceDisabled' + output_error "$(get_text 'errorServiceDisabled')" + output "Run the following commands before starting service again:\n" + output "uci set ${packageName}.config.enabled='1'; uci commit $packageName;\n" return 1 fi if [ -n "$validation_result" ] && [ "$validation_result" != '0' ]; then output 1 "$_FAIL_\n" - output "${_ERROR_}: The $packageName config validation failed!\n" + json add error 'errorConfigValidation' + output_error "$(get_text 'errorConfigValidation')" output "Please check if the '$packageConfigFile' contains correct values for config options.\n" - state add 'errorSummary' 'errorConfigValidation' return 1 fi _system_health_check || { output 1 "$_FAIL_\n"; return 1; } @@ -578,6 +608,7 @@ load_environment() { load_network "$param" ;; esac + load_environment_flag=1 } # shellcheck disable=SC2317 @@ -620,8 +651,8 @@ load_network() { is_wan_up() { local sleepCount='1' param="$1" if [ -z "$(uci_get network "$procd_wan_interface")" ]; then - state add 'errorSummary' 'errorNoWanInterface' "$procd_wan_interface" - state add 'errorSummary' 'errorNoWanInterfaceHint' + json add error 'errorNoWanInterface' "$procd_wan_interface" + json add error 'errorNoWanInterfaceHint' return 1 fi network_flush_cache @@ -629,7 +660,7 @@ is_wan_up() { if [ -n "$wanGW" ]; then return 0 else - state add 'errorSummary' 'errorNoWanGateway' + json add error 'errorNoWanGateway' return 1 fi } @@ -666,7 +697,7 @@ nft_file() { cp -f "$nftTempFile" "$nftPermFile"; then output_okn else - state add 'errorSummary' 'errorNftFileInstall' "$nftTempFile" + json add error 'errorNftFileInstall' "$nftTempFile" output_failn fi ;; @@ -685,7 +716,7 @@ nftset() { [ -x "$nft" ] || return 1 if [ "${#nftset4}" -gt '255' ]; then - state add 'errorSummary' 'errorNftsetNameTooLong' "$nftset4" + json add error 'errorNftsetNameTooLong' "$nftset4" return 1 fi @@ -706,7 +737,7 @@ nftset() { [ -z "$param4" ] && param4="$(resolveip_to_nftset4 "$param")" [ -z "$param6" ] && param6="$(resolveip_to_nftset6 "$param")" if [ -z "$param4" ] && [ -z "$param6" ]; then - state add 'errorSummary' 'errorFailedToResolve' "$param" + json add error 'errorFailedToResolve' "$param" else [ -n "$param4" ] && nft4 add element inet "$nftTable" "$nftset4" "{ $param4 }" && ipv4_error=0 [ -n "$param6" ] && nft6 add element inet "$nftTable" "$nftset6" "{ $param6 }" && ipv6_error=0 @@ -831,73 +862,49 @@ cleanup_sets() { done } -state() { - local action="$1" param="$2" value="${3//#/_}" - local array_name - shift 3 -# shellcheck disable=SC2124 - local extras="$@" - local line error_id error_extra label +json() { + local status message stats i + local action="$1" param="$2" value="$3"; shift 3; local info="$*"; + [ "$param" = 'error' ] && param='errors' + [ "$param" = 'warning' ] && param='warnings' + { json_load_file "$runningStatusFile" || json_init; } >/dev/null 2>&1 case "$action" in - add) - line="$(eval echo "\$$param")" - eval "$param"='${line:+$line#}${value}${extras:+ $extras}' - ;; - json) - json_init - json_add_object "$packageName" - case "$param" in - errorSummary) - array_name='errors';; - warningSummary) - array_name='warnings';; - esac - json_add_array "$array_name" - if [ -n "$(eval echo "\$$param")" ]; then - while read -r line; do - if str_contains "$line" ' '; then - error_id="${line% *}" - error_extra="${line#* }" - else - error_id="$line" + 'get') + if json_select "$param" >/dev/null 2>&1; then + if [ -n "$value" ]; then + { + if json_select "$value"; then + json_get_var 'i' "${info:-code}" + json_select .. fi - json_add_object "$array_name" - json_add_string 'id' "$error_id" - json_add_string 'extra' "$error_extra" - json_close_object - done </dev/null 2>&1 + else + json_get_keys i + fi + printf "%b" "$i" + json_select .. fi - json_close_array - json_close_object - json_dump + return ;; - print) - [ -z "$(eval echo "\$$param")" ] && return 0 - case "$param" in - errorSummary) - label="${_ERROR_}:";; - warningSummary) - label="${_WARNING_}:";; - esac - while read -r line; do - if str_contains "$line" ' '; then - error_id="${line% *}" - error_extra="${line#* }" - printf "%b $(get_text "$error_id")\n" "$label" "$error_extra" - else - error_id="$line" - printf "%b $(get_text "$error_id")\n" "$label" - fi - done </dev/null 2>&1 + json_add_object "" + json_add_string 'code' "$value" + json_add_string 'info' "$info" + json_close_object + json_select .. ;; - set) - eval "$param"='${value}${extras:+ $extras}' + 'init') + json_init + json_add_array 'errors' + json_close_array + json_add_array 'warnings' + json_close_array ;; esac + mkdir -p "${runningStatusFile%/*}" + json_dump > "$runningStatusFile" + sync } resolver() { @@ -952,7 +959,6 @@ resolver() { for d in $value; do nftset 'add_dnsmasq_element' "$iface" "$target" "$type" "$uid" "$name" "$d" done -# nftset 'add_dnsmasq_element' "$iface" "$target" "$type" "$uid" "$name" "$(str_to_dnsmsaq_nftset "$value")" ;; create_resolver_set) [ -n "$resolver_set_supported" ] || return 1 @@ -960,14 +966,14 @@ resolver() { ;; check_support) if [ ! -x "$nft" ]; then - state add 'errorSummary' 'errorNoNft' + json add error 'errorNoNft' return 1 fi - if ! dnsmasq -v 2>/dev/null | grep -q 'no-nftset' && dnsmasq -v 2>/dev/null | grep -q 'nftset'; then + if check_dnsmasq_nftset; then resolver_set_supported='true' return 0 else - state add 'warningSummary' 'warningResolverNotSupported' + json add warning 'warningResolverNotSupported' return 1 fi ;; @@ -1074,20 +1080,20 @@ dns_policy_routing() { if [ -z "${dest_dns_ipv4}${dest_dns_ipv6}" ]; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessNoInterfaceDns' "'$dest_dns'" + json add error 'errorPolicyProcessNoInterfaceDns' "'$dest_dns'" return 1 fi if [ -z "$ipv6_enabled" ] && is_ipv6 "$(str_first_word "$src_addr")"; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessNoIpv6' "$name" + json add error 'errorPolicyProcessNoIpv6' "$name" return 1 fi if { is_ipv4 "$(str_first_word "$src_addr")" && [ -z "$dest_dns_ipv4" ]; } || \ { is_ipv6 "$(str_first_word "$src_addr")" && [ -z "$dest_dns_ipv6" ]; }; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessMismatchFamily' "${name}: '$src_addr' '$dest_dns'" + json add error 'errorPolicyProcessMismatchFamily' "${name}: '$src_addr' '$dest_dns'" return 1 fi @@ -1119,7 +1125,7 @@ dns_policy_routing() { resolved_ipv4="$(resolveip_to_nftset4 "$d")" resolved_ipv6="$(resolveip_to_nftset6 "$d")" if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then - state add 'errorSummary' 'errorFailedToResolve' "$d" + 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" @@ -1154,15 +1160,15 @@ dns_policy_routing() { if [ -n "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ] && [ "$ipv6_error" -eq '1' ]; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessInsertionFailed' "$name" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param4" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param6" + json add error 'errorPolicyProcessInsertionFailed' "$name" + json add error 'errorPolicyProcessCMD' "nft $param4" + json add error 'errorPolicyProcessCMD' "nft $param6" logger -t "$packageName" "ERROR: nft $param4" logger -t "$packageName" "ERROR: nft $param6" elif [ -z "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ]; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessInsertionFailedIpv4' "$name" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param4" + json add error 'errorPolicyProcessInsertionFailedIpv4' "$name" + json add error 'errorPolicyProcessCMD' "nft $param4" logger -t "$packageName" "ERROR: nft $param4" fi done @@ -1183,7 +1189,7 @@ policy_routing() { if [ -z "$ipv6_enabled" ] && \ { is_ipv6 "$(str_first_word "$src_addr")" || is_ipv6 "$(str_first_word "$dest_addr")"; }; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessNoIpv6' "$name" + json add error 'errorPolicyProcessNoIpv6' "$name" return 1 fi @@ -1203,14 +1209,14 @@ policy_routing() { dest6="return" else processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessUnknownFwmark' "$iface" + json add error 'errorPolicyProcessUnknownFwmark' "$iface" return 1 fi # TODO: implement actual family mismatch check on lists # if is_family_mismatch "$src_addr" "$dest_addr"; then # processPolicyError='true' -# state add 'errorSummary' 'errorPolicyProcessMismatchFamily' "${name}: '$src_addr' '$dest_addr'" +# json add error 'errorPolicyProcessMismatchFamily' "${name}: '$src_addr' '$dest_addr'" # return 1 # fi @@ -1229,7 +1235,7 @@ policy_routing() { unset proto_i elif ! is_supported_protocol "$proto_i"; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessUnknownProtocol' "${name}: '$proto_i'" + json add error 'errorPolicyProcessUnknownProtocol' "${name}: '$proto_i'" return 1 fi @@ -1255,7 +1261,7 @@ policy_routing() { resolved_ipv4="$(resolveip_to_nftset4 "$d")" resolved_ipv6="$(resolveip_to_nftset6 "$d")" if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then - state add 'errorSummary' 'errorFailedToResolve' "$d" + 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" @@ -1296,7 +1302,7 @@ policy_routing() { resolved_ipv4="$(resolveip_to_nftset4 "$d")" resolved_ipv6="$(resolveip_to_nftset6 "$d")" if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then - state add 'errorSummary' 'errorFailedToResolve' "$d" + 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" @@ -1352,15 +1358,15 @@ policy_routing() { nft6 "$param6" "$dest6" || ipv6_error='1' if [ -n "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ] && [ "$ipv6_error" -eq '1' ]; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessInsertionFailed' "$name" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param4 $dest4" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param6 $dest6" + json add error 'errorPolicyProcessInsertionFailed' "$name" + json add error 'errorPolicyProcessCMD' "nft $param4 $dest4" + json add error 'errorPolicyProcessCMD' "nft $param6 $dest6" logger -t "$packageName" "ERROR: nft $param4 $dest4" logger -t "$packageName" "ERROR: nft $param6 $dest6" elif [ -z "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ]; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessInsertionFailedIpv4' "$name" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param4 $dest4" + json add error 'errorPolicyProcessInsertionFailedIpv4' "$name" + json add error 'errorPolicyProcessCMD' "nft $param4 $dest4" logger -t "$packageName" "ERROR: nft $param4 $dest4" fi done @@ -1384,15 +1390,15 @@ policy_routing() { fi if [ -n "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ] && [ "$ipv6_error" -eq '1' ]; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessInsertionFailed' "$name" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param4" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param6" + json add error 'errorPolicyProcessInsertionFailed' "$name" + json add error 'errorPolicyProcessCMD' "nft $param4" + json add error 'errorPolicyProcessCMD' "nft $param6" logger -t "$packageName" "ERROR: nft $param4" logger -t "$packageName" "ERROR: nft $param6" elif [ -z "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ]; then processPolicyError='true' - state add 'errorSummary' 'errorPolicyProcessInsertionFailedIpv4' "$name" - state add 'errorSummary' 'errorPolicyProcessCMD' "nft $param4" + json add error 'errorPolicyProcessInsertionFailedIpv4' "$name" + json add error 'errorPolicyProcessCMD' "nft $param4" logger -t "$packageName" "ERROR: nft $param4" fi fi @@ -1427,11 +1433,11 @@ dns_policy_process() { unset processDnsPolicyError output 2 "Routing '$name' DNS to $dest_dns " if [ -z "$src_addr" ]; then - state add 'errorSummary' 'errorPolicyNoSrcDest' "$name" + json add error 'errorPolicyNoSrcDest' "$name" output_fail; return 1; fi if [ -z "$dest_dns" ]; then - state add 'errorSummary' 'errorPolicyNoDns' "$name" + json add error 'errorPolicyNoDns' "$name" output_fail; return 1; fi @@ -1474,15 +1480,15 @@ policy_process() { [ "$proto" = 'all' ] && unset proto output 2 "Routing '$name' via $interface " if [ -z "${src_addr}${src_port}${dest_addr}${dest_port}" ]; then - state add 'errorSummary' 'errorPolicyNoSrcDest' "$name" + json add error 'errorPolicyNoSrcDest' "$name" output_fail; return 1; fi if [ -z "$interface" ]; then - state add 'errorSummary' 'errorPolicyNoInterface' "$name" + json add error 'errorPolicyNoInterface' "$name" output_fail; return 1; fi if ! is_supported_interface "$interface"; then - state add 'errorSummary' 'errorPolicyUnknownInterface' "$name" + json add error 'errorPolicyUnknownInterface' "$name" output_fail; return 1; fi @@ -1509,6 +1515,7 @@ policy_process() { local filter_list_src_addr='phys_dev phys_dev_negative mac_address mac_address_negative domain domain_negative ipv4 ipv4_negative ipv6 ipv6_negative' local filter_list_dest_addr='domain domain_negative ipv4 ipv4_negative ipv6 ipv6_negative' local filter_group_src_addr filtered_value_src_addr filter_group_dest_addr filtered_value_dest_addr + local processed_value_src_addr processed_value_dest_addr [ -z "$src_addr" ] && filter_list_src_addr='none' for filter_group_src_addr in $filter_list_src_addr; do filtered_value_src_addr="$(filter_options "$filter_group_src_addr" "$src_addr")" @@ -1524,11 +1531,27 @@ 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" fi done fi done + for i in $src_addr; do + if ! str_contains "$processed_value_src_addr" "$i"; then + processPolicyError='true' + json add error 'errorPolicyProcessUnknownEntry' "$name: $i" + fi + done + + for i in $dest_addr; do + if ! str_contains "$processed_value_dest_addr" "$i"; then + processPolicyError='true' + json add error 'errorPolicyProcessUnknownEntry' "$name: $i" + fi + done + if [ -n "$processPolicyError" ]; then output_fail else @@ -1540,7 +1563,7 @@ interface_routing() { local action="$1" tid="$2" mark="$3" iface="$4" gw4="$5" dev="$6" gw6="$7" dev6="$8" priority="$9" local dscp s=0 i ipv4_error=1 ipv6_error=1 if [ -z "$tid" ] || [ -z "$mark" ] || [ -z "$iface" ]; then - state add 'errorSummary' 'errorInterfaceRoutingEmptyValues' + json add error 'errorInterfaceRoutingEmptyValues' return 1 fi case "$action" in @@ -1602,7 +1625,7 @@ EOF elif ip -6 address show dev "$dev6" | grep -q "POINTOPOINT"; then try ip -6 route add default dev "$dev6" table "$tid" metric "$procd_wan6_metric" || ipv6_error=1 else - state add 'errorSummary' 'errorInterfaceRoutingUnknownDevType' "$dev6" + json add error 'errorInterfaceRoutingUnknownDevType' "$dev6" fi # if ! ip -6 route add default via "$gw6" dev "$dev6" table "$tid" >/dev/null 2>&1; then # try ip -6 route add default dev "$dev6" table "$tid" metric "$procd_wan6_metric" || ipv6_error=1 @@ -1693,7 +1716,7 @@ EOF elif ip -6 address show dev "$dev6" | grep -q "POINTOPOINT"; then try ip -6 route add default dev "$dev6" table "$tid" metric "$procd_wan6_metric" || ipv6_error=1 else - state add 'errorSummary' 'errorInterfaceRoutingUnknownDevType' "$dev6" + json add error 'errorInterfaceRoutingUnknownDevType' "$dev6" fi while read -r i; do # shellcheck disable=SC2086 @@ -1752,7 +1775,7 @@ process_interface() { if [ "$iface" = 'tor' ]; then case "$action" in - create|reload) + create|reload|reload_interface) torDnsPort="$(get_tor_dns_port)" torTrafficPort="$(get_tor_traffic_port)" displayText="${iface}/53->${torDnsPort}/80,443->${torTrafficPort}" @@ -1769,7 +1792,7 @@ process_interface() { disabled="$(uci_get 'network' "$iface" 'disabled')" listen_port="$(uci_get 'network' "$iface" 'listen_port')" case "$action" in - create|reload) + create|reload|reload_interface) if [ "$disabled" != '1' ] && [ -n "$listen_port" ]; then if [ -n "$wanIface4" ]; then ip rule del sport "$listen_port" table "pbr_${wanIface4}" >/dev/null 2>&1 @@ -1796,7 +1819,7 @@ process_interface() { [ "$((ifaceMark))" -gt "$((fw_mask))" ] && return 1 if is_ovpn "$iface" && ! is_ovpn_valid "$iface"; then - : || state add 'warningSummary' 'warningInvalidOVPNConfig' "$iface" + : || json add warning 'warningInvalidOVPNConfig' "$iface" fi network_get_device dev "$iface" @@ -1841,7 +1864,7 @@ process_interface() { gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ $dispStatus}\n" if is_netifd_table_interface "$iface"; then output_okb; else output_ok; fi else - state add 'errorSummary' 'errorFailedSetup' "$displayText" + json add error 'errorFailedSetup' "$displayText" output_fail fi ;; @@ -1918,7 +1941,7 @@ process_interface() { gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ $dispStatus}\n" if is_netifd_table_interface "$iface"; then output_okb; else output_ok; fi else - state add 'errorSummary' 'errorFailedReload' "$displayText" + json add error 'errorFailedReload' "$displayText" output_fail fi else @@ -1936,26 +1959,26 @@ user_file_process() { local shellBin="${SHELL:-/bin/ash}" [ "$enabled" -gt '0' ] || return 0 if [ ! -s "$path" ]; then - state add 'errorSummary' 'errorUserFileNotFound' "$path" + json add error 'errorUserFileNotFound' "$path" output_fail return 1 fi if ! $shellBin -n "$path"; then - state add 'errorSummary' 'errorUserFileSyntax' "$path" + json add error 'errorUserFileSyntax' "$path" output_fail return 1 fi if is_bad_user_file_nft_call "$path"; then - state add 'errorSummary' 'errorIncompatibleUserFile' "$path" + json add error 'errorIncompatibleUserFile' "$path" output_fail return 1 fi output 2 "Running $path " # shellcheck disable=SC1090 if ! . "$path"; then - state add 'errorSummary' 'errorUserFileRunning' "$path" + json add error 'errorUserFileRunning' "$path" if grep -q -w 'curl' "$path" && ! is_present 'curl'; then - state add 'errorSummary' 'errorUserFileNoCurl' "$path" + json add error 'errorUserFileNoCurl' "$path" fi output_fail return 1 @@ -1995,11 +2018,14 @@ on_interface_reload() { } start_service() { - local resolverStoredHash resolverNewHash i param="$1" reloadedIface + local resolverStoredHash resolverNewHash i param="$1" reloadedIface k [ -n "$pbr_boot_flag" ] && return 0 + [ "$param" = 'on_boot' ] && return 0 load_environment "${param:-on_start}" "$(load_validate_config)" || return 1 - is_wan_up "$param" || return 1 + + output "Processing environment (${param:-on_start}) " + is_wan_up "$param" || { output_error "$(get_text 'errorUplinkDown')"; return 1; } process_interface 'all' 'prepare' config_foreach process_interface 'interface' 'pre_init' @@ -2056,6 +2082,7 @@ start_service() { case $serviceStartTrigger in on_interface_reload) + output_okn output 1 "Reloading Interface: $reloadedIface " json_add_array 'gateways' process_interface 'all' 'prepare' @@ -2073,6 +2100,7 @@ start_service() { cleanup_marking_chains cleanup_rt_tables nft_file 'create' + output_okn output 1 'Processing interfaces ' json_add_array 'gateways' process_interface 'all' 'prepare' @@ -2116,21 +2144,38 @@ start_service() { esac if [ -z "$gatewaySummary" ]; then - state add 'errorSummary' 'errorNoGateways' + json add error 'errorNoGateways' fi + json_add_int 'packageCompat' "$packageCompat" json_add_object 'status' [ -n "$gatewaySummary" ] && json_add_string 'gateways' "$gatewaySummary" - [ -n "$errorSummary" ] && json_add_string 'errors' "$errorSummary" - [ -n "$warningSummary" ] && json_add_string 'warnings' "$warningSummary" + json_close_object + json_add_array 'errors' + for k in $(json get errors); do + json_add_object "$k" + json_add_string 'code' "$(json get error "$k" 'code')" + json_add_string 'info' "$(json get error "$k" 'info')" + json_close_object + done + json_close_array + json_add_array 'warnings' + for k in $(json get warnings); do + json_add_object "$k" + json_add_string 'code' "$(json get warning "$k" 'code')" + json_add_string 'info' "$(json get warning "$k" 'info')" + json_close_object + done + json_close_array if [ "$strict_enforcement" -ne '0' ] && str_contains "$gatewaySummary" '0.0.0.0'; then json_add_string 'mode' 'strict' fi - json_close_object procd_close_data procd_close_instance } service_started() { + [ -n "$pbr_boot_flag" ] && return 0 + local error warning c if nft_file 'exists'; then procd_set_config_changed firewall if nft_file 'exists'; then @@ -2142,12 +2187,26 @@ service_started() { else [ -n "$gatewaySummary" ] && output "$serviceName (nft mode) started with gateways:\n${gatewaySummary}" fi - state print 'errorSummary' - state print 'warningSummary' + error="$(json get error)" + warning="$(json get warning)" + if [ -n "$error" ]; then + for c in $error; do + code="$(json get error "$c" 'code')" + info="$(json get error "$c" 'info')" + output_error "$(get_text "$code" "$info")" + done + fi + if [ -n "$warning" ]; then + for c in $warning; do + code="$(json get warning "$c" 'code')" + info="$(json get warning "$c" 'info')" + output_warning "$(get_text "$code" "$info")" + done + fi touch "$packageLockFile" - if [ -n "$errorSummary" ]; then + if [ -n "$error" ]; then return 2 - elif [ -n "$warningSummary" ]; then + elif [ -n "$warning" ]; then return 1 else return 0 @@ -2165,9 +2224,9 @@ service_triggers() { load_validate_policy load_validate_include procd_close_validate - if [ -n "$pbr_boot_flag" ]; then + if [ -n "$pbr_boot_flag" ] && is_integer "$procd_boot_trigger_delay"; then output "Setting trigger (on_boot) " - procd_add_raw_trigger "interface.*.up" 5000 "/etc/init.d/${packageName}" start && output_okn || output_failn + procd_add_raw_trigger "interface.*.up" "$procd_boot_trigger_delay" "/etc/init.d/${packageName}" start && output_okn || output_failn else procd_open_trigger procd_add_config_trigger "config.change" 'openvpn' "/etc/init.d/${packageName}" reload 'on_openvpn_change' @@ -2353,6 +2412,7 @@ status_service() { # shellcheck disable=SC2120 load_validate_config() { uci_load_validate "$packageName" "$packageName" "$1" "${2}${3:+ $3}" \ + 'debug_dnsmasq:bool:0' \ 'enabled:bool:0' \ 'strict_enforcement:bool:1' \ 'ipv6_enabled:bool:0' \ @@ -2364,8 +2424,9 @@ load_validate_config() { 'icmp_interface:or("", tor, uci("network", "@interface"))' \ 'ignored_interface:list(or(tor, uci("network", "@interface")))' \ 'supported_interface:list(or(ignore, tor, regex("xray_.*"), uci("network", "@interface")))' \ - 'procd_reload_delay:integer:0' \ + 'procd_boot_trigger_delay:range(1000,10000):5000' \ 'procd_lan_device:list(or(network)):br-lan' \ + 'procd_reload_delay:uinteger:0' \ 'procd_wan_interface:network:wan' \ 'procd_wan6_interface:network:wan6' \ 'wan_ip_rules_priority:uinteger:30000' \ -- 2.30.2