net/connstats: invert network logging data flow (#6272)

Previously, tstun.Wrapper and magicsock.Conn managed their
own statistics data structure and relied on an external call to
Extract to extract (and reset) the statistics.
This makes it difficult to ensure a maximum size on the statistics
as the caller has no introspection into whether the number
of unique connections is getting too large.

Invert the control flow such that a *connstats.Statistics
is registered with tstun.Wrapper and magicsock.Conn.
Methods on non-nil *connstats.Statistics are called for every packet.
This allows the implementation of connstats.Statistics (in the future)
to better control when it needs to flush to ensure
bounds on maximum sizes.

The value registered into tstun.Wrapper and magicsock.Conn could
be an interface, but that has two performance detriments:

1. Method calls on interface values are more expensive since
they must go through a virtual method dispatch.

2. The implementation would need a sync.Mutex to protect the
statistics value instead of using an atomic.Pointer.

Given that methods on constats.Statistics are called for every packet,
we want reduce the CPU cost on this hot path.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
Joe Tsai
2022-11-28 15:59:33 -08:00
committed by GitHub
parent 35c10373b5
commit 2e5d08ec4f
10 changed files with 172 additions and 252 deletions

View File

@@ -22,15 +22,14 @@ import (
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/disco"
"tailscale.com/net/connstats"
"tailscale.com/net/packet"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tunstats"
"tailscale.com/syncs"
"tailscale.com/tstime/mono"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netlogtype"
"tailscale.com/util/clientmetric"
"tailscale.com/wgengine/filter"
)
@@ -170,10 +169,7 @@ type Wrapper struct {
disableTSMPRejected bool
// stats maintains per-connection counters.
stats struct {
enabled atomic.Bool
tunstats.Statistics
}
stats atomic.Pointer[connstats.Statistics]
}
// tunReadResult is the result of a TUN read, or an injected result pretending to be a TUN read.
@@ -568,8 +564,8 @@ func (t *Wrapper) Read(buf []byte, offset int) (int, error) {
}
}
if t.stats.enabled.Load() {
t.stats.UpdateTx(buf[offset:][:n])
if stats := t.stats.Load(); stats != nil {
stats.UpdateTxVirtual(buf[offset:][:n])
}
t.noteActivity()
return n, nil
@@ -701,8 +697,8 @@ func (t *Wrapper) Write(buf []byte, offset int) (int, error) {
}
func (t *Wrapper) tdevWrite(buf []byte, offset int) (int, error) {
if t.stats.enabled.Load() {
t.stats.UpdateRx(buf[offset:])
if stats := t.stats.Load(); stats != nil {
stats.UpdateRxVirtual(buf[offset:])
}
if t.isTAP {
return t.tapWrite(buf, offset)
@@ -843,18 +839,10 @@ func (t *Wrapper) Unwrap() tun.Device {
return t.tdev
}
// SetStatisticsEnabled enables per-connections packet counters.
// Disabling statistics gathering does not reset the counters.
// ExtractStatistics must be called to reset the counters and
// be periodically called while enabled to avoid unbounded memory use.
func (t *Wrapper) SetStatisticsEnabled(enable bool) {
t.stats.enabled.Store(enable)
}
// ExtractStatistics extracts and resets the counters for all active connections.
// It must be called periodically otherwise the memory used is unbounded.
func (t *Wrapper) ExtractStatistics() map[netlogtype.Connection]netlogtype.Counts {
return t.stats.Extract()
// SetStatistics specifies a per-connection statistics aggregator.
// Nil may be specified to disable statistics gathering.
func (t *Wrapper) SetStatistics(stats *connstats.Statistics) {
t.stats.Store(stats)
}
var (

View File

@@ -19,6 +19,7 @@ import (
"go4.org/netipx"
"golang.zx2c4.com/wireguard/tun/tuntest"
"tailscale.com/disco"
"tailscale.com/net/connstats"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/tstest"
@@ -283,11 +284,6 @@ func TestWriteAndInject(t *testing.T) {
t.Errorf("%s not received", packet)
}
}
// Statistics gathering is disabled by default.
if stats := tun.ExtractStatistics(); len(stats) > 0 {
t.Errorf("tun.ExtractStatistics = %v, want {}", stats)
}
}
func TestFilter(t *testing.T) {
@@ -336,15 +332,17 @@ func TestFilter(t *testing.T) {
}()
var buf [MaxPacketSize]byte
tun.SetStatisticsEnabled(true)
stats := new(connstats.Statistics)
tun.SetStatistics(stats)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var n int
var err error
var filtered bool
if stats := tun.ExtractStatistics(); len(stats) > 0 {
t.Errorf("tun.ExtractStatistics = %v, want {}", stats)
tunStats, _ := stats.Extract()
if len(tunStats) > 0 {
t.Errorf("connstats.Statistics.Extract = %v, want {}", stats)
}
if tt.dir == in {
@@ -377,7 +375,7 @@ func TestFilter(t *testing.T) {
}
}
got := tun.ExtractStatistics()
got, _ := stats.Extract()
want := map[netlogtype.Connection]netlogtype.Counts{}
if !tt.drop {
var p packet.Parsed