Scan for Bluetooth Classic + BLE devices.
(self)
| 3102 | it is unmanaged (Ragnar marks its own scan adapters unmanaged so NM |
| 3103 | doesn't add competing routes), left in monitor mode, or its driver |
| 3104 | loaded after NM started. Treating sysfs as a fallback that only runs |
| 3105 | when nmcli returned *nothing* silently dropped exactly those adapters, |
| 3106 | so a third dongle never joined the sweep. sysfs is the authority on |
| 3107 | "this radio exists"; nmcli only adds names sysfs might miss. |
| 3108 | """ |
| 3109 | found = set() |
| 3110 | |
| 3111 | try: |
| 3112 | result = subprocess.run( |
| 3113 | ['nmcli', '-t', '-f', 'DEVICE,TYPE,STATE', 'device'], |
| 3114 | capture_output=True, text=True, timeout=5 |
| 3115 | ) |
| 3116 | if result.returncode == 0: |
| 3117 | for line in result.stdout.strip().split('\n'): |
| 3118 | parts = line.split(':') |
| 3119 | if len(parts) >= 3 and parts[1] == 'wifi': |
| 3120 | found.add(parts[0]) |
| 3121 | except Exception as e: |
| 3122 | logger.debug(f"nmcli detection failed: {e}") |
| 3123 | |
| 3124 | # sysfs: every wireless netdev, including nl80211 *and* wext-only |
| 3125 | # dongles that report no wireless/ dir but do have phy80211/. |
| 3126 | try: |
| 3127 | for name in os.listdir('/sys/class/net'): |
| 3128 | if os.path.isdir(f'/sys/class/net/{name}/wireless') \ |
| 3129 | or os.path.isdir(f'/sys/class/net/{name}/phy80211'): |
| 3130 | found.add(name) |
| 3131 | except Exception as e: |
| 3132 | logger.debug(f"sysfs wifi detection failed: {e}") |
| 3133 | |
| 3134 | # Drop the monitor/child vifs our own tooling (and airmon-ng) creates, |
| 3135 | # so a rebuilt monitor interface never doubles up with its parent. |
| 3136 | interfaces = sorted(n for n in found |
| 3137 | if not n.endswith('mon') and not n.startswith('mon')) |
| 3138 | |
| 3139 | blocked = [n for n in interfaces if self._iface_rfkill_blocked(n)] |
| 3140 | if blocked and not quiet: |
| 3141 | # A soft-blocked dongle is present in sysfs but cannot scan; say so |
| 3142 | # explicitly because the symptom (adapter listed, zero networks) is |
| 3143 | # otherwise indistinguishable from a driver problem. |
| 3144 | logger.warning( |
| 3145 | f"Wardriving: {', '.join(blocked)} is rfkill-blocked and will not " |
| 3146 | f"scan until unblocked — run 'sudo rfkill unblock all'") |
| 3147 | # quiet=True on the periodic hot-plug rescan, so it doesn't log this |
| 3148 | # every cycle — only the genuine start-up detection is announced. |
| 3149 | (logger.debug if quiet else logger.info)( |
| 3150 | f"Wardriving detected {len(interfaces)} WiFi interface(s): " |
| 3151 | f"{', '.join(interfaces) or '(none)'}") |
| 3152 | return interfaces |
| 3153 | |
| 3154 | @staticmethod |
| 3155 | def _iface_rfkill_blocked(iface): |
| 3156 | """True when this radio is soft/hard rfkill-blocked. A freshly plugged |
| 3157 | USB dongle commonly comes up blocked, so it enumerates but never scans.""" |
| 3158 | try: |
| 3159 | phy = os.path.realpath(f'/sys/class/net/{iface}/phy80211') |
| 3160 | for entry in os.listdir(f'{phy}/rfkill'): |
| 3161 | if not entry.startswith('rfkill'): |