cmd/tailscale/cli: add "configure jetkvm" subcommand

To write the init script.

And fix the JetKVM detection to work during early boot while the filesystem
and modules are still being loaded; it wasn't being detected on early boot
and then tailscaled was failing to start because it didn't know it was on JetKVM
and didn't modprobe tun.

Updates #16524

Change-Id: I0524ca3abd7ace68a69af96aab4175d32c07e116
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-07-11 08:51:02 -07:00 committed by Brad Fitzpatrick
parent 04e8d21b0b
commit 30da2e1c32
3 changed files with 94 additions and 1 deletions

View File

@ -0,0 +1,81 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android && arm
package cli
import (
"bytes"
"context"
"errors"
"flag"
"os"
"runtime"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/version/distro"
)
func init() {
maybeJetKVMConfigureCmd = jetKVMConfigureCmd
}
func jetKVMConfigureCmd() *ffcli.Command {
if runtime.GOOS != "linux" || distro.Get() != distro.JetKVM {
return nil
}
return &ffcli.Command{
Name: "jetkvm",
Exec: runConfigureJetKVM,
ShortUsage: "tailscale configure jetkvm",
ShortHelp: "Configure JetKVM to run tailscaled at boot",
LongHelp: strings.TrimSpace(`
This command configures the JetKVM host to run tailscaled at boot.
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("jetkvm")
return fs
})(),
}
}
func runConfigureJetKVM(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
if runtime.GOOS != "linux" || distro.Get() != distro.JetKVM {
return errors.New("only implemented on JetKVM")
}
err := os.WriteFile("/etc/init.d/S22tailscale", bytes.TrimLeft([]byte(`
#!/bin/sh
# /etc/init.d/S22tailscale
# Start/stop tailscaled
case "$1" in
start)
/userdata/tailscale/tailscaled > /dev/null 2>&1 &
;;
stop)
killall tailscaled
;;
*)
echo "Usage: $0 {start|stop}"
exit 1
;;
esac
`), "\n"), 0755)
if err != nil {
return err
}
if err := os.Symlink("/userdata/tailscale/tailscale", "/bin/tailscale"); err != nil {
if !os.IsExist(err) {
return err
}
}
printf("Done. Now restart your JetKVM.\n")
return nil
}

View File

@ -10,6 +10,8 @@
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
) )
var maybeJetKVMConfigureCmd func() *ffcli.Command // non-nil only on Linux/arm for JetKVM
func configureCmd() *ffcli.Command { func configureCmd() *ffcli.Command {
return &ffcli.Command{ return &ffcli.Command{
Name: "configure", Name: "configure",
@ -29,6 +31,7 @@ func configureCmd() *ffcli.Command {
synologyConfigureCertCmd(), synologyConfigureCertCmd(),
ccall(maybeSysExtCmd), ccall(maybeSysExtCmd),
ccall(maybeVPNConfigCmd), ccall(maybeVPNConfigCmd),
ccall(maybeJetKVMConfigureCmd),
), ),
} }
} }

View File

@ -9,6 +9,7 @@
"os" "os"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"tailscale.com/types/lazy" "tailscale.com/types/lazy"
"tailscale.com/util/lineiter" "tailscale.com/util/lineiter"
@ -103,12 +104,20 @@ func linuxDistro() Distro {
return Unraid return Unraid
case have("/etc/alpine-release"): case have("/etc/alpine-release"):
return Alpine return Alpine
case haveDir("/userdata/jetkvm") && haveDir("/sys/kernel/config/usb_gadget/jetkvm"): case runtime.GOARCH == "arm" && isDeviceModel("JetKVM"):
return JetKVM return JetKVM
} }
return "" return ""
} }
func isDeviceModel(want string) bool {
if runtime.GOOS != "linux" {
return false
}
v, _ := os.ReadFile("/sys/firmware/devicetree/base/model")
return want == strings.Trim(string(v), "\x00\r\n\t ")
}
func freebsdDistro() Distro { func freebsdDistro() Distro {
switch { switch {
case have("/etc/pfSense-rc"): case have("/etc/pfSense-rc"):