mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 14:03:56 +08:00
🌈 Sync 2026-10-01 13:09:37
This commit is contained in:
1 parent
dceb6de0e4
commit
889951b6d0
116 files changed
+6376
-1093
No files matched your search
@@ -27,8 +27,13 @@ done
|
||||
if [ -f "$SHAPES_FILE" ]; then
|
||||
jsonfilter -i "$SHAPES_FILE" -e '@[*].ip' 2>/dev/null | while read -r ip; do
|
||||
rate=$(jsonfilter -i "$SHAPES_FILE" -e "@[@.ip='$ip'].rate_kbit" 2>/dev/null)
|
||||
# Absent on symmetric records, which is all of them before asymmetric
|
||||
# shaping existed; trafficctl-shape.sh reads empty as "same as
|
||||
# download". The label has to be passed explicitly because the upload
|
||||
# rate is the fifth positional.
|
||||
rate_up=$(jsonfilter -i "$SHAPES_FILE" -e "@[@.ip='$ip'].rate_kbit_up" 2>/dev/null)
|
||||
if [ -n "$rate" ] && [ "$rate" != "0" ]; then
|
||||
/usr/local/bin/trafficctl-shape.sh add "$ip" "$rate" || \
|
||||
/usr/local/bin/trafficctl-shape.sh add "$ip" "$rate" "shape_$ip" "$rate_up" || \
|
||||
logger -t trafficctl "Failed to restore shape for $ip (${rate}kbit)"
|
||||
fi
|
||||
done
|
||||
@@ -56,8 +61,13 @@ if [ -f "$RULES_FILE" ] && tctl_persist_enabled; then
|
||||
# a subnet does not change meaning across the upgrade.
|
||||
mode=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].mode" 2>/dev/null)
|
||||
[ -z "$mode" ] && mode=$(tctl_ratelimit_default_mode "$ip")
|
||||
# Absent on every record written before the directions could
|
||||
# differ, and on every symmetric one since. tctl_ratelimit_add
|
||||
# reads that as "same as download", which is what those records
|
||||
# have always meant.
|
||||
param_up=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].param_up" 2>/dev/null)
|
||||
[ -n "$param" ] && [ "$param" != "0" ] && \
|
||||
tctl_ratelimit_add "$ip" "$param" "$(tctl_ratelimit_comment "$ip")" "$mode" 2>/dev/null || \
|
||||
tctl_ratelimit_add "$ip" "$param" "$(tctl_ratelimit_comment "$ip")" "$mode" "$param_up" 2>/dev/null || \
|
||||
logger -t trafficctl "Failed to restore ratelimit for $ip (${param}kbit)"
|
||||
;;
|
||||
pfw_pause|pfw_limit)
|
||||
|
||||
@@ -43,10 +43,10 @@ list)
|
||||
printf '"unblock":{"ip":"str","label":"str"},'
|
||||
printf '"macfilter_add":{"ip":"str"},'
|
||||
printf '"macfilter_remove":{"ip":"str"},'
|
||||
printf '"ratelimit":{"ip":"str","rate_kbit":"int","label":"str","mode":"str"},'
|
||||
printf '"ratelimit":{"ip":"str","rate_kbit":"int","label":"str","mode":"str","rate_kbit_up":"int"},'
|
||||
printf '"ratelimit_stats":{},'
|
||||
printf '"subnets":{},'
|
||||
printf '"shape_add":{"ip":"str","rate_kbit":"int","label":"str"},'
|
||||
printf '"shape_add":{"ip":"str","rate_kbit":"int","label":"str","rate_kbit_up":"int"},'
|
||||
printf '"shape_remove":{"ip":"str","label":"str"},'
|
||||
printf '"shape_status":{"ip":"str"},'
|
||||
printf '"shape_stats":{},'
|
||||
@@ -159,10 +159,16 @@ call)
|
||||
label=$(sanitize_label "$(echo "$input" | jsonfilter -e '@.label' 2>/dev/null)")
|
||||
mode=$(echo "$input" | jsonfilter -e '@.mode' 2>/dev/null)
|
||||
case "$mode" in each|shared) ;; *) mode="" ;; esac
|
||||
rate_up=$(echo "$input" | jsonfilter -e '@.rate_kbit_up' 2>/dev/null)
|
||||
# A caller that does not send the field — any UI older than this
|
||||
# method signature — means "symmetric", NOT "no upload allowed". Zero
|
||||
# is normalised away for the same reason: a zero ceiling polices every
|
||||
# packet, which is a total upload block reporting itself as a limit.
|
||||
[ "$rate_up" = "0" ] && rate_up=""
|
||||
if [ -z "$ip" ]; then
|
||||
echo '{"ok":false,"msg":"ip required"}'
|
||||
else
|
||||
"$SCRIPTS/trafficctl-ratelimit.sh" "$ip" "${rate:-0}" "$label" "$mode"
|
||||
"$SCRIPTS/trafficctl-ratelimit.sh" "$ip" "${rate:-0}" "$label" "$mode" "$rate_up"
|
||||
fi
|
||||
;;
|
||||
ratelimit_stats)
|
||||
@@ -180,10 +186,13 @@ call)
|
||||
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
|
||||
rate=$(echo "$input" | jsonfilter -e '@.rate_kbit' 2>/dev/null)
|
||||
label=$(sanitize_label "$(echo "$input" | jsonfilter -e '@.label' 2>/dev/null)")
|
||||
rate_up=$(echo "$input" | jsonfilter -e '@.rate_kbit_up' 2>/dev/null)
|
||||
# Same reading as the limiter: absent or zero means "same as download".
|
||||
[ "$rate_up" = "0" ] && rate_up=""
|
||||
if [ -z "$ip" ] || [ -z "$rate" ]; then
|
||||
echo '{"ok":false,"msg":"ip and rate_kbit required"}'
|
||||
else
|
||||
"$SCRIPTS/trafficctl-shape.sh" add "$ip" "$rate" "$label"
|
||||
"$SCRIPTS/trafficctl-shape.sh" add "$ip" "$rate" "$label" "$rate_up"
|
||||
fi
|
||||
;;
|
||||
shape_remove)
|
||||
|
||||
@@ -119,18 +119,26 @@ tctl_conntrack_flush_v6() {
|
||||
# meter keyed by address); "shared" makes the whole target share one bucket.
|
||||
# For a single host the two are identical.
|
||||
tctl_ratelimit_add() {
|
||||
local ip="$1" rate_kbit="$2" comment="$3" mode="${4:-shared}"
|
||||
local ip="$1" rate_kbit="$2" comment="$3" mode="${4:-shared}" rate_kbit_up="$5"
|
||||
# Empty means "same as download" — the only meaning this had before the two
|
||||
# directions could differ. Not zero: a zero ceiling policed every packet and
|
||||
# would be a silent total block reported as a successful limit.
|
||||
[ -z "$rate_kbit_up" ] && rate_kbit_up="$rate_kbit"
|
||||
local rate_kbyte=$((rate_kbit / 8))
|
||||
[ "$rate_kbyte" -lt 1 ] && rate_kbyte=1
|
||||
local rate_kbyte_up=$((rate_kbit_up / 8))
|
||||
[ "$rate_kbyte_up" -lt 1 ] && rate_kbyte_up=1
|
||||
|
||||
local slug dl_expr ul_expr ul6_expr mac
|
||||
slug=$(tctl_target_slug "$ip")
|
||||
# The two directions were already separate rules on separate hooks — they
|
||||
# were symmetric only because both took the same number.
|
||||
if [ "$mode" = "each" ]; then
|
||||
dl_expr="ip daddr $ip meter tctl_d_$slug { ip daddr limit rate over ${rate_kbyte} kbytes/second }"
|
||||
ul_expr="ip saddr $ip meter tctl_u_$slug { ip saddr limit rate over ${rate_kbyte} kbytes/second }"
|
||||
ul_expr="ip saddr $ip meter tctl_u_$slug { ip saddr limit rate over ${rate_kbyte_up} kbytes/second }"
|
||||
else
|
||||
dl_expr="ip daddr $ip limit rate over ${rate_kbyte} kbytes/second"
|
||||
ul_expr="ip saddr $ip limit rate over ${rate_kbyte} kbytes/second"
|
||||
ul_expr="ip saddr $ip limit rate over ${rate_kbyte_up} kbytes/second"
|
||||
fi
|
||||
|
||||
# Upload over IPv6, keyed on the MAC. This direction and only this one:
|
||||
@@ -155,7 +163,7 @@ tctl_ratelimit_add() {
|
||||
# the wrong one does not silently under-match — it fails to load.
|
||||
ul6_expr=""
|
||||
if mac=$(tctl_target_mac "$ip"); then
|
||||
ul6_expr="meta protocol ip6 ether saddr $mac limit rate over ${rate_kbyte} kbytes/second"
|
||||
ul6_expr="meta protocol ip6 ether saddr $mac limit rate over ${rate_kbyte_up} kbytes/second"
|
||||
fi
|
||||
|
||||
if [ "$TCTL_FW" = "nft" ]; then
|
||||
@@ -837,15 +845,18 @@ tctl_persist_enabled() {
|
||||
}
|
||||
|
||||
tctl_persist_save() {
|
||||
local type="$1" ip="$2" param="$3" mode="$4"
|
||||
local type="$1" ip="$2" param="$3" mode="$4" param_up="$5"
|
||||
[ -d "$(dirname "$TCTL_RULES_FILE")" ] || mkdir -p "$(dirname "$TCTL_RULES_FILE")"
|
||||
[ -f "$TCTL_RULES_FILE" ] || echo '[]' > "$TCTL_RULES_FILE"
|
||||
local tmp="${TCTL_RULES_FILE}.tmp"
|
||||
# The mode field is written only when the caller has one, so records for
|
||||
# rule types that have no bucket layout (blocks, port forwards) keep their
|
||||
# exact previous shape.
|
||||
# exact previous shape. param_up follows the same rule: the caller passes it
|
||||
# only for an asymmetric limit, so a symmetric one still writes the record
|
||||
# it always wrote and an older package can still read it back.
|
||||
local extra=""
|
||||
[ -n "$mode" ] && extra=",\"mode\":\"$mode\""
|
||||
[ -n "$param_up" ] && extra="$extra,\"param_up\":\"$param_up\""
|
||||
# Remove existing entry for same ip+type, append new one
|
||||
awk -v ip="$ip" -v t="$type" -v p="$param" -v x="$extra" '
|
||||
{
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
# shellcheck shell=dash
|
||||
# Rate-limit bandwidth in both directions (policer).
|
||||
#
|
||||
# Usage: trafficctl-ratelimit.sh <target> <rate_kbit> [label] [mode]
|
||||
# Usage: trafficctl-ratelimit.sh <target> <rate_kbit> [label] [mode] [rate_up]
|
||||
# target a host (10.0.20.122), a CIDR (10.0.20.0/24), or "all"
|
||||
# mode each — every address in the target gets its own bucket (default
|
||||
# for CIDR/all, so "5 Mbit each" means per device)
|
||||
# shared — the whole target shares one bucket (aggregate cap)
|
||||
# rate_up upload ceiling in kbit/s; omitted means the same as rate_kbit,
|
||||
# which is what this did before the directions could differ
|
||||
# rate_kbit=0 removes the limit.
|
||||
|
||||
. /usr/local/bin/trafficctl-fw.sh
|
||||
@@ -14,6 +16,11 @@
|
||||
IP="$1"
|
||||
RATE="$2"
|
||||
MODE="$4"
|
||||
# Fifth positional so every existing caller keeps working unchanged. Empty is
|
||||
# "same as download", never zero — a zero ceiling policed every packet, which
|
||||
# is a total block dressed up as a limit.
|
||||
RATE_UP="$5"
|
||||
[ -z "$RATE_UP" ] && RATE_UP="$RATE"
|
||||
|
||||
if [ -z "$IP" ] || [ -z "$RATE" ]; then
|
||||
echo '{"ok":false,"msg":"usage: trafficctl-ratelimit.sh <target> <rate_kbit> [label] [each|shared]"}'
|
||||
@@ -50,11 +57,20 @@ if [ "$RATE" = "0" ]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# How the applied ceiling is spelled out. Naming one number when the
|
||||
# operator gave two would hide exactly the thing they asked for.
|
||||
rate_text() {
|
||||
if [ "$RATE_UP" != "$RATE" ]; then
|
||||
echo "${RATE} kbit/s down, ${RATE_UP} kbit/s up"
|
||||
else
|
||||
echo "${RATE} kbit/s"
|
||||
fi
|
||||
}
|
||||
tctl_ratelimit_remove "$IP" "$COMMENT" 2>/dev/null
|
||||
TCTL_RL_DOWNLOAD_FAILED=0
|
||||
TCTL_RL_UPLOAD_FAILED=0
|
||||
TCTL_RL_UPLOAD6_OK=0
|
||||
tctl_ratelimit_add "$IP" "$RATE" "$COMMENT" "$MODE"
|
||||
tctl_ratelimit_add "$IP" "$RATE" "$COMMENT" "$MODE" "$RATE_UP"
|
||||
|
||||
# IPv6 coverage is partial by construction and the operator has to know
|
||||
# which part: upload is policed on the MAC, download is not policed at all
|
||||
@@ -75,13 +91,16 @@ else
|
||||
# The mode is persisted with the rate. Without it the restore hook fell back
|
||||
# to tctl_ratelimit_add's own default ("shared"), so a subnet limited
|
||||
# "5 Mbit each" came back after a reboot as 5 Mbit for the entire subnet.
|
||||
tctl_persist_enabled && tctl_persist_save "ratelimit" "$IP" "$RATE" "$MODE"
|
||||
tctl_log "ratelimit_set" "$IP" "${RATE}kbit" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
||||
# The upload rate is persisted alongside, and only when it differs — a
|
||||
# symmetric limit keeps producing exactly the record it always did.
|
||||
tctl_persist_enabled && tctl_persist_save "ratelimit" "$IP" "$RATE" "$MODE" \
|
||||
"$([ "$RATE_UP" != "$RATE" ] && echo "$RATE_UP")"
|
||||
tctl_log "ratelimit_set" "$IP" "$(rate_text)" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
||||
if [ "$TCTL_RL_DOWNLOAD_FAILED" = "1" ]; then
|
||||
echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"msg\":\"rate limit ${RATE} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE\"}"
|
||||
echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE\"}"
|
||||
elif [ "$TCTL_RL_UPLOAD_FAILED" = "1" ]; then
|
||||
echo "{\"ok\":true,\"ipv6_upload\":false,\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]\"}"
|
||||
else
|
||||
echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"msg\":\"rate limit ${RATE} kbit/s for $IP (both directions, $MODE)$V6NOTE\"}"
|
||||
echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE\"}"
|
||||
fi
|
||||
fi
|
||||
@@ -24,6 +24,13 @@ IP="$2"
|
||||
RATE="$3"
|
||||
# shellcheck disable=SC2034
|
||||
LABEL="${4:-shape_$IP}"
|
||||
# Upload ceiling. Appended as a fifth positional so every existing caller —
|
||||
# the rpcd backend, the Telegram bot, the DHCP new-device hook and the boot
|
||||
# restore — keeps working unchanged. Empty means "same as download", which is
|
||||
# what shaping did before this existed; it is deliberately NOT treated as zero,
|
||||
# since a zero ceiling is a total upload block that would report success.
|
||||
RATE_UP="$5"
|
||||
[ -z "$RATE_UP" ] && RATE_UP="$RATE"
|
||||
|
||||
# A classid minor is 16 bit and two values are already taken — 1:1 is the root
|
||||
# class and 1:fffe the default one — so a minor cannot be derived from the
|
||||
@@ -41,8 +48,14 @@ re_quote() {
|
||||
echo "$1" | sed 's/[].[^$*\/]/\\&/g'
|
||||
}
|
||||
|
||||
# Records are extracted by pattern, not parsed, so this expression decides what
|
||||
# counts as a record at all. A record it does not match is not merely misread —
|
||||
# it disappears from used_minors(), alloc_classid hands its minor to another
|
||||
# device, and the two then share an HTB class: removing one tears down the
|
||||
# other. The optional rate_kbit_up group is therefore at the TAIL, and every
|
||||
# reader below tolerates both the old three-field form and the new one.
|
||||
shapes_entries() {
|
||||
grep -oE '\{"ip":"[^"]+","rate_kbit":[0-9]+(,"classid":"1:[0-9a-f]+")?\}' \
|
||||
grep -oE '\{"ip":"[^"]+","rate_kbit":[0-9]+(,"classid":"1:[0-9a-f]+")?(,"rate_kbit_up":[0-9]+)?\}' \
|
||||
"$SHAPES_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
@@ -52,7 +65,19 @@ lookup_classid() {
|
||||
ip="$1"
|
||||
esc=$(re_quote "$ip")
|
||||
shapes_entries \
|
||||
| sed -n "s/^{\"ip\":\"$esc\",\"rate_kbit\":[0-9]*,\"classid\":\"\(1:[0-9a-f]*\)\"}$/\1/p" \
|
||||
| sed -n "s/^{\"ip\":\"$esc\",\"rate_kbit\":[0-9]*,\"classid\":\"\(1:[0-9a-f]*\)\"\(,\"rate_kbit_up\":[0-9]*\)\{0,1\}}$/\1/p" \
|
||||
| head -1
|
||||
}
|
||||
|
||||
# Upload ceiling recorded for this address. Empty means the record predates
|
||||
# asymmetric shaping, or was written symmetric — both mean "same as download",
|
||||
# which is what every shape did before this existed.
|
||||
lookup_rate_up() {
|
||||
local ip esc
|
||||
ip="$1"
|
||||
esc=$(re_quote "$ip")
|
||||
shapes_entries \
|
||||
| sed -n "s/^{\"ip\":\"$esc\",\"rate_kbit\":[0-9]*,\"classid\":\"1:[0-9a-f]*\",\"rate_kbit_up\":\([0-9]*\)}$/\1/p" \
|
||||
| head -1
|
||||
}
|
||||
|
||||
@@ -179,7 +204,7 @@ shape_detach() {
|
||||
}
|
||||
|
||||
save_shape() {
|
||||
local ip="$1" rate="$2" classid="$3"
|
||||
local ip="$1" rate="$2" classid="$3" rate_up="$4"
|
||||
mkdir -p "$(dirname "$SHAPES_FILE")"
|
||||
[ ! -f "$SHAPES_FILE" ] && echo "[]" > "$SHAPES_FILE"
|
||||
|
||||
@@ -209,7 +234,12 @@ save_shape() {
|
||||
local filtered
|
||||
filtered=$(echo "$old_entries" | grep -v "^{\"ip\":\"$esc\"," || true)
|
||||
if [ "$rate" -gt 0 ] 2>/dev/null; then
|
||||
local entry="{\"ip\":\"$ip\",\"rate_kbit\":$rate,\"classid\":\"$classid\"}"
|
||||
# rate_kbit_up is written only when the two directions differ, so a
|
||||
# symmetric shape still produces exactly the record it always did.
|
||||
# Downgrading the package then keeps working for the common case.
|
||||
local up_field=""
|
||||
[ -n "$rate_up" ] && [ "$rate_up" != "$rate" ] && up_field=",\"rate_kbit_up\":$rate_up"
|
||||
local entry="{\"ip\":\"$ip\",\"rate_kbit\":$rate,\"classid\":\"$classid\"$up_field}"
|
||||
if [ -n "$filtered" ]; then
|
||||
filtered=$(printf "%s\n%s" "$filtered" "$entry")
|
||||
else
|
||||
@@ -258,16 +288,27 @@ do_add() {
|
||||
|
||||
# Upload: shaped pre-NAT on the IFB device fed from LAN ingress.
|
||||
# Reported separately so a missing kmod-ifb doesn't look like total failure.
|
||||
# The two directions are already separate HTB classes on separate devices,
|
||||
# so giving them separate rates costs nothing beyond its own burst.
|
||||
local ul_burst
|
||||
ul_burst=$(( RATE_UP * 125 / 100 ))
|
||||
[ "$ul_burst" -lt 1600 ] && ul_burst=1600
|
||||
local ul_ok=1
|
||||
if ensure_ifb; then
|
||||
shape_attach "$IFB_DEV" src "$IP" "$classid" "$RATE" "$burst_bytes" || ul_ok=0
|
||||
shape_attach "$IFB_DEV" src "$IP" "$classid" "$RATE_UP" "$ul_burst" || ul_ok=0
|
||||
else
|
||||
ul_ok=0
|
||||
fi
|
||||
|
||||
save_shape "$IP" "$RATE" "$classid"
|
||||
save_shape "$IP" "$RATE" "$classid" "$RATE_UP"
|
||||
local rates="${RATE} kbit/s"
|
||||
[ "$RATE_UP" != "$RATE" ] && rates="${RATE} kbit/s down, ${RATE_UP} kbit/s up"
|
||||
if [ "$ul_ok" = "1" ]; then
|
||||
echo "{\"ok\":true,\"msg\":\"shape ${RATE} kbit/s applied to $IP both directions (class $classid)\"}"
|
||||
echo "{\"ok\":true,\"msg\":\"shape $rates applied to $IP both directions (class $classid)\"}"
|
||||
elif [ "$RATE_UP" != "$RATE" ]; then
|
||||
# Saying "applied" here would be a lie about the half the operator
|
||||
# explicitly asked to differ.
|
||||
echo "{\"ok\":true,\"msg\":\"shape ${RATE} kbit/s applied to $IP for download only — the ${RATE_UP} kbit/s upload ceiling was NOT applied, it needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched\"}"
|
||||
else
|
||||
echo "{\"ok\":true,\"msg\":\"shape ${RATE} kbit/s applied to $IP (download only — upload needs kmod-ifb: opkg/apk install kmod-ifb kmod-sched)\"}"
|
||||
fi
|
||||
|
||||
Reference in new issue
Block a user