Last active
October 9, 2021 11:09
-
-
Save mertyildiran/466af329532a23dc4caa908faee3243f to your computer and use it in GitHub Desktop.
HTTP dissector with TCPDUMP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 errorsMapMutex sync.Mutex | |
var errors uint | |
// Too bad for perf that a... is evaluated | |
func Error(t string, s string, a ...interface{}) { | |
errorsMapMutex.Lock() | |
errors++ | |
nb, _ := errorsMap[t] | |
errorsMap[t] = nb + 1 | |
errorsMapMutex.Unlock() | |
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.Lock() | |
h.parent.urls = append(h.parent.urls, req.URL.String()) | |
h.parent.Unlock() | |
} else { | |
res, err := http.ReadResponse(b, nil) | |
var req string | |
h.parent.Lock() | |
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:] | |
} | |
h.parent.Unlock() | |
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 | |
sync.Mutex | |
} | |
func (t *tcpStream) Accept(tcp *layers.TCP, ci gopacket.CaptureInfo, dir reassembly.TCPFlowDirection, nextSeq 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, nextSeq, 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.Fatalf("could not create: %v", err) | |
} | |
defer inactive.CleanUp() | |
if err = inactive.SetSnapLen(*snaplen); err != nil { | |
log.Fatalf("could not set snap length: %v", err) | |
} else if err = inactive.SetPromisc(*promisc); err != nil { | |
log.Fatalf("could not set promisc mode: %v", err) | |
} else if err = inactive.SetTimeout(time.Second); err != nil { | |
log.Fatalf("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 { | |
errorsMapMutex.Lock() | |
errorMapLen := len(errorsMap) | |
errorsMapMutex.Unlock() | |
fmt.Fprintf(os.Stderr, "Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v)\n", count, bytes, time.Since(start), errors, errorMapLen) | |
} | |
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]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment