tsnet,client,cmd/tailscale/cli: expose service details on all clients

This commit is contained in:
Adriano Sela Aviles
2026-03-15 10:08:21 -07:00
parent 5e667007bb
commit 6e10596b04
4 changed files with 129 additions and 0 deletions

View File

@@ -686,6 +686,18 @@ func (lc *Client) status(ctx context.Context, queryString string) (*ipnstate.Sta
return decodeJSON[*ipnstate.Status](body)
}
// GetServiceDetails returns the VIP services that the control plane has
// approved this node to serve, including their names, assigned IP addresses,
// ports, and annotations (e.g. proxy service configuration). Returns nil if
// no service details are present.
func (lc *Client) GetServiceDetails(ctx context.Context) ([]*tailcfg.ServiceDetail, error) {
body, err := lc.get200(ctx, "/localapi/v0/service-details")
if err != nil {
return nil, err
}
return decodeJSON[[]*tailcfg.ServiceDetail](body)
}
// IDToken is a request to get an OIDC ID token for an audience.
// The token can be presented to any resource provider which offers OIDC
// Federation.

View File

@@ -269,6 +269,7 @@ func newRootCmd() *ffcli.Command {
nilOrCall(maybeNetlockCmd),
licensesCmd,
exitNodeCmd(),
serviceCmd(),
nilOrCall(maybeUpdateCmd),
whoisCmd,
debugCmd(),

View File

@@ -0,0 +1,103 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"sort"
"strings"
"text/tabwriter"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/tailcfg"
)
func serviceCmd() *ffcli.Command {
return &ffcli.Command{
Name: "service",
ShortUsage: "tailscale service <subcommand> [flags]",
ShortHelp: "Manage and inspect Tailscale VIP services",
Subcommands: []*ffcli.Command{
{
Name: "list",
ShortUsage: "tailscale service list [--json]",
ShortHelp: "List VIP services approved for this node",
LongHelp: strings.TrimSpace(`
The 'tailscale service list' command shows the VIP services that the control
plane has approved this node to serve, including their assigned IP addresses,
accepted ports, and any application-specific annotations.
`),
Exec: runServiceList,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("list")
fs.BoolVar(&serviceArgs.json, "json", false, "output in JSON format")
return fs
})(),
},
},
}
}
var serviceArgs struct {
json bool
}
func runServiceList(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unexpected non-flag arguments to 'tailscale service list'")
}
details, err := localClient.GetServiceDetails(ctx)
if err != nil {
return err
}
if serviceArgs.json {
enc := json.NewEncoder(Stdout)
enc.SetIndent("", " ")
enc.Encode(details)
return nil
}
if len(details) == 0 {
printf("No VIP services configured for this node.\n")
return nil
}
printServiceDetails(details)
return nil
}
func printServiceDetails(details []*tailcfg.ServiceDetail) {
w := tabwriter.NewWriter(Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "NAME\tADDRS\tPORTS\t")
fmt.Fprintln(w, "----\t-----\t-----\t")
for _, svc := range details {
addrs := make([]string, len(svc.Addrs))
for i, a := range svc.Addrs {
addrs[i] = a.String()
}
ports := make([]string, len(svc.Ports))
for i, p := range svc.Ports {
ports[i] = p.String()
}
fmt.Fprintf(w, "%s\t%s\t%s\t\n",
svc.Name,
strings.Join(addrs, ", "),
strings.Join(ports, ", "),
)
if len(svc.Annotations) > 0 {
// Print annotations sorted by key, indented under the service row.
keys := make([]string, 0, len(svc.Annotations))
for k := range svc.Annotations {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Fprintf(w, " %s: %s\t\t\t\n", k, svc.Annotations[k])
}
}
}
w.Flush()
}

View File

@@ -564,6 +564,19 @@ func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
return ip4, ip6
}
// GetServiceDetails returns the VIP services that the control plane has
// approved this node to serve, including their names, assigned IP addresses,
// ports, and application-specific annotations (e.g. proxy service
// configuration). Returns nil if the server is not running or no service
// details have been received yet.
func (s *Server) GetServiceDetails() []*tailcfg.ServiceDetail {
nm := s.lb.NetMap()
if nm == nil {
return nil
}
return nm.GetVIPServiceDetails()
}
// LogtailWriter returns an [io.Writer] that writes to Tailscale's logging service and will be only visible to Tailscale's
// support team. Logs written there cannot be retrieved by the user. This method always returns a non-nil value.
func (s *Server) LogtailWriter() io.Writer {