#!/bin/sh ########################################################### # ______ _ ____ _____ # # | ____|| | / __ \ / ____| # # | |__ | | ___ __ __| | | | ___ | (___ # # | __| | | / _ \\ \/ /| | | | / _ \ \___ \ # # | | | || __/ > < | |__| || (_) |____) | # # |_| |_| \___|/_/\_\ \___\_\ \___/|_____/ # # # ########################################################### # FlexQoS maintained by dave14305 # Contributors: @maghuro # shellcheck disable=SC1090,SC1091,SC2039,SC2154,SC3043 version=2.0.0 release=2021-11-07 # Forked from FreshJR_QOS v8.8, written by FreshJR07 https://github.com/FreshJR07/FreshJR_QOS # License # FlexQoS is free to use under the GNU General Public License, version 3 (GPL-3.0). # https://opensource.org/licenses/GPL-3.0 # initialize Merlin Addon API helper functions . /usr/sbin/helper.sh # -x is a flag to show verbose script output for debugging purposes only if [ "${1}" = "-x" ]; then shift set -x fi # Global variables readonly SCRIPTNAME_DISPLAY="FlexQoS" readonly SCRIPTNAME="flexqos" readonly GIT_REPO="https://raw.githubusercontent.com/dave14305/${SCRIPTNAME_DISPLAY}" GIT_BRANCH="$(am_settings_get "${SCRIPTNAME}_branch")" if [ -z "${GIT_BRANCH}" ]; then GIT_BRANCH="master" fi GIT_URL="${GIT_REPO}/${GIT_BRANCH}" readonly ADDON_DIR="/jffs/addons/${SCRIPTNAME}" readonly WEBUIPATH="${ADDON_DIR}/${SCRIPTNAME}.asp" readonly SCRIPTPATH="${ADDON_DIR}/${SCRIPTNAME}.sh" readonly LOCKFILE="/tmp/addonwebui.lock" IPv6_enabled="$(nvram get ipv6_service)" qdisc="$(am_settings_get "${SCRIPTNAME}"_qdisc)" qdisc="${qdisc:=1}" # default to fq_codel (1) if not set # Update version number in custom_settings.txt for reading in WebUI if [ "$(am_settings_get "${SCRIPTNAME}"_ver)" != "${version}" ]; then am_settings_set "${SCRIPTNAME}"_ver "${version}" fi # If Merlin fq_codel patch is active, use original tc binary for passing commands # Will be obsolete in 386.1 and higher. if [ -e "/usr/sbin/realtc" ]; then TC="/usr/sbin/realtc" else TC="/usr/sbin/tc" fi # Detect if script is run from an SSH shell interactively or being invoked via cron or from the WebUI (unattended) if tty >/dev/null 2>&1; then mode="interactive" else mode="unattended" fi # marks for iptables rules # We use the ffff value to avoid conflicts with predefined apps in AppDB so there would be no conflict # with any user-defined AppDB rules. Net_mark="09" Work_mark="06" Gaming_mark="08" Others_mark="0a" Web_mark="18" Streaming_mark="04" Downloads_mark="03" Learn_mark="3f" Net_DSCP="CS6" Work_DSCP="AF41" Gaming_DSCP="CS6" Others_DSCP="CS0" Web_DSCP="CS0" Streaming_DSCP="AF41" Downloads_DSCP="CS1" Learn_DSCP="CS0" logmsg() { if [ "$#" = "0" ]; then return fi logger -t "${SCRIPTNAME_DISPLAY}" "$*" } # logmsg Red() { printf -- '\033[1;31m%s\033[0m\n' "${1}" } Green() { printf -- '\033[1;32m%s\033[0m\n' "${1}" } Blue() { printf -- '\033[1;36m%s\033[0m\n' "${1}" } Yellow() { printf -- '\033[1;33m%s\033[0m\n' "${1}" } get_class_mark() { case "${1}" in 0) printf "%s\n" "${Net_mark}" ;; 1) printf "%s\n" "${Gaming_mark}" ;; 2) printf "%s\n" "${Streaming_mark}" ;; 3) printf "%s\n" "${Work_mark}" ;; 4) printf "%s\n" "${Web_mark}" ;; 5) printf "%s\n" "${Downloads_mark}" ;; 6) printf "%s\n" "${Others_mark}" ;; 7) printf "%s\n" "${Learn_mark}" ;; *) printf "%s\n" "" ;; esac } iptables_static_rules() { local OUTPUTCLS OUTPUTCLS="$(am_settings_get "${SCRIPTNAME}"_outputcls)" OUTPUTCLS="$(get_class_mark "${OUTPUTCLS:-5}")" # If setting not found, use default value of 5 (File Transfers) printf "Applying iptables static rules\n" # Reference for VPN Fix origin: https://www.snbforums.com/threads/36836/page-78#post-412034 # Partially fixed in https://github.com/RMerl/asuswrt-merlin.ng/commit/f7d6478df7b934c9540fa9740ad71d49d84a1756 iptables -t mangle -D OUTPUT -o "${wan}" -p udp -m multiport --dports 53,123 -j MARK --set-mark 0x40"${Net_mark}"0fff/0xc03f0fff > /dev/null 2>&1 # Outbound DNS & NTP iptables -t mangle -A OUTPUT -o "${wan}" -p udp -m multiport --dports 53,123 -j MARK --set-mark 0x40"${Net_mark}"0fff/0xc03f0fff iptables -t mangle -D OUTPUT -o "${wan}" -p tcp -m multiport --dports 53,853 -j MARK --set-mark 0x40"${Net_mark}"0fff/0xc03f0fff > /dev/null 2>&1 # Outbound DNS and DoT iptables -t mangle -A OUTPUT -o "${wan}" -p tcp -m multiport --dports 53,853 -j MARK --set-mark 0x40"${Net_mark}"0fff/0xc03f0fff iptables -t mangle -D OUTPUT -o "${wan}" -p udp -m multiport ! --dports 53,123 -j MARK --set-mark 0x40"${OUTPUTCLS}"ffff/0xc03fffff > /dev/null 2>&1 #VPN Fix - (Fixes upload traffic not detected when the router is acting as a VPN Client) iptables -t mangle -A OUTPUT -o "${wan}" -p udp -m multiport ! --dports 53,123 -j MARK --set-mark 0x40"${OUTPUTCLS}"ffff/0xc03fffff iptables -t mangle -D OUTPUT -o "${wan}" -p tcp -m multiport ! --dports 53,853 -j MARK --set-mark 0x40"${OUTPUTCLS}"ffff/0xc03fffff > /dev/null 2>&1 #VPN Fix - (Fixes upload traffic not detected when the router is acting as a VPN Client) iptables -t mangle -A OUTPUT -o "${wan}" -p tcp -m multiport ! --dports 53,853 -j MARK --set-mark 0x40"${OUTPUTCLS}"ffff/0xc03fffff iptables -t mangle -N "${SCRIPTNAME_DISPLAY}" 2>/dev/null iptables -t mangle -F "${SCRIPTNAME_DISPLAY}" 2>/dev/null iptables -t mangle -D POSTROUTING -j "${SCRIPTNAME_DISPLAY}" 2>/dev/null iptables -t mangle -A POSTROUTING -j "${SCRIPTNAME_DISPLAY}" if [ "${IPv6_enabled}" != "disabled" ]; then ip6tables -t mangle -D OUTPUT -o "${wan}" -p udp -m multiport --dports 53,123 -j MARK --set-mark 0x40"${Net_mark}"0fff/0xc03f0fff > /dev/null 2>&1 # Outbound DNS & NTP ip6tables -t mangle -A OUTPUT -o "${wan}" -p udp -m multiport --dports 53,123 -j MARK --set-mark 0x40"${Net_mark}"0fff/0xc03f0fff ip6tables -t mangle -D OUTPUT -o "${wan}" -p tcp -m multiport --dports 53,853 -j MARK --set-mark 0x40"${Net_mark}"0fff/0xc03f0fff > /dev/null 2>&1 # Outbound DNS and DoT ip6tables -t mangle -A OUTPUT -o "${wan}" -p tcp -m multiport --dports 53,853 -j MARK --set-mark 0x40"${Net_mark}"0fff/0xc03f0fff ip6tables -t mangle -D OUTPUT -o "${wan}" -p udp -m multiport ! --dports 53,123 -j MARK --set-mark 0x40"${OUTPUTCLS}"ffff/0xc03fffff > /dev/null 2>&1 #VPN Fix - (Fixes upload traffic not detected when the router is acting as a VPN Client) ip6tables -t mangle -A OUTPUT -o "${wan}" -p udp -m multiport ! --dports 53,123 -j MARK --set-mark 0x40"${OUTPUTCLS}"ffff/0xc03fffff ip6tables -t mangle -D OUTPUT -o "${wan}" -p tcp -m multiport ! --dports 53,853 -j MARK --set-mark 0x40"${OUTPUTCLS}"ffff/0xc03fffff > /dev/null 2>&1 #VPN Fix - (Fixes upload traffic not detected when the router is acting as a VPN Client) ip6tables -t mangle -A OUTPUT -o "${wan}" -p tcp -m multiport ! --dports 53,853 -j MARK --set-mark 0x40"${OUTPUTCLS}"ffff/0xc03fffff ip6tables -t mangle -N "${SCRIPTNAME_DISPLAY}" 2>/dev/null ip6tables -t mangle -F "${SCRIPTNAME_DISPLAY}" 2>/dev/null ip6tables -t mangle -D POSTROUTING -j "${SCRIPTNAME_DISPLAY}" 2>/dev/null ip6tables -t mangle -A POSTROUTING -j "${SCRIPTNAME_DISPLAY}" fi if [ "${qdisc}" = "2" ]; then modprobe xt_comment # Setup mark to DSCP mappings for CAKE tins iptables -t mangle -N "${SCRIPTNAME_DISPLAY}_DSCP" 2>/dev/null iptables -t mangle -F "${SCRIPTNAME_DISPLAY}_DSCP" 2>/dev/null iptables -t mangle -D POSTROUTING -j "${SCRIPTNAME_DISPLAY}_DSCP" 2>/dev/null iptables -t mangle -A POSTROUTING -j "${SCRIPTNAME_DISPLAY}_DSCP" # Setup chain for AppDB rules iptables -t mangle -N "${SCRIPTNAME_DISPLAY}_AppDB" 2>/dev/null iptables -t mangle -F "${SCRIPTNAME_DISPLAY}_AppDB" 2>/dev/null iptables -t mangle -D POSTROUTING -j "${SCRIPTNAME_DISPLAY}_AppDB" 2>/dev/null iptables -t mangle -A POSTROUTING -j "${SCRIPTNAME_DISPLAY}_AppDB" # Voice tin iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x060000/0x3f0000 -j DSCP --set-dscp-class CS6 -m comment --comment "VoIP services" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x060000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x080000/0x3f0000 -j DSCP --set-dscp-class CS6 -m comment --comment "Online games" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x080000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x090000/0x3f0000 -j DSCP --set-dscp-class CS6 -m comment --comment "Management tools and protocols" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x090000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x120000/0x3f0000 -j DSCP --set-dscp-class CS6 -m comment --comment "Network protocols" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x120000/0x3f0000 -j RETURN # Video tin iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x000000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Instant messengers" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x000000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x040000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Media streaming services" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x040000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x050000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Email messaging services" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x050000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x110000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Business tools" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x110000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x0f0000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Web instant messengers" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x0f0000/0x3f0000 -j RETURN # Bulk tin iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x010000/0x3f0000 -j DSCP --set-dscp-class CS1 -m comment --comment "Peer-to-peer networks" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x010000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x030000/0x3f0000 -j DSCP --set-dscp-class CS1 -m comment --comment "File sharing services and tools" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x030000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x0e0000/0x3f0000 -j DSCP --set-dscp-class CS1 -m comment --comment "Security update tools" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x0e0000/0x3f0000 -j RETURN iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x1a0000/0x3f0000 -j DSCP --set-dscp-class CS1 -m comment --comment "Advertisements" iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x1a0000/0x3f0000 -j RETURN # Besteffort - default iptables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -j DSCP --set-dscp-class CS0 if [ "${IPv6_enabled}" != "disabled" ]; then # Setup mark to DSCP mappings for CAKE tins ip6tables -t mangle -N "${SCRIPTNAME_DISPLAY}_DSCP" 2>/dev/null ip6tables -t mangle -F "${SCRIPTNAME_DISPLAY}_DSCP" 2>/dev/null ip6tables -t mangle -D POSTROUTING -j "${SCRIPTNAME_DISPLAY}_DSCP" 2>/dev/null ip6tables -t mangle -A POSTROUTING -j "${SCRIPTNAME_DISPLAY}_DSCP" # Setup chain for AppDB rules ip6tables -t mangle -N "${SCRIPTNAME_DISPLAY}_AppDB" 2>/dev/null ip6tables -t mangle -F "${SCRIPTNAME_DISPLAY}_AppDB" 2>/dev/null ip6tables -t mangle -D POSTROUTING -j "${SCRIPTNAME_DISPLAY}_AppDB" 2>/dev/null ip6tables -t mangle -A POSTROUTING -j "${SCRIPTNAME_DISPLAY}_AppDB" # Voice tin ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x060000/0x3f0000 -j DSCP --set-dscp-class CS6 -m comment --comment "VoIP services" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x060000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x080000/0x3f0000 -j DSCP --set-dscp-class CS6 -m comment --comment "Online games" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x080000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x090000/0x3f0000 -j DSCP --set-dscp-class CS6 -m comment --comment "Management tools and protocols" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x090000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x120000/0x3f0000 -j DSCP --set-dscp-class CS6 -m comment --comment "Network protocols" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x120000/0x3f0000 -j RETURN # Video tin ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x000000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Instant messengers" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x000000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x040000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Media streaming services" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x040000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x050000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Email messaging services" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x050000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x110000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Business tools" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x110000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x0f0000/0x3f0000 -j DSCP --set-dscp-class AF41 -m comment --comment "Web instant messengers" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x0f0000/0x3f0000 -j RETURN # Bulk tin ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x010000/0x3f0000 -j DSCP --set-dscp-class CS1 -m comment --comment "Peer-to-peer networks" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x010000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x030000/0x3f0000 -j DSCP --set-dscp-class CS1 -m comment --comment "File sharing services and tools" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x030000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x0e0000/0x3f0000 -j DSCP --set-dscp-class CS1 -m comment --comment "Security update tools" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x0e0000/0x3f0000 -j RETURN ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x1a0000/0x3f0000 -j DSCP --set-dscp-class CS1 -m comment --comment "Advertisements" ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -m mark --mark 0x1a0000/0x3f0000 -j RETURN # Besteffort - default ip6tables -t mangle -A "${SCRIPTNAME_DISPLAY}_DSCP" -j DSCP --set-dscp-class CS0 fi fi } get_static_filter() { local MARK local FLOWID MARK="${1}" FLOWID="${2}" printf "filter add dev %s protocol all prio 5 u32 match mark 0x80%sffff 0xc03fffff flowid %s\n" "${tclan}" "${MARK}" "${FLOWID}" printf "filter add dev %s protocol all prio 5 u32 match mark 0x40%sffff 0xc03fffff flowid %s\n" "${tcwan}" "${MARK}" "${FLOWID}" } # get_static_filter write_appdb_static_rules() { # These rules define the flowid (priority level) of the Class destinations selected by users in iptables rules. # Previous versions of the script were susceptible to the chosen Class being overridden by the users AppDB rules. # Adding these filters ensures the Class you select in iptables rules is strictly observed. # prio 5 is used because the first default filter rule (mark 0x80030000 0xc03f0000) is found at prio 6 as of this writing, # so we want these filters to always take precedence over the built-in filters. # File is overwritten (>) if it exists and later appended by write_appdb_rules() and write_custom_rates(). { get_static_filter "${Net_mark}" "${Net_flow}" get_static_filter "${Work_mark}" "${Work_flow}" get_static_filter "${Gaming_mark}" "${Gaming_flow}" get_static_filter "${Others_mark}" "${Others_flow}" get_static_filter "${Web_mark}" "${Web_flow}" get_static_filter "${Streaming_mark}" "${Streaming_flow}" get_static_filter "${Downloads_mark}" "${Downloads_flow}" get_static_filter "${Learn_mark}" "${Learn_flow}" } >> "/tmp/${SCRIPTNAME}_tcrules" } # write_appdb_static_rules get_burst() { local RATE local DURATION local BURST local MIN_BURST RATE="${1}" DURATION="${2}" # acceptable added latency in microseconds (1ms) # https://github.com/tohojo/sqm-scripts/blob/master/src/functions.sh # let's assume ATM/AAL5 to be the worst case encapsulation # and 48 Bytes a reasonable worst case per packet overhead MIN_BURST=$(( WANMTU + 48 )) # add 48 bytes to MTU for the overhead MIN_BURST=$(( MIN_BURST + 47 )) # now do ceil(Min_BURST / 48) * 53 in shell integer arithmic MIN_BURST=$(( MIN_BURST / 48 )) MIN_BURST=$(( MIN_BURST * 53 )) # for MTU 1489 to 1536 this will result in MIN_BURST = 1749 Bytes BURST=$((DURATION*RATE/8000)) # If the calculated burst is less than ASUS' minimum value of 3200, use 3200 # to avoid problems with child and leaf classes outside of FlexQoS scope that use 3200. # If using fq_codel option, use 1600 as a minimum burst. if [ "$(am_settings_get "${SCRIPTNAME}"_qdisc)" = "0" ]; then if [ "${BURST}" -lt 3200 ]; then BURST=3200 fi elif [ "${BURST}" -lt "${MIN_BURST}" ]; then BURST="${MIN_BURST}" fi printf "%s" "${BURST}" } # get_burst get_cburst() { local RATE local BURST local MIN_BURST RATE="${1}" # https://github.com/tohojo/sqm-scripts/blob/master/src/functions.sh # let's assume ATM/AAL5 to be the worst case encapsulation # and 48 Bytes a reasonable worst case per packet overhead MIN_BURST=$(( WANMTU + 48 )) # add 48 bytes to MTU for the overhead MIN_BURST=$(( MIN_BURST + 47 )) # now do ceil(Min_BURST / 48) * 53 in shell integer arithmic MIN_BURST=$(( MIN_BURST / 48 )) MIN_BURST=$(( MIN_BURST * 53 )) # for MTU 1489 to 1536 this will result in MIN_BURST = 1749 Bytes BURST=$((RATE*1000/1280000)) BURST=$((BURST*1600)) # If the calculated burst is less than ASUS' minimum value of 3200, use 3200 # to avoid problems with child and leaf classes outside of FlexQoS scope that use 3200. if [ "${BURST}" -lt 3200 ]; then if [ "$(am_settings_get "${SCRIPTNAME}"_qdisc)" = "0" ]; then BURST=3200 else BURST="${MIN_BURST}" fi fi printf "%s" "${BURST}" } # get_cburst get_quantum() { local RATE local QUANTUM local MIN_QUANTUM RATE="${1}" # https://github.com/tohojo/sqm-scripts/blob/master/src/functions.sh # let's assume ATM/AAL5 to be the worst case encapsulation # and 48 Bytes a reasonable worst case per packet overhead MIN_QUANTUM=$(( WANMTU + 48 )) # add 48 bytes to MTU for the overhead MIN_QUANTUM=$(( MIN_QUANTUM + 47 )) # now do ceil(Min_BURST / 48) * 53 in shell integer arithmic MIN_QUANTUM=$(( MIN_QUANTUM / 48 )) MIN_QUANTUM=$(( MIN_QUANTUM * 53 )) # for MTU 1489 to 1536 this will result in MIN_BURST = 1749 Bytes QUANTUM=$((RATE*1000/8/10)) # If the calculated quantum is less than the MTU, use MTU+14 as the quantum if [ "${QUANTUM}" -lt "${MIN_QUANTUM}" ]; then QUANTUM="${MIN_QUANTUM}" fi printf "%s" "${QUANTUM}" } # get_quantum get_overhead() { local NVRAM_OVERHEAD local NVRAM_ATM local NVRAM_MPU local OVERHEAD NVRAM_OVERHEAD="$(nvram get qos_overhead)" if [ -n "${NVRAM_OVERHEAD}" ] && [ "${NVRAM_OVERHEAD}" -gt "0" ]; then OVERHEAD="overhead ${NVRAM_OVERHEAD}" NVRAM_ATM="$(nvram get qos_atm)" case ${qdisc} in 2) # CAKE NVRAM_MPU="$(nvram get qos_mpu)" if [ -n "${NVRAM_MPU}" ] && [ "${NVRAM_MPU}" -gt "0" ]; then OVERHEAD="${OVERHEAD} mpu ${NVRAM_MPU}" fi if [ "${NVRAM_ATM}" = "1" ]; then OVERHEAD="${OVERHEAD} atm" elif [ "${NVRAM_ATM}" = "2" ]; then OVERHEAD="${OVERHEAD} ptm" fi ;; *) if [ "${NVRAM_ATM}" = "1" ]; then OVERHEAD="${OVERHEAD} linklayer atm" fi ;; esac fi printf "%s" "${OVERHEAD}" } # get_overhead get_custom_rate_rule() { local IFACE local PRIO local RATE local CEIL local DURATION IFACE="${1}" PRIO="${2}" RATE="${3}" CEIL="${4}" DURATION=1000 # 1000 microseconds = 1 ms printf "class change dev %s parent 1:1 classid 1:1%s htb %s prio %s rate %sKbit ceil %sKbit burst %sb cburst %sb quantum %s\n" \ "${IFACE}" "${PRIO}" "$(get_overhead)" "${PRIO}" "${RATE}" "${CEIL}" "$(get_burst "${CEIL}" "${DURATION}")" "$(get_cburst "${CEIL}")" "$(get_quantum "${RATE}")" } # get_custom_rate_rule write_custom_rates() { local i if [ "${DownCeil}" -gt "0" ] && [ "${UpCeil}" -gt "0" ]; then # For all 8 classes (0-7), write the tc commands needed to modify the bandwidth rates and related parameters # that get assigned in set_tc_variables(). # File is appended (>>) because it is initially created in write_appdb_static_rules(). { for i in 0 1 2 3 4 5 6 7 do eval get_custom_rate_rule "${tclan}" "${i}" \$DownRate${i} \$DownCeil${i} eval get_custom_rate_rule "${tcwan}" "${i}" \$UpRate${i} \$UpCeil${i} done } >> "/tmp/${SCRIPTNAME}_tcrules" fi } # write_custom_rates set_tc_variables() { # Read various settings from the router and construct the variables needed to implement the custom rules. local drp0 drp1 drp2 drp3 drp4 drp5 drp6 drp7 local dcp0 dcp1 dcp2 dcp3 dcp4 dcp5 dcp6 dcp7 local urp0 urp1 urp2 urp3 urp4 urp5 urp6 urp7 local ucp0 ucp1 ucp2 ucp3 ucp4 ucp5 ucp6 ucp7 # shellcheck disable=SC2034 local Cat0DownBandPercent Cat1DownBandPercent Cat2DownBandPercent Cat3DownBandPercent Cat4DownBandPercent Cat5DownBandPercent Cat6DownBandPercent Cat7DownBandPercent # shellcheck disable=SC2034 local Cat0DownCeilPercent Cat1DownCeilPercent Cat2DownCeilPercent Cat3DownCeilPercent Cat4DownCeilPercent Cat5DownCeilPercent Cat6DownCeilPercent Cat7DownCeilPercent # shellcheck disable=SC2034 local Cat0UpBandPercent Cat1UpBandPercent Cat2UpBandPercent Cat3UpBandPercent Cat4UpBandPercent Cat5UpBandPercent Cat6UpBandPercent Cat7UpBandPercent # shellcheck disable=SC2034 local Cat0UpCeilPercent Cat1UpCeilPercent Cat2UpCeilPercent Cat3UpCeilPercent Cat4UpCeilPercent Cat5UpCeilPercent Cat6UpCeilPercent Cat7UpCeilPercent local flowid local line local i tclan="br0" # Determine the WAN interface name used by tc by finding the existing htb root qdisc that is NOT br0. # If not found, check the dev_wan file created by Adaptive QoS. # If still not determined, assume eth0 but something is probably wrong at this point. tcwan="$("${TC}" qdisc ls | sed -n 's/qdisc htb.*dev \([^b][^r].*\) root.*/\1/p')" if [ -z "${tcwan}" ] && [ -s "/tmp/bwdpi/dev_wan" ]; then tcwan="$(/bin/grep -oE "eth[0-9]|usb[0-9]" /tmp/bwdpi/dev_wan)" fi if [ -z "${tcwan}" ]; then tcwan="eth0" fi # Detect the default filter rule for Untracked traffic (Mark 000000) if it exists. # Newer 384 stock firmware dropped this rule, so Untracked traffic flows into the Work-From-Home priority by default. # First check for older ASUS default rule (0x80000000 0xc000ffff). # If not found, get the prio for the Work-From-Home Instant messengers category 00 (0x80000000 0xc03f0000) and subtract 1. undf_prio="$("${TC}" filter show dev br0 | /bin/grep -i -m1 -B1 "0x80000000 0xc000ffff" | sed -nE 's/.* pref ([0-9]+) .*/\1/p')" if [ -z "${undf_prio}" ]; then undf_prio="$("${TC}" filter show dev br0 | /bin/grep -i -m1 -B1 "0x80000000 0xc03f0000" | sed -nE 's/.* pref ([0-9]+) .*/\1/p')" undf_prio="$((undf_prio-1))" fi read -r \ drp0 drp1 drp2 drp3 drp4 drp5 drp6 drp7 \ dcp0 dcp1 dcp2 dcp3 dcp4 dcp5 dcp6 dcp7 \ urp0 urp1 urp2 urp3 urp4 urp5 urp6 urp7 \ ucp0 ucp1 ucp2 ucp3 ucp4 ucp5 ucp6 ucp7 \ <<EOF $(echo "${bwrates}" | sed 's/^<//g;s/[<>]/ /g') EOF # read priority order of QoS categories as set by user on the QoS page of the GUI flowid=0 while read -r line; do if [ "$(echo "${line}" | cut -c 1)" = '[' ]; then flowid="$(echo "${line}" | cut -c 2)" fi case "${line}" in '0') Work_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp3}" eval "Cat${flowid}DownCeilPercent=${dcp3}" eval "Cat${flowid}UpBandPercent=${urp3}" eval "Cat${flowid}UpCeilPercent=${ucp3}" ;; '1') Downloads_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp5}" eval "Cat${flowid}DownCeilPercent=${dcp5}" eval "Cat${flowid}UpBandPercent=${urp5}" eval "Cat${flowid}UpCeilPercent=${ucp5}" ;; '4') # Special handling for category 4 since it is duplicated between Streaming and Learn-From-Home. # We have to find the priority placement of Learn-From-Home versus Streaming in the QoS GUI to know # if the first time we encounter a 4 in the file if it is meant to be Streaming or Learn-From-Home. # The second time we encounter a 4, we know it is meant for the remaining option. if nvram get bwdpi_app_rulelist | /bin/grep -qE "<4,13(<.*)?<4<"; then # Learn-From-Home is higher priority than Streaming if [ -z "${Learn_flow}" ]; then Learn_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp7}" eval "Cat${flowid}DownCeilPercent=${dcp7}" eval "Cat${flowid}UpBandPercent=${urp7}" eval "Cat${flowid}UpCeilPercent=${ucp7}" else Streaming_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp2}" eval "Cat${flowid}DownCeilPercent=${dcp2}" eval "Cat${flowid}UpBandPercent=${urp2}" eval "Cat${flowid}UpCeilPercent=${ucp2}" fi else # Streaming is higher priority than Learn-From-Home if [ -z "${Streaming_flow}" ]; then Streaming_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp2}" eval "Cat${flowid}DownCeilPercent=${dcp2}" eval "Cat${flowid}UpBandPercent=${urp2}" eval "Cat${flowid}UpCeilPercent=${ucp2}" else Learn_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp7}" eval "Cat${flowid}DownCeilPercent=${dcp7}" eval "Cat${flowid}UpBandPercent=${urp7}" eval "Cat${flowid}UpCeilPercent=${ucp7}" fi fi # Check Learn-From-Home and Streaming priority order ;; '7') Others_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp6}" eval "Cat${flowid}DownCeilPercent=${dcp6}" eval "Cat${flowid}UpBandPercent=${urp6}" eval "Cat${flowid}UpCeilPercent=${ucp6}" ;; '8') Gaming_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp1}" eval "Cat${flowid}DownCeilPercent=${dcp1}" eval "Cat${flowid}UpBandPercent=${urp1}" eval "Cat${flowid}UpCeilPercent=${ucp1}" ;; '9') Net_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp0}" eval "Cat${flowid}DownCeilPercent=${dcp0}" eval "Cat${flowid}UpBandPercent=${urp0}" eval "Cat${flowid}UpCeilPercent=${ucp0}" ;; '24') Web_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp4}" eval "Cat${flowid}DownCeilPercent=${dcp4}" eval "Cat${flowid}UpBandPercent=${urp4}" eval "Cat${flowid}UpCeilPercent=${ucp4}" ;; 'na') # This is how the old ASUS default category would appear, but this option will soon be deprecated # when all supported models are using the new QoS Categories. Learn_flow="1:1${flowid}" eval "Cat${flowid}DownBandPercent=${drp7}" eval "Cat${flowid}DownCeilPercent=${dcp7}" eval "Cat${flowid}UpBandPercent=${urp7}" eval "Cat${flowid}UpCeilPercent=${ucp7}" ;; esac done <<EOF $(sed -E '/^ceil_/d;s/rule=//g;/\{/q' /tmp/bwdpi/qosd.conf | head -n -1) EOF #calculate up/down rates based on user-provided bandwidth from GUI #GUI shows in Mb/s; nvram stores in Kb/s DownCeil="$(printf "%.0f" "$(nvram get qos_ibw)")" UpCeil="$(printf "%.0f" "$(nvram get qos_obw)")" # Only apply custom rates if Manual Bandwidth mode set in QoS page if [ "${DownCeil}" -gt "0" ] && [ "${UpCeil}" -gt "0" ]; then # Automatic bandwidth mode incompatible with custom rates i=0 while [ "${i}" -lt "8" ] do eval "DownRate${i}=\$((DownCeil\*Cat${i}DownBandPercent/100))" eval "UpRate${i}=\$((UpCeil\*Cat${i}UpBandPercent/100))" eval "DownCeil${i}=\$((DownCeil\*Cat${i}DownCeilPercent/100))" eval "UpCeil${i}=\$((UpCeil\*Cat${i}UpCeilPercent/100))" i="$((i+1))" done fi # Auto Bandwidth check } # set_tc_variables appdb() { # Search TrendMicro appdb file for matches to user-specified string. Return up to 25 matches local line cat_decimal /bin/grep -m 25 -i "${1}" /tmp/bwdpi/bwdpi.app.db | while read -r line; do echo "${line}" | awk -F "," '{printf " Application: %s, Mark: %02X%04X, Default Class: ", $4, $1, $2}' cat_decimal=$(echo "${line}" | cut -f 1 -d "," ) case "${cat_decimal}" in '9'|'18'|'19'|'20') printf "Net Control Packets" ;; '0'|'5'|'6'|'15'|'17') printf "Work-From-Home" ;; '8') printf "Gaming" ;; '7'|'10'|'11'|'21'|'23') printf "Others" ;; '13'|'24') printf "Web Surfing" ;; '4') printf "Video and Audio Streaming" ;; '1'|'3'|'14') printf "File Transferring" ;; *) printf "Unknown" ;; esac printf "\n" done } # appdb webconfigpage() { local urlpage urlproto urldomain urlport # Eye candy function that will construct a URL to display after install or upgrade so a user knows where to # find the webUI page. In most cases though, they will go to the Adaptive QoS tab and find the FlexQoS sub-tab anyway. urlpage="$(sed -nE "/${SCRIPTNAME_DISPLAY}/ s/.*url\: \"(user[0-9]+\.asp)\".*/\1/p" /tmp/menuTree.js)" if [ "$(nvram get http_enable)" = "1" ]; then urlproto="https" else urlproto="http" fi if [ -n "$(nvram get lan_domain)" ]; then urldomain="$(nvram get lan_hostname).$(nvram get lan_domain)" else urldomain="$(nvram get lan_ipaddr)" fi if [ "$(nvram get ${urlproto}_lanport)" = "80" ] || [ "$(nvram get ${urlproto}_lanport)" = "443" ]; then urlport="" else urlport=":$(nvram get ${urlproto}_lanport)" fi if echo "${urlpage}" | grep -qE "user[0-9]+\.asp"; then printf "Advanced configuration available via:\n" Blue " ${urlproto}://${urldomain}${urlport}/${urlpage}" fi } # webconfigpage scriptinfo() { # Version header used in interactive sessions [ "${mode}" = "interactive" ] || return printf "\n" Green "${SCRIPTNAME_DISPLAY} v${version} released ${release}" if [ "${GIT_BRANCH}" != "master" ]; then Yellow " Development channel" fi printf "\n" } # scriptinfo debug() { local RMODEL ipt_debug appdb_debug [ -z "$(nvram get odmpid)" ] && RMODEL="$(nvram get productid)" || RMODEL="$(nvram get odmpid)" Green "[SPOILER=\"${SCRIPTNAME_DISPLAY} Debug\"][CODE]" scriptinfo printf "Debug date : %s\n" "$(date +'%Y-%m-%d %H:%M:%S%z')" printf "Router Model : %s\n" "${RMODEL}" printf "Firmware Ver : %s_%s\n" "$(nvram get buildno)" "$(nvram get extendno)" printf "DPI/Sig Ver : %s / %s\n" "$(nvram get bwdpi_dpi_ver)" "$(nvram get bwdpi_sig_ver)" get_config set_tc_variables printf "WAN iface : %s\n" "${wan}" printf "tc WAN iface : %s\n" "${tcwan}" printf "IPv6 : %s\n" "${IPv6_enabled}" printf "Undf Prio : %s\n" "${undf_prio}" printf "Down Band : %s\n" "${DownCeil}" printf "Up Band : %s\n" "${UpCeil}" printf "**************\n" printf "Net Control : %s\n" "${Net_flow}" printf "Work-From-Home: %s\n" "${Work_flow}" printf "Gaming : %s\n" "${Gaming_flow}" printf "Others : %s\n" "${Others_flow}" printf "Web Surfing : %s\n" "${Web_flow}" printf "Streaming : %s\n" "${Streaming_flow}" printf "File Transfers: %s\n" "${Downloads_flow}" printf "Learn-From-Home: %s\n" "${Learn_flow}" printf "**************\n" # Only print custom rates if Manual Bandwidth setting is enabled on QoS page if [ "${DownCeil}" -gt "0" ] && [ "${UpCeil}" -gt "0" ]; then printf "Downrates : %7s, %7s, %7s, %7s, %7s, %7s, %7s, %7s\n" "${DownRate0}" "${DownRate1}" "${DownRate2}" "${DownRate3}" "${DownRate4}" "${DownRate5}" "${DownRate6}" "${DownRate7}" printf "Downceils : %7s, %7s, %7s, %7s, %7s, %7s, %7s, %7s\n" "${DownCeil0}" "${DownCeil1}" "${DownCeil2}" "${DownCeil3}" "${DownCeil4}" "${DownCeil5}" "${DownCeil6}" "${DownCeil7}" printf "Uprates : %7s, %7s, %7s, %7s, %7s, %7s, %7s, %7s\n" "${UpRate0}" "${UpRate1}" "${UpRate2}" "${UpRate3}" "${UpRate4}" "${UpRate5}" "${UpRate6}" "${UpRate7}" printf "Upceils : %7s, %7s, %7s, %7s, %7s, %7s, %7s, %7s\n" "${UpCeil0}" "${UpCeil1}" "${UpCeil2}" "${UpCeil3}" "${UpCeil4}" "${UpCeil5}" "${UpCeil6}" "${UpCeil7}" printf "**************\n" else printf "Custom rates disabled with Automatic Bandwidth mode!\n" printf "**************\n" fi ipt_debug="$(am_settings_get "${SCRIPTNAME}"_iptables)" printf "iptables settings: %s\n" "${ipt_debug:-Defaults}" write_iptables_rules # Remove superfluous commands from the output in order to focus on the parsed details /bin/sed -E "/^ip[6]?tables -t mangle -F ${SCRIPTNAME_DISPLAY}/d; s/ip[6]?tables -t mangle -A ${SCRIPTNAME_DISPLAY} //g; s/[[:space:]]{2,}/ /g" "/tmp/${SCRIPTNAME}_iprules" printf "**************\n" appdb_debug="$(am_settings_get "${SCRIPTNAME}"_appdb)" printf "appdb rules: %s\n" "${appdb_debug:-Defaults}" true > "/tmp/${SCRIPTNAME}_tcrules" write_custom_qdisc write_appdb_rules write_custom_rates cat "/tmp/${SCRIPTNAME}_tcrules" Green "[/CODE][/SPOILER]" # Since these tmp files aren't being used to apply rules, we delete them to avoid confusion about the last known ruleset rm "/tmp/${SCRIPTNAME}_iprules" "/tmp/${SCRIPTNAME}_tcrules" printf "\n" Yellow "Copy the text from [SPOILER] to [/SPOILER] and paste into a forum post at snbforums.com" } # debug get_dscp_class() { # Map class destination field from webui settings to the established class/flowid based on user priorities # flowid will be one of 1:10 - 1:17, depending on the user priority sequencing in the QoS GUI # Input: numeric class destination from iptables rule local dscp case "${1}" in 0) dscp="${Net_DSCP}" ;; 1) dscp="${Gaming_DSCP}" ;; 2) dscp="${Streaming_DSCP}" ;; 3) dscp="${Work_DSCP}" ;; 4) dscp="${Web_DSCP}" ;; 5) dscp="${Downloads_DSCP}" ;; 6) dscp="${Others_DSCP}" ;; 7) dscp="${Learn_DSCP}" ;; # return empty if destination missing *) dscp="CS0" ;; esac printf "%s\n" "${dscp}" } # get_dscp_class get_flowid() { # Map class destination field from webui settings to the established class/flowid based on user priorities # flowid will be one of 1:10 - 1:17, depending on the user priority sequencing in the QoS GUI # Input: numeric class destination from iptables rule local flowid case "${1}" in 0) flowid="${Net_flow}" ;; 1) flowid="${Gaming_flow}" ;; 2) flowid="${Streaming_flow}" ;; 3) flowid="${Work_flow}" ;; 4) flowid="${Web_flow}" ;; 5) flowid="${Downloads_flow}" ;; 6) flowid="${Others_flow}" ;; 7) flowid="${Learn_flow}" ;; # return empty if destination missing *) flowid="" ;; esac printf "%s\n" "${flowid}" } # get_flowid Is_Valid_CIDR() { /bin/grep -qE '^[!]?([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$' } # Is_Valid_CIDR Is_Valid_Port() { /bin/grep -qE '^[!]?([0-9]{1,5})((:[0-9]{1,5})?|(,[0-9]{1,5})*)$' } # Is_Valid_Port Is_Valid_Mark() { /bin/grep -qE '^[!]?[A-Fa-f0-9]{2}([A-Fa-f0-9]{4}|[\*]{4})$' } # Is_Valid_Mark parse_appdb_rule_cake() { # Process an appdb custom rule into the appropriate tc filter syntax # Input: $1 = Mark from appdb rule XXYYYY XX=Category(hex) YYYY=ID(hex or ****) # $2 = Class destination # Output: stdout is written directly to the /tmp/flexqos_iptables_rules file via redirect in write_appdb_rules(), # so don't add unnecessary output in this function. local cat id local mark local dscp # Only process if Mark is a valid format if echo "${1}" | Is_Valid_Mark; then # Extract category and appid from mark cat="$(echo "${1}" | cut -c 1-2)" id="$(echo "${1}" | cut -c 3-6)" # check if wildcard mark if [ "${id}" = "****" ]; then # Replace asterisks with zeros and use category mask # This mark and mask mark="0x${cat}0000/0x3f0000" appid="$(printf '%d,' 0x${cat})" appname="$( grep ^${appid} /tmp/bwdpi/bwdpi.cat.db | cut -d, -f2 )" elif [ "${1}" = "000000" ]; then # unidentified traffic needs a special mask mark="0x${1}/0x00ffff" appname="Untracked" else # specific application mark mark="0x${1}/0x3fffff" appid="$(printf '%d,%d,' 0x${cat} 0x${id})" appname="$( grep ^${appid} /tmp/bwdpi/bwdpi.app.db | cut -d, -f4 )" fi # get destination dscp class dscp="$(get_dscp_class "${2}")" printf "iptables -t mangle -A %s_AppDB -m mark --mark %s -j DSCP --set-dscp-class %s -m comment --comment \"%s\"\n" "${SCRIPTNAME_DISPLAY}" "${mark}" "${dscp}" "${appname}" printf "iptables -t mangle -A %s_AppDB -m mark --mark %s -j RETURN\n" "${SCRIPTNAME_DISPLAY}" "${mark}" if [ "${IPv6_enabled}" != "disabled" ]; then printf "ip6tables -t mangle -A %s_AppDB -m mark --mark %s -j DSCP --set-dscp-class %s -m comment --comment \"%s\"\n" "${SCRIPTNAME_DISPLAY}" "${mark}" "${dscp}" "${appname}" printf "ip6tables -t mangle -A %s_AppDB -m mark --mark %s -j RETURN\n" "${SCRIPTNAME_DISPLAY}" "${mark}" fi fi # Is_Valid_Mark } # parse_appdb_rule_cake parse_appdb_rule() { # Process an appdb custom rule into the appropriate tc filter syntax # Input: $1 = Mark from appdb rule XXYYYY XX=Category(hex) YYYY=ID(hex or ****) # $2 = Class destination # Output: stdout is written directly to the /tmp/flexqos_appdb_rules file via redirect in write_appdb_rules(), # so don't add unnecessary output in this function. local cat id local DOWN_mark UP_mark local flowid local currmask local prio currprio local currhandledown currhandleup # Only process if Mark is a valid format if echo "${1}" | Is_Valid_Mark; then # Extract category and appid from mark cat="$(echo "${1}" | cut -c 1-2)" id="$(echo "${1}" | cut -c 3-6)" # check if wildcard mark if [ "${id}" = "****" ]; then # Replace asterisks with zeros and use category mask # This mark and mask DOWN_mark="0x80${cat}0000 0xc03f0000" UP_mark="0x40${cat}0000 0xc03f0000" elif [ "${1}" = "000000" ]; then # unidentified traffic needs a special mask DOWN_mark="0x80${1} 0xc000ffff" UP_mark="0x40${1} 0xc000ffff" else # specific application mark DOWN_mark="0x80${1} 0xc03fffff" UP_mark="0x40${1} 0xc03fffff" fi # get destination class flowid="$(get_flowid "${2}")" # To override the default tc filters with our custom filter rules, we need to insert our rules # at a higher priority (lower number) than the built-in filter for each category. if [ "${1}" = "000000" ]; then # special mask for unidentified traffic currmask="0xc000ffff" else currmask="0xc03f0000" fi # search the tc filter temp file we made in write_appdb_rules() for the existing priority of the # category we are going to override with a custom appdb filter rule. # e.g. If we are going to make a rule for appdb mark 1400C5, we need to find the current priority of category 14. prio="$(/bin/grep -i -m 1 -B1 "0x80${cat}0000 ${currmask}" "/tmp/${SCRIPTNAME}_tmp_tcfilterdown" | sed -nE 's/.* pref ([0-9]+) .*/\1/p')" currprio="${prio}" # If there is no existing filter for the category, use the undf_prio defined in set_tc_variables(). # This is usually only necessary for Untracked traffic (mark 000000). # Otherwise, take the current priority and subtract 1 so that our rule will be processed earlier than the default rule. if [ -z "${prio}" ]; then prio="${undf_prio}" else prio="$((prio-1))" fi # Build and echo the tc filter commands based on the possible actions required: # 1. Change an existing filter to point to a new flowid (mostly relevant for wildcard appdb rules). # 2. Insert a new filter at a higher priority than the existing filter that would otherwise match this mark. if { [ "${id}" = "****" ] || [ "${1}" = "000000" ]; } && [ -n "${currprio}" ]; then # change existing rule for wildcard marks and Untracked mark only if current priority already determined. # Need to get handle of existing filter for proper tc filter change syntax. currhandledown="$(/bin/grep -i -m 1 -B1 "0x80${cat}0000 ${currmask}" "/tmp/${SCRIPTNAME}_tmp_tcfilterdown" | sed -nE 's/.* fh ([0-9a-f:]+) .*/\1/p')" currhandleup="$(/bin/grep -i -m 1 -B1 "0x40${cat}0000 ${currmask}" "/tmp/${SCRIPTNAME}_tmp_tcfilterup" | sed -nE 's/.* fh ([0-9a-f:]+) .*/\1/p')" printf "filter change dev %s prio %s protocol all handle %s u32 flowid %s\n" "${tclan}" "${currprio}" "${currhandledown}" "${flowid}" printf "filter change dev %s prio %s protocol all handle %s u32 flowid %s\n" "${tcwan}" "${currprio}" "${currhandleup}" "${flowid}" else # add new rule for individual app one priority level higher (-1) printf "filter add dev %s protocol all prio %s u32 match mark %s flowid %s\n" "${tclan}" "${prio}" "${DOWN_mark}" "${flowid}" printf "filter add dev %s protocol all prio %s u32 match mark %s flowid %s\n" "${tcwan}" "${prio}" "${UP_mark}" "${flowid}" fi fi # Is_Valid_Mark } # parse_appdb_rule create_ipset() { # To translate IPv4 iptables rules using local IPv4 addresses, create 2 ipsets and 2 iptables rules to track # corresponding IPv6 addresses for a given IPv4 local address # Input: $1 = local IP/CIDR (minus optional negation) # Output: stdout ipset and iptables commands local LOCALIP IPV6LIFETIME IPV6RASTATE # If IPv6 is disabled, return early [ "${IPv6_enabled}" = "disabled" ] && return # Strip optional negation if present LOCALIP="${1}" IPV6RASTATE="$(nvram get ipv6_autoconf_type)" # 0=Stateless, 1=Stateful ipset -! create "${LOCALIP}-mac" hash:mac timeout "$(nvram get dhcp_lease)" 2>/dev/null ipset -! flush "${LOCALIP}-mac" 2>/dev/null case "${IPv6_enabled}" in dhcp6|other) #Native or Static if [ "${IPV6RASTATE}" = "1" ]; then # Stateful, get DHCP Lifetime IPV6LIFETIME="$(nvram get ipv6_dhcp_lifetime)" else # Stateless, use hard-coded value from firmware IPV6LIFETIME=600 fi ;; *) IPV6LIFETIME=600 ;; esac ipset -! create "${LOCALIP}" hash:ip family inet6 timeout "${IPV6LIFETIME}" 2>/dev/null ipset -! flush "${LOCALIP}" 2>/dev/null printf "iptables -t mangle -D PREROUTING -i %s -m conntrack --ctstate NEW -s %s -j SET --add-set %s-mac src --exist 2>/dev/null\n" "${lan}" "${LOCALIP}" "${LOCALIP}" printf "ip6tables -t mangle -D PREROUTING -i %s -m conntrack --ctstate NEW -m set --match-set %s-mac src -j SET --add-set %s src --exist 2>/dev/null\n" "${lan}" "${LOCALIP}" "${LOCALIP}" printf "iptables -t mangle -I PREROUTING -i %s -m conntrack --ctstate NEW -s %s -j SET --add-set %s-mac src --exist\n" "${lan}" "${LOCALIP}" "${LOCALIP}" printf "ip6tables -t mangle -I PREROUTING -i %s -m conntrack --ctstate NEW -m set --match-set %s-mac src -j SET --add-set %s src --exist\n" "${lan}" "${LOCALIP}" "${LOCALIP}" } parse_iptablerule() { # Process an iptables custom rule into the appropriate iptables syntax # Input: $1 = local IP (e.g. 192.168.1.100 !192.168.1.100 192.168.1.100/31 !192.168.1.100/31) # $2 = remote IP (e.g. 9.9.9.9 !9.9.9.9 9.9.9.0/24 !9.9.9.0/24) # $3 = protocol (e.g. both, tcp, or udp) # $4 = local port (e.g. 443 !443 1234:5678 !1234:5678 53,123,853 !53,123,853) # $5 = remote port (e.g. 443 !443 1234:5678 !1234:5678 53,123,853 !53,123,853) # $6 = mark (e.g. XXYYYY !XXYYYY XX=Category(hex) YYYY=ID(hex or ****)) # $7 = class destination (e.g. 0-7) # Output: stdout is written directly to the /tmp/flexqos_iprules file via redirect in write_iptables_rules(), # so don't add unnecessary output in this function. local DOWN_Lip UP_Lip CIDR local DOWN_Lip6 UP_Lip6 local DOWN_Rip UP_Rip local PROTOS proto local DOWN_Lport UP_Lport local DOWN_Rport UP_Rport local tmpMark DOWN_mark UP_mark local DOWN_dst UP_dst Dst_mark # local IP # Check for acceptable IP format if echo "${1}" | Is_Valid_CIDR; then # print ! (if present) and remaining CIDR DOWN_Lip="$(echo "${1}" | sed -E 's/^([!])?/\1 -d /')" UP_Lip="$(echo "${1}" | sed -E 's/^([!])?/\1 -s /')" # Only create ipset if there is no remote IP/CIDR defined, since the IPv6 rule would not work with remote IPv4 CIDR if ! echo "${2}" | Is_Valid_CIDR; then # Alternate syntax for IPv6 ipset matching CIDR="$(echo "${1}" | sed -E 's/^!//')" create_ipset "${CIDR}" # 2>/dev/null DOWN_Lip6="$(echo "${1}" | sed -E 's/^([!])?(([0-9]{1,3}\.){3}[0-9]{1,3}(\/[0-9]{1,2})?)/-m set \1 --match-set \2 dst/')" UP_Lip6="$(echo "${1}" | sed -E 's/^([!])?(([0-9]{1,3}\.){3}[0-9]{1,3}(\/[0-9]{1,2})?)/-m set \1 --match-set \2 src/')" fi else DOWN_Lip="" UP_Lip="" DOWN_Lip6="" UP_Lip6="" fi # remote IP # Check for acceptable IP format if echo "${2}" | Is_Valid_CIDR; then # print ! (if present) and remaining CIDR DOWN_Rip="$(echo "${2}" | sed -E 's/^([!])?/\1 -s /')" UP_Rip="$(echo "${2}" | sed -E 's/^([!])?/\1 -d /')" else DOWN_Rip="" UP_Rip="" fi # protocol (required when port specified) if [ "${3}" = "tcp" ] || [ "${3}" = "udp" ]; then # print protocol directly PROTOS="${3}" elif [ "${#4}" -gt "1" ] || [ "${#5}" -gt "1" ]; then # proto=both & ports are defined PROTOS="tcp>udp" # separated by > because IFS will be temporarily set to '>' by calling function. TODO Fix Me else # neither proto nor ports defined PROTOS="all" fi # local port if echo "${4}" | Is_Valid_Port; then # Use multiport to specify any port specification: # single port, multiple ports, port range DOWN_Lport="-m multiport $(echo "${4}" | sed -E 's/^([!])?/\1 --dports /')" UP_Lport="-m multiport $(echo "${4}" | sed -E 's/^([!])?/\1 --sports /')" else DOWN_Lport="" UP_Lport="" fi # remote port if echo "${5}" | Is_Valid_Port; then # Use multiport to specify any port specification: # single port, multiple ports, port range DOWN_Rport="-m multiport $(echo "${5}" | sed -E 's/^([!])?/\1 --sports /')" UP_Rport="-m multiport $(echo "${5}" | sed -E 's/^([!])?/\1 --dports /')" else DOWN_Rport="" UP_Rport="" fi # mark if echo "${6}" | Is_Valid_Mark; then tmpMark="${6}" # Use a tmp variable since we have to manipulate the contents for ! and **** DOWN_mark="-m mark" UP_mark="-m mark" if [ "$(echo "${tmpMark}" | cut -c 1)" = "!" ]; then # first char is ! DOWN_mark="${DOWN_mark} !" UP_mark="${UP_mark} !" tmpMark="$(echo "${tmpMark}" | sed -E 's/^!//')" # strip the ! fi # Extract category and appid from mark cat="$(echo "${tmpMark}" | cut -c 1-2)" id="$(echo "${tmpMark}" | cut -c 3-6)" # check if wildcard mark if [ "${id}" = "****" ]; then # replace **** with 0000 and use category mask DOWN_mark="${DOWN_mark} --mark 0x80${cat}0000/0xc03f0000" UP_mark="${UP_mark} --mark 0x40${cat}0000/0xc03f0000" else DOWN_mark="${DOWN_mark} --mark 0x80${tmpMark}/0xc03fffff" UP_mark="${UP_mark} --mark 0x40${tmpMark}/0xc03fffff" fi else DOWN_mark="" UP_mark="" fi # if all parameters are empty stop processing the rule if [ -z "${DOWN_Lip}${DOWN_Rip}${DOWN_Lport}${DOWN_Rport}${DOWN_mark}" ]; then return fi # destination mark # numbers come from webui select options for class field Dst_mark="$(get_class_mark "${7}")" if [ -z "${Dst_mark}" ]; then return fi DOWN_dst="-j MARK --set-mark 0x80${Dst_mark}ffff/0xc03fffff" UP_dst="-j MARK --set-mark 0x40${Dst_mark}ffff/0xc03fffff" # This block is redirected to the /tmp/flexqos_iprules file, so no extraneous output, please # If proto=both we have to create 2 statements, one for tcp and one for udp. for proto in ${PROTOS}; do # download ipv4 printf "iptables -t mangle -A %s -o %s %s %s -p %s %s %s %s %s\n" "${SCRIPTNAME_DISPLAY}" "${lan}" "${DOWN_Lip}" "${DOWN_Rip}" "${proto}" "${DOWN_Lport}" "${DOWN_Rport}" "${DOWN_mark}" "${DOWN_dst}" # upload ipv4 printf "iptables -t mangle -A %s -o %s %s %s -p %s %s %s %s %s\n" "${SCRIPTNAME_DISPLAY}" "${wan}" "${UP_Lip}" "${UP_Rip}" "${proto}" "${UP_Lport}" "${UP_Rport}" "${UP_mark}" "${UP_dst}" # If rule contains no IPv4 remote addresses, and IPv6 is enabled, add a corresponding rule for IPv6 if [ "${IPv6_enabled}" != "disabled" ] && [ -z "${DOWN_Rip}" ]; then # download ipv6 printf "ip6tables -t mangle -A %s -o %s %s -p %s %s %s %s %s\n" "${SCRIPTNAME_DISPLAY}" "${lan}" "${DOWN_Lip6}" "${proto}" "${DOWN_Lport}" "${DOWN_Rport}" "${DOWN_mark}" "${DOWN_dst}" # upload ipv6 printf "ip6tables -t mangle -A %s -o %s %s -p %s %s %s %s %s\n" "${SCRIPTNAME_DISPLAY}" "${wan}" "${UP_Lip6}" "${proto}" "${UP_Lport}" "${UP_Rport}" "${UP_mark}" "${UP_dst}" fi done } # parse_iptablerule about() { scriptinfo cat <<EOF License ${SCRIPTNAME_DISPLAY} is free to use under the GNU General Public License, version 3 (GPL-3.0). https://opensource.org/licenses/GPL-3.0 For discussion visit this thread: https://www.snbforums.com/forums/asuswrt-merlin-addons.60/?prefix_id=8 https://github.com/dave14305/FlexQoS (Source Code) About Script Changes Unidentified traffic destination away from Work-From-Home into Others Script Changes HTTPS traffic destination away from Net Control into Web Surfing Script Changes Guaranteed Bandwidth per QoS category into logical percentages of upload and download. Script includes misc default rules (Wifi Calling) - UDP traffic on remote ports 500 & 4500 moved into Work-From-Home (Facetime) - UDP traffic on local ports 16384 - 16415 moved into Work-From-Home (Usenet) - TCP traffic on remote ports 119 & 563 moved into File Transfers (Gaming) - Gaming TCP traffic from remote ports 80 & 443 moved into File Transfers. (Snapchat) - Moved into Others (Speedtest.net) - Moved into File Transfers (Google Play) - Moved into File Transfers (Apple AppStore)- Moved into File Transfers (VPN Fix) - Router VPN Client upload traffic moved into File Transfers instead of whitelisted (Gaming Manual) - Unidentified traffic for specified devices, not originating from ports 80/443, moved into Gaming Gaming Rule Note Gaming traffic originating from ports 80 & 443 is primarily downloads & patches (some lobby/login protocols mixed within) Manually configurable rule will take untracked traffic for specified devices, not originating from server ports 80/443, and place it into Gaming Use of this gaming rule REQUIRES devices to have a continuous static ip assignment & this range needs to be passed into the script EOF } backup() { # Backup existing user rules in /jffs/addons/custom_settings.txt # Input: create [force]|restore|remove case "${1}" in 'create') if [ "${2}" != "force" ] && [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ]; then grep "# Backup date" "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" printf "A backup already exists. Do you want to overwrite this backup? [1=Yes 2=No]: " read -r yn if [ "${yn}" != "1" ]; then Yellow "Backup cancelled." return fi fi printf "Running backup...\n" { printf "#!/bin/sh\n" printf "# Backup date: %s\n" "$(date +'%Y-%m-%d %H:%M:%S%z')" printf ". /usr/sbin/helper.sh\n" [ -n "$(am_settings_get "${SCRIPTNAME}"_iptables)" ] && printf "am_settings_set %s_iptables \"%s\"\n" "${SCRIPTNAME}" "$(am_settings_get "${SCRIPTNAME}"_iptables)" [ -n "$(am_settings_get "${SCRIPTNAME}"_iptables_names)" ] && printf "am_settings_set %s_iptables_names \"%s\"\n" "${SCRIPTNAME}" "$(am_settings_get "${SCRIPTNAME}"_iptables_names)" [ -n "$(am_settings_get "${SCRIPTNAME}"_appdb)" ] && printf "am_settings_set %s_appdb \"%s\"\n" "${SCRIPTNAME}" "$(am_settings_get "${SCRIPTNAME}"_appdb)" [ -n "$(am_settings_get "${SCRIPTNAME}"_bwrates)" ] && printf "am_settings_set %s_bwrates \"%s\"\n" "${SCRIPTNAME}" "$(am_settings_get "${SCRIPTNAME}"_bwrates)" [ -n "$(am_settings_get "${SCRIPTNAME}"_qdisc)" ] && printf "am_settings_set %s_qdisc \"%s\"\n" "${SCRIPTNAME}" "$(am_settings_get "${SCRIPTNAME}"_qdisc)" } > "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" if /bin/grep -q "${SCRIPTNAME}_" "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh"; then Green "Backup done to ${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" else rm "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" Yellow "Backup cancelled. All settings using default values." fi ;; 'restore') if [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ]; then Yellow "$(grep "# Backup date" "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh")" printf "Do you want to restore this backup? [1=Yes 2=No]: " read -r yn if [ "${yn}" = "1" ]; then sh "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" Green "Backup restored!" needrestart=1 else Yellow "Restore cancelled." fi else Red "No backup file exists!" fi ;; 'remove') [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ] && rm "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" Green "Backup deleted." ;; esac } # backup download_file() { # Download file from Github once to a temp location. If the same as the destination file, don't replace. # Otherwise move it from the temp location to the destination. if curl -fsL --retry 3 --connect-timeout 3 "${GIT_URL}/${1}" -o "/tmp/${1}"; then if [ "$(md5sum "/tmp/${1}" | awk '{print $1}')" != "$(md5sum "${2}" 2>/dev/null | awk '{print $1}')" ]; then mv -f "/tmp/${1}" "${2}" logmsg "Updated $(basename "${1}")" else logmsg "File $(basename "${2}") is already up-to-date" rm -f "/tmp/${1}" 2>/dev/null fi else logmsg "Updating $(basename "${1}") failed" fi } # download_file compare_remote_version() { # Check version on Github and determine the difference with the installed version # Outcomes: Version update, or no update local remotever # Fetch version of the shell script on Github remotever="$(curl -fsN --retry 3 --connect-timeout 3 "${GIT_URL}/$(basename "${SCRIPTPATH}")" | /bin/grep "^version=" | sed -e 's/version=//')" if [ "$( echo "${version}" | sed 's/\.//g' )" -lt "$( echo "${remotever}" | sed 's/\.//g' )" ]; then # strip the . from version string for numeric comparison # version upgrade echo "${remotever}" else printf "NoUpdate\n" fi } # compare_remote_version update() { # Check for, and optionally apply updates. # Parameter options: check (do not update), silent (update without prompting) local updatestatus yn scriptinfo printf "Checking for updates\n" # Update the webui status thorugh detect_update.js ajax call. printf "var verUpdateStatus = \"%s\";\n" "InProgress" > "/www/ext/${SCRIPTNAME}/detect_update.js" updatestatus="$(compare_remote_version)" # Check to make sure we got back a valid status from compare_remote_version(). If not, indicate Error. case "${updatestatus}" in 'NoUpdate'|[0-9].[0-9].[0-9]) ;; *) updatestatus="Error" esac printf "var verUpdateStatus = \"%s\";\n" "${updatestatus}" > "/www/ext/${SCRIPTNAME}/detect_update.js" if [ "${1}" = "check" ]; then # Do not proceed with any updating if check function requested return fi if [ "${mode}" = "interactive" ] && [ -z "${1}" ]; then case "${updatestatus}" in 'NoUpdate') Green " You have the latest version installed" printf " Would you like to overwrite your existing installation anyway? [1=Yes 2=No]: " ;; 'Error') Red " Error determining remote version status!" PressEnter return ;; *) # New Version Number Green " ${SCRIPTNAME_DISPLAY} v${updatestatus} is now available!" printf " Would you like to update now? [1=Yes 2=No]: " ;; esac read -r yn printf "\n" if [ "${yn}" != "1" ]; then Green " No Changes have been made" return 0 fi fi printf "Installing: %s...\n\n" "${SCRIPTNAME_DISPLAY}" download_file "$(basename "${SCRIPTPATH}")" "${SCRIPTPATH}" exec sh "${SCRIPTPATH}" -install "${1}" exit } # update prompt_restart() { # Restart QoS so that FlexQoS changes can take effect. # Possible values for $needrestart: # 0: No restart needed (initialized in main) # 1: Restart needed, but prompt user if interactive session # 2: Restart needed, do not prompt (force) local yn if [ "${needrestart}" -gt "0" ]; then if [ "${mode}" = "interactive" ]; then if [ "${needrestart}" = "1" ]; then printf "\nWould you like to restart QoS for modifications to take effect? [1=Yes 2=No]: " read -r yn if [ "${yn}" = "2" ]; then needrestart=0 return fi fi fi printf "Restarting QoS and firewall...\n" service "restart_qos;restart_firewall" needrestart=0 fi } # prompt_restart menu() { # Minimal interactive, menu-driven interface for basic maintenance functions. local yn [ "${mode}" = "interactive" ] || return clear sed -n '2,10p' "${0}" # display banner scriptinfo printf " (1) about explain functionality\n" printf " (2) update check for updates\n" printf " (3) debug traffic control parameters\n" printf " (4) restart restart QoS\n" printf " (5) backup create settings backup\n" if [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ]; then printf " (6) restore restore settings from backup\n" printf " (7) delete remove backup\n" fi printf "\n (9) uninstall uninstall script\n" printf " (e) exit\n" printf "\nMake a selection: " read -r input case "${input}" in '1') about ;; '2') update ;; '3') debug ;; '4') needrestart=1 prompt_restart ;; '5') backup create ;; '6') if [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ]; then backup restore else Red "$input is not a valid option!" fi ;; '7') if [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ]; then backup remove else Red "$input is not a valid option!" fi ;; '9') scriptinfo printf " Do you want to uninstall %s [1=Yes 2=No]: " "${SCRIPTNAME_DISPLAY}" read -r yn if [ "${yn}" = "1" ]; then printf "\n" sh "${SCRIPTPATH}" -uninstall printf "\n" exit fi printf "\n" Yellow "${SCRIPTNAME_DISPLAY} has NOT been uninstalled" ;; 'e'|'E'|'exit') return ;; *) Red "$input is not a valid option!" ;; esac PressEnter menu # stay in the menu loop until exit is chosen } # menu remove_webui() { local prev_webui_page local FD printf "Removing WebUI...\n" prev_webui_page="$(sed -nE "s/^\{url\: \"(user[0-9]+\.asp)\"\, tabName\: \"${SCRIPTNAME_DISPLAY}\"\}\,$/\1/p" /tmp/menuTree.js 2>/dev/null)" if [ -n "${prev_webui_page}" ]; then # Remove page from the UI menu system FD=386 eval exec "${FD}>${LOCKFILE}" /usr/bin/flock -x "${FD}" umount /www/require/modules/menuTree.js 2>/dev/null sed -i "\~tabName: \"${SCRIPTNAME_DISPLAY}\"},~d" /tmp/menuTree.js if diff -q /tmp/menuTree.js /www/require/modules/menuTree.js >/dev/null 2>&1; then # no more custom pages mounted, so remove the file rm /tmp/menuTree.js else # Still some modifications from another script so remount mount -o bind /tmp/menuTree.js /www/require/modules/menuTree.js fi /usr/bin/flock -u "${FD}" # Remove last mounted asp page rm -f "/www/user/${prev_webui_page}" 2>/dev/null # Look for previously mounted asp pages that are orphaned now and delete them /bin/grep -l "${SCRIPTNAME_DISPLAY} maintained by dave14305" /www/user/user*.asp 2>/dev/null | while read -r oldfile do rm "${oldfile}" done fi rm -rf "/www/ext/${SCRIPTNAME}" 2>/dev/null # remove js helper scripts } # remove_webui install_webui() { local prev_webui_page local FD # if this is an install or update...otherwise it's a normal startup/mount if [ -z "${1}" ]; then printf "Downloading WebUI files...\n" download_file "$(basename "${WEBUIPATH}")" "${WEBUIPATH}" # cleanup obsolete files from previous versions [ -L "/www/ext/${SCRIPTNAME}" ] && rm "/www/ext/${SCRIPTNAME}" 2>/dev/null [ -d "${ADDON_DIR}/table" ] && rm -r "${ADDON_DIR}/table" [ -f "${ADDON_DIR}/${SCRIPTNAME}_arrays.js" ] && rm "${ADDON_DIR}/${SCRIPTNAME}_arrays.js" fi FD=386 eval exec "${FD}>${LOCKFILE}" /usr/bin/flock -x "${FD}" # Check if the webpage is already mounted in the GUI and reuse that page prev_webui_page="$(sed -nE "s/^\{url\: \"(user[0-9]+\.asp)\"\, tabName\: \"${SCRIPTNAME_DISPLAY}\"\}\,$/\1/p" /tmp/menuTree.js 2>/dev/null)" if [ -n "${prev_webui_page}" ]; then # use the same filename as before am_webui_page="${prev_webui_page}" else # get a new mountpoint am_get_webui_page "${WEBUIPATH}" fi if [ "${am_webui_page}" = "none" ]; then logmsg "No API slots available to install web page" else cp -p "${WEBUIPATH}" "/www/user/${am_webui_page}" if [ ! -f /tmp/menuTree.js ]; then cp /www/require/modules/menuTree.js /tmp/ mount -o bind /tmp/menuTree.js /www/require/modules/menuTree.js fi if ! /bin/grep -q "{url: \"$am_webui_page\", tabName: \"${SCRIPTNAME_DISPLAY}\"}," /tmp/menuTree.js; then umount /www/require/modules/menuTree.js 2>/dev/null sed -i "\~{url: \"$am_webui_page\"~d; \~tabName: \"${SCRIPTNAME_DISPLAY}\"},~d" /tmp/menuTree.js sed -i "/url: \"QoS_Stats.asp\", tabName:/i {url: \"$am_webui_page\", tabName: \"${SCRIPTNAME_DISPLAY}\"}," /tmp/menuTree.js mount -o bind /tmp/menuTree.js /www/require/modules/menuTree.js fi fi /usr/bin/flock -u "${FD}" [ ! -d "/www/ext/${SCRIPTNAME}" ] && mkdir -p "/www/ext/${SCRIPTNAME}" } Init_UserScript() { # Properly setup an empty Merlin user script local userscript if [ -z "${1}" ]; then return fi userscript="/jffs/scripts/$1" if [ ! -f "${userscript}" ]; then # If script doesn't exist yet, create with shebang printf "#!/bin/sh\n\n" > "${userscript}" elif [ -f "${userscript}" ] && ! head -1 "${userscript}" | /bin/grep -qE "^#!/bin/sh"; then # Script exists but no shebang, so insert it at line 1 sed -i '1s~^~#!/bin/sh\n~' "${userscript}" elif [ "$(tail -c1 "${userscript}" | wc -l)" = "0" ]; then # Script exists with shebang, but no linefeed before EOF; makes appending content unpredictable if missing printf "\n" >> "${userscript}" fi if [ ! -x "${userscript}" ]; then # Ensure script is executable by owner chmod 755 "${userscript}" fi } # Init_UserScript Auto_ServiceEventEnd() { # Borrowed from Adamm00 # https://github.com/Adamm00/IPSet_ASUS/blob/master/firewall.sh local cmdline Init_UserScript "service-event-end" # Delete existing lines related to this script sed -i "\~${SCRIPTNAME_DISPLAY} Addition~d" /jffs/scripts/service-event-end # Add line to handle wrs and sig_check events that require reapplying settings cmdline="if [ \"\$2\" = \"wrs\" ] || [ \"\$2\" = \"sig_check\" ]; then { sh ${SCRIPTPATH} -start & } ; fi # ${SCRIPTNAME_DISPLAY} Addition" echo "${cmdline}" >> /jffs/scripts/service-event-end # Add line to handle other events triggered from webui cmdline="if echo \"\$2\" | /bin/grep -q \"^${SCRIPTNAME}\"; then { sh ${SCRIPTPATH} \"\${2#${SCRIPTNAME}}\" & } ; fi # ${SCRIPTNAME_DISPLAY} Addition" echo "${cmdline}" >> /jffs/scripts/service-event-end } # Auto_ServiceEventEnd Auto_FirewallStart() { # Borrowed from Adamm00 # https://github.com/Adamm00/IPSet_ASUS/blob/master/firewall.sh local cmdline Init_UserScript "firewall-start" # Delete existing lines related to this script sed -i "\~${SCRIPTNAME_DISPLAY} Addition~d" /jffs/scripts/firewall-start # Add line to trigger script on firewall startup cmdline="sh ${SCRIPTPATH} -start & # ${SCRIPTNAME_DISPLAY} Addition" if /bin/grep -vE "^#" /jffs/scripts/firewall-start | /bin/grep -q "Skynet"; then # If Skynet also installed, insert this script before Skynet so it doesn't have to wait for Skynet to startup before applying QoS # Won't delay Skynet startup since we fork into the background sed -i "/Skynet/i ${cmdline}" /jffs/scripts/firewall-start else # Skynet not installed, so just append echo "${cmdline}" >> /jffs/scripts/firewall-start fi # is Skynet also installed? } # Auto_FirewallStart setup_aliases() { # shortcuts to launching script local cmdline if [ -d /opt/bin ]; then # Entware is installed, so setup link to /opt/bin printf "Adding %s link in Entware /opt/bin...\n" "${SCRIPTNAME}" ln -sf "${SCRIPTPATH}" "/opt/bin/${SCRIPTNAME}" else # Setup shell alias printf "Adding %s alias in profile.add...\n" "${SCRIPTNAME}" sed -i "/alias ${SCRIPTNAME}/d" /jffs/configs/profile.add 2>/dev/null cmdline="alias ${SCRIPTNAME}=\"sh ${SCRIPTPATH}\" # ${SCRIPTNAME_DISPLAY} Addition" echo "${cmdline}" >> /jffs/configs/profile.add fi } # setup_aliases Firmware_Check() { printf "Checking firmware support...\n" if ! nvram get rc_support | grep -q am_addons; then Red "${SCRIPTNAME_DISPLAY} requires ASUSWRT-Merlin Addon API support. Installation aborted." printf "\nInstall FreshJR_QOS via amtm as an alternative for your firmware version.\n" return 1 fi if [ "$(nvram get qos_enable)" != "1" ] || [ "$(nvram get qos_type)" != "1" ]; then Red "Adaptive QoS is not enabled. Please enable it in the GUI. Aborting installation." return 1 fi if [ "$(nvram get jffs2_scripts)" != "1" ]; then Red "\"Enable JFFS custom scripts and configs\" is not enabled. Please enable it in the GUI. Aborting installation." return 1 fi } # Firmware_Check install() { # Install script and download webui file # This is also called by the update process once a new script is downlaoded by update() function if [ "${mode}" = "interactive" ]; then clear scriptinfo printf "Installing %s...\n" "${SCRIPTNAME_DISPLAY}" if ! Firmware_Check; then PressEnter rm -f "${0}" 2>/dev/null exit 5 fi fi if [ ! -d "${ADDON_DIR}" ]; then printf "Creating directories...\n" mkdir -p "${ADDON_DIR}" chmod 755 "${ADDON_DIR}" fi if [ ! -f "${SCRIPTPATH}" ]; then cp -p "${0}" "${SCRIPTPATH}" fi if [ ! -x "${SCRIPTPATH}" ]; then chmod 755 "${SCRIPTPATH}" fi install_webui generate_bwdpi_arrays force printf "Adding %s entries to Merlin user scripts...\n" "${SCRIPTNAME_DISPLAY}" Auto_FirewallStart Auto_ServiceEventEnd setup_aliases if [ "${mode}" = "interactive" ]; then Green "${SCRIPTNAME_DISPLAY} installation complete!" scriptinfo webconfigpage if [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ] && ! /bin/grep -qE "^${SCRIPTNAME}_[^(ver )]" /jffs/addons/custom_settings.txt ; then Green "Backup found!" backup restore fi [ "$(nvram get qos_enable)" = "1" ] && needrestart=1 else [ "$(nvram get qos_enable)" = "1" ] && needrestart=2 fi # Remove setting if set to default value 1 (enabled) sed -i "/^${SCRIPTNAME}_conntrack 1/d" /jffs/addons/custom_settings.txt # Remove deprecated 3:30 AM cron job if exists sed -i "\~${SCRIPTNAME_DISPLAY}~d" /jffs/scripts/services-start 2>/dev/null cru d "${SCRIPTNAME}" 2>/dev/null } # install uninstall() { local yn printf "Removing entries from Merlin user scripts...\n" sed -i "\~${SCRIPTNAME_DISPLAY}~d" /jffs/scripts/firewall-start 2>/dev/null sed -i "\~${SCRIPTNAME_DISPLAY}~d" /jffs/scripts/service-event-end 2>/dev/null printf "Removing aliases and shortcuts...\n" sed -i "/alias ${SCRIPTNAME}/d" /jffs/configs/profile.add 2>/dev/null rm -f "/opt/bin/${SCRIPTNAME}" 2>/dev/null printf "Removing delayed cron job...\n" cru d "${SCRIPTNAME}_5min" 2>/dev/null remove_webui printf "Removing %s settings...\n" "${SCRIPTNAME_DISPLAY}" if [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ]; then printf "Backup found! Would you like to keep it? [1=Yes 2=No]: " read -r yn if [ "${yn}" = "2" ]; then printf "Deleting Backup...\n" rm "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" fi else printf "Do you want to backup your settings before uninstall? [1=Yes 2=No]: " read -r yn if [ "${yn}" = "1" ]; then printf "Backing up %s settings...\n" "${SCRIPTNAME_DISPLAY}" backup create force fi fi sed -i "/^${SCRIPTNAME}_/d" /jffs/addons/custom_settings.txt if [ -f "${ADDON_DIR}/restore_${SCRIPTNAME}_settings.sh" ]; then printf "Deleting %s folder contents except Backup file...\n" "${SCRIPTNAME_DISPLAY}" /usr/bin/find "${ADDON_DIR}" ! -name restore_"${SCRIPTNAME}"_settings.sh ! -exec test -d {} \; -a -exec rm {} + else printf "Deleting %s directory...\n" "${SCRIPTNAME_DISPLAY}" rm -rf "${ADDON_DIR}" fi Green "${SCRIPTNAME_DISPLAY} has been uninstalled" needrestart=1 } # uninstall get_config() { local iptables_rules_defined local drp0 drp1 drp2 drp3 drp4 drp5 drp6 drp7 local dcp0 dcp1 dcp2 dcp3 dcp4 dcp5 dcp6 dcp7 local urp0 urp1 urp2 urp3 urp4 urp5 urp6 urp7 local ucp0 ucp1 ucp2 ucp3 ucp4 ucp5 ucp6 ucp7 # Read settings from Addon API config file. If not defined, set default values iptables_rules="$(am_settings_get "${SCRIPTNAME}"_iptables)" if [ -z "${iptables_rules}" ]; then iptables_rules="<>>udp>>500,4500>>3<>>udp>16384:16415>>>3<>>tcp>>119,563>>5<>>tcp>>80,443>08****>5" fi appdb_rules="$(am_settings_get "${SCRIPTNAME}"_appdb)" if [ -z "${appdb_rules}" ]; then appdb_rules="<000000>6<00006B>6<0D0007>5<0D0086>5<0D00A0>5<12003F>4<13****>4<14****>4" fi bwrates="$(am_settings_get "${SCRIPTNAME}"_bwrates)" if [ -z "${bwrates}" ]; then # New settings not set if [ -z "$(am_settings_get "${SCRIPTNAME}"_bandwidth)" ]; then # Old settings not set either, so set the defaults bwrates="<5>15>30>20>10>5>10>5<100>100>100>100>100>100>100>100<5>15>10>20>10>5>30>5<100>100>100>100>100>100>100>100" else # Convert bandwidth to bwrates by reading existing values into the re-sorted order read -r \ drp0 drp1 drp2 drp3 drp4 drp5 drp6 drp7 \ dcp0 dcp1 dcp2 dcp3 dcp4 dcp5 dcp6 dcp7 \ urp0 urp1 urp2 urp3 urp4 urp5 urp6 urp7 \ ucp0 ucp1 ucp2 ucp3 ucp4 ucp5 ucp6 ucp7 \ <<EOF $(am_settings_get "${SCRIPTNAME}"_bandwidth | sed 's/^<//g;s/[<>]/ /g') EOF am_settings_set "${SCRIPTNAME}"_bwrates "<${drp0}>${drp2}>${drp5}>${drp1}>${drp4}>${drp7}>${drp3}>${drp6}<${dcp0}>${dcp2}>${dcp5}>${dcp1}>${dcp4}>${dcp7}>${dcp3}>${dcp6}<${urp0}>${urp2}>${urp5}>${urp1}>${urp4}>${urp7}>${urp3}>${urp6}<${ucp0}>${ucp2}>${ucp5}>${ucp1}>${ucp4}>${ucp7}>${ucp3}>${ucp6}" bwrates="<${drp0}>${drp2}>${drp5}>${drp1}>${drp4}>${drp7}>${drp3}>${drp6}<${dcp0}>${dcp2}>${dcp5}>${dcp1}>${dcp4}>${dcp7}>${dcp3}>${dcp6}<${urp0}>${urp2}>${urp5}>${urp1}>${urp4}>${urp7}>${urp3}>${urp6}<${ucp0}>${ucp2}>${ucp5}>${ucp1}>${ucp4}>${ucp7}>${ucp3}>${ucp6}" if [ "${bwrates}" != "<5>15>30>20>10>5>10>5<100>100>100>100>100>100>100>100<5>15>10>20>10>5>30>5<100>100>100>100>100>100>100>100" ]; then am_settings_set "${SCRIPTNAME}"_bwrates "<${drp0}>${drp2}>${drp5}>${drp1}>${drp4}>${drp7}>${drp3}>${drp6}<${dcp0}>${dcp2}>${dcp5}>${dcp1}>${dcp4}>${dcp7}>${dcp3}>${dcp6}<${urp0}>${urp2}>${urp5}>${urp1}>${urp4}>${urp7}>${urp3}>${urp6}<${ucp0}>${ucp2}>${ucp5}>${ucp1}>${ucp4}>${ucp7}>${ucp3}>${ucp6}" fi sed -i "/^${SCRIPTNAME}_bandwidth /d" /jffs/addons/custom_settings.txt fi fi } # get_config validate_iptables_rules() { # Basic check to ensure the number of rules present in the iptables chain matches the number of expected rules # Does not verify that the rules present match the rules in the config, since the config hasn't been parsed at this point. local iptables_rules_defined iptables_rules_expected iptables_rulespresent local fail fail=0 iptables_rules_defined="$(echo "${iptables_rules}" | sed 's/</\n/g' | /bin/grep -vc "^$")" iptables_rules_expected=$((iptables_rules_defined*2+1)) # 1 download and upload rule per user rule, plus 1 for chain definition iptables_rulespresent="$(iptables -t mangle -S ${SCRIPTNAME_DISPLAY} | wc -l)" # count rules in chain plus chain itself if [ "${iptables_rulespresent}" -lt "${iptables_rules_expected}" ]; then fail=1 fi if [ "${qdisc}" = "2" ]; then iptables_rulespresent="$(iptables -t mangle -S ${SCRIPTNAME_DISPLAY}_DSCP | wc -l)" # count rules in DSCP chain plus chain itself if [ "${iptables_rulespresent}" -lt "13" ]; then fail=1 fi iptables_rules_defined="$(echo "${appdb_rules}" | sed 's/</\n/g' | /bin/grep -vc "^$")" iptables_rules_expected=$((iptables_rules_defined*2+1)) # 1 set and 1 return rule per user rule, plus 1 for chain definition iptables_rulespresent="$(iptables -t mangle -S ${SCRIPTNAME_DISPLAY}_AppDB | wc -l)" # count rules in AppDB chain plus chain itself if [ "${iptables_rulespresent}" -lt "${iptables_rules_expected}" ]; then fail=1 fi fi return ${fail} } # validate_iptables_rules write_iptables_rules() { # loop through iptables rules and write an iptables command to a temporary file for later execution local OLDIFS local localip remoteip proto lport rport mark class true > "/tmp/${SCRIPTNAME}_iprules" OLDIFS="${IFS}" # Save existing field separator IFS=">" # Set custom field separator to match rule format # read the rules, 1 per line and break into separate fields echo "${iptables_rules}" | sed 's/</\n/g' | while read -r localip remoteip proto lport rport mark class do # Ensure at least one criteria field is populated if [ -n "${localip}${remoteip}${proto}${lport}${rport}${mark}" ]; then # Process the rule and the stdout containing the resulting rule gets saved to the temporary script file parse_iptablerule "${localip}" "${remoteip}" "${proto}" "${lport}" "${rport}" "${mark}" "${class}" >> "/tmp/${SCRIPTNAME}_iprules" 2>/dev/null fi done IFS="${OLDIFS}" # Restore saved field separator } # write_iptables_rules write_appdb_rules() { # Write the user appdb rules to the existing tcrules file created during write_appdb_static_rules() local OLDIFS local mark class # Save the current filter rules once to avoid repeated calls in parse_appdb_rule() to determine existing prios "${TC}" filter show dev "${tclan}" parent 1: > "/tmp/${SCRIPTNAME}_tmp_tcfilterdown" "${TC}" filter show dev "${tcwan}" parent 1: > "/tmp/${SCRIPTNAME}_tmp_tcfilterup" # loop through appdb rules and write a tc command to a temporary script file OLDIFS="${IFS}" # Save existing field separator IFS=">" # Set custom field separator to match rule format # read the rules, 1 per line and break into separate fields echo "${appdb_rules}" | sed 's/</\n/g' | while read -r mark class do # Ensure the appdb mark is populated if [ -n "${mark}" ]; then case "${qdisc}" in 2) parse_appdb_rule_cake "${mark}" "${class}" >> "/tmp/${SCRIPTNAME}_iprules" 2>/dev/null ;; *) parse_appdb_rule "${mark}" "${class}" >> "/tmp/${SCRIPTNAME}_tcrules" 2>/dev/null ;; esac fi done IFS="${OLDIFS}" # Restore old field separator } # write_appdb_rules get_fq_quantum() { local BANDWIDTH BANDWIDTH="${1}" if [ "${BANDWIDTH}" -lt "51200" ]; then printf "quantum 300\n" fi } # get_fq_quantum get_fq_target() { # Adapted from sqm-scripts adapt_target_to_slow_link() and adapt_interval_to_slow_link(). # https://github.com/tohojo/sqm-scripts/blob/master/src/functions.sh local BANDWIDTH local TARGET INTERVAL BANDWIDTH="${1}" # for ATM the worst case expansion including overhead seems to be 33 cells of 53 bytes each # MAX DELAY = 1000 * 1000 * 33 * 53 * 8 / 1000 max delay in microseconds at 1kbps TARGET=$(/usr/bin/awk -vBANDWIDTH="${BANDWIDTH}" 'BEGIN { print int( 1000 * 1000 * 33 * 53 * 8 / 1000 / BANDWIDTH ) }') if [ "${TARGET}" -gt "5000" ]; then # Increase interval by the same amount that target got increased INTERVAL=$(( (100 - 5) * 1000 + TARGET )) printf "target %sus interval %sus\n" "${TARGET}" "${INTERVAL}" fi } # get_fq_target check_nvram() { case ${qdisc} in 2) # CAKE if [ "$(nvram get runner_disable_force)" != "1" ]; then nvram set runner_disable_force=1 nvram set runner_disable=1 nvram commit runner disable fc config --hw-accel 0 2>/dev/null fi if [ "$(nvram get fc_disable_force)" != "1" ]; then nvram set fc_disable_force=1 nvram set fc_disable=1 nvram commit fc disable fc flush fi ;; *) # fq_codel if [ "$(nvram get runner_disable_force)" = "1" ]; then nvram unset runner_disable_force nvram set runner_disable=0 nvram commit runner enable fc config --hw-accel 1 2>/dev/null archerctl sysport_tm disable 2>/dev/null fi if [ "$(nvram get fc_disable_force)" = "1" ]; then nvram unset fc_disable_force nvram set fc_disable=0 nvram commit fc enable fc flush fi ;; esac } write_custom_qdisc() { local i case ${qdisc} in 1) # fq_codel check_nvram if [ "$(${TC} qdisc show dev ${tclan} parent 1: | grep -c fq_codel)" -lt "9" ]; then { printf "qdisc replace dev %s parent 1:2 handle 102: fq_codel limit 1000 noecn\n" "${tclan}" printf "qdisc replace dev %s parent 1:2 handle 102: fq_codel limit 1000 noecn\n" "${tcwan}" for i in 0 1 2 3 4 5 6 7 do printf "qdisc replace dev %s parent 1:1%s handle 11%s: fq_codel %s limit 1000 %s\n" "${tclan}" "${i}" "${i}" "$(get_fq_quantum "${DownCeil}")" "$(get_fq_target "${DownCeil}")" printf "qdisc replace dev %s parent 1:1%s handle 11%s: fq_codel %s limit 1000 %s noecn\n" "${tcwan}" "${i}" "${i}" "$(get_fq_quantum "${UpCeil}")" "$(get_fq_target "${UpCeil}")" done } >> "/tmp/${SCRIPTNAME}_tcrules" 2>/dev/null fi ;; 2) # CAKE check_nvram if [ "$(${TC} qdisc show dev ${tclan} parent 1:1 | grep -c cake)" -lt "1" ]; then { # Download printf "qdisc del dev %s root\n" "${tclan}" printf "qdisc add dev %s root handle 1: htb default 1\n" "${tclan}" printf "class add dev %s parent 1: classid 1:2 htb prio 0 rate 10Gbit ceil 10Gbit quantum 200000\n" "${tclan}" printf "filter add dev %s parent 1: protocol all prio 1 u32 match mark 0x0000 0xc0000000 flowid 1:2\n" "${tclan}" printf "class add dev %s parent 1: classid 1:1 htb prio 0 rate 10Gbit ceil 10Gbit quantum 200000\n" "${tclan}" printf "qdisc add dev %s parent 1:1 handle 110: cake bandwidth %skbit diffserv4 dual-dsthost ingress %s\n" "${tclan}" "${DownCeil}" "$(get_overhead)" # Upload printf "qdisc del dev %s root\n" "${tcwan}" printf "qdisc add dev %s root handle 120: cake bandwidth %skbit diffserv4 dual-srchost nat %s\n" "${tcwan}" "${UpCeil}" "$(get_overhead)" } >> "/tmp/${SCRIPTNAME}_tcrules" 2>/dev/null fi ;; esac } # write_custom_qdisc check_qos_tc() { # Check the status of the existing tc class and filter setup by stock Adaptive QoS before custom settings applied. # Only br0 interface is checked since we have not yet identified the tcwan interface name yet. dlclasscnt="$("${TC}" class show dev br0 parent 1: | /bin/grep -c "parent")" # should be 8 dlfiltercnt="$("${TC}" filter show dev br0 parent 1: | /bin/grep -cE "flowid 1:1[0-7] *$")" # should be 39 or 40 dlqdisccnt="$("${TC}" qdisc show dev br0 | /bin/grep -c " parent 1:1[0-7] ")" # should be 8 # Check class count, filter count, qdisc count, and tcwan interface name defined with an htb qdisc if [ "${dlclasscnt}" -lt "8" ] || [ "${dlfiltercnt}" -lt "39" ] || [ "${dlqdisccnt}" -lt "8" ] || [ -z "$("${TC}" qdisc ls | sed -n 's/qdisc htb.*dev \([^b][^r].*\) root.*/\1/p')" ]; then if [ "${qdisc}" = "2" ]; then # CAKE might already be active, so Adaptive QoS tc structures may not be present dlqdisccnt="$("${TC}" qdisc show dev br0 | /bin/grep -c " cake ")" # should be 1 if [ "${dlqdisccnt}" -eq 1 ]; then return 1 fi else return 0 fi else return 1 fi } # check_qos_tc validate_tc_rules() { # Check the existing tc filter rules against the user configuration. If any rule missing, force creation of all rules # Must run after set_tc_variables() to ensure flowid can be determined local OLDIFS filtermissing local mark class flowid { # print a list of existing filters in the format of an appdb rule for easy comparison. Write to tmp file "${TC}" filter show dev "${tclan}" parent 1: | sed -nE '/flowid/ { N; s/\n//g; s/.*flowid (1:1[0-7]).*mark 0x[48]0([0-9a-fA-F]{6}).*/<\2>\1/p }' "${TC}" filter show dev "${tcwan}" parent 1: | sed -nE '/flowid/ { N; s/\n//g; s/.*flowid (1:1[0-7]).*mark 0x[48]0([0-9a-fA-F]{6}).*/<\2>\1/p }' } > "/tmp/${SCRIPTNAME}_checktcrules" 2>/dev/null OLDIFS="${IFS}" IFS=">" filtermissing="0" while read -r mark class do if [ -n "${mark}" ]; then flowid="$(get_flowid "${class}")" mark="$(echo "${mark}" | sed 's/\*/0/g')" if [ "$(/bin/grep -ic "<${mark}>${flowid}" "/tmp/${SCRIPTNAME}_checktcrules")" -lt "2" ]; then filtermissing="$((filtermissing+1))" break # stop checking after the first missing rule is identified fi fi done <<EOF $(echo "${appdb_rules}" | sed 's/</\n/g') EOF IFS="${OLDIFS}" if [ "${filtermissing}" -gt "0" ]; then # reapply tc rules return 1 else rm "/tmp/${SCRIPTNAME}_checktcrules" 2>/dev/null return 0 fi } # validate_tc_rules schedule_check_job() { # Schedule check for 5 minutes after startup to ensure no qos tc resets cru a "${SCRIPTNAME}"_5min "$(/bin/date -D '%s' +'%M %H %d %m %a' -d $(($(/bin/date +%s)+300))) ${SCRIPTPATH} -check" } # schedule_check_job get_cake_stats(){ local wanif refresh refresh="$(am_settings_get flexqos_refresh)" refresh="${refresh:=3}" # default to 3 seconds if unset wanif="$(tc qdisc ls | grep -v br0 | grep cake | cut -d' ' -f5)" while true; do [ -z "$(nvram get login_timestamp)" ] && break STATS_TIME="$(/bin/date +%s)" STATS_UPLOAD="$(tc -s -j qdisc show dev ${wanif} root 2>/dev/null)" STATS_DOWNLOAD="$(tc -s -j qdisc show dev br0 parent 1:1 2>/dev/null)" [ -z "$STATS_UPLOAD" ] && STATS_UPLOAD='[]' [ -z "$STATS_DOWNLOAD" ] && STATS_DOWNLOAD='[]' printf "var tcdata_wan_array=%s;\nvar tcdata_lan_array=%s;\nvar cake_statstime=%d;\n<%% bwdpi_conntrack(); %%>;\n" "$STATS_UPLOAD" "$STATS_DOWNLOAD" "$STATS_TIME" > /www/ext/${SCRIPTNAME}/ajax_gettcdata.asp sleep ${refresh} done & } startup() { local sleepdelay if [ "$(nvram get qos_enable)" != "1" ] || [ "$(nvram get qos_type)" != "1" ]; then logmsg "Adaptive QoS is not enabled. Skipping ${SCRIPTNAME_DISPLAY} startup." return 1 fi # adaptive qos not enabled if [ "${qdisc}" = "2" ] && ! nvram get rc_support | tr ' ' '\n' | grep -q "^cake$"; then logmsg "CAKE is not available in this firmware version. Reverting to fq_codel qdisc." qdisc=1 fi Check_Lock install_webui mount generate_bwdpi_arrays get_config # Check if iptables rules present # htb+fq_codel: compare existing rules in FlexQoS chain to settings # cake: compare existing rules in FlexQoS chain to settings; check for full FlexQoS_DSCP chain populated; check for FlexQoS_AppDB chain populated [ -f "/tmp/${SCRIPTNAME}_iprules" ] && rm "/tmp/${SCRIPTNAME}_iprules" if ! validate_iptables_rules; then iptables_static_rules 2>&1 | logger -t "${SCRIPTNAME_DISPLAY}" write_iptables_rules [ "${qdisc}" = "2" ] && write_appdb_rules if [ -s "/tmp/${SCRIPTNAME}_iprules" ]; then logmsg "Applying iptables custom rules" . "/tmp/${SCRIPTNAME}_iprules" 2>&1 | logger -t "${SCRIPTNAME_DISPLAY}" if [ "$(am_settings_get "${SCRIPTNAME}"_conntrack)" != "0" ]; then # Flush conntrack table so that existing connections will be processed by new iptables rules logmsg "Flushing conntrack table" /usr/sbin/conntrack -F conntrack >/dev/null 2>&1 fi fi else logmsg "iptables rules already present" fi cru d "${SCRIPTNAME}"_5min 2>/dev/null # Wait for QoS to finish starting up # is it starting from scratch ? Count classes, qdiscs and filters # is it a check after a successful start ? # if htb+fq_codel, count classes, qdiscs and filters again. # if cake, check for 2 cake qdiscs and continue sleepdelay=0 while check_qos_tc; do [ "${sleepdelay}" = "0" ] && logmsg "TC Modification Delayed Start" sleep 10s if [ "${sleepdelay}" -ge "180" ]; then logmsg "QoS state: Classes=${dlclasscnt} | Filters=${dlfiltercnt} | qdiscs=${dlqdisccnt}" if [ ! -f "/tmp/${SCRIPTNAME}_restartonce" ]; then touch "/tmp/${SCRIPTNAME}_restartonce" logmsg "TC Modification Delay reached maximum 180 seconds. Restarting QoS." service "restart_qos;restart_firewall" else logmsg "TC Modification Delay reached maximum 180 seconds again. Canceling startup!" rm "/tmp/${SCRIPTNAME}_restartonce" 2>/dev/null fi return 1 else sleepdelay=$((sleepdelay+10)) fi done [ "${sleepdelay}" -gt "0" ] && logmsg "TC Modification delayed for ${sleepdelay} seconds" rm "/tmp/${SCRIPTNAME}_restartonce" 2>/dev/null set_tc_variables [ -f "/tmp/${SCRIPTNAME}_tcrules" ] && rm "/tmp/${SCRIPTNAME}_tcrules" # Update qdisc if not present (fq_codel or cake) # Update classes (htb+fq_codel) # Update appdb filter rules (htb/fq_codel) write_custom_qdisc if [ "${qdisc}" != "2" ]; then # if TC modifcations have not been applied then run modification script if ! validate_tc_rules; then write_appdb_static_rules write_appdb_rules write_custom_rates else logmsg "No TC modifications necessary" fi fi if [ -s "/tmp/${SCRIPTNAME}_tcrules" ]; then logmsg "Applying AppDB rules and TC rates" if ! "${TC}" -force -batch "/tmp/${SCRIPTNAME}_tcrules" >"/tmp/${SCRIPTNAME}_tcrules.log" 2>&1; then cp -f "/tmp/${SCRIPTNAME}_tcrules" "/tmp/${SCRIPTNAME}_tcrules.err" logmsg "ERROR! Check /tmp/${SCRIPTNAME}_tcrules.log" else rm "/tmp/${SCRIPTNAME}_tmp_tcfilterdown" "/tmp/${SCRIPTNAME}_tmp_tcfilterup" "/tmp/${SCRIPTNAME}_tcrules.log" "/tmp/${SCRIPTNAME}_checktcrules" "/tmp/${SCRIPTNAME}_tcrules.err" 2>/dev/null schedule_check_job fi fi } # startup show_help() { scriptinfo Red "You have entered an invalid command" cat <<EOF Available commands: ${SCRIPTNAME} -about explains functionality ${SCRIPTNAME} -appdb string search appdb for application marks ${SCRIPTNAME} -update checks for updates ${SCRIPTNAME} -restart restart QoS and Firewall ${SCRIPTNAME} -install install script ${SCRIPTNAME} -uninstall uninstall script & delete from disk ${SCRIPTNAME} -enable enable script ${SCRIPTNAME} -disable disable script but do not delete from disk ${SCRIPTNAME} -backup backup user settings ${SCRIPTNAME} -debug print debug info ${SCRIPTNAME} -develop switch to development channel ${SCRIPTNAME} -stable switch to stable channel ${SCRIPTNAME} -menu interactive main menu EOF webconfigpage } # show_help generate_bwdpi_arrays() { # generate if not exist, plus after wrs restart (signature update) # generate if signature rule file is newer than js file # generate if js file is smaller than source file (source not present yet during boot) # prepend wc variables with zero in case file doesn't exist, to avoid bad number error if [ ! -f "/www/user/${SCRIPTNAME}/${SCRIPTNAME}_arrays.js" ] || \ [ "$(/bin/date -r /jffs/signature/rule.trf +%s)" -gt "$(/bin/date -r "/www/user/${SCRIPTNAME}/${SCRIPTNAME}_arrays.js" +%s)" ] || \ [ "${1}" = "force" ] || \ [ "0$(wc -c < "/www/user/${SCRIPTNAME}/${SCRIPTNAME}_arrays.js")" -lt "0$(wc -c 2>/dev/null < /tmp/bwdpi/bwdpi.app.db)" ]; then { printf "var catdb_mark_array = [ \"000000\"" awk -F, '{ printf(", \"%02X****\"",$1) }' /tmp/bwdpi/bwdpi.cat.db 2>/dev/null awk -F, '{ printf(", \"%02X%04X\"",$1,$2) }' /tmp/bwdpi/bwdpi.app.db 2>/dev/null printf ", \"\" ];" printf "var catdb_label_array = [ \"Untracked\"" awk -F, '{ printf(", \"%s (%02X)\"",$2, $1) }' /tmp/bwdpi/bwdpi.cat.db 2>/dev/null awk -F, '{ printf(", \"%s\"",$4) }' /tmp/bwdpi/bwdpi.app.db 2>/dev/null printf ", \"\" ];" } > "/www/user/${SCRIPTNAME}/${SCRIPTNAME}_arrays.js" fi } PressEnter(){ [ "${mode}" = "interactive" ] || return printf "\n" while true; do printf "Press enter to continue..." read -r "key" case "${key}" in *) break ;; esac done return 0 } Kill_Lock() { if [ -f "/tmp/${SCRIPTNAME}.lock" ] && [ -d "/proc/$(sed -n '1p' "/tmp/${SCRIPTNAME}.lock")" ]; then logmsg "[*] Killing Delayed Process (pid=$(sed -n '1p' "/tmp/${SCRIPTNAME}.lock"))" logmsg "[*] $(ps | awk -v pid="$(sed -n '1p' "/tmp/${SCRIPTNAME}.lock")" '$1 == pid')" kill "$(sed -n '1p' "/tmp/${SCRIPTNAME}.lock")" fi rm -rf "/tmp/${SCRIPTNAME}.lock" } # Kill_Lock Check_Lock() { if [ -f "/tmp/${SCRIPTNAME}.lock" ] && [ -d "/proc/$(sed -n '1p' "/tmp/${SCRIPTNAME}.lock")" ] && [ "$(sed -n '1p' "/tmp/${SCRIPTNAME}.lock")" != "$$" ]; then Kill_Lock fi printf "%s\n" "$$" > "/tmp/${SCRIPTNAME}.lock" lock="true" } # Check_Lock get_wan_setting() { local varname varval varname="${1}" prefixes="wan0_ wan1_" if [ "$(nvram get wans_mode)" = "lb" ] ; then for prefix in $prefixes; do state="$(nvram get "${prefix}"state_t)" sbstate="$(nvram get "${prefix}"sbstate_t)" auxstate="$(nvram get "${prefix}"auxstate_t)" # is_wan_connect() [ "${state}" = "2" ] || continue [ "${sbstate}" = "0" ] || continue [ "${auxstate}" = "0" ] || [ "${auxstate}" = "2" ] || continue # get_wan_ifname() proto="$(nvram get "${prefix}"proto)" if [ "${proto}" = "pppoe" ] || [ "${proto}" = "pptp" ] || [ "${proto}" = "l2tp" ] ; then varval="$(nvram get "${prefix}"pppoe_"${varname}")" else varval="$(nvram get "${prefix}""${varname}")" fi done else for prefix in $prefixes; do primary="$(nvram get "${prefix}"primary)" [ "${primary}" = "1" ] && break done proto="$(nvram get "${prefix}"proto)" if [ "${proto}" = "pppoe" ] || [ "${proto}" = "pptp" ] || [ "${proto}" = "l2tp" ] ; then varval="$(nvram get "${prefix}"pppoe_"${varname}")" else varval="$(nvram get "${prefix}""${varname}")" fi fi printf "%s" "${varval}" } # get_wan_setting arg1="${1#-}" if [ -z "${arg1}" ] || [ "${arg1}" = "menu" ] && ! /bin/grep -qE "${SCRIPTPATH} .* # ${SCRIPTNAME_DISPLAY}" /jffs/scripts/firewall-start; then arg1="install" fi wan="$(get_wan_setting 'ifname')" WANMTU="$(get_wan_setting 'mtu')" lan="$(nvram get lan_ifname)" needrestart=0 # initialize variable used in prompt_restart() case "${arg1}" in 'start'|'check') logmsg "$0 (pid=$$) called in ${mode} mode with $# args: $*" startup ;; 'appdb') appdb "${2}" ;; 'install'|'enable') install "${2}" ;; 'uninstall') uninstall ;; 'disable') sed -i "/${SCRIPTNAME}/d" /jffs/scripts/firewall-start 2>/dev/null sed -i "/${SCRIPTNAME}/d" /jffs/scripts/service-event-end 2>/dev/null remove_webui needrestart=2 ;; 'backup') backup create force ;; 'debug') debug ;; 'about') about ;; update*) # updatecheck, updatesilent, or plain update update "${arg1#update}" # strip 'update' from arg1 to pass to update function ;; 'develop') if [ "$(am_settings_get "${SCRIPTNAME}_branch")" = "develop" ]; then printf "Already set to development branch.\n" else am_settings_set "${SCRIPTNAME}_branch" "develop" printf "Set to development branch. Triggering update...\n" exec "${0}" updatesilent fi ;; 'stable') if [ -z "$(am_settings_get "${SCRIPTNAME}_branch")" ]; then printf "Already set to stable branch.\n" else sed -i "/^${SCRIPTNAME}_branch /d" /jffs/addons/custom_settings.txt printf "Set to stable branch. Triggering update...\n" exec "${0}" updatesilent fi ;; 'menu'|'') menu ;; 'restart') needrestart=2 ;; 'flushct') sed -i "/^${SCRIPTNAME}_conntrack /d" /jffs/addons/custom_settings.txt Green "Enabled conntrack flushing." ;; 'noflushct') am_settings_set "${SCRIPTNAME}_conntrack" "0" Yellow "Disabled conntrack flushing." ;; stats) get_cake_stats ;; *) show_help ;; esac prompt_restart if [ "${lock}" = "true" ]; then rm -rf "/tmp/${SCRIPTNAME}.lock"; fi