summaryrefslogtreecommitdiffhomepage
path: root/ipn/localapi/localapi.go
diff options
context:
space:
mode:
authorBrad Fitzpatrick <bradfitz@tailscale.com>2026-04-06 23:45:17 +0000
committerBrad Fitzpatrick <bradfitz@tailscale.com>2026-04-06 23:45:17 +0000
commit07869784472492e9420632a8c10a86bbdf888b5f (patch)
tree9ce1dc200c51781c44446906b1c1cbc957533a9f /ipn/localapi/localapi.go
parentd0cd0906d5d40567d10aebfab1b8ebe14ca48f64 (diff)
downloadtailscale-bradfitz/dial_local.tar.xz
tailscale-bradfitz/dial_local.zip
net/tsdial, ipn/localapi, client/local: let clients dial non-Tailscale addresses directlybradfitz/dial_local
Add a tsdial.Dialer.UserDialPlan method that resolves an address and reports whether the dialer would route it via Tailscale. The LocalAPI /dial handler now uses this to skip proxying for addresses that aren't Tailscale routes (e.g. localhost), returning a Dial-Self response with the resolved address so the client can dial it directly. This avoids an unnecessary round-trip through the daemon for local connections. The client's UserDial handles the new response by dialing the resolved address itself, and the server passes the pre-resolved IP:port for Tailscale dials to avoid redundant DNS lookups. Updates tailscale/corp#39702 Change-Id: I78d640f11ccd92f43ddd505cbb0db8fee19f43a6 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Diffstat (limited to 'ipn/localapi/localapi.go')
-rw-r--r--ipn/localapi/localapi.go26
1 files changed, 22 insertions, 4 deletions
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index 5eec66e64..c5ae3f846 100644
--- a/ipn/localapi/localapi.go
+++ b/ipn/localapi/localapi.go
@@ -1168,16 +1168,34 @@ func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
http.Error(w, "missing Dial-Host or Dial-Port header", http.StatusBadRequest)
return
}
+ network := cmp.Or(r.Header.Get("Dial-Network"), "tcp")
+
+ addr := net.JoinHostPort(hostStr, portStr)
+
+ // Check whether the resolved address is a Tailscale route.
+ // If not, tell the client to dial it directly so the connection
+ // comes from the calling user's UID rather than our root-owned daemon.
+ ipp, viaTailscale, err := h.b.Dialer().UserDialPlan(r.Context(), network, addr)
+ if err != nil {
+ http.Error(w, "resolve failure: "+err.Error(), http.StatusBadGateway)
+ return
+ }
+ if !viaTailscale {
+ w.Header().Set("Dial-Self", "true")
+ w.Header().Set("Dial-Addr", ipp.String())
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
return
}
- network := cmp.Or(r.Header.Get("Dial-Network"), "tcp")
-
- addr := net.JoinHostPort(hostStr, portStr)
- outConn, err := h.b.Dialer().UserDial(r.Context(), network, addr)
+ // Dial via Tailscale using the resolved IP:port to avoid a TOCTOU
+ // race with DNS re-resolution.
+ outConn, err := h.b.Dialer().UserDial(r.Context(), network, ipp.String())
if err != nil {
http.Error(w, "dial failure: "+err.Error(), http.StatusBadGateway)
return