feature/syslog, cmd/tailscaled, logpolicy: add optional --syslog flag

Add a new modular syslog feature providing a tailscaled --syslog flag
that sends the daemon's logs to the system syslog daemon instead of
stderr, which is useful when running as a daemon without a service
manager that captures stderr (e.g. OpenWrt's procd).

The feature package registers two new hooks: one to register its flag
before flag parsing, and one that tailscaled calls early in main to
redirect the standard library's default logger. Because logpolicy later
points the default logger at logtail, whose local console copy writes
to stderr, logpolicy now also consults the hook and sends its console
copy to the same sink (with timestamps disabled, as syslog records its
own).

The feature is linked by default only on Linux, FreeBSD, and OpenBSD,
and can be removed with the ts_omit_syslog build tag. If connecting to
the syslog daemon fails at startup, tailscaled logs a warning and
continues logging to stderr.

Fixes #16270

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I8f3a92d4c1e6b70a5d29e4f61b3c874250a9de13
This commit is contained in:
Brad Fitzpatrick 2026-07-17 19:26:54 +00:00 committed by Brad Fitzpatrick
parent def265083b
commit 0433cc6929
10 changed files with 137 additions and 2 deletions

View File

@ -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+

View File

@ -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{

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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",

View File

@ -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]

47
feature/syslog/syslog.go Normal file
View File

@ -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
})

View File

@ -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) {