mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 07:43:56 +08:00
🤞 Sync 2026-09-30 23:44:05
This commit is contained in:
1 parent
e3fde93105
commit
44bb740ab2
206 files changed
+5736
-2799
No files matched your search
@@ -51,8 +51,13 @@ if [ -f "$RULES_FILE" ] && tctl_persist_enabled; then
|
||||
logger -t trafficctl "Failed to restore block for $ip"
|
||||
;;
|
||||
ratelimit)
|
||||
# Records written before the mode was stored have none; they
|
||||
# get the same default the CLI would have given the target, so
|
||||
# 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")
|
||||
[ -n "$param" ] && [ "$param" != "0" ] && \
|
||||
tctl_ratelimit_add "$ip" "$param" "$(tctl_ratelimit_comment "$ip")" 2>/dev/null || \
|
||||
tctl_ratelimit_add "$ip" "$param" "$(tctl_ratelimit_comment "$ip")" "$mode" 2>/dev/null || \
|
||||
logger -t trafficctl "Failed to restore ratelimit for $ip (${param}kbit)"
|
||||
;;
|
||||
pfw_pause|pfw_limit)
|
||||
|
||||
@@ -45,6 +45,7 @@ list)
|
||||
printf '"macfilter_remove":{"ip":"str"},'
|
||||
printf '"ratelimit":{"ip":"str","rate_kbit":"int","label":"str","mode":"str"},'
|
||||
printf '"ratelimit_stats":{},'
|
||||
printf '"subnets":{},'
|
||||
printf '"shape_add":{"ip":"str","rate_kbit":"int","label":"str"},'
|
||||
printf '"shape_remove":{"ip":"str","label":"str"},'
|
||||
printf '"shape_status":{"ip":"str"},'
|
||||
@@ -169,6 +170,11 @@ call)
|
||||
"$SCRIPTS/trafficctl-ratelimit-stats.sh"
|
||||
printf '}'
|
||||
;;
|
||||
subnets)
|
||||
printf '{"result":'
|
||||
"$SCRIPTS/trafficctl-subnets.sh"
|
||||
printf '}'
|
||||
;;
|
||||
shape_add)
|
||||
read -r input
|
||||
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
|
||||
|
||||
@@ -270,6 +270,21 @@ tctl_ratelimit_list() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Which bucket layout a target gets when the caller names no mode.
|
||||
#
|
||||
# A block limited "shared" would let one device starve the rest, so per-device
|
||||
# is the sane default whenever the target covers more than one address. Lives
|
||||
# here rather than in trafficctl-ratelimit.sh because the reboot-restore hook
|
||||
# has to reach the same answer for a record written before modes were stored —
|
||||
# two copies of this rule would mean a limit that changes meaning on reboot.
|
||||
tctl_ratelimit_default_mode() {
|
||||
case "$1" in
|
||||
*/32) echo "shared" ;; # a /32 is one host; both modes are identical
|
||||
*/*) echo "each" ;; # any wider block: per-device buckets
|
||||
*) echo "shared" ;; # bare host address
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Internet Blocking ──────────────────────────────────────────────────────
|
||||
|
||||
# A block is two rules: the address-keyed one for IPv4, and a MAC-keyed one
|
||||
@@ -822,12 +837,17 @@ tctl_persist_enabled() {
|
||||
}
|
||||
|
||||
tctl_persist_save() {
|
||||
local type="$1" ip="$2" param="$3"
|
||||
local type="$1" ip="$2" param="$3" mode="$4"
|
||||
[ -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.
|
||||
local extra=""
|
||||
[ -n "$mode" ] && extra=",\"mode\":\"$mode\""
|
||||
# Remove existing entry for same ip+type, append new one
|
||||
awk -v ip="$ip" -v t="$type" -v p="$param" '
|
||||
awk -v ip="$ip" -v t="$type" -v p="$param" -v x="$extra" '
|
||||
{
|
||||
gsub(/^\[/,""); gsub(/\]$/,"")
|
||||
n=split($0, items, "},{")
|
||||
@@ -841,7 +861,7 @@ tctl_persist_save() {
|
||||
first=0
|
||||
}
|
||||
if (!first) printf ","
|
||||
printf "{\"type\":\"%s\",\"ip\":\"%s\",\"param\":\"%s\"}]", t, ip, p
|
||||
printf "{\"type\":\"%s\",\"ip\":\"%s\",\"param\":\"%s\"%s}]", t, ip, p, x
|
||||
}' "$TCTL_RULES_FILE" > "$tmp"
|
||||
mv "$tmp" "$TCTL_RULES_FILE"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=dash
|
||||
# Show active rate-limit statistics.
|
||||
# Output: JSON array [{"ip":"...","rate_kbit":N,"packets":N,"bytes":N,"pass_packets":0,"pass_bytes":0}]
|
||||
# Output: JSON array
|
||||
# [{"ip":"...","mode":"each|shared","rate_kbit":N,"packets":N,"bytes":N,
|
||||
# "pass_packets":0,"pass_bytes":0}]
|
||||
#
|
||||
# One entry per target, counters summed across chains: the download rule is
|
||||
# installed on every LAN device's egress chain, so a router with three bridges
|
||||
# holds three copies of the same limit. Only the chain on the device the target
|
||||
# actually lives behind ever matches, but which copy that is cannot be known
|
||||
# here — taking any single one at random reported zero drops for most targets.
|
||||
|
||||
. /usr/local/bin/trafficctl-fw.sh
|
||||
|
||||
@@ -13,7 +21,13 @@ if [ "$TCTL_FW" = "nft" ]; then
|
||||
packets = 0
|
||||
bytes = 0
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i == "daddr" && i < NF) ip = $(i+1)
|
||||
# FIRST daddr only. In "each" mode the rule reads
|
||||
# ip daddr <target> meter m { ip daddr limit rate over ... }
|
||||
# and the meter key is a bare "ip daddr" with no operand, so
|
||||
# overwriting on every match reported the target as "limit" —
|
||||
# every per-device subnet limit was invisible to this output, to
|
||||
# the dashboard, and to tctl_has_limit.
|
||||
if (ip == "" && $i == "daddr" && i < NF && $(i+1) ~ /^[0-9]/) ip = $(i+1)
|
||||
if ($i == "rate" && $(i+1) == "over") {
|
||||
val = $(i+2)
|
||||
gsub(/[^0-9]/, "", val)
|
||||
@@ -27,15 +41,29 @@ if [ "$TCTL_FW" = "nft" ]; then
|
||||
}
|
||||
}
|
||||
if (ip != "") {
|
||||
if (first) printf ","
|
||||
printf "{\"ip\":\"%s\",\"rate_kbit\":%d,\"packets\":%.0f,\"bytes\":%.0f,\"pass_packets\":0,\"pass_bytes\":0}", ip, rate, packets, bytes
|
||||
first = 1
|
||||
if (!(ip in seen)) { seen[ip] = 1; order[++n] = ip }
|
||||
# A meter keyed on the address is what makes the bucket per-device.
|
||||
mode[ip] = (index($0, " meter ") > 0) ? "each" : "shared"
|
||||
rate_of[ip] = rate
|
||||
pkts[ip] += packets
|
||||
byts[ip] += bytes
|
||||
}
|
||||
}
|
||||
BEGIN { printf "["; first = 0 }
|
||||
END { printf "]\n" }
|
||||
END {
|
||||
printf "["
|
||||
for (i = 1; i <= n; i++) {
|
||||
ip = order[i]
|
||||
if (i > 1) printf ","
|
||||
printf "{\"ip\":\"%s\",\"mode\":\"%s\",\"rate_kbit\":%d,\"packets\":%.0f,\"bytes\":%.0f,\"pass_packets\":0,\"pass_bytes\":0}", \
|
||||
ip, mode[ip], rate_of[ip], pkts[ip], byts[ip]
|
||||
}
|
||||
printf "]\n"
|
||||
}
|
||||
'
|
||||
else
|
||||
# hashlimit is keyed on dstip whatever mode was asked for, so on this path
|
||||
# every bucket is per-address — "each" is the honest answer, not the
|
||||
# requested mode. Documented in docs/API.md: shared needs nftables.
|
||||
iptables -t mangle -L FORWARD -nvx 2>/dev/null | grep "rl_ratelimit" | awk '
|
||||
{
|
||||
packets = $1
|
||||
@@ -57,12 +85,21 @@ else
|
||||
}
|
||||
}
|
||||
if (ip != "" && ip != "0.0.0.0/0") {
|
||||
if (first) printf ","
|
||||
printf "{\"ip\":\"%s\",\"rate_kbit\":%d,\"packets\":%.0f,\"bytes\":%.0f,\"pass_packets\":0,\"pass_bytes\":0}", ip, rate, packets, bytes
|
||||
first = 1
|
||||
if (!(ip in seen)) { seen[ip] = 1; order[++n] = ip }
|
||||
rate_of[ip] = rate
|
||||
pkts[ip] += packets
|
||||
byts[ip] += bytes
|
||||
}
|
||||
}
|
||||
BEGIN { printf "["; first = 0 }
|
||||
END { printf "]\n" }
|
||||
END {
|
||||
printf "["
|
||||
for (i = 1; i <= n; i++) {
|
||||
ip = order[i]
|
||||
if (i > 1) printf ","
|
||||
printf "{\"ip\":\"%s\",\"mode\":\"each\",\"rate_kbit\":%d,\"packets\":%.0f,\"bytes\":%.0f,\"pass_packets\":0,\"pass_bytes\":0}", \
|
||||
ip, rate_of[ip], pkts[ip], byts[ip]
|
||||
}
|
||||
printf "]\n"
|
||||
}
|
||||
'
|
||||
fi
|
||||
@@ -26,14 +26,8 @@ TARGET=$(tctl_validate_target "$IP") || {
|
||||
}
|
||||
IP="$TARGET"
|
||||
|
||||
# A block limited "shared" would let one device starve the rest, so per-device
|
||||
# is the sane default whenever the target covers more than one address.
|
||||
if [ -z "$MODE" ]; then
|
||||
case "$IP" in
|
||||
*/32) MODE="shared" ;; # a /32 is one host; both modes are identical
|
||||
*/*) MODE="each" ;; # any wider block: per-device buckets
|
||||
*) MODE="shared" ;; # bare host address
|
||||
esac
|
||||
MODE=$(tctl_ratelimit_default_mode "$IP")
|
||||
fi
|
||||
case "$MODE" in
|
||||
each|shared) ;;
|
||||
@@ -78,7 +72,10 @@ else
|
||||
echo "{\"ok\":false,\"msg\":\"failed to set rate limit for $IP (no usable WAN or LAN ingress device)\"}"
|
||||
exit 1
|
||||
fi
|
||||
tctl_persist_enabled && tctl_persist_save "ratelimit" "$IP" "$RATE"
|
||||
# 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}"
|
||||
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\"}"
|
||||
|
||||
@@ -244,7 +244,15 @@ do_add() {
|
||||
|
||||
# Download: queue on the LAN device, matching traffic destined to the client.
|
||||
if ! shape_attach "$LAN_DEV" dst "$IP" "$classid" "$RATE" "$burst_bytes"; then
|
||||
echo "{\"ok\":false,\"msg\":\"tc setup failed for $IP on $LAN_DEV\"}"
|
||||
# Naming the cause matters here: declining to replace a foreign root
|
||||
# qdisc is deliberate (it is how an SQM/cake setup survives this app),
|
||||
# but "tc setup failed" reads as a bug and gives no way to act on it.
|
||||
# The rate limiter is a policer, owns no qdisc, and works alongside SQM.
|
||||
if ! root_qdisc_replaceable "$LAN_DEV"; then
|
||||
echo "{\"ok\":false,\"msg\":\"shaping declined on $LAN_DEV: another QoS setup (SQM/cake or a custom HTB tree) owns the root qdisc and will not be torn down. Use the limiter instead — it is a policer and coexists with SQM.\"}"
|
||||
else
|
||||
echo "{\"ok\":false,\"msg\":\"tc setup failed for $IP on $LAN_DEV\"}"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=dash
|
||||
# List the subnets this router monitors, as CIDRs the dashboard can offer as
|
||||
# rate-limit targets.
|
||||
#
|
||||
# Output: JSON array
|
||||
# [{"cidr":"192.168.20.0/24","device":"br-guest","kind":"lan"},
|
||||
# {"cidr":"10.0.5.0/24","device":"br-lan","kind":"routed"}]
|
||||
#
|
||||
# kind is "lan" for a directly connected subnet and "routed" for one reached
|
||||
# via a next-hop on a LAN device (or listed in trafficctl.main.extra_subnets).
|
||||
#
|
||||
# This list is exactly the set a limit can be ENFORCED on: the netdev hooks
|
||||
# that police traffic are attached to the devices these subnets resolve to, so
|
||||
# a subnet missing from here (a masquerading non-lan zone, for instance) would
|
||||
# take a rule that never matches a packet. The dashboard warns off such a
|
||||
# target instead of pretending it applied.
|
||||
|
||||
. /usr/local/bin/trafficctl-fw.sh
|
||||
|
||||
tctl_monitored_subnets | awk '
|
||||
# Octets are derived with arithmetic and printed individually: each is below
|
||||
# 256, while the packed address is up to 2^32-1 and busybox awk formats "%d"
|
||||
# through a 32-bit int.
|
||||
function ip2s(v, o1, o2, o3, o4, rem) {
|
||||
o1 = int(v / 16777216); rem = v - o1 * 16777216
|
||||
o2 = int(rem / 65536); rem = rem - o2 * 65536
|
||||
o3 = int(rem / 256)
|
||||
o4 = rem - o3 * 256
|
||||
return sprintf("%d.%d.%d.%d", o1, o2, o3, o4)
|
||||
}
|
||||
{
|
||||
base = $2 + 0
|
||||
block = $3 + 0
|
||||
if (block < 1) next
|
||||
# block is a power of two (1 << (32 - mask)); count it back down rather
|
||||
# than calling log(), which awk only has in floating point.
|
||||
mask = 32
|
||||
b = block
|
||||
while (b > 1) { b = b / 2; mask-- }
|
||||
# router_int is 0 for a subnet that has no local address inside it, which
|
||||
# is what tells a routed subnet from a connected one.
|
||||
# Written long-hand: the bashism linter reads the whole file, and
|
||||
# `name = (...)` at the start of a line looks like a shell array to it.
|
||||
kind = "lan"
|
||||
if ($4 + 0 == 0) { kind = "routed" }
|
||||
if (n++) printf ","
|
||||
else printf "["
|
||||
printf "{\"cidr\":\"%s/%d\",\"device\":\"%s\",\"kind\":\"%s\"}", ip2s(base), mask, $1, kind
|
||||
}
|
||||
END { if (!n) printf "["; printf "]\n" }
|
||||
'
|
||||
@@ -6,7 +6,7 @@
|
||||
"luci.trafficctl": [
|
||||
"version",
|
||||
"summary", "device", "bytes", "ifaces",
|
||||
"ratelimit_stats", "shape_stats", "rdns",
|
||||
"ratelimit_stats", "shape_stats", "rdns", "subnets",
|
||||
"portfw_list", "names_list", "netify_status", "netify_list",
|
||||
"shape_status",
|
||||
"config_get", "telegram_config_get",
|
||||
|
||||
Reference in new issue
Block a user