mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-12 16:39:09 +02:00
Pure plumbing, no change on the wire. Both halves exist for the same reason: the value this PR needs to publish is not reachable from the code paths that have to publish it, and widening the signatures is separable from moving the value. On the management side, the resolution between the account setting and the server-config default moves into resolveDebugUploadURL, and the default becomes a parameter that ToSyncResponse and ToComponentSyncResponse take rather than something read off the server config at the point of use. The callers that have a server config pass its value, which is what the code read before, so every response keeps the destination it had. The peer fan-out builds its sync with a nil server config, so anything derived from that config inside the conversion is unavailable there; a parameter is reachable from the fan-out. On the client side, Engine.Start takes the login response's PeerConfig. Nothing reads it yet.
201 lines
8.0 KiB
Go
201 lines
8.0 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
|
|
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
|
|
|
"github.com/netbirdio/netbird/client/ssh/auth"
|
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc"
|
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
|
nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
)
|
|
|
|
// ToComponentSyncResponse builds a SyncResponse carrying the compact
|
|
// NetworkMapEnvelope for capability-aware peers. The legacy proto.NetworkMap
|
|
// field is intentionally left empty — capable peers ignore it and the
|
|
// envelope alone is the authoritative wire shape.
|
|
//
|
|
// PeerConfig is computed once server-side using the receiving peer's own
|
|
// account-level network metadata. EnableSSH inside PeerConfig is left at
|
|
// peer.SSHEnabled (the peer's local setting); account-policy-driven SSH is
|
|
// computed by the client from the envelope's GroupIDToUserIDs / AllowedUserIDs
|
|
// inside Calculate(), so the SshConfig.SshEnabled bit may flip true on the
|
|
// client even though the server-side PeerConfig reports false.
|
|
func ToComponentSyncResponse(
|
|
ctx context.Context,
|
|
config *nbconfig.Config,
|
|
httpConfig *nbconfig.HttpServerConfig,
|
|
deviceFlowConfig *nbconfig.DeviceAuthorizationFlow,
|
|
peer *nmdata.Peer,
|
|
turnCredentials *Token,
|
|
relayCredentials *Token,
|
|
components *types.NetworkMapComponents,
|
|
proxyPatch *types.NetworkMap,
|
|
dnsName string,
|
|
checks []*nmdata.PostureChecks,
|
|
settings *nmdata.AccountSettingsInfo,
|
|
extraSettings *types.ExtraSettings,
|
|
peerGroups []string,
|
|
dnsFwdPort int64,
|
|
debugUploadDefault string,
|
|
) *proto.SyncResponse {
|
|
//
|
|
// 'component' parameter is expected to never be nil
|
|
// 'peer' parameter is expected to never be nil
|
|
//
|
|
// TODO (dmitri) consider using invariants?
|
|
//
|
|
enableSSH := computeSSHEnabledForPeer(components, peer)
|
|
peerConfig := toPeerConfig(peer, components.Network, dnsName, settings, httpConfig, deviceFlowConfig, enableSSH, components.ForceRoutingPeerDNSResolution)
|
|
|
|
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
|
|
useSourcePrefixes := peer.SupportsSourcePrefixes()
|
|
|
|
userIDClaim := auth.DefaultUserIDClaim
|
|
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
|
userIDClaim = httpConfig.AuthUserIDClaim
|
|
}
|
|
|
|
envelope := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
|
Components: components,
|
|
PeerConfig: peerConfig,
|
|
DNSDomain: dnsName,
|
|
DNSForwarderPort: dnsFwdPort,
|
|
UserIDClaim: userIDClaim,
|
|
ProxyPatch: toProxyPatch(proxyPatch, dnsName, includeIPv6, useSourcePrefixes, peer.ProxyMeta.Embedded),
|
|
})
|
|
|
|
resp := &proto.SyncResponse{
|
|
PeerConfig: peerConfig,
|
|
NetworkMapEnvelope: envelope,
|
|
Checks: toProtocolChecks(ctx, checks),
|
|
Version: int32(sharedgrpc.ComponentNetworkMap),
|
|
}
|
|
|
|
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings, settings, debugUploadDefault)
|
|
resp.NetbirdConfig = integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings)
|
|
|
|
// settings == nil → field stays nil → "no info in this snapshot", client
|
|
// preserves the deadline it already had. settings non-nil → emit either a
|
|
// valid deadline or the explicit-zero "disabled" sentinel via
|
|
// encodeSessionExpiresAt.
|
|
if settings != nil {
|
|
resp.SessionExpiresAt = encodeSessionExpiresAt(
|
|
peer.SessionExpiresAt(settings.PeerLoginExpirationEnabled, settings.PeerLoginExpiration),
|
|
)
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
// toProxyPatch converts a proxy-injected *types.NetworkMap into the wire
|
|
// patch the components envelope ships alongside. Returns nil when there are
|
|
// no fragments to merge — proto3 omits a nil message field, so the receiver
|
|
// sees no patch and skips the merge step entirely.
|
|
//
|
|
// We reuse the legacy proto-conversion helpers (toProtocolRoutes,
|
|
// toProtocolFirewallRules, toProtocolRoutesFirewallRules,
|
|
// appendRemotePeerConfig, ForwardingRule.ToProto) because the proxy
|
|
// delivers fragments pre-expanded — there's no raw component shape to
|
|
// derive them from. Components purity isn't violated: proxy data isn't
|
|
// policy-graph-derived, it's externally injected post-Calculate, so the
|
|
// client merges it on top of its locally-computed NetworkMap.
|
|
func toProxyPatch(nm *types.NetworkMap, dnsName string, includeIPv6, useSourcePrefixes, localIsProxy bool) *proto.ProxyPatch {
|
|
if nm == nil {
|
|
return nil
|
|
}
|
|
if len(nm.Peers) == 0 && len(nm.OfflinePeers) == 0 && len(nm.FirewallRules) == 0 &&
|
|
len(nm.Routes) == 0 && len(nm.RoutesFirewallRules) == 0 && len(nm.ForwardingRules) == 0 {
|
|
return nil
|
|
}
|
|
|
|
patch := &proto.ProxyPatch{
|
|
Peers: networkmap.AppendRemotePeerConfig(nil, nm.Peers, dnsName, includeIPv6, localIsProxy),
|
|
OfflinePeers: networkmap.AppendRemotePeerConfig(nil, nm.OfflinePeers, dnsName, includeIPv6, localIsProxy),
|
|
FirewallRules: networkmap.ToProtocolFirewallRules(nm.FirewallRules, includeIPv6, useSourcePrefixes),
|
|
Routes: networkmap.ToProtocolRoutes(nm.Routes),
|
|
RouteFirewallRules: networkmap.ToProtocolRoutesFirewallRules(nm.RoutesFirewallRules),
|
|
}
|
|
if len(nm.ForwardingRules) > 0 {
|
|
patch.ForwardingRules = make([]*proto.ForwardingRule, 0, len(nm.ForwardingRules))
|
|
for _, r := range nm.ForwardingRules {
|
|
patch.ForwardingRules = append(patch.ForwardingRules, r.ToProto())
|
|
}
|
|
}
|
|
return patch
|
|
}
|
|
|
|
// computeSSHEnabledForPeer mirrors the SSH-server-activation bit that
|
|
// Calculate() folds into NetworkMap.EnableSSH. Components-format peers
|
|
// receive a freshly-computed PeerConfig.SshConfig.SshEnabled at sync time;
|
|
// without this helper the field would be incorrectly false for any peer
|
|
// that's the destination of an SSH-enabling policy without having
|
|
// peer.SSHEnabled set locally.
|
|
//
|
|
// Mirrors the two activation paths Calculate() uses:
|
|
// 1. Explicit: rule.Protocol == NetbirdSSH and peer is in the rule's
|
|
// destinations.
|
|
// 2. Legacy implicit: rule covers TCP/22 or TCP/22022 (or ALL), peer is in
|
|
// destinations, AND the peer has SSHEnabled set locally — this is the
|
|
// "allow-all/TCP-22 implies SSH activation for SSH-capable peers" path.
|
|
//
|
|
// The full SSH AuthorizedUsers map is still produced by the client when it
|
|
// runs Calculate() over the envelope.
|
|
func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nmdata.Peer) bool {
|
|
if c == nil || peer == nil {
|
|
return false
|
|
}
|
|
// Mirror Calculate's `getAllPeersFromGroups` invariant: target peer must
|
|
// exist in c.Peers, otherwise no rule applies to it.
|
|
if _, ok := c.Peers[peer.ID]; !ok {
|
|
return false
|
|
}
|
|
for _, policy := range c.Policies {
|
|
if policy == nil || !policy.Enabled {
|
|
continue
|
|
}
|
|
for _, rule := range policy.Rules {
|
|
if ruleEnablesSSHForPeer(c, rule, peer) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ruleEnablesSSHForPeer returns true when rule is active, targets peer, and
|
|
// either explicitly authorises SSH or covers the legacy TCP/22 path while the
|
|
// peer itself has SSH enabled locally.
|
|
func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *nmdata.PolicyRule, peer *nmdata.Peer) bool {
|
|
if rule == nil || !rule.Enabled {
|
|
return false
|
|
}
|
|
if !peerInDestinations(c, rule, peer.ID) {
|
|
return false
|
|
}
|
|
if rule.Protocol == string(types.PolicyRuleProtocolNetbirdSSH) {
|
|
return true
|
|
}
|
|
return peer.SSHEnabled && nmdata.PolicyRuleImpliesLegacySSH(rule)
|
|
}
|
|
|
|
// peerInDestinations reports whether peerID is in any of rule.Destinations'
|
|
// groups (or matches DestinationResource if it's a peer-typed resource —
|
|
// for non-peer types Calculate falls through to group lookup, so we mirror
|
|
// that exactly to avoid silent divergence).
|
|
func peerInDestinations(c *types.NetworkMapComponents, rule *nmdata.PolicyRule, peerID string) bool {
|
|
if rule.DestinationResource.Type == string(types.ResourceTypePeer) && rule.DestinationResource.ID != "" {
|
|
return rule.DestinationResource.ID == peerID
|
|
}
|
|
for _, groupID := range rule.Destinations {
|
|
if c.IsPeerInGroup(peerID, groupID) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|