mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 11:39:57 +00:00
292 lines
7.0 KiB
Go
292 lines
7.0 KiB
Go
package netflow
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
|
"github.com/netbirdio/netbird/client/internal/netflow/types"
|
|
"github.com/netbirdio/netbird/flow/proto"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
type testServer struct {
|
|
proto.UnimplementedFlowServiceServer
|
|
events chan *proto.FlowEvent
|
|
acks chan *proto.FlowEventAck
|
|
grpcSrv *grpc.Server
|
|
addr string
|
|
handlerDone chan struct{} // signaled each time Events() exits
|
|
handlerStarted chan struct{} // signaled each time Events() begins
|
|
}
|
|
|
|
func newTestServer(t *testing.T) *testServer {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
s := &testServer{
|
|
events: make(chan *proto.FlowEvent, 100),
|
|
acks: make(chan *proto.FlowEventAck, 100),
|
|
grpcSrv: grpc.NewServer(),
|
|
addr: listener.Addr().String(),
|
|
handlerDone: make(chan struct{}, 10),
|
|
handlerStarted: make(chan struct{}, 10),
|
|
}
|
|
|
|
proto.RegisterFlowServiceServer(s.grpcSrv, s)
|
|
|
|
go func() {
|
|
if err := s.grpcSrv.Serve(listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
|
|
t.Logf("server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
t.Cleanup(func() {
|
|
s.grpcSrv.Stop()
|
|
})
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *testServer) Events(stream proto.FlowService_EventsServer) error {
|
|
defer func() {
|
|
select {
|
|
case s.handlerDone <- struct{}{}:
|
|
default:
|
|
}
|
|
}()
|
|
|
|
err := stream.Send(&proto.FlowEventAck{IsInitiator: true})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
select {
|
|
case s.handlerStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(stream.Context())
|
|
defer cancel()
|
|
|
|
go func() {
|
|
defer cancel()
|
|
for {
|
|
event, err := stream.Recv()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if !event.IsInitiator {
|
|
select {
|
|
case s.events <- event:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case ack := <-s.acks:
|
|
if err := stream.Send(ack); err != nil {
|
|
return err
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSendEventReceiveAck(t *testing.T) {
|
|
_, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
t.Cleanup(cancel)
|
|
|
|
server := newTestServer(t)
|
|
manager := createManager(t, server.addr, 60*time.Second) // set high to prevent retries in this test
|
|
defer manager.Close()
|
|
|
|
assert.Eventually(t, func() bool {
|
|
select {
|
|
case <-server.handlerStarted:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, 3*time.Second, 100*time.Millisecond)
|
|
|
|
event1 := types.EventFields{
|
|
FlowID: uuid.New(),
|
|
Type: types.TypeStart,
|
|
Direction: types.Ingress,
|
|
DestIP: ipAddr("172.16.1.2"),
|
|
DestPort: 2345,
|
|
Protocol: 6,
|
|
}
|
|
manager.logger.StoreEvent(event1)
|
|
event2 := types.EventFields{
|
|
FlowID: uuid.New(),
|
|
Type: types.TypeStart,
|
|
Direction: types.Ingress,
|
|
DestIP: ipAddr("172.16.1.1"),
|
|
DestPort: 1234,
|
|
Protocol: 6,
|
|
}
|
|
manager.logger.StoreEvent(event2)
|
|
|
|
// verify the server received logged events
|
|
serverSideEvents := make([]*proto.FlowEvent, 0)
|
|
assert.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-server.events:
|
|
serverSideEvents = append(serverSideEvents, event)
|
|
if len(serverSideEvents) == 2 {
|
|
return true
|
|
}
|
|
default:
|
|
if len(serverSideEvents) == 2 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
serverSideFlowIds := make([]uuid.UUID, 0, 2)
|
|
slices.Values(serverSideEvents)(func(e *proto.FlowEvent) bool {
|
|
id, err := uuid.FromBytes(e.FlowFields.FlowId)
|
|
assert.NoError(t, err)
|
|
serverSideFlowIds = append(serverSideFlowIds, id)
|
|
return true
|
|
})
|
|
assert.ElementsMatch(t, []uuid.UUID{event1.FlowID, event2.FlowID}, serverSideFlowIds)
|
|
|
|
// verify the manager tracks un-acked events
|
|
unackedEvents := manager.eventsWithoutAcks.GetEvents()
|
|
assert.Len(t, unackedEvents, 2)
|
|
flowIds := make([]uuid.UUID, 0)
|
|
slices.Values(unackedEvents)(func(e *types.Event) bool {
|
|
flowIds = append(flowIds, e.FlowID)
|
|
return true
|
|
})
|
|
assert.ElementsMatch(t, flowIds, []uuid.UUID{event1.FlowID, event2.FlowID})
|
|
}
|
|
|
|
// verify handling of retries:
|
|
// - unacked events are retried
|
|
// - when acks arrive, events are removed from the un-acked event tracker
|
|
func TestRetryEvents(t *testing.T) {
|
|
_, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
t.Cleanup(cancel)
|
|
|
|
server := newTestServer(t)
|
|
manager := createManager(t, server.addr, time.Second) // set low to start retries sooner
|
|
defer manager.Close()
|
|
|
|
assert.Eventually(t, func() bool {
|
|
select {
|
|
case <-server.handlerStarted:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, 3*time.Second, 100*time.Millisecond)
|
|
|
|
event1 := types.EventFields{
|
|
FlowID: uuid.New(),
|
|
Type: types.TypeStart,
|
|
Direction: types.Ingress,
|
|
DestIP: ipAddr("172.16.1.2"),
|
|
DestPort: 2345,
|
|
Protocol: 6,
|
|
}
|
|
manager.logger.StoreEvent(event1)
|
|
event2 := types.EventFields{
|
|
FlowID: uuid.New(),
|
|
Type: types.TypeStart,
|
|
Direction: types.Ingress,
|
|
DestIP: ipAddr("172.16.1.1"),
|
|
DestPort: 1234,
|
|
Protocol: 6,
|
|
}
|
|
manager.logger.StoreEvent(event2)
|
|
|
|
// verify the server received retries of logged events
|
|
serverSideEvents := make([]*proto.FlowEvent, 0)
|
|
func() {
|
|
c := time.After(2500 * time.Millisecond)
|
|
for {
|
|
select {
|
|
case event := <-server.events:
|
|
serverSideEvents = append(serverSideEvents, event)
|
|
case <-c:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
assert.True(t, len(serverSideEvents) > 2) // must see retries
|
|
|
|
uniqueServerSideEvents := make(map[uuid.UUID]*proto.FlowEvent)
|
|
slices.Values(serverSideEvents)(func(e *proto.FlowEvent) bool {
|
|
id, err := uuid.FromBytes(e.FlowFields.FlowId)
|
|
assert.NoError(t, err)
|
|
uniqueServerSideEvents[id] = e
|
|
return true
|
|
})
|
|
assert.Contains(t, uniqueServerSideEvents, event1.FlowID)
|
|
assert.Contains(t, uniqueServerSideEvents, event2.FlowID)
|
|
|
|
// ack events
|
|
server.acks <- &proto.FlowEventAck{EventId: uniqueServerSideEvents[event1.FlowID].EventId}
|
|
server.acks <- &proto.FlowEventAck{EventId: uniqueServerSideEvents[event2.FlowID].EventId}
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
unackedEvents := manager.eventsWithoutAcks.GetEvents()
|
|
assert.Empty(c, unackedEvents)
|
|
|
|
}, 3*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
func createManager(t *testing.T, serverAddr string, retryInterval time.Duration) *Manager {
|
|
t.Helper()
|
|
|
|
mockIFace := &mockIFaceMapper{
|
|
address: wgaddr.Address{
|
|
Network: netip.MustParsePrefix("192.168.1.1/32"),
|
|
},
|
|
isUserspaceBind: true,
|
|
}
|
|
|
|
publicKey := []byte("test-public-key")
|
|
manager := NewManager(mockIFace, publicKey, nil)
|
|
manager.retryInterval = retryInterval
|
|
|
|
initialConfig := &types.FlowConfig{
|
|
Enabled: true,
|
|
URL: fmt.Sprintf("http://%s", serverAddr),
|
|
TokenPayload: "initial-payload",
|
|
TokenSignature: "initial-signature",
|
|
Interval: 500 * time.Millisecond,
|
|
}
|
|
|
|
err := manager.Update(initialConfig)
|
|
require.NoError(t, err)
|
|
|
|
return manager
|
|
}
|
|
|
|
func ipAddr(a string) netip.Addr {
|
|
addr, _ := netip.ParseAddr(a)
|
|
return addr
|
|
}
|