2020-02-05 22:16:58 +00:00
// Copyright (c) 2020 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.
2020-02-13 22:53:43 +00:00
// Package logpolicy manages the creation or reuse of logtail loggers,
// caching collection instance state on disk for use on future runs of
// programs on the same machine.
2020-02-05 22:16:58 +00:00
package logpolicy
import (
2020-03-26 03:40:24 +00:00
"bytes"
2020-02-05 22:16:58 +00:00
"context"
2020-04-04 16:20:54 +01:00
"crypto/tls"
2020-02-05 22:16:58 +00:00
"encoding/json"
2020-02-19 03:21:02 +00:00
"fmt"
2020-02-05 22:16:58 +00:00
"io/ioutil"
"log"
2020-04-04 16:20:54 +01:00
"net"
"net/http"
2021-02-04 17:20:17 +00:00
"net/url"
2020-02-05 22:16:58 +00:00
"os"
2020-04-05 01:34:25 +01:00
"os/exec"
2020-02-05 22:16:58 +00:00
"path/filepath"
"runtime"
2020-04-04 16:20:54 +01:00
"strconv"
2020-04-09 22:46:19 +01:00
"strings"
2020-04-04 16:20:54 +01:00
"time"
2020-02-05 22:16:58 +00:00
2020-12-09 23:28:31 +00:00
"golang.org/x/term"
2020-02-05 22:16:58 +00:00
"tailscale.com/atomicfile"
"tailscale.com/logtail"
"tailscale.com/logtail/filch"
2020-05-29 00:53:19 +01:00
"tailscale.com/net/netns"
2020-04-26 16:31:14 +01:00
"tailscale.com/net/tlsdial"
2020-08-13 23:25:54 +01:00
"tailscale.com/net/tshttpproxy"
2020-08-11 04:44:26 +01:00
"tailscale.com/paths"
2020-07-02 19:26:33 +01:00
"tailscale.com/smallzstd"
2020-08-05 22:50:43 +01:00
"tailscale.com/types/logger"
2020-11-16 18:13:06 +00:00
"tailscale.com/util/racebuild"
2020-02-05 22:16:58 +00:00
"tailscale.com/version"
)
2020-02-13 22:53:43 +00:00
// Config represents an instance of logs in a collection.
2020-02-05 22:16:58 +00:00
type Config struct {
Collection string
PrivateID logtail . PrivateID
PublicID logtail . PublicID
}
2020-02-13 22:53:43 +00:00
// Policy is a logger and its public ID.
2020-02-05 22:16:58 +00:00
type Policy struct {
2020-02-13 22:53:43 +00:00
// Logtail is the logger.
2020-12-21 17:03:39 +00:00
Logtail * logtail . Logger
2020-02-13 22:53:43 +00:00
// PublicID is the logger's instance identifier.
2020-02-05 22:16:58 +00:00
PublicID logtail . PublicID
}
2020-02-13 22:53:43 +00:00
// ToBytes returns the JSON representation of c.
2020-02-05 22:16:58 +00:00
func ( c * Config ) ToBytes ( ) [ ] byte {
data , err := json . MarshalIndent ( c , "" , "\t" )
if err != nil {
2020-08-05 22:50:43 +01:00
log . Fatalf ( "logpolicy.Config marshal: %v" , err )
2020-02-05 22:16:58 +00:00
}
return data
}
2020-02-13 22:53:43 +00:00
// Save writes the JSON representation of c to stateFile.
2020-02-19 03:21:02 +00:00
func ( c * Config ) save ( stateFile string ) error {
2020-02-05 22:16:58 +00:00
c . PublicID = c . PrivateID . Public ( )
2020-02-19 03:21:02 +00:00
if err := os . MkdirAll ( filepath . Dir ( stateFile ) , 0750 ) ; err != nil {
2020-02-13 22:53:43 +00:00
return err
}
2020-02-05 22:16:58 +00:00
data := c . ToBytes ( )
2020-02-13 22:53:43 +00:00
if err := atomicfile . WriteFile ( stateFile , data , 0600 ) ; err != nil {
return err
2020-02-05 22:16:58 +00:00
}
2020-02-13 22:53:43 +00:00
return nil
2020-02-05 22:16:58 +00:00
}
2020-02-13 22:53:43 +00:00
// ConfigFromBytes parses a a Config from its JSON encoding.
func ConfigFromBytes ( jsonEnc [ ] byte ) ( * Config , error ) {
2020-02-05 22:16:58 +00:00
c := & Config { }
2020-02-13 22:53:43 +00:00
if err := json . Unmarshal ( jsonEnc , c ) ; err != nil {
2020-02-05 22:16:58 +00:00
return nil , err
}
return c , nil
}
2020-02-13 22:53:43 +00:00
// stderrWriter is an io.Writer that always writes to the latest
// os.Stderr, even if os.Stderr changes during the lifetime of the
// stderrWriter value.
2020-02-05 22:16:58 +00:00
type stderrWriter struct { }
2020-02-13 22:53:43 +00:00
func ( stderrWriter ) Write ( buf [ ] byte ) ( int , error ) {
2020-02-05 22:16:58 +00:00
return os . Stderr . Write ( buf )
}
type logWriter struct {
logger * log . Logger
}
2020-02-13 22:53:43 +00:00
func ( l logWriter ) Write ( buf [ ] byte ) ( int , error ) {
l . logger . Printf ( "%s" , buf )
2020-02-05 22:16:58 +00:00
return len ( buf ) , nil
}
2020-02-19 03:21:02 +00:00
// logsDir returns the directory to use for log configuration and
// buffer storage.
2020-08-05 22:50:43 +01:00
func logsDir ( logf logger . Logf ) string {
2020-08-11 04:44:26 +01:00
// STATE_DIRECTORY is set by systemd 240+ but we support older
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
2020-04-05 01:34:25 +01:00
systemdStateDir := os . Getenv ( "STATE_DIRECTORY" )
if systemdStateDir != "" {
2020-08-05 22:50:43 +01:00
logf ( "logpolicy: using $STATE_DIRECTORY, %q" , systemdStateDir )
2020-04-05 01:34:25 +01:00
return systemdStateDir
2020-02-19 03:21:02 +00:00
}
2020-08-11 04:44:26 +01:00
// Default to e.g. /var/lib/tailscale or /var/db/tailscale on Unix.
if d := paths . DefaultTailscaledStateFile ( ) ; d != "" {
d = filepath . Dir ( d ) // directory of e.g. "/var/lib/tailscale/tailscaled.state"
if err := os . MkdirAll ( d , 0700 ) ; err == nil {
logf ( "logpolicy: using system state directory %q" , d )
return d
}
}
2020-02-19 03:21:02 +00:00
cacheDir , err := os . UserCacheDir ( )
if err == nil {
2020-08-05 22:50:43 +01:00
d := filepath . Join ( cacheDir , "Tailscale" )
logf ( "logpolicy: using UserCacheDir, %q" , d )
return d
2020-02-19 03:21:02 +00:00
}
2020-04-05 01:18:04 +01:00
// Use the current working directory, unless we're being run by a
// service manager that sets it to /.
wd , err := os . Getwd ( )
if err == nil && wd != "/" {
2020-08-05 22:50:43 +01:00
logf ( "logpolicy: using current directory, %q" , wd )
2020-04-05 01:18:04 +01:00
return wd
}
// No idea where to put stuff. Try to create a temp dir. It'll
// mean we might lose some logs and rotate through log IDs, but
// it's something.
tmp , err := ioutil . TempDir ( "" , "tailscaled-log-*" )
if err != nil {
panic ( "no safe place found to store log state" )
}
2020-08-05 22:50:43 +01:00
logf ( "logpolicy: using temp directory, %q" , tmp )
2020-04-05 01:18:04 +01:00
return tmp
2020-02-19 03:21:02 +00:00
}
2020-03-26 03:40:24 +00:00
// runningUnderSystemd reports whether we're running under systemd.
func runningUnderSystemd ( ) bool {
if runtime . GOOS == "linux" && os . Getppid ( ) == 1 {
slurp , _ := ioutil . ReadFile ( "/proc/1/stat" )
return bytes . HasPrefix ( slurp , [ ] byte ( "1 (systemd) " ) )
}
return false
}
2020-04-05 01:34:25 +01:00
// tryFixLogStateLocation is a temporary fixup for
// https://github.com/tailscale/tailscale/issues/247 . We accidentally
// wrote logging state files to /, and then later to $CACHE_DIRECTORY
// (which is incorrect because the log ID is not reconstructible if
// deleted - it's state, not cache data).
//
// If log state for cmdname exists in / or $CACHE_DIRECTORY, and no
// log state for that command exists in dir, then the log state is
// moved from whereever it does exist, into dir. Leftover logs state
// in / and $CACHE_DIRECTORY is deleted.
func tryFixLogStateLocation ( dir , cmdname string ) {
2020-07-30 15:47:19 +01:00
switch runtime . GOOS {
case "linux" , "freebsd" , "openbsd" :
// These are the OSes where we might have written stuff into
// root. Others use different logic to find the logs storage
// dir.
default :
return
}
2020-04-05 01:34:25 +01:00
if cmdname == "" {
log . Printf ( "[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale" )
return
}
if dir == "/" {
// Trying to store things in / still. That's a bug, but don't
// abort hard.
log . Printf ( "[unexpected] storing logging config in /, please file a bug at https://github.com/tailscale/tailscale" )
return
}
if os . Getuid ( ) != 0 {
// Only root could have written log configs to weird places.
return
}
// We stored logs in 2 incorrect places: either /, or CACHE_DIR
// (aka /var/cache/tailscale). We want to move files into the
// provided dir, preferring those in CACHE_DIR over those in / if
// both exist. If files already exist in dir, don't
// overwrite. Finally, once we've maybe moved files around, we
// want to delete leftovers in / and CACHE_DIR, to clean up after
// our past selves.
files := [ ] string {
fmt . Sprintf ( "%s.log.conf" , cmdname ) ,
fmt . Sprintf ( "%s.log1.txt" , cmdname ) ,
fmt . Sprintf ( "%s.log2.txt" , cmdname ) ,
}
// checks if any of the files above exist in d.
checkExists := func ( d string ) ( bool , error ) {
for _ , file := range files {
p := filepath . Join ( d , file )
_ , err := os . Stat ( p )
if os . IsNotExist ( err ) {
continue
} else if err != nil {
return false , fmt . Errorf ( "stat %q: %w" , p , err )
}
return true , nil
}
return false , nil
}
// move files from d into dir, if they exist.
moveFiles := func ( d string ) error {
for _ , file := range files {
src := filepath . Join ( d , file )
_ , err := os . Stat ( src )
if os . IsNotExist ( err ) {
continue
} else if err != nil {
return fmt . Errorf ( "stat %q: %v" , src , err )
}
dst := filepath . Join ( dir , file )
bs , err := exec . Command ( "mv" , src , dst ) . CombinedOutput ( )
if err != nil {
return fmt . Errorf ( "mv %q %q: %v (%s)" , src , dst , err , bs )
}
}
return nil
}
existsInRoot , err := checkExists ( "/" )
if err != nil {
log . Printf ( "checking for configs in /: %v" , err )
return
}
existsInCache := false
cacheDir := os . Getenv ( "CACHE_DIRECTORY" )
if cacheDir != "" {
existsInCache , err = checkExists ( "/var/cache/tailscale" )
if err != nil {
log . Printf ( "checking for configs in %s: %v" , cacheDir , err )
}
}
existsInDest , err := checkExists ( dir )
if err != nil {
log . Printf ( "checking for configs in %s: %v" , dir , err )
return
}
switch {
case ! existsInRoot && ! existsInCache :
// No leftover files, nothing to do.
return
case existsInDest :
// Already have "canonical" configs, just delete any remnants
// (below).
case existsInCache :
// CACHE_DIRECTORY takes precedence over /, move files from
// there.
if err := moveFiles ( cacheDir ) ; err != nil {
log . Print ( err )
return
}
case existsInRoot :
// Files from root is better than nothing.
if err := moveFiles ( "/" ) ; err != nil {
log . Print ( err )
return
}
}
// If moving succeeded, or we didn't need to move files, try to
// delete any leftover files, but it's okay if we can't delete
// them for some reason.
dirs := [ ] string { }
if existsInCache {
dirs = append ( dirs , cacheDir )
}
if existsInRoot {
dirs = append ( dirs , "/" )
}
for _ , d := range dirs {
for _ , file := range files {
p := filepath . Join ( d , file )
_ , err := os . Stat ( p )
if os . IsNotExist ( err ) {
continue
} else if err != nil {
log . Printf ( "stat %q: %v" , p , err )
return
}
if err := os . Remove ( p ) ; err != nil {
log . Printf ( "rm %q: %v" , p , err )
}
}
}
}
2020-02-13 22:53:43 +00:00
// New returns a new log policy (a logger and its instance ID) for a
2020-02-19 03:21:02 +00:00
// given collection name.
func New ( collection string ) * Policy {
2020-02-05 22:16:58 +00:00
var lflags int
2020-12-09 23:28:31 +00:00
if term . IsTerminal ( 2 ) || runtime . GOOS == "windows" {
2020-02-05 22:16:58 +00:00
lflags = 0
} else {
lflags = log . LstdFlags
}
2020-10-19 16:10:05 +01:00
if v , _ := strconv . ParseBool ( os . Getenv ( "TS_DEBUG_LOG_TIME" ) ) ; v {
lflags = log . LstdFlags | log . Lmicroseconds
}
2020-03-26 03:40:24 +00:00
if runningUnderSystemd ( ) {
// If journalctl is going to prepend its own timestamp
// anyway, no need to add one.
lflags = 0
}
2020-02-13 22:53:43 +00:00
console := log . New ( stderrWriter { } , "" , lflags )
2020-02-05 22:16:58 +00:00
2020-08-05 22:50:43 +01:00
var earlyErrBuf bytes . Buffer
earlyLogf := func ( format string , a ... interface { } ) {
fmt . Fprintf ( & earlyErrBuf , format , a ... )
earlyErrBuf . WriteByte ( '\n' )
}
dir := logsDir ( earlyLogf )
2020-04-05 01:34:25 +01:00
2020-07-30 15:47:19 +01:00
cmdName := version . CmdName ( )
tryFixLogStateLocation ( dir , cmdName )
2020-04-05 01:34:25 +01:00
2020-07-30 15:47:19 +01:00
cfgPath := filepath . Join ( dir , fmt . Sprintf ( "%s.log.conf" , cmdName ) )
2021-02-05 18:53:15 +00:00
// The Windows service previously ran as tailscale-ipn.exe, so
// let's keep using that log base name if it exists.
if runtime . GOOS == "windows" && cmdName == "tailscaled" {
const oldCmdName = "tailscale-ipn"
oldPath := filepath . Join ( dir , oldCmdName + ".log.conf" )
if fi , err := os . Stat ( oldPath ) ; err == nil && fi . Mode ( ) . IsRegular ( ) {
cfgPath = oldPath
cmdName = oldCmdName
}
}
2020-02-05 22:16:58 +00:00
var oldc * Config
2020-02-19 03:21:02 +00:00
data , err := ioutil . ReadFile ( cfgPath )
2020-02-05 22:16:58 +00:00
if err != nil {
2020-08-05 22:50:43 +01:00
earlyLogf ( "logpolicy.Read %v: %v" , cfgPath , err )
2020-02-05 22:16:58 +00:00
oldc = & Config { }
oldc . Collection = collection
} else {
oldc , err = ConfigFromBytes ( data )
if err != nil {
2020-08-05 22:50:43 +01:00
earlyLogf ( "logpolicy.Config unmarshal: %v" , err )
2020-02-05 22:16:58 +00:00
oldc = & Config { }
}
}
newc := * oldc
if newc . Collection != collection {
log . Printf ( "logpolicy.Config: config collection %q does not match %q" , newc . Collection , collection )
// We picked up an incompatible config file.
// Regenerate the private ID.
newc . PrivateID = logtail . PrivateID { }
newc . Collection = collection
}
2020-10-01 01:13:41 +01:00
if newc . PrivateID . IsZero ( ) {
2020-02-05 22:16:58 +00:00
newc . PrivateID , err = logtail . NewPrivateID ( )
if err != nil {
log . Fatalf ( "logpolicy: NewPrivateID() should never fail" )
}
}
newc . PublicID = newc . PrivateID . Public ( )
if newc != * oldc {
2020-02-19 03:21:02 +00:00
if err := newc . save ( cfgPath ) ; err != nil {
2020-08-05 22:50:43 +01:00
earlyLogf ( "logpolicy.Config.Save: %v" , err )
2020-02-13 22:53:43 +00:00
}
2020-02-05 22:16:58 +00:00
}
c := logtail . Config {
Collection : newc . Collection ,
PrivateID : newc . PrivateID ,
2020-02-13 22:53:43 +00:00
Stderr : logWriter { console } ,
2020-02-05 22:16:58 +00:00
NewZstdEncoder : func ( ) logtail . Encoder {
2020-07-02 19:26:33 +01:00
w , err := smallzstd . NewEncoder ( nil )
2020-02-05 22:16:58 +00:00
if err != nil {
panic ( err )
}
return w
} ,
2020-04-26 16:31:14 +01:00
HTTPC : & http . Client { Transport : newLogtailTransport ( logtail . DefaultHost ) } ,
2020-02-05 22:16:58 +00:00
}
2021-02-04 17:38:30 +00:00
if val , ok := os . LookupEnv ( "TS_LOG_TARGET" ) ; ok {
2021-02-04 17:20:17 +00:00
log . Println ( "You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult." )
c . BaseURL = val
u , _ := url . Parse ( val )
c . HTTPC = & http . Client { Transport : newLogtailTransport ( u . Host ) }
}
2020-07-30 15:47:19 +01:00
filchBuf , filchErr := filch . New ( filepath . Join ( dir , cmdName ) , filch . Options { } )
2020-02-05 22:16:58 +00:00
if filchBuf != nil {
c . Buffer = filchBuf
}
2020-12-21 17:03:39 +00:00
lw := logtail . NewLogger ( c , log . Printf )
2020-02-05 22:16:58 +00:00
log . SetFlags ( 0 ) // other logflags are set on console, not here
log . SetOutput ( lw )
2020-08-05 22:50:43 +01:00
log . Printf ( "Program starting: v%v, Go %v: %#v" ,
2020-10-27 04:23:58 +00:00
version . Long ,
2020-11-16 18:13:06 +00:00
goVersion ( ) ,
2020-04-09 22:46:19 +01:00
os . Args )
2020-08-05 22:50:43 +01:00
log . Printf ( "LogID: %v" , newc . PublicID )
2020-02-05 22:16:58 +00:00
if filchErr != nil {
2020-05-01 09:51:19 +01:00
log . Printf ( "filch failed: %v" , filchErr )
2020-02-05 22:16:58 +00:00
}
2020-08-05 22:50:43 +01:00
if earlyErrBuf . Len ( ) != 0 {
log . Printf ( "%s" , earlyErrBuf . Bytes ( ) )
}
2020-02-05 22:16:58 +00:00
return & Policy {
Logtail : lw ,
PublicID : newc . PublicID ,
}
}
2020-12-21 18:53:18 +00:00
// SetVerbosityLevel controls the verbosity level that should be
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
// are increasingly verbose.
//
// It should not be changed concurrently with log writes.
func ( p * Policy ) SetVerbosityLevel ( level int ) {
p . Logtail . SetVerbosityLevel ( level )
}
2020-02-05 22:16:58 +00:00
// Close immediately shuts down the logger.
func ( p * Policy ) Close ( ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
cancel ( )
p . Shutdown ( ctx )
}
// Shutdown gracefully shuts down the logger, finishing any current
// log upload if it can be done before ctx is canceled.
func ( p * Policy ) Shutdown ( ctx context . Context ) error {
if p . Logtail != nil {
2020-08-05 22:50:43 +01:00
log . Printf ( "flushing log." )
2020-02-05 22:16:58 +00:00
return p . Logtail . Shutdown ( ctx )
}
return nil
}
2020-04-04 16:20:54 +01:00
2020-04-26 16:31:14 +01:00
// newLogtailTransport returns the HTTP Transport we use for uploading
// logs to the given host name.
func newLogtailTransport ( host string ) * http . Transport {
2020-04-04 16:20:54 +01:00
// Start with a copy of http.DefaultTransport and tweak it a bit.
tr := http . DefaultTransport . ( * http . Transport ) . Clone ( )
2020-08-13 23:25:54 +01:00
tr . Proxy = tshttpproxy . ProxyFromEnvironment
2020-08-27 04:02:16 +01:00
tshttpproxy . SetTransportGetProxyConnectHeader ( tr )
2020-08-13 23:25:54 +01:00
2020-04-04 16:20:54 +01:00
// We do our own zstd compression on uploads, and responses never contain any payload,
// so don't send "Accept-Encoding: gzip" to save a few bytes on the wire, since there
// will never be any body to decompress:
tr . DisableCompression = true
// Log whenever we dial:
tr . DialContext = func ( ctx context . Context , netw , addr string ) ( net . Conn , error ) {
2020-06-01 18:50:37 +01:00
nd := netns . FromDialer ( & net . Dialer {
Timeout : 30 * time . Second ,
KeepAlive : 30 * time . Second ,
} )
2020-04-04 16:20:54 +01:00
t0 := time . Now ( )
c , err := nd . DialContext ( ctx , netw , addr )
d := time . Since ( t0 ) . Round ( time . Millisecond )
if err != nil {
log . Printf ( "logtail: dial %q failed: %v (in %v)" , addr , err , d )
} else {
log . Printf ( "logtail: dialed %q in %v" , addr , d )
}
return c , err
}
// We're contacting exactly 1 hostname, so the default's 100
// max idle conns is very high for our needs. Even 2 is
// probably double what we need:
tr . MaxIdleConns = 2
// Provide knob to force HTTP/1 for log uploads.
// TODO(bradfitz): remove this debug knob once we've decided
// to upload via HTTP/1 or HTTP/2 (probably HTTP/1). Or we might just enforce
// it server-side.
if h1 , _ := strconv . ParseBool ( os . Getenv ( "TS_DEBUG_FORCE_H1_LOGS" ) ) ; h1 {
tr . TLSClientConfig = nil // DefaultTransport's was already initialized w/ h2
tr . ForceAttemptHTTP2 = false
tr . TLSNextProto = map [ string ] func ( authority string , c * tls . Conn ) http . RoundTripper { }
}
2020-04-26 16:31:14 +01:00
tr . TLSClientConfig = tlsdial . Config ( host , tr . TLSClientConfig )
2020-04-04 16:20:54 +01:00
return tr
}
2020-11-16 18:13:06 +00:00
func goVersion ( ) string {
v := strings . TrimPrefix ( runtime . Version ( ) , "go" )
if racebuild . On {
return v + "-race"
}
return v
}