diff --git a/cmd/tailscale/cli/web.go b/cmd/tailscale/cli/web.go index 38fb737e5..99944ce3b 100644 --- a/cmd/tailscale/cli/web.go +++ b/cmd/tailscale/cli/web.go @@ -5,6 +5,7 @@ package cli import ( + "bufio" "bytes" "context" _ "embed" @@ -15,11 +16,13 @@ "log" "net/http" "net/http/cgi" + "os" "os/exec" "runtime" "strings" "github.com/peterbourgon/ff/v2/ffcli" + "go4.org/mem" "tailscale.com/client/tailscale" "tailscale.com/ipn" "tailscale.com/tailcfg" @@ -82,17 +85,63 @@ func runWeb(ctx context.Context, args []string) error { return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler)) } -func auth() (string, error) { +// authorize checks whether the provided user has access to the web UI. +func authorize(name string) error { if distro.Get() == distro.Synology { - cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi") - out, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("auth: %v: %s", err, out) - } - return string(out), nil + return authorizeSynology(name) } + return nil +} - return "", nil +// authorizeSynology checks whether the provided user has access to the web UI +// by consulting the membership of the "administrators" group. +func authorizeSynology(name string) error { + f, err := os.Open("/etc/group") + if err != nil { + return err + } + defer f.Close() + s := bufio.NewScanner(f) + var agLine string + for s.Scan() { + if !mem.HasPrefix(mem.B(s.Bytes()), mem.S("administrators:")) { + continue + } + agLine = s.Text() + break + } + if err := s.Err(); err != nil { + return err + } + if agLine == "" { + return fmt.Errorf("admin group not defined") + } + agEntry := strings.Split(agLine, ":") + if len(agEntry) < 4 { + return fmt.Errorf("malformed admin group entry") + } + agMembers := agEntry[3] + for _, m := range strings.Split(agMembers, ",") { + if m == name { + return nil + } + } + return fmt.Errorf("not a member of administrators group") +} + +// authenticate returns the name of the user accessing the web UI. +// Note: This is different from a tailscale user, and is typically the local +// user on the node. +func authenticate() (string, error) { + if distro.Get() != distro.Synology { + return "", nil + } + cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi") + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("auth: %v: %s", err, out) + } + return strings.TrimSpace(string(out)), nil } func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool { @@ -198,8 +247,13 @@ func webHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := auth() + user, err := authenticate() if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + if err := authorize(user); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index d742cd4f6..6620c8461 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -55,7 +55,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep W tailscale.com/util/endian from tailscale.com/net/netns L tailscale.com/util/lineread from tailscale.com/net/interfaces tailscale.com/version from tailscale.com/cmd/tailscale/cli+ - tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli + tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+ tailscale.com/wgengine/filter from tailscale.com/types/netmap golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305 diff --git a/paths/paths.go b/paths/paths.go index 8e437a4f0..9f36af03c 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -11,6 +11,8 @@ "path/filepath" "runtime" "sync/atomic" + + "tailscale.com/version/distro" ) // AppSharedDir is a string set by the iOS or Android app on start @@ -26,11 +28,15 @@ func DefaultTailscaledSocket() string { if runtime.GOOS == "darwin" { return "/var/run/tailscaled.socket" } - if runtime.GOOS == "linux" { - // TODO(crawshaw): does this path change with DSM7? - const synologySock = "/volume1/@appstore/Tailscale/var/tailscaled.sock" // SYNOPKG_PKGDEST in scripts/installer - if fi, err := os.Stat(filepath.Dir(synologySock)); err == nil && fi.IsDir() { - return synologySock + if distro.Get() == 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" + if fi, err := os.Stat(dsm6Sock); err == nil && !fi.IsDir() { + return dsm6Sock + } + if fi, err := os.Stat(dsm7Sock); err == nil && !fi.IsDir() { + return dsm7Sock } } if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() {