From 5507e1f7a5e5edec21a507e0298a7fe28d0fa73d Mon Sep 17 00:00:00 2001 From: Givi Khojanashvili Date: Fri, 2 Jun 2023 15:26:33 +0400 Subject: [PATCH] Add SSH accept rule on the client (#924) --- client/internal/acl/manager.go | 44 +++++++++++++++----- client/internal/acl/manager_test.go | 62 ++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 4d848630f..3fe77d0c9 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -9,6 +9,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/firewall" + "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/iface" mgmProto "github.com/netbirdio/netbird/management/proto" ) @@ -45,7 +46,30 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) { return } - rules := d.squashAcceptRules(networkMap) + rules, squashedProtocols := d.squashAcceptRules(networkMap) + + enableSSH := (networkMap.PeerConfig != nil && + networkMap.PeerConfig.SshConfig != nil && + networkMap.PeerConfig.SshConfig.SshEnabled) + if _, ok := squashedProtocols[mgmProto.FirewallRule_ALL]; ok { + enableSSH = enableSSH && !ok + } + if _, ok := squashedProtocols[mgmProto.FirewallRule_TCP]; ok { + enableSSH = enableSSH && !ok + } + + // if TCP protocol rules not squashed and SSH enabled + // we add default firewall rule which accepts connection to any peer + // in the network by SSH (TCP 22 port). + if enableSSH { + rules = append(rules, &mgmProto.FirewallRule{ + PeerIP: "0.0.0.0", + Direction: mgmProto.FirewallRule_IN, + Action: mgmProto.FirewallRule_ACCEPT, + Protocol: mgmProto.FirewallRule_TCP, + Port: strconv.Itoa(ssh.DefaultSSHPort), + }) + } // if we got empty rules list but management not set networkMap.FirewallRulesIsEmpty flag // we have old version of management without rules handling, we should allow all traffic @@ -202,11 +226,13 @@ func (d *DefaultManager) addOutRules(ip net.IP, protocol firewall.Protocol, port } // squashAcceptRules does complex logic to convert many rules which allows connection by traffic type -// to all peers in the network man to one rule which just accepts that type of the traffic. +// to all peers in the network map to one rule which just accepts that type of the traffic. // // NOTE: It will not squash two rules for same protocol if one covers all peers in the network, // but other has port definitions or has drop policy. -func (d *DefaultManager) squashAcceptRules(networkMap *mgmProto.NetworkMap) []*mgmProto.FirewallRule { +func (d *DefaultManager) squashAcceptRules( + networkMap *mgmProto.NetworkMap, +) ([]*mgmProto.FirewallRule, map[mgmProto.FirewallRuleProtocol]struct{}) { totalIPs := 0 for _, p := range networkMap.RemotePeers { for range p.AllowedIps { @@ -293,13 +319,13 @@ func (d *DefaultManager) squashAcceptRules(networkMap *mgmProto.NetworkMap) []*m squash(in, mgmProto.FirewallRule_IN) squash(out, mgmProto.FirewallRule_OUT) - if len(squashedRules) == 0 { - return networkMap.FirewallRules - } - // if all protocol was squashed everything is allow and we can ignore all other rules if _, ok := squashedProtocols[mgmProto.FirewallRule_ALL]; ok { - return squashedRules + return squashedRules, squashedProtocols + } + + if len(squashedRules) == 0 { + return networkMap.FirewallRules, squashedProtocols } var rules []*mgmProto.FirewallRule @@ -316,7 +342,7 @@ func (d *DefaultManager) squashAcceptRules(networkMap *mgmProto.NetworkMap) []*m rules = append(rules, r) } - return append(rules, squashedRules...) + return append(rules, squashedRules...), squashedProtocols } func convertToFirewallProtocol(protocol mgmProto.FirewallRuleProtocol) firewall.Protocol { diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index a92d5a07e..a0060f1df 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -168,7 +168,7 @@ func TestDefaultManagerSquashRules(t *testing.T) { } manager := &DefaultManager{} - rules := manager.squashAcceptRules(networkMap) + rules, _ := manager.squashAcceptRules(networkMap) if len(rules) != 2 { t.Errorf("rules should contain 2, got: %v", rules) return @@ -266,7 +266,65 @@ func TestDefaultManagerSquashRulesNoAffect(t *testing.T) { } manager := &DefaultManager{} - if rules := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) { + if rules, _ := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) { t.Errorf("we should got same amount of rules as intput, got %v", len(rules)) } } + +func TestDefaultManagerEnableSSHRules(t *testing.T) { + networkMap := &mgmProto.NetworkMap{ + PeerConfig: &mgmProto.PeerConfig{ + SshConfig: &mgmProto.SSHConfig{ + SshEnabled: true, + }, + }, + RemotePeers: []*mgmProto.RemotePeerConfig{ + {AllowedIps: []string{"10.93.0.1"}}, + {AllowedIps: []string{"10.93.0.2"}}, + {AllowedIps: []string{"10.93.0.3"}}, + }, + FirewallRules: []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.FirewallRule_IN, + Action: mgmProto.FirewallRule_ACCEPT, + Protocol: mgmProto.FirewallRule_TCP, + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.FirewallRule_IN, + Action: mgmProto.FirewallRule_ACCEPT, + Protocol: mgmProto.FirewallRule_TCP, + }, + { + PeerIP: "10.93.0.3", + Direction: mgmProto.FirewallRule_OUT, + Action: mgmProto.FirewallRule_ACCEPT, + Protocol: mgmProto.FirewallRule_UDP, + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + iface := mocks.NewMockIFaceMapper(ctrl) + iface.EXPECT().IsUserspaceBind().Return(true) + // iface.EXPECT().Name().Return("lo") + iface.EXPECT().SetFiltering(gomock.Any()) + + // we receive one rule from the management so for testing purposes ignore it + acl, err := Create(iface) + if err != nil { + t.Errorf("create ACL manager: %v", err) + return + } + defer acl.Stop() + + acl.ApplyFiltering(networkMap) + + if len(acl.rulesPairs) != 4 { + t.Errorf("expect 4 rules (last must be SSH), got: %d", len(acl.rulesPairs)) + return + } +}