// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Package tsnet embeds a Tailscale node directly into a Go program,
// allowing it to join a tailnet and accept or dial connections without
// running a separate tailscaled daemon or requiring any system-level
// configuration.
//
// # Overview
//
// Normally, Tailscale runs as a background system service (tailscaled)
// that manages a virtual network interface for the whole machine. tsnet
// takes a different approach: it runs a fully self-contained Tailscale
// node inside your process using a userspace TCP/IP stack (gVisor).
// This means:
//
//   - No root privileges required.
//   - No system daemons to install or manage.
//   - Multiple independent Tailscale nodes can run within a single binary.
//   - The node's [Tailscale identity] and state are stored in a directory you control.
//
// The core type is [Server], which represents one embedded Tailscale
// node. Calling [Server.Listen] or [Server.Dial] routes traffic
// exclusively over the tailnet. The standard library's [net.Listener]
// and [net.Conn] interfaces are returned, so any existing Go HTTP
// server, gRPC server, or other net-based code works without
// modification.
//
// # Usage
//
//	import "tailscale.com/tsnet"
//
//	s := &tsnet.Server{
//		Hostname: "my-service",
//		AuthKey:  os.Getenv("TS_AUTHKEY"),
//	}
//	defer s.Close()
//
//	ln, err := s.Listen("tcp", ":80")
//	if err != nil {
//		log.Fatal(err)
//	}
//	log.Fatal(http.Serve(ln, myHandler))
//
// On first run, if no [Server.AuthKey] is provided and the node is not
// already enrolled, the server logs an authentication URL. Open it in a
// browser to add the node to your tailnet.
//
// # Authentication
//
// A [Server] authenticates using, in order of precedence:
//
//  1. [Server.AuthKey].
//
//  2. The TS_AUTHKEY environment variable.
//
//  3. The TS_AUTH_KEY environment variable.
//
//  4. An OAuth client secret ([Server.ClientSecret] or TS_CLIENT_SECRET),
//     used to mint an auth key.
//
//  5. Workload identity federation ([Server.ClientID] plus
//     [Server.IDToken] or [Server.Audience]). Available only if the
//     program imports the feature:
//
//     import _ "tailscale.com/feature/identityfederation"
//
//     The feature is not linked by default to keep the AWS SDK and
//     other cloud-provider dependencies out of programs that don't
//     use workload identity federation.
//
//  6. An interactive login URL printed to [Server.UserLogf].
//
// If the node is already enrolled (state found in [Server.Store]), the
// auth key is ignored unless TSNET_FORCE_LOGIN=1 is set.
//
// # Identifying callers
//
// Use the WhoIs method on the client returned by [Server.LocalClient]
// to identify who is making a request:
//
//	lc, _ := srv.LocalClient()
//	http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//		who, err := lc.WhoIs(r.Context(), r.RemoteAddr)
//		if err != nil {
//			http.Error(w, err.Error(), 500)
//			return
//		}
//		fmt.Fprintf(w, "Hello, %s!", who.UserProfile.LoginName)
//	}))
//
// # Tailscale Funnel
//
// [Server.ListenFunnel] exposes your service on the public internet.
// [Tailscale Funnel] currently supports TCP on ports 443, 8443, and
// 10000. HTTPS must be enabled in the Tailscale admin console.
//
//	ln, err := srv.ListenFunnel("tcp", ":443")
//	// ln is a TLS listener; connections can come from anywhere on the
//	// internet as well as from your tailnet.
//
//	// To restrict to public traffic only:
//	ln, err = srv.ListenFunnel("tcp", ":443", tsnet.FunnelOnly())
//
// # Tailscale Services
//
// [Server.ListenService] advertises the node as a host for a named
// [Tailscale Service]. The node must use a tag-based identity. To
// advertise multiple ports, call ListenService once per port.
//
//	srv.AdvertiseTags = []string{"tag:myservice"}
//
//	ln, err := srv.ListenService("svc:my-service", tsnet.ServiceModeHTTP{
//		HTTPS: true,
//		Port:  443,
//	})
//	log.Printf("Listening on https://%s", ln.FQDN)
//
// # Running multiple nodes in one process
//
// Each [Server] instance is an independent node. Give each a unique
// [Server.Dir] and [Server.Hostname]:
//
//	for _, name := range []string{"frontend", "backend"} {
//		srv := &tsnet.Server{
//			Hostname:  name,
//			Dir:       filepath.Join(baseDir, name),
//			AuthKey:   os.Getenv("TS_AUTHKEY"),
//			Ephemeral: true,
//		}
//		srv.Start()
//	}
//
// [Tailscale identity]: https://tailscale.com/docs/concepts/tailscale-identity
// [Tailscale Funnel]: https://tailscale.com/docs/features/tailscale-funnel
// [Tailscale Service]: https://tailscale.com/docs/features/tailscale-services
package tsnet

import (
	"context"
	crand "crypto/rand"
	"crypto/tls"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"log"
	"math"
	"net"
	"net/http"
	"net/netip"
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/tailscale/wireguard-go/tun"
	"tailscale.com/client/local"
	"tailscale.com/control/controlclient"
	"tailscale.com/envknob"
	_ "tailscale.com/feature/c2n"
	_ "tailscale.com/feature/condregister/netlog"
	_ "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"
	"tailscale.com/ipn/ipnstate"
	"tailscale.com/ipn/localapi"
	"tailscale.com/ipn/store"
	"tailscale.com/ipn/store/mem"
	"tailscale.com/logpolicy"
	"tailscale.com/logtail"
	"tailscale.com/logtail/filch"
	"tailscale.com/net/memnet"
	"tailscale.com/net/netmon"
	"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"
	"tailscale.com/types/logid"
	"tailscale.com/types/nettype"
	"tailscale.com/types/views"
	"tailscale.com/util/clientmetric"
	"tailscale.com/util/mak"
	"tailscale.com/util/set"
	"tailscale.com/util/testenv"
	"tailscale.com/wgengine"
	"tailscale.com/wgengine/netstack"
)

// Server is an embedded Tailscale server.
//
// Its exported fields may be changed until the first method call.
type Server struct {
	// Dir specifies the name of the directory to use for
	// state. If empty, a directory is selected automatically
	// under os.UserConfigDir (https://golang.org/pkg/os/#UserConfigDir).
	// based on the name of the binary.
	//
	// If you want to use multiple tsnet services in the same
	// binary, you will need to make sure that Dir is set uniquely
	// for each service. A good pattern for this is to have a
	// "base" directory (such as your mutable storage folder) and
	// then append the hostname on the end of it.
	Dir string

	// Store specifies the state store to use.
	//
	// If nil, a new FileStore is initialized at `Dir/tailscaled.state`.
	// See tailscale.com/ipn/store for supported stores.
	//
	// Logs will automatically be uploaded to log.tailscale.com,
	// where the configuration file for logging will be saved at
	// `Dir/tailscaled.log.conf`.
	Store ipn.StateStore

	// Hostname is the hostname to present to the control server.
	// If empty, the binary name is used.
	Hostname string

	// UserLogf, if non-nil, specifies the logger to use for logs generated by
	// the Server itself intended to be seen by the user such as the AuthURL for
	// login and status updates. If unset, log.Printf is used.
	UserLogf logger.Logf

	// Logf, if set is used for logs generated by the backend such as the
	// LocalBackend and MagicSock. It is verbose and intended for debugging.
	// If unset, logs are discarded.
	Logf logger.Logf

	// Ephemeral, if true, specifies that the instance should register
	// as an Ephemeral node (https://tailscale.com/s/ephemeral-nodes).
	Ephemeral bool

	// AuthKey, if non-empty, is the auth key to create the node
	// and will be preferred over the TS_AUTHKEY environment
	// variable. If the node is already created (from state
	// previously stored in Store), then this field is not
	// 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.
	// If empty, it defaults to the TS_CONTROL_URL environment variable.
	// If that is also empty, the Tailscale default is used.
	ControlURL string

	// RunWebClient, if true, runs a client for managing this node over
	// its Tailscale interface on port 5252.
	RunWebClient bool

	// Port is the UDP port to listen on for WireGuard and peer-to-peer
	// traffic. If zero, a port is automatically selected. Leave this
	// field at zero unless you know what you are doing.
	Port uint16

	// 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

	// Tun, if non-nil, specifies a custom tun.Device to use for packet I/O.
	//
	// This field must be set before calling Start.
	Tun tun.Device

	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
	resetServeStateOnce sync.Once
	logbuffer           *filch.Filch
	logtail             *logtail.Logger
	logid               logid.PublicID

	mu                  sync.Mutex
	listeners           map[listenKey]*listener
	nextEphemeralPort   uint16 // next port to try in ephemeral range; 0 means use ephemeralPortFirst
	fallbackTCPHandlers set.HandleSet[FallbackTCPHandler]
	dialer              *tsdial.Dialer
	advertisedServices  map[tailcfg.ServiceName]int
	closeOnce           sync.Once
}

