Skip to content

Commit 2183d4f

Browse files
authored
Merge branch 'master' into plugin-docs
2 parents 5296be6 + 65fca20 commit 2183d4f

4 files changed

Lines changed: 73 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
## 0.116.4
6+
7+
- Fix remote server failing to bind when other services (e.g. Tailscale) hold the same ports on different network interfaces, by falling back to `127.0.0.1` when `0.0.0.0` is unavailable.
8+
59
## 0.116.3
610

711
- Fix remote server failing to bind when other services (e.g. Tailscale) hold the same ports on different network interfaces, by falling back to `127.0.0.1` when `0.0.0.0` is unavailable.

docs/config/remote.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ There are three ways to connect the web frontend to your ECA session. Pick the o
3131
1. Start ECA — it will log the connection URL and auth token to stderr. The URL is a deep-link you can open directly:
3232

3333
```
34-
https://web.eca.dev?host=192.168.1.42:7777&pass=a3f8b2c1...&protocol=https
34+
https://web.eca.dev?host=192.168.1.42:7777&pass=a3f8b2c1...&protocol=http
3535
```
3636

3737
2. Open `https://web.eca.dev` and enter your machine's LAN IP (e.g. `192.168.1.42`) and password

resources/ECA_VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.116.3
1+
0.116.4

src/eca/remote/server.clj

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
(:import
1212
[java.io IOException]
1313
[java.net BindException Inet4Address InetAddress NetworkInterface]
14-
[org.eclipse.jetty.server NetworkConnector Server]))
14+
[org.eclipse.jetty.server NetworkConnector Server ServerConnector]))
1515

1616
(set! *warn-on-reflection* true)
1717

@@ -63,36 +63,61 @@
6363
(.isSiteLocalAddress (InetAddress/getByName ip))
6464
(catch Exception _ false)))
6565

66-
(def ^:private bind-hosts
67-
"Ordered list of bind addresses to try per port.
68-
0.0.0.0 gives full connectivity; 127.0.0.1 is the fallback when another
69-
service (e.g. Tailscale) already holds the port on a specific interface."
70-
["0.0.0.0" "127.0.0.1"])
66+
(defn ^:private try-start-jetty
67+
"Tries to start Jetty on the given port and host.
68+
Returns the Server on success, nil on BindException/IOException."
69+
^Server [handler port host]
70+
(try
71+
(let [server (jetty/run-jetty handler {:port port :host host :join? false})]
72+
(logger/debug logger-tag (str "Bound to " host ":" port))
73+
server)
74+
(catch BindException _ nil)
75+
(catch IOException _ nil)))
76+
77+
(defn ^:private add-connector!
78+
"Adds a secondary ServerConnector to an existing Jetty server.
79+
Returns true on success, false on bind failure."
80+
[^Server server port host]
81+
(try
82+
(let [connector (doto (ServerConnector. server)
83+
(.setHost host)
84+
(.setPort port))]
85+
(.addConnector server connector)
86+
(.start connector)
87+
(logger/debug logger-tag (str "Added connector " host ":" port))
88+
true)
89+
(catch BindException _ false)
90+
(catch IOException _ false)))
7191

7292
(defn ^:private try-start-jetty-any-host
73-
"Tries to start Jetty on the given port, attempting each bind address in
74-
bind-hosts order. Returns the Server on success, nil if all fail."
75-
^Server [handler port]
76-
(reduce (fn [_ bind-host]
77-
(try
78-
(let [server (jetty/run-jetty handler {:port port :host bind-host :join? false})]
79-
(logger/debug logger-tag (str "Bound to " bind-host ":" port))
80-
(reduced server))
81-
(catch BindException _ nil)
82-
(catch IOException _ nil)))
83-
nil
84-
bind-hosts))
93+
"Tries to start Jetty on the given port. Attempts 0.0.0.0 first for full
94+
connectivity. When that fails (e.g. Tailscale holds the port on its virtual
95+
interface), binds to 127.0.0.1 and adds the LAN IP as a secondary connector
96+
so that both Tailscale proxy (which targets localhost) and Direct LAN work.
97+
Returns [server bind-host] on success, nil if all fail."
98+
[handler port lan-ip]
99+
;; 1. Try 0.0.0.0 — covers all interfaces in one binding
100+
(if-let [server (try-start-jetty handler port "0.0.0.0")]
101+
[server "0.0.0.0"]
102+
;; 2. 0.0.0.0 failed — bind localhost first (for Tailscale proxy), then
103+
;; add the LAN IP as a secondary connector so Direct LAN also works.
104+
(when-let [server (try-start-jetty handler port "127.0.0.1")]
105+
(when lan-ip
106+
(if (add-connector! server port lan-ip)
107+
(logger/debug logger-tag (str "Also listening on " lan-ip ":" port " for Direct LAN"))
108+
(logger/warn logger-tag (str "Could not bind to " lan-ip ":" port " — Direct LAN connections may not work"))))
109+
[server (if lan-ip "127.0.0.1+lan" "127.0.0.1")])))
85110

