From 78c36f53fee9f03eff7ef09ada79662424db8a15 Mon Sep 17 00:00:00 2001 From: Claire Wang Date: Wed, 27 Mar 2024 15:45:02 -0400 Subject: [PATCH] exit node dst wip --- cmd/tailscale/cli/set.go | 59 +++++++++++++++++---------------- cmd/tailscale/cli/up.go | 3 ++ ipn/ipnlocal/local.go | 15 +++++++++ ipn/prefs.go | 62 ++++++++++++++++++++--------------- util/syspolicy/policy_keys.go | 2 +- wgengine/netlog/logger.go | 8 ++--- wgengine/userspace.go | 3 +- wgengine/wgcfg/config.go | 5 +-- 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 } }