diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index bfaf75270..0f8ec3ada 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -323,6 +323,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/feature/runtimemetrics from tailscale.com/feature/condregister L tailscale.com/feature/sdnotify from tailscale.com/feature/condregister LD tailscale.com/feature/ssh from tailscale.com/cmd/tailscaled + L tailscale.com/feature/syslog from tailscale.com/feature/condregister tailscale.com/feature/syspolicy from tailscale.com/feature/condregister tailscale.com/feature/taildrop from tailscale.com/feature/condregister tailscale.com/feature/tailnetlock from tailscale.com/feature/condregister @@ -745,7 +746,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de iter from maps+ log from expvar+ log/internal from log - LD log/syslog from tailscale.com/ssh/tailssh + LD log/syslog from tailscale.com/ssh/tailssh+ maps from tailscale.com/clientupdate+ math from archive/tar+ math/big from crypto/dsa+ diff --git a/cmd/tailscaled/deps_test.go b/cmd/tailscaled/deps_test.go index ed8a83aad..f18facd7b 100644 --- a/cmd/tailscaled/deps_test.go +++ b/cmd/tailscaled/deps_test.go @@ -33,6 +33,21 @@ func TestOmitSSH(t *testing.T) { }.Check(t) } +func TestOmitSyslog(t *testing.T) { + const msg = "unexpected syslog usage with ts_omit_syslog" + deptest.DepChecker{ + GOOS: "linux", + GOARCH: "amd64", + // Tailscale SSH's incubator also uses log/syslog, so omit + // SSH too to lock down the standard library package. + Tags: "ts_omit_syslog,ts_omit_ssh,ts_include_cli", + BadDeps: map[string]string{ + "log/syslog": msg, + "tailscale.com/feature/syslog": msg, + }, + }.Check(t) +} + func TestOmitSyspolicy(t *testing.T) { const msg = "unexpected syspolicy usage with ts_omit_syspolicy" deptest.DepChecker{ diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 41128564a..d7c19f51b 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -233,6 +233,9 @@ func main() { if f, ok := hookRegisterOutboundProxyFlags.GetOk(); ok { f() } + if f, ok := feature.HookRegisterLogSinkFlags.GetOk(); ok { + f() + } if runtime.GOOS == "plan9" && os.Getenv("_NETSHELL_CHILD_") != "" { os.Args = []string{"tailscaled", "be-child", "plan9-netshell"} @@ -261,6 +264,10 @@ func main() { } } + if f, ok := feature.HookLogSink.GetOk(); ok { + f() // redirects the default logger (e.g. to syslog) if requested by flags + } + if fd, ok := envknob.LookupInt("TS_PARENT_DEATH_FD"); ok && fd > 2 { go dieOnPipeReadErrorOfFD(fd) } diff --git a/feature/buildfeatures/feature_syslog_disabled.go b/feature/buildfeatures/feature_syslog_disabled.go new file mode 100644 index 000000000..2e15efe8a --- /dev/null +++ b/feature/buildfeatures/feature_syslog_disabled.go @@ -0,0 +1,13 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// Code generated by gen.go; DO NOT EDIT. + +//go:build ts_omit_syslog + +package buildfeatures + +// HasSyslog is whether the binary was built with support for modular feature "tailscaled --syslog flag support to send logs to the system syslog daemon". +// Specifically, it's whether the binary was NOT built with the "ts_omit_syslog" build tag. +// It's a const so it can be used for dead code elimination. +const HasSyslog = false diff --git a/feature/buildfeatures/feature_syslog_enabled.go b/feature/buildfeatures/feature_syslog_enabled.go new file mode 100644 index 000000000..3167c971a --- /dev/null +++ b/feature/buildfeatures/feature_syslog_enabled.go @@ -0,0 +1,13 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// Code generated by gen.go; DO NOT EDIT. + +//go:build !ts_omit_syslog + +package buildfeatures + +// HasSyslog is whether the binary was built with support for modular feature "tailscaled --syslog flag support to send logs to the system syslog daemon". +// Specifically, it's whether the binary was NOT built with the "ts_omit_syslog" build tag. +// It's a const so it can be used for dead code elimination. +const HasSyslog = true diff --git a/feature/condregister/maybe_syslog.go b/feature/condregister/maybe_syslog.go new file mode 100644 index 000000000..9527c472b --- /dev/null +++ b/feature/condregister/maybe_syslog.go @@ -0,0 +1,8 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +//go:build (linux || freebsd || openbsd) && !ts_omit_syslog + +package condregister + +import _ "tailscale.com/feature/syslog" diff --git a/feature/featuretags/featuretags.go b/feature/featuretags/featuretags.go index 428a5f2b8..d567baeb9 100644 --- a/feature/featuretags/featuretags.go +++ b/feature/featuretags/featuretags.go @@ -267,6 +267,10 @@ type FeatureMeta struct { Sym: "Synology", Desc: "Synology NAS integration (applies to Linux builds only)", }, + "syslog": { + Sym: "Syslog", + Desc: "tailscaled --syslog flag support to send logs to the system syslog daemon", + }, "syspolicy": {Sym: "SystemPolicy", Desc: "System policy configuration (MDM) support"}, "systray": { Sym: "SysTray", diff --git a/feature/hooks.go b/feature/hooks.go index 7611499a1..465f645fd 100644 --- a/feature/hooks.go +++ b/feature/hooks.go @@ -4,6 +4,7 @@ package feature import ( + "io" "net/http" "net/url" "os" @@ -13,6 +14,21 @@ "tailscale.com/types/persist" ) +// HookRegisterLogSinkFlags is a hook for the syslog feature to register +// its flags (such as tailscaled's --syslog) with the process's default +// flag set. If set, tailscaled calls it before flag parsing. +var HookRegisterLogSinkFlags Hook[func()] + +// HookLogSink is a hook for the syslog feature to redirect the process's +// logs to an alternate sink. If set, tailscaled calls it once early in +// main, after flag parsing; on that first call, if the user requested an +// alternate sink, it points the standard library's default logger at that +// sink. It returns the sink, or nil if logs are not being redirected. +// Later callers (such as logpolicy, which otherwise writes its console +// copy of logs to stderr) use the returned writer to send their logs to +// the same place. +var HookLogSink Hook[func() io.Writer] + // HookCanAutoUpdate is a hook for the clientupdate package // to conditionally initialize. var HookCanAutoUpdate Hook[func() bool] diff --git a/feature/syslog/syslog.go b/feature/syslog/syslog.go new file mode 100644 index 000000000..b3301beb6 --- /dev/null +++ b/feature/syslog/syslog.go @@ -0,0 +1,47 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +//go:build unix + +// Package syslog provides the tailscaled --syslog flag, which sends the +// daemon's logs to the system syslog daemon instead of stderr. +package syslog + +import ( + "flag" + "io" + "log" + "log/syslog" + "sync" + + "tailscale.com/feature" +) + +func init() { + feature.Register("syslog") + feature.HookRegisterLogSinkFlags.Set(registerFlags) + feature.HookLogSink.Set(logSink) +} + +var useSyslog bool + +func registerFlags() { + flag.BoolVar(&useSyslog, "syslog", false, "log to the system syslog daemon instead of stderr") +} + +// logSink returns the writer to which logs should be sent, pointing the +// standard library's default logger at it on first call. It returns nil if +// syslog logging was not requested or the syslog daemon is unreachable. +var logSink = sync.OnceValue(func() io.Writer { + if !useSyslog { + return nil + } + w, err := syslog.New(syslog.LOG_DAEMON|syslog.LOG_INFO, "tailscaled") + if err != nil { + log.Printf("syslog: connecting to syslog daemon failed; continuing to log to stderr: %v", err) + return nil + } + log.SetFlags(0) // syslog records its own timestamps + log.SetOutput(w) + return w +}) diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go index 63021bfc3..78b3ccc13 100644 --- a/logpolicy/logpolicy.go +++ b/logpolicy/logpolicy.go @@ -546,7 +546,18 @@ func (opts Options) init(disableLogging bool) (*logtail.Config, *Policy) { // anyway, no need to add one. lflags = 0 } - console := log.New(stderrWriter{}, "", lflags) + var conWriter io.Writer = stderrWriter{} + if buildfeatures.HasSyslog { + if f, ok := feature.HookLogSink.GetOk(); ok { + if w := f(); w != nil { + // Logs are being redirected elsewhere (e.g. to syslog, + // which records its own timestamps). + conWriter = w + lflags = 0 + } + } + } + console := log.New(conWriter, "", lflags) var earlyErrBuf bytes.Buffer earlyLogf := func(format string, a ...any) {