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. // - 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 and convert it to the wire form the // engine consumes. // // 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, } } return &EnvelopeResult{ NetworkMap: protoNM, Components: components, }, nil } 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) }