diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b3a5e85bf..a19713c5d 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -579,6 +579,9 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func if m := b.sshOnButUnusableHealthCheckMessageLocked(); m != "" { s.Health = append(s.Health, m) } + if version.IsUnstableBuild() { + s.Health = append(s.Health, "This is an unstable (development) version of Tailscale; frequent updates and bugs are likely") + } if b.netMap != nil { s.CertDomains = append([]string(nil), b.netMap.DNS.CertDomains...) s.MagicDNSSuffix = b.netMap.MagicDNSSuffix() diff --git a/version/prop.go b/version/prop.go index 1a6b2056b..946059fed 100644 --- a/version/prop.go +++ b/version/prop.go @@ -8,7 +8,9 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" + "sync" "tailscale.com/syncs" ) @@ -74,3 +76,31 @@ func IsWindowsGUI() bool { exe = filepath.Base(exe) return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn") } + +var ( + isUnstableOnce sync.Once + isUnstableBuild bool +) + +// IsUnstableBuild reports whether this is an unstable build. +// That is, whether its minor version number is odd. +func IsUnstableBuild() bool { + isUnstableOnce.Do(initUnstable) + return isUnstableBuild +} + +func initUnstable() { + _, rest, ok := strings.Cut(Short, ".") + if !ok { + return + } + minorStr, _, ok := strings.Cut(rest, ".") + if !ok { + return + } + minor, err := strconv.Atoi(minorStr) + if err != nil { + return + } + isUnstableBuild = minor%2 == 1 +}