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

Fixes #12465

Change-Id: I9b7c87315a3d2b2ecae2b8db9e94b4f5a1eef74a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
dependabot/npm_and_yarn/cmd/tsconnect/braces-3.0.3
Brad Fitzpatrick 5 months ago committed by Brad Fitzpatrick
parent 72c8f7700b
commit 6908fb0de3

@ -253,11 +253,16 @@ func (lc *LocalClient) sendWithHeaders(
} }
if res.StatusCode != wantStatus { if res.StatusCode != wantStatus {
err = fmt.Errorf("%v: %s", res.Status, bytes.TrimSpace(slurp)) 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 return slurp, res.Header, nil
} }
type httpStatusError struct {
error
HTTPStatus int
}
func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) { func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
return lc.send(ctx, "GET", path, 200, nil) 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. // 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) { func (lc *LocalClient) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr)) body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
if err != nil { 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 nil, err
} }
return decodeJSON[*apitype.WhoIsResponse](body) return decodeJSON[*apitype.WhoIsResponse](body)

@ -6,9 +6,14 @@
package tailscale package tailscale
import ( import (
"context"
"net"
"net/http"
"net/http/httptest"
"testing" "testing"
"tailscale.com/tstest/deptest" "tailscale.com/tstest/deptest"
"tailscale.com/types/key"
) )
func TestGetServeConfigFromJSON(t *testing.T) { 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) { func TestDeps(t *testing.T) {
deptest.DepChecker{ deptest.DepChecker{
BadDeps: map[string]string{ BadDeps: map[string]string{

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

@ -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. // 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 the IP address is a Tailscale IP, the provided port may be 0.
// If ok == true, n and u are valid. // If ok == true, n and u are valid.

@ -448,6 +448,7 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
// by the localapi WhoIs method. // by the localapi WhoIs method.
type localBackendWhoIsMethods interface { type localBackendWhoIsMethods interface {
WhoIs(netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) 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 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) http.Error(w, "whois access denied", http.StatusForbidden)
return return
} }
var (
n tailcfg.NodeView
u tailcfg.UserProfile
ok bool
)
var ipp netip.AddrPort var ipp netip.AddrPort
if v := r.FormValue("addr"); v != "" { 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) ipp = netip.AddrPortFrom(ip, 0)
} else { } else {
var err error var err error
@ -468,11 +481,13 @@ func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request,
return return
} }
} }
if ipp.IsValid() {
n, u, ok = b.WhoIs(ipp)
}
} else { } else {
http.Error(w, "missing 'addr' parameter", http.StatusBadRequest) http.Error(w, "missing 'addr' parameter", http.StatusBadRequest)
return return
} }
n, u, ok := b.WhoIs(ipp)
if !ok { if !ok {
http.Error(w, "no match for IP:port", http.StatusNotFound) http.Error(w, "no match for IP:port", http.StatusNotFound)
return return

@ -31,6 +31,7 @@ import (
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tsd" "tailscale.com/tsd"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/logid" "tailscale.com/types/logid"
"tailscale.com/util/slicesx" "tailscale.com/util/slicesx"
@ -99,26 +100,46 @@ func TestSetPushDeviceToken(t *testing.T) {
} }
type whoIsBackend struct { type whoIsBackend struct {
whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
peerCaps map[netip.Addr]tailcfg.PeerCapMap 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) { func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
return b.whoIs(ipp) 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 { func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
return b.peerCaps[ip] 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) // 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{ h := &Handler{
PermitRead: true, 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() rec := httptest.NewRecorder()
t.Run(input, func(t *testing.T) { t.Run(input, func(t *testing.T) {
b := whoIsBackend{ b := whoIsBackend{
@ -129,14 +150,14 @@ func TestWhoIsJustIP(t *testing.T) {
t.Fatalf("backend called with %v; want %v", ipp, want) t.Fatalf("backend called with %v; want %v", ipp, want)
} }
} }
return (&tailcfg.Node{ return match()
ID: 123, },
Addresses: []netip.Prefix{ whoIsNodeKey: func(k key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
netip.MustParsePrefix("100.101.102.103/32"), if k.String() != keyStr {
}, t.Fatalf("backend called with %v; want %v", k, keyStr)
}).View(), }
tailcfg.UserProfile{ID: 456, DisplayName: "foo"}, return match()
true
}, },
peerCaps: map[netip.Addr]tailcfg.PeerCapMap{ peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{ 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) 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 var res apitype.WhoIsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil { 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 { if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
t.Errorf("res.Node.ID=%v, want %v", got, want) t.Errorf("res.Node.ID=%v, want %v", got, want)

Loading…
Cancel
Save