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

Signed-off-by: Maisem Ali <maisem@tailscale.com>
pull/2042/head
Maisem Ali 4 years ago committed by Maisem Ali
parent 5088af68cf
commit 95e296fd96

@ -5,6 +5,7 @@
package cli package cli
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
_ "embed" _ "embed"
@ -15,11 +16,13 @@ import (
"log" "log"
"net/http" "net/http"
"net/http/cgi" "net/http/cgi"
"os"
"os/exec" "os/exec"
"runtime" "runtime"
"strings" "strings"
"github.com/peterbourgon/ff/v2/ffcli" "github.com/peterbourgon/ff/v2/ffcli"
"go4.org/mem"
"tailscale.com/client/tailscale" "tailscale.com/client/tailscale"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@ -82,17 +85,63 @@ func runWeb(ctx context.Context, args []string) error {
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler)) 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 { if distro.Get() == distro.Synology {
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi") return authorizeSynology(name)
out, err := cmd.CombinedOutput() }
if err != nil { return nil
return "", fmt.Errorf("auth: %v: %s", err, out) }
// 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 { func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
@ -198,8 +247,13 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := auth() user, err := authenticate()
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if err := authorize(user); err != nil {
http.Error(w, err.Error(), http.StatusForbidden) http.Error(w, err.Error(), http.StatusForbidden)
return return
} }

@ -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 W tailscale.com/util/endian from tailscale.com/net/netns
L tailscale.com/util/lineread from tailscale.com/net/interfaces L tailscale.com/util/lineread from tailscale.com/net/interfaces
tailscale.com/version from tailscale.com/cmd/tailscale/cli+ 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 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/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305 golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305

@ -11,6 +11,8 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync/atomic" "sync/atomic"
"tailscale.com/version/distro"
) )
// AppSharedDir is a string set by the iOS or Android app on start // AppSharedDir is a string set by the iOS or Android app on start
@ -26,11 +28,15 @@ func DefaultTailscaledSocket() string {
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
return "/var/run/tailscaled.socket" return "/var/run/tailscaled.socket"
} }
if runtime.GOOS == "linux" { if distro.Get() == distro.Synology {
// TODO(crawshaw): does this path change with DSM7? // TODO(maisem): be smarter about this. We can parse /etc/VERSION.
const synologySock = "/volume1/@appstore/Tailscale/var/tailscaled.sock" // SYNOPKG_PKGDEST in scripts/installer const dsm6Sock = "/var/packages/Tailscale/etc/tailscaled.sock"
if fi, err := os.Stat(filepath.Dir(synologySock)); err == nil && fi.IsDir() { const dsm7Sock = "/var/packages/Tailscale/var/tailscaled.sock"
return synologySock 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() { if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() {

Loading…
Cancel
Save