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:
Brad Fitzpatrick
2026-04-06 23:45:17 +00:00
committed by Brad Fitzpatrick
parent 649781df84
commit 0e10a3f580
6 changed files with 276 additions and 4 deletions
+13
View File
@@ -972,6 +972,19 @@ func (lc *Client) UserDial(ctx context.Context, network, host string, port uint1
if res.StatusCode != http.StatusSwitchingProtocols {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode == http.StatusOK && res.Header.Get("Dial-Self") == "true" {
// Server told us to dial the address ourselves rather than
// proxying through the daemon. This happens for non-Tailscale
// addresses where the daemon shouldn't dial as root on the
// client's behalf. The server provides the resolved address
// to avoid a TOCTOU race with DNS re-resolution.
addr := res.Header.Get("Dial-Addr")
if addr == "" {
return nil, errors.New("server returned Dial-Self without Dial-Addr")
}
var d net.Dialer
return d.DialContext(ctx, network, addr)
}
return nil, fmt.Errorf("unexpected HTTP response: %s, %s", res.Status, body)
}
// From here on, the underlying net.Conn is ours to use, but there
+51
View File
@@ -61,6 +61,57 @@ func TestWhoIsPeerNotFound(t *testing.T) {
}
}
func TestUserDialSelf(t *testing.T) {
// Start a real TCP listener that the client should dial directly
// when the server tells it to dial-self.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
c.Write([]byte("hello"))
c.Close()
}
}()
targetAddr := ln.Addr().(*net.TCPAddr)
// Mock LocalAPI server that returns Dial-Self response.
nw := nettest.GetNetwork(t)
ts := nettest.NewHTTPServer(nw, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Dial-Self", "true")
w.Header().Set("Dial-Addr", targetAddr.String())
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
lc := &Client{
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
return nw.Dial(ctx, network, ts.Listener.Addr().String())
},
}
conn, err := lc.UserDial(context.Background(), "tcp", targetAddr.IP.String(), uint16(targetAddr.Port))
if err != nil {
t.Fatalf("UserDial: %v", err)
}
defer conn.Close()
buf := make([]byte, 5)
n, err := conn.Read(buf)
if err != nil {
t.Fatalf("Read: %v", err)
}
if got := string(buf[:n]); got != "hello" {
t.Errorf("got %q, want %q", got, "hello")
}
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
BadDeps: map[string]string{