Explorar el Código

cmd/tailscale/web: restrict web access to synology admins.

Signed-off-by: Maisem Ali <[email protected]>
Maisem Ali hace 4 años
padre
commit
95e296fd96
Se han modificado 3 ficheros con 74 adiciones y 14 borrados
  1. 62 8
      cmd/tailscale/cli/web.go
  2. 1 1
      cmd/tailscale/depaware.txt
  3. 11 5
      paths/paths.go

+ 62 - 8
cmd/tailscale/cli/web.go

@@ -5,6 +5,7 @@
 package cli
 
 import (
+	"bufio"
 	"bytes"
 	"context"
 	_ "embed"
@@ -15,11 +16,13 @@ import (
 	"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 authorizeSynology(name)
+	}
+	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
 		}
-		return string(out), nil
+		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")
+}
 
-	return "", nil
+// 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
 	}

+ 1 - 1
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

+ 11 - 5
paths/paths.go

@@ -11,6 +11,8 @@ import (
 	"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() {