// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package ipnlocal import ( "errors" "tailscale.com/ipn" "tailscale.com/tailcfg" "tailscale.com/util/clientmetric" ) // Counter metrics for edit/change events var ( // metricExitNodeEnabled is incremented when the user enables an exit node independent of the node's characteristics. metricExitNodeEnabled = clientmetric.NewCounter("prefs_exit_node_enabled") // metricExitNodeEnabledSuggested is incremented when the user enables the suggested exit node. metricExitNodeEnabledSuggested = clientmetric.NewCounter("prefs_exit_node_enabled_suggested") // metricExitNodeEnabledMullvad is incremented when the user enables a Mullvad exit node. metricExitNodeEnabledMullvad = clientmetric.NewCounter("prefs_exit_node_enabled_mullvad") // metricWantRunningEnabled is incremented when WantRunning transitions from false to true. metricWantRunningEnabled = clientmetric.NewCounter("prefs_want_running_enabled") // metricWantRunningDisabled is incremented when WantRunning transitions from true to false. metricWantRunningDisabled = clientmetric.NewCounter("prefs_want_running_disabled") ) type exitNodeProperty string const ( exitNodeTypePreferred exitNodeProperty = "suggested" // The exit node is the last suggested exit node exitNodeTypeMullvad exitNodeProperty = "mullvad" // The exit node is a Mullvad exit node ) // prefsMetricsEditEvent encapsulates information needed to record metrics related // to any changes to preferences. type prefsMetricsEditEvent struct { change *ipn.MaskedPrefs // the preference mask used to update the preferences pNew ipn.PrefsView // new preferences (after ApplyUpdates) pOld ipn.PrefsView // old preferences (before ApplyUpdates) node *nodeBackend // the node the event is associated with lastSuggestedExitNode tailcfg.StableNodeID // the last suggested exit node } // record records changes to preferences as clientmetrics. func (e *prefsMetricsEditEvent) record() error { if e.change == nil || e.node == nil { return errors.New("prefsMetricsEditEvent: missing required fields") } // Record up/down events. if e.change.WantRunningSet && (e.pNew.WantRunning() != e.pOld.WantRunning()) { if e.pNew.WantRunning() { metricWantRunningEnabled.Add(1) } else { metricWantRunningDisabled.Add(1) } } // Record any changes to exit node settings. if e.change.ExitNodeIDSet || e.change.ExitNodeIPSet { if exitNodeTypes, ok := e.exitNodeType(e.pNew.ExitNodeID()); ok { // We have switched to a valid exit node if ok is true. metricExitNodeEnabled.Add(1) // We may have some additional characteristics we should also record. for _, t := range exitNodeTypes { switch t { case exitNodeTypePreferred: metricExitNodeEnabledSuggested.Add(1) case exitNodeTypeMullvad: metricExitNodeEnabledMullvad.Add(1) } } } } return nil } // exitNodeTypesLocked returns type of exit node for the given stable ID. // An exit node may have multiple type (can be both mullvad and preferred // simultaneously for example). // // This will return ok as true if the supplied stable ID resolves to a known peer, // false otherwise. The caller is responsible for ensuring that the id belongs to // an exit node. func (e *prefsMetricsEditEvent) exitNodeType(id tailcfg.StableNodeID) (props []exitNodeProperty, isNode bool) { var peer tailcfg.NodeView if peer, isNode = e.node.PeerByStableID(id); isNode { if tailcfg.StableNodeID(id) == e.lastSuggestedExitNode { props = append(props, exitNodeTypePreferred) } if peer.IsWireGuardOnly() { props = append(props, exitNodeTypeMullvad) } } return props, isNode }