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 contains types for the Tailscale local API and control plane API.
|
||||||
package apitype
|
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.
|
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
|
||||||
type WhoIsResponse struct {
|
type WhoIsResponse struct {
|
||||||
|
@ -30,3 +34,34 @@ type WaitingFile struct {
|
||||||
Name string
|
Name string
|
||||||
Size int64
|
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
|
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
|
// SetComponentDebugLogging sets component's debug logging enabled for
|
||||||
// the provided duration. If the duration is in the past, the debug logging
|
// the provided duration. If the duration is in the past, the debug logging
|
||||||
// is disabled.
|
// is disabled.
|
||||||
|
|
|
@ -108,6 +108,11 @@ var debugCmd = &ffcli.Command{
|
||||||
Exec: localAPIAction("rebind"),
|
Exec: localAPIAction("rebind"),
|
||||||
ShortHelp: "force a magicsock rebind",
|
ShortHelp: "force a magicsock rebind",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "subnet-router",
|
||||||
|
Exec: runDebugSubnetRouter,
|
||||||
|
ShortHelp: "debug connectivity to a host through a subnet router",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "prefs",
|
Name: "prefs",
|
||||||
Exec: runPrefs,
|
Exec: runPrefs,
|
||||||
|
@ -546,3 +551,17 @@ func runDebugComponentLogs(ctx context.Context, args []string) error {
|
||||||
}
|
}
|
||||||
return nil
|
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"
|
"go4.org/netipx"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
"tailscale.com/doctor"
|
"tailscale.com/doctor"
|
||||||
|
@ -3724,6 +3725,193 @@ func (b *LocalBackend) DebugReSTUN() error {
|
||||||
return nil
|
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) {
|
func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
|
||||||
ig, ok := b.e.(wgengine.InternalsGetter)
|
ig, ok := b.e.(wgengine.InternalsGetter)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -59,6 +59,7 @@ var handler = map[string]localAPIHandler{
|
||||||
"check-prefs": (*Handler).serveCheckPrefs,
|
"check-prefs": (*Handler).serveCheckPrefs,
|
||||||
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
||||||
"debug": (*Handler).serveDebug,
|
"debug": (*Handler).serveDebug,
|
||||||
|
"debug-subnet-route": (*Handler).serveDebugSubnetRoute,
|
||||||
"derpmap": (*Handler).serveDERPMap,
|
"derpmap": (*Handler).serveDERPMap,
|
||||||
"dial": (*Handler).serveDial,
|
"dial": (*Handler).serveDial,
|
||||||
"file-targets": (*Handler).serveFileTargets,
|
"file-targets": (*Handler).serveFileTargets,
|
||||||
|
@ -417,6 +418,26 @@ func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Requ
|
||||||
json.NewEncoder(w).Encode(res)
|
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,
|
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
|
||||||
// for platforms where we want to link it in.
|
// for platforms where we want to link it in.
|
||||||
var serveProfileFunc func(http.ResponseWriter, *http.Request)
|
var serveProfileFunc func(http.ResponseWriter, *http.Request)
|
||||||
|
|
|
@ -1289,7 +1289,11 @@ func (e *userspaceEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, cb func
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
peer := pip.Node
|
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)
|
e.logf("ping(%v): sending %v ping to %v %v ...", ip, pingType, peer.Key.ShortString(), peer.ComputedName)
|
||||||
switch pingType {
|
switch pingType {
|
||||||
case "disco":
|
case "disco":
|
||||||
|
@ -1297,7 +1301,7 @@ func (e *userspaceEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, cb func
|
||||||
case "TSMP":
|
case "TSMP":
|
||||||
e.sendTSMPPing(ip, peer, res, cb)
|
e.sendTSMPPing(ip, peer, res, cb)
|
||||||
case "ICMP":
|
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")
|
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)
|
srcIP, err := e.mySelfIPMatchingFamily(destIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
res.Err = err.Error()
|
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)) {
|
func (e *watchdogEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, cb func(*ipnstate.PingResult)) {
|
||||||
e.watchdog("Ping", func() { e.wrap.Ping(ip, pingType, cb) })
|
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) {
|
func (e *watchdogEngine) RegisterIPPortIdentity(ipp netip.AddrPort, tsIP netip.Addr) {
|
||||||
e.watchdog("RegisterIPPortIdentity", func() { e.wrap.RegisterIPPortIdentity(ipp, tsIP) })
|
e.watchdog("RegisterIPPortIdentity", func() { e.wrap.RegisterIPPortIdentity(ipp, tsIP) })
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,6 +158,8 @@ type Engine interface {
|
||||||
// then call cb with its ping latency & method.
|
// then call cb with its ping latency & method.
|
||||||
Ping(ip netip.Addr, pingType tailcfg.PingType, cb func(*ipnstate.PingResult))
|
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
|
// RegisterIPPortIdentity registers a given node (identified by its
|
||||||
// Tailscale IP) as temporarily having the given IP:port for whois lookups.
|
// 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
|
// The IP:port is generally a localhost IP and an ephemeral port, used
|
||||||
|
|
Loading…
Reference in New Issue