#!/bin/sh /etc/rc.common # shellcheck disable=SC3043,SC1091,SC2155,SC3020,SC3010,SC2016,SC3060,SC3003,SC3015,SC3044 START=99 STOP=99 USE_PROCD=1 VERSION="dev" UPD_CHANNEL="release" EXTRA_COMMANDS="check_version update auto_setup auto_setup_noninteractive validate_custom_rules health_check" EXTRA_HELP=" check_version Check for updates update Update qosmate auto_setup Automatically configure qosmate auto_setup_noninteractive Automatically configure qosmate with no interaction validate_custom_rules Validate custom rules health_check Check if QoSmate is properly configured and running" REQUIRED_PACKAGES="kmod-sched ip-full kmod-veth tc-full kmod-netem kmod-sched-ctinfo kmod-ifb kmod-sched-cake kmod-sched-red luci-lib-jsonc lua jq coreutils-sleep" ### Utility vars _NL_=' ' DEFAULT_IFS=" ${_NL_}" IFS="$DEFAULT_IFS" ### repo-related vars # GH repo author (can be overriden for testing) : "${QOSMATE_REPO_AUTHOR:=hudra0}" MAIN_BRANCH_BACKEND=main MAIN_BRANCH_FRONTEND=main # GH API URLs (can be overridden) : "${QOSMATE_GH_API_URL:="https://api.github.com/repos/${QOSMATE_REPO_AUTHOR}"}" : "${QOSMATE_GH_API_URL_BACKEND:="${QOSMATE_GH_API_URL}/qosmate"}" : "${QOSMATE_GH_API_URL_FRONTEND:="${QOSMATE_GH_API_URL}/luci-app-qosmate"}" # GH raw backend URL (can be overridden) : "${QOSMATE_GH_RAW_URL_BACKEND_MAIN:="https://raw.githubusercontent.com/${QOSMATE_REPO_AUTHOR}/qosmate/${MAIN_BRANCH_BACKEND}"}" ### Local paths QOSMATE_D=/etc/qosmate.d QOSMATE_CFG_FILE=/etc/config/qosmate TMP_CFG_DIR=/tmp/qosmate_tmp TMP_CFG_FILE=$TMP_CFG_DIR/qosmate QOSMATE_DEFAULTS_FILE=${QOSMATE_D}/qosmate-defaults QOSMATE_CUSTOM_RULES_FILE=${QOSMATE_D}/custom_rules.nft QOSMATE_INLINE_RULES_FILE=${QOSMATE_D}/inline_dscptag.nft QOSMATE_RUN_DIR=/tmp/qosmate # Local path of JS files QOSMATE_UPD_DIR=/var/run/qosmate-update ### Components QOSMATE_COMPONENTS="BACKEND FRONTEND" QOSMATE_FILES_REG_PATH_BACKEND=${QOSMATE_D}/backend_reg.md5 QOSMATE_FILES_REG_PATH_FRONTEND=${QOSMATE_D}/frontend_reg.md5 ### Backend files QOSMATE_SERVICE_PATH=/etc/init.d/qosmate QOSMATE_MAIN_SCRIPT=/etc/qosmate.sh QOSMATE_HOTPLUG_SCRIPT=/etc/hotplug.d/iface/13-qosmateHotplug # stores the version and update channel QOSMATE_VERSION_FILE_BACKEND=$QOSMATE_SERVICE_PATH QOSMATE_FILE_TYPES_BACKEND="GEN EXTRA" # change the value if adding more types QOSMATE_AUTORATE_SCRIPT=/etc/qosmate-autorate.sh QOSMATE_AUTORATE_TC_SCRIPT=/etc/qosmate-autorate-tc.sh QOSMATE_AUTORATE_SERVICE=/etc/init.d/qosmate-autorate QOSMATE_GEN_FILES_BACKEND=" $QOSMATE_SERVICE_PATH $QOSMATE_MAIN_SCRIPT $QOSMATE_DEFAULTS_FILE $QOSMATE_AUTORATE_SCRIPT $QOSMATE_AUTORATE_TC_SCRIPT $QOSMATE_AUTORATE_SERVICE /usr/lib/tc/experimental.dist /usr/lib/tc/normal.dist /usr/lib/tc/normmix20-64.dist /usr/lib/tc/pareto.dist /usr/lib/tc/paretonormal.dist" QOSMATE_EXEC_FILES_BACKEND=" $QOSMATE_SERVICE_PATH $QOSMATE_MAIN_SCRIPT $QOSMATE_AUTORATE_SCRIPT $QOSMATE_AUTORATE_SERVICE" QOSMATE_EXTRA_FILES_BACKEND="" # might be useful in the future? ### Frontend files ### !!! When adding or removing frontend (or other) files which need path fixups, ### remember to update appropriate fixup files QOSMATE_FRONTEND_DIR_JS=/www/luci-static/resources/view/qosmate # stores the version and update channel QOSMATE_VERSION_FILE_FRONTEND="${QOSMATE_FRONTEND_DIR_JS}/settings.js" QOSMATE_FILE_TYPES_FRONTEND="GEN EXTRA JS" # change the value if adding more types QOSMATE_GEN_FILES_FRONTEND=" /usr/share/luci/menu.d/luci-app-qosmate.json /usr/share/rpcd/acl.d/luci-app-qosmate.json /usr/libexec/rpcd/luci.qosmate /usr/libexec/rpcd/luci.qosmate_stats" QOSMATE_EXTRA_FILES_FRONTEND="" # might be useful in the future? QOSMATE_JS_FILENAMES=" settings.js hfsc.js cake.js advanced.js rules.js ratelimits.js connections.js custom_rules.js ipsets.js statistics.js" # generate js file list with paths QOSMATE_JS_FILES_FRONTEND= for js_file in ${QOSMATE_JS_FILENAMES}; do QOSMATE_JS_FILES_FRONTEND="${QOSMATE_JS_FILES_FRONTEND}${QOSMATE_FRONTEND_DIR_JS}/${js_file}${_NL_}" done QOSMATE_EXEC_FILES_FRONTEND=" /usr/libexec/rpcd/luci.qosmate /usr/libexec/rpcd/luci.qosmate_stats" # Silence shellcheck unused vars warnings : "$START" "$STOP" "$USE_PROCD" "$VERSION" "$UPD_CHANNEL" "$EXTRA_COMMANDS" "$EXTRA_HELP" "$REQUIRED_PACKAGES" \ "$MAIN_BRANCH_FRONTEND" "$QOSMATE_FILES_REG_PATH_BACKEND" "$QOSMATE_FILES_REG_PATH_FRONTEND" \ "$QOSMATE_VERSION_FILE_BACKEND" "$QOSMATE_FILE_TYPES_BACKEND" "$QOSMATE_GEN_FILES_BACKEND" \ "$QOSMATE_EXEC_FILES_BACKEND" "$QOSMATE_EXTRA_FILES_BACKEND" "$QOSMATE_FILE_TYPES_FRONTEND" \ "$QOSMATE_GEN_FILES_FRONTEND" "$QOSMATE_EXTRA_FILES_FRONTEND" "$QOSMATE_EXEC_FILES_FRONTEND" \ "$QOSMATE_AUTORATE_SCRIPT" "$QOSMATE_AUTORATE_TC_SCRIPT" "$QOSMATE_AUTORATE_SERVICE" \ "${global_enabled:=}" "${ROOT_QDISC:=}" "${gameqdisc:=}" ### Utility functions uci_tmp() { uci -c "$TMP_CFG_DIR" "$@" } commit_tmp_config() { uci_tmp commit qosmate && cp "$TMP_CFG_FILE" "$QOSMATE_CFG_FILE" local rv=$? rm -rf "$TMP_CFG_DIR" [ $rv = 0 ] || error_out "Failed to save the new config." return $rv } # 1 - string # 2 - path to file write_str_to_file() { printf '%s\n' "$1" > "$2" || { error_out "Failed to write to file '$2'."; return 1; } : } # 0 - (optional) '-p' # 1 - path try_mkdir() { local IFS="$DEFAULT_IFS" p= [ "$1" = '-p' ] && { p='-p'; shift; } [ -d "$1" ] && return 0 mkdir ${p} "$1" || { error_out "Failed to create directory '$1'."; return 1; } : } check_util() { command -v "$1" 1>/dev/null; } error_out() { log_msg -err "${@}"; } # prints each argument to a separate line print_msg() { local _arg for _arg in "$@" do case "${_arg}" in '') printf '\n' ;; # print out empty lines *) printf '%s\n' "${_arg}" esac done : } # logs each argument separately and prints to a separate line # optional arguments: '-err', '-warn' to set logged error level log_msg() { local msgs_prefix='' _arg err_l=info msgs_dest local IFS="$DEFAULT_IFS" for _arg in "$@" do case "${_arg}" in "-err") err_l=err msgs_prefix="Error: " ;; "-warn") err_l=warn msgs_prefix="Warning: " ;; '') printf '\n' ;; # print out empty lines *) case "$err_l" in err|warn) msgs_dest="/dev/stderr" ;; *) msgs_dest="/dev/stdout" esac printf '%s\n' "${msgs_prefix}${_arg}" > "$msgs_dest" logger -t qosmate -p user."$err_l" "${msgs_prefix}${_arg}" msgs_prefix='' esac done : } # check if var names are safe to use with eval are_var_names_safe() { local var_name for var_name in "$@"; do case "$var_name" in *[!a-zA-Z_]*) error_out "Invalid var name '$var_name'."; return 1; esac done : } ### Update-related functions # 1 - component: # return codes: # 0 - OK # 1 - general error # 2 - missing files # 3 - missing reg file # 4 - non-matching md5sums check_files_integrity() { local files_missing='' reg_missing='' reg_file file files file_types component="$1" eval "file_types=\"\${QOSMATE_FILE_TYPES_${component}}\" reg_file=\"\${QOSMATE_FILES_REG_PATH_${component}}\"" [ -f "$reg_file" ] || reg_missing=1 files="$(print_file_list "$component" ALL)" && [ -n "$files" ] || { error_out "Failed to get file list for component '$component'."; return 1; } local IFS="$_NL_" for file in $files; do [ -z "$file" ] || [ -f "$file" ] && continue error_out "Missing file: '$file'." files_missing=1 done IFS="$DEFAULT_IFS" [ -z "$files_missing" ] || return 2 [ -z "$reg_missing" ] || { error_out "$reg_file is not found. Can not check files integrity."; return 3; } md5sum -c "$reg_file" &>/dev/null || return 4 : } # 1 - component: BACKEND|FRONTEND # 2 - types: 'ALL' (doesn't print executable files) or any combination (space-separated) of 'GEN', 'EXTRA', 'JS', 'EXEC' print_file_list() { local me=print_file_list file_type files='' \ component="$1" file_types="$2" [ -n "$1" ] && [ -n "$2" ] || { error_out "$me: missing args."; return 1; } case "$component" in BACKEND|FRONTEND) ;; *) error_out "$me: invalid component '$component'."; return 1 esac if [ "$file_types" = ALL ]; then eval "file_types=\"\${QOSMATE_FILE_TYPES_${component}}\"" fi for file_type in ${file_types}; do case "$file_type" in GEN|EXTRA|JS|EXEC) eval "files=\"${files}\${QOSMATE_${file_type}_FILES_${component}}${_NL_}\"" ;; *) error_out "$me: invalid type '$file_type'"; return 1 esac done # remove extra newlines, leading and trailing whitespaces and tabs printf '%s\n' "$files" | sed "s/^\s*//;s/\s*$//;/^$/d" : } # When updating, old version of the script should call post_update_[X] functions from the new version # post_update_1 should be called via '/bin/sh post_update_1' *before* new version is installed # post_update_2 should be called via '/bin/sh "$QOSMATE_SERVICE_PATH" post_update_2' *after* new version is installed # This allows flexibility when adding new features etc. post_update_1() { : # return $? } post_update_2() { # migrate_config return $? } # Fetches qosmate distribution for specified component # 1 - component (BACKEND|FRONTEND) # 2 - tarball url fetch_qosmate_component() { [ -n "$1" ] && [ -n "$2" ] || { error_out "fetch_qosmate_component: missing arguments."; return 1; } local component="$1" fetch_tarball_url="$2" local fetch_rv extract_dir upd_dir="${QOSMATE_UPD_DIR}/${component}" local tarball="${upd_dir}/remote_qosmate.tar.gz" ucl_err_file="${upd_dir}/ucl_err" case "$component" in BACKEND) repo_name=qosmate ;; FRONTEND) repo_name=luci-app-qosmate ;; *) error_out "$me: invalid component '$component'."; return 1 esac rm -f "$ucl_err_file" "${tarball}" rm -rf "${upd_dir}/${QOSMATE_REPO_AUTHOR}-${repo_name}-"* try_mkdir -p "$upd_dir" || return 1 uclient-fetch "$fetch_tarball_url" -O "${tarball}" 2> "$ucl_err_file" && grep -q "Download completed" "$ucl_err_file" && tar -C "${upd_dir}" -xzf "${tarball}" && extract_dir="$(find "${upd_dir}/" -type d -name "${QOSMATE_REPO_AUTHOR}-${repo_name}-*")" && [ -n "$extract_dir" ] && [ "$extract_dir" != "/" ] fetch_rv=${?} rm -f "${tarball}" [ "$fetch_rv" != 0 ] && [ -s "$ucl_err_file" ] && log_msg "uclient-fetch output: ${_NL_}$(cat "$ucl_err_file")." rm -f "$ucl_err_file" [ "$fetch_rv" = 0 ] && { mv "${extract_dir:-?}"/* "${upd_dir:-?}/" || { rm -rf "${extract_dir:-?}"; error_out "Failed to move files to dist dir."; return 1; } } rm -rf "${extract_dir:-?}" return $fetch_rv } # Get GitHub ref and tarball url for specified component, update channel, branch and version # 1 - component (BACKEND|FRONTEND) # 2 - update channel: release|snapshot|branch=|commit= # 3 - version (optional): [qosmate_version|commit_hash] # Output via variables: # $4 - github ref (version/commit hash), $5 - tarball url, $6 - version type ('version' or 'commit') get_gh_ref_data() { set_res_vars() { if [ "$gh_channel" = release ]; then version="${gh_ref#v}" else version="$gh_ref" fi eval "$4"='$version' "$5"='${gh_url_api}/tarball/${gh_ref}' "$6"='$gh_ver_type' \ "${component}_prev_ref"='$gh_ref' "${component}_prev_ver_type"='$gh_ver_type' \ "${component}_prev_upd_channel"='$gh_channel' "${component}_prev_version"='$version' } get_and_process_ref() { local branch \ channel="$1" ptrn="$2" branches="$3" main_branch="$4" err_file="$5" case "${channel}" in release) uclient-fetch "${gh_url_api}/releases" -O- 2> "$err_file" | { jsonfilter -e '@[@.prerelease=false]' | jsonfilter -a -e "@[@.target_commitish=\"${main_branch}\"].tag_name" cat 1>/dev/null } ;; snapshot|branch=*|commit=*) for branch in ${branches} do uclient-fetch "${gh_url_api}/commits?sha=${branch}" -O- 2> "$err_file" | { jsonfilter -e '@[@.commit]["url"]' | sed 's/.*\///' # only leave the commit hash cat 1>/dev/null } done esac | if [ -n "${ptrn}" ] then grep "${ptrn}" else head -n1 # get latest version or commit cat 1>/dev/null fi } local branches='' main_branch grep_ptrn='' \ gh_ref='' gh_ver_type='' gh_url_api ref_fetch_tmp_dir="/tmp/qosmate-gh" ref_fetch_rv=0 \ prev_ref prev_ver_type prev_upd_channel prev_version \ component="$1" gh_channel="$2" version="$3" [ "$gh_channel" = release ] && version="${version#v}" local ref_ucl_err_file="${ref_fetch_tmp_dir}/ucl_err" are_var_names_safe "$4" "$5" "$6" || return 1 eval "$4='' $5='' $6='' gh_url_api=\"\${QOSMATE_GH_API_URL_${component}}\"" eval "prev_ref=\"\${${component}_prev_ref}\" prev_ver_type=\"\${${component}_prev_ver_type}\" prev_upd_channel=\"\${${component}_prev_upd_channel}\" prev_version=\"\${${component}_prev_version}\"" # if previously stored data exists, use it without API query or cache check if [ -n "$prev_ref" ] && [ -n "$prev_ver_type" ] && \ [ "$prev_upd_channel" = "$gh_channel" ] && [ "$version" = "$prev_version" ]; then gh_ref="$prev_ref" gh_ver_type="$prev_ver_type" else # if commit hash is specified and it's 40-char long, use it directly without API query or cache check case "$gh_channel" in snapshot|branch=*|commit=*) [ "${#version}" = 40 ] && gh_ref="$version" esac if [ -z "$gh_ref" ]; then # ref cache local cache_file cache_filename="${component}_${version}_${gh_channel}" ref_cache_dir="/tmp/qosmate_cache" cache_ttl case "$gh_channel" in commit=*) cache_ttl=2880 ;; # 48 hours *) cache_ttl=10 # 10 minutes esac # clean up old cache find "${ref_cache_dir:-?}" -maxdepth 1 -type f -mmin +"${cache_ttl}" -exec rm -f {} \; 2>/dev/null # check if the query is cached cache_file="$(find "${ref_cache_dir:-?}" -maxdepth 1 -type f -name "${cache_filename}" -print 2>/dev/null)" case "$cache_file" in '') ;; # found nothing *[^"$_NL_"]*"${_NL_}"*[^"${_NL_}"]*) # found multiple files - delete them local file IFS="$_NL_" for file in $cache_file; do [ -n "$file" ] || continue rm -f "$file" done IFS="$DEFAULT_IFS" ;; *) # found cached query if [ -z "$IGNORE_CACHE" ] && [ -f "$cache_file" ] && read -r prev_ref prev_ver_type < "$cache_file" && [ -n "$prev_ref" ] && [ -n "$prev_ver_type" ]; then gh_ref="$prev_ref" gh_ver_type="$prev_ver_type" else rm -f "${cache_file:-???}" fi esac fi fi if [ -n "$gh_ref" ]; then set_res_vars "$@" return 0 fi try_mkdir -p "$ref_fetch_tmp_dir" || return 1 rm -f "$ref_ucl_err_file" eval "main_branch=\"\${MAIN_BRANCH_${component}}\"" case "$gh_channel" in release) gh_ver_type=version [ -n "$version" ] && grep_ptrn="^v${version}$" ;; snapshot) gh_ver_type=commit branches="$main_branch" if [ -n "$version" ]; then grep_ptrn="^${version}$" fi ;; branch=*) gh_ver_type=commit branches="${gh_channel#*=}" if [ -n "$version" ]; then grep_ptrn="^${version}$" fi ;; commit=*) gh_ver_type=commit local gh_hash="${gh_channel#*=}" if [ "${#gh_hash}" = 40 ]; then # if upd. ch. is 'commit', the upd. ch. string includes commit hash - # if it's 40-char long, use it directly without API query gh_ref="$gh_hash" else branches="$( uclient-fetch "${gh_url_api}/branches" -O- 2> "$ref_ucl_err_file" | { jsonfilter -e '@[@]["name"]'; cat 1>/dev/null; } )" [ -n "$branches" ] || { error_out "Failed to get $component branches via GH API (url: '${gh_url_api}/branches')." [ -f "$ref_ucl_err_file" ] && log_msg "uclient-fetch log:${_NL_}$(cat "$ref_ucl_err_file")" rm -f "$ref_ucl_err_file" return 1 } rm -f "$ref_ucl_err_file" grep_ptrn="^${gh_hash}" fi ;; *) error_out "Invalid update channel '$gh_channel'." return 1 esac # Get ref via GH API [ -z "$gh_ref" ] && gh_ref="$(get_and_process_ref "$gh_channel" "$grep_ptrn" "$branches" "$main_branch" "$ref_ucl_err_file")" if [ -z "$gh_ref" ] && [ -f "$ref_ucl_err_file" ] && ! grep -q "Download completed" "$ref_ucl_err_file"; then error_out "Failed to get $component GitHub download URL for $gh_ver_type '$version' (update channel: '$gh_channel')." \ "uclient-fetch log:${_NL_}$(cat "$ref_ucl_err_file")" ref_fetch_rv=1 fi rm -rf "${ref_fetch_tmp_dir:-?}" [ "$ref_fetch_rv" = 0 ] || return 1 # validate resulting ref case "$gh_ref" in *[^"$_NL_"]*"${_NL_}"*[^"${_NL_}"]*) error_out "Got multiple $component download URLs for version '$version'." \ "If using commit hash, please specify the complete commit hash string." return 1 ;; ''|*[!a-zA-Z0-9._-]*) error_out "Failed to get $component GitHub download URL for $gh_ver_type '$version' (update channel: '$gh_channel')." return 1 esac # write the query result to cache try_mkdir -p "$ref_cache_dir" && printf '%s\n' "$gh_ref $gh_ver_type" > "${ref_cache_dir}/${cache_filename}" set_res_vars "$@" : } # get update channel and version from local frontend file # 1 - var name for version output # 2 - var name for upd. channel output # 3 - path to file get_frontend_spec() { local me=get_frontend_spec rv=0 failed_spec='' spec_res='' spec_line spec_path="$3" are_var_names_safe "$1" "$2" || return 1 [ -n "$3" ] || { error_out "$me: missing args."; return 1; } # assumes string enclosed in FS and no prior FS present in the line spec_res="$( awk -v v_ptrn_1="^[ ]*const UI_VERSION[ ]*=" -v v_ptrn_2="^[-a-zA-Z0-9_.]+$" \ -v u_ptrn_1="^[ ]*const UI_UPD_CHANNEL[ ]*=" -v u_ptrn_2="^[-a-zA-Z0-9_.=]+$" \ -F "'" ' BEGIN{rv=1} { if (v_match_res != "" && u_match_res != "") {rv = v_match_res + u_match_res; exit} } $0~v_ptrn_1 { v_match_res=2 if ( $2~v_ptrn_2 ) {print "version:" $2; v_match_res=0} next } $0~u_ptrn_1 { u_match_res=3 if ( $2~u_ptrn_2 ) {print "upd_channel:" $2; u_match_res=0} next } END{exit rv} ' "$spec_path" )" || { rv=$? case $rv in 2) failed_spec=version ;; 3) failed_spec="update channel" ;; *) failed_spec="version and update channel" ;; esac error_out "$me: Failed to get frontend $failed_spec from file '$spec_path'." } local IFS="$_NL_" for spec_line in $spec_res; do case "$spec_line" in '') continue ;; version:*) eval "$1"='${spec_line#*:}' ;; upd_channel:*) eval "$2"='${spec_line#*:}' ;; *) error_out "$me: got unexpected string when parsing file '$spec_path'."; return 1 esac done return $rv } # Get version and update channel from local file or from repo # 1 - var name for version output # 2 - var name for update channel output # 3 - component (BACKEND|FRONTEND) # 4 - origin (local|remote) # Error codes: 1 - failed to get version, 2 - got invalid version get_component_spec() { local gv_version='' gv_upd_channel='' me=get_component_spec \ gv_component="$3" gv_origin="$4" are_var_names_safe "$1" "$2" || return 1 # get update channel case "$gv_component" in BACKEND) gv_upd_channel="$UPD_CHANNEL" ;; FRONTEND) get_frontend_spec gv_version gv_upd_channel "$QOSMATE_VERSION_FILE_FRONTEND" || return 1 ;; *) error_out "$me: invalid component '$gv_component'."; return 1 esac # get version case "$gv_origin" in local) [ "$gv_component" = BACKEND ] && gv_version="$VERSION" ;; remote) get_gh_ref_data "$gv_component" "$gv_upd_channel" "" gv_version _ _ || return 1 ;; *) error_out "$me: invalid origin '$gv_origin'."; return 1 esac : "$gv_version" # Silence shellcheck warning eval "$1"='$gv_version' eval "$2"='$gv_upd_channel' : } # Check and print versions for local and remote backend and frontend # sets global variables: $BACKEND_upd_avail, $FRONTEND_upd_avail # (optional) '-n' to not print update tip # (optional) '-i' to ignore cache # (optional) '-c '' # Return codes: # 0 - no update # 1 - error # 254 - update available check_version() { local origin notify_origin components='' components_arg='' component notify_component cv_version cv_upd_channel local_version \ upd_avail='' no_print_tip= while getopts ":c:ni" opt; do case ${opt} in c) components_arg=$OPTARG ;; n) no_print_tip=1 ;; i) IGNORE_CACHE=1 ;; # global var *) ;; esac done components="${components_arg:-"$QOSMATE_COMPONENTS"}" for component in $components; do case "$component" in BACKEND) notify_component=Backend ;; FRONTEND) notify_component=Frontend ;; *) error_out "check_version: invalid component '$component'."; return 1 esac print_msg "$notify_component versions:" local upd_ch_printed= for origin in local remote; do case "$origin" in local) notify_origin=Current ;; remote) notify_origin=Latest esac get_component_spec cv_version cv_upd_channel "$component" "$origin" || { error_out "Failed to get $origin $component version." \ "To force re-installation of $component, use the command 'service qosmate update -f -c $component'." return 1 } [ -z "$upd_ch_printed" ] && { print_msg " Update channel: $cv_upd_channel"; upd_ch_printed=1; } case "$origin" in local) local_version="$cv_version" ;; remote) if [ "$cv_version" = "$local_version" ]; then unset "${component}_upd_avail" else upd_avail=1 eval "${component}_upd_avail=1" fi esac printf '%s\n' " $notify_origin version: $cv_version" done done if [ -n "$upd_avail" ]; then [ -z "$no_print_tip" ] && printf '\n%s\n%s\n' "A new version of QoSmate is available." \ "To update, run: /etc/init.d/qosmate update" return 254 else printf '\n%s\n' "QoSmate components '$components' are up to date." fi : } # Optional args: # -s : simulate update (intended for testing: service qosmate update -s -v ) # -c : only update specified component # -v [|package[=]|release|snapshot|branch=|commit=] : version string # -U : force this update channel (overrides the upd. channel derived from '-v' option) # -W : force this version (overrides the version derived from '-v' option) # -f : force update # -i : ignore previous cache results update() { upd_failed() { rm -rf "${QOSMATE_UPD_DIR:-?}" [ -n "$*" ] && error_out "$@" error_out "Failed to update QoSmate." } pkg_update_not_impl() { upd_failed "Update channel 'package' not implemented." } # 1 - fixup type: # 2 - path to fixup file # 3 - distribution dir fixup_paths() { local fixup_line='' fetched_path dest_path me=fixup_paths \ fixup_type="$1" fixup_file="$2" dist_dir="$3" [ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] || { error_out "$me: missing args."; return 1; } while IFS='' read -r fixup_line || [ -n "$fixup_line" ]; do case "$fixup_line" in "#"*) continue ;; # skip comments *=*) fetched_path="${fixup_line%%=*}" fetched_path="${fetched_path%/}" dest_path="${fixup_line#*=}" dest_path="${dest_path%/}" [ -n "$fetched_path" ] && [ "$fetched_path" != "/" ] && [ -n "$dest_path" ] || { error_out "$me: invalid line in fixup file: '$fixup_line'."; return 1; } # warn about and skip non-existing files and directories [ -e "${dist_dir:-???}${fetched_path:-???}" ] || { log_msg -warn "$me: path '${dist_dir}${fetched_path}' does not exist."; continue; } case "$fixup_type" in dir) try_mkdir -p "${dist_dir}${dest_path}" && mv "${dist_dir:-???}${fetched_path:-???}"/* "${dist_dir:-???}${dest_path:-???}/" ;; file) try_mkdir -p "${dist_dir}${dest_path%/*}" && mv "${dist_dir:-???}${fetched_path:-???}" "${dist_dir}${dest_path}" ;; *) error_out "$me: invalid fixup type '$fixup_type'."; return 1 esac || { error_out "Failed to move $fixup_type '${dist_dir}${fetched_path}' to '${dist_dir}${dest_path}'." return 1 } ;; *) continue esac done < "$fixup_file" || return 1 : } unexp_arg() { upd_failed "update: unexpected argument '$1'."; } local file origin new_file_list exec_files sim_path='' req_ver='' ver_type ver_str_arg='' \ extract_dir='' dist_dir='' upd_version='' tarball_url='' file_list_query_path \ backend_upd_req='' upd_component_arg='' upd_components='' req_upd_components='' \ req_upd_channel upd_channel='' def_upd_channel='' force_upd_channel='' force_ver='' force_update='' IGNORE_CACHE= while getopts ":s:v:c:U:W:fi" opt; do case ${opt} in c) upd_component_arg=$OPTARG ;; s) sim_path=$OPTARG ;; v) ver_str_arg=$OPTARG force_update=1 ;; U) force_upd_channel=$OPTARG force_update=1 ;; W) force_ver=$OPTARG force_update=1 ;; f) force_update=1 ;; i) IGNORE_CACHE=1 ;; # global var *) unexp_arg "$OPTARG"; return 1 esac done shift $((OPTIND-1)) [ -z "${*}" ] || { unexp_arg "${*}"; return 1; } case "$upd_component_arg" in '') ;; backend|BACKEND) upd_component_arg=BACKEND ;; frontend|FRONTEND) upd_component_arg=FRONTEND ;; *) upd_failed "Unexpected component '$upd_component_arg'"; return 1 esac req_upd_components="${upd_component_arg:-"$QOSMATE_COMPONENTS"}" if [ -n "$force_update" ]; then upd_components="$req_upd_components" else check_version -n -c "$req_upd_components" case $? in 0) return 0 ;; 1) return 1 ;; 254) print_msg "Updates available. Do you want to update? [y/N] " read -r answer case "$answer" in y|Y) ;; *) print_msg "Update cancelled." return 0 esac esac for component in $req_upd_components; do eval "[ -n \"\${${component}_upd_avail}\" ]" && upd_components="${upd_components}${component} " done fi log_msg "Updating QoSmate components '$upd_components'..." # parse version string from arguments into $req_upd_channel, $req_ver case "$ver_str_arg" in '') ;; package*) pkg_update_not_impl return 1 ;; release) req_upd_channel="${ver_str_arg}" req_ver='' ;; snapshot) req_upd_channel="${ver_str_arg}" req_ver='' ;; commit=*) req_upd_channel="${ver_str_arg}" req_ver="${ver_str_arg#*=}" ;; branch=*) req_upd_channel="$ver_str_arg" req_ver='' ;; [0-9]*|v[0-9]*) req_upd_channel=release req_ver="${ver_str_arg#*=}" req_ver="${req_ver#v}" ;; *) upd_failed "Invalid version string '$ver_str_arg'." return 1 esac req_upd_channel="${force_upd_channel:-"${req_upd_channel}"}" req_ver="${force_ver:-"${req_ver}"}" # updating multiple components to same commit hash makes no sense case "$req_upd_channel" in snapshot|branch=*|commit=*) case "$upd_components" in BACKEND*FRONTEND|FRONTEND*BACKEND) [ -n "$req_ver" ] && { upd_failed "Can not update multiple components '$upd_components' to version '$req_ver'." return 1 } esac esac rm -rf "${QOSMATE_UPD_DIR:-?}" try_mkdir -p "$QOSMATE_UPD_DIR" || { upd_failed; return 1; } if [ -n "$sim_path" ] then log_msg "Updating in simulation mode." [ -d "$sim_path" ] || { upd_failed "Update simulation directory '$sim_path' does not exist."; return 1; } [ -n "${req_ver}" ] || { upd_failed "Specify new version."; return 1; } def_upd_channel=release upd_version="${req_ver}" : "${req_upd_channel:="${def_upd_channel}"}" for component in $upd_components; do [ -d "${sim_path}/${component}" ] || { upd_failed "Simulation source directory doesn't have ${component} directory"; return 1; } done cp -rT "$sim_path" "$QOSMATE_UPD_DIR" fi case "$upd_components" in *BACKEND*) backend_upd_req=1 esac for component in $upd_components; do upd_channel= # set default update channel case "$component" in BACKEND) def_upd_channel="${UPD_CHANNEL:-release}" ;; FRONTEND) if [ -n "$req_upd_channel" ]; then : elif [ ! -f "$QOSMATE_VERSION_FILE_FRONTEND" ]; then def_upd_channel="${UPD_CHANNEL:-release}" else get_frontend_spec _ def_upd_channel "$QOSMATE_VERSION_FILE_FRONTEND" || { error_out "Failed to get current FRONTEND update channel. Defaulting to 'release'." def_upd_channel=release } fi esac upd_channel="${req_upd_channel:-"${def_upd_channel}"}" dist_dir="${QOSMATE_UPD_DIR}/${component}" case "$upd_channel" in package) pkg_update_not_impl return 1 ;; *) if [ -n "$sim_path" ] then log_msg "" "Updating $component to version '$upd_version' (update channel: '$upd_channel')." else get_gh_ref_data "$component" "$upd_channel" "$req_ver" upd_version tarball_url ver_type || return 1 case "$upd_channel" in commit=*) # set update channel to 'commit=' upd_channel="${upd_channel%=*}=${upd_version}" esac log_msg "" "Downloading $component, $ver_type '$upd_version' (update channel: '$upd_channel')." fetch_qosmate_component "$component" "$tarball_url" || { upd_failed; return 1; } fi if [ -n "$backend_upd_req" ]; then file_list_query_path="${QOSMATE_UPD_DIR}/BACKEND${QOSMATE_SERVICE_PATH}" else file_list_query_path="${QOSMATE_SERVICE_PATH}" fi new_file_list="$(/bin/sh "$file_list_query_path" print_file_list "$component" ALL)" && write_str_to_file "$new_file_list" "${dist_dir}/new_file_list" && [ -n "$new_file_list" ] || { upd_failed "Failed to get file list from the fetched QoSmate version." \ "NOTE: QoSmate versions prior to v1.2.0 do not support the new update mechanism." return 1 } exec_files="$(/bin/sh "$file_list_query_path" print_file_list "$component" EXEC)" && write_str_to_file "$exec_files" "${dist_dir}/exec_files" || { upd_failed; return 1; } eval "ver_${component}"='$upd_version' eval "upd_channel_${component}"='$upd_channel' # fix-up paths if needed local dir_fixup_file="${dist_dir}/dir_fixups.txt" local path_fixup_file="${dist_dir}/file_fixups.txt" if [ -s "$dir_fixup_file" ]; then fixup_paths "dir" "$dir_fixup_file" "$dist_dir" || { upd_failed "Failed to fix-up dir paths."; return 1; } fi if [ -s "$path_fixup_file" ]; then fixup_paths "file" "$path_fixup_file" "$dist_dir" || { upd_failed "Failed to fix-up file paths."; return 1; } fi esac done [ -n "$backend_upd_req" ] && /bin/sh "${QOSMATE_UPD_DIR}/BACKEND${QOSMATE_SERVICE_PATH}" post_update_1 case "$ver_str_arg" in package*) pkg_update_not_impl return 1 ;; *) install_qosmate_files "$QOSMATE_UPD_DIR" "$upd_components" || { upd_failed; return 1; } rm -rf "${QOSMATE_UPD_DIR:-?}" esac [ -n "$backend_upd_req" ] && chmod +x "$QOSMATE_SERVICE_PATH" /bin/sh "$QOSMATE_SERVICE_PATH" post_update_2 # post_update_2 is called when updating either component log_msg "QoSmate components '$upd_components' have been successfully updated." if [ -n "$backend_upd_req" ] && "$QOSMATE_SERVICE_PATH" enabled; then log_msg "" "Restarting QoSmate." ${QOSMATE_SERVICE_PATH} restart fi : } # 1 - path to upper distribution dir (containing a dir for each component) # 2 - component(s): install_qosmate_files() { inst_failed() { [ -n "$1" ] && error_out "$1" error_out "Failed to install new $component files." } local file preinst_path curr_files new_file_list new_file_list exec_files='' \ dist_dir ver_file_path frontend_updated='' \ upper_dist_dir="$1" components="$2" version upd_channel for component in $components; do log_msg "" "Installing new $component files..." eval "version=\"\${ver_${component}}\" upd_channel=\"\${upd_channel_${component}}\"" [ -n "$version" ] && [ -n "$upd_channel" ] || { inst_failed "Internal error: failed to get version and update channel for component '$component'."; return 1; } dist_dir="${upper_dist_dir}/${component}" # read new file list new_file_list="$(cat "${dist_dir}/new_file_list")" && [ -n "$new_file_list" ] && exec_files="$(cat "${dist_dir}/exec_files")" || { rm -f "${dist_dir}/new_file_list" "${dist_dir}/exec_files" inst_failed "Failed to read new file list." return 1 } rm -f "${dist_dir}/exec_files" # get current file list curr_files="$(print_file_list "$component" ALL)" || { inst_failed; return 1; } eval "ver_file_path=\"\${QOSMATE_VERSION_FILE_${component}}\"" # version and update channel string replacement vars local ver_ptrn_prefix ver_repl_str upd_ch_repl_str case "$component" in BACKEND) ver_ptrn_prefix= ver_repl_str="VERSION=\"$version\"" upd_ch_repl_str="UPD_CHANNEL=\"$upd_channel\"" ;; FRONTEND) ver_ptrn_prefix="const UI_" ver_repl_str="VERSION = '$version';" upd_ch_repl_str="UPD_CHANNEL = '$upd_channel';" ;; esac # set version and update channel in component's main file local preinst_ver_file_path="${dist_dir}${ver_file_path}" sed -i " /^\s*${ver_ptrn_prefix}VERSION\s*=/{s/.*/${ver_ptrn_prefix}${ver_repl_str}/;} /^\s*${ver_ptrn_prefix}UPD_CHANNEL\s*=/{s/.*/${ver_ptrn_prefix}${upd_ch_repl_str}/;}" \ "$preinst_ver_file_path" && # verify that substitution worked grep -q "^${ver_ptrn_prefix}${ver_repl_str}" "$preinst_ver_file_path" && grep -q "^${ver_ptrn_prefix}${upd_ch_repl_str}" "$preinst_ver_file_path" || { inst_failed "Failed to set version in file '$preinst_ver_file_path'."; return 1; } # Check for changed files local curr_reg_file changed_files='' unchanged_files='' man_changed_files='' \ prefixed_curr_reg_file="${dist_dir}/prefixed_reg_${component}.md5" eval "curr_reg_file=\"\${QOSMATE_FILES_REG_PATH_${component}}\"" if [ -s "$curr_reg_file" ]; then # prefix file paths in the reg file for md5sum comparison sed -E "/^$/d;s~([^ ]+$)~${dist_dir}\\1~" "$curr_reg_file" > "$prefixed_curr_reg_file" unchanged_files="$(md5sum -c "$prefixed_curr_reg_file" 2>/dev/null | sed -n "/:\s*OK\s*$/{s/\s*:\s*OK\s*$//;s~^\s*${dist_dir}~~;p;}")" rm -f "$prefixed_curr_reg_file" # remove unchanged files from $new_file_list to reliably get a list of files to copy changed_files="$( printf '%s\n' "$unchanged_files" | awk ' NR==FNR {unch[$0];next} ($0=="" || $0 in unch) {next} {print} ' - "${dist_dir}/new_file_list" )" # Detect manually modified files man_changed_files="$( md5sum -c "$curr_reg_file" 2>/dev/null | sed -n "/:\s*FAILED\s*$/{s/\s*:\s*FAILED\s*$//;p;}" | awk ' NR == FNR {new[$0]; next} $0 in new {print} ' "${dist_dir}/new_file_list" - )" # Add manually modified files to changed files if [ -n "$man_changed_files" ]; then changed_files="$(printf '%s\n' "${changed_files}${_NL_}${man_changed_files}" | sort -u | sed '/^$/d')" fi else changed_files="$new_file_list" fi local IFS="$_NL_" for file in $unchanged_files; do [ -n "$file" ] || continue log_msg "File '$file' did not change - not updating." done local mod_files_bk_dir="/tmp/qosmate_old_modified_files" for file in $man_changed_files; do [ -n "$file" ] && [ -f "$file" ] || continue log_msg -warn "File '$file' was manually modified - overwriting." if try_mkdir -p "$mod_files_bk_dir" && cp "$file" "${mod_files_bk_dir}/${file##*/}"; then log_msg "Saved a backup copy of manually modified file to ${mod_files_bk_dir}/${file##*/}" else log_msg -warn "Can not create a backup copy of manually modified file '$file' - overwriting anyway." fi done # Copy changed files for file in $changed_files do preinst_path="${dist_dir}${file}" log_msg "Copying file '${file}'." try_mkdir -p "${file%/*}" && cp "$preinst_path" "$file" || { inst_failed "Failed to copy file '$preinst_path' to '$file'."; return 1; } [ "$component" = FRONTEND ] && frontend_updated=1 done # delete obsolete files for file in ${curr_files} do [ -f "$file" ] || continue # check for $file in $new_file_list, allowing newline as list delimiter case "$new_file_list" in "$file"|"${file}${_NL_}"*|*"${_NL_}${file}"|*"${_NL_}${file}${_NL_}"*) continue ;; *) log_msg "Deleting obsolete file '$file'." rm -f "$file" esac done # make files executable set -- $exec_files # relying on IFS=\n chmod +x "$@" || { inst_failed "Failed to make files executable."; return 1; } # save the md5sum registry file if needed if [ -n "$changed_files" ] || [ ! -s "$curr_reg_file" ]; then # make md5sum registry of new files # shellcheck disable=SC2046 set -- $(printf '%s\n' "$new_file_list" | sed "/^$/d;s~^\s*~${dist_dir}~") # relying on IFS=\n md5sums="$(md5sum "$@")" && [ -n "$md5sums" ] && try_mkdir -p "${curr_reg_file%/*}" && printf '%s\n' "$md5sums" | sed "s~\s${dist_dir}~ ~" > "$curr_reg_file" || { inst_failed "Failed to register new files."; return 1; } fi IFS="$DEFAULT_IFS" done # Restart rpcd only if any frontend files were updated [ -n "$frontend_updated" ] && /etc/init.d/rpcd restart : } ### Config-related functions preserve_config_files() { local path save_req='' \ paths="$QOSMATE_MAIN_SCRIPT $QOSMATE_SERVICE_PATH $QOSMATE_HOTPLUG_SCRIPT $QOSMATE_D" \ tmp_file="/tmp/qosmate_sysupgr" \ sysupgr_file="/etc/sysupgrade.conf" rm -f "$tmp_file" printf '\n' if [ "$PRESERVE_CONFIG_FILES" = 1 ]; then for path in $paths; do grep -qxF "$path" "$sysupgr_file" && continue echo "$path" >> "$tmp_file" || return 1 save_req=1 done if [ -n "$save_req" ]; then cat "$tmp_file" >> "$sysupgr_file" && print_msg "Config files have been added to $sysupgr_file for preservation." else print_msg "$sysupgr_file already lists qosmate config files." fi else print_msg "Preservation of config files is disabled." # Remove the config files from sysupgrade.conf if they exist [ -f "$sysupgr_file" ] || return 0 cp "$sysupgr_file" "$tmp_file" || return 1 for path in $paths; do grep -qxF "$path" "$tmp_file" || continue sed -i "\|^$path$|d" "$tmp_file" || return 1 save_req=1 done if [ -n "$save_req" ]; then mv "$tmp_file" "$sysupgr_file" && print_msg "Config files have been removed from $sysupgr_file." else print_msg "$sysupgr_file does not list qosmate config files." fi fi rm -f "$tmp_file" : } # return codes: # 0 - OK # 1 - error # 154 - error in default config # 155 - missing sections and/or options in user config parse_config() { local file miss_sec_f="/tmp/qosmate-missing-sections" miss_opt_f="/tmp/qosmate-missing-options" for file in "$QOSMATE_CFG_FILE" "$QOSMATE_DEFAULTS_FILE"; do [ -f "$file" ] || { error_out "$file not found." return 1 } done awk -v miss_sec_f="$miss_sec_f" -v miss_opt_f="$miss_opt_f" \ -v bad_quotes="(\"|'.*'.*')" -v p1="'[^']*$" -v p2=".*'" ' \ function get_val(s){ if (sub(p1,"",s) && sub(p2,"",s)) return s; else return "ERROR" } BEGIN{ rv=0 # Primary settings must be printed first def_prim_arr["settings:UPRATE"] def_prim_arr["settings:DOWNRATE"] } # serialize user config into user_sections_arr, user_opts_arr NR == FNR { if ($0 ~ bad_quotes) next # ignore lines with double-quotes or more than 2 single-quotes if ( $0 ~ /^[ ]*config[ ]/ ) { section = get_val($0) if (section == "ERROR" || section !~ /^[a-zA-Z0-9_]+$/) next # ignore invalid section declarations user_sections_arr[section] next } if ( $0 ~ /^[ ]*option[ ]/ ) { if ( ! section || $2 !~ /^[a-zA-Z0-9_]+$/ ) next # ignore options w/ invalid keys or outside section val = get_val($0) if ( val == "ERROR" || ! val ) next # ignore options w/ invalid or empty values user_opts_arr[section ":" $2] = val } next } # serialize default config into def_prim_arr, def_secondary_arr # and check against it for missing sections or options in user config FNR == 1 { section = ""; section_ind = 0 } $0 ~ /^[ ]*($|#)/ {next} # ignore comments and empty lines /^[ ]*config[ ]/ { if ($0 ~ bad_quotes) {rv=154;exit} section = get_val($0) if (section == "ERROR" || section !~ /^[a-zA-Z0-9_]+$/) {rv=154; exit} if (section in user_sections_arr) {section_ind++;next} # user config is missing section missing_sections = missing_sections section "=" section_ind "\n" print section >> miss_sec_f section_ind++ next } /^[ ]*option[ ]/ { if ($0 ~ bad_quotes) {rv=154;exit} if ( ! section || $2 !~ /^[a-zA-Z0-9_]+$/ ) {rv=154; exit} val = get_val($0) if (val == "ERROR") {rv=154; exit} ser_opt = section ":" $2 if (ser_opt in def_prim_arr) def_prim_arr[ser_opt] = val else def_secondary_arr[ser_opt] = val if (! val) next # do not check user opts vs def opts without value if (ser_opt in user_opts_arr) next # user config is missing option missing_opts = missing_opts ser_opt "=" val "\n" print ser_opt >> miss_opt_f } END{ if (rv == 1 || rv == 154) exit rv if (missing_sections) { rv = 155 print "missing_sections=\"" missing_sections "\"" } if (missing_opts) { rv = 155 print "missing_opts=\"" missing_opts "\"" } for (ser_opt in def_prim_arr) {if (! def_prim_arr[ser_opt]) exit 154} # check that primary opts are set in def config printf "%s", "ser_def_opts=\"" for (ser_opt in def_prim_arr) {print ser_opt "=" def_prim_arr[ser_opt]} for (ser_opt in def_secondary_arr) print ser_opt "=" def_secondary_arr[ser_opt] print "\"" exit rv }' "$QOSMATE_CFG_FILE" "$QOSMATE_DEFAULTS_FILE" rv=$? [ $rv != 155 ] && { rm -f "$miss_sec_f" "$miss_opt_f"; return $rv; } for file in "$miss_sec_f" "$miss_opt_f"; do [ -f "$file" ] || continue log_msg -warn "" "Missing config ${file##*-}:" "$(cat "$file")" rm -f "$file" done return 155 } load_and_fix_config() { export QOSMATE_CONFIG_LOADED= try_load_and_fix_config "$@" && { QOSMATE_CONFIG_LOADED=1; return 0; } # 155 = config loaded but some options/sections missing [ $? != 155 ] && { error_out "Failed to load config." uci revert qosmate 2>/dev/null } return 1 } # return codes: # 0 - check or fix OK # 1 - error try_load_and_fix_config() { parse_ser_opt() { case "$4" in '') return 0 ;; *:*=*) ;; *) error_out "parse_ser_opt: unexpected input '$4'."; return 1 esac local gso_section="${4%%:*}" gso_val="${4#*=}" gso_opt_name="${4#*:}" gso_opt_name="${gso_opt_name%"=$gso_val"}" eval "$1=\"$gso_section\" $2=\"$gso_opt_name\" $3=\"$gso_val\"" } local nofix= [ "$1" = '-nofix' ] && nofix=1 if [ ! -f "$QOSMATE_CFG_FILE" ]; then [ -n "$nofix" ] && return 1 log_msg -warn "Config file not found, restoring from qosmate-defaults." # qosmate-defaults is checked for and restored if required by start() using the update() routine [ -f "$QOSMATE_DEFAULTS_FILE" ] || { error_out "$QOSMATE_DEFAULTS_FILE not found."; return 1; } uci import qosmate < "$QOSMATE_DEFAULTS_FILE" || { error_out "Failed to restore config." return 1 } log_msg "Config file restored." fi local IFS="$DEFAULT_IFS" section section_name ser_opt opt_name def_val \ parse_rv parse_res missing_sections='' missing_opts='' ser_def_opts='' # Validate config and generate $parse_res for config repair parse_res="$(parse_config)" parse_rv=$? case $parse_rv in 0|155) ;; 1) error_out "Failed to parse config."; return 1 ;; 154) error_out "Failed to process $QOSMATE_DEFAULTS_FILE."; return 1 ;; *) error_out "Unexpected return code $parse_rv when parsing config."; return 1 ;; esac eval "$parse_res" || { error_out "Failed to parse config."; return 1; } if [ "$parse_rv" = 155 ] && [ -z "$nofix" ]; then log_msg "" "Adding missing sections and options from the default config." IFS="$_NL_" for section in $missing_sections; do [ -n "$section" ] || continue section_name="${section%%=*}" index="${section#*=}" log_msg "Adding config section '$section_name'." uci set "qosmate.${section_name}=${section_name}" && uci reorder "qosmate.${section_name}=${index}" || return 1 done for ser_opt in $missing_opts; do [ -n "$ser_opt" ] || continue parse_ser_opt section opt_name def_val "$ser_opt" || return 1 [ -n "$section" ] && [ -n "$opt_name" ] && [ -n "$def_val" ] || continue log_msg "Adding option '$opt_name' to section '$section' with default value '$def_val'." uci set "qosmate.${section}.${opt_name}=${def_val}" || { error_out "Failed to add config option."; return 1; } done IFS="$DEFAULT_IFS" uci commit qosmate || return 1 parse_rv=0 fi config_load qosmate || return 1 IFS="$_NL_" for ser_opt in $ser_def_opts; do IFS="$DEFAULT_IFS" parse_ser_opt section opt_name def_val "$ser_opt" || return 1 var_name="$opt_name" # Var name and def val overrides case "${section}:${opt_name}" in global:enabled) var_name=global_enabled ;; advanced:ACKRATE) def_val=$((UPRATE * 5 / 100)) ;; hfsc:GAMEUP) def_val=$((UPRATE*15/100+400)) ;; hfsc:GAMEDOWN) def_val=$((DOWNRATE*15/100+400)) ;; hfsc:netem_direction) var_name=NETEM_DIRECTION ;; # Autorate defaults (based on configured rates) autorate:enabled) var_name=AUTORATE_ENABLED ;; autorate:min_ul_rate) def_val=$((UPRATE * 25 / 100)); var_name=AUTORATE_MIN_UL ;; autorate:base_ul_rate) def_val="$UPRATE"; var_name=AUTORATE_BASE_UL ;; autorate:max_ul_rate) def_val=$((UPRATE * 105 / 100)); var_name=AUTORATE_MAX_UL ;; autorate:min_dl_rate) def_val=$((DOWNRATE * 25 / 100)); var_name=AUTORATE_MIN_DL ;; autorate:base_dl_rate) def_val="$DOWNRATE"; var_name=AUTORATE_BASE_DL ;; autorate:max_dl_rate) def_val=$((DOWNRATE * 105 / 100)); var_name=AUTORATE_MAX_DL ;; autorate:interval) var_name=AUTORATE_INTERVAL ;; autorate:latency_increase_threshold) var_name=AUTORATE_LAT_INC_THR ;; autorate:latency_decrease_threshold) var_name=AUTORATE_LAT_DEC_THR ;; autorate:reflectors) var_name=AUTORATE_REFLECTORS ;; autorate:refractory_increase) var_name=AUTORATE_REFRACT_INC ;; autorate:refractory_decrease) var_name=AUTORATE_REFRACT_DEC ;; autorate:adjust_up_factor) var_name=AUTORATE_ADJ_UP ;; autorate:adjust_down_factor) var_name=AUTORATE_ADJ_DOWN ;; esac NO_EXPORT='' config_get "$var_name" "$section" "$opt_name" "$def_val" done IFS="$DEFAULT_IFS" # Ensure valid values for DOWNRATE, UPRATE case "$ROOT_QDISC" in hfsc|hybrid) local opt val commit_req='' min_val=1000 for opt in DOWNRATE UPRATE; do eval "val=\"\${${opt}}\"" if ! [ "$val" -gt 0 ]; then log_msg -warn "$opt is '$val' for $ROOT_QDISC. Setting to minimum value of $min_val kbps." eval "$opt=$min_val" uci set "qosmate.settings.${opt}=${min_val}" commit_req=1 fi done [ -z "$commit_req" ] || uci commit qosmate || return 1 esac # Limit DOWNRATE based on BWMAXRATIO if [ "$UPRATE" -gt 0 ] && [ $((DOWNRATE > UPRATE*BWMAXRATIO)) -eq 1 ]; then print_msg "We limit the downrate to at most $BWMAXRATIO times the upstream rate to ensure no upstream ACK floods occur which can cause game packet drops" DOWNRATE=$((BWMAXRATIO*UPRATE)) fi return $parse_rv } migrate_config() { try_migrate_config local rv=$? [ $rv = 0 ] || { error_out "Failed to migrate config." uci_tmp revert qosmate 2>/dev/null } rm -rf "$TMP_CFG_DIR" return $rv } try_migrate_config() { report_migration() { local msg="Link layer settings migrated from settings for qdisc '$1' to advanced section: preset=$2" [ -n "$3" ] && msg="$msg, overhead=$3" log_msg "$msg" } delete_premigration_settings() { local qdisc old_settings for qdisc in cake hfsc; do eval "old_settings=\"\${${qdisc}_old_settings}\"" for setting in $old_settings; do uci_tmp -q delete "qosmate.$qdisc.$setting" done done } . /lib/functions.sh local commit_req='' [ -f "$QOSMATE_CFG_FILE" ] && try_mkdir -p "$TMP_CFG_DIR" && cp "$QOSMATE_CFG_FILE" "$TMP_CFG_FILE" || return 1 # Check for and correct the custom_rules section awk " BEGIN{cor=3; inc=3} /^[ ]*config[ ]+qosmate[ ]+'custom_rules'[ ]*$/{inc=4} /^[ ]*config[ ]+custom_rules[ ]+'custom_rules'[ ]*$/{cor=4} END{exit cor inc} " "$TMP_CFG_FILE" case "$?" in 33) # No custom_rules section found ;; 43) # Only correct section found ;; 34) # Only incorrect section found - correct it commit_req=1 print_msg "Incorrect custom_rules section found. Correcting..." sed -i "s/config qosmate 'custom_rules'/config custom_rules 'custom_rules'/" "$TMP_CFG_FILE" print_msg "custom_rules section corrected." ;; 44) # Both correct and incorrect sections found - remove duplicate commit_req=1 print_msg "Both incorrect and correct custom_rules sections found. Removing the incorrect one..." uci_tmp -q delete qosmate.@qosmate[0] || return 1 print_msg "Incorrect custom_rules section removed." ;; *) error_out "Unexpected return code '$?' when checking the 'custom_rules' section." return 1 ;; esac # Migrate link layer settings to advanced section based on active QDisc local root_qdisc qdisc settings setting val adv_preset \ has_old_settings=0 \ cake_old_settings="COMMON_LINK_PRESETS OVERHEAD MPU LINK_COMPENSATION ETHER_VLAN_KEYWORD" \ hfsc_old_settings="LINKTYPE OH" : "$cake_old_settings" "$hfsc_old_settings" UCI_CONFIG_DIR="$TMP_CFG_DIR" config_load qosmate || return 1 config_get root_qdisc settings ROOT_QDISC config_get adv_preset advanced COMMON_LINK_PRESETS # Check for old settings, migrate settings for CAKE or set OLD_${setting} vars for HFSC for qdisc in cake hfsc; do eval "settings=\"\${${qdisc}_old_settings}\"" for setting in $settings; do config_get val "$qdisc" "$setting" [ -n "$val" ] || continue has_old_settings=1 if [ "$qdisc" = "$root_qdisc" ]; then eval "OLD_${setting}=\"${val}\"" # Immediately migrate CAKE settings [ "$qdisc" = cake ] && { uci_tmp set "qosmate.advanced.$setting=$val" || return 1; } fi done done # Skip migration if not needed if [ -n "$adv_preset" ]; then if [ "$has_old_settings" = 0 ]; then [ -n "$commit_req" ] || return 0 commit_tmp_config return $? fi delete_premigration_settings commit_tmp_config return $? fi # Clean up old settings delete_premigration_settings # Finalize CAKE migration [ "$root_qdisc" = cake ] && { if [ -n "$OLD_COMMON_LINK_PRESETS" ]; then commit_tmp_config || return 1 report_migration cake "$OLD_COMMON_LINK_PRESETS" "$OLD_OVERHEAD" else uci_tmp set qosmate.advanced.COMMON_LINK_PRESETS=ethernet && commit_tmp_config || return 1 log_msg "Added missing link layer preset: ethernet." fi return 0 } # Migrate settings for non-cake root qdiscs # Use default "ethernet" if LINKTYPE not set case "${OLD_LINKTYPE:-ethernet}" in atm) COMMON_LINK_PRESETS=atm # ATM used the $OH variable - only set if different from default [ -n "$OLD_OH" ] && [ "$OLD_OH" != "44" ] && { OH="$OLD_OH" uci_tmp set qosmate.advanced.OVERHEAD="$OLD_OH" || return 1 } ;; docsis) COMMON_LINK_PRESETS=docsis ;; *) COMMON_LINK_PRESETS=ethernet ;; esac uci_tmp set qosmate.advanced.COMMON_LINK_PRESETS="$COMMON_LINK_PRESETS" && commit_tmp_config || return 1 report_migration "$root_qdisc" "$COMMON_LINK_PRESETS" "$OH" : } ### Servce functions # Check if qosmate is active # Return codes: # 0 - active # 1 - inactive # 250 - error is_qosmate_active() { [ -n "$1" ] || { error_out "is_qosmate_active: specify WAN device"; return 250; } tc qdisc show dev "$1" 2>/dev/null | grep -qE "hfsc|cake|htb" && \ nft -t list table inet dscptag >/dev/null 2>&1 } # Validate a single nftables file # Args: $1=file_path, $2=file_type, $3=tmp_file # Returns: 0 on success, 1 on failure validate_nft_file() { local file_path="$1" file_type="$2" tmp_file="$3" echo "Validating $file_type ($file_path):" >> "$tmp_file" if [ ! -s "$file_path" ]; then echo " - File is empty or does not exist. Skipping." >> "$tmp_file" return 0 fi case "$file_type" in "full table rules") if nft --check --file "$file_path" >> "$tmp_file" 2>&1; then echo " - Syntax check PASSED." >> "$tmp_file" return 0 else echo " - Syntax check FAILED." >> "$tmp_file" return 1 fi ;; "inline rules") validate_inline_rules "$file_path" "$tmp_file" ;; *) echo " - ERROR: Unknown file type '$file_type'." >> "$tmp_file" return 1 ;; esac } # Validate inline rules with context wrapping # Args: $1=file_path, $2=tmp_file validate_inline_rules() { local file_path="$1" tmp_file="$2" local check_file="/tmp/qosmate_inline_init_check.nft" echo " - Checking for forbidden keywords (table, chain, hook, priority)..." >> "$tmp_file" if grep -Eq '^[[:space:]]*(table|chain|type|hook|priority)[[:space:]]+' "$file_path"; then echo " - Keyword check FAILED. Forbidden keyword found." >> "$tmp_file" return 1 fi printf '%s\n%s\n' " - Keyword check PASSED." \ " - Checking NFT syntax (within context)..." >> "$tmp_file" # Create temporary file with context { printf '%s\n\t%s\n' "table inet __qosmate_validation_ctx {" \ "chain __dscptag_init_ctx {" cat "$file_path" printf '\n\t%s\n%s\n' "}" "}" } > "$check_file" if nft --check --file "$check_file" 2>>"$tmp_file"; then echo " - Syntax check PASSED." >> "$tmp_file" rm -f "$check_file" return 0 else echo " - Syntax check FAILED." >> "$tmp_file" rm -f "$check_file" return 1 fi } validate_custom_rules() { local tmp_file="/tmp/qosmate_custom_rules_validation.txt" local fail=0 # Clear previous validation results true > "$tmp_file" # Delete existing table before validation to avoid conflicts with existing sets/meters echo "Deleting existing qosmate_custom table before validation..." >> "$tmp_file" nft destroy table inet qosmate_custom # Validate both rule types if ! validate_nft_file "$QOSMATE_CUSTOM_RULES_FILE" "full table rules" "$tmp_file"; then fail=$((fail + 1)) fi if ! validate_nft_file "$QOSMATE_INLINE_RULES_FILE" "inline rules" "$tmp_file"; then fail=$((fail + 1)) fi # Report final result if [ $fail -eq 0 ]; then printf '\n%s\n' "Overall validation: PASSED" >> "$tmp_file" return 0 else printf '\n%s\n' "Overall validation: FAILED" >> "$tmp_file" return 1 fi } # Detect package manager (opkg or apk) detect_package_manager() { local has_apk=0 has_opkg=0 check_util apk && has_apk=1 check_util opkg && has_opkg=1 if [ $has_apk -eq 1 ] && [ $has_opkg -eq 1 ]; then echo "conflict" elif [ $has_apk -eq 1 ]; then echo "apk" elif [ $has_opkg -eq 1 ]; then echo "opkg" else echo "unknown" fi } # Check for valid package manager check_pkg_manager() { case "$1" in apk|opkg) return 0 ;; conflict) error_out "Multiple package managers detected (apk and opkg). Package management disabled." return 1 ;; *) error_out "No supported package manager found." return 1 ;; esac } # Check if a package is installed check_package() { local pkg="$1" case "$pkg_manager" in "apk") apk list -I "$pkg" 2>/dev/null | grep -q "^$pkg-[0-9]" return $? ;; "opkg") opkg list-installed | grep -q "^$pkg " return $? ;; *) return 1 ;; esac } install_packages() { # Flag to indicate if opkg update is needed local need_update=0 check_pkg_manager "$pkg_manager" || return 1 # Check if any packages are missing for pkg in $REQUIRED_PACKAGES; do if ! check_package "$pkg"; then print_msg "$pkg is not installed." need_update=1 break fi done # Run update if at least one package is missing if [ "$need_update" -eq 1 ]; then print_msg "Updating package list..." case "$pkg_manager" in "apk") apk update ;; "opkg") opkg update ;; esac # Install missing packages for pkg in $REQUIRED_PACKAGES; do if ! check_package "$pkg"; then log_msg "Installing $pkg..." case "$pkg_manager" in "apk") apk add "$pkg" || { error_out "Failed to install $pkg." return 1 # Abort if the installation fails } ;; "opkg") opkg install "$pkg" || { error_out "Failed to install $pkg." return 1 # Abort if the installation fails } ;; esac fi done fi } check_package_status() { local missing_packages="" for pkg in $REQUIRED_PACKAGES; do if ! check_package "$pkg"; then missing_packages="$missing_packages $pkg" fi done if [ -n "$missing_packages" ]; then error_out "Missing packages:$missing_packages" return 1 fi return 0 } create_hotplug_script() { [ -s "$QOSMATE_HOTPLUG_SCRIPT" ] && { log_msg "Hotplug script already exists."; return 0; } log_msg "" "Creating the hotplug script." cat > "$QOSMATE_HOTPLUG_SCRIPT" << 'EOF' #!/bin/sh [ -n "$DEVICE" ] || exit 0 if [ "$ACTION" = ifup ]; then . /lib/functions.sh config_load qosmate || { logger -t qosmate -p user.err "Failed to load config." exit 1 } config_get qosmate_enabled global enabled if [ "$qosmate_enabled" = "1" ]; then logger -t qosmate "Reloading qosmate.sh due to $ACTION of $INTERFACE ($DEVICE)" /etc/init.d/qosmate enable /etc/init.d/qosmate restart else logger -t qosmate "qosmate is disabled in the configuration. Not executing the script." fi fi EOF } manage_custom_rules_file() { local action="$1" case "$action" in create) # Ensure the directory exists try_mkdir -p "${QOSMATE_D}" # Create the files if they don't exist [ ! -f "$QOSMATE_CUSTOM_RULES_FILE" ] && touch "$QOSMATE_CUSTOM_RULES_FILE" [ ! -f "$QOSMATE_INLINE_RULES_FILE" ] && touch "$QOSMATE_INLINE_RULES_FILE" ;; delete) # Not used at the moment... rm -f "$QOSMATE_CUSTOM_RULES_FILE" rm -f "$QOSMATE_INLINE_RULES_FILE" ;; esac } start_service() { # handle first run after installation or update from older versions if [ "$VERSION" = dev ]; then log_msg "" "Completing the upgrade of the update mechanism..." update -f || return 1 # shellcheck disable=SC1090 . "${QOSMATE_SERVICE_PATH}" # source updated init script fi # Check files and re-fetch them if any are missing for component in $QOSMATE_COMPONENTS; do check_files_integrity "$component" rv=$? case $rv in 0) ;; # integrity OK 1) return 1 ;; 2|3) # missing files local fix_upd_channel='' fix_version='' force_version='' case "$component" in BACKEND) fix_version="$VERSION" fix_upd_channel="$UPD_CHANNEL" ;; FRONTEND) get_component_spec fix_version fix_upd_channel FRONTEND local 2>/dev/null esac case "$fix_version" in dev|''|"1.1.0"|"v1.1.0") ;; *) force_version="-W $fix_version" esac : "${fix_upd_channel:=release}" if ! update -U "$fix_upd_channel" $force_version -c "$component"; then log_msg -warn "Failed to fix qosmate installation automatically." [ $rv = 2 ] && { log_msg "Either connect to the Internet and run '/etc/init.d/qosmate start' to have missing files automatically fetched," \ "or manually download and save them in designated paths." return 1 } # Do not fail on missing reg files fi ;; 4) ;; # non-matching md5sums esac done install_packages create_hotplug_script load_and_fix_config || return 1 # Enable the global option uci set qosmate.global.enabled='1' uci commit qosmate export global_enabled=1 # Create custom rules file if it doesn't exist manage_custom_rules_file create nft destroy table inet qosmate_custom nft -f "$QOSMATE_CUSTOM_RULES_FILE" preserve_config_files # Add config files to sysupgrade.conf try_mkdir -p "$QOSMATE_RUN_DIR" # Save the current WAN interface to a temporary file printf '%s\n' "$WAN" > /tmp/qosmate_wan /bin/sh "$QOSMATE_MAIN_SCRIPT" || { error_out "Failed to start qosmate."; return 1; } /etc/init.d/firewall reload && enabled || /etc/init.d/qosmate enable || return 1 # Start autorate service if enabled (separate procd-managed service) if [ "$AUTORATE_ENABLED" = "1" ] && [ -f /etc/init.d/qosmate-autorate ]; then log_msg "" "Starting autorate service..." /etc/init.d/qosmate-autorate enable 2>/dev/null /etc/init.d/qosmate-autorate start fi log_msg "Service started" } stop_service() { # Read the old WAN interface from the temporary file OLD_WAN=$(cat /tmp/qosmate_wan 2>/dev/null) if [ -z "$OLD_WAN" ]; then # If the temporary file doesn't exist, fall back to WAN from config load_and_fix_config OLD_WAN="$WAN" fi print_msg "Stopping service qosmate..." # Stop autorate service (separate procd-managed service) /etc/init.d/qosmate-autorate stop 2>/dev/null # Only disable if not in shutdown and not in restart if [ "$DISABLE_ON_STOP" != "0" ] && [ -z "$FROM_SHUTDOWN" ]; then # Keep autorate enable-state in sync with qosmate disable-state. /etc/init.d/qosmate-autorate disable 2>/dev/null disable uci set qosmate.global.enabled='0' uci commit qosmate fi # Remove custom rules table nft destroy table inet qosmate_custom ## Delete files rm -f "$QOSMATE_HOTPLUG_SCRIPT" rm -f /usr/share/nftables.d/ruleset-post/dscptag.nft ## Delete the old qdiscs and IFB associated with the old WAN interface tc qdisc del dev "$OLD_WAN" root > /dev/null 2>&1 tc qdisc del dev ifb-"$OLD_WAN" root > /dev/null 2>&1 tc qdisc del dev "$OLD_WAN" ingress > /dev/null 2>&1 # Remove IFB interface ip link del ifb-"$OLD_WAN" 2>/dev/null nft destroy table inet dscptag # Remove temporary/runtime files rm -f /tmp/qosmate_wan rm -rf "$QOSMATE_RUN_DIR" print_msg "Reloading network service..." /etc/init.d/network reload /etc/init.d/firewall reload log_msg "Service stopped" } shutdown() { # Set variable for shutdown FROM_SHUTDOWN=1 # Call stop_service stop_service } status_service() { load_and_fix_config -nofix print_msg "==== qosmate Status ====" # Check if autostart is enabled if enabled; then print_msg "qosmate autostart is enabled." else print_msg "qosmate autostart is not enabled." fi # Check if the service is enabled in UCI config local config_enabled=true not_managing= if [ "$global_enabled" != 1 ]; then config_enabled=false not_managing=", but qosmate is not managing it" fi print_msg "qosmate global:enabled is $config_enabled." local dir dev qdisc qdiscs active_qdisc \ IFB="ifb-$WAN" # Check and report if traffic shaping is active for dir in egress ingress; do case "$dir" in egress) dev="$WAN" ;; ingress) dev="$IFB" ;; esac qdiscs="$(tc qdisc show dev "$dev" 2>/dev/null | sed -n 's/^qdisc\s\s*\([^ \t]*\).* root .*/\1/p')" qdiscs="${qdiscs//$'\n'/ }" active_qdisc='' for qdisc in cake cake_mq hfsc htb; do case "${qdiscs}" in "${qdisc}"|"${qdisc} "*|*" ${qdisc}"|*" ${qdisc} "*) active_qdisc="$qdisc" esac done case "$active_qdisc" in cake|cake_mq) qdisc_print=CAKE ;; hfsc) qdisc_print=HFSC ;; htb) qdisc_print=HTB ;; esac if [ -n "$active_qdisc" ]; then print_msg "Traffic shaping ($qdisc_print) is active on the $dir interface ($dev)$not_managing." else print_msg "No traffic shaping is active on the $dir interface ($dev)." fi done # Show summary of current settings print_msg "" "==== Current Settings ====" \ "Upload rate: $UPRATE kbps" \ "Download rate: $DOWNRATE kbps" \ "Game traffic upload: $GAMEUP kbps" \ "Game traffic download: $GAMEDOWN kbps" if [ "$ROOT_QDISC" = "cake" ]; then print_msg "Queue discipline: CAKE (Root qdisc)" local active_cake_type='' read -r active_cake_type < /tmp/qosmate/cake_type 2>/dev/null if [ "$active_cake_type" = "cake_mq" ]; then print_msg "Multi-Queue CAKE: active (cake_mq)" elif [ "$USE_MQ" = "1" ]; then if [ "$active_cake_type" = "cake" ]; then print_msg "Multi-Queue CAKE: not available, using standard cake" else print_msg "Multi-Queue CAKE: enabled in config (service not running)" fi else print_msg "Multi-Queue CAKE: off" fi elif [ "$ROOT_QDISC" = "htb" ]; then print_msg "Queue discipline: HTB (Root qdisc)" else print_msg "Queue discipline: $gameqdisc (for game traffic in HFSC)" if [ "$ROOT_QDISC" = "hybrid" ] && [ "$USE_MQ" = "1" ]; then print_msg "Multi-Queue CAKE: not supported in hybrid mode" fi fi if [ "$AUTORATE_ENABLED" = "1" ]; then if [ -f /etc/init.d/qosmate-autorate ] && /etc/init.d/qosmate-autorate running; then print_msg "Autorate: active" else print_msg "Autorate: enabled in config but not running" fi else print_msg "Autorate: off" fi # Display QoSmate version information print_msg "" "==== Version Information ====" check_version # Display system information print_msg "" "==== System Information ====" ubus call system board # Display health check information print_msg "" "==== Health Check ====" health_check # Check for flow offloading settings (incompatible with qosmate) print_msg "" "==== Flow Offloading Check ====" local flow_offloading_enabled=0 local flow_offloading_hw_enabled=0 # Check software flow offloading in firewall defaults if [ "$(uci -q get firewall.@defaults[0].flow_offloading)" = "1" ]; then flow_offloading_enabled=1 print_msg "WARNING: Software flow offloading is enabled." fi # Check hardware flow offloading in firewall defaults if [ "$(uci -q get firewall.@defaults[0].flow_offloading_hw)" = "1" ]; then flow_offloading_hw_enabled=1 print_msg "WARNING: Hardware flow offloading is enabled." fi if [ $flow_offloading_enabled -eq 1 ] || [ $flow_offloading_hw_enabled -eq 1 ]; then print_msg "CRITICAL: Flow offloading is incompatible with qosmate and may cause issues!" print_msg "To disable flow offloading, run:" if [ $flow_offloading_enabled -eq 1 ]; then print_msg " uci set firewall.@defaults[0].flow_offloading='0'" fi if [ $flow_offloading_hw_enabled -eq 1 ]; then print_msg " uci set firewall.@defaults[0].flow_offloading_hw='0'" fi print_msg " uci commit firewall" print_msg " /etc/init.d/firewall restart" else print_msg "Flow offloading is disabled (compatible with qosmate)." fi # Display WAN interface information print_msg "" "==== WAN Interface Information ====" ifstatus wan | grep -e device # Display QoSmate configuration print_msg "" "==== QoSmate Configuration ====" cat "$QOSMATE_CFG_FILE" print_msg "==== Package Status ====" if check_pkg_manager "$pkg_manager" && check_package_status; then print_msg "All required packages are installed." else print_msg "Some required packages are missing. QoSmate may not function correctly." fi print_msg "" "==== Detailed Technical Information ====" \ "Traffic Control (tc) Queues:" tc -s qdisc print_msg "" "==== Nftables Ruleset (dscptag) ====" nft list ruleset | grep 'chain dscptag' -A 100 print_msg "" "==== Custom Rules Table Status ====" if nft list table inet qosmate_custom &>/dev/null; then print_msg "Custom rules table (qosmate_custom) is active." \ "Current custom rules:" nft list table inet qosmate_custom else print_msg "Custom rules table (qosmate_custom) is not active or doesn't exist." fi print_msg "" "==== Inline Rules Status ====" if [ -s "$QOSMATE_INLINE_RULES_FILE" ]; then print_msg "Inline rules are configured:" cat "$QOSMATE_INLINE_RULES_FILE" printf '\n' else print_msg "No inline rules configured." fi } restart() { DISABLE_ON_STOP=0 stop_service sleep 1 # Ensure all processes have been properly terminated start_service } reload_service() { restart } # 1 - speedtest command # 2 - Upload|Download # I/O via STDIN/STDOUT parse_speedtest_output() { local speedtest_cmd="$1" direction="$2" case "$speedtest_cmd" in speedtest-go*) grep "${direction}:" | grep -oE '[0-9]+\.[0-9]+' | head -n1 ;; "speedtest --simple"*) # match to (Upload:|Download:).*, print field 2. if no match, return 1 awk "BEGIN{rv=1} \$0~/$direction:/ {print \$2; rv=0; exit} END{exit rv}" ;; *) error_out "Unexpected speedtest command '$speedtest_cmd'." false ;; esac || { error_out "Failed to get the $direction speed." echo "0" return 1 } : } auto_setup() { try_auto_setup "$@" && return 0 error_out "auto-setup failed." uci revert qosmate 2>/dev/null return 1 } # for non-interactive setup, call with '-n [gaming_ip_address]' try_auto_setup() { # shellcheck disable=SC2329 is_ip_configured() { local ip ip_type section_id="$1" gaming_ip="$2" for ip_type in src_ip dest_ip; do config_get ip "$section_id" "$ip_type" [ "$ip" = "$gaming_ip" ] && { ip_configured=1; break; } done } local L3_DEVICE WAN_INTERFACE FINAL_INTERFACE SPEEDTEST_CMD FREE_SPACE SPEED_RESULT DOWNLOAD_SPEED UPLOAD_SPEED local gaming_ip='' noninteractive='' speed_choice response speedtest_req direction if [ "$1" = "-n" ]; then gaming_ip="$2" noninteractive=1 else noninteractive= fi [ -z "$noninteractive" ] && print_msg "Starting qosmate auto-setup..." # Detect WAN interface WAN_INTERFACE=$(ifstatus wan | grep -e '"device"' | cut -d'"' -f4) L3_DEVICE=$(ifstatus wan | grep -e '"l3_device"' | cut -d'"' -f4) if [ -z "$WAN_INTERFACE" ] && [ -z "$L3_DEVICE" ]; then error_out "Unable to detect WAN interface. Please set it manually in the configuration." return 1 fi FINAL_INTERFACE=${L3_DEVICE:-$WAN_INTERFACE} print_msg "Detected WAN interface: $FINAL_INTERFACE" # Stop qosmate if it's running if is_qosmate_active "$FINAL_INTERFACE"; then print_msg "Stopping qosmate for accurate speed test results..." stop_service sleep 5 # Give some time for the network to stabilize fi if [ -n "$noninteractive" ]; then speedtest_req=1 else while :; do print_msg "Do you want to run a speed test or enter speeds manually? [test/manual]" read -r speed_choice case "$speed_choice" in *[A-Z]*) speed_choice="$(printf %s "$speed_choice" | tr 'A-Z' 'a-z')" esac case "$speed_choice" in test|manual) break esac print_msg "Invalid input '$speed_choice'. Please enter 'test' or 'manual'." done if [[ "$speed_choice" = manual ]]; then for direction in download upload; do while :; do print_msg "Please enter your $direction speed in Mbit/s:" read -r response case "$response" in ''|*[!0-9.]*|*.*.*) # do not allow empty string or irrelevant characters or 2x '.' print_msg "Invalid input '$response'. Please try again." continue ;; *[0-9]*) break ;; *) # do not allow input without digits print_msg "Invalid input '$response'. Please try again." continue ;; esac done case "$direction" in download) DOWNLOAD_SPEED="$response" ;; upload) UPLOAD_SPEED="$response" ;; esac done speedtest_req= else print_msg "This will run a speed test to configure qosmate. Do you want to continue? [y/N]" read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then print_msg "Auto-setup cancelled." return 0 fi speedtest_req=1 fi fi if [ -n "$speedtest_req" ]; then # Check for speedtest-go first if check_util speedtest-go; then print_msg "speedtest-go is already installed. Using it for the speed test." SPEEDTEST_CMD="speedtest-go" else print_msg "speedtest-go is not found. Checking for python3-speedtest-cli..." if check_util speedtest; then print_msg "python3-speedtest-cli is already installed. Using it for the speed test." SPEEDTEST_CMD="speedtest --simple" else print_msg "Neither speedtest-go nor python3-speedtest-cli is installed. Attempting to install speedtest-go..." check_pkg_manager "$pkg_manager" || return 1 # Check for sufficient space (adjust the required space as needed) FREE_SPACE=$(df /overlay | awk 'NR==2 {print $4}') if [ "$FREE_SPACE" -lt 15360 ]; then # Assuming 15MB for speedtest-go print_msg "Not enough space for speedtest-go. Attempting to install python3-speedtest-cli instead..." else case "$pkg_manager" in "apk") apk update && apk add speedtest-go ;; "opkg") opkg update && opkg install speedtest-go ;; esac if [ $? -eq 0 ]; then SPEEDTEST_CMD="speedtest-go" else error_out "Failed to install speedtest-go. Attempting to install python3-speedtest-cli instead..." fi fi # If speedtest-go installation failed or there wasn't enough space, try python3-speedtest-cli if [ -z "$SPEEDTEST_CMD" ]; then if [ "$FREE_SPACE" -lt 1024 ]; then # 1MB for python3-speedtest-cli error_out "Error: Not enough free space to install any speedtest tool." \ "Auto-setup cannot continue. Please free up some space and try again." return 1 fi case "$pkg_manager" in "apk") apk update && apk add python3-speedtest-cli ;; "opkg") opkg update && opkg install python3-speedtest-cli python3-speedtest-cli-src ;; esac if [ $? -eq 0 ]; then SPEEDTEST_CMD="speedtest --simple" else error_out "Failed to install python3-speedtest-cli. Auto-setup cannot continue." return 1 fi fi fi fi print_msg "Running speed test... This may take a few minutes." SPEED_RESULT=$($SPEEDTEST_CMD) DOWNLOAD_SPEED=$(printf %s "$SPEED_RESULT" | parse_speedtest_output "$SPEEDTEST_CMD" Download) && UPLOAD_SPEED=$(printf %s "$SPEED_RESULT" | parse_speedtest_output "$SPEEDTEST_CMD" Upload) || return 1 print_msg "Speed test results:" \ "Download speed: $DOWNLOAD_SPEED Mbit/s" \ "Upload speed: $UPLOAD_SPEED Mbit/s" fi # Convert speeds to kbps and apply 90% rule DOWNRATE=$(awk -v speed="$DOWNLOAD_SPEED" 'BEGIN {print int(speed * 1000 * 0.9)}') UPRATE=$(awk -v speed="$UPLOAD_SPEED" 'BEGIN {print int(speed * 1000 * 0.9)}') print_msg "QoS configuration:" \ "DOWNRATE: $DOWNRATE kbps (90% of measured download speed)" \ "UPRATE: $UPRATE kbps (90% of measured upload speed)" [ -f "$QOSMATE_CFG_FILE" ] || { error_out "config file '$QOSMATE_CFG_FILE' not found." return 1 } config_load qosmate && uci set qosmate.settings.WAN="$FINAL_INTERFACE" && uci set qosmate.settings.DOWNRATE="$DOWNRATE" && uci set qosmate.settings.UPRATE="$UPRATE" || return 1 # Section for gaming device IP if [ -z "$noninteractive" ]; then print_msg "Would you like to add a gaming device IP for prioritization? [y/N]" read -r response if [[ "$response" =~ ^[Yy]$ ]]; then print_msg "Please enter the IP address of your gaming device:" read -r gaming_ip fi fi if [ -n "$gaming_ip" ]; then # Validate IP address format if [[ $gaming_ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then # Check if rules for this IP already exist local ip_configured= config_foreach is_ip_configured rule "$gaming_ip" # $ip_configured is set in is_ip_configured() if [ -n "$ip_configured" ]; then print_msg "Rules for IP $gaming_ip already exist. Skipping addition of new rules." gaming_ip= fi else print_msg "Invalid IP address format. No gaming device rules added." gaming_ip= fi fi if [ -n "$gaming_ip" ]; then local direction ip_dir port_dir for direction in Inbound Outbound; do case "$direction" in Inbound) ip_dir=dest port_dir=src ;; Outbound) ip_dir=src port_dir=dest ;; esac uci add qosmate rule 1>/dev/null && uci set "qosmate.@rule[-1].name=Game_Console_${direction}" && uci set "qosmate.@rule[-1].proto=udp" && uci set "qosmate.@rule[-1].${ip_dir}_ip=${gaming_ip}" && uci add_list "qosmate.@rule[-1].${port_dir}_port=!=80" && uci add_list "qosmate.@rule[-1].${port_dir}_port=!=443" && uci set "qosmate.@rule[-1].class=cs5" && uci set "qosmate.@rule[-1].counter=1" || return 1 done print_msg "Gaming device rules added for IP: $gaming_ip" else print_msg "No gaming device IP added." fi uci commit qosmate || return 1 print_msg "Configuration updated. New settings:" grep -E "option (WAN|DOWNRATE|UPRATE)" "$QOSMATE_CFG_FILE" print_msg "Auto-setup complete. qosmate has been configured with detected settings." \ "To apply these changes, please restart qosmate by running: $QOSMATE_SERVICE_PATH restart" : } auto_setup_noninteractive() { local output_file="/tmp/qosmate_auto_setup_output.txt" auto_setup_rv { echo "Starting qosmate non-interactive auto-setup..." auto_setup -n "$1" 2>&1 } > "$output_file" auto_setup_rv=$? echo "$output_file" return $auto_setup_rv } health_check() { # Check if QoSmate is properly configured and running local status="" errors=0 service_enabled=0 config_status='' # Check the config if load_and_fix_config -nofix 1>/dev/null; then config_status=ok else config_status=failed # *** NOTE THE CHANGE FROM 'missing' TO 'failed' *** errors=$((errors + 1)) fi # Check global:enabled and if the service is enabled enabled && service_enabled=1 case "${service_enabled}${global_enabled}" in 11) status="${status}service:enabled;" ;; 00) status="${status}service:disabled;"; errors=$((errors + 1)) ;; *) status="${status}service:invalid_state;"; errors=$((errors + 1)) ;; esac # Check nftables configuration if nft list table inet dscptag >/dev/null 2>&1; then status="${status}nft:ok;" else status="${status}nft:failed;" errors=$((errors + 1)) fi # Check tc configuration on WAN interface if [ -n "$WAN" ] && tc qdisc show dev "$WAN" 2>/dev/null | grep -E -q "hfsc|cake|htb"; then status="${status}tc:ok;" else status="${status}tc:failed;" errors=$((errors + 1)) fi status="${status}config:${config_status};" # Check required packages local missing_packages="" for pkg in $REQUIRED_PACKAGES; do if ! check_package "$pkg"; then missing_packages="${missing_packages}${pkg} " fi done if [ -z "$missing_packages" ]; then status="${status}packages:ok;" else status="${status}packages:missing:${missing_packages};" errors=$((errors + 1)) fi # Check files existence and md5sums for component in ${QOSMATE_COMPONENTS}; do if check_files_integrity "$component"; then status="${status}${component}_integrity:ok;" else status="${status}${component}_integrity:failed;" errors=$((errors + 1)) fi done # Output status echo "status=$status;errors=$errors" # Return code depending on errors [ $errors -eq 0 ] || return 1 return 0 } pkg_manager="$(detect_package_manager)" # global variable ### Process command-line args # if called directrly via /bin/sh with one of the keywords, set $action to the keyword case "$1" in update|print_file_list|post_update_1|post_update_2) action="$1"; shift esac case "$action" in update|print_file_list|post_update_1|post_update_2) "$action" "$@"; exit $? ;; esac :