#!/usr/bin/env python3 """Bounded native brcmfmac ESP-NOW v1 TX/RX probe (not proof by command ACK). Uses nl80211 vendor DCMD actframe and registered management RX. No monitor interface, firmware replacement, or ESP32 change. Pair with a distinct peer. """ import argparse import errno import json from pathlib import Path import re import select import socket import struct import time def log(**values): print(json.dumps({"time": time.time(), **values}), flush=True) def enums(name): text = Path("/usr/include/linux/nl80211.h").read_text() text = re.sub(r"/\*.*?\*/", "", text, flags=re.S) body = re.search(r"enum\s+" + name + r"\s*\{(.*?)\};", text, re.S).group(1) result = {} current = -1 for item in body.split(","): if not item.strip(): continue match = re.fullmatch(r"\s*(\w+)\s*(?:=\s*(.*))?", item, re.S) key, expression = match.groups() if expression: expression = re.sub(r"\b[A-Za-z_]\w*\b", lambda m: str(result[m.group()]), expression) if not re.fullmatch(r"[\d+\-\s]+", expression): raise ValueError("Unsupported enum expression") current = sum(int(re.sub(r"\s", "", p)) for p in re.findall(r"[+\-]?\s*\d+", expression)) else: current += 1 result[key] = current return result def attr(kind, payload): size = 4 + len(payload) return struct.pack("=HH", size, kind) + payload + bytes((-size) & 3) def attrs(data): output = {} while len(data) >= 4: size, kind = struct.unpack_from("=HH", data) if size < 4 or size > len(data): raise ValueError("Bad netlink attribute") output[kind & 0x3fff] = data[4:size] data = data[(size + 3) & ~3:] return output class Netlink: def __init__(self): self.sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, 16) self.sock.bind((0, 0)) self.sock.settimeout(3) self.sequence = 0 def receive(self): data = self.sock.recv(65536) while len(data) >= 16: size, kind, flags, sequence, pid = struct.unpack_from("=IHHII", data) if size < 16 or size > len(data): raise ValueError("Bad netlink message") yield kind, sequence, data[16:size] data = data[(size + 3) & ~3:] def request(self, family, command, data): self.sequence += 1 payload = struct.pack("=BBH", command, 1, 0) + data self.sock.sendto(struct.pack("=IHHII", 16 + len(payload), family, 5, self.sequence, 0) + payload, (0, 0)) output = [] for _ in range(32): for kind, sequence, payload in self.receive(): if sequence != self.sequence: continue if kind == 2: error = struct.unpack_from("=i", payload)[0] if error: raise OSError(-error, errno.errorcode.get(-error, "netlink error")) return output if kind == 3: return output output.append(payload) raise RuntimeError("Too many responses") class Firmware: """Small fixed-operation adapter used for reversible radio power policy.""" def __init__(self): self.link = Netlink() self.commands = enums("nl80211_commands") self.attributes = enums("nl80211_attrs") self.family = struct.unpack("=H", attrs(self.link.request(16, 3, attr(2, b"nl80211\0"))[0][4:])[1])[0] self.base = attr(self.attributes["NL80211_ATTR_IFINDEX"], struct.pack("=I", socket.if_nametoindex("wlan0"))) self.base += attr(self.attributes["NL80211_ATTR_VENDOR_ID"], struct.pack("=I", 0x001018)) self.base += attr(self.attributes["NL80211_ATTR_VENDOR_SUBCMD"], struct.pack("=I", 1)) def call(self, command, data, size, setting=False): header = struct.pack("=IiIII", command, size, 20, int(setting), 0) replies = self.link.request(self.family, self.commands["NL80211_CMD_VENDOR"], self.base + attr(self.attributes["NL80211_ATTR_VENDOR_DATA"], header + data)) return b"".join(attrs(attrs(reply[4:])[self.attributes["NL80211_ATTR_VENDOR_DATA"]])[2] for reply in replies) def power_policy(self): return {"mpc": struct.unpack_from(" 250 or len(random) != 4: raise ValueError("ESP-NOW v1 payload too large") return b"\x7f\x18\xfe\x34" + random + bytes((221, len(payload) + 5)) + b"\x18\xfe\x34\x04\x01" + payload def decode_body(body): if len(body) < 15 or body[:4] != b"\x7f\x18\xfe\x34": raise ValueError("Not an ESP-NOW action body") if body[8] != 221 or body[10:14] != b"\x18\xfe\x34\x04": raise ValueError("Bad ESP-NOW vendor element") size = body[9] if size < 5 or 10 + size > len(body): raise ValueError("Truncated ESP-NOW element") # Team fits one element; accept the single-element v2 layout as well. if body[14] & 15 not in (1, 2) or body[14] & 16: raise ValueError("Unsupported ESP-NOW version/fragmentation") return body[15:10 + size] def configure_channel(channel): if not 1 <= channel <= 11: raise ValueError("Channel outside bounded pairing range") commands, attributes = enums("nl80211_commands"), enums("nl80211_attrs") link = Netlink() try: family = struct.unpack("=H", attrs(link.request(16, 3, attr(2, b"nl80211\0"))[0][4:])[1])[0] base = attr(attributes["NL80211_ATTR_IFINDEX"], struct.pack("=I", socket.if_nametoindex("wlan0"))) base += attr(attributes["NL80211_ATTR_VENDOR_ID"], struct.pack("=I", 0x001018)) base += attr(attributes["NL80211_ATTR_VENDOR_SUBCMD"], struct.pack("=I", 1)) header = struct.pack("=IiIII", 30, 4, 20, 1, 0) link.request(family, commands["NL80211_CMD_VENDOR"], base + attr(attributes["NL80211_ATTR_VENDOR_DATA"], header + struct.pack("= next_tx: destination = b"\xff" * 6 if count % 2 == 0 else peer payload = b"TMPROBE1:" + args.tag.encode() + b":" + struct.pack("