From 08d6dd089b9b5848f717e147e07c5cd5bd51f9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20F=C3=BCcher?= Date: Mon, 23 Mar 2026 22:50:51 -0300 Subject: [PATCH 1/3] Fix remote server on Windows stealing TLS traffic from tunnel adapters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, binding to 0.0.0.0 on a port also used by Tailscale or WireGuard captures their TLS traffic, causing handshake failures. Detect active tunnel interfaces and, when present, bind to specific interfaces (127.0.0.1 + LAN IP) instead of the wildcard address. 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca --- CHANGELOG.md | 1 + src/eca/remote/server.clj | 71 ++++++++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4e3e038f..f15ff6abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Bump plumcp to 0.2.0-beta5. - Fix auto-continue clobbering new prompt status and losing the stop button. +- Fix remote server on Windows stealing TLS traffic from Tailscale/WireGuard when using the same port, by binding to specific interfaces instead of `0.0.0.0` when tunnel adapters are detected. ## 0.116.5 diff --git a/src/eca/remote/server.clj b/src/eca/remote/server.clj index d73403794..e382da493 100644 --- a/src/eca/remote/server.clj +++ b/src/eca/remote/server.clj @@ -30,6 +30,14 @@ These are deprioritized when detecting the LAN IP." #"^(docker|br-|veth|vbox|virbr|tailscale|lo|tun|tap|wg|zt)") +(def ^:private tunnel-interface-re + "Matches network interface name or display name for tunnel/VPN services that + may use port-based proxying (e.g. `tailscale serve`). On Windows, binding + 0.0.0.0 on the same port would steal their TLS traffic. + Java on Windows returns names like 'iftype53_32768' for Tailscale with + display name 'Tailscale Tunnel', so both fields must be checked." + #"(?i)(tailscale|wireguard|zerotier)") + (defn ^:private interface-priority "Returns a sort priority for a network interface (lower = preferred). Real hardware interfaces (wifi, ethernet) are preferred over virtual ones." @@ -37,6 +45,27 @@ (let [name (.getName ni)] (if (re-find virtual-interface-re name) 1 0))) +(def ^:private windows? + (-> (System/getProperty "os.name" "") + (.toLowerCase) + (.startsWith "windows"))) + +(defn ^:private has-tunnel-interfaces? + "Returns true if any active tunnel/VPN network interface is detected. + On Windows, binding 0.0.0.0 on a port used by such services (e.g. Tailscale serve) + would capture their TLS traffic, causing handshake failures. + Checks both getName() and getDisplayName() because Java on Windows uses + opaque names like 'iftype53_32768' while the display name is 'Tailscale Tunnel'." + [] + (try + (boolean + (some (fn [^NetworkInterface ni] + (and (.isUp ni) + (or (re-find tunnel-interface-re (.getName ni)) + (re-find tunnel-interface-re (.getDisplayName ni))))) + (enumeration-seq (NetworkInterface/getNetworkInterfaces)))) + (catch Exception _ false))) + (defn ^:private detect-lan-ip "Enumerates network interfaces to find a site-local (private) IPv4 address. Prefers real hardware interfaces (wifi, ethernet) over virtual ones (docker, vbox). @@ -105,24 +134,36 @@ (catch BindException _ false) (catch IOException _ false))) +(defn ^:private start-on-specific-interfaces + "Binds to 127.0.0.1 first (for localhost / reverse proxy access), then adds + the LAN IP as a secondary connector for Direct LAN access. + Returns [server bind-host] on success, nil if bind fails." + [handler port lan-ip] + (when-let [server (try-start-jetty handler port "127.0.0.1")] + (when lan-ip + (if (add-connector! server port lan-ip) + (logger/debug logger-tag (str "Also listening on " lan-ip ":" port " for Direct LAN")) + (logger/warn logger-tag (str "Could not bind to " lan-ip ":" port " — Direct LAN connections may not work")))) + [server (if lan-ip "127.0.0.1+lan" "127.0.0.1")])) + (defn ^:private try-start-jetty-any-host - "Tries to start Jetty on the given port. Attempts 0.0.0.0 first for full - connectivity. When that fails (e.g. Tailscale holds the port on its virtual - interface), binds to 127.0.0.1 and adds the LAN IP as a secondary connector - so that both Tailscale proxy (which targets localhost) and Direct LAN work. + "Tries to start Jetty on the given port. On Windows with active tunnel + interfaces (Tailscale, WireGuard, etc.), skips the 0.0.0.0 wildcard bind + because Windows would capture traffic on the tunnel interface, preventing + services like `tailscale serve` from terminating TLS on the same port. + Otherwise attempts 0.0.0.0 first for full connectivity, falling back to + 127.0.0.1 + LAN IP connector. Returns [server bind-host] on success, nil if all fail." [handler port lan-ip] - ;; 1. Try 0.0.0.0 — covers all interfaces in one binding - (if-let [server (try-start-jetty handler port "0.0.0.0")] - [server "0.0.0.0"] - ;; 2. 0.0.0.0 failed — bind localhost first (for Tailscale proxy), then - ;; add the LAN IP as a secondary connector so Direct LAN also works. - (when-let [server (try-start-jetty handler port "127.0.0.1")] - (when lan-ip - (if (add-connector! server port lan-ip) - (logger/debug logger-tag (str "Also listening on " lan-ip ":" port " for Direct LAN")) - (logger/warn logger-tag (str "Could not bind to " lan-ip ":" port " — Direct LAN connections may not work")))) - [server (if lan-ip "127.0.0.1+lan" "127.0.0.1")]))) + (if (and windows? (has-tunnel-interfaces?)) + ;; On Windows with tunnel interfaces, bind only to specific interfaces + ;; to avoid stealing traffic from Tailscale/WireGuard virtual interfaces. + (do (logger/debug logger-tag "Tunnel interface detected on Windows, binding to specific interfaces only") + (start-on-specific-interfaces handler port lan-ip)) + ;; Default: try 0.0.0.0 first, fall back to specific interfaces + (if-let [server (try-start-jetty handler port "0.0.0.0")] + [server "0.0.0.0"] + (start-on-specific-interfaces handler port lan-ip)))) (defn ^:private start-with-retry "Tries sequential ports starting from base-port up to max-port-attempts. From 812dc4097ce962c3402e649f73f037b3770ffab2 Mon Sep 17 00:00:00 2001 From: Eric Dallo Date: Thu, 26 Mar 2026 09:22:51 -0300 Subject: [PATCH 2/3] Fix changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4762cf2c8..030bad220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Fix remote server on Windows stealing TLS traffic from Tailscale/WireGuard when using the same port, by binding to specific interfaces instead of `0.0.0.0` when tunnel adapters are detected. + ## 0.117.0 - Fix `/compact` triggering empty-response retries and rejected tool errors after the compact tool finishes. @@ -15,7 +17,6 @@ - Add configurable shell for `shell_command` tool via `toolCall.shellCommand.path` and `toolCall.shellCommand.args`. #370 - Fix providers disappearing from `/login` after saving an API key. eca-emacs#196 - Fix `remote.enabled` in project-local `.eca/config.json` being ignored when a global config also exists. -- Fix remote server on Windows stealing TLS traffic from Tailscale/WireGuard when using the same port, by binding to specific interfaces instead of `0.0.0.0` when tunnel adapters are detected. ## 0.116.5 From 4fc6b10155390c0948e01639c6a38d3ebbc0bdc0 Mon Sep 17 00:00:00 2001 From: Eric Dallo Date: Thu, 26 Mar 2026 09:32:11 -0300 Subject: [PATCH 3/3] Fix windows function --- src/eca/remote/server.clj | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/eca/remote/server.clj b/src/eca/remote/server.clj index c9e151776..eaf3bbd21 100644 --- a/src/eca/remote/server.clj +++ b/src/eca/remote/server.clj @@ -9,10 +9,15 @@ [eca.remote.auth :as auth] [eca.remote.routes :as routes] [eca.remote.sse :as sse] + [eca.shared :as shared] [ring.adapter.jetty :as jetty]) (:import [java.io IOException] - [java.net BindException Inet4Address InetAddress NetworkInterface] + [java.net + BindException + Inet4Address + InetAddress + NetworkInterface] [java.security KeyFactory KeyStore] [java.security.cert CertificateFactory] [java.security.spec PKCS8EncodedKeySpec] @@ -53,11 +58,6 @@ (let [name (.getName ni)] (if (re-find virtual-interface-re name) 1 0))) -(def ^:private windows? - (-> (System/getProperty "os.name" "") - (.toLowerCase) - (.startsWith "windows"))) - (defn ^:private has-tunnel-interfaces? "Returns true if any active tunnel/VPN network interface is detected. On Windows, binding 0.0.0.0 on a port used by such services (e.g. Tailscale serve) @@ -141,11 +141,11 @@ (string/join)) key-bytes (.decode (Base64/getDecoder) ^String b64) pk (or (try (.generatePrivate (KeyFactory/getInstance "RSA") - (PKCS8EncodedKeySpec. key-bytes)) - (catch Exception _ nil)) + (PKCS8EncodedKeySpec. key-bytes)) + (catch Exception _ nil)) (try (.generatePrivate (KeyFactory/getInstance "EC") - (PKCS8EncodedKeySpec. key-bytes)) - (catch Exception _ nil))) + (PKCS8EncodedKeySpec. key-bytes)) + (catch Exception _ nil))) ks (doto (KeyStore/getInstance (KeyStore/getDefaultType)) (.load nil nil) (.setKeyEntry "server" pk (char-array 0) @@ -225,7 +225,7 @@ 127.0.0.1 + LAN IP connector. Returns [server bind-host] on success, nil if all fail." [handler port lan-ip ^SSLContext ssl-context] - (if (and windows? (has-tunnel-interfaces?)) + (if (and shared/windows-os? (has-tunnel-interfaces?)) ;; On Windows with tunnel interfaces, bind only to specific interfaces ;; to avoid stealing traffic from Tailscale/WireGuard virtual interfaces. (do (logger/debug logger-tag "Tunnel interface detected on Windows, binding to specific interfaces only") @@ -305,8 +305,8 @@ "Direct LAN connections to " host-base " will not work. " "Use a different port, stop the conflicting service, or connect via Tailscale."))) (logger/info logger-tag (str "🌐 Remote server started on port " actual-port - (when ssl-context " (HTTPS)") - " — use /remote for connection details")) + (when ssl-context " (HTTPS)") + " — use /remote for connection details")) {:server jetty-server :sse-connections* sse-connections* :heartbeat-stop-ch heartbeat-ch