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
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user