From 77123a569ba1055f091db06e2d1b59c09b02f108 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 4 Nov 2025 12:36:04 -0800 Subject: [PATCH] wgengine/netlog: include node OS in logged attributes (#17755) Include the node's OS with network flow log information. Refactor the JSON-length computation to be a bit more precise. Updates tailscale/corp#33352 Fixes tailscale/corp#34030 Signed-off-by: Joe Tsai --- cmd/k8s-operator/depaware.txt | 2 +- cmd/tailscaled/depaware.txt | 1 + cmd/tsidp/depaware.txt | 2 +- tsnet/depaware.txt | 2 +- types/netlogtype/netlogtype.go | 15 +++------------ wgengine/netlog/record.go | 26 ++++++++++++++++++++++---- wgengine/netlog/record_test.go | 2 ++ 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt index 8d1f7fa06..ebd22770e 100644 --- a/cmd/k8s-operator/depaware.txt +++ b/cmd/k8s-operator/depaware.txt @@ -825,7 +825,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/ tailscale.com/tsweb from tailscale.com/util/eventbus tailscale.com/tsweb/varz from tailscale.com/util/usermetric+ tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal+ - tailscale.com/types/bools from tailscale.com/tsnet + tailscale.com/types/bools from tailscale.com/tsnet+ tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+ tailscale.com/types/empty from tailscale.com/ipn+ tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index c1708711a..bdc110e1a 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -392,6 +392,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/tsweb from tailscale.com/util/eventbus tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled+ tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal+ + tailscale.com/types/bools from tailscale.com/wgengine/netlog tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+ tailscale.com/types/empty from tailscale.com/ipn+ tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled diff --git a/cmd/tsidp/depaware.txt b/cmd/tsidp/depaware.txt index 80c8e04a8..ebf03b541 100644 --- a/cmd/tsidp/depaware.txt +++ b/cmd/tsidp/depaware.txt @@ -230,7 +230,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar tailscale.com/tsweb from tailscale.com/util/eventbus tailscale.com/tsweb/varz from tailscale.com/tsweb+ tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal+ - tailscale.com/types/bools from tailscale.com/tsnet + tailscale.com/types/bools from tailscale.com/tsnet+ tailscale.com/types/dnstype from tailscale.com/client/local+ tailscale.com/types/empty from tailscale.com/ipn+ tailscale.com/types/ipproto from tailscale.com/ipn+ diff --git a/tsnet/depaware.txt b/tsnet/depaware.txt index ef0fe0667..4817a511a 100644 --- a/tsnet/depaware.txt +++ b/tsnet/depaware.txt @@ -225,7 +225,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware) LDW tailscale.com/tsweb from tailscale.com/util/eventbus tailscale.com/tsweb/varz from tailscale.com/tsweb+ tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal+ - tailscale.com/types/bools from tailscale.com/tsnet + tailscale.com/types/bools from tailscale.com/tsnet+ tailscale.com/types/dnstype from tailscale.com/client/local+ tailscale.com/types/empty from tailscale.com/ipn+ tailscale.com/types/ipproto from tailscale.com/ipn+ diff --git a/types/netlogtype/netlogtype.go b/types/netlogtype/netlogtype.go index 86d645b35..cc38684a3 100644 --- a/types/netlogtype/netlogtype.go +++ b/types/netlogtype/netlogtype.go @@ -44,18 +44,6 @@ const ( // Each [ConnectionCounts] occupies at most [MaxConnectionCountsJSONSize]. MinMessageJSONSize = len(messageJSON) - nodeJSON = `{"nodeId":` + maxJSONStableID + `,"name":"","addresses":` + maxJSONAddrs + `,"user":"","tags":[]}` - maxJSONAddrV4 = `"255.255.255.255"` - maxJSONAddrV6 = `"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"` - maxJSONAddrs = `[` + maxJSONAddrV4 + `,` + maxJSONAddrV6 + `]` - - // MinNodeJSONSize is the overhead size of Node when it is - // serialized as JSON assuming that each field is minimally populated. - // It does not account for bytes occupied by - // [Node.Name], [Node.User], or [Node.Tags]. The [Node.Addresses] - // is assumed to contain a pair of IPv4 and IPv6 address. - MinNodeJSONSize = len(nodeJSON) - maxJSONConnCounts = `{` + maxJSONConn + `,` + maxJSONCounts + `}` maxJSONConn = `"proto":` + maxJSONProto + `,"src":` + maxJSONAddrPort + `,"dst":` + maxJSONAddrPort maxJSONProto = `255` @@ -82,6 +70,9 @@ type Node struct { // Addresses are the Tailscale IP addresses of the node. Addresses []netip.Addr `json:"addresses,omitempty"` + // OS is the operating system of the node. + OS string `json:"os,omitzero"` // e.g., "linux" + // User is the user that owns the node. // It is not populated if the node is tagged. User string `json:"user,omitzero"` // e.g., "johndoe@example.com" diff --git a/wgengine/netlog/record.go b/wgengine/netlog/record.go index b8db26fc5..45e30fabe 100644 --- a/wgengine/netlog/record.go +++ b/wgengine/netlog/record.go @@ -13,6 +13,7 @@ import ( "unicode/utf8" "tailscale.com/tailcfg" + "tailscale.com/types/bools" "tailscale.com/types/netlogtype" "tailscale.com/util/set" ) @@ -134,17 +135,31 @@ func compareConnCnts(x, y netlogtype.ConnectionCounts) int { } // jsonLen computes an upper-bound on the size of the JSON representation. -func (nu nodeUser) jsonLen() int { +func (nu nodeUser) jsonLen() (n int) { if !nu.Valid() { return len(`{"nodeId":""}`) } - n := netlogtype.MinNodeJSONSize + jsonQuotedLen(nu.Name()) + 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() { - n += jsonQuotedLen(nu.user.LoginName()) + } 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 } @@ -166,6 +181,9 @@ func (nu nodeUser) toNode() netlogtype.Node { } 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) diff --git a/wgengine/netlog/record_test.go b/wgengine/netlog/record_test.go index d3ab8b86c..7dd840d29 100644 --- a/wgengine/netlog/record_test.go +++ b/wgengine/netlog/record_test.go @@ -190,6 +190,7 @@ func TestToNode(t *testing.T) { node: &tailcfg.Node{ StableID: "n123456CNTL", Addresses: []netip.Prefix{prefix("100.1.2.3")}, + Hostinfo: (&tailcfg.Hostinfo{OS: "linux"}).View(), User: 12345, }, user: &tailcfg.UserProfile{ @@ -199,6 +200,7 @@ func TestToNode(t *testing.T) { want: netlogtype.Node{ NodeID: "n123456CNTL", Addresses: []netip.Addr{addr("100.1.2.3")}, + OS: "linux", User: "user@domain", }, },