mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-17 04:01:41 +00:00
ipn/ipnlocal: add c2n /debug/{goroutines,prefs,metrics}
* and move goroutine scrubbing code to its own package for reuse * bump capver to 45 Change-Id: I9b4dfa5af44d2ecada6cc044cd1b5674ee427575 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
7686446c60
commit
9bdf0cd8cd
@ -275,6 +275,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||||
|
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
|
||||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||||
|
@ -8,12 +8,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/util/goroutines"
|
||||||
)
|
)
|
||||||
|
|
||||||
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||||
@ -22,7 +21,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
|||||||
|
|
||||||
zbuf := new(bytes.Buffer)
|
zbuf := new(bytes.Buffer)
|
||||||
zw := gzip.NewWriter(zbuf)
|
zw := gzip.NewWriter(zbuf)
|
||||||
zw.Write(scrubbedGoroutineDump())
|
zw.Write(goroutines.ScrubbedGoroutineDump())
|
||||||
zw.Close()
|
zw.Close()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||||
@ -40,83 +39,3 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
|||||||
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
|
||||||
// values of arguments scrubbed out, lest it contain some private key material.
|
|
||||||
func scrubbedGoroutineDump() []byte {
|
|
||||||
var buf []byte
|
|
||||||
// Grab stacks multiple times into increasingly larger buffer sizes
|
|
||||||
// to minimize the risk that we blow past our iOS memory limit.
|
|
||||||
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
|
|
||||||
buf = make([]byte, size)
|
|
||||||
buf = buf[:runtime.Stack(buf, true)]
|
|
||||||
if len(buf) < size {
|
|
||||||
// It fit.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return scrubHex(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrubHex(buf []byte) []byte {
|
|
||||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
|
||||||
|
|
||||||
foreachHexAddress(buf, func(in []byte) {
|
|
||||||
if string(in) == "0x0" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if v, ok := saw[string(in)]; ok {
|
|
||||||
for i := range in {
|
|
||||||
in[i] = '_'
|
|
||||||
}
|
|
||||||
copy(in, v)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inStr := string(in)
|
|
||||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
|
||||||
for i := range in {
|
|
||||||
in[i] = '_'
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
in[0] = '?'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
|
||||||
saw[inStr] = v
|
|
||||||
copy(in, v)
|
|
||||||
})
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
var ohx = []byte("0x")
|
|
||||||
|
|
||||||
// foreachHexAddress calls f with each subslice of b that matches
|
|
||||||
// regexp `0x[0-9a-f]*`.
|
|
||||||
func foreachHexAddress(b []byte, f func([]byte)) {
|
|
||||||
for len(b) > 0 {
|
|
||||||
i := bytes.Index(b, ohx)
|
|
||||||
if i == -1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b = b[i:]
|
|
||||||
hx := hexPrefix(b)
|
|
||||||
f(hx)
|
|
||||||
b = b[len(hx):]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hexPrefix(b []byte) []byte {
|
|
||||||
for i, c := range b {
|
|
||||||
if i < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !isHexByte(c) {
|
|
||||||
return b[:i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func isHexByte(b byte) bool {
|
|
||||||
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
|
|
||||||
}
|
|
||||||
|
@ -10,14 +10,28 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/util/clientmetric"
|
||||||
|
"tailscale.com/util/goroutines"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON := func(v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/echo":
|
case "/echo":
|
||||||
// Test handler.
|
// Test handler.
|
||||||
body, _ := io.ReadAll(r.Body)
|
body, _ := io.ReadAll(r.Body)
|
||||||
w.Write(body)
|
w.Write(body)
|
||||||
|
case "/debug/goroutines":
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Write(goroutines.ScrubbedGoroutineDump())
|
||||||
|
case "/debug/prefs":
|
||||||
|
writeJSON(b.Prefs())
|
||||||
|
case "/debug/metrics":
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
clientmetric.WritePrometheusExpositionFormat(w)
|
||||||
case "/ssh/usernames":
|
case "/ssh/usernames":
|
||||||
var req tailcfg.C2NSSHUsernamesRequest
|
var req tailcfg.C2NSSHUsernamesRequest
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
@ -31,8 +45,7 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
writeJSON(res)
|
||||||
json.NewEncoder(w).Encode(res)
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, "unknown c2n path", http.StatusBadRequest)
|
http.Error(w, "unknown c2n path", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,8 @@ type CapabilityVersion int
|
|||||||
// - 42: 2022-09-06: NextDNS DoH support; see https://github.com/tailscale/tailscale/pull/5556
|
// - 42: 2022-09-06: NextDNS DoH support; see https://github.com/tailscale/tailscale/pull/5556
|
||||||
// - 43: 2022-09-21: clients can return usernames for SSH
|
// - 43: 2022-09-21: clients can return usernames for SSH
|
||||||
// - 44: 2022-09-22: MapResponse.ControlDialPlan
|
// - 44: 2022-09-22: MapResponse.ControlDialPlan
|
||||||
const CurrentCapabilityVersion CapabilityVersion = 44
|
// - 45: 2022-09-26: c2n /debug/{goroutines,prefs,metrics}
|
||||||
|
const CurrentCapabilityVersion CapabilityVersion = 45
|
||||||
|
|
||||||
type StableID string
|
type StableID string
|
||||||
|
|
||||||
|
93
util/goroutines/goroutines.go
Normal file
93
util/goroutines/goroutines.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// The goroutines package contains utilities for getting active goroutines.
|
||||||
|
package goroutines
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||||
|
// values of arguments scrubbed out, lest it contain some private key material.
|
||||||
|
func ScrubbedGoroutineDump() []byte {
|
||||||
|
var buf []byte
|
||||||
|
// Grab stacks multiple times into increasingly larger buffer sizes
|
||||||
|
// to minimize the risk that we blow past our iOS memory limit.
|
||||||
|
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
|
||||||
|
buf = make([]byte, size)
|
||||||
|
buf = buf[:runtime.Stack(buf, true)]
|
||||||
|
if len(buf) < size {
|
||||||
|
// It fit.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scrubHex(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrubHex(buf []byte) []byte {
|
||||||
|
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||||
|
|
||||||
|
foreachHexAddress(buf, func(in []byte) {
|
||||||
|
if string(in) == "0x0" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v, ok := saw[string(in)]; ok {
|
||||||
|
for i := range in {
|
||||||
|
in[i] = '_'
|
||||||
|
}
|
||||||
|
copy(in, v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inStr := string(in)
|
||||||
|
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||||
|
for i := range in {
|
||||||
|
in[i] = '_'
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
in[0] = '?'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||||
|
saw[inStr] = v
|
||||||
|
copy(in, v)
|
||||||
|
})
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
var ohx = []byte("0x")
|
||||||
|
|
||||||
|
// foreachHexAddress calls f with each subslice of b that matches
|
||||||
|
// regexp `0x[0-9a-f]*`.
|
||||||
|
func foreachHexAddress(b []byte, f func([]byte)) {
|
||||||
|
for len(b) > 0 {
|
||||||
|
i := bytes.Index(b, ohx)
|
||||||
|
if i == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b = b[i:]
|
||||||
|
hx := hexPrefix(b)
|
||||||
|
f(hx)
|
||||||
|
b = b[len(hx):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexPrefix(b []byte) []byte {
|
||||||
|
for i, c := range b {
|
||||||
|
if i < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isHexByte(c) {
|
||||||
|
return b[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHexByte(b byte) bool {
|
||||||
|
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
|
||||||
|
}
|
@ -2,12 +2,12 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package controlclient
|
package goroutines
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestScrubbedGoroutineDump(t *testing.T) {
|
func TestScrubbedGoroutineDump(t *testing.T) {
|
||||||
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
|
t.Logf("Got:\n%s\n", ScrubbedGoroutineDump())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScrubHex(t *testing.T) {
|
func TestScrubHex(t *testing.T) {
|
Loading…
x
Reference in New Issue
Block a user