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 <shayne@tailscale.com>
pull/6383/head
shayne 2 years ago committed by GitHub
parent 1b65630e83
commit 98114bf608
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -15,6 +15,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"os" "os"
"strconv"
"strings" "strings"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
@ -222,9 +223,44 @@ func runStatus(ctx context.Context, args []string) error {
outln() outln()
printHealth() printHealth()
} }
printFunnelStatus(ctx)
return nil 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. // isRunningOrStarting reports whether st is in state Running or Starting.
// It also returns a description of the status suitable to display to a user. // It also returns a description of the status suitable to display to a user.
func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) { func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {

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

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

Loading…
Cancel
Save