Compare commits

...

12 Commits

Author SHA1 Message Date
Denton Gentry c8fb4f8c79 VERSION.txt: this is v1.22.1
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
3 years ago
Joonas Kuorilehto 6562f4c6c7 cmd/tailscale: allow use of flags in gokrazy
Enable use of command line arguments with tailscale cli on gokrazy. Before
this change using arguments like "up" would cause tailscale cli to be
repeatedly restarted by gokrazy process supervisor.

We never want to have gokrazy restart tailscale cli, even if user would
manually start the process.

Expected usage is that user creates files:

flags/tailscale.com/cmd/tailscale/flags.txt:

    up

flags/tailscale.com/cmd/tailscaled/flags.txt:

    --statedir=/perm/tailscaled/
    --tun=userspace-networking

Then tailscale prints URL for user to log in with browser.

Alternatively it should be possible to use up with auth key to allow
unattended gokrazy installs.

Signed-off-by: Joonas Kuorilehto <joneskoo@derbian.fi>
(cherry picked from commit c1b3500a05)
3 years ago
Brad Fitzpatrick 9fd9abfd3f net/interfaces: add FreeBSD default route lookup (portmapping, etc)
Updates #4101 (probably fixes)

Change-Id: I2b75ee3ced276fb7b211f17c382621cf1ef882fa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 61cdcf4082)
3 years ago
Maisem Ali 24319e840d net/socks5: always close client connections after serving
Customer reported an issue where the connections were not closing, and
would instead just stay open. This commit makes it so that we close out
the connection regardless of what error we see. I've verified locally
that it fixes the issue, we should add a test for this.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 2fb087891b)
3 years ago
Robert Fritzsche 0a399bb6c6 tstime/mono: fix Before function comment
Signed-off-by: Robert Fritzsche <r.fritzsche@gridx.de>
(cherry picked from commit 0e62a7d1a2)
3 years ago
Josh Bleecher Snyder 0b5b3287cb go.toolchain.rev: bump to Go 1.17.8
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
(cherry picked from commit 7ddf2e2fea)
3 years ago
Brad Fitzpatrick dee0b7f8b8 cmd/tailscale: tell gokrazy to not manage the CLI as a daemon
In the future we'll probably want to run the "tailscale web"
server instead, but for now stop the infinite restart loop.

See https://gokrazy.org/userguide/process-interface/ for details.

Updates #1866

Change-Id: I4133a5fdb859b848813972620495865727fe397a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit f18bb6397b)
3 years ago
Brad Fitzpatrick 231f8b74e4 cmd/tailscaled: default to userspace-networking mode on gokrazy, set paths
One of the current few steps to run Tailscale on gokrazy is to
specify the --tun=userspace-networking flag:

    https://gokrazy.org/userguide/install/tailscale/

Instead, make it the default for now. Later we can change the
default to kernel mode if available and fall back to userspace
mode like Synology, once #391 is done.

Likewise, set default paths for Gokrazy, as its filesystem hierarchy
is not the Linux standard one. Instead, use the conventional paths as
documented at https://gokrazy.org/userguide/install/tailscale/.

Updates #1866

RELNOTE=default to userspace-networking mode on gokrazy

Change-Id: I3766159a294738597b4b30629d2860312dbb7609
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit db85384f9c)
3 years ago
Brad Fitzpatrick d0698cfcec version, hostinfo: recognize gokrazy as a distro
Now:

/tmp/breakglass3929186798 # /user/tailscale debug hostinfo
{
  "IPNVersion": "1.23.0-date.20220107",
  "OS": "linux",
  "OSVersion": "Gokrazy; kernel=5.16.11",
  "DeviceModel": "Raspberry Pi 4 Model B Rev 1.2",
  "Hostname": "gokrazy",
  "GoArch": "arm64"
}

Also, cache the distro lookup. It doesn't change while the program is
running:

name   old time/op    new time/op    delta
Get-6    5.21µs ± 5%    0.00µs ± 3%   -99.91%  (p=0.008 n=5+5)

