net-tools/bin/net
Natalie 68c848dc56 feat(@tools/net-tools): add tray icon system
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-10 02:20:23 -07:00

247 lines
9 KiB
Python
Executable file

#!/usr/bin/env python3
"""net — the one command for the mesh.
Every verb is a thin face over the same machinery the fleet agent runs (this
script imports smart-lan-router.py as a module — shims, identity, and config
are written exactly once). The GUI and tray call these same verbs, so no
surface can disagree with another.
net status fleet table (every agent's last snapshot)
net whoami which host this is, roles, vantage
net doctor [host] probe lan/wg/identity per path, name the chokepoint
net sync force-converge this node's /etc/hosts + ssh now
net up | net down bring the wg tunnel up / down
net enroll phone NAME --os ios|android [--wg 10.9.0.N]
wg peer + QR (wg-phone-add) + declared entry
net gui open the Mesh control window (darwin)
"""
from __future__ import annotations
import importlib.util
import json
import os
import shutil
import subprocess
import sys
import time
# --- locate the repo + import the agent as a library ---------------------------
SELF = os.path.abspath(__file__)
while os.path.islink(SELF):
link = os.readlink(SELF)
SELF = link if os.path.isabs(link) else os.path.join(os.path.dirname(SELF), link)
ROOT = os.path.dirname(SELF)
while ROOT != "/" and not os.path.isfile(os.path.join(ROOT, "data", "mesh-hosts.json")):
ROOT = os.path.dirname(ROOT)
AGENT_PY = os.path.join(ROOT, "smart-lan-router", "smart-lan-router.py")
_spec = importlib.util.spec_from_file_location("slr", AGENT_PY)
slr = importlib.util.module_from_spec(_spec)
sys.modules["slr"] = slr
_spec.loader.exec_module(slr)
def data() -> dict:
return slr.load_json(slr.find_data_file())
def overlay() -> dict:
p = os.path.join(ROOT, "data", "lan-state.json")
try:
return slr.load_json(p)
except (FileNotFoundError, json.JSONDecodeError, OSError):
return {}
def host_entry(d: dict, name: str) -> dict | None:
for h in d.get("hosts", []):
if h["name"] == name or name in (h.get("aliases") or []):
return h
return None
def ping_ms(ip: str, timeout_s: int = 2) -> float | None:
ping = shutil.which("ping") or "/sbin/ping"
flag = "-t" if slr.PLATFORM == "darwin" else "-W"
rc, out, _ = slr._run([ping, "-c", "1", flag, str(timeout_s), ip], timeout_s + 2)
if rc != 0:
return None
import re
m = re.search(r"time=([\d.]+)", out)
return float(m.group(1)) if m else 0.0
# --- verbs ---------------------------------------------------------------------
def cmd_whoami(_args: list[str]) -> int:
ctx = slr.build_ctx(slr.find_data_file())
cfg = slr.load_config(slr.find_data_file())
home, gw, gwif = slr.is_home(cfg)
print(f"host : {ctx['self_name'] or 'UNKNOWN — not in mesh-hosts.json'}")
print(f"platform : {slr.PLATFORM}")
print(f"roles : {', '.join(sorted(ctx['roles']))}")
print(f"location : {'HOME' if home else 'AWAY'} (gw {gw} on {gwif})")
print(f"vantage : {'LAN-capable' if (ctx['self_lan'] is not None or 'route' in ctx['roles']) else 'mesh-only'}")
return 0
def cmd_status(_args: list[str]) -> int:
os.execv(os.path.join(ROOT, "bin", "fleet-status"), ["fleet-status"])
def cmd_sync(_args: list[str]) -> int:
rc1 = subprocess.run(["sudo", os.path.join(ROOT, "bin", "mesh-hosts-render"), "--install"]).returncode
rc2 = subprocess.run([os.path.join(ROOT, "bin", "host-apply"), "--ssh-apply"]).returncode
return rc1 or rc2
def cmd_doctor(args: list[str]) -> int:
d = data()
ov = overlay()
me = slr.identify_self(d)
my_name = me["name"] if me else None
targets = []
if args:
h = host_entry(d, args[0])
if not h:
print(f"doctor: unknown host '{args[0]}'", file=sys.stderr)
return 1
targets = [h]
else:
targets = [h for h in d["hosts"] if h["name"] != my_name and h.get("ssh_user") is not None]
worst = 0
for h in targets:
name = h["name"]
lan_ip = ov.get(name) or h.get("lan")
wg_ip = h.get("wg")
print(f"\n{name}" + (f" (aliases: {', '.join(h['aliases'])})" if h.get("aliases") else ""))
lan_ms = ping_ms(lan_ip) if lan_ip else None
wg_ms = ping_ms(wg_ip) if wg_ip else None
if lan_ip:
print(f" lan {lan_ip:<14} {'%.1f ms' % lan_ms if lan_ms is not None else 'UNREACHABLE'}")
if wg_ip:
print(f" wg {wg_ip:<14} {'%.1f ms' % wg_ms if wg_ms is not None else 'UNREACHABLE'}")
ident = h.get("identity")
ident_ok = None
if ident and lan_ip:
url = ident["url"].replace("{ip}", lan_ip)
rc, out, _ = slr._run(["/usr/bin/curl", "-s", "--max-time", "4", url], 6)
ident_ok = rc == 0 and all(m in out for m in ident.get("markers", []))
print(f" svc {url.split('/')[2]:<14} {'OK' if ident_ok else 'no answer'}")
# verdict
if lan_ip and lan_ms is not None:
print(f" → healthy: direct LAN path ({lan_ms:.1f} ms)")
elif wg_ms is not None:
if lan_ip:
print(f" → LAN path dead but mesh alive — use {name}.wg; check the host's LAN link/switch")
worst = max(worst, 1)
else:
print(f" → reachable via mesh ({wg_ms:.1f} ms) — normal for this host")
else:
print(f" → DOWN on every path — host offline, or this node's tunnel is down")
worst = max(worst, 2)
print()
return worst
def _wg_conf() -> str:
cand = [os.path.expanduser("~/.wireguard/wg1.conf"), "/etc/wireguard/wg1.conf"]
for c in cand:
if os.path.exists(c):
return c
return cand[0]
def cmd_up(_args: list[str]) -> int:
return subprocess.run(["sudo", shutil.which("wg-quick") or "wg-quick", "up", _wg_conf()]).returncode
def cmd_down(_args: list[str]) -> int:
return subprocess.run(["sudo", shutil.which("wg-quick") or "wg-quick", "down", _wg_conf()]).returncode
def cmd_enroll(args: list[str]) -> int:
if not args or args[0] != "phone" or len(args) < 2:
print("usage: net enroll phone <name> [--os ios|android] [--wg 10.9.0.N]", file=sys.stderr)
return 1
name = args[1]
osname = "ios"
wg_ip = None
rest = args[2:]
while rest:
if rest[0] == "--os" and len(rest) > 1:
osname, rest = rest[1], rest[2:]
elif rest[0] == "--wg" and len(rest) > 1:
wg_ip, rest = rest[1], rest[2:]
else:
print(f"enroll: unknown arg {rest[0]}", file=sys.stderr)
return 1
wpa = shutil.which("wg-phone-add") or os.path.expanduser(
"~/Code/@scripts/session-tools/bin/wg-phone-add")
if not os.path.exists(wpa):
print("enroll: wg-phone-add not found (session-tools)", file=sys.stderr)
return 1
cmd = [wpa, "-d", name] + (["-i", wg_ip] if wg_ip else [])
if subprocess.run(cmd).returncode != 0:
return 1
# read the address wg-phone-add allocated
addr_file = os.path.expanduser(f"~/.config/wg-mesh/clients/{name}/address")
try:
with open(addr_file, encoding="utf-8") as fh:
wg_ip = fh.read().strip()
except OSError:
print(f"enroll: peer created but {addr_file} unreadable — add the JSON entry manually", file=sys.stderr)
return 1
df = slr.find_data_file()
d = slr.load_json(df)
if host_entry(d, name):
print(f"enroll: {name} already declared")
return 0
d["hosts"].append({
"name": name, "aliases": [], "class": "phone",
"role": f"phone ({osname}) — wg mesh client via WireGuard app (DNS=10.9.0.2); no agent, no sshd",
"os": osname, "ssh_user": None, "wg": wg_ip,
"lan": None, "public": None, "mac": None, "identity": None,
})
with open(df, "w", encoding="utf-8") as fh:
json.dump(d, fh, indent=2, ensure_ascii=False)
fh.write("\n")
subprocess.run(["git", "-C", ROOT, "add", "data/mesh-hosts.json"], capture_output=True)
print(f"enroll: {name} ({osname}) declared at {wg_ip} — staged; fleet converges after the next autocommit+push")
return 0
def cmd_gui(_args: list[str]) -> int:
if slr.PLATFORM != "darwin":
print("gui: darwin-only for now (use `net status` / the web dashboard)", file=sys.stderr)
return 1
py = os.path.join(ROOT, "tray", ".venv", "bin", "python")
if not os.path.exists(py):
print("gui: tray venv missing — run tray/install-tray.sh first", file=sys.stderr)
return 1
return subprocess.run([py, os.path.join(ROOT, "gui", "mesh-gui.py")]).returncode
VERBS = {
"status": cmd_status, "whoami": cmd_whoami, "sync": cmd_sync,
"doctor": cmd_doctor, "up": cmd_up, "down": cmd_down,
"enroll": cmd_enroll, "gui": cmd_gui,
}
def main() -> int:
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
print(__doc__.strip())
return 0
verb = sys.argv[1]
fn = VERBS.get(verb)
if fn is None:
print(f"net: unknown verb '{verb}' (try: {', '.join(VERBS)})", file=sys.stderr)
return 1
return fn(sys.argv[2:])
if __name__ == "__main__":
sys.exit(main())