From ae8a052c5f253bed2dcdd19479424a97ee1741d7 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 23 Oct 2023 15:07:54 -0700 Subject: [PATCH] cmd/tailscale/cli: update {serve,funnel} status Signed-off-by: Tyler Smalley --- cmd/tailscale/cli/funnel.go | 2 +- cmd/tailscale/cli/serve_legacy.go | 14 +-- cmd/tailscale/cli/serve_v2.go | 173 +++++++++++++++++++++++++----- 3 files changed, 154 insertions(+), 35 deletions(-) diff --git a/cmd/tailscale/cli/funnel.go b/cmd/tailscale/cli/funnel.go index 6086646fe..90a0ec898 100644 --- a/cmd/tailscale/cli/funnel.go +++ b/cmd/tailscale/cli/funnel.go @@ -54,7 +54,7 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command { Subcommands: []*ffcli.Command{ { Name: "status", - Exec: e.runServeStatus, + Exec: e.runLegacyServeStatus, ShortHelp: "show current serve/funnel status", FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) { fs.BoolVar(&e.json, "json", false, "output JSON") diff --git a/cmd/tailscale/cli/serve_legacy.go b/cmd/tailscale/cli/serve_legacy.go index 6058f2ee0..65a698294 100644 --- a/cmd/tailscale/cli/serve_legacy.go +++ b/cmd/tailscale/cli/serve_legacy.go @@ -97,7 +97,7 @@ EXAMPLES Subcommands: []*ffcli.Command{ { Name: "status", - Exec: e.runServeStatus, + Exec: e.runLegacyServeStatus, ShortHelp: "show current serve/funnel status", FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) { fs.BoolVar(&e.json, "json", false, "output JSON") @@ -635,13 +635,13 @@ func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error { return errors.New("error: serve config does not exist") } -// runServeStatus is the entry point for the "serve status" +// runLegacyServeStatus is the entry point for the "serve status" // subcommand and prints the current serve config. // // Examples: // - tailscale status // - tailscale status --json -func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error { +func (e *serveEnv) runLegacyServeStatus(ctx context.Context, args []string) error { sc, err := e.lc.GetServeConfig(ctx) if err != nil { return err @@ -665,13 +665,13 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error { return err } if sc.IsTCPForwardingAny() { - if err := printTCPStatusTree(ctx, sc, st); err != nil { + if err := printLegacyTCPStatusTree(ctx, sc, st); err != nil { return err } printf("\n") } for hp := range sc.Web { - err := e.printWebStatusTree(sc, hp) + err := e.printLegacyWebStatusTree(sc, hp) if err != nil { return err } @@ -688,7 +688,7 @@ func (e *serveEnv) stdout() io.Writer { return os.Stdout } -func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status) error { +func printLegacyTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status) error { dnsName := strings.TrimSuffix(st.Self.DNSName, ".") for p, h := range sc.TCP { if h.TCPForward == "" { @@ -713,7 +713,7 @@ func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.S return nil } -func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) error { +func (e *serveEnv) printLegacyWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) error { // No-op if no serve config if sc == nil { return nil diff --git a/cmd/tailscale/cli/serve_v2.go b/cmd/tailscale/cli/serve_v2.go index a2b8cae56..71479a53f 100644 --- a/cmd/tailscale/cli/serve_v2.go +++ b/cmd/tailscale/cli/serve_v2.go @@ -132,7 +132,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command { Subcommands: []*ffcli.Command{ { Name: "status", - Exec: e.runServeStatus, + Exec: e.runServeStatus(subcmd), ShortHelp: "view current proxy configuration", FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) { fs.BoolVar(&e.json, "json", false, "output JSON") @@ -445,34 +445,12 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN portPart = "" } - srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) { - switch { - case h.Path != "": - return "path", h.Path - case h.Proxy != "": - return "proxy", h.Proxy - case h.Text != "": - return "text", "\"" + elipticallyTruncate(h.Text, 20) + "\"" - } - return "", "" - } - if sc.Web[hp] != nil { - var mounts []string - - for k := range sc.Web[hp].Handlers { - mounts = append(mounts, k) - } - sort.Slice(mounts, func(i, j int) bool { - return len(mounts[i]) < len(mounts[j]) - }) - - for _, m := range mounts { - h := sc.Web[hp].Handlers[m] - t, d := srvTypeAndDesc(h) - output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, dnsName, portPart, m)) - output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d)) + webTree, err := e.webStatusTree(sc, st, hp) + if err != nil { + return err.Error() } + output.WriteString(webTree) } else if sc.TCP[srvPort] != nil { h := sc.TCP[srvPort] @@ -868,6 +846,147 @@ func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error { return nil } +// runServeStatus is the entry point for the "serve status" +// subcommand and prints the current serve config. +// +// Examples: +// - tailscale status +// - tailscale status --json +func (e *serveEnv) runServeStatus(subcmd serveMode) execFunc { + e.subcmd = subcmd + + return func(ctx context.Context, args []string) error { + sc, err := e.lc.GetServeConfig(ctx) + if err != nil { + return err + } + + if e.json { + j, err := json.MarshalIndent(sc, "", " ") + if err != nil { + return err + } + j = append(j, '\n') + e.stdout().Write(j) + return nil + } + + if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) { + fmt.Printf("No %s config\n", infoMap[e.subcmd].Name) + return nil + } + + st, err := e.getLocalClientStatusWithoutPeers(ctx) + if err != nil { + return err + } + + if sc.IsTCPForwardingAny() { + tree := e.tcpStatusTree(sc, st) + printf(tree) + } + for hp := range sc.Web { + tree, err := e.webStatusTree(sc, st, hp) + if err != nil { + return err + } + printf(tree) + } + printFunnelWarning(sc) + return nil + } +} + +func (e *serveEnv) webStatusTree(sc *ipn.ServeConfig, st *ipnstate.Status, hp ipn.HostPort) (string, error) { + var output strings.Builder + + // No-op if no serve config + if sc == nil { + return "", nil + } + fStatus := "tailnet only" + if sc.AllowFunnel[hp] { + fStatus = "public" + } + _, portStr, _ := net.SplitHostPort(string(hp)) + + port, err := parseServePort(portStr) + if err != nil { + return "", fmt.Errorf("invalid port %q: %w", portStr, err) + } + + scheme := "https" + if sc.IsServingHTTP(port) { + scheme = "http" + } + + portPart := ":" + portStr + if scheme == "http" && portStr == "80" || + scheme == "https" && portStr == "443" { + portPart = "" + } + + srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) { + switch { + case h.Path != "": + return "path", h.Path + case h.Proxy != "": + return "proxy", h.Proxy + case h.Text != "": + return "text", "\"" + elipticallyTruncate(h.Text, 20) + "\"" + } + return "", "" + } + + var mounts []string + + for k := range sc.Web[hp].Handlers { + mounts = append(mounts, k) + } + sort.Slice(mounts, func(i, j int) bool { + return len(mounts[i]) < len(mounts[j]) + }) + + dnsName := strings.TrimSuffix(st.Self.DNSName, ".") + + for _, m := range mounts { + h := sc.Web[hp].Handlers[m] + t, d := srvTypeAndDesc(h) + + output.WriteString(fmt.Sprintf("%s://%s%s%s (%s)\n", scheme, dnsName, portPart, m, fStatus)) + output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d)) + } + + return output.String(), nil +} + +func (e *serveEnv) tcpStatusTree(sc *ipn.ServeConfig, st *ipnstate.Status) string { + var output strings.Builder + + dnsName := strings.TrimSuffix(st.Self.DNSName, ".") + for p, h := range sc.TCP { + if h.TCPForward == "" { + continue + } + hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(p)))) + tlsStatus := "TLS over TCP" + if h.TerminateTLS != "" { + tlsStatus = "TLS terminated" + } + fStatus := "tailnet only" + if sc.AllowFunnel[hp] { + fStatus = "public" + } + output.WriteString(fmt.Sprintf("tcp://%s (%s, %s)\n", hp, tlsStatus, fStatus)) + for _, a := range st.TailscaleIPs { + ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(p))) + output.WriteString(fmt.Sprintf("|-- tcp://%s\n", ipp)) + } + output.WriteString(fmt.Sprintf("|--> tcp://%s\n\n", h.TCPForward)) + } + return output.String() +} + // expandProxyTargetDev expands the supported target values to be proxied // allowing for input values to be a port number, a partial URL, or a full URL // including a path.