feature/featuretags, all: add build features, use existing ones in more places

Saves 270 KB.

Updates #12614

Change-Id: I4c3fe06d32c49edb3a4bb0758a8617d83f291cf5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-30 14:47:40 -07:00
committed by Brad Fitzpatrick
parent aa5b2ce83b
commit c45f8813b4
35 changed files with 407 additions and 166 deletions
+14 -6
View File
@@ -14,7 +14,6 @@ import (
"runtime"
"strconv"
"github.com/tailscale/peercred"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/safesocket"
@@ -63,8 +62,8 @@ type ConnIdentity struct {
notWindows bool // runtime.GOOS != "windows"
// Fields used when NotWindows:
isUnixSock bool // Conn is a *net.UnixConn
creds *peercred.Creds // or nil if peercred.Get was not implemented on this OS
isUnixSock bool // Conn is a *net.UnixConn
creds PeerCreds // or nil if peercred.Get was not implemented on this OS
// Used on Windows:
// TODO(bradfitz): merge these into the peercreds package and
@@ -97,9 +96,18 @@ func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
return ""
}
func (ci *ConnIdentity) Pid() int { return ci.pid }
func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds }
func (ci *ConnIdentity) Pid() int { return ci.pid }
func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
func (ci *ConnIdentity) Creds() PeerCreds { return ci.creds }
// PeerCreds is the interface for a github.com/tailscale/peercred.Creds,
// if linked into the binary.
//
// (It's not used on some platforms, or if ts_omit_unixsocketidentity is set.)
type PeerCreds interface {
UserID() (uid string, ok bool)
PID() (pid int, ok bool)
}
var metricIssue869Workaround = clientmetric.NewCounter("issue_869_workaround")
@@ -0,0 +1,25 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && ts_omit_unixsocketidentity
package ipnauth
import (
"net"
"tailscale.com/types/logger"
)
// GetConnIdentity extracts the identity information from the connection
// based on the user who owns the other end of the connection.
// and couldn't. The returned connIdentity has NotWindows set to true.
func GetConnIdentity(_ logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
return &ConnIdentity{conn: c, notWindows: true}, nil
}
// WindowsToken is unsupported when GOOS != windows and always returns
// ErrNotImplemented.
func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) {
return nil, ErrNotImplemented
}
@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows
//go:build !windows && !ts_omit_unixsocketidentity
package ipnauth
+30 -17
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"path"
"reflect"
"runtime"
"strconv"
"strings"
"time"
@@ -33,26 +34,34 @@ import (
// exists for that, a map entry with an empty method is used as a fallback.
var c2nHandlers = map[methodAndPath]c2nHandler{
// Debug.
req("/echo"): handleC2NEcho,
req("/debug/goroutines"): handleC2NDebugGoroutines,
req("/debug/prefs"): handleC2NDebugPrefs,
req("/debug/metrics"): handleC2NDebugMetrics,
req("/debug/component-logging"): handleC2NDebugComponentLogging,
req("/debug/logheap"): handleC2NDebugLogHeap,
req("/debug/netmap"): handleC2NDebugNetMap,
req("/echo"): handleC2NEcho,
}
// PPROF - We only expose a subset of typical pprof endpoints for security.
req("/debug/pprof/heap"): handleC2NPprof,
req("/debug/pprof/allocs"): handleC2NPprof,
func init() {
if buildfeatures.HasSSH {
RegisterC2N("/ssh/usernames", handleC2NSSHUsernames)
}
if buildfeatures.HasLogTail {
RegisterC2N("POST /logtail/flush", handleC2NLogtailFlush)
}
if buildfeatures.HasDebug {
RegisterC2N("POST /sockstats", handleC2NSockStats)
req("POST /logtail/flush"): handleC2NLogtailFlush,
req("POST /sockstats"): handleC2NSockStats,
// pprof:
// we only expose a subset of typical pprof endpoints for security.
RegisterC2N("/debug/pprof/heap", handleC2NPprof)
RegisterC2N("/debug/pprof/allocs", handleC2NPprof)
// SSH
req("/ssh/usernames"): handleC2NSSHUsernames,
// Linux netfilter.
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
RegisterC2N("/debug/goroutines", handleC2NDebugGoroutines)
RegisterC2N("/debug/prefs", handleC2NDebugPrefs)
RegisterC2N("/debug/metrics", handleC2NDebugMetrics)
RegisterC2N("/debug/component-logging", handleC2NDebugComponentLogging)
RegisterC2N("/debug/logheap", handleC2NDebugLogHeap)
RegisterC2N("/debug/netmap", handleC2NDebugNetMap)
}
if runtime.GOOS == "linux" && buildfeatures.HasOSRouter {
RegisterC2N("POST /netfilter-kind", handleC2NSetNetfilterKind)
}
}
// RegisterC2N registers a new c2n handler for the given pattern.
@@ -265,6 +274,10 @@ func handleC2NPprof(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
}
func handleC2NSSHUsernames(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasSSH {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
return
}
var req tailcfg.C2NSSHUsernamesRequest
if r.Method == "POST" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+5 -2
View File
@@ -1442,7 +1442,7 @@ func (b *LocalBackend) WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeVi
cn := b.currentNode()
nid, ok := cn.NodeByAddr(ipp.Addr())
if !ok {
if !ok && buildfeatures.HasNetstack {
var ip netip.Addr
if ipp.Port() != 0 {
var protos []string
@@ -5015,6 +5015,9 @@ func (b *LocalBackend) SetVarRoot(dir string) {
//
// It should only be called before the LocalBackend is used.
func (b *LocalBackend) SetLogFlusher(flushFunc func()) {
if !buildfeatures.HasLogTail {
return
}
b.logFlushFunc = flushFunc
}
@@ -5023,7 +5026,7 @@ func (b *LocalBackend) SetLogFlusher(flushFunc func()) {
//
// TryFlushLogs should not block.
func (b *LocalBackend) TryFlushLogs() bool {
if b.logFlushFunc == nil {
if !buildfeatures.HasLogTail || b.logFlushFunc == nil {
return false
}
b.logFlushFunc()
+25 -23
View File
@@ -354,33 +354,35 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
}
if strings.HasPrefix(r.URL.Path, "/dns-query") {
if buildfeatures.HasDNS && strings.HasPrefix(r.URL.Path, "/dns-query") {
metricDNSCalls.Add(1)
h.handleDNSQuery(w, r)
return
}
switch r.URL.Path {
case "/v0/goroutines":
h.handleServeGoroutines(w, r)
return
case "/v0/env":
h.handleServeEnv(w, r)
return
case "/v0/metrics":
h.handleServeMetrics(w, r)
return
case "/v0/magicsock":
h.handleServeMagicsock(w, r)
return
case "/v0/dnsfwd":
h.handleServeDNSFwd(w, r)
return
case "/v0/interfaces":
h.handleServeInterfaces(w, r)
return
case "/v0/sockstats":
h.handleServeSockStats(w, r)
return
if buildfeatures.HasDebug {
switch r.URL.Path {
case "/v0/goroutines":
h.handleServeGoroutines(w, r)
return
case "/v0/env":
h.handleServeEnv(w, r)
return
case "/v0/metrics":
h.handleServeMetrics(w, r)
return
case "/v0/magicsock":
h.handleServeMagicsock(w, r)
return
case "/v0/dnsfwd":
h.handleServeDNSFwd(w, r)
return
case "/v0/interfaces":
h.handleServeInterfaces(w, r)
return
case "/v0/sockstats":
h.handleServeSockStats(w, r)
return
}
}
if ph, ok := peerAPIHandlers[r.URL.Path]; ok {
ph(h, w, r)
+6
View File
@@ -12,6 +12,7 @@ import (
"runtime"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/types/logger"
@@ -237,6 +238,11 @@ func connIsLocalAdmin(logf logger.Logf, ci *ipnauth.ConnIdentity, operatorUID st
// Linux.
fallthrough
case "linux":
if !buildfeatures.HasUnixSocketIdentity {
// Everybody is an admin if support for unix socket identities
// is omitted for the build.
return true
}
uid, ok := ci.Creds().UserID()
if !ok {
return false
+6
View File
@@ -10,6 +10,8 @@ import (
"net"
"net/http"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/logpolicy"
)
@@ -23,6 +25,10 @@ import (
// precludes that from working and instead the GUI fails to dial out.
// So, go through tailscaled (with a CONNECT request) instead.
func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasOutboundProxy {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
return
}
ctx := r.Context()
if r.Method != "CONNECT" {
panic("[unexpected] miswired")
+17
View File
@@ -15,6 +15,7 @@ import (
"net"
"net/http"
"os/user"
"runtime"
"strconv"
"strings"
"sync"
@@ -24,6 +25,7 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
@@ -120,6 +122,10 @@ func (s *Server) awaitBackend(ctx context.Context) (_ *ipnlocal.LocalBackend, ok
// This is primarily for the Windows GUI, because wintun can take awhile to
// come up. See https://github.com/tailscale/tailscale/issues/6522.
func (s *Server) serveServerStatus(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasDebug && runtime.GOOS != "windows" {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotFound)
return
}
ctx := r.Context()
w.Header().Set("Content-Type", "application/json")
@@ -382,6 +388,9 @@ func isAllDigit(s string) bool {
// connection. It's intended to give your non-root webserver access
// (www-data, caddy, nginx, etc) to certs.
func (a *actor) CanFetchCerts() bool {
if !buildfeatures.HasACME {
return false
}
if a.ci.IsUnixSock() && a.ci.Creds() != nil {
connUID, ok := a.ci.Creds().UserID()
if ok && connUID == userIDFromString(envknob.String("TS_PERMIT_CERT_UID")) {
@@ -398,6 +407,10 @@ func (a *actor) CanFetchCerts() bool {
//
// onDone must be called when the HTTP request is done.
func (s *Server) addActiveHTTPRequest(req *http.Request, actor ipnauth.Actor) (onDone func(), err error) {
if runtime.GOOS != "windows" && !buildfeatures.HasUnixSocketIdentity {
return func() {}, nil
}
if actor == nil {
return nil, errors.New("internal error: nil actor")
}
@@ -538,6 +551,10 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
// Windows and via $DEBUG_LISTENER/debug/ipn when tailscaled's --debug flag
// is used to run a debug server.
func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasDebug {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotFound)
return
}
lb := s.lb.Load()
if lb == nil {
http.Error(w, "no LocalBackend", http.StatusServiceUnavailable)