// FallbackTCPHandler describes the callback which
// conditionally handles an incoming TCP flow for the
// provided (src/port, dst/port) 4-tuple. These are registered
// as handlers of last resort, and are called only if no
// listener could handle the incoming flow.
//
// If the callback returns intercept=false, the flow is rejected.
//
// When intercept=true, the behavior depends on whether the returned handler
// is non-nil: if nil, the connection is rejected. If non-nil, handler takes
// over the TCP conn.
type FallbackTCPHandler func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool)

// Dial connects to the address on the tailnet.
// It will start the server if it has not been started yet.
func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, error) {
	if err := s.Start(); err != nil {
		return nil, err
	}
	if err := s.awaitRunning(ctx); err != nil {
		return nil, err
	}
	return s.dialer.UserDial(ctx, network, address)
}

// awaitRunning waits until the backend is in state Running.
// If the backend is in state Starting, it blocks until it reaches
// a terminal state (such as Stopped, NeedsMachineAuth)
// or the context expires.
func (s *Server) awaitRunning(ctx context.Context) error {
	st := s.lb.State()
	for {
		if err := ctx.Err(); err != nil {
			return err
		}
		switch st {
		case ipn.Running:
			return nil
		case ipn.NeedsLogin, ipn.Starting:
			// Even after LocalBackend.Start, the state machine is still briefly
			// in the "NeedsLogin" state. So treat that as also "Starting" and
			// wait for us to get out of that state.
			s.lb.WatchNotifications(ctx, ipn.NotifyInitialState, nil, func(n *ipn.Notify) (keepGoing bool) {
				if n.State != nil {
					st = *n.State
				}
				return st == ipn.NeedsLogin || st == ipn.Starting
			})
		default:
			return fmt.Errorf("tsnet: backend in state %v", st)
		}
	}
}

// HTTPClient returns an HTTP client that is configured to connect over Tailscale.
//
// This is useful if you need to have your tsnet services connect to other devices on
// your tailnet.
func (s *Server) HTTPClient() *http.Client {
	return &http.Client{
		Transport: &http.Transport{
			DialContext: s.Dial,
		},
	}
}

// LocalClient returns a LocalClient that speaks to s.
//
// It will start the server if it has not been started yet. If the server's
// already been started successfully, it doesn't return an error.
func (s *Server) LocalClient() (*local.Client, error) {
	if err := s.Start(); err != nil {
		return nil, err
	}
	return s.localClient, nil
}

// TestHooks are hooks meant for internal-testing only; they're not stable
// or documented, intentionally.
var TestHooks testHooks

type testHooks struct{}

// LocalBackend returns the [ipnlocal.LocalBackend] backing s. It panics
// outside of tests.
func (testHooks) LocalBackend(s *Server) *ipnlocal.LocalBackend {
	testenv.AssertInTest()
	return s.lb
}

// Loopback starts a routing server on a loopback address.
//
// The server has multiple functions.
//
// It can be used as a SOCKS5 proxy onto the tailnet.
// Authentication is required with the username "tsnet" and
// the value of proxyCred used as the password.
//
// The HTTP server also serves out the "LocalAPI" on /localapi.
// As the LocalAPI is powerful, access to endpoints requires BOTH passing a
// "Sec-Tailscale: localapi" HTTP header and passing localAPICred as basic auth.
//
// If you only need to use the LocalAPI from Go, then prefer LocalClient
// as it does not require communication via TCP.
func (s *Server) Loopback() (addr string, proxyCred, localAPICred string, err error) {
	if err := s.Start(); err != nil {
		return "", "", "", err
	}

	if s.loopbackListener == nil {
		var proxyCred [16]byte
		if _, err := crand.Read(proxyCred[:]); err != nil {
			return "", "", "", err
		}
		s.proxyCred = hex.EncodeToString(proxyCred[:])

		var cred [16]byte
		if _, err := crand.Read(cred[:]); err != nil {
			return "", "", "", err
		}
		s.localAPICred = hex.EncodeToString(cred[:])

		ln, err := net.Listen("tcp", "127.0.0.1:0")
		if err != nil {
			return "", "", "", err
		}
		s.loopbackListener = ln

		socksLn, httpLn := proxymux.SplitSOCKSAndHTTP(ln)

		// TODO: add HTTP proxy support. Probably requires factoring
		// out the CONNECT code from tailscaled/proxy.go that uses
		// httputil.ReverseProxy and adding auth support.
		go func() {
			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
			h := &localSecHandler{h: lah, cred: s.localAPICred}

			if err := http.Serve(httpLn, h); err != nil {
				s.logf("localapi tcp serve error: %v", err)
			}
		}()
		s5l := logger.WithPrefix(s.logf, "socks5: ")
		s5s := &socks5.Server{
			Logf:     s5l,
			Dialer:   s.dialer.UserDial,
			Username: "tsnet",
			Password: s.proxyCred,
		}
		go func() {
			s5l("SOCKS5 server exited: %v", s5s.Serve(socksLn))
		}()
	}

	lbAddr := s.loopbackListener.Addr()
	if lbAddr == nil {
		// https://github.com/tailscale/tailscale/issues/7488
		panic("loopbackListener has no Addr")
	}
	return lbAddr.String(), s.proxyCred, s.localAPICred, nil
}

type localSecHandler struct {
	h    http.Handler
	cred string
}

func (h *localSecHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Header.Get("Sec-Tailscale") != "localapi" {
		w.WriteHeader(403)
		io.WriteString(w, "missing 'Sec-Tailscale: localapi' header")
		return
	}
	h.h.ServeHTTP(w, r)
}

// Start connects the server to the tailnet.
// Optional: any calls to Dial/Listen will also call Start.
func (s *Server) Start() error {
	hostinfo.SetPackage("tsnet")
	s.initOnce.Do(s.doInit)
	return s.initErr
}

