mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-08 16:58:35 +00:00
client/web: add Sec-Fetch-Site CSRF protection (#16046)
RELNOTE=Fix CSRF errors in the client Web UI Replace gorilla/csrf with a Sec-Fetch-Site based CSRF protection middleware that falls back to comparing the Host & Origin headers if no SFS value is passed by the client. Add an -origin override to the web CLI that allows callers to specify the origin at which the web UI will be available if it is hosted behind a reverse proxy or within another application via CGI. Updates #14872 Updates #15065 Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
This commit is contained in:
parent
3ee4c60ff0
commit
a05924a9e5
@ -249,7 +249,6 @@ export function useAPI() {
|
|||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
let csrfToken: string
|
|
||||||
let synoToken: string | undefined // required for synology API requests
|
let synoToken: string | undefined // required for synology API requests
|
||||||
let unraidCsrfToken: string | undefined // required for unraid POST requests (#8062)
|
let unraidCsrfToken: string | undefined // required for unraid POST requests (#8062)
|
||||||
|
|
||||||
@ -298,12 +297,10 @@ export function apiFetch<T>(
|
|||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": contentType,
|
"Content-Type": contentType,
|
||||||
"X-CSRF-Token": csrfToken,
|
|
||||||
},
|
},
|
||||||
body: body,
|
body: body,
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
updateCsrfToken(r)
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
return r.text().then((err) => {
|
return r.text().then((err) => {
|
||||||
throw new Error(err)
|
throw new Error(err)
|
||||||
@ -322,13 +319,6 @@ export function apiFetch<T>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCsrfToken(r: Response) {
|
|
||||||
const tok = r.headers.get("X-CSRF-Token")
|
|
||||||
if (tok) {
|
|
||||||
csrfToken = tok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSynoToken(token?: string) {
|
export function setSynoToken(token?: string) {
|
||||||
synoToken = token
|
synoToken = token
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -14,14 +13,14 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
|
||||||
"tailscale.com/client/local"
|
"tailscale.com/client/local"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/clientupdate"
|
"tailscale.com/clientupdate"
|
||||||
@ -60,6 +59,12 @@ type Server struct {
|
|||||||
cgiMode bool
|
cgiMode bool
|
||||||
pathPrefix string
|
pathPrefix string
|
||||||
|
|
||||||
|
// originOverride is the origin that the web UI is accessible from.
|
||||||
|
// This value is used in the fallback CSRF checks when Sec-Fetch-Site is not
|
||||||
|
// available. In this case the application will compare Host and Origin
|
||||||
|
// header values to determine if the request is from the same origin.
|
||||||
|
originOverride string
|
||||||
|
|
||||||
apiHandler http.Handler // serves api endpoints; csrf-protected
|
apiHandler http.Handler // serves api endpoints; csrf-protected
|
||||||
assetsHandler http.Handler // serves frontend assets
|
assetsHandler http.Handler // serves frontend assets
|
||||||
assetsCleanup func() // called from Server.Shutdown
|
assetsCleanup func() // called from Server.Shutdown
|
||||||
@ -150,6 +155,9 @@ type ServerOpts struct {
|
|||||||
// as completed.
|
// as completed.
|
||||||
// This field is required for ManageServerMode mode.
|
// This field is required for ManageServerMode mode.
|
||||||
WaitAuthURL func(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
|
WaitAuthURL func(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
|
||||||
|
|
||||||
|
// OriginOverride specifies the origin that the web UI will be accessible from if hosted behind a reverse proxy or CGI.
|
||||||
|
OriginOverride string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer constructs a new Tailscale web client server.
|
// NewServer constructs a new Tailscale web client server.
|
||||||
@ -169,15 +177,16 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
|||||||
opts.LocalClient = &local.Client{}
|
opts.LocalClient = &local.Client{}
|
||||||
}
|
}
|
||||||
s = &Server{
|
s = &Server{
|
||||||
mode: opts.Mode,
|
mode: opts.Mode,
|
||||||
logf: opts.Logf,
|
logf: opts.Logf,
|
||||||
devMode: envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
|
devMode: envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
|
||||||
lc: opts.LocalClient,
|
lc: opts.LocalClient,
|
||||||
cgiMode: opts.CGIMode,
|
cgiMode: opts.CGIMode,
|
||||||
pathPrefix: opts.PathPrefix,
|
pathPrefix: opts.PathPrefix,
|
||||||
timeNow: opts.TimeNow,
|
timeNow: opts.TimeNow,
|
||||||
newAuthURL: opts.NewAuthURL,
|
newAuthURL: opts.NewAuthURL,
|
||||||
waitAuthURL: opts.WaitAuthURL,
|
waitAuthURL: opts.WaitAuthURL,
|
||||||
|
originOverride: opts.OriginOverride,
|
||||||
}
|
}
|
||||||
if opts.PathPrefix != "" {
|
if opts.PathPrefix != "" {
|
||||||
// Enforce that path prefix always has a single leading '/'
|
// Enforce that path prefix always has a single leading '/'
|
||||||
@ -205,7 +214,7 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
|||||||
|
|
||||||
var metric string
|
var metric string
|
||||||
s.apiHandler, metric = s.modeAPIHandler(s.mode)
|
s.apiHandler, metric = s.modeAPIHandler(s.mode)
|
||||||
s.apiHandler = s.withCSRF(s.apiHandler)
|
s.apiHandler = s.csrfProtect(s.apiHandler)
|
||||||
|
|
||||||
// Don't block startup on reporting metric.
|
// Don't block startup on reporting metric.
|
||||||
// Report in separate go routine with 5 second timeout.
|
// Report in separate go routine with 5 second timeout.
|
||||||
@ -218,23 +227,64 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) withCSRF(h http.Handler) http.Handler {
|
func (s *Server) csrfProtect(h http.Handler) http.Handler {
|
||||||
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// CSRF is not required for GET, HEAD, or OPTIONS requests.
|
||||||
// ref https://github.com/tailscale/tailscale/pull/14822
|
if slices.Contains([]string{"GET", "HEAD", "OPTIONS"}, r.Method) {
|
||||||
// signal to the CSRF middleware that the request is being served over
|
|
||||||
// plaintext HTTP to skip TLS-only header checks.
|
|
||||||
withSetPlaintext := func(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r = csrf.PlaintextHTTPRequest(r)
|
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
})
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// NB: the order of the withSetPlaintext and csrfProtect calls is important
|
// first attempt to use Sec-Fetch-Site header (sent by all modern
|
||||||
// to ensure that we signal to the CSRF middleware that the request is being
|
// browsers to "potentially trustworthy" origins i.e. localhost or those
|
||||||
// served over plaintext HTTP and not over TLS as it presumes by default.
|
// served over HTTPS)
|
||||||
return withSetPlaintext(csrfProtect(h))
|
secFetchSite := r.Header.Get("Sec-Fetch-Site")
|
||||||
|
if secFetchSite == "same-origin" {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
} else if secFetchSite != "" {
|
||||||
|
http.Error(w, fmt.Sprintf("CSRF request denied with Sec-Fetch-Site %q", secFetchSite), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if Sec-Fetch-Site is not available we presume we are operating over HTTP.
|
||||||
|
// We fall back to comparing the Origin & Host headers.
|
||||||
|
|
||||||
|
// use the Host header to determine the expected origin
|
||||||
|
// (use the override if set to allow for reverse proxying)
|
||||||
|
host := r.Host
|
||||||
|
if host == "" {
|
||||||
|
http.Error(w, "CSRF request denied with no Host header", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.originOverride != "" {
|
||||||
|
host = s.originOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
originHeader := r.Header.Get("Origin")
|
||||||
|
if originHeader == "" {
|
||||||
|
http.Error(w, "CSRF request denied with no Origin header", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsedOrigin, err := url.Parse(originHeader)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("CSRF request denied with invalid Origin %q", r.Header.Get("Origin")), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
origin := parsedOrigin.Host
|
||||||
|
if origin == "" {
|
||||||
|
http.Error(w, "CSRF request denied with no host in the Origin header", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if origin != host {
|
||||||
|
http.Error(w, fmt.Sprintf("CSRF request denied with mismatched Origin %q and Host %q", origin, host), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
|
func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
|
||||||
@ -452,7 +502,6 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
|
|||||||
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
|
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
|
||||||
// which protects the handler using gorilla csrf.
|
// which protects the handler using gorilla csrf.
|
||||||
func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
|
||||||
switch {
|
switch {
|
||||||
case r.URL.Path == "/api/data" && r.Method == httpm.GET:
|
case r.URL.Path == "/api/data" && r.Method == httpm.GET:
|
||||||
s.serveGetNodeData(w, r)
|
s.serveGetNodeData(w, r)
|
||||||
@ -575,7 +624,6 @@ func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
|
||||||
path := strings.TrimPrefix(r.URL.Path, "/api")
|
path := strings.TrimPrefix(r.URL.Path, "/api")
|
||||||
switch {
|
switch {
|
||||||
case path == "/data" && r.Method == httpm.GET:
|
case path == "/data" && r.Method == httpm.GET:
|
||||||
@ -1276,37 +1324,6 @@ func (s *Server) proxyRequestToLocalAPI(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// csrfKey returns a key that can be used for CSRF protection.
|
|
||||||
// If an error occurs during key creation, the error is logged and the active process terminated.
|
|
||||||
// If the server is running in CGI mode, the key is cached to disk and reused between requests.
|
|
||||||
// If an error occurs during key storage, the error is logged and the active process terminated.
|
|
||||||
func (s *Server) csrfKey() []byte {
|
|
||||||
csrfFile := filepath.Join(os.TempDir(), "tailscale-web-csrf.key")
|
|
||||||
|
|
||||||
// if running in CGI mode, try to read from disk, but ignore errors
|
|
||||||
if s.cgiMode {
|
|
||||||
key, _ := os.ReadFile(csrfFile)
|
|
||||||
if len(key) == 32 {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new key
|
|
||||||
key := make([]byte, 32)
|
|
||||||
if _, err := rand.Read(key); err != nil {
|
|
||||||
log.Fatalf("error generating CSRF key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if running in CGI mode, try to write the newly created key to disk, and exit if it fails.
|
|
||||||
if s.cgiMode {
|
|
||||||
if err := os.WriteFile(csrfFile, key, 0600); err != nil {
|
|
||||||
log.Fatalf("unable to store CSRF key: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// enforcePrefix returns a HandlerFunc that enforces a given path prefix is used in requests,
|
// enforcePrefix returns a HandlerFunc that enforces a given path prefix is used in requests,
|
||||||
// then strips it before invoking h.
|
// then strips it before invoking h.
|
||||||
// Unlike http.StripPrefix, it does not return a 404 if the prefix is not present.
|
// Unlike http.StripPrefix, it does not return a 404 if the prefix is not present.
|
||||||
|
@ -11,7 +11,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -21,14 +20,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/gorilla/csrf"
|
|
||||||
"tailscale.com/client/local"
|
"tailscale.com/client/local"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/net/memnet"
|
"tailscale.com/net/memnet"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tstest/nettest"
|
|
||||||
"tailscale.com/types/views"
|
"tailscale.com/types/views"
|
||||||
"tailscale.com/util/httpm"
|
"tailscale.com/util/httpm"
|
||||||
)
|
)
|
||||||
@ -1492,81 +1489,99 @@ func mockWaitAuthURL(_ context.Context, id string, src tailcfg.NodeID) (*tailcfg
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCSRFProtect(t *testing.T) {
|
func TestCSRFProtect(t *testing.T) {
|
||||||
s := &Server{}
|
tests := []struct {
|
||||||
|
name string
|
||||||
mux := http.NewServeMux()
|
method string
|
||||||
mux.HandleFunc("GET /test/csrf-token", func(w http.ResponseWriter, r *http.Request) {
|
secFetchSite string
|
||||||
token := csrf.Token(r)
|
host string
|
||||||
_, err := io.WriteString(w, token)
|
origin string
|
||||||
if err != nil {
|
originOverride string
|
||||||
t.Fatal(err)
|
wantError bool
|
||||||
}
|
}{
|
||||||
})
|
{
|
||||||
mux.HandleFunc("POST /test/csrf-protected", func(w http.ResponseWriter, r *http.Request) {
|
name: "GET requests with no header are allowed",
|
||||||
_, err := io.WriteString(w, "ok")
|
method: "GET",
|
||||||
if err != nil {
|
},
|
||||||
t.Fatal(err)
|
{
|
||||||
}
|
name: "POST requests with same-origin are allowed",
|
||||||
})
|
method: "POST",
|
||||||
h := s.withCSRF(mux)
|
secFetchSite: "same-origin",
|
||||||
ser := nettest.NewHTTPServer(nettest.GetNetwork(t), h)
|
},
|
||||||
defer ser.Close()
|
{
|
||||||
|
name: "POST requests with cross-site are not allowed",
|
||||||
jar, err := cookiejar.New(nil)
|
method: "POST",
|
||||||
if err != nil {
|
secFetchSite: "cross-site",
|
||||||
t.Fatalf("unable to construct cookie jar: %v", err)
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST requests with unknown sec-fetch-site values are not allowed",
|
||||||
|
method: "POST",
|
||||||
|
secFetchSite: "new-unknown-value",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST requests with none are not allowed",
|
||||||
|
method: "POST",
|
||||||
|
secFetchSite: "none",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST requests with no sec-fetch-site header but matching host and origin are allowed",
|
||||||
|
method: "POST",
|
||||||
|
host: "example.com",
|
||||||
|
origin: "https://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST requests with no sec-fetch-site and non-matching host and origin are not allowed",
|
||||||
|
method: "POST",
|
||||||
|
host: "example.com",
|
||||||
|
origin: "https://example.net",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST requests with no sec-fetch-site and and origin that matches the override are allowed",
|
||||||
|
method: "POST",
|
||||||
|
originOverride: "example.net",
|
||||||
|
host: "internal.example.foo", // Host can be changed by reverse proxies
|
||||||
|
origin: "http://example.net",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
client := ser.Client()
|
for _, tt := range tests {
|
||||||
client.Jar = jar
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintf(w, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
// make GET request to populate cookie jar
|
s := &Server{
|
||||||
resp, err := client.Get(ser.URL + "/test/csrf-token")
|
originOverride: tt.originOverride,
|
||||||
if err != nil {
|
}
|
||||||
t.Fatalf("unable to make request: %v", err)
|
withCSRF := s.csrfProtect(handler)
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
t.Fatalf("unexpected status: %v", resp.Status)
|
|
||||||
}
|
|
||||||
tokenBytes, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to read body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
csrfToken := strings.TrimSpace(string(tokenBytes))
|
r := httptest.NewRequest(tt.method, "http://example.com/", nil)
|
||||||
if csrfToken == "" {
|
if tt.secFetchSite != "" {
|
||||||
t.Fatal("empty csrf token")
|
r.Header.Set("Sec-Fetch-Site", tt.secFetchSite)
|
||||||
}
|
}
|
||||||
|
if tt.host != "" {
|
||||||
|
r.Host = tt.host
|
||||||
|
}
|
||||||
|
if tt.origin != "" {
|
||||||
|
r.Header.Set("Origin", tt.origin)
|
||||||
|
}
|
||||||
|
|
||||||
// make a POST request without the CSRF header; ensure it fails
|
w := httptest.NewRecorder()
|
||||||
resp, err = client.Post(ser.URL+"/test/csrf-protected", "text/plain", nil)
|
withCSRF.ServeHTTP(w, r)
|
||||||
if err != nil {
|
res := w.Result()
|
||||||
t.Fatalf("unable to make request: %v", err)
|
defer res.Body.Close()
|
||||||
}
|
if tt.wantError {
|
||||||
if resp.StatusCode != http.StatusForbidden {
|
if res.StatusCode != http.StatusForbidden {
|
||||||
t.Fatalf("unexpected status: %v", resp.Status)
|
t.Errorf("expected status forbidden, got %v", res.StatusCode)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
// make a POST request with the CSRF header; ensure it succeeds
|
}
|
||||||
req, err := http.NewRequest("POST", ser.URL+"/test/csrf-protected", nil)
|
if res.StatusCode != http.StatusOK {
|
||||||
if err != nil {
|
t.Errorf("expected status ok, got %v", res.StatusCode)
|
||||||
t.Fatalf("error building request: %v", err)
|
}
|
||||||
}
|
})
|
||||||
req.Header.Set("X-CSRF-Token", csrfToken)
|
|
||||||
resp, err = client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to make request: %v", err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
t.Fatalf("unexpected status: %v", resp.Status)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
out, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to read body: %v", err)
|
|
||||||
}
|
|
||||||
if string(out) != "ok" {
|
|
||||||
t.Fatalf("unexpected body: %q", out)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,8 +144,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||||
github.com/google/uuid from github.com/prometheus-community/pro-bing+
|
github.com/google/uuid from github.com/prometheus-community/pro-bing+
|
||||||
github.com/gorilla/csrf from tailscale.com/client/web
|
|
||||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||||
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
|
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
|
||||||
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
|
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
|
||||||
@ -1112,13 +1110,12 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
W debug/dwarf from debug/pe
|
W debug/dwarf from debug/pe
|
||||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||||
embed from github.com/tailscale/web-client-prebuilt+
|
embed from github.com/tailscale/web-client-prebuilt+
|
||||||
encoding from encoding/gob+
|
encoding from encoding/json+
|
||||||
encoding/asn1 from crypto/x509+
|
encoding/asn1 from crypto/x509+
|
||||||
encoding/base32 from github.com/fxamacker/cbor/v2+
|
encoding/base32 from github.com/fxamacker/cbor/v2+
|
||||||
encoding/base64 from encoding/json+
|
encoding/base64 from encoding/json+
|
||||||
encoding/binary from compress/gzip+
|
encoding/binary from compress/gzip+
|
||||||
encoding/csv from github.com/spf13/pflag
|
encoding/csv from github.com/spf13/pflag
|
||||||
encoding/gob from github.com/gorilla/securecookie
|
|
||||||
encoding/hex from crypto/x509+
|
encoding/hex from crypto/x509+
|
||||||
encoding/json from expvar+
|
encoding/json from expvar+
|
||||||
encoding/pem from crypto/tls+
|
encoding/pem from crypto/tls+
|
||||||
@ -1140,7 +1137,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
hash/fnv from google.golang.org/protobuf/internal/detrand
|
hash/fnv from google.golang.org/protobuf/internal/detrand
|
||||||
hash/maphash from go4.org/mem
|
hash/maphash from go4.org/mem
|
||||||
html from html/template+
|
html from html/template+
|
||||||
html/template from github.com/gorilla/csrf+
|
html/template from tailscale.com/util/eventbus
|
||||||
internal/abi from crypto/x509/internal/macos+
|
internal/abi from crypto/x509/internal/macos+
|
||||||
internal/asan from internal/runtime/maps+
|
internal/asan from internal/runtime/maps+
|
||||||
internal/bisect from internal/godebug
|
internal/bisect from internal/godebug
|
||||||
@ -1172,7 +1169,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
internal/runtime/math from internal/runtime/maps+
|
internal/runtime/math from internal/runtime/maps+
|
||||||
internal/runtime/sys from crypto/subtle+
|
internal/runtime/sys from crypto/subtle+
|
||||||
L internal/runtime/syscall from runtime+
|
L internal/runtime/syscall from runtime+
|
||||||
internal/saferio from debug/pe+
|
W internal/saferio from debug/pe
|
||||||
internal/singleflight from net
|
internal/singleflight from net
|
||||||
internal/stringslite from embed+
|
internal/stringslite from embed+
|
||||||
internal/sync from sync+
|
internal/sync from sync+
|
||||||
|
@ -43,6 +43,7 @@ Tailscale, as opposed to a CLI or a native app.
|
|||||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||||
webf.StringVar(&webArgs.prefix, "prefix", "", "URL prefix added to requests (for cgi or reverse proxies)")
|
webf.StringVar(&webArgs.prefix, "prefix", "", "URL prefix added to requests (for cgi or reverse proxies)")
|
||||||
webf.BoolVar(&webArgs.readonly, "readonly", false, "run web UI in read-only mode")
|
webf.BoolVar(&webArgs.readonly, "readonly", false, "run web UI in read-only mode")
|
||||||
|
webf.StringVar(&webArgs.origin, "origin", "", "origin at which the web UI is served (if behind a reverse proxy or used with cgi)")
|
||||||
return webf
|
return webf
|
||||||
})(),
|
})(),
|
||||||
Exec: runWeb,
|
Exec: runWeb,
|
||||||
@ -53,6 +54,7 @@ var webArgs struct {
|
|||||||
cgi bool
|
cgi bool
|
||||||
prefix string
|
prefix string
|
||||||
readonly bool
|
readonly bool
|
||||||
|
origin string
|
||||||
}
|
}
|
||||||
|
|
||||||
func tlsConfigFromEnvironment() *tls.Config {
|
func tlsConfigFromEnvironment() *tls.Config {
|
||||||
@ -115,6 +117,9 @@ func runWeb(ctx context.Context, args []string) error {
|
|||||||
if webArgs.readonly {
|
if webArgs.readonly {
|
||||||
opts.Mode = web.ReadOnlyServerMode
|
opts.Mode = web.ReadOnlyServerMode
|
||||||
}
|
}
|
||||||
|
if webArgs.origin != "" {
|
||||||
|
opts.OriginOverride = webArgs.origin
|
||||||
|
}
|
||||||
webServer, err := web.NewServer(opts)
|
webServer, err := web.NewServer(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("tailscale.web: %v", err)
|
log.Printf("tailscale.web: %v", err)
|
||||||
|
@ -27,8 +27,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||||
DW github.com/google/uuid from tailscale.com/clientupdate+
|
DW github.com/google/uuid from tailscale.com/clientupdate+
|
||||||
github.com/gorilla/csrf from tailscale.com/client/web
|
|
||||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
|
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
|
||||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||||
@ -319,12 +317,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
W debug/dwarf from debug/pe
|
W debug/dwarf from debug/pe
|
||||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||||
embed from github.com/peterbourgon/ff/v3+
|
embed from github.com/peterbourgon/ff/v3+
|
||||||
encoding from encoding/gob+
|
encoding from encoding/json+
|
||||||
encoding/asn1 from crypto/x509+
|
encoding/asn1 from crypto/x509+
|
||||||
encoding/base32 from github.com/fxamacker/cbor/v2+
|
encoding/base32 from github.com/fxamacker/cbor/v2+
|
||||||
encoding/base64 from encoding/json+
|
encoding/base64 from encoding/json+
|
||||||
encoding/binary from compress/gzip+
|
encoding/binary from compress/gzip+
|
||||||
encoding/gob from github.com/gorilla/securecookie
|
|
||||||
encoding/hex from crypto/x509+
|
encoding/hex from crypto/x509+
|
||||||
encoding/json from expvar+
|
encoding/json from expvar+
|
||||||
encoding/pem from crypto/tls+
|
encoding/pem from crypto/tls+
|
||||||
@ -338,7 +335,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
hash/crc32 from compress/gzip+
|
hash/crc32 from compress/gzip+
|
||||||
hash/maphash from go4.org/mem
|
hash/maphash from go4.org/mem
|
||||||
html from html/template+
|
html from html/template+
|
||||||
html/template from github.com/gorilla/csrf+
|
html/template from tailscale.com/util/eventbus
|
||||||
image from github.com/skip2/go-qrcode+
|
image from github.com/skip2/go-qrcode+
|
||||||
image/color from github.com/skip2/go-qrcode+
|
image/color from github.com/skip2/go-qrcode+
|
||||||
image/png from github.com/skip2/go-qrcode
|
image/png from github.com/skip2/go-qrcode
|
||||||
@ -372,7 +369,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
internal/runtime/math from internal/runtime/maps+
|
internal/runtime/math from internal/runtime/maps+
|
||||||
internal/runtime/sys from crypto/subtle+
|
internal/runtime/sys from crypto/subtle+
|
||||||
L internal/runtime/syscall from runtime+
|
L internal/runtime/syscall from runtime+
|
||||||
internal/saferio from debug/pe+
|
W internal/saferio from debug/pe
|
||||||
internal/singleflight from net
|
internal/singleflight from net
|
||||||
internal/stringslite from embed+
|
internal/stringslite from embed+
|
||||||
internal/sync from sync+
|
internal/sync from sync+
|
||||||
|
@ -123,8 +123,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||||
DW github.com/google/uuid from tailscale.com/clientupdate+
|
DW github.com/google/uuid from tailscale.com/clientupdate+
|
||||||
github.com/gorilla/csrf from tailscale.com/client/web
|
|
||||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||||
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
|
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
|
||||||
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
|
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
|
||||||
@ -590,12 +588,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
W debug/dwarf from debug/pe
|
W debug/dwarf from debug/pe
|
||||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||||
embed from github.com/tailscale/web-client-prebuilt+
|
embed from github.com/tailscale/web-client-prebuilt+
|
||||||
encoding from encoding/gob+
|
encoding from encoding/json+
|
||||||
encoding/asn1 from crypto/x509+
|
encoding/asn1 from crypto/x509+
|
||||||
encoding/base32 from github.com/fxamacker/cbor/v2+
|
encoding/base32 from github.com/fxamacker/cbor/v2+
|
||||||
encoding/base64 from encoding/json+
|
encoding/base64 from encoding/json+
|
||||||
encoding/binary from compress/gzip+
|
encoding/binary from compress/gzip+
|
||||||
encoding/gob from github.com/gorilla/securecookie
|
|
||||||
encoding/hex from crypto/x509+
|
encoding/hex from crypto/x509+
|
||||||
encoding/json from expvar+
|
encoding/json from expvar+
|
||||||
encoding/pem from crypto/tls+
|
encoding/pem from crypto/tls+
|
||||||
@ -609,7 +606,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
hash/crc32 from compress/gzip+
|
hash/crc32 from compress/gzip+
|
||||||
hash/maphash from go4.org/mem
|
hash/maphash from go4.org/mem
|
||||||
html from html/template+
|
html from html/template+
|
||||||
html/template from github.com/gorilla/csrf+
|
html/template from tailscale.com/util/eventbus
|
||||||
internal/abi from crypto/x509/internal/macos+
|
internal/abi from crypto/x509/internal/macos+
|
||||||
internal/asan from internal/runtime/maps+
|
internal/asan from internal/runtime/maps+
|
||||||
internal/bisect from internal/godebug
|
internal/bisect from internal/godebug
|
||||||
@ -640,7 +637,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
internal/runtime/math from internal/runtime/maps+
|
internal/runtime/math from internal/runtime/maps+
|
||||||
internal/runtime/sys from crypto/subtle+
|
internal/runtime/sys from crypto/subtle+
|
||||||
L internal/runtime/syscall from runtime+
|
L internal/runtime/syscall from runtime+
|
||||||
internal/saferio from debug/pe+
|
W internal/saferio from debug/pe
|
||||||
internal/singleflight from net
|
internal/singleflight from net
|
||||||
internal/stringslite from embed+
|
internal/stringslite from embed+
|
||||||
internal/sync from sync+
|
internal/sync from sync+
|
||||||
|
@ -113,8 +113,6 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
|||||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||||
DWI github.com/google/uuid from github.com/prometheus-community/pro-bing+
|
DWI github.com/google/uuid from github.com/prometheus-community/pro-bing+
|
||||||
LDW github.com/gorilla/csrf from tailscale.com/client/web
|
|
||||||
LDW github.com/gorilla/securecookie from github.com/gorilla/csrf
|
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||||
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
|
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
|
||||||
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
|
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
|
||||||
@ -534,12 +532,11 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
|||||||
W debug/dwarf from debug/pe
|
W debug/dwarf from debug/pe
|
||||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||||
embed from github.com/tailscale/web-client-prebuilt+
|
embed from github.com/tailscale/web-client-prebuilt+
|
||||||
encoding from encoding/gob+
|
encoding from encoding/json+
|
||||||
encoding/asn1 from crypto/x509+
|
encoding/asn1 from crypto/x509+
|
||||||
encoding/base32 from github.com/fxamacker/cbor/v2+
|
encoding/base32 from github.com/fxamacker/cbor/v2+
|
||||||
encoding/base64 from encoding/json+
|
encoding/base64 from encoding/json+
|
||||||
encoding/binary from compress/gzip+
|
encoding/binary from compress/gzip+
|
||||||
LDW encoding/gob from github.com/gorilla/securecookie
|
|
||||||
encoding/hex from crypto/x509+
|
encoding/hex from crypto/x509+
|
||||||
encoding/json from expvar+
|
encoding/json from expvar+
|
||||||
encoding/pem from crypto/tls+
|
encoding/pem from crypto/tls+
|
||||||
@ -553,7 +550,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
|||||||
hash/crc32 from compress/gzip+
|
hash/crc32 from compress/gzip+
|
||||||
hash/maphash from go4.org/mem
|
hash/maphash from go4.org/mem
|
||||||
html from html/template+
|
html from html/template+
|
||||||
LDW html/template from github.com/gorilla/csrf+
|
LDW html/template from tailscale.com/util/eventbus
|
||||||
internal/abi from crypto/x509/internal/macos+
|
internal/abi from crypto/x509/internal/macos+
|
||||||
internal/asan from internal/runtime/maps+
|
internal/asan from internal/runtime/maps+
|
||||||
internal/bisect from internal/godebug
|
internal/bisect from internal/godebug
|
||||||
@ -584,7 +581,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
|||||||
internal/runtime/math from internal/runtime/maps+
|
internal/runtime/math from internal/runtime/maps+
|
||||||
internal/runtime/sys from crypto/subtle+
|
internal/runtime/sys from crypto/subtle+
|
||||||
LA internal/runtime/syscall from runtime+
|
LA internal/runtime/syscall from runtime+
|
||||||
LDW internal/saferio from debug/pe+
|
W internal/saferio from debug/pe
|
||||||
internal/singleflight from net
|
internal/singleflight from net
|
||||||
internal/stringslite from embed+
|
internal/stringslite from embed+
|
||||||
internal/sync from sync+
|
internal/sync from sync+
|
||||||
|
Loading…
x
Reference in New Issue
Block a user