name   old alloc/op   new alloc/op   delta
Get-6      792B ± 0%        0B       -100.00%  (p=0.008 n=5+5)

name   old allocs/op  new allocs/op  delta
Get-6      8.00 ± 0%      0.00       -100.00%  (p=0.008 n=5+5)

Updates #1866

Change-Id: Ifb9a63b94287010d3f4c8bfeb6b78119e8a9b203
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 58a6c9b2b8)
3 years ago
Brad Fitzpatrick bfd7f9d318 tstime/rate: deflake TestLongRunningQPS even more
Previous de-flakings:
* 8cf1af8a07 for #3733
* 30458c71c8 for #2727

Fixes #4044

Change-Id: I506cf1ff37bb224f5a9929f1998901e60b24535d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 6a2e94cbeb)
3 years ago
Brad Fitzpatrick fca3592c1c net/interfaces: get Linux default route from netlink as fallback
If it's in a non-standard table, as it is on Unifi UDM Pro, apparently.

Updates #4038 (probably fixes, but don't have hardware to verify)

Change-Id: I2cb9a098d8bb07d1a97a6045b686aca31763a937
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 55095df644)
3 years ago
Denton Gentry 4e0b00ad83 VERSION.txt: this is v1.22.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
3 years ago

@ -1 +1 @@
1.21.0
1.22.1

@ -125,10 +125,22 @@ func CleanUpArgs(args []string) []string {
}
// Run runs the CLI. The args do not include the binary name.
func Run(args []string) error {
func Run(args []string) (err error) {
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
args = []string{"version"}
}
if runtime.GOOS == "linux" && distro.Get() == distro.Gokrazy &&
os.Getenv("GOKRAZY_FIRST_START") == "1" {
defer func() {
// Exit with 125 otherwise the CLI binary is restarted
// forever in a loop by the Gokrazy process supervisor.
// See https://gokrazy.org/userguide/process-interface/
if err != nil {
log.Println(err)
}
os.Exit(125)
}()
}
var warnOnce sync.Once
tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
@ -194,7 +206,7 @@ change in the future.
}
})
err := rootCmd.Run(context.Background())
err = rootCmd.Run(context.Background())
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
}

@ -4,8 +4,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
L github.com/klauspost/compress/flate from nhooyr.io/websocket
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
@ -97,6 +103,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http+
golang.org/x/net/http/httpproxy from net/http

@ -74,7 +74,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress from github.com/klauspost/compress/zstd
L github.com/klauspost/compress/flate from nhooyr.io/websocket

@ -68,11 +68,27 @@ func defaultTunName() string {
// as a magic value that uses/creates any free number.
return "utun"
case "linux":
if distro.Get() == distro.Synology {
switch distro.Get() {
case distro.Synology:
// Try TUN, but fall back to userspace networking if needed.
// See https://github.com/tailscale/tailscale-synology/issues/35
return "tailscale0,userspace-networking"
case distro.Gokrazy:
// Gokrazy doesn't yet work in tun mode because the whole
// Gokrazy thing is no C code, and Tailscale currently
// depends on the iptables binary for Linux's
// wgengine/router.
// But on Gokrazy there's no legacy iptables, so we could use netlink
// to program nft-iptables directly. It just isn't done yet;
// see https://github.com/tailscale/tailscale/issues/391
//
// But Gokrazy does have the tun module built-in, so users
// can stil run --tun=tailscale0 if they wish, if they
// arrange for iptables to be present or run in "tailscale
// up --netfilter-mode=off" mode, perhaps. Untested.
return "userspace-networking"
}
}
return "tailscale0"
}

@ -1 +1 @@
fd6b85b80f5fbbfaddef34e7ab6ac83384c50e4d
dce70b6d327c7a30b81701f4cc134b56c4e6c229

@ -113,6 +113,8 @@ func osVersionLinux() string {
return fmt.Sprintf("Synology %s%s", m["productversion"], attr)
case distro.OpenWrt:
return fmt.Sprintf("OpenWrt %s%s", m["DISTRIB_RELEASE"], attr)
case distro.Gokrazy:
return fmt.Sprintf("Gokrazy%s", attr)
}
return fmt.Sprintf("Other%s", attr)
}

