From b7dc4203c161de81191c427667a673944f5b4f23 Mon Sep 17 00:00:00 2001 From: Fabian Wienand Date: Thu, 8 Aug 2024 09:46:21 +0200 Subject: [PATCH] cmds/exp/tcpdump: add tests Signed-off-by: Fabian Wienand --- cmds/exp/tcpdump/domain_test.go | 85 ++++++++++ cmds/exp/tcpdump/ethernet.go | 5 +- cmds/exp/tcpdump/ethernet_test.go | 100 ++++++++++++ cmds/exp/tcpdump/filter.txt | 1 + cmds/exp/tcpdump/icmp_test.go | 64 ++++++++ cmds/exp/tcpdump/main.go | 198 ++++++++++------------ cmds/exp/tcpdump/main_test.go | 200 +++++++++++++++++++++++ cmds/exp/tcpdump/tcp.go | 22 +-- cmds/exp/tcpdump/tcp_test.go | 262 ++++++++++++++++++++++++++++++ cmds/exp/tcpdump/util.go | 59 ++++--- cmds/exp/tcpdump/util_test.go | 130 +++++++++++++++ 11 files changed, 979 insertions(+), 147 deletions(-) create mode 100644 cmds/exp/tcpdump/domain_test.go create mode 100644 cmds/exp/tcpdump/ethernet_test.go create mode 100644 cmds/exp/tcpdump/filter.txt create mode 100644 cmds/exp/tcpdump/icmp_test.go create mode 100644 cmds/exp/tcpdump/main_test.go create mode 100644 cmds/exp/tcpdump/tcp_test.go create mode 100644 cmds/exp/tcpdump/util_test.go diff --git a/cmds/exp/tcpdump/domain_test.go b/cmds/exp/tcpdump/domain_test.go new file mode 100644 index 0000000000..90dd5b727f --- /dev/null +++ b/cmds/exp/tcpdump/domain_test.go @@ -0,0 +1,85 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "testing" + + "github.com/gopacket/gopacket/layers" +) + +func TestDnsData(t *testing.T) { + tests := []struct { + name string + layer *layers.DNS + expected string + }{ + { + name: "No error response code, no answers", + layer: &layers.DNS{ + ID: 1234, + ResponseCode: layers.DNSResponseCodeNoErr, + Questions: []layers.DNSQuestion{ + {Type: layers.DNSTypeA, Name: []byte("example.com")}, + }, + }, + expected: "1234 A? example.com (0)", + }, + { + name: "Error response code", + layer: &layers.DNS{ + ID: 5678, + ResponseCode: layers.DNSResponseCodeNXDomain, + }, + expected: "5678 Non-Existent Domain (0)", + }, + { + name: "answer", + layer: &layers.DNS{ + ID: 9101, + ResponseCode: layers.DNSResponseCodeNoErr, + AA: true, + Answers: []layers.DNSResourceRecord{ + {Name: []byte("example.com"), Type: layers.DNSTypeA, Class: layers.DNSClassAny, IP: []byte{192, 0, 2, 1}}, + }, + }, + expected: "9101* (0)", + }, + { + name: " 2 answers", + layer: &layers.DNS{ + ID: 9101, + ResponseCode: layers.DNSResponseCodeNoErr, + AA: true, + Answers: []layers.DNSResourceRecord{ + {Name: []byte("example.com"), Type: layers.DNSTypeA, Class: layers.DNSClassAny, IP: []byte{192, 0, 2, 1}}, + {Name: []byte("example.io"), Type: layers.DNSTypeA, Class: layers.DNSClassAny, IP: []byte{192, 0, 2, 2}}, + }, + }, + expected: "9101* 2/0/0 , (0)", + }, + { + name: "Recursive desired", + layer: &layers.DNS{ + ID: 1213, + ResponseCode: layers.DNSResponseCodeNoErr, + RD: true, + Answers: []layers.DNSResourceRecord{ + {Name: []byte("example.com"), Type: layers.DNSTypeA, Class: layers.DNSClassAny, IP: []byte{192, 0, 2, 1}}, + }, + }, + expected: "1213+ (0)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := dnsData(tt.layer) + if result != tt.expected { + t.Errorf("dnsData() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/cmds/exp/tcpdump/ethernet.go b/cmds/exp/tcpdump/ethernet.go index c509518c0e..fdd2384eee 100644 --- a/cmds/exp/tcpdump/ethernet.go +++ b/cmds/exp/tcpdump/ethernet.go @@ -10,8 +10,8 @@ import ( ) func (cmd cmd) ethernetInfo(ethernetLayer gopacket.LinkLayer, networkLayer gopacket.NetworkLayer) string { - if !cmd.Opts.ether { - return fmt.Sprintf("%s %s", networkLayer.NetworkFlow().EndpointType(), cmd.Opts.device) + if !cmd.Opts.Ether { + return fmt.Sprintf("%s %s", networkLayer.NetworkFlow().EndpointType(), cmd.Opts.Device) } src, dst := ethernetLayer.LinkFlow().Endpoints() @@ -20,6 +20,7 @@ func (cmd cmd) ethernetInfo(ethernetLayer gopacket.LinkLayer, networkLayer gopac if dstHost == "ff:ff:ff:ff:ff:ff" { dstHost = "Broadcast" } + length := len(ethernetLayer.LayerContents()) + len(ethernetLayer.LayerPayload()) return fmt.Sprintf("%s > %s, ethertype %s, length %d:", src, dstHost, networkLayer.NetworkFlow().EndpointType(), length) diff --git a/cmds/exp/tcpdump/ethernet_test.go b/cmds/exp/tcpdump/ethernet_test.go new file mode 100644 index 0000000000..25baeaecee --- /dev/null +++ b/cmds/exp/tcpdump/ethernet_test.go @@ -0,0 +1,100 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "testing" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +func TestEthernetInfo(t *testing.T) { + tests := []struct { + name string + cmd cmd + ethernetLayer gopacket.LinkLayer + networkLayer gopacket.NetworkLayer + expectedOutput string + }{ + { + name: "Ether option disabled", + cmd: cmd{ + Opts: flags{ + Ether: false, + Device: "eth0", + }, + }, + ethernetLayer: &layers.Ethernet{ + SrcMAC: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: []byte{0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB}, + EthernetType: layers.EthernetTypeIPv4, + }, + networkLayer: &layers.IPv4{ + SrcIP: []byte{192, 168, 0, 1}, + DstIP: []byte{192, 168, 0, 2}, + Protocol: layers.IPProtocolTCP, + }, + expectedOutput: "IPv4 eth0", + }, + { + name: "Ether option enabled with broadcast", + cmd: cmd{ + Opts: flags{ + Ether: true, + Device: "eth0", + }, + }, + ethernetLayer: &layers.Ethernet{ + SrcMAC: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + EthernetType: layers.EthernetTypeIPv4, + Length: 14, + BaseLayer: layers.BaseLayer{ + Contents: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0x08, 0x00}, + }, + }, + networkLayer: &layers.IPv4{ + SrcIP: []byte{192, 168, 0, 1}, + DstIP: []byte{192, 168, 0, 2}, + Protocol: layers.IPProtocolTCP, + }, + expectedOutput: "00:11:22:33:44:55 > Broadcast, ethertype IPv4, length 14:", + }, + { + name: "Ether option enabled with unicast", + cmd: cmd{ + Opts: flags{ + Ether: true, + Device: "eth0", + }, + }, + ethernetLayer: &layers.Ethernet{ + SrcMAC: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: []byte{0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB}, + EthernetType: layers.EthernetTypeIPv4, + Length: 14, + BaseLayer: layers.BaseLayer{ + Contents: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0x08, 0x00}, + }, + }, + networkLayer: &layers.IPv4{ + SrcIP: []byte{192, 168, 0, 1}, + DstIP: []byte{192, 168, 0, 2}, + Protocol: layers.IPProtocolTCP, + }, + expectedOutput: "00:11:22:33:44:55 > 66:77:88:99:aa:bb, ethertype IPv4, length 14:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.cmd.ethernetInfo(tt.ethernetLayer, tt.networkLayer) + if result != tt.expectedOutput { + t.Errorf("ethernetInfo() = %v, want %v", result, tt.expectedOutput) + } + }) + } +} diff --git a/cmds/exp/tcpdump/filter.txt b/cmds/exp/tcpdump/filter.txt new file mode 100644 index 0000000000..f5483dbfe3 --- /dev/null +++ b/cmds/exp/tcpdump/filter.txt @@ -0,0 +1 @@ +tcp \ No newline at end of file diff --git a/cmds/exp/tcpdump/icmp_test.go b/cmds/exp/tcpdump/icmp_test.go new file mode 100644 index 0000000000..8afaa98c89 --- /dev/null +++ b/cmds/exp/tcpdump/icmp_test.go @@ -0,0 +1,64 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "testing" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +func TestParseICMP(t *testing.T) { + tests := []struct { + name string + packet gopacket.Packet + expectedOutput string + }{ + { + name: "ICMPv4 Echo Request", + packet: gopacket.NewPacket( + []byte{ + 0x08, 0x00, 0x4d, 0x3d, 0x00, 0x46, 0x00, 0x01, 0x61, 0x62, 0x63, 0x64, + }, + layers.LayerTypeICMPv4, + gopacket.Default, + ), + expectedOutput: "ICMP EchoRequest, id 70, seq 1, length 12", + }, + { + name: "ICMPv6 Echo Request", + packet: gopacket.NewPacket( + []byte{ + 0x80, 0x00, 0x4d, 0x3d, 0x1c, 0x46, 0x00, 0x01, 0x61, 0x62, 0x63, 0x64, + }, + layers.LayerTypeICMPv6, + gopacket.Default, + ), + expectedOutput: "ICMP6 EchoRequest, length 12", + }, + { + name: "Non-ICMP Packet", + packet: gopacket.NewPacket( + []byte{ + 0x45, 0x00, 0x00, 0x3c, 0x1c, 0x46, 0x40, 0x00, 0x40, 0x06, 0xb1, 0xe6, 0xc0, 0xa8, 0x00, 0x68, + 0xc0, 0xa8, 0x00, 0x01, + }, + layers.LayerTypeIPv4, + gopacket.Default, + ), + expectedOutput: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseICMP(tt.packet) + if result != tt.expectedOutput { + t.Errorf("parseICMP() = %v, want %v", result, tt.expectedOutput) + } + }) + } +} diff --git a/cmds/exp/tcpdump/main.go b/cmds/exp/tcpdump/main.go index f79e92f18c..c97be9997d 100644 --- a/cmds/exp/tcpdump/main.go +++ b/cmds/exp/tcpdump/main.go @@ -26,31 +26,31 @@ import ( ) type flags struct { - help bool - countPkg int - filter string - snapshotLength int - device string - noPromisc bool - count bool - listDevices bool - numerical bool - number bool - t bool - tt bool - ttt bool - tttt bool - ttttt bool - firstPacketProcessed bool - verbose bool - data bool - dataWithHeader bool - quiet bool - ascii bool - ether bool - filterFile string - timeStampInNanoSeconds bool - icmpOnly bool + Help bool + CountPkg int + Filter string + SnapshotLength int + Device string + NoPromisc bool + Count bool + ListDevices bool + Numerical bool + Number bool + T bool + TT bool + TTT bool + TTTT bool + TTTTT bool + FirstPacketProcessed bool + Verbose bool + Data bool + DataWithHeader bool + Quiet bool + ASCII bool + Ether bool + FilterFile string + TimeStampInNanoSeconds bool + IcmpOnly bool } const tcpdumpHelp = ` tcpdump [ -ADehnpqtvx# ] [ -icmp ] @@ -67,36 +67,36 @@ func parseFlags(args []string, out io.Writer) (cmd, error) { opts := flags{} fs := flag.NewFlagSet(args[0], flag.ExitOnError) - fs.IntVar(&opts.countPkg, "c", 0, "Exit after receiving count packets") - fs.BoolVar(&opts.help, "help", false, "Print help message") - fs.BoolVar(&opts.help, "h", false, "Print help message") - fs.StringVar(&opts.device, "i", "", "Listen on interface") - fs.StringVar(&opts.device, "interface", "", "Listen on interface") - fs.IntVar(&opts.snapshotLength, "s", 262144, "snarf snaplen bytes of data from each packet rather than the default of 262144 bytes") - fs.IntVar(&opts.snapshotLength, "snapshot-length", 262144, "narf snaplen bytes of data from each packet rather than the default of 262144 bytes") - fs.BoolVar(&opts.noPromisc, "p", false, "Set non-promiscuous mode") - fs.BoolVar(&opts.noPromisc, "no-promiscuous-mode", false, "Set non-promiscuous mode") - fs.BoolVar(&opts.count, "count", false, "Print only the number of packets captured") - fs.BoolVar(&opts.listDevices, "D", false, "Print the list of the network interfaces available on the system and on which tcpdump can capture packets") - fs.BoolVar(&opts.listDevices, "list-interfaces", false, "Print the list of the network interfaces available on the system and on which tcpdump can capture packets") - fs.BoolVar(&opts.numerical, "n", false, "Don't convert addresses (i.e., host addresses, port numbers, etc.) to names") - fs.BoolVar(&opts.number, "#", false, " Print an optional packet number at the beginning of the line") - fs.BoolVar(&opts.number, "number", false, " Print an optional packet number at the beginning of the line") - fs.BoolVar(&opts.icmpOnly, "icmp", false, "Only capture ICMP packets") - fs.BoolVar(&opts.ether, "e", false, "Print the link-level header on each dump line. This can be used, for example, to print MAC layer addresses for protocols such as Ethernet and IEEE 802.11.") - fs.BoolVar(&opts.t, "t", false, "Don't print a timestamp on each dump line") - fs.BoolVar(&opts.tt, "tt", false, "Print the timestamp, as seconds since January 1, 1970, 00:00:00, UTC, and fractions of a second since that time, on each dump line") - fs.BoolVar(&opts.ttt, "ttt", false, "Print a delta (microsecond or nanosecond resolution depending on the --time-stamp-precision option) between current and previous line on each dump line. The default is microsecond resolution") - fs.BoolVar(&opts.tttt, "tttt", false, "Print a timestamp, as hours, minutes, seconds, and fractions of a second since midnight, preceded by the date, on each dump line") - fs.BoolVar(&opts.ttttt, "ttttt", false, "Print a delta (microsecond or nanosecond resolution depending on the --time-stamp-precision option) between current and first line on each dump line. The default is microsecond resolution") - fs.BoolVar(&opts.timeStampInNanoSeconds, "nano", false, "Print the timestamp in nanosecond resolution (instead of microseconds)") - fs.BoolVar(&opts.data, "x", false, "When parsing and printing, in addition to printing the headers of each packet, print the data of each packet (minus its link level header) in hex") - fs.BoolVar(&opts.dataWithHeader, "xx", false, "When parsing and printing, in addition to printing the headers of each packet, print the data of each packet (including its link level header) in hex") - fs.StringVar(&opts.filterFile, "F", "", "Use file as input for the filter expression. An additional expression given on the command line is ignored.") - fs.BoolVar(&opts.ascii, "A", false, "Print each packet (minus its link level header) in ASCII. Handy for capturing web pages") - fs.BoolVar(&opts.quiet, "q", false, "Quiet output. Print less protocol information so output lines are shorter") - fs.BoolVar(&opts.verbose, "v", false, "When parsing and printing, produce (slightly more) verbose output. For example, the time to live, identification, total length and options in an IP packet are printed. Also enables additional packet integrity checks such as verifying the IP and ICMP header checksum") - fs.BoolVar(&opts.verbose, "verbose", false, "When parsing and printing, produce (slightly more) verbose output. For example, the time to live, identification, total length and options in an IP packet are printed. Also enables additional packet integrity checks such as verifying the IP and ICMP header checksum") + fs.IntVar(&opts.CountPkg, "c", 0, "Exit after receiving count packets") + fs.BoolVar(&opts.Help, "help", false, "Print help message") + fs.BoolVar(&opts.Help, "h", false, "Print help message") + fs.StringVar(&opts.Device, "i", "", "Listen on interface") + fs.StringVar(&opts.Device, "interface", "", "Listen on interface") + fs.IntVar(&opts.SnapshotLength, "s", 262144, "snarf snaplen bytes of data from each packet rather than the default of 262144 bytes") + fs.IntVar(&opts.SnapshotLength, "snapshot-length", 262144, "narf snaplen bytes of data from each packet rather than the default of 262144 bytes") + fs.BoolVar(&opts.NoPromisc, "p", false, "Set non-promiscuous mode") + fs.BoolVar(&opts.NoPromisc, "no-promiscuous-mode", false, "Set non-promiscuous mode") + fs.BoolVar(&opts.Count, "count", false, "Print only the number of packets captured") + fs.BoolVar(&opts.ListDevices, "D", false, "Print the list of the network interfaces available on the system and on which tcpdump can capture packets") + fs.BoolVar(&opts.ListDevices, "list-interfaces", false, "Print the list of the network interfaces available on the system and on which tcpdump can capture packets") + fs.BoolVar(&opts.Numerical, "n", false, "Don't convert addresses (i.e., host addresses, port numbers, etc.) to names") + fs.BoolVar(&opts.Number, "#", false, " Print an optional packet number at the beginning of the line") + fs.BoolVar(&opts.Number, "number", false, " Print an optional packet number at the beginning of the line") + fs.BoolVar(&opts.IcmpOnly, "icmp", false, "Only capture ICMP packets") + fs.BoolVar(&opts.Ether, "e", false, "Print the link-level header on each dump line. This can be used, for example, to print MAC layer addresses for protocols such as Ethernet and IEEE 802.11.") + fs.BoolVar(&opts.T, "t", false, "Don't print a timestamp on each dump line") + fs.BoolVar(&opts.TT, "tt", false, "Print the timestamp, as seconds since January 1, 1970, 00:00:00, UTC, and fractions of a second since that time, on each dump line") + fs.BoolVar(&opts.TTT, "ttt", false, "Print a delta (microsecond or nanosecond resolution depending on the --time-stamp-precision option) between current and previous line on each dump line. The default is microsecond resolution") + fs.BoolVar(&opts.TTTT, "tttt", false, "Print a timestamp, as hours, minutes, seconds, and fractions of a second since midnight, preceded by the date, on each dump line") + fs.BoolVar(&opts.TTTTT, "ttttt", false, "Print a delta (microsecond or nanosecond resolution depending on the --time-stamp-precision option) between current and first line on each dump line. The default is microsecond resolution") + fs.BoolVar(&opts.TimeStampInNanoSeconds, "nano", false, "Print the timestamp in nanosecond resolution (instead of microseconds)") + fs.BoolVar(&opts.Data, "x", false, "When parsing and printing, in addition to printing the headers of each packet, print the data of each packet (minus its link level header) in hex") + fs.BoolVar(&opts.DataWithHeader, "xx", false, "When parsing and printing, in addition to printing the headers of each packet, print the data of each packet (including its link level header) in hex") + fs.StringVar(&opts.FilterFile, "F", "", "Use file as input for the filter expression. An additional expression given on the command line is ignored.") + fs.BoolVar(&opts.ASCII, "A", false, "Print each packet (minus its link level header) in ASCII. Handy for capturing web pages") + fs.BoolVar(&opts.Quiet, "q", false, "Quiet output. Print less protocol information so output lines are shorter") + fs.BoolVar(&opts.Verbose, "v", false, "When parsing and printing, produce (slightly more) verbose output. For example, the time to live, identification, total length and options in an IP packet are printed. Also enables additional packet integrity checks such as verifying the IP and ICMP header checksum") + fs.BoolVar(&opts.Verbose, "verbose", false, "When parsing and printing, produce (slightly more) verbose output. For example, the time to live, identification, total length and options in an IP packet are printed. Also enables additional packet integrity checks such as verifying the IP and ICMP header checksum") fs.Usage = func() { fmt.Fprintf(out, "%s\n\n", tcpdumpHelp) @@ -106,7 +106,7 @@ func parseFlags(args []string, out io.Writer) (cmd, error) { fs.Parse(unixflag.ArgsToGoArgs(args[1:])) - if opts.verbose && opts.quiet { + if opts.Verbose && opts.Quiet { return cmd{}, fmt.Errorf("cannot use both -v and -q flags") } @@ -117,21 +117,21 @@ func parseFlags(args []string, out io.Writer) (cmd, error) { } } - opts.filter = filter + opts.Filter = filter - if opts.filterFile != "" { - if data, err := os.ReadFile(opts.filterFile); err == nil { - opts.filter = string(data) + if opts.FilterFile != "" { + if data, err := os.ReadFile(opts.FilterFile); err == nil { + opts.Filter = string(data) } else { return cmd{}, fmt.Errorf("failed to read filter file: %v", err) } } - return cmd{Opts: opts, out: out}, nil + return cmd{Opts: opts, Out: out}, nil } type cmd struct { - out io.Writer + Out io.Writer Opts flags } @@ -141,17 +141,17 @@ func (cmd *cmd) run() error { err error ) - if cmd.Opts.help { + if cmd.Opts.Help { fmt.Println(tcpdumpHelp) return nil } - if cmd.Opts.listDevices { + if cmd.Opts.ListDevices { return listDevices() } - if cmd.Opts.device == "" { + if cmd.Opts.Device == "" { return fmt.Errorf("no device specified") } @@ -167,7 +167,7 @@ func (cmd *cmd) run() error { cancel() }() - if src, err = pcap.OpenLive(cmd.Opts.device, int32(cmd.Opts.snapshotLength), !cmd.Opts.noPromisc, 0, false); err != nil { + if src, err = pcap.OpenLive(cmd.Opts.Device, int32(cmd.Opts.SnapshotLength), !cmd.Opts.NoPromisc, 0, false); err != nil { if strings.Contains(err.Error(), "operation not permitted") { return fmt.Errorf("you don't have permission to capture on that/these device(s)") } @@ -177,39 +177,39 @@ func (cmd *cmd) run() error { } defer src.Close() - if err := src.SetBPFFilter(cmd.Opts.filter); err != nil { + if err := src.SetBPFFilter(cmd.Opts.Filter); err != nil { return err } packetSource := gopacket.NewPacketSource(src, layers.LinkTypeEthernet) packetSource.NoCopy = true - fmt.Fprintf(cmd.out, "tcpdump: verbose output suppressed, use -v for full protocol decode\nlistening on %s, link-type %d, snapshot length %d bytes\n", cmd.Opts.device, src.LinkType(), cmd.Opts.snapshotLength) + fmt.Fprintf(cmd.Out, "tcpdump: verbose output suppressed, use -v for full protocol decode\nlistening on %s, link-type %d, snapshot length %d bytes\n", cmd.Opts.Device, src.LinkType(), cmd.Opts.SnapshotLength) var ( capturedPackets int timeStamp time.Time ) - if cmd.Opts.ttttt { + if cmd.Opts.TTTTT { timeStamp = time.Now() } for { select { case <-ctx.Done(): - fmt.Fprintf(cmd.out, "\n%d packets captured\n", capturedPackets) + fmt.Fprintf(cmd.Out, "\n%d packets captured\n", capturedPackets) return nil case packet := <-packetSource.PacketsCtx(ctx): capturedPackets++ - if cmd.Opts.countPkg > 0 && capturedPackets >= cmd.Opts.countPkg { + if cmd.Opts.CountPkg > 0 && capturedPackets >= cmd.Opts.CountPkg { return nil } - if !cmd.Opts.count { + if !cmd.Opts.Count { pkgTime := cmd.processPacket(packet, capturedPackets, timeStamp) - if cmd.Opts.ttt { + if cmd.Opts.TTT { timeStamp = pkgTime } } @@ -230,7 +230,7 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp timeStamp string ) - if cmd.Opts.number { + if cmd.Opts.Number { no = fmt.Sprintf("%d ", num) } @@ -239,7 +239,7 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp } if err := packet.ErrorLayer(); err != nil { - fmt.Fprintf(cmd.out, "skipping packet no. %d: %v\n", num, err) + fmt.Fprintf(cmd.Out, "skipping packet no. %d: %v\n", num, err) return lastPkgTimeStamp } @@ -257,7 +257,7 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp etherInfo := cmd.ethernetInfo(ethernetLayer, networkLayer) - if cmd.Opts.verbose { + if cmd.Opts.Verbose { switch layer := networkLayer.(type) { case *layers.IPv4: etherInfo += fmt.Sprintf(" (tos 0x%x, ttl %d, id %d, offset %d, flags [%s], proto %s (%d), length %d)\n", layer.TOS, layer.TTL, layer.Id, layer.FragOffset, layer.Flags, layer.Protocol, layer.Protocol, len(layer.Contents)+len(layer.Payload)) @@ -270,11 +270,11 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp srcAddr, dstAddr = networkSrc.String(), networkDst.String() - if srcHostNames, err := net.LookupAddr(srcAddr); err == nil && len(srcHostNames) > 0 && !cmd.Opts.numerical { + if srcHostNames, err := net.LookupAddr(srcAddr); err == nil && len(srcHostNames) > 0 && !cmd.Opts.Numerical { srcAddr = srcHostNames[0] } - if dstHostNames, err := net.LookupAddr(dstAddr); err == nil && len(dstHostNames) > 0 && !cmd.Opts.numerical { + if dstHostNames, err := net.LookupAddr(dstAddr); err == nil && len(dstHostNames) > 0 && !cmd.Opts.Numerical { dstAddr = dstHostNames[0] } @@ -288,7 +288,7 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp data := parseICMP(packet) - if cmd.Opts.icmpOnly && data == "" { + if cmd.Opts.IcmpOnly && data == "" { return lastPkgTimeStamp } @@ -304,7 +304,7 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp // parse the application layer applicationLayer := packet.ApplicationLayer() - if applicationLayer != nil && !cmd.Opts.quiet { + if applicationLayer != nil && !cmd.Opts.Quiet { switch layer := applicationLayer.(type) { case *layers.DNS: data = dnsData(layer) @@ -322,7 +322,7 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp switch layer := transportLayer.(type) { case *layers.TCP: - data = tcpData(layer, length, cmd.Opts.verbose, cmd.Opts.quiet) + data = tcpData(layer, length, cmd.Opts.Verbose, cmd.Opts.Quiet) case *layers.UDP: data = fmt.Sprintf("UDP, length %d", length) case *layers.UDPLite: @@ -336,7 +336,7 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp timeStamp = cmd.parseTimeStamp(pkgTimeStamp, lastPkgTimeStamp) - fmt.Fprintf(cmd.out, "%s%s %s %s%s > %s%s: %s\n", + fmt.Fprintf(cmd.Out, "%s%s %s %s%s > %s%s: %s\n", no, timeStamp, etherInfo, @@ -347,39 +347,17 @@ func (cmd *cmd) processPacket(packet gopacket.Packet, num int, lastPkgTimeStamp data) switch { - case cmd.Opts.ascii: - fmt.Fprintf(cmd.out, "%s\n", applicationLayer.LayerContents()) - case cmd.Opts.data: - fmt.Fprintf(cmd.out, "%s\n", formatPacketData(packet.Data()[14:])) - case cmd.Opts.dataWithHeader: - fmt.Fprintf(cmd.out, "%s\n", formatPacketData(packet.Data())) + case cmd.Opts.ASCII: + fmt.Fprintf(cmd.Out, "%s\n", applicationLayer.LayerContents()) + case cmd.Opts.Data: + fmt.Fprintf(cmd.Out, "%s\n", formatPacketData(packet.Data()[14:])) + case cmd.Opts.DataWithHeader: + fmt.Fprintf(cmd.Out, "%s\n", formatPacketData(packet.Data())) } return pkgTimeStamp } -func formatPacketData(data []byte) string { - var result string - for i := 0; i < len(data); i += 16 { - // Print the offset - result += fmt.Sprintf("0x%04x: ", i) - - // Print the hex values - for j := 0; j < 16; j++ { - if i+j < len(data) { - result += fmt.Sprintf("%02x", data[i+j]) - } else { - result += " " - } - if j%2 == 1 { - result += " " - } - } - result += "\n" - } - return result -} - func main() { // Disable logrus logging logrus.SetLevel(logrus.PanicLevel) diff --git a/cmds/exp/tcpdump/main_test.go b/cmds/exp/tcpdump/main_test.go new file mode 100644 index 0000000000..3fc2e95099 --- /dev/null +++ b/cmds/exp/tcpdump/main_test.go @@ -0,0 +1,200 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package main + +import ( + "bytes" + "os" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +func TestParseFlags(t *testing.T) { + tmpDir := t.TempDir() + tmp, err := os.CreateTemp(tmpDir, "filter.txt") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + + tmp.Write([]byte("tcp port 80")) + + tests := []struct { + name string + args []string + expectedCmd cmd + expectedErr bool + }{ + { + name: "default with count limit", + args: []string{"cmd", "-c", "10"}, + expectedCmd: cmd{ + Opts: flags{ + CountPkg: 10, + SnapshotLength: 262144, + }, + }, + expectedErr: false, + }, + { + name: "help", + args: []string{"cmd", "-h"}, + expectedCmd: cmd{ + Opts: flags{ + SnapshotLength: 262144, + Help: true, + }, + }, + expectedErr: false, + }, + { + name: "verbose and quiet", + args: []string{"cmd", "-v", "-q"}, + expectedErr: true, + }, + { + name: "filter file", + args: []string{"cmd", "-F", tmp.Name()}, + expectedCmd: cmd{ + Opts: flags{ + SnapshotLength: 262144, + Filter: "tcp port 80", + FilterFile: tmp.Name(), + }, + }, + }, + { + name: "missing filter file", + args: []string{"cmd", "-F", "xyz"}, + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := &bytes.Buffer{} + cmd, err := parseFlags(tt.args, out) + if (err != nil) != tt.expectedErr { + t.Errorf("parseFlags() error = %v, expectedErr %v", err, tt.expectedErr) + } + + if !tt.expectedErr { + if diff := cmp.Diff(tt.expectedCmd, cmd, cmpopts.IgnoreFields(cmd, "Out")); diff != "" { + t.Errorf("parseFlags() mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestProcessPacket(t *testing.T) { + tests := []struct { + name string + packetData []byte + opts flags + num int + lastPkgTimeStamp time.Time + expectedOutput string + }{ + { + name: "IPv4 TCP packet", + packetData: []byte{ + // Ethernet header + 0x00, 0x1c, 0x42, 0x00, 0x00, 0x08, 0x00, 0x1c, 0x42, 0x00, 0x00, 0x01, 0x08, 0x00, + // IPv4 header + 0x45, 0x00, 0x00, 0x3c, 0x1c, 0x46, 0x40, 0x00, 0x40, 0x06, 0xb1, 0xe6, 0xc0, 0xa8, 0x00, 0x68, 0xc0, 0xa8, 0x00, 0x01, + // TCP header + 0x00, 0x50, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x02, 0x20, 0x00, 0x91, 0x7c, 0x00, 0x00, + }, + num: 1, + lastPkgTimeStamp: time.Now(), + opts: flags{Number: true, Verbose: true, Device: "eth0", Numerical: true}, + expectedOutput: "1 00:00:00.000000 IPv4 eth0 (tos 0x0, ttl 64, id 7238, offset 0, flags [DF], proto TCP (6), length 40)\n 192.168.0.104.80 > 192.168.0.1.80: Flags [S], cksum 0x917c, seq 0, ack 0, win 8192, options [], length 0\n", + }, + { + name: "IPv4 TCP packet with quiet flag", + packetData: []byte{ + // Ethernet header + 0x00, 0x1c, 0x42, 0x00, 0x00, 0x08, 0x00, 0x1c, 0x42, 0x00, 0x00, 0x01, 0x08, 0x00, + // IPv4 header + 0x45, 0x00, 0x00, 0x3c, 0x1c, 0x46, 0x40, 0x00, 0x40, 0x06, 0xb1, 0xe6, 0xc0, 0xa8, 0x00, 0x68, 0xc0, 0xa8, 0x00, 0x01, + // TCP header + 0x00, 0x50, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x02, 0x20, 0x00, 0x91, 0x7c, 0x00, 0x00, + }, + num: 1, + lastPkgTimeStamp: time.Now(), + opts: flags{Quiet: true, Device: "eth0", Numerical: true}, + expectedOutput: "00:00:00.000000 IPv4 eth0 192.168.0.104.80 > 192.168.0.1.80: TCP, length 0\n", + }, + { + name: "IPv4 UDP packet", + packetData: []byte{ + // Ethernet header + 0x00, 0x1c, 0x42, 0x00, 0x00, 0x08, 0x00, 0x1c, 0x42, 0x00, 0x00, 0x01, 0x08, 0x00, + // IPv4 header + 0x45, 0x00, 0x00, 0x1c, 0x1c, 0x46, 0x40, 0x00, 0x40, 0x11, 0xb1, 0xe6, 0xc0, 0xa8, 0x00, 0x68, 0xc0, 0xa8, 0x00, 0x01, + // UDP header + 0x00, 0x35, 0x00, 0x35, 0x00, 0x08, 0x91, 0x7c, + }, + num: 2, + lastPkgTimeStamp: time.Now(), + opts: flags{Number: true, Verbose: true, Device: "eth0", Numerical: true}, + expectedOutput: "2 00:00:00.000000 IPv4 eth0 (tos 0x0, ttl 64, id 7238, offset 0, flags [DF], proto UDP (17), length 28)\n 192.168.0.104.53 > 192.168.0.1.53: UDP, length 0\n", + }, + { + name: "IPv4 UDPLite packet", + packetData: []byte{ + // Ethernet header + 0x00, 0x1c, 0x42, 0x00, 0x00, 0x08, 0x00, 0x1c, 0x42, 0x00, 0x00, 0x01, 0x08, 0x00, + // IPv4 header + 0x45, 0x00, 0x00, 0x1c, 0x1c, 0x46, 0x40, 0x00, 0x40, 0x88, 0xb1, 0xe6, 0xc0, 0xa8, 0x00, 0x68, 0xc0, 0xa8, 0x00, 0x01, + // UDPLite header + 0x00, 0x35, 0x00, 0x35, 0x00, 0x08, 0x91, 0x7c, + }, + num: 3, + lastPkgTimeStamp: time.Now(), + opts: flags{Number: true, Device: "eth0", Numerical: true}, + expectedOutput: "3 00:00:00.000000 IPv4 eth0 192.168.0.104.53 > 192.168.0.1.53: UDPLite, length 0\n", + }, + { + name: "IPv4 DNS packet and data with header flag", + packetData: []byte{ + // Ethernet header + 0x00, 0x1c, 0x42, 0x00, 0x00, 0x08, 0x00, 0x1c, 0x42, 0x00, 0x00, 0x01, 0x08, 0x00, + // IPv4 header + 0x45, 0x00, 0x00, 0x3c, 0x1c, 0x46, 0x40, 0x00, 0x40, 0x11, 0xb1, 0xe6, 0xc0, 0xa8, 0x00, 0x68, 0xc0, 0xa8, 0x00, 0x01, + // UDP header + 0x00, 0x35, 0x00, 0x35, 0x00, 0x28, 0x91, 0x7c, + // DNS header + 0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, + }, + num: 4, + lastPkgTimeStamp: time.Now(), + opts: flags{DataWithHeader: true, Device: "eth0", Numerical: true}, + expectedOutput: "00:00:00.000000 IPv4 eth0 192.168.0.104.53 > 192.168.0.1.53: 4660+ A? www.google.com (32)\n0x0000: 001c 4200 0008 001c 4200 0001 0800 4500 \n0x0010: 003c 1c46 4000 4011 b1e6 c0a8 0068 c0a8 \n0x0020: 0001 0035 0035 0028 917c 1234 0100 0001 \n0x0030: 0000 0000 0000 0377 7777 0667 6f6f 676c \n0x0040: 6503 636f 6d00 0001 0001 \n\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + packet := gopacket.NewPacket(tt.packetData, layers.LayerTypeEthernet, gopacket.Default) + + cmd := &cmd{ + Out: new(bytes.Buffer), + Opts: tt.opts, + } + + cmd.processPacket(packet, tt.num, tt.lastPkgTimeStamp) + + output := cmd.Out.(*bytes.Buffer).String() + if diff := cmp.Diff(tt.expectedOutput, output); diff != "" { + t.Errorf("processPacket() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/cmds/exp/tcpdump/tcp.go b/cmds/exp/tcpdump/tcp.go index 6d343c4c8b..92e48480a2 100644 --- a/cmds/exp/tcpdump/tcp.go +++ b/cmds/exp/tcpdump/tcp.go @@ -78,20 +78,16 @@ func tcpOptions(options []layers.TCPOption) string { // tcpOptionToString returns a string representation of the TCP option. func tcpOptionToString(opt layers.TCPOption) string { - switch opt.OptionType { - case layers.TCPOptionKindMSS: - if len(opt.OptionData) >= 2 { - return fmt.Sprintf("%s val %v", - opt.OptionType, - binary.BigEndian.Uint16(opt.OptionData)) - } + if opt.OptionType == layers.TCPOptionKindMSS && len(opt.OptionData) == 2 { + return fmt.Sprintf("%s val %v", + opt.OptionType, + binary.BigEndian.Uint16(opt.OptionData)) + } - case layers.TCPOptionKindTimestamps: - if len(opt.OptionData) == 8 { - return fmt.Sprintf("%s val %v", - opt.OptionType, - binary.BigEndian.Uint32(opt.OptionData[:4])) - } + if opt.OptionType == layers.TCPOptionKindTimestamps && len(opt.OptionData) == 8 { + return fmt.Sprintf("%s val %v", + opt.OptionType, + binary.BigEndian.Uint32(opt.OptionData[:4])) } return fmt.Sprintf("%s", opt.OptionType) diff --git a/cmds/exp/tcpdump/tcp_test.go b/cmds/exp/tcpdump/tcp_test.go new file mode 100644 index 0000000000..40a7191916 --- /dev/null +++ b/cmds/exp/tcpdump/tcp_test.go @@ -0,0 +1,262 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "testing" + + "github.com/gopacket/gopacket/layers" +) + +func TestTcpFlags(t *testing.T) { + tests := []struct { + name string + layer layers.TCP + expected string + }{ + { + name: "No flags set", + layer: layers.TCP{}, + expected: "", + }, + { + name: "PSH flag set", + layer: layers.TCP{PSH: true}, + expected: "P", + }, + { + name: "FIN flag set", + layer: layers.TCP{FIN: true}, + expected: "F", + }, + { + name: "SYN flag set", + layer: layers.TCP{SYN: true}, + expected: "S", + }, + { + name: "RST flag set", + layer: layers.TCP{RST: true}, + expected: "R", + }, + { + name: "URG flag set", + layer: layers.TCP{URG: true}, + expected: "U", + }, + { + name: "ECE flag set", + layer: layers.TCP{ECE: true}, + expected: "E", + }, + { + name: "CWR flag set", + layer: layers.TCP{CWR: true}, + expected: "C", + }, + { + name: "NS flag set", + layer: layers.TCP{NS: true}, + expected: "N", + }, + { + name: "ACK flag set", + layer: layers.TCP{ACK: true}, + expected: ".", + }, + { + name: "Multiple flags set", + layer: layers.TCP{PSH: true, SYN: true, ACK: true}, + expected: "PS.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tcpFlags(tt.layer) + if result != tt.expected { + t.Errorf("tcpFlags() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestTcpOptionToString(t *testing.T) { + tests := []struct { + name string + opt layers.TCPOption + expected string + }{ + { + name: "MSS option with valid data", + opt: layers.TCPOption{ + OptionType: layers.TCPOptionKindMSS, + OptionData: []byte{0x00, 0x64}, + }, + expected: "MSS val 100", + }, + { + name: "MSS option with invalid data", + opt: layers.TCPOption{ + OptionType: layers.TCPOptionKindMSS, + OptionData: []byte{0x00}, + }, + expected: "MSS", + }, + { + name: "Timestamps option with valid data", + opt: layers.TCPOption{ + OptionType: layers.TCPOptionKindTimestamps, + OptionData: []byte{0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x65}, + }, + expected: "Timestamps val 100", + }, + { + name: "Timestamps option with invalid data", + opt: layers.TCPOption{ + OptionType: layers.TCPOptionKindTimestamps, + OptionData: []byte{0x00, 0x00, 0x00, 0x64}, + }, + expected: "Timestamps", + }, + { + name: "Unknown option type", + opt: layers.TCPOption{ + OptionType: 0xFF, + OptionData: []byte{}, + }, + expected: "Unknown(255)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tcpOptionToString(tt.opt) + if result != tt.expected { + t.Errorf("tcpOptionToString() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestTcpOptions(t *testing.T) { + tests := []struct { + name string + options []layers.TCPOption + expected string + }{ + { + name: "No options", + options: []layers.TCPOption{}, + expected: "", + }, + { + name: "Single MSS option", + options: []layers.TCPOption{ + { + OptionType: layers.TCPOptionKindMSS, + OptionData: []byte{0x00, 0x64}, + }, + }, + expected: "MSS val 100", + }, + { + name: "Multiple options", + options: []layers.TCPOption{ + { + OptionType: layers.TCPOptionKindMSS, + OptionData: []byte{0x00, 0x64}, + }, + { + OptionType: layers.TCPOptionKindTimestamps, + OptionData: []byte{0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x65}, + }, + }, + expected: "MSS val 100,Timestamps val 100", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tcpOptions(tt.options) + if result != tt.expected { + t.Errorf("tcpOptions() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestTcpData(t *testing.T) { + tests := []struct { + name string + layer *layers.TCP + length int + verbose bool + quiet bool + expected string + }{ + { + name: "Quiet mode", + layer: &layers.TCP{ + Seq: 1000, + Ack: 2000, + Window: 3000, + Checksum: 0x1234, + Options: []layers.TCPOption{}, + }, + length: 100, + verbose: false, + quiet: true, + expected: "TCP, length 100", + }, + { + name: "Verbose mode", + layer: &layers.TCP{ + Seq: 1000, + Ack: 2000, + Window: 3000, + Checksum: 0x1234, + Options: []layers.TCPOption{ + { + OptionType: layers.TCPOptionKindMSS, + OptionData: []byte{0x00, 0x64}, + }, + }, + }, + length: 100, + verbose: true, + quiet: false, + expected: "Flags [], cksum 0x1234, seq 1000, ack 2000, win 3000, options [MSS val 100], length 100", + }, + { + name: "Default mode", + layer: &layers.TCP{ + Seq: 1000, + Ack: 2000, + Window: 3000, + Checksum: 0x1234, + Options: []layers.TCPOption{ + { + OptionType: layers.TCPOptionKindMSS, + OptionData: []byte{0x00, 0x64}, + }, + }, + }, + length: 100, + verbose: false, + quiet: false, + expected: "Flags [], seq 1000, ack 2000, win 3000, options [MSS val 100], length 100", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tcpData(tt.layer, tt.length, tt.verbose, tt.quiet) + if result != tt.expected { + t.Errorf("tcpData() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/cmds/exp/tcpdump/util.go b/cmds/exp/tcpdump/util.go index 2abc50dd67..ecd61294ae 100644 --- a/cmds/exp/tcpdump/util.go +++ b/cmds/exp/tcpdump/util.go @@ -35,7 +35,7 @@ var wellKnownPortsMap = map[string]string{ // wellKnownPorts returns the well-known name of the port or the port number itself. func (cmd cmd) wellKnownPorts(port string) string { - if name, ok := wellKnownPortsMap[port]; ok && !cmd.Opts.numerical { + if name, ok := wellKnownPortsMap[port]; ok && !cmd.Opts.Numerical { return name } @@ -57,31 +57,46 @@ func listDevices() error { } // parseTimeStamp returns the timestamp in the format specified by the user using the -t, -tt, -ttt, -tttt, -ttttt, -nano flags. -func (cmd *cmd) parseTimeStamp(currentTimestamp, lastTimeStamp time.Time) (timeStamp string) { - switch { - case cmd.Opts.t: +func (cmd *cmd) parseTimeStamp(currentTimestamp, lastTimeStamp time.Time) string { + if cmd.Opts.T { return "" - case cmd.Opts.tt: + } + if cmd.Opts.TT { return fmt.Sprintf("%d", currentTimestamp.Unix()) - case cmd.Opts.ttt, cmd.Opts.ttttt: - switch cmd.Opts.timeStampInNanoSeconds { - case true: - if !cmd.Opts.firstPacketProcessed { - cmd.Opts.firstPacketProcessed = true - return "00:00:00.000000000" + } + if cmd.Opts.TTT { + diff := currentTimestamp.Sub(lastTimeStamp) + if cmd.Opts.TimeStampInNanoSeconds { + return fmt.Sprintf("%02d:%02d:%02d.%09d", int(diff.Hours()), int(diff.Minutes())%60, int(diff.Seconds())%60, diff.Nanoseconds()%1e9) + } + + return fmt.Sprintf("%02d:%02d:%02d.%06d", int(diff.Hours()), int(diff.Minutes())%60, int(diff.Seconds())%60, diff.Microseconds()%1e6) + } + if cmd.Opts.TTTT { + diff := currentTimestamp.Sub(lastTimeStamp) + return fmt.Sprintf("%02d:%02d:%02d", int(diff.Hours()), int(diff.Minutes())%60, int(diff.Seconds())%60) + } + return currentTimestamp.Format("15:04:05.000000") +} + +func formatPacketData(data []byte) string { + var result string + for i := 0; i < len(data); i += 16 { + // Print the offset + result += fmt.Sprintf("0x%04x: ", i) + + // Print the hex values + for j := 0; j < 16; j++ { + if i+j < len(data) { + result += fmt.Sprintf("%02x", data[i+j]) + } else { + result += " " } - return time.Unix(0, 0).Add(currentTimestamp.Sub(lastTimeStamp)).Format("15:04:05.000000000") - default: - if !cmd.Opts.firstPacketProcessed { - cmd.Opts.firstPacketProcessed = true - return "00:00:00.000000" + if j%2 == 1 { + result += " " } - return time.Unix(0, 0).Add(currentTimestamp.Sub(lastTimeStamp)).Format("15:04:05.000000") } - case cmd.Opts.tttt: - midnight := time.Now().Truncate(24 * time.Hour) - return currentTimestamp.Sub(midnight).String() + result += "\n" } - - return currentTimestamp.Format("15:04:05.000000") + return result } diff --git a/cmds/exp/tcpdump/util_test.go b/cmds/exp/tcpdump/util_test.go new file mode 100644 index 0000000000..906e760881 --- /dev/null +++ b/cmds/exp/tcpdump/util_test.go @@ -0,0 +1,130 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "strings" + "testing" + "time" +) + +func TestWellKnownPorts(t *testing.T) { + tests := []struct { + name string + cmd cmd + port string + expected string + }{ + { + name: "HTTP port with name", + cmd: cmd{Opts: flags{Numerical: false}}, + port: "80", + expected: "http", + }, + { + name: "HTTPS port with name", + cmd: cmd{Opts: flags{Numerical: false}}, + port: "443", + expected: "https", + }, + { + name: "Unknown port with numerical", + cmd: cmd{Opts: flags{Numerical: true}}, + port: "8080", + expected: "8080", + }, + { + name: "Known port with numerical", + cmd: cmd{Opts: flags{Numerical: true}}, + port: "22", + expected: "22", + }, + { + name: "Unknown port without numerical", + cmd: cmd{Opts: flags{Numerical: false}}, + port: "8080", + expected: "8080", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.cmd.wellKnownPorts(tt.port) + if result != tt.expected { + t.Errorf("wellKnownPorts(%v) = %v, want %v", tt.port, result, tt.expected) + } + }) + } +} + +func TestParseTimeStamp(t *testing.T) { + tests := []struct { + name string + cmd cmd + currentTimestamp time.Time + lastTimeStamp time.Time + expected string + }{ + { + name: "Option -t", + cmd: cmd{Opts: flags{T: true}}, + currentTimestamp: time.Unix(1633072800, 0), + lastTimeStamp: time.Unix(1633072800, 0), + expected: "", + }, + { + name: "Option -tt", + cmd: cmd{Opts: flags{TT: true}}, + currentTimestamp: time.Unix(1633072800, 0), + lastTimeStamp: time.Unix(1633072700, 0), + expected: "1633072800", + }, + { + name: "Option -ttt with nano", + cmd: cmd{Opts: flags{TTT: true, TimeStampInNanoSeconds: true}}, + currentTimestamp: time.Unix(1633072800, 0), + lastTimeStamp: time.Unix(1633072700, 0), + expected: "00:01:40.000000000", + }, + { + name: "Option -ttt with micro", + cmd: cmd{Opts: flags{TTT: true, TimeStampInNanoSeconds: false}}, + currentTimestamp: time.Unix(1633072800, 0), + lastTimeStamp: time.Unix(1633072700, 0), + expected: "00:01:40.000000", + }, + { + name: "Option -tttt", + cmd: cmd{Opts: flags{TTTT: true}}, + currentTimestamp: time.Unix(1633072800, 0), + lastTimeStamp: time.Unix(1633072700, 0), + expected: "00:01:40", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.cmd.parseTimeStamp(tt.currentTimestamp, tt.lastTimeStamp) + if result != tt.expected { + t.Errorf("parseTimeStamp() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestFormatPacketData(t *testing.T) { + data := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11} + expected := `0x0000: 0001 0203 0405 0607 0809 0a0b 0c0d 0e0f +0x0010: 1011 +` + + result := formatPacketData(data) + trimmedExpected := strings.TrimSpace(expected) + trimmedResult := strings.TrimSpace(result) + + if trimmedResult != trimmedExpected { + t.Errorf("Expected:\n%s\nGot:\n%s", trimmedExpected, trimmedResult) + } +}