@ -550,11 +550,13 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
// Following changes are triggered via the eventbus.
b . linkChange ( & netmon . ChangeDelta { New : netMon . InterfaceState ( ) } )
if buildfeatures . HasPeerAPIServer {
if tunWrap , ok := b . sys . Tun . GetOK ( ) ; ok {
tunWrap . PeerAPIPort = b . GetPeerAPIPort
} else {
b . logf ( "[unexpected] failed to wire up PeerAPI port for engine %T" , e )
}
}
if buildfeatures . HasDebug {
for _ , component := range ipn . DebuggableComponents {
@ -972,6 +974,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
b . updateFilterLocked ( prefs )
updateExitNodeUsageWarning ( prefs , delta . New , b . health )
if buildfeatures . HasPeerAPIServer {
cn := b . currentNode ( )
nm := cn . NetMap ( )
if peerAPIListenAsync && nm != nil && b . state == ipn . Running {
@ -983,6 +986,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
b . goTracker . Go ( b . initPeerAPIListener )
}
}
}
}
// Captive portal detection hooks.
@ -1368,7 +1372,7 @@ func peerStatusFromNode(ps *ipnstate.PeerStatus, n tailcfg.NodeView) {
ps . PublicKey = n . Key ( )
ps . ID = n . StableID ( )
ps . Created = n . Created ( )
ps . ExitNodeOption = tsaddr. ContainsExitRoutes ( n . AllowedIPs ( ) )
ps . ExitNodeOption = buildfeatures. HasUseExitNode && tsaddr. ContainsExitRoutes ( n . AllowedIPs ( ) )
if n . Tags ( ) . Len ( ) != 0 {
v := n . Tags ( )
ps . Tags = & v
@ -1897,6 +1901,9 @@ func (b *LocalBackend) applySysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
//
// b.mu must be held.
func ( b * LocalBackend ) applyExitNodeSysPolicyLocked ( prefs * ipn . Prefs ) ( anyChange bool ) {
if ! buildfeatures . HasUseExitNode {
return false
}
if exitNodeIDStr , _ := b . polc . GetString ( pkey . ExitNodeID , "" ) ; exitNodeIDStr != "" {
exitNodeID := tailcfg . StableNodeID ( exitNodeIDStr )
@ -2002,7 +2009,7 @@ func (b *LocalBackend) sysPolicyChanged(policy policyclient.PolicyChange) {
b . mu . Unlock ( )
}
if policy. HasChanged ( pkey . AllowedSuggestedExitNodes ) {
if buildfeatures. HasUseExitNode && policy. HasChanged ( pkey . AllowedSuggestedExitNodes ) {
b . refreshAllowedSuggestions ( )
// Re-evaluate exit node suggestion now that the policy setting has changed.
if _ , err := b . SuggestExitNode ( ) ; err != nil && ! errors . Is ( err , ErrNoPreferredDERP ) {
@ -2073,6 +2080,9 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
// mustationsAreWorthyOfRecalculatingSuggestedExitNode reports whether any mutation type in muts is
// worthy of recalculating the suggested exit node.
func mutationsAreWorthyOfRecalculatingSuggestedExitNode ( muts [ ] netmap . NodeMutation , cn * nodeBackend , sid tailcfg . StableNodeID ) bool {
if ! buildfeatures . HasUseExitNode {
return false
}
for _ , m := range muts {
n , ok := cn . NodeByID ( m . NodeIDBeingMutated ( ) )
if ! ok {
@ -2126,6 +2136,9 @@ func mutationsAreWorthyOfTellingIPNBus(muts []netmap.NodeMutation) bool {
//
// b.mu must be held.
func ( b * LocalBackend ) resolveAutoExitNodeLocked ( prefs * ipn . Prefs ) ( prefsChanged bool ) {
if ! buildfeatures . HasUseExitNode {
return false
}
// As of 2025-07-08, the only supported auto exit node expression is [ipn.AnyExitNode].
//
// However, to maintain forward compatibility with future auto exit node expressions,
@ -2170,6 +2183,9 @@ func (b *LocalBackend) resolveAutoExitNodeLocked(prefs *ipn.Prefs) (prefsChanged
//
// b.mu must be held.
func ( b * LocalBackend ) resolveExitNodeIPLocked ( prefs * ipn . Prefs ) ( prefsChanged bool ) {
if ! buildfeatures . HasUseExitNode {
return false
}
// If we have a desired IP on file, try to find the corresponding node.
if ! prefs . ExitNodeIP . IsValid ( ) {
return false
@ -2455,6 +2471,11 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
}
}
var c2nHandler http . Handler
if buildfeatures . HasC2N {
c2nHandler = http . HandlerFunc ( b . handleC2N )
}
// TODO(apenwarr): The only way to change the ServerURL is to
// re-run b.Start, because this is the only place we create a
// new controlclient. EditPrefs allows you to overwrite ServerURL,
@ -2475,7 +2496,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
PopBrowserURL : b . tellClientToBrowseToURL ,
Dialer : b . Dialer ( ) ,
Observer : b ,
C2NHandler : http. HandlerFunc ( b . handleC2N ) ,
C2NHandler : c2nHandler ,
DialPlan : & b . dialPlan , // pointer because it can't be copied
ControlKnobs : b . sys . ControlKnobs ( ) ,
Shutdown : ccShutdown ,
@ -2623,6 +2644,7 @@ func (b *LocalBackend) updateFilterLocked(prefs ipn.PrefsView) {
}
}
if prefs . Valid ( ) {
if buildfeatures . HasAdvertiseRoutes {
for _ , r := range prefs . AdvertiseRoutes ( ) . All ( ) {
if r . Bits ( ) == 0 {
// When offering a default route to the world, we
@ -2650,6 +2672,7 @@ func (b *LocalBackend) updateFilterLocked(prefs ipn.PrefsView) {
logNetsB . AddPrefix ( r )
}
}
}
// App connectors handle DNS requests for app domains over PeerAPI (corp#11961),
// but a safety check verifies the requesting peer has at least permission
@ -2658,7 +2681,7 @@ func (b *LocalBackend) updateFilterLocked(prefs ipn.PrefsView) {
// The correct filter rules are synthesized by the coordination server
// and sent down, but the address needs to be part of the 'local net' for the
// filter package to even bother checking the filter rules, so we set them here.
if prefs. AppConnector ( ) . Advertise {
if buildfeatures. HasAppConnectors && prefs. AppConnector ( ) . Advertise {
localNetsB . Add ( netip . MustParseAddr ( "0.0.0.0" ) )
localNetsB . Add ( netip . MustParseAddr ( "::0" ) )
}
@ -3712,6 +3735,9 @@ func (b *LocalBackend) Ping(ctx context.Context, ip netip.Addr, pingType tailcfg
}
func ( b * LocalBackend ) pingPeerAPI ( ctx context . Context , ip netip . Addr ) ( peer tailcfg . NodeView , peerBase string , err error ) {
if ! buildfeatures . HasPeerAPIClient {
return peer , peerBase , feature . ErrUnavailable
}
var zero tailcfg . NodeView
ctx , cancel := context . WithTimeout ( ctx , 10 * time . Second )
defer cancel ( )
@ -4051,6 +4077,9 @@ var exitNodeMisconfigurationWarnable = health.Register(&health.Warnable{
// updateExitNodeUsageWarning updates a warnable meant to notify users of
// configuration issues that could break exit node usage.
func updateExitNodeUsageWarning ( p ipn . PrefsView , state * netmon . State , healthTracker * health . Tracker ) {
if ! buildfeatures . HasUseExitNode {
return
}
var msg string
if p . ExitNodeIP ( ) . IsValid ( ) || p . ExitNodeID ( ) != "" {
warn , _ := netutil . CheckReversePathFiltering ( state )
@ -4070,6 +4099,9 @@ func (b *LocalBackend) checkExitNodePrefsLocked(p *ipn.Prefs) error {
if ! tryingToUseExitNode {
return nil
}
if ! buildfeatures . HasUseExitNode {
return feature . ErrUnavailable
}
if err := featureknob . CanUseExitNode ( ) ; err != nil {
return err
@ -4110,6 +4142,9 @@ func (b *LocalBackend) SetUseExitNodeEnabled(actor ipnauth.Actor, v bool) (ipn.P
defer unlock ( )
p0 := b . pm . CurrentPrefs ( )
if ! buildfeatures . HasUseExitNode {
return p0 , nil
}
if v && p0 . ExitNodeID ( ) != "" {
// Already on.
return p0 , nil
@ -4240,6 +4275,9 @@ func (b *LocalBackend) checkEditPrefsAccessLocked(actor ipnauth.Actor, prefs ipn
//
// b.mu must be held.
func ( b * LocalBackend ) changeDisablesExitNodeLocked ( prefs ipn . PrefsView , change * ipn . MaskedPrefs ) bool {
if ! buildfeatures . HasUseExitNode {
return false
}
if ! change . AutoExitNodeSet && ! change . ExitNodeIDSet && ! change . ExitNodeIPSet {
// The change does not affect exit node usage.
return false
@ -4577,6 +4615,9 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
// GetPeerAPIPort returns the port number for the peerapi server
// running on the provided IP.
func ( b * LocalBackend ) GetPeerAPIPort ( ip netip . Addr ) ( port uint16 , ok bool ) {
if ! buildfeatures . HasPeerAPIServer {
return 0 , false
}
b . mu . Lock ( )
defer b . mu . Unlock ( )
for _ , pln := range b . peerAPIListeners {
@ -4936,11 +4977,13 @@ func (b *LocalBackend) authReconfig() {
// Keep the dialer updated about whether we're supposed to use
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
// can use it for name resolution)
if buildfeatures . HasUseExitNode {
if dohURLOK {
b . dialer . SetExitDNSDoH ( dohURL )
} else {
b . dialer . SetExitDNSDoH ( "" )
}
}
cfg , err := nmcfg . WGCfg ( nm , b . logf , flags , prefs . ExitNodeID ( ) )
if err != nil {
@ -5064,6 +5107,9 @@ func (b *LocalBackend) TailscaleVarRoot() string {
//
// b.mu must be held.
func ( b * LocalBackend ) closePeerAPIListenersLocked ( ) {
if ! buildfeatures . HasPeerAPIServer {
return
}
b . peerAPIServer = nil
for _ , pln := range b . peerAPIListeners {
pln . Close ( )
@ -5079,6 +5125,9 @@ func (b *LocalBackend) closePeerAPIListenersLocked() {
const peerAPIListenAsync = runtime . GOOS == "windows" || runtime . GOOS == "android"
func ( b * LocalBackend ) initPeerAPIListener ( ) {
if ! buildfeatures . HasPeerAPIServer {
return
}
b . logf ( "[v1] initPeerAPIListener: entered" )
b . mu . Lock ( )
defer b . mu . Unlock ( )
@ -5903,6 +5952,9 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
// RefreshExitNode determines which exit node to use based on the current
// prefs and netmap and switches to it if needed.
func ( b * LocalBackend ) RefreshExitNode ( ) {
if ! buildfeatures . HasUseExitNode {
return
}
if b . resolveExitNode ( ) {
b . authReconfig ( )
}
@ -5918,6 +5970,9 @@ func (b *LocalBackend) RefreshExitNode() {
//
// b.mu must not be held.
func ( b * LocalBackend ) resolveExitNode ( ) ( changed bool ) {
if ! buildfeatures . HasUseExitNode {
return false
}
b . mu . Lock ( )
defer b . mu . Unlock ( )
@ -6468,6 +6523,9 @@ func (b *LocalBackend) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpd
//
// If exitNodeID is the zero valid, it returns "", false.
func exitNodeCanProxyDNS ( nm * netmap . NetworkMap , peers map [ tailcfg . NodeID ] tailcfg . NodeView , exitNodeID tailcfg . StableNodeID ) ( dohURL string , ok bool ) {
if ! buildfeatures . HasUseExitNode {
return "" , false
}
if exitNodeID . IsZero ( ) {
return "" , false
}
@ -7084,6 +7142,9 @@ var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
//
// b.mu.lock() must be held.
func ( b * LocalBackend ) suggestExitNodeLocked ( ) ( response apitype . ExitNodeSuggestionResponse , err error ) {
if ! buildfeatures . HasUseExitNode {
return response , feature . ErrUnavailable
}
lastReport := b . MagicConn ( ) . GetLastNetcheckReport ( b . ctx )
prevSuggestion := b . lastSuggestedExitNode
@ -7101,6 +7162,9 @@ func (b *LocalBackend) suggestExitNodeLocked() (response apitype.ExitNodeSuggest
}
func ( b * LocalBackend ) SuggestExitNode ( ) ( response apitype . ExitNodeSuggestionResponse , err error ) {
if ! buildfeatures . HasUseExitNode {
return response , feature . ErrUnavailable
}
b . mu . Lock ( )
defer b . mu . Unlock ( )
return b . suggestExitNodeLocked ( )
@ -7117,6 +7181,9 @@ func (b *LocalBackend) getAllowedSuggestions() set.Set[tailcfg.StableNodeID] {
// refreshAllowedSuggestions rebuilds the set of permitted exit nodes
// from the current [pkey.AllowedSuggestedExitNodes] value.
func ( b * LocalBackend ) refreshAllowedSuggestions ( ) {
if ! buildfeatures . HasUseExitNode {
return
}
b . allowedSuggestedExitNodesMu . Lock ( )
defer b . allowedSuggestedExitNodesMu . Unlock ( )
b . allowedSuggestedExitNodes = fillAllowedSuggestions ( b . polc )