This commit is contained in:
2026-02-19 10:07:43 +00:00
parent 007438e372
commit 6e637ecf77
1763 changed files with 60820 additions and 279516 deletions

690
vendor/tailscale.com/tsnet/tsnet.go generated vendored
View File

@@ -27,12 +27,16 @@ import (
"time"
"tailscale.com/client/local"
"tailscale.com/client/tailscale"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
_ "tailscale.com/feature/condregister"
_ "tailscale.com/feature/c2n"
_ "tailscale.com/feature/condregister/identityfederation"
_ "tailscale.com/feature/condregister/oauthkey"
_ "tailscale.com/feature/condregister/portmapper"
_ "tailscale.com/feature/condregister/useproxy"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/internal/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
@@ -48,6 +52,7 @@ import (
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/types/bools"
"tailscale.com/types/logger"
@@ -112,6 +117,37 @@ type Server struct {
// used.
AuthKey string
// ClientSecret, if non-empty, is the OAuth client secret
// that will be used to generate authkeys via OAuth. It
// will be preferred over the TS_CLIENT_SECRET environment
// variable. If the node is already created (from state
// previously stored in Store), then this field is not
// used.
ClientSecret string
// ClientID, if non-empty, is the client ID used to generate
// authkeys via workload identity federation. It will be
// preferred over the TS_CLIENT_ID environment variable.
// If the node is already created (from state previously
// stored in Store), then this field is not used.
ClientID string
// IDToken, if non-empty, is the ID token from the identity
// provider to exchange with the control server for workload
// identity federation. It will be preferred over the
// TS_ID_TOKEN environment variable. If the node is already
// created (from state previously stored in Store), then this
// field is not used.
IDToken string
// Audience, if non-empty, is the audience to use when requesting
// an ID token from a well-known identity provider to exchange
// with the control server for workload identity federation. It
// will be preferred over the TS_AUDIENCE environment variable. If
// the node is already created (from state previously stored in Store),
// then this field is not used.
Audience string
// ControlURL optionally specifies the coordination server URL.
// If empty, the Tailscale default is used.
ControlURL string
@@ -125,27 +161,32 @@ type Server struct {
// field at zero unless you know what you are doing.
Port uint16
getCertForTesting func(*tls.ClientHelloInfo) (*tls.Certificate, error)
// AdvertiseTags specifies tags that should be applied to this node, for
// purposes of ACL enforcement. These can be referenced from the ACL policy
// document. Note that advertising a tag on the client doesn't guarantee
// that the control server will allow the node to adopt that tag.
AdvertiseTags []string
initOnce sync.Once
initErr error
lb *ipnlocal.LocalBackend
sys *tsd.System
netstack *netstack.Impl
netMon *netmon.Monitor
rootPath string // the state directory
hostname string
shutdownCtx context.Context
shutdownCancel context.CancelFunc
proxyCred string // SOCKS5 proxy auth for loopbackListener
localAPICred string // basic auth password for loopbackListener
loopbackListener net.Listener // optional loopback for localapi and proxies
localAPIListener net.Listener // in-memory, used by localClient
localClient *local.Client // in-memory
localAPIServer *http.Server
logbuffer *filch.Filch
logtail *logtail.Logger
logid logid.PublicID
initOnce sync.Once
initErr error
lb *ipnlocal.LocalBackend
sys *tsd.System
netstack *netstack.Impl
netMon *netmon.Monitor
rootPath string // the state directory
hostname string
shutdownCtx context.Context
shutdownCancel context.CancelFunc
proxyCred string // SOCKS5 proxy auth for loopbackListener
localAPICred string // basic auth password for loopbackListener
loopbackListener net.Listener // optional loopback for localapi and proxies
localAPIListener net.Listener // in-memory, used by localClient
localClient *local.Client // in-memory
localAPIServer *http.Server
resetServeConfigOnce sync.Once
logbuffer *filch.Filch
logtail *logtail.Logger
logid logid.PublicID
mu sync.Mutex
listeners map[listenKey]*listener
@@ -275,7 +316,13 @@ func (s *Server) Loopback() (addr string, proxyCred, localAPICred string, err er
// out the CONNECT code from tailscaled/proxy.go that uses
// httputil.ReverseProxy and adding auth support.
go func() {
lah := localapi.NewHandler(ipnauth.Self, s.lb, s.logf, s.logid)
lah := localapi.NewHandler(localapi.HandlerConfig{
Actor: ipnauth.Self,
Backend: s.lb,
Logf: s.logf,
LogID: s.logid,
EventBus: s.sys.Bus.Get(),
})
lah.PermitWrite = true
lah.PermitRead = true
lah.RequiredPassword = s.localAPICred
@@ -335,7 +382,7 @@ func (s *Server) Up(ctx context.Context) (*ipnstate.Status, error) {
return nil, fmt.Errorf("tsnet.Up: %w", err)
}
watcher, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialState|ipn.NotifyNoPrivateKeys)
watcher, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialState)
if err != nil {
return nil, fmt.Errorf("tsnet.Up: %w", err)
}
@@ -349,8 +396,8 @@ func (s *Server) Up(ctx context.Context) (*ipnstate.Status, error) {
if n.ErrMessage != nil {
return nil, fmt.Errorf("tsnet.Up: backend: %s", *n.ErrMessage)
}
if s := n.State; s != nil {
if *s == ipn.Running {
if st := n.State; st != nil {
if *st == ipn.Running {
status, err := lc.Status(ctx)
if err != nil {
return nil, fmt.Errorf("tsnet.Up: %w", err)
@@ -359,11 +406,15 @@ func (s *Server) Up(ctx context.Context) (*ipnstate.Status, error) {
return nil, errors.New("tsnet.Up: running, but no ip")
}
// Clear the persisted serve config state to prevent stale configuration
// from code changes. This is a temporary workaround until we have a better
// way to handle this. (2023-03-11)
if err := lc.SetServeConfig(ctx, new(ipn.ServeConfig)); err != nil {
return nil, fmt.Errorf("tsnet.Up: %w", err)
// The first time Up is run, clear the persisted serve config.
// We do this to prevent messy interactions with stale config in
// the face of code changes.
var srvResetErr error
s.resetServeConfigOnce.Do(func() {
srvResetErr = lc.SetServeConfig(ctx, new(ipn.ServeConfig))
})
if srvResetErr != nil {
return nil, fmt.Errorf("tsnet.Up: clearing serve config: %w", err)
}
return status, nil
@@ -435,8 +486,8 @@ func (s *Server) Close() error {
for _, ln := range s.listeners {
ln.closeLocked()
}
wg.Wait()
s.sys.Bus.Get().Close()
s.closed = true
return nil
}
@@ -482,6 +533,16 @@ func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
return ip4, ip6
}
// LogtailWriter returns an [io.Writer] that writes to Tailscale's logging service and will be only visible to Tailscale's
// support team. Logs written there cannot be retrieved by the user. This method always returns a non-nil value.
func (s *Server) LogtailWriter() io.Writer {
if s.logtail == nil {
return io.Discard
}
return s.logtail
}
func (s *Server) getAuthKey() string {
if v := s.AuthKey; v != "" {
return v
@@ -492,6 +553,34 @@ func (s *Server) getAuthKey() string {
return os.Getenv("TS_AUTH_KEY")
}
func (s *Server) getClientSecret() string {
if v := s.ClientSecret; v != "" {
return v
}
return os.Getenv("TS_CLIENT_SECRET")
}
func (s *Server) getClientID() string {
if v := s.ClientID; v != "" {
return v
}
return os.Getenv("TS_CLIENT_ID")
}
func (s *Server) getIDToken() string {
if v := s.IDToken; v != "" {
return v
}
return os.Getenv("TS_ID_TOKEN")
}
func (s *Server) getAudience() string {
if v := s.Audience; v != "" {
return v
}
return os.Getenv("TS_AUDIENCE")
}
func (s *Server) start() (reterr error) {
var closePool closeOnErrorPool
defer closePool.closeAllIfError(&reterr)
@@ -534,10 +623,7 @@ func (s *Server) start() (reterr error) {
if err != nil {
return err
}
s.rootPath, err = getTSNetDir(s.logf, confDir, prog)
if err != nil {
return err
}
s.rootPath = filepath.Join(confDir, "tsnet-"+prog)
}
if err := os.MkdirAll(s.rootPath, 0700); err != nil {
return err
@@ -558,26 +644,28 @@ func (s *Server) start() (reterr error) {
s.Logf(format, a...)
}
sys := new(tsd.System)
sys := tsd.NewSystem()
s.sys = sys
if err := s.startLogger(&closePool, sys.HealthTracker(), tsLogf); err != nil {
if err := s.startLogger(&closePool, sys.HealthTracker.Get(), tsLogf); err != nil {
return err
}
s.netMon, err = netmon.New(tsLogf)
s.netMon, err = netmon.New(sys.Bus.Get(), tsLogf)
if err != nil {
return err
}
closePool.add(s.netMon)
s.dialer = &tsdial.Dialer{Logf: tsLogf} // mutated below (before used)
s.dialer.SetBus(sys.Bus.Get())
eng, err := wgengine.NewUserspaceEngine(tsLogf, wgengine.Config{
EventBus: sys.Bus.Get(),
ListenPort: s.Port,
NetMon: s.netMon,
Dialer: s.dialer,
SetSubsystem: sys.Set,
ControlKnobs: sys.ControlKnobs(),
HealthTracker: sys.HealthTracker(),
HealthTracker: sys.HealthTracker.Get(),
Metrics: sys.UserMetricsRegistry(),
})
if err != nil {
@@ -585,7 +673,7 @@ func (s *Server) start() (reterr error) {
}
closePool.add(s.dialer)
sys.Set(eng)
sys.HealthTracker().SetMetricsRegistry(sys.UserMetricsRegistry())
sys.HealthTracker.Get().SetMetricsRegistry(sys.UserMetricsRegistry())
// TODO(oxtoacart): do we need to support Taildrive on tsnet, and if so, how?
ns, err := netstack.Create(tsLogf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper())
@@ -659,7 +747,11 @@ func (s *Server) start() (reterr error) {
prefs.WantRunning = true
prefs.ControlURL = s.ControlURL
prefs.RunWebClient = s.RunWebClient
authKey := s.getAuthKey()
prefs.AdvertiseTags = s.AdvertiseTags
authKey, err := s.resolveAuthKey()
if err != nil {
return fmt.Errorf("error resolving auth key: %w", err)
}
err = lb.Start(ipn.Options{
UpdatePrefs: prefs,
AuthKey: authKey,
@@ -679,7 +771,13 @@ func (s *Server) start() (reterr error) {
go s.printAuthURLLoop()
// Run the localapi handler, to allow fetching LetsEncrypt certs.
lah := localapi.NewHandler(ipnauth.Self, lb, tsLogf, s.logid)
lah := localapi.NewHandler(localapi.HandlerConfig{
Actor: ipnauth.Self,
Backend: lb,
Logf: tsLogf,
LogID: s.logid,
EventBus: sys.Bus.Get(),
})
lah.PermitWrite = true
lah.PermitRead = true
@@ -699,6 +797,51 @@ func (s *Server) start() (reterr error) {
return nil
}
func (s *Server) resolveAuthKey() (string, error) {
authKey := s.getAuthKey()
var err error
// Try to use an OAuth secret to generate an auth key if that functionality
// is available.
resolveViaOAuth, oauthOk := tailscale.HookResolveAuthKey.GetOk()
if oauthOk {
clientSecret := authKey
if authKey == "" {
clientSecret = s.getClientSecret()
}
authKey, err = resolveViaOAuth(s.shutdownCtx, clientSecret, s.AdvertiseTags)
if err != nil {
return "", err
}
}
// Try to resolve the auth key via workload identity federation if that functionality
// is available and no auth key is yet determined.
resolveViaWIF, wifOk := tailscale.HookResolveAuthKeyViaWIF.GetOk()
if wifOk && authKey == "" {
clientID := s.getClientID()
idToken := s.getIDToken()
audience := s.getAudience()
if clientID != "" && idToken == "" && audience == "" {
return "", fmt.Errorf("client ID for workload identity federation found, but ID token and audience are empty")
}
if idToken != "" && audience != "" {
return "", fmt.Errorf("only one of ID token and audience should be for workload identity federation")
}
if clientID == "" {
if idToken != "" {
return "", fmt.Errorf("ID token for workload identity federation found, but client ID is empty")
}
if audience != "" {
return "", fmt.Errorf("audience for workload identity federation found, but client ID is empty")
}
}
authKey, err = resolveViaWIF(s.shutdownCtx, s.ControlURL, clientID, idToken, audience, s.AdvertiseTags)
if err != nil {
return "", err
}
}
return authKey, nil
}
func (s *Server) startLogger(closePool *closeOnErrorPool, health *health.Tracker, tsLogf logger.Logf) error {
if testenv.InTest() {
return nil
@@ -730,6 +873,7 @@ func (s *Server) startLogger(closePool *closeOnErrorPool, health *health.Tracker
Stderr: io.Discard, // log everything to Buffer
Buffer: s.logbuffer,
CompressLogs: true,
Bus: s.sys.Bus.Get(),
HTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, s.netMon, health, tsLogf)},
MetricsDelta: clientmetric.EncodeLogTailMetricsDelta,
}
@@ -894,103 +1038,6 @@ func (s *Server) getUDPHandlerForFlow(src, dst netip.AddrPort) (handler func(net
return func(c nettype.ConnPacketConn) { ln.handle(c) }, true
}
// getTSNetDir usually just returns filepath.Join(confDir, "tsnet-"+prog)
// with no error.
//
// One special case is that it renames old "tslib-" directories to
// "tsnet-", and that rename might return an error.
//
// TODO(bradfitz): remove this maybe 6 months after 2022-03-17,
// once people (notably Tailscale corp services) have updated.
func getTSNetDir(logf logger.Logf, confDir, prog string) (string, error) {
oldPath := filepath.Join(confDir, "tslib-"+prog)
newPath := filepath.Join(confDir, "tsnet-"+prog)
fi, err := os.Lstat(oldPath)
if os.IsNotExist(err) {
// Common path.
return newPath, nil
}
if err != nil {
return "", err
}
if !fi.IsDir() {
return "", fmt.Errorf("expected old tslib path %q to be a directory; got %v", oldPath, fi.Mode())
}
// At this point, oldPath exists and is a directory. But does
// the new path exist?
fi, err = os.Lstat(newPath)
if err == nil && fi.IsDir() {
// New path already exists somehow. Ignore the old one and
// don't try to migrate it.
return newPath, nil
}
if err != nil && !os.IsNotExist(err) {
return "", err
}
if err := os.Rename(oldPath, newPath); err != nil {
return "", err
}
logf("renamed old tsnet state storage directory %q to %q", oldPath, newPath)
return newPath, nil
}
// APIClient returns a tailscale.Client that can be used to make authenticated
// requests to the Tailscale control server.
// It requires the user to set tailscale.I_Acknowledge_This_API_Is_Unstable.
//
// Deprecated: use AuthenticatedAPITransport with tailscale.com/client/tailscale/v2 instead.
func (s *Server) APIClient() (*tailscale.Client, error) {
if !tailscale.I_Acknowledge_This_API_Is_Unstable {
return nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable")
}
if err := s.Start(); err != nil {
return nil, err
}
c := tailscale.NewClient("-", nil)
c.UserAgent = "tailscale-tsnet"
c.HTTPClient = &http.Client{Transport: s.lb.KeyProvingNoiseRoundTripper()}
return c, nil
}
// I_Acknowledge_This_API_Is_Experimental must be set true to use AuthenticatedAPITransport()
// for now.
var I_Acknowledge_This_API_Is_Experimental = false
// AuthenticatedAPITransport provides an HTTP transport that can be used with
// the control server API without needing additional authentication details. It
// authenticates using the current client's nodekey.
//
// It requires the user to set I_Acknowledge_This_API_Is_Experimental.
//
// For example:
//
// import "net/http"
// import "tailscale.com/client/tailscale/v2"
// import "tailscale.com/tsnet"
//
// var s *tsnet.Server
// ...
// rt, err := s.AuthenticatedAPITransport()
// // handler err ...
// var client tailscale.Client{HTTP: http.Client{
// Timeout: 1*time.Minute,
// UserAgent: "your-useragent-here",
// Transport: rt,
// }}
func (s *Server) AuthenticatedAPITransport() (http.RoundTripper, error) {
if !I_Acknowledge_This_API_Is_Experimental {
return nil, errors.New("use of AuthenticatedAPITransport without setting I_Acknowledge_This_API_Is_Experimental")
}
if err := s.Start(); err != nil {
return nil, err
}
return s.lb.KeyProvingNoiseRoundTripper(), nil
}
// Listen announces only on the Tailscale network.
// It will start the server if it has not been started yet.
//
@@ -1082,9 +1129,6 @@ func (s *Server) RegisterFallbackTCPHandler(cb FallbackTCPHandler) func() {
// It calls GetCertificate on the localClient, passing in the ClientHelloInfo.
// For testing, if s.getCertForTesting is set, it will call that instead.
func (s *Server) getCert(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if s.getCertForTesting != nil {
return s.getCertForTesting(hi)
}
lc, err := s.LocalClient()
if err != nil {
return nil, err
@@ -1097,13 +1141,33 @@ type FunnelOption interface {
funnelOption()
}
type funnelOnly int
type funnelOnly struct{}
func (funnelOnly) funnelOption() {}
// FunnelOnly configures the listener to only respond to connections from Tailscale Funnel.
// The local tailnet will not be able to connect to the listener.
func FunnelOnly() FunnelOption { return funnelOnly(1) }
func FunnelOnly() FunnelOption { return funnelOnly{} }
type funnelTLSConfig struct{ conf *tls.Config }
func (f funnelTLSConfig) funnelOption() {}
// FunnelTLSConfig configures the TLS configuration for [Server.ListenFunnel]
//
// This is rarely needed but can permit requiring client certificates, specific
// ciphers suites, etc.
//
// The provided conf should at least be able to get a certificate, setting
// GetCertificate, Certificates or GetConfigForClient appropriately.
// The most common configuration is to set GetCertificate to
// Server.LocalClient's GetCertificate method.
//
// Unless [FunnelOnly] is also used, the configuration is also used for
// in-tailnet connections that don't arrive over Funnel.
func FunnelTLSConfig(conf *tls.Config) FunnelOption {
return funnelTLSConfig{conf: conf}
}
// ListenFunnel announces on the public internet using Tailscale Funnel.
//
@@ -1136,6 +1200,26 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
return nil, err
}
// Process, validate opts.
lnOn := listenOnBoth
var tlsConfig *tls.Config
for _, opt := range opts {
switch v := opt.(type) {
case funnelTLSConfig:
if v.conf == nil {
return nil, errors.New("invalid nil FunnelTLSConfig")
}
tlsConfig = v.conf
case funnelOnly:
lnOn = listenOnFunnel
default:
return nil, fmt.Errorf("unknown opts FunnelOption type %T", v)
}
}
if tlsConfig == nil {
tlsConfig = &tls.Config{GetCertificate: s.getCert}
}
ctx := context.Background()
st, err := s.Up(ctx)
if err != nil {
@@ -1164,28 +1248,288 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
}
domain := st.CertDomains[0]
hp := ipn.HostPort(domain + ":" + portStr)
var cleanupOnClose func() error
if !srvConfig.AllowFunnel[hp] {
mak.Set(&srvConfig.AllowFunnel, hp, true)
srvConfig.AllowFunnel[hp] = true
if err := lc.SetServeConfig(ctx, srvConfig); err != nil {
return nil, err
}
cleanupOnClose = func() error {
sc, err := lc.GetServeConfig(ctx)
if err != nil {
return fmt.Errorf("cleaning config changes: %w", err)
}
if sc.AllowFunnel != nil {
delete(sc.AllowFunnel, hp)
}
if err := lc.SetServeConfig(ctx, sc); err != nil {
return fmt.Errorf("cleaning config changes: %w", err)
}
return nil
}
}
// Start a funnel listener.
lnOn := listenOnBoth
for _, opt := range opts {
if _, ok := opt.(funnelOnly); ok {
lnOn = listenOnFunnel
}
}
ln, err := s.listen(network, addr, lnOn)
if err != nil {
return nil, err
}
return tls.NewListener(ln, &tls.Config{
GetCertificate: s.getCert,
}), nil
ln = &cleanupListener{Listener: ln, cleanup: cleanupOnClose}
return tls.NewListener(ln, tlsConfig), nil
}
// ServiceMode defines how a Service is run. Currently supported modes are:
// - [ServiceModeTCP]
// - [ServiceModeHTTP]
//
// For more information, see [Server.ListenService].
type ServiceMode interface {
// network is the network this Service will advertise on. Per Go convention,
// this should be lowercase, e.g. 'tcp'.
network() string
}
// serviceModeWithPort is a convenience type to extract the port from
// ServiceMode types which have one.
type serviceModeWithPort interface {
ServiceMode
port() uint16
}
// ServiceModeTCP is used to configure a TCP Service via [Server.ListenService].
type ServiceModeTCP struct {
// Port is the TCP port to advertise. If this Service needs to advertise
// multiple ports, call ListenService multiple times.
Port uint16
// TerminateTLS means that TLS connections will be terminated before being
// forwarded to the listener. In this case, the only server name indicator
// (SNI) permitted is the Service's fully-qualified domain name.
TerminateTLS bool
// PROXYProtocolVersion indicates whether to send a PROXY protocol header
// before forwarding the connection to the listener and which version of the
// protocol to use.
//
// For more information, see
// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
PROXYProtocolVersion int
}
func (ServiceModeTCP) network() string { return "tcp" }
func (m ServiceModeTCP) port() uint16 { return m.Port }
// ServiceModeHTTP is used to configure an HTTP Service via
// [Server.ListenService].
type ServiceModeHTTP struct {
// Port is the TCP port to advertise. If this Service needs to advertise
// multiple ports, call ListenService multiple times.
Port uint16
// HTTPS, if true, means that the listener should handle connections as
// HTTPS connections. In this case, the only server name indicator (SNI)
// permitted is the Service's fully-qualified domain name.
HTTPS bool
// AcceptAppCaps defines the app capabilities to forward to the server. The
// keys in this map are the mount points for each set of capabilities.
//
// By example,
//
// AcceptAppCaps: map[string][]string{
// "/": {"example.com/cap/all-paths"},
// "/foo": {"example.com/cap/all-paths", "example.com/cap/foo"},
// }
//
// would forward example.com/cap/all-paths to all paths on the server and
// example.com/cap/foo only to paths beginning with /foo.
//
// For more information on app capabilities, see
// https://tailscale.com/kb/1537/grants-app-capabilities
AcceptAppCaps map[string][]string
// PROXYProtocolVersion indicates whether to send a PROXY protocol header
// before forwarding the connection to the listener and which version of the
// protocol to use.
//
// For more information, see
// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
PROXYProtocol int
}
func (ServiceModeHTTP) network() string { return "tcp" }
func (m ServiceModeHTTP) port() uint16 { return m.Port }
func (m ServiceModeHTTP) capsMap() map[string][]tailcfg.PeerCapability {
capsMap := map[string][]tailcfg.PeerCapability{}
for path, capNames := range m.AcceptAppCaps {
caps := make([]tailcfg.PeerCapability, 0, len(capNames))
for _, c := range capNames {
caps = append(caps, tailcfg.PeerCapability(c))
}
capsMap[path] = caps
}
return capsMap
}
// A ServiceListener is a network listener for a Tailscale Service. For more
// information about Services, see
// https://tailscale.com/kb/1552/tailscale-services
type ServiceListener struct {
net.Listener
addr addr
// FQDN is the fully-qualifed domain name of this Service.
FQDN string
}
// Addr returns the listener's network address. This will be the Service's
// fully-qualified domain name (FQDN) and the port.
//
// A hostname is not truly a network address, but Services listen on multiple
// addresses (the IPv4 and IPv6 virtual IPs).
func (sl ServiceListener) Addr() net.Addr {
return sl.addr
}
// ErrUntaggedServiceHost is returned by ListenService when run on a node
// without any ACL tags. A node must use a tag-based identity to act as a
// Service host. For more information, see:
// https://tailscale.com/kb/1552/tailscale-services#prerequisites
var ErrUntaggedServiceHost = errors.New("service hosts must be tagged nodes")
// ListenService creates a network listener for a Tailscale Service. This will
// advertise this node as hosting the Service. Note that:
// - Approval must still be granted by an admin or by ACL auto-approval rules.
// - Service hosts must be tagged nodes.
// - A valid Service host must advertise all ports defined for the Service.
//
// To advertise a Service with multiple ports, run ListenService multiple times.
// For more information about Services, see
// https://tailscale.com/kb/1552/tailscale-services
func (s *Server) ListenService(name string, mode ServiceMode) (*ServiceListener, error) {
if err := tailcfg.ServiceName(name).Validate(); err != nil {
return nil, err
}
if mode == nil {
return nil, errors.New("mode may not be nil")
}
svcName := name
// TODO(hwh33,tailscale/corp#35859): support TUN mode
ctx := context.Background()
_, err := s.Up(ctx)
if err != nil {
return nil, err
}
st := s.lb.StatusWithoutPeers()
if st.Self.Tags == nil || st.Self.Tags.Len() == 0 {
return nil, ErrUntaggedServiceHost
}
advertisedServices := s.lb.Prefs().AdvertiseServices().AsSlice()
if !slices.Contains(advertisedServices, svcName) {
// TODO(hwh33,tailscale/corp#35860): clean these prefs up when (a) we
// exit early due to error or (b) when the returned listener is closed.
_, err = s.lb.EditPrefs(&ipn.MaskedPrefs{
AdvertiseServicesSet: true,
Prefs: ipn.Prefs{
AdvertiseServices: append(advertisedServices, svcName),
},
})
if err != nil {
return nil, fmt.Errorf("updating advertised Services: %w", err)
}
}
srvConfig := new(ipn.ServeConfig)
sc, srvConfigETag, err := s.lb.ServeConfigETag()
if err != nil {
return nil, fmt.Errorf("fetching current serve config: %w", err)
}
if sc.Valid() {
srvConfig = sc.AsStruct()
}
fqdn := tailcfg.ServiceName(svcName).WithoutPrefix() + "." + st.CurrentTailnet.MagicDNSSuffix
// svcAddr is used to implement Addr() on the returned listener.
svcAddr := addr{
network: mode.network(),
// A hostname is not a network address, but Services listen on
// multiple addresses (the IPv4 and IPv6 virtual IPs), and there's
// no clear winner here between the two. Therefore prefer the FQDN.
//
// In the case of TCP or HTTP Services, the port will be added below.
addr: fqdn,
}
if m, ok := mode.(serviceModeWithPort); ok {
if m.port() == 0 {
return nil, errors.New("must specify a port to advertise")
}
svcAddr.addr += ":" + strconv.Itoa(int(m.port()))
}
// Start listening on a local TCP socket.
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
return nil, fmt.Errorf("starting local listener: %w", err)
}
switch m := mode.(type) {
case ServiceModeTCP:
// Forward all connections from service-hostname:port to our socket.
srvConfig.SetTCPForwardingForService(
m.Port, ln.Addr().String(), m.TerminateTLS,
tailcfg.ServiceName(svcName), m.PROXYProtocolVersion, st.CurrentTailnet.MagicDNSSuffix)
case ServiceModeHTTP:
// For HTTP Services, proxy all connections to our socket.
mds := st.CurrentTailnet.MagicDNSSuffix
haveRootHandler := false
// We need to add a separate proxy for each mount point in the caps map.
for path, caps := range m.capsMap() {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
h := ipn.HTTPHandler{
AcceptAppCaps: caps,
Proxy: ln.Addr().String(),
}
if path == "/" {
haveRootHandler = true
} else {
h.Proxy += path
}
srvConfig.SetWebHandler(&h, svcName, m.Port, path, m.HTTPS, mds)
}
// We always need a root handler.
if !haveRootHandler {
h := ipn.HTTPHandler{Proxy: ln.Addr().String()}
srvConfig.SetWebHandler(&h, svcName, m.Port, "/", m.HTTPS, mds)
}
default:
ln.Close()
return nil, fmt.Errorf("unknown ServiceMode type %T", m)
}
if err := s.lb.SetServeConfig(srvConfig, srvConfigETag); err != nil {
ln.Close()
return nil, err
}
// TODO(hwh33,tailscale/corp#35860): clean up state (advertising prefs,
// serve config changes) when the returned listener is closed.
return &ServiceListener{
Listener: ln,
FQDN: fqdn,
addr: svcAddr,
}, nil
}
type listenOn string
@@ -1282,6 +1626,12 @@ func (s *Server) listen(network, addr string, lnOn listenOn) (net.Listener, erro
return ln, nil
}
// GetRootPath returns the root path of the tsnet server.
// This is where the state file and other data is stored.
func (s *Server) GetRootPath() string {
return s.rootPath
}
// CapturePcap can be called by the application code compiled with tsnet to save a pcap
// of packets which the netstack within tsnet sees. This is expected to be useful during
// debugging, probably not useful for production.
@@ -1343,7 +1693,12 @@ func (ln *listener) Accept() (net.Conn, error) {
}
}
func (ln *listener) Addr() net.Addr { return addr{ln} }
func (ln *listener) Addr() net.Addr {
return addr{
network: ln.keys[0].network,
addr: ln.addr,
}
}
func (ln *listener) Close() error {
ln.s.mu.Lock()
@@ -1383,7 +1738,26 @@ func (ln *listener) handle(c net.Conn) {
// Server returns the tsnet Server associated with the listener.
func (ln *listener) Server() *Server { return ln.s }
type addr struct{ ln *listener }
type addr struct {
network, addr string
}
func (a addr) Network() string { return a.ln.keys[0].network }
func (a addr) String() string { return a.ln.addr }
func (a addr) Network() string { return a.network }
func (a addr) String() string { return a.addr }
// cleanupListener wraps a net.Listener with a function to be run on Close.
type cleanupListener struct {
net.Listener
cleanupOnce sync.Once
cleanup func() error // nil if unused
}
func (cl *cleanupListener) Close() error {
var cleanupErr error
cl.cleanupOnce.Do(func() {
if cl.cleanup != nil {
cleanupErr = cl.cleanup()
}
})
return errors.Join(cl.Listener.Close(), cleanupErr)
}