// SPDX-FileCopyrightText: 2022 Free Mobile // SPDX-License-Identifier: AGPL-3.0-only package flows import ( "fmt" "math" "math/rand/v2" "net/netip" "slices" "testing" "time" "akvorado/common/helpers" ) func TestRateToCount(t *testing.T) { rates := []float64{0.2, 0.4, 0.6, 1, 1.4, 2.3, 2.8, 3, 3.2, 4.7, 1200} now := time.Now().Truncate(time.Second) for _, rate := range rates { result := 0 for range 1000 { result += rateToCount(rate, now) now = now.Add(time.Second) } computedRate := float64(result) / 1000 if math.Abs(rate-computedRate) > rate*0.01 { t.Errorf("rateToCount(%f) was %f on average", rate, computedRate) } } } func TestRandomIP(t *testing.T) { prefixes := []string{ "192.168.0.0/24", "192.168.0.0/16", "172.16.0.0/12", "192.168.14.1/32", "0.0.0.0/0", "2001:db8::/32", "2001:db8:a:b::/64", "2001:db8:a:c:d::1/128", } r := rand.New(rand.NewPCG(0, 0)) for _, p := range prefixes { prefix := netip.MustParsePrefix(p) for range 1000 { ip := randomIP(prefix, r) if !prefix.Contains(ip) { t.Errorf("randomIP(%q) == %q not in prefix", p, ip) break } } } } func TestPeakHourDistance(t *testing.T) { cases := []struct { Pos helpers.Pos Peak time.Duration Now time.Duration Expected float64 }{ {helpers.Mark(), 6 * time.Hour, 6 * time.Hour, 1}, {helpers.Mark(), 6 * time.Hour, 0, 0.5}, {helpers.Mark(), 6 * time.Hour, 18 * time.Hour, 0}, {helpers.Mark(), 12 * time.Hour, 13 * time.Hour, 11. / 12}, {helpers.Mark(), 12 * time.Hour, 11 * time.Hour, 11. / 12}, {helpers.Mark(), 12 * time.Hour, 14 * time.Hour, 10. / 12}, {helpers.Mark(), 12 * time.Hour, 15 * time.Hour, 9. / 12}, {helpers.Mark(), 12 * time.Hour, 16 * time.Hour, 8. / 12}, {helpers.Mark(), 12 * time.Hour, 17 * time.Hour, 7. / 12}, {helpers.Mark(), 12 * time.Hour, 18 * time.Hour, 6. / 12}, {helpers.Mark(), 12 * time.Hour, 19 * time.Hour, 5. / 12}, } for _, tc := range cases { got := peakHourDistance(tc.Now, tc.Peak) if math.Abs(got-tc.Expected) > tc.Expected*0.01 { t.Errorf("%speakHourDistance(%s, %s) == %f, expected %f", tc.Pos, tc.Peak, tc.Now, got, tc.Expected) } } } func TestChooseRandom(t *testing.T) { cases := [][]int{ nil, {}, {6}, {1, 2, 3, 4, 10, 12}, } r := rand.New(rand.NewPCG(0, 0)) for _, tc := range cases { t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) { results := map[int]bool{} for range 100 { result := chooseRandom(r, tc) results[result] = true if len(tc) == 0 { if result != 0 { t.Fatalf("chooseRandom() == %d instead of 0", result) } break } if !slices.Contains(tc, result) { t.Fatalf("chooseRandom() returned %d, not in slice", result) } } if len(tc) != 0 && len(results) != len(tc) { t.Fatalf("chooseRandom() did not explore all results (only %d)", len(results)) } }) } } func TestGenerateFlows(t *testing.T) { cases := []struct { Pos helpers.Pos FlowConfiguration Expected []generatedFlow }{ { Pos: helpers.Mark(), FlowConfiguration: FlowConfiguration{ PerSecond: 1, InIfIndex: []int{10}, OutIfIndex: []int{20, 21}, PeakHour: 21 * time.Hour, Multiplier: 3.1, // 6 hours from peak time → ~2 SrcNet: netip.MustParsePrefix("192.0.2.0/24"), DstNet: netip.MustParsePrefix("203.0.113.0/24"), SrcAS: []uint32{65201}, DstAS: []uint32{65202}, SrcPort: []uint16{443}, Protocol: []string{"tcp"}, Size: 1400, }, Expected: []generatedFlow{ { SrcAddr: netip.MustParseAddr("192.0.2.155"), DstAddr: netip.MustParseAddr("203.0.113.59"), EType: 0x800, IPFlow: IPFlow{ Octets: 1011, Packets: 1, Proto: 6, SrcPort: 443, DstPort: 33550, InputInt: 10, OutputInt: 21, SrcAS: 65201, DstAS: 65202, ForwardStatus: 64, SrcMask: 24, DstMask: 24, }, }, { SrcAddr: netip.MustParseAddr("192.0.2.3"), DstAddr: netip.MustParseAddr("203.0.113.52"), EType: 0x800, IPFlow: IPFlow{ Octets: 1478, Packets: 1, Proto: 6, SrcPort: 443, DstPort: 33200, InputInt: 10, OutputInt: 20, SrcAS: 65201, DstAS: 65202, ForwardStatus: 64, SrcMask: 24, DstMask: 24, }, }, { SrcAddr: netip.MustParseAddr("192.0.2.151"), DstAddr: netip.MustParseAddr("203.0.113.245"), EType: 2048, IPFlow: IPFlow{ Packets: 1, Octets: 1311, Proto: 6, InputInt: 10, OutputInt: 20, SrcPort: 443, DstPort: 33389, SrcAS: 65201, DstAS: 65202, ForwardStatus: 64, SrcMask: 24, DstMask: 24, }, }, }, }, { Pos: helpers.Mark(), FlowConfiguration: FlowConfiguration{ PerSecond: 1, InIfIndex: []int{20}, OutIfIndex: []int{10, 11}, PeakHour: 3 * time.Hour, Multiplier: 4, // 12 hours from peak time → ~1 SrcNet: netip.MustParsePrefix("2001:db8::1/128"), DstNet: netip.MustParsePrefix("2001:db8:2::/64"), SrcAS: []uint32{65201}, DstAS: []uint32{65202}, DstPort: []uint16{443}, Protocol: []string{"tcp"}, Size: 1200, }, Expected: []generatedFlow{ { SrcAddr: netip.MustParseAddr("2001:db8::1"), DstAddr: netip.MustParseAddr("2001:db8:2:0:9b3b:48ac:f003:34f4"), EType: 0x86dd, IPFlow: IPFlow{ Octets: 866, Packets: 1, Proto: 6, SrcPort: 33820, DstPort: 443, InputInt: 20, OutputInt: 11, SrcAS: 65201, DstAS: 65202, ForwardStatus: 64, SrcMask: 128, DstMask: 64, }, }, }, }, { Pos: helpers.Mark(), FlowConfiguration: FlowConfiguration{ PerSecond: 1, InIfIndex: []int{20}, OutIfIndex: []int{10, 11}, PeakHour: 3 * time.Hour, Multiplier: 4, // 12 hours from peak time → ~1 SrcNet: netip.MustParsePrefix("2001:db8::1/128"), DstNet: netip.MustParsePrefix("2001:db8:2::/64"), SrcAS: []uint32{65201}, DstAS: []uint32{65202}, DstPort: []uint16{443}, Protocol: []string{"tcp"}, Size: 1200, ReverseDirectionRatio: 0.1, }, Expected: []generatedFlow{ { SrcAddr: netip.MustParseAddr("2001:db8::1"), DstAddr: netip.MustParseAddr("2001:db8:2:0:9b3b:48ac:f003:34f4"), EType: 0x86dd, IPFlow: IPFlow{ Octets: 866, Packets: 1, Proto: 6, SrcPort: 33820, DstPort: 443, InputInt: 20, OutputInt: 11, SrcAS: 65201, DstAS: 65202, ForwardStatus: 64, SrcMask: 128, DstMask: 64, }, }, { DstAddr: netip.MustParseAddr("2001:db8::1"), SrcAddr: netip.MustParseAddr("2001:db8:2:0:9b3b:48ac:f003:34f4"), EType: 0x86dd, IPFlow: IPFlow{ Octets: 866 / 10, Packets: 1, Proto: 6, DstPort: 33820, SrcPort: 443, OutputInt: 20, InputInt: 11, DstAS: 65201, SrcAS: 65202, ForwardStatus: 64, DstMask: 128, SrcMask: 64, }, }, }, }, } now := time.Date(2022, 3, 18, 15, 0, 0, 0, time.UTC) for _, tc := range cases { t.Run(fmt.Sprintf("case %s", tc.Pos), func(t *testing.T) { got := generateFlows([]FlowConfiguration{tc.FlowConfiguration}, 0, now) if diff := helpers.Diff(got, tc.Expected); diff != "" { t.Fatalf("%sgeneratedFlows() (-got, +want):\n%s", tc.Pos, diff) } }) } }