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

+39 -1
View File
@@ -1,7 +1,7 @@
import json
import unittest
from mudfish_decky.client import MudfishClient, MudfishError, SigninError
from mudfish_decky.client import AuthError, MudfishClient, MudfishError, SigninError
BASE = "http://127.0.0.1:8282"
SIGNED_OUT = {"status": 200, "isSignedIn": False, "username": "u", "token_v3": "123",
@@ -74,6 +74,44 @@ class ClientTest(unittest.TestCase):
with self.assertRaises(SigninError):
MudfishClient(BASE, t).ensure_signed_in()
def test_ensure_signed_in_force_skips_shortcut(self):
t = FakeTransport({
("GET", BASE + "/do/mudrun/status"): [dict(SIGNED_OUT, isSignedIn=True)],
("POST", "https://api.mudfish.net/signin_ng_v3"): [SIGNIN_OK],
("POST", BASE + "/do/auth"): [{"status": 200, "msg": "OK"}],
})
MudfishClient(BASE, t).ensure_signed_in(force=True)
self.assertEqual(len(t.calls_to("POST", "https://api.mudfish.net/signin_ng_v3")), 1)
def test_ensure_signed_in_http_401_is_signin_error(self):
t = FakeTransport({("GET", BASE + "/do/mudrun/status"): [SIGNED_OUT]})
real = t.__call__
def transport(method, url, body, headers):
if "signin_ng_v3" in url:
return 401, b"{}"
return real(method, url, body, headers)
with self.assertRaises(SigninError):
MudfishClient(BASE, transport).ensure_signed_in()
def test_ensure_signed_in_without_token_is_signin_error(self):
t = FakeTransport({("GET", BASE + "/do/mudrun/status"): [dict(SIGNED_OUT, autologin=True, token_v3="")]})
with self.assertRaises(SigninError):
MudfishClient(BASE, t).ensure_signed_in()
def test_graphql_without_user_is_auth_error(self):
t = FakeTransport({("POST", "https://api.mudfish.net/graphql"): [{"errors": ["unauthorized"]}]})
with self.assertRaises(AuthError):
MudfishClient(BASE, t).prog_conf("JWT", "api.mudfish.net")
def test_prog_http_401_is_auth_error(self):
def transport(method, url, body, headers):
return 401, b"{}"
with self.assertRaises(AuthError):
MudfishClient(BASE, transport).save_prog_conf("JWT", "api.mudfish.net", {})
def test_start_accepts_201(self):
t = FakeTransport({("POST", BASE + "/do/mudfish/start"): [{"status": 201}]})
MudfishClient(BASE, t).start()