nim-libp2p/libp2p/debugutils.nim

269 lines
7.8 KiB
Nim

# Nim-LibP2P
# Copyright (c) 2023 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
## To enable dump of all incoming and outgoing unencrypted messages you need
## to compile project with ``-d:libp2p_dump`` compile-time option. When this
## option enabled ``nim-libp2p`` will create dumps of unencrypted messages for
## every peer libp2p communicates.
##
## Every file is created with name "<PeerId>.pbcap". One file represents
## all the communication with peer which identified by ``PeerId``.
##
## File can have multiple protobuf encoded messages of this format:
##
## Protobuf's message fields:
## 1. SeqID: optional uint64
## 2. Timestamp: required uint64
## 3. Type: optional uint64
## 4. Direction: required uint64
## 5. LocalAddress: optional bytes
## 6. RemoteAddress: optional bytes
## 7. Message: required bytes
import os
import nimcrypto/utils, stew/endians2
import protobuf/minprotobuf, stream/connection, protocols/secure/secure,
multiaddress, peerid, varint, muxers/mplex/coder
from times import getTime, toUnix, fromUnix, nanosecond, format, Time,
NanosecondRange, initTime
from strutils import toHex, repeat
export peerid, multiaddress
type
FlowDirection* = enum
Outgoing, Incoming
ProtoMessage* = object
timestamp*: uint64
direction*: FlowDirection
message*: seq[byte]
seqID*: Opt[uint64]
mtype*: Opt[uint64]
local*: Opt[MultiAddress]
remote*: Opt[MultiAddress]
const
libp2p_dump_dir* {.strdefine.} = "nim-libp2p"
## default directory where all the dumps will be stored, if the path
## relative it will be created in home directory. You can overload this path
## using ``-d:libp2p_dump_dir=<otherpath>``.
proc getTimestamp(): uint64 =
## This procedure is present because `stdlib.times` missing it.
let time = getTime()
uint64(time.toUnix() * 1_000_000_000 + time.nanosecond())
proc getTimedate(value: uint64): string =
## This procedure is workaround for `stdlib.times` to just convert
## nanoseconds' timestamp to DateTime object.
let time = initTime(int64(value div 1_000_000_000), value mod 1_000_000_000)
time.format("yyyy-MM-dd HH:mm:ss'.'fffzzz")
proc dumpMessage*(conn: SecureConn, direction: FlowDirection,
data: openArray[byte]) =
## Store unencrypted message ``data`` to dump file, all the metadata will be
## extracted from ``conn`` instance.
var pb = initProtoBuffer(options = {WithVarintLength})
pb.write(2, getTimestamp())
pb.write(4, uint64(direction))
conn.observedAddr.withValue(oaddr):
pb.write(6, oaddr)
pb.write(7, data)
pb.finish()
let dirName =
if isAbsolute(libp2p_dump_dir):
libp2p_dump_dir
else:
getHomeDir() / libp2p_dump_dir
let fileName = $(conn.peerId) & ".pbcap"
# This is debugging procedure so it should not generate any exceptions,
# and we going to return at every possible OS error.
if not(dirExists(dirName)):
try:
createDir(dirName)
except CatchableError:
return
let pathName = dirName / fileName
var handle: File
try:
if open(handle, pathName, fmAppend):
discard writeBuffer(handle, addr pb.buffer[pb.offset], pb.getLen())
finally:
close(handle)
proc decodeDumpMessage*(data: openArray[byte]): Opt[ProtoMessage] =
## Decode protobuf's message ProtoMessage from array of bytes ``data``.
var
pb = initProtoBuffer(data)
value: uint64
ma1, ma2: MultiAddress
pmsg: ProtoMessage
let
r2 = pb.getField(2, pmsg.timestamp)
r4 = pb.getField(4, value)
r7 = pb.getField(7, pmsg.message)
if not r2.get(false) or not r4.get(false) or not r7.get(false):
return Opt.none(ProtoMessage)
# `case` statement could not work here with an error "selector must be of an
# ordinal type, float or string"
pmsg.direction =
if value == uint64(Outgoing):
Outgoing
elif value == uint64(Incoming):
Incoming
else:
return Opt.none(ProtoMessage)
let r1 = pb.getField(1, value)
if r1.get(false):
pmsg.seqID = Opt.some(value)
let r3 = pb.getField(3, value)
if r3.get(false):
pmsg.mtype = Opt.some(value)
let
r5 = pb.getField(5, ma1)
r6 = pb.getField(6, ma2)
if r5.get(false):
pmsg.local = Opt.some(ma1)
if r6.get(false):
pmsg.remote = Opt.some(ma2)
Opt.some(pmsg)
iterator messages*(data: seq[byte]): Opt[ProtoMessage] =
## Iterate over sequence of bytes and decode all the ``ProtoMessage``
## messages we found.
var value: uint64
var size: int
var offset = 0
while offset < len(data):
value = 0
size = 0
let res = PB.getUVarint(data.toOpenArray(offset, len(data) - 1),
size, value)
if res.isOk():
if (value > 0'u64) and (value < uint64(len(data) - offset)):
offset += size
yield decodeDumpMessage(data.toOpenArray(offset,
offset + int(value) - 1))
# value is previously checked to be less then len(data) which is `int`.
offset += int(value)
else:
break
else:
break
proc dumpHex*(pbytes: openArray[byte], groupBy = 1, ascii = true): string =
## Get hexadecimal dump of memory for array ``pbytes``.
var res = ""
var offset = 0
var ascii = ""
while offset < len(pbytes):
if (offset mod 16) == 0:
res = res & toHex(uint64(offset)) & ": "
for k in 0 ..< groupBy:
let ch = pbytes[offset + k]
ascii.add(if ord(ch) > 31 and ord(ch) < 127: char(ch) else: '.')
let item =
case groupBy:
of 1:
toHex(pbytes[offset])
of 2:
toHex(uint16.fromBytes(pbytes.toOpenArray(offset, len(pbytes) - 1)))
of 4:
toHex(uint32.fromBytes(pbytes.toOpenArray(offset, len(pbytes) - 1)))
of 8:
toHex(uint64.fromBytes(pbytes.toOpenArray(offset, len(pbytes) - 1)))
else:
""
res.add(item)
res.add(" ")
offset = offset + groupBy
if (offset mod 16) == 0:
res.add(" ")
res.add(ascii)
ascii.setLen(0)
res.add("\p")
if (offset mod 16) != 0:
let spacesCount = ((16 - (offset mod 16)) div groupBy) *
(groupBy * 2 + 1) + 1
res = res & repeat(' ', spacesCount)
res = res & ascii
res.add("\p")
res
proc mplexMessage*(data: seq[byte]): string =
var value = 0'u64
var size = 0
let res = PB.getUVarint(data, size, value)
if res.isOk():
if size < len(data) and data[size] == 0x00'u8:
let header = cast[MessageType](value and 0x07'u64)
let ident = (value shr 3)
"mplex: [" & $header & ", ident = " & $ident & "] "
else:
""
else:
""
proc toString*(msg: ProtoMessage, dump = true): string =
## Convert message ``msg`` to its string representation.
## If ``dump`` is ``true`` (default) full hexadecimal dump with ascii will be
## used, otherwise just a simple hexadecimal string will be printed.
var res = getTimedate(msg.timestamp)
let direction =
case msg.direction
of Incoming:
" << "
of Outgoing:
" >> "
let address =
block:
let local = block:
msg.local.withValue(loc): "[" & $loc & "]"
else: "[LOCAL]"
let remote = block:
msg.remote.withValue(rem): "[" & $rem & "]"
else: "[REMOTE]"
local & direction & remote
let seqid = block:
msg.seqID.wihValue(seqid): "seqID = " & $seqid & " "
else: ""
let mtype = block:
msg.mtype.withValue(typ): "type = " & $typ & " "
else: ""
res.add(" ")
res.add(address)
res.add(" ")
res.add(mtype)
res.add(seqid)
res.add(mplexMessage(msg.message))
res.add(" ")
res.add("\p")
if dump:
res.add(dumpHex(msg.message))
else:
res.add(utils.toHex(msg.message))
res.add("\p")
res