Update
This commit is contained in:
274
vendor/tailscale.com/wgengine/netlog/logger.go
generated
vendored
274
vendor/tailscale.com/wgengine/netlog/logger.go
generated
vendored
@@ -1,274 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package netlog provides a logger that monitors a TUN device and
|
||||
// periodically records any traffic into a log stream.
|
||||
package netlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/connstats"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// pollPeriod specifies how often to poll for network traffic.
|
||||
const pollPeriod = 5 * time.Second
|
||||
|
||||
// Device is an abstraction over a tunnel device or a magic socket.
|
||||
// Both *tstun.Wrapper and *magicsock.Conn implement this interface.
|
||||
type Device interface {
|
||||
SetStatistics(*connstats.Statistics)
|
||||
}
|
||||
|
||||
type noopDevice struct{}
|
||||
|
||||
func (noopDevice) SetStatistics(*connstats.Statistics) {}
|
||||
|
||||
// Logger logs statistics about every connection.
|
||||
// At present, it only logs connections within a tailscale network.
|
||||
// Exit node traffic is not logged for privacy reasons.
|
||||
// The zero value is ready for use.
|
||||
type Logger struct {
|
||||
mu sync.Mutex // protects all fields below
|
||||
|
||||
logger *logtail.Logger
|
||||
stats *connstats.Statistics
|
||||
tun Device
|
||||
sock Device
|
||||
|
||||
addrs map[netip.Addr]bool
|
||||
prefixes map[netip.Prefix]bool
|
||||
}
|
||||
|
||||
// Running reports whether the logger is running.
|
||||
func (nl *Logger) Running() bool {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
return nl.logger != nil
|
||||
}
|
||||
|
||||
var testClient *http.Client
|
||||
|
||||
// Startup starts an asynchronous network logger that monitors
|
||||
// statistics for the provided tun and/or sock device.
|
||||
//
|
||||
// The tun Device captures packets within the tailscale network,
|
||||
// where at least one address is a tailscale IP address.
|
||||
// The source is always from the perspective of the current node.
|
||||
// If one of the other endpoint is not a tailscale IP address,
|
||||
// then it suggests the use of a subnet router or exit node.
|
||||
// For example, when using a subnet router, the source address is
|
||||
// the tailscale IP address of the current node, and
|
||||
// the destination address is an IP address within the subnet range.
|
||||
// In contrast, when acting as a subnet router, the source address is
|
||||
// an IP address within the subnet range, and the destination is a
|
||||
// tailscale IP address that initiated the subnet proxy connection.
|
||||
// In this case, the node acting as a subnet router is acting on behalf
|
||||
// of some remote endpoint within the subnet range.
|
||||
// The tun is used to populate the VirtualTraffic, SubnetTraffic,
|
||||
// and ExitTraffic fields in Message.
|
||||
//
|
||||
// The sock Device captures packets at the magicsock layer.
|
||||
// The source is always a tailscale IP address and the destination
|
||||
// is a non-tailscale IP address to contact for that particular tailscale node.
|
||||
// The IP protocol and source port are always zero.
|
||||
// The sock is used to populated the PhysicalTraffic field in Message.
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, health *health.Tracker, logExitFlowEnabledEnabled bool) error {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
if nl.logger != nil {
|
||||
return fmt.Errorf("network logger already running for %v", nl.logger.PrivateID().Public())
|
||||
}
|
||||
|
||||
// Startup a log stream to Tailscale's logging service.
|
||||
logf := log.Printf
|
||||
httpc := &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, health, logf)}
|
||||
if testClient != nil {
|
||||
httpc = testClient
|
||||
}
|
||||
nl.logger = logtail.NewLogger(logtail.Config{
|
||||
Collection: "tailtraffic.log.tailscale.io",
|
||||
PrivateID: nodeLogID,
|
||||
CopyPrivateID: domainLogID,
|
||||
Stderr: io.Discard,
|
||||
CompressLogs: true,
|
||||
HTTPC: httpc,
|
||||
// TODO(joetsai): Set Buffer? Use an in-memory buffer for now.
|
||||
|
||||
// Include process sequence numbers to identify missing samples.
|
||||
IncludeProcID: true,
|
||||
IncludeProcSequence: true,
|
||||
}, logf)
|
||||
nl.logger.SetSockstatsLabel(sockstats.LabelNetlogLogger)
|
||||
|
||||
// Startup a data structure to track per-connection statistics.
|
||||
// There is a maximum size for individual log messages that logtail
|
||||
// can upload to the Tailscale log service, so stay below this limit.
|
||||
const maxLogSize = 256 << 10
|
||||
const maxConns = (maxLogSize - netlogtype.MaxMessageJSONSize) / netlogtype.MaxConnectionCountsJSONSize
|
||||
nl.stats = connstats.NewStatistics(pollPeriod, maxConns, func(start, end time.Time, virtual, physical map[netlogtype.Connection]netlogtype.Counts) {
|
||||
nl.mu.Lock()
|
||||
addrs := nl.addrs
|
||||
prefixes := nl.prefixes
|
||||
nl.mu.Unlock()
|
||||
recordStatistics(nl.logger, nodeID, start, end, virtual, physical, addrs, prefixes, logExitFlowEnabledEnabled)
|
||||
})
|
||||
|
||||
// Register the connection tracker into the TUN device.
|
||||
if tun == nil {
|
||||
tun = noopDevice{}
|
||||
}
|
||||
nl.tun = tun
|
||||
nl.tun.SetStatistics(nl.stats)
|
||||
|
||||
// Register the connection tracker into magicsock.
|
||||
if sock == nil {
|
||||
sock = noopDevice{}
|
||||
}
|
||||
nl.sock = sock
|
||||
nl.sock.SetStatistics(nl.stats)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordStatistics(logger *logtail.Logger, nodeID tailcfg.StableNodeID, start, end time.Time, connstats, sockStats map[netlogtype.Connection]netlogtype.Counts, addrs map[netip.Addr]bool, prefixes map[netip.Prefix]bool, logExitFlowEnabled bool) {
|
||||
m := netlogtype.Message{NodeID: nodeID, Start: start.UTC(), End: end.UTC()}
|
||||
|
||||
classifyAddr := func(a netip.Addr) (isTailscale, withinRoute bool) {
|
||||
// NOTE: There could be mis-classifications where an address is treated
|
||||
// as a Tailscale IP address because the subnet range overlaps with
|
||||
// the subnet range that Tailscale IP addresses are allocated from.
|
||||
// This should never happen for IPv6, but could happen for IPv4.
|
||||
withinRoute = addrs[a]
|
||||
for p := range prefixes {
|
||||
if p.Contains(a) && p.Bits() > 0 {
|
||||
withinRoute = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return withinRoute && tsaddr.IsTailscaleIP(a), withinRoute && !tsaddr.IsTailscaleIP(a)
|
||||
}
|
||||
|
||||
exitTraffic := make(map[netlogtype.Connection]netlogtype.Counts)
|
||||
for conn, cnts := range connstats {
|
||||
srcIsTailscaleIP, srcWithinSubnet := classifyAddr(conn.Src.Addr())
|
||||
dstIsTailscaleIP, dstWithinSubnet := classifyAddr(conn.Dst.Addr())
|
||||
switch {
|
||||
case srcIsTailscaleIP && dstIsTailscaleIP:
|
||||
m.VirtualTraffic = append(m.VirtualTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
|
||||
case srcWithinSubnet || dstWithinSubnet:
|
||||
m.SubnetTraffic = append(m.SubnetTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
|
||||
default:
|
||||
const anonymize = true
|
||||
if anonymize && !logExitFlowEnabled {
|
||||
// Only preserve the address if it is a Tailscale IP address.
|
||||
srcOrig, dstOrig := conn.Src, conn.Dst
|
||||
conn = netlogtype.Connection{} // scrub everything by default
|
||||
if srcIsTailscaleIP {
|
||||
conn.Src = netip.AddrPortFrom(srcOrig.Addr(), 0)
|
||||
}
|
||||
if dstIsTailscaleIP {
|
||||
conn.Dst = netip.AddrPortFrom(dstOrig.Addr(), 0)
|
||||
}
|
||||
}
|
||||
exitTraffic[conn] = exitTraffic[conn].Add(cnts)
|
||||
}
|
||||
}
|
||||
for conn, cnts := range exitTraffic {
|
||||
m.ExitTraffic = append(m.ExitTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
|
||||
}
|
||||
for conn, cnts := range sockStats {
|
||||
m.PhysicalTraffic = append(m.PhysicalTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
|
||||
}
|
||||
|
||||
if len(m.VirtualTraffic)+len(m.SubnetTraffic)+len(m.ExitTraffic)+len(m.PhysicalTraffic) > 0 {
|
||||
if b, err := json.Marshal(m); err != nil {
|
||||
logger.Logf("json.Marshal error: %v", err)
|
||||
} else {
|
||||
logger.Logf("%s", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeRouteMaps(cfg *router.Config) (addrs map[netip.Addr]bool, prefixes map[netip.Prefix]bool) {
|
||||
addrs = make(map[netip.Addr]bool)
|
||||
for _, p := range cfg.LocalAddrs {
|
||||
if p.IsSingleIP() {
|
||||
addrs[p.Addr()] = true
|
||||
}
|
||||
}
|
||||
prefixes = make(map[netip.Prefix]bool)
|
||||
insertPrefixes := func(rs []netip.Prefix) {
|
||||
for _, p := range rs {
|
||||
if p.IsSingleIP() {
|
||||
addrs[p.Addr()] = true
|
||||
} else {
|
||||
prefixes[p] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
insertPrefixes(cfg.Routes)
|
||||
insertPrefixes(cfg.SubnetRoutes)
|
||||
return addrs, prefixes
|
||||
}
|
||||
|
||||
// ReconfigRoutes configures the network logger with updated routes.
|
||||
// The cfg is used to classify the types of connections captured by
|
||||
// the tun Device passed to Startup.
|
||||
func (nl *Logger) ReconfigRoutes(cfg *router.Config) {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
// TODO(joetsai): There is a race where deleted routes are not known at
|
||||
// the time of extraction. We need to keep old routes around for a bit.
|
||||
nl.addrs, nl.prefixes = makeRouteMaps(cfg)
|
||||
}
|
||||
|
||||
// Shutdown shuts down the network logger.
|
||||
// This attempts to flush out all pending log messages.
|
||||
// Even if an error is returned, the logger is still shut down.
|
||||
func (nl *Logger) Shutdown(ctx context.Context) error {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
if nl.logger == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown in reverse order of Startup.
|
||||
// Do not hold lock while shutting down since this may flush one last time.
|
||||
nl.mu.Unlock()
|
||||
nl.sock.SetStatistics(nil)
|
||||
nl.tun.SetStatistics(nil)
|
||||
err1 := nl.stats.Shutdown(ctx)
|
||||
err2 := nl.logger.Shutdown(ctx)
|
||||
nl.mu.Lock()
|
||||
|
||||
// Purge state.
|
||||
nl.logger = nil
|
||||
nl.stats = nil
|
||||
nl.tun = nil
|
||||
nl.sock = nil
|
||||
nl.addrs = nil
|
||||
nl.prefixes = nil
|
||||
|
||||
return multierr.New(err1, err2)
|
||||
}
|
||||
494
vendor/tailscale.com/wgengine/netlog/netlog.go
generated
vendored
Normal file
494
vendor/tailscale.com/wgengine/netlog/netlog.go
generated
vendored
Normal file
@@ -0,0 +1,494 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_netlog && !ts_omit_logtail
|
||||
|
||||
// Package netlog provides a logger that monitors a TUN device and
|
||||
// periodically records any traffic into a log stream.
|
||||
package netlog
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netlogfunc"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/wgengine/router"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
// pollPeriod specifies how often to poll for network traffic.
|
||||
const pollPeriod = 5 * time.Second
|
||||
|
||||
// Device is an abstraction over a tunnel device or a magic socket.
|
||||
// Both *tstun.Wrapper and *magicsock.Conn implement this interface.
|
||||
type Device interface {
|
||||
SetConnectionCounter(netlogfunc.ConnectionCounter)
|
||||
}
|
||||
|
||||
type noopDevice struct{}
|
||||
|
||||
func (noopDevice) SetConnectionCounter(netlogfunc.ConnectionCounter) {}
|
||||
|
||||
// Logger logs statistics about every connection.
|
||||
// At present, it only logs connections within a tailscale network.
|
||||
// By default, exit node traffic is not logged for privacy reasons
|
||||
// unless the Tailnet administrator opts-into explicit logging.
|
||||
// The zero value is ready for use.
|
||||
type Logger struct {
|
||||
mu syncs.Mutex // protects all fields below
|
||||
logf logger.Logf
|
||||
|
||||
// shutdownLocked shuts down the logger.
|
||||
// The mutex must be held when calling.
|
||||
shutdownLocked func(context.Context) error
|
||||
|
||||
record record // the current record of network connection flows
|
||||
recordLen int // upper bound on JSON length of record
|
||||
recordsChan chan record // set to nil when shutdown
|
||||
flushTimer *time.Timer // fires when record should flush to recordsChan
|
||||
|
||||
// Information about Tailscale nodes.
|
||||
// These are read-only once updated by ReconfigNetworkMap.
|
||||
selfNode nodeUser
|
||||
allNodes map[netip.Addr]nodeUser // includes selfNode; nodeUser values are always valid
|
||||
|
||||
// Information about routes.
|
||||
// These are read-only once updated by ReconfigRoutes.
|
||||
routeAddrs set.Set[netip.Addr]
|
||||
routePrefixes []netip.Prefix
|
||||
}
|
||||
|
||||
// Running reports whether the logger is running.
|
||||
func (nl *Logger) Running() bool {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
return nl.shutdownLocked != nil
|
||||
}
|
||||
|
||||
var testClient *http.Client
|
||||
|
||||
// Startup starts an asynchronous network logger that monitors
|
||||
// statistics for the provided tun and/or sock device.
|
||||
//
|
||||
// The tun [Device] captures packets within the tailscale network,
|
||||
// where at least one address is usually a tailscale IP address.
|
||||
// The source is usually from the perspective of the current node.
|
||||
// If one of the other endpoint is not a tailscale IP address,
|
||||
// then it suggests the use of a subnet router or exit node.
|
||||
// For example, when using a subnet router, the source address is
|
||||
// the tailscale IP address of the current node, and
|
||||
// the destination address is an IP address within the subnet range.
|
||||
// In contrast, when acting as a subnet router, the source address is
|
||||
// an IP address within the subnet range, and the destination is a
|
||||
// tailscale IP address that initiated the subnet proxy connection.
|
||||
// In this case, the node acting as a subnet router is acting on behalf
|
||||
// of some remote endpoint within the subnet range.
|
||||
// The tun is used to populate the VirtualTraffic, SubnetTraffic,
|
||||
// and ExitTraffic fields in [netlogtype.Message].
|
||||
//
|
||||
// The sock [Device] captures packets at the magicsock layer.
|
||||
// The source is always a tailscale IP address and the destination
|
||||
// is a non-tailscale IP address to contact for that particular tailscale node.
|
||||
// The IP protocol and source port are always zero.
|
||||
// The sock is used to populated the PhysicalTraffic field in [netlogtype.Message].
|
||||
//
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func (nl *Logger) Startup(logf logger.Logf, nm *netmap.NetworkMap, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, health *health.Tracker, bus *eventbus.Bus, logExitFlowEnabledEnabled bool) error {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
|
||||
if nl.shutdownLocked != nil {
|
||||
return fmt.Errorf("network logger already running")
|
||||
}
|
||||
nl.selfNode, nl.allNodes = makeNodeMaps(nm)
|
||||
|
||||
// Startup a log stream to Tailscale's logging service.
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
httpc := &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, health, logf)}
|
||||
if testClient != nil {
|
||||
httpc = testClient
|
||||
}
|
||||
logger := logtail.NewLogger(logtail.Config{
|
||||
Collection: "tailtraffic.log.tailscale.io",
|
||||
PrivateID: nodeLogID,
|
||||
CopyPrivateID: domainLogID,
|
||||
Bus: bus,
|
||||
Stderr: io.Discard,
|
||||
CompressLogs: true,
|
||||
HTTPC: httpc,
|
||||
// TODO(joetsai): Set Buffer? Use an in-memory buffer for now.
|
||||
|
||||
// Include process sequence numbers to identify missing samples.
|
||||
IncludeProcID: true,
|
||||
IncludeProcSequence: true,
|
||||
}, logf)
|
||||
logger.SetSockstatsLabel(sockstats.LabelNetlogLogger)
|
||||
|
||||
// Register the connection tracker into the TUN device.
|
||||
tun = cmp.Or[Device](tun, noopDevice{})
|
||||
tun.SetConnectionCounter(nl.updateVirtConn)
|
||||
|
||||
// Register the connection tracker into magicsock.
|
||||
sock = cmp.Or[Device](sock, noopDevice{})
|
||||
sock.SetConnectionCounter(nl.updatePhysConn)
|
||||
|
||||
// Startup a goroutine to record log messages.
|
||||
// This is done asynchronously so that the cost of serializing
|
||||
// the network flow log message never stalls processing of packets.
|
||||
nl.record = record{}
|
||||
nl.recordLen = 0
|
||||
nl.recordsChan = make(chan record, 100)
|
||||
recorderDone := make(chan struct{})
|
||||
go func(recordsChan chan record) {
|
||||
defer close(recorderDone)
|
||||
for rec := range recordsChan {
|
||||
msg := rec.toMessage(false, !logExitFlowEnabledEnabled)
|
||||
if b, err := jsonv2.Marshal(msg, jsontext.AllowInvalidUTF8(true)); err != nil {
|
||||
if nl.logf != nil {
|
||||
nl.logf("netlog: json.Marshal error: %v", err)
|
||||
}
|
||||
} else {
|
||||
logger.Logf("%s", b)
|
||||
}
|
||||
}
|
||||
}(nl.recordsChan)
|
||||
|
||||
// Register the mechanism for shutting down.
|
||||
nl.shutdownLocked = func(ctx context.Context) error {
|
||||
tun.SetConnectionCounter(nil)
|
||||
sock.SetConnectionCounter(nil)
|
||||
|
||||
// Flush and process all pending records.
|
||||
nl.flushRecordLocked()
|
||||
close(nl.recordsChan)
|
||||
nl.recordsChan = nil
|
||||
<-recorderDone
|
||||
recorderDone = nil
|
||||
|
||||
// Try to upload all pending records.
|
||||
err := logger.Shutdown(ctx)
|
||||
|
||||
// Purge state.
|
||||
nl.shutdownLocked = nil
|
||||
nl.selfNode = nodeUser{}
|
||||
nl.allNodes = nil
|
||||
nl.routeAddrs = nil
|
||||
nl.routePrefixes = nil
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
tailscaleServiceIPv4 = tsaddr.TailscaleServiceIP()
|
||||
tailscaleServiceIPv6 = tsaddr.TailscaleServiceIPv6()
|
||||
)
|
||||
|
||||
func (nl *Logger) updateVirtConn(proto ipproto.Proto, src, dst netip.AddrPort, packets, bytes int, recv bool) {
|
||||
// Network logging is defined as traffic between two Tailscale nodes.
|
||||
// Traffic with the internal Tailscale service is not with another node
|
||||
// and should not be logged. It also happens to be a high volume
|
||||
// amount of discrete traffic flows (e.g., DNS lookups).
|
||||
switch dst.Addr() {
|
||||
case tailscaleServiceIPv4, tailscaleServiceIPv6:
|
||||
return
|
||||
}
|
||||
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
|
||||
// Lookup the connection and increment the counts.
|
||||
nl.initRecordLocked()
|
||||
conn := netlogtype.Connection{Proto: proto, Src: src, Dst: dst}
|
||||
cnts, found := nl.record.virtConns[conn]
|
||||
if !found {
|
||||
cnts.connType = nl.addNewVirtConnLocked(conn)
|
||||
}
|
||||
if recv {
|
||||
cnts.RxPackets += uint64(packets)
|
||||
cnts.RxBytes += uint64(bytes)
|
||||
} else {
|
||||
cnts.TxPackets += uint64(packets)
|
||||
cnts.TxBytes += uint64(bytes)
|
||||
}
|
||||
nl.record.virtConns[conn] = cnts
|
||||
}
|
||||
|
||||
// addNewVirtConnLocked adds the first insertion of a physical connection.
|
||||
// The [Logger.mu] must be held.
|
||||
func (nl *Logger) addNewVirtConnLocked(c netlogtype.Connection) connType {
|
||||
// Check whether this is the first insertion of the src and dst node.
|
||||
// If so, compute the additional JSON bytes that would be added
|
||||
// to the record for the node information.
|
||||
var srcNodeLen, dstNodeLen int
|
||||
srcNode, srcSeen := nl.record.seenNodes[c.Src.Addr()]
|
||||
if !srcSeen {
|
||||
srcNode = nl.allNodes[c.Src.Addr()]
|
||||
if srcNode.Valid() {
|
||||
srcNodeLen = srcNode.jsonLen()
|
||||
}
|
||||
}
|
||||
dstNode, dstSeen := nl.record.seenNodes[c.Dst.Addr()]
|
||||
if !dstSeen {
|
||||
dstNode = nl.allNodes[c.Dst.Addr()]
|
||||
if dstNode.Valid() {
|
||||
dstNodeLen = dstNode.jsonLen()
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the additional [netlogtype.ConnectionCounts]
|
||||
// and [netlogtype.Node] information would exceed [maxLogSize].
|
||||
if nl.recordLen+netlogtype.MaxConnectionCountsJSONSize+srcNodeLen+dstNodeLen > maxLogSize {
|
||||
nl.flushRecordLocked()
|
||||
nl.initRecordLocked()
|
||||
}
|
||||
|
||||
// Insert newly seen src and/or dst nodes.
|
||||
if !srcSeen && srcNode.Valid() {
|
||||
nl.record.seenNodes[c.Src.Addr()] = srcNode
|
||||
}
|
||||
if !dstSeen && dstNode.Valid() {
|
||||
nl.record.seenNodes[c.Dst.Addr()] = dstNode
|
||||
}
|
||||
nl.recordLen += netlogtype.MaxConnectionCountsJSONSize + srcNodeLen + dstNodeLen
|
||||
|
||||
// Classify the traffic type.
|
||||
var srcIsSelfNode bool
|
||||
if nl.selfNode.Valid() {
|
||||
srcIsSelfNode = nl.selfNode.Addresses().ContainsFunc(func(p netip.Prefix) bool {
|
||||
return c.Src.Addr() == p.Addr() && p.IsSingleIP()
|
||||
})
|
||||
}
|
||||
switch {
|
||||
case srcIsSelfNode && dstNode.Valid():
|
||||
return virtualTraffic
|
||||
case srcIsSelfNode:
|
||||
// TODO: Should we swap src for the node serving as the proxy?
|
||||
// It is relatively useless always using the self IP address.
|
||||
if nl.withinRoutesLocked(c.Dst.Addr()) {
|
||||
return subnetTraffic // a client using another subnet router
|
||||
} else {
|
||||
return exitTraffic // a client using exit an exit node
|
||||
}
|
||||
case dstNode.Valid():
|
||||
if nl.withinRoutesLocked(c.Src.Addr()) {
|
||||
return subnetTraffic // serving as a subnet router
|
||||
} else {
|
||||
return exitTraffic // serving as an exit node
|
||||
}
|
||||
default:
|
||||
return unknownTraffic
|
||||
}
|
||||
}
|
||||
|
||||
func (nl *Logger) updatePhysConn(proto ipproto.Proto, src, dst netip.AddrPort, packets, bytes int, recv bool) {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
|
||||
// Lookup the connection and increment the counts.
|
||||
nl.initRecordLocked()
|
||||
conn := netlogtype.Connection{Proto: proto, Src: src, Dst: dst}
|
||||
cnts, found := nl.record.physConns[conn]
|
||||
if !found {
|
||||
nl.addNewPhysConnLocked(conn)
|
||||
}
|
||||
if recv {
|
||||
cnts.RxPackets += uint64(packets)
|
||||
cnts.RxBytes += uint64(bytes)
|
||||
} else {
|
||||
cnts.TxPackets += uint64(packets)
|
||||
cnts.TxBytes += uint64(bytes)
|
||||
}
|
||||
nl.record.physConns[conn] = cnts
|
||||
}
|
||||
|
||||
// addNewPhysConnLocked adds the first insertion of a physical connection.
|
||||
// The [Logger.mu] must be held.
|
||||
func (nl *Logger) addNewPhysConnLocked(c netlogtype.Connection) {
|
||||
// Check whether this is the first insertion of the src node.
|
||||
var srcNodeLen int
|
||||
srcNode, srcSeen := nl.record.seenNodes[c.Src.Addr()]
|
||||
if !srcSeen {
|
||||
srcNode = nl.allNodes[c.Src.Addr()]
|
||||
if srcNode.Valid() {
|
||||
srcNodeLen = srcNode.jsonLen()
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the additional [netlogtype.ConnectionCounts]
|
||||
// and [netlogtype.Node] information would exceed [maxLogSize].
|
||||
if nl.recordLen+netlogtype.MaxConnectionCountsJSONSize+srcNodeLen > maxLogSize {
|
||||
nl.flushRecordLocked()
|
||||
nl.initRecordLocked()
|
||||
}
|
||||
|
||||
// Insert newly seen src and/or dst nodes.
|
||||
if !srcSeen && srcNode.Valid() {
|
||||
nl.record.seenNodes[c.Src.Addr()] = srcNode
|
||||
}
|
||||
nl.recordLen += netlogtype.MaxConnectionCountsJSONSize + srcNodeLen
|
||||
}
|
||||
|
||||
// initRecordLocked initialize the current record if uninitialized.
|
||||
// The [Logger.mu] must be held.
|
||||
func (nl *Logger) initRecordLocked() {
|
||||
if nl.recordLen != 0 {
|
||||
return
|
||||
}
|
||||
nl.record = record{
|
||||
selfNode: nl.selfNode,
|
||||
start: time.Now().UTC(),
|
||||
seenNodes: make(map[netip.Addr]nodeUser),
|
||||
virtConns: make(map[netlogtype.Connection]countsType),
|
||||
physConns: make(map[netlogtype.Connection]netlogtype.Counts),
|
||||
}
|
||||
nl.recordLen = netlogtype.MinMessageJSONSize + nl.selfNode.jsonLen()
|
||||
|
||||
// Start a time to auto-flush the record.
|
||||
// Avoid tickers since continually waking up a goroutine
|
||||
// is expensive on battery powered devices.
|
||||
nl.flushTimer = time.AfterFunc(pollPeriod, func() {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
if !nl.record.start.IsZero() && time.Since(nl.record.start) > pollPeriod/2 {
|
||||
nl.flushRecordLocked()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// flushRecordLocked flushes the current record if initialized.
|
||||
// The [Logger.mu] must be held.
|
||||
func (nl *Logger) flushRecordLocked() {
|
||||
if nl.recordLen == 0 {
|
||||
return
|
||||
}
|
||||
nl.record.end = time.Now().UTC()
|
||||
if nl.recordsChan != nil {
|
||||
select {
|
||||
case nl.recordsChan <- nl.record:
|
||||
default:
|
||||
if nl.logf != nil {
|
||||
nl.logf("netlog: dropped record due to processing backlog")
|
||||
}
|
||||
}
|
||||
}
|
||||
if nl.flushTimer != nil {
|
||||
nl.flushTimer.Stop()
|
||||
nl.flushTimer = nil
|
||||
}
|
||||
nl.record = record{}
|
||||
nl.recordLen = 0
|
||||
}
|
||||
|
||||
func makeNodeMaps(nm *netmap.NetworkMap) (selfNode nodeUser, allNodes map[netip.Addr]nodeUser) {
|
||||
if nm == nil {
|
||||
return
|
||||
}
|
||||
allNodes = make(map[netip.Addr]nodeUser)
|
||||
if nm.SelfNode.Valid() {
|
||||
selfNode = nodeUser{nm.SelfNode, nm.UserProfiles[nm.SelfNode.User()]}
|
||||
for _, addr := range nm.SelfNode.Addresses().All() {
|
||||
if addr.IsSingleIP() {
|
||||
allNodes[addr.Addr()] = selfNode
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, peer := range nm.Peers {
|
||||
if peer.Valid() {
|
||||
for _, addr := range peer.Addresses().All() {
|
||||
if addr.IsSingleIP() {
|
||||
allNodes[addr.Addr()] = nodeUser{peer, nm.UserProfiles[peer.User()]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selfNode, allNodes
|
||||
}
|
||||
|
||||
// ReconfigNetworkMap configures the network logger with an updated netmap.
|
||||
func (nl *Logger) ReconfigNetworkMap(nm *netmap.NetworkMap) {
|
||||
selfNode, allNodes := makeNodeMaps(nm) // avoid holding lock while making maps
|
||||
nl.mu.Lock()
|
||||
nl.selfNode, nl.allNodes = selfNode, allNodes
|
||||
nl.mu.Unlock()
|
||||
}
|
||||
|
||||
func makeRouteMaps(cfg *router.Config) (addrs set.Set[netip.Addr], prefixes []netip.Prefix) {
|
||||
addrs = make(set.Set[netip.Addr])
|
||||
insertPrefixes := func(rs []netip.Prefix) {
|
||||
for _, p := range rs {
|
||||
if p.IsSingleIP() {
|
||||
addrs.Add(p.Addr())
|
||||
} else {
|
||||
prefixes = append(prefixes, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
insertPrefixes(cfg.LocalAddrs)
|
||||
insertPrefixes(cfg.Routes)
|
||||
insertPrefixes(cfg.SubnetRoutes)
|
||||
return addrs, prefixes
|
||||
}
|
||||
|
||||
// ReconfigRoutes configures the network logger with updated routes.
|
||||
// The cfg is used to classify the types of connections captured by
|
||||
// the tun Device passed to Startup.
|
||||
func (nl *Logger) ReconfigRoutes(cfg *router.Config) {
|
||||
addrs, prefixes := makeRouteMaps(cfg) // avoid holding lock while making maps
|
||||
nl.mu.Lock()
|
||||
nl.routeAddrs, nl.routePrefixes = addrs, prefixes
|
||||
nl.mu.Unlock()
|
||||
}
|
||||
|
||||
// withinRoutesLocked reports whether a is within the configured routes,
|
||||
// which should only contain Tailscale addresses and subnet routes.
|
||||
// The [Logger.mu] must be held.
|
||||
func (nl *Logger) withinRoutesLocked(a netip.Addr) bool {
|
||||
if nl.routeAddrs.Contains(a) {
|
||||
return true
|
||||
}
|
||||
for _, p := range nl.routePrefixes {
|
||||
if p.Contains(a) && p.Bits() > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Shutdown shuts down the network logger.
|
||||
// This attempts to flush out all pending log messages.
|
||||
// Even if an error is returned, the logger is still shut down.
|
||||
func (nl *Logger) Shutdown(ctx context.Context) error {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
if nl.shutdownLocked == nil {
|
||||
return nil
|
||||
}
|
||||
return nl.shutdownLocked(ctx)
|
||||
}
|
||||
14
vendor/tailscale.com/wgengine/netlog/netlog_omit.go
generated
vendored
Normal file
14
vendor/tailscale.com/wgengine/netlog/netlog_omit.go
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_netlog || ts_omit_logtail
|
||||
|
||||
package netlog
|
||||
|
||||
type Logger struct{}
|
||||
|
||||
func (*Logger) Startup(...any) error { return nil }
|
||||
func (*Logger) Running() bool { return false }
|
||||
func (*Logger) Shutdown(any) error { return nil }
|
||||
func (*Logger) ReconfigNetworkMap(any) {}
|
||||
func (*Logger) ReconfigRoutes(any) {}
|
||||
218
vendor/tailscale.com/wgengine/netlog/record.go
generated
vendored
Normal file
218
vendor/tailscale.com/wgengine/netlog/record.go
generated
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_netlog && !ts_omit_logtail
|
||||
|
||||
package netlog
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/bools"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// maxLogSize is the maximum number of bytes for a log message.
|
||||
const maxLogSize = 256 << 10
|
||||
|
||||
// record is the in-memory representation of a [netlogtype.Message].
|
||||
// It uses maps to efficiently look-up addresses and connections.
|
||||
// In contrast, [netlogtype.Message] is designed to be JSON serializable,
|
||||
// where complex keys types are not well support in JSON objects.
|
||||
type record struct {
|
||||
selfNode nodeUser
|
||||
|
||||
start time.Time
|
||||
end time.Time
|
||||
|
||||
seenNodes map[netip.Addr]nodeUser
|
||||
|
||||
virtConns map[netlogtype.Connection]countsType
|
||||
physConns map[netlogtype.Connection]netlogtype.Counts
|
||||
}
|
||||
|
||||
// nodeUser is a node with additional user profile information.
|
||||
type nodeUser struct {
|
||||
tailcfg.NodeView
|
||||
user tailcfg.UserProfileView // UserProfileView for NodeView.User
|
||||
}
|
||||
|
||||
// countsType is a counts with classification information about the connection.
|
||||
type countsType struct {
|
||||
netlogtype.Counts
|
||||
connType connType
|
||||
}
|
||||
|
||||
type connType uint8
|
||||
|
||||
const (
|
||||
unknownTraffic connType = iota
|
||||
virtualTraffic
|
||||
subnetTraffic
|
||||
exitTraffic
|
||||
)
|
||||
|
||||
// toMessage converts a [record] into a [netlogtype.Message].
|
||||
func (r record) toMessage(excludeNodeInfo, anonymizeExitTraffic bool) netlogtype.Message {
|
||||
if !r.selfNode.Valid() {
|
||||
return netlogtype.Message{}
|
||||
}
|
||||
|
||||
m := netlogtype.Message{
|
||||
NodeID: r.selfNode.StableID(),
|
||||
Start: r.start.UTC(),
|
||||
End: r.end.UTC(),
|
||||
}
|
||||
|
||||
// Convert node fields.
|
||||
if !excludeNodeInfo {
|
||||
m.SrcNode = r.selfNode.toNode()
|
||||
seenIDs := set.Of(r.selfNode.ID())
|
||||
for _, node := range r.seenNodes {
|
||||
if _, ok := seenIDs[node.ID()]; !ok && node.Valid() {
|
||||
m.DstNodes = append(m.DstNodes, node.toNode())
|
||||
seenIDs.Add(node.ID())
|
||||
}
|
||||
}
|
||||
slices.SortFunc(m.DstNodes, func(x, y netlogtype.Node) int {
|
||||
return cmp.Compare(x.NodeID, y.NodeID)
|
||||
})
|
||||
}
|
||||
|
||||
// Converter traffic fields.
|
||||
anonymizedExitTraffic := make(map[netlogtype.Connection]netlogtype.Counts)
|
||||
for conn, cnts := range r.virtConns {
|
||||
switch cnts.connType {
|
||||
case virtualTraffic:
|
||||
m.VirtualTraffic = append(m.VirtualTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
|
||||
case subnetTraffic:
|
||||
m.SubnetTraffic = append(m.SubnetTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
|
||||
default:
|
||||
if anonymizeExitTraffic {
|
||||
conn = netlogtype.Connection{ // scrub the IP protocol type
|
||||
Src: netip.AddrPortFrom(conn.Src.Addr(), 0), // scrub the port number
|
||||
Dst: netip.AddrPortFrom(conn.Dst.Addr(), 0), // scrub the port number
|
||||
}
|
||||
if !r.seenNodes[conn.Src.Addr()].Valid() {
|
||||
conn.Src = netip.AddrPort{} // not a Tailscale node, so scrub the address
|
||||
}
|
||||
if !r.seenNodes[conn.Dst.Addr()].Valid() {
|
||||
conn.Dst = netip.AddrPort{} // not a Tailscale node, so scrub the address
|
||||
}
|
||||
anonymizedExitTraffic[conn] = anonymizedExitTraffic[conn].Add(cnts.Counts)
|
||||
continue
|
||||
}
|
||||
m.ExitTraffic = append(m.ExitTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
|
||||
}
|
||||
}
|
||||
for conn, cnts := range anonymizedExitTraffic {
|
||||
m.ExitTraffic = append(m.ExitTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
|
||||
}
|
||||
for conn, cnts := range r.physConns {
|
||||
m.PhysicalTraffic = append(m.PhysicalTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
|
||||
}
|
||||
|
||||
// Sort the connections for deterministic results.
|
||||
slices.SortFunc(m.VirtualTraffic, compareConnCnts)
|
||||
slices.SortFunc(m.SubnetTraffic, compareConnCnts)
|
||||
slices.SortFunc(m.ExitTraffic, compareConnCnts)
|
||||
slices.SortFunc(m.PhysicalTraffic, compareConnCnts)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func compareConnCnts(x, y netlogtype.ConnectionCounts) int {
|
||||
return cmp.Or(
|
||||
netip.AddrPort.Compare(x.Src, y.Src),
|
||||
netip.AddrPort.Compare(x.Dst, y.Dst),
|
||||
cmp.Compare(x.Proto, y.Proto))
|
||||
}
|
||||
|
||||
// jsonLen computes an upper-bound on the size of the JSON representation.
|
||||
func (nu nodeUser) jsonLen() (n int) {
|
||||
if !nu.Valid() {
|
||||
return len(`{"nodeId":""}`)
|
||||
}
|
||||
n += len(`{}`)
|
||||
n += len(`"nodeId":`) + jsonQuotedLen(string(nu.StableID())) + len(`,`)
|
||||
if len(nu.Name()) > 0 {
|
||||
n += len(`"name":`) + jsonQuotedLen(nu.Name()) + len(`,`)
|
||||
}
|
||||
if nu.Addresses().Len() > 0 {
|
||||
n += len(`"addresses":[]`)
|
||||
for _, addr := range nu.Addresses().All() {
|
||||
n += bools.IfElse(addr.Addr().Is4(), len(`"255.255.255.255"`), len(`"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"`)) + len(",")
|
||||
}
|
||||
}
|
||||
if nu.Hostinfo().Valid() && len(nu.Hostinfo().OS()) > 0 {
|
||||
n += len(`"os":`) + jsonQuotedLen(nu.Hostinfo().OS()) + len(`,`)
|
||||
}
|
||||
if nu.Tags().Len() > 0 {
|
||||
n += len(`"tags":[]`)
|
||||
for _, tag := range nu.Tags().All() {
|
||||
n += jsonQuotedLen(tag) + len(",")
|
||||
}
|
||||
} else if nu.user.Valid() && nu.user.ID() == nu.User() && len(nu.user.LoginName()) > 0 {
|
||||
n += len(`"user":`) + jsonQuotedLen(nu.user.LoginName()) + len(",")
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// toNode converts the [nodeUser] into a [netlogtype.Node].
|
||||
func (nu nodeUser) toNode() netlogtype.Node {
|
||||
if !nu.Valid() {
|
||||
return netlogtype.Node{}
|
||||
}
|
||||
n := netlogtype.Node{
|
||||
NodeID: nu.StableID(),
|
||||
Name: strings.TrimSuffix(nu.Name(), "."),
|
||||
}
|
||||
var ipv4, ipv6 netip.Addr
|
||||
for _, addr := range nu.Addresses().All() {
|
||||
switch {
|
||||
case addr.IsSingleIP() && addr.Addr().Is4():
|
||||
ipv4 = addr.Addr()
|
||||
case addr.IsSingleIP() && addr.Addr().Is6():
|
||||
ipv6 = addr.Addr()
|
||||
}
|
||||
}
|
||||
n.Addresses = []netip.Addr{ipv4, ipv6}
|
||||
n.Addresses = slices.DeleteFunc(n.Addresses, func(a netip.Addr) bool { return !a.IsValid() })
|
||||
if nu.Hostinfo().Valid() {
|
||||
n.OS = nu.Hostinfo().OS()
|
||||
}
|
||||
if nu.Tags().Len() > 0 {
|
||||
n.Tags = nu.Tags().AsSlice()
|
||||
slices.Sort(n.Tags)
|
||||
n.Tags = slices.Compact(n.Tags)
|
||||
} else if nu.user.Valid() && nu.user.ID() == nu.User() {
|
||||
n.User = nu.user.LoginName()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// jsonQuotedLen computes the length of the JSON serialization of s
|
||||
// according to [jsontext.AppendQuote].
|
||||
func jsonQuotedLen(s string) int {
|
||||
n := len(`"`) + len(s) + len(`"`)
|
||||
for i, r := range s {
|
||||
switch {
|
||||
case r == '\b', r == '\t', r == '\n', r == '\f', r == '\r', r == '"', r == '\\':
|
||||
n += len(`\X`) - 1
|
||||
case r < ' ':
|
||||
n += len(`\uXXXX`) - 1
|
||||
case r == utf8.RuneError:
|
||||
if _, m := utf8.DecodeRuneInString(s[i:]); m == 1 { // exactly an invalid byte
|
||||
n += len("<22>") - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
Reference in New Issue
Block a user