cmd/tailscale/cli: Add output for services to serve status

This commit rewrote a part of runServeStatus and added services info to
status subcomand.

Fixes tailscale/corp#22954

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
KevinLiang10 2025-03-16 22:28:50 -04:00
parent 793ca2fb43
commit 1103609548
2 changed files with 138 additions and 64 deletions

View File

@ -12,6 +12,7 @@ import (
"io"
"log"
"net"
"net/netip"
"net/url"
"os"
"path"
@ -625,7 +626,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
return nil
}
printFunnelStatus(ctx)
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0 && len(sc.Services) == 0) {
printf("No serve config\n")
return nil
}
@ -633,26 +634,67 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
if err != nil {
return err
}
if sc.IsTCPForwardingAny() {
if err := printTCPStatusTree(ctx, sc, st); err != nil {
return err
}
nodeDNSName := strings.TrimSuffix(st.Self.DNSName, ".")
if sc.IsNodeTCPForwardingAny() {
printTCPStatusTree(sc.TCP, nodeDNSName, st.TailscaleIPs, sc.AllowFunnel)
printf("\n")
}
for hp := range sc.Web {
err := e.printWebStatusTree(sc, hp)
if sc.IsNodeServingWeb() {
err := printWebStatusTree(sc, nodeDNSName)
if err != nil {
return err
}
printf("\n")
}
if sc.HostingServices() {
if err := printServicesStatusTree(e, sc, st); err != nil {
return err
}
}
printFunnelWarning(sc)
return nil
}
func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status) error {
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
for p, h := range sc.TCP {
func printServicesStatusTree(e *serveEnv, sc *ipn.ServeConfig, st *ipnstate.Status) error {
print("Services:\n")
if st.Self == nil || st.Self.CapMap == nil {
return nil
}
var svcIPMap tailcfg.ServiceIPMappings
serviceIPMaps, err := tailcfg.UnmarshalNodeCapJSON[tailcfg.ServiceIPMappings](st.Self.CapMap, tailcfg.NodeAttrServiceHost)
if len(serviceIPMaps) != 1 || err != nil {
if len(serviceIPMaps) != 0 {
fmt.Fprintln(e.stderr(), "error: unable to get service IP mappings")
}
svcIPMap = nil
print("No service have IPs assigned yet.\n")
print("They're either partially configured, not advertised or not approved.\n")
} else {
svcIPMap = serviceIPMaps[0]
}
print("\n")
for svcName, svcCfg := range sc.Services {
print(svcName.String() + ":\n")
host := svcName.WithoutPrefix() + "." + st.CurrentTailnet.MagicDNSSuffix
if ips, ok := svcIPMap[svcName]; ok {
printTCPStatusTree(svcCfg.TCP, host, ips, nil)
} else {
if svcIPMap != nil {
print("Service doesn't have IPs assigned yet.\n")
print("It's either partially configured, not advertised or not approved.\n")
}
printTCPStatusTree(svcCfg.TCP, host, nil, nil)
}
printWebStatusTree(sc, svcName.String())
print("\n")
}
return nil
}
func printTCPStatusTree(tcpMap map[uint16]*ipn.TCPPortHandler, dnsName string, ips []netip.Addr, allowFunnel map[ipn.HostPort]bool) {
for p, h := range tcpMap {
if h.TCPForward == "" {
continue
}
@ -662,72 +704,81 @@ func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.S
tlsStatus = "TLS terminated"
}
fStatus := "tailnet only"
if sc.AllowFunnel[hp] {
if allowFunnel[hp] {
fStatus = "Funnel on"
}
printf("|-- tcp://%s (%s, %s)\n", hp, tlsStatus, fStatus)
for _, a := range st.TailscaleIPs {
for _, a := range ips {
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(p)))
printf("|-- tcp://%s\n", ipp)
}
printf("|--> tcp://%s\n", h.TCPForward)
}
return nil
}
func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) error {
// No-op if no serve config
if sc == nil {
return nil
}
fStatus := "tailnet only"
if sc.AllowFunnel[hp] {
fStatus = "Funnel on"
}
host, 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, host) {
scheme = "http"
}
portPart := ":" + portStr
if scheme == "http" && portStr == "80" ||
scheme == "https" && portStr == "443" {
portPart = ""
}
if scheme == "http" {
hostname, _, _ := strings.Cut(host, ".")
printf("%s://%s%s (%s)\n", scheme, hostname, portPart, fStatus)
}
printf("%s://%s%s (%s)\n", scheme, host, portPart, fStatus)
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) + "\""
func printWebStatusTree(sc *ipn.ServeConfig, dnsName string) error {
webMap := sc.Web
forService := ipn.IsServiceName(dnsName)
if forService {
svcName := tailcfg.ServiceName(dnsName)
if svc, ok := sc.Services[svcName]; ok && svc != nil {
webMap = svc.Web
} else {
// If the service is not found, just silently skip it.
return nil
}
return "", ""
}
for hp := range webMap {
fStatus := "tailnet only"
if !forService && sc.AllowFunnel[hp] {
fStatus = "Funnel on"
}
host, portStr, _ := net.SplitHostPort(string(hp))
mounts := slicesx.MapKeys(sc.Web[hp].Handlers)
sort.Slice(mounts, func(i, j int) bool {
return len(mounts[i]) < len(mounts[j])
})
maxLen := len(mounts[len(mounts)-1])
port, err := parseServePort(portStr)
if err != nil {
return fmt.Errorf("invalid port %q: %w", portStr, err)
}
for _, m := range mounts {
h := sc.Web[hp].Handlers[m]
t, d := srvTypeAndDesc(h)
printf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d)
scheme := "https"
if sc.IsServingHTTP(port, dnsName) {
scheme = "http"
}
portPart := ":" + portStr
if scheme == "http" && portStr == "80" ||
scheme == "https" && portStr == "443" {
portPart = ""
}
if scheme == "http" {
hostname, _, _ := strings.Cut(host, ".")
printf("%s://%s%s (%s)\n", scheme, hostname, portPart, fStatus)
}
printf("%s://%s%s (%s)\n", scheme, host, portPart, fStatus)
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 "", ""
}
mounts := slicesx.MapKeys(webMap[hp].Handlers)
sort.Slice(mounts, func(i, j int) bool {
return len(mounts[i]) < len(mounts[j])
})
maxLen := len(mounts[len(mounts)-1])
for _, m := range mounts {
h := webMap[hp].Handlers[m]
t, d := srvTypeAndDesc(h)
printf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d)
}
print("\n")
}
return nil

View File

@ -230,9 +230,9 @@ func (sc *ServeConfig) HasPathHandler() bool {
return false
}
// IsTCPForwardingAny reports whether ServeConfig is currently forwarding in
// IsNodeTCPForwardingAny reports whether ServeConfig is currently forwarding in
// TCPForward mode on any port. This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingAny() bool {
func (sc *ServeConfig) IsNodeTCPForwardingAny() bool {
if sc == nil || len(sc.TCP) == 0 {
return false
}
@ -244,6 +244,29 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
return false
}
func (sc *ServeConfig) IsNodeServingWeb() bool {
if sc == nil || len(sc.Web) == 0 {
return false
}
for _, webServerConfig := range sc.Web {
if len(webServerConfig.Handlers) > 0 {
return true
}
}
return false
}
func (sc *ServeConfig) HostingServices() bool {
if sc == nil || len(sc.Services) == 0 {
return false
}
for _, svc := range sc.Services {
if len(svc.TCP) > 0 || len(svc.Web) > 0 || svc.Tun {
return true
}
}
return false
}
// IsServiceName reports whether if the given string is a valid service name.
func IsServiceName(s string) bool {
return tailcfg.ServiceName(s).Validate() == nil