Skip to content

Commit cd920fe

Browse files
authored
NETOBSERV-2299: Automation CLI packet capture case (#411)
* CLI Packet Capture Test Cases * cherrypick code * removed tests and adding only single test and removed unwanted pcap helper function * Update code for Pcap file name to be consistant with other test * Solve Lint Error
1 parent 79fa9f9 commit cd920fe

File tree

3 files changed

+292
-1
lines changed

3 files changed

+292
-1
lines changed

e2e/integration-tests/cli.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,26 @@ func getFlowsJSONFile() (string, error) {
156156
}
157157
return absPath, nil
158158
}
159+
160+
// get latest .pcapng file
161+
func getPcapngFile() (string, error) {
162+
var files []string
163+
outputDir := "./output/pcap/"
164+
dirFS := os.DirFS(outputDir)
165+
files, err := fs.Glob(dirFS, "*.pcapng")
166+
if err != nil {
167+
return "", err
168+
}
169+
// this could be problematic if two tests are running in parallel with --copy=true
170+
var mostRecentFile fs.FileInfo
171+
for _, file := range files {
172+
fileInfo, err := os.Stat(outputDir + file)
173+
if err != nil {
174+
return "", nil
175+
}
176+
if mostRecentFile == nil || fileInfo.ModTime().After(mostRecentFile.ModTime()) {
177+
mostRecentFile = fileInfo
178+
}
179+
}
180+
return outputDir + mostRecentFile.Name(), nil
181+
}

e2e/integration-tests/integration_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,47 @@ var _ = g.Describe("NetObserv CLI e2e integration test suite", g.Ordered, func()
9999
o.Expect(allPods).To(o.HaveLen(totalExpectedPods), fmt.Sprintf("Number of CLI pods are not as expected %d", totalExpectedPods))
100100
})
101101