@ -687,7 +687,8 @@ func netInterfaces() ([]Interface, error) {
return ret, nil
}
// DefaultRouteDetails are the
// DefaultRouteDetails are the details about a default route returned
// by DefaultRoute.
type DefaultRouteDetails struct {
// InterfaceName is the interface name. It must always be populated.
// It's like "eth0" (Linux), "Ethernet 2" (Windows), "en0" (macOS).

@ -0,0 +1,136 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This might work on other BSDs, but only tested on FreeBSD.
// Originally a fork of interfaces_darwin.go with slightly different flags.
//go:build freebsd
// +build freebsd
package interfaces
import (
"errors"
"fmt"
"log"
"net"
"syscall"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"inet.af/netaddr"
)
func defaultRoute() (d DefaultRouteDetails, err error) {
idx, err := DefaultRouteInterfaceIndex()
if err != nil {
return d, err
}
iface, err := net.InterfaceByIndex(idx)
if err != nil {
return d, err
}
d.InterfaceName = iface.Name
d.InterfaceIndex = idx
return d, nil
}
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
func fetchRoutingTable() (rib []byte, err error) {
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
}
func DefaultRouteInterfaceIndex() (int, error) {
// $ netstat -nr
// Routing tables
// Internet:
// Destination Gateway Flags Netif Expire
// default 10.0.0.1 UGSc en0 <-- want this one
// default 10.0.0.1 UGScI en1
// From man netstat:
// U RTF_UP Route usable
// G RTF_GATEWAY Destination requires forwarding by intermediary
// S RTF_STATIC Manually added
// c RTF_PRCLONING Protocol-specified generate new routes on use
// I RTF_IFSCOPE Route is associated with an interface scope
rib, err := fetchRoutingTable()
if err != nil {
return 0, fmt.Errorf("route.FetchRIB: %w", err)
}
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
if err != nil {
return 0, fmt.Errorf("route.ParseRIB: %w", err)
}
indexSeen := map[int]int{} // index => count
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
indexSeen[rm.Index]++
}
if len(indexSeen) == 0 {
return 0, errors.New("no gateway index found")
}
if len(indexSeen) == 1 {
for idx := range indexSeen {
return idx, nil
}
}
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
}
func init() {
likelyHomeRouterIP = likelyHomeRouterIPBSDFetchRIB
}
func likelyHomeRouterIPBSDFetchRIB() (ret netaddr.IP, ok bool) {
rib, err := fetchRoutingTable()
if err != nil {
log.Printf("routerIP/FetchRIB: %v", err)
return ret, false
}
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
if err != nil {
log.Printf("routerIP/ParseRIB: %v", err)
return ret, false
}
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_IFSCOPE = 0x1000000
if rm.Flags&unix.RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
if len(rm.Addrs) > unix.RTAX_GATEWAY {
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
// Expect 0.0.0.0 as DST field.
continue
}
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
if !ok {
continue
}
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
}
}
return ret, false
}

@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !linux && !windows && !darwin
// +build !linux,!windows,!darwin
//go:build !linux && !windows && !darwin && !freebsd
// +build !linux,!windows,!darwin,!freebsd
package interfaces

