mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
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:
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 == tailscale.ErrPeerNotFound {
|
||||||
|
return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to query local tailscaled status: %w", err)
|
return fmt.Errorf("failed to query local tailscaled status for %v: %w", clientKey, 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
@ -100,6 +101,7 @@ 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)
|
||||||
|
whoIsNodeKey func(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||||
peerCaps map[netip.Addr]tailcfg.PeerCapMap
|
peerCaps map[netip.Addr]tailcfg.PeerCapMap
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,18 +109,37 @@ func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.U
|
|||||||
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{
|
|
||||||
netip.MustParsePrefix("100.101.102.103/32"),
|
|
||||||
},
|
},
|
||||||
}).View(),
|
whoIsNodeKey: func(k key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||||
tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
|
if k.String() != keyStr {
|
||||||
true
|
t.Fatalf("backend called with %v; want %v", k, keyStr)
|
||||||
|
}
|
||||||
|
return match()
|
||||||
|
|
||||||
},
|
},
|
||||||
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…
x
Reference in New Issue
Block a user