Browse Source

cmd/tailscale/cli, ipn/localapi: add funnel status to status command (#6402)

Fixes #6400

open up GETs for localapi serve-config to allow read-only access to
ServeConfig

`tailscale status` will include "Funnel on" status when Funnel is
configured. Prints nothing if Funnel is not running.

Example:

 $ tailscale status
 <nodes redacted>

 # Funnel on:
 #     - https://node-name.corp.ts.net
 #     - https://node-name.corp.ts.net:8443
 #     - tcp://node-name.corp.ts.net:10000

Signed-off-by: Shayne Sweeney <[email protected]>
shayne 3 years ago
parent
commit
98114bf608
4 changed files with 73 additions and 32 deletions
  1. 3 4
      cmd/tailscale/cli/serve.go
  2. 36 0
      cmd/tailscale/cli/status.go
  3. 14 19
      ipn/localapi/localapi.go
  4. 20 9
      ipn/serve.go

+ 3 - 4
cmd/tailscale/cli/serve.go

@@ -517,7 +517,7 @@ func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.S
 			tlsStatus = "TLS terminated"
 		}
 		fStatus := "tailnet only"
-		if sc.IsFunnelOn(hp) {
+		if sc.AllowFunnel[hp] {
 			fStatus = "Funnel on"
 		}
 		printf("|-- tcp://%s (%s, %s)\n", hp, tlsStatus, fStatus)
@@ -535,7 +535,7 @@ func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
 		return
 	}
 	fStatus := "tailnet only"
-	if sc.IsFunnelOn(hp) {
+	if sc.AllowFunnel[hp] {
 		fStatus = "Funnel on"
 	}
 	host, portStr, _ := net.SplitHostPort(string(hp))
@@ -690,8 +690,7 @@ func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
 	}
 	dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
 	hp := ipn.HostPort(dnsName + ":" + srvPortStr)
-	isFun := sc.IsFunnelOn(hp)
-	if on && isFun || !on && !isFun {
+	if on == sc.AllowFunnel[hp] {
 		// Nothing to do.
 		return nil
 	}

+ 36 - 0
cmd/tailscale/cli/status.go

@@ -15,6 +15,7 @@ import (
 	"net/http"
 	"net/netip"
 	"os"
+	"strconv"
 	"strings"
 
 	"github.com/peterbourgon/ff/v3/ffcli"
@@ -222,9 +223,44 @@ func runStatus(ctx context.Context, args []string) error {
 		outln()
 		printHealth()
 	}
+	printFunnelStatus(ctx)
 	return nil
 }
 
+// printFunnelStatus prints the status of the funnel, if it's running.
+// It prints nothing if the funnel is not running.
+func printFunnelStatus(ctx context.Context) {
+	sc, err := localClient.GetServeConfig(ctx)
+	if err != nil {
+		outln()
+		printf("# Funnel:\n")
+		printf("#     - Unable to get Funnel status: %v\n", err)
+		return
+	}
+	if !sc.IsFunnelOn() {
+		return
+	}
+	outln()
+	printf("# Funnel on:\n")
+	for hp, on := range sc.AllowFunnel {
+		if !on { // if present, should be on
+			continue
+		}
+		sni, portStr, _ := net.SplitHostPort(string(hp))
+		p, _ := strconv.ParseUint(portStr, 10, 16)
+		isTCP := sc.IsTCPForwardingOnPort(uint16(p))
+		url := "https://"
+		if isTCP {
+			url = "tcp://"
+		}
+		url += sni
+		if isTCP || p != 443 {
+			url += ":" + portStr
+		}
+		printf("#     - %s\n", url)
+	}
+}
+
 // isRunningOrStarting reports whether st is in state Running or Starting.
 // It also returns a description of the status suitable to display to a user.
 func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {

+ 14 - 19
ipn/localapi/localapi.go

@@ -540,37 +540,32 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
 }
 
 func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
-	if !h.PermitWrite {
-		http.Error(w, "serve config denied", http.StatusForbidden)
-		return
-	}
-
-	w.Header().Set("Content-Type", "application/json")
-
 	switch r.Method {
 	case "GET":
+		if !h.PermitRead {
+			http.Error(w, "serve config denied", http.StatusForbidden)
+			return
+		}
+		w.Header().Set("Content-Type", "application/json")
 		config := h.b.ServeConfig()
 		json.NewEncoder(w).Encode(config)
 	case "POST":
+		if !h.PermitWrite {
+			http.Error(w, "serve config denied", http.StatusForbidden)
+			return
+		}
 		configIn := new(ipn.ServeConfig)
 		if err := json.NewDecoder(r.Body).Decode(configIn); err != nil {
-			json.NewEncoder(w).Encode(struct {
-				Error error
-			}{
-				Error: fmt.Errorf("decoding config: %w", err),
-			})
+			writeErrorJSON(w, fmt.Errorf("decoding config: %w", err))
 			return
 		}
-		err := h.b.SetServeConfig(configIn)
-		if err != nil {
-			json.NewEncoder(w).Encode(struct {
-				Error error
-			}{
-				Error: fmt.Errorf("updating config: %w", err),
-			})
+		if err := h.b.SetServeConfig(configIn); err != nil {
+			writeErrorJSON(w, fmt.Errorf("updating config: %w", err))
 			return
 		}
 		w.WriteHeader(http.StatusOK)
+	default:
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
 	}
 }
 

+ 20 - 9
ipn/serve.go

@@ -81,15 +81,18 @@ func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
 // GetWebHandler returns the HTTPHandler for the given host:port and mount point.
 // Returns nil if the handler does not exist.
 func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler {
-	if sc.Web[hp] != nil {
-		return sc.Web[hp].Handlers[mount]
+	if sc == nil || sc.Web[hp] == nil {
+		return nil
 	}
-	return nil
+	return sc.Web[hp].Handlers[mount]
 }
 
 // GetTCPPortHandler returns the TCPPortHandler for the given port.
 // If the port is not configured, nil is returned.
 func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
+	if sc == nil {
+		return nil
+	}
 	return sc.TCP[port]
 }
 
@@ -97,7 +100,7 @@ func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
 // in TCPForward mode on any port.
 // This is exclusive of Web/HTTPS serving.
 func (sc *ServeConfig) IsTCPForwardingAny() bool {
-	if len(sc.TCP) == 0 {
+	if sc == nil || len(sc.TCP) == 0 {
 		return false
 	}
 	for _, h := range sc.TCP {
@@ -112,7 +115,7 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
 // in TCPForward mode on the given port.
 // This is exclusive of Web/HTTPS serving.
 func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
-	if sc.TCP[port] == nil {
+	if sc == nil || sc.TCP[port] == nil {
 		return false
 	}
 	return !sc.TCP[port].HTTPS
@@ -122,14 +125,22 @@ func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
 // Web/HTTPS on the given port.
 // This is exclusive of TCPForwarding.
 func (sc *ServeConfig) IsServingWeb(port uint16) bool {
-	if sc.TCP[port] == nil {
+	if sc == nil || sc.TCP[port] == nil {
 		return false
 	}
 	return sc.TCP[port].HTTPS
 }
 
 // IsFunnelOn checks if ServeConfig is currently allowing
-// funnel traffic on for the given host:port.
-func (sc *ServeConfig) IsFunnelOn(hp HostPort) bool {
-	return sc.AllowFunnel[hp]
+// funnel traffic for any host:port.
+func (sc *ServeConfig) IsFunnelOn() bool {
+	if sc == nil {
+		return false
+	}
+	for _, b := range sc.AllowFunnel {
+		if b {
+			return true
+		}
+	}
+	return false
 }