From c611d8480bb4a7af35afff3d61ad48168de0d8d6 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 28 Jan 2021 15:29:17 -0800 Subject: [PATCH] cmd/tailscaled: add whois/identd-ish debug handler --- ipn/ipnserver/server.go | 40 +++++++++++++++++++++++++++++++++ ipn/local.go | 49 +++++++++++++++++++++++++++++++++++++++++ tailcfg/tailcfg.go | 6 +++++ 3 files changed, 95 insertions(+) diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 7518c6e2a..69d160d52 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -7,6 +7,7 @@ package ipnserver import ( "bufio" "context" + "encoding/json" "errors" "fmt" "io" @@ -32,6 +33,7 @@ import ( "tailscale.com/net/netstat" "tailscale.com/safesocket" "tailscale.com/smallzstd" + "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/util/pidowner" "tailscale.com/util/systemd" @@ -620,6 +622,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() ( opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) { serveHTMLStatus(w, b) }) + opts.DebugMux.Handle("/whois", whoIsHandler{b}) } server.b = b @@ -883,3 +886,40 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int { } return 0 } + +// whoIsHandler is the debug server's /debug?ip=$IP HTTP handler. +type whoIsHandler struct { + b *ipn.LocalBackend +} + +func (h whoIsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + b := h.b + var ip netaddr.IP + if v := r.FormValue("ip"); v != "" { + var err error + ip, err = netaddr.ParseIP(r.FormValue("ip")) + if err != nil { + http.Error(w, "invalid 'ip' parameter", 400) + return + } + } else { + http.Error(w, "missing 'ip' parameter", 400) + return + } + n, u, ok := b.WhoIs(ip) + if !ok { + http.Error(w, "no match for IP", 404) + return + } + res := &tailcfg.WhoIsResponse{ + Node: n, + UserProfile: &u, + } + j, err := json.MarshalIndent(res, "", "\t") + if err != nil { + http.Error(w, "JSON encoding error", 500) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(j) +} diff --git a/ipn/local.go b/ipn/local.go index 580c49abc..335be2244 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -90,6 +90,7 @@ type LocalBackend struct { hostinfo *tailcfg.Hostinfo // netMap is not mutated in-place once set. netMap *controlclient.NetworkMap + nodeByAddr map[netaddr.IP]*tailcfg.Node activeLogin string // last logged LoginName from netMap engineStatus EngineStatus endpoints []string @@ -234,7 +235,22 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) { }) } } +} +// WhoIs reports the node and user who owns the node with the given IP. +// If ok == true, n and u are valid. +func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) { + b.mu.Lock() + defer b.mu.Unlock() + n, ok = b.nodeByAddr[ip] + if !ok { + return nil, u, false + } + u, ok = b.netMap.UserProfiles[n.User] + if !ok { + return nil, u, false + } + return n, u, true } // SetDecompressor sets a decompression function, which must be a zstd @@ -1507,6 +1523,39 @@ func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) { b.logf("active login: %v", login) b.activeLogin = login } + + if nm == nil { + b.nodeByAddr = nil + return + } + + // Update the nodeByAddr index. + if b.nodeByAddr == nil { + b.nodeByAddr = map[netaddr.IP]*tailcfg.Node{} + } + // First pass, mark everything unwanted. + for k := range b.nodeByAddr { + b.nodeByAddr[k] = nil + } + addNode := func(n *tailcfg.Node) { + for _, ipp := range n.Addresses { + if ipp.IsSingleIP() { + b.nodeByAddr[ipp.IP] = n + } + } + } + if nm.SelfNode != nil { + addNode(nm.SelfNode) + } + for _, p := range nm.Peers { + addNode(p) + } + // Third pass, actually delete the unwanted items. + for k, v := range b.nodeByAddr { + if v == nil { + delete(b.nodeByAddr, k) + } + } } // TestOnlyPublicKeys returns the current machine and node public diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 4e7c51f79..ba215000e 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -935,3 +935,9 @@ func eqCIDRs(a, b []netaddr.IPPrefix) bool { func eqTimePtr(a, b *time.Time) bool { return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b)) } + +// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler. +type WhoIsResponse struct { + Node *Node + UserProfile *UserProfile +}