Skip to content

Add remote.bindHost config to force interface#478

Merged
ericdallo merged 2 commits into
editor-code-assistant:masterfrom
rschmukler:rs/bind-host-config
May 30, 2026
Merged

Add remote.bindHost config to force interface#478
ericdallo merged 2 commits into
editor-code-assistant:masterfrom
rschmukler:rs/bind-host-config

Conversation

@rschmukler
Copy link
Copy Markdown
Contributor

Summary

With multiple ECA instances on macOS, the remote server alternates between
binding the 0.0.0.0 wildcard and falling back to 127.0.0.1 on the same
port. Loopback-bound instances are unreachable over a VPN (e.g. Tailscale),
and there's no way to control which interface an instance binds to —
remote.host is display-only.

Environment

  • macOS (dual-stack, net.inet6.ip6.v6only=0)
  • remote.enabled: true, default port 7777, Tailscale active

Current behavior

The bind logic tries 0.0.0.0 first per port, falling back to 127.0.0.1 on
BindException. Since the first instance binds the IPv6 wildcard (*:7777), a
second can still bind 127.0.0.1:7777, producing an alternating pattern:

Instance Ends up on
#1 0.0.0.0:7777 (wildcard)
#2 127.0.0.1:7777
#3 0.0.0.0:7778 (wildcard)
#4 127.0.0.1:7778

Confirmed via lsof:

eca  22966  ...  TCP *:7777 (LISTEN)
eca  23176  ...  TCP 127.0.0.1:7777 (LISTEN)

Only the wildcard-bound instance is reachable over Tailscale; loopback ones
silently fail from other devices, with no indication which instance won.

Steps to reproduce

  1. macOS + Tailscale active, remote.enabled: true.
  2. Start two or more ECA instances.
  3. lsof -iTCP:7777 -sTCP:LISTEN -n -P → one on *:7777, one on 127.0.0.1:7777.
  4. Connect to <tailscale-ip>:7777 from another device — works or silently
    fails depending on which instance won the wildcard.

Proposed fix

Add a remote.bindHost option to pin the server to a specific interface (e.g.
the Tailscale IP, or 0.0.0.0 to always claim the wildcard), with no loopback
fallback. Port discovery still applies on the chosen interface.

Notes

macOS/dual-stack specific — on a stricter stack the 127.0.0.1 fallback would
fail with EADDRINUSE and jump to the next port instead.

@rschmukler rschmukler force-pushed the rs/bind-host-config branch 3 times, most recently from a793e25 to 875b901 Compare May 30, 2026 00:41
The remote server tries the 0.0.0.0 wildcard first and silently falls
back to 127.0.0.1 plus the detected LAN IP when the wildcard is taken —
which routinely happens when running multiple ECA instances on the same
port. A loopback-bound instance is unreachable over a VPN, and the
Tailscale CGNAT range (100.64.0.0/10) is never auto-added as a LAN
connector since it isn't a site-local address, so remote.host (which is
display-only) gave no way to fix it.

remote.bindHost pins the server to a specific interface — e.g. a
Tailscale IP, or 0.0.0.0 to always claim the wildcard — binding only
there with no fallback. Port discovery (7777-7796) is preserved and
unaffected; it just steps ports on the chosen interface. When unset,
behaviour is unchanged. The display host defaults to bindHost when host
is omitted and it isn't the wildcard.
@rschmukler rschmukler force-pushed the rs/bind-host-config branch from 875b901 to a0085ef Compare May 30, 2026 00:45
Copy link
Copy Markdown
Member

@ericdallo ericdallo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I don't have that in my linux.
I wonder if we should just use config.host here, I created it which that purpose WDYT?

@rschmukler
Copy link
Copy Markdown
Contributor Author

There is legitimate use for the two existing separately. Eg. If you had a DNS name you wanted to use for /remote but it didn't necessarily resolve as bindable on the host (eg. Behind a nginx server or something)

I don't have too strong of an opinion though so I defer to your judgement. This approach also just maintains backward compatibility with the existing behavior.

@ericdallo ericdallo merged commit a947de6 into editor-code-assistant:master May 30, 2026
7 checks passed
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