@ -11,12 +11,16 @@ import (
"fmt"
"io"
"log"
"net"
"os"
"os/exec"
"runtime"
"strings"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
"go4.org/mem"
"golang.org/x/sys/unix"
"inet.af/netaddr"
"tailscale.com/syncs"
"tailscale.com/util/lineread"
@ -70,9 +74,7 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
if err != nil {
return nil // ignore error, skip line and keep going
}
const RTF_UP = 0x0001
const RTF_GATEWAY = 0x0002
if flags&(RTF_UP|RTF_GATEWAY) != RTF_UP|RTF_GATEWAY {
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
return nil
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
@ -145,7 +147,62 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
d.InterfaceName = v
return d, err
}
return d, err
// Issue 4038: the default route (such as on Unifi UDM Pro)
// might be in a non-default table, so it won't show up in
// /proc/net/route. Use netlink to find the default route.
//
// TODO(bradfitz): this allocates a fair bit. We should track
// this in wgengine/monitor instead and have
// interfaces.GetState take a link monitor or similar so the
// routing table can be cached and the monitor's existing
// subscription to route changes can update the cached state,
// rather than querying the whole thing every time like
// defaultRouteFromNetlink does.
//
// Then we should just always try to use the cached route
// table from netlink every time, and only use /proc/net/route
// as a fallback for weird environments where netlink might be
// banned but /proc/net/route is emulated (e.g. stuff like
// Cloud Run?).
return defaultRouteFromNetlink()
}
func defaultRouteFromNetlink() (d DefaultRouteDetails, err error) {
c, err := rtnetlink.Dial(&netlink.Config{Strict: true})
if err != nil {
return d, fmt.Errorf("defaultRouteFromNetlink: Dial: %w", err)
}
defer c.Close()
rms, err := c.Route.List()
if err != nil {
return d, fmt.Errorf("defaultRouteFromNetlink: List: %w", err)
}
for _, rm := range rms {
if rm.Attributes.Gateway == nil {
// A default route has a gateway. If it doesn't, skip it.
continue
}
if rm.Attributes.Dst != nil {
// A default route has a nil destination to mean anything
// so ignore any route for a specific destination.
// TODO(bradfitz): better heuristic?
// empirically this seems like enough.
continue
}
// TODO(bradfitz): care about address family, if
// callers ever start caring about v4-vs-v6 default
// route differences.
idx := int(rm.Attributes.OutIface)
if idx == 0 {
continue
}
if iface, err := net.InterfaceByIndex(idx); err == nil {
d.InterfaceName = iface.Name
d.InterfaceIndex = idx
return d, nil
}
}
return d, errNoDefaultRoute
}
var zeroRouteBytes = []byte("00000000")
@ -155,6 +212,8 @@ var procNetRoutePath = "/proc/net/route"
// /proc/net/route looking for a default route.
const maxProcNetRouteRead = 1000
var errNoDefaultRoute = errors.New("no default route found")
func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
f, err := os.Open(procNetRoutePath)
if err != nil {
@ -168,7 +227,7 @@ func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
lineNum++
line, err := br.ReadSlice('\n')
if err == io.EOF || lineNum > maxProcNetRouteRead {
return "", fmt.Errorf("no default routes found: %w", err)
return "", errNoDefaultRoute
}
if err != nil {
return "", err

@ -5,7 +5,9 @@
package interfaces
import (
"errors"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
@ -107,3 +109,14 @@ func BenchmarkDefaultRouteInterface(b *testing.B) {
}
}
}
func TestRouteLinuxNetlink(t *testing.T) {
d, err := defaultRouteFromNetlink()
if errors.Is(err, fs.ErrPermission) {
t.Skip(err)
}
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %+v", d)
}

@ -112,11 +112,11 @@ func (s *Server) Serve(l net.Listener) error {
return err
}
go func() {
defer c.Close()
conn := &Conn{clientConn: c, srv: s}
err := conn.Run()
if err != nil {
s.logf("client connection failed: %v", err)
conn.clientConn.Close()
}
}()
}

