feat: Replace SQM with per-client speed control for paid WiFi upgrade

Add parahub-speed-control script (nftables set + tc HTB) for per-IP
speed shaping. Free tier 512kbps, paid tier unlimited. Heartbeat now
parses paid_clients from API response and syncs nftables set.

Replaced sqm-scripts/kmod-sched-cake/luci-app-sqm packages with
tc-full/kmod-ifb/kmod-sched-htb. Section 8 of uci-defaults creates
init.d service for speed control instead of SQM config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 22:35:16 +00:00
parent 968f90611e
commit ef1b9c10ea
4 changed files with 112 additions and 22 deletions

View File

@@ -525,22 +525,26 @@ fi
uci commit dhcp uci commit dhcp
# ============================================================================ # ============================================================================
# 8. SQM TRAFFIC SHAPING (Bumblebee only — guest 512 kbps limit) # 8. SPEED CONTROL (Bumblebee only — per-client shaping, paid Lightning upgrade)
# ============================================================================ # ============================================================================
if [ "$ROLE" != "bee" ]; then if [ "$ROLE" != "bee" ]; then
uci batch <<-SQM_EOF chmod +x /usr/bin/parahub-speed-control
set sqm.guest=queue
set sqm.guest.enabled='1' # Create init.d service for speed control
set sqm.guest.interface='br-guest' cat > /etc/init.d/parahub-speed <<'INITEOF'
set sqm.guest.download='512' #!/bin/sh /etc/rc.common
set sqm.guest.upload='512' START=99
set sqm.guest.qdisc='cake' start() { /usr/bin/parahub-speed-control init; }
set sqm.guest.script='piece_of_cake.qos' stop() {
set sqm.guest.linklayer='ethernet' tc qdisc del dev br-guest root 2>/dev/null
set sqm.guest.overhead='44' tc qdisc del dev br-guest ingress 2>/dev/null
SQM_EOF ip link del ifb-guest 2>/dev/null
uci commit sqm nft delete table inet parahub 2>/dev/null
}
INITEOF
chmod +x /etc/init.d/parahub-speed
/etc/init.d/parahub-speed enable
fi fi
# ============================================================================ # ============================================================================

View File

