🤞 Sync 2026-09-27 22:16:29

This commit is contained in:
github-actions[bot] committed 2026-09-27 22:16:29 +08:00
1 parent 8ede4a8320
commit 618f51e9dc
459 files changed
+44817 -2191

No files matched your search

@@ -0,0 +1,89 @@
config trafficctl 'main'
option enabled '1'
option default_mode 'limiter'
option persist_rules '0'
# Extra subnets to monitor (space-separated CIDRs), for downstream-router
# clients that have no kernel route on this router. Subnets with a
# "via" route over a LAN interface are picked up automatically.
option extra_subnets ''
# Resolve names for devices with no DHCP lease here (routed/downstream
# clients) via reverse DNS. rdns_server is a space-separated list of
# resolvers tried in order before the system resolver — point it at the
# downstream router (e.g. a MikroTik) so its lease names get used.
option resolve_names '1'
option rdns_server ''
option rdns_ttl '300'
# Optional netifyd (DPI) integration — adds application labels per device.
# Inactive unless the agent is installed and its socket exists.
option netify_enabled '1'
option netify_socket '/var/run/netifyd/netifyd.sock'
# Where flow telemetry is published. Agent v5's unix socket is a
# request/response API that never pushes flows — the socket SINK does,
# usually tcp://127.0.0.1:1780. Empty = auto-detect from
# /etc/netifyd/netify-sink-socket.json.
option netify_endpoint ''
option netify_interval '45'
# IFB device used to shape upload pre-NAT (needs kmod-ifb).
option shape_ifb 'tctl-ifb0'
# Dashboard defaults for a browser that has not chosen its own. The Poll
# and Window chips still override per-browser — these only decide what a
# fresh session starts with, which is the part an admin can usefully set
# once for a router rather than per person (#12).
#
# poll_interval: seconds between live samples; 0 disables polling.
# A smaller number costs a conntrack read per tick on the router, which
# is the overhead the request was about.
# avg_window: seconds of history the displayed speed is averaged over.
# Samples kept per device are avg_window/poll_interval, so a long window
# with a short poll is the expensive combination.
option poll_interval '2'
option avg_window '15'
config newdevice 'newdevice'
# Apply a limit automatically the first time a device is seen. Off by
# default: it is the only setting here that restricts a device nobody
# has looked at yet. limit_kbit 0 means "no limit" and makes enabled
# a no-op. An existing limit is never overwritten.
option enabled '0'
option limit_kbit '0'
option limit_mode 'limiter'
config logging 'logging'
option enabled '1'
option log_file '/tmp/trafficctl/activity.log'
option max_lines '500'
option syslog '1'
option log_blocks '1'
option log_ratelimits '1'
option log_shapes '1'
option log_telegram '1'
option log_config '1'
config telegram 'telegram'
option enabled '0'
option bot_token ''
option chat_id ''
option poll_interval '3'
option notify_new_device '1'
option notify_known_device '0'
option control_enabled '1'
option notify_template ''
option btn_block_inet '1'
option btn_block_wifi '1'
option btn_limiter '1'
option btn_shaper '1'
config metrics 'metrics'
# Prometheus/OpenMetrics exporter. Off by default: it exposes device
# names, addresses and traffic volumes without a LuCI session.
option enabled '0'
# Shared token for the CGI endpoint (empty = no token check).
option token ''
# Per-device gauges (connections, blocked, limits, name/mac). Costs a
# full summary run per scrape — set 0 for byte counters only.
option state '1'
# Port-forward metrics.
option portfw '1'
# DPI application metrics. Off by default: device x application is a
# cardinality trap on a busy network.
option apps '0'
@@ -0,0 +1,103 @@
#!/bin/sh
# shellcheck shell=dash
# Triggered by dnsmasq on DHCP events (add/old/del).
# Available env vars: ACTION, MACADDR, IPADDR, HOSTNAME
#
# Two consumers sit on "this MAC is new":
# * the Telegram bot, which wants a notification
# * the default-limit feature, which applies a rate limit or shape
#
# They are deliberately independent — either can be on without the other —
# and they use different notions of "known". The bot has always keyed off its
# own telegram_known.json, which the bot itself rewrites; the limiter uses the
# seen-MAC ledger in trafficctl-fw.sh, because applying a limit is not an
# action that may be repeated or misfired.
[ "$ACTION" = "add" ] || exit 0
[ -n "$MACADDR" ] || exit 0
# ── Telegram notification ──────────────────────────────────────────────────
KNOWN_FILE="/etc/trafficctl/telegram_known.json"
NOTIFY_FLAG="/tmp/trafficctl_tg_newdev"
if [ "$(uci -q get trafficctl.telegram.enabled)" = "1" ] &&
! grep -q "\"$MACADDR\"" "$KNOWN_FILE" 2>/dev/null; then
mkdir -p "$(dirname "$NOTIFY_FLAG")"
# HOSTNAME comes from the dnsmasq dhcp hotplug environment (see header),
# not a POSIX shell builtin — shellcheck's SC3028 is a false positive.
# shellcheck disable=SC3028
printf '%s\t%s\t%s\n' "$MACADDR" "${IPADDR:-?}" "${HOSTNAME:-unknown}" >> "$NOTIFY_FLAG"
fi
# ── Default limit for a device seen for the first time ─────────────────────
[ "$(uci -q get trafficctl.newdevice.enabled 2>/dev/null)" = "1" ] || exit 0
LIMIT_KBIT=$(uci -q get trafficctl.newdevice.limit_kbit 2>/dev/null)
[ -n "$LIMIT_KBIT" ] && [ "$LIMIT_KBIT" -gt 0 ] 2>/dev/null || exit 0
. /usr/local/bin/trafficctl-fw.sh
# The ledger decides what "new" means, so a run that has no ledger yet cannot
# answer the question — and the wrong answer here limits every device on the
# LAN as its lease renews.
#
# Seeding alone would not make that answer trustworthy: the seed reads the
# leases and the neighbour table, and both can be thin or empty early in boot,
# so a device missing from them would look new purely because the router had
# not noticed it yet. The first pass therefore records the baseline and
# applies nothing at all, whatever the seed happened to find.
if ! tctl_seen_ready; then
tctl_seen_seed
tctl_seen_mark "$MACADDR"
logger -t trafficctl "newdevice: seeded the seen-MAC ledger ($(tctl_seen_count) devices); no limit applied on this pass"
exit 0
fi
tctl_seen_known "$MACADDR" && exit 0
# Mark BEFORE acting. A failure while applying the limit must not leave the
# device unmarked, or every subsequent lease renewal retries — and each retry
# is another chance to race with an operator setting a limit by hand.
tctl_seen_mark "$MACADDR"
[ -n "$IPADDR" ] || {
logger -t trafficctl "newdevice: $MACADDR is new but has no address yet; no limit applied"
exit 0
}
# Never override a limit somebody set deliberately.
if tctl_has_limit "$IPADDR"; then
logger -t trafficctl "newdevice: $IPADDR ($MACADDR) already has a limit; left alone"
exit 0
fi
LIMIT_MODE=$(uci -q get trafficctl.newdevice.limit_mode 2>/dev/null)
[ -n "$LIMIT_MODE" ] || LIMIT_MODE="limiter"
case "$LIMIT_MODE" in
shaper)
TCTL_VIA=newdevice TCTL_SRC=hotplug \
/usr/local/bin/trafficctl-shape.sh add "$IPADDR" "$LIMIT_KBIT" \
"shape_$IPADDR" >/dev/null 2>&1
rc=$?
;;
*)
TCTL_VIA=newdevice TCTL_SRC=hotplug \
/usr/local/bin/trafficctl-ratelimit.sh "$IPADDR" "$LIMIT_KBIT" \
"$(tctl_ratelimit_comment "$IPADDR")" >/dev/null 2>&1
rc=$?
;;
esac
if [ "$rc" = "0" ]; then
# trafficctl-ratelimit.sh writes its own activity-log entry and picks up
# the via=newdevice/src=hotplug exported above, so the limiter path is
# already recorded. trafficctl-shape.sh logs nothing, hence this branch.
[ "$LIMIT_MODE" = "shaper" ] &&
tctl_log "shape_set" "$IPADDR" \
"${LIMIT_KBIT}kbit default for new device $MACADDR" "newdevice" "hotplug"
else
logger -t trafficctl "newdevice: failed to apply ${LIMIT_KBIT}kbit to $IPADDR ($MACADDR)"
fi
@@ -0,0 +1,80 @@
#!/bin/sh
# shellcheck shell=dash
# Restore persistent rules after LAN interface comes up
. /usr/local/bin/trafficctl-fw.sh
# Clean up nft monitor table on lan ifdown to avoid stale counters
if [ "$ACTION" = "ifdown" ] && [ "$INTERFACE" = "lan" ]; then
nft delete table inet trafficctl_mon 2>/dev/null
exit 0
fi
[ "$ACTION" = "ifup" ] && [ "$INTERFACE" = "lan" ] || exit 0
SHAPES_FILE="/etc/trafficctl/shapes.json"
RULES_FILE="/etc/trafficctl/rules.json"
# Wait for network stack to be ready (up to 10 seconds)
tries=0
while ! tc qdisc show dev "$(tctl_get_lan_device)" >/dev/null 2>&1; do
tries=$((tries + 1))
[ "$tries" -ge 20 ] && break
sleep 0.5 2>/dev/null || sleep 1
done
# Restore shaping rules
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)
if [ -n "$rate" ] && [ "$rate" != "0" ]; then
/usr/local/bin/trafficctl-shape.sh add "$ip" "$rate" || \
logger -t trafficctl "Failed to restore shape for $ip (${rate}kbit)"
fi
done
fi
# Restore blocks and ratelimits (if persist enabled)
if [ -f "$RULES_FILE" ] && tctl_persist_enabled; then
jsonfilter -i "$RULES_FILE" -e '@[*]' 2>/dev/null | while read -r _; do true; done
for idx in $(seq 0 99); do
type=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].type" 2>/dev/null)
[ -z "$type" ] && break
ip=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].ip" 2>/dev/null)
param=$(jsonfilter -i "$RULES_FILE" -e "@[$idx].param" 2>/dev/null)
[ -z "$ip" ] && continue
case "$type" in
block)
tctl_block_add "$ip" "$(tctl_block_comment "$ip")" 2>/dev/null || \
logger -t trafficctl "Failed to restore block for $ip"
;;
ratelimit)
[ -n "$param" ] && [ "$param" != "0" ] && \
tctl_ratelimit_add "$ip" "$param" "$(tctl_ratelimit_comment "$ip")" 2>/dev/null || \
logger -t trafficctl "Failed to restore ratelimit for $ip (${param}kbit)"
;;
pfw_pause|pfw_limit)
# ip field is a composite key: scope/proto/ip|local/port
_scope=${ip%%/*}; _rest=${ip#*/}
_proto=${_rest%%/*}; _rest=${_rest#*/}
_pip=${_rest%%/*}; _pport=${_rest#*/}
[ "$_pip" = "local" ] && _pip="-"
if [ "$type" = "pfw_pause" ]; then
/usr/local/bin/trafficctl-portfw.sh pause "$_scope" "$_proto" "$_pip" "$_pport" >/dev/null 2>&1 || \
logger -t trafficctl "Failed to restore port pause for $_pport"
else
[ -n "$param" ] && [ "$param" != "0" ] && \
/usr/local/bin/trafficctl-portfw.sh limit "$_scope" "$_proto" "$_pip" "$_pport" "$param" >/dev/null 2>&1 || \
logger -t trafficctl "Failed to restore port limit for $_pport"
fi
;;
esac
done
fi
# Pre-create nft monitor table if software offload is active
if [ "$(tctl_get_offload_mode)" = "software" ]; then
/usr/local/bin/trafficctl-bytes-nft.sh >/dev/null 2>&1 &
fi
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh /etc/rc.common
# shellcheck shell=dash
# START/STOP/USE_PROCD are consumed by the sourced /etc/rc.common framework.
# shellcheck disable=SC2034
START=99
STOP=10
USE_PROCD=1
start_service() {
local enabled
config_load trafficctl
config_get enabled telegram enabled 0
[ "$enabled" = "1" ] || return 0
procd_open_instance telegram
procd_set_param command /usr/local/bin/trafficctl-telegram.sh
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn 3600 5 5
procd_set_param file /etc/config/trafficctl
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "trafficctl"
}
reload_service() {
stop
start
}