mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-09 03:25:11 -04:00
Compare commits
3 Commits
v0.35.2
...
conntrack-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
572bea6a71 | ||
|
|
b8cb3c968a | ||
|
|
f57bc604a8 |
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin
|
||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testIn
|
||||
skip: go.mod,go.sum
|
||||
only_warn: 1
|
||||
golangci:
|
||||
|
||||
@@ -197,7 +197,7 @@ func (m *Manager) AllowNetbird() error {
|
||||
}
|
||||
|
||||
_, err := m.AddPeerFiltering(
|
||||
net.IP{0, 0, 0, 0},
|
||||
net.ParseIP("0.0.0.0"),
|
||||
"all",
|
||||
nil,
|
||||
nil,
|
||||
@@ -215,6 +215,11 @@ func (m *Manager) AllowNetbird() error {
|
||||
// Flush doesn't need to be implemented for this manager
|
||||
func (m *Manager) Flush() error { return nil }
|
||||
|
||||
// CollectStats returns connection tracking statistics
|
||||
func (m *Manager) CollectStats() []*firewall.FlowStats {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConntrackEstablished() []string {
|
||||
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,9 @@ type Manager interface {
|
||||
|
||||
// Flush the changes to firewall controller
|
||||
Flush() error
|
||||
|
||||
// CollectStats returns the statistics of the firewall manager
|
||||
CollectStats() []*FlowStats
|
||||
}
|
||||
|
||||
func GenKey(format string, pair RouterPair) string {
|
||||
|
||||
107
client/firewall/manager/stats.go
Normal file
107
client/firewall/manager/stats.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DirectionInbound Direction = 0
|
||||
DirectionOutbound Direction = 1
|
||||
)
|
||||
|
||||
type Direction uint8
|
||||
|
||||
func (d Direction) String() string {
|
||||
switch d {
|
||||
case DirectionInbound:
|
||||
return "inbound"
|
||||
case DirectionOutbound:
|
||||
return "outbound"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// FlowStats tracks statistics for an individual connection
|
||||
type FlowStats struct {
|
||||
StartTime time.Time
|
||||
LastSeen time.Time
|
||||
BytesIn atomic.Uint64
|
||||
BytesOut atomic.Uint64
|
||||
PacketsIn atomic.Uint64
|
||||
PacketsOut atomic.Uint64
|
||||
Protocol uint8
|
||||
Direction Direction
|
||||
SourceIP net.IP
|
||||
DestIP net.IP
|
||||
SourcePort uint16
|
||||
DestPort uint16
|
||||
}
|
||||
|
||||
func (f *FlowStats) Clone() *FlowStats {
|
||||
flowCopy := FlowStats{
|
||||
StartTime: f.StartTime,
|
||||
LastSeen: f.LastSeen,
|
||||
Protocol: f.Protocol,
|
||||
Direction: f.Direction,
|
||||
SourceIP: slices.Clone(f.SourceIP),
|
||||
DestIP: slices.Clone(f.DestIP),
|
||||
SourcePort: f.SourcePort,
|
||||
DestPort: f.DestPort,
|
||||
}
|
||||
flowCopy.BytesIn.Store(f.BytesIn.Load())
|
||||
flowCopy.BytesOut.Store(f.BytesOut.Load())
|
||||
flowCopy.PacketsIn.Store(f.PacketsIn.Load())
|
||||
flowCopy.PacketsOut.Store(f.PacketsOut.Load())
|
||||
|
||||
return &flowCopy
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler interface
|
||||
func (f *FlowStats) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
StartTime time.Time
|
||||
LastSeen time.Time
|
||||
BytesIn uint64
|
||||
BytesOut uint64
|
||||
PacketsIn uint64
|
||||
PacketsOut uint64
|
||||
Protocol Protocol
|
||||
Direction string
|
||||
SourceIP net.IP
|
||||
DestIP net.IP
|
||||
SourcePort uint16
|
||||
DestPort uint16
|
||||
}{
|
||||
StartTime: f.StartTime,
|
||||
LastSeen: f.LastSeen,
|
||||
BytesIn: f.BytesIn.Load(),
|
||||
BytesOut: f.BytesOut.Load(),
|
||||
PacketsIn: f.PacketsIn.Load(),
|
||||
PacketsOut: f.PacketsOut.Load(),
|
||||
Protocol: protoFromInt(f.Protocol),
|
||||
Direction: f.Direction.String(),
|
||||
SourceIP: f.SourceIP,
|
||||
DestIP: f.DestIP,
|
||||
SourcePort: f.SourcePort,
|
||||
DestPort: f.DestPort,
|
||||
})
|
||||
}
|
||||
|
||||
func protoFromInt(p uint8) Protocol {
|
||||
switch p {
|
||||
case 6:
|
||||
return ProtocolTCP
|
||||
case 17:
|
||||
return ProtocolUDP
|
||||
case 1:
|
||||
return ProtocolICMP
|
||||
default:
|
||||
return Protocol(strconv.Itoa(int(p)))
|
||||
}
|
||||
}
|
||||
@@ -323,6 +323,11 @@ func (m *Manager) Flush() error {
|
||||
return m.aclManager.Flush()
|
||||
}
|
||||
|
||||
// CollectStats returns connection tracking statistics
|
||||
func (m *Manager) CollectStats() []*firewall.FlowStats {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) createWorkTable() (*nftables.Table, error) {
|
||||
tables, err := m.rConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,17 +17,17 @@ func (m *Manager) Reset(stateManager *statemanager.Manager) error {
|
||||
|
||||
if m.udpTracker != nil {
|
||||
m.udpTracker.Close()
|
||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout)
|
||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, nil)
|
||||
}
|
||||
|
||||
if m.icmpTracker != nil {
|
||||
m.icmpTracker.Close()
|
||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout)
|
||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, nil)
|
||||
}
|
||||
|
||||
if m.tcpTracker != nil {
|
||||
m.tcpTracker.Close()
|
||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout)
|
||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, nil)
|
||||
}
|
||||
|
||||
if m.nativeFirewall != nil {
|
||||
|
||||
@@ -29,17 +29,17 @@ func (m *Manager) Reset(*statemanager.Manager) error {
|
||||
|
||||
if m.udpTracker != nil {
|
||||
m.udpTracker.Close()
|
||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout)
|
||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, nil)
|
||||
}
|
||||
|
||||
if m.icmpTracker != nil {
|
||||
m.icmpTracker.Close()
|
||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout)
|
||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, nil)
|
||||
}
|
||||
|
||||
if m.tcpTracker != nil {
|
||||
m.tcpTracker.Close()
|
||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout)
|
||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, nil)
|
||||
}
|
||||
|
||||
if !isWindowsFirewallReachable() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
// BaseConnTrack provides common fields and locking for all connection types
|
||||
type BaseConnTrack struct {
|
||||
sync.RWMutex
|
||||
SourceIP net.IP
|
||||
DestIP net.IP
|
||||
SourcePort uint16
|
||||
|
||||
@@ -64,7 +64,7 @@ func BenchmarkAtomicOperations(b *testing.B) {
|
||||
// Memory pressure tests
|
||||
func BenchmarkMemoryPressure(b *testing.B) {
|
||||
b.Run("TCPHighLoad", func(b *testing.B) {
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
// Generate different IPs
|
||||
@@ -79,17 +79,17 @@ func BenchmarkMemoryPressure(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
srcIdx := i % len(srcIPs)
|
||||
dstIdx := (i + 1) % len(dstIPs)
|
||||
tracker.TrackOutbound(srcIPs[srcIdx], dstIPs[dstIdx], uint16(i%65535), 80, TCPSyn)
|
||||
tracker.TrackOutbound(srcIPs[srcIdx], dstIPs[dstIdx], uint16(i%65535), 80, TCPSyn, nil)
|
||||
|
||||
// Simulate some valid inbound packets
|
||||
if i%3 == 0 {
|
||||
tracker.IsValidInbound(dstIPs[dstIdx], srcIPs[srcIdx], 80, uint16(i%65535), TCPAck)
|
||||
tracker.IsValidInbound(dstIPs[dstIdx], srcIPs[srcIdx], 80, uint16(i%65535), TCPAck, nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("UDPHighLoad", func(b *testing.B) {
|
||||
tracker := NewUDPTracker(DefaultUDPTimeout)
|
||||
tracker := NewUDPTracker(DefaultUDPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
// Generate different IPs
|
||||
@@ -104,11 +104,11 @@ func BenchmarkMemoryPressure(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
srcIdx := i % len(srcIPs)
|
||||
dstIdx := (i + 1) % len(dstIPs)
|
||||
tracker.TrackOutbound(srcIPs[srcIdx], dstIPs[dstIdx], uint16(i%65535), 80)
|
||||
tracker.TrackOutbound(srcIPs[srcIdx], dstIPs[dstIdx], uint16(i%65535), 80, nil)
|
||||
|
||||
// Simulate some valid inbound packets
|
||||
if i%3 == 0 {
|
||||
tracker.IsValidInbound(dstIPs[dstIdx], srcIPs[srcIdx], 80, uint16(i%65535))
|
||||
tracker.IsValidInbound(dstIPs[dstIdx], srcIPs[srcIdx], 80, uint16(i%65535), nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket/layers"
|
||||
|
||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,10 +41,11 @@ type ICMPTracker struct {
|
||||
mutex sync.RWMutex
|
||||
done chan struct{}
|
||||
ipPool *PreallocatedIPs
|
||||
stats *Stats
|
||||
}
|
||||
|
||||
// NewICMPTracker creates a new ICMP connection tracker
|
||||
func NewICMPTracker(timeout time.Duration) *ICMPTracker {
|
||||
func NewICMPTracker(timeout time.Duration, stats *Stats) *ICMPTracker {
|
||||
if timeout == 0 {
|
||||
timeout = DefaultICMPTimeout
|
||||
}
|
||||
@@ -53,6 +56,7 @@ func NewICMPTracker(timeout time.Duration) *ICMPTracker {
|
||||
cleanupTicker: time.NewTicker(ICMPCleanupInterval),
|
||||
done: make(chan struct{}),
|
||||
ipPool: NewPreallocatedIPs(),
|
||||
stats: stats,
|
||||
}
|
||||
|
||||
go tracker.cleanupRoutine()
|
||||
@@ -60,7 +64,7 @@ func NewICMPTracker(timeout time.Duration) *ICMPTracker {
|
||||
}
|
||||
|
||||
// TrackOutbound records an outbound ICMP Echo Request
|
||||
func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16) {
|
||||
func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16, packetData []byte) {
|
||||
key := makeICMPKey(srcIP, dstIP, id, seq)
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
@@ -83,14 +87,22 @@ func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq u
|
||||
conn.lastSeen.Store(now)
|
||||
conn.established.Store(true)
|
||||
t.connections[key] = conn
|
||||
|
||||
if t.stats != nil {
|
||||
t.stats.TrackNewConnection(1, srcIP, dstIP, 0, 0, fw.DirectionOutbound)
|
||||
}
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
|
||||
if t.stats != nil {
|
||||
key := makeConnKey(srcIP, dstIP, 0, 0)
|
||||
t.stats.TrackPacket(1, false, uint64(len(packetData)), false, key)
|
||||
}
|
||||
conn.lastSeen.Store(now)
|
||||
}
|
||||
|
||||
// IsValidInbound checks if an inbound ICMP Echo Reply matches a tracked request
|
||||
func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16, icmpType uint8) bool {
|
||||
func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16, icmpType uint8, packetData []byte) bool {
|
||||
switch icmpType {
|
||||
case uint8(layers.ICMPv4TypeDestinationUnreachable),
|
||||
uint8(layers.ICMPv4TypeTimeExceeded):
|
||||
@@ -115,6 +127,11 @@ func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq
|
||||
return false
|
||||
}
|
||||
|
||||
if t.stats != nil {
|
||||
key := makeConnKey(srcIP, dstIP, 0, 0)
|
||||
t.stats.TrackPacket(1, false, uint64(len(packetData)), true, key)
|
||||
}
|
||||
|
||||
return conn.IsEstablished() &&
|
||||
ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) &&
|
||||
ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) &&
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
func BenchmarkICMPTracker(b *testing.B) {
|
||||
b.Run("TrackOutbound", func(b *testing.B) {
|
||||
tracker := NewICMPTracker(DefaultICMPTimeout)
|
||||
tracker := NewICMPTracker(DefaultICMPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.1")
|
||||
@@ -15,12 +15,12 @@ func BenchmarkICMPTracker(b *testing.B) {
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), uint16(i%65535))
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), uint16(i%65535), nil)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("IsValidInbound", func(b *testing.B) {
|
||||
tracker := NewICMPTracker(DefaultICMPTimeout)
|
||||
tracker := NewICMPTracker(DefaultICMPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.1")
|
||||
@@ -28,12 +28,12 @@ func BenchmarkICMPTracker(b *testing.B) {
|
||||
|
||||
// Pre-populate some connections
|
||||
for i := 0; i < 1000; i++ {
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i), uint16(i))
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i), uint16(i), nil)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tracker.IsValidInbound(dstIP, srcIP, uint16(i%1000), uint16(i%1000), 0)
|
||||
tracker.IsValidInbound(dstIP, srcIP, uint16(i%1000), uint16(i%1000), 0, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
172
client/firewall/uspfilter/conntrack/stats.go
Normal file
172
client/firewall/uspfilter/conntrack/stats.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package conntrack
|
||||
|
||||
import (
|
||||
"net"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||
)
|
||||
|
||||
// Stats represents connection tracking statistics
|
||||
type Stats struct {
|
||||
TotalConnsCreated atomic.Uint64
|
||||
TotalConnsTimedOut atomic.Uint64
|
||||
TotalPacketsDropped atomic.Uint64
|
||||
ActiveConns atomic.Int64
|
||||
|
||||
TCPConns atomic.Int64
|
||||
UDPConns atomic.Int64
|
||||
ICMPConns atomic.Int64
|
||||
|
||||
TCPStateStats struct {
|
||||
SynReceived atomic.Uint64
|
||||
Established atomic.Uint64
|
||||
FinWait atomic.Uint64
|
||||
TimeWait atomic.Uint64
|
||||
InvalidStates atomic.Uint64
|
||||
}
|
||||
|
||||
PacketStats struct {
|
||||
TCPPackets atomic.Uint64
|
||||
UDPPackets atomic.Uint64
|
||||
ICMPPackets atomic.Uint64
|
||||
}
|
||||
|
||||
flowMutex sync.RWMutex
|
||||
flows map[ConnKey]*fw.FlowStats
|
||||
}
|
||||
|
||||
// NewStats creates a new Stats instance
|
||||
func NewStats() *Stats {
|
||||
return &Stats{
|
||||
flows: make(map[ConnKey]*fw.FlowStats),
|
||||
}
|
||||
}
|
||||
|
||||
// TrackNewConnection records a new connection
|
||||
func (s *Stats) TrackNewConnection(proto uint8, srcIP net.IP, dstIP net.IP, srcPort, dstPort uint16, direction fw.Direction) {
|
||||
s.TotalConnsCreated.Add(1)
|
||||
s.ActiveConns.Add(1)
|
||||
|
||||
switch proto {
|
||||
case 6: // TCP
|
||||
s.TCPConns.Add(1)
|
||||
case 17: // UDP
|
||||
s.UDPConns.Add(1)
|
||||
case 1: // ICMP
|
||||
s.ICMPConns.Add(1)
|
||||
}
|
||||
|
||||
flow := &fw.FlowStats{
|
||||
StartTime: time.Now(),
|
||||
LastSeen: time.Now(),
|
||||
Protocol: proto,
|
||||
Direction: direction,
|
||||
SourceIP: slices.Clone(srcIP),
|
||||
DestIP: slices.Clone(dstIP),
|
||||
SourcePort: srcPort,
|
||||
DestPort: dstPort,
|
||||
}
|
||||
|
||||
key := makeConnKey(srcIP, dstIP, srcPort, dstPort)
|
||||
s.flowMutex.Lock()
|
||||
s.flows[key] = flow
|
||||
s.flowMutex.Unlock()
|
||||
}
|
||||
|
||||
// TrackConnectionClosed records a connection closure
|
||||
func (s *Stats) TrackConnectionClosed(proto uint8, timedOut bool, key ConnKey) {
|
||||
s.ActiveConns.Add(-1)
|
||||
|
||||
if timedOut {
|
||||
s.TotalConnsTimedOut.Add(1)
|
||||
}
|
||||
|
||||
switch proto {
|
||||
case 6: // TCP
|
||||
s.TCPConns.Add(-1)
|
||||
case 17: // UDP
|
||||
s.UDPConns.Add(-1)
|
||||
case 1: // ICMP
|
||||
s.ICMPConns.Add(-1)
|
||||
}
|
||||
|
||||
s.flowMutex.Lock()
|
||||
delete(s.flows, key)
|
||||
s.flowMutex.Unlock()
|
||||
}
|
||||
|
||||
// TrackPacket records packet statistics
|
||||
func (s *Stats) TrackPacket(proto uint8, dropped bool, bytes uint64, isInbound bool, key ConnKey) {
|
||||
if dropped {
|
||||
s.TotalPacketsDropped.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
switch proto {
|
||||
case 6: // TCP
|
||||
s.PacketStats.TCPPackets.Add(1)
|
||||
case 17: // UDP
|
||||
s.PacketStats.UDPPackets.Add(1)
|
||||
case 1: // ICMP
|
||||
s.PacketStats.ICMPPackets.Add(1)
|
||||
}
|
||||
|
||||
s.flowMutex.RLock()
|
||||
if flow, exists := s.flows[key]; exists {
|
||||
if isInbound {
|
||||
flow.BytesIn.Add(bytes)
|
||||
flow.PacketsIn.Add(1)
|
||||
} else {
|
||||
flow.BytesOut.Add(bytes)
|
||||
flow.PacketsOut.Add(1)
|
||||
}
|
||||
flow.LastSeen = time.Now()
|
||||
}
|
||||
s.flowMutex.RUnlock()
|
||||
}
|
||||
|
||||
// TrackTCPState updates TCP state statistics
|
||||
func (s *Stats) TrackTCPState(newState TCPState) {
|
||||
switch newState {
|
||||
case TCPStateSynReceived:
|
||||
s.TCPStateStats.SynReceived.Add(1)
|
||||
case TCPStateEstablished:
|
||||
s.TCPStateStats.Established.Add(1)
|
||||
case TCPStateFinWait1, TCPStateFinWait2:
|
||||
s.TCPStateStats.FinWait.Add(1)
|
||||
case TCPStateTimeWait:
|
||||
s.TCPStateStats.TimeWait.Add(1)
|
||||
default:
|
||||
s.TCPStateStats.InvalidStates.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// GetFlowSnapshot returns a copy of current flow statistics if enabled
|
||||
func (s *Stats) GetFlowSnapshot() []*fw.FlowStats {
|
||||
s.flowMutex.RLock()
|
||||
defer s.flowMutex.RUnlock()
|
||||
|
||||
snapshot := make([]*fw.FlowStats, 0, len(s.flows))
|
||||
for _, flow := range s.flows {
|
||||
snapshot = append(snapshot, flow.Clone())
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// CleanupFlows removes flow entries older than the specified duration if enabled
|
||||
func (s *Stats) CleanupFlows(maxAge time.Duration) {
|
||||
threshold := time.Now().Add(-maxAge)
|
||||
|
||||
s.flowMutex.Lock()
|
||||
defer s.flowMutex.Unlock()
|
||||
|
||||
for key, flow := range s.flows {
|
||||
if flow.LastSeen.Before(threshold) {
|
||||
delete(s.flows, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -62,7 +64,6 @@ type TCPConnKey struct {
|
||||
type TCPConnTrack struct {
|
||||
BaseConnTrack
|
||||
State TCPState
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// TCPTracker manages TCP connection states
|
||||
@@ -73,16 +74,18 @@ type TCPTracker struct {
|
||||
done chan struct{}
|
||||
timeout time.Duration
|
||||
ipPool *PreallocatedIPs
|
||||
stats *Stats
|
||||
}
|
||||
|
||||
// NewTCPTracker creates a new TCP connection tracker
|
||||
func NewTCPTracker(timeout time.Duration) *TCPTracker {
|
||||
func NewTCPTracker(timeout time.Duration, stats *Stats) *TCPTracker {
|
||||
tracker := &TCPTracker{
|
||||
connections: make(map[ConnKey]*TCPConnTrack),
|
||||
cleanupTicker: time.NewTicker(TCPCleanupInterval),
|
||||
done: make(chan struct{}),
|
||||
timeout: timeout,
|
||||
ipPool: NewPreallocatedIPs(),
|
||||
stats: stats,
|
||||
}
|
||||
|
||||
go tracker.cleanupRoutine()
|
||||
@@ -90,15 +93,13 @@ func NewTCPTracker(timeout time.Duration) *TCPTracker {
|
||||
}
|
||||
|
||||
// TrackOutbound processes an outbound TCP packet and updates connection state
|
||||
func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8) {
|
||||
// Create key before lock
|
||||
func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8, packetData []byte) {
|
||||
key := makeConnKey(srcIP, dstIP, srcPort, dstPort)
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
t.mutex.Lock()
|
||||
conn, exists := t.connections[key]
|
||||
if !exists {
|
||||
// Use preallocated IPs
|
||||
srcIPCopy := t.ipPool.Get()
|
||||
dstIPCopy := t.ipPool.Get()
|
||||
copyIP(srcIPCopy, srcIP)
|
||||
@@ -116,24 +117,69 @@ func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d
|
||||
conn.lastSeen.Store(now)
|
||||
conn.established.Store(false)
|
||||
t.connections[key] = conn
|
||||
|
||||
if t.stats != nil {
|
||||
t.stats.TrackNewConnection(6, srcIP, dstIP, srcPort, dstPort, fw.DirectionOutbound)
|
||||
}
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
|
||||
// Lock individual connection for state update
|
||||
conn.Lock()
|
||||
oldState := conn.State
|
||||
t.updateState(conn, flags, true)
|
||||
if oldState != conn.State && t.stats != nil {
|
||||
t.stats.TrackTCPState(conn.State)
|
||||
}
|
||||
conn.Unlock()
|
||||
|
||||
if t.stats != nil {
|
||||
t.stats.TrackPacket(6, false, uint64(len(packetData)), false, key)
|
||||
}
|
||||
conn.lastSeen.Store(now)
|
||||
}
|
||||
|
||||
// IsValidInbound checks if an inbound TCP packet matches a tracked connection
|
||||
func (t *TCPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8) bool {
|
||||
func (t *TCPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8, packetData []byte) bool {
|
||||
|
||||
if !isValidFlagCombination(flags) {
|
||||
return false
|
||||
}
|
||||
|
||||
key := makeConnKey(dstIP, srcIP, dstPort, srcPort)
|
||||
// Handle new SYN packets
|
||||
if flags&TCPSyn != 0 && flags&TCPAck == 0 {
|
||||
key := makeConnKey(dstIP, srcIP, dstPort, srcPort)
|
||||
t.mutex.Lock()
|
||||
if _, exists := t.connections[key]; !exists {
|
||||
// Use preallocated IPs
|
||||
srcIPCopy := t.ipPool.Get()
|
||||
dstIPCopy := t.ipPool.Get()
|
||||
copyIP(srcIPCopy, dstIP)
|
||||
copyIP(dstIPCopy, srcIP)
|
||||
|
||||
conn := &TCPConnTrack{
|
||||
BaseConnTrack: BaseConnTrack{
|
||||
SourceIP: srcIPCopy,
|
||||
DestIP: dstIPCopy,
|
||||
SourcePort: dstPort,
|
||||
DestPort: srcPort,
|
||||
},
|
||||
State: TCPStateSynReceived,
|
||||
}
|
||||
conn.lastSeen.Store(time.Now().UnixNano())
|
||||
conn.established.Store(false)
|
||||
t.connections[key] = conn
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
|
||||
if t.stats != nil {
|
||||
t.stats.TrackPacket(6, false, uint64(len(packetData)), true, key)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Look up existing connection
|
||||
key := makeConnKey(dstIP, srcIP, dstPort, srcPort)
|
||||
t.mutex.RLock()
|
||||
conn, exists := t.connections[key]
|
||||
t.mutex.RUnlock()
|
||||
@@ -142,10 +188,15 @@ func (t *TCPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16,
|
||||
return false
|
||||
}
|
||||
|
||||
if t.stats != nil {
|
||||
t.stats.TrackPacket(6, false, uint64(len(packetData)), true, key)
|
||||
}
|
||||
|
||||
// Handle RST packets
|
||||
if flags&TCPRst != 0 {
|
||||
conn.Lock()
|
||||
if conn.IsEstablished() || conn.State == TCPStateSynSent || conn.State == TCPStateSynReceived {
|
||||
isEstablished := conn.IsEstablished()
|
||||
if isEstablished || conn.State == TCPStateSynSent || conn.State == TCPStateSynReceived {
|
||||
conn.State = TCPStateClosed
|
||||
conn.SetEstablished(false)
|
||||
conn.Unlock()
|
||||
@@ -155,6 +206,7 @@ func (t *TCPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16,
|
||||
return false
|
||||
}
|
||||
|
||||
// Update state
|
||||
conn.Lock()
|
||||
t.updateState(conn, flags, false)
|
||||
conn.UpdateLastSeen()
|
||||
@@ -277,11 +329,6 @@ func (t *TCPTracker) isValidStateForFlags(state TCPState, flags uint8) bool {
|
||||
return flags&TCPFin != 0 || flags&TCPAck != 0
|
||||
case TCPStateLastAck:
|
||||
return flags&TCPAck != 0
|
||||
case TCPStateClosed:
|
||||
// Accept retransmitted ACKs in closed state
|
||||
// This is important because the final ACK might be lost
|
||||
// and the peer will retransmit their FIN-ACK
|
||||
return flags&TCPAck != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestTCPStateMachine(t *testing.T) {
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("100.64.0.1")
|
||||
@@ -58,7 +58,7 @@ func TestTCPStateMachine(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isValid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, tt.flags)
|
||||
isValid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, tt.flags, nil)
|
||||
require.Equal(t, !tt.wantDrop, isValid, tt.desc)
|
||||
})
|
||||
}
|
||||
@@ -76,17 +76,17 @@ func TestTCPStateMachine(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
// Send initial SYN
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, nil)
|
||||
|
||||
// Receive SYN-ACK
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck)
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, nil)
|
||||
require.True(t, valid, "SYN-ACK should be allowed")
|
||||
|
||||
// Send ACK
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, nil)
|
||||
|
||||
// Test data transfer
|
||||
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPPush|TCPAck)
|
||||
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPPush|TCPAck, nil)
|
||||
require.True(t, valid, "Data should be allowed after handshake")
|
||||
},
|
||||
},
|
||||
@@ -99,18 +99,18 @@ func TestTCPStateMachine(t *testing.T) {
|
||||
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||
|
||||
// Send FIN
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, nil)
|
||||
|
||||
// Receive ACK for FIN
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck)
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, nil)
|
||||
require.True(t, valid, "ACK for FIN should be allowed")
|
||||
|
||||
// Receive FIN from other side
|
||||
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck)
|
||||
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, nil)
|
||||
require.True(t, valid, "FIN should be allowed")
|
||||
|
||||
// Send final ACK
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -122,11 +122,14 @@ func TestTCPStateMachine(t *testing.T) {
|
||||
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||
|
||||
// Receive RST
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst)
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst, nil)
|
||||
require.True(t, valid, "RST should be allowed for established connection")
|
||||
|
||||
// Connection is logically dead but we don't enforce blocking subsequent packets
|
||||
// The connection will be cleaned up by timeout
|
||||
// Verify connection is closed
|
||||
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPPush|TCPAck, nil)
|
||||
t.Helper()
|
||||
|
||||
require.False(t, valid, "Data should be blocked after RST")
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -138,13 +141,13 @@ func TestTCPStateMachine(t *testing.T) {
|
||||
establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort)
|
||||
|
||||
// Both sides send FIN+ACK
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck)
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, nil)
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, nil)
|
||||
require.True(t, valid, "Simultaneous FIN should be allowed")
|
||||
|
||||
// Both sides send final ACK
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck)
|
||||
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, nil)
|
||||
valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, nil)
|
||||
require.True(t, valid, "Final ACKs should be allowed")
|
||||
},
|
||||
},
|
||||
@@ -154,7 +157,7 @@ func TestTCPStateMachine(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
tracker = NewTCPTracker(DefaultTCPTimeout)
|
||||
tracker = NewTCPTracker(DefaultTCPTimeout, nil)
|
||||
tt.test(t)
|
||||
})
|
||||
}
|
||||
@@ -162,7 +165,7 @@ func TestTCPStateMachine(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRSTHandling(t *testing.T) {
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("100.64.0.1")
|
||||
@@ -181,12 +184,12 @@ func TestRSTHandling(t *testing.T) {
|
||||
name: "RST in established",
|
||||
setupState: func() {
|
||||
// Establish connection first
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn)
|
||||
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, nil)
|
||||
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, nil)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, nil)
|
||||
},
|
||||
sendRST: func() {
|
||||
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst)
|
||||
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst, nil)
|
||||
},
|
||||
wantValid: true,
|
||||
desc: "Should accept RST for established connection",
|
||||
@@ -195,7 +198,7 @@ func TestRSTHandling(t *testing.T) {
|
||||
name: "RST without connection",
|
||||
setupState: func() {},
|
||||
sendRST: func() {
|
||||
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst)
|
||||
tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst, nil)
|
||||
},
|
||||
wantValid: false,
|
||||
desc: "Should reject RST without connection",
|
||||
@@ -223,17 +226,17 @@ func TestRSTHandling(t *testing.T) {
|
||||
func establishConnection(t *testing.T, tracker *TCPTracker, srcIP, dstIP net.IP, srcPort, dstPort uint16) {
|
||||
t.Helper()
|
||||
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, nil)
|
||||
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck)
|
||||
valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, nil)
|
||||
require.True(t, valid, "SYN-ACK should be allowed")
|
||||
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, nil)
|
||||
}
|
||||
|
||||
func BenchmarkTCPTracker(b *testing.B) {
|
||||
b.Run("TrackOutbound", func(b *testing.B) {
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.1")
|
||||
@@ -241,12 +244,12 @@ func BenchmarkTCPTracker(b *testing.B) {
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, TCPSyn)
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, TCPSyn, nil)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("IsValidInbound", func(b *testing.B) {
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.1")
|
||||
@@ -254,17 +257,17 @@ func BenchmarkTCPTracker(b *testing.B) {
|
||||
|
||||
// Pre-populate some connections
|
||||
for i := 0; i < 1000; i++ {
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, TCPSyn)
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, TCPSyn, nil)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%1000), TCPAck)
|
||||
tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%1000), TCPAck, nil)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ConcurrentAccess", func(b *testing.B) {
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
||||
tracker := NewTCPTracker(DefaultTCPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.1")
|
||||
@@ -274,9 +277,9 @@ func BenchmarkTCPTracker(b *testing.B) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
if i%2 == 0 {
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, TCPSyn)
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, TCPSyn, nil)
|
||||
} else {
|
||||
tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%65535), TCPAck)
|
||||
tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%65535), TCPAck, nil)
|
||||
}
|
||||
i++
|
||||
}
|
||||
@@ -287,14 +290,14 @@ func BenchmarkTCPTracker(b *testing.B) {
|
||||
// Benchmark connection cleanup
|
||||
func BenchmarkCleanup(b *testing.B) {
|
||||
b.Run("TCPCleanup", func(b *testing.B) {
|
||||
tracker := NewTCPTracker(100 * time.Millisecond) // Short timeout for testing
|
||||
tracker := NewTCPTracker(100*time.Millisecond, nil) // Short timeout for testing
|
||||
defer tracker.Close()
|
||||
|
||||
// Pre-populate with expired connections
|
||||
srcIP := net.ParseIP("192.168.1.1")
|
||||
dstIP := net.ParseIP("192.168.1.2")
|
||||
for i := 0; i < 10000; i++ {
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, TCPSyn)
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, TCPSyn, nil)
|
||||
}
|
||||
|
||||
// Wait for connections to expire
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,10 +28,11 @@ type UDPTracker struct {
|
||||
mutex sync.RWMutex
|
||||
done chan struct{}
|
||||
ipPool *PreallocatedIPs
|
||||
stats *Stats
|
||||
}
|
||||
|
||||
// NewUDPTracker creates a new UDP connection tracker
|
||||
func NewUDPTracker(timeout time.Duration) *UDPTracker {
|
||||
func NewUDPTracker(timeout time.Duration, stats *Stats) *UDPTracker {
|
||||
if timeout == 0 {
|
||||
timeout = DefaultUDPTimeout
|
||||
}
|
||||
@@ -40,6 +43,7 @@ func NewUDPTracker(timeout time.Duration) *UDPTracker {
|
||||
cleanupTicker: time.NewTicker(UDPCleanupInterval),
|
||||
done: make(chan struct{}),
|
||||
ipPool: NewPreallocatedIPs(),
|
||||
stats: stats,
|
||||
}
|
||||
|
||||
go tracker.cleanupRoutine()
|
||||
@@ -47,7 +51,7 @@ func NewUDPTracker(timeout time.Duration) *UDPTracker {
|
||||
}
|
||||
|
||||
// TrackOutbound records an outbound UDP connection
|
||||
func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) {
|
||||
func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, packetData []byte) {
|
||||
key := makeConnKey(srcIP, dstIP, srcPort, dstPort)
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
@@ -70,14 +74,21 @@ func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d
|
||||
conn.lastSeen.Store(now)
|
||||
conn.established.Store(true)
|
||||
t.connections[key] = conn
|
||||
|
||||
if t.stats != nil {
|
||||
t.stats.TrackNewConnection(17, srcIP, dstIP, srcPort, dstPort, fw.DirectionOutbound)
|
||||
}
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
|
||||
if t.stats != nil {
|
||||
t.stats.TrackPacket(17, false, uint64(len(packetData)), false, key)
|
||||
}
|
||||
conn.lastSeen.Store(now)
|
||||
}
|
||||
|
||||
// IsValidInbound checks if an inbound packet matches a tracked connection
|
||||
func (t *UDPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) bool {
|
||||
func (t *UDPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, packetData []byte) bool {
|
||||
key := makeConnKey(dstIP, srcIP, dstPort, srcPort)
|
||||
|
||||
t.mutex.RLock()
|
||||
@@ -92,6 +103,10 @@ func (t *UDPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16,
|
||||
return false
|
||||
}
|
||||
|
||||
if t.stats != nil {
|
||||
t.stats.TrackPacket(17, false, uint64(len(packetData)), true, key)
|
||||
}
|
||||
|
||||
return conn.IsEstablished() &&
|
||||
ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) &&
|
||||
ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) &&
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestNewUDPTracker(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tracker := NewUDPTracker(tt.timeout)
|
||||
tracker := NewUDPTracker(tt.timeout, nil)
|
||||
assert.NotNil(t, tracker)
|
||||
assert.Equal(t, tt.wantTimeout, tracker.timeout)
|
||||
assert.NotNil(t, tracker.connections)
|
||||
@@ -40,7 +40,7 @@ func TestNewUDPTracker(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUDPTracker_TrackOutbound(t *testing.T) {
|
||||
tracker := NewUDPTracker(DefaultUDPTimeout)
|
||||
tracker := NewUDPTracker(DefaultUDPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.2")
|
||||
@@ -48,7 +48,7 @@ func TestUDPTracker_TrackOutbound(t *testing.T) {
|
||||
srcPort := uint16(12345)
|
||||
dstPort := uint16(53)
|
||||
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, nil)
|
||||
|
||||
// Verify connection was tracked
|
||||
key := makeConnKey(srcIP, dstIP, srcPort, dstPort)
|
||||
@@ -63,7 +63,7 @@ func TestUDPTracker_TrackOutbound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUDPTracker_IsValidInbound(t *testing.T) {
|
||||
tracker := NewUDPTracker(1 * time.Second)
|
||||
tracker := NewUDPTracker(1*time.Second, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.2")
|
||||
@@ -72,7 +72,7 @@ func TestUDPTracker_IsValidInbound(t *testing.T) {
|
||||
dstPort := uint16(53)
|
||||
|
||||
// Track outbound connection
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort)
|
||||
tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -144,7 +144,7 @@ func TestUDPTracker_IsValidInbound(t *testing.T) {
|
||||
if tt.sleep > 0 {
|
||||
time.Sleep(tt.sleep)
|
||||
}
|
||||
got := tracker.IsValidInbound(tt.srcIP, tt.dstIP, tt.srcPort, tt.dstPort)
|
||||
got := tracker.IsValidInbound(tt.srcIP, tt.dstIP, tt.srcPort, tt.dstPort, nil)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func TestUDPTracker_Cleanup(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, conn := range connections {
|
||||
tracker.TrackOutbound(conn.srcIP, conn.dstIP, conn.srcPort, conn.dstPort)
|
||||
tracker.TrackOutbound(conn.srcIP, conn.dstIP, conn.srcPort, conn.dstPort, nil)
|
||||
}
|
||||
|
||||
// Verify initial connections
|
||||
@@ -211,7 +211,7 @@ func TestUDPTracker_Cleanup(t *testing.T) {
|
||||
|
||||
func BenchmarkUDPTracker(b *testing.B) {
|
||||
b.Run("TrackOutbound", func(b *testing.B) {
|
||||
tracker := NewUDPTracker(DefaultUDPTimeout)
|
||||
tracker := NewUDPTracker(DefaultUDPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.1")
|
||||
@@ -219,12 +219,12 @@ func BenchmarkUDPTracker(b *testing.B) {
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80)
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, nil)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("IsValidInbound", func(b *testing.B) {
|
||||
tracker := NewUDPTracker(DefaultUDPTimeout)
|
||||
tracker := NewUDPTracker(DefaultUDPTimeout, nil)
|
||||
defer tracker.Close()
|
||||
|
||||
srcIP := net.ParseIP("192.168.1.1")
|
||||
@@ -232,12 +232,12 @@ func BenchmarkUDPTracker(b *testing.B) {
|
||||
|
||||
// Pre-populate some connections
|
||||
for i := 0; i < 1000; i++ {
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80)
|
||||
tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, nil)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%1000))
|
||||
tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%1000), nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ import (
|
||||
|
||||
const layerTypeAll = 0
|
||||
|
||||
const EnvDisableConntrack = "NB_DISABLE_CONNTRACK"
|
||||
const (
|
||||
EnvDisableConntrack = "NB_DISABLE_CONNTRACK"
|
||||
EnvEnableStats = "NB_ENABLE_CONNTRACK_STATS"
|
||||
)
|
||||
|
||||
var (
|
||||
errRouteNotSupported = fmt.Errorf("route not supported with userspace firewall")
|
||||
@@ -52,6 +55,9 @@ type Manager struct {
|
||||
udpTracker *conntrack.UDPTracker
|
||||
icmpTracker *conntrack.ICMPTracker
|
||||
tcpTracker *conntrack.TCPTracker
|
||||
|
||||
statsEnabled bool
|
||||
stats *conntrack.Stats
|
||||
}
|
||||
|
||||
// decoder for packages
|
||||
@@ -84,6 +90,7 @@ func CreateWithNativeFirewall(iface IFaceMapper, nativeFirewall firewall.Manager
|
||||
|
||||
func create(iface IFaceMapper) (*Manager, error) {
|
||||
disableConntrack, _ := strconv.ParseBool(os.Getenv(EnvDisableConntrack))
|
||||
enableStats, _ := strconv.ParseBool(os.Getenv(EnvEnableStats))
|
||||
|
||||
m := &Manager{
|
||||
decoders: sync.Pool{
|
||||
@@ -103,15 +110,21 @@ func create(iface IFaceMapper) (*Manager, error) {
|
||||
incomingRules: make(map[string]RuleSet),
|
||||
wgIface: iface,
|
||||
stateful: !disableConntrack,
|
||||
statsEnabled: enableStats,
|
||||
}
|
||||
|
||||
if enableStats {
|
||||
m.stats = conntrack.NewStats()
|
||||
log.Info("connection tracking statistics enabled")
|
||||
}
|
||||
|
||||
// Only initialize trackers if stateful mode is enabled
|
||||
if disableConntrack {
|
||||
log.Info("conntrack is disabled")
|
||||
} else {
|
||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout)
|
||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout)
|
||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout)
|
||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.stats)
|
||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.stats)
|
||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.stats)
|
||||
}
|
||||
|
||||
if err := iface.SetFilter(m); err != nil {
|
||||
@@ -304,7 +317,10 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool {
|
||||
if d.decoded[1] == layers.LayerTypeUDP {
|
||||
// Track UDP state only if enabled
|
||||
if m.stateful {
|
||||
m.trackUDPOutbound(d, srcIP, dstIP)
|
||||
m.udpTracker.TrackOutbound(srcIP, dstIP,
|
||||
uint16(d.udp.SrcPort),
|
||||
uint16(d.udp.DstPort),
|
||||
packetData)
|
||||
}
|
||||
return m.checkUDPHooks(d, dstIP, packetData)
|
||||
}
|
||||
@@ -313,9 +329,16 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool {
|
||||
if m.stateful {
|
||||
switch d.decoded[1] {
|
||||
case layers.LayerTypeTCP:
|
||||
m.trackTCPOutbound(d, srcIP, dstIP)
|
||||
m.tcpTracker.TrackOutbound(srcIP, dstIP,
|
||||
uint16(d.tcp.SrcPort),
|
||||
uint16(d.tcp.DstPort),
|
||||
getTCPFlags(&d.tcp),
|
||||
packetData)
|
||||
case layers.LayerTypeICMPv4:
|
||||
m.trackICMPOutbound(d, srcIP, dstIP)
|
||||
m.icmpTracker.TrackOutbound(srcIP, dstIP,
|
||||
d.icmp4.Id,
|
||||
d.icmp4.Seq,
|
||||
packetData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,17 +356,6 @@ func (m *Manager) extractIPs(d *decoder) (srcIP, dstIP net.IP) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) trackTCPOutbound(d *decoder, srcIP, dstIP net.IP) {
|
||||
flags := getTCPFlags(&d.tcp)
|
||||
m.tcpTracker.TrackOutbound(
|
||||
srcIP,
|
||||
dstIP,
|
||||
uint16(d.tcp.SrcPort),
|
||||
uint16(d.tcp.DstPort),
|
||||
flags,
|
||||
)
|
||||
}
|
||||
|
||||
func getTCPFlags(tcp *layers.TCP) uint8 {
|
||||
var flags uint8
|
||||
if tcp.SYN {
|
||||
@@ -367,15 +379,6 @@ func getTCPFlags(tcp *layers.TCP) uint8 {
|
||||
return flags
|
||||
}
|
||||
|
||||
func (m *Manager) trackUDPOutbound(d *decoder, srcIP, dstIP net.IP) {
|
||||
m.udpTracker.TrackOutbound(
|
||||
srcIP,
|
||||
dstIP,
|
||||
uint16(d.udp.SrcPort),
|
||||
uint16(d.udp.DstPort),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *Manager) checkUDPHooks(d *decoder, dstIP net.IP, packetData []byte) bool {
|
||||
for _, ipKey := range []string{dstIP.String(), "0.0.0.0", "::"} {
|
||||
if rules, exists := m.outgoingRules[ipKey]; exists {
|
||||
@@ -389,17 +392,6 @@ func (m *Manager) checkUDPHooks(d *decoder, dstIP net.IP, packetData []byte) boo
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) trackICMPOutbound(d *decoder, srcIP, dstIP net.IP) {
|
||||
if d.icmp4.TypeCode.Type() == layers.ICMPv4TypeEchoRequest {
|
||||
m.icmpTracker.TrackOutbound(
|
||||
srcIP,
|
||||
dstIP,
|
||||
d.icmp4.Id,
|
||||
d.icmp4.Seq,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// dropFilter implements filtering logic for incoming packets
|
||||
func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet) bool {
|
||||
m.mutex.RLock()
|
||||
@@ -423,7 +415,7 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet) bool {
|
||||
}
|
||||
|
||||
// Check connection state only if enabled
|
||||
if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP) {
|
||||
if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP, packetData) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -447,7 +439,7 @@ func (m *Manager) isWireguardTraffic(srcIP, dstIP net.IP) bool {
|
||||
return m.wgNetwork.Contains(srcIP) && m.wgNetwork.Contains(dstIP)
|
||||
}
|
||||
|
||||
func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool {
|
||||
func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool {
|
||||
switch d.decoded[1] {
|
||||
case layers.LayerTypeTCP:
|
||||
return m.tcpTracker.IsValidInbound(
|
||||
@@ -456,6 +448,7 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool
|
||||
uint16(d.tcp.SrcPort),
|
||||
uint16(d.tcp.DstPort),
|
||||
getTCPFlags(&d.tcp),
|
||||
packetData,
|
||||
)
|
||||
|
||||
case layers.LayerTypeUDP:
|
||||
@@ -464,6 +457,7 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool
|
||||
dstIP,
|
||||
uint16(d.udp.SrcPort),
|
||||
uint16(d.udp.DstPort),
|
||||
packetData,
|
||||
)
|
||||
|
||||
case layers.LayerTypeICMPv4:
|
||||
@@ -473,6 +467,7 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool
|
||||
d.icmp4.Id,
|
||||
d.icmp4.Seq,
|
||||
d.icmp4.TypeCode.Type(),
|
||||
packetData,
|
||||
)
|
||||
|
||||
// TODO: ICMPv6
|
||||
@@ -612,3 +607,11 @@ func (m *Manager) RemovePacketHook(hookID string) error {
|
||||
}
|
||||
return fmt.Errorf("hook with given id not found")
|
||||
}
|
||||
|
||||
// CollectStats returns connection tracking statistics
|
||||
func (m *Manager) CollectStats() []*firewall.FlowStats {
|
||||
if m.stats == nil {
|
||||
return nil
|
||||
}
|
||||
return m.stats.GetFlowSnapshot()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -965,6 +966,114 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFirewallStats(b *testing.B) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
stats bool
|
||||
longLived bool
|
||||
conns int
|
||||
}{
|
||||
{"nostats_short_100", false, false, 100},
|
||||
{"stats_short_100", true, false, 100},
|
||||
{"nostats_long_100", false, true, 100},
|
||||
{"stats_long_100", true, true, 100},
|
||||
{"nostats_short_1000", false, false, 1000},
|
||||
{"stats_short_1000", true, false, 1000},
|
||||
{"nostats_long_1000", false, true, 1000},
|
||||
{"stats_long_1000", true, true, 1000},
|
||||
}
|
||||
|
||||
for _, sc := range scenarios {
|
||||
b.Run(sc.name, func(b *testing.B) {
|
||||
manager, _ := Create(&IFaceMock{
|
||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||
})
|
||||
defer b.Cleanup(func() {
|
||||
require.NoError(b, manager.Reset(nil))
|
||||
})
|
||||
|
||||
b.Setenv(EnvEnableStats, strconv.FormatBool(sc.stats))
|
||||
|
||||
manager.SetNetwork(&net.IPNet{
|
||||
IP: net.ParseIP("100.64.0.0"),
|
||||
Mask: net.CIDRMask(10, 32),
|
||||
})
|
||||
|
||||
// Generate test IPs
|
||||
srcIPs := make([]net.IP, sc.conns)
|
||||
dstIPs := make([]net.IP, sc.conns)
|
||||
for i := 0; i < sc.conns; i++ {
|
||||
srcIPs[i] = generateRandomIPs(1)[0]
|
||||
dstIPs[i] = generateRandomIPs(1)[0]
|
||||
}
|
||||
|
||||
// Pre-generate packets
|
||||
inPackets := make([][]byte, sc.conns)
|
||||
outPackets := make([][]byte, sc.conns)
|
||||
for i := 0; i < sc.conns; i++ {
|
||||
inPackets[i] = generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i],
|
||||
80, uint16(1024+i), uint16(conntrack.TCPPush|conntrack.TCPAck))
|
||||
outPackets[i] = generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i],
|
||||
uint16(1024+i), 80, uint16(conntrack.TCPPush|conntrack.TCPAck))
|
||||
|
||||
if sc.longLived {
|
||||
// Establish connection
|
||||
syn := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i],
|
||||
uint16(1024+i), 80, uint16(conntrack.TCPSyn))
|
||||
synAck := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i],
|
||||
80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck))
|
||||
ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i],
|
||||
uint16(1024+i), 80, uint16(conntrack.TCPAck))
|
||||
|
||||
manager.processOutgoingHooks(syn)
|
||||
manager.dropFilter(synAck, manager.incomingRules)
|
||||
manager.processOutgoingHooks(ack)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
connIdx := i % sc.conns
|
||||
|
||||
if !sc.longLived {
|
||||
// New connection each time
|
||||
syn := generateTCPPacketWithFlags(b, srcIPs[connIdx], dstIPs[connIdx],
|
||||
uint16(1024+connIdx), 80, uint16(conntrack.TCPSyn))
|
||||
synAck := generateTCPPacketWithFlags(b, dstIPs[connIdx], srcIPs[connIdx],
|
||||
80, uint16(1024+connIdx), uint16(conntrack.TCPSyn|conntrack.TCPAck))
|
||||
ack := generateTCPPacketWithFlags(b, srcIPs[connIdx], dstIPs[connIdx],
|
||||
uint16(1024+connIdx), 80, uint16(conntrack.TCPAck))
|
||||
|
||||
manager.processOutgoingHooks(syn)
|
||||
manager.dropFilter(synAck, manager.incomingRules)
|
||||
manager.processOutgoingHooks(ack)
|
||||
}
|
||||
|
||||
// Data transfer
|
||||
manager.processOutgoingHooks(outPackets[connIdx])
|
||||
manager.dropFilter(inPackets[connIdx], manager.incomingRules)
|
||||
|
||||
if !sc.longLived {
|
||||
// Tear down
|
||||
finClient := generateTCPPacketWithFlags(b, srcIPs[connIdx], dstIPs[connIdx],
|
||||
uint16(1024+connIdx), 80, uint16(conntrack.TCPFin|conntrack.TCPAck))
|
||||
ackServer := generateTCPPacketWithFlags(b, dstIPs[connIdx], srcIPs[connIdx],
|
||||
80, uint16(1024+connIdx), uint16(conntrack.TCPAck))
|
||||
finServer := generateTCPPacketWithFlags(b, dstIPs[connIdx], srcIPs[connIdx],
|
||||
80, uint16(1024+connIdx), uint16(conntrack.TCPFin|conntrack.TCPAck))
|
||||
ackClient := generateTCPPacketWithFlags(b, srcIPs[connIdx], dstIPs[connIdx],
|
||||
uint16(1024+connIdx), 80, uint16(conntrack.TCPAck))
|
||||
|
||||
manager.processOutgoingHooks(finClient)
|
||||
manager.dropFilter(ackServer, manager.incomingRules)
|
||||
manager.dropFilter(finServer, manager.incomingRules)
|
||||
manager.processOutgoingHooks(ackClient)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// generateTCPPacketWithFlags creates a TCP packet with specific flags
|
||||
func generateTCPPacketWithFlags(b *testing.B, srcIP, dstIP net.IP, srcPort, dstPort, flags uint16) []byte {
|
||||
b.Helper()
|
||||
|
||||
@@ -400,7 +400,7 @@ func TestProcessOutgoingHooks(t *testing.T) {
|
||||
Mask: net.CIDRMask(16, 32),
|
||||
}
|
||||
manager.udpTracker.Close()
|
||||
manager.udpTracker = conntrack.NewUDPTracker(100 * time.Millisecond)
|
||||
manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, nil)
|
||||
defer func() {
|
||||
require.NoError(t, manager.Reset(nil))
|
||||
}()
|
||||
@@ -518,7 +518,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
||||
}
|
||||
|
||||
manager.udpTracker.Close() // Close the existing tracker
|
||||
manager.udpTracker = conntrack.NewUDPTracker(200 * time.Millisecond)
|
||||
manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, nil)
|
||||
manager.decoders = sync.Pool{
|
||||
New: func() any {
|
||||
d := &decoder{
|
||||
|
||||
@@ -68,16 +68,17 @@ func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
pattern = strings.ToLower(dns.Fqdn(pattern))
|
||||
origPattern := pattern
|
||||
isWildcard := strings.HasPrefix(pattern, "*.")
|
||||
if isWildcard {
|
||||
pattern = pattern[2:]
|
||||
}
|
||||
pattern = dns.Fqdn(pattern)
|
||||
origPattern = dns.Fqdn(origPattern)
|
||||
|
||||
// First remove any existing handler with same pattern (case-insensitive) and priority
|
||||
// First remove any existing handler with same original pattern and priority
|
||||
for i := len(c.handlers) - 1; i >= 0; i-- {
|
||||
if strings.EqualFold(c.handlers[i].OrigPattern, origPattern) && c.handlers[i].Priority == priority {
|
||||
if c.handlers[i].OrigPattern == origPattern && c.handlers[i].Priority == priority {
|
||||
if c.handlers[i].StopHandler != nil {
|
||||
c.handlers[i].StopHandler.stop()
|
||||
}
|
||||
@@ -125,10 +126,10 @@ func (c *HandlerChain) RemoveHandler(pattern string, priority int) {
|
||||
|
||||
pattern = dns.Fqdn(pattern)
|
||||
|
||||
// Find and remove handlers matching both original pattern (case-insensitive) and priority
|
||||
// Find and remove handlers matching both original pattern and priority
|
||||
for i := len(c.handlers) - 1; i >= 0; i-- {
|
||||
entry := c.handlers[i]
|
||||
if strings.EqualFold(entry.OrigPattern, pattern) && entry.Priority == priority {
|
||||
if entry.OrigPattern == pattern && entry.Priority == priority {
|
||||
if entry.StopHandler != nil {
|
||||
entry.StopHandler.stop()
|
||||
}
|
||||
@@ -143,9 +144,9 @@ func (c *HandlerChain) HasHandlers(pattern string) bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
pattern = strings.ToLower(dns.Fqdn(pattern))
|
||||
pattern = dns.Fqdn(pattern)
|
||||
for _, entry := range c.handlers {
|
||||
if strings.EqualFold(entry.Pattern, pattern) {
|
||||
if entry.Pattern == pattern {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -157,7 +158,7 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
qname := strings.ToLower(r.Question[0].Name)
|
||||
qname := r.Question[0].Name
|
||||
log.Tracef("handling DNS request for domain=%s", qname)
|
||||
|
||||
c.mu.RLock()
|
||||
@@ -186,9 +187,9 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||
// If handler wants subdomain matching, allow suffix match
|
||||
// Otherwise require exact match
|
||||
if entry.MatchSubdomains {
|
||||
matched = strings.EqualFold(qname, entry.Pattern) || strings.HasSuffix(qname, "."+entry.Pattern)
|
||||
matched = qname == entry.Pattern || strings.HasSuffix(qname, "."+entry.Pattern)
|
||||
} else {
|
||||
matched = strings.EqualFold(qname, entry.Pattern)
|
||||
matched = qname == entry.Pattern
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -507,173 +507,5 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
||||
|
||||
// Test 4: Remove last handler
|
||||
chain.RemoveHandler(testDomain, nbdns.PriorityDefault)
|
||||
|
||||
assert.False(t, chain.HasHandlers(testDomain))
|
||||
}
|
||||
|
||||
func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scenario string
|
||||
addHandlers []struct {
|
||||
pattern string
|
||||
priority int
|
||||
subdomains bool
|
||||
shouldMatch bool
|
||||
}
|
||||
query string
|
||||
expectedCalls int
|
||||
}{
|
||||
{
|
||||
name: "case insensitive exact match",
|
||||
scenario: "handler registered lowercase, query uppercase",
|
||||
addHandlers: []struct {
|
||||
pattern string
|
||||
priority int
|
||||
subdomains bool
|
||||
shouldMatch bool
|
||||
}{
|
||||
{"example.com.", nbdns.PriorityDefault, false, true},
|
||||
},
|
||||
query: "EXAMPLE.COM.",
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "case insensitive wildcard match",
|
||||
scenario: "handler registered mixed case wildcard, query different case",
|
||||
addHandlers: []struct {
|
||||
pattern string
|
||||
priority int
|
||||
subdomains bool
|
||||
shouldMatch bool
|
||||
}{
|
||||
{"*.Example.Com.", nbdns.PriorityDefault, false, true},
|
||||
},
|
||||
query: "sub.EXAMPLE.COM.",
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple handlers different case same domain",
|
||||
scenario: "second handler should replace first despite case difference",
|
||||
addHandlers: []struct {
|
||||
pattern string
|
||||
priority int
|
||||
subdomains bool
|
||||
shouldMatch bool
|
||||
}{
|
||||
{"EXAMPLE.COM.", nbdns.PriorityDefault, false, false},
|
||||
{"example.com.", nbdns.PriorityDefault, false, true},
|
||||
},
|
||||
query: "ExAmPlE.cOm.",
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "subdomain matching case insensitive",
|
||||
scenario: "handler with MatchSubdomains true should match regardless of case",
|
||||
addHandlers: []struct {
|
||||
pattern string
|
||||
priority int
|
||||
subdomains bool
|
||||
shouldMatch bool
|
||||
}{
|
||||
{"example.com.", nbdns.PriorityDefault, true, true},
|
||||
},
|
||||
query: "SUB.EXAMPLE.COM.",
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "root zone case insensitive",
|
||||
scenario: "root zone handler should match regardless of case",
|
||||
addHandlers: []struct {
|
||||
pattern string
|
||||
priority int
|
||||
subdomains bool
|
||||
shouldMatch bool
|
||||
}{
|
||||
{".", nbdns.PriorityDefault, false, true},
|
||||
},
|
||||
query: "EXAMPLE.COM.",
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple handlers different priority",
|
||||
scenario: "should call higher priority handler despite case differences",
|
||||
addHandlers: []struct {
|
||||
pattern string
|
||||
priority int
|
||||
subdomains bool
|
||||
shouldMatch bool
|
||||
}{
|
||||
{"EXAMPLE.COM.", nbdns.PriorityDefault, false, false},
|
||||
{"example.com.", nbdns.PriorityMatchDomain, false, false},
|
||||
{"Example.Com.", nbdns.PriorityDNSRoute, false, true},
|
||||
},
|
||||
query: "example.com.",
|
||||
expectedCalls: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
chain := nbdns.NewHandlerChain()
|
||||
handlerCalls := make(map[string]bool) // track which patterns were called
|
||||
|
||||
// Add handlers according to test case
|
||||
for _, h := range tt.addHandlers {
|
||||
var handler dns.Handler
|
||||
pattern := h.pattern // capture pattern for closure
|
||||
|
||||
if h.subdomains {
|
||||
subHandler := &nbdns.MockSubdomainHandler{
|
||||
Subdomains: true,
|
||||
}
|
||||
if h.shouldMatch {
|
||||
subHandler.On("ServeDNS", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
handlerCalls[pattern] = true
|
||||
w := args.Get(0).(dns.ResponseWriter)
|
||||
r := args.Get(1).(*dns.Msg)
|
||||
resp := new(dns.Msg)
|
||||
resp.SetRcode(r, dns.RcodeSuccess)
|
||||
assert.NoError(t, w.WriteMsg(resp))
|
||||
}).Once()
|
||||
}
|
||||
handler = subHandler
|
||||
} else {
|
||||
mockHandler := &nbdns.MockHandler{}
|
||||
if h.shouldMatch {
|
||||
mockHandler.On("ServeDNS", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
handlerCalls[pattern] = true
|
||||
w := args.Get(0).(dns.ResponseWriter)
|
||||
r := args.Get(1).(*dns.Msg)
|
||||
resp := new(dns.Msg)
|
||||
resp.SetRcode(r, dns.RcodeSuccess)
|
||||
assert.NoError(t, w.WriteMsg(resp))
|
||||
}).Once()
|
||||
}
|
||||
handler = mockHandler
|
||||
}
|
||||
|
||||
chain.AddHandler(pattern, handler, h.priority, nil)
|
||||
}
|
||||
|
||||
// Execute request
|
||||
r := new(dns.Msg)
|
||||
r.SetQuestion(tt.query, dns.TypeA)
|
||||
chain.ServeDNS(&mockResponseWriter{}, r)
|
||||
|
||||
// Verify each handler was called exactly as expected
|
||||
for _, h := range tt.addHandlers {
|
||||
wasCalled := handlerCalls[h.pattern]
|
||||
assert.Equal(t, h.shouldMatch, wasCalled,
|
||||
"Handler for pattern %q was %s when it should%s have been",
|
||||
h.pattern,
|
||||
map[bool]string{true: "called", false: "not called"}[wasCalled],
|
||||
map[bool]string{true: "", false: " not"}[wasCalled == h.shouldMatch])
|
||||
}
|
||||
|
||||
// Verify total number of calls
|
||||
assert.Equal(t, tt.expectedCalls, len(handlerCalls),
|
||||
"Wrong number of total handler calls")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (h *Manager) allowDNSFirewall() error {
|
||||
IsRange: false,
|
||||
Values: []int{ListenPort},
|
||||
}
|
||||
dnsRules, err := h.firewall.AddPeerFiltering(net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.RuleDirectionIN, firewall.ActionAccept, "", "")
|
||||
dnsRules, err := h.firewall.AddPeerFiltering(net.ParseIP("0.0.0.0"), firewall.ProtocolUDP, nil, dport, firewall.RuleDirectionIN, firewall.ActionAccept, "", "")
|
||||
if err != nil {
|
||||
log.Errorf("failed to add allow DNS router rules, err: %v", err)
|
||||
return err
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/netbirdio/netbird/client/firewall"
|
||||
"github.com/netbirdio/netbird/client/firewall/manager"
|
||||
firewallmanager "github.com/netbirdio/netbird/client/firewall/manager"
|
||||
"github.com/netbirdio/netbird/client/iface"
|
||||
"github.com/netbirdio/netbird/client/iface/bind"
|
||||
"github.com/netbirdio/netbird/client/iface/device"
|
||||
@@ -158,7 +158,7 @@ type Engine struct {
|
||||
|
||||
statusRecorder *peer.Status
|
||||
|
||||
firewall manager.Manager
|
||||
firewall firewallmanager.Manager
|
||||
routeManager routemanager.Manager
|
||||
acl acl.Manager
|
||||
dnsForwardMgr *dnsfwd.Manager
|
||||
@@ -406,9 +406,13 @@ func (e *Engine) Start() error {
|
||||
e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager)
|
||||
if err != nil {
|
||||
log.Errorf("failed creating firewall manager: %s", err)
|
||||
} else if e.firewall != nil {
|
||||
if err := e.initFirewall(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.firewall != nil && e.firewall.IsServerRouteSupported() {
|
||||
err = e.routeManager.EnableServerRouter(e.firewall)
|
||||
if err != nil {
|
||||
e.close()
|
||||
return fmt.Errorf("enable server router: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,41 +455,6 @@ func (e *Engine) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) initFirewall(error) error {
|
||||
if e.firewall.IsServerRouteSupported() {
|
||||
if err := e.routeManager.EnableServerRouter(e.firewall); err != nil {
|
||||
e.close()
|
||||
return fmt.Errorf("enable server router: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if e.rpManager == nil || !e.config.RosenpassEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
rosenpassPort := e.rpManager.GetAddress().Port
|
||||
port := manager.Port{Values: []int{rosenpassPort}}
|
||||
|
||||
// this rule is static and will be torn down on engine down by the firewall manager
|
||||
if _, err := e.firewall.AddPeerFiltering(
|
||||
net.IP{0, 0, 0, 0},
|
||||
manager.ProtocolUDP,
|
||||
nil,
|
||||
&port,
|
||||
manager.RuleDirectionIN,
|
||||
manager.ActionAccept,
|
||||
"",
|
||||
"",
|
||||
); err != nil {
|
||||
log.Errorf("failed to allow rosenpass interface traffic: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("rosenpass interface traffic allowed on port %d", rosenpassPort)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// modifyPeers updates peers that have been modified (e.g. IP address has been changed).
|
||||
// It closes the existing connection, removes it from the peerConns map, and creates a new one.
|
||||
func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||
@@ -1607,6 +1576,14 @@ func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) {
|
||||
return nm, nil
|
||||
}
|
||||
|
||||
// GetFirewallStats returns the firewall stats
|
||||
func (e *Engine) GetFirewallStats() []*firewallmanager.FlowStats {
|
||||
if e.firewall != nil {
|
||||
return e.firewall.CollectStats()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateDNSForwarder start or stop the DNS forwarder based on the domains and the feature flag
|
||||
func (e *Engine) updateDNSForwarder(enabled bool, domains []string) {
|
||||
if !enabled {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"github.com/netbirdio/netbird/client/anonymize"
|
||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||
"github.com/netbirdio/netbird/client/internal/peer"
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||
@@ -44,6 +45,7 @@ iptables.txt: Anonymized iptables rules with packet counters, if --system-info f
|
||||
nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided.
|
||||
config.txt: Anonymized configuration information of the NetBird client.
|
||||
network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
|
||||
firewall_stats.json: Anonymized firewall statistics of the NetBird client.
|
||||
state.json: Anonymized client state dump containing netbird states.
|
||||
|
||||
|
||||
@@ -181,21 +183,33 @@ func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleReques
|
||||
}
|
||||
|
||||
if req.GetSystemInfo() {
|
||||
s.addSystemInfo(req, anonymizer, archive)
|
||||
if err := s.addRoutes(req, anonymizer, archive); err != nil {
|
||||
log.Errorf("Failed to add routes to debug bundle: %v", err)
|
||||
}
|
||||
|
||||
if err := s.addInterfaces(req, anonymizer, archive); err != nil {
|
||||
log.Errorf("Failed to add interfaces to debug bundle: %v", err)
|
||||
}
|
||||
|
||||
if err := s.addFirewallRules(req, anonymizer, archive); err != nil {
|
||||
log.Errorf("Failed to add firewall rules to debug bundle: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.addNetworkMap(req, anonymizer, archive); err != nil {
|
||||
return fmt.Errorf("add network map: %w", err)
|
||||
}
|
||||
|
||||
if err := s.addFirewallStats(req, anonymizer, archive); err != nil {
|
||||
log.Errorf("Failed to add firewall stats to debug bundle: %v", err)
|
||||
}
|
||||
|
||||
if err := s.addStateFile(req, anonymizer, archive); err != nil {
|
||||
log.Errorf("Failed to add state file to debug bundle: %v", err)
|
||||
}
|
||||
|
||||
if s.logFile != "console" {
|
||||
if err := s.addLogfile(req, anonymizer, archive); err != nil {
|
||||
return fmt.Errorf("add log file: %w", err)
|
||||
}
|
||||
if err := s.addLogfile(req, anonymizer, archive); err != nil {
|
||||
return fmt.Errorf("add log file: %w", err)
|
||||
}
|
||||
|
||||
if err := archive.Close(); err != nil {
|
||||
@@ -204,20 +218,6 @@ func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleReques
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) addSystemInfo(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) {
|
||||
if err := s.addRoutes(req, anonymizer, archive); err != nil {
|
||||
log.Errorf("Failed to add routes to debug bundle: %v", err)
|
||||
}
|
||||
|
||||
if err := s.addInterfaces(req, anonymizer, archive); err != nil {
|
||||
log.Errorf("Failed to add interfaces to debug bundle: %v", err)
|
||||
}
|
||||
|
||||
if err := s.addFirewallRules(req, anonymizer, archive); err != nil {
|
||||
log.Errorf("Failed to add firewall rules to debug bundle: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) addReadme(req *proto.DebugBundleRequest, archive *zip.Writer) error {
|
||||
if req.GetAnonymize() {
|
||||
readmeReader := strings.NewReader(readmeContent)
|
||||
@@ -358,6 +358,32 @@ func (s *Server) addNetworkMap(req *proto.DebugBundleRequest, anonymizer *anonym
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) addFirewallStats(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
|
||||
if s.connectClient == nil || s.connectClient.Engine() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stats := s.connectClient.Engine().GetFirewallStats()
|
||||
if stats == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.GetAnonymize() {
|
||||
anonymizeStatIPs(stats, anonymizer)
|
||||
}
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(stats, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal firewall stats: %w", err)
|
||||
}
|
||||
|
||||
if err := addFileToZip(archive, bytes.NewReader(jsonBytes), "firewall_stats.json"); err != nil {
|
||||
return fmt.Errorf("add firewall stats to zip: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) addStateFile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error {
|
||||
path := statemanager.GetDefaultStatePath()
|
||||
if path == "" {
|
||||
@@ -942,3 +968,18 @@ func anonymizeSlice(v []any, anonymizer *anonymize.Anonymizer) []any {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func anonymizeStatIPs(stats []*firewall.FlowStats, anonymizer *anonymize.Anonymizer) {
|
||||
for _, stat := range stats {
|
||||
if stat.SourceIP != nil {
|
||||
if ip, ok := netip.AddrFromSlice(stat.SourceIP); ok {
|
||||
stat.SourceIP = anonymizer.AnonymizeIP(ip).AsSlice()
|
||||
}
|
||||
}
|
||||
if stat.DestIP != nil {
|
||||
if ip, ok := netip.AddrFromSlice(stat.DestIP); ok {
|
||||
stat.DestIP = anonymizer.AnonymizeIP(ip).AsSlice()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1252,12 +1252,6 @@ func (am *DefaultAccountManager) GetAccountIDFromToken(ctx context.Context, clai
|
||||
// syncJWTGroups processes the JWT groups for a user, updates the account based on the groups,
|
||||
// and propagates changes to peers if group propagation is enabled.
|
||||
func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims) error {
|
||||
if claim, exists := claims.Raw[jwtclaims.IsToken]; exists {
|
||||
if isToken, ok := claim.(bool); ok && isToken {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -2729,19 +2729,6 @@ func TestAccount_SetJWTGroups(t *testing.T) {
|
||||
|
||||
assert.NoError(t, manager.Store.SaveAccount(context.Background(), account), "unable to save account")
|
||||
|
||||
t.Run("skip sync for token auth type", func(t *testing.T) {
|
||||
claims := jwtclaims.AuthorizationClaims{
|
||||
UserId: "user1",
|
||||
Raw: jwt.MapClaims{"groups": []interface{}{"group3"}, "is_token": true},
|
||||
}
|
||||
err = manager.syncJWTGroups(context.Background(), "accountID", claims)
|
||||
assert.NoError(t, err, "unable to sync jwt groups")
|
||||
|
||||
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
|
||||
assert.NoError(t, err, "unable to get user")
|
||||
assert.Len(t, user.AutoGroups, 0, "JWT groups should not be synced")
|
||||
})
|
||||
|
||||
t.Run("empty jwt groups", func(t *testing.T) {
|
||||
claims := jwtclaims.AuthorizationClaims{
|
||||
UserId: "user1",
|
||||
@@ -2835,7 +2822,7 @@ func TestAccount_SetJWTGroups(t *testing.T) {
|
||||
assert.Len(t, user.AutoGroups, 1, "new group should be added")
|
||||
})
|
||||
|
||||
t.Run("remove all JWT groups when list is empty", func(t *testing.T) {
|
||||
t.Run("remove all JWT groups", func(t *testing.T) {
|
||||
claims := jwtclaims.AuthorizationClaims{
|
||||
UserId: "user1",
|
||||
Raw: jwt.MapClaims{"groups": []interface{}{}},
|
||||
@@ -2846,20 +2833,7 @@ func TestAccount_SetJWTGroups(t *testing.T) {
|
||||
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
|
||||
assert.NoError(t, err, "unable to get user")
|
||||
assert.Len(t, user.AutoGroups, 1, "only non-JWT groups should remain")
|
||||
assert.Contains(t, user.AutoGroups, "group1", "group1 should still be present")
|
||||
})
|
||||
|
||||
t.Run("remove all JWT groups when claim does not exist", func(t *testing.T) {
|
||||
claims := jwtclaims.AuthorizationClaims{
|
||||
UserId: "user2",
|
||||
Raw: jwt.MapClaims{},
|
||||
}
|
||||
err = manager.syncJWTGroups(context.Background(), "accountID", claims)
|
||||
assert.NoError(t, err, "unable to sync jwt groups")
|
||||
|
||||
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user2")
|
||||
assert.NoError(t, err, "unable to get user")
|
||||
assert.Len(t, user.AutoGroups, 0, "all JWT groups should be removed")
|
||||
assert.Contains(t, user.AutoGroups, "group1", " group1 should still be present")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3063,9 +3037,9 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) {
|
||||
minMsPerOpCICD float64
|
||||
maxMsPerOpCICD float64
|
||||
}{
|
||||
{"Small", 50, 5, 1, 3, 3, 11},
|
||||
{"Small", 50, 5, 1, 3, 3, 10},
|
||||
{"Medium", 500, 100, 7, 13, 10, 70},
|
||||
{"Large", 5000, 200, 65, 80, 60, 220},
|
||||
{"Large", 5000, 200, 65, 80, 60, 200},
|
||||
{"Small single", 50, 10, 1, 3, 3, 70},
|
||||
{"Medium single", 500, 10, 7, 13, 10, 26},
|
||||
{"Large 5", 5000, 15, 65, 80, 60, 200},
|
||||
@@ -3205,7 +3179,7 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) {
|
||||
maxMsPerOpCICD float64
|
||||
}{
|
||||
{"Small", 50, 5, 107, 120, 107, 160},
|
||||
{"Medium", 500, 100, 105, 140, 105, 220},
|
||||
{"Medium", 500, 100, 105, 140, 105, 190},
|
||||
{"Large", 5000, 200, 180, 220, 180, 350},
|
||||
{"Small single", 50, 10, 107, 120, 105, 160},
|
||||
{"Medium single", 500, 10, 105, 140, 105, 170},
|
||||
|
||||
@@ -474,10 +474,6 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty
|
||||
return status.Errorf(status.InvalidArgument, "deleting group ALL is not allowed")
|
||||
}
|
||||
|
||||
if len(group.Resources) > 0 {
|
||||
return &GroupLinkError{"network resource", group.Resources[0].ID}
|
||||
}
|
||||
|
||||
if isLinked, linkedRoute := isGroupLinkedToRoute(ctx, transaction, group.AccountID, group.ID); isLinked {
|
||||
return &GroupLinkError{"route", string(linkedRoute.NetID)}
|
||||
}
|
||||
@@ -533,10 +529,7 @@ func isGroupLinkedToRoute(ctx context.Context, transaction store.Store, accountI
|
||||
}
|
||||
|
||||
for _, r := range routes {
|
||||
isLinked := slices.Contains(r.Groups, groupID) ||
|
||||
slices.Contains(r.PeerGroups, groupID) ||
|
||||
slices.Contains(r.AccessControlGroups, groupID)
|
||||
if isLinked {
|
||||
if slices.Contains(r.Groups, groupID) || slices.Contains(r.PeerGroups, groupID) {
|
||||
return true, r
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,6 +725,10 @@ components:
|
||||
PolicyRuleMinimum:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Policy rule ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7mg
|
||||
name:
|
||||
description: Policy rule name identifier
|
||||
type: string
|
||||
@@ -786,31 +790,6 @@ components:
|
||||
- end
|
||||
|
||||
PolicyRuleUpdate:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PolicyRuleMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
id:
|
||||
description: Policy rule ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7mg
|
||||
sources:
|
||||
description: Policy rule source group IDs
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: "ch8i4ug6lnn4g9hqv797"
|
||||
destinations:
|
||||
description: Policy rule destination group IDs
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: "ch8i4ug6lnn4g9h7v7m0"
|
||||
required:
|
||||
- sources
|
||||
- destinations
|
||||
|
||||
PolicyRuleCreate:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PolicyRuleMinimum'
|
||||
- type: object
|
||||
@@ -838,10 +817,6 @@ components:
|
||||
- $ref: '#/components/schemas/PolicyRuleMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
id:
|
||||
description: Policy rule ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7mg
|
||||
sources:
|
||||
description: Policy rule source group IDs
|
||||
type: array
|
||||
@@ -861,6 +836,10 @@ components:
|
||||
PolicyMinimum:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Policy ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7mg
|
||||
name:
|
||||
description: Policy name identifier
|
||||
type: string
|
||||
@@ -875,6 +854,7 @@ components:
|
||||
example: true
|
||||
required:
|
||||
- name
|
||||
- description
|
||||
- enabled
|
||||
PolicyUpdate:
|
||||
allOf:
|
||||
@@ -894,33 +874,11 @@ components:
|
||||
$ref: '#/components/schemas/PolicyRuleUpdate'
|
||||
required:
|
||||
- rules
|
||||
PolicyCreate:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PolicyMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
source_posture_checks:
|
||||
description: Posture checks ID's applied to policy source groups
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: "chacdk86lnnboviihd70"
|
||||
rules:
|
||||
description: Policy rule object for policy UI editor
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PolicyRuleUpdate'
|
||||
required:
|
||||
- rules
|
||||
Policy:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PolicyMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
id:
|
||||
description: Policy ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7mg
|
||||
source_posture_checks:
|
||||
description: Posture checks ID's applied to policy source groups
|
||||
type: array
|
||||
@@ -2505,7 +2463,7 @@ paths:
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/PolicyCreate'
|
||||
$ref: '#/components/schemas/PolicyUpdate'
|
||||
responses:
|
||||
'200':
|
||||
description: A Policy object
|
||||
|
||||
@@ -879,7 +879,7 @@ type PersonalAccessTokenRequest struct {
|
||||
// Policy defines model for Policy.
|
||||
type Policy struct {
|
||||
// Description Policy friendly description
|
||||
Description *string `json:"description,omitempty"`
|
||||
Description string `json:"description"`
|
||||
|
||||
// Enabled Policy status
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -897,32 +897,17 @@ type Policy struct {
|
||||
SourcePostureChecks []string `json:"source_posture_checks"`
|
||||
}
|
||||
|
||||
// PolicyCreate defines model for PolicyCreate.
|
||||
type PolicyCreate struct {
|
||||
// Description Policy friendly description
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// Enabled Policy status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Name Policy name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
// Rules Policy rule object for policy UI editor
|
||||
Rules []PolicyRuleUpdate `json:"rules"`
|
||||
|
||||
// SourcePostureChecks Posture checks ID's applied to policy source groups
|
||||
SourcePostureChecks *[]string `json:"source_posture_checks,omitempty"`
|
||||
}
|
||||
|
||||
// PolicyMinimum defines model for PolicyMinimum.
|
||||
type PolicyMinimum struct {
|
||||
// Description Policy friendly description
|
||||
Description *string `json:"description,omitempty"`
|
||||
Description string `json:"description"`
|
||||
|
||||
// Enabled Policy status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Id Policy ID
|
||||
Id *string `json:"id,omitempty"`
|
||||
|
||||
// Name Policy name identifier
|
||||
Name string `json:"name"`
|
||||
}
|
||||
@@ -985,6 +970,9 @@ type PolicyRuleMinimum struct {
|
||||
// Enabled Policy rule status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Id Policy rule ID
|
||||
Id *string `json:"id,omitempty"`
|
||||
|
||||
// Name Policy rule name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -1051,11 +1039,14 @@ type PolicyRuleUpdateProtocol string
|
||||
// PolicyUpdate defines model for PolicyUpdate.
|
||||
type PolicyUpdate struct {
|
||||
// Description Policy friendly description
|
||||
Description *string `json:"description,omitempty"`
|
||||
Description string `json:"description"`
|
||||
|
||||
// Enabled Policy status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Id Policy ID
|
||||
Id *string `json:"id,omitempty"`
|
||||
|
||||
// Name Policy name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -1482,7 +1473,7 @@ type PutApiPeersPeerIdJSONRequestBody = PeerRequest
|
||||
type PostApiPoliciesJSONRequestBody = PolicyUpdate
|
||||
|
||||
// PutApiPoliciesPolicyIdJSONRequestBody defines body for PutApiPoliciesPolicyId for application/json ContentType.
|
||||
type PutApiPoliciesPolicyIdJSONRequestBody = PolicyCreate
|
||||
type PutApiPoliciesPolicyIdJSONRequestBody = PolicyUpdate
|
||||
|
||||
// PostApiPostureChecksJSONRequestBody defines body for PostApiPostureChecks for application/json ContentType.
|
||||
type PostApiPostureChecksJSONRequestBody = PostureCheckUpdate
|
||||
|
||||
@@ -133,21 +133,16 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s
|
||||
return
|
||||
}
|
||||
|
||||
description := ""
|
||||
if req.Description != nil {
|
||||
description = *req.Description
|
||||
}
|
||||
|
||||
policy := &types.Policy{
|
||||
ID: policyID,
|
||||
AccountID: accountID,
|
||||
Name: req.Name,
|
||||
Enabled: req.Enabled,
|
||||
Description: description,
|
||||
Description: req.Description,
|
||||
}
|
||||
for _, rule := range req.Rules {
|
||||
var ruleID string
|
||||
if rule.Id != nil && policyID != "" {
|
||||
if rule.Id != nil {
|
||||
ruleID = *rule.Id
|
||||
}
|
||||
|
||||
@@ -375,7 +370,7 @@ func toPolicyResponse(groups []*types.Group, policy *types.Policy) *api.Policy {
|
||||
ap := &api.Policy{
|
||||
Id: &policy.ID,
|
||||
Name: policy.Name,
|
||||
Description: &policy.Description,
|
||||
Description: policy.Description,
|
||||
Enabled: policy.Enabled,
|
||||
SourcePostureChecks: policy.SourcePostureChecks,
|
||||
}
|
||||
|
||||
@@ -154,7 +154,6 @@ func TestPoliciesGetPolicy(t *testing.T) {
|
||||
|
||||
func TestPoliciesWritePolicy(t *testing.T) {
|
||||
str := func(s string) *string { return &s }
|
||||
emptyString := ""
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
@@ -185,9 +184,8 @@ func TestPoliciesWritePolicy(t *testing.T) {
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedPolicy: &api.Policy{
|
||||
Id: str("id-was-set"),
|
||||
Name: "Default POSTed Policy",
|
||||
Description: &emptyString,
|
||||
Id: str("id-was-set"),
|
||||
Name: "Default POSTed Policy",
|
||||
Rules: []api.PolicyRule{
|
||||
{
|
||||
Id: str("id-was-set"),
|
||||
@@ -234,9 +232,8 @@ func TestPoliciesWritePolicy(t *testing.T) {
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedPolicy: &api.Policy{
|
||||
Id: str("id-existed"),
|
||||
Name: "Default POSTed Policy",
|
||||
Description: &emptyString,
|
||||
Id: str("id-existed"),
|
||||
Name: "Default POSTed Policy",
|
||||
Rules: []api.PolicyRule{
|
||||
{
|
||||
Id: str("id-existed"),
|
||||
|
||||
@@ -175,7 +175,6 @@ func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Requ
|
||||
claimMaps[m.audience+jwtclaims.AccountIDSuffix] = account.Id
|
||||
claimMaps[m.audience+jwtclaims.DomainIDSuffix] = account.Domain
|
||||
claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = account.DomainCategory
|
||||
claimMaps[jwtclaims.IsToken] = true
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
|
||||
newRequest := r.WithContext(context.WithValue(r.Context(), jwtclaims.TokenUserProperty, jwtToken)) //nolint
|
||||
// Update the current request with the new context information.
|
||||
|
||||
@@ -22,8 +22,6 @@ const (
|
||||
LastLoginSuffix = "nb_last_login"
|
||||
// Invited claim indicates that an incoming JWT is from a user that just accepted an invitation
|
||||
Invited = "nb_invited"
|
||||
// IsToken claim indicates that auth type from the user is a token
|
||||
IsToken = "is_token"
|
||||
)
|
||||
|
||||
// ExtractClaims Extract function type
|
||||
|
||||
@@ -195,10 +195,6 @@ func (w *Worker) generateProperties(ctx context.Context) properties {
|
||||
groups int
|
||||
routes int
|
||||
routesWithRGGroups int
|
||||
networks int
|
||||
networkResources int
|
||||
networkRouters int
|
||||
networkRoutersWithPG int
|
||||
nameservers int
|
||||
uiClient int
|
||||
version string
|
||||
@@ -223,16 +219,6 @@ func (w *Worker) generateProperties(ctx context.Context) properties {
|
||||
}
|
||||
|
||||
groups += len(account.Groups)
|
||||
networks += len(account.Networks)
|
||||
networkResources += len(account.NetworkResources)
|
||||
|
||||
networkRouters += len(account.NetworkRouters)
|
||||
for _, router := range account.NetworkRouters {
|
||||
if len(router.PeerGroups) > 0 {
|
||||
networkRoutersWithPG++
|
||||
}
|
||||
}
|
||||
|
||||
routes += len(account.Routes)
|
||||
for _, route := range account.Routes {
|
||||
if len(route.PeerGroups) > 0 {
|
||||
@@ -326,10 +312,6 @@ func (w *Worker) generateProperties(ctx context.Context) properties {
|
||||
metricsProperties["rules_with_src_posture_checks"] = rulesWithSrcPostureChecks
|
||||
metricsProperties["posture_checks"] = postureChecks
|
||||
metricsProperties["groups"] = groups
|
||||
metricsProperties["networks"] = networks
|
||||
metricsProperties["network_resources"] = networkResources
|
||||
metricsProperties["network_routers"] = networkRouters
|
||||
metricsProperties["network_routers_with_groups"] = networkRoutersWithPG
|
||||
metricsProperties["routes"] = routes
|
||||
metricsProperties["routes_with_routing_groups"] = routesWithRGGroups
|
||||
metricsProperties["nameservers"] = nameservers
|
||||
|
||||
@@ -5,9 +5,6 @@ import (
|
||||
"testing"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
@@ -175,31 +172,6 @@ func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
|
||||
},
|
||||
},
|
||||
},
|
||||
Networks: []*networkTypes.Network{
|
||||
{
|
||||
ID: "1",
|
||||
AccountID: "1",
|
||||
},
|
||||
},
|
||||
NetworkResources: []*resourceTypes.NetworkResource{
|
||||
{
|
||||
ID: "1",
|
||||
AccountID: "1",
|
||||
NetworkID: "1",
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
AccountID: "1",
|
||||
NetworkID: "1",
|
||||
},
|
||||
},
|
||||
NetworkRouters: []*routerTypes.NetworkRouter{
|
||||
{
|
||||
ID: "1",
|
||||
AccountID: "1",
|
||||
NetworkID: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -228,15 +200,6 @@ func TestGenerateProperties(t *testing.T) {
|
||||
if properties["routes"] != 2 {
|
||||
t.Errorf("expected 2 routes, got %d", properties["routes"])
|
||||
}
|
||||
if properties["networks"] != 1 {
|
||||
t.Errorf("expected 1 networks, got %d", properties["networks"])
|
||||
}
|
||||
if properties["network_resources"] != 2 {
|
||||
t.Errorf("expected 2 network_resources, got %d", properties["network_resources"])
|
||||
}
|
||||
if properties["network_routers"] != 1 {
|
||||
t.Errorf("expected 1 network_routers, got %d", properties["network_routers"])
|
||||
}
|
||||
if properties["rules"] != 4 {
|
||||
t.Errorf("expected 4 rules, got %d", properties["rules"])
|
||||
}
|
||||
|
||||
@@ -111,7 +111,6 @@ func (n *NetworkResource) ToRoute(peer *nbpeer.Peer, router *routerTypes.Network
|
||||
NetID: route.NetID(n.Name),
|
||||
Description: n.Description,
|
||||
Peer: peer.Key,
|
||||
PeerID: peer.ID,
|
||||
PeerGroups: nil,
|
||||
Masquerade: router.Masquerade,
|
||||
Metric: router.Metric,
|
||||
|
||||
@@ -932,11 +932,11 @@ func BenchmarkUpdateAccountPeers(b *testing.B) {
|
||||
}{
|
||||
{"Small", 50, 5, 90, 120, 90, 120},
|
||||
{"Medium", 500, 100, 110, 150, 120, 260},
|
||||
{"Large", 5000, 200, 800, 1700, 2500, 5000},
|
||||
{"Large", 5000, 200, 800, 1390, 2500, 4600},
|
||||
{"Small single", 50, 10, 90, 120, 90, 120},
|
||||
{"Medium single", 500, 10, 110, 170, 120, 200},
|
||||
{"Large 5", 5000, 15, 1300, 2100, 4900, 7000},
|
||||
{"Extra Large", 2000, 2000, 1300, 2400, 4000, 6400},
|
||||
{"Large 5", 5000, 15, 1300, 2100, 5000, 7000},
|
||||
{"Extra Large", 2000, 2000, 1300, 2100, 4000, 6000},
|
||||
}
|
||||
|
||||
log.SetOutput(io.Discard)
|
||||
|
||||
@@ -74,19 +74,6 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
|
||||
"peerH",
|
||||
},
|
||||
},
|
||||
"GroupWorkstations": {
|
||||
ID: "GroupWorkstations",
|
||||
Name: "GroupWorkstations",
|
||||
Peers: []string{
|
||||
"peerB",
|
||||
"peerA",
|
||||
"peerD",
|
||||
"peerE",
|
||||
"peerF",
|
||||
"peerG",
|
||||
"peerH",
|
||||
},
|
||||
},
|
||||
"GroupSwarm": {
|
||||
ID: "GroupSwarm",
|
||||
Name: "swarm",
|
||||
@@ -140,7 +127,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
|
||||
Action: types.PolicyTrafficActionAccept,
|
||||
Sources: []string{
|
||||
"GroupSwarm",
|
||||
"GroupWorkstations",
|
||||
"GroupAll",
|
||||
},
|
||||
Destinations: []string{
|
||||
"GroupSwarm",
|
||||
@@ -172,8 +159,6 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
|
||||
assert.Contains(t, peers, account.Peers["peerD"])
|
||||
assert.Contains(t, peers, account.Peers["peerE"])
|
||||
assert.Contains(t, peers, account.Peers["peerF"])
|
||||
assert.Contains(t, peers, account.Peers["peerG"])
|
||||
assert.Contains(t, peers, account.Peers["peerH"])
|
||||
|
||||
epectedFirewallRules := []*types.FirewallRule{
|
||||
{
|
||||
@@ -204,6 +189,21 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
|
||||
Protocol: "all",
|
||||
Port: "",
|
||||
},
|
||||
{
|
||||
PeerIP: "100.65.254.139",
|
||||
Direction: types.FirewallRuleDirectionOUT,
|
||||
Action: "accept",
|
||||
Protocol: "all",
|
||||
Port: "",
|
||||
},
|
||||
{
|
||||
PeerIP: "100.65.254.139",
|
||||
Direction: types.FirewallRuleDirectionIN,
|
||||
Action: "accept",
|
||||
Protocol: "all",
|
||||
Port: "",
|
||||
},
|
||||
|
||||
{
|
||||
PeerIP: "100.65.62.5",
|
||||
Direction: types.FirewallRuleDirectionOUT,
|
||||
@@ -280,16 +280,10 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
|
||||
},
|
||||
}
|
||||
assert.Len(t, firewallRules, len(epectedFirewallRules))
|
||||
|
||||
for _, rule := range firewallRules {
|
||||
contains := false
|
||||
for _, expectedRule := range epectedFirewallRules {
|
||||
if rule.IsEqual(expectedRule) {
|
||||
contains = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, contains, "rule not found in expected rules %#v", rule)
|
||||
slices.SortFunc(epectedFirewallRules, sortFunc())
|
||||
slices.SortFunc(firewallRules, sortFunc())
|
||||
for i := range firewallRules {
|
||||
assert.Equal(t, epectedFirewallRules[i], firewallRules[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ func toProtocolRoute(route *route.Route) *proto.Route {
|
||||
}
|
||||
|
||||
func toProtocolRoutes(routes []*route.Route) []*proto.Route {
|
||||
protoRoutes := make([]*proto.Route, 0, len(routes))
|
||||
protoRoutes := make([]*proto.Route, 0)
|
||||
for _, r := range routes {
|
||||
protoRoutes = append(protoRoutes, toProtocolRoute(r))
|
||||
}
|
||||
|
||||
@@ -303,47 +303,55 @@ func (a *Account) GetPeerNetworkMap(
|
||||
return nm
|
||||
}
|
||||
|
||||
func (a *Account) addNetworksRoutingPeers(
|
||||
networkResourcesRoutes []*route.Route,
|
||||
peer *nbpeer.Peer,
|
||||
peersToConnect []*nbpeer.Peer,
|
||||
expiredPeers []*nbpeer.Peer,
|
||||
isRouter bool,
|
||||
sourcePeers map[string]struct{},
|
||||
) []*nbpeer.Peer {
|
||||
|
||||
networkRoutesPeers := make(map[string]struct{}, len(networkResourcesRoutes))
|
||||
func (a *Account) addNetworksRoutingPeers(networkResourcesRoutes []*route.Route, peer *nbpeer.Peer, peersToConnect []*nbpeer.Peer, expiredPeers []*nbpeer.Peer, isRouter bool, sourcePeers []string) []*nbpeer.Peer {
|
||||
missingPeers := map[string]struct{}{}
|
||||
for _, r := range networkResourcesRoutes {
|
||||
networkRoutesPeers[r.PeerID] = struct{}{}
|
||||
}
|
||||
if r.Peer == peer.Key {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(sourcePeers, peer.ID)
|
||||
|
||||
for _, existingPeer := range peersToConnect {
|
||||
delete(sourcePeers, existingPeer.ID)
|
||||
delete(networkRoutesPeers, existingPeer.ID)
|
||||
}
|
||||
for _, expPeer := range expiredPeers {
|
||||
delete(sourcePeers, expPeer.ID)
|
||||
delete(networkRoutesPeers, expPeer.ID)
|
||||
}
|
||||
|
||||
missingPeers := make(map[string]struct{}, len(sourcePeers)+len(networkRoutesPeers))
|
||||
if isRouter {
|
||||
for p := range sourcePeers {
|
||||
missingPeers[p] = struct{}{}
|
||||
missing := true
|
||||
for _, p := range slices.Concat(peersToConnect, expiredPeers) {
|
||||
if r.Peer == p.Key {
|
||||
missing = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if missing {
|
||||
missingPeers[r.Peer] = struct{}{}
|
||||
}
|
||||
}
|
||||
for p := range networkRoutesPeers {
|
||||
missingPeers[p] = struct{}{}
|
||||
|
||||
if isRouter {
|
||||
for _, s := range sourcePeers {
|
||||
if s == peer.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
missing := true
|
||||
for _, p := range slices.Concat(peersToConnect, expiredPeers) {
|
||||
if s == p.ID {
|
||||
missing = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if missing {
|
||||
p, ok := a.Peers[s]
|
||||
if ok {
|
||||
missingPeers[p.Key] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for p := range missingPeers {
|
||||
if missingPeer := a.Peers[p]; missingPeer != nil {
|
||||
peersToConnect = append(peersToConnect, missingPeer)
|
||||
for _, p2 := range a.Peers {
|
||||
if p2.Key == p {
|
||||
peersToConnect = append(peersToConnect, p2)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return peersToConnect
|
||||
}
|
||||
|
||||
@@ -1037,32 +1045,37 @@ func (a *Account) connResourcesGenerator(ctx context.Context) (func(*PolicyRule,
|
||||
// for destination group peers, call this method with an empty list of sourcePostureChecksIDs
|
||||
func (a *Account) getAllPeersFromGroups(ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, bool) {
|
||||
peerInGroups := false
|
||||
uniquePeerIDs := a.getUniquePeerIDsFromGroupsIDs(ctx, groups)
|
||||
filteredPeers := make([]*nbpeer.Peer, 0, len(uniquePeerIDs))
|
||||
for _, p := range uniquePeerIDs {
|
||||
peer, ok := a.Peers[p]
|
||||
if !ok || peer == nil {
|
||||
filteredPeers := make([]*nbpeer.Peer, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
group, ok := a.Groups[g]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// validate the peer based on policy posture checks applied
|
||||
isValid := a.validatePostureChecksOnPeer(ctx, sourcePostureChecksIDs, peer.ID)
|
||||
if !isValid {
|
||||
continue
|
||||
}
|
||||
for _, p := range group.Peers {
|
||||
peer, ok := a.Peers[p]
|
||||
if !ok || peer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := validatedPeersMap[peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
// validate the peer based on policy posture checks applied
|
||||
isValid := a.validatePostureChecksOnPeer(ctx, sourcePostureChecksIDs, peer.ID)
|
||||
if !isValid {
|
||||
continue
|
||||
}
|
||||
|
||||
if peer.ID == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
if _, ok := validatedPeersMap[peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeers = append(filteredPeers, peer)
|
||||
if peer.ID == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeers = append(filteredPeers, peer)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPeers, peerInGroups
|
||||
}
|
||||
|
||||
@@ -1138,7 +1151,7 @@ func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, poli
|
||||
continue
|
||||
}
|
||||
|
||||
rulePeers := a.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers, validatedPeersMap)
|
||||
rulePeers := a.getRulePeers(rule, peerID, distributionPeers, validatedPeersMap)
|
||||
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN)
|
||||
fwRules = append(fwRules, rules...)
|
||||
}
|
||||
@@ -1146,7 +1159,7 @@ func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, poli
|
||||
return fwRules
|
||||
}
|
||||
|
||||
func (a *Account) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}, validatedPeersMap map[string]struct{}) []*nbpeer.Peer {
|
||||
func (a *Account) getRulePeers(rule *PolicyRule, peerID string, distributionPeers map[string]struct{}, validatedPeersMap map[string]struct{}) []*nbpeer.Peer {
|
||||
distPeersWithPolicy := make(map[string]struct{})
|
||||
for _, id := range rule.Sources {
|
||||
group := a.Groups[id]
|
||||
@@ -1160,7 +1173,7 @@ func (a *Account) getRulePeers(rule *PolicyRule, postureChecks []string, peerID
|
||||
}
|
||||
_, distPeer := distributionPeers[pID]
|
||||
_, valid := validatedPeersMap[pID]
|
||||
if distPeer && valid && a.validatePostureChecksOnPeer(context.Background(), postureChecks, pID) {
|
||||
if distPeer && valid {
|
||||
distPeersWithPolicy[pID] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -1258,11 +1271,7 @@ func (a *Account) GetPeerNetworkResourceFirewallRules(ctx context.Context, peer
|
||||
distributionPeers := getPoliciesSourcePeers(resourceAppliedPolicies, a.Groups)
|
||||
|
||||
rules := a.getRouteFirewallRules(ctx, peer.ID, resourceAppliedPolicies, route, validatedPeersMap, distributionPeers)
|
||||
for _, rule := range rules {
|
||||
if len(rule.SourceRanges) > 0 {
|
||||
routesFirewallRules = append(routesFirewallRules, rule)
|
||||
}
|
||||
}
|
||||
routesFirewallRules = append(routesFirewallRules, rules...)
|
||||
}
|
||||
|
||||
return routesFirewallRules
|
||||
@@ -1294,10 +1303,10 @@ func (a *Account) GetResourcePoliciesMap() map[string][]*Policy {
|
||||
}
|
||||
|
||||
// GetNetworkResourcesRoutesToSync returns network routes for syncing with a specific peer and its ACL peers.
|
||||
func (a *Account) GetNetworkResourcesRoutesToSync(ctx context.Context, peerID string, resourcePolicies map[string][]*Policy, routers map[string]map[string]*routerTypes.NetworkRouter) (bool, []*route.Route, map[string]struct{}) {
|
||||
func (a *Account) GetNetworkResourcesRoutesToSync(ctx context.Context, peerID string, resourcePolicies map[string][]*Policy, routers map[string]map[string]*routerTypes.NetworkRouter) (bool, []*route.Route, []string) {
|
||||
var isRoutingPeer bool
|
||||
var routes []*route.Route
|
||||
allSourcePeers := make(map[string]struct{}, len(a.Peers))
|
||||
var allSourcePeers []string
|
||||
|
||||
for _, resource := range a.NetworkResources {
|
||||
var addSourcePeers bool
|
||||
@@ -1310,22 +1319,23 @@ func (a *Account) GetNetworkResourcesRoutesToSync(ctx context.Context, peerID st
|
||||
}
|
||||
}
|
||||
|
||||
addedResourceRoute := false
|
||||
for _, policy := range resourcePolicies[resource.ID] {
|
||||
peers := a.getUniquePeerIDsFromGroupsIDs(ctx, policy.SourceGroups())
|
||||
if addSourcePeers {
|
||||
for _, pID := range a.getPostureValidPeers(peers, policy.SourcePostureChecks) {
|
||||
allSourcePeers[pID] = struct{}{}
|
||||
for _, sourceGroup := range policy.SourceGroups() {
|
||||
group := a.GetGroup(sourceGroup)
|
||||
if group == nil {
|
||||
log.WithContext(ctx).Warnf("policy %s has source group %s that doesn't exist under account %s, will continue map generation without it", policy.ID, sourceGroup, a.Id)
|
||||
continue
|
||||
}
|
||||
} else if slices.Contains(peers, peerID) && a.validatePostureChecksOnPeer(ctx, policy.SourcePostureChecks, peerID) {
|
||||
// add routes for the resource if the peer is in the distribution group
|
||||
for peerId, router := range networkRoutingPeers {
|
||||
routes = append(routes, a.getNetworkResourcesRoutes(resource, peerId, router, resourcePolicies)...)
|
||||
|
||||
// routing peer should be able to connect with all source peers
|
||||
if addSourcePeers {
|
||||
allSourcePeers = append(allSourcePeers, group.Peers...)
|
||||
} else if slices.Contains(group.Peers, peerID) {
|
||||
// add routes for the resource if the peer is in the distribution group
|
||||
for peerId, router := range networkRoutingPeers {
|
||||
routes = append(routes, a.getNetworkResourcesRoutes(resource, peerId, router, resourcePolicies)...)
|
||||
}
|
||||
}
|
||||
addedResourceRoute = true
|
||||
}
|
||||
if addedResourceRoute {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1333,42 +1343,6 @@ func (a *Account) GetNetworkResourcesRoutesToSync(ctx context.Context, peerID st
|
||||
return isRoutingPeer, routes, allSourcePeers
|
||||
}
|
||||
|
||||
func (a *Account) getPostureValidPeers(inputPeers []string, postureChecksIDs []string) []string {
|
||||
var dest []string
|
||||
for _, peerID := range inputPeers {
|
||||
if a.validatePostureChecksOnPeer(context.Background(), postureChecksIDs, peerID) {
|
||||
dest = append(dest, peerID)
|
||||
}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
func (a *Account) getUniquePeerIDsFromGroupsIDs(ctx context.Context, groups []string) []string {
|
||||
peerIDs := make(map[string]struct{}, len(groups)) // we expect at least one peer per group as initial capacity
|
||||
for _, groupID := range groups {
|
||||
group := a.GetGroup(groupID)
|
||||
if group == nil {
|
||||
log.WithContext(ctx).Warnf("group %s doesn't exist under account %s, will continue map generation without it", groupID, a.Id)
|
||||
continue
|
||||
}
|
||||
|
||||
if group.IsGroupAll() || len(groups) == 1 {
|
||||
return group.Peers
|
||||
}
|
||||
|
||||
for _, peerID := range group.Peers {
|
||||
peerIDs[peerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(peerIDs))
|
||||
for peerID := range peerIDs {
|
||||
ids = append(ids, peerID)
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// getNetworkResources filters and returns a list of network resources associated with the given network ID.
|
||||
func (a *Account) getNetworkResources(networkID string) []*resourceTypes.NetworkResource {
|
||||
var resources []*resourceTypes.NetworkResource
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
)
|
||||
|
||||
@@ -316,19 +310,19 @@ func Test_GetResourcePoliciesMap(t *testing.T) {
|
||||
|
||||
func Test_AddNetworksRoutingPeersAddsMissingPeers(t *testing.T) {
|
||||
account := setupTestAccount()
|
||||
peer := &nbpeer.Peer{Key: "peer1Key", ID: "peer1"}
|
||||
peer := &nbpeer.Peer{Key: "peer1"}
|
||||
networkResourcesRoutes := []*route.Route{
|
||||
{Peer: "peer2Key", PeerID: "peer2"},
|
||||
{Peer: "peer3Key", PeerID: "peer3"},
|
||||
{Peer: "peer2Key"},
|
||||
{Peer: "peer3Key"},
|
||||
}
|
||||
peersToConnect := []*nbpeer.Peer{
|
||||
{Key: "peer2Key", ID: "peer2"},
|
||||
{Key: "peer2Key"},
|
||||
}
|
||||
expiredPeers := []*nbpeer.Peer{
|
||||
{Key: "peer4Key", ID: "peer4"},
|
||||
{Key: "peer4Key"},
|
||||
}
|
||||
|
||||
result := account.addNetworksRoutingPeers(networkResourcesRoutes, peer, peersToConnect, expiredPeers, false, map[string]struct{}{})
|
||||
result := account.addNetworksRoutingPeers(networkResourcesRoutes, peer, peersToConnect, expiredPeers, false, []string{})
|
||||
require.Len(t, result, 2)
|
||||
require.Equal(t, "peer2Key", result[0].Key)
|
||||
require.Equal(t, "peer3Key", result[1].Key)
|
||||
@@ -345,7 +339,7 @@ func Test_AddNetworksRoutingPeersIgnoresExistingPeers(t *testing.T) {
|
||||
}
|
||||
expiredPeers := []*nbpeer.Peer{}
|
||||
|
||||
result := account.addNetworksRoutingPeers(networkResourcesRoutes, peer, peersToConnect, expiredPeers, false, map[string]struct{}{})
|
||||
result := account.addNetworksRoutingPeers(networkResourcesRoutes, peer, peersToConnect, expiredPeers, false, []string{})
|
||||
require.Len(t, result, 1)
|
||||
require.Equal(t, "peer2Key", result[0].Key)
|
||||
}
|
||||
@@ -364,7 +358,7 @@ func Test_AddNetworksRoutingPeersAddsExpiredPeers(t *testing.T) {
|
||||
{Key: "peer3Key"},
|
||||
}
|
||||
|
||||
result := account.addNetworksRoutingPeers(networkResourcesRoutes, peer, peersToConnect, expiredPeers, false, map[string]struct{}{})
|
||||
result := account.addNetworksRoutingPeers(networkResourcesRoutes, peer, peersToConnect, expiredPeers, false, []string{})
|
||||
require.Len(t, result, 1)
|
||||
require.Equal(t, "peer2Key", result[0].Key)
|
||||
}
|
||||
@@ -376,382 +370,6 @@ func Test_AddNetworksRoutingPeersHandlesNoMissingPeers(t *testing.T) {
|
||||
peersToConnect := []*nbpeer.Peer{}
|
||||
expiredPeers := []*nbpeer.Peer{}
|
||||
|
||||
result := account.addNetworksRoutingPeers(networkResourcesRoutes, peer, peersToConnect, expiredPeers, false, map[string]struct{}{})
|
||||
result := account.addNetworksRoutingPeers(networkResourcesRoutes, peer, peersToConnect, expiredPeers, false, []string{})
|
||||
require.Len(t, result, 0)
|
||||
}
|
||||
|
||||
const (
|
||||
accID = "accountID"
|
||||
network1ID = "network1ID"
|
||||
group1ID = "group1"
|
||||
accNetResourcePeer1ID = "peer1"
|
||||
accNetResourcePeer2ID = "peer2"
|
||||
accNetResourceRouter1ID = "router1"
|
||||
accNetResource1ID = "resource1ID"
|
||||
accNetResourceRestrictPostureCheckID = "restrictPostureCheck"
|
||||
accNetResourceRelaxedPostureCheckID = "relaxedPostureCheck"
|
||||
accNetResourceLockedPostureCheckID = "lockedPostureCheck"
|
||||
accNetResourceLinuxPostureCheckID = "linuxPostureCheck"
|
||||
)
|
||||
|
||||
var (
|
||||
accNetResourcePeer1IP = net.IP{192, 168, 1, 1}
|
||||
accNetResourcePeer2IP = net.IP{192, 168, 1, 2}
|
||||
accNetResourceRouter1IP = net.IP{192, 168, 1, 3}
|
||||
accNetResourceValidPeers = map[string]struct{}{accNetResourcePeer1ID: {}, accNetResourcePeer2ID: {}}
|
||||
)
|
||||
|
||||
func getBasicAccountsWithResource() *Account {
|
||||
return &Account{
|
||||
Id: accID,
|
||||
Peers: map[string]*nbpeer.Peer{
|
||||
accNetResourcePeer1ID: {
|
||||
ID: accNetResourcePeer1ID,
|
||||
AccountID: accID,
|
||||
Key: "peer1Key",
|
||||
IP: accNetResourcePeer1IP,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
GoOS: "linux",
|
||||
WtVersion: "0.35.1",
|
||||
KernelVersion: "4.4.0",
|
||||
},
|
||||
},
|
||||
accNetResourcePeer2ID: {
|
||||
ID: accNetResourcePeer2ID,
|
||||
AccountID: accID,
|
||||
Key: "peer2Key",
|
||||
IP: accNetResourcePeer2IP,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
GoOS: "windows",
|
||||
WtVersion: "0.34.1",
|
||||
KernelVersion: "4.4.0",
|
||||
},
|
||||
},
|
||||
accNetResourceRouter1ID: {
|
||||
ID: accNetResourceRouter1ID,
|
||||
AccountID: accID,
|
||||
Key: "router1Key",
|
||||
IP: accNetResourceRouter1IP,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
GoOS: "linux",
|
||||
WtVersion: "0.35.1",
|
||||
KernelVersion: "4.4.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
Groups: map[string]*Group{
|
||||
group1ID: {
|
||||
ID: group1ID,
|
||||
Peers: []string{accNetResourcePeer1ID, accNetResourcePeer2ID},
|
||||
},
|
||||
},
|
||||
Networks: []*networkTypes.Network{
|
||||
{
|
||||
ID: network1ID,
|
||||
AccountID: accID,
|
||||
Name: "network1",
|
||||
},
|
||||
},
|
||||
NetworkRouters: []*routerTypes.NetworkRouter{
|
||||
{
|
||||
ID: accNetResourceRouter1ID,
|
||||
NetworkID: network1ID,
|
||||
AccountID: accID,
|
||||
Peer: accNetResourceRouter1ID,
|
||||
PeerGroups: []string{},
|
||||
Masquerade: false,
|
||||
Metric: 100,
|
||||
},
|
||||
},
|
||||
NetworkResources: []*resourceTypes.NetworkResource{
|
||||
{
|
||||
ID: accNetResource1ID,
|
||||
AccountID: accID,
|
||||
NetworkID: network1ID,
|
||||
Address: "10.10.10.0/24",
|
||||
Prefix: netip.MustParsePrefix("10.10.10.0/24"),
|
||||
Type: resourceTypes.NetworkResourceType("subnet"),
|
||||
},
|
||||
},
|
||||
Policies: []*Policy{
|
||||
{
|
||||
ID: "policy1ID",
|
||||
AccountID: accID,
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{
|
||||
{
|
||||
ID: "rule1ID",
|
||||
Enabled: true,
|
||||
Sources: []string{group1ID},
|
||||
DestinationResource: Resource{
|
||||
ID: accNetResource1ID,
|
||||
Type: "Host",
|
||||
},
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"80"},
|
||||
Action: PolicyTrafficActionAccept,
|
||||
},
|
||||
},
|
||||
SourcePostureChecks: nil,
|
||||
},
|
||||
},
|
||||
PostureChecks: []*posture.Checks{
|
||||
{
|
||||
ID: accNetResourceRestrictPostureCheckID,
|
||||
Name: accNetResourceRestrictPostureCheckID,
|
||||
Checks: posture.ChecksDefinition{
|
||||
NBVersionCheck: &posture.NBVersionCheck{
|
||||
MinVersion: "0.35.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: accNetResourceRelaxedPostureCheckID,
|
||||
Name: accNetResourceRelaxedPostureCheckID,
|
||||
Checks: posture.ChecksDefinition{
|
||||
NBVersionCheck: &posture.NBVersionCheck{
|
||||
MinVersion: "0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: accNetResourceLockedPostureCheckID,
|
||||
Name: accNetResourceLockedPostureCheckID,
|
||||
Checks: posture.ChecksDefinition{
|
||||
NBVersionCheck: &posture.NBVersionCheck{
|
||||
MinVersion: "7.7.7",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: accNetResourceLinuxPostureCheckID,
|
||||
Name: accNetResourceLinuxPostureCheckID,
|
||||
Checks: posture.ChecksDefinition{
|
||||
OSVersionCheck: &posture.OSVersionCheck{
|
||||
Linux: &posture.MinKernelVersionCheck{
|
||||
MinKernelVersion: "0.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NetworksNetMapGenWithNoPostureChecks(t *testing.T) {
|
||||
account := getBasicAccountsWithResource()
|
||||
|
||||
// all peers should match the policy
|
||||
|
||||
// validate for peer1
|
||||
isRouter, networkResourcesRoutes, sourcePeers := account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate for peer2
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer2ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate routes for router1
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourceRouter1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.True(t, isRouter, "should be router")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 2, "expected source peers don't match")
|
||||
assert.NotNil(t, sourcePeers[accNetResourcePeer1ID], "expected source peers don't match")
|
||||
assert.NotNil(t, sourcePeers[accNetResourcePeer2ID], "expected source peers don't match")
|
||||
|
||||
// validate rules for router1
|
||||
rules := account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers[accNetResourceRouter1ID], accNetResourceValidPeers, networkResourcesRoutes, account.GetResourcePoliciesMap())
|
||||
assert.Len(t, rules, 1, "expected rules count don't match")
|
||||
assert.Equal(t, uint16(80), rules[0].Port, "should have port 80")
|
||||
assert.Equal(t, "tcp", rules[0].Protocol, "should have protocol tcp")
|
||||
if !slices.Contains(rules[0].SourceRanges, accNetResourcePeer1IP.String()+"/32") {
|
||||
t.Errorf("%s should have source range of peer1 %s", rules[0].SourceRanges, accNetResourcePeer1IP.String())
|
||||
}
|
||||
if !slices.Contains(rules[0].SourceRanges, accNetResourcePeer2IP.String()+"/32") {
|
||||
t.Errorf("%s should have source range of peer2 %s", rules[0].SourceRanges, accNetResourcePeer2IP.String())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NetworksNetMapGenWithPostureChecks(t *testing.T) {
|
||||
account := getBasicAccountsWithResource()
|
||||
|
||||
// should allow peer1 to match the policy
|
||||
policy := account.Policies[0]
|
||||
policy.SourcePostureChecks = []string{accNetResourceRestrictPostureCheckID}
|
||||
|
||||
// validate for peer1
|
||||
isRouter, networkResourcesRoutes, sourcePeers := account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate for peer2
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer2ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 0, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate routes for router1
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourceRouter1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.True(t, isRouter, "should be router")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 1, "expected source peers don't match")
|
||||
assert.NotNil(t, sourcePeers[accNetResourcePeer1ID], "expected source peers don't match")
|
||||
|
||||
// validate rules for router1
|
||||
rules := account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers[accNetResourceRouter1ID], accNetResourceValidPeers, networkResourcesRoutes, account.GetResourcePoliciesMap())
|
||||
assert.Len(t, rules, 1, "expected rules count don't match")
|
||||
assert.Equal(t, uint16(80), rules[0].Port, "should have port 80")
|
||||
assert.Equal(t, "tcp", rules[0].Protocol, "should have protocol tcp")
|
||||
if !slices.Contains(rules[0].SourceRanges, accNetResourcePeer1IP.String()+"/32") {
|
||||
t.Errorf("%s should have source range of peer1 %s", rules[0].SourceRanges, accNetResourcePeer1IP.String())
|
||||
}
|
||||
if slices.Contains(rules[0].SourceRanges, accNetResourcePeer2IP.String()+"/32") {
|
||||
t.Errorf("%s should not have source range of peer2 %s", rules[0].SourceRanges, accNetResourcePeer2IP.String())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NetworksNetMapGenWithNoMatchedPostureChecks(t *testing.T) {
|
||||
account := getBasicAccountsWithResource()
|
||||
|
||||
// should not match any peer
|
||||
policy := account.Policies[0]
|
||||
policy.SourcePostureChecks = []string{accNetResourceLockedPostureCheckID}
|
||||
|
||||
// validate for peer1
|
||||
isRouter, networkResourcesRoutes, sourcePeers := account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 0, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate for peer2
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer2ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 0, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate routes for router1
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourceRouter1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.True(t, isRouter, "should be router")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate rules for router1
|
||||
rules := account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers[accNetResourceRouter1ID], accNetResourceValidPeers, networkResourcesRoutes, account.GetResourcePoliciesMap())
|
||||
assert.Len(t, rules, 0, "expected rules count don't match")
|
||||
}
|
||||
|
||||
func Test_NetworksNetMapGenWithTwoPoliciesAndPostureChecks(t *testing.T) {
|
||||
account := getBasicAccountsWithResource()
|
||||
|
||||
// should allow peer1 to match the policy
|
||||
policy := account.Policies[0]
|
||||
policy.SourcePostureChecks = []string{accNetResourceRestrictPostureCheckID}
|
||||
|
||||
// should allow peer1 and peer2 to match the policy
|
||||
newPolicy := &Policy{
|
||||
ID: "policy2ID",
|
||||
AccountID: accID,
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{
|
||||
{
|
||||
ID: "policy2ID",
|
||||
Enabled: true,
|
||||
Sources: []string{group1ID},
|
||||
DestinationResource: Resource{
|
||||
ID: accNetResource1ID,
|
||||
Type: "Host",
|
||||
},
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Ports: []string{"22"},
|
||||
Action: PolicyTrafficActionAccept,
|
||||
},
|
||||
},
|
||||
SourcePostureChecks: []string{accNetResourceRelaxedPostureCheckID},
|
||||
}
|
||||
|
||||
account.Policies = append(account.Policies, newPolicy)
|
||||
|
||||
// validate for peer1
|
||||
isRouter, networkResourcesRoutes, sourcePeers := account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate for peer2
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer2ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate routes for router1
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourceRouter1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.True(t, isRouter, "should be router")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 2, "expected source peers don't match")
|
||||
assert.NotNil(t, sourcePeers[accNetResourcePeer1ID], "expected source peers don't match")
|
||||
assert.NotNil(t, sourcePeers[accNetResourcePeer2ID], "expected source peers don't match")
|
||||
|
||||
// validate rules for router1
|
||||
rules := account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers[accNetResourceRouter1ID], accNetResourceValidPeers, networkResourcesRoutes, account.GetResourcePoliciesMap())
|
||||
assert.Len(t, rules, 2, "expected rules count don't match")
|
||||
assert.Equal(t, uint16(80), rules[0].Port, "should have port 80")
|
||||
assert.Equal(t, "tcp", rules[0].Protocol, "should have protocol tcp")
|
||||
if !slices.Contains(rules[0].SourceRanges, accNetResourcePeer1IP.String()+"/32") {
|
||||
t.Errorf("%s should have source range of peer1 %s", rules[0].SourceRanges, accNetResourcePeer1IP.String())
|
||||
}
|
||||
if slices.Contains(rules[0].SourceRanges, accNetResourcePeer2IP.String()+"/32") {
|
||||
t.Errorf("%s should not have source range of peer2 %s", rules[0].SourceRanges, accNetResourcePeer2IP.String())
|
||||
}
|
||||
|
||||
assert.Equal(t, uint16(22), rules[1].Port, "should have port 22")
|
||||
assert.Equal(t, "tcp", rules[1].Protocol, "should have protocol tcp")
|
||||
if !slices.Contains(rules[1].SourceRanges, accNetResourcePeer1IP.String()+"/32") {
|
||||
t.Errorf("%s should have source range of peer1 %s", rules[1].SourceRanges, accNetResourcePeer1IP.String())
|
||||
}
|
||||
if !slices.Contains(rules[1].SourceRanges, accNetResourcePeer2IP.String()+"/32") {
|
||||
t.Errorf("%s should have source range of peer2 %s", rules[1].SourceRanges, accNetResourcePeer2IP.String())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NetworksNetMapGenWithTwoPostureChecks(t *testing.T) {
|
||||
account := getBasicAccountsWithResource()
|
||||
|
||||
// two posture checks should match only the peers that match both checks
|
||||
policy := account.Policies[0]
|
||||
policy.SourcePostureChecks = []string{accNetResourceRelaxedPostureCheckID, accNetResourceLinuxPostureCheckID}
|
||||
|
||||
// validate for peer1
|
||||
isRouter, networkResourcesRoutes, sourcePeers := account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate for peer2
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourcePeer2ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.False(t, isRouter, "expected router status")
|
||||
assert.Len(t, networkResourcesRoutes, 0, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 0, "expected source peers don't match")
|
||||
|
||||
// validate routes for router1
|
||||
isRouter, networkResourcesRoutes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), accNetResourceRouter1ID, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap())
|
||||
assert.True(t, isRouter, "should be router")
|
||||
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
|
||||
assert.Len(t, sourcePeers, 1, "expected source peers don't match")
|
||||
assert.NotNil(t, sourcePeers[accNetResourcePeer1ID], "expected source peers don't match")
|
||||
|
||||
// validate rules for router1
|
||||
rules := account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers[accNetResourceRouter1ID], accNetResourceValidPeers, networkResourcesRoutes, account.GetResourcePoliciesMap())
|
||||
assert.Len(t, rules, 1, "expected rules count don't match")
|
||||
assert.Equal(t, uint16(80), rules[0].Port, "should have port 80")
|
||||
assert.Equal(t, "tcp", rules[0].Protocol, "should have protocol tcp")
|
||||
if !slices.Contains(rules[0].SourceRanges, accNetResourcePeer1IP.String()+"/32") {
|
||||
t.Errorf("%s should have source range of peer1 %s", rules[0].SourceRanges, accNetResourcePeer1IP.String())
|
||||
}
|
||||
if slices.Contains(rules[0].SourceRanges, accNetResourcePeer2IP.String()+"/32") {
|
||||
t.Errorf("%s should not have source range of peer2 %s", rules[0].SourceRanges, accNetResourcePeer2IP.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,15 +35,6 @@ type FirewallRule struct {
|
||||
Port string
|
||||
}
|
||||
|
||||
// IsEqual checks if two firewall rules are equal.
|
||||
func (r *FirewallRule) IsEqual(other *FirewallRule) bool {
|
||||
return r.PeerIP == other.PeerIP &&
|
||||
r.Direction == other.Direction &&
|
||||
r.Action == other.Action &&
|
||||
r.Protocol == other.Protocol &&
|
||||
r.Port == other.Port
|
||||
}
|
||||
|
||||
// generateRouteFirewallRules generates a list of firewall rules for a given route.
|
||||
func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) []*RouteFirewallRule {
|
||||
rulesExists := make(map[string]struct{})
|
||||
|
||||
@@ -117,20 +117,9 @@ func (p *Policy) RuleGroups() []string {
|
||||
|
||||
// SourceGroups returns a slice of all unique source groups referenced in the policy's rules.
|
||||
func (p *Policy) SourceGroups() []string {
|
||||
if len(p.Rules) == 1 {
|
||||
return p.Rules[0].Sources
|
||||
}
|
||||
groups := make(map[string]struct{}, len(p.Rules))
|
||||
groups := make([]string, 0)
|
||||
for _, rule := range p.Rules {
|
||||
for _, source := range rule.Sources {
|
||||
groups[source] = struct{}{}
|
||||
}
|
||||
groups = append(groups, rule.Sources...)
|
||||
}
|
||||
|
||||
groupIDs := make([]string, 0, len(groups))
|
||||
for groupID := range groups {
|
||||
groupIDs = append(groupIDs, groupID)
|
||||
}
|
||||
|
||||
return groupIDs
|
||||
return groups
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ type Route struct {
|
||||
NetID NetID
|
||||
Description string
|
||||
Peer string
|
||||
PeerID string `gorm:"-"`
|
||||
PeerGroups []string `gorm:"serializer:json"`
|
||||
NetworkType NetworkType
|
||||
Masquerade bool
|
||||
@@ -121,7 +120,6 @@ func (r *Route) Copy() *Route {
|
||||
KeepRoute: r.KeepRoute,
|
||||
NetworkType: r.NetworkType,
|
||||
Peer: r.Peer,
|
||||
PeerID: r.PeerID,
|
||||
PeerGroups: slices.Clone(r.PeerGroups),
|
||||
Metric: r.Metric,
|
||||
Masquerade: r.Masquerade,
|
||||
@@ -148,7 +146,6 @@ func (r *Route) IsEqual(other *Route) bool {
|
||||
other.KeepRoute == r.KeepRoute &&
|
||||
other.NetworkType == r.NetworkType &&
|
||||
other.Peer == r.Peer &&
|
||||
other.PeerID == r.PeerID &&
|
||||
other.Metric == r.Metric &&
|
||||
other.Masquerade == r.Masquerade &&
|
||||
other.Enabled == r.Enabled &&
|
||||
|
||||
Reference in New Issue
Block a user