Under tailscaled --tun=userspace-networking (no TUN device — the default in unprivileged containers), processes cannot dial tailnet addresses at all: inbound connections are proxied to loopback by tailscaled, but outbound 100.64.0.0/10 dials have no route. omarchy-send could receive on such boxes but never send, probe, or discover over the tailnet. New internal/tsproxy auto-detects this: when no local interface carries a tailnet address but a SOCKS5 proxy answers at localhost:1055 (the conventional --socks5-server address), tailnet-bound connections route through it. LAN traffic is never proxied; explicit HTTPS_PROXY/ HTTP_PROXY/NO_PROXY win over detection, restoring the env-var support the custom transports had silently dropped. Also guard NotePeer against the loopback trap the same proxying causes: inbound registers all appear to come from 127.0.0.1, and recording that over a peer's routable address made a TUI "send" loop back to our own receiver while looking delivered. Live-verified from an omaterm container (userspace tailscaled): headless send discovered and messaged a tailnet peer with no env vars set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
package tsproxy
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestIsTailnetHost(t *testing.T) {
|
|
cases := []struct {
|
|
host string
|
|
want bool
|
|
}{
|
|
{"100.90.62.102", true}, // tailnet (CGNAT range)
|
|
{"100.64.0.1", true}, // range start
|
|
{"100.127.255.254", true} /* range end */, {"100.128.0.1", false}, // just past the /10
|
|
{"192.168.1.46", false}, // LAN
|
|
{"127.0.0.1", false}, // loopback
|
|
{"colossus", false}, // hostname, not an IP
|
|
{"", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := isTailnetHost(c.host); got != c.want {
|
|
t.Errorf("isTailnetHost(%q) = %v, want %v", c.host, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// stubEnvProxy replaces the env-var proxy lookup for a test. The stdlib's
|
|
// ProxyFromEnvironment caches the environment process-wide on first use, so
|
|
// t.Setenv can't drive these tests reliably.
|
|
func stubEnvProxy(t *testing.T, u *url.URL) {
|
|
t.Helper()
|
|
orig := envProxy
|
|
envProxy = func(*http.Request) (*url.URL, error) { return u, nil }
|
|
t.Cleanup(func() { envProxy = orig })
|
|
}
|
|
|
|
// Explicit proxy env vars must win over auto-detection, like the default
|
|
// transport.
|
|
func TestProxyFuncEnvOverride(t *testing.T) {
|
|
stubEnvProxy(t, &url.URL{Scheme: "socks5", Host: "127.0.0.1:9999"})
|
|
prime(&url.URL{Scheme: "socks5", Host: conventionalAddr}) // detection would say 1055
|
|
req, _ := http.NewRequest(http.MethodGet, "https://100.90.62.102:53317/info", nil)
|
|
u, err := ProxyFunc(req)
|
|
if err != nil {
|
|
t.Fatalf("ProxyFunc: %v", err)
|
|
}
|
|
if u == nil || u.Host != "127.0.0.1:9999" {
|
|
t.Errorf("proxy = %v, want explicit socks5://127.0.0.1:9999", u)
|
|
}
|
|
}
|
|
|
|
// Non-tailnet destinations never get the auto-detected proxy.
|
|
func TestProxyFuncNonTailnetDirect(t *testing.T) {
|
|
stubEnvProxy(t, nil)
|
|
prime(&url.URL{Scheme: "socks5", Host: conventionalAddr})
|
|
req, _ := http.NewRequest(http.MethodGet, "https://192.168.1.46:53317/info", nil)
|
|
u, err := ProxyFunc(req)
|
|
if err != nil {
|
|
t.Fatalf("ProxyFunc: %v", err)
|
|
}
|
|
if u != nil {
|
|
t.Errorf("proxy = %v, want nil (direct) for a LAN destination", u)
|
|
}
|
|
}
|
|
|
|
// A tailnet destination uses the cached detection result; with the cache
|
|
// primed to "proxy present" the URL comes back, and direct otherwise.
|
|
func TestProxyFuncTailnetUsesDetection(t *testing.T) {
|
|
stubEnvProxy(t, nil)
|
|
req, _ := http.NewRequest(http.MethodGet, "https://100.90.62.102:53317/info", nil)
|
|
|
|
prime(&url.URL{Scheme: "socks5", Host: conventionalAddr})
|
|
u, err := ProxyFunc(req)
|
|
if err != nil {
|
|
t.Fatalf("ProxyFunc: %v", err)
|
|
}
|
|
if u == nil || u.Scheme != "socks5" || u.Host != conventionalAddr {
|
|
t.Errorf("proxy = %v, want socks5://%s", u, conventionalAddr)
|
|
}
|
|
|
|
prime(nil)
|
|
if u, _ := ProxyFunc(req); u != nil {
|
|
t.Errorf("proxy = %v, want nil when no proxy detected", u)
|
|
}
|
|
}
|
|
|
|
// prime seeds the detection cache for tests.
|
|
func prime(u *url.URL) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
checked = time.Now()
|
|
cached = u
|
|
}
|