mirror of
https://github.com/tailscale/tailscale.git
synced 2026-07-15 01:53:08 -04:00
net/tstun,wgengine,ipn/ipnlocal: feed per-peer data-plane attrs from the route manager
Previously tstun.Wrapper.SetWGConfig walked wgcfg.Config.Peers on every netmap to rebuild its own IP-to-peer table for masquerade NAT rewrites and jailed-peer classification. Now the tun layer instead consumes the route manager's shared immutable outbound snapshot directly, via a new Engine.SetPeerRoutes method: LocalBackend pushes the snapshot (plus this node's native Tailscale addresses) after every route manager commit that can change it, and per-packet lookups read the interned PeerRoute attributes from that table. When no current peer is jailed or masqueraded, LocalBackend installs a nil table (gated on RouteManager.HasDataPlaneAttrs), preserving the per-packet nil-check fast path. The exitNodeRequiresMasq machinery is deleted: its purpose was populating the table with all peers so that more-specific entries shadow an exit node's /0, and the always-full route manager table gives that shadowing inherently. This is another step toward removing the Peers field from wgcfg.Config. Updates #12542 Change-Id: Ifce09ca929a3f2511303ca1d6efdd583739494ce Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"go4.org/mem"
|
||||
"go4.org/netipx"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
@@ -62,6 +63,7 @@
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/routemanager"
|
||||
"tailscale.com/net/traffic"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
@@ -2510,6 +2512,7 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
for k := range changedAllowedIPs {
|
||||
b.e.SyncDevicePeer(k)
|
||||
}
|
||||
b.setDataPlanePeerRoutes()
|
||||
|
||||
// Reset the WireGuard session for peers whose disco key changed in
|
||||
// a way that indicates a restart, flushing their dead session keys;
|
||||
@@ -2524,11 +2527,9 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
// and its lazy peer creation reads live state via
|
||||
// [wgengine.Engine.SetPeerConfigFunc]. What still rides
|
||||
// authReconfig is everything else derived from the full peer set:
|
||||
// OS routes (router.Config), the quad-100 resolver's MagicDNS
|
||||
// hosts map (dnsConfigForNetmap), and tstun's per-peer config
|
||||
// (masquerade addresses and jailed peers, via SetWGConfig). Once
|
||||
// those become delta-aware too, this can be gated on the route
|
||||
// manager's OS-routes changes and the tstun-relevant fields
|
||||
// OS routes (router.Config) and the quad-100 resolver's MagicDNS
|
||||
// hosts map (dnsConfigForNetmap). Once those become delta-aware
|
||||
// too, this can be gated on the route manager's OS-routes changes
|
||||
// instead of firing on every peer change.
|
||||
needsAuthReconfig = needsAuthReconfig || peersUpsertedOrRemoved
|
||||
if needsAuthReconfig {
|
||||
@@ -6177,6 +6178,10 @@ func (b *LocalBackend) authReconfigLocked() {
|
||||
cfg.Peers[i].AllowedIPs = extras.AppendTo(cfg.Peers[i].AllowedIPs)
|
||||
}
|
||||
}
|
||||
// The prefs and extras commits above can both change the outbound
|
||||
// table (such as installing the selected exit node's /0 routes),
|
||||
// so refresh the data plane's view of it.
|
||||
b.setDataPlanePeerRoutes()
|
||||
|
||||
err = b.e.Reconfig(cfg, rcfg, dcfg)
|
||||
if err == wgengine.ErrNoChanges {
|
||||
@@ -6190,6 +6195,37 @@ func (b *LocalBackend) authReconfigLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
// setDataPlanePeerRoutes pushes the route manager's outbound table and
|
||||
// this node's native Tailscale addresses into the engine's tun-layer
|
||||
// data plane, which uses them for per-packet NAT rewrites and
|
||||
// jailed-filter selection. It must be called after every route manager
|
||||
// commit that can change the outbound table or the set of peers with
|
||||
// data-plane attributes: netmap updates (full or delta), prefs changes,
|
||||
// and extra-allowed-IP changes.
|
||||
//
|
||||
// When no current peer is jailed or masqueraded, it installs nil, which
|
||||
// keeps the data plane's per-packet fast path to a nil check.
|
||||
func (b *LocalBackend) setDataPlanePeerRoutes() {
|
||||
nb := b.currentNode()
|
||||
var native4, native6 netip.Addr
|
||||
var routes *bart.Table[*routemanager.PeerRoute]
|
||||
if nb.routeMgr.HasDataPlaneAttrs() {
|
||||
routes = nb.routeMgr.Outbound()
|
||||
if nm := nb.NetMap(); nm != nil {
|
||||
for _, pfx := range nm.GetAddresses().All() {
|
||||
a := pfx.Addr()
|
||||
switch {
|
||||
case a.Is4() && !native4.IsValid() && tsaddr.IsTailscaleIP(a):
|
||||
native4 = a
|
||||
case a.Is6() && !native6.IsValid() && tsaddr.IsTailscaleIP(a):
|
||||
native6 = a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.e.SetPeerRoutes(native4, native6, routes)
|
||||
}
|
||||
|
||||
// shouldUseOneCGNATRoute reports whether we should prefer to make one big
|
||||
// CGNAT /10 route rather than a /32 per peer.
|
||||
//
|
||||
@@ -7306,6 +7342,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
login = cmp.Or(profileFromView(nm.UserProfiles[nm.User()]).LoginName, "<missing-profile>")
|
||||
}
|
||||
discoChanged := b.currentNode().SetNetMap(nm)
|
||||
b.setDataPlanePeerRoutes()
|
||||
if ms, ok := b.sys.MagicSock.GetOK(); ok {
|
||||
if nm != nil {
|
||||
if nm.Cached {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
@@ -29,6 +30,7 @@
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/routemanager"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
@@ -1973,6 +1975,9 @@ func (e *mockEngine) SetJailedFilter(f *filter.Filter) {
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
func (e *mockEngine) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) {
|
||||
}
|
||||
|
||||
func (e *mockEngine) SetStatusCallback(cb wgengine.StatusCallback) {
|
||||
e.mu.Lock()
|
||||
e.statusCb = cb
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -28,6 +26,7 @@
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/packet/checksum"
|
||||
"tailscale.com/net/routemanager"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tstime/mono"
|
||||
@@ -587,9 +586,9 @@ func findV6(addrs []netip.Prefix) netip.Addr {
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
// peerConfigTable contains configuration for individual peers and related
|
||||
// information necessary to perform peer-specific operations. It should be
|
||||
// treated as immutable.
|
||||
// peerConfigTable contains the per-peer route attributes and related
|
||||
// information necessary to perform peer-specific operations. It should
|
||||
// be treated as immutable.
|
||||
//
|
||||
// The nil value is a valid configuration.
|
||||
type peerConfigTable struct {
|
||||
@@ -602,57 +601,17 @@ type peerConfigTable struct {
|
||||
// inbound packet is IPv6.
|
||||
nativeAddr4, nativeAddr6 netip.Addr
|
||||
|
||||
// byIP contains configuration for each peer, indexed by a peer's IP
|
||||
// address(es).
|
||||
byIP bart.Table[*peerConfig]
|
||||
|
||||
// masqAddrCounts is a count of peers by MasqueradeAsIP.
|
||||
// TODO? for logging
|
||||
masqAddrCounts map[netip.Addr]int
|
||||
}
|
||||
|
||||
// peerConfig is the configuration for a single peer.
|
||||
type peerConfig struct {
|
||||
// dstMasqAddr{4,6} are the addresses that should be used as the
|
||||
// source address when masquerading packets to this peer (i.e.
|
||||
// SNAT). If an address is not valid, the packet should not be
|
||||
// masqueraded for that address family.
|
||||
dstMasqAddr4 netip.Addr
|
||||
dstMasqAddr6 netip.Addr
|
||||
|
||||
// jailed is whether this peer is "jailed" (i.e. is restricted from being
|
||||
// able to initiate connections to this node). This is the case for shared
|
||||
// nodes.
|
||||
jailed bool
|
||||
// byIP maps a peer's IP addresses and routed prefixes to the
|
||||
// peer's route attributes. It is a shared immutable snapshot
|
||||
// from [routemanager.RouteManager.Outbound].
|
||||
byIP *bart.Table[*routemanager.PeerRoute]
|
||||
}
|
||||
|
||||
func (c *peerConfigTable) String() string {
|
||||
if c == nil {
|
||||
return "peerConfigTable(nil)"
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("peerConfigTable{")
|
||||
fmt.Fprintf(&b, "nativeAddr4: %v, ", c.nativeAddr4)
|
||||
fmt.Fprintf(&b, "nativeAddr6: %v, ", c.nativeAddr6)
|
||||
|
||||
// TODO: figure out how to iterate/debug/print c.byIP
|
||||
|
||||
b.WriteString("}")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (c *peerConfig) String() string {
|
||||
if c == nil {
|
||||
return "peerConfig(nil)"
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("peerConfig{")
|
||||
fmt.Fprintf(&b, "dstMasqAddr4: %v, ", c.dstMasqAddr4)
|
||||
fmt.Fprintf(&b, "dstMasqAddr6: %v, ", c.dstMasqAddr6)
|
||||
fmt.Fprintf(&b, "jailed: %v}", c.jailed)
|
||||
|
||||
return b.String()
|
||||
return fmt.Sprintf("peerConfigTable{nativeAddr4: %v, nativeAddr6: %v}", c.nativeAddr4, c.nativeAddr6)
|
||||
}
|
||||
|
||||
// mapDstIP returns the destination IP to use for a packet to dst.
|
||||
@@ -675,10 +634,10 @@ func (pc *peerConfigTable) mapDstIP(src, oldDst netip.Addr) netip.Addr {
|
||||
return oldDst
|
||||
}
|
||||
|
||||
if oldDst.Is4() && pc.nativeAddr4.IsValid() && c.dstMasqAddr4 == oldDst {
|
||||
if oldDst.Is4() && pc.nativeAddr4.IsValid() && c.MasqAddr4 == oldDst {
|
||||
return pc.nativeAddr4
|
||||
}
|
||||
if oldDst.Is6() && pc.nativeAddr6.IsValid() && c.dstMasqAddr6 == oldDst {
|
||||
if oldDst.Is6() && pc.nativeAddr6.IsValid() && c.MasqAddr6 == oldDst {
|
||||
return pc.nativeAddr6
|
||||
}
|
||||
return oldDst
|
||||
@@ -708,105 +667,17 @@ func (pc *peerConfigTable) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
|
||||
|
||||
// Perform SNAT based on the address family and whether we have a valid
|
||||
// addr.
|
||||
if oldSrc.Is4() && c.dstMasqAddr4.IsValid() {
|
||||
return c.dstMasqAddr4
|
||||
if oldSrc.Is4() && c.MasqAddr4.IsValid() {
|
||||
return c.MasqAddr4
|
||||
}
|
||||
if oldSrc.Is6() && c.dstMasqAddr6.IsValid() {
|
||||
return c.dstMasqAddr6
|
||||
if oldSrc.Is6() && c.MasqAddr6.IsValid() {
|
||||
return c.MasqAddr6
|
||||
}
|
||||
|
||||
// No SNAT; use old src
|
||||
return oldSrc
|
||||
}
|
||||
|
||||
// peerConfigTableFromWGConfig generates a peerConfigTable from nm. If NAT is
|
||||
// not required, and no additional configuration is present, it returns nil.
|
||||
func peerConfigTableFromWGConfig(wcfg *wgcfg.Config) *peerConfigTable {
|
||||
if wcfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nativeAddr4 := findV4(wcfg.Addresses)
|
||||
nativeAddr6 := findV6(wcfg.Addresses)
|
||||
if !nativeAddr4.IsValid() && !nativeAddr6.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := &peerConfigTable{
|
||||
nativeAddr4: nativeAddr4,
|
||||
nativeAddr6: nativeAddr6,
|
||||
masqAddrCounts: make(map[netip.Addr]int),
|
||||
}
|
||||
|
||||
// When using an exit node that requires masquerading, we need to
|
||||
// fill out the routing table with all peers not just the ones that
|
||||
// require masquerading.
|
||||
exitNodeRequiresMasq := false // true if using an exit node and it requires masquerading
|
||||
for _, p := range wcfg.Peers {
|
||||
isExitNode := slices.Contains(p.AllowedIPs, tsaddr.AllIPv4()) || slices.Contains(p.AllowedIPs, tsaddr.AllIPv6())
|
||||
if isExitNode {
|
||||
hasMasqAddr := false ||
|
||||
(p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
|
||||
(p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
|
||||
if hasMasqAddr {
|
||||
exitNodeRequiresMasq = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
byIPSize := 0
|
||||
for i := range wcfg.Peers {
|
||||
p := &wcfg.Peers[i]
|
||||
|
||||
// Build a routing table that configures DNAT (i.e. changing
|
||||
// the V4MasqAddr/V6MasqAddr for a given peer to the current
|
||||
// peer's v4/v6 IP).
|
||||
var addrToUse4, addrToUse6 netip.Addr
|
||||
if p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
|
||||
addrToUse4 = *p.V4MasqAddr
|
||||
ret.masqAddrCounts[addrToUse4]++
|
||||
}
|
||||
if p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
|
||||
addrToUse6 = *p.V6MasqAddr
|
||||
ret.masqAddrCounts[addrToUse6]++
|
||||
}
|
||||
|
||||
// If the exit node requires masquerading, set the masquerade
|
||||
// addresses to our native addresses.
|
||||
if exitNodeRequiresMasq {
|
||||
if !addrToUse4.IsValid() && nativeAddr4.IsValid() {
|
||||
addrToUse4 = nativeAddr4
|
||||
}
|
||||
if !addrToUse6.IsValid() && nativeAddr6.IsValid() {
|
||||
addrToUse6 = nativeAddr6
|
||||
}
|
||||
}
|
||||
|
||||
if !addrToUse4.IsValid() && !addrToUse6.IsValid() && !p.IsJailed {
|
||||
// NAT not required for this peer.
|
||||
continue
|
||||
}
|
||||
|
||||
// Use the same peer configuration for each address of the peer.
|
||||
pc := &peerConfig{
|
||||
dstMasqAddr4: addrToUse4,
|
||||
dstMasqAddr6: addrToUse6,
|
||||
jailed: p.IsJailed,
|
||||
}
|
||||
|
||||
// Insert an entry into our routing table for each allowed IP.
|
||||
for _, ip := range p.AllowedIPs {
|
||||
ret.byIP.Insert(ip, pc)
|
||||
byIPSize++
|
||||
}
|
||||
}
|
||||
if byIPSize == 0 && len(ret.masqAddrCounts) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (pc *peerConfigTable) inboundPacketIsJailed(p *packet.Parsed) bool {
|
||||
if pc == nil {
|
||||
return false
|
||||
@@ -815,7 +686,7 @@ func (pc *peerConfigTable) inboundPacketIsJailed(p *packet.Parsed) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return c.jailed
|
||||
return c.Jailed
|
||||
}
|
||||
|
||||
func (pc *peerConfigTable) outboundPacketIsJailed(p *packet.Parsed) bool {
|
||||
@@ -826,7 +697,7 @@ func (pc *peerConfigTable) outboundPacketIsJailed(p *packet.Parsed) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return c.jailed
|
||||
return c.Jailed
|
||||
}
|
||||
|
||||
// SetIPer is the interface expected to be implemented by the TAP implementation
|
||||
@@ -836,16 +707,40 @@ type SetIPer interface {
|
||||
SetIP(ipV4, ipV6 netip.Addr) error
|
||||
}
|
||||
|
||||
// SetWGConfig is called when a new NetworkMap is received.
|
||||
// SetWGConfig is called when a new NetworkMap is received. Its only
|
||||
// remaining job is updating the TAP device's IP addresses; the
|
||||
// per-peer route attributes arrive via [Wrapper.SetPeerRoutes].
|
||||
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
|
||||
if t.isTAP {
|
||||
if sip, ok := t.tdev.(SetIPer); ok {
|
||||
sip.SetIP(findV4(wcfg.Addresses), findV6(wcfg.Addresses))
|
||||
}
|
||||
}
|
||||
cfg := peerConfigTableFromWGConfig(wcfg)
|
||||
}
|
||||
|
||||
// SetPeerRoutes is called whenever this node's Tailscale addresses or
|
||||
// the route manager's outbound table change. native4 and native6 are
|
||||
// this node's own Tailscale addresses, and routes maps each peer's
|
||||
// addresses and routed prefixes to its route attributes.
|
||||
//
|
||||
// A nil routes table disables all per-packet peer processing (NAT
|
||||
// rewrites and jailed-filter selection); callers pass nil when no
|
||||
// current peer has any such attributes, which keeps the common
|
||||
// per-packet path to a nil check.
|
||||
func (t *Wrapper) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) {
|
||||
var cfg *peerConfigTable
|
||||
if routes != nil && (native4.IsValid() || native6.IsValid()) {
|
||||
cfg = &peerConfigTable{
|
||||
nativeAddr4: native4,
|
||||
nativeAddr6: native6,
|
||||
byIP: routes,
|
||||
}
|
||||
}
|
||||
old := t.peerConfig.Swap(cfg)
|
||||
if !reflect.DeepEqual(old, cfg) {
|
||||
// Log only on nil-ness or native address transitions; the routes
|
||||
// table changes with every routing update and is too chatty to log.
|
||||
if (old == nil) != (cfg == nil) ||
|
||||
(old != nil && (old.nativeAddr4 != cfg.nativeAddr4 || old.nativeAddr6 != cfg.nativeAddr6)) {
|
||||
t.logf("peer config: %v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/tailscale/wireguard-go/tun/tuntest"
|
||||
@@ -29,6 +30,7 @@
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/packet/checksum"
|
||||
"tailscale.com/net/routemanager"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/ipproto"
|
||||
@@ -42,7 +44,6 @@
|
||||
"tailscale.com/util/usermetric"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/netstack/gro"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func udp4(src, dst string, sport, dport uint16) []byte {
|
||||
@@ -710,22 +711,23 @@ func TestFilterDiscoLoop(t *testing.T) {
|
||||
}
|
||||
|
||||
// TODO(andrew-d): refactor this test to no longer use addrFam, after #11945
|
||||
// removed it in peerConfigFromWGConfig
|
||||
func TestPeerCfg_NAT(t *testing.T) {
|
||||
node := func(ip, masqIP netip.Addr, otherAllowedIPs ...netip.Prefix) wgcfg.Peer {
|
||||
p := wgcfg.Peer{
|
||||
PublicKey: key.NewNode().Public(),
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.PrefixFrom(ip, ip.BitLen()),
|
||||
},
|
||||
type testPeer struct {
|
||||
pr *routemanager.PeerRoute
|
||||
pfxs []netip.Prefix
|
||||
}
|
||||
node := func(ip, masqIP netip.Addr, otherAllowedIPs ...netip.Prefix) testPeer {
|
||||
pr := &routemanager.PeerRoute{Key: key.NewNode().Public()}
|
||||
switch {
|
||||
case masqIP.Is4():
|
||||
pr.MasqAddr4 = masqIP
|
||||
case masqIP.Is6():
|
||||
pr.MasqAddr6 = masqIP
|
||||
}
|
||||
if masqIP.Is4() {
|
||||
p.V4MasqAddr = new(masqIP)
|
||||
} else {
|
||||
p.V6MasqAddr = new(masqIP)
|
||||
return testPeer{
|
||||
pr: pr,
|
||||
pfxs: append([]netip.Prefix{netip.PrefixFrom(ip, ip.BitLen())}, otherAllowedIPs...),
|
||||
}
|
||||
p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...)
|
||||
return p
|
||||
}
|
||||
test := func(addrFam ipproto.Version) {
|
||||
var (
|
||||
@@ -734,7 +736,6 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
selfNativeIP = netip.MustParseAddr("100.64.0.1")
|
||||
selfEIP1 = netip.MustParseAddr("100.64.1.1")
|
||||
selfEIP2 = netip.MustParseAddr("100.64.1.2")
|
||||
selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
|
||||
|
||||
peer1IP = netip.MustParseAddr("100.64.0.2")
|
||||
peer2IP = netip.MustParseAddr("100.64.0.3")
|
||||
@@ -749,7 +750,6 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
selfNativeIP = netip.MustParseAddr("fd7a:115c:a1e0::a")
|
||||
selfEIP1 = netip.MustParseAddr("fd7a:115c:a1e0::1a")
|
||||
selfEIP2 = netip.MustParseAddr("fd7a:115c:a1e0::1b")
|
||||
selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
|
||||
|
||||
peer1IP = netip.MustParseAddr("fd7a:115c:a1e0::b")
|
||||
peer2IP = netip.MustParseAddr("fd7a:115c:a1e0::c")
|
||||
@@ -769,13 +769,13 @@ type dnatTest struct {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wcfg *wgcfg.Config
|
||||
peers []testPeer // nil means no config at all
|
||||
snatMap map[netip.Addr]netip.Addr // dst -> src
|
||||
dnat []dnatTest
|
||||
}{
|
||||
{
|
||||
name: "no-cfg",
|
||||
wcfg: nil,
|
||||
name: "no-cfg",
|
||||
peers: nil,
|
||||
snatMap: map[netip.Addr]netip.Addr{
|
||||
peer1IP: selfNativeIP,
|
||||
peer2IP: selfNativeIP,
|
||||
@@ -789,12 +789,9 @@ type dnatTest struct {
|
||||
},
|
||||
{
|
||||
name: "single-peer-requires-nat",
|
||||
wcfg: &wgcfg.Config{
|
||||
Addresses: selfAddrs,
|
||||
Peers: []wgcfg.Peer{
|
||||
node(peer1IP, noIP),
|
||||
node(peer2IP, selfEIP2),
|
||||
},
|
||||
peers: []testPeer{
|
||||
node(peer1IP, noIP),
|
||||
node(peer2IP, selfEIP2),
|
||||
},
|
||||
snatMap: map[netip.Addr]netip.Addr{
|
||||
peer1IP: selfNativeIP,
|
||||
@@ -810,12 +807,9 @@ type dnatTest struct {
|
||||
},
|
||||
{
|
||||
name: "multiple-peers-require-nat",
|
||||
wcfg: &wgcfg.Config{
|
||||
Addresses: selfAddrs,
|
||||
Peers: []wgcfg.Peer{
|
||||
node(peer1IP, selfEIP1),
|
||||
node(peer2IP, selfEIP2),
|
||||
},
|
||||
peers: []testPeer{
|
||||
node(peer1IP, selfEIP1),
|
||||
node(peer2IP, selfEIP2),
|
||||
},
|
||||
snatMap: map[netip.Addr]netip.Addr{
|
||||
peer1IP: selfEIP1,
|
||||
@@ -831,12 +825,9 @@ type dnatTest struct {
|
||||
},
|
||||
{
|
||||
name: "multiple-peers-require-nat-with-subnet",
|
||||
wcfg: &wgcfg.Config{
|
||||
Addresses: selfAddrs,
|
||||
Peers: []wgcfg.Peer{
|
||||
node(peer1IP, selfEIP1),
|
||||
node(peer2IP, selfEIP2, subnet),
|
||||
},
|
||||
peers: []testPeer{
|
||||
node(peer1IP, selfEIP1),
|
||||
node(peer2IP, selfEIP2, subnet),
|
||||
},
|
||||
snatMap: map[netip.Addr]netip.Addr{
|
||||
peer1IP: selfEIP1,
|
||||
@@ -852,12 +843,9 @@ type dnatTest struct {
|
||||
},
|
||||
{
|
||||
name: "multiple-peers-require-nat-with-default-route",
|
||||
wcfg: &wgcfg.Config{
|
||||
Addresses: selfAddrs,
|
||||
Peers: []wgcfg.Peer{
|
||||
node(peer1IP, selfEIP1),
|
||||
node(peer2IP, selfEIP2, exitRoute),
|
||||
},
|
||||
peers: []testPeer{
|
||||
node(peer1IP, selfEIP1),
|
||||
node(peer2IP, selfEIP2, exitRoute),
|
||||
},
|
||||
snatMap: map[netip.Addr]netip.Addr{
|
||||
peer1IP: selfEIP1,
|
||||
@@ -873,12 +861,9 @@ type dnatTest struct {
|
||||
},
|
||||
{
|
||||
name: "no-nat",
|
||||
wcfg: &wgcfg.Config{
|
||||
Addresses: selfAddrs,
|
||||
Peers: []wgcfg.Peer{
|
||||
node(peer1IP, noIP),
|
||||
node(peer2IP, noIP),
|
||||
},
|
||||
peers: []testPeer{
|
||||
node(peer1IP, noIP),
|
||||
node(peer2IP, noIP),
|
||||
},
|
||||
snatMap: map[netip.Addr]netip.Addr{
|
||||
peer1IP: selfNativeIP,
|
||||
@@ -894,12 +879,9 @@ type dnatTest struct {
|
||||
},
|
||||
{
|
||||
name: "exit-node-require-nat-peer-doesnt",
|
||||
wcfg: &wgcfg.Config{
|
||||
Addresses: selfAddrs,
|
||||
Peers: []wgcfg.Peer{
|
||||
node(peer1IP, noIP),
|
||||
node(peer2IP, selfEIP2, exitRoute),
|
||||
},
|
||||
peers: []testPeer{
|
||||
node(peer1IP, noIP),
|
||||
node(peer2IP, selfEIP2, exitRoute),
|
||||
},
|
||||
snatMap: map[netip.Addr]netip.Addr{
|
||||
peer1IP: selfNativeIP,
|
||||
@@ -916,7 +898,20 @@ type dnatTest struct {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(fmt.Sprintf("%v/%v", addrFam, tc.name), func(t *testing.T) {
|
||||
pcfg := peerConfigTableFromWGConfig(tc.wcfg)
|
||||
var pcfg *peerConfigTable
|
||||
if tc.peers != nil {
|
||||
pcfg = &peerConfigTable{byIP: &bart.Table[*routemanager.PeerRoute]{}}
|
||||
if selfNativeIP.Is4() {
|
||||
pcfg.nativeAddr4 = selfNativeIP
|
||||
} else {
|
||||
pcfg.nativeAddr6 = selfNativeIP
|
||||
}
|
||||
for _, p := range tc.peers {
|
||||
for _, pfx := range p.pfxs {
|
||||
pcfg.byIP.Insert(pfx, p.pr)
|
||||
}
|
||||
}
|
||||
}
|
||||
for peer, want := range tc.snatMap {
|
||||
if got := pcfg.selectSrcIP(selfNativeIP, peer); got != want {
|
||||
t.Errorf("selectSrcIP[%v]: got %v; want %v", peer, got, want)
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"unsafe"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
wgconn "github.com/tailscale/wireguard-go/conn"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
@@ -54,6 +55,7 @@
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/ping"
|
||||
"tailscale.com/net/routemanager"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/net/stun/stuntest"
|
||||
"tailscale.com/net/tstun"
|
||||
@@ -247,6 +249,35 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, ln nettype.PacketListe
|
||||
func (s *magicStack) Reconfig(cfg *wgcfg.Config) error {
|
||||
s.tsTun.SetWGConfig(cfg)
|
||||
|
||||
// In production, LocalBackend feeds the tun-layer data plane its
|
||||
// per-peer route attributes from the route manager. Tests that
|
||||
// bypass LocalBackend need to build the table from cfg.Peers here
|
||||
// so that masquerade rewrites and jailed classification work.
|
||||
tbl := &bart.Table[*routemanager.PeerRoute]{}
|
||||
for _, p := range cfg.Peers {
|
||||
pr := &routemanager.PeerRoute{Key: p.PublicKey, Jailed: p.IsJailed}
|
||||
if p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
|
||||
pr.MasqAddr4 = *p.V4MasqAddr
|
||||
}
|
||||
if p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
|
||||
pr.MasqAddr6 = *p.V6MasqAddr
|
||||
}
|
||||
for _, pfx := range p.AllowedIPs {
|
||||
tbl.Insert(pfx, pr)
|
||||
}
|
||||
}
|
||||
var native4, native6 netip.Addr
|
||||
for _, pfx := range cfg.Addresses {
|
||||
a := pfx.Addr()
|
||||
switch {
|
||||
case a.Is4() && !native4.IsValid():
|
||||
native4 = a
|
||||
case a.Is6() && !native6.IsValid():
|
||||
native6 = a
|
||||
}
|
||||
}
|
||||
s.tsTun.SetPeerRoutes(native4, native6, tbl)
|
||||
|
||||
// In production, LocalBackend installs a PeerByIPPacketFunc via
|
||||
// Engine.SetPeerByIPPacketFunc. Tests that bypass LocalBackend need
|
||||
// to install one here for outbound packet routing.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"go4.org/mem"
|
||||
@@ -33,6 +34,7 @@
|
||||
"tailscale.com/net/ipset"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/routemanager"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
@@ -1023,6 +1025,10 @@ func (e *userspaceEngine) SetJailedFilter(filt *filter.Filter) {
|
||||
e.tundev.SetJailedFilter(filt)
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) {
|
||||
e.tundev.SetPeerRoutes(native4, native6, routes)
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) SetStatusCallback(cb StatusCallback) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/routemanager"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -125,6 +127,17 @@ type Engine interface {
|
||||
// SetJailedFilter updates the packet filter for jailed nodes.
|
||||
SetJailedFilter(*filter.Filter)
|
||||
|
||||
// SetPeerRoutes updates the per-peer route attributes used by the
|
||||
// tun-layer data plane for per-packet NAT rewrites and
|
||||
// jailed-filter selection. native4 and native6 are this node's own
|
||||
// Tailscale addresses, and routes maps each peer's addresses and
|
||||
// routed prefixes to its attributes; it is a shared immutable
|
||||
// snapshot from [routemanager.RouteManager.Outbound].
|
||||
//
|
||||
// A nil routes table disables all per-packet peer processing;
|
||||
// callers pass nil when no current peer has any such attributes.
|
||||
SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute])
|
||||
|
||||
// SetStatusCallback sets the function to call when the
|
||||
// WireGuard status changes.
|
||||
SetStatusCallback(StatusCallback)
|
||||
|
||||
Reference in New Issue
Block a user