mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-09 15:09:08 +02:00
fix management <-> shared dependencies
This commit is contained in:
@@ -109,7 +109,7 @@ func (m *managerImpl) AddResourceToGroupInTransaction(ctx context.Context, trans
|
|||||||
}
|
}
|
||||||
|
|
||||||
event := func() {
|
event := func() {
|
||||||
m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceAddedToGroup, group.EventMetaResource(networkResource))
|
m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceAddedToGroup, group.EventMetaResource(types.TwinNetworkResource(networkResource)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return event, nil
|
return event, nil
|
||||||
@@ -133,7 +133,7 @@ func (m *managerImpl) RemoveResourceFromGroupInTransaction(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
event := func() {
|
event := func() {
|
||||||
m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceRemovedFromGroup, group.EventMetaResource(networkResource))
|
m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceRemovedFromGroup, group.EventMetaResource(types.TwinNetworkResource(networkResource)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return event, nil
|
return event, nil
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ func (am *DefaultAccountManager) CreatePeerJob(ctx context.Context, accountID, p
|
|||||||
return status.NewPeerNotPartOfAccountError()
|
return status.NewPeerNotPartOfAccountError()
|
||||||
}
|
}
|
||||||
|
|
||||||
meetMinVer, err := posture.MeetsMinVersion(remoteJobsMinVer, p.Meta.WtVersion)
|
meetMinVer, err := version.MeetsMinVersion(remoteJobsMinVer, p.Meta.WtVersion)
|
||||||
if !version.IsDevelopmentVersion(p.Meta.WtVersion) && (!meetMinVer || err != nil) {
|
if !version.IsDevelopmentVersion(p.Meta.WtVersion) && (!meetMinVer || err != nil) {
|
||||||
return status.Errorf(status.PreconditionFailed, "peer version %s does not meet the minimum required version %s for remote jobs", p.Meta.WtVersion, remoteJobsMinVer)
|
return status.Errorf(status.PreconditionFailed, "peer version %s does not meet the minimum required version %s for remote jobs", p.Meta.WtVersion, remoteJobsMinVer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/util"
|
"github.com/netbirdio/netbird/management/server/util"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Peer capability constants mirror the proto enum values.
|
// Peer capability constants mirror the proto enum values.
|
||||||
const (
|
const (
|
||||||
PeerCapabilitySourcePrefixes int32 = 1
|
PeerCapabilitySourcePrefixes = nmdata.PeerCapabilitySourcePrefixes
|
||||||
PeerCapabilityIPv6Overlay int32 = 2
|
PeerCapabilityIPv6Overlay = nmdata.PeerCapabilityIPv6Overlay
|
||||||
PeerCapabilityComponentNetworkMap int32 = 3
|
PeerCapabilityComponentNetworkMap = nmdata.PeerCapabilityComponentNetworkMap
|
||||||
)
|
)
|
||||||
|
|
||||||
// Peer represents a machine connected to the network.
|
// Peer represents a machine connected to the network.
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ package posture
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
|
||||||
|
|
||||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
nbversion "github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NBVersionCheck struct {
|
type NBVersionCheck struct {
|
||||||
@@ -16,14 +14,8 @@ type NBVersionCheck struct {
|
|||||||
|
|
||||||
var _ Check = (*NBVersionCheck)(nil)
|
var _ Check = (*NBVersionCheck)(nil)
|
||||||
|
|
||||||
// sanitizeVersion removes anything after the pre-release tag (e.g., "-dev", "-alpha", etc.)
|
|
||||||
func sanitizeVersion(version string) string {
|
|
||||||
parts := strings.Split(version, "-")
|
|
||||||
return parts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NBVersionCheck) Check(ctx context.Context, peer nbpeer.Peer) (bool, error) {
|
func (n *NBVersionCheck) Check(ctx context.Context, peer nbpeer.Peer) (bool, error) {
|
||||||
meetsMin, err := MeetsMinVersion(n.MinVersion, peer.Meta.WtVersion)
|
meetsMin, err := nbversion.MeetsMinVersion(n.MinVersion, peer.Meta.WtVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -48,21 +40,3 @@ func (n *NBVersionCheck) Validate() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MeetsMinVersion checks if the peer's version meets or exceeds the minimum required version
|
|
||||||
func MeetsMinVersion(minVer, peerVer string) (bool, error) {
|
|
||||||
peerVer = sanitizeVersion(peerVer)
|
|
||||||
minVer = sanitizeVersion(minVer)
|
|
||||||
|
|
||||||
peerNBVer, err := version.NewVersion(peerVer)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
constraints, err := version.NewConstraint(">= " + minVer)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return constraints.Check(peerNBVer), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -139,68 +139,3 @@ func TestNBVersionCheck_Validate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMeetsMinVersion(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
minVer string
|
|
||||||
peerVer string
|
|
||||||
want bool
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Peer version greater than min version",
|
|
||||||
minVer: "0.26.0",
|
|
||||||
peerVer: "0.60.1",
|
|
||||||
want: true,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Peer version equals min version",
|
|
||||||
minVer: "1.0.0",
|
|
||||||
peerVer: "1.0.0",
|
|
||||||
want: true,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Peer version less than min version",
|
|
||||||
minVer: "1.0.0",
|
|
||||||
peerVer: "0.9.9",
|
|
||||||
want: false,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Peer version with pre-release tag greater than min version",
|
|
||||||
minVer: "1.0.0",
|
|
||||||
peerVer: "1.0.1-alpha",
|
|
||||||
want: true,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid peer version format",
|
|
||||||
minVer: "1.0.0",
|
|
||||||
peerVer: "dev",
|
|
||||||
want: false,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid min version format",
|
|
||||||
minVer: "invalid.version",
|
|
||||||
peerVer: "1.0.0",
|
|
||||||
want: false,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := MeetsMinVersion(tt.minVer, tt.peerVer)
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
|
||||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||||
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
||||||
@@ -21,6 +20,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/posture"
|
"github.com/netbirdio/netbird/management/server/posture"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/integration_reference"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestGetAccount_LoadsCustomDomains verifies GetAccount populates account.Domains.
|
// TestGetAccount_LoadsCustomDomains verifies GetAccount populates account.Domains.
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func (a *Account) toNetworkMapData(
|
|||||||
nmd.NameServerGroups = append(nmd.NameServerGroups, twinNSG(nsg))
|
nmd.NameServerGroups = append(nmd.NameServerGroups, twinNSG(nsg))
|
||||||
}
|
}
|
||||||
for _, res := range a.NetworkResources {
|
for _, res := range a.NetworkResources {
|
||||||
nmd.NetworkResources = append(nmd.NetworkResources, twinNetworkResource(res))
|
nmd.NetworkResources = append(nmd.NetworkResources, TwinNetworkResource(res))
|
||||||
}
|
}
|
||||||
for _, pc := range a.PostureChecks {
|
for _, pc := range a.PostureChecks {
|
||||||
if pc != nil {
|
if pc != nil {
|
||||||
@@ -274,7 +274,7 @@ func TwinRoute(r *nbroute.Route) *nmdata.Route {
|
|||||||
return twinRoute(r)
|
return twinRoute(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func twinNetworkResource(r *resourceTypes.NetworkResource) *nmdata.NetworkResource {
|
func TwinNetworkResource(r *resourceTypes.NetworkResource) *nmdata.NetworkResource {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,40 +2,25 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
nbroute "github.com/netbirdio/netbird/route"
|
nbroute "github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||||
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
|
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Type aliases for types relocated to shared/management/types so that the
|
// Type aliases for types relocated to shared/management/types so that the
|
||||||
// client-side compute path can depend on them
|
// client-side compute path can depend on them
|
||||||
|
|
||||||
type DNSSettings = sharedtypes.DNSSettings
|
|
||||||
|
|
||||||
type FirewallRule = sharedtypes.FirewallRule
|
type FirewallRule = sharedtypes.FirewallRule
|
||||||
|
|
||||||
type Group = sharedtypes.Group
|
|
||||||
type GroupPeer = sharedtypes.GroupPeer
|
|
||||||
|
|
||||||
type Network = sharedtypes.Network
|
|
||||||
type NetworkMap = sharedtypes.NetworkMap
|
type NetworkMap = sharedtypes.NetworkMap
|
||||||
type ForwardingRule = sharedtypes.ForwardingRule
|
type ForwardingRule = sharedtypes.ForwardingRule
|
||||||
|
|
||||||
type Policy = sharedtypes.Policy
|
|
||||||
type PolicyUpdateOperation = sharedtypes.PolicyUpdateOperation
|
|
||||||
|
|
||||||
type PolicyRule = sharedtypes.PolicyRule
|
|
||||||
type PolicyUpdateOperationType = sharedtypes.PolicyUpdateOperationType
|
|
||||||
type PolicyTrafficActionType = sharedtypes.PolicyTrafficActionType
|
type PolicyTrafficActionType = sharedtypes.PolicyTrafficActionType
|
||||||
type PolicyRuleProtocolType = sharedtypes.PolicyRuleProtocolType
|
type PolicyRuleProtocolType = sharedtypes.PolicyRuleProtocolType
|
||||||
type PolicyRuleDirection = sharedtypes.PolicyRuleDirection
|
|
||||||
type RulePortRange = sharedtypes.RulePortRange
|
type RulePortRange = sharedtypes.RulePortRange
|
||||||
|
|
||||||
type Resource = sharedtypes.Resource
|
|
||||||
type ResourceType = sharedtypes.ResourceType
|
type ResourceType = sharedtypes.ResourceType
|
||||||
|
|
||||||
type RouteFirewallRule = sharedtypes.RouteFirewallRule
|
type RouteFirewallRule = sharedtypes.RouteFirewallRule
|
||||||
@@ -52,19 +37,16 @@ type NetworkMapComponentsCompact = sharedtypes.NetworkMapComponentsCompact
|
|||||||
type LookupMap = sharedtypes.LookupMap
|
type LookupMap = sharedtypes.LookupMap
|
||||||
type FirewallRuleContext = sharedtypes.FirewallRuleContext
|
type FirewallRuleContext = sharedtypes.FirewallRuleContext
|
||||||
|
|
||||||
const (
|
|
||||||
GroupIssuedAPI = sharedtypes.GroupIssuedAPI
|
|
||||||
GroupIssuedJWT = sharedtypes.GroupIssuedJWT
|
|
||||||
GroupIssuedIntegration = sharedtypes.GroupIssuedIntegration
|
|
||||||
GroupAllName = sharedtypes.GroupAllName
|
|
||||||
)
|
|
||||||
|
|
||||||
// Function forwarders preserve types.X(...) call sites that previously
|
// Function forwarders preserve types.X(...) call sites that previously
|
||||||
// resolved to package-local funcs. Plain forwarders (not var aliases) keep
|
// resolved to package-local funcs. Plain forwarders (not var aliases) keep
|
||||||
// the symbol immutable and allow the inliner to flatten the call.
|
// the symbol immutable and allow the inliner to flatten the call.
|
||||||
|
|
||||||
|
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
|
||||||
|
return sharedtypes.ParseRuleString(rule)
|
||||||
|
}
|
||||||
|
|
||||||
func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool {
|
func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool {
|
||||||
return sharedtypes.PolicyRuleImpliesLegacySSH(rule)
|
return nmdata.PolicyRuleImpliesLegacySSH(twinRule(rule))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExpandPortsAndRanges / AppendIPv6FirewallRule / GenerateRouteFirewallRules
|
// ExpandPortsAndRanges / AppendIPv6FirewallRule / GenerateRouteFirewallRules
|
||||||
@@ -87,35 +69,16 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
|
|||||||
return sharedtypes.GenerateRouteFirewallRules(ctx, twinRoute(route), twinRule(rule), TwinPeers(groupPeers), direction, includeIPv6)
|
return sharedtypes.GenerateRouteFirewallRules(ctx, twinRoute(route), twinRule(rule), TwinPeers(groupPeers), direction, includeIPv6)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AllocateIPv6Subnet(r *rand.Rand) net.IPNet {
|
|
||||||
return sharedtypes.AllocateIPv6Subnet(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNetwork() *Network {
|
|
||||||
return sharedtypes.NewNetwork()
|
|
||||||
}
|
|
||||||
|
|
||||||
func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, error) {
|
|
||||||
return sharedtypes.AllocatePeerIP(prefix, takenIps)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
|
|
||||||
return sharedtypes.AllocateRandomPeerIP(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AllocateRandomPeerIPv6(prefix netip.Prefix) (netip.Addr, error) {
|
|
||||||
return sharedtypes.AllocateRandomPeerIPv6(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
|
|
||||||
return sharedtypes.ParseRuleString(rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FirewallRuleDirectionIN = sharedtypes.FirewallRuleDirectionIN
|
FirewallRuleDirectionIN = sharedtypes.FirewallRuleDirectionIN
|
||||||
FirewallRuleDirectionOUT = sharedtypes.FirewallRuleDirectionOUT
|
FirewallRuleDirectionOUT = sharedtypes.FirewallRuleDirectionOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AllowedIPsFormat = sharedtypes.AllowedIPsFormat
|
||||||
|
AllowedIPsV6Format = sharedtypes.AllowedIPsV6Format
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceTypePeer = sharedtypes.ResourceTypePeer
|
ResourceTypePeer = sharedtypes.ResourceTypePeer
|
||||||
ResourceTypeDomain = sharedtypes.ResourceTypeDomain
|
ResourceTypeDomain = sharedtypes.ResourceTypeDomain
|
||||||
@@ -135,15 +98,3 @@ const (
|
|||||||
PolicyRuleProtocolICMP = sharedtypes.PolicyRuleProtocolICMP
|
PolicyRuleProtocolICMP = sharedtypes.PolicyRuleProtocolICMP
|
||||||
PolicyRuleProtocolNetbirdSSH = sharedtypes.PolicyRuleProtocolNetbirdSSH
|
PolicyRuleProtocolNetbirdSSH = sharedtypes.PolicyRuleProtocolNetbirdSSH
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
PolicyRuleFlowDirect = sharedtypes.PolicyRuleFlowDirect
|
|
||||||
PolicyRuleFlowBidirect = sharedtypes.PolicyRuleFlowBidirect
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultRuleName = sharedtypes.DefaultRuleName
|
|
||||||
DefaultRuleDescription = sharedtypes.DefaultRuleDescription
|
|
||||||
DefaultPolicyName = sharedtypes.DefaultPolicyName
|
|
||||||
DefaultPolicyDescription = sharedtypes.DefaultPolicyDescription
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
"github.com/netbirdio/netbird/shared/management/integration_reference"
|
||||||
"github.com/netbirdio/netbird/management/server/networks/resources/types"
|
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -68,7 +68,7 @@ func (g *Group) EventMeta() map[string]any {
|
|||||||
return map[string]any{"name": g.Name}
|
return map[string]any{"name": g.Name}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) EventMetaResource(resource *types.NetworkResource) map[string]any {
|
func (g *Group) EventMetaResource(resource *nmdata.NetworkResource) map[string]any {
|
||||||
return map[string]any{"name": g.Name, "id": g.ID, "resource_name": resource.Name, "resource_id": resource.ID, "resource_type": resource.Type}
|
return map[string]any{"name": g.Name, "id": g.ID, "resource_name": resource.Name, "resource_id": resource.ID, "resource_type": resource.Type}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,14 +21,14 @@ import (
|
|||||||
type (
|
type (
|
||||||
Account = types.Account
|
Account = types.Account
|
||||||
|
|
||||||
DNSSettings = sharedtypes.DNSSettings
|
DNSSettings = types.DNSSettings
|
||||||
FirewallRule = sharedtypes.FirewallRule
|
FirewallRule = sharedtypes.FirewallRule
|
||||||
ForwardingRule = sharedtypes.ForwardingRule
|
ForwardingRule = sharedtypes.ForwardingRule
|
||||||
Group = sharedtypes.Group
|
Group = types.Group
|
||||||
Network = sharedtypes.Network
|
Network = types.Network
|
||||||
Policy = sharedtypes.Policy
|
Policy = types.Policy
|
||||||
PolicyRule = sharedtypes.PolicyRule
|
PolicyRule = types.PolicyRule
|
||||||
Resource = sharedtypes.Resource
|
Resource = types.Resource
|
||||||
RulePortRange = sharedtypes.RulePortRange
|
RulePortRange = sharedtypes.RulePortRange
|
||||||
RouteFirewallRule = sharedtypes.RouteFirewallRule
|
RouteFirewallRule = sharedtypes.RouteFirewallRule
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,32 +1,5 @@
|
|||||||
package types
|
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")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// PolicyRuleFlowDirect allows traffic from source to destination
|
// PolicyRuleFlowDirect allows traffic from source to destination
|
||||||
PolicyRuleFlowDirect = PolicyRuleDirection("direct")
|
PolicyRuleFlowDirect = PolicyRuleDirection("direct")
|
||||||
@@ -184,85 +157,3 @@ func (p *Policy) SourceGroups() []string {
|
|||||||
|
|
||||||
return groupIDs
|
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
|
|
||||||
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 (1–65535): %d", p)
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
"github.com/netbirdio/netbird/shared/management/integration_reference"
|
||||||
"github.com/netbirdio/netbird/util/crypt"
|
"github.com/netbirdio/netbird/util/crypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/idp/dex"
|
"github.com/netbirdio/netbird/idp/dex"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
"github.com/netbirdio/netbird/shared/management/integration_reference"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -19,34 +19,3 @@ func Difference(a, b []string) []string {
|
|||||||
func ToPtr[T any](value T) *T {
|
func ToPtr[T any](value T) *T {
|
||||||
return &value
|
return &value
|
||||||
}
|
}
|
||||||
|
|
||||||
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 !contains(result, item) {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range arr2 {
|
|
||||||
if !contains(result, item) {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains[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 util
|
|
||||||
|
|
||||||
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})
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||||
"github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
@@ -122,7 +121,7 @@ func DecodeEnvelope(ctx context.Context, env *proto.NetworkMapEnvelope) (*types.
|
|||||||
Resources: fromCompactResources(),
|
Resources: fromCompactResources(),
|
||||||
}
|
}
|
||||||
if gc.IsAll {
|
if gc.IsAll {
|
||||||
group.Name = types.GroupAllName
|
group.Name = nmdata.GroupAllName
|
||||||
}
|
}
|
||||||
c.Groups[groupID] = group
|
c.Groups[groupID] = group
|
||||||
}
|
}
|
||||||
@@ -327,10 +326,10 @@ func decodeAccountSettings(as *proto.AccountSettingsCompact) *nmdata.AccountSett
|
|||||||
func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nmdata.Peer {
|
func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nmdata.Peer {
|
||||||
var caps []int32
|
var caps []int32
|
||||||
if pc.SupportsSourcePrefixes {
|
if pc.SupportsSourcePrefixes {
|
||||||
caps = append(caps, nbpeer.PeerCapabilitySourcePrefixes)
|
caps = append(caps, nmdata.PeerCapabilitySourcePrefixes)
|
||||||
}
|
}
|
||||||
if pc.SupportsIpv6 {
|
if pc.SupportsIpv6 {
|
||||||
caps = append(caps, nbpeer.PeerCapabilityIPv6Overlay)
|
caps = append(caps, nmdata.PeerCapabilityIPv6Overlay)
|
||||||
}
|
}
|
||||||
peer := &nmdata.Peer{
|
peer := &nmdata.Peer{
|
||||||
ID: peerID,
|
ID: peerID,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package nmdata
|
|||||||
|
|
||||||
import "slices"
|
import "slices"
|
||||||
|
|
||||||
const groupAllName = "All"
|
// GroupAllName is the reserved name of the default group that contains every
|
||||||
|
// peer in an account.
|
||||||
|
const GroupAllName = "All"
|
||||||
|
|
||||||
// Group is the slim twin of types.Group.
|
// Group is the slim twin of types.Group.
|
||||||
type Group struct {
|
type Group struct {
|
||||||
@@ -14,7 +16,7 @@ type Group struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) IsGroupAll() bool {
|
func (g *Group) IsGroupAll() bool {
|
||||||
return g.Name == groupAllName
|
return g.Name == GroupAllName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) Copy() *Group {
|
func (g *Group) Copy() *Group {
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Peer capability constants mirror the proto enum values.
|
||||||
const (
|
const (
|
||||||
peerCapabilitySourcePrefixes int32 = 1
|
PeerCapabilitySourcePrefixes int32 = 1
|
||||||
peerCapabilityIPv6Overlay int32 = 2
|
PeerCapabilityIPv6Overlay int32 = 2
|
||||||
|
PeerCapabilityComponentNetworkMap int32 = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// Peer is the slim twin of peer.Peer.
|
// Peer is the slim twin of peer.Peer.
|
||||||
@@ -78,11 +80,11 @@ func (p *Peer) HasCapability(capability int32) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) SupportsIPv6() bool {
|
func (p *Peer) SupportsIPv6() bool {
|
||||||
return !p.Meta.Flags.DisableIPv6 && p.HasCapability(peerCapabilityIPv6Overlay)
|
return !p.Meta.Flags.DisableIPv6 && p.HasCapability(PeerCapabilityIPv6Overlay)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) SupportsSourcePrefixes() bool {
|
func (p *Peer) SupportsSourcePrefixes() bool {
|
||||||
return p.HasCapability(peerCapabilitySourcePrefixes)
|
return p.HasCapability(PeerCapabilitySourcePrefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) AddedWithSSOLogin() bool {
|
func (p *Peer) AddedWithSSOLogin() bool {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package types
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/posture"
|
|
||||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
@@ -25,28 +24,6 @@ type supportedFeatures struct {
|
|||||||
|
|
||||||
type LookupMap map[string]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.
|
// ExpandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules.
|
||||||
func ExpandPortsAndRanges(base FirewallRule, rule *nmdata.PolicyRule, peer *nmdata.Peer) []*FirewallRule {
|
func ExpandPortsAndRanges(base FirewallRule, rule *nmdata.PolicyRule, peer *nmdata.Peer) []*FirewallRule {
|
||||||
features := peerSupportedFirewallFeatures(peer.Meta.WtVersion)
|
features := peerSupportedFirewallFeatures(peer.Meta.WtVersion)
|
||||||
@@ -117,13 +94,13 @@ func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
|
|||||||
|
|
||||||
var features supportedFeatures
|
var features supportedFeatures
|
||||||
|
|
||||||
meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinNativeSSHVer, peerVer)
|
meetMinVer, err := version.MeetsMinVersion(firewallRuleMinNativeSSHVer, peerVer)
|
||||||
features.nativeSSH = err == nil && meetMinVer
|
features.nativeSSH = err == nil && meetMinVer
|
||||||
|
|
||||||
if features.nativeSSH {
|
if features.nativeSSH {
|
||||||
features.portRanges = true
|
features.portRanges = true
|
||||||
} else {
|
} else {
|
||||||
meetMinVer, err = posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer)
|
meetMinVer, err = version.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer)
|
||||||
features.portRanges = err == nil && meetMinVer
|
features.portRanges = err == nil && meetMinVer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,20 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/c-robinson/iplib"
|
|
||||||
"github.com/rs/xid"
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/management/server/util"
|
|
||||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||||
"github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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 generates Wireguard AllowedIPs format (e.g. 100.64.30.1/32)
|
||||||
AllowedIPsFormat = "%s/32"
|
AllowedIPsFormat = "%s/32"
|
||||||
// AllowedIPsV6Format generates AllowedIPs format for v6 (e.g. fd12:3456:7890::1/128)
|
// AllowedIPsV6Format generates AllowedIPs format for v6 (e.g. fd12:3456:7890::1/128)
|
||||||
AllowedIPsV6Format = "%s/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 {
|
type NetworkMap struct {
|
||||||
@@ -56,11 +36,11 @@ type NetworkMap struct {
|
|||||||
|
|
||||||
func (nm *NetworkMap) Merge(other *NetworkMap) {
|
func (nm *NetworkMap) Merge(other *NetworkMap) {
|
||||||
nm.Peers = mergeUniquePeersByID(nm.Peers, other.Peers)
|
nm.Peers = mergeUniquePeersByID(nm.Peers, other.Peers)
|
||||||
nm.Routes = util.MergeUnique(nm.Routes, other.Routes)
|
nm.Routes = mergeUnique(nm.Routes, other.Routes)
|
||||||
nm.OfflinePeers = mergeUniquePeersByID(nm.OfflinePeers, other.OfflinePeers)
|
nm.OfflinePeers = mergeUniquePeersByID(nm.OfflinePeers, other.OfflinePeers)
|
||||||
nm.FirewallRules = util.MergeUnique(nm.FirewallRules, other.FirewallRules)
|
nm.FirewallRules = mergeUnique(nm.FirewallRules, other.FirewallRules)
|
||||||
nm.RoutesFirewallRules = util.MergeUnique(nm.RoutesFirewallRules, other.RoutesFirewallRules)
|
nm.RoutesFirewallRules = mergeUnique(nm.RoutesFirewallRules, other.RoutesFirewallRules)
|
||||||
nm.ForwardingRules = util.MergeUnique(nm.ForwardingRules, other.ForwardingRules)
|
nm.ForwardingRules = mergeUnique(nm.ForwardingRules, other.ForwardingRules)
|
||||||
nm.ForceRoutingPeerDNSResolution = nm.ForceRoutingPeerDNSResolution || other.ForceRoutingPeerDNSResolution
|
nm.ForceRoutingPeerDNSResolution = nm.ForceRoutingPeerDNSResolution || other.ForceRoutingPeerDNSResolution
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,245 +101,33 @@ func ipToBytes(ip net.IP) []byte {
|
|||||||
return ip.To16()
|
return ip.To16()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Network struct {
|
type comparableObject[T any] interface {
|
||||||
Identifier string `json:"id"`
|
Equal(other T) bool
|
||||||
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:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNetwork creates a new Network initializing it with a Serial=0
|
func mergeUnique[T comparableObject[T]](arr1, arr2 []T) []T {
|
||||||
// It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets)
|
var result []T
|
||||||
// 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)
|
|
||||||
|
|
||||||
s := rand.NewSource(time.Now().UnixNano())
|
for _, item := range arr1 {
|
||||||
r := rand.New(s)
|
if !containsEqual(result, item) {
|
||||||
intn := r.Intn(len(sub))
|
result = append(result, item)
|
||||||
|
|
||||||
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 offset := uint32(1); offset < totalIPs-1; offset++ {
|
for _, item := range arr2 {
|
||||||
candidate := baseIP + offset
|
if !containsEqual(result, item) {
|
||||||
if _, exists := taken[candidate]; !exists {
|
result = append(result, item)
|
||||||
return uint32ToIP(candidate), nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 containsEqual[T comparableObject[T]](slice []T, element T) bool {
|
||||||
func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
|
for _, item := range slice {
|
||||||
b := prefix.Masked().Addr().As4()
|
if item.Equal(element) {
|
||||||
baseIP := binary.BigEndian.Uint32(b[:])
|
return true
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,264 +1,41 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewNetwork(t *testing.T) {
|
type mergeTestObject struct {
|
||||||
network := NewNetwork()
|
value int
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllocatePeerIP(t *testing.T) {
|
func (t mergeTestObject) Equal(other mergeTestObject) bool {
|
||||||
prefix := netip.MustParsePrefix("100.64.0.0/24")
|
return t.value == other.value
|
||||||
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 TestAllocatePeerIPSmallSubnet(t *testing.T) {
|
func Test_MergeUniqueArraysWithoutDuplicates(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)
|
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
|
||||||
prefix := netip.MustParsePrefix("10.0.0.0/27")
|
arr2 := []mergeTestObject{{value: 2}, {value: 3}}
|
||||||
var ips []netip.Addr
|
result := mergeUnique(arr1, arr2)
|
||||||
|
assert.Len(t, result, 3)
|
||||||
// Allocate all available IPs in the /27 network
|
assert.Contains(t, result, mergeTestObject{value: 1})
|
||||||
for i := 0; i < 30; i++ {
|
assert.Contains(t, result, mergeTestObject{value: 2})
|
||||||
ip, err := AllocatePeerIP(prefix, ips)
|
assert.Contains(t, result, mergeTestObject{value: 3})
|
||||||
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 TestAllocatePeerIPVariousCIDRs(t *testing.T) {
|
func Test_MergeUniqueHandlesEmptyArrays(t *testing.T) {
|
||||||
testCases := []struct {
|
arr1 := []mergeTestObject{}
|
||||||
name string
|
arr2 := []mergeTestObject{}
|
||||||
cidr string
|
result := mergeUnique(arr1, arr2)
|
||||||
expectedUsable int
|
assert.Empty(t, result)
|
||||||
}{
|
|
||||||
{"/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 TestGenerateIPs(t *testing.T) {
|
func Test_MergeUniqueHandlesOneEmptyArray(t *testing.T) {
|
||||||
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
|
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
|
||||||
ips, ipsLen := generateIPs(&ipNet, map[string]struct{}{"100.64.0.0": {}})
|
arr2 := []mergeTestObject{}
|
||||||
if ipsLen != 252 {
|
result := mergeUnique(arr1, arr2)
|
||||||
t.Errorf("expected 252 ips, got %d", len(ips))
|
assert.Len(t, result, 2)
|
||||||
return
|
assert.Contains(t, result, mergeTestObject{value: 1})
|
||||||
}
|
assert.Contains(t, result, mergeTestObject{value: 2})
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/ssh/auth"
|
"github.com/netbirdio/netbird/client/ssh/auth"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||||
@@ -868,7 +867,7 @@ func (c *NetworkMapComponents) networkResourceToRoute(resource *nmdata.NetworkRe
|
|||||||
Description: resource.Description,
|
Description: resource.Description,
|
||||||
}
|
}
|
||||||
|
|
||||||
if resource.Type == string(resourceTypes.Host) || resource.Type == string(resourceTypes.Subnet) {
|
if resource.Type == string(ResourceTypeHost) || resource.Type == string(ResourceTypeSubnet) {
|
||||||
r.Network = resource.Prefix
|
r.Network = resource.Prefix
|
||||||
|
|
||||||
r.NetworkType = nmdata.NetworkTypeIPv4
|
r.NetworkType = nmdata.NetworkTypeIPv4
|
||||||
@@ -877,7 +876,7 @@ func (c *NetworkMapComponents) networkResourceToRoute(resource *nmdata.NetworkRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resource.Type == string(resourceTypes.Domain) {
|
if resource.Type == string(ResourceTypeDomain) {
|
||||||
domainList, err := domain.FromStringList([]string{resource.Domain})
|
domainList, err := domain.FromStringList([]string{resource.Domain})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
r.Domains = domainList
|
r.Domains = domainList
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PolicyUpdateOperationType operation type
|
|
||||||
type PolicyUpdateOperationType int
|
|
||||||
|
|
||||||
// PolicyTrafficActionType action type for the firewall
|
// PolicyTrafficActionType action type for the firewall
|
||||||
type PolicyTrafficActionType string
|
type PolicyTrafficActionType string
|
||||||
|
|
||||||
// PolicyRuleProtocolType type of traffic
|
// PolicyRuleProtocolType type of traffic
|
||||||
type PolicyRuleProtocolType string
|
type PolicyRuleProtocolType string
|
||||||
|
|
||||||
// PolicyRuleDirection direction of traffic
|
const (
|
||||||
type PolicyRuleDirection string
|
// 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")
|
||||||
|
)
|
||||||
|
|
||||||
// RulePortRange represents a range of ports for a firewall rule.
|
// RulePortRange represents a range of ports for a firewall rule.
|
||||||
type RulePortRange struct {
|
type RulePortRange struct {
|
||||||
@@ -39,187 +56,84 @@ func (r *RulePortRange) Equal(other *RulePortRange) bool {
|
|||||||
return r.Start == other.Start && r.End == other.End
|
return r.Start == other.Start && r.End == other.End
|
||||||
}
|
}
|
||||||
|
|
||||||
// PolicyRule is the metadata of the policy
|
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
|
||||||
type PolicyRule struct {
|
rule = strings.TrimSpace(strings.ToLower(rule))
|
||||||
// ID of the policy rule
|
if rule == "all" {
|
||||||
ID string `gorm:"primaryKey"`
|
return PolicyRuleProtocolALL, RulePortRange{}, nil
|
||||||
|
}
|
||||||
|
if rule == "icmp" {
|
||||||
|
return PolicyRuleProtocolICMP, RulePortRange{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// PolicyID is a reference to Policy that this object belongs
|
split := strings.Split(rule, "/")
|
||||||
PolicyID string `json:"-" gorm:"index"`
|
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
|
protoStr := strings.TrimSpace(split[0])
|
||||||
Name string
|
portStr := strings.TrimSpace(split[1])
|
||||||
|
|
||||||
// Description of the rule visible in the UI
|
var protocol PolicyRuleProtocolType
|
||||||
Description string
|
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
|
||||||
|
default:
|
||||||
|
return "", RulePortRange{}, fmt.Errorf("invalid protocol: %q", protoStr)
|
||||||
|
}
|
||||||
|
|
||||||
// Enabled status of rule in the system
|
portRange, err := parsePortRange(portStr)
|
||||||
Enabled bool
|
if err != nil {
|
||||||
|
return "", RulePortRange{}, err
|
||||||
|
}
|
||||||
|
|
||||||
// Action policy accept or drops packets
|
return protocol, portRange, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy returns a copy of a policy rule
|
func parsePortRange(portStr string) (RulePortRange, error) {
|
||||||
func (pm *PolicyRule) Copy() *PolicyRule {
|
if strings.Contains(portStr, "-") {
|
||||||
rule := &PolicyRule{
|
rangeParts := strings.Split(portStr, "-")
|
||||||
ID: pm.ID,
|
if len(rangeParts) != 2 {
|
||||||
PolicyID: pm.PolicyID,
|
return RulePortRange{}, fmt.Errorf("invalid port range %q", portStr)
|
||||||
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,
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
if x.End != y.End {
|
start, err := parsePort(strings.TrimSpace(rangeParts[0]))
|
||||||
if x.End < y.End {
|
if err != nil {
|
||||||
return -1
|
return RulePortRange{}, err
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
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))
|
p, err := parsePort(portStr)
|
||||||
copy(sorted1, a)
|
if err != nil {
|
||||||
copy(sorted2, b)
|
return RulePortRange{}, err
|
||||||
slices.SortFunc(sorted1, cmp)
|
}
|
||||||
slices.SortFunc(sorted2, cmp)
|
|
||||||
return slices.EqualFunc(sorted1, sorted2, func(x, y RulePortRange) bool {
|
return RulePortRange{Start: uint16(p), End: uint16(p)}, nil
|
||||||
return x.Start == y.Start && x.End == y.End
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func authorizedGroupsEqual(a, b map[string][]string) bool {
|
func parsePort(portStr string) (int, error) {
|
||||||
if len(a) != len(b) {
|
|
||||||
return false
|
if portStr == "" {
|
||||||
|
return 0, errors.New("empty port")
|
||||||
}
|
}
|
||||||
for k, va := range a {
|
p, err := strconv.Atoi(portStr)
|
||||||
vb, ok := b[k]
|
if err != nil {
|
||||||
if !ok {
|
return 0, fmt.Errorf("invalid port %q: %w", portStr, err)
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !stringSlicesEqualUnordered(va, vb) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true
|
if p < 1 || p > 65535 {
|
||||||
|
return 0, fmt.Errorf("port out of range (1–65535): %d", p)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ResourceType string
|
type ResourceType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -12,28 +8,3 @@ const (
|
|||||||
ResourceTypeHost ResourceType = "host"
|
ResourceTypeHost ResourceType = "host"
|
||||||
ResourceTypeSubnet ResourceType = "subnet"
|
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 (r *Resource) FromAPIRequest(req *api.Resource) {
|
|
||||||
if req == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.ID = req.Id
|
|
||||||
r.Type = ResourceType(req.Type)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user