all: detect JetKVM and specialize a handful of things for it

Updates #16524

Change-Id: I183428de8c65d7155d82979d2d33f031c22e3331
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-07-10 11:14:08 -07:00 committed by Brad Fitzpatrick
parent bebc796e6c
commit fbc6a9ec5a
10 changed files with 89 additions and 12 deletions

View File

@ -257,14 +257,13 @@ func main() {
// Only apply a default statepath when neither have been provided, so that a // Only apply a default statepath when neither have been provided, so that a
// user may specify only --statedir if they wish. // user may specify only --statedir if they wish.
if args.statepath == "" && args.statedir == "" { if args.statepath == "" && args.statedir == "" {
if runtime.GOOS == "plan9" { if paths.MakeAutomaticStateDir() {
home, err := os.UserHomeDir() d := paths.DefaultTailscaledStateDir()
if err != nil { if d != "" {
log.Fatalf("failed to get home directory: %v", err) args.statedir = d
} if err := os.MkdirAll(d, 0700); err != nil {
args.statedir = filepath.Join(home, "tailscale-state") log.Fatalf("failed to create state directory: %v", err)
if err := os.MkdirAll(args.statedir, 0700); err != nil { }
log.Fatalf("failed to create state directory: %v", err)
} }
} else { } else {
args.statepath = paths.DefaultTailscaledStateFile() args.statepath = paths.DefaultTailscaledStateFile()

View File

@ -224,6 +224,9 @@ func LogsDir(logf logger.Logf) string {
logf("logpolicy: using LocalAppData dir %v", dir) logf("logpolicy: using LocalAppData dir %v", dir)
return dir return dir
case "linux": case "linux":
if distro.Get() == distro.JetKVM {
return "/userdata/tailscale/var"
}
// STATE_DIRECTORY is set by systemd 240+ but we support older // STATE_DIRECTORY is set by systemd 240+ but we support older
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237. // systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
systemdStateDir := os.Getenv("STATE_DIRECTORY") systemdStateDir := os.Getenv("STATE_DIRECTORY")

View File

@ -22,6 +22,7 @@ import (
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/cmpver" "tailscale.com/util/cmpver"
"tailscale.com/version/distro"
) )
type kv struct { type kv struct {
@ -38,6 +39,10 @@ var publishOnce sync.Once
// //
// The health tracker may be nil; the knobs may be nil and are ignored on this platform. // The health tracker may be nil; the knobs may be nil and are ignored on this platform.
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, interfaceName string) (ret OSConfigurator, err error) { func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, interfaceName string) (ret OSConfigurator, err error) {
if distro.Get() == distro.JetKVM {
return NewNoopManager()
}
env := newOSConfigEnv{ env := newOSConfigEnv{
fs: directFS{}, fs: directFS{},
dbusPing: dbusPing, dbusPing: dbusPing,

View File

@ -24,6 +24,9 @@ import (
// CrateTAP is the hook set by feature/tap. // CrateTAP is the hook set by feature/tap.
var CreateTAP feature.Hook[func(logf logger.Logf, tapName, bridgeName string) (tun.Device, error)] var CreateTAP feature.Hook[func(logf logger.Logf, tapName, bridgeName string) (tun.Device, error)]
// modprobeTunHook is a Linux-specific hook to run "/sbin/modprobe tun".
var modprobeTunHook feature.Hook[func() error]
// New returns a tun.Device for the requested device name, along with // New returns a tun.Device for the requested device name, along with
// the OS-dependent name that was allocated to the device. // the OS-dependent name that was allocated to the device.
func New(logf logger.Logf, tunName string) (tun.Device, string, error) { func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
@ -51,7 +54,22 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
if runtime.GOOS == "plan9" { if runtime.GOOS == "plan9" {
cleanUpPlan9Interfaces() cleanUpPlan9Interfaces()
} }
dev, err = tun.CreateTUN(tunName, int(DefaultTUNMTU())) // Try to create the TUN device up to two times. If it fails
// the first time and we're on Linux, try a desperate
// "modprobe tun" to load the tun module and try again.
for try := range 2 {
dev, err = tun.CreateTUN(tunName, int(DefaultTUNMTU()))
if err == nil || !modprobeTunHook.IsSet() {
if try > 0 {
logf("created TUN device %q after doing `modprobe tun`", tunName)
}
break
}
if modprobeTunHook.Get()() != nil {
// modprobe failed; no point trying again.
break
}
}
} }
if err != nil { if err != nil {
return nil, "", err return nil, "", err

View File

@ -17,6 +17,14 @@ import (
func init() { func init() {
tunDiagnoseFailure = diagnoseLinuxTUNFailure tunDiagnoseFailure = diagnoseLinuxTUNFailure
modprobeTunHook.Set(func() error {
_, err := modprobeTun()
return err
})
}
func modprobeTun() ([]byte, error) {
return exec.Command("/sbin/modprobe", "tun").CombinedOutput()
} }
func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf, createErr error) { func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf, createErr error) {
@ -36,7 +44,7 @@ func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf, createErr error)
kernel := utsReleaseField(&un) kernel := utsReleaseField(&un)
logf("Linux kernel version: %s", kernel) logf("Linux kernel version: %s", kernel)
modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput() modprobeOut, err := modprobeTun()
if err == nil { if err == nil {
logf("'modprobe tun' successful") logf("'modprobe tun' successful")
// Either tun is currently loaded, or it's statically // Either tun is currently loaded, or it's statically

View File

@ -6,6 +6,7 @@
package paths package paths
import ( import (
"log"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -70,6 +71,37 @@ func DefaultTailscaledStateFile() string {
return "" return ""
} }
// DefaultTailscaledStateDir returns the default state directory
// to use for tailscaled, for use when the user provided neither
// a state directory or state file path to use.
//
// It returns the empty string if there's no reasonable default.
func DefaultTailscaledStateDir() string {
if runtime.GOOS == "plan9" {
home, err := os.UserHomeDir()
if err != nil {
log.Fatalf("failed to get home directory: %v", err)
}
return filepath.Join(home, "tailscale-state")
}
return filepath.Dir(DefaultTailscaledStateFile())
}
// MakeAutomaticStateDir reports whether the platform
// automatically creates the state directory for tailscaled
// when it's absent.
func MakeAutomaticStateDir() bool {
switch runtime.GOOS {
case "plan9":
return true
case "linux":
if distro.Get() == distro.JetKVM {
return true
}
}
return false
}
// MkStateDir ensures that dirPath, the daemon's configuration directory // MkStateDir ensures that dirPath, the daemon's configuration directory
// containing machine keys etc, both exists and has the correct permissions. // containing machine keys etc, both exists and has the correct permissions.
// We want it to only be accessible to the user the daemon is running under. // We want it to only be accessible to the user the daemon is running under.

View File

@ -21,6 +21,9 @@ func init() {
} }
func statePath() string { func statePath() string {
if runtime.GOOS == "linux" && distro.Get() == distro.JetKVM {
return "/userdata/tailscale/var/tailscaled.state"
}
switch runtime.GOOS { switch runtime.GOOS {
case "linux", "illumos", "solaris": case "linux", "illumos", "solaris":
return "/var/lib/tailscale/tailscaled.state" return "/var/lib/tailscale/tailscaled.state"

View File

@ -23,6 +23,11 @@ func detectFirewallMode(logf logger.Logf, prefHint string) FirewallMode {
hostinfo.SetFirewallMode("nft-gokrazy") hostinfo.SetFirewallMode("nft-gokrazy")
return FirewallModeNfTables return FirewallModeNfTables
} }
if distro.Get() == distro.JetKVM {
// JetKVM doesn't have iptables.
hostinfo.SetFirewallMode("nft-jetkvm")
return FirewallModeNfTables
}
mode := envknob.String("TS_DEBUG_FIREWALL_MODE") mode := envknob.String("TS_DEBUG_FIREWALL_MODE")
// If the envknob isn't set, fall back to the pref suggested by c2n or // If the envknob isn't set, fall back to the pref suggested by c2n or

View File

@ -688,8 +688,9 @@ func (i *iptablesRunner) DelMagicsockPortRule(port uint16, network string) error
// IPTablesCleanUp removes all Tailscale added iptables rules. // IPTablesCleanUp removes all Tailscale added iptables rules.
// Any errors that occur are logged to the provided logf. // Any errors that occur are logged to the provided logf.
func IPTablesCleanUp(logf logger.Logf) { func IPTablesCleanUp(logf logger.Logf) {
if distro.Get() == distro.Gokrazy { switch distro.Get() {
// Gokrazy uses nftables and doesn't have the "iptables" command. case distro.Gokrazy, distro.JetKVM:
// These use nftables and don't have the "iptables" command.
// Avoid log spam on cleanup. (#12277) // Avoid log spam on cleanup. (#12277)
return return
} }

View File

@ -31,6 +31,7 @@ const (
Unraid = Distro("unraid") Unraid = Distro("unraid")
Alpine = Distro("alpine") Alpine = Distro("alpine")
UBNT = Distro("ubnt") // Ubiquiti Networks UBNT = Distro("ubnt") // Ubiquiti Networks
JetKVM = Distro("jetkvm")
) )
var distro lazy.SyncValue[Distro] var distro lazy.SyncValue[Distro]
@ -102,6 +103,8 @@ 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"):
return JetKVM
} }
return "" return ""
} }