2
0
Эх сурвалжийг харах

ipn/localapi,client/tailscale,cmd/derper: add WhoIs lookup by nodekey, use in derper

Fixes #12465

Change-Id: I9b7c87315a3d2b2ecae2b8db9e94b4f5a1eef74a
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 1 жил өмнө
parent
commit
6908fb0de3

+ 28 - 1
client/tailscale/localclient.go

@@ -253,11 +253,16 @@ func (lc *LocalClient) sendWithHeaders(
 	}
 	if res.StatusCode != wantStatus {
 		err = fmt.Errorf("%v: %s", res.Status, bytes.TrimSpace(slurp))
-		return nil, nil, bestError(err, slurp)
+		return nil, nil, httpStatusError{bestError(err, slurp), res.StatusCode}
 	}
 	return slurp, res.Header, nil
 }
 
+type httpStatusError struct {
+	error
+	HTTPStatus int
+}
+
 func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
 	return lc.send(ctx, "GET", path, 200, nil)
 }
@@ -278,9 +283,31 @@ func decodeJSON[T any](b []byte) (ret T, err error) {
 }
 
 // WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
+//
+// If not found, the error is ErrPeerNotFound.
 func (lc *LocalClient) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
 	body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
 	if err != nil {
+		if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
+			return nil, ErrPeerNotFound
+		}
+		return nil, err
+	}
+	return decodeJSON[*apitype.WhoIsResponse](body)
+}
+
+// ErrPeerNotFound is returned by WhoIs and WhoIsNodeKey when a peer is not found.
+var ErrPeerNotFound = errors.New("peer not found")
+
+// WhoIsNodeKey returns the owner of the given wireguard public key.
+//
+// If not found, the error is ErrPeerNotFound.
+func (lc *LocalClient) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*apitype.WhoIsResponse, error) {
+	body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(key.String()))
+	if err != nil {
+		if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
+			return nil, ErrPeerNotFound
+		}
 		return nil, err
 	}
 	return decodeJSON[*apitype.WhoIsResponse](body)

+ 31 - 0
client/tailscale/localclient_test.go

@@ -6,9 +6,14 @@
 package tailscale
 
 import (
+	"context"
+	"net"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
 	"tailscale.com/tstest/deptest"
+	"tailscale.com/types/key"
 )
 
 func TestGetServeConfigFromJSON(t *testing.T) {
@@ -30,6 +35,32 @@ func TestGetServeConfigFromJSON(t *testing.T) {
 	}
 }
 
