Skip to content

fix(gateway): start on loopback when the interface to listen on is not there yet - #206

Merged
fylorn merged 2 commits into
mainfrom
claude/core-listen-missing-nic-7qwhdl
Sep 25, 2026
Merged

fylorn merged 2 commits into
mainfrom
claude/core-listen-missing-nic-7qwhdl

Conversation

@fylorn

@fylorn fylorn commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Requested by F · project thread

What this changes

Before: when listen.gateway.bind names an interface that is missing or offline (or a fixed address that is not on this machine), twcore serve exits at startup.

After: the gateway starts on loopback only and reports why in the listen status (gw.listen.no_such_nic / gw.listen.nic_offline / gw.listen.addr_unavailable). It re-resolves every 3 seconds, adds the interface once it appears, moves to its new address when it changes, and drops back to loopback when it goes away. Loopback is never touched. A loopback that cannot be bound (port taken) is still fatal at startup, as before.

Why

On Windows, ThinkWatch Lite's WSL adoption (Lite #216) binds the gateway to the WSL virtual adapter (vEthernet (WSL)) under NAT networking. That adapter only exists once WSL has started and gets a new address every WSL restart. A gateway autostarted at login therefore could not start at all, taking every client on the machine down with it.

How it was verified

  • 4 new tests in crates/tw-gateway/tests/hotreload.rs: missing interface starts on loopback and later switches to a resolvable config; a fixed address not on the machine starts on loopback; a taken loopback port still fails serve_at; a port change while the interface is missing moves loopback to the new port. hotreload 23/23, tw-control --test listen 8/8.
  • Real binary against a real interface (Linux, veth pair): started twcore serve with bind: twtest0 before the interface existed → listened on 127.0.0.1 only with a warning; created it with 10.98.0.1 → listener added within 3 s; re-addressed to 10.98.0.7 → moved; deleted → back to loopback only; recreated with 10.98.0.9 → added again. Loopback answered throughout.
  • cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo test --workspace (88 suites), scripts/smoke.sh 60 passed / 0 failed. Locally the clippy run needed -A clippy::nonminimal_bool for a pre-existing lint in tw-dialect/src/convert.rs that my older local toolchain (1.94) reports; untouched here.
  • Nothing on the credential or forwarding path changed.

Notes for review

  • serve_at now takes the GatewayListen instead of pre-resolved addresses, so resolution lives in one place.
  • A periodic re-check never uses the "release the retiring listener first" path; that briefly stops accepting, and repeating it every few seconds against a port someone else holds would be a periodic outage. Only a real relisten (config reload / save) does it.
  • If the config changes to an interface that is missing while a non-loopback listener is serving, the current listeners are kept (unchanged behavior). If only loopback is serving, it moves to the new port right away.
  • SaveListen still rejects an interface that is missing at save time (409); unchanged.
  • No control protocol change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JpGLt1RZLUuYEXYwao3yGk


Generated by Claude Code

…t there yet

twcore used to resolve `listen.gateway.bind` once before starting and
exit when the interface was missing or offline. On Windows the desktop
app can bind the gateway to the WSL virtual adapter (`vEthernet (WSL)`)
so that clients inside WSL reach it under NAT networking. That adapter
only exists once WSL has started and gets a new address every time WSL
restarts, so a gateway started at login could not start at all, and
every client on the machine lost it along with the WSL ones.

Resolution now happens inside the listener: when the interface is not
there, or a fixed address is not on this machine yet, the gateway
listens on loopback only and says why in the listen status. It asks the
system again every three seconds and adds the interface once it
appears, moves to its new address when it changes, and drops back to
loopback when it goes away. A loopback that cannot be bound (the port
is taken) is still fatal at startup, as before.

A periodic check never asks a retiring listener to give its port up
first; that dance briefly stops accepting connections, and repeating it
every few seconds against a port someone else holds would turn into a
periodic outage. Only a real relisten (config change, save) does it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpGLt1RZLUuYEXYwao3yGk
@fylorn fylorn self-assigned this Sep 25, 2026
@fylorn
fylorn merged commit 3899d66 into main Sep 25, 2026
4 checks passed
@fylorn
fylorn deleted the claude/core-listen-missing-nic-7qwhdl branch September 25, 2026 17:10
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.

2 participants