From d96f882acb2576d329f2eb8a6c7340ccf6c5c701 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Tue, 27 Jun 2023 17:26:15 +0200 Subject: [PATCH 01/27] seems to work but delete fails --- client/firewall/uspfilter/uspfilter.go | 96 +++++++++++++++----------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index adc2d552a..e834cf8b6 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -23,8 +23,8 @@ type IFaceMapper interface { // Manager userspace firewall manager type Manager struct { - outgoingRules []Rule - incomingRules []Rule + outgoingRules map[string][]Rule + incomingRules map[string][]Rule rulesIndex map[string]int wgNetwork *net.IPNet decoders sync.Pool @@ -62,6 +62,8 @@ func Create(iface IFaceMapper) (*Manager, error) { return d }, }, + outgoingRules: make(map[string][]Rule), + incomingRules: make(map[string][]Rule), } if err := iface.SetFilter(m); err != nil { @@ -126,10 +128,10 @@ func (m *Manager) AddFiltering( m.mutex.Lock() var p int if direction == fw.RuleDirectionIN { - m.incomingRules = append(m.incomingRules, r) + m.incomingRules[r.ip.String()] = append(m.incomingRules[r.ip.String()], r) p = len(m.incomingRules) - 1 } else { - m.outgoingRules = append(m.outgoingRules, r) + m.outgoingRules[r.ip.String()] = append(m.outgoingRules[r.ip.String()], r) p = len(m.outgoingRules) - 1 } m.rulesIndex[r.id] = p @@ -156,11 +158,11 @@ func (m *Manager) DeleteRule(rule fw.Rule) error { var toUpdate []Rule if r.direction == fw.RuleDirectionIN { - m.incomingRules = append(m.incomingRules[:p], m.incomingRules[p+1:]...) - toUpdate = m.incomingRules + m.incomingRules[r.ip.String()] = append(m.incomingRules[r.ip.String()][:p], m.incomingRules[r.ip.String()][p+1:]...) + toUpdate = m.incomingRules[r.ip.String()] } else { - m.outgoingRules = append(m.outgoingRules[:p], m.outgoingRules[p+1:]...) - toUpdate = m.outgoingRules + m.outgoingRules[r.ip.String()] = append(m.outgoingRules[r.ip.String()][:p], m.outgoingRules[r.ip.String()][p+1:]...) + toUpdate = m.outgoingRules[r.ip.String()] } for i := 0; i < len(toUpdate); i++ { @@ -174,8 +176,8 @@ func (m *Manager) Reset() error { m.mutex.Lock() defer m.mutex.Unlock() - m.outgoingRules = m.outgoingRules[:0] - m.incomingRules = m.incomingRules[:0] + m.outgoingRules = make(map[string][]Rule) + m.incomingRules = make(map[string][]Rule) m.rulesIndex = make(map[string]int) return nil @@ -192,7 +194,7 @@ func (m *Manager) DropIncoming(packetData []byte) bool { } // dropFilter imlements same logic for booth direction of the traffic -func (m *Manager) dropFilter(packetData []byte, rules []Rule, isIncomingPacket bool) bool { +func (m *Manager) dropFilter(packetData []byte, rules map[string][]Rule, isIncomingPacket bool) bool { m.mutex.RLock() defer m.mutex.RUnlock() @@ -226,29 +228,37 @@ func (m *Manager) dropFilter(packetData []byte, rules []Rule, isIncomingPacket b } payloadLayer := d.decoded[1] + var srcIP, dstIP net.IP + var ipRules []Rule + switch ipLayer { + case layers.LayerTypeIPv4: + if isIncomingPacket { + srcIP = d.ip4.SrcIP + ipRules = rules[srcIP.String()] + } else { + dstIP = d.ip4.DstIP + ipRules = rules[dstIP.String()] + } + case layers.LayerTypeIPv6: + if isIncomingPacket { + srcIP = d.ip6.SrcIP + ipRules = rules[srcIP.String()] + } else { + dstIP = d.ip6.DstIP + ipRules = rules[dstIP.String()] + } + } + // check if IP address match by IP - for _, rule := range rules { + for _, rule := range ipRules { if rule.matchByIP { - switch ipLayer { - case layers.LayerTypeIPv4: - if isIncomingPacket { - if !d.ip4.SrcIP.Equal(rule.ip) { - continue - } - } else { - if !d.ip4.DstIP.Equal(rule.ip) { - continue - } + if isIncomingPacket { + if !srcIP.Equal(rule.ip) { + continue } - case layers.LayerTypeIPv6: - if isIncomingPacket { - if !d.ip6.SrcIP.Equal(rule.ip) { - continue - } - } else { - if !d.ip6.DstIP.Equal(rule.ip) { - continue - } + } else { + if !dstIP.Equal(rule.ip) { + continue } } } @@ -328,11 +338,11 @@ func (m *Manager) AddUDPPacketHook( var toUpdate []Rule if in { r.direction = fw.RuleDirectionIN - m.incomingRules = append([]Rule{r}, m.incomingRules...) - toUpdate = m.incomingRules + m.incomingRules[r.ip.String()] = append([]Rule{r}, m.incomingRules[r.ip.String()]...) + toUpdate = m.incomingRules[r.ip.String()] } else { - m.outgoingRules = append([]Rule{r}, m.outgoingRules...) - toUpdate = m.outgoingRules + m.outgoingRules[r.ip.String()] = append([]Rule{r}, m.outgoingRules[r.ip.String()]...) + toUpdate = m.outgoingRules[r.ip.String()] } for i := range toUpdate { @@ -345,14 +355,18 @@ func (m *Manager) AddUDPPacketHook( // RemovePacketHook removes packet hook by given ID func (m *Manager) RemovePacketHook(hookID string) error { - for _, r := range m.incomingRules { - if r.id == hookID { - return m.DeleteRule(&r) + for _, arr := range m.incomingRules { + for _, r := range arr { + if r.id == hookID { + return m.DeleteRule(&r) + } } } - for _, r := range m.outgoingRules { - if r.id == hookID { - return m.DeleteRule(&r) + for _, arr := range m.outgoingRules { + for _, r := range arr { + if r.id == hookID { + return m.DeleteRule(&r) + } } } return fmt.Errorf("hook with given id not found") From b39ffef22c8855c08fd35029aaedd91d9f50d642 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Tue, 27 Jun 2023 17:44:05 +0200 Subject: [PATCH 02/27] add missing all rule --- client/firewall/uspfilter/uspfilter.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index e834cf8b6..9d16925cf 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -129,10 +129,10 @@ func (m *Manager) AddFiltering( var p int if direction == fw.RuleDirectionIN { m.incomingRules[r.ip.String()] = append(m.incomingRules[r.ip.String()], r) - p = len(m.incomingRules) - 1 + p = len(m.incomingRules[r.ip.String()]) - 1 } else { m.outgoingRules[r.ip.String()] = append(m.outgoingRules[r.ip.String()], r) - p = len(m.outgoingRules) - 1 + p = len(m.outgoingRules[r.ip.String()]) - 1 } m.rulesIndex[r.id] = p m.mutex.Unlock() @@ -234,18 +234,18 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string][]Rule, isIncom case layers.LayerTypeIPv4: if isIncomingPacket { srcIP = d.ip4.SrcIP - ipRules = rules[srcIP.String()] + ipRules = append(rules[srcIP.String()], rules["0.0.0.0"]...) } else { dstIP = d.ip4.DstIP - ipRules = rules[dstIP.String()] + ipRules = append(rules[dstIP.String()], rules["0.0.0.0"]...) } case layers.LayerTypeIPv6: if isIncomingPacket { srcIP = d.ip6.SrcIP - ipRules = rules[srcIP.String()] + ipRules = append(rules[srcIP.String()], rules["::"]...) } else { dstIP = d.ip6.DstIP - ipRules = rules[dstIP.String()] + ipRules = append(rules[dstIP.String()], rules["::"]...) } } From 51878659f8382794a01ecb7cfc0517e7db55bc5b Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Wed, 28 Jun 2023 02:50:12 +0200 Subject: [PATCH 03/27] remove Rule index map --- client/firewall/uspfilter/uspfilter.go | 113 ++++++++++++------------- 1 file changed, 53 insertions(+), 60 deletions(-) diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 9d16925cf..3cbd3e6af 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -23,9 +23,8 @@ type IFaceMapper interface { // Manager userspace firewall manager type Manager struct { - outgoingRules map[string][]Rule - incomingRules map[string][]Rule - rulesIndex map[string]int + outgoingRules map[string]map[string]Rule + incomingRules map[string]map[string]Rule wgNetwork *net.IPNet decoders sync.Pool @@ -48,7 +47,6 @@ type decoder struct { // Create userspace firewall manager constructor func Create(iface IFaceMapper) (*Manager, error) { m := &Manager{ - rulesIndex: make(map[string]int), decoders: sync.Pool{ New: func() any { d := &decoder{ @@ -62,8 +60,8 @@ func Create(iface IFaceMapper) (*Manager, error) { return d }, }, - outgoingRules: make(map[string][]Rule), - incomingRules: make(map[string][]Rule), + outgoingRules: make(map[string]map[string]Rule), + incomingRules: make(map[string]map[string]Rule), } if err := iface.SetFilter(m); err != nil { @@ -126,15 +124,17 @@ func (m *Manager) AddFiltering( } m.mutex.Lock() - var p int if direction == fw.RuleDirectionIN { - m.incomingRules[r.ip.String()] = append(m.incomingRules[r.ip.String()], r) - p = len(m.incomingRules[r.ip.String()]) - 1 + if _, ok := m.incomingRules[r.ip.String()]; !ok { + m.incomingRules[r.ip.String()] = make(map[string]Rule) + } + m.incomingRules[r.ip.String()][r.id] = r } else { - m.outgoingRules[r.ip.String()] = append(m.outgoingRules[r.ip.String()], r) - p = len(m.outgoingRules[r.ip.String()]) - 1 + if _, ok := m.outgoingRules[r.ip.String()]; !ok { + m.outgoingRules[r.ip.String()] = make(map[string]Rule) + } + m.outgoingRules[r.ip.String()][r.id] = r } - m.rulesIndex[r.id] = p m.mutex.Unlock() return &r, nil @@ -150,24 +150,20 @@ func (m *Manager) DeleteRule(rule fw.Rule) error { return fmt.Errorf("delete rule: invalid rule type: %T", rule) } - p, ok := m.rulesIndex[r.id] - if !ok { - return fmt.Errorf("delete rule: no rule with such id: %v", r.id) - } - delete(m.rulesIndex, r.id) - - var toUpdate []Rule if r.direction == fw.RuleDirectionIN { - m.incomingRules[r.ip.String()] = append(m.incomingRules[r.ip.String()][:p], m.incomingRules[r.ip.String()][p+1:]...) - toUpdate = m.incomingRules[r.ip.String()] + _, ok := m.incomingRules[r.ip.String()][r.id] + if !ok { + return fmt.Errorf("delete rule: no rule with such id: %v", r.id) + } + delete(m.incomingRules[r.ip.String()], r.id) } else { - m.outgoingRules[r.ip.String()] = append(m.outgoingRules[r.ip.String()][:p], m.outgoingRules[r.ip.String()][p+1:]...) - toUpdate = m.outgoingRules[r.ip.String()] + _, ok := m.outgoingRules[r.ip.String()][r.id] + if !ok { + return fmt.Errorf("delete rule: no rule with such id: %v", r.id) + } + delete(m.outgoingRules[r.ip.String()], r.id) } - for i := 0; i < len(toUpdate); i++ { - m.rulesIndex[toUpdate[i].id] = i - } return nil } @@ -176,9 +172,8 @@ func (m *Manager) Reset() error { m.mutex.Lock() defer m.mutex.Unlock() - m.outgoingRules = make(map[string][]Rule) - m.incomingRules = make(map[string][]Rule) - m.rulesIndex = make(map[string]int) + m.outgoingRules = make(map[string]map[string]Rule) + m.incomingRules = make(map[string]map[string]Rule) return nil } @@ -194,7 +189,7 @@ func (m *Manager) DropIncoming(packetData []byte) bool { } // dropFilter imlements same logic for booth direction of the traffic -func (m *Manager) dropFilter(packetData []byte, rules map[string][]Rule, isIncomingPacket bool) bool { +func (m *Manager) dropFilter(packetData []byte, rules map[string]map[string]Rule, isIncomingPacket bool) bool { m.mutex.RLock() defer m.mutex.RUnlock() @@ -226,41 +221,39 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string][]Rule, isIncom log.Errorf("unknown layer: %v", d.decoded[0]) return true } - payloadLayer := d.decoded[1] - var srcIP, dstIP net.IP - var ipRules []Rule + var ip net.IP switch ipLayer { case layers.LayerTypeIPv4: if isIncomingPacket { - srcIP = d.ip4.SrcIP - ipRules = append(rules[srcIP.String()], rules["0.0.0.0"]...) + ip = d.ip4.SrcIP } else { - dstIP = d.ip4.DstIP - ipRules = append(rules[dstIP.String()], rules["0.0.0.0"]...) + ip = d.ip4.DstIP } case layers.LayerTypeIPv6: if isIncomingPacket { - srcIP = d.ip6.SrcIP - ipRules = append(rules[srcIP.String()], rules["::"]...) + ip = d.ip6.SrcIP } else { - dstIP = d.ip6.DstIP - ipRules = append(rules[dstIP.String()], rules["::"]...) + ip = d.ip6.DstIP } } + _, ok := rules["0.0.0.0"] + if ok { + return false + } + + _, ok = rules["::"] + if ok { + return false + } + + payloadLayer := d.decoded[1] + // check if IP address match by IP - for _, rule := range ipRules { - if rule.matchByIP { - if isIncomingPacket { - if !srcIP.Equal(rule.ip) { - continue - } - } else { - if !dstIP.Equal(rule.ip) { - continue - } - } + for _, rule := range rules[ip.String()] { + if rule.matchByIP && !ip.Equal(rule.ip) { + continue } if rule.protoLayer == layerTypeAll { @@ -335,19 +328,19 @@ func (m *Manager) AddUDPPacketHook( } m.mutex.Lock() - var toUpdate []Rule if in { r.direction = fw.RuleDirectionIN - m.incomingRules[r.ip.String()] = append([]Rule{r}, m.incomingRules[r.ip.String()]...) - toUpdate = m.incomingRules[r.ip.String()] + if _, ok := m.incomingRules[r.ip.String()]; !ok { + m.incomingRules[r.ip.String()] = make(map[string]Rule) + } + m.incomingRules[r.ip.String()][r.id] = r } else { - m.outgoingRules[r.ip.String()] = append([]Rule{r}, m.outgoingRules[r.ip.String()]...) - toUpdate = m.outgoingRules[r.ip.String()] + if _, ok := m.outgoingRules[r.ip.String()]; !ok { + m.outgoingRules[r.ip.String()] = make(map[string]Rule) + } + m.outgoingRules[r.ip.String()][r.id] = r } - for i := range toUpdate { - m.rulesIndex[toUpdate[i].id] = i - } m.mutex.Unlock() return r.id From 33a155d9aa94024c6e760203c272ab45286f66d6 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Wed, 28 Jun 2023 03:03:01 +0200 Subject: [PATCH 04/27] fix all rules check --- client/firewall/uspfilter/uspfilter.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 3cbd3e6af..94725658a 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -238,13 +238,7 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string]map[string]Rule } } - _, ok := rules["0.0.0.0"] - if ok { - return false - } - - _, ok = rules["::"] - if ok { + if len(rules["0.0.0.0"]) > 0 || len(rules["::"]) > 0 { return false } From 54fe05f6d86180500a2efe9cb08ed81ae6377ebb Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Wed, 28 Jun 2023 10:35:29 +0200 Subject: [PATCH 05/27] fix test --- client/firewall/uspfilter/uspfilter_test.go | 54 ++++++++++----------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index eed31c627..ab921b1eb 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -123,8 +123,8 @@ func TestManagerDeleteRule(t *testing.T) { return } - if idx, ok := m.rulesIndex[rule2.GetRuleID()]; !ok || len(m.incomingRules) != 1 || idx != 0 { - t.Errorf("rule2 is not in the rulesIndex") + if _, ok := m.incomingRules[ip.String()][rule2.GetRuleID()]; !ok { + t.Errorf("rule2 is not in the incomingRules") } err = m.DeleteRule(rule2) @@ -133,8 +133,8 @@ func TestManagerDeleteRule(t *testing.T) { return } - if len(m.rulesIndex) != 0 || len(m.incomingRules) != 0 { - t.Errorf("rule1 still in the rulesIndex") + if _, ok := m.incomingRules[ip.String()][rule2.GetRuleID()]; ok { + t.Errorf("rule2 is not in the incomingRules") } } @@ -169,26 +169,29 @@ func TestAddUDPPacketHook(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { manager := &Manager{ - incomingRules: []Rule{}, - outgoingRules: []Rule{}, - rulesIndex: make(map[string]int), + incomingRules: map[string]map[string]Rule{}, + outgoingRules: map[string]map[string]Rule{}, } manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook) var addedRule Rule if tt.in { - if len(manager.incomingRules) != 1 { + if len(manager.incomingRules[tt.ip.String()]) != 1 { t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules)) return } - addedRule = manager.incomingRules[0] + for _, rule := range manager.incomingRules[tt.ip.String()] { + addedRule = rule + } } else { if len(manager.outgoingRules) != 1 { t.Errorf("expected 1 outgoing rule, got %d", len(manager.outgoingRules)) return } - addedRule = manager.outgoingRules[0] + for _, rule := range manager.outgoingRules[tt.ip.String()] { + addedRule = rule + } } if !tt.ip.Equal(addedRule.ip) { @@ -211,17 +214,6 @@ func TestAddUDPPacketHook(t *testing.T) { t.Errorf("expected udpHook to be set") return } - - // Ensure rulesIndex is correctly updated - index, ok := manager.rulesIndex[addedRule.id] - if !ok { - t.Errorf("expected rule to be in rulesIndex") - return - } - if index != 0 { - t.Errorf("expected rule index to be 0, got %d", index) - return - } }) } } @@ -256,7 +248,7 @@ func TestManagerReset(t *testing.T) { return } - if len(m.rulesIndex) != 0 || len(m.outgoingRules) != 0 || len(m.incomingRules) != 0 { + if len(m.outgoingRules) != 0 || len(m.incomingRules) != 0 { t.Errorf("rules is not empty") } } @@ -346,10 +338,12 @@ func TestRemovePacketHook(t *testing.T) { // Assert the hook is added by finding it in the manager's outgoing rules found := false - for _, rule := range manager.outgoingRules { - if rule.id == hookID { - found = true - break + for _, arr := range manager.outgoingRules { + for _, rule := range arr { + if rule.id == hookID { + found = true + break + } } } @@ -364,9 +358,11 @@ func TestRemovePacketHook(t *testing.T) { } // Assert the hook is removed by checking it in the manager's outgoing rules - for _, rule := range manager.outgoingRules { - if rule.id == hookID { - t.Fatalf("The hook was not removed properly.") + for _, arr := range manager.outgoingRules { + for _, rule := range arr { + if rule.id == hookID { + t.Fatalf("The hook was not removed properly.") + } } } } From e074c244878ec04d5cbf20bf76d5b885745c829b Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Wed, 28 Jun 2023 14:09:23 +0200 Subject: [PATCH 06/27] add type for RuleSet --- client/firewall/uspfilter/uspfilter.go | 21 ++++++++++++--------- client/firewall/uspfilter/uspfilter_test.go | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 94725658a..ab27f631f 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -21,10 +21,13 @@ type IFaceMapper interface { SetFilter(iface.PacketFilter) error } +// RuleSet is a set of rules grouped by a string key +type RuleSet map[string]Rule + // Manager userspace firewall manager type Manager struct { - outgoingRules map[string]map[string]Rule - incomingRules map[string]map[string]Rule + outgoingRules map[string]RuleSet + incomingRules map[string]RuleSet wgNetwork *net.IPNet decoders sync.Pool @@ -60,8 +63,8 @@ func Create(iface IFaceMapper) (*Manager, error) { return d }, }, - outgoingRules: make(map[string]map[string]Rule), - incomingRules: make(map[string]map[string]Rule), + outgoingRules: make(map[string]RuleSet), + incomingRules: make(map[string]RuleSet), } if err := iface.SetFilter(m); err != nil { @@ -126,12 +129,12 @@ func (m *Manager) AddFiltering( m.mutex.Lock() if direction == fw.RuleDirectionIN { if _, ok := m.incomingRules[r.ip.String()]; !ok { - m.incomingRules[r.ip.String()] = make(map[string]Rule) + m.incomingRules[r.ip.String()] = make(RuleSet) } m.incomingRules[r.ip.String()][r.id] = r } else { if _, ok := m.outgoingRules[r.ip.String()]; !ok { - m.outgoingRules[r.ip.String()] = make(map[string]Rule) + m.outgoingRules[r.ip.String()] = make(RuleSet) } m.outgoingRules[r.ip.String()][r.id] = r } @@ -172,8 +175,8 @@ func (m *Manager) Reset() error { m.mutex.Lock() defer m.mutex.Unlock() - m.outgoingRules = make(map[string]map[string]Rule) - m.incomingRules = make(map[string]map[string]Rule) + m.outgoingRules = make(map[string]RuleSet) + m.incomingRules = make(map[string]RuleSet) return nil } @@ -189,7 +192,7 @@ func (m *Manager) DropIncoming(packetData []byte) bool { } // dropFilter imlements same logic for booth direction of the traffic -func (m *Manager) dropFilter(packetData []byte, rules map[string]map[string]Rule, isIncomingPacket bool) bool { +func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet, isIncomingPacket bool) bool { m.mutex.RLock() defer m.mutex.RUnlock() diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index ab921b1eb..c7f38a44f 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -169,8 +169,8 @@ func TestAddUDPPacketHook(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { manager := &Manager{ - incomingRules: map[string]map[string]Rule{}, - outgoingRules: map[string]map[string]Rule{}, + incomingRules: map[string]RuleSet{}, + outgoingRules: map[string]RuleSet{}, } manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook) From 42db9773f4184f8877bcafb96d70ffb0bc54df92 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 10 Jul 2023 22:09:16 +0300 Subject: [PATCH 07/27] Remove unused netbird UI dependencies (#1007) * remove unused netbird-ui dependencies in deb package * build netbird-ui with support for legacy appindicator * add rpm package dendencies * add binary build package * remove dependencies --- .github/workflows/release.yml | 2 +- .goreleaser_ui.yaml | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09ea43ab3..1a3c04f5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,7 +116,7 @@ jobs: run: git --no-pager diff --exit-code - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-mingw-w64-x86-64 + run: sudo apt update && sudo apt install -y -q libappindicator3-dev gir1.2-appindicator3-0.1 libxxf86vm-dev gcc-mingw-w64-x86-64 - name: Install rsrc run: go install github.com/akavel/rsrc@v0.10.2 - name: Generate windows rsrc diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index 197eab3f8..c6f7a7c34 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -11,6 +11,8 @@ builds: - amd64 ldflags: - -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser + tags: + - legacy_appindicator mod_timestamp: '{{ .CommitTimestamp }}' - id: netbird-ui-windows @@ -55,9 +57,6 @@ nfpms: - src: client/ui/disconnected.png dst: /usr/share/pixmaps/netbird.png dependencies: - - libayatana-appindicator3-1 - - libgtk-3-dev - - libappindicator3-dev - netbird - maintainer: Netbird @@ -75,9 +74,6 @@ nfpms: - src: client/ui/disconnected.png dst: /usr/share/pixmaps/netbird.png dependencies: - - libayatana-appindicator3-1 - - libgtk-3-dev - - libappindicator3-dev - netbird uploads: From 6e264d9de704b4ae50ef462e3d8e32ea2a575fa2 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Tue, 11 Jul 2023 19:58:21 +0200 Subject: [PATCH 08/27] fix rule order to solve DNS resolver issue --- client/firewall/uspfilter/uspfilter.go | 46 ++++++++++++++++---------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index ab27f631f..5cc215256 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -241,20 +241,32 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet, isInco } } - if len(rules["0.0.0.0"]) > 0 || len(rules["::"]) > 0 { - return false + filter, ok := validateRule(ip, packetData, rules[ip.String()], d) + if ok { + return filter + } + filter, ok = validateRule(ip, packetData, rules["0.0.0.0"], d) + if ok { + return filter + } + filter, ok = validateRule(ip, packetData, rules["::"], d) + if ok { + return filter } - payloadLayer := d.decoded[1] + // default policy is DROP ALL + return true +} - // check if IP address match by IP - for _, rule := range rules[ip.String()] { +func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decoder) (bool, bool) { + payloadLayer := d.decoded[1] + for _, rule := range rules { if rule.matchByIP && !ip.Equal(rule.ip) { continue } if rule.protoLayer == layerTypeAll { - return rule.drop + return rule.drop, true } if payloadLayer != rule.protoLayer { @@ -264,38 +276,36 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet, isInco switch payloadLayer { case layers.LayerTypeTCP: if rule.sPort == 0 && rule.dPort == 0 { - return rule.drop + return rule.drop, true } if rule.sPort != 0 && rule.sPort == uint16(d.tcp.SrcPort) { - return rule.drop + return rule.drop, true } if rule.dPort != 0 && rule.dPort == uint16(d.tcp.DstPort) { - return rule.drop + return rule.drop, true } case layers.LayerTypeUDP: // if rule has UDP hook (and if we are here we match this rule) // we ignore rule.drop and call this hook if rule.udpHook != nil { - return rule.udpHook(packetData) + return rule.udpHook(packetData), true } if rule.sPort == 0 && rule.dPort == 0 { - return rule.drop + return rule.drop, true } if rule.sPort != 0 && rule.sPort == uint16(d.udp.SrcPort) { - return rule.drop + return rule.drop, true } if rule.dPort != 0 && rule.dPort == uint16(d.udp.DstPort) { - return rule.drop + return rule.drop, true } - return rule.drop + return rule.drop, true case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6: - return rule.drop + return rule.drop, true } } - - // default policy is DROP ALL - return true + return false, false } // SetNetwork of the wireguard interface to which filtering applied From 5cb9a126f1b5dd267a8c9661ac773175144906fa Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 13 Jul 2023 11:49:15 +0300 Subject: [PATCH 09/27] Fix pre-shared key not persistent (#1011) * update pre-shared key if new key is not empty * add unit test for empty pre-shared key --- client/internal/config.go | 10 ++++++---- client/internal/config_test.go | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/client/internal/config.go b/client/internal/config.go index c8edcfb88..cd665016b 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -215,10 +215,12 @@ func update(input ConfigInput) (*Config, error) { } if input.PreSharedKey != nil && config.PreSharedKey != *input.PreSharedKey { - log.Infof("new pre-shared key provided, updated to %s (old value %s)", - *input.PreSharedKey, config.PreSharedKey) - config.PreSharedKey = *input.PreSharedKey - refresh = true + if *input.PreSharedKey != "" { + log.Infof("new pre-shared key provides, updated to %s (old value %s)", + *input.PreSharedKey, config.PreSharedKey) + config.PreSharedKey = *input.PreSharedKey + refresh = true + } } if config.SSHKey == "" { diff --git a/client/internal/config_test.go b/client/internal/config_test.go index d4c207aed..25e8f7b2e 100644 --- a/client/internal/config_test.go +++ b/client/internal/config_test.go @@ -63,7 +63,22 @@ func TestGetConfig(t *testing.T) { assert.Equal(t, config.ManagementURL.String(), managementURL) assert.Equal(t, config.PreSharedKey, preSharedKey) - // case 4: existing config, but new managementURL has been provided -> update config + // case 4: new empty pre-shared key config -> fetch it + newPreSharedKey := "" + config, err = UpdateOrCreateConfig(ConfigInput{ + ManagementURL: managementURL, + AdminURL: adminURL, + ConfigPath: path, + PreSharedKey: &newPreSharedKey, + }) + if err != nil { + return + } + + assert.Equal(t, config.ManagementURL.String(), managementURL) + assert.Equal(t, config.PreSharedKey, preSharedKey) + + // case 5: existing config, but new managementURL has been provided -> update config newManagementURL := "https://test.newManagement.url:33071" config, err = UpdateOrCreateConfig(ConfigInput{ ManagementURL: newManagementURL, From c6af1037d9d75cb8d7ba92bfdfb214e991efa503 Mon Sep 17 00:00:00 2001 From: pascal-fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 14 Jul 2023 20:44:35 +0200 Subject: [PATCH 10/27] FIx error on ip6tables not available (#999) * adding check operation to confirm if ip*tables is available * linter * linter --- client/firewall/iptables/manager_linux.go | 13 +- .../internal/routemanager/firewall_linux.go | 13 ++ .../internal/routemanager/iptables_linux.go | 200 ++++++++++-------- 3 files changed, 132 insertions(+), 94 deletions(-) diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 84e20bd0b..6ddab0b8f 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -58,13 +58,17 @@ func Create(wgIface iFaceMapper) (*Manager, error) { if err != nil { return nil, fmt.Errorf("iptables is not installed in the system or not supported") } - m.ipv4Client = ipv4Client + if isIptablesClientAvailable(ipv4Client) { + m.ipv4Client = ipv4Client + } ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6) if err != nil { log.Errorf("ip6tables is not installed in the system or not supported: %v", err) } else { - m.ipv6Client = ipv6Client + if isIptablesClientAvailable(ipv6Client) { + m.ipv6Client = ipv6Client + } } if err := m.Reset(); err != nil { @@ -73,6 +77,11 @@ func Create(wgIface iFaceMapper) (*Manager, error) { return m, nil } +func isIptablesClientAvailable(client *iptables.IPTables) bool { + _, err := client.ListChains("filter") + return err == nil +} + // AddFiltering rule to the firewall // // If comment is empty rule ID is used as comment diff --git a/client/internal/routemanager/firewall_linux.go b/client/internal/routemanager/firewall_linux.go index f4358c7d1..959724ed3 100644 --- a/client/internal/routemanager/firewall_linux.go +++ b/client/internal/routemanager/firewall_linux.go @@ -35,7 +35,15 @@ func NewFirewall(parentCTX context.Context) firewallManager { if isIptablesSupported() { log.Debugf("iptables is supported") ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4) + if !isIptablesClientAvailable(ipv4Client) { + log.Infof("iptables is missing for ipv4") + ipv4Client = nil + } ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6) + if !isIptablesClientAvailable(ipv6Client) { + log.Infof("iptables is missing for ipv6") + ipv6Client = nil + } return &iptablesManager{ ctx: ctx, @@ -59,6 +67,11 @@ func NewFirewall(parentCTX context.Context) firewallManager { return manager } +func isIptablesClientAvailable(client *iptables.IPTables) bool { + _, err := client.ListChains("filter") + return err == nil +} + func getInPair(pair routerPair) routerPair { return routerPair{ ID: pair.ID, diff --git a/client/internal/routemanager/iptables_linux.go b/client/internal/routemanager/iptables_linux.go index c738e2165..be469b82a 100644 --- a/client/internal/routemanager/iptables_linux.go +++ b/client/internal/routemanager/iptables_linux.go @@ -61,24 +61,28 @@ func (i *iptablesManager) CleanRoutingRules() { log.Debug("flushing tables") errMSGFormat := "iptables: failed cleaning %s chain %s,error: %v" - err = i.ipv4Client.ClearAndDeleteChain(iptablesFilterTable, iptablesRoutingForwardingChain) - if err != nil { - log.Errorf(errMSGFormat, ipv4, iptablesRoutingForwardingChain, err) + if i.ipv4Client != nil { + err = i.ipv4Client.ClearAndDeleteChain(iptablesFilterTable, iptablesRoutingForwardingChain) + if err != nil { + log.Errorf(errMSGFormat, ipv4, iptablesRoutingForwardingChain, err) + } + + err = i.ipv4Client.ClearAndDeleteChain(iptablesNatTable, iptablesRoutingNatChain) + if err != nil { + log.Errorf(errMSGFormat, ipv4, iptablesRoutingNatChain, err) + } } - err = i.ipv4Client.ClearAndDeleteChain(iptablesNatTable, iptablesRoutingNatChain) - if err != nil { - log.Errorf(errMSGFormat, ipv4, iptablesRoutingNatChain, err) - } + if i.ipv6Client != nil { + err = i.ipv6Client.ClearAndDeleteChain(iptablesFilterTable, iptablesRoutingForwardingChain) + if err != nil { + log.Errorf(errMSGFormat, ipv6, iptablesRoutingForwardingChain, err) + } - err = i.ipv6Client.ClearAndDeleteChain(iptablesFilterTable, iptablesRoutingForwardingChain) - if err != nil { - log.Errorf(errMSGFormat, ipv6, iptablesRoutingForwardingChain, err) - } - - err = i.ipv6Client.ClearAndDeleteChain(iptablesNatTable, iptablesRoutingNatChain) - if err != nil { - log.Errorf(errMSGFormat, ipv6, iptablesRoutingNatChain, err) + err = i.ipv6Client.ClearAndDeleteChain(iptablesNatTable, iptablesRoutingNatChain) + if err != nil { + log.Errorf(errMSGFormat, ipv6, iptablesRoutingNatChain, err) + } } log.Info("done cleaning up iptables rules") @@ -96,37 +100,41 @@ func (i *iptablesManager) RestoreOrCreateContainers() error { errMSGFormat := "iptables: failed creating %s chain %s,error: %v" - err := createChain(i.ipv4Client, iptablesFilterTable, iptablesRoutingForwardingChain) - if err != nil { - return fmt.Errorf(errMSGFormat, ipv4, iptablesRoutingForwardingChain, err) + if i.ipv4Client != nil { + err := createChain(i.ipv4Client, iptablesFilterTable, iptablesRoutingForwardingChain) + if err != nil { + return fmt.Errorf(errMSGFormat, ipv4, iptablesRoutingForwardingChain, err) + } + + err = createChain(i.ipv4Client, iptablesNatTable, iptablesRoutingNatChain) + if err != nil { + return fmt.Errorf(errMSGFormat, ipv4, iptablesRoutingNatChain, err) + } + + err = i.restoreRules(i.ipv4Client) + if err != nil { + return fmt.Errorf("iptables: error while restoring ipv4 rules: %v", err) + } } - err = createChain(i.ipv4Client, iptablesNatTable, iptablesRoutingNatChain) - if err != nil { - return fmt.Errorf(errMSGFormat, ipv4, iptablesRoutingNatChain, err) + if i.ipv6Client != nil { + err := createChain(i.ipv6Client, iptablesFilterTable, iptablesRoutingForwardingChain) + if err != nil { + return fmt.Errorf(errMSGFormat, ipv6, iptablesRoutingForwardingChain, err) + } + + err = createChain(i.ipv6Client, iptablesNatTable, iptablesRoutingNatChain) + if err != nil { + return fmt.Errorf(errMSGFormat, ipv6, iptablesRoutingNatChain, err) + } + + err = i.restoreRules(i.ipv6Client) + if err != nil { + return fmt.Errorf("iptables: error while restoring ipv6 rules: %v", err) + } } - err = createChain(i.ipv6Client, iptablesFilterTable, iptablesRoutingForwardingChain) - if err != nil { - return fmt.Errorf(errMSGFormat, ipv6, iptablesRoutingForwardingChain, err) - } - - err = createChain(i.ipv6Client, iptablesNatTable, iptablesRoutingNatChain) - if err != nil { - return fmt.Errorf(errMSGFormat, ipv6, iptablesRoutingNatChain, err) - } - - err = i.restoreRules(i.ipv4Client) - if err != nil { - return fmt.Errorf("iptables: error while restoring ipv4 rules: %v", err) - } - - err = i.restoreRules(i.ipv6Client) - if err != nil { - return fmt.Errorf("iptables: error while restoring ipv6 rules: %v", err) - } - - err = i.addJumpRules() + err := i.addJumpRules() if err != nil { return fmt.Errorf("iptables: error while creating jump rules: %v", err) } @@ -140,34 +148,38 @@ func (i *iptablesManager) addJumpRules() error { if err != nil { return err } - rule := append(iptablesDefaultForwardingRule, ipv4Forwarding) - err = i.ipv4Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...) - if err != nil { - return err + if i.ipv4Client != nil { + rule := append(iptablesDefaultForwardingRule, ipv4Forwarding) + + err = i.ipv4Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...) + if err != nil { + return err + } + i.rules[ipv4][ipv4Forwarding] = rule + + rule = append(iptablesDefaultNatRule, ipv4Nat) + err = i.ipv4Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...) + if err != nil { + return err + } + i.rules[ipv4][ipv4Nat] = rule } - i.rules[ipv4][ipv4Forwarding] = rule + if i.ipv6Client != nil { + rule := append(iptablesDefaultForwardingRule, ipv6Forwarding) + err = i.ipv6Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...) + if err != nil { + return err + } + i.rules[ipv6][ipv6Forwarding] = rule - rule = append(iptablesDefaultNatRule, ipv4Nat) - err = i.ipv4Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...) - if err != nil { - return err + rule = append(iptablesDefaultNatRule, ipv6Nat) + err = i.ipv6Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...) + if err != nil { + return err + } + i.rules[ipv6][ipv6Nat] = rule } - i.rules[ipv4][ipv4Nat] = rule - - rule = append(iptablesDefaultForwardingRule, ipv6Forwarding) - err = i.ipv6Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...) - if err != nil { - return err - } - i.rules[ipv6][ipv6Forwarding] = rule - - rule = append(iptablesDefaultNatRule, ipv6Nat) - err = i.ipv6Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...) - if err != nil { - return err - } - i.rules[ipv6][ipv6Nat] = rule return nil } @@ -177,35 +189,39 @@ func (i *iptablesManager) cleanJumpRules() error { var err error errMSGFormat := "iptables: failed cleaning rule from %s chain %s,err: %v" rule, found := i.rules[ipv4][ipv4Forwarding] - if found { - log.Debugf("iptables: removing %s rule: %s ", ipv4, ipv4Forwarding) - err = i.ipv4Client.DeleteIfExists(iptablesFilterTable, iptablesForwardChain, rule...) - if err != nil { - return fmt.Errorf(errMSGFormat, ipv4, iptablesForwardChain, err) + if i.ipv4Client != nil { + if found { + log.Debugf("iptables: removing %s rule: %s ", ipv4, ipv4Forwarding) + err = i.ipv4Client.DeleteIfExists(iptablesFilterTable, iptablesForwardChain, rule...) + if err != nil { + return fmt.Errorf(errMSGFormat, ipv4, iptablesForwardChain, err) + } + } + rule, found = i.rules[ipv4][ipv4Nat] + if found { + log.Debugf("iptables: removing %s rule: %s ", ipv4, ipv4Nat) + err = i.ipv4Client.DeleteIfExists(iptablesNatTable, iptablesPostRoutingChain, rule...) + if err != nil { + return fmt.Errorf(errMSGFormat, ipv4, iptablesPostRoutingChain, err) + } } } - rule, found = i.rules[ipv4][ipv4Nat] - if found { - log.Debugf("iptables: removing %s rule: %s ", ipv4, ipv4Nat) - err = i.ipv4Client.DeleteIfExists(iptablesNatTable, iptablesPostRoutingChain, rule...) - if err != nil { - return fmt.Errorf(errMSGFormat, ipv4, iptablesPostRoutingChain, err) + if i.ipv6Client == nil { + rule, found = i.rules[ipv6][ipv6Forwarding] + if found { + log.Debugf("iptables: removing %s rule: %s ", ipv6, ipv6Forwarding) + err = i.ipv6Client.DeleteIfExists(iptablesFilterTable, iptablesForwardChain, rule...) + if err != nil { + return fmt.Errorf(errMSGFormat, ipv6, iptablesForwardChain, err) + } } - } - rule, found = i.rules[ipv6][ipv6Forwarding] - if found { - log.Debugf("iptables: removing %s rule: %s ", ipv6, ipv6Forwarding) - err = i.ipv6Client.DeleteIfExists(iptablesFilterTable, iptablesForwardChain, rule...) - if err != nil { - return fmt.Errorf(errMSGFormat, ipv6, iptablesForwardChain, err) - } - } - rule, found = i.rules[ipv6][ipv6Nat] - if found { - log.Debugf("iptables: removing %s rule: %s ", ipv6, ipv6Nat) - err = i.ipv6Client.DeleteIfExists(iptablesNatTable, iptablesPostRoutingChain, rule...) - if err != nil { - return fmt.Errorf(errMSGFormat, ipv6, iptablesPostRoutingChain, err) + rule, found = i.rules[ipv6][ipv6Nat] + if found { + log.Debugf("iptables: removing %s rule: %s ", ipv6, ipv6Nat) + err = i.ipv6Client.DeleteIfExists(iptablesNatTable, iptablesPostRoutingChain, rule...) + if err != nil { + return fmt.Errorf(errMSGFormat, ipv6, iptablesPostRoutingChain, err) + } } } return nil From 9c2c0e7934d04e17986a1a17670ee75054379832 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 14 Jul 2023 20:45:40 +0200 Subject: [PATCH 11/27] Check links of groups before delete it (#1010) * Check links of groups before delete it * Add delete group handler test * Rename dns error msg * Add delete group test * Remove rule check The policy cover this scenario * Fix test * Check disabled management grps * Change error message * Add new activity for group delete event --- management/server/account.go | 2 +- management/server/account_test.go | 5 +- management/server/activity/codes.go | 8 + management/server/group.go | 87 +++++++++- management/server/group_test.go | 164 ++++++++++++++++++ management/server/http/groups_handler.go | 9 +- management/server/http/groups_handler_test.go | 98 ++++++++++- management/server/http/util/util.go | 12 +- management/server/mock_server/account_mock.go | 6 +- 9 files changed, 368 insertions(+), 23 deletions(-) create mode 100644 management/server/group_test.go diff --git a/management/server/account.go b/management/server/account.go index 417c32c09..68302d2ba 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -81,7 +81,7 @@ type AccountManager interface { GetGroup(accountId, groupID string) (*Group, error) SaveGroup(accountID, userID string, group *Group) error UpdateGroup(accountID string, groupID string, operations []GroupUpdateOperation) (*Group, error) - DeleteGroup(accountId, groupID string) error + DeleteGroup(accountId, userId, groupID string) error ListGroups(accountId string) ([]*Group, error) GroupAddPeer(accountId, groupID, peerID string) error GroupDeletePeer(accountId, groupID, peerKey string) error diff --git a/management/server/account_test.go b/management/server/account_test.go index f90abc41b..1609abafc 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1083,7 +1083,10 @@ func TestAccountManager_NetworkUpdates(t *testing.T) { } }() - if err := manager.DeleteGroup(account.Id, group.ID); err != nil { + // clean policy is pre requirement for delete group + _ = manager.DeletePolicy(account.Id, policy.ID, userID) + + if err := manager.DeleteGroup(account.Id, "", group.ID); err != nil { t.Errorf("delete group: %v", err) return } diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index 03ec709e5..e571c3a0c 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -95,6 +95,8 @@ const ( UserBlocked // UserUnblocked indicates that a user unblocked another user UserUnblocked + // GroupDeleted indicates that a user deleted group + GroupDeleted ) const ( @@ -192,6 +194,8 @@ const ( UserBlockedMessage string = "User blocked" // UserUnblockedMessage is a human-readable text message of the UserUnblocked activity UserUnblockedMessage string = "User unblocked" + // GroupDeletedMessage is a human-readable text message of the GroupDeleted activity + GroupDeletedMessage string = "Group deleted" ) // Activity that triggered an Event @@ -294,6 +298,8 @@ func (a Activity) Message() string { return UserBlockedMessage case UserUnblocked: return UserUnblockedMessage + case GroupDeleted: + return GroupDeletedMessage default: return "UNKNOWN_ACTIVITY" } @@ -342,6 +348,8 @@ func (a Activity) StringCode() string { return "group.add" case GroupUpdated: return "group.update" + case GroupDeleted: + return "group.delete" case GroupRemovedFromPeer: return "peer.group.delete" case GroupAddedToPeer: diff --git a/management/server/group.go b/management/server/group.go index dd1229c86..53571e099 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -1,11 +1,23 @@ package server import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" - log "github.com/sirupsen/logrus" ) +type GroupLinkError struct { + Resource string + Name string +} + +func (e *GroupLinkError) Error() string { + return fmt.Sprintf("group has been linked to %s: %s", e.Resource, e.Name) +} + // Group of the peers for ACL type Group struct { // ID of the group @@ -203,15 +215,80 @@ func (am *DefaultAccountManager) UpdateGroup(accountID string, } // DeleteGroup object of the peers -func (am *DefaultAccountManager) DeleteGroup(accountID, groupID string) error { - unlock := am.Store.AcquireAccountLock(accountID) +func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string) error { + unlock := am.Store.AcquireAccountLock(accountId) defer unlock() - account, err := am.Store.GetAccount(accountID) + account, err := am.Store.GetAccount(accountId) if err != nil { return err } + g, ok := account.Groups[groupID] + if !ok { + return nil + } + + // check route links + for _, r := range account.Routes { + for _, g := range r.Groups { + if g == groupID { + return &GroupLinkError{"route", r.NetID} + } + } + } + + // check DNS links + for _, dns := range account.NameServerGroups { + for _, g := range dns.Groups { + if g == groupID { + return &GroupLinkError{"name server groups", dns.Name} + } + } + } + + // check ACL links + for _, policy := range account.Policies { + for _, rule := range policy.Rules { + for _, src := range rule.Sources { + if src == groupID { + return &GroupLinkError{"policy", policy.Name} + } + } + + for _, dst := range rule.Destinations { + if dst == groupID { + return &GroupLinkError{"policy", policy.Name} + } + } + } + } + + // check setup key links + for _, setupKey := range account.SetupKeys { + for _, grp := range setupKey.AutoGroups { + if grp == groupID { + return &GroupLinkError{"setup key", setupKey.Name} + } + } + } + + // check user links + for _, user := range account.Users { + for _, grp := range user.AutoGroups { + if grp == groupID { + return &GroupLinkError{"user", user.Id} + } + } + } + + // check DisabledManagementGroups + for _, disabledMgmGrp := range account.DNSSettings.DisabledManagementGroups { + if disabledMgmGrp == groupID { + return &GroupLinkError{"disabled DNS management groups", g.Name} + } + } + delete(account.Groups, groupID) account.Network.IncSerial() @@ -219,6 +296,8 @@ func (am *DefaultAccountManager) DeleteGroup(accountID, groupID string) error { return err } + am.storeEvent(userId, groupID, accountId, activity.GroupDeleted, g.EventMeta()) + return am.updateAccountPeers(account) } diff --git a/management/server/group_test.go b/management/server/group_test.go new file mode 100644 index 000000000..3e2d6d3cc --- /dev/null +++ b/management/server/group_test.go @@ -0,0 +1,164 @@ +package server + +import ( + "testing" + + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/route" +) + +const ( + groupAdminUserID = "testingAdminUser" +) + +func TestDefaultAccountManager_DeleteGroup(t *testing.T) { + am, err := createManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestGroupAccount(am) + if err != nil { + t.Error("failed to init testing account") + } + + testCases := []struct { + name string + groupID string + expectedReason string + }{ + { + "route", + "grp-for-route", + "route", + }, + { + "name server groups", + "grp-for-name-server-grp", + "name server groups", + }, + { + "policy", + "grp-for-policies", + "policy", + }, + { + "setup keys", + "grp-for-keys", + "setup key", + }, + { + "users", + "grp-for-users", + "user", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err = am.DeleteGroup(account.Id, "", testCase.groupID) + if err == nil { + t.Errorf("delete %s group successfully", testCase.groupID) + return + } + + gErr, ok := err.(*GroupLinkError) + if !ok { + t.Error("invalid error type") + return + } + if gErr.Resource != testCase.expectedReason { + t.Errorf("invalid error case: %s, expected: %s", gErr.Resource, testCase.expectedReason) + } + }) + } +} + +func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { + accountID := "testingAcc" + domain := "example.com" + + groupForRoute := &Group{ + "grp-for-route", + "Group for route", + GroupIssuedAPI, + make([]string, 0), + } + + groupForNameServerGroups := &Group{ + "grp-for-name-server-grp", + "Group for name server groups", + GroupIssuedAPI, + make([]string, 0), + } + + groupForPolicies := &Group{ + "grp-for-policies", + "Group for policies", + GroupIssuedAPI, + make([]string, 0), + } + + groupForSetupKeys := &Group{ + "grp-for-keys", + "Group for setup keys", + GroupIssuedAPI, + make([]string, 0), + } + + groupForUsers := &Group{ + "grp-for-users", + "Group for users", + GroupIssuedAPI, + make([]string, 0), + } + + routeResource := &route.Route{ + ID: "example route", + Groups: []string{groupForRoute.ID}, + } + + nameServerGroup := &nbdns.NameServerGroup{ + ID: "example name server group", + Groups: []string{groupForNameServerGroups.ID}, + } + + policy := &Policy{ + ID: "example policy", + Rules: []*PolicyRule{ + { + ID: "example policy rule", + Destinations: []string{groupForPolicies.ID}, + }, + }, + } + + setupKey := &SetupKey{ + Id: "example setup key", + AutoGroups: []string{groupForSetupKeys.ID}, + } + + user := &User{ + Id: "example user", + AutoGroups: []string{groupForUsers.ID}, + } + account := newAccountWithId(accountID, groupAdminUserID, domain) + account.Routes[routeResource.ID] = routeResource + account.NameServerGroups[nameServerGroup.ID] = nameServerGroup + account.Policies = append(account.Policies, policy) + account.SetupKeys[setupKey.Id] = setupKey + account.Users[user.Id] = user + + err := am.Store.SaveAccount(account) + if err != nil { + return nil, err + } + + _ = am.SaveGroup(accountID, groupAdminUserID, groupForRoute) + _ = am.SaveGroup(accountID, groupAdminUserID, groupForNameServerGroups) + _ = am.SaveGroup(accountID, groupAdminUserID, groupForPolicies) + _ = am.SaveGroup(accountID, groupAdminUserID, groupForSetupKeys) + _ = am.SaveGroup(accountID, groupAdminUserID, groupForUsers) + + return am.Store.GetAccount(account.Id) +} diff --git a/management/server/http/groups_handler.go b/management/server/http/groups_handler.go index 966c3f678..30c78e21b 100644 --- a/management/server/http/groups_handler.go +++ b/management/server/http/groups_handler.go @@ -168,7 +168,7 @@ func (h *GroupsHandler) CreateGroup(w http.ResponseWriter, r *http.Request) { // DeleteGroup handles group deletion request func (h *GroupsHandler) DeleteGroup(w http.ResponseWriter, r *http.Request) { claims := h.claimsExtractor.FromRequestContext(r) - account, _, err := h.accountManager.GetAccountFromToken(claims) + account, user, err := h.accountManager.GetAccountFromToken(claims) if err != nil { util.WriteError(err, w) return @@ -192,8 +192,13 @@ func (h *GroupsHandler) DeleteGroup(w http.ResponseWriter, r *http.Request) { return } - err = h.accountManager.DeleteGroup(aID, groupID) + err = h.accountManager.DeleteGroup(aID, user.Id, groupID) if err != nil { + _, ok := err.(*server.GroupLinkError) + if ok { + util.WriteErrorResponse(err.Error(), http.StatusBadRequest, w) + return + } util.WriteError(err, w) return } diff --git a/management/server/http/groups_handler_test.go b/management/server/http/groups_handler_test.go index 73ca8db6a..44603059a 100644 --- a/management/server/http/groups_handler_test.go +++ b/management/server/http/groups_handler_test.go @@ -11,17 +11,15 @@ import ( "strings" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/status" - "github.com/gorilla/mux" - - "github.com/netbirdio/netbird/management/server/jwtclaims" - "github.com/magiconair/properties/assert" "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" + "github.com/netbirdio/netbird/management/server/status" ) var TestPeers = map[string]*server.Peer{ @@ -94,6 +92,18 @@ func initGroupTestData(user *server.User, groups ...*server.Group) *GroupsHandle }, }, user, nil }, + DeleteGroupFunc: func(accountID, userId, groupID string) error { + if groupID == "linked-grp" { + return &server.GroupLinkError{ + Resource: "something", + Name: "linked-grp", + } + } + if groupID == "invalid-grp" { + return fmt.Errorf("internal error") + } + return nil + }, }, claimsExtractor: jwtclaims.NewClaimsExtractor( jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { @@ -297,3 +307,79 @@ func TestWriteGroup(t *testing.T) { }) } } + +func TestDeleteGroup(t *testing.T) { + tt := []struct { + name string + expectedStatus int + expectedBody bool + requestType string + requestPath string + }{ + { + name: "Try to delete linked group", + requestType: http.MethodDelete, + requestPath: "/api/groups/linked-grp", + expectedStatus: http.StatusBadRequest, + expectedBody: true, + }, + { + name: "Try to cause internal error", + requestType: http.MethodDelete, + requestPath: "/api/groups/invalid-grp", + expectedStatus: http.StatusInternalServerError, + expectedBody: true, + }, + { + name: "Try to cause internal error", + requestType: http.MethodDelete, + requestPath: "/api/groups/invalid-grp", + expectedStatus: http.StatusInternalServerError, + expectedBody: true, + }, + { + name: "Delete group", + requestType: http.MethodDelete, + requestPath: "/api/groups/any-grp", + expectedStatus: http.StatusOK, + expectedBody: false, + }, + } + + adminUser := server.NewAdminUser("test_user") + p := initGroupTestData(adminUser) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) + + router := mux.NewRouter() + router.HandleFunc("/api/groups/{groupId}", p.DeleteGroup).Methods("DELETE") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + } + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v, content: %s", + status, tc.expectedStatus, string(content)) + return + } + + if tc.expectedBody { + got := &util.ErrorResponse{} + + if err = json.Unmarshal(content, &got); err != nil { + t.Fatalf("Sent content is not in correct json format; %v", err) + } + assert.Equal(t, got.Code, tc.expectedStatus) + } + }) + } +} diff --git a/management/server/http/util/util.go b/management/server/http/util/util.go index 407443251..44f4919f5 100644 --- a/management/server/http/util/util.go +++ b/management/server/http/util/util.go @@ -13,6 +13,11 @@ import ( "github.com/netbirdio/netbird/management/server/status" ) +type ErrorResponse struct { + Message string `json:"message"` + Code int `json:"code"` +} + // WriteJSONObject simply writes object to the HTTP reponse in JSON format func WriteJSONObject(w http.ResponseWriter, obj interface{}) { w.WriteHeader(http.StatusOK) @@ -58,14 +63,9 @@ func (d *Duration) UnmarshalJSON(b []byte) error { // WriteErrorResponse prepares and writes an error response i nJSON func WriteErrorResponse(errMsg string, httpStatus int, w http.ResponseWriter) { - type errorResponse struct { - Message string `json:"message"` - Code int `json:"code"` - } - w.WriteHeader(httpStatus) w.Header().Set("Content-Type", "application/json; charset=UTF-8") - err := json.NewEncoder(w).Encode(&errorResponse{ + err := json.NewEncoder(w).Encode(&ErrorResponse{ Message: errMsg, Code: httpStatus, }) diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 9482c5ec6..eb31d2a79 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -32,7 +32,7 @@ type MockAccountManager struct { GetGroupFunc func(accountID, groupID string) (*server.Group, error) SaveGroupFunc func(accountID, userID string, group *server.Group) error UpdateGroupFunc func(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error) - DeleteGroupFunc func(accountID, groupID string) error + DeleteGroupFunc func(accountID, userId, groupID string) error ListGroupsFunc func(accountID string) ([]*server.Group, error) GroupAddPeerFunc func(accountID, groupID, peerKey string) error GroupDeletePeerFunc func(accountID, groupID, peerKey string) error @@ -275,9 +275,9 @@ func (am *MockAccountManager) UpdateGroup(accountID string, groupID string, oper } // DeleteGroup mock implementation of DeleteGroup from server.AccountManager interface -func (am *MockAccountManager) DeleteGroup(accountID, groupID string) error { +func (am *MockAccountManager) DeleteGroup(accountId, userId, groupID string) error { if am.DeleteGroupFunc != nil { - return am.DeleteGroupFunc(accountID, groupID) + return am.DeleteGroupFunc(accountId, userId, groupID) } return status.Errorf(codes.Unimplemented, "method DeleteGroup is not implemented") } From 7ebe58f20abcfe798994f2457913b3ab0f8f6436 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 14 Jul 2023 21:56:22 +0200 Subject: [PATCH 12/27] Feature/permanent dns (#967) * Add DNS list argument for mobile client * Write testable code Many places are checked the wgInterface != nil condition. It is doing it just because to avoid the real wgInterface creation for tests. Instead of this involve a wgInterface interface what is moc-able. * Refactor the DNS server internal code structure With the fake resolver has been involved several if-else statement and generated some unused variables to distinguish the listener and fake resolver solutions at running time. With this commit the fake resolver and listener based solution has been moved into two separated structure. Name of this layer is the 'service'. With this modification the unit test looks simpler and open the option to add new logic for the permanent DNS service usage for mobile systems. * Remove is running check in test We can not ensure the state well so remove this check. The test will fail if the server is not running well. --- client/android/client.go | 23 +- client/android/dns_list.go | 26 ++ client/android/dns_list_test.go | 24 ++ client/cmd/up.go | 2 +- client/internal/connect.go | 29 +- client/internal/dns/host_android.go | 6 +- client/internal/dns/host_darwin.go | 4 +- client/internal/dns/host_linux.go | 6 +- client/internal/dns/host_windows.go | 4 +- client/internal/dns/mockServer.go | 5 + client/internal/dns/network_manager_linux.go | 4 +- client/internal/dns/resolvconf_linux.go | 4 +- client/internal/dns/server.go | 328 +++++------------ client/internal/dns/server_export.go | 29 ++ client/internal/dns/server_export_test.go | 24 ++ client/internal/dns/server_test.go | 359 +++++++++++++++---- client/internal/dns/service.go | 18 + client/internal/dns/service_listener.go | 145 ++++++++ client/internal/dns/service_memory.go | 139 +++++++ client/internal/dns/service_memory_test.go | 31 ++ client/internal/dns/systemd_linux.go | 3 +- client/internal/dns/wgiface.go | 14 + client/internal/dns/wgiface_windows.go | 13 + client/internal/engine.go | 29 +- client/internal/mobile_dependency.go | 9 +- client/server/server.go | 4 +- iface/iface_android.go | 2 - iface/iface_nonandroid.go | 2 - iface/tun_android.go | 1 + iface/tun_linux.go | 4 +- 30 files changed, 925 insertions(+), 366 deletions(-) create mode 100644 client/android/dns_list.go create mode 100644 client/android/dns_list_test.go create mode 100644 client/internal/dns/server_export.go create mode 100644 client/internal/dns/server_export_test.go create mode 100644 client/internal/dns/service.go create mode 100644 client/internal/dns/service_listener.go create mode 100644 client/internal/dns/service_memory.go create mode 100644 client/internal/dns/service_memory_test.go create mode 100644 client/internal/dns/wgiface.go create mode 100644 client/internal/dns/wgiface_windows.go diff --git a/client/android/client.go b/client/android/client.go index 05801659f..d8f561e18 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/stdnet" @@ -35,6 +36,11 @@ type RouteListener interface { routemanager.RouteListener } +// DnsReadyListener export internal dns ReadyListener for mobile +type DnsReadyListener interface { + dns.ReadyListener +} + func init() { formatter.SetLogcatFormatter(log.StandardLogger()) } @@ -49,6 +55,7 @@ type Client struct { ctxCancelLock *sync.Mutex deviceName string routeListener routemanager.RouteListener + onHostDnsFn func([]string) } // NewClient instantiate a new Client @@ -65,7 +72,7 @@ func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover } // Run start the internal client. It is a blocker function -func (c *Client) Run(urlOpener URLOpener) error { +func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener) error { cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{ ConfigPath: c.cfgFile, }) @@ -90,7 +97,8 @@ func (c *Client) Run(urlOpener URLOpener) error { // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - return internal.RunClient(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener) + c.onHostDnsFn = func([]string) {} + return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener) } // Stop the internal client and free the resources @@ -126,6 +134,17 @@ func (c *Client) PeersList() *PeerInfoArray { return &PeerInfoArray{items: peerInfos} } +// OnUpdatedHostDNS update the DNS servers addresses for root zones +func (c *Client) OnUpdatedHostDNS(list *DNSList) error { + dnsServer, err := dns.GetServerDns() + if err != nil { + return err + } + + dnsServer.OnUpdatedHostDNSServer(list.items) + return nil +} + // SetConnectionListener set the network connection listener func (c *Client) SetConnectionListener(listener ConnectionListener) { c.recorder.SetConnectionListener(listener) diff --git a/client/android/dns_list.go b/client/android/dns_list.go new file mode 100644 index 000000000..76b922220 --- /dev/null +++ b/client/android/dns_list.go @@ -0,0 +1,26 @@ +package android + +import "fmt" + +// DNSList is a wrapper of []string +type DNSList struct { + items []string +} + +// Add new DNS address to the collection +func (array *DNSList) Add(s string) { + array.items = append(array.items, s) +} + +// Get return an element of the collection +func (array *DNSList) Get(i int) (string, error) { + if i >= len(array.items) || i < 0 { + return "", fmt.Errorf("out of range") + } + return array.items[i], nil +} + +// Size return with the size of the collection +func (array *DNSList) Size() int { + return len(array.items) +} diff --git a/client/android/dns_list_test.go b/client/android/dns_list_test.go new file mode 100644 index 000000000..93aea78a8 --- /dev/null +++ b/client/android/dns_list_test.go @@ -0,0 +1,24 @@ +package android + +import "testing" + +func TestDNSList_Get(t *testing.T) { + l := DNSList{ + items: make([]string, 1), + } + + _, err := l.Get(0) + if err != nil { + t.Errorf("invalid error: %s", err) + } + + _, err = l.Get(-1) + if err == nil { + t.Errorf("expected error but got nil") + } + + _, err = l.Get(1) + if err == nil { + t.Errorf("expected error but got nil") + } +} diff --git a/client/cmd/up.go b/client/cmd/up.go index 3ebe1ce4b..a275e88db 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -104,7 +104,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { var cancel context.CancelFunc ctx, cancel = context.WithCancel(ctx) SetupCloseHandler(ctx, cancel) - return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()), nil, nil, nil) + return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String())) } func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { diff --git a/client/internal/connect.go b/client/internal/connect.go index 91c0e5d76..87aab0b54 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -12,6 +12,7 @@ import ( "google.golang.org/grpc/codes" gstatus "google.golang.org/grpc/status" + "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/stdnet" @@ -24,7 +25,24 @@ import ( ) // RunClient with main logic. -func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, routeListener routemanager.RouteListener) error { +func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) error { + return runClient(ctx, config, statusRecorder, MobileDependency{}) +} + +// RunClientMobile with main logic on mobile system +func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, routeListener routemanager.RouteListener, dnsAddresses []string, dnsReadyListener dns.ReadyListener) error { + // in case of non Android os these variables will be nil + mobileDependency := MobileDependency{ + TunAdapter: tunAdapter, + IFaceDiscover: iFaceDiscover, + RouteListener: routeListener, + HostDNSAddresses: dnsAddresses, + DnsReadyListener: dnsReadyListener, + } + return runClient(ctx, config, statusRecorder, mobileDependency) +} + +func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, mobileDependency MobileDependency) error { backOff := &backoff.ExponentialBackOff{ InitialInterval: time.Second, RandomizationFactor: 1, @@ -151,14 +169,7 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, return wrapErr(err) } - // in case of non Android os these variables will be nil - md := MobileDependency{ - TunAdapter: tunAdapter, - IFaceDiscover: iFaceDiscover, - RouteListener: routeListener, - } - - engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, md, statusRecorder) + engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, statusRecorder) err = engine.Start() if err != nil { log.Errorf("error while starting Netbird Connection Engine: %s", err) diff --git a/client/internal/dns/host_android.go b/client/internal/dns/host_android.go index 1cb07f0c7..4ab7b32d8 100644 --- a/client/internal/dns/host_android.go +++ b/client/internal/dns/host_android.go @@ -1,13 +1,9 @@ package dns -import ( - "github.com/netbirdio/netbird/iface" -) - type androidHostManager struct { } -func newHostManager(wgInterface *iface.WGIface) (hostManager, error) { +func newHostManager(wgInterface WGIface) (hostManager, error) { return &androidHostManager{}, nil } diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index 677efaef4..0960b7961 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -9,8 +9,6 @@ import ( "strings" log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/iface" ) const ( @@ -34,7 +32,7 @@ type systemConfigurator struct { createdKeys map[string]struct{} } -func newHostManager(_ *iface.WGIface) (hostManager, error) { +func newHostManager(_ WGIface) (hostManager, error) { return &systemConfigurator{ createdKeys: make(map[string]struct{}), }, nil diff --git a/client/internal/dns/host_linux.go b/client/internal/dns/host_linux.go index ee80ab5f6..7838c988f 100644 --- a/client/internal/dns/host_linux.go +++ b/client/internal/dns/host_linux.go @@ -5,10 +5,10 @@ package dns import ( "bufio" "fmt" - "github.com/netbirdio/netbird/iface" - log "github.com/sirupsen/logrus" "os" "strings" + + log "github.com/sirupsen/logrus" ) const ( @@ -25,7 +25,7 @@ const ( type osManagerType int -func newHostManager(wgInterface *iface.WGIface) (hostManager, error) { +func newHostManager(wgInterface WGIface) (hostManager, error) { osManager, err := getOSDNSManagerType() if err != nil { return nil, err diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 7d609ec04..cea806bd2 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -6,8 +6,6 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/sys/windows/registry" - - "github.com/netbirdio/netbird/iface" ) const ( @@ -33,7 +31,7 @@ type registryConfigurator struct { existingSearchDomains []string } -func newHostManager(wgInterface *iface.WGIface) (hostManager, error) { +func newHostManager(wgInterface WGIface) (hostManager, error) { guid, err := wgInterface.GetInterfaceGUIDString() if err != nil { return nil, err diff --git a/client/internal/dns/mockServer.go b/client/internal/dns/mockServer.go index ff218b888..8970eec6e 100644 --- a/client/internal/dns/mockServer.go +++ b/client/internal/dns/mockServer.go @@ -31,6 +31,11 @@ func (m *MockServer) DnsIP() string { return "" } +func (m *MockServer) OnUpdatedHostDNSServer(strings []string) { + //TODO implement me + panic("implement me") +} + // UpdateDNSServer mock implementation of UpdateDNSServer from Server interface func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error { if m.UpdateDNSServerFunc != nil { diff --git a/client/internal/dns/network_manager_linux.go b/client/internal/dns/network_manager_linux.go index 1fa713f46..0b7ae7d4c 100644 --- a/client/internal/dns/network_manager_linux.go +++ b/client/internal/dns/network_manager_linux.go @@ -14,8 +14,6 @@ import ( "github.com/hashicorp/go-version" "github.com/miekg/dns" log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/iface" ) const ( @@ -72,7 +70,7 @@ func (s networkManagerConnSettings) cleanDeprecatedSettings() { } } -func newNetworkManagerDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error) { +func newNetworkManagerDbusConfigurator(wgInterface WGIface) (hostManager, error) { obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode) if err != nil { return nil, err diff --git a/client/internal/dns/resolvconf_linux.go b/client/internal/dns/resolvconf_linux.go index 0d2616f31..d50ca4550 100644 --- a/client/internal/dns/resolvconf_linux.go +++ b/client/internal/dns/resolvconf_linux.go @@ -8,8 +8,6 @@ import ( "strings" log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/iface" ) const resolvconfCommand = "resolvconf" @@ -18,7 +16,7 @@ type resolvconf struct { ifaceName string } -func newResolvConfConfigurator(wgInterface *iface.WGIface) (hostManager, error) { +func newResolvConfConfigurator(wgInterface WGIface) (hostManager, error) { return &resolvconf{ ifaceName: wgInterface.Name(), }, nil diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 2dd8bdeac..6dd8f1904 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -3,29 +3,20 @@ package dns import ( "context" "fmt" - "math/big" - "net" "net/netip" - "runtime" "sync" - "time" - "github.com/google/gopacket" - "github.com/google/gopacket/layers" "github.com/miekg/dns" "github.com/mitchellh/hashstructure/v2" log "github.com/sirupsen/logrus" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/iface" ) -const ( - defaultPort = 53 - customPort = 5053 - defaultIP = "127.0.0.1" - customIP = "127.0.0.153" -) +// ReadyListener is a notification mechanism what indicate the server is ready to handle host dns address changes +type ReadyListener interface { + OnReady() +} // Server is a dns server interface type Server interface { @@ -33,6 +24,7 @@ type Server interface { Stop() DnsIP() string UpdateDNSServer(serial uint64, update nbdns.Config) error + OnUpdatedHostDNSServer(strings []string) } type registeredHandlerMap map[string]handlerWithStop @@ -42,21 +34,19 @@ type DefaultServer struct { ctx context.Context ctxCancel context.CancelFunc mux sync.Mutex - udpFilterHookID string - server *dns.Server - dnsMux *dns.ServeMux + service service dnsMuxMap registeredHandlerMap localResolver *localResolver - wgInterface *iface.WGIface + wgInterface WGIface hostManager hostManager updateSerial uint64 - listenerIsRunning bool - runtimePort int - runtimeIP string previousConfigHash uint64 currentConfig hostDNSConfig - customAddress *netip.AddrPort - enabled bool + + // permanent related properties + permanent bool + hostsDnsList []string + hostsDnsListLock sync.Mutex } type handlerWithStop interface { @@ -70,9 +60,7 @@ type muxUpdate struct { } // NewDefaultServer returns a new dns server -func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAddress string, initialDnsCfg *nbdns.Config) (*DefaultServer, error) { - mux := dns.NewServeMux() - +func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress string) (*DefaultServer, error) { var addrPort *netip.AddrPort if customAddress != "" { parsedAddrPort, err := netip.ParseAddrPort(customAddress) @@ -82,37 +70,44 @@ func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAdd addrPort = &parsedAddrPort } - ctx, stop := context.WithCancel(ctx) + var dnsService service + if wgInterface.IsUserspaceBind() { + dnsService = newServiceViaMemory(wgInterface) + } else { + dnsService = newServiceViaListener(wgInterface, addrPort) + } + return newDefaultServer(ctx, wgInterface, dnsService), nil +} + +// NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems +func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, hostsDnsList []string) *DefaultServer { + log.Debugf("host dns address list is: %v", hostsDnsList) + ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface)) + ds.permanent = true + ds.hostsDnsList = hostsDnsList + ds.addHostRootZone() + setServerDns(ds) + return ds +} + +func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service) *DefaultServer { + ctx, stop := context.WithCancel(ctx) defaultServer := &DefaultServer{ ctx: ctx, ctxCancel: stop, - server: &dns.Server{ - Net: "udp", - Handler: mux, - UDPSize: 65535, - }, - dnsMux: mux, + service: dnsService, dnsMuxMap: make(registeredHandlerMap), localResolver: &localResolver{ registeredMap: make(registrationMap), }, - wgInterface: wgInterface, - customAddress: addrPort, + wgInterface: wgInterface, } - if initialDnsCfg != nil { - defaultServer.enabled = hasValidDnsServer(initialDnsCfg) - } - - if wgInterface.IsUserspaceBind() { - defaultServer.evelRuntimeAddressForUserspace() - } - - return defaultServer, nil + return defaultServer } -// Initialize instantiate host manager. It required to be initialized wginterface +// Initialize instantiate host manager and the dns service func (s *DefaultServer) Initialize() (err error) { s.mux.Lock() defer s.mux.Unlock() @@ -121,72 +116,23 @@ func (s *DefaultServer) Initialize() (err error) { return nil } - if !s.wgInterface.IsUserspaceBind() { - s.evalRuntimeAddress() + if s.permanent { + err = s.service.Listen() + if err != nil { + return err + } } + s.hostManager, err = newHostManager(s.wgInterface) return } -// listen runs the listener in a go routine -func (s *DefaultServer) listen() { - // nil check required in unit tests - if s.wgInterface != nil && s.wgInterface.IsUserspaceBind() { - s.udpFilterHookID = s.filterDNSTraffic() - s.setListenerStatus(true) - return - } - - log.Debugf("starting dns on %s", s.server.Addr) - - go func() { - s.setListenerStatus(true) - defer s.setListenerStatus(false) - - err := s.server.ListenAndServe() - if err != nil { - log.Errorf("dns server running with %d port returned an error: %v. Will not retry", s.runtimePort, err) - } - }() -} - // DnsIP returns the DNS resolver server IP address // // When kernel space interface used it return real DNS server listener IP address // For bind interface, fake DNS resolver address returned (second last IP address from Nebird network) func (s *DefaultServer) DnsIP() string { - if !s.enabled { - return "" - } - return s.runtimeIP -} - -func (s *DefaultServer) getFirstListenerAvailable() (string, int, error) { - ips := []string{defaultIP, customIP} - if runtime.GOOS != "darwin" && s.wgInterface != nil { - ips = append([]string{s.wgInterface.Address().IP.String()}, ips...) - } - ports := []int{defaultPort, customPort} - for _, port := range ports { - for _, ip := range ips { - addrString := fmt.Sprintf("%s:%d", ip, port) - udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString)) - probeListener, err := net.ListenUDP("udp", udpAddr) - if err == nil { - err = probeListener.Close() - if err != nil { - log.Errorf("got an error closing the probe listener, error: %s", err) - } - return ip, port, nil - } - log.Warnf("binding dns on %s is not available, error: %s", addrString, err) - } - } - return "", 0, fmt.Errorf("unable to find an unused ip and port combination. IPs tested: %v and ports %v", ips, ports) -} - -func (s *DefaultServer) setListenerStatus(running bool) { - s.listenerIsRunning = running + return s.service.RuntimeIP() } // Stop stops the server @@ -202,37 +148,23 @@ func (s *DefaultServer) Stop() { } } - err := s.stopListener() - if err != nil { - log.Error(err) - } + s.service.Stop() } -func (s *DefaultServer) stopListener() error { - if s.wgInterface != nil && s.wgInterface.IsUserspaceBind() && s.listenerIsRunning { - // udpFilterHookID here empty only in the unit tests - if filter := s.wgInterface.GetFilter(); filter != nil && s.udpFilterHookID != "" { - if err := filter.RemovePacketHook(s.udpFilterHookID); err != nil { - log.Errorf("unable to remove DNS packet hook: %s", err) - } - } - s.udpFilterHookID = "" - s.listenerIsRunning = false - return nil - } +// OnUpdatedHostDNSServer update the DNS servers addresses for root zones +// It will be applied if the mgm server do not enforce DNS settings for root zone +func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) { + s.hostsDnsListLock.Lock() + defer s.hostsDnsListLock.Unlock() - if !s.listenerIsRunning { - return nil + s.hostsDnsList = hostsDnsList + _, ok := s.dnsMuxMap[nbdns.RootZone] + if ok { + log.Debugf("on new host DNS config but skip to apply it") + return } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - err := s.server.ShutdownContext(ctx) - if err != nil { - return fmt.Errorf("stopping dns server listener returned an error: %v", err) - } - return nil + log.Debugf("update host DNS settings: %+v", hostsDnsList) + s.addHostRootZone() } // UpdateDNSServer processes an update received from the management service @@ -283,12 +215,10 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { // is the service should be disabled, we stop the listener or fake resolver // and proceed with a regular update to clean up the handlers and records - if !update.ServiceEnable { - if err := s.stopListener(); err != nil { - log.Error(err) - } - } else if !s.listenerIsRunning { - s.listen() + if update.ServiceEnable { + _ = s.service.Listen() + } else if !s.permanent { + s.service.Stop() } localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones) @@ -299,15 +229,14 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { if err != nil { return fmt.Errorf("not applying dns update, error: %v", err) } - muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...) s.updateMux(muxUpdates) s.updateLocalResolver(localRecords) - s.currentConfig = dnsConfigToHostDNSConfig(update, s.runtimeIP, s.runtimePort) + s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort()) hostUpdate := s.currentConfig - if s.runtimePort != defaultPort && !s.hostManager.supportCustomPort() { + if s.service.RuntimePort() != defaultPort && !s.hostManager.supportCustomPort() { log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " + "Learn more at: https://netbird.io/docs/how-to-guides/nameservers#local-resolver") hostUpdate.routeAll = false @@ -412,19 +341,32 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) { muxUpdateMap := make(registeredHandlerMap) + var isContainRootUpdate bool + for _, update := range muxUpdates { - s.registerMux(update.domain, update.handler) + s.service.RegisterMux(update.domain, update.handler) muxUpdateMap[update.domain] = update.handler if existingHandler, ok := s.dnsMuxMap[update.domain]; ok { existingHandler.stop() } + + if update.domain == nbdns.RootZone { + isContainRootUpdate = true + } } for key, existingHandler := range s.dnsMuxMap { _, found := muxUpdateMap[key] if !found { - existingHandler.stop() - s.deregisterMux(key) + if !isContainRootUpdate && key == nbdns.RootZone { + s.hostsDnsListLock.Lock() + s.addHostRootZone() + s.hostsDnsListLock.Unlock() + existingHandler.stop() + } else { + existingHandler.stop() + s.service.DeregisterMux(key) + } } } @@ -455,14 +397,6 @@ func getNSHostPort(ns nbdns.NameServer) string { return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port) } -func (s *DefaultServer) registerMux(pattern string, handler dns.Handler) { - s.dnsMux.Handle(pattern, handler) -} - -func (s *DefaultServer) deregisterMux(pattern string) { - s.dnsMux.HandleRemove(pattern) -} - // upstreamCallbacks returns two functions, the first one is used to deactivate // the upstream resolver from the configuration, the second one is used to // reactivate it. Not allowed to call reactivate before deactivate. @@ -490,7 +424,7 @@ func (s *DefaultServer) upstreamCallbacks( for i, item := range s.currentConfig.domains { if _, found := removeIndex[item.domain]; found { s.currentConfig.domains[i].disabled = true - s.deregisterMux(item.domain) + s.service.DeregisterMux(item.domain) removeIndex[item.domain] = i } } @@ -507,7 +441,7 @@ func (s *DefaultServer) upstreamCallbacks( continue } s.currentConfig.domains[i].disabled = false - s.registerMux(domain, handler) + s.service.RegisterMux(domain, handler) } l := log.WithField("nameservers", nsGroup.NameServers) @@ -523,93 +457,13 @@ func (s *DefaultServer) upstreamCallbacks( return } -func (s *DefaultServer) filterDNSTraffic() string { - filter := s.wgInterface.GetFilter() - if filter == nil { - log.Error("can't set DNS filter, filter not initialized") - return "" +func (s *DefaultServer) addHostRootZone() { + handler := newUpstreamResolver(s.ctx) + handler.upstreamServers = make([]string, len(s.hostsDnsList)) + for n, ua := range s.hostsDnsList { + handler.upstreamServers[n] = fmt.Sprintf("%s:53", ua) } - - firstLayerDecoder := layers.LayerTypeIPv4 - if s.wgInterface.Address().Network.IP.To4() == nil { - firstLayerDecoder = layers.LayerTypeIPv6 - } - - hook := func(packetData []byte) bool { - // Decode the packet - packet := gopacket.NewPacket(packetData, firstLayerDecoder, gopacket.Default) - - // Get the UDP layer - udpLayer := packet.Layer(layers.LayerTypeUDP) - udp := udpLayer.(*layers.UDP) - - msg := new(dns.Msg) - if err := msg.Unpack(udp.Payload); err != nil { - log.Tracef("parse DNS request: %v", err) - return true - } - - writer := responseWriter{ - packet: packet, - device: s.wgInterface.GetDevice().Device, - } - go s.dnsMux.ServeDNS(&writer, msg) - return true - } - - return filter.AddUDPPacketHook(false, net.ParseIP(s.runtimeIP), uint16(s.runtimePort), hook) -} - -func (s *DefaultServer) evelRuntimeAddressForUserspace() { - s.runtimeIP = getLastIPFromNetwork(s.wgInterface.Address().Network, 1) - s.runtimePort = defaultPort - s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, s.runtimePort) -} - -func (s *DefaultServer) evalRuntimeAddress() { - defer func() { - s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, s.runtimePort) - }() - - if s.customAddress != nil { - s.runtimeIP = s.customAddress.Addr().String() - s.runtimePort = int(s.customAddress.Port()) - return - } - - ip, port, err := s.getFirstListenerAvailable() - if err != nil { - log.Error(err) - return - } - s.runtimeIP = ip - s.runtimePort = port -} - -func getLastIPFromNetwork(network *net.IPNet, fromEnd int) string { - // Calculate the last IP in the CIDR range - var endIP net.IP - for i := 0; i < len(network.IP); i++ { - endIP = append(endIP, network.IP[i]|^network.Mask[i]) - } - - // convert to big.Int - endInt := big.NewInt(0) - endInt.SetBytes(endIP) - - // subtract fromEnd from the last ip - fromEndBig := big.NewInt(int64(fromEnd)) - resultInt := big.NewInt(0) - resultInt.Sub(endInt, fromEndBig) - - return net.IP(resultInt.Bytes()).String() -} - -func hasValidDnsServer(cfg *nbdns.Config) bool { - for _, c := range cfg.NameServerGroups { - if c.Primary { - return true - } - } - return false + handler.deactivate = func() {} + handler.reactivate = func() {} + s.service.RegisterMux(nbdns.RootZone, handler) } diff --git a/client/internal/dns/server_export.go b/client/internal/dns/server_export.go new file mode 100644 index 000000000..bd1f74266 --- /dev/null +++ b/client/internal/dns/server_export.go @@ -0,0 +1,29 @@ +package dns + +import ( + "fmt" + "sync" +) + +var ( + mutex sync.Mutex + server Server +) + +// GetServerDns export the DNS server instance in static way. It used by the Mobile client +func GetServerDns() (Server, error) { + mutex.Lock() + if server == nil { + mutex.Unlock() + return nil, fmt.Errorf("DNS server not instantiated yet") + } + s := server + mutex.Unlock() + return s, nil +} + +func setServerDns(newServerServer Server) { + mutex.Lock() + server = newServerServer + defer mutex.Unlock() +} diff --git a/client/internal/dns/server_export_test.go b/client/internal/dns/server_export_test.go new file mode 100644 index 000000000..784dcb3ad --- /dev/null +++ b/client/internal/dns/server_export_test.go @@ -0,0 +1,24 @@ +package dns + +import ( + "testing" +) + +func TestGetServerDns(t *testing.T) { + _, err := GetServerDns() + if err == nil { + t.Errorf("invalid dns server instance") + } + + srv := &MockServer{} + setServerDns(srv) + + srvB, err := GetServerDns() + if err != nil { + t.Errorf("invalid dns server instance: %s", err) + } + + if srvB != srv { + t.Errorf("missmatch dns instances") + } +} diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 2b234fcc0..73349de89 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -11,14 +11,53 @@ import ( "time" "github.com/golang/mock/gomock" - "github.com/miekg/dns" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/firewall/uspfilter" "github.com/netbirdio/netbird/client/internal/stdnet" nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/formatter" "github.com/netbirdio/netbird/iface" pfmock "github.com/netbirdio/netbird/iface/mocks" ) +type mocWGIface struct { + filter iface.PacketFilter +} + +func (w *mocWGIface) Name() string { + panic("implement me") +} + +func (w *mocWGIface) Address() iface.WGAddress { + ip, network, _ := net.ParseCIDR("100.66.100.0/24") + return iface.WGAddress{ + IP: ip, + Network: network, + } +} + +func (w *mocWGIface) GetFilter() iface.PacketFilter { + return w.filter +} + +func (w *mocWGIface) GetDevice() *iface.DeviceWrapper { + panic("implement me") +} + +func (w *mocWGIface) GetInterfaceGUIDString() (string, error) { + panic("implement me") +} + +func (w *mocWGIface) IsUserspaceBind() bool { + return false +} + +func (w *mocWGIface) SetFilter(filter iface.PacketFilter) error { + w.filter = filter + return nil +} + var zoneRecords = []nbdns.SimpleRecord{ { Name: "peera.netbird.cloud", @@ -29,6 +68,11 @@ var zoneRecords = []nbdns.SimpleRecord{ }, } +func init() { + log.SetLevel(log.TraceLevel) + formatter.SetTextFormatter(log.StandardLogger()) +} + func TestUpdateDNSServer(t *testing.T) { nameServers := []nbdns.NameServer{ { @@ -224,7 +268,7 @@ func TestUpdateDNSServer(t *testing.T) { t.Log(err) } }() - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", nil) + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "") if err != nil { t.Fatal(err) } @@ -242,8 +286,6 @@ func TestUpdateDNSServer(t *testing.T) { dnsServer.dnsMuxMap = testCase.initUpstreamMap dnsServer.localResolver.registeredMap = testCase.initLocalMap dnsServer.updateSerial = testCase.initSerial - // pretend we are running - dnsServer.listenerIsRunning = true err = dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate) if err != nil { @@ -282,7 +324,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { ov := os.Getenv("NB_WG_KERNEL_DISABLED") defer os.Setenv("NB_WG_KERNEL_DISABLED", ov) - os.Setenv("NB_WG_KERNEL_DISABLED", "true") + _ = os.Setenv("NB_WG_KERNEL_DISABLED", "true") newNet, err := stdnet.NewNet(nil) if err != nil { t.Errorf("create stdnet: %v", err) @@ -316,17 +358,17 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { } packetfilter := pfmock.NewMockPacketFilter(ctrl) - packetfilter.EXPECT().SetNetwork(ipNet) packetfilter.EXPECT().DropOutgoing(gomock.Any()).AnyTimes() - packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() - packetfilter.EXPECT().RemovePacketHook(gomock.Any()).AnyTimes() + packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + packetfilter.EXPECT().RemovePacketHook(gomock.Any()) + packetfilter.EXPECT().SetNetwork(ipNet) if err := wgIface.SetFilter(packetfilter); err != nil { t.Errorf("set packet filter: %v", err) return } - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", nil) + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "") if err != nil { t.Errorf("create DNS server: %v", err) return @@ -421,21 +463,23 @@ func TestDNSServerStartStop(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - dnsServer := getDefaultServerWithNoHostManager(t, testCase.addrPort) - - dnsServer.hostManager = newNoopHostMocker() - dnsServer.listen() - time.Sleep(100 * time.Millisecond) - if !dnsServer.listenerIsRunning { - t.Fatal("dns server listener is not running") + dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort) + if err != nil { + t.Fatalf("%v", err) } + dnsServer.hostManager = newNoopHostMocker() + err = dnsServer.service.Listen() + if err != nil { + t.Fatalf("dns server is not running: %s", err) + } + time.Sleep(100 * time.Millisecond) defer dnsServer.Stop() - err := dnsServer.localResolver.registerRecord(zoneRecords[0]) + err = dnsServer.localResolver.registerRecord(zoneRecords[0]) if err != nil { t.Error(err) } - dnsServer.dnsMux.Handle("netbird.cloud", dnsServer.localResolver) + dnsServer.service.RegisterMux("netbird.cloud", dnsServer.localResolver) resolver := &net.Resolver{ PreferGo: true, @@ -443,7 +487,7 @@ func TestDNSServerStartStop(t *testing.T) { d := net.Dialer{ Timeout: time.Second * 5, } - addr := fmt.Sprintf("%s:%d", dnsServer.runtimeIP, dnsServer.runtimePort) + addr := fmt.Sprintf("%s:%d", dnsServer.service.RuntimeIP(), dnsServer.service.RuntimePort()) conn, err := d.DialContext(ctx, network, addr) if err != nil { t.Log(err) @@ -478,7 +522,7 @@ func TestDNSServerStartStop(t *testing.T) { func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { hostManager := &mockHostConfigurator{} server := DefaultServer{ - dnsMux: dns.DefaultServeMux, + service: newServiceViaMemory(&mocWGIface{}), localResolver: &localResolver{ registeredMap: make(registrationMap), }, @@ -541,62 +585,237 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { } } -func getDefaultServerWithNoHostManager(t *testing.T, addrPort string) *DefaultServer { - mux := dns.NewServeMux() +func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { + wgIFace, err := createWgInterfaceWithBind(t) + if err != nil { + t.Fatal("failed to initialize wg interface") + } + defer wgIFace.Close() - var parsedAddrPort *netip.AddrPort - if addrPort != "" { - parsed, err := netip.ParseAddrPort(addrPort) - if err != nil { - t.Fatal(err) - } - parsedAddrPort = &parsed + var dnsList []string + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList) + err = dnsServer.Initialize() + if err != nil { + t.Errorf("failed to initialize DNS server: %v", err) + return + } + defer dnsServer.Stop() + + dnsServer.OnUpdatedHostDNSServer([]string{"8.8.8.8"}) + + resolver := newDnsResolver(dnsServer.service.RuntimeIP(), dnsServer.service.RuntimePort()) + _, err = resolver.LookupHost(context.Background(), "netbird.io") + if err != nil { + t.Errorf("failed to resolve: %s", err) + } +} + +func TestDNSPermanent_updateUpstream(t *testing.T) { + wgIFace, err := createWgInterfaceWithBind(t) + if err != nil { + t.Fatal("failed to initialize wg interface") + } + defer wgIFace.Close() + + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}) + err = dnsServer.Initialize() + if err != nil { + t.Errorf("failed to initialize DNS server: %v", err) + return + } + defer dnsServer.Stop() + + // check initial state + resolver := newDnsResolver(dnsServer.service.RuntimeIP(), dnsServer.service.RuntimePort()) + _, err = resolver.LookupHost(context.Background(), "netbird.io") + if err != nil { + t.Errorf("failed to resolve: %s", err) } - dnsServer := &dns.Server{ - Net: "udp", - Handler: mux, - UDPSize: 65535, - } - - ctx, cancel := context.WithCancel(context.TODO()) - - ds := &DefaultServer{ - ctx: ctx, - ctxCancel: cancel, - server: dnsServer, - dnsMux: mux, - dnsMuxMap: make(registeredHandlerMap), - localResolver: &localResolver{ - registeredMap: make(registrationMap), + update := nbdns.Config{ + ServiceEnable: true, + CustomZones: []nbdns.CustomZone{ + { + Domain: "netbird.cloud", + Records: zoneRecords, + }, + }, + NameServerGroups: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("8.8.4.4"), + NSType: nbdns.UDPNameServerType, + Port: 53, + }, + }, + Enabled: true, + Primary: true, + }, }, - customAddress: parsedAddrPort, - } - ds.evalRuntimeAddress() - return ds -} - -func TestGetLastIPFromNetwork(t *testing.T) { - tests := []struct { - addr string - ip string - }{ - {"2001:db8::/32", "2001:db8:ffff:ffff:ffff:ffff:ffff:fffe"}, - {"192.168.0.0/30", "192.168.0.2"}, - {"192.168.0.0/16", "192.168.255.254"}, - {"192.168.0.0/24", "192.168.0.254"}, } - for _, tt := range tests { - _, ipnet, err := net.ParseCIDR(tt.addr) - if err != nil { - t.Errorf("Error parsing CIDR: %v", err) - return - } + err = dnsServer.UpdateDNSServer(1, update) + if err != nil { + t.Errorf("failed to update dns server: %s", err) + } - lastIP := getLastIPFromNetwork(ipnet, 1) - if lastIP != tt.ip { - t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP) - } + _, err = resolver.LookupHost(context.Background(), "netbird.io") + if err != nil { + t.Errorf("failed to resolve: %s", err) + } + ips, err := resolver.LookupHost(context.Background(), zoneRecords[0].Name) + if err != nil { + t.Fatalf("failed resolve zone record: %v", err) + } + if ips[0] != zoneRecords[0].RData { + t.Fatalf("invalid zone record: %v", err) + } + + update2 := nbdns.Config{ + ServiceEnable: true, + CustomZones: []nbdns.CustomZone{ + { + Domain: "netbird.cloud", + Records: zoneRecords, + }, + }, + NameServerGroups: []*nbdns.NameServerGroup{}, + } + + err = dnsServer.UpdateDNSServer(2, update2) + if err != nil { + t.Errorf("failed to update dns server: %s", err) + } + + _, err = resolver.LookupHost(context.Background(), "netbird.io") + if err != nil { + t.Errorf("failed to resolve: %s", err) + } + + ips, err = resolver.LookupHost(context.Background(), zoneRecords[0].Name) + if err != nil { + t.Fatalf("failed resolve zone record: %v", err) + } + if ips[0] != zoneRecords[0].RData { + t.Fatalf("invalid zone record: %v", err) + } +} + +func TestDNSPermanent_matchOnly(t *testing.T) { + wgIFace, err := createWgInterfaceWithBind(t) + if err != nil { + t.Fatal("failed to initialize wg interface") + } + defer wgIFace.Close() + + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}) + err = dnsServer.Initialize() + if err != nil { + t.Errorf("failed to initialize DNS server: %v", err) + return + } + defer dnsServer.Stop() + + // check initial state + resolver := newDnsResolver(dnsServer.service.RuntimeIP(), dnsServer.service.RuntimePort()) + _, err = resolver.LookupHost(context.Background(), "netbird.io") + if err != nil { + t.Errorf("failed to resolve: %s", err) + } + + update := nbdns.Config{ + ServiceEnable: true, + CustomZones: []nbdns.CustomZone{ + { + Domain: "netbird.cloud", + Records: zoneRecords, + }, + }, + NameServerGroups: []*nbdns.NameServerGroup{ + { + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("8.8.4.4"), + NSType: nbdns.UDPNameServerType, + Port: 53, + }, + }, + Domains: []string{"customdomain.com"}, + Primary: false, + }, + }, + } + + err = dnsServer.UpdateDNSServer(1, update) + if err != nil { + t.Errorf("failed to update dns server: %s", err) + } + + _, err = resolver.LookupHost(context.Background(), "netbird.io") + if err != nil { + t.Errorf("failed to resolve: %s", err) + } + ips, err := resolver.LookupHost(context.Background(), zoneRecords[0].Name) + if err != nil { + t.Fatalf("failed resolve zone record: %v", err) + } + if ips[0] != zoneRecords[0].RData { + t.Fatalf("invalid zone record: %v", err) + } + _, err = resolver.LookupHost(context.Background(), "customdomain.com") + if err != nil { + t.Errorf("failed to resolve: %s", err) + } +} + +func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { + ov := os.Getenv("NB_WG_KERNEL_DISABLED") + defer os.Setenv("NB_WG_KERNEL_DISABLED", ov) + + _ = os.Setenv("NB_WG_KERNEL_DISABLED", "true") + newNet, err := stdnet.NewNet(nil) + if err != nil { + t.Fatalf("create stdnet: %v", err) + return nil, nil + } + + wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.2/24", iface.DefaultMTU, nil, newNet) + if err != nil { + t.Fatalf("build interface wireguard: %v", err) + return nil, err + } + + err = wgIface.Create() + if err != nil { + t.Fatalf("crate and init wireguard interface: %v", err) + return nil, err + } + + pf, err := uspfilter.Create(wgIface) + if err != nil { + t.Fatalf("failed to create uspfilter: %v", err) + return nil, err + } + + err = wgIface.SetFilter(pf) + if err != nil { + t.Fatalf("set packet filter: %v", err) + return nil, err + } + + return wgIface, nil +} + +func newDnsResolver(ip string, port int) *net.Resolver { + return &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: time.Second * 3, + } + addr := fmt.Sprintf("%s:%d", ip, port) + return d.DialContext(ctx, network, addr) + }, } } diff --git a/client/internal/dns/service.go b/client/internal/dns/service.go new file mode 100644 index 000000000..523976e54 --- /dev/null +++ b/client/internal/dns/service.go @@ -0,0 +1,18 @@ +package dns + +import ( + "github.com/miekg/dns" +) + +const ( + defaultPort = 53 +) + +type service interface { + Listen() error + Stop() + RegisterMux(domain string, handler dns.Handler) + DeregisterMux(key string) + RuntimePort() int + RuntimeIP() string +} diff --git a/client/internal/dns/service_listener.go b/client/internal/dns/service_listener.go new file mode 100644 index 000000000..687ca2459 --- /dev/null +++ b/client/internal/dns/service_listener.go @@ -0,0 +1,145 @@ +package dns + +import ( + "context" + "fmt" + "net" + "net/netip" + "runtime" + "sync" + "time" + + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" +) + +const ( + customPort = 5053 + defaultIP = "127.0.0.1" + customIP = "127.0.0.153" +) + +type serviceViaListener struct { + wgInterface WGIface + dnsMux *dns.ServeMux + customAddr *netip.AddrPort + server *dns.Server + runtimeIP string + runtimePort int + listenerIsRunning bool + listenerFlagLock sync.Mutex +} + +func newServiceViaListener(wgIface WGIface, customAddr *netip.AddrPort) *serviceViaListener { + mux := dns.NewServeMux() + + s := &serviceViaListener{ + wgInterface: wgIface, + dnsMux: mux, + customAddr: customAddr, + server: &dns.Server{ + Net: "udp", + Handler: mux, + UDPSize: 65535, + }, + } + return s +} + +func (s *serviceViaListener) Listen() error { + s.listenerFlagLock.Lock() + defer s.listenerFlagLock.Unlock() + + if s.listenerIsRunning { + return nil + } + + var err error + s.runtimeIP, s.runtimePort, err = s.evalRuntimeAddress() + if err != nil { + log.Errorf("failed to eval runtime address: %s", err) + return err + } + s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, s.runtimePort) + + log.Debugf("starting dns on %s", s.server.Addr) + go func() { + s.setListenerStatus(true) + defer s.setListenerStatus(false) + + err := s.server.ListenAndServe() + if err != nil { + log.Errorf("dns server running with %d port returned an error: %v. Will not retry", s.runtimePort, err) + } + }() + return nil +} + +func (s *serviceViaListener) Stop() { + s.listenerFlagLock.Lock() + defer s.listenerFlagLock.Unlock() + + if !s.listenerIsRunning { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := s.server.ShutdownContext(ctx) + if err != nil { + log.Errorf("stopping dns server listener returned an error: %v", err) + } +} + +func (s *serviceViaListener) RegisterMux(pattern string, handler dns.Handler) { + s.dnsMux.Handle(pattern, handler) +} + +func (s *serviceViaListener) DeregisterMux(pattern string) { + s.dnsMux.HandleRemove(pattern) +} + +func (s *serviceViaListener) RuntimePort() int { + return s.runtimePort +} + +func (s *serviceViaListener) RuntimeIP() string { + return s.runtimeIP +} + +func (s *serviceViaListener) setListenerStatus(running bool) { + s.listenerIsRunning = running +} + +func (s *serviceViaListener) getFirstListenerAvailable() (string, int, error) { + ips := []string{defaultIP, customIP} + if runtime.GOOS != "darwin" { + ips = append([]string{s.wgInterface.Address().IP.String()}, ips...) + } + ports := []int{defaultPort, customPort} + for _, port := range ports { + for _, ip := range ips { + addrString := fmt.Sprintf("%s:%d", ip, port) + udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString)) + probeListener, err := net.ListenUDP("udp", udpAddr) + if err == nil { + err = probeListener.Close() + if err != nil { + log.Errorf("got an error closing the probe listener, error: %s", err) + } + return ip, port, nil + } + log.Warnf("binding dns on %s is not available, error: %s", addrString, err) + } + } + return "", 0, fmt.Errorf("unable to find an unused ip and port combination. IPs tested: %v and ports %v", ips, ports) +} + +func (s *serviceViaListener) evalRuntimeAddress() (string, int, error) { + if s.customAddr != nil { + return s.customAddr.Addr().String(), int(s.customAddr.Port()), nil + } + + return s.getFirstListenerAvailable() +} diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go new file mode 100644 index 000000000..128dda840 --- /dev/null +++ b/client/internal/dns/service_memory.go @@ -0,0 +1,139 @@ +package dns + +import ( + "fmt" + "math/big" + "net" + "sync" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" +) + +type serviceViaMemory struct { + wgInterface WGIface + dnsMux *dns.ServeMux + runtimeIP string + runtimePort int + udpFilterHookID string + listenerIsRunning bool + listenerFlagLock sync.Mutex +} + +func newServiceViaMemory(wgIface WGIface) *serviceViaMemory { + s := &serviceViaMemory{ + wgInterface: wgIface, + dnsMux: dns.NewServeMux(), + + runtimeIP: getLastIPFromNetwork(wgIface.Address().Network, 1), + runtimePort: defaultPort, + } + return s +} + +func (s *serviceViaMemory) Listen() error { + s.listenerFlagLock.Lock() + defer s.listenerFlagLock.Unlock() + + if s.listenerIsRunning { + return nil + } + + var err error + s.udpFilterHookID, err = s.filterDNSTraffic() + if err != nil { + return err + } + s.listenerIsRunning = true + + log.Debugf("dns service listening on: %s", s.RuntimeIP()) + return nil +} + +func (s *serviceViaMemory) Stop() { + s.listenerFlagLock.Lock() + defer s.listenerFlagLock.Unlock() + + if !s.listenerIsRunning { + return + } + + if err := s.wgInterface.GetFilter().RemovePacketHook(s.udpFilterHookID); err != nil { + log.Errorf("unable to remove DNS packet hook: %s", err) + } + + s.listenerIsRunning = false +} + +func (s *serviceViaMemory) RegisterMux(pattern string, handler dns.Handler) { + s.dnsMux.Handle(pattern, handler) +} + +func (s *serviceViaMemory) DeregisterMux(pattern string) { + s.dnsMux.HandleRemove(pattern) +} + +func (s *serviceViaMemory) RuntimePort() int { + return s.runtimePort +} + +func (s *serviceViaMemory) RuntimeIP() string { + return s.runtimeIP +} + +func (s *serviceViaMemory) filterDNSTraffic() (string, error) { + filter := s.wgInterface.GetFilter() + if filter == nil { + return "", fmt.Errorf("can't set DNS filter, filter not initialized") + } + + firstLayerDecoder := layers.LayerTypeIPv4 + if s.wgInterface.Address().Network.IP.To4() == nil { + firstLayerDecoder = layers.LayerTypeIPv6 + } + + hook := func(packetData []byte) bool { + // Decode the packet + packet := gopacket.NewPacket(packetData, firstLayerDecoder, gopacket.Default) + + // Get the UDP layer + udpLayer := packet.Layer(layers.LayerTypeUDP) + udp := udpLayer.(*layers.UDP) + + msg := new(dns.Msg) + if err := msg.Unpack(udp.Payload); err != nil { + log.Tracef("parse DNS request: %v", err) + return true + } + + writer := responseWriter{ + packet: packet, + device: s.wgInterface.GetDevice().Device, + } + go s.dnsMux.ServeDNS(&writer, msg) + return true + } + + return filter.AddUDPPacketHook(false, net.ParseIP(s.runtimeIP), uint16(s.runtimePort), hook), nil +} + +func getLastIPFromNetwork(network *net.IPNet, fromEnd int) string { + // Calculate the last IP in the CIDR range + var endIP net.IP + for i := 0; i < len(network.IP); i++ { + endIP = append(endIP, network.IP[i]|^network.Mask[i]) + } + + // convert to big.Int + endInt := big.NewInt(0) + endInt.SetBytes(endIP) + + // subtract fromEnd from the last ip + fromEndBig := big.NewInt(int64(fromEnd)) + resultInt := big.NewInt(0) + resultInt.Sub(endInt, fromEndBig) + + return net.IP(resultInt.Bytes()).String() +} diff --git a/client/internal/dns/service_memory_test.go b/client/internal/dns/service_memory_test.go new file mode 100644 index 000000000..bea4f4ce8 --- /dev/null +++ b/client/internal/dns/service_memory_test.go @@ -0,0 +1,31 @@ +package dns + +import ( + "net" + "testing" +) + +func TestGetLastIPFromNetwork(t *testing.T) { + tests := []struct { + addr string + ip string + }{ + {"2001:db8::/32", "2001:db8:ffff:ffff:ffff:ffff:ffff:fffe"}, + {"192.168.0.0/30", "192.168.0.2"}, + {"192.168.0.0/16", "192.168.255.254"}, + {"192.168.0.0/24", "192.168.0.254"}, + } + + for _, tt := range tests { + _, ipnet, err := net.ParseCIDR(tt.addr) + if err != nil { + t.Errorf("Error parsing CIDR: %v", err) + return + } + + lastIP := getLastIPFromNetwork(ipnet, 1) + if lastIP != tt.ip { + t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP) + } + } +} diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index e870181af..0358b0251 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -15,7 +15,6 @@ import ( "golang.org/x/sys/unix" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/iface" ) const ( @@ -53,7 +52,7 @@ type systemdDbusLinkDomainsInput struct { MatchOnly bool } -func newSystemdDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error) { +func newSystemdDbusConfigurator(wgInterface WGIface) (hostManager, error) { iface, err := net.InterfaceByName(wgInterface.Name()) if err != nil { return nil, err diff --git a/client/internal/dns/wgiface.go b/client/internal/dns/wgiface.go new file mode 100644 index 000000000..ee487867a --- /dev/null +++ b/client/internal/dns/wgiface.go @@ -0,0 +1,14 @@ +//go:build !windows + +package dns + +import "github.com/netbirdio/netbird/iface" + +// WGIface defines subset methods of interface required for manager +type WGIface interface { + Name() string + Address() iface.WGAddress + IsUserspaceBind() bool + GetFilter() iface.PacketFilter + GetDevice() *iface.DeviceWrapper +} diff --git a/client/internal/dns/wgiface_windows.go b/client/internal/dns/wgiface_windows.go new file mode 100644 index 000000000..80705a9c0 --- /dev/null +++ b/client/internal/dns/wgiface_windows.go @@ -0,0 +1,13 @@ +package dns + +import "github.com/netbirdio/netbird/iface" + +// WGIface defines subset methods of interface required for manager +type WGIface interface { + Name() string + Address() iface.WGAddress + IsUserspaceBind() bool + GetFilter() iface.PacketFilter + GetDevice() *iface.DeviceWrapper + GetInterfaceGUIDString() (string, error) +} diff --git a/client/internal/engine.go b/client/internal/engine.go index 9cab064b1..ad9b79d7a 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -190,23 +190,25 @@ func (e *Engine) Start() error { } var routes []*route.Route - var dnsCfg *nbdns.Config if runtime.GOOS == "android" { - routes, dnsCfg, err = e.readInitialSettings() + routes, err = e.readInitialSettings() if err != nil { return err } - } - - if e.dnsServer == nil { + if e.dnsServer == nil { + e.dnsServer = dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses) + go e.mobileDep.DnsReadyListener.OnReady() + } + } else { // todo fix custom address - dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress, dnsCfg) - if err != nil { - e.close() - return err + if e.dnsServer == nil { + e.dnsServer, err = dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress) + if err != nil { + e.close() + return err + } } - e.dnsServer = dnsServer } e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, routes) @@ -1045,14 +1047,13 @@ func (e *Engine) close() { } } -func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { +func (e *Engine) readInitialSettings() ([]*route.Route, error) { netMap, err := e.mgmClient.GetNetworkMap() if err != nil { - return nil, nil, err + return nil, err } routes := toRoutes(netMap.GetRoutes()) - dnsCfg := toDNSConfig(netMap.GetDNSConfig()) - return routes, &dnsCfg, nil + return routes, nil } func findIPFromInterfaceName(ifaceName string) (net.IP, error) { diff --git a/client/internal/mobile_dependency.go b/client/internal/mobile_dependency.go index 18742b4cc..fc8fa61ce 100644 --- a/client/internal/mobile_dependency.go +++ b/client/internal/mobile_dependency.go @@ -1,6 +1,7 @@ package internal import ( + "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/iface" @@ -8,7 +9,9 @@ import ( // MobileDependency collect all dependencies for mobile platform type MobileDependency struct { - TunAdapter iface.TunAdapter - IFaceDiscover stdnet.ExternalIFaceDiscover - RouteListener routemanager.RouteListener + TunAdapter iface.TunAdapter + IFaceDiscover stdnet.ExternalIFaceDiscover + RouteListener routemanager.RouteListener + HostDNSAddresses []string + DnsReadyListener dns.ReadyListener } diff --git a/client/server/server.go b/client/server/server.go index 5743e57ed..4633260b2 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -102,7 +102,7 @@ func (s *Server) Start() error { } go func() { - if err := internal.RunClient(ctx, config, s.statusRecorder, nil, nil, nil); err != nil { + if err := internal.RunClient(ctx, config, s.statusRecorder); err != nil { log.Errorf("init connections: %v", err) } }() @@ -391,7 +391,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes } go func() { - if err := internal.RunClient(ctx, s.config, s.statusRecorder, nil, nil, nil); err != nil { + if err := internal.RunClient(ctx, s.config, s.statusRecorder); err != nil { log.Errorf("run client connection: %v", err) return } diff --git a/iface/iface_android.go b/iface/iface_android.go index 6f47e5aa7..208eff7a8 100644 --- a/iface/iface_android.go +++ b/iface/iface_android.go @@ -5,7 +5,6 @@ import ( "sync" "github.com/pion/transport/v2" - log "github.com/sirupsen/logrus" ) // NewWGIFace Creates a new WireGuard interface instance @@ -34,7 +33,6 @@ func NewWGIFace(ifaceName string, address string, mtu int, tunAdapter TunAdapter func (w *WGIface) CreateOnMobile(mIFaceArgs MobileIFaceArguments) error { w.mu.Lock() defer w.mu.Unlock() - log.Debugf("create WireGuard interface %s", w.tun.DeviceName()) return w.tun.Create(mIFaceArgs) } diff --git a/iface/iface_nonandroid.go b/iface/iface_nonandroid.go index 9622207bb..da4ef13fd 100644 --- a/iface/iface_nonandroid.go +++ b/iface/iface_nonandroid.go @@ -7,7 +7,6 @@ import ( "sync" "github.com/pion/transport/v2" - log "github.com/sirupsen/logrus" ) // NewWGIFace Creates a new WireGuard interface instance @@ -38,6 +37,5 @@ func (w *WGIface) CreateOnMobile(mIFaceArgs MobileIFaceArguments) error { func (w *WGIface) Create() error { w.mu.Lock() defer w.mu.Unlock() - log.Debugf("create WireGuard interface %s", w.tun.DeviceName()) return w.tun.Create() } diff --git a/iface/tun_android.go b/iface/tun_android.go index e54a6f730..d45f05282 100644 --- a/iface/tun_android.go +++ b/iface/tun_android.go @@ -34,6 +34,7 @@ func newTunDevice(address WGAddress, mtu int, tunAdapter TunAdapter, transportNe } func (t *tunDevice) Create(mIFaceArgs MobileIFaceArguments) error { + log.Info("create tun interface") var err error routesString := t.routesToString(mIFaceArgs.Routes) t.fd, err = t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, mIFaceArgs.Dns, routesString) diff --git a/iface/tun_linux.go b/iface/tun_linux.go index bb3b5a47a..93c03436e 100644 --- a/iface/tun_linux.go +++ b/iface/tun_linux.go @@ -12,14 +12,14 @@ import ( func (c *tunDevice) Create() error { if WireGuardModuleIsLoaded() { - log.Info("using kernel WireGuard") + log.Infof("create tun interface with kernel WireGuard support: %s", c.DeviceName()) return c.createWithKernel() } if !tunModuleIsLoaded() { return fmt.Errorf("couldn't check or load tun module") } - log.Info("using userspace WireGuard") + log.Infof("create tun interface with userspace WireGuard support: %s", c.DeviceName()) var err error c.netInterface, err = c.createWithUserspace() if err != nil { From 7ddde41c92d57625327df29d840e169f46aa584b Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Mon, 17 Jul 2023 11:46:10 +0200 Subject: [PATCH 13/27] Do not persist filestore when deleting indices As both TokenID2UserID and HashedPAT2TokenID are in-memory indices and not stored in the file. --- management/server/file_store.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/management/server/file_store.go b/management/server/file_store.go index 4bbe95a10..0902c731f 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -334,7 +334,7 @@ func (s *FileStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error { delete(s.HashedPAT2TokenID, hashedToken) - return s.persist(s.storeFile) + return nil } // DeleteTokenID2UserIDIndex removes an entry from the indexing map TokenID2UserID @@ -344,7 +344,7 @@ func (s *FileStore) DeleteTokenID2UserIDIndex(tokenID string) error { delete(s.TokenID2UserID, tokenID) - return s.persist(s.storeFile) + return nil } // GetAccountByPrivateDomain returns account by private domain From e69ec6ab6a477034c34b945d58c03ee0a9a14091 Mon Sep 17 00:00:00 2001 From: Givi Khojanashvili Date: Tue, 18 Jul 2023 13:12:50 +0400 Subject: [PATCH 14/27] Optimize ACL performance (#994) * Optimize rules with All groups * Use IP sets in ACLs (nftables implementation) * Fix squash rule when we receive optimized rules list from management --- client/firewall/firewall.go | 4 + client/firewall/iptables/manager_linux.go | 4 + .../firewall/iptables/manager_linux_test.go | 10 +- client/firewall/nftables/manager_linux.go | 363 +++++++++++++++--- .../firewall/nftables/manager_linux_test.go | 23 +- client/firewall/nftables/rule_linux.go | 9 +- client/firewall/nftables/ruleset_linux.go | 115 ++++++ .../firewall/nftables/ruleset_linux_test.go | 122 ++++++ client/firewall/uspfilter/uspfilter.go | 4 + client/firewall/uspfilter/uspfilter_test.go | 14 +- client/internal/acl/manager.go | 118 +++++- client/internal/acl/manager_create.go | 6 +- client/internal/acl/manager_create_linux.go | 5 +- management/server/policy.go | 30 +- management/server/policy_test.go | 14 + 15 files changed, 727 insertions(+), 114 deletions(-) create mode 100644 client/firewall/nftables/ruleset_linux.go create mode 100644 client/firewall/nftables/ruleset_linux_test.go diff --git a/client/firewall/firewall.go b/client/firewall/firewall.go index f91adb7c1..5d003e2f0 100644 --- a/client/firewall/firewall.go +++ b/client/firewall/firewall.go @@ -51,6 +51,7 @@ type Manager interface { dPort *Port, direction RuleDirection, action Action, + ipsetName string, comment string, ) (Rule, error) @@ -60,5 +61,8 @@ type Manager interface { // Reset firewall to the default state Reset() error + // Flush the changes to firewall controller + Flush() error + // TODO: migrate routemanager firewal actions to this interface } diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 6ddab0b8f..a4a7a6c3b 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -92,6 +92,7 @@ func (m *Manager) AddFiltering( dPort *fw.Port, direction fw.RuleDirection, action fw.Action, + ipsetName string, comment string, ) (fw.Rule, error) { m.mutex.Lock() @@ -202,6 +203,9 @@ func (m *Manager) Reset() error { return nil } +// Flush doesn't need to be implemented for this manager +func (m *Manager) Flush() error { return nil } + // reset firewall chain, clear it and drop it func (m *Manager) reset(client *iptables.IPTables, table string) error { ok, err := client.ChainExists(table, ChainInputFilterName) diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index 7c78a4ee2..cbf9d4c76 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -68,7 +68,7 @@ func TestIptablesManager(t *testing.T) { t.Run("add first rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.2") port := &fw.Port{Values: []int{8080}} - rule1, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic") + rule1, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, true, rule1.(*Rule).specs...) @@ -81,7 +81,7 @@ func TestIptablesManager(t *testing.T) { Values: []int{8043: 8046}, } rule2, err = manager.AddFiltering( - ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTPS traffic from ports range") + ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTPS traffic from ports range") require.NoError(t, err, "failed to add rule") checkRuleSpecs(t, ipv4Client, ChainInputFilterName, true, rule2.(*Rule).specs...) @@ -107,7 +107,7 @@ func TestIptablesManager(t *testing.T) { // add second rule ip := net.ParseIP("10.20.0.3") port := &fw.Port{Values: []int{5353}} - _, err = manager.AddFiltering(ip, "udp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept Fake DNS traffic") + _, err = manager.AddFiltering(ip, "udp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept Fake DNS traffic") require.NoError(t, err, "failed to add rule") err = manager.Reset() @@ -167,9 +167,9 @@ func TestIptablesCreatePerformance(t *testing.T) { for i := 0; i < testMax; i++ { port := &fw.Port{Values: []int{1000 + i}} if i%2 == 0 { - _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic") + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") } else { - _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic") + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") } require.NoError(t, err, "failed to add rule") diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index e94f93f9e..71085276d 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -6,12 +6,14 @@ import ( "fmt" "net" "net/netip" + "strconv" "strings" "sync" + "time" "github.com/google/nftables" "github.com/google/nftables/expr" - "github.com/google/uuid" + log "github.com/sirupsen/logrus" "golang.org/x/sys/unix" fw "github.com/netbirdio/netbird/client/firewall" @@ -29,11 +31,14 @@ const ( FilterOutputChainName = "netbird-acl-output-filter" ) +var anyIP = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + // Manager of iptables firewall type Manager struct { mutex sync.Mutex - conn *nftables.Conn + rConn *nftables.Conn + sConn *nftables.Conn tableIPv4 *nftables.Table tableIPv6 *nftables.Table @@ -43,6 +48,10 @@ type Manager struct { filterInputChainIPv6 *nftables.Chain filterOutputChainIPv6 *nftables.Chain + rulesetManager *rulesetManager + setRemovedIPs map[string]struct{} + setRemoved map[string]*nftables.Set + wgIface iFaceMapper } @@ -54,8 +63,23 @@ type iFaceMapper interface { // Create nftables firewall manager func Create(wgIface iFaceMapper) (*Manager, error) { + // sConn is used for creating sets and adding/removing elements from them + // it's differ then rConn (which does create new conn for each flush operation) + // and is permanent. Using same connection for booth type of operations + // overloads netlink with high amount of rules ( > 10000) + sConn, err := nftables.New(nftables.AsLasting()) + if err != nil { + return nil, err + } + m := &Manager{ - conn: &nftables.Conn{}, + rConn: &nftables.Conn{}, + sConn: sConn, + + rulesetManager: newRuleManager(), + setRemovedIPs: map[string]struct{}{}, + setRemoved: map[string]*nftables.Set{}, + wgIface: wgIface, } @@ -77,6 +101,7 @@ func (m *Manager) AddFiltering( dPort *fw.Port, direction fw.RuleDirection, action fw.Action, + ipsetName string, comment string, ) (fw.Rule, error) { m.mutex.Lock() @@ -84,6 +109,7 @@ func (m *Manager) AddFiltering( var ( err error + ipset *nftables.Set table *nftables.Table chain *nftables.Chain ) @@ -107,6 +133,46 @@ func (m *Manager) AddFiltering( return nil, err } + rawIP := ip.To4() + if rawIP == nil { + rawIP = ip.To16() + } + + rulesetID := m.getRulesetID(ip, proto, sPort, dPort, direction, action, ipsetName) + + if ipsetName != "" { + // if we already have set with given name, just add ip to the set + // and return rule with new ID in other case let's create rule + // with fresh created set and set element + + var isSetNew bool + ipset, err := m.rConn.GetSetByName(table, ipsetName) + if err != nil { + if ipset, err = m.createSet(table, rawIP, ipsetName); err != nil { + return nil, fmt.Errorf("get set name: %v", err) + } + isSetNew = true + } + + if err := m.sConn.SetAddElements(ipset, []nftables.SetElement{{Key: rawIP}}); err != nil { + return nil, fmt.Errorf("add set element for the first time: %v", err) + } + if err := m.sConn.Flush(); err != nil { + return nil, fmt.Errorf("flush add elements: %v", err) + } + + if !isSetNew { + // if we already have nftables rules with set for given direction + // just add new rule to the ruleset and return new fw.Rule object + + if ruleset, ok := m.rulesetManager.getRuleset(rulesetID); ok { + return m.rulesetManager.addRule(ruleset, rawIP) + } + // if ipset exists but it is not linked to rule for given direction + // create new rule for direction and bind ipset to it later + } + } + ifaceKey := expr.MetaKeyIIFNAME if direction == fw.RuleDirectionOUT { ifaceKey = expr.MetaKeyOIFNAME @@ -146,39 +212,47 @@ func (m *Manager) AddFiltering( }) } - // don't use IP matching if IP is ip 0.0.0.0 - if s := ip.String(); s != "0.0.0.0" && s != "::" { + // check if rawIP contains zeroed IPv4 0.0.0.0 or same IPv6 value + // in that case not add IP match expression into the rule definition + if !bytes.HasPrefix(anyIP, rawIP) { // source address position - var adrLen, adrOffset uint32 - if ip.To4() == nil { - adrLen = 16 - adrOffset = 8 - } else { - adrLen = 4 - adrOffset = 12 + addrLen := uint32(len(rawIP)) + addrOffset := uint32(12) + if addrLen == 16 { + addrOffset = 8 } // change to destination address position if need if direction == fw.RuleDirectionOUT { - adrOffset += adrLen + addrOffset += addrLen } - ipToAdd, _ := netip.AddrFromSlice(ip) - add := ipToAdd.Unmap() - expressions = append(expressions, &expr.Payload{ DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, - Offset: adrOffset, - Len: adrLen, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: add.AsSlice(), + Offset: addrOffset, + Len: addrLen, }, ) + // add individual IP for match if no ipset defined + if ipset == nil { + expressions = append(expressions, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: rawIP, + }, + ) + } else { + expressions = append(expressions, + &expr.Lookup{ + SourceRegister: 1, + SetName: ipsetName, + SetID: ipset.ID, + }, + ) + } } if sPort != nil && len(sPort.Values) != 0 { @@ -219,39 +293,76 @@ func (m *Manager) AddFiltering( expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictDrop}) } - id := uuid.New().String() - userData := []byte(strings.Join([]string{id, comment}, " ")) + userData := []byte(strings.Join([]string{rulesetID, comment}, " ")) - _ = m.conn.InsertRule(&nftables.Rule{ + rule := m.rConn.InsertRule(&nftables.Rule{ Table: table, Chain: chain, Position: 0, Exprs: expressions, UserData: userData, }) - - if err := m.conn.Flush(); err != nil { - return nil, err + if err := m.rConn.Flush(); err != nil { + return nil, fmt.Errorf("flush insert rule: %v", err) } - list, err := m.conn.GetRules(table, chain) - if err != nil { - return nil, err + ruleset := m.rulesetManager.createRuleset(rulesetID, rule, ipset) + return m.rulesetManager.addRule(ruleset, rawIP) +} + +// getRulesetID returns ruleset ID based on given parameters +func (m *Manager) getRulesetID( + ip net.IP, + proto fw.Protocol, + sPort *fw.Port, + dPort *fw.Port, + direction fw.RuleDirection, + action fw.Action, + ipsetName string, +) string { + rulesetID := ":" + strconv.Itoa(int(direction)) + ":" + if sPort != nil { + rulesetID += sPort.String() + } + rulesetID += ":" + if dPort != nil { + rulesetID += dPort.String() + } + rulesetID += ":" + rulesetID += strconv.Itoa(int(action)) + if ipsetName == "" { + return "ip:" + ip.String() + rulesetID + } + return "set:" + ipsetName + rulesetID +} + +// createSet in given table by name +func (m *Manager) createSet( + table *nftables.Table, + rawIP []byte, + name string, +) (*nftables.Set, error) { + keyType := nftables.TypeIPAddr + if len(rawIP) == 16 { + keyType = nftables.TypeIP6Addr + } + // else we create new ipset and continue creating rule + ipset := &nftables.Set{ + Name: name, + Table: table, + Dynamic: true, + KeyType: keyType, } - // Add the rule to the chain - rule := &Rule{id: id} - for _, r := range list { - if bytes.Equal(r.UserData, userData) { - rule.Rule = r - break - } - } - if rule.Rule == nil { - return nil, fmt.Errorf("rule not found") + if err := m.rConn.AddSet(ipset, nil); err != nil { + return nil, fmt.Errorf("create set: %v", err) } - return rule, nil + if err := m.rConn.Flush(); err != nil { + return nil, fmt.Errorf("flush created set: %v", err) + } + + return ipset, nil } // chain returns the chain for the given IP address with specific settings @@ -315,7 +426,7 @@ func (m *Manager) table(family nftables.TableFamily) (*nftables.Table, error) { } func (m *Manager) createTableIfNotExists(family nftables.TableFamily) (*nftables.Table, error) { - tables, err := m.conn.ListTablesOfFamily(family) + tables, err := m.rConn.ListTablesOfFamily(family) if err != nil { return nil, fmt.Errorf("list of tables: %w", err) } @@ -326,7 +437,11 @@ func (m *Manager) createTableIfNotExists(family nftables.TableFamily) (*nftables } } - return m.conn.AddTable(&nftables.Table{Name: FilterTableName, Family: nftables.TableFamilyIPv4}), nil + table := m.rConn.AddTable(&nftables.Table{Name: FilterTableName, Family: nftables.TableFamilyIPv4}) + if err := m.rConn.Flush(); err != nil { + return nil, err + } + return table, nil } func (m *Manager) createChainIfNotExists( @@ -341,7 +456,7 @@ func (m *Manager) createChainIfNotExists( return nil, err } - chains, err := m.conn.ListChainsOfTableFamily(family) + chains, err := m.rConn.ListChainsOfTableFamily(family) if err != nil { return nil, fmt.Errorf("list of chains: %w", err) } @@ -362,7 +477,7 @@ func (m *Manager) createChainIfNotExists( Policy: &polAccept, } - chain = m.conn.AddChain(chain) + chain = m.rConn.AddChain(chain) ifaceKey := expr.MetaKeyIIFNAME shiftDSTAddr := 0 @@ -429,7 +544,7 @@ func (m *Manager) createChainIfNotExists( ) } - _ = m.conn.AddRule(&nftables.Rule{ + _ = m.rConn.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: expressions, @@ -444,12 +559,13 @@ func (m *Manager) createChainIfNotExists( }, &expr.Verdict{Kind: expr.VerdictDrop}, } - _ = m.conn.AddRule(&nftables.Rule{ + _ = m.rConn.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: expressions, }) - if err := m.conn.Flush(); err != nil { + + if err := m.rConn.Flush(); err != nil { return nil, err } @@ -458,16 +574,58 @@ func (m *Manager) createChainIfNotExists( // DeleteRule from the firewall by rule definition func (m *Manager) DeleteRule(rule fw.Rule) error { + m.mutex.Lock() + defer m.mutex.Unlock() + nativeRule, ok := rule.(*Rule) if !ok { return fmt.Errorf("invalid rule type") } - if err := m.conn.DelRule(nativeRule.Rule); err != nil { - return err + if nativeRule.nftRule == nil { + return nil } - return m.conn.Flush() + if nativeRule.nftSet != nil { + // call twice of delete set element raises error + // so we need to check if element is already removed + key := fmt.Sprintf("%s:%v", nativeRule.nftSet.Name, nativeRule.ip) + if _, ok := m.setRemovedIPs[key]; !ok { + err := m.sConn.SetDeleteElements(nativeRule.nftSet, []nftables.SetElement{{Key: nativeRule.ip}}) + if err != nil { + log.Errorf("delete elements for set %q: %v", nativeRule.nftSet.Name, err) + } + if err := m.sConn.Flush(); err != nil { + return err + } + m.setRemovedIPs[key] = struct{}{} + } + } + + if m.rulesetManager.deleteRule(nativeRule) { + // deleteRule indicates that we still have IP in the ruleset + // it means we should not remove the nftables rule but need to update set + // so we prepare IP to be removed from set on the next flush call + return nil + } + + // ruleset doesn't contain IP anymore (or contains only one), remove nft rule + if err := m.rConn.DelRule(nativeRule.nftRule); err != nil { + log.Errorf("failed to delete rule: %v", err) + } + if err := m.rConn.Flush(); err != nil { + return err + } + nativeRule.nftRule = nil + + if nativeRule.nftSet != nil { + if _, ok := m.setRemoved[nativeRule.nftSet.Name]; !ok { + m.setRemoved[nativeRule.nftSet.Name] = nativeRule.nftSet + } + nativeRule.nftSet = nil + } + + return nil } // Reset firewall to the default state @@ -475,27 +633,116 @@ func (m *Manager) Reset() error { m.mutex.Lock() defer m.mutex.Unlock() - chains, err := m.conn.ListChains() + chains, err := m.rConn.ListChains() if err != nil { return fmt.Errorf("list of chains: %w", err) } for _, c := range chains { if c.Name == FilterInputChainName || c.Name == FilterOutputChainName { - m.conn.DelChain(c) + m.rConn.DelChain(c) } } - tables, err := m.conn.ListTables() + tables, err := m.rConn.ListTables() if err != nil { return fmt.Errorf("list of tables: %w", err) } for _, t := range tables { if t.Name == FilterTableName { - m.conn.DelTable(t) + m.rConn.DelTable(t) } } - return m.conn.Flush() + return m.rConn.Flush() +} + +// Flush rule/chain/set operations from the buffer +// +// Method also get all rules after flush and refreshes handle values in the rulesets +func (m *Manager) Flush() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if err := m.flushWithBackoff(); err != nil { + return err + } + + // set must be removed after flush rule changes + // otherwise we will get error + for _, s := range m.setRemoved { + m.rConn.FlushSet(s) + m.rConn.DelSet(s) + } + + if len(m.setRemoved) > 0 { + if err := m.flushWithBackoff(); err != nil { + return err + } + } + + m.setRemovedIPs = map[string]struct{}{} + m.setRemoved = map[string]*nftables.Set{} + + if err := m.refreshRuleHandles(m.tableIPv4, m.filterInputChainIPv4); err != nil { + log.Errorf("failed to refresh rule handles ipv4 input chain: %v", err) + } + + if err := m.refreshRuleHandles(m.tableIPv4, m.filterOutputChainIPv4); err != nil { + log.Errorf("failed to refresh rule handles IPv4 output chain: %v", err) + } + + if err := m.refreshRuleHandles(m.tableIPv6, m.filterInputChainIPv6); err != nil { + log.Errorf("failed to refresh rule handles IPv6 input chain: %v", err) + } + + if err := m.refreshRuleHandles(m.tableIPv6, m.filterOutputChainIPv6); err != nil { + log.Errorf("failed to refresh rule handles IPv6 output chain: %v", err) + } + + return nil +} + +func (m *Manager) flushWithBackoff() (err error) { + backoff := 4 + backoffTime := 1000 * time.Millisecond + for i := 0; ; i++ { + err = m.rConn.Flush() + if err != nil { + if !strings.Contains(err.Error(), "busy") { + return + } + log.Error("failed to flush nftables, retrying...") + if i == backoff-1 { + return err + } + time.Sleep(backoffTime) + backoffTime = backoffTime * 2 + continue + } + break + } + return +} + +func (m *Manager) refreshRuleHandles(table *nftables.Table, chain *nftables.Chain) error { + if table == nil || chain == nil { + return nil + } + + list, err := m.rConn.GetRules(table, chain) + if err != nil { + return err + } + + for _, rule := range list { + if len(rule.UserData) != 0 { + if err := m.rulesetManager.setNftRuleHandle(rule); err != nil { + log.Errorf("failed to set rule handle: %v", err) + } + } + } + + return nil } func encodePort(port fw.Port) []byte { diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go index 9c0d247c5..164d5d0dc 100644 --- a/client/firewall/nftables/manager_linux_test.go +++ b/client/firewall/nftables/manager_linux_test.go @@ -55,7 +55,7 @@ func TestNftablesManager(t *testing.T) { // just check on the local interface manager, err := Create(mock) require.NoError(t, err) - time.Sleep(time.Second) + time.Sleep(time.Second * 3) defer func() { err = manager.Reset() @@ -75,11 +75,16 @@ func TestNftablesManager(t *testing.T) { fw.RuleDirectionIN, fw.ActionDrop, "", + "", ) require.NoError(t, err, "failed to add rule") + err = manager.Flush() + require.NoError(t, err, "failed to flush") + rules, err := testClient.GetRules(manager.tableIPv4, manager.filterInputChainIPv4) require.NoError(t, err, "failed to get rules") + // test expectations: // 1) regular rule // 2) "accept extra routed traffic rule" for the interface @@ -135,6 +140,9 @@ func TestNftablesManager(t *testing.T) { err = manager.DeleteRule(rule) require.NoError(t, err, "failed to delete rule") + err = manager.Flush() + require.NoError(t, err, "failed to flush") + rules, err = testClient.GetRules(manager.tableIPv4, manager.filterInputChainIPv4) require.NoError(t, err, "failed to get rules") // test expectations: @@ -167,7 +175,7 @@ func TestNFtablesCreatePerformance(t *testing.T) { // just check on the local interface manager, err := Create(mock) require.NoError(t, err) - time.Sleep(time.Second) + time.Sleep(time.Second * 3) defer func() { if err := manager.Reset(); err != nil { @@ -181,13 +189,18 @@ func TestNFtablesCreatePerformance(t *testing.T) { for i := 0; i < testMax; i++ { port := &fw.Port{Values: []int{1000 + i}} if i%2 == 0 { - _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic") + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") } else { - _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic") + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") } - require.NoError(t, err, "failed to add rule") + + if i%100 == 0 { + err = manager.Flush() + require.NoError(t, err, "failed to flush") + } } + t.Logf("execution avg per rule: %s", time.Since(start)/time.Duration(testMax)) }) } diff --git a/client/firewall/nftables/rule_linux.go b/client/firewall/nftables/rule_linux.go index 7fe0dcb5e..98d1147cd 100644 --- a/client/firewall/nftables/rule_linux.go +++ b/client/firewall/nftables/rule_linux.go @@ -6,11 +6,14 @@ import ( // Rule to handle management of rules type Rule struct { - *nftables.Rule - id string + nftRule *nftables.Rule + nftSet *nftables.Set + + ruleID string + ip []byte } // GetRuleID returns the rule id func (r *Rule) GetRuleID() string { - return r.id + return r.ruleID } diff --git a/client/firewall/nftables/ruleset_linux.go b/client/firewall/nftables/ruleset_linux.go new file mode 100644 index 000000000..536a5ee18 --- /dev/null +++ b/client/firewall/nftables/ruleset_linux.go @@ -0,0 +1,115 @@ +package nftables + +import ( + "bytes" + "fmt" + + "github.com/google/nftables" + "github.com/rs/xid" +) + +// nftRuleset links native firewall rule and ipset to ACL generated rules +type nftRuleset struct { + nftRule *nftables.Rule + nftSet *nftables.Set + issuedRules map[string]*Rule + rulesetID string +} + +type rulesetManager struct { + rulesets map[string]*nftRuleset + + nftSetName2rulesetID map[string]string + issuedRuleID2rulesetID map[string]string +} + +func newRuleManager() *rulesetManager { + return &rulesetManager{ + rulesets: map[string]*nftRuleset{}, + + nftSetName2rulesetID: map[string]string{}, + issuedRuleID2rulesetID: map[string]string{}, + } +} + +func (r *rulesetManager) getRuleset(rulesetID string) (*nftRuleset, bool) { + ruleset, ok := r.rulesets[rulesetID] + return ruleset, ok +} + +func (r *rulesetManager) createRuleset( + rulesetID string, + nftRule *nftables.Rule, + nftSet *nftables.Set, +) *nftRuleset { + ruleset := nftRuleset{ + rulesetID: rulesetID, + nftRule: nftRule, + nftSet: nftSet, + issuedRules: map[string]*Rule{}, + } + r.rulesets[ruleset.rulesetID] = &ruleset + if nftSet != nil { + r.nftSetName2rulesetID[nftSet.Name] = ruleset.rulesetID + } + return &ruleset +} + +func (r *rulesetManager) addRule( + ruleset *nftRuleset, + ip []byte, +) (*Rule, error) { + if _, ok := r.rulesets[ruleset.rulesetID]; !ok { + return nil, fmt.Errorf("ruleset not found") + } + + rule := Rule{ + nftRule: ruleset.nftRule, + nftSet: ruleset.nftSet, + ruleID: xid.New().String(), + ip: ip, + } + + ruleset.issuedRules[rule.ruleID] = &rule + r.issuedRuleID2rulesetID[rule.ruleID] = ruleset.rulesetID + + return &rule, nil +} + +// deleteRule from ruleset and returns true if contains other rules +func (r *rulesetManager) deleteRule(rule *Rule) bool { + rulesetID, ok := r.issuedRuleID2rulesetID[rule.ruleID] + if !ok { + return false + } + + ruleset := r.rulesets[rulesetID] + if ruleset.nftRule == nil { + return false + } + delete(r.issuedRuleID2rulesetID, rule.ruleID) + delete(ruleset.issuedRules, rule.ruleID) + + if len(ruleset.issuedRules) == 0 { + delete(r.rulesets, ruleset.rulesetID) + if rule.nftSet != nil { + delete(r.nftSetName2rulesetID, rule.nftSet.Name) + } + return false + } + return true +} + +// setNftRuleHandle finds rule by userdata which contains rulesetID and updates it's handle number +// +// This is important to do, because after we add rule to the nftables we can't update it until +// we set correct handle value to it. +func (r *rulesetManager) setNftRuleHandle(nftRule *nftables.Rule) error { + split := bytes.Split(nftRule.UserData, []byte(" ")) + ruleset, ok := r.rulesets[string(split[0])] + if !ok { + return fmt.Errorf("ruleset not found") + } + *ruleset.nftRule = *nftRule + return nil +} diff --git a/client/firewall/nftables/ruleset_linux_test.go b/client/firewall/nftables/ruleset_linux_test.go new file mode 100644 index 000000000..74b37d8f8 --- /dev/null +++ b/client/firewall/nftables/ruleset_linux_test.go @@ -0,0 +1,122 @@ +package nftables + +import ( + "testing" + + "github.com/google/nftables" + "github.com/stretchr/testify/require" +) + +func TestRulesetManager_createRuleset(t *testing.T) { + // Create a ruleset manager. + rulesetManager := newRuleManager() + + // Create a ruleset. + rulesetID := "ruleset-1" + nftRule := nftables.Rule{ + UserData: []byte(rulesetID), + } + ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, nil) + require.NotNil(t, ruleset, "createRuleset() failed") + require.Equal(t, ruleset.rulesetID, rulesetID, "rulesetID is incorrect") + require.Equal(t, ruleset.nftRule, &nftRule, "nftRule is incorrect") +} + +func TestRulesetManager_addRule(t *testing.T) { + // Create a ruleset manager. + rulesetManager := newRuleManager() + + // Create a ruleset. + rulesetID := "ruleset-1" + nftRule := nftables.Rule{} + ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, nil) + + // Add a rule to the ruleset. + ip := []byte("192.168.1.1") + rule, err := rulesetManager.addRule(ruleset, ip) + require.NoError(t, err, "addRule() failed") + require.NotNil(t, rule, "rule should not be nil") + require.NotEqual(t, rule.ruleID, "ruleID is empty") + require.EqualValues(t, rule.ip, ip, "ip is incorrect") + require.Contains(t, ruleset.issuedRules, rule.ruleID, "ruleID already exists in ruleset") + require.Contains(t, rulesetManager.issuedRuleID2rulesetID, rule.ruleID, "ruleID already exists in ruleset manager") + + ruleset2 := &nftRuleset{ + rulesetID: "ruleset-2", + } + _, err = rulesetManager.addRule(ruleset2, ip) + require.Error(t, err, "addRule() should have failed") +} + +func TestRulesetManager_deleteRule(t *testing.T) { + // Create a ruleset manager. + rulesetManager := newRuleManager() + + // Create a ruleset. + rulesetID := "ruleset-1" + nftRule := nftables.Rule{} + ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, nil) + + // Add a rule to the ruleset. + ip := []byte("192.168.1.1") + rule, err := rulesetManager.addRule(ruleset, ip) + require.NoError(t, err, "addRule() failed") + require.NotNil(t, rule, "rule should not be nil") + + ip2 := []byte("192.168.1.1") + rule2, err := rulesetManager.addRule(ruleset, ip2) + require.NoError(t, err, "addRule() failed") + require.NotNil(t, rule2, "rule should not be nil") + + hasNext := rulesetManager.deleteRule(rule) + require.True(t, hasNext, "deleteRule() should have returned true") + + // Check that the rule is no longer in the manager. + require.NotContains(t, rulesetManager.issuedRuleID2rulesetID, rule.ruleID, "rule should have been deleted") + + hasNext = rulesetManager.deleteRule(rule2) + require.False(t, hasNext, "deleteRule() should have returned false") +} + +func TestRulesetManager_setNftRuleHandle(t *testing.T) { + // Create a ruleset manager. + rulesetManager := newRuleManager() + // Create a ruleset. + rulesetID := "ruleset-1" + nftRule := nftables.Rule{} + ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, nil) + // Add a rule to the ruleset. + ip := []byte("192.168.0.1") + + rule, err := rulesetManager.addRule(ruleset, ip) + require.NoError(t, err, "addRule() failed") + require.NotNil(t, rule, "rule should not be nil") + + nftRuleCopy := nftRule + nftRuleCopy.Handle = 2 + nftRuleCopy.UserData = []byte(rulesetID) + err = rulesetManager.setNftRuleHandle(&nftRuleCopy) + require.NoError(t, err, "setNftRuleHandle() failed") + // check correct work with references + require.Equal(t, nftRule.Handle, uint64(2), "nftRule.Handle is incorrect") +} + +func TestRulesetManager_getRuleset(t *testing.T) { + // Create a ruleset manager. + rulesetManager := newRuleManager() + // Create a ruleset. + rulesetID := "ruleset-1" + nftRule := nftables.Rule{} + nftSet := nftables.Set{ + ID: 2, + } + ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, &nftSet) + require.NotNil(t, ruleset, "createRuleset() failed") + + find, ok := rulesetManager.getRuleset(rulesetID) + require.True(t, ok, "getRuleset() failed") + require.Equal(t, ruleset, find, "getRulesetBySetID() failed") + + _, ok = rulesetManager.getRuleset("does-not-exist") + require.False(t, ok, "getRuleset() failed") +} diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 5cc215256..3dead1db4 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -84,6 +84,7 @@ func (m *Manager) AddFiltering( dPort *fw.Port, direction fw.RuleDirection, action fw.Action, + ipsetName string, comment string, ) (fw.Rule, error) { r := Rule{ @@ -181,6 +182,9 @@ func (m *Manager) Reset() error { return nil } +// Flush doesn't need to be implemented for this manager +func (m *Manager) Flush() error { return nil } + // DropOutgoing filter outgoing packets func (m *Manager) DropOutgoing(packetData []byte) bool { return m.dropFilter(packetData, m.outgoingRules, false) diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index c7f38a44f..bc94f59c1 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -63,7 +63,7 @@ func TestManagerAddFiltering(t *testing.T) { action := fw.ActionDrop comment := "Test rule" - rule, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment) + rule, err := m.AddFiltering(ip, proto, nil, port, direction, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -98,7 +98,7 @@ func TestManagerDeleteRule(t *testing.T) { action := fw.ActionDrop comment := "Test rule" - rule, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment) + rule, err := m.AddFiltering(ip, proto, nil, port, direction, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -111,7 +111,7 @@ func TestManagerDeleteRule(t *testing.T) { action = fw.ActionDrop comment = "Test rule 2" - rule2, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment) + rule2, err := m.AddFiltering(ip, proto, nil, port, direction, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -236,7 +236,7 @@ func TestManagerReset(t *testing.T) { action := fw.ActionDrop comment := "Test rule" - _, err = m.AddFiltering(ip, proto, nil, port, direction, action, comment) + _, err = m.AddFiltering(ip, proto, nil, port, direction, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -274,7 +274,7 @@ func TestNotMatchByIP(t *testing.T) { action := fw.ActionAccept comment := "Test rule" - _, err = m.AddFiltering(ip, proto, nil, nil, direction, action, comment) + _, err = m.AddFiltering(ip, proto, nil, nil, direction, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -390,9 +390,9 @@ func TestUSPFilterCreatePerformance(t *testing.T) { for i := 0; i < testMax; i++ { port := &fw.Port{Values: []int{1000 + i}} if i%2 == 0 { - _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic") + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") } else { - _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic") + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") } require.NoError(t, err, "failed to add rule") diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 95f2c253e..d4b6930a0 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -33,9 +33,22 @@ type Manager interface { // DefaultManager uses firewall manager to handle type DefaultManager struct { - manager firewall.Manager - rulesPairs map[string][]firewall.Rule - mutex sync.Mutex + manager firewall.Manager + ipsetCounter int + rulesPairs map[string][]firewall.Rule + mutex sync.Mutex +} + +type ipsetInfo struct { + name string + ipCount int +} + +func newDefaultManager(fm firewall.Manager) *DefaultManager { + return &DefaultManager{ + manager: fm, + rulesPairs: make(map[string][]firewall.Rule), + } } // ApplyFiltering firewall rules to the local firewall manager processed by ACL policy. @@ -61,6 +74,12 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) { return } + defer func() { + if err := d.manager.Flush(); err != nil { + log.Error("failed to flush firewall rules: ", err) + } + }() + rules, squashedProtocols := d.squashAcceptRules(networkMap) enableSSH := (networkMap.PeerConfig != nil && @@ -108,8 +127,32 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) { applyFailed := false newRulePairs := make(map[string][]firewall.Rule) + ipsetByRuleSelectors := make(map[string]*ipsetInfo) + + // calculate which IP's can be grouped in by which ipset + // to do that we use rule selector (which is just rule properties without IP's) for _, r := range rules { - pairID, rulePair, err := d.protoRuleToFirewallRule(r) + selector := d.getRuleGroupingSelector(r) + ipset, ok := ipsetByRuleSelectors[selector] + if !ok { + ipset = &ipsetInfo{} + } + + ipset.ipCount++ + ipsetByRuleSelectors[selector] = ipset + } + + for _, r := range rules { + // if this rule is member of rule selection with more than DefaultIPsCountForSet + // it's IP address can be used in the ipset for firewall manager which supports it + ipset := ipsetByRuleSelectors[d.getRuleGroupingSelector(r)] + ipsetName := "" + if ipset.name == "" { + d.ipsetCounter++ + ipset.name = fmt.Sprintf("nb%07d", d.ipsetCounter) + } + ipsetName = ipset.name + pairID, rulePair, err := d.protoRuleToFirewallRule(r, ipsetName) if err != nil { log.Errorf("failed to apply firewall rule: %+v, %v", r, err) applyFailed = true @@ -154,7 +197,10 @@ func (d *DefaultManager) Stop() { } } -func (d *DefaultManager) protoRuleToFirewallRule(r *mgmProto.FirewallRule) (string, []firewall.Rule, error) { +func (d *DefaultManager) protoRuleToFirewallRule( + r *mgmProto.FirewallRule, + ipsetName string, +) (string, []firewall.Rule, error) { ip := net.ParseIP(r.PeerIP) if ip == nil { return "", nil, fmt.Errorf("invalid IP address, skipping firewall rule") @@ -190,9 +236,9 @@ func (d *DefaultManager) protoRuleToFirewallRule(r *mgmProto.FirewallRule) (stri var err error switch r.Direction { case mgmProto.FirewallRule_IN: - rules, err = d.addInRules(ip, protocol, port, action, "") + rules, err = d.addInRules(ip, protocol, port, action, ipsetName, "") case mgmProto.FirewallRule_OUT: - rules, err = d.addOutRules(ip, protocol, port, action, "") + rules, err = d.addOutRules(ip, protocol, port, action, ipsetName, "") default: return "", nil, fmt.Errorf("invalid direction, skipping firewall rule") } @@ -205,9 +251,17 @@ func (d *DefaultManager) protoRuleToFirewallRule(r *mgmProto.FirewallRule) (stri return ruleID, rules, nil } -func (d *DefaultManager) addInRules(ip net.IP, protocol firewall.Protocol, port *firewall.Port, action firewall.Action, comment string) ([]firewall.Rule, error) { +func (d *DefaultManager) addInRules( + ip net.IP, + protocol firewall.Protocol, + port *firewall.Port, + action firewall.Action, + ipsetName string, + comment string, +) ([]firewall.Rule, error) { var rules []firewall.Rule - rule, err := d.manager.AddFiltering(ip, protocol, nil, port, firewall.RuleDirectionIN, action, comment) + rule, err := d.manager.AddFiltering( + ip, protocol, nil, port, firewall.RuleDirectionIN, action, ipsetName, comment) if err != nil { return nil, fmt.Errorf("failed to add firewall rule: %v", err) } @@ -217,7 +271,8 @@ func (d *DefaultManager) addInRules(ip net.IP, protocol firewall.Protocol, port return rules, nil } - rule, err = d.manager.AddFiltering(ip, protocol, port, nil, firewall.RuleDirectionOUT, action, comment) + rule, err = d.manager.AddFiltering( + ip, protocol, port, nil, firewall.RuleDirectionOUT, action, ipsetName, comment) if err != nil { return nil, fmt.Errorf("failed to add firewall rule: %v", err) } @@ -225,9 +280,17 @@ func (d *DefaultManager) addInRules(ip net.IP, protocol firewall.Protocol, port return append(rules, rule), nil } -func (d *DefaultManager) addOutRules(ip net.IP, protocol firewall.Protocol, port *firewall.Port, action firewall.Action, comment string) ([]firewall.Rule, error) { +func (d *DefaultManager) addOutRules( + ip net.IP, + protocol firewall.Protocol, + port *firewall.Port, + action firewall.Action, + ipsetName string, + comment string, +) ([]firewall.Rule, error) { var rules []firewall.Rule - rule, err := d.manager.AddFiltering(ip, protocol, nil, port, firewall.RuleDirectionOUT, action, comment) + rule, err := d.manager.AddFiltering( + ip, protocol, nil, port, firewall.RuleDirectionOUT, action, ipsetName, comment) if err != nil { return nil, fmt.Errorf("failed to add firewall rule: %v", err) } @@ -237,7 +300,8 @@ func (d *DefaultManager) addOutRules(ip net.IP, protocol firewall.Protocol, port return rules, nil } - rule, err = d.manager.AddFiltering(ip, protocol, port, nil, firewall.RuleDirectionIN, action, comment) + rule, err = d.manager.AddFiltering( + ip, protocol, port, nil, firewall.RuleDirectionIN, action, ipsetName, comment) if err != nil { return nil, fmt.Errorf("failed to add firewall rule: %v", err) } @@ -282,6 +346,10 @@ func (d *DefaultManager) squashAcceptRules( in := protoMatch{} out := protoMatch{} + // trace which type of protocols was squashed + squashedRules := []*mgmProto.FirewallRule{} + squashedProtocols := map[mgmProto.FirewallRuleProtocol]struct{}{} + // this function we use to do calculation, can we squash the rules by protocol or not. // We summ amount of Peers IP for given protocol we found in original rules list. // But we zeroed the IP's for protocol if: @@ -298,12 +366,22 @@ func (d *DefaultManager) squashAcceptRules( if _, ok := protocols[r.Protocol]; !ok { protocols[r.Protocol] = map[string]int{} } - match := protocols[r.Protocol] - if _, ok := match[r.PeerIP]; ok { + // special case, when we recieve this all network IP address + // it means that rules for that protocol was already optimized on the + // management side + if r.PeerIP == "0.0.0.0" { + squashedRules = append(squashedRules, r) + squashedProtocols[r.Protocol] = struct{}{} return } - match[r.PeerIP] = i + + ipset := protocols[r.Protocol] + + if _, ok := ipset[r.PeerIP]; ok { + return + } + ipset[r.PeerIP] = i } for i, r := range networkMap.FirewallRules { @@ -324,9 +402,6 @@ func (d *DefaultManager) squashAcceptRules( mgmProto.FirewallRule_UDP, } - // trace which type of protocols was squashed - squashedRules := []*mgmProto.FirewallRule{} - squashedProtocols := map[mgmProto.FirewallRuleProtocol]struct{}{} squash := func(matches protoMatch, direction mgmProto.FirewallRuleDirection) { for _, protocol := range protocolOrders { if ipset, ok := matches[protocol]; !ok || len(ipset) != totalIPs || len(ipset) < 2 { @@ -382,6 +457,11 @@ func (d *DefaultManager) squashAcceptRules( return append(rules, squashedRules...), squashedProtocols } +// getRuleGroupingSelector takes all rule properties except IP address to build selector +func (d *DefaultManager) getRuleGroupingSelector(rule *mgmProto.FirewallRule) string { + return fmt.Sprintf("%v:%v:%v:%s", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port) +} + func convertToFirewallProtocol(protocol mgmProto.FirewallRuleProtocol) firewall.Protocol { switch protocol { case mgmProto.FirewallRule_TCP: diff --git a/client/internal/acl/manager_create.go b/client/internal/acl/manager_create.go index 4987ec587..7d9e6b430 100644 --- a/client/internal/acl/manager_create.go +++ b/client/internal/acl/manager_create.go @@ -6,7 +6,6 @@ import ( "fmt" "runtime" - "github.com/netbirdio/netbird/client/firewall" "github.com/netbirdio/netbird/client/firewall/uspfilter" ) @@ -18,10 +17,7 @@ func Create(iface IFaceMapper) (manager *DefaultManager, err error) { if err != nil { return nil, err } - return &DefaultManager{ - manager: fm, - rulesPairs: make(map[string][]firewall.Rule), - }, nil + return newDefaultManager(fm), nil } return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS) } diff --git a/client/internal/acl/manager_create_linux.go b/client/internal/acl/manager_create_linux.go index 168114fc2..de4e8adb9 100644 --- a/client/internal/acl/manager_create_linux.go +++ b/client/internal/acl/manager_create_linux.go @@ -29,8 +29,5 @@ func Create(iface IFaceMapper) (manager *DefaultManager, err error) { } } - return &DefaultManager{ - manager: fm, - rulesPairs: make(map[string][]firewall.Rule), - }, nil + return newDefaultManager(fm), nil } diff --git a/management/server/policy.go b/management/server/policy.go index eb26c8202..54158eeac 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -2,9 +2,11 @@ package server import ( _ "embed" - "fmt" + "strconv" "strings" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" @@ -240,7 +242,15 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*Peer, int), fun peersExists := make(map[string]struct{}) rules := make([]*FirewallRule, 0) peers := make([]*Peer, 0) + + all, err := a.GetGroupAll() + if err != nil { + log.Errorf("failed to get group all: %v", err) + all = &Group{} + } + return func(rule *PolicyRule, groupPeers []*Peer, direction int) { + isAll := (len(all.Peers) - 1) == len(groupPeers) for _, peer := range groupPeers { if peer == nil { continue @@ -250,29 +260,33 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*Peer, int), fun peersExists[peer.ID] = struct{}{} } - fwRule := FirewallRule{ + fr := FirewallRule{ PeerIP: peer.IP.String(), Direction: direction, Action: string(rule.Action), Protocol: string(rule.Protocol), } - ruleID := fmt.Sprintf("%s%d", peer.ID+peer.IP.String(), direction) - ruleID += string(rule.Protocol) + string(rule.Action) + strings.Join(rule.Ports, ",") + if isAll { + fr.PeerIP = "0.0.0.0" + } + + ruleID := (rule.ID + fr.PeerIP + strconv.Itoa(direction) + + fr.Protocol + fr.Action + strings.Join(rule.Ports, ",")) if _, ok := rulesExists[ruleID]; ok { continue } rulesExists[ruleID] = struct{}{} if len(rule.Ports) == 0 { - rules = append(rules, &fwRule) + rules = append(rules, &fr) continue } for _, port := range rule.Ports { - addRule := fwRule - addRule.Port = port - rules = append(rules, &addRule) + pr := fr // clone rule and add set new port + pr.Port = port + rules = append(rules, &pr) } } }, func() ([]*Peer, []*FirewallRule) { diff --git a/management/server/policy_test.go b/management/server/policy_test.go index d154e54f1..bf003ffac 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -126,6 +126,20 @@ func TestAccount_getPeersByPolicy(t *testing.T) { assert.Contains(t, peers, account.Peers["peerF"]) epectedFirewallRules := []*FirewallRule{ + { + PeerIP: "0.0.0.0", + Direction: firewallRuleDirectionIN, + Action: "accept", + Protocol: "all", + Port: "", + }, + { + PeerIP: "0.0.0.0", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "all", + Port: "", + }, { PeerIP: "100.65.14.88", Direction: firewallRuleDirectionIN, From 3027d8f27e1e9f10f4973add8dfe69598153f914 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 19 Jul 2023 19:10:27 +0200 Subject: [PATCH 15/27] Sync the iptables/nftables usage with acl logic (#1017) --- .../internal/routemanager/firewall_linux.go | 47 +++---------------- .../internal/routemanager/iptables_linux.go | 27 +++++++++++ .../routemanager/iptables_linux_test.go | 28 ++++------- .../internal/routemanager/nftables_linux.go | 27 +++++++++++ .../routemanager/nftables_linux_test.go | 39 +++++---------- 5 files changed, 81 insertions(+), 87 deletions(-) diff --git a/client/internal/routemanager/firewall_linux.go b/client/internal/routemanager/firewall_linux.go index 959724ed3..8b27c8967 100644 --- a/client/internal/routemanager/firewall_linux.go +++ b/client/internal/routemanager/firewall_linux.go @@ -6,8 +6,6 @@ import ( "context" "fmt" - "github.com/coreos/go-iptables/iptables" - "github.com/google/nftables" log "github.com/sirupsen/logrus" ) @@ -30,46 +28,13 @@ func genKey(format string, input string) string { // NewFirewall if supported, returns an iptables manager, otherwise returns a nftables manager func NewFirewall(parentCTX context.Context) firewallManager { - ctx, cancel := context.WithCancel(parentCTX) - - if isIptablesSupported() { - log.Debugf("iptables is supported") - ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4) - if !isIptablesClientAvailable(ipv4Client) { - log.Infof("iptables is missing for ipv4") - ipv4Client = nil - } - ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6) - if !isIptablesClientAvailable(ipv6Client) { - log.Infof("iptables is missing for ipv6") - ipv6Client = nil - } - - return &iptablesManager{ - ctx: ctx, - stop: cancel, - ipv4Client: ipv4Client, - ipv6Client: ipv6Client, - rules: make(map[string]map[string][]string), - } + manager, err := newNFTablesManager(parentCTX) + if err == nil { + log.Debugf("nftables firewall manager will be used") + return manager } - - log.Debugf("iptables is not supported, using nftables") - - manager := &nftablesManager{ - ctx: ctx, - stop: cancel, - conn: &nftables.Conn{}, - chains: make(map[string]map[string]*nftables.Chain), - rules: make(map[string]*nftables.Rule), - } - - return manager -} - -func isIptablesClientAvailable(client *iptables.IPTables) bool { - _, err := client.ListChains("filter") - return err == nil + log.Debugf("fallback to iptables firewall manager: %s", err) + return newIptablesManager(parentCTX) } func getInPair(pair routerPair) routerPair { diff --git a/client/internal/routemanager/iptables_linux.go b/client/internal/routemanager/iptables_linux.go index be469b82a..b058278f3 100644 --- a/client/internal/routemanager/iptables_linux.go +++ b/client/internal/routemanager/iptables_linux.go @@ -49,6 +49,28 @@ type iptablesManager struct { mux sync.Mutex } +func newIptablesManager(parentCtx context.Context) *iptablesManager { + ctx, cancel := context.WithCancel(parentCtx) + ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4) + if !isIptablesClientAvailable(ipv4Client) { + log.Infof("iptables is missing for ipv4") + ipv4Client = nil + } + ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6) + if !isIptablesClientAvailable(ipv6Client) { + log.Infof("iptables is missing for ipv6") + ipv6Client = nil + } + + return &iptablesManager{ + ctx: ctx, + stop: cancel, + ipv4Client: ipv4Client, + ipv6Client: ipv6Client, + rules: make(map[string]map[string][]string), + } +} + // CleanRoutingRules cleans existing iptables resources that we created by the agent func (i *iptablesManager) CleanRoutingRules() { i.mux.Lock() @@ -453,3 +475,8 @@ func getIptablesRuleType(table string) string { } return ruleType } + +func isIptablesClientAvailable(client *iptables.IPTables) bool { + _, err := client.ListChains("filter") + return err == nil +} diff --git a/client/internal/routemanager/iptables_linux_test.go b/client/internal/routemanager/iptables_linux_test.go index a8db05e8a..c26355e56 100644 --- a/client/internal/routemanager/iptables_linux_test.go +++ b/client/internal/routemanager/iptables_linux_test.go @@ -16,17 +16,7 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) { t.SkipNow() } - ctx, cancel := context.WithCancel(context.TODO()) - ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4) - ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6) - - manager := &iptablesManager{ - ctx: ctx, - stop: cancel, - ipv4Client: ipv4Client, - ipv6Client: ipv6Client, - rules: make(map[string]map[string][]string), - } + manager := newIptablesManager(context.TODO()) defer manager.CleanRoutingRules() @@ -37,21 +27,21 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) { require.Len(t, manager.rules[ipv4], 2, "should have created minimal rules for ipv4") - exists, err := ipv4Client.Exists(iptablesFilterTable, iptablesForwardChain, manager.rules[ipv4][ipv4Forwarding]...) + exists, err := manager.ipv4Client.Exists(iptablesFilterTable, iptablesForwardChain, manager.rules[ipv4][ipv4Forwarding]...) require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv4, iptablesFilterTable, iptablesForwardChain) require.True(t, exists, "forwarding rule should exist") - exists, err = ipv4Client.Exists(iptablesNatTable, iptablesPostRoutingChain, manager.rules[ipv4][ipv4Nat]...) + exists, err = manager.ipv4Client.Exists(iptablesNatTable, iptablesPostRoutingChain, manager.rules[ipv4][ipv4Nat]...) require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv4, iptablesNatTable, iptablesPostRoutingChain) require.True(t, exists, "postrouting rule should exist") require.Len(t, manager.rules[ipv6], 2, "should have created minimal rules for ipv6") - exists, err = ipv6Client.Exists(iptablesFilterTable, iptablesForwardChain, manager.rules[ipv6][ipv6Forwarding]...) + exists, err = manager.ipv6Client.Exists(iptablesFilterTable, iptablesForwardChain, manager.rules[ipv6][ipv6Forwarding]...) require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv6, iptablesFilterTable, iptablesForwardChain) require.True(t, exists, "forwarding rule should exist") - exists, err = ipv6Client.Exists(iptablesNatTable, iptablesPostRoutingChain, manager.rules[ipv6][ipv6Nat]...) + exists, err = manager.ipv6Client.Exists(iptablesNatTable, iptablesPostRoutingChain, manager.rules[ipv6][ipv6Nat]...) require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv6, iptablesNatTable, iptablesPostRoutingChain) require.True(t, exists, "postrouting rule should exist") @@ -64,13 +54,13 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) { forward4RuleKey := genKey(forwardingFormat, pair.ID) forward4Rule := genRuleSpec(routingFinalForwardJump, forward4RuleKey, pair.source, pair.destination) - err = ipv4Client.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forward4Rule...) + err = manager.ipv4Client.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forward4Rule...) require.NoError(t, err, "inserting rule should not return error") nat4RuleKey := genKey(natFormat, pair.ID) nat4Rule := genRuleSpec(routingFinalNatJump, nat4RuleKey, pair.source, pair.destination) - err = ipv4Client.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, nat4Rule...) + err = manager.ipv4Client.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, nat4Rule...) require.NoError(t, err, "inserting rule should not return error") pair = routerPair{ @@ -83,13 +73,13 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) { forward6RuleKey := genKey(forwardingFormat, pair.ID) forward6Rule := genRuleSpec(routingFinalForwardJump, forward6RuleKey, pair.source, pair.destination) - err = ipv6Client.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forward6Rule...) + err = manager.ipv6Client.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forward6Rule...) require.NoError(t, err, "inserting rule should not return error") nat6RuleKey := genKey(natFormat, pair.ID) nat6Rule := genRuleSpec(routingFinalNatJump, nat6RuleKey, pair.source, pair.destination) - err = ipv6Client.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, nat6Rule...) + err = manager.ipv6Client.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, nat6Rule...) require.NoError(t, err, "inserting rule should not return error") delete(manager.rules, ipv4) diff --git a/client/internal/routemanager/nftables_linux.go b/client/internal/routemanager/nftables_linux.go index 4f4f82224..dde8c143c 100644 --- a/client/internal/routemanager/nftables_linux.go +++ b/client/internal/routemanager/nftables_linux.go @@ -81,6 +81,25 @@ type nftablesManager struct { mux sync.Mutex } +func newNFTablesManager(parentCtx context.Context) (*nftablesManager, error) { + ctx, cancel := context.WithCancel(parentCtx) + + mgr := &nftablesManager{ + ctx: ctx, + stop: cancel, + conn: &nftables.Conn{}, + chains: make(map[string]map[string]*nftables.Chain), + rules: make(map[string]*nftables.Rule), + } + + err := mgr.isSupported() + if err != nil { + return nil, err + } + + return mgr, nil +} + // CleanRoutingRules cleans existing nftables rules from the system func (n *nftablesManager) CleanRoutingRules() { n.mux.Lock() @@ -386,6 +405,14 @@ func (n *nftablesManager) removeRoutingRule(format string, pair routerPair) erro return nil } +func (n *nftablesManager) isSupported() error { + _, err := n.conn.ListChains() + if err != nil { + return fmt.Errorf("nftables is not supported: %s", err) + } + return nil +} + // getPayloadDirectives get expression directives based on ip version and direction func getPayloadDirectives(direction string, isIPv4 bool, isIPv6 bool) (uint32, uint32, []byte) { switch { diff --git a/client/internal/routemanager/nftables_linux_test.go b/client/internal/routemanager/nftables_linux_test.go index 7ff8dd125..01fc38885 100644 --- a/client/internal/routemanager/nftables_linux_test.go +++ b/client/internal/routemanager/nftables_linux_test.go @@ -14,21 +14,16 @@ import ( func TestNftablesManager_RestoreOrCreateContainers(t *testing.T) { - ctx, cancel := context.WithCancel(context.TODO()) - - manager := &nftablesManager{ - ctx: ctx, - stop: cancel, - conn: &nftables.Conn{}, - chains: make(map[string]map[string]*nftables.Chain), - rules: make(map[string]*nftables.Rule), + manager, err := newNFTablesManager(context.TODO()) + if err != nil { + t.Fatalf("failed to create nftables manager: %s", err) } nftablesTestingClient := &nftables.Conn{} defer manager.CleanRoutingRules() - err := manager.RestoreOrCreateContainers() + err = manager.RestoreOrCreateContainers() require.NoError(t, err, "shouldn't return error") require.Len(t, manager.chains, 2, "should have created chains for ipv4 and ipv6") @@ -134,21 +129,16 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) { for _, testCase := range insertRuleTestCases { t.Run(testCase.name, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.TODO()) - - manager := &nftablesManager{ - ctx: ctx, - stop: cancel, - conn: &nftables.Conn{}, - chains: make(map[string]map[string]*nftables.Chain), - rules: make(map[string]*nftables.Rule), + manager, err := newNFTablesManager(context.TODO()) + if err != nil { + t.Fatalf("failed to create nftables manager: %s", err) } nftablesTestingClient := &nftables.Conn{} defer manager.CleanRoutingRules() - err := manager.RestoreOrCreateContainers() + err = manager.RestoreOrCreateContainers() require.NoError(t, err, "shouldn't return error") err = manager.InsertRoutingRules(testCase.inputPair) @@ -239,21 +229,16 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) { for _, testCase := range removeRuleTestCases { t.Run(testCase.name, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.TODO()) - - manager := &nftablesManager{ - ctx: ctx, - stop: cancel, - conn: &nftables.Conn{}, - chains: make(map[string]map[string]*nftables.Chain), - rules: make(map[string]*nftables.Rule), + manager, err := newNFTablesManager(context.TODO()) + if err != nil { + t.Fatalf("failed to create nftables manager: %s", err) } nftablesTestingClient := &nftables.Conn{} defer manager.CleanRoutingRules() - err := manager.RestoreOrCreateContainers() + err = manager.RestoreOrCreateContainers() require.NoError(t, err, "shouldn't return error") table := manager.tableIPv4 From a4d830ef83de9e0a7711b1e3f0d07b76d7b73c1e Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 21 Jul 2023 10:34:49 +0300 Subject: [PATCH 16/27] Fix Okta IDP device authorization (#1023) * hide okta netbird attributes fields * fix: update full user profile --- management/server/idp/okta.go | 61 ++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/management/server/idp/okta.go b/management/server/idp/okta.go index 85813138d..9d00bc6ee 100644 --- a/management/server/idp/okta.go +++ b/management/server/idp/okta.go @@ -270,21 +270,32 @@ func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) { // UpdateUserAppMetadata updates user app metadata based on userID and metadata map. func (om *OktaManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error { - var pendingInvite bool - if appMetadata.WTPendingInvite != nil { - pendingInvite = *appMetadata.WTPendingInvite + user, resp, err := om.client.User.GetUser(context.Background(), userID) + if err != nil { + return err } - _, resp, err := om.client.User.UpdateUser(context.Background(), userID, - okta.User{ - Profile: &okta.UserProfile{ - wtAccountID: appMetadata.WTAccountID, - wtPendingInvite: pendingInvite, - }, - }, - nil, - ) + if resp.StatusCode != http.StatusOK { + if om.appMetrics != nil { + om.appMetrics.IDPMetrics().CountRequestStatusError() + } + return fmt.Errorf("unable to update user, statusCode %d", resp.StatusCode) + } + + profile := *user.Profile + + if appMetadata.WTPendingInvite != nil { + profile[wtPendingInvite] = *appMetadata.WTPendingInvite + } + + if appMetadata.WTAccountID != "" { + profile[wtAccountID] = appMetadata.WTAccountID + } + + user.Profile = &profile + _, resp, err = om.client.User.UpdateUser(context.Background(), userID, *user, nil) if err != nil { + fmt.Println(err.Error()) return err } @@ -311,7 +322,9 @@ func (om *OktaManager) InviteUserByID(_ string) error { // updateUserProfileSchema updates the Okta user schema to include custom fields, // wt_account_id and wt_pending_invite. func updateUserProfileSchema(client *okta.Client) error { - required := true + // Ensure Okta doesn't enforce user input for these fields, as they are solely used by Netbird + userPermissions := []*okta.UserSchemaAttributePermission{{Action: "HIDE", Principal: "SELF"}} + _, resp, err := client.UserSchema.UpdateUserProfile( context.Background(), "default", @@ -322,18 +335,20 @@ func updateUserProfileSchema(client *okta.Client) error { Type: "object", Properties: map[string]*okta.UserSchemaAttribute{ wtAccountID: { - MaxLength: 100, - MinLength: 1, - Required: &required, - Scope: "NONE", - Title: "Wt Account Id", - Type: "string", + MaxLength: 100, + MinLength: 1, + Required: new(bool), + Scope: "NONE", + Title: "Wt Account Id", + Type: "string", + Permissions: userPermissions, }, wtPendingInvite: { - Required: new(bool), - Scope: "NONE", - Title: "Wt Pending Invite", - Type: "boolean", + Required: new(bool), + Scope: "NONE", + Title: "Wt Pending Invite", + Type: "boolean", + Permissions: userPermissions, }, }, }, From 6ad38476154931280d15836b1b5da793c914cb3a Mon Sep 17 00:00:00 2001 From: Givi Khojanashvili Date: Fri, 21 Jul 2023 19:45:58 +0400 Subject: [PATCH 17/27] Fix nfset not binds to the rule (#1024) --- client/firewall/nftables/manager_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 71085276d..081aee48d 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -146,7 +146,7 @@ func (m *Manager) AddFiltering( // with fresh created set and set element var isSetNew bool - ipset, err := m.rConn.GetSetByName(table, ipsetName) + ipset, err = m.rConn.GetSetByName(table, ipsetName) if err != nil { if ipset, err = m.createSet(table, rawIP, ipsetName); err != nil { return nil, fmt.Errorf("get set name: %v", err) From 97b6e79809c65ab2a754656599714e9080a500f5 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Sat, 22 Jul 2023 13:54:08 +0200 Subject: [PATCH 18/27] Fix DefaultAccountManager GetGroupsFromTheToken false positive tests (#1019) This fixes the test logic creates copy of account with empty id and re-pointing the indices to it. Also, adds additional check for empty ID in SaveAccount method of FileStore. --- management/server/account_test.go | 10 ++++++++-- management/server/file_store.go | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 1609abafc..12458f57a 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -470,11 +470,15 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) { require.NoError(t, err, "unable to create account manager") accountID := initAccount.Id - _, err = manager.GetAccountByUserOrAccountID(userId, accountID, domain) + acc, err := manager.GetAccountByUserOrAccountID(userId, accountID, domain) require.NoError(t, err, "create init user failed") + // as initAccount was created without account id we have to take the id after account initialization + // that happens inside the GetAccountByUserOrAccountID where the id is getting generated + // it is important to set the id as it help to avoid creating additional account with empty Id and re-pointing indices to it + initAccount.Id = acc.Id claims := jwtclaims.AuthorizationClaims{ - AccountId: accountID, + AccountId: accountID, // is empty as it is based on accountID right after initialization of initAccount Domain: domain, UserId: userId, DomainCategory: "test-category", @@ -491,6 +495,7 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) { initAccount.Settings.JWTGroupsEnabled = true err := manager.Store.SaveAccount(initAccount) require.NoError(t, err, "save account failed") + require.Len(t, manager.Store.GetAllAccounts(), 1, "only one account should exist") account, _, err := manager.GetAccountFromToken(claims) require.NoError(t, err, "get account by token failed") @@ -502,6 +507,7 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) { initAccount.Settings.JWTGroupsClaimName = "idp-groups" err := manager.Store.SaveAccount(initAccount) require.NoError(t, err, "save account failed") + require.Len(t, manager.Store.GetAllAccounts(), 1, "only one account should exist") account, _, err := manager.GetAccountFromToken(claims) require.NoError(t, err, "get account by token failed") diff --git a/management/server/file_store.go b/management/server/file_store.go index 0902c731f..0e95e3a05 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -289,6 +289,10 @@ func (s *FileStore) SaveAccount(account *Account) error { s.mux.Lock() defer s.mux.Unlock() + if account.Id == "" { + return status.Errorf(status.InvalidArgument, "account id should not be empty") + } + accountCopy := account.Copy() s.Accounts[accountCopy.Id] = accountCopy From 2541c78dd02a89f6670e22801dbd073e5fdfb1f0 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sat, 22 Jul 2023 17:56:27 +0200 Subject: [PATCH 19/27] Use error level for JWT parsing error logs (#1026) --- management/server/http/middleware/auth_middleware.go | 2 +- management/server/jwtclaims/jwtValidator.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go index a8c81012a..898ad0875 100644 --- a/management/server/http/middleware/auth_middleware.go +++ b/management/server/http/middleware/auth_middleware.go @@ -57,7 +57,7 @@ func (m *AuthMiddleware) Handler(h http.Handler) http.Handler { case "bearer": err := m.CheckJWTFromRequest(w, r) if err != nil { - log.Debugf("Error when validating JWT claims: %s", err.Error()) + log.Errorf("Error when validating JWT claims: %s", err.Error()) util.WriteError(status.Errorf(status.Unauthorized, "token invalid"), w) return } diff --git a/management/server/jwtclaims/jwtValidator.go b/management/server/jwtclaims/jwtValidator.go index b206eb794..d15327566 100644 --- a/management/server/jwtclaims/jwtValidator.go +++ b/management/server/jwtclaims/jwtValidator.go @@ -155,7 +155,7 @@ func (m *JWTValidator) ValidateAndParse(token string) (*jwt.Token, error) { // Check if there was an error in parsing... if err != nil { - log.Debugf("error parsing token: %v", err) + log.Errorf("error parsing token: %v", err) return nil, fmt.Errorf("Error parsing token: %w", err) } From 6c2ed4b4f22d2cd5395a6dee3dcf3f383d206fca Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 22 Jul 2023 18:39:23 +0200 Subject: [PATCH 20/27] Add default forward rule (#1021) * Add default forward rule * Fix * Add multiple forward rules * Fix delete rule error handling --- .../internal/routemanager/nftables_linux.go | 165 ++++++++++++++++-- 1 file changed, 152 insertions(+), 13 deletions(-) diff --git a/client/internal/routemanager/nftables_linux.go b/client/internal/routemanager/nftables_linux.go index dde8c143c..ca7d74f2a 100644 --- a/client/internal/routemanager/nftables_linux.go +++ b/client/internal/routemanager/nftables_linux.go @@ -19,6 +19,9 @@ const ( nftablesTable = "netbird-rt" nftablesRoutingForwardingChain = "netbird-rt-fwd" nftablesRoutingNatChain = "netbird-rt-nat" + + userDataAcceptForwardRuleSrc = "frwacceptsrc" + userDataAcceptForwardRuleDst = "frwacceptdst" ) // constants needed to create nftable rules @@ -71,25 +74,28 @@ var ( ) type nftablesManager struct { - ctx context.Context - stop context.CancelFunc - conn *nftables.Conn - tableIPv4 *nftables.Table - tableIPv6 *nftables.Table - chains map[string]map[string]*nftables.Chain - rules map[string]*nftables.Rule - mux sync.Mutex + ctx context.Context + stop context.CancelFunc + conn *nftables.Conn + tableIPv4 *nftables.Table + tableIPv6 *nftables.Table + chains map[string]map[string]*nftables.Chain + rules map[string]*nftables.Rule + filterTable *nftables.Table + defaultForwardRules []*nftables.Rule + mux sync.Mutex } func newNFTablesManager(parentCtx context.Context) (*nftablesManager, error) { ctx, cancel := context.WithCancel(parentCtx) mgr := &nftablesManager{ - ctx: ctx, - stop: cancel, - conn: &nftables.Conn{}, - chains: make(map[string]map[string]*nftables.Chain), - rules: make(map[string]*nftables.Rule), + ctx: ctx, + stop: cancel, + conn: &nftables.Conn{}, + chains: make(map[string]map[string]*nftables.Chain), + rules: make(map[string]*nftables.Rule), + defaultForwardRules: make([]*nftables.Rule, 2), } err := mgr.isSupported() @@ -97,6 +103,11 @@ func newNFTablesManager(parentCtx context.Context) (*nftablesManager, error) { return nil, err } + err = mgr.readFilterTable() + if err != nil { + return nil, err + } + return mgr, nil } @@ -109,6 +120,13 @@ func (n *nftablesManager) CleanRoutingRules() { n.conn.FlushTable(n.tableIPv6) n.conn.FlushTable(n.tableIPv4) } + + if n.defaultForwardRules[0] != nil { + err := n.eraseDefaultForwardRule() + if err != nil { + log.Errorf("failed to delete forward rule: %s", err) + } + } log.Debugf("flushing tables result in: %v error", n.conn.Flush()) } @@ -241,6 +259,112 @@ func (n *nftablesManager) refreshRulesMap() error { return nil } +func (n *nftablesManager) readFilterTable() error { + tables, err := n.conn.ListTables() + if err != nil { + return err + } + + for _, t := range tables { + if t.Name == "filter" { + n.filterTable = t + return nil + } + } + return nil +} + +func (n *nftablesManager) eraseDefaultForwardRule() error { + if n.defaultForwardRules[0] == nil { + return nil + } + + err := n.refreshDefaultForwardRule() + if err != nil { + return err + } + + for i, r := range n.defaultForwardRules { + err = n.conn.DelRule(r) + if err != nil { + log.Errorf("failed to delete forward rule (%d): %s", i, err) + } + n.defaultForwardRules[i] = nil + } + return nil +} + +func (n *nftablesManager) refreshDefaultForwardRule() error { + rules, err := n.conn.GetRules(n.defaultForwardRules[0].Table, n.defaultForwardRules[0].Chain) + if err != nil { + return fmt.Errorf("unable to list rules in forward chain: %s", err) + } + + found := false + for i, r := range n.defaultForwardRules { + for _, rule := range rules { + if string(rule.UserData) == string(r.UserData) { + n.defaultForwardRules[i] = rule + found = true + break + } + } + } + if !found { + return fmt.Errorf("unable to find forward accept rule") + } + + return nil +} + +func (n *nftablesManager) acceptForwardRule(sourceNetwork string) error { + src := generateCIDRMatcherExpressions("source", sourceNetwork) + dst := generateCIDRMatcherExpressions("destination", "0.0.0.0/0") + + var exprs []expr.Any + exprs = append(src, append(dst, &expr.Verdict{ + Kind: expr.VerdictAccept, + })...) + + r := &nftables.Rule{ + Table: n.filterTable, + Chain: &nftables.Chain{ + Name: "FORWARD", + Table: n.filterTable, + Type: nftables.ChainTypeFilter, + Hooknum: nftables.ChainHookForward, + Priority: nftables.ChainPriorityFilter, + }, + Exprs: exprs, + UserData: []byte(userDataAcceptForwardRuleSrc), + } + + n.defaultForwardRules[0] = n.conn.AddRule(r) + + src = generateCIDRMatcherExpressions("source", "0.0.0.0/0") + dst = generateCIDRMatcherExpressions("destination", sourceNetwork) + + exprs = append(src, append(dst, &expr.Verdict{ + Kind: expr.VerdictAccept, + })...) + + r = &nftables.Rule{ + Table: n.filterTable, + Chain: &nftables.Chain{ + Name: "FORWARD", + Table: n.filterTable, + Type: nftables.ChainTypeFilter, + Hooknum: nftables.ChainHookForward, + Priority: nftables.ChainPriorityFilter, + }, + Exprs: exprs, + UserData: []byte(userDataAcceptForwardRuleDst), + } + + n.defaultForwardRules[1] = n.conn.AddRule(r) + return nil +} + // checkOrCreateDefaultForwardingRules checks if the default forwarding rules are enabled func (n *nftablesManager) checkOrCreateDefaultForwardingRules() { _, foundIPv4 := n.rules[ipv4Forwarding] @@ -294,6 +418,14 @@ func (n *nftablesManager) InsertRoutingRules(pair routerPair) error { } } + if n.defaultForwardRules[0] == nil && n.filterTable != nil { + err = n.acceptForwardRule(pair.source) + if err != nil { + log.Errorf("unable to create default forward rule: %s", err) + } + log.Debugf("default accept forward rule added") + } + err = n.conn.Flush() if err != nil { return fmt.Errorf("nftables: unable to insert rules for %s: %v", pair.destination, err) @@ -374,6 +506,13 @@ func (n *nftablesManager) RemoveRoutingRules(pair routerPair) error { return err } + if len(n.rules) == 2 && n.defaultForwardRules[0] != nil { + err := n.eraseDefaultForwardRule() + if err != nil { + log.Errorf("failed to delte default fwd rule: %s", err) + } + } + err = n.conn.Flush() if err != nil { return fmt.Errorf("nftables: received error while applying rule removal for %s: %v", pair.destination, err) From 76db4f801ae226c81531657678c654b6b6f0b07b Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sat, 22 Jul 2023 19:30:59 +0200 Subject: [PATCH 21/27] Record idp manager type (#1027) This allows to define priority on support different managers --- management/cmd/management.go | 6 +++++- management/server/metrics/selfhosted.go | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/management/cmd/management.go b/management/cmd/management.go index eee8c5c57..842dcbaa2 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -218,7 +218,11 @@ var ( if !disableMetrics { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - metricsWorker := metrics.NewWorker(ctx, installationID, store, peersUpdateManager) + idpManager := "disabled" + if config.IdpManagerConfig != nil && config.IdpManagerConfig.ManagerType != "" { + idpManager = config.IdpManagerConfig.ManagerType + } + metricsWorker := metrics.NewWorker(ctx, installationID, store, peersUpdateManager, idpManager) go metricsWorker.Run() } diff --git a/management/server/metrics/selfhosted.go b/management/server/metrics/selfhosted.go index b8ce935c2..b2402118d 100644 --- a/management/server/metrics/selfhosted.go +++ b/management/server/metrics/selfhosted.go @@ -59,6 +59,7 @@ type ConnManager interface { type Worker struct { ctx context.Context id string + idpManager string dataSource DataSource connManager ConnManager startupTime time.Time @@ -66,11 +67,12 @@ type Worker struct { } // NewWorker returns a metrics worker -func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager) *Worker { +func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker { currentTime := time.Now() return &Worker{ ctx: ctx, id: id, + idpManager: idpManager, dataSource: dataSource, connManager: connManager, startupTime: currentTime, @@ -277,6 +279,7 @@ func (w *Worker) generateProperties() properties { metricsProperties["min_active_peer_version"] = minActivePeerVersion metricsProperties["max_active_peer_version"] = maxActivePeerVersion metricsProperties["ui_clients"] = uiClient + metricsProperties["idp_manager"] = w.idpManager for protocol, count := range rulesProtocol { metricsProperties["rules_protocol_"+protocol] = count From 6dee89379bd076fa538e7653d2a97f02f551cd53 Mon Sep 17 00:00:00 2001 From: Givi Khojanashvili Date: Mon, 24 Jul 2023 15:00:23 +0400 Subject: [PATCH 22/27] Feat optimize acl performance iptables (#1025) * use ipset for iptables * Update unit-tests for iptables * Remove debug code * Update dependencies * Create separate sets for dPort and sPort rules * Fix iptables tests * Fix 0.0.0.0 processing in iptables with ipset --- client/firewall/iptables/manager_linux.go | 141 +++++++++++++++--- .../firewall/iptables/manager_linux_test.go | 107 +++++++++++-- client/firewall/iptables/rule.go | 7 +- go.mod | 1 + go.sum | 2 + 5 files changed, 225 insertions(+), 33 deletions(-) diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index a4a7a6c3b..fa51122af 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -8,6 +8,7 @@ import ( "github.com/coreos/go-iptables/iptables" "github.com/google/uuid" + "github.com/nadoo/ipset" log "github.com/sirupsen/logrus" fw "github.com/netbirdio/netbird/client/firewall" @@ -35,6 +36,8 @@ type Manager struct { inputDefaultRuleSpecs []string outputDefaultRuleSpecs []string wgIface iFaceMapper + + rulesets map[string]ruleset } // iFaceMapper defines subset methods of interface required for manager @@ -43,6 +46,11 @@ type iFaceMapper interface { Address() iface.WGAddress } +type ruleset struct { + rule *Rule + ips map[string]string +} + // Create iptables firewall manager func Create(wgIface iFaceMapper) (*Manager, error) { m := &Manager{ @@ -51,6 +59,11 @@ func Create(wgIface iFaceMapper) (*Manager, error) { "-i", wgIface.Name(), "-j", ChainInputFilterName, "-s", wgIface.Address().String()}, outputDefaultRuleSpecs: []string{ "-o", wgIface.Name(), "-j", ChainOutputFilterName, "-d", wgIface.Address().String()}, + rulesets: make(map[string]ruleset), + } + + if err := ipset.Init(); err != nil { + return nil, fmt.Errorf("init ipset: %w", err) } // init clients for booth ipv4 and ipv6 @@ -111,22 +124,45 @@ func (m *Manager) AddFiltering( if sPort != nil && sPort.Values != nil { sPortVal = strconv.Itoa(sPort.Values[0]) } + ipsetName = m.transformIPsetName(ipsetName, sPortVal, dPortVal) ruleID := uuid.New().String() if comment == "" { comment = ruleID } - specs := m.filterRuleSpecs( - "filter", - ip, - string(protocol), - sPortVal, - dPortVal, - direction, - action, - comment, - ) + if ipsetName != "" { + rs, rsExists := m.rulesets[ipsetName] + if !rsExists { + if err := ipset.Flush(ipsetName); err != nil { + log.Errorf("flush ipset %q before use it: %v", ipsetName, err) + } + if err := ipset.Create(ipsetName); err != nil { + return nil, fmt.Errorf("failed to create ipset: %w", err) + } + } + + if err := ipset.Add(ipsetName, ip.String()); err != nil { + return nil, fmt.Errorf("failed to add IP to ipset: %w", err) + } + + if rsExists { + // if ruleset already exists it means we already have the firewall rule + // so we need to update IPs in the ruleset and return new fw.Rule object for ACL manager. + rs.ips[ip.String()] = ruleID + return &Rule{ + ruleID: ruleID, + ipsetName: ipsetName, + ip: ip.String(), + dst: direction == fw.RuleDirectionOUT, + v6: ip.To4() == nil, + }, nil + } + // this is new ipset so we need to create firewall rule for it + } + + specs := m.filterRuleSpecs("filter", ip, string(protocol), sPortVal, dPortVal, + direction, action, comment, ipsetName) if direction == fw.RuleDirectionOUT { ok, err := client.Exists("filter", ChainOutputFilterName, specs...) @@ -154,12 +190,24 @@ func (m *Manager) AddFiltering( } } - return &Rule{ - id: ruleID, - specs: specs, - dst: direction == fw.RuleDirectionOUT, - v6: ip.To4() == nil, - }, nil + rule := &Rule{ + ruleID: ruleID, + specs: specs, + ipsetName: ipsetName, + ip: ip.String(), + dst: direction == fw.RuleDirectionOUT, + v6: ip.To4() == nil, + } + if ipsetName != "" { + // ipset name is defined and it means that this rule was created + // for it, need to assosiate it with ruleset + m.rulesets[ipsetName] = ruleset{ + rule: rule, + ips: map[string]string{rule.ip: ruleID}, + } + } + + return rule, nil } // DeleteRule from the firewall by rule definition @@ -180,6 +228,31 @@ func (m *Manager) DeleteRule(rule fw.Rule) error { client = m.ipv6Client } + if rs, ok := m.rulesets[r.ipsetName]; ok { + // delete IP from ruleset IPs list and ipset + if _, ok := rs.ips[r.ip]; ok { + if err := ipset.Del(r.ipsetName, r.ip); err != nil { + return fmt.Errorf("failed to delete ip from ipset: %w", err) + } + delete(rs.ips, r.ip) + } + + // if after delete, set still contains other IPs, + // no need to delete firewall rule and we should exit here + if len(rs.ips) != 0 { + return nil + } + + // we delete last IP from the set, that means we need to delete + // set itself and assosiated firewall rule too + delete(m.rulesets, r.ipsetName) + + if err := ipset.Destroy(r.ipsetName); err != nil { + log.Errorf("delete empty ipset: %v", err) + } + r = rs.rule + } + if r.dst { return client.Delete("filter", ChainOutputFilterName, r.specs...) } @@ -246,6 +319,16 @@ func (m *Manager) reset(client *iptables.IPTables, table string) error { return nil } + for ipsetName := range m.rulesets { + if err := ipset.Flush(ipsetName); err != nil { + log.Errorf("flush ipset %q during reset: %v", ipsetName, err) + } + if err := ipset.Destroy(ipsetName); err != nil { + log.Errorf("delete ipset %q during reset: %v", ipsetName, err) + } + delete(m.rulesets, ipsetName) + } + return nil } @@ -253,6 +336,7 @@ func (m *Manager) reset(client *iptables.IPTables, table string) error { func (m *Manager) filterRuleSpecs( table string, ip net.IP, protocol string, sPort, dPort string, direction fw.RuleDirection, action fw.Action, comment string, + ipsetName string, ) (specs []string) { matchByIP := true // don't use IP matching if IP is ip 0.0.0.0 @@ -262,11 +346,19 @@ func (m *Manager) filterRuleSpecs( switch direction { case fw.RuleDirectionIN: if matchByIP { - specs = append(specs, "-s", ip.String()) + if ipsetName != "" { + specs = append(specs, "-m", "set", "--set", ipsetName, "src") + } else { + specs = append(specs, "-s", ip.String()) + } } case fw.RuleDirectionOUT: if matchByIP { - specs = append(specs, "-d", ip.String()) + if ipsetName != "" { + specs = append(specs, "-m", "set", "--set", ipsetName, "dst") + } else { + specs = append(specs, "-d", ip.String()) + } } } if protocol != "all" { @@ -348,3 +440,16 @@ func (m *Manager) actionToStr(action fw.Action) string { } return "DROP" } + +func (m *Manager) transformIPsetName(ipsetName string, sPort, dPort string) string { + if ipsetName == "" { + return "" + } else if sPort != "" && dPort != "" { + return ipsetName + "-sport-dport" + } else if sPort != "" { + return ipsetName + "-sport" + } else if dPort != "" { + return ipsetName + "-dport" + } + return ipsetName +} diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index cbf9d4c76..84e27ed14 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -55,12 +55,13 @@ func TestIptablesManager(t *testing.T) { // just check on the local interface manager, err := Create(mock) require.NoError(t, err) + time.Sleep(time.Second) defer func() { - if err := manager.Reset(); err != nil { - t.Errorf("clear the manager state: %v", err) - } + err := manager.Reset() + require.NoError(t, err, "clear the manager state") + time.Sleep(time.Second) }() @@ -88,19 +89,17 @@ func TestIptablesManager(t *testing.T) { }) t.Run("delete first rule", func(t *testing.T) { - if err := manager.DeleteRule(rule1); err != nil { - require.NoError(t, err, "failed to delete rule") - } + err := manager.DeleteRule(rule1) + require.NoError(t, err, "failed to delete rule") checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, false, rule1.(*Rule).specs...) }) t.Run("delete second rule", func(t *testing.T) { - if err := manager.DeleteRule(rule2); err != nil { - require.NoError(t, err, "failed to delete rule") - } + err := manager.DeleteRule(rule2) + require.NoError(t, err, "failed to delete rule") - checkRuleSpecs(t, ipv4Client, ChainInputFilterName, false, rule2.(*Rule).specs...) + require.Empty(t, manager.rulesets, "rulesets index after removed second rule must be empty") }) t.Run("reset check", func(t *testing.T) { @@ -122,6 +121,88 @@ func TestIptablesManager(t *testing.T) { }) } +func TestIptablesManagerIPSet(t *testing.T) { + ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) + require.NoError(t, err) + + mock := &iFaceMock{ + NameFunc: func() string { + return "lo" + }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: net.ParseIP("10.20.0.1"), + Network: &net.IPNet{ + IP: net.ParseIP("10.20.0.0"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + } + }, + } + + // just check on the local interface + manager, err := Create(mock) + require.NoError(t, err) + + time.Sleep(time.Second) + + defer func() { + err := manager.Reset() + require.NoError(t, err, "clear the manager state") + + time.Sleep(time.Second) + }() + + var rule1 fw.Rule + t.Run("add first rule with set", func(t *testing.T) { + ip := net.ParseIP("10.20.0.2") + port := &fw.Port{Values: []int{8080}} + rule1, err = manager.AddFiltering( + ip, "tcp", nil, port, fw.RuleDirectionOUT, + fw.ActionAccept, "default", "accept HTTP traffic", + ) + require.NoError(t, err, "failed to add rule") + + checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, true, rule1.(*Rule).specs...) + require.Equal(t, rule1.(*Rule).ipsetName, "default-dport", "ipset name must be set") + require.Equal(t, rule1.(*Rule).ip, "10.20.0.2", "ipset IP must be set") + }) + + var rule2 fw.Rule + t.Run("add second rule", func(t *testing.T) { + ip := net.ParseIP("10.20.0.3") + port := &fw.Port{ + Values: []int{443}, + } + rule2, err = manager.AddFiltering( + ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, + "default", "accept HTTPS traffic from ports range", + ) + require.NoError(t, err, "failed to add rule") + require.Equal(t, rule2.(*Rule).ipsetName, "default-sport", "ipset name must be set") + require.Equal(t, rule2.(*Rule).ip, "10.20.0.3", "ipset IP must be set") + }) + + t.Run("delete first rule", func(t *testing.T) { + err := manager.DeleteRule(rule1) + require.NoError(t, err, "failed to delete rule") + + require.NotContains(t, manager.rulesets, rule1.(*Rule).ruleID, "rule must be removed form the ruleset index") + }) + + t.Run("delete second rule", func(t *testing.T) { + err := manager.DeleteRule(rule2) + require.NoError(t, err, "failed to delete rule") + + require.Empty(t, manager.rulesets, "rulesets index after removed second rule must be empty") + }) + + t.Run("reset check", func(t *testing.T) { + err = manager.Reset() + require.NoError(t, err, "failed to reset") + }) +} + func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, chainName string, mustExists bool, rulespec ...string) { exists, err := ipv4Client.Exists("filter", chainName, rulespec...) require.NoError(t, err, "failed to check rule") @@ -153,9 +234,9 @@ func TestIptablesCreatePerformance(t *testing.T) { time.Sleep(time.Second) defer func() { - if err := manager.Reset(); err != nil { - t.Errorf("clear the manager state: %v", err) - } + err := manager.Reset() + require.NoError(t, err, "clear the manager state") + time.Sleep(time.Second) }() diff --git a/client/firewall/iptables/rule.go b/client/firewall/iptables/rule.go index a417891ea..f65030d39 100644 --- a/client/firewall/iptables/rule.go +++ b/client/firewall/iptables/rule.go @@ -2,13 +2,16 @@ package iptables // Rule to handle management of rules type Rule struct { - id string + ruleID string + ipsetName string + specs []string + ip string dst bool v6 bool } // GetRuleID returns the rule id func (r *Rule) GetRuleID() string { - return r.id + return r.ruleID } diff --git a/go.mod b/go.mod index 9e3d49490..4cb1c48c2 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/mdlayher/socket v0.4.0 github.com/miekg/dns v1.1.43 github.com/mitchellh/hashstructure/v2 v2.0.2 + github.com/nadoo/ipset v0.5.0 github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pion/logging v0.2.2 diff --git a/go.sum b/go.sum index 73d293084..b374e507e 100644 --- a/go.sum +++ b/go.sum @@ -485,6 +485,8 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc= +github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0 h1:hirFRfx3grVA/9eEyjME5/z3nxdJlN9kfQpvWWPk32g= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c h1:wK/s4nyZj/GF/kFJQjX6nqNfE0G3gcqd6hhnPCyp4sw= From b0364da67c027fb1038a34555a4c017f91fbc766 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 26 Jul 2023 14:00:47 +0200 Subject: [PATCH 23/27] Wg ebpf proxy (#911) EBPF proxy between TURN (relay) and WireGuard to reduce number of used ports used by the NetBird agent. - Separate the wg configuration from the proxy logic - In case if eBPF type proxy has only one single proxy instance - In case if the eBPF is not supported fallback to the original proxy Implementation Between the signature of eBPF type proxy and original proxy has differences so this is why the factory structure exists --- client/internal/engine.go | 25 +- client/internal/engine_test.go | 4 +- client/internal/peer/conn.go | 163 +++++++------ client/internal/peer/conn_test.go | 40 +++- client/internal/proxy/dummy.go | 72 ------ client/internal/proxy/noproxy.go | 42 ---- client/internal/proxy/proxy.go | 35 --- client/internal/proxy/wireguard.go | 128 ---------- client/internal/wgproxy/bpf/portreplace.c | 90 +++++++ client/internal/wgproxy/bpf_bpfeb.go | 120 ++++++++++ client/internal/wgproxy/bpf_bpfeb.o | Bin 0 -> 6224 bytes client/internal/wgproxy/bpf_bpfel.go | 120 ++++++++++ client/internal/wgproxy/bpf_bpfel.o | Bin 0 -> 6224 bytes client/internal/wgproxy/factory.go | 20 ++ client/internal/wgproxy/factory_linux.go | 19 ++ client/internal/wgproxy/factory_nonlinux.go | 7 + client/internal/wgproxy/loader.go | 80 +++++++ client/internal/wgproxy/loader_test.go | 18 ++ client/internal/wgproxy/portlookup.go | 32 +++ client/internal/wgproxy/portlookup_test.go | 42 ++++ client/internal/wgproxy/proxy.go | 12 + client/internal/wgproxy/proxy_ebpf.go | 250 ++++++++++++++++++++ client/internal/wgproxy/proxy_ebpf_test.go | 56 +++++ client/internal/wgproxy/proxy_userspace.go | 105 ++++++++ go.mod | 2 +- go.sum | 10 +- iface/bind/udp_mux_universal.go | 2 +- iface/iface.go | 2 +- 28 files changed, 1117 insertions(+), 379 deletions(-) delete mode 100644 client/internal/proxy/dummy.go delete mode 100644 client/internal/proxy/noproxy.go delete mode 100644 client/internal/proxy/proxy.go delete mode 100644 client/internal/proxy/wireguard.go create mode 100644 client/internal/wgproxy/bpf/portreplace.c create mode 100644 client/internal/wgproxy/bpf_bpfeb.go create mode 100644 client/internal/wgproxy/bpf_bpfeb.o create mode 100644 client/internal/wgproxy/bpf_bpfel.go create mode 100644 client/internal/wgproxy/bpf_bpfel.o create mode 100644 client/internal/wgproxy/factory.go create mode 100644 client/internal/wgproxy/factory_linux.go create mode 100644 client/internal/wgproxy/factory_nonlinux.go create mode 100644 client/internal/wgproxy/loader.go create mode 100644 client/internal/wgproxy/loader_test.go create mode 100644 client/internal/wgproxy/portlookup.go create mode 100644 client/internal/wgproxy/portlookup_test.go create mode 100644 client/internal/wgproxy/proxy.go create mode 100644 client/internal/wgproxy/proxy_ebpf.go create mode 100644 client/internal/wgproxy/proxy_ebpf_test.go create mode 100644 client/internal/wgproxy/proxy_userspace.go diff --git a/client/internal/engine.go b/client/internal/engine.go index ad9b79d7a..038f39e5c 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -20,8 +20,8 @@ import ( "github.com/netbirdio/netbird/client/internal/acl" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/proxy" "github.com/netbirdio/netbird/client/internal/routemanager" + "github.com/netbirdio/netbird/client/internal/wgproxy" nbssh "github.com/netbirdio/netbird/client/ssh" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/iface" @@ -101,7 +101,8 @@ type Engine struct { ctx context.Context - wgInterface *iface.WGIface + wgInterface *iface.WGIface + wgProxyFactory *wgproxy.Factory udpMux *bind.UniversalUDPMuxDefault udpMuxConn io.Closer @@ -132,6 +133,7 @@ func NewEngine( signalClient signal.Client, mgmClient mgm.Client, config *EngineConfig, mobileDep MobileDependency, statusRecorder *peer.Status, ) *Engine { + return &Engine{ ctx: ctx, cancel: cancel, @@ -146,6 +148,7 @@ func NewEngine( networkSerial: 0, sshServerFunc: nbssh.DefaultSSHServer, statusRecorder: statusRecorder, + wgProxyFactory: wgproxy.NewFactory(config.WgPort), } } @@ -282,7 +285,7 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error { for _, p := range peersUpdate { peerPubKey := p.GetWgPubKey() if peerConn, ok := e.peerConns[peerPubKey]; ok { - if peerConn.GetConf().ProxyConfig.AllowedIps != strings.Join(p.AllowedIps, ",") { + if peerConn.WgConfig().AllowedIps != strings.Join(p.AllowedIps, ",") { modified = append(modified, p) continue } @@ -795,9 +798,7 @@ func (e *Engine) connWorker(conn *peer.Conn, peerKey string) { // we might have received new STUN and TURN servers meanwhile, so update them e.syncMsgMux.Lock() - conf := conn.GetConf() - conf.StunTurn = append(e.STUNs, e.TURNs...) - conn.UpdateConf(conf) + conn.UpdateStunTurn(append(e.STUNs, e.TURNs...)) e.syncMsgMux.Unlock() err := conn.Open() @@ -826,9 +827,9 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e stunTurn = append(stunTurn, e.STUNs...) stunTurn = append(stunTurn, e.TURNs...) - proxyConfig := proxy.Config{ + wgConfig := peer.WgConfig{ RemoteKey: pubKey, - WgListenAddr: fmt.Sprintf("127.0.0.1:%d", e.config.WgPort), + WgListenPort: e.config.WgPort, WgInterface: e.wgInterface, AllowedIps: allowedIPs, PreSharedKey: e.config.PreSharedKey, @@ -845,13 +846,13 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e Timeout: timeout, UDPMux: e.udpMux.UDPMuxDefault, UDPMuxSrflx: e.udpMux, - ProxyConfig: proxyConfig, + WgConfig: wgConfig, LocalWgPort: e.config.WgPort, NATExternalIPs: e.parseNATExternalIPMappings(), UserspaceBind: e.wgInterface.IsUserspaceBind(), } - peerConn, err := peer.NewConn(config, e.statusRecorder, e.mobileDep.TunAdapter, e.mobileDep.IFaceDiscover) + peerConn, err := peer.NewConn(config, e.statusRecorder, e.wgProxyFactory, e.mobileDep.TunAdapter, e.mobileDep.IFaceDiscover) if err != nil { return nil, err } @@ -1008,6 +1009,10 @@ func (e *Engine) parseNATExternalIPMappings() []string { } func (e *Engine) close() { + if err := e.wgProxyFactory.Free(); err != nil { + log.Errorf("failed closing ebpf proxy: %s", err) + } + log.Debugf("removing Netbird interface %s", e.config.WgIfaceName) if e.wgInterface != nil { if err := e.wgInterface.Close(); err != nil { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 4d67968f6..60c07c0c9 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -367,9 +367,9 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { t.Errorf("expecting Engine.peerConns to contain peer %s", p) } expectedAllowedIPs := strings.Join(p.AllowedIps, ",") - if conn.GetConf().ProxyConfig.AllowedIps != expectedAllowedIPs { + if conn.WgConfig().AllowedIps != expectedAllowedIPs { t.Errorf("expecting peer %s to have AllowedIPs= %s, got %s", p.GetWgPubKey(), - expectedAllowedIPs, conn.GetConf().ProxyConfig.AllowedIps) + expectedAllowedIPs, conn.WgConfig().AllowedIps) } } }) diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 9a3ef106b..9247ed3c5 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -10,9 +10,10 @@ import ( "github.com/pion/ice/v2" log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "github.com/netbirdio/netbird/client/internal/proxy" "github.com/netbirdio/netbird/client/internal/stdnet" + "github.com/netbirdio/netbird/client/internal/wgproxy" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/iface/bind" signal "github.com/netbirdio/netbird/signal/client" @@ -23,8 +24,18 @@ import ( const ( iceKeepAliveDefault = 4 * time.Second iceDisconnectedTimeoutDefault = 6 * time.Second + + defaultWgKeepAlive = 25 * time.Second ) +type WgConfig struct { + WgListenPort int + RemoteKey string + WgInterface *iface.WGIface + AllowedIps string + PreSharedKey *wgtypes.Key +} + // ConnConfig is a peer Connection configuration type ConnConfig struct { @@ -43,7 +54,7 @@ type ConnConfig struct { Timeout time.Duration - ProxyConfig proxy.Config + WgConfig WgConfig UDPMux ice.UDPMux UDPMuxSrflx ice.UniversalUDPMux @@ -98,7 +109,9 @@ type Conn struct { statusRecorder *Status - proxy proxy.Proxy + wgProxyFactory *wgproxy.Factory + wgProxy wgproxy.Proxy + remoteModeCh chan ModeMessage meta meta @@ -122,14 +135,19 @@ func (conn *Conn) GetConf() ConnConfig { return conn.config } -// UpdateConf updates the connection config -func (conn *Conn) UpdateConf(conf ConnConfig) { - conn.config = conf +// WgConfig returns the WireGuard config +func (conn *Conn) WgConfig() WgConfig { + return conn.config.WgConfig +} + +// UpdateStunTurn update the turn and stun addresses +func (conn *Conn) UpdateStunTurn(turnStun []*ice.URL) { + conn.config.StunTurn = turnStun } // NewConn creates a new not opened Conn to the remote peer. // To establish a connection run Conn.Open -func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) (*Conn, error) { +func NewConn(config ConnConfig, statusRecorder *Status, wgProxyFactory *wgproxy.Factory, adapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) (*Conn, error) { return &Conn{ config: config, mu: sync.Mutex{}, @@ -139,6 +157,7 @@ func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter remoteAnswerCh: make(chan OfferAnswer), statusRecorder: statusRecorder, remoteModeCh: make(chan ModeMessage, 1), + wgProxyFactory: wgProxyFactory, adapter: adapter, iFaceDiscover: iFaceDiscover, }, nil @@ -215,12 +234,12 @@ func (conn *Conn) candidateTypes() []ice.CandidateType { func (conn *Conn) Open() error { log.Debugf("trying to connect to peer %s", conn.config.Key) - peerState := State{PubKey: conn.config.Key} - - peerState.IP = strings.Split(conn.config.ProxyConfig.AllowedIps, "/")[0] - peerState.ConnStatusUpdate = time.Now() - peerState.ConnStatus = conn.status - + peerState := State{ + PubKey: conn.config.Key, + IP: strings.Split(conn.config.WgConfig.AllowedIps, "/")[0], + ConnStatusUpdate: time.Now(), + ConnStatus: conn.status, + } err := conn.statusRecorder.UpdatePeerState(peerState) if err != nil { log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err) @@ -275,10 +294,11 @@ func (conn *Conn) Open() error { defer conn.notifyDisconnected() conn.mu.Unlock() - peerState = State{PubKey: conn.config.Key} - - peerState.ConnStatus = conn.status - peerState.ConnStatusUpdate = time.Now() + peerState = State{ + PubKey: conn.config.Key, + ConnStatus: conn.status, + ConnStatusUpdate: time.Now(), + } err = conn.statusRecorder.UpdatePeerState(peerState) if err != nil { log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err) @@ -309,19 +329,12 @@ func (conn *Conn) Open() error { remoteWgPort = remoteOfferAnswer.WgListenPort } // the ice connection has been established successfully so we are ready to start the proxy - err = conn.startProxy(remoteConn, remoteWgPort) + remoteAddr, err := conn.configureConnection(remoteConn, remoteWgPort) if err != nil { return err } - if conn.proxy.Type() == proxy.TypeDirectNoProxy { - host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String()) - rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String()) - // direct Wireguard connection - log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, conn.config.LocalWgPort, rhost, remoteWgPort) - } else { - log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String()) - } + log.Infof("connected to peer %s, endpoint address: %s", conn.config.Key, remoteAddr.String()) // wait until connection disconnected or has been closed externally (upper layer, e.g. engine) select { @@ -338,54 +351,60 @@ func isRelayCandidate(candidate ice.Candidate) bool { return candidate.Type() == ice.CandidateTypeRelay } -// startProxy starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected -func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error { +// configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected +func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int) (net.Addr, error) { conn.mu.Lock() defer conn.mu.Unlock() - var pair *ice.CandidatePair pair, err := conn.agent.GetSelectedCandidatePair() if err != nil { - return err + return nil, err } - peerState := State{PubKey: conn.config.Key} - p := conn.getProxy(pair, remoteWgPort) - conn.proxy = p - err = p.Start(remoteConn) + var endpoint net.Addr + if isRelayCandidate(pair.Local) { + log.Debugf("setup relay connection") + conn.wgProxy = conn.wgProxyFactory.GetProxy() + endpoint, err = conn.wgProxy.AddTurnConn(remoteConn) + if err != nil { + return nil, err + } + } else { + // To support old version's with direct mode we attempt to punch an additional role with the remote wireguard port + go conn.punchRemoteWGPort(pair, remoteWgPort) + endpoint = remoteConn.RemoteAddr() + } + + endpointUdpAddr, _ := net.ResolveUDPAddr(endpoint.Network(), endpoint.String()) + + err = conn.config.WgConfig.WgInterface.UpdatePeer(conn.config.WgConfig.RemoteKey, conn.config.WgConfig.AllowedIps, defaultWgKeepAlive, endpointUdpAddr, conn.config.WgConfig.PreSharedKey) if err != nil { - return err + if conn.wgProxy != nil { + _ = conn.wgProxy.CloseConn() + } + return nil, err } conn.status = StatusConnected - peerState.ConnStatus = conn.status - peerState.ConnStatusUpdate = time.Now() - peerState.LocalIceCandidateType = pair.Local.Type().String() - peerState.RemoteIceCandidateType = pair.Remote.Type().String() + peerState := State{ + PubKey: conn.config.Key, + ConnStatus: conn.status, + ConnStatusUpdate: time.Now(), + LocalIceCandidateType: pair.Local.Type().String(), + RemoteIceCandidateType: pair.Remote.Type().String(), + Direct: !isRelayCandidate(pair.Local), + } if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay { peerState.Relayed = true } - peerState.Direct = p.Type() == proxy.TypeDirectNoProxy || p.Type() == proxy.TypeNoProxy err = conn.statusRecorder.UpdatePeerState(peerState) if err != nil { log.Warnf("unable to save peer's state, got error: %v", err) } - return nil -} - -// todo rename this method and the proxy package to something more appropriate -func (conn *Conn) getProxy(pair *ice.CandidatePair, remoteWgPort int) proxy.Proxy { - if isRelayCandidate(pair.Local) { - return proxy.NewWireGuardProxy(conn.config.ProxyConfig) - } - - // To support old version's with direct mode we attempt to punch an additional role with the remote wireguard port - go conn.punchRemoteWGPort(pair, remoteWgPort) - - return proxy.NewNoProxy(conn.config.ProxyConfig) + return endpoint, nil } func (conn *Conn) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) { @@ -414,22 +433,22 @@ func (conn *Conn) cleanup() error { conn.mu.Lock() defer conn.mu.Unlock() + var err1, err2, err3 error if conn.agent != nil { - err := conn.agent.Close() - if err != nil { - return err + err1 = conn.agent.Close() + if err1 == nil { + conn.agent = nil } - conn.agent = nil } - if conn.proxy != nil { - err := conn.proxy.Close() - if err != nil { - return err - } - conn.proxy = nil + if conn.wgProxy != nil { + err2 = conn.wgProxy.CloseConn() + conn.wgProxy = nil } + // todo: is it problem if we try to remove a peer what is never existed? + err3 = conn.config.WgConfig.WgInterface.RemovePeer(conn.config.WgConfig.RemoteKey) + if conn.notifyDisconnected != nil { conn.notifyDisconnected() conn.notifyDisconnected = nil @@ -437,10 +456,11 @@ func (conn *Conn) cleanup() error { conn.status = StatusDisconnected - peerState := State{PubKey: conn.config.Key} - peerState.ConnStatus = conn.status - peerState.ConnStatusUpdate = time.Now() - + peerState := State{ + PubKey: conn.config.Key, + ConnStatus: conn.status, + ConnStatusUpdate: time.Now(), + } err := conn.statusRecorder.UpdatePeerState(peerState) if err != nil { // pretty common error because by that time Engine can already remove the peer and status won't be available. @@ -449,8 +469,13 @@ func (conn *Conn) cleanup() error { } log.Debugf("cleaned up connection to peer %s", conn.config.Key) - - return nil + if err1 != nil { + return err1 + } + if err2 != nil { + return err2 + } + return err3 } // SetSignalOffer sets a handler function to be triggered by Conn when a new connection offer has to be signalled to the remote peer diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index dedc381ac..ac2fa5c00 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -5,12 +5,11 @@ import ( "testing" "time" - "github.com/netbirdio/netbird/client/internal/stdnet" - "github.com/magiconair/properties/assert" "github.com/pion/ice/v2" - "github.com/netbirdio/netbird/client/internal/proxy" + "github.com/netbirdio/netbird/client/internal/stdnet" + "github.com/netbirdio/netbird/client/internal/wgproxy" "github.com/netbirdio/netbird/iface" ) @@ -20,7 +19,6 @@ var connConf = ConnConfig{ StunTurn: []*ice.URL{}, InterfaceBlackList: nil, Timeout: time.Second, - ProxyConfig: proxy.Config{}, LocalWgPort: 51820, } @@ -37,7 +35,11 @@ func TestNewConn_interfaceFilter(t *testing.T) { } func TestConn_GetKey(t *testing.T) { - conn, err := NewConn(connConf, nil, nil, nil) + wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort) + defer func() { + _ = wgProxyFactory.Free() + }() + conn, err := NewConn(connConf, nil, wgProxyFactory, nil, nil) if err != nil { return } @@ -48,8 +50,11 @@ func TestConn_GetKey(t *testing.T) { } func TestConn_OnRemoteOffer(t *testing.T) { - - conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil) + wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort) + defer func() { + _ = wgProxyFactory.Free() + }() + conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil) if err != nil { return } @@ -82,8 +87,11 @@ func TestConn_OnRemoteOffer(t *testing.T) { } func TestConn_OnRemoteAnswer(t *testing.T) { - - conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil) + wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort) + defer func() { + _ = wgProxyFactory.Free() + }() + conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil) if err != nil { return } @@ -115,8 +123,11 @@ func TestConn_OnRemoteAnswer(t *testing.T) { wg.Wait() } func TestConn_Status(t *testing.T) { - - conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil) + wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort) + defer func() { + _ = wgProxyFactory.Free() + }() + conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil) if err != nil { return } @@ -142,8 +153,11 @@ func TestConn_Status(t *testing.T) { } func TestConn_Close(t *testing.T) { - - conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil) + wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort) + defer func() { + _ = wgProxyFactory.Free() + }() + conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil) if err != nil { return } diff --git a/client/internal/proxy/dummy.go b/client/internal/proxy/dummy.go deleted file mode 100644 index ebd7cd68f..000000000 --- a/client/internal/proxy/dummy.go +++ /dev/null @@ -1,72 +0,0 @@ -package proxy - -import ( - "context" - log "github.com/sirupsen/logrus" - "net" - "time" -) - -// DummyProxy just sends pings to the RemoteKey peer and reads responses -type DummyProxy struct { - conn net.Conn - remote string - ctx context.Context - cancel context.CancelFunc -} - -func NewDummyProxy(remote string) *DummyProxy { - p := &DummyProxy{remote: remote} - p.ctx, p.cancel = context.WithCancel(context.Background()) - return p -} - -func (p *DummyProxy) Close() error { - p.cancel() - return nil -} - -func (p *DummyProxy) Start(remoteConn net.Conn) error { - p.conn = remoteConn - go func() { - buf := make([]byte, 1500) - for { - select { - case <-p.ctx.Done(): - return - default: - _, err := p.conn.Read(buf) - if err != nil { - log.Errorf("error while reading RemoteKey %s proxy %v", p.remote, err) - return - } - //log.Debugf("received %s from %s", string(buf[:n]), p.remote) - } - - } - }() - - go func() { - for { - select { - case <-p.ctx.Done(): - return - default: - _, err := p.conn.Write([]byte("hello")) - //log.Debugf("sent ping to %s", p.remote) - if err != nil { - log.Errorf("error while writing to RemoteKey %s proxy %v", p.remote, err) - return - } - time.Sleep(5 * time.Second) - } - } - - }() - - return nil -} - -func (p *DummyProxy) Type() Type { - return TypeDummy -} diff --git a/client/internal/proxy/noproxy.go b/client/internal/proxy/noproxy.go deleted file mode 100644 index dcfe182fd..000000000 --- a/client/internal/proxy/noproxy.go +++ /dev/null @@ -1,42 +0,0 @@ -package proxy - -import ( - log "github.com/sirupsen/logrus" - "net" -) - -// NoProxy is used just to configure WireGuard without any local proxy in between. -// Used when the WireGuard interface is userspace and uses bind.ICEBind -type NoProxy struct { - config Config -} - -// NewNoProxy creates a new NoProxy with a provided config -func NewNoProxy(config Config) *NoProxy { - return &NoProxy{config: config} -} - -// Close removes peer from the WireGuard interface -func (p *NoProxy) Close() error { - err := p.config.WgInterface.RemovePeer(p.config.RemoteKey) - if err != nil { - return err - } - return nil -} - -// Start just updates WireGuard peer with the remote address -func (p *NoProxy) Start(remoteConn net.Conn) error { - - log.Debugf("using NoProxy to connect to peer %s at %s", p.config.RemoteKey, remoteConn.RemoteAddr().String()) - addr, err := net.ResolveUDPAddr("udp", remoteConn.RemoteAddr().String()) - if err != nil { - return err - } - return p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive, - addr, p.config.PreSharedKey) -} - -func (p *NoProxy) Type() Type { - return TypeNoProxy -} diff --git a/client/internal/proxy/proxy.go b/client/internal/proxy/proxy.go deleted file mode 100644 index a0b9e98a1..000000000 --- a/client/internal/proxy/proxy.go +++ /dev/null @@ -1,35 +0,0 @@ -package proxy - -import ( - "github.com/netbirdio/netbird/iface" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "io" - "net" - "time" -) - -const DefaultWgKeepAlive = 25 * time.Second - -type Type string - -const ( - TypeDirectNoProxy Type = "DirectNoProxy" - TypeWireGuard Type = "WireGuard" - TypeDummy Type = "Dummy" - TypeNoProxy Type = "NoProxy" -) - -type Config struct { - WgListenAddr string - RemoteKey string - WgInterface *iface.WGIface - AllowedIps string - PreSharedKey *wgtypes.Key -} - -type Proxy interface { - io.Closer - // Start creates a local remoteConn and starts proxying data from/to remoteConn - Start(remoteConn net.Conn) error - Type() Type -} diff --git a/client/internal/proxy/wireguard.go b/client/internal/proxy/wireguard.go deleted file mode 100644 index ec3c6a730..000000000 --- a/client/internal/proxy/wireguard.go +++ /dev/null @@ -1,128 +0,0 @@ -package proxy - -import ( - "context" - log "github.com/sirupsen/logrus" - "net" -) - -// WireGuardProxy proxies -type WireGuardProxy struct { - ctx context.Context - cancel context.CancelFunc - - config Config - - remoteConn net.Conn - localConn net.Conn -} - -func NewWireGuardProxy(config Config) *WireGuardProxy { - p := &WireGuardProxy{config: config} - p.ctx, p.cancel = context.WithCancel(context.Background()) - return p -} - -func (p *WireGuardProxy) updateEndpoint() error { - udpAddr, err := net.ResolveUDPAddr(p.localConn.LocalAddr().Network(), p.localConn.LocalAddr().String()) - if err != nil { - return err - } - // add local proxy connection as a Wireguard peer - err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive, - udpAddr, p.config.PreSharedKey) - if err != nil { - return err - } - - return nil -} - -func (p *WireGuardProxy) Start(remoteConn net.Conn) error { - p.remoteConn = remoteConn - - var err error - p.localConn, err = net.Dial("udp", p.config.WgListenAddr) - if err != nil { - log.Errorf("failed dialing to local Wireguard port %s", err) - return err - } - - err = p.updateEndpoint() - if err != nil { - log.Errorf("error while updating Wireguard peer endpoint [%s] %v", p.config.RemoteKey, err) - return err - } - - go p.proxyToRemote() - go p.proxyToLocal() - - return nil -} - -func (p *WireGuardProxy) Close() error { - p.cancel() - if c := p.localConn; c != nil { - err := p.localConn.Close() - if err != nil { - return err - } - } - err := p.config.WgInterface.RemovePeer(p.config.RemoteKey) - if err != nil { - return err - } - return nil -} - -// proxyToRemote proxies everything from Wireguard to the RemoteKey peer -// blocks -func (p *WireGuardProxy) proxyToRemote() { - - buf := make([]byte, 1500) - for { - select { - case <-p.ctx.Done(): - log.Debugf("stopped proxying to remote peer %s due to closed connection", p.config.RemoteKey) - return - default: - n, err := p.localConn.Read(buf) - if err != nil { - continue - } - - _, err = p.remoteConn.Write(buf[:n]) - if err != nil { - continue - } - } - } -} - -// proxyToLocal proxies everything from the RemoteKey peer to local Wireguard -// blocks -func (p *WireGuardProxy) proxyToLocal() { - - buf := make([]byte, 1500) - for { - select { - case <-p.ctx.Done(): - log.Debugf("stopped proxying from remote peer %s due to closed connection", p.config.RemoteKey) - return - default: - n, err := p.remoteConn.Read(buf) - if err != nil { - continue - } - - _, err = p.localConn.Write(buf[:n]) - if err != nil { - continue - } - } - } -} - -func (p *WireGuardProxy) Type() Type { - return TypeWireGuard -} diff --git a/client/internal/wgproxy/bpf/portreplace.c b/client/internal/wgproxy/bpf/portreplace.c new file mode 100644 index 000000000..25994116b --- /dev/null +++ b/client/internal/wgproxy/bpf/portreplace.c @@ -0,0 +1,90 @@ +#include +#include // ETH_P_IP +#include +#include +#include +#include +#include + +#define bpf_printk(fmt, ...) \ + ({ \ + char ____fmt[] = fmt; \ + bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \ + }) + +const __u32 map_key_proxy_port = 0; +const __u32 map_key_wg_port = 1; + +struct bpf_map_def SEC("maps") xdp_port_map = { + .type = BPF_MAP_TYPE_ARRAY, + .key_size = sizeof(__u32), + .value_size = sizeof(__u16), + .max_entries = 10, +}; + +__u16 proxy_port = 0; +__u16 wg_port = 0; + +bool read_port_settings() { + __u16 *value; + value = bpf_map_lookup_elem(&xdp_port_map, &map_key_proxy_port); + if(!value) { + return false; + } + + proxy_port = *value; + + value = bpf_map_lookup_elem(&xdp_port_map, &map_key_wg_port); + if(!value) { + return false; + } + wg_port = *value; + + return true; +} + +SEC("xdp") +int xdp_prog_func(struct xdp_md *ctx) { + if(proxy_port == 0 || wg_port == 0) { + if(!read_port_settings()){ + return XDP_PASS; + } + bpf_printk("proxy port: %d, wg port: %d", proxy_port, wg_port); + } + + void *data = (void *)(long)ctx->data; + void *data_end = (void *)(long)ctx->data_end; + struct ethhdr *eth = data; + struct iphdr *ip = (data + sizeof(struct ethhdr)); + struct udphdr *udp = (data + sizeof(struct ethhdr) + sizeof(struct iphdr)); + + // return early if not enough data + if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) > data_end){ + return XDP_PASS; + } + + // skip non IPv4 packages + if (eth->h_proto != htons(ETH_P_IP)) { + return XDP_PASS; + } + + if (ip->protocol != IPPROTO_UDP) { + return XDP_PASS; + } + + // 2130706433 = 127.0.0.1 + if (ip->daddr != htonl(2130706433)) { + return XDP_PASS; + } + + if (udp->source != htons(wg_port)){ + return XDP_PASS; + } + + __be16 new_src_port = udp->dest; + __be16 new_dst_port = htons(proxy_port); + udp->dest = new_dst_port; + udp->source = new_src_port; + return XDP_PASS; +} +char _license[] SEC("license") = "GPL"; \ No newline at end of file diff --git a/client/internal/wgproxy/bpf_bpfeb.go b/client/internal/wgproxy/bpf_bpfeb.go new file mode 100644 index 000000000..37a5cb22c --- /dev/null +++ b/client/internal/wgproxy/bpf_bpfeb.go @@ -0,0 +1,120 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 +// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64 + +package wgproxy + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +// loadBpf returns the embedded CollectionSpec for bpf. +func loadBpf() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_BpfBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load bpf: %w", err) + } + + return spec, err +} + +// loadBpfObjects loads bpf and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *bpfObjects +// *bpfPrograms +// *bpfMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadBpf() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// bpfSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfSpecs struct { + bpfProgramSpecs + bpfMapSpecs +} + +// bpfSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfProgramSpecs struct { + XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"` +} + +// bpfMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfMapSpecs struct { + XdpPortMap *ebpf.MapSpec `ebpf:"xdp_port_map"` +} + +// bpfObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfObjects struct { + bpfPrograms + bpfMaps +} + +func (o *bpfObjects) Close() error { + return _BpfClose( + &o.bpfPrograms, + &o.bpfMaps, + ) +} + +// bpfMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfMaps struct { + XdpPortMap *ebpf.Map `ebpf:"xdp_port_map"` +} + +func (m *bpfMaps) Close() error { + return _BpfClose( + m.XdpPortMap, + ) +} + +// bpfPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfPrograms struct { + XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"` +} + +func (p *bpfPrograms) Close() error { + return _BpfClose( + p.XdpProgFunc, + ) +} + +func _BpfClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed bpf_bpfeb.o +var _BpfBytes []byte diff --git a/client/internal/wgproxy/bpf_bpfeb.o b/client/internal/wgproxy/bpf_bpfeb.o new file mode 100644 index 0000000000000000000000000000000000000000..c3aa757d6c3bf49effa51939e90b9896b7b96596 GIT binary patch literal 6224 zcmb_gU1*!v89tw^HE!1|PMoY_E9E$CgPhuqWp`<|%tj4!=L|zc=72leeO9E;)`w(C zmgGcUGU|&B%+`yrA25T`bn8VJV+6)XD1&&t^2T7k=tV9D3xh#mH@$114DWf)k92%$ zk1+Pa_nr57f6sZ(&v&G^re5tysUXY-+zgBiXL%xZ@=XXn8k zsqnLA+)w0B=sM=VsO|1$jQyx;PY#a>rCA;j2Ps+3h^N^uYJKm8F(M%@jS9|8FYKOC zSgQ4OI6oksGx#7D?_2SJI8L0>{$v%!_mNco zedQxsen&x(uQ_>H0S=(gZqa>Lk}IwgN&;@~JyaeseE=cFs=gTy}cD^Bsr4;&k{ zFQ8*Qvng+9Ib|G z-7Cf5SrsH+NDrt$gYudZbS)72q*7_i|0UhkMK_m0PTcPc_H(HyiuqPvy~3!fQMeRA zgfcXuW;2Wz;%YH!i$*(KUWrzsP{U}qHVjwB$HYpt8823&qKvC8QE0Uhq^0s;Xp7;( zO0_^lsZ>?6(sZ-cSSe`XQc<3St1Swu_xTacHQ~$q8U<$%)~F)EkIjP9Ilp zowH@vL34aac7sx5YxU%nT8#Xx3Cn?crL|-*U8z+U2hp+NNvl089CK#h-`vi)Ow(Du zB%AT~qS}HTchoACijBd~$m9B+;+3M7PfFXBLA`?F1(`;y;mH!0TdO7qCS<8qt2Wb9 zv(sTNJf9o1)uL%!AD&cpg<6HC=X1G>7iKSnm%fqn&5HSAv0=ki(qozN(N{)KzdSy^ zDHJlU+RGzyWmZKiE?O&`PXa+EnOtI^f4*ld|;G`;LjirFKMakEjd#wtxQYPNJ) z-K-NTu(fscG{^k7Y0rw!?#+(K^jg?0UW^t*YqcH;?q$xNP`gsIOZhgIOREt_O%-6F zlE)e19s~j#By+mEL7F8TD*9WfMuqE#ZVRP+1IDjjy!h(t;kVC!XDXb1JvSAGBCN!P zsM?Ig$mJ%^r$&u)FRTzFX$t%8l;(VU0WF@y`*^mDjAt}&DfW!SaUH<3gZVk)Sx*~& zBS{huzAHtz;GqenvuOr1LV5`+@-hHvtN6VTTU&I<82V$LrKG&-MJ=DWK(QEW|5zKxWSQ`8eN&!BRHuyskyn!(%&KUd;lm_b}k=0my zDuQAFeqX|ZHrKyhCLaJL{%K%WKMhP9YtQ&@{xx9N ze*@UHzXj~(=UR5+tF`N3wN{;Y3D$ww)FZ+1)867Vc=iWNiIl)usq$Go1I+8LwJ(Ba z{|pvG;5jynzYdC+xQX8mUySEQP#$;+IAD4z%mngBZcjhgSYF+SMi1n#+;uD7q(R&YjDIUtu#Ej%zfg zn@q-Dr5GeLlEF~_FZ2^c;TqV3`iG71@1D}0Zxr))zQz9cAWH+#rh0c@yrcTPp8fK-jmJ&2%w^9x>rdG)@pKY_ zcc>?4fVH_2>URHoos)F;1>MMYfLQxAEBgV-$3$$NKZX zCYP)|+v|8N_VceBCqKWnr~0>!zcIH>&)b?iz%jby#``rC`^;aTzkc{_CE0p${&<&n z`$N53o2;EQnHYD7WqI4~mPt2B^HFOxCQOT2Qwema31ns0Hi1)G_@})}c>l^)_HsydG<*#K2y^a1SO?kOeYEZuJl}j7_dr}@&DM4L-rP`zLySKl; ze-G>61Vqf6Q=eW^15Brt-Gg4$9z7z&S+GN=)kLhtj!DH@Z&bD`(072ldRjs;f22p^ zwU{c@eA{uXL->!qA`Rx-qT(G)br?G(aUA_^3da?C40$)$x5v~3xb+QHcMvM+Btg2j%hh{ZQK$6#)G=I(_cH3di#k^&3ll?G@k^2Li3B@2Q|M5 zenRtiz<(P&sDm+6ikG{?qZ9;Dv=i;yU^wjbL7PYbjK5=?Pfah$aj6`;z5pS+^g8%7 zFmh_=5p)i`*)r!f??FDP`6T!m%`bwV)BGm*?|}z(b>W9`@(Oow{j4cI!WwgaH?=K! z+Wy4BKhyTqAs;p*x!O2Ru4bBSX*)I^o{tpn>I2VCzXwK4c_`|mPp%ogJtY6JWutb7 z@~w7=;y-~6{7FCh2$AvsY{}KhD}+4C{mZ3NK~#ct*5@Mht3j<67FVibxE3^OV))!r zdHEbQm7rWmXM*93@Icqog?ivkdeHi->9SuamGbqn9~6Ss#F<96teinJ=$)ZHAFTUw z-Al#5gme-vCr(O71Kx%dv@GEItW+t>|0Udw6+4zjmfs%;j&n6Dvguk{{`f&rA%8W1 z2xX`Q)v6yZhsA8r5S50%RuAfduV8Rc>-+WbF;Op8!#!pM!cIhacIcwyh&hV$%)~Flq>L`Pn?q7 znrF*_gXYwr*K$gYsa5CoQi%Ah3Cn?cC6r_!Q79Ex1~9OpDWjbbmN}z8-`vc&mtbe_ zb*~zJKPWAmyn|XUm#qvuBM-}mitAY=pYqI52Gj})m%Rjh4Nc{^+-fE76KLK+SR$M{`@ynu30vn%~p)NLSk%meB`Sm7tW85 zA8?3)4NX-`^-3l%w$+5Gt{$WBF9o9)ykc;}&sJ-C8uch!lcM*CWn8UfjIj)p4XQO& zRy*pXbZl%bJ`rogKhPfk zK#1EGjskCM{4cZ-0FKoIPxA2G02Y5xc@e)4+_9r{2FHPCpd&VUlr=VaAT_Bsd2Cvk zd0?ngZ}QmD*yORRvB~41g_*~v8vhC7>x7c`1`jer3mO}JPU9ZT+nGJwnKVukKNg~) zaUOW`De7q9PT<=b{{{Lk+={j>+zEVN{&Pt%onzYmeHVc&F6HKys_rxp$=ubApGe)O*}jga2e(?2foXZYr@a}DX@3ZLtZ9vPs1+O1 z-rUezaUqt(bUW~LD=3J0J|r#7-U}9{ea^zX*WR`;?{Kss(I5LK2tdTt&k%r2eA_?c zZ&C>|{kIfs+TXP><74YdGXD3-fDGn3HJIaR*O+xB?zS+;$8}4>GL`i>Z(+upwd{G{ z&RUqc58@%wKgaM50uV8MrU*brzbH`)mmR$6U{i0?|Bj>IcJPjaA3FGng*m>b4(`zP zG~PaAC3 zi@`kSNCwY1c)`Ir2R9si%fVX?zU$!o4&HU}BL~0S+ZT)D?hzF9_Yt|vb8iguW9bDh zQ}XH}uPTRqa={tD4h~$Kh6{!3t3EHE__Ao-few5k$FGq5q7@nBD ze0kLW3Y!ssT%j@5jE=gae;^(IiN zq_)`6t!a3c*hGK!`D}D?{qeh~1>I;l#ilV|=~*z$r@Uq8fW6rw^Nw0POG1!GadZZ-1+U+kXN&#&G)=w7|7@ z$N3O_8Gj6qO{V_|TL_-EB-7s1yc_@4bK;wR(g}WkYxy!Vw(six6Xu^sTIuHVA>@b6 zUtCYnqV7k?_neszGk=_BcTDWE%a;8oN|BoAg6|y6<`z17+ lc$$>Z`;Ys}CU>mAgwh$JG~d?$Y|7~lY* Date: Wed, 26 Jul 2023 16:21:04 +0200 Subject: [PATCH 24/27] Avoid compiling linux NewFactory for Android (#1032) --- client/internal/wgproxy/factory_linux.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/internal/wgproxy/factory_linux.go b/client/internal/wgproxy/factory_linux.go index 0ae9b57b3..aae4a95d6 100644 --- a/client/internal/wgproxy/factory_linux.go +++ b/client/internal/wgproxy/factory_linux.go @@ -1,3 +1,5 @@ +//go:build !android + package wgproxy import ( From 7794b744f8b039ed2053d5e7328d716a3aeb5a98 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 27 Jul 2023 12:31:07 +0300 Subject: [PATCH 25/27] Add PKCE authorization flow (#1012) Enhance the user experience by enabling authentication to Netbird using Single Sign-On (SSO) with any Identity Provider (IDP) provider. Current client offers this capability through the Device Authorization Flow, however, is not widely supported by many IDPs, and even some that do support it do not provide a complete verification URL. To address these challenges, this pull request enable Authorization Code Flow with Proof Key for Code Exchange (PKCE) for client logins, which is a more widely adopted and secure approach to facilitate SSO with various IDP providers. --- .../workflows/test-docker-compose-linux.yml | 11 +- client/android/login.go | 39 +- client/cmd/login.go | 35 +- client/internal/auth/device_flow.go | 202 +++++++ .../device_flow_test.go} | 31 +- client/internal/auth/oauth.go | 90 +++ client/internal/auth/pkce_flow.go | 217 ++++++++ client/internal/auth/util.go | 62 +++ client/internal/device_auth.go | 12 +- client/internal/oauth.go | 286 ---------- client/internal/pkce_auth.go | 128 +++++ client/server/server.go | 52 +- infrastructure_files/base.setup.env | 8 +- infrastructure_files/configure.sh | 13 + infrastructure_files/management.json.tmpl | 12 + infrastructure_files/setup.env.example | 6 + management/client/client.go | 1 + management/client/client_test.go | 46 ++ management/client/grpc.go | 34 ++ management/client/mock.go | 8 + management/cmd/management.go | 18 +- management/proto/management.pb.go | 527 +++++++++++------- management/proto/management.proto | 23 +- management/proto/management_grpc.pb.go | 46 ++ management/server/config.go | 15 +- management/server/grpcserver.go | 46 ++ .../mock_server/management_server_mock.go | 8 + 27 files changed, 1395 insertions(+), 581 deletions(-) create mode 100644 client/internal/auth/device_flow.go rename client/internal/{oauth_test.go => auth/device_flow_test.go} (93%) create mode 100644 client/internal/auth/oauth.go create mode 100644 client/internal/auth/pkce_flow.go create mode 100644 client/internal/auth/util.go delete mode 100644 client/internal/oauth.go create mode 100644 client/internal/pkce_auth.go diff --git a/.github/workflows/test-docker-compose-linux.yml b/.github/workflows/test-docker-compose-linux.yml index 274edcb6b..3910de0f2 100644 --- a/.github/workflows/test-docker-compose-linux.yml +++ b/.github/workflows/test-docker-compose-linux.yml @@ -69,6 +69,7 @@ jobs: CI_NETBIRD_AUTH_JWT_CERTS: https://example.eu.auth0.com/.well-known/jwks.json CI_NETBIRD_AUTH_TOKEN_ENDPOINT: https://example.eu.auth0.com/oauth/token CI_NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT: https://example.eu.auth0.com/oauth/device/code + CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT: https://example.eu.auth0.com/authorize CI_NETBIRD_AUTH_REDIRECT_URI: "/peers" CI_NETBIRD_TOKEN_SOURCE: "idToken" CI_NETBIRD_AUTH_USER_ID_CLAIM: "email" @@ -91,8 +92,8 @@ jobs: grep LETSENCRYPT_DOMAIN docker-compose.yml | egrep 'LETSENCRYPT_DOMAIN=$' grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM - grep -A 1 ProviderConfig management.json | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE - grep Scope management.json | grep "$CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE" + grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE + grep -A 8 DeviceAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE" grep UseIDToken management.json | grep false grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP grep -A 3 IdpManagerConfig management.json | grep -A 1 ClientConfig | grep Issuer | grep $CI_NETBIRD_AUTH_AUTHORITY @@ -100,6 +101,12 @@ jobs: grep -A 5 IdpManagerConfig management.json | grep -A 3 ClientConfig | grep ClientID | grep $CI_NETBIRD_IDP_MGMT_CLIENT_ID grep -A 6 IdpManagerConfig management.json | grep -A 4 ClientConfig | grep ClientSecret | grep $CI_NETBIRD_IDP_MGMT_CLIENT_SECRET grep -A 7 IdpManagerConfig management.json | grep -A 5 ClientConfig | grep GrantType | grep client_credentials + grep -A 2 PKCEAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_AUDIENCE + grep -A 3 PKCEAuthorizationFlow management.json | grep -A 2 ProviderConfig | grep ClientID | grep $CI_NETBIRD_AUTH_CLIENT_ID + grep -A 4 PKCEAuthorizationFlow management.json | grep -A 3 ProviderConfig | grep ClientSecret | grep $CI_NETBIRD_AUTH_CLIENT_SECRET + grep -A 5 PKCEAuthorizationFlow management.json | grep -A 4 ProviderConfig | grep AuthorizationEndpoint | grep $CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT + grep -A 6 PKCEAuthorizationFlow management.json | grep -A 5 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT + grep -A 7 PKCEAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES" - name: run docker compose up working-directory: infrastructure_files diff --git a/client/android/login.go b/client/android/login.go index 518942cb6..ad334541c 100644 --- a/client/android/login.go +++ b/client/android/login.go @@ -6,15 +6,14 @@ import ( "time" "github.com/cenkalti/backoff/v4" - log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" gstatus "google.golang.org/grpc/status" "github.com/netbirdio/netbird/client/cmd" - "github.com/netbirdio/netbird/client/system" - "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/auth" + "github.com/netbirdio/netbird/client/system" ) // SSOListener is async listener for mobile framework @@ -87,9 +86,15 @@ func (a *Auth) saveConfigIfSSOSupported() (bool, error) { err := a.withBackOff(a.ctx, func() (err error) { _, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL) if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound { - supportsSSO = false - err = nil + _, err = internal.GetPKCEAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL) + if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound { + supportsSSO = false + err = nil + } + + return err } + return err }) @@ -183,27 +188,15 @@ func (a *Auth) login(urlOpener URLOpener) error { return nil } -func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*internal.TokenInfo, error) { - providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL) +func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, error) { + oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config) if err != nil { - s, ok := gstatus.FromError(err) - if ok && s.Code() == codes.NotFound { - return nil, fmt.Errorf("no SSO provider returned from management. " + - "If you are using hosting Netbird see documentation at " + - "https://github.com/netbirdio/netbird/tree/main/management for details") - } else if ok && s.Code() == codes.Unimplemented { - return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+ - "please update your servver or use Setup Keys to login", a.config.ManagementURL) - } else { - return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err) - } + return nil, err } - hostedClient := internal.NewHostedDeviceFlow(providerConfig.ProviderConfig) - - flowInfo, err := hostedClient.RequestDeviceCode(context.TODO()) + flowInfo, err := oAuthFlow.RequestAuthInfo(context.TODO()) if err != nil { - return nil, fmt.Errorf("getting a request device code failed: %v", err) + return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err) } go urlOpener.Open(flowInfo.VerificationURIComplete) @@ -211,7 +204,7 @@ func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*internal.TokenInfo, waitTimeout := time.Duration(flowInfo.ExpiresIn) waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout*time.Second) defer cancel() - tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo) + tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo) if err != nil { return nil, fmt.Errorf("waiting for browser login failed: %v", err) } diff --git a/client/cmd/login.go b/client/cmd/login.go index 83ed7f3b8..566f661a3 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "github.com/netbirdio/netbird/client/internal/auth" "strings" "time" @@ -163,31 +164,15 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C return nil } -func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*internal.TokenInfo, error) { - providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL) +func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) { + oAuthFlow, err := auth.NewOAuthFlow(ctx, config) if err != nil { - s, ok := gstatus.FromError(err) - if ok && s.Code() == codes.NotFound { - return nil, fmt.Errorf("no SSO provider returned from management. " + - "If you are using hosting Netbird see documentation at " + - "https://github.com/netbirdio/netbird/tree/main/management for details") - } else if ok && s.Code() == codes.Unimplemented { - mgmtURL := managementURL - if mgmtURL == "" { - mgmtURL = internal.DefaultManagementURL - } - return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+ - "please update your servver or use Setup Keys to login", mgmtURL) - } else { - return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err) - } + return nil, err } - hostedClient := internal.NewHostedDeviceFlow(providerConfig.ProviderConfig) - - flowInfo, err := hostedClient.RequestDeviceCode(context.TODO()) + flowInfo, err := oAuthFlow.RequestAuthInfo(context.TODO()) if err != nil { - return nil, fmt.Errorf("getting a request device code failed: %v", err) + return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err) } openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode) @@ -196,7 +181,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second) defer c() - tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo) + tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo) if err != nil { return nil, fmt.Errorf("waiting for browser login failed: %v", err) } @@ -206,8 +191,10 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) { var codeMsg string - if !strings.Contains(verificationURIComplete, userCode) { - codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode) + if userCode != "" { + if !strings.Contains(verificationURIComplete, userCode) { + codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode) + } } err := open.Run(verificationURIComplete) diff --git a/client/internal/auth/device_flow.go b/client/internal/auth/device_flow.go new file mode 100644 index 000000000..c28e42772 --- /dev/null +++ b/client/internal/auth/device_flow.go @@ -0,0 +1,202 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "github.com/netbirdio/netbird/client/internal" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// HostedGrantType grant type for device flow on Hosted +const ( + HostedGrantType = "urn:ietf:params:oauth:grant-type:device_code" +) + +var _ OAuthFlow = &DeviceAuthorizationFlow{} + +// DeviceAuthorizationFlow implements the OAuthFlow interface, +// for the Device Authorization Flow. +type DeviceAuthorizationFlow struct { + providerConfig internal.DeviceAuthProviderConfig + + HTTPClient HTTPClient +} + +// RequestDeviceCodePayload used for request device code payload for auth0 +type RequestDeviceCodePayload struct { + Audience string `json:"audience"` + ClientID string `json:"client_id"` + Scope string `json:"scope"` +} + +// TokenRequestPayload used for requesting the auth0 token +type TokenRequestPayload struct { + GrantType string `json:"grant_type"` + DeviceCode string `json:"device_code,omitempty"` + ClientID string `json:"client_id"` + RefreshToken string `json:"refresh_token,omitempty"` +} + +// TokenRequestResponse used for parsing Hosted token's response +type TokenRequestResponse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` + TokenInfo +} + +// NewDeviceAuthorizationFlow returns device authorization flow client +func NewDeviceAuthorizationFlow(config internal.DeviceAuthProviderConfig) (*DeviceAuthorizationFlow, error) { + httpTransport := http.DefaultTransport.(*http.Transport).Clone() + httpTransport.MaxIdleConns = 5 + + httpClient := &http.Client{ + Timeout: 10 * time.Second, + Transport: httpTransport, + } + + return &DeviceAuthorizationFlow{ + providerConfig: config, + HTTPClient: httpClient, + }, nil +} + +// GetClientID returns the provider client id +func (d *DeviceAuthorizationFlow) GetClientID(ctx context.Context) string { + return d.providerConfig.ClientID +} + +// RequestAuthInfo requests a device code login flow information from Hosted +func (d *DeviceAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlowInfo, error) { + form := url.Values{} + form.Add("client_id", d.providerConfig.ClientID) + form.Add("audience", d.providerConfig.Audience) + form.Add("scope", d.providerConfig.Scope) + req, err := http.NewRequest("POST", d.providerConfig.DeviceAuthEndpoint, + strings.NewReader(form.Encode())) + if err != nil { + return AuthFlowInfo{}, fmt.Errorf("creating request failed with error: %v", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err := d.HTTPClient.Do(req) + if err != nil { + return AuthFlowInfo{}, fmt.Errorf("doing request failed with error: %v", err) + } + + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return AuthFlowInfo{}, fmt.Errorf("reading body failed with error: %v", err) + } + + if res.StatusCode != 200 { + return AuthFlowInfo{}, fmt.Errorf("request device code returned status %d error: %s", res.StatusCode, string(body)) + } + + deviceCode := AuthFlowInfo{} + err = json.Unmarshal(body, &deviceCode) + if err != nil { + return AuthFlowInfo{}, fmt.Errorf("unmarshaling response failed with error: %v", err) + } + + // Fallback to the verification_uri if the IdP doesn't support verification_uri_complete + if deviceCode.VerificationURIComplete == "" { + deviceCode.VerificationURIComplete = deviceCode.VerificationURI + } + + return deviceCode, err +} + +func (d *DeviceAuthorizationFlow) requestToken(info AuthFlowInfo) (TokenRequestResponse, error) { + form := url.Values{} + form.Add("client_id", d.providerConfig.ClientID) + form.Add("grant_type", HostedGrantType) + form.Add("device_code", info.DeviceCode) + + req, err := http.NewRequest("POST", d.providerConfig.TokenEndpoint, strings.NewReader(form.Encode())) + if err != nil { + return TokenRequestResponse{}, fmt.Errorf("failed to create request access token: %v", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err := d.HTTPClient.Do(req) + if err != nil { + return TokenRequestResponse{}, fmt.Errorf("failed to request access token with error: %v", err) + } + + defer func() { + err := res.Body.Close() + if err != nil { + return + } + }() + + body, err := io.ReadAll(res.Body) + if err != nil { + return TokenRequestResponse{}, fmt.Errorf("failed reading access token response body with error: %v", err) + } + + if res.StatusCode > 499 { + return TokenRequestResponse{}, fmt.Errorf("access token response returned code: %s", string(body)) + } + + tokenResponse := TokenRequestResponse{} + err = json.Unmarshal(body, &tokenResponse) + if err != nil { + return TokenRequestResponse{}, fmt.Errorf("parsing token response failed with error: %v", err) + } + + return tokenResponse, nil +} + +// WaitToken waits user's login and authorize the app. Once the user's authorize +// it retrieves the access token from Hosted's endpoint and validates it before returning +func (d *DeviceAuthorizationFlow) WaitToken(ctx context.Context, info AuthFlowInfo) (TokenInfo, error) { + interval := time.Duration(info.Interval) * time.Second + ticker := time.NewTicker(interval) + for { + select { + case <-ctx.Done(): + return TokenInfo{}, ctx.Err() + case <-ticker.C: + + tokenResponse, err := d.requestToken(info) + if err != nil { + return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err) + } + + if tokenResponse.Error != "" { + if tokenResponse.Error == "authorization_pending" { + continue + } else if tokenResponse.Error == "slow_down" { + interval = interval + (3 * time.Second) + ticker.Reset(interval) + continue + } + + return TokenInfo{}, fmt.Errorf(tokenResponse.ErrorDescription) + } + + tokenInfo := TokenInfo{ + AccessToken: tokenResponse.AccessToken, + TokenType: tokenResponse.TokenType, + RefreshToken: tokenResponse.RefreshToken, + IDToken: tokenResponse.IDToken, + ExpiresIn: tokenResponse.ExpiresIn, + UseIDToken: d.providerConfig.UseIDToken, + } + + err = isValidAccessToken(tokenInfo.GetTokenToUse(), d.providerConfig.Audience) + if err != nil { + return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err) + } + + return tokenInfo, err + } + } +} diff --git a/client/internal/oauth_test.go b/client/internal/auth/device_flow_test.go similarity index 93% rename from client/internal/oauth_test.go rename to client/internal/auth/device_flow_test.go index aa71fa0eb..dc950ac63 100644 --- a/client/internal/oauth_test.go +++ b/client/internal/auth/device_flow_test.go @@ -1,17 +1,17 @@ -package internal +package auth import ( "context" "fmt" + "github.com/golang-jwt/jwt" + "github.com/netbirdio/netbird/client/internal" + "github.com/stretchr/testify/require" "io" "net/http" "net/url" "strings" "testing" "time" - - "github.com/golang-jwt/jwt" - "github.com/stretchr/testify/require" ) type mockHTTPClient struct { @@ -53,7 +53,7 @@ func TestHosted_RequestDeviceCode(t *testing.T) { testingErrFunc require.ErrorAssertionFunc expectedErrorMSG string testingFunc require.ComparisonAssertionFunc - expectedOut DeviceAuthInfo + expectedOut AuthFlowInfo expectedMSG string expectPayload string } @@ -92,7 +92,7 @@ func TestHosted_RequestDeviceCode(t *testing.T) { testingFunc: require.EqualValues, expectPayload: expectPayload, } - testCase4Out := DeviceAuthInfo{ExpiresIn: 10} + testCase4Out := AuthFlowInfo{ExpiresIn: 10} testCase4 := test{ name: "Got Device Code", inputResBody: fmt.Sprintf("{\"expires_in\":%d}", testCase4Out.ExpiresIn), @@ -113,8 +113,8 @@ func TestHosted_RequestDeviceCode(t *testing.T) { err: testCase.inputReqError, } - hosted := Hosted{ - providerConfig: ProviderConfig{ + deviceFlow := &DeviceAuthorizationFlow{ + providerConfig: internal.DeviceAuthProviderConfig{ Audience: expectedAudience, ClientID: expectedClientID, Scope: expectedScope, @@ -125,7 +125,7 @@ func TestHosted_RequestDeviceCode(t *testing.T) { HTTPClient: &httpClient, } - authInfo, err := hosted.RequestDeviceCode(context.TODO()) + authInfo, err := deviceFlow.RequestAuthInfo(context.TODO()) testCase.testingErrFunc(t, err, testCase.expectedErrorMSG) require.EqualValues(t, expectPayload, httpClient.reqBody, "payload should match") @@ -145,7 +145,7 @@ func TestHosted_WaitToken(t *testing.T) { inputMaxReqs int inputCountResBody string inputTimeout time.Duration - inputInfo DeviceAuthInfo + inputInfo AuthFlowInfo inputAudience string testingErrFunc require.ErrorAssertionFunc expectedErrorMSG string @@ -155,7 +155,7 @@ func TestHosted_WaitToken(t *testing.T) { expectPayload string } - defaultInfo := DeviceAuthInfo{ + defaultInfo := AuthFlowInfo{ DeviceCode: "test", ExpiresIn: 10, Interval: 1, @@ -278,8 +278,8 @@ func TestHosted_WaitToken(t *testing.T) { countResBody: testCase.inputCountResBody, } - hosted := Hosted{ - providerConfig: ProviderConfig{ + deviceFlow := DeviceAuthorizationFlow{ + providerConfig: internal.DeviceAuthProviderConfig{ Audience: testCase.inputAudience, ClientID: clientID, TokenEndpoint: "test.hosted.com/token", @@ -287,11 +287,12 @@ func TestHosted_WaitToken(t *testing.T) { Scope: "openid", UseIDToken: false, }, - HTTPClient: &httpClient} + HTTPClient: &httpClient, + } ctx, cancel := context.WithTimeout(context.TODO(), testCase.inputTimeout) defer cancel() - tokenInfo, err := hosted.WaitToken(ctx, testCase.inputInfo) + tokenInfo, err := deviceFlow.WaitToken(ctx, testCase.inputInfo) testCase.testingErrFunc(t, err, testCase.expectedErrorMSG) require.EqualValues(t, testCase.expectPayload, httpClient.reqBody, "payload should match") diff --git a/client/internal/auth/oauth.go b/client/internal/auth/oauth.go new file mode 100644 index 000000000..d7365df60 --- /dev/null +++ b/client/internal/auth/oauth.go @@ -0,0 +1,90 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal" +) + +// OAuthFlow represents an interface for authorization using different OAuth 2.0 flows +type OAuthFlow interface { + RequestAuthInfo(ctx context.Context) (AuthFlowInfo, error) + WaitToken(ctx context.Context, info AuthFlowInfo) (TokenInfo, error) + GetClientID(ctx context.Context) string +} + +// HTTPClient http client interface for API calls +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// AuthFlowInfo holds information for the OAuth 2.0 authorization flow +type AuthFlowInfo struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURIComplete string `json:"verification_uri_complete"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` +} + +// Claims used when validating the access token +type Claims struct { + Audience interface{} `json:"aud"` +} + +// TokenInfo holds information of issued access token +type TokenInfo struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IDToken string `json:"id_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + UseIDToken bool `json:"-"` +} + +// GetTokenToUse returns either the access or id token based on UseIDToken field +func (t TokenInfo) GetTokenToUse() string { + if t.UseIDToken { + return t.IDToken + } + return t.AccessToken +} + +// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration. +func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) { + log.Debug("getting device authorization flow info") + + // Try to initialize the Device Authorization Flow + deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL) + if err == nil { + return NewDeviceAuthorizationFlow(deviceFlowInfo.ProviderConfig) + } + + log.Debugf("getting device authorization flow info failed with error: %v", err) + log.Debugf("falling back to pkce authorization flow info") + + // If Device Authorization Flow failed, try the PKCE Authorization Flow + pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL) + if err != nil { + s, ok := gstatus.FromError(err) + if ok && s.Code() == codes.NotFound { + return nil, fmt.Errorf("no SSO provider returned from management. " + + "If you are using hosting Netbird see documentation at " + + "https://github.com/netbirdio/netbird/tree/main/management for details") + } else if ok && s.Code() == codes.Unimplemented { + return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+ + "please update your server or use Setup Keys to login", config.ManagementURL) + } else { + return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err) + } + } + + return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig) +} diff --git a/client/internal/auth/pkce_flow.go b/client/internal/auth/pkce_flow.go new file mode 100644 index 000000000..9451ce055 --- /dev/null +++ b/client/internal/auth/pkce_flow.go @@ -0,0 +1,217 @@ +package auth + +import ( + "context" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "fmt" + "net" + "net/http" + "net/url" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + + "github.com/netbirdio/netbird/client/internal" +) + +var _ OAuthFlow = &PKCEAuthorizationFlow{} + +const ( + queryState = "state" + queryCode = "code" + defaultPKCETimeoutSeconds = 300 +) + +// PKCEAuthorizationFlow implements the OAuthFlow interface for +// the Authorization Code Flow with PKCE. +type PKCEAuthorizationFlow struct { + providerConfig internal.PKCEAuthProviderConfig + state string + codeVerifier string + oAuthConfig *oauth2.Config +} + +// NewPKCEAuthorizationFlow returns new PKCE authorization code flow. +func NewPKCEAuthorizationFlow(config internal.PKCEAuthProviderConfig) (*PKCEAuthorizationFlow, error) { + var availableRedirectURL string + + // find the first available redirect URL + for _, redirectURL := range config.RedirectURLs { + if !isRedirectURLPortUsed(redirectURL) { + availableRedirectURL = redirectURL + break + } + } + + if availableRedirectURL == "" { + return nil, fmt.Errorf("no available port found from configured redirect URLs: %q", config.RedirectURLs) + } + + cfg := &oauth2.Config{ + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: config.AuthorizationEndpoint, + TokenURL: config.TokenEndpoint, + }, + RedirectURL: availableRedirectURL, + Scopes: strings.Split(config.Scope, " "), + } + + return &PKCEAuthorizationFlow{ + providerConfig: config, + oAuthConfig: cfg, + }, nil +} + +// GetClientID returns the provider client id +func (p *PKCEAuthorizationFlow) GetClientID(_ context.Context) string { + return p.providerConfig.ClientID +} + +// RequestAuthInfo requests a authorization code login flow information. +func (p *PKCEAuthorizationFlow) RequestAuthInfo(_ context.Context) (AuthFlowInfo, error) { + state, err := randomBytesInHex(24) + if err != nil { + return AuthFlowInfo{}, fmt.Errorf("could not generate random state: %v", err) + } + p.state = state + + codeVerifier, err := randomBytesInHex(64) + if err != nil { + return AuthFlowInfo{}, fmt.Errorf("could not create a code verifier: %v", err) + } + p.codeVerifier = codeVerifier + + codeChallenge := createCodeChallenge(codeVerifier) + authURL := p.oAuthConfig.AuthCodeURL( + state, + oauth2.SetAuthURLParam("code_challenge_method", "S256"), + oauth2.SetAuthURLParam("code_challenge", codeChallenge), + oauth2.SetAuthURLParam("audience", p.providerConfig.Audience), + ) + + return AuthFlowInfo{ + VerificationURIComplete: authURL, + ExpiresIn: defaultPKCETimeoutSeconds, + }, nil +} + +// WaitToken waits for the OAuth token in the PKCE Authorization Flow. +// It starts an HTTP server to receive the OAuth token callback and waits for the token or an error. +// Once the token is received, it is converted to TokenInfo and validated before returning. +func (p *PKCEAuthorizationFlow) WaitToken(ctx context.Context, _ AuthFlowInfo) (TokenInfo, error) { + tokenChan := make(chan *oauth2.Token, 1) + errChan := make(chan error, 1) + + go p.startServer(tokenChan, errChan) + + select { + case <-ctx.Done(): + return TokenInfo{}, ctx.Err() + case token := <-tokenChan: + return p.handleOAuthToken(token) + case err := <-errChan: + return TokenInfo{}, err + } +} + +func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errChan chan<- error) { + parsedURL, err := url.Parse(p.oAuthConfig.RedirectURL) + if err != nil { + errChan <- fmt.Errorf("failed to parse redirect URL: %v", err) + return + } + port := parsedURL.Port() + + server := http.Server{Addr: fmt.Sprintf(":%s", port)} + defer func() { + if err := server.Shutdown(context.Background()); err != nil { + log.Errorf("error while shutting down pkce flow server: %v", err) + } + }() + + http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + query := req.URL.Query() + + state := query.Get(queryState) + // Prevent timing attacks on state + if subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 { + errChan <- fmt.Errorf("invalid state") + return + } + + code := query.Get(queryCode) + if code == "" { + errChan <- fmt.Errorf("missing code") + return + } + + // Exchange the authorization code for the OAuth token + token, err := p.oAuthConfig.Exchange( + req.Context(), + code, + oauth2.SetAuthURLParam("code_verifier", p.codeVerifier), + ) + if err != nil { + errChan <- fmt.Errorf("OAuth token exchange failed: %v", err) + return + } + + tokenChan <- token + }) + + if err := server.ListenAndServe(); err != nil { + errChan <- err + } +} + +func (p *PKCEAuthorizationFlow) handleOAuthToken(token *oauth2.Token) (TokenInfo, error) { + tokenInfo := TokenInfo{ + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + TokenType: token.TokenType, + ExpiresIn: token.Expiry.Second(), + UseIDToken: p.providerConfig.UseIDToken, + } + if idToken, ok := token.Extra("id_token").(string); ok { + tokenInfo.IDToken = idToken + } + + if err := isValidAccessToken(tokenInfo.GetTokenToUse(), p.providerConfig.Audience); err != nil { + return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err) + } + + return tokenInfo, nil +} + +func createCodeChallenge(codeVerifier string) string { + sha2 := sha256.Sum256([]byte(codeVerifier)) + return base64.RawURLEncoding.EncodeToString(sha2[:]) +} + +// isRedirectURLPortUsed checks if the port used in the redirect URL is in use. +func isRedirectURLPortUsed(redirectURL string) bool { + parsedURL, err := url.Parse(redirectURL) + if err != nil { + log.Errorf("failed to parse redirect URL: %v", err) + return true + } + + addr := fmt.Sprintf(":%s", parsedURL.Port()) + conn, err := net.DialTimeout("tcp", addr, 3*time.Second) + if err != nil { + return false + } + defer func() { + if err := conn.Close(); err != nil { + log.Errorf("error while closing the connection: %v", err) + } + }() + + return true +} diff --git a/client/internal/auth/util.go b/client/internal/auth/util.go new file mode 100644 index 000000000..33a0e6e35 --- /dev/null +++ b/client/internal/auth/util.go @@ -0,0 +1,62 @@ +package auth + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "reflect" + "strings" +) + +func randomBytesInHex(count int) (string, error) { + buf := make([]byte, count) + _, err := io.ReadFull(rand.Reader, buf) + if err != nil { + return "", fmt.Errorf("could not generate %d random bytes: %v", count, err) + } + + return hex.EncodeToString(buf), nil +} + +// isValidAccessToken is a simple validation of the access token +func isValidAccessToken(token string, audience string) error { + if token == "" { + return fmt.Errorf("token received is empty") + } + + encodedClaims := strings.Split(token, ".")[1] + claimsString, err := base64.RawURLEncoding.DecodeString(encodedClaims) + if err != nil { + return err + } + + claims := Claims{} + err = json.Unmarshal(claimsString, &claims) + if err != nil { + return err + } + + if claims.Audience == nil { + return fmt.Errorf("required token field audience is absent") + } + + // Audience claim of JWT can be a string or an array of strings + typ := reflect.TypeOf(claims.Audience) + switch typ.Kind() { + case reflect.String: + if claims.Audience == audience { + return nil + } + case reflect.Slice: + for _, aud := range claims.Audience.([]interface{}) { + if audience == aud { + return nil + } + } + } + + return fmt.Errorf("invalid JWT token audience field") +} diff --git a/client/internal/device_auth.go b/client/internal/device_auth.go index 0273bb8e4..8e68f7544 100644 --- a/client/internal/device_auth.go +++ b/client/internal/device_auth.go @@ -16,11 +16,11 @@ import ( // DeviceAuthorizationFlow represents Device Authorization Flow information type DeviceAuthorizationFlow struct { Provider string - ProviderConfig ProviderConfig + ProviderConfig DeviceAuthProviderConfig } -// ProviderConfig has all attributes needed to initiate a device authorization flow -type ProviderConfig struct { +// DeviceAuthProviderConfig has all attributes needed to initiate a device authorization flow +type DeviceAuthProviderConfig struct { // ClientID An IDP application client id ClientID string // ClientSecret An IDP application client secret @@ -88,7 +88,7 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmU deviceAuthorizationFlow := DeviceAuthorizationFlow{ Provider: protoDeviceAuthorizationFlow.Provider.String(), - ProviderConfig: ProviderConfig{ + ProviderConfig: DeviceAuthProviderConfig{ Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(), ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(), ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(), @@ -105,7 +105,7 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmU deviceAuthorizationFlow.ProviderConfig.Scope = "openid" } - err = isProviderConfigValid(deviceAuthorizationFlow.ProviderConfig) + err = isDeviceAuthProviderConfigValid(deviceAuthorizationFlow.ProviderConfig) if err != nil { return DeviceAuthorizationFlow{}, err } @@ -113,7 +113,7 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmU return deviceAuthorizationFlow, nil } -func isProviderConfigValid(config ProviderConfig) error { +func isDeviceAuthProviderConfigValid(config DeviceAuthProviderConfig) error { errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator" if config.Audience == "" { return fmt.Errorf(errorMSGFormat, "Audience") diff --git a/client/internal/oauth.go b/client/internal/oauth.go deleted file mode 100644 index f8b8b8adb..000000000 --- a/client/internal/oauth.go +++ /dev/null @@ -1,286 +0,0 @@ -package internal - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "reflect" - "strings" - "time" -) - -// OAuthClient is a OAuth client interface for various idp providers -type OAuthClient interface { - RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) - WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error) - GetClientID(ctx context.Context) string -} - -// HTTPClient http client interface for API calls -type HTTPClient interface { - Do(req *http.Request) (*http.Response, error) -} - -// DeviceAuthInfo holds information for the OAuth device login flow -type DeviceAuthInfo struct { - DeviceCode string `json:"device_code"` - UserCode string `json:"user_code"` - VerificationURI string `json:"verification_uri"` - VerificationURIComplete string `json:"verification_uri_complete"` - ExpiresIn int `json:"expires_in"` - Interval int `json:"interval"` -} - -// HostedGrantType grant type for device flow on Hosted -const ( - HostedGrantType = "urn:ietf:params:oauth:grant-type:device_code" - HostedRefreshGrant = "refresh_token" -) - -// Hosted client -type Hosted struct { - providerConfig ProviderConfig - - HTTPClient HTTPClient -} - -// RequestDeviceCodePayload used for request device code payload for auth0 -type RequestDeviceCodePayload struct { - Audience string `json:"audience"` - ClientID string `json:"client_id"` - Scope string `json:"scope"` -} - -// TokenRequestPayload used for requesting the auth0 token -type TokenRequestPayload struct { - GrantType string `json:"grant_type"` - DeviceCode string `json:"device_code,omitempty"` - ClientID string `json:"client_id"` - RefreshToken string `json:"refresh_token,omitempty"` -} - -// TokenRequestResponse used for parsing Hosted token's response -type TokenRequestResponse struct { - Error string `json:"error"` - ErrorDescription string `json:"error_description"` - TokenInfo -} - -// Claims used when validating the access token -type Claims struct { - Audience interface{} `json:"aud"` -} - -// TokenInfo holds information of issued access token -type TokenInfo struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - IDToken string `json:"id_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - UseIDToken bool `json:"-"` -} - -// GetTokenToUse returns either the access or id token based on UseIDToken field -func (t TokenInfo) GetTokenToUse() string { - if t.UseIDToken { - return t.IDToken - } - return t.AccessToken -} - -// NewHostedDeviceFlow returns an Hosted OAuth client -func NewHostedDeviceFlow(config ProviderConfig) *Hosted { - httpTransport := http.DefaultTransport.(*http.Transport).Clone() - httpTransport.MaxIdleConns = 5 - - httpClient := &http.Client{ - Timeout: 10 * time.Second, - Transport: httpTransport, - } - - return &Hosted{ - providerConfig: config, - HTTPClient: httpClient, - } -} - -// GetClientID returns the provider client id -func (h *Hosted) GetClientID(ctx context.Context) string { - return h.providerConfig.ClientID -} - -// RequestDeviceCode requests a device code login flow information from Hosted -func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) { - form := url.Values{} - form.Add("client_id", h.providerConfig.ClientID) - form.Add("audience", h.providerConfig.Audience) - form.Add("scope", h.providerConfig.Scope) - req, err := http.NewRequest("POST", h.providerConfig.DeviceAuthEndpoint, - strings.NewReader(form.Encode())) - if err != nil { - return DeviceAuthInfo{}, fmt.Errorf("creating request failed with error: %v", err) - } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - res, err := h.HTTPClient.Do(req) - if err != nil { - return DeviceAuthInfo{}, fmt.Errorf("doing request failed with error: %v", err) - } - - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return DeviceAuthInfo{}, fmt.Errorf("reading body failed with error: %v", err) - } - - if res.StatusCode != 200 { - return DeviceAuthInfo{}, fmt.Errorf("request device code returned status %d error: %s", res.StatusCode, string(body)) - } - - deviceCode := DeviceAuthInfo{} - err = json.Unmarshal(body, &deviceCode) - if err != nil { - return DeviceAuthInfo{}, fmt.Errorf("unmarshaling response failed with error: %v", err) - } - - // Fallback to the verification_uri if the IdP doesn't support verification_uri_complete - if deviceCode.VerificationURIComplete == "" { - deviceCode.VerificationURIComplete = deviceCode.VerificationURI - } - - return deviceCode, err -} - -func (h *Hosted) requestToken(info DeviceAuthInfo) (TokenRequestResponse, error) { - form := url.Values{} - form.Add("client_id", h.providerConfig.ClientID) - form.Add("grant_type", HostedGrantType) - form.Add("device_code", info.DeviceCode) - req, err := http.NewRequest("POST", h.providerConfig.TokenEndpoint, strings.NewReader(form.Encode())) - if err != nil { - return TokenRequestResponse{}, fmt.Errorf("failed to create request access token: %v", err) - } - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - res, err := h.HTTPClient.Do(req) - if err != nil { - return TokenRequestResponse{}, fmt.Errorf("failed to request access token with error: %v", err) - } - - defer func() { - err := res.Body.Close() - if err != nil { - return - } - }() - - body, err := io.ReadAll(res.Body) - if err != nil { - return TokenRequestResponse{}, fmt.Errorf("failed reading access token response body with error: %v", err) - } - - if res.StatusCode > 499 { - return TokenRequestResponse{}, fmt.Errorf("access token response returned code: %s", string(body)) - } - - tokenResponse := TokenRequestResponse{} - err = json.Unmarshal(body, &tokenResponse) - if err != nil { - return TokenRequestResponse{}, fmt.Errorf("parsing token response failed with error: %v", err) - } - - return tokenResponse, nil -} - -// WaitToken waits user's login and authorize the app. Once the user's authorize -// it retrieves the access token from Hosted's endpoint and validates it before returning -func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error) { - interval := time.Duration(info.Interval) * time.Second - ticker := time.NewTicker(interval) - for { - select { - case <-ctx.Done(): - return TokenInfo{}, ctx.Err() - case <-ticker.C: - - tokenResponse, err := h.requestToken(info) - if err != nil { - return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err) - } - - if tokenResponse.Error != "" { - if tokenResponse.Error == "authorization_pending" { - continue - } else if tokenResponse.Error == "slow_down" { - interval = interval + (3 * time.Second) - ticker.Reset(interval) - continue - } - - return TokenInfo{}, fmt.Errorf(tokenResponse.ErrorDescription) - } - - tokenInfo := TokenInfo{ - AccessToken: tokenResponse.AccessToken, - TokenType: tokenResponse.TokenType, - RefreshToken: tokenResponse.RefreshToken, - IDToken: tokenResponse.IDToken, - ExpiresIn: tokenResponse.ExpiresIn, - UseIDToken: h.providerConfig.UseIDToken, - } - - err = isValidAccessToken(tokenInfo.GetTokenToUse(), h.providerConfig.Audience) - if err != nil { - return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err) - } - - return tokenInfo, err - } - } -} - -// isValidAccessToken is a simple validation of the access token -func isValidAccessToken(token string, audience string) error { - if token == "" { - return fmt.Errorf("token received is empty") - } - - encodedClaims := strings.Split(token, ".")[1] - claimsString, err := base64.RawURLEncoding.DecodeString(encodedClaims) - if err != nil { - return err - } - - claims := Claims{} - err = json.Unmarshal(claimsString, &claims) - if err != nil { - return err - } - - if claims.Audience == nil { - return fmt.Errorf("required token field audience is absent") - } - - // Audience claim of JWT can be a string or an array of strings - typ := reflect.TypeOf(claims.Audience) - switch typ.Kind() { - case reflect.String: - if claims.Audience == audience { - return nil - } - case reflect.Slice: - for _, aud := range claims.Audience.([]interface{}) { - if audience == aud { - return nil - } - } - } - - return fmt.Errorf("invalid JWT token audience field") -} diff --git a/client/internal/pkce_auth.go b/client/internal/pkce_auth.go new file mode 100644 index 000000000..2efbae97b --- /dev/null +++ b/client/internal/pkce_auth.go @@ -0,0 +1,128 @@ +package internal + +import ( + "context" + "fmt" + "net/url" + + log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + mgm "github.com/netbirdio/netbird/management/client" +) + +// PKCEAuthorizationFlow represents PKCE Authorization Flow information +type PKCEAuthorizationFlow struct { + ProviderConfig PKCEAuthProviderConfig +} + +// PKCEAuthProviderConfig has all attributes needed to initiate pkce authorization flow +type PKCEAuthProviderConfig struct { + // ClientID An IDP application client id + ClientID string + // ClientSecret An IDP application client secret + ClientSecret string + // Audience An Audience for to authorization validation + Audience string + // TokenEndpoint is the endpoint of an IDP manager where clients can obtain access token + TokenEndpoint string + // AuthorizationEndpoint is the endpoint of an IDP manager where clients can obtain authorization code + AuthorizationEndpoint string + // Scopes provides the scopes to be included in the token request + Scope string + // RedirectURL handles authorization code from IDP manager + RedirectURLs []string + // UseIDToken indicates if the id token should be used for authentication + UseIDToken bool +} + +// GetPKCEAuthorizationFlowInfo initialize a PKCEAuthorizationFlow instance and return with it +func GetPKCEAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL *url.URL) (PKCEAuthorizationFlow, error) { + // validate our peer's Wireguard PRIVATE key + myPrivateKey, err := wgtypes.ParseKey(privateKey) + if err != nil { + log.Errorf("failed parsing Wireguard key %s: [%s]", privateKey, err.Error()) + return PKCEAuthorizationFlow{}, err + } + + var mgmTLSEnabled bool + if mgmURL.Scheme == "https" { + mgmTLSEnabled = true + } + + log.Debugf("connecting to Management Service %s", mgmURL.String()) + mgmClient, err := mgm.NewClient(ctx, mgmURL.Host, myPrivateKey, mgmTLSEnabled) + if err != nil { + log.Errorf("failed connecting to Management Service %s %v", mgmURL.String(), err) + return PKCEAuthorizationFlow{}, err + } + log.Debugf("connected to the Management service %s", mgmURL.String()) + + defer func() { + err = mgmClient.Close() + if err != nil { + log.Warnf("failed to close the Management service client %v", err) + } + }() + + serverKey, err := mgmClient.GetServerPublicKey() + if err != nil { + log.Errorf("failed while getting Management Service public key: %v", err) + return PKCEAuthorizationFlow{}, err + } + + protoPKCEAuthorizationFlow, err := mgmClient.GetPKCEAuthorizationFlow(*serverKey) + if err != nil { + if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { + log.Warnf("server couldn't find pkce flow, contact admin: %v", err) + return PKCEAuthorizationFlow{}, err + } + log.Errorf("failed to retrieve pkce flow: %v", err) + return PKCEAuthorizationFlow{}, err + } + + authFlow := PKCEAuthorizationFlow{ + ProviderConfig: PKCEAuthProviderConfig{ + Audience: protoPKCEAuthorizationFlow.GetProviderConfig().GetAudience(), + ClientID: protoPKCEAuthorizationFlow.GetProviderConfig().GetClientID(), + ClientSecret: protoPKCEAuthorizationFlow.GetProviderConfig().GetClientSecret(), + TokenEndpoint: protoPKCEAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(), + AuthorizationEndpoint: protoPKCEAuthorizationFlow.GetProviderConfig().GetAuthorizationEndpoint(), + Scope: protoPKCEAuthorizationFlow.GetProviderConfig().GetScope(), + RedirectURLs: protoPKCEAuthorizationFlow.GetProviderConfig().GetRedirectURLs(), + UseIDToken: protoPKCEAuthorizationFlow.GetProviderConfig().GetUseIDToken(), + }, + } + + err = isPKCEProviderConfigValid(authFlow.ProviderConfig) + if err != nil { + return PKCEAuthorizationFlow{}, err + } + + return authFlow, nil +} + +func isPKCEProviderConfigValid(config PKCEAuthProviderConfig) error { + errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator" + if config.Audience == "" { + return fmt.Errorf(errorMSGFormat, "Audience") + } + if config.ClientID == "" { + return fmt.Errorf(errorMSGFormat, "Client ID") + } + if config.TokenEndpoint == "" { + return fmt.Errorf(errorMSGFormat, "Token Endpoint") + } + if config.AuthorizationEndpoint == "" { + return fmt.Errorf(errorMSGFormat, "Authorization Auth Endpoint") + } + if config.Scope == "" { + return fmt.Errorf(errorMSGFormat, "PKCE Auth Scopes") + } + if config.RedirectURLs == nil { + return fmt.Errorf(errorMSGFormat, "PKCE Redirect URLs") + } + return nil +} diff --git a/client/server/server.go b/client/server/server.go index 4633260b2..b7cca947f 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -3,6 +3,7 @@ package server import ( "context" "fmt" + "github.com/netbirdio/netbird/client/internal/auth" "sync" "time" @@ -38,8 +39,8 @@ type Server struct { type oauthAuthFlow struct { expiresAt time.Time - client internal.OAuthClient - info internal.DeviceAuthInfo + flow auth.OAuthFlow + info auth.AuthFlowInfo waitCancel context.CancelFunc } @@ -206,28 +207,15 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro state.Set(internal.StatusConnecting) if msg.SetupKey == "" { - providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL) + oAuthFlow, err := auth.NewOAuthFlow(ctx, config) if err != nil { state.Set(internal.StatusLoginFailed) - s, ok := gstatus.FromError(err) - if ok && s.Code() == codes.NotFound { - return nil, gstatus.Errorf(codes.NotFound, "no SSO provider returned from management. "+ - "If you are using hosting Netbird see documentation at "+ - "https://github.com/netbirdio/netbird/tree/main/management for details") - } else if ok && s.Code() == codes.Unimplemented { - return nil, gstatus.Errorf(codes.Unimplemented, "the management server, %s, does not support SSO providers, "+ - "please update your server or use Setup Keys to login", config.ManagementURL) - } else { - log.Errorf("getting device authorization flow info failed with error: %v", err) - return nil, err - } + return nil, err } - hostedClient := internal.NewHostedDeviceFlow(providerConfig.ProviderConfig) - - if s.oauthAuthFlow.client != nil && s.oauthAuthFlow.client.GetClientID(ctx) == hostedClient.GetClientID(context.TODO()) { + if s.oauthAuthFlow.flow != nil && s.oauthAuthFlow.flow.GetClientID(ctx) == oAuthFlow.GetClientID(context.TODO()) { if s.oauthAuthFlow.expiresAt.After(time.Now().Add(90 * time.Second)) { - log.Debugf("using previous device flow info") + log.Debugf("using previous oauth flow info") return &proto.LoginResponse{ NeedsSSOLogin: true, VerificationURI: s.oauthAuthFlow.info.VerificationURI, @@ -242,25 +230,25 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro } } - deviceAuthInfo, err := hostedClient.RequestDeviceCode(context.TODO()) + authInfo, err := oAuthFlow.RequestAuthInfo(context.TODO()) if err != nil { - log.Errorf("getting a request device code failed: %v", err) + log.Errorf("getting a request OAuth flow failed: %v", err) return nil, err } s.mutex.Lock() - s.oauthAuthFlow.client = hostedClient - s.oauthAuthFlow.info = deviceAuthInfo - s.oauthAuthFlow.expiresAt = time.Now().Add(time.Duration(deviceAuthInfo.ExpiresIn) * time.Second) + s.oauthAuthFlow.flow = oAuthFlow + s.oauthAuthFlow.info = authInfo + s.oauthAuthFlow.expiresAt = time.Now().Add(time.Duration(authInfo.ExpiresIn) * time.Second) s.mutex.Unlock() state.Set(internal.StatusNeedsLogin) return &proto.LoginResponse{ NeedsSSOLogin: true, - VerificationURI: deviceAuthInfo.VerificationURI, - VerificationURIComplete: deviceAuthInfo.VerificationURIComplete, - UserCode: deviceAuthInfo.UserCode, + VerificationURI: authInfo.VerificationURI, + VerificationURIComplete: authInfo.VerificationURIComplete, + UserCode: authInfo.UserCode, }, nil } @@ -289,8 +277,8 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin s.actCancel = cancel s.mutex.Unlock() - if s.oauthAuthFlow.client == nil { - return nil, gstatus.Errorf(codes.Internal, "oauth client is not initialized") + if s.oauthAuthFlow.flow == nil { + return nil, gstatus.Errorf(codes.Internal, "oauth flow is not initialized") } state := internal.CtxGetState(ctx) @@ -304,10 +292,10 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin state.Set(internal.StatusConnecting) s.mutex.Lock() - deviceAuthInfo := s.oauthAuthFlow.info + flowInfo := s.oauthAuthFlow.info s.mutex.Unlock() - if deviceAuthInfo.UserCode != msg.UserCode { + if flowInfo.UserCode != msg.UserCode { state.Set(internal.StatusLoginFailed) return nil, gstatus.Errorf(codes.InvalidArgument, "sso user code is invalid") } @@ -324,7 +312,7 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin s.oauthAuthFlow.waitCancel = cancel s.mutex.Unlock() - tokenInfo, err := s.oauthAuthFlow.client.WaitToken(waitCTX, deviceAuthInfo) + tokenInfo, err := s.oauthAuthFlow.flow.WaitToken(waitCTX, flowInfo) if err != nil { if err == context.Canceled { return nil, nil diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index 6fc85d63d..4bcec128d 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -43,6 +43,10 @@ NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN=${NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN:-f NETBIRD_DISABLE_ANONYMOUS_METRICS=${NETBIRD_DISABLE_ANONYMOUS_METRICS:-false} NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken} +# PKCE authorization flow +NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"} +NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false} + # exports export NETBIRD_DOMAIN export NETBIRD_AUTH_CLIENT_ID @@ -80,4 +84,6 @@ export NETBIRD_AUTH_USER_ID_CLAIM export NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE export NETBIRD_TOKEN_SOURCE export NETBIRD_AUTH_DEVICE_AUTH_SCOPE -export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN \ No newline at end of file +export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN +export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT +export NETBIRD_AUTH_PKCE_USE_ID_TOKEN \ No newline at end of file diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh index 6b083e29a..477528696 100755 --- a/infrastructure_files/configure.sh +++ b/infrastructure_files/configure.sh @@ -99,12 +99,17 @@ export NETBIRD_AUTH_AUTHORITY=$(jq -r '.issuer' openid-configuration.json) export NETBIRD_AUTH_JWT_CERTS=$(jq -r '.jwks_uri' openid-configuration.json) export NETBIRD_AUTH_TOKEN_ENDPOINT=$(jq -r '.token_endpoint' openid-configuration.json) export NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT=$(jq -r '.device_authorization_endpoint' openid-configuration.json) +export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT=$(jq -r '.authorization_endpoint' openid-configuration.json) if [[ ! -z "${NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID}" ]]; then # user enabled Device Authorization Grant feature export NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="hosted" fi +if [ "$NETBIRD_TOKEN_SOURCE" = "idToken" ]; then + export NETBIRD_AUTH_PKCE_USE_ID_TOKEN=true +fi + # Check if letsencrypt was disabled if [[ "$NETBIRD_DISABLE_LETSENCRYPT" == "true" ]]; then export NETBIRD_DASHBOARD_ENDPOINT="https://$NETBIRD_DOMAIN:443" @@ -151,6 +156,14 @@ if [ -n "$NETBIRD_MGMT_IDP" ]; then export NETBIRD_IDP_MGMT_EXTRA_CONFIG=$EXTRA_CONFIG fi +IFS=',' read -r -a REDIRECT_URL_PORTS <<< "$NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS" +REDIRECT_URLS="" +for port in "${REDIRECT_URL_PORTS[@]}"; do + REDIRECT_URLS+="\"http://localhost:${port}\"," +done + +export NETBIRD_AUTH_PKCE_REDIRECT_URLS=${REDIRECT_URLS%,} + env | grep NETBIRD envsubst docker-compose.yml diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index c2c93a665..e74b93b32 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -59,5 +59,17 @@ "Scope": "$NETBIRD_AUTH_DEVICE_AUTH_SCOPE", "UseIDToken": $NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN } + }, + "PKCEAuthorizationFlow": { + "ProviderConfig": { + "Audience": "$NETBIRD_AUTH_AUDIENCE", + "ClientID": "$NETBIRD_AUTH_CLIENT_ID", + "ClientSecret": "$NETBIRD_AUTH_CLIENT_SECRET", + "AuthorizationEndpoint": "$NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT", + "TokenEndpoint": "$NETBIRD_AUTH_TOKEN_ENDPOINT", + "Scope": "$NETBIRD_AUTH_SUPPORTED_SCOPES", + "RedirectURLs": [$NETBIRD_AUTH_PKCE_REDIRECT_URLS], + "UseIDToken": $NETBIRD_AUTH_PKCE_USE_ID_TOKEN + } } } diff --git a/infrastructure_files/setup.env.example b/infrastructure_files/setup.env.example index 32523e8d0..9b03ccd2d 100644 --- a/infrastructure_files/setup.env.example +++ b/infrastructure_files/setup.env.example @@ -37,6 +37,12 @@ NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE NETBIRD_AUTH_DEVICE_AUTH_SCOPE="openid" NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN=false # ------------------------------------------- +# OIDC PKCE Authorization Flow +# ------------------------------------------- +# Comma separated port numbers. if already in use, PKCE flow will choose an available port from the list as an alternative +# eg. 53000,54000 +NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS="53000" +# ------------------------------------------- # IDP Management # ------------------------------------------- # eg. zitadel, auth0, azure, keycloak diff --git a/management/client/client.go b/management/client/client.go index d2022f806..db7bd239b 100644 --- a/management/client/client.go +++ b/management/client/client.go @@ -15,5 +15,6 @@ type Client interface { Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error) Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) + GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) GetNetworkMap() (*proto.NetworkMap, error) } diff --git a/management/client/client_test.go b/management/client/client_test.go index 41c5a7257..d3d99dc85 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -400,3 +400,49 @@ func Test_GetDeviceAuthorizationFlow(t *testing.T) { assert.Equal(t, expectedFlowInfo.Provider, flowInfo.Provider, "provider should match") assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientID, flowInfo.ProviderConfig.ClientID, "provider configured client ID should match") } + +func Test_GetPKCEAuthorizationFlow(t *testing.T) { + s, lis, mgmtMockServer, serverKey := startMockManagement(t) + defer s.GracefulStop() + + testKey, err := wgtypes.GenerateKey() + if err != nil { + log.Fatal(err) + } + + serverAddr := lis.Addr().String() + ctx := context.Background() + + client, err := NewClient(ctx, serverAddr, testKey, false) + if err != nil { + log.Fatalf("error while creating testClient: %v", err) + } + + expectedFlowInfo := &proto.PKCEAuthorizationFlow{ + ProviderConfig: &proto.ProviderConfig{ + ClientID: "client", + ClientSecret: "secret", + }, + } + + mgmtMockServer.GetPKCEAuthorizationFlowFunc = func(ctx context.Context, req *mgmtProto.EncryptedMessage) (*proto.EncryptedMessage, error) { + encryptedResp, err := encryption.EncryptMessage(serverKey, client.key, expectedFlowInfo) + if err != nil { + return nil, err + } + + return &mgmtProto.EncryptedMessage{ + WgPubKey: serverKey.PublicKey().String(), + Body: encryptedResp, + Version: 0, + }, nil + } + + flowInfo, err := client.GetPKCEAuthorizationFlow(serverKey) + if err != nil { + t.Error("error while retrieving pkce auth flow information") + } + + assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientID, flowInfo.ProviderConfig.ClientID, "provider configured client ID should match") + assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientSecret, flowInfo.ProviderConfig.ClientSecret, "provider configured client secret should match") +} diff --git a/management/client/grpc.go b/management/client/grpc.go index d2ca8c088..e4caed4b0 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -366,6 +366,40 @@ func (c *GrpcClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.D return flowInfoResp, nil } +// GetPKCEAuthorizationFlow returns a pkce authorization flow information. +// It also takes care of encrypting and decrypting messages. +func (c *GrpcClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) { + if !c.ready() { + return nil, fmt.Errorf("no connection to management in order to get pkce authorization flow") + } + mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*2) + defer cancel() + + message := &proto.PKCEAuthorizationFlowRequest{} + encryptedMSG, err := encryption.EncryptMessage(serverKey, c.key, message) + if err != nil { + return nil, err + } + + resp, err := c.realClient.GetPKCEAuthorizationFlow(mgmCtx, &proto.EncryptedMessage{ + WgPubKey: c.key.PublicKey().String(), + Body: encryptedMSG, + }) + if err != nil { + return nil, err + } + + flowInfoResp := &proto.PKCEAuthorizationFlow{} + err = encryption.DecryptMessage(serverKey, c.key, resp.Body, flowInfoResp) + if err != nil { + errWithMSG := fmt.Errorf("failed to decrypt pkce authorization flow message: %s", err) + log.Error(errWithMSG) + return nil, errWithMSG + } + + return flowInfoResp, nil +} + func (c *GrpcClient) notifyDisconnected() { c.connStateCallbackLock.RLock() defer c.connStateCallbackLock.RUnlock() diff --git a/management/client/mock.go b/management/client/mock.go index ccad538c1..3f2e13cc7 100644 --- a/management/client/mock.go +++ b/management/client/mock.go @@ -13,6 +13,7 @@ type MockClient struct { RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) + GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) } func (m *MockClient) Close() error { @@ -57,6 +58,13 @@ func (m *MockClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.D return m.GetDeviceAuthorizationFlowFunc(serverKey) } +func (m *MockClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) { + if m.GetPKCEAuthorizationFlowFunc == nil { + return nil, nil + } + return m.GetPKCEAuthorizationFlow(serverKey) +} + // GetNetworkMap mock implementation of GetNetworkMap from mgm.Client interface func (m *MockClient) GetNetworkMap() (*proto.NetworkMap, error) { return nil, nil diff --git a/management/cmd/management.go b/management/cmd/management.go index 842dcbaa2..7cbc2bb96 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -426,6 +426,15 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) { config.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope } } + + if config.PKCEAuthorizationFlow != nil { + log.Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s", + oidcConfig.TokenEndpoint, config.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint) + config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint + log.Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s", + oidcConfig.AuthorizationEndpoint, config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint) + config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint + } } return config, err @@ -433,10 +442,11 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) { // OIDCConfigResponse used for parsing OIDC config response type OIDCConfigResponse struct { - Issuer string `json:"issuer"` - TokenEndpoint string `json:"token_endpoint"` - DeviceAuthEndpoint string `json:"device_authorization_endpoint"` - JwksURI string `json:"jwks_uri"` + Issuer string `json:"issuer"` + TokenEndpoint string `json:"token_endpoint"` + DeviceAuthEndpoint string `json:"device_authorization_endpoint"` + JwksURI string `json:"jwks_uri"` + AuthorizationEndpoint string `json:"authorization_endpoint"` } // fetchOIDCConfig fetches OIDC configuration from the IDP diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 250e3db05..eb80f9299 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.9 +// protoc v3.21.12 // source: management.proto package proto @@ -162,7 +162,7 @@ func (x FirewallRuleDirection) Number() protoreflect.EnumNumber { // Deprecated: Use FirewallRuleDirection.Descriptor instead. func (FirewallRuleDirection) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{25, 0} + return file_management_proto_rawDescGZIP(), []int{27, 0} } type FirewallRuleAction int32 @@ -208,7 +208,7 @@ func (x FirewallRuleAction) Number() protoreflect.EnumNumber { // Deprecated: Use FirewallRuleAction.Descriptor instead. func (FirewallRuleAction) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{25, 1} + return file_management_proto_rawDescGZIP(), []int{27, 1} } type FirewallRuleProtocol int32 @@ -263,7 +263,7 @@ func (x FirewallRuleProtocol) Number() protoreflect.EnumNumber { // Deprecated: Use FirewallRuleProtocol.Descriptor instead. func (FirewallRuleProtocol) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{25, 2} + return file_management_proto_rawDescGZIP(), []int{27, 2} } type EncryptedMessage struct { @@ -1477,7 +1477,96 @@ func (x *DeviceAuthorizationFlow) GetProviderConfig() *ProviderConfig { return nil } -// ProviderConfig has all attributes needed to initiate a device authorization flow +// PKCEAuthorizationFlowRequest empty struct for future expansion +type PKCEAuthorizationFlowRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *PKCEAuthorizationFlowRequest) Reset() { + *x = PKCEAuthorizationFlowRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PKCEAuthorizationFlowRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PKCEAuthorizationFlowRequest) ProtoMessage() {} + +func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PKCEAuthorizationFlowRequest.ProtoReflect.Descriptor instead. +func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{18} +} + +// PKCEAuthorizationFlow represents Authorization Code Flow information +// that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow +// with Proof Key for Code Exchange (PKCE). See https://datatracker.ietf.org/doc/html/rfc7636 +type PKCEAuthorizationFlow struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProviderConfig *ProviderConfig `protobuf:"bytes,1,opt,name=ProviderConfig,proto3" json:"ProviderConfig,omitempty"` +} + +func (x *PKCEAuthorizationFlow) Reset() { + *x = PKCEAuthorizationFlow{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PKCEAuthorizationFlow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PKCEAuthorizationFlow) ProtoMessage() {} + +func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PKCEAuthorizationFlow.ProtoReflect.Descriptor instead. +func (*PKCEAuthorizationFlow) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{19} +} + +func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig { + if x != nil { + return x.ProviderConfig + } + return nil +} + +// ProviderConfig has all attributes needed to initiate a device/pkce authorization flow type ProviderConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1500,12 +1589,16 @@ type ProviderConfig struct { Scope string `protobuf:"bytes,7,opt,name=Scope,proto3" json:"Scope,omitempty"` // UseIDToken indicates if the id token should be used for authentication UseIDToken bool `protobuf:"varint,8,opt,name=UseIDToken,proto3" json:"UseIDToken,omitempty"` + // AuthorizationEndpoint is the endpoint of an IDP manager where clients can obtain authorization code. + AuthorizationEndpoint string `protobuf:"bytes,9,opt,name=AuthorizationEndpoint,proto3" json:"AuthorizationEndpoint,omitempty"` + // RedirectURLs handles authorization code from IDP manager + RedirectURLs []string `protobuf:"bytes,10,rep,name=RedirectURLs,proto3" json:"RedirectURLs,omitempty"` } func (x *ProviderConfig) Reset() { *x = ProviderConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1518,7 +1611,7 @@ func (x *ProviderConfig) String() string { func (*ProviderConfig) ProtoMessage() {} func (x *ProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1531,7 +1624,7 @@ func (x *ProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderConfig.ProtoReflect.Descriptor instead. func (*ProviderConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{18} + return file_management_proto_rawDescGZIP(), []int{20} } func (x *ProviderConfig) GetClientID() string { @@ -1590,6 +1683,20 @@ func (x *ProviderConfig) GetUseIDToken() bool { return false } +func (x *ProviderConfig) GetAuthorizationEndpoint() string { + if x != nil { + return x.AuthorizationEndpoint + } + return "" +} + +func (x *ProviderConfig) GetRedirectURLs() []string { + if x != nil { + return x.RedirectURLs + } + return nil +} + // Route represents a route.Route object type Route struct { state protoimpl.MessageState @@ -1608,7 +1715,7 @@ type Route struct { func (x *Route) Reset() { *x = Route{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1621,7 +1728,7 @@ func (x *Route) String() string { func (*Route) ProtoMessage() {} func (x *Route) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1634,7 +1741,7 @@ func (x *Route) ProtoReflect() protoreflect.Message { // Deprecated: Use Route.ProtoReflect.Descriptor instead. func (*Route) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{19} + return file_management_proto_rawDescGZIP(), []int{21} } func (x *Route) GetID() string { @@ -1700,7 +1807,7 @@ type DNSConfig struct { func (x *DNSConfig) Reset() { *x = DNSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1713,7 +1820,7 @@ func (x *DNSConfig) String() string { func (*DNSConfig) ProtoMessage() {} func (x *DNSConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1726,7 +1833,7 @@ func (x *DNSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DNSConfig.ProtoReflect.Descriptor instead. func (*DNSConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{20} + return file_management_proto_rawDescGZIP(), []int{22} } func (x *DNSConfig) GetServiceEnable() bool { @@ -1763,7 +1870,7 @@ type CustomZone struct { func (x *CustomZone) Reset() { *x = CustomZone{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1776,7 +1883,7 @@ func (x *CustomZone) String() string { func (*CustomZone) ProtoMessage() {} func (x *CustomZone) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1789,7 +1896,7 @@ func (x *CustomZone) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomZone.ProtoReflect.Descriptor instead. func (*CustomZone) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{21} + return file_management_proto_rawDescGZIP(), []int{23} } func (x *CustomZone) GetDomain() string { @@ -1822,7 +1929,7 @@ type SimpleRecord struct { func (x *SimpleRecord) Reset() { *x = SimpleRecord{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1835,7 +1942,7 @@ func (x *SimpleRecord) String() string { func (*SimpleRecord) ProtoMessage() {} func (x *SimpleRecord) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1848,7 +1955,7 @@ func (x *SimpleRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use SimpleRecord.ProtoReflect.Descriptor instead. func (*SimpleRecord) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{22} + return file_management_proto_rawDescGZIP(), []int{24} } func (x *SimpleRecord) GetName() string { @@ -1900,7 +2007,7 @@ type NameServerGroup struct { func (x *NameServerGroup) Reset() { *x = NameServerGroup{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1913,7 +2020,7 @@ func (x *NameServerGroup) String() string { func (*NameServerGroup) ProtoMessage() {} func (x *NameServerGroup) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1926,7 +2033,7 @@ func (x *NameServerGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServerGroup.ProtoReflect.Descriptor instead. func (*NameServerGroup) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{23} + return file_management_proto_rawDescGZIP(), []int{25} } func (x *NameServerGroup) GetNameServers() []*NameServer { @@ -1964,7 +2071,7 @@ type NameServer struct { func (x *NameServer) Reset() { *x = NameServer{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1977,7 +2084,7 @@ func (x *NameServer) String() string { func (*NameServer) ProtoMessage() {} func (x *NameServer) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1990,7 +2097,7 @@ func (x *NameServer) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServer.ProtoReflect.Descriptor instead. func (*NameServer) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{24} + return file_management_proto_rawDescGZIP(), []int{26} } func (x *NameServer) GetIP() string { @@ -2030,7 +2137,7 @@ type FirewallRule struct { func (x *FirewallRule) Reset() { *x = FirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2043,7 +2150,7 @@ func (x *FirewallRule) String() string { func (*FirewallRule) ProtoMessage() {} func (x *FirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2056,7 +2163,7 @@ func (x *FirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead. func (*FirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{25} + return file_management_proto_rawDescGZIP(), []int{27} } func (x *FirewallRule) GetPeerIP() string { @@ -2270,121 +2377,140 @@ var file_management_proto_rawDesc = []byte{ 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, - 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x90, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, - 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, - 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, - 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, - 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, - 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, - 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, - 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, - 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, - 0x44, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, - 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, - 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, - 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, - 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, - 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, - 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, - 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, - 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0x7f, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, - 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, - 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, - 0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, - 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, - 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, - 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, - 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, - 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, - 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, - 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, - 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, - 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x32, 0xf7, 0x02, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, - 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, - 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, + 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, + 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, + 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, + 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, + 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, + 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, + 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, + 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, + 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, + 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, + 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, + 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, + 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, + 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, + 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, + 0x7f, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, + 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, + 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, + 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, + 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, + 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, + 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, + 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, + 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, + 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, + 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, + 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, + 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, + 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, + 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, + 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, + 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22, + 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, + 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, + 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x32, 0xd1, 0x03, + 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, + 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, - 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, + 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, + 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, + 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, + 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -2400,7 +2526,7 @@ func file_management_proto_rawDescGZIP() []byte { } var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 28) var file_management_proto_goTypes = []interface{}{ (HostConfig_Protocol)(0), // 0: management.HostConfig.Protocol (DeviceAuthorizationFlowProvider)(0), // 1: management.DeviceAuthorizationFlow.provider @@ -2425,15 +2551,17 @@ var file_management_proto_goTypes = []interface{}{ (*SSHConfig)(nil), // 20: management.SSHConfig (*DeviceAuthorizationFlowRequest)(nil), // 21: management.DeviceAuthorizationFlowRequest (*DeviceAuthorizationFlow)(nil), // 22: management.DeviceAuthorizationFlow - (*ProviderConfig)(nil), // 23: management.ProviderConfig - (*Route)(nil), // 24: management.Route - (*DNSConfig)(nil), // 25: management.DNSConfig - (*CustomZone)(nil), // 26: management.CustomZone - (*SimpleRecord)(nil), // 27: management.SimpleRecord - (*NameServerGroup)(nil), // 28: management.NameServerGroup - (*NameServer)(nil), // 29: management.NameServer - (*FirewallRule)(nil), // 30: management.FirewallRule - (*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp + (*PKCEAuthorizationFlowRequest)(nil), // 23: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 24: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 25: management.ProviderConfig + (*Route)(nil), // 26: management.Route + (*DNSConfig)(nil), // 27: management.DNSConfig + (*CustomZone)(nil), // 28: management.CustomZone + (*SimpleRecord)(nil), // 29: management.SimpleRecord + (*NameServerGroup)(nil), // 30: management.NameServerGroup + (*NameServer)(nil), // 31: management.NameServer + (*FirewallRule)(nil), // 32: management.FirewallRule + (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp } var file_management_proto_depIdxs = []int32{ 14, // 0: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig @@ -2444,7 +2572,7 @@ var file_management_proto_depIdxs = []int32{ 9, // 5: management.LoginRequest.peerKeys:type_name -> management.PeerKeys 14, // 6: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig 17, // 7: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 31, // 8: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 33, // 8: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp 15, // 9: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig 16, // 10: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig 15, // 11: management.WiretrusteeConfig.signal:type_name -> management.HostConfig @@ -2453,35 +2581,38 @@ var file_management_proto_depIdxs = []int32{ 20, // 14: management.PeerConfig.sshConfig:type_name -> management.SSHConfig 17, // 15: management.NetworkMap.peerConfig:type_name -> management.PeerConfig 19, // 16: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 24, // 17: management.NetworkMap.Routes:type_name -> management.Route - 25, // 18: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 26, // 17: management.NetworkMap.Routes:type_name -> management.Route + 27, // 18: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig 19, // 19: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 30, // 20: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 32, // 20: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule 20, // 21: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig 1, // 22: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 23, // 23: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 28, // 24: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 26, // 25: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 27, // 26: management.CustomZone.Records:type_name -> management.SimpleRecord - 29, // 27: management.NameServerGroup.NameServers:type_name -> management.NameServer - 2, // 28: management.FirewallRule.Direction:type_name -> management.FirewallRule.direction - 3, // 29: management.FirewallRule.Action:type_name -> management.FirewallRule.action - 4, // 30: management.FirewallRule.Protocol:type_name -> management.FirewallRule.protocol - 5, // 31: management.ManagementService.Login:input_type -> management.EncryptedMessage - 5, // 32: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 13, // 33: management.ManagementService.GetServerKey:input_type -> management.Empty - 13, // 34: management.ManagementService.isHealthy:input_type -> management.Empty - 5, // 35: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 36: management.ManagementService.Login:output_type -> management.EncryptedMessage - 5, // 37: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 12, // 38: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 13, // 39: management.ManagementService.isHealthy:output_type -> management.Empty - 5, // 40: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 36, // [36:41] is the sub-list for method output_type - 31, // [31:36] is the sub-list for method input_type - 31, // [31:31] is the sub-list for extension type_name - 31, // [31:31] is the sub-list for extension extendee - 0, // [0:31] is the sub-list for field type_name + 25, // 23: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 25, // 24: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 30, // 25: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 28, // 26: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 29, // 27: management.CustomZone.Records:type_name -> management.SimpleRecord + 31, // 28: management.NameServerGroup.NameServers:type_name -> management.NameServer + 2, // 29: management.FirewallRule.Direction:type_name -> management.FirewallRule.direction + 3, // 30: management.FirewallRule.Action:type_name -> management.FirewallRule.action + 4, // 31: management.FirewallRule.Protocol:type_name -> management.FirewallRule.protocol + 5, // 32: management.ManagementService.Login:input_type -> management.EncryptedMessage + 5, // 33: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 13, // 34: management.ManagementService.GetServerKey:input_type -> management.Empty + 13, // 35: management.ManagementService.isHealthy:input_type -> management.Empty + 5, // 36: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 37: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 38: management.ManagementService.Login:output_type -> management.EncryptedMessage + 5, // 39: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 12, // 40: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 13, // 41: management.ManagementService.isHealthy:output_type -> management.Empty + 5, // 42: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 5, // 43: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 38, // [38:44] is the sub-list for method output_type + 32, // [32:38] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -2707,7 +2838,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProviderConfig); i { + switch v := v.(*PKCEAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -2719,7 +2850,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Route); i { + switch v := v.(*PKCEAuthorizationFlow); i { case 0: return &v.state case 1: @@ -2731,7 +2862,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DNSConfig); i { + switch v := v.(*ProviderConfig); i { case 0: return &v.state case 1: @@ -2743,7 +2874,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomZone); i { + switch v := v.(*Route); i { case 0: return &v.state case 1: @@ -2755,7 +2886,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SimpleRecord); i { + switch v := v.(*DNSConfig); i { case 0: return &v.state case 1: @@ -2767,7 +2898,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServerGroup); i { + switch v := v.(*CustomZone); i { case 0: return &v.state case 1: @@ -2779,7 +2910,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServer); i { + switch v := v.(*SimpleRecord); i { case 0: return &v.state case 1: @@ -2791,6 +2922,30 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NameServerGroup); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NameServer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FirewallRule); i { case 0: return &v.state @@ -2809,7 +2964,7 @@ func file_management_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, NumEnums: 5, - NumMessages: 26, + NumMessages: 28, NumExtensions: 0, NumServices: 1, }, diff --git a/management/proto/management.proto b/management/proto/management.proto index e1c0fee37..d5b925d73 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -31,6 +31,13 @@ service ManagementService { // EncryptedMessage of the request has a body of DeviceAuthorizationFlowRequest. // EncryptedMessage of the response has a body of DeviceAuthorizationFlow. rpc GetDeviceAuthorizationFlow(EncryptedMessage) returns (EncryptedMessage) {} + + // Exposes a PKCE authorization code flow information + // This is used for initiating a Oauth 2 authorization grant flow + // with Proof Key for Code Exchange (PKCE) which will be used by our clients to Login. + // EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest. + // EncryptedMessage of the response has a body of PKCEAuthorizationFlow. + rpc GetPKCEAuthorizationFlow(EncryptedMessage) returns (EncryptedMessage) {} } message EncryptedMessage { @@ -237,7 +244,17 @@ message DeviceAuthorizationFlow { } } -// ProviderConfig has all attributes needed to initiate a device authorization flow +// PKCEAuthorizationFlowRequest empty struct for future expansion +message PKCEAuthorizationFlowRequest {} + +// PKCEAuthorizationFlow represents Authorization Code Flow information +// that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow +// with Proof Key for Code Exchange (PKCE). See https://datatracker.ietf.org/doc/html/rfc7636 +message PKCEAuthorizationFlow { + ProviderConfig ProviderConfig = 1; +} + +// ProviderConfig has all attributes needed to initiate a device/pkce authorization flow message ProviderConfig { // An IDP application client id string ClientID = 1; @@ -256,6 +273,10 @@ message ProviderConfig { string Scope = 7; // UseIDToken indicates if the id token should be used for authentication bool UseIDToken = 8; + // AuthorizationEndpoint is the endpoint of an IDP manager where clients can obtain authorization code. + string AuthorizationEndpoint = 9; + // RedirectURLs handles authorization code from IDP manager + repeated string RedirectURLs = 10; } // Route represents a route.Route object diff --git a/management/proto/management_grpc.pb.go b/management/proto/management_grpc.pb.go index a7d306bb8..5e2bcd225 100644 --- a/management/proto/management_grpc.pb.go +++ b/management/proto/management_grpc.pb.go @@ -37,6 +37,12 @@ type ManagementServiceClient interface { // EncryptedMessage of the request has a body of DeviceAuthorizationFlowRequest. // EncryptedMessage of the response has a body of DeviceAuthorizationFlow. GetDeviceAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) + // Exposes a PKCE authorization code flow information + // This is used for initiating a Oauth 2 authorization grant flow + // with Proof Key for Code Exchange (PKCE) which will be used by our clients to Login. + // EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest. + // EncryptedMessage of the response has a body of PKCEAuthorizationFlow. + GetPKCEAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) } type managementServiceClient struct { @@ -115,6 +121,15 @@ func (c *managementServiceClient) GetDeviceAuthorizationFlow(ctx context.Context return out, nil } +func (c *managementServiceClient) GetPKCEAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + out := new(EncryptedMessage) + err := c.cc.Invoke(ctx, "/management.ManagementService/GetPKCEAuthorizationFlow", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ManagementServiceServer is the server API for ManagementService service. // All implementations must embed UnimplementedManagementServiceServer // for forward compatibility @@ -138,6 +153,12 @@ type ManagementServiceServer interface { // EncryptedMessage of the request has a body of DeviceAuthorizationFlowRequest. // EncryptedMessage of the response has a body of DeviceAuthorizationFlow. GetDeviceAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) + // Exposes a PKCE authorization code flow information + // This is used for initiating a Oauth 2 authorization grant flow + // with Proof Key for Code Exchange (PKCE) which will be used by our clients to Login. + // EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest. + // EncryptedMessage of the response has a body of PKCEAuthorizationFlow. + GetPKCEAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) mustEmbedUnimplementedManagementServiceServer() } @@ -160,6 +181,9 @@ func (UnimplementedManagementServiceServer) IsHealthy(context.Context, *Empty) ( func (UnimplementedManagementServiceServer) GetDeviceAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { return nil, status.Errorf(codes.Unimplemented, "method GetDeviceAuthorizationFlow not implemented") } +func (UnimplementedManagementServiceServer) GetPKCEAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPKCEAuthorizationFlow not implemented") +} func (UnimplementedManagementServiceServer) mustEmbedUnimplementedManagementServiceServer() {} // UnsafeManagementServiceServer may be embedded to opt out of forward compatibility for this service. @@ -266,6 +290,24 @@ func _ManagementService_GetDeviceAuthorizationFlow_Handler(srv interface{}, ctx return interceptor(ctx, in, info, handler) } +func _ManagementService_GetPKCEAuthorizationFlow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptedMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).GetPKCEAuthorizationFlow(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/management.ManagementService/GetPKCEAuthorizationFlow", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).GetPKCEAuthorizationFlow(ctx, req.(*EncryptedMessage)) + } + return interceptor(ctx, in, info, handler) +} + // ManagementService_ServiceDesc is the grpc.ServiceDesc for ManagementService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -289,6 +331,10 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetDeviceAuthorizationFlow", Handler: _ManagementService_GetDeviceAuthorizationFlow_Handler, }, + { + MethodName: "GetPKCEAuthorizationFlow", + Handler: _ManagementService_GetPKCEAuthorizationFlow_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/management/server/config.go b/management/server/config.go index 32a468e91..ea0143988 100644 --- a/management/server/config.go +++ b/management/server/config.go @@ -42,6 +42,8 @@ type Config struct { IdpManagerConfig *idp.Config DeviceAuthorizationFlow *DeviceAuthorizationFlow + + PKCEAuthorizationFlow *PKCEAuthorizationFlow } // GetAuthAudiences returns the audience from the http config and device authorization flow config @@ -101,7 +103,14 @@ type DeviceAuthorizationFlow struct { ProviderConfig ProviderConfig } -// ProviderConfig has all attributes needed to initiate a device authorization flow +// PKCEAuthorizationFlow represents Authorization Code Flow information +// that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow +// with Proof Key for Code Exchange (PKCE). See https://datatracker.ietf.org/doc/html/rfc7636 +type PKCEAuthorizationFlow struct { + ProviderConfig ProviderConfig +} + +// ProviderConfig has all attributes needed to initiate a device/pkce authorization flow type ProviderConfig struct { // ClientID An IDP application client id ClientID string @@ -116,10 +125,14 @@ type ProviderConfig struct { TokenEndpoint string // DeviceAuthEndpoint is the endpoint of an IDP manager where clients can obtain device authorization code DeviceAuthEndpoint string + // AuthorizationEndpoint is the endpoint of an IDP manager where clients can obtain authorization code + AuthorizationEndpoint string // Scopes provides the scopes to be included in the token request Scope string // UseIDToken indicates if the id token should be used for authentication UseIDToken bool + // RedirectURL handles authorization code from IDP manager + RedirectURLs []string } // validateURL validates input http url diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index a2a0caf5a..94cb1de9d 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -543,3 +543,49 @@ func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto. Body: encryptedResp, }, nil } + +// GetPKCEAuthorizationFlow returns a pkce authorization flow information +// This is used for initiating an Oauth 2 pkce authorization grant flow +// which will be used by our clients to Login +func (s *GRPCServer) GetPKCEAuthorizationFlow(_ context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + peerKey, err := wgtypes.ParseKey(req.GetWgPubKey()) + if err != nil { + errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetPKCEAuthorizationFlow request.", req.WgPubKey) + log.Warn(errMSG) + return nil, status.Error(codes.InvalidArgument, errMSG) + } + + err = encryption.DecryptMessage(peerKey, s.wgKey, req.Body, &proto.PKCEAuthorizationFlowRequest{}) + if err != nil { + errMSG := fmt.Sprintf("error while decrypting peer's message with Wireguard public key %s.", req.WgPubKey) + log.Warn(errMSG) + return nil, status.Error(codes.InvalidArgument, errMSG) + } + + if s.config.PKCEAuthorizationFlow == nil { + return nil, status.Error(codes.NotFound, "no pkce authorization flow information available") + } + + flowInfoResp := &proto.PKCEAuthorizationFlow{ + ProviderConfig: &proto.ProviderConfig{ + Audience: s.config.PKCEAuthorizationFlow.ProviderConfig.Audience, + ClientID: s.config.PKCEAuthorizationFlow.ProviderConfig.ClientID, + ClientSecret: s.config.PKCEAuthorizationFlow.ProviderConfig.ClientSecret, + TokenEndpoint: s.config.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint, + AuthorizationEndpoint: s.config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint, + Scope: s.config.PKCEAuthorizationFlow.ProviderConfig.Scope, + RedirectURLs: s.config.PKCEAuthorizationFlow.ProviderConfig.RedirectURLs, + UseIDToken: s.config.PKCEAuthorizationFlow.ProviderConfig.UseIDToken, + }, + } + + encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, flowInfoResp) + if err != nil { + return nil, status.Error(codes.Internal, "failed to encrypt no pkce authorization flow information") + } + + return &proto.EncryptedMessage{ + WgPubKey: s.wgKey.PublicKey().String(), + Body: encryptedResp, + }, nil +} diff --git a/management/server/mock_server/management_server_mock.go b/management/server/mock_server/management_server_mock.go index 97ae5f328..29544b53f 100644 --- a/management/server/mock_server/management_server_mock.go +++ b/management/server/mock_server/management_server_mock.go @@ -16,6 +16,7 @@ type ManagementServiceServerMock struct { GetServerKeyFunc func(context.Context, *proto.Empty) (*proto.ServerKeyResponse, error) IsHealthyFunc func(context.Context, *proto.Empty) (*proto.Empty, error) GetDeviceAuthorizationFlowFunc func(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) + GetPKCEAuthorizationFlowFunc func(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) } func (m ManagementServiceServerMock) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { @@ -52,3 +53,10 @@ func (m ManagementServiceServerMock) GetDeviceAuthorizationFlow(ctx context.Cont } return nil, status.Errorf(codes.Unimplemented, "method GetDeviceAuthorizationFlow not implemented") } + +func (m ManagementServiceServerMock) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + if m.GetPKCEAuthorizationFlowFunc != nil { + return m.GetPKCEAuthorizationFlowFunc(ctx, req) + } + return nil, status.Errorf(codes.Unimplemented, "method GetPKCEAuthorizationFlow not implemented") +} From 24713fbe59915a77ee69613dd3661e08d70f4c53 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 27 Jul 2023 15:34:27 +0200 Subject: [PATCH 26/27] Move ebpf code to its own package to avoid crash issues in Android (#1033) * Move ebpf code to its own package to avoid crash issues in Android Older versions of android crashes because of the bytecode files Even when they aren't loaded as it was our case * move c file to own folder * fix lint --- .../internal/wgproxy/{ => ebpf}/bpf_bpfeb.go | 2 +- .../internal/wgproxy/{ => ebpf}/bpf_bpfeb.o | Bin .../internal/wgproxy/{ => ebpf}/bpf_bpfel.go | 2 +- .../internal/wgproxy/{ => ebpf}/bpf_bpfel.o | Bin client/internal/wgproxy/{ => ebpf}/loader.go | 18 +++++++++++------- .../wgproxy/{ => ebpf}/loader_test.go | 8 ++++---- .../wgproxy/{bpf => ebpf/src}/portreplace.c | 0 client/internal/wgproxy/proxy_ebpf.go | 12 +++++++----- 8 files changed, 24 insertions(+), 18 deletions(-) rename client/internal/wgproxy/{ => ebpf}/bpf_bpfeb.go (99%) rename client/internal/wgproxy/{ => ebpf}/bpf_bpfeb.o (100%) rename client/internal/wgproxy/{ => ebpf}/bpf_bpfel.go (99%) rename client/internal/wgproxy/{ => ebpf}/bpf_bpfel.o (100%) rename client/internal/wgproxy/{ => ebpf}/loader.go (77%) rename client/internal/wgproxy/{ => ebpf}/loader_test.go (62%) rename client/internal/wgproxy/{bpf => ebpf/src}/portreplace.c (100%) diff --git a/client/internal/wgproxy/bpf_bpfeb.go b/client/internal/wgproxy/ebpf/bpf_bpfeb.go similarity index 99% rename from client/internal/wgproxy/bpf_bpfeb.go rename to client/internal/wgproxy/ebpf/bpf_bpfeb.go index 37a5cb22c..4943513ef 100644 --- a/client/internal/wgproxy/bpf_bpfeb.go +++ b/client/internal/wgproxy/ebpf/bpf_bpfeb.go @@ -2,7 +2,7 @@ //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 // +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64 -package wgproxy +package ebpf import ( "bytes" diff --git a/client/internal/wgproxy/bpf_bpfeb.o b/client/internal/wgproxy/ebpf/bpf_bpfeb.o similarity index 100% rename from client/internal/wgproxy/bpf_bpfeb.o rename to client/internal/wgproxy/ebpf/bpf_bpfeb.o diff --git a/client/internal/wgproxy/bpf_bpfel.go b/client/internal/wgproxy/ebpf/bpf_bpfel.go similarity index 99% rename from client/internal/wgproxy/bpf_bpfel.go rename to client/internal/wgproxy/ebpf/bpf_bpfel.go index c553b9da4..cac39aa01 100644 --- a/client/internal/wgproxy/bpf_bpfel.go +++ b/client/internal/wgproxy/ebpf/bpf_bpfel.go @@ -2,7 +2,7 @@ //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 // +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64 -package wgproxy +package ebpf import ( "bytes" diff --git a/client/internal/wgproxy/bpf_bpfel.o b/client/internal/wgproxy/ebpf/bpf_bpfel.o similarity index 100% rename from client/internal/wgproxy/bpf_bpfel.o rename to client/internal/wgproxy/ebpf/bpf_bpfel.o diff --git a/client/internal/wgproxy/loader.go b/client/internal/wgproxy/ebpf/loader.go similarity index 77% rename from client/internal/wgproxy/loader.go rename to client/internal/wgproxy/ebpf/loader.go index 5833b9c25..18134d439 100644 --- a/client/internal/wgproxy/loader.go +++ b/client/internal/wgproxy/ebpf/loader.go @@ -1,6 +1,6 @@ //go:build linux && !android -package wgproxy +package ebpf import ( _ "embed" @@ -15,17 +15,20 @@ const ( mapKeyWgPort uint32 = 1 ) -//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang-14 bpf bpf/portreplace.c -- +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang-14 bpf src/portreplace.c -- -type eBPF struct { +// EBPF is a wrapper for eBPF program +type EBPF struct { link link.Link } -func newEBPF() *eBPF { - return &eBPF{} +// NewEBPF create new EBPF instance +func NewEBPF() *EBPF { + return &EBPF{} } -func (l *eBPF) load(proxyPort, wgPort int) error { +// Load load ebpf program +func (l *EBPF) Load(proxyPort, wgPort int) error { // it required for Docker err := rlimit.RemoveMemlock() if err != nil { @@ -72,7 +75,8 @@ func (l *eBPF) load(proxyPort, wgPort int) error { return err } -func (l *eBPF) free() error { +// Free free ebpf program +func (l *EBPF) Free() error { if l.link != nil { return l.link.Close() } diff --git a/client/internal/wgproxy/loader_test.go b/client/internal/wgproxy/ebpf/loader_test.go similarity index 62% rename from client/internal/wgproxy/loader_test.go rename to client/internal/wgproxy/ebpf/loader_test.go index ad0fed236..6ce323e70 100644 --- a/client/internal/wgproxy/loader_test.go +++ b/client/internal/wgproxy/ebpf/loader_test.go @@ -1,16 +1,16 @@ //go:build linux -package wgproxy +package ebpf import ( "testing" ) func Test_newEBPF(t *testing.T) { - ebpf := newEBPF() - err := ebpf.load(1234, 51892) + ebpf := NewEBPF() + err := ebpf.Load(1234, 51892) defer func() { - _ = ebpf.free() + _ = ebpf.Free() }() if err != nil { t.Errorf("%s", err) diff --git a/client/internal/wgproxy/bpf/portreplace.c b/client/internal/wgproxy/ebpf/src/portreplace.c similarity index 100% rename from client/internal/wgproxy/bpf/portreplace.c rename to client/internal/wgproxy/ebpf/src/portreplace.c diff --git a/client/internal/wgproxy/proxy_ebpf.go b/client/internal/wgproxy/proxy_ebpf.go index 7683aa52a..dc6993ba0 100644 --- a/client/internal/wgproxy/proxy_ebpf.go +++ b/client/internal/wgproxy/proxy_ebpf.go @@ -14,11 +14,13 @@ import ( "github.com/google/gopacket/layers" log "github.com/sirupsen/logrus" + + ebpf2 "github.com/netbirdio/netbird/client/internal/wgproxy/ebpf" ) -// WGEBPFProxy definition for proxy with eBPF support +// WGEBPFProxy definition for proxy with EBPF support type WGEBPFProxy struct { - ebpf *eBPF + ebpf *ebpf2.EBPF lastUsedPort uint16 localWGListenPort int @@ -34,7 +36,7 @@ func NewWGEBPFProxy(wgPort int) *WGEBPFProxy { log.Debugf("instantiate ebpf proxy") wgProxy := &WGEBPFProxy{ localWGListenPort: wgPort, - ebpf: newEBPF(), + ebpf: ebpf2.NewEBPF(), lastUsedPort: 0, turnConnStore: make(map[uint16]net.Conn), } @@ -54,7 +56,7 @@ func (p *WGEBPFProxy) Listen() error { return err } - err = p.ebpf.load(wgPorxyPort, p.localWGListenPort) + err = p.ebpf.Load(wgPorxyPort, p.localWGListenPort) if err != nil { return err } @@ -107,7 +109,7 @@ func (p *WGEBPFProxy) Free() error { err1 = p.conn.Close() } - err2 = p.ebpf.free() + err2 = p.ebpf.Free() if p.rawConn != nil { err3 = p.rawConn.Close() } From 64f6343fcc3e153bb91d8c936297b807cda96502 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 28 Jul 2023 19:10:12 +0300 Subject: [PATCH 27/27] Add html screen for pkce flow (#1034) * add html screen for pkce flow * remove unused CSS classes in pkce-auth-msg.html * remove links to external sources --- client/internal/auth/pkce_flow.go | 61 +++++++++----- client/internal/templates/embed.go | 8 ++ client/internal/templates/pkce-auth-msg.html | 87 ++++++++++++++++++++ 3 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 client/internal/templates/embed.go create mode 100644 client/internal/templates/pkce-auth-msg.html diff --git a/client/internal/auth/pkce_flow.go b/client/internal/auth/pkce_flow.go index 9451ce055..482b137b8 100644 --- a/client/internal/auth/pkce_flow.go +++ b/client/internal/auth/pkce_flow.go @@ -6,6 +6,7 @@ import ( "crypto/subtle" "encoding/base64" "fmt" + "html/template" "net" "net/http" "net/url" @@ -16,6 +17,7 @@ import ( "golang.org/x/oauth2" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/templates" ) var _ OAuthFlow = &PKCEAuthorizationFlow{} @@ -136,33 +138,35 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC }() http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - query := req.URL.Query() + tokenValidatorFunc := func() (*oauth2.Token, error) { + query := req.URL.Query() - state := query.Get(queryState) - // Prevent timing attacks on state - if subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 { - errChan <- fmt.Errorf("invalid state") - return + state := query.Get(queryState) + // Prevent timing attacks on state + if subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 { + return nil, fmt.Errorf("invalid state") + } + + code := query.Get(queryCode) + if code == "" { + return nil, fmt.Errorf("missing code") + } + + return p.oAuthConfig.Exchange( + req.Context(), + code, + oauth2.SetAuthURLParam("code_verifier", p.codeVerifier), + ) } - code := query.Get(queryCode) - if code == "" { - errChan <- fmt.Errorf("missing code") - return - } - - // Exchange the authorization code for the OAuth token - token, err := p.oAuthConfig.Exchange( - req.Context(), - code, - oauth2.SetAuthURLParam("code_verifier", p.codeVerifier), - ) + token, err := tokenValidatorFunc() if err != nil { - errChan <- fmt.Errorf("OAuth token exchange failed: %v", err) - return + errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err) + renderPKCEFlowTmpl(w, err) } tokenChan <- token + renderPKCEFlowTmpl(w, nil) }) if err := server.ListenAndServe(); err != nil { @@ -215,3 +219,20 @@ func isRedirectURLPortUsed(redirectURL string) bool { return true } + +func renderPKCEFlowTmpl(w http.ResponseWriter, authError error) { + tmpl, err := template.New("pkce-auth-flow").Parse(templates.PKCEAuthMsgTmpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := make(map[string]string) + if authError != nil { + data["Error"] = authError.Error() + } + + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/client/internal/templates/embed.go b/client/internal/templates/embed.go new file mode 100644 index 000000000..5c0ead176 --- /dev/null +++ b/client/internal/templates/embed.go @@ -0,0 +1,8 @@ +package templates + +import ( + _ "embed" +) + +//go:embed pkce-auth-msg.html +var PKCEAuthMsgTmpl string diff --git a/client/internal/templates/pkce-auth-msg.html b/client/internal/templates/pkce-auth-msg.html new file mode 100644 index 000000000..efd1e06a3 --- /dev/null +++ b/client/internal/templates/pkce-auth-msg.html @@ -0,0 +1,87 @@ + + + + + + + +
+ +
+ {{ if .Error }} + + + + +
+
+ Login failed +
+ {{ .Error }}. +
+ {{ else }} + + + + +
+
+ Login successful +
+ Your device is now registered and logged in to NetBird. +
+ You can now close this window. +
+ {{ end }} +
+ +