mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	c2n was already a conditional feature, but it didn't have a feature/c2n directory before (rather, it was using consts + DCE). This adds it, and moves some code, which removes the httprec dependency. Also, remove some unnecessary code from our httprec fork. Updates #12614 Change-Id: I2fbe538e09794c517038e35a694a363312c426a2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			1371 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1371 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| // Package tsnet provides Tailscale as a library.
 | |
| 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"
 | |
| 
 | |
| 	"tailscale.com/client/local"
 | |
| 	"tailscale.com/control/controlclient"
 | |
| 	"tailscale.com/envknob"
 | |
| 	_ "tailscale.com/feature/c2n"
 | |
| 	_ "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/tsd"
 | |
| 	"tailscale.com/types/bools"
 | |
| 	"tailscale.com/types/logger"
 | |
| 	"tailscale.com/types/logid"
 | |
| 	"tailscale.com/types/nettype"
 | |
| 	"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
 | |
| 
 | |
| 	// ControlURL optionally specifies the coordination server URL.
 | |
| 	// If 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
 | |
| 
 | |
| 	getCertForTesting func(*tls.ClientHelloInfo) (*tls.Certificate, error)
 | |
| 
 | |
| 	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
 | |
| 
 | |
| 	mu                  sync.Mutex
 | |
| 	listeners           map[listenKey]*listener
 | |
| 	fallbackTCPHandlers set.HandleSet[FallbackTCPHandler]
 | |
| 	dialer              *tsdial.Dialer
 | |
| 	closed              bool
 | |
| }
 | |
| 
 | |
| // 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
 | |
| }
 | |
| 
 | |
| // 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|ipn.NotifyNoPrivateKeys)
 | |
| 	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 s := n.State; s != nil {
 | |
| 			if *s == 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")
 | |
| 				}
 | |
| 
 | |
| 				// 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)
 | |
| 				}
 | |
| 
 | |
| 				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 {
 | |
| 	s.mu.Lock()
 | |
| 	defer s.mu.Unlock()
 | |
| 	if s.closed {
 | |
| 		return fmt.Errorf("tsnet: %w", net.ErrClosed)
 | |
| 	}
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 	var wg sync.WaitGroup
 | |
| 	wg.Add(1)
 | |
| 	go func() {
 | |
| 		defer wg.Done()
 | |
| 		// Perform a best-effort final flush.
 | |
| 		if s.logtail != nil {
 | |
| 			s.logtail.Shutdown(ctx)
 | |
| 		}
 | |
| 		if s.logbuffer != nil {
 | |
| 			s.logbuffer.Close()
 | |
| 		}
 | |
| 	}()
 | |
| 	wg.Add(1)
 | |
| 	go func() {
 | |
| 		defer wg.Done()
 | |
| 		if s.localAPIServer != nil {
 | |
| 			s.localAPIServer.Shutdown(ctx)
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	if s.netstack != nil {
 | |
| 		s.netstack.Close()
 | |
| 		s.netstack = nil
 | |
| 	}
 | |
| 	if s.shutdownCancel != nil {
 | |
| 		s.shutdownCancel()
 | |
| 	}
 | |
| 	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()
 | |
| 	}
 | |
| 
 | |
| 	for _, ln := range s.listeners {
 | |
| 		ln.closeLocked()
 | |
| 	}
 | |
| 	wg.Wait()
 | |
| 	s.sys.Bus.Get().Close()
 | |
| 	s.closed = true
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| 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.NetMap()
 | |
| 	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.NetMap()
 | |
| 	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) 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":
 | |
| 			// When compiled as a framework (via TailscaleKit in libtailscale),
 | |
| 			// os.Executable() returns an error, so fall back to "tsnet" there
 | |
| 			// too.
 | |
| 			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{
 | |
| 		EventBus:      sys.Bus.Get(),
 | |
| 		ListenPort:    s.Port,
 | |
| 		NetMon:        s.netMon,
 | |
| 		Dialer:        s.dialer,
 | |
| 		SetSubsystem:  sys.Set,
 | |
| 		ControlKnobs:  sys.ControlKnobs(),
 | |
| 		HealthTracker: sys.HealthTracker.Get(),
 | |
| 		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)
 | |
| 	ns.ProcessLocalIPs = true
 | |
| 	ns.ProcessSubnets = true
 | |
| 	ns.GetTCPHandlerForFlow = s.getTCPHandlerForFlow
 | |
| 	ns.GetUDPHandlerForFlow = s.getUDPHandlerForFlow
 | |
| 	s.netstack = ns
 | |
| 	s.dialer.UseNetstackForIP = func(ip netip.Addr) bool {
 | |
| 		_, ok := eng.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.ControlURL
 | |
| 	prefs.RunWebClient = s.RunWebClient
 | |
| 	prefs.AdvertiseTags = s.AdvertiseTags
 | |
| 	authKey := s.getAuthKey()
 | |
| 	// Try to use an OAuth secret to generate an auth key if that functionality
 | |
| 	// is available.
 | |
| 	if f, ok := tailscale.HookResolveAuthKey.GetOk(); ok {
 | |
| 		authKey, err = f(s.shutdownCtx, s.getAuthKey(), prefs.AdvertiseTags)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("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) 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() {
 | |
| 	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 <-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)
 | |
| }
 | |
| 
 | |
| // 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
 | |
| 	}
 | |
| 	return s.netstack.ListenPacket(network, ap.String())
 | |
| }
 | |
| 
 | |
| // 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) {
 | |
| 	if s.getCertForTesting != nil {
 | |
| 		return s.getCertForTesting(hi)
 | |
| 	}
 | |
| 	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)
 | |
| 	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
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Start a funnel listener.
 | |
| 	ln, err := s.listen(network, addr, lnOn)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return tls.NewListener(ln, tlsConfig), 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)
 | |
| 	}
 | |
| 	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
 | |
| }
 | |
| 
 | |
| 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
 | |
| 	}
 | |
| 	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})
 | |
| 	}
 | |
| 
 | |
| 	ln := &listener{
 | |
| 		s:    s,
 | |
| 		keys: keys,
 | |
| 		addr: addr,
 | |
| 
 | |
| 		closedc: make(chan struct{}),
 | |
| 		conn:    make(chan net.Conn),
 | |
| 	}
 | |
| 	s.mu.Lock()
 | |
| 	for _, key := range keys {
 | |
| 		if _, ok := s.listeners[key]; ok {
 | |
| 			s.mu.Unlock()
 | |
| 			return nil, fmt.Errorf("tsnet: listener already open for %s, %s", network, addr)
 | |
| 		}
 | |
| 	}
 | |
| 	if s.listeners == nil {
 | |
| 		s.listeners = make(map[listenKey]*listener)
 | |
| 	}
 | |
| 	for _, key := range keys {
 | |
| 		s.listeners[key] = ln
 | |
| 	}
 | |
| 	s.mu.Unlock()
 | |
| 	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.
 | |
| //
 | |
| // 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/kb/1023/troubleshooting/#can-i-examine-network-traffic-inside-the-encrypted-tunnel
 | |
| 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
 | |
| }
 | |
| 
 | |
| func (ln *listener) Accept() (net.Conn, error) {
 | |
| 	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 { return addr{ln} }
 | |
| 
 | |
| 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
 | |
| 	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{ ln *listener }
 | |
| 
 | |
| func (a addr) Network() string { return a.ln.keys[0].network }
 | |
| func (a addr) String() string  { return a.ln.addr }
 |