clientupdate: best-effort restart of tailscaled on init.d systems (#18568)

Not all Linux distros use systemd yet, for example GL.iNet KVM devices
use busybox's init, which is similar to SysV init.
This is a best-effort restart attempt after the update, it probably
won't cover 100% of init.d setups out there.

Fixes #18567

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
Andrew Lytvynov
2026-02-03 12:57:05 -08:00
committed by GitHub
parent 7b96c4c23e
commit 54d70c8312
+51 -7
View File
@@ -11,11 +11,11 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"maps" "maps"
"net/http" "net/http"
"os" "os"
@@ -27,6 +27,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"tailscale.com/envknob"
"tailscale.com/feature" "tailscale.com/feature"
"tailscale.com/hostinfo" "tailscale.com/hostinfo"
"tailscale.com/types/lazy" "tailscale.com/types/lazy"
@@ -288,6 +289,10 @@ func Update(args Arguments) error {
} }
func (up *Updater) confirm(ver string) bool { func (up *Updater) confirm(ver string) bool {
if envknob.Bool("TS_UPDATE_SKIP_VERSION_CHECK") {
up.Logf("current version: %v, latest version %v; forcing an update due to TS_UPDATE_SKIP_VERSION_CHECK", up.currentVersion, ver)
return true
}
// Only check version when we're not switching tracks. // Only check version when we're not switching tracks.
if up.Track == "" || up.Track == CurrentTrack { if up.Track == "" || up.Track == CurrentTrack {
switch c := cmpver.Compare(up.currentVersion, ver); { switch c := cmpver.Compare(up.currentVersion, ver); {
@@ -865,12 +870,17 @@ func (up *Updater) updateLinuxBinary() error {
if err := os.Remove(dlPath); err != nil { if err := os.Remove(dlPath); err != nil {
up.Logf("failed to clean up %q: %v", dlPath, err) up.Logf("failed to clean up %q: %v", dlPath, err)
} }
if err := restartSystemdUnit(context.Background()); err != nil {
err = restartSystemdUnit(up.Logf)
if errors.Is(err, errors.ErrUnsupported) {
err = restartInitD()
if errors.Is(err, errors.ErrUnsupported) { if errors.Is(err, errors.ErrUnsupported) {
up.Logf("Tailscale binaries updated successfully.\nPlease restart tailscaled to finish the update.") err = errors.New("tailscaled is not running under systemd or init.d")
} else {
up.Logf("Tailscale binaries updated successfully, but failed to restart tailscaled: %s.\nPlease restart tailscaled to finish the update.", err)
} }
}
if err != nil {
up.Logf("Tailscale binaries updated successfully, but failed to restart tailscaled: %s.\nPlease restart tailscaled to finish the update.", err)
} else { } else {
up.Logf("Success") up.Logf("Success")
} }
@@ -878,13 +888,13 @@ func (up *Updater) updateLinuxBinary() error {
return nil return nil
} }
func restartSystemdUnit(ctx context.Context) error { func restartSystemdUnit(logf logger.Logf) error {
if _, err := exec.LookPath("systemctl"); err != nil { if _, err := exec.LookPath("systemctl"); err != nil {
// Likely not a systemd-managed distro. // Likely not a systemd-managed distro.
return errors.ErrUnsupported return errors.ErrUnsupported
} }
if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil { if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
return fmt.Errorf("systemctl daemon-reload failed: %w\noutput: %s", err, out) logf("systemctl daemon-reload failed: %w\noutput: %s", err, out)
} }
if out, err := exec.Command("systemctl", "restart", "tailscaled.service").CombinedOutput(); err != nil { if out, err := exec.Command("systemctl", "restart", "tailscaled.service").CombinedOutput(); err != nil {
return fmt.Errorf("systemctl restart failed: %w\noutput: %s", err, out) return fmt.Errorf("systemctl restart failed: %w\noutput: %s", err, out)
@@ -892,6 +902,40 @@ func restartSystemdUnit(ctx context.Context) error {
return nil return nil
} }
// restartInitD attempts best-effort restart of tailscale on init.d systems
// (for example, GL.iNet KVM devices running busybox). It returns
// errors.ErrUnsupported if the expected service script is not found.
//
// There's probably a million variations of init.d configs out there, and this
// function does not intend to support all of them.
func restartInitD() error {
const initDir = "/etc/init.d/"
files, err := os.ReadDir(initDir)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return errors.ErrUnsupported
}
return err
}
for _, f := range files {
// Skip anything other than regular files.
if !f.Type().IsRegular() {
continue
}
// The script will be called something like /etc/init.d/tailscale or
// /etc/init.d/S99tailscale.
if n := f.Name(); strings.HasSuffix(n, "tailscale") {
path := filepath.Join(initDir, n)
if out, err := exec.Command(path, "restart").CombinedOutput(); err != nil {
return fmt.Errorf("%q failed: %w\noutput: %s", path+" restart", err, out)
}
return nil
}
}
// Init script for tailscale not found.
return errors.ErrUnsupported
}
func (up *Updater) downloadLinuxTarball(ver string) (string, error) { func (up *Updater) downloadLinuxTarball(ver string) (string, error) {
dlDir, err := os.UserCacheDir() dlDir, err := os.UserCacheDir()
if err != nil { if err != nil {