From 7048b8793196b78961800617f113b518644a1e43 Mon Sep 17 00:00:00 2001 From: crn4 Date: Fri, 29 May 2026 13:50:06 +0200 Subject: [PATCH] fix wasm bin filesize - type aliases --- client/internal/engine.go | 2 +- management/server/types/account.go | 156 +----------------- management/server/types/account_components.go | 2 +- management/server/types/account_test.go | 2 +- management/server/types/aliases.go | 142 ++++++++++++++++ shared/management/networkmap/decode.go | 29 ++-- shared/management/networkmap/encode.go | 2 +- shared/management/networkmap/envelope.go | 2 +- shared/management/networkmap/envelope_test.go | 31 ++++ .../management}/types/dns_settings.go | 0 shared/management/types/firewall_helpers.go | 131 +++++++++++++++ .../management}/types/firewall_rule.go | 4 +- .../management}/types/firewall_rule_test.go | 12 +- .../management}/types/group.go | 0 .../management}/types/network.go | 0 .../management}/types/network_test.go | 0 .../types/networkmap_components.go | 42 ++--- .../types/networkmap_components_compact.go | 0 .../management}/types/policy.go | 0 .../management}/types/policyrule.go | 0 .../management}/types/resource.go | 0 .../management}/types/route_firewall_rule.go | 0 22 files changed, 362 insertions(+), 195 deletions(-) create mode 100644 management/server/types/aliases.go rename {management/server => shared/management}/types/dns_settings.go (100%) create mode 100644 shared/management/types/firewall_helpers.go rename {management/server => shared/management}/types/firewall_rule.go (97%) rename {management/server => shared/management}/types/firewall_rule_test.go (92%) rename {management/server => shared/management}/types/group.go (100%) rename {management/server => shared/management}/types/network.go (100%) rename {management/server => shared/management}/types/network_test.go (100%) rename {management/server => shared/management}/types/networkmap_components.go (96%) rename {management/server => shared/management}/types/networkmap_components_compact.go (100%) rename {management/server => shared/management}/types/policy.go (100%) rename {management/server => shared/management}/types/policyrule.go (100%) rename {management/server => shared/management}/types/resource.go (100%) rename {management/server => shared/management}/types/route_firewall_rule.go (100%) diff --git a/client/internal/engine.go b/client/internal/engine.go index d44e16619..fa7f3730d 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -61,7 +61,7 @@ import ( cProto "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/system" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/server/types" + types "github.com/netbirdio/netbird/shared/management/types" "github.com/netbirdio/netbird/route" mgm "github.com/netbirdio/netbird/shared/management/client" "github.com/netbirdio/netbird/shared/management/domain" diff --git a/management/server/types/account.go b/management/server/types/account.go index 5328d34ae..ed42a4872 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -42,26 +42,8 @@ const ( PrivateCategory = "private" UnknownCategory = "unknown" - // firewallRuleMinPortRangesVer defines the minimum peer version that supports port range rules. - firewallRuleMinPortRangesVer = "0.48.0" - // firewallRuleMinNativeSSHVer defines the minimum peer version that supports native SSH features in the firewall rules. - firewallRuleMinNativeSSHVer = "0.60.0" - - // nativeSSHPortString defines the default port number as a string used for native SSH connections; this port is used by clients when hijacking ssh connections. - nativeSSHPortString = "22022" - nativeSSHPortNumber = 22022 - // defaultSSHPortString defines the standard SSH port number as a string, commonly used for default SSH connections. - defaultSSHPortString = "22" - defaultSSHPortNumber = 22 ) -type supportedFeatures struct { - nativeSSH bool - portRanges bool -} - -type LookupMap map[string]struct{} - // AccountMeta is a struct that contains a stripped down version of the Account object. // It doesn't carry any peers, groups, policies, or routes, etc. Just some metadata (e.g. ID, created by, created at, etc). type AccountMeta struct { @@ -1037,7 +1019,7 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P default: authorizedUsers[auth.Wildcard] = a.getAllowedUserIDs() } - } else if peerInDestinations && policyRuleImpliesLegacySSH(rule) && peer.SSHEnabled { + } else if peerInDestinations && PolicyRuleImpliesLegacySSH(rule) && peer.SSHEnabled { sshEnabled = true authorizedUsers[auth.Wildcard] = a.getAllowedUserIDs() } @@ -1103,15 +1085,15 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 { rules = append(rules, &fr) } else { - rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...) + rules = append(rules, ExpandPortsAndRanges(fr, rule, targetPeer)...) } - rules = appendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, firewallRuleContext{ - direction: direction, - dirStr: strconv.Itoa(direction), - protocolStr: string(protocol), - actionStr: string(rule.Action), - portsJoined: strings.Join(rule.Ports, ","), + rules = AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, FirewallRuleContext{ + Direction: direction, + DirStr: strconv.Itoa(direction), + ProtocolStr: string(protocol), + ActionStr: string(rule.Action), + PortsJoined: strings.Join(rule.Ports, ","), }) } }, func() ([]*nbpeer.Peer, []*FirewallRule) { @@ -1119,37 +1101,6 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer } } -// PolicyRuleImpliesLegacySSH reports whether the rule (without an explicit -// NetbirdSSH protocol) implicitly authorises SSH because it permits TCP/22 or -// TCP/22022 — either by ALL-protocol coverage or by an explicit port/port-range -// containing one of those. Exposed for ToComponentSyncResponse so the -// envelope-format response mirrors the legacy SshConfig.SshEnabled bit. -func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool { - return policyRuleImpliesLegacySSH(rule) -} - -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 -} - // getAllPeersFromGroups for given peer ID and list of groups // // Returns a list of peers from specified groups that pass specified posture checks @@ -1249,7 +1200,7 @@ func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, poli } rulePeers := a.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers, validatedPeersMap) - rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6) + rules := GenerateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6) fwRules = append(fwRules, rules...) } } @@ -1742,95 +1693,6 @@ func (a *Account) createProxyPolicy(svc *service.Service, target *service.Target } } -// expandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules -func expandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule { - features := peerSupportedFirewallFeatures(peer.Meta.WtVersion) - - var expanded []*FirewallRule - - for _, port := range rule.Ports { - fr := base - fr.Port = port - expanded = append(expanded, &fr) - } - - for _, portRange := range rule.PortRanges { - // prefer PolicyRule.Ports - if len(rule.Ports) > 0 { - break - } - fr := base - - if features.portRanges { - fr.PortRange = portRange - } else { - // Peer doesn't support port ranges, only allow single-port ranges - if portRange.Start != portRange.End { - continue - } - fr.Port = strconv.FormatUint(uint64(portRange.Start), 10) - } - expanded = append(expanded, &fr) - } - - if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == PolicyRuleProtocolNetbirdSSH { - expanded = addNativeSSHRule(base, expanded) - } - - return expanded -} - -// addNativeSSHRule adds a native SSH rule (port 22022) to the expanded rules if the base rule has port 22 configured. -func addNativeSSHRule(base FirewallRule, expanded []*FirewallRule) []*FirewallRule { - shouldAdd := false - for _, fr := range expanded { - if isPortInRule(nativeSSHPortString, 22022, fr) { - return expanded - } - if isPortInRule(defaultSSHPortString, 22, fr) { - shouldAdd = true - } - } - if !shouldAdd { - return expanded - } - - fr := base - fr.Port = nativeSSHPortString - return append(expanded, &fr) -} - -func isPortInRule(portString string, portInt uint16, rule *FirewallRule) bool { - return rule.Port == portString || (rule.PortRange.Start <= portInt && portInt <= rule.PortRange.End) -} - -// shouldCheckRulesForNativeSSH determines whether specific policy rules should be checked for native SSH support. -// While users can add the nativeSSHPortString, we look for cases when they used port 22 and based on SSH enabled -// in both management and client, we indicate to add the native port. -func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *nbpeer.Peer) bool { - return supportsNative && peer.SSHEnabled && peer.Meta.Flags.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP -} - -// peerSupportedFirewallFeatures checks if the peer version supports port ranges. -func peerSupportedFirewallFeatures(peerVer string) supportedFeatures { - if strings.Contains(peerVer, "dev") { - return supportedFeatures{true, true} - } - - var features supportedFeatures - - meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinNativeSSHVer, peerVer) - features.nativeSSH = err == nil && meetMinVer - - if features.nativeSSH { - features.portRanges = true - } else { - meetMinVer, err = posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer) - features.portRanges = err == nil && meetMinVer - } - - return features -} // filterZoneRecordsForPeers filters DNS records to only include peers to connect. // AAAA records are excluded when the requesting peer lacks IPv6 capability. diff --git a/management/server/types/account_components.go b/management/server/types/account_components.go index a2bab49d4..4dd785273 100644 --- a/management/server/types/account_components.go +++ b/management/server/types/account_components.go @@ -477,7 +477,7 @@ func (a *Account) getPeersGroupsPoliciesRoutes( default: sshReqs.needAllowedUserIDs = true } - } else if policyRuleImpliesLegacySSH(rule) && peerSSHEnabled { + } else if PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled { sshReqs.needAllowedUserIDs = true } } diff --git a/management/server/types/account_test.go b/management/server/types/account_test.go index b55b41638..52fa3829e 100644 --- a/management/server/types/account_test.go +++ b/management/server/types/account_test.go @@ -700,7 +700,7 @@ func Test_ExpandPortsAndRanges_SSHRuleExpansion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := expandPortsAndRanges(tt.base, tt.rule, tt.peer) + result := ExpandPortsAndRanges(tt.base, tt.rule, tt.peer) var ports []string for _, fr := range result { diff --git a/management/server/types/aliases.go b/management/server/types/aliases.go new file mode 100644 index 000000000..949bd7432 --- /dev/null +++ b/management/server/types/aliases.go @@ -0,0 +1,142 @@ +package types + +import ( + "context" + "math/rand" + "net" + "net/netip" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" + nbroute "github.com/netbirdio/netbird/route" + sharedtypes "github.com/netbirdio/netbird/shared/management/types" +) + +// Type aliases for types relocated to shared/management/types so that the +// client-side compute path can depend on them + +type DNSSettings = sharedtypes.DNSSettings + +type FirewallRule = sharedtypes.FirewallRule + +type Group = sharedtypes.Group +type GroupPeer = sharedtypes.GroupPeer + +type Network = sharedtypes.Network +type NetworkMap = sharedtypes.NetworkMap +type ForwardingRule = sharedtypes.ForwardingRule + +type Policy = sharedtypes.Policy +type PolicyUpdateOperation = sharedtypes.PolicyUpdateOperation + +type PolicyRule = sharedtypes.PolicyRule +type PolicyUpdateOperationType = sharedtypes.PolicyUpdateOperationType +type PolicyTrafficActionType = sharedtypes.PolicyTrafficActionType +type PolicyRuleProtocolType = sharedtypes.PolicyRuleProtocolType +type PolicyRuleDirection = sharedtypes.PolicyRuleDirection +type RulePortRange = sharedtypes.RulePortRange + +type Resource = sharedtypes.Resource +type ResourceType = sharedtypes.ResourceType + +type RouteFirewallRule = sharedtypes.RouteFirewallRule + +type NetworkMapComponents = sharedtypes.NetworkMapComponents +type AccountSettingsInfo = sharedtypes.AccountSettingsInfo + +type GroupCompact = sharedtypes.GroupCompact +type NetworkMapComponentsCompact = sharedtypes.NetworkMapComponentsCompact + +type LookupMap = sharedtypes.LookupMap +type FirewallRuleContext = sharedtypes.FirewallRuleContext + +const ( + GroupIssuedAPI = sharedtypes.GroupIssuedAPI + GroupIssuedJWT = sharedtypes.GroupIssuedJWT + GroupIssuedIntegration = sharedtypes.GroupIssuedIntegration + GroupAllName = sharedtypes.GroupAllName +) + +// Function forwarders preserve types.X(...) call sites that previously +// resolved to package-local funcs. Plain forwarders (not var aliases) keep +// the symbol immutable and allow the inliner to flatten the call. + +func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool { + return sharedtypes.PolicyRuleImpliesLegacySSH(rule) +} + +func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule { + return sharedtypes.ExpandPortsAndRanges(base, rule, peer) +} + +func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule { + return sharedtypes.AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, rc) +} + +func CalculateNetworkMapFromComponents(ctx context.Context, components *NetworkMapComponents) *NetworkMap { + return sharedtypes.CalculateNetworkMapFromComponents(ctx, components) +} + +func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule { + return sharedtypes.GenerateRouteFirewallRules(ctx, route, rule, groupPeers, direction, includeIPv6) +} + +func AllocateIPv6Subnet(r *rand.Rand) net.IPNet { + return sharedtypes.AllocateIPv6Subnet(r) +} + +func NewNetwork() *Network { + return sharedtypes.NewNetwork() +} + +func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, error) { + return sharedtypes.AllocatePeerIP(prefix, takenIps) +} + +func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) { + return sharedtypes.AllocateRandomPeerIP(prefix) +} + +func AllocateRandomPeerIPv6(prefix netip.Prefix) (netip.Addr, error) { + return sharedtypes.AllocateRandomPeerIPv6(prefix) +} + +func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) { + return sharedtypes.ParseRuleString(rule) +} + +const ( + FirewallRuleDirectionIN = sharedtypes.FirewallRuleDirectionIN + FirewallRuleDirectionOUT = sharedtypes.FirewallRuleDirectionOUT +) + +const ( + ResourceTypePeer = sharedtypes.ResourceTypePeer + ResourceTypeDomain = sharedtypes.ResourceTypeDomain + ResourceTypeHost = sharedtypes.ResourceTypeHost + ResourceTypeSubnet = sharedtypes.ResourceTypeSubnet +) + +const ( + PolicyTrafficActionAccept = sharedtypes.PolicyTrafficActionAccept + PolicyTrafficActionDrop = sharedtypes.PolicyTrafficActionDrop +) + +const ( + PolicyRuleProtocolALL = sharedtypes.PolicyRuleProtocolALL + PolicyRuleProtocolTCP = sharedtypes.PolicyRuleProtocolTCP + PolicyRuleProtocolUDP = sharedtypes.PolicyRuleProtocolUDP + PolicyRuleProtocolICMP = sharedtypes.PolicyRuleProtocolICMP + PolicyRuleProtocolNetbirdSSH = sharedtypes.PolicyRuleProtocolNetbirdSSH +) + +const ( + PolicyRuleFlowDirect = sharedtypes.PolicyRuleFlowDirect + PolicyRuleFlowBidirect = sharedtypes.PolicyRuleFlowBidirect +) + +const ( + DefaultRuleName = sharedtypes.DefaultRuleName + DefaultRuleDescription = sharedtypes.DefaultRuleDescription + DefaultPolicyName = sharedtypes.DefaultPolicyName + DefaultPolicyDescription = sharedtypes.DefaultPolicyDescription +) diff --git a/shared/management/networkmap/decode.go b/shared/management/networkmap/decode.go index 96d71f0db..73ff30809 100644 --- a/shared/management/networkmap/decode.go +++ b/shared/management/networkmap/decode.go @@ -8,14 +8,16 @@ import ( "strconv" "time" + log "github.com/sirupsen/logrus" + nbdns "github.com/netbirdio/netbird/dns" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/types" nbroute "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/proto" + "github.com/netbirdio/netbird/shared/management/types" ) // DecodeEnvelope converts a NetworkMapEnvelope into a NetworkMapComponents @@ -77,16 +79,22 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, // wire; we re-key by the peer's WireGuard public key (base64) so the // in-memory components struct uses a stable identifier across // snapshots. peerIDByIndex lets downstream phases resolve wire indexes - // back to that key. + // back to that key. A peer with a missing or malformed wg_pub_key is + // skipped (and its index keeps "" so any cross-reference falls into the + // same missing-peer branch downstream) — matches legacy behaviour, which + // degrades gracefully rather than aborting the whole sync on a single + // bad row. peerIDByIndex := make([]string, len(full.Peers)) for idx, pc := range full.Peers { if pc == nil { - return nil, fmt.Errorf("invalid envelope: peers[%d] is nil", idx) + log.Warnf("envelope: peers[%d] is nil, skipping", idx) + continue } - if len(pc.WgPubKey) == 0 { - return nil, fmt.Errorf("invalid envelope: peers[%d] missing wg_pub_key", idx) + if len(pc.WgPubKey) != 32 { + log.Warnf("envelope: peers[%d] wg_pub_key length %d (want 32), skipping", idx, len(pc.WgPubKey)) + continue } - peerID := encodeWgKeyBase64(pc.WgPubKey) + peerID := base64.StdEncoding.EncodeToString(pc.WgPubKey) peer := decodePeerCompact(pc, peerID, full.AgentVersions) c.Peers[peerID] = peer peerIDByIndex[idx] = peerID @@ -273,7 +281,7 @@ func decodePeerCompact(pc *proto.PeerCompact, peerID string, agentVersions []str } peer := &nbpeer.Peer{ ID: peerID, - Key: encodeWgKeyBase64(pc.WgPubKey), + Key: peerID, SSHKey: string(pc.SshPubKey), SSHEnabled: pc.SshEnabled, DNSLabel: pc.DnsLabel, @@ -568,13 +576,6 @@ func protocolFromProto(p proto.RuleProtocol) types.PolicyRuleProtocolType { } } -func encodeWgKeyBase64(raw []byte) string { - if len(raw) != 32 { - return "" - } - return base64.StdEncoding.EncodeToString(raw) -} - func lookupAgentVersion(table []string, idx uint32) string { if int(idx) < len(table) { return table[idx] diff --git a/shared/management/networkmap/encode.go b/shared/management/networkmap/encode.go index 1b1c3380e..e808480ea 100644 --- a/shared/management/networkmap/encode.go +++ b/shared/management/networkmap/encode.go @@ -21,7 +21,7 @@ import ( "net/netip" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/types" nbroute "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/netiputil" diff --git a/shared/management/networkmap/envelope.go b/shared/management/networkmap/envelope.go index b61d85bbe..642391e7b 100644 --- a/shared/management/networkmap/envelope.go +++ b/shared/management/networkmap/envelope.go @@ -5,7 +5,7 @@ import ( "encoding/base64" "fmt" - "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/types" "github.com/netbirdio/netbird/shared/management/proto" ) diff --git a/shared/management/networkmap/envelope_test.go b/shared/management/networkmap/envelope_test.go index f0c40d67d..666e86cee 100644 --- a/shared/management/networkmap/envelope_test.go +++ b/shared/management/networkmap/envelope_test.go @@ -97,6 +97,37 @@ func TestEnvelopeToNetworkMap_FullPayloadMissing(t *testing.T) { require.Error(t, err, "envelope with no Full payload must produce an error") } +// TestDecodeEnvelope_MalformedWgKeyPeerSkipped feeds an envelope where one +// peer has a wg_pub_key that is not 32 bytes long. The decoder must skip +// that peer (keeping the rest of the snapshot usable) instead of aborting +// the whole sync — mirrors legacy behaviour that tolerates an occasional +// bad row. +func TestDecodeEnvelope_MalformedWgKeyPeerSkipped(t *testing.T) { + c, localPeerKey := buildSmokeComponents(t) + envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{ + Components: c, + DNSDomain: "netbird.cloud", + }) + require.NotNil(t, envelope.GetFull()) + + full := envelope.GetFull() + require.Len(t, full.Peers, 2, "smoke fixture should have two peers") + + // Truncate the second peer's wg_pub_key so it fails the length gate. + full.Peers[1].WgPubKey = full.Peers[1].WgPubKey[:31] + + wire, err := goproto.Marshal(envelope) + require.NoError(t, err, "marshal envelope") + var decoded proto.NetworkMapEnvelope + require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope") + + result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud") + require.NoError(t, err, "EnvelopeToNetworkMap must tolerate one bad peer key") + require.NotNil(t, result) + require.NotNil(t, result.Components) + require.Len(t, result.Components.Peers, 1, "the well-formed peer survives, the malformed one is dropped") +} + // buildSmokeComponents returns a minimal NetworkMapComponents (2 peers, 1 // group, 1 allow policy) plus the receiving peer's WG public key. Sufficient // to validate the encode → marshal → decode → Calculate pipeline produces diff --git a/management/server/types/dns_settings.go b/shared/management/types/dns_settings.go similarity index 100% rename from management/server/types/dns_settings.go rename to shared/management/types/dns_settings.go diff --git a/shared/management/types/firewall_helpers.go b/shared/management/types/firewall_helpers.go new file mode 100644 index 000000000..df2bf14ea --- /dev/null +++ b/shared/management/types/firewall_helpers.go @@ -0,0 +1,131 @@ +package types + +import ( + "strconv" + "strings" + + "github.com/netbirdio/netbird/management/server/posture" + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + +const ( + firewallRuleMinPortRangesVer = "0.48.0" + firewallRuleMinNativeSSHVer = "0.60.0" + + nativeSSHPortString = "22022" + nativeSSHPortNumber = 22022 + defaultSSHPortString = "22" + defaultSSHPortNumber = 22 +) + +type supportedFeatures struct { + nativeSSH bool + portRanges bool +} + +type LookupMap map[string]struct{} + +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 +} + +// ExpandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules. +func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule { + features := peerSupportedFirewallFeatures(peer.Meta.WtVersion) + + var expanded []*FirewallRule + + for _, port := range rule.Ports { + fr := base + fr.Port = port + expanded = append(expanded, &fr) + } + + for _, portRange := range rule.PortRanges { + if len(rule.Ports) > 0 { + break + } + fr := base + + if features.portRanges { + fr.PortRange = portRange + } else { + if portRange.Start != portRange.End { + continue + } + fr.Port = strconv.FormatUint(uint64(portRange.Start), 10) + } + expanded = append(expanded, &fr) + } + + if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == PolicyRuleProtocolNetbirdSSH { + expanded = addNativeSSHRule(base, expanded) + } + + return expanded +} + +func addNativeSSHRule(base FirewallRule, expanded []*FirewallRule) []*FirewallRule { + shouldAdd := false + for _, fr := range expanded { + if isPortInRule(nativeSSHPortString, 22022, fr) { + return expanded + } + if isPortInRule(defaultSSHPortString, 22, fr) { + shouldAdd = true + } + } + if !shouldAdd { + return expanded + } + + fr := base + fr.Port = nativeSSHPortString + return append(expanded, &fr) +} + +func isPortInRule(portString string, portInt uint16, rule *FirewallRule) bool { + return rule.Port == portString || (rule.PortRange.Start <= portInt && portInt <= rule.PortRange.End) +} + +func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *nbpeer.Peer) bool { + return supportsNative && peer.SSHEnabled && peer.Meta.Flags.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP +} + +func peerSupportedFirewallFeatures(peerVer string) supportedFeatures { + if strings.Contains(peerVer, "dev") { + return supportedFeatures{true, true} + } + + var features supportedFeatures + + meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinNativeSSHVer, peerVer) + features.nativeSSH = err == nil && meetMinVer + + if features.nativeSSH { + features.portRanges = true + } else { + meetMinVer, err = posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer) + features.portRanges = err == nil && meetMinVer + } + + return features +} diff --git a/management/server/types/firewall_rule.go b/shared/management/types/firewall_rule.go similarity index 97% rename from management/server/types/firewall_rule.go rename to shared/management/types/firewall_rule.go index b76a94290..87dcfe307 100644 --- a/management/server/types/firewall_rule.go +++ b/shared/management/types/firewall_rule.go @@ -47,11 +47,11 @@ func (r *FirewallRule) Equal(other *FirewallRule) bool { return reflect.DeepEqual(r, other) } -// generateRouteFirewallRules generates a list of firewall rules for a given route. +// GenerateRouteFirewallRules generates a list of firewall rules for a given route. // For static routes, source ranges match the destination family (v4 or v6). // For dynamic routes (domain-based), separate v4 and v6 rules are generated // so the routing peer's forwarding chain allows both address families. -func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule { +func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule { rulesExists := make(map[string]struct{}) rules := make([]*RouteFirewallRule, 0) diff --git a/management/server/types/firewall_rule_test.go b/shared/management/types/firewall_rule_test.go similarity index 92% rename from management/server/types/firewall_rule_test.go rename to shared/management/types/firewall_rule_test.go index 8d97a46bc..9de4ca04a 100644 --- a/management/server/types/firewall_rule_test.go +++ b/shared/management/types/firewall_rule_test.go @@ -57,7 +57,7 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) { Protocol: PolicyRuleProtocolALL, } - rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) + rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) require.Len(t, rules, 1) assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges, "v4 route should only have v4 sources") @@ -86,7 +86,7 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) { Protocol: PolicyRuleProtocolALL, } - rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) + rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) require.Len(t, rules, 1) assert.Equal(t, []string{"fd00::1/128"}, rules[0].SourceRanges, "v6 route should only have v6 sources") @@ -115,7 +115,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) { Protocol: PolicyRuleProtocolALL, } - rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) + rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) require.Len(t, rules, 2, "dynamic route should produce both v4 and v6 rules") assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges) @@ -143,7 +143,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) { Protocol: PolicyRuleProtocolALL, } - rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) + rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) require.Len(t, rules, 1, "no v6 peers means only v4 rule") assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges) @@ -173,7 +173,7 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) { Protocol: PolicyRuleProtocolALL, } - rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false) + rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false) assert.Empty(t, rules, "v6 route should produce no rules when includeIPv6 is false") }) @@ -190,7 +190,7 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) { Protocol: PolicyRuleProtocolALL, } - rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false) + rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false) require.Len(t, rules, 1, "dynamic route with includeIPv6=false should produce only v4 rule") assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges) }) diff --git a/management/server/types/group.go b/shared/management/types/group.go similarity index 100% rename from management/server/types/group.go rename to shared/management/types/group.go diff --git a/management/server/types/network.go b/shared/management/types/network.go similarity index 100% rename from management/server/types/network.go rename to shared/management/types/network.go diff --git a/management/server/types/network_test.go b/shared/management/types/network_test.go similarity index 100% rename from management/server/types/network_test.go rename to shared/management/types/network_test.go diff --git a/management/server/types/networkmap_components.go b/shared/management/types/networkmap_components.go similarity index 96% rename from management/server/types/networkmap_components.go rename to shared/management/types/networkmap_components.go index 008c04184..f341e9071 100644 --- a/management/server/types/networkmap_components.go +++ b/shared/management/types/networkmap_components.go @@ -263,7 +263,7 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ( default: authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs() } - } else if peerInDestinations && policyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled { + } else if peerInDestinations && PolicyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled { sshEnabled = true authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs() } @@ -330,15 +330,15 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nbpeer.Peer) ( if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 { rules = append(rules, &fr) } else { - rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...) + rules = append(rules, ExpandPortsAndRanges(fr, rule, targetPeer)...) } - rules = appendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, firewallRuleContext{ - direction: direction, - dirStr: dirStr, - protocolStr: protocolStr, - actionStr: actionStr, - portsJoined: portsJoined, + rules = AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, FirewallRuleContext{ + Direction: direction, + DirStr: dirStr, + ProtocolStr: protocolStr, + ActionStr: actionStr, + PortsJoined: portsJoined, }) } }, func() ([]*nbpeer.Peer, []*FirewallRule) { @@ -691,7 +691,7 @@ func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID } rulePeers := c.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers) - rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6) + rules := GenerateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6) fwRules = append(fwRules, rules...) } } @@ -960,21 +960,21 @@ func (c *NetworkMapComponents) addNetworksRoutingPeers( return peersToConnect } -type firewallRuleContext struct { - direction int - dirStr string - protocolStr string - actionStr string - portsJoined string +type FirewallRuleContext struct { + Direction int + DirStr string + ProtocolStr string + ActionStr string + PortsJoined string } -func appendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc firewallRuleContext) []*FirewallRule { +func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule { if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6() || !targetPeer.IPv6.IsValid() { return rules } v6IP := peer.IPv6.String() - v6RuleID := rule.ID + v6IP + rc.dirStr + rc.protocolStr + rc.actionStr + rc.portsJoined + v6RuleID := rule.ID + v6IP + rc.DirStr + rc.ProtocolStr + rc.ActionStr + rc.PortsJoined if _, ok := rulesExists[v6RuleID]; ok { return rules } @@ -983,12 +983,12 @@ func appendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct v6fr := FirewallRule{ PolicyID: rule.ID, PeerIP: v6IP, - Direction: rc.direction, - Action: rc.actionStr, - Protocol: rc.protocolStr, + Direction: rc.Direction, + Action: rc.ActionStr, + Protocol: rc.ProtocolStr, } if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 { return append(rules, &v6fr) } - return append(rules, expandPortsAndRanges(v6fr, rule, targetPeer)...) + return append(rules, ExpandPortsAndRanges(v6fr, rule, targetPeer)...) } diff --git a/management/server/types/networkmap_components_compact.go b/shared/management/types/networkmap_components_compact.go similarity index 100% rename from management/server/types/networkmap_components_compact.go rename to shared/management/types/networkmap_components_compact.go diff --git a/management/server/types/policy.go b/shared/management/types/policy.go similarity index 100% rename from management/server/types/policy.go rename to shared/management/types/policy.go diff --git a/management/server/types/policyrule.go b/shared/management/types/policyrule.go similarity index 100% rename from management/server/types/policyrule.go rename to shared/management/types/policyrule.go diff --git a/management/server/types/resource.go b/shared/management/types/resource.go similarity index 100% rename from management/server/types/resource.go rename to shared/management/types/resource.go diff --git a/management/server/types/route_firewall_rule.go b/shared/management/types/route_firewall_rule.go similarity index 100% rename from management/server/types/route_firewall_rule.go rename to shared/management/types/route_firewall_rule.go