102+
g.It("OCP-73458: Verify packet capture creates pcapng file and filters by port", g.Label("PacketCapture"), func() {
103+
g.DeferCleanup(func() {
104+
cleanup()
105+
})
106+
107+
// Run packet capture with port 8080 filter
108+
targetPort := uint16(8080)
109+
cliArgs := []string{"packets", "--port=8080", "--copy=true", "--max-bytes=100000000", "--max-time=1m"}
110+
out, err := e2e.RunCommand(ilog, ocNetObservBinPath, cliArgs...)
111+
writeOutput(filePrefix+"-packetOutput", out)
112+
o.Expect(err).NotTo(o.HaveOccurred(), fmt.Sprintf("Error running command %v", err))
113+
114+
_, err = isCLIDone(clientset, cliNS)
115+
o.Expect(err).NotTo(o.HaveOccurred(), fmt.Sprintf("CLI didn't finish %v", err))
116+
117+
// Verify pcapng file is created
118+
pcapngFile, err := getPcapngFile()
119+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get pcapng file")
120+
o.Expect(pcapngFile).NotTo(o.BeEmpty(), "Pcapng file path should not be empty")
121+
122+
ilog.Infof("==> Pcapng file created at: %s", pcapngFile)
123+
124+
// Verify file exists and has content
125+
fileInfo, err := os.Stat(pcapngFile)
126+
o.Expect(err).NotTo(o.HaveOccurred(), fmt.Sprintf("Pcapng file should exist at %s", pcapngFile))
127+
o.Expect(fileInfo.Size()).To(o.BeNumerically(">", 0), "Pcapng file should have content")
128+
129+
ilog.Infof("==> Pcapng file size: %d bytes", fileInfo.Size())
130+
131+
// Read and analyze packets from pcapng file
132+
packets, err := ReadPcapngFile(pcapngFile)
133+
o.Expect(err).NotTo(o.HaveOccurred(), fmt.Sprintf("Failed to read pcapng file: %v", err))
134+
o.Expect(packets).NotTo(o.BeEmpty(), "Pcapng file should contain packets")
135+
136+
ilog.Infof("Found %d total packets in pcapng file", len(packets))
137+
// Verify packets are filtered by port 8080
138+
filteredPackets := FilterPacketsByPort(packets, targetPort)
139+
o.Expect(filteredPackets).NotTo(o.BeEmpty(), fmt.Sprintf("Should have captured packets on port %d", targetPort))
140+
141+
ilog.Infof("Found %d packets on port %d", len(filteredPackets), targetPort)
142+
})
102143
g.It("OCP-73458: Verify regexes filters are applied", g.Label("Regexes"), func() {
103144
g.DeferCleanup(func() {
104145
cleanup()
@@ -138,7 +179,6 @@ var _ = g.Describe("NetObserv CLI e2e integration test suite", g.Ordered, func()
138179
}
139180
o.Expect(found).To(o.BeTrue(), fmt.Sprintf("Didn't found any flow matching SrcK8S_Namespace=~%s", nsfilter))
140181
})
141-
142182
g.It("OCP-82597: Verify sampling value of 1 is applied in captured flows", g.Label("Sampling"), func() {
143183

144184
g.DeferCleanup(func() {
@@ -243,6 +283,7 @@ var _ = g.Describe("NetObserv CLI e2e integration test suite", g.Ordered, func()
243283
o.Expect(err).NotTo(o.HaveOccurred(), fmt.Sprintf("Failed to query Prometheus for metrics: %v", err))
244284
o.Expect(metricValue).To(o.BeNumerically(">=", 0), fmt.Sprintf("Prometheus should return a valid metric value, but got %v", metricValue))
245285
})
286+
246287
g.Describe("OCP-84801: Verify CLI runs under correct privileges", g.Label("Privileges"), func() {
247288

248289
tests := []struct {
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
//go:build !e2e
2+
3+
package integrationtests
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"io"
9+
"os"
10+
"strings"
11+
12+
"github.com/gopacket/gopacket"
13+
"github.com/gopacket/gopacket/layers"
14+
"github.com/gopacket/gopacket/pcapgo"
15+
)
16+
17+
// PacketInfo holds information about a captured packet
18+
type PacketInfo struct {
19+
Timestamp int64
20+
Length int
21+
SrcIP string
22+
DstIP string
23+
SrcPort uint16
24+
DstPort uint16
25+
Protocol string
26+
Comments []string
27+
HasTCP bool
28+
HasUDP bool
29+
HasIPv4 bool
30+
HasIPv6 bool
31+
K8sMetadata map[string]string
32+
}
33+
34+
// PacketFilter defines filtering criteria for packets
35+
type PacketFilter struct {
36+
Port *uint16 // Filter by port (source or destination)
37+
SrcPort *uint16 // Filter by source port
38+
DstPort *uint16 // Filter by destination port
39+
Protocol string // Filter by protocol (TCP, UDP, ICMP, etc.)
40+
SrcIP string // Filter by source IP
41+
DstIP string // Filter by destination IP
42+
MinLength int // Minimum packet length
43+
MaxLength int // Maximum packet length
44+
}
45+
46+
// ReadPcapngFile reads a pcapng file and returns all packets
47+
func ReadPcapngFile(filepath string) ([]PacketInfo, error) {
48+
return ReadPcapngFileWithFilter(filepath, nil)
49+
}
50+
51+
// ReadPcapngFileWithFilter reads a pcapng file and returns filtered packets
52+
func ReadPcapngFileWithFilter(filepath string, filter *PacketFilter) ([]PacketInfo, error) {
53+
f, err := os.Open(filepath)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to open pcapng file: %w", err)
56+
}
57+
defer f.Close()
58+
59+
ngReader, err := pcapgo.NewNgReader(f, pcapgo.DefaultNgReaderOptions)
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to create pcapng reader: %w", err)
62+
}
63+
64+
var packets []PacketInfo
65+
66+
for {
67+
data, ci, opts, err := ngReader.ReadPacketDataWithOptions()
68+
if err != nil {
69+
if errors.Is(err, io.EOF) {
70+
break
71+
}
72+
return nil, fmt.Errorf("error reading packet data: %w", err)
73+
}
74+
75+
packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.Default)
76+
packetInfo := extractPacketInfo(packet, ci, &opts)
77+
78+
// Apply filter if provided
79+
if filter != nil && !matchesFilter(&packetInfo, filter) {
80+
continue
81+
}
82+
83+
packets = append(packets, packetInfo)
84+
}
85+
86+
return packets, nil
87+
}
88+
89+
// extractPacketInfo extracts information from a packet
90+
func extractPacketInfo(packet gopacket.Packet, ci gopacket.CaptureInfo, opts *pcapgo.NgPacketOptions) PacketInfo {
91+
info := PacketInfo{
92+
Timestamp: ci.Timestamp.Unix(),
93+
Length: ci.Length,
94+
K8sMetadata: make(map[string]string),
95+
}
96+
97+
// Extract comments from NgPacketOptions (contains k8s metadata)
98+
if len(opts.Comments) > 0 {
99+
info.Comments = opts.Comments
100+
extractK8sMetadata(&info)
101+
}
102+
103+
// Check for IPv4 layer
104+
if ipv4Layer := packet.Layer(layers.LayerTypeIPv4); ipv4Layer != nil {
105+
ipv4, _ := ipv4Layer.(*layers.IPv4)
106+
info.SrcIP = ipv4.SrcIP.String()
107+
info.DstIP = ipv4.DstIP.String()
108+
info.HasIPv4 = true
109+
info.Protocol = ipv4.Protocol.String()
110+
}
111+
112+
// Check for IPv6 layer
113+
if ipv6Layer := packet.Layer(layers.LayerTypeIPv6); ipv6Layer != nil {
114+
ipv6, _ := ipv6Layer.(*layers.IPv6)
115+
info.SrcIP = ipv6.SrcIP.String()
116+
info.DstIP = ipv6.DstIP.String()
117+
info.HasIPv6 = true
118+
info.Protocol = ipv6.NextHeader.String()
119+
}
120+
121+
// Check for TCP layer
122+
if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
123+
tcp, _ := tcpLayer.(*layers.TCP)
124+
info.SrcPort = uint16(tcp.SrcPort)
125+
info.DstPort = uint16(tcp.DstPort)
126+
info.HasTCP = true
127+
if info.Protocol == "" {
128+
info.Protocol = "TCP"
129+
}
130+
}
131+
132+
// Check for UDP layer
133+
if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil {
134+
udp, _ := udpLayer.(*layers.UDP)
135+
info.SrcPort = uint16(udp.SrcPort)
136+
info.DstPort = uint16(udp.DstPort)
137+
info.HasUDP = true
138+
if info.Protocol == "" {
139+
info.Protocol = "UDP"
140+
}
141+
}
142+
143+
// Check for ICMP layers
144+
if packet.Layer(layers.LayerTypeICMPv4) != nil {
145+
info.Protocol = "ICMPv4"
146+
}
147+
if packet.Layer(layers.LayerTypeICMPv6) != nil {
148+
info.Protocol = "ICMPv6"
149+
}
150+
151+
return info
152+
}
153+
154+
// extractK8sMetadata parses k8s metadata from packet comments
155+
func extractK8sMetadata(info *PacketInfo) {
156+
for _, comment := range info.Comments {
157+
lines := strings.Split(comment, "\n")
158+
for _, line := range lines {
159+
if strings.Contains(line, ":") {
160+
parts := strings.SplitN(line, ":", 2)
161+
if len(parts) == 2 {
162+
key := strings.TrimSpace(parts[0])
163+
value := strings.TrimSpace(parts[1])
164+
info.K8sMetadata[key] = value
165+
}
166+
}
167+
}
168+
}
169+
}
170+
171+
// matchesFilter checks if a packet matches the filter criteria
172+
func matchesFilter(info *PacketInfo, filter *PacketFilter) bool {
173+
// Filter by port (either source or destination)
174+
if filter.Port != nil {
175+
if info.SrcPort != *filter.Port && info.DstPort != *filter.Port {
176+
return false
177+
}
178+
}
179+
180+
// Filter by source port
181+
if filter.SrcPort != nil && info.SrcPort != *filter.SrcPort {
182+
return false
183+
}
184+
185+
// Filter by destination port
186+
if filter.DstPort != nil && info.DstPort != *filter.DstPort {
187+
return false
188+
}
189+
190+
// Filter by protocol
191+
if filter.Protocol != "" && !strings.EqualFold(info.Protocol, filter.Protocol) {
192+
return false
193+
}
194+
195+
// Filter by source IP
196+
if filter.SrcIP != "" && info.SrcIP != filter.SrcIP {
197+
return false
198+
}
199+
200+
// Filter by destination IP
201+
if filter.DstIP != "" && info.DstIP != filter.DstIP {
202+
return false
203+
}
204+
205+
// Filter by minimum length
206+
if filter.MinLength > 0 && info.Length < filter.MinLength {
207+
return false
208+
}
209+
210+
// Filter by maximum length
211+
if filter.MaxLength > 0 && info.Length > filter.MaxLength {
212+
return false
213+
}
214+
215+
return true
216+
}
217+
218+
// FilterPacketsByPort filters packets by port (source or destination)
219+
func FilterPacketsByPort(packets []PacketInfo, port uint16) []PacketInfo {
220+
var filtered []PacketInfo
221+
for _, p := range packets {
222+
if p.SrcPort == port || p.DstPort == port {
223+
filtered = append(filtered, p)
224+
}
225+
}
226+
return filtered
227+
}

0 commit comments

Comments
 (0)