// Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause // Package nmcfg converts a controlclient.NetMap into a wgcfg config. package nmcfg import ( "bufio" "cmp" "fmt" "net/netip" "strings" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/wgengine/wgcfg" ) func nodeDebugName(n tailcfg.NodeView) string { name, _, _ := strings.Cut(cmp.Or(n.Name(), n.Hostinfo().Hostname()), ".") return name } // cidrIsSubnet reports whether cidr is a non-default-route subnet // exported by node that is not one of its own self addresses. func cidrIsSubnet(node tailcfg.NodeView, cidr netip.Prefix) bool { if cidr.Bits() == 0 { return false } if !cidr.IsSingleIP() { return true } if tsaddr.IsTailscaleIP(cidr.Addr()) { return false } for _, selfCIDR := range node.Addresses().All() { if cidr == selfCIDR { return false } } return true } // WGCfg returns the NetworkMaps's WireGuard configuration. // // The config does not include peers; wireguard-go gets those from the // live per-peer config source installed via // [tailscale.com/wgengine.Engine.SetPeerConfigFunc], fed by the route // manager. WGCfg still walks the peers to log which ones are not // routable and why, mirroring the route manager's filtering. func WGCfg(pk key.NodePrivate, nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, exitNode tailcfg.StableNodeID) (*wgcfg.Config, error) { cfg := &wgcfg.Config{ PrivateKey: pk, Addresses: nm.GetAddresses().AsSlice(), } var skippedExitNode, skippedSubnetRouter, skippedExpired []tailcfg.NodeView for _, peer := range nm.Peers { if peer.DiscoKey().IsZero() && peer.HomeDERP() == 0 && !peer.IsWireGuardOnly() { // Peer predates both DERP and active discovery, we cannot // communicate with it. logf("[v1] wgcfg: skipped peer %s, doesn't offer DERP or disco", peer.Key().ShortString()) continue } // Skip expired peers; we'll end up failing to connect to them // anyway, since control intentionally breaks node keys for // expired peers so that we can't discover endpoints via DERP. if peer.Expired() { skippedExpired = append(skippedExpired, peer) continue } didExitNodeLog := false for _, allowedIP := range peer.AllowedIPs().All() { if allowedIP.Bits() == 0 && peer.StableID() != exitNode { if didExitNodeLog { // Don't log about both the IPv4 /0 and IPv6 /0. continue } didExitNodeLog = true skippedExitNode = append(skippedExitNode, peer) } else if cidrIsSubnet(peer, allowedIP) { if (flags & netmap.AllowSubnetRoutes) == 0 { skippedSubnetRouter = append(skippedSubnetRouter, peer) } } } } logList := func(title string, nodes []tailcfg.NodeView) { if len(nodes) == 0 { return } logf("[v1] wgcfg: %s from %d nodes: %s", title, len(nodes), logger.ArgWriter(func(bw *bufio.Writer) { const max = 5 for i, n := range nodes { if i == max { fmt.Fprintf(bw, "... +%d", len(nodes)-max) return } if i > 0 { bw.WriteString(", ") } fmt.Fprintf(bw, "%s (%s)", nodeDebugName(n), n.StableID()) } })) } logList("skipped unselected exit nodes", skippedExitNode) logList("did not accept subnet routes", skippedSubnetRouter) logList("skipped expired peers", skippedExpired) return cfg, nil }