Files
tailscale/net/routecheck/report.go
T
Simon Law cb7e536804 feature/routecheck,ipn/routecheck: probe reachability in the background
Previously, refreshing the routecheck.Client would probe to generate a
new routecheck.Report, but this method was only wired up to the
LocalAPI and the `tailscale routecheck` command. However, waiting for
a probe to finish before choosing a router would take too long, so we
must keep a regularly updated report to be consulted as necessary.

This patch adds a Start and Close method to the routecheck.Client and
starts it in the background from features/routecheck. To enable this
feature for a given node, set both of the following node attributes:
`client-side-reachability` and `client-side-reachability-routecheck`.

This patch also wires up the RouterTracker.OnRoutersChange hook, which
fires a callback whenever a new network map includes information about
a router node, This signals to the routecheck.Client that it might
need to schedule another probe, if the shape of the routing table has
changed materially.

Updates #17366
Updates tailscale/corp#33033

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-07-02 20:26:27 -07:00

153 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package routecheck
import (
"cmp"
"iter"
"maps"
"net/netip"
"slices"
"time"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
jsonv1 "github.com/go-json-experiment/json/v1"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
)
var (
metricReport = clientmetric.NewCounter("routecheck_report")
)
// Report returns the latest reachability report.
// It returns nil if a report isnt available, which happens during initialization.
func (c *Client) Report() *Report {
metricReport.Add(1)
return c.report.Load()
}
// Report contains the result of a single routecheck.
type Report struct {
// Done is the time when the report was finished.
Done time.Time `json:"done"`
// Reachable is the set of nodes that were reachable from the current host
// when this report was compiled. Missing nodes may or may not be reachable.
Reachable NodeSet `json:"reachable"`
// LastProbed tracks the last time a given node was probed.
// This is used to rate-limit reachability probing, so an entrys
// presence doesnt imply that it is reachable.
LastProbed map[tailcfg.NodeID]time.Time `json:"-"` // not marshaled
}
// RoutablePrefixes returns a map of routable network prefixes associated with
// each prefixs routers that were reachable by the current host,
// at the time the report was finished.
// Each slice of routers are ordered by their node ID.
//
// Note: Fallback routes are not supported by design. If a subnet prefix
// contained within another more general prefix has no reachable routers,
// traffic is still sent to one of those unreachable routers.
// Routers for the general prefix arent candidates. See tailscale/tailscale#18550.
func (rp Report) RoutablePrefixes() RoutablePrefixes {
var out map[netip.Prefix][]Node
for _, n := range rp.Reachable {
for _, p := range n.Routes {
mak.Set(&out, p, append(out[p], n))
}
}
for p := range out {
slices.SortFunc(out[p], Node.Compare)
}
return out
}
// Node represents a node in the reachability report.
type Node struct {
ID tailcfg.NodeID `json:"id"`
// Name is the FQDN of the node.
// It is also the MagicDNS name for the node.
// It has a trailing dot.
// e.g. "host.tail-scale.ts.net."
Name string `json:"name"`
// Addr is the IP address that was probed.
Addr netip.Addr `json:"addr"`
// Routes are the subnets that the node will route.
Routes []netip.Prefix `json:"routes"`
}
// Compare returns an integer comparing two nodes, ordered by their node ID.
// The result will be 0 if n.ID == n2.ID, -1 if n.ID < n2.ID, and +1 if n.ID > n2.ID.
func (n Node) Compare(n2 Node) int {
return cmp.Compare(n.ID, n2.ID)
}
// NodeSet is a set of nodes keyed by node ID, so duplicates are easily detected.
// To prevent stuttering, it marshals itself as a JSON array, sorted by node ID.
type NodeSet map[tailcfg.NodeID]Node
var (
_ jsonv1.Marshaler = &NodeSet{}
_ jsonv1.Unmarshaler = &NodeSet{}
_ jsonv2.MarshalerTo = &NodeSet{}
_ jsonv2.UnmarshalerFrom = &NodeSet{}
)
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (ns NodeSet) MarshalJSONTo(enc *jsontext.Encoder) error {
nodes := slices.SortedFunc(maps.Values(ns), Node.Compare)
return jsonv2.MarshalEncode(enc, nodes)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (ns *NodeSet) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
var nodes []Node
if err := jsonv2.UnmarshalDecode(dec, &nodes); err != nil {
return err
}
if *ns == nil {
*ns = make(NodeSet, len(nodes))
}
for _, n := range nodes {
(*ns)[n.ID] = n
}
return nil
}
// MarshalJSON implements [jsonv1.Marshaler].
func (ns *NodeSet) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(ns, jsonv1.DefaultOptionsV1())
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (ns *NodeSet) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, ns, jsonv1.DefaultOptionsV1())
}
// RoutablePrefixes is a map of routers,
// keyed by the network prefix for which they route.
type RoutablePrefixes map[netip.Prefix][]Node
// Sorted returns an iterator over the map of routers,
// ordered by the network prefix as described in [netip.Prefix.Compare].
func (rt RoutablePrefixes) Sorted() iter.Seq2[netip.Prefix, []Node] {
return func(yield func(netip.Prefix, []Node) bool) {
prefixes := slices.SortedFunc(maps.Keys(rt), netip.Prefix.Compare)
for _, p := range prefixes {
if !yield(p, rt[p]) {
return
}
}
}
}