From: Mieczyslaw Nalewaj Date: Thu, 27 Nov 2025 12:17:50 +0000 (+0100) Subject: leds: add "network" LED trigger (lan/wan/wlan) X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=2aa1185fb05eb7f2e4c7e4f9c66e3727010328af;p=thirdparty%2Fopenwrt.git leds: add "network" LED trigger (lan/wan/wlan) LED trigger for network interfaces. - Aggregated per-family (lan/wan/wlan). - Family and flags are taken from device tree properties: - "family" : simple family string "lan" | "wan" | "wlan" - "mode" : any combination of "link", "tx", "rx" flags Priority/combination: * If "mode" present: flags come from "mode" and take precedence. Family is taken from "family" if present, otherwise from the LED name. * If only "family" present: use its family and default flags = link+tx+rx. * If neither present: fall back to LED device name parsing. - Suffix "-online" is valid ONLY in the LED name (label), e.g. "green:wlan-online". It indicates the online variant but is applied only when DT "mode" is absent. Behaviour: - wlan (normal): blink/solid driven by throughput table - lan/wan (normal): one-shot blink on TX/RX packet change - *-online variants: steady ON while any interface of the family has carrier Interfaces are auto-tracked by name match (lan0, wan1, wlan2, phy0, wl1, ath0, ra0...). Up to MAX_IFACES (16) interfaces per family This trigger is intended for board/device authors and drivers to provide simple network-activity LED behaviour without per-interface wiring in userspace. Refresh bcm27xx patches in the same commit to account for line shifts in drivers/leds/trigger/Kconfig and Makefile, ensuring clean applies for bisectability. Signed-off-by: Mieczyslaw Nalewaj Link: https://github.com/openwrt/openwrt/pull/19903 Signed-off-by: Robert Marko --- diff --git a/package/kernel/linux/modules/leds.mk b/package/kernel/linux/modules/leds.mk index f7a32936eb0..a8ed73c3b7c 100644 --- a/package/kernel/linux/modules/leds.mk +++ b/package/kernel/linux/modules/leds.mk @@ -70,6 +70,22 @@ endef $(eval $(call KernelPackage,ledtrig-gpio)) +define KernelPackage/ledtrig-network + SUBMENU:=$(LEDS_MENU) + TITLE:=LED Network Trigger + KCONFIG:=CONFIG_LEDS_TRIGGER_NETWORK + FILES:=$(LED_TRIGGER_DIR)/ledtrig-network.ko + AUTOLOAD:=$(call AutoLoad,50,ledtrig-network) +endef + +define KernelPackage/ledtrig-network/description + Kernel module that allows LEDs to be controlled by network interfaces + aggregated by family (lan/wan/wlan), with an optional per-LED '-online' mode. +endef + +$(eval $(call KernelPackage,ledtrig-network)) + + define KernelPackage/ledtrig-transient SUBMENU:=$(LEDS_MENU) TITLE:=LED Transient Trigger diff --git a/target/linux/bcm27xx/patches-6.12/950-0189-leds-Add-the-actpwr-trigger.patch b/target/linux/bcm27xx/patches-6.12/950-0189-leds-Add-the-actpwr-trigger.patch index ce0a9bae382..8196b9b11fe 100644 --- a/target/linux/bcm27xx/patches-6.12/950-0189-leds-Add-the-actpwr-trigger.patch +++ b/target/linux/bcm27xx/patches-6.12/950-0189-leds-Add-the-actpwr-trigger.patch @@ -17,7 +17,7 @@ Signed-off-by: Phil Elwell --- a/drivers/leds/trigger/Kconfig +++ b/drivers/leds/trigger/Kconfig -@@ -168,4 +168,15 @@ config LEDS_TRIGGER_INPUT_EVENTS +@@ -177,4 +177,15 @@ config LEDS_TRIGGER_INPUT_EVENTS When build as a module this driver will be called ledtrig-input-events. @@ -35,10 +35,10 @@ Signed-off-by: Phil Elwell endif # LEDS_TRIGGERS --- a/drivers/leds/trigger/Makefile +++ b/drivers/leds/trigger/Makefile -@@ -17,3 +17,4 @@ obj-$(CONFIG_LEDS_TRIGGER_NETDEV) += led - obj-$(CONFIG_LEDS_TRIGGER_PATTERN) += ledtrig-pattern.o +@@ -18,3 +18,4 @@ obj-$(CONFIG_LEDS_TRIGGER_PATTERN) += le obj-$(CONFIG_LEDS_TRIGGER_TTY) += ledtrig-tty.o obj-$(CONFIG_LEDS_TRIGGER_INPUT_EVENTS) += ledtrig-input-events.o + obj-$(CONFIG_LEDS_TRIGGER_NETWORK) += ledtrig-network.o +obj-$(CONFIG_LEDS_TRIGGER_ACTPWR) += ledtrig-actpwr.o --- /dev/null +++ b/drivers/leds/trigger/ledtrig-actpwr.c diff --git a/target/linux/generic/config-6.12 b/target/linux/generic/config-6.12 index 2003f4714cb..3a914f94f95 100644 --- a/target/linux/generic/config-6.12 +++ b/target/linux/generic/config-6.12 @@ -3216,6 +3216,7 @@ CONFIG_LEDS_TRIGGER_HEARTBEAT=y # CONFIG_LEDS_TRIGGER_INPUT_EVENTS is not set # CONFIG_LEDS_TRIGGER_MTD is not set CONFIG_LEDS_TRIGGER_NETDEV=y +# CONFIG_LEDS_TRIGGER_NETWORK is not set # CONFIG_LEDS_TRIGGER_ONESHOT is not set # CONFIG_LEDS_TRIGGER_PANIC is not set # CONFIG_LEDS_TRIGGER_PATTERN is not set diff --git a/target/linux/generic/config-6.18 b/target/linux/generic/config-6.18 index 8392e855cf3..590d7c3eee3 100644 --- a/target/linux/generic/config-6.18 +++ b/target/linux/generic/config-6.18 @@ -3343,6 +3343,7 @@ CONFIG_LEDS_TRIGGER_HEARTBEAT=y # CONFIG_LEDS_TRIGGER_INPUT_EVENTS is not set # CONFIG_LEDS_TRIGGER_MTD is not set CONFIG_LEDS_TRIGGER_NETDEV=y +# CONFIG_LEDS_TRIGGER_NETWORK is not set # CONFIG_LEDS_TRIGGER_ONESHOT is not set # CONFIG_LEDS_TRIGGER_PANIC is not set # CONFIG_LEDS_TRIGGER_PATTERN is not set diff --git a/target/linux/generic/files/drivers/leds/trigger/ledtrig-network.c b/target/linux/generic/files/drivers/leds/trigger/ledtrig-network.c new file mode 100644 index 00000000000..e2e60c3a26f --- /dev/null +++ b/target/linux/generic/files/drivers/leds/trigger/ledtrig-network.c @@ -0,0 +1,1149 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * LED trigger for network interfaces. + * + * - Aggregated per-family (lan/wan/wlan). + * - Family and flags are taken from device tree properties: + * - "family" : simple family string "lan" | "wan" | "wlan" + * - "mode" : any combination of "link", "tx", "rx" flags + * Priority/combination: + * * If "mode" present: flags come from "mode" and take precedence. + * Family is taken from "family" if present, otherwise from the LED name. + * * If only "family" present: use its family and default flags = link+tx+rx. + * * If neither present: fall back to LED device name parsing. + * + * - Suffix "-online" is valid ONLY in the LED name (label), e.g. "green:wlan-online". + * It indicates the online variant but is applied only when DT "mode" is absent. + * + * Behaviour: + * - wlan (normal): blink/solid driven by throughput table + * - lan/wan (normal): one-shot blink on TX/RX packet change + * - *-online variants: steady ON while any interface of the family has carrier + * + * Interfaces are auto-tracked by name match (lan0, wan1, wlan2, phy0, wl1, ath0, ra0...). + * Up to MAX_IFACES (16) interfaces per family. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../leds.h" + +#define MAX_IFACES 16 +#define DEFAULT_INTERVAL_MS 50 +/* Work reschedule period = 2 * DEFAULT_INTERVAL_MS; named explicitly + * to avoid silent divergence between the zero-iface and normal paths. + */ +#define WORK_INTERVAL_MS (2 * DEFAULT_INTERVAL_MS) + +enum net_trig_type { + NET_TRIG_LAN = 0, + NET_TRIG_WAN, + NET_TRIG_WLAN, + NET_TRIG_TYPE_MAX, +}; + +static const char * const type_names[] = { + [NET_TRIG_LAN] = "lan", + [NET_TRIG_WAN] = "wan", + [NET_TRIG_WLAN] = "wlan", +}; + +/* labels indexed by bitmask: (link<<2)|(tx<<1)|(rx<<0) */ +static const char *const labels[] = { + "", /* 0 */ + "(rx)", /* 1 */ + "(tx)", /* 2 */ + "(tx rx)", /* 3 */ + "(link)", /* 4 */ + "(link rx)", /* 5 */ + "(link tx)", /* 6 */ + "(link tx rx)", /* 7 */ +}; + +/* wlan throughput table */ +static const struct { + u32 throughput; + unsigned long on_ms; + unsigned long off_ms; +} wlan_tpt_table[] = { + { 64, 200, 800 }, + { 512, 200, 300 }, + { 2048, 200, 150 }, + {10000, 200, 50 }, + {54000, 100, 50 }, +}; + +struct net_mgr { + enum net_trig_type type; /* family: lan/wan/wlan */ + + struct mutex lock; + struct notifier_block notifier; + struct delayed_work work; + + struct net_device *devs[MAX_IFACES]; + int dev_slot_limit; + + u64 agg_tx_packets; + u64 agg_rx_packets; + u64 agg_tx_bytes; + u64 agg_rx_bytes; + + struct list_head leds; + atomic_t refcnt; +}; + +struct net_led { + struct list_head node; + struct led_classdev *led_cdev; + struct net_mgr *mgr; + + u64 last_tx_packets; + u64 last_rx_packets; + u64 last_tx_bytes; + u64 last_rx_bytes; + + bool link; + bool tx; + bool rx; +}; + +/* + * Locking rules: + * - managers_lock protects managers[] and entry->mgr reassignment. + * - m->lock protects m->leds, m->devs[], agg_* and per-entry last_* while linked to m. + * - Moving an entry between managers requires managers_lock and both manager locks. + */ +static DEFINE_MUTEX(managers_lock); +static struct net_mgr *managers[NET_TRIG_TYPE_MAX]; + +static ssize_t net_flag_show(struct device *dev, struct device_attribute *attr, char *buf); +static ssize_t net_flag_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count); +static ssize_t net_dev_show(struct device *dev, struct device_attribute *attr, char *buf); +static ssize_t net_dev_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count); + +/* helpers */ +static inline void led_set_off_full(struct led_classdev *led, bool on) +{ + led_stop_software_blink(led); + led_set_brightness(led, on ? LED_FULL : LED_OFF); +} + +static inline void led_set_oneshot_ms(struct led_classdev *led, unsigned long ms, bool invert) +{ + led_stop_software_blink(led); + led_blink_set_oneshot(led, &ms, &ms, invert); +} + +static inline void net_mgr_lock_pair(struct net_mgr *a, struct net_mgr *b) +{ + if (a == b) { + mutex_lock(&a->lock); + return; + } + + if ((unsigned long)a < (unsigned long)b) { + mutex_lock(&a->lock); + mutex_lock(&b->lock); + } else { + mutex_lock(&b->lock); + mutex_lock(&a->lock); + } +} + +static inline void net_mgr_unlock_pair(struct net_mgr *a, struct net_mgr *b) +{ + if (a == b) { + mutex_unlock(&a->lock); + return; + } + + if ((unsigned long)a < (unsigned long)b) { + mutex_unlock(&b->lock); + mutex_unlock(&a->lock); + } else { + mutex_unlock(&a->lock); + mutex_unlock(&b->lock); + } +} + +/* helper: check that next char after prefix is digit, '-' or NUL */ +static inline bool next_char_ok(const char *s, size_t pos) +{ + char c = s[pos]; + return c == '\0' || c == '-' || (c >= '0' && c <= '9'); +} + +/* name matching: lan/wan; wlan matched by various wifi prefixes with restriction + * additionally accept ath (Atheros) and ra/rai (Ralink/MediaTek) prefixes. + * For lan and wan require next char to be digit/'-' or end to avoid accidental matches. + */ +static bool name_matches_type(const char *name, enum net_trig_type type) +{ + if (!name) + return false; + + switch (type) { + case NET_TRIG_LAN: + /* accept "lan", "lanX", "lan-X" */ + if (!strncmp(name, "lan", 3)) + return next_char_ok(name, 3); + return false; + + case NET_TRIG_WAN: + /* accept "wan", "wanX", "wan-X" */ + if (!strncmp(name, "wan", 3)) + return next_char_ok(name, 3); + return false; + + case NET_TRIG_WLAN: + /* accept "phyX", "wlX"/"wl-..." or "wlan" and common driver prefixes */ + if (!strncmp(name, "phy", 3)) + return next_char_ok(name, 3); + if (!strncmp(name, "wlan", 4)) + return next_char_ok(name, 4); + if (!strncmp(name, "wl", 2)) + return next_char_ok(name, 2); + /* Atheros (athX) */ + if (!strncmp(name, "ath", 3)) + return next_char_ok(name, 3); + /* Ralink/MediaTek: check "rai" (e.g. rai0) first, then "ra" (ra0) */ + if (!strncmp(name, "rai", 3)) + return next_char_ok(name, 3); + if (!strncmp(name, "ra", 2)) + return next_char_ok(name, 2); + return false; + + default: + return false; + } +} + +/* Extract family type (lan/wan/wlan) from a string. + * If "online" is supplied, also detects and strips the "-online" suffix. + * Returns enum net_trig_type or -1 on error. + */ +static int parse_family_token(const char *fn, bool *online) +{ + size_t len; + const size_t sufflen = sizeof("-online") - 1; + + if (online) + *online = false; + + if (!fn) + return -1; + + len = strlen(fn); + + if (online && (len > sufflen) && !strcmp(fn + len - sufflen, "-online")) { + *online = true; + len -= sufflen; + } + + if (len == 3 && !strncmp(fn, "lan", 3)) + return NET_TRIG_LAN; + if (len == 3 && !strncmp(fn, "wan", 3)) + return NET_TRIG_WAN; + if (len == 4 && !strncmp(fn, "wlan", 4)) + return NET_TRIG_WLAN; + + return -1; +} + +/* Parse only flags from a string: recognized tokens are "link", "tx", "rx". + * Any other token => -EINVAL. If no flags found, set all three true. + * Suitable for DT "mode" (strict) or parsing flags from name when no mode present. + */ +static int parse_flags_from_string(const char *fn, bool *link, bool *tx, bool *rx) +{ + size_t len; + const char *buf = NULL; + size_t buflen = 0; + size_t i; + bool found = false; + + if (!fn || !link || !tx || !rx) + return -EINVAL; + + *link = false; + *tx = false; + *rx = false; + + len = strlen(fn); + for (i = 0; i < len; i++) { + if (fn[i] == ' ' || fn[i] == '\t' || fn[i] == ',') { + buf = NULL; + buflen = 0; + continue; + } + + if (!buf) { + buf = fn + i; + buflen = 0; + } + buflen++; + + if ((i + 1 == len) || fn[i + 1] == ' ' || fn[i + 1] == '\t' || fn[i + 1] == ',') { + if (buflen == 4 && !strncmp(buf, "link", 4)) { + *link = true; + found = true; + } else if (buflen == 2 && !strncmp(buf, "tx", 2)) { + *tx = true; + found = true; + } else if (buflen == 2 && !strncmp(buf, "rx", 2)) { + *rx = true; + found = true; + } else { + /* unknown token */ + return -EINVAL; + } + + buf = NULL; + buflen = 0; + } + } + + /* default: if no flags found, set all true */ + if (!found) { + *link = true; + *tx = true; + *rx = true; + } + + return 0; +} + +/* safe stats read wrapper */ +static void get_dev_stats_safe(struct net_device *dev, struct rtnl_link_stats64 *st) +{ + memset(st, 0, sizeof(*st)); + dev_get_stats(dev, st); +} + +/* Update single LED according to manager aggregates and per-LED flags. + * any_online indicates whether any tracked interface currently has carrier. + * If LED is online-only (link && !tx && !rx) it is driven directly by any_online. + */ +static void update_led(struct net_led *e, struct net_mgr *m, bool any_online) +{ + unsigned long on_ms, off_ms; + struct led_classdev *led = e->led_cdev; + + /* defensive: avoid deref if led unexpectedly NULL */ + if (!led) + return; + + /* If LED requested online-only, reflect any_online */ + if (READ_ONCE(e->link) && !READ_ONCE(e->tx) && !READ_ONCE(e->rx)) { + led_set_off_full(led, any_online); + return; + } + + /* For non-online-only LEDs: if no tracked interface has carrier, + * keep the LED off and reset history baseline to avoid spurious deltas + * when carrier later returns. + */ + if (!any_online) { + led_set_off_full(led, false); + + e->last_tx_packets = m->agg_tx_packets; + e->last_rx_packets = m->agg_rx_packets; + e->last_tx_bytes = m->agg_tx_bytes; + e->last_rx_bytes = m->agg_rx_bytes; + return; + } + + /* non-online-only behaviour depends on family */ + if (m->type == NET_TRIG_WLAN) { + /* throughput-driven */ + u64 bytes_delta = 0; + u64 kbps = 0; + int idx = 0; + int t; + + if (READ_ONCE(e->tx) && m->agg_tx_bytes >= e->last_tx_bytes) + bytes_delta += m->agg_tx_bytes - e->last_tx_bytes; + if (READ_ONCE(e->rx) && m->agg_rx_bytes >= e->last_rx_bytes) + bytes_delta += m->agg_rx_bytes - e->last_rx_bytes; + + /* Avoid overflow when multiplying bytes_delta; cap to max u64. */ + if (bytes_delta > ULLONG_MAX / 8) + kbps = ULLONG_MAX; + else + kbps = div64_u64(bytes_delta * 8, WORK_INTERVAL_MS); + + if (kbps == 0) { + led_set_off_full(led, READ_ONCE(e->link)); + } else { + for (t = 0; t < ARRAY_SIZE(wlan_tpt_table); t++) { + if (kbps >= wlan_tpt_table[t].throughput) + idx = t; + else + break; + } + + on_ms = wlan_tpt_table[idx].on_ms; + off_ms = wlan_tpt_table[idx].off_ms; + if (READ_ONCE(e->link)) + led_blink_set(led, &on_ms, &off_ms); + else + led_blink_set(led, &off_ms, &on_ms); + } + + e->last_tx_bytes = m->agg_tx_bytes; + e->last_rx_bytes = m->agg_rx_bytes; + } else { + /* LAN/WAN: oneshot on packet-count change */ + u64 tx_sum = m->agg_tx_packets; + u64 rx_sum = m->agg_rx_packets; + + if ((READ_ONCE(e->tx) && tx_sum != e->last_tx_packets) || + (READ_ONCE(e->rx) && rx_sum != e->last_rx_packets)) { + unsigned long ms = DEFAULT_INTERVAL_MS; + + led_set_oneshot_ms(led, ms, READ_ONCE(e->link)); + } else { + led_set_off_full(led, READ_ONCE(e->link)); + } + + e->last_tx_packets = tx_sum; + e->last_rx_packets = rx_sum; + } +} + +/* Three-stage work: snapshot devices, collect stats, update LEDs. + * Stats are collected without m->lock to avoid lock inversion with the + * network stack (dev_get_stats may acquire driver locks / send notifiers). + */ +static void net_mgr_work(struct work_struct *work) +{ + struct net_mgr *m = container_of(work, struct net_mgr, work.work); + struct net_device *snap[MAX_IFACES]; + int snap_count = 0; + u64 agg_tx_packets = 0, agg_rx_packets = 0; + u64 agg_tx_bytes = 0, agg_rx_bytes = 0; + bool any_online = false; + struct net_led *e; + int i; + + /* Stage 1: snapshot device references under m->lock. */ + mutex_lock(&m->lock); + for (i = 0; i < m->dev_slot_limit; i++) { + if (m->devs[i]) { + snap[snap_count] = m->devs[i]; + dev_hold(snap[snap_count]); + snap_count++; + } + } + mutex_unlock(&m->lock); + + /* Stage 2: collect stats without m->lock. + * Devices are pinned by dev_hold from stage 1. + */ + for (i = 0; i < snap_count; i++) { + struct net_device *dev = snap[i]; + struct rtnl_link_stats64 st; + + get_dev_stats_safe(dev, &st); + agg_tx_packets += st.tx_packets; + agg_rx_packets += st.rx_packets; + agg_tx_bytes += st.tx_bytes; + agg_rx_bytes += st.rx_bytes; + + if (netif_running(dev) && netif_carrier_ok(dev)) + any_online = true; + + dev_put(dev); + } + + /* Stage 3: update aggregates and LEDs under m->lock. + * Note: any_online was sampled in stage 2 without m->lock, so it may + * lag reality by up to one WORK_INTERVAL_MS cycle. This is acceptable + * for LED visual purposes. + */ + mutex_lock(&m->lock); + + m->agg_tx_packets = agg_tx_packets; + m->agg_rx_packets = agg_rx_packets; + m->agg_tx_bytes = agg_tx_bytes; + m->agg_rx_bytes = agg_rx_bytes; + + list_for_each_entry(e, &m->leds, node) + update_led(e, m, any_online); + + mutex_unlock(&m->lock); + + schedule_delayed_work(&m->work, msecs_to_jiffies(WORK_INTERVAL_MS)); +} + +/* Remove device and compact trailing NULLs in devs[]. + * Caller must hold m->lock (or other serializing lock)! + */ +static void net_mgr_remove_dev(struct net_mgr *m, int hole) +{ + int last = m->dev_slot_limit - 1; + + /* Trim trailing NULL slots first */ + while (last > hole && !m->devs[last]) { + last--; + m->dev_slot_limit--; + } + + if (last > hole) { + /* Swap hole with last non-NULL slot */ + m->devs[hole] = m->devs[last]; + m->devs[last] = NULL; + } + /* last == hole: the hole is the last slot; just shrink */ + m->dev_slot_limit--; +} + +/* notifier: manage tracked devices */ +static int net_mgr_notify(struct notifier_block *nb, unsigned long event, void *ptr) +{ + struct netdev_notifier_info *info = ptr; + struct net_device *dev = NULL; + /* to_put collects any reference that must be dropped after mutex release */ + struct net_device *to_put = NULL; + struct net_mgr *m = container_of(nb, struct net_mgr, notifier); + int i, id = -1, newid; + + if (event != NETDEV_REGISTER && event != NETDEV_UNREGISTER && + event != NETDEV_CHANGENAME) + return NOTIFY_DONE; + + if (!info) + return NOTIFY_DONE; + + dev = info->dev; + if (!dev) + return NOTIFY_DONE; + + mutex_lock(&m->lock); + for (i = 0; i < m->dev_slot_limit; i++) { + if (m->devs[i] == dev) { + id = i; + break; + } + } + + switch (event) { + case NETDEV_UNREGISTER: + if (id >= 0 && m->devs[id]) { + to_put = m->devs[id]; + m->devs[id] = NULL; + pr_info("%s - interface %s unregistered\n", + type_names[m->type], dev->name); + net_mgr_remove_dev(m, id); + } + break; + + case NETDEV_CHANGENAME: + if (id >= 0 && !name_matches_type(dev->name, m->type)) { + if (m->devs[id]) { + to_put = m->devs[id]; + m->devs[id] = NULL; + pr_info("%s - interface renamed to %s (no longer matches), untracked\n", + type_names[m->type], dev->name); + net_mgr_remove_dev(m, id); + } + break; + } + fallthrough; + + case NETDEV_REGISTER: + if (id < 0 && name_matches_type(dev->name, m->type)) { + newid = -1; + for (i = 0; i < m->dev_slot_limit; i++) { + if (!m->devs[i]) { + newid = i; + break; + } + } + if (newid < 0 && m->dev_slot_limit < MAX_IFACES) + newid = m->dev_slot_limit++; + + if (newid >= 0) { + dev_hold(dev); + m->devs[newid] = dev; + pr_info("%s - interface %s registered\n", + type_names[m->type], dev->name); + } + } + break; + } + + mutex_unlock(&m->lock); + + /* drop reference outside the lock to avoid lock inversion */ + if (to_put) + dev_put(to_put); + + return NOTIFY_DONE; +} + +static void net_mgr_put(struct net_mgr *m) +{ + int i; + + if (!m) + return; + + if (!atomic_dec_and_mutex_lock(&m->refcnt, &managers_lock)) + return; + + if (managers[m->type] == m) + managers[m->type] = NULL; + mutex_unlock(&managers_lock); + + cancel_delayed_work_sync(&m->work); + unregister_netdevice_notifier(&m->notifier); + + mutex_lock(&m->lock); + for (i = 0; i < m->dev_slot_limit; i++) { + if (m->devs[i]) + dev_put(m->devs[i]); + } + mutex_unlock(&m->lock); + + kfree(m); +} + +/* find/create manager for base family */ +static struct net_mgr *net_mgr_get(enum net_trig_type type) +{ + struct net_mgr *m, *existing = NULL; + int i; + + if (type >= NET_TRIG_TYPE_MAX) + return NULL; + + /* fast-path: if already exists, bump ref and return */ + mutex_lock(&managers_lock); + m = managers[type]; + if (m) { + atomic_inc(&m->refcnt); + mutex_unlock(&managers_lock); + return m; + } + mutex_unlock(&managers_lock); + + /* allocate and init (not yet published) */ + m = kzalloc(sizeof(*m), GFP_KERNEL); + if (!m) + return NULL; + + m->type = type; + mutex_init(&m->lock); + INIT_LIST_HEAD(&m->leds); + atomic_set(&m->refcnt, 1); + INIT_DELAYED_WORK(&m->work, net_mgr_work); + + m->notifier.notifier_call = net_mgr_notify; + m->notifier.priority = 0; + + if (register_netdevice_notifier(&m->notifier)) { + kfree(m); + return NULL; + } + + /* publish manager, handle rare race where another thread created it first */ + mutex_lock(&managers_lock); + existing = managers[type]; + if (existing) { + /* use existing one: increase refcount, drop our allocation & deregister our (not-used) notifier */ + atomic_inc(&existing->refcnt); + mutex_unlock(&managers_lock); + unregister_netdevice_notifier(&m->notifier); /* deregister only our own! */ + for (i = 0; i < m->dev_slot_limit; i++) { + if (m->devs[i]) + dev_put(m->devs[i]); + } + kfree(m); + return existing; + } + + managers[type] = m; + mutex_unlock(&managers_lock); + + /* start background work */ + schedule_delayed_work(&m->work, 0); + + return m; +} + +static DEVICE_ATTR(link, 0644, net_flag_show, net_flag_store); +static DEVICE_ATTR(tx, 0644, net_flag_show, net_flag_store); +static DEVICE_ATTR(rx, 0644, net_flag_show, net_flag_store); +static DEVICE_ATTR(family, 0644, net_dev_show, net_dev_store); + +static struct attribute *net_attrs[] = { + &dev_attr_link.attr, + &dev_attr_tx.attr, + &dev_attr_rx.attr, + &dev_attr_family.attr, + NULL, +}; + +static const struct attribute_group net_attr_group = { + .attrs = net_attrs, +}; + +static ssize_t net_flag_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct net_led *entry; + int val; + + entry = led_trigger_get_drvdata(dev); + if (!entry) + return -ENODEV; + + if (attr == &dev_attr_link) + val = READ_ONCE(entry->link); + else if (attr == &dev_attr_tx) + val = READ_ONCE(entry->tx); + else if (attr == &dev_attr_rx) + val = READ_ONCE(entry->rx); + else + return -EINVAL; + + return sysfs_emit(buf, "%d\n", val); +} + +static ssize_t net_flag_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct net_led *entry; + struct net_mgr *mgr; + bool val; + int ret; + + entry = led_trigger_get_drvdata(dev); + if (!entry) + return -ENODEV; + + ret = kstrtobool(buf, &val); + if (ret) + return ret; + + /* Pin mgr via refcnt to prevent UAF. + * Hold managers_lock across the flag write so that net_dev_store() + * cannot reassign entry->mgr concurrently. + */ + mutex_lock(&managers_lock); + mgr = entry->mgr; + if (!mgr || !atomic_inc_not_zero(&mgr->refcnt)) { + mutex_unlock(&managers_lock); + return -ENODEV; + } + + mutex_lock(&mgr->lock); + + if (attr == &dev_attr_link) + WRITE_ONCE(entry->link, val); + else if (attr == &dev_attr_tx) + WRITE_ONCE(entry->tx, val); + else if (attr == &dev_attr_rx) + WRITE_ONCE(entry->rx, val); + else { + mutex_unlock(&mgr->lock); + mutex_unlock(&managers_lock); + net_mgr_put(mgr); + return -EINVAL; + } + + ret = count; + + pr_info("LED %s - network trigger flags changed to %s%s%s\n", + dev_name(entry->led_cdev->dev), + READ_ONCE(entry->link) ? "link " : "", + READ_ONCE(entry->tx) ? "tx " : "", + READ_ONCE(entry->rx) ? "rx" : ""); + + mutex_unlock(&mgr->lock); + mutex_unlock(&managers_lock); + + schedule_delayed_work(&mgr->work, 0); + net_mgr_put(mgr); + + return ret; +} + +static ssize_t net_dev_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct net_led *entry; + struct net_mgr *mgr; + ssize_t ret; + + entry = led_trigger_get_drvdata(dev); + if (!entry) + return -ENODEV; + + /* Protect dereference with managers_lock and refcount */ + mutex_lock(&managers_lock); + mgr = entry->mgr; + if (mgr && !atomic_inc_not_zero(&mgr->refcnt)) + mgr = NULL; + mutex_unlock(&managers_lock); + + if (!mgr) + return -ENODEV; + + ret = sysfs_emit(buf, "%s\n", type_names[mgr->type]); + net_mgr_put(mgr); + return ret; +} + +static ssize_t net_dev_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + char tmp[32]; + char *trimmed; + size_t len = count; + struct net_led *entry; + int parsed; + struct net_mgr *old_mgr = NULL, *new_mgr; + size_t i; + + entry = led_trigger_get_drvdata(dev); + if (!entry) + return -ENODEV; + + /* Pin old_mgr via refcnt to prevent UAF */ + mutex_lock(&managers_lock); + old_mgr = entry->mgr; + if (old_mgr && !atomic_inc_not_zero(&old_mgr->refcnt)) + old_mgr = NULL; + mutex_unlock(&managers_lock); + if (!old_mgr) + return -ENODEV; + + if (len == 0 || len >= sizeof(tmp)) { + net_mgr_put(old_mgr); + return -EINVAL; + } + + /* copy raw input from sysfs buffer and ensure null-termination */ + memcpy(tmp, buf, len); + tmp[len] = '\0'; + + /* trim leading/trailing spaces, tabs, and newlines (\r, \n) */ + trimmed = strim(tmp); + len = strlen(trimmed); + + /* reject internal spaces/tabs inside the token */ + for (i = 0; i < len; i++) { + if (trimmed[i] == ' ' || trimmed[i] == '\t') { + net_mgr_put(old_mgr); + return -EINVAL; + } + } + + parsed = parse_family_token(trimmed, NULL); + if (parsed < 0) { + net_mgr_put(old_mgr); + return -EINVAL; + } + + if (old_mgr->type == parsed) { + net_mgr_put(old_mgr); + return count; /* no change */ + } + + /* get or create new manager (increments refcnt) */ + new_mgr = net_mgr_get(parsed); + if (!new_mgr) { + net_mgr_put(old_mgr); + return -ENOMEM; + } + + /* Move entry between manager lists. + * managers_lock serializes entry->mgr reassignment against all other paths. + * Both manager locks are required because both old_mgr->leds and new_mgr->leds + * are accessed by net_mgr_work() under the respective manager lock. + */ + mutex_lock(&managers_lock); + net_mgr_lock_pair(old_mgr, new_mgr); + + /* Verify entry still belongs to old_mgr */ + if (entry->mgr != old_mgr) { + net_mgr_unlock_pair(old_mgr, new_mgr); + mutex_unlock(&managers_lock); + net_mgr_put(new_mgr); + net_mgr_put(old_mgr); + return -ENODEV; + } + + list_del(&entry->node); + list_add_tail(&entry->node, &new_mgr->leds); + entry->mgr = new_mgr; + + /* initialize history so future reads won't see a spurious delta */ + entry->last_tx_packets = new_mgr->agg_tx_packets; + entry->last_rx_packets = new_mgr->agg_rx_packets; + entry->last_tx_bytes = new_mgr->agg_tx_bytes; + entry->last_rx_bytes = new_mgr->agg_rx_bytes; + + pr_info("LED %s - network trigger family changed to %s\n", + dev_name(entry->led_cdev->dev), type_names[parsed]); + + net_mgr_unlock_pair(old_mgr, new_mgr); + mutex_unlock(&managers_lock); + + schedule_delayed_work(&new_mgr->work, 0); + + /* Two puts for old_mgr: + * (1) the local pin acquired by atomic_inc_not_zero() at the top. + * (2) the structural ref that entry->mgr held for old_mgr, + * which is now orphaned because entry->mgr was reassigned to new_mgr + * (new_mgr's ref came from net_mgr_get() and was adopted by entry->mgr). + */ + net_mgr_put(old_mgr); /* (1) local pin */ + net_mgr_put(old_mgr); /* (2) structural ref from old entry->mgr */ + + return count; +} + +/* deactivate: detach led from manager */ +static void net_deactivate(struct led_classdev *led_cdev) +{ + struct net_led *entry; + struct net_mgr *m; + + entry = led_trigger_get_drvdata(led_cdev->dev); + if (!entry) + return; + + mutex_lock(&managers_lock); + m = entry->mgr; + if (!m || !atomic_inc_not_zero(&m->refcnt)) { + mutex_unlock(&managers_lock); + led_set_trigger_data(led_cdev, NULL); + kfree(entry); + return; + } + + /* managers_lock is held continuously from above until after list_del, + * so entry->mgr cannot be reassigned by net_dev_store() concurrently. + * No re-verify needed. + */ + mutex_lock(&m->lock); + list_del(&entry->node); + entry->mgr = NULL; + mutex_unlock(&m->lock); + mutex_unlock(&managers_lock); + + led_set_off_full(led_cdev, false); + led_set_trigger_data(led_cdev, NULL); + + pr_info("LED %s - trigger %s%s detached\n", + dev_name(led_cdev->dev), + type_names[m->type], + labels[(READ_ONCE(entry->link) << 2) | + (READ_ONCE(entry->tx) << 1) | + (READ_ONCE(entry->rx) << 0)]); + + kfree(entry); + net_mgr_put(m); /* structural ref transferred from entry */ + net_mgr_put(m); /* local pin taken above */ +} + +/* activate/deactivate: attach led to manager and remember flags */ +static int net_activate(struct led_classdev *led_cdev) +{ + const char *fn = NULL; + const char *dt_family = NULL; + const char *dt_mode = NULL; + int parsed = -1; + bool link = false; + bool tx = false; + bool rx = false; + bool online; + struct net_mgr *m; + struct net_led *entry; + const char *name; + const char *sep; + int ret; + + if (!led_cdev) { + pr_err("network: net_activate called with NULL led_cdev\n"); + return -EINVAL; + } + if (!led_cdev->dev) { + pr_err("network: LED device is NULL, aborting activate\n"); + return -EINVAL; + } + + name = dev_name(led_cdev->dev); + if (!name || !*name) { + pr_err("network: LED has no name, aborting activate\n"); + return -EINVAL; + } + + if (led_trigger_get_drvdata(led_cdev->dev)) { + pr_warn("network: LED %s already has trigger_data set, refusing attach\n", + name); + return -EBUSY; + } + + if (led_cdev->dev->of_node) { + of_property_read_string(led_cdev->dev->of_node, "family", &dt_family); + of_property_read_string(led_cdev->dev->of_node, "mode", &dt_mode); + } + + /* function part from name (after last ':') */ + sep = strrchr(name, ':'); + if (sep && sep[1] != '\0') + fn = sep + 1; + else + fn = name; + + if (dt_mode) { + /* DT mode present: strict parsing, empty string is invalid */ + if (*dt_mode == '\0') + return -EINVAL; + + ret = parse_flags_from_string(dt_mode, &link, &tx, &rx); + if (ret) + return -EINVAL; + + /* family: from dt_family if present, otherwise from name */ + if (dt_family) { + parsed = parse_family_token(dt_family, NULL); + if (parsed < 0) { + pr_info("network: invalid family '%s' for LED %s\n", + dt_family, name); + return -EINVAL; + } + } else { + /* parse family from name; online flag parsed but not used here */ + parsed = parse_family_token(fn, &online); + if (parsed < 0) { + pr_info("network: no family in name and no family for LED %s\n", + name); + return -EINVAL; + } + } + } else if (dt_family) { + /* Only dt_family present: use its family. Flags depend on name "-online" */ + parsed = parse_family_token(dt_family, NULL); + if (parsed < 0) { + pr_info("network: invalid family '%s' for LED %s\n", + dt_family, name); + return -EINVAL; + } + + /* test whether name contains "-online" */ + parse_family_token(fn, &online); + if (online) { + /* name indicated online variant and no mode -> online-only */ + link = true; + tx = false; + rx = false; + } else { + link = true; + tx = true; + rx = true; + } + } else { + /* No DT properties: family and flags come from the LED name. + * If name had "-online" -> online-only. + */ + parsed = parse_family_token(fn, &online); + if (parsed < 0) { + pr_info("network: unknown function '%s' for LED %s\n", + fn ?: "", name); + return -EINVAL; + } + + if (online) { + /* online variant in name => online-only */ + link = true; + tx = false; + rx = false; + } else { + link = true; + tx = true; + rx = true; + } + } + + m = net_mgr_get(parsed); + if (!m) + return -ENOMEM; + + entry = kzalloc(sizeof(*entry), GFP_KERNEL); + if (!entry) { + net_mgr_put(m); + return -ENOMEM; + } + + entry->led_cdev = led_cdev; + entry->mgr = m; + entry->last_tx_packets = 0; + entry->last_rx_packets = 0; + entry->last_tx_bytes = 0; + entry->last_rx_bytes = 0; + WRITE_ONCE(entry->link, link); + WRITE_ONCE(entry->tx, tx); + WRITE_ONCE(entry->rx, rx); + + /* attach under manager lock and initialize history to current aggregates */ + mutex_lock(&m->lock); + list_add_tail(&entry->node, &m->leds); + + /* initialize history so future reads won't see a spurious delta */ + entry->last_tx_packets = m->agg_tx_packets; + entry->last_rx_packets = m->agg_rx_packets; + entry->last_tx_bytes = m->agg_tx_bytes; + entry->last_rx_bytes = m->agg_rx_bytes; + mutex_unlock(&m->lock); + + /* Publish trigger data after entry is fully linked into the manager list. + * net_deactivate() relies on entry being on the list before list_del(). + * Any sysfs access before this point returns -ENODEV, which is safe. + */ + led_set_trigger_data(led_cdev, entry); + + pr_info("LED %s - trigger %s%s attached\n", + name, + type_names[m->type], + labels[(READ_ONCE(entry->link) << 2) | + (READ_ONCE(entry->tx) << 1) | + (READ_ONCE(entry->rx) << 0)]); + + return 0; +} + +static struct led_trigger network_trigger = { + .name = "network", + .activate = net_activate, + .deactivate = net_deactivate, + /* Use .groups + led_trigger_get_drvdata/set_drvdata; avoid device_create_file/remove_file. */ + .groups = (const struct attribute_group *[]) { &net_attr_group, NULL }, +}; + +module_led_trigger(network_trigger); + +MODULE_AUTHOR("Mieczyslaw Nalewaj "); +MODULE_DESCRIPTION("LED trigger for network interfaces - aggregated by family; supports link/tx/rx and -online"); +MODULE_LICENSE("GPL"); diff --git a/target/linux/generic/hack-6.12/820-ledtrig-network-module.patch b/target/linux/generic/hack-6.12/820-ledtrig-network-module.patch new file mode 100644 index 00000000000..79693641263 --- /dev/null +++ b/target/linux/generic/hack-6.12/820-ledtrig-network-module.patch @@ -0,0 +1,58 @@ +From: Mieczyslaw Nalewaj +Date: Thu, 27 Nov 2025 13:17:50 +0100 +Subject: leds: add "network" LED trigger (lan/wan/wlan) + +LED trigger for network interfaces. + + - Aggregated per-family (lan/wan/wlan). + - Family and flags are taken from device tree properties: + - "family" : simple family string "lan" | "wan" | "wlan" + - "mode" : any combination of "link", "tx", "rx" flags + Priority/combination: + * If "mode" present: flags come from "mode" and take precedence. + Family is taken from "family" if present, otherwise from the LED name. + * If only "family" present: use its family and default flags = link+tx+rx. + * If neither present: fall back to LED device name parsing. + + - Suffix "-online" is valid ONLY in the LED name (label), + e.g. "green:wlan-online". It indicates the online variant but is applied + only when DT "mode" is absent. + +Behaviour: + - wlan (normal): blink/solid driven by throughput table + - lan/wan (normal): one-shot blink on TX/RX packet change + - *-online variants: steady ON while any interface of the family has carrier + + Interfaces are auto-tracked by name match (lan0, wan1, wlan2, phy0, wl1, + ath0, ra0...). Up to MAX_IFACES (16) interfaces per family + +This trigger is intended for board/device authors and drivers to provide simple +network-activity LED behaviour without per-interface wiring in userspace. + +Signed-off-by: Mieczyslaw Nalewaj +--- +--- a/drivers/leds/trigger/Kconfig ++++ b/drivers/leds/trigger/Kconfig +@@ -129,6 +129,15 @@ config LEDS_TRIGGER_NETDEV + This allows LEDs to be controlled by network device activity. + If unsure, say Y. + ++config LEDS_TRIGGER_NETWORK ++ tristate "LED trigger for network interfaces" ++ depends on NET ++ help ++ Per-family network LED trigger: aggregates lan/wan/wlan stats; ++ family from DT 'family' or name; flags from DT 'mode'; ++ '-online' - online-only if mode absent. ++ If unsure, say Y. ++ + config LEDS_TRIGGER_PATTERN + tristate "LED Pattern Trigger" + help +--- a/drivers/leds/trigger/Makefile ++++ b/drivers/leds/trigger/Makefile +@@ -16,3 +16,4 @@ obj-$(CONFIG_LEDS_TRIGGER_NETDEV) += led + obj-$(CONFIG_LEDS_TRIGGER_PATTERN) += ledtrig-pattern.o + obj-$(CONFIG_LEDS_TRIGGER_TTY) += ledtrig-tty.o + obj-$(CONFIG_LEDS_TRIGGER_INPUT_EVENTS) += ledtrig-input-events.o ++obj-$(CONFIG_LEDS_TRIGGER_NETWORK) += ledtrig-network.o diff --git a/target/linux/generic/hack-6.18/820-ledtrig-network-module.patch b/target/linux/generic/hack-6.18/820-ledtrig-network-module.patch new file mode 100644 index 00000000000..79693641263 --- /dev/null +++ b/target/linux/generic/hack-6.18/820-ledtrig-network-module.patch @@ -0,0 +1,58 @@ +From: Mieczyslaw Nalewaj +Date: Thu, 27 Nov 2025 13:17:50 +0100 +Subject: leds: add "network" LED trigger (lan/wan/wlan) + +LED trigger for network interfaces. + + - Aggregated per-family (lan/wan/wlan). + - Family and flags are taken from device tree properties: + - "family" : simple family string "lan" | "wan" | "wlan" + - "mode" : any combination of "link", "tx", "rx" flags + Priority/combination: + * If "mode" present: flags come from "mode" and take precedence. + Family is taken from "family" if present, otherwise from the LED name. + * If only "family" present: use its family and default flags = link+tx+rx. + * If neither present: fall back to LED device name parsing. + + - Suffix "-online" is valid ONLY in the LED name (label), + e.g. "green:wlan-online". It indicates the online variant but is applied + only when DT "mode" is absent. + +Behaviour: + - wlan (normal): blink/solid driven by throughput table + - lan/wan (normal): one-shot blink on TX/RX packet change + - *-online variants: steady ON while any interface of the family has carrier + + Interfaces are auto-tracked by name match (lan0, wan1, wlan2, phy0, wl1, + ath0, ra0...). Up to MAX_IFACES (16) interfaces per family + +This trigger is intended for board/device authors and drivers to provide simple +network-activity LED behaviour without per-interface wiring in userspace. + +Signed-off-by: Mieczyslaw Nalewaj +--- +--- a/drivers/leds/trigger/Kconfig ++++ b/drivers/leds/trigger/Kconfig +@@ -129,6 +129,15 @@ config LEDS_TRIGGER_NETDEV + This allows LEDs to be controlled by network device activity. + If unsure, say Y. + ++config LEDS_TRIGGER_NETWORK ++ tristate "LED trigger for network interfaces" ++ depends on NET ++ help ++ Per-family network LED trigger: aggregates lan/wan/wlan stats; ++ family from DT 'family' or name; flags from DT 'mode'; ++ '-online' - online-only if mode absent. ++ If unsure, say Y. ++ + config LEDS_TRIGGER_PATTERN + tristate "LED Pattern Trigger" + help +--- a/drivers/leds/trigger/Makefile ++++ b/drivers/leds/trigger/Makefile +@@ -16,3 +16,4 @@ obj-$(CONFIG_LEDS_TRIGGER_NETDEV) += led + obj-$(CONFIG_LEDS_TRIGGER_PATTERN) += ledtrig-pattern.o + obj-$(CONFIG_LEDS_TRIGGER_TTY) += ledtrig-tty.o + obj-$(CONFIG_LEDS_TRIGGER_INPUT_EVENTS) += ledtrig-input-events.o ++obj-$(CONFIG_LEDS_TRIGGER_NETWORK) += ledtrig-network.o