Merge branch 'main' into feat/auto-upgrade

This commit is contained in:
M Essam Hamed
2025-10-06 14:56:16 +03:00
245 changed files with 7274 additions and 2078 deletions

View File

@@ -300,9 +300,12 @@ func (a *Account) GetPeerNetworkMap(
if dnsManagementStatus {
var zones []nbdns.CustomZone
if peersCustomZone.Domain != "" {
zones = append(zones, peersCustomZone)
records := filterZoneRecordsForPeers(peer, peersCustomZone, peersToConnect)
zones = append(zones, nbdns.CustomZone{
Domain: peersCustomZone.Domain,
Records: records,
})
}
dnsUpdate.CustomZones = zones
dnsUpdate.NameServerGroups = getPeerNSGroups(a, peerID)
@@ -998,8 +1001,20 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P
continue
}
sourcePeers, peerInSources := a.getAllPeersFromGroups(ctx, rule.Sources, peer.ID, policy.SourcePostureChecks, validatedPeersMap)
destinationPeers, peerInDestinations := a.getAllPeersFromGroups(ctx, rule.Destinations, peer.ID, nil, validatedPeersMap)
var sourcePeers, destinationPeers []*nbpeer.Peer
var peerInSources, peerInDestinations bool
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
sourcePeers, peerInSources = a.getPeerFromResource(rule.SourceResource, peer.ID)
} else {
sourcePeers, peerInSources = a.getAllPeersFromGroups(ctx, rule.Sources, peer.ID, policy.SourcePostureChecks, validatedPeersMap)
}
if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" {
destinationPeers, peerInDestinations = a.getPeerFromResource(rule.DestinationResource, peer.ID)
} else {
destinationPeers, peerInDestinations = a.getAllPeersFromGroups(ctx, rule.Destinations, peer.ID, nil, validatedPeersMap)
}
if rule.Bidirectional {
if peerInSources {
@@ -1121,6 +1136,15 @@ func (a *Account) getAllPeersFromGroups(ctx context.Context, groups []string, pe
return filteredPeers, peerInGroups
}
func (a *Account) getPeerFromResource(resource Resource, peerID string) ([]*nbpeer.Peer, bool) {
peer := a.GetPeer(resource.ID)
if peer == nil {
return []*nbpeer.Peer{}, false
}
return []*nbpeer.Peer{peer}, resource.ID == peerID
}
// validatePostureChecksOnPeer validates the posture checks on a peer
func (a *Account) validatePostureChecksOnPeer(ctx context.Context, sourcePostureChecksID []string, peerID string) bool {
peer, ok := a.Peers[peerID]
@@ -1376,7 +1400,12 @@ func (a *Account) GetNetworkResourcesRoutesToSync(ctx context.Context, peerID st
addedResourceRoute := false
for _, policy := range resourcePolicies[resource.ID] {
peers := a.getUniquePeerIDsFromGroupsIDs(ctx, policy.SourceGroups())
var peers []string
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
peers = []string{policy.Rules[0].SourceResource.ID}
} else {
peers = a.getUniquePeerIDsFromGroupsIDs(ctx, policy.SourceGroups())
}
if addSourcePeers {
for _, pID := range a.getPostureValidPeers(peers, policy.SourcePostureChecks) {
allSourcePeers[pID] = struct{}{}
@@ -1651,3 +1680,24 @@ func peerSupportsPortRanges(peerVer string) bool {
meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer)
return err == nil && meetMinVer
}
// filterZoneRecordsForPeers filters DNS records to only include peers to connect.
func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, peersToConnect []*nbpeer.Peer) []nbdns.SimpleRecord {
filteredRecords := make([]nbdns.SimpleRecord, 0, len(customZone.Records))
peerIPs := make(map[string]struct{})
// Add peer's own IP to include its own DNS records
peerIPs[peer.IP.String()] = struct{}{}
for _, peerToConnect := range peersToConnect {
peerIPs[peerToConnect.IP.String()] = struct{}{}
}
for _, record := range customZone.Records {
if _, exists := peerIPs[record.RData]; exists {
filteredRecords = append(filteredRecords, record)
}
}
return filteredRecords
}

View File

@@ -2,14 +2,17 @@ package types
import (
"context"
"fmt"
"net"
"net/netip"
"slices"
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
nbdns "github.com/netbirdio/netbird/dns"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
@@ -835,3 +838,109 @@ func Test_NetworksNetMapGenShouldExcludeOtherRouters(t *testing.T) {
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
assert.Len(t, sourcePeers, 2, "expected source peers don't match")
}
func Test_FilterZoneRecordsForPeers(t *testing.T) {
tests := []struct {
name string
peer *nbpeer.Peer
customZone nbdns.CustomZone
peersToConnect []*nbpeer.Peer
expectedRecords []nbdns.SimpleRecord
}{
{
name: "empty peers to connect",
customZone: nbdns.CustomZone{
Domain: "netbird.cloud.",
Records: []nbdns.SimpleRecord{
{Name: "peer1.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
},
},
peersToConnect: []*nbpeer.Peer{},
peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
expectedRecords: []nbdns.SimpleRecord{
{Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
},
},
{
name: "multiple peers multiple records match",
customZone: nbdns.CustomZone{
Domain: "netbird.cloud.",
Records: func() []nbdns.SimpleRecord {
var records []nbdns.SimpleRecord
for i := 1; i <= 100; i++ {
records = append(records, nbdns.SimpleRecord{
Name: fmt.Sprintf("peer%d.netbird.cloud", i),
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: 300,
RData: fmt.Sprintf("10.0.%d.%d", i/256, i%256),
})
}
return records
}(),
},
peersToConnect: func() []*nbpeer.Peer {
var peers []*nbpeer.Peer
for _, i := range []int{1, 5, 10, 25, 50, 75, 100} {
peers = append(peers, &nbpeer.Peer{
ID: fmt.Sprintf("peer%d", i),
IP: net.ParseIP(fmt.Sprintf("10.0.%d.%d", i/256, i%256)),
})
}
return peers
}(),
peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
expectedRecords: func() []nbdns.SimpleRecord {
var records []nbdns.SimpleRecord
for _, i := range []int{1, 5, 10, 25, 50, 75, 100} {
records = append(records, nbdns.SimpleRecord{
Name: fmt.Sprintf("peer%d.netbird.cloud", i),
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: 300,
RData: fmt.Sprintf("10.0.%d.%d", i/256, i%256),
})
}
return records
}(),
},
{
name: "peers with multiple DNS labels",
customZone: nbdns.CustomZone{
Domain: "netbird.cloud.",
Records: []nbdns.SimpleRecord{
{Name: "peer1.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "peer1-alt.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "peer1-backup.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "peer2.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2"},
{Name: "peer2-service.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2"},
{Name: "peer3.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.3"},
{Name: "peer3-alt.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.3"},
{Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
},
},
peersToConnect: []*nbpeer.Peer{
{ID: "peer1", IP: net.ParseIP("10.0.0.1"), DNSLabel: "peer1", ExtraDNSLabels: []string{"peer1-alt", "peer1-backup"}},
{ID: "peer2", IP: net.ParseIP("10.0.0.2"), DNSLabel: "peer2", ExtraDNSLabels: []string{"peer2-service"}},
},
peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
expectedRecords: []nbdns.SimpleRecord{
{Name: "peer1.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "peer1-alt.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "peer1-backup.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "peer2.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2"},
{Name: "peer2-service.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2"},
{Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filterZoneRecordsForPeers(tt.peer, tt.customZone, tt.peersToConnect)
assert.Equal(t, len(tt.expectedRecords), len(result))
assert.ElementsMatch(t, tt.expectedRecords, result)
})
}
}

View File

@@ -1,5 +1,12 @@
package types
import (
"errors"
"fmt"
"strconv"
"strings"
)
const (
// PolicyTrafficActionAccept indicates that the traffic is accepted
PolicyTrafficActionAccept = PolicyTrafficActionType("accept")
@@ -134,3 +141,83 @@ func (p *Policy) SourceGroups() []string {
return groupIDs
}
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
rule = strings.TrimSpace(strings.ToLower(rule))
if rule == "all" {
return PolicyRuleProtocolALL, RulePortRange{}, nil
}
if rule == "icmp" {
return PolicyRuleProtocolICMP, RulePortRange{}, nil
}
split := strings.Split(rule, "/")
if len(split) != 2 {
return "", RulePortRange{}, errors.New("invalid rule format: expected protocol/port or protocol/port-range")
}
protoStr := strings.TrimSpace(split[0])
portStr := strings.TrimSpace(split[1])
var protocol PolicyRuleProtocolType
switch protoStr {
case "tcp":
protocol = PolicyRuleProtocolTCP
case "udp":
protocol = PolicyRuleProtocolUDP
case "icmp":
return "", RulePortRange{}, errors.New("icmp does not accept ports; use 'icmp' without '/…'")
default:
return "", RulePortRange{}, fmt.Errorf("invalid protocol: %q", protoStr)
}
portRange, err := parsePortRange(portStr)
if err != nil {
return "", RulePortRange{}, err
}
return protocol, portRange, nil
}
func parsePortRange(portStr string) (RulePortRange, error) {
if strings.Contains(portStr, "-") {
rangeParts := strings.Split(portStr, "-")
if len(rangeParts) != 2 {
return RulePortRange{}, fmt.Errorf("invalid port range %q", portStr)
}
start, err := parsePort(strings.TrimSpace(rangeParts[0]))
if err != nil {
return RulePortRange{}, err
}
end, err := parsePort(strings.TrimSpace(rangeParts[1]))
if err != nil {
return RulePortRange{}, err
}
if start > end {
return RulePortRange{}, fmt.Errorf("invalid port range: start %d > end %d", start, end)
}
return RulePortRange{Start: uint16(start), End: uint16(end)}, nil
}
p, err := parsePort(portStr)
if err != nil {
return RulePortRange{}, err
}
return RulePortRange{Start: uint16(p), End: uint16(p)}, nil
}
func parsePort(portStr string) (int, error) {
if portStr == "" {
return 0, errors.New("empty port")
}
p, err := strconv.Atoi(portStr)
if err != nil {
return 0, fmt.Errorf("invalid port %q: %w", portStr, err)
}
if p < 1 || p > 65535 {
return 0, fmt.Errorf("port out of range (165535): %d", p)
}
return p, nil
}

View File

@@ -4,9 +4,18 @@ import (
"github.com/netbirdio/netbird/shared/management/http/api"
)
type ResourceType string
const (
ResourceTypePeer ResourceType = "peer"
ResourceTypeDomain ResourceType = "domain"
ResourceTypeHost ResourceType = "host"
ResourceTypeSubnet ResourceType = "subnet"
)
type Resource struct {
ID string
Type string
Type ResourceType
}
func (r *Resource) ToAPIResponse() *api.Resource {
@@ -26,5 +35,5 @@ func (r *Resource) FromAPIRequest(req *api.Resource) {
}
r.ID = req.Id
r.Type = string(req.Type)
r.Type = ResourceType(req.Type)
}