// 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")}, Hostinfo: (&tailcfg.Hostinfo{OS: "linux"}).View(), User: 12345, }, user: &tailcfg.UserProfile{ ID: 12345, LoginName: "user@domain", }, want: netlogtype.Node{ NodeID: "n123456CNTL", Addresses: []netip.Addr{addr("100.1.2.3")}, OS: "linux", 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) } }