// Up connects the server to the tailnet and waits until it is running.
// On success it returns the current status, including a Tailscale IP address.
func (s *Server) Up(ctx context.Context) (*ipnstate.Status, error) {
	lc, err := s.LocalClient() // calls Start
	if err != nil {
		return nil, fmt.Errorf("tsnet.Up: %w", err)
	}

	watcher, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialState)
	if err != nil {
		return nil, fmt.Errorf("tsnet.Up: %w", err)
	}
	defer watcher.Close()

	for {
		n, err := watcher.Next()
		if err != nil {
			return nil, fmt.Errorf("tsnet.Up: %w", err)
		}
		if n.ErrMessage != nil {
			return nil, fmt.Errorf("tsnet.Up: backend: %s", *n.ErrMessage)
		}
		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)
				}
				if len(status.TailscaleIPs) == 0 {
					return nil, errors.New("tsnet.Up: running, but no ip")
				}

				// The first time Up is run, clear the persisted serve config
				// and Service advertisements. We do this to prevent messy
				// interactions with stale config in the face of code changes.
				var srvCfgErr error
				var svcAdErr error
				s.resetServeStateOnce.Do(func() {
					if err := lc.SetServeConfig(ctx, new(ipn.ServeConfig)); err != nil {
						srvCfgErr = fmt.Errorf("clearing serve config: %w", err)
					}
					_, err := s.lb.EditPrefs(&ipn.MaskedPrefs{
						AdvertiseServicesSet: true,
						Prefs: ipn.Prefs{
							AdvertiseServices: []string{},
						},
					})
					if err != nil {
						svcAdErr = fmt.Errorf("clearing Service advertisements: %w", err)
					}
				})
				if err := errors.Join(srvCfgErr, svcAdErr); err != nil {
					return nil, fmt.Errorf("tsnet.Up: %w", err)
				}

				return status, nil
			}
			// TODO: in the future, return an error on ipn.NeedsLogin
			// and ipn.NeedsMachineAuth to improve the UX of trying
			// out the tsnet package.
			//
			// Unfortunately today, even when using an AuthKey we
			// briefly see these states. It would be nice to fix.
		}
	}
}

// Close stops the server.
//
// It must not be called before or concurrently with Start.
func (s *Server) Close() error {
	didClose := false
	s.closeOnce.Do(func() {
		didClose = true
		s.close()
	})
	if !didClose {
		return fmt.Errorf("tsnet: %w", net.ErrClosed)
	}
	return nil
}

func (s *Server) close() {
	// Close listeners under s.mu, then release before the heavy shutdown
	// operations. We must not hold s.mu during netstack.Close, lb.Shutdown,
	// etc. because callbacks from gVisor (e.g. getTCPHandlerForFlow)
	// acquire s.mu, and waiting for those goroutines while holding the lock
	// would deadlock.
	s.mu.Lock()
	for _, ln := range s.listeners {
		ln.closeLocked()
	}
	s.mu.Unlock()

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	var wg sync.WaitGroup
	wg.Go(func() {
		// Perform a best-effort final flush.
		if s.logtail != nil {
			s.logtail.Shutdown(ctx)
		}
		if s.logbuffer != nil {
			s.logbuffer.Close()
		}
	})
	wg.Go(func() {
		if s.localAPIServer != nil {
			s.localAPIServer.Shutdown(ctx)
		}
	})

	if s.shutdownCancel != nil {
		s.shutdownCancel()
	}
	if s.netstack != nil {
		s.netstack.Close()
	}
	if s.lb != nil {
		s.lb.Shutdown()
	}
	if s.netMon != nil {
		s.netMon.Close()
	}
	if s.dialer != nil {
		s.dialer.Close()
	}
	if s.localAPIListener != nil {
		s.localAPIListener.Close()
	}
	if s.loopbackListener != nil {
		s.loopbackListener.Close()
	}

	wg.Wait()
	s.sys.Bus.Get().Close()
}

func (s *Server) doInit() {
	s.shutdownCtx, s.shutdownCancel = context.WithCancel(context.Background())
	if err := s.start(); err != nil {
		s.initErr = fmt.Errorf("tsnet: %w", err)
	}
}

// CertDomains returns the list of domains for which the server can
// provide TLS certificates. These are also the DNS names for the
// Server.
// If the server is not running, it returns nil.
func (s *Server) CertDomains() []string {
	nm := s.lb.NetMapNoPeers()
	if nm == nil {
		return nil
	}
	return slices.Clone(nm.DNS.CertDomains)
}

// TailscaleIPs returns IPv4 and IPv6 addresses for this node. If the node
// has not yet joined a tailnet or is otherwise unaware of its own IP addresses,
// the returned ip4, ip6 will be !netip.IsValid().
func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
	nm := s.lb.NetMapNoPeers()
	if nm == nil {
		return
	}
	addrs := nm.GetAddresses()
	for _, addr := range addrs.All() {
		ip := addr.Addr()
		if ip.Is6() {
			ip6 = ip
		}
		if ip.Is4() {
			ip4 = ip
		}
	}

	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
	}
	if v := os.Getenv("TS_AUTHKEY"); v != "" {
		return v
	}
	return os.Getenv("TS_AUTH_KEY")
}

