diff --git a/control/controlknobs/controlknobs.go b/control/controlknobs/controlknobs.go index 6e3a62967..bea8bfd19 100644 --- a/control/controlknobs/controlknobs.go +++ b/control/controlknobs/controlknobs.go @@ -72,6 +72,9 @@ type Knobs struct { // ProbeUDPLifetime is whether the node should probe UDP path lifetime on // the tail end of an active direct connection in magicsock. ProbeUDPLifetime atomic.Bool + + // NetworkFlowLoggingDstEnable is whether network flow logging on destinations for exit nodes is enabled. + NetworkFlowLoggingDstEnable atomic.Bool } // UpdateFromNodeAttributes updates k (if non-nil) based on the provided self @@ -96,6 +99,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables) seamlessKeyRenewal = has(tailcfg.NodeAttrSeamlessKeyRenewal) probeUDPLifetime = has(tailcfg.NodeAttrProbeUDPLifetime) + networkFlowLoggingDstEnable = has(tailcfg.NodeAttrNetworkFlowLoggingDst) ) if has(tailcfg.NodeAttrOneCGNATEnable) { @@ -118,6 +122,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { k.LinuxForceNfTables.Store(forceNfTables) k.SeamlessKeyRenewal.Store(seamlessKeyRenewal) k.ProbeUDPLifetime.Store(probeUDPLifetime) + k.NetworkFlowLoggingDstEnable.Store(networkFlowLoggingDstEnable) } // AsDebugJSON returns k as something that can be marshalled with json.Marshal @@ -141,5 +146,6 @@ func (k *Knobs) AsDebugJSON() map[string]any { "LinuxForceNfTables": k.LinuxForceNfTables.Load(), "SeamlessKeyRenewal": k.SeamlessKeyRenewal.Load(), "ProbeUDPLifetime": k.ProbeUDPLifetime.Load(), + "NewtorkFlowLoggingDstEnable": k.NetworkFlowLoggingDstEnable.Load(), } } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index c194063d5..eab106883 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -2230,6 +2230,9 @@ const ( // NodeAttrDisableWebClient disables using the web client. NodeAttrDisableWebClient NodeCapability = "disable-web-client" + + // NodeAttrNewtorkFLowLogging enables logging destination addresses for exit nodes. + NodeAttrNetworkFlowLoggingDst NodeCapability = "network-flow-logging-dst" ) // SetDNSRequest is a request to add a DNS record. diff --git a/wgengine/netlog/logger.go b/wgengine/netlog/logger.go index 5eaa52375..17f30f750 100644 --- a/wgengine/netlog/logger.go +++ b/wgengine/netlog/logger.go @@ -56,6 +56,8 @@ type Logger struct { addrs map[netip.Addr]bool prefixes map[netip.Prefix]bool + + enableLogDst bool } // Running reports whether the logger is running. @@ -92,7 +94,8 @@ var testClient *http.Client // The IP protocol and source port are always zero. // The sock is used to populated the PhysicalTraffic field in Message. // The netMon parameter is optional; if non-nil it's used to do faster interface lookups. -func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor) error { +// enableLogDst is a control knob boolean. +func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, enableLogDst bool) error { nl.mu.Lock() defer nl.mu.Unlock() if nl.logger != nil { @@ -130,7 +133,7 @@ func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID lo addrs := nl.addrs prefixes := nl.prefixes nl.mu.Unlock() - recordStatistics(nl.logger, nodeID, start, end, virtual, physical, addrs, prefixes) + recordStatistics(nl.logger, nodeID, start, end, virtual, physical, addrs, prefixes, nl.enableLogDst) }) // Register the connection tracker into the TUN device. @@ -147,10 +150,12 @@ func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID lo nl.sock = sock nl.sock.SetStatistics(nl.stats) + nl.enableLogDst = enableLogDst + return nil } -func recordStatistics(logger *logtail.Logger, nodeID tailcfg.StableNodeID, start, end time.Time, connstats, sockStats map[netlogtype.Connection]netlogtype.Counts, addrs map[netip.Addr]bool, prefixes map[netip.Prefix]bool) { +func recordStatistics(logger *logtail.Logger, nodeID tailcfg.StableNodeID, start, end time.Time, connstats, sockStats map[netlogtype.Connection]netlogtype.Counts, addrs map[netip.Addr]bool, prefixes map[netip.Prefix]bool, enableLogDst bool) { m := netlogtype.Message{NodeID: nodeID, Start: start.UTC(), End: end.UTC()} classifyAddr := func(a netip.Addr) (isTailscale, withinRoute bool) { @@ -179,7 +184,7 @@ func recordStatistics(logger *logtail.Logger, nodeID tailcfg.StableNodeID, start m.SubnetTraffic = append(m.SubnetTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts}) default: const anonymize = true - if anonymize { + if anonymize && !enableLogDst { // Only preserve the address if it is a Tailscale IP address. srcOrig, dstOrig := conn.Src, conn.Dst conn = netlogtype.Connection{} // scrub everything by default diff --git a/wgengine/userspace.go b/wgengine/userspace.go index b35e792bd..1e0850739 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -932,8 +932,9 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, if netLogRunning && !e.networkLogger.Running() { nid := cfg.NetworkLogging.NodeID tid := cfg.NetworkLogging.DomainID + enableLogDst := e.controlKnobs.NetworkFlowLoggingDstEnable.Load() e.logf("wgengine: Reconfig: starting up network logger (node:%s tailnet:%s)", nid.Public(), tid.Public()) - if err := e.networkLogger.Startup(cfg.NodeID, nid, tid, e.tundev, e.magicConn, e.netMon); err != nil { + if err := e.networkLogger.Startup(cfg.NodeID, nid, tid, e.tundev, e.magicConn, e.netMon, enableLogDst); err != nil { e.logf("wgengine: Reconfig: error starting up network logger: %v", err) } e.networkLogger.ReconfigRoutes(routerCfg)