mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-05 04:21:01 +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"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/appc"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/envknob"
|
"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)
|
_, err := lc.send(ctx, "POST", "/localapi/v0/shutdown", 200, nil)
|
||||||
return err
|
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/runtime/protoimpl from github.com/prometheus/client_model/go+
|
||||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||||
tailscale.com from tailscale.com/version
|
tailscale.com from tailscale.com/version
|
||||||
|
tailscale.com/appc from tailscale.com/client/local
|
||||||
💣 tailscale.com/atomicfile from tailscale.com/cmd/derper+
|
💣 tailscale.com/atomicfile from tailscale.com/cmd/derper+
|
||||||
tailscale.com/client/local from tailscale.com/derp/derpserver
|
tailscale.com/client/local from tailscale.com/derp/derpserver
|
||||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/local
|
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
|
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||||
tailscale.com/util/eventbus from tailscale.com/net/netmon+
|
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/hashx from tailscale.com/util/deephash
|
||||||
tailscale.com/util/lineiter from tailscale.com/hostinfo+
|
tailscale.com/util/lineiter from tailscale.com/hostinfo+
|
||||||
tailscale.com/util/mak from tailscale.com/health+
|
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 from k8s.io/apimachinery/pkg/runtime/serializer/json+
|
||||||
sigs.k8s.io/yaml/goyaml.v2 from sigs.k8s.io/yaml+
|
sigs.k8s.io/yaml/goyaml.v2 from sigs.k8s.io/yaml+
|
||||||
tailscale.com from tailscale.com/version
|
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/atomicfile from tailscale.com/ipn+
|
||||||
tailscale.com/client/local from tailscale.com/client/tailscale+
|
tailscale.com/client/local from tailscale.com/client/tailscale+
|
||||||
tailscale.com/client/tailscale from tailscale.com/cmd/k8s-operator+
|
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,
|
idTokenCmd,
|
||||||
configureHostCmd(),
|
configureHostCmd(),
|
||||||
systrayCmd,
|
systrayCmd,
|
||||||
|
appcRoutesCmd,
|
||||||
),
|
),
|
||||||
FlagSet: rootfs,
|
FlagSet: rootfs,
|
||||||
Exec: func(ctx context.Context, args []string) error {
|
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 from tailscale.com/cmd/tailscale/cli
|
||||||
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
|
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
|
||||||
tailscale.com from tailscale.com/version
|
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/atomicfile from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/client/local from tailscale.com/client/tailscale+
|
tailscale.com/client/local from tailscale.com/client/tailscale+
|
||||||
L tailscale.com/client/systray from tailscale.com/cmd/tailscale/cli
|
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
|
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/util/eventbus from tailscale.com/client/local+
|
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/groupmember from tailscale.com/client/web
|
||||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
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/mem from tailscale.com/control/controlbase+
|
||||||
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com from tailscale.com/version
|
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/atomicfile from tailscale.com/ipn+
|
||||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnauth+
|
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnauth+
|
||||||
tailscale.com/clientupdate from tailscale.com/ipn/ipnlocal+
|
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/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||||
tailscale.com from tailscale.com/version
|
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/atomicfile from tailscale.com/ipn+
|
||||||
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
||||||
tailscale.com/client/local from tailscale.com/client/web+
|
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/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||||
tailscale.com from tailscale.com/version
|
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/atomicfile from tailscale.com/ipn+
|
||||||
tailscale.com/client/local from tailscale.com/client/web+
|
tailscale.com/client/local from tailscale.com/client/web+
|
||||||
tailscale.com/client/tailscale from tailscale.com/internal/client/tailscale
|
tailscale.com/client/tailscale from tailscale.com/internal/client/tailscale
|
||||||
|
@ -7124,6 +7124,15 @@ func (b *LocalBackend) readRouteInfoLocked() (*appc.RouteInfo, error) {
|
|||||||
return ri, nil
|
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.
|
// seamlessRenewalEnabled reports whether seamless key renewals are enabled.
|
||||||
//
|
//
|
||||||
// As of 2025-09-11, this is the default behaviour unless nodes receive
|
// As of 2025-09-11, this is the default behaviour unless nodes receive
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/dns/dnsmessage"
|
"golang.org/x/net/dns/dnsmessage"
|
||||||
|
"tailscale.com/appc"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/clientupdate"
|
"tailscale.com/clientupdate"
|
||||||
"tailscale.com/envknob"
|
"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
|
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
||||||
// without a trailing slash:
|
// without a trailing slash:
|
||||||
"alpha-set-device-attrs": (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690
|
"alpha-set-device-attrs": (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690
|
||||||
|
"appc-route-info": (*Handler).serveGetAppcRouteInfo,
|
||||||
"bugreport": (*Handler).serveBugReport,
|
"bugreport": (*Handler).serveBugReport,
|
||||||
"check-ip-forwarding": (*Handler).serveCheckIPForwarding,
|
"check-ip-forwarding": (*Handler).serveCheckIPForwarding,
|
||||||
"check-prefs": (*Handler).serveCheckPrefs,
|
"check-prefs": (*Handler).serveCheckPrefs,
|
||||||
@ -2111,3 +2113,21 @@ func (h *Handler) serveShutdown(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
eventbus.Publish[Shutdown](ec).Publish(Shutdown{})
|
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/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||||
tailscale.com from tailscale.com/version
|
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/atomicfile from tailscale.com/ipn+
|
||||||
tailscale.com/client/local from tailscale.com/client/web+
|
tailscale.com/client/local from tailscale.com/client/web+
|
||||||
tailscale.com/client/tailscale from tailscale.com/internal/client/tailscale
|
tailscale.com/client/tailscale from tailscale.com/internal/client/tailscale
|
||||||
|
Loading…
x
Reference in New Issue
Block a user