func (s *Server) getControlURL() string {
	if v := s.ControlURL; v != "" {
		return v
	}
	return os.Getenv("TS_CONTROL_URL")
}

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)

	exe, err := os.Executable()
	if err != nil {
		switch runtime.GOOS {
		case "js", "wasip1":
			// These platforms don't implement os.Executable (at least as of Go
			// 1.21), but we don't really care much: it's only used as a default
			// directory and hostname when they're not supplied. But we can fall
			// back to "tsnet" as well.
			exe = "tsnet"
		case "ios", "darwin":
			// When compiled as a framework (via TailscaleKit in libtailscale),
			// os.Executable() returns an error on iOS. The same failure occurs
			// on macOS (darwin) when the framework is loaded in a process
			// launched by a debugger or certain host environments (e.g. Xcode),
			// where the OS does not expose a resolvable executable path to the
			// embedded Go runtime. Fall back to "tsnet" in both cases — the
			// value is only used as a default hostname/directory when neither
			// Server.Hostname nor Server.Dir is set.
			exe = "tsnet"
		default:
			return err
		}
	}
	prog := strings.TrimSuffix(strings.ToLower(filepath.Base(exe)), ".exe")

	s.hostname = s.Hostname
	if s.hostname == "" {
		s.hostname = prog
	}

	s.rootPath = s.Dir
	if s.Store != nil {
		_, isMemStore := s.Store.(*mem.Store)
		if isMemStore && !s.Ephemeral {
			return fmt.Errorf("in-memory store is only supported for Ephemeral nodes")
		}
	}

	if s.rootPath == "" {
		confDir, err := os.UserConfigDir()
		if err != nil {
			return err
		}
		s.rootPath = filepath.Join(confDir, "tsnet-"+prog)
	}
	if err := os.MkdirAll(s.rootPath, 0700); err != nil {
		return err
	}
	if fi, err := os.Stat(s.rootPath); err != nil {
		return err
	} else if !fi.IsDir() {
		return fmt.Errorf("%v is not a directory", s.rootPath)
	}

	tsLogf := func(format string, a ...any) {
		if s.logtail != nil {
			s.logtail.Logf(format, a...)
		}
		if s.Logf == nil {
			return
		}
		s.Logf(format, a...)
	}

	sys := tsd.NewSystem()
	s.sys = sys
	if err := s.startLogger(&closePool, sys.HealthTracker.Get(), tsLogf); err != nil {
		return err
	}

	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{
		Tun:           s.Tun,
		EventBus:      sys.Bus.Get(),
		ListenPort:    s.Port,
		NetMon:        s.netMon,
		Dialer:        s.dialer,
		SetSubsystem:  sys.Set,
		ControlKnobs:  sys.ControlKnobs(),
		HealthTracker: sys.HealthTracker.Get(),
		ExtraRootCAs:  sys.ExtraRootCAs,
		Metrics:       sys.UserMetricsRegistry(),
	})
	if err != nil {
		return err
	}
	closePool.add(s.dialer)
	sys.Set(eng)
	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())
	if err != nil {
		return fmt.Errorf("netstack.Create: %w", err)
	}
	sys.Tun.Get().Start()
	sys.Set(ns)
	if s.Tun == nil {
		// Only process packets in netstack when using the default fake TUN.
		// When a TUN is provided, let packets flow through it instead.
		ns.ProcessLocalIPs = true
		ns.ProcessSubnets = true
	} else {
		// When using a TUN, check gVisor for registered endpoints to handle
		// packets for tsnet listeners and outbound connection replies.
		ns.CheckLocalTransportEndpoints = true
	}
	ns.GetTCPHandlerForFlow = s.getTCPHandlerForFlow
	ns.GetUDPHandlerForFlow = s.getUDPHandlerForFlow
	s.netstack = ns
	s.dialer.UseNetstackForIP = func(ip netip.Addr) bool {
		// s.lb is assigned below, before any dials can happen.
		_, ok := s.lb.PeerForIP(ip)
		return ok
	}
	s.dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
		// Note: don't just return ns.DialContextTCP or we'll return
		// *gonet.TCPConn(nil) instead of a nil interface which trips up
		// callers.
		v4, v6 := s.TailscaleIPs()
		src := bools.IfElse(dst.Addr().Is6(), v6, v4)
		tcpConn, err := ns.DialContextTCPWithBind(ctx, src, dst)
		if err != nil {
			return nil, err
		}
		return tcpConn, nil
	}
	s.dialer.NetstackDialUDP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
		// Note: don't just return ns.DialContextUDP or we'll return
		// *gonet.UDPConn(nil) instead of a nil interface which trips up
		// callers.
		v4, v6 := s.TailscaleIPs()
		src := bools.IfElse(dst.Addr().Is6(), v6, v4)
		udpConn, err := ns.DialContextUDPWithBind(ctx, src, dst)
		if err != nil {
			return nil, err
		}
		return udpConn, nil
	}

	if s.Store == nil {
		stateFile := filepath.Join(s.rootPath, "tailscaled.state")
		s.logf("tsnet running state path %s", stateFile)
		s.Store, err = store.New(tsLogf, stateFile)
		if err != nil {
			return err
		}
	}
	sys.Set(s.Store)

	loginFlags := controlclient.LoginDefault
	if s.Ephemeral {
		loginFlags = controlclient.LoginEphemeral
	}
	lb, err := ipnlocal.NewLocalBackend(tsLogf, s.logid, sys, loginFlags|controlclient.LocalBackendStartKeyOSNeutral)
	if err != nil {
		return fmt.Errorf("NewLocalBackend: %v", err)
	}
	lb.SetTCPHandlerForFunnelFlow(s.getTCPHandlerForFunnelFlow)
	lb.SetVarRoot(s.rootPath)
	s.logf("tsnet starting with hostname %q, varRoot %q", s.hostname, s.rootPath)
	s.lb = lb
	if err := ns.Start(lb); err != nil {
		return fmt.Errorf("failed to start netstack: %w", err)
	}
	closePool.addFunc(func() { s.lb.Shutdown() })
	prefs := ipn.NewPrefs()
	prefs.Hostname = s.hostname
	prefs.WantRunning = true
	prefs.ControlURL = s.getControlURL()
	prefs.RunWebClient = s.RunWebClient
	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,
	})
	if err != nil {
		return fmt.Errorf("starting backend: %w", err)
	}
	st := lb.State()
	if st == ipn.NeedsLogin || envknob.Bool("TSNET_FORCE_LOGIN") {
		s.logf("LocalBackend state is %v; running StartLoginInteractive...", st)
		if err := s.lb.StartLoginInteractive(s.shutdownCtx); err != nil {
			return fmt.Errorf("StartLoginInteractive: %w", err)
		}
	} else if authKey != "" {
		s.logf("Authkey is set; but state is %v. Ignoring authkey. Re-run with TSNET_FORCE_LOGIN=1 to force use of authkey.", st)
	}
	go s.printAuthURLLoop()

	// Run the localapi handler, to allow fetching LetsEncrypt certs.
	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

	// Create an in-process listener.
	// nettest.Listen provides a in-memory pipe based implementation for net.Conn.
	lal := memnet.Listen("local-tailscaled.sock:80")
	s.localAPIListener = lal
	s.localClient = &local.Client{Dial: lal.Dial}
	s.localAPIServer = &http.Server{Handler: lah}
	s.lb.ConfigureWebClient(s.localClient)
	go func() {
		if err := s.localAPIServer.Serve(lal); err != nil && err != http.ErrServerClosed {
			s.logf("localapi serve error: %v", err)
		}
	}()
	closePool.add(s.localAPIListener)
	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, tailscale.ResolveAuthKeyArgs{
			AuthKey: clientSecret,
			Tags:    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, tailscale.ResolveAuthKeyWIFArgs{
			BaseURL:  s.getControlURL(),
			ClientID: clientID,
			IDToken:  idToken,
			Audience: audience,
			Tags:     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
	}
	cfgPath := filepath.Join(s.rootPath, "tailscaled.log.conf")
	lpc, err := logpolicy.ConfigFromFile(cfgPath)
	switch {
	case os.IsNotExist(err):
		lpc = logpolicy.NewConfig(logtail.CollectionNode)
		if err := lpc.Save(cfgPath); err != nil {
			return fmt.Errorf("logpolicy.Config.Save for %v: %w", cfgPath, err)
		}
	case err != nil:
		return fmt.Errorf("logpolicy.LoadConfig for %v: %w", cfgPath, err)
	}
	if err := lpc.Validate(logtail.CollectionNode); err != nil {
		return fmt.Errorf("logpolicy.Config.Validate for %v: %w", cfgPath, err)
	}
	s.logid = lpc.PublicID

	s.logbuffer, err = filch.New(filepath.Join(s.rootPath, "tailscaled"), filch.Options{ReplaceStderr: false})
	if err != nil {
		return fmt.Errorf("error creating filch: %w", err)
	}
	closePool.add(s.logbuffer)
	c := logtail.Config{
		Collection:   lpc.Collection,
		PrivateID:    lpc.PrivateID,
		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,
	}
	s.logtail = logtail.NewLogger(c, tsLogf)
	closePool.addFunc(func() { s.logtail.Shutdown(context.Background()) })
	return nil
}

type closeOnErrorPool []func()

func (p *closeOnErrorPool) add(c io.Closer)   { *p = append(*p, func() { c.Close() }) }
func (p *closeOnErrorPool) addFunc(fn func()) { *p = append(*p, fn) }
func (p closeOnErrorPool) closeAllIfError(errp *error) {
	if *errp != nil {
		for _, closeFn := range p {
			closeFn()
		}
	}
}

func (s *Server) logf(format string, a ...any) {
	if s.logtail != nil {
		s.logtail.Logf(format, a...)
	}
	if s.UserLogf != nil {
		s.UserLogf(format, a...)
		return
	}
	log.Printf(format, a...)
}

// printAuthURLLoop loops once every few seconds while the server is still running and
// is in NeedsLogin state, printing out the auth URL.
func (s *Server) printAuthURLLoop() {
	ctx, cancel := context.WithCancel(s.shutdownCtx)
	defer cancel()
	stateCh := make(chan struct{}, 1)
	go s.lb.WatchNotifications(ctx, ipn.NotifyInitialState, nil, func(n *ipn.Notify) (keepGoing bool) {
		if n.State == nil {
			return true
		}

		// No need to block, we only want to make sure the loop below is not
		// blocking on time.After if there's a new state available.
		select {
		case stateCh <- struct{}{}:
		default:
		}
		return true
	})
	for {
		if s.shutdownCtx.Err() != nil {
			return
		}
		if st := s.lb.State(); st != ipn.NeedsLogin && st != ipn.NoState {
			s.logf("AuthLoop: state is %v; done", st)
			return
		}
		st := s.lb.StatusWithoutPeers()
		if st.AuthURL != "" {
			s.logf("To start this tsnet server, restart with TS_AUTHKEY set, or go to: %s", st.AuthURL)
		}
		select {
		case <-time.After(5 * time.Second):
		case <-stateCh:
		case <-s.shutdownCtx.Done():
			return
		}
	}
}

