🏅 Sync 2026-07-25 20:22:21

This commit is contained in:
github-actions[bot] committed 2026-07-25 20:22:21 +08:00
1 parent 971b549a31
commit f708dca2cf
19 files changed
+1051 -118

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=qosmate
PKG_VERSION:=0.5.48
PKG_RELEASE:=3
PKG_RELEASE:=4
PKG_MAINTAINER:=Markus Hütter <mh@hudra.net>
PKG_LICENSE:=GPL-3.0-or-later
+8 -22
View File
@@ -377,6 +377,8 @@ logread | grep qosmate-autorate
QoSmate allows you to define custom DSCP (Differentiated Services Code Point) marking rules to prioritize specific types of traffic. These rules are defined in the `/etc/config/qosmate` file under the `rule` sections and via luci-app-qosmate.
> **Rules are only evaluated in the upload direction.** QoSmate stores the DSCP of a connection in conntrack on egress, and `tc-ctinfo` restores it on incoming packets before the firewall sees them. WAN → LAN traffic therefore bypasses rule evaluation completely. One rule that matches the upload direction (for example the `src_ip` of your device) automatically covers the download direction of the same connection. A mirrored "inbound" rule is not needed - it can never match, and its counter stays at 0.
| Config option | Description | Type | Default |
| ------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | ------- |
| name | A unique name for the rule. Used for identification and logging. | string | |
@@ -424,50 +426,33 @@ config rule
option class 'cs1'
option counter '1'
```
Explanation: This rule assigns low priority to P2P traffic (like BitTorrent) by marking it as CS1. Using `list` for both `src_port` and `dest_port` covers both incoming and outgoing P2P traffic.
Explanation: This rule assigns low priority to P2P traffic (like BitTorrent) by marking it as CS1. Both port lists are matched on outgoing packets: `src_port` catches traffic sent from your local peer's listening port, `dest_port` catches traffic sent to a remote peer's listening port.
3. Call of Duty Game Traffic:
```
config rule
option name 'cod1'
option name 'cod'
option proto 'udp'
option src_ip '192.168.1.208'
option src_port '3074'
option dest_port '30000-65535'
option class 'cs5'
option counter '1'
config rule
option name 'cod2'
option proto 'udp'
option dest_ip '192.168.1.208'
option dest_port '3074'
option class 'cs5'
option counter '1'
```
Explanation: These rules prioritize Call of Duty game traffic. The first rule targets outgoing traffic from the game console (IP 192.168.1.208), while the second rule handles incoming traffic. Both use CS5 class, which is typically used for gaming traffic due to its high priority. The wide range of destination ports in the first rule covers the game's server ports.
Explanation: This rule prioritizes Call of Duty game traffic sent by the console (IP 192.168.1.208) from port 3074 to the game's server ports. It uses the CS5 class, which is typically used for gaming traffic due to its high priority. The incoming traffic of these connections is handled automatically via conntrack, so a second rule for the download direction is not required.
4. Generic Game Console/Gaming PC Traffic:
```
config rule
option name 'Game_Console_Outbound'
option name 'Game_Console'
option proto 'udp'
option src_ip '192.168.1.208'
list dest_port '!=80'
list dest_port '!=443'
option class 'cs5'
option counter '1'
config rule
option name 'Game_Console_Inbound'
option proto 'udp'
option dest_ip '192.168.1.208'
list src_port '!=80'
list src_port '!=443'
option class 'cs5'
option counter '1'
```
Explanation: These rules provide a more generic approach to prioritizing game console/gaming pc traffic. The outbound rule prioritizes all UDP traffic from the console (192.168.1.208) except for ports 80 and 443 (common web traffic). The inbound rule does the same for incoming traffic. This approach ensures that game-related traffic gets priority while allowing normal web browsing to use default priorities. The use of '!=' (not equal) in the port lists demonstrates how to exclude specific ports from the rule.
Explanation: This rule provides a more generic approach to prioritizing game console/gaming pc traffic. It prioritizes all UDP traffic from the console (192.168.1.208) except for ports 80 and 443 (common web traffic), which ensures that game-related traffic gets priority while allowing normal web browsing to use default priorities. The use of '!=' (not equal) in the port list demonstrates how to exclude specific ports from the rule.
This is more or less equivalent to the `realtime4` and `realtime6` variables from the SimpleHFSCgamer script. However, this rule is even better as it excludes UDP port 80 and 443, which are often used for QUIC. This is likely less of an issue on a gaming console than on a gaming PC, where a YouTube video using QUIC might be running alongside the game.
This rule is also applied when the auto-setup is used via CLI or UI and a Gaming Device IP (optional) is entered.
@@ -1453,6 +1438,7 @@ QoSmate handles traffic in both directions (upload and download) using the follo
- If the conditional bit (128) is set, the original DSCP value is restored:
- DSCP is extracted as `mark & 63`
- This DSCP value is applied to incoming packets
- Since the DSCP is already restored at this point, WAN → LAN packets skip the nftables marking chain, which is why rule counters only increase for the upload direction
3. **IFB (Intermediate Functional Block):**
- Linux can only shape outgoing traffic
+5 -15
View File
@@ -83,6 +83,10 @@ config autorate 'autorate'
option flash_history '0'
option log_changes '0'
# Rules are only evaluated on egress (LAN -> WAN): QoSmate stores the DSCP in conntrack
# and tc-ctinfo restores it on ingress. One rule matching the upload direction therefore
# covers both directions of a connection - a mirrored inbound rule would never match.
#config rule 'cod1'
# option name 'cod1'
# option proto 'udp'
@@ -92,14 +96,6 @@ config autorate 'autorate'
# option class 'cs5'
# option counter '1'
#config rule 'cod2'
# option name 'cod3'
# option proto 'udp'
# option dest_ip '192.168.1.208'
# option dest_port '3074'
# option class 'cs5'
# option counter '1'
#config rule
# option name 'TeamViewer'
# option proto 'tcp'
@@ -109,17 +105,11 @@ config autorate 'autorate'
# option counter '1'
#config rule
# option name 'ServerDownPrio1'
# option name 'ServerDownPrio'
# option src_ip '192.168.15.0/24'
# option class 'cs1'
# option counter '1'
#config rule
# option name 'ServerDownPrio2'
# option dest_ip '192.168.15.0/24'
# option class 'cs1'
# option counter '1'
#config ipset
# option name 'gaming_devices'
# option mode 'static'
+135 -45
View File
@@ -9,12 +9,13 @@ USE_PROCD=1
VERSION="dev"
UPD_CHANNEL="release"
EXTRA_COMMANDS="check_version update auto_setup auto_setup_noninteractive validate_custom_rules health_check"
EXTRA_COMMANDS="check_version update auto_setup auto_setup_noninteractive auto_setup_detached 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
auto_setup_detached Start the non-interactive auto-setup in the background
validate_custom_rules Validate custom rules
health_check Check if QoSmate is properly configured and running"
@@ -54,6 +55,7 @@ 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
QOSMATE_AUTO_SETUP_OUTPUT=/tmp/qosmate_auto_setup_output.txt
# Local path of JS files
QOSMATE_UPD_DIR=/var/run/qosmate-update
@@ -197,7 +199,7 @@ print_msg() {
# 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 msgs_prefix='' _arg err_l=info
local IFS="$DEFAULT_IFS"
for _arg in "$@"
@@ -207,11 +209,12 @@ log_msg() {
"-warn") err_l=warn msgs_prefix="Warning: " ;;
'') printf '\n' ;; # print out empty lines
*)
# Write to the inherited descriptors instead of redirecting to /dev/stdout: that
# re-opens the target, and when stdout is a regular file '>' truncates it.
case "$err_l" in
err|warn) msgs_dest="/dev/stderr" ;;
*) msgs_dest="/dev/stdout"
err|warn) printf '%s\n' "${msgs_prefix}${_arg}" >&2 ;;
*) printf '%s\n' "${msgs_prefix}${_arg}"
esac
printf '%s\n' "${msgs_prefix}${_arg}" > "$msgs_dest"
logger -t qosmate -p user."$err_l" "${msgs_prefix}${_arg}"
msgs_prefix=''
esac
@@ -2218,6 +2221,48 @@ parse_speedtest_output() {
:
}
# Prints WAN throughput in Mbit/s once per second as "<rx> <tx>", until killed.
# 1 - network device
sample_wan_rates() {
local stats="/sys/class/net/${1}/statistics" prev_rx prev_tx rx tx
read -r prev_rx < "$stats/rx_bytes" && read -r prev_tx < "$stats/tx_bytes" || return 1
while sleep 1; do
read -r rx < "$stats/rx_bytes" && read -r tx < "$stats/tx_bytes" || return 1
printf '%s %s\n' $(( (rx - prev_rx) * 8 / 1000000 )) $(( (tx - prev_tx) * 8 / 1000000 ))
prev_rx=$rx prev_tx=$tx
done
}
# Median of the samples reaching at least half of the peak sample, which is the plateau of a test
# phase. Ignores ramp-up, idle time and the ACK traffic of the opposite direction.
# Prints 0 if there is no usable plateau.
# 1 - samples file, 2 - column (1 = rx, 2 = tx)
plateau_rate() {
[ -s "$1" ] || { echo "0"; return 0; }
awk -v col="$2" '
{ v[NR] = $col + 0; if (v[NR] > peak) peak = v[NR] }
END {
if (peak <= 0) { print 0; exit }
for (i = 1; i <= NR; i++) if (v[i] >= peak / 2) s[++n] = v[i]
if (n < 3) { print 0; exit }
for (i = 2; i <= n; i++) { x = s[i]; for (j = i - 1; j > 0 && s[j] > x; j--) s[j + 1] = s[j]; s[j + 1] = x }
print (n % 2) ? s[(n + 1) / 2] : int((s[n / 2] + s[n / 2 + 1]) / 2)
}' "$1" 2>/dev/null
}
# 1, 2 - rates to compare
lower_of() {
awk -v a="$1" -v b="$2" 'BEGIN{print (a < b) ? a : b}'
}
# Warns when the speed test result is implausibly higher than what crossed the interface.
# 1 - direction label, 2 - reported rate, 3 - measured rate
warn_if_overreported() {
awk -v r="$2" -v m="$3" 'BEGIN{exit !(m > 0 && r > m * 1.25)}' &&
print_msg "Warning: the speed test reported $2 Mbit/s $1, but only $3 Mbit/s crossed the WAN interface. Using the measured value."
:
}
auto_setup() {
try_auto_setup "$@" && return 0
error_out "auto-setup failed."
@@ -2380,15 +2425,49 @@ try_auto_setup() {
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
local samples_file=/tmp/qosmate-speedtest-samples sampler_pid speedtest_rv retried=''
local reported_down reported_up measured_down measured_up
while :; do
sample_wan_rates "$FINAL_INTERFACE" > "$samples_file" &
sampler_pid=$!
SPEED_RESULT=$($SPEEDTEST_CMD 2>&1)
speedtest_rv=$?
kill "$sampler_pid" 2>/dev/null
[ "$speedtest_rv" = 0 ] &&
DOWNLOAD_SPEED=$(printf %s "$SPEED_RESULT" | parse_speedtest_output "$SPEEDTEST_CMD" Download) &&
UPLOAD_SPEED=$(printf %s "$SPEED_RESULT" | parse_speedtest_output "$SPEEDTEST_CMD" Upload) &&
break
[ -z "$retried" ] || {
# include the tool output, otherwise the failure is only reported generically
error_out "Speed test failed (exit code $speedtest_rv). Speed test output:" "$SPEED_RESULT"
return 1
}
retried=1
log_msg -warn "Speed test failed (exit code $speedtest_rv), retrying once."
done
# A speed test measures at the socket, so it can report several times the rate that actually
# crossed the link (speedtest-go does this on upload). Cross-check with the WAN counters.
reported_down="$DOWNLOAD_SPEED" reported_up="$UPLOAD_SPEED"
measured_down=$(plateau_rate "$samples_file" 1)
measured_up=$(plateau_rate "$samples_file" 2)
rm -f "$samples_file"
# Use the lower value: the test may over-report, the counters may include foreign traffic
[ "$measured_down" != 0 ] && DOWNLOAD_SPEED=$(lower_of "$reported_down" "$measured_down")
[ "$measured_up" != 0 ] && UPLOAD_SPEED=$(lower_of "$reported_up" "$measured_up")
warn_if_overreported download "$reported_down" "$measured_down"
warn_if_overreported upload "$reported_up" "$measured_up"
print_msg "Speed test results:" \
"Download speed: $DOWNLOAD_SPEED Mbit/s" \
"Upload speed: $UPLOAD_SPEED Mbit/s"
"Upload speed: $UPLOAD_SPEED Mbit/s" \
"WAN counters during the test: $measured_down Mbit/s down, $measured_up Mbit/s up (speed test: $reported_down / $reported_up)"
fi
# Convert speeds to kbps and apply 90% rule
@@ -2419,43 +2498,29 @@ try_auto_setup() {
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
# Exactly one outcome is reported, so the UI can always tell the user what happened
local ip_configured=
if [ -z "$gaming_ip" ]; then
print_msg "No gaming device IP given - no rule added."
elif [[ ! "$gaming_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
print_msg "Invalid IP address format '$gaming_ip' - no gaming device rule added."
else
config_foreach is_ip_configured rule "$gaming_ip" # sets $ip_configured
if [ -n "$ip_configured" ]; then
print_msg "A rule for IP $gaming_ip already exists - keeping it."
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
# Only an egress rule is needed: the DSCP is stored in conntrack and restored on ingress via tc-ctinfo
uci add qosmate rule 1>/dev/null &&
uci set "qosmate.@rule[-1].name=Game_Console_${direction}" &&
uci set "qosmate.@rule[-1].name=Game_Console" &&
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].src_ip=${gaming_ip}" &&
uci add_list "qosmate.@rule[-1].dest_port=!=80" &&
uci add_list "qosmate.@rule[-1].dest_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."
print_msg "Gaming device rule added for IP: $gaming_ip"
fi
fi
uci commit qosmate || return 1
@@ -2470,16 +2535,41 @@ try_auto_setup() {
}
auto_setup_noninteractive() {
local output_file="/tmp/qosmate_auto_setup_output.txt" auto_setup_rv
local auto_setup_rv
# Truncate up front and append afterwards. Truncating while a writer already holds an offset
# leaves a gap that the kernel fills with NUL bytes, and ubus file.read stops at the first NUL,
# so a polling caller would only ever see the beginning of the file.
: > "$QOSMATE_AUTO_SETUP_OUTPUT"
{
echo "Starting qosmate non-interactive auto-setup..."
auto_setup -n "$1" 2>&1
} > "$output_file"
auto_setup_rv=$?
echo "$output_file"
auto_setup_rv=$?
# lets callers polling this file tell a finished run from a partial or stale one
echo "AUTO_SETUP_DONE rv=$auto_setup_rv"
} >> "$QOSMATE_AUTO_SETUP_OUTPUT"
echo "$QOSMATE_AUTO_SETUP_OUTPUT"
return $auto_setup_rv
}
# Starts the non-interactive auto-setup detached and prints the output file path right away.
# A speed test regularly takes longer than uhttpd's script_timeout (60s by default), which kills
# the CGI and leaves the caller with a half-written file, so the caller polls the file instead.
# 1 - optional gaming device IP
auto_setup_detached() {
printf '%s\n' "Starting qosmate auto-setup..." > "$QOSMATE_AUTO_SETUP_OUTPUT" || return 1
# A new session is required, not just a background job: ash waits for children of the same
# session in a command substitution, which is how callers read this function's output.
if check_util setsid; then
# stdin is closed as well, otherwise the run keeps the caller's pipe open - for a request
# from the web UI that blocks the connection the UI then polls on
setsid "$QOSMATE_SERVICE_PATH" auto_setup_noninteractive "$1" >/dev/null 2>&1 <&- &
else
log_msg -warn "setsid is not available, running the auto-setup in the foreground."
auto_setup_noninteractive "$1" >/dev/null 2>&1
fi
echo "$QOSMATE_AUTO_SETUP_OUTPUT"
}
health_check() {
# Check if QoSmate is properly configured and running
local status="" errors=0 service_enabled=0 config_status=''
+13 -8
View File
@@ -25,15 +25,21 @@ error_out() { log_msg -err "${@}"; }
# prints each argument to a separate line
print_msg() {
local _arg msgs_dest="/dev/stdout" msgs_prefix=''
# Write to the inherited descriptors instead of redirecting to /dev/stdout: that re-opens the
# target, and when stdout is a regular file '>' truncates it.
local _arg to_stderr='' msgs_prefix=''
for _arg in "$@"
do
case "${_arg}" in
-err) msgs_dest="/dev/stderr" msgs_prefix="Error: " ;;
-warn) msgs_dest="/dev/stderr" msgs_prefix="Warning: " ;;
-err) to_stderr=1 msgs_prefix="Error: " ;;
-warn) to_stderr=1 msgs_prefix="Warning: " ;;
'') printf '\n' ;; # print out empty lines
*)
printf '%s\n' "${msgs_prefix}${_arg}" > "$msgs_dest"
if [ -n "$to_stderr" ]; then
printf '%s\n' "${msgs_prefix}${_arg}" >&2
else
printf '%s\n' "${msgs_prefix}${_arg}"
fi
msgs_prefix=''
esac
done
@@ -43,7 +49,7 @@ print_msg() {
# 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 msgs_prefix='' _arg err_l=info
local IFS="$DEFAULT_IFS"
for _arg in "$@"
@@ -54,10 +60,9 @@ log_msg() {
'') printf '\n' ;; # print out empty lines
*)
case "$err_l" in
err|warn) msgs_dest="/dev/stderr" ;;
*) msgs_dest="/dev/stdout"
err|warn) printf '%s\n' "${msgs_prefix}${_arg}" >&2 ;;
*) printf '%s\n' "${msgs_prefix}${_arg}"
esac
printf '%s\n' "${msgs_prefix}${_arg}" > "$msgs_dest"
logger -t qosmate -p user."$err_l" "${msgs_prefix}${_arg}"
msgs_prefix=''
esac