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
parent
1b65630e83
commit
98114bf608
|
@ -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":
|
||||||
configIn := new(ipn.ServeConfig)
|
if !h.PermitWrite {
|
||||||
if err := json.NewDecoder(r.Body).Decode(configIn); err != nil {
|
http.Error(w, "serve config denied", http.StatusForbidden)
|
||||||
json.NewEncoder(w).Encode(struct {
|
|
||||||
Error error
|
|
||||||
}{
|
|
||||||
Error: fmt.Errorf("decoding config: %w", err),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := h.b.SetServeConfig(configIn)
|
configIn := new(ipn.ServeConfig)
|
||||||
if 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
|
return
|
||||||
}{
|
}
|
||||||
Error: fmt.Errorf("updating config: %w", err),
|
if err := h.b.SetServeConfig(configIn); err != nil {
|
||||||
})
|
writeErrorJSON(w, 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
ipn/serve.go
29
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.
|
// 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…
Reference in New Issue