diff --git a/control/controlclient/map.go b/control/controlclient/map.go index 1bc57fdf2..fb6d93dd9 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -45,6 +45,7 @@ type mapSession struct { collectServices bool previousPeers []*tailcfg.Node // for delta-purposes lastDomain string + lastDomainAuditLogID string lastHealth []string lastPopBrowserURL string stickyDebug tailcfg.Debug // accumulated opt.Bool values @@ -113,6 +114,9 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo if resp.Domain != "" { ms.lastDomain = resp.Domain } + if resp.DomainDataPlaneAuditLogID != "" { + ms.lastDomainAuditLogID = resp.DomainDataPlaneAuditLogID + } if resp.Health != nil { ms.lastHealth = resp.Health } @@ -143,20 +147,21 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo } nm := &netmap.NetworkMap{ - NodeKey: ms.privateNodeKey.Public(), - PrivateKey: ms.privateNodeKey, - MachineKey: ms.machinePubKey, - Peers: resp.Peers, - UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile), - Domain: ms.lastDomain, - DNS: *ms.lastDNSConfig, - PacketFilter: ms.lastParsedPacketFilter, - SSHPolicy: ms.lastSSHPolicy, - CollectServices: ms.collectServices, - DERPMap: ms.lastDERPMap, - Debug: debug, - ControlHealth: ms.lastHealth, - TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled, + NodeKey: ms.privateNodeKey.Public(), + PrivateKey: ms.privateNodeKey, + MachineKey: ms.machinePubKey, + Peers: resp.Peers, + UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile), + Domain: ms.lastDomain, + DomainAuditLogID: ms.lastDomainAuditLogID, + DNS: *ms.lastDNSConfig, + PacketFilter: ms.lastParsedPacketFilter, + SSHPolicy: ms.lastSSHPolicy, + CollectServices: ms.collectServices, + DERPMap: ms.lastDERPMap, + Debug: debug, + ControlHealth: ms.lastHealth, + TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled, } ms.netMapBuilding = nm diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index a2270870a..1961d9ce1 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -76,6 +76,11 @@ type NetworkMap struct { // Domain is the current Tailnet name. Domain string + // DomainAuditLogID is an audit log ID provided by control and + // only populated if the domain opts into data-plane audit logging. + // If this is empty, then data-plane audit logging is disabled. + DomainAuditLogID string + UserProfiles map[tailcfg.UserID]tailcfg.UserProfile } diff --git a/wgengine/wgcfg/config.go b/wgengine/wgcfg/config.go index 80acd0d20..3598856be 100644 --- a/wgengine/wgcfg/config.go +++ b/wgengine/wgcfg/config.go @@ -8,6 +8,7 @@ import ( "net/netip" + "tailscale.com/logtail" "tailscale.com/types/key" ) @@ -22,6 +23,13 @@ type Config struct { MTU uint16 DNS []netip.Addr Peers []Peer + + // NetworkLogging enables network logging. + // It is disabled if either ID is the zero value. + NetworkLogging struct { + NodeID logtail.PrivateID + DomainID logtail.PrivateID + } } type Peer struct { diff --git a/wgengine/wgcfg/nmcfg/nmcfg.go b/wgengine/wgcfg/nmcfg/nmcfg.go index e553b9148..acd02eb04 100644 --- a/wgengine/wgcfg/nmcfg/nmcfg.go +++ b/wgengine/wgcfg/nmcfg/nmcfg.go @@ -11,6 +11,8 @@ "net/netip" "strings" + "golang.org/x/exp/slices" + "tailscale.com/logtail" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/logger" @@ -58,6 +60,25 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, Peers: make([]wgcfg.Peer, 0, len(nm.Peers)), } + // Setup log IDs for data plane audit logging. + if nm.SelfNode != nil { + canNetworkLog := slices.Contains(nm.SelfNode.Capabilities, tailcfg.CapabilityDataPlaneAuditLogs) + if canNetworkLog && nm.SelfNode.DataPlaneAuditLogID != "" && nm.DomainAuditLogID != "" { + nodeID, errNode := logtail.ParsePrivateID(nm.SelfNode.DataPlaneAuditLogID) + if errNode != nil { + logf("[v1] wgcfg: unable to parse node audit log ID: %v", errNode) + } + domainID, errDomain := logtail.ParsePrivateID(nm.DomainAuditLogID) + if errDomain != nil { + logf("[v1] wgcfg: unable to parse domain audit log ID: %v", errDomain) + } + if errNode == nil && errDomain == nil { + cfg.NetworkLogging.NodeID = nodeID + cfg.NetworkLogging.DomainID = domainID + } + } + } + // Logging buffers skippedUnselected := new(bytes.Buffer) skippedIPs := new(bytes.Buffer) diff --git a/wgengine/wgcfg/wgcfg_clone.go b/wgengine/wgcfg/wgcfg_clone.go index 6b4d0dbad..abf319c22 100644 --- a/wgengine/wgcfg/wgcfg_clone.go +++ b/wgengine/wgcfg/wgcfg_clone.go @@ -9,6 +9,7 @@ import ( "net/netip" + "tailscale.com/logtail" "tailscale.com/types/key" ) @@ -31,12 +32,16 @@ func (src *Config) Clone() *Config { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _ConfigCloneNeedsRegeneration = Config(struct { - Name string - PrivateKey key.NodePrivate - Addresses []netip.Prefix - MTU uint16 - DNS []netip.Addr - Peers []Peer + Name string + PrivateKey key.NodePrivate + Addresses []netip.Prefix + MTU uint16 + DNS []netip.Addr + Peers []Peer + NetworkLogging struct { + NodeID logtail.PrivateID + DomainID logtail.PrivateID + } }{}) // Clone makes a deep copy of Peer.