feature/capture: move packet capture to feature/*, out of iOS + CLI

We had the debug packet capture code + Lua dissector in the CLI + the
iOS app. Now we don't, with tests to lock it in.

As a bonus, tailscale.com/net/packet and tailscale.com/net/flowtrack
no longer appear in the CLI's binary either.

A new build tag ts_omit_capture disables the packet capture code and
was added to build_dist.sh's --extra-small mode.

Updates #12614

Change-Id: I79b0628c0d59911bd4d510c732284d97b0160f10
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-01-23 20:39:28 -08:00 committed by Brad Fitzpatrick
parent 2c98c44d9a
commit 68a66ee81b
23 changed files with 620 additions and 484 deletions

View File

@ -37,7 +37,7 @@ while [ "$#" -gt 1 ]; do
--extra-small) --extra-small)
shift shift
ldflags="$ldflags -w -s" ldflags="$ldflags -w -s"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan" tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture"
;; ;;
--box) --box)
shift shift

View File

@ -802,6 +802,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/envknob from tailscale.com/client/tailscale+ tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/envknob/featureknob from tailscale.com/client/web+ tailscale.com/envknob/featureknob from tailscale.com/client/web+
tailscale.com/feature from tailscale.com/feature/wakeonlan+ tailscale.com/feature from tailscale.com/feature/wakeonlan+
tailscale.com/feature/capture from tailscale.com/feature/condregister
tailscale.com/feature/condregister from tailscale.com/tsnet tailscale.com/feature/condregister from tailscale.com/tsnet
L tailscale.com/feature/tap from tailscale.com/feature/condregister L tailscale.com/feature/tap from tailscale.com/feature/condregister
tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister
@ -814,7 +815,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+ tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+ tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/ipn/localapi from tailscale.com/tsnet tailscale.com/ipn/localapi from tailscale.com/tsnet+
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+ tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
@ -969,7 +970,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/version from tailscale.com/client/web+ tailscale.com/version from tailscale.com/client/web+
tailscale.com/version/distro from tailscale.com/client/web+ tailscale.com/version/distro from tailscale.com/client/web+
tailscale.com/wgengine from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+ tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap+ tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap+
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+

View File

@ -212,7 +212,7 @@ change in the future.
exitNodeCmd(), exitNodeCmd(),
updateCmd, updateCmd,
whoisCmd, whoisCmd,
debugCmd, debugCmd(),
driveCmd, driveCmd,
idTokenCmd, idTokenCmd,
advertiseCmd(), advertiseCmd(),

View File

@ -25,10 +25,12 @@ import (
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tka" "tailscale.com/tka"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/tstest/deptest"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/opt" "tailscale.com/types/opt"
"tailscale.com/types/persist" "tailscale.com/types/persist"
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
"tailscale.com/util/set"
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
@ -1568,3 +1570,31 @@ func TestDocs(t *testing.T) {
} }
walk(t, root) walk(t, root)
} }
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "linux",
GOARCH: "arm64",
WantDeps: set.Of(
"tailscale.com/feature/capture/dissector", // want the Lua by default
),
BadDeps: map[string]string{
"tailscale.com/feature/capture": "don't link capture code",
"tailscale.com/net/packet": "why we passing packets in the CLI?",
"tailscale.com/net/flowtrack": "why we tracking flows in the CLI?",
},
}.Check(t)
}
func TestDepsNoCapture(t *testing.T) {
deptest.DepChecker{
GOOS: "linux",
GOARCH: "arm64",
Tags: "ts_omit_capture",
BadDeps: map[string]string{
"tailscale.com/feature/capture": "don't link capture code",
"tailscale.com/feature/capture/dissector": "don't like the Lua",
},
}.Check(t)
}

View File

@ -0,0 +1,80 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !ts_omit_capture
package cli
import (
"context"
"flag"
"fmt"
"io"
"os"
"os/exec"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/feature/capture/dissector"
)
func init() {
debugCaptureCmd = mkDebugCaptureCmd
}
func mkDebugCaptureCmd() *ffcli.Command {
return &ffcli.Command{
Name: "capture",
ShortUsage: "tailscale debug capture",
Exec: runCapture,
ShortHelp: "Stream pcaps for debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("capture")
fs.StringVar(&captureArgs.outFile, "o", "", "path to stream the pcap (or - for stdout), leave empty to start wireshark")
return fs
})(),
}
}
var captureArgs struct {
outFile string
}
func runCapture(ctx context.Context, args []string) error {
stream, err := localClient.StreamDebugCapture(ctx)
if err != nil {
return err
}
defer stream.Close()
switch captureArgs.outFile {
case "-":
fmt.Fprintln(Stderr, "Press Ctrl-C to stop the capture.")
_, err = io.Copy(os.Stdout, stream)
return err
case "":
lua, err := os.CreateTemp("", "ts-dissector")
if err != nil {
return err
}
defer os.Remove(lua.Name())
io.WriteString(lua, dissector.Lua)
if err := lua.Close(); err != nil {
return err
}
wireshark := exec.CommandContext(ctx, "wireshark", "-X", "lua_script:"+lua.Name(), "-k", "-i", "-")
wireshark.Stdin = stream
wireshark.Stdout = os.Stdout
wireshark.Stderr = os.Stderr
return wireshark.Run()
}
f, err := os.OpenFile(captureArgs.outFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
fmt.Fprintln(Stderr, "Press Ctrl-C to stop the capture.")
_, err = io.Copy(f, stream)
return err
}

View File

@ -20,7 +20,6 @@ import (
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
"os/exec"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
@ -45,10 +44,14 @@ import (
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/must" "tailscale.com/util/must"
"tailscale.com/wgengine/capture"
) )
var debugCmd = &ffcli.Command{ var (
debugCaptureCmd func() *ffcli.Command // or nil
)
func debugCmd() *ffcli.Command {
return &ffcli.Command{
Name: "debug", Name: "debug",
Exec: runDebug, Exec: runDebug,
ShortUsage: "tailscale debug <debug-flags | subcommand>", ShortUsage: "tailscale debug <debug-flags | subcommand>",
@ -62,7 +65,7 @@ var debugCmd = &ffcli.Command{
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty") fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
return fs return fs
})(), })(),
Subcommands: []*ffcli.Command{ Subcommands: nonNilCmds([]*ffcli.Command{
{ {
Name: "derp-map", Name: "derp-map",
ShortUsage: "tailscale debug derp-map", ShortUsage: "tailscale debug derp-map",
@ -285,17 +288,7 @@ var debugCmd = &ffcli.Command{
Exec: runDebugDERP, Exec: runDebugDERP,
ShortHelp: "Test a DERP configuration", ShortHelp: "Test a DERP configuration",
}, },
{ ccall(debugCaptureCmd),
Name: "capture",
ShortUsage: "tailscale debug capture",
Exec: runCapture,
ShortHelp: "Stream pcaps for debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("capture")
fs.StringVar(&captureArgs.outFile, "o", "", "path to stream the pcap (or - for stdout), leave empty to start wireshark")
return fs
})(),
},
{ {
Name: "portmap", Name: "portmap",
ShortUsage: "tailscale debug portmap", ShortUsage: "tailscale debug portmap",
@ -345,7 +338,8 @@ var debugCmd = &ffcli.Command{
ShortHelp: "Print Go's runtime/debug.BuildInfo", ShortHelp: "Print Go's runtime/debug.BuildInfo",
Exec: runGoBuildInfo, Exec: runGoBuildInfo,
}, },
}, }...),
}
} }
func runGoBuildInfo(ctx context.Context, args []string) error { func runGoBuildInfo(ctx context.Context, args []string) error {
@ -1036,50 +1030,6 @@ func runSetExpire(ctx context.Context, args []string) error {
return localClient.DebugSetExpireIn(ctx, setExpireArgs.in) return localClient.DebugSetExpireIn(ctx, setExpireArgs.in)
} }
var captureArgs struct {
outFile string
}
func runCapture(ctx context.Context, args []string) error {
stream, err := localClient.StreamDebugCapture(ctx)
if err != nil {
return err
}
defer stream.Close()
switch captureArgs.outFile {
case "-":
fmt.Fprintln(Stderr, "Press Ctrl-C to stop the capture.")
_, err = io.Copy(os.Stdout, stream)
return err
case "":
lua, err := os.CreateTemp("", "ts-dissector")
if err != nil {
return err
}
defer os.Remove(lua.Name())
lua.Write([]byte(capture.DissectorLua))
if err := lua.Close(); err != nil {
return err
}
wireshark := exec.CommandContext(ctx, "wireshark", "-X", "lua_script:"+lua.Name(), "-k", "-i", "-")
wireshark.Stdin = stream
wireshark.Stdout = os.Stdout
wireshark.Stderr = os.Stderr
return wireshark.Run()
}
f, err := os.OpenFile(captureArgs.outFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
fmt.Fprintln(Stderr, "Press Ctrl-C to stop the capture.")
_, err = io.Copy(f, stream)
return err
}
var debugPortmapArgs struct { var debugPortmapArgs struct {
duration time.Duration duration time.Duration
gatewayAddr string gatewayAddr string

View File

@ -88,6 +88,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/drive from tailscale.com/client/tailscale+ tailscale.com/drive from tailscale.com/client/tailscale+
tailscale.com/envknob from tailscale.com/client/tailscale+ tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/envknob/featureknob from tailscale.com/client/web tailscale.com/envknob/featureknob from tailscale.com/client/web
tailscale.com/feature/capture/dissector from tailscale.com/cmd/tailscale/cli
tailscale.com/health from tailscale.com/net/tlsdial+ tailscale.com/health from tailscale.com/net/tlsdial+
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
tailscale.com/hostinfo from tailscale.com/client/web+ tailscale.com/hostinfo from tailscale.com/client/web+
@ -102,7 +103,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
tailscale.com/net/dnscache from tailscale.com/control/controlhttp+ tailscale.com/net/dnscache from tailscale.com/control/controlhttp+
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp+ tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp+
tailscale.com/net/flowtrack from tailscale.com/net/packet
tailscale.com/net/netaddr from tailscale.com/ipn+ tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
tailscale.com/net/neterror from tailscale.com/net/netcheck+ tailscale.com/net/neterror from tailscale.com/net/netcheck+
@ -110,7 +110,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+ 💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+ 💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/netutil from tailscale.com/client/tailscale+ tailscale.com/net/netutil from tailscale.com/client/tailscale+
tailscale.com/net/packet from tailscale.com/wgengine/capture
tailscale.com/net/ping from tailscale.com/net/netcheck tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/portmapper from tailscale.com/cmd/tailscale/cli+ tailscale.com/net/portmapper from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+ tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
@ -133,7 +132,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/tsweb/varz from tailscale.com/util/usermetric tailscale.com/tsweb/varz from tailscale.com/util/usermetric
tailscale.com/types/dnstype from tailscale.com/tailcfg+ tailscale.com/types/dnstype from tailscale.com/tailcfg+
tailscale.com/types/empty from tailscale.com/ipn tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ tailscale.com/types/ipproto from tailscale.com/ipn+
tailscale.com/types/key from tailscale.com/client/tailscale+ tailscale.com/types/key from tailscale.com/client/tailscale+
tailscale.com/types/lazy from tailscale.com/util/testenv+ tailscale.com/types/lazy from tailscale.com/util/testenv+
tailscale.com/types/logger from tailscale.com/client/web+ tailscale.com/types/logger from tailscale.com/client/web+
@ -185,7 +184,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+ W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/client/web+ tailscale.com/version from tailscale.com/client/web+
tailscale.com/version/distro from tailscale.com/client/web+ tailscale.com/version/distro from tailscale.com/client/web+
tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap
golang.org/x/crypto/argon2 from tailscale.com/tka golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+ golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+

View File

@ -260,6 +260,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/envknob from tailscale.com/client/tailscale+ tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/envknob/featureknob from tailscale.com/client/web+ tailscale.com/envknob/featureknob from tailscale.com/client/web+
tailscale.com/feature from tailscale.com/feature/wakeonlan+ tailscale.com/feature from tailscale.com/feature/wakeonlan+
tailscale.com/feature/capture from tailscale.com/feature/condregister
tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled
L tailscale.com/feature/tap from tailscale.com/feature/condregister L tailscale.com/feature/tap from tailscale.com/feature/condregister
tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister
@ -273,7 +274,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/ipnlocal from tailscale.com/cmd/tailscaled+ tailscale.com/ipn/ipnlocal from tailscale.com/cmd/tailscaled+
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+ tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+ tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
@ -422,7 +423,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/version/distro from tailscale.com/client/web+ tailscale.com/version/distro from tailscale.com/client/web+
W tailscale.com/wf from tailscale.com/cmd/tailscaled W tailscale.com/wf from tailscale.com/cmd/tailscaled
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+ tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+ tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap+ tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap+
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+

View File

@ -13,21 +13,44 @@ import (
"sync" "sync"
"time" "time"
_ "embed" "tailscale.com/feature"
"tailscale.com/ipn/localapi"
"tailscale.com/net/packet" "tailscale.com/net/packet"
"tailscale.com/util/set" "tailscale.com/util/set"
) )
//go:embed ts-dissector.lua func init() {
var DissectorLua string feature.Register("capture")
localapi.Register("debug-capture", serveLocalAPIDebugCapture)
}
// Callback describes a function which is called to func serveLocalAPIDebugCapture(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
// record packets when debugging packet-capture. ctx := r.Context()
// Such callbacks must not take ownership of the if !h.PermitWrite {
// provided data slice: it may only copy out of it http.Error(w, "debug access denied", http.StatusForbidden)
// within the lifetime of the function. return
type Callback func(Path, time.Time, []byte, packet.CaptureMeta) }
if r.Method != "POST" {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
w.(http.Flusher).Flush()
b := h.LocalBackend()
s := b.GetOrSetCaptureSink(newSink)
unregister := s.RegisterOutput(w)
select {
case <-ctx.Done():
case <-s.WaitCh():
}
unregister()
b.ClearCaptureSink()
}
var bufferPool = sync.Pool{ var bufferPool = sync.Pool{
New: func() any { New: func() any {
@ -57,29 +80,8 @@ func writePktHeader(w *bytes.Buffer, when time.Time, length int) {
binary.Write(w, binary.LittleEndian, uint32(length)) // total length binary.Write(w, binary.LittleEndian, uint32(length)) // total length
} }
// Path describes where in the data path the packet was captured. // newSink creates a new capture sink.
type Path uint8 func newSink() packet.CaptureSink {
// Valid Path values.
const (
// FromLocal indicates the packet was logged as it traversed the FromLocal path:
// i.e.: A packet from the local system into the TUN.
FromLocal Path = 0
// FromPeer indicates the packet was logged upon reception from a remote peer.
FromPeer Path = 1
// SynthesizedToLocal indicates the packet was generated from within tailscaled,
// and is being routed to the local machine's network stack.
SynthesizedToLocal Path = 2
// SynthesizedToPeer indicates the packet was generated from within tailscaled,
// and is being routed to a remote Wireguard peer.
SynthesizedToPeer Path = 3
// PathDisco indicates the packet is information about a disco frame.
PathDisco Path = 254
)
// New creates a new capture sink.
func New() *Sink {
ctx, c := context.WithCancel(context.Background()) ctx, c := context.WithCancel(context.Background())
return &Sink{ return &Sink{
ctx: ctx, ctx: ctx,
@ -126,6 +128,10 @@ func (s *Sink) RegisterOutput(w io.Writer) (unregister func()) {
} }
} }
func (s *Sink) CaptureCallback() packet.CaptureCallback {
return s.LogPacket
}
// NumOutputs returns the number of outputs registered with the sink. // NumOutputs returns the number of outputs registered with the sink.
func (s *Sink) NumOutputs() int { func (s *Sink) NumOutputs() int {
s.mu.Lock() s.mu.Lock()
@ -174,7 +180,7 @@ func customDataLen(meta packet.CaptureMeta) int {
// LogPacket is called to insert a packet into the capture. // LogPacket is called to insert a packet into the capture.
// //
// This function does not take ownership of the provided data slice. // This function does not take ownership of the provided data slice.
func (s *Sink) LogPacket(path Path, when time.Time, data []byte, meta packet.CaptureMeta) { func (s *Sink) LogPacket(path packet.CapturePath, when time.Time, data []byte, meta packet.CaptureMeta) {
select { select {
case <-s.ctx.Done(): case <-s.ctx.Done():
return return

View File

@ -0,0 +1,12 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package dissector contains the Lua dissector for Tailscale packets.
package dissector
import (
_ "embed"
)
//go:embed ts-dissector.lua
var Lua string

View File

@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !ts_omit_capture
package condregister
import _ "tailscale.com/feature/capture"

View File

@ -73,6 +73,7 @@ import (
"tailscale.com/net/netmon" "tailscale.com/net/netmon"
"tailscale.com/net/netns" "tailscale.com/net/netns"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/net/packet"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial" "tailscale.com/net/tsdial"
"tailscale.com/paths" "tailscale.com/paths"
@ -115,7 +116,6 @@ import (
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/version/distro" "tailscale.com/version/distro"
"tailscale.com/wgengine" "tailscale.com/wgengine"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router" "tailscale.com/wgengine/router"
@ -209,7 +209,7 @@ type LocalBackend struct {
// Tailscale on port 5252. // Tailscale on port 5252.
exposeRemoteWebClientAtomicBool atomic.Bool exposeRemoteWebClientAtomicBool atomic.Bool
shutdownCalled bool // if Shutdown has been called shutdownCalled bool // if Shutdown has been called
debugSink *capture.Sink debugSink packet.CaptureSink
sockstatLogger *sockstatlog.Logger sockstatLogger *sockstatlog.Logger
// getTCPHandlerForFunnelFlow returns a handler for an incoming TCP flow for // getTCPHandlerForFunnelFlow returns a handler for an incoming TCP flow for
@ -948,6 +948,40 @@ func (b *LocalBackend) onHealthChange(w *health.Warnable, us *health.UnhealthySt
} }
} }
// GetOrSetCaptureSink returns the current packet capture sink, creating it
// with the provided newSink function if it does not already exist.
func (b *LocalBackend) GetOrSetCaptureSink(newSink func() packet.CaptureSink) packet.CaptureSink {
b.mu.Lock()
defer b.mu.Unlock()
if b.debugSink != nil {
return b.debugSink
}
s := newSink()
b.debugSink = s
b.e.InstallCaptureHook(s.CaptureCallback())
return s
}
func (b *LocalBackend) ClearCaptureSink() {
// 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
default:
}
if b.debugSink != nil && b.debugSink.NumOutputs() == 0 {
s := b.debugSink
b.e.InstallCaptureHook(nil)
b.debugSink = nil
s.Close()
}
}
// Shutdown halts the backend and all its sub-components. The backend // Shutdown halts the backend and all its sub-components. The backend
// can no longer be used after Shutdown returns. // can no longer be used after Shutdown returns.
func (b *LocalBackend) Shutdown() { func (b *LocalBackend) Shutdown() {
@ -7154,48 +7188,6 @@ func (b *LocalBackend) ResetAuth() error {
return b.resetForProfileChangeLockedOnEntry(unlock) return b.resetForProfileChangeLockedOnEntry(unlock)
} }
// 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
}
func (b *LocalBackend) GetPeerEndpointChanges(ctx context.Context, ip netip.Addr) ([]magicsock.EndpointChange, error) { func (b *LocalBackend) GetPeerEndpointChanges(ctx context.Context, ip netip.Addr) ([]magicsock.EndpointChange, error) {
pip, ok := b.e.PeerForIP(ip) pip, ok := b.e.PeerForIP(ip)
if !ok { if !ok {

View File

@ -68,12 +68,12 @@ import (
"tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/magicsock"
) )
type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request) type LocalAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
// handler is the set of LocalAPI handlers, keyed by the part of the // handler is the set of LocalAPI handlers, keyed by the part of the
// Request.URL.Path after "/localapi/v0/". If the key ends with a trailing slash // Request.URL.Path after "/localapi/v0/". If the key ends with a trailing slash
// then it's a prefix match. // then it's a prefix match.
var handler = map[string]localAPIHandler{ var handler = map[string]LocalAPIHandler{
// The prefix match handlers end with a slash: // The prefix match handlers end with a slash:
"cert/": (*Handler).serveCert, "cert/": (*Handler).serveCert,
"file-put/": (*Handler).serveFilePut, "file-put/": (*Handler).serveFilePut,
@ -90,7 +90,6 @@ var handler = map[string]localAPIHandler{
"check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding, "check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding,
"component-debug-logging": (*Handler).serveComponentDebugLogging, "component-debug-logging": (*Handler).serveComponentDebugLogging,
"debug": (*Handler).serveDebug, "debug": (*Handler).serveDebug,
"debug-capture": (*Handler).serveDebugCapture,
"debug-derp-region": (*Handler).serveDebugDERPRegion, "debug-derp-region": (*Handler).serveDebugDERPRegion,
"debug-dial-types": (*Handler).serveDebugDialTypes, "debug-dial-types": (*Handler).serveDebugDialTypes,
"debug-log": (*Handler).serveDebugLog, "debug-log": (*Handler).serveDebugLog,
@ -152,6 +151,14 @@ var handler = map[string]localAPIHandler{
"whois": (*Handler).serveWhoIs, "whois": (*Handler).serveWhoIs,
} }
// Register registers a new LocalAPI handler for the given name.
func Register(name string, fn LocalAPIHandler) {
if _, ok := handler[name]; ok {
panic("duplicate LocalAPI handler registration: " + name)
}
handler[name] = fn
}
var ( var (
// The clientmetrics package is stateful, but we want to expose a simple // The clientmetrics package is stateful, but we want to expose a simple
// imperative API to local clients, so we need to keep track of // imperative API to local clients, so we need to keep track of
@ -196,6 +203,10 @@ type Handler struct {
clock tstime.Clock clock tstime.Clock
} }
func (h *Handler) LocalBackend() *ipnlocal.LocalBackend {
return h.b
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.b == nil { if h.b == nil {
http.Error(w, "server has no local backend", http.StatusInternalServerError) http.Error(w, "server has no local backend", http.StatusInternalServerError)
@ -260,7 +271,7 @@ func (h *Handler) validHost(hostname string) bool {
// handlerForPath returns the LocalAPI handler for the provided Request.URI.Path. // handlerForPath returns the LocalAPI handler for the provided Request.URI.Path.
// (the path doesn't include any query parameters) // (the path doesn't include any query parameters)
func handlerForPath(urlPath string) (h localAPIHandler, ok bool) { func handlerForPath(urlPath string) (h LocalAPIHandler, ok bool) {
if urlPath == "/" { if urlPath == "/" {
return (*Handler).serveLocalAPIRoot, true return (*Handler).serveLocalAPIRoot, true
} }
@ -2689,21 +2700,6 @@ func defBool(a string, def bool) bool {
return v 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(http.StatusOK)
w.(http.Flusher).Flush()
h.b.StreamDebugCapture(r.Context(), w)
}
func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead { if !h.PermitRead {
http.Error(w, "debug-log access denied", http.StatusForbidden) http.Error(w, "debug-log access denied", http.StatusForbidden)

75
net/packet/capture.go Normal file
View File

@ -0,0 +1,75 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"io"
"net/netip"
"time"
)
// Callback describes a function which is called to
// record packets when debugging packet-capture.
// Such callbacks must not take ownership of the
// provided data slice: it may only copy out of it
// within the lifetime of the function.
type CaptureCallback func(CapturePath, time.Time, []byte, CaptureMeta)
// CaptureSink is the minimal interface from [tailscale.com/feature/capture]'s
// Sink type that is needed by the core (magicsock/LocalBackend/wgengine/etc).
// This lets the relativel heavy feature/capture package be optionally linked.
type CaptureSink interface {
// Close closes
Close() error
// NumOutputs returns the number of outputs registered with the sink.
NumOutputs() int
// CaptureCallback returns a callback which can be used to
// write packets to the sink.
CaptureCallback() CaptureCallback
// WaitCh returns a channel which blocks until
// the sink is closed.
WaitCh() <-chan struct{}
// RegisterOutput connects an output to this sink, which
// will be written to with a pcap stream as packets are logged.
// A function is returned which unregisters the output when
// called.
//
// If w implements io.Closer, it will be closed upon error
// or when the sink is closed. If w implements http.Flusher,
// it will be flushed periodically.
RegisterOutput(w io.Writer) (unregister func())
}
// CaptureMeta contains metadata that is used when debugging.
type CaptureMeta struct {
DidSNAT bool // SNAT was performed & the address was updated.
OriginalSrc netip.AddrPort // The source address before SNAT was performed.
DidDNAT bool // DNAT was performed & the address was updated.
OriginalDst netip.AddrPort // The destination address before DNAT was performed.
}
// CapturePath describes where in the data path the packet was captured.
type CapturePath uint8
// CapturePath values
const (
// FromLocal indicates the packet was logged as it traversed the FromLocal path:
// i.e.: A packet from the local system into the TUN.
FromLocal CapturePath = 0
// FromPeer indicates the packet was logged upon reception from a remote peer.
FromPeer CapturePath = 1
// SynthesizedToLocal indicates the packet was generated from within tailscaled,
// and is being routed to the local machine's network stack.
SynthesizedToLocal CapturePath = 2
// SynthesizedToPeer indicates the packet was generated from within tailscaled,
// and is being routed to a remote Wireguard peer.
SynthesizedToPeer CapturePath = 3
// PathDisco indicates the packet is information about a disco frame.
PathDisco CapturePath = 254
)

View File

@ -34,14 +34,6 @@ const (
TCPECNBits TCPFlag = TCPECNEcho | TCPCWR TCPECNBits TCPFlag = TCPECNEcho | TCPCWR
) )
// CaptureMeta contains metadata that is used when debugging.
type CaptureMeta struct {
DidSNAT bool // SNAT was performed & the address was updated.
OriginalSrc netip.AddrPort // The source address before SNAT was performed.
DidDNAT bool // DNAT was performed & the address was updated.
OriginalDst netip.AddrPort // The destination address before DNAT was performed.
}
// Parsed is a minimal decoding of a packet suitable for use in filters. // Parsed is a minimal decoding of a packet suitable for use in filters.
type Parsed struct { type Parsed struct {
// b is the byte buffer that this decodes. // b is the byte buffer that this decodes.

View File

@ -36,7 +36,6 @@ import (
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/usermetric" "tailscale.com/util/usermetric"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/netstack/gro" "tailscale.com/wgengine/netstack/gro"
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
@ -208,7 +207,7 @@ type Wrapper struct {
// stats maintains per-connection counters. // stats maintains per-connection counters.
stats atomic.Pointer[connstats.Statistics] stats atomic.Pointer[connstats.Statistics]
captureHook syncs.AtomicValue[capture.Callback] captureHook syncs.AtomicValue[packet.CaptureCallback]
metrics *metrics metrics *metrics
} }
@ -955,7 +954,7 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
} }
} }
if captHook != nil { if captHook != nil {
captHook(capture.FromLocal, t.now(), p.Buffer(), p.CaptureMeta) captHook(packet.FromLocal, t.now(), p.Buffer(), p.CaptureMeta)
} }
if !t.disableFilter { if !t.disableFilter {
var response filter.Response var response filter.Response
@ -1101,9 +1100,9 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []i
return n, err return n, err
} }
func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed, captHook capture.Callback, pc *peerConfigTable, gro *gro.GRO) (filter.Response, *gro.GRO) { func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed, captHook packet.CaptureCallback, pc *peerConfigTable, gro *gro.GRO) (filter.Response, *gro.GRO) {
if captHook != nil { if captHook != nil {
captHook(capture.FromPeer, t.now(), p.Buffer(), p.CaptureMeta) captHook(packet.FromPeer, t.now(), p.Buffer(), p.CaptureMeta)
} }
if p.IPProto == ipproto.TSMP { if p.IPProto == ipproto.TSMP {
@ -1317,7 +1316,7 @@ func (t *Wrapper) InjectInboundPacketBuffer(pkt *stack.PacketBuffer, buffs [][]b
p.Decode(buf) p.Decode(buf)
captHook := t.captureHook.Load() captHook := t.captureHook.Load()
if captHook != nil { if captHook != nil {
captHook(capture.SynthesizedToLocal, t.now(), p.Buffer(), p.CaptureMeta) captHook(packet.SynthesizedToLocal, t.now(), p.Buffer(), p.CaptureMeta)
} }
invertGSOChecksum(buf, pkt.GSOOptions) invertGSOChecksum(buf, pkt.GSOOptions)
@ -1449,7 +1448,7 @@ func (t *Wrapper) InjectOutboundPacketBuffer(pkt *stack.PacketBuffer) error {
} }
if capt := t.captureHook.Load(); capt != nil { if capt := t.captureHook.Load(); capt != nil {
b := pkt.ToBuffer() b := pkt.ToBuffer()
capt(capture.SynthesizedToPeer, t.now(), b.Flatten(), packet.CaptureMeta{}) capt(packet.SynthesizedToPeer, t.now(), b.Flatten(), packet.CaptureMeta{})
} }
t.injectOutbound(tunInjectedRead{packet: pkt}) t.injectOutbound(tunInjectedRead{packet: pkt})
@ -1491,6 +1490,6 @@ var (
metricPacketOutDropSelfDisco = clientmetric.NewCounter("tstun_out_to_wg_drop_self_disco") metricPacketOutDropSelfDisco = clientmetric.NewCounter("tstun_out_to_wg_drop_self_disco")
) )
func (t *Wrapper) InstallCaptureHook(cb capture.Callback) { func (t *Wrapper) InstallCaptureHook(cb packet.CaptureCallback) {
t.captureHook.Store(cb) t.captureHook.Store(cb)
} }

View File

@ -40,7 +40,6 @@ import (
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/util/must" "tailscale.com/util/must"
"tailscale.com/util/usermetric" "tailscale.com/util/usermetric"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
) )
@ -871,14 +870,14 @@ func TestPeerCfg_NAT(t *testing.T) {
// with the correct parameters when various packet operations are performed. // with the correct parameters when various packet operations are performed.
func TestCaptureHook(t *testing.T) { func TestCaptureHook(t *testing.T) {
type captureRecord struct { type captureRecord struct {
path capture.Path path packet.CapturePath
now time.Time now time.Time
pkt []byte pkt []byte
meta packet.CaptureMeta meta packet.CaptureMeta
} }
var captured []captureRecord var captured []captureRecord
hook := func(path capture.Path, now time.Time, pkt []byte, meta packet.CaptureMeta) { hook := func(path packet.CapturePath, now time.Time, pkt []byte, meta packet.CaptureMeta) {
captured = append(captured, captureRecord{ captured = append(captured, captureRecord{
path: path, path: path,
now: now, now: now,
@ -935,19 +934,19 @@ func TestCaptureHook(t *testing.T) {
// Assert that the right packets are captured. // Assert that the right packets are captured.
want := []captureRecord{ want := []captureRecord{
{ {
path: capture.FromPeer, path: packet.FromPeer,
pkt: []byte("Write1"), pkt: []byte("Write1"),
}, },
{ {
path: capture.FromPeer, path: packet.FromPeer,
pkt: []byte("Write2"), pkt: []byte("Write2"),
}, },
{ {
path: capture.SynthesizedToLocal, path: packet.SynthesizedToLocal,
pkt: []byte("InjectInboundPacketBuffer"), pkt: []byte("InjectInboundPacketBuffer"),
}, },
{ {
path: capture.SynthesizedToPeer, path: packet.SynthesizedToPeer,
pkt: []byte("InjectOutboundPacketBuffer"), pkt: []byte("InjectOutboundPacketBuffer"),
}, },
} }

View File

@ -24,6 +24,7 @@ func TestDeps(t *testing.T) {
"github.com/google/uuid": "see tailscale/tailscale#13760", "github.com/google/uuid": "see tailscale/tailscale#13760",
"tailscale.com/clientupdate/distsign": "downloads via AppStore, not distsign", "tailscale.com/clientupdate/distsign": "downloads via AppStore, not distsign",
"github.com/tailscale/hujson": "no config file support on iOS", "github.com/tailscale/hujson": "no config file support on iOS",
"tailscale.com/feature/capture": "no debug packet capture on iOS",
}, },
}.Check(t) }.Check(t)
} }

View File

@ -61,7 +61,6 @@ import (
"tailscale.com/util/set" "tailscale.com/util/set"
"tailscale.com/util/testenv" "tailscale.com/util/testenv"
"tailscale.com/util/usermetric" "tailscale.com/util/usermetric"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/wgint" "tailscale.com/wgengine/wgint"
) )
@ -238,7 +237,7 @@ type Conn struct {
stats atomic.Pointer[connstats.Statistics] stats atomic.Pointer[connstats.Statistics]
// captureHook, if non-nil, is the pcap logging callback when capturing. // captureHook, if non-nil, is the pcap logging callback when capturing.
captureHook syncs.AtomicValue[capture.Callback] captureHook syncs.AtomicValue[packet.CaptureCallback]
// discoPrivate is the private naclbox key used for active // discoPrivate is the private naclbox key used for active
// discovery traffic. It is always present, and immutable. // discovery traffic. It is always present, and immutable.
@ -655,7 +654,7 @@ func deregisterMetrics(m *metrics) {
// log debug information into the pcap stream. This function // log debug information into the pcap stream. This function
// can be called with a nil argument to uninstall the capture // can be called with a nil argument to uninstall the capture
// hook. // hook.
func (c *Conn) InstallCaptureHook(cb capture.Callback) { func (c *Conn) InstallCaptureHook(cb packet.CaptureCallback) {
c.captureHook.Store(cb) c.captureHook.Store(cb)
} }
@ -1709,7 +1708,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netip.AddrPort, derpNodeSrc ke
// Emit information about the disco frame into the pcap stream // Emit information about the disco frame into the pcap stream
// if a capture hook is installed. // if a capture hook is installed.
if cb := c.captureHook.Load(); cb != nil { if cb := c.captureHook.Load(); cb != nil {
cb(capture.PathDisco, time.Now(), disco.ToPCAPFrame(src, derpNodeSrc, payload), packet.CaptureMeta{}) cb(packet.PathDisco, time.Now(), disco.ToPCAPFrame(src, derpNodeSrc, payload), packet.CaptureMeta{})
} }
dm, err := disco.Parse(payload) dm, err := disco.Parse(payload)

View File

@ -51,7 +51,6 @@ import (
"tailscale.com/util/testenv" "tailscale.com/util/testenv"
"tailscale.com/util/usermetric" "tailscale.com/util/usermetric"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/netlog" "tailscale.com/wgengine/netlog"
@ -1594,7 +1593,7 @@ var (
metricNumMinorChanges = clientmetric.NewCounter("wgengine_minor_changes") metricNumMinorChanges = clientmetric.NewCounter("wgengine_minor_changes")
) )
func (e *userspaceEngine) InstallCaptureHook(cb capture.Callback) { func (e *userspaceEngine) InstallCaptureHook(cb packet.CaptureCallback) {
e.tundev.InstallCaptureHook(cb) e.tundev.InstallCaptureHook(cb)
e.magicConn.InstallCaptureHook(cb) e.magicConn.InstallCaptureHook(cb)
} }

View File

@ -17,10 +17,10 @@ import (
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns" "tailscale.com/net/dns"
"tailscale.com/net/packet"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/router" "tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
@ -162,7 +162,7 @@ func (e *watchdogEngine) Done() <-chan struct{} {
return e.wrap.Done() return e.wrap.Done()
} }
func (e *watchdogEngine) InstallCaptureHook(cb capture.Callback) { func (e *watchdogEngine) InstallCaptureHook(cb packet.CaptureCallback) {
e.wrap.InstallCaptureHook(cb) e.wrap.InstallCaptureHook(cb)
} }

View File

@ -11,10 +11,10 @@ import (
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns" "tailscale.com/net/dns"
"tailscale.com/net/packet"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/router" "tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
@ -129,5 +129,5 @@ type Engine interface {
// InstallCaptureHook registers a function to be called to capture // InstallCaptureHook registers a function to be called to capture
// packets traversing the data path. The hook can be uninstalled by // packets traversing the data path. The hook can be uninstalled by
// calling this function with a nil value. // calling this function with a nil value.
InstallCaptureHook(capture.Callback) InstallCaptureHook(packet.CaptureCallback)
} }