Compare commits
2 commits
ff67bd6a71
...
05e70f127d
| Author | SHA1 | Date | |
|---|---|---|---|
| 05e70f127d | |||
| 7d6e339dcb |
7 changed files with 320 additions and 2 deletions
19
README.md
19
README.md
|
|
@ -153,6 +153,25 @@ The receiver already listens on all interfaces, so it's reachable at its Tailsca
|
||||||
IP with nothing else to configure. Sending and receiving both work, because the
|
IP with nothing else to configure. Sending and receiving both work, because the
|
||||||
probe is a two-way handshake (each side learns the other).
|
probe is a two-way handshake (each side learns the other).
|
||||||
|
|
||||||
|
#### Containers / userspace-networking tailscaled
|
||||||
|
|
||||||
|
When tailscaled runs with `--tun=userspace-networking` (typical in unprivileged
|
||||||
|
containers — no TUN device), processes cannot dial tailnet addresses directly;
|
||||||
|
outbound tailnet traffic only works through tailscaled's built-in SOCKS5 proxy.
|
||||||
|
omarchy-send handles this automatically: when the box has no tailnet address on
|
||||||
|
a local interface but a proxy answers at `localhost:1055`, connections to
|
||||||
|
`100.64.0.0/10` destinations are routed through it — LAN traffic is never
|
||||||
|
proxied, and explicit `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY` variables override
|
||||||
|
the auto-detection. The one thing the box must provide is the proxy itself:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tailscaled --tun=userspace-networking --socks5-server=localhost:1055 …
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that inbound connections on such boxes appear to come from `127.0.0.1`
|
||||||
|
(tailscaled re-dials loopback); omarchy-send keeps a peer's routable address
|
||||||
|
rather than letting those registers overwrite it.
|
||||||
|
|
||||||
#### Public-IP boxes: firewall the port
|
#### Public-IP boxes: firewall the port
|
||||||
|
|
||||||
The receiver binds **all interfaces**, so on a box with a public IP, port `53317`
|
The receiver binds **all interfaces**, so on a box with a public IP, port `53317`
|
||||||
|
|
|
||||||
45
install.sh
45
install.sh
|
|
@ -305,6 +305,16 @@ foreground TUI, not a background daemon. Start it with:
|
||||||
On a headless box, run it inside a TTY (tmux, or `ssh -t`). It listens on TCP
|
On a headless box, run it inside a TTY (tmux, or `ssh -t`). It listens on TCP
|
||||||
port **53317**. Auto-accept and an optional PIN live in the config / Settings tab.
|
port **53317**. Auto-accept and an optional PIN live in the config / Settings tab.
|
||||||
|
|
||||||
|
**You can SEND messages from the CLI** — no TUI, no TTY, works from scripts and
|
||||||
|
agents. To message another device (e.g. to notify the user on their desktop):
|
||||||
|
|
||||||
|
omarchy-send -to "<device alias>" -message "<text>"
|
||||||
|
|
||||||
|
Add `-send-pin <pin>` if the target requires a PIN, and `-wait 30s` to allow
|
||||||
|
longer for discovery. Works over the LAN and Tailscale alike; exit code 0 means
|
||||||
|
delivered. To send files, pass paths instead (`omarchy-send <file>…`) — that
|
||||||
|
opens the TUI with them staged, so it needs a TTY.
|
||||||
|
|
||||||
**Config:** `~/.config/omarchy-send/config.json`
|
**Config:** `~/.config/omarchy-send/config.json`
|
||||||
(keys: `alias`, `receiveDir`, `port`, `autoAccept`, `pin`, `knownPeers`, …).
|
(keys: `alias`, `receiveDir`, `port`, `autoAccept`, `pin`, `knownPeers`, …).
|
||||||
|
|
||||||
|
|
@ -334,7 +344,10 @@ land in `@@RECV_DIR@@`** (authoritative: the `receiveDir` key in
|
||||||
`~/.config/omarchy-send/config.json`). Files still transferring carry a `.part`
|
`~/.config/omarchy-send/config.json`). Files still transferring carry a `.part`
|
||||||
suffix — skip them. Text messages appear in the TUI's Messages tab, not on disk.
|
suffix — skip them. Text messages appear in the TUI's Messages tab, not on disk.
|
||||||
Receiving requires the TUI running (`omarchy-send`; use tmux or `ssh -t` when
|
Receiving requires the TUI running (`omarchy-send`; use tmux or `ssh -t` when
|
||||||
headless). Full notes: `~/.config/omarchy-send/AGENTS.md`.
|
headless). **You can SEND a message to another device from the CLI** (no TUI/TTY,
|
||||||
|
fine for scripts and agents): `omarchy-send -to "<alias>" -message "<text>"`
|
||||||
|
(plus `-send-pin <pin>` if the target requires one); exit 0 = delivered. Works
|
||||||
|
over LAN and Tailscale. Full notes: `~/.config/omarchy-send/AGENTS.md`.
|
||||||
<!-- END omarchy-send (managed by installer) -->
|
<!-- END omarchy-send (managed by installer) -->
|
||||||
BLK
|
BLK
|
||||||
sed -i "s|@@RECV_DIR@@|$RECV_DIR|g" "$blk"
|
sed -i "s|@@RECV_DIR@@|$RECV_DIR|g" "$blk"
|
||||||
|
|
@ -453,6 +466,36 @@ if [ "$MODE" != "remote" ] && [ -n "$PUBLIC_IP" ]; then
|
||||||
echo " curl -sk https://<public-ip>:$PORT/api/localsend/v2/info # should time out"
|
echo " curl -sk https://<public-ip>:$PORT/api/localsend/v2/info # should time out"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---- userspace-networking Tailscale: outbound proxy check -----------------
|
||||||
|
# Under tailscaled --tun=userspace-networking (no TUN device — the default in
|
||||||
|
# unprivileged containers), processes cannot dial OUT to tailnet addresses at
|
||||||
|
# all; tailscaled's SOCKS5 proxy is the only outbound path. omarchy-send
|
||||||
|
# auto-detects and uses it at localhost:1055 — so check it's there and say
|
||||||
|
# exactly what to do when it isn't (receive works either way; send doesn't).
|
||||||
|
if command -v tailscale >/dev/null 2>&1 && [ -z "$TS_IFACE" ] \
|
||||||
|
&& tailscale status >/dev/null 2>&1; then
|
||||||
|
if (exec 3<>/dev/tcp/127.0.0.1/1055) 2>/dev/null; then
|
||||||
|
exec 3>&- || true
|
||||||
|
echo
|
||||||
|
echo "==> Userspace-networking Tailscale detected; SOCKS5 proxy found at"
|
||||||
|
echo " localhost:1055 — sends to tailnet devices will route through it"
|
||||||
|
echo " automatically. Nothing to do."
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo "⚠ Tailscale is running in userspace-networking mode (no TUN interface)"
|
||||||
|
echo " and no SOCKS5 proxy is listening on localhost:1055. This box can"
|
||||||
|
echo " RECEIVE over the tailnet, but CANNOT SEND to tailnet devices until"
|
||||||
|
echo " tailscaled is restarted with its proxy enabled — add this flag to"
|
||||||
|
echo " however tailscaled is launched (entrypoint, unit, …), keeping the"
|
||||||
|
echo " existing --state/--socket flags:"
|
||||||
|
echo
|
||||||
|
echo " tailscaled --tun=userspace-networking --socks5-server=localhost:1055 …"
|
||||||
|
echo
|
||||||
|
echo " omarchy-send then uses the proxy automatically — no env vars needed."
|
||||||
|
echo " (Explicit HTTPS_PROXY/HTTP_PROXY/NO_PROXY are honoured as overrides.)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":$BIN_DIR:"*) : ;;
|
*":$BIN_DIR:"*) : ;;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"omarchy-send/internal/discovery"
|
"omarchy-send/internal/discovery"
|
||||||
"omarchy-send/internal/protocol"
|
"omarchy-send/internal/protocol"
|
||||||
"omarchy-send/internal/transfer"
|
"omarchy-send/internal/transfer"
|
||||||
|
"omarchy-send/internal/tsproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// errOpen wraps a failure to open a source file, so the send loop can skip just
|
// errOpen wraps a failure to open a source file, so the send loop can skip just
|
||||||
|
|
@ -60,6 +61,10 @@ func New(self protocol.DeviceInfo) *Sender {
|
||||||
// and waiting for response headers after the body is sent.
|
// and waiting for response headers after the body is sent.
|
||||||
Timeout: 0,
|
Timeout: 0,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
|
// Proxy env vars + tailnet SOCKS5 auto-detection (see
|
||||||
|
// discovery) so transfers also work from userspace-networking
|
||||||
|
// Tailscale boxes.
|
||||||
|
Proxy: tsproxy.ProxyFunc,
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
DialContext: (&net.Dialer{Timeout: 10 * time.Second}).DialContext,
|
DialContext: (&net.Dialer{Timeout: 10 * time.Second}).DialContext,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
|
|
||||||
"omarchy-send/internal/dbg"
|
"omarchy-send/internal/dbg"
|
||||||
"omarchy-send/internal/protocol"
|
"omarchy-send/internal/protocol"
|
||||||
|
"omarchy-send/internal/tsproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventKind distinguishes peer lifecycle events.
|
// EventKind distinguishes peer lifecycle events.
|
||||||
|
|
@ -73,6 +74,11 @@ func New(self protocol.DeviceInfo) *Discoverer {
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 3 * time.Second,
|
Timeout: 3 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
|
// Proxy env vars are honoured like the default transport, and
|
||||||
|
// tailnet destinations are auto-routed through the local
|
||||||
|
// tailscaled SOCKS5 proxy on userspace-networking boxes (no
|
||||||
|
// TUN), where direct outbound tailnet dials cannot work.
|
||||||
|
Proxy: tsproxy.ProxyFunc,
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -338,6 +344,12 @@ func (d *Discoverer) Probe(ctx context.Context, host string) error {
|
||||||
return lastErr
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isLoopback reports whether ip parses as a loopback address (e.g. 127.0.0.1).
|
||||||
|
func isLoopback(ip string) bool {
|
||||||
|
p := net.ParseIP(ip)
|
||||||
|
return p != nil && p.IsLoopback()
|
||||||
|
}
|
||||||
|
|
||||||
// hostPort splits an optional :port off host, defaulting to the LocalSend port.
|
// hostPort splits an optional :port off host, defaulting to the LocalSend port.
|
||||||
// It handles bare IPv6 by requiring the [::]:port form for a custom port.
|
// It handles bare IPv6 by requiring the [::]:port form for a custom port.
|
||||||
func hostPort(host string) (string, int) {
|
func hostPort(host string) (string, int) {
|
||||||
|
|
@ -361,7 +373,13 @@ func (d *Discoverer) NotePeer(info protocol.DeviceInfo, ip string) {
|
||||||
|
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
prev, existed := d.peers[info.Fingerprint]
|
prev, existed := d.peers[info.Fingerprint]
|
||||||
changed := !existed || prev.IP != ip || prev.Info.Alias != info.Alias
|
// Never downgrade a routable address to loopback: behind a
|
||||||
|
// userspace-networking tailscaled, inbound registers all appear to come
|
||||||
|
// from 127.0.0.1 — recording that would make us "reply" to ourselves.
|
||||||
|
if existed && isLoopback(ip) && !isLoopback(prev.IP) {
|
||||||
|
peer.IP = prev.IP
|
||||||
|
}
|
||||||
|
changed := !existed || prev.IP != peer.IP || prev.Info.Alias != info.Alias
|
||||||
d.peers[info.Fingerprint] = peer
|
d.peers[info.Fingerprint] = peer
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,32 @@ func TestProbeUnreachableErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A register arriving via a userspace-networking tailscaled proxy appears to
|
||||||
|
// come from 127.0.0.1; that must not overwrite a peer's known routable address
|
||||||
|
// (sending to it would loop back to our own receiver).
|
||||||
|
func TestNotePeerKeepsRoutableOverLoopback(t *testing.T) {
|
||||||
|
d := New(protocol.DeviceInfo{Fingerprint: "self-fp"})
|
||||||
|
info := protocol.DeviceInfo{Alias: "gav", Fingerprint: "gav-fp", Port: 53317}
|
||||||
|
|
||||||
|
d.NotePeer(info, "100.91.41.111")
|
||||||
|
d.NotePeer(info, "127.0.0.1") // inbound register through the local proxy
|
||||||
|
if got := d.Snapshot()[0].IP; got != "100.91.41.111" {
|
||||||
|
t.Errorf("IP downgraded to %q, want 100.91.41.111 kept", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A first sight at loopback is still recorded (nothing better known)…
|
||||||
|
d2 := New(protocol.DeviceInfo{Fingerprint: "self-fp"})
|
||||||
|
d2.NotePeer(info, "127.0.0.1")
|
||||||
|
if got := d2.Snapshot()[0].IP; got != "127.0.0.1" {
|
||||||
|
t.Errorf("first-sight IP = %q, want 127.0.0.1", got)
|
||||||
|
}
|
||||||
|
// …and upgrades to the routable address as soon as one is learned.
|
||||||
|
d2.NotePeer(info, "100.91.41.111")
|
||||||
|
if got := d2.Snapshot()[0].IP; got != "100.91.41.111" {
|
||||||
|
t.Errorf("IP = %q, want upgrade to 100.91.41.111", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHostPortDefaults(t *testing.T) {
|
func TestHostPortDefaults(t *testing.T) {
|
||||||
if h, p := hostPort("colossus"); h != "colossus" || p != protocol.DefaultPort {
|
if h, p := hostPort("colossus"); h != "colossus" || p != protocol.DefaultPort {
|
||||||
t.Errorf("hostPort(bare) = %q,%d; want colossus,%d", h, p, protocol.DefaultPort)
|
t.Errorf("hostPort(bare) = %q,%d; want colossus,%d", h, p, protocol.DefaultPort)
|
||||||
|
|
|
||||||
111
internal/tsproxy/tsproxy.go
Normal file
111
internal/tsproxy/tsproxy.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
// Package tsproxy routes tailnet-bound HTTP connections through the local
|
||||||
|
// tailscaled SOCKS5 proxy when — and only when — the box needs it. On a box
|
||||||
|
// whose tailscaled runs with --tun=userspace-networking (e.g. an unprivileged
|
||||||
|
// container), there is no TUN interface, so ordinary outbound dials to
|
||||||
|
// 100.64.0.0/10 addresses cannot route; tailscaled's --socks5-server is the
|
||||||
|
// only outbound path. On a normal box the tailnet address is a local interface
|
||||||
|
// address and no proxy is involved.
|
||||||
|
package tsproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"omarchy-send/internal/dbg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conventionalAddr is where tailscaled's SOCKS5 proxy conventionally listens
|
||||||
|
// (--socks5-server=localhost:1055, the address used throughout Tailscale's
|
||||||
|
// userspace-networking docs). An explicit HTTPS_PROXY/HTTP_PROXY overrides it.
|
||||||
|
const conventionalAddr = "127.0.0.1:1055"
|
||||||
|
|
||||||
|
// detectTTL bounds how long a detection result is trusted, so a tailscaled
|
||||||
|
// (re)started after us is picked up without restarting omarchy-send.
|
||||||
|
const detectTTL = 30 * time.Second
|
||||||
|
|
||||||
|
// tailnetCIDR is Tailscale's CGNAT range; every tailnet IPv4 falls in it.
|
||||||
|
var tailnetCIDR = func() *net.IPNet {
|
||||||
|
_, n, _ := net.ParseCIDR("100.64.0.0/10")
|
||||||
|
return n
|
||||||
|
}()
|
||||||
|
|
||||||
|
// envProxy resolves the proxy environment variables; a seam for tests (the
|
||||||
|
// stdlib caches the env process-wide on first use).
|
||||||
|
var envProxy = http.ProxyFromEnvironment
|
||||||
|
|
||||||
|
// ProxyFunc is an http.Transport.Proxy implementation. Explicit proxy
|
||||||
|
// environment variables (HTTPS_PROXY/HTTP_PROXY/NO_PROXY) always win, like the
|
||||||
|
// default transport; otherwise requests to tailnet addresses are routed via
|
||||||
|
// the local tailscaled SOCKS5 proxy when the box can't dial them directly.
|
||||||
|
func ProxyFunc(req *http.Request) (*url.URL, error) {
|
||||||
|
if u, err := envProxy(req); err != nil || u != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
if isTailnetHost(req.URL.Hostname()) {
|
||||||
|
return detect(), nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTailnetHost reports whether host is an IP in the Tailscale CGNAT range.
|
||||||
|
func isTailnetHost(host string) bool {
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
return ip != nil && tailnetCIDR.Contains(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
checked time.Time
|
||||||
|
cached *url.URL // non-nil when tailnet dials must go through the proxy
|
||||||
|
)
|
||||||
|
|
||||||
|
// detect decides (with a short cache) whether tailnet destinations need the
|
||||||
|
// local SOCKS5 proxy: only when no local interface carries a tailnet address
|
||||||
|
// (i.e. userspace networking — a TUN box can dial directly) AND something is
|
||||||
|
// listening at the conventional proxy address.
|
||||||
|
func detect() *url.URL {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if time.Since(checked) < detectTTL {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
checked = time.Now()
|
||||||
|
prev := cached
|
||||||
|
cached = nil
|
||||||
|
if !hasLocalTailnetAddr() && listening(conventionalAddr) {
|
||||||
|
cached = &url.URL{Scheme: "socks5", Host: conventionalAddr}
|
||||||
|
}
|
||||||
|
if (cached == nil) != (prev == nil) {
|
||||||
|
dbg.Logf("tsproxy: tailnet SOCKS5 proxy at %s: %v", conventionalAddr, cached != nil)
|
||||||
|
}
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasLocalTailnetAddr reports whether any local interface has a tailnet
|
||||||
|
// address — true on kernel-TUN tailscale boxes, false under userspace
|
||||||
|
// networking (the box's own tailnet IP is not a local address there).
|
||||||
|
func hasLocalTailnetAddr() bool {
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, a := range addrs {
|
||||||
|
if ipn, ok := a.(*net.IPNet); ok && tailnetCIDR.Contains(ipn.IP) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// listening reports whether a TCP listener answers at addr.
|
||||||
|
func listening(addr string) bool {
|
||||||
|
c, err := net.DialTimeout("tcp", addr, 300*time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_ = c.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
96
internal/tsproxy/tsproxy_test.go
Normal file
96
internal/tsproxy/tsproxy_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue