mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-08 22:49:10 +02:00
Propagate IPv6 capability changes to other peers
This commit is contained in:
@@ -931,7 +931,7 @@ func getPeerIPDNSLabel(ip netip.Addr, peerHostName string) (string, error) {
|
|||||||
// SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible
|
// SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible
|
||||||
func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||||
var peer *nbpeer.Peer
|
var peer *nbpeer.Peer
|
||||||
var updated, versionChanged bool
|
var updated, versionChanged, ipv6CapabilityChanged bool
|
||||||
var err error
|
var err error
|
||||||
var postureChecks []*posture.Checks
|
var postureChecks []*posture.Checks
|
||||||
var peerGroupIDs []string
|
var peerGroupIDs []string
|
||||||
@@ -967,7 +967,9 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldHasIPv6Cap := peer.HasCapability(nbpeer.PeerCapabilityIPv6Overlay)
|
||||||
updated, versionChanged = peer.UpdateMetaIfNew(sync.Meta)
|
updated, versionChanged = peer.UpdateMetaIfNew(sync.Meta)
|
||||||
|
ipv6CapabilityChanged = oldHasIPv6Cap != peer.HasCapability(nbpeer.PeerCapabilityIPv6Overlay)
|
||||||
if updated {
|
if updated {
|
||||||
am.metrics.AccountManagerMetrics().CountPeerMetUpdate()
|
am.metrics.AccountManagerMetrics().CountPeerMetUpdate()
|
||||||
log.WithContext(ctx).Tracef("peer %s metadata updated", peer.ID)
|
log.WithContext(ctx).Tracef("peer %s metadata updated", peer.ID)
|
||||||
@@ -991,7 +993,7 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy
|
|||||||
return nil, nil, nil, 0, err
|
return nil, nil, nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if isStatusChanged || sync.UpdateAccountPeers || (updated && (len(postureChecks) > 0 || versionChanged)) {
|
if isStatusChanged || sync.UpdateAccountPeers || ipv6CapabilityChanged || (updated && (len(postureChecks) > 0 || versionChanged)) {
|
||||||
err = am.networkMapController.OnPeersUpdated(ctx, accountID, []string{peer.ID})
|
err = am.networkMapController.OnPeersUpdated(ctx, accountID, []string{peer.ID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, 0, fmt.Errorf("notify network map controller of peer update: %w", err)
|
return nil, nil, nil, 0, fmt.Errorf("notify network map controller of peer update: %w", err)
|
||||||
@@ -1041,6 +1043,7 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer
|
|||||||
var peer *nbpeer.Peer
|
var peer *nbpeer.Peer
|
||||||
var updateRemotePeers bool
|
var updateRemotePeers bool
|
||||||
var isPeerUpdated bool
|
var isPeerUpdated bool
|
||||||
|
var ipv6CapabilityChanged bool
|
||||||
var postureChecks []*posture.Checks
|
var postureChecks []*posture.Checks
|
||||||
var peerGroupIDs []string
|
var peerGroupIDs []string
|
||||||
|
|
||||||
@@ -1080,7 +1083,9 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldHasIPv6Cap := peer.HasCapability(nbpeer.PeerCapabilityIPv6Overlay)
|
||||||
isPeerUpdated, _ = peer.UpdateMetaIfNew(login.Meta)
|
isPeerUpdated, _ = peer.UpdateMetaIfNew(login.Meta)
|
||||||
|
ipv6CapabilityChanged = oldHasIPv6Cap != peer.HasCapability(nbpeer.PeerCapabilityIPv6Overlay)
|
||||||
if isPeerUpdated {
|
if isPeerUpdated {
|
||||||
am.metrics.AccountManagerMetrics().CountPeerMetUpdate()
|
am.metrics.AccountManagerMetrics().CountPeerMetUpdate()
|
||||||
shouldStorePeer = true
|
shouldStorePeer = true
|
||||||
@@ -1118,7 +1123,7 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer
|
|||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateRemotePeers || isStatusChanged || (isPeerUpdated && len(postureChecks) > 0) {
|
if updateRemotePeers || isStatusChanged || ipv6CapabilityChanged || (isPeerUpdated && len(postureChecks) > 0) {
|
||||||
err = am.networkMapController.OnPeersUpdated(ctx, accountID, []string{peer.ID})
|
err = am.networkMapController.OnPeersUpdated(ctx, accountID, []string{peer.ID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, fmt.Errorf("notify network map controller of peer update: %w", err)
|
return nil, nil, nil, fmt.Errorf("notify network map controller of peer update: %w", err)
|
||||||
|
|||||||
@@ -2790,6 +2790,53 @@ func TestPeerWillHaveIPv6(t *testing.T) {
|
|||||||
assert.False(t, peerWillHaveIPv6(emptySettings, []string{"group-a"}, "all-group-id"), "no IPv6 groups means no IPv6")
|
assert.False(t, peerWillHaveIPv6(emptySettings, []string{"group-a"}, "all-group-id"), "no IPv6 groups means no IPv6")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSyncPeer_IPv6CapabilityChangePropagates ensures that when a peer reports
|
||||||
|
// a new IPv6 overlay capability via SyncPeer (e.g. after a client upgrade or
|
||||||
|
// flipping --disable-ipv6) without bumping its WtVersion, other account peers
|
||||||
|
// receive a fresh network map so their AAAA records for it become unstale.
|
||||||
|
func TestSyncPeer_IPv6CapabilityChangePropagates(t *testing.T) {
|
||||||
|
manager, updateManager, _, peer1, peer2, _ := setupNetworkMapTest(t)
|
||||||
|
|
||||||
|
updMsg := updateManager.CreateChannel(context.Background(), peer1.ID)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
updateManager.CloseChannel(context.Background(), peer1.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Drain any initial updates from setup.
|
||||||
|
drain := func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-updMsg:
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drain()
|
||||||
|
|
||||||
|
t.Run("no propagation when capabilities are unchanged", func(t *testing.T) {
|
||||||
|
_, _, _, _, err := manager.SyncPeer(context.Background(), types.PeerSync{
|
||||||
|
WireGuardPubKey: peer2.Key,
|
||||||
|
Meta: peer2.Meta,
|
||||||
|
}, peer2.AccountID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
peerShouldNotReceiveUpdate(t, updMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("propagation when IPv6 capability is added", func(t *testing.T) {
|
||||||
|
newMeta := peer2.Meta
|
||||||
|
newMeta.Capabilities = append([]int32{}, peer2.Meta.Capabilities...)
|
||||||
|
newMeta.Capabilities = append(newMeta.Capabilities, nbpeer.PeerCapabilityIPv6Overlay)
|
||||||
|
|
||||||
|
_, _, _, _, err := manager.SyncPeer(context.Background(), types.PeerSync{
|
||||||
|
WireGuardPubKey: peer2.Key,
|
||||||
|
Meta: newMeta,
|
||||||
|
}, peer2.AccountID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
peerShouldReceiveUpdate(t, updMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdatePeer_DnsLabelCollisionWithFQDN(t *testing.T) {
|
func TestUpdatePeer_DnsLabelCollisionWithFQDN(t *testing.T) {
|
||||||
manager, _, err := createManager(t)
|
manager, _, err := createManager(t)
|
||||||
require.NoError(t, err, "unable to create account manager")
|
require.NoError(t, err, "unable to create account manager")
|
||||||
|
|||||||
@@ -293,10 +293,9 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn
|
|||||||
// Only advertise AAAA for peers that have a valid IPv6, whose client supports it,
|
// Only advertise AAAA for peers that have a valid IPv6, whose client supports it,
|
||||||
// and that belong to an IPv6-enabled group. Old clients don't configure v6 on their
|
// and that belong to an IPv6-enabled group. Old clients don't configure v6 on their
|
||||||
// WireGuard interface, so resolving their AAAA causes connections to hang.
|
// WireGuard interface, so resolving their AAAA causes connections to hang.
|
||||||
// Edge case: toggling --disable-ipv6 on a peer without a version change does not
|
// Capability changes (client upgrade/downgrade, --disable-ipv6 toggle) propagate
|
||||||
// propagate to other peers, so AAAA records can be stale until the next full sync.
|
// to other peers via SyncPeer/LoginPeer regardless of version change, so AAAA
|
||||||
// This is accepted because v4 connectivity is unaffected. Can be fixed by adding
|
// records refresh when a peer first reports the IPv6 overlay capability.
|
||||||
// capability-change detection to the SyncPeer propagation condition.
|
|
||||||
_, peerAllowed := ipv6AllowedPeers[peer.ID]
|
_, peerAllowed := ipv6AllowedPeers[peer.ID]
|
||||||
hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed
|
hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed
|
||||||
if hasIPv6 {
|
if hasIPv6 {
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNetworkMapComponents_IPv6EndToEnd(t *testing.T) {
|
||||||
|
account := createComponentTestAccount()
|
||||||
|
|
||||||
|
// Make all peers IPv6-capable and assign IPv6 addrs.
|
||||||
|
v6Caps := []int32{nbpeer.PeerCapabilityIPv6Overlay, nbpeer.PeerCapabilitySourcePrefixes}
|
||||||
|
account.Peers["peer-src-1"].Meta.Capabilities = v6Caps
|
||||||
|
account.Peers["peer-src-1"].IPv6 = netip.MustParseAddr("fd00::1")
|
||||||
|
account.Peers["peer-src-2"].Meta.Capabilities = v6Caps
|
||||||
|
account.Peers["peer-src-2"].IPv6 = netip.MustParseAddr("fd00::2")
|
||||||
|
account.Peers["peer-dst-1"].Meta.Capabilities = v6Caps
|
||||||
|
account.Peers["peer-dst-1"].IPv6 = netip.MustParseAddr("fd00::3")
|
||||||
|
|
||||||
|
// Mark group-src and group-dst as IPv6-enabled.
|
||||||
|
account.Settings.IPv6EnabledGroups = []string{"group-src", "group-dst"}
|
||||||
|
|
||||||
|
validated := allPeersValidated(account)
|
||||||
|
nm := networkMapFromComponents(t, account, "peer-src-1", validated)
|
||||||
|
|
||||||
|
require.NotNil(t, nm)
|
||||||
|
|
||||||
|
t.Run("v6 AAAA records emitted", func(t *testing.T) {
|
||||||
|
require.NotEmpty(t, nm.DNSConfig.CustomZones, "expected at least one custom zone")
|
||||||
|
var hasAAAA bool
|
||||||
|
var hasA bool
|
||||||
|
for _, z := range nm.DNSConfig.CustomZones {
|
||||||
|
for _, r := range z.Records {
|
||||||
|
if r.Type == int(dns.TypeAAAA) {
|
||||||
|
hasAAAA = true
|
||||||
|
}
|
||||||
|
if r.Type == int(dns.TypeA) {
|
||||||
|
hasA = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, hasA, "expected A records")
|
||||||
|
assert.True(t, hasAAAA, "expected AAAA records for IPv6-enabled peers")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v6 AllowedIPs would be advertised", func(t *testing.T) {
|
||||||
|
// nm.Peers contains *nbpeer.Peer; IPv6 should be set on those peers
|
||||||
|
var foundV6 bool
|
||||||
|
for _, p := range nm.Peers {
|
||||||
|
if p.IPv6.IsValid() {
|
||||||
|
foundV6 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, foundV6, "remote peers should have IPv6 set so AllowedIPs gets v6")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v6 firewall rules emitted", func(t *testing.T) {
|
||||||
|
require.NotEmpty(t, nm.FirewallRules, "expected firewall rules")
|
||||||
|
var hasV4 bool
|
||||||
|
var hasV6 bool
|
||||||
|
for _, r := range nm.FirewallRules {
|
||||||
|
addr, err := netip.ParseAddr(r.PeerIP)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if addr.Is4() {
|
||||||
|
hasV4 = true
|
||||||
|
}
|
||||||
|
if addr.Is6() {
|
||||||
|
hasV6 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, hasV4, "expected at least one v4 firewall rule (peer IP)")
|
||||||
|
assert.True(t, hasV6, "expected at least one v6 firewall rule (peer IPv6)")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNetworkMapComponents_RemotePeerWithoutCapability checks the asymmetric
|
||||||
|
// case where the target peer is IPv6-capable but a remote peer has an IPv6
|
||||||
|
// address assigned in the DB without yet reporting the capability flag.
|
||||||
|
// In that case the remote peer's v6 still appears in AllowedIPs (gated on
|
||||||
|
// the target peer's capability) but its AAAA record does not (gated on the
|
||||||
|
// remote peer's own capability).
|
||||||
|
func TestNetworkMapComponents_RemotePeerWithoutCapability(t *testing.T) {
|
||||||
|
account := createComponentTestAccount()
|
||||||
|
|
||||||
|
v6Caps := []int32{nbpeer.PeerCapabilityIPv6Overlay, nbpeer.PeerCapabilitySourcePrefixes}
|
||||||
|
// Target is fully capable.
|
||||||
|
account.Peers["peer-src-1"].Meta.Capabilities = v6Caps
|
||||||
|
account.Peers["peer-src-1"].IPv6 = netip.MustParseAddr("fd00::1")
|
||||||
|
// Remote peer has v6 assigned but no capability flag yet (e.g. old client).
|
||||||
|
account.Peers["peer-dst-1"].IPv6 = netip.MustParseAddr("fd00::3")
|
||||||
|
|
||||||
|
account.Settings.IPv6EnabledGroups = []string{"group-src", "group-dst"}
|
||||||
|
|
||||||
|
validated := allPeersValidated(account)
|
||||||
|
nm := networkMapFromComponents(t, account, "peer-src-1", validated)
|
||||||
|
require.NotNil(t, nm)
|
||||||
|
|
||||||
|
t.Run("AllowedIPs include remote v6", func(t *testing.T) {
|
||||||
|
var dst *nbpeer.Peer
|
||||||
|
for _, p := range nm.Peers {
|
||||||
|
if p.ID == "peer-dst-1" {
|
||||||
|
dst = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, dst)
|
||||||
|
assert.True(t, dst.IPv6.IsValid(), "remote peer's v6 should still be present so AllowedIPs gets v6/128 (gated on target peer cap)")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no AAAA for non-capable remote peer", func(t *testing.T) {
|
||||||
|
for _, z := range nm.DNSConfig.CustomZones {
|
||||||
|
for _, r := range z.Records {
|
||||||
|
if r.Type == int(dns.TypeAAAA) && r.RData == "fd00::3" {
|
||||||
|
t.Errorf("AAAA record for non-capable remote peer should NOT be emitted, got %+v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNetworkMapComponents_IPv6Disabled_NoV6Output asserts that a peer that
|
||||||
|
// does not support IPv6 (e.g. older client without the capability flag) gets
|
||||||
|
// no v6 firewall rules and no AAAA records, even if other peers have IPv6.
|
||||||
|
func TestNetworkMapComponents_IPv6Disabled_NoV6Output(t *testing.T) {
|
||||||
|
account := createComponentTestAccount()
|
||||||
|
|
||||||
|
v6Caps := []int32{nbpeer.PeerCapabilityIPv6Overlay}
|
||||||
|
account.Peers["peer-src-2"].Meta.Capabilities = v6Caps
|
||||||
|
account.Peers["peer-src-2"].IPv6 = netip.MustParseAddr("fd00::2")
|
||||||
|
account.Peers["peer-dst-1"].Meta.Capabilities = v6Caps
|
||||||
|
account.Peers["peer-dst-1"].IPv6 = netip.MustParseAddr("fd00::3")
|
||||||
|
// peer-src-1 (target) intentionally has no capability and no IPv6.
|
||||||
|
|
||||||
|
account.Settings.IPv6EnabledGroups = []string{"group-src", "group-dst"}
|
||||||
|
|
||||||
|
validated := allPeersValidated(account)
|
||||||
|
nm := networkMapFromComponents(t, account, "peer-src-1", validated)
|
||||||
|
require.NotNil(t, nm)
|
||||||
|
|
||||||
|
t.Run("no v6 firewall rules", func(t *testing.T) {
|
||||||
|
for _, r := range nm.FirewallRules {
|
||||||
|
addr, err := netip.ParseAddr(r.PeerIP)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.False(t, addr.Is6(), "v6 firewall rules should not be emitted for non-IPv6 peer (got %s)", r.PeerIP)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user