mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-04 20:12:16 +02:00
cmd/tailscale/cli,client,ipn: add appc-routes cli command
Allow the user to access information about routes an app connector has learned, such as how many routes for each domain. Fixes tailscale/corp#32624 Signed-off-by: Fran Bull <fran@tailscale.com>
This commit is contained in:
parent
976389c0f7
commit
65d6c80695
@ -27,6 +27,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/appc"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
@ -1374,3 +1375,11 @@ func (lc *Client) ShutdownTailscaled(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/shutdown", 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (lc *Client) GetAppConnectorRouteInfo(ctx context.Context) (appc.RouteInfo, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/appc-route-info")
|
||||
if err != nil {
|
||||
return appc.RouteInfo{}, err
|
||||
}
|
||||
return decodeJSON[appc.RouteInfo](body)
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
google.golang.org/protobuf/runtime/protoimpl from github.com/prometheus/client_model/go+
|
||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/client/local
|
||||
💣 tailscale.com/atomicfile from tailscale.com/cmd/derper+
|
||||
tailscale.com/client/local from tailscale.com/derp/derpserver
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/local
|
||||
@ -151,6 +152,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/eventbus from tailscale.com/net/netmon+
|
||||
tailscale.com/util/execqueue from tailscale.com/appc
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/lineiter from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/health+
|
||||
|
@ -769,7 +769,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
sigs.k8s.io/yaml from k8s.io/apimachinery/pkg/runtime/serializer/json+
|
||||
sigs.k8s.io/yaml/goyaml.v2 from sigs.k8s.io/yaml+
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/local from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/k8s-operator+
|
||||
|
153
cmd/tailscale/cli/appcroutes.go
Normal file
153
cmd/tailscale/cli/appcroutes.go
Normal file
@ -0,0 +1,153 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/appc"
|
||||
)
|
||||
|
||||
var appcRoutesArgs struct {
|
||||
all bool
|
||||
domainMap bool
|
||||
n bool
|
||||
}
|
||||
|
||||
var appcRoutesCmd = &ffcli.Command{
|
||||
Name: "appc-routes",
|
||||
ShortUsage: "tailscale appc-routes",
|
||||
Exec: runAppcRoutesInfo,
|
||||
ShortHelp: "Print the current app connector routes",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("appc-routes")
|
||||
fs.BoolVar(&appcRoutesArgs.all, "all", false, "Print learned domains and routes and extra policy configured routes.")
|
||||
fs.BoolVar(&appcRoutesArgs.domainMap, "map", false, "Print the map of learned domains: [routes].")
|
||||
fs.BoolVar(&appcRoutesArgs.n, "n", false, "Print the total number of routes this node advertises.")
|
||||
return fs
|
||||
})(),
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The 'tailscale appc-routes' command prints the current App Connector route status.
|
||||
|
||||
By default this command prints the domains configured in the app connector configuration and how many routes have been
|
||||
learned for each domain.
|
||||
|
||||
--all prints the routes learned from the domains configured in the app connector configuration; and any extra routes provided
|
||||
in the the policy app connector 'routes' field.
|
||||
|
||||
--map prints the routes learned from the domains configured in the app connector configuration.
|
||||
|
||||
-n prints the total number of routes advertised by this device, whether learned, set in the policy, or set locally.
|
||||
|
||||
For more information about App Connectors, refer to
|
||||
https://tailscale.com/kb/1281/app-connectors
|
||||
`),
|
||||
}
|
||||
|
||||
func getAllOutput(ri *appc.RouteInfo) (string, error) {
|
||||
domains, err := json.MarshalIndent(ri.Domains, " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
control, err := json.MarshalIndent(ri.Control, " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s := fmt.Sprintf(`Learned Routes
|
||||
==============
|
||||
%s
|
||||
|
||||
Routes from Policy
|
||||
==================
|
||||
%s
|
||||
`, domains, control)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type domainCount struct {
|
||||
domain string
|
||||
count int
|
||||
}
|
||||
|
||||
func getSummarizeLearnedOutput(ri *appc.RouteInfo) string {
|
||||
x := make([]domainCount, len(ri.Domains))
|
||||
i := 0
|
||||
maxDomainWidth := 0
|
||||
for k, v := range ri.Domains {
|
||||
if len(k) > maxDomainWidth {
|
||||
maxDomainWidth = len(k)
|
||||
}
|
||||
x[i] = domainCount{domain: k, count: len(v)}
|
||||
i++
|
||||
}
|
||||
slices.SortFunc(x, func(i, j domainCount) int {
|
||||
if i.count > j.count {
|
||||
return -1
|
||||
}
|
||||
if i.count < j.count {
|
||||
return 1
|
||||
}
|
||||
if i.domain > j.domain {
|
||||
return 1
|
||||
}
|
||||
if i.domain < j.domain {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
s := ""
|
||||
fmtString := fmt.Sprintf("%%-%ds %%d\n", maxDomainWidth) // eg "%-10s %d\n"
|
||||
for _, dc := range x {
|
||||
s += fmt.Sprintf(fmtString, dc.domain, dc.count)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func runAppcRoutesInfo(ctx context.Context, args []string) error {
|
||||
prefs, err := localClient.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !prefs.AppConnector.Advertise {
|
||||
fmt.Println("not a connector")
|
||||
return nil
|
||||
}
|
||||
|
||||
if appcRoutesArgs.n {
|
||||
fmt.Println(len(prefs.AdvertiseRoutes))
|
||||
return nil
|
||||
}
|
||||
|
||||
routeInfo, err := localClient.GetAppConnectorRouteInfo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if appcRoutesArgs.domainMap {
|
||||
domains, err := json.Marshal(routeInfo.Domains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(domains))
|
||||
return nil
|
||||
}
|
||||
|
||||
if appcRoutesArgs.all {
|
||||
s, err := getAllOutput(&routeInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Print(getSummarizeLearnedOutput(&routeInfo))
|
||||
return nil
|
||||
}
|
@ -276,6 +276,7 @@ change in the future.
|
||||
idTokenCmd,
|
||||
configureHostCmd(),
|
||||
systrayCmd,
|
||||
appcRoutesCmd,
|
||||
),
|
||||
FlagSet: rootfs,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
|
@ -70,6 +70,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
software.sslmate.com/src/go-pkcs12 from tailscale.com/cmd/tailscale/cli
|
||||
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/client/local+
|
||||
💣 tailscale.com/atomicfile from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/local from tailscale.com/client/tailscale+
|
||||
L tailscale.com/client/systray from tailscale.com/cmd/tailscale/cli
|
||||
@ -168,6 +169,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/eventbus from tailscale.com/client/local+
|
||||
tailscale.com/util/execqueue from tailscale.com/appc
|
||||
tailscale.com/util/groupmember from tailscale.com/client/web
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
|
@ -51,7 +51,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 go4.org/mem from tailscale.com/control/controlbase+
|
||||
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnauth+
|
||||
tailscale.com/clientupdate from tailscale.com/ipn/ipnlocal+
|
||||
|
@ -240,7 +240,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/client/local from tailscale.com/client/web+
|
||||
|
@ -211,7 +211,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/local from tailscale.com/client/web+
|
||||
tailscale.com/client/tailscale from tailscale.com/internal/client/tailscale
|
||||
|
@ -7124,6 +7124,15 @@ func (b *LocalBackend) readRouteInfoLocked() (*appc.RouteInfo, error) {
|
||||
return ri, nil
|
||||
}
|
||||
|
||||
// ReadRouteInfo returns the app connector route information that is
|
||||
// stored in prefs to be consistent across restarts. It should be up
|
||||
// to date with the RouteInfo in memory being used by appc.
|
||||
func (b *LocalBackend) ReadRouteInfo() (*appc.RouteInfo, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.readRouteInfoLocked()
|
||||
}
|
||||
|
||||
// seamlessRenewalEnabled reports whether seamless key renewals are enabled.
|
||||
//
|
||||
// As of 2025-09-11, this is the default behaviour unless nodes receive
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/appc"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
@ -73,6 +74,7 @@ var handler = map[string]LocalAPIHandler{
|
||||
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
||||
// without a trailing slash:
|
||||
"alpha-set-device-attrs": (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690
|
||||
"appc-route-info": (*Handler).serveGetAppcRouteInfo,
|
||||
"bugreport": (*Handler).serveBugReport,
|
||||
"check-ip-forwarding": (*Handler).serveCheckIPForwarding,
|
||||
"check-prefs": (*Handler).serveCheckPrefs,
|
||||
@ -2111,3 +2113,21 @@ func (h *Handler) serveShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
eventbus.Publish[Shutdown](ec).Publish(Shutdown{})
|
||||
}
|
||||
|
||||
func (h *Handler) serveGetAppcRouteInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
res, err := h.b.ReadRouteInfo()
|
||||
if err != nil {
|
||||
if errors.Is(err, ipn.ErrStateNotExist) {
|
||||
res = &appc.RouteInfo{}
|
||||
} else {
|
||||
WriteErrorJSON(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/local from tailscale.com/client/web+
|
||||
tailscale.com/client/tailscale from tailscale.com/internal/client/tailscale
|
||||
|
Loading…
x
Reference in New Issue
Block a user