Merge branch 'main' into embedded-vnc

This commit is contained in:
Viktor Liu
2026-08-27 16:01:14 +02:00
482 changed files with 34604 additions and 6503 deletions
-103
View File
@@ -1,103 +0,0 @@
package types
import (
"net/netip"
"time"
)
// ComponentPeer is the self-contained peer representation used by
// NetworkMapComponents and the calculated NetworkMap. It carries exactly the
// subset of peer data that crosses the components wire format, so the shared
// calculation layer stays independent of the management server's domain
// types.
type ComponentPeer struct {
ID string
Key string
IP netip.Addr
IPv6 netip.Addr
DNSLabel string
SSHKey string
SSHEnabled bool
ServerSSHAllowed bool
AgentVersion string
SupportsSourcePrefixes bool
SupportsIPv6 bool
LoginExpirationEnabled bool
AddedWithSSOLogin bool
LastLogin time.Time
}
// FQDN returns the peer's FQDN combined of the peer's DNS label and the system's DNS domain.
func (p *ComponentPeer) FQDN(dnsDomain string) string {
if dnsDomain == "" {
return ""
}
return p.DNSLabel + "." + dnsDomain
}
// LoginExpired indicates whether the peer's login has expired, mirroring the
// server-side peer semantics: only SSO-added peers with login expiration
// enabled can expire.
func (p *ComponentPeer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
if !p.AddedWithSSOLogin || !p.LoginExpirationEnabled {
return false, 0
}
timeLeft := time.Until(p.LastLogin.Add(expiresIn))
return timeLeft <= 0, timeLeft
}
// GroupAllName is the reserved name of the default group that contains every peer in an account.
const GroupAllName = "All"
// ComponentGroup is the self-contained group representation used by
// NetworkMapComponents: just the membership view the network-map calculation
// needs, without the server's storage fields.
type ComponentGroup struct {
ID string
PublicID string
Name string
Peers []string
}
// IsGroupAll checks if the group is a default "All" group.
func (g *ComponentGroup) IsGroupAll() bool {
return g.Name == GroupAllName
}
// ComponentRouter is the self-contained network-router representation used by
// NetworkMapComponents.
type ComponentRouter struct {
NetworkID string
PublicID string
Peer string
PeerGroups []string
Masquerade bool
Metric int
Enabled bool
}
// ComponentResourceType mirrors the network-resource type enum on the
// components wire format.
type ComponentResourceType string
const (
ComponentResourceHost ComponentResourceType = "host"
ComponentResourceSubnet ComponentResourceType = "subnet"
ComponentResourceDomain ComponentResourceType = "domain"
)
// ComponentResource is the self-contained network-resource representation
// used by NetworkMapComponents.
type ComponentResource struct {
ID string
PublicID string
NetworkID string
AccountID string
Name string
Description string
Type ComponentResourceType
Address string
Domain string
Prefix netip.Prefix
Enabled bool
}
-16
View File
@@ -1,16 +0,0 @@
package types
// DNSSettings defines dns settings at the account level
type DNSSettings struct {
// DisabledManagementGroups groups whose DNS management is disabled
DisabledManagementGroups []string `gorm:"serializer:json"`
}
// Copy returns a copy of the DNS settings
func (d DNSSettings) Copy() DNSSettings {
settings := DNSSettings{
DisabledManagementGroups: make([]string, len(d.DisabledManagementGroups)),
}
copy(settings.DisabledManagementGroups, d.DisabledManagementGroups)
return settings
}
+7 -28
View File
@@ -3,6 +3,7 @@ package types
import (
"strconv"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/version"
)
@@ -23,31 +24,9 @@ type supportedFeatures struct {
type LookupMap map[string]struct{}
func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool {
return rule.Protocol == PolicyRuleProtocolALL || (rule.Protocol == PolicyRuleProtocolTCP && (portsIncludesSSH(rule.Ports) || portRangeIncludesSSH(rule.PortRanges)))
}
func portRangeIncludesSSH(portRanges []RulePortRange) bool {
for _, pr := range portRanges {
if (pr.Start <= defaultSSHPortNumber && pr.End >= defaultSSHPortNumber) || (pr.Start <= nativeSSHPortNumber && pr.End >= nativeSSHPortNumber) {
return true
}
}
return false
}
func portsIncludesSSH(ports []string) bool {
for _, port := range ports {
if port == defaultSSHPortString || port == nativeSSHPortString {
return true
}
}
return false
}
// ExpandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules.
func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPeer) []*FirewallRule {
features := peerSupportedFirewallFeatures(peer.AgentVersion)
func ExpandPortsAndRanges(base FirewallRule, rule *nmdata.PolicyRule, peer *nmdata.Peer) []*FirewallRule {
features := peerSupportedFirewallFeatures(peer.Meta.WtVersion)
var expanded []*FirewallRule
@@ -64,7 +43,7 @@ func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPe
fr := base
if features.portRanges {
fr.PortRange = portRange
fr.PortRange = RulePortRange{Start: portRange.Start, End: portRange.End}
} else {
if portRange.Start != portRange.End {
continue
@@ -74,7 +53,7 @@ func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPe
expanded = append(expanded, &fr)
}
if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == PolicyRuleProtocolNetbirdSSH {
if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == string(PolicyRuleProtocolNetbirdSSH) {
expanded = addNativeSSHRule(base, expanded)
}
@@ -104,8 +83,8 @@ func isPortInRule(portString string, portInt uint16, rule *FirewallRule) bool {
return rule.Port == portString || (rule.PortRange.Start <= portInt && portInt <= rule.PortRange.End)
}
func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *ComponentPeer) bool {
return supportsNative && peer.SSHEnabled && peer.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP
func shouldCheckRulesForNativeSSH(supportsNative bool, rule *nmdata.PolicyRule, peer *nmdata.Peer) bool {
return supportsNative && peer.SSHEnabled && peer.Meta.Flags.ServerSSHAllowed && rule.Protocol == string(PolicyRuleProtocolTCP)
}
func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
+11 -10
View File
@@ -10,6 +10,7 @@ import (
log "github.com/sirupsen/logrus"
nbroute "github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
const (
@@ -50,7 +51,7 @@ func (r *FirewallRule) Equal(other *FirewallRule) bool {
// For static routes, source ranges match the destination family (v4 or v6).
// For dynamic routes (domain-based), separate v4 and v6 rules are generated
// so the routing peer's forwarding chain allows both address families.
func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*ComponentPeer, direction int, includeIPv6 bool) []*RouteFirewallRule {
func GenerateRouteFirewallRules(ctx context.Context, route *nmdata.Route, rule *nmdata.PolicyRule, groupPeers []*nmdata.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule {
rulesExists := make(map[string]struct{})
rules := make([]*RouteFirewallRule, 0)
@@ -71,11 +72,11 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
baseRule := RouteFirewallRule{
PolicyID: rule.PolicyID,
RouteID: route.ID,
RouteID: nbroute.ID(route.ID),
SourceRanges: sourceRanges,
Action: string(rule.Action),
Action: rule.Action,
Destination: route.Network.String(),
Protocol: string(rule.Protocol),
Protocol: rule.Protocol,
Domains: route.Domains,
IsDynamic: route.IsDynamic(),
}
@@ -93,7 +94,7 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
v6Rule.SourceRanges = v6Sources
if isDefaultV4 {
v6Rule.Destination = "::/0"
v6Rule.RouteID = route.ID + "-v6-default"
v6Rule.RouteID = nbroute.ID(route.ID + "-v6-default")
}
if len(rule.Ports) == 0 {
rules = append(rules, generateRulesWithPortRanges(v6Rule, rule, rulesExists)...)
@@ -106,7 +107,7 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
}
// splitPeerSourcesByFamily separates peer IPs into v4 (/32) and v6 (/128) source ranges.
func splitPeerSourcesByFamily(groupPeers []*ComponentPeer) (v4, v6 []string) {
func splitPeerSourcesByFamily(groupPeers []*nmdata.Peer) (v4, v6 []string) {
v4 = make([]string, 0, len(groupPeers))
v6 = make([]string, 0, len(groupPeers))
for _, peer := range groupPeers {
@@ -122,7 +123,7 @@ func splitPeerSourcesByFamily(groupPeers []*ComponentPeer) (v4, v6 []string) {
}
// generateRulesForPeer generates rules for a given peer based on ports and port ranges.
func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *nmdata.PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
rules := make([]*RouteFirewallRule, 0)
ruleIDBase := generateRuleIDBase(rule, baseRule)
@@ -138,7 +139,7 @@ func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, r
if _, ok := rulesExists[ruleID]; !ok {
rulesExists[ruleID] = struct{}{}
pr := baseRule
pr.PortRange = portRange
pr.PortRange = RulePortRange{Start: portRange.Start, End: portRange.End}
rules = append(rules, &pr)
}
}
@@ -150,7 +151,7 @@ func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, r
}
// generateRulesWithPorts generates rules when specific ports are provided.
func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rule *nmdata.PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
rules := make([]*RouteFirewallRule, 0)
ruleIDBase := generateRuleIDBase(rule, baseRule)
@@ -176,6 +177,6 @@ func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rul
}
// generateRuleIDBase generates the base rule ID for checking duplicates.
func generateRuleIDBase(rule *PolicyRule, baseRule RouteFirewallRule) string {
func generateRuleIDBase(rule *nmdata.PolicyRule, baseRule RouteFirewallRule) string {
return rule.ID + strings.Join(baseRule.SourceRanges, ",") + strconv.Itoa(FirewallRuleDirectionIN) + baseRule.Protocol + baseRule.Action
}
+34 -34
View File
@@ -8,12 +8,12 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
func TestSplitPeerSourcesByFamily(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -35,7 +35,7 @@ func TestSplitPeerSourcesByFamily(t *testing.T) {
}
func TestGenerateRouteFirewallRules_V4Route(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -45,15 +45,15 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) {
},
}
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
Network: netip.MustParsePrefix("10.0.0.0/24"),
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
@@ -64,7 +64,7 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) {
}
func TestGenerateRouteFirewallRules_V6Route(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -74,15 +74,15 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) {
},
}
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
Network: netip.MustParsePrefix("2001:db8::/32"),
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
@@ -92,7 +92,7 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) {
}
func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -102,16 +102,16 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) {
},
}
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
NetworkType: nmdata.NetworkTypeDomain,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
@@ -125,21 +125,21 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) {
}
func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{IP: netip.MustParseAddr("100.64.0.1")},
{IP: netip.MustParseAddr("100.64.0.2")},
}
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
NetworkType: nmdata.NetworkTypeDomain,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
@@ -149,7 +149,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) {
}
func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -161,15 +161,15 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
}
t.Run("v6 route excluded", func(t *testing.T) {
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
Network: netip.MustParsePrefix("2001:db8::/32"),
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
@@ -177,16 +177,16 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
})
t.Run("dynamic route only v4", func(t *testing.T) {
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
NetworkType: nmdata.NetworkTypeDomain,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
+23 -285
View File
@@ -1,47 +1,28 @@
package types
import (
"encoding/binary"
"fmt"
"math/rand"
"net"
"net/netip"
"slices"
"sync"
"time"
"github.com/c-robinson/iplib"
"github.com/rs/xid"
"golang.org/x/exp/maps"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/management/status"
)
const (
// SubnetSize is a size of the subnet of the global network, e.g. 100.77.0.0/16
SubnetSize = 16
// NetSize is a global network size 100.64.0.0/10
NetSize = 10
// AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.64.30.1/32)
AllowedIPsFormat = "%s/32"
// AllowedIPsV6Format generates AllowedIPs format for v6 (e.g. fd12:3456:7890::1/128)
AllowedIPsV6Format = "%s/128"
// IPv6SubnetSize is the prefix length of per-account IPv6 subnets.
// Each account gets a /64 from its unique /48 ULA prefix.
IPv6SubnetSize = 64
)
type NetworkMap struct {
Peers []*ComponentPeer
Network *Network
Routes []*route.Route
Peers []*nmdata.Peer
Network *nmdata.Network
Routes []*nmdata.Route
DNSConfig nbdns.Config
OfflinePeers []*ComponentPeer
OfflinePeers []*nmdata.Peer
FirewallRules []*FirewallRule
RoutesFirewallRules []*RouteFirewallRule
ForwardingRules []*ForwardingRule
@@ -65,39 +46,8 @@ func (nm *NetworkMap) Merge(other *NetworkMap) {
nm.ForceRoutingPeerDNSResolution = nm.ForceRoutingPeerDNSResolution || other.ForceRoutingPeerDNSResolution
}
type comparableObject[T any] interface {
Equal(other T) bool
}
func mergeUnique[T comparableObject[T]](arr1, arr2 []T) []T {
var result []T
for _, item := range arr1 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
for _, item := range arr2 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
return result
}
func containsEqual[T comparableObject[T]](slice []T, element T) bool {
for _, item := range slice {
if item.Equal(element) {
return true
}
}
return false
}
func mergeUniquePeersByID(peers1, peers2 []*ComponentPeer) []*ComponentPeer {
result := make(map[string]*ComponentPeer)
func mergeUniquePeersByID(peers1, peers2 []*nmdata.Peer) []*nmdata.Peer {
result := make(map[string]*nmdata.Peer)
for _, peer := range peers1 {
result[peer.ID] = peer
}
@@ -153,245 +103,33 @@ func ipToBytes(ip net.IP) []byte {
return ip.To16()
}
type Network struct {
Identifier string `json:"id"`
Net net.IPNet `gorm:"serializer:json"`
// NetV6 is the IPv6 ULA subnet for this account's overlay. Empty if not yet allocated.
NetV6 net.IPNet `gorm:"serializer:json"`
Dns string
// Serial is an ID that increments by 1 when any change to the network happened (e.g. new peer has been added).
// Used to synchronize state to the client apps.
Serial uint64
Mu sync.Mutex `json:"-" gorm:"-"`
type comparableObject[T any] interface {
Equal(other T) bool
}
// NewNetwork creates a new Network initializing it with a Serial=0
// It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets)
// and a random /64 subnet from fd00:4e42::/32 for IPv6.
func NewNetwork() *Network {
n := iplib.NewNet4(net.ParseIP("100.64.0.0"), NetSize)
sub, _ := n.Subnet(SubnetSize)
func mergeUnique[T comparableObject[T]](arr1, arr2 []T) []T {
var result []T
s := rand.NewSource(time.Now().UnixNano())
r := rand.New(s)
intn := r.Intn(len(sub))
return &Network{
Identifier: xid.New().String(),
Net: sub[intn].IPNet,
NetV6: AllocateIPv6Subnet(r),
Dns: "",
Serial: 0,
}
}
// AllocateIPv6Subnet generates a random RFC 4193 ULA /64 prefix.
// The format follows RFC 4193 section 3.1: fd + 40-bit Global ID + 16-bit Subnet ID.
// The Global ID and Subnet ID are randomized (simplified from the SHA-1 algorithm
// in section 3.2.2), giving 2^56 possible /64 subnets across all accounts.
func AllocateIPv6Subnet(r *rand.Rand) net.IPNet {
ip := make(net.IP, 16)
ip[0] = 0xfd
// Bytes 1-5: 40-bit random Global ID
ip[1] = byte(r.Intn(256))
ip[2] = byte(r.Intn(256))
ip[3] = byte(r.Intn(256))
ip[4] = byte(r.Intn(256))
ip[5] = byte(r.Intn(256))
// Bytes 6-7: 16-bit random Subnet ID
ip[6] = byte(r.Intn(256))
ip[7] = byte(r.Intn(256))
return net.IPNet{
IP: ip,
Mask: net.CIDRMask(IPv6SubnetSize, 128),
}
}
// IncSerial increments Serial by 1 reflecting that the network state has been changed
func (n *Network) IncSerial() {
n.Mu.Lock()
defer n.Mu.Unlock()
n.Serial++
}
// CurrentSerial returns the Network.Serial of the network (latest state id)
func (n *Network) CurrentSerial() uint64 {
n.Mu.Lock()
defer n.Mu.Unlock()
return n.Serial
}
func (n *Network) Copy() *Network {
n.Mu.Lock()
defer n.Mu.Unlock()
return &Network{
Identifier: n.Identifier,
Net: n.Net,
NetV6: n.NetV6,
Dns: n.Dns,
Serial: n.Serial,
}
}
// AllocatePeerIP picks an available IP from a netip.Prefix.
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps.
// E.g. if prefix=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3.
func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, error) {
b := prefix.Masked().Addr().As4()
baseIP := binary.BigEndian.Uint32(b[:])
hostBits := 32 - prefix.Bits()
totalIPs := uint32(1 << hostBits)
taken := make(map[uint32]struct{}, len(takenIps)+1)
taken[baseIP] = struct{}{} // reserve network IP
taken[baseIP+totalIPs-1] = struct{}{} // reserve broadcast IP
for _, ip := range takenIps {
ab := ip.As4()
taken[binary.BigEndian.Uint32(ab[:])] = struct{}{}
}
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
maxAttempts := (int(totalIPs) - len(taken)) / 100
for i := 0; i < maxAttempts; i++ {
offset := uint32(rng.Intn(int(totalIPs-2))) + 1
candidate := baseIP + offset
if _, exists := taken[candidate]; !exists {
return uint32ToIP(candidate), nil
for _, item := range arr1 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
for offset := uint32(1); offset < totalIPs-1; offset++ {
candidate := baseIP + offset
if _, exists := taken[candidate]; !exists {
return uint32ToIP(candidate), nil
for _, item := range arr2 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
return netip.Addr{}, status.Errorf(status.PreconditionFailed, "network %s is out of IPs", prefix.String())
return result
}
// AllocateRandomPeerIP picks a random available IP from a netip.Prefix.
func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
b := prefix.Masked().Addr().As4()
baseIP := binary.BigEndian.Uint32(b[:])
hostBits := 32 - prefix.Bits()
totalIPs := uint32(1 << hostBits)
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
offset := uint32(rng.Intn(int(totalIPs-2))) + 1
candidate := baseIP + offset
return uint32ToIP(candidate), nil
}
// AllocateRandomPeerIPv6 picks a random host address within the given IPv6 prefix.
// Only the host bits (after the prefix length) are randomized.
func AllocateRandomPeerIPv6(prefix netip.Prefix) (netip.Addr, error) {
ones := prefix.Bits()
if ones == 0 || ones > 126 || !prefix.Addr().Is6() {
return netip.Addr{}, fmt.Errorf("invalid IPv6 subnet: %s", prefix.String())
}
ip := prefix.Addr().As16()
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
// Determine which byte the host bits start in
firstHostByte := ones / 8
// If the prefix doesn't end on a byte boundary, handle the partial byte
partialBits := ones % 8
if partialBits > 0 {
// Keep the network bits in the partial byte, randomize the rest
hostMask := byte(0xff >> partialBits)
ip[firstHostByte] = (ip[firstHostByte] & ^hostMask) | (byte(rng.Intn(256)) & hostMask)
firstHostByte++
}
// Randomize remaining full host bytes
for i := firstHostByte; i < 16; i++ {
ip[i] = byte(rng.Intn(256))
}
// Avoid all-zeros and all-ones host parts by checking only host bits.
if isHostAllZeroOrOnes(ip[:], ones) {
ip = prefix.Masked().Addr().As16()
ip[15] |= 0x01
}
return netip.AddrFrom16(ip).Unmap(), nil
}
// isHostAllZeroOrOnes checks whether all host bits (after prefixLen) are zero or all ones.
func isHostAllZeroOrOnes(ip []byte, prefixLen int) bool {
hostStart := prefixLen / 8
partialBits := prefixLen % 8
hostSlice := slices.Clone(ip[hostStart:])
if partialBits > 0 {
hostSlice[0] &= 0xff >> partialBits
}
allZero := !slices.ContainsFunc(hostSlice, func(v byte) bool { return v != 0 })
if allZero {
return true
}
// Build the all-ones mask for host bits
onesMask := make([]byte, len(hostSlice))
for i := range onesMask {
onesMask[i] = 0xff
}
if partialBits > 0 {
onesMask[0] = 0xff >> partialBits
}
return slices.Equal(hostSlice, onesMask)
}
func uint32ToIP(n uint32) netip.Addr {
var b [4]byte
binary.BigEndian.PutUint32(b[:], n)
return netip.AddrFrom4(b)
}
// generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list
func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int) {
var ips []net.IP
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); incIP(ip) {
if _, ok := exclusions[ip.String()]; !ok && ip[3] != 0 {
ips = append(ips, copyIP(ip))
}
}
// remove network address, broadcast and Fake DNS resolver address
lenIPs := len(ips)
switch {
case lenIPs < 2:
return ips, lenIPs
case lenIPs < 3:
return ips[1 : len(ips)-1], lenIPs - 2
default:
return ips[1 : len(ips)-2], lenIPs - 3
}
}
func copyIP(ip net.IP) net.IP {
dup := make(net.IP, len(ip))
copy(dup, ip)
return dup
}
func incIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
func containsEqual[T comparableObject[T]](slice []T, element T) bool {
for _, item := range slice {
if item.Equal(element) {
return true
}
}
return false
}
@@ -1,41 +0,0 @@
package types
import (
"testing"
"github.com/stretchr/testify/assert"
)
type testObject struct {
value int
}
func (t testObject) Equal(other testObject) bool {
return t.value == other.value
}
func Test_MergeUniqueArraysWithoutDuplicates(t *testing.T) {
arr1 := []testObject{{value: 1}, {value: 2}}
arr2 := []testObject{{value: 2}, {value: 3}}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 3)
assert.Contains(t, result, testObject{value: 1})
assert.Contains(t, result, testObject{value: 2})
assert.Contains(t, result, testObject{value: 3})
}
func Test_MergeUniqueHandlesEmptyArrays(t *testing.T) {
arr1 := []testObject{}
arr2 := []testObject{}
result := mergeUnique(arr1, arr2)
assert.Empty(t, result)
}
func Test_MergeUniqueHandlesOneEmptyArray(t *testing.T) {
arr1 := []testObject{{value: 1}, {value: 2}}
arr2 := []testObject{}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 2)
assert.Contains(t, result, testObject{value: 1})
assert.Contains(t, result, testObject{value: 2})
}
+24 -247
View File
@@ -1,264 +1,41 @@
package types
import (
"encoding/binary"
"net"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewNetwork(t *testing.T) {
network := NewNetwork()
// generated net should be a subnet of a larger 100.64.0.0/10 net
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
assert.Equal(t, ipNet.Contains(network.Net.IP), true)
type mergeTestObject struct {
value int
}
func TestAllocatePeerIP(t *testing.T) {
prefix := netip.MustParsePrefix("100.64.0.0/24")
var ips []netip.Addr
for i := 0; i < 252; i++ {
ip, err := AllocatePeerIP(prefix, ips)
if err != nil {
t.Fatal(err)
}
ips = append(ips, ip)
}
assert.Len(t, ips, 252)
uniq := make(map[string]struct{})
for _, ip := range ips {
if _, ok := uniq[ip.String()]; !ok {
uniq[ip.String()] = struct{}{}
} else {
t.Errorf("found duplicate IP %s", ip.String())
}
}
func (t mergeTestObject) Equal(other mergeTestObject) bool {
return t.value == other.value
}
func TestAllocatePeerIPSmallSubnet(t *testing.T) {
// Test /27 network (10.0.0.0/27) - should only have 30 usable IPs (10.0.0.1 to 10.0.0.30)
prefix := netip.MustParsePrefix("10.0.0.0/27")
var ips []netip.Addr
// Allocate all available IPs in the /27 network
for i := 0; i < 30; i++ {
ip, err := AllocatePeerIP(prefix, ips)
if err != nil {
t.Fatal(err)
}
// Verify IP is within the correct range
if !prefix.Contains(ip) {
t.Errorf("allocated IP %s is not within network %s", ip.String(), prefix.String())
}
ips = append(ips, ip)
}
assert.Len(t, ips, 30)
// Verify all IPs are unique
uniq := make(map[string]struct{})
for _, ip := range ips {
if _, ok := uniq[ip.String()]; !ok {
uniq[ip.String()] = struct{}{}
} else {
t.Errorf("found duplicate IP %s", ip.String())
}
}
// Try to allocate one more IP - should fail as network is full
_, err := AllocatePeerIP(prefix, ips)
if err == nil {
t.Error("expected error when network is full, but got none")
}
func Test_MergeUniqueArraysWithoutDuplicates(t *testing.T) {
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
arr2 := []mergeTestObject{{value: 2}, {value: 3}}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 3)
assert.Contains(t, result, mergeTestObject{value: 1})
assert.Contains(t, result, mergeTestObject{value: 2})
assert.Contains(t, result, mergeTestObject{value: 3})
}
func TestAllocatePeerIPVariousCIDRs(t *testing.T) {
testCases := []struct {
name string
cidr string
expectedUsable int
}{
{"/30 network", "192.168.1.0/30", 2}, // 4 total - 2 reserved = 2 usable
{"/29 network", "192.168.1.0/29", 6}, // 8 total - 2 reserved = 6 usable
{"/28 network", "192.168.1.0/28", 14}, // 16 total - 2 reserved = 14 usable
{"/27 network", "192.168.1.0/27", 30}, // 32 total - 2 reserved = 30 usable
{"/26 network", "192.168.1.0/26", 62}, // 64 total - 2 reserved = 62 usable
{"/25 network", "192.168.1.0/25", 126}, // 128 total - 2 reserved = 126 usable
{"/16 network", "10.0.0.0/16", 65534}, // 65536 total - 2 reserved = 65534 usable
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
prefix, err := netip.ParsePrefix(tc.cidr)
require.NoError(t, err)
prefix = prefix.Masked()
var ips []netip.Addr
// For larger networks, test only a subset to avoid long test runs
testCount := tc.expectedUsable
if testCount > 1000 {
testCount = 1000
}
// Allocate IPs and verify they're within the correct range
for i := 0; i < testCount; i++ {
ip, err := AllocatePeerIP(prefix, ips)
require.NoError(t, err, "failed to allocate IP %d", i)
// Verify IP is within the correct range
assert.True(t, prefix.Contains(ip), "allocated IP %s is not within network %s", ip.String(), prefix.String())
// Verify IP is not network or broadcast address
networkAddr := prefix.Masked().Addr()
hostBits := 32 - prefix.Bits()
b := networkAddr.As4()
baseIP := binary.BigEndian.Uint32(b[:])
broadcastIP := uint32ToIP(baseIP + (1 << hostBits) - 1)
assert.NotEqual(t, networkAddr, ip, "allocated network address %s", ip.String())
assert.NotEqual(t, broadcastIP, ip, "allocated broadcast address %s", ip.String())
ips = append(ips, ip)
}
assert.Len(t, ips, testCount)
// Verify all IPs are unique
uniq := make(map[string]struct{})
for _, ip := range ips {
ipStr := ip.String()
assert.NotContains(t, uniq, ipStr, "found duplicate IP %s", ipStr)
uniq[ipStr] = struct{}{}
}
})
}
func Test_MergeUniqueHandlesEmptyArrays(t *testing.T) {
arr1 := []mergeTestObject{}
arr2 := []mergeTestObject{}
result := mergeUnique(arr1, arr2)
assert.Empty(t, result)
}
func TestGenerateIPs(t *testing.T) {
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
ips, ipsLen := generateIPs(&ipNet, map[string]struct{}{"100.64.0.0": {}})
if ipsLen != 252 {
t.Errorf("expected 252 ips, got %d", len(ips))
return
}
if ips[len(ips)-1].String() != "100.64.0.253" {
t.Errorf("expected last ip to be: 100.64.0.253, got %s", ips[len(ips)-1].String())
}
}
func TestNewNetworkHasIPv6(t *testing.T) {
network := NewNetwork()
assert.NotNil(t, network.NetV6.IP, "v6 subnet should be allocated")
assert.True(t, network.NetV6.IP.To4() == nil, "v6 subnet should be IPv6")
assert.Equal(t, byte(0xfd), network.NetV6.IP[0], "v6 subnet should be ULA (fd prefix)")
ones, bits := network.NetV6.Mask.Size()
assert.Equal(t, 64, ones, "v6 subnet should be /64")
assert.Equal(t, 128, bits)
}
func TestAllocateIPv6SubnetUniqueness(t *testing.T) {
seen := make(map[string]struct{})
for i := 0; i < 100; i++ {
network := NewNetwork()
key := network.NetV6.IP.String()
_, duplicate := seen[key]
assert.False(t, duplicate, "duplicate v6 subnet: %s", key)
seen[key] = struct{}{}
}
}
func TestAllocateRandomPeerIPv6(t *testing.T) {
prefix := netip.MustParsePrefix("fd12:3456:7890:abcd::/64")
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
assert.True(t, ip.Is6(), "should be IPv6")
assert.True(t, prefix.Contains(ip), "should be within subnet")
// First 8 bytes (network prefix) should match
b := ip.As16()
prefixBytes := prefix.Addr().As16()
assert.Equal(t, prefixBytes[:8], b[:8], "prefix should match")
// Interface ID should not be all zeros
allZero := true
for _, v := range b[8:] {
if v != 0 {
allZero = false
break
}
}
assert.False(t, allZero, "interface ID should not be all zeros")
}
func TestAllocateRandomPeerIPv6_VariousPrefixes(t *testing.T) {
tests := []struct {
name string
cidr string
prefix int
}{
{"standard /64", "fd00:1234:5678:abcd::/64", 64},
{"small /112", "fd00:1234:5678:abcd::/112", 112},
{"large /48", "fd00:1234::/48", 48},
{"non-boundary /60", "fd00:1234:5670::/60", 60},
{"non-boundary /52", "fd00:1230::/52", 52},
{"minimum /120", "fd00:1234:5678:abcd::100/120", 120},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefix, err := netip.ParsePrefix(tt.cidr)
require.NoError(t, err)
prefix = prefix.Masked()
assert.Equal(t, tt.prefix, prefix.Bits())
for i := 0; i < 50; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
assert.True(t, prefix.Contains(ip), "IP %s should be within %s", ip, prefix)
}
})
}
}
func TestAllocateRandomPeerIPv6_PreservesNetworkBits(t *testing.T) {
// For a /112, bytes 0-13 should be preserved, only bytes 14-15 should vary
prefix := netip.MustParsePrefix("fd00:1234:5678:abcd:ef01:2345:6789:0/112")
prefixBytes := prefix.Addr().As16()
for i := 0; i < 20; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
// First 14 bytes (112 bits = 14 bytes) must match the network
b := ip.As16()
assert.Equal(t, prefixBytes[:14], b[:14], "network bytes should be preserved for /112")
}
}
func TestAllocateRandomPeerIPv6_NonByteBoundary(t *testing.T) {
// For a /60, the first 7.5 bytes are network, so byte 7 is partial
prefix := netip.MustParsePrefix("fd00:1234:5678:abc0::/60")
prefixBytes := prefix.Addr().As16()
for i := 0; i < 50; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
b := ip.As16()
assert.True(t, prefix.Contains(ip), "IP %s should be within %s", ip, prefix)
// First 7 bytes must match exactly
assert.Equal(t, prefixBytes[:7], b[:7], "full network bytes should match for /60")
// Byte 7: top 4 bits (0xc = 1100) must be preserved
assert.Equal(t, prefixBytes[7]&0xf0, b[7]&0xf0, "partial byte network bits should be preserved for /60")
}
func Test_MergeUniqueHandlesOneEmptyArray(t *testing.T) {
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
arr2 := []mergeTestObject{}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 2)
assert.Contains(t, result, mergeTestObject{value: 1})
assert.Contains(t, result, mergeTestObject{value: 2})
}
+126 -109
View File
@@ -13,33 +13,34 @@ import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
auth "github.com/netbirdio/netbird/shared/sessionauth"
)
type NetworkMapComponents struct {
PeerID string
Network *Network
AccountSettings *AccountSettingsInfo
DNSSettings *DNSSettings
Network *nmdata.Network
AccountSettings *nmdata.AccountSettingsInfo
DNSSettings *nmdata.DNSSettings
CustomZoneDomain string
Peers map[string]*ComponentPeer
Groups map[string]*ComponentGroup
Policies []*Policy
Routes []*route.Route
NameServerGroups []*nbdns.NameServerGroup
AllDNSRecords []nbdns.SimpleRecord
AccountZones []nbdns.CustomZone
ResourcePoliciesMap map[string][]*Policy
RoutersMap map[string]map[string]*ComponentRouter
NetworkResources []*ComponentResource
Peers map[string]*nmdata.Peer
Groups map[string]*nmdata.Group
Policies []*nmdata.Policy
Routes []*nmdata.Route
NameServerGroups []*nmdata.NameServerGroup
AllDNSRecords []nmdata.SimpleRecord
AccountZones []nmdata.CustomZone
ResourcePoliciesMap map[string][]*nmdata.Policy
RoutersMap map[string]map[string]*nmdata.NetworkRouter
NetworkResources []*nmdata.NetworkResource
GroupIDToUserIDs map[string][]string
AllowedUserIDs map[string]struct{}
PostureFailedPeers map[string]map[string]struct{}
RouterPeers map[string]*ComponentPeer
RouterPeers map[string]*nmdata.Peer
// NetworkXIDToPublicID maps Network.ID (xid) → PublicID.
// Consumed by the envelope encoder to
@@ -51,20 +52,21 @@ type NetworkMapComponents struct {
// Same role as NetworkXIDToPublicID, used for PostureFailedPeers keys and
// policy SourcePostureChecks references.
PostureCheckXIDToPublicID map[string]string
routesByPeerOnce sync.Once
routesByPeerIdx map[string][]routeIndexEntry
// true when returning an empty-like map (returned instead of nil)
empty bool
// ForceRoutingPeerDNSResolution forces the peer to run/use routing-peer DNS
// resolution regardless of the account-global setting, for reverse-proxy
// domain targets.
ForceRoutingPeerDNSResolution bool
routesByPeerOnce sync.Once
routesByPeerIdx map[string][]routeIndexEntry
// true when returning an empty-like map (returned instead of nil)
empty bool
}
type routeIndexEntry struct {
route *route.Route
route *nmdata.Route
viaGroup bool
}
@@ -80,15 +82,15 @@ func EmptyNetworkMapComponents(nm *NetworkMapComponents) *NetworkMapComponents {
return nm
}
func (c *NetworkMapComponents) GetPeerInfo(peerID string) *ComponentPeer {
func (c *NetworkMapComponents) GetPeerInfo(peerID string) *nmdata.Peer {
return c.Peers[peerID]
}
func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *ComponentPeer {
func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *nmdata.Peer {
return c.RouterPeers[peerID]
}
func (c *NetworkMapComponents) GetGroupInfo(groupID string) *ComponentGroup {
func (c *NetworkMapComponents) GetGroupInfo(groupID string) *nmdata.Group {
return c.Groups[groupID]
}
@@ -144,8 +146,8 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
peersToConnect, expiredPeers := c.filterPeersByLoginExpiration(aclPeers)
includeIPv6 := false
if p := c.Peers[targetPeerID]; p != nil {
includeIPv6 = p.SupportsIPv6 && p.IPv6.IsValid()
if p := c.GetPeerInfo(targetPeerID); p != nil {
includeIPv6 = p.SupportsIPv6() && p.IPv6.IsValid()
}
routesUpdate := filterAndExpandRoutes(c.getRoutesToSync(targetPeerID, peersToConnect, peerGroups), includeIPv6)
routesFirewallRules := c.getPeerRoutesFirewallRules(ctx, targetPeerID, includeIPv6)
@@ -176,11 +178,11 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
if c.CustomZoneDomain != "" && len(c.AllDNSRecords) > 0 {
customZones = append(customZones, nbdns.CustomZone{
Domain: c.CustomZoneDomain,
Records: c.AllDNSRecords,
Records: toRealRecords(c.AllDNSRecords),
})
}
customZones = append(customZones, c.AccountZones...)
customZones = append(customZones, toRealZones(c.AccountZones)...)
dnsUpdate.CustomZones = customZones
dnsUpdate.NameServerGroups = c.getPeerNSGroupsFromGroups(targetPeerID, peerGroups)
@@ -188,7 +190,7 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
return &NetworkMap{
Peers: peersToConnectIncludingRouters,
Network: c.Network.Copy(),
Network: c.Network,
Routes: append(filterAndExpandRoutes(networkResourcesRoutes, includeIPv6), routesUpdate...),
DNSConfig: dnsUpdate,
OfflinePeers: expiredPeers,
@@ -209,7 +211,7 @@ func (c *NetworkMapComponents) IsEmpty() bool {
// peerConnectionResult holds the output of getPeerConnectionResources.
type peerConnectionResult struct {
peers []*ComponentPeer
peers []*nmdata.Peer
firewallRules []*FirewallRule
authorizedUsers map[string]map[string]struct{}
vncAuthorizedUsers map[string]map[string]struct{}
@@ -227,11 +229,11 @@ func (c *NetworkMapComponents) getPeerConnectionResources(ctx context.Context, t
state := NewPeerConnResolveState()
for _, policy := range c.Policies {
if !policy.Enabled {
if policy == nil || !policy.Enabled {
continue
}
for _, rule := range policy.Rules {
if !rule.Enabled {
if rule == nil || !rule.Enabled {
continue
}
c.applyPolicyRule(ctx, rule, policy.SourcePostureChecks, targetPeer, targetPeerID, generateResources, state)
@@ -251,21 +253,21 @@ func (c *NetworkMapComponents) getPeerConnectionResources(ctx context.Context, t
func (c *NetworkMapComponents) applyPolicyRule(
ctx context.Context,
rule *PolicyRule,
rule *nmdata.PolicyRule,
sourcePostureChecks []string,
targetPeer *ComponentPeer,
targetPeer *nmdata.Peer,
targetPeerID string,
generateResources func(*PolicyRule, []*ComponentPeer, int),
generateResources func(*nmdata.PolicyRule, []*nmdata.Peer, int),
state *PeerConnResolveState,
) {
sourcePeers, peerInSources := c.resolveRuleEndpoint(rule.SourceResource, rule.Sources, targetPeerID, sourcePostureChecks)
destinationPeers, peerInDestinations := c.resolveRuleEndpoint(rule.DestinationResource, rule.Destinations, targetPeerID, nil)
cb := RuleAuthCallbacks{
CollectSSHUsers: func(r *PolicyRule, t map[string]map[string]struct{}) {
CollectSSHUsers: func(r *nmdata.PolicyRule, t map[string]map[string]struct{}) {
c.collectAuthorizedUsers(ctx, r, t)
},
CollectVNCUsers: func(r *PolicyRule, t map[string]map[string]struct{}) {
CollectVNCUsers: func(r *nmdata.PolicyRule, t map[string]map[string]struct{}) {
c.collectAuthorizedUsers(ctx, r, t)
},
GetAllowedUserIDs: c.getAllowedUserIDs,
@@ -274,19 +276,19 @@ func (c *NetworkMapComponents) applyPolicyRule(
}
func (c *NetworkMapComponents) resolveRuleEndpoint(
resource Resource,
resource nmdata.Resource,
groups []string,
peerID string,
postureChecks []string,
) ([]*ComponentPeer, bool) {
if resource.Type == ResourceTypePeer && resource.ID != "" {
) ([]*nmdata.Peer, bool) {
if resource.Type == string(ResourceTypePeer) && resource.ID != "" {
return c.getPeerFromResource(resource, peerID, postureChecks)
}
return c.getAllPeersFromGroups(groups, peerID, postureChecks)
}
// collectAuthorizedUsers populates the target map with authorized user mappings from the rule.
func (c *NetworkMapComponents) collectAuthorizedUsers(ctx context.Context, rule *PolicyRule, target map[string]map[string]struct{}) {
func (c *NetworkMapComponents) collectAuthorizedUsers(ctx context.Context, rule *nmdata.PolicyRule, target map[string]map[string]struct{}) {
switch {
case len(rule.AuthorizedGroups) > 0:
MergeAuthorizedGroupUsers(ctx, rule.AuthorizedGroups, c.GroupIDToUserIDs, target)
@@ -306,13 +308,13 @@ func (c *NetworkMapComponents) getAllowedUserIDs() map[string]struct{} {
return make(map[string]struct{})
}
func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *ComponentPeer) (func(*PolicyRule, []*ComponentPeer, int), func() ([]*ComponentPeer, []*FirewallRule)) {
func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nmdata.Peer) (func(*nmdata.PolicyRule, []*nmdata.Peer, int), func() ([]*nmdata.Peer, []*FirewallRule)) {
rulesExists := make(map[string]struct{})
peersExists := make(map[string]struct{})
rules := make([]*FirewallRule, 0)
peers := make([]*ComponentPeer, 0)
peers := make([]*nmdata.Peer, 0)
return func(rule *PolicyRule, groupPeers []*ComponentPeer, direction int) {
return func(rule *nmdata.PolicyRule, groupPeers []*nmdata.Peer, direction int) {
effectiveRule, protocol := NormalizePolicyRuleProtocol(rule)
rule = effectiveRule
@@ -362,15 +364,15 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *ComponentPeer)
PortsJoined: portsJoined,
})
}
}, func() ([]*ComponentPeer, []*FirewallRule) {
}, func() ([]*nmdata.Peer, []*FirewallRule) {
return peers, rules
}
}
func (c *NetworkMapComponents) getAllPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string) ([]*ComponentPeer, bool) {
func (c *NetworkMapComponents) getAllPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string) ([]*nmdata.Peer, bool) {
peerInGroups := false
uniquePeerIDs := c.getUniquePeerIDsFromGroupsIDs(groups)
filteredPeers := make([]*ComponentPeer, 0, len(uniquePeerIDs))
filteredPeers := make([]*nmdata.Peer, 0, len(uniquePeerIDs))
for _, p := range uniquePeerIDs {
peerInfo := c.GetPeerInfo(p)
@@ -422,28 +424,28 @@ func (c *NetworkMapComponents) getUniquePeerIDsFromGroupsIDs(groups []string) []
return ids
}
func (c *NetworkMapComponents) getPeerFromResource(resource Resource, peerID string, postureChecks []string) ([]*ComponentPeer, bool) {
func (c *NetworkMapComponents) getPeerFromResource(resource nmdata.Resource, peerID string, postureChecks []string) ([]*nmdata.Peer, bool) {
if resource.ID == peerID {
if len(postureChecks) > 0 && !c.ValidatePostureChecksOnPeer(peerID, postureChecks) {
return []*ComponentPeer{}, false
return []*nmdata.Peer{}, false
}
return []*ComponentPeer{}, true
return []*nmdata.Peer{}, true
}
peerInfo := c.GetPeerInfo(resource.ID)
if peerInfo == nil {
return []*ComponentPeer{}, false
return []*nmdata.Peer{}, false
}
if len(postureChecks) > 0 && !c.ValidatePostureChecksOnPeer(resource.ID, postureChecks) {
return []*ComponentPeer{}, false
return []*nmdata.Peer{}, false
}
return []*ComponentPeer{peerInfo}, false
return []*nmdata.Peer{peerInfo}, false
}
func (c *NetworkMapComponents) filterPeersByLoginExpiration(aclPeers []*ComponentPeer) ([]*ComponentPeer, []*ComponentPeer) {
peersToConnect := make([]*ComponentPeer, 0, len(aclPeers))
var expiredPeers []*ComponentPeer
func (c *NetworkMapComponents) filterPeersByLoginExpiration(aclPeers []*nmdata.Peer) ([]*nmdata.Peer, []*nmdata.Peer) {
peersToConnect := make([]*nmdata.Peer, 0, len(aclPeers))
var expiredPeers []*nmdata.Peer
for _, p := range aclPeers {
expired, _ := p.LoginExpired(c.AccountSettings.PeerLoginExpiration)
@@ -483,7 +485,7 @@ func (c *NetworkMapComponents) getPeerNSGroupsFromGroups(peerID string, groupLis
for _, gID := range nsGroup.Groups {
if _, found := groupList[gID]; found {
if !c.peerIsNameserver(peerIPStr, nsGroup) {
peerNSGroups = append(peerNSGroups, nsGroup.Copy())
peerNSGroups = append(peerNSGroups, toRealNSGroup(nsGroup))
}
break
}
@@ -493,7 +495,7 @@ func (c *NetworkMapComponents) getPeerNSGroupsFromGroups(peerID string, groupLis
return peerNSGroups
}
func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nbdns.NameServerGroup) bool {
func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nmdata.NameServerGroup) bool {
for _, ns := range nsGroup.NameServers {
if peerIPStr == ns.IP.String() {
return true
@@ -505,8 +507,8 @@ func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nbdns
// filterAndExpandRoutes drops v6 routes for non-capable peers and duplicates
// the default v4 route (0.0.0.0/0) as ::/0 for v6-capable peers.
// TODO: the "-v6" suffix on IDs could collide with user-supplied route IDs.
func filterAndExpandRoutes(routes []*route.Route, includeIPv6 bool) []*route.Route {
filtered := make([]*route.Route, 0, len(routes))
func filterAndExpandRoutes(routes []*nmdata.Route, includeIPv6 bool) []*nmdata.Route {
filtered := make([]*nmdata.Route, 0, len(routes))
for _, r := range routes {
if !includeIPv6 && r.Network.Addr().Is6() {
continue
@@ -518,14 +520,14 @@ func filterAndExpandRoutes(routes []*route.Route, includeIPv6 bool) []*route.Rou
v6.ID = r.ID + "-v6-default"
v6.NetID = r.NetID + "-v6"
v6.Network = netip.MustParsePrefix("::/0")
v6.NetworkType = route.IPv6Network
v6.NetworkType = nmdata.NetworkTypeIPv6
filtered = append(filtered, v6)
}
}
return filtered
}
func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*ComponentPeer, peerGroups LookupMap) []*route.Route {
func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*nmdata.Peer, peerGroups LookupMap) []*nmdata.Route {
routes, peerDisabledRoutes := c.getRoutingPeerRoutes(peerID)
peerRoutesMembership := make(LookupMap)
for _, r := range append(routes, peerDisabledRoutes...) {
@@ -542,7 +544,7 @@ func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*Compon
return routes
}
func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoutes []*route.Route, disabledRoutes []*route.Route) {
func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoutes []*nmdata.Route, disabledRoutes []*nmdata.Route) {
peerInfo := c.GetPeerInfo(peerID)
if peerInfo == nil {
peerInfo = c.GetRouterPeerInfo(peerID)
@@ -551,9 +553,9 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute
return enabledRoutes, disabledRoutes
}
seenRoute := make(map[route.ID]struct{})
seenRoute := make(map[string]struct{})
takeRoute := func(r *route.Route) {
takeRoute := func(r *nmdata.Route) {
if _, ok := seenRoute[r.ID]; ok {
return
}
@@ -572,7 +574,7 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute
if entry.viaGroup {
newPeerRoute := entry.route.Copy()
newPeerRoute.PeerGroups = nil
newPeerRoute.ID = route.ID(string(entry.route.ID) + ":" + peerID)
newPeerRoute.ID = entry.route.ID + ":" + peerID
takeRoute(newPeerRoute)
continue
}
@@ -605,8 +607,8 @@ func (c *NetworkMapComponents) routesByPeer() map[string][]routeIndexEntry {
return c.routesByPeerIdx
}
func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route {
var filteredRoutes []*route.Route
func (c *NetworkMapComponents) filterRoutesByGroups(routes []*nmdata.Route, groupListMap LookupMap) []*nmdata.Route {
var filteredRoutes []*nmdata.Route
for _, r := range routes {
for _, groupID := range r.Groups {
_, found := groupListMap[groupID]
@@ -619,8 +621,8 @@ func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, group
return filteredRoutes
}
func (c *NetworkMapComponents) filterRoutesFromPeersOfSameHAGroup(routes []*route.Route, peerMemberships LookupMap) []*route.Route {
var filteredRoutes []*route.Route
func (c *NetworkMapComponents) filterRoutesFromPeersOfSameHAGroup(routes []*nmdata.Route, peerMemberships LookupMap) []*nmdata.Route {
var filteredRoutes []*nmdata.Route
for _, r := range routes {
_, found := peerMemberships[string(r.GetHAUniqueID())]
if !found {
@@ -653,7 +655,7 @@ func (c *NetworkMapComponents) getPeerRoutesFirewallRules(ctx context.Context, p
return routesFirewallRules
}
func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool) []*RouteFirewallRule {
func (c *NetworkMapComponents) getDefaultPermit(r *nmdata.Route, includeIPv6 bool) []*RouteFirewallRule {
if r.Network.Addr().Is6() && !includeIPv6 {
return nil
}
@@ -670,7 +672,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool
Protocol: string(PolicyRuleProtocolALL),
Domains: r.Domains,
IsDynamic: r.IsDynamic(),
RouteID: r.ID,
RouteID: route.ID(r.ID),
}
rules := []*RouteFirewallRule{&rule}
@@ -681,7 +683,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool
ruleV6.SourceRanges = []string{"::/0"}
if isDefaultV4 {
ruleV6.Destination = "::/0"
ruleV6.RouteID = r.ID + "-v6-default"
ruleV6.RouteID = route.ID(r.ID + "-v6-default")
}
rules = append(rules, &ruleV6)
}
@@ -689,7 +691,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool
return rules
}
func (c *NetworkMapComponents) getDistributionGroupsPeers(r *route.Route) map[string]struct{} {
func (c *NetworkMapComponents) getDistributionGroupsPeers(r *nmdata.Route) map[string]struct{} {
distPeers := make(map[string]struct{})
for _, id := range r.Groups {
group := c.GetGroupInfo(id)
@@ -704,11 +706,17 @@ func (c *NetworkMapComponents) getDistributionGroupsPeers(r *route.Route) map[st
return distPeers
}
func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups []string) []*Policy {
routePolicies := make([]*Policy, 0)
func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups []string) []*nmdata.Policy {
routePolicies := make([]*nmdata.Policy, 0)
for _, groupID := range accessControlGroups {
for _, policy := range c.Policies {
if policy == nil {
continue
}
for _, rule := range policy.Rules {
if rule == nil {
continue
}
if slices.Contains(rule.Destinations, groupID) {
routePolicies = append(routePolicies, policy)
}
@@ -719,15 +727,15 @@ func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups
return routePolicies
}
func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, distributionPeers map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule {
func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID string, policies []*nmdata.Policy, route *nmdata.Route, distributionPeers map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule {
var fwRules []*RouteFirewallRule
for _, policy := range policies {
if !policy.Enabled {
if policy == nil || !policy.Enabled {
continue
}
for _, rule := range policy.Rules {
if !rule.Enabled {
if rule == nil || !rule.Enabled {
continue
}
@@ -739,7 +747,7 @@ func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID
return fwRules
}
func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}) []*ComponentPeer {
func (c *NetworkMapComponents) getRulePeers(rule *nmdata.PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}) []*nmdata.Peer {
distPeersWithPolicy := make(map[string]struct{})
for _, id := range rule.Sources {
group := c.GetGroupInfo(id)
@@ -758,7 +766,7 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st
}
}
}
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" {
_, distPeer := distributionPeers[rule.SourceResource.ID]
_, valid := c.Peers[rule.SourceResource.ID]
if distPeer && valid && c.ValidatePostureChecksOnPeer(rule.SourceResource.ID, postureChecks) {
@@ -766,7 +774,7 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st
}
}
distributionGroupPeers := make([]*ComponentPeer, 0, len(distPeersWithPolicy))
distributionGroupPeers := make([]*nmdata.Peer, 0, len(distPeersWithPolicy))
for pID := range distPeersWithPolicy {
peerInfo := c.GetPeerInfo(pID)
if peerInfo == nil {
@@ -777,9 +785,9 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st
return distributionGroupPeers
}
func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (bool, []*route.Route, map[string]struct{}) {
func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (bool, []*nmdata.Route, map[string]struct{}) {
var isRoutingPeer bool
var routes []*route.Route
var routes []*nmdata.Route
allSourcePeers := make(map[string]struct{})
for _, resource := range c.NetworkResources {
@@ -806,14 +814,17 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (b
func (c *NetworkMapComponents) processResourcePolicies(
peerID string,
resource *ComponentResource,
networkRoutingPeers map[string]*ComponentRouter,
resource *nmdata.NetworkResource,
networkRoutingPeers map[string]*nmdata.NetworkRouter,
addSourcePeers bool,
allSourcePeers map[string]struct{},
) []*route.Route {
var routes []*route.Route
) []*nmdata.Route {
var routes []*nmdata.Route
for _, policy := range c.ResourcePoliciesMap[resource.ID] {
if policy == nil || !policy.Enabled || len(policy.Rules) == 0 || policy.Rules[0] == nil {
continue
}
peers := c.getResourcePolicyPeers(policy)
if addSourcePeers {
for _, pID := range c.getPostureValidPeers(peers, policy.SourcePostureChecks) {
@@ -833,17 +844,17 @@ func (c *NetworkMapComponents) processResourcePolicies(
return routes
}
func (c *NetworkMapComponents) getResourcePolicyPeers(policy *Policy) []string {
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
func (c *NetworkMapComponents) getResourcePolicyPeers(policy *nmdata.Policy) []string {
if policy.Rules[0].SourceResource.Type == string(ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" {
return []string{policy.Rules[0].SourceResource.ID}
}
return c.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
}
func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *ComponentResource, peerID string, router *ComponentRouter) []*route.Route {
func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *nmdata.NetworkResource, peerID string, router *nmdata.NetworkRouter) []*nmdata.Route {
resourceAppliedPolicies := c.ResourcePoliciesMap[resource.ID]
var routes []*route.Route
var routes []*nmdata.Route
if len(resourceAppliedPolicies) > 0 {
peerInfo := c.GetPeerInfo(peerID)
if peerInfo != nil {
@@ -854,9 +865,9 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *ComponentReso
return routes
}
func (c *NetworkMapComponents) networkResourceToRoute(resource *ComponentResource, peer *ComponentPeer, router *ComponentRouter) *route.Route {
r := &route.Route{
ID: route.ID(resource.ID + ":" + peer.ID),
func (c *NetworkMapComponents) networkResourceToRoute(resource *nmdata.NetworkResource, peer *nmdata.Peer, router *nmdata.NetworkRouter) *nmdata.Route {
r := &nmdata.Route{
ID: resource.ID + ":" + peer.ID,
AccountID: resource.AccountID,
Peer: peer.Key,
PeerID: peer.ID,
@@ -864,24 +875,24 @@ func (c *NetworkMapComponents) networkResourceToRoute(resource *ComponentResourc
Masquerade: router.Masquerade,
Enabled: resource.Enabled,
KeepRoute: true,
NetID: route.NetID(resource.Name),
NetID: resource.Name,
Description: resource.Description,
}
if resource.Type == ComponentResourceHost || resource.Type == ComponentResourceSubnet {
if resource.Type == string(ResourceTypeHost) || resource.Type == string(ResourceTypeSubnet) {
r.Network = resource.Prefix
r.NetworkType = route.IPv4Network
r.NetworkType = nmdata.NetworkTypeIPv4
if resource.Prefix.Addr().Is6() {
r.NetworkType = route.IPv6Network
r.NetworkType = nmdata.NetworkTypeIPv6
}
}
if resource.Type == ComponentResourceDomain {
if resource.Type == string(ResourceTypeDomain) {
domainList, err := domain.FromStringList([]string{resource.Domain})
if err == nil {
r.Domains = domainList
r.NetworkType = route.DomainNetwork
r.NetworkType = nmdata.NetworkTypeDomain
r.Network = netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32)
}
}
@@ -899,7 +910,7 @@ func (c *NetworkMapComponents) getPostureValidPeers(inputPeers []string, posture
return dest
}
func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*route.Route, includeIPv6 bool) []*RouteFirewallRule {
func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*nmdata.Route, includeIPv6 bool) []*RouteFirewallRule {
routesFirewallRules := make([]*RouteFirewallRule, 0)
peerInfo := c.GetPeerInfo(peerID)
@@ -927,11 +938,17 @@ func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.C
return routesFirewallRules
}
func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[string]struct{} {
func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*nmdata.Policy) map[string]struct{} {
sourcePeers := make(map[string]struct{})
for _, policy := range policies {
if policy == nil {
continue
}
for _, rule := range policy.Rules {
if rule == nil {
continue
}
for _, sourceGroup := range rule.Sources {
group := c.GetGroupInfo(sourceGroup)
if group == nil {
@@ -943,7 +960,7 @@ func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[st
}
}
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" {
sourcePeers[rule.SourceResource.ID] = struct{}{}
}
}
@@ -953,13 +970,13 @@ func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[st
}
func (c *NetworkMapComponents) addNetworksRoutingPeers(
networkResourcesRoutes []*route.Route,
networkResourcesRoutes []*nmdata.Route,
peerID string,
peersToConnect []*ComponentPeer,
expiredPeers []*ComponentPeer,
peersToConnect []*nmdata.Peer,
expiredPeers []*nmdata.Peer,
isRouter bool,
sourcePeers map[string]struct{},
) []*ComponentPeer {
) []*nmdata.Peer {
networkRoutesPeers := make(map[string]struct{}, len(networkResourcesRoutes))
for _, r := range networkResourcesRoutes {
@@ -1009,8 +1026,8 @@ type FirewallRuleContext struct {
PortsJoined string
}
func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *ComponentPeer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule {
if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6 || !targetPeer.IPv6.IsValid() {
func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nmdata.Peer, rule *nmdata.PolicyRule, rc FirewallRuleContext) []*FirewallRule {
if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6() || !targetPeer.IPv6.IsValid() {
return rules
}
@@ -1,8 +1,7 @@
package types
import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/route"
nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
type GroupCompact struct {
@@ -13,26 +12,26 @@ type GroupCompact struct {
type NetworkMapComponentsCompact struct {
PeerID string
Network *Network
AccountSettings *AccountSettingsInfo
DNSSettings *DNSSettings
Network *nmdata.Network
AccountSettings *nmdata.AccountSettingsInfo
DNSSettings *nmdata.DNSSettings
CustomZoneDomain string
AllPeers []*ComponentPeer
AllPeers []*nmdata.Peer
PeerIndexes []int
RouterPeerIndexes []int
Groups map[string]*GroupCompact
AllPolicies []*Policy
AllPolicies []*nmdata.Policy
PolicyIndexes []int
ResourcePoliciesMap map[string][]int
Routes []*route.Route
NameServerGroups []*nbdns.NameServerGroup
AllDNSRecords []nbdns.SimpleRecord
AccountZones []nbdns.CustomZone
Routes []*nmdata.Route
NameServerGroups []*nmdata.NameServerGroup
AllDNSRecords []nmdata.SimpleRecord
AccountZones []nmdata.CustomZone
RoutersMap map[string]map[string]*ComponentRouter
NetworkResources []*ComponentResource
RoutersMap map[string]map[string]*nmdata.NetworkRouter
NetworkResources []*nmdata.NetworkResource
GroupIDToUserIDs map[string][]string
AllowedUserIDs map[string]struct{}
@@ -41,7 +40,7 @@ type NetworkMapComponentsCompact struct {
func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact {
peerToIndex := make(map[string]int)
var allPeers []*ComponentPeer
var allPeers []*nmdata.Peer
for id, peer := range c.Peers {
if _, exists := peerToIndex[id]; !exists {
@@ -81,8 +80,8 @@ func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact {
}
}
policyToIndex := make(map[*Policy]int)
var allPolicies []*Policy
policyToIndex := make(map[*nmdata.Policy]int)
var allPolicies []*nmdata.Policy
for _, policy := range c.Policies {
if _, exists := policyToIndex[policy]; !exists {
@@ -147,7 +146,7 @@ func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact {
}
func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
peers := make(map[string]*ComponentPeer, len(c.PeerIndexes))
peers := make(map[string]*nmdata.Peer, len(c.PeerIndexes))
for _, idx := range c.PeerIndexes {
if idx >= 0 && idx < len(c.AllPeers) {
peer := c.AllPeers[idx]
@@ -155,7 +154,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
}
}
routerPeers := make(map[string]*ComponentPeer, len(c.RouterPeerIndexes))
routerPeers := make(map[string]*nmdata.Peer, len(c.RouterPeerIndexes))
for _, idx := range c.RouterPeerIndexes {
if idx >= 0 && idx < len(c.AllPeers) {
peer := c.AllPeers[idx]
@@ -163,7 +162,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
}
}
groups := make(map[string]*ComponentGroup, len(c.Groups))
groups := make(map[string]*nmdata.Group, len(c.Groups))
for id, gc := range c.Groups {
peerIDs := make([]string, 0, len(gc.PeerIndexes))
for _, idx := range gc.PeerIndexes {
@@ -171,25 +170,24 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
peerIDs = append(peerIDs, c.AllPeers[idx].ID)
}
}
groups[id] = &ComponentGroup{
ID: id,
groups[id] = &nmdata.Group{
Name: gc.Name,
Peers: peerIDs,
}
}
policies := make([]*Policy, len(c.PolicyIndexes))
policies := make([]*nmdata.Policy, len(c.PolicyIndexes))
for i, idx := range c.PolicyIndexes {
if idx >= 0 && idx < len(c.AllPolicies) {
policies[i] = c.AllPolicies[idx]
}
}
var resourcePoliciesMap map[string][]*Policy
var resourcePoliciesMap map[string][]*nmdata.Policy
if len(c.ResourcePoliciesMap) > 0 {
resourcePoliciesMap = make(map[string][]*Policy, len(c.ResourcePoliciesMap))
resourcePoliciesMap = make(map[string][]*nmdata.Policy, len(c.ResourcePoliciesMap))
for resID, indexes := range c.ResourcePoliciesMap {
pols := make([]*Policy, 0, len(indexes))
pols := make([]*nmdata.Policy, 0, len(indexes))
for _, idx := range indexes {
if idx >= 0 && idx < len(c.AllPolicies) {
pols = append(pols, c.AllPolicies[idx])
+70
View File
@@ -0,0 +1,70 @@
package types
import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
// This file holds the twin→real converters that survive the twin-NetworkMap
// refactor: only the DNS materialization. NetworkMap.DNSConfig stays a real
// nbdns.Config (the client DNS type), so Calculate converts the twin DNS
// components to nbdns at the output boundary. Peers/Routes/Network flow as
// twins all the way through and need no conversion.
func toRealNSGroup(n *nmdata.NameServerGroup) *nbdns.NameServerGroup {
if n == nil {
return nil
}
nameServers := make([]nbdns.NameServer, 0, len(n.NameServers))
for _, ns := range n.NameServers {
nameServers = append(nameServers, nbdns.NameServer{
IP: ns.IP,
NSType: nbdns.NameServerType(ns.NSType),
Port: ns.Port,
})
}
return &nbdns.NameServerGroup{
ID: n.ID,
Name: n.Name,
Description: n.Description,
NameServers: nameServers,
Groups: n.Groups,
Primary: n.Primary,
Domains: n.Domains,
Enabled: n.Enabled,
SearchDomainsEnabled: n.SearchDomainsEnabled,
}
}
func toRealRecords(recs []nmdata.SimpleRecord) []nbdns.SimpleRecord {
if recs == nil {
return nil
}
out := make([]nbdns.SimpleRecord, len(recs))
for i, r := range recs {
out[i] = nbdns.SimpleRecord{
Name: r.Name,
Type: r.Type,
Class: r.Class,
TTL: r.TTL,
RData: r.RData,
}
}
return out
}
func toRealZones(zones []nmdata.CustomZone) []nbdns.CustomZone {
if zones == nil {
return nil
}
out := make([]nbdns.CustomZone, len(zones))
for i, z := range zones {
out[i] = nbdns.CustomZone{
Domain: z.Domain,
Records: toRealRecords(z.Records),
SearchDomainDisabled: z.SearchDomainDisabled,
NonAuthoritative: z.NonAuthoritative,
}
}
return out
}
-272
View File
@@ -1,272 +0,0 @@
package types
import (
"errors"
"fmt"
"strconv"
"strings"
)
const (
// PolicyTrafficActionAccept indicates that the traffic is accepted
PolicyTrafficActionAccept = PolicyTrafficActionType("accept")
// PolicyTrafficActionDrop indicates that the traffic is dropped
PolicyTrafficActionDrop = PolicyTrafficActionType("drop")
)
const (
// PolicyRuleProtocolALL type of traffic
PolicyRuleProtocolALL = PolicyRuleProtocolType("all")
// PolicyRuleProtocolTCP type of traffic
PolicyRuleProtocolTCP = PolicyRuleProtocolType("tcp")
// PolicyRuleProtocolUDP type of traffic
PolicyRuleProtocolUDP = PolicyRuleProtocolType("udp")
// PolicyRuleProtocolICMP type of traffic
PolicyRuleProtocolICMP = PolicyRuleProtocolType("icmp")
// PolicyRuleProtocolNetbirdSSH type of traffic
PolicyRuleProtocolNetbirdSSH = PolicyRuleProtocolType("netbird-ssh")
// PolicyRuleProtocolNetbirdVNC type of traffic
PolicyRuleProtocolNetbirdVNC = PolicyRuleProtocolType("netbird-vnc")
)
const (
// PolicyRuleFlowDirect allows traffic from source to destination
PolicyRuleFlowDirect = PolicyRuleDirection("direct")
// PolicyRuleFlowBidirect allows traffic to both directions
PolicyRuleFlowBidirect = PolicyRuleDirection("bidirect")
)
const (
// DefaultRuleName is a name for the Default rule that is created for every account
DefaultRuleName = "Default"
// DefaultRuleDescription is a description for the Default rule that is created for every account
DefaultRuleDescription = "This is a default rule that allows connections between all the resources"
// DefaultPolicyName is a name for the Default policy that is created for every account
DefaultPolicyName = "Default"
// DefaultPolicyDescription is a description for the Default policy that is created for every account
DefaultPolicyDescription = "This is a default policy that allows connections between all the resources"
)
// PolicyUpdateOperation operation object with type and values to be applied
type PolicyUpdateOperation struct {
Type PolicyUpdateOperationType
Values []string
}
// Policy of the Rego query
type Policy struct {
// ID of the policy'
ID string `gorm:"primaryKey"`
PublicID string `json:"-"`
// AccountID is a reference to Account that this object belongs
AccountID string `json:"-" gorm:"index"`
// Name of the Policy
Name string
// Description of the policy visible in the UI
Description string
// Enabled status of the policy
Enabled bool
// Rules of the policy
Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id;constraint:OnDelete:CASCADE;"`
// SourcePostureChecks are ID references to Posture checks for policy source groups
SourcePostureChecks []string `gorm:"serializer:json"`
}
// Copy returns a copy of the policy.
func (p *Policy) Copy() *Policy {
c := &Policy{
ID: p.ID,
AccountID: p.AccountID,
PublicID: p.PublicID,
Name: p.Name,
Description: p.Description,
Enabled: p.Enabled,
Rules: make([]*PolicyRule, len(p.Rules)),
SourcePostureChecks: make([]string, len(p.SourcePostureChecks)),
}
for i, r := range p.Rules {
c.Rules[i] = r.Copy()
}
copy(c.SourcePostureChecks, p.SourcePostureChecks)
return c
}
func (p *Policy) Equal(other *Policy) bool {
if p == nil || other == nil {
return p == other
}
if p.ID != other.ID ||
p.AccountID != other.AccountID ||
p.Name != other.Name ||
p.Description != other.Description ||
p.Enabled != other.Enabled {
return false
}
if !stringSlicesEqualUnordered(p.SourcePostureChecks, other.SourcePostureChecks) {
return false
}
if len(p.Rules) != len(other.Rules) {
return false
}
otherRules := make(map[string]*PolicyRule, len(other.Rules))
for _, r := range other.Rules {
otherRules[r.ID] = r
}
for _, r := range p.Rules {
otherRule, ok := otherRules[r.ID]
if !ok {
return false
}
if !r.Equal(otherRule) {
return false
}
}
return true
}
// EventMeta returns activity event meta related to this policy
func (p *Policy) EventMeta() map[string]any {
return map[string]any{"name": p.Name}
}
// UpgradeAndFix different version of policies to latest version
func (p *Policy) UpgradeAndFix() {
for _, r := range p.Rules {
// start migrate from version v0.20.3
if r.Protocol == "" {
r.Protocol = PolicyRuleProtocolALL
}
if r.Protocol == PolicyRuleProtocolALL && !r.Bidirectional {
r.Bidirectional = true
}
// -- v0.20.4
}
}
// RuleGroups returns a list of all groups referenced in the policy's rules,
// including sources and destinations.
func (p *Policy) RuleGroups() []string {
groups := make([]string, 0)
for _, rule := range p.Rules {
groups = append(groups, rule.Sources...)
groups = append(groups, rule.Destinations...)
}
return groups
}
// SourceGroups returns a slice of all unique source groups referenced in the policy's rules.
func (p *Policy) SourceGroups() []string {
if len(p.Rules) == 1 {
return p.Rules[0].Sources
}
groups := make(map[string]struct{}, len(p.Rules))
for _, rule := range p.Rules {
for _, source := range rule.Sources {
groups[source] = struct{}{}
}
}
groupIDs := make([]string, 0, len(groups))
for groupID := range groups {
groupIDs = append(groupIDs, groupID)
}
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 '/…'")
case "netbird-ssh":
return PolicyRuleProtocolNetbirdSSH, RulePortRange{Start: nativeSSHPortNumber, End: nativeSSHPortNumber}, nil
case "netbird-vnc":
return PolicyRuleProtocolNetbirdVNC, RulePortRange{Start: vncInternalPort, End: vncInternalPort}, nil
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
}
@@ -6,11 +6,12 @@ import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
auth "github.com/netbirdio/netbird/shared/sessionauth"
)
// vncInternalPort is the internal port the VNC server listens on (behind DNAT from 5900).
const vncInternalPort = 25900
// VNCInternalPort is the internal port the VNC server listens on (behind DNAT from 5900).
const VNCInternalPort = 25900
// PeerConnResolveState carries the in-progress maps mutated by per-rule
// resolution while walking an account's policies.
@@ -47,8 +48,8 @@ type VNCSessionPubKey struct {
// direction-and-auth logic while keeping their own context/state plumbing for
// authorized-user collection and allowed-user lookups.
type RuleAuthCallbacks struct {
CollectSSHUsers func(*PolicyRule, map[string]map[string]struct{})
CollectVNCUsers func(*PolicyRule, map[string]map[string]struct{})
CollectSSHUsers func(*nmdata.PolicyRule, map[string]map[string]struct{})
CollectVNCUsers func(*nmdata.PolicyRule, map[string]map[string]struct{})
GetAllowedUserIDs func() map[string]struct{}
}
@@ -58,13 +59,13 @@ type RuleAuthCallbacks struct {
// resolver (Account vs NetworkMapComponents), which also decide the peer
// representation the resource generator works with.
func ApplyResolvedRuleToState[P any](
rule *PolicyRule,
rule *nmdata.PolicyRule,
sourcePeers []P,
destPeers []P,
peerInSources bool,
peerInDestinations bool,
targetPeerSSHEnabled bool,
generateResources func(*PolicyRule, []P, int),
generateResources func(*nmdata.PolicyRule, []P, int),
cb RuleAuthCallbacks,
state *PeerConnResolveState,
) {
@@ -72,15 +73,15 @@ func ApplyResolvedRuleToState[P any](
receivingPeer := peerInDestinations || (rule.Bidirectional && peerInSources)
switch {
case rule.Protocol == PolicyRuleProtocolNetbirdSSH:
case rule.Protocol == string(PolicyRuleProtocolNetbirdSSH):
if !receivingPeer {
return
}
state.SSHEnabled = true
cb.CollectSSHUsers(rule, state.AuthorizedUsers)
case rule.Protocol == PolicyRuleProtocolNetbirdVNC:
case rule.Protocol == string(PolicyRuleProtocolNetbirdVNC):
cb.handleVNCRule(rule, peerInSources, peerInDestinations, state)
case PolicyRuleImpliesLegacySSH(rule) && targetPeerSSHEnabled:
case nmdata.PolicyRuleImpliesLegacySSH(rule) && targetPeerSSHEnabled:
if !receivingPeer {
return
}
@@ -94,7 +95,7 @@ func ApplyResolvedRuleToState[P any](
// peer that appears in the rule's sources also needs the SessionPubKey
// pushed (otherwise the Noise_IK handshake against that peer would fail
// because its authorizer wouldn't know the client's static key).
func (cb RuleAuthCallbacks) handleVNCRule(rule *PolicyRule, peerInSources, peerInDestinations bool, state *PeerConnResolveState) {
func (cb RuleAuthCallbacks) handleVNCRule(rule *nmdata.PolicyRule, peerInSources, peerInDestinations bool, state *PeerConnResolveState) {
receivingPeer := peerInDestinations || (rule.Bidirectional && peerInSources)
if !receivingPeer {
return
@@ -123,12 +124,12 @@ func MergeWildcardUsers(dst map[string]map[string]struct{}, users map[string]str
// emitRuleDirections dispatches generateResources for each direction the rule
// applies in for the target peer.
func emitRuleDirections[P any](
rule *PolicyRule,
rule *nmdata.PolicyRule,
sourcePeers []P,
destPeers []P,
peerInSources bool,
peerInDestinations bool,
generateResources func(*PolicyRule, []P, int),
generateResources func(*nmdata.PolicyRule, []P, int),
) {
if rule.Bidirectional {
if peerInSources {
@@ -191,24 +192,35 @@ func EnsureWildcardUser(target map[string]map[string]struct{}, authorizedUser st
target[auth.Wildcard][authorizedUser] = struct{}{}
}
// NormalizePolicyRuleProtocol maps NetBird virtual protocols (netbird-ssh,
// netbird-vnc) to TCP for the on-the-wire firewall view. For NetbirdVNC the
// rule is also scoped to the embedded VNC port so a VNC-only rule doesn't
// degrade into an unscoped TCP allow when the user left Ports empty.
// Returns the effective rule (possibly a shallow copy with Ports overridden)
// and the resulting protocol.
func NormalizePolicyRuleProtocol(rule *PolicyRule) (*PolicyRule, PolicyRuleProtocolType) {
switch rule.Protocol {
case PolicyRuleProtocolNetbirdSSH:
return rule, PolicyRuleProtocolTCP
case PolicyRuleProtocolNetbirdVNC:
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
scoped := *rule
scoped.Ports = []string{strconv.Itoa(vncInternalPort)}
return &scoped, PolicyRuleProtocolTCP
}
return rule, PolicyRuleProtocolTCP
// WirePolicyRuleProtocol maps the NetBird virtual protocols (netbird-ssh,
// netbird-vnc) to the protocol that goes on the wire, and leaves every other
// protocol as it is.
func WirePolicyRuleProtocol(protocol PolicyRuleProtocolType) PolicyRuleProtocolType {
switch protocol {
case PolicyRuleProtocolNetbirdSSH, PolicyRuleProtocolNetbirdVNC:
return PolicyRuleProtocolTCP
default:
return rule, rule.Protocol
return protocol
}
}
// VNCScopedPorts returns the ports a netbird-vnc rule is scoped to when it
// declares none of its own, so a VNC-only rule doesn't degrade into an
// unscoped TCP allow.
func VNCScopedPorts() []string {
return []string{strconv.Itoa(VNCInternalPort)}
}
// NormalizePolicyRuleProtocol maps a rule's protocol with
// WirePolicyRuleProtocol and scopes a portless netbird-vnc rule to the
// embedded VNC port. It returns the effective rule, which is a shallow copy
// only when the ports had to be overridden.
func NormalizePolicyRuleProtocol(rule *nmdata.PolicyRule) (*nmdata.PolicyRule, PolicyRuleProtocolType) {
protocol := WirePolicyRuleProtocol(PolicyRuleProtocolType(rule.Protocol))
if rule.Protocol != string(PolicyRuleProtocolNetbirdVNC) || len(rule.Ports) > 0 || len(rule.PortRanges) > 0 {
return rule, protocol
}
scoped := *rule
scoped.Ports = VNCScopedPorts()
return &scoped, protocol
}
@@ -2,6 +2,8 @@ package types
import (
"testing"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
// TestHandleVNCRule_BidirectionalDistributesPubkeyToSourcePeer covers the
@@ -13,15 +15,15 @@ import (
// fix in handleVNCRule must distribute the pubkey to either side of a
// bidirectional rule.
func TestHandleVNCRule_BidirectionalDistributesPubkeyToSourcePeer(t *testing.T) {
rule := &PolicyRule{
Protocol: PolicyRuleProtocolNetbirdVNC,
rule := &nmdata.PolicyRule{
Protocol: string(PolicyRuleProtocolNetbirdVNC),
Bidirectional: true,
AuthorizedUser: "user1",
SessionPubKey: "pubkey-base64",
SessionDisplayName: "Alice",
}
cb := RuleAuthCallbacks{
CollectVNCUsers: func(_ *PolicyRule, _ map[string]map[string]struct{}) {},
CollectVNCUsers: func(_ *nmdata.PolicyRule, _ map[string]map[string]struct{}) {},
}
state := NewPeerConnResolveState()
@@ -40,14 +42,14 @@ func TestHandleVNCRule_BidirectionalDistributesPubkeyToSourcePeer(t *testing.T)
// a strictly source-to-destination rule still must not push the
// SessionPubKey to peers that appear only in sources.
func TestHandleVNCRule_UnidirectionalSourceGetsNoPubkey(t *testing.T) {
rule := &PolicyRule{
Protocol: PolicyRuleProtocolNetbirdVNC,
rule := &nmdata.PolicyRule{
Protocol: string(PolicyRuleProtocolNetbirdVNC),
Bidirectional: false,
AuthorizedUser: "user1",
SessionPubKey: "pubkey-base64",
}
cb := RuleAuthCallbacks{
CollectVNCUsers: func(_ *PolicyRule, _ map[string]map[string]struct{}) {},
CollectVNCUsers: func(_ *nmdata.PolicyRule, _ map[string]map[string]struct{}) {},
}
state := NewPeerConnResolveState()
@@ -62,14 +64,14 @@ func TestHandleVNCRule_UnidirectionalSourceGetsNoPubkey(t *testing.T) {
// destination peers must always receive the SessionPubKey since they're
// the ones that need to authenticate the incoming Noise handshake.
func TestHandleVNCRule_DestinationAlwaysGetsPubkey(t *testing.T) {
rule := &PolicyRule{
Protocol: PolicyRuleProtocolNetbirdVNC,
rule := &nmdata.PolicyRule{
Protocol: string(PolicyRuleProtocolNetbirdVNC),
Bidirectional: false,
AuthorizedUser: "user1",
SessionPubKey: "pubkey-base64",
}
cb := RuleAuthCallbacks{
CollectVNCUsers: func(_ *PolicyRule, _ map[string]map[string]struct{}) {},
CollectVNCUsers: func(_ *nmdata.PolicyRule, _ map[string]map[string]struct{}) {},
}
state := NewPeerConnResolveState()
@@ -89,18 +91,18 @@ func TestHandleVNCRule_DestinationAlwaysGetsPubkey(t *testing.T) {
func TestApplyResolvedRule_BidirectionalSSHEnablesSourcePeer(t *testing.T) {
collected := false
cb := RuleAuthCallbacks{
CollectSSHUsers: func(_ *PolicyRule, target map[string]map[string]struct{}) {
CollectSSHUsers: func(_ *nmdata.PolicyRule, target map[string]map[string]struct{}) {
collected = true
target["local"] = map[string]struct{}{"user1": {}}
},
}
rule := &PolicyRule{
Protocol: PolicyRuleProtocolNetbirdSSH,
rule := &nmdata.PolicyRule{
Protocol: string(PolicyRuleProtocolNetbirdSSH),
Bidirectional: true,
}
state := NewPeerConnResolveState()
ApplyResolvedRuleToState(rule, nil, nil, true /*peerInSources*/, false /*peerInDestinations*/, false, func(*PolicyRule, []*ComponentPeer, int) {}, cb, state)
ApplyResolvedRuleToState(rule, nil, nil, true /*peerInSources*/, false /*peerInDestinations*/, false, func(*nmdata.PolicyRule, []*nmdata.Peer, int) {}, cb, state)
if !state.SSHEnabled {
t.Fatal("expected SSH enabled on source-side peer of bidirectional SSH rule")
@@ -119,17 +121,17 @@ func TestApplyResolvedRule_BidirectionalSSHEnablesSourcePeer(t *testing.T) {
func TestApplyResolvedRule_UnidirectionalSSHSkipsSourcePeer(t *testing.T) {
collected := false
cb := RuleAuthCallbacks{
CollectSSHUsers: func(_ *PolicyRule, _ map[string]map[string]struct{}) {
CollectSSHUsers: func(_ *nmdata.PolicyRule, _ map[string]map[string]struct{}) {
collected = true
},
}
rule := &PolicyRule{
Protocol: PolicyRuleProtocolNetbirdSSH,
rule := &nmdata.PolicyRule{
Protocol: string(PolicyRuleProtocolNetbirdSSH),
Bidirectional: false,
}
state := NewPeerConnResolveState()
ApplyResolvedRuleToState(rule, nil, nil, true /*peerInSources*/, false /*peerInDestinations*/, false, func(*PolicyRule, []*ComponentPeer, int) {}, cb, state)
ApplyResolvedRuleToState(rule, nil, nil, true /*peerInSources*/, false /*peerInDestinations*/, false, func(*nmdata.PolicyRule, []*nmdata.Peer, int) {}, cb, state)
if state.SSHEnabled {
t.Fatal("expected SSH NOT enabled on source-only peer of unidirectional SSH rule")
+93 -192
View File
@@ -1,22 +1,41 @@
package types
import (
"slices"
"errors"
"fmt"
"strconv"
"strings"
"github.com/netbirdio/netbird/shared/management/proto"
)
// PolicyUpdateOperationType operation type
type PolicyUpdateOperationType int
// PolicyTrafficActionType action type for the firewall
type PolicyTrafficActionType string
// PolicyRuleProtocolType type of traffic
type PolicyRuleProtocolType string
// PolicyRuleDirection direction of traffic
type PolicyRuleDirection string
const (
// PolicyTrafficActionAccept indicates that the traffic is accepted
PolicyTrafficActionAccept = PolicyTrafficActionType("accept")
// PolicyTrafficActionDrop indicates that the traffic is dropped
PolicyTrafficActionDrop = PolicyTrafficActionType("drop")
)
const (
// PolicyRuleProtocolALL type of traffic
PolicyRuleProtocolALL = PolicyRuleProtocolType("all")
// PolicyRuleProtocolTCP type of traffic
PolicyRuleProtocolTCP = PolicyRuleProtocolType("tcp")
// PolicyRuleProtocolUDP type of traffic
PolicyRuleProtocolUDP = PolicyRuleProtocolType("udp")
// PolicyRuleProtocolICMP type of traffic
PolicyRuleProtocolICMP = PolicyRuleProtocolType("icmp")
// PolicyRuleProtocolNetbirdSSH type of traffic
PolicyRuleProtocolNetbirdSSH = PolicyRuleProtocolType("netbird-ssh")
// PolicyRuleProtocolNetbirdVNC type of traffic
PolicyRuleProtocolNetbirdVNC = PolicyRuleProtocolType("netbird-vnc")
)
// RulePortRange represents a range of ports for a firewall rule.
type RulePortRange struct {
@@ -39,204 +58,86 @@ func (r *RulePortRange) Equal(other *RulePortRange) bool {
return r.Start == other.Start && r.End == other.End
}
// PolicyRule is the metadata of the policy
type PolicyRule struct {
// ID of the policy rule
ID string `gorm:"primaryKey"`
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
}
// PolicyID is a reference to Policy that this object belongs
PolicyID string `json:"-" gorm:"index"`
split := strings.Split(rule, "/")
if len(split) != 2 {
return "", RulePortRange{}, errors.New("invalid rule format: expected protocol/port or protocol/port-range")
}
// Name of the rule visible in the UI
Name string
protoStr := strings.TrimSpace(split[0])
portStr := strings.TrimSpace(split[1])
// Description of the rule visible in the UI
Description string
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 '/…'")
case "netbird-ssh":
return PolicyRuleProtocolNetbirdSSH, RulePortRange{Start: nativeSSHPortNumber, End: nativeSSHPortNumber}, nil
case "netbird-vnc":
return PolicyRuleProtocolNetbirdVNC, RulePortRange{Start: VNCInternalPort, End: VNCInternalPort}, nil
default:
return "", RulePortRange{}, fmt.Errorf("invalid protocol: %q", protoStr)
}
// Enabled status of rule in the system
Enabled bool
portRange, err := parsePortRange(portStr)
if err != nil {
return "", RulePortRange{}, err
}
// Action policy accept or drops packets
Action PolicyTrafficActionType
// Destinations policy destination groups
Destinations []string `gorm:"serializer:json"`
// DestinationResource policy destination resource that the rule is applied to
DestinationResource Resource `gorm:"serializer:json"`
// Sources policy source groups
Sources []string `gorm:"serializer:json"`
// SourceResource policy source resource that the rule is applied to
SourceResource Resource `gorm:"serializer:json"`
// Bidirectional define if the rule is applicable in both directions, sources, and destinations
Bidirectional bool
// Protocol type of the traffic
Protocol PolicyRuleProtocolType
// Ports or it ranges list
Ports []string `gorm:"serializer:json"`
// PortRanges a list of port ranges.
PortRanges []RulePortRange `gorm:"serializer:json"`
// AuthorizedGroups is a map of groupIDs and their respective access to local users via ssh
AuthorizedGroups map[string][]string `gorm:"serializer:json"`
// AuthorizedUser is a list of userIDs that are authorized to access local resources via ssh
AuthorizedUser string
// SessionPubKey is the base64 X25519 public key used with Noise_IK to
// bind a VNC session to the AuthorizedUser. Set together with
// AuthorizedUser when the rule was created via temporary-access for a
// VNC scope; empty otherwise.
SessionPubKey string
// SessionDisplayName is a human-readable label for the user the
// SessionPubKey was issued to (typically display name, falling back
// to email or user id). The daemon surfaces it on the host's
// per-connection approval prompt so the user being asked can
// recognise who is requesting access.
SessionDisplayName string
return protocol, portRange, nil
}
// Copy returns a copy of a policy rule
func (pm *PolicyRule) Copy() *PolicyRule {
rule := &PolicyRule{
ID: pm.ID,
PolicyID: pm.PolicyID,
Name: pm.Name,
Description: pm.Description,
Enabled: pm.Enabled,
Action: pm.Action,
Destinations: make([]string, len(pm.Destinations)),
DestinationResource: pm.DestinationResource,
Sources: make([]string, len(pm.Sources)),
SourceResource: pm.SourceResource,
Bidirectional: pm.Bidirectional,
Protocol: pm.Protocol,
Ports: make([]string, len(pm.Ports)),
PortRanges: make([]RulePortRange, len(pm.PortRanges)),
AuthorizedGroups: make(map[string][]string, len(pm.AuthorizedGroups)),
AuthorizedUser: pm.AuthorizedUser,
SessionPubKey: pm.SessionPubKey,
SessionDisplayName: pm.SessionDisplayName,
}
copy(rule.Destinations, pm.Destinations)
copy(rule.Sources, pm.Sources)
copy(rule.Ports, pm.Ports)
copy(rule.PortRanges, pm.PortRanges)
for k, v := range pm.AuthorizedGroups {
rule.AuthorizedGroups[k] = make([]string, len(v))
copy(rule.AuthorizedGroups[k], v)
}
return rule
}
func (pm *PolicyRule) Equal(other *PolicyRule) bool {
if pm == nil || other == nil {
return pm == other
}
if pm.ID != other.ID ||
pm.PolicyID != other.PolicyID ||
pm.Name != other.Name ||
pm.Description != other.Description ||
pm.Enabled != other.Enabled ||
pm.Action != other.Action ||
pm.Bidirectional != other.Bidirectional ||
pm.Protocol != other.Protocol ||
pm.SourceResource != other.SourceResource ||
pm.DestinationResource != other.DestinationResource ||
pm.AuthorizedUser != other.AuthorizedUser ||
pm.SessionPubKey != other.SessionPubKey ||
pm.SessionDisplayName != other.SessionDisplayName {
return false
}
if !stringSlicesEqualUnordered(pm.Sources, other.Sources) {
return false
}
if !stringSlicesEqualUnordered(pm.Destinations, other.Destinations) {
return false
}
if !stringSlicesEqualUnordered(pm.Ports, other.Ports) {
return false
}
if !portRangeSlicesEqualUnordered(pm.PortRanges, other.PortRanges) {
return false
}
if !authorizedGroupsEqual(pm.AuthorizedGroups, other.AuthorizedGroups) {
return false
}
return true
}
func stringSlicesEqualUnordered(a, b []string) bool {
if len(a) != len(b) {
return false
}
if len(a) == 0 {
return true
}
sorted1 := make([]string, len(a))
sorted2 := make([]string, len(b))
copy(sorted1, a)
copy(sorted2, b)
slices.Sort(sorted1)
slices.Sort(sorted2)
return slices.Equal(sorted1, sorted2)
}
func portRangeSlicesEqualUnordered(a, b []RulePortRange) bool {
if len(a) != len(b) {
return false
}
if len(a) == 0 {
return true
}
cmp := func(x, y RulePortRange) int {
if x.Start != y.Start {
if x.Start < y.Start {
return -1
}
return 1
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)
}
if x.End != y.End {
if x.End < y.End {
return -1
}
return 1
start, err := parsePort(strings.TrimSpace(rangeParts[0]))
if err != nil {
return RulePortRange{}, err
}
return 0
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
}
sorted1 := make([]RulePortRange, len(a))
sorted2 := make([]RulePortRange, len(b))
copy(sorted1, a)
copy(sorted2, b)
slices.SortFunc(sorted1, cmp)
slices.SortFunc(sorted2, cmp)
return slices.EqualFunc(sorted1, sorted2, func(x, y RulePortRange) bool {
return x.Start == y.Start && x.End == y.End
})
p, err := parsePort(portStr)
if err != nil {
return RulePortRange{}, err
}
return RulePortRange{Start: uint16(p), End: uint16(p)}, nil
}
func authorizedGroupsEqual(a, b map[string][]string) bool {
if len(a) != len(b) {
return false
func parsePort(portStr string) (int, error) {
if portStr == "" {
return 0, errors.New("empty port")
}
for k, va := range a {
vb, ok := b[k]
if !ok {
return false
}
if !stringSlicesEqualUnordered(va, vb) {
return false
}
p, err := strconv.Atoi(portStr)
if err != nil {
return 0, fmt.Errorf("invalid port %q: %w", portStr, err)
}
return true
if p < 1 || p > 65535 {
return 0, fmt.Errorf("port out of range (165535): %d", p)
}
return p, nil
}
+6 -26
View File
@@ -1,9 +1,5 @@
package types
import (
"github.com/netbirdio/netbird/shared/management/http/api"
)
type ResourceType string
const (
@@ -13,27 +9,11 @@ const (
ResourceTypeSubnet ResourceType = "subnet"
)
type Resource struct {
ID string
Type ResourceType
}
func (r *Resource) ToAPIResponse() *api.Resource {
if r.ID == "" && r.Type == "" {
return nil
}
return &api.Resource{
Id: r.ID,
Type: api.ResourceType(r.Type),
func (t ResourceType) Valid() bool {
switch t {
case ResourceTypePeer, ResourceTypeDomain, ResourceTypeHost, ResourceTypeSubnet:
return true
default:
return false
}
}
func (r *Resource) FromAPIRequest(req *api.Resource) {
if req == nil {
return
}
r.ID = req.Id
r.Type = ResourceType(req.Type)
}