+func TestWhoIsPeerNotFound(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(404)
+	}))
+	defer ts.Close()
+
+	lc := &LocalClient{
+		Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
+			var std net.Dialer
+			return std.DialContext(ctx, network, ts.Listener.Addr().(*net.TCPAddr).String())
+		},
+	}
+	var k key.NodePublic
+	if err := k.UnmarshalText([]byte("nodekey:5c8f86d5fc70d924e55f02446165a5dae8f822994ad26bcf4b08fd841f9bf261")); err != nil {
+		t.Fatal(err)
+	}
+	res, err := lc.WhoIsNodeKey(context.Background(), k)
+	if err != ErrPeerNotFound {
+		t.Errorf("got (%v, %v), want ErrPeerNotFound", res, err)
+	}
+	res, err = lc.WhoIs(context.Background(), "1.2.3.4:5678")
+	if err != ErrPeerNotFound {
+		t.Errorf("got (%v, %v), want ErrPeerNotFound", res, err)
+	}
+}
+
 func TestDeps(t *testing.T) {
 	deptest.DepChecker{
 		BadDeps: map[string]string{

+ 7 - 8
derp/derp_server.go

@@ -1151,20 +1151,19 @@ func (c *sclient) requestMeshUpdate() {
 	}
 }
 
+var localClient tailscale.LocalClient
+
 // verifyClient checks whether the client is allowed to connect to the derper,
 // depending on how & whether the server's been configured to verify.
 func (s *Server) verifyClient(ctx context.Context, clientKey key.NodePublic, info *clientInfo, clientIP netip.Addr) error {
 	// tailscaled-based verification:
 	if s.verifyClientsLocalTailscaled {
-		status, err := tailscale.Status(ctx)
-		if err != nil {
-			return fmt.Errorf("failed to query local tailscaled status: %w", err)
+		_, err := localClient.WhoIsNodeKey(ctx, clientKey)
+		if err == tailscale.ErrPeerNotFound {
+			return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
 		}
-		if clientKey == status.Self.PublicKey {
-			return nil
-		}
-		if _, exists := status.Peer[clientKey]; !exists {
-			return fmt.Errorf("client %v not in set of peers", clientKey)
+		if err != nil {
+			return fmt.Errorf("failed to query local tailscaled status for %v: %w", clientKey, err)
 		}
 	}
 

+ 20 - 0
ipn/ipnlocal/local.go

@@ -966,6 +966,26 @@ func peerStatusFromNode(ps *ipnstate.PeerStatus, n tailcfg.NodeView) {
 	}
 }
 
+// WhoIsNodeKey returns the peer info of given public key, if it exists.
+func (b *LocalBackend) WhoIsNodeKey(k key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+	// TODO(bradfitz): add nodeByKey like nodeByAddr instead of walking peers.
+	if b.netMap == nil {
+		return n, u, false
+	}
+	if self := b.netMap.SelfNode; self.Valid() && self.Key() == k {
+		return self, b.netMap.UserProfiles[self.User()], true
+	}
+	for _, n := range b.peers {
+		if n.Key() == k {
+			u, ok = b.netMap.UserProfiles[n.User()]
+			return n, u, ok
+		}
+	}
+	return n, u, false
+}
+
 // WhoIs reports the node and user who owns the node with the given IP:port.
 // If the IP address is a Tailscale IP, the provided port may be 0.
 // If ok == true, n and u are valid.

+ 17 - 2
ipn/localapi/localapi.go

@@ -448,6 +448,7 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
 // by the localapi WhoIs method.
 type localBackendWhoIsMethods interface {
 	WhoIs(netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
+	WhoIsNodeKey(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
 	PeerCaps(netip.Addr) tailcfg.PeerCapMap
 }
 
@@ -456,9 +457,21 @@ func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request,
 		http.Error(w, "whois access denied", http.StatusForbidden)
 		return
 	}
+	var (
+		n  tailcfg.NodeView
+		u  tailcfg.UserProfile
+		ok bool
+	)
 	var ipp netip.AddrPort
 	if v := r.FormValue("addr"); v != "" {
-		if ip, err := netip.ParseAddr(v); err == nil {
+		if strings.HasPrefix(v, "nodekey:") {
+			var k key.NodePublic
+			if err := k.UnmarshalText([]byte(v)); err != nil {
+				http.Error(w, "invalid nodekey in 'addr' parameter", http.StatusBadRequest)
+				return
+			}
+			n, u, ok = b.WhoIsNodeKey(k)
+		} else if ip, err := netip.ParseAddr(v); err == nil {
 			ipp = netip.AddrPortFrom(ip, 0)
 		} else {
 			var err error
@@ -468,11 +481,13 @@ func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request,
 				return
 			}
 		}
+		if ipp.IsValid() {
+			n, u, ok = b.WhoIs(ipp)
+		}
 	} else {
 		http.Error(w, "missing 'addr' parameter", http.StatusBadRequest)
 		return
 	}
-	n, u, ok := b.WhoIs(ipp)
 	if !ok {
 		http.Error(w, "no match for IP:port", http.StatusNotFound)
 		return

+ 38 - 14
ipn/localapi/localapi_test.go

@@ -31,6 +31,7 @@ import (
 	"tailscale.com/tailcfg"
 	"tailscale.com/tsd"
 	"tailscale.com/tstest"
+	"tailscale.com/types/key"
 	"tailscale.com/types/logger"
 	"tailscale.com/types/logid"
 	"tailscale.com/util/slicesx"
@@ -99,26 +100,46 @@ func TestSetPushDeviceToken(t *testing.T) {
 }
 
 type whoIsBackend struct {
-	whoIs    func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
-	peerCaps map[netip.Addr]tailcfg.PeerCapMap
+	whoIs        func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
+	whoIsNodeKey func(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
+	peerCaps     map[netip.Addr]tailcfg.PeerCapMap
 }
 
 func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
 	return b.whoIs(ipp)
 }
 
+func (b whoIsBackend) WhoIsNodeKey(k key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
+	return b.whoIsNodeKey(k)
+}
+
 func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
 	return b.peerCaps[ip]
 }
 
-// Tests that the WhoIs handler accepts either IPs or IP:ports.
+// Tests that the WhoIs handler accepts IPs, IP:ports, or nodekeys.
 //
 // From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
-func TestWhoIsJustIP(t *testing.T) {
+//
+// And https://github.com/tailscale/tailscale/issues/12465
+func TestWhoIsArgTypes(t *testing.T) {
 	h := &Handler{
 		PermitRead: true,
 	}
-	for _, input := range []string{"100.101.102.103", "127.0.0.1:123"} {
+
+	match := func() (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
+		return (&tailcfg.Node{
+				ID: 123,
+				Addresses: []netip.Prefix{
+					netip.MustParsePrefix("100.101.102.103/32"),
+				},
+			}).View(),
+			tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
+			true
+	}
+
+	const keyStr = "nodekey:5c8f86d5fc70d924e55f02446165a5dae8f822994ad26bcf4b08fd841f9bf261"
+	for _, input := range []string{"100.101.102.103", "127.0.0.1:123", keyStr} {
 		rec := httptest.NewRecorder()
 		t.Run(input, func(t *testing.T) {
 			b := whoIsBackend{
@@ -129,14 +150,14 @@ func TestWhoIsJustIP(t *testing.T) {
 							t.Fatalf("backend called with %v; want %v", ipp, want)
 						}
 					}
-					return (&tailcfg.Node{
-							ID: 123,
-							Addresses: []netip.Prefix{
-								netip.MustParsePrefix("100.101.102.103/32"),
-							},
-						}).View(),
-						tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
-						true
+					return match()
+				},
+				whoIsNodeKey: func(k key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
+					if k.String() != keyStr {
+						t.Fatalf("backend called with %v; want %v", k, keyStr)
+					}
+					return match()
+
 				},
 				peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
 					netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
@@ -146,9 +167,12 @@ func TestWhoIsJustIP(t *testing.T) {
 			}
 			h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
 
+			if rec.Code != 200 {
+				t.Fatalf("response code %d", rec.Code)
+			}
 			var res apitype.WhoIsResponse
 			if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
-				t.Fatal(err)
+				t.Fatalf("parsing response %#q: %v", rec.Body.Bytes(), err)
 			}
 			if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
 				t.Errorf("res.Node.ID=%v, want %v", got, want)