wgengine/netlog: embed node information in network flow logs (#17668)
This rewrites the netlog package to support embedding node information in network flow logs. Some bit of complexity comes in trying to pre-compute the expected size of the log message after JSON serialization to ensure that we can respect maximum body limits in log uploading. We also fix a bug in tstun, where we were recording the IP address after SNAT, which was resulting in non-sensible connection flows being logged. Updates tailscale/corp#33352 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_netlog && !ts_omit_logtail
|
||||
|
||||
package netlog
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func addr(s string) netip.Addr {
|
||||
if s == "" {
|
||||
return netip.Addr{}
|
||||
}
|
||||
return must.Get(netip.ParseAddr(s))
|
||||
}
|
||||
func addrPort(s string) netip.AddrPort {
|
||||
if s == "" {
|
||||
return netip.AddrPort{}
|
||||
}
|
||||
return must.Get(netip.ParseAddrPort(s))
|
||||
}
|
||||
func prefix(s string) netip.Prefix {
|
||||
if p, err := netip.ParsePrefix(s); err == nil {
|
||||
return p
|
||||
}
|
||||
a := addr(s)
|
||||
return netip.PrefixFrom(a, a.BitLen())
|
||||
}
|
||||
|
||||
func conn(proto ipproto.Proto, src, dst string) netlogtype.Connection {
|
||||
return netlogtype.Connection{Proto: proto, Src: addrPort(src), Dst: addrPort(dst)}
|
||||
}
|
||||
|
||||
func counts(txP, txB, rxP, rxB uint64) netlogtype.Counts {
|
||||
return netlogtype.Counts{TxPackets: txP, TxBytes: txB, RxPackets: rxP, RxBytes: rxB}
|
||||
}
|
||||
|
||||
func TestToMessage(t *testing.T) {
|
||||
rec := record{
|
||||
selfNode: nodeUser{NodeView: (&tailcfg.Node{
|
||||
ID: 123456,
|
||||
StableID: "n123456CNTL",
|
||||
Name: "src.tail123456.ts.net",
|
||||
Addresses: []netip.Prefix{prefix("100.1.2.3")},
|
||||
Tags: []string{"tag:src"},
|
||||
}).View()},
|
||||
start: time.Now(),
|
||||
end: time.Now().Add(5 * time.Second),
|
||||
|
||||
seenNodes: map[netip.Addr]nodeUser{
|
||||
addr("100.1.2.4"): {NodeView: (&tailcfg.Node{
|
||||
ID: 123457,
|
||||
StableID: "n123457CNTL",
|
||||
Name: "dst1.tail123456.ts.net",
|
||||
Addresses: []netip.Prefix{prefix("100.1.2.4")},
|
||||
Tags: []string{"tag:dst1"},
|
||||
}).View()},
|
||||
addr("100.1.2.5"): {NodeView: (&tailcfg.Node{
|
||||
ID: 123458,
|
||||
StableID: "n123458CNTL",
|
||||
Name: "dst2.tail123456.ts.net",
|
||||
Addresses: []netip.Prefix{prefix("100.1.2.5")},
|
||||
Tags: []string{"tag:dst2"},
|
||||
}).View()},
|
||||
},
|
||||
|
||||
virtConns: map[netlogtype.Connection]countsType{
|
||||
conn(0x1, "100.1.2.3:1234", "100.1.2.4:80"): {Counts: counts(12, 34, 56, 78), connType: virtualTraffic},
|
||||
conn(0x1, "100.1.2.3:1234", "100.1.2.5:80"): {Counts: counts(23, 45, 78, 790), connType: virtualTraffic},
|
||||
conn(0x6, "172.16.1.1:80", "100.1.2.4:1234"): {Counts: counts(91, 54, 723, 621), connType: subnetTraffic},
|
||||
conn(0x6, "172.16.1.2:443", "100.1.2.5:1234"): {Counts: counts(42, 813, 3, 1823), connType: subnetTraffic},
|
||||
conn(0x6, "172.16.1.3:80", "100.1.2.6:1234"): {Counts: counts(34, 52, 78, 790), connType: subnetTraffic},
|
||||
conn(0x6, "100.1.2.3:1234", "12.34.56.78:80"): {Counts: counts(11, 110, 10, 100), connType: exitTraffic},
|
||||
conn(0x6, "100.1.2.4:1234", "23.34.56.78:80"): {Counts: counts(423, 1, 6, 123), connType: exitTraffic},
|
||||
conn(0x6, "100.1.2.4:1234", "23.34.56.78:443"): {Counts: counts(22, 220, 20, 200), connType: exitTraffic},
|
||||
conn(0x6, "100.1.2.5:1234", "45.34.56.78:80"): {Counts: counts(33, 330, 30, 300), connType: exitTraffic},
|
||||
conn(0x6, "100.1.2.6:1234", "67.34.56.78:80"): {Counts: counts(44, 440, 40, 400), connType: exitTraffic},
|
||||
conn(0x6, "42.54.72.42:555", "18.42.7.1:777"): {Counts: counts(44, 440, 40, 400)},
|
||||
},
|
||||
|
||||
physConns: map[netlogtype.Connection]netlogtype.Counts{
|
||||
conn(0, "100.1.2.4:0", "4.3.2.1:1234"): counts(12, 34, 56, 78),
|
||||
conn(0, "100.1.2.5:0", "4.3.2.10:1234"): counts(78, 56, 34, 12),
|
||||
},
|
||||
}
|
||||
rec.seenNodes[rec.selfNode.toNode().Addresses[0]] = rec.selfNode
|
||||
|
||||
got := rec.toMessage(false, false)
|
||||
want := netlogtype.Message{
|
||||
NodeID: rec.selfNode.StableID(),
|
||||
Start: rec.start,
|
||||
End: rec.end,
|
||||
SrcNode: rec.selfNode.toNode(),
|
||||
DstNodes: []netlogtype.Node{
|
||||
rec.seenNodes[addr("100.1.2.4")].toNode(),
|
||||
rec.seenNodes[addr("100.1.2.5")].toNode(),
|
||||
},
|
||||
VirtualTraffic: []netlogtype.ConnectionCounts{
|
||||
{Connection: conn(0x1, "100.1.2.3:1234", "100.1.2.4:80"), Counts: counts(12, 34, 56, 78)},
|
||||
{Connection: conn(0x1, "100.1.2.3:1234", "100.1.2.5:80"), Counts: counts(23, 45, 78, 790)},
|
||||
},
|
||||
SubnetTraffic: []netlogtype.ConnectionCounts{
|
||||
{Connection: conn(0x6, "172.16.1.1:80", "100.1.2.4:1234"), Counts: counts(91, 54, 723, 621)},
|
||||
{Connection: conn(0x6, "172.16.1.2:443", "100.1.2.5:1234"), Counts: counts(42, 813, 3, 1823)},
|
||||
{Connection: conn(0x6, "172.16.1.3:80", "100.1.2.6:1234"), Counts: counts(34, 52, 78, 790)},
|
||||
},
|
||||
ExitTraffic: []netlogtype.ConnectionCounts{
|
||||
{Connection: conn(0x6, "42.54.72.42:555", "18.42.7.1:777"), Counts: counts(44, 440, 40, 400)},
|
||||
{Connection: conn(0x6, "100.1.2.3:1234", "12.34.56.78:80"), Counts: counts(11, 110, 10, 100)},
|
||||
{Connection: conn(0x6, "100.1.2.4:1234", "23.34.56.78:80"), Counts: counts(423, 1, 6, 123)},
|
||||
{Connection: conn(0x6, "100.1.2.4:1234", "23.34.56.78:443"), Counts: counts(22, 220, 20, 200)},
|
||||
{Connection: conn(0x6, "100.1.2.5:1234", "45.34.56.78:80"), Counts: counts(33, 330, 30, 300)},
|
||||
{Connection: conn(0x6, "100.1.2.6:1234", "67.34.56.78:80"), Counts: counts(44, 440, 40, 400)},
|
||||
},
|
||||
PhysicalTraffic: []netlogtype.ConnectionCounts{
|
||||
{Connection: conn(0, "100.1.2.4:0", "4.3.2.1:1234"), Counts: counts(12, 34, 56, 78)},
|
||||
{Connection: conn(0, "100.1.2.5:0", "4.3.2.10:1234"), Counts: counts(78, 56, 34, 12)},
|
||||
},
|
||||
}
|
||||
if d := cmp.Diff(got, want, cmpopts.EquateComparable(netip.Addr{}, netip.AddrPort{})); d != "" {
|
||||
t.Errorf("toMessage(false, false) mismatch (-got +want):\n%s", d)
|
||||
}
|
||||
|
||||
got = rec.toMessage(true, false)
|
||||
want.SrcNode = netlogtype.Node{}
|
||||
want.DstNodes = nil
|
||||
if d := cmp.Diff(got, want, cmpopts.EquateComparable(netip.Addr{}, netip.AddrPort{})); d != "" {
|
||||
t.Errorf("toMessage(true, false) mismatch (-got +want):\n%s", d)
|
||||
}
|
||||
|
||||
got = rec.toMessage(true, true)
|
||||
want.ExitTraffic = []netlogtype.ConnectionCounts{
|
||||
{Connection: conn(0, "", ""), Counts: counts(44+44, 440+440, 40+40, 400+400)},
|
||||
{Connection: conn(0, "100.1.2.3:0", ""), Counts: counts(11, 110, 10, 100)},
|
||||
{Connection: conn(0, "100.1.2.4:0", ""), Counts: counts(423+22, 1+220, 6+20, 123+200)},
|
||||
{Connection: conn(0, "100.1.2.5:0", ""), Counts: counts(33, 330, 30, 300)},
|
||||
}
|
||||
if d := cmp.Diff(got, want, cmpopts.EquateComparable(netip.Addr{}, netip.AddrPort{})); d != "" {
|
||||
t.Errorf("toMessage(true, true) mismatch (-got +want):\n%s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToNode(t *testing.T) {
|
||||
tests := []struct {
|
||||
node *tailcfg.Node
|
||||
user *tailcfg.UserProfile
|
||||
want netlogtype.Node
|
||||
}{
|
||||
{},
|
||||
{
|
||||
node: &tailcfg.Node{
|
||||
StableID: "n123456CNTL",
|
||||
Name: "test.tail123456.ts.net",
|
||||
Addresses: []netip.Prefix{prefix("100.1.2.3")},
|
||||
Tags: []string{"tag:dupe", "tag:test", "tag:dupe"},
|
||||
User: 12345, // should be ignored
|
||||
},
|
||||
want: netlogtype.Node{
|
||||
NodeID: "n123456CNTL",
|
||||
Name: "test.tail123456.ts.net",
|
||||
Addresses: []netip.Addr{addr("100.1.2.3")},
|
||||
Tags: []string{"tag:dupe", "tag:test"},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: &tailcfg.Node{
|
||||
StableID: "n123456CNTL",
|
||||
Addresses: []netip.Prefix{prefix("100.1.2.3")},
|
||||
User: 12345,
|
||||
},
|
||||
want: netlogtype.Node{
|
||||
NodeID: "n123456CNTL",
|
||||
Addresses: []netip.Addr{addr("100.1.2.3")},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: &tailcfg.Node{
|
||||
StableID: "n123456CNTL",
|
||||
Addresses: []netip.Prefix{prefix("100.1.2.3")},
|
||||
User: 12345,
|
||||
},
|
||||
user: &tailcfg.UserProfile{
|
||||
ID: 12345,
|
||||
LoginName: "user@domain",
|
||||
},
|
||||
want: netlogtype.Node{
|
||||
NodeID: "n123456CNTL",
|
||||
Addresses: []netip.Addr{addr("100.1.2.3")},
|
||||
User: "user@domain",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
nu := nodeUser{tt.node.View(), tt.user.View()}
|
||||
got := nu.toNode()
|
||||
b := must.Get(jsonv2.Marshal(got))
|
||||
if len(b) > nu.jsonLen() {
|
||||
t.Errorf("jsonLen = %v, want >= %d", nu.jsonLen(), len(b))
|
||||
}
|
||||
if d := cmp.Diff(got, tt.want, cmpopts.EquateComparable(netip.Addr{})); d != "" {
|
||||
t.Errorf("toNode mismatch (-got +want):\n%s", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzQuotedLen(f *testing.F) {
|
||||
for _, s := range quotedLenTestdata {
|
||||
f.Add(s)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
testQuotedLen(t, s)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuotedLen(t *testing.T) {
|
||||
for _, s := range quotedLenTestdata {
|
||||
testQuotedLen(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
var quotedLenTestdata = []string{
|
||||
"", // empty string
|
||||
func() string {
|
||||
b := make([]byte, 128)
|
||||
for i := range b {
|
||||
b[i] = byte(i)
|
||||
}
|
||||
return string(b)
|
||||
}(), // all ASCII
|
||||
"�", // replacement rune
|
||||
"\xff", // invalid UTF-8
|
||||
"ʕ◔ϖ◔ʔ", // Unicode gopher
|
||||
}
|
||||
|
||||
func testQuotedLen(t *testing.T, in string) {
|
||||
got := jsonQuotedLen(in)
|
||||
b, _ := jsontext.AppendQuote(nil, in)
|
||||
want := len(b)
|
||||
if got != want {
|
||||
t.Errorf("jsonQuotedLen(%q) = %v, want %v", in, got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user