// networkForFamily returns one of "tcp4", "tcp6", "udp4", or "udp6".
//
// netBase is "tcp" or "udp" (without any '4' or '6' suffix).
func networkForFamily(netBase string, is6 bool) string {
	switch netBase {
	case "tcp":
		if is6 {
			return "tcp6"
		}
		return "tcp4"
	case "udp":
		if is6 {
			return "udp6"
		}
		return "udp4"
	}
	panic("unexpected")
}

// listenerForDstAddr returns a listener for the provided network and
// destination IP/port. It matches from most specific to least specific.
// For example:
//
//   - ("tcp4", IP, port)
//   - ("tcp", IP, port)
//   - ("tcp4", "", port)
//   - ("tcp", "", port)
//
// The netBase is "tcp" or "udp" (without any '4' or '6' suffix).
//
// Listeners which do not specify an IP address will match for traffic
// for the local node (that is, a destination address of the IPv4 or
// IPv6 address of this node) only. To listen for traffic on other addresses
// such as those routed inbound via subnet routes, explicitly specify
// the listening address or use RegisterFallbackTCPHandler.
func (s *Server) listenerForDstAddr(netBase string, dst netip.AddrPort, funnel bool) (_ *listener, ok bool) {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Search for a listener with the specified IP
	for _, net := range [2]string{
		networkForFamily(netBase, dst.Addr().Is6()),
		netBase,
	} {
		if ln, ok := s.listeners[listenKey{net, dst.Addr(), dst.Port(), funnel}]; ok {
			return ln, true
		}
	}

	// Search for a listener without an IP if the destination was
	// one of the native IPs of the node.
	if ip4, ip6 := s.TailscaleIPs(); dst.Addr() == ip4 || dst.Addr() == ip6 {
		for _, net := range [2]string{
			networkForFamily(netBase, dst.Addr().Is6()),
			netBase,
		} {
			if ln, ok := s.listeners[listenKey{net, netip.Addr{}, dst.Port(), funnel}]; ok {
				return ln, true
			}
		}
	}

	return nil, false
}

func (s *Server) getTCPHandlerForFunnelFlow(src netip.AddrPort, dstPort uint16) (handler func(net.Conn)) {
	ipv4, ipv6 := s.TailscaleIPs()
	var dst netip.AddrPort
	if src.Addr().Is4() {
		if !ipv4.IsValid() {
			return nil
		}
		dst = netip.AddrPortFrom(ipv4, dstPort)
	} else {
		if !ipv6.IsValid() {
			return nil
		}
		dst = netip.AddrPortFrom(ipv6, dstPort)
	}
	ln, ok := s.listenerForDstAddr("tcp", dst, true)
	if !ok {
		return nil
	}
	return ln.handle
}

func (s *Server) getTCPHandlerForFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
	ln, ok := s.listenerForDstAddr("tcp", dst, false)
	if !ok {
		s.mu.Lock()
		defer s.mu.Unlock()
		for _, handler := range s.fallbackTCPHandlers {
			connHandler, intercept := handler(src, dst)
			if intercept {
				return connHandler, intercept
			}
		}
		return nil, true // don't handle, don't forward to localhost
	}
	return ln.handle, true
}

func (s *Server) getUDPHandlerForFlow(src, dst netip.AddrPort) (handler func(nettype.ConnPacketConn), intercept bool) {
	ln, ok := s.listenerForDstAddr("udp", dst, false)
	if !ok {
		return nil, true // don't handle, don't forward to localhost
	}
	return func(c nettype.ConnPacketConn) { ln.handle(c) }, true
}

// Listen announces only on the Tailscale network.
// It will start the server if it has not been started yet.
//
// Listeners which do not specify an IP address will match for traffic
// for the local node (that is, a destination address of the IPv4 or
// IPv6 address of this node) only. To listen for traffic on other addresses
// such as those routed inbound via subnet routes, explicitly specify
// the listening address or use RegisterFallbackTCPHandler.
func (s *Server) Listen(network, addr string) (net.Listener, error) {
	return s.listen(network, addr, listenOnTailnet)
}

// ListenSSH listens on the Tailscale network for SSH connections at the given
// addr (e.g. ":2222"). The returned listener's Accept method yields net.Conn
// values that are actually *tailssh.Session, providing access to the
// connecting peer's Tailscale identity, PTY information, signals, and more.
//
// Basic applications can use the returned connections as plain net.Conn
// (Read/Write/Close). Applications that need richer SSH semantics should
// type-assert to *tailssh.Session.
//
// SSH support must be linked into the binary by importing
// _ "tailscale.com/feature/ssh". Without that import, ListenSSH returns an
// error.
//
// If s has not been started yet, it will be started.
func (s *Server) ListenSSH(addr string) (net.Listener, error) {
	rawLn, err := s.Listen("tcp", addr)
	if err != nil {
		return nil, err
	}
	sshLn, err := s.lb.ListenSSH(rawLn, s.logf)
	if err != nil {
		rawLn.Close()
		return nil, err
	}
	return sshLn, nil
}

// ListenPacket announces on the Tailscale network.
//
// The network must be "udp", "udp4" or "udp6". The addr must be of the form
// "ip:port" (or "[ip]:port") where ip is a valid IPv4 or IPv6 address
// corresponding to "udp4" or "udp6" respectively. IP must be specified.
//
// If s has not been started yet, it will be started.
func (s *Server) ListenPacket(network, addr string) (net.PacketConn, error) {
	ap, err := resolveListenAddr(network, addr)
	if err != nil {
		return nil, err
	}
	if !ap.Addr().IsValid() {
		return nil, fmt.Errorf("tsnet.ListenPacket(%q, %q): address must be a valid IP", network, addr)
	}
	if network == "udp" {
		if ap.Addr().Is4() {
			network = "udp4"
		} else {
			network = "udp6"
		}
	}
	if err := s.Start(); err != nil {
		return nil, err
	}

	// Create the gVisor PacketConn first so it can handle port 0 allocation.
	pc, err := s.netstack.ListenPacket(network, ap.String())
	if err != nil {
		return nil, err
	}

	// If port 0 was requested, use the port gVisor assigned.
	if ap.Port() == 0 {
		if p := portFromAddr(pc.LocalAddr()); p != 0 {
			ap = netip.AddrPortFrom(ap.Addr(), p)
			addr = ap.String()
		}
	}

	ln, err := s.registerListener(network, addr, ap, listenOnTailnet, nil)
	if err != nil {
		pc.Close()
		return nil, err
	}

	return &udpPacketConn{
		PacketConn: pc,
		ln:         ln,
	}, nil
}

// udpPacketConn wraps a net.PacketConn to unregister from s.listeners on Close.
type udpPacketConn struct {
	net.PacketConn
	ln *listener
}

func (c *udpPacketConn) Close() error {
	c.ln.Close()
	return c.PacketConn.Close()
}

