Skip to content

Find a paired machine again after either side changes network - #453

Merged
Tryanks merged 3 commits into
mainfrom
remote-resume-across-networks
Sep 16, 2026
Merged

Tryanks merged 3 commits into
mainfrom
remote-resume-across-networks

Conversation

@Tryanks

@Tryanks Tryanks commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Closes #409.

Behaviour

A pairing is bound to the machine identity (host_id), not to an address. When the saved origin stops answering, or this device's interface addresses change, a native client races every origin it knows for the same machine and resumes the same session without re-pairing or manual address entry.

  • PairedHost gains a bounded, ordered candidates list (max 16): origins that completed hello before, the addresses the machine reports in hello_ok, and mDNS hints. Older hosts.json records (one origin, or legacy addrs+port) still load; the one-origin form re-serializes unchanged.
  • hello_ok now carries addrs and port; hello_rejected carries host_id. A hello_ok or token rejection from a different machine at a stale address counts as unreachable, never as "Pair again". Only the paired machine (or an older host at the saved origin that does not name itself) can end the pairing.
  • Reconnect racing (250 ms stagger, 5 s budget, max 32 origins) covers saved candidates, mDNS hints, hotspot gateways (iOS/Android/macOS/Windows hotspot subnets), the .1 of private networks up to /24, and every guest of a private /28 or smaller. Only for plain http origins; https pairings are never downgraded. The browser client keeps its fixed page origin.
  • The native connection loop polls the local interface set (2.5 s while reconnecting, 10 s while connected). A change cuts the backoff short with a fresh candidate list, or probes a live socket within 3 s.
  • Backoff resets its delay on a network change while the attempt counter keeps counting, so the banner and the store's per-attempt browse dedup keep working.
  • Host side: mdns-sd 0.21.3 already re-announces addr_auto services on interface changes (ip-check timer every 5 s), so the beacon needs no change beyond the hello fields. The listener already binds all interfaces.

The transport is now the single owner of "find the host again"; the UI store only forwards mDNS hints through Wake::Candidates (iOS can drive Bonjour only from the UI thread). refresh_origin is replaced by discover_origins.

Tests

  • Literal older hosts.json fixtures load and migrate; promotion and bounded candidates.
  • hello_verdict against literal hello_ok/hello_rejected shapes: mismatch is non-terminal, missing host_id primary-vs-candidate, addrs filtering.
  • Interface probe derivation and interface-set change detection (pure functions).
  • Integration (two real servers): a stranger at the saved address rejecting the token stays Reconnecting, the answering candidate is promoted, token and stamp untouched; literal hello_ok/hello_rejected JSON asserted.
  • Backoff: network change shortens the next delay without restarting attempt numbers.

Checks run

cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo build --workspace --locked, cargo test --workspace --locked (all green), cargo machete, iOS sim / wasm32 / Android arm64 cargo check with -D warnings.

Live check: headless host paired on port 47500, then restarted on 47501 with the same data dir; the client hit connection-refused on the saved origin, picked up the mDNS hint, promoted the new origin in hosts.json and reconnected with the original token (one device record on the host, no re-pair). Not exercised live: real phone hotspots and interface aliasing (covered by pure-function tests).

Known gaps

  • When this device hosts a /24 hotspot, the machine's guest address cannot be guessed; mDNS or the machine's reported addresses must supply it.
  • CGNAT (100.64/10) and other non-private networks get no gateway probe by design.
  • Promotion (transport thread) and stamp_connected (UI) both load→modify→save hosts.json; they run sequentially in practice but there is no file lock.
  • Racing sends the bearer token to guessed LAN origins over plain HTTP, the same exposure as the previous mDNS-hint origin swap, widened to gateway/subnet probes.

hello_ok now carries the host's non-loopback, non-link-local interface
addresses and its listening port, and hello_rejected carries the host_id, so
a client can remember where else the paired machine answers and can tell its
own machine's refusal from a stranger that now owns a stale address.

local_addrs moves to discovery, shared by pairing codes and hello, and
if-addrs becomes an unconditional dependency of tcode-remote (mdns-sd already
pulls it on every target).
A pairing is bound to the machine identity, never to an address. PairedHost
keeps a bounded, ordered list of candidate origins next to the one that last
worked: origins that completed hello before, the addresses the machine
reported in hello_ok, and LAN discovery hints. Older hosts.json records load
unchanged; legacy multi-address records migrate their extra addresses to
candidates.

The native connection loop owns finding the machine again. The saved origin
is tried alone first; once it fails to answer, or as soon as this device's
interface addresses change, it races the candidates and probes derived from
the device's own networks (hotspot and small-subnet gateways, every guest of
a /28 hotspot it shares) with a five-second budget each. Every reply is
judged by host_id: the first answer from the paired machine wins and is
promoted and persisted, a different machine at any address is a plain miss,
and only the paired machine's own token rejection ends the pairing. HTTPS
pairings are never downgraded; the browser client keeps its page origin.

The loop polls its interface set (2.5 s while reconnecting, 10 s while
connected): a change cuts the backoff short with a reset delay, or probes a
live socket so one left on a vanished interface is replaced within seconds.
The store's per-attempt mDNS browse now only feeds hints to the transport
(Wake::Candidates) instead of swapping the origin itself.

Closes #409.
Describe candidate origins in hosts.json, the identity-verified racing and
interface probes, hotspot behaviour, the network-change wake, and that HTTPS
pairings are never downgraded. DESIGN.md notes that finding the machine at
another address is silent in the Reconnecting banner.
@Tryanks
Tryanks merged commit f8dca71 into main Sep 16, 2026
11 of 12 checks passed
@Tryanks
Tryanks deleted the remote-resume-across-networks branch September 16, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remote: resume the same session across network changes (LAN → LAN, and future off-LAN)

1 participant