mirror of
https://github.com/moby/moby.git
synced 2026-01-11 10:41:43 +00:00
contrib: add Wireshark plugins for NetworkDB
Contribute a Wireshark plugin for decrypting and dissecting hashicorp/memberlist messages. And contribue a plugin for dissecting the NetworkDB messages transported as memberlist User messages. Add a feature to NetworkDB to log the encryption keys to a file for the Wireshark memberlist plugin to consume, configured using an environment variable. Signed-off-by: Cory Snider <csnider@mirantis.com>
This commit is contained in:
111
contrib/wireshark/README.md
Normal file
111
contrib/wireshark/README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
Wireshark Dissectors for NetworkDB
|
||||
==================================
|
||||
|
||||
`memberlist.lua` is a [Wireshark][] plugin
|
||||
which registers a `memberlist` protocol
|
||||
that can dissect the [memberlist][] TCP and UDP protocols.
|
||||
The `memberlist` protocol can be configured to dissect user data
|
||||
as the protocol named in the `memberlist.userdata_dissector` preference.
|
||||
|
||||
`moby-networkdb.lua` is a Wireshark plugin which registers
|
||||
a protocol named `networkdbgossip`
|
||||
that dissects NetworkDB gossip messages.
|
||||
As node-to-node communications for NetworkDB
|
||||
are transported as memberlist user messages,
|
||||
the memberlist protocol dissector must be configured
|
||||
to invoke the networkdbgossip protocol as a sub-dissector.
|
||||
(Read: set the preference `memberlist.userdata_dissector:networkdbgossip`)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
### Install Wireshark 4.5 or newer.
|
||||
Wireshark 4.4 has an incomplete msgpack protocol dissector
|
||||
that is unable to properly decode memberlist messages.
|
||||
As of 2025-06-30 Wireshark 4.5 has yet to be released.
|
||||
A [nightly build][ws.dl] may be required.
|
||||
|
||||
### Install the plugins
|
||||
Configure Wireshark/Tshark to load `memberlist.lua` and `moby-networkdb.lua`.
|
||||
Refer to [the Wireshark Lua manual][ws.lua.intro] for further instruction.
|
||||
|
||||
### Configure the ProtoBuf protocol
|
||||
|
||||
NetworkDB messages are serialized to protobuf,
|
||||
which is not self-describing.
|
||||
The ProtoBuf Wireshark protocol needs to be supplied with
|
||||
the protobuf IDL definitions of the messages
|
||||
in order to dissect them.
|
||||
|
||||
1. Clone [moby/moby][] for the NetworkDB IDL definitions.
|
||||
2. Clone [protocolbuffers/protobuf][] for the protobuf "standard library" IDL.
|
||||
3. Configure the ProtoBuf protocol (Preferences -> Protocols -> ProtoBuf)
|
||||
as follows:
|
||||
- ✅ Load .proto files on startup. (`protobuf.reload_protos`)
|
||||
- ✅ Dissect Protobuf fields as Wireshark fields. (`protobuf.pbf_as_hf`)
|
||||
- Add entries to the Protobuf Search Paths table (`uat:protobuf.search_paths`):
|
||||
- path/to/protocolbuffers/protobuf/src
|
||||
- path/to/moby/moby/vendor
|
||||
- path/to/moby/moby
|
||||
- path/to/moby/moby/libnetwork/networkdb (✅ Load all files)
|
||||
- path/to/moby/moby/libnetwork/drivers/overlay (✅ Load all files)
|
||||
|
||||
Note that it is not sufficient to just grab the .proto files from the repos.
|
||||
The directory structure is necessary for the definitions to load properly.
|
||||
|
||||
### Configure the Memberlist protocol
|
||||
|
||||
Configure memberlist to dissect user data messages as NetworkDB gossip.
|
||||
- In Preferences -> Protocols -> MEMBERLIST,
|
||||
set the User Data Dissector to `networkdbgossip`.
|
||||
(`memberlist.userdata_dissector`)
|
||||
- Optional: set Memberlist TCP+UDP port(s) (`memberlist.ports`) as needed.
|
||||
E.g. a value such as `7946,10000-10999` would be useful
|
||||
for analyzing packet captures from NetworkDB unit tests.
|
||||
|
||||
Usage Notes
|
||||
-----------
|
||||
|
||||
### Encryption
|
||||
|
||||
The memberlist protocol dissector supports decryption
|
||||
of encrypted memberlist messages
|
||||
when provided with a file containing the encryption keys used.
|
||||
In Preferences -> Protocols -> MEMBERLIST,
|
||||
set the Encryption Key Logfile Path
|
||||
(`memberlist.keylog`)
|
||||
to a file containing the encryption keys.
|
||||
|
||||
The logfile should list the encryption keys
|
||||
as hexadecimal strings, delimited by newlines.
|
||||
|
||||
dockerd may be configured to write the NetworkDB encryption keys to a logfile
|
||||
by setting the environment variable `NETWORKDBKEYLOGFILE`
|
||||
to the path where the file should reside.
|
||||
|
||||
### Known Issues
|
||||
|
||||
The NetworkDB protocol may fail to load with an error when Wireshark is first started:
|
||||
|
||||
moby-networkdb.lua:4: bad argument #1 to 'new'
|
||||
(Field_new: a field with this name must exist)
|
||||
|
||||
This is due to [a known issue in Wireshark.](https://gitlab.com/wireshark/wireshark/-/issues/20161)
|
||||
|
||||
Workaround: reload Lua plugins after Wireshark has been initialized.
|
||||
|
||||
- Menu: Analyze -> Reload Lua Plugins, or
|
||||
- Keyboard shortcut: Ctrl-Shift-L (Windows/Linux), ⇧⌘L (macOS)
|
||||
|
||||
### Limitations
|
||||
|
||||
- Only memberlist encryption version 1 (AES-GCM 128, no padding) is supported,
|
||||
not version 0 (AES-GCM 128 using PKCS#7 padding).
|
||||
- Labelled messages cannot currently be decrypted.
|
||||
|
||||
[memberlist]: https://github.com/hashicorp/memberlist
|
||||
[Wireshark]: https://wireshark.org
|
||||
[ws.dl]: https://www.wireshark.org/download/automated/
|
||||
[ws.lua.intro]: https://www.wireshark.org/docs/wsdg_html_chunked/wsluarm.html#wsluarm_intro
|
||||
[moby/moby]: https://github.com/moby/moby
|
||||
[protocolbuffers/protobuf]: https://github.com/protocolbuffers/protobuf
|
||||
587
contrib/wireshark/memberlist.lua
Normal file
587
contrib/wireshark/memberlist.lua
Normal file
@@ -0,0 +1,587 @@
|
||||
local msgpack, lzw, crc32 -- Forward declarations for library code inlined at the end of this chunk.
|
||||
|
||||
local memberlist_protocol = Proto("Memberlist", "Memberlist Protocol")
|
||||
|
||||
local message_type_enum = {
|
||||
[0] = "Ping",
|
||||
[1] = "IndirectPing",
|
||||
[2] = "ACKResp",
|
||||
[3] = "Suspect",
|
||||
[4] = "Alive",
|
||||
[5] = "Dead",
|
||||
[6] = "PushPull",
|
||||
[7] = "Compound",
|
||||
[8] = "User",
|
||||
[9] = "Compress",
|
||||
[10] = "Encrypt",
|
||||
[11] = "NACKResp",
|
||||
[12] = "HasCRC",
|
||||
[13] = "Err",
|
||||
[244] = "HasLabel",
|
||||
}
|
||||
local message_type = ProtoField.uint8("memberlist.message_type", "Message Type", base.DEC, message_type_enum)
|
||||
|
||||
local crc = ProtoField.uint32("memberlist.crc", "CRC", base.HEX)
|
||||
|
||||
local label_size = ProtoField.uint8("memberlist.label_size", "Label Size", base.DEC)
|
||||
local label = ProtoField.string("memberlist.label", "Label")
|
||||
|
||||
local encryption_version = ProtoField.uint8("memberlist.encryption.version", "Encryption Version", base.DEC)
|
||||
local encrypted_nonce = ProtoField.bytes("memberlist.encryption.nonce", "AES-GCM Nonce", base.NONE)
|
||||
local encrypted_ciphertext = ProtoField.bytes("memberlist.encryption.ciphertext", "Ciphertext", base.NONE)
|
||||
local encrypted_tag = ProtoField.bytes("memberlist.encryption.tag", "AEAD tag", base.NONE)
|
||||
local ciphertext_length = ProtoField.uint32("memberlist.encryption.length", "Ciphertext length", base.DEC)
|
||||
|
||||
local decompressed_data = ProtoField.bytes("memberlist.decompressed_data", "Decompressed Data")
|
||||
|
||||
local compound_parts = ProtoField.uint8("memberlist.compound.parts", "Compound Parts", base.DEC)
|
||||
local compound_part_length = ProtoField.uint16("memberlist.compound.part_length", "Compound Part Length", base.DEC)
|
||||
local userdata = ProtoField.bytes("memberlist.userdata", "User Data", base.NONE)
|
||||
|
||||
memberlist_protocol.fields = {
|
||||
message_type,
|
||||
crc,
|
||||
label_size,
|
||||
label,
|
||||
encryption_version,
|
||||
encrypted_nonce,
|
||||
encrypted_ciphertext,
|
||||
encrypted_tag,
|
||||
ciphertext_length,
|
||||
decompressed_data,
|
||||
compound_parts,
|
||||
compound_part_length,
|
||||
userdata,
|
||||
}
|
||||
|
||||
local default_settings = { ports = 7946 }
|
||||
|
||||
memberlist_protocol.prefs.userdata_dissector = Pref.string("User Data Dissector", "", "Dissector to apply to User message bodies")
|
||||
memberlist_protocol.prefs.ports = Pref.range("Memberlist TCP+UDP Port(s)", default_settings.ports, "", 65535)
|
||||
memberlist_protocol.prefs.keylog = Pref.string("Encryption Key Logfile Path", "", [[
|
||||
Relative or absolute path to a file containing the symmetric keys for decrypting ciphered memberlist messages.
|
||||
The file should contain the keys, encoded as hex, one per line.]])
|
||||
|
||||
local msgpack_string_field = Field.new("msgpack.string")
|
||||
|
||||
local function dissect_userdata(buffer, pinfo, tree)
|
||||
local dissector = memberlist_protocol.prefs.userdata_dissector
|
||||
if dissector ~= "" then
|
||||
local d = Dissector.get(dissector)
|
||||
if d == nil then
|
||||
tree:add(userdata, buffer())
|
||||
return
|
||||
end
|
||||
local success, err = pcall(Dissector.call, d, buffer, pinfo, tree)
|
||||
if success then return
|
||||
else
|
||||
tree:add_expert_info(PI_DISSECTOR_BUG, PI_ERROR, "Dissector " .. dissector .. " failed: " .. tostring(err))
|
||||
tree:add(userdata, buffer())
|
||||
end
|
||||
else
|
||||
tree:add(userdata, buffer())
|
||||
end
|
||||
end
|
||||
|
||||
local function try_decrypt(buffer, tree, aead)
|
||||
local path = memberlist_protocol.prefs.keylog
|
||||
if path == "" then return buffer end
|
||||
|
||||
local cryptoversion = buffer(0, 1)
|
||||
if cryptoversion:uint() ~= 1 then return buffer end
|
||||
local nonce = buffer(1, 12)
|
||||
buffer = buffer(13):tvb()
|
||||
local tagSize = 16
|
||||
local ciphertext = buffer(0, buffer:len()-tagSize)
|
||||
local tag = buffer(buffer:len()-tagSize)
|
||||
|
||||
local cipher = GcryptCipher.open(GCRY_CIPHER_AES, GCRY_CIPHER_MODE_GCM, 0)
|
||||
|
||||
-- Don't bother caching the keys in-process.
|
||||
-- The filesystem cache is performant enough,
|
||||
-- and the OS is better equipped to invalidate the cache.
|
||||
for hexkey in io.lines(path) do
|
||||
cipher:ctl(GCRYCTL_RESET, ByteArray.new())
|
||||
cipher:setkey(ByteArray.new(hexkey))
|
||||
cipher:setiv(nonce:bytes())
|
||||
if aead then cipher:authenticate(aead) end
|
||||
local decrypted = cipher:decrypt(NULL, ciphertext:bytes())
|
||||
local result, err = cipher:checktag(tag:bytes())
|
||||
if result == 0 then
|
||||
local subtree = tree:add(memberlist_protocol, buffer(), "Encrypted Memberlist")
|
||||
subtree:add(encryption_version, cryptoversion)
|
||||
subtree:add(encrypted_nonce, nonce)
|
||||
subtree:add(encrypted_ciphertext, ciphertext)
|
||||
subtree:add(encrypted_tag, tag)
|
||||
return decrypted:tvb("Decrypted")
|
||||
end
|
||||
end
|
||||
return buffer
|
||||
end
|
||||
|
||||
function memberlist_protocol.dissector(buffer, pinfo, tree)
|
||||
local length = buffer:len()
|
||||
if length == 0 then return end
|
||||
|
||||
pinfo.cols.protocol = memberlist_protocol.name
|
||||
|
||||
if pinfo.port_type ~= 2 then -- UDP
|
||||
buffer = try_decrypt(buffer, tree)
|
||||
end
|
||||
|
||||
local opcode = buffer(0, 1):uint()
|
||||
local subtree = tree:add(memberlist_protocol, buffer(), "Memberlist Protocol, Type: " .. (message_type_enum[opcode] or tostring(opcode)))
|
||||
|
||||
local msgtype_tree = subtree:add(message_type, buffer(0, 1))
|
||||
local buffer = buffer(1):tvb()
|
||||
|
||||
if opcode == 244 and buffer:len() > 0 then -- HasLabel
|
||||
local label_length = buffer(0, 1):uint()
|
||||
subtree:add(label_size, buffer(0, 1))
|
||||
subtree:add(label, buffer(1, label_length))
|
||||
memberlist_protocol.dissector(buffer(1 + label_length):tvb(), pinfo, subtree)
|
||||
return
|
||||
elseif opcode == 12 and buffer:len() > 4 then -- HasCRC
|
||||
subtree:add(crc, buffer(0, 4))
|
||||
local expected = buffer(0, 4):uint()
|
||||
buffer = buffer(4):tvb()
|
||||
local actual = crc32(buffer():raw())
|
||||
if expected ~= actual then
|
||||
subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "Actual CRC32 is " .. tostring(actual))
|
||||
end
|
||||
memberlist_protocol.dissector(buffer, pinfo, subtree)
|
||||
return
|
||||
elseif opcode == 6 then -- PushPull
|
||||
local msglen = Dissector.get("msgpack"):call(buffer, pinfo, subtree) -- pushPullHeader
|
||||
local header = msgpack.decode(buffer(0, msglen):raw())
|
||||
buffer = buffer(msglen):tvb()
|
||||
for _ = 1, header.Nodes do
|
||||
buffer = buffer(Dissector.get("msgpack"):call(buffer, pinfo, subtree)):tvb()
|
||||
end
|
||||
if header.UserStateLen > 0 then
|
||||
dissect_userdata(buffer(0, header.UserStateLen):tvb(), pinfo, tree)
|
||||
buffer = buffer(header.UserStateLen):tvb()
|
||||
end
|
||||
elseif opcode == 7 and buffer:len() >= 1 then -- Compound
|
||||
local nparts = buffer(0, 1)
|
||||
msgtype_tree:add(compound_parts, nparts)
|
||||
nparts = nparts:uint()
|
||||
msgtype_tree:append_text(" (" .. nparts .. " parts)")
|
||||
buffer = buffer(1):tvb()
|
||||
local partlengths = {}
|
||||
for i = 0, nparts-1 do
|
||||
local partlen = buffer(0, 2)
|
||||
msgtype_tree:add(compound_part_length, partlen)
|
||||
table.insert(partlengths, partlen:uint())
|
||||
buffer = buffer(2):tvb()
|
||||
end
|
||||
|
||||
for _, part_length in ipairs(partlengths) do
|
||||
if buffer:len() < part_length then
|
||||
subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "Truncated part in Compound message")
|
||||
return
|
||||
end
|
||||
local part = buffer(0, part_length):tvb()
|
||||
memberlist_protocol.dissector(part, pinfo, subtree)
|
||||
buffer = buffer(part_length):tvb()
|
||||
end
|
||||
return
|
||||
elseif opcode == 8 then -- User
|
||||
if pinfo.port_type == 2 then -- TCP
|
||||
local headerlen = Dissector.get("msgpack"):call(buffer, pinfo, subtree)
|
||||
local header = msgpack.decode(buffer(0, headerlen):raw())
|
||||
buffer = buffer(headerlen):tvb()
|
||||
if header.UserMsgLen ~= nil then
|
||||
dissect_userdata(buffer(0, header.UserMsgLen):tvb(), pinfo, tree)
|
||||
else
|
||||
subtree:add(userdata, buffer())
|
||||
end
|
||||
else
|
||||
dissect_userdata(buffer, pinfo, tree)
|
||||
end
|
||||
return
|
||||
elseif opcode == 10 then -- Encrypt
|
||||
local ciphertextlen = buffer(0, 4)
|
||||
buffer = buffer(4):tvb()
|
||||
msgtype_tree:add(ciphertext_length, ciphertextlen)
|
||||
local aead = ByteArray.new("0a") .. ciphertextlen:bytes()
|
||||
memberlist_protocol.dissector(try_decrypt(buffer(0, ciphertextlen:uint()), msgtype_tree, aead), pinfo, tree)
|
||||
return
|
||||
end
|
||||
|
||||
if buffer:len() > 0 then
|
||||
Dissector.get("msgpack"):call(buffer, pinfo, subtree)
|
||||
if opcode == 9 then -- Compress
|
||||
local fields = { msgpack_string_field() }
|
||||
for k,v in ipairs(fields) do
|
||||
if v.value == "Buf" then
|
||||
local compressed_data = fields[k+3].range
|
||||
local rawdata = compressed_data:raw()
|
||||
local decompressed, err = lzw.decompress(compressed_data:raw())
|
||||
if err ~= nil then
|
||||
subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "Decompression failed: " .. err)
|
||||
return
|
||||
end
|
||||
local dtree = subtree:add(decompressed_data, compressed_data, decompressed)
|
||||
memberlist_protocol.dissector(ByteArray.new(decompressed, true):tvb("Decompressed"), pinfo, tree)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local udp_port = DissectorTable.get("udp.port")
|
||||
local tcp_port = DissectorTable.get("tcp.port")
|
||||
function memberlist_protocol.prefs_changed()
|
||||
if default_settings.ports ~= memberlist_protocol.prefs.ports then
|
||||
udp_port:remove(default_settings.ports, memberlist_protocol)
|
||||
tcp_port:remove(default_settings.ports, memberlist_protocol)
|
||||
default_settings.ports = memberlist_protocol.prefs.ports
|
||||
udp_port:add(default_settings.ports, memberlist_protocol)
|
||||
tcp_port:add(default_settings.ports, memberlist_protocol)
|
||||
end
|
||||
end
|
||||
|
||||
udp_port:add(default_settings.ports, memberlist_protocol)
|
||||
tcp_port:add(default_settings.ports, memberlist_protocol)
|
||||
|
||||
-------------------------
|
||||
-- Library definitions --
|
||||
-------------------------
|
||||
|
||||
crc32 = (function()
|
||||
local crc32_lut = {
|
||||
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
|
||||
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
|
||||
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
|
||||
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c,
|
||||
}
|
||||
|
||||
local function crc32(s, crc)
|
||||
if crc == nil then crc = 0 end
|
||||
crc = crc ~ 0xffffffff
|
||||
for i = 1,#s do
|
||||
crc = (crc >> 4) ~ crc32_lut[((crc ~ string.byte(s, i) ) & 0xf) + 1]
|
||||
crc = (crc >> 4) ~ crc32_lut[((crc ~ (string.byte(s, i) >> 4)) & 0xf) + 1]
|
||||
end
|
||||
return crc ~ 0xffffffff
|
||||
end
|
||||
return crc32
|
||||
end)()
|
||||
|
||||
lzw = (function()
|
||||
--[[
|
||||
Port of "compress/lzw".Reader from the Go standard library.
|
||||
|
||||
Copyright 2011 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
]]--
|
||||
local maxWidth = 12
|
||||
local decoderInvalidCode = 0xffff
|
||||
local flushBuffer = 1 << maxWidth
|
||||
|
||||
local function decompress(compressed)
|
||||
local bits, nBits, litWidth = 0, 0, 8
|
||||
local width = litWidth + 1
|
||||
local clear = 1 << litWidth
|
||||
local eof, hi = clear+1, clear+1
|
||||
local overflow = 1 << width
|
||||
local last = decoderInvalidCode
|
||||
local suffix, prefix = {}, {}
|
||||
local i = 1
|
||||
local output = {}
|
||||
local code
|
||||
|
||||
while i <= string.len(compressed) do
|
||||
::continue::
|
||||
while nBits < width do
|
||||
local x = string.byte(compressed, i)
|
||||
i = i+1
|
||||
bits = bits | (x << nBits)
|
||||
nBits = nBits+8
|
||||
end
|
||||
code = bits & ((1 << width) - 1)
|
||||
bits = bits >> width
|
||||
nBits = nBits - width
|
||||
|
||||
if code < clear then -- literal
|
||||
table.insert(output, string.char(code))
|
||||
if last ~= decoderInvalidCode then
|
||||
suffix[hi] = string.char(code)
|
||||
prefix[hi] = last
|
||||
end
|
||||
elseif code == clear then -- clear code
|
||||
width = litWidth+1
|
||||
hi = eof
|
||||
overflow = 1 << width
|
||||
last = decoderInvalidCode
|
||||
goto continue
|
||||
elseif code == eof then -- end of file
|
||||
return table.concat(output)
|
||||
elseif code <= hi then -- code in dictionary
|
||||
local c, tmp = code, {}
|
||||
if code == hi and last ~= decoderInvalidCode then
|
||||
c = last
|
||||
while c >= clear do
|
||||
c = prefix[c]
|
||||
end
|
||||
table.insert(tmp, string.char(c))
|
||||
c = last
|
||||
end
|
||||
while c >= clear do
|
||||
table.insert(tmp, suffix[c])
|
||||
c = prefix[c]
|
||||
end
|
||||
table.insert(tmp, string.char(c))
|
||||
for i = #tmp, 1, -1 do
|
||||
table.insert(output, tmp[i])
|
||||
end
|
||||
if last ~= decoderInvalidCode then
|
||||
suffix[hi] = string.char(c)
|
||||
prefix[hi] = last
|
||||
end
|
||||
else
|
||||
return nil, "Invalid code: " .. code .. " at position " .. i
|
||||
end
|
||||
|
||||
last, hi = code, hi + 1
|
||||
if hi >= overflow then
|
||||
if hi > overflow then
|
||||
return nil, "unreachable"
|
||||
end
|
||||
if width == maxWidth then
|
||||
last = decoderInvalidCode
|
||||
hi = hi - 1
|
||||
else
|
||||
width = width + 1
|
||||
overflow = 1 << width
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil, "Unexpected end of input: "..code
|
||||
end
|
||||
|
||||
return { decompress = decompress }
|
||||
end)()
|
||||
|
||||
msgpack = (function()
|
||||
--[[----------------------------------------------------------------------------
|
||||
|
||||
MessagePack encoder / decoder written in pure Lua 5.3 / Lua 5.4
|
||||
written by Sebastian Steinhauer <s.steinhauer@yahoo.de>
|
||||
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
|
||||
--]]----------------------------------------------------------------------------
|
||||
local pack, unpack = string.pack, string.unpack
|
||||
local mtype, utf8len = math.type, utf8.len
|
||||
local tconcat, tunpack = table.concat, table.unpack
|
||||
local ssub = string.sub
|
||||
local type, pcall, pairs, select = type, pcall, pairs, select
|
||||
|
||||
--[[----------------------------------------------------------------------------
|
||||
|
||||
Decoder
|
||||
|
||||
--]]----------------------------------------------------------------------------
|
||||
local decode_value -- forward declaration
|
||||
|
||||
local function decode_array(data, position, length)
|
||||
local elements, value = {}
|
||||
for i = 1, length do
|
||||
value, position = decode_value(data, position)
|
||||
elements[i] = value
|
||||
end
|
||||
return elements, position
|
||||
end
|
||||
|
||||
local function decode_map(data, position, length)
|
||||
local elements, key, value = {}
|
||||
for i = 1, length do
|
||||
key, position = decode_value(data, position)
|
||||
value, position = decode_value(data, position)
|
||||
elements[key] = value
|
||||
end
|
||||
return elements, position
|
||||
end
|
||||
|
||||
local decoder_functions = {
|
||||
[0xc0] = function(data, position)
|
||||
return nil, position
|
||||
end,
|
||||
[0xc2] = function(data, position)
|
||||
return false, position
|
||||
end,
|
||||
[0xc3] = function(data, position)
|
||||
return true, position
|
||||
end,
|
||||
[0xc4] = function(data, position)
|
||||
return unpack('>s1', data, position)
|
||||
end,
|
||||
[0xc5] = function(data, position)
|
||||
return unpack('>s2', data, position)
|
||||
end,
|
||||
[0xc6] = function(data, position)
|
||||
return unpack('>s4', data, position)
|
||||
end,
|
||||
[0xca] = function(data, position)
|
||||
return unpack('>f', data, position)
|
||||
end,
|
||||
[0xcb] = function(data, position)
|
||||
return unpack('>d', data, position)
|
||||
end,
|
||||
[0xcc] = function(data, position)
|
||||
return unpack('>B', data, position)
|
||||
end,
|
||||
[0xcd] = function(data, position)
|
||||
return unpack('>I2', data, position)
|
||||
end,
|
||||
[0xce] = function(data, position)
|
||||
return unpack('>I4', data, position)
|
||||
end,
|
||||
[0xcf] = function(data, position)
|
||||
return unpack('>I8', data, position)
|
||||
end,
|
||||
[0xd0] = function(data, position)
|
||||
return unpack('>b', data, position)
|
||||
end,
|
||||
[0xd1] = function(data, position)
|
||||
return unpack('>i2', data, position)
|
||||
end,
|
||||
[0xd2] = function(data, position)
|
||||
return unpack('>i4', data, position)
|
||||
end,
|
||||
[0xd3] = function(data, position)
|
||||
return unpack('>i8', data, position)
|
||||
end,
|
||||
[0xd9] = function(data, position)
|
||||
return unpack('>s1', data, position)
|
||||
end,
|
||||
[0xda] = function(data, position)
|
||||
return unpack('>s2', data, position)
|
||||
end,
|
||||
[0xdb] = function(data, position)
|
||||
return unpack('>s4', data, position)
|
||||
end,
|
||||
[0xdc] = function(data, position)
|
||||
local length
|
||||
length, position = unpack('>I2', data, position)
|
||||
return decode_array(data, position, length)
|
||||
end,
|
||||
[0xdd] = function(data, position)
|
||||
local length
|
||||
length, position = unpack('>I4', data, position)
|
||||
return decode_array(data, position, length)
|
||||
end,
|
||||
[0xde] = function(data, position)
|
||||
local length
|
||||
length, position = unpack('>I2', data, position)
|
||||
return decode_map(data, position, length)
|
||||
end,
|
||||
[0xdf] = function(data, position)
|
||||
local length
|
||||
length, position = unpack('>I4', data, position)
|
||||
return decode_map(data, position, length)
|
||||
end,
|
||||
}
|
||||
|
||||
-- add fix-array, fix-map, fix-string, fix-int stuff
|
||||
for i = 0x00, 0x7f do
|
||||
decoder_functions[i] = function(data, position)
|
||||
return i, position
|
||||
end
|
||||
end
|
||||
for i = 0x80, 0x8f do
|
||||
decoder_functions[i] = function(data, position)
|
||||
return decode_map(data, position, i - 0x80)
|
||||
end
|
||||
end
|
||||
for i = 0x90, 0x9f do
|
||||
decoder_functions[i] = function(data, position)
|
||||
return decode_array(data, position, i - 0x90)
|
||||
end
|
||||
end
|
||||
for i = 0xa0, 0xbf do
|
||||
decoder_functions[i] = function(data, position)
|
||||
local length = i - 0xa0
|
||||
return ssub(data, position, position + length - 1), position + length
|
||||
end
|
||||
end
|
||||
for i = 0xe0, 0xff do
|
||||
decoder_functions[i] = function(data, position)
|
||||
return -32 + (i - 0xe0), position
|
||||
end
|
||||
end
|
||||
|
||||
decode_value = function(data, position)
|
||||
local byte, value
|
||||
byte, position = unpack('B', data, position)
|
||||
value, position = decoder_functions[byte](data, position)
|
||||
return value, position
|
||||
end
|
||||
|
||||
|
||||
--[[----------------------------------------------------------------------------
|
||||
|
||||
Interface
|
||||
|
||||
--]]----------------------------------------------------------------------------
|
||||
return {
|
||||
_AUTHOR = 'Sebastian Steinhauer <s.steinhauer@yahoo.de>',
|
||||
_VERSION = '0.6.1',
|
||||
|
||||
-- primary decode function
|
||||
decode = function(data, position)
|
||||
local values, value, ok = {}
|
||||
position = position or 1
|
||||
while position <= #data do
|
||||
ok, value, position = pcall(decode_value, data, position)
|
||||
if ok then
|
||||
values[#values + 1] = value
|
||||
else
|
||||
return nil, 'cannot decode MessagePack'
|
||||
end
|
||||
end
|
||||
return tunpack(values)
|
||||
end,
|
||||
|
||||
-- decode just one value
|
||||
decode_one = function(data, position)
|
||||
local value, ok
|
||||
ok, value, position = pcall(decode_value, data, position or 1)
|
||||
if ok then
|
||||
return value, position
|
||||
else
|
||||
return nil, 'cannot decode MessagePack'
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
--[[----------------------------------------------------------------------------
|
||||
--]]----------------------------------------------------------------------------
|
||||
end)()
|
||||
59
contrib/wireshark/moby-networkdb.lua
Normal file
59
contrib/wireshark/moby-networkdb.lua
Normal file
@@ -0,0 +1,59 @@
|
||||
-- https://wiki.wireshark.org/Protobuf
|
||||
local protobuf_dissector = Dissector.get("protobuf")
|
||||
|
||||
local msgtype_field = Field.new("pbf.networkdb.GossipMessage.type")
|
||||
local msgdata_field = Field.new("pbf.networkdb.GossipMessage.data")
|
||||
|
||||
local msgtype_map = {
|
||||
[1] = "networkdb.NetworkEvent",
|
||||
[2] = "networkdb.TableEvent",
|
||||
[3] = "networkdb.NetworkPushPull",
|
||||
[4] = "networkdb.BulkSyncMessage",
|
||||
[5] = "networkdb.CompoundMessage",
|
||||
[6] = "networkdb.NodeEvent",
|
||||
}
|
||||
|
||||
local function last_fieldinfo(field)
|
||||
local fieldset = { field() }
|
||||
return fieldset[#fieldset]
|
||||
end
|
||||
|
||||
local gossip_proto = Proto("networkdbgossip", "Moby NetworkDB Gossip")
|
||||
function gossip_proto.dissector(tvb, pinfo, tree)
|
||||
pinfo.private["pb_msg_type"] = "message,networkdb.GossipMessage"
|
||||
pcall(Dissector.call, protobuf_dissector, tvb, pinfo, tree)
|
||||
|
||||
local msgtype_fieldinfo = last_fieldinfo(msgtype_field)
|
||||
if not msgtype_fieldinfo then return end
|
||||
local msgdata_fieldinfo = last_fieldinfo(msgdata_field)
|
||||
if not msgdata_fieldinfo then return end
|
||||
if msgdata_fieldinfo.offset < msgtype_fieldinfo.offset + #msgtype_fieldinfo then return end
|
||||
local decodeas = msgtype_map[msgtype_fieldinfo()]
|
||||
if not decodeas then return end
|
||||
pinfo.private["pb_msg_type"] = "message," .. decodeas
|
||||
local ok, err = pcall(Dissector.call, protobuf_dissector, msgdata_fieldinfo.range:tvb(), pinfo, tree)
|
||||
if not ok then tree:add_expert_info(PI_DISSECTOR_BUG, PI_ERROR, "Dissector for " .. decodeas .. " failed: " .. tostring(err)) end
|
||||
end
|
||||
|
||||
local tableevent_msgtype_map = {
|
||||
["overlay_peer_table"] = "overlay.PeerRecord",
|
||||
["endpoint_table"] = "libnetwork.EndpointRecord",
|
||||
}
|
||||
|
||||
local tableevent_name_field = Field.new("pbf.networkdb.TableEvent.table_name")
|
||||
local tableevent_proto = Proto("networkdb-tableevent", "NetworkDB Table Event")
|
||||
function tableevent_proto.dissector(tvb, pinfo, tree)
|
||||
local table_name_fieldinfo = last_fieldinfo(tableevent_name_field)
|
||||
if not table_name_fieldinfo then return end
|
||||
local decodeas = tableevent_msgtype_map[table_name_fieldinfo()]
|
||||
if not decodeas then return end
|
||||
pinfo.private["pb_msg_type"] = "message," .. decodeas
|
||||
local ok, err = pcall(Dissector.call, protobuf_dissector, tvb, pinfo, tree)
|
||||
if not ok then tree:add_expert_info(PI_DISSECTOR_BUG, PI_ERROR, "Dissector for " .. decodeas .. " failed: " .. tostring(err)) end
|
||||
end
|
||||
|
||||
|
||||
local protobuf_field_table = DissectorTable.get("protobuf_field")
|
||||
protobuf_field_table:add("networkdb.CompoundMessage.SimpleMessage.Payload", gossip_proto)
|
||||
protobuf_field_table:add("networkdb.BulkSyncMessage.payload", gossip_proto)
|
||||
protobuf_field_table:add("networkdb.TableEvent.value", tableevent_proto)
|
||||
@@ -65,6 +65,7 @@ func (nDB *NetworkDB) SetKey(key []byte) {
|
||||
if nDB.keyring != nil {
|
||||
nDB.keyring.AddKey(key)
|
||||
}
|
||||
logEncKeys(context.TODO(), key)
|
||||
}
|
||||
|
||||
// SetPrimaryKey sets the given key as the primary key. This should have
|
||||
@@ -127,6 +128,7 @@ func (nDB *NetworkDB) clusterInit() error {
|
||||
for i, key := range nDB.config.Keys {
|
||||
log.G(context.TODO()).Debugf("Encryption key %d: %.5s", i+1, hex.EncodeToString(key))
|
||||
}
|
||||
logEncKeys(context.TODO(), nDB.config.Keys...)
|
||||
nDB.keyring, err = memberlist.NewKeyring(nDB.config.Keys, nDB.config.Keys[0])
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
45
libnetwork/networkdb/debug.go
Normal file
45
libnetwork/networkdb/debug.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package networkdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
|
||||
"github.com/containerd/log"
|
||||
)
|
||||
|
||||
func logEncKeys(ctx context.Context, keys ...[]byte) {
|
||||
klpath := os.Getenv("NETWORKDBKEYLOGFILE")
|
||||
if klpath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
die := func(err error) {
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": klpath,
|
||||
}).Error("could not write to NetworkDB encryption-key log")
|
||||
}
|
||||
f, err := os.OpenFile(klpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
die(err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
die(err)
|
||||
}
|
||||
}()
|
||||
|
||||
tohex := hex.NewEncoder(f)
|
||||
for _, key := range keys {
|
||||
if _, err := tohex.Write(key); err != nil {
|
||||
die(err)
|
||||
return
|
||||
}
|
||||
if _, err := f.WriteString("\n"); err != nil {
|
||||
die(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user