FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,166 @@
|
||||
"""Read-only network diagnostics, independent of a desktop applet's manager view."""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
_IFACE = re.compile(r"[A-Za-z0-9_.:@-]{1,100}\Z")
|
||||
|
||||
def _command(argv, *, timeout=4):
|
||||
try:
|
||||
result = subprocess.run(argv, capture_output=True, text=True, timeout=timeout,
|
||||
env={**os.environ, "LC_ALL": "C", "LANG": "C"})
|
||||
return result.returncode, result.stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None, ""
|
||||
|
||||
def _json(argv):
|
||||
code, text = _command(argv)
|
||||
if code != 0:
|
||||
return None
|
||||
try:
|
||||
value = json.loads(text)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return value if type(value) is list else None
|
||||
|
||||
def _active(service):
|
||||
code, text = _command(["systemctl", "is-active", service])
|
||||
return code == 0 and text.strip() == "active"
|
||||
|
||||
def _available(name):
|
||||
return bool(shutil.which(name) or any(
|
||||
(Path(directory) / name).is_file() and os.access(Path(directory) / name, os.X_OK)
|
||||
for directory in ["/usr/sbin", "/sbin", "/usr/bin", "/bin"]))
|
||||
|
||||
def _ifupdown(state_file=Path("/run/network/ifstate")):
|
||||
"""Read active interface names from bounded state, never credential/config files."""
|
||||
try:
|
||||
fd = os.open(state_file, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW | os.O_NONBLOCK)
|
||||
try:
|
||||
info = os.fstat(fd)
|
||||
if (not stat.S_ISREG(info.st_mode) or info.st_uid not in {0, os.getuid()}
|
||||
or info.st_mode & 0o022 or info.st_size > 65536):
|
||||
return set()
|
||||
raw = os.read(fd, 65537)
|
||||
finally:
|
||||
os.close(fd)
|
||||
if len(raw) > 65536:
|
||||
return set()
|
||||
text = raw.decode("ascii")
|
||||
except (OSError, UnicodeError):
|
||||
return set()
|
||||
names = set()
|
||||
for line in text.splitlines():
|
||||
if "=" not in line:
|
||||
continue
|
||||
name, mapped = line.split("=", 1)
|
||||
if _IFACE.fullmatch(name) and _IFACE.fullmatch(mapped):
|
||||
names.add(name)
|
||||
return names
|
||||
|
||||
def _nm_devices():
|
||||
"""Only device metadata, never connection profiles, SSIDs or saved secrets."""
|
||||
code, text = _command([
|
||||
"nmcli", "-t", "-f", "GENERAL.DEVICE,GENERAL.STATE,GENERAL.NM-MANAGED", "device", "show",
|
||||
])
|
||||
if code != 0:
|
||||
return None
|
||||
devices = {}
|
||||
current = None
|
||||
for line in text.splitlines():
|
||||
if ":" not in line:
|
||||
continue
|
||||
key, value = line.split(":", 1)
|
||||
# nmcli escapes colons/backslashes in terse output. Linux interface
|
||||
# names containing either are excluded rather than guessed.
|
||||
if key == "GENERAL.DEVICE":
|
||||
current = value if _IFACE.fullmatch(value) and "\\" not in value else None
|
||||
if current:
|
||||
devices[current] = {"managed": None, "state": ""}
|
||||
elif current and key == "GENERAL.STATE":
|
||||
devices[current]["state"] = value[:120]
|
||||
elif current and key == "GENERAL.NM-MANAGED":
|
||||
devices[current]["managed"] = {"yes": True, "no": False}.get(value.strip().lower())
|
||||
return devices
|
||||
|
||||
def summarize(addresses, routes, nm_devices=None, ifupdown=None, managers=None):
|
||||
"""Separate observed kernel connectivity from network-manager ownership."""
|
||||
nm_devices = {} if nm_devices is None else nm_devices
|
||||
ifupdown = set() if ifupdown is None else set(ifupdown)
|
||||
managers = {} if managers is None else managers
|
||||
defaults = []
|
||||
for family, rows in routes.items():
|
||||
for row in rows or []:
|
||||
name = row.get("dev")
|
||||
if row.get("dst") != "default" or not isinstance(name, str) or _IFACE.fullmatch(name) is None:
|
||||
continue
|
||||
defaults.append({"family": family, "interface": name,
|
||||
"gateway": row.get("gateway"), "metric": row.get("metric")})
|
||||
interfaces = []
|
||||
for row in addresses or []:
|
||||
name = row.get("ifname")
|
||||
if not isinstance(name, str) or _IFACE.fullmatch(name) is None:
|
||||
continue
|
||||
flags = row.get("flags") if isinstance(row.get("flags"), list) else []
|
||||
addresses_public = []
|
||||
for address in row.get("addr_info", []):
|
||||
if (isinstance(address, dict) and address.get("family") in {"inet", "inet6"}
|
||||
and isinstance(address.get("local"), str)):
|
||||
addresses_public.append({"family": address["family"], "address": address["local"],
|
||||
"prefixlen": address.get("prefixlen"),
|
||||
"scope": address.get("scope")})
|
||||
nm = nm_devices.get(name, {})
|
||||
managed = nm.get("managed")
|
||||
if managed is True:
|
||||
owner = "NetworkManager"
|
||||
elif name in ifupdown:
|
||||
owner = "ifupdown"
|
||||
else:
|
||||
owner = "unverified"
|
||||
default = any(route["interface"] == name for route in defaults)
|
||||
link = name != "lo" and "UP" in flags and row.get("operstate") in {"UP", "UNKNOWN"}
|
||||
usable_address = any(a["scope"] == "global" for a in addresses_public)
|
||||
interfaces.append({
|
||||
"name": name, "link_up": link, "operstate": row.get("operstate"),
|
||||
"addresses": addresses_public, "has_default_route": default,
|
||||
"manager": owner, "networkmanager_managed": managed,
|
||||
"networkmanager_state": nm.get("state", ""),
|
||||
"local_route_ready": bool(link and usable_address and default),
|
||||
})
|
||||
ready = any(item["local_route_ready"] for item in interfaces)
|
||||
warnings = []
|
||||
for item in interfaces:
|
||||
if item["local_route_ready"] and item["networkmanager_managed"] is False:
|
||||
warnings.append({
|
||||
"code": "CONNECTED_INTERFACE_NOT_MANAGED_BY_NETWORKMANAGER",
|
||||
"interface": item["name"],
|
||||
"message": "A NetworkManager desktop applet may not represent this working interface.",
|
||||
})
|
||||
return {"ok": True, "interfaces": interfaces, "default_routes": defaults,
|
||||
"managers": managers, "connectivity": "LINK_ADDRESS_DEFAULT_ROUTE" if ready else "NOT_ESTABLISHED",
|
||||
"internet_verified": False,
|
||||
"internet_note": "Link, address and route checks do not verify DNS or Internet access.",
|
||||
"warnings": warnings}
|
||||
|
||||
def inspect():
|
||||
addresses = _json(["ip", "-j", "address", "show"])
|
||||
routes4 = _json(["ip", "-j", "-4", "route", "show", "default"])
|
||||
routes6 = _json(["ip", "-j", "-6", "route", "show", "default"])
|
||||
nm = _nm_devices() if shutil.which("nmcli") else None
|
||||
managers = {
|
||||
"NetworkManager": {"available": bool(shutil.which("nmcli")), "active": _active("NetworkManager.service")},
|
||||
"ifupdown": {"available": _available("ifquery"), "active": _active("networking.service")},
|
||||
"systemd-networkd": {"active": _active("systemd-networkd.service")},
|
||||
}
|
||||
active = _ifupdown() if managers["ifupdown"]["active"] else set()
|
||||
value = summarize(addresses, {"ipv4": routes4, "ipv6": routes6}, nm, active, managers)
|
||||
if addresses is None or (routes4 is None and routes6 is None):
|
||||
value.update(ok=False, error="NETWORK_KERNEL_STATUS_UNAVAILABLE")
|
||||
if nm is None and managers["NetworkManager"]["available"]:
|
||||
value["warnings"].append({"code": "NETWORKMANAGER_STATUS_UNAVAILABLE"})
|
||||
return value
|
||||
Reference in new issue
Block a user