net/tsdial, ipn/localapi, client/local: let clients dial non-Tailscale addresses directly
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. Thanks to giacomo and Moyao for pointing this out! Updates tailscale/corp#39702 Change-Id: I78d640f11ccd92f43ddd505cbb0db8fee19f43a6 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
649781df84
commit
0e10a3f580
@@ -1169,16 +1169,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
|
||||
|
||||
@@ -500,3 +500,69 @@ func TestServeWithUnhealthyState(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDialSelf(t *testing.T) {
|
||||
h := handlerForTest(t, &Handler{
|
||||
PermitRead: true,
|
||||
PermitWrite: true,
|
||||
b: newTestLocalBackend(t),
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
port string
|
||||
wantSelf bool
|
||||
wantAddr string
|
||||
wantStatus int
|
||||
}{
|
||||
{
|
||||
name: "loopback_v4",
|
||||
host: "127.0.0.1",
|
||||
port: "8080",
|
||||
wantSelf: true,
|
||||
wantAddr: "127.0.0.1:8080",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "loopback_v6",
|
||||
host: "::1",
|
||||
port: "8080",
|
||||
wantSelf: true,
|
||||
wantAddr: "[::1]:8080",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "localhost",
|
||||
host: "localhost",
|
||||
port: "3000",
|
||||
wantSelf: true,
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "http://local-tailscaled.sock/localapi/v0/dial", nil)
|
||||
req.Header.Set("Connection", "upgrade")
|
||||
req.Header.Set("Upgrade", "ts-dial")
|
||||
req.Header.Set("Dial-Host", tt.host)
|
||||
req.Header.Set("Dial-Port", tt.port)
|
||||
resp := httptest.NewRecorder()
|
||||
h.serveDial(resp, req)
|
||||
|
||||
if resp.Code != tt.wantStatus {
|
||||
t.Fatalf("status = %d, want %d; body: %s", resp.Code, tt.wantStatus, resp.Body.String())
|
||||
}
|
||||
gotSelf := resp.Header().Get("Dial-Self") == "true"
|
||||
if gotSelf != tt.wantSelf {
|
||||
t.Errorf("Dial-Self = %v, want %v", gotSelf, tt.wantSelf)
|
||||
}
|
||||
if tt.wantAddr != "" {
|
||||
if got := resp.Header().Get("Dial-Addr"); got != tt.wantAddr {
|
||||
t.Errorf("Dial-Addr = %q, want %q", got, tt.wantAddr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user