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:
1 parent
f957489866
commit
8c595389a4
9 files changed
+372
-58
No files matched your search
@@ -27,6 +27,10 @@ class SigninError(MudfishError):
|
||||
pass
|
||||
|
||||
|
||||
class AuthError(MudfishError):
|
||||
"""The cloud API rejected our jwt (expired session); a forced re-signin may fix it."""
|
||||
|
||||
|
||||
# Decky's bundled Python ships without a CA store, so point it at SteamOS's system bundle.
|
||||
SYSTEM_CA_BUNDLES = ["/etc/ssl/certs/ca-certificates.crt", "/etc/ca-certificates/extracted/tls-ca-bundle.pem"]
|
||||
|
||||
@@ -84,18 +88,27 @@ class MudfishClient:
|
||||
def status(self) -> dict:
|
||||
return self._json("GET", self.base_url + "/do/mudrun/status")
|
||||
|
||||
def ensure_signed_in(self) -> dict:
|
||||
"""Replay the launcher's remember-me autologin when the daemon has no session."""
|
||||
def ensure_signed_in(self, force: bool = False) -> dict:
|
||||
"""Replay the launcher's remember-me autologin when the daemon has no session (or force=True)."""
|
||||
st = self.status()
|
||||
if st.get("isSignedIn"):
|
||||
if st.get("isSignedIn") and not force:
|
||||
return st
|
||||
if not st.get("autologin") and not st.get("token_v3"):
|
||||
if not st.get("token_v3") or not st.get("username"):
|
||||
raise SigninError("no saved login")
|
||||
body, ctype = _multipart({
|
||||
"autologin": "ON", "username": st["username"], "password": st["token_v3"],
|
||||
"item_conf_mode": st["item_conf_mode"], "version": st["version"],
|
||||
"item_conf_mode": st.get("item_conf_mode", ""), "version": st.get("version", ""),
|
||||
})
|
||||
signin = self._json("POST", f"https://{st['api_server']}/signin_ng_v3", body, {"Content-Type": ctype})
|
||||
url = f"https://{st['api_server']}/signin_ng_v3"
|
||||
code, data = self.transport("POST", url, body, {"Content-Type": ctype})
|
||||
if code in (401, 403):
|
||||
raise SigninError(f"signin HTTP {code}")
|
||||
if not 200 <= code < 300:
|
||||
raise MudfishError(f"HTTP {code} from {url}")
|
||||
try:
|
||||
signin = json.loads(data)
|
||||
except ValueError:
|
||||
raise MudfishError(f"invalid response from {url}")
|
||||
if signin.get("status") != 200:
|
||||
raise SigninError(f"signin status {signin.get('status')}")
|
||||
auth = self._post_json(self.base_url + "/do/auth", {
|
||||
@@ -120,12 +133,24 @@ class MudfishClient:
|
||||
def rtts(self) -> dict:
|
||||
return rtts_from_response(self._json("GET", self.base_url + "/do/rtt/staticnodes"))
|
||||
|
||||
def _graphql_user(self, jwt, api, fields) -> dict:
|
||||
resp = self._post_json(f"https://{api}/graphql", {"query": "{ user { %s } }" % fields}, {"Authorization": jwt})
|
||||
def _cloud(self, jwt, url, payload) -> dict:
|
||||
code, data = self.transport("POST", url, json.dumps(payload).encode(),
|
||||
{"Content-Type": "application/json", "Authorization": jwt})
|
||||
if code in (401, 403):
|
||||
raise AuthError(f"HTTP {code} from {url}")
|
||||
if not 200 <= code < 300:
|
||||
raise MudfishError(f"HTTP {code} from {url}")
|
||||
try:
|
||||
return resp["data"]["user"]
|
||||
except (KeyError, TypeError):
|
||||
raise MudfishError("graphql returned no user")
|
||||
return json.loads(data)
|
||||
except ValueError:
|
||||
raise MudfishError(f"invalid response from {url}")
|
||||
|
||||
def _graphql_user(self, jwt, api, fields) -> dict:
|
||||
resp = self._cloud(jwt, f"https://{api}/graphql", {"query": "{ user { %s } }" % fields})
|
||||
user = (resp.get("data") or {}).get("user") if isinstance(resp, dict) else None
|
||||
if not user:
|
||||
raise AuthError("graphql returned no user")
|
||||
return user
|
||||
|
||||
def static_nodes(self, jwt, api) -> list:
|
||||
return self._graphql_user(jwt, api, "staticNodes { sid location }")["staticNodes"]
|
||||
@@ -134,7 +159,7 @@ class MudfishClient:
|
||||
return self._graphql_user(jwt, api, "progConf { %s }" % " ".join(PROG_CONF_FIELDS))["progConf"]
|
||||
|
||||
def save_prog_conf(self, jwt, api, conf: dict) -> dict:
|
||||
resp = self._post_json(f"https://{api}/prog", conf, {"Authorization": jwt})
|
||||
resp = self._cloud(jwt, f"https://{api}/prog", conf)
|
||||
if resp.get("status") != 200:
|
||||
raise MudfishError(resp.get("msg") or f"prog status {resp.get('status')}")
|
||||
return resp
|
||||
|
||||
Reference in new issue
Block a user