2021-05-14 15:53:55 +00:00
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package tsnet provides Tailscale as a library.
//
// It is an experimental work in progress.
package tsnet
import (
2021-12-01 04:39:12 +00:00
"context"
2021-05-14 15:53:55 +00:00
"fmt"
"log"
"net"
2021-10-06 04:40:19 +00:00
"net/http"
2021-05-14 15:53:55 +00:00
"os"
"path/filepath"
2022-03-23 03:24:57 +00:00
"runtime"
2021-05-14 15:53:55 +00:00
"strings"
"sync"
"time"
2021-12-01 04:39:12 +00:00
"inet.af/netaddr"
2021-10-06 04:40:19 +00:00
"tailscale.com/client/tailscale"
2021-05-14 15:53:55 +00:00
"tailscale.com/control/controlclient"
2021-08-26 21:50:55 +00:00
"tailscale.com/envknob"
2021-05-14 15:53:55 +00:00
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
2021-10-06 04:40:19 +00:00
"tailscale.com/ipn/localapi"
2022-02-28 21:08:45 +00:00
"tailscale.com/ipn/store"
"tailscale.com/ipn/store/mem"
2021-10-06 04:40:19 +00:00
"tailscale.com/net/nettest"
2021-12-01 04:39:12 +00:00
"tailscale.com/net/tsdial"
2021-05-14 15:53:55 +00:00
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"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
2022-02-28 21:08:45 +00:00
// 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.
Store ipn . StateStore
2021-05-14 15:53:55 +00:00
// Hostname is the hostname to present to the control server.
2021-09-01 14:54:49 +00:00
// If empty, the binary name is used.
2021-05-14 15:53:55 +00:00
Hostname string
// Logf, if non-nil, specifies the logger to use. By default,
// log.Printf is used.
Logf logger . Logf
2022-02-18 20:55:22 +00:00
// Ephemeral, if true, specifies that the instance should register
// as an Ephemeral node (https://tailscale.com/kb/1111/ephemeral-nodes/).
2022-02-22 23:32:13 +00:00
Ephemeral bool
2022-02-18 20:55:22 +00:00
2022-03-17 16:03:02 +00:00
initOnce sync . Once
initErr error
lb * ipnlocal . LocalBackend
linkMon * monitor . Mon
localAPIListener net . Listener
rootPath string // the state directory
hostname string
shutdownCtx context . Context
shutdownCancel context . CancelFunc
2021-05-14 15:53:55 +00:00
mu sync . Mutex
listeners map [ listenKey ] * listener
2021-12-28 09:06:33 +00:00
dialer * tsdial . Dialer
}
// Dial connects to the address on the tailnet.
2022-01-03 19:20:40 +00:00
// It will start the server if it has not been started yet.
2021-12-28 09:06:33 +00:00
func ( s * Server ) Dial ( ctx context . Context , network , address string ) ( net . Conn , error ) {
2022-01-03 19:20:40 +00:00
if err := s . Start ( ) ; err != nil {
2021-12-28 09:06:33 +00:00
return nil , err
}
return s . dialer . UserDial ( ctx , network , address )
2021-05-14 15:53:55 +00:00
}
2022-01-03 19:20:40 +00:00
// Start connects the server to the tailnet.
// Optional: any calls to Dial/Listen will also call Start.
func ( s * Server ) Start ( ) error {
2022-03-23 03:24:57 +00:00
if runtime . GOOS == "darwin" && runtime . Version ( ) == "go1.18" {
log . Fatalf ( "Tailscale is broken on macOS with go1.18 due to upstream bug https://github.com/golang/go/issues/51759; use 1.18.1+ or Tailscale's Go fork" )
}
2021-12-28 09:06:33 +00:00
s . initOnce . Do ( s . doInit )
return s . initErr
}
2022-03-17 16:03:02 +00:00
// Close stops the server.
//
// It must not be called before or concurrently with Start.
func ( s * Server ) Close ( ) error {
s . shutdownCancel ( )
s . lb . Shutdown ( )
s . linkMon . Close ( )
s . localAPIListener . Close ( )
s . mu . Lock ( )
defer s . mu . Unlock ( )
for _ , ln := range s . listeners {
ln . Close ( )
}
s . listeners = nil
return nil
}
2022-02-28 21:08:45 +00:00
func ( s * Server ) doInit ( ) {
2022-03-17 16:03:02 +00:00
s . shutdownCtx , s . shutdownCancel = context . WithCancel ( context . Background ( ) )
2022-02-28 21:08:45 +00:00
if err := s . start ( ) ; err != nil {
s . initErr = fmt . Errorf ( "tsnet: %w" , err )
}
}
2021-05-14 15:53:55 +00:00
func ( s * Server ) start ( ) error {
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
}
2022-02-28 21:08:45 +00:00
s . rootPath = s . Dir
if s . Store != nil && ! s . Ephemeral {
if _ , ok := s . Store . ( * mem . Store ) ; ! ok {
return fmt . Errorf ( "in-memory store is only supported for Ephemeral nodes" )
}
}
2022-03-17 16:03:02 +00:00
logf := s . logf
2022-02-28 21:08:45 +00:00
if s . rootPath == "" {
2021-05-14 15:53:55 +00:00
confDir , err := os . UserConfigDir ( )
if err != nil {
return err
}
2022-03-17 16:03:02 +00:00
s . rootPath , err = getTSNetDir ( logf , confDir , prog )
if err != nil {
return err
}
2022-02-28 21:08:45 +00:00
if err := os . MkdirAll ( s . rootPath , 0700 ) ; err != nil {
2021-05-14 15:53:55 +00:00
return err
}
}
2022-02-28 21:08:45 +00:00
if fi , err := os . Stat ( s . rootPath ) ; err != nil {
2021-05-14 15:53:55 +00:00
return err
} else if ! fi . IsDir ( ) {
2022-02-28 21:08:45 +00:00
return fmt . Errorf ( "%v is not a directory" , s . rootPath )
2021-05-14 15:53:55 +00:00
}
// TODO(bradfitz): start logtail? don't use filch, perhaps?
// only upload plumbed Logf?
2022-03-17 16:03:02 +00:00
s . linkMon , err = monitor . New ( logf )
2021-05-14 15:53:55 +00:00
if err != nil {
return err
}
2021-12-28 09:06:33 +00:00
s . dialer = new ( tsdial . Dialer ) // mutated below (before used)
2021-05-14 15:53:55 +00:00
eng , err := wgengine . NewUserspaceEngine ( logf , wgengine . Config {
ListenPort : 0 ,
2022-03-17 16:03:02 +00:00
LinkMonitor : s . linkMon ,
2021-12-28 09:06:33 +00:00
Dialer : s . dialer ,
2021-05-14 15:53:55 +00:00
} )
if err != nil {
return err
}
tunDev , magicConn , ok := eng . ( wgengine . InternalsGetter ) . GetInternals ( )
if ! ok {
return fmt . Errorf ( "%T is not a wgengine.InternalsGetter" , eng )
}
2021-12-28 09:06:33 +00:00
ns , err := netstack . Create ( logf , tunDev , eng , magicConn , s . dialer )
2021-05-14 15:53:55 +00:00
if err != nil {
return fmt . Errorf ( "netstack.Create: %w" , err )
}
2021-10-29 23:21:18 +00:00
ns . ProcessLocalIPs = true
2021-05-14 15:53:55 +00:00
ns . ForwardTCPIn = s . forwardTCP
if err := ns . Start ( ) ; err != nil {
return fmt . Errorf ( "failed to start netstack: %w" , err )
}
2021-12-28 09:06:33 +00:00
s . dialer . UseNetstackForIP = func ( ip netaddr . IP ) bool {
2021-12-01 04:39:12 +00:00
_ , ok := eng . PeerForIP ( ip )
return ok
}
2021-12-28 09:06:33 +00:00
s . dialer . NetstackDialTCP = func ( ctx context . Context , dst netaddr . IPPort ) ( net . Conn , error ) {
2021-12-03 16:33:05 +00:00
return ns . DialContextTCP ( ctx , dst )
2021-12-01 04:39:12 +00:00
}
2021-05-14 15:53:55 +00:00
2022-02-28 21:08:45 +00:00
if s . Store == nil {
2022-03-17 16:03:02 +00:00
stateFile := filepath . Join ( s . rootPath , "tailscaled.state" )
logf ( "tsnet running state path %s" , stateFile )
s . Store , err = store . New ( logf , stateFile )
2022-02-28 21:08:45 +00:00
if err != nil {
return err
}
2021-05-14 15:53:55 +00:00
}
2022-03-17 16:03:02 +00:00
logid := "tsnet-TODO" // https://github.com/tailscale/tailscale/issues/3866
2021-05-14 15:53:55 +00:00
2022-02-18 20:55:22 +00:00
loginFlags := controlclient . LoginDefault
2022-02-22 23:32:13 +00:00
if s . Ephemeral {
2022-02-18 20:55:22 +00:00
loginFlags = controlclient . LoginEphemeral
}
2022-02-28 21:08:45 +00:00
lb , err := ipnlocal . NewLocalBackend ( logf , logid , s . Store , s . dialer , eng , loginFlags )
2021-05-14 15:53:55 +00:00
if err != nil {
return fmt . Errorf ( "NewLocalBackend: %v" , err )
}
2022-02-28 21:08:45 +00:00
lb . SetVarRoot ( s . rootPath )
2022-03-17 16:03:02 +00:00
logf ( "tsnet starting with hostname %q, varRoot %q" , s . hostname , s . rootPath )
2021-05-14 15:53:55 +00:00
s . lb = lb
lb . SetDecompressor ( func ( ) ( controlclient . Decompressor , error ) {
return smallzstd . NewDecoder ( nil )
} )
prefs := ipn . NewPrefs ( )
prefs . Hostname = s . hostname
prefs . WantRunning = true
2022-03-17 16:03:02 +00:00
authKey := os . Getenv ( "TS_AUTHKEY" )
2021-05-14 15:53:55 +00:00
err = lb . Start ( ipn . Options {
StateKey : ipn . GlobalDaemonStateKey ,
UpdatePrefs : prefs ,
2022-03-17 16:03:02 +00:00
AuthKey : authKey ,
2021-05-14 15:53:55 +00:00
} )
if err != nil {
return fmt . Errorf ( "starting backend: %w" , err )
}
2022-03-20 03:00:43 +00:00
st := lb . State ( )
if st == ipn . NeedsLogin || envknob . Bool ( "TSNET_FORCE_LOGIN" ) {
logf ( "LocalBackend state is %v; running StartLoginInteractive..." , st )
2021-05-14 15:53:55 +00:00
s . lb . StartLoginInteractive ( )
2022-03-17 16:03:02 +00:00
} else if authKey != "" {
2022-03-20 03:00:43 +00:00
logf ( "TS_AUTHKEY is set; but state is %v. Ignoring authkey. Re-run with TSNET_FORCE_LOGIN=1 to force use of authkey." , st )
2021-05-14 15:53:55 +00:00
}
2022-03-17 16:03:02 +00:00
go s . printAuthURLLoop ( )
2021-10-06 04:40:19 +00:00
// Run the localapi handler, to allow fetching LetsEncrypt certs.
lah := localapi . NewHandler ( lb , logf , logid )
lah . PermitWrite = true
lah . PermitRead = true
// Create an in-process listener.
// nettest.Listen provides a in-memory pipe based implementation for net.Conn.
// TODO(maisem): Rename nettest package to remove "test".
lal := nettest . Listen ( "local-tailscaled.sock:80" )
2022-03-17 16:03:02 +00:00
s . localAPIListener = lal
2021-10-06 04:40:19 +00:00
// Override the Tailscale client to use the in-process listener.
tailscale . TailscaledDialer = lal . Dial
go func ( ) {
if err := http . Serve ( lal , lah ) ; err != nil {
logf ( "localapi serve error: %v" , err )
}
} ( )
2021-05-14 15:53:55 +00:00
return nil
}
2022-03-17 16:03:02 +00:00
func ( s * Server ) logf ( format string , a ... interface { } ) {
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
}
}
}
2021-05-14 15:53:55 +00:00
func ( s * Server ) forwardTCP ( c net . Conn , port uint16 ) {
s . mu . Lock ( )
ln , ok := s . listeners [ listenKey { "tcp" , "" , fmt . Sprint ( 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 ( )
}
}
2022-03-17 16:03:02 +00:00
// 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
}
2021-12-28 09:06:33 +00:00
// Listen announces only on the Tailscale network.
2022-01-03 19:20:40 +00:00
// It will start the server if it has not been started yet.
2021-05-14 15:53:55 +00:00
func ( s * Server ) Listen ( network , addr string ) ( net . Listener , error ) {
host , port , err := net . SplitHostPort ( addr )
if err != nil {
return nil , fmt . Errorf ( "tsnet: %w" , err )
}
2022-01-03 19:20:40 +00:00
if err := s . Start ( ) ; err != nil {
2021-12-28 09:06:33 +00:00
return nil , err
2021-05-14 15:53:55 +00:00
}
key := listenKey { network , host , port }
ln := & listener {
s : s ,
key : key ,
addr : addr ,
conn : make ( chan net . Conn ) ,
}
s . mu . Lock ( )
if s . listeners == nil {
s . listeners = map [ listenKey ] * listener { }
}
if _ , ok := s . listeners [ key ] ; ok {
s . mu . Unlock ( )
return nil , fmt . Errorf ( "tsnet: listener already open for %s, %s" , network , addr )
}
s . listeners [ key ] = ln
s . mu . Unlock ( )
return ln , nil
}
type listenKey struct {
network string
host string
port string
}
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
}
type addr struct { ln * listener }
func ( a addr ) Network ( ) string { return a . ln . key . network }
func ( a addr ) String ( ) string { return a . ln . addr }