fix: turn VPN off on sleep/resume and unload; harden teardown

- Detect resume (boottime-monotonic jump) and tear down the tunnel: on wake
  NetworkManager drops Mudfish's relay route and all traffic black-holes.
- Turn the tunnel off on plugin unload (Deck shutdown/restart).
- One verified teardown path for disconnect, connect failure/timeout and
  node-change failure; keep the route snapshot if tap0 stays up.
- Toggle can always turn off a stuck tunnel; status failure with tap0 up
  is reported as an error, not off.
- Re-sign in once when the cloud rejects an expired jwt.
- Expire cached pings after 10 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ysj7705andClaude Opus 5.5 committed 2026-10-24 21:21:21 +09:00
1 parent f957489866
commit 8c595389a4
9 files changed
+372 -58

No files matched your search

+20 -4
View File
@@ -65,12 +65,17 @@ export function Panel() {
const [advanced, setAdvanced] = useState(false);
const [nodes, setNodes] = useState<NodeInfo[]>([]);
const polling = useRef(false);
const acting = useRef(false);
const refresh = async () => {
if (polling.current) return;
polling.current = true;
try {
setState(await getState());
const next = await getState();
// A poll that started before a toggle press would overwrite the action's result.
if (!acting.current) setState(next);
} catch (e) {
console.error("mudfish: getState failed", e);
} finally {
polling.current = false;
}
@@ -84,18 +89,28 @@ export function Panel() {
useEffect(() => {
if (!advanced) return;
const load = async () => setNodes(await listNodes());
const load = async () => {
try {
setNodes(await listNodes());
} catch (e) {
console.error("mudfish: listNodes failed", e);
}
};
load();
const id = setInterval(load, NODES_POLL_MS);
return () => clearInterval(id);
}, [advanced]);
const run = async (action: () => Promise<VpnState>, pending?: Phase) => {
acting.current = true;
setBusy(true);
if (pending && state) setState({ ...state, phase: pending, message: null });
try {
setState(await action());
} catch (e) {
if (state) setState({ ...state, phase: "error", message: `요청 실패: ${e}` });
} finally {
acting.current = false;
setBusy(false);
}
};
@@ -110,7 +125,8 @@ export function Panel() {
);
}
const active = state.phase === "on" || state.phase === "connecting";
// Checked whenever any tunnel exists (even a stuck one), so the toggle can always turn it off.
const active = state.tunnel || state.phase === "on" || state.phase === "connecting";
const options = [
{ data: "auto", label: "자동 (핑 최저)" },
...nodes.map((n) => ({ data: String(n.sid), label: `${n.label} · ${rttText(n.rtt)}` })),
@@ -128,7 +144,7 @@ export function Panel() {
<ToggleField
label="미꾸라지 Full VPN"
checked={active}
disabled={busy || state.phase === "connecting" || state.phase === "missing"}
disabled={busy || state.phase === "missing"}
onChange={(on) => run(on ? connect : disconnect, on ? "connecting" : undefined)}
/>
</PanelSectionRow>