mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-07 08:07:42 +00:00
wgengine: start logging DISCO frames to pcap stream
Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
parent
da75e49223
commit
2ca6dd1f1d
@ -69,12 +69,6 @@
|
|||||||
// It must not hold onto the packet struct, as its backing storage will be reused.
|
// It must not hold onto the packet struct, as its backing storage will be reused.
|
||||||
type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response
|
type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response
|
||||||
|
|
||||||
// CaptureFunc describes a callback 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 CaptureFunc func(capture.Path, time.Time, []byte)
|
|
||||||
|
|
||||||
// Wrapper augments a tun.Device with packet filtering and injection.
|
// Wrapper augments a tun.Device with packet filtering and injection.
|
||||||
type Wrapper struct {
|
type Wrapper struct {
|
||||||
logf logger.Logf
|
logf logger.Logf
|
||||||
@ -181,7 +175,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[CaptureFunc]
|
captureHook syncs.AtomicValue[capture.Callback]
|
||||||
}
|
}
|
||||||
|
|
||||||
// tunInjectedRead is an injected packet pretending to be a tun.Read().
|
// tunInjectedRead is an injected packet pretending to be a tun.Read().
|
||||||
@ -942,6 +936,6 @@ func (t *Wrapper) SetStatistics(stats *connstats.Statistics) {
|
|||||||
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 CaptureFunc) {
|
func (t *Wrapper) InstallCaptureHook(cb capture.Callback) {
|
||||||
t.captureHook.Store(cb)
|
t.captureHook.Store(cb)
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,13 @@
|
|||||||
//go:embed ts-dissector.lua
|
//go:embed ts-dissector.lua
|
||||||
var DissectorLua string
|
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)
|
||||||
|
|
||||||
var bufferPool = sync.Pool{
|
var bufferPool = sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
return new(bytes.Buffer)
|
return new(bytes.Buffer)
|
||||||
@ -65,6 +72,9 @@ func writePktHeader(w *bytes.Buffer, when time.Time, length int) {
|
|||||||
// SynthesizedToPeer indicates the packet was generated from within tailscaled,
|
// SynthesizedToPeer indicates the packet was generated from within tailscaled,
|
||||||
// and is being routed to a remote Wireguard peer.
|
// and is being routed to a remote Wireguard peer.
|
||||||
SynthesizedToPeer Path = 3
|
SynthesizedToPeer Path = 3
|
||||||
|
|
||||||
|
// PathDisco indicates the packet is information about a disco frame.
|
||||||
|
PathDisco Path = 254
|
||||||
)
|
)
|
||||||
|
|
||||||
// New creates a new capture sink.
|
// New creates a new capture sink.
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
function hasbit(x, p)
|
||||||
|
return x % (p + p) >= p
|
||||||
|
end
|
||||||
|
|
||||||
tsdebug_ll = Proto("tsdebug", "Tailscale debug")
|
tsdebug_ll = Proto("tsdebug", "Tailscale debug")
|
||||||
PATH = ProtoField.string("tsdebug.PATH","PATH", base.ASCII)
|
PATH = ProtoField.string("tsdebug.PATH","PATH", base.ASCII)
|
||||||
tsdebug_ll.fields = {PATH}
|
tsdebug_ll.fields = {PATH}
|
||||||
@ -14,14 +18,134 @@ function tsdebug_ll.dissector(buffer, pinfo, tree)
|
|||||||
elseif path_id == 1 then subtree:add(PATH, "FromPeer")
|
elseif path_id == 1 then subtree:add(PATH, "FromPeer")
|
||||||
elseif path_id == 2 then subtree:add(PATH, "Synthesized (Inbound / ToLocal)")
|
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 == 3 then subtree:add(PATH, "Synthesized (Outbound / ToPeer)")
|
||||||
|
elseif path_id == 254 then subtree:add(PATH, "Disco frame")
|
||||||
end
|
end
|
||||||
offset = offset + 2
|
offset = offset + 2
|
||||||
|
|
||||||
-- -- Handover rest of data to ip dissector
|
-- -- Handover rest of data to lower-level dissector
|
||||||
local data_buffer = buffer:range(offset, packet_length-offset):tvb()
|
local data_buffer = buffer:range(offset, packet_length-offset):tvb()
|
||||||
Dissector.get("ip"):call(data_buffer, pinfo, tree)
|
if path_id == 254 then
|
||||||
|
Dissector.get("tsdisco"):call(data_buffer, pinfo, tree)
|
||||||
|
else
|
||||||
|
Dissector.get("ip"):call(data_buffer, pinfo, tree)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Install the dissector on link-layer ID 147 (User-defined protocol 0)
|
-- Install the dissector on link-layer ID 147 (User-defined protocol 0)
|
||||||
local eth_table = DissectorTable.get("wtap_encap")
|
local eth_table = DissectorTable.get("wtap_encap")
|
||||||
eth_table:add(wtap.USER0, tsdebug_ll)
|
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.ipv4("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)
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
@ -62,6 +63,7 @@
|
|||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
"tailscale.com/util/uniq"
|
"tailscale.com/util/uniq"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
|
"tailscale.com/wgengine/capture"
|
||||||
"tailscale.com/wgengine/monitor"
|
"tailscale.com/wgengine/monitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -348,6 +350,9 @@ type Conn struct {
|
|||||||
// stats maintains per-connection counters.
|
// stats maintains per-connection counters.
|
||||||
stats atomic.Pointer[connstats.Statistics]
|
stats atomic.Pointer[connstats.Statistics]
|
||||||
|
|
||||||
|
// captureHook, if non-nil, is the pcap logging callback when capturing.
|
||||||
|
captureHook syncs.AtomicValue[capture.Callback]
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// mu guards all following fields; see userspaceEngine lock
|
// mu guards all following fields; see userspaceEngine lock
|
||||||
// ordering rules against the engine. For derphttp, mu must
|
// ordering rules against the engine. For derphttp, mu must
|
||||||
@ -664,6 +669,14 @@ func NewConn(opts Options) (*Conn, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstallCaptureHook installs a callback which is called to
|
||||||
|
// 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) {
|
||||||
|
c.captureHook.Store(cb)
|
||||||
|
}
|
||||||
|
|
||||||
// ignoreSTUNPackets sets a STUN packet processing func that does nothing.
|
// ignoreSTUNPackets sets a STUN packet processing func that does nothing.
|
||||||
func (c *Conn) ignoreSTUNPackets() {
|
func (c *Conn) ignoreSTUNPackets() {
|
||||||
c.stunReceiveFunc.Store(func([]byte, netip.AddrPort) {})
|
c.stunReceiveFunc.Store(func([]byte, netip.AddrPort) {})
|
||||||
@ -2017,6 +2030,34 @@ func (c *Conn) sendDiscoMessage(dst netip.AddrPort, dstKey key.NodePublic, dstDi
|
|||||||
return sent, err
|
return sent, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discoPcapFrame marshals the bytes for a pcap record that describe a
|
||||||
|
// disco frame.
|
||||||
|
//
|
||||||
|
// Warning: Alloc garbage. Acceptable while capturing.
|
||||||
|
func discoPcapFrame(src netip.AddrPort, derpNodeSrc key.NodePublic, payload []byte) []byte {
|
||||||
|
var (
|
||||||
|
b bytes.Buffer
|
||||||
|
flag uint8
|
||||||
|
)
|
||||||
|
b.Grow(128) // Most disco frames will probably be smaller than this.
|
||||||
|
|
||||||
|
if src.Addr() == derpMagicIPAddr {
|
||||||
|
flag |= 0x01
|
||||||
|
}
|
||||||
|
b.WriteByte(flag) // 1b: flag
|
||||||
|
|
||||||
|
derpSrc := derpNodeSrc.Raw32()
|
||||||
|
b.Write(derpSrc[:]) // 32b: derp public key
|
||||||
|
binary.Write(&b, binary.LittleEndian, uint16(src.Port())) // 2b: port
|
||||||
|
addr, _ := src.Addr().MarshalBinary()
|
||||||
|
binary.Write(&b, binary.LittleEndian, uint16(len(addr))) // 2b: len(addr)
|
||||||
|
b.Write(addr) // Xb: addr
|
||||||
|
binary.Write(&b, binary.LittleEndian, uint16(len(payload))) // 2b: len(payload)
|
||||||
|
b.Write(payload) // Xb: payload
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
// handleDiscoMessage handles a discovery message and reports whether
|
// handleDiscoMessage handles a discovery message and reports whether
|
||||||
// msg was a Tailscale inter-node discovery message.
|
// msg was a Tailscale inter-node discovery message.
|
||||||
//
|
//
|
||||||
@ -2099,6 +2140,12 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netip.AddrPort, derpNodeSrc ke
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(), discoPcapFrame(src, derpNodeSrc, payload))
|
||||||
|
}
|
||||||
|
|
||||||
dm, err := disco.Parse(payload)
|
dm, err := disco.Parse(payload)
|
||||||
if debugDisco() {
|
if debugDisco() {
|
||||||
c.logf("magicsock: disco: disco.Parse = %T, %v", dm, err)
|
c.logf("magicsock: disco: disco.Parse = %T, %v", dm, err)
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
"tailscale.com/util/deephash"
|
"tailscale.com/util/deephash"
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
|
"tailscale.com/wgengine/capture"
|
||||||
"tailscale.com/wgengine/filter"
|
"tailscale.com/wgengine/filter"
|
||||||
"tailscale.com/wgengine/magicsock"
|
"tailscale.com/wgengine/magicsock"
|
||||||
"tailscale.com/wgengine/monitor"
|
"tailscale.com/wgengine/monitor"
|
||||||
@ -1580,6 +1581,7 @@ func (ls fwdDNSLinkSelector) PickLink(ip netip.Addr) (linkName string) {
|
|||||||
metricNumMinorChanges = clientmetric.NewCounter("wgengine_minor_changes")
|
metricNumMinorChanges = clientmetric.NewCounter("wgengine_minor_changes")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *userspaceEngine) InstallCaptureHook(cb CaptureCallback) {
|
func (e *userspaceEngine) InstallCaptureHook(cb capture.Callback) {
|
||||||
e.tundev.InstallCaptureHook(tstun.CaptureFunc(cb))
|
e.tundev.InstallCaptureHook(cb)
|
||||||
|
e.magicConn.InstallCaptureHook(cb)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"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/magicsock"
|
"tailscale.com/wgengine/magicsock"
|
||||||
"tailscale.com/wgengine/monitor"
|
"tailscale.com/wgengine/monitor"
|
||||||
@ -201,6 +202,6 @@ func (e *watchdogEngine) Wait() {
|
|||||||
e.wrap.Wait()
|
e.wrap.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *watchdogEngine) InstallCaptureHook(cb CaptureCallback) {
|
func (e *watchdogEngine) InstallCaptureHook(cb capture.Callback) {
|
||||||
e.wrap.InstallCaptureHook(cb)
|
e.wrap.InstallCaptureHook(cb)
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,6 @@ type Status struct {
|
|||||||
// into network map updates.
|
// into network map updates.
|
||||||
type NetworkMapCallback func(*netmap.NetworkMap)
|
type NetworkMapCallback func(*netmap.NetworkMap)
|
||||||
|
|
||||||
// CaptureCallback is the type used to record packets when
|
|
||||||
// debugging packet-capture. This function 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(capture.Path, time.Time, []byte)
|
|
||||||
|
|
||||||
// someHandle is allocated so its pointer address acts as a unique
|
// someHandle is allocated so its pointer address acts as a unique
|
||||||
// map key handle. (It needs to have non-zero size for Go to guarantee
|
// map key handle. (It needs to have non-zero size for Go to guarantee
|
||||||
// the pointer is unique.)
|
// the pointer is unique.)
|
||||||
@ -182,5 +176,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(CaptureCallback)
|
InstallCaptureHook(capture.Callback)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user