summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--cmd/tailscale/cli/set.go59
-rw-r--r--cmd/tailscale/cli/up.go3
-rw-r--r--ipn/ipnlocal/local.go15
-rw-r--r--ipn/prefs.go62
-rw-r--r--util/syspolicy/policy_keys.go2
-rw-r--r--wgengine/netlog/logger.go8
-rw-r--r--wgengine/userspace.go3
-rw-r--r--wgengine/wgcfg/config.go5
8 files changed, 94 insertions, 63 deletions
diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go
index 4049eb12e..80d93c695 100644
--- a/cmd/tailscale/cli/set.go
+++ b/cmd/tailscale/cli/set.go
@@ -38,24 +38,25 @@ Only settings explicitly mentioned will be set. There are no default values.`,
}
type setArgsT struct {
- acceptRoutes bool
- acceptDNS bool
- exitNodeIP string
- exitNodeAllowLANAccess bool
- shieldsUp bool
- runSSH bool
- runWebClient bool
- hostname string
- advertiseRoutes string
- advertiseDefaultRoute bool
- advertiseConnector bool
- opUser string
- acceptedRisks string
- profileName string
- forceDaemon bool
- updateCheck bool
- updateApply bool
- postureChecking bool
+ acceptRoutes bool
+ acceptDNS bool
+ exitNodeIP string
+ exitNodeAllowLANAccess bool
+ exitDestinationFlowLogs bool
+ shieldsUp bool
+ runSSH bool
+ runWebClient bool
+ hostname string
+ advertiseRoutes string
+ advertiseDefaultRoute bool
+ advertiseConnector bool
+ opUser string
+ acceptedRisks string
+ profileName string
+ forceDaemon bool
+ updateCheck bool
+ updateApply bool
+ postureChecking bool
}
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
@@ -66,6 +67,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.acceptDNS, "accept-dns", false, "accept DNS configuration from the admin panel")
setf.StringVar(&setArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
setf.BoolVar(&setArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
+ setf.BoolVar(&setArgs.exitDestinationFlowLogs, "exit-destination-flow-logs", false, "Enable exit node destination in network flow logs")
setf.BoolVar(&setArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
setf.BoolVar(&setArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
setf.StringVar(&setArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
@@ -106,16 +108,17 @@ func runSet(ctx context.Context, args []string) (retErr error) {
maskedPrefs := &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
- ProfileName: setArgs.profileName,
- RouteAll: setArgs.acceptRoutes,
- CorpDNS: setArgs.acceptDNS,
- ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess,
- ShieldsUp: setArgs.shieldsUp,
- RunSSH: setArgs.runSSH,
- RunWebClient: setArgs.runWebClient,
- Hostname: setArgs.hostname,
- OperatorUser: setArgs.opUser,
- ForceDaemon: setArgs.forceDaemon,
+ ProfileName: setArgs.profileName,
+ RouteAll: setArgs.acceptRoutes,
+ CorpDNS: setArgs.acceptDNS,
+ ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess,
+ ExitDestinationFlowLogs: setArgs.exitDestinationFlowLogs,
+ ShieldsUp: setArgs.shieldsUp,
+ RunSSH: setArgs.runSSH,
+ RunWebClient: setArgs.runWebClient,
+ Hostname: setArgs.hostname,
+ OperatorUser: setArgs.opUser,
+ ForceDaemon: setArgs.forceDaemon,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: setArgs.updateCheck,
Apply: opt.NewBool(setArgs.updateApply),
diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go
index 43f36f819..f3c11d695 100644
--- a/cmd/tailscale/cli/up.go
+++ b/cmd/tailscale/cli/up.go
@@ -723,6 +723,7 @@ func init() {
addPrefFlagMapping("auto-update", "AutoUpdate.Apply")
addPrefFlagMapping("advertise-connector", "AppConnector")
addPrefFlagMapping("posture-checking", "PostureChecking")
+ addPrefFlagMapping("exit-destination-flow-logs", "ExitDestinationFlowLogs")
}
func addPrefFlagMapping(flagName string, prefNames ...string) {
@@ -951,6 +952,8 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
set(exitNodeIPStr())
case "exit-node-allow-lan-access":
set(prefs.ExitNodeAllowLANAccess)
+ case "exit-destination-flow-logs":
+ set(prefs.ExitDestinationFlowLogs)
case "advertise-tags":
set(strings.Join(prefs.AdvertiseTags, ","))
case "hostname":
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index 2ae020df7..010462a56 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -1150,6 +1150,9 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
if setExitNodeID(prefs, st.NetMap) {
prefsChanged = true
}
+ if setExitDstFlowLogs(prefs) {
+ prefsChanged = true
+ }
if applySysPolicy(prefs) {
prefsChanged = true
}
@@ -1335,6 +1338,15 @@ func applySysPolicy(prefs *ipn.Prefs) (anyChange bool) {
return anyChange
}
+func setExitDstFlowLogs(prefs *ipn.Prefs) (anyChange bool) {
+ fmt.Printf("set exit dst flow pref")
+ if enable, err := syspolicy.GetBoolean(syspolicy.ExitDestinationFlowLogs, prefs.ExitDestinationFlowLogs); err == nil && prefs.ExitDestinationFlowLogs != enable {
+ prefs.ExitDestinationFlowLogs = enable
+ anyChange = true
+ }
+ return anyChange
+}
+
var _ controlclient.NetmapDeltaUpdater = (*LocalBackend)(nil)
// UpdateNetmapDelta implements controlclient.NetmapDeltaUpdater.
@@ -3247,6 +3259,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
// everything in this function treats b.prefs as completely new
// anyway. No-op if no exit node resolution is needed.
setExitNodeID(newp, netMap)
+ setExitDstFlowLogs(newp)
// applySysPolicy does likewise so we can also ignore its return value.
applySysPolicy(newp)
// We do this to avoid holding the lock while doing everything else.
@@ -3628,6 +3641,8 @@ func (b *LocalBackend) authReconfig() {
return
}
+ cfg.NetworkLogging.ExitDestinationFlowLogs = prefs.ExitDestinationFlowLogs()
+
oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.ControlKnobs(), version.OS())
rcfg := b.routerConfig(cfg, prefs, oneCGNATRoute)
diff --git a/ipn/prefs.go b/ipn/prefs.go
index ef81cd08b..4d0947a47 100644
--- a/ipn/prefs.go
+++ b/ipn/prefs.go
@@ -109,6 +109,9 @@ type Prefs struct {
// routed directly or via the exit node.
ExitNodeAllowLANAccess bool
+ // ExitDestinationFlowLogs indicates whether exit node destination is recorded in network flow logs.
+ ExitDestinationFlowLogs bool
+
// CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists.
CorpDNS bool
@@ -274,33 +277,34 @@ type AppConnectorPrefs struct {
type MaskedPrefs struct {
Prefs
- ControlURLSet bool `json:",omitempty"`
- RouteAllSet bool `json:",omitempty"`
- AllowSingleHostsSet bool `json:",omitempty"`
- ExitNodeIDSet bool `json:",omitempty"`
- ExitNodeIPSet bool `json:",omitempty"`
- ExitNodeAllowLANAccessSet bool `json:",omitempty"`
- CorpDNSSet bool `json:",omitempty"`
- RunSSHSet bool `json:",omitempty"`
- RunWebClientSet bool `json:",omitempty"`
- WantRunningSet bool `json:",omitempty"`
- LoggedOutSet bool `json:",omitempty"`
- ShieldsUpSet bool `json:",omitempty"`
- AdvertiseTagsSet bool `json:",omitempty"`
- HostnameSet bool `json:",omitempty"`
- NotepadURLsSet bool `json:",omitempty"`
- ForceDaemonSet bool `json:",omitempty"`
- EggSet bool `json:",omitempty"`
- AdvertiseRoutesSet bool `json:",omitempty"`
- NoSNATSet bool `json:",omitempty"`
- NetfilterModeSet bool `json:",omitempty"`
- OperatorUserSet bool `json:",omitempty"`
- ProfileNameSet bool `json:",omitempty"`
- AutoUpdateSet AutoUpdatePrefsMask `json:",omitempty"`
- AppConnectorSet bool `json:",omitempty"`
- PostureCheckingSet bool `json:",omitempty"`
- NetfilterKindSet bool `json:",omitempty"`
- DriveSharesSet bool `json:",omitempty"`
+ ControlURLSet bool `json:",omitempty"`
+ RouteAllSet bool `json:",omitempty"`
+ AllowSingleHostsSet bool `json:",omitempty"`
+ ExitDestinationFlowLogsSet bool `json:",omitempty"`
+ ExitNodeIDSet bool `json:",omitempty"`
+ ExitNodeIPSet bool `json:",omitempty"`
+ ExitNodeAllowLANAccessSet bool `json:",omitempty"`
+ CorpDNSSet bool `json:",omitempty"`
+ RunSSHSet bool `json:",omitempty"`
+ RunWebClientSet bool `json:",omitempty"`
+ WantRunningSet bool `json:",omitempty"`
+ LoggedOutSet bool `json:",omitempty"`
+ ShieldsUpSet bool `json:",omitempty"`
+ AdvertiseTagsSet bool `json:",omitempty"`
+ HostnameSet bool `json:",omitempty"`
+ NotepadURLsSet bool `json:",omitempty"`
+ ForceDaemonSet bool `json:",omitempty"`
+ EggSet bool `json:",omitempty"`
+ AdvertiseRoutesSet bool `json:",omitempty"`
+ NoSNATSet bool `json:",omitempty"`
+ NetfilterModeSet bool `json:",omitempty"`
+ OperatorUserSet bool `json:",omitempty"`
+ ProfileNameSet bool `json:",omitempty"`
+ AutoUpdateSet AutoUpdatePrefsMask `json:",omitempty"`
+ AppConnectorSet bool `json:",omitempty"`
+ PostureCheckingSet bool `json:",omitempty"`
+ NetfilterKindSet bool `json:",omitempty"`
+ DriveSharesSet bool `json:",omitempty"`
}
type AutoUpdatePrefsMask struct {
@@ -475,6 +479,9 @@ func (p *Prefs) pretty(goos string) string {
if p.ShieldsUp {
sb.WriteString("shields=true ")
}
+ if p.ExitDestinationFlowLogs {
+ sb.WriteString("exitdestinationflowlogs=true ")
+ }
if p.ExitNodeIP.IsValid() {
fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess)
} else if !p.ExitNodeID.IsZero() {
@@ -545,6 +552,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.ExitNodeID == p2.ExitNodeID &&
p.ExitNodeIP == p2.ExitNodeIP &&
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
+ p.ExitDestinationFlowLogs == p2.ExitDestinationFlowLogs &&
p.CorpDNS == p2.CorpDNS &&
p.RunSSH == p2.RunSSH &&
p.RunWebClient == p2.RunWebClient &&
diff --git a/util/syspolicy/policy_keys.go b/util/syspolicy/policy_keys.go
index 166bbe601..64656c62e 100644
--- a/util/syspolicy/policy_keys.go
+++ b/util/syspolicy/policy_keys.go
@@ -67,7 +67,7 @@ const (
// The default is 0 unless otherwise stated.
LogSCMInteractions Key = "LogSCMInteractions"
FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock"
-
+ ExitDestinationFlowLogs Key = "ExitDestinationFlowLogs"
// PostureChecking indicates if posture checking is enabled and the client shall gather
// posture data.
// Key is a string value that specifies an option: "always", "never", "user-decides".
diff --git a/wgengine/netlog/logger.go b/wgengine/netlog/logger.go
index 5eaa52375..4233125a9 100644
--- a/wgengine/netlog/logger.go
+++ b/wgengine/netlog/logger.go
@@ -92,7 +92,7 @@ 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 {
+func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, enableExitDstFlowLogs bool) error {
nl.mu.Lock()
defer nl.mu.Unlock()
if nl.logger != nil {
@@ -130,7 +130,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, enableExitDstFlowLogs)
})
// Register the connection tracker into the TUN device.
@@ -150,7 +150,7 @@ func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID lo
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, enableExitDstFlowLogs bool) {
m := netlogtype.Message{NodeID: nodeID, Start: start.UTC(), End: end.UTC()}
classifyAddr := func(a netip.Addr) (isTailscale, withinRoute bool) {
@@ -179,7 +179,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 && !enableExitDstFlowLogs {
// 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 73ca40336..6357ec064 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
+ enableExitDstFlowLogs := cfg.NetworkLogging.ExitDestinationFlowLogs
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, enableExitDstFlowLogs); err != nil {
e.logf("wgengine: Reconfig: error starting up network logger: %v", err)
}
e.networkLogger.ReconfigRoutes(routerCfg)
diff --git a/wgengine/wgcfg/config.go b/wgengine/wgcfg/config.go
index 76583a8e8..937fe3143 100644
--- a/wgengine/wgcfg/config.go
+++ b/wgengine/wgcfg/config.go
@@ -28,8 +28,9 @@ type Config struct {
// NetworkLogging enables network logging.
// It is disabled if either ID is the zero value.
NetworkLogging struct {
- NodeID logid.PrivateID
- DomainID logid.PrivateID
+ NodeID logid.PrivateID
+ DomainID logid.PrivateID
+ ExitDestinationFlowLogs bool
}
}