| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // Package tsnet provides Tailscale as a library.
- //
- // It is an experimental work in progress.
- package tsnet
- import (
- "context"
- crand "crypto/rand"
- "encoding/hex"
- "errors"
- "fmt"
- "io"
- "log"
- "math"
- "net"
- "net/http"
- "net/netip"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "time"
- "tailscale.com/client/tailscale"
- "tailscale.com/control/controlclient"
- "tailscale.com/envknob"
- "tailscale.com/hostinfo"
- "tailscale.com/ipn"
- "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/tsdial"
- "tailscale.com/smallzstd"
- "tailscale.com/types/logger"
- "tailscale.com/util/mak"
- "tailscale.com/wgengine"
- "tailscale.com/wgengine/monitor"
- "tailscale.com/wgengine/netstack"
- )
- // Server is an embedded Tailscale server.
- //
- // Its exported fields may be changed until the first call to Listen.
- 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.
- 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 uploaded to log.tailscale.io,
- // 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
- // Logf, if non-nil, specifies the logger to use. By default,
- // log.Printf is used.
- Logf logger.Logf
- // Ephemeral, if true, specifies that the instance should register
- // as an Ephemeral node (https://tailscale.com/kb/1111/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 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
- initOnce sync.Once
- initErr error
- lb *ipnlocal.LocalBackend
- netstack *netstack.Impl
- linkMon *monitor.Mon
- rootPath string // the state directory
- hostname string
- shutdownCtx context.Context
- shutdownCancel context.CancelFunc
- localAPICred string // basic auth password for localAPITCPListener
- localAPITCPListener net.Listener // optional loopback, restricted to PID
- localAPIListener net.Listener // in-memory, used by localClient
- localClient *tailscale.LocalClient // in-memory
- logbuffer *filch.Filch
- logtail *logtail.Logger
- logid string
- mu sync.Mutex
- listeners map[listenKey]*listener
- dialer *tsdial.Dialer
- }
- // 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
- }
- return s.dialer.UserDial(ctx, network, address)
- }
- // 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() (*tailscale.LocalClient, error) {
- if err := s.Start(); err != nil {
- return nil, err
- }
- return s.localClient, nil
- }
- // LoopbackLocalAPI returns a loopback ip:port listening for the "LocalAPI".
- //
- // As the LocalAPI is powerful, access to endpoints requires BOTH passing a
- // "Sec-Tailscale: localapi" HTTP header and passing cred as a basic auth.
- //
- // It will start the server and the local client listener if they have not
- // been started yet.
- //
- // If you only need to use the LocalAPI from Go, then prefer LocalClient
- // as it does not require communication via TCP.
- func (s *Server) LoopbackLocalAPI() (addr string, cred string, err error) {
- if err := s.Start(); err != nil {
- return "", "", err
- }
- if s.localAPITCPListener == nil {
- 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.localAPITCPListener = ln
- go func() {
- lah := localapi.NewHandler(s.lb, s.logf, s.logid)
- lah.PermitWrite = true
- lah.PermitRead = true
- lah.RequiredPassword = s.localAPICred
- h := &localSecHandler{h: lah, cred: s.localAPICred}
- if err := http.Serve(s.localAPITCPListener, h); err != nil {
- s.logf("localapi tcp serve error: %v", err)
- }
- }()
- }
- return s.localAPITCPListener.Addr().String(), 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")
- }
- 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 {
- 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()
- }
- }()
- if _, isMemStore := s.Store.(*mem.Store); isMemStore && s.Ephemeral && s.lb != nil {
- wg.Add(1)
- go func() {
- defer wg.Done()
- // Perform a best-effort logout.
- s.lb.LogoutSync(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.linkMon != nil {
- s.linkMon.Close()
- }
- if s.dialer != nil {
- s.dialer.Close()
- }
- if s.localAPIListener != nil {
- s.localAPIListener.Close()
- }
- if s.localAPITCPListener != nil {
- s.localAPITCPListener.Close()
- }
- s.mu.Lock()
- defer s.mu.Unlock()
- for _, ln := range s.listeners {
- ln.Close()
- }
- s.listeners = nil
- wg.Wait()
- 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)
- }
- }
- func (s *Server) getAuthKey() string {
- if v := s.AuthKey; v != "" {
- return v
- }
- return os.Getenv("TS_AUTHKEY")
- }
- func (s *Server) start() (reterr error) {
- var closePool closeOnErrorPool
- defer closePool.closeAllIfError(&reterr)
- exe, err := os.Executable()
- if err != nil {
- 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")
- }
- }
- logf := s.logf
- if s.rootPath == "" {
- confDir, err := os.UserConfigDir()
- if err != nil {
- return err
- }
- s.rootPath, err = getTSNetDir(logf, confDir, prog)
- if err != nil {
- return err
- }
- 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)
- }
- 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.String()
- 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,
- NewZstdEncoder: func() logtail.Encoder {
- w, err := smallzstd.NewEncoder(nil)
- if err != nil {
- panic(err)
- }
- return w
- },
- HTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost)},
- }
- s.logtail = logtail.NewLogger(c, logf)
- closePool.addFunc(func() { s.logtail.Shutdown(context.Background()) })
- s.linkMon, err = monitor.New(logf)
- if err != nil {
- return err
- }
- closePool.add(s.linkMon)
- s.dialer = &tsdial.Dialer{Logf: logf} // mutated below (before used)
- eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
- ListenPort: 0,
- LinkMonitor: s.linkMon,
- Dialer: s.dialer,
- })
- if err != nil {
- return err
- }
- closePool.add(s.dialer)
- tunDev, magicConn, dns, ok := eng.(wgengine.InternalsGetter).GetInternals()
- if !ok {
- return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
- }
- ns, err := netstack.Create(logf, tunDev, eng, magicConn, s.dialer, dns)
- if err != nil {
- return fmt.Errorf("netstack.Create: %w", err)
- }
- ns.ProcessLocalIPs = true
- ns.ForwardTCPIn = s.forwardTCP
- 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) {
- return ns.DialContextTCP(ctx, dst)
- }
- if s.Store == nil {
- stateFile := filepath.Join(s.rootPath, "tailscaled.state")
- logf("tsnet running state path %s", stateFile)
- s.Store, err = store.New(logf, stateFile)
- if err != nil {
- return err
- }
- }
- loginFlags := controlclient.LoginDefault
- if s.Ephemeral {
- loginFlags = controlclient.LoginEphemeral
- }
- lb, err := ipnlocal.NewLocalBackend(logf, s.logid, s.Store, s.dialer, eng, loginFlags)
- if err != nil {
- return fmt.Errorf("NewLocalBackend: %v", err)
- }
- lb.SetVarRoot(s.rootPath)
- 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() })
- lb.SetDecompressor(func() (controlclient.Decompressor, error) {
- return smallzstd.NewDecoder(nil)
- })
- prefs := ipn.NewPrefs()
- prefs.Hostname = s.hostname
- prefs.WantRunning = true
- prefs.ControlURL = s.ControlURL
- authKey := s.getAuthKey()
- 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") {
- logf("LocalBackend state is %v; running StartLoginInteractive...", st)
- s.lb.StartLoginInteractive()
- } else if authKey != "" {
- 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(lb, logf, s.logid)
- 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 = &tailscale.LocalClient{Dial: lal.Dial}
- go func() {
- if err := http.Serve(lal, lah); err != nil {
- logf("localapi serve error: %v", err)
- }
- }()
- closePool.add(s.localAPIListener)
- 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 ...interface{}) {
- if s.logtail != nil {
- s.logtail.Logf(format, a...)
- }
- if s.Logf != nil {
- s.Logf(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 {
- s.logf("printAuthURLLoop: state is %v; stopping", 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
- }
- }
- }
- func (s *Server) forwardTCP(c net.Conn, port uint16) {
- s.mu.Lock()
- ln, ok := s.listeners[listenKey{"tcp", "", port}]
- s.mu.Unlock()
- if !ok {
- c.Close()
- return
- }
- t := time.NewTimer(time.Second)
- defer t.Stop()
- select {
- case ln.conn <- c:
- case <-t.C:
- c.Close()
- }
- }
- // getTSNetDir usually just returns filepath.Join(confDir, "tsnet-"+prog)
- // with no error.
- //
- // One special case is that it renames old "tslib-" directories to
- // "tsnet-", and that rename might return an error.
- //
- // TODO(bradfitz): remove this maybe 6 months after 2022-03-17,
- // once people (notably Tailscale corp services) have updated.
- func getTSNetDir(logf logger.Logf, confDir, prog string) (string, error) {
- oldPath := filepath.Join(confDir, "tslib-"+prog)
- newPath := filepath.Join(confDir, "tsnet-"+prog)
- fi, err := os.Lstat(oldPath)
- if os.IsNotExist(err) {
- // Common path.
- return newPath, nil
- }
- if err != nil {
- return "", err
- }
- if !fi.IsDir() {
- return "", fmt.Errorf("expected old tslib path %q to be a directory; got %v", oldPath, fi.Mode())
- }
- // At this point, oldPath exists and is a directory. But does
- // the new path exist?
- fi, err = os.Lstat(newPath)
- if err == nil && fi.IsDir() {
- // New path already exists somehow. Ignore the old one and
- // don't try to migrate it.
- return newPath, nil
- }
- if err != nil && !os.IsNotExist(err) {
- return "", err
- }
- if err := os.Rename(oldPath, newPath); err != nil {
- return "", err
- }
- logf("renamed old tsnet state storage directory %q to %q", oldPath, newPath)
- return newPath, nil
- }
- // APIClient returns a tailscale.Client that can be used to make authenticated
- // requests to the Tailscale control server.
- // It requires the user to set tailscale.I_Acknowledge_This_API_Is_Unstable.
- func (s *Server) APIClient() (*tailscale.Client, error) {
- if !tailscale.I_Acknowledge_This_API_Is_Unstable {
- return nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable")
- }
- if err := s.Start(); err != nil {
- return nil, err
- }
- c := tailscale.NewClient("-", nil)
- c.HTTPClient = &http.Client{Transport: s.lb.KeyProvingNoiseRoundTripper()}
- return c, nil
- }
- // Listen announces only on the Tailscale network.
- // It will start the server if it has not been started yet.
- func (s *Server) Listen(network, addr string) (net.Listener, error) {
- switch network {
- case "", "tcp", "tcp4", "tcp6":
- default:
- return nil, errors.New("unsupported network type")
- }
- host, portStr, err := net.SplitHostPort(addr)
- if err != nil {
- return nil, fmt.Errorf("tsnet: %w", err)
- }
- port, err := net.LookupPort(network, portStr)
- if err != nil || port < 0 || port > math.MaxUint16 {
- return nil, fmt.Errorf("invalid port: %w", err)
- }
- if err := s.Start(); err != nil {
- return nil, err
- }
- key := listenKey{network, host, uint16(port)}
- ln := &listener{
- s: s,
- key: key,
- addr: addr,
- conn: make(chan net.Conn),
- }
- s.mu.Lock()
- if _, ok := s.listeners[key]; ok {
- s.mu.Unlock()
- return nil, fmt.Errorf("tsnet: listener already open for %s, %s", network, addr)
- }
- mak.Set(&s.listeners, key, ln)
- s.mu.Unlock()
- return ln, nil
- }
- type listenKey struct {
- network string
- host string
- port uint16
- }
- type listener struct {
- s *Server
- key listenKey
- addr string
- conn chan net.Conn
- }
- func (ln *listener) Accept() (net.Conn, error) {
- c, ok := <-ln.conn
- if !ok {
- return nil, fmt.Errorf("tsnet: %w", net.ErrClosed)
- }
- return c, nil
- }
- func (ln *listener) Addr() net.Addr { return addr{ln} }
- func (ln *listener) Close() error {
- ln.s.mu.Lock()
- defer ln.s.mu.Unlock()
- if v, ok := ln.s.listeners[ln.key]; ok && v == ln {
- delete(ln.s.listeners, ln.key)
- close(ln.conn)
- }
- return nil
- }
- // 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.key.network }
- func (a addr) String() string { return a.ln.addr }
|