Compare commits
1 Commits
main
...
andrew/deb
Author | SHA1 | Date |
---|---|---|
![]() |
d8b9698eaa |
|
@ -5,7 +5,11 @@
|
|||
// Package apitype contains types for the Tailscale local API and control plane API.
|
||||
package apitype
|
||||
|
||||
import "tailscale.com/tailcfg"
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
|
||||
type WhoIsResponse struct {
|
||||
|
@ -30,3 +34,34 @@ type WaitingFile struct {
|
|||
Name string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// TODO: docs
|
||||
type SubnetRouteDebugResponse struct {
|
||||
InputAddr string
|
||||
Addresses []SubnetRouteDebugAddress
|
||||
Nodes []SubnetRouteDebugNode
|
||||
Errors []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type SubnetRouteDebugAddress struct {
|
||||
Addr netip.Addr
|
||||
Source string
|
||||
}
|
||||
|
||||
type SubnetRouteDebugPingResponse struct {
|
||||
IP netip.Addr
|
||||
Err string `json:",omitempty"`
|
||||
LatencySeconds float64 `json:",omitempty"`
|
||||
}
|
||||
|
||||
// TODO: docs
|
||||
type SubnetRouteDebugNode struct {
|
||||
StableID tailcfg.StableNodeID
|
||||
Name string
|
||||
AllowedIPs []netip.Prefix
|
||||
Primary []netip.Prefix `json:",omitempty"`
|
||||
Online string
|
||||
IsExitNode bool
|
||||
DiscoPing *SubnetRouteDebugPingResponse `json:",omitempty"`
|
||||
ICMPPing *SubnetRouteDebugPingResponse `json:",omitempty"`
|
||||
}
|
||||
|
|
|
@ -348,6 +348,22 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: docs
|
||||
func (lc *LocalClient) DebugSubnetRoute(ctx context.Context, addr string) (*apitype.SubnetRouteDebugResponse, error) {
|
||||
urlvals := make(url.Values)
|
||||
urlvals.Set("addr", addr)
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-subnet-route?"+urlvals.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
var res apitype.SubnetRouteDebugResponse
|
||||
if err := json.Unmarshal(body, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// SetComponentDebugLogging sets component's debug logging enabled for
|
||||
// the provided duration. If the duration is in the past, the debug logging
|
||||
// is disabled.
|
||||
|
|
|
@ -108,6 +108,11 @@ var debugCmd = &ffcli.Command{
|
|||
Exec: localAPIAction("rebind"),
|
||||
ShortHelp: "force a magicsock rebind",
|
||||
},
|
||||
{
|
||||
Name: "subnet-router",
|
||||
Exec: runDebugSubnetRouter,
|
||||
ShortHelp: "debug connectivity to a host through a subnet router",
|
||||
},
|
||||
{
|
||||
Name: "prefs",
|
||||
Exec: runPrefs,
|
||||
|
@ -546,3 +551,17 @@ func runDebugComponentLogs(ctx context.Context, args []string) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDebugSubnetRouter(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: debug subnet-router <hostname-or-ipv6>")
|
||||
}
|
||||
|
||||
s, err := localClient.DebugSubnetRoute(ctx, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j, _ := json.MarshalIndent(s, "", "\t")
|
||||
outln(string(j))
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"go4.org/netipx"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/doctor"
|
||||
|
@ -3724,6 +3725,193 @@ func (b *LocalBackend) DebugReSTUN() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) DebugSubnetRoute(ctx context.Context, addr string) (*apitype.SubnetRouteDebugResponse, error) {
|
||||
b.mu.Lock()
|
||||
nm := b.netMap
|
||||
b.mu.Unlock()
|
||||
|
||||
if nm == nil {
|
||||
return nil, errors.New("no netmap")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out := &apitype.SubnetRouteDebugResponse{
|
||||
InputAddr: addr,
|
||||
}
|
||||
|
||||
returnWithError := func(format string, args ...any) (*apitype.SubnetRouteDebugResponse, error) {
|
||||
if len(format) > 0 && format[len(format)-1] != '\n' {
|
||||
format += "\n"
|
||||
}
|
||||
out.Errors = append(out.Errors, fmt.Sprintf(format, args...))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(addr)
|
||||
if err != nil {
|
||||
// Try resolving the address using both the Go and platform-specific resolver
|
||||
var addrs []apitype.SubnetRouteDebugAddress
|
||||
for _, preferGo := range []bool{true, false} {
|
||||
resolver := net.Resolver{PreferGo: preferGo}
|
||||
ips, err := resolver.LookupNetIP(ctx, "ip", addr)
|
||||
if err != nil {
|
||||
return returnWithError("error resolving address %q: %v", addr, err)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
addrs = append(addrs, apitype.SubnetRouteDebugAddress{
|
||||
Addr: ip.Unmap(),
|
||||
Source: fmt.Sprintf("net.Resolver{PreferGo: %v}", preferGo),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the first IP, since we always expect it.
|
||||
// TODO: try all IPs?
|
||||
out.Addresses = addrs
|
||||
ip = addrs[0].Addr
|
||||
} else {
|
||||
out.Addresses = []apitype.SubnetRouteDebugAddress{{
|
||||
Addr: ip,
|
||||
Source: "user",
|
||||
}}
|
||||
}
|
||||
ip = ip.Unmap()
|
||||
|
||||
// Try to determine which subnet router is routing this address.
|
||||
type nodeWithMatching struct {
|
||||
*tailcfg.Node
|
||||
MatchingAllowedIPs []netip.Prefix
|
||||
}
|
||||
var ns []nodeWithMatching
|
||||
for _, peer := range nm.Peers {
|
||||
curr := nodeWithMatching{Node: peer}
|
||||
for _, allowedip := range peer.AllowedIPs {
|
||||
if !allowedip.Contains(ip) {
|
||||
continue
|
||||
}
|
||||
curr.MatchingAllowedIPs = append(curr.MatchingAllowedIPs, allowedip)
|
||||
}
|
||||
|
||||
if len(curr.MatchingAllowedIPs) > 0 {
|
||||
ns = append(ns, curr)
|
||||
}
|
||||
}
|
||||
if len(ns) == 0 {
|
||||
return returnWithError("this node has no peers advertising a route for %s", ip)
|
||||
}
|
||||
|
||||
// For each possible subnet router, check the status.
|
||||
type nodeRes struct {
|
||||
dn apitype.SubnetRouteDebugNode
|
||||
err error
|
||||
}
|
||||
nodeResults := make(chan nodeRes, len(ns))
|
||||
grp, grpCtx := errgroup.WithContext(ctx)
|
||||
grp.SetLimit(5)
|
||||
for _, node := range ns {
|
||||
node := node // capture loop variable
|
||||
grp.Go(func() error {
|
||||
var retErr error
|
||||
dn := apitype.SubnetRouteDebugNode{
|
||||
StableID: node.StableID,
|
||||
Name: node.Name,
|
||||
AllowedIPs: node.MatchingAllowedIPs,
|
||||
}
|
||||
defer func() {
|
||||
nodeResults <- nodeRes{dn, retErr}
|
||||
}()
|
||||
|
||||
// Check PrimaryRoutes
|
||||
for _, pref := range node.PrimaryRoutes {
|
||||
if pref.Contains(ip) {
|
||||
dn.Primary = append(dn.Primary, pref)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for exit node
|
||||
if tsaddr.ContainsExitRoutes(node.AllowedIPs) {
|
||||
dn.IsExitNode = true
|
||||
}
|
||||
|
||||
// Do online checks after gathering all data that doesn't
|
||||
// require an interaction, so we can 'continue' if the node
|
||||
// isn't online.
|
||||
if node.Online == nil {
|
||||
dn.Online = "unknown"
|
||||
return nil
|
||||
} else if !*node.Online {
|
||||
dn.Online = "false"
|
||||
return nil
|
||||
} else {
|
||||
dn.Online = "true"
|
||||
}
|
||||
|
||||
// Try pinging the node itself
|
||||
// TODO: try all IPs?
|
||||
// TODO: check if we have the right v4/v6 address support
|
||||
candidates := []struct {
|
||||
ty tailcfg.PingType
|
||||
res **apitype.SubnetRouteDebugPingResponse
|
||||
}{
|
||||
{tailcfg.PingDisco, &dn.DiscoPing},
|
||||
{tailcfg.PingICMP, &dn.ICMPPing},
|
||||
}
|
||||
for _, cand := range candidates {
|
||||
ip := node.Addresses[0].Addr()
|
||||
|
||||
res := &apitype.SubnetRouteDebugPingResponse{
|
||||
IP: ip,
|
||||
}
|
||||
*cand.res = res
|
||||
|
||||
pingRes := make(chan *ipnstate.PingResult, 1)
|
||||
b.e.Ping(ip, cand.ty, func(pr *ipnstate.PingResult) {
|
||||
select {
|
||||
case pingRes <- pr:
|
||||
default:
|
||||
}
|
||||
})
|
||||
select {
|
||||
case pr := <-pingRes:
|
||||
if pr.Err != "" {
|
||||
res.Err = pr.Err
|
||||
} else {
|
||||
res.LatencySeconds = pr.LatencySeconds
|
||||
}
|
||||
|
||||
case <-grpCtx.Done():
|
||||
res.Err = grpCtx.Err().Error()
|
||||
retErr = fmt.Errorf("context canceled while waiting for %s response from %v: %v", cand.ty, ip, grpCtx.Err())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
grp.Wait()
|
||||
|
||||
resultsLoop:
|
||||
for i := 0; i < len(ns); i++ {
|
||||
select {
|
||||
case res := <-nodeResults:
|
||||
if res.err != nil {
|
||||
out.Errors = append(out.Errors, res.err.Error())
|
||||
}
|
||||
out.Nodes = append(out.Nodes, res.dn)
|
||||
|
||||
// We could have finished before starting all goroutines, so we
|
||||
// need to handle the case where our channel doesn't have all
|
||||
// the responses.
|
||||
default:
|
||||
break resultsLoop
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
|
||||
ig, ok := b.e.(wgengine.InternalsGetter)
|
||||
if !ok {
|
||||
|
|
|
@ -59,6 +59,7 @@ var handler = map[string]localAPIHandler{
|
|||
"check-prefs": (*Handler).serveCheckPrefs,
|
||||
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
||||
"debug": (*Handler).serveDebug,
|
||||
"debug-subnet-route": (*Handler).serveDebugSubnetRoute,
|
||||
"derpmap": (*Handler).serveDERPMap,
|
||||
"dial": (*Handler).serveDial,
|
||||
"file-targets": (*Handler).serveFileTargets,
|
||||
|
@ -417,6 +418,26 @@ func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Requ
|
|||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugSubnetRoute(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
addr := r.FormValue("addr")
|
||||
res, err := h.b.DebugSubnetRoute(r.Context(), addr)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Errors []string
|
||||
}{
|
||||
Errors: []string{err.Error()},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
|
||||
// for platforms where we want to link it in.
|
||||
var serveProfileFunc func(http.ResponseWriter, *http.Request)
|
||||
|
|
|
@ -1289,7 +1289,11 @@ func (e *userspaceEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, cb func
|
|||
return
|
||||
}
|
||||
peer := pip.Node
|
||||
e.PingPeer(ip, peer, pingType, cb)
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) PingPeer(ip netip.Addr, peer *tailcfg.Node, pingType tailcfg.PingType, cb func(*ipnstate.PingResult)) {
|
||||
res := &ipnstate.PingResult{IP: ip.String()}
|
||||
e.logf("ping(%v): sending %v ping to %v %v ...", ip, pingType, peer.Key.ShortString(), peer.ComputedName)
|
||||
switch pingType {
|
||||
case "disco":
|
||||
|
@ -1297,7 +1301,7 @@ func (e *userspaceEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, cb func
|
|||
case "TSMP":
|
||||
e.sendTSMPPing(ip, peer, res, cb)
|
||||
case "ICMP":
|
||||
e.sendICMPEchoRequest(ip, peer, res, cb)
|
||||
e.SendICMPEchoRequest(ip, peer, res, cb)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1318,7 +1322,7 @@ func (e *userspaceEngine) mySelfIPMatchingFamily(dst netip.Addr) (src netip.Addr
|
|||
return netip.Addr{}, errors.New("no self address in netmap matching address family")
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) {
|
||||
func (e *userspaceEngine) SendICMPEchoRequest(destIP netip.Addr, peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) {
|
||||
srcIP, err := e.mySelfIPMatchingFamily(destIP)
|
||||
if err != nil {
|
||||
res.Err = err.Error()
|
||||
|
|
|
@ -169,6 +169,9 @@ func (e *watchdogEngine) DiscoPublicKey() (k key.DiscoPublic) {
|
|||
func (e *watchdogEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, cb func(*ipnstate.PingResult)) {
|
||||
e.watchdog("Ping", func() { e.wrap.Ping(ip, pingType, cb) })
|
||||
}
|
||||
func (e *watchdogEngine) PingPeer(ip netip.Addr, peer *tailcfg.Node, pingType tailcfg.PingType, cb func(*ipnstate.PingResult)) {
|
||||
e.watchdog("PingPeer", func() { e.wrap.PingPeer(ip, peer, pingType, cb) })
|
||||
}
|
||||
func (e *watchdogEngine) RegisterIPPortIdentity(ipp netip.AddrPort, tsIP netip.Addr) {
|
||||
e.watchdog("RegisterIPPortIdentity", func() { e.wrap.RegisterIPPortIdentity(ipp, tsIP) })
|
||||
}
|
||||
|
|
|
@ -158,6 +158,8 @@ type Engine interface {
|
|||
// then call cb with its ping latency & method.
|
||||
Ping(ip netip.Addr, pingType tailcfg.PingType, cb func(*ipnstate.PingResult))
|
||||
|
||||
PingPeer(ip netip.Addr, peer *tailcfg.Node, pingType tailcfg.PingType, cb func(*ipnstate.PingResult))
|
||||
|
||||
// RegisterIPPortIdentity registers a given node (identified by its
|
||||
// Tailscale IP) as temporarily having the given IP:port for whois lookups.
|
||||
// The IP:port is generally a localhost IP and an ephemeral port, used
|
||||
|
|
Loading…
Reference in New Issue