Compare commits

...

1 Commits

Author SHA1 Message Date
Andrew Dunham d8b9698eaa WIP
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id806c5c62b5097d9a5a7600324349ce7692d4d55
2022-10-21 11:08:39 -04:00
8 changed files with 291 additions and 3 deletions

View File

@ -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"`
}

View File

@ -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.

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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()

View File

@ -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) })
}

View File

@ -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