aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/google/gopacket/examples/reassemblydump/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/google/gopacket/examples/reassemblydump/main.go')
-rw-r--r--vendor/github.com/google/gopacket/examples/reassemblydump/main.go650
1 files changed, 650 insertions, 0 deletions
diff --git a/vendor/github.com/google/gopacket/examples/reassemblydump/main.go b/vendor/github.com/google/gopacket/examples/reassemblydump/main.go
new file mode 100644
index 0000000..9fc3791
--- /dev/null
+++ b/vendor/github.com/google/gopacket/examples/reassemblydump/main.go
@@ -0,0 +1,650 @@
+// Copyright 2012 Google, Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file in the root of the source
+// tree.
+
+// The pcapdump binary implements a tcpdump-like command line tool with gopacket
+// using pcap as a backend data collection mechanism.
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "compress/gzip"
+ "encoding/binary"
+ "encoding/hex"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "path"
+ "runtime/pprof"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/google/gopacket"
+ "github.com/google/gopacket/examples/util"
+ "github.com/google/gopacket/ip4defrag"
+ "github.com/google/gopacket/layers" // pulls in all layers decoders
+ "github.com/google/gopacket/pcap"
+ "github.com/google/gopacket/reassembly"
+)
+
+var maxcount = flag.Int("c", -1, "Only grab this many packets, then exit")
+var decoder = flag.String("decoder", "", "Name of the decoder to use (default: guess from capture)")
+var statsevery = flag.Int("stats", 1000, "Output statistics every N packets")
+var lazy = flag.Bool("lazy", false, "If true, do lazy decoding")
+var nodefrag = flag.Bool("nodefrag", false, "If true, do not do IPv4 defrag")
+var checksum = flag.Bool("checksum", false, "Check TCP checksum")
+var nooptcheck = flag.Bool("nooptcheck", false, "Do not check TCP options (useful to ignore MSS on captures with TSO)")
+var ignorefsmerr = flag.Bool("ignorefsmerr", false, "Ignore TCP FSM errors")
+var allowmissinginit = flag.Bool("allowmissinginit", false, "Support streams without SYN/SYN+ACK/ACK sequence")
+var verbose = flag.Bool("verbose", false, "Be verbose")
+var debug = flag.Bool("debug", false, "Display debug information")
+var quiet = flag.Bool("quiet", false, "Be quiet regarding errors")
+
+// http
+var nohttp = flag.Bool("nohttp", false, "Disable HTTP parsing")
+var output = flag.String("output", "", "Path to create file for HTTP 200 OK responses")
+var writeincomplete = flag.Bool("writeincomplete", false, "Write incomplete response")
+
+var hexdump = flag.Bool("dump", false, "Dump HTTP request/response as hex")
+var hexdumppkt = flag.Bool("dumppkt", false, "Dump packet as hex")
+
+// capture
+var iface = flag.String("i", "eth0", "Interface to read packets from")
+var fname = flag.String("r", "", "Filename to read from, overrides -i")
+var snaplen = flag.Int("s", 65536, "Snap length (number of bytes max to read per packet")
+var tstype = flag.String("timestamp_type", "", "Type of timestamps to use")
+var promisc = flag.Bool("promisc", true, "Set promiscuous mode")
+
+var memprofile = flag.String("memprofile", "", "Write memory profile")
+
+var stats struct {
+ ipdefrag int
+ missedBytes int
+ pkt int
+ sz int
+ totalsz int
+ rejectFsm int
+ rejectOpt int
+ rejectConnFsm int
+ reassembled int
+ outOfOrderBytes int
+ outOfOrderPackets int
+ biggestChunkBytes int
+ biggestChunkPackets int
+ overlapBytes int
+ overlapPackets int
+}
+
+const closeTimeout time.Duration = time.Hour * 24 // Closing inactive: TODO: from CLI
+const timeout time.Duration = time.Minute * 5 // Pending bytes: TODO: from CLI
+
+/*
+ * HTTP part
+ */
+
+type httpReader struct {
+ ident string
+ isClient bool
+ bytes chan []byte
+ data []byte
+ hexdump bool
+ parent *tcpStream
+}
+
+func (h *httpReader) Read(p []byte) (int, error) {
+ ok := true
+ for ok && len(h.data) == 0 {
+ h.data, ok = <-h.bytes
+ }
+ if !ok || len(h.data) == 0 {
+ return 0, io.EOF
+ }
+
+ l := copy(p, h.data)
+ h.data = h.data[l:]
+ return l, nil
+}
+
+var outputLevel int
+var errorsMap map[string]uint
+var errors uint
+
+// Too bad for perf that a... is evaluated
+func Error(t string, s string, a ...interface{}) {
+ errors++
+ nb, _ := errorsMap[t]
+ errorsMap[t] = nb + 1
+ if outputLevel >= 0 {
+ fmt.Printf(s, a...)
+ }
+}
+func Info(s string, a ...interface{}) {
+ if outputLevel >= 1 {
+ fmt.Printf(s, a...)
+ }
+}
+func Debug(s string, a ...interface{}) {
+ if outputLevel >= 2 {
+ fmt.Printf(s, a...)
+ }
+}
+
+func (h *httpReader) run(wg *sync.WaitGroup) {
+ defer wg.Done()
+ b := bufio.NewReader(h)
+ for true {
+ if h.isClient {
+ req, err := http.ReadRequest(b)
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ break
+ } else if err != nil {
+ Error("HTTP-request", "HTTP/%s Request error: %s (%v,%+v)\n", h.ident, err, err, err)
+ continue
+ }
+ body, err := ioutil.ReadAll(req.Body)
+ s := len(body)
+ if err != nil {
+ Error("HTTP-request-body", "Got body err: %s\n", err)
+ } else if h.hexdump {
+ Info("Body(%d/0x%x)\n%s\n", len(body), len(body), hex.Dump(body))
+ }
+ req.Body.Close()
+ Info("HTTP/%s Request: %s %s (body:%d)\n", h.ident, req.Method, req.URL, s)
+ h.parent.urls = append(h.parent.urls, req.URL.String())
+ } else {
+ res, err := http.ReadResponse(b, nil)
+ var req string
+ if len(h.parent.urls) == 0 {
+ req = fmt.Sprintf("<no-request-seen>")
+ } else {
+ req, h.parent.urls = h.parent.urls[0], h.parent.urls[1:]
+ }
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ break
+ } else if err != nil {
+ Error("HTTP-response", "HTTP/%s Response error: %s (%v,%+v)\n", h.ident, err, err, err)
+ continue
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ s := len(body)
+ if err != nil {
+ Error("HTTP-response-body", "HTTP/%s: failed to get body(parsed len:%d): %s\n", h.ident, s, err)
+ }
+ if h.hexdump {
+ Info("Body(%d/0x%x)\n%s\n", len(body), len(body), hex.Dump(body))
+ }
+ res.Body.Close()
+ sym := ","
+ if res.ContentLength > 0 && res.ContentLength != int64(s) {
+ sym = "!="
+ }
+ contentType, ok := res.Header["Content-Type"]
+ if !ok {
+ contentType = []string{http.DetectContentType(body)}
+ }
+ encoding := res.Header["Content-Encoding"]
+ Info("HTTP/%s Response: %s URL:%s (%d%s%d%s) -> %s\n", h.ident, res.Status, req, res.ContentLength, sym, s, contentType, encoding)
+ if (err == nil || *writeincomplete) && *output != "" {
+ base := url.QueryEscape(path.Base(req))
+ if err != nil {
+ base = "incomplete-" + base
+ }
+ base = path.Join(*output, base)
+ if len(base) > 250 {
+ base = base[:250] + "..."
+ }
+ if base == *output {
+ base = path.Join(*output, "noname")
+ }
+ target := base
+ n := 0
+ for true {
+ _, err := os.Stat(target)
+ //if os.IsNotExist(err) != nil {
+ if err != nil {
+ break
+ }
+ target = fmt.Sprintf("%s-%d", base, n)
+ n++
+ }
+ f, err := os.Create(target)
+ if err != nil {
+ Error("HTTP-create", "Cannot create %s: %s\n", target, err)
+ continue
+ }
+ var r io.Reader
+ r = bytes.NewBuffer(body)
+ if len(encoding) > 0 && (encoding[0] == "gzip" || encoding[0] == "deflate") {
+ r, err = gzip.NewReader(r)
+ if err != nil {
+ Error("HTTP-gunzip", "Failed to gzip decode: %s", err)
+ }
+ }
+ if err == nil {
+ w, err := io.Copy(f, r)
+ if _, ok := r.(*gzip.Reader); ok {
+ r.(*gzip.Reader).Close()
+ }
+ f.Close()
+ if err != nil {
+ Error("HTTP-save", "%s: failed to save %s (l:%d): %s\n", h.ident, target, w, err)
+ } else {
+ Info("%s: Saved %s (l:%d)\n", h.ident, target, w)
+ }
+ }
+ }
+ }
+ }
+}
+
+/*
+ * The TCP factory: returns a new Stream
+ */
+type tcpStreamFactory struct {
+ wg sync.WaitGroup
+ doHTTP bool
+}
+
+func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream {
+ Debug("* NEW: %s %s\n", net, transport)
+ fsmOptions := reassembly.TCPSimpleFSMOptions{
+ SupportMissingEstablishment: *allowmissinginit,
+ }
+ stream := &tcpStream{
+ net: net,
+ transport: transport,
+ isDNS: tcp.SrcPort == 53 || tcp.DstPort == 53,
+ isHTTP: (tcp.SrcPort == 80 || tcp.DstPort == 80) && factory.doHTTP,
+ reversed: tcp.SrcPort == 80,
+ tcpstate: reassembly.NewTCPSimpleFSM(fsmOptions),
+ ident: fmt.Sprintf("%s:%s", net, transport),
+ optchecker: reassembly.NewTCPOptionCheck(),
+ }
+ if stream.isHTTP {
+ stream.client = httpReader{
+ bytes: make(chan []byte),
+ ident: fmt.Sprintf("%s %s", net, transport),
+ hexdump: *hexdump,
+ parent: stream,
+ isClient: true,
+ }
+ stream.server = httpReader{
+ bytes: make(chan []byte),
+ ident: fmt.Sprintf("%s %s", net.Reverse(), transport.Reverse()),
+ hexdump: *hexdump,
+ parent: stream,
+ }
+ factory.wg.Add(2)
+ go stream.client.run(&factory.wg)
+ go stream.server.run(&factory.wg)
+ }
+ return stream
+}
+
+func (factory *tcpStreamFactory) WaitGoRoutines() {
+ factory.wg.Wait()
+}
+
+/*
+ * The assembler context
+ */
+type Context struct {
+ CaptureInfo gopacket.CaptureInfo
+}
+
+func (c *Context) GetCaptureInfo() gopacket.CaptureInfo {
+ return c.CaptureInfo
+}
+
+/*
+ * TCP stream
+ */
+
+/* It's a connection (bidirectional) */
+type tcpStream struct {
+ tcpstate *reassembly.TCPSimpleFSM
+ fsmerr bool
+ optchecker reassembly.TCPOptionCheck
+ net, transport gopacket.Flow
+ isDNS bool
+ isHTTP bool
+ reversed bool
+ client httpReader
+ server httpReader
+ urls []string
+ ident string
+}
+
+func (t *tcpStream) Accept(tcp *layers.TCP, ci gopacket.CaptureInfo, dir reassembly.TCPFlowDirection, acked reassembly.Sequence, start *bool, ac reassembly.AssemblerContext) bool {
+ // FSM
+ if !t.tcpstate.CheckState(tcp, dir) {
+ Error("FSM", "%s: Packet rejected by FSM (state:%s)\n", t.ident, t.tcpstate.String())
+ stats.rejectFsm++
+ if !t.fsmerr {
+ t.fsmerr = true
+ stats.rejectConnFsm++
+ }
+ if !*ignorefsmerr {
+ return false
+ }
+ }
+ // Options
+ err := t.optchecker.Accept(tcp, ci, dir, acked, start)
+ if err != nil {
+ Error("OptionChecker", "%s: Packet rejected by OptionChecker: %s\n", t.ident, err)
+ stats.rejectOpt++
+ if !*nooptcheck {
+ return false
+ }
+ }
+ // Checksum
+ accept := true
+ if *checksum {
+ c, err := tcp.ComputeChecksum()
+ if err != nil {
+ Error("ChecksumCompute", "%s: Got error computing checksum: %s\n", t.ident, err)
+ accept = false
+ } else if c != 0x0 {
+ Error("Checksum", "%s: Invalid checksum: 0x%x\n", t.ident, c)
+ accept = false
+ }
+ }
+ if !accept {
+ stats.rejectOpt++
+ }
+ return accept
+}
+
+func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.AssemblerContext) {
+ dir, start, end, skip := sg.Info()
+ length, saved := sg.Lengths()
+ // update stats
+ sgStats := sg.Stats()
+ if skip > 0 {
+ stats.missedBytes += skip
+ }
+ stats.sz += length - saved
+ stats.pkt += sgStats.Packets
+ if sgStats.Chunks > 1 {
+ stats.reassembled++
+ }
+ stats.outOfOrderPackets += sgStats.QueuedPackets
+ stats.outOfOrderBytes += sgStats.QueuedBytes
+ if length > stats.biggestChunkBytes {
+ stats.biggestChunkBytes = length
+ }
+ if sgStats.Packets > stats.biggestChunkPackets {
+ stats.biggestChunkPackets = sgStats.Packets
+ }
+ if sgStats.OverlapBytes != 0 && sgStats.OverlapPackets == 0 {
+ fmt.Printf("bytes:%d, pkts:%d\n", sgStats.OverlapBytes, sgStats.OverlapPackets)
+ panic("Invalid overlap")
+ }
+ stats.overlapBytes += sgStats.OverlapBytes
+ stats.overlapPackets += sgStats.OverlapPackets
+
+ var ident string
+ if dir == reassembly.TCPDirClientToServer {
+ ident = fmt.Sprintf("%v %v(%s): ", t.net, t.transport, dir)
+ } else {
+ ident = fmt.Sprintf("%v %v(%s): ", t.net.Reverse(), t.transport.Reverse(), dir)
+ }
+ Debug("%s: SG reassembled packet with %d bytes (start:%v,end:%v,skip:%d,saved:%d,nb:%d,%d,overlap:%d,%d)\n", ident, length, start, end, skip, saved, sgStats.Packets, sgStats.Chunks, sgStats.OverlapBytes, sgStats.OverlapPackets)
+ if skip == -1 && *allowmissinginit {
+ // this is allowed
+ } else if skip != 0 {
+ // Missing bytes in stream: do not even try to parse it
+ return
+ }
+ data := sg.Fetch(length)
+ if t.isDNS {
+ dns := &layers.DNS{}
+ var decoded []gopacket.LayerType
+ if len(data) < 2 {
+ if len(data) > 0 {
+ sg.KeepFrom(0)
+ }
+ return
+ }
+ dnsSize := binary.BigEndian.Uint16(data[:2])
+ missing := int(dnsSize) - len(data[2:])
+ Debug("dnsSize: %d, missing: %d\n", dnsSize, missing)
+ if missing > 0 {
+ Info("Missing some bytes: %d\n", missing)
+ sg.KeepFrom(0)
+ return
+ }
+ p := gopacket.NewDecodingLayerParser(layers.LayerTypeDNS, dns)
+ err := p.DecodeLayers(data[2:], &decoded)
+ if err != nil {
+ Error("DNS-parser", "Failed to decode DNS: %v\n", err)
+ } else {
+ Debug("DNS: %s\n", gopacket.LayerDump(dns))
+ }
+ if len(data) > 2+int(dnsSize) {
+ sg.KeepFrom(2 + int(dnsSize))
+ }
+ } else if t.isHTTP {
+ if length > 0 {
+ if *hexdump {
+ Debug("Feeding http with:\n%s", hex.Dump(data))
+ }
+ if dir == reassembly.TCPDirClientToServer && !t.reversed {
+ t.client.bytes <- data
+ } else {
+ t.server.bytes <- data
+ }
+ }
+ }
+}
+
+func (t *tcpStream) ReassemblyComplete(ac reassembly.AssemblerContext) bool {
+ Debug("%s: Connection closed\n", t.ident)
+ if t.isHTTP {
+ close(t.client.bytes)
+ close(t.server.bytes)
+ }
+ // do not remove the connection to allow last ACK
+ return false
+}
+
+func main() {
+ defer util.Run()()
+ var handle *pcap.Handle
+ var err error
+ if *debug {
+ outputLevel = 2
+ } else if *verbose {
+ outputLevel = 1
+ } else if *quiet {
+ outputLevel = -1
+ }
+ errorsMap = make(map[string]uint)
+ if *fname != "" {
+ if handle, err = pcap.OpenOffline(*fname); err != nil {
+ log.Fatal("PCAP OpenOffline error:", err)
+ }
+ } else {
+ // This is a little complicated because we want to allow all possible options
+ // for creating the packet capture handle... instead of all this you can
+ // just call pcap.OpenLive if you want a simple handle.
+ inactive, err := pcap.NewInactiveHandle(*iface)
+ if err != nil {
+ log.Fatal("could not create: %v", err)
+ }
+ defer inactive.CleanUp()
+ if err = inactive.SetSnapLen(*snaplen); err != nil {
+ log.Fatal("could not set snap length: %v", err)
+ } else if err = inactive.SetPromisc(*promisc); err != nil {
+ log.Fatal("could not set promisc mode: %v", err)
+ } else if err = inactive.SetTimeout(time.Second); err != nil {
+ log.Fatal("could not set timeout: %v", err)
+ }
+ if *tstype != "" {
+ if t, err := pcap.TimestampSourceFromString(*tstype); err != nil {
+ log.Fatalf("Supported timestamp types: %v", inactive.SupportedTimestamps())
+ } else if err := inactive.SetTimestampSource(t); err != nil {
+ log.Fatalf("Supported timestamp types: %v", inactive.SupportedTimestamps())
+ }
+ }
+ if handle, err = inactive.Activate(); err != nil {
+ log.Fatal("PCAP Activate error:", err)
+ }
+ defer handle.Close()
+ }
+ if len(flag.Args()) > 0 {
+ bpffilter := strings.Join(flag.Args(), " ")
+ Info("Using BPF filter %q\n", bpffilter)
+ if err = handle.SetBPFFilter(bpffilter); err != nil {
+ log.Fatal("BPF filter error:", err)
+ }
+ }
+
+ var dec gopacket.Decoder
+ var ok bool
+ decoder_name := *decoder
+ if decoder_name == "" {
+ decoder_name = fmt.Sprintf("%s", handle.LinkType())
+ }
+ if dec, ok = gopacket.DecodersByLayerName[decoder_name]; !ok {
+ log.Fatalln("No decoder named", decoder_name)
+ }
+ source := gopacket.NewPacketSource(handle, dec)
+ source.Lazy = *lazy
+ source.NoCopy = true
+ Info("Starting to read packets\n")
+ count := 0
+ bytes := int64(0)
+ start := time.Now()
+ defragger := ip4defrag.NewIPv4Defragmenter()
+
+ streamFactory := &tcpStreamFactory{doHTTP: !*nohttp}
+ streamPool := reassembly.NewStreamPool(streamFactory)
+ assembler := reassembly.NewAssembler(streamPool)
+
+ signalChan := make(chan os.Signal, 1)
+ signal.Notify(signalChan, os.Interrupt)
+
+ for packet := range source.Packets() {
+ count++
+ Debug("PACKET #%d\n", count)
+ data := packet.Data()
+ bytes += int64(len(data))
+ if *hexdumppkt {
+ Debug("Packet content (%d/0x%x)\n%s\n", len(data), len(data), hex.Dump(data))
+ }
+
+ // defrag the IPv4 packet if required
+ if !*nodefrag {
+ ip4Layer := packet.Layer(layers.LayerTypeIPv4)
+ if ip4Layer == nil {
+ continue
+ }
+ ip4 := ip4Layer.(*layers.IPv4)
+ l := ip4.Length
+ newip4, err := defragger.DefragIPv4(ip4)
+ if err != nil {
+ log.Fatalln("Error while de-fragmenting", err)
+ } else if newip4 == nil {
+ Debug("Fragment...\n")
+ continue // packet fragment, we don't have whole packet yet.
+ }
+ if newip4.Length != l {
+ stats.ipdefrag++
+ Debug("Decoding re-assembled packet: %s\n", newip4.NextLayerType())
+ pb, ok := packet.(gopacket.PacketBuilder)
+ if !ok {
+ panic("Not a PacketBuilder")
+ }
+ nextDecoder := newip4.NextLayerType()
+ nextDecoder.Decode(newip4.Payload, pb)
+ }
+ }
+
+ tcp := packet.Layer(layers.LayerTypeTCP)
+ if tcp != nil {
+ tcp := tcp.(*layers.TCP)
+ if *checksum {
+ err := tcp.SetNetworkLayerForChecksum(packet.NetworkLayer())
+ if err != nil {
+ log.Fatalf("Failed to set network layer for checksum: %s\n", err)
+ }
+ }
+ c := Context{
+ CaptureInfo: packet.Metadata().CaptureInfo,
+ }
+ stats.totalsz += len(tcp.Payload)
+ assembler.AssembleWithContext(packet.NetworkLayer().NetworkFlow(), tcp, &c)
+ }
+ if count%*statsevery == 0 {
+ ref := packet.Metadata().CaptureInfo.Timestamp
+ flushed, closed := assembler.FlushWithOptions(reassembly.FlushOptions{T: ref.Add(-timeout), TC: ref.Add(-closeTimeout)})
+ Debug("Forced flush: %d flushed, %d closed (%s)", flushed, closed, ref)
+ }
+
+ done := *maxcount > 0 && count >= *maxcount
+ if count%*statsevery == 0 || done {
+ fmt.Fprintf(os.Stderr, "Processed %v packets (%v bytes) in %v (errors: %v, type:%v)\n", count, bytes, time.Since(start), errors, len(errorsMap))
+ }
+ select {
+ case <-signalChan:
+ fmt.Fprintf(os.Stderr, "\nCaught SIGINT: aborting\n")
+ done = true
+ default:
+ // NOP: continue
+ }
+ if done {
+ break
+ }
+ }
+
+ closed := assembler.FlushAll()
+ Debug("Final flush: %d closed", closed)
+ if outputLevel >= 2 {
+ streamPool.Dump()
+ }
+
+ if *memprofile != "" {
+ f, err := os.Create(*memprofile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ pprof.WriteHeapProfile(f)
+ f.Close()
+ }
+
+ streamFactory.WaitGoRoutines()
+ Debug("%s\n", assembler.Dump())
+ if !*nodefrag {
+ fmt.Printf("IPdefrag:\t\t%d\n", stats.ipdefrag)
+ }
+ fmt.Printf("TCP stats:\n")
+ fmt.Printf(" missed bytes:\t\t%d\n", stats.missedBytes)
+ fmt.Printf(" total packets:\t\t%d\n", stats.pkt)
+ fmt.Printf(" rejected FSM:\t\t%d\n", stats.rejectFsm)
+ fmt.Printf(" rejected Options:\t%d\n", stats.rejectOpt)
+ fmt.Printf(" reassembled bytes:\t%d\n", stats.sz)
+ fmt.Printf(" total TCP bytes:\t%d\n", stats.totalsz)
+ fmt.Printf(" conn rejected FSM:\t%d\n", stats.rejectConnFsm)
+ fmt.Printf(" reassembled chunks:\t%d\n", stats.reassembled)
+ fmt.Printf(" out-of-order packets:\t%d\n", stats.outOfOrderPackets)
+ fmt.Printf(" out-of-order bytes:\t%d\n", stats.outOfOrderBytes)
+ fmt.Printf(" biggest-chunk packets:\t%d\n", stats.biggestChunkPackets)
+ fmt.Printf(" biggest-chunk bytes:\t%d\n", stats.biggestChunkBytes)
+ fmt.Printf(" overlap packets:\t%d\n", stats.overlapPackets)
+ fmt.Printf(" overlap bytes:\t\t%d\n", stats.overlapBytes)
+ fmt.Printf("Errors: %d\n", errors)
+ for e, _ := range errorsMap {
+ fmt.Printf(" %s:\t\t%d\n", e, errorsMap[e])
+ }
+}