Previously, any peer added or removed by an incremental netmap delta was only visible to wireguard-go after a full authReconfig: wgcfg's ReconfigDevice re-installed a PeerLookupFunc closing over a freshly built map of every peer's allowed IPs, doing O(n) work per change. Instead, install the wireguard-go device hooks once, backed by live state. Engine.SetPeerConfigFunc installs a single long-lived PeerLookupFunc that queries LocalBackend's per-node RouteManager on demand, and Engine.SyncDevicePeer does O(1) per-peer device sync (remove, or update allowed IPs) as each delta mutation is applied. Full reconfigs keep an O(n peers) device sync for now, but with no lookup closure to reinstall and no removed-peer resurrection race; a later change removes full-config peer syncing entirely. The RouteManager's PeerAllowedIPs accessor backs the new hooks: its sorted output makes unchanged state a no-op update, and its peer filtering mirrors nmcfg.WGCfg, so expired peers and peers predating both DERP and disco contribute no prefixes and thus cannot be lazily created in the device, which matters because wireguard-go validates inbound source IPs against per-peer allowed IPs. The engine's SetPeerByIPPacketFunc callback is now authoritative when installed, since LocalBackend's implementation covers subnet routes and exit-node routes via the RouteManager's outbound table; the engine's own reconfig-time BART table only serves engines running without a LocalBackend. The forced authReconfig on peer add/remove stays for now: the WireGuard device no longer needs it, but OS routes, the quad-100 resolver's MagicDNS hosts map, and tstun's masquerade/jailed peer config are still derived from the full peer set. Making those delta-aware is the next step before gating it. Updates #12542 Change-Id: I3ba8c7c324bca0ad0269279d03f53b1f17fb63a2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/wgengine"
|
|
)
|
|
|
|
// lookupPeerByIP returns the node public key for the peer that should
|
|
// handle traffic to the given IP address. It is installed as the
|
|
// [wgengine.Engine.SetPeerByIPPacketFunc] callback: exact node
|
|
// addresses hit the nodeByAddr fast path, and subnet routes and
|
|
// exit-node default routes fall back to the RouteManager's outbound
|
|
// table, so it stays correct under incremental netmap deltas.
|
|
//
|
|
// It is called by wireguard-go on every outbound packet (not cached),
|
|
// so it must be fast.
|
|
func (b *LocalBackend) lookupPeerByIP(ip netip.Addr) (key.NodePublic, bool) {
|
|
nb := b.currentNode()
|
|
if nid, ok := nb.NodeByAddr(ip); ok {
|
|
peer, ok := nb.NodeByID(nid)
|
|
if !ok {
|
|
return key.NodePublic{}, false
|
|
}
|
|
return peer.Key(), true
|
|
}
|
|
if pr, ok := nb.routeMgr.Outbound().Lookup(ip); ok {
|
|
return pr.Key, true
|
|
}
|
|
return key.NodePublic{}, false
|
|
}
|
|
|
|
// peerAllowedIPs returns the prefixes from which the peer with the
|
|
// given public key is currently allowed to originate traffic, or
|
|
// ok=false if the peer is unknown (or currently routable via no
|
|
// prefix at all). It is installed as the
|
|
// [wgengine.Engine.SetPeerConfigFunc] callback, backing wireguard-go's
|
|
// lazy peer creation and per-delta peer sync.
|
|
func (b *LocalBackend) peerAllowedIPs(k key.NodePublic) (_ []netip.Prefix, ok bool) {
|
|
return b.currentNode().PeerAllowedIPs(k)
|
|
}
|
|
|
|
// resolveMagicDNS resolves a MagicDNS hostname to the owning node's IP
|
|
// address, respecting the requested network address family ("tcp4",
|
|
// "tcp6", "tcp", etc.). It accepts peer FQDNs ("foo.tail-scale.ts.net"),
|
|
// short names ("foo"), and DNS.ExtraRecords entries (service VIPs).
|
|
// The hostname must be lowercase with no trailing dot. It is installed
|
|
// as the [tsdial.Dialer.SetResolveMagicDNS] callback.
|
|
func (b *LocalBackend) resolveMagicDNS(hostname, network string) (_ netip.Addr, ok bool) {
|
|
nb := b.currentNode()
|
|
if nid, ok := nb.NodeByName(hostname); ok {
|
|
n, ok := nb.NodeByID(nid)
|
|
if !ok {
|
|
b.logf("[unexpected] resolveMagicDNS: NodeByName(%q) returned node %v but NodeByID failed", hostname, nid)
|
|
return netip.Addr{}, false
|
|
}
|
|
if ip, ok := nodeAddrForNetwork(n, network); ok {
|
|
return ip, true
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
if ip, ok := nb.ExtraDNSByName(hostname); ok && addrFamilyMatch(ip, network) {
|
|
return ip, true
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// nodeAddrForNetwork returns the best address from n for the given
|
|
// network ("tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"). For
|
|
// unqualified networks ("tcp", "udp"), it prefers IPv4.
|
|
func nodeAddrForNetwork(n tailcfg.NodeView, network string) (_ netip.Addr, ok bool) {
|
|
addrs := n.Addresses()
|
|
if addrs.Len() == 0 {
|
|
return netip.Addr{}, false
|
|
}
|
|
want4 := strings.HasSuffix(network, "4")
|
|
want6 := strings.HasSuffix(network, "6")
|
|
var v6 netip.Addr
|
|
for _, pfx := range addrs.All() {
|
|
ip := pfx.Addr()
|
|
if want4 && ip.Is4() {
|
|
return ip, true
|
|
}
|
|
if want6 && ip.Is6() {
|
|
return ip, true
|
|
}
|
|
if !want4 && !want6 {
|
|
if ip.Is4() {
|
|
return ip, true
|
|
}
|
|
if !v6.IsValid() {
|
|
v6 = ip
|
|
}
|
|
}
|
|
}
|
|
if v6.IsValid() {
|
|
return v6, true
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// addrFamilyMatch reports whether ip is compatible with the requested
|
|
// network address family.
|
|
func addrFamilyMatch(ip netip.Addr, network string) bool {
|
|
if strings.HasSuffix(network, "4") {
|
|
return ip.Is4()
|
|
}
|
|
if strings.HasSuffix(network, "6") {
|
|
return ip.Is6()
|
|
}
|
|
return true
|
|
}
|
|
|
|
// peerForIP returns which peer is responsible for a given IP address.
|
|
// Despite the name, it can also return the self node (with IsSelf set).
|
|
// It handles both Tailscale IPs (returning the owning peer or self) and
|
|
// non-Tailscale addresses like subnet-routed IPs or exit-node global
|
|
// internet IPs (returning whichever peer would route that traffic).
|
|
// It is installed as the [wgengine.Engine.SetPeerForIPFunc] callback.
|
|
func (b *LocalBackend) peerForIP(ip netip.Addr) (_ wgengine.PeerForIP, ok bool) {
|
|
nb := b.currentNode()
|
|
|
|
if tsaddr.IsTailscaleIP(ip) {
|
|
if nid, ok := nb.NodeByAddr(ip); ok {
|
|
n, ok := nb.NodeByID(nid)
|
|
if !ok {
|
|
b.logf("[unexpected] peerForIP: NodeByAddr(%v) returned node %v but NodeByID failed", ip, nid)
|
|
return wgengine.PeerForIP{}, false
|
|
}
|
|
self := nb.Self()
|
|
return wgengine.PeerForIP{
|
|
Node: n,
|
|
IsSelf: self.Valid() && self.ID() == nid,
|
|
Route: netip.PrefixFrom(ip, ip.BitLen()),
|
|
}, true
|
|
}
|
|
}
|
|
|
|
pk, route, ok := b.e.PeerKeyForIP(ip)
|
|
if !ok {
|
|
return wgengine.PeerForIP{}, false
|
|
}
|
|
nid, ok := nb.NodeByKey(pk)
|
|
if !ok {
|
|
return wgengine.PeerForIP{}, false
|
|
}
|
|
n, ok := nb.NodeByID(nid)
|
|
if !ok {
|
|
return wgengine.PeerForIP{}, false
|
|
}
|
|
return wgengine.PeerForIP{Node: n, Route: route}, true
|
|
}
|