record.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !ts_omit_netlog && !ts_omit_logtail
  4. package netlog
  5. import (
  6. "cmp"
  7. "net/netip"
  8. "slices"
  9. "strings"
  10. "time"
  11. "unicode/utf8"
  12. "tailscale.com/tailcfg"
  13. "tailscale.com/types/bools"
  14. "tailscale.com/types/netlogtype"
  15. "tailscale.com/util/set"
  16. )
  17. // maxLogSize is the maximum number of bytes for a log message.
  18. const maxLogSize = 256 << 10
  19. // record is the in-memory representation of a [netlogtype.Message].
  20. // It uses maps to efficiently look-up addresses and connections.
  21. // In contrast, [netlogtype.Message] is designed to be JSON serializable,
  22. // where complex keys types are not well support in JSON objects.
  23. type record struct {
  24. selfNode nodeUser
  25. start time.Time
  26. end time.Time
  27. seenNodes map[netip.Addr]nodeUser
  28. virtConns map[netlogtype.Connection]countsType
  29. physConns map[netlogtype.Connection]netlogtype.Counts
  30. }
  31. // nodeUser is a node with additional user profile information.
  32. type nodeUser struct {
  33. tailcfg.NodeView
  34. user tailcfg.UserProfileView // UserProfileView for NodeView.User
  35. }
  36. // countsType is a counts with classification information about the connection.
  37. type countsType struct {
  38. netlogtype.Counts
  39. connType connType
  40. }
  41. type connType uint8
  42. const (
  43. unknownTraffic connType = iota
  44. virtualTraffic
  45. subnetTraffic
  46. exitTraffic
  47. )
  48. // toMessage converts a [record] into a [netlogtype.Message].
  49. func (r record) toMessage(excludeNodeInfo, anonymizeExitTraffic bool) netlogtype.Message {
  50. if !r.selfNode.Valid() {
  51. return netlogtype.Message{}
  52. }
  53. m := netlogtype.Message{
  54. NodeID: r.selfNode.StableID(),
  55. Start: r.start.UTC(),
  56. End: r.end.UTC(),
  57. }
  58. // Convert node fields.
  59. if !excludeNodeInfo {
  60. m.SrcNode = r.selfNode.toNode()
  61. seenIDs := set.Of(r.selfNode.ID())
  62. for _, node := range r.seenNodes {
  63. if _, ok := seenIDs[node.ID()]; !ok && node.Valid() {
  64. m.DstNodes = append(m.DstNodes, node.toNode())
  65. seenIDs.Add(node.ID())
  66. }
  67. }
  68. slices.SortFunc(m.DstNodes, func(x, y netlogtype.Node) int {
  69. return cmp.Compare(x.NodeID, y.NodeID)
  70. })
  71. }
  72. // Converter traffic fields.
  73. anonymizedExitTraffic := make(map[netlogtype.Connection]netlogtype.Counts)
  74. for conn, cnts := range r.virtConns {
  75. switch cnts.connType {
  76. case virtualTraffic:
  77. m.VirtualTraffic = append(m.VirtualTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
  78. case subnetTraffic:
  79. m.SubnetTraffic = append(m.SubnetTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
  80. default:
  81. if anonymizeExitTraffic {
  82. conn = netlogtype.Connection{ // scrub the IP protocol type
  83. Src: netip.AddrPortFrom(conn.Src.Addr(), 0), // scrub the port number
  84. Dst: netip.AddrPortFrom(conn.Dst.Addr(), 0), // scrub the port number
  85. }
  86. if !r.seenNodes[conn.Src.Addr()].Valid() {
  87. conn.Src = netip.AddrPort{} // not a Tailscale node, so scrub the address
  88. }
  89. if !r.seenNodes[conn.Dst.Addr()].Valid() {
  90. conn.Dst = netip.AddrPort{} // not a Tailscale node, so scrub the address
  91. }
  92. anonymizedExitTraffic[conn] = anonymizedExitTraffic[conn].Add(cnts.Counts)
  93. continue
  94. }
  95. m.ExitTraffic = append(m.ExitTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
  96. }
  97. }
  98. for conn, cnts := range anonymizedExitTraffic {
  99. m.ExitTraffic = append(m.ExitTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
  100. }
  101. for conn, cnts := range r.physConns {
  102. m.PhysicalTraffic = append(m.PhysicalTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
  103. }
  104. // Sort the connections for deterministic results.
  105. slices.SortFunc(m.VirtualTraffic, compareConnCnts)
  106. slices.SortFunc(m.SubnetTraffic, compareConnCnts)
  107. slices.SortFunc(m.ExitTraffic, compareConnCnts)
  108. slices.SortFunc(m.PhysicalTraffic, compareConnCnts)
  109. return m
  110. }
  111. func compareConnCnts(x, y netlogtype.ConnectionCounts) int {
  112. return cmp.Or(
  113. netip.AddrPort.Compare(x.Src, y.Src),
  114. netip.AddrPort.Compare(x.Dst, y.Dst),
  115. cmp.Compare(x.Proto, y.Proto))
  116. }
  117. // jsonLen computes an upper-bound on the size of the JSON representation.
  118. func (nu nodeUser) jsonLen() (n int) {
  119. if !nu.Valid() {
  120. return len(`{"nodeId":""}`)
  121. }
  122. n += len(`{}`)
  123. n += len(`"nodeId":`) + jsonQuotedLen(string(nu.StableID())) + len(`,`)
  124. if len(nu.Name()) > 0 {
  125. n += len(`"name":`) + jsonQuotedLen(nu.Name()) + len(`,`)
  126. }
  127. if nu.Addresses().Len() > 0 {
  128. n += len(`"addresses":[]`)
  129. for _, addr := range nu.Addresses().All() {
  130. n += bools.IfElse(addr.Addr().Is4(), len(`"255.255.255.255"`), len(`"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"`)) + len(",")
  131. }
  132. }
  133. if nu.Hostinfo().Valid() && len(nu.Hostinfo().OS()) > 0 {
  134. n += len(`"os":`) + jsonQuotedLen(nu.Hostinfo().OS()) + len(`,`)
  135. }
  136. if nu.Tags().Len() > 0 {
  137. n += len(`"tags":[]`)
  138. for _, tag := range nu.Tags().All() {
  139. n += jsonQuotedLen(tag) + len(",")
  140. }
  141. } else if nu.user.Valid() && nu.user.ID() == nu.User() && len(nu.user.LoginName()) > 0 {
  142. n += len(`"user":`) + jsonQuotedLen(nu.user.LoginName()) + len(",")
  143. }
  144. return n
  145. }
  146. // toNode converts the [nodeUser] into a [netlogtype.Node].
  147. func (nu nodeUser) toNode() netlogtype.Node {
  148. if !nu.Valid() {
  149. return netlogtype.Node{}
  150. }
  151. n := netlogtype.Node{
  152. NodeID: nu.StableID(),
  153. Name: strings.TrimSuffix(nu.Name(), "."),
  154. }
  155. var ipv4, ipv6 netip.Addr
  156. for _, addr := range nu.Addresses().All() {
  157. switch {
  158. case addr.IsSingleIP() && addr.Addr().Is4():
  159. ipv4 = addr.Addr()
  160. case addr.IsSingleIP() && addr.Addr().Is6():
  161. ipv6 = addr.Addr()
  162. }
  163. }
  164. n.Addresses = []netip.Addr{ipv4, ipv6}
  165. n.Addresses = slices.DeleteFunc(n.Addresses, func(a netip.Addr) bool { return !a.IsValid() })
  166. if nu.Hostinfo().Valid() {
  167. n.OS = nu.Hostinfo().OS()
  168. }
  169. if nu.Tags().Len() > 0 {
  170. n.Tags = nu.Tags().AsSlice()
  171. slices.Sort(n.Tags)
  172. n.Tags = slices.Compact(n.Tags)
  173. } else if nu.user.Valid() && nu.user.ID() == nu.User() {
  174. n.User = nu.user.LoginName()
  175. }
  176. return n
  177. }
  178. // jsonQuotedLen computes the length of the JSON serialization of s
  179. // according to [jsontext.AppendQuote].
  180. func jsonQuotedLen(s string) int {
  181. n := len(`"`) + len(s) + len(`"`)
  182. for i, r := range s {
  183. switch {
  184. case r == '\b', r == '\t', r == '\n', r == '\f', r == '\r', r == '"', r == '\\':
  185. n += len(`\X`) - 1
  186. case r < ' ':
  187. n += len(`\uXXXX`) - 1
  188. case r == utf8.RuneError:
  189. if _, m := utf8.DecodeRuneInString(s[i:]); m == 1 { // exactly an invalid byte
  190. n += len("�") - 1
  191. }
  192. }
  193. }
  194. return n
  195. }