mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
[management] Add native ssh port rule on 22 (#4810)
Implements feature-aware firewall rule expansion: derives peer-supported features (native SSH, portRanges) from peer version, prefers explicit Ports over PortRanges when expanding, conditionally appends a native SSH (22022) rule when policy and peer support allow, and adds helpers plus tests for SSH expansion behavior.
This commit is contained in:
@@ -40,8 +40,20 @@ const (
|
||||
|
||||
// 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"
|
||||
// defaultSSHPortString defines the standard SSH port number as a string, commonly used for default SSH connections.
|
||||
defaultSSHPortString = "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.
|
||||
@@ -1650,22 +1662,24 @@ func (a *Account) AddAllGroup(disableDefaultPolicy bool) error {
|
||||
|
||||
// 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
|
||||
|
||||
if len(rule.Ports) > 0 {
|
||||
for _, port := range rule.Ports {
|
||||
fr := base
|
||||
fr.Port = port
|
||||
expanded = append(expanded, &fr)
|
||||
}
|
||||
return expanded
|
||||
for _, port := range rule.Ports {
|
||||
fr := base
|
||||
fr.Port = port
|
||||
expanded = append(expanded, &fr)
|
||||
}
|
||||
|
||||
supportPortRanges := peerSupportsPortRanges(peer.Meta.WtVersion)
|
||||
for _, portRange := range rule.PortRanges {
|
||||
// prefer PolicyRule.Ports
|
||||
if len(rule.Ports) > 0 {
|
||||
break
|
||||
}
|
||||
fr := base
|
||||
|
||||
if supportPortRanges {
|
||||
if features.portRanges {
|
||||
fr.PortRange = portRange
|
||||
} else {
|
||||
// Peer doesn't support port ranges, only allow single-port ranges
|
||||
@@ -1677,17 +1691,63 @@ func expandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer
|
||||
expanded = append(expanded, &fr)
|
||||
}
|
||||
|
||||
if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) {
|
||||
expanded = addNativeSSHRule(base, expanded)
|
||||
}
|
||||
|
||||
return expanded
|
||||
}
|
||||
|
||||
// peerSupportsPortRanges checks if the peer version supports port ranges.
|
||||
func peerSupportsPortRanges(peerVer string) bool {
|
||||
if strings.Contains(peerVer, "dev") {
|
||||
return true
|
||||
// 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
|
||||
}
|
||||
|
||||
meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer)
|
||||
return err == nil && meetMinVer
|
||||
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.
|
||||
|
||||
@@ -839,6 +839,272 @@ func Test_NetworksNetMapGenShouldExcludeOtherRouters(t *testing.T) {
|
||||
assert.Len(t, sourcePeers, 2, "expected source peers don't match")
|
||||
}
|
||||
|
||||
func Test_ExpandPortsAndRanges_SSHRuleExpansion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
peer *nbpeer.Peer
|
||||
rule *PolicyRule
|
||||
base FirewallRule
|
||||
expectedPorts []string
|
||||
}{
|
||||
{
|
||||
name: "adds port 22022 when SSH enabled on modern peer with port 22",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22", "22022"},
|
||||
},
|
||||
{
|
||||
name: "adds port 22022 once when port 22 is duplicated within policy",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22", "80", "22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22", "80", "22", "22022"},
|
||||
},
|
||||
{
|
||||
name: "does not add 22022 for peer with old version",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.59.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22"},
|
||||
},
|
||||
{
|
||||
name: "does not add 22022 when SSHEnabled is false",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: false,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22"},
|
||||
},
|
||||
{
|
||||
name: "does not add 22022 when ServerSSHAllowed is false",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: false},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22"},
|
||||
},
|
||||
{
|
||||
name: "does not add 22022 for UDP protocol",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolUDP,
|
||||
Ports: []string{"22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "udp"},
|
||||
expectedPorts: []string{"22"},
|
||||
},
|
||||
{
|
||||
name: "does not add 22022 when port 22 not in rule",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"80", "443"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"80", "443"},
|
||||
},
|
||||
{
|
||||
name: "does not duplicate 22022 when already present",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22", "22022"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22", "22022"},
|
||||
},
|
||||
{
|
||||
name: "does not duplicate 22022 when already within a port range",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
PortRanges: []RulePortRange{{Start: 20, End: 32000}},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"20-32000"},
|
||||
},
|
||||
{
|
||||
name: "adds 22022 when port 22 in port range",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
PortRanges: []RulePortRange{{Start: 20, End: 25}},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"20-25", "22022"},
|
||||
},
|
||||
{
|
||||
name: "adds single 22022 once when port 22 in multiple port ranges",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.60.0",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
PortRanges: []RulePortRange{{Start: 20, End: 25}, {Start: 10, End: 100}},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"20-25", "10-100", "22022"},
|
||||
},
|
||||
{
|
||||
name: "dev suffix version supports all features",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "0.50.0-dev",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22", "22022"},
|
||||
},
|
||||
{
|
||||
name: "dev suffix version supports all features",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "dev",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22", "22022"},
|
||||
},
|
||||
{
|
||||
name: "development suffix version supports all features",
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "peer1",
|
||||
SSHEnabled: true,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: "development",
|
||||
Flags: nbpeer.Flags{ServerSSHAllowed: true},
|
||||
},
|
||||
},
|
||||
rule: &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22"},
|
||||
},
|
||||
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
|
||||
expectedPorts: []string{"22", "22022"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := expandPortsAndRanges(tt.base, tt.rule, tt.peer)
|
||||
|
||||
var ports []string
|
||||
for _, fr := range result {
|
||||
if fr.Port != "" {
|
||||
ports = append(ports, fr.Port)
|
||||
} else if fr.PortRange.Start > 0 {
|
||||
ports = append(ports, fmt.Sprintf("%d-%d", fr.PortRange.Start, fr.PortRange.End))
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedPorts, ports, "expanded ports should match expected")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FilterZoneRecordsForPeers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user