feature/featuretags: add features for c2n, peerapi, advertise/use routes/exit nodes

Saves 262 KB so far. I'm sure I missed some places, but shotizam says
these were the low hanging fruit.

Updates #12614

Change-Id: Ia31c01b454f627e6d0470229aae4e19d615e45e3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-10-01 19:18:46 -07:00
committed by Brad Fitzpatrick
parent 2cd518a8b6
commit a208cb9fd5
29 changed files with 469 additions and 79 deletions
+12 -4
View File
@@ -32,12 +32,17 @@ import (
// c2nHandlers maps an HTTP method and URI path (without query parameters) to
// its handler. The exact method+path match is preferred, but if no entry
// exists for that, a map entry with an empty method is used as a fallback.
var c2nHandlers = map[methodAndPath]c2nHandler{
// Debug.
req("/echo"): handleC2NEcho,
}
var c2nHandlers map[methodAndPath]c2nHandler
func init() {
c2nHandlers = map[methodAndPath]c2nHandler{}
if buildfeatures.HasC2N {
// Echo is the basic "ping" handler as used by the control plane to probe
// whether a node is reachable. In particular, it's important for
// high-availability subnet routers for the control plane to probe which of
// several candidate nodes is reachable and actually alive.
RegisterC2N("/echo", handleC2NEcho)
}
if buildfeatures.HasSSH {
RegisterC2N("/ssh/usernames", handleC2NSSHUsernames)
}
@@ -69,6 +74,9 @@ func init() {
// A pattern is like "GET /foo" (specific to an HTTP method) or "/foo" (all
// methods). It panics if the pattern is already registered.
func RegisterC2N(pattern string, h func(*LocalBackend, http.ResponseWriter, *http.Request)) {
if !buildfeatures.HasC2N {
return
}
k := req(pattern)
if _, ok := c2nHandlers[k]; ok {
panic(fmt.Sprintf("c2n: duplicate handler for %q", pattern))
+112 -45
View File
@@ -550,10 +550,12 @@ 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 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.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 {
@@ -972,15 +974,17 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
b.updateFilterLocked(prefs)
updateExitNodeUsageWarning(prefs, delta.New, b.health)
cn := b.currentNode()
nm := cn.NetMap()
if peerAPIListenAsync && nm != nil && b.state == ipn.Running {
want := nm.GetAddresses().Len()
have := len(b.peerAPIListeners)
b.logf("[v1] linkChange: have %d peerAPIListeners, want %d", have, want)
if have < want {
b.logf("linkChange: peerAPIListeners too low; trying again")
b.goTracker.Go(b.initPeerAPIListener)
if buildfeatures.HasPeerAPIServer {
cn := b.currentNode()
nm := cn.NetMap()
if peerAPIListenAsync && nm != nil && b.state == ipn.Running {
want := nm.GetAddresses().Len()
have := len(b.peerAPIListeners)
b.logf("[v1] linkChange: have %d peerAPIListeners, want %d", have, want)
if have < want {
b.logf("linkChange: peerAPIListeners too low; trying again")
b.goTracker.Go(b.initPeerAPIListener)
}
}
}
}
@@ -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,31 +2644,33 @@ func (b *LocalBackend) updateFilterLocked(prefs ipn.PrefsView) {
}
}
if prefs.Valid() {
for _, r := range prefs.AdvertiseRoutes().All() {
if r.Bits() == 0 {
// When offering a default route to the world, we
// filter out locally reachable LANs, so that the
// default route effectively appears to be a "guest
// wifi": you get internet access, but to additionally
// get LAN access the LAN(s) need to be offered
// explicitly as well.
localInterfaceRoutes, hostIPs, err := interfaceRoutes()
if err != nil {
b.logf("getting local interface routes: %v", err)
continue
if buildfeatures.HasAdvertiseRoutes {
for _, r := range prefs.AdvertiseRoutes().All() {
if r.Bits() == 0 {
// When offering a default route to the world, we
// filter out locally reachable LANs, so that the
// default route effectively appears to be a "guest
// wifi": you get internet access, but to additionally
// get LAN access the LAN(s) need to be offered
// explicitly as well.
localInterfaceRoutes, hostIPs, err := interfaceRoutes()
if err != nil {
b.logf("getting local interface routes: %v", err)
continue
}
s, err := shrinkDefaultRoute(r, localInterfaceRoutes, hostIPs)
if err != nil {
b.logf("computing default route filter: %v", err)
continue
}
localNetsB.AddSet(s)
} else {
localNetsB.AddPrefix(r)
// When advertising a non-default route, we assume
// this is a corporate subnet that should be present
// in the audit logs.
logNetsB.AddPrefix(r)
}
s, err := shrinkDefaultRoute(r, localInterfaceRoutes, hostIPs)
if err != nil {
b.logf("computing default route filter: %v", err)
continue
}
localNetsB.AddSet(s)
} else {
localNetsB.AddPrefix(r)
// When advertising a non-default route, we assume
// this is a corporate subnet that should be present
// in the audit logs.
logNetsB.AddPrefix(r)
}
}
@@ -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,10 +4977,12 @@ 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 dohURLOK {
b.dialer.SetExitDNSDoH(dohURL)
} else {
b.dialer.SetExitDNSDoH("")
if buildfeatures.HasUseExitNode {
if dohURLOK {
b.dialer.SetExitDNSDoH(dohURL)
} else {
b.dialer.SetExitDNSDoH("")
}
}
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, prefs.ExitNodeID())
@@ -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)
+17 -12
View File
@@ -530,6 +530,9 @@ func (nb *nodeBackend) dnsConfigForNetmap(prefs ipn.PrefsView, selfExpired bool,
}
func (nb *nodeBackend) exitNodeCanProxyDNS(exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
if !buildfeatures.HasUseExitNode {
return "", false
}
nb.mu.Lock()
defer nb.mu.Unlock()
return exitNodeCanProxyDNS(nb.netMap, nb.peers, exitNodeID)
@@ -769,18 +772,20 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
// If we're using an exit node and that exit node is new enough (1.19.x+)
// to run a DoH DNS proxy, then send all our DNS traffic through it,
// unless we find resolvers with UseWithExitNode set, in which case we use that.
if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok {
filtered := useWithExitNodeResolvers(nm.DNS.Resolvers)
if len(filtered) > 0 {
addDefault(filtered)
} else {
// If no default global resolvers with the override
// are configured, configure the exit node's resolver.
addDefault([]*dnstype.Resolver{{Addr: dohURL}})
}
if buildfeatures.HasUseExitNode {
if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok {
filtered := useWithExitNodeResolvers(nm.DNS.Resolvers)
if len(filtered) > 0 {
addDefault(filtered)
} else {
// If no default global resolvers with the override
// are configured, configure the exit node's resolver.
addDefault([]*dnstype.Resolver{{Addr: dohURL}})
}
addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS.Routes))
return dcfg
addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS.Routes))
return dcfg
}
}
// If the user has set default resolvers ("override local DNS"), prefer to
@@ -788,7 +793,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
// node resolvers, use those as the default.
if len(nm.DNS.Resolvers) > 0 {
addDefault(nm.DNS.Resolvers)
} else {
} else if buildfeatures.HasUseExitNode {
if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok {
addDefault(resolvers)
}
+14
View File
@@ -26,6 +26,7 @@ import (
"golang.org/x/net/dns/dnsmessage"
"golang.org/x/net/http/httpguts"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/hostinfo"
@@ -131,6 +132,9 @@ type peerAPIListener struct {
}
func (pln *peerAPIListener) Close() error {
if !buildfeatures.HasPeerAPIServer {
return nil
}
if pln.ln != nil {
return pln.ln.Close()
}
@@ -138,6 +142,9 @@ func (pln *peerAPIListener) Close() error {
}
func (pln *peerAPIListener) serve() {
if !buildfeatures.HasPeerAPIServer {
return
}
if pln.ln == nil {
return
}
@@ -319,6 +326,9 @@ func peerAPIRequestShouldGetSecurityHeaders(r *http.Request) bool {
//
// It panics if the path is already registered.
func RegisterPeerAPIHandler(path string, f func(PeerAPIHandler, http.ResponseWriter, *http.Request)) {
if !buildfeatures.HasPeerAPIServer {
return
}
if _, ok := peerAPIHandlers[path]; ok {
panic(fmt.Sprintf("duplicate PeerAPI handler %q", path))
}
@@ -337,6 +347,10 @@ var (
)
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasPeerAPIServer {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
return
}
if err := h.validatePeerAPIRequest(r); err != nil {
metricInvalidRequests.Add(1)
h.logf("invalid request from %v: %v", h.remoteAddr, err)
+4
View File
@@ -6,6 +6,7 @@ package ipnlocal
import (
"errors"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
@@ -85,6 +86,9 @@ func (e *prefsMetricsEditEvent) record() error {
// 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) {
if !buildfeatures.HasUseExitNode {
return nil, false
}
var peer tailcfg.NodeView
if peer, isNode = e.node.PeerByStableID(id); isNode {
+19 -5
View File
@@ -72,7 +72,6 @@ var handler = map[string]LocalAPIHandler{
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
// without a trailing slash:
"alpha-set-device-attrs": (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690
"bugreport": (*Handler).serveBugReport,
"check-ip-forwarding": (*Handler).serveCheckIPForwarding,
"check-prefs": (*Handler).serveCheckPrefs,
"check-reverse-path-filtering": (*Handler).serveCheckReversePathFiltering,
@@ -90,21 +89,17 @@ var handler = map[string]LocalAPIHandler{
"logtap": (*Handler).serveLogTap,
"metrics": (*Handler).serveMetrics,
"ping": (*Handler).servePing,
"pprof": (*Handler).servePprof,
"prefs": (*Handler).servePrefs,
"query-feature": (*Handler).serveQueryFeature,
"reload-config": (*Handler).reloadConfig,
"reset-auth": (*Handler).serveResetAuth,
"set-dns": (*Handler).serveSetDNS,
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
"set-gui-visible": (*Handler).serveSetGUIVisible,
"set-push-device-token": (*Handler).serveSetPushDeviceToken,
"set-udp-gro-forwarding": (*Handler).serveSetUDPGROForwarding,
"set-use-exit-node-enabled": (*Handler).serveSetUseExitNodeEnabled,
"shutdown": (*Handler).serveShutdown,
"start": (*Handler).serveStart,
"status": (*Handler).serveStatus,
"suggest-exit-node": (*Handler).serveSuggestExitNode,
"update/check": (*Handler).serveUpdateCheck,
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
"usermetrics": (*Handler).serveUserMetrics,
@@ -116,6 +111,17 @@ func init() {
if buildfeatures.HasAppConnectors {
Register("appc-route-info", (*Handler).serveGetAppcRouteInfo)
}
if buildfeatures.HasUseExitNode {
Register("suggest-exit-node", (*Handler).serveSuggestExitNode)
Register("set-use-exit-node-enabled", (*Handler).serveSetUseExitNodeEnabled)
}
if buildfeatures.HasACME {
Register("set-dns", (*Handler).serveSetDNS)
}
if buildfeatures.HasDebug {
Register("bugreport", (*Handler).serveBugReport)
Register("pprof", (*Handler).servePprof)
}
}
// Register registers a new LocalAPI handler for the given name.
@@ -1291,6 +1297,10 @@ func (h *Handler) serveSetGUIVisible(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) serveSetUseExitNodeEnabled(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasUseExitNode {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
return
}
if r.Method != httpm.POST {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
@@ -1629,6 +1639,10 @@ func dnsMessageTypeForString(s string) (t dnsmessage.Type, err error) {
// serveSuggestExitNode serves a POST endpoint for returning a suggested exit node.
func (h *Handler) serveSuggestExitNode(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasUseExitNode {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
return
}
if r.Method != httpm.GET {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
return
+4
View File
@@ -20,6 +20,7 @@ import (
"tailscale.com/atomicfile"
"tailscale.com/drive"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
@@ -787,6 +788,9 @@ func (p *Prefs) AdvertisesExitNode() bool {
// SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two
// /0 exit node routes.
func (p *Prefs) SetAdvertiseExitNode(runExit bool) {
if !buildfeatures.HasAdvertiseExitNode {
return
}
if p == nil {
return
}