mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-12 05:37:32 +00:00
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:

committed by
Brad Fitzpatrick

parent
2c98c44d9a
commit
68a66ee81b
@@ -1,238 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package capture formats packet logging into a debug pcap stream.
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
//go:embed ts-dissector.lua
|
||||
var DissectorLua string
|
||||
|
||||
// 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 Callback func(Path, time.Time, []byte, packet.CaptureMeta)
|
||||
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
const flushPeriod = 100 * time.Millisecond
|
||||
|
||||
func writePcapHeader(w io.Writer) {
|
||||
binary.Write(w, binary.LittleEndian, uint32(0xA1B2C3D4)) // pcap magic number
|
||||
binary.Write(w, binary.LittleEndian, uint16(2)) // version major
|
||||
binary.Write(w, binary.LittleEndian, uint16(4)) // version minor
|
||||
binary.Write(w, binary.LittleEndian, uint32(0)) // this zone
|
||||
binary.Write(w, binary.LittleEndian, uint32(0)) // zone significant figures
|
||||
binary.Write(w, binary.LittleEndian, uint32(65535)) // max packet len
|
||||
binary.Write(w, binary.LittleEndian, uint32(147)) // link-layer ID - USER0
|
||||
}
|
||||
|
||||
func writePktHeader(w *bytes.Buffer, when time.Time, length int) {
|
||||
s := when.Unix()
|
||||
us := when.UnixMicro() - (s * 1000000)
|
||||
|
||||
binary.Write(w, binary.LittleEndian, uint32(s)) // timestamp in seconds
|
||||
binary.Write(w, binary.LittleEndian, uint32(us)) // timestamp microseconds
|
||||
binary.Write(w, binary.LittleEndian, uint32(length)) // length present
|
||||
binary.Write(w, binary.LittleEndian, uint32(length)) // total length
|
||||
}
|
||||
|
||||
// Path describes where in the data path the packet was captured.
|
||||
type Path uint8
|
||||
|
||||
// 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())
|
||||
return &Sink{
|
||||
ctx: ctx,
|
||||
ctxCancel: c,
|
||||
}
|
||||
}
|
||||
|
||||
// Type Sink handles callbacks with packets to be logged,
|
||||
// formatting them into a pcap stream which is mirrored to
|
||||
// all registered outputs.
|
||||
type Sink struct {
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
|
||||
mu sync.Mutex
|
||||
outputs set.HandleSet[io.Writer]
|
||||
flushTimer *time.Timer // or nil if none running
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (s *Sink) RegisterOutput(w io.Writer) (unregister func()) {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return func() {}
|
||||
default:
|
||||
}
|
||||
|
||||
writePcapHeader(w)
|
||||
s.mu.Lock()
|
||||
hnd := s.outputs.Add(w)
|
||||
s.mu.Unlock()
|
||||
|
||||
return func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.outputs, hnd)
|
||||
}
|
||||
}
|
||||
|
||||
// NumOutputs returns the number of outputs registered with the sink.
|
||||
func (s *Sink) NumOutputs() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return len(s.outputs)
|
||||
}
|
||||
|
||||
// Close shuts down the sink. Future calls to LogPacket
|
||||
// are ignored, and any registered output that implements
|
||||
// io.Closer is closed.
|
||||
func (s *Sink) Close() error {
|
||||
s.ctxCancel()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.flushTimer != nil {
|
||||
s.flushTimer.Stop()
|
||||
s.flushTimer = nil
|
||||
}
|
||||
|
||||
for _, o := range s.outputs {
|
||||
if o, ok := o.(io.Closer); ok {
|
||||
o.Close()
|
||||
}
|
||||
}
|
||||
s.outputs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitCh returns a channel which blocks until
|
||||
// the sink is closed.
|
||||
func (s *Sink) WaitCh() <-chan struct{} {
|
||||
return s.ctx.Done()
|
||||
}
|
||||
|
||||
func customDataLen(meta packet.CaptureMeta) int {
|
||||
length := 4
|
||||
if meta.DidSNAT {
|
||||
length += meta.OriginalSrc.Addr().BitLen() / 8
|
||||
}
|
||||
if meta.DidDNAT {
|
||||
length += meta.OriginalDst.Addr().BitLen() / 8
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
// LogPacket is called to insert a packet into the capture.
|
||||
//
|
||||
// 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) {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
extraLen := customDataLen(meta)
|
||||
b := bufferPool.Get().(*bytes.Buffer)
|
||||
b.Reset()
|
||||
b.Grow(16 + extraLen + len(data)) // 16b pcap header + len(metadata) + len(payload)
|
||||
defer bufferPool.Put(b)
|
||||
|
||||
writePktHeader(b, when, len(data)+extraLen)
|
||||
|
||||
// Custom tailscale debugging data
|
||||
binary.Write(b, binary.LittleEndian, uint16(path))
|
||||
if meta.DidSNAT {
|
||||
binary.Write(b, binary.LittleEndian, uint8(meta.OriginalSrc.Addr().BitLen()/8))
|
||||
b.Write(meta.OriginalSrc.Addr().AsSlice())
|
||||
} else {
|
||||
binary.Write(b, binary.LittleEndian, uint8(0)) // SNAT addr len == 0
|
||||
}
|
||||
if meta.DidDNAT {
|
||||
binary.Write(b, binary.LittleEndian, uint8(meta.OriginalDst.Addr().BitLen()/8))
|
||||
b.Write(meta.OriginalDst.Addr().AsSlice())
|
||||
} else {
|
||||
binary.Write(b, binary.LittleEndian, uint8(0)) // DNAT addr len == 0
|
||||
}
|
||||
|
||||
b.Write(data)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var hadError []set.Handle
|
||||
for hnd, o := range s.outputs {
|
||||
if _, err := o.Write(b.Bytes()); err != nil {
|
||||
hadError = append(hadError, hnd)
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, hnd := range hadError {
|
||||
if o, ok := s.outputs[hnd].(io.Closer); ok {
|
||||
o.Close()
|
||||
}
|
||||
delete(s.outputs, hnd)
|
||||
}
|
||||
|
||||
if s.flushTimer == nil {
|
||||
s.flushTimer = time.AfterFunc(flushPeriod, func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, o := range s.outputs {
|
||||
if f, ok := o.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
s.flushTimer = nil
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,169 +0,0 @@
|
||||
function hasbit(x, p)
|
||||
return x % (p + p) >= p
|
||||
end
|
||||
|
||||
tsdebug_ll = Proto("tsdebug", "Tailscale debug")
|
||||
PATH = ProtoField.string("tsdebug.PATH","PATH", base.ASCII)
|
||||
SNAT_IP_4 = ProtoField.ipv4("tsdebug.SNAT_IP_4", "Pre-NAT Source IPv4 address")
|
||||
SNAT_IP_6 = ProtoField.ipv6("tsdebug.SNAT_IP_6", "Pre-NAT Source IPv6 address")
|
||||
DNAT_IP_4 = ProtoField.ipv4("tsdebug.DNAT_IP_4", "Pre-NAT Dest IPv4 address")
|
||||
DNAT_IP_6 = ProtoField.ipv6("tsdebug.DNAT_IP_6", "Pre-NAT Dest IPv6 address")
|
||||
tsdebug_ll.fields = {PATH, SNAT_IP_4, SNAT_IP_6, DNAT_IP_4, DNAT_IP_6}
|
||||
|
||||
function tsdebug_ll.dissector(buffer, pinfo, tree)
|
||||
pinfo.cols.protocol = tsdebug_ll.name
|
||||
packet_length = buffer:len()
|
||||
local offset = 0
|
||||
local subtree = tree:add(tsdebug_ll, buffer(), "Tailscale packet")
|
||||
|
||||
-- -- Get path UINT16
|
||||
local path_id = buffer:range(offset, 2):le_uint()
|
||||
if path_id == 0 then subtree:add(PATH, "FromLocal")
|
||||
elseif path_id == 1 then subtree:add(PATH, "FromPeer")
|
||||
elseif path_id == 2 then subtree:add(PATH, "Synthesized (Inbound / ToLocal)")
|
||||
elseif path_id == 3 then subtree:add(PATH, "Synthesized (Outbound / ToPeer)")
|
||||
elseif path_id == 254 then subtree:add(PATH, "Disco frame")
|
||||
end
|
||||
offset = offset + 2
|
||||
|
||||
-- -- Get SNAT address
|
||||
local snat_addr_len = buffer:range(offset, 1):le_uint()
|
||||
if snat_addr_len == 4 then subtree:add(SNAT_IP_4, buffer:range(offset + 1, snat_addr_len))
|
||||
elseif snat_addr_len > 0 then subtree:add(SNAT_IP_6, buffer:range(offset + 1, snat_addr_len))
|
||||
end
|
||||
offset = offset + 1 + snat_addr_len
|
||||
|
||||
-- -- Get DNAT address
|
||||
local dnat_addr_len = buffer:range(offset, 1):le_uint()
|
||||
if dnat_addr_len == 4 then subtree:add(DNAT_IP_4, buffer:range(offset + 1, dnat_addr_len))
|
||||
elseif dnat_addr_len > 0 then subtree:add(DNAT_IP_6, buffer:range(offset + 1, dnat_addr_len))
|
||||
end
|
||||
offset = offset + 1 + dnat_addr_len
|
||||
|
||||
-- -- Handover rest of data to lower-level dissector
|
||||
local data_buffer = buffer:range(offset, packet_length-offset):tvb()
|
||||
if path_id == 254 then
|
||||
Dissector.get("tsdisco"):call(data_buffer, pinfo, tree)
|
||||
else
|
||||
Dissector.get("ip"):call(data_buffer, pinfo, tree)
|
||||
end
|
||||
end
|
||||
|
||||
-- Install the dissector on link-layer ID 147 (User-defined protocol 0)
|
||||
local eth_table = DissectorTable.get("wtap_encap")
|
||||
eth_table:add(wtap.USER0, tsdebug_ll)
|
||||
|
||||
|
||||
local ts_dissectors = DissectorTable.new("ts.proto", "Tailscale-specific dissectors", ftypes.STRING, base.NONE)
|
||||
|
||||
|
||||
--
|
||||
-- DISCO metadata dissector
|
||||
--
|
||||
tsdisco_meta = Proto("tsdisco", "Tailscale DISCO metadata")
|
||||
DISCO_IS_DERP = ProtoField.bool("tsdisco.IS_DERP","From DERP")
|
||||
DISCO_SRC_IP_4 = ProtoField.ipv4("tsdisco.SRC_IP_4", "Source IPv4 address")
|
||||
DISCO_SRC_IP_6 = ProtoField.ipv6("tsdisco.SRC_IP_6", "Source IPv6 address")
|
||||
DISCO_SRC_PORT = ProtoField.uint16("tsdisco.SRC_PORT","Source port", base.DEC)
|
||||
DISCO_DERP_PUB = ProtoField.bytes("tsdisco.DERP_PUB", "DERP public key", base.SPACE)
|
||||
tsdisco_meta.fields = {DISCO_IS_DERP, DISCO_SRC_PORT, DISCO_DERP_PUB, DISCO_SRC_IP_4, DISCO_SRC_IP_6}
|
||||
|
||||
function tsdisco_meta.dissector(buffer, pinfo, tree)
|
||||
pinfo.cols.protocol = tsdisco_meta.name
|
||||
packet_length = buffer:len()
|
||||
local offset = 0
|
||||
local subtree = tree:add(tsdisco_meta, buffer(), "DISCO metadata")
|
||||
|
||||
-- Parse flags
|
||||
local from_derp = hasbit(buffer(offset, 1):le_uint(), 0)
|
||||
subtree:add(DISCO_IS_DERP, from_derp) -- Flag bit 0
|
||||
offset = offset + 1
|
||||
-- Parse DERP public key
|
||||
if from_derp then
|
||||
subtree:add(DISCO_DERP_PUB, buffer(offset, 32))
|
||||
end
|
||||
offset = offset + 32
|
||||
|
||||
-- Parse source port
|
||||
subtree:add(DISCO_SRC_PORT, buffer:range(offset, 2):le_uint())
|
||||
offset = offset + 2
|
||||
|
||||
-- Parse source address
|
||||
local addr_len = buffer:range(offset, 2):le_uint()
|
||||
offset = offset + 2
|
||||
if addr_len == 4 then subtree:add(DISCO_SRC_IP_4, buffer:range(offset, addr_len))
|
||||
else subtree:add(DISCO_SRC_IP_6, buffer:range(offset, addr_len))
|
||||
end
|
||||
offset = offset + addr_len
|
||||
|
||||
-- Handover to the actual disco frame dissector
|
||||
offset = offset + 2 -- skip over payload len
|
||||
local data_buffer = buffer:range(offset, packet_length-offset):tvb()
|
||||
Dissector.get("disco"):call(data_buffer, pinfo, tree)
|
||||
end
|
||||
|
||||
ts_dissectors:add(1, tsdisco_meta)
|
||||
|
||||
--
|
||||
-- DISCO frame dissector
|
||||
--
|
||||
tsdisco_frame = Proto("disco", "Tailscale DISCO frame")
|
||||
DISCO_TYPE = ProtoField.string("disco.TYPE", "Message type", base.ASCII)
|
||||
DISCO_VERSION = ProtoField.uint8("disco.VERSION","Protocol version", base.DEC)
|
||||
DISCO_TXID = ProtoField.bytes("disco.TXID", "Transaction ID", base.SPACE)
|
||||
DISCO_NODEKEY = ProtoField.bytes("disco.NODE_KEY", "Node key", base.SPACE)
|
||||
DISCO_PONG_SRC = ProtoField.ipv6("disco.PONG_SRC", "Pong source")
|
||||
DISCO_PONG_SRC_PORT = ProtoField.uint16("disco.PONG_SRC_PORT","Source port", base.DEC)
|
||||
DISCO_UNKNOWN = ProtoField.bytes("disco.UNKNOWN_DATA", "Trailing data", base.SPACE)
|
||||
tsdisco_frame.fields = {DISCO_TYPE, DISCO_VERSION, DISCO_TXID, DISCO_NODEKEY, DISCO_PONG_SRC, DISCO_PONG_SRC_PORT, DISCO_UNKNOWN}
|
||||
|
||||
function tsdisco_frame.dissector(buffer, pinfo, tree)
|
||||
packet_length = buffer:len()
|
||||
local offset = 0
|
||||
local subtree = tree:add(tsdisco_frame, buffer(), "DISCO frame")
|
||||
|
||||
-- Message type
|
||||
local message_type = buffer(offset, 1):le_uint()
|
||||
offset = offset + 1
|
||||
if message_type == 1 then subtree:add(DISCO_TYPE, "Ping")
|
||||
elseif message_type == 2 then subtree:add(DISCO_TYPE, "Pong")
|
||||
elseif message_type == 3 then subtree:add(DISCO_TYPE, "Call me maybe")
|
||||
end
|
||||
|
||||
-- Message version
|
||||
local message_version = buffer(offset, 1):le_uint()
|
||||
offset = offset + 1
|
||||
subtree:add(DISCO_VERSION, message_version)
|
||||
|
||||
-- TXID (Ping / Pong)
|
||||
if message_type == 1 or message_type == 2 then
|
||||
subtree:add(DISCO_TXID, buffer(offset, 12))
|
||||
offset = offset + 12
|
||||
end
|
||||
|
||||
-- NodeKey (Ping)
|
||||
if message_type == 1 then
|
||||
subtree:add(DISCO_NODEKEY, buffer(offset, 32))
|
||||
offset = offset + 32
|
||||
end
|
||||
|
||||
-- Src (Pong)
|
||||
if message_type == 2 then
|
||||
subtree:add(DISCO_PONG_SRC, buffer:range(offset, 16))
|
||||
offset = offset + 16
|
||||
end
|
||||
-- Src port (Pong)
|
||||
if message_type == 2 then
|
||||
subtree:add(DISCO_PONG_SRC_PORT, buffer(offset, 2):le_uint())
|
||||
offset = offset + 2
|
||||
end
|
||||
|
||||
-- TODO(tom): Parse CallMeMaybe.MyNumber
|
||||
|
||||
local trailing = buffer:range(offset, packet_length-offset)
|
||||
if trailing:len() > 0 then
|
||||
subtree:add(DISCO_UNKNOWN, trailing)
|
||||
end
|
||||
end
|
||||
|
||||
ts_dissectors:add(2, tsdisco_frame)
|
@@ -61,7 +61,6 @@ import (
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/util/usermetric"
|
||||
"tailscale.com/wgengine/capture"
|
||||
"tailscale.com/wgengine/wgint"
|
||||
)
|
||||
|
||||
@@ -238,7 +237,7 @@ type Conn struct {
|
||||
stats atomic.Pointer[connstats.Statistics]
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// can be called with a nil argument to uninstall the capture
|
||||
// hook.
|
||||
func (c *Conn) InstallCaptureHook(cb capture.Callback) {
|
||||
func (c *Conn) InstallCaptureHook(cb packet.CaptureCallback) {
|
||||
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
|
||||
// if a capture hook is installed.
|
||||
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)
|
||||
|
@@ -51,7 +51,6 @@ import (
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/util/usermetric"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/capture"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/netlog"
|
||||
@@ -1594,7 +1593,7 @@ var (
|
||||
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.magicConn.InstallCaptureHook(cb)
|
||||
}
|
||||
|
@@ -17,10 +17,10 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/capture"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
@@ -162,7 +162,7 @@ func (e *watchdogEngine) Done() <-chan struct{} {
|
||||
return e.wrap.Done()
|
||||
}
|
||||
|
||||
func (e *watchdogEngine) InstallCaptureHook(cb capture.Callback) {
|
||||
func (e *watchdogEngine) InstallCaptureHook(cb packet.CaptureCallback) {
|
||||
e.wrap.InstallCaptureHook(cb)
|
||||
}
|
||||
|
||||
|
@@ -11,10 +11,10 @@ import (
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/capture"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
@@ -129,5 +129,5 @@ type Engine interface {
|
||||
// InstallCaptureHook registers a function to be called to capture
|
||||
// packets traversing the data path. The hook can be uninstalled by
|
||||
// calling this function with a nil value.
|
||||
InstallCaptureHook(capture.Callback)
|
||||
InstallCaptureHook(packet.CaptureCallback)
|
||||
}
|
||||
|
Reference in New Issue
Block a user