feat: allow specifying port ranges for sniffing

This commit is contained in:
Toby 2024-06-30 12:04:59 -07:00
parent b481b49a28
commit deeeafd8d7
9 changed files with 276 additions and 182 deletions

107
extras/utils/portunion.go Normal file
View file

@ -0,0 +1,107 @@
package utils
import (
"sort"
"strconv"
"strings"
)
// PortUnion is a collection of multiple port ranges.
type PortUnion []PortRange
// PortRange represents a range of ports.
// Start and End are inclusive. [Start, End]
type PortRange struct {
Start, End uint16
}
// ParsePortUnion parses a string of comma-separated port ranges (or single ports) into a PortUnion.
// Returns nil if the input is invalid.
// The returned PortUnion is guaranteed to be normalized.
func ParsePortUnion(s string) PortUnion {
if s == "all" || s == "*" {
// Wildcard special case
return PortUnion{PortRange{0, 65535}}
}
var result PortUnion
portStrs := strings.Split(s, ",")
for _, portStr := range portStrs {
if strings.Contains(portStr, "-") {
// Port range
portRange := strings.Split(portStr, "-")
if len(portRange) != 2 {
return nil
}
start, err := strconv.ParseUint(portRange[0], 10, 16)
if err != nil {
return nil
}
end, err := strconv.ParseUint(portRange[1], 10, 16)
if err != nil {
return nil
}
if start > end {
start, end = end, start
}
result = append(result, PortRange{uint16(start), uint16(end)})
} else {
// Single port
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return nil
}
result = append(result, PortRange{uint16(port), uint16(port)})
}
}
if result == nil {
return nil
}
return result.Normalize()
}
// Normalize normalizes a PortUnion.
// No overlapping ranges, ranges are sorted from low to high.
func (u PortUnion) Normalize() PortUnion {
if len(u) == 0 {
return u
}
sort.Slice(u, func(i, j int) bool {
if u[i].Start == u[j].Start {
return u[i].End < u[j].End
}
return u[i].Start < u[j].Start
})
normalized := PortUnion{u[0]}
for _, current := range u[1:] {
last := &normalized[len(normalized)-1]
if current.Start <= last.End+1 {
if current.End > last.End {
last.End = current.End
}
} else {
normalized = append(normalized, current)
}
}
return normalized
}
// Ports returns all ports in the PortUnion as a slice.
func (u PortUnion) Ports() []uint16 {
var ports []uint16
for _, r := range u {
for i := r.Start; i <= r.End; i++ {
ports = append(ports, i)
}
}
return ports
}
// Contains returns true if the PortUnion contains the given port.
func (u PortUnion) Contains(port uint16) bool {
for _, r := range u {
if port >= r.Start && port <= r.End {
return true
}
}
return false
}

View file

@ -0,0 +1,92 @@
package utils
import (
"reflect"
"testing"
)
func TestParsePortUnion(t *testing.T) {
tests := []struct {
name string
s string
want PortUnion
}{
{
name: "empty",
s: "",
want: nil,
},
{
name: "all 1",
s: "all",
want: PortUnion{{0, 65535}},
},
{
name: "all 2",
s: "*",
want: PortUnion{{0, 65535}},
},
{
name: "single port",
s: "1234",
want: PortUnion{{1234, 1234}},
},
{
name: "multiple ports (unsorted)",
s: "5678,1234,9012",
want: PortUnion{{1234, 1234}, {5678, 5678}, {9012, 9012}},
},
{
name: "one range",
s: "1234-1240",
want: PortUnion{{1234, 1240}},
},
{
name: "one range (reversed)",
s: "1240-1234",
want: PortUnion{{1234, 1240}},
},
{
name: "multiple ports and ranges (reversed, unsorted, overlapping)",
s: "5678,1200-1236,9100-9012,1234-1240",
want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}},
},
{
name: "invalid 1",
s: "1234-",
want: nil,
},
{
name: "invalid 2",
s: "1234-ggez",
want: nil,
},
{
name: "invalid 3",
s: "233,",
want: nil,
},
{
name: "invalid 4",
s: "1234-1240-1250",
want: nil,
},
{
name: "invalid 5",
s: "-,,",
want: nil,
},
{
name: "invalid 6",
s: "http",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ParsePortUnion(tt.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParsePortUnion() = %v, want %v", got, tt.want)
}
})
}
}