#!/usr/bin/env python3 # SPDX-License-Identifier: MPL-2.0 """Nokia Wi-Fi adapter: vendor supplicant, private client socket, no credentials in argv.""" import hashlib import ipaddress import json import os from pathlib import Path import re import signal import socket import struct import subprocess import sys import time import uuid SOCKET = '/work/wifi-control.sock' STATE = Path('/run/tundra-wifi/state.json') WPA = '/work/radio-sockets/wpa_wlan0' ERRORS = {'WIFI_UNAVAILABLE', 'WIFI_DISABLED', 'WIFI_INVALID_REQUEST', 'WIFI_INVALID_PASSWORD', 'WIFI_UNSUPPORTED_SECURITY', 'WIFI_CONNECTION_FAILED', 'WIFI_DHCP_FAILED', 'WIFI_SAVE_FAILED', 'NOT_SUPPORTED'} def decode_ssid(text): raw = bytearray() for part in re.findall(r'\\x[0-9a-fA-F]{2}|\\.|[^\\]+|\\$', text): if re.fullmatch(r'\\x[0-9a-fA-F]{2}', part): raw.append(int(part[2:], 16)) elif part.startswith('\\'): raw.extend({'\\n':b'\n','\\r':b'\r','\\t':b'\t','\\\\':b'\\','\\"':b'"'}.get(part,part.encode())) else: raw.extend(part.encode()) return raw.decode('utf-8', errors='replace') def security(flags): if 'EAP' in flags or 'WEP' in flags: return 'unsupported' if 'PSK' in flags: return 'wpa-psk' if 'SAE' in flags: return 'unsupported' return 'open' def parse_scan(text): rows=[] for line in text.splitlines()[1:]: parts=line.split('\t',4) if len(parts)!=5 or not re.fullmatch(r'(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}',parts[0]): continue try: signal_level=max(0,min(100,2*(int(parts[2])+100))) except ValueError: continue ssid=decode_ssid(parts[4]) if ssid: rows.append({'id':parts[0].lower(),'ssid':ssid,'signal':signal_level,'security':security(parts[3])}) return sorted(rows,key=lambda row:-row['signal'])[:128] def android(*args, check=True): result=subprocess.run(['/system/bin/'+args[0],*args[1:]],capture_output=True,text=True,timeout=5, env={'PATH':'/system/bin','LD_LIBRARY_PATH':'/system/lib'}) if check and result.returncode: raise RuntimeError('WIFI_UNAVAILABLE') return result.stdout.strip() def command(value, required=True): with socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM) as sock: # An abstract return address is reachable by the host supplicant outside our chroot. sock.bind('\0vulpes-wifi-'+uuid.uuid4().hex) sock.settimeout(3);sock.connect(WPA) sock.send(('IFNAME=wlan0 '+value).encode()) result=sock.recv(65536).decode('utf-8',errors='replace').strip() if required and (result.startswith('FAIL') or result=='UNKNOWN COMMAND'): raise RuntimeError('WIFI_UNAVAILABLE') return result class Wifi: def __init__(self): self.last_scan=[];self.original_enabled=None;self.route=None;self.last_lease=0 self.initial_resolver=Path('/etc/resolv.conf').read_bytes() if Path('/etc/resolv.conf').exists() else None self.rule_owned=False;self.stopping=False self.lease_network=None def enabled(self): try: return bool(int(Path('/sys/class/net/wlan0/flags').read_text(),16)&1) except OSError: return False def state(self): enabled=self.enabled() result={'available':True,'enabled':enabled,'state':'disconnected','network':None,'connectionInformation':{}} if not enabled: result['state']='disabled';return result status=dict(line.split('=',1) for line in command('STATUS').splitlines() if '=' in line) result['macAddress']=status.get('address','') if status.get('wpa_state')=='COMPLETED': result['state']='connected' result['network']={'ssid':decode_ssid(status.get('ssid','')),'savedId':status.get('id'), 'security':security(status.get('key_mgmt','')),'signal':50} try: result['connectionInformation']['ipAddress']=str(ipaddress.IPv4Address(status.get('ip_address',''))) except ValueError: pass poll=command('SIGNAL_POLL',False) for line in poll.splitlines(): try: name,value=line.split('=',1);value=int(value) if name=='RSSI': result['network']['signal']=max(0,min(100,2*(value+100))) if name=='LINKSPEED': result['connectionInformation']['linkSpeed']=value except ValueError: pass return result def known(self): rows=[] for line in command('LIST_NETWORKS').splitlines()[1:]: parts=line.split('\t',3) if len(parts)!=4 or not parts[0].isdigit(): continue flags=command('GET_NETWORK '+parts[0]+' key_mgmt',False) rows.append({'id':parts[0],'ssid':decode_ssid(parts[1]),'security':security(flags)}) return rows def scan(self): state=self.state() if not state['enabled']:return {**state,'networks':[]} # Subscribe before requesting a scan so fast completions are not lost. with socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM) as events: events.bind('\0vulpes-wifi-scan-'+uuid.uuid4().hex);events.settimeout(3);events.connect(WPA) events.send(b'IFNAME=wlan0 ATTACH');events.recv(4096) command('SCAN',False) deadline=time.monotonic()+4 while time.monotonic()6:raise ValueError('WIFI_INVALID_REQUEST') command('REMOVE_NETWORK '+ident) if command('SAVE_CONFIG',False)!='OK':raise RuntimeError('WIFI_SAVE_FAILED') return True raise ValueError('NOT_SUPPORTED') def restore(self): if self.original_enabled is not None: try:self.set_enabled(self.original_enabled) except Exception:pass if self.rule_owned: android('ip','rule','del','priority','21900','fwmark','0x0/0xffff','uidrange','2000-2000','lookup','21052',check=False) android('ip','route','flush','table','21052',check=False) resolver=Path('/etc/resolv.conf') if self.initial_resolver is None:resolver.unlink(missing_ok=True) else:resolver.write_bytes(self.initial_resolver) def receive(conn, limit=8192): data=b'' while b'\n' not in data: part=conn.recv(4096) if not part:break data+=part if len(data)>limit:raise ValueError('WIFI_INVALID_REQUEST') return json.loads(data) def main(): if sys.argv[1:]==['--client']: raw=sys.stdin.buffer.read(8193) if len(raw)>8192:raise ValueError('WIFI_INVALID_REQUEST') value=json.loads(raw) with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as conn: conn.settimeout(40);conn.connect(SOCKET);conn.sendall(json.dumps(value).encode()+b'\n') print(json.dumps(receive(conn,65536))) return if sys.argv[1:] or os.getuid()!=0:raise SystemExit('Root broker or --client required') os.umask(0o022);wifi=Wifi();running=True def stop(*_): nonlocal running running=False;wifi.stopping=True signal.signal(signal.SIGTERM,stop) Path(SOCKET).unlink(missing_ok=True) with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as server: server.bind(SOCKET);os.chown(SOCKET,0,2000);os.chmod(SOCKET,0o660);server.listen(8);server.settimeout(1) print('Nokia Wi-Fi broker ready',flush=True) try: while running: wifi.publish() try:conn,_=server.accept() except socket.timeout:continue with conn: conn.settimeout(2) _,uid,_=struct.unpack('3i',conn.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12)) if uid not in (0,2000):continue try:reply={'result':wifi.request(receive(conn))} except (ValueError,RuntimeError,OSError,subprocess.SubprocessError) as error: reply={'error':str(error) if str(error) in ERRORS else 'WIFI_UNAVAILABLE'} try:conn.sendall(json.dumps(reply).encode()+b'\n') except OSError:pass finally: wifi.restore();Path(SOCKET).unlink(missing_ok=True) if __name__=='__main__':main()