86111
(defn ^:private start-with-retry
87112
"Tries sequential ports starting from base-port up to max-port-attempts.
88113
For each port, tries all bind-hosts before moving to the next port.
89-
Returns [server actual-port] on success, nil if all attempts fail."
90-
[handler base-port]
114+
Returns [server actual-port bind-host] on success, nil if all attempts fail."
115+
[handler base-port lan-ip]
91116
(loop [port base-port
92117
attempts 0]
93118
(when (< attempts max-port-attempts)
94-
(if-let [server (try-start-jetty-any-host handler port)]
95-
[server (.getLocalPort ^NetworkConnector (first (.getConnectors ^Server server)))]
119+
(if-let [[server bind-host] (try-start-jetty-any-host handler port lan-ip)]
120+
[server (.getLocalPort ^NetworkConnector (first (.getConnectors ^Server server))) bind-host]
96121
(do (logger/debug logger-tag (str "Port " port " in use, trying " (inc port) "..."))
97122
(recur (inc port) (inc attempts)))))))
98123

@@ -108,6 +133,7 @@
108133
(when (:enabled remote-config)
109134
(let [token (or (:password remote-config) (auth/generate-token))
110135
host-base (or (:host remote-config) (detect-host))
136+
lan-ip (detect-lan-ip)
111137
user-port (:port remote-config)
112138
;; Use atom so the handler sees host:port after Jetty resolves the actual port
113139
host+port* (atom host-base)
@@ -116,16 +142,16 @@
116142
:host* host+port*
117143
:sse-connections* sse-connections*})]
118144
(try
119-
(if-let [[^Server jetty-server actual-port]
145+
(if-let [[^Server jetty-server actual-port bind-host]
120146
(if user-port
121147
;; User-specified port: single attempt, try all bind hosts
122-
(if-let [server (try-start-jetty-any-host handler user-port)]
123-
[server (.getLocalPort ^NetworkConnector (first (.getConnectors ^Server server)))]
148+
(if-let [[server bh] (try-start-jetty-any-host handler user-port lan-ip)]
149+
[server (.getLocalPort ^NetworkConnector (first (.getConnectors ^Server server))) bh]
124150
(do (logger/warn logger-tag "Port" user-port "is already in use."
125151
"Remote server will not start.")
126152
nil))
127153
;; Default: try sequential ports starting from default-port
128-
(or (start-with-retry handler default-port)
154+
(or (start-with-retry handler default-port lan-ip)
129155
(do (logger/warn logger-tag
130156
(str "Could not bind to ports " default-port "-"
131157
(+ default-port (dec max-port-attempts))
@@ -135,15 +161,24 @@
135161
_ (reset! host+port* host-with-port)
136162
heartbeat-ch (sse/start-heartbeat! sse-connections*)
137163
private? (private-ip? host-base)
138-
connect-url (when-not private?
164+
localhost-only? (and (= bind-host "127.0.0.1") (not lan-ip))
165+
connect-url (if private?
166+
(str "https://web.eca.dev?host="
167+
host-with-port
168+
"&pass=" token
169+
"&protocol=http")
139170
(str "https://web.eca.dev?host="
140171
host-with-port
141-
"&pass=" token))]
172+
"&pass=" token
173+
"&protocol=https"))]
174+
(when (and localhost-only? private? (not= host-base "127.0.0.1"))
175+
(logger/warn logger-tag
176+
(str "⚠️ Bound to 127.0.0.1:" actual-port " (localhost only) because another service "
177+
"(e.g. Tailscale) holds port " actual-port " on the external interface. "
178+
"Direct LAN connections to " host-base " will not work. "
179+
"Use a different port, stop the conflicting service, or connect via Tailscale.")))
142180
(logger/info logger-tag (str "🌐 Remote server started on port " actual-port))
143-
(if connect-url
144-
(logger/info logger-tag (str "🔗 " connect-url))
145-
(do (logger/info logger-tag (str "🔗 http://" host-with-port))
146-
(logger/info logger-tag "📖 Private IP detected — see https://eca.dev/config/remote for connection options")))
181+
(logger/info logger-tag (str "🔗 " connect-url))
147182
{:server jetty-server
148183
:sse-connections* sse-connections*
149184
:heartbeat-stop-ch heartbeat-ch

0 commit comments

Comments
 (0)