1// Copyright 2012 Google, Inc. All rights reserved. 2// 3// Use of this source code is governed by a BSD-style license 4// that can be found in the LICENSE file in the root of the source 5// tree. 6 7// The pcapdump binary implements a tcpdump-like command line tool with gopacket 8// using pcap as a backend data collection mechanism. 9package main 10 11import ( 12 "bufio" 13 "bytes" 14 "compress/gzip" 15 "encoding/binary" 16 "encoding/hex" 17 "flag" 18 "fmt" 19 "io" 20 "io/ioutil" 21 "log" 22 "net/http" 23 "net/url" 24 "os" 25 "os/signal" 26 "path" 27 "runtime/pprof" 28 "strings" 29 "sync" 30 "time" 31 32 "github.com/google/gopacket" 33 "github.com/google/gopacket/examples/util" 34 "github.com/google/gopacket/ip4defrag" 35 "github.com/google/gopacket/layers" // pulls in all layers decoders 36 "github.com/google/gopacket/pcap" 37 "github.com/google/gopacket/reassembly" 38) 39 40var maxcount = flag.Int("c", -1, "Only grab this many packets, then exit") 41var decoder = flag.String("decoder", "", "Name of the decoder to use (default: guess from capture)") 42var statsevery = flag.Int("stats", 1000, "Output statistics every N packets") 43var lazy = flag.Bool("lazy", false, "If true, do lazy decoding") 44var nodefrag = flag.Bool("nodefrag", false, "If true, do not do IPv4 defrag") 45var checksum = flag.Bool("checksum", false, "Check TCP checksum") 46var nooptcheck = flag.Bool("nooptcheck", false, "Do not check TCP options (useful to ignore MSS on captures with TSO)") 47var ignorefsmerr = flag.Bool("ignorefsmerr", false, "Ignore TCP FSM errors") 48var allowmissinginit = flag.Bool("allowmissinginit", false, "Support streams without SYN/SYN+ACK/ACK sequence") 49var verbose = flag.Bool("verbose", false, "Be verbose") 50var debug = flag.Bool("debug", false, "Display debug information") 51var quiet = flag.Bool("quiet", false, "Be quiet regarding errors") 52 53// http 54var nohttp = flag.Bool("nohttp", false, "Disable HTTP parsing") 55var output = flag.String("output", "", "Path to create file for HTTP 200 OK responses") 56var writeincomplete = flag.Bool("writeincomplete", false, "Write incomplete response") 57 58var hexdump = flag.Bool("dump", false, "Dump HTTP request/response as hex") 59var hexdumppkt = flag.Bool("dumppkt", false, "Dump packet as hex") 60 61// capture 62var iface = flag.String("i", "eth0", "Interface to read packets from") 63var fname = flag.String("r", "", "Filename to read from, overrides -i") 64var snaplen = flag.Int("s", 65536, "Snap length (number of bytes max to read per packet") 65var tstype = flag.String("timestamp_type", "", "Type of timestamps to use") 66var promisc = flag.Bool("promisc", true, "Set promiscuous mode") 67 68var memprofile = flag.String("memprofile", "", "Write memory profile") 69 70var stats struct { 71 ipdefrag int 72 missedBytes int 73 pkt int 74 sz int 75 totalsz int 76 rejectFsm int 77 rejectOpt int 78 rejectConnFsm int 79 reassembled int 80 outOfOrderBytes int 81 outOfOrderPackets int 82 biggestChunkBytes int 83 biggestChunkPackets int 84 overlapBytes int 85 overlapPackets int 86} 87 88const closeTimeout time.Duration = time.Hour * 24 // Closing inactive: TODO: from CLI 89const timeout time.Duration = time.Minute * 5 // Pending bytes: TODO: from CLI 90 91/* 92 * HTTP part 93 */ 94 95type httpReader struct { 96 ident string 97 isClient bool 98 bytes chan []byte 99 data []byte 100 hexdump bool 101 parent *tcpStream 102} 103 104func (h *httpReader) Read(p []byte) (int, error) { 105 ok := true 106 for ok && len(h.data) == 0 { 107 h.data, ok = <-h.bytes 108 } 109 if !ok || len(h.data) == 0 { 110 return 0, io.EOF 111 } 112 113 l := copy(p, h.data) 114 h.data = h.data[l:] 115 return l, nil 116} 117 118var outputLevel int 119var errorsMap map[string]uint 120var errorsMapMutex sync.Mutex 121var errors uint 122 123// Too bad for perf that a... is evaluated 124func Error(t string, s string, a ...interface{}) { 125 errorsMapMutex.Lock() 126 errors++ 127 nb, _ := errorsMap[t] 128 errorsMap[t] = nb + 1 129 errorsMapMutex.Unlock() 130 if outputLevel >= 0 { 131 fmt.Printf(s, a...) 132 } 133} 134func Info(s string, a ...interface{}) { 135 if outputLevel >= 1 { 136 fmt.Printf(s, a...) 137 } 138} 139func Debug(s string, a ...interface{}) { 140 if outputLevel >= 2 { 141 fmt.Printf(s, a...) 142 } 143} 144 145func (h *httpReader) run(wg *sync.WaitGroup) { 146 defer wg.Done() 147 b := bufio.NewReader(h) 148 for true { 149 if h.isClient { 150 req, err := http.ReadRequest(b) 151 if err == io.EOF || err == io.ErrUnexpectedEOF { 152 break 153 } else if err != nil { 154 Error("HTTP-request", "HTTP/%s Request error: %s (%v,%+v)\n", h.ident, err, err, err) 155 continue 156 } 157 body, err := ioutil.ReadAll(req.Body) 158 s := len(body) 159 if err != nil { 160 Error("HTTP-request-body", "Got body err: %s\n", err) 161 } else if h.hexdump { 162 Info("Body(%d/0x%x)\n%s\n", len(body), len(body), hex.Dump(body)) 163 } 164 req.Body.Close() 165 Info("HTTP/%s Request: %s %s (body:%d)\n", h.ident, req.Method, req.URL, s) 166 h.parent.Lock() 167 h.parent.urls = append(h.parent.urls, req.URL.String()) 168 h.parent.Unlock() 169 } else { 170 res, err := http.ReadResponse(b, nil) 171 var req string 172 h.parent.Lock() 173 if len(h.parent.urls) == 0 { 174 req = fmt.Sprintf("<no-request-seen>") 175 } else { 176 req, h.parent.urls = h.parent.urls[0], h.parent.urls[1:] 177 } 178 h.parent.Unlock() 179 if err == io.EOF || err == io.ErrUnexpectedEOF { 180 break 181 } else if err != nil { 182 Error("HTTP-response", "HTTP/%s Response error: %s (%v,%+v)\n", h.ident, err, err, err) 183 continue 184 } 185 body, err := ioutil.ReadAll(res.Body) 186 s := len(body) 187 if err != nil { 188 Error("HTTP-response-body", "HTTP/%s: failed to get body(parsed len:%d): %s\n", h.ident, s, err) 189 } 190 if h.hexdump { 191 Info("Body(%d/0x%x)\n%s\n", len(body), len(body), hex.Dump(body)) 192 } 193 res.Body.Close() 194 sym := "," 195 if res.ContentLength > 0 && res.ContentLength != int64(s) { 196 sym = "!=" 197 } 198 contentType, ok := res.Header["Content-Type"] 199 if !ok { 200 contentType = []string{http.DetectContentType(body)} 201 } 202 encoding := res.Header["Content-Encoding"] 203 Info("HTTP/%s Response: %s URL:%s (%d%s%d%s) -> %s\n", h.ident, res.Status, req, res.ContentLength, sym, s, contentType, encoding) 204 if (err == nil || *writeincomplete) && *output != "" { 205 base := url.QueryEscape(path.Base(req)) 206 if err != nil { 207 base = "incomplete-" + base 208 } 209 base = path.Join(*output, base) 210 if len(base) > 250 { 211 base = base[:250] + "..." 212 } 213 if base == *output { 214 base = path.Join(*output, "noname") 215 } 216 target := base 217 n := 0 218 for true { 219 _, err := os.Stat(target) 220 //if os.IsNotExist(err) != nil { 221 if err != nil { 222 break 223 } 224 target = fmt.Sprintf("%s-%d", base, n) 225 n++ 226 } 227 f, err := os.Create(target) 228 if err != nil { 229 Error("HTTP-create", "Cannot create %s: %s\n", target, err) 230 continue 231 } 232 var r io.Reader 233 r = bytes.NewBuffer(body) 234 if len(encoding) > 0 && (encoding[0] == "gzip" || encoding[0] == "deflate") { 235 r, err = gzip.NewReader(r) 236 if err != nil { 237 Error("HTTP-gunzip", "Failed to gzip decode: %s", err) 238 } 239 } 240 if err == nil { 241 w, err := io.Copy(f, r) 242 if _, ok := r.(*gzip.Reader); ok { 243 r.(*gzip.Reader).Close() 244 } 245 f.Close() 246 if err != nil { 247 Error("HTTP-save", "%s: failed to save %s (l:%d): %s\n", h.ident, target, w, err) 248 } else { 249 Info("%s: Saved %s (l:%d)\n", h.ident, target, w) 250 } 251 } 252 } 253 } 254 } 255} 256 257/* 258 * The TCP factory: returns a new Stream 259 */ 260type tcpStreamFactory struct { 261 wg sync.WaitGroup 262 doHTTP bool 263} 264 265func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream { 266 Debug("* NEW: %s %s\n", net, transport) 267 fsmOptions := reassembly.TCPSimpleFSMOptions{ 268 SupportMissingEstablishment: *allowmissinginit, 269 } 270 stream := &tcpStream{ 271 net: net, 272 transport: transport, 273 isDNS: tcp.SrcPort == 53 || tcp.DstPort == 53, 274 isHTTP: (tcp.SrcPort == 80 || tcp.DstPort == 80) && factory.doHTTP, 275 reversed: tcp.SrcPort == 80, 276 tcpstate: reassembly.NewTCPSimpleFSM(fsmOptions), 277 ident: fmt.Sprintf("%s:%s", net, transport), 278 optchecker: reassembly.NewTCPOptionCheck(), 279 } 280 if stream.isHTTP { 281 stream.client = httpReader{ 282 bytes: make(chan []byte), 283 ident: fmt.Sprintf("%s %s", net, transport), 284 hexdump: *hexdump, 285 parent: stream, 286 isClient: true, 287 } 288 stream.server = httpReader{ 289 bytes: make(chan []byte), 290 ident: fmt.Sprintf("%s %s", net.Reverse(), transport.Reverse()), 291 hexdump: *hexdump, 292 parent: stream, 293 } 294 factory.wg.Add(2) 295 go stream.client.run(&factory.wg) 296 go stream.server.run(&factory.wg) 297 } 298 return stream 299} 300 301func (factory *tcpStreamFactory) WaitGoRoutines() { 302 factory.wg.Wait() 303} 304 305/* 306 * The assembler context 307 */ 308type Context struct { 309 CaptureInfo gopacket.CaptureInfo 310} 311 312func (c *Context) GetCaptureInfo() gopacket.CaptureInfo { 313 return c.CaptureInfo 314} 315 316/* 317 * TCP stream 318 */ 319 320/* It's a connection (bidirectional) */ 321type tcpStream struct { 322 tcpstate *reassembly.TCPSimpleFSM 323 fsmerr bool 324 optchecker reassembly.TCPOptionCheck 325 net, transport gopacket.Flow 326 isDNS bool 327 isHTTP bool 328 reversed bool 329 client httpReader 330 server httpReader 331 urls []string 332 ident string 333 sync.Mutex 334} 335 336func (t *tcpStream) Accept(tcp *layers.TCP, ci gopacket.CaptureInfo, dir reassembly.TCPFlowDirection, nextSeq reassembly.Sequence, start *bool, ac reassembly.AssemblerContext) bool { 337 // FSM 338 if !t.tcpstate.CheckState(tcp, dir) { 339 Error("FSM", "%s: Packet rejected by FSM (state:%s)\n", t.ident, t.tcpstate.String()) 340 stats.rejectFsm++ 341 if !t.fsmerr { 342 t.fsmerr = true 343 stats.rejectConnFsm++ 344 } 345 if !*ignorefsmerr { 346 return false 347 } 348 } 349 // Options 350 err := t.optchecker.Accept(tcp, ci, dir, nextSeq, start) 351 if err != nil { 352 Error("OptionChecker", "%s: Packet rejected by OptionChecker: %s\n", t.ident, err) 353 stats.rejectOpt++ 354 if !*nooptcheck { 355 return false 356 } 357 } 358 // Checksum 359 accept := true 360 if *checksum { 361 c, err := tcp.ComputeChecksum() 362 if err != nil { 363 Error("ChecksumCompute", "%s: Got error computing checksum: %s\n", t.ident, err) 364 accept = false 365 } else if c != 0x0 { 366 Error("Checksum", "%s: Invalid checksum: 0x%x\n", t.ident, c) 367 accept = false 368 } 369 } 370 if !accept { 371 stats.rejectOpt++ 372 } 373 return accept 374} 375 376func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.AssemblerContext) { 377 dir, start, end, skip := sg.Info() 378 length, saved := sg.Lengths() 379 // update stats 380 sgStats := sg.Stats() 381 if skip > 0 { 382 stats.missedBytes += skip 383 } 384 stats.sz += length - saved 385 stats.pkt += sgStats.Packets 386 if sgStats.Chunks > 1 { 387 stats.reassembled++ 388 } 389 stats.outOfOrderPackets += sgStats.QueuedPackets 390 stats.outOfOrderBytes += sgStats.QueuedBytes 391 if length > stats.biggestChunkBytes { 392 stats.biggestChunkBytes = length 393 } 394 if sgStats.Packets > stats.biggestChunkPackets { 395 stats.biggestChunkPackets = sgStats.Packets 396 } 397 if sgStats.OverlapBytes != 0 && sgStats.OverlapPackets == 0 { 398 fmt.Printf("bytes:%d, pkts:%d\n", sgStats.OverlapBytes, sgStats.OverlapPackets) 399 panic("Invalid overlap") 400 } 401 stats.overlapBytes += sgStats.OverlapBytes 402 stats.overlapPackets += sgStats.OverlapPackets 403 404 var ident string 405 if dir == reassembly.TCPDirClientToServer { 406 ident = fmt.Sprintf("%v %v(%s): ", t.net, t.transport, dir) 407 } else { 408 ident = fmt.Sprintf("%v %v(%s): ", t.net.Reverse(), t.transport.Reverse(), dir) 409 } 410 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) 411 if skip == -1 && *allowmissinginit { 412 // this is allowed 413 } else if skip != 0 { 414 // Missing bytes in stream: do not even try to parse it 415 return 416 } 417 data := sg.Fetch(length) 418 if t.isDNS { 419 dns := &layers.DNS{} 420 var decoded []gopacket.LayerType 421 if len(data) < 2 { 422 if len(data) > 0 { 423 sg.KeepFrom(0) 424 } 425 return 426 } 427 dnsSize := binary.BigEndian.Uint16(data[:2]) 428 missing := int(dnsSize) - len(data[2:]) 429 Debug("dnsSize: %d, missing: %d\n", dnsSize, missing) 430 if missing > 0 { 431 Info("Missing some bytes: %d\n", missing) 432 sg.KeepFrom(0) 433 return 434 } 435 p := gopacket.NewDecodingLayerParser(layers.LayerTypeDNS, dns) 436 err := p.DecodeLayers(data[2:], &decoded) 437 if err != nil { 438 Error("DNS-parser", "Failed to decode DNS: %v\n", err) 439 } else { 440 Debug("DNS: %s\n", gopacket.LayerDump(dns)) 441 } 442 if len(data) > 2+int(dnsSize) { 443 sg.KeepFrom(2 + int(dnsSize)) 444 } 445 } else if t.isHTTP { 446 if length > 0 { 447 if *hexdump { 448 Debug("Feeding http with:\n%s", hex.Dump(data)) 449 } 450 if dir == reassembly.TCPDirClientToServer && !t.reversed { 451 t.client.bytes <- data 452 } else { 453 t.server.bytes <- data 454 } 455 } 456 } 457} 458 459func (t *tcpStream) ReassemblyComplete(ac reassembly.AssemblerContext) bool { 460 Debug("%s: Connection closed\n", t.ident) 461 if t.isHTTP { 462 close(t.client.bytes) 463 close(t.server.bytes) 464 } 465 // do not remove the connection to allow last ACK 466 return false 467} 468 469func main() { 470 defer util.Run()() 471 var handle *pcap.Handle 472 var err error 473 if *debug { 474 outputLevel = 2 475 } else if *verbose { 476 outputLevel = 1 477 } else if *quiet { 478 outputLevel = -1 479 } 480 errorsMap = make(map[string]uint) 481 if *fname != "" { 482 if handle, err = pcap.OpenOffline(*fname); err != nil { 483 log.Fatal("PCAP OpenOffline error:", err) 484 } 485 } else { 486 // This is a little complicated because we want to allow all possible options 487 // for creating the packet capture handle... instead of all this you can 488 // just call pcap.OpenLive if you want a simple handle. 489 inactive, err := pcap.NewInactiveHandle(*iface) 490 if err != nil { 491 log.Fatalf("could not create: %v", err) 492 } 493 defer inactive.CleanUp() 494 if err = inactive.SetSnapLen(*snaplen); err != nil { 495 log.Fatalf("could not set snap length: %v", err) 496 } else if err = inactive.SetPromisc(*promisc); err != nil { 497 log.Fatalf("could not set promisc mode: %v", err) 498 } else if err = inactive.SetTimeout(time.Second); err != nil { 499 log.Fatalf("could not set timeout: %v", err) 500 } 501 if *tstype != "" { 502 if t, err := pcap.TimestampSourceFromString(*tstype); err != nil { 503 log.Fatalf("Supported timestamp types: %v", inactive.SupportedTimestamps()) 504 } else if err := inactive.SetTimestampSource(t); err != nil { 505 log.Fatalf("Supported timestamp types: %v", inactive.SupportedTimestamps()) 506 } 507 } 508 if handle, err = inactive.Activate(); err != nil { 509 log.Fatal("PCAP Activate error:", err) 510 } 511 defer handle.Close() 512 } 513 if len(flag.Args()) > 0 { 514 bpffilter := strings.Join(flag.Args(), " ") 515 Info("Using BPF filter %q\n", bpffilter) 516 if err = handle.SetBPFFilter(bpffilter); err != nil { 517 log.Fatal("BPF filter error:", err) 518 } 519 } 520 521 var dec gopacket.Decoder 522 var ok bool 523 decoder_name := *decoder 524 if decoder_name == "" { 525 decoder_name = fmt.Sprintf("%s", handle.LinkType()) 526 } 527 if dec, ok = gopacket.DecodersByLayerName[decoder_name]; !ok { 528 log.Fatalln("No decoder named", decoder_name) 529 } 530 source := gopacket.NewPacketSource(handle, dec) 531 source.Lazy = *lazy 532 source.NoCopy = true 533 Info("Starting to read packets\n") 534 count := 0 535 bytes := int64(0) 536 start := time.Now() 537 defragger := ip4defrag.NewIPv4Defragmenter() 538 539 streamFactory := &tcpStreamFactory{doHTTP: !*nohttp} 540 streamPool := reassembly.NewStreamPool(streamFactory) 541 assembler := reassembly.NewAssembler(streamPool) 542 543 signalChan := make(chan os.Signal, 1) 544 signal.Notify(signalChan, os.Interrupt) 545 546 for packet := range source.Packets() { 547 count++ 548 Debug("PACKET #%d\n", count) 549 data := packet.Data() 550 bytes += int64(len(data)) 551 if *hexdumppkt { 552 Debug("Packet content (%d/0x%x)\n%s\n", len(data), len(data), hex.Dump(data)) 553 } 554 555 // defrag the IPv4 packet if required 556 if !*nodefrag { 557 ip4Layer := packet.Layer(layers.LayerTypeIPv4) 558 if ip4Layer == nil { 559 continue 560 } 561 ip4 := ip4Layer.(*layers.IPv4) 562 l := ip4.Length 563 newip4, err := defragger.DefragIPv4(ip4) 564 if err != nil { 565 log.Fatalln("Error while de-fragmenting", err) 566 } else if newip4 == nil { 567 Debug("Fragment...\n") 568 continue // packet fragment, we don't have whole packet yet. 569 } 570 if newip4.Length != l { 571 stats.ipdefrag++ 572 Debug("Decoding re-assembled packet: %s\n", newip4.NextLayerType()) 573 pb, ok := packet.(gopacket.PacketBuilder) 574 if !ok { 575 panic("Not a PacketBuilder") 576 } 577 nextDecoder := newip4.NextLayerType() 578 nextDecoder.Decode(newip4.Payload, pb) 579 } 580 } 581 582 tcp := packet.Layer(layers.LayerTypeTCP) 583 if tcp != nil { 584 tcp := tcp.(*layers.TCP) 585 if *checksum { 586 err := tcp.SetNetworkLayerForChecksum(packet.NetworkLayer()) 587 if err != nil { 588 log.Fatalf("Failed to set network layer for checksum: %s\n", err) 589 } 590 } 591 c := Context{ 592 CaptureInfo: packet.Metadata().CaptureInfo, 593 } 594 stats.totalsz += len(tcp.Payload) 595 assembler.AssembleWithContext(packet.NetworkLayer().NetworkFlow(), tcp, &c) 596 } 597 if count%*statsevery == 0 { 598 ref := packet.Metadata().CaptureInfo.Timestamp 599 flushed, closed := assembler.FlushWithOptions(reassembly.FlushOptions{T: ref.Add(-timeout), TC: ref.Add(-closeTimeout)}) 600 Debug("Forced flush: %d flushed, %d closed (%s)", flushed, closed, ref) 601 } 602 603 done := *maxcount > 0 && count >= *maxcount 604 if count%*statsevery == 0 || done { 605 errorsMapMutex.Lock() 606 errorMapLen := len(errorsMap) 607 errorsMapMutex.Unlock() 608 fmt.Fprintf(os.Stderr, "Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v)\n", count, bytes, time.Since(start), errors, errorMapLen) 609 } 610 select { 611 case <-signalChan: 612 fmt.Fprintf(os.Stderr, "\nCaught SIGINT: aborting\n") 613 done = true 614 default: 615 // NOP: continue 616 } 617 if done { 618 break 619 } 620 } 621 622 closed := assembler.FlushAll() 623 Debug("Final flush: %d closed", closed) 624 if outputLevel >= 2 { 625 streamPool.Dump() 626 } 627 628 if *memprofile != "" { 629 f, err := os.Create(*memprofile) 630 if err != nil { 631 log.Fatal(err) 632 } 633 pprof.WriteHeapProfile(f) 634 f.Close() 635 } 636 637 streamFactory.WaitGoRoutines() 638 Debug("%s\n", assembler.Dump()) 639 if !*nodefrag { 640 fmt.Printf("IPdefrag:\t\t%d\n", stats.ipdefrag) 641 } 642 fmt.Printf("TCP stats:\n") 643 fmt.Printf(" missed bytes:\t\t%d\n", stats.missedBytes) 644 fmt.Printf(" total packets:\t\t%d\n", stats.pkt) 645 fmt.Printf(" rejected FSM:\t\t%d\n", stats.rejectFsm) 646 fmt.Printf(" rejected Options:\t%d\n", stats.rejectOpt) 647 fmt.Printf(" reassembled bytes:\t%d\n", stats.sz) 648 fmt.Printf(" total TCP bytes:\t%d\n", stats.totalsz) 649 fmt.Printf(" conn rejected FSM:\t%d\n", stats.rejectConnFsm) 650 fmt.Printf(" reassembled chunks:\t%d\n", stats.reassembled) 651 fmt.Printf(" out-of-order packets:\t%d\n", stats.outOfOrderPackets) 652 fmt.Printf(" out-of-order bytes:\t%d\n", stats.outOfOrderBytes) 653 fmt.Printf(" biggest-chunk packets:\t%d\n", stats.biggestChunkPackets) 654 fmt.Printf(" biggest-chunk bytes:\t%d\n", stats.biggestChunkBytes) 655 fmt.Printf(" overlap packets:\t%d\n", stats.overlapPackets) 656 fmt.Printf(" overlap bytes:\t\t%d\n", stats.overlapBytes) 657 fmt.Printf("Errors: %d\n", errors) 658 for e, _ := range errorsMap { 659 fmt.Printf(" %s:\t\t%d\n", e, errorsMap[e]) 660 } 661} 662