diff --git a/.gitignore b/.gitignore index 40975fc54..8af2d4f84 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ AGENTS.md /test/.log /local.mk /test/spec/Readme.adoc +__pycache__/ diff --git a/board/aarch64/acer-connect-vero-w6m/rootfs/usr/share/product/acer,connect-vero-w/etc/factory-config.cfg b/board/aarch64/acer-connect-vero-w6m/rootfs/usr/share/product/acer,connect-vero-w/etc/factory-config.cfg index d8d42b8f6..eb1e5b9da 100644 --- a/board/aarch64/acer-connect-vero-w6m/rootfs/usr/share/product/acer,connect-vero-w/etc/factory-config.cfg +++ b/board/aarch64/acer-connect-vero-w6m/rootfs/usr/share/product/acer,connect-vero-w/etc/factory-config.cfg @@ -22,7 +22,6 @@ "name": "radio0", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "DE", "band": "2.4GHz", "channel": "auto" } @@ -31,7 +30,6 @@ "name": "radio1", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "DE", "band": "6GHz", "channel": "auto" } @@ -40,12 +38,14 @@ "name": "radio2", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "DE", "band": "5GHz", "channel": "auto" } } - ] + ], + "infix-hardware:wifi": { + "country-code": "DE" + } }, "ietf-interfaces:interfaces": { "interface": [ @@ -457,7 +457,7 @@ ] }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/bananapi-bpi-r3/README.md b/board/aarch64/bananapi-bpi-r3/README.md index 91a1aaf47..b21fda14f 100644 --- a/board/aarch64/bananapi-bpi-r3/README.md +++ b/board/aarch64/bananapi-bpi-r3/README.md @@ -212,6 +212,36 @@ sync 2. Set boot switches to eMMC mode (see image above) 3. Power on +## Giving the Board a Permanent MAC Address + +The BPI-R3 family has no EEPROM with a factory-programmed MAC address. +Out of the box every Ethernet port therefore gets a random address on +each boot, and the hostname, which is derived from the base address, +changes with it. Worse, both WiFi radios carry the same MediaTek +default address on every board, so two boards cannot even connect to +each other over WiFi: a client refuses to authenticate to an access +point that has its own address. + +The eMMC image carries an empty `factory` partition for this purpose. +Write a product record to it once, from the Infix shell, and the board +keeps that identity across reboots and upgrades: + +``` +echo '{"mac-address":"02:00:00:ba:01:00","serial-number":"banana1","vendor":"Bananapi","product-name":"BPI-R3"}' \ + | onieprom -e | dd of=/dev/disk/by-partlabel/factory +``` + +Give each board its own base address and serial number, then reboot. +The base address becomes the hostname suffix, the wired ports get base ++ 1, base + 2, and so on in name order, and the WiFi interfaces get +addresses derived from the base. With the record above the board is +named `bpi-ba-01-00`, `lan1` is `02:00:00:ba:01:02` and `wifi0` is +`06:00:00:ba:01:00`. + +A locally administered address like `02:xx:xx:xx:xx:xx` is fine for a +lab. For boards that will share a network with other equipment, use a +range you own. See [Vital Product Data](../../../doc/vpd.md#factory-partition) +for the record format and what else it can carry. ## Troubleshooting diff --git a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3.dtsi b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3.dtsi index abe2870cc..ef1b24659 100644 --- a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3.dtsi +++ b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3.dtsi @@ -1,4 +1,18 @@ / { + /* + * U-Boot writes its ethaddr, which it generates at random on + * every boot, into the node behind the ethernet0 alias. The + * kernel then reports that port, and every switch port that + * inherits from it, as having a permanent hardware address. + * Drop the aliases so the ports come up with a random address + * instead, which init.d/22-macaddr replaces with a chassis- + * derived one when a product VPD is present. + */ + aliases { + /delete-property/ ethernet0; + /delete-property/ ethernet1; + }; + chosen { infix { /* Default admin user password: 'admin' */ diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/etc/modprobe.d/mt7915e-wed.conf b/board/aarch64/bananapi-bpi-r3/rootfs/etc/modprobe.d/mt7915e-wed.conf new file mode 100644 index 000000000..2d323064b --- /dev/null +++ b/board/aarch64/bananapi-bpi-r3/rootfs/etc/modprobe.d/mt7915e-wed.conf @@ -0,0 +1,2 @@ +# Wireless Ethernet Dispatch, the WiFi hardware offload path +options mt7915e wed_enable=1 diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/libexec/bpi-r3/macaddr b/board/aarch64/bananapi-bpi-r3/rootfs/usr/libexec/bpi-r3/macaddr new file mode 100755 index 000000000..3e5efb239 --- /dev/null +++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/libexec/bpi-r3/macaddr @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Give ports without a hardware MAC address a chassis-derived one. + +The BPI-R3 family has no MAC EEPROM, so every port boots with a random +address, and the switch ports inherit that from their conduit. When +the product VPD provides a base MAC, hand each such port a stable +address derived from it: base + 1, base + 2, ... in interface name +order. This mirrors what products with real hardware addresses get +from their device tree. Ports with a permanent address are left alone. + +Runs from /etc/product/init.d, before the DSA conduit is renamed, so +it sorts as eth0 and takes base + 1. +""" +import json +import os +import subprocess +import sys + +SYSTEM_JSON = "/run/system.json" +NET = "/sys/class/net" +NET_ADDR_PERM = 0 + + +def log(msg): + subprocess.run(["logger", "-k", "-p", "user.notice", "-t", "macaddr", msg], + check=False) + + +def vpd_base_mac(): + try: + with open(SYSTEM_JSON, encoding="ascii") as f: + system = json.load(f) + except (OSError, ValueError): + return None + + product = system.get("vpd", {}).get("product", {}) + return product.get("data", {}).get("mac-address") + + +def mac_add(mac, offset): + num = int(mac.replace(":", ""), 16) + offset + num %= 1 << 48 + return ":".join(f"{(num >> 8 * i) & 0xff:02x}" for i in range(5, -1, -1)) + + +def read(path): + with open(path, encoding="ascii") as f: + return f.read().strip() + + +def ports(): + """Wired ports whose address the kernel did not get from hardware.""" + for name in sorted(os.listdir(NET)): + path = os.path.join(NET, name) + if not os.path.exists(os.path.join(path, "device")): + continue + if os.path.exists(os.path.join(path, "phy80211")): + continue + try: + if int(read(os.path.join(path, "addr_assign_type"))) == NET_ADDR_PERM: + continue + except (OSError, ValueError): + continue + + yield name + + +def main(): + base = vpd_base_mac() + if not base: + return 0 + + for offset, name in enumerate(ports(), start=1): + mac = mac_add(base, offset) + rc = subprocess.run(["ip", "link", "set", "dev", name, "address", mac], + check=False) + if rc.returncode: + log(f"failed setting {name} address {mac}") + continue + log(f"{name}: {mac} (chassis + {offset})") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg index 799bb7aef..c018183b3 100644 --- a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg +++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg @@ -22,7 +22,6 @@ "name": "radio0", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "DE", "band": "2.4GHz", "channel": "auto" } @@ -31,12 +30,14 @@ "name": "radio1", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "DE", "band": "5GHz", "channel": "auto" } } - ] + ], + "infix-hardware:wifi": { + "country-code": "DE" + } }, "ietf-interfaces:interfaces": { "interface": [ @@ -450,7 +451,7 @@ ] }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/product/init.d/S22-macaddr b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/product/init.d/S22-macaddr new file mode 120000 index 000000000..f44f78b9d --- /dev/null +++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/product/init.d/S22-macaddr @@ -0,0 +1 @@ +/usr/libexec/bpi-r3/macaddr \ No newline at end of file diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/factory-config.cfg b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/factory-config.cfg index a4b6256cb..182c6f535 100644 --- a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/factory-config.cfg +++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/factory-config.cfg @@ -22,7 +22,6 @@ "name": "radio0", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "DE", "band": "2.4GHz", "channel": "auto" } @@ -31,12 +30,14 @@ "name": "radio1", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "DE", "band": "5GHz", "channel": "auto" } } - ] + ], + "infix-hardware:wifi": { + "country-code": "DE" + } }, "ietf-interfaces:interfaces": { "interface": [ @@ -416,7 +417,7 @@ ] }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/product/init.d/S22-macaddr b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/product/init.d/S22-macaddr new file mode 120000 index 000000000..f44f78b9d --- /dev/null +++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/product/init.d/S22-macaddr @@ -0,0 +1 @@ +/usr/libexec/bpi-r3/macaddr \ No newline at end of file diff --git a/board/aarch64/bananapi-bpi-r3/uboot/mt7986-env.dtsi b/board/aarch64/bananapi-bpi-r3/uboot/mt7986-env.dtsi index 6964340f8..39cb3e1eb 100644 --- a/board/aarch64/bananapi-bpi-r3/uboot/mt7986-env.dtsi +++ b/board/aarch64/bananapi-bpi-r3/uboot/mt7986-env.dtsi @@ -13,7 +13,7 @@ fdt_addr_r = "0x43f00000"; kernel_addr_r = "0x44000000"; scriptaddr = "0x48000000"; - ramdisk_addr_r = "0x4A000000"; + ramdisk_addr_r = "0x50000000"; en8811h_fw_part = "0#en8811h_fw"; en8811h_fw_dm_dir = "EthMD32.dm.bin"; diff --git a/board/aarch64/bananapi-bpi-r4/rootfs/usr/share/product/bananapi,bpi-r4-2g5/etc/factory-config.cfg b/board/aarch64/bananapi-bpi-r4/rootfs/usr/share/product/bananapi,bpi-r4-2g5/etc/factory-config.cfg index 78c9a69bd..c95c464d5 100644 --- a/board/aarch64/bananapi-bpi-r4/rootfs/usr/share/product/bananapi,bpi-r4-2g5/etc/factory-config.cfg +++ b/board/aarch64/bananapi-bpi-r4/rootfs/usr/share/product/bananapi,bpi-r4-2g5/etc/factory-config.cfg @@ -374,7 +374,7 @@ ] }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/bananapi-bpi-r4/rootfs/usr/share/product/bananapi,bpi-r4/etc/factory-config.cfg b/board/aarch64/bananapi-bpi-r4/rootfs/usr/share/product/bananapi,bpi-r4/etc/factory-config.cfg index d5fa27042..7b6d66926 100644 --- a/board/aarch64/bananapi-bpi-r4/rootfs/usr/share/product/bananapi,bpi-r4/etc/factory-config.cfg +++ b/board/aarch64/bananapi-bpi-r4/rootfs/usr/share/product/bananapi,bpi-r4/etc/factory-config.cfg @@ -366,7 +366,7 @@ ] }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/bananapi-bpi-r64/rootfs/usr/share/product/bananapi,bpi-r64/etc/factory-config.cfg b/board/aarch64/bananapi-bpi-r64/rootfs/usr/share/product/bananapi,bpi-r64/etc/factory-config.cfg index af616e23c..632787cbd 100644 --- a/board/aarch64/bananapi-bpi-r64/rootfs/usr/share/product/bananapi,bpi-r64/etc/factory-config.cfg +++ b/board/aarch64/bananapi-bpi-r64/rootfs/usr/share/product/bananapi,bpi-r64/etc/factory-config.cfg @@ -15,12 +15,14 @@ "name": "radio0", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "DE", "band": "2.4GHz", "channel": "auto" } } - ] + ], + "infix-hardware:wifi": { + "country-code": "DE" + } }, "ietf-interfaces:interfaces": { "interface": [ @@ -408,7 +410,7 @@ ] }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/friendlyarm-nanopi-r2s/rootfs/usr/share/product/friendlyarm,nanopi-r2s/etc/factory-config.cfg b/board/aarch64/friendlyarm-nanopi-r2s/rootfs/usr/share/product/friendlyarm,nanopi-r2s/etc/factory-config.cfg index ea01bc806..3ef1d99aa 100644 --- a/board/aarch64/friendlyarm-nanopi-r2s/rootfs/usr/share/product/friendlyarm,nanopi-r2s/etc/factory-config.cfg +++ b/board/aarch64/friendlyarm-nanopi-r2s/rootfs/usr/share/product/friendlyarm,nanopi-r2s/etc/factory-config.cfg @@ -350,7 +350,7 @@ ] }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/raspberrypi-rpi64/README.md b/board/aarch64/raspberrypi-rpi64/README.md index 711bc2eb4..68151d68b 100644 --- a/board/aarch64/raspberrypi-rpi64/README.md +++ b/board/aarch64/raspberrypi-rpi64/README.md @@ -118,19 +118,19 @@ Then configure the WiFi interface using the keystore reference: ``` admin@infix:/> configure +admin@infix:/config/> set hardware wifi country-code US admin@infix:/config/> edit interface wifi0 admin@infix:/config/interface/wifi0/> set ipv4 dhcp-client -admin@infix:/config/interface/wifi0/> set wifi ssid YourNetworkName -admin@infix:/config/interface/wifi0/> set wifi secret mywifi -admin@infix:/config/interface/wifi0/> set wifi country-code US +admin@infix:/config/interface/wifi0/> set wifi station ssid YourNetworkName +admin@infix:/config/interface/wifi0/> set wifi station security secret mywifi admin@infix:/config/interface/wifi0/> leave ``` > [!NOTE] > The WiFi password (8-63 characters) is stored securely in the keystore as > `mywifi` (or any name you choose), which is then referenced in the WiFi -> configuration. The country-code must match your location for regulatory -> compliance (e.g., US, SE, DE, JP). +> configuration. The country code is one setting for all radios and must +> match your location for regulatory compliance (e.g., US, SE, DE, JP). ### Touch Screen Support diff --git a/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg b/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg index 33998c5d0..09f87bfd0 100644 --- a/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg +++ b/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg @@ -13,10 +13,7 @@ }, { "name": "radio0", - "class": "infix-hardware:wifi", - "infix-hardware:wifi-radio": { - "country-code": "00" - } + "class": "infix-hardware:wifi" } ] }, @@ -245,35 +242,6 @@ } ] }, - "infix-schedule:schedules": { - "schedule": [ - { - "name": "nightly", - "description": "Every night at 03:00", - "recurrence": { - "frequency": "ietf-schedule:daily", - "byhour": [ - 3 - ] - } - }, - { - "name": "weekly", - "description": "Sunday nights at 03:00", - "recurrence": { - "frequency": "ietf-schedule:weekly", - "byday": [ - { - "weekday": "sunday" - } - ], - "byhour": [ - 3 - ] - } - } - ] - }, "ntp": { "enabled": true, "server": [ @@ -298,7 +266,7 @@ "infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo=" }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,400/etc/factory-config.cfg b/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,400/etc/factory-config.cfg index 0094635ba..4a4f80e5f 100644 --- a/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,400/etc/factory-config.cfg +++ b/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,400/etc/factory-config.cfg @@ -27,10 +27,7 @@ }, { "name": "radio0", - "class": "infix-hardware:wifi", - "infix-hardware:wifi-radio": { - "country-code": "00" - } + "class": "infix-hardware:wifi" } ] }, @@ -283,7 +280,7 @@ "infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo=" }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/factory-config.cfg b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/factory-config.cfg index e39e37c1d..fef2ca84d 100644 --- a/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/factory-config.cfg +++ b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/factory-config.cfg @@ -272,7 +272,7 @@ "infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo=" }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/arm/raspberrypi-rpi2/rootfs/usr/share/product/raspberrypi,2-model-b/etc/factory-config.cfg b/board/arm/raspberrypi-rpi2/rootfs/usr/share/product/raspberrypi,2-model-b/etc/factory-config.cfg index 75cca7917..4b4eb4cc0 100644 --- a/board/arm/raspberrypi-rpi2/rootfs/usr/share/product/raspberrypi,2-model-b/etc/factory-config.cfg +++ b/board/arm/raspberrypi-rpi2/rootfs/usr/share/product/raspberrypi,2-model-b/etc/factory-config.cfg @@ -254,7 +254,7 @@ "infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo=" }, "infix-meta:meta": { - "version": "1.10" + "version": "1.11" }, "infix-services:mdns": { "enabled": true diff --git a/board/common/rootfs/usr/libexec/infix/hostapd.sh b/board/common/rootfs/usr/libexec/infix/hostapd.sh new file mode 100755 index 000000000..69631868a --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/hostapd.sh @@ -0,0 +1,164 @@ +#!/bin/sh +# Run hostapd, learn the other nodes' access points, steer dual-band +# clients to 5 GHz while it runs, and hand the clients over when stopped. +# +# A client with a good signal has no reason to roam, so when its access +# point goes away it only notices once the beacons stop, and then has to +# scan for a new network. An 802.11v BSS transition request with +# disassociation imminent makes it roam to another access point while +# the radio is still up. Clients without 802.11v are deauthenticated +# by hostapd on exit, as before. + +# A client given candidates roams in well under a second, one without +# has to scan first, a few seconds on a real radio across three bands. +# A client that roams never tells the old access point either, hostapd +# drops it from its station list when the timer runs out, so wait a +# little longer than that before giving up, but stay well inside the +# ten seconds the service gets to stop before it is killed. +TIMER=50 # beacon intervals (100 ms) until hostapd disassociates a client that stays +WAIT=7 # seconds to wait for the clients to leave + +# The hostapd configs among the arguments, which also carry options +confs() +{ + for arg in $CONFS; do + case $arg in + *.conf) echo "$arg" ;; + esac + done +} + +bsses() +{ + for sock in /run/hostapd/*; do + [ -S "$sock" ] && echo "${sock##*/}" + done +} + +stations() +{ + for bss in $(bsses); do + hostapd_cli -i "$bss" list_sta 2>/dev/null + done +} + +# Ask a station to leave, naming the other access points of the SSID +# wifi-neighbors.py has heard of: a client does not look for a new +# access point on a request without candidates. +ask_to_move() +{ + bss=$1 + sta=$2 + cands="" + if [ -s /run/wifi-neighbors/$bss ]; then + for cand in $(grep -E '^[0-9a-f]{2}(:[0-9a-f]{2}){5}(,[0-9]+){4}$' /run/wifi-neighbors/$bss); do + cands="$cands neighbor=$cand" + done + fi + if [ -n "$cands" ]; then + # shellcheck disable=SC2086 + hostapd_cli -i "$bss" bss_tm_req "$sta" disassoc_imminent=1 disassoc_timer=$TIMER \ + pref=1 abridged=1 $cands >/dev/null 2>&1 + else + hostapd_cli -i "$bss" disassoc_imminent "$sta" $TIMER >/dev/null 2>&1 + fi +} + +handover() +{ + # Only clients that do 802.11v can be asked to move, and hostapd only + # does so with bss_transition on + grep -qs '^bss_transition=1' $(confs) || return 0 + + num=0 + for bss in $(bsses); do + for sta in $(hostapd_cli -i "$bss" list_sta 2>/dev/null); do + ask_to_move "$bss" "$sta" + num=$((num + 1)) + done + done + if [ $num -eq 0 ]; then + logger -t hostapd -p daemon.notice "stop: no stations on $(bsses | tr '\n' ' ')" + return 0 + fi + + end=$(($(date +%s) + WAIT)) + while [ "$(date +%s)" -lt "$end" ]; do + [ -z "$(stations)" ] && break + sleep 0.2 + done + + left=$(stations | grep -c .) + if [ "$left" -eq 0 ]; then + logger -t hostapd -p daemon.notice "stop: asked $num station(s) to move, all left" + else + logger -t hostapd -p daemon.notice "stop: asked $num station(s) to move, $left still here" + fi +} + +# Band steering pairs, a 2.4 GHz BSS and the 5/6 GHz twin it defers to, +# from the no_probe_resp_if_seen_on directives in the radio configs. +pairs() +{ + for conf in "$@"; do + case $conf in + *.conf) ;; + *) continue ;; + esac + awk -F= '/^(interface|bss)=/ { cur = $2 } + /^no_probe_resp_if_seen_on=/ { print cur, $2 }' "$conf" + done +} + +# Wait for the control socket of a BSS, up to ten seconds +wait_bss() +{ + i=0 + while [ ! -S /run/hostapd/$1 ] && [ $i -lt 50 ]; do + sleep 0.2 + i=$((i + 1)) + done + [ -S /run/hostapd/$1 ] +} + +helpers() +{ + # Exchange access point lists with the other nodes once hostapd is up + for conf in "$@"; do + case $conf in *.conf) ;; *) continue ;; esac + wait_bss "$(sed -n 's/^interface=//p' "$conf")" || continue + /usr/libexec/infix/wifi-neighbors.py "$@" & + echo $! >> /run/wifi-steer/pids + break + done + + # Steer dual-band clients to the higher band, one loop per pair + pairs "$@" | while read -r bss twin; do + wait_bss "$bss" || continue + /usr/libexec/infix/wifi-steer.sh "$bss" "$twin" & + echo $! >> /run/wifi-steer/pids + done +} + +stop() +{ + [ -f /run/wifi-steer/pids ] && kill $(cat /run/wifi-steer/pids) 2>/dev/null + rm -rf /run/wifi-steer + handover + kill -TERM "$pid" 2>/dev/null +} + +CONFS=$* +rm -rf /run/wifi-steer +mkdir -p /run/wifi-steer +trap stop TERM INT +hostapd "$@" & +pid=$! +helpers "$@" & +echo $! >> /run/wifi-steer/pids +rc=0 +while kill -0 "$pid" 2>/dev/null; do + wait "$pid" + rc=$? +done +exit $rc diff --git a/board/common/rootfs/usr/libexec/infix/init.d/00-probe b/board/common/rootfs/usr/libexec/infix/init.d/00-probe index e2054cefb..d41c196de 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/00-probe +++ b/board/common/rootfs/usr/libexec/infix/init.d/00-probe @@ -480,11 +480,46 @@ def probe_dmisystem(out): return 0 +FACTORY_PART = "/dev/disk/by-partlabel/factory" + + +def factory_partition_vpd(): + """Product VPD from an ONIE TLV at the start of the factory partition. + + Boards without a VPD EEPROM, e.g. SBCs, can be given an identity by + writing a TLV to a partition labelled "factory". Anyone with write + access to the disk can change it, so it is never trusted. + """ + if not os.path.exists(FACTORY_PART): + return None + + try: + with open(FACTORY_PART, "rb", 0) as f: + data = onieprom.from_tlv(f) + except Exception: # pylint: disable=broad-except + return None + + if not data: + return None + + return { + "board": "product", + "available": True, + "trusted": False, + "data": data, + } + + def probe_dtsystem(out): """Probe DTS based system, expects a VPD in ONIE PROM format.""" dtsys = DTSystem() vpds = dtsys.infix_vpds() + if not any(vpd["board"] == "product" and vpd["available"] for vpd in vpds): + vpd = factory_partition_vpd() + if vpd: + vpds.append(vpd) + model = dtsys.base.str("model") if model: out["product-name"] = model diff --git a/board/common/rootfs/usr/libexec/infix/iw.py b/board/common/rootfs/usr/libexec/infix/iw.py index ff6335148..b702357ea 100755 --- a/board/common/rootfs/usr/libexec/infix/iw.py +++ b/board/common/rootfs/usr/libexec/infix/iw.py @@ -7,12 +7,18 @@ iw.py dev - List all interfaces grouped by PHY iw.py info - Get PHY or interface information iw.py survey - Get channel survey data + iw.py survey-scan [passive] + - Scan all channels, then get survey data """ +import os import sys import json import subprocess import re +import secrets +import select +import time def decode_iw_ssid(ssid): """Decode iw escaped SSID (\\xHH) to UTF-8, stripping non-printable chars.""" try: @@ -22,6 +28,9 @@ def decode_iw_ssid(ssid): return ''.join(c for c in ssid if c.isprintable()) +SCAN_BUDGET = 40 # seconds for one channel survey, scan included + + def run_iw(*args): """Run iw command and return output""" try: @@ -486,6 +495,192 @@ def parse_dev(): return result +def parse_dev_types(): + """ + Parse 'iw dev' output + Returns: dict mapping PHY numbers to list of (interface, type) tuples + """ + output = run_iw('dev') + if not output: + return {} + + result = {} + current_phy = None + current_if = None + + for line in output.splitlines(): + stripped = line.strip() + if line.startswith('phy#'): + current_phy = line.replace('phy#', '').strip() + result.setdefault(current_phy, []) + current_if = None + elif current_phy and stripped.startswith('Interface '): + current_if = stripped.split(None, 1)[1] + result[current_phy].append([current_if, None]) + elif current_if and stripped.startswith('type '): + result[current_phy][-1][1] = stripped.split(None, 1)[1] + + return {phy: [tuple(e) for e in entries] for phy, entries in result.items()} + + +def wait_scan_done(events, ifname, deadline): + """ + Read 'iw event' output until the scan on ifname finishes or aborts. + Returns 'finished', 'aborted' or None at the deadline. + + The pipe is read unbuffered: a second line that arrives in the same + read would otherwise sit in a buffer that select() knows nothing of. + """ + fd = events.stdout.fileno() + buf = getattr(events, 'leftover', b'') + while True: + while b'\n' in buf: + line, buf = buf.split(b'\n', 1) + events.leftover = buf + line = line.decode(errors='replace') + if not line.startswith(f'{ifname} '): + continue + if 'scan finished' in line: + return 'finished' + if 'scan aborted' in line: + return 'aborted' + remaining = deadline - time.monotonic() + if remaining <= 0: + return None + ready, _, _ = select.select([fd], [], [], remaining) + if not ready: + return None + chunk = os.read(fd, 4096) + if not chunk: + return None + buf += chunk + + +def survey_scan(radio, passive=False): + """ + Scan every channel on the radio, then dump the survey. + + Returns the parse_survey() list, or {'error': ...}. The scan runs + on one interface of the radio, preferring a station or mesh point + over an access point since those can scan without extra flags. + + The scan is triggered and then waited for over 'iw event', the + combined 'iw scan' spins on the netlink socket for minutes on some + kernels. A trigger fails with EBUSY while wpa_supplicant runs its + own scan, so wait for that one to finish and try again. + """ + try: + with open(f'/sys/class/ieee80211/{radio}/index') as f: + phy = f.read().strip() + except OSError: + return {'error': f'no such radio: {radio}'} + + ifaces = parse_dev_types().get(phy, []) + order = ['managed', 'mesh point', 'AP'] + ifaces = sorted((i for i in ifaces if i[1] != 'AP/VLAN'), + key=lambda i: order.index(i[1]) if i[1] in order else len(order)) + + # A radio nobody has configured yet has no interface to scan with, + # which is exactly when a survey helps pick a band and channel. + # Borrow one for the duration of the scan, under a name no one + # would configure. + tmp = None + if not ifaces: + tmp = 'survey-' + secrets.token_hex(3) + try: + subprocess.run(['iw', 'phy', radio, 'interface', 'add', tmp, 'type', 'managed'], + capture_output=True, text=True, timeout=5, check=True) + subprocess.run(['ip', 'link', 'set', tmp, 'up'], + capture_output=True, text=True, timeout=5, check=True) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: + subprocess.run(['iw', 'dev', tmp, 'del'], capture_output=True) + err = getattr(e, 'stderr', '') or '' + return {'error': f'no interface on {radio} to scan with: {err.strip() or e}'} + ifaces = [(tmp, 'managed')] + + try: + return survey_scan_on(ifaces[0], passive) + finally: + if tmp: + subprocess.run(['iw', 'dev', tmp, 'del'], capture_output=True) + + +def survey_scan_on(iface, passive): + """Scan on one (ifname, iftype), then dump the survey, see survey_scan().""" + ifname, iftype = iface + args = ['iw', 'dev', ifname, 'scan', 'trigger'] + if iftype == 'AP': + args.append('ap-force') + if passive: + args.append('passive') + + # The action runs on the configuration daemon's thread, which has a + # minute per request, so the whole scan gets well under that. + deadline = time.monotonic() + SCAN_BUDGET + events = subprocess.Popen(['iw', 'event'], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + try: + err = 'scan failed' + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + return {'error': f'scan on {ifname} timed out'} + result = subprocess.run(args, capture_output=True, text=True, + timeout=min(10, remaining)) + if result.returncode == 0: + break + err = result.stderr.strip() or err + if '(-16)' not in err: + return {'error': f'scan on {ifname} failed: {err}'} + # Another scan is running, wait for it to end and try again + if wait_scan_done(events, ifname, deadline) is None: + return {'error': f'scan on {ifname} failed: {err}'} + + state = wait_scan_done(events, ifname, deadline) + if state is None: + return {'error': f'scan on {ifname} timed out'} + if state == 'aborted': + return {'error': f'scan on {ifname} was aborted'} + except subprocess.TimeoutExpired: + return {'error': f'scan on {ifname} timed out'} + finally: + events.kill() + events.wait() + + channels = parse_survey(ifname) + if not any(ch['in_use'] for ch in channels): + # Not every driver flags the operating channel, mac80211_hwsim + # does not, take it from the interface instead. + freq = parse_interface_info(ifname).get('frequency') + for ch in channels: + ch['in_use'] = ch['frequency'] == freq + + return channels + + +def parse_wds_ports(ifname): + """ + List the WDS ports of an access point: the AP/VLAN interfaces on the + same PHY that carry its MAC address. + Returns: [ifname, ...] + """ + info = parse_interface_info(ifname) + mac = info.get('mac') + ports = [] + + for phy, ifaces in parse_dev().items(): + if ifname not in ifaces: + continue + for dev in ifaces: + if dev == ifname: + continue + devinfo = parse_interface_info(dev) + if devinfo.get('iftype') == 'AP/VLAN' and devinfo.get('mac') == mac: + ports.append(dev) + + return ports + + def parse_link(ifname): """ Parse 'iw dev link' output for station mode @@ -626,9 +821,11 @@ def main(): 'dev': 'List all interfaces grouped by PHY', 'info': 'Get PHY or interface information (requires device)', 'survey': 'Get channel survey data (requires interface)', + 'survey-scan': 'Scan all channels, then get survey data (requires radio)', 'station': 'Get connected stations in AP mode (requires interface)', 'link': 'Get link info in station mode (requires interface)', 'mesh': 'Get mesh parameters in mesh point mode (requires interface)', + 'wds': 'List the WDS ports of an access point (requires interface)', 'caps': 'Get HT/VHT capability bitmasks (requires PHY/radio)' }, 'examples': [ @@ -640,6 +837,7 @@ def main(): 'iw.py link wlan0', 'iw.py mesh wifi0', 'iw.py survey wlan0', + 'iw.py survey-scan radio0 passive', 'iw.py caps radio0' ] }, indent=2)) @@ -677,11 +875,21 @@ def main(): data = {'error': 'mesh command requires interface argument'} else: data = parse_mesh_param(sys.argv[2]) + elif command == 'wds': + if len(sys.argv) < 3: + data = {'error': 'wds command requires interface argument'} + else: + data = parse_wds_ports(sys.argv[2]) elif command == 'survey': if len(sys.argv) < 3: data = {'error': 'survey command requires interface argument'} else: data = parse_survey(sys.argv[2]) + elif command == 'survey-scan': + if len(sys.argv) < 3: + data = {'error': 'survey-scan command requires radio argument'} + else: + data = survey_scan(sys.argv[2], 'passive' in sys.argv[3:]) elif command == 'caps': if len(sys.argv) < 3: data = {'error': 'caps command requires PHY/radio argument'} @@ -691,6 +899,8 @@ def main(): data = {'error': f'Unknown command: {command}'} print(json.dumps(data, indent=2, ensure_ascii=False)) + if command == 'survey-scan' and isinstance(data, dict) and 'error' in data: + sys.exit(1) except Exception as e: print(json.dumps({'error': str(e)})) diff --git a/board/common/rootfs/usr/libexec/infix/wifi-channel-map.py b/board/common/rootfs/usr/libexec/infix/wifi-channel-map.py index 706058517..01781d570 100755 --- a/board/common/rootfs/usr/libexec/infix/wifi-channel-map.py +++ b/board/common/rootfs/usr/libexec/infix/wifi-channel-map.py @@ -54,6 +54,17 @@ def get_channel_frequency(channel, band='2.4'): return None +def freq_to_band(freq): + """Band name for a frequency in MHz, or None""" + if 2400 <= freq <= 2500: + return '2.4 GHz' + if 5000 <= freq <= 5900: + return '5 GHz' + if 5925 <= freq <= 7125: + return '6 GHz' + return None + + def get_busy_percentage(channel_data): """Calculate channel busy percentage""" active = channel_data.get('active-time', 0) @@ -84,7 +95,7 @@ def draw_channel_graph_2_4ghz(survey_data): for ch_data in survey_data: freq = ch_data.get('frequency') ch_num = freq_to_channel(freq) - if ch_num and 1 <= ch_num <= 14: + if ch_num and freq_to_band(freq) == '2.4 GHz': busy_pct = get_busy_percentage(ch_data) channels[ch_num] = { 'freq': freq, @@ -107,14 +118,27 @@ def draw_channel_graph_2_4ghz(survey_data): print(f"Non-overlapping channels: 1, 6, 11 (shown in {Colors.GREEN}green{Colors.RESET})") print() - # Draw frequency scale + # 80 columns span 2400-2500 MHz, so one column is 1.25 MHz and a + # 20 MHz channel is 16 columns wide, centred on its frequency. + width = 80 + cols_per_mhz = width / 100 + + def col(freq): + return int(round((freq - 2400) * cols_per_mhz)) + + ruler = [' '] * width + ticks = [' '] * width + for mhz in range(2400, 2500, 20): + label = str(mhz) + for i, c in enumerate(label): + if col(mhz) + i < width: + ruler[col(mhz) + i] = c + ticks[col(mhz)] = '|' print("Frequency (MHz):") - print("2400 2420 2440 2460 2480") - print("|-----------|-----------|-----------|-----------|") + print(" " + ''.join(ruler)) + print(" " + ''.join(ticks).replace(' ', '-')) - # Draw each channel as a bar showing its 20 MHz width - # Each channel occupies ~4 adjacent channels worth of space - for ch in range(1, 14): + for ch in range(1, 15): if ch not in channels: continue @@ -123,7 +147,6 @@ def draw_channel_graph_2_4ghz(survey_data): is_in_use = data['in_use'] noise = data['noise'] - # Determine color based on status if is_in_use: color = Colors.BG_BLUE marker = '█' @@ -133,49 +156,25 @@ def draw_channel_graph_2_4ghz(survey_data): elif busy_pct >= 25: color = Colors.YELLOW marker = '▒' - elif busy_pct > 0: + elif busy_pct >= 1: color = Colors.CYAN marker = '░' else: color = Colors.GRAY marker = '·' - # Non-overlapping channels get green color if ch in [1, 6, 11] and not is_in_use and busy_pct < 10: color = Colors.GREEN - # Calculate position (each channel is offset by 5 MHz = 1 position) - # Channel 1 is at 2412 MHz, base is 2400 - offset = ((data['freq'] - 2400) // 5) - - # Draw channel bar (20 MHz = 4 positions wide) - line = ' ' * 80 - line_arr = list(line) - - # Mark the channel span (20 MHz width) - for i in range(4): - pos = offset + i - 2 # Center the 20 MHz around channel - if 0 <= pos < len(line_arr): - line_arr[pos] = marker - - # Add channel label - label_pos = offset - if 0 <= label_pos < len(line_arr) - 5: - # Clear space for label - for i in range(5): - if label_pos + i < len(line_arr): - line_arr[label_pos + i] = ' ' - - line = ''.join(line_arr) - - # Status indicators - status = "" - if is_in_use: - status = f" {Colors.BOLD}[IN USE]{Colors.RESET}" + line = [' '] * width + for pos in range(col(data['freq'] - 10), col(data['freq'] + 10)): + if 0 <= pos < width: + line[pos] = marker + status = f" {Colors.BOLD}[IN USE]{Colors.RESET}" if is_in_use else "" busy_color = get_utilization_color(busy_pct) - print(f"{color}Ch{ch:2d}{Colors.RESET} {color}{line}{Colors.RESET} " + print(f"{color}Ch{ch:2d}{Colors.RESET} {color}{''.join(line)}{Colors.RESET} " f"{busy_color}{busy_pct:5.1f}%{Colors.RESET} " f"{noise:4d}dBm{status}") @@ -196,12 +195,17 @@ def draw_channel_list(survey_data): print(f"{'Ch':<4} {'Freq':<6} {'Noise':<8} {'Busy%':<8} {'Utilization Bar':<40}") print("-" * 80) + band = None for ch_data in sorted(survey_data, key=lambda x: x.get('frequency', 0)): freq = ch_data.get('frequency') ch_num = freq_to_channel(freq) if not ch_num: continue + if freq_to_band(freq) != band: + band = freq_to_band(freq) + print(f"{Colors.BOLD}{band}{Colors.RESET}") + noise = ch_data.get('noise', -100) busy_pct = get_busy_percentage(ch_data) is_in_use = ch_data.get('in-use', False) @@ -237,7 +241,7 @@ def draw_overlap_pie(survey_data): for ch_data in survey_data: freq = ch_data.get('frequency') ch_num = freq_to_channel(freq) - if ch_num and 1 <= ch_num <= 13: + if ch_num and freq_to_band(freq) == '2.4 GHz': busy_pct = get_busy_percentage(ch_data) channels[ch_num] = { 'busy': busy_pct, @@ -549,78 +553,108 @@ def busy_to_height(busy_pct): def draw_recommendations(survey_data, json_output=False): - """Analyze channels and provide recommendations""" - # Parse channel data - channels = {} - in_use_channel = None + """Analyze channels and provide recommendations, per band""" + bands = {} + current = None for ch_data in survey_data: freq = ch_data.get('frequency') ch_num = freq_to_channel(freq) - if ch_num: - busy_pct = get_busy_percentage(ch_data) - channels[ch_num] = { - 'busy': busy_pct, - 'noise': ch_data.get('noise', -100), - 'in_use': ch_data.get('in-use', False) - } - if ch_data.get('in-use'): - in_use_channel = ch_num - - # Find least congested non-overlapping channels - best_channels = [] - for ch in [1, 6, 11]: - if ch in channels: - best_channels.append((ch, channels[ch]['busy'])) + band = freq_to_band(freq) if freq else None + if not ch_num or not band: + continue - best_channels.sort(key=lambda x: x[1]) + entry = { + 'channel': ch_num, + 'frequency': freq, + 'busy': get_busy_percentage(ch_data), + 'noise': ch_data.get('noise', -100), + } + bands.setdefault(band, []).append(entry) + if ch_data.get('in-use'): + current = dict(entry, band=band) + + # Least busy channels per band. On 2.4 GHz only the three channels + # that do not overlap are worth recommending. + best = {} + for band, entries in bands.items(): + if band == '2.4 GHz': + entries = [e for e in entries if e['channel'] in (1, 6, 11)] + best[band] = sorted(entries, key=lambda e: e['busy'])[:3] - # JSON output if json_output: output = { - "recommended_channels": [ - {"channel": ch, "busy_percent": round(busy, 1)} - for ch, busy in best_channels - ] + "recommended_channels": { + band: [{"channel": e['channel'], "busy_percent": round(e['busy'], 1)} + for e in entries] + for band, entries in best.items() + } } - if in_use_channel: - output["current_channel"] = in_use_channel - output["current_busy_percent"] = round(channels.get(in_use_channel, {}).get('busy', 0), 1) + if current: + output["current_channel"] = current['channel'] + output["current_band"] = current['band'] + output["current_busy_percent"] = round(current['busy'], 1) print(json.dumps(output, indent=2)) return - # Text output print(f"\n{Colors.BOLD}Channel Recommendations{Colors.RESET}") print("=" * 80) - if in_use_channel: - print(f"Current channel: {Colors.BOLD}{in_use_channel}{Colors.RESET}") - current_busy = channels.get(in_use_channel, {}).get('busy', 0) - if current_busy > 50: - print(f" {Colors.RED}⚠{Colors.RESET} High congestion detected ({current_busy:.1f}% busy)") - elif current_busy > 25: - print(f" {Colors.YELLOW}⚠{Colors.RESET} Moderate congestion ({current_busy:.1f}% busy)") + if current: + print(f"Current channel: {Colors.BOLD}{current['channel']}{Colors.RESET} ({current['band']})") + busy = current['busy'] + if busy > 50: + print(f" {Colors.RED}⚠{Colors.RESET} High congestion detected ({busy:.1f}% busy)") + elif busy > 25: + print(f" {Colors.YELLOW}⚠{Colors.RESET} Moderate congestion ({busy:.1f}% busy)") else: - print(f" {Colors.GREEN}✓{Colors.RESET} Good channel utilization ({current_busy:.1f}% busy)") + print(f" {Colors.GREEN}✓{Colors.RESET} Good channel utilization ({busy:.1f}% busy)") - print(f"\nRecommended non-overlapping channels (2.4 GHz):") - for i, (ch, busy) in enumerate(best_channels[:3], 1): - color = get_utilization_color(busy) - marker = "★" if i == 1 else " " - print(f" {marker} Channel {ch:2d}: {color}{busy:5.1f}% busy{Colors.RESET}") + for band in ('2.4 GHz', '5 GHz', '6 GHz'): + if band not in best: + continue + what = "non-overlapping channels" if band == '2.4 GHz' else "channels" + print(f"\nLeast busy {what} ({band}):") + for i, e in enumerate(best[band], 1): + color = get_utilization_color(e['busy']) + marker = "★" if i == 1 else " " + print(f" {marker} Channel {e['channel']:3d}: {color}{e['busy']:5.1f}% busy{Colors.RESET}") print() +def find_channels(data): + """ + Collect every survey channel list in the input. + + The channel-survey action output is wrapped in its path, with + 'rpc -j' that is hardware/component/wifi-radio/channel-survey, with + RESTCONF it is 'infix-hardware:output'. Dig for the lists rather + than assume one wrapping. + """ + found = [] + if isinstance(data, dict): + for key, value in data.items(): + if key == 'channel' and isinstance(value, list) and \ + all(isinstance(ch, dict) and 'frequency' in ch for ch in value): + found.extend(value) + else: + found.extend(find_channels(value)) + elif isinstance(data, list): + for item in data: + found.extend(find_channels(item)) + return found + + def main(): parser = argparse.ArgumentParser( description='Visualize WiFi channel overlap and utilization', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=''' Examples: - # Read from yanger output (show all sections) - yanger -x "ixll -A ssh host sudo" ietf-hardware | %(prog)s + # Read the channel-survey action output (show all sections) + rpc -j "/ietf-hardware:hardware/component[name='radio0']/infix-hardware:wifi-radio/channel-survey" | %(prog)s # Read from file %(prog)s survey_data.json @@ -670,22 +704,10 @@ def main(): else: data = json.load(sys.stdin) - # Extract survey data from hardware components - survey_data = [] - hardware = data.get('ietf-hardware:hardware', {}) - components = hardware.get('component', []) - - for component in components: - if component.get('class') == 'infix-hardware:wifi': - wifi_radio = component.get('infix-hardware:wifi-radio', {}) - survey = wifi_radio.get('survey', {}) - channels = survey.get('channel', []) - if channels: - survey_data.extend(channels) - + survey_data = find_channels(data) if not survey_data: print("No WiFi survey data found in input", file=sys.stderr) - print("Expected format: yanger ietf-hardware output with wifi-radio survey data", file=sys.stderr) + print("Expected format: output of the infix-hardware channel-survey action", file=sys.stderr) sys.exit(1) # Generate SVG if requested (exclusive mode) diff --git a/board/common/rootfs/usr/libexec/infix/wifi-neighbors.py b/board/common/rootfs/usr/libexec/infix/wifi-neighbors.py new file mode 100755 index 000000000..05fa709e9 --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/wifi-neighbors.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +""" +Tell the other nodes about our access points, learn about theirs. + +Usage: wifi-neighbors.py ... + +A client asked to move needs to know where to, and a node knows nothing +about the other nodes' access points. Every 30 seconds each node sends +one frame per network its access points are bridged to, listing them: +BSSID, frequency, PHY type and SSID. The network is the one the 802.11r +key exchange uses, ft_iface in the hostapd config, so the announcements +reach exactly the nodes a client can roam to. Bridges flood the frames +over the backhaul like any multicast, no IP address is needed and no +radio ever leaves its channel. + +Anything on that network can send such a frame, so every line carries a +tag made with the 802.11r key of its SSID, which the nodes serving the +SSID already share. A line whose tag does not check out, or that names +an SSID this node does not serve, is dropped. An access point without +an 802.11r key is neither announced nor learned. + +The first line of a frame is 'infix-wifi '. Nodes +only listen to their own version, a frame of another version is logged +once per sender and dropped, so a mixed set of nodes shows up in the +log instead of as a silent lack of neighbors. Bump VERSION when the +frame format changes. + +What is heard is kept for a few periods and handed to hostapd as +802.11k neighbors of every access point with the same SSID, for its +neighbor reports and for the transition requests sent by hostapd.sh and +wifi-steer.sh, which read the candidate list of a BSS from +/run/wifi-neighbors/, one bss_tm_req neighbor= argument per line. + +The same access points are also the 802.11r key holders a client can +roam to. They go into the BSS's key holder file, rxkh_file in the +hostapd config, and hostapd is told to reload it, so that it pushes the +key of every client to them right away rather than having them fetch it +at the roam, from a node that may be gone by then. + +A node that hears a new neighbor announces itself at once, so two nodes +know each other within a second of the backhaul coming up. +""" +import hmac +import hashlib +import os +import re +import select +import socket +import struct +import subprocess +import sys +import time + +ETHERTYPE = 0x88B5 # IEEE 802 local experimental 1 +GROUP = bytes.fromhex('034b4b000001') # locally administered group address +MAGIC = b'infix-wifi' +VERSION = 2 +PERIOD = 10 +EXPIRE = 35 +MAX_NEIGHBORS = 64 +DIR = '/run/wifi-neighbors' +MAC = re.compile(r'^([0-9a-f]{2}:){5}[0-9a-f]{2}$') + + +def log(msg): + subprocess.run(['logger', '-t', 'hostapd', '-p', 'daemon.notice', msg], + capture_output=True) + + +def hostapd_cli(bss, *args): + try: + res = subprocess.run(['hostapd_cli', '-i', bss, *args], + capture_output=True, text=True, timeout=5) + return res.stdout if res.returncode == 0 else '' + except (OSError, subprocess.SubprocessError): + return '' + + +def rxkh_key(path): + """The 802.11r key from a key holder file, None without one.""" + try: + for line in open(path).read().splitlines(): + # r0kh= , the key is what every node + # derives from the mobility domain and the secret + if line.startswith('r0kh='): + return line.split()[-1].encode() + except OSError: + pass + return None + + +def parse_configs(paths): + """{bss: (ft_iface, key, rxkh)} for every BSS, all None without 802.11r.""" + bsses = {} + for path in paths: + if not path.endswith('.conf'): + continue + cur = None + try: + lines = open(path).read().splitlines() + except OSError: + continue + for line in lines: + if line.startswith(('interface=', 'bss=')): + cur = line.split('=', 1)[1].strip() + bsses.setdefault(cur, [None, None, None]) + elif line.startswith('ft_iface=') and cur: + bsses[cur][0] = line.split('=', 1)[1].strip() + elif line.startswith('rxkh_file=') and cur: + bsses[cur][2] = line.split('=', 1)[1].strip() + bsses[cur][1] = rxkh_key(bsses[cur][2]) + return {bss: tuple(v) for bss, v in bsses.items()} + + +def opclass(freq): + """20 MHz operating class of a frequency.""" + if freq >= 5925: + return 131 + if freq >= 5745: + return 124 + if freq >= 5500: + return 121 + if freq >= 5260: + return 118 + if freq >= 5180: + return 115 + return 81 + + +def channel(freq): + if freq == 5935: + return 2 + if freq >= 5955: + return (freq - 5950) // 5 + if freq >= 5000: + return (freq - 5000) // 5 + if freq == 2484: + return 14 + return (freq - 2407) // 5 + + +def valid(bssid, freq, phy, ssid): + """A line is only used if every field fits in a neighbor report.""" + return (MAC.match(bssid) is not None + and (2412 <= freq <= 2484 or 5180 <= freq <= 5885 + or freq == 5935 or 5955 <= freq <= 7115) + and 1 <= channel(freq) <= 233 + and 0 <= phy <= 255 + and 1 <= len(ssid.encode()) <= 32) + + +def tag(key, bssid, freq, phy, ssid): + msg = f'{bssid} {freq} {phy} {ssid}'.encode() + return hmac.new(key, msg, hashlib.sha256).hexdigest()[:32] + + +def own_bsses(bsses): + """{bss: (bssid, freq, phy, ssid)} from hostapd, for the keyed BSSes up.""" + out = {} + for bss, (_, key, _) in bsses.items(): + if not key: + continue + cfg = hostapd_cli(bss, 'get_config') + status = hostapd_cli(bss, 'status') + bssid = re.search(r'^bssid=(\S+)', cfg, re.M) + ssid = re.search(r'^ssid=(.*)$', cfg, re.M) + freq = re.search(r'^freq=(\d+)', status, re.M) + if not (bssid and ssid and freq): + continue + # dot11PHYType: OFDM, HT, VHT, HE + if 'ieee80211ax=1' in status: + phy = 14 + elif 'ieee80211ac=1' in status: + phy = 9 + elif 'ieee80211n=1' in status: + phy = 7 + else: + phy = 4 + entry = (bssid.group(1).lower(), int(freq.group(1)), phy, ssid.group(1)) + if valid(*entry): + out[bss] = entry + return out + + +def announcement(host, own, bsses): + lines = [b'%s %d %s' % (MAGIC, VERSION, host.encode())] + for bss, (bssid, freq, phy, ssid) in own.items(): + key = bsses[bss][1] + lines.append(f'{bssid} {freq} {phy} {tag(key, bssid, freq, phy, ssid)} {ssid}'.encode()) + return b'\n'.join(lines) + b'\n' + + +def parse_announcement(data, keys, seen): + """[(bssid, freq, phy, ssid)] for the lines tagged with a key of ours.""" + try: + text = data.decode() + except UnicodeDecodeError: + return [] + lines = text.split('\n') + head = lines[0].split(' ', 2) if lines else [] + if len(head) != 3 or head[0] != MAGIC.decode(): + return [] + if head[1] != str(VERSION): + if head[2] not in seen: + seen.add(head[2]) + log(f'wifi-neighbors: {head[2][:64]} announces version {head[1][:8]}, ' + f'ours is {VERSION}, ignoring it') + return [] + out = [] + for line in lines[1:]: + parts = line.split(' ', 4) + if len(parts) < 5: + continue + bssid, ssid = parts[0].lower(), parts[4] + try: + freq, phy = int(parts[1]), int(parts[2]) + except ValueError: + continue + key = keys.get(ssid) + if not key or not valid(bssid, freq, phy, ssid): + continue + if not hmac.compare_digest(parts[3], tag(key, bssid, freq, phy, ssid)): + continue + out.append((bssid, freq, phy, ssid)) + return out + + +def candidate(bssid, freq, phy): + # BSSID information: AP reachable, same security and key scope + return f'{bssid},1151,{opclass(freq)},{channel(freq)},{phy}' + + +def nr_hex(bssid, freq, phy): + info = 1151 + return (bssid.replace(':', '') + struct.pack(' (bssid, freq, phy, ssid, last seen) + published = {} + next_send = 0.0 + last_send = 0.0 + own = {} + keys = {} # ssid -> 802.11r key, for the SSIDs we serve + other = set() # nodes heard announcing another version + while True: + now = time.monotonic() + if now >= next_send: + own = own_bsses(bsses) + keys = {ssid: bsses[bss][1] for bss, (_, _, _, ssid) in own.items()} + payload = struct.pack('!H', ETHERTYPE) + announcement(host, own, bsses) + for s, ifc in socks.items(): + try: + s.send(GROUP + s.getsockname()[4] + payload) + except OSError: + pass + next_send = now + PERIOD + last_send = now + + ready, _, _ = select.select(list(socks), [], [], max(0.1, next_send - now)) + for s in ready: + try: + data = s.recv(2048) + except OSError: + continue + stamp = time.monotonic() + for bssid, freq, phy, ssid in parse_announcement(data[14:], keys, other): + if any(bssid == o[0] for o in own.values()): + continue + if bssid not in neighbors and len(neighbors) >= MAX_NEIGHBORS: + continue + # Answer a newcomer right away, at most once a second + if bssid not in neighbors and stamp - last_send > 1: + next_send = stamp + neighbors[bssid] = (bssid, freq, phy, ssid, stamp) + + cutoff = time.monotonic() - EXPIRE + for bssid in [b for b, n in neighbors.items() if n[4] < cutoff]: + del neighbors[bssid] + if own: + try: + publish(own, {b: n[:4] for b, n in neighbors.items()}, published, bsses) + except OSError as err: + log(f'wifi-neighbors: {err}') + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + pass diff --git a/board/common/rootfs/usr/libexec/infix/wifi-steer.sh b/board/common/rootfs/usr/libexec/infix/wifi-steer.sh new file mode 100755 index 000000000..0f9cd64db --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/wifi-steer.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# Move dual-band clients from a 2.4 GHz access point to its 5/6 GHz twin. +# +# Usage: wifi-steer.sh <2.4 GHz bss> +# +# hostapd keeps a list of the clients each radio has seen lately. A +# client connected on 2.4 GHz that the twin has seen is dual-band and in +# range of the twin, so ask it to move with an 802.11v BSS transition +# request naming the twin. Clients that ignore the request stay: a +# client is asked again only after a cooldown, and one that has shrugged +# off a couple of requests is left alone for an hour. A client with a +# weak 2.4 GHz signal is left alone too, 5 GHz would be worse, and +# nobody is sent to a twin that is down or still checking for radar. +bss=$1 +twin=$2 + +PERIOD=15 # seconds between rounds +COOLDOWN=120 # seconds before asking the same client again +GIVEUP=2 # requests a client may shrug off before it is left alone ... +LONG=3600 # ... for this long +MIN_SIGNAL=-65 # dBm on 2.4 GHz below which a client is left alone + +state=/run/wifi-steer/$bss +mkdir -p "$state" + +# Neighbor report candidate for the twin: BSSID, BSSID information, +# operating class, channel and PHY type. Taken from the twin's own +# neighbor entry when it keeps one (802.11k), else built from its status +# with the 20 MHz operating class of the channel, enough for the client +# to find the BSS and learn the rest from its beacons. +candidate() +{ + bssid=$(hostapd_cli -i "$twin" get_config 2>/dev/null | sed -n 's/^bssid=//p') + [ -n "$bssid" ] || return 1 + + nr=$(hostapd_cli -i "$twin" show_neighbor 2>/dev/null | \ + awk -v b="$bssid" 'tolower($1) == tolower(b) { for (i = 2; i <= NF; i++) if ($i ~ /^nr=/) print substr($i, 4) }') + if [ ${#nr} -ge 26 ]; then + info=$(echo "$nr" | cut -c13-20) + info=$(printf '%d' "0x$(echo "$info" | cut -c7-8)$(echo "$info" | cut -c5-6)$(echo "$info" | cut -c3-4)$(echo "$info" | cut -c1-2)") + op=$(printf '%d' "0x$(echo "$nr" | cut -c21-22)") + chan=$(printf '%d' "0x$(echo "$nr" | cut -c23-24)") + phy=$(printf '%d' "0x$(echo "$nr" | cut -c25-26)") + echo "$bssid,$info,$op,$chan,$phy" + return 0 + fi + + status=$(hostapd_cli -i "$twin" status 2>/dev/null) + freq=$(echo "$status" | sed -n 's/^freq=//p') + chan=$(echo "$status" | sed -n 's/^channel=//p') + [ -n "$freq" ] && [ -n "$chan" ] || return 1 + if [ "$freq" -ge 5925 ]; then + op=131 + elif [ "$freq" -ge 5745 ]; then + op=124 + elif [ "$freq" -ge 5500 ]; then + op=121 + elif [ "$freq" -ge 5260 ]; then + op=118 + elif [ "$freq" -ge 5180 ]; then + op=115 + else + op=81 + fi + # BSSID information: AP reachable, same security and key scope. + echo "$bssid,1151,$op,$chan,9" +} + +while sleep $PERIOD; do + stas=$(hostapd_cli -i "$bss" list_sta 2>/dev/null) + [ -n "$stas" ] || continue + # Nothing to move to while the twin is down or checking for radar + hostapd_cli -i "$twin" status 2>/dev/null | grep -q '^state=ENABLED' || continue + # hostapd_cli has no shorthand for the seen-on list, ask hostapd directly + seen=$(hostapd_cli -i "$twin" raw TRACK_STA_LIST 2>/dev/null) + [ -n "$seen" ] || continue + cand=$(candidate) || continue + now=$(date +%s) + + for sta in $stas; do + echo "$seen" | grep -qi "^$sta " || continue + read -r last tries < "$state/$sta" 2>/dev/null || { last=0; tries=0; } + wait=$COOLDOWN + [ "${tries:-0}" -lt $GIVEUP ] || wait=$LONG + [ $((now - last)) -ge $wait ] || continue + [ "${tries:-0}" -lt $GIVEUP ] || tries=0 + signal=$(hostapd_cli -i "$bss" sta "$sta" 2>/dev/null | sed -n 's/^signal=//p') + if [ -n "$signal" ] && [ "$signal" -lt $MIN_SIGNAL ]; then + continue + fi + hostapd_cli -i "$bss" bss_tm_req "$sta" pref=1 abridged=1 valid_int=255 \ + "neighbor=$cand" >/dev/null 2>&1 + echo "$now $((tries + 1))" > "$state/$sta" + logger -t hostapd -p daemon.notice "$bss: asked $sta to move to $twin" + done +done diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index ee824d1f9..ead13b213 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -8,6 +8,15 @@ All notable changes to the project are documented in this file. ### Changes +- Fix `onieprom -e`, encoding JSON to a TLV failed with a `NameError`. +- Boards without a VPD EEPROM can be given a fixed identity by writing an + ONIE TLV to a disk partition labelled `factory`, see the + [VPD documentation](vpd.md#factory-partition). +- Banana Pi BPI-R3 ports get stable, chassis-derived addresses at boot + when the product VPD provides a base MAC. +- WiFi interfaces now get a unique address derived from the chassis MAC + by default. Several APs on one radio, or two devices of the same + model, no longer need `custom-phys-address` to tell them apart. - Upgrade Linux kernel to 6.18.55 (LTS) - Add IPv6 dynamic routing: RIPng and OSPFv3. Both reuse the existing ietf-rip and ietf-ospf models, selected per control-plane-protocol by the @@ -38,12 +47,45 @@ All notable changes to the project are documented in this file. the keystore. Its default route and DNS servers are used like those from a DHCP server, and the TCP MSS of forwarded connections is clamped to the session MTU, see [PPPoE Client][pppoe] +- The WiFi country code is now one setting for the whole system, `hardware + wifi country-code`, instead of one per radio. It defaults to the world + domain and is applied as soon as it is set, so a radio without an + interface is in the right regulatory domain too. Existing configurations + are migrated, the first radio's code wins, and a code the regulatory + database does not know, which never had any effect, becomes the world + domain +- WiFi band steering now also moves dual-band clients that are already + connected on 2.4 GHz to the 5 or 6 GHz access point of the same SSID, + with an 802.11v request +- A WiFi access point asks its clients to move to another access point, + with 802.11v, before it stops for a reboot, an upgrade, or a + configuration change, so clients that support it roam without losing + their connection. Devices tell each other about their access points, + over the network the access points are bridged to, list the other + devices' access points in their 802.11k neighbor reports, and hand them + a client's 802.11r keys as soon as it connects, so the client can roam + on after the access point it first connected to has gone down +- 802.11r access points take a `key-holder-secret`, a keystore key the + access points of a mobility domain hand each other a client's keys with. + It is mandatory, and must not be the WiFi password: whoever holds it can + read every client's keys. Existing configurations are upgraded with the + WiFi password as the secret so they keep roaming, change it to a key of + its own, see the [WiFi documentation](wifi.md#80211r---fast-bss-transition) +- WiFi channel survey is now on request: `show hardware survey` in the + CLI, a Scan channels button per radio on the WebUI WiFi page, or the + `channel-survey` action on the radio. The survey covers every channel the + radio supports, also while it is connected or serving clients. The + always-on `survey` container under the radio is gone, it only ever held + the operating channel on a busy radio and slowed down every hardware query ### Fixes - WebUI: uploading a software bundle failed on slow storage - WebUI: the Overview page showed the hostname template, e.g. `rpi-%m`, and never showed the boot partition +- WebUI: a YANG module file cut short in the schema cache, for example + by a restart during download, crashed the WebUI at every start until + the cache was removed by hand - LLDP neighbors were listed without their system name, descriptions, and capabilities, in the CLI, the WebUI, and the operational datastore - The manufacturer from a VPD was not shown as `mfg-name` of its @@ -59,9 +101,33 @@ All notable changes to the project are documented in this file. - Fix #1423: the default route from a DHCPv6 client, learned from router advertisements, is now a static route with the DHCPv6 route preference, like a DHCPv4 route. Before, the route preference setting was ignored +- Fix #1679: a WiFi station set up on an interface that was in scan-only + mode, as in the Raspberry Pi 4 factory configuration, did not connect + until the device was rebooted +- A WiFi station never connected to a WPA3-only access point, which + includes every access point on 6 GHz +- WiFi scan results listed WPA3-only networks as WPA2-Personal +- Fast roaming (802.11r) between WiFi access points on different devices + failed for WPA3 clients, which fell back to a full reconnect +- Changing the MAC address of a WiFi access point could leave it, and + the other access points on the same radio, down until the WiFi service + was restarted [pppoe]: https://www.kernelkit.org/infix/latest/pppoe/ + +### Added + +- WiFi 4-address (WDS) links: a station with `wds` enabled can be a bridge + port, and an access point gets a `wds-link` interface per remote station + to bridge it. Together they build wireless bridges and repeaters, see + [WDS Backhaul and Repeaters][wds]. The station leaf `peer-bssid` pins a + station to one access point +- MT7986 boards (Banana Pi BPI-R3, BPI-R3 Mini, Acer Connect Vero W6m): + WiFi hardware offloading is now active + +[wds]: https://www.kernelkit.org/infix/latest/wifi/#wds-backhaul-and-repeaters + [v26.09.0][] - 2026-09-30 ------------------------- diff --git a/doc/vpd.md b/doc/vpd.md index 6adf21be8..6c32b3982 100644 --- a/doc/vpd.md +++ b/doc/vpd.md @@ -54,6 +54,34 @@ Every VPD is listed in the _ietf-hardware_ model as a component named `vpd-`, e.g., `vpd-product` for the example above. +## Factory Partition + +Boards without a VPD EEPROM, such as single board computers, boot with +a random MAC address on every port and the vendor's default address on +every WiFi radio. Two such boards of the same model cannot even talk +to each other over WiFi, since both radios claim the same address. + +Such a board can be given an identity by writing an ONIE TLV to the +start of a disk partition labelled `factory`. The system reads it +when the device tree provides no product VPD. The `onieprom` tool +encodes the JSON form described [below](#json-encoding): + +``` +echo '{"mac-address":"02:00:00:ba:01:00","serial-number":"banana1","vendor":"Bananapi","product-name":"BPI-R3"}' \ + | onieprom -e | dd of=/dev/disk/by-partlabel/factory +``` + +A VPD read this way is never trusted, so it can only provide identity, +never the factory password. On the Banana Pi BPI-R3 family the base +MAC address it provides is also handed out to the ports at boot: every +wired port without a hardware address gets base + 1, base + 2, and so +on, in interface name order. +WiFi interfaces derive their addresses from the base MAC as described +in [WiFi](wifi.md#multi-ssid-configuration). + +The image for the Banana Pi BPI-R3 family already carries an empty +`factory` partition for this purpose. + ## Product VPD The VPD named `product` describes the device as a whole. Its diff --git a/doc/wifi.md b/doc/wifi.md index 233b07ba4..00cf35478 100644 --- a/doc/wifi.md +++ b/doc/wifi.md @@ -1,7 +1,8 @@ # Wi-Fi (Wireless LAN) -Infix includes comprehensive Wi-Fi support for both client (Station) and -Access Point modes. When a compatible Wi-Fi adapter is detected, the system +Infix supports Wi-Fi as a client (Station), as an Access Point, as an +802.11s mesh point, and as a 4-address (WDS) link for wireless bridges +and repeaters. When a compatible Wi-Fi adapter is detected, the system automatically creates a WiFi radio (PHY) in factory-config, that can host virtual interfaces. @@ -16,7 +17,7 @@ Infix uses a two-layer WiFi architecture: 2. **WiFi Interface (Network layer)**: Virtual interface on a radio - Configured via `infix-interfaces` module - - Can operate in Station (client) or Access Point mode + - Operates in Station (client), Access Point, Mesh Point or WDS link mode - Each interface references a parent radio ## Naming Conventions @@ -44,7 +45,9 @@ Where `N` is a number (0, 1, 2, ...). - USB hotplug is not supported - adapters must be present at boot - Interface naming may be inconsistent with multiple USB Wi-Fi adapters -- AP and Station modes cannot be mixed on the same radio +- A station and access points on the same radio must share a channel: the + station follows its access point, so pin the radio to that channel on + both ends. See [WDS Backhaul and Repeaters](#wds-backhaul-and-repeaters) ## Supported Wi-Fi Adapters @@ -92,14 +95,21 @@ Radios are automatically discovered and named `radio0`, `radio1`, etc. ### Country Code ⚠ -The radio defaults to "00" for World domain, but some systems may ship with a -factory default country code (typically "DE" for the BPi-R3). +The country code is one setting for all radios in the system, since the +regulatory domain is one setting in the kernel. It defaults to "00", the +world domain: no 6 GHz, listen-only on 5 GHz, and no access point or mesh +point can be configured. Some systems ship with a factory default +(typically "DE" for the BPi-R3). + +
admin@example:/> configure
+admin@example:/config/> set hardware wifi country-code DE
+admin@example:/config/> leave
+
> [!IMPORTANT] Legal notice! -> The `country-code` setting is **legally required** and determines -> which WiFi channels and power levels are permitted in your -> location. Using an incorrect country code may violate local wireless -> regulations. +> The country code is **legally required** and determines which WiFi +> channels and power levels are permitted in your location. Using an +> incorrect country code may violate local wireless regulations. **Common country codes, see [ISO 3166-1 alpha-2][1] for the complete list**: @@ -120,13 +130,13 @@ factory default country code (typically "DE" for the BPi-R3). ### Basic Radio Setup -Configure the radio with channel, power, and regulatory domain. +Configure the radio with band and channel. The country code above covers +all radios. **For Station (client) mode:**
admin@example:/> configure
 admin@example:/config/> edit hardware component radio0 wifi-radio
-admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
 admin@example:/config/hardware/component/radio0/wifi-radio/> leave
 
@@ -134,7 +144,6 @@ admin@example:/config/hardware/component/radio0/wifi-radio/> leave
admin@example:/> configure
 admin@example:/config/> edit hardware component radio0 wifi-radio
-admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
 admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
 admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
 admin@example:/config/hardware/component/radio0/wifi-radio/> set channel-width 80MHz
@@ -143,8 +152,6 @@ admin@example:/config/hardware/component/radio0/wifi-radio/> leave
 
 **Key radio parameters:**
 
-- `country-code`: Two-letter [ISO 3166-1 alpha-2][1] code, determines allowed
-  channels and maximum power. Examples: US, DE, GB, SE, FR, JP.  
   **⚠ Must match your physical location for legal compliance! ⚠**
 - `band`: 2.4GHz, 5GHz, or 6GHz (required for AP mode). Automatically enables
   appropriate WiFi standards:
@@ -270,7 +277,6 @@ interface referencing it:
 
 
admin@example:/> configure
 admin@example:/config/> edit hardware component radio0 wifi-radio
-admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
 admin@example:/config/hardware/component/radio0/wifi-radio/> leave
 
@@ -325,6 +331,32 @@ station is associated to. It appears only while connected. When several access points share one SSID (a roaming network), the `bssid` is what tells them apart, and it changes as the station roams between them. +### Channel Survey + +A channel survey shows how busy each channel is, which helps when picking a +channel for an access point or when a link performs worse than its signal +strength suggests. The radio has to leave its operating channel to measure +the others, so traffic on it pauses for a few seconds. Because of that the +survey only runs when asked for, it is not collected in the background. + +
admin@example:/> show hardware radio0 survey
+
+ +The output lists every channel the radio supports with its noise floor and +utilization, marks the operating channel, and suggests the least busy +channels per band. A radio that has no interface yet can be surveyed too, +which helps when picking a band and channel for it. Add `passive` to listen longer on each channel without sending +probe requests, which gives a steadier utilization reading at the cost of a +longer pause. + +In the WebUI, each radio on the WiFi page has a **Scan channels** button that +draws the same survey as a chart. + +Over NETCONF or RESTCONF the survey is the `channel-survey` action on the +radio's hardware component. Its output is a list of channels keyed by +frequency, with the busy, receive and transmit time out of the total time +the radio spent listening on each. + ## Passphrase Requirements To ensure your connection is secure and compatible with all network @@ -470,8 +502,8 @@ IoT devices, or segregating traffic into different VLANs. **Step 1: Configure the radio** (shared by all APs)
admin@example:/> configure
+admin@example:/config/> set hardware wifi country-code DE
 admin@example:/config/> edit hardware component radio0 wifi-radio
-admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
 admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
 admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
 admin@example:/config/hardware/component/radio0/wifi-radio/> leave
@@ -514,7 +546,6 @@ admin@example:/config/interface/wifi1/> set wifi radio radio0
 admin@example:/config/interface/wifi1/> set wifi access-point ssid GuestNetwork
 admin@example:/config/interface/wifi1/> set wifi access-point security mode wpa2-wpa3-personal
 admin@example:/config/interface/wifi1/> set wifi access-point security secret guest-secret
-admin@example:/config/interface/wifi1/> set custom-phys-address static 00:0c:43:26:60:01
 
 # IoT AP - IoT devices (WPA2 for older device compatibility)
 admin@example:/config/> edit interface wifi2
@@ -522,23 +553,19 @@ admin@example:/config/interface/wifi2/> set wifi radio radio0
 admin@example:/config/interface/wifi2/> set wifi access-point ssid IoT-Devices
 admin@example:/config/interface/wifi2/> set wifi access-point security mode wpa2-personal
 admin@example:/config/interface/wifi2/> set wifi access-point security secret iot-secret
-admin@example:/config/interface/wifi2/> set custom-phys-address static 00:0c:43:26:60:02
 admin@example:/config/interface/wifi2/> leave
 
-> [!IMPORTANT] -> **MAC Address Requirement for Multi-SSID:** -> When creating multiple AP interfaces on the same radio, you **must** configure -> a unique MAC address for each secondary interface (wifi1, wifi2, etc.) using -> `set custom-phys-address static `. All interfaces on the same radio inherit -> the radio's hardware MAC address by default, which causes network conflicts. Only -> the primary interface (alphabetically first, e.g., wifi0) should use the default -> hardware MAC address. -> -> Choose MAC addresses from the same locally-administered range: -> - Primary (wifi0): Uses hardware MAC (e.g., `00:0c:43:26:60:00`) -> - Secondary (wifi1): `00:0c:43:26:60:01` (increment last octet) -> - Tertiary (wifi2): `00:0c:43:26:60:02` (increment last octet) +> [!NOTE] +> Each WiFi interface gets its own MAC address, derived from the chassis +> MAC by changing only the first octet: the locally administered bit is +> set and the interface number plus one is stored in the upper bits. On +> a device with chassis MAC `00:53:00:c0:ff:ee`, `wifi0` becomes +> `06:53:00:c0:ff:ee` and `wifi1` becomes `0a:53:00:c0:ff:ee`. Several +> APs on one radio therefore never share an address, and two devices of +> the same model never collide. To use a specific address instead, set +> `custom-phys-address` on the interface, see [Common Interface +> Settings](iface.md#custom-mac-address). **Result:** Three SSIDs broadcasting simultaneously on radio0: @@ -569,17 +596,41 @@ your requirements. ### 802.11r - Fast BSS Transition -Enable 802.11r for fast handoff (<50ms) between APs with the same SSID: +Enable 802.11r for fast handoff (<50ms) between APs with the same SSID. +The access points hand each other a roaming client's keys, protected +with a secret of their own, the key holder secret. Create it in the +keystore first, on every device, with the same value: ``` -admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r +admin@example:/config/> edit keystore symmetric-keys symmetric-key roaming-key +admin@example:/config/keystore/symmetric-keys/symmetric-key/roaming-key/> set key-format passphrase-key-format +admin@example:/config/keystore/symmetric-keys/symmetric-key/roaming-key/> set cleartext-symmetric-key +admin@example:/config/keystore/symmetric-keys/symmetric-key/roaming-key/> end +admin@example:/config/> edit interface wifi0 +admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r key-holder-secret roaming-key admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain 4f57 ``` +Do not reuse the WiFi password as the key holder secret: whoever holds +the secret can read every client's keys, and the clients know the WiFi +password. Configurations from before the key holder secret existed are +upgraded with the WiFi password as the secret, so they keep roaming; +change it to a key of its own. + **Requirements:** - All APs in roaming group must have **identical** SSID - All APs must have **identical** passphrase (same keystore secret) +- All APs must have the **same key holder secret** - All APs must use the **same mobility-domain** identifier +- APs on different devices must be ports of bridges that are connected + to each other, over a cable, a mesh or a WDS backhaul. The APs hand a + client's keys to each other over that network as soon as it connects, + which WPA3 clients need for a fast transition, so a client can keep + roaming after the AP it first connected to has gone down. On a bridge + with VLAN filtering + the keys travel in the APs' VLAN, so each device needs a VLAN + interface on the bridge for that VLAN, the one carrying its IP address + there is enough **Mobility Domain Options:** - Explicit 4-character hex value (e.g., `4f57`) - default if not specified @@ -623,6 +674,16 @@ admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11k Enables neighbor reports and beacon reports, allowing clients to discover nearby APs before roaming. + +Devices with access points on the same network tell each other about +them, over that network, so every access point knows the other access +points of its SSID on the other devices and lists them in its neighbor +reports. A client asking where else its network exists gets the real +answer, and a client asked to move, see below, is told where to. A +device that comes up later is known to the others within a minute. +The exchange is protected with the 802.11r key holder secret, so it +needs fast roaming on the access points, see above. + ### 802.11v - BSS Transition Management Enable 802.11v for network-assisted roaming: @@ -633,6 +694,13 @@ admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v Allows APs to suggest better APs to clients, improving roaming decisions. +An access point about to stop, because the device reboots, is upgraded, +or its WiFi configuration changes, also uses 802.11v to ask its clients +to move first, naming the other access points of the SSID it knows of. +Clients that support it roam to one of them while the radio is still +up, instead of noticing the loss afterwards and scanning for a new +network. Clients without 802.11v are disconnected as before. + #### Band Steering (MBO) Enabling `dot11v` also turns on MBO (Multi-Band Operation), advertised in @@ -641,11 +709,28 @@ the same SSID exists on another band and decide for itself when to move, while 802.11v BSS Transition Management lets the AP suggest a better target. -On top of the client-cooperative hints, the AP applies active steering: -on a 2.4 GHz access-point it suppresses probe responses to clients that -were recently seen on the same SSID on the 5/6 GHz band, nudging -dual-band clients onto the higher band. MBO is **enabled by default** -whenever `dot11v` is enabled: +On top of these hints, the device steers dual-band clients to 5 or 6 GHz +itself. Each radio remembers the clients it has seen during the last +minute. A 2.4 GHz access point whose SSID also exists on a higher band +of the same device treats a client that higher band has seen as +dual-band and in range of it: + +- it does not answer the client's probe requests, so a client choosing + a network tends to pick the higher band, +- a client that is connected on 2.4 GHz anyway, for example because it + joined while the higher band was down, is asked to move with an + 802.11v request naming the higher band, as long as its 2.4 GHz signal + is good enough for the move to make sense and the higher band is up. + A client that shrugs off a couple of requests is left alone for an + hour. + +A client is never refused on 2.4 GHz, so one that cannot get in on the +higher band still has a way in. Clients that only support 2.4 GHz are +never seen on the higher band and are not affected, and clients that +ignore 802.11v requests stay where they are. Band +steering is **enabled by default** whenever `dot11v` is enabled. The +setting is read on the 2.4 GHz access point, the one that defers and +moves clients; on a 5 or 6 GHz access point it has no effect: ``` admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v @@ -704,9 +789,8 @@ Repeat for all APs that should participate in the roaming group. IEEE 802.11s is a wireless mesh networking standard operating at Layer 2. Mesh nodes form peer links directly with each other and route traffic -using HWMP (Hybrid Wireless Mesh Protocol), which is built into the -Linux mac80211 subsystem. There is no central controller; nodes -discover peers and find paths on their own. +using HWMP (Hybrid Wireless Mesh Protocol). There is no central +controller; nodes discover peers and find paths on their own. The standard defines two node roles: @@ -718,11 +802,13 @@ The standard defines two node roles: In practice, a node bridging the mesh interface to a LAN acts as a mesh portal. +For a backhaul with a single root, where multi-hop and self-healing are +not needed, see [WDS Backhaul and Repeaters](#wds-backhaul-and-repeaters). +That variant can use WiFi hardware offloading, mesh cannot. + > [!NOTE] -> Not all WiFi hardware supports 802.11s mesh. The driver must implement -> mesh point mode in mac80211. Check your adapter's capabilities with -> `iw phy info` and look for "mesh point" under "Supported interface -> modes". +> Not all WiFi hardware supports 802.11s mesh, see the adapter list +> under [Supported Wi-Fi Adapters](#supported-wi-fi-adapters). ### 802.11s vs EasyMesh @@ -733,22 +819,22 @@ portal. | **Single point of failure** | None | Controller | | **Multi-hop** | True N-hop | Limited (1-2 hops) | | **Vendor lock-in** | None | Common | -| **Linux support** | Kernel-native (mac80211) | Requires proprietary firmware | +| **Vendor software** | None needed | Required | -Infix uses 802.11s because it runs entirely in the kernel with no -proprietary components. +Infix uses 802.11s because it is an open standard that works across +vendors without proprietary components. ### Mesh configuration -A mesh point requires the radio to have `band`, `channel`, and a valid -`country-code` configured. Mesh and AP modes cannot coexist on the same -radio. +A mesh point requires the radio to have `band` and `channel` configured, +and the system a country code. Mesh and AP modes cannot coexist on the +same radio. **Step 1: Configure the radio**
admin@example:/> configure
+admin@example:/config/> set hardware wifi country-code DE
 admin@example:/config/> edit hardware component radio1 wifi-radio
-admin@example:/config/hardware/component/radio1/wifi-radio/> set country-code DE
 admin@example:/config/hardware/component/radio1/wifi-radio/> set band 5GHz
 admin@example:/config/hardware/component/radio1/wifi-radio/> set channel 36
 admin@example:/config/hardware/component/radio1/wifi-radio/> leave
@@ -805,8 +891,127 @@ AP interfaces for clients on another:
 ![802.11s mesh backhaul with roaming-enabled access points](img/wifi-mesh-roaming.svg)
 
 With 802.11r/k/v roaming enabled on the APs (same SSID, same
-passphrase, same mobility domain), clients hand off between nodes while
-the mesh carries backhaul traffic.
+passphrase, same key holder secret, same mobility domain), clients hand
+off between nodes while the mesh carries backhaul traffic.
+
+## WDS Backhaul and Repeaters
+
+A WiFi station normally carries only its own traffic and cannot be a
+bridge port.  In 4-address mode, also called WDS, it can forward traffic
+for the devices behind it.  That is what makes a device with a station
+and an access point a repeater, and a device with a station and wired
+ports a wireless bridge.
+
+Both ends take part.  The satellite enables `wds` on its station.  The
+root accepts the station on one of its access points and gives it a
+`wds-link` interface: a bridge port, one per satellite, created by
+configuration and tied to the satellite's MAC address.
+
+Compared to an [802.11s mesh](#80211s-mesh-point-mode), a WDS backhaul
+is a star with one root, without multi-hop or self-healing.  In return
+it is a plain access point and station link, which WiFi hardware
+offloading supports where mesh is not.
+
+### Root: access point with WDS ports
+
+On the root, two kinds of interface share the job.  The access point is
+the one the satellites connect to: it advertises the backhaul SSID and
+handles authentication, and there is one per radio.  Each `wds-link`
+is the port for one satellite: that is where its traffic appears and
+what you put in the bridge.  With two satellites on `radio1`:
+
+```
+radio1
+ ├── backhaul    access point, the SSID the satellites connect to
+ ├── wds-garage  port for the garage satellite, bridge port
+ └── wds-attic   port for the attic satellite, bridge port
+```
+
+A 4-address station forwards traffic for the devices behind it, so it
+cannot share the access point's interface with the ordinary clients.
+The `wds-link` is that separate port.  Any access point can take WDS
+stations, even one that serves clients, but give the backhaul its own
+SSID, advertised by the root only, so the satellites can land nowhere
+else.  For each satellite, add a `wds-link` interface naming the access
+point and the satellite's station MAC address, and make it a port of
+the bridge.  The interface uses the access point's radio and MAC
+address, so it takes neither a `radio` nor a `custom-phys-address`.
+
+
admin@root:/config/> edit interface backhaul
+admin@root:/config/interface/backhaul/> set wifi radio radio1
+admin@root:/config/interface/backhaul/> set wifi access-point ssid my-backhaul
+admin@root:/config/interface/backhaul/> set wifi access-point security secret backhaul-key
+admin@root:/config/interface/backhaul/> end
+admin@root:/config/> edit interface wds-garage
+admin@root:/config/interface/wds-garage/> set type wifi
+admin@root:/config/interface/wds-garage/> set wifi wds-link access-point backhaul
+admin@root:/config/interface/wds-garage/> set wifi wds-link peer-address 02:13:37:13:37:12
+admin@root:/config/interface/wds-garage/> set bridge-port bridge br0
+admin@root:/config/interface/wds-garage/> leave
+
+ +VLANs and other bridge port settings are configured on the `wds-link` +interface like on any other port. The port is down until the satellite +connects, and goes down again when it leaves, or within about half a +minute if the satellite disappears without notice: + +
admin@root:/> show interface wds-garage
+name               : wds-garage
+type               : wifi
+operational status : up
+higher-layer-if    : br0
+mode               : wds-link
+connected          : yes
+signal             : -48 dBm (good)
+
+ +### Satellite: 4-address station + +On the satellite, configure a station for the backhaul SSID with `wds` +enabled and make it a bridge port, next to the wired ports and any local +access points. `peer-bssid` is optional and pins the station to the +root's access point: + +
admin@garage:/config/> edit interface uplink
+admin@garage:/config/interface/uplink/> set wifi radio radio1
+admin@garage:/config/interface/uplink/> set wifi station ssid my-backhaul
+admin@garage:/config/interface/uplink/> set wifi station wds true
+admin@garage:/config/interface/uplink/> set wifi station peer-bssid 02:13:37:13:37:01
+admin@garage:/config/interface/uplink/> set wifi station security secret backhaul-key
+admin@garage:/config/interface/uplink/> set bridge-port bridge br0
+admin@garage:/config/interface/uplink/> leave
+
+ +A station without `wds` cannot be a bridge port, the configuration is +rejected. + +The access point at the other end must accept 4-address stations. A +root with a `wds-link` for the satellite does, and so do most access +points with a WDS or 4-address option. One that does not still lets +the station connect and authenticate, then drops all its traffic. +Behind such an access point, use a plain station with an address of its +own and route or masquerade the network behind it instead, see +[Firewall](firewall.md). + +### Repeater + +A repeater is a satellite that also runs an access point for clients, +bridged with the backhaul station. The station and the access point +can share a radio, but then all radios in the backhaul must use the same +channel: the station follows the root's channel and the local access +point has a fixed one. A channel change on the root, for example from +radar detection, leaves the satellites disconnected until they are +reconfigured. + +Clients on a repeater keep their own MAC addresses, so DHCP reservations +and per-port VLANs work as on a wired network. With the same client +SSID on the root and the repeaters, the [roaming +features](#fast-roaming-between-access-points) apply as usual. Keep the +backhaul SSID separate from the client SSIDs, or a satellite may connect +to another satellite instead of the root. + +A satellite with both a WDS backhaul and a cable to the same LAN forms a +loop, as with any two bridge ports to the same network. ## Troubleshooting @@ -819,8 +1024,8 @@ If issues arise, try the following troubleshooting steps: the passphrase matches the network password 3. **Review logs**: Check system logs with `show log` for Wi-Fi related errors -4. **Regulatory compliance**: Ensure the country-code on the radio - matches your location +4. **Regulatory compliance**: Ensure the WiFi country code matches your + location 5. **Hardware detection**: Confirm the WiFi radio appears in `show hardware` diff --git a/package/feature-wifi/wifimedium b/package/feature-wifi/wifimedium index 58aeb4d84..55d335e88 100755 --- a/package/feature-wifi/wifimedium +++ b/package/feature-wifi/wifimedium @@ -13,7 +13,9 @@ This daemon is that process. On each DUT it: * registers on the "mac80211_hwsim" genl family and receives every frame the local radios transmit (HWSIM_CMD_FRAME), * acknowledges each transmit back to the kernel (HWSIM_CMD_TX_INFO_FRAME) so - mac80211's TX path completes, and + mac80211's TX path completes. A unicast frame is only acknowledged if its + receiver has been heard on the medium recently, so a station that vanishes + stops acking like on real RF and the AP's inactivity probing works, and * relays the frame per radio: each radio (phy radioN) is paired by name with a carrier NIC (netdev radioN) that joins one multicast "cell" -- a QEMU socket multicast group shared by every DUT's radioN (see test/virt/quad and @@ -181,9 +183,11 @@ class Netlink: attrs += put_attr(HWSIM_ATTR_FREQ, struct.pack("=I", freq)) self._send(self.family_id, HWSIM_CMD_FRAME, attrs) - def tx_ack(self, transmitter, flags, tx_info, tx_info_flags, cookie): + def tx_ack(self, transmitter, flags, tx_info, tx_info_flags, cookie, ack=True): + if ack: + flags |= HWSIM_TX_STAT_ACK attrs = put_attr(HWSIM_ATTR_ADDR_TRANSMITTER, transmitter) - attrs += put_attr(HWSIM_ATTR_FLAGS, struct.pack("=I", flags | HWSIM_TX_STAT_ACK)) + attrs += put_attr(HWSIM_ATTR_FLAGS, struct.pack("=I", flags)) attrs += put_attr(HWSIM_ATTR_SIGNAL, struct.pack("=i", RX_SIGNAL)) if tx_info: attrs += put_attr(HWSIM_ATTR_TX_INFO, tx_info) @@ -232,7 +236,10 @@ def open_medium(ifname): # The carrier NIC is unconfigured (it is not a real DUT port), so Infix # leaves it administratively down -- a raw packet socket on a down link # carries no frames. Bring it up; wifimedium owns the medium, like hwsim0. - ifup(ifname) + # A full-size data frame is a 1500 byte MSDU plus 802.11 header, mesh + # control, encryption and LLC, and A-MSDUs are larger still, so the + # carrier needs a far larger MTU than the default 1500. + ifup(ifname, mtu=9000) sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_WIFIMEDIUM)) sock.bind((ifname, ETH_P_WIFIMEDIUM)) @@ -251,6 +258,25 @@ def open_medium(ifname): # raw 802.11 frame. WIRE = struct.Struct("=6sI") +# Transmitter addresses heard on the medium and when. mac80211 stations send +# a keep-alive at least every 30 s when idle, so one not heard for twice that +# is gone and frames to it go unacknowledged. +ALIVE_TIMEOUT = 60.0 +seen = {} + + +def heard(frame): + if len(frame) >= 16: + seen[frame[10:16]] = time.monotonic() + + +def acked(frame): + """Would the receiver of this frame acknowledge it?""" + if len(frame) < 10 or frame[4] & 1: + return True # multicast: no ack expected, keep hwsim's default + last = seen.get(frame[4:10]) + return last is not None and time.monotonic() - last < ALIVE_TIMEOUT + def wait_radios_renamed(timeout=20): """Wait until the hwsim phys have been renamed phyN -> radioN. @@ -307,8 +333,10 @@ def discover_radios(): return radios -def ifup(ifname): - """Bring an interface up (best effort).""" +def ifup(ifname, mtu=None): + """Bring an interface up (best effort), optionally with a larger MTU.""" + if mtu: + subprocess.run(["ip", "link", "set", ifname, "mtu", str(mtu)], check=False) subprocess.run(["ip", "link", "set", ifname, "up"], check=False) @@ -364,13 +392,17 @@ def main(): # Complete the kernel TX path regardless of delivery. nl.tx_ack(tx, flags, a.get(HWSIM_ATTR_TX_INFO), a.get(HWSIM_ATTR_TX_INFO_FLAGS), - a.get(HWSIM_ATTR_COOKIE)) + a.get(HWSIM_ATTR_COOKIE), acked(frame)) # Send only onto the transmitting radio's own carrier. r = by_addr1.get(tx) if r: - r["sock"].send(ETH_BROADCAST + r["mac"] + ETYPE + - WIRE.pack(tx, freq) + frame) - dbg(f"tx {r['name']} freq={freq} len={len(frame)}") + try: + r["sock"].send(ETH_BROADCAST + r["mac"] + ETYPE + + WIRE.pack(tx, freq) + frame) + dbg(f"tx {r['name']} freq={freq} len={len(frame)}") + except OSError as e: + # Lost on the air, like a collision would be. + log(f"tx {r['name']} len={len(frame)} dropped: {e}") else: dbg(f"tx from unknown radio {tx.hex()} -- dropped") off += nla_align(mlen) @@ -389,6 +421,7 @@ def main(): continue tx, freq = WIRE.unpack_from(pkt, 14) frame = pkt[14 + WIRE.size:] + heard(frame) # Inject into THIS radio only -- its carrier is its cell. nl.inject(r["addr1"], frame, freq) dbg(f"rx {r['name']} tx={tx.hex()} freq={freq} len={len(frame)}") diff --git a/patches/hostapd/0003-hostapd-Allow-binding-WDS-stations-to-preconfigured-i.patch b/patches/hostapd/0003-hostapd-Allow-binding-WDS-stations-to-preconfigured-i.patch new file mode 100644 index 000000000..98650c753 --- /dev/null +++ b/patches/hostapd/0003-hostapd-Allow-binding-WDS-stations-to-preconfigured-i.patch @@ -0,0 +1,268 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= +Date: Fri, 2 Oct 2026 10:00:00 +0200 +Subject: [PATCH 3/3] hostapd: Allow binding WDS stations to preconfigured + interfaces +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +A 4-address WDS station is bound to an AP_VLAN interface that hostapd +creates on association, named .sta, and removes again on +disassociation. Nothing outside hostapd can configure such a port +ahead of time, since neither its name nor its lifetime is known. + +Add a per-BSS option mapping a station MAC address to an interface +name: + + wds_sta_ifname= + +A matching station is bound to that interface instead. The interface +must already exist; hostapd brings it up and binds the station on +association, and on disassociation only sets it down. Its bridge +membership is never touched, so an external network manager can +create the port, enslave it and configure it like any other bridge +port before the station ever shows up. + +A name of IFNAMSIZ characters or a second line for the same address is +rejected, and a port the kernel refuses to bind the station to is set +down again. + +Stations without a mapping keep the current behaviour. + +Signed-off-by: Mattias Walström +--- +diff -ruN a/hostapd/config_file.c b/hostapd/config_file.c +--- a/hostapd/config_file.c ++++ b/hostapd/config_file.c +@@ -2368,6 +2368,57 @@ + #endif /* CONFIG_TESTING_OPTIONS */ + + ++/* ++ * wds_sta_ifname= ++ * ++ * Bind the 4-address WDS station with the given MAC address to an ++ * existing, externally managed interface instead of creating ++ * .sta. The interface is left in place when the station ++ * disassociates, and its bridge membership is never touched. ++ */ ++static int hostapd_config_parse_wds_sta_ifname(struct hostapd_bss_config *bss, ++ char *pos, int line) ++{ ++ struct hostapd_wds_sta_ifname *e, *dup; ++ char *ifname; ++ ++ ifname = os_strchr(pos, ' '); ++ if (!ifname) ++ goto fail; ++ *ifname++ = '\0'; ++ while (*ifname == ' ') ++ ifname++; ++ if (!*ifname || os_strlen(ifname) >= IFNAMSIZ) ++ goto fail; ++ ++ e = os_zalloc(sizeof(*e)); ++ if (!e) ++ return 1; ++ if (hwaddr_aton(pos, e->addr)) { ++ os_free(e); ++ goto fail; ++ } ++ for (dup = bss->wds_sta_ifname; dup; dup = dup->next) { ++ if (os_memcmp(dup->addr, e->addr, ETH_ALEN) == 0) { ++ os_free(e); ++ wpa_printf(MSG_ERROR, ++ "Line %d: duplicate wds_sta_ifname for %s", ++ line, pos); ++ return 1; ++ } ++ } ++ os_strlcpy(e->ifname, ifname, sizeof(e->ifname)); ++ e->next = bss->wds_sta_ifname; ++ bss->wds_sta_ifname = e; ++ ++ return 0; ++fail: ++ wpa_printf(MSG_ERROR, "Line %d: invalid wds_sta_ifname '%s'", ++ line, pos); ++ return 1; ++} ++ ++ + static int hostapd_config_fill(struct hostapd_config *conf, + struct hostapd_bss_config *bss, + const char *buf, char *pos, int line) +@@ -2383,6 +2434,9 @@ + os_strlcpy(bss->vlan_bridge, pos, sizeof(bss->vlan_bridge)); + } else if (os_strcmp(buf, "wds_bridge") == 0) { + os_strlcpy(bss->wds_bridge, pos, sizeof(bss->wds_bridge)); ++ } else if (os_strcmp(buf, "wds_sta_ifname") == 0) { ++ if (hostapd_config_parse_wds_sta_ifname(bss, pos, line)) ++ return 1; + } else if (os_strcmp(buf, "driver") == 0) { + int j; + const struct wpa_driver_ops *driver = NULL; +diff -ruN a/src/ap/ap_config.c b/src/ap/ap_config.c +--- a/src/ap/ap_config.c ++++ b/src/ap/ap_config.c +@@ -890,6 +890,12 @@ + os_free(conf->radius); + os_free(conf->radius_das_shared_secret); + hostapd_config_free_vlan(conf); ++ while (conf->wds_sta_ifname) { ++ struct hostapd_wds_sta_ifname *e = conf->wds_sta_ifname; ++ ++ conf->wds_sta_ifname = e->next; ++ os_free(e); ++ } + os_free(conf->time_zone); + os_free(conf->supported_rates); + os_free(conf->basic_rates); +diff -ruN a/src/ap/ap_config.h b/src/ap/ap_config.h +--- a/src/ap/ap_config.h ++++ b/src/ap/ap_config.h +@@ -281,6 +281,12 @@ + /** + * struct hostapd_bss_config - Per-BSS configuration + */ ++struct hostapd_wds_sta_ifname { ++ struct hostapd_wds_sta_ifname *next; ++ u8 addr[ETH_ALEN]; ++ char ifname[IFNAMSIZ + 1]; ++}; ++ + struct hostapd_bss_config { + char iface[IFNAMSIZ + 1]; + char bridge[IFNAMSIZ + 1]; +@@ -357,6 +363,7 @@ + struct mac_acl_entry *deny_mac; + int num_deny_mac; + int wds_sta; ++ struct hostapd_wds_sta_ifname *wds_sta_ifname; + int isolate; + int start_disabled; + +diff -ruN a/src/ap/ap_drv_ops.c b/src/ap/ap_drv_ops.c +--- a/src/ap/ap_drv_ops.c ++++ b/src/ap/ap_drv_ops.c +@@ -391,7 +391,9 @@ + int hostapd_set_wds_sta(struct hostapd_data *hapd, char *ifname_wds, + const u8 *addr, int aid, int val) + { ++ struct hostapd_wds_sta_ifname *e; + const char *bridge = NULL; ++ char name[IFNAMSIZ + 1]; + + if (hapd->driver == NULL || hapd->driver->set_wds_sta == NULL) + return -1; +@@ -399,6 +401,20 @@ + bridge = hapd->conf->wds_bridge; + else if (hapd->conf->bridge[0]) + bridge = hapd->conf->bridge; ++ ++ if (!ifname_wds) ++ ifname_wds = name; ++ ifname_wds[0] = '\0'; ++ for (e = hapd->conf->wds_sta_ifname; e; e = e->next) { ++ if (os_memcmp(e->addr, addr, ETH_ALEN) != 0) ++ continue; ++ /* Externally managed interface: hand the driver its name ++ * and keep it out of any bridge handling. */ ++ os_strlcpy(ifname_wds, e->ifname, IFNAMSIZ + 1); ++ bridge = NULL; ++ break; ++ } ++ + return hapd->driver->set_wds_sta(hapd->drv_priv, addr, aid, val, + bridge, ifname_wds); + } +diff -ruN a/src/drivers/driver.h b/src/drivers/driver.h +--- a/src/drivers/driver.h ++++ b/src/drivers/driver.h +@@ -4518,7 +4518,11 @@ + * @bridge_ifname: Bridge interface to use for the WDS station or %NULL + * to indicate that bridge is not to be used + * @ifname_wds: Buffer to return the interface name for the new WDS +- * station or %NULL to indicate name is not returned. ++ * station or %NULL to indicate name is not returned. If the ++ * buffer holds a name on entry, that existing interface is used ++ * as is: it is not created, not added to or removed from a ++ * bridge, and is only set down, not removed, when the station ++ * is unbound. + * Returns: 0 on success, -1 on failure + */ + int (*set_wds_sta)(void *priv, const u8 *addr, int aid, int val, +diff -ruN a/src/drivers/driver_nl80211.c b/src/drivers/driver_nl80211.c +--- a/src/drivers/driver_nl80211.c ++++ b/src/drivers/driver_nl80211.c +@@ -9317,22 +9317,35 @@ + char name[IFNAMSIZ + 1]; + union wpa_event_data event; + bool add_br = false; ++ bool external = false; + int ret; + +- ret = os_snprintf(name, sizeof(name), "%s.sta%d", bss->ifname, aid); +- if (ret >= (int) sizeof(name)) +- wpa_printf(MSG_WARNING, +- "nl80211: WDS interface name was truncated"); +- else if (ret < 0) +- return ret; +- +- if (ifname_wds) +- os_strlcpy(ifname_wds, name, IFNAMSIZ + 1); ++ if (ifname_wds && ifname_wds[0]) { ++ os_strlcpy(name, ifname_wds, sizeof(name)); ++ external = true; ++ } else { ++ ret = os_snprintf(name, sizeof(name), "%s.sta%d", bss->ifname, ++ aid); ++ if (ret >= (int) sizeof(name)) ++ wpa_printf(MSG_WARNING, ++ "nl80211: WDS interface name was truncated"); ++ else if (ret < 0) ++ return ret; ++ ++ if (ifname_wds) ++ os_strlcpy(ifname_wds, name, IFNAMSIZ + 1); ++ } + + wpa_printf(MSG_DEBUG, "nl80211: Set WDS STA addr=" MACSTR + " aid=%d val=%d name=%s", MAC2STR(addr), aid, val, name); + if (val) { + if (!if_nametoindex(name)) { ++ if (external) { ++ wpa_printf(MSG_ERROR, ++ "nl80211: WDS STA interface %s does not exist", ++ name); ++ return -1; ++ } + if (nl80211_create_iface(drv, name, + NL80211_IFTYPE_AP_VLAN, + bss->addr, 1, NULL, NULL, 0) < +@@ -9360,9 +9373,22 @@ + bridge_ifname, name) < 0) + return -1; + +- return i802_set_sta_vlan(priv, addr, name, 0, +- NL80211_DRV_LINK_ID_NA); ++ ret = i802_set_sta_vlan(priv, addr, name, 0, ++ NL80211_DRV_LINK_ID_NA); ++ /* The kernel only binds a station to an AP_VLAN of this ++ * BSS, so a refused external interface is not ours to keep ++ * up. */ ++ if (ret < 0 && external) ++ linux_set_iface_flags(drv->global->ioctl_sock, name, 0); ++ return ret; + } else { ++ if (external) { ++ i802_set_sta_vlan(priv, addr, bss->ifname, 0, ++ NL80211_DRV_LINK_ID_NA); ++ linux_set_iface_flags(drv->global->ioctl_sock, name, 0); ++ return 0; ++ } ++ + if (bridge_ifname && + linux_br_del_if(drv->global->ioctl_sock, bridge_ifname, + name) < 0) diff --git a/patches/hostapd/0004-hostapd-Add-ft_iface-for-the-802.11r-key-holder-excha.patch b/patches/hostapd/0004-hostapd-Add-ft_iface-for-the-802.11r-key-holder-excha.patch new file mode 100644 index 000000000..8c76a3077 --- /dev/null +++ b/patches/hostapd/0004-hostapd-Add-ft_iface-for-the-802.11r-key-holder-excha.patch @@ -0,0 +1,79 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= +Date: Tue, 7 Oct 2026 15:00:00 +0200 +Subject: [PATCH 4/4] hostapd: Add ft_iface for the 802.11r key holder exchange +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The R0KH/R1KH exchange is sent on the bridge the BSS is a port of, or +on the BSS interface itself. On a VLAN filtering bridge a frame sent +from the bridge device belongs to the bridge's own untagged VLAN, if it +has one, which need not be the VLAN of the access points. Nor can the +bridge option name another interface: hostapd then moves the BSS into +that bridge. + +Add a per-BSS option for the interface to use for the exchange, for +example the VLAN interface of the access points' VLAN: + + ft_iface= + +Signed-off-by: Mattias Walström +--- + hostapd/config_file.c | 2 ++ + hostapd/hostapd.conf | 5 +++++ + src/ap/ap_config.h | 1 + + src/ap/wpa_auth_glue.c | 3 ++- + 4 files changed, 10 insertions(+), 1 deletion(-) + +diff -ruN a/hostapd/config_file.c b/hostapd/config_file.c +--- a/hostapd/config_file.c ++++ b/hostapd/config_file.c +@@ -2419,6 +2419,8 @@ + sizeof(conf->bss[0]->iface)); + } else if (os_strcmp(buf, "bridge") == 0) { + os_strlcpy(bss->bridge, pos, sizeof(bss->bridge)); ++ } else if (os_strcmp(buf, "ft_iface") == 0) { ++ os_strlcpy(bss->ft_iface, pos, sizeof(bss->ft_iface)); + } else if (os_strcmp(buf, "bridge_hairpin") == 0) { + bss->bridge_hairpin = atoi(pos); + } else if (os_strcmp(buf, "vlan_bridge") == 0) { +diff -ruN a/hostapd/hostapd.conf b/hostapd/hostapd.conf +--- a/hostapd/hostapd.conf ++++ b/hostapd/hostapd.conf +@@ -19,6 +19,11 @@ + # has been started to change the interface mode). If needed, the bridge + # interface is also created. + #bridge=br0 ++ ++# Interface to send and receive the 802.11r R0KH/R1KH exchange on. Defaults ++# to the bridge, or to the BSS interface when no bridge is configured. Use ++# it to name the VLAN interface of the access points on a VLAN aware bridge. ++#ft_iface=br0.1 + + # Driver interface type (wired/none/nl80211/bsd); + # default: nl80211). nl80211 is used with all Linux mac80211 drivers. +diff -ruN a/src/ap/ap_config.h b/src/ap/ap_config.h +--- a/src/ap/ap_config.h ++++ b/src/ap/ap_config.h +@@ -290,6 +290,7 @@ + struct hostapd_bss_config { + char iface[IFNAMSIZ + 1]; + char bridge[IFNAMSIZ + 1]; ++ char ft_iface[IFNAMSIZ + 1]; + char vlan_bridge[IFNAMSIZ + 1]; + char wds_bridge[IFNAMSIZ + 1]; + int bridge_hairpin; /* hairpin_mode on bridge members */ +diff -ruN a/src/ap/wpa_auth_glue.c b/src/ap/wpa_auth_glue.c +--- a/src/ap/wpa_auth_glue.c ++++ b/src/ap/wpa_auth_glue.c +@@ -1918,7 +1918,8 @@ + wpa_key_mgmt_ft(hapd->conf->wpa_key_mgmt)) { + const char *ft_iface; + +- ft_iface = hapd->conf->bridge[0] ? hapd->conf->bridge : ++ ft_iface = hapd->conf->ft_iface[0] ? hapd->conf->ft_iface : ++ hapd->conf->bridge[0] ? hapd->conf->bridge : + hapd->conf->iface; + hapd->l2 = l2_packet_init(ft_iface, NULL, ETH_P_RRB, + hostapd_rrb_receive, hapd, 1); diff --git a/patches/hostapd/0005-hostapd-Push-PMK-R1-to-the-R1KHs-added-by-RELOAD_RXK.patch b/patches/hostapd/0005-hostapd-Push-PMK-R1-to-the-R1KHs-added-by-RELOAD_RXK.patch new file mode 100644 index 000000000..08f3a0565 --- /dev/null +++ b/patches/hostapd/0005-hostapd-Push-PMK-R1-to-the-R1KHs-added-by-RELOAD_RXK.patch @@ -0,0 +1,79 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= +Date: Thu, 9 Oct 2026 18:30:00 +0200 +Subject: [PATCH 5/5] hostapd: Push PMK-R1 to the R1KHs added by RELOAD_RXKHS +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +With pmk_r1_push the R0KH hands a PMK-R1 to every configured R1KH when +a station authenticates, and never again. An R1KH configured later, +through RELOAD_RXKHS, gets nothing for the stations already here, so a +roam to it still has to pull from this R0KH, which fails once this +access point has gone down. + +Push the keys of every station to the R1KHs again after a reload. An +R1KH that already has a key just replaces it. + +Signed-off-by: Mattias Walström +--- + hostapd/ctrl_iface.c | 5 +++++ + src/ap/wpa_auth.h | 1 + + src/ap/wpa_auth_ft.c | 21 +++++++++++++++++++++ + 3 files changed, 27 insertions(+) + +diff -ruN a/hostapd/ctrl_iface.c b/hostapd/ctrl_iface.c +--- a/hostapd/ctrl_iface.c ++++ b/hostapd/ctrl_iface.c +@@ -1527,6 +1527,11 @@ + return -1; + } + ++ /* R1KHs added now have not seen the keys of the stations already ++ * authenticated here, hand them over so a roam to them does not ++ * depend on this R0KH still being around. */ ++ wpa_ft_push_pmk_r1_all(hapd->wpa_auth); ++ + return 0; + } + +diff -ruN a/src/ap/wpa_auth_ft.c b/src/ap/wpa_auth_ft.c +--- a/src/ap/wpa_auth_ft.c ++++ b/src/ap/wpa_auth_ft.c +@@ -4976,4 +4976,25 @@ + } + } + ++ ++/* ++ * Push the PMK-R1 of every station this R0KH holds a PMK-R0 for, to ++ * all configured R1KHs. For use when the R1KH list has changed: the ++ * push at authentication only reached the R1KHs known at the time. ++ */ ++void wpa_ft_push_pmk_r1_all(struct wpa_authenticator *wpa_auth) ++{ ++ struct wpa_ft_pmk_cache *cache; ++ struct wpa_ft_pmk_r0_sa *r0; ++ ++ if (!wpa_auth || !wpa_auth->conf.pmk_r1_push || !wpa_auth->ft_pmk_cache) ++ return; ++ ++ cache = wpa_auth->ft_pmk_cache; ++ dl_list_for_each(r0, &cache->pmk_r0, struct wpa_ft_pmk_r0_sa, list) { ++ r0->pmk_r1_pushed = 0; ++ wpa_ft_push_pmk_r1(wpa_auth, r0->spa); ++ } ++} ++ + #endif /* CONFIG_IEEE80211R_AP */ +diff -ruN a/src/ap/wpa_auth.h b/src/ap/wpa_auth.h +--- a/src/ap/wpa_auth.h ++++ b/src/ap/wpa_auth.h +@@ -594,6 +594,7 @@ + const u8 *dst_addr, u8 oui_suffix, const u8 *data, + size_t data_len); + void wpa_ft_push_pmk_r1(struct wpa_authenticator *wpa_auth, const u8 *addr); ++void wpa_ft_push_pmk_r1_all(struct wpa_authenticator *wpa_auth); + void wpa_ft_deinit(struct wpa_authenticator *wpa_auth); + void wpa_ft_sta_deinit(struct wpa_state_machine *sm); + int wpa_ft_fetch_pmk_r1(struct wpa_authenticator *wpa_auth, diff --git a/patches/hostapd/0006-hostapd-Cancel-the-key-holder-timers-before-RELOAD_RX.patch b/patches/hostapd/0006-hostapd-Cancel-the-key-holder-timers-before-RELOAD_RX.patch new file mode 100644 index 000000000..ab35172c7 --- /dev/null +++ b/patches/hostapd/0006-hostapd-Cancel-the-key-holder-timers-before-RELOAD_RX.patch @@ -0,0 +1,35 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= +Date: Sat, 11 Oct 2026 10:00:00 +0200 +Subject: [PATCH 6/6] hostapd: Cancel the key holder timers before RELOAD_RXKHS + frees them +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +A key holder learned through a wildcard R0KH or R1KH entry is freed by +a timer, and every entry may hold sequence number state with a timer of +its own. RELOAD_RXKHS freed the lists without cancelling either, so a +delete timer later fired on whatever entry had been allocated at the +old address and removed it, and the sequence state leaked. + +Tear the timers and sequence state down first, the way wpa_deinit does. + +Signed-off-by: Mattias Walström +--- +diff -ruN a/hostapd/ctrl_iface.c b/hostapd/ctrl_iface.c +--- a/hostapd/ctrl_iface.c ++++ b/hostapd/ctrl_iface.c +@@ -1518,6 +1518,12 @@ + struct hostapd_bss_config *conf = hapd->conf; + int err; + ++ /* Key holders learned through the wildcard entries carry a delete ++ * timer, and every entry may hold sequence number state. Drop ++ * both before the lists are freed under them, or the timer fires ++ * on whatever entry has since taken the address. */ ++ if (hapd->wpa_auth) ++ wpa_ft_deinit(hapd->wpa_auth); + hostapd_config_clear_rxkhs(conf); + + err = hostapd_config_read_rxkh_file(conf, conf->rxkh_file); diff --git a/patches/linux/6.18.55/0001-net-phy-marvell10g-Support-firmware-loading-on-88X33.patch b/patches/linux/6.18.55/0001-net-phy-marvell10g-Support-firmware-loading-on-88X33.patch index 761e03a8b..b97cb0c84 100644 --- a/patches/linux/6.18.55/0001-net-phy-marvell10g-Support-firmware-loading-on-88X33.patch +++ b/patches/linux/6.18.55/0001-net-phy-marvell10g-Support-firmware-loading-on-88X33.patch @@ -1,7 +1,7 @@ From 1bdc682ca8860bb8299b6c25cc91ebf032a23cfa Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 19 Sep 2023 18:38:10 +0200 -Subject: [PATCH 01/78] net: phy: marvell10g: Support firmware loading on +Subject: [PATCH 01/84] net: phy: marvell10g: Support firmware loading on 88X3310 When probing, if a device is waiting for firmware to be loaded into @@ -15,7 +15,7 @@ fall back. 1 file changed, 161 insertions(+) diff --git a/drivers/net/phy/marvell10g.c b/drivers/net/phy/marvell10g.c -index 8fd42131cdbf..44294519231e 100644 +index 8fd42131cdbf9..44294519231ed 100644 --- a/drivers/net/phy/marvell10g.c +++ b/drivers/net/phy/marvell10g.c @@ -25,6 +25,7 @@ diff --git a/patches/linux/6.18.55/0002-net-phy-marvell10g-Fix-power-up-when-strapped-to-sta.patch b/patches/linux/6.18.55/0002-net-phy-marvell10g-Fix-power-up-when-strapped-to-sta.patch index 713ec69a7..f64c022d2 100644 --- a/patches/linux/6.18.55/0002-net-phy-marvell10g-Fix-power-up-when-strapped-to-sta.patch +++ b/patches/linux/6.18.55/0002-net-phy-marvell10g-Fix-power-up-when-strapped-to-sta.patch @@ -1,7 +1,7 @@ From 920227e8923cf970730a4bed22bd2aa55dc9387e Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 21 Nov 2023 20:15:24 +0100 -Subject: [PATCH 02/78] net: phy: marvell10g: Fix power-up when strapped to +Subject: [PATCH 02/84] net: phy: marvell10g: Fix power-up when strapped to start powered down On devices which are hardware strapped to start powered down (PDSTATE @@ -12,7 +12,7 @@ affected by this setting. 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/drivers/net/phy/marvell10g.c b/drivers/net/phy/marvell10g.c -index 44294519231e..40439db49601 100644 +index 44294519231ed..40439db49601d 100644 --- a/drivers/net/phy/marvell10g.c +++ b/drivers/net/phy/marvell10g.c @@ -322,11 +322,23 @@ static int mv3310_power_down(struct phy_device *phydev) diff --git a/patches/linux/6.18.55/0003-net-phy-marvell10g-Add-LED-support-for-88X3310.patch b/patches/linux/6.18.55/0003-net-phy-marvell10g-Add-LED-support-for-88X3310.patch index 370716246..5202d6d5b 100644 --- a/patches/linux/6.18.55/0003-net-phy-marvell10g-Add-LED-support-for-88X3310.patch +++ b/patches/linux/6.18.55/0003-net-phy-marvell10g-Add-LED-support-for-88X3310.patch @@ -1,7 +1,7 @@ From c473017acbed8a778f07f9f1ce55028e85a9ad62 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Wed, 15 Nov 2023 20:58:42 +0100 -Subject: [PATCH 03/78] net: phy: marvell10g: Add LED support for 88X3310 +Subject: [PATCH 03/84] net: phy: marvell10g: Add LED support for 88X3310 Pickup the LEDs from the state in which the hardware reset or bootloader left them, but also support further configuration via @@ -21,7 +21,7 @@ Trigger support: 1 file changed, 422 insertions(+) diff --git a/drivers/net/phy/marvell10g.c b/drivers/net/phy/marvell10g.c -index 40439db49601..7ae4744f147b 100644 +index 40439db49601d..7ae4744f147bb 100644 --- a/drivers/net/phy/marvell10g.c +++ b/drivers/net/phy/marvell10g.c @@ -28,6 +28,7 @@ diff --git a/patches/linux/6.18.55/0004-net-phy-marvell10g-Support-LEDs-tied-to-a-single-med.patch b/patches/linux/6.18.55/0004-net-phy-marvell10g-Support-LEDs-tied-to-a-single-med.patch index 09f301669..1791e5bce 100644 --- a/patches/linux/6.18.55/0004-net-phy-marvell10g-Support-LEDs-tied-to-a-single-med.patch +++ b/patches/linux/6.18.55/0004-net-phy-marvell10g-Support-LEDs-tied-to-a-single-med.patch @@ -1,7 +1,7 @@ From 7ad37cfa715d86cc20f29432f106cedd457e4f26 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 12 Dec 2023 09:51:05 +0100 -Subject: [PATCH 04/78] net: phy: marvell10g: Support LEDs tied to a single +Subject: [PATCH 04/84] net: phy: marvell10g: Support LEDs tied to a single media side In a combo-port setup, i.e. where both the copper and fiber interface @@ -18,7 +18,7 @@ versa for the SFP cage. 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/drivers/net/phy/marvell10g.c b/drivers/net/phy/marvell10g.c -index 7ae4744f147b..5e48355ffcef 100644 +index 7ae4744f147bb..5e48355ffcef4 100644 --- a/drivers/net/phy/marvell10g.c +++ b/drivers/net/phy/marvell10g.c @@ -192,6 +192,9 @@ struct mv3310_chip { diff --git a/patches/linux/6.18.55/0005-net-phy-Do-not-resume-PHY-when-attaching.patch b/patches/linux/6.18.55/0005-net-phy-Do-not-resume-PHY-when-attaching.patch index 4b8eb49ac..1ddd979d2 100644 --- a/patches/linux/6.18.55/0005-net-phy-Do-not-resume-PHY-when-attaching.patch +++ b/patches/linux/6.18.55/0005-net-phy-Do-not-resume-PHY-when-attaching.patch @@ -1,7 +1,7 @@ From 5f95f31c7e48087019db7d673ff32a31f4e06619 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Wed, 27 Mar 2024 10:10:19 +0100 -Subject: [PATCH 05/78] net: phy: Do not resume PHY when attaching +Subject: [PATCH 05/84] net: phy: Do not resume PHY when attaching The PHY should not start negotiating with its link-partner until explicitly instructed to do so. @@ -19,7 +19,7 @@ administratively down. 1 file changed, 1 deletion(-) diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c -index 26b08e3dbd1d..7597308534a0 100644 +index 26b08e3dbd1de..7597308534a0a 100644 --- a/drivers/net/phy/phy_device.c +++ b/drivers/net/phy/phy_device.c @@ -1753,7 +1753,6 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev, diff --git a/patches/linux/6.18.55/0006-net-bridge-avoid-classifying-unknown-multicast-as-mr.patch b/patches/linux/6.18.55/0006-net-bridge-avoid-classifying-unknown-multicast-as-mr.patch index fa5d143f3..06c87d32e 100644 --- a/patches/linux/6.18.55/0006-net-bridge-avoid-classifying-unknown-multicast-as-mr.patch +++ b/patches/linux/6.18.55/0006-net-bridge-avoid-classifying-unknown-multicast-as-mr.patch @@ -1,7 +1,7 @@ From c77bae505246ca5585adfa9219077af6fe98efa7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 4 Mar 2024 16:47:28 +0100 -Subject: [PATCH 06/78] net: bridge: avoid classifying unknown multicast as +Subject: [PATCH 06/84] net: bridge: avoid classifying unknown multicast as mrouters_only Unknown multicast, MAC/IPv4/IPv6, should always be flooded according to @@ -26,7 +26,7 @@ Signed-off-by: Joachim Wiberg 7 files changed, 57 insertions(+), 14 deletions(-) diff --git a/include/uapi/linux/if_bridge.h b/include/uapi/linux/if_bridge.h -index e52f8207ab27..306fa8a94c81 100644 +index e52f8207ab278..306fa8a94c819 100644 --- a/include/uapi/linux/if_bridge.h +++ b/include/uapi/linux/if_bridge.h @@ -835,6 +835,7 @@ enum br_boolopt_id { @@ -38,7 +38,7 @@ index e52f8207ab27..306fa8a94c81 100644 }; diff --git a/net/bridge/br.c b/net/bridge/br.c -index c37e52e2f29a..0759bc37d6f0 100644 +index c37e52e2f29ad..0759bc37d6f0f 100644 --- a/net/bridge/br.c +++ b/net/bridge/br.c @@ -312,6 +312,9 @@ int br_boolopt_toggle(struct net_bridge *br, enum br_boolopt_id opt, bool on, @@ -61,7 +61,7 @@ index c37e52e2f29a..0759bc37d6f0 100644 /* shouldn't be called with unsupported options */ WARN_ON(1); diff --git a/net/bridge/br_device.c b/net/bridge/br_device.c -index 3055a72959cb..0e5e7821d44c 100644 +index 3055a72959cb6..0e5e7821d44cd 100644 --- a/net/bridge/br_device.c +++ b/net/bridge/br_device.c @@ -89,10 +89,10 @@ netdev_tx_t br_dev_xmit(struct sk_buff *skb, struct net_device *dev) @@ -101,7 +101,7 @@ index 3055a72959cb..0e5e7821d44c 100644 br_stp_timer_init(br); br_multicast_init(br); diff --git a/net/bridge/br_forward.c b/net/bridge/br_forward.c -index dea09096ad0f..f0a613238e0d 100644 +index dea09096ad0fb..f0a613238e0d5 100644 --- a/net/bridge/br_forward.c +++ b/net/bridge/br_forward.c @@ -199,15 +199,20 @@ static struct net_bridge_port *maybe_deliver( @@ -140,7 +140,7 @@ index dea09096ad0f..f0a613238e0d 100644 continue; break; diff --git a/net/bridge/br_input.c b/net/bridge/br_input.c -index ac0f56076e93..9af38febff25 100644 +index ac0f56076e93b..9af38febff25d 100644 --- a/net/bridge/br_input.c +++ b/net/bridge/br_input.c @@ -186,9 +186,13 @@ int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb @@ -170,7 +170,7 @@ index ac0f56076e93..9af38febff25 100644 br_multicast_flood(mdst, skb, brmctx, local_rcv, false); } diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c -index fda1f89ee22a..db837e27ed8a 100644 +index fda1f89ee22ae..db837e27ed8a2 100644 --- a/net/bridge/br_multicast.c +++ b/net/bridge/br_multicast.c @@ -3838,6 +3838,11 @@ static void br_multicast_err_count(const struct net_bridge *br, @@ -206,7 +206,7 @@ index fda1f89ee22a..db837e27ed8a 100644 ipv6_addr_is_all_snoopers(&ipv6_hdr(skb)->daddr)) br_ip6_multicast_mrd_rcv(brmctx, pmctx, skb); diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h -index e4c8bc8576df..b99aa1d6a260 100644 +index e4c8bc8576df6..b99aa1d6a2608 100644 --- a/net/bridge/br_private.h +++ b/net/bridge/br_private.h @@ -491,6 +491,7 @@ enum net_bridge_opts { diff --git a/patches/linux/6.18.55/0007-net-bridge-Ignore-router-ports-when-forwarding-L2-mu.patch b/patches/linux/6.18.55/0007-net-bridge-Ignore-router-ports-when-forwarding-L2-mu.patch index fcd288fd8..dd700b21a 100644 --- a/patches/linux/6.18.55/0007-net-bridge-Ignore-router-ports-when-forwarding-L2-mu.patch +++ b/patches/linux/6.18.55/0007-net-bridge-Ignore-router-ports-when-forwarding-L2-mu.patch @@ -1,7 +1,7 @@ From fe3d0113cb67a4e4287329bc1a3b311f9c707cf1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 5 Mar 2024 06:44:41 +0100 -Subject: [PATCH 07/78] net: bridge: Ignore router ports when forwarding L2 +Subject: [PATCH 07/84] net: bridge: Ignore router ports when forwarding L2 multicast Multicast router ports are either statically configured or learned from @@ -16,7 +16,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h -index b99aa1d6a260..93f3bbc37590 100644 +index b99aa1d6a2608..93f3bbc37590a 100644 --- a/net/bridge/br_private.h +++ b/net/bridge/br_private.h @@ -1088,7 +1088,10 @@ br_multicast_get_first_rport_node(struct net_bridge_mcast *brmctx, diff --git a/patches/linux/6.18.55/0008-net-bridge-drop-delay-for-applying-strict-multicast-.patch b/patches/linux/6.18.55/0008-net-bridge-drop-delay-for-applying-strict-multicast-.patch index 4151a986e..17d9e1549 100644 --- a/patches/linux/6.18.55/0008-net-bridge-drop-delay-for-applying-strict-multicast-.patch +++ b/patches/linux/6.18.55/0008-net-bridge-drop-delay-for-applying-strict-multicast-.patch @@ -1,7 +1,7 @@ From e00f0142f3eef963aef0a046761fda5a2e7712e8 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 4 Apr 2024 16:36:30 +0200 -Subject: [PATCH 08/78] net: bridge: drop delay for applying strict multicast +Subject: [PATCH 08/84] net: bridge: drop delay for applying strict multicast filtering This *local* patch drops the initial delay before applying strict multicast @@ -24,7 +24,7 @@ Signed-off-by: Joachim Wiberg 2 files changed, 8 insertions(+), 38 deletions(-) diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c -index db837e27ed8a..2ccfe3c7b20e 100644 +index db837e27ed8a2..2ccfe3c7b20e3 100644 --- a/net/bridge/br_multicast.c +++ b/net/bridge/br_multicast.c @@ -1761,10 +1761,6 @@ static void br_ip6_multicast_querier_expired(struct timer_list *t) @@ -163,7 +163,7 @@ index db837e27ed8a..2ccfe3c7b20e 100644 #endif diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h -index 93f3bbc37590..ec6a85023b5a 100644 +index 93f3bbc37590a..ec6a85023b5ac 100644 --- a/net/bridge/br_private.h +++ b/net/bridge/br_private.h @@ -80,7 +80,6 @@ struct bridge_mcast_own_query { diff --git a/patches/linux/6.18.55/0009-net-bridge-Differentiate-MDB-additions-from-modifica.patch b/patches/linux/6.18.55/0009-net-bridge-Differentiate-MDB-additions-from-modifica.patch index f4dad3714..f248a3f0f 100644 --- a/patches/linux/6.18.55/0009-net-bridge-Differentiate-MDB-additions-from-modifica.patch +++ b/patches/linux/6.18.55/0009-net-bridge-Differentiate-MDB-additions-from-modifica.patch @@ -1,7 +1,7 @@ From f24cd427adb4d2f1c56c339085bdce4ee1d61844 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Thu, 16 May 2024 14:51:54 +0200 -Subject: [PATCH 09/78] net: bridge: Differentiate MDB additions from +Subject: [PATCH 09/84] net: bridge: Differentiate MDB additions from modifications Before this change, the reception of an IGMPv3 report (and analogously @@ -27,7 +27,7 @@ introducing a RTM_SETMDB events to be used in the latter scenario. 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/include/uapi/linux/rtnetlink.h b/include/uapi/linux/rtnetlink.h -index dab9493c791b..40a3534f0f1d 100644 +index dab9493c791b8..40a3534f0f1d7 100644 --- a/include/uapi/linux/rtnetlink.h +++ b/include/uapi/linux/rtnetlink.h @@ -142,6 +142,8 @@ enum { @@ -40,7 +40,7 @@ index dab9493c791b..40a3534f0f1d 100644 RTM_NEWNSID = 88, #define RTM_NEWNSID RTM_NEWNSID diff --git a/net/bridge/br_mdb.c b/net/bridge/br_mdb.c -index a80ec51fe18d..4cbf3381564d 100644 +index a80ec51fe18d1..4cbf3381564d7 100644 --- a/net/bridge/br_mdb.c +++ b/net/bridge/br_mdb.c @@ -752,7 +752,7 @@ static int br_mdb_replace_group_sg(const struct br_mdb_config *cfg, @@ -62,7 +62,7 @@ index a80ec51fe18d..4cbf3381564d 100644 if (br_multicast_should_handle_mode(brmctx, cfg->group.proto)) br_multicast_star_g_handle_mode(pg, cfg->filter_mode); diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c -index 2ccfe3c7b20e..aad576e4af65 100644 +index 2ccfe3c7b20e3..aad576e4af65d 100644 --- a/net/bridge/br_multicast.c +++ b/net/bridge/br_multicast.c @@ -616,7 +616,7 @@ static void br_multicast_fwd_src_handle(struct net_bridge_group_src *src) diff --git a/patches/linux/6.18.55/0010-nvmem-layouts-onie-tlv-Let-device-probe-even-when-TL.patch b/patches/linux/6.18.55/0010-nvmem-layouts-onie-tlv-Let-device-probe-even-when-TL.patch index 2ad996086..015557b79 100644 --- a/patches/linux/6.18.55/0010-nvmem-layouts-onie-tlv-Let-device-probe-even-when-TL.patch +++ b/patches/linux/6.18.55/0010-nvmem-layouts-onie-tlv-Let-device-probe-even-when-TL.patch @@ -1,7 +1,7 @@ From e47f23a9767fc22465731ab728215afa25579c20 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 24 Nov 2023 23:29:55 +0100 -Subject: [PATCH 10/78] nvmem: layouts: onie-tlv: Let device probe even when +Subject: [PATCH 10/84] nvmem: layouts: onie-tlv: Let device probe even when TLV is invalid Before this change, probing an NVMEM device, expected to contain a @@ -17,7 +17,7 @@ simply refrain from registering any cells in those cases. 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/nvmem/layouts/onie-tlv.c b/drivers/nvmem/layouts/onie-tlv.c -index 8b0f3c1b8a0e..de85690aaa30 100644 +index 8b0f3c1b8a0e9..de85690aaa303 100644 --- a/drivers/nvmem/layouts/onie-tlv.c +++ b/drivers/nvmem/layouts/onie-tlv.c @@ -198,7 +198,7 @@ static int onie_tlv_parse_table(struct nvmem_layout *layout) diff --git a/patches/linux/6.18.55/0011-net-usb-r8152-add-r8153b-support-for-link-activity-L.patch b/patches/linux/6.18.55/0011-net-usb-r8152-add-r8153b-support-for-link-activity-L.patch index ab0ad78fd..642ed68cf 100644 --- a/patches/linux/6.18.55/0011-net-usb-r8152-add-r8153b-support-for-link-activity-L.patch +++ b/patches/linux/6.18.55/0011-net-usb-r8152-add-r8153b-support-for-link-activity-L.patch @@ -1,7 +1,7 @@ From bd5c4068ec34ff0af11b1f52a3c06d43c8af39b1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 11 Aug 2024 11:27:35 +0200 -Subject: [PATCH 11/78] net: usb: r8152: add r8153b support for link/activity +Subject: [PATCH 11/84] net: usb: r8152: add r8153b support for link/activity LEDs This patch adds support for the link/activity LEDs on the NanoPi R2S @@ -18,7 +18,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 8 insertions(+) diff --git a/drivers/net/usb/r8152.c b/drivers/net/usb/r8152.c -index 8cf4e81f8f88..1bb93efb96c6 100644 +index 8cf4e81f8f882..1bb93efb96c68 100644 --- a/drivers/net/usb/r8152.c +++ b/drivers/net/usb/r8152.c @@ -41,6 +41,11 @@ diff --git a/patches/linux/6.18.55/0012-arm64-dts-mediatek-mt7986a-rename-BPi-R3-ports-to-ma.patch b/patches/linux/6.18.55/0012-arm64-dts-mediatek-mt7986a-rename-BPi-R3-ports-to-ma.patch index d8140edde..6bd443f13 100644 --- a/patches/linux/6.18.55/0012-arm64-dts-mediatek-mt7986a-rename-BPi-R3-ports-to-ma.patch +++ b/patches/linux/6.18.55/0012-arm64-dts-mediatek-mt7986a-rename-BPi-R3-ports-to-ma.patch @@ -1,7 +1,7 @@ From 7552cf30d52b0798298af29063a44da182e9b6ee Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 10 Aug 2025 18:52:54 +0200 -Subject: [PATCH 12/78] arm64: dts: mediatek: mt7986a: rename BPi R3 ports to +Subject: [PATCH 12/84] arm64: dts: mediatek: mt7986a: rename BPi R3 ports to match case For ref. see: https://wiki.banana-pi.org/File:Bpi-r3_Metal_case.jpg @@ -15,7 +15,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/arch/arm64/boot/dts/mediatek/mt7986a-bananapi-bpi-r3.dts b/arch/arm64/boot/dts/mediatek/mt7986a-bananapi-bpi-r3.dts -index e7654dc9a1c9..4cc6aff07225 100644 +index e7654dc9a1c9b..4cc6aff072256 100644 --- a/arch/arm64/boot/dts/mediatek/mt7986a-bananapi-bpi-r3.dts +++ b/arch/arm64/boot/dts/mediatek/mt7986a-bananapi-bpi-r3.dts @@ -46,7 +46,7 @@ fan: pwm-fan { diff --git a/patches/linux/6.18.55/0013-drm-panel-simple-Add-a-timing-for-the-Raspberry-Pi-7.patch b/patches/linux/6.18.55/0013-drm-panel-simple-Add-a-timing-for-the-Raspberry-Pi-7.patch index 94951986d..d9baf1baf 100644 --- a/patches/linux/6.18.55/0013-drm-panel-simple-Add-a-timing-for-the-Raspberry-Pi-7.patch +++ b/patches/linux/6.18.55/0013-drm-panel-simple-Add-a-timing-for-the-Raspberry-Pi-7.patch @@ -1,7 +1,7 @@ From 34593fc729c0656763733bf93719b8c8c9408f91 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Wed, 20 Aug 2025 21:38:24 +0200 -Subject: [PATCH 13/78] drm/panel-simple: Add a timing for the Raspberry Pi 7" +Subject: [PATCH 13/84] drm/panel-simple: Add a timing for the Raspberry Pi 7" panel The Raspberry Pi 7" 800x480 panel uses a Toshiba TC358762 DSI @@ -46,7 +46,7 @@ Signed-off-by: Dave Stevenson 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/panel/panel-simple.c b/drivers/gpu/drm/panel/panel-simple.c -index 878a5dc7748f..d5da86abea70 100644 +index 878a5dc7748fb..d5da86abea700 100644 --- a/drivers/gpu/drm/panel/panel-simple.c +++ b/drivers/gpu/drm/panel/panel-simple.c @@ -400,7 +400,8 @@ static int panel_simple_get_modes(struct drm_panel *panel, diff --git a/patches/linux/6.18.55/0014-input-touchscreen-edt-ft5x06-Add-polled-mode.patch b/patches/linux/6.18.55/0014-input-touchscreen-edt-ft5x06-Add-polled-mode.patch index adab4e5d7..be2602222 100644 --- a/patches/linux/6.18.55/0014-input-touchscreen-edt-ft5x06-Add-polled-mode.patch +++ b/patches/linux/6.18.55/0014-input-touchscreen-edt-ft5x06-Add-polled-mode.patch @@ -1,7 +1,7 @@ From e443789978265aac625d474e2ceeebdf62ce5b8e Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Thu, 21 Aug 2025 11:20:23 +0200 -Subject: [PATCH 14/78] input:touchscreen:edt-ft5x06: Add polled mode +Subject: [PATCH 14/84] input:touchscreen:edt-ft5x06: Add polled mode Not all hardware has interrupts therefore we need to poll the touchscreen. @@ -10,7 +10,7 @@ to poll the touchscreen. 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c -index 4efdb467b6c6..6ec1078081d5 100644 +index 4efdb467b6c61..6ec1078081d58 100644 --- a/drivers/input/touchscreen/edt-ft5x06.c +++ b/drivers/input/touchscreen/edt-ft5x06.c @@ -77,6 +77,9 @@ diff --git a/patches/linux/6.18.55/0015-FIX-net-dsa-mv88e6xxx-Fix-timeout-on-waiting-for-PPU.patch b/patches/linux/6.18.55/0015-FIX-net-dsa-mv88e6xxx-Fix-timeout-on-waiting-for-PPU.patch index 72e36b64f..f67302009 100644 --- a/patches/linux/6.18.55/0015-FIX-net-dsa-mv88e6xxx-Fix-timeout-on-waiting-for-PPU.patch +++ b/patches/linux/6.18.55/0015-FIX-net-dsa-mv88e6xxx-Fix-timeout-on-waiting-for-PPU.patch @@ -1,7 +1,7 @@ From dab336b722de649666ef241636207f10745a0f1e Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 12 Mar 2024 10:27:24 +0100 -Subject: [PATCH 15/78] [FIX] net: dsa: mv88e6xxx: Fix timeout on waiting for +Subject: [PATCH 15/84] [FIX] net: dsa: mv88e6xxx: Fix timeout on waiting for PPU on 6393X In a multi-chip setup, delays of up to 750ms are observed before the @@ -12,7 +12,7 @@ register 0, bit 15). Therefore, increase the timeout threshold to 1s. 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index e9270c266884..37cd099180d6 100644 +index e9270c266884c..37cd099180d64 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -87,7 +87,7 @@ int mv88e6xxx_write(struct mv88e6xxx_chip *chip, int addr, int reg, u16 val) diff --git a/patches/linux/6.18.55/0016-net-dsa-mv88e6xxx-Improve-indirect-register-access-p.patch b/patches/linux/6.18.55/0016-net-dsa-mv88e6xxx-Improve-indirect-register-access-p.patch index 17d51c3c4..231b841d8 100644 --- a/patches/linux/6.18.55/0016-net-dsa-mv88e6xxx-Improve-indirect-register-access-p.patch +++ b/patches/linux/6.18.55/0016-net-dsa-mv88e6xxx-Improve-indirect-register-access-p.patch @@ -1,7 +1,7 @@ From fbb87a65e898ae04f9801aa820f586d046ba329c Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Wed, 27 Mar 2024 15:52:43 +0100 -Subject: [PATCH 16/78] net: dsa: mv88e6xxx: Improve indirect register access +Subject: [PATCH 16/84] net: dsa: mv88e6xxx: Improve indirect register access perf on 6393 When operating in multi-chip mode, the 6393 family maps a subset of @@ -21,7 +21,7 @@ other accesses use the regular indirect interface. 5 files changed, 119 insertions(+) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index 37cd099180d6..e8980609b45d 100644 +index 37cd099180d64..e8980609b45da 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -6554,6 +6554,13 @@ static int mv88e6xxx_detect(struct mv88e6xxx_chip *chip) @@ -39,7 +39,7 @@ index 37cd099180d6..e8980609b45d 100644 chip->info->prod_num, chip->info->name, rev); diff --git a/drivers/net/dsa/mv88e6xxx/global1.h b/drivers/net/dsa/mv88e6xxx/global1.h -index 3dbb7a1b8fe1..5ea956aadecc 100644 +index 3dbb7a1b8fe11..5ea956aadecc9 100644 --- a/drivers/net/dsa/mv88e6xxx/global1.h +++ b/drivers/net/dsa/mv88e6xxx/global1.h @@ -218,6 +218,9 @@ @@ -53,7 +53,7 @@ index 3dbb7a1b8fe1..5ea956aadecc 100644 #define MV88E6XXX_G1_CTL2 0x1c #define MV88E6185_G1_CTL2_CASCADE_PORT_MASK 0xf000 diff --git a/drivers/net/dsa/mv88e6xxx/global2.h b/drivers/net/dsa/mv88e6xxx/global2.h -index 82f9b410de0b..3663645621c9 100644 +index 82f9b410de0b8..3663645621c9d 100644 --- a/drivers/net/dsa/mv88e6xxx/global2.h +++ b/drivers/net/dsa/mv88e6xxx/global2.h @@ -143,6 +143,9 @@ @@ -67,7 +67,7 @@ index 82f9b410de0b..3663645621c9 100644 #define MV88E6XXX_G2_EEPROM_CMD 0x14 #define MV88E6XXX_G2_EEPROM_CMD_BUSY 0x8000 diff --git a/drivers/net/dsa/mv88e6xxx/smi.c b/drivers/net/dsa/mv88e6xxx/smi.c -index a990271b7482..f54bb0e79030 100644 +index a990271b74823..f54bb0e79030c 100644 --- a/drivers/net/dsa/mv88e6xxx/smi.c +++ b/drivers/net/dsa/mv88e6xxx/smi.c @@ -8,6 +8,8 @@ @@ -168,7 +168,7 @@ index a990271b7482..f54bb0e79030 100644 chip->smi_ops = &mv88e6xxx_smi_indirect_ops; else diff --git a/drivers/net/dsa/mv88e6xxx/smi.h b/drivers/net/dsa/mv88e6xxx/smi.h -index c6c71d5757f5..788cf68b7b33 100644 +index c6c71d5757f5d..788cf68b7b334 100644 --- a/drivers/net/dsa/mv88e6xxx/smi.h +++ b/drivers/net/dsa/mv88e6xxx/smi.h @@ -31,6 +31,36 @@ diff --git a/patches/linux/6.18.55/0017-net-dsa-mv88e6xxx-Honor-ports-being-managed-via-in-b.patch b/patches/linux/6.18.55/0017-net-dsa-mv88e6xxx-Honor-ports-being-managed-via-in-b.patch index 54b80314d..8248b1baa 100644 --- a/patches/linux/6.18.55/0017-net-dsa-mv88e6xxx-Honor-ports-being-managed-via-in-b.patch +++ b/patches/linux/6.18.55/0017-net-dsa-mv88e6xxx-Honor-ports-being-managed-via-in-b.patch @@ -1,7 +1,7 @@ From 32bf97b633ba31514b0b0756a1c0891bc67254ba Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Mon, 22 Apr 2024 23:18:01 +0200 -Subject: [PATCH 17/78] net: dsa: mv88e6xxx: Honor ports being managed via +Subject: [PATCH 17/84] net: dsa: mv88e6xxx: Honor ports being managed via in-band-status Keep all link parameters in their unforced states when the port is @@ -17,7 +17,7 @@ when in-band-status is being used. 1 file changed, 6 insertions(+) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index e8980609b45d..0a535dc0a48c 100644 +index e8980609b45da..0a535dc0a48c1 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -965,6 +965,9 @@ static void mv88e6xxx_mac_link_down(struct phylink_config *config, diff --git a/patches/linux/6.18.55/0018-net-dsa-mv88e6xxx-Limit-rsvd2cpu-policy-to-user-port.patch b/patches/linux/6.18.55/0018-net-dsa-mv88e6xxx-Limit-rsvd2cpu-policy-to-user-port.patch index a4c117e94..16c5fa667 100644 --- a/patches/linux/6.18.55/0018-net-dsa-mv88e6xxx-Limit-rsvd2cpu-policy-to-user-port.patch +++ b/patches/linux/6.18.55/0018-net-dsa-mv88e6xxx-Limit-rsvd2cpu-policy-to-user-port.patch @@ -1,7 +1,7 @@ From fadc315c8a6f0305edb24d3fbba80aa2e712d2a1 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Wed, 24 Apr 2024 22:41:04 +0200 -Subject: [PATCH 18/78] net: dsa: mv88e6xxx: Limit rsvd2cpu policy to user +Subject: [PATCH 18/84] net: dsa: mv88e6xxx: Limit rsvd2cpu policy to user ports on 6393X For packets with a DA in the IEEE reserved L2 group range, originating @@ -33,7 +33,7 @@ forwarded like any other. 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c -index 66b1b7277281..df19dfa5cc3d 100644 +index 66b1b72772810..df19dfa5cc3d8 100644 --- a/drivers/net/dsa/mv88e6xxx/port.c +++ b/drivers/net/dsa/mv88e6xxx/port.c @@ -1434,6 +1434,23 @@ static int mv88e6393x_port_policy_write_all(struct mv88e6xxx_chip *chip, diff --git a/patches/linux/6.18.55/0019-net-dsa-tag_dsa-Use-tag-priority-as-initial-skb-prio.patch b/patches/linux/6.18.55/0019-net-dsa-tag_dsa-Use-tag-priority-as-initial-skb-prio.patch index 088b22c42..47c737e95 100644 --- a/patches/linux/6.18.55/0019-net-dsa-tag_dsa-Use-tag-priority-as-initial-skb-prio.patch +++ b/patches/linux/6.18.55/0019-net-dsa-tag_dsa-Use-tag-priority-as-initial-skb-prio.patch @@ -1,7 +1,7 @@ From 52728881ff09f7e48fb26145d21b1dd83f6643e7 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 28 May 2024 10:38:42 +0200 -Subject: [PATCH 19/78] net: dsa: tag_dsa: Use tag priority as initial +Subject: [PATCH 19/84] net: dsa: tag_dsa: Use tag priority as initial skb->priority Use the 3-bit priority field from the DSA tag as the initial packet @@ -23,7 +23,7 @@ implemented, support the setup that is likely to be the most common; a 1 file changed, 7 insertions(+) diff --git a/net/dsa/tag_dsa.c b/net/dsa/tag_dsa.c -index 2a2c4fb61a65..a00ae6bf2971 100644 +index 2a2c4fb61a65c..a00ae6bf29717 100644 --- a/net/dsa/tag_dsa.c +++ b/net/dsa/tag_dsa.c @@ -323,6 +323,13 @@ static struct sk_buff *dsa_rcv_ll(struct sk_buff *skb, struct net_device *dev, diff --git a/patches/linux/6.18.55/0020-net-dsa-Support-MDB-memberships-whose-L2-addresses-o.patch b/patches/linux/6.18.55/0020-net-dsa-Support-MDB-memberships-whose-L2-addresses-o.patch index ca0afe350..2fd0550e7 100644 --- a/patches/linux/6.18.55/0020-net-dsa-Support-MDB-memberships-whose-L2-addresses-o.patch +++ b/patches/linux/6.18.55/0020-net-dsa-Support-MDB-memberships-whose-L2-addresses-o.patch @@ -1,7 +1,7 @@ From 5fe097173ea1563a7f1e3b93fa0347699a94e19e Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 16 Jan 2024 16:00:55 +0100 -Subject: [PATCH 20/78] net: dsa: Support MDB memberships whose L2 addresses +Subject: [PATCH 20/84] net: dsa: Support MDB memberships whose L2 addresses overlap Multiple IP multicast groups (32 for v4, 2^80 for v6) map to the same @@ -33,7 +33,7 @@ which previously skipped reference countung on user ports. 1 file changed, 16 deletions(-) diff --git a/net/dsa/switch.c b/net/dsa/switch.c -index 3d2feeea897b..628e8a884dde 100644 +index 3d2feeea897b6..628e8a884dde5 100644 --- a/net/dsa/switch.c +++ b/net/dsa/switch.c @@ -164,14 +164,6 @@ static int dsa_port_do_mdb_add(struct dsa_port *dp, diff --git a/patches/linux/6.18.55/0021-net-dsa-Support-EtherType-based-priority-overrides.patch b/patches/linux/6.18.55/0021-net-dsa-Support-EtherType-based-priority-overrides.patch index 8d18a6a2a..d15ddf78f 100644 --- a/patches/linux/6.18.55/0021-net-dsa-Support-EtherType-based-priority-overrides.patch +++ b/patches/linux/6.18.55/0021-net-dsa-Support-EtherType-based-priority-overrides.patch @@ -1,7 +1,7 @@ From b1f583e91f7d447d2a81feb2753b8a483c3ff24f Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Thu, 21 Mar 2024 19:12:15 +0100 -Subject: [PATCH 21/78] net: dsa: Support EtherType based priority overrides +Subject: [PATCH 21/84] net: dsa: Support EtherType based priority overrides --- include/net/dsa.h | 4 ++++ @@ -9,7 +9,7 @@ Subject: [PATCH 21/78] net: dsa: Support EtherType based priority overrides 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/include/net/dsa.h b/include/net/dsa.h -index 5cb456bf4639..c31d4e910f07 100644 +index 5cb456bf46394..c31d4e910f075 100644 --- a/include/net/dsa.h +++ b/include/net/dsa.h @@ -958,6 +958,10 @@ struct dsa_switch_ops { @@ -24,7 +24,7 @@ index 5cb456bf4639..c31d4e910f07 100644 /* * Suspend and resume diff --git a/net/dsa/user.c b/net/dsa/user.c -index f59d66f0975d..58174ad612d2 100644 +index f59d66f0975d7..58174ad612d21 100644 --- a/net/dsa/user.c +++ b/net/dsa/user.c @@ -2336,6 +2336,34 @@ dsa_user_dcbnl_add_dscp_prio(struct net_device *dev, struct dcb_app *app) diff --git a/patches/linux/6.18.55/0022-net-dsa-mv88e6xxx-Support-EtherType-based-priority-o.patch b/patches/linux/6.18.55/0022-net-dsa-mv88e6xxx-Support-EtherType-based-priority-o.patch index 363882976..a99e35982 100644 --- a/patches/linux/6.18.55/0022-net-dsa-mv88e6xxx-Support-EtherType-based-priority-o.patch +++ b/patches/linux/6.18.55/0022-net-dsa-mv88e6xxx-Support-EtherType-based-priority-o.patch @@ -1,7 +1,7 @@ From ba6e63577971f172e2dcc46071d1b3471d9d1a12 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 22 Mar 2024 16:15:43 +0100 -Subject: [PATCH 22/78] net: dsa: mv88e6xxx: Support EtherType based priority +Subject: [PATCH 22/84] net: dsa: mv88e6xxx: Support EtherType based priority overrides --- @@ -14,7 +14,7 @@ Subject: [PATCH 22/78] net: dsa: mv88e6xxx: Support EtherType based priority 6 files changed, 207 insertions(+), 4 deletions(-) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index 0a535dc0a48c..c708f1cce9b3 100644 +index 0a535dc0a48c1..c708f1cce9b30 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -1688,6 +1688,11 @@ static int mv88e6xxx_rmu_setup(struct mv88e6xxx_chip *chip) @@ -117,7 +117,7 @@ index 0a535dc0a48c..c708f1cce9b3 100644 static int mv88e6xxx_register_switch(struct mv88e6xxx_chip *chip) diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h -index e073446ee7d0..5d5e7ab63fca 100644 +index e073446ee7d02..5d5e7ab63fca3 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.h +++ b/drivers/net/dsa/mv88e6xxx/chip.h @@ -301,6 +301,11 @@ struct mv88e6xxx_port { @@ -170,7 +170,7 @@ index e073446ee7d0..5d5e7ab63fca 100644 struct mv88e6xxx_bus_ops { diff --git a/drivers/net/dsa/mv88e6xxx/global2.c b/drivers/net/dsa/mv88e6xxx/global2.c -index 30a6ffa7817b..3f16a7ef3fa2 100644 +index 30a6ffa7817b0..3f16a7ef3fa28 100644 --- a/drivers/net/dsa/mv88e6xxx/global2.c +++ b/drivers/net/dsa/mv88e6xxx/global2.c @@ -315,7 +315,7 @@ int mv88e6xxx_g2_atu_stats_get(struct mv88e6xxx_chip *chip, u16 *stats) @@ -244,7 +244,7 @@ index 30a6ffa7817b..3f16a7ef3fa2 100644 * Offset 0x15: EEPROM Data (for 16-bit data access) * Offset 0x15: EEPROM Addr (for 8-bit data access) diff --git a/drivers/net/dsa/mv88e6xxx/global2.h b/drivers/net/dsa/mv88e6xxx/global2.h -index 3663645621c9..496269c26c50 100644 +index 3663645621c9d..496269c26c508 100644 --- a/drivers/net/dsa/mv88e6xxx/global2.h +++ b/drivers/net/dsa/mv88e6xxx/global2.h @@ -138,6 +138,7 @@ @@ -266,7 +266,7 @@ index 3663645621c9..496269c26c50 100644 int mv88e6xxx_g2_trunk_mask_write(struct mv88e6xxx_chip *chip, int num, bool hash, u16 mask); diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c -index df19dfa5cc3d..e1fee09eae2f 100644 +index df19dfa5cc3d8..e1fee09eae2fc 100644 --- a/drivers/net/dsa/mv88e6xxx/port.c +++ b/drivers/net/dsa/mv88e6xxx/port.c @@ -1560,6 +1560,52 @@ int mv88e6351_port_set_ether_type(struct mv88e6xxx_chip *chip, int port, @@ -323,7 +323,7 @@ index df19dfa5cc3d..e1fee09eae2f 100644 * Offset 0x19: Port IEEE Priority Remapping Registers [4-7] */ diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h -index c1d2f99efb1c..d2ba9ed1f23e 100644 +index c1d2f99efb1c6..d2ba9ed1f23ea 100644 --- a/drivers/net/dsa/mv88e6xxx/port.h +++ b/drivers/net/dsa/mv88e6xxx/port.h @@ -286,7 +286,18 @@ diff --git a/patches/linux/6.18.55/0023-net-dsa-mv88e6xxx-Add-mqprio-qdisc-support.patch b/patches/linux/6.18.55/0023-net-dsa-mv88e6xxx-Add-mqprio-qdisc-support.patch index d8fed0a32..3017381cd 100644 --- a/patches/linux/6.18.55/0023-net-dsa-mv88e6xxx-Add-mqprio-qdisc-support.patch +++ b/patches/linux/6.18.55/0023-net-dsa-mv88e6xxx-Add-mqprio-qdisc-support.patch @@ -1,7 +1,7 @@ From b12a958ebe126f89d0bcd2057733a270de63e883 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 28 May 2024 11:04:22 +0200 -Subject: [PATCH 23/78] net: dsa: mv88e6xxx: Add mqprio qdisc support +Subject: [PATCH 23/84] net: dsa: mv88e6xxx: Add mqprio qdisc support Add support for attaching mqprio qdisc's to mv88e6xxx ports and use the packet's traffic class as the outgoing priority when no PCP bits @@ -29,7 +29,7 @@ handle the mapping down to the "real" number. 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index c708f1cce9b3..2dd0573e101e 100644 +index c708f1cce9b30..2dd0573e101eb 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -32,6 +32,7 @@ @@ -131,7 +131,7 @@ index c708f1cce9b3..2dd0573e101e 100644 return dsa_register_switch(ds); diff --git a/net/dsa/tag_dsa.c b/net/dsa/tag_dsa.c -index a00ae6bf2971..55c296e0b5b0 100644 +index a00ae6bf29717..55c296e0b5b0e 100644 --- a/net/dsa/tag_dsa.c +++ b/net/dsa/tag_dsa.c @@ -180,8 +180,10 @@ static struct sk_buff *dsa_xmit_ll(struct sk_buff *skb, struct net_device *dev, diff --git a/patches/linux/6.18.55/0024-net-dsa-mv88e6xxx-Use-VLAN-prio-over-IP-when-both-ar.patch b/patches/linux/6.18.55/0024-net-dsa-mv88e6xxx-Use-VLAN-prio-over-IP-when-both-ar.patch index 78c8791f8..8ce67fbab 100644 --- a/patches/linux/6.18.55/0024-net-dsa-mv88e6xxx-Use-VLAN-prio-over-IP-when-both-ar.patch +++ b/patches/linux/6.18.55/0024-net-dsa-mv88e6xxx-Use-VLAN-prio-over-IP-when-both-ar.patch @@ -1,7 +1,7 @@ From 5905be94b871ac828acbae66f6a2c21ea0c48dfd Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Wed, 29 May 2024 13:20:41 +0200 -Subject: [PATCH 24/78] net: dsa: mv88e6xxx: Use VLAN prio over IP when both +Subject: [PATCH 24/84] net: dsa: mv88e6xxx: Use VLAN prio over IP when both are available Switch the priority sourcing precdence to prefer VLAN PCP over IP @@ -26,7 +26,7 @@ main reasons for choosing the new default: 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index 2dd0573e101e..15c80e66aefd 100644 +index 2dd0573e101eb..15c80e66aefd2 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -3430,9 +3430,13 @@ static int mv88e6xxx_setup_port(struct mv88e6xxx_chip *chip, int port) diff --git a/patches/linux/6.18.55/0025-FIX-net-dsa-mv88e6xxx-Trap-locally-terminated-VLANs.patch b/patches/linux/6.18.55/0025-FIX-net-dsa-mv88e6xxx-Trap-locally-terminated-VLANs.patch index f87376948..02722ba24 100644 --- a/patches/linux/6.18.55/0025-FIX-net-dsa-mv88e6xxx-Trap-locally-terminated-VLANs.patch +++ b/patches/linux/6.18.55/0025-FIX-net-dsa-mv88e6xxx-Trap-locally-terminated-VLANs.patch @@ -1,7 +1,7 @@ From 508ff078d94e3083fcf905ff59fcb477a3dbe507 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 26 Nov 2024 19:45:59 +0100 -Subject: [PATCH 25/78] [FIX] net: dsa: mv88e6xxx: Trap locally terminated +Subject: [PATCH 25/84] [FIX] net: dsa: mv88e6xxx: Trap locally terminated VLANs Before this change, in a setup like the following, packets assigned to @@ -30,7 +30,7 @@ cause all packets assigned to these VLANs to properly terminated. 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index 15c80e66aefd..397ae2b78df8 100644 +index 15c80e66aefd2..397ae2b78df84 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -2619,7 +2619,7 @@ static int mv88e6xxx_port_broadcast_sync(struct mv88e6xxx_chip *chip, int port, @@ -117,7 +117,7 @@ index 15c80e66aefd..397ae2b78df8 100644 return err; diff --git a/include/net/switchdev.h b/include/net/switchdev.h -index 8346b0d29542..764cd3dd4645 100644 +index 8346b0d29542c..764cd3dd46454 100644 --- a/include/net/switchdev.h +++ b/include/net/switchdev.h @@ -104,6 +104,10 @@ struct switchdev_obj_port_vlan { @@ -132,7 +132,7 @@ index 8346b0d29542..764cd3dd4645 100644 #define SWITCHDEV_OBJ_PORT_VLAN(OBJ) \ diff --git a/net/dsa/user.c b/net/dsa/user.c -index 58174ad612d2..c0019d0c1172 100644 +index 58174ad612d21..c0019d0c1172d 100644 --- a/net/dsa/user.c +++ b/net/dsa/user.c @@ -1796,6 +1796,8 @@ static int dsa_user_vlan_rx_add_vid(struct net_device *dev, __be16 proto, diff --git a/patches/linux/6.18.55/0026-net-dsa-mv88e6xxx-collapse-disabled-state-into-block.patch b/patches/linux/6.18.55/0026-net-dsa-mv88e6xxx-collapse-disabled-state-into-block.patch index 00afddfb0..6b33635b9 100644 --- a/patches/linux/6.18.55/0026-net-dsa-mv88e6xxx-collapse-disabled-state-into-block.patch +++ b/patches/linux/6.18.55/0026-net-dsa-mv88e6xxx-collapse-disabled-state-into-block.patch @@ -1,7 +1,7 @@ From c24a806cd5b1728102098a6e3da24ee19f908884 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 16 Jan 2025 12:35:12 +0100 -Subject: [PATCH 26/78] net: dsa: mv88e6xxx: collapse disabled state into +Subject: [PATCH 26/84] net: dsa: mv88e6xxx: collapse disabled state into blocking This patch changes the behavior of switchcore ports wrt. the port state. @@ -22,7 +22,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 2 deletions(-) diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c -index e1fee09eae2f..e28fdd21de38 100644 +index e1fee09eae2fc..e28fdd21de38e 100644 --- a/drivers/net/dsa/mv88e6xxx/port.c +++ b/drivers/net/dsa/mv88e6xxx/port.c @@ -793,8 +793,6 @@ int mv88e6xxx_port_set_state(struct mv88e6xxx_chip *chip, int port, u8 state) diff --git a/patches/linux/6.18.55/0027-net-dsa-mv88e6xxx-Only-activate-LAG-offloading-when-.patch b/patches/linux/6.18.55/0027-net-dsa-mv88e6xxx-Only-activate-LAG-offloading-when-.patch index d96c4386d..8e79bbbf0 100644 --- a/patches/linux/6.18.55/0027-net-dsa-mv88e6xxx-Only-activate-LAG-offloading-when-.patch +++ b/patches/linux/6.18.55/0027-net-dsa-mv88e6xxx-Only-activate-LAG-offloading-when-.patch @@ -1,7 +1,7 @@ From b1b26dd227ed5099626c1fcb0e843b81d3daae65 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Wed, 12 Feb 2025 22:03:14 +0100 -Subject: [PATCH 27/78] net: dsa: mv88e6xxx: Only activate LAG offloading when +Subject: [PATCH 27/84] net: dsa: mv88e6xxx: Only activate LAG offloading when bridged The current port isolation scheme for mv88e6xxx is detailed here: @@ -50,7 +50,7 @@ CPU; egress traffic always relies on software hashing. 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index 397ae2b78df8..71458342a274 100644 +index 397ae2b78df84..71458342a274c 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -3066,6 +3066,7 @@ static int mv88e6xxx_port_bridge_join(struct dsa_switch *ds, int port, diff --git a/patches/linux/6.18.55/0028-net-dsa-mv88e6xxx-Add-LED-support-for-6393X.patch b/patches/linux/6.18.55/0028-net-dsa-mv88e6xxx-Add-LED-support-for-6393X.patch index 159a15a7d..e9939eac1 100644 --- a/patches/linux/6.18.55/0028-net-dsa-mv88e6xxx-Add-LED-support-for-6393X.patch +++ b/patches/linux/6.18.55/0028-net-dsa-mv88e6xxx-Add-LED-support-for-6393X.patch @@ -1,7 +1,7 @@ From 307811da452a9d4b665bc0f06c514d74d8c26ab9 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Wed, 14 Jan 2026 18:22:41 +0100 -Subject: [PATCH 28/78] net: dsa: mv88e6xxx: Add LED support for 6393X +Subject: [PATCH 28/84] net: dsa: mv88e6xxx: Add LED support for 6393X Original commit: commit 462277b926140ee2d231317e92afb6cabf640268 @@ -22,7 +22,7 @@ Date: Thu Nov 16 21:59:35 2023 +0100 4 files changed, 450 insertions(+) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index 71458342a274..f0a662e9b847 100644 +index 71458342a274c..f0a662e9b8477 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -5669,6 +5669,7 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = { @@ -34,7 +34,7 @@ index 71458342a274..f0a662e9b847 100644 .port_set_upstream_port = mv88e6393x_port_set_upstream_port, .stats_snapshot = mv88e6390_g1_stats_snapshot, diff --git a/drivers/net/dsa/mv88e6xxx/leds.c b/drivers/net/dsa/mv88e6xxx/leds.c -index ab3bc645da56..7c5904300849 100644 +index ab3bc645da566..7c59043008494 100644 --- a/drivers/net/dsa/mv88e6xxx/leds.c +++ b/drivers/net/dsa/mv88e6xxx/leds.c @@ -102,6 +102,58 @@ struct mv88e6xxx_led_hwconfig { @@ -452,7 +452,7 @@ index ab3bc645da56..7c5904300849 100644 + return ret; +} diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c -index e28fdd21de38..1985b8553611 100644 +index e28fdd21de38e..1985b85536112 100644 --- a/drivers/net/dsa/mv88e6xxx/port.c +++ b/drivers/net/dsa/mv88e6xxx/port.c @@ -1604,6 +1604,39 @@ int mv88e6xxx_port_relinquish_ether_type(struct mv88e6xxx_chip *chip, int port) @@ -496,7 +496,7 @@ index e28fdd21de38..1985b8553611 100644 * Offset 0x19: Port IEEE Priority Remapping Registers [4-7] */ diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h -index d2ba9ed1f23e..f4a56671fc4c 100644 +index d2ba9ed1f23ea..f4a56671fc4c2 100644 --- a/drivers/net/dsa/mv88e6xxx/port.h +++ b/drivers/net/dsa/mv88e6xxx/port.h @@ -444,6 +444,9 @@ diff --git a/patches/linux/6.18.55/0029-wifi-brcmfmac-check-connection-state-before-querying.patch b/patches/linux/6.18.55/0029-wifi-brcmfmac-check-connection-state-before-querying.patch index 1e7ccf9bb..1a760398b 100644 --- a/patches/linux/6.18.55/0029-wifi-brcmfmac-check-connection-state-before-querying.patch +++ b/patches/linux/6.18.55/0029-wifi-brcmfmac-check-connection-state-before-querying.patch @@ -1,7 +1,7 @@ From 59c469b24ce0aee40cde6aa1ea986cf42dfc150c Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Mon, 19 Jan 2026 13:06:53 +0100 -Subject: [PATCH 29/78] wifi: brcmfmac: check connection state before querying +Subject: [PATCH 29/84] wifi: brcmfmac: check connection state before querying station info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -23,7 +23,7 @@ Signed-off-by: Mattias Walström 1 file changed, 5 insertions(+) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c -index 1fc9ecd86d5d..4189a127f4b3 100644 +index 1fc9ecd86d5d9..4189a127f4b37 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -3156,6 +3156,11 @@ brcmf_cfg80211_get_station(struct wiphy *wiphy, struct net_device *ndev, diff --git a/patches/linux/6.18.55/0030-wifi-brcmfmac-suppress-log-spam-for-regulatory-restr.patch b/patches/linux/6.18.55/0030-wifi-brcmfmac-suppress-log-spam-for-regulatory-restr.patch index bb686aac7..9e8571cd8 100644 --- a/patches/linux/6.18.55/0030-wifi-brcmfmac-suppress-log-spam-for-regulatory-restr.patch +++ b/patches/linux/6.18.55/0030-wifi-brcmfmac-suppress-log-spam-for-regulatory-restr.patch @@ -1,7 +1,7 @@ From 96510a92c5e05d36dfd1b66e4e7719adc3b6aaf1 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Tue, 20 Jan 2026 20:12:10 +0100 -Subject: [PATCH 30/78] wifi: brcmfmac: suppress log spam for +Subject: [PATCH 30/84] wifi: brcmfmac: suppress log spam for regulatory-restricted channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -20,7 +20,7 @@ Signed-off-by: Mattias Walström 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c -index 4189a127f4b3..88dac288a398 100644 +index 4189a127f4b37..88dac288a3986 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -8133,7 +8133,12 @@ brcmf_set_channel(struct brcmf_cfg80211_info *cfg, struct ieee80211_channel *cha diff --git a/patches/linux/6.18.55/0031-wifi-brcmfmac-reduce-log-noise-during-AP-to-station-.patch b/patches/linux/6.18.55/0031-wifi-brcmfmac-reduce-log-noise-during-AP-to-station-.patch index d6eee3ae1..19e03bc57 100644 --- a/patches/linux/6.18.55/0031-wifi-brcmfmac-reduce-log-noise-during-AP-to-station-.patch +++ b/patches/linux/6.18.55/0031-wifi-brcmfmac-reduce-log-noise-during-AP-to-station-.patch @@ -1,7 +1,7 @@ From 10cc35ebdae09afe942ce3e054be910b608af6ca Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Tue, 20 Jan 2026 20:18:45 +0100 -Subject: [PATCH 31/78] wifi: brcmfmac: reduce log noise during AP to station +Subject: [PATCH 31/84] wifi: brcmfmac: reduce log noise during AP to station transition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -27,7 +27,7 @@ Signed-off-by: Mattias Walström 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c -index 88dac288a398..0081ceb6c782 100644 +index 88dac288a3986..0081ceb6c7829 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -1030,13 +1030,15 @@ static void brcmf_scan_config_mpc(struct brcmf_if *ifp, int mpc) diff --git a/patches/linux/6.18.55/0032-net-phy-air_en8811h-add-OF-device-table-for-auto-loa.patch b/patches/linux/6.18.55/0032-net-phy-air_en8811h-add-OF-device-table-for-auto-loa.patch index 980f7bbfd..c1dc01b07 100644 --- a/patches/linux/6.18.55/0032-net-phy-air_en8811h-add-OF-device-table-for-auto-loa.patch +++ b/patches/linux/6.18.55/0032-net-phy-air_en8811h-add-OF-device-table-for-auto-loa.patch @@ -1,7 +1,7 @@ From 98a492d3fa5e2472984335bfec5353b293677983 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Tue, 17 Feb 2026 21:59:59 +0100 -Subject: [PATCH 32/78] net: phy: air_en8811h: add OF device table for +Subject: [PATCH 32/84] net: phy: air_en8811h: add OF device table for auto-loading mdio_uevent() only emits an OF-style MODALIAS via @@ -20,7 +20,7 @@ rejects drivers that provide an of_match_table. 1 file changed, 6 insertions(+) diff --git a/drivers/net/phy/air_en8811h.c b/drivers/net/phy/air_en8811h.c -index badd65f0ccee..b609465247a2 100644 +index badd65f0ccee2..b609465247a28 100644 --- a/drivers/net/phy/air_en8811h.c +++ b/drivers/net/phy/air_en8811h.c @@ -1184,6 +1184,12 @@ static int en8811h_suspend(struct phy_device *phydev) diff --git a/patches/linux/6.18.55/0033-drm-vc4-dsi-enable-video-and-then-retry-failed-trans.patch b/patches/linux/6.18.55/0033-drm-vc4-dsi-enable-video-and-then-retry-failed-trans.patch index d50b355fd..e0f4ea1dc 100644 --- a/patches/linux/6.18.55/0033-drm-vc4-dsi-enable-video-and-then-retry-failed-trans.patch +++ b/patches/linux/6.18.55/0033-drm-vc4-dsi-enable-video-and-then-retry-failed-trans.patch @@ -1,7 +1,7 @@ From fe79facb35cd0c40ad8ad042680666351196cb22 Mon Sep 17 00:00:00 2001 From: Dave Stevenson Date: Fri, 20 Sep 2024 12:05:18 +0100 -Subject: [PATCH 33/78] drm: vc4: dsi: enable video and then retry failed +Subject: [PATCH 33/84] drm: vc4: dsi: enable video and then retry failed transfers The DSI block appears to be able to come up stuck in a condition where @@ -18,7 +18,7 @@ Signed-off-by: Dave Stevenson 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_dsi.c b/drivers/gpu/drm/vc4/vc4_dsi.c -index 458e5d987964..439d7a04127d 100644 +index 458e5d9879645..439d7a04127d8 100644 --- a/drivers/gpu/drm/vc4/vc4_dsi.c +++ b/drivers/gpu/drm/vc4/vc4_dsi.c @@ -286,6 +286,8 @@ diff --git a/patches/linux/6.18.55/0034-drm-vc4-dsi-Clocks-should-be-running-before-reset.patch b/patches/linux/6.18.55/0034-drm-vc4-dsi-Clocks-should-be-running-before-reset.patch index 59d6dc79e..168dbccd6 100644 --- a/patches/linux/6.18.55/0034-drm-vc4-dsi-Clocks-should-be-running-before-reset.patch +++ b/patches/linux/6.18.55/0034-drm-vc4-dsi-Clocks-should-be-running-before-reset.patch @@ -1,7 +1,7 @@ From bd07a3e59ac1c8f8ebfb54f5dc4e96c925f8381d Mon Sep 17 00:00:00 2001 From: Dave Stevenson Date: Wed, 8 Jun 2022 17:23:47 +0100 -Subject: [PATCH 34/78] drm: vc4: dsi: Clocks should be running before reset +Subject: [PATCH 34/84] drm: vc4: dsi: Clocks should be running before reset The initialisation sequence differs slightly from the documentation in that the clocks are meant to be running before resets and @@ -13,7 +13,7 @@ Signed-off-by: Dave Stevenson 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_dsi.c b/drivers/gpu/drm/vc4/vc4_dsi.c -index 439d7a04127d..1eba498d276f 100644 +index 439d7a04127d8..1eba498d276f8 100644 --- a/drivers/gpu/drm/vc4/vc4_dsi.c +++ b/drivers/gpu/drm/vc4/vc4_dsi.c @@ -926,6 +926,21 @@ static void vc4_dsi_bridge_pre_enable(struct drm_bridge *bridge, diff --git a/patches/linux/6.18.55/0035-drm-vc4-Ensure-DSI-is-enabled-for-FIFO-resets.patch b/patches/linux/6.18.55/0035-drm-vc4-Ensure-DSI-is-enabled-for-FIFO-resets.patch index 5dd80c251..286e34185 100644 --- a/patches/linux/6.18.55/0035-drm-vc4-Ensure-DSI-is-enabled-for-FIFO-resets.patch +++ b/patches/linux/6.18.55/0035-drm-vc4-Ensure-DSI-is-enabled-for-FIFO-resets.patch @@ -1,7 +1,7 @@ From 95a1f57cf66771a609d4155774264d962a99d297 Mon Sep 17 00:00:00 2001 From: Dave Stevenson Date: Fri, 5 Apr 2024 17:51:55 +0100 -Subject: [PATCH 35/78] drm/vc4: Ensure DSI is enabled for FIFO resets +Subject: [PATCH 35/84] drm/vc4: Ensure DSI is enabled for FIFO resets The block must be enabled for the FIFO resets to be actioned, so ensure this is the case. @@ -12,7 +12,7 @@ Signed-off-by: Dave Stevenson 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_dsi.c b/drivers/gpu/drm/vc4/vc4_dsi.c -index 1eba498d276f..de963edefb76 100644 +index 1eba498d276f8..de963edefb760 100644 --- a/drivers/gpu/drm/vc4/vc4_dsi.c +++ b/drivers/gpu/drm/vc4/vc4_dsi.c @@ -400,7 +400,8 @@ diff --git a/patches/linux/6.18.55/0036-drm-vc4-Reset-DSI-AFE-on-disable.patch b/patches/linux/6.18.55/0036-drm-vc4-Reset-DSI-AFE-on-disable.patch index 4418dc762..a1208bb9d 100644 --- a/patches/linux/6.18.55/0036-drm-vc4-Reset-DSI-AFE-on-disable.patch +++ b/patches/linux/6.18.55/0036-drm-vc4-Reset-DSI-AFE-on-disable.patch @@ -1,7 +1,7 @@ From 2f6f4f9db2f1a65c8b445dc44fce4fe3b7e82755 Mon Sep 17 00:00:00 2001 From: Dave Stevenson Date: Thu, 26 May 2022 18:56:19 +0100 -Subject: [PATCH 36/78] drm: vc4: Reset DSI AFE on disable +Subject: [PATCH 36/84] drm: vc4: Reset DSI AFE on disable vc4_dsi_bridge_disable wasn't resetting things during shutdown, so add that in. @@ -12,7 +12,7 @@ Signed-off-by: Dave Stevenson 1 file changed, 20 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_dsi.c b/drivers/gpu/drm/vc4/vc4_dsi.c -index de963edefb76..9756821ebf27 100644 +index de963edefb760..9756821ebf27c 100644 --- a/drivers/gpu/drm/vc4/vc4_dsi.c +++ b/drivers/gpu/drm/vc4/vc4_dsi.c @@ -360,6 +360,16 @@ diff --git a/patches/linux/6.18.55/0037-drm-vc4-dsi-Handle-the-different-command-FIFO-widths.patch b/patches/linux/6.18.55/0037-drm-vc4-dsi-Handle-the-different-command-FIFO-widths.patch index b8ce8fd24..7f56c5548 100644 --- a/patches/linux/6.18.55/0037-drm-vc4-dsi-Handle-the-different-command-FIFO-widths.patch +++ b/patches/linux/6.18.55/0037-drm-vc4-dsi-Handle-the-different-command-FIFO-widths.patch @@ -1,7 +1,7 @@ From 985a117c1a98da834b30c3a698675191341b53e1 Mon Sep 17 00:00:00 2001 From: Dave Stevenson Date: Wed, 20 Nov 2024 13:58:08 +0000 -Subject: [PATCH 37/78] drm: vc4: dsi: Handle the different command FIFO widths +Subject: [PATCH 37/84] drm: vc4: dsi: Handle the different command FIFO widths DSI0 and DSI1 have different widths for the command FIFO (24bit vs 32bit), but the driver was assuming the 32bit width of DSI1 @@ -17,7 +17,7 @@ Signed-off-by: Dave Stevenson 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_dsi.c b/drivers/gpu/drm/vc4/vc4_dsi.c -index 9756821ebf27..b1cc30b8aa79 100644 +index 9756821ebf27c..b1cc30b8aa797 100644 --- a/drivers/gpu/drm/vc4/vc4_dsi.c +++ b/drivers/gpu/drm/vc4/vc4_dsi.c @@ -44,7 +44,6 @@ diff --git a/patches/linux/6.18.55/0038-drm-bridge-tc358762-Program-the-DPI-mode-into-the-ch.patch b/patches/linux/6.18.55/0038-drm-bridge-tc358762-Program-the-DPI-mode-into-the-ch.patch index e27586d0a..ec78465d7 100644 --- a/patches/linux/6.18.55/0038-drm-bridge-tc358762-Program-the-DPI-mode-into-the-ch.patch +++ b/patches/linux/6.18.55/0038-drm-bridge-tc358762-Program-the-DPI-mode-into-the-ch.patch @@ -1,7 +1,7 @@ From 3eb94e911cb22e62435f70b33b515e93a5697e5d Mon Sep 17 00:00:00 2001 From: Dave Stevenson Date: Tue, 9 Jan 2024 17:37:00 +0000 -Subject: [PATCH 38/78] drm/bridge: tc358762: Program the DPI mode into the +Subject: [PATCH 38/84] drm/bridge: tc358762: Program the DPI mode into the chip The autodetection of resolution/timing by the TC358762 can lead @@ -16,7 +16,7 @@ Signed-off-by: Dave Stevenson 1 file changed, 15 insertions(+) diff --git a/drivers/gpu/drm/bridge/tc358762.c b/drivers/gpu/drm/bridge/tc358762.c -index 98df3e667d4a..73b169026e63 100644 +index 98df3e667d4aa..73b169026e63b 100644 --- a/drivers/gpu/drm/bridge/tc358762.c +++ b/drivers/gpu/drm/bridge/tc358762.c @@ -53,6 +53,12 @@ diff --git a/patches/linux/6.18.55/0039-drm-bridge-tc358762-revert-move-ops-to-enable.patch b/patches/linux/6.18.55/0039-drm-bridge-tc358762-revert-move-ops-to-enable.patch index 32e36366b..df60cf3b3 100644 --- a/patches/linux/6.18.55/0039-drm-bridge-tc358762-revert-move-ops-to-enable.patch +++ b/patches/linux/6.18.55/0039-drm-bridge-tc358762-revert-move-ops-to-enable.patch @@ -1,7 +1,7 @@ From e4d2f293250646a8109932ce816acfa1434cfc1c Mon Sep 17 00:00:00 2001 From: Dave Stevenson Date: Tue, 9 Jan 2024 18:44:49 +0000 -Subject: [PATCH 39/78] drm/bridge: tc358762: revert move ops to enable +Subject: [PATCH 39/84] drm/bridge: tc358762: revert move ops to enable Reverts 8a4b2fc9c91a ("drm/bridge: tc358762: Split register programming from pre-enable to enable") as we want the config commands sent before video starts. @@ -12,7 +12,7 @@ Signed-off-by: Dave Stevenson 1 file changed, 10 deletions(-) diff --git a/drivers/gpu/drm/bridge/tc358762.c b/drivers/gpu/drm/bridge/tc358762.c -index 73b169026e63..6164932edfec 100644 +index 73b169026e63b..6164932edfec9 100644 --- a/drivers/gpu/drm/bridge/tc358762.c +++ b/drivers/gpu/drm/bridge/tc358762.c @@ -202,15 +202,6 @@ static void tc358762_pre_enable(struct drm_bridge *bridge, diff --git a/patches/linux/6.18.55/0040-drm-bridge-tc358762-Set-pre_enabled-on-pre_enable-to.patch b/patches/linux/6.18.55/0040-drm-bridge-tc358762-Set-pre_enabled-on-pre_enable-to.patch index 702d2e779..b047d785d 100644 --- a/patches/linux/6.18.55/0040-drm-bridge-tc358762-Set-pre_enabled-on-pre_enable-to.patch +++ b/patches/linux/6.18.55/0040-drm-bridge-tc358762-Set-pre_enabled-on-pre_enable-to.patch @@ -1,7 +1,7 @@ From 0a1c5fc25afd628b5547348a4a65b1764f53f287 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Sat, 4 Apr 2026 18:04:19 +0200 -Subject: [PATCH 40/78] drm/bridge: tc358762: Set pre_enabled on pre_enable to +Subject: [PATCH 40/84] drm/bridge: tc358762: Set pre_enabled on pre_enable to prevent regulator imbalance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -16,7 +16,7 @@ Signed-off-by: Mattias Walström 1 file changed, 2 insertions(+) diff --git a/drivers/gpu/drm/bridge/tc358762.c b/drivers/gpu/drm/bridge/tc358762.c -index 6164932edfec..8da66b06d6b6 100644 +index 6164932edfec9..8da66b06d6b63 100644 --- a/drivers/gpu/drm/bridge/tc358762.c +++ b/drivers/gpu/drm/bridge/tc358762.c @@ -205,6 +205,8 @@ static void tc358762_pre_enable(struct drm_bridge *bridge, diff --git a/patches/linux/6.18.55/0041-net-pcs-add-standalone-PCS-registration-infrastructu.patch b/patches/linux/6.18.55/0041-net-pcs-add-standalone-PCS-registration-infrastructu.patch index fd33eebc3..8084b423c 100644 --- a/patches/linux/6.18.55/0041-net-pcs-add-standalone-PCS-registration-infrastructu.patch +++ b/patches/linux/6.18.55/0041-net-pcs-add-standalone-PCS-registration-infrastructu.patch @@ -1,7 +1,7 @@ From 384c6992b3b1e67d6515d00f20ac03a4deb2d3e9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 5 Apr 2026 11:33:00 +0200 -Subject: [PATCH 41/78] net/pcs: add standalone PCS registration infrastructure +Subject: [PATCH 41/84] net/pcs: add standalone PCS registration infrastructure Add a simple registration mechanism that allows platform PCS drivers to register their phylink_pcs instances, and consumers (e.g. Ethernet MAC @@ -22,7 +22,7 @@ Based on work by Daniel Golle . create mode 100644 include/linux/pcs/pcs-standalone.h diff --git a/drivers/net/pcs/Kconfig b/drivers/net/pcs/Kconfig -index ecbc3530e780..f6b4de7f972c 100644 +index ecbc3530e780c..f6b4de7f972cc 100644 --- a/drivers/net/pcs/Kconfig +++ b/drivers/net/pcs/Kconfig @@ -5,6 +5,10 @@ @@ -37,7 +37,7 @@ index ecbc3530e780..f6b4de7f972c 100644 tristate "Synopsys DesignWare Ethernet XPCS" select PHYLINK diff --git a/drivers/net/pcs/Makefile b/drivers/net/pcs/Makefile -index 4f7920618b90..0cb0057f2b8e 100644 +index 4f7920618b900..0cb0057f2b8e6 100644 --- a/drivers/net/pcs/Makefile +++ b/drivers/net/pcs/Makefile @@ -4,6 +4,7 @@ @@ -50,7 +50,7 @@ index 4f7920618b90..0cb0057f2b8e 100644 obj-$(CONFIG_PCS_MTK_LYNXI) += pcs-mtk-lynxi.o diff --git a/drivers/net/pcs/pcs-standalone.c b/drivers/net/pcs/pcs-standalone.c new file mode 100644 -index 000000000000..f7b46de59636 +index 0000000000000..f7b46de59636c --- /dev/null +++ b/drivers/net/pcs/pcs-standalone.c @@ -0,0 +1,95 @@ @@ -151,7 +151,7 @@ index 000000000000..f7b46de59636 +MODULE_LICENSE("GPL"); diff --git a/include/linux/pcs/pcs-standalone.h b/include/linux/pcs/pcs-standalone.h new file mode 100644 -index 000000000000..520835cdeee8 +index 0000000000000..520835cdeee83 --- /dev/null +++ b/include/linux/pcs/pcs-standalone.h @@ -0,0 +1,26 @@ diff --git a/patches/linux/6.18.55/0042-net-pcs-add-MediaTek-MT7988-USXGMII-PCS-driver.patch b/patches/linux/6.18.55/0042-net-pcs-add-MediaTek-MT7988-USXGMII-PCS-driver.patch index ae942d64f..98267cc8b 100644 --- a/patches/linux/6.18.55/0042-net-pcs-add-MediaTek-MT7988-USXGMII-PCS-driver.patch +++ b/patches/linux/6.18.55/0042-net-pcs-add-MediaTek-MT7988-USXGMII-PCS-driver.patch @@ -1,7 +1,7 @@ From 93142130e7b12203fb7b8c421bc5c4f3a6c065e7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 6 Apr 2026 14:14:23 +0200 -Subject: [PATCH 42/78] net/pcs: add MediaTek MT7988 USXGMII PCS driver +Subject: [PATCH 42/84] net/pcs: add MediaTek MT7988 USXGMII PCS driver Add a PCS driver for the USXGMII subsystem found in the MediaTek MT7988 SoC (usxgmiisys0 at 0x10080000, usxgmiisys1 at 0x10081000). The hardware @@ -27,7 +27,7 @@ Daniel Golle . create mode 100644 drivers/net/pcs/pcs-mtk-usxgmii.c diff --git a/drivers/net/pcs/Kconfig b/drivers/net/pcs/Kconfig -index f6b4de7f972c..563c12d6aa9c 100644 +index f6b4de7f972cc..563c12d6aa9cf 100644 --- a/drivers/net/pcs/Kconfig +++ b/drivers/net/pcs/Kconfig @@ -29,6 +29,16 @@ config PCS_MTK_LYNXI @@ -48,7 +48,7 @@ index f6b4de7f972c..563c12d6aa9c 100644 tristate "Renesas RZ/N1, RZ/N2H, RZ/T2H MII converter" depends on OF diff --git a/drivers/net/pcs/Makefile b/drivers/net/pcs/Makefile -index 0cb0057f2b8e..b876cb2157b1 100644 +index 0cb0057f2b8e6..b876cb2157b15 100644 --- a/drivers/net/pcs/Makefile +++ b/drivers/net/pcs/Makefile @@ -8,4 +8,5 @@ obj-$(CONFIG_PCS_STANDALONE) += pcs-standalone.o @@ -59,7 +59,7 @@ index 0cb0057f2b8e..b876cb2157b1 100644 obj-$(CONFIG_PCS_RZN1_MIIC) += pcs-rzn1-miic.o diff --git a/drivers/net/pcs/pcs-mtk-usxgmii.c b/drivers/net/pcs/pcs-mtk-usxgmii.c new file mode 100644 -index 000000000000..f0e7d418db38 +index 0000000000000..f0e7d418db388 --- /dev/null +++ b/drivers/net/pcs/pcs-mtk-usxgmii.c @@ -0,0 +1,394 @@ diff --git a/patches/linux/6.18.55/0043-net-ethernet-mediatek-add-USXGMII-support-for-MT7988.patch b/patches/linux/6.18.55/0043-net-ethernet-mediatek-add-USXGMII-support-for-MT7988.patch index 82352f2fd..cf105113c 100644 --- a/patches/linux/6.18.55/0043-net-ethernet-mediatek-add-USXGMII-support-for-MT7988.patch +++ b/patches/linux/6.18.55/0043-net-ethernet-mediatek-add-USXGMII-support-for-MT7988.patch @@ -1,7 +1,7 @@ From 54ba118cc31f35998a57a2b12809037fc675ad61 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 6 Apr 2026 14:15:43 +0200 -Subject: [PATCH 43/78] net: ethernet: mediatek: add USXGMII support for MT7988 +Subject: [PATCH 43/84] net: ethernet: mediatek: add USXGMII support for MT7988 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -33,7 +33,7 @@ Daniel Golle . 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/drivers/net/ethernet/mediatek/mtk_eth_path.c b/drivers/net/ethernet/mediatek/mtk_eth_path.c -index b4c01e2878f6..57380776032a 100644 +index b4c01e2878f6f..57380776032a7 100644 --- a/drivers/net/ethernet/mediatek/mtk_eth_path.c +++ b/drivers/net/ethernet/mediatek/mtk_eth_path.c @@ -37,6 +37,12 @@ static const char *mtk_eth_path_name(u64 path) @@ -126,7 +126,7 @@ index b4c01e2878f6..57380776032a 100644 +} + diff --git a/drivers/net/ethernet/mediatek/mtk_eth_soc.c b/drivers/net/ethernet/mediatek/mtk_eth_soc.c -index 3f43e487d887..37ff40b09815 100644 +index 3f43e487d8875..37ff40b098157 100644 --- a/drivers/net/ethernet/mediatek/mtk_eth_soc.c +++ b/drivers/net/ethernet/mediatek/mtk_eth_soc.c @@ -22,6 +22,7 @@ @@ -211,7 +211,7 @@ index 3f43e487d887..37ff40b09815 100644 mac_ops = &rt5350_phylink_ops; diff --git a/drivers/net/ethernet/mediatek/mtk_eth_soc.h b/drivers/net/ethernet/mediatek/mtk_eth_soc.h -index 88a9b3b23bea..340ecb824f59 100644 +index 88a9b3b23bea5..340ecb824f59a 100644 --- a/drivers/net/ethernet/mediatek/mtk_eth_soc.h +++ b/drivers/net/ethernet/mediatek/mtk_eth_soc.h @@ -567,6 +567,7 @@ diff --git a/patches/linux/6.18.55/0044-arm64-dts-mediatek-mt7988a-add-USXGMII-PCS-nodes.patch b/patches/linux/6.18.55/0044-arm64-dts-mediatek-mt7988a-add-USXGMII-PCS-nodes.patch index 1d2a19627..b0bcac233 100644 --- a/patches/linux/6.18.55/0044-arm64-dts-mediatek-mt7988a-add-USXGMII-PCS-nodes.patch +++ b/patches/linux/6.18.55/0044-arm64-dts-mediatek-mt7988a-add-USXGMII-PCS-nodes.patch @@ -1,7 +1,7 @@ From 262f24d9ace1f68b30a1e6f1728be00300addf2e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 6 Apr 2026 14:15:56 +0200 -Subject: [PATCH 44/78] arm64: dts: mediatek: mt7988a: add USXGMII PCS nodes +Subject: [PATCH 44/84] arm64: dts: mediatek: mt7988a: add USXGMII PCS nodes Add device nodes for the two USXGMII subsystem blocks (usxgmiisys0 at 0x10080000 and usxgmiisys1 at 0x10081000), each referencing its clock, @@ -14,7 +14,7 @@ property, so the Ethernet driver can discover the PCS at probe time. 1 file changed, 20 insertions(+) diff --git a/arch/arm64/boot/dts/mediatek/mt7988a.dtsi b/arch/arm64/boot/dts/mediatek/mt7988a.dtsi -index 366203a72d6d..5ae43390f6c2 100644 +index 366203a72d6d2..5ae43390f6c2c 100644 --- a/arch/arm64/boot/dts/mediatek/mt7988a.dtsi +++ b/arch/arm64/boot/dts/mediatek/mt7988a.dtsi @@ -708,6 +708,24 @@ xfi_pll: clock-controller@11f40000 { diff --git a/patches/linux/6.18.55/0045-arm64-dts-mediatek-bananapi-bpi-r4-enable-SFP-ports-.patch b/patches/linux/6.18.55/0045-arm64-dts-mediatek-bananapi-bpi-r4-enable-SFP-ports-.patch index 7b74dcb09..3772aeeb7 100644 --- a/patches/linux/6.18.55/0045-arm64-dts-mediatek-bananapi-bpi-r4-enable-SFP-ports-.patch +++ b/patches/linux/6.18.55/0045-arm64-dts-mediatek-bananapi-bpi-r4-enable-SFP-ports-.patch @@ -1,7 +1,7 @@ From 4ef35eea0eaca6789595196ca8399154ba53f034 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 6 Apr 2026 14:16:11 +0200 -Subject: [PATCH 45/78] arm64: dts: mediatek: bananapi-bpi-r4: enable SFP+ +Subject: [PATCH 45/84] arm64: dts: mediatek: bananapi-bpi-r4: enable SFP+ ports and WPS button Enable the SFP+ cages wired to gmac1 and gmac2. The USXGMII PCS nodes @@ -25,7 +25,7 @@ Signed-off-by: Joachim Wiberg 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/mediatek/mt7988a-bananapi-bpi-r4.dts b/arch/arm64/boot/dts/mediatek/mt7988a-bananapi-bpi-r4.dts -index 4b3796ba82e3..499f0c91c213 100644 +index 4b3796ba82e36..499f0c91c2137 100644 --- a/arch/arm64/boot/dts/mediatek/mt7988a-bananapi-bpi-r4.dts +++ b/arch/arm64/boot/dts/mediatek/mt7988a-bananapi-bpi-r4.dts @@ -27,6 +27,7 @@ &gmac1 { @@ -37,7 +37,7 @@ index 4b3796ba82e3..499f0c91c213 100644 &pca9545 { diff --git a/arch/arm64/boot/dts/mediatek/mt7988a-bananapi-bpi-r4.dtsi b/arch/arm64/boot/dts/mediatek/mt7988a-bananapi-bpi-r4.dtsi -index 0ff69dae45d3..2a904a76f3fe 100644 +index 0ff69dae45d32..2a904a76f3fe3 100644 --- a/arch/arm64/boot/dts/mediatek/mt7988a-bananapi-bpi-r4.dtsi +++ b/arch/arm64/boot/dts/mediatek/mt7988a-bananapi-bpi-r4.dtsi @@ -3,9 +3,9 @@ diff --git a/patches/linux/6.18.55/0046-net-phy-sfp-add-OEM-SFP-10G-T-I-quirk.patch b/patches/linux/6.18.55/0046-net-phy-sfp-add-OEM-SFP-10G-T-I-quirk.patch index c0c5676a2..31ff41af2 100644 --- a/patches/linux/6.18.55/0046-net-phy-sfp-add-OEM-SFP-10G-T-I-quirk.patch +++ b/patches/linux/6.18.55/0046-net-phy-sfp-add-OEM-SFP-10G-T-I-quirk.patch @@ -1,7 +1,7 @@ From 81a4f7212dc829bf60e7c96bcae2317d5545a428 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 7 Apr 2026 07:34:52 +0200 -Subject: [PATCH 46/78] net: phy: sfp: add OEM SFP-10G-T-I quirk +Subject: [PATCH 46/84] net: phy: sfp: add OEM SFP-10G-T-I quirk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -26,7 +26,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 11 insertions(+) diff --git a/drivers/net/phy/sfp.c b/drivers/net/phy/sfp.c -index af43530ffb2c..15a4b67ece2c 100644 +index af43530ffb2c3..15a4b67ece2cb 100644 --- a/drivers/net/phy/sfp.c +++ b/drivers/net/phy/sfp.c @@ -451,6 +451,16 @@ static void sfp_fixup_rollball_cc(struct sfp *sfp) diff --git a/patches/linux/6.18.55/0047-net-dsa-mv88e6xxx-Trap-PTP-frames-on-timestamping-po.patch b/patches/linux/6.18.55/0047-net-dsa-mv88e6xxx-Trap-PTP-frames-on-timestamping-po.patch index a42c7ec9c..3fd65153d 100644 --- a/patches/linux/6.18.55/0047-net-dsa-mv88e6xxx-Trap-PTP-frames-on-timestamping-po.patch +++ b/patches/linux/6.18.55/0047-net-dsa-mv88e6xxx-Trap-PTP-frames-on-timestamping-po.patch @@ -1,7 +1,7 @@ From d2cf171d24e9d08ad6a3bb43ad6790c4e03b3626 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 17 Apr 2026 09:13:04 +0000 -Subject: [PATCH 47/78] net: dsa: mv88e6xxx: Trap PTP frames on timestamping +Subject: [PATCH 47/84] net: dsa: mv88e6xxx: Trap PTP frames on timestamping ports, on 6393X Similar to the Peridot (6390), the designation of PTP frames as @@ -23,7 +23,7 @@ a port. 7 files changed, 67 insertions(+), 1 deletion(-) diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c -index f0a662e9b847..54bf531fcc35 100644 +index f0a662e9b8477..54bf531fcc35c 100644 --- a/drivers/net/dsa/mv88e6xxx/chip.c +++ b/drivers/net/dsa/mv88e6xxx/chip.c @@ -5700,7 +5700,7 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = { @@ -36,7 +36,7 @@ index f0a662e9b847..54bf531fcc35 100644 .pcs_ops = &mv88e6393x_pcs_ops, }; diff --git a/drivers/net/dsa/mv88e6xxx/hwtstamp.c b/drivers/net/dsa/mv88e6xxx/hwtstamp.c -index 6e6472a3b75a..446e1c616752 100644 +index 6e6472a3b75ad..446e1c616752d 100644 --- a/drivers/net/dsa/mv88e6xxx/hwtstamp.c +++ b/drivers/net/dsa/mv88e6xxx/hwtstamp.c @@ -13,6 +13,7 @@ @@ -80,7 +80,7 @@ index 6e6472a3b75a..446e1c616752 100644 { const struct mv88e6xxx_ptp_ops *ptp_ops = chip->info->ops->ptp_ops; diff --git a/drivers/net/dsa/mv88e6xxx/hwtstamp.h b/drivers/net/dsa/mv88e6xxx/hwtstamp.h -index c359821d5a6e..c51cd1ab2978 100644 +index c359821d5a6ea..c51cd1ab29788 100644 --- a/drivers/net/dsa/mv88e6xxx/hwtstamp.h +++ b/drivers/net/dsa/mv88e6xxx/hwtstamp.h @@ -129,6 +129,8 @@ int mv88e6xxx_hwtstamp_setup(struct mv88e6xxx_chip *chip); @@ -93,7 +93,7 @@ index c359821d5a6e..c51cd1ab2978 100644 int mv88e6165_global_disable(struct mv88e6xxx_chip *chip); diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c -index 1985b8553611..843972077576 100644 +index 1985b85536112..843972077576c 100644 --- a/drivers/net/dsa/mv88e6xxx/port.c +++ b/drivers/net/dsa/mv88e6xxx/port.c @@ -1484,6 +1484,16 @@ int mv88e6393x_port_set_upstream_port(struct mv88e6xxx_chip *chip, int port, @@ -114,7 +114,7 @@ index 1985b8553611..843972077576 100644 { u16 ptr; diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h -index f4a56671fc4c..5f733f84b348 100644 +index f4a56671fc4c2..5f733f84b3481 100644 --- a/drivers/net/dsa/mv88e6xxx/port.h +++ b/drivers/net/dsa/mv88e6xxx/port.h @@ -283,6 +283,8 @@ @@ -136,7 +136,7 @@ index f4a56671fc4c..5f733f84b348 100644 int mv88e6xxx_port_set_message_port(struct mv88e6xxx_chip *chip, int port, bool message_port); diff --git a/drivers/net/dsa/mv88e6xxx/ptp.c b/drivers/net/dsa/mv88e6xxx/ptp.c -index f7603573d3a9..327a9f786437 100644 +index f7603573d3a98..327a9f786437a 100644 --- a/drivers/net/dsa/mv88e6xxx/ptp.c +++ b/drivers/net/dsa/mv88e6xxx/ptp.c @@ -444,6 +444,29 @@ const struct mv88e6xxx_ptp_ops mv88e6390_ptp_ops = { @@ -170,7 +170,7 @@ index f7603573d3a9..327a9f786437 100644 { struct mv88e6xxx_chip *chip = cc_to_chip(cc); diff --git a/drivers/net/dsa/mv88e6xxx/ptp.h b/drivers/net/dsa/mv88e6xxx/ptp.h -index 95bdddb0bf39..54dce1fbf1aa 100644 +index 95bdddb0bf39f..54dce1fbf1aa2 100644 --- a/drivers/net/dsa/mv88e6xxx/ptp.h +++ b/drivers/net/dsa/mv88e6xxx/ptp.h @@ -75,6 +75,7 @@ void mv88e6xxx_ptp_free(struct mv88e6xxx_chip *chip); diff --git a/patches/linux/6.18.55/0048-wifi-mt76-mt7615-add-MODULE_DEVICE_TABLE-for-mt7622-.patch b/patches/linux/6.18.55/0048-wifi-mt76-mt7615-add-MODULE_DEVICE_TABLE-for-mt7622-.patch index fc12fd035..aa0afe3b0 100644 --- a/patches/linux/6.18.55/0048-wifi-mt76-mt7615-add-MODULE_DEVICE_TABLE-for-mt7622-.patch +++ b/patches/linux/6.18.55/0048-wifi-mt76-mt7615-add-MODULE_DEVICE_TABLE-for-mt7622-.patch @@ -1,7 +1,7 @@ From b6ad175129b948b44460af7ef7701e244e3a3b55 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Apr 2026 15:30:01 +0200 -Subject: [PATCH 48/78] wifi: mt76: mt7615: add MODULE_DEVICE_TABLE for mt7622 +Subject: [PATCH 48/84] wifi: mt76: mt7615: add MODULE_DEVICE_TABLE for mt7622 wmac Without MODULE_DEVICE_TABLE(of, ...) the OF compatible alias is never @@ -15,7 +15,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 1 insertion(+) diff --git a/drivers/net/wireless/mediatek/mt76/mt7615/soc.c b/drivers/net/wireless/mediatek/mt76/mt7615/soc.c -index 06a0f2a141e8..538ea1dc333b 100644 +index 06a0f2a141e8c..538ea1dc333b0 100644 --- a/drivers/net/wireless/mediatek/mt76/mt7615/soc.c +++ b/drivers/net/wireless/mediatek/mt76/mt7615/soc.c @@ -56,6 +56,7 @@ static const struct of_device_id mt7622_wmac_of_match[] = { diff --git a/patches/linux/6.18.55/0049-PCI-mediatek-gen3-Fix-PERST-control-timing-during-sy.patch b/patches/linux/6.18.55/0049-PCI-mediatek-gen3-Fix-PERST-control-timing-during-sy.patch index dddc27bc9..7a26b9240 100644 --- a/patches/linux/6.18.55/0049-PCI-mediatek-gen3-Fix-PERST-control-timing-during-sy.patch +++ b/patches/linux/6.18.55/0049-PCI-mediatek-gen3-Fix-PERST-control-timing-during-sy.patch @@ -1,7 +1,7 @@ From eb6bf2a9d6967617eea9b242603e0352f3856a00 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Wed, 22 Apr 2026 10:24:43 +0200 -Subject: [PATCH 49/78] PCI: mediatek-gen3: Fix PERST# control timing during +Subject: [PATCH 49/84] PCI: mediatek-gen3: Fix PERST# control timing during system startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -30,7 +30,7 @@ PERST# control timing during system startup"). 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/drivers/pci/controller/pcie-mediatek-gen3.c b/drivers/pci/controller/pcie-mediatek-gen3.c -index e45c43ccc84c..af84bc63bcad 100644 +index e45c43ccc84c2..af84bc63bcad5 100644 --- a/drivers/pci/controller/pcie-mediatek-gen3.c +++ b/drivers/pci/controller/pcie-mediatek-gen3.c @@ -479,16 +479,35 @@ static int mtk_pcie_startup_port(struct mtk_gen3_pcie *pcie) diff --git a/patches/linux/6.18.55/0050-net-dsa-mv88e6xxx-Derive-LED-names-from-device-name.patch b/patches/linux/6.18.55/0050-net-dsa-mv88e6xxx-Derive-LED-names-from-device-name.patch index 0cb957ef3..f0d7957b4 100644 --- a/patches/linux/6.18.55/0050-net-dsa-mv88e6xxx-Derive-LED-names-from-device-name.patch +++ b/patches/linux/6.18.55/0050-net-dsa-mv88e6xxx-Derive-LED-names-from-device-name.patch @@ -1,7 +1,7 @@ From ebaca6761deffa22bf18f0dfda287fa0e3210694 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Wed, 12 Aug 2026 10:08:53 +0200 -Subject: [PATCH 50/78] net: dsa: mv88e6xxx: Derive LED names from device name +Subject: [PATCH 50/84] net: dsa: mv88e6xxx: Derive LED names from device name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -16,7 +16,7 @@ Signed-off-by: Mattias Walström 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/net/dsa/mv88e6xxx/leds.c b/drivers/net/dsa/mv88e6xxx/leds.c -index 7c5904300849..2c09c37be077 100644 +index 7c59043008494..2c09c37be077b 100644 --- a/drivers/net/dsa/mv88e6xxx/leds.c +++ b/drivers/net/dsa/mv88e6xxx/leds.c @@ -874,8 +874,8 @@ int mv88e6xxx_port_setup_leds(struct mv88e6xxx_chip *chip, int port) diff --git a/patches/linux/6.18.55/0051-phy-sparx5-serdes-make-it-selectable-for-ARCH_LAN969.patch b/patches/linux/6.18.55/0051-phy-sparx5-serdes-make-it-selectable-for-ARCH_LAN969.patch index 04e64f390..07e76a5b9 100644 --- a/patches/linux/6.18.55/0051-phy-sparx5-serdes-make-it-selectable-for-ARCH_LAN969.patch +++ b/patches/linux/6.18.55/0051-phy-sparx5-serdes-make-it-selectable-for-ARCH_LAN969.patch @@ -1,7 +1,7 @@ From 4706c45681cda924ab8eaf9015be7a911c15c183 Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Fri, 31 Oct 2025 13:18:12 +0100 -Subject: [PATCH 51/78] phy: sparx5-serdes: make it selectable for ARCH_LAN969X +Subject: [PATCH 51/84] phy: sparx5-serdes: make it selectable for ARCH_LAN969X LAN969x uses the SparX-5 SERDES driver, so make it selectable for ARCH_LAN969X. @@ -18,7 +18,7 @@ Signed-off-by: Vinod Koul 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/phy/microchip/Kconfig b/drivers/phy/microchip/Kconfig -index 2f0045e874ac..2e6d1224711e 100644 +index 2f0045e874ac8..2e6d1224711e3 100644 --- a/drivers/phy/microchip/Kconfig +++ b/drivers/phy/microchip/Kconfig @@ -6,7 +6,7 @@ diff --git a/patches/linux/6.18.55/0052-net-sparx5-fix-wrong-chip-ids-for-TSN-SKUs.patch b/patches/linux/6.18.55/0052-net-sparx5-fix-wrong-chip-ids-for-TSN-SKUs.patch index 2ac52662f..b8380741a 100644 --- a/patches/linux/6.18.55/0052-net-sparx5-fix-wrong-chip-ids-for-TSN-SKUs.patch +++ b/patches/linux/6.18.55/0052-net-sparx5-fix-wrong-chip-ids-for-TSN-SKUs.patch @@ -1,7 +1,7 @@ From 30f7485ae40aaad3d66606c49c196c28d65983ae Mon Sep 17 00:00:00 2001 From: Daniel Machon Date: Wed, 6 May 2026 09:25:38 +0200 -Subject: [PATCH 52/78] net: sparx5: fix wrong chip ids for TSN SKUs +Subject: [PATCH 52/84] net: sparx5: fix wrong chip ids for TSN SKUs The TSN SKUs in enum spx5_target_chiptype have incorrect IDs: @@ -30,7 +30,7 @@ Signed-off-by: Jakub Kicinski 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_main.h b/drivers/net/ethernet/microchip/sparx5/sparx5_main.h -index fe7d8bcc0cd9..2f1b50ba8ad3 100644 +index fe7d8bcc0cd99..2f1b50ba8ad34 100644 --- a/drivers/net/ethernet/microchip/sparx5/sparx5_main.h +++ b/drivers/net/ethernet/microchip/sparx5/sparx5_main.h @@ -31,11 +31,11 @@ enum spx5_target_chiptype { diff --git a/patches/linux/6.18.55/0053-net-sparx5-configure-serdes-for-1000BASE-X-in-sparx5.patch b/patches/linux/6.18.55/0053-net-sparx5-configure-serdes-for-1000BASE-X-in-sparx5.patch index 63e7c6fab..fabcf356c 100644 --- a/patches/linux/6.18.55/0053-net-sparx5-configure-serdes-for-1000BASE-X-in-sparx5.patch +++ b/patches/linux/6.18.55/0053-net-sparx5-configure-serdes-for-1000BASE-X-in-sparx5.patch @@ -1,7 +1,7 @@ From 1ac619e910f3911443963cd2359dd89958213313 Mon Sep 17 00:00:00 2001 From: Daniel Machon Date: Wed, 6 May 2026 09:25:39 +0200 -Subject: [PATCH 53/78] net: sparx5: configure serdes for 1000BASE-X in +Subject: [PATCH 53/84] net: sparx5: configure serdes for 1000BASE-X in sparx5_port_init() sparx5_port_init() only invokes sparx5_serdes_set() and the associated @@ -28,7 +28,7 @@ Signed-off-by: Jakub Kicinski 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_port.c b/drivers/net/ethernet/microchip/sparx5/sparx5_port.c -index 04bc8fffaf96..62c49893de3c 100644 +index 04bc8fffaf961..62c49893de3c3 100644 --- a/drivers/net/ethernet/microchip/sparx5/sparx5_port.c +++ b/drivers/net/ethernet/microchip/sparx5/sparx5_port.c @@ -1128,7 +1128,8 @@ int sparx5_port_init(struct sparx5 *sparx5, diff --git a/patches/linux/6.18.55/0054-dt-bindings-mmc-atmel-sama5d2-sdhci-add-microchip-la.patch b/patches/linux/6.18.55/0054-dt-bindings-mmc-atmel-sama5d2-sdhci-add-microchip-la.patch index 856b42976..617348f3c 100644 --- a/patches/linux/6.18.55/0054-dt-bindings-mmc-atmel-sama5d2-sdhci-add-microchip-la.patch +++ b/patches/linux/6.18.55/0054-dt-bindings-mmc-atmel-sama5d2-sdhci-add-microchip-la.patch @@ -1,7 +1,7 @@ From 7fe972637a8ffbdf55391e7f6927649a50caae9a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 19 Aug 2026 11:35:29 +0200 -Subject: [PATCH 54/78] dt-bindings: mmc: atmel,sama5d2-sdhci: add +Subject: [PATCH 54/84] dt-bindings: mmc: atmel,sama5d2-sdhci: add microchip,lan969x-sdhci The LAN969x SDMMC controller has its own base clock divider and, unlike @@ -15,7 +15,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/mmc/atmel,sama5d2-sdhci.yaml b/Documentation/devicetree/bindings/mmc/atmel,sama5d2-sdhci.yaml -index ba75623b7778..860e6736e2eb 100644 +index ba75623b7778b..860e6736e2eb9 100644 --- a/Documentation/devicetree/bindings/mmc/atmel,sama5d2-sdhci.yaml +++ b/Documentation/devicetree/bindings/mmc/atmel,sama5d2-sdhci.yaml @@ -18,6 +18,7 @@ properties: diff --git a/patches/linux/6.18.55/0055-mmc-sdhci-of-at91-add-LAN969x-support.patch b/patches/linux/6.18.55/0055-mmc-sdhci-of-at91-add-LAN969x-support.patch index eabf5693f..106b00a5d 100644 --- a/patches/linux/6.18.55/0055-mmc-sdhci-of-at91-add-LAN969x-support.patch +++ b/patches/linux/6.18.55/0055-mmc-sdhci-of-at91-add-LAN969x-support.patch @@ -1,7 +1,7 @@ From 7b461e9b726584d2d4cc908a6120138504e52127 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 19 Aug 2026 11:35:44 +0200 -Subject: [PATCH 55/78] mmc: sdhci-of-at91: add LAN969x support +Subject: [PATCH 55/84] mmc: sdhci-of-at91: add LAN969x support The LAN969x SDMMC controller is an Atmel SDMMC IP block, but the driver has no compatible for it, so the eMMC on LAN969x boards never probes. @@ -18,7 +18,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/drivers/mmc/host/sdhci-of-at91.c b/drivers/mmc/host/sdhci-of-at91.c -index 7c4ac65f247d..5dcb88cba0d9 100644 +index 7c4ac65f247d3..5dcb88cba0d9c 100644 --- a/drivers/mmc/host/sdhci-of-at91.c +++ b/drivers/mmc/host/sdhci-of-at91.c @@ -39,6 +39,9 @@ struct sdhci_at91_soc_data { diff --git a/patches/linux/6.18.55/0056-mmc-sdhci-of-at91-stop-SDCLK-on-reset-and-add-eMMC-h.patch b/patches/linux/6.18.55/0056-mmc-sdhci-of-at91-stop-SDCLK-on-reset-and-add-eMMC-h.patch index eb4ceed8e..ce1337166 100644 --- a/patches/linux/6.18.55/0056-mmc-sdhci-of-at91-stop-SDCLK-on-reset-and-add-eMMC-h.patch +++ b/patches/linux/6.18.55/0056-mmc-sdhci-of-at91-stop-SDCLK-on-reset-and-add-eMMC-h.patch @@ -1,7 +1,7 @@ From a55cfe74da903a95ff10f6f3659f6e4d26eb5a13 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 19 Aug 2026 11:37:48 +0200 -Subject: [PATCH 56/78] mmc: sdhci-of-at91: stop SDCLK on reset and add eMMC +Subject: [PATCH 56/84] mmc: sdhci-of-at91: stop SDCLK on reset and add eMMC hardware reset The controller is reset, and its signaling mode changed, with SDCLK @@ -20,7 +20,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 50 insertions(+) diff --git a/drivers/mmc/host/sdhci-of-at91.c b/drivers/mmc/host/sdhci-of-at91.c -index 5dcb88cba0d9..6a9e4a7fa705 100644 +index 5dcb88cba0d9c..6a9e4a7fa705f 100644 --- a/drivers/mmc/host/sdhci-of-at91.c +++ b/drivers/mmc/host/sdhci-of-at91.c @@ -25,6 +25,7 @@ diff --git a/patches/linux/6.18.55/0057-net-sparx5-lan969x-populate-netdev-of_node.patch b/patches/linux/6.18.55/0057-net-sparx5-lan969x-populate-netdev-of_node.patch index 396a1cb41..7aa6bf19c 100644 --- a/patches/linux/6.18.55/0057-net-sparx5-lan969x-populate-netdev-of_node.patch +++ b/patches/linux/6.18.55/0057-net-sparx5-lan969x-populate-netdev-of_node.patch @@ -1,7 +1,7 @@ From 0f61d1e5c4910ed9cd79fb1b3ebccae2d69498d3 Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Mon, 10 Nov 2025 13:42:53 +0100 -Subject: [PATCH 57/78] net: sparx5/lan969x: populate netdev of_node +Subject: [PATCH 57/84] net: sparx5/lan969x: populate netdev of_node Populate of_node for the port netdevs, to make the individual ports of_nodes available in sysfs. @@ -15,7 +15,7 @@ Signed-off-by: Jakub Kicinski 1 file changed, 2 insertions(+) diff --git a/drivers/net/ethernet/microchip/sparx5/sparx5_main.c b/drivers/net/ethernet/microchip/sparx5/sparx5_main.c -index 40b1bfc600a7..582145713cfd 100644 +index 40b1bfc600a79..582145713cfd1 100644 --- a/drivers/net/ethernet/microchip/sparx5/sparx5_main.c +++ b/drivers/net/ethernet/microchip/sparx5/sparx5_main.c @@ -395,6 +395,8 @@ static int sparx5_create_port(struct sparx5 *sparx5, diff --git a/patches/linux/6.18.55/0058-arm64-dts-microchip-add-LAN969x-clock-header-file.patch b/patches/linux/6.18.55/0058-arm64-dts-microchip-add-LAN969x-clock-header-file.patch index db7c9cedb..7499652f4 100644 --- a/patches/linux/6.18.55/0058-arm64-dts-microchip-add-LAN969x-clock-header-file.patch +++ b/patches/linux/6.18.55/0058-arm64-dts-microchip-add-LAN969x-clock-header-file.patch @@ -1,7 +1,7 @@ From 3bf84f8d8dcbd2c444ba8249ee5789941def7215 Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Mon, 2 Mar 2026 12:20:11 +0100 -Subject: [PATCH 58/78] arm64: dts: microchip: add LAN969x clock header file +Subject: [PATCH 58/84] arm64: dts: microchip: add LAN969x clock header file LAN969x uses hardware clock indexes, so document theses in a header to make them humanly readable. @@ -18,7 +18,7 @@ Signed-off-by: Claudiu Beznea diff --git a/arch/arm64/boot/dts/microchip/clk-lan9691.h b/arch/arm64/boot/dts/microchip/clk-lan9691.h new file mode 100644 -index 000000000000..0f2d7a0f881e +index 0000000000000..0f2d7a0f881ef --- /dev/null +++ b/arch/arm64/boot/dts/microchip/clk-lan9691.h @@ -0,0 +1,24 @@ diff --git a/patches/linux/6.18.55/0059-arm64-dts-microchip-add-LAN969x-support.patch b/patches/linux/6.18.55/0059-arm64-dts-microchip-add-LAN969x-support.patch index 8fa346b04..051bb988e 100644 --- a/patches/linux/6.18.55/0059-arm64-dts-microchip-add-LAN969x-support.patch +++ b/patches/linux/6.18.55/0059-arm64-dts-microchip-add-LAN969x-support.patch @@ -1,7 +1,7 @@ From 2783fbf634082295b24d8573ce91ea33017bb7b8 Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Mon, 2 Mar 2026 12:20:12 +0100 -Subject: [PATCH 59/78] arm64: dts: microchip: add LAN969x support +Subject: [PATCH 59/84] arm64: dts: microchip: add LAN969x support Add support for Microchip LAN969x switch SoC series by adding the SoC DTSI. @@ -18,7 +18,7 @@ Signed-off-by: Claudiu Beznea diff --git a/arch/arm64/boot/dts/microchip/lan9691.dtsi b/arch/arm64/boot/dts/microchip/lan9691.dtsi new file mode 100644 -index 000000000000..235e56bebbdb +index 0000000000000..235e56bebbdbb --- /dev/null +++ b/arch/arm64/boot/dts/microchip/lan9691.dtsi @@ -0,0 +1,488 @@ diff --git a/patches/linux/6.18.55/0060-arm64-dts-microchip-add-EV23X71A-board.patch b/patches/linux/6.18.55/0060-arm64-dts-microchip-add-EV23X71A-board.patch index 84e3ff4d0..ff24eea11 100644 --- a/patches/linux/6.18.55/0060-arm64-dts-microchip-add-EV23X71A-board.patch +++ b/patches/linux/6.18.55/0060-arm64-dts-microchip-add-EV23X71A-board.patch @@ -1,7 +1,7 @@ From 129276c0d1704ca77d786d6aa300913b28d92e1f Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Mon, 2 Mar 2026 12:20:14 +0100 -Subject: [PATCH 60/78] arm64: dts: microchip: add EV23X71A board +Subject: [PATCH 60/84] arm64: dts: microchip: add EV23X71A board Microchip EV23X71A is an LAN9696 based evaluation board. @@ -19,7 +19,7 @@ Signed-off-by: Claudiu Beznea create mode 100644 arch/arm64/boot/dts/microchip/lan9696-ev23x71a.dts diff --git a/arch/arm64/boot/dts/microchip/Makefile b/arch/arm64/boot/dts/microchip/Makefile -index c6e0313eea0f..09d16fc1ce9a 100644 +index c6e0313eea0f9..09d16fc1ce9ac 100644 --- a/arch/arm64/boot/dts/microchip/Makefile +++ b/arch/arm64/boot/dts/microchip/Makefile @@ -1,4 +1,5 @@ @@ -30,7 +30,7 @@ index c6e0313eea0f..09d16fc1ce9a 100644 dtb-$(CONFIG_ARCH_SPARX5) += sparx5_pcb135.dtb sparx5_pcb135_emmc.dtb diff --git a/arch/arm64/boot/dts/microchip/lan9696-ev23x71a.dts b/arch/arm64/boot/dts/microchip/lan9696-ev23x71a.dts new file mode 100644 -index 000000000000..4012ea7d07bb +index 0000000000000..4012ea7d07bbd --- /dev/null +++ b/arch/arm64/boot/dts/microchip/lan9696-ev23x71a.dts @@ -0,0 +1,756 @@ diff --git a/patches/linux/6.18.55/0061-arm64-dts-microchip-lan969x-add-OTP-node.patch b/patches/linux/6.18.55/0061-arm64-dts-microchip-lan969x-add-OTP-node.patch index 040b0870d..17c126c5b 100644 --- a/patches/linux/6.18.55/0061-arm64-dts-microchip-lan969x-add-OTP-node.patch +++ b/patches/linux/6.18.55/0061-arm64-dts-microchip-lan969x-add-OTP-node.patch @@ -1,7 +1,7 @@ From 6f418f0c5d3dd1ca1d431e23d8149413c5aa8f9a Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Fri, 15 May 2026 13:59:09 +0200 -Subject: [PATCH 61/78] arm64: dts: microchip: lan969x: add OTP node +Subject: [PATCH 61/84] arm64: dts: microchip: lan969x: add OTP node Add the required OTP on LAN969x. @@ -15,7 +15,7 @@ Signed-off-by: Claudiu Beznea 1 file changed, 5 insertions(+) diff --git a/arch/arm64/boot/dts/microchip/lan9691.dtsi b/arch/arm64/boot/dts/microchip/lan9691.dtsi -index 235e56bebbdb..ed997d87dd09 100644 +index 235e56bebbdbb..ed997d87dd09f 100644 --- a/arch/arm64/boot/dts/microchip/lan9691.dtsi +++ b/arch/arm64/boot/dts/microchip/lan9691.dtsi @@ -100,6 +100,11 @@ usb: usb@300000 { diff --git a/patches/linux/6.18.55/0062-arm64-dts-microchip-lan969x-add-SDMMC-nodes.patch b/patches/linux/6.18.55/0062-arm64-dts-microchip-lan969x-add-SDMMC-nodes.patch index fcfa1253d..b2160f91f 100644 --- a/patches/linux/6.18.55/0062-arm64-dts-microchip-lan969x-add-SDMMC-nodes.patch +++ b/patches/linux/6.18.55/0062-arm64-dts-microchip-lan969x-add-SDMMC-nodes.patch @@ -1,7 +1,7 @@ From a49eecdbfa8d6daa61c5baa4d2ebf3d006358fcb Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 19 Aug 2026 11:38:37 +0200 -Subject: [PATCH 62/78] arm64: dts: microchip: lan969x: add SDMMC nodes +Subject: [PATCH 62/84] arm64: dts: microchip: lan969x: add SDMMC nodes The SoC has two SDMMC controllers, neither of which is described, so boards with eMMC have no way to enable it. Add both, disabled by @@ -13,7 +13,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 22 insertions(+) diff --git a/arch/arm64/boot/dts/microchip/lan9691.dtsi b/arch/arm64/boot/dts/microchip/lan9691.dtsi -index ed997d87dd09..59a19462a5db 100644 +index ed997d87dd09f..59a19462a5db5 100644 --- a/arch/arm64/boot/dts/microchip/lan9691.dtsi +++ b/arch/arm64/boot/dts/microchip/lan9691.dtsi @@ -396,6 +396,28 @@ clks: clock-controller@e00c00b4 { diff --git a/patches/linux/6.18.55/0063-arm64-dts-microchip-ev23x71a-enable-eMMC.patch b/patches/linux/6.18.55/0063-arm64-dts-microchip-ev23x71a-enable-eMMC.patch index 8b2a898d8..29cc020d9 100644 --- a/patches/linux/6.18.55/0063-arm64-dts-microchip-ev23x71a-enable-eMMC.patch +++ b/patches/linux/6.18.55/0063-arm64-dts-microchip-ev23x71a-enable-eMMC.patch @@ -1,7 +1,7 @@ From 8dd67f639beee3bbe5c8992d0314b40ad46bb726 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 19 Aug 2026 11:38:37 +0200 -Subject: [PATCH 63/78] arm64: dts: microchip: ev23x71a: enable eMMC +Subject: [PATCH 63/84] arm64: dts: microchip: ev23x71a: enable eMMC The board has an 8-bit eMMC on SDMMC0, and defines the emmc_sd pinctrl group for it, but nothing enables the controller. @@ -12,7 +12,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 11 insertions(+) diff --git a/arch/arm64/boot/dts/microchip/lan9696-ev23x71a.dts b/arch/arm64/boot/dts/microchip/lan9696-ev23x71a.dts -index 4012ea7d07bb..7f12fc5c321f 100644 +index 4012ea7d07bbd..7f12fc5c321fe 100644 --- a/arch/arm64/boot/dts/microchip/lan9696-ev23x71a.dts +++ b/arch/arm64/boot/dts/microchip/lan9696-ev23x71a.dts @@ -463,6 +463,17 @@ phy27: phy@27 { diff --git a/patches/linux/6.18.55/0064-wifi-brcmfmac-survey-the-requested-interface-not-the.patch b/patches/linux/6.18.55/0064-wifi-brcmfmac-survey-the-requested-interface-not-the.patch index 8c224bce4..2145bae07 100644 --- a/patches/linux/6.18.55/0064-wifi-brcmfmac-survey-the-requested-interface-not-the.patch +++ b/patches/linux/6.18.55/0064-wifi-brcmfmac-survey-the-requested-interface-not-the.patch @@ -1,7 +1,7 @@ From 10d713882efe78b7be0dbda7e469ce7dd1830933 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Tue, 30 Jun 2026 15:42:48 +0200 -Subject: [PATCH 64/78] wifi: brcmfmac: survey the requested interface, not the +Subject: [PATCH 64/84] wifi: brcmfmac: survey the requested interface, not the primary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -28,7 +28,7 @@ Signed-off-by: Mattias Walström 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c -index 0081ceb6c782..8b245460262d 100644 +index 0081ceb6c7829..8b245460262d9 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -8124,11 +8124,11 @@ brcmf_dump_obss(struct brcmf_if *ifp, struct cca_msrmnt_query req, diff --git a/patches/linux/6.18.55/0065-wifi-brcmfmac-honor-caller-s-rtnl-lock-when-stopping.patch b/patches/linux/6.18.55/0065-wifi-brcmfmac-honor-caller-s-rtnl-lock-when-stopping.patch index 6b43d2c04..431eadd2b 100644 --- a/patches/linux/6.18.55/0065-wifi-brcmfmac-honor-caller-s-rtnl-lock-when-stopping.patch +++ b/patches/linux/6.18.55/0065-wifi-brcmfmac-honor-caller-s-rtnl-lock-when-stopping.patch @@ -1,7 +1,7 @@ From 0d0351b6cb2d86eab7728fcda9a6a89489e5eb0f Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Tue, 30 Jun 2026 15:43:14 +0200 -Subject: [PATCH 65/78] wifi: brcmfmac: honor caller's rtnl lock when stopping +Subject: [PATCH 65/84] wifi: brcmfmac: honor caller's rtnl lock when stopping primary interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -26,7 +26,7 @@ Signed-off-by: Mattias Walström 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c -index 048b5d500215..b0c79b50dc9a 100644 +index 048b5d500215e..b0c79b50dc9a2 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c @@ -935,9 +935,11 @@ static void brcmf_del_if(struct brcmf_pub *drvr, s32 bsscfgidx, diff --git a/patches/linux/6.18.55/0066-wifi-brcmfmac-let-cfg_to_ndev-return-NULL-and-harden.patch b/patches/linux/6.18.55/0066-wifi-brcmfmac-let-cfg_to_ndev-return-NULL-and-harden.patch index 8badcc793..f0633bb10 100644 --- a/patches/linux/6.18.55/0066-wifi-brcmfmac-let-cfg_to_ndev-return-NULL-and-harden.patch +++ b/patches/linux/6.18.55/0066-wifi-brcmfmac-let-cfg_to_ndev-return-NULL-and-harden.patch @@ -1,7 +1,7 @@ From 438486c55109e46bf0227704a6e28e3ccb4c751b Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Tue, 30 Jun 2026 15:46:25 +0200 -Subject: [PATCH 66/78] wifi: brcmfmac: let cfg_to_ndev() return NULL and +Subject: [PATCH 66/84] wifi: brcmfmac: let cfg_to_ndev() return NULL and harden its callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -47,7 +47,7 @@ Signed-off-by: Mattias Walström 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/btcoex.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/btcoex.c -index 67c0c5a92f99..265f3f5d5307 100644 +index 67c0c5a92f998..265f3f5d53079 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/btcoex.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/btcoex.c @@ -141,6 +141,9 @@ static void brcmf_btcoex_boost_wifi(struct brcmf_btcoex_info *btci, @@ -71,7 +71,7 @@ index 67c0c5a92f99..265f3f5d5307 100644 case BRCMF_BTCOEX_DISABLED: brcmf_dbg(INFO, "DHCP session starts\n"); diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c -index 8b245460262d..fd8c39254d46 100644 +index 8b245460262d9..fd8c39254d466 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -822,11 +822,18 @@ struct wireless_dev *brcmf_apsta_add_vif(struct wiphy *wiphy, const char *name, @@ -214,7 +214,7 @@ index 8b245460262d..fd8c39254d46 100644 if (err) { bphy_err(drvr, "Country code iovar returned err = %d\n", err); diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.h b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.h -index 273c80f2d483..ee7e9dc17dce 100644 +index 273c80f2d483a..ee7e9dc17dce0 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.h +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.h @@ -433,7 +433,9 @@ static inline struct brcmf_cfg80211_vif *wdev_to_vif(struct wireless_dev *wdev) @@ -229,7 +229,7 @@ index 273c80f2d483..ee7e9dc17dce 100644 static inline struct brcmf_cfg80211_info *ndev_to_cfg(struct net_device *ndev) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c -index b0c79b50dc9a..28e677660865 100644 +index b0c79b50dc9a2..28e6776608658 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/core.c @@ -1028,8 +1028,11 @@ static int brcmf_inetaddr_changed(struct notifier_block *nb, @@ -246,7 +246,7 @@ index b0c79b50dc9a..28e677660865 100644 /* retrieve the table from firmware */ ret = brcmf_fil_iovar_data_get(ifp, "arp_hostip", addr_table, diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/p2p.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/p2p.c -index e1752a513c73..5b550e5a4e77 100644 +index e1752a513c733..5b550e5a4e77e 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/p2p.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/p2p.c @@ -2115,8 +2115,12 @@ static int brcmf_p2p_disable_p2p_if(struct brcmf_cfg80211_vif *vif) diff --git a/patches/linux/6.18.55/0067-wifi-brcmfmac-support-deletion-and-recreation-of-the.patch b/patches/linux/6.18.55/0067-wifi-brcmfmac-support-deletion-and-recreation-of-the.patch index 8adbf256a..2f47f04fd 100644 --- a/patches/linux/6.18.55/0067-wifi-brcmfmac-support-deletion-and-recreation-of-the.patch +++ b/patches/linux/6.18.55/0067-wifi-brcmfmac-support-deletion-and-recreation-of-the.patch @@ -1,7 +1,7 @@ From 0c6d68f2957c4a73c66a2fbe6524cf54c11ad138 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Tue, 30 Jun 2026 16:05:17 +0200 -Subject: [PATCH 67/78] wifi: brcmfmac: support deletion and recreation of the +Subject: [PATCH 67/84] wifi: brcmfmac: support deletion and recreation of the primary interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -66,7 +66,7 @@ Signed-off-by: Mattias Walström 2 files changed, 174 insertions(+), 7 deletions(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c -index fd8c39254d46..c17930471845 100644 +index fd8c39254d466..c179304718455 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -124,6 +124,9 @@ struct cca_msrmnt_query { @@ -282,7 +282,7 @@ index fd8c39254d46..c17930471845 100644 case NL80211_IFTYPE_ADHOC: case NL80211_IFTYPE_AP_VLAN: diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/p2p.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/p2p.c -index 5b550e5a4e77..b1b85da83371 100644 +index 5b550e5a4e77e..b1b85da83371f 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/p2p.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/p2p.c @@ -1156,6 +1156,8 @@ static s32 brcmf_p2p_af_searching_channel(struct brcmf_p2p_info *p2p) diff --git a/patches/linux/6.18.55/0068-wifi-brcmfmac-report-port-authorized-after-offloaded.patch b/patches/linux/6.18.55/0068-wifi-brcmfmac-report-port-authorized-after-offloaded.patch index 4b5aa554c..e287e9ddc 100644 --- a/patches/linux/6.18.55/0068-wifi-brcmfmac-report-port-authorized-after-offloaded.patch +++ b/patches/linux/6.18.55/0068-wifi-brcmfmac-report-port-authorized-after-offloaded.patch @@ -1,7 +1,7 @@ From e0016e70627bbd29ad17dfdcf6890d6692ad2679 Mon Sep 17 00:00:00 2001 From: Mattias Walström Date: Thu, 10 Sep 2026 08:29:47 +0200 -Subject: [PATCH 68/78] wifi: brcmfmac: report port authorized after offloaded +Subject: [PATCH 68/84] wifi: brcmfmac: report port authorized after offloaded PSK/SAE handshake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -58,7 +58,7 @@ Signed-off-by: Mattias Walström 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c -index c17930471845..064daf8c42ec 100644 +index c179304718455..064daf8c42ec9 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -6705,6 +6705,23 @@ brcmf_bss_connect_done(struct brcmf_cfg80211_info *cfg, diff --git a/patches/linux/6.18.55/0069-dt-bindings-arm-AT91-document-Novarq-Tactical-1000.patch b/patches/linux/6.18.55/0069-dt-bindings-arm-AT91-document-Novarq-Tactical-1000.patch index b4de3473b..b33794948 100644 --- a/patches/linux/6.18.55/0069-dt-bindings-arm-AT91-document-Novarq-Tactical-1000.patch +++ b/patches/linux/6.18.55/0069-dt-bindings-arm-AT91-document-Novarq-Tactical-1000.patch @@ -1,7 +1,7 @@ From 87bb123eb9add21c4ed4bce67313dcc37b367079 Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Fri, 9 Jan 2026 13:27:00 +0100 -Subject: [PATCH 69/78] dt-bindings: arm: AT91: document Novarq Tactical 1000 +Subject: [PATCH 69/84] dt-bindings: arm: AT91: document Novarq Tactical 1000 Novarq Tactical 1000 is a Microchip LAN9696 based 24x1G + 4x10G SFP switch. @@ -12,7 +12,7 @@ Signed-off-by: Robert Marko 1 file changed, 6 insertions(+) diff --git a/Documentation/devicetree/bindings/arm/atmel-at91.yaml b/Documentation/devicetree/bindings/arm/atmel-at91.yaml -index 3a34b7a2e8d4..49666e92c04a 100644 +index 3a34b7a2e8d4e..49666e92c04a2 100644 --- a/Documentation/devicetree/bindings/arm/atmel-at91.yaml +++ b/Documentation/devicetree/bindings/arm/atmel-at91.yaml @@ -241,6 +241,12 @@ properties: diff --git a/patches/linux/6.18.55/0070-arm64-dts-microchip-add-Novarq-Tactical-1000.patch b/patches/linux/6.18.55/0070-arm64-dts-microchip-add-Novarq-Tactical-1000.patch index 783c72ff4..3dff6da21 100644 --- a/patches/linux/6.18.55/0070-arm64-dts-microchip-add-Novarq-Tactical-1000.patch +++ b/patches/linux/6.18.55/0070-arm64-dts-microchip-add-Novarq-Tactical-1000.patch @@ -1,7 +1,7 @@ From ded124119dbb2ad94c870b56449f12798043e63f Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Tue, 30 Sep 2025 13:37:49 +0200 -Subject: [PATCH 70/78] arm64: dts: microchip: add Novarq Tactical 1000 +Subject: [PATCH 70/84] arm64: dts: microchip: add Novarq Tactical 1000 Novarq Tactical 1000 is a LAN9696 based switch featuring 24x1G and 4x10G SFP ports. @@ -15,7 +15,7 @@ Signed-off-by: Robert Marko create mode 100644 arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts diff --git a/arch/arm64/boot/dts/microchip/Makefile b/arch/arm64/boot/dts/microchip/Makefile -index 09d16fc1ce9a..0ba970d308ec 100644 +index 09d16fc1ce9ac..0ba970d308ec4 100644 --- a/arch/arm64/boot/dts/microchip/Makefile +++ b/arch/arm64/boot/dts/microchip/Makefile @@ -1,5 +1,6 @@ @@ -27,7 +27,7 @@ index 09d16fc1ce9a..0ba970d308ec 100644 dtb-$(CONFIG_ARCH_SPARX5) += sparx5_pcb135.dtb sparx5_pcb135_emmc.dtb diff --git a/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts b/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts new file mode 100644 -index 000000000000..e9fa26e05804 +index 0000000000000..e9fa26e058047 --- /dev/null +++ b/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts @@ -0,0 +1,886 @@ diff --git a/patches/linux/6.18.55/0071-arm64-dts-microchip-tactical-1000-add-port-names.patch b/patches/linux/6.18.55/0071-arm64-dts-microchip-tactical-1000-add-port-names.patch index 1a8306c2e..79c9c29a8 100644 --- a/patches/linux/6.18.55/0071-arm64-dts-microchip-tactical-1000-add-port-names.patch +++ b/patches/linux/6.18.55/0071-arm64-dts-microchip-tactical-1000-add-port-names.patch @@ -1,7 +1,7 @@ From aa86655cabeb7c303f3acbc59a36754d1b5383c5 Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Tue, 30 Jun 2026 11:23:47 +0200 -Subject: [PATCH 71/78] arm64: dts: microchip: tactical-1000: add port names +Subject: [PATCH 71/84] arm64: dts: microchip: tactical-1000: add port names Now that driver supports parsing the "label" property, populate port names as they are physically wired up. @@ -13,7 +13,7 @@ Signed-off-by: Robert Marko 1 file changed, 29 insertions(+) diff --git a/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts b/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts -index e9fa26e05804..e7e4654af98f 100644 +index e9fa26e058047..e7e4654af98fc 100644 --- a/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts +++ b/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts @@ -626,6 +626,7 @@ port0: port@0 { diff --git a/patches/linux/6.18.55/0072-arm64-dts-microchip-tactical-1000-adapt-to-6.18.patch b/patches/linux/6.18.55/0072-arm64-dts-microchip-tactical-1000-adapt-to-6.18.patch index 7c61c7250..4f6f1f9fc 100644 --- a/patches/linux/6.18.55/0072-arm64-dts-microchip-tactical-1000-adapt-to-6.18.patch +++ b/patches/linux/6.18.55/0072-arm64-dts-microchip-tactical-1000-adapt-to-6.18.patch @@ -1,7 +1,7 @@ From a5fe28d8813c1265151399c88961117e95db9c7d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 20 Sep 2026 12:22:43 +0200 -Subject: [PATCH 72/78] arm64: dts: microchip: tactical-1000: adapt to 6.18 +Subject: [PATCH 72/84] arm64: dts: microchip: tactical-1000: adapt to 6.18 Novarq develop against 7.3, which has three things 6.18 does not: a QSPI controller node, a tmon that also provides the fan PWM, and hence a SoC @@ -20,7 +20,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts b/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts -index e7e4654af98f..bd6953fc9d4a 100644 +index e7e4654af98fc..bd6953fc9d4a3 100644 --- a/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts +++ b/arch/arm64/boot/dts/microchip/lan9696-tactical-1000.dts @@ -5,7 +5,6 @@ diff --git a/patches/linux/6.18.55/0073-dt-bindings-arm-AT91-document-EV23X71A-board.patch b/patches/linux/6.18.55/0073-dt-bindings-arm-AT91-document-EV23X71A-board.patch index 5a53d4404..07546b9fd 100644 --- a/patches/linux/6.18.55/0073-dt-bindings-arm-AT91-document-EV23X71A-board.patch +++ b/patches/linux/6.18.55/0073-dt-bindings-arm-AT91-document-EV23X71A-board.patch @@ -1,7 +1,7 @@ From b390f0eaf27fd05c7a1436f3941964758b7a4fb4 Mon Sep 17 00:00:00 2001 From: Robert Marko Date: Mon, 2 Mar 2026 12:20:13 +0100 -Subject: [PATCH 73/78] dt-bindings: arm: AT91: document EV23X71A board +Subject: [PATCH 73/84] dt-bindings: arm: AT91: document EV23X71A board Microchip EV23X71A board is an LAN9696 based evaluation board. @@ -16,7 +16,7 @@ Signed-off-by: Claudiu Beznea 1 file changed, 6 insertions(+) diff --git a/Documentation/devicetree/bindings/arm/atmel-at91.yaml b/Documentation/devicetree/bindings/arm/atmel-at91.yaml -index 49666e92c04a..b7c37a1cece0 100644 +index 49666e92c04a2..b7c37a1cece03 100644 --- a/Documentation/devicetree/bindings/arm/atmel-at91.yaml +++ b/Documentation/devicetree/bindings/arm/atmel-at91.yaml @@ -241,6 +241,12 @@ properties: diff --git a/patches/linux/6.18.55/0074-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch b/patches/linux/6.18.55/0074-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch index 6395b557c..fc09504fd 100644 --- a/patches/linux/6.18.55/0074-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch +++ b/patches/linux/6.18.55/0074-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch @@ -1,7 +1,7 @@ From 8fc86e86edc2464d419084beb306cb95cf5946fc Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 17:04:21 +0200 -Subject: [PATCH 74/78] net: dsa: Skip DCB default priority init on unsupported +Subject: [PATCH 74/84] net: dsa: Skip DCB default priority init on unsupported switches A driver serving several chip generations has one dsa_switch_ops for @@ -17,7 +17,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/net/dsa/user.c b/net/dsa/user.c -index c0019d0c1172..b881c177628c 100644 +index c0019d0c1172d..b881c177628ce 100644 --- a/net/dsa/user.c +++ b/net/dsa/user.c @@ -2511,12 +2511,13 @@ static int dsa_user_dcbnl_init(struct net_device *dev) diff --git a/patches/linux/6.18.55/0075-net-dsa-Generalise-the-global-DCB-APP-mirroring-help.patch b/patches/linux/6.18.55/0075-net-dsa-Generalise-the-global-DCB-APP-mirroring-help.patch index 9ed0da249..f6f580904 100644 --- a/patches/linux/6.18.55/0075-net-dsa-Generalise-the-global-DCB-APP-mirroring-help.patch +++ b/patches/linux/6.18.55/0075-net-dsa-Generalise-the-global-DCB-APP-mirroring-help.patch @@ -1,7 +1,7 @@ From 3d3f7ba164355546fc2188315622356b4f02f681 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 22 Sep 2026 13:51:07 +0200 -Subject: [PATCH 75/78] net: dsa: Generalise the global DCB APP mirroring +Subject: [PATCH 75/84] net: dsa: Generalise the global DCB APP mirroring helper A switch with one classification table for all its ports programs it @@ -19,7 +19,7 @@ Signed-off-by: Joachim Wiberg 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/net/dsa/user.c b/net/dsa/user.c -index b881c177628c..193e54b7d85e 100644 +index b881c177628ce..193e54b7d85ea 100644 --- a/net/dsa/user.c +++ b/net/dsa/user.c @@ -2243,11 +2243,13 @@ dsa_user_dcbnl_set_default_prio(struct net_device *dev, struct dcb_app *app) diff --git a/patches/linux/6.18.55/0076-net-dsa-Support-the-PCP-APP-selector.patch b/patches/linux/6.18.55/0076-net-dsa-Support-the-PCP-APP-selector.patch index e3d2b348d..4fd9cb7dc 100644 --- a/patches/linux/6.18.55/0076-net-dsa-Support-the-PCP-APP-selector.patch +++ b/patches/linux/6.18.55/0076-net-dsa-Support-the-PCP-APP-selector.patch @@ -1,7 +1,7 @@ From 32c1befbedb0f46361960a0d321c743711a87762 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 17:04:22 +0200 -Subject: [PATCH 76/78] net: dsa: Support the PCP APP selector +Subject: [PATCH 76/84] net: dsa: Support the PCP APP selector Add port_add_pcp_prio, port_del_pcp_prio and port_get_pcp_prio switch ops and route the DCB_APP_SEL_PCP selector to them, mirroring the DSCP @@ -27,7 +27,7 @@ Signed-off-by: Joachim Wiberg 2 files changed, 206 insertions(+) diff --git a/include/net/dsa.h b/include/net/dsa.h -index c31d4e910f07..f271376b7758 100644 +index c31d4e910f075..f271376b77582 100644 --- a/include/net/dsa.h +++ b/include/net/dsa.h @@ -447,6 +447,10 @@ struct dsa_switch { @@ -55,7 +55,7 @@ index c31d4e910f07..f271376b7758 100644 /* * Suspend and resume diff --git a/net/dsa/user.c b/net/dsa/user.c -index 193e54b7d85e..85b06f3a4337 100644 +index 193e54b7d85ea..85b06f3a43370 100644 --- a/net/dsa/user.c +++ b/net/dsa/user.c @@ -2342,6 +2342,172 @@ dsa_user_dcbnl_add_dscp_prio(struct net_device *dev, struct dcb_app *app) diff --git a/patches/linux/6.18.55/0077-net-dsa-Support-DCB-priority-rewrite.patch b/patches/linux/6.18.55/0077-net-dsa-Support-DCB-priority-rewrite.patch index 34fa95eea..e3e632007 100644 --- a/patches/linux/6.18.55/0077-net-dsa-Support-DCB-priority-rewrite.patch +++ b/patches/linux/6.18.55/0077-net-dsa-Support-DCB-priority-rewrite.patch @@ -1,7 +1,7 @@ From b45635ee810e5edadedbc000b737281b2c517695 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 9 Sep 2026 17:10:32 +0200 -Subject: [PATCH 77/78] net: dsa: Support DCB priority rewrite +Subject: [PATCH 77/84] net: dsa: Support DCB priority rewrite The DCB rewrite table maps a priority back to the PCP and DEI, or the DSCP, that frames are remarked with on egress. DSA has no @@ -26,7 +26,7 @@ Signed-off-by: Joachim Wiberg 2 files changed, 279 insertions(+), 1 deletion(-) diff --git a/include/net/dsa.h b/include/net/dsa.h -index f271376b7758..3a36904e57f8 100644 +index f271376b77582..3a36904e57f81 100644 --- a/include/net/dsa.h +++ b/include/net/dsa.h @@ -451,6 +451,12 @@ struct dsa_switch { @@ -60,7 +60,7 @@ index f271376b7758..3a36904e57f8 100644 /* * Suspend and resume diff --git a/net/dsa/user.c b/net/dsa/user.c -index 85b06f3a4337..349bacfebb08 100644 +index 85b06f3a43370..349bacfebb086 100644 --- a/net/dsa/user.c +++ b/net/dsa/user.c @@ -2665,6 +2665,266 @@ static int __maybe_unused dsa_user_dcbnl_ieee_delapp(struct net_device *dev, diff --git a/patches/linux/6.18.55/0078-net-dsa-Support-the-IEEE-ETS-managed-object.patch b/patches/linux/6.18.55/0078-net-dsa-Support-the-IEEE-ETS-managed-object.patch index adc56c667..9ee8936bd 100644 --- a/patches/linux/6.18.55/0078-net-dsa-Support-the-IEEE-ETS-managed-object.patch +++ b/patches/linux/6.18.55/0078-net-dsa-Support-the-IEEE-ETS-managed-object.patch @@ -1,7 +1,7 @@ From 621214da6d9ee1c6ebef311b193dab441c53d359 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 22 Sep 2026 13:53:23 +0200 -Subject: [PATCH 78/78] net: dsa: Support the IEEE ETS managed object +Subject: [PATCH 78/84] net: dsa: Support the IEEE ETS managed object A switch port's transmission selection is configured today through the ets queuing discipline, which numbers its bands the other way round @@ -32,7 +32,7 @@ Signed-off-by: Joachim Wiberg 2 files changed, 82 insertions(+) diff --git a/include/net/dsa.h b/include/net/dsa.h -index 3a36904e57f8..fa47901fa9a9 100644 +index 3a36904e57f81..fa47901fa9a98 100644 --- a/include/net/dsa.h +++ b/include/net/dsa.h @@ -849,6 +849,8 @@ static inline bool dsa_port_tree_same(const struct dsa_port *a, @@ -64,7 +64,7 @@ index 3a36904e57f8..fa47901fa9a9 100644 * Suspend and resume */ diff --git a/net/dsa/user.c b/net/dsa/user.c -index 349bacfebb08..e9809fda8d89 100644 +index 349bacfebb086..e9809fda8d89e 100644 --- a/net/dsa/user.c +++ b/net/dsa/user.c @@ -3040,9 +3040,77 @@ static const struct ethtool_ops dsa_user_ethtool_ops = { diff --git a/patches/linux/6.18.55/0079-net-ethernet-mtk_wed-map-WO-memory-regions-without-r.patch b/patches/linux/6.18.55/0079-net-ethernet-mtk_wed-map-WO-memory-regions-without-r.patch new file mode 100644 index 000000000..06790e47f --- /dev/null +++ b/patches/linux/6.18.55/0079-net-ethernet-mtk_wed-map-WO-memory-regions-without-r.patch @@ -0,0 +1,45 @@ +From 9f01f451cd49c71b37d15e26fd3b4ae6c381a95b Mon Sep 17 00:00:00 2001 +From: Mattias Walström +Date: Mon, 5 Oct 2026 14:37:56 +0200 +Subject: [PATCH 79/84] net: ethernet: mtk_wed: map WO memory regions without + requesting them +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Since the conversion to of_reserved_mem_region_to_resource_byname(), +the WO firmware regions are mapped with devm_ioremap_resource(), which +also requests the region. On MT7986 that request fails and WED never +attaches: + + platform 15010000.wed: error -EBUSY: can't request region for resource [mem 0x4fd00000-0x4fd3ffff] + platform 15010000.wed: failed to attach wed device + +The wo-data region is in addition shared by both WED instances, so a +request-based mapping can never succeed for the second one. Go back +to a plain devm_ioremap(), as the code did before the conversion and +as qcom_wcnss did for the same regression. + +Fixes: e27dba1951ce ("net: Use of_reserved_mem_region_to_resource{_byname}() for "memory-region"") +Signed-off-by: Mattias Walström +--- + drivers/net/ethernet/mediatek/mtk_wed_mcu.c | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/drivers/net/ethernet/mediatek/mtk_wed_mcu.c b/drivers/net/ethernet/mediatek/mtk_wed_mcu.c +index 0d38183c6ba70..be469c753c71b 100644 +--- a/drivers/net/ethernet/mediatek/mtk_wed_mcu.c ++++ b/drivers/net/ethernet/mediatek/mtk_wed_mcu.c +@@ -246,9 +246,9 @@ mtk_wed_get_memory_region(struct mtk_wed_hw *hw, const char *name, + + region->phy_addr = res.start; + region->size = resource_size(&res); +- region->addr = devm_ioremap_resource(hw->dev, &res); +- if (IS_ERR(region->addr)) +- return PTR_ERR(region->addr); ++ region->addr = devm_ioremap(hw->dev, region->phy_addr, region->size); ++ if (!region->addr) ++ return -ENOMEM; + + return 0; + } diff --git a/patches/linux/6.18.55/0080-wifi-mt76-wed-fix-kernel-panic-on-non-DBDC-MT7986.patch b/patches/linux/6.18.55/0080-wifi-mt76-wed-fix-kernel-panic-on-non-DBDC-MT7986.patch new file mode 100644 index 000000000..30cabae30 --- /dev/null +++ b/patches/linux/6.18.55/0080-wifi-mt76-wed-fix-kernel-panic-on-non-DBDC-MT7986.patch @@ -0,0 +1,111 @@ +From a18588e61131b3562a778c02afd8f193c172d727 Mon Sep 17 00:00:00 2001 +From: Zhi-Jun You +Date: Wed, 15 Jul 2026 23:21:12 +0800 +Subject: [PATCH 80/84] wifi: mt76: wed: fix kernel panic on non-DBDC MT7986 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +In mt76_wed_init_rx_buf, it's hardcoded to use MT_RXQ_MAIN. +But for non-DBDC MT7986 MT_RXQ_BAND1 is used for RX data queue which +leads to kernel panic when attaching WED. + +Use the correct RX queue by checking WED version and band_idx. + +v2 and band 1 -> MT_RXQ_BAND1 +Others -> MT_RXQ_MAIN + +Kernel panic: + +Unable to handle kernel access to user memory outside uaccess routines at virtual address 0000000000000000 +Mem abort info: + ESR = 0x0000000096000005 + EC = 0x25: DABT (current EL), IL = 32 bits + SET = 0, FnV = 0 + EA = 0, S1PTW = 0 + FSC = 0x05: level 1 translation fault +Data abort info: + ISV = 0, ISS = 0x00000005, ISS2 = 0x00000000 + CM = 0, WnR = 0, TnD = 0, TagAccess = 0 + GCS = 0, Overlay = 0, DirtyBit = 0, Xs = 0 +Internal error: Oops: 0000000096000005 [#1] SMP +CPU: 1 UID: 0 PID: 925 Comm: kmodloader Tainted: G O 6.18.26 #0 NONE +Tainted: [O]=OOT_MODULE +Hardware name: Acer Connect Vero W6m (DT) +pstate: 40400005 (nZcv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--) +pc : page_pool_alloc_frag_netmem+0x1c/0x1bc +lr : page_pool_alloc_frag+0xc/0x34 +sp : ffffffc081dab660 +x29: ffffffc081dab660 x28: ffffffc081dabc60 x27: ffffff80091af040 +x26: 0000008000000000 x25: ffffff80091a8898 x24: ffffff80091a5440 +x23: 0000000000001000 x22: 0000000140000000 x21: ffffff80091a2040 +x20: ffffff8003f1d780 x19: 0000000000000000 x18: 0000000000000020 +x17: ffffffbfbf0ac000 x16: ffffffc080ee0000 x15: ffffff80049d47ca +x14: 000000000000037b x13: 000000000000037b x12: 0000000000000001 +x11: 0000000000000000 x10: 0000000000000000 x9 : 0000000000000000 +x8 : ffffff8003f1d7c0 x7 : 0000000000000000 x6 : ffffff8003f1d780 +x5 : 0000000000000680 x4 : 0000000000000000 x3 : 0000000000002824 +x2 : 0000000000000000 x1 : ffffffc081dab71c x0 : 0000000000000000 +Call trace: + page_pool_alloc_frag_netmem+0x1c/0x1bc (P) + page_pool_alloc_frag+0xc/0x34 + mt76_wed_init_rx_buf+0xf8/0x2ac [mt76] + mtk_wed_start+0x79c/0x12ac + mt7915_dma_start+0x274/0x63c [mt7915e] + mt7915_dma_start+0x5b4/0x63c [mt7915e] + mt7915_dma_init+0x49c/0x81c [mt7915e] + mt7915_register_device+0x24c/0x530 [mt7915e] + mt7915_mmio_probe+0x91c/0x1980 [mt7915e] + platform_probe+0x58/0xa0 + really_probe+0xb8/0x2a8 + __driver_probe_device+0x74/0x118 + driver_probe_device+0x3c/0xe0 + __driver_attach+0x88/0x154 + bus_for_each_dev+0x60/0xb0 + driver_attach+0x20/0x28 + bus_add_driver+0xdc/0x200 + driver_register+0x64/0x118 + __platform_driver_register+0x20/0x30 + init_module+0x74/0x1000 [mt7915e] + do_one_initcall+0x4c/0x1f8 + do_init_module+0x50/0x210 + load_module+0x15f8/0x1b10 + __do_sys_init_module+0x1a8/0x260 + __arm64_sys_init_module+0x18/0x20 + invoke_syscall.constprop.0+0x4c/0xd0 + do_el0_svc+0x3c/0xd0 + el0_svc+0x18/0x60 + el0t_64_sync_handler+0x98/0xdc + el0t_64_sync+0x158/0x15c +Code: aa0003f3 a9025bf5 a90363f7 d2820017 (b9400000) + +Signed-off-by: Zhi-Jun You +Link: https://patch.msgid.link/20260715152113.553-1-hujy652@gmail.com +Signed-off-by: Felix Fietkau +(cherry picked from commit 8a177dabeeead47dda4d4f91331282790eed0097) +Signed-off-by: Mattias Walström +--- + drivers/net/wireless/mediatek/mt76/wed.c | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/drivers/net/wireless/mediatek/mt76/wed.c b/drivers/net/wireless/mediatek/mt76/wed.c +index fbd7e59c73aaf..2ebf4edc689e9 100644 +--- a/drivers/net/wireless/mediatek/mt76/wed.c ++++ b/drivers/net/wireless/mediatek/mt76/wed.c +@@ -33,10 +33,15 @@ u32 mt76_wed_init_rx_buf(struct mtk_wed_device *wed, int size) + { + struct mtk_wed_bm_desc *desc = wed->rx_buf_ring.desc; + struct mt76_dev *dev = mt76_wed_to_dev(wed); +- struct mt76_queue *q = &dev->q_rx[MT_RXQ_MAIN]; + struct mt76_txwi_cache *t = NULL; ++ struct mt76_queue *q; + int i; + ++ if (wed->version == 2 && dev->phy.band_idx) ++ q = &dev->q_rx[MT_RXQ_BAND1]; ++ else ++ q = &dev->q_rx[MT_RXQ_MAIN]; ++ + for (i = 0; i < size; i++) { + dma_addr_t addr; + u32 offset; diff --git a/patches/linux/6.18.55/0081-wifi-mt76-mt7915-set-up-WPDMA-RX-ring-0-for-WED-on-a.patch b/patches/linux/6.18.55/0081-wifi-mt76-mt7915-set-up-WPDMA-RX-ring-0-for-WED-on-a.patch new file mode 100644 index 000000000..2001630ad --- /dev/null +++ b/patches/linux/6.18.55/0081-wifi-mt76-mt7915-set-up-WPDMA-RX-ring-0-for-WED-on-a.patch @@ -0,0 +1,44 @@ +From bdd10e66dc3ed278f607ee818a8cd807e23775ed Mon Sep 17 00:00:00 2001 +From: Mattias Walström +Date: Wed, 7 Oct 2026 22:20:00 +0200 +Subject: [PATCH 81/84] wifi: mt76: mt7915: set up WPDMA RX ring 0 for WED on a + band1-only device +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +On a single-band MT7986 wmac bound to band 1, next to a PCIe card for +the other bands, the band0 RX data queue is never allocated, so the +WED is attached with WPDMA RX ring 0 unconfigured while it drives ring +0 and 1 alike. On a BPI-R3 that ends in a stall of ring 1 after some +thousand frames: no descriptor armed, the WED's index two ahead of the +WPDMA's, the MAC counting RX FIFO full, and clients associating without +any data getting through. Allocate the band0 queue when WED does RX, +so the WED finds a valid, idle ring 0. + +Signed-off-by: Mattias Walström +--- + drivers/net/wireless/mediatek/mt76/mt7915/dma.c | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/dma.c b/drivers/net/wireless/mediatek/mt76/mt7915/dma.c +index 009ef713f4379..851931ae55530 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7915/dma.c ++++ b/drivers/net/wireless/mediatek/mt76/mt7915/dma.c +@@ -506,8 +506,14 @@ int mt7915_dma_init(struct mt7915_dev *dev, struct mt7915_phy *phy2) + if (ret) + return ret; + +- /* rx data queue for band0 */ +- if (!dev->phy.mt76->band_idx) { ++ /* rx data queue for band0. Also on a device whose only phy is ++ * band1 when WED does RX: the WED drives WPDMA RX ring 0 and 1 ++ * alike, and with ring 0 never set up it stalls on ring 1 after ++ * a while, the ring sits with no descriptor armed and the MAC ++ * counts RX FIFO full. Give it an empty but valid ring 0. */ ++ if (!dev->phy.mt76->band_idx || ++ (mtk_wed_device_active(&mdev->mmio.wed) && ++ mtk_wed_get_rx_capa(&mdev->mmio.wed))) { + if (mtk_wed_device_active(&mdev->mmio.wed) && + mtk_wed_get_rx_capa(&mdev->mmio.wed)) { + mdev->q_rx[MT_RXQ_MAIN].flags = diff --git a/patches/linux/6.18.55/0082-wifi-mt76-mt7915-add-WDS-support-when-WED-is-enabled.patch b/patches/linux/6.18.55/0082-wifi-mt76-mt7915-add-WDS-support-when-WED-is-enabled.patch new file mode 100644 index 000000000..002aa2024 --- /dev/null +++ b/patches/linux/6.18.55/0082-wifi-mt76-mt7915-add-WDS-support-when-WED-is-enabled.patch @@ -0,0 +1,431 @@ +From dd9214a3a26dc3854852cdf4ae84706174dc1a36 Mon Sep 17 00:00:00 2001 +From: Felix Fietkau +Date: Mon, 20 Oct 2025 15:59:12 +0200 +Subject: [PATCH 82/84] wifi: mt76: mt7915: add WDS support when WED is enabled + +The current WED only supports 256 wcid, whereas mt7986 can support up to +512 entries, so firmware provides a rule to get sta_info by DA when wcid +is set to 0x3ff by txd. Also, WED provides a register to overwrite txd +wcid, that is, wcid[9:8] can be overwritten by 0x3 and wcid[7:0] is set +to 0xff by host driver. + +However, firmware is unable to get sta_info from DA as DA != RA for +4addr cases, so firmware and wifi host driver both use wcid (256 - 271) +and (768 ~ 783) for sync up to get correct sta_info. + +Currently WDS+WED config is completely broken on MT7986/7981 devices if +without this patch. + +Signed-off-by: Bo Jiao +Signed-off-by: Sujuan Chen +Signed-off-by: Shengyu Qu +Signed-off-by: Felix Fietkau +--- + drivers/net/wireless/mediatek/mt76/mt76.h | 3 + + .../wireless/mediatek/mt76/mt7915/debugfs.c | 2 +- + .../net/wireless/mediatek/mt76/mt7915/mac.c | 2 +- + .../net/wireless/mediatek/mt76/mt7915/main.c | 56 +++++++++++++++++-- + .../net/wireless/mediatek/mt76/mt7915/mcu.c | 55 +++++++++++------- + .../net/wireless/mediatek/mt76/mt7915/mcu.h | 1 + + .../wireless/mediatek/mt76/mt7915/mt7915.h | 16 +++++- + drivers/net/wireless/mediatek/mt76/util.c | 13 ++++- + drivers/net/wireless/mediatek/mt76/util.h | 7 ++- + 9 files changed, 122 insertions(+), 33 deletions(-) + +diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h +index 2a186b47659b6..9cb7e2da472fc 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt76.h ++++ b/drivers/net/wireless/mediatek/mt76/mt76.h +@@ -29,6 +29,9 @@ + + #define MT76_TOKEN_FREE_THR 64 + ++#define MT76_WED_WDS_MIN 256 ++#define MT76_WED_WDS_MAX 272 ++ + #define MT_QFLAG_WED_RING GENMASK(1, 0) + #define MT_QFLAG_WED_TYPE GENMASK(4, 2) + #define MT_QFLAG_WED BIT(5) +diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/debugfs.c b/drivers/net/wireless/mediatek/mt76/mt7915/debugfs.c +index b287b7d9394e2..ec2287459587e 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7915/debugfs.c ++++ b/drivers/net/wireless/mediatek/mt76/mt7915/debugfs.c +@@ -1385,7 +1385,7 @@ static ssize_t mt7915_sta_fixed_rate_set(struct file *file, + + out: + vif = container_of((void *)msta->vif, struct ieee80211_vif, drv_priv); +- ret = mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, &phy, field); ++ ret = mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, &msta->wcid, &phy, field); + if (ret) + return -EFAULT; + +diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/mac.c b/drivers/net/wireless/mediatek/mt76/mt7915/mac.c +index dda4e7d179536..b431467c0eeee 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7915/mac.c ++++ b/drivers/net/wireless/mediatek/mt76/mt7915/mac.c +@@ -1972,7 +1972,7 @@ void mt7915_mac_sta_rc_work(struct work_struct *work) + if (changed & (IEEE80211_RC_SUPP_RATES_CHANGED | + IEEE80211_RC_NSS_CHANGED | + IEEE80211_RC_BW_CHANGED)) +- mt7915_mcu_add_rate_ctrl(dev, vif, sta, true); ++ mt7915_mcu_add_rate_ctrl(dev, vif, sta, &msta->wcid, true); + + if (changed & IEEE80211_RC_SMPS_CHANGED) + mt7915_mcu_add_smps(dev, vif, sta); +diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/main.c b/drivers/net/wireless/mediatek/mt76/mt7915/main.c +index 9d546eb8022ac..eb2dc410bba25 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7915/main.c ++++ b/drivers/net/wireless/mediatek/mt76/mt7915/main.c +@@ -758,7 +758,14 @@ int mt7915_mac_sta_add(struct mt76_dev *mdev, struct ieee80211_vif *vif, + bool ext_phy = mvif->phy != &dev->phy; + int idx; + +- idx = mt76_wcid_alloc(dev->mt76.wcid_mask, MT7915_WTBL_STA); ++ if (mtk_wed_device_active(&dev->mt76.mmio.wed) && ++ !is_mt7915(&dev->mt76) && ++ test_bit(MT_WCID_FLAG_4ADDR, &msta->wcid.flags)) ++ idx = __mt76_wcid_alloc(mdev->wcid_mask, MT76_WED_WDS_MIN, ++ MT76_WED_WDS_MAX); ++ else ++ idx = mt76_wcid_alloc(mdev->wcid_mask, MT7915_WTBL_STA); ++ + if (idx < 0) + return -ENOSPC; + +@@ -847,7 +854,7 @@ int mt7915_mac_sta_event(struct mt76_dev *mdev, struct ieee80211_vif *vif, + addr = mt7915_mac_wtbl_lmac_addr(dev, msta->wcid.idx, 30); + mt76_rmw_field(dev, addr, GENMASK(7, 0), 0xa0); + +- ret = mt7915_mcu_add_rate_ctrl(dev, vif, sta, false); ++ ret = mt7915_mcu_add_rate_ctrl(dev, vif, sta, &msta->wcid, false); + if (ret) + return ret; + +@@ -1278,6 +1285,40 @@ mt7915_set_bitrate_mask(struct ieee80211_hw *hw, struct ieee80211_vif *vif, + return 0; + } + ++static void mt7915_sta_wed_set_4addr(struct mt7915_dev *dev, struct ieee80211_vif *vif, ++ struct ieee80211_sta *sta) ++{ ++ struct mt7915_sta *msta = (struct mt7915_sta *)sta->drv_priv; ++ int min = MT76_WED_WDS_MIN, max = MT76_WED_WDS_MAX; ++ int idx, prev_idx = msta->wcid.idx; ++ struct mt76_wcid wcid = msta->wcid; ++ int state; ++ ++ if (!is_mt7915(&dev->mt76)) ++ return; ++ ++ if (msta->wcid.idx >= min && msta->wcid.idx < max) ++ return; ++ ++ idx = __mt76_wcid_alloc(dev->mt76.wcid_mask, min, max); ++ if (idx < 0) ++ return; ++ ++ wcid.idx = idx; ++ state = msta->wcid.sta ? CONN_STATE_PORT_SECURE : CONN_STATE_DISCONNECT; ++ __mt7915_mcu_add_sta(dev, vif, sta, &wcid, state, true); ++ mt7915_mcu_add_rate_ctrl(dev, vif, sta, &wcid, false); ++ rcu_assign_pointer(dev->mt76.wcid[idx], &msta->wcid); ++ msta->wcid.idx = idx; ++ ++ synchronize_rcu(); ++ ++ rcu_assign_pointer(dev->mt76.wcid[prev_idx], NULL); ++ mt76_wcid_mask_clear(dev->mt76.wcid_mask, prev_idx); ++ wcid.idx = prev_idx; ++ __mt7915_mcu_add_sta(dev, vif, sta, &wcid, CONN_STATE_DISCONNECT, false); ++} ++ + static void mt7915_sta_set_4addr(struct ieee80211_hw *hw, + struct ieee80211_vif *vif, + struct ieee80211_sta *sta, +@@ -1291,6 +1332,9 @@ static void mt7915_sta_set_4addr(struct ieee80211_hw *hw, + else + clear_bit(MT_WCID_FLAG_4ADDR, &msta->wcid.flags); + ++ if (mtk_wed_device_active(&dev->mt76.mmio.wed) && enabled) ++ mt7915_sta_wed_set_4addr(dev, vif, sta); ++ + if (!msta->wcid.sta) + return; + +@@ -1740,15 +1784,19 @@ mt7915_net_fill_forward_path(struct ieee80211_hw *hw, + if (!mtk_wed_device_active(wed)) + return -ENODEV; + +- if (msta->wcid.idx > 0xff) ++ if (msta->wcid.idx > MT7915_WTBL_STA) + return -EIO; + + path->type = DEV_PATH_MTK_WDMA; + path->dev = ctx->dev; + path->mtk_wdma.wdma_idx = wed->wdma_idx; + path->mtk_wdma.bss = mvif->mt76.idx; +- path->mtk_wdma.wcid = is_mt7915(&dev->mt76) ? msta->wcid.idx : 0x3ff; + path->mtk_wdma.queue = phy->mt76->band_idx; ++ if (test_bit(MT_WCID_FLAG_4ADDR, &msta->wcid.flags) || ++ is_mt7915(&dev->mt76)) ++ path->mtk_wdma.wcid = msta->wcid.idx; ++ else ++ path->mtk_wdma.wcid = 0x3ff; + + ctx->dev = NULL; + +diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c +index 29b3f0cda1ce7..1ecce494bb09c 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c ++++ b/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c +@@ -1367,6 +1367,7 @@ mt7915_mcu_get_mmps_mode(enum ieee80211_smps_mode smps) + int mt7915_mcu_set_fixed_rate_ctrl(struct mt7915_dev *dev, + struct ieee80211_vif *vif, + struct ieee80211_sta *sta, ++ struct mt76_wcid *wcid, + void *data, u32 field) + { + struct mt7915_vif *mvif = (struct mt7915_vif *)vif->drv_priv; +@@ -1438,7 +1439,7 @@ int mt7915_mcu_add_smps(struct mt7915_dev *dev, struct ieee80211_vif *vif, + if (ret) + return ret; + +- return mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, NULL, ++ return mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, &msta->wcid, NULL, + RATE_PARAM_MMPS_UPDATE); + } + +@@ -1447,17 +1448,19 @@ mt7915_mcu_set_spe_idx(struct mt7915_dev *dev, struct ieee80211_vif *vif, + struct ieee80211_sta *sta) + { + struct mt7915_vif *mvif = (struct mt7915_vif *)vif->drv_priv; ++ struct mt7915_sta *msta = (struct mt7915_sta *)sta->drv_priv; + struct mt76_phy *mphy = mvif->phy->mt76; + u8 spe_idx = mt76_connac_spe_idx(mphy->antenna_mask); + +- return mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, &spe_idx, +- RATE_PARAM_SPE_UPDATE); ++ return mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, &msta->wcid, ++ &spe_idx, RATE_PARAM_SPE_UPDATE); + } + + static int + mt7915_mcu_add_rate_ctrl_fixed(struct mt7915_dev *dev, + struct ieee80211_vif *vif, +- struct ieee80211_sta *sta) ++ struct ieee80211_sta *sta, ++ struct mt76_wcid *wcid) + { + struct mt7915_vif *mvif = (struct mt7915_vif *)vif->drv_priv; + struct cfg80211_chan_def *chandef = &mvif->phy->mt76->chandef; +@@ -1505,7 +1508,7 @@ mt7915_mcu_add_rate_ctrl_fixed(struct mt7915_dev *dev, + + /* fixed single rate */ + if (nrates == 1) { +- ret = mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, &phy, ++ ret = mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, wcid, &phy, + RATE_PARAM_FIXED_MCS); + if (ret) + return ret; +@@ -1527,7 +1530,7 @@ mt7915_mcu_add_rate_ctrl_fixed(struct mt7915_dev *dev, + else + mt76_rmw_field(dev, addr, GENMASK(15, 12), phy.sgi); + +- ret = mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, &phy, ++ ret = mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, wcid, &phy, + RATE_PARAM_FIXED_GI); + if (ret) + return ret; +@@ -1535,7 +1538,7 @@ mt7915_mcu_add_rate_ctrl_fixed(struct mt7915_dev *dev, + + /* fixed HE_LTF */ + if (mask->control[band].he_ltf != GENMASK(7, 0)) { +- ret = mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, &phy, ++ ret = mt7915_mcu_set_fixed_rate_ctrl(dev, vif, sta, wcid, &phy, + RATE_PARAM_FIXED_HE_LTF); + if (ret) + return ret; +@@ -1648,15 +1651,14 @@ mt7915_mcu_sta_rate_ctrl_tlv(struct sk_buff *skb, struct mt7915_dev *dev, + } + + int mt7915_mcu_add_rate_ctrl(struct mt7915_dev *dev, struct ieee80211_vif *vif, +- struct ieee80211_sta *sta, bool changed) ++ struct ieee80211_sta *sta, struct mt76_wcid *wcid, ++ bool changed) + { + struct mt7915_vif *mvif = (struct mt7915_vif *)vif->drv_priv; +- struct mt7915_sta *msta = (struct mt7915_sta *)sta->drv_priv; + struct sk_buff *skb; + int ret; + +- skb = mt76_connac_mcu_alloc_sta_req(&dev->mt76, &mvif->mt76, +- &msta->wcid); ++ skb = mt76_connac_mcu_alloc_sta_req(&dev->mt76, &mvif->mt76, wcid); + if (IS_ERR(skb)) + return PTR_ERR(skb); + +@@ -1681,7 +1683,7 @@ int mt7915_mcu_add_rate_ctrl(struct mt7915_dev *dev, struct ieee80211_vif *vif, + * and updates as peer fixed rate parameters, which overrides + * sta_rec_ra and firmware rate control algorithm. + */ +- return mt7915_mcu_add_rate_ctrl_fixed(dev, vif, sta); ++ return mt7915_mcu_add_rate_ctrl_fixed(dev, vif, sta, wcid); + } + + static int +@@ -1712,8 +1714,9 @@ mt7915_mcu_add_group(struct mt7915_dev *dev, struct ieee80211_vif *vif, + sizeof(req), true); + } + +-int mt7915_mcu_add_sta(struct mt7915_dev *dev, struct ieee80211_vif *vif, +- struct ieee80211_sta *sta, int conn_state, bool newly) ++int __mt7915_mcu_add_sta(struct mt7915_dev *dev, struct ieee80211_vif *vif, ++ struct ieee80211_sta *sta, struct mt76_wcid *wcid, ++ int conn_state, bool newly) + { + struct mt7915_vif *mvif = (struct mt7915_vif *)vif->drv_priv; + struct ieee80211_link_sta *link_sta; +@@ -1724,8 +1727,10 @@ int mt7915_mcu_add_sta(struct mt7915_dev *dev, struct ieee80211_vif *vif, + msta = sta ? (struct mt7915_sta *)sta->drv_priv : &mvif->sta; + link_sta = sta ? &sta->deflink : NULL; + +- skb = mt76_connac_mcu_alloc_sta_req(&dev->mt76, &mvif->mt76, +- &msta->wcid); ++ if (!wcid) ++ wcid = &msta->wcid; ++ ++ skb = mt76_connac_mcu_alloc_sta_req(&dev->mt76, &mvif->mt76, wcid); + if (IS_ERR(skb)) + return PTR_ERR(skb); + +@@ -2423,10 +2428,20 @@ int mt7915_mcu_init_firmware(struct mt7915_dev *dev) + + mt76_connac_mcu_del_wtbl_all(&dev->mt76); + +- if ((mtk_wed_device_active(&dev->mt76.mmio.wed) && +- is_mt7915(&dev->mt76)) || +- !mtk_wed_get_rx_capa(&dev->mt76.mmio.wed)) +- mt7915_mcu_wa_cmd(dev, MCU_WA_PARAM_CMD(CAPABILITY), 0, 0, 0); ++#if IS_ENABLED(CONFIG_NET_MEDIATEK_SOC_WED) ++ if (mtk_wed_device_active(&dev->mt76.mmio.wed)) { ++ if (is_mt7915(&dev->mt76) || ++ !mtk_wed_get_rx_capa(&dev->mt76.mmio.wed)) ++ ret = mt7915_mcu_wa_cmd(dev, MCU_WA_PARAM_CMD(CAPABILITY), ++ 0, 0, 0); ++ else ++ ret = mt7915_mcu_wa_cmd(dev, MCU_WA_PARAM_CMD(SET), ++ MCU_WA_PARAM_WED_VERSION, ++ dev->mt76.mmio.wed.rev_id, 0); ++ if (ret) ++ return ret; ++ } ++#endif + + ret = mt7915_mcu_set_mwds(dev, 1); + if (ret) +diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7915/mcu.h +index 4c25abb0297de..6ffa5ea5bc0d6 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7915/mcu.h ++++ b/drivers/net/wireless/mediatek/mt76/mt7915/mcu.h +@@ -278,6 +278,7 @@ enum { + MCU_WA_PARAM_PDMA_RX = 0x04, + MCU_WA_PARAM_CPU_UTIL = 0x0b, + MCU_WA_PARAM_RED = 0x0e, ++ MCU_WA_PARAM_WED_VERSION = 0x32, + MCU_WA_PARAM_RED_SETTING = 0x40, + }; + +diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h b/drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h +index 27857347f05f4..cc56ef27308c0 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h ++++ b/drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h +@@ -475,8 +475,16 @@ int mt7915_mcu_add_dev_info(struct mt7915_phy *phy, + struct ieee80211_vif *vif, bool enable); + int mt7915_mcu_add_bss_info(struct mt7915_phy *phy, + struct ieee80211_vif *vif, int enable); +-int mt7915_mcu_add_sta(struct mt7915_dev *dev, struct ieee80211_vif *vif, +- struct ieee80211_sta *sta, int conn_state, bool newly); ++int __mt7915_mcu_add_sta(struct mt7915_dev *dev, struct ieee80211_vif *vif, ++ struct ieee80211_sta *sta, struct mt76_wcid *wcid, ++ int conn_state, bool newly); ++static inline int ++mt7915_mcu_add_sta(struct mt7915_dev *dev, struct ieee80211_vif *vif, ++ struct ieee80211_sta *sta, int conn_state, bool newly) ++{ ++ return __mt7915_mcu_add_sta(dev, vif, sta, NULL, conn_state, newly); ++} ++ + int mt7915_mcu_add_tx_ba(struct mt7915_dev *dev, + struct ieee80211_ampdu_params *params, + bool add); +@@ -494,7 +502,8 @@ int mt7915_mcu_set_protection(struct mt7915_phy *phy, struct ieee80211_vif *vif, + int mt7915_mcu_add_obss_spr(struct mt7915_phy *phy, struct ieee80211_vif *vif, + struct ieee80211_he_obss_pd *he_obss_pd); + int mt7915_mcu_add_rate_ctrl(struct mt7915_dev *dev, struct ieee80211_vif *vif, +- struct ieee80211_sta *sta, bool changed); ++ struct ieee80211_sta *sta, struct mt76_wcid *wcid, ++ bool changed); + int mt7915_mcu_add_smps(struct mt7915_dev *dev, struct ieee80211_vif *vif, + struct ieee80211_sta *sta); + int mt7915_set_channel(struct mt76_phy *mphy); +@@ -504,6 +513,7 @@ int mt7915_mcu_update_edca(struct mt7915_dev *dev, void *req); + int mt7915_mcu_set_fixed_rate_ctrl(struct mt7915_dev *dev, + struct ieee80211_vif *vif, + struct ieee80211_sta *sta, ++ struct mt76_wcid *wcid, + void *data, u32 field); + int mt7915_mcu_set_eeprom(struct mt7915_dev *dev); + int mt7915_mcu_get_eeprom(struct mt7915_dev *dev, u32 offset, u8 *read_buf); +diff --git a/drivers/net/wireless/mediatek/mt76/util.c b/drivers/net/wireless/mediatek/mt76/util.c +index 97249ebb4bc8f..6ac7cfdd8786f 100644 +--- a/drivers/net/wireless/mediatek/mt76/util.c ++++ b/drivers/net/wireless/mediatek/mt76/util.c +@@ -42,12 +42,19 @@ bool ____mt76_poll_msec(struct mt76_dev *dev, u32 offset, u32 mask, u32 val, + } + EXPORT_SYMBOL_GPL(____mt76_poll_msec); + +-int mt76_wcid_alloc(u32 *mask, int size) ++int __mt76_wcid_alloc(u32 *mask, int min, int size) + { ++ u32 min_mask = ~0; + int i, idx = 0, cur; + ++ mask += min / 32; ++ min %= 32; ++ if (min > 0) ++ min_mask = ~((1 << min) - 1); ++ + for (i = 0; i < DIV_ROUND_UP(size, 32); i++) { +- idx = ffs(~mask[i]); ++ idx = ffs(~mask[i] & min_mask); ++ min_mask = ~0; + if (!idx) + continue; + +@@ -62,7 +69,7 @@ int mt76_wcid_alloc(u32 *mask, int size) + + return -1; + } +-EXPORT_SYMBOL_GPL(mt76_wcid_alloc); ++EXPORT_SYMBOL_GPL(__mt76_wcid_alloc); + + int mt76_get_min_avg_rssi(struct mt76_dev *dev, u8 phy_idx) + { +diff --git a/drivers/net/wireless/mediatek/mt76/util.h b/drivers/net/wireless/mediatek/mt76/util.h +index 260965dde94cf..68432fce2f56a 100644 +--- a/drivers/net/wireless/mediatek/mt76/util.h ++++ b/drivers/net/wireless/mediatek/mt76/util.h +@@ -27,7 +27,12 @@ enum { + #define MT76_INCR(_var, _size) \ + (_var = (((_var) + 1) % (_size))) + +-int mt76_wcid_alloc(u32 *mask, int size); ++int __mt76_wcid_alloc(u32 *mask, int min, int size); ++ ++static inline int mt76_wcid_alloc(u32 *mask, int size) ++{ ++ return __mt76_wcid_alloc(mask, 0, size); ++} + + static inline void + mt76_wcid_mask_set(u32 *mask, int idx) diff --git a/patches/linux/6.18.55/0083-net-ethernet-mtk_wed-increase-WED-v2-WDMA-RESV_BUFF-.patch b/patches/linux/6.18.55/0083-net-ethernet-mtk_wed-increase-WED-v2-WDMA-RESV_BUFF-.patch new file mode 100644 index 000000000..35f803a10 --- /dev/null +++ b/patches/linux/6.18.55/0083-net-ethernet-mtk_wed-increase-WED-v2-WDMA-RESV_BUFF-.patch @@ -0,0 +1,49 @@ +From 9671f1849832c44b0e306a1a5e2fedb7157424d1 Mon Sep 17 00:00:00 2001 +From: Shiji Yang +Date: Wed, 19 Aug 2026 21:07:07 +0800 +Subject: [PATCH 83/84] net: ethernet: mtk_wed: increase WED v2 WDMA RESV_BUFF + to 0x80 + +Change WDMA RESV_BUFF from 0x40 to 0x80 to avoid CDM TX FIFO overflow. +Without this patch mt7986 and mt7981 may have WDMA TX hang issue. This +patch was pulled from mtk-openwrt-feeds GPL open source project. + +Link: https://github.com/mediatek/mtk-openwrt-feeds/commit/07c87502e854b68b48544d101b6fe17ec059b97b +Signed-off-by: Shiji Yang +Reviewed-by: Simon Horman +Acked-by: Lorenzo Bianconi +Link: https://patch.msgid.link/OSZPR01MB779537889255E2F606E47EABBCA52@OSZPR01MB7795.jpnprd01.prod.outlook.com +Signed-off-by: Jakub Kicinski +--- + drivers/net/ethernet/mediatek/mtk_wed.c | 5 +++++ + drivers/net/ethernet/mediatek/mtk_wed_regs.h | 1 + + 2 files changed, 6 insertions(+) + +diff --git a/drivers/net/ethernet/mediatek/mtk_wed.c b/drivers/net/ethernet/mediatek/mtk_wed.c +index 1ed1f88dd7f8b..fdb487e5d46e1 100644 +--- a/drivers/net/ethernet/mediatek/mtk_wed.c ++++ b/drivers/net/ethernet/mediatek/mtk_wed.c +@@ -2071,6 +2071,11 @@ mtk_wed_dma_enable(struct mtk_wed_device *dev) + wdma_set(dev, MTK_WDMA_GLO_CFG, MTK_WDMA_GLO_CFG_TX_DMA_EN); + } + ++ if (mtk_wed_is_v2(dev->hw)) ++ wdma_m32(dev, MTK_WDMA_GLO_CFG, ++ MTK_WDMA_GLO_CFG_RESV_BUFF, ++ FIELD_PREP(MTK_WDMA_GLO_CFG_RESV_BUFF, 0x80)); ++ + wed_set(dev, MTK_WED_GLO_CFG, + MTK_WED_GLO_CFG_TX_DMA_EN | + MTK_WED_GLO_CFG_RX_DMA_EN); +diff --git a/drivers/net/ethernet/mediatek/mtk_wed_regs.h b/drivers/net/ethernet/mediatek/mtk_wed_regs.h +index c71190924816c..e5f83100ded52 100644 +--- a/drivers/net/ethernet/mediatek/mtk_wed_regs.h ++++ b/drivers/net/ethernet/mediatek/mtk_wed_regs.h +@@ -433,6 +433,7 @@ struct mtk_wdma_desc { + #define MTK_WDMA_GLO_CFG_TX_DMA_BUSY BIT(1) + #define MTK_WDMA_GLO_CFG_RX_DMA_EN BIT(2) + #define MTK_WDMA_GLO_CFG_RX_DMA_BUSY BIT(3) ++#define MTK_WDMA_GLO_CFG_RESV_BUFF GENMASK(23, 16) + #define MTK_WDMA_GLO_CFG_RX_INFO3_PRERES BIT(26) + #define MTK_WDMA_GLO_CFG_RX_INFO2_PRERES BIT(27) + #define MTK_WDMA_GLO_CFG_RX_INFO1_PRERES BIT(28) diff --git a/patches/linux/6.18.55/0084-wifi-mt76-mt7915-publish-wcid-before-MCU-add-command.patch b/patches/linux/6.18.55/0084-wifi-mt76-mt7915-publish-wcid-before-MCU-add-command.patch new file mode 100644 index 000000000..b86e38f5a --- /dev/null +++ b/patches/linux/6.18.55/0084-wifi-mt76-mt7915-publish-wcid-before-MCU-add-command.patch @@ -0,0 +1,54 @@ +From cfd8b803f7a4f8281d10f1f7121ff0fcebbda5b7 Mon Sep 17 00:00:00 2001 +From: Ryan Leung +Date: Fri, 21 Aug 2026 09:00:01 +0000 +Subject: [PATCH 84/84] wifi: mt76: mt7915: publish wcid before MCU add + commands + +mt7915_add_interface() enables the BSS/STA in firmware via +mt7915_mcu_add_bss_info() and mt7915_mcu_add_sta() before publishing +dev->mt76.wcid[idx] with rcu_assign_pointer(). Firmware can start +generating tx-status/tx-free events referencing that wcid as soon as it +processes those MCU commands, but mt76's rx/tx-free handlers (e.g. +mt7915_mac_tx_free()) look up dev->mt76.wcid[idx] to service them, and +won't find it published yet. This can lead to memory corruption and +kernel crashes shortly after an AP interface is brought up. + +This ordering was introduced by commit 8e3e7567b8c1 ("mt76: mt7915: add +sta_rec with EXTRA_INFO_NEW for the first time only"): +mt7915_mcu_add_sta() derived its "newly added" flag from +!rcu_access_pointer(dev->mt76.wcid[idx]), so the publish had to happen +after that call. Commit 33eb14f10290 ("wifi: mt76: mt7915: use mac80211 +.sta_state op") later replaced that flag with a caller-supplied `newly` +argument to mt7915_mcu_add_sta(), now simply hardcoded to true, so the +ordering is no longer required. + +Move the rcu_assign_pointer() before the MCU add_bss_info/add_sta +calls, so that the wcid is visible to lookups before firmware is told +it's live. This makes the ordering symmetric with +mt7915_remove_interface(), which keeps the pointer published until +after the MCU disable commands have been issued. + +Closes: https://github.com/openwrt/openwrt/issues/24594 +Fixes: 8e3e7567b8c1 ("mt76: mt7915: add sta_rec with EXTRA_INFO_NEW for the first time only") +Signed-off-by: Ryan Leung +Link: https://patch.msgid.link/20260821-mt7915-wcid-publish-order-v2-1-acf0b7f32cbd@protonmail.com +Signed-off-by: Felix Fietkau +--- + drivers/net/wireless/mediatek/mt76/mt7915/main.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/main.c b/drivers/net/wireless/mediatek/mt76/mt7915/main.c +index eb2dc410bba25..1f91e56b288f4 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7915/main.c ++++ b/drivers/net/wireless/mediatek/mt76/mt7915/main.c +@@ -273,9 +273,9 @@ static int mt7915_add_interface(struct ieee80211_hw *hw, + mt7915_init_bitrate_mask(vif); + memset(&mvif->cap, -1, sizeof(mvif->cap)); + ++ rcu_assign_pointer(dev->mt76.wcid[idx], &mvif->sta.wcid); + mt7915_mcu_add_bss_info(phy, vif, true); + mt7915_mcu_add_sta(dev, vif, NULL, CONN_STATE_PORT_SECURE, true); +- rcu_assign_pointer(dev->mt76.wcid[idx], &mvif->sta.wcid); + + mutex_unlock(&dev->mt76.mutex); + diff --git a/src/bin/copy.c b/src/bin/copy.c index e4cacaaef..8fd47f550 100644 --- a/src/bin/copy.c +++ b/src/bin/copy.c @@ -47,6 +47,7 @@ static char *xpath = "/*"; static int debug; static int force; static int timeout; +static int json_out; static int dry_run; static int sanitize; static int redact; @@ -952,6 +953,7 @@ static int usage_rpc(int rc) "Options:\n" " -d Enable debug mode, verbose output on stderr\n" " -h This help text\n" + " -j Print RPC output as JSON\n" " -t SEC Timeout for the operation, or default %d sec\n" " -v Show version\n" "\n" @@ -985,6 +987,77 @@ static bool is_leaflist(sr_conn_ctx_t *conn, const char *rpc_xpath, const char * return rc; } +/* + * Tree variant of rpc_exec(), prints the output as JSON. Used by show + * for actions with list output, which sr_print_val() cannot render. + */ +static int rpc_exec_json(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, + const char *rpc_xpath, int argc, char *argv[]) +{ + struct lyd_node *tree = NULL, *op = NULL; + const struct ly_ctx *ctx; + sr_data_t *output = NULL; + char *str = NULL; + int rc = 1, err = 0, i; + + ctx = sr_acquire_context(conn); + if (!ctx) { + warnx("failed acquiring libyang context"); + return 1; + } + + if (lyd_new_path(NULL, ctx, rpc_xpath, NULL, 0, &tree) || + lyd_find_path(tree, rpc_xpath, 0, &op)) { + warnx("invalid RPC xpath %s", rpc_xpath); + goto cleanup; + } + + for (i = 0; i < argc - 1; i += 2) { + const char *key = argv[i]; + char *val, *token, *saveptr; + + val = strdup(argv[i + 1]); + if (!val) { + warnx("Memory allocation failed"); + goto cleanup; + } + + if (strchr(val, ',') && is_leaflist(conn, rpc_xpath, key)) { + for (token = strtok_r(val, ",", &saveptr); token; + token = strtok_r(NULL, ",", &saveptr)) + err = lyd_new_path(op, NULL, key, token, 0, NULL); + } else + err = lyd_new_path(op, NULL, key, val, 0, NULL); + free(val); + + if (err) { + warnx("invalid RPC argument %s = %s", key, argv[i + 1]); + goto cleanup; + } + } + + dbg("Sending RPC %s (timeout: %d ms)", rpc_xpath, timeout * 1000); + err = sr_rpc_send_tree(sess, tree, timeout * 1000, &output); + if (err != SR_ERR_OK) { + sysrepo_print_error(sess); + warnx("RPC execution failed: %s", sr_strerror(err)); + goto cleanup; + } + + if (output && output->tree && + !lyd_print_mem(&str, output->tree, LYD_JSON, LYD_PRINT_SIBLINGS)) + puts(str); + free(str); + rc = 0; + +cleanup: + sr_release_data(output); + lyd_free_all(tree); + sr_release_context(conn); + + return rc; +} + /* Execute RPC from CLI arguments: xpath and key-value pairs */ static int rpc_exec(const char *rpc_xpath, int argc, char *argv[]) { @@ -1002,6 +1075,11 @@ static int rpc_exec(const char *rpc_xpath, int argc, char *argv[]) if (err != SR_ERR_OK) return 1; + if (json_out) { + rc = rpc_exec_json(conn, sess, rpc_xpath, argc, argv); + goto cleanup; + } + for (i = 0; i < argc - 1; i += 2) { const char *key = argv[i]; const char *val = argv[i + 1]; @@ -1125,13 +1203,16 @@ static int rpc_main(int argc, char *argv[]) timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT"); - while ((c = getopt(argc, argv, "dht:v")) != EOF) { + while ((c = getopt(argc, argv, "dhjt:v")) != EOF) { switch(c) { case 'd': debug = 1; break; case 'h': return usage_rpc(0); + case 'j': + json_out = 1; + break; case 't': timeout = atoi(optarg); break; diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py index e45e9e3a8..46eac8f2f 100755 --- a/src/bin/show/__init__.py +++ b/src/bin/show/__init__.py @@ -2,6 +2,7 @@ import re import subprocess +import sys import json from typing import List import os @@ -82,17 +83,68 @@ def tftp(args: List[str]) -> None: def hardware(args: List[str]) -> None: + """show hardware [ [survey [passive]]]""" + if len(args) >= 2 and args[1] == "survey": + channel_survey(args[0], "passive" in args[2:]) + return + data = get_json("/ietf-hardware:hardware") if not data: print("No hardware data retrieved.") return + if args: + hw = data.get("ietf-hardware:hardware", {}) + hw["component"] = [c for c in hw.get("component", []) if c.get("name") == args[0]] + if not hw["component"]: + print(f"No hardware component named {args[0]}.") + return + if RAW_OUTPUT: print(json.dumps(data, indent=2)) return cli_pretty(data, "show-hardware") +def channel_survey(radio: str, passive: bool) -> None: + """Run the channel-survey action on a WiFi radio and render the channel map""" + # Same character set as the hardware component name in the model + if not re.fullmatch(r"[a-zA-Z0-9_][a-zA-Z0-9_.:+@-]*", radio): + print(f"No WiFi radio named {radio}.") + return + + xpath = f"/ietf-hardware:hardware/component[name='{radio}']/infix-hardware:wifi-radio/channel-survey" + cmd = ["rpc", "-j", xpath] + if passive: + cmd += ["passive", "true"] + + # Progress goes to stderr, past the pager, which would otherwise + # print the chart twice when it arrives piecemeal after the scan + print(f"Scanning channels on {radio}, this takes a few seconds ...", + file=sys.stderr, flush=True) + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + if "does not exist" in result.stderr: + print(f"No WiFi radio named {radio}.") + return + # The rpc tool prints the device's reason, then its own verdict + for line in result.stderr.splitlines(): + if line.startswith("rpc: ") and "RPC execution failed" not in line: + print(re.sub(r" \(\d+\)$", "", line[len("rpc: "):])) + return + print(result.stderr.strip() or "Channel survey failed") + return + + if RAW_OUTPUT: + print(result.stdout) + return + + chart = subprocess.run(["/usr/libexec/infix/wifi-channel-map.py"], + input=result.stdout, capture_output=True, text=True) + sys.stdout.write(chart.stdout or chart.stderr) + sys.stdout.flush() + + def ntp(args: List[str]) -> None: # Create argument parser for ntp subcommands parser = argparse.ArgumentParser(prog='show ntp', add_help=False) diff --git a/src/confd/bin/gen-hardware b/src/confd/bin/gen-hardware index 8679bc730..3870a7302 100755 --- a/src/confd/bin/gen-hardware +++ b/src/confd/bin/gen-hardware @@ -53,7 +53,6 @@ gen_radio() "name": "$radio", "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": { - "country-code": "00", "band": "$band", "channel": "auto" } diff --git a/src/confd/configure.ac b/src/confd/configure.ac index 28f0c6e2f..1a13a1ffb 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -1,6 +1,6 @@ AC_PREREQ(2.61) # confd version is same as system YANG model version, step on breaking changes -AC_INIT([confd], [1.10], [https://github.com/kernelkit/infix/issues]) +AC_INIT([confd], [1.11], [https://github.com/kernelkit/infix/issues]) AM_INIT_AUTOMAKE(1.11 foreign subdir-objects) AM_SILENT_RULES(yes) @@ -24,6 +24,7 @@ AC_CONFIG_FILES([ share/migrate/1.8/Makefile share/migrate/1.9/Makefile share/migrate/1.10/Makefile + share/migrate/1.11/Makefile yang/Makefile yang/confd/Makefile yang/test-mode/Makefile diff --git a/src/confd/share/migrate/1.11/10-wifi-country-code.sh b/src/confd/share/migrate/1.11/10-wifi-country-code.sh new file mode 100644 index 000000000..b3c3f1028 --- /dev/null +++ b/src/confd/share/migrate/1.11/10-wifi-country-code.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Move the WiFi country code from each radio to the box-wide +# hardware/wifi/country-code leaf. +# +# The regulatory domain is one setting in the kernel, so a code per radio +# was misleading, and it was only ever applied once the radio had an +# interface. The first radio naming a real country wins. A radio left +# at the world domain ("00") sets nothing, that is the default without +# the leaf. +# +# The leaf only takes the codes the regulatory database knows. The old +# per-radio leaf accepted any ISO code, and a code the database lacks +# never did anything, so such a code becomes the world domain here. + +file=$1 +temp=${file}.tmp +yang=${WIFI_COUNTRY_CODES_YANG:-$(ls /usr/share/yang/modules/confd/infix-wifi-country-codes@*.yang 2>/dev/null | tail -1)} + +codes=$(jq -r '[ (.["ietf-hardware:hardware"].component // [])[] + | .["infix-hardware:wifi-radio"]?["country-code"]? // empty + | select(. != "00") ] | join(" ")' "$file") + +# The radios could disagree, the kernel has only one domain, so whichever +# daemon started last won. Say which one the migration keeps. +case $(echo "$codes" | tr ' ' '\n' | sort -u | wc -l) in + 0|1) ;; + *) + logger -t migrate -p user.warning \ + "$file: radios have different country codes ($codes), keeping the first radio's" + ;; +esac + +cc= +for code in $codes; do + if [ -n "$yang" ] && ! grep -qF "enum \"$code\"" "$yang"; then + logger -t migrate -p user.warning \ + "$file: WiFi country code $code is not in the regulatory database, using the world domain" + continue + fi + cc=$code + break +done + +jq --arg cc "$cc" ' + if $cc != "" then + .["ietf-hardware:hardware"]["infix-hardware:wifi"] = {"country-code": $cc} + else . end + | if .["ietf-hardware:hardware"].component then + .["ietf-hardware:hardware"].component |= + [ .[] | del(.["infix-hardware:wifi-radio"]["country-code"]) ] + else . end +' "$file" > "$temp" && mv "$temp" "$file" diff --git a/src/confd/share/migrate/1.11/20-wifi-key-holder-secret.sh b/src/confd/share/migrate/1.11/20-wifi-key-holder-secret.sh new file mode 100644 index 000000000..20ee2ded6 --- /dev/null +++ b/src/confd/share/migrate/1.11/20-wifi-key-holder-secret.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# 802.11r access points get a mandatory key-holder-secret, the keystore +# key the access points of a mobility domain hand each other a roaming +# client's keys with. The key used to be derived from the WiFi password. +# +# Keep existing setups roaming: point the new leaf at the access point's +# own secret, which gives the same key as before on every node, and say +# in the log that it should be changed to a key of its own. + +file=$1 +temp=${file}.tmp + +migrated=$(jq -r ' + [ (.["ietf-interfaces:interfaces"].interface // [])[] + | select(.["infix-interfaces:wifi"]["access-point"].roaming.dot11r != null) + | select(.["infix-interfaces:wifi"]["access-point"].roaming.dot11r["key-holder-secret"] == null) + | select(.["infix-interfaces:wifi"]["access-point"].security.secret != null) + | .name ] | join(" ")' "$file") + +for ifname in $migrated; do + logger -t migrate -p user.warning \ + "$file: $ifname: 802.11r key holder secret set to the WiFi password, change it to a key of its own" +done + +jq ' + if .["ietf-interfaces:interfaces"].interface then + .["ietf-interfaces:interfaces"].interface |= [ .[] + | if .["infix-interfaces:wifi"]["access-point"].roaming.dot11r != null + and .["infix-interfaces:wifi"]["access-point"].roaming.dot11r["key-holder-secret"] == null then + if .["infix-interfaces:wifi"]["access-point"].security.secret != null then + .["infix-interfaces:wifi"]["access-point"].roaming.dot11r["key-holder-secret"] + = .["infix-interfaces:wifi"]["access-point"].security.secret + else + del(.["infix-interfaces:wifi"]["access-point"].roaming.dot11r) + end + else . end ] + else . end +' "$file" > "$temp" && mv "$temp" "$file" diff --git a/src/confd/share/migrate/1.11/Makefile.am b/src/confd/share/migrate/1.11/Makefile.am new file mode 100644 index 000000000..284b59586 --- /dev/null +++ b/src/confd/share/migrate/1.11/Makefile.am @@ -0,0 +1,2 @@ +migratedir = $(pkgdatadir)/migrate/1.11 +dist_migrate_DATA = 10-wifi-country-code.sh 20-wifi-key-holder-secret.sh diff --git a/src/confd/share/migrate/Makefile.am b/src/confd/share/migrate/Makefile.am index 755ac16a4..2f73b3c33 100644 --- a/src/confd/share/migrate/Makefile.am +++ b/src/confd/share/migrate/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 +SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11 migratedir = $(pkgdatadir)/migrate diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 69433e568..2aa0b3b75 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -506,6 +506,17 @@ static confd_dependency_t dep_wifi_interfaces(struct lyd_node **diff, struct lyd radio_node = lydx_get_xpathf(config, "/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/radio", ifname); + if (!radio_node) { + /* A WDS link has no radio of its own, follow its access point */ + struct lyd_node *ap = lydx_get_xpathf(config, + "/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/wds-link/access-point", + ifname); + + if (ap) + radio_node = lydx_get_xpathf(config, + "/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/radio", + lyd_get_value(ap)); + } if (!radio_node) continue; @@ -723,14 +734,16 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod } } - /* ietf-hardware */ - if ((rc = hardware_change(session, config, diff, event, confd))) - goto free_diff; - /* ietf-interfaces */ if ((rc = interfaces_change(session, config, diff, event, confd))) goto free_diff; + /* ietf-hardware, after the interfaces: hostapd binds to WiFi + * netdevs, so it must be (re)started only once the interface + * pipeline has created or recreated them. */ + if ((rc = hardware_change(session, config, diff, event, confd))) + goto free_diff; + /* infix-dhcp-client*/ if ((rc = dhcp_client_change(session, config, diff, event, confd))) goto free_diff; @@ -1018,6 +1031,10 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) if (rc) goto err; + rc = hardware_rpc_init(&confd); + if (rc) + goto err; + rc = support_rpc_init(&confd); if (rc) goto err; diff --git a/src/confd/src/core.h b/src/confd/src/core.h index e784e6a5b..7cafc5550 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -196,6 +196,14 @@ static inline int register_rpc(sr_session_ctx_t *session, const char *xpath, return rc; } +/* Fail an RPC/action with MSG as the NETCONF error message */ +static inline int rpc_failed(sr_session_ctx_t *session, const char *msg) +{ + sr_session_set_netconf_error(session, "application", "operation-failed", + NULL, NULL, msg, 0); + return SR_ERR_OPERATION_FAILED; +} + static inline int register_rpc_tree(sr_session_ctx_t *session, const char *xpath, sr_rpc_tree_cb cb, void *arg, sr_subscription_ctx_t **sub) { @@ -288,6 +296,7 @@ int services_change(sr_session_ctx_t *session, struct lyd_node *config, struct l /* hardware.c */ int hardware_candidate_init(struct confd *confd); +int hardware_rpc_init(struct confd *confd); int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); /* keystore.c */ diff --git a/src/confd/src/hardware.c b/src/confd/src/hardware.c index d883b2c39..4ef853ec9 100644 --- a/src/confd/src/hardware.c +++ b/src/confd/src/hardware.c @@ -20,6 +20,8 @@ #define XPATH_BASE_ "/ietf-hardware:hardware" #define HOSTAPD_CONF "/etc/hostapd-%s.conf" #define HOSTAPD_CONF_NEXT HOSTAPD_CONF"+" +#define WIFI_RXKH_DIR "/run/wifi-neighbors" +#define WIFI_RXKH_FILE WIFI_RXKH_DIR "/%s.rxkh" #define HOSTAPD_SERVICE "/etc/finit.d/available/hostapd.conf" #define GPSD_CONF "/etc/finit.d/available/gpsd.conf" #define GPSD_CONF_NEXT GPSD_CONF"+" @@ -221,6 +223,75 @@ static const char *resolve_mobility_domain(const char *mobility_domain, const ch return hash_result; } +/* + * Cleartext of a keystore symmetric key, NULL when unset. Caller frees. + */ +static unsigned char *wifi_keystore_secret(struct lyd_node *config, const char *name) +{ + struct lyd_node *node; + const char *b64; + + if (!name) + return NULL; + + node = lydx_get_xpathf(config, "/keystore/symmetric-keys/symmetric-key[name='%s']/cleartext-symmetric-key", name); + if (!node) + return NULL; + + b64 = lyd_get_value(node); + if (!b64) + return NULL; + + return base64_decode((const unsigned char *)b64, strlen(b64), NULL); +} + +/* + * Key for the 802.11r key holder exchange between access points, hex + * encoded SHA-256 over the mobility domain and the key holder secret. + * Every AP of the mobility domain derives the same key, so the wildcard + * R0KH/R1KH entries let any of them fetch a roaming client's PMK-R1 from + * the AP it came from, which is what fast transition needs for WPA3 + * (SAE) clients: their PMK comes from the SAE handshake and cannot be + * regenerated locally the way a WPA2 PSK can. + * + * The secret is the dot11r key-holder-secret, never the passphrase: a + * holder of this key can fetch any client's keys and is pushed them, and + * the key tags the announcements wifi-neighbors.py sends on the APs' + * network, so a key derived from the passphrase would hand every client + * the keys of the others and give anyone on the network an offline + * oracle for the passphrase. + */ +static int wifi_ft_key(const char *mobility_domain, const unsigned char *secret, char *out, size_t len) +{ + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int dlen = 0; + EVP_MD_CTX *ctx; + size_t i; + + if (len < 2 * 32 + 1) + return -1; + + ctx = EVP_MD_CTX_new(); + if (!ctx) + return -1; + + if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1 || + EVP_DigestUpdate(ctx, "infix-ft:", 9) != 1 || + EVP_DigestUpdate(ctx, mobility_domain, strlen(mobility_domain)) != 1 || + EVP_DigestUpdate(ctx, ":", 1) != 1 || + EVP_DigestUpdate(ctx, secret, strlen((const char *)secret)) != 1 || + EVP_DigestFinal_ex(ctx, digest, &dlen) != 1 || dlen < 32) { + EVP_MD_CTX_free(ctx); + return -1; + } + EVP_MD_CTX_free(ctx); + + for (i = 0; i < 32; i++) + snprintf(out + 2 * i, 3, "%02x", digest[i]); + + return 0; +} + /* * Find an AP interface on a higher-band radio (5/6 GHz) advertising the * same SSID as the caller's 2.4 GHz BSS, for no_probe_resp_if_seen_on=. @@ -249,7 +320,7 @@ static const char *wifi_find_higher_band_twin(struct lyd_node *config, if (!wifi) continue; ap = lydx_get_child(wifi, "access-point"); - if (!ap) + if (!ap || !lydx_is_enabled(cif, "enabled")) continue; ssid = lydx_get_cattr(ap, "ssid"); if (!ssid || strcmp(ssid, current_ssid)) @@ -317,6 +388,10 @@ static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name, ap = lydx_get_child(wifi, "access-point"); if (!ap) continue; + /* hostapd brings every BSS it is given up, a disabled one + * must not be in its config at all. */ + if (!lydx_is_enabled(cif, "enabled")) + continue; list = realloc(list, sizeof(char *) * (n + 1)); ifname = lydx_get_cattr(cif, "name"); @@ -339,12 +414,66 @@ static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name, return 0; } +/* Emit the 4-address WDS ports (wds-link interfaces) of an AP */ +/* + * The 802.11r key holder list of a BSS, read by hostapd at start and on + * RELOAD_RXKHS. wifi-neighbors.py rewrites it with the other nodes' + * access points as R1KHs, keeping these wildcard entries. + */ +static int wifi_write_rxkh(const char *ifname, const char *ft_key) +{ + FILE *fp; + + if (fmkpath(0755, WIFI_RXKH_DIR)) + return -1; + + fp = fopenf("w", WIFI_RXKH_FILE, ifname); + if (!fp) + return -1; + + fprintf(fp, "r0kh=ff:ff:ff:ff:ff:ff * %s\n", ft_key); + fprintf(fp, "r1kh=00:00:00:00:00:00 00:00:00:00:00:00 %s\n", ft_key); + + return fclose(fp); +} + +static void wifi_gen_wds_ports(FILE *hostapd, struct lyd_node *config, const char *ap_ifname) +{ + struct lyd_node *cifs, *cif; + bool first = true; + + cifs = lydx_get_descendant(config, "interfaces", "interface", NULL); + LYX_LIST_FOR_EACH(cifs, cif, "interface") { + struct lyd_node *wds; + const char *ap; + + wds = lydx_get_descendant(lyd_child(cif), "wifi", "wds-link", NULL); + if (!wds) + continue; + + ap = lydx_get_cattr(wds, "access-point"); + if (!ap || strcmp(ap, ap_ifname)) + continue; + + if (first) { + fprintf(hostapd, "# 4-address WDS ports\n"); + fprintf(hostapd, "wds_sta=1\n"); + /* A deauth from a vanishing peer is easily lost, and the + * default 300 s leaves a dead backhaul port up that long. */ + fprintf(hostapd, "ap_max_inactivity=30\n"); + first = false; + } + fprintf(hostapd, "wds_sta_ifname=%s %s\n", + lydx_get_cattr(wds, "peer-address"), lydx_get_cattr(cif, "name")); + } +} + /* Helper: Write SSID and security configuration (shared between primary and BSS) */ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd_node *config, bool is_bss, const char *band) { - struct lyd_node *wifi, *ap, *security, *secret_node, *roaming; + struct lyd_node *wifi, *ap, *security, *roaming; struct lyd_node *dot11k, *dot11r, *dot11v; - const char *ssid, *hidden, *security_mode, *secret_name; + const char *ssid, *hidden, *security_mode; const char *mobility_domain, *mobility_domain_raw; const char *nas_identifier_cfg; bool enable_80211k, enable_80211r, enable_80211v, enable_mbo; @@ -361,6 +490,7 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd if (is_bss) { fprintf(hostapd, "\n# BSS %s\n", ifname); fprintf(hostapd, "bss=%s\n", ifname); + fprintf(hostapd, "ctrl_interface=/run/hostapd\n"); } /* Check 802.11k/r/v configuration */ @@ -427,21 +557,8 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd security_mode = "open"; /* Get secret from keystore if needed */ - if (strcmp(security_mode, "open") != 0) { - secret_name = lydx_get_cattr(security, "secret"); - if (secret_name) { - const char *b64; - - secret_node = lydx_get_xpathf(config, - "/keystore/symmetric-keys/symmetric-key[name='%s']/cleartext-symmetric-key", - secret_name); - if (secret_node) { - b64 = lyd_get_value(secret_node); - if (b64) - secret = base64_decode((const unsigned char *)b64, strlen(b64), NULL); - } - } - } + if (strcmp(security_mode, "open") != 0) + secret = wifi_keystore_secret(config, lydx_get_cattr(security, "secret")); /* Resolve "auto" to concrete mode based on band */ if (!strcmp(security_mode, "auto")) @@ -482,6 +599,10 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd /* 802.11r: Fast BSS Transition */ if (enable_80211r) { + const char *bridge = lydx_get_cattr(lydx_get_child(cif, "bridge-port"), "bridge"); + unsigned char *kh_secret; + char ft_key[65]; + fprintf(hostapd, "# Fast BSS Transition (802.11r)\n"); fprintf(hostapd, "mobility_domain=%s\n", mobility_domain); /* Over-the-air FT: the client authenticates directly with the @@ -490,6 +611,48 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd fprintf(hostapd, "ft_over_ds=0\n"); fprintf(hostapd, "ft_psk_generate_local=1\n"); fprintf(hostapd, "nas_identifier=%s\n", nas_identifier_cfg); + /* The key holders of all APs on the SSID reach each other over + * the network the APs are bridged to, see wifi_ft_key(). On a + * VLAN filtering bridge that is the APs' VLAN: use its VLAN + * interface when there is one, the bridge device itself has + * no say in which VLAN its frames end up in. */ + kh_secret = wifi_keystore_secret(config, lydx_get_cattr(dot11r, "key-holder-secret")); + if (!kh_secret) + ERROR("%s: 802.11r key holder secret not found in keystore, no key exchange", ifname); + if (kh_secret && !wifi_ft_key(mobility_domain, kh_secret, ft_key, sizeof(ft_key))) { + if (bridge) { + const char *pvid = lydx_get_cattr(lydx_get_child(cif, "bridge-port"), "pvid"); + const char *ft_iface = bridge; + + if (pvid) { + struct lyd_node *vif; + + vif = lydx_get_xpathf(config, "/interfaces/interface[vlan/id='%s' and vlan/lower-layer-if='%s']/name", + pvid, bridge); + if (vif) + ft_iface = lyd_get_value(vif); + } + fprintf(hostapd, "ft_iface=%s\n", ft_iface); + } + /* The key holders live in a file so that wifi-neighbors.py + * can add the other nodes' access points as it hears of + * them and have hostapd reload the list. Start with the + * wildcards, which accept any node holding the key. */ + if (!wifi_write_rxkh(ifname, ft_key)) + fprintf(hostapd, "rxkh_file=" WIFI_RXKH_FILE "\n", ifname); + /* Hand every R1KH the PMK-R1 as soon as a client + * authenticates: a roam must not depend on this node, + * the R0KH, still being up to pull the key from. */ + fprintf(hostapd, "pmk_r1_push=1\n"); + /* A client roaming away from a node that is going down + * asks for a key that node can no longer hand out. + * Give up on the fetch quickly and reject, the client + * then logs in the normal way; waiting in silence makes + * it blacklist the target instead. */ + fprintf(hostapd, "rkh_pull_timeout=300\n"); + fprintf(hostapd, "rkh_pull_retries=1\n"); + } + free(kh_secret); } /* 802.11k: Radio Resource Management */ @@ -505,6 +668,8 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd fprintf(hostapd, "bss_transition=1\n"); } + wifi_gen_wds_ports(hostapd, config, ifname); + /* OKC: Opportunistic Key Caching */ if (roaming) { const char *okc = lydx_get_cattr(roaming, "okc"); @@ -521,16 +686,14 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd fprintf(hostapd, "mbo=1\n"); - /* Required for no_probe_resp_if_seen_on below: without it - * hostapd keeps no sta_track list, so the twin radio never - * knows which clients it has seen. Radio-level, emit once - * in the main section, never per BSS. */ - if (!is_bss) - fprintf(hostapd, "track_sta_max_num=100\n"); - - /* Active band steering: on a 2.4 GHz BSS, suppress probe - * responses to clients recently seen on the same-SSID 5/6 - * GHz BSS, nudging dual-band clients to the higher band. */ + /* Band steering on a 2.4 GHz BSS with a same-SSID twin on 5/6 + * GHz: a client the twin has seen lately gets no probe response + * here, so it tends to join the twin instead. The seen-on list + * is kept per radio, see the radio section. Clients already + * connected are moved by wifi-steer.sh, which finds the pairs + * by this directive. Refusing authentication as well would + * lock a client out when the twin cannot take it, e.g. during + * its radar check, so that is deliberately not done. */ twin = wifi_find_higher_band_twin(config, band, ssid); if (twin) fprintf(hostapd, "no_probe_resp_if_seen_on=%s\n", twin); @@ -865,9 +1028,18 @@ static void wifi_build_vht_capab(char *out, size_t sz, unsigned int vht_cap, int } } +/* The box-wide WiFi country code, NULL when none is configured */ +static const char *wifi_country_code(struct lyd_node *config) +{ + struct lyd_node *wifi; + + wifi = lydx_get_descendant(config, "hardware", "wifi", NULL); + return lydx_get_cattr(wifi, "country-code"); +} + /* Helper: Write radio-specific configuration */ static void wifi_gen_radio_config(FILE *hostapd, const char *radio_name, - struct lyd_node *radio_node) + struct lyd_node *radio_node, struct lyd_node *config) { const char *country, *channel, *band, *width; unsigned int ht_cap = 0, vht_cap = 0; @@ -876,7 +1048,7 @@ static void wifi_gen_radio_config(FILE *hostapd, const char *radio_name, int ch = 0; bool legacy_rates, he = false; - country = lydx_get_cattr(radio_node, "country-code"); + country = wifi_country_code(config); band = lydx_get_cattr(radio_node, "band"); channel = lydx_get_cattr(radio_node, "channel"); width = lydx_get_cattr(radio_node, "channel-width"); @@ -908,6 +1080,13 @@ static void wifi_gen_radio_config(FILE *hostapd, const char *radio_name, /* Use short preamble for better throughput on modern clients */ fprintf(hostapd, "preamble=1\n"); + /* Remember the clients this radio has seen, for band steering on + * the other radios. A dual-band client is refused on 2.4 GHz + * while it is on this list, so keep the list short-lived: that is + * the longest a client that cannot get in on 5 GHz has to wait. */ + fprintf(hostapd, "track_sta_max_num=100\n"); + fprintf(hostapd, "track_sta_max_age=60\n"); + if (band) { if (!strcmp(band, "2.4GHz")) { /* hw_mode=g: 2.4GHz with 802.11g (OFDM) as baseline */ @@ -1133,7 +1312,7 @@ static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs, fprintf(hostapd, "\n"); /* Radio-specific configuration */ - wifi_gen_radio_config(hostapd, radio_name, radio_node); + wifi_gen_radio_config(hostapd, radio_name, radio_node, config); /* Add BSS sections for secondary APs (multi-SSID) */ for (i = 1; i < ap_count; i++) { @@ -1208,6 +1387,7 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l sr_event_t event, struct confd *confd) { struct lyd_node *difs = NULL, *dif = NULL; + int country_changed = 0; int rc = SR_ERR_OK; int gps_changed = 0; int wifi_changed = 0; @@ -1215,7 +1395,22 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l if (!lydx_find_xpathf(diff, XPATH_BASE_)) return SR_ERR_OK; - difs = lydx_get_descendant(diff, "hardware", "component", NULL); + /* + * The country code is one setting for every radio. Apply the + * regulatory domain here, not from hostapd, so a radio without an + * interface is in the right domain too. Every radio's hostapd + * config carries the code, so visit them all when it changes. + */ + if (lydx_get_xpathf(diff, XPATH_BASE_ "/infix-hardware:wifi/country-code")) { + country_changed = 1; + if (event == SR_EV_DONE) + systemf("iw reg set %s", wifi_country_code(config) ?: "00"); + } + + if (country_changed) + difs = lydx_get_descendant(config, "hardware", "component", NULL); + else + difs = lydx_get_descendant(diff, "hardware", "component", NULL); LYX_LIST_FOR_EACH(difs, dif, "component") { enum lydx_op op; @@ -1232,6 +1427,9 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l continue; class = lydx_get_cattr(cif, "class"); + if (country_changed && strcmp(class, "infix-hardware:wifi") && + !lydx_get_xpathf(diff, XPATH_BASE_ "/component[name='%s']", name)) + continue; /* Handle USB components */ if (!strcmp(class, "infix-hardware:usb")) { @@ -1253,13 +1451,14 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l goto err; } } else if (!strcmp(class, "infix-hardware:wifi")) { - struct lyd_node *interfaces_config, *interfaces_diff; struct lyd_node **wifi_iface_list = NULL; - struct lyd_node *ap; + struct lyd_node *interfaces_config; struct lyd_node *cwifi_radio; int wifi_iface_count = 0; char src[40], dst[40]; + char **ap_list = NULL; int ap_interfaces = 0; + int i; switch (event) { case SR_EV_ABORT: @@ -1267,27 +1466,25 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l case SR_EV_CHANGE: break; case SR_EV_DONE: - interfaces_diff = lydx_get_descendant(diff, "interfaces", "interface", NULL); - - wifi_find_interfaces_on_radio(interfaces_diff, name, - &wifi_iface_list, &wifi_iface_count); - if (wifi_iface_count > 0) { - ap = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "access-point", NULL); - if (ap && lydx_get_op(ap) != LYDX_OP_DELETE) { - snprintf(src, sizeof(src), HOSTAPD_CONF_NEXT, name); - snprintf(dst, sizeof(dst), HOSTAPD_CONF, name); - - if (fexistf(HOSTAPD_CONF_NEXT, name)) { - (void)rename(src, dst); - ap_interfaces++; - } - } - } - if (!ap_interfaces) { + /* The changed interface need not be an AP, a + * wds-link or station on the radio also lands + * here, so judge by the APs left in config. */ + interfaces_config = lydx_get_descendant(config, "interfaces", "interface", NULL); + wifi_find_radio_aps(interfaces_config, name, &ap_list, &ap_interfaces); + for (i = 0; i < ap_interfaces; i++) + free(ap_list[i]); + free(ap_list); + + if (ap_interfaces) { + snprintf(src, sizeof(src), HOSTAPD_CONF_NEXT, name); + snprintf(dst, sizeof(dst), HOSTAPD_CONF, name); + + if (fexistf(HOSTAPD_CONF_NEXT, name)) + (void)rename(src, dst); + } else { erasef(HOSTAPD_CONF, name); erasef(HOSTAPD_CONF_NEXT, name); } - free(wifi_iface_list); /* All radios share one hostapd process; the service is * (re)generated after the component loop below. */ wifi_changed = 1; @@ -1431,8 +1628,11 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l rc = SR_ERR_INTERNAL; } else { fprintf(fp, "# Generated by confd, do not edit.\n"); - fprintf(fp, "service name:hostapd \\\n"); - fprintf(fp, "\t[2345] hostapd -P /run/hostapd.pid"); + /* The wrapper hands the clients over to another + * access point before hostapd stops, give it time. */ + fprintf(fp, "service name:hostapd kill:10 \\\n"); + fprintf(fp, "\t[2345] /usr/libexec/infix/hostapd.sh" + " -g /run/hostapd.global -P /run/hostapd.pid"); for (i = 0; i < gl.gl_pathc; i++) fprintf(fp, " %s", gl.gl_pathv[i]); fprintf(fp, " \\\n\t-- Wi-Fi Access Points\n"); @@ -1451,6 +1651,128 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l return rc; } + +/* + * Scan all channels on a radio and report how busy each one is, the + * channel-survey action in infix-hardware.yang. The scan takes the + * radio off its operating channel for a few seconds, which is why this + * is an action and not operational data. The helper does the scan and + * the parsing, see iw.py survey-scan. + */ +static int wifi_channel_survey(sr_session_ctx_t *session, uint32_t sub_id, const char *op_path, + const struct lyd_node *input, sr_event_t event, uint32_t request_id, + struct lyd_node *output, void *priv) +{ + static const struct { const char *json, *yang; } times[] = { + { "noise", "noise" }, + { "active_time", "active-time" }, + { "busy_time", "busy-time" }, + { "receive_time", "receive-time" }, + { "transmit_time", "transmit-time" }, + }; + struct lyd_node *component, *chan; + const char *radio, *passive; + json_error_t jerr; + json_t *root, *entry; + size_t index; + FILE *pp; + + if (event != SR_EV_RPC) + return SR_ERR_OK; + + component = lyd_parent(lyd_parent(input)); + radio = component ? lydx_get_cattr(component, "name") : NULL; + if (!radio) + return rpc_failed(session, "Cannot tell which radio to survey"); + + passive = lydx_get_cattr((struct lyd_node *)input, "passive"); + pp = popenf("r", "/usr/libexec/infix/iw.py survey-scan %s %s", radio, + passive && !strcmp(passive, "true") ? "passive" : ""); + if (!pp) + return rpc_failed(session, "Failed starting channel survey"); + + root = json_loadf(pp, 0, &jerr); + pclose(pp); + if (!root) + return rpc_failed(session, "Channel survey returned no data"); + + if (!json_is_array(root)) { + json_t *err = json_object_get(root, "error"); + char msg[256]; + + snprintf(msg, sizeof(msg), "Channel survey failed: %s", + json_is_string(err) ? json_string_value(err) : "unknown error"); + json_decref(root); + return rpc_failed(session, msg); + } + + json_array_foreach(root, index, entry) { + json_t *freq = json_object_get(entry, "frequency"); + char val[32]; + + if (!json_is_integer(freq)) + continue; + + snprintf(val, sizeof(val), "%lld", json_integer_value(freq)); + if (lyd_new_list(output, NULL, "channel", LYD_NEW_VAL_OUTPUT, &chan, val)) { + ERROR("channel-survey: failed adding channel %s", val); + continue; + } + + lyd_new_term(chan, NULL, "in-use", + json_is_true(json_object_get(entry, "in_use")) ? "true" : "false", + 0, NULL); + + for (size_t i = 0; i < NELEMS(times); i++) { + json_t *v = json_object_get(entry, times[i].json); + + if (!json_is_integer(v)) + continue; + + snprintf(val, sizeof(val), "%lld", json_integer_value(v)); + lyd_new_term(chan, NULL, times[i].yang, val, 0, NULL); + } + } + json_decref(root); + + return SR_ERR_OK; +} + +/* The action only exists when the wifi feature is enabled, see wifi.inc */ +static bool wifi_feature_enabled(struct confd *confd) +{ + const struct lys_module *mod; + const struct ly_ctx *ctx; + bool enabled = false; + + ctx = sr_acquire_context(confd->conn); + if (!ctx) + return false; + + mod = ly_ctx_get_module_implemented(ctx, "infix-hardware"); + if (mod) + enabled = lys_feature_value(mod, "wifi") == LY_SUCCESS; + sr_release_context(confd->conn); + + return enabled; +} + +int hardware_rpc_init(struct confd *confd) +{ + int rc = 0; + + if (!wifi_feature_enabled(confd)) + return SR_ERR_OK; + + REGISTER_RPC_TREE(confd->session, XPATH_BASE_ "/component/infix-hardware:wifi-radio/channel-survey", + wifi_channel_survey, NULL, &confd->sub); + + return SR_ERR_OK; +fail: + ERROR("Init hardware rpc failed: %s", sr_strerror(rc)); + return rc; +} + int hardware_candidate_init(struct confd *confd) { int rc = 0; diff --git a/src/confd/src/if-wifi.c b/src/confd/src/if-wifi.c index 8b9fca53a..88e49107d 100644 --- a/src/confd/src/if-wifi.c +++ b/src/confd/src/if-wifi.c @@ -8,6 +8,8 @@ */ #include +#include +#include #include #include @@ -109,7 +111,7 @@ int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif) wifi_mode_t wifi_get_mode(struct lyd_node *iface) { - struct lyd_node *ap, *mesh, *wifi; + struct lyd_node *ap, *mesh, *wds, *wifi; wifi = lydx_get_child(iface, "wifi"); if (!wifi) @@ -127,6 +129,12 @@ wifi_mode_t wifi_get_mode(struct lyd_node *iface) return wifi_mesh; } + wds = lydx_get_child(wifi, "wds-link"); + if (wds) { + if (lydx_get_op(wds) != LYDX_OP_DELETE) + return wifi_wds; + } + /* * Need to return station even if "station" also is false, * because station is the default scanning mode. @@ -154,16 +162,86 @@ int wifi_mode_changed(struct lyd_node *wifi) if (node && (op == LYDX_OP_CREATE || op == LYDX_OP_DELETE)) return 1; + node = lydx_get_child(wifi, "wds-link"); + if (node) + op = lydx_get_op(node); + if (node && (op == LYDX_OP_CREATE || op == LYDX_OP_DELETE)) + return 1; + + /* Scan-only <-> station, and the 4-address flag is set at creation */ + node = lydx_get_child(wifi, "station"); + if (node) + op = lydx_get_op(node); + if (node && (op == LYDX_OP_CREATE || op == LYDX_OP_DELETE)) + return 1; + if (node && lydx_get_child(node, "wds")) + return 1; + return 0; } +/* + * A changed wpa_supplicant config only takes effect when the daemon + * reloads it, so nudge the service whenever the wifi subtree changed. + */ +static int wifi_gen_reload(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net) +{ + const char *ifname = lydx_get_cattr(cif, "name"); + const char *svc; + FILE *fp; + + if (!lydx_get_child(dif, "wifi")) + return SR_ERR_OK; + + switch (wifi_get_mode(cif)) { + case wifi_station: + svc = "wifi"; + break; + case wifi_mesh: + svc = "mesh"; + break; + default: + return SR_ERR_OK; + } + + fp = dagger_fopen_net_init(net, ifname, NETDAG_INIT_DAEMON, "wifi-reload.sh"); + if (!fp) + return SR_ERR_INTERNAL; + + fprintf(fp, "initctl -bfq touch %s@%s\n", svc, ifname); + fclose(fp); + + return SR_ERR_OK; +} + +/* Settings of an existing station or mesh point changed */ +int wifi_gen_settings(sr_session_ctx_t *session, struct lyd_node *dif, + struct lyd_node *cif, struct dagger *net) +{ + int rc; + + switch (wifi_get_mode(cif)) { + case wifi_station: + rc = wifi_validate_secret(session, cif) ? : wifi_gen_station(cif); + break; + case wifi_mesh: + rc = wifi_gen_mesh(cif); + break; + default: + return SR_ERR_OK; + } + + return rc ? : wifi_gen_reload(dif, cif, net); +} + /* * Generate wpa_supplicant config for station mode */ int wifi_gen_station(struct lyd_node *cif) { - const char *ifname, *ssid, *secret_name, *security_mode, *radio; - struct lyd_node *security, *secret_node, *radio_node, *station, *wifi; + const char *ifname, *ssid, *secret_name, *security_mode; + struct lyd_node *security, *secret_node, *station, *wifi; + const char *bssid = NULL; unsigned char *secret = NULL; FILE *wpa_supplicant = NULL; char *security_str = NULL; @@ -176,10 +254,10 @@ int wifi_gen_station(struct lyd_node *cif) if (!wifi) return SR_ERR_OK; - radio = lydx_get_cattr(wifi, "radio"); station = lydx_get_child(wifi, "station"); if (station) { ssid = lydx_get_cattr(station, "ssid"); + bssid = lydx_get_cattr(station, "peer-bssid"); security = lydx_get_child(station, "security"); security_mode = lydx_get_cattr(security, "mode"); secret_name = lydx_get_cattr(security, "secret"); @@ -191,9 +269,8 @@ int wifi_gen_station(struct lyd_node *cif) secret_name = NULL; } - radio_node = lydx_get_xpathf(cif, - "/ietf-hardware:hardware/component[name='%s']/infix-hardware:wifi-radio", radio); - country = lydx_get_cattr(radio_node, "country-code"); + country = lydx_get_cattr(lydx_get_xpathf(cif, "/ietf-hardware:hardware/infix-hardware:wifi"), + "country-code"); if (secret_name && strcmp(security_mode, "disabled") != 0) { const char *b64; @@ -229,8 +306,12 @@ int wifi_gen_station(struct lyd_node *cif) if (!strcmp(security_mode, "disabled")) asprintf(&security_str, "key_mgmt=NONE"); else if (secret) + /* ieee80211w=1: MFP capable. WPA3-only APs, and every + * AP on 6 GHz, require it and are skipped as candidates + * without it; WPA2 APs without MFP still work. */ asprintf(&security_str, "key_mgmt=FT-SAE FT-PSK SAE WPA-PSK\n" + " ieee80211w=1\n" " psk=\"%s\"", secret); /* bgscan="" disables background scanning once associated: on a @@ -239,9 +320,13 @@ int wifi_gen_station(struct lyd_node *cif) fprintf(wpa_supplicant, "network={\n" " bgscan=\"\"\n" - " ssid=\"%s\"\n" + " scan_ssid=1\n" + " ssid=\"%s\"\n", ssid); + if (bssid) + fprintf(wpa_supplicant, " bssid=%s\n", bssid); + fprintf(wpa_supplicant, " %s\n" - "}\n", ssid, security_str); + "}\n", security_str); free(security_str); } else { /* Scan-only mode - no station container configured */ @@ -358,7 +443,8 @@ int wifi_gen_mesh(struct lyd_node *cif) radio_node = lydx_get_xpathf(cif, "/ietf-hardware:hardware/component[name='%s']/infix-hardware:wifi-radio", radio); - country = lydx_get_cattr(radio_node, "country-code"); + country = lydx_get_cattr(lydx_get_xpathf(cif, "/ietf-hardware:hardware/infix-hardware:wifi"), + "country-code"); band = lydx_get_cattr(radio_node, "band"); width = lydx_get_cattr(radio_node, "channel-width"); channel = atoi(lydx_get_cattr(radio_node, "channel") ? : "0"); @@ -481,12 +567,75 @@ static int wifi_get_probe_timeout(sr_session_ctx_t *session, const char *radio) /* * Add WiFi virtual interface using iw */ +int wifi_add_deps(struct lyd_node *cif) +{ + struct lyd_node *wds; + const char *ap; + int err; + + wds = lydx_get_descendant(lyd_child(cif), "wifi", "wds-link", NULL); + if (!wds) + return 0; + + ap = lydx_get_cattr(wds, "access-point"); + err = dagger_add_dep(&confd.netdag, lydx_get_cattr(cif, "name"), ap); + if (err) + return ERR_IFACE(cif, err, "Unable to depend on \"%s\"", ap); + + return 0; +} + +/* + * Default address for a VIF without custom-phys-address. Every VIF + * on a radio would otherwise inherit the radio's own address, and on + * boards without a per-unit EEPROM that address is the vendor default, + * shared by every unit of the model. Derive from the chassis MAC + * instead, encoded in the first octet so it never meets the port + * addresses, which are chassis + N: set the locally administered bit + * and add the interface number plus one in bits 2-7, e.g. wifi0 on + * chassis 00:53:00:c0:ff:ee becomes 06:53:00:c0:ff:ee and wifi1 + * 0a:53:00:c0:ff:ee. Names without a number use a hash of the name. + */ +static int wifi_default_addr(const char *ifname, char *buf, size_t len) +{ + const char *chassis = get_chassis_addr(); + unsigned int n = 0; + uint8_t mac[6]; + const char *p; + + if (!chassis) + return -1; + + if (sscanf(chassis, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) != 6) + return -1; + + for (p = ifname; *p && !isdigit((unsigned char)*p); p++) + ; + if (*p) { + n = strtoul(p, NULL, 10); + } else { + for (p = ifname; *p; p++) + n = n * 33 + (unsigned char)*p; + } + n = (n + 1) & 0x3f; + + /* Locally administered, unicast whatever the chassis says */ + mac[0] = 0x02 | (n << 2); + + snprintf(buf, len, " addr %02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return 0; +} + int wifi_add_iface(struct lyd_node *cif, struct dagger *net) { const char *ifname, *radio; struct lyd_node *wifi; wifi_mode_t mode; int probe_timeout; + char addr[32] = ""; + char custom[18]; FILE *iw; int rc = SR_ERR_OK; @@ -498,10 +647,14 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net) return SR_ERR_INVAL_ARG; } - radio = lydx_get_cattr(wifi, "radio"); - if (!radio) { - ERROR("WiFi interface %s: missing radio reference", ifname); - return SR_ERR_INVAL_ARG; + mode = wifi_get_mode(cif); + if (mode == wifi_wds) { + /* A WDS link has no radio of its own, it is a port of its AP */ + const char *ap = lydx_get_cattr(lydx_get_child(wifi, "wds-link"), "access-point"); + + radio = lydx_get_cattr(lydx_get_xpathf(cif, "../interface[name='%s']/wifi", ap), "radio"); + } else { + radio = lydx_get_cattr(wifi, "radio"); } iw = dagger_fopen_net_init(net, ifname, NETDAG_INIT_PRE, "wifi-iface.sh"); @@ -510,12 +663,13 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net) return SR_ERR_INTERNAL; } - mode = wifi_get_mode(cif); probe_timeout = wifi_get_probe_timeout(net->session, radio); fprintf(iw, "# Generated by Infix confd - WiFi Interface Creation\n"); fprintf(iw, "# Create %s interface %s on radio %s\n", - mode == wifi_station ? "station" : (mode == wifi_mesh ? "mesh" : "access point"), ifname, radio); + mode == wifi_station ? "station" : + mode == wifi_mesh ? "mesh" : + mode == wifi_wds ? "wds-link" : "access point", ifname, radio); /* Wait for PHY if probe-timeout is set (slow USB dongles) */ if (probe_timeout > 0) { @@ -539,21 +693,43 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net) fprintf(iw, " exit 0\n"); fprintf(iw, "fi\n\n"); + /* A WDS link is created with its AP's address, see below */ + if (mode != wifi_wds && interface_get_phys_addr(cif, custom)) + wifi_default_addr(ifname, addr, sizeof(addr)); + switch(mode) { - case wifi_station: - fprintf(iw, "iw phy %s interface add %s type managed\n", radio, ifname); + case wifi_station: { + struct lyd_node *station = lydx_get_child(wifi, "station"); + + fprintf(iw, "iw phy %s interface add %s type managed%s%s\n", radio, ifname, + station && lydx_is_enabled(station, "wds") ? " 4addr on" : "", addr); wifi_gen_station(cif); fprintf(iw, "initctl -bfq enable wifi@%s\n", ifname); fprintf(iw, "initctl -bfq touch wifi@%s\n", ifname); + /* A running instance from before the netdev was recreated + * only gets a SIGHUP from the touch and keeps stale driver + * state, so make sure it starts over on the new netdev. */ + fprintf(iw, "initctl -bnq restart wpa_supplicant:%s\n", ifname); break; + } + case wifi_wds: { + const char *ap = lydx_get_cattr(lydx_get_child(wifi, "wds-link"), "access-point"); + + /* An AP_VLAN pairs with the AP of the same MAC address, and + * hostapd brings it up when the station associates. */ + fprintf(iw, "iw dev %s interface add %s type __ap_vlan" + " addr $(cat /sys/class/net/%s/address) 4addr on\n", ap, ifname, ap); + break; + } case wifi_ap: - fprintf(iw, "iw phy %s interface add %s type __ap\n", radio, ifname); + fprintf(iw, "iw phy %s interface add %s type __ap%s\n", radio, ifname, addr); break; case wifi_mesh: - fprintf(iw, "iw phy %s interface add %s type mesh\n", radio, ifname); + fprintf(iw, "iw phy %s interface add %s type mesh%s\n", radio, ifname, addr); wifi_gen_mesh(cif); fprintf(iw, "initctl -bfq enable mesh@%s\n", ifname); fprintf(iw, "initctl -bfq touch mesh@%s\n", ifname); + fprintf(iw, "initctl -bnq restart wpa_supplicant:%s\n", ifname); break; default: ERROR("WiFi mode %d unknown", mode); @@ -587,6 +763,10 @@ int wifi_del_iface(struct lyd_node *dif, struct dagger *net) fprintf(iw, "initctl -bfq disable mesh@%s\n", ifname); fprintf(iw, "initctl -bfq disable wifi@%s\n", ifname); + /* hostapd adopts a re-added netdev of a name it still holds and + * turns it back into an AP, so make it forget the name first. + * hostapd_cli cannot talk to the global socket, wpa_cli can. */ + fprintf(iw, "wpa_cli -g /run/hostapd.global raw \"REMOVE %s\" >/dev/null 2>&1\n", ifname); fprintf(iw, "ip link set %s down\n", ifname); fprintf(iw, "iw dev %s disconnect 2>/dev/null\n", ifname); fprintf(iw, "iw dev %s del 2>/dev/null || ip link del %s 2>/dev/null || true\n", ifname, ifname); diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index 4f085c2a3..f84ec4edf 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -457,12 +457,7 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net, case IFT_ETH: return netdag_gen_ethtool(net, cif, dif); case IFT_WIFI: - if (wifi_get_mode(cif) == wifi_station) - return wifi_validate_secret(session, cif) - ? : wifi_gen_station(cif); - if (wifi_get_mode(cif) == wifi_mesh) - return wifi_gen_mesh(cif); - return 0; + return wifi_gen_settings(session, dif, cif, net); case IFT_PPPOE: return ppp_gen(session, dif, cif, net); case IFT_DUMMY: @@ -665,9 +660,9 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net bool pppd_owned; /* link state and MTU */ const char *attr; int err = 0; + bool wds; FILE *ip; - err = netdag_gen_iface_timeout(net, ifname, iftype); if (err) goto err; @@ -729,7 +724,14 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net } pppd_owned = iftype_from_iface(cif) == IFT_PPPOE; - fprintf(ip, "link set dev %s%s", ifname, pppd_owned ? "" : " down"); + /* A wds-link port is raised by hostapd when its station associates + * and must not be bounced for a change of its settings, the + * station would not notice and the port would stay down. */ + wds = iftype_from_iface(cif) == IFT_WIFI && wifi_get_mode(cif) == wifi_wds; + if (pppd_owned || (wds && lydx_is_enabled(cif, "enabled"))) + fprintf(ip, "link set dev %s", ifname); + else + fprintf(ip, "link set dev %s down", ifname); /* Set generic link attributes */ err = err ? : netdag_gen_ipv4_autoconf(net, cif, dif); @@ -769,8 +771,8 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net attr = lydx_get_cattr(cif, "description"); fprintf(ip, "link set alias \"%s\" dev %s\n", attr ?: "", ifname); - /* Bring interface back up, if enabled */ - if (lydx_is_enabled(cif, "enabled") && !pppd_owned) + /* Bring interface back up, if enabled, unless pppd or hostapd owns it */ + if (lydx_is_enabled(cif, "enabled") && !pppd_owned && !wds) fprintf(ip, "link set dev %s up state up\n", ifname); err = err ? : netdag_gen_sysctl(net, cif, dif); @@ -802,6 +804,7 @@ static int netdag_init_iface(struct lyd_node *cif) case IFT_VETH: return veth_add_deps(cif); case IFT_WIFI: + return wifi_add_deps(cif); case IFT_DUMMY: case IFT_ETH: case IFT_GRE: diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index aa7743f39..fc8b0f4df 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -133,13 +133,17 @@ typedef enum wifi_mode_t { wifi_station, wifi_ap, wifi_mesh, + wifi_wds, wifi_unknown } wifi_mode_t; int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif); +int wifi_add_deps(struct lyd_node *cif); int wifi_add_iface(struct lyd_node *cif, struct dagger *net); int wifi_del_iface(struct lyd_node *dif, struct dagger *net); int wifi_mode_changed(struct lyd_node *wifi); +int wifi_gen_settings(sr_session_ctx_t *session, struct lyd_node *dif, + struct lyd_node *cif, struct dagger *net); int wifi_gen_station(struct lyd_node *cif); int wifi_gen_mesh(struct lyd_node *cif); wifi_mode_t wifi_get_mode(struct lyd_node *wifi); diff --git a/src/confd/src/system-software.c b/src/confd/src/system-software.c index a714944c5..f184a283a 100644 --- a/src/confd/src/system-software.c +++ b/src/confd/src/system-software.c @@ -93,13 +93,6 @@ static int infix_system_sw_set_boot_order(sr_session_ctx_t *session, uint32_t su return SR_ERR_OK; } -static int rpc_failed(sr_session_ctx_t *session, const char *msg) -{ - sr_session_set_netconf_error(session, "application", "operation-failed", - NULL, NULL, msg, 0); - return SR_ERR_OPERATION_FAILED; -} - /* Append output leaf PATH/LEAF from the state file, skipped when absent. */ static int add_output(sr_val_t **output, size_t *cnt, const char *path, const char *leaf, json_t *val) diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index bad5753a0..f6a55600f 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -27,7 +27,7 @@ MODULES=( "infix-syslog@2026-09-24.yang" "iana-hardware@2018-03-13.yang" "ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor" - "infix-hardware@2026-10-04.yang" + "infix-hardware@2026-10-06.yang" "ieee802-dot1q-types@2022-10-29.yang" "infix-ip@2026-04-28.yang" "infix-if-type@2026-10-03.yang" diff --git a/src/confd/yang/confd/infix-hardware.yang b/src/confd/yang/confd/infix-hardware.yang index a35c38c23..44a52c98a 100644 --- a/src/confd/yang/confd/infix-hardware.yang +++ b/src/confd/yang/confd/infix-hardware.yang @@ -21,6 +21,18 @@ module infix-hardware { contact "kernelkit@googlegroups.com"; description "Vital Product Data augmentation of ieee-hardware and deviations."; + revision 2026-10-06 { + description "Move the WiFi country code from each radio to the box-wide + hardware/wifi container, the regulatory domain is one setting."; + reference "internal"; + } + + revision 2026-10-05 { + description "Replace the WiFi radio survey container with the channel-survey + action, survey data is collected on request only."; + reference "internal"; + } + revision 2026-10-04 { description "Add locate RPC, blinks LEDs to identify the chassis."; reference "internal"; @@ -203,6 +215,34 @@ module infix-hardware { deviation "/iehw:hardware/iehw:component/iehw:asset-id" { deviate not-supported; } + augment "/iehw:hardware" { + description + "Settings shared by every WiFi radio in the system."; + + container wifi { + if-feature wifi; + description + "WiFi settings that apply to all radios."; + + leaf country-code { + type iwcc:country-code; + default "00"; + description + "Two-letter ISO 3166-1 country code, the regulatory domain for + every WiFi radio in the system. It decides which channels + and transmit power levels the radios may use. + + The default '00' is the world domain: no 6 GHz, the 5 GHz + band listen-only, and no access point or mesh point can be + configured. + + Examples: 'US', 'DE', 'JP'. + + WARNING: Incorrect values may violate local laws and regulations."; + } + } + } + augment "/iehw:hardware/iehw:component" { leaf phys-address { type yang:phys-address; @@ -291,22 +331,6 @@ module infix-hardware { a WiFi radio (class 'ih:wifi'). WiFi radios are physical devices that can host multiple virtual WiFi interfaces (APs or Stations)."; - leaf country-code { - type iwcc:country-code; - mandatory true; - description - "Two-letter ISO 3166-1 country code for regulatory compliance. - - Sets the regulatory domain for this radio, determining: - - Allowed channels and frequencies - - Maximum transmit power - - DFS (Dynamic Frequency Selection) requirements - - Examples: 'US', 'DE', 'JP'. - - WARNING: Incorrect values may violate local laws and regulations."; - } - leaf channel { type union { type uint16 { @@ -327,7 +351,7 @@ module infix-hardware { Channel availability depends on: - Configured band (2.4/5/6 GHz) - - Regulatory domain (country-code) + - Regulatory domain (hardware wifi country-code) - Hardware capabilities Common channels: @@ -444,7 +468,7 @@ module infix-hardware { Channels depend on: - Hardware capabilities - - Configured country-code + - The system's WiFi country code - Band selection This list reflects actual usable channels after applying @@ -533,98 +557,108 @@ module infix-hardware { } /* - * Channel survey data (operational state) + * Channel survey, on demand */ - container survey { - config false; + action channel-survey { description - "WiFi channel survey data providing channel utilization - and interference information. - - This data is collected from the WiFi driver and provides - insights into channel occupancy, noise levels, and RF activity."; - - list channel { - key "frequency"; - description - "Per-channel survey information. + "Scan all channels the radio supports and report how busy each + one is. - Includes utilization metrics for all channels scanned by - the radio, not just the currently active channel."; + The radio leaves its operating channel for the duration of + the scan, a few seconds, so traffic on the radio pauses. + Survey data is not collected in the background, this action + is the only way to get it."; - leaf frequency { - type uint32; - units "MHz"; + input { + leaf passive { + type boolean; + default false; description - "Channel center frequency in MHz. - - Examples: - - 2412 MHz (2.4 GHz channel 1) - - 5180 MHz (5 GHz channel 36) - - 5955 MHz (6 GHz channel 1)"; + "Listen only, do not send probe requests. A passive scan + dwells longer on each channel and gives a better busy-time + sample, at the cost of a longer pause in traffic."; } + } - leaf in-use { - type boolean; + output { + list channel { + key "frequency"; description - "Whether this channel is currently in use by the radio. + "Survey of one channel. Channel utilization is + busy-time / active-time."; + + leaf frequency { + type uint32; + units "MHz"; + description + "Channel center frequency in MHz. + + Examples: + - 2412 MHz (2.4 GHz channel 1) + - 5180 MHz (5 GHz channel 36) + - 5955 MHz (6 GHz channel 1)"; + } - Only one channel will have this set to true at a time."; - } + leaf in-use { + type boolean; + description + "Whether this is the radio's operating channel."; + } - leaf noise { - type int16; - units "dBm"; - description - "Background noise level on this channel in dBm. + leaf noise { + type int16; + units "dBm"; + description + "Background noise level on this channel in dBm. - Lower (more negative) values indicate cleaner RF environment. + Lower (more negative) values indicate cleaner RF environment. - Typical values: - - -95 to -100 dBm: Very low noise (excellent) - - -85 to -95 dBm: Low noise (good) - - -75 to -85 dBm: Moderate noise - - -65 to -75 dBm: High noise (congested)"; - } + Typical values: + - -95 to -100 dBm: Very low noise (excellent) + - -85 to -95 dBm: Low noise (good) + - -75 to -85 dBm: Moderate noise + - -65 to -75 dBm: High noise (congested)"; + } - leaf active-time { - type uint32; - units "milliseconds"; - description - "Total time the radio was active on this channel. + leaf active-time { + type uint32; + units "milliseconds"; + description + "Total time the radio was active on this channel. - This is the survey measurement period for this channel."; - } + This is the survey measurement period for this channel."; + } - leaf busy-time { - type uint32; - units "milliseconds"; - description - "Time the channel was detected as busy. + leaf busy-time { + type uint32; + units "milliseconds"; + description + "Time the channel was detected as busy. - Includes time spent receiving frames, transmitting frames, - and time the channel was busy due to other sources. + Includes time spent receiving frames, transmitting frames, + and time the channel was busy due to other sources. - Channel utilization = (busy-time / active-time) * 100%"; - } + Channel utilization = (busy-time / active-time) * 100%"; + } - leaf receive-time { - type uint32; - units "milliseconds"; - description - "Time spent receiving frames on this channel. + leaf receive-time { + type uint32; + units "milliseconds"; + description + "Time spent receiving frames on this channel. - Subset of busy-time spent on frame reception."; - } + Subset of busy-time spent on frame reception."; + } - leaf transmit-time { - type uint32; - units "milliseconds"; - description - "Time spent transmitting frames on this channel. + leaf transmit-time { + type uint32; + units "milliseconds"; + description + "Time spent transmitting frames on this channel. - Subset of busy-time spent on frame transmission."; + Subset of busy-time spent on frame transmission."; + } } } } diff --git a/src/confd/yang/confd/infix-hardware@2026-10-04.yang b/src/confd/yang/confd/infix-hardware@2026-10-06.yang similarity index 100% rename from src/confd/yang/confd/infix-hardware@2026-10-04.yang rename to src/confd/yang/confd/infix-hardware@2026-10-06.yang diff --git a/src/confd/yang/confd/infix-if-bridge.yang b/src/confd/yang/confd/infix-if-bridge.yang index 7c5eff40b..5b3bba7db 100644 --- a/src/confd/yang/confd/infix-if-bridge.yang +++ b/src/confd/yang/confd/infix-if-bridge.yang @@ -29,6 +29,12 @@ submodule infix-if-bridge { contact "kernelkit@googlegroups.com"; description "Linux bridge extension for ietf-interfaces."; + revision 2026-10-02 { + description + "Allow WDS link interfaces and 4-address stations as bridge ports."; + reference "internal"; + } + revision 2026-04-29 { description "Add operational state for multicast router ports per bridge."; reference "internal"; @@ -939,8 +945,8 @@ submodule infix-if-bridge { must "not(../ip:ipv4/ip:address or ../ip:ipv6/ip:address)" { error-message "Bridge ports cannot have IP addresses configured."; } - must "not(derived-from-or-self(../if:type, 'infix-ift:wifi')) or ../infix-if:wifi/infix-if:access-point or ../infix-if:wifi/infix-if:mesh-point" { - error-message "WiFi interfaces can only be bridge ports when configured as Access Points or Mesh Points."; + must "not(derived-from-or-self(../if:type, 'infix-ift:wifi')) or ../infix-if:wifi/infix-if:access-point or ../infix-if:wifi/infix-if:mesh-point or ../infix-if:wifi/infix-if:wds-link or ../infix-if:wifi/infix-if:station/infix-if:wds = 'true'" { + error-message "WiFi interfaces can only be bridge ports as access point, mesh point, WDS link, or station with wds enabled."; } description "Bridge association and port specific settings."; uses bridge-port-common; diff --git a/src/confd/yang/confd/infix-if-bridge@2026-04-29.yang b/src/confd/yang/confd/infix-if-bridge@2026-10-02.yang similarity index 100% rename from src/confd/yang/confd/infix-if-bridge@2026-04-29.yang rename to src/confd/yang/confd/infix-if-bridge@2026-10-02.yang diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 078e50204..9ced95415 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -43,11 +43,28 @@ submodule infix-if-wifi { interfaces provide network-layer configuration (SSID, security). Key features: - - Dual mode support: AP and Station + - Modes: Access Point, Station, 802.11s Mesh Point and 4-address + (WDS) link - Multi-SSID: Multiple APs on same radio - Security: WPA2/WPA3 with keystore integration - Operational state: Connection status, RSSI, client lists"; + revision 2026-10-06 { + description + "Access point and mesh point require the box-wide WiFi country code, + the per-radio country-code leaf is gone. 802.11r gets a mandatory + key-holder-secret, the key the access points of a mobility domain + hand each other a client's keys with."; + reference "internal"; + } + + revision 2026-10-02 { + description + "Add 4-address (WDS) support: wds-link mode for access point side + ports, and the station leaves 'wds' and 'peer-bssid'."; + reference "internal"; + } + revision 2026-09-24 { description "Constrain the character set of mesh-id and nas-identifier."; @@ -132,18 +149,21 @@ submodule infix-if-wifi { The interface must reference a radio defined in infix-wifi-radio module, which provides the physical layer configuration."; + must "radio or wds-link" { + error-message "A WiFi interface must reference a radio"; + } + leaf radio { type leafref { path "/iehw:hardware/iehw:component/iehw:name"; } - mandatory true; must "derived-from-or-self(/iehw:hardware/iehw:component[iehw:name=current()]/iehw:class, 'ih:wifi')" { error-message "Referenced hardware component must be a WiFi radio (class 'ih:wifi')"; } - must "count(/if:interfaces/if:interface[wifi/radio = current()][not(wifi/access-point)]) <= 1" { + must "count(/if:interfaces/if:interface[wifi/radio = current()][not(wifi/access-point)][not(wifi/wds-link)]) <= 1" { error-message "Only one station or scan interface is allowed per radio"; } - must "count(/if:interfaces/if:interface[wifi/radio = current()][not(infix-if:custom-phys-address/*)]) <= 1" { + must "count(/if:interfaces/if:interface[wifi/radio = current()][not(infix-if:custom-phys-address/*)][not(wifi/wds-link)]) <= 1" { error-message "Only one interface per radio can use the default MAC address. Configure custom-phys-address on additional interfaces."; } @@ -152,7 +172,8 @@ submodule infix-if-wifi { References a hardware component with class 'ih:wifi'. The radio must exist and be configured before creating - virtual interfaces. + virtual interfaces. Not set for a WDS link, which uses + the radio of its access point. Example: 'phy0' for the first WiFi radio. @@ -171,9 +192,11 @@ submodule infix-if-wifi { - Station mode: Connect to an existing WiFi network - Access Point mode: Create a WiFi network for clients - Mesh Point mode: Create an 802.11s mesh link + - WDS link: Bridge port for one 4-address station on a local + access point Note: A radio can host either: - - Multiple AP interfaces (multi-SSID), OR + - Multiple AP interfaces (multi-SSID), with WDS links, OR - A single Station interface, OR - A single Mesh Point interface @@ -193,6 +216,28 @@ submodule infix-if-wifi { Example use case: Connect to upstream WiFi network."; + leaf wds { + type boolean; + default false; + description + "4-address (WDS) mode. + + Lets the station forward frames for other devices, which + is what allows it to be a bridge port. Use it to bridge + the station with wired ports or a local access point, as + in a repeater. The access point must accept 4-address + stations, see the wds-link mode."; + } + + leaf peer-bssid { + type yang:mac-address; + description + "Only associate to the access point with this BSSID. + + Without it the station picks any access point advertising + the SSID."; + } + leaf ssid { type string { length "1..32"; @@ -375,8 +420,8 @@ submodule infix-if-wifi { error-message "Parent radio must have 'channel' configured for Access Point mode"; } - must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:country-code != '00'" { - error-message "Country code '00' (world regulatory domain) is not allowed for Access Point mode. Please configure a specific country code on the radio."; + must "/iehw:hardware/ih:wifi/ih:country-code != '00'" { + error-message "Set the WiFi country code (hardware wifi country-code) before configuring an access point, the world domain '00' is not allowed."; } leaf ssid { @@ -508,6 +553,20 @@ submodule infix-if-wifi { Reduces handoff latency from ~1s to <50ms through pre-authentication. Required for seamless roaming."; + leaf key-holder-secret { + type ks:central-symmetric-key-ref; + mandatory true; + description + "Secret the access points of the mobility domain use + to hand each other a roaming client's keys. + + References a symmetric key in the keystore. Set the + same key on every device with an access point in the + mobility domain. Give it a value of its own: whoever + holds this secret can read every client's keys, so it + must not be the WiFi password the clients know."; + } + leaf mobility-domain { type mobility-domain; default "4f57"; @@ -565,6 +624,12 @@ submodule infix-if-wifi { encouraging dual-band devices to prefer 5GHz over 2.4GHz when signal quality permits. + Acts on a 2.4GHz access point whose SSID is also + served on 5 or 6GHz by this device: it defers + dual-band clients to that access point, and moves + the ones already connected. On a 5 or 6GHz + access point the setting has no effect. + MBO steers clients via 802.11v BSS Transition Management, hence its placement under dot11v; enabling dot11v enables band steering by default. @@ -682,8 +747,8 @@ submodule infix-if-wifi { error-message "Parent radio must have 'channel' configured for mesh mode"; } - must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:country-code != '00'" { - error-message "Country code '00' is not allowed for mesh mode."; + must "/iehw:hardware/ih:wifi/ih:country-code != '00'" { + error-message "Set the WiFi country code (hardware wifi country-code) before configuring a mesh point, the world domain '00' is not allowed."; } must "not(/if:interfaces/if:interface[wifi/access-point][wifi/radio = current()/../radio])" { @@ -811,6 +876,97 @@ submodule infix-if-wifi { } } } + + case wds-link { + container wds-link { + presence "Configure a 4-address WDS port of a local access point"; + + description + "Bridge port for one 4-address (WDS) station on an access + point of this device. + + The access point binds the station with the given MAC + address to this interface. Add the interface to the access + point's bridge and configure VLANs like for any other port; + one wds-link per repeater or remote bridge. The port is up + while the station is associated. + + Give the access point an SSID and secret of its own, for + the WDS links only: a station that knows them and uses + the MAC address below is bound to this port, with every + VLAN the port carries. + + The interface uses the access point's radio and MAC + address."; + + must "not(../../custom-phys-address/*)" { + error-message "A wds-link uses the MAC address of its access point"; + } + + must "not(../radio)" { + error-message "A wds-link uses the radio of its access point, do not set radio"; + } + + leaf access-point { + type leafref { + path "/if:interfaces/if:interface/if:name"; + } + mandatory true; + must "/if:interfaces/if:interface[if:name = current()]/wifi/access-point" { + error-message "wds-link must reference an access point interface on this device"; + } + description + "Access point interface this port belongs to."; + } + + leaf peer-address { + type yang:mac-address; + mandatory true; + must "count(/if:interfaces/if:interface[wifi/wds-link/access-point = current()/../access-point][wifi/wds-link/peer-address = current()]) = 1" { + error-message "Only one wds-link per station on an access point"; + } + description + "MAC address of the 4-address station bound to this port."; + } + + /* Operational state */ + + leaf connected { + config false; + type boolean; + description + "True while the station is associated and bound to this + port."; + } + + leaf signal-strength { + config false; + type int16; + units "dBm"; + description + "Signal strength of the station in dBm. Only present + while connected."; + } + + leaf rx-speed { + config false; + type uint32; + units "100 kbps"; + description + "Last received data rate from the station in 100 kbps. + Only present while connected."; + } + + leaf tx-speed { + config false; + type uint32; + units "100 kbps"; + description + "Last transmitted data rate to the station in 100 kbps. + Only present while connected."; + } + } + } } } } diff --git a/src/confd/yang/confd/infix-if-wifi@2026-09-24.yang b/src/confd/yang/confd/infix-if-wifi@2026-10-06.yang similarity index 100% rename from src/confd/yang/confd/infix-if-wifi@2026-09-24.yang rename to src/confd/yang/confd/infix-if-wifi@2026-10-06.yang diff --git a/src/confd/yang/confd/infix-wifi-country-codes.yang b/src/confd/yang/confd/infix-wifi-country-codes.yang index 9c797e42a..5b6d01f74 100644 --- a/src/confd/yang/confd/infix-wifi-country-codes.yang +++ b/src/confd/yang/confd/infix-wifi-country-codes.yang @@ -1,6 +1,6 @@ module infix-wifi-country-codes { yang-version 1.1; - namespace "urn:infix:wifi-country-codes"; + namespace "urn:infix:wifi-country-codes:ns:yang:1.0"; prefix iwcc; organization "KernelKit"; @@ -8,46 +8,48 @@ module infix-wifi-country-codes { contact "kernelkit@googlegroups.com"; description - "This module defines country codes for WiFi regulatory domain - configuration based on ISO 3166-1 alpha-2 standard. + "Country codes for the WiFi regulatory domain, ISO 3166-1 alpha-2, + limited to the countries the Linux wireless regulatory database + has rules for."; - This model provides country code definitions for use in - 802.11 wireless LAN regulatory compliance configuration. + revision 2026-10-06 { + description + "Limit the list to the countries the Linux regulatory database has + rules for, a code without rules is silently ignored by the kernel + and leaves the radios in the world domain."; + reference + "wireless-regdb 2026.09.03"; + } - The regulatory domain configuration follows the principles - established in IETF RFCs for wireless access point management."; revision 2025-11-28 { description "Add support for 00 - World regulatory domain."; reference - "Internal"; - } + "Internal"; + } + revision 2025-06-02 { description "Initial revision for WiFi country code support."; reference - "RFC 5415: Control And Provisioning of Wireless Access Points (CAPWAP) Protocol Specification - RFC 5416: Control and Provisioning of Wireless Access Points (CAPWAP) Protocol Binding for IEEE 802.11"; + "ISO 3166-1:2020 Codes for the representation of names of countries + and their subdivisions -- Part 1: Country codes"; } typedef country-code { type enumeration { - enum "00" { description "World regulatory domain (no country restrictions)"; } + enum "00" { description "World regulatory domain, the most restrictive one: no 6 GHz, listen-only on 5 GHz, no access point"; } enum "AD" { description "Andorra"; } enum "AE" { description "United Arab Emirates"; } enum "AF" { description "Afghanistan"; } - enum "AG" { description "Antigua and Barbuda"; } enum "AI" { description "Anguilla"; } enum "AL" { description "Albania"; } enum "AM" { description "Armenia"; } - enum "AO" { description "Angola"; } - enum "AQ" { description "Antarctica"; } enum "AR" { description "Argentina"; } enum "AS" { description "American Samoa"; } enum "AT" { description "Austria"; } enum "AU" { description "Australia"; } enum "AW" { description "Aruba"; } - enum "AX" { description "Åland Islands"; } enum "AZ" { description "Azerbaijan"; } enum "BA" { description "Bosnia and Herzegovina"; } enum "BB" { description "Barbados"; } @@ -56,41 +58,29 @@ module infix-wifi-country-codes { enum "BF" { description "Burkina Faso"; } enum "BG" { description "Bulgaria"; } enum "BH" { description "Bahrain"; } - enum "BI" { description "Burundi"; } - enum "BJ" { description "Benin"; } enum "BL" { description "Saint Barthélemy"; } enum "BM" { description "Bermuda"; } enum "BN" { description "Brunei Darussalam"; } enum "BO" { description "Bolivia"; } - enum "BQ" { description "Bonaire, Sint Eustatius and Saba"; } enum "BR" { description "Brazil"; } enum "BS" { description "Bahamas"; } enum "BT" { description "Bhutan"; } - enum "BV" { description "Bouvet Island"; } enum "BW" { description "Botswana"; } enum "BY" { description "Belarus"; } enum "BZ" { description "Belize"; } enum "CA" { description "Canada"; } - enum "CC" { description "Cocos (Keeling) Islands"; } - enum "CD" { description "Congo, Democratic Republic of the"; } enum "CF" { description "Central African Republic"; } - enum "CG" { description "Congo"; } enum "CH" { description "Switzerland"; } enum "CI" { description "Côte d'Ivoire"; } - enum "CK" { description "Cook Islands"; } enum "CL" { description "Chile"; } - enum "CM" { description "Cameroon"; } enum "CN" { description "China"; } enum "CO" { description "Colombia"; } enum "CR" { description "Costa Rica"; } enum "CU" { description "Cuba"; } - enum "CV" { description "Cabo Verde"; } - enum "CW" { description "Curaçao"; } enum "CX" { description "Christmas Island"; } enum "CY" { description "Cyprus"; } enum "CZ" { description "Czechia"; } enum "DE" { description "Germany"; } - enum "DJ" { description "Djibouti"; } enum "DK" { description "Denmark"; } enum "DM" { description "Dominica"; } enum "DO" { description "Dominican Republic"; } @@ -98,37 +88,25 @@ module infix-wifi-country-codes { enum "EC" { description "Ecuador"; } enum "EE" { description "Estonia"; } enum "EG" { description "Egypt"; } - enum "EH" { description "Western Sahara"; } - enum "ER" { description "Eritrea"; } enum "ES" { description "Spain"; } enum "ET" { description "Ethiopia"; } enum "FI" { description "Finland"; } - enum "FJ" { description "Fiji"; } - enum "FK" { description "Falkland Islands (Malvinas)"; } enum "FM" { description "Micronesia"; } enum "FO" { description "Faroe Islands"; } enum "FR" { description "France"; } - enum "GA" { description "Gabon"; } enum "GB" { description "United Kingdom"; } enum "GD" { description "Grenada"; } enum "GE" { description "Georgia"; } enum "GF" { description "French Guiana"; } - enum "GG" { description "Guernsey"; } enum "GH" { description "Ghana"; } enum "GI" { description "Gibraltar"; } enum "GL" { description "Greenland"; } - enum "GM" { description "Gambia"; } - enum "GN" { description "Guinea"; } enum "GP" { description "Guadeloupe"; } - enum "GQ" { description "Equatorial Guinea"; } enum "GR" { description "Greece"; } - enum "GS" { description "South Georgia and the South Sandwich Islands"; } enum "GT" { description "Guatemala"; } enum "GU" { description "Guam"; } - enum "GW" { description "Guinea-Bissau"; } enum "GY" { description "Guyana"; } enum "HK" { description "Hong Kong"; } - enum "HM" { description "Heard Island and McDonald Islands"; } enum "HN" { description "Honduras"; } enum "HR" { description "Croatia"; } enum "HT" { description "Haiti"; } @@ -138,71 +116,52 @@ module infix-wifi-country-codes { enum "IL" { description "Israel"; } enum "IM" { description "Isle of Man"; } enum "IN" { description "India"; } - enum "IO" { description "British Indian Ocean Territory"; } - enum "IQ" { description "Iraq"; } enum "IR" { description "Iran"; } enum "IS" { description "Iceland"; } enum "IT" { description "Italy"; } - enum "JE" { description "Jersey"; } enum "JM" { description "Jamaica"; } enum "JO" { description "Jordan"; } enum "JP" { description "Japan"; } enum "KE" { description "Kenya"; } - enum "KG" { description "Kyrgyzstan"; } enum "KH" { description "Cambodia"; } - enum "KI" { description "Kiribati"; } - enum "KM" { description "Comoros"; } enum "KN" { description "Saint Kitts and Nevis"; } enum "KP" { description "Korea, Democratic People's Republic of"; } enum "KR" { description "Korea, Republic of"; } enum "KW" { description "Kuwait"; } enum "KY" { description "Cayman Islands"; } enum "KZ" { description "Kazakhstan"; } - enum "LA" { description "Lao People's Democratic Republic"; } enum "LB" { description "Lebanon"; } enum "LC" { description "Saint Lucia"; } enum "LI" { description "Liechtenstein"; } enum "LK" { description "Sri Lanka"; } - enum "LR" { description "Liberia"; } enum "LS" { description "Lesotho"; } enum "LT" { description "Lithuania"; } enum "LU" { description "Luxembourg"; } enum "LV" { description "Latvia"; } - enum "LY" { description "Libya"; } enum "MA" { description "Morocco"; } enum "MC" { description "Monaco"; } enum "MD" { description "Moldova"; } enum "ME" { description "Montenegro"; } enum "MF" { description "Saint Martin (French part)"; } - enum "MG" { description "Madagascar"; } enum "MH" { description "Marshall Islands"; } enum "MK" { description "North Macedonia"; } - enum "ML" { description "Mali"; } - enum "MM" { description "Myanmar"; } enum "MN" { description "Mongolia"; } enum "MO" { description "Macao"; } enum "MP" { description "Northern Mariana Islands"; } enum "MQ" { description "Martinique"; } enum "MR" { description "Mauritania"; } - enum "MS" { description "Montserrat"; } enum "MT" { description "Malta"; } enum "MU" { description "Mauritius"; } enum "MV" { description "Maldives"; } enum "MW" { description "Malawi"; } enum "MX" { description "Mexico"; } enum "MY" { description "Malaysia"; } - enum "MZ" { description "Mozambique"; } enum "NA" { description "Namibia"; } - enum "NC" { description "New Caledonia"; } - enum "NE" { description "Niger"; } - enum "NF" { description "Norfolk Island"; } enum "NG" { description "Nigeria"; } enum "NI" { description "Nicaragua"; } enum "NL" { description "Netherlands"; } enum "NO" { description "Norway"; } enum "NP" { description "Nepal"; } - enum "NR" { description "Nauru"; } - enum "NU" { description "Niue"; } enum "NZ" { description "New Zealand"; } enum "OM" { description "Oman"; } enum "PA" { description "Panama"; } @@ -213,9 +172,7 @@ module infix-wifi-country-codes { enum "PK" { description "Pakistan"; } enum "PL" { description "Poland"; } enum "PM" { description "Saint Pierre and Miquelon"; } - enum "PN" { description "Pitcairn"; } enum "PR" { description "Puerto Rico"; } - enum "PS" { description "Palestine, State of"; } enum "PT" { description "Portugal"; } enum "PW" { description "Palau"; } enum "PY" { description "Paraguay"; } @@ -226,52 +183,33 @@ module infix-wifi-country-codes { enum "RU" { description "Russian Federation"; } enum "RW" { description "Rwanda"; } enum "SA" { description "Saudi Arabia"; } - enum "SB" { description "Solomon Islands"; } - enum "SC" { description "Seychelles"; } - enum "SD" { description "Sudan"; } enum "SE" { description "Sweden"; } enum "SG" { description "Singapore"; } - enum "SH" { description "Saint Helena, Ascension and Tristan da Cunha"; } enum "SI" { description "Slovenia"; } - enum "SJ" { description "Svalbard and Jan Mayen"; } enum "SK" { description "Slovakia"; } - enum "SL" { description "Sierra Leone"; } enum "SM" { description "San Marino"; } enum "SN" { description "Senegal"; } - enum "SO" { description "Somalia"; } enum "SR" { description "Suriname"; } - enum "SS" { description "South Sudan"; } - enum "ST" { description "Sao Tome and Principe"; } enum "SV" { description "El Salvador"; } enum "SX" { description "Sint Maarten (Dutch part)"; } enum "SY" { description "Syrian Arab Republic"; } - enum "SZ" { description "Eswatini"; } enum "TC" { description "Turks and Caicos Islands"; } enum "TD" { description "Chad"; } - enum "TF" { description "French Southern Territories"; } enum "TG" { description "Togo"; } enum "TH" { description "Thailand"; } - enum "TJ" { description "Tajikistan"; } - enum "TK" { description "Tokelau"; } - enum "TL" { description "Timor-Leste"; } - enum "TM" { description "Turkmenistan"; } enum "TN" { description "Tunisia"; } - enum "TO" { description "Tonga"; } enum "TR" { description "Turkey"; } enum "TT" { description "Trinidad and Tobago"; } - enum "TV" { description "Tuvalu"; } enum "TW" { description "Taiwan"; } enum "TZ" { description "Tanzania"; } enum "UA" { description "Ukraine"; } enum "UG" { description "Uganda"; } - enum "UM" { description "United States Minor Outlying Islands"; } enum "US" { description "United States of America"; } enum "UY" { description "Uruguay"; } enum "UZ" { description "Uzbekistan"; } enum "VA" { description "Holy See (Vatican City State)"; } enum "VC" { description "Saint Vincent and the Grenadines"; } enum "VE" { description "Venezuela"; } - enum "VG" { description "Virgin Islands, British"; } enum "VI" { description "Virgin Islands, U.S."; } enum "VN" { description "Viet Nam"; } enum "VU" { description "Vanuatu"; } @@ -280,14 +218,15 @@ module infix-wifi-country-codes { enum "YE" { description "Yemen"; } enum "YT" { description "Mayotte"; } enum "ZA" { description "South Africa"; } - enum "ZM" { description "Zambia"; } enum "ZW" { description "Zimbabwe"; } } description - "Complete list of ISO 3166-1 alpha-2 country codes for - regulatory domain configuration."; + "ISO 3166-1 alpha-2 country codes for the WiFi regulatory domain, + limited to the countries the Linux wireless regulatory database + has rules for, plus '00' for the world domain."; reference "ISO 3166-1:2020 Codes for the representation of names of countries - and their subdivisions -- Part 1: Country codes"; + and their subdivisions -- Part 1: Country codes + https://git.kernel.org/pub/scm/linux/kernel/git/wens/wireless-regdb.git"; } } diff --git a/src/confd/yang/confd/infix-wifi-country-codes@2025-11-28.yang b/src/confd/yang/confd/infix-wifi-country-codes@2026-10-06.yang similarity index 100% rename from src/confd/yang/confd/infix-wifi-country-codes@2025-11-28.yang rename to src/confd/yang/confd/infix-wifi-country-codes@2026-10-06.yang diff --git a/src/klish-plugin-infix/src/infix.c b/src/klish-plugin-infix/src/infix.c index c301f3678..dd58c012d 100644 --- a/src/klish-plugin-infix/src/infix.c +++ b/src/klish-plugin-infix/src/infix.c @@ -353,6 +353,13 @@ int infix_ifaces(kcontext_t *ctx) return 0; } +int infix_wifi_radios(kcontext_t *ctx) +{ + (void)ctx; + system("ls /sys/class/ieee80211/ 2>/dev/null"); + return 0; +} + /* Note: uses shellf() for pipes, but all arguments are hardcoded by callers */ static int firewall_dbus_completion(const char *interface, const char *method, const char *parser) { @@ -954,6 +961,7 @@ int kplugin_infix_init(kcontext_t *ctx) kplugin_add_syms(plugin, ksym_new("path", infix_path)); kplugin_add_syms(plugin, ksym_new("rename", infix_rename)); kplugin_add_syms(plugin, ksym_new("ifaces", infix_ifaces)); + kplugin_add_syms(plugin, ksym_new("wifi_radios", infix_wifi_radios)); kplugin_add_syms(plugin, ksym_new("users", infix_users)); kplugin_add_syms(plugin, ksym_new("groups", infix_groups)); kplugin_add_syms(plugin, ksym_new("sym_keys", infix_sym_keys)); diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index 1f897bf5d..bcca3206e 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -122,6 +122,13 @@ + + + + + + + @@ -684,9 +691,25 @@ echo "Public: $pub" show mdns - + + + + + + show hardware "$KLISH_PARAM_component" survey |pager + + + + + show hardware "$KLISH_PARAM_component" survey passive |pager + + + + + + - show hardware |pager + show hardware ${KLISH_PARAM_component:+"$KLISH_PARAM_component"} |pager diff --git a/src/onieprom/onieprom/__main__.py b/src/onieprom/onieprom/__main__.py index f3d581b4d..b08206d6e 100644 --- a/src/onieprom/onieprom/__main__.py +++ b/src/onieprom/onieprom/__main__.py @@ -4,6 +4,8 @@ def main(): import os import sys + from onieprom import into_tlv, from_tlv + parser = argparse.ArgumentParser(prog='onieprom') parser.add_argument("infile", nargs="?", default=sys.stdin, type=argparse.FileType("rb", 0)) @@ -30,9 +32,15 @@ def main(): sys.exit(1) if args.encode: - args.outfile.buffer.write(into_tlv(json.load(args.infile))) + out = getattr(args.outfile, "buffer", args.outfile) + out.write(into_tlv(json.load(args.infile))) else: - args.outfile.write(json.dumps(from_tlv(args.infile))) + out = args.outfile + data = json.dumps(from_tlv(args.infile)) + if hasattr(out, "buffer"): + out.write(data) + else: + out.write(data.encode()) if __name__ == "__main__": main() diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index ad661b556..d4eff3fde 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -1457,6 +1457,16 @@ def pr_proto_wifi(self, pipe=''): peers_data = mesh.get("peers", {}) peers = peers_data.get("peer", []) data_str = f"{mode}, mesh-id: {mesh_id}, peers: {len(peers)}" + elif "wds-link" in self.wifi: + wds = self.wifi["wds-link"] + mode = "WDS" + signal = wds.get("signal-strength") + if wds.get("connected") and signal is not None: + data_str = f"{mode}, connected, signal: {signal_to_status(signal)}" + elif wds.get("connected"): + data_str = f"{mode}, connected" + else: + data_str = f"{mode}, not connected" else: station=self.wifi.get("station", {}) ssid = station.get("ssid", "------") @@ -1764,6 +1774,19 @@ def _addr_lines(addrs): print(f"{'mesh-id':<{19}}: {mesh_id}") print(f"{'connected peers':<{19}}: {len(peers)}") self.pr_wifi_peers() + elif "wds-link" in self.wifi: + wds = self.wifi['wds-link'] + signal = wds.get('signal-strength') + print(f"{'mode':<{19}}: wds-link") + print(f"{'connected':<{19}}: {'yes' if wds.get('connected') else 'no'}") + if signal is not None: + print(f"{'signal':<{19}}: {signal} dBm ({signal_to_status(signal)})") + rx_speed = wds.get('rx-speed') + tx_speed = wds.get('tx-speed') + if rx_speed is not None: + print(f"{'rx bitrate':<{19}}: {rx_speed / 10:.1f} Mbps") + if tx_speed is not None: + print(f"{'tx bitrate':<{19}}: {tx_speed / 10:.1f} Mbps") else: mode = "station" station = self.wifi.get('station', {}) diff --git a/src/statd/python/yanger/ietf_hardware.py b/src/statd/python/yanger/ietf_hardware.py index 548bd1b1a..b34be178f 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -606,39 +606,6 @@ def thermal_sensor_components(): return components -def get_survey_data(ifname): - """Get channel survey data using iw.py script""" - channels = [] - - try: - survey_data = HOST.run_json(("/usr/libexec/infix/iw.py", "survey", ifname), default=[]) - - for entry in survey_data: - channel = { - "frequency": entry.get("frequency"), - "in-use": entry.get("in_use", False) - } - - # Add optional fields if present - if "noise" in entry: - channel["noise"] = entry["noise"] - if "active_time" in entry: - channel["active-time"] = entry["active_time"] - if "busy_time" in entry: - channel["busy-time"] = entry["busy_time"] - if "receive_time" in entry: - channel["receive-time"] = entry["receive_time"] - if "transmit_time" in entry: - channel["transmit-time"] = entry["transmit_time"] - - channels.append(channel) - - except Exception: - pass - - return channels - - def get_phy_info(phy_name): """Get complete PHY information using iw.py script""" try: @@ -757,20 +724,6 @@ def wifi_radio_components(): num_ifaces = iw_info.get('num_virtual_interfaces', 0) wifi_radio_data['num-virtual-interfaces'] = num_ifaces - # Get survey data if we have an interface - iface = phy_data.get("iface") - if iface: - try: - channels = get_survey_data(iface) - - if channels: - wifi_radio_data["survey"] = { - "channel": channels - } - except Exception: - # If survey fails, continue without survey data - pass - # Add wifi-radio data to component if wifi_radio_data: component["infix-hardware:wifi-radio"] = wifi_radio_data diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py index 1cbcdbc3a..9d1635fef 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -114,6 +114,13 @@ def iplink2yang_operstate(iplink): "LOWERLAYERDOWN": "lower-layer-down", "NOTPRESENT": "not-present" } + if iplink["operstate"] == "UNKNOWN": + # Interfaces without carrier handling (AP_VLAN, tunnels) stay + # in UNKNOWN; the link flags tell the real state. + flags = iplink.get("flags", []) + if "UP" not in flags: + return "down" + return "up" if "LOWER_UP" in flags else "lower-layer-down" return xlate.get(iplink["operstate"], "unknown") diff --git a/src/statd/python/yanger/ietf_interfaces/wifi.py b/src/statd/python/yanger/ietf_interfaces/wifi.py index 06617f4bb..39615a8d1 100644 --- a/src/statd/python/yanger/ietf_interfaces/wifi.py +++ b/src/statd/python/yanger/ietf_interfaces/wifi.py @@ -28,6 +28,16 @@ def get_iw_stations(ifname): return [] +def get_iw_wds_ports(ifname): + """Get the WDS ports of an AP via iw.py""" + try: + data = HOST.run(('/usr/libexec/infix/iw.py', 'wds', ifname), default='[]') + return json.loads(data) + except Exception: + pass + return [] + + def get_iw_mesh_param(ifname): """Get mesh parameters via iw.py (mesh point mode)""" try: @@ -60,8 +70,11 @@ def wifi_ap(ifname): if info.get('ssid'): ap_data['ssid'] = info['ssid'] - # Get connected stations + # Connected stations, including the 4-address ones the kernel lists + # under the AP's WDS ports rather than under the AP itself stations = get_iw_stations(ifname) + for port in get_iw_wds_ports(ifname): + stations += get_iw_stations(port) if stations: ap_data['stations'] = {'station': stations} @@ -153,6 +166,25 @@ def wifi_station(ifname): return {'station': station_data} if station_data else {} +def wifi_wds(ifname): + """Operational data for a wds-link port (AP_VLAN). + + The station bound to the port is the only entry in its station dump, + so that is both the connected flag and the link quality. + """ + stations = get_iw_stations(ifname) + if not stations: + return {'wds-link': {'connected': False}} + + sta = stations[0] + data = {'connected': True} + for key in ('signal-strength', 'rx-speed', 'tx-speed'): + if sta.get(key) is not None: + data[key] = sta[key] + + return {'wds-link': data} + + def wifi(ifname): """Main entry point - detect mode and return appropriate data""" info = get_iw_info(ifname) @@ -165,6 +197,8 @@ def wifi(ifname): if mode == 'ap': result.update(wifi_ap(ifname)) + elif mode == 'ap/vlan': + result.update(wifi_wds(ifname)) elif mode == 'mesh point': result.update(wifi_mesh(ifname, info)) else: @@ -253,12 +287,15 @@ def extract_encryption(flags): 'auth_type': 'Unknown' } - # Extract WPA protocols - if 'WPA3' in flags: + # wpa_supplicant labels every RSN network WPA2, so WPA3 is told + # apart by its key management: SAE only is WPA3, SAE next to PSK + # is a WPA2/WPA3 transition network. + rsn = 'WPA2-' in flags + if rsn and 'SAE' in flags: encryption_info['protocols'].append('WPA3') - if 'WPA2' in flags: + if rsn and ('PSK' in flags or 'EAP' in flags): encryption_info['protocols'].append('WPA2') - if 'WPA-' in flags and 'WPA2' not in flags and 'WPA3' not in flags: + if 'WPA-' in flags and not rsn: encryption_info['protocols'].append('WPA') # Extract key management methods diff --git a/src/webui/internal/handlers/configure_hardware.go b/src/webui/internal/handlers/configure_hardware.go index bb4a4d94c..621907cfb 100644 --- a/src/webui/internal/handlers/configure_hardware.go +++ b/src/webui/internal/handlers/configure_hardware.go @@ -31,6 +31,9 @@ import ( const hwRoot = "/ietf-hardware:hardware" const hwCandPath = candidatePath + hwRoot +// hwWiFiCountryPath is the schema path of the box-wide WiFi country code. +const hwWiFiCountryPath = hwRoot + "/infix-hardware:wifi/country-code" + // hwCompCfgRow is one configured component in the main table. Per-class // fields are populated only for the matching Class. IsUSB/IsWiFi/IsGPS // spare the template from dispatching on stringly-typed Class slugs. @@ -47,18 +50,15 @@ type hwCompCfgRow struct { Unlocked bool // admin-state == "unlocked" // WiFi-specific. - CountryCode string - Channel string - Band string + Channel string + Band string // Schema descriptions carried per-row so the fold-out forms are // self-contained — Go templates can't pass extra arguments through // {{template}}. - CountryOptions []schema.IdentityOption BandOptions []schema.IdentityOption DescDescription string DescAdminState string - DescCountry string DescBand string DescChannel string } @@ -79,7 +79,10 @@ type cfgHardwarePageData struct { AvailableUSB []hwAvailable AvailableWiFi []hwAvailable AvailableGPS []hwAvailable + // Box-wide WiFi country code, one setting for every radio. + CountryCode string CountryOptions []schema.IdentityOption + DescCountry string BandOptions []schema.IdentityOption Desc map[string]string Error string @@ -112,13 +115,13 @@ func (h *ConfigureHardwareHandler) Overview(w http.ResponseWriter, r *http.Reque compPath := "/ietf-hardware:hardware/component" radioPath := compPath + "/infix-hardware:wifi-radio" data.Desc = map[string]string{ - "description": schema.DescriptionOf(mgr, compPath+"/description"), - "admin-state": schema.DescriptionOf(mgr, compPath+"/state/admin-state"), - "country-code": schema.DescriptionOf(mgr, radioPath+"/country-code"), - "channel": schema.DescriptionOf(mgr, radioPath+"/channel"), - "band": schema.DescriptionOf(mgr, radioPath+"/band"), + "description": schema.DescriptionOf(mgr, compPath+"/description"), + "admin-state": schema.DescriptionOf(mgr, compPath+"/state/admin-state"), + "channel": schema.DescriptionOf(mgr, radioPath+"/channel"), + "band": schema.DescriptionOf(mgr, radioPath+"/band"), } - data.CountryOptions = schema.OptionsFor(mgr, radioPath+"/country-code") + data.DescCountry = schema.DescriptionOf(mgr, hwWiFiCountryPath) + data.CountryOptions = schema.OptionsFor(mgr, hwWiFiCountryPath) data.BandOptions = schema.OptionsFor(mgr, radioPath+"/band") } @@ -146,6 +149,7 @@ func (h *ConfigureHardwareHandler) Overview(w http.ResponseWriter, r *http.Reque log.Printf("configure hardware: operational fetch: %v", operErr) } + data.CountryCode = cfgWrap.wifiCountryCode() configured := make(map[string]bool, len(cfgWrap.Hardware.Component)) for _, c := range cfgWrap.Hardware.Component { configured[c.Name] = true @@ -209,13 +213,10 @@ func (h *ConfigureHardwareHandler) buildRow(c hwComponentJSON, class string, dat row.Unlocked = c.State != nil && c.State.AdminState == adminStateUnlocked case classWiFi: row.IsWiFi = true - row.CountryOptions = data.CountryOptions row.BandOptions = data.BandOptions - row.DescCountry = data.Desc["country-code"] row.DescBand = data.Desc["band"] row.DescChannel = data.Desc["channel"] if c.WiFiRadio != nil { - row.CountryCode = c.WiFiRadio.CountryCode row.Band = c.WiFiRadio.Band row.Channel = wifiChannelString(c.WiFiRadio.Channel) } @@ -341,6 +342,35 @@ func (h *ConfigureHardwareHandler) CreateHardware(w http.ResponseWriter, r *http renderSavedRedirect(w, name+" added", "/configure/hardware") } +// SaveWiFiCountry writes or clears the box-wide WiFi country code. +// POST /configure/hardware/wifi +func (h *ConfigureHardwareHandler) SaveWiFiCountry(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if err := h.putWiFiCountry(r.Context(), r.FormValue("country-code")); err != nil { + log.Printf("configure hardware wifi country: %v", err) + renderSaveError(w, err) + return + } + renderSaved(w, "WiFi country code saved") +} + +// putWiFiCountry sets the box-wide country code, or removes it when +// country is empty so the radios fall back to the world domain default. +func (h *ConfigureHardwareHandler) putWiFiCountry(ctx context.Context, country string) error { + country = strings.TrimSpace(country) + path := hwCandPath + "/infix-hardware:wifi/country-code" + if country == "" { + if err := h.RC.Delete(ctx, path); err != nil && !restconf.IsNotFound(err) { + return err + } + return nil + } + return h.RC.Put(ctx, path, map[string]any{"infix-hardware:country-code": country}) +} + func (h *ConfigureHardwareHandler) putAdminState(ctx context.Context, name, state string) error { return h.RC.Put(ctx, hwComponentPath(name)+"/state/admin-state", map[string]any{"ietf-hardware:admin-state": state}) @@ -351,15 +381,12 @@ func (h *ConfigureHardwareHandler) putWiFiRadio(ctx context.Context, name string map[string]any{"infix-hardware:wifi-radio": radio}) } -// parseWiFiRadio builds the wifi-radio body from form fields. Country -// code is mandatory; band and channel are optional and only included -// when non-empty so we don't clobber YANG defaults with empty strings. +// parseWiFiRadio builds the wifi-radio body from form fields. Band and +// channel are optional and only included when non-empty so we don't +// clobber YANG defaults with empty strings; the result may be empty, +// which still creates the presence container. func parseWiFiRadio(r *http.Request) (map[string]any, error) { - country := strings.TrimSpace(r.FormValue("country-code")) - if country == "" { - return nil, fmt.Errorf("country code is required") - } - radio := map[string]any{"country-code": country} + radio := map[string]any{} if band := strings.TrimSpace(r.FormValue("band")); band != "" { radio["band"] = band } diff --git a/src/webui/internal/handlers/configure_interfaces.go b/src/webui/internal/handlers/configure_interfaces.go index 86c8e12f1..6ed5ce3ad 100644 --- a/src/webui/internal/handlers/configure_interfaces.go +++ b/src/webui/internal/handlers/configure_interfaces.go @@ -296,7 +296,8 @@ func (h *ConfigureInterfacesHandler) Overview(w http.ResponseWriter, r *http.Req "wifi-forwarding": descOr(mgr, ifPath+"/infix-interfaces:wifi/mesh-point/forwarding", "Layer-2 mesh forwarding. Leave on to let this node relay traffic for other mesh points and to bridge the mesh interface into a LAN (mesh portal). Off means only locally destined traffic is received."), "wifi-mesh-secret": descOr(mgr, ifPath+"/infix-interfaces:wifi/mesh-point/security/secret", "Pre-shared key reference for the WPA3-SAE mesh. All mesh points in the same mesh must share the same key."), "wifi-dot11k": descOr(mgr, ifPath+"/infix-interfaces:wifi/access-point/roaming/dot11k", "802.11k Radio Resource Management: neighbor and beacon reports let clients discover nearby APs before roaming."), - "wifi-dot11r": descOr(mgr, ifPath+"/infix-interfaces:wifi/access-point/roaming/dot11r", "802.11r Fast BSS Transition: pre-authentication cuts handoff time to under 50 ms. Requires WPA2/WPA3 security, plus identical SSID, passphrase and mobility domain on all APs in the group."), + "wifi-dot11r": descOr(mgr, ifPath+"/infix-interfaces:wifi/access-point/roaming/dot11r", "802.11r Fast BSS Transition: pre-authentication cuts handoff time to under 50 ms. Requires WPA2/WPA3 security, plus identical SSID, passphrase, key holder secret and mobility domain on all APs in the group."), + "wifi-dot11r-key": descOr(mgr, ifPath+"/infix-interfaces:wifi/access-point/roaming/dot11r/key-holder-secret", "Keystore key the access points of the mobility domain hand each other a roaming client's keys with. Same key on every device, and not the WiFi password: whoever holds it can read every client's keys."), "wifi-dot11r-md": descOr(mgr, ifPath+"/infix-interfaces:wifi/access-point/roaming/dot11r/mobility-domain", "802.11r mobility domain: four hex digits shared by every AP clients roam between, or 'hash' to derive it from the SSID (OpenWrt-compatible)."), "wifi-dot11r-nas": descOr(mgr, ifPath+"/infix-interfaces:wifi/access-point/roaming/dot11r/nas-identifier", "NAS-Identifier for 802.11r key lookup, unique per AP. 'auto' derives -.."), "wifi-dot11v": descOr(mgr, ifPath+"/infix-interfaces:wifi/access-point/roaming/dot11v", "802.11v BSS Transition Management: lets the AP suggest a better AP to clients (network-assisted roaming)."), @@ -351,7 +352,7 @@ func (h *ConfigureInterfacesHandler) Overview(w http.ResponseWriter, r *http.Req // by the inline "+ New radio" form in the WiFi fieldset and by // the WiFi interface row's mirrored radio editor. const radioSchemaPath = "/ietf-hardware:hardware/component/infix-hardware:wifi-radio" - data.WizardCountryOptions = schema.OptionsFor(mgr, radioSchemaPath+"/country-code") + data.WizardCountryOptions = schema.OptionsFor(mgr, hwWiFiCountryPath) data.WizardBandOptions = schema.OptionsFor(mgr, radioSchemaPath+"/band") data.CountryOptions = data.WizardCountryOptions data.BandOptions = data.WizardBandOptions @@ -481,7 +482,7 @@ func (h *ConfigureInterfacesHandler) Overview(w http.ResponseWriter, r *http.Req // Configured WiFi radios live in candidate (so the picker reflects // uncommitted edits the user is in the middle of). Available radios // = detected (operational class=wifi) - those already in candidate. - data.WizardWifiRadios = buildWifiRadioOptions(hwCand.Hardware.Component) + data.WizardWifiRadios = buildWifiRadioOptions(hwCand.Hardware.Component, hwCand.wifiCountryCode()) configuredRadioNames := make(map[string]bool, len(data.WizardWifiRadios)) for _, r := range data.WizardWifiRadios { configuredRadioNames[r.Name] = true @@ -507,7 +508,7 @@ func (h *ConfigureInterfacesHandler) Overview(w http.ResponseWriter, r *http.Req } // Populate the mirrored radio editor for WiFi interface rows from // the already-fetched candidate hardware tree (no extra fetch). - radios := indexWifiRadios(hwCand.Hardware.Component) + radios := indexWifiRadios(hwCand.Hardware.Component, hwCand.wifiCountryCode()) for i := range data.Interfaces { row := &data.Interfaces[i] if !row.IsWifi || row.WiFi == nil || row.WiFi.Radio == "" { @@ -1074,11 +1075,7 @@ func (h *ConfigureInterfacesHandler) WizardCreateRadio(w http.ResponseWriter, r renderSaveError(w, fmt.Errorf("radio name is required")) return } - if country == "" { - renderSaveError(w, fmt.Errorf("country code is required")) - return - } - radio := map[string]any{"country-code": country} + radio := map[string]any{} if band != "" { radio["band"] = band } @@ -1100,6 +1097,16 @@ func (h *ConfigureInterfacesHandler) WizardCreateRadio(w http.ResponseWriter, r "class": "infix-hardware:wifi", "infix-hardware:wifi-radio": radio, } + // The country code is one setting for every radio; the form offers + // it here so a first radio can be set up in one go. + if country != "" { + cc := map[string]any{"infix-hardware:country-code": country} + if err := h.RC.Put(r.Context(), candidatePath+hwWiFiCountryPath, cc); err != nil { + log.Printf("wizard create radio %q: country: %v", name, err) + renderSaveError(w, err) + return + } + } body := map[string]any{"ietf-hardware:component": []map[string]any{comp}} path := candidatePath + "/ietf-hardware:hardware/component=" + url.PathEscape(name) if err := h.RC.Put(r.Context(), path, body); err != nil { @@ -1115,7 +1122,7 @@ func (h *ConfigureInterfacesHandler) renderRadioPicker(w http.ResponseWriter, r if err := h.RC.Get(r.Context(), candidatePath+"/ietf-hardware:hardware", &hwCand); err != nil { log.Printf("wizard radio refresh: %v", err) } - radios := buildWifiRadioOptions(hwCand.Hardware.Component) + radios := buildWifiRadioOptions(hwCand.Hardware.Component, hwCand.wifiCountryCode()) if !containsRadioName(radios, selected) { // Race / fetch failure — surface the new radio anyway. radios = append([]wifiRadioOption{{Name: selected, Label: selected}}, radios...) @@ -1804,25 +1811,29 @@ func (h *ConfigureInterfacesHandler) SaveWifi(w http.ResponseWriter, r *http.Req // Both halves go in one patch, so a rejected save leaves the // candidate untouched. p := restconf.NewYangPatch(candidatePath) - // Radio half, only when the form actually carried a country (the - // wifi-radio container's mandatory leaf). Without it parseWiFiRadio - // would reject a form whose user only touched the WiFi side and left - // the radio fields untouched-empty. + // Hardware half: the radio's band and channel when the form carried + // them, and the box-wide country code when it was picked. A form + // whose user only touched the WiFi side leaves hardware alone. + hw := map[string]any{} if strings.TrimSpace(r.FormValue("country-code")) != "" { + hw["infix-hardware:wifi"] = map[string]any{ + "country-code": strings.TrimSpace(r.FormValue("country-code")), + } + } + if strings.TrimSpace(r.FormValue("band")) != "" || strings.TrimSpace(r.FormValue("channel")) != "" { rc, err := parseWiFiRadio(r) if err != nil { renderSaveError(w, err) return } - p.Merge(hwRoot, map[string]any{ - "ietf-hardware:hardware": map[string]any{ - "component": []map[string]any{{ - "name": radio, - "class": "infix-hardware:wifi", - "infix-hardware:wifi-radio": rc, - }}, - }, - }) + hw["component"] = []map[string]any{{ + "name": radio, + "class": "infix-hardware:wifi", + "infix-hardware:wifi-radio": rc, + }} + } + if len(hw) > 0 { + p.Merge(hwRoot, map[string]any{"ietf-hardware:hardware": hw}) } wifi := map[string]any{"radio": radio, mode: leaf} p.Replace(ifaceTarget(name)+"/infix-interfaces:wifi", map[string]any{"infix-interfaces:wifi": wifi}) @@ -1871,6 +1882,11 @@ func wifiRoamingFromForm(r *http.Request) (map[string]any, error) { } if r.FormValue("dot11r") == "on" { dot11r := map[string]any{} + key := strings.TrimSpace(r.FormValue("key-holder-secret")) + if key == "" { + return nil, fmt.Errorf("802.11r needs a key holder secret, a keystore key shared by the access points") + } + dot11r["key-holder-secret"] = key md := strings.ToLower(strings.TrimSpace(r.FormValue("mobility-domain"))) if md != "" { if md != "hash" && !isHex4(md) { @@ -1977,9 +1993,9 @@ func ifaceTarget(name string) string { // indexWifiRadios picks WiFi radio components out of the hardware // candidate tree and returns the minimal subset the WiFi interface -// editor mirrors (name, country, band, channel). Components without -// a wifi-radio container are skipped. -func indexWifiRadios(comps []hwComponentJSON) map[string]*ifaceRadioMirror { +// editor mirrors (name, band, channel, plus the box-wide country). +// Components without a wifi-radio container are skipped. +func indexWifiRadios(comps []hwComponentJSON, country string) map[string]*ifaceRadioMirror { out := make(map[string]*ifaceRadioMirror, len(comps)) for _, c := range comps { if c.WiFiRadio == nil { @@ -1987,7 +2003,7 @@ func indexWifiRadios(comps []hwComponentJSON) map[string]*ifaceRadioMirror { } m := &ifaceRadioMirror{ Name: c.Name, - CountryCode: c.WiFiRadio.CountryCode, + CountryCode: country, Band: c.WiFiRadio.Band, } if ch, ok := c.WiFiRadio.Channel.(float64); ok && ch > 0 { @@ -2658,9 +2674,10 @@ type wifiRadioOption struct { // configured WiFi radios (class=wifi with a wifi-radio container present // in running config) and returns picker entries with a label that hints // at band/channel/country. APReady reflects the YANG must-clauses on -// access-point — band, channel, and country-code all set, with country -// != "00" (world regulatory domain is rejected for AP mode). -func buildWifiRadioOptions(comps []hwComponentJSON) []wifiRadioOption { +// access-point — band and channel set on the radio and the box-wide +// country code set to something other than "00" (the world regulatory +// domain is rejected for AP mode). +func buildWifiRadioOptions(comps []hwComponentJSON, country string) []wifiRadioOption { var out []wifiRadioOption for _, c := range comps { if shortClass(c.Class) != classWiFi || c.WiFiRadio == nil { @@ -2669,7 +2686,7 @@ func buildWifiRadioOptions(comps []hwComponentJSON) []wifiRadioOption { ch := wifiChannelString(c.WiFiRadio.Channel) opt := wifiRadioOption{ Name: c.Name, - Country: c.WiFiRadio.CountryCode, + Country: country, Band: c.WiFiRadio.Band, Channel: ch, } diff --git a/src/webui/internal/handlers/dashboard.go b/src/webui/internal/handlers/dashboard.go index 2e476e640..7f78a0553 100644 --- a/src/webui/internal/handlers/dashboard.go +++ b/src/webui/internal/handlers/dashboard.go @@ -410,10 +410,26 @@ const ( type hardwareWrapper struct { Hardware struct { + WiFi *hwWiFiJSON `json:"infix-hardware:wifi"` Component []hwComponentJSON `json:"component"` } `json:"ietf-hardware:hardware"` } +// hwWiFiJSON is the box-wide WiFi container, one country code for every +// radio. +type hwWiFiJSON struct { + CountryCode string `json:"country-code"` +} + +// wifiCountryCode returns the box-wide WiFi country code, "00" (the +// world domain, the YANG default) when none is configured. +func (w hardwareWrapper) wifiCountryCode() string { + if w.Hardware.WiFi == nil || w.Hardware.WiFi.CountryCode == "" { + return "00" + } + return w.Hardware.WiFi.CountryCode +} + type hwComponentJSON struct { Name string `json:"name"` Class string `json:"class"` diff --git a/src/webui/internal/handlers/interfaces.go b/src/webui/internal/handlers/interfaces.go index c1dc9ea4e..2a6c06b0d 100644 --- a/src/webui/internal/handlers/interfaces.go +++ b/src/webui/internal/handlers/interfaces.go @@ -96,6 +96,18 @@ type wifiJSON struct { AccessPoint *wifiAPJSON `json:"access-point"` Station *wifiStationJSON `json:"station"` MeshPoint *wifiMeshJSON `json:"mesh-point"` + WDSLink *wifiWDSJSON `json:"wds-link"` +} + +// wifiWDSJSON mirrors the wds-link container: a 4-address station bound +// to this port of a local access point. +type wifiWDSJSON struct { + AccessPoint string `json:"access-point"` + PeerAddress string `json:"peer-address"` + Connected *bool `json:"connected"` + SignalStrength *int `json:"signal-strength"` + RxSpeed int `json:"rx-speed"` + TxSpeed int `json:"tx-speed"` } type wifiAPJSON struct { @@ -119,8 +131,9 @@ type wifiRoamingJSON struct { } type wifiDot11rJSON struct { - MobilityDomain string `json:"mobility-domain"` - NASIdentifier string `json:"nas-identifier"` + KeyHolderSecret string `json:"key-holder-secret"` + MobilityDomain string `json:"mobility-domain"` + NASIdentifier string `json:"nas-identifier"` } type wifiDot11vJSON struct { @@ -177,15 +190,8 @@ type wifiScanResultJSON struct { Encryption []string `json:"encryption"` } -// WiFi radio survey RESTCONF structures (from ietf-hardware:hardware). - -type wifiRadioJSON struct { - Survey *wifiSurveyJSON `json:"survey"` -} - -type wifiSurveyJSON struct { - Channel []surveyChanJSON `json:"channel"` -} +// WiFi channel survey, the output of the infix-hardware channel-survey +// action on a radio. type surveyChanJSON struct { Frequency int `json:"frequency"` @@ -565,6 +571,12 @@ func makeIfaceEntry(iface ifaceJSON, fwdSet map[string]bool) ifaceEntry { } else if mp := iface.WiFi.MeshPoint; mp != nil { n := len(mp.Peers.Peer) e.Detail = fmt.Sprintf("Mesh, mesh-id: %s, peers: %d", mp.MeshID, n) + } else if wds := iface.WiFi.WDSLink; wds != nil { + if wds.Connected != nil && *wds.Connected { + e.Detail = "WDS, connected" + } else { + e.Detail = "WDS, not connected" + } } } @@ -771,6 +783,17 @@ func buildDetailData(r *http.Request, iface *ifaceJSON) ifaceDetailData { for _, p := range mp.Peers.Peer { d.WiFiStations = append(d.WiFiStations, buildWifiStaEntry(p)) } + } else if wds := iface.WiFi.WDSLink; wds != nil { + d.WiFiMode = "WDS Link" + if wds.SignalStrength != nil { + d.WiFiSignal = fmt.Sprintf("%d dBm", *wds.SignalStrength) + } + if wds.RxSpeed > 0 { + d.WiFiRxSpeed = fmt.Sprintf("%.1f Mbps", float64(wds.RxSpeed)/10) + } + if wds.TxSpeed > 0 { + d.WiFiTxSpeed = fmt.Sprintf("%.1f Mbps", float64(wds.TxSpeed)/10) + } } else if st := iface.WiFi.Station; st != nil { d.WiFiMode = "Station" d.WiFiSSID = st.SSID diff --git a/src/webui/internal/handlers/wifi.go b/src/webui/internal/handlers/wifi.go index e1ce23097..e2aba1072 100644 --- a/src/webui/internal/handlers/wifi.go +++ b/src/webui/internal/handlers/wifi.go @@ -4,19 +4,18 @@ package handlers import ( "context" + "errors" "fmt" "html/template" "log" "net/http" + "net/url" "sync" + "time" "infix/webui/internal/restconf" ) -// wifiRadioHWJSON extends the hardware component wifi-radio container with -// operational fields from infix-hardware YANG that are not in wifiRadioJSON. -// wifiRadioJSON (defined in interfaces.go) only covers survey data; this -// struct captures the full operational state returned by RESTCONF. // wifiMaxIfJSON maps the max-interfaces container from infix-hardware YANG. type wifiMaxIfJSON struct { AP int `json:"ap"` @@ -25,7 +24,6 @@ type wifiMaxIfJSON struct { } type wifiRadioHWJSON struct { - CountryCode string `json:"country-code"` // ISO 3166-1, rw Channel interface{} `json:"channel"` // uint16 or "auto", rw Band string `json:"band"` // rw Frequency int `json:"frequency"` // MHz, operational @@ -33,7 +31,6 @@ type wifiRadioHWJSON struct { Driver string `json:"driver"` Bands []wifiBandJSON `json:"bands"` MaxInterfaces *wifiMaxIfJSON `json:"max-interfaces"` - Survey *wifiSurveyJSON `json:"survey"` } type wifiBandJSON struct { @@ -74,8 +71,8 @@ type WiFiRadio struct { VHTCapable bool HECapable bool Bands []WiFiBand - SurveySVG template.HTML Interfaces []WiFiInterface + Survey wifiSurveyData } type WiFiBand struct { @@ -86,17 +83,6 @@ type WiFiBand struct { HECapable bool } -// ChannelSurvey holds processed survey data for one channel. -type ChannelSurvey struct { - Frequency int - Channel int - InUse bool - Noise int - ActiveTime int64 - BusyTime int64 - UtilPct int // BusyTime/ActiveTime * 100 -} - // WiFiInterface is the template data for a virtual WiFi interface. type WiFiInterface struct { Name string @@ -207,6 +193,51 @@ func (h *WiFiHandler) Overview(w http.ResponseWriter, r *http.Request) { } } +// wifiSurveyData is the template data for the channel survey fragment of +// one radio: the chart after a scan, or why there is none. +type wifiSurveyData struct { + Radio string + SVG template.HTML + Error string +} + +// Survey runs the channel-survey action on a radio and renders the result +// as the survey fragment of the radio's card. The scan takes the radio off +// its channel for a few seconds, so it only runs on request. +// POST /wifi/{name}/survey +func (h *WiFiHandler) Survey(w http.ResponseWriter, r *http.Request) { + data := wifiSurveyData{Radio: r.PathValue("name")} + + var reply struct { + Output struct { + Channel []surveyChanJSON `json:"channel"` + } `json:"infix-hardware:output"` + } + + ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) + defer cancel() + + path := "/data/ietf-hardware:hardware/component=" + url.PathEscape(data.Radio) + + "/infix-hardware:wifi-radio/channel-survey" + if err := h.RC.CallRPC(ctx, path, nil, &reply); err != nil { + log.Printf("wifi: channel survey %s: %v", data.Radio, err) + data.Error = "Channel survey failed" + var re *restconf.Error + if errors.As(err, &re) && re.Message != "" { + data.Error = re.Message + } + } else if len(reply.Output.Channel) == 0 { + data.Error = "The radio reported no survey data" + } else { + data.SVG = renderSurveySVG(reply.Output.Channel) + } + + if err := h.Template.ExecuteTemplate(w, "wifi-survey", data); err != nil { + log.Printf("wifi: template error: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } +} + // buildWiFiRadios assembles the WiFiRadio slice from hardware components // and interface data, matching interfaces to their radio by name. func buildWiFiRadios(components []hwComponentWiFiJSON, ifaces []ifaceJSON) []WiFiRadio { @@ -226,6 +257,7 @@ func buildWiFiRadios(components []hwComponentWiFiJSON, ifaces []ifaceJSON) []WiF Driver: r.Driver, Channel: wifiChannelString(r.Channel), Manufacturer: c.MfgName, + Survey: wifiSurveyData{Radio: c.Name}, } // Capability flags: check per-band capabilities; if any band supports @@ -277,11 +309,6 @@ func buildWiFiRadios(components []hwComponentWiFiJSON, ifaces []ifaceJSON) []WiF radio.MaxAP = fmt.Sprintf("%d", r.MaxInterfaces.AP) } - // Generate channel survey SVG if survey data exists. - if r.Survey != nil && len(r.Survey.Channel) > 0 { - radio.SurveySVG = renderSurveySVG(r.Survey.Channel) - } - // Attach wifi interfaces that reference this radio. radio.Interfaces = buildWiFiInterfaces(c.Name, ifaces) diff --git a/src/webui/internal/handlers/wifi_mesh_test.go b/src/webui/internal/handlers/wifi_mesh_test.go index c5c303761..d6a25bb6f 100644 --- a/src/webui/internal/handlers/wifi_mesh_test.go +++ b/src/webui/internal/handlers/wifi_mesh_test.go @@ -40,7 +40,7 @@ const wifiCfgFixture = `{"ietf-interfaces:interfaces":{"interface":[ "security":{"secret":"mesh-psk"}}}}, {"name":"wifi1-ap","type":"infix-if-type:wifi", "infix-interfaces:wifi":{"radio":"phy1","access-point":{"ssid":"office","security":{"mode":"wpa3-personal","secret":"psk"}, - "roaming":{"dot11k":{},"dot11r":{"mobility-domain":"hash"},"dot11v":{"band-steering":false},"okc":false}}}}, + "roaming":{"dot11k":{},"dot11r":{"key-holder-secret":"ft-key","mobility-domain":"hash"},"dot11v":{"band-steering":false},"okc":false}}}}, {"name":"wifi2","type":"infix-if-type:wifi", "infix-interfaces:wifi":{"radio":"phy2","station":{"ssid":"upstream","security":{"secret":"psk"}}}}, {"name":"wifi3-mesh","type":"infix-if-type:wifi", @@ -149,14 +149,14 @@ func roamingForm(t *testing.T, v url.Values) *http.Request { func TestWiFiRoamingFromForm(t *testing.T) { roaming, err := wifiRoamingFromForm(roamingForm(t, url.Values{ - "dot11k": {"on"}, "dot11r": {"on"}, "mobility-domain": {"AB12"}, "nas-identifier": {"auto"}, + "dot11k": {"on"}, "dot11r": {"on"}, "key-holder-secret": {"ft-key"}, "mobility-domain": {"AB12"}, "nas-identifier": {"auto"}, "dot11v": {"on"}, "band-steering": {"on"}, "okc": {"on"}, })) if err != nil { t.Fatal(err) } r := roaming["dot11r"].(map[string]any) - if r["mobility-domain"] != "ab12" || r["nas-identifier"] != "auto" { + if r["key-holder-secret"] != "ft-key" || r["mobility-domain"] != "ab12" || r["nas-identifier"] != "auto" { t.Errorf("dot11r = %v", r) } // okc and band-steering are on: both at their YANG default, so absent. @@ -172,7 +172,7 @@ func TestWiFiRoamingFromForm(t *testing.T) { // Unticked presence containers and blank sub-fields are simply absent; // SaveWifi PUTs the whole container so that is enough to clear them. - roaming, err = wifiRoamingFromForm(roamingForm(t, url.Values{"dot11r": {"on"}, "mobility-domain": {"hash"}})) + roaming, err = wifiRoamingFromForm(roamingForm(t, url.Values{"dot11r": {"on"}, "key-holder-secret": {"ft-key"}, "mobility-domain": {"hash"}})) if err != nil { t.Fatal(err) } @@ -205,9 +205,12 @@ func TestWiFiRoamingFromForm(t *testing.T) { t.Error("blank nas-identifier should be absent") } - if _, err := wifiRoamingFromForm(roamingForm(t, url.Values{"dot11r": {"on"}, "mobility-domain": {"xyz"}})); err == nil { + if _, err := wifiRoamingFromForm(roamingForm(t, url.Values{"dot11r": {"on"}, "key-holder-secret": {"ft-key"}, "mobility-domain": {"xyz"}})); err == nil { t.Error("expected error for bad mobility domain") } + if _, err := wifiRoamingFromForm(roamingForm(t, url.Values{"dot11r": {"on"}, "mobility-domain": {"hash"}})); err == nil { + t.Error("expected error without key holder secret") + } } // realTemplates parses the on-disk templates the way server.go does, so @@ -329,7 +332,7 @@ func TestConfigureInterfacesRendersMeshAndRoamingEditors(t *testing.T) { for _, want := range []string{ `value="mesh-point" checked`, `name="mesh-id"`, `value="backhaul"`, `name="forwarding"`, `Roaming (802.11k/r/v)`, - `name="dot11k" checked`, `name="mobility-domain"`, `value="hash"`, `data-fold-target="wifi-row-wifi1-ap-dot11r-md wifi-row-wifi1-ap-dot11r-nas"`, + `name="dot11k" checked`, `name="mobility-domain"`, `value="hash"`, `name="key-holder-secret"`, `data-fold-target="wifi-row-wifi1-ap-dot11r-key wifi-row-wifi1-ap-dot11r-md wifi-row-wifi1-ap-dot11r-nas"`, `add-iface-wifi-mode-mesh`, `add-iface-wifi-meshid-row`, `roaming`, } { diff --git a/src/webui/internal/handlers/wifi_save_test.go b/src/webui/internal/handlers/wifi_save_test.go index ede183884..230bb5ac9 100644 --- a/src/webui/internal/handlers/wifi_save_test.go +++ b/src/webui/internal/handlers/wifi_save_test.go @@ -77,3 +77,23 @@ func TestSaveWifiBadRadioWritesNothing(t *testing.T) { t.Fatalf("candidate written despite form error: %+v", rc.Patches) } } + +func TestSaveWifiCountryGoesToBoxWideLeaf(t *testing.T) { + w, rc := postSaveWifi(t, url.Values{ + "mode": {"access-point"}, "radio": {"radio0"}, "ssid": {"lab"}, + "sec-mode": {"wpa2-wpa3-personal"}, "secret": {"psk"}, + "country-code": {"SE"}, "band": {"5GHz"}, "channel": {"36"}, + }) + if w.Code != http.StatusOK { + t.Fatalf("status %d: %s", w.Code, w.Body.String()) + } + hw := rc.Patches[0].Edits[0].Value.(map[string]any)["ietf-hardware:hardware"].(map[string]any) + wifi, _ := hw["infix-hardware:wifi"].(map[string]any) + if wifi["country-code"] != "SE" { + t.Errorf("country not on the box-wide leaf: %v", hw) + } + comps := hw["component"].([]map[string]any) + if _, has := comps[0]["infix-hardware:wifi-radio"].(map[string]any)["country-code"]; has { + t.Errorf("country still on the radio: %v", comps[0]) + } +} diff --git a/src/webui/internal/handlers/wifi_survey_test.go b/src/webui/internal/handlers/wifi_survey_test.go new file mode 100644 index 000000000..9937ca7b6 --- /dev/null +++ b/src/webui/internal/handlers/wifi_survey_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT + +package handlers + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "infix/webui/internal/restconf" +) + +const surveyActionPath = "/data/ietf-hardware:hardware/component=radio0/infix-hardware:wifi-radio/channel-survey" + +// fakeSurveyAction serves the channel-survey action on radio0 with reply. +func fakeSurveyAction(t *testing.T, reply string, status int) *httptest.Server { + t.Helper() + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != surveyActionPath || r.Method != http.MethodPost { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + http.Error(w, "unexpected", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/yang-data+json") + w.WriteHeader(status) + io.WriteString(w, reply) //nolint:errcheck + })) + t.Cleanup(srv.Close) + return srv +} + +func surveyRequest(t *testing.T, srv *httptest.Server) string { + t.Helper() + h := &WiFiHandler{ + Template: realTemplates(t, nil, "layouts/*.html", "pages/wifi.html"), + RC: restconf.NewClient(srv.URL, true), + } + req := httptest.NewRequest(http.MethodPost, "/wifi/radio0/survey", nil) + req.SetPathValue("name", "radio0") + req = req.WithContext(restconf.ContextWithCredentials(req.Context(), + restconf.Credentials{Username: "admin", Password: "secret"})) + rec := httptest.NewRecorder() + h.Survey(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status %d, want 200", rec.Code) + } + return rec.Body.String() +} + +func TestWiFiSurveyRendersChart(t *testing.T) { + reply := `{"infix-hardware:output":{"channel":[ + {"frequency":2412,"in-use":false,"noise":-92,"active-time":120,"busy-time":15}, + {"frequency":2437,"in-use":true,"noise":-92,"active-time":1000,"busy-time":125, + "receive-time":60,"transmit-time":30}]}}` + out := surveyRequest(t, fakeSurveyAction(t, reply, http.StatusOK)) + + for _, want := range []string{`class="survey-chart"`, "Scan again", `hx-post="/wifi/radio0/survey"`} { + if !strings.Contains(out, want) { + t.Errorf("missing %q in\n%s", want, out) + } + } + if strings.Contains(out, "alert-error") { + t.Errorf("unexpected error in\n%s", out) + } +} + +func TestWiFiSurveyShowsDeviceError(t *testing.T) { + reply := `{"ietf-restconf:errors":{"error":[{"error-type":"application", + "error-tag":"operation-failed", + "error-message":"Channel survey failed: no interface on radio0 to scan with"}]}}` + out := surveyRequest(t, fakeSurveyAction(t, reply, http.StatusInternalServerError)) + + for _, want := range []string{"alert-error", "no interface on radio0", "Scan channels"} { + if !strings.Contains(out, want) { + t.Errorf("missing %q in\n%s", want, out) + } + } + if strings.Contains(out, "survey-chart") { + t.Errorf("chart rendered on error:\n%s", out) + } +} + +func TestWiFiPageOffersSurveyScan(t *testing.T) { + tmpl := realTemplates(t, nil, "layouts/*.html", "pages/wifi.html") + comps := []hwComponentWiFiJSON{{Name: "radio0", WiFiRadio: &wifiRadioHWJSON{Band: "2.4GHz"}}} + data := wifiData{Radios: buildWiFiRadios(comps, nil)} + + var buf strings.Builder + if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil { + t.Fatalf("render: %v", err) + } + out := buf.String() + for _, want := range []string{`id="wifi-survey-radio0"`, `hx-post="/wifi/radio0/survey"`, "Scan channels"} { + if !strings.Contains(out, want) { + t.Errorf("missing %q in\n%s", want, out) + } + } + if strings.Contains(out, "survey-chart") { + t.Errorf("chart rendered before any scan:\n%s", out) + } +} diff --git a/src/webui/internal/schema/cache_broken_test.go b/src/webui/internal/schema/cache_broken_test.go new file mode 100644 index 000000000..c173e84b1 --- /dev/null +++ b/src/webui/internal/schema/cache_broken_test.go @@ -0,0 +1,109 @@ +package schema + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "infix/webui/internal/restconf" +) + +// A module cut short, next to a submodule that uses a type from it, makes +// goyang dereference nil instead of reporting an error. +func writeBrokenCache(t *testing.T, dir string) { + t.Helper() + files := map[string]string{ + "m@2026-01-01.yang": "module m { yang-version 1.1; namespace \"urn:m\"; prefix m; include s; typedef t { ", + "s@2026-01-01.yang": "submodule s { yang-version 1.1; belongs-to m { prefix m; } leaf x { type t; } }", + } + for name, body := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0640); err != nil { + t.Fatal(err) + } + } +} + +func TestLoadRejectsBrokenModule(t *testing.T) { + dir := t.TempDir() + writeBrokenCache(t, dir) + if _, err := Load(dir); err == nil { + t.Fatal("Load accepted a cut-short module") + } +} + +func TestLoadFromCacheDropsBrokenCache(t *testing.T) { + dir := t.TempDir() + writeBrokenCache(t, dir) + if err := os.WriteFile(filepath.Join(dir, ".version"), []byte("v1\n"), 0640); err != nil { + t.Fatal(err) + } + + if err := NewCache(nil, dir, "v1").LoadFromCache(); err == nil { + t.Fatal("broken cache loaded without error") + } + entries, _ := os.ReadDir(dir) + for _, e := range entries { + if strings.HasSuffix(e.Name(), ".yang") { + t.Errorf("%s kept in a broken cache", e.Name()) + } + } + if b, _ := os.ReadFile(filepath.Join(dir, ".version")); string(b) != "v1\n" { + t.Errorf("stamp = %q", b) + } +} + +// fakeFetcher serves a modules-state listing and the module files from a +// map. Other Fetcher methods are never called. +type fakeFetcher struct { + restconf.Fetcher + modules map[string]string // name@revision -> body +} + +func (f fakeFetcher) Get(_ context.Context, path string, target any) error { + ms := target.(*rfc7895ModulesState) + for key := range f.modules { + name, rev, _ := strings.Cut(key, "@") + ms.ModulesState.Module = append(ms.ModulesState.Module, struct { + Name string `json:"name"` + Revision string `json:"revision"` + Submodule []struct { + Name string `json:"name"` + Revision string `json:"revision"` + } `json:"submodule"` + }{Name: name, Revision: rev}) + } + return nil +} + +func (f fakeFetcher) GetYANG(_ context.Context, name, revision string) ([]byte, error) { + return []byte(f.modules[name+"@"+revision]), nil +} + +func TestFetchModulesPrunesStaleRevisions(t *testing.T) { + dir := t.TempDir() + stale := filepath.Join(dir, "example@2025-01-01.yang") + if err := os.WriteFile(stale, []byte("module example { namespace x; prefix x; }"), 0640); err != nil { + t.Fatal(err) + } + + rc := fakeFetcher{modules: map[string]string{ + "example@2026-01-01": "module example { namespace x; prefix x; }", + }} + if _, err := FetchModules(context.Background(), rc, dir); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(stale); !os.IsNotExist(err) { + t.Errorf("stale revision kept: %v", err) + } + if _, err := os.Stat(filepath.Join(dir, "example@2026-01-01.yang")); err != nil { + t.Errorf("current revision missing: %v", err) + } + entries, _ := os.ReadDir(dir) + for _, e := range entries { + if strings.HasPrefix(e.Name(), ".example") { + t.Errorf("temporary file left behind: %s", e.Name()) + } + } +} diff --git a/src/webui/internal/schema/fetch.go b/src/webui/internal/schema/fetch.go index 41a55fbc1..940a47287 100644 --- a/src/webui/internal/schema/fetch.go +++ b/src/webui/internal/schema/fetch.go @@ -6,6 +6,7 @@ import ( "log" "os" "path/filepath" + "strings" "infix/webui/internal/restconf" ) @@ -71,9 +72,32 @@ func FetchModules(ctx context.Context, rc restconf.Fetcher, cacheDir string) ([] } downloaded = append(downloaded, m) } + prune(cacheDir, modules) return downloaded, nil } +// prune removes cached YANG files the device no longer lists, such as an +// earlier revision of a module, so that two revisions never load together. +func prune(cacheDir string, modules []ModuleInfo) { + keep := make(map[string]bool, len(modules)) + for _, m := range modules { + keep[m.filename()] = true + } + entries, err := os.ReadDir(cacheDir) + if err != nil { + return + } + for _, e := range entries { + name := e.Name() + if e.IsDir() || !strings.HasSuffix(name, ".yang") || keep[name] { + continue + } + if err := os.Remove(filepath.Join(cacheDir, name)); err == nil { + log.Printf("schema: dropped %s, no longer on the device", name) + } + } +} + // listModules queries the device for the list of implemented YANG modules. func listModules(ctx context.Context, rc restconf.Fetcher) ([]ModuleInfo, error) { // Try RFC 7895 modules-state first. @@ -117,7 +141,27 @@ func downloadIfMissing(ctx context.Context, rc restconf.Fetcher, cacheDir string return fmt.Errorf("GET /yang/%s: %w", m.filename(), err) } - if err := os.WriteFile(dest, data, 0640); err != nil { + // Write through a temporary name so that a restart in the middle of + // the download never leaves a cut-short module behind. + tmp, err := os.CreateTemp(cacheDir, "."+m.Name+".*") + if err != nil { + return fmt.Errorf("write %s: %w", dest, err) + } + if _, err := tmp.Write(data); err != nil { + tmp.Close() + os.Remove(tmp.Name()) + return fmt.Errorf("write %s: %w", dest, err) + } + if err := tmp.Close(); err != nil { + os.Remove(tmp.Name()) + return fmt.Errorf("write %s: %w", dest, err) + } + if err := os.Chmod(tmp.Name(), 0640); err != nil { + os.Remove(tmp.Name()) + return fmt.Errorf("write %s: %w", dest, err) + } + if err := os.Rename(tmp.Name(), dest); err != nil { + os.Remove(tmp.Name()) return fmt.Errorf("write %s: %w", dest, err) } log.Printf("schema: cached %s", m.filename()) diff --git a/src/webui/internal/schema/manager.go b/src/webui/internal/schema/manager.go index b888cf744..8095e1fd4 100644 --- a/src/webui/internal/schema/manager.go +++ b/src/webui/internal/schema/manager.go @@ -23,7 +23,16 @@ type Manager struct { // Load parses all .yang files in yangDir and returns a Manager. // Errors from Process() that are non-fatal (e.g. unresolved augments for // modules that were not downloaded) are logged but do not abort loading. -func Load(yangDir string) (*Manager, error) { +func Load(yangDir string) (mgr *Manager, err error) { + // goyang dereferences nil on some malformed input, for example a + // module file cut short, instead of returning an error. The cache is + // rebuilt from the device on failure, so turn that into an error. + defer func() { + if r := recover(); r != nil { + mgr, err = nil, fmt.Errorf("schema: parse %s: %v", yangDir, r) + } + }() + ms := yang.NewModules() ms.Path = []string{yangDir} diff --git a/src/webui/internal/schema/refresh.go b/src/webui/internal/schema/refresh.go index 31279d460..18243f617 100644 --- a/src/webui/internal/schema/refresh.go +++ b/src/webui/internal/schema/refresh.go @@ -41,13 +41,23 @@ func (c *Cache) dropStale() error { if b, err := os.ReadFile(stamp); err == nil && strings.TrimSpace(string(b)) == c.version { return nil } + log.Printf("schema: cache in %s is for another image, dropped", c.dir) + return c.drop() +} + +// drop empties the cache directory and stamps it for this image, so the +// next Refresh downloads every module again. +func (c *Cache) drop() error { if err := os.RemoveAll(c.dir); err != nil { return err } if err := os.MkdirAll(c.dir, 0750); err != nil { return err } - log.Printf("schema: cache in %s is for another image, dropped", c.dir) + if c.version == "" { + return nil + } + stamp := filepath.Join(c.dir, ".version") return os.WriteFile(stamp, []byte(c.version+"\n"), 0640) } @@ -80,7 +90,12 @@ func (c *Cache) LoadFromCache() error { mgr, err := Load(c.dir) if err != nil { - return fmt.Errorf("schema: load from cache: %w", err) + // A broken file would fail every start, drop the lot and let + // the next Refresh fetch a fresh copy. + if derr := c.drop(); derr != nil { + return fmt.Errorf("schema: load from cache: %w (and dropping it: %v)", err, derr) + } + return fmt.Errorf("schema: load from cache: %w, cache dropped", err) } c.mu.Lock() c.manager = mgr @@ -122,7 +137,10 @@ func (c *Cache) Refresh(ctx context.Context) error { mgr, err := Load(c.dir) if err != nil { - return fmt.Errorf("schema refresh: load: %w", err) + if derr := c.drop(); derr != nil { + return fmt.Errorf("schema refresh: load: %w (and dropping the cache: %v)", err, derr) + } + return fmt.Errorf("schema refresh: load: %w, cache dropped", err) } c.mu.Lock() diff --git a/src/webui/internal/server/server.go b/src/webui/internal/server/server.go index 61aee687a..02c951de6 100644 --- a/src/webui/internal/server/server.go +++ b/src/webui/internal/server/server.go @@ -308,6 +308,7 @@ func New( mux.HandleFunc("POST /maintenance/system/datetime", sys.SetDatetime) mux.HandleFunc("GET /routing", routing.Overview) mux.HandleFunc("GET /wifi", wifi.Overview) + mux.HandleFunc("POST /wifi/{name}/survey", wifi.Survey) mux.HandleFunc("GET /hardware", hw.Overview) mux.HandleFunc("POST /hardware/locate", hw.Locate) mux.HandleFunc("GET /vpn", vpn.Overview) @@ -409,6 +410,7 @@ func New( mux.HandleFunc("GET /configure/hardware", cfgHw.Overview) mux.HandleFunc("POST /configure/hardware", cfgHw.CreateHardware) mux.HandleFunc("POST /configure/hardware/usb/{name}", cfgHw.SaveUSBPort) + mux.HandleFunc("POST /configure/hardware/wifi", cfgHw.SaveWiFiCountry) mux.HandleFunc("POST /configure/hardware/wifi/{name}", cfgHw.SaveWiFiRadio) mux.HandleFunc("POST /configure/hardware/gps/{name}", cfgHw.SaveGPS) mux.HandleFunc("DELETE /configure/hardware/{name}", cfgHw.DeleteComponent) diff --git a/src/webui/static/css/style.css b/src/webui/static/css/style.css index c69ae3b9a..a44b4f1cb 100644 --- a/src/webui/static/css/style.css +++ b/src/webui/static/css/style.css @@ -2205,6 +2205,37 @@ details[open] > .cfg-multi-summary { width: auto; } +/* Click the chart to open it in an overlay sized to the window. */ +.survey-zoom { + display: block; + width: 100%; + padding: 0; + border: 0; + background: none; + cursor: zoom-in; +} + +.survey-zoom .survey-chart { + margin: 0 auto; +} + +.survey-dialog { + min-width: 0; + width: min(96vw, 1400px); +} + +.survey-dialog .iface-modal-body { + padding: 1rem; +} + +.survey-dialog .survey-chart { + display: block; + width: 100%; + height: auto; + max-height: 82vh; + margin: 0 auto; +} + .wifi-ssid { font-weight: 400; color: var(--fg-muted); diff --git a/src/webui/static/js/app.js b/src/webui/static/js/app.js index b946bb495..126e982ef 100644 --- a/src/webui/static/js/app.js +++ b/src/webui/static/js/app.js @@ -1100,6 +1100,28 @@ function setBlockEnabled(el, on) { }); }); + // WiFi channel survey: click the chart to open a copy of it in the + // page's survey dialog, sized to the window. Delegated so it survives + // the htmx swap that replaces the chart on every scan. A click on the + // backdrop closes the dialog, like the close button. + document.addEventListener('click', function (e) { + var zoom = e.target.closest && e.target.closest('[data-survey-zoom]'); + if (!zoom) return; + var dlg = document.getElementById('survey-dialog'); + var svg = zoom.querySelector('svg'); + if (!dlg || !dlg.showModal || !svg) return; + var body = document.getElementById('survey-dialog-body'); + var radio = document.getElementById('survey-dialog-radio'); + body.innerHTML = ''; + body.appendChild(svg.cloneNode(true)); + if (radio) radio.textContent = zoom.getAttribute('data-survey-zoom') || ''; + dlg.showModal(); + }); + document.addEventListener('click', function (e) { + var dlg = e.target; + if (dlg && dlg.id === 'survey-dialog' && dlg.open) dlg.close(); + }); + // Add Interface modal — open via data-show-modal, close via // data-close-modal. Mirrors the existing data-show/data-hide vocabulary // for action-on-target attributes. Native .showModal() gives us @@ -1411,8 +1433,7 @@ function setBlockEnabled(el, on) { }); // Configure > Hardware "+ Add hardware" picker: sync the hidden class - // input from the selected option's data-class and reveal class-specific - // fields (currently WiFi country-code). + // input from the selected option's data-class. document.addEventListener('change', function (e) { var sel = e.target.closest && e.target.closest('#add-hw-picker'); if (!sel) return; @@ -1420,12 +1441,6 @@ function setBlockEnabled(el, on) { var cls = (opt && opt.getAttribute('data-class')) || ''; var classInput = document.getElementById('add-hw-class'); if (classInput) classInput.value = cls; - var wifi = document.getElementById('add-hw-wifi-fields'); - // setBlockEnabled, not .hidden: the country select is inside this - // nested hidden span, so opening the row left it disabled. - if (wifi) setBlockEnabled(wifi, cls === 'wifi'); - var country = document.getElementById('add-hw-wifi-country'); - if (country) country.required = (cls === 'wifi'); }); })(); diff --git a/src/webui/templates/pages/configure-hardware.html b/src/webui/templates/pages/configure-hardware.html index 7096185f4..0ca6e44d1 100644 --- a/src/webui/templates/pages/configure-hardware.html +++ b/src/webui/templates/pages/configure-hardware.html @@ -20,6 +20,27 @@ {{end}} {{end}} + +
+

Channel Survey

+ +
+
+
+{{end}} + +{{define "wifi-survey"}} +
+ {{if .SVG}} + + {{else if .Error}} +
{{.Error}}
+ {{else}} +

Scans every channel the radio supports and shows how busy each one is. + The radio leaves its channel for a few seconds, so traffic pauses during the scan.

+ {{end}} +
+
+ +
{{end}} diff --git a/test/case/interfaces/wifi.yaml b/test/case/interfaces/wifi.yaml index 974e35e2b..55efbeaf4 100644 --- a/test/case/interfaces/wifi.yaml +++ b/test/case/interfaces/wifi.yaml @@ -10,3 +10,15 @@ - name: WiFi Band Steering across a dual-band Access Point case: wifi_band_steering/test.py + +- name: WiFi 4-address (WDS) link between two bridges + case: wifi_wds_link_2dut/test.py + +- name: WiFi repeater with two SSIDs in VLANs over a 4-address (WDS) backhaul + case: wifi_wds_repeater/test.py + +- name: WiFi station set up from a scan-only interface + case: wifi_station_from_scan/test.py + +- name: WiFi channel survey on a connected radio + case: wifi_channel_survey/test.py diff --git a/test/case/interfaces/wifi_ap_multi_station/test.py b/test/case/interfaces/wifi_ap_multi_station/test.py index 6d2bb6e88..f9c19aa65 100755 --- a/test/case/interfaces/wifi_ap_multi_station/test.py +++ b/test/case/interfaces/wifi_ap_multi_station/test.py @@ -78,8 +78,7 @@ def leased(dut): with test.step("Configure the ap as an Access Point on radio0"): ap.put_config_dicts({ - "ietf-hardware": {"hardware": {"component": [ - wifi.radio("radio0", band="2.4GHz", channel=1)]}}, + "ietf-hardware": wifi.hardware(wifi.radio("radio0", band="2.4GHz", channel=1)), "ietf-keystore": wifi.keystore({"wifi": PSK}), "ietf-interfaces": {"interfaces": {"interface": [ wifi.iface("wifi0", AP_MAC, { @@ -99,7 +98,7 @@ def leased(dut): with test.step("Configure the stations on radio0"): def configure_station(mac, dut): dut.put_config_dicts({ - "ietf-hardware": {"hardware": {"component": [wifi.radio("radio0")]}}, + "ietf-hardware": wifi.hardware(wifi.radio("radio0")), "ietf-keystore": wifi.keystore({"wifi": PSK}), "ietf-interfaces": {"interfaces": {"interface": [ wifi.iface("wifi0", mac, { diff --git a/test/case/interfaces/wifi_ap_station_2dut/test.py b/test/case/interfaces/wifi_ap_station_2dut/test.py index 8a596f501..d7f2130bc 100755 --- a/test/case/interfaces/wifi_ap_station_2dut/test.py +++ b/test/case/interfaces/wifi_ap_station_2dut/test.py @@ -53,8 +53,7 @@ with test.step("Configure the ap as an Access Point and the station on radio0"): parallel( lambda: ap.put_config_dicts({ - "ietf-hardware": {"hardware": {"component": [ - wifi.radio("radio0", band="2.4GHz", channel=1)]}}, + "ietf-hardware": wifi.hardware(wifi.radio("radio0", band="2.4GHz", channel=1)), "ietf-keystore": wifi.keystore({"wifi": PSK}), "ietf-interfaces": {"interfaces": {"interface": [ # hwsim defaults every radio0 to 02:00:00:00:00:00, so the AP @@ -74,7 +73,7 @@ }]}}, }), lambda: station.put_config_dicts({ - "ietf-hardware": {"hardware": {"component": [wifi.radio("radio0")]}}, + "ietf-hardware": wifi.hardware(wifi.radio("radio0")), "ietf-keystore": wifi.keystore({"wifi": PSK}), "ietf-interfaces": {"interfaces": {"interface": [ wifi.iface("wifi0", "02:00:00:00:00:02", { diff --git a/test/case/interfaces/wifi_band_steering/test.adoc b/test/case/interfaces/wifi_band_steering/test.adoc index af7edd2dd..5f0242b12 100644 --- a/test/case/interfaces/wifi_band_steering/test.adoc +++ b/test/case/interfaces/wifi_band_steering/test.adoc @@ -45,5 +45,10 @@ image::topology.svg[WiFi Band Steering across a dual-band Access Point topology, . Verify the client associates to the 'campus' SSID . Verify band steering put the client on the 5GHz BSS . Verify the client leases an address over 5GHz +. Disable the 5GHz BSS +. Verify the client falls back to the 2.4GHz BSS +. Enable the 5GHz BSS again +. Verify a scanning client is steered back to the 5GHz BSS +. Verify the client still has its address after the move diff --git a/test/case/interfaces/wifi_band_steering/test.py b/test/case/interfaces/wifi_band_steering/test.py index 3eab7d0f5..4f43f3aba 100755 --- a/test/case/interfaces/wifi_band_steering/test.py +++ b/test/case/interfaces/wifi_band_steering/test.py @@ -72,9 +72,10 @@ def ap_bss(name, radio_name, bssid): with infamy.Test() as test: with test.step("Set up topology and attach to the ap and the client"): env = infamy.Env() - ap, client = parallel( + ap, client, clientsh = parallel( lambda: env.attach("ap", "mgmt"), lambda: env.attach("client", "mgmt"), + lambda: env.attach("client", "mgmt", "ssh"), ) wifi.skip_unless_supported(test, ap, client) @@ -83,10 +84,9 @@ def ap_bss(name, radio_name, bssid): # dedicated band-steering cell (cell2) in test/virt/quad. parallel( lambda: ap.put_config_dicts({ - "ietf-hardware": {"hardware": {"component": [ + "ietf-hardware": wifi.hardware( wifi.radio("radio2", band="2.4GHz", channel=1), - wifi.radio("radio3", band="5GHz", channel=36), - ]}}, + wifi.radio("radio3", band="5GHz", channel=36)), "ietf-keystore": wifi.keystore({"wifi": PSK}), "ietf-interfaces": {"interfaces": {"interface": [ {"name": "br0", "type": "infix-if-type:bridge", "enabled": True, @@ -104,7 +104,7 @@ def ap_bss(name, radio_name, bssid): # (cell2). No band/channel pinned: the one radio scans both bands and # lets band steering decide where it lands. lambda: client.put_config_dicts({ - "ietf-hardware": {"hardware": {"component": [wifi.radio("radio2")]}}, + "ietf-hardware": wifi.hardware(wifi.radio("radio2")), "ietf-keystore": wifi.keystore({"wifi": PSK}), "ietf-interfaces": {"interfaces": {"interface": [ wifi.iface("wifi0", CLIENT_MAC, { @@ -136,4 +136,37 @@ def ap_bss(name, radio_name, bssid): iface.address_exist(client, "wifi0", POOL_END), attempts=60, interval=2) + # The 5GHz BSS goes away: the client falls back to 2.4GHz once the + # 5GHz radio has not seen it for a minute. When the 5GHz BSS comes + # back, the client is connected on the wrong band. A client that + # scans is seen on 5GHz again and gets asked to move there. Real + # clients scan in the background on their own, this one is told to. + with test.step("Disable the 5GHz BSS"): + ap.put_config_dicts({"ietf-interfaces": {"interfaces": { + "interface": [{"name": "wifi1", "enabled": False}]}}}) + + with test.step("Verify the client falls back to the 2.4GHz BSS"): + until(lambda: CLIENT_MAC in wifi.ap_stations(ap, "wifi0"), + attempts=90, interval=2) + + with test.step("Enable the 5GHz BSS again"): + ap.put_config_dicts({"ietf-interfaces": {"interfaces": { + "interface": [{"name": "wifi1", "enabled": True}]}}}) + until(lambda: iface.is_oper_up(ap, "wifi1"), attempts=60, interval=2) + + with test.step("Verify a scanning client is steered back to the 5GHz BSS"): + def steered(): + if CLIENT_MAC in wifi.ap_stations(ap, "wifi1"): + return True + clientsh.runsh("sudo wpa_cli -i wifi0 scan >/dev/null") + return False + until(steered, attempts=20, interval=10) + assert CLIENT_MAC not in wifi.ap_stations(ap, "wifi0"), \ + "client still associated on 2.4GHz" + + with test.step("Verify the client still has its address after the move"): + until(lambda: iface.address_exist(client, "wifi0", POOL_START) or + iface.address_exist(client, "wifi0", POOL_END), + attempts=30, interval=2) + test.succeed() diff --git a/test/case/interfaces/wifi_channel_survey/Readme.adoc b/test/case/interfaces/wifi_channel_survey/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/wifi_channel_survey/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/wifi_channel_survey/test.adoc b/test/case/interfaces/wifi_channel_survey/test.adoc new file mode 100644 index 000000000..44da16e43 --- /dev/null +++ b/test/case/interfaces/wifi_channel_survey/test.adoc @@ -0,0 +1,34 @@ +=== WiFi channel survey on a connected radio + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wifi_channel_survey] + +==== Description + +A channel survey tells how busy each channel is. Collecting it means +leaving the operating channel, so it is not done in the background but +on request, with the channel-survey action on the radio. The action has +to work on a radio that is in use: here on the ap, which is serving a +station, and on the station, which is associated to the ap. + +Topology: +.... + host ==(mgmt)== ap ))) ~ cell ~ ((( station ==(mgmt)== host +.... + +==== Topology + +image::topology.svg[WiFi channel survey on a connected radio topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to the ap and the station +. Configure the ap as an Access Point on channel 1 and the station on radio0 +. Verify the station associates to the ap over the wifi link +. Run a channel survey on radio0 of the station +. Verify the station's survey reports 2412 MHz as the channel in use +. Verify the station's survey covers more channels than the one in use +. Run a channel survey on radio0 of the ap +. Verify the ap's survey reports 2412 MHz as the channel in use +. Verify the station is still associated to the ap after the surveys + + diff --git a/test/case/interfaces/wifi_channel_survey/test.py b/test/case/interfaces/wifi_channel_survey/test.py new file mode 100755 index 000000000..94091bdc1 --- /dev/null +++ b/test/case/interfaces/wifi_channel_survey/test.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +r""" +WiFi channel survey on a connected radio + +A channel survey tells how busy each channel is. Collecting it means +leaving the operating channel, so it is not done in the background but +on request, with the channel-survey action on the radio. The action has +to work on a radio that is in use: here on the ap, which is serving a +station, and on the station, which is associated to the ap. + +Topology: +.... + host ==(mgmt)== ap ))) ~ cell ~ ((( station ==(mgmt)== host +.... +""" +import infamy +import infamy.wifi as wifi +from infamy.util import until, parallel + +SSID = "infix-survey" +PSK = "infixinfix" +FREQ = 2412 # channel 1 + + +with infamy.Test() as test: + with test.step("Set up topology and attach to the ap and the station"): + env = infamy.Env() + ap, station = parallel( + lambda: env.attach("ap", "mgmt"), + lambda: env.attach("station", "mgmt"), + ) + wifi.skip_unless_supported(test, ap, station) + + with test.step("Configure the ap as an Access Point on channel 1 and the station on radio0"): + parallel( + lambda: ap.put_config_dicts({ + "ietf-hardware": wifi.hardware(wifi.radio("radio0", band="2.4GHz", channel=1)), + "ietf-keystore": wifi.keystore({"wifi": PSK}), + "ietf-interfaces": {"interfaces": {"interface": [ + wifi.iface("wifi0", "02:00:00:00:00:01", { + "radio": "radio0", + "access-point": { + "ssid": SSID, + "security": {"mode": "wpa2-wpa3-personal", "secret": "wifi"}, + }, + }), + ]}}, + }), + lambda: station.put_config_dicts({ + "ietf-hardware": wifi.hardware(wifi.radio("radio0")), + "ietf-keystore": wifi.keystore({"wifi": PSK}), + "ietf-interfaces": {"interfaces": {"interface": [ + wifi.iface("wifi0", "02:00:00:00:00:02", { + "radio": "radio0", + "station": { + "ssid": SSID, + "security": {"mode": "auto", "secret": "wifi"}, + }, + }), + ]}}, + }), + ) + + with test.step("Verify the station associates to the ap over the wifi link"): + until(lambda: wifi.associated(station, SSID), attempts=60, interval=2) + + with test.step("Run a channel survey on radio0 of the station"): + channels = wifi.channel_survey(station, "radio0") + + with test.step("Verify the station's survey reports 2412 MHz as the channel in use"): + assert wifi.in_use_frequency(channels) == FREQ, channels + + with test.step("Verify the station's survey covers more channels than the one in use"): + assert len(channels) > 1, channels + + with test.step("Run a channel survey on radio0 of the ap"): + channels = wifi.channel_survey(ap, "radio0") + + with test.step("Verify the ap's survey reports 2412 MHz as the channel in use"): + assert wifi.in_use_frequency(channels) == FREQ, channels + + with test.step("Verify the station is still associated to the ap after the surveys"): + until(lambda: wifi.associated(station, SSID), attempts=30, interval=2) + + test.succeed() diff --git a/test/case/interfaces/wifi_channel_survey/topology.dot b/test/case/interfaces/wifi_channel_survey/topology.dot new file mode 100644 index 000000000..4e8fd2123 --- /dev/null +++ b/test/case/interfaces/wifi_channel_survey/topology.dot @@ -0,0 +1,44 @@ +graph "wifi-channel-survey" { + layout="neato"; + overlap="false"; + esep="+40"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt1 | mgmt2 }", + pos="0,0!", + requires="controller", + ]; + + ap [ + label="{ mgmt | wifi } | ap", + pos="6,2!", + requires="infix", + ]; + + station [ + label="{ mgmt | wifi } | station", + pos="6,-2!", + requires="infix", + ]; + + // The wireless cell the ap and the station share, modelled as a medium + // node both join (one radio each, same index -> same cell): + // * physical: maps onto a real over-the-air RF cell; + // * virtual (qeneth): maps onto one of the multicast cells over which + // the wifimedium relay bridges mac80211_hwsim frames. + // The mapper guarantees the two DUTs actually share this medium. + cell [ + label="cell", + pos="9,0!", + requires="wifi-radio", + ]; + + host:mgmt1 -- ap:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- station:mgmt [requires="mgmt", color="lightgray"] + + ap:wifi -- cell [requires="wifi2.4GHz", style="dashed"] + station:wifi -- cell [requires="wifi2.4GHz", style="dashed"] +} diff --git a/test/case/interfaces/wifi_channel_survey/topology.svg b/test/case/interfaces/wifi_channel_survey/topology.svg new file mode 100644 index 000000000..f78f26411 --- /dev/null +++ b/test/case/interfaces/wifi_channel_survey/topology.svg @@ -0,0 +1,68 @@ + + + + + + +wifi-channel-survey + + + +host + +host + +mgmt1 + +mgmt2 + + + +ap + +mgmt + +wifi + +ap + + + +host:mgmt1--ap:mgmt + + + + +station + +mgmt + +wifi + +station + + + +host:mgmt2--station:mgmt + + + + +cell + +cell + + + +ap:wifi--cell + + + + +station:wifi--cell + + + + diff --git a/test/case/interfaces/wifi_mesh_roaming/test.adoc b/test/case/interfaces/wifi_mesh_roaming/test.adoc index 77290bed3..b74b724f2 100644 --- a/test/case/interfaces/wifi_mesh_roaming/test.adoc +++ b/test/case/interfaces/wifi_mesh_roaming/test.adoc @@ -60,5 +60,10 @@ image::topology.svg[WiFi Mesh backhaul with roaming Access Points topology, alig . Take down the client's current AP to force a roam . Verify the client roams to another node's AP . Verify connectivity is restored after roaming +. Stop the WiFi service on the client's current node +. Verify the client roams to the remaining node's AP +. Verify the client was asked to move, given candidates, and roamed without disconnecting +. Verify connectivity is restored after the handover +. Start the WiFi service again on the stopped node diff --git a/test/case/interfaces/wifi_mesh_roaming/test.py b/test/case/interfaces/wifi_mesh_roaming/test.py index a2c1ecc06..4c79fe40e 100755 --- a/test/case/interfaces/wifi_mesh_roaming/test.py +++ b/test/case/interfaces/wifi_mesh_roaming/test.py @@ -49,6 +49,7 @@ MESH_ID = "backhaul" MESH_PSK = "meshmeshmesh" WIFI_PSK = "infixinfix" +FT_SECRET = "roamroamroam" CLIENT_IP = "10.0.0.9" HOST_IP = "10.0.0.1" @@ -64,7 +65,7 @@ ("gw3", "02:00:00:00:00:03", "02:00:00:00:0a:03"), ] -SECRETS = {"mesh-secret": MESH_PSK, "wifi-secret": WIFI_PSK} +SECRETS = {"mesh-secret": MESH_PSK, "wifi-secret": WIFI_PSK, "ft-secret": FT_SECRET} def gw_config(mesh_mac, ap_mac, uplink=None): @@ -83,7 +84,7 @@ def gw_config(mesh_mac, ap_mac, uplink=None): "ssid": SSID, "security": {"mode": "wpa2-wpa3-personal", "secret": "wifi-secret"}, "roaming": { - "dot11r": {"mobility-domain": "hash"}, + "dot11r": {"key-holder-secret": "ft-secret", "mobility-domain": "hash"}, "dot11k": {}, "dot11v": {}, }, @@ -96,10 +97,9 @@ def gw_config(mesh_mac, ap_mac, uplink=None): "infix-interfaces:bridge-port": {"bridge": "br0"}, }) return { - "ietf-hardware": {"hardware": {"component": [ + "ietf-hardware": wifi.hardware( wifi.radio("radio0", band="5GHz", channel=36), - wifi.radio("radio1", band="2.4GHz", channel=1), - ]}}, + wifi.radio("radio1", band="2.4GHz", channel=1)), "ietf-keystore": wifi.keystore(SECRETS), "ietf-interfaces": {"interfaces": {"interface": interfaces}}, } @@ -111,13 +111,18 @@ def gw_config(mesh_mac, ap_mac, uplink=None): # Connect to all four nodes concurrently -- each attach probes the # node and downloads its YANG models, so doing them in parallel cuts # the setup time roughly four-fold. - gw1, gw2, gw3, client = parallel( + gw1, gw2, gw3, client, gw1sh, gw2sh, gw3sh, clientsh = parallel( lambda: env.attach("gw1", "mgmt"), lambda: env.attach("gw2", "mgmt"), lambda: env.attach("gw3", "mgmt"), lambda: env.attach("client", "mgmt"), + lambda: env.attach("gw1", "mgmt", "ssh"), + lambda: env.attach("gw2", "mgmt", "ssh"), + lambda: env.attach("gw3", "mgmt", "ssh"), + lambda: env.attach("client", "mgmt", "ssh"), ) gw_duts = [gw1, gw2, gw3] + shells = {"gw1": gw1sh, "gw2": gw2sh, "gw3": gw3sh} gws = [(name, dut, mesh, ap) for (name, mesh, ap), dut in zip(GWS, gw_duts)] wifi.skip_unless_supported(test, client, *gw_duts) @@ -136,8 +141,7 @@ def gw_config(mesh_mac, ap_mac, uplink=None): # associates to live in the same cell only when they share an index. # See doc/wifi.md and test/virt/quad. client.put_config_dicts({ - "ietf-hardware": {"hardware": {"component": [ - wifi.radio("radio1", band="2.4GHz", channel=1)]}}, + "ietf-hardware": wifi.hardware(wifi.radio("radio1", band="2.4GHz", channel=1)), "ietf-keystore": wifi.keystore(SECRETS), "ietf-interfaces": {"interfaces": {"interface": [ wifi.iface("wifi0", CLIENT_MAC, { @@ -183,8 +187,16 @@ def gw_config(mesh_mac, ap_mac, uplink=None): with infamy.IsolatedMacVlan(hlan) as ns: ns.addip(HOST_IP) + # Give a ping a few tries, the radios have just come up. + def reaches(addr): + try: + ns.ping(addr) + return True + except Exception: + return False + with test.step("Verify the client is reachable across the mesh"): - ns.must_reach(CLIENT_IP) + until(lambda: reaches(CLIENT_IP), attempts=10, interval=2) with test.step("Take down the client's current AP to force a roam"): first_dut.put_config_dicts({"ietf-interfaces": {"interfaces": { @@ -198,6 +210,35 @@ def gw_config(mesh_mac, ap_mac, uplink=None): print(f"client roamed from {first_ap} to {new_ap}") with test.step("Verify connectivity is restored after roaming"): - ns.must_reach(CLIENT_IP) + until(lambda: reaches(CLIENT_IP), attempts=15, interval=2) + + # A node going down for a reboot or an upgrade asks its clients + # to move first, so a client with a strong signal roams instead + # of waiting for the beacons to stop. + second_bssid = wifi.station_bssid(client) + second_ap, _ = aps[second_bssid] + + with test.step("Stop the WiFi service on the client's current node"): + shells[second_ap].runsh("initctl stop hostapd") + + with test.step("Verify the client roams to the remaining node's AP"): + until(lambda: wifi.station_bssid(client) in aps and + wifi.station_bssid(client) not in (first_bssid, second_bssid), + attempts=30, interval=1) + third_ap, _ = aps[wifi.station_bssid(client)] + print(f"client roamed from {second_ap} to {third_ap}") + + with test.step("Verify the client was asked to move, given candidates, and roamed without disconnecting"): + def log(): + return clientsh.runsh("sed -n '/WNM: Disassociation Imminent/,$p' /var/log/syslog").stdout + until(lambda: "Disassociation Imminent" in log(), attempts=10, interval=1) + assert "Preferred List Available" in log(), log() + assert "CTRL-EVENT-DISCONNECTED" not in log(), log() + + with test.step("Verify connectivity is restored after the handover"): + until(lambda: reaches(CLIENT_IP), attempts=15, interval=2) + + with test.step("Start the WiFi service again on the stopped node"): + shells[second_ap].runsh("initctl start hostapd") test.succeed() diff --git a/test/case/interfaces/wifi_station_from_scan/Readme.adoc b/test/case/interfaces/wifi_station_from_scan/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/wifi_station_from_scan/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/wifi_station_from_scan/test.adoc b/test/case/interfaces/wifi_station_from_scan/test.adoc new file mode 100644 index 000000000..3eb98e127 --- /dev/null +++ b/test/case/interfaces/wifi_station_from_scan/test.adoc @@ -0,0 +1,32 @@ +=== WiFi station set up from a scan-only interface + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wifi_station_from_scan] + +==== Description + +A WiFi interface with only a radio, no station or access point, is in +scan-only mode. That is how the factory configuration of boards with a +built-in radio ships, so the usual way to get online is to add the station +settings to that existing interface. The connection has to come up from +that change alone, without a reboot or a service restart. + +Topology: +.... + host ==(mgmt)== ap ))) ~ cell ~ ((( station ==(mgmt)== host +.... + +==== Topology + +image::topology.svg[WiFi station set up from a scan-only interface topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to the ap and the station +. Configure the ap with access point 'infix-scan' and a DHCP server on 192.168.21.1 +. Configure wifi0 on the station with only radio0, scan-only mode +. Verify the station sees 'infix-scan' in its scan results +. Add station settings for 'infix-scan' to wifi0 on the station +. Verify the station associates to 'infix-scan' without a restart +. Verify the station leases 192.168.21.100 over wifi + + diff --git a/test/case/interfaces/wifi_station_from_scan/test.py b/test/case/interfaces/wifi_station_from_scan/test.py new file mode 100755 index 000000000..9af9a916f --- /dev/null +++ b/test/case/interfaces/wifi_station_from_scan/test.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +r""" +WiFi station set up from a scan-only interface + +A WiFi interface with only a radio, no station or access point, is in +scan-only mode. That is how the factory configuration of boards with a +built-in radio ships, so the usual way to get online is to add the station +settings to that existing interface. The connection has to come up from +that change alone, without a reboot or a service restart. + +Topology: +.... + host ==(mgmt)== ap ))) ~ cell ~ ((( station ==(mgmt)== host +.... +""" +import infamy +import infamy.iface as iface +import infamy.wifi as wifi +from infamy.util import until, parallel + +SSID = "infix-scan" +PSK = "infixinfix" + +SUBNET = "192.168.21.0/24" +AP_IP = "192.168.21.1" +LEASE = "192.168.21.100" + + +with infamy.Test() as test: + with test.step("Set up topology and attach to the ap and the station"): + env = infamy.Env() + ap, station = parallel( + lambda: env.attach("ap", "mgmt"), + lambda: env.attach("station", "mgmt"), + ) + wifi.skip_unless_supported(test, ap, station) + + with test.step("Configure the ap with access point 'infix-scan' and a DHCP server on 192.168.21.1"): + ap.put_config_dicts({ + "ietf-hardware": wifi.hardware(wifi.radio("radio0", band="2.4GHz", channel=1)), + "ietf-keystore": wifi.keystore({"wifi": PSK}), + "ietf-interfaces": {"interfaces": {"interface": [ + wifi.iface("wifi0", "02:00:00:00:00:01", { + "radio": "radio0", + "access-point": { + "ssid": SSID, + "security": {"mode": "wpa2-wpa3-personal", "secret": "wifi"}, + }, + }, ipv4={"address": [{"ip": AP_IP, "prefix-length": 24}]}), + ]}}, + "infix-dhcp-server": {"dhcp-server": {"subnet": [{ + "subnet": SUBNET, + "pool": {"start-address": LEASE, "end-address": LEASE}, + }]}}, + }) + + with test.step("Configure wifi0 on the station with only radio0, scan-only mode"): + station.put_config_dicts({ + "ietf-hardware": wifi.hardware(wifi.radio("radio0")), + "ietf-interfaces": {"interfaces": {"interface": [ + wifi.iface("wifi0", "02:00:00:00:00:02", {"radio": "radio0"}, + ipv4={"infix-dhcp-client:dhcp": {}}), + ]}}, + }) + + with test.step("Verify the station sees 'infix-scan' in its scan results"): + until(lambda: SSID in {n.get("ssid") for n in + wifi.station(station).get("scan-results") or []}, + attempts=60, interval=2) + + with test.step("Add station settings for 'infix-scan' to wifi0 on the station"): + station.put_config_dicts({ + "ietf-keystore": wifi.keystore({"wifi": PSK}), + "ietf-interfaces": {"interfaces": {"interface": [ + {"name": "wifi0", "infix-interfaces:wifi": { + "radio": "radio0", + "station": { + "ssid": SSID, + "security": {"mode": "auto", "secret": "wifi"}, + }, + }}, + ]}}, + }) + + with test.step("Verify the station associates to 'infix-scan' without a restart"): + until(lambda: wifi.associated(station, SSID), attempts=60, interval=2) + + with test.step("Verify the station leases 192.168.21.100 over wifi"): + until(lambda: iface.address_exist(station, "wifi0", LEASE), + attempts=60, interval=2) + + test.succeed() diff --git a/test/case/interfaces/wifi_station_from_scan/topology.dot b/test/case/interfaces/wifi_station_from_scan/topology.dot new file mode 100644 index 000000000..7a05357a1 --- /dev/null +++ b/test/case/interfaces/wifi_station_from_scan/topology.dot @@ -0,0 +1,44 @@ +graph "wifi-station-from-scan" { + layout="neato"; + overlap="false"; + esep="+40"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt1 | mgmt2 }", + pos="0,0!", + requires="controller", + ]; + + ap [ + label="{ mgmt | wifi } | ap", + pos="6,2!", + requires="infix", + ]; + + station [ + label="{ mgmt | wifi } | station", + pos="6,-2!", + requires="infix", + ]; + + // The wireless cell the ap and the station share, modelled as a medium + // node both join (one radio each, same index -> same cell): + // * physical: maps onto a real over-the-air RF cell; + // * virtual (qeneth): maps onto one of the multicast cells over which + // the wifimedium relay bridges mac80211_hwsim frames. + // The mapper guarantees the two DUTs actually share this medium. + cell [ + label="cell", + pos="9,0!", + requires="wifi-radio", + ]; + + host:mgmt1 -- ap:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- station:mgmt [requires="mgmt", color="lightgray"] + + ap:wifi -- cell [requires="wifi2.4GHz", style="dashed"] + station:wifi -- cell [requires="wifi2.4GHz", style="dashed"] +} diff --git a/test/case/interfaces/wifi_station_from_scan/topology.svg b/test/case/interfaces/wifi_station_from_scan/topology.svg new file mode 100644 index 000000000..9c8741aff --- /dev/null +++ b/test/case/interfaces/wifi_station_from_scan/topology.svg @@ -0,0 +1,68 @@ + + + + + + +wifi-station-from-scan + + + +host + +host + +mgmt1 + +mgmt2 + + + +ap + +mgmt + +wifi + +ap + + + +host:mgmt1--ap:mgmt + + + + +station + +mgmt + +wifi + +station + + + +host:mgmt2--station:mgmt + + + + +cell + +cell + + + +ap:wifi--cell + + + + +station:wifi--cell + + + + diff --git a/test/case/interfaces/wifi_wds_link_2dut/Readme.adoc b/test/case/interfaces/wifi_wds_link_2dut/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/wifi_wds_link_2dut/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/wifi_wds_link_2dut/test.adoc b/test/case/interfaces/wifi_wds_link_2dut/test.adoc new file mode 100644 index 000000000..9d2bfdf02 --- /dev/null +++ b/test/case/interfaces/wifi_wds_link_2dut/test.adoc @@ -0,0 +1,47 @@ +=== WiFi 4-address (WDS) link between two bridges + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wifi_wds_link_2dut] + +==== Description + +Two DUTs: the root runs an access point with a WDS port, the satellite a +4-address station. Both ends are bridge ports, so the radio link joins +the two bridges at layer 2. + +On the root, wds0 is created by configuration before any station shows up +and gets its VLAN membership like any other bridge port. The access point +binds the station with the configured MAC address to it: the port comes up +when the station associates and goes down, but stays, when it leaves. On +the satellite, the station is a bridge port, which needs 4-address mode. + +The DHCP lease over the link is the data-plane check: the request and the +reply cross both bridges and the radio in opposite directions. + +Topology: +.... + host ==(mgmt)== root ))) ~ cell ~ ((( satellite ==(mgmt)== host + br0/vlan10 -- wds0 ~~~~~~~~~~~~~~~~~~~~~~ wifi0 -- br0 +.... + +==== Topology + +image::topology.svg[WiFi 4-address (WDS) link between two bridges topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to the root and the satellite +. Configure the root with access point 'infix-wds' and WDS port wds0 untagged in VLAN 10 on br0 +. Verify wds0 on the root exists, is down and is an untagged member of VLAN 10 +. Configure the satellite with a 4-address station for 'infix-wds' in br0, br0 as DHCP client +. Verify the satellite's wifi0 associates to 'infix-wds' +. Verify wds0 on the root comes up and reports the station connected +. Verify access point wifi0 on the root lists the satellite 02:00:00:00:00:02 as a station +. Verify the satellite leases 192.168.20.100 on br0 over the WDS link +. Set a description on wds0 on the root +. Verify wds0 on the root stays up with the station connected +. Disable wifi0 on the satellite +. Verify wds0 on the root goes down but remains a member of VLAN 10 +. Enable wifi0 on the satellite again +. Verify wds0 on the root comes up again + + diff --git a/test/case/interfaces/wifi_wds_link_2dut/test.py b/test/case/interfaces/wifi_wds_link_2dut/test.py new file mode 100755 index 000000000..58e523edf --- /dev/null +++ b/test/case/interfaces/wifi_wds_link_2dut/test.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +r""" +WiFi 4-address (WDS) link between two bridges + +Two DUTs: the root runs an access point with a WDS port, the satellite a +4-address station. Both ends are bridge ports, so the radio link joins +the two bridges at layer 2. + +On the root, wds0 is created by configuration before any station shows up +and gets its VLAN membership like any other bridge port. The access point +binds the station with the configured MAC address to it: the port comes up +when the station associates and goes down, but stays, when it leaves. On +the satellite, the station is a bridge port, which needs 4-address mode. + +The DHCP lease over the link is the data-plane check: the request and the +reply cross both bridges and the radio in opposite directions. + +Topology: +.... + host ==(mgmt)== root ))) ~ cell ~ ((( satellite ==(mgmt)== host + br0/vlan10 -- wds0 ~~~~~~~~~~~~~~~~~~~~~~ wifi0 -- br0 +.... +""" +import infamy +import infamy.iface as iface +import infamy.wifi as wifi + +from infamy.util import until, parallel + +SSID = "infix-wds" +PSK = "infixinfix" + +ROOT_AP_MAC = "02:00:00:00:00:01" +SAT_MAC = "02:00:00:00:00:02" + +SUBNET = "192.168.20.0/24" +ROOT_IP = "192.168.20.1" +LEASE = "192.168.20.100" + + +def root_config(): + return { + "ietf-hardware": wifi.hardware(wifi.radio("radio0", band="2.4GHz", channel=1)), + "ietf-keystore": wifi.keystore({"wifi": PSK}), + "ietf-interfaces": {"interfaces": {"interface": [ + { + "name": "br0", + "type": "infix-if-type:bridge", + "enabled": True, + "bridge": {"vlans": {"vlan": [ + {"vid": 10, "untagged": ["wds0"], "tagged": ["br0"]}, + ]}}, + }, + { + "name": "vlan10", + "type": "infix-if-type:vlan", + "enabled": True, + "vlan": {"id": 10, "lower-layer-if": "br0"}, + "ipv4": {"address": [{"ip": ROOT_IP, "prefix-length": 24}]}, + }, + wifi.iface("wifi0", ROOT_AP_MAC, { + "radio": "radio0", + "access-point": { + "ssid": SSID, + "security": {"mode": "wpa3-personal", "secret": "wifi"}, + }, + }), + wifi.wds_link("wds0", "wifi0", SAT_MAC, bridge="br0", pvid=10), + ]}}, + "infix-dhcp-server": {"dhcp-server": {"subnet": [{ + "subnet": SUBNET, + "pool": {"start-address": LEASE, "end-address": LEASE}, + }]}}, + } + + +def satellite_config(): + return { + "ietf-hardware": wifi.hardware(wifi.radio("radio0")), + "ietf-keystore": wifi.keystore({"wifi": PSK}), + "ietf-interfaces": {"interfaces": {"interface": [ + { + "name": "br0", + "type": "infix-if-type:bridge", + "enabled": True, + "ietf-ip:ipv4": {"infix-dhcp-client:dhcp": {}}, + }, + wifi.iface("wifi0", SAT_MAC, { + "radio": "radio0", + "station": { + "ssid": SSID, + "wds": True, + "peer-bssid": ROOT_AP_MAC, + "security": {"mode": "auto", "secret": "wifi"}, + }, + }, bridge="br0"), + ]}}, + } + + +def station_enabled(enabled): + return {"ietf-interfaces": {"interfaces": {"interface": [ + {"name": "wifi0", "enabled": enabled}]}}} + + +with infamy.Test() as test: + with test.step("Set up topology and attach to the root and the satellite"): + env = infamy.Env() + root, satellite = parallel( + lambda: env.attach("root", "mgmt"), + lambda: env.attach("satellite", "mgmt"), + ) + wifi.skip_unless_supported(test, root, satellite) + + with test.step("Configure the root with access point 'infix-wds' and WDS port wds0 untagged in VLAN 10 on br0"): + root.put_config_dicts(root_config()) + + with test.step("Verify wds0 on the root exists, is down and is an untagged member of VLAN 10"): + until(lambda: iface.exist(root, "wds0"), attempts=30) + until(lambda: "wds0" in wifi.bridge_vlan_members(root, "br0", 10), attempts=30) + if iface.is_oper_up(root, "wds0"): + test.fail() + + with test.step("Configure the satellite with a 4-address station for 'infix-wds' in br0, br0 as DHCP client"): + satellite.put_config_dicts(satellite_config()) + + with test.step("Verify the satellite's wifi0 associates to 'infix-wds'"): + until(lambda: wifi.associated(satellite, SSID), attempts=60, interval=2) + + with test.step("Verify wds0 on the root comes up and reports the station connected"): + until(lambda: iface.is_oper_up(root, "wds0"), attempts=30, interval=2) + until(lambda: wifi.wds_connected(root, "wds0"), attempts=30, interval=2) + + with test.step("Verify access point wifi0 on the root lists the satellite 02:00:00:00:00:02 as a station"): + until(lambda: SAT_MAC in wifi.ap_stations(root, "wifi0"), attempts=30, interval=2) + + with test.step("Verify the satellite leases 192.168.20.100 on br0 over the WDS link"): + until(lambda: iface.address_exist(satellite, "br0", LEASE), + attempts=60, interval=2) + + with test.step("Set a description on wds0 on the root"): + root.put_config_dicts({"ietf-interfaces": {"interfaces": {"interface": [ + {"name": "wds0", "description": "satellite"}]}}}) + + with test.step("Verify wds0 on the root stays up with the station connected"): + until(lambda: iface.is_oper_up(root, "wds0") and wifi.wds_connected(root, "wds0"), + attempts=10, interval=1) + + with test.step("Disable wifi0 on the satellite"): + satellite.put_config_dicts(station_enabled(False)) + + with test.step("Verify wds0 on the root goes down but remains a member of VLAN 10"): + until(lambda: not iface.is_oper_up(root, "wds0"), attempts=90, interval=2) + until(lambda: not wifi.wds_connected(root, "wds0"), attempts=30, interval=2) + if "wds0" not in wifi.bridge_vlan_members(root, "br0", 10): + test.fail() + + with test.step("Enable wifi0 on the satellite again"): + satellite.put_config_dicts(station_enabled(True)) + + with test.step("Verify wds0 on the root comes up again"): + until(lambda: iface.is_oper_up(root, "wds0"), attempts=60, interval=2) + until(lambda: wifi.wds_connected(root, "wds0"), attempts=30, interval=2) + + test.succeed() diff --git a/test/case/interfaces/wifi_wds_link_2dut/topology.dot b/test/case/interfaces/wifi_wds_link_2dut/topology.dot new file mode 100644 index 000000000..c626b88ae --- /dev/null +++ b/test/case/interfaces/wifi_wds_link_2dut/topology.dot @@ -0,0 +1,40 @@ +graph "wifi-wds-link-2dut" { + layout="neato"; + overlap="false"; + esep="+40"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt1 | mgmt2 }", + pos="0,0!", + requires="controller", + ]; + + root [ + label="{ mgmt | wifi } | root", + pos="6,2!", + requires="infix", + ]; + + satellite [ + label="{ mgmt | wifi } | satellite", + pos="6,-2!", + requires="infix", + ]; + + // The wireless cell the root and the satellite share, see + // wifi_ap_station_2dut for how it maps onto RF and onto hwsim. + cell [ + label="cell", + pos="9,0!", + requires="wifi-radio", + ]; + + host:mgmt1 -- root:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- satellite:mgmt [requires="mgmt", color="lightgray"] + + root:wifi -- cell [requires="wifi2.4GHz", style="dashed"] + satellite:wifi -- cell [requires="wifi2.4GHz", style="dashed"] +} diff --git a/test/case/interfaces/wifi_wds_link_2dut/topology.svg b/test/case/interfaces/wifi_wds_link_2dut/topology.svg new file mode 100644 index 000000000..d17cd1db9 --- /dev/null +++ b/test/case/interfaces/wifi_wds_link_2dut/topology.svg @@ -0,0 +1,68 @@ + + + + + + +wifi-wds-link-2dut + + + +host + +host + +mgmt1 + +mgmt2 + + + +root + +mgmt + +wifi + +root + + + +host:mgmt1--root:mgmt + + + + +satellite + +mgmt + +wifi + +satellite + + + +host:mgmt2--satellite:mgmt + + + + +cell + +cell + + + +root:wifi--cell + + + + +satellite:wifi--cell + + + + diff --git a/test/case/interfaces/wifi_wds_repeater/Readme.adoc b/test/case/interfaces/wifi_wds_repeater/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/wifi_wds_repeater/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/wifi_wds_repeater/test.adoc b/test/case/interfaces/wifi_wds_repeater/test.adoc new file mode 100644 index 000000000..252c468db --- /dev/null +++ b/test/case/interfaces/wifi_wds_repeater/test.adoc @@ -0,0 +1,63 @@ +=== WiFi repeater with two SSIDs in VLANs over a 4-address (WDS) backhaul + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wifi_wds_repeater] + +==== Description + +Four DUTs. The root runs the backhaul access point 'infix-backhaul' and +a VLAN filtering bridge: the WDS port and the wired uplink carry VLAN 10 +and VLAN 20 tagged, and the root serves DHCP in each VLAN. The repeater +has a 4-address station on the backhaul, carrying both VLANs tagged, and +two access points on the same radio as access ports: 'infix-home' +untagged in VLAN 10 and 'infix-guest' untagged in VLAN 20. Two plain +stations join them, one per SSID. + +Each station must lease an address from the DHCP server of its own VLAN +on the root. That proves both that the 4-address backhaul carries the +stations' own MAC addresses and that the VLAN tags survive the trip. +The host behind the root reaches both stations on their VLANs. + +Taking the backhaul down and up again shows it is a transparent bridge +port: the stations lose and regain reach without re-associating. + +Finally the guest access point gets a new MAC address, which recreates +its netdev. The home access point on the same radio must not be +disturbed, and the guest access point has to come back at the new +address with its client. + +Topology: +.... + host ==(lan, VLAN 10+20)== root (AP 'infix-backhaul') + wds0 ))) ~ cell ~ ((( wifi0 repeater wifi1 (AP 'infix-home', VLAN 10) ))) home + wifi2 (AP 'infix-guest', VLAN 20) ))) guest +.... + +==== Topology + +image::topology.svg[WiFi repeater with two SSIDs in VLANs over a 4-address (WDS) backhaul topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to the root, the repeater, home and guest +. Configure the root with access point 'infix-backhaul', WDS port wds0 and the uplink tagged in VLAN 10 and 20, DHCP in each VLAN +. Configure the repeater with a 4-address station tagged in VLAN 10 and 20, access point 'infix-home' untagged in VLAN 10 and 'infix-guest' untagged in VLAN 20 +. Configure home as a DHCP station for 'infix-home' and guest as a DHCP station for 'infix-guest' +. Verify the repeater's wifi0 associates to 'infix-backhaul' +. Verify wds0 on the root is up +. Verify home is on the repeater's 'infix-home' access point, BSSID 02:00:00:00:0a:02 +. Verify guest is on the repeater's 'infix-guest' access point, BSSID 02:00:00:00:0b:02 +. Verify home leases 10.10.0.9 from the root's VLAN 10 DHCP server through the backhaul +. Verify guest leases 10.20.0.9 from the root's VLAN 20 DHCP server through the backhaul +. Verify the host reaches home at 10.10.0.9 on VLAN 10 through the repeater +. Verify the host reaches guest at 10.20.0.9 on VLAN 20 through the repeater +. Disable the repeater's backhaul station wifi0 +. Verify home at 10.10.0.9 and guest at 10.20.0.9 are no longer reachable from the host +. Enable the repeater's backhaul station wifi0 again +. Verify the host reaches home at 10.10.0.9 and guest at 10.20.0.9 again +. Change the address of the repeater's 'infix-guest' access point to 02:00:00:00:0b:12 +. Verify wifi2 on the repeater reports the new address and both access points are up +. Verify guest associates to 'infix-guest' at the new address, BSSID 02:00:00:00:0b:12 +. Verify the host reaches home at 10.10.0.9 and guest at 10.20.0.9 after the address change +. Verify home is still on its access point + + diff --git a/test/case/interfaces/wifi_wds_repeater/test.py b/test/case/interfaces/wifi_wds_repeater/test.py new file mode 100755 index 000000000..bc92e981f --- /dev/null +++ b/test/case/interfaces/wifi_wds_repeater/test.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +r""" +WiFi repeater with two SSIDs in VLANs over a 4-address (WDS) backhaul + +Four DUTs. The root runs the backhaul access point 'infix-backhaul' and +a VLAN filtering bridge: the WDS port and the wired uplink carry VLAN 10 +and VLAN 20 tagged, and the root serves DHCP in each VLAN. The repeater +has a 4-address station on the backhaul, carrying both VLANs tagged, and +two access points on the same radio as access ports: 'infix-home' +untagged in VLAN 10 and 'infix-guest' untagged in VLAN 20. Two plain +stations join them, one per SSID. + +Each station must lease an address from the DHCP server of its own VLAN +on the root. That proves both that the 4-address backhaul carries the +stations' own MAC addresses and that the VLAN tags survive the trip. +The host behind the root reaches both stations on their VLANs. + +Taking the backhaul down and up again shows it is a transparent bridge +port: the stations lose and regain reach without re-associating. + +Finally the guest access point gets a new MAC address, which recreates +its netdev. The home access point on the same radio must not be +disturbed, and the guest access point has to come back at the new +address with its client. + +Topology: +.... + host ==(lan, VLAN 10+20)== root (AP 'infix-backhaul') + wds0 ))) ~ cell ~ ((( wifi0 repeater wifi1 (AP 'infix-home', VLAN 10) ))) home + wifi2 (AP 'infix-guest', VLAN 20) ))) guest +.... +""" +import infamy +import infamy.iface as iface +import infamy.wifi as wifi +from infamy.util import until, parallel + +BACKHAUL_SSID = "infix-backhaul" +HOME_SSID = "infix-home" +GUEST_SSID = "infix-guest" +PSK = "infixinfix" + +ROOT_AP_MAC = "02:00:00:00:00:01" +REPEATER_STA_MAC = "02:00:00:00:00:02" +HOME_AP_MAC = "02:00:00:00:0a:02" +GUEST_AP_MAC = "02:00:00:00:0b:02" +GUEST_AP_MAC_NEW = "02:00:00:00:0b:12" +HOME_MAC = "02:00:00:00:00:09" +GUEST_MAC = "02:00:00:00:00:0a" + +HOME_HOST_IP = "10.10.0.1" +HOME_ROOT_IP = "10.10.0.2" +HOME_IP = "10.10.0.9" +GUEST_HOST_IP = "10.20.0.1" +GUEST_ROOT_IP = "10.20.0.2" +GUEST_IP = "10.20.0.9" + +SECRETS = {"backhaul": PSK, "home": PSK, "guest": PSK} + + +def root_config(uplink): + return { + "ietf-hardware": wifi.hardware(wifi.radio("radio0", band="2.4GHz", channel=1)), + "ietf-keystore": wifi.keystore(SECRETS), + "ietf-interfaces": {"interfaces": {"interface": [ + {"name": "br0", "type": "infix-if-type:bridge", "enabled": True, + "bridge": {"vlans": {"vlan": [ + {"vid": 10, "tagged": [uplink, "wds0", "br0"]}, + {"vid": 20, "tagged": [uplink, "wds0", "br0"]}, + ]}}}, + {"name": "vlan10", "type": "infix-if-type:vlan", "enabled": True, + "vlan": {"id": 10, "lower-layer-if": "br0"}, + "ipv4": {"address": [{"ip": HOME_ROOT_IP, "prefix-length": 24}]}}, + {"name": "vlan20", "type": "infix-if-type:vlan", "enabled": True, + "vlan": {"id": 20, "lower-layer-if": "br0"}, + "ipv4": {"address": [{"ip": GUEST_ROOT_IP, "prefix-length": 24}]}}, + {"name": uplink, "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}}, + wifi.iface("wifi0", ROOT_AP_MAC, { + "radio": "radio0", + "access-point": { + "ssid": BACKHAUL_SSID, + "security": {"mode": "wpa2-wpa3-personal", "secret": "backhaul"}, + }, + }), + wifi.wds_link("wds0", "wifi0", REPEATER_STA_MAC, bridge="br0"), + ]}}, + "infix-dhcp-server": {"dhcp-server": {"subnet": [ + {"subnet": "10.10.0.0/24", + "pool": {"start-address": "10.10.0.100", "end-address": "10.10.0.100"}, + "host": [{"address": HOME_IP, "match": {"mac-address": HOME_MAC}}]}, + {"subnet": "10.20.0.0/24", + "pool": {"start-address": "10.20.0.100", "end-address": "10.20.0.100"}, + "host": [{"address": GUEST_IP, "match": {"mac-address": GUEST_MAC}}]}, + ]}}, + } + + +def repeater_config(): + return { + "ietf-hardware": wifi.hardware(wifi.radio("radio0", band="2.4GHz", channel=1)), + "ietf-keystore": wifi.keystore(SECRETS), + "ietf-interfaces": {"interfaces": {"interface": [ + {"name": "br0", "type": "infix-if-type:bridge", "enabled": True, + "bridge": {"vlans": {"vlan": [ + {"vid": 10, "untagged": ["wifi1"], "tagged": ["wifi0"]}, + {"vid": 20, "untagged": ["wifi2"], "tagged": ["wifi0"]}, + ]}}}, + wifi.iface("wifi0", REPEATER_STA_MAC, { + "radio": "radio0", + "station": { + "ssid": BACKHAUL_SSID, + "wds": True, + "peer-bssid": ROOT_AP_MAC, + "security": {"mode": "auto", "secret": "backhaul"}, + }, + }, bridge="br0"), + wifi.iface("wifi1", HOME_AP_MAC, { + "radio": "radio0", + "access-point": { + "ssid": HOME_SSID, + "security": {"mode": "wpa2-wpa3-personal", "secret": "home"}, + }, + }, bridge="br0", pvid=10), + wifi.iface("wifi2", GUEST_AP_MAC, { + "radio": "radio0", + "access-point": { + "ssid": GUEST_SSID, + "security": {"mode": "wpa2-wpa3-personal", "secret": "guest"}, + }, + }, bridge="br0", pvid=20), + ]}}, + } + + +def station_config(mac, ssid, secret): + return { + "ietf-hardware": wifi.hardware(wifi.radio("radio0")), + "ietf-keystore": wifi.keystore(SECRETS), + "ietf-interfaces": {"interfaces": {"interface": [ + wifi.iface("wifi0", mac, { + "radio": "radio0", + "station": { + "ssid": ssid, + "security": {"mode": "auto", "secret": secret}, + }, + }, ipv4={"infix-dhcp-client:dhcp": {}}), + ]}}, + } + + +def backhaul_enabled(enabled): + return {"ietf-interfaces": {"interfaces": {"interface": [ + {"name": "wifi0", "enabled": enabled}]}}} + + +def reaches(ns, addr): + try: + ns.ping(addr) + return True + except Exception: + return False + + +with infamy.Test() as test: + with test.step("Set up topology and attach to the root, the repeater, home and guest"): + env = infamy.Env() + root, repeater, home, guest = parallel( + lambda: env.attach("root", "mgmt"), + lambda: env.attach("repeater", "mgmt"), + lambda: env.attach("home", "mgmt"), + lambda: env.attach("guest", "mgmt"), + ) + wifi.skip_unless_supported(test, root, repeater, home, guest) + + with test.step("Configure the root with access point 'infix-backhaul', WDS port wds0 and the uplink tagged in VLAN 10 and 20, DHCP in each VLAN"): + _, uplink = env.ltop.xlate("root", "uplink") + root.put_config_dicts(root_config(uplink)) + + with test.step("Configure the repeater with a 4-address station tagged in VLAN 10 and 20, access point 'infix-home' untagged in VLAN 10 and 'infix-guest' untagged in VLAN 20"): + repeater.put_config_dicts(repeater_config()) + + with test.step("Configure home as a DHCP station for 'infix-home' and guest as a DHCP station for 'infix-guest'"): + parallel( + lambda: home.put_config_dicts(station_config(HOME_MAC, HOME_SSID, "home")), + lambda: guest.put_config_dicts(station_config(GUEST_MAC, GUEST_SSID, "guest")), + ) + + with test.step("Verify the repeater's wifi0 associates to 'infix-backhaul'"): + until(lambda: wifi.associated(repeater, BACKHAUL_SSID), attempts=60, interval=2) + + with test.step("Verify wds0 on the root is up"): + until(lambda: iface.is_oper_up(root, "wds0"), attempts=30, interval=2) + + with test.step("Verify home is on the repeater's 'infix-home' access point, BSSID 02:00:00:00:0a:02"): + until(lambda: wifi.station_bssid(home) == HOME_AP_MAC, attempts=60, interval=2) + + with test.step("Verify guest is on the repeater's 'infix-guest' access point, BSSID 02:00:00:00:0b:02"): + until(lambda: wifi.station_bssid(guest) == GUEST_AP_MAC, attempts=60, interval=2) + + with test.step("Verify home leases 10.10.0.9 from the root's VLAN 10 DHCP server through the backhaul"): + until(lambda: iface.address_exist(home, "wifi0", HOME_IP), attempts=60, interval=2) + + with test.step("Verify guest leases 10.20.0.9 from the root's VLAN 20 DHCP server through the backhaul"): + until(lambda: iface.address_exist(guest, "wifi0", GUEST_IP), attempts=60, interval=2) + + _, hlan = env.ltop.xlate("host", "lan") + with infamy.IsolatedMacVlan(hlan) as ns: + ns.runsh(f""" + set -ex + ip link add dev vlan10 link iface up type vlan id 10 + ip link add dev vlan20 link iface up type vlan id 20 + ip addr add {HOME_HOST_IP}/24 dev vlan10 + ip addr add {GUEST_HOST_IP}/24 dev vlan20 + """) + + with test.step("Verify the host reaches home at 10.10.0.9 on VLAN 10 through the repeater"): + until(lambda: reaches(ns, HOME_IP), attempts=30, interval=2) + + with test.step("Verify the host reaches guest at 10.20.0.9 on VLAN 20 through the repeater"): + until(lambda: reaches(ns, GUEST_IP), attempts=30, interval=2) + + with test.step("Disable the repeater's backhaul station wifi0"): + repeater.put_config_dicts(backhaul_enabled(False)) + + with test.step("Verify home at 10.10.0.9 and guest at 10.20.0.9 are no longer reachable from the host"): + until(lambda: not iface.is_oper_up(root, "wds0"), attempts=30, interval=2) + ns.must_not_reach(HOME_IP) + ns.must_not_reach(GUEST_IP) + + with test.step("Enable the repeater's backhaul station wifi0 again"): + repeater.put_config_dicts(backhaul_enabled(True)) + + with test.step("Verify the host reaches home at 10.10.0.9 and guest at 10.20.0.9 again"): + until(lambda: iface.is_oper_up(root, "wds0"), attempts=60, interval=2) + until(lambda: reaches(ns, HOME_IP), attempts=30, interval=2) + until(lambda: reaches(ns, GUEST_IP), attempts=30, interval=2) + + with test.step("Change the address of the repeater's 'infix-guest' access point to 02:00:00:00:0b:12"): + repeater.put_config_dicts({"ietf-interfaces": {"interfaces": {"interface": [{ + "name": "wifi2", + "custom-phys-address": {"static": GUEST_AP_MAC_NEW}, + }]}}}) + + with test.step("Verify wifi2 on the repeater reports the new address and both access points are up"): + until(lambda: (iface.get_phys_address(repeater, "wifi2") or "").lower() == GUEST_AP_MAC_NEW, + attempts=30, interval=2) + until(lambda: iface.is_oper_up(repeater, "wifi2"), attempts=45, interval=2) + until(lambda: iface.is_oper_up(repeater, "wifi1"), attempts=30, interval=2) + + with test.step("Verify guest associates to 'infix-guest' at the new address, BSSID 02:00:00:00:0b:12"): + until(lambda: wifi.station_bssid(guest) == GUEST_AP_MAC_NEW, attempts=60, interval=2) + + with test.step("Verify the host reaches home at 10.10.0.9 and guest at 10.20.0.9 after the address change"): + until(lambda: reaches(ns, HOME_IP), attempts=30, interval=2) + until(lambda: reaches(ns, GUEST_IP), attempts=30, interval=2) + + with test.step("Verify home is still on its access point"): + if wifi.station_bssid(home) != HOME_AP_MAC: + test.fail() + + test.succeed() diff --git a/test/case/interfaces/wifi_wds_repeater/topology.dot b/test/case/interfaces/wifi_wds_repeater/topology.dot new file mode 100644 index 000000000..3d5489e30 --- /dev/null +++ b/test/case/interfaces/wifi_wds_repeater/topology.dot @@ -0,0 +1,59 @@ +graph "wifi-wds-repeater" { + layout="neato"; + overlap="false"; + esep="+40"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt1 | mgmt2 | mgmt3 | mgmt4 | lan }", + pos="0,0!", + requires="controller", + ]; + + root [ + label="{ mgmt | uplink | wifi } | root", + pos="6,3!", + requires="infix", + ]; + + repeater [ + label="{ mgmt | wifi } | repeater", + pos="9,0!", + requires="infix", + ]; + + home [ + label="{ mgmt | wifi } | home", + pos="6,-3!", + requires="infix", + ]; + + guest [ + label="{ mgmt | wifi } | guest", + pos="12,-3!", + requires="infix", + ]; + + // One cell: the repeater's backhaul station and its two access points + // share a radio, so all four nodes hear each other. Each SSID is + // advertised by exactly one node, which decides who joins whom. + cell [ + label="cell", + pos="12,0!", + requires="wifi-radio", + ]; + + host:mgmt1 -- root:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- repeater:mgmt [requires="mgmt", color="lightgray"] + host:mgmt3 -- home:mgmt [requires="mgmt", color="lightgray"] + host:mgmt4 -- guest:mgmt [requires="mgmt", color="lightgray"] + + host:lan -- root:uplink [color="black"] + + root:wifi -- cell [requires="wifi2.4GHz", style="dashed"] + repeater:wifi -- cell [requires="wifi2.4GHz", style="dashed"] + home:wifi -- cell [requires="wifi2.4GHz", style="dashed"] + guest:wifi -- cell [requires="wifi2.4GHz", style="dashed"] +} diff --git a/test/case/interfaces/wifi_wds_repeater/topology.svg b/test/case/interfaces/wifi_wds_repeater/topology.svg new file mode 100644 index 000000000..5d0ca7f8a --- /dev/null +++ b/test/case/interfaces/wifi_wds_repeater/topology.svg @@ -0,0 +1,121 @@ + + + + + + +wifi-wds-repeater + + + +host + +host + +mgmt1 + +mgmt2 + +mgmt3 + +mgmt4 + +lan + + + +root + +mgmt + +uplink + +wifi + +root + + + +host:mgmt1--root:mgmt + + + + +host:lan--root:uplink + + + + +repeater + +mgmt + +wifi + +repeater + + + +host:mgmt2--repeater:mgmt + + + + +home + +mgmt + +wifi + +home + + + +host:mgmt3--home:mgmt + + + + +guest + +mgmt + +wifi + +guest + + + +host:mgmt4--guest:mgmt + + + + +cell + +cell + + + +root:wifi--cell + + + + +repeater:wifi--cell + + + + +home:wifi--cell + + + + +guest:wifi--cell + + + + diff --git a/test/case/routing/ospf_bfd/ospfv2.adoc b/test/case/routing/ospf_bfd/ospfv2.adoc index 78e548c64..a5e31f327 100644 --- a/test/case/routing/ospf_bfd/ospfv2.adoc +++ b/test/case/routing/ospf_bfd/ospfv2.adoc @@ -12,9 +12,9 @@ This can typically happen when one logical link, from OSPF's perspective, is made up of multiple physical links containing media converters without link fault forwarding. -Note: OSPFv3 next-hops are IPv6 link-local addresses, so the active path is -verified with traceroute rather than by matching a RIB next-hop, and its BFD -peers are only known by their session count. +Note: OSPFv3 next-hops and BFD peers are IPv6 link-local addresses, unknown in +advance, so both versions verify the active path with traceroute rather than by +matching a RIB next-hop, and count BFD sessions rather than name their peers. ==== Topology diff --git a/test/case/routing/ospf_bfd/ospfv3.adoc b/test/case/routing/ospf_bfd/ospfv3.adoc index 65aa96443..5fbe3a0de 100644 --- a/test/case/routing/ospf_bfd/ospfv3.adoc +++ b/test/case/routing/ospf_bfd/ospfv3.adoc @@ -12,9 +12,9 @@ This can typically happen when one logical link, from OSPF's perspective, is made up of multiple physical links containing media converters without link fault forwarding. -Note: OSPFv3 next-hops are IPv6 link-local addresses, so the active path is -verified with traceroute rather than by matching a RIB next-hop, and its BFD -peers are only known by their session count. +Note: OSPFv3 next-hops and BFD peers are IPv6 link-local addresses, unknown in +advance, so both versions verify the active path with traceroute rather than by +matching a RIB next-hop, and count BFD sessions rather than name their peers. ==== Topology diff --git a/test/infamy/netconf.py b/test/infamy/netconf.py index 23242aebe..566d7a62a 100644 --- a/test/infamy/netconf.py +++ b/test/infamy/netconf.py @@ -441,6 +441,35 @@ def call_action(self, xpath, input_data=None): xml = "" + lyd.print_mem("xml", with_siblings=True, pretty=False) + "" return self.ncc.dispatch(xml) + def call_action_output(self, xpath, input_data=None): + """Call NETCONF action, returning the output as a nested dict""" + reply = self.call_action(xpath, input_data) + xml = reply.xml + if isinstance(xml, str): + xml = xml.encode() + + def to_dict(elem): + if not len(elem): + return (elem.text or "").strip() + out = {} + for child in elem: + name = lxml.etree.QName(child).localname + value = to_dict(child) + if name in out: + if not isinstance(out[name], list): + out[name] = [out[name]] + out[name].append(value) + else: + out[name] = value + return out + + output = to_dict(fromstring(xml)) + # Lists with one entry come back as a dict, lift them to a list + for key, value in output.items(): + if isinstance(value, dict) and key not in ("ok",): + output[key] = [value] + return output + def get_schemas_list(self): schemas = [] data = self.get_dict("/netconf-state") diff --git a/test/infamy/restconf.py b/test/infamy/restconf.py index b23a90690..0df048900 100644 --- a/test/infamy/restconf.py +++ b/test/infamy/restconf.py @@ -542,6 +542,18 @@ def call_action(self, xpath, input_data=None): return response.content + def call_action_output(self, xpath, input_data=None): + """Call RESTCONF action, returning the output as a nested dict""" + content = self.call_action(xpath, input_data) + if not content: + return {} + + data = json.loads(content) + for key, value in data.items(): + if key.endswith(":output"): + return value + return data + def delete_xpath(self, xpath): """Delete XPath from running config""" coverage.track_xpath(xpath) diff --git a/test/infamy/transport.py b/test/infamy/transport.py index 8866d6f4b..d8d36a2d1 100644 --- a/test/infamy/transport.py +++ b/test/infamy/transport.py @@ -75,6 +75,16 @@ def call_action(self, xpath, input_data=None): """ pass + @abstractmethod + def call_action_output(self, xpath, input_data=None): + """Invoke a YANG action at `xpath`, returning its output. + + The output is a nested dict of the action's output nodes, lists + as Python lists. Leaf values are strings on NETCONF and typed + on RESTCONF, compare with str(). + """ + pass + def __getitem__(self, key): if key in self.mapping: return self.mapping[key] diff --git a/test/infamy/wifi.py b/test/infamy/wifi.py index 942c838b0..dcf5046d3 100644 --- a/test/infamy/wifi.py +++ b/test/infamy/wifi.py @@ -6,9 +6,9 @@ import base64 -def radio(name, country="SE", band=None, channel=None): +def radio(name, band=None, channel=None): """ietf-hardware component for a WiFi radio.""" - settings = {"country-code": country} + settings = {} if band: settings["band"] = band if channel is not None: @@ -20,6 +20,14 @@ def radio(name, country="SE", band=None, channel=None): } +def hardware(*radios, country="SE"): + """ietf-hardware config: the radios plus the box-wide WiFi country code.""" + return {"hardware": { + "infix-hardware:wifi": {"country-code": country}, + "component": list(radios), + }} + + def keystore(secrets): """ietf-keystore config with a passphrase entry per {name: psk}.""" return {"keystore": {"symmetric-keys": {"symmetric-key": [ @@ -31,7 +39,7 @@ def keystore(secrets): ]}}} -def iface(name, mac, wifi, ipv4=None, bridge=None): +def iface(name, mac, wifi, ipv4=None, bridge=None, pvid=None): """ietf-interfaces entry for a WiFi VIF. wifi is the infix-interfaces:wifi container: the radio plus one of @@ -48,9 +56,52 @@ def iface(name, mac, wifi, ipv4=None, bridge=None): ifc["ietf-ip:ipv4"] = ipv4 if bridge: ifc["infix-interfaces:bridge-port"] = {"bridge": bridge} + if pvid is not None: + ifc["infix-interfaces:bridge-port"]["pvid"] = pvid + return ifc + + +def wds_link(name, ap, peer, bridge=None, pvid=None): + """ietf-interfaces entry for a wds-link port of access point ap. + + The port inherits the AP's radio and MAC address, so unlike iface() it + takes neither a radio nor a custom-phys-address. + """ + ifc = { + "name": name, + "type": "infix-if-type:wifi", + "enabled": True, + "infix-interfaces:wifi": { + "wds-link": {"access-point": ap, "peer-address": peer}, + }, + } + if bridge: + ifc["infix-interfaces:bridge-port"] = {"bridge": bridge} + if pvid is not None: + ifc["infix-interfaces:bridge-port"]["pvid"] = pvid return ifc +def channel_survey(target, radio="radio0", passive=False): + """Run the channel-survey action on radio, return its channel list. + + Each entry has at least a frequency, the operating channel has + in-use set. Values are strings on NETCONF, compare with str(). + """ + xpath = f"/ietf-hardware:hardware/component[name='{radio}']" \ + "/infix-hardware:wifi-radio/channel-survey" + output = target.call_action_output(xpath, {"passive": passive} if passive else None) + return output.get("channel", []) + + +def in_use_frequency(channels): + """Frequency in MHz of the channel marked in-use, or None.""" + for ch in channels: + if str(ch.get("in-use")).lower() == "true": + return int(ch["frequency"]) + return None + + def skip_unless_supported(test, *targets): """Skip the test unless every target advertises the wifi feature.""" for target in targets: @@ -84,6 +135,21 @@ def station_bssid(target, ifname="wifi0"): return (station(target, ifname).get("bssid") or "").lower() +def wds_connected(target, ifname): + """True once the station of the wds-link port ifname is bound to it.""" + return _wifi(target.get_iface(ifname)).get("wds-link", {}).get("connected") is True + + +def bridge_vlan_members(target, bridge, vid, tagging="untagged"): + """Ports listed as tagged/untagged members of vid on bridge, from operational.""" + ifc = target.get_iface(bridge) or {} + br = ifc.get("bridge") or ifc.get("infix-interfaces:bridge") or {} + for vlan in (br.get("vlans") or {}).get("vlan") or []: + if vlan.get("vid") == vid: + return set(vlan.get(tagging) or []) + return set() + + def ap_stations(target, ifname="wifi0"): """MACs of the stations currently associated to this AP BSS, lowercase.""" ap = _wifi(target.get_iface(ifname)).get("access-point") or {}
+ {{if .CountryOptions}} +
+
WiFi
+
+ + + + + + +
Country code{{template "field-info" .DescCountry}} + + + + +
+
+
+ {{end}}
Hardware unlocked {{else}}locked{{end}} {{else if $c.IsWiFi}} - {{$c.CountryCode}}{{if $c.Band}} · {{$c.Band}}{{end}} + {{if $c.Band}}{{$c.Band}}{{else}}configured{{end}} {{else if $c.IsGPS}} configured {{else}}—{{end}} @@ -109,13 +130,6 @@ {{end}} - - @@ -199,16 +213,6 @@ hx-confirm="Reset description to its YANG default?">{{template "icon-reset"}} - - Country code{{template "field-info" .DescCountry}} - - - - - Band{{template "field-info" .DescBand}} diff --git a/src/webui/templates/pages/configure-interfaces.html b/src/webui/templates/pages/configure-interfaces.html index 5e4109ee7..f5142d282 100644 --- a/src/webui/templates/pages/configure-interfaces.html +++ b/src/webui/templates/pages/configure-interfaces.html @@ -437,8 +437,8 @@

WiFi

Country code - + {{range $.WizardCountryOptions}}{{end}} @@ -616,14 +616,14 @@

WiFi

form validation. */}} {{$rm := ""}}{{if $ap}}{{$rm = $ap.Roaming}}{{end}} {{$hasK := false}}{{$hasR := false}}{{$hasV := false}}{{$okc := true}}{{$bs := true}} - {{$md := "4f57"}}{{$nas := "auto"}} + {{$md := "4f57"}}{{$nas := "auto"}}{{$khs := ""}} {{if $rm}} {{if $rm.Dot11k}}{{$hasK = true}}{{end}} - {{if $rm.Dot11r}}{{$hasR = true}}{{if $rm.Dot11r.MobilityDomain}}{{$md = $rm.Dot11r.MobilityDomain}}{{end}}{{if $rm.Dot11r.NASIdentifier}}{{$nas = $rm.Dot11r.NASIdentifier}}{{end}}{{end}} + {{if $rm.Dot11r}}{{$hasR = true}}{{$khs = $rm.Dot11r.KeyHolderSecret}}{{if $rm.Dot11r.MobilityDomain}}{{$md = $rm.Dot11r.MobilityDomain}}{{end}}{{if $rm.Dot11r.NASIdentifier}}{{$nas = $rm.Dot11r.NASIdentifier}}{{end}}{{end}} {{if $rm.Dot11v}}{{$hasV = true}}{{if $rm.Dot11v.BandSteering}}{{$bs = deref $rm.Dot11v.BandSteering}}{{end}}{{end}} {{if $rm.OKC}}{{$okc = deref $rm.OKC}}{{end}} {{end}} - {{$dot11rRows := printf "wifi-row-%s-dot11r-md wifi-row-%s-dot11r-nas" $ifname $ifname}} + {{$dot11rRows := printf "wifi-row-%s-dot11r-key wifi-row-%s-dot11r-md wifi-row-%s-dot11r-nas" $ifname $ifname $ifname}} {{$dot11vRows := printf "wifi-row-%s-dot11v-bs" $ifname}}
WiFi + + Key holder secret{{template "field-info" (index $d "wifi-dot11r-key")}} + + + + Mobility domain{{template "field-info" (index $d "wifi-dot11r-md")}} Add Interface - Country code{{template "field-info" "ISO 3166-1 country code — required. Regulators tie the radio's channel/power allowance to this; '00' (world) cannot be used in Access Point mode."}} + Country code{{template "field-info" "ISO 3166-1 country code, one setting shared by every radio in the system. Regulators tie the channel/power allowance to this; '00' (world) cannot be used in Access Point mode."}} - + {{range .WizardCountryOptions}}{{end}} diff --git a/src/webui/templates/pages/wifi.html b/src/webui/templates/pages/wifi.html index 9cfdab16c..a97527ac5 100644 --- a/src/webui/templates/pages/wifi.html +++ b/src/webui/templates/pages/wifi.html @@ -49,14 +49,12 @@
- {{if .SurveySVG}}
Channel Survey
-
- {{.SurveySVG}} +
+ {{template "wifi-survey" .Survey}}
- {{end}} {{range .Interfaces}} @@ -159,4 +157,35 @@

No WiFi radios detected.