feat: japan node ranking and leftover route detection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ysj7705andClaude Opus 5.5 committed 2026-10-24 20:44:43 +09:00
1 parent e60873b75a
commit 18b8ee1dd7
4 files changed
+168

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
"""Japan node filtering and ping ranking."""
from dataclasses import dataclass
from typing import Optional
JAPAN_PREFIX = "JP "
@dataclass(frozen=True)
class Node:
sid: int
label: str
rtt: Optional[float]
def rtts_from_response(resp: dict) -> dict:
"""Map sid -> average RTT (ms) from /do/rtt/staticnodes, skipping failed probes.
While Mudfish is mid-round the endpoint answers {"status": 300} without "rtts".
"""
return {r["sid"]: float(r["rttavg"]) for r in resp.get("rtts", []) if r.get("success", 0) > 0}
def _label(location: str) -> str:
start, end = location.find("("), location.rfind(")")
if start == -1 or end <= start:
return location
return location[start + 1:end]
def japan_nodes(static_nodes: list, rtts: dict) -> list:
"""Japan nodes sorted by ping; unmeasured nodes go last, by label."""
nodes = [Node(n["sid"], _label(n["location"]), rtts.get(n["sid"]))
for n in static_nodes if n["location"].startswith(JAPAN_PREFIX)]
measured = sorted((n for n in nodes if n.rtt is not None), key=lambda n: n.rtt)
unmeasured = sorted((n for n in nodes if n.rtt is None), key=lambda n: n.label)
return measured + unmeasured
def pick_auto(nodes: list) -> Optional[Node]:
return next((n for n in nodes if n.rtt is not None), None)
+36
View File
@@ -0,0 +1,36 @@
"""Detect and remove the per-node host routes Mudfish leaves behind after it stops."""
import subprocess
from typing import Optional
def host_routes(route_text: str) -> set:
"""Host routes (no prefix length, no proto, with a gateway) from `ip -4 route` output."""
routes = set()
for line in route_text.splitlines():
parts = line.split()
if not parts or "/" in parts[0] or parts[0] == "default":
continue
if "proto" in parts or "via" not in parts:
continue
routes.add(" ".join(parts))
return routes
def leftover_routes(before: Optional[set], after_text: str) -> list:
"""Host routes added since `before`, excluding tunnel routes. No snapshot means delete nothing."""
if before is None:
return []
added = host_routes(after_text) - set(before)
return sorted(r for r in added if " dev tap" not in f" {r}")
def delete_routes(lines: list, run=subprocess.run) -> int:
deleted = 0
for line in lines:
try:
result = run(["ip", "route", "del", *line.split()], capture_output=True, timeout=5)
except Exception:
continue
if result.returncode == 0:
deleted += 1
return deleted