@@ -34,26 +34,38 @@ esac
PAYLOAD="{\"mac\":\"${MAC}\",\"hostname\":\"${HOSTNAME}\",\"yggdrasil_address\":\"${YGG_ADDR}\",\"firmware_version\":\"25.12.0-rc4\",\"hardware_profile\":\"${HW}\",\"uptime\":${UPTIME},\"private_ssid\":\"${SSID}\",\"firmware_role\":\"${ROLE}\",\"mesh_ip\":\"${MESH_IP}\"}" PAYLOAD="{\"mac\":\"${MAC}\",\"hostname\":\"${HOSTNAME}\",\"yggdrasil_address\":\"${YGG_ADDR}\",\"firmware_version\":\"25.12.0-rc4\",\"hardware_profile\":\"${HW}\",\"uptime\":${UPTIME},\"private_ssid\":\"${SSID}\",\"firmware_role\":\"${ROLE}\",\"mesh_ip\":\"${MESH_IP}\"}"
RESPONSE=""
if [ "$ROLE" = "bee" ]; then if [ "$ROLE" = "bee" ]; then
# Bee: no yggdrasil, use public URL only # Bee: no yggdrasil, use public URL only
curl -s -m 10 -X POST \ RESPONSE=$(curl -s -m 10 -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer ${HEARTBEAT_KEY}" \ -H "Authorization: Bearer ${HEARTBEAT_KEY}" \
-d "$PAYLOAD" \ -d "$PAYLOAD" \
"${PARAHUB_API_PUBLIC}" >/dev/null 2>&1 "${PARAHUB_API_PUBLIC}" 2>/dev/null)
else else
# Bumblebee: try yggdrasil first, fallback to public # Bumblebee: try yggdrasil first, fallback to public
if ping6 -c 1 -W 3 200:abb9:5810:37d3:8a4c:98a6:b82b:969a >/dev/null 2>&1; then if ping6 -c 1 -W 3 200:abb9:5810:37d3:8a4c:98a6:b82b:969a >/dev/null 2>&1; then
curl -s -m 10 -X POST \ RESPONSE=$(curl -s -m 10 -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer ${HEARTBEAT_KEY}" \ -H "Authorization: Bearer ${HEARTBEAT_KEY}" \
-d "$PAYLOAD" \ -d "$PAYLOAD" \
"${PARAHUB_API_YGG}" >/dev/null 2>&1 "${PARAHUB_API_YGG}" 2>/dev/null)
else else
curl -s -m 10 -X POST \ RESPONSE=$(curl -s -m 10 -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer ${HEARTBEAT_KEY}" \ -H "Authorization: Bearer ${HEARTBEAT_KEY}" \
-d "$PAYLOAD" \ -d "$PAYLOAD" \
"${PARAHUB_API_PUBLIC}" >/dev/null 2>&1 "${PARAHUB_API_PUBLIC}" 2>/dev/null)
fi fi
fi fi
# Sync paid_clients to speed control (Bumblebee only)
if [ "$ROLE" != "bee" ] && [ -x /usr/bin/parahub-speed-control ] && [ -n "$RESPONSE" ]; then
PAID_IPS=$(echo "$RESPONSE" | jsonfilter -e '$.paid_clients[*]' 2>/dev/null)
# Flush and re-add all paid IPs
parahub-speed-control flush
for IP in $PAID_IPS; do
parahub-speed-control add "$IP"
done
fi

View File

@@ -0,0 +1,74 @@
#!/bin/sh
# Parahub Mesh — Per-client speed control
# Free tier: 512kbps (default), Paid tier: full speed (via nftables set + tc HTB)
#
# Usage: parahub-speed-control init|add <ip>|remove <ip>|list|flush
IFACE="br-guest"
SLOW_RATE="512kbit"
case "$1" in
init)
# Remove SQM if active
/etc/init.d/sqm stop 2>/dev/null
/etc/init.d/sqm disable 2>/dev/null
# --- Egress shaping (router → client, i.e. download for client) ---
tc qdisc del dev $IFACE root 2>/dev/null
tc qdisc add dev $IFACE root handle 1: htb default 10
tc class add dev $IFACE parent 1: classid 1:1 htb rate 1000mbit
tc class add dev $IFACE parent 1:1 classid 1:10 htb rate $SLOW_RATE ceil $SLOW_RATE # free tier
tc class add dev $IFACE parent 1:1 classid 1:20 htb rate 1000mbit # paid tier
tc qdisc add dev $IFACE parent 1:10 fq_codel
tc qdisc add dev $IFACE parent 1:20 fq_codel
# tc filter: packets with mark 0x20 → paid class
tc filter add dev $IFACE parent 1: protocol ip handle 0x20 fw flowid 1:20
# --- Ingress shaping (client → router, i.e. upload for client) via IFB ---
ip link add ifb-guest type ifb 2>/dev/null
ip link set ifb-guest up
tc qdisc del dev $IFACE ingress 2>/dev/null
tc qdisc add dev $IFACE ingress
tc filter add dev $IFACE parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb-guest
tc qdisc del dev ifb-guest root 2>/dev/null
tc qdisc add dev ifb-guest root handle 1: htb default 10
tc class add dev ifb-guest parent 1: classid 1:1 htb rate 1000mbit
tc class add dev ifb-guest parent 1:1 classid 1:10 htb rate $SLOW_RATE ceil $SLOW_RATE
tc class add dev ifb-guest parent 1:1 classid 1:20 htb rate 1000mbit
tc qdisc add dev ifb-guest parent 1:10 fq_codel
tc qdisc add dev ifb-guest parent 1:20 fq_codel
tc filter add dev ifb-guest parent 1: protocol ip handle 0x20 fw flowid 1:20
# --- nftables: paid_clients set + mark rules ---
nft add table inet parahub 2>/dev/null
nft flush table inet parahub 2>/dev/null
nft add set inet parahub paid_clients '{ type ipv4_addr; }'
nft add chain inet parahub speed_mark '{ type filter hook forward priority -150; }'
nft add rule inet parahub speed_mark ip daddr @paid_clients meta mark set 0x20
nft add rule inet parahub speed_mark ip saddr @paid_clients meta mark set 0x20
logger -t parahub-speed "Speed control initialized: free=${SLOW_RATE}, paid=unlimited"
;;
add)
[ -z "$2" ] && echo "Usage: $0 add <ip>" && exit 1
nft add element inet parahub paid_clients "{ $2 }" 2>/dev/null
logger -t parahub-speed "Added paid client: $2"
;;
remove)
[ -z "$2" ] && echo "Usage: $0 remove <ip>" && exit 1
nft delete element inet parahub paid_clients "{ $2 }" 2>/dev/null
logger -t parahub-speed "Removed paid client: $2"
;;
list)
nft list set inet parahub paid_clients 2>/dev/null
;;
flush)
nft flush set inet parahub paid_clients 2>/dev/null
;;
*)
echo "Usage: $0 {init|add <ip>|remove <ip>|list|flush}"
exit 1
;;
esac

View File

@@ -86,10 +86,10 @@ PACKAGES_BUMBLEBEE=(
wireguard-tools wireguard-tools
luci-proto-wireguard luci-proto-wireguard
https-dns-proxy https-dns-proxy
sqm-scripts tc-full
kmod-sched-cake kmod-ifb
kmod-sched-htb
luci luci
luci-app-sqm
tcpdump tcpdump
iperf3 iperf3
iwinfo iwinfo