uaf.git

uaf

~
git clone https://github.com/debuman/uaf.git

ap_scanner/utils.py (6830B)

      1 """Utility functions: channel/frequency mapping, privilege checks, interface detection."""
      2 
      3 import os
      4 import shutil
      5 import subprocess
      6 from pathlib import Path
      7 
      8 # 2.4 GHz channels (1-14)
      9 CHANNELS_2G = list(range(1, 15))
     10 
     11 # 5 GHz channels (common UNII bands)
     12 CHANNELS_5G = [
     13     36, 40, 44, 48,       # UNII-1
     14     52, 56, 60, 64,       # UNII-2
     15     100, 104, 108, 112,   # UNII-2 Extended
     16     116, 120, 124, 128,
     17     132, 136, 140, 144,
     18     149, 153, 157, 161,   # UNII-3
     19     165,
     20 ]
     21 
     22 # 6 GHz channels (WiFi 6E / 7, UNII-5 through UNII-8)
     23 CHANNELS_6G = [
     24     1, 5, 9, 13, 17, 21, 25, 29,       # UNII-5
     25     33, 37, 41, 45, 49, 53, 57, 61,
     26     65, 69, 73, 77, 81, 85, 89, 93,     # UNII-6
     27     97, 101, 105, 109, 113, 117,
     28     121, 125, 129, 133, 137, 141,        # UNII-7
     29     145, 149, 153, 157, 161, 165,
     30     169, 173, 177, 181, 185, 189,        # UNII-8
     31     193, 197, 201, 205, 209, 213,
     32     217, 221, 225, 229, 233,
     33 ]
     34 
     35 ALL_CHANNELS = CHANNELS_2G + CHANNELS_5G
     36 
     37 # Channel -> Center Frequency (MHz) mapping
     38 CHANNEL_TO_FREQ: dict[int, int] = {}
     39 # 2.4 GHz: channel 1 = 2412 MHz, each channel +5 MHz, channel 14 = 2484 MHz
     40 for ch in range(1, 14):
     41     CHANNEL_TO_FREQ[ch] = 2407 + ch * 5
     42 CHANNEL_TO_FREQ[14] = 2484
     43 # 5 GHz: freq = 5000 + channel * 5
     44 for ch in CHANNELS_5G:
     45     CHANNEL_TO_FREQ[ch] = 5000 + ch * 5
     46 # 6 GHz: freq = 5950 + channel * 5  (channel 1 = 5955 MHz)
     47 CHANNEL_TO_FREQ_6G: dict[int, int] = {}
     48 for ch in CHANNELS_6G:
     49     CHANNEL_TO_FREQ_6G[ch] = 5950 + ch * 5
     50 
     51 # Reverse mapping (2.4G + 5G only; 6G channels overlap with 2.4G/5G numbers)
     52 FREQ_TO_CHANNEL: dict[int, int] = {freq: ch for ch, freq in CHANNEL_TO_FREQ.items()}
     53 # Add 6 GHz reverse mapping (no overlap since frequencies are unique)
     54 for ch, freq in CHANNEL_TO_FREQ_6G.items():
     55     FREQ_TO_CHANNEL[freq] = ch
     56 
     57 
     58 def freq_to_channel(freq_mhz: int) -> int:
     59     """Convert frequency in MHz to channel number."""
     60     if freq_mhz in FREQ_TO_CHANNEL:
     61         return FREQ_TO_CHANNEL[freq_mhz]
     62     # Approximate: find closest frequency
     63     if 2400 <= freq_mhz <= 2500:
     64         return round((freq_mhz - 2407) / 5)
     65     elif 5925 <= freq_mhz <= 7125:
     66         return round((freq_mhz - 5950) / 5)
     67     elif freq_mhz >= 5000:
     68         return round((freq_mhz - 5000) / 5)
     69     return 0
     70 
     71 
     72 def channel_to_freq(channel: int) -> int:
     73     """Convert channel number to frequency in MHz."""
     74     return CHANNEL_TO_FREQ.get(channel, 0)
     75 
     76 
     77 def is_5ghz(channel: int) -> bool:
     78     """Check if a channel is in the 5 GHz band."""
     79     return channel in CHANNELS_5G
     80 
     81 
     82 def channel_band(channel: int, freq: int = 0) -> str:
     83     """Return '2.4', '5', or '6' for a given channel.
     84 
     85     Uses frequency to disambiguate overlapping channel numbers between
     86     2.4GHz/5GHz and 6GHz bands.
     87     """
     88     if freq >= 5925:
     89         return "6"
     90     if freq >= 4900:
     91         return "5"
     92     if freq > 0:
     93         return "2.4"
     94     # Fallback: channel number only (no freq info, cannot detect 6GHz)
     95     return "5" if is_5ghz(channel) else "2.4"
     96 
     97 
     98 def check_root() -> bool:
     99     """Check if running as root."""
    100     return os.geteuid() == 0
    101 
    102 
    103 def check_iw() -> bool:
    104     """Check if 'iw' command is available."""
    105     return shutil.which("iw") is not None
    106 
    107 
    108 def check_ip() -> bool:
    109     """Check if 'ip' command is available."""
    110     return shutil.which("ip") is not None
    111 
    112 
    113 def find_wireless_interfaces() -> list[str]:
    114     """Find available wireless interfaces by checking /sys/class/net/*/wireless."""
    115     interfaces = []
    116     net_path = Path("/sys/class/net")
    117     if not net_path.exists():
    118         return interfaces
    119     for iface_path in net_path.iterdir():
    120         wireless_path = iface_path / "wireless"
    121         if wireless_path.exists():
    122             interfaces.append(iface_path.name)
    123     return sorted(interfaces)
    124 
    125 
    126 def get_interface_mode(interface: str) -> str | None:
    127     """Get the current mode of a wireless interface (managed, monitor, etc.)."""
    128     try:
    129         result = subprocess.run(
    130             ["iw", "dev", interface, "info"],
    131             capture_output=True, text=True, timeout=5,
    132         )
    133         if result.returncode != 0:
    134             return None
    135         for line in result.stdout.splitlines():
    136             line = line.strip()
    137             if line.startswith("type "):
    138                 return line.split("type ", 1)[1].strip()
    139     except (subprocess.TimeoutExpired, FileNotFoundError):
    140         return None
    141     return None
    142 
    143 
    144 def get_interface_driver(interface: str) -> str:
    145     """Get the driver name for a wireless interface."""
    146     driver_link = Path(f"/sys/class/net/{interface}/device/driver")
    147     try:
    148         return driver_link.resolve().name
    149     except (OSError, ValueError):
    150         return "unknown"
    151 
    152 
    153 def get_supported_channels(interface: str) -> list[int]:
    154     """Query the adapter's supported channels via 'iw phy'.
    155 
    156     Returns a sorted list of channels the hardware can actually use.
    157     Falls back to ALL_CHANNELS if the query fails.
    158     """
    159     # Get phy name for this interface
    160     phy_path = Path(f"/sys/class/net/{interface}/phy80211/name")
    161     try:
    162         phy_name = phy_path.read_text().strip()
    163     except (OSError, ValueError):
    164         return ALL_CHANNELS
    165 
    166     try:
    167         result = subprocess.run(
    168             ["iw", "phy", phy_name, "channels"],
    169             capture_output=True, text=True, timeout=5,
    170         )
    171         if result.returncode != 0:
    172             return ALL_CHANNELS
    173     except (subprocess.TimeoutExpired, FileNotFoundError):
    174         return ALL_CHANNELS
    175 
    176     channels = []
    177     for line in result.stdout.splitlines():
    178         line = line.strip()
    179         # Lines look like: "* 2412 MHz [1] (20.0 dBm)"
    180         # or disabled: "* 5260 MHz [52] (disabled)"
    181         if not line.startswith("*"):
    182             continue
    183         if "disabled" in line.lower():
    184             continue
    185         # Note: "no IR" channels are valid for monitor mode (receive-only)
    186         # Extract channel number from [N]
    187         bracket_start = line.find("[")
    188         bracket_end = line.find("]")
    189         if bracket_start == -1 or bracket_end == -1:
    190             continue
    191         try:
    192             ch = int(line[bracket_start + 1 : bracket_end])
    193             channels.append(ch)
    194         except ValueError:
    195             continue
    196 
    197     return sorted(set(channels)) if channels else ALL_CHANNELS
    198 
    199 
    200 def parse_channels_arg(channels_str: str) -> list[int]:
    201     """Parse channel argument string like '1,6,11' or '36-48' into a list of channels."""
    202     channels = []
    203     for part in channels_str.split(","):
    204         part = part.strip()
    205         if "-" in part:
    206             start, end = part.split("-", 1)
    207             start, end = int(start.strip()), int(end.strip())
    208             channels.extend(ch for ch in ALL_CHANNELS if start <= ch <= end)
    209         else:
    210             ch = int(part)
    211             if ch in ALL_CHANNELS:
    212                 channels.append(ch)
    213     return sorted(set(channels)) if channels else ALL_CHANNELS