// ListenTLS announces only on the Tailscale network.
// It returns a TLS listener wrapping the tsnet listener.
// It will start the server if it has not been started yet.
func (s *Server) ListenTLS(network, addr string) (net.Listener, error) {
	if network != "tcp" {
		return nil, fmt.Errorf("ListenTLS(%q, %q): only tcp is supported", network, addr)
	}
	ctx := context.Background()
	st, err := s.Up(ctx)
	if err != nil {
		return nil, err
	}
	if !st.CurrentTailnet.MagicDNSEnabled {
		return nil, errors.New("tsnet: you must enable MagicDNS in the DNS page of the admin panel to proceed. See https://tailscale.com/s/https")
	}
	if len(st.CertDomains) == 0 {
		return nil, errors.New("tsnet: you must enable HTTPS in the admin panel to proceed. See https://tailscale.com/s/https")
	}

	ln, err := s.listen(network, addr, listenOnTailnet)
	if err != nil {
		return nil, err
	}
	return tls.NewListener(ln, &tls.Config{
		GetCertificate: s.getCert,
	}), nil
}

// RegisterFallbackTCPHandler registers a callback which will be called
// to handle a TCP flow to this tsnet node, for which no listeners will handle.
//
// If multiple fallback handlers are registered, they will be called in an
// undefined order. See FallbackTCPHandler for details on handling a flow.
//
// The returned function can be used to deregister this callback.
func (s *Server) RegisterFallbackTCPHandler(cb FallbackTCPHandler) func() {
	s.mu.Lock()
	defer s.mu.Unlock()
	hnd := s.fallbackTCPHandlers.Add(cb)
	return func() {
		s.mu.Lock()
		defer s.mu.Unlock()
		delete(s.fallbackTCPHandlers, hnd)
	}
}

// getCert is the GetCertificate function used by ListenTLS.
//
// 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) {
	lc, err := s.LocalClient()
	if err != nil {
		return nil, err
	}
	return lc.GetCertificate(hi)
}

// FunnelOption is an option passed to ListenFunnel to configure the listener.
type FunnelOption interface {
	funnelOption()
}

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{} }

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.
//
// It also by default listens on your local tailnet, so connections can
// come from either inside or outside your network. To restrict connections
// to be just from the internet, use the FunnelOnly option.
//
// Currently (2023-03-10), Funnel only supports TCP on ports 443, 8443, and 10000.
// The supported host name is limited to that configured for the tsnet.Server.
// As such, the standard way to create funnel is:
//
//	s.ListenFunnel("tcp", ":443")
//
// and the only other supported addrs currently are ":8443" and ":10000".
//
// It will start the server if it has not been started yet.
func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.Listener, error) {
	if network != "tcp" {
		return nil, fmt.Errorf("ListenFunnel(%q, %q): only tcp is supported", network, addr)
	}
	host, portStr, err := net.SplitHostPort(addr)
	if err != nil {
		return nil, err
	}
	if host != "" {
		return nil, fmt.Errorf("ListenFunnel(%q, %q): host must be empty", network, addr)
	}
	port, err := strconv.ParseUint(portStr, 10, 16)
	if err != nil {
		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 {
		return nil, err
	}
	// TODO(sonia,tailscale/corp#10577): We may want to use the interactive enable
	// flow here instead of CheckFunnelAccess to allow the user to turn on Funnel
	// if not already on. Specifically when running from a terminal.
	// See cli.serveEnv.verifyFunnelEnabled.
	if err := ipn.CheckFunnelAccess(uint16(port), st.Self); err != nil {
		return nil, err
	}

	lc := s.localClient

	// May not have funnel enabled. Enable it.
	srvConfig, err := lc.GetServeConfig(ctx)
	if err != nil {
		return nil, err
	}
	if srvConfig == nil {
		srvConfig = &ipn.ServeConfig{}
	}
	if len(st.CertDomains) == 0 {
		return nil, errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https")
	}
	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.
	ln, err := s.listen(network, addr, lnOn)
	if err != nil {
		return nil, err
	}
	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

	// Used by Close.
	closeOnce sync.Once
	closeErr  error               // written to during execution of closeOnce, read by Close()
	s         *Server             // read and written to during execution of closeOnce
	svcName   tailcfg.ServiceName // read during execution of closeOnce
	mode      ServiceMode         // read during execution of closeOnce
}

// 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
}

// cleanServeConfig cleans serve config changes made to support this listener.
// This should only be called by Close.
func (sl *ServiceListener) cleanServeConfig() error {
	sc, etag, err := sl.s.lb.ServeConfigETag()
	if err != nil {
		return fmt.Errorf("fetching current config: %w", err)
	}
	if !sc.Valid() || !sc.Services().Contains(sl.svcName) {
		return nil
	}
	srvConfig := sc.AsStruct()
	svcConfig := srvConfig.Services[sl.svcName]
	switch m := sl.mode.(type) {
	case ServiceModeTCP:
		delete(svcConfig.TCP, m.Port)
	case ServiceModeHTTP:
		hp := net.JoinHostPort(sl.FQDN, strconv.Itoa(int(m.Port)))
		delete(svcConfig.Web, ipn.HostPort(hp))
		delete(svcConfig.TCP, m.Port)
	default:
		return fmt.Errorf("unexpected ServiceMode %T", sl.mode)
	}
	if err := sl.s.lb.SetServeConfig(srvConfig, etag); err != nil {
		return fmt.Errorf("setting config: %w", err)
	}
	return nil
}

// Close closes the listener and clears state related to hosting the Service.
// Behavior is undefined after the [Server] has been closed.
func (sl *ServiceListener) Close() error {
	// We should only clean up state once. Otherwise we can stomp on state
	// created by new listeners.
	sl.closeOnce.Do(func() {
		sl.s.mu.Lock()
		defer sl.s.mu.Unlock()

		// Two pieces of state we need to clear:
		//  1. The Service advertisement pref
		//  2. Artifacts in the serve config
		// Then we can close the listener.

		var adErr error
		if err := sl.s.decrementServiceAdvertisementLocked(sl.svcName); err != nil {
			adErr = fmt.Errorf("managing Service advertisements: %w", err)
		}

		var srvCfgErr error
		if err := sl.cleanServeConfig(); err != nil {
			srvCfgErr = fmt.Errorf("cleaning config changes: %w", err)
		}

		sl.closeErr = errors.Join(sl.Listener.Close(), adErr, srvCfgErr)
	})
	return sl.closeErr
}

// 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")

// advertiseService ensures the Service is advertised by this node.
func (s *Server) advertiseService(name tailcfg.ServiceName) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	advertised := s.lb.Prefs().AdvertiseServices()
	if !views.SliceContains(advertised, name.String()) {
		newAdvertised := make([]string, 0, advertised.Len()+1)
		newAdvertised = advertised.AppendTo(newAdvertised)
		newAdvertised = append(newAdvertised, name.String())
		_, err := s.lb.EditPrefs(&ipn.MaskedPrefs{
			AdvertiseServicesSet: true,
			Prefs: ipn.Prefs{
				AdvertiseServices: newAdvertised,
			},
		})
		if err != nil {
			return err
		}
	}
	mak.Set(&s.advertisedServices, name, s.advertisedServices[name]+1)
	return nil
}

// decrementServiceAdvertisement decrements the count of listeners this node has
// advertising the Service. Advertisement of the Service will be withdrawn if
// the count hits zero. It is an error to call this function when the Service is
// not being advertised by this node.
func (s *Server) decrementServiceAdvertisementLocked(name tailcfg.ServiceName) error {
	cleanAdvertisement := func() error {
		delete(s.advertisedServices, name)
		advertised := s.lb.Prefs().AdvertiseServices()
		if !views.SliceContains(advertised, name.String()) {
			return nil
		}
		newAdvertised := make([]string, 0, advertised.Len()-1)
		for _, svc := range advertised.All() {
			if svc == name.String() {
				continue
			}
			newAdvertised = append(newAdvertised, svc)
		}
		_, err := s.lb.EditPrefs(&ipn.MaskedPrefs{
			AdvertiseServicesSet: true,
			Prefs: ipn.Prefs{
				AdvertiseServices: newAdvertised,
			},
		})
		return err
	}

	if s.advertisedServices[name] <= 0 {
		advertisements := s.advertisedServices[name]
		// We somehow mismatched increments and decrements. Clear current
		// advertisements and surface the mismatch as an error.
		return errors.Join(
			cleanAdvertisement(),
			fmt.Errorf("service decrement requested with %d advertisements", advertisements),
		)
	}
	s.advertisedServices[name]--
	if s.advertisedServices[name] > 0 {
		// If there are still listeners advertising the Service, then there's
		// nothing more for us to do.
		return nil
	}
	return cleanAdvertisement()
}

// 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
//
// This function will start the server if it is not already started.
func (s *Server) ListenService(name string, mode ServiceMode) (*ServiceListener, error) {
	svcName := tailcfg.ServiceName(name)
	if err := svcName.Validate(); err != nil {
		return nil, err
	}
	if mode == nil {
		return nil, errors.New("mode may not be nil")
	}

	// We collect cleanup tasks as we go and execute these on error. If we make
	// it to the end we abandon these cleanup tasks by setting onError to nil.
	var onError []func()
	defer func() {
		for _, f := range onError {
			f()
		}
	}()

	// 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
	}

	if err := s.advertiseService(svcName); err != nil {
		return nil, fmt.Errorf("advertising Service: %w", err)
	}
	onError = append(onError, func() {
		s.mu.Lock()
		defer s.mu.Unlock()
		s.decrementServiceAdvertisementLocked(svcName)
	})

	srvCfg := new(ipn.ServeConfig)
	sc, srvCfgETag, err := s.lb.ServeConfigETag()
	if err != nil {
		return nil, fmt.Errorf("fetching current serve config: %w", err)
	}
	if sc.Valid() {
		srvCfg = sc.AsStruct()
	}

	fqdn := 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")
		}
		if svcCfg, ok := srvCfg.Services[svcName]; ok {
			if _, handlerExists := svcCfg.TCP[m.port()]; handlerExists {
				// We know that a handler must have been started in this runtime
				// because serve config is reset on the first [Server.Up].
				return nil, errors.New("a Service handler already exists for this port")
			}
		}
		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)
	}
	onError = append(onError, func() { ln.Close() })

	switch m := mode.(type) {
	case ServiceModeTCP:
		// Forward all connections from service-hostname:port to our socket.
		srvCfg.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
			}
			srvCfg.SetWebHandler(&h, svcName.String(), m.Port, path, m.HTTPS, mds)
		}
		// We always need a root handler.
		if !haveRootHandler {
			h := ipn.HTTPHandler{Proxy: ln.Addr().String()}
			srvCfg.SetWebHandler(&h, svcName.String(), m.Port, "/", m.HTTPS, mds)
		}
	default:
		return nil, fmt.Errorf("unknown ServiceMode type %T", m)
	}

	if err := s.lb.SetServeConfig(srvCfg, srvCfgETag); err != nil {
		return nil, err
	}

	onError = nil
	return &ServiceListener{
		Listener: ln,
		FQDN:     fqdn,
		addr:     svcAddr,
		s:        s,
		svcName:  svcName,
		mode:     mode,
	}, nil
}

type listenOn string

const (
	listenOnTailnet = listenOn("listen-on-tailnet")
	listenOnFunnel  = listenOn("listen-on-funnel")
	listenOnBoth    = listenOn("listen-on-both")
)

// resolveListenAddr resolves a network and address into a netip.AddrPort. The
// returned netip.AddrPort.Addr will be the zero value if the address is empty.
// The port must be a valid port number. The caller is responsible for checking
// the network and address are valid.
//
// It resolves well-known port names and validates the address is a valid IP
// literal for the network.
func resolveListenAddr(network, addr string) (netip.AddrPort, error) {
	var zero netip.AddrPort
	host, portStr, err := net.SplitHostPort(addr)
	if err != nil {
		return zero, fmt.Errorf("tsnet: %w", err)
	}
	port, err := net.LookupPort(network, portStr)
	if err != nil || port < 0 || port > math.MaxUint16 {
		// LookupPort returns an error on out of range values so the bounds
		// checks on port should be unnecessary, but harmless. If they do
		// match, worst case this error message says "invalid port: <nil>".
		return zero, fmt.Errorf("invalid port: %w", err)
	}
	if host == "" {
		return netip.AddrPortFrom(netip.Addr{}, uint16(port)), nil
	}

	bindHostOrZero, err := netip.ParseAddr(host)
	if err != nil {
		return zero, fmt.Errorf("invalid Listen addr %q; host part must be empty or IP literal", host)
	}
	// Normalize unspecified addresses (0.0.0.0, ::) to the zero value,
	// equivalent to an empty host, so they match the node's own IPs.
	if bindHostOrZero.IsUnspecified() {
		return netip.AddrPortFrom(netip.Addr{}, uint16(port)), nil
	}
	if strings.HasSuffix(network, "4") && !bindHostOrZero.Is4() {
		return zero, fmt.Errorf("invalid non-IPv4 addr %v for network %q", host, network)
	}
	if strings.HasSuffix(network, "6") && !bindHostOrZero.Is6() {
		return zero, fmt.Errorf("invalid non-IPv6 addr %v for network %q", host, network)
	}
	return netip.AddrPortFrom(bindHostOrZero, uint16(port)), nil
}

// ephemeral port range for non-TUN listeners requesting port 0. This range is
// chosen to reduce the probability of collision with host listeners, avoiding
// both the typical ephemeral range, and privilege listener ranges. Collisions
// may still occur and could for example shadow host sockets in a netstack+TUN
// situation, the range here is a UX improvement, not a guarantee that
// application authors will never have to consider these cases.
const (
	ephemeralPortFirst = 10002
	ephemeralPortLast  = 19999
)

func (s *Server) listen(network, addr string, lnOn listenOn) (net.Listener, error) {
	switch network {
	case "", "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
	default:
		return nil, errors.New("unsupported network type")
	}
	host, err := resolveListenAddr(network, addr)
	if err != nil {
		return nil, err
	}
	if err := s.Start(); err != nil {
		return nil, err
	}

	isTCP := network == "" || network == "tcp" || network == "tcp4" || network == "tcp6"

	// When using a TUN with TCP, create a gVisor TCP listener.
	// gVisor handles port 0 allocation natively.
	var gonetLn net.Listener
	if s.Tun != nil && isTCP {
		gonetLn, err = s.listenTCP(network, host)
		if err != nil {
			return nil, err
		}
		// If port 0 was requested, update host to the port gVisor assigned
		// so that the listenKey uses the real port.
		if host.Port() == 0 {
			if p := portFromAddr(gonetLn.Addr()); p != 0 {
				host = netip.AddrPortFrom(host.Addr(), p)
				addr = listenAddr(host)
			}
		}
	}

	ln, err := s.registerListener(network, addr, host, lnOn, gonetLn)
	if err != nil {
		if gonetLn != nil {
			gonetLn.Close()
		}
		return nil, err
	}
	return ln, nil
}

// listenTCP creates a gVisor TCP listener for TUN mode.
func (s *Server) listenTCP(network string, host netip.AddrPort) (net.Listener, error) {
	var nsNetwork string
	nsAddr := host
	switch {
	case network == "tcp4" || network == "tcp6":
		nsNetwork = network
	case host.Addr().Is4():
		nsNetwork = "tcp4"
	case host.Addr().Is6():
		nsNetwork = "tcp6"
	default:
		// Wildcard address: use tcp6 for dual-stack (accepts both v4 and v6).
		nsNetwork = "tcp6"
		nsAddr = netip.AddrPortFrom(netip.IPv6Unspecified(), host.Port())
	}
	ln, err := s.netstack.ListenTCP(nsNetwork, nsAddr.String())
	if err != nil {
		return nil, fmt.Errorf("tsnet: %w", err)
	}
	return ln, nil
}

// registerListener allocates a port (if 0) and registers the listener in
// s.listeners under s.mu.
func (s *Server) registerListener(network, addr string, host netip.AddrPort, lnOn listenOn, gonetLn net.Listener) (*listener, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Allocate an ephemeral port for non-TUN listeners requesting port 0.
	if host.Port() == 0 && gonetLn == nil {
		p, ok := s.allocEphemeralLocked(network, host.Addr(), lnOn)
		if !ok {
			return nil, errors.New("tsnet: no available port in ephemeral range")
		}
		host = netip.AddrPortFrom(host.Addr(), p)
		addr = listenAddr(host)
	}

	var keys []listenKey
	switch lnOn {
	case listenOnTailnet:
		keys = append(keys, listenKey{network, host.Addr(), host.Port(), false})
	case listenOnFunnel:
		keys = append(keys, listenKey{network, host.Addr(), host.Port(), true})
	case listenOnBoth:
		keys = append(keys, listenKey{network, host.Addr(), host.Port(), false})
		keys = append(keys, listenKey{network, host.Addr(), host.Port(), true})
	}

	for _, key := range keys {
		if _, ok := s.listeners[key]; ok {
			return nil, fmt.Errorf("tsnet: listener already open for %s, %s", network, addr)
		}
	}

	ln := &listener{
		s:       s,
		keys:    keys,
		addr:    addr,
		closedc: make(chan struct{}),
		conn:    make(chan net.Conn),
		gonetLn: gonetLn,
	}
	if s.listeners == nil {
		s.listeners = make(map[listenKey]*listener)
	}
	for _, key := range keys {
		s.listeners[key] = ln
	}
	return ln, nil
}

// allocEphemeralLocked finds an unused port in [ephemeralPortFirst,
// ephemeralPortLast] that does not collide with any existing listener for the
// given network, host, and listenOn. s.mu must be held.
func (s *Server) allocEphemeralLocked(network string, host netip.Addr, lnOn listenOn) (uint16, bool) {
	if s.nextEphemeralPort < ephemeralPortFirst || s.nextEphemeralPort > ephemeralPortLast {
		s.nextEphemeralPort = ephemeralPortFirst
	}
	start := s.nextEphemeralPort
	for {
		p := s.nextEphemeralPort
		s.nextEphemeralPort++
		if s.nextEphemeralPort > ephemeralPortLast {
			s.nextEphemeralPort = ephemeralPortFirst
		}
		if !s.portInUseLocked(network, host, p, lnOn) {
			return p, true
		}
		if s.nextEphemeralPort == start {
			return 0, false
		}
	}
}

// portInUseLocked reports whether any listenKey for the given network, host,
// port, and listenOn already exists in s.listeners.
func (s *Server) portInUseLocked(network string, host netip.Addr, port uint16, lnOn listenOn) bool {
	switch lnOn {
	case listenOnTailnet:
		_, ok := s.listeners[listenKey{network, host, port, false}]
		return ok
	case listenOnFunnel:
		_, ok := s.listeners[listenKey{network, host, port, true}]
		return ok
	case listenOnBoth:
		_, ok1 := s.listeners[listenKey{network, host, port, false}]
		_, ok2 := s.listeners[listenKey{network, host, port, true}]
		return ok1 || ok2
	}
	return false
}

// listenAddr formats host as a listen address string.
// If host has no IP, it returns ":port".
func listenAddr(host netip.AddrPort) string {
	if !host.Addr().IsValid() {
		return ":" + strconv.Itoa(int(host.Port()))
	}
	return host.String()
}

// portFromAddr extracts the port from a net.Addr, or returns 0.
func portFromAddr(a net.Addr) uint16 {
	switch v := a.(type) {
	case *net.TCPAddr:
		return uint16(v.Port)
	case *net.UDPAddr:
		return uint16(v.Port)
	}
	if ap, err := netip.ParseAddrPort(a.String()); err == nil {
		return ap.Port()
	}
	return 0
}

// 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.
//
// Packets will be written to the pcap until the process exits. The pcap needs a Lua dissector
// to be installed in Wireshark in order to decode properly: wgengine/capture/ts-dissector.lua
// in this repository.
// https://tailscale.com/docs/reference/troubleshooting/network-configuration/inspect-unencrypted-packets
func (s *Server) CapturePcap(ctx context.Context, pcapFile string) error {
	stream, err := s.localClient.StreamDebugCapture(ctx)
	if err != nil {
		return err
	}

	f, err := os.OpenFile(pcapFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		stream.Close()
		return err
	}

	go func(stream io.ReadCloser, f *os.File) {
		defer stream.Close()
		defer f.Close()
		_, _ = io.Copy(f, stream)
	}(stream, f)

	return nil
}

// Sys returns a handle to the Tailscale subsystems of this node.
//
// This is not a stable API, nor are the APIs of the returned subsystems.
func (s *Server) Sys() *tsd.System {
	return s.sys
}

type listenKey struct {
	network string
	host    netip.Addr // or zero value for unspecified
	port    uint16
	funnel  bool
}

type listener struct {
	s       *Server
	keys    []listenKey
	addr    string
	conn    chan net.Conn // unbuffered, never closed
	closedc chan struct{} // closed on [listener.Close]
	closed  bool          // guarded by s.mu

	// gonetLn, if set, is the gonet.Listener that handles new connections.
	// gonetLn is set by [listen] when a TUN is in use and terminates the listener.
	// gonetLn is nil when TUN is nil.
	gonetLn net.Listener
}

func (ln *listener) Accept() (net.Conn, error) {
	if ln.gonetLn != nil {
		return ln.gonetLn.Accept()
	}
	select {
	case c := <-ln.conn:
		return c, nil
	case <-ln.closedc:
		return nil, fmt.Errorf("tsnet: %w", net.ErrClosed)
	}
}

func (ln *listener) Addr() net.Addr {
	if ln.gonetLn != nil {
		return ln.gonetLn.Addr()
	}
	return addr{
		network: ln.keys[0].network,
		addr:    ln.addr,
	}
}

func (ln *listener) Close() error {
	ln.s.mu.Lock()
	defer ln.s.mu.Unlock()
	return ln.closeLocked()
}

// closeLocked closes the listener.
// It must be called with ln.s.mu held.
func (ln *listener) closeLocked() error {
	if ln.closed {
		return fmt.Errorf("tsnet: %w", net.ErrClosed)
	}
	for _, key := range ln.keys {
		if v, ok := ln.s.listeners[key]; ok && v == ln {
			delete(ln.s.listeners, key)
		}
	}
	close(ln.closedc)
	ln.closed = true
	if ln.gonetLn != nil {
		ln.gonetLn.Close()
	}
	return nil
}

func (ln *listener) handle(c net.Conn) {
	select {
	case ln.conn <- c:
		return
	case <-ln.closedc:
	case <-ln.s.shutdownCtx.Done():
	case <-time.After(time.Second):
		// TODO(bradfitz): this isn't ideal. Think about how
		// we how we want to do pushback.
	}
	c.Close()
}

// Server returns the tsnet Server associated with the listener.
func (ln *listener) Server() *Server { return ln.s }

type addr struct {
	network, addr string
}

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)
}
