-
Notifications
You must be signed in to change notification settings - Fork 8
/
main.go
359 lines (308 loc) · 12.2 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
//go:build linux
// This implements a versatile packet generation tool designed for network performance
// and stress testing. It supports various methods of packet sending including direct AF_PACKET access,
// raw sockets, and high-level abstractions like net.Conn. Users can specify source and destination
// IPs, MAC addresses, payload size, and duration for the tests. This tool is useful for network
// administrators and developers looking to evaluate network equipment, protocols, or software
// under different conditions and loads.
// However it's main intent was to compare the performance of different packet sending methods.
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"os/signal"
"runtime"
"sort"
"sync"
"syscall"
"time"
"github.com/atoonk/go-pktgen/pktgen"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"github.com/vishvananda/netlink"
)
type resultItem struct {
Method string
PacketsPS int // Packets per second
Mbps int
}
var (
method string
iface string
srcIP string
dstIP string
srcPort int
dstPort int
payloadSize int
srcMAC string
dstMAC string
duration int
streams int
)
// Define global or package-level variables if needed
var rootCmd = &cobra.Command{
Use: "pktgen",
Short: "Packet generator tool",
Long: `A versatile packet generation tool designed for network performance and stress testing.`,
Run: runPacketGenerator,
}
// this starts the packet generator, using the method specified by the user
func runPacketGenerator(cmd *cobra.Command, args []string) {
srcMACAddr, err := net.ParseMAC(srcMAC)
if err != nil {
log.Fatalf("Invalid source MAC address: %v", err)
}
dstMACAddr, err := net.ParseMAC(dstMAC)
if err != nil {
log.Fatalf("Invalid destination MAC address: %v", err)
}
dstIPParsed := net.ParseIP(dstIP)
// check if the destination IP is a valid IP address
if dstIPParsed == nil {
log.Fatalf("Invalid destination IP address: %s", dstIP)
}
srcIPParsed := net.ParseIP(srcIP)
// check if the source IP is a valid IP address
if srcIPParsed == nil {
log.Fatalf("Invalid source IP address: %s", srcIP)
}
// This is the check if the number of TX queues
// No real benefit to have more streams than TX queues
// Will also break AF_XDP so need a check
txQueues, err := pktgen.GetCurrentTXQueues(iface)
if err == nil && txQueues < streams {
fmt.Printf("Error: Number of TX queues (%d) is less than the number of streams (%d)\n", txQueues, streams)
fmt.Printf("Pleease increase the number of TX queues using ethtool -L %s [tx|combined] %d or lower the number of streams to %d\n", iface, streams, txQueues)
os.Exit(1)
}
// Handling multiple streams
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
setupSignalHandler(cancel)
if method == "benchmark" {
runBenchmark(ctx, iface, srcIPParsed, dstIPParsed, srcPort, dstPort, payloadSize, duration, srcMACAddr, dstMACAddr, streams)
} else {
// start the printStats goroutine
go printStats(iface, payloadSize, ctx)
// run the streams, that will know what method to start
wg := &sync.WaitGroup{}
for i := 0; i < streams; i++ {
wg.Add(1)
go func(streamNum int) {
defer wg.Done()
runStream(ctx, method, iface, srcIPParsed, dstIPParsed, srcPort, dstPort, payloadSize, srcMACAddr, dstMACAddr, duration, streamNum)
}(i + 1)
}
wg.Wait()
}
}
// runBenchmark executes a series of packet sending tests using different methods
// specified in the methods map. It collects and displays the results in terms of packets
// per second and Mbps. The function gracefully handles interrupts, allowing for a clean
// shutdown and accurate results reporting.
//
// Parameters:
// - ifaceName: The network interface name for sending packets.
// - srcIPParsed, dstIPParsed: Source and destination IP addresses for the packets.
// - srcPort, dstPort: Source and destination ports for UDP/TCP packets.
// - payloadSize: The size of the packet payload in bytes.
// - duration: The duration of each test in seconds.
// - dstMACAddr, srcMACAddr: Destination and source MAC addresses for packet crafting.
//
// The function sorts and displays the results in a table, highlighting the method's performance.func runBenchmark(ifaceName string, srcIPParsed, dstIPParsed net.IP, srcPort, dstPort, payloadSize, duration *int, dstMACAddr, srcMACAddr net.HardwareAddr) {
func runBenchmark(ctx context.Context, ifaceName string, srcIPParsed, dstIPParsed net.IP, srcPort, dstPort, payloadSize, duration int, srcMACAddr, dstMACAddr net.HardwareAddr, streams int) {
var resultsSlice []resultItem
// define the methods as a slice
methods := []string{"af_xdp", "af_packet", "net_conn", "udp_syscall", "raw_socket", "af_pcap", "pkt_conn"}
// Iterate over methods, run test and collect results
for _, TestType := range methods {
counterT0 := counterStats(ifaceName)
wg := &sync.WaitGroup{}
for i := 0; i < streams; i++ {
wg.Add(1)
go func(streamNum int) {
var sender pktgen.Sender
switch TestType {
case "af_xdp":
sender = pktgen.NewAFXdpSender(ifaceName, srcIPParsed, dstIPParsed, srcPort, dstPort, payloadSize, dstMACAddr, srcMACAddr, streamNum-1)
case "af_packet":
sender = pktgen.NewAFPacketSender(ifaceName, srcIPParsed, dstIPParsed, srcPort, dstPort, payloadSize, dstMACAddr, srcMACAddr)
case "net_conn":
sender = pktgen.NewNetConnSender(dstIPParsed, dstPort, payloadSize)
case "udp_syscall":
sender = pktgen.NewAFInetSyscallSender(dstIPParsed, dstPort, payloadSize)
case "raw_socket":
sender = pktgen.NewRawSocketSender(srcIPParsed, dstIPParsed, srcPort, dstPort, payloadSize)
case "af_pcap":
sender = pktgen.NewAFPcapSender(ifaceName, srcIPParsed, dstIPParsed, srcPort, dstPort, payloadSize, srcMACAddr, dstMACAddr)
case "pkt_conn":
sender = pktgen.NewBatchConnSender(dstIPParsed, dstPort, payloadSize)
}
runTest(sender, duration, ifaceName, payloadSize) // Assume runTest is modified to not print directly
defer wg.Done()
}(i + 1)
}
wg.Wait()
counterT1 := counterStats(ifaceName)
packetsPS := int(counterT1-counterT0) / duration
mbps := (packetsPS * payloadSize * 8) / (1000 * 1000)
resultsSlice = append(resultsSlice, resultItem{Method: TestType, PacketsPS: packetsPS, Mbps: mbps})
// Check if the context has been cancelled before continuing to the next method
if ctx.Err() != nil {
break
}
}
// Sort results by Packets per second, highest first
sort.Slice(resultsSlice, func(i, j int) bool {
return resultsSlice[i].PacketsPS > resultsSlice[j].PacketsPS
})
// Create and print the table
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Method", "Packets/s", "Mb/s"})
table.SetAutoFormatHeaders(false) // Prevents automatic capitalization
for _, item := range resultsSlice {
table.Append([]string{item.Method, fmt.Sprintf("%d", item.PacketsPS), fmt.Sprintf("%d", item.Mbps)})
}
table.Render()
}
// runTest runs a test with the given sender and prints the results
func runTest(sender pktgen.Sender, duration int, ifaceName string, payloadSize int) {
// Create a context that will be canceled when the timeout is reached or an interrupt signal is received
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle interrupt signal
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
cancel()
}()
// Apply timeout if duration is not -1
if duration >= 0 {
var cancelFunc context.CancelFunc
ctx, cancelFunc = context.WithTimeout(ctx, time.Duration(duration)*time.Second)
defer cancelFunc()
}
// Start sending packets in a goroutine
errChan := make(chan error, 1)
go func() {
err := sender.Send(ctx)
errChan <- err
}()
// Wait for sending to finish or be cancelled
select {
case err := <-errChan:
if err != nil {
log.Fatalf("Error sending packet: %v", err)
}
case <-ctx.Done():
// Wait for the sending process to finish or be cancelled
}
}
// counterStats returns the number of transmitted packets per second
func counterStats(ifaceName string) uint64 {
// Fetch the link by name
link, err := netlink.LinkByName(ifaceName)
if err != nil {
fmt.Printf("Error fetching link: %v\n", err)
return 0
}
// Accessing the statistics from the link attributes
attrs := link.Attrs()
if attrs == nil || attrs.Statistics == nil {
fmt.Println("Failed to get link attributes or statistics")
return 0 // Skip this iteration if stats are unavailable
}
// Print the number of packets transmitted per second
return attrs.Statistics.TxPackets
}
// printStats prints the number of transmitted packets per second
func printStats(ifaceName string, frameLen int, ctx context.Context) {
var prevTxPackets, numPkts uint64
for {
select {
case <-ctx.Done():
return // Exit the goroutine when the context is cancelled
default:
time.Sleep(time.Duration(1) * time.Second)
pktCounter := counterStats(ifaceName)
// skip the first loop, when prevTxPackets is 0
if prevTxPackets == 0 {
prevTxPackets = pktCounter
continue
}
// Calculate the difference in transmitted packets
numPkts = pktCounter - prevTxPackets
prevTxPackets = pktCounter
// Print the number of packets transmitted per second
// also account for thernet, ipv4 and udp header
// ethernet header = 14 bytes
// ipv4 header = 20 bytes
// udp header = 8 bytes
// total = 42 bytes
bps := (numPkts * uint64(frameLen+42) * 8)
//fmt.Printf("%d packets/s (%d Mb/s)\n", numPkts, (numPkts*uint64(frameLen)*8)/(1000*1000))
fmt.Printf("%d packets/s (%d Mb/s)\n", numPkts, bps/(1000*1000))
}
}
}
func runStream(ctx context.Context, method, iface string, srcIP, dstIP net.IP, srcPort, dstPort, payloadSize int, srcMAC, dstMAC net.HardwareAddr, duration, streamNum int) {
// Replicate the switch logic from your existing `main()`
// For example:
var sender pktgen.Sender
switch method {
case "af_pcap":
sender = pktgen.NewAFPacketSender(iface, srcIP, dstIP, srcPort, dstPort, payloadSize, srcMAC, dstMAC)
case "af_packet":
sender = pktgen.NewAFPacketSender(iface, srcIP, dstIP, srcPort, dstPort, payloadSize, srcMAC, dstMAC)
case "af_xdp":
sender = pktgen.NewAFXdpSender(iface, srcIP, dstIP, srcPort, dstPort, payloadSize, srcMAC, dstMAC, streamNum-1)
case "net_conn":
sender = pktgen.NewNetConnSender(dstIP, dstPort, payloadSize)
case "udp_syscall":
sender = pktgen.NewAFInetSyscallSender(dstIP, dstPort, payloadSize)
case "raw_socket":
sender = pktgen.NewRawSocketSender(srcIP, dstIP, srcPort, dstPort, payloadSize)
case "pkt_conn":
sender = pktgen.NewBatchConnSender(dstIP, dstPort, payloadSize)
default:
log.Fatalf("Unsupported method: %s", method)
}
runTest(sender, duration, iface, payloadSize)
}
func setupSignalHandler(cancelFunc context.CancelFunc) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
fmt.Println("\nReceived interrupt signal, stopping all streams...")
cancelFunc()
}()
}
func init() {
rootCmd.PersistentFlags().StringVar(&method, "method", "af_packet", "method to use for sending packets [af_packet, net_conn, udp_syscall, raw_socket, af_pcap, pkt_conn, benchmark]")
rootCmd.PersistentFlags().StringVar(&iface, "iface", "eth0", "Interface to use")
rootCmd.PersistentFlags().StringVar(&srcIP, "srcip", "192.168.64.1", "Source IP address")
rootCmd.PersistentFlags().StringVar(&dstIP, "dstip", "192.168.64.2", "Destination IP address")
rootCmd.PersistentFlags().IntVar(&srcPort, "srcport", 12345, "Source UDP port")
rootCmd.PersistentFlags().IntVar(&dstPort, "dstport", 12345, "Destination UDP port")
rootCmd.PersistentFlags().IntVar(&payloadSize, "payloadsize", 100, "Size of the payload in bytes")
rootCmd.PersistentFlags().StringVar(&srcMAC, "srcmac", "de:ad:be:ef:ca:fe", "Source MAC address")
rootCmd.PersistentFlags().StringVar(&dstMAC, "dstmac", "c0:ff:ee:00:00:00", "Destination MAC address")
rootCmd.PersistentFlags().IntVar(&duration, "duration", 5, "Duration of the benchmark in seconds")
rootCmd.PersistentFlags().IntVar(&streams, "streams", 1, "Number of concurrent streams for sending packets")
}
func main() {
// should only run on linux
if runtime.GOOS != "linux" {
fmt.Println("This tool only runs on Linux")
os.Exit(1)
}
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}