Devices: fixes from review round 18

- A set-up headset whose alias goes through a jump host (ProxyJump or
  ProxyCommand in ~/.ssh/config) is reached through it, address by address,
  still pinned per headset.
- A refused switch puts the header's switcher back on the headset in use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-29 00:17:29 +10:00
1 parent 90fd2684ba
commit c3e3f2629c
3 files changed
+39 -1

No files matched your search

+9
View File
@@ -344,6 +344,15 @@ class Connecting(unittest.TestCase):
self.assertEqual(self.link.snapshot()["phase"], "connected")
self.assertEqual(self.link.named_route(), ("frame-bare", pinned)) # terminals go there too
def test_a_set_up_headset_behind_a_jump_host_is_left_to_ssh(self):
d = self.device("10.99.99.98", "10.99.99.99") # neither answers directly
self.hosts({"10.99.99.98": "wrong", "10.99.99.99": "ok"})
with mock.patch.object(fl, "ssh_g", return_value=("frame-t", 22, "steamos", True)):
self.link.connect(["start"])
s = self.link.snapshot()
self.assertEqual((s["phase"], s["via"]["host"]), ("connected", "10.99.99.99"), s["error"])
self.assertIn(f"HostKeyAlias=frame-control-{d['id']}", self.routes[-1][1]) # still pinned per headset
def test_a_bare_alias_behind_a_jump_host_is_left_to_ssh(self):
self.link.override = "frame-jump"
self.hosts({"10.99.99.99": "ok"}) # ssh's ProxyJump would get there
+25
View File
@@ -584,6 +584,31 @@ class Link:
ranked = [(a, "from ~/.ssh/config")]
else:
ranked = frame_devices.order_addresses(device["addresses"], net.get("id"), bool(ts.get("up")))
if ssh_g(device["alias"])[3]:
# ~/.ssh/config sends this alias through a jump host: a direct probe says
# nothing, so let ssh (through the jump host) try each address in turn.
with self.cond:
self.state["probes"] = [dict(a, why=why, state="waiting", detail="Through a jump host", ip=None,
rtt_ms=None, label=a.get("label") or "") for a, why in ranked]
self.stage("find", "done", f"{device['alias']} goes through a jump host; ssh finds it")
for i, (a, why) in enumerate(ranked):
if i:
for sid in ("ssh", "identity", "login"):
self.stage(sid, "pending", "")
outcome = self.handshake(device, a, {"ip": None, "rtt_ms": None}, device.get("user"))
if outcome == "ok":
self.probe_update(i, state="answered", detail="Reached through the jump host")
self.publish(via={"host": a["host"], "kind": a["kind"], "ip": None, "rtt_ms": None,
"why": "through a jump host", "network": net.get("id"),
"network_name": net["name"]})
self.learn(device, a["host"], net, None)
return True
with self.cond:
why_not = (self.state["error"] or {}).get("message") or "SSH failed"
self.probe_update(i, state="sshfailed", detail=why_not)
if outcome != "next":
return False
return False
with self.cond:
self.state["probes"] = [{"host": a["host"], "kind": a["kind"], "label": a.get("label") or "",
"why": why, "state": "waiting", "detail": "Waiting", "ip": None,
+5 -1
View File
@@ -2677,7 +2677,11 @@ async function useDevice(id) {
const d = dv.list.find(x => x.id === id);
if (!d || d.active) return;
devGen++; // from here on, answers about the last headset are dropped
await act(`Switch to ${d.name}`, () => api("/api/devices", { action: "use", id }));
const res = await act(`Switch to ${d.name}`, () => api("/api/devices", { action: "use", id }));
if (!res) { // refused (an install is running): show the headset still in use
const current = dv.list.find(x => x.active);
if (current) $("devSel").value = current.id;
}
}
async function devAction(label, body, btn) {
const res = await act(label, () => api("/api/devices", body), btn);