mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-06 15:01:28 +02:00
Merge branch 'main' into embedded-vnc
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
package networkmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
@@ -24,7 +25,7 @@ import (
|
||||
// ID scheme on the client side:
|
||||
//
|
||||
// Peers base64(wg_pub_key) // stable across snapshots
|
||||
func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, error) {
|
||||
func DecodeEnvelope(ctx context.Context, env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, error) {
|
||||
full := env.GetFull()
|
||||
if full == nil {
|
||||
return nil, fmt.Errorf("envelope has no Full payload")
|
||||
@@ -35,28 +36,28 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
|
||||
Network: decodeAccountNetwork(full.Network),
|
||||
AccountSettings: decodeAccountSettings(full.AccountSettings),
|
||||
CustomZoneDomain: full.CustomZoneDomain,
|
||||
Peers: make(map[string]*types.ComponentPeer, len(full.Peers)),
|
||||
Groups: make(map[string]*types.ComponentGroup, len(full.Groups)),
|
||||
Policies: make([]*types.Policy, 0, len(full.Policies)),
|
||||
Routes: make([]*nbroute.Route, 0, len(full.Routes)),
|
||||
NameServerGroups: make([]*nbdns.NameServerGroup, 0, len(full.NameserverGroups)),
|
||||
Peers: make(map[string]*nmdata.Peer, len(full.Peers)),
|
||||
Groups: make(map[string]*nmdata.Group, len(full.Groups)),
|
||||
Policies: make([]*nmdata.Policy, 0, len(full.Policies)),
|
||||
Routes: make([]*nmdata.Route, 0, len(full.Routes)),
|
||||
NameServerGroups: make([]*nmdata.NameServerGroup, 0, len(full.NameserverGroups)),
|
||||
AllDNSRecords: decodeSimpleRecords(full.AllDnsRecords),
|
||||
AccountZones: decodeCustomZones(full.AccountZones),
|
||||
ResourcePoliciesMap: make(map[string][]*types.Policy),
|
||||
RoutersMap: make(map[string]map[string]*types.ComponentRouter),
|
||||
NetworkResources: make([]*types.ComponentResource, 0, len(full.NetworkResources)),
|
||||
RouterPeers: make(map[string]*types.ComponentPeer),
|
||||
ResourcePoliciesMap: make(map[string][]*nmdata.Policy),
|
||||
RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter),
|
||||
NetworkResources: make([]*nmdata.NetworkResource, 0, len(full.NetworkResources)),
|
||||
RouterPeers: make(map[string]*nmdata.Peer),
|
||||
AllowedUserIDs: stringSliceToSet(full.AllowedUserIds),
|
||||
PostureFailedPeers: make(map[string]map[string]struct{}, len(full.PostureFailedPeers)),
|
||||
GroupIDToUserIDs: make(map[string][]string, len(full.GroupIdToUserIds)),
|
||||
}
|
||||
|
||||
if full.DnsSettings != nil {
|
||||
c.DNSSettings = &types.DNSSettings{
|
||||
c.DNSSettings = &nmdata.DNSSettings{
|
||||
DisabledManagementGroups: full.DnsSettings.DisabledManagementGroupIds,
|
||||
}
|
||||
} else {
|
||||
c.DNSSettings = &types.DNSSettings{}
|
||||
c.DNSSettings = &nmdata.DNSSettings{}
|
||||
}
|
||||
|
||||
// Phase 1: peers. The envelope's peers slice is index-addressed on the
|
||||
@@ -98,20 +99,36 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
|
||||
log.WithField("peer idx", idx).Error("unrecognized peer idx during decoding")
|
||||
}
|
||||
}
|
||||
group := &types.ComponentGroup{
|
||||
ID: groupID,
|
||||
PublicID: gc.Id,
|
||||
Peers: peerIDs,
|
||||
|
||||
fromCompactResources := func() []nmdata.Resource {
|
||||
var toret []nmdata.Resource
|
||||
|
||||
for _, r := range gc.Resources {
|
||||
res := resourceFromProto(r, peerIDByIndex)
|
||||
if res == (nmdata.Resource{}) {
|
||||
log.WithContext(ctx).Warnf("skipping invalid resource in group compact: %s", r.String())
|
||||
continue
|
||||
}
|
||||
toret = append(toret, res)
|
||||
}
|
||||
|
||||
return toret
|
||||
}
|
||||
|
||||
group := &nmdata.Group{
|
||||
PublicID: gc.Id,
|
||||
Peers: peerIDs,
|
||||
Resources: fromCompactResources(),
|
||||
}
|
||||
if gc.IsAll {
|
||||
group.Name = types.GroupAllName
|
||||
group.Name = nmdata.GroupAllName
|
||||
}
|
||||
c.Groups[groupID] = group
|
||||
}
|
||||
|
||||
// Phase 3: policies (PolicyCompact = one rule per entry; current data
|
||||
// model is 1 rule per policy).
|
||||
policyByID := make(map[string]*types.Policy, len(full.Policies))
|
||||
policyByID := make(map[string]*nmdata.Policy, len(full.Policies))
|
||||
for i, pc := range full.Policies {
|
||||
if pc == nil {
|
||||
return nil, fmt.Errorf("invalid envelope: policies[%d] is nil", i)
|
||||
@@ -148,7 +165,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
|
||||
// Phase 7: routers_map (outer key = network seq id, inner key = peer-id
|
||||
// reconstructed from peer_index). Synthesized network id is "net_<seq>".
|
||||
for networkID, list := range full.RoutersMap {
|
||||
inner := make(map[string]*types.ComponentRouter, len(list.Entries))
|
||||
inner := make(map[string]*nmdata.NetworkRouter, len(list.Entries))
|
||||
for _, entry := range list.Entries {
|
||||
if !entry.PeerIndexSet {
|
||||
continue
|
||||
@@ -158,10 +175,8 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
|
||||
continue
|
||||
}
|
||||
peerID := peerIDByIndex[entry.PeerIndex]
|
||||
inner[peerID] = &types.ComponentRouter{
|
||||
NetworkID: networkID,
|
||||
inner[peerID] = &nmdata.NetworkRouter{
|
||||
PublicID: entry.Id,
|
||||
Peer: peerID,
|
||||
PeerGroups: entry.PeerGroupIds,
|
||||
Masquerade: entry.Masquerade,
|
||||
Metric: int(entry.Metric),
|
||||
@@ -180,7 +195,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
|
||||
if len(ids.Ids) == 0 {
|
||||
continue
|
||||
}
|
||||
policies := make([]*types.Policy, 0, len(ids.Ids))
|
||||
policies := make([]*nmdata.Policy, 0, len(ids.Ids))
|
||||
for _, id := range ids.Ids {
|
||||
if p, ok := policyByID[id]; ok {
|
||||
policies = append(policies, p)
|
||||
@@ -193,6 +208,15 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 8: rebuild resource_policies_map
|
||||
for _, r := range c.NetworkResources {
|
||||
policies := policiesForNetworkResource(r.ID, c.Policies, c.Groups)
|
||||
if len(policies) == 0 {
|
||||
continue
|
||||
}
|
||||
c.ResourcePoliciesMap[r.ID] = policies
|
||||
}
|
||||
|
||||
// Phase 9: group_id_to_user_ids — wire keys are seq ids, synth to strings.
|
||||
for groupId, list := range full.GroupIdToUserIds {
|
||||
c.GroupIDToUserIDs[groupId] = append([]string(nil), list.UserIds...)
|
||||
@@ -228,17 +252,54 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func networkResourceGroups(resourceId string, groups map[string]*nmdata.Group) []string {
|
||||
var toret []string
|
||||
for _, group := range groups {
|
||||
for _, resource := range group.Resources {
|
||||
if resource.ID == resourceId {
|
||||
toret = append(toret, group.PublicID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return toret
|
||||
}
|
||||
|
||||
func policiesForNetworkResource(resourceId string, allPolicies []*nmdata.Policy, groups map[string]*nmdata.Group) []*nmdata.Policy {
|
||||
var toret []*nmdata.Policy
|
||||
|
||||
networkResourceGroups := networkResourceGroups(resourceId, groups)
|
||||
for _, p := range allPolicies {
|
||||
if p == nil || !p.Enabled || len(p.Rules) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// there's always only one rule in each policy
|
||||
if p.Rules[0].DestinationResource.ID == resourceId {
|
||||
toret = append(toret, p)
|
||||
continue
|
||||
}
|
||||
for _, groupId := range networkResourceGroups {
|
||||
if slices.Contains(p.Rules[0].Destinations, groupId) {
|
||||
toret = append(toret, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toret
|
||||
}
|
||||
|
||||
// decodeAccountNetwork never returns nil — Calculate() dereferences
|
||||
// c.Network unconditionally, and servers that predate the fix omit the field
|
||||
// entirely from the empty-components envelope.
|
||||
func decodeAccountNetwork(an *proto.AccountNetwork) *types.Network {
|
||||
n := &types.Network{}
|
||||
func decodeAccountNetwork(an *proto.AccountNetwork) *nmdata.Network {
|
||||
n := &nmdata.Network{}
|
||||
if an == nil {
|
||||
return n
|
||||
}
|
||||
n.Identifier = an.Identifier
|
||||
n.Dns = an.Dns
|
||||
n.Serial = an.Serial
|
||||
n.Serial = int64(an.Serial)
|
||||
if an.NetCidr != "" {
|
||||
if _, ipnet, err := net.ParseCIDR(an.NetCidr); err == nil && ipnet != nil {
|
||||
n.Net = *ipnet
|
||||
@@ -252,32 +313,51 @@ func decodeAccountNetwork(an *proto.AccountNetwork) *types.Network {
|
||||
return n
|
||||
}
|
||||
|
||||
func decodeAccountSettings(as *proto.AccountSettingsCompact) *types.AccountSettingsInfo {
|
||||
func decodeAccountSettings(as *proto.AccountSettingsCompact) *nmdata.AccountSettingsInfo {
|
||||
if as == nil {
|
||||
return &types.AccountSettingsInfo{}
|
||||
return &nmdata.AccountSettingsInfo{}
|
||||
}
|
||||
return &types.AccountSettingsInfo{
|
||||
return &nmdata.AccountSettingsInfo{
|
||||
PeerLoginExpirationEnabled: as.PeerLoginExpirationEnabled,
|
||||
PeerLoginExpiration: time.Duration(as.PeerLoginExpirationNs),
|
||||
}
|
||||
}
|
||||
|
||||
func decodePeerCompact(pc *proto.PeerCompact, peerID string) *types.ComponentPeer {
|
||||
peer := &types.ComponentPeer{
|
||||
func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nmdata.Peer {
|
||||
var caps []int32
|
||||
if pc.SupportsSourcePrefixes {
|
||||
caps = append(caps, nmdata.PeerCapabilitySourcePrefixes)
|
||||
}
|
||||
if pc.SupportsIpv6 {
|
||||
caps = append(caps, nmdata.PeerCapabilityIPv6Overlay)
|
||||
}
|
||||
peer := &nmdata.Peer{
|
||||
ID: peerID,
|
||||
Key: peerID,
|
||||
SSHKey: string(pc.SshPubKey),
|
||||
SSHEnabled: pc.SshEnabled,
|
||||
DNSLabel: pc.DnsLabel,
|
||||
LoginExpirationEnabled: pc.LoginExpirationEnabled,
|
||||
AgentVersion: pc.AgentVersion,
|
||||
SupportsSourcePrefixes: pc.SupportsSourcePrefixes,
|
||||
SupportsIPv6: pc.SupportsIpv6,
|
||||
ServerSSHAllowed: pc.ServerSshAllowed,
|
||||
AddedWithSSOLogin: pc.AddedWithSsoLogin,
|
||||
ProxyMeta: nmdata.ProxyMeta{Embedded: pc.ProxyEmbedded},
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
WtVersion: pc.AgentVersion,
|
||||
Capabilities: caps,
|
||||
Flags: nmdata.Flags{
|
||||
ServerSSHAllowed: pc.ServerSshAllowed,
|
||||
},
|
||||
},
|
||||
}
|
||||
if pc.AddedWithSsoLogin {
|
||||
// Set a non-empty UserID so (*Peer).AddedWithSSOLogin() returns true.
|
||||
// The original UserID isn't on the wire; the value is intentionally
|
||||
// visibly synthetic so any future consumer that mistakes UserID for a
|
||||
// real account user xid won't silently match (or worse, write the
|
||||
// sentinel into a downstream record).
|
||||
peer.UserID = "<env-sso>"
|
||||
}
|
||||
if pc.LastLoginUnixNano != 0 {
|
||||
peer.LastLogin = time.Unix(0, pc.LastLoginUnixNano)
|
||||
t := time.Unix(0, pc.LastLoginUnixNano)
|
||||
peer.LastLogin = &t
|
||||
}
|
||||
switch len(pc.Ip) {
|
||||
case 4:
|
||||
@@ -295,13 +375,13 @@ func decodePeerCompact(pc *proto.PeerCompact, peerID string) *types.ComponentPee
|
||||
return peer
|
||||
}
|
||||
|
||||
func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex []string) *types.Policy {
|
||||
rule := &types.PolicyRule{
|
||||
func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex []string) *nmdata.Policy {
|
||||
rule := &nmdata.PolicyRule{
|
||||
ID: policyID, // 1 rule per policy → reuse synthesized id
|
||||
PolicyID: policyID,
|
||||
Enabled: true,
|
||||
Action: actionFromProto(pc.Action),
|
||||
Protocol: protocolFromProto(pc.Protocol),
|
||||
Action: string(actionFromProto(pc.Action)),
|
||||
Protocol: string(protocolFromProto(pc.Protocol)),
|
||||
Bidirectional: pc.Bidirectional,
|
||||
Ports: uint32SliceToStrings(pc.Ports),
|
||||
PortRanges: portRangesFromProto(pc.PortRanges),
|
||||
@@ -314,11 +394,11 @@ func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex
|
||||
SourceResource: resourceFromProto(pc.SourceResource, peerIDByIndex),
|
||||
DestinationResource: resourceFromProto(pc.DestinationResource, peerIDByIndex),
|
||||
}
|
||||
return &types.Policy{
|
||||
return &nmdata.Policy{
|
||||
ID: policyID,
|
||||
PublicID: pc.Id,
|
||||
Enabled: true,
|
||||
Rules: []*types.PolicyRule{rule},
|
||||
Rules: []*nmdata.PolicyRule{rule},
|
||||
SourcePostureChecks: pc.SourcePostureCheckIds,
|
||||
}
|
||||
}
|
||||
@@ -326,15 +406,19 @@ func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex
|
||||
// resourceFromProto rebuilds types.Resource. For peer-typed resources the
|
||||
// peer reference is reconstructed from the envelope's peer index — wire
|
||||
// format ships no xid for peers, so we use the synthesized peer id.
|
||||
func resourceFromProto(r *proto.ResourceCompact, peerIDByIndex []string) types.Resource {
|
||||
if r == nil {
|
||||
return types.Resource{}
|
||||
func resourceFromProto(r *proto.ResourceCompact, peerIDByIndex []string) nmdata.Resource {
|
||||
if r == nil || !types.ResourceType(r.Type).Valid() {
|
||||
return nmdata.Resource{}
|
||||
}
|
||||
out := types.Resource{Type: types.ResourceType(r.Type)}
|
||||
if r.PeerIndexSet && int(r.PeerIndex) < len(peerIDByIndex) {
|
||||
out.ID = peerIDByIndex[r.PeerIndex]
|
||||
|
||||
if r.Type == string(types.ResourceTypePeer) {
|
||||
if !r.PeerIndexSet || int(r.PeerIndex) >= len(peerIDByIndex) {
|
||||
return nmdata.Resource{}
|
||||
}
|
||||
return nmdata.Resource{Type: r.Type, ID: peerIDByIndex[int(r.PeerIndex)]}
|
||||
}
|
||||
return out
|
||||
|
||||
return nmdata.Resource{Type: r.Type, ID: r.Id}
|
||||
}
|
||||
|
||||
// authorizedGroupsFromProto inverts encodeAuthorizedGroups: the wire form
|
||||
@@ -355,15 +439,15 @@ func authorizedGroupsFromProto(m map[string]*proto.UserNameList) map[string][]st
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nbroute.Route {
|
||||
r := &nbroute.Route{
|
||||
ID: nbroute.ID(rr.Id),
|
||||
func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nmdata.Route {
|
||||
r := &nmdata.Route{
|
||||
ID: rr.Id,
|
||||
PublicID: rr.Id,
|
||||
NetID: nbroute.NetID(rr.NetId),
|
||||
NetID: rr.NetId,
|
||||
Description: rr.Description,
|
||||
Domains: domainsFromPunycode(rr.Domains),
|
||||
KeepRoute: rr.KeepRoute,
|
||||
NetworkType: nbroute.NetworkType(rr.NetworkType),
|
||||
NetworkType: int(rr.NetworkType),
|
||||
Masquerade: rr.Masquerade,
|
||||
Metric: int(rr.Metric),
|
||||
Enabled: rr.Enabled,
|
||||
@@ -383,8 +467,8 @@ func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nbroute.Route {
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGroup {
|
||||
out := &nbdns.NameServerGroup{
|
||||
func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nmdata.NameServerGroup {
|
||||
out := &nmdata.NameServerGroup{
|
||||
ID: nsg.Id,
|
||||
PublicID: nsg.Id,
|
||||
Groups: nsg.GroupIds,
|
||||
@@ -392,13 +476,13 @@ func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGr
|
||||
Domains: nsg.Domains,
|
||||
Enabled: nsg.Enabled,
|
||||
SearchDomainsEnabled: nsg.SearchDomainsEnabled,
|
||||
NameServers: make([]nbdns.NameServer, 0, len(nsg.Nameservers)),
|
||||
NameServers: make([]nmdata.NameServer, 0, len(nsg.Nameservers)),
|
||||
}
|
||||
for _, ns := range nsg.Nameservers {
|
||||
if addr, err := netip.ParseAddr(ns.IP); err == nil {
|
||||
out.NameServers = append(out.NameServers, nbdns.NameServer{
|
||||
out.NameServers = append(out.NameServers, nmdata.NameServer{
|
||||
IP: addr,
|
||||
NSType: nbdns.NameServerType(ns.NSType),
|
||||
NSType: int(ns.NSType),
|
||||
Port: int(ns.Port),
|
||||
})
|
||||
}
|
||||
@@ -406,14 +490,14 @@ func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGr
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeNetworkResource(nr *proto.NetworkResourceRaw) *types.ComponentResource {
|
||||
out := &types.ComponentResource{
|
||||
func decodeNetworkResource(nr *proto.NetworkResourceRaw) *nmdata.NetworkResource {
|
||||
out := &nmdata.NetworkResource{
|
||||
ID: nr.Id,
|
||||
PublicID: nr.Id,
|
||||
NetworkID: nr.NetworkSeq,
|
||||
Name: nr.Name,
|
||||
Description: nr.Description,
|
||||
Type: types.ComponentResourceType(nr.Type),
|
||||
Type: nr.Type,
|
||||
Address: nr.Address,
|
||||
Domain: nr.DomainValue,
|
||||
Enabled: nr.Enabled,
|
||||
@@ -426,10 +510,10 @@ func decodeNetworkResource(nr *proto.NetworkResourceRaw) *types.ComponentResourc
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeSimpleRecords(records []*proto.SimpleRecord) []nbdns.SimpleRecord {
|
||||
out := make([]nbdns.SimpleRecord, 0, len(records))
|
||||
func decodeSimpleRecords(records []*proto.SimpleRecord) []nmdata.SimpleRecord {
|
||||
out := make([]nmdata.SimpleRecord, 0, len(records))
|
||||
for _, r := range records {
|
||||
out = append(out, nbdns.SimpleRecord{
|
||||
out = append(out, nmdata.SimpleRecord{
|
||||
Name: r.Name,
|
||||
Type: int(r.Type),
|
||||
Class: r.Class,
|
||||
@@ -440,10 +524,10 @@ func decodeSimpleRecords(records []*proto.SimpleRecord) []nbdns.SimpleRecord {
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeCustomZones(zones []*proto.CustomZone) []nbdns.CustomZone {
|
||||
out := make([]nbdns.CustomZone, 0, len(zones))
|
||||
func decodeCustomZones(zones []*proto.CustomZone) []nmdata.CustomZone {
|
||||
out := make([]nmdata.CustomZone, 0, len(zones))
|
||||
for _, z := range zones {
|
||||
out = append(out, nbdns.CustomZone{
|
||||
out = append(out, nmdata.CustomZone{
|
||||
Domain: z.Domain,
|
||||
Records: decodeSimpleRecords(z.Records),
|
||||
SearchDomainDisabled: z.SearchDomainDisabled,
|
||||
@@ -464,16 +548,16 @@ func uint32SliceToStrings(ports []uint32) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func portRangesFromProto(ranges []*proto.PortInfo_Range) []types.RulePortRange {
|
||||
func portRangesFromProto(ranges []*proto.PortInfo_Range) []nmdata.RulePortRange {
|
||||
if len(ranges) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]types.RulePortRange, 0, len(ranges))
|
||||
out := make([]nmdata.RulePortRange, 0, len(ranges))
|
||||
for _, r := range ranges {
|
||||
if r == nil || r.Start > 65535 || r.End > 65535 {
|
||||
continue
|
||||
}
|
||||
out = append(out, types.RulePortRange{
|
||||
out = append(out, nmdata.RulePortRange{
|
||||
Start: uint16(r.Start),
|
||||
End: uint16(r.End),
|
||||
})
|
||||
|
||||
61
shared/management/networkmap/decode_test.go
Normal file
61
shared/management/networkmap/decode_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package networkmap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
protobuf "google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
func TestDecodePolicy(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
nmdata.Resource{Type: "peer", ID: "valid-id"},
|
||||
resourceFromProto(
|
||||
&proto.ResourceCompact{Type: "peer", PeerIndexSet: true, PeerIndex: uint32(1)},
|
||||
[]string{"invalid-id-0", "valid-id", "invalid-id-2"}))
|
||||
// check invalid peer index returns an empty resource
|
||||
assert.Equal(t,
|
||||
nmdata.Resource{},
|
||||
resourceFromProto(
|
||||
&proto.ResourceCompact{Type: "peer", PeerIndexSet: true, PeerIndex: uint32(100)},
|
||||
[]string{"invalid-id-0", "valid-id", "invalid-id-2"}))
|
||||
assert.Equal(t,
|
||||
nmdata.Resource{Type: "domain", ID: "domain"},
|
||||
resourceFromProto(
|
||||
&proto.ResourceCompact{Type: "domain", Id: "domain"}, []string{}))
|
||||
assert.Equal(t,
|
||||
nmdata.Resource{Type: "host", ID: "host"},
|
||||
resourceFromProto(
|
||||
&proto.ResourceCompact{Type: "host", Id: "host"}, []string{}))
|
||||
assert.Equal(t,
|
||||
nmdata.Resource{Type: "subnet", ID: "subnet"},
|
||||
resourceFromProto(
|
||||
&proto.ResourceCompact{Type: "subnet", Id: "subnet"}, []string{}))
|
||||
// an unknown resource type return an empty resource
|
||||
assert.Equal(t,
|
||||
nmdata.Resource{},
|
||||
resourceFromProto(
|
||||
&proto.ResourceCompact{Type: "boom", Id: "boom"}, []string{}))
|
||||
}
|
||||
|
||||
// ResourceCompact fields 1-3 are the v0.77 wire contract. Retyping any of them
|
||||
// makes peers on either side of the change silently drop policy resources, so
|
||||
// the encoding is pinned here as raw bytes: field 1 "peer" (bytes), field 2
|
||||
// true (varint), field 3 7 (varint).
|
||||
func TestResourceCompactLegacyWireFormat(t *testing.T) {
|
||||
legacy := []byte{0x0a, 0x04, 'p', 'e', 'e', 'r', 0x10, 0x01, 0x18, 0x07}
|
||||
|
||||
var decoded proto.ResourceCompact
|
||||
require.NoError(t, protobuf.Unmarshal(legacy, &decoded))
|
||||
assert.Equal(t, "peer", decoded.Type)
|
||||
assert.True(t, decoded.PeerIndexSet)
|
||||
assert.Equal(t, uint32(7), decoded.PeerIndex)
|
||||
|
||||
encoded, err := protobuf.Marshal(&proto.ResourceCompact{Type: "peer", PeerIndexSet: true, PeerIndex: 7})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, legacy, encoded)
|
||||
}
|
||||
@@ -18,10 +18,11 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
goproto "google.golang.org/protobuf/proto"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"net/netip"
|
||||
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/shared/management/types"
|
||||
"github.com/netbirdio/netbird/shared/netiputil"
|
||||
@@ -29,7 +30,7 @@ import (
|
||||
)
|
||||
|
||||
// ToProtocolRoutes converts a slice of typed routes to their proto form.
|
||||
func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
|
||||
func ToProtocolRoutes(routes []*nmdata.Route) []*proto.Route {
|
||||
protoRoutes := make([]*proto.Route, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
protoRoutes = append(protoRoutes, ToProtocolRoute(r))
|
||||
@@ -38,7 +39,7 @@ func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
|
||||
}
|
||||
|
||||
// ToProtocolRoute converts one typed route to its proto form.
|
||||
func ToProtocolRoute(route *nbroute.Route) *proto.Route {
|
||||
func ToProtocolRoute(route *nmdata.Route) *proto.Route {
|
||||
return &proto.Route{
|
||||
ID: string(route.ID),
|
||||
NetID: string(route.NetID),
|
||||
@@ -275,8 +276,9 @@ func ToProtocolDNSConfig(update nbdns.Config, cache DNSConfigCache, forwardPort
|
||||
}
|
||||
|
||||
// AppendRemotePeerConfig appends typed peers as proto.RemotePeerConfig
|
||||
// entries to dst and returns the result.
|
||||
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.ComponentPeer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig {
|
||||
// entries to dst and returns the result. localIsProxy reports whether the peer
|
||||
// receiving this config is itself an embedded proxy.
|
||||
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nmdata.Peer, dnsName string, includeIPv6 bool, localIsProxy bool) []*proto.RemotePeerConfig {
|
||||
for _, rPeer := range peers {
|
||||
allowedIPs := []string{rPeer.IP.String() + "/32"}
|
||||
if includeIPv6 && rPeer.IPv6.IsValid() {
|
||||
@@ -287,7 +289,8 @@ func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.Compon
|
||||
AllowedIps: allowedIPs,
|
||||
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
||||
Fqdn: rPeer.FQDN(dnsName),
|
||||
AgentVersion: rPeer.AgentVersion,
|
||||
AgentVersion: rPeer.Meta.WtVersion,
|
||||
LazyState: lazyStateFor(localIsProxy, rPeer),
|
||||
})
|
||||
}
|
||||
return dst
|
||||
@@ -328,6 +331,18 @@ func BuildSessionPubKeysProto(ctx context.Context, in []types.VNCSessionPubKey)
|
||||
return out
|
||||
}
|
||||
|
||||
// lazyStateFor returns the per-peer lazy override for a remote peer. Connections
|
||||
// involving an ephemeral proxy peer on either endpoint default to lazy so shared
|
||||
// proxy infrastructure is not kept permanently connected to every peer. All
|
||||
// other peers follow the account-wide flag. A future admin-facing per-peer
|
||||
// setting can return LazyStateEager here to force a peer always-active.
|
||||
func lazyStateFor(localIsProxy bool, rPeer *nmdata.Peer) proto.LazyState {
|
||||
if localIsProxy || rPeer.ProxyMeta.Embedded {
|
||||
return proto.LazyState_LazyStateLazy
|
||||
}
|
||||
return proto.LazyState_LazyStateDefault
|
||||
}
|
||||
|
||||
// BuildAuthorizedUsersProto deduplicates user-IDs into a hashed list and
|
||||
// builds per-machine-user index maps. Returns (hashedUsers, machineUsers).
|
||||
// Errors from individual hash failures are logged via the provided context;
|
||||
|
||||
@@ -36,7 +36,7 @@ type EnvelopeResult struct {
|
||||
// dnsName is the account's DNS domain ("netbird.cloud" etc.); used when
|
||||
// rebuilding the per-peer FQDNs that proto.RemotePeerConfig carries.
|
||||
func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, localPeerKey, dnsName string) (*EnvelopeResult, error) {
|
||||
components, err := DecodeEnvelope(env)
|
||||
components, err := DecodeEnvelope(ctx, env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode envelope: %w", err)
|
||||
}
|
||||
@@ -54,8 +54,8 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo
|
||||
}
|
||||
components.PeerID = canonicalKey
|
||||
|
||||
includeIPv6 := localPeer.SupportsIPv6 && localPeer.IPv6.IsValid()
|
||||
useSourcePrefixes := localPeer.SupportsSourcePrefixes
|
||||
includeIPv6 := localPeer.SupportsIPv6() && localPeer.IPv6.IsValid()
|
||||
useSourcePrefixes := localPeer.SupportsSourcePrefixes()
|
||||
|
||||
typedNM := components.Calculate(ctx)
|
||||
|
||||
@@ -74,11 +74,11 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo
|
||||
protoNM.Routes = ToProtocolRoutes(typedNM.Routes)
|
||||
protoNM.DNSConfig = ToProtocolDNSConfig(typedNM.DNSConfig, nil, dnsFwdPort)
|
||||
|
||||
remotePeers := AppendRemotePeerConfig(nil, typedNM.Peers, dnsName, includeIPv6)
|
||||
remotePeers := AppendRemotePeerConfig(nil, typedNM.Peers, dnsName, includeIPv6, localPeer.ProxyMeta.Embedded)
|
||||
protoNM.RemotePeers = remotePeers
|
||||
protoNM.RemotePeersIsEmpty = len(remotePeers) == 0
|
||||
|
||||
protoNM.OfflinePeers = AppendRemotePeerConfig(nil, typedNM.OfflinePeers, dnsName, includeIPv6)
|
||||
protoNM.OfflinePeers = AppendRemotePeerConfig(nil, typedNM.OfflinePeers, dnsName, includeIPv6, localPeer.ProxyMeta.Embedded)
|
||||
|
||||
firewallRules := ToProtocolFirewallRules(typedNM.FirewallRules, includeIPv6, useSourcePrefixes)
|
||||
protoNM.FirewallRules = firewallRules
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
nbnetworkmap "github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
@@ -55,13 +56,13 @@ func TestEnvelopeToNetworkMap_RoundTrip(t *testing.T) {
|
||||
func TestCalculate_FirewallRuleProtocol_NeverNetbirdSSH(t *testing.T) {
|
||||
c, localPeerKey := buildSmokeComponents(t)
|
||||
// Replace the smoke policy with a NetbirdSSH-protocol allow.
|
||||
c.Policies = []*types.Policy{{
|
||||
c.Policies = []*nmdata.Policy{{
|
||||
ID: "pol-ssh", PublicID: "2", Enabled: true,
|
||||
Rules: []*types.PolicyRule{{
|
||||
Rules: []*nmdata.PolicyRule{{
|
||||
ID: "rule-ssh",
|
||||
Enabled: true,
|
||||
Action: types.PolicyTrafficActionAccept,
|
||||
Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||
Action: string(types.PolicyTrafficActionAccept),
|
||||
Protocol: string(types.PolicyRuleProtocolNetbirdSSH),
|
||||
Bidirectional: true,
|
||||
Sources: []string{"group-all"},
|
||||
Destinations: []string{"group-all"},
|
||||
@@ -94,13 +95,13 @@ func TestCalculate_FirewallRuleProtocol_NeverNetbirdSSH(t *testing.T) {
|
||||
func TestEnvelopeToNetworkMap_VNCPolicyProducesVncAuth(t *testing.T) {
|
||||
c, localPeerKey := buildSmokeComponents(t)
|
||||
c.GroupIDToUserIDs = map[string][]string{"1": {"user-1"}}
|
||||
c.Policies = []*types.Policy{{
|
||||
c.Policies = []*nmdata.Policy{{
|
||||
ID: "pol-vnc", PublicID: "2", Enabled: true,
|
||||
Rules: []*types.PolicyRule{{
|
||||
Rules: []*nmdata.PolicyRule{{
|
||||
ID: "rule-vnc",
|
||||
Enabled: true,
|
||||
Action: types.PolicyTrafficActionAccept,
|
||||
Protocol: types.PolicyRuleProtocolNetbirdVNC,
|
||||
Action: string(types.PolicyTrafficActionAccept),
|
||||
Protocol: string(types.PolicyRuleProtocolNetbirdVNC),
|
||||
Bidirectional: true,
|
||||
Sources: []string{"group-all"},
|
||||
Destinations: []string{"group-all"},
|
||||
@@ -214,39 +215,39 @@ func TestDecodeEnvelope_MalformedWgKeyPeerSkipped(t *testing.T) {
|
||||
func TestEnvelopeRoundTrip_AllGroupShortCircuitParity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
peers := map[string]*types.ComponentPeer{}
|
||||
peers := map[string]*nmdata.Peer{}
|
||||
for i, id := range []string{"peer-T", "peer-S", "peer-ALL", "peer-O"} {
|
||||
peers[id] = &types.ComponentPeer{
|
||||
ID: id,
|
||||
Key: randomWgKey(t),
|
||||
IP: netip.AddrFrom4([4]byte{100, 64, 0, byte(i + 1)}),
|
||||
DNSLabel: id,
|
||||
AgentVersion: "0.40.0",
|
||||
peers[id] = &nmdata.Peer{
|
||||
ID: id,
|
||||
Key: randomWgKey(t),
|
||||
IP: netip.AddrFrom4([4]byte{100, 64, 0, byte(i + 1)}),
|
||||
DNSLabel: id,
|
||||
Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||
}
|
||||
}
|
||||
|
||||
c := &types.NetworkMapComponents{
|
||||
PeerID: "peer-T",
|
||||
Network: &types.Network{
|
||||
Network: &nmdata.Network{
|
||||
Identifier: "net-all-groups",
|
||||
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
|
||||
Serial: 1,
|
||||
},
|
||||
AccountSettings: &types.AccountSettingsInfo{},
|
||||
DNSSettings: &types.DNSSettings{},
|
||||
AccountSettings: &nmdata.AccountSettingsInfo{},
|
||||
DNSSettings: &nmdata.DNSSettings{},
|
||||
Peers: peers,
|
||||
Groups: map[string]*types.ComponentGroup{
|
||||
"g-src": {ID: "g-src", PublicID: "1", Name: "staff", Peers: []string{"peer-T", "peer-S"}},
|
||||
"g-all": {ID: "g-all", PublicID: "2", Name: "All", Peers: []string{"peer-ALL"}},
|
||||
"g-two": {ID: "g-two", PublicID: "3", Name: "second", Peers: []string{"peer-T", "peer-O"}},
|
||||
Groups: map[string]*nmdata.Group{
|
||||
"g-src": {PublicID: "1", Name: "staff", Peers: []string{"peer-T", "peer-S"}},
|
||||
"g-all": {PublicID: "2", Name: "All", Peers: []string{"peer-ALL"}},
|
||||
"g-two": {PublicID: "3", Name: "second", Peers: []string{"peer-T", "peer-O"}},
|
||||
},
|
||||
Policies: []*types.Policy{{
|
||||
Policies: []*nmdata.Policy{{
|
||||
ID: "pol-multi-dest", PublicID: "10", Enabled: true,
|
||||
Rules: []*types.PolicyRule{{
|
||||
Rules: []*nmdata.PolicyRule{{
|
||||
ID: "rule-multi-dest",
|
||||
Enabled: true,
|
||||
Action: types.PolicyTrafficActionAccept,
|
||||
Protocol: types.PolicyRuleProtocolALL,
|
||||
Action: string(types.PolicyTrafficActionAccept),
|
||||
Protocol: string(types.PolicyRuleProtocolALL),
|
||||
Sources: []string{"g-src"},
|
||||
Destinations: []string{"g-all", "g-two"},
|
||||
}},
|
||||
@@ -302,12 +303,12 @@ func TestEnvelopeToNetworkMap_EmptyComponents(t *testing.T) {
|
||||
localPeerKey := randomWgKey(t)
|
||||
c := types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
|
||||
PeerID: "peer-A",
|
||||
Network: &types.Network{
|
||||
Network: &nmdata.Network{
|
||||
Identifier: "net-empty",
|
||||
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
|
||||
Serial: 7,
|
||||
},
|
||||
Peers: map[string]*types.ComponentPeer{
|
||||
Peers: map[string]*nmdata.Peer{
|
||||
"peer-A": {ID: "peer-A", Key: localPeerKey, IP: netip.AddrFrom4([4]byte{100, 64, 0, 1})},
|
||||
},
|
||||
})
|
||||
@@ -362,33 +363,33 @@ func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) {
|
||||
peerAKey := randomWgKey(t)
|
||||
peerBKey := randomWgKey(t)
|
||||
|
||||
peerA := &types.ComponentPeer{
|
||||
ID: "peer-A",
|
||||
Key: peerAKey,
|
||||
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
|
||||
DNSLabel: "peerA",
|
||||
AgentVersion: "0.40.0",
|
||||
peerA := &nmdata.Peer{
|
||||
ID: "peer-A",
|
||||
Key: peerAKey,
|
||||
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
|
||||
DNSLabel: "peerA",
|
||||
Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||
}
|
||||
peerB := &types.ComponentPeer{
|
||||
ID: "peer-B",
|
||||
Key: peerBKey,
|
||||
IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}),
|
||||
DNSLabel: "peerB",
|
||||
AgentVersion: "0.40.0",
|
||||
peerB := &nmdata.Peer{
|
||||
ID: "peer-B",
|
||||
Key: peerBKey,
|
||||
IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}),
|
||||
DNSLabel: "peerB",
|
||||
Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||
}
|
||||
|
||||
group := &types.ComponentGroup{
|
||||
ID: "group-all", PublicID: "1", Name: "All",
|
||||
group := &nmdata.Group{
|
||||
PublicID: "1", Name: "All",
|
||||
Peers: []string{"peer-A", "peer-B"},
|
||||
}
|
||||
|
||||
policy := &types.Policy{
|
||||
policy := &nmdata.Policy{
|
||||
ID: "pol-allow", PublicID: "1", Enabled: true,
|
||||
Rules: []*types.PolicyRule{{
|
||||
Rules: []*nmdata.PolicyRule{{
|
||||
ID: "rule-allow",
|
||||
Enabled: true,
|
||||
Action: types.PolicyTrafficActionAccept,
|
||||
Protocol: types.PolicyRuleProtocolALL,
|
||||
Action: string(types.PolicyTrafficActionAccept),
|
||||
Protocol: string(types.PolicyRuleProtocolALL),
|
||||
Bidirectional: true,
|
||||
Sources: []string{"group-all"},
|
||||
Destinations: []string{"group-all"},
|
||||
@@ -397,21 +398,21 @@ func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) {
|
||||
|
||||
c := &types.NetworkMapComponents{
|
||||
PeerID: "peer-A",
|
||||
Network: &types.Network{
|
||||
Network: &nmdata.Network{
|
||||
Identifier: "net-smoke",
|
||||
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
|
||||
Serial: 1,
|
||||
},
|
||||
AccountSettings: &types.AccountSettingsInfo{},
|
||||
DNSSettings: &types.DNSSettings{},
|
||||
Peers: map[string]*types.ComponentPeer{
|
||||
AccountSettings: &nmdata.AccountSettingsInfo{},
|
||||
DNSSettings: &nmdata.DNSSettings{},
|
||||
Peers: map[string]*nmdata.Peer{
|
||||
"peer-A": peerA,
|
||||
"peer-B": peerB,
|
||||
},
|
||||
Groups: map[string]*types.ComponentGroup{
|
||||
Groups: map[string]*nmdata.Group{
|
||||
"group-all": group,
|
||||
},
|
||||
Policies: []*types.Policy{policy},
|
||||
Policies: []*nmdata.Policy{policy},
|
||||
}
|
||||
return c, peerAKey
|
||||
}
|
||||
|
||||
812
shared/management/networkmap/networkmapcompute.go
Normal file
812
shared/management/networkmap/networkmapcompute.go
Normal file
@@ -0,0 +1,812 @@
|
||||
package networkmap
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
type sshRequirements struct {
|
||||
neededGroupIDs map[string]struct{}
|
||||
needAllowedUserIDs bool
|
||||
}
|
||||
|
||||
// GetPeerNetworkMapComponents computes the peer's NetworkMapComponents from the
|
||||
// slim twin store. It mirrors the former Account.GetPeerNetworkMapComponents
|
||||
// exactly, operating on nmdata twins throughout — no Account reference and no
|
||||
// twin↔real conversion, since the produced components hold twins.
|
||||
func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMapComponents {
|
||||
nmd.InjectProxyPolicies()
|
||||
|
||||
forceRoutingPeerDNS := nmd.forcesRoutingPeerDNSResolution(peerID)
|
||||
|
||||
peer := nmd.Peers[peerID]
|
||||
if peer == nil {
|
||||
return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: nmd.Network,
|
||||
Peers: map[string]*nmdata.Peer{peerID: peer},
|
||||
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
|
||||
})
|
||||
}
|
||||
|
||||
if _, ok := nmd.ValidatedPeers[peerID]; !ok {
|
||||
return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: nmd.Network,
|
||||
Peers: map[string]*nmdata.Peer{peerID: peer},
|
||||
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
|
||||
})
|
||||
}
|
||||
|
||||
components := &types.NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: nmd.Network,
|
||||
AccountSettings: nmd.AccountSettings,
|
||||
DNSSettings: nmd.DNSSettings,
|
||||
CustomZoneDomain: peersCustomZone.Domain,
|
||||
NameServerGroups: make([]*nmdata.NameServerGroup, 0),
|
||||
ResourcePoliciesMap: make(map[string][]*nmdata.Policy),
|
||||
RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter),
|
||||
NetworkResources: make([]*nmdata.NetworkResource, 0),
|
||||
PostureFailedPeers: make(map[string]map[string]struct{}, len(nmd.PostureChecks)),
|
||||
RouterPeers: make(map[string]*nmdata.Peer),
|
||||
NetworkXIDToPublicID: nmd.NetworkXIDToPublicID,
|
||||
PostureCheckXIDToPublicID: nmd.PostureCheckXIDToPublicID,
|
||||
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
|
||||
}
|
||||
|
||||
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := nmd.getPeersGroupsPoliciesRoutes(peerID, peer.SSHEnabled, &components.PostureFailedPeers)
|
||||
|
||||
if len(sshReqs.neededGroupIDs) > 0 {
|
||||
components.GroupIDToUserIDs = filterGroupIDToUserIDs(nmd.GroupIDToUserIDs, sshReqs.neededGroupIDs)
|
||||
}
|
||||
if sshReqs.needAllowedUserIDs {
|
||||
components.AllowedUserIDs = nmd.getAllowedUserIDs()
|
||||
}
|
||||
|
||||
components.Peers = relevantPeers
|
||||
components.Groups = relevantGroups
|
||||
components.Policies = relevantPolicies
|
||||
components.Routes = relevantRoutes
|
||||
components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid())
|
||||
|
||||
peerGroups := nmd.GetPeerGroups(peerID)
|
||||
components.AccountZones = nmd.appliedZones(peerGroups)
|
||||
components.AccountZones = append(components.AccountZones, nmd.privateServiceZones(peerGroups)...)
|
||||
|
||||
for _, nsGroup := range nmd.NameServerGroups {
|
||||
if nsGroup != nil && nsGroup.Enabled {
|
||||
for _, gID := range nsGroup.Groups {
|
||||
if _, found := relevantGroups[gID]; found {
|
||||
components.NameServerGroups = append(components.NameServerGroups, nsGroup)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, resource := range nmd.NetworkResources {
|
||||
if resource == nil || !resource.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
policies, exists := nmd.ResourcePolicies[resource.ID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
addSourcePeers := false
|
||||
|
||||
networkRoutingPeers, routerExists := nmd.Routers[resource.NetworkID]
|
||||
if routerExists {
|
||||
if _, ok := networkRoutingPeers[peerID]; ok {
|
||||
addSourcePeers = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if policy == nil || !policy.Enabled || len(policy.Rules) == 0 || policy.Rules[0] == nil {
|
||||
continue
|
||||
}
|
||||
if addSourcePeers {
|
||||
var peers []string
|
||||
if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" {
|
||||
peers = []string{policy.Rules[0].SourceResource.ID}
|
||||
} else {
|
||||
peers = nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
|
||||
}
|
||||
for _, pID := range nmd.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, &components.PostureFailedPeers) {
|
||||
if _, exists := components.Peers[pID]; !exists {
|
||||
components.Peers[pID] = nmd.Peers[pID]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
peerInSources := false
|
||||
if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" {
|
||||
peerInSources = policy.Rules[0].SourceResource.ID == peerID
|
||||
} else {
|
||||
for _, groupID := range policy.SourceGroups() {
|
||||
if group := nmd.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) {
|
||||
peerInSources = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !peerInSources {
|
||||
continue
|
||||
}
|
||||
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(policy.SourcePostureChecks, peerID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := components.PostureFailedPeers[pname]; !ok {
|
||||
components.PostureFailedPeers[pname] = make(map[string]struct{})
|
||||
}
|
||||
components.PostureFailedPeers[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
addSourcePeers = true
|
||||
}
|
||||
|
||||
for _, rule := range policy.Rules {
|
||||
if rule == nil || !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
if g := nmd.Groups[srcGroupID]; g != nil {
|
||||
if _, exists := components.Groups[srcGroupID]; !exists {
|
||||
components.Groups[srcGroupID] = g
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
if g := nmd.Groups[dstGroupID]; g != nil {
|
||||
if _, exists := components.Groups[dstGroupID]; !exists {
|
||||
components.Groups[dstGroupID] = g
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
components.ResourcePoliciesMap[resource.ID] = policies
|
||||
}
|
||||
|
||||
if addSourcePeers {
|
||||
components.RoutersMap[resource.NetworkID] = networkRoutingPeers
|
||||
for peerIDKey := range networkRoutingPeers {
|
||||
p := nmd.Peers[peerIDKey]
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
// An unapproved peer must not carry traffic, so it is kept out of
|
||||
// RouterPeers as well: the envelope encoder indexes that map into
|
||||
// the wire peer table, from which the client restores every entry.
|
||||
if _, validated := nmd.ValidatedPeers[peerIDKey]; !validated {
|
||||
continue
|
||||
}
|
||||
if _, exists := components.RouterPeers[peerIDKey]; !exists {
|
||||
components.RouterPeers[peerIDKey] = p
|
||||
}
|
||||
if _, exists := components.Peers[peerIDKey]; !exists {
|
||||
components.Peers[peerIDKey] = p
|
||||
}
|
||||
}
|
||||
components.NetworkResources = append(components.NetworkResources, resource)
|
||||
}
|
||||
}
|
||||
|
||||
filterGroupPeers(&components.Groups, components.Peers)
|
||||
filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers)
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
|
||||
peerID string,
|
||||
peerSSHEnabled bool,
|
||||
postureFailedPeers *map[string]map[string]struct{},
|
||||
) (map[string]*nmdata.Peer, map[string]*nmdata.Group, []*nmdata.Policy, []*nmdata.Route, sshRequirements) {
|
||||
relevantPeerIDs := make(map[string]*nmdata.Peer, len(nmd.Peers)/4)
|
||||
relevantGroupIDs := make(map[string]*nmdata.Group, len(nmd.Groups)/4)
|
||||
relevantPolicies := make([]*nmdata.Policy, 0, len(nmd.Policies))
|
||||
relevantRoutes := make([]*nmdata.Route, 0, len(nmd.Routes))
|
||||
sshReqs := sshRequirements{neededGroupIDs: make(map[string]struct{})}
|
||||
|
||||
relevantPeerIDs[peerID] = nmd.Peers[peerID]
|
||||
|
||||
peerGroupSet := nmd.GetPeerGroups(peerID)
|
||||
for groupID := range peerGroupSet {
|
||||
relevantGroupIDs[groupID] = nmd.Groups[groupID]
|
||||
}
|
||||
|
||||
routeAccessControlGroups := make(map[string]struct{})
|
||||
for _, r := range nmd.Routes {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
relevant := r.Peer == peerID
|
||||
if !relevant {
|
||||
for _, groupID := range r.PeerGroups {
|
||||
if _, ok := peerGroupSet[groupID]; ok {
|
||||
relevant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !relevant && r.Enabled {
|
||||
for _, groupID := range r.Groups {
|
||||
if _, ok := peerGroupSet[groupID]; ok {
|
||||
relevant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !relevant {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, groupID := range r.PeerGroups {
|
||||
if g := nmd.Groups[groupID]; g != nil {
|
||||
relevantGroupIDs[groupID] = g
|
||||
}
|
||||
}
|
||||
for _, groupID := range r.Groups {
|
||||
if g := nmd.Groups[groupID]; g != nil {
|
||||
relevantGroupIDs[groupID] = g
|
||||
}
|
||||
}
|
||||
if r.Enabled {
|
||||
for _, groupID := range r.AccessControlGroups {
|
||||
if g := nmd.Groups[groupID]; g != nil {
|
||||
relevantGroupIDs[groupID] = g
|
||||
}
|
||||
routeAccessControlGroups[groupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Peer != "" {
|
||||
if _, ok := nmd.ValidatedPeers[r.Peer]; ok {
|
||||
if p := nmd.Peers[r.Peer]; p != nil {
|
||||
relevantPeerIDs[r.Peer] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, groupID := range r.PeerGroups {
|
||||
g := nmd.Groups[groupID]
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
for _, pid := range g.Peers {
|
||||
if _, exists := relevantPeerIDs[pid]; exists {
|
||||
continue
|
||||
}
|
||||
if _, ok := nmd.ValidatedPeers[pid]; !ok {
|
||||
continue
|
||||
}
|
||||
if p := nmd.Peers[pid]; p != nil {
|
||||
relevantPeerIDs[pid] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
relevantRoutes = append(relevantRoutes, r)
|
||||
}
|
||||
|
||||
for _, policy := range nmd.Policies {
|
||||
if policy == nil || !policy.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
policyRelevant := false
|
||||
for _, rule := range policy.Rules {
|
||||
if rule == nil || !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(routeAccessControlGroups) > 0 {
|
||||
for _, destGroupID := range rule.Destinations {
|
||||
if _, needed := routeAccessControlGroups[destGroupID]; needed {
|
||||
policyRelevant = true
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
if g := nmd.Groups[srcGroupID]; g != nil {
|
||||
relevantGroupIDs[srcGroupID] = g
|
||||
}
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
if g := nmd.Groups[dstGroupID]; g != nil {
|
||||
relevantGroupIDs[dstGroupID] = g
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sourcePeers, destinationPeers []string
|
||||
var peerInSources, peerInDestinations bool
|
||||
|
||||
if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID != "" {
|
||||
sourcePeers = []string{rule.SourceResource.ID}
|
||||
if rule.SourceResource.ID == peerID {
|
||||
peerInSources = true
|
||||
}
|
||||
} else {
|
||||
sourcePeers, peerInSources = nmd.getPeersFromGroups(rule.Sources, peerID, policy.SourcePostureChecks, postureFailedPeers)
|
||||
}
|
||||
|
||||
if rule.DestinationResource.Type == string(types.ResourceTypePeer) && rule.DestinationResource.ID != "" {
|
||||
destinationPeers = []string{rule.DestinationResource.ID}
|
||||
if rule.DestinationResource.ID == peerID {
|
||||
peerInDestinations = true
|
||||
}
|
||||
} else {
|
||||
destinationPeers, peerInDestinations = nmd.getPeersFromGroups(rule.Destinations, peerID, nil, postureFailedPeers)
|
||||
}
|
||||
|
||||
if peerInSources {
|
||||
policyRelevant = true
|
||||
for _, pid := range destinationPeers {
|
||||
relevantPeerIDs[pid] = nmd.Peers[pid]
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
if g := nmd.Groups[dstGroupID]; g != nil {
|
||||
relevantGroupIDs[dstGroupID] = g
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if peerInDestinations {
|
||||
policyRelevant = true
|
||||
for _, pid := range sourcePeers {
|
||||
relevantPeerIDs[pid] = nmd.Peers[pid]
|
||||
}
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
if g := nmd.Groups[srcGroupID]; g != nil {
|
||||
relevantGroupIDs[srcGroupID] = g
|
||||
}
|
||||
}
|
||||
|
||||
if rule.Protocol == string(types.PolicyRuleProtocolNetbirdSSH) {
|
||||
switch {
|
||||
case len(rule.AuthorizedGroups) > 0:
|
||||
for groupID := range rule.AuthorizedGroups {
|
||||
sshReqs.neededGroupIDs[groupID] = struct{}{}
|
||||
}
|
||||
case rule.AuthorizedUser != "":
|
||||
default:
|
||||
sshReqs.needAllowedUserIDs = true
|
||||
}
|
||||
} else if nmdata.PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled {
|
||||
sshReqs.needAllowedUserIDs = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if policyRelevant {
|
||||
relevantPolicies = append(relevantPolicies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string,
|
||||
postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) {
|
||||
peerInGroups := false
|
||||
filteredPeerIDs := make([]string, 0, len(groups))
|
||||
seenPeerIds := make(map[string]struct{}, len(groups))
|
||||
|
||||
for _, gid := range groups {
|
||||
group := nmd.Groups[gid]
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if group.IsGroupAll() || len(groups) == 1 {
|
||||
filteredPeerIDs = make([]string, 0, len(group.Peers))
|
||||
peerInGroups = false
|
||||
for _, pid := range group.Peers {
|
||||
peer, ok := nmd.Peers[pid]
|
||||
if !ok || peer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := nmd.ValidatedPeers[peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, peer.ID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if peer.ID == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeerIDs = append(filteredPeerIDs, peer.ID)
|
||||
}
|
||||
return filteredPeerIDs, peerInGroups
|
||||
}
|
||||
|
||||
for _, pid := range group.Peers {
|
||||
if _, seen := seenPeerIds[pid]; seen {
|
||||
continue
|
||||
}
|
||||
seenPeerIds[pid] = struct{}{}
|
||||
peer, ok := nmd.Peers[pid]
|
||||
if !ok || peer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := nmd.ValidatedPeers[peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, peer.ID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if peer.ID == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeerIDs = append(filteredPeerIDs, peer.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPeerIDs, peerInGroups
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChecksID []string, peerID string) (bool, string) {
|
||||
peer, ok := nmd.Peers[peerID]
|
||||
if !ok || peer == nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
for _, postureChecksID := range sourcePostureChecksID {
|
||||
if valid, cached := nmd.cachedPostureCheckResult(postureChecksID, peerID); cached {
|
||||
if !valid {
|
||||
return false, postureChecksID
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
postureChecks := nmd.PostureChecks[postureChecksID]
|
||||
if postureChecks == nil {
|
||||
continue
|
||||
}
|
||||
if !postureChecks.Passes(peer) {
|
||||
return false, postureChecksID
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) PrecomputePostureValidation() {
|
||||
if len(nmd.PostureChecks) == 0 {
|
||||
nmd.PostureValidation = nil
|
||||
return
|
||||
}
|
||||
|
||||
checkPeerIDs := make(map[string]map[string]struct{})
|
||||
for _, policy := range nmd.Policies {
|
||||
if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
groupPeerIDs := nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
|
||||
for _, postureChecksID := range policy.SourcePostureChecks {
|
||||
set := checkPeerIDs[postureChecksID]
|
||||
if set == nil {
|
||||
set = make(map[string]struct{}, len(groupPeerIDs))
|
||||
checkPeerIDs[postureChecksID] = set
|
||||
}
|
||||
for _, pid := range groupPeerIDs {
|
||||
set[pid] = struct{}{}
|
||||
}
|
||||
for _, rule := range policy.Rules {
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID != "" {
|
||||
set[rule.SourceResource.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := make(map[string]map[string]bool, len(checkPeerIDs))
|
||||
for postureChecksID, peerIDs := range checkPeerIDs {
|
||||
results[postureChecksID] = nmd.evaluatePostureChecksForPeers(postureChecksID, peerIDs)
|
||||
}
|
||||
nmd.PostureValidation = results
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) evaluatePostureChecksForPeers(postureChecksID string, peerIDs map[string]struct{}) map[string]bool {
|
||||
postureChecks := nmd.PostureChecks[postureChecksID]
|
||||
if postureChecks == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
checks := postureChecks.GetChecks()
|
||||
results := make(map[string]bool, len(peerIDs))
|
||||
for peerID := range peerIDs {
|
||||
peer := nmd.Peers[peerID]
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
results[peerID] = nmdata.PassesChecks(checks, peer)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) cachedPostureCheckResult(postureChecksID, peerID string) (bool, bool) {
|
||||
results, ok := nmd.PostureValidation[postureChecksID]
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
if results == nil {
|
||||
return true, true
|
||||
}
|
||||
valid, found := results[peerID]
|
||||
return valid, found
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) getPostureValidPeersSaveFailed(inputPeers []string, postureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) []string {
|
||||
var dest []string
|
||||
for _, peerID := range inputPeers {
|
||||
if _, validated := nmd.ValidatedPeers[peerID]; !validated {
|
||||
continue
|
||||
}
|
||||
valid, pname := nmd.validatePostureChecksOnPeerGetFailed(postureChecksIDs, peerID)
|
||||
if valid {
|
||||
dest = append(dest, peerID)
|
||||
continue
|
||||
}
|
||||
if pname == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peerID] = struct{}{}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// forcesRoutingPeerDNSResolution reports whether the given peer must run
|
||||
// routing-peer DNS resolution regardless of the account-global
|
||||
// RoutingPeerDNSResolutionEnabled setting: true when the peer routes a domain
|
||||
// network resource targeted by an enabled reverse-proxy service, so the peer's
|
||||
// DNS forwarder starts and can resolve the target for the embedded proxy peers.
|
||||
func (nmd *NetworkMapData) forcesRoutingPeerDNSResolution(peerID string) bool {
|
||||
if len(nmd.ProxyTargetedDomainResourceIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, resource := range nmd.NetworkResources {
|
||||
if resource == nil || !resource.Enabled || resource.Type != string(types.ResourceTypeDomain) {
|
||||
continue
|
||||
}
|
||||
if _, ok := nmd.ProxyTargetedDomainResourceIDs[resource.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, isRouter := nmd.Routers[resource.NetworkID][peerID]; isRouter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetPeerGroups returns the set of group IDs the peer belongs to. The
|
||||
// underlying peer→groups index is built once per NetworkMapData and the
|
||||
// returned set is shared — callers must not mutate it.
|
||||
func (nmd *NetworkMapData) GetPeerGroups(peerID string) map[string]struct{} {
|
||||
nmd.peerGroupsOnce.Do(func() {
|
||||
idx := make(map[string]map[string]struct{}, len(nmd.Peers))
|
||||
for groupID, group := range nmd.Groups {
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
for _, pid := range group.Peers {
|
||||
set, ok := idx[pid]
|
||||
if !ok {
|
||||
set = make(map[string]struct{})
|
||||
idx[pid] = set
|
||||
}
|
||||
set[groupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
nmd.peerGroupsIdx = idx
|
||||
})
|
||||
|
||||
if set, ok := nmd.peerGroupsIdx[peerID]; ok {
|
||||
return set
|
||||
}
|
||||
return map[string]struct{}{}
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) getUniquePeerIDsFromGroupsIDs(groups []string) []string {
|
||||
peerIDs := make(map[string]struct{}, len(groups))
|
||||
for _, groupID := range groups {
|
||||
group := nmd.Groups[groupID]
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if group.IsGroupAll() || len(groups) == 1 {
|
||||
return group.Peers
|
||||
}
|
||||
|
||||
for _, peerID := range group.Peers {
|
||||
peerIDs[peerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(peerIDs))
|
||||
for peerID := range peerIDs {
|
||||
ids = append(ids, peerID)
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) getAllowedUserIDs() map[string]struct{} {
|
||||
return nmd.AllowedUserIDs
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) appliedZones(peerGroups map[string]struct{}) []nmdata.CustomZone {
|
||||
if len(peerGroups) == 0 {
|
||||
return nil
|
||||
}
|
||||
var out []nmdata.CustomZone
|
||||
for _, cand := range nmd.AppliedZoneCandidates {
|
||||
if peerInDistributionGroups(peerGroups, cand.DistributionGroups) {
|
||||
out = append(out, cand.Zone)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) privateServiceZones(peerGroups map[string]struct{}) []nmdata.CustomZone {
|
||||
byApex := make(map[string]*nmdata.CustomZone)
|
||||
var order []string
|
||||
for _, cand := range nmd.PrivateServiceCandidates {
|
||||
if !peerInDistributionGroups(peerGroups, cand.AccessGroups) {
|
||||
continue
|
||||
}
|
||||
zone, exists := byApex[cand.Zone.Domain]
|
||||
if !exists {
|
||||
nz := nmdata.CustomZone{
|
||||
Domain: cand.Zone.Domain,
|
||||
SearchDomainDisabled: cand.Zone.SearchDomainDisabled,
|
||||
NonAuthoritative: cand.Zone.NonAuthoritative,
|
||||
}
|
||||
byApex[cand.Zone.Domain] = &nz
|
||||
zone = &nz
|
||||
order = append(order, cand.Zone.Domain)
|
||||
}
|
||||
zone.Records = append(zone.Records, cand.Zone.Records...)
|
||||
}
|
||||
|
||||
var out []nmdata.CustomZone
|
||||
for _, apex := range order {
|
||||
zone := byApex[apex]
|
||||
if len(zone.Records) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, *zone)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func peerInDistributionGroups(peerGroups map[string]struct{}, groups []string) bool {
|
||||
for _, g := range groups {
|
||||
if _, ok := peerGroups[g]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func filterGroupPeers(groups *map[string]*nmdata.Group, peers map[string]*nmdata.Peer) {
|
||||
for groupID, groupInfo := range *groups {
|
||||
filteredPeers := make([]string, 0, len(groupInfo.Peers))
|
||||
for _, pid := range groupInfo.Peers {
|
||||
if _, exists := peers[pid]; exists {
|
||||
filteredPeers = append(filteredPeers, pid)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredPeers) != len(groupInfo.Peers) {
|
||||
ng := groupInfo.Copy()
|
||||
ng.Peers = filteredPeers
|
||||
(*groups)[groupID] = ng
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*nmdata.Policy, resourcePoliciesMap map[string][]*nmdata.Policy, peers map[string]*nmdata.Peer) {
|
||||
if len(*postureFailedPeers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
referencedPostureChecks := make(map[string]struct{})
|
||||
for _, policy := range policies {
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
referencedPostureChecks[checkID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, resPolicies := range resourcePoliciesMap {
|
||||
for _, policy := range resPolicies {
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
referencedPostureChecks[checkID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for checkID, failedPeers := range *postureFailedPeers {
|
||||
if _, referenced := referencedPostureChecks[checkID]; !referenced {
|
||||
delete(*postureFailedPeers, checkID)
|
||||
continue
|
||||
}
|
||||
for peerID := range failedPeers {
|
||||
if _, exists := peers[peerID]; !exists {
|
||||
delete(failedPeers, peerID)
|
||||
}
|
||||
}
|
||||
if len(failedPeers) == 0 {
|
||||
delete(*postureFailedPeers, checkID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterDNSRecordsByPeers(records []nmdata.SimpleRecord, peers map[string]*nmdata.Peer, includeIPv6 bool) []nmdata.SimpleRecord {
|
||||
if len(records) == 0 || len(peers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
peerIPs := make(map[string]struct{}, len(peers)*2)
|
||||
for _, peer := range peers {
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
peerIPs[peer.IP.String()] = struct{}{}
|
||||
if includeIPv6 && peer.IPv6.IsValid() {
|
||||
peerIPs[peer.IPv6.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
filteredRecords := make([]nmdata.SimpleRecord, 0, len(records))
|
||||
for _, record := range records {
|
||||
if _, exists := peerIPs[record.RData]; exists {
|
||||
filteredRecords = append(filteredRecords, record)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRecords
|
||||
}
|
||||
|
||||
func filterGroupIDToUserIDs(fullMap map[string][]string, neededGroupIDs map[string]struct{}) map[string][]string {
|
||||
if len(neededGroupIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := make(map[string][]string, len(neededGroupIDs))
|
||||
for groupID := range neededGroupIDs {
|
||||
if users, ok := fullMap[groupID]; ok {
|
||||
filtered[groupID] = users
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
1610
shared/management/networkmap/networkmapcompute_test.go
Normal file
1610
shared/management/networkmap/networkmapcompute_test.go
Normal file
File diff suppressed because it is too large
Load Diff
79
shared/management/networkmap/networkmapdata.go
Normal file
79
shared/management/networkmap/networkmapdata.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package networkmap
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
// NetworkMapData is a dependency-light, slim twin of the server Account. It
|
||||
// carries only the state GetPeerNetworkMapComponents needs, expressed in the
|
||||
// fresh nmdata twin types. A builder converts an Account into a NetworkMapData
|
||||
// once per account; the per-peer components calculation then runs on this twin
|
||||
// with no reference back to the Account.
|
||||
type NetworkMapData struct { //nolint:revive // established name across the codebase
|
||||
Peers map[string]*nmdata.Peer
|
||||
Groups map[string]*nmdata.Group
|
||||
Policies []*nmdata.Policy
|
||||
Routes []*nmdata.Route
|
||||
NameServerGroups []*nmdata.NameServerGroup
|
||||
NetworkResources []*nmdata.NetworkResource
|
||||
|
||||
Network *nmdata.Network
|
||||
DNSSettings *nmdata.DNSSettings
|
||||
AccountSettings *nmdata.AccountSettingsInfo
|
||||
|
||||
PostureChecks map[string]*nmdata.PostureChecks
|
||||
|
||||
// PostureValidation holds the precomputed posture-check results, keyed by
|
||||
// posture check ID then peer ID. Filled by PrecomputePostureValidation; a
|
||||
// present but nil inner map marks a check ID that resolves to no posture
|
||||
// check, which the calc treats as passing.
|
||||
PostureValidation map[string]map[string]bool
|
||||
|
||||
AllowedUserIDs map[string]struct{}
|
||||
NetworkXIDToPublicID map[string]string
|
||||
PostureCheckXIDToPublicID map[string]string
|
||||
ValidatedPeers map[string]struct{}
|
||||
ResourcePolicies map[string][]*nmdata.Policy
|
||||
Routers map[string]map[string]*nmdata.NetworkRouter
|
||||
GroupIDToUserIDs map[string][]string
|
||||
DNSDomain string
|
||||
|
||||
// ProxyTargetedDomainResourceIDs is the account-level half of
|
||||
// forcesRoutingPeerDNSResolution: domain network resources targeted by an
|
||||
// enabled reverse-proxy service.
|
||||
ProxyTargetedDomainResourceIDs map[string]struct{}
|
||||
|
||||
AppliedZoneCandidates []AppliedZoneCandidate
|
||||
PrivateServiceCandidates []PrivateServiceCandidate
|
||||
|
||||
// Services are the account's reverse-proxy services, persisted ones and
|
||||
// the in-memory ones synthesised from agent-network state. They are the
|
||||
// source of the proxy ACLs injectProxyPolicies synthesises, which no
|
||||
// builder can load because they are never written to the database.
|
||||
Services []*nmdata.Service
|
||||
|
||||
peerGroupsOnce sync.Once
|
||||
peerGroupsIdx map[string]map[string]struct{}
|
||||
|
||||
proxyPoliciesOnce sync.Once
|
||||
}
|
||||
|
||||
// AppliedZoneCandidate is an account-level custom DNS zone reduced to the
|
||||
// per-peer decision the components calc still makes: include the zone only when
|
||||
// the peer belongs to one of its distribution groups. Record conversion is done
|
||||
// once at build time.
|
||||
type AppliedZoneCandidate struct {
|
||||
DistributionGroups []string
|
||||
Zone nmdata.CustomZone
|
||||
}
|
||||
|
||||
// PrivateServiceCandidate is a single private service's synthesized records,
|
||||
// carried per apex zone. The builder resolves proxy-cluster connectivity and
|
||||
// domain-suffix matching once; the calc merges the candidates whose AccessGroups
|
||||
// the peer belongs to, grouped by Zone.Domain.
|
||||
type PrivateServiceCandidate struct {
|
||||
AccessGroups []string
|
||||
Zone nmdata.CustomZone
|
||||
}
|
||||
18
shared/management/networkmap/nmdata/account_settings.go
Normal file
18
shared/management/networkmap/nmdata/account_settings.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package nmdata
|
||||
|
||||
import "time"
|
||||
|
||||
// AccountSettingsInfo is the slim twin of types.AccountSettingsInfo.
|
||||
type AccountSettingsInfo struct {
|
||||
PeerLoginExpirationEnabled bool
|
||||
PeerLoginExpiration time.Duration
|
||||
PeerInactivityExpirationEnabled bool
|
||||
PeerInactivityExpiration time.Duration
|
||||
DNSDomain string
|
||||
IPv6EnabledGroups []string
|
||||
RoutingPeerDNSResolutionEnabled bool
|
||||
LazyConnectionEnabled bool
|
||||
AutoUpdateVersion string
|
||||
AutoUpdateAlways bool
|
||||
MetricsPushEnabled bool
|
||||
}
|
||||
18
shared/management/networkmap/nmdata/dns.go
Normal file
18
shared/management/networkmap/nmdata/dns.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package nmdata
|
||||
|
||||
// SimpleRecord is the slim twin of dns.SimpleRecord.
|
||||
type SimpleRecord struct {
|
||||
Name string
|
||||
Type int
|
||||
Class string
|
||||
TTL int
|
||||
RData string
|
||||
}
|
||||
|
||||
// CustomZone is the slim twin of dns.CustomZone.
|
||||
type CustomZone struct {
|
||||
Domain string
|
||||
Records []SimpleRecord
|
||||
SearchDomainDisabled bool
|
||||
NonAuthoritative bool
|
||||
}
|
||||
6
shared/management/networkmap/nmdata/dns_settings.go
Normal file
6
shared/management/networkmap/nmdata/dns_settings.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package nmdata
|
||||
|
||||
// DNSSettings is the slim twin of types.DNSSettings.
|
||||
type DNSSettings struct {
|
||||
DisabledManagementGroups []string
|
||||
}
|
||||
30
shared/management/networkmap/nmdata/group.go
Normal file
30
shared/management/networkmap/nmdata/group.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package nmdata
|
||||
|
||||
import "slices"
|
||||
|
||||
// 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.
|
||||
type Group struct {
|
||||
ID string
|
||||
Name string
|
||||
PublicID string
|
||||
Peers []string
|
||||
Resources []Resource
|
||||
}
|
||||
|
||||
func (g *Group) IsGroupAll() bool {
|
||||
return g.Name == GroupAllName
|
||||
}
|
||||
|
||||
func (g *Group) Copy() *Group {
|
||||
return &Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
PublicID: g.PublicID,
|
||||
Peers: slices.Clone(g.Peers),
|
||||
Resources: slices.Clone(g.Resources),
|
||||
}
|
||||
}
|
||||
84
shared/management/networkmap/nmdata/group_test.go
Normal file
84
shared/management/networkmap/nmdata/group_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGroupCopy_AllFieldsCopied fills every Group field with a unique non-zero
|
||||
// value derived from its field path, so a field added to Group but forgotten
|
||||
// in Copy fails here by name without the test needing an update. The unique
|
||||
// per-path values also catch fields swapped inside Copy.
|
||||
func TestGroupCopy_AllFieldsCopied(t *testing.T) {
|
||||
src := &Group{}
|
||||
seed := 0
|
||||
fillValue(t, reflect.ValueOf(src).Elem(), "Group", &seed)
|
||||
|
||||
copied := src.Copy()
|
||||
|
||||
srcV := reflect.ValueOf(src).Elem()
|
||||
copiedV := reflect.ValueOf(copied).Elem()
|
||||
for i := 0; i < srcV.NumField(); i++ {
|
||||
name := srcV.Type().Field(i).Name
|
||||
if !reflect.DeepEqual(srcV.Field(i).Interface(), copiedV.Field(i).Interface()) {
|
||||
t.Errorf("field %s not copied: src=%#v copy=%#v",
|
||||
name, srcV.Field(i).Interface(), copiedV.Field(i).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < srcV.NumField(); i++ {
|
||||
f := srcV.Field(i)
|
||||
if f.Kind() != reflect.Slice || f.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
name := srcV.Type().Field(i).Name
|
||||
fillValue(t, f.Index(0), name+"-mutated", &seed)
|
||||
if reflect.DeepEqual(f.Interface(), copiedV.Field(i).Interface()) {
|
||||
t.Errorf("field %s shares memory with the copy", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fillValue sets v to a deterministic non-zero value derived from its field
|
||||
// path. Kinds it does not handle fail the test loudly, so the filler is
|
||||
// extended together with the struct instead of silently under-testing new
|
||||
// fields.
|
||||
func fillValue(t *testing.T, v reflect.Value, path string, seed *int) {
|
||||
t.Helper()
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
v.SetString(path)
|
||||
case reflect.Bool:
|
||||
v.SetBool(true)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
*seed++
|
||||
v.SetInt(int64(*seed))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
*seed++
|
||||
v.SetUint(uint64(*seed))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
*seed++
|
||||
v.SetFloat(float64(*seed))
|
||||
case reflect.Slice:
|
||||
s := reflect.MakeSlice(v.Type(), 2, 2)
|
||||
fillValue(t, s.Index(0), path+"[0]", seed)
|
||||
fillValue(t, s.Index(1), path+"[1]", seed)
|
||||
v.Set(s)
|
||||
case reflect.Struct:
|
||||
settable := 0
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
if !f.CanSet() {
|
||||
continue
|
||||
}
|
||||
settable++
|
||||
fillValue(t, f, path+"."+v.Type().Field(i).Name, seed)
|
||||
}
|
||||
if settable == 0 {
|
||||
t.Fatalf("struct %s at %s has no settable fields — extend fillValue to construct it", v.Type(), path)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unsupported kind %s at %s — extend fillValue", v.Kind(), path)
|
||||
}
|
||||
}
|
||||
24
shared/management/networkmap/nmdata/nameserver.go
Normal file
24
shared/management/networkmap/nmdata/nameserver.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package nmdata
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// NameServerGroup is the slim twin of dns.NameServerGroup.
|
||||
type NameServerGroup struct {
|
||||
ID string
|
||||
PublicID string
|
||||
Name string
|
||||
Description string
|
||||
NameServers []NameServer
|
||||
Groups []string
|
||||
Primary bool
|
||||
Domains []string
|
||||
Enabled bool
|
||||
SearchDomainsEnabled bool
|
||||
}
|
||||
|
||||
// NameServer is the slim twin of dns.NameServer.
|
||||
type NameServer struct {
|
||||
IP netip.Addr
|
||||
NSType int
|
||||
Port int
|
||||
}
|
||||
16
shared/management/networkmap/nmdata/network.go
Normal file
16
shared/management/networkmap/nmdata/network.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package nmdata
|
||||
|
||||
import "net"
|
||||
|
||||
// Network is the slim twin of types.Network.
|
||||
type Network struct {
|
||||
Identifier string
|
||||
Net net.IPNet
|
||||
NetV6 net.IPNet
|
||||
Dns string
|
||||
Serial int64
|
||||
}
|
||||
|
||||
func (n *Network) CurrentSerial() uint64 {
|
||||
return uint64(n.Serial)
|
||||
}
|
||||
18
shared/management/networkmap/nmdata/network_resource.go
Normal file
18
shared/management/networkmap/nmdata/network_resource.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package nmdata
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// NetworkResource is the slim twin of resources/types.NetworkResource.
|
||||
type NetworkResource struct {
|
||||
ID string
|
||||
NetworkID string
|
||||
AccountID string
|
||||
PublicID string
|
||||
Name string
|
||||
Description string
|
||||
Type string
|
||||
Address string // TODO: isn't persisted in the DB
|
||||
Domain string
|
||||
Prefix netip.Prefix
|
||||
Enabled bool
|
||||
}
|
||||
10
shared/management/networkmap/nmdata/network_router.go
Normal file
10
shared/management/networkmap/nmdata/network_router.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package nmdata
|
||||
|
||||
// NetworkRouter is the slim twin of routers/types.NetworkRouter.
|
||||
type NetworkRouter struct {
|
||||
PublicID string
|
||||
PeerGroups []string
|
||||
Masquerade bool
|
||||
Metric int
|
||||
Enabled bool
|
||||
}
|
||||
129
shared/management/networkmap/nmdata/peer.go
Normal file
129
shared/management/networkmap/nmdata/peer.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Peer capability constants mirror the proto enum values.
|
||||
const (
|
||||
PeerCapabilitySourcePrefixes int32 = 1
|
||||
PeerCapabilityIPv6Overlay int32 = 2
|
||||
PeerCapabilityComponentNetworkMap int32 = 3
|
||||
)
|
||||
|
||||
// Peer is the slim twin of peer.Peer.
|
||||
type Peer struct {
|
||||
ID string
|
||||
Key string
|
||||
SSHKey string
|
||||
DNSLabel string
|
||||
UserID string
|
||||
SSHEnabled bool
|
||||
LoginExpirationEnabled bool
|
||||
LastLogin *time.Time
|
||||
IP netip.Addr
|
||||
IPv6 netip.Addr
|
||||
RequiresApproval bool
|
||||
ExtraDNSLabels []string
|
||||
Meta PeerSystemMeta
|
||||
ProxyMeta ProxyMeta
|
||||
Location PeerLocation
|
||||
}
|
||||
|
||||
// ProxyMeta is the slim twin of peer.ProxyMeta.
|
||||
type ProxyMeta struct {
|
||||
Embedded bool
|
||||
Cluster string
|
||||
}
|
||||
|
||||
// PeerSystemMeta is the slim twin of peer.PeerSystemMeta.
|
||||
type PeerSystemMeta struct {
|
||||
WtVersion string
|
||||
GoOS string
|
||||
OSVersion string
|
||||
KernelVersion string
|
||||
NetworkAddresses []NetworkAddress
|
||||
Files []File
|
||||
Capabilities []int32
|
||||
Flags Flags
|
||||
SyncMessageVersion int
|
||||
}
|
||||
|
||||
// Flags is the slim twin of peer.Flags.
|
||||
type Flags struct {
|
||||
ServerSSHAllowed bool
|
||||
DisableIPv6 bool
|
||||
}
|
||||
|
||||
// NetworkAddress is the slim twin of peer.NetworkAddress.
|
||||
type NetworkAddress struct {
|
||||
NetIP netip.Prefix
|
||||
}
|
||||
|
||||
// File is the slim twin of peer.File.
|
||||
type File struct {
|
||||
Path string
|
||||
ProcessIsRunning bool
|
||||
}
|
||||
|
||||
// PeerLocation is the slim twin of peer.Location.
|
||||
type PeerLocation struct {
|
||||
CountryCode string
|
||||
CityName string
|
||||
ConnectionIP net.IP
|
||||
}
|
||||
|
||||
func (p *Peer) HasCapability(capability int32) bool {
|
||||
return slices.Contains(p.Meta.Capabilities, capability)
|
||||
}
|
||||
|
||||
func (p *Peer) SupportsIPv6() bool {
|
||||
return !p.Meta.Flags.DisableIPv6 && p.HasCapability(PeerCapabilityIPv6Overlay)
|
||||
}
|
||||
|
||||
func (p *Peer) SupportsSourcePrefixes() bool {
|
||||
return p.HasCapability(PeerCapabilitySourcePrefixes)
|
||||
}
|
||||
|
||||
func (p *Peer) AddedWithSSOLogin() bool {
|
||||
return p.UserID != ""
|
||||
}
|
||||
|
||||
func (p *Peer) FQDN(dnsDomain string) string {
|
||||
if dnsDomain == "" {
|
||||
return ""
|
||||
}
|
||||
return p.DNSLabel + "." + dnsDomain
|
||||
}
|
||||
|
||||
func (p *Peer) GetLastLogin() time.Time {
|
||||
if p.LastLogin != nil {
|
||||
return *p.LastLogin
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// SessionExpiresAt mirrors peer.Peer.SessionExpiresAt.
|
||||
func (p *Peer) SessionExpiresAt(accountExpirationEnabled bool, expiresIn time.Duration) time.Time {
|
||||
if !accountExpirationEnabled || !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
|
||||
return time.Time{}
|
||||
}
|
||||
last := p.GetLastLogin()
|
||||
if last.IsZero() {
|
||||
return time.Time{}
|
||||
}
|
||||
return last.Add(expiresIn).UTC()
|
||||
}
|
||||
|
||||
func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
|
||||
if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
|
||||
return false, 0
|
||||
}
|
||||
expiresAt := p.GetLastLogin().Add(expiresIn)
|
||||
now := time.Now()
|
||||
timeLeft := expiresAt.Sub(now)
|
||||
return timeLeft <= 0, timeLeft
|
||||
}
|
||||
98
shared/management/networkmap/nmdata/policy.go
Normal file
98
shared/management/networkmap/nmdata/policy.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package nmdata
|
||||
|
||||
const (
|
||||
policyRuleProtocolALL = "all"
|
||||
policyRuleProtocolTCP = "tcp"
|
||||
|
||||
defaultSSHPortString = "22"
|
||||
nativeSSHPortString = "22022"
|
||||
defaultSSHPortNumber uint16 = 22
|
||||
nativeSSHPortNumber uint16 = 22022
|
||||
)
|
||||
|
||||
// Policy is the slim twin of types.Policy.
|
||||
type Policy struct {
|
||||
ID string
|
||||
PublicID string
|
||||
Enabled bool
|
||||
SourcePostureChecks []string
|
||||
Rules []*PolicyRule
|
||||
}
|
||||
|
||||
// PolicyRule is the slim twin of types.PolicyRule.
|
||||
type PolicyRule struct {
|
||||
ID string
|
||||
PolicyID string
|
||||
Enabled bool
|
||||
Action string
|
||||
Protocol string
|
||||
Bidirectional bool
|
||||
Sources []string
|
||||
Destinations []string
|
||||
SourceResource Resource
|
||||
DestinationResource Resource
|
||||
Ports []string
|
||||
PortRanges []RulePortRange
|
||||
AuthorizedGroups map[string][]string
|
||||
AuthorizedUser string
|
||||
SessionPubKey string
|
||||
SessionDisplayName string
|
||||
}
|
||||
|
||||
// RulePortRange is the slim twin of types.RulePortRange.
|
||||
type RulePortRange struct {
|
||||
Start uint16
|
||||
End uint16
|
||||
}
|
||||
|
||||
// Resource is the slim twin of types.Resource.
|
||||
type Resource struct {
|
||||
ID string
|
||||
Type string
|
||||
}
|
||||
|
||||
func (p *Policy) SourceGroups() []string {
|
||||
if len(p.Rules) == 1 && p.Rules[0] != nil {
|
||||
return p.Rules[0].Sources
|
||||
}
|
||||
groups := make(map[string]struct{}, len(p.Rules))
|
||||
for _, rule := range p.Rules {
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
for _, source := range rule.Sources {
|
||||
groups[source] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
groupIDs := make([]string, 0, len(groups))
|
||||
for groupID := range groups {
|
||||
groupIDs = append(groupIDs, groupID)
|
||||
}
|
||||
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
// PolicyRuleImpliesLegacySSH is the twin-typed sibling of types.PolicyRuleImpliesLegacySSH.
|
||||
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
|
||||
}
|
||||
67
shared/management/networkmap/nmdata/posture.go
Normal file
67
shared/management/networkmap/nmdata/posture.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package nmdata
|
||||
|
||||
const (
|
||||
checkActionAllow = "allow"
|
||||
checkActionDeny = "deny"
|
||||
)
|
||||
|
||||
// PostureChecks is the slim twin of posture.Checks.
|
||||
type PostureChecks struct {
|
||||
ID string
|
||||
Checks ChecksDefinition
|
||||
}
|
||||
|
||||
// ChecksDefinition is the slim twin of posture.ChecksDefinition.
|
||||
type ChecksDefinition struct {
|
||||
NBVersionCheck *NBVersionCheck
|
||||
OSVersionCheck *OSVersionCheck
|
||||
GeoLocationCheck *GeoLocationCheck
|
||||
PeerNetworkRangeCheck *PeerNetworkRangeCheck
|
||||
ProcessCheck *ProcessCheck
|
||||
}
|
||||
|
||||
// Check is the slim twin of posture.Check. It is sealed: only the check types
|
||||
// in this package implement it.
|
||||
type Check interface {
|
||||
check(peer *Peer) (bool, error)
|
||||
}
|
||||
|
||||
// Passes reports whether the peer satisfies every check in this bundle. It
|
||||
// mirrors the server posture path: a check returning (false, _) — including on
|
||||
// an evaluation error — fails the bundle.
|
||||
func (pc *PostureChecks) Passes(peer *Peer) bool {
|
||||
return PassesChecks(pc.GetChecks(), peer)
|
||||
}
|
||||
|
||||
// PassesChecks is Passes over an already built check set, for callers that
|
||||
// evaluate many peers against the same bundle.
|
||||
func PassesChecks(checks []Check, peer *Peer) bool {
|
||||
for _, c := range checks {
|
||||
valid, _ := c.check(peer)
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetChecks returns the initialized checks in the same order as posture.Checks.GetChecks.
|
||||
func (pc *PostureChecks) GetChecks() []Check {
|
||||
var checks []Check
|
||||
if pc.Checks.NBVersionCheck != nil {
|
||||
checks = append(checks, pc.Checks.NBVersionCheck)
|
||||
}
|
||||
if pc.Checks.OSVersionCheck != nil {
|
||||
checks = append(checks, pc.Checks.OSVersionCheck)
|
||||
}
|
||||
if pc.Checks.GeoLocationCheck != nil {
|
||||
checks = append(checks, pc.Checks.GeoLocationCheck)
|
||||
}
|
||||
if pc.Checks.PeerNetworkRangeCheck != nil {
|
||||
checks = append(checks, pc.Checks.PeerNetworkRangeCheck)
|
||||
}
|
||||
if pc.Checks.ProcessCheck != nil {
|
||||
checks = append(checks, pc.Checks.ProcessCheck)
|
||||
}
|
||||
return checks
|
||||
}
|
||||
45
shared/management/networkmap/nmdata/posture_geo_location.go
Normal file
45
shared/management/networkmap/nmdata/posture_geo_location.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package nmdata
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GeoLocation is the slim twin of posture.Location.
|
||||
type GeoLocation struct {
|
||||
CountryCode string
|
||||
CityName string
|
||||
}
|
||||
|
||||
// GeoLocationCheck is the slim twin of posture.GeoLocationCheck.
|
||||
type GeoLocationCheck struct {
|
||||
Locations []GeoLocation
|
||||
Action string
|
||||
}
|
||||
|
||||
func (g *GeoLocationCheck) check(peer *Peer) (bool, error) {
|
||||
if peer.Location.CountryCode == "" && peer.Location.CityName == "" {
|
||||
return false, fmt.Errorf("peer's location is not set")
|
||||
}
|
||||
|
||||
for _, loc := range g.Locations {
|
||||
if loc.CountryCode == peer.Location.CountryCode {
|
||||
if loc.CityName == "" || loc.CityName == peer.Location.CityName {
|
||||
switch g.Action {
|
||||
case checkActionDeny:
|
||||
return false, nil
|
||||
case checkActionAllow:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("invalid geo location action: %s", g.Action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.Action == checkActionDeny {
|
||||
return true, nil
|
||||
}
|
||||
if g.Action == checkActionAllow {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid geo location action: %s", g.Action)
|
||||
}
|
||||
38
shared/management/networkmap/nmdata/posture_nb_version.go
Normal file
38
shared/management/networkmap/nmdata/posture_nb_version.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
// NBVersionCheck is the slim twin of posture.NBVersionCheck.
|
||||
type NBVersionCheck struct {
|
||||
MinVersion string
|
||||
}
|
||||
|
||||
func (n *NBVersionCheck) check(peer *Peer) (bool, error) {
|
||||
return meetsMinVersion(n.MinVersion, peer.Meta.WtVersion)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func sanitizeVersion(v string) string {
|
||||
parts := strings.Split(v, "-")
|
||||
return parts[0]
|
||||
}
|
||||
62
shared/management/networkmap/nmdata/posture_network.go
Normal file
62
shared/management/networkmap/nmdata/posture_network.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// PeerNetworkRangeCheck is the slim twin of posture.PeerNetworkRangeCheck.
|
||||
type PeerNetworkRangeCheck struct {
|
||||
Action string
|
||||
Ranges []netip.Prefix
|
||||
}
|
||||
|
||||
func (p *PeerNetworkRangeCheck) check(peer *Peer) (bool, error) {
|
||||
peerPrefixes := make([]netip.Prefix, 0, len(peer.Meta.NetworkAddresses)+1)
|
||||
for _, peerNetAddr := range peer.Meta.NetworkAddresses {
|
||||
peerPrefixes = append(peerPrefixes, peerNetAddr.NetIP)
|
||||
}
|
||||
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 prefixContains(outer, inner netip.Prefix) bool {
|
||||
outer = outer.Masked()
|
||||
inner = inner.Masked()
|
||||
return outer.Bits() <= inner.Bits() &&
|
||||
outer.Addr().BitLen() == inner.Addr().BitLen() &&
|
||||
outer.Contains(inner.Addr())
|
||||
}
|
||||
79
shared/management/networkmap/nmdata/posture_os_version.go
Normal file
79
shared/management/networkmap/nmdata/posture_os_version.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
// MinVersionCheck is the slim twin of posture.MinVersionCheck.
|
||||
type MinVersionCheck struct {
|
||||
MinVersion string
|
||||
}
|
||||
|
||||
// MinKernelVersionCheck is the slim twin of posture.MinKernelVersionCheck.
|
||||
type MinKernelVersionCheck struct {
|
||||
MinKernelVersion string
|
||||
}
|
||||
|
||||
// OSVersionCheck is the slim twin of posture.OSVersionCheck.
|
||||
type OSVersionCheck struct {
|
||||
Android *MinVersionCheck
|
||||
Darwin *MinVersionCheck
|
||||
Ios *MinVersionCheck
|
||||
Linux *MinKernelVersionCheck
|
||||
Windows *MinKernelVersionCheck
|
||||
}
|
||||
|
||||
func (c *OSVersionCheck) check(peer *Peer) (bool, error) {
|
||||
switch peer.Meta.GoOS {
|
||||
case "android":
|
||||
return checkMinVersion(peer.Meta.OSVersion, c.Android)
|
||||
case "darwin":
|
||||
return checkMinVersion(peer.Meta.OSVersion, c.Darwin)
|
||||
case "ios":
|
||||
return checkMinVersion(peer.Meta.OSVersion, c.Ios)
|
||||
case "linux":
|
||||
kernelVersion := strings.Split(peer.Meta.KernelVersion, "-")[0]
|
||||
return checkMinKernelVersion(kernelVersion, c.Linux)
|
||||
case "windows":
|
||||
return checkMinKernelVersion(peer.Meta.KernelVersion, c.Windows)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func checkMinVersion(peerVersion string, check *MinVersionCheck) (bool, error) {
|
||||
if check == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
peerNBVersion, err := version.NewVersion(peerVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(">= " + check.MinVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return constraints.Check(peerNBVersion), nil
|
||||
}
|
||||
|
||||
func checkMinKernelVersion(peerVersion string, check *MinKernelVersionCheck) (bool, error) {
|
||||
if check == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
peerNBVersion, err := version.NewVersion(peerVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(">= " + check.MinKernelVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return constraints.Check(peerNBVersion), nil
|
||||
}
|
||||
56
shared/management/networkmap/nmdata/posture_process.go
Normal file
56
shared/management/networkmap/nmdata/posture_process.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Process is the slim twin of posture.Process.
|
||||
type Process struct {
|
||||
LinuxPath string
|
||||
MacPath string
|
||||
WindowsPath string
|
||||
}
|
||||
|
||||
// ProcessCheck is the slim twin of posture.ProcessCheck.
|
||||
type ProcessCheck struct {
|
||||
Processes []Process
|
||||
}
|
||||
|
||||
func (p *ProcessCheck) check(peer *Peer) (bool, error) {
|
||||
peerActiveProcesses := extractPeerActiveProcesses(peer.Meta.Files)
|
||||
|
||||
var pathSelector func(Process) string
|
||||
switch peer.Meta.GoOS {
|
||||
case "linux":
|
||||
pathSelector = func(process Process) string { return process.LinuxPath }
|
||||
case "darwin":
|
||||
pathSelector = func(process Process) string { return process.MacPath }
|
||||
case "windows":
|
||||
pathSelector = func(process Process) string { return process.WindowsPath }
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported peer's operating system: %s", peer.Meta.GoOS)
|
||||
}
|
||||
|
||||
return p.areAllProcessesRunning(peerActiveProcesses, pathSelector), nil
|
||||
}
|
||||
|
||||
func (p *ProcessCheck) areAllProcessesRunning(activeProcesses []string, pathSelector func(Process) string) bool {
|
||||
for _, process := range p.Processes {
|
||||
path := pathSelector(process)
|
||||
if path == "" || !slices.Contains(activeProcesses, path) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func extractPeerActiveProcesses(files []File) []string {
|
||||
activeProcesses := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
if file.ProcessIsRunning {
|
||||
activeProcesses = append(activeProcesses, file.Path)
|
||||
}
|
||||
}
|
||||
return activeProcesses
|
||||
}
|
||||
108
shared/management/networkmap/nmdata/route.go
Normal file
108
shared/management/networkmap/nmdata/route.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
)
|
||||
|
||||
// NetworkType mirrors route.NetworkType iota values.
|
||||
const (
|
||||
NetworkTypeInvalid = 0
|
||||
NetworkTypeIPv4 = 1
|
||||
NetworkTypeIPv6 = 2
|
||||
NetworkTypeDomain = 3
|
||||
|
||||
haSeparator = "|"
|
||||
)
|
||||
|
||||
// Route is the slim twin of route.Route.
|
||||
type Route struct {
|
||||
ID string
|
||||
AccountID string
|
||||
PublicID string
|
||||
Network netip.Prefix
|
||||
Domains domain.List
|
||||
KeepRoute bool
|
||||
NetID string
|
||||
Description string
|
||||
Peer string
|
||||
PeerID string
|
||||
PeerGroups []string
|
||||
NetworkType int
|
||||
Masquerade bool
|
||||
Metric int
|
||||
Enabled bool
|
||||
Groups []string
|
||||
AccessControlGroups []string
|
||||
SkipAutoApply bool
|
||||
}
|
||||
|
||||
func (r *Route) Equal(other *Route) bool {
|
||||
if r == nil && other == nil {
|
||||
return true
|
||||
} else if r == nil || other == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return other.ID == r.ID &&
|
||||
other.Description == r.Description &&
|
||||
other.NetID == r.NetID &&
|
||||
other.Network == r.Network &&
|
||||
slices.Equal(r.Domains, other.Domains) &&
|
||||
other.KeepRoute == r.KeepRoute &&
|
||||
other.NetworkType == r.NetworkType &&
|
||||
other.Peer == r.Peer &&
|
||||
other.PeerID == r.PeerID &&
|
||||
other.Metric == r.Metric &&
|
||||
other.Masquerade == r.Masquerade &&
|
||||
other.Enabled == r.Enabled &&
|
||||
slices.Equal(r.Groups, other.Groups) &&
|
||||
slices.Equal(r.PeerGroups, other.PeerGroups) &&
|
||||
slices.Equal(r.AccessControlGroups, other.AccessControlGroups) &&
|
||||
other.SkipAutoApply == r.SkipAutoApply
|
||||
}
|
||||
|
||||
func (r *Route) IsDynamic() bool {
|
||||
return r.NetworkType == NetworkTypeDomain
|
||||
}
|
||||
|
||||
func (r *Route) NetString() string {
|
||||
if r.IsDynamic() && r.Domains != nil {
|
||||
return r.Domains.SafeString()
|
||||
}
|
||||
return r.Network.String()
|
||||
}
|
||||
|
||||
func (r *Route) GetHAUniqueID() string {
|
||||
return r.NetID + haSeparator + r.NetString()
|
||||
}
|
||||
|
||||
func (r *Route) GetResourceID() string {
|
||||
return strings.Split(r.ID, ":")[0]
|
||||
}
|
||||
|
||||
func (r *Route) Copy() *Route {
|
||||
return &Route{
|
||||
ID: r.ID,
|
||||
AccountID: r.AccountID,
|
||||
PublicID: r.PublicID,
|
||||
Network: r.Network,
|
||||
Domains: slices.Clone(r.Domains),
|
||||
KeepRoute: r.KeepRoute,
|
||||
NetID: r.NetID,
|
||||
Description: r.Description,
|
||||
Peer: r.Peer,
|
||||
PeerID: r.PeerID,
|
||||
PeerGroups: slices.Clone(r.PeerGroups),
|
||||
NetworkType: r.NetworkType,
|
||||
Masquerade: r.Masquerade,
|
||||
Metric: r.Metric,
|
||||
Enabled: r.Enabled,
|
||||
Groups: slices.Clone(r.Groups),
|
||||
AccessControlGroups: slices.Clone(r.AccessControlGroups),
|
||||
SkipAutoApply: r.SkipAutoApply,
|
||||
}
|
||||
}
|
||||
25
shared/management/networkmap/nmdata/service.go
Normal file
25
shared/management/networkmap/nmdata/service.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package nmdata
|
||||
|
||||
// Service is the slim twin of the reverse-proxy service.Service. It carries
|
||||
// only the state proxy-policy injection reads: the persisted reverse-proxy
|
||||
// services and the in-memory ones synthesised from agent-network state, which
|
||||
// are never written to the database.
|
||||
type Service struct {
|
||||
ID string
|
||||
Enabled bool
|
||||
Private bool
|
||||
Mode string
|
||||
ProxyCluster string
|
||||
AccessGroups []string
|
||||
Targets []*ServiceTarget
|
||||
}
|
||||
|
||||
// ServiceTarget is the slim twin of service.Target.
|
||||
type ServiceTarget struct {
|
||||
Enabled bool
|
||||
Path string
|
||||
Port uint16
|
||||
Protocol string
|
||||
TargetID string
|
||||
TargetType string
|
||||
}
|
||||
111
shared/management/networkmap/peers_custom_zone.go
Normal file
111
shared/management/networkmap/peers_custom_zone.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package networkmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/miekg/dns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
const peersZoneRecordTTL = 300
|
||||
|
||||
// PeersCustomZone builds the peers DNS zone from twin peer rows. It is the
|
||||
// single source of the zone-record logic; Account.GetPeersCustomZone delegates
|
||||
// here via twins.
|
||||
func PeersCustomZone(ctx context.Context, accountID string, dnsDomain string, peers map[string]*nmdata.Peer, ipv6AllowedPeers map[string]struct{}) nmdata.CustomZone {
|
||||
var merr *multierror.Error
|
||||
|
||||
if dnsDomain == "" {
|
||||
log.WithContext(ctx).Error("no dns domain is set, returning empty zone")
|
||||
return nmdata.CustomZone{}
|
||||
}
|
||||
|
||||
customZone := nmdata.CustomZone{
|
||||
Domain: dns.Fqdn(dnsDomain),
|
||||
Records: make([]nmdata.SimpleRecord, 0, len(peers)),
|
||||
}
|
||||
|
||||
domainSuffix := "." + dnsDomain
|
||||
|
||||
var sb strings.Builder
|
||||
for _, peer := range peers {
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
if peer.DNSLabel == "" {
|
||||
merr = multierror.Append(merr, fmt.Errorf("peer %s has an empty DNS label", peer.ID))
|
||||
continue
|
||||
}
|
||||
|
||||
sb.Grow(len(peer.DNSLabel) + len(domainSuffix))
|
||||
sb.WriteString(peer.DNSLabel)
|
||||
sb.WriteString(domainSuffix)
|
||||
|
||||
fqdn := sb.String()
|
||||
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
|
||||
Name: fqdn,
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: peersZoneRecordTTL,
|
||||
RData: peer.IP.String(),
|
||||
})
|
||||
// Only advertise AAAA for peers that have a valid IPv6, whose client supports it,
|
||||
// and that belong to an IPv6-enabled group. Old clients don't configure v6 on their
|
||||
// WireGuard interface, so resolving their AAAA causes connections to hang.
|
||||
// Capability changes (client upgrade/downgrade, --disable-ipv6 toggle) propagate
|
||||
// to other peers via SyncPeer/LoginPeer regardless of version change, so AAAA
|
||||
// records refresh when a peer first reports the IPv6 overlay capability.
|
||||
_, peerAllowed := ipv6AllowedPeers[peer.ID]
|
||||
hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed
|
||||
if hasIPv6 {
|
||||
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
|
||||
Name: fqdn,
|
||||
Type: int(dns.TypeAAAA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: peersZoneRecordTTL,
|
||||
RData: peer.IPv6.String(),
|
||||
})
|
||||
}
|
||||
sb.Reset()
|
||||
|
||||
for _, extraLabel := range peer.ExtraDNSLabels {
|
||||
sb.Grow(len(extraLabel) + len(domainSuffix))
|
||||
sb.WriteString(extraLabel)
|
||||
sb.WriteString(domainSuffix)
|
||||
|
||||
extraFqdn := sb.String()
|
||||
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
|
||||
Name: extraFqdn,
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: peersZoneRecordTTL,
|
||||
RData: peer.IP.String(),
|
||||
})
|
||||
if hasIPv6 {
|
||||
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
|
||||
Name: extraFqdn,
|
||||
Type: int(dns.TypeAAAA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: peersZoneRecordTTL,
|
||||
RData: peer.IPv6.String(),
|
||||
})
|
||||
}
|
||||
sb.Reset()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
if merr != nil {
|
||||
log.WithContext(ctx).Errorf("error generating custom zone for account %s: %v", accountID, merr)
|
||||
}
|
||||
}()
|
||||
|
||||
return customZone
|
||||
}
|
||||
209
shared/management/networkmap/proxypolicies.go
Normal file
209
shared/management/networkmap/proxypolicies.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package networkmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceModeUDP = "udp"
|
||||
|
||||
privateServicePortHTTP = 80
|
||||
privateServicePortHTTPS = 443
|
||||
)
|
||||
|
||||
// InjectProxyPolicies synthesises the in-memory ACLs that carry reverse-proxy
|
||||
// traffic and appends them to the twin's policies. They are never persisted,
|
||||
// so no builder can load them: a proxy-access policy lets a cluster's proxy
|
||||
// peers reach each enabled target of a service, and a private-access policy
|
||||
// lets a private service's AccessGroups reach those proxy peers on HTTP(S).
|
||||
//
|
||||
// GetPeerNetworkMapComponents calls it, so every caller of the twin gets the
|
||||
// same policy set no matter which builder produced it. It runs at most once
|
||||
// per twin, and is safe to call again to force the synthesis early.
|
||||
func (nmd *NetworkMapData) InjectProxyPolicies() {
|
||||
nmd.proxyPoliciesOnce.Do(nmd.injectProxyPolicies)
|
||||
}
|
||||
|
||||
func (nmd *NetworkMapData) injectProxyPolicies() {
|
||||
if len(nmd.Services) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
proxyPeersByCluster := nmd.proxyPeersByCluster()
|
||||
if len(proxyPeersByCluster) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, svc := range nmd.Services {
|
||||
if svc == nil || !svc.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
proxyPeers := proxyPeersByCluster[svc.ProxyCluster]
|
||||
for _, target := range svc.Targets {
|
||||
if target == nil || !target.Enabled {
|
||||
continue
|
||||
}
|
||||
port, ok := resolveTargetPort(target)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, proxyPeer := range proxyPeers {
|
||||
nmd.addInjectedPolicy(proxyAccessPolicy(svc, target, proxyPeer, port))
|
||||
}
|
||||
}
|
||||
|
||||
nmd.injectPrivateServicePolicies(svc, proxyPeers)
|
||||
}
|
||||
}
|
||||
|
||||
// injectPrivateServicePolicies synthesises AccessGroups → cluster proxy peers on TCP 80/443.
|
||||
func (nmd *NetworkMapData) injectPrivateServicePolicies(svc *nmdata.Service, proxyPeers []*nmdata.Peer) {
|
||||
if !svc.Private || len(svc.AccessGroups) == 0 || len(proxyPeers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// A service's AccessGroups can name groups that no longer exist — persisted
|
||||
// services and the agent-network synthesiser both carry the ids verbatim from
|
||||
// their own state. An unresolvable source authorises nothing, so drop it here
|
||||
// rather than let the network-map assembly resolve it to a nil group.
|
||||
sources := nmd.existingGroupIDs(svc.AccessGroups)
|
||||
if len(sources) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, proxyPeer := range proxyPeers {
|
||||
nmd.addInjectedPolicy(privateAccessPolicy(svc, proxyPeer, sources))
|
||||
}
|
||||
}
|
||||
|
||||
// addInjectedPolicy appends the policy to the twin's policy set, and to the
|
||||
// policies of the network resource it targets — mirroring the account path,
|
||||
// where the resource-policy map was built after injection.
|
||||
func (nmd *NetworkMapData) addInjectedPolicy(policy *nmdata.Policy) {
|
||||
nmd.Policies = append(nmd.Policies, policy)
|
||||
|
||||
resourceID := policy.Rules[0].DestinationResource.ID
|
||||
if resourceID == "" {
|
||||
return
|
||||
}
|
||||
for _, resource := range nmd.NetworkResources {
|
||||
if resource == nil || !resource.Enabled || resource.ID != resourceID {
|
||||
continue
|
||||
}
|
||||
if nmd.ResourcePolicies == nil {
|
||||
nmd.ResourcePolicies = make(map[string][]*nmdata.Policy)
|
||||
}
|
||||
nmd.ResourcePolicies[resourceID] = append(nmd.ResourcePolicies[resourceID], policy)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func proxyAccessPolicy(svc *nmdata.Service, target *nmdata.ServiceTarget, proxyPeer *nmdata.Peer, port uint16) *nmdata.Policy {
|
||||
policyID := fmt.Sprintf("proxy-access-%s-%s-%s", svc.ID, proxyPeer.ID, target.Path)
|
||||
|
||||
protocol := types.PolicyRuleProtocolTCP
|
||||
if svc.Mode == serviceModeUDP {
|
||||
protocol = types.PolicyRuleProtocolUDP
|
||||
}
|
||||
|
||||
return &nmdata.Policy{
|
||||
ID: policyID,
|
||||
// The envelope encoder puts public ids on the wire and degrades to an
|
||||
// empty one when a policy has none. A synthesised policy has no
|
||||
// persisted row to take a public id from, and its own id is already
|
||||
// stable and unique, so it serves as both.
|
||||
PublicID: policyID,
|
||||
Enabled: true,
|
||||
Rules: []*nmdata.PolicyRule{
|
||||
{
|
||||
ID: policyID,
|
||||
PolicyID: policyID,
|
||||
Enabled: true,
|
||||
SourceResource: nmdata.Resource{ID: proxyPeer.ID, Type: string(types.ResourceTypePeer)},
|
||||
DestinationResource: nmdata.Resource{ID: target.TargetID, Type: target.TargetType},
|
||||
Bidirectional: false,
|
||||
Protocol: string(protocol),
|
||||
Action: string(types.PolicyTrafficActionAccept),
|
||||
PortRanges: []nmdata.RulePortRange{{Start: port, End: port}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func privateAccessPolicy(svc *nmdata.Service, proxyPeer *nmdata.Peer, accessGroups []string) *nmdata.Policy {
|
||||
policyID := fmt.Sprintf("private-access-%s-%s", svc.ID, proxyPeer.ID)
|
||||
|
||||
return &nmdata.Policy{
|
||||
ID: policyID,
|
||||
PublicID: policyID,
|
||||
Enabled: true,
|
||||
Rules: []*nmdata.PolicyRule{
|
||||
{
|
||||
ID: policyID,
|
||||
PolicyID: policyID,
|
||||
Enabled: true,
|
||||
Sources: slices.Clone(accessGroups),
|
||||
DestinationResource: nmdata.Resource{ID: proxyPeer.ID, Type: string(types.ResourceTypePeer)},
|
||||
Bidirectional: false,
|
||||
Protocol: string(types.PolicyRuleProtocolTCP),
|
||||
Action: string(types.PolicyTrafficActionAccept),
|
||||
PortRanges: []nmdata.RulePortRange{
|
||||
{Start: privateServicePortHTTP, End: privateServicePortHTTP},
|
||||
{Start: privateServicePortHTTPS, End: privateServicePortHTTPS},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resolveTargetPort(target *nmdata.ServiceTarget) (uint16, bool) {
|
||||
if target.Port != 0 {
|
||||
return target.Port, true
|
||||
}
|
||||
|
||||
switch target.Protocol {
|
||||
case "https", "tls":
|
||||
return privateServicePortHTTPS, true
|
||||
case "http":
|
||||
return privateServicePortHTTP, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// proxyPeersByCluster groups the account's embedded proxy peers by the cluster
|
||||
// they serve. Sorted by peer ID so the synthesised policy order is stable.
|
||||
func (nmd *NetworkMapData) proxyPeersByCluster() map[string][]*nmdata.Peer {
|
||||
var proxyPeers map[string][]*nmdata.Peer
|
||||
for _, peer := range nmd.Peers {
|
||||
if peer == nil || !peer.ProxyMeta.Embedded {
|
||||
continue
|
||||
}
|
||||
if proxyPeers == nil {
|
||||
proxyPeers = make(map[string][]*nmdata.Peer)
|
||||
}
|
||||
proxyPeers[peer.ProxyMeta.Cluster] = append(proxyPeers[peer.ProxyMeta.Cluster], peer)
|
||||
}
|
||||
for _, peers := range proxyPeers {
|
||||
slices.SortFunc(peers, func(a, b *nmdata.Peer) int { return strings.Compare(a.ID, b.ID) })
|
||||
}
|
||||
return proxyPeers
|
||||
}
|
||||
|
||||
// existingGroupIDs returns the subset of groupIDs that resolve to a group,
|
||||
// preserving the input order.
|
||||
func (nmd *NetworkMapData) existingGroupIDs(groupIDs []string) []string {
|
||||
out := make([]string, 0, len(groupIDs))
|
||||
for _, groupID := range groupIDs {
|
||||
if _, ok := nmd.Groups[groupID]; ok {
|
||||
out = append(out, groupID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user