mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 16:26:38 +00:00
Add private network posture check (#1606)
* wip: Add PrivateNetworkCheck checks interface implementation * use generic CheckAction constant * Add private network check to posture checks * Fix copy function target in posture checks * Add network check functionality to posture package * regenerate the openapi specs * Update Posture Check actions in test file * Remove unused function * Refactor network address handling in PrivateNetworkCheck * Refactor Prefixes to Ranges in private network checks * Implement private network checks in posture checks handler tests * Add test for check copy * Add gorm serializer for network range
This commit is contained in:
@@ -2,6 +2,7 @@ package posture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
@@ -9,9 +10,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
NBVersionCheckName = "NBVersionCheck"
|
||||
OSVersionCheckName = "OSVersionCheck"
|
||||
GeoLocationCheckName = "GeoLocationCheck"
|
||||
NBVersionCheckName = "NBVersionCheck"
|
||||
OSVersionCheckName = "OSVersionCheck"
|
||||
GeoLocationCheckName = "GeoLocationCheck"
|
||||
PrivateNetworkCheckName = "PrivateNetworkCheck"
|
||||
|
||||
CheckActionAllow string = "allow"
|
||||
CheckActionDeny string = "deny"
|
||||
)
|
||||
|
||||
// Check represents an interface for performing a check on a peer.
|
||||
@@ -39,9 +44,10 @@ type Checks struct {
|
||||
|
||||
// ChecksDefinition contains definition of actual check
|
||||
type ChecksDefinition struct {
|
||||
NBVersionCheck *NBVersionCheck `json:",omitempty"`
|
||||
OSVersionCheck *OSVersionCheck `json:",omitempty"`
|
||||
GeoLocationCheck *GeoLocationCheck `json:",omitempty"`
|
||||
NBVersionCheck *NBVersionCheck `json:",omitempty"`
|
||||
OSVersionCheck *OSVersionCheck `json:",omitempty"`
|
||||
GeoLocationCheck *GeoLocationCheck `json:",omitempty"`
|
||||
PrivateNetworkCheck *PrivateNetworkCheck `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Copy returns a copy of a checks definition.
|
||||
@@ -54,7 +60,7 @@ func (cd ChecksDefinition) Copy() ChecksDefinition {
|
||||
}
|
||||
if cd.OSVersionCheck != nil {
|
||||
cdCopy.OSVersionCheck = &OSVersionCheck{}
|
||||
osCheck := cdCopy.OSVersionCheck
|
||||
osCheck := cd.OSVersionCheck
|
||||
if osCheck.Android != nil {
|
||||
cdCopy.OSVersionCheck.Android = &MinVersionCheck{MinVersion: osCheck.Android.MinVersion}
|
||||
}
|
||||
@@ -79,6 +85,14 @@ func (cd ChecksDefinition) Copy() ChecksDefinition {
|
||||
}
|
||||
copy(cdCopy.GeoLocationCheck.Locations, geoCheck.Locations)
|
||||
}
|
||||
if cd.PrivateNetworkCheck != nil {
|
||||
privateNetCheck := cd.PrivateNetworkCheck
|
||||
cdCopy.PrivateNetworkCheck = &PrivateNetworkCheck{
|
||||
Action: privateNetCheck.Action,
|
||||
Ranges: make([]netip.Prefix, len(privateNetCheck.Ranges)),
|
||||
}
|
||||
copy(cdCopy.PrivateNetworkCheck.Ranges, privateNetCheck.Ranges)
|
||||
}
|
||||
return cdCopy
|
||||
}
|
||||
|
||||
@@ -116,6 +130,9 @@ func (pc *Checks) GetChecks() []Check {
|
||||
if pc.Checks.GeoLocationCheck != nil {
|
||||
checks = append(checks, pc.Checks.GeoLocationCheck)
|
||||
}
|
||||
if pc.Checks.PrivateNetworkCheck != nil {
|
||||
checks = append(checks, pc.Checks.PrivateNetworkCheck)
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package posture
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -216,3 +217,62 @@ func TestChecks_Validate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecks_Copy(t *testing.T) {
|
||||
check := &Checks{
|
||||
ID: "1",
|
||||
Name: "default",
|
||||
Description: "description",
|
||||
AccountID: "accountID",
|
||||
Checks: ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{
|
||||
MinVersion: "0.25.0",
|
||||
},
|
||||
OSVersionCheck: &OSVersionCheck{
|
||||
Android: &MinVersionCheck{
|
||||
MinVersion: "13",
|
||||
},
|
||||
Darwin: &MinVersionCheck{
|
||||
MinVersion: "14.2.0",
|
||||
},
|
||||
Ios: &MinVersionCheck{
|
||||
MinVersion: "17.3.0",
|
||||
},
|
||||
Linux: &MinKernelVersionCheck{
|
||||
MinKernelVersion: "6.5.11-linuxkit",
|
||||
},
|
||||
Windows: &MinKernelVersionCheck{
|
||||
MinKernelVersion: "10.0.14393",
|
||||
},
|
||||
},
|
||||
GeoLocationCheck: &GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
Action: CheckActionAllow,
|
||||
},
|
||||
PrivateNetworkCheck: &PrivateNetworkCheck{
|
||||
Ranges: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
},
|
||||
Action: CheckActionDeny,
|
||||
},
|
||||
},
|
||||
}
|
||||
checkCopy := check.Copy()
|
||||
|
||||
assert.Equal(t, check.ID, checkCopy.ID)
|
||||
assert.Equal(t, check.Name, checkCopy.Name)
|
||||
assert.Equal(t, check.Description, checkCopy.Description)
|
||||
assert.Equal(t, check.AccountID, checkCopy.AccountID)
|
||||
assert.Equal(t, check.Checks.Copy(), checkCopy.Checks.Copy())
|
||||
assert.ElementsMatch(t, check.GetChecks(), checkCopy.GetChecks())
|
||||
|
||||
// Updating the original check should not take effect on copy
|
||||
check.Name = "name"
|
||||
assert.NotSame(t, check, checkCopy)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ import (
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
)
|
||||
|
||||
const (
|
||||
GeoLocationActionAllow string = "allow"
|
||||
GeoLocationActionDeny string = "deny"
|
||||
)
|
||||
|
||||
type Location struct {
|
||||
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
|
||||
CountryCode string
|
||||
@@ -39,9 +34,9 @@ func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) {
|
||||
if loc.CountryCode == peer.Location.CountryCode {
|
||||
if loc.CityName == "" || loc.CityName == peer.Location.CityName {
|
||||
switch g.Action {
|
||||
case GeoLocationActionDeny:
|
||||
case CheckActionDeny:
|
||||
return false, nil
|
||||
case GeoLocationActionAllow:
|
||||
case CheckActionAllow:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("invalid geo location action: %s", g.Action)
|
||||
@@ -51,11 +46,11 @@ func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) {
|
||||
}
|
||||
// At this point, no location in the list matches the peer's location
|
||||
// For action deny and no location match, allow the peer
|
||||
if g.Action == GeoLocationActionDeny {
|
||||
if g.Action == CheckActionDeny {
|
||||
return true, nil
|
||||
}
|
||||
// For action allow and no location match, deny the peer
|
||||
if g.Action == GeoLocationActionAllow {
|
||||
if g.Action == CheckActionAllow {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
Action: CheckActionAllow,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
@@ -54,7 +54,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CountryCode: "DE",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
Action: CheckActionAllow,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
@@ -78,7 +78,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
Action: CheckActionAllow,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
@@ -97,7 +97,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CountryCode: "US",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
Action: CheckActionAllow,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
@@ -121,7 +121,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
Action: CheckActionDeny,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
@@ -143,7 +143,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CountryCode: "US",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
Action: CheckActionDeny,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
@@ -167,7 +167,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
Action: CheckActionDeny,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
@@ -187,7 +187,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
Action: CheckActionDeny,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
@@ -202,7 +202,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
Action: CheckActionAllow,
|
||||
},
|
||||
wantErr: true,
|
||||
isValid: false,
|
||||
@@ -217,7 +217,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
Action: CheckActionDeny,
|
||||
},
|
||||
wantErr: true,
|
||||
isValid: false,
|
||||
|
||||
54
management/server/posture/network.go
Normal file
54
management/server/posture/network.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
)
|
||||
|
||||
type PrivateNetworkCheck struct {
|
||||
Action string
|
||||
Ranges []netip.Prefix `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
var _ Check = (*PrivateNetworkCheck)(nil)
|
||||
|
||||
func (p *PrivateNetworkCheck) Check(peer nbpeer.Peer) (bool, error) {
|
||||
if len(peer.Meta.NetworkAddresses) == 0 {
|
||||
return false, fmt.Errorf("peer's does not contain private network addresses")
|
||||
}
|
||||
|
||||
maskedPrefixes := make([]netip.Prefix, 0, len(p.Ranges))
|
||||
for _, prefix := range p.Ranges {
|
||||
maskedPrefixes = append(maskedPrefixes, prefix.Masked())
|
||||
}
|
||||
|
||||
for _, peerNetAddr := range peer.Meta.NetworkAddresses {
|
||||
peerMaskedPrefix := peerNetAddr.NetIP.Masked()
|
||||
if slices.Contains(maskedPrefixes, peerMaskedPrefix) {
|
||||
switch p.Action {
|
||||
case CheckActionDeny:
|
||||
return false, nil
|
||||
case CheckActionAllow:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("invalid private network check action: %s", p.Action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.Action == CheckActionDeny {
|
||||
return true, nil
|
||||
}
|
||||
if p.Action == CheckActionAllow {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid private network check action: %s", p.Action)
|
||||
}
|
||||
|
||||
func (p *PrivateNetworkCheck) Name() string {
|
||||
return PrivateNetworkCheckName
|
||||
}
|
||||
149
management/server/posture/network_test.go
Normal file
149
management/server/posture/network_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
)
|
||||
|
||||
func TestPrivateNetworkCheck_Check(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
check PrivateNetworkCheck
|
||||
peer nbpeer.Peer
|
||||
wantErr bool
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
name: "Peer private networks matches the allowed range",
|
||||
check: PrivateNetworkCheck{
|
||||
Action: CheckActionAllow,
|
||||
Ranges: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
},
|
||||
},
|
||||
peer: nbpeer.Peer{
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
NetworkAddresses: []nbpeer.NetworkAddress{
|
||||
{
|
||||
NetIP: netip.MustParsePrefix("192.168.0.123/24"),
|
||||
},
|
||||
{
|
||||
NetIP: netip.MustParsePrefix("fe80::6089:eaff:fe0c:232f/64"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Peer private networks doesn't matches the allowed range",
|
||||
check: PrivateNetworkCheck{
|
||||
Action: CheckActionAllow,
|
||||
Ranges: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
},
|
||||
},
|
||||
peer: nbpeer.Peer{
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
NetworkAddresses: []nbpeer.NetworkAddress{
|
||||
{
|
||||
NetIP: netip.MustParsePrefix("198.19.249.3/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Peer with no privates network in the allow range",
|
||||
check: PrivateNetworkCheck{
|
||||
Action: CheckActionAllow,
|
||||
Ranges: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/16"),
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
},
|
||||
},
|
||||
peer: nbpeer.Peer{},
|
||||
wantErr: true,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Peer private networks matches the denied range",
|
||||
check: PrivateNetworkCheck{
|
||||
Action: CheckActionDeny,
|
||||
Ranges: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
},
|
||||
},
|
||||
peer: nbpeer.Peer{
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
NetworkAddresses: []nbpeer.NetworkAddress{
|
||||
{
|
||||
NetIP: netip.MustParsePrefix("192.168.0.123/24"),
|
||||
},
|
||||
{
|
||||
NetIP: netip.MustParsePrefix("fe80::6089:eaff:fe0c:232f/64"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Peer private networks doesn't matches the denied range",
|
||||
check: PrivateNetworkCheck{
|
||||
Action: CheckActionDeny,
|
||||
Ranges: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
},
|
||||
},
|
||||
peer: nbpeer.Peer{
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
NetworkAddresses: []nbpeer.NetworkAddress{
|
||||
{
|
||||
NetIP: netip.MustParsePrefix("198.19.249.3/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Peer with no private networks in the denied range",
|
||||
check: PrivateNetworkCheck{
|
||||
Action: CheckActionDeny,
|
||||
Ranges: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/16"),
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
},
|
||||
},
|
||||
peer: nbpeer.Peer{},
|
||||
wantErr: true,
|
||||
isValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isValid, err := tt.check.Check(tt.peer)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.isValid, isValid)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user