mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-05 00:26:39 +00:00
This change enables admins to configure posture checks for connecting public IPs of their peers. It changes the behavior of the check as well and now the evaluation is if the received network is part of the configured network.
102 lines
3.3 KiB
Go
102 lines
3.3 KiB
Go
package posture
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/netip"
|
|
"slices"
|
|
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/shared/management/status"
|
|
)
|
|
|
|
type PeerNetworkRangeCheck struct {
|
|
Action string
|
|
Ranges []netip.Prefix `gorm:"serializer:json"`
|
|
}
|
|
|
|
var _ Check = (*PeerNetworkRangeCheck)(nil)
|
|
|
|
// prefixContains reports whether outer fully contains inner (equal counts as contained).
|
|
// Requires the same address family, that outer is no more specific than inner (its
|
|
// netmask is shorter or equal), and that inner's network address falls inside outer.
|
|
// This is stricter than netip.Prefix.Contains(Addr) — a peer's /24 NIC will not match a
|
|
// configured /32 rule, since the rule covers a single host but the NIC describes a whole
|
|
// subnet whose host bits are unknown.
|
|
func prefixContains(outer, inner netip.Prefix) bool {
|
|
outer = outer.Masked()
|
|
inner = inner.Masked()
|
|
return outer.Bits() <= inner.Bits() &&
|
|
outer.Addr().BitLen() == inner.Addr().BitLen() && // same family
|
|
outer.Contains(inner.Addr())
|
|
}
|
|
|
|
// Check evaluates configured ranges against the peer's local network interface prefixes
|
|
// and its public connection IP (as a /32 or /128). A configured range matches when it
|
|
// fully contains one of those prefixes, so operators can target both private subnets
|
|
// and public CIDRs (e.g. 1.0.0.0/24, 2.2.2.2/32). Including the connection IP is what
|
|
// lets a public-range posture check work — peer.Meta.NetworkAddresses only carries
|
|
// local NIC addresses.
|
|
func (p *PeerNetworkRangeCheck) Check(ctx context.Context, peer nbpeer.Peer) (bool, error) {
|
|
peerPrefixes := make([]netip.Prefix, 0, len(peer.Meta.NetworkAddresses)+1)
|
|
for _, peerNetAddr := range peer.Meta.NetworkAddresses {
|
|
peerPrefixes = append(peerPrefixes, peerNetAddr.NetIP)
|
|
}
|
|
// Unmap collapses 4-in-6 forms (::ffff:a.b.c.d) so an IPv4 range matches.
|
|
if connIP := peer.Location.ConnectionIP; len(connIP) > 0 {
|
|
if addr, ok := netip.AddrFromSlice(connIP); ok {
|
|
addr = addr.Unmap()
|
|
peerPrefixes = append(peerPrefixes, netip.PrefixFrom(addr, addr.BitLen()))
|
|
}
|
|
}
|
|
|
|
if len(peerPrefixes) == 0 {
|
|
return false, fmt.Errorf("peer's does not contain peer network range addresses")
|
|
}
|
|
|
|
for _, peerPrefix := range peerPrefixes {
|
|
for _, rangePrefix := range p.Ranges {
|
|
if !prefixContains(rangePrefix, peerPrefix) {
|
|
continue
|
|
}
|
|
switch p.Action {
|
|
case CheckActionDeny:
|
|
return false, nil
|
|
case CheckActionAllow:
|
|
return true, nil
|
|
default:
|
|
return false, fmt.Errorf("invalid peer network range check action: %s", p.Action)
|
|
}
|
|
}
|
|
}
|
|
|
|
if p.Action == CheckActionDeny {
|
|
return true, nil
|
|
}
|
|
if p.Action == CheckActionAllow {
|
|
return false, nil
|
|
}
|
|
|
|
return false, fmt.Errorf("invalid peer network range check action: %s", p.Action)
|
|
}
|
|
|
|
func (p *PeerNetworkRangeCheck) Name() string {
|
|
return PeerNetworkRangeCheckName
|
|
}
|
|
|
|
func (p *PeerNetworkRangeCheck) Validate() error {
|
|
if p.Action == "" {
|
|
return status.Errorf(status.InvalidArgument, "action for peer network range check shouldn't be empty")
|
|
}
|
|
|
|
allowedActions := []string{CheckActionAllow, CheckActionDeny}
|
|
if !slices.Contains(allowedActions, p.Action) {
|
|
return fmt.Errorf("%s action is not valid", p.Name())
|
|
}
|
|
|
|
if len(p.Ranges) == 0 {
|
|
return fmt.Errorf("%s network ranges shouldn't be empty", p.Name())
|
|
}
|
|
return nil
|
|
}
|