all: implement pcap streaming for datapath debugging

Updates: tailscale/corp#8470

Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
Tom DNetto
2023-01-19 14:28:49 -08:00
committed by Tom
parent 1acdcff63e
commit 99b9d7a621
12 changed files with 422 additions and 1 deletions

View File

@@ -76,6 +76,7 @@ import (
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
@@ -147,6 +148,7 @@ type LocalBackend struct {
em *expiryManager // non-nil
sshAtomicBool atomic.Bool
shutdownCalled bool // if Shutdown has been called
debugSink *capture.Sink
// lastProfileID tracks the last profile we've seen from the ProfileManager.
// It's used to detect when the user has changed their profile.
@@ -516,6 +518,11 @@ func (b *LocalBackend) Shutdown() {
b.sshServer = nil
}
b.closePeerAPIListenersLocked()
if b.debugSink != nil {
b.e.InstallCaptureHook(nil)
b.debugSink.Close()
b.debugSink = nil
}
b.mu.Unlock()
b.unregisterLinkMon()
@@ -4837,3 +4844,45 @@ func (b *LocalBackend) ResetAuth() error {
}
return b.resetForProfileChangeLockedOnEntry()
}
// StreamDebugCapture writes a pcap stream of packets traversing
// tailscaled to the provided response writer.
func (b *LocalBackend) StreamDebugCapture(ctx context.Context, w io.Writer) error {
var s *capture.Sink
b.mu.Lock()
if b.debugSink == nil {
s = capture.New()
b.debugSink = s
b.e.InstallCaptureHook(s.LogPacket)
} else {
s = b.debugSink
}
b.mu.Unlock()
unregister := s.RegisterOutput(w)
select {
case <-ctx.Done():
case <-s.WaitCh():
}
unregister()
// Shut down & uninstall the sink if there are no longer
// any outputs on it.
b.mu.Lock()
defer b.mu.Unlock()
select {
case <-b.ctx.Done():
return nil
default:
}
if b.debugSink != nil && b.debugSink.NumOutputs() == 0 {
s := b.debugSink
b.e.InstallCaptureHook(nil)
b.debugSink = nil
return s.Close()
}
return nil
}

View File

@@ -68,6 +68,7 @@ var handler = map[string]localAPIHandler{
"debug-derp-region": (*Handler).serveDebugDERPRegion,
"debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches,
"debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules,
"debug-capture": (*Handler).serveDebugCapture,
"derpmap": (*Handler).serveDERPMap,
"dev-set-state-store": (*Handler).serveDevSetStateStore,
"set-push-device-token": (*Handler).serveSetPushDeviceToken,
@@ -1556,6 +1557,21 @@ func defBool(a string, def bool) bool {
return v
}
func (h *Handler) serveDebugCapture(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(200)
w.(http.Flusher).Flush()
h.b.StreamDebugCapture(r.Context(), w)
}
var (
metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")