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>
This commit is contained in:
Brad Fitzpatrick 2024-06-14 08:05:47 -07:00 committed by Brad Fitzpatrick
parent 72c8f7700b
commit 6908fb0de3
6 changed files with 141 additions and 25 deletions

View File

@ -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)

View File

@ -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{

View File

@ -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)
_, err := localClient.WhoIsNodeKey(ctx, clientKey)
if err == tailscale.ErrPeerNotFound {
return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
}
if err != nil {
return fmt.Errorf("failed to query local tailscaled status: %w", err)
}
if clientKey == status.Self.PublicKey {
return nil
}
if _, exists := status.Peer[clientKey]; !exists {
return fmt.Errorf("client %v not in set of peers", clientKey)
return fmt.Errorf("failed to query local tailscaled status for %v: %w", clientKey, err)
}
}

View File

@ -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.

View File

@ -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

View File

@ -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)