mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 23:29:08 +02:00
* [client,management] Skip route firewall rule computation when no firewall A peer that runs with the firewall disabled has no ACL manager and no firewall to program, so nothing ever reads RoutesFirewallRules: the only consumers are acl.Manager, which is reached solely when e.acl is set, and the legacy-management probe in updateNetworkMap, which is guarded by a non-nil firewall. Building those rules is the most expensive part of a sync on a peer that routes many network resources. On a 15k-peer deployment a debug bundle showed getPeerNetworkResourceFirewallRules accounting for 62% of the allocations of Calculate, and Calculate for effectively all of the allocations of handleSync, which was taking 3.2s on average and holding the engine lock for the duration. Let the caller ask Calculate to leave the rules out. The client passes its existing DisableFirewall setting; the management server keeps the default and still produces them. RoutesFirewallRulesIsEmpty is set from the resulting empty list, so a receiver that would otherwise infer legacy management from an empty rule set does not misread the skip. * [client,management] Cover the skip flag through the envelope Review feedback on #7624. The components test compared only the length of the peer firewall rules, so a change to their content would have passed while the message claimed they came out unchanged. Compare the slices. The skip path was also only exercised by setting the field directly on the components, which bypasses the envelope conversion where RoutesFirewallRulesIsEmpty is derived. That bit is what keeps the client from reading skipped rules as a legacy management server, so it gets a test that goes through EnvelopeToNetworkMap with the flag set. * [management] Give the router a peer ACL so the rule comparison bites Review feedback on #7624. peer-router-1 appears in no peer ACL in the shared fixture, so its FirewallRules came out empty and the equality assertion compared two empty slices — it would have passed even if the peer rules were dropped entirely. Add a policy covering the router and require the baseline to be non-empty before comparing.
196 lines
7.0 KiB
Go
196 lines
7.0 KiB
Go
package networkmap
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
"github.com/netbirdio/netbird/shared/management/types"
|
|
)
|
|
|
|
// EnvelopeResult is what the client engine consumes after receiving a
|
|
// component-format NetworkMap. Both fields are populated:
|
|
//
|
|
// - NetworkMap is the *proto.NetworkMap shape the engine reads today via
|
|
// update.GetNetworkMap() — built from the envelope's components by
|
|
// running Calculate() locally + converting back through the shared
|
|
// proto helpers + merging the optional ProxyPatch.
|
|
// - Components is the *types.NetworkMapComponents the engine retains so
|
|
// future incremental delta updates have a base to apply changes
|
|
// against. The client keeps it under its sync lock.
|
|
type EnvelopeResult struct {
|
|
NetworkMap *proto.NetworkMap
|
|
Components *types.NetworkMapComponents
|
|
}
|
|
|
|
// EnvelopeToNetworkMap is the full client-side pipeline: decode the
|
|
// component envelope back to a typed NetworkMapComponents, run Calculate()
|
|
// locally to produce the typed NetworkMap, convert it to the wire form the
|
|
// engine consumes, and fold in any ProxyPatch the server attached.
|
|
//
|
|
// localPeerKey is the receiving peer's WG pub key (used to derive
|
|
// includeIPv6 / useSourcePrefixes from the receiving peer's own record in
|
|
// the components struct, mirroring legacy ToSyncResponse behaviour).
|
|
//
|
|
// dnsName is the account's DNS domain ("netbird.cloud" etc.); used when
|
|
// rebuilding the per-peer FQDNs that proto.RemotePeerConfig carries.
|
|
//
|
|
// skipRouteFirewallRules leaves RoutesFirewallRules empty. Callers that have
|
|
// no firewall to program pass true: the rules are the most expensive part of
|
|
// Calculate on a peer that routes many network resources, and nothing reads
|
|
// them afterwards.
|
|
func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, localPeerKey, dnsName string, skipRouteFirewallRules bool) (*EnvelopeResult, error) {
|
|
components, err := DecodeEnvelope(ctx, env)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode envelope: %w", err)
|
|
}
|
|
|
|
// Find the receiving peer in the decoded components by WG key.
|
|
// c.Peers is keyed by canonical base64 of the raw 32-byte pub key
|
|
// (decoder re-encodes the bytes off the wire). The caller may pass a
|
|
// non-canonical encoding (some persisted production keys carry
|
|
// non-zero trailing padding bits that survived a legacy import), so
|
|
// round-trip through raw bytes once to canonicalize before lookup.
|
|
canonicalKey := canonicalizeWgKey(localPeerKey)
|
|
localPeer := components.Peers[canonicalKey]
|
|
if localPeer == nil {
|
|
return nil, fmt.Errorf("receiving peer (wg_key prefix %q) not found among %d decoded peers — components have no PeerID, Calculate would return empty", trimKey(localPeerKey), len(components.Peers))
|
|
}
|
|
components.PeerID = canonicalKey
|
|
components.SkipRouteFirewallRules = skipRouteFirewallRules
|
|
|
|
includeIPv6 := localPeer.SupportsIPv6() && localPeer.IPv6.IsValid()
|
|
useSourcePrefixes := localPeer.SupportsSourcePrefixes()
|
|
|
|
typedNM := components.Calculate(ctx)
|
|
|
|
full := env.GetFull()
|
|
dnsFwdPort := int64(0)
|
|
if full != nil {
|
|
dnsFwdPort = full.DnsForwarderPort
|
|
}
|
|
|
|
protoNM := &proto.NetworkMap{
|
|
Serial: typedNM.Network.CurrentSerial(),
|
|
}
|
|
if full != nil {
|
|
protoNM.PeerConfig = full.PeerConfig
|
|
}
|
|
protoNM.Routes = ToProtocolRoutes(typedNM.Routes)
|
|
protoNM.DNSConfig = ToProtocolDNSConfig(typedNM.DNSConfig, nil, dnsFwdPort)
|
|
|
|
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, localPeer.ProxyMeta.Embedded)
|
|
|
|
firewallRules := ToProtocolFirewallRules(typedNM.FirewallRules, includeIPv6, useSourcePrefixes)
|
|
protoNM.FirewallRules = firewallRules
|
|
protoNM.FirewallRulesIsEmpty = len(firewallRules) == 0
|
|
|
|
routesFirewallRules := ToProtocolRoutesFirewallRules(typedNM.RoutesFirewallRules)
|
|
protoNM.RoutesFirewallRules = routesFirewallRules
|
|
protoNM.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
|
|
|
|
if typedNM.AuthorizedUsers != nil {
|
|
hashedUsers, machineUsers := BuildAuthorizedUsersProto(ctx, typedNM.AuthorizedUsers)
|
|
userIDClaim := ""
|
|
if full != nil {
|
|
userIDClaim = full.UserIdClaim
|
|
}
|
|
protoNM.SshAuth = &proto.SSHAuth{
|
|
AuthorizedUsers: hashedUsers,
|
|
MachineUsers: machineUsers,
|
|
UserIDClaim: userIDClaim,
|
|
}
|
|
}
|
|
|
|
if typedNM.ForwardingRules != nil {
|
|
forwardingRules := make([]*proto.ForwardingRule, 0, len(typedNM.ForwardingRules))
|
|
for _, rule := range typedNM.ForwardingRules {
|
|
forwardingRules = append(forwardingRules, rule.ToProto())
|
|
}
|
|
protoNM.ForwardingRules = forwardingRules
|
|
}
|
|
|
|
// Merge the proxy patch the server attached. Mirrors the legacy
|
|
// NetworkMap.Merge step that the server runs after Calculate().
|
|
if full != nil && full.ProxyPatch != nil {
|
|
mergeProxyPatch(protoNM, full.ProxyPatch)
|
|
}
|
|
|
|
return &EnvelopeResult{
|
|
NetworkMap: protoNM,
|
|
Components: components,
|
|
}, nil
|
|
}
|
|
|
|
// mergeProxyPatch folds a ProxyPatch's pre-expanded fragments into the
|
|
// proto.NetworkMap that Calculate() produced. Mirrors types.NetworkMap.Merge
|
|
// — same six collections, deduplicated where the legacy merge dedupes.
|
|
func mergeProxyPatch(nm *proto.NetworkMap, patch *proto.ProxyPatch) {
|
|
nm.RemotePeers = appendUniquePeers(nm.RemotePeers, patch.Peers)
|
|
nm.OfflinePeers = appendUniquePeers(nm.OfflinePeers, patch.OfflinePeers)
|
|
nm.FirewallRules = append(nm.FirewallRules, patch.FirewallRules...)
|
|
nm.Routes = append(nm.Routes, patch.Routes...)
|
|
nm.RoutesFirewallRules = append(nm.RoutesFirewallRules, patch.RouteFirewallRules...)
|
|
nm.ForwardingRules = append(nm.ForwardingRules, patch.ForwardingRules...)
|
|
if len(nm.RemotePeers) > 0 {
|
|
nm.RemotePeersIsEmpty = false
|
|
}
|
|
if len(nm.FirewallRules) > 0 {
|
|
nm.FirewallRulesIsEmpty = false
|
|
}
|
|
if len(nm.RoutesFirewallRules) > 0 {
|
|
nm.RoutesFirewallRulesIsEmpty = false
|
|
}
|
|
}
|
|
|
|
// appendUniquePeers dedupes by WgPubKey — mirrors legacy
|
|
// mergeUniquePeersByID's intent (legacy keyed off Peer.ID; in proto form the
|
|
// closest stable identifier is WgPubKey).
|
|
func appendUniquePeers(dst, extra []*proto.RemotePeerConfig) []*proto.RemotePeerConfig {
|
|
if len(extra) == 0 {
|
|
return dst
|
|
}
|
|
seen := make(map[string]struct{}, len(dst))
|
|
for _, p := range dst {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
seen[p.WgPubKey] = struct{}{}
|
|
}
|
|
for _, p := range extra {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
if _, ok := seen[p.WgPubKey]; ok {
|
|
continue
|
|
}
|
|
seen[p.WgPubKey] = struct{}{}
|
|
dst = append(dst, p)
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func trimKey(s string) string {
|
|
if len(s) > 12 {
|
|
return s[:12]
|
|
}
|
|
return s
|
|
}
|
|
|
|
// canonicalizeWgKey normalises a base64-encoded WireGuard public key so it
|
|
// matches the canonical encoding emitted by the envelope decoder. Returns
|
|
// the input unchanged when it does not decode to 32 raw bytes (caller will
|
|
// hit a miss in the peer map and surface the error).
|
|
func canonicalizeWgKey(s string) string {
|
|
raw, err := base64.StdEncoding.DecodeString(s)
|
|
if err != nil || len(raw) != 32 {
|
|
return s
|
|
}
|
|
return base64.StdEncoding.EncodeToString(raw)
|
|
}
|