Files
StarPilot/openpilot/starpilot/galaxy/local_access.py
T
firestar5683 00364fd645 Galaxy Access
Separate Galaxy authentication and local access policy from web presentation.

Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
2026-10-14 19:56:17 -05:00

76 lines
3.5 KiB
Python

"""Actual LAN addresses for direct Galaxy access, independent of the tunnel."""
from ipaddress import IPv4Address, ip_network
import json
import subprocess
import threading
import time
LAN = tuple(ip_network(value) for value in ('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '169.254.0.0/16'))
# Virtual links can carry RFC1918 addresses too; they are not a local Wi-Fi/Ethernet address.
TUNNEL_KINDS = frozenset(('tun', 'wireguard', 'gre', 'gretap', 'ipip', 'sit', 'vti', 'vti6', 'vxlan', 'geneve'))
TUNNEL_PREFIXES = ('tun', 'tap', 'wg', 'tailscale', 'zt', 'docker', 'veth', 'br-', 'p2p', 'usb', 'rndis', 'ppp', 'wwan', 'rmnet')
class LocalAccess:
def __init__(self, port=8082, *, run=subprocess.run, clock=time.monotonic):
self.port, self.run, self.clock = port, run, clock
self.lock = threading.Lock()
self.cached, self.expiry = None, 0.0
def snapshot(self):
with self.lock:
if self.cached is not None and self.clock() < self.expiry:
return self.cached
addresses = []
reason = 'Connect to the Galaxy hotspot, or put the comma and phone on the same Wi-Fi or Ethernet network.'
try:
result = self.run(['ip', '-j', '-4', 'address', 'show', 'up'], capture_output=True, timeout=1, check=True)
if len(result.stdout) > 65536:
raise ValueError('Network inventory too large')
interfaces = json.loads(result.stdout)
if not isinstance(interfaces, list):
raise ValueError('Invalid network inventory')
for interface in interfaces[:64]:
if len(addresses) >= 32:
break
if type(interface) is not dict:
continue
name = interface.get('ifname', '')
flags = interface.get('flags', [])
info_rows = interface.get('addr_info', [])
if (not isinstance(name, str) or not name or type(flags) is not list or
'UP' not in flags or 'LOOPBACK' in flags or type(info_rows) is not list or
name.lower().startswith(TUNNEL_PREFIXES) or
interface.get('operstate') in ('DOWN', 'LOWERLAYERDOWN', 'NOTPRESENT') or
isinstance(interface.get('linkinfo'), dict) and
interface['linkinfo'].get('info_kind') in TUNNEL_KINDS):
continue
for info in info_rows[:16]:
if len(addresses) >= 32:
break
if type(info) is not dict:
continue
if (info.get('family') != 'inet' or info.get('scope') != 'global' or
info.get('valid_life_time') == 0 or info.get('preferred_life_time') == 0 or
info.get('tentative') or info.get('dadfailed')):
continue
address = IPv4Address(info['local'])
if not any(address in network for network in LAN):
continue
url = f'http://{address}:{self.port}/'
if any(row['url'] == url for row in addresses):
continue
addresses.append({'interface': name[:64], 'label': f'{address} ({name[:64]})', 'url': url})
addresses.sort(key=lambda row: (row['interface'], row['url']))
except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, AttributeError):
reason = 'The comma could not read its network addresses. Check its Network settings and refresh.'
addresses = []
self.cached = {'available': bool(addresses), 'addresses': addresses,
'reason': '' if addresses else reason}
self.expiry = self.clock() + 5.0
return self.cached