@ -28,7 +28,8 @@ func DefaultTailscaledSocket() string {
if runtime.GOOS == "darwin" {
return "/var/run/tailscaled.socket"
}
if distro.Get() == distro.Synology {
switch distro.Get() {
case distro.Synology:
// TODO(maisem): be smarter about this. We can parse /etc/VERSION.
const dsm6Sock = "/var/packages/Tailscale/etc/tailscaled.sock"
const dsm7Sock = "/var/packages/Tailscale/var/tailscaled.sock"
@ -38,6 +39,8 @@ func DefaultTailscaledSocket() string {
if fi, err := os.Stat(dsm7Sock); err == nil && !fi.IsDir() {
return dsm7Sock
}
case distro.Gokrazy:
return "/perm/tailscaled/tailscaled.sock"
}
if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() {
return "/var/run/tailscale/tailscaled.sock"

@ -14,6 +14,7 @@ import (
"runtime"
"golang.org/x/sys/unix"
"tailscale.com/version/distro"
)
func init() {
@ -34,6 +35,9 @@ func statePath() string {
}
func stateFileUnix() string {
if distro.Get() == distro.Gokrazy {
return "/perm/tailscaled/tailscaled.state"
}
path := statePath()
if path == "" {
return ""

@ -53,7 +53,7 @@ func (t Time) After(n Time) bool {
return t > n
}
// After reports t < n, whether t is before n.
// Before reports t < n, whether t is before n.
func (t Time) Before(n Time) bool {
return t < n
}

@ -16,7 +16,6 @@ package rate
import (
"context"
"math"
"runtime"
"sync"
"sync/atomic"
"testing"
@ -155,61 +154,6 @@ func TestSimultaneousRequests(t *testing.T) {
}
}
func TestLongRunningQPS(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
if runtime.GOOS == "openbsd" {
t.Skip("low resolution time.Sleep invalidates test (golang.org/issue/14183)")
return
}
// The test runs for a few seconds executing many requests and then checks
// that overall number of requests is reasonable.
const (
limit = 100
burst = 100
)
var numOK = int32(0)
lim := NewLimiter(limit, burst)
var wg sync.WaitGroup
f := func() {
if ok := lim.Allow(); ok {
atomic.AddInt32(&numOK, 1)
}
wg.Done()
}
// This will still offer ~500 requests per second,
// but won't consume outrageous amount of CPU.
start := time.Now()
end := start.Add(1 * time.Second)
ticker := time.NewTicker(2 * time.Millisecond)
defer ticker.Stop()
for now := range ticker.C {
if now.After(end) {
break
}
wg.Add(1)
go f()
}
wg.Wait()
elapsed := time.Since(start)
ideal := burst + (limit * float64(elapsed) / float64(time.Second))
// We should never get more requests than allowed.
if want := int32(ideal + 1); numOK > want {
t.Errorf("numOK = %d, want %d (ideal %f)", numOK, want, ideal)
}
// We should get close-ish to the number of requests allowed.
// Trying to get too close causes flakes. Treat this as a sanity check.
if want := int32(0.9 * ideal); numOK < want {
t.Errorf("numOK = %d, want %d (ideal %f)", numOK, want, ideal)
}
}
type request struct {
t time.Time
n int

@ -8,6 +8,7 @@ package distro
import (
"os"
"runtime"
"sync/atomic"
)
type Distro string
@ -22,17 +23,25 @@ const (
Pfsense = Distro("pfsense")
OPNsense = Distro("opnsense")
TrueNAS = Distro("truenas")
Gokrazy = Distro("gokrazy")
)
var distroAtomic atomic.Value // of Distro
// Get returns the current distro, or the empty string if unknown.
func Get() Distro {
if runtime.GOOS == "linux" {
return linuxDistro()
d, ok := distroAtomic.Load().(Distro)
if ok {
return d
}
if runtime.GOOS == "freebsd" {
return freebsdDistro()
switch runtime.GOOS {
case "linux":
d = linuxDistro()
case "freebsd":
d = freebsdDistro()
}
return ""
distroAtomic.Store(d) // even if empty
return d
}
func have(file string) bool {
@ -62,6 +71,8 @@ func linuxDistro() Distro {
return NixOS
case have("/etc/config/uLinux.conf"):
return QNAP
case haveDir("/gokrazy"):
return Gokrazy
}
return ""
}

@ -0,0 +1,16 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package distro
import "testing"
func BenchmarkGet(b *testing.B) {
b.ReportAllocs()
var d Distro
for i := 0; i < b.N; i++ {
d = Get()
}
_ = d
}
Loading…
Cancel
Save