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
// user may specify only --statedir if they wish.
if args.statepath == "" && args.statedir == "" {
if runtime.GOOS == "plan9" {
home, err := os.UserHomeDir()
if err != nil {
log.Fatalf("failed to get home directory: %v", err)
}
args.statedir = filepath.Join(home, "tailscale-state")
if err := os.MkdirAll(args.statedir, 0700); err != nil {
log.Fatalf("failed to create state directory: %v", err)
if paths.MakeAutomaticStateDir() {
d := paths.DefaultTailscaledStateDir()
if d != "" {
args.statedir = d
if err := os.MkdirAll(d, 0700); err != nil {
log.Fatalf("failed to create state directory: %v", err)
}
}
} else {
args.statepath = paths.DefaultTailscaledStateFile()

View File

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

View File

@ -22,6 +22,7 @@ import (
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/cmpver"
"tailscale.com/version/distro"
)
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.
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{
fs: directFS{},
dbusPing: dbusPing,

View File

@ -24,6 +24,9 @@ import (
// CrateTAP is the hook set by feature/tap.
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
// the OS-dependent name that was allocated to the device.
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" {
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 {
return nil, "", err

View File

@ -17,6 +17,14 @@ import (
func init() {
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) {
@ -36,7 +44,7 @@ func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf, createErr error)
kernel := utsReleaseField(&un)
logf("Linux kernel version: %s", kernel)
modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput()
modprobeOut, err := modprobeTun()
if err == nil {
logf("'modprobe tun' successful")
// Either tun is currently loaded, or it's statically

View File

@ -6,6 +6,7 @@
package paths
import (
"log"
"os"
"path/filepath"
"runtime"
@ -70,6 +71,37 @@ func DefaultTailscaledStateFile() string {
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
// 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.

View File

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

View File

@ -23,6 +23,11 @@ func detectFirewallMode(logf logger.Logf, prefHint string) FirewallMode {
hostinfo.SetFirewallMode("nft-gokrazy")
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")
// 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.
// Any errors that occur are logged to the provided logf.
func IPTablesCleanUp(logf logger.Logf) {
if distro.Get() == distro.Gokrazy {
// Gokrazy uses nftables and doesn't have the "iptables" command.
switch distro.Get() {
case distro.Gokrazy, distro.JetKVM:
// These use nftables and don't have the "iptables" command.
// Avoid log spam on cleanup. (#12277)
return
}

View File

@ -31,6 +31,7 @@ const (
Unraid = Distro("unraid")
Alpine = Distro("alpine")
UBNT = Distro("ubnt") // Ubiquiti Networks
JetKVM = Distro("jetkvm")
)
var distro lazy.SyncValue[Distro]
@ -102,6 +103,8 @@ func linuxDistro() Distro {
return Unraid
case have("/etc/alpine-release"):
return Alpine
case haveDir("/userdata/jetkvm") && haveDir("/sys/kernel/config/usb_gadget/jetkvm"):
return JetKVM
}
return ""
}