├── README.mediawiki └── redis-wireshark.lua /README.mediawiki: -------------------------------------------------------------------------------- 1 | == redis-wireshark == 2 | 3 | === Motivation === 4 | 5 | To watch the network activity associated with Redis using the popular tool Wireshark (formerly Ethereal). 6 | 7 | === Usage === 8 | 9 | To use this, copy redis-wireshark.lua to any filename you like under [https://www.wireshark.org/docs/wsug_html_chunked/ChPluginFolders.html ~/.local/lib/wireshark/plugins]. 10 | Then when you run Wireshark it will understand TCP communications on port 6379 as Redis messages, 11 | and will know how to interpret them. It also works with the command-line program tshark. 12 | 13 | === Notes === 14 | 15 | Depending on your system configuration, you may need to be root to capture live traffic. 16 | Since running plugins as root is not necessarily a great idea, try capturing some data to a PCAP file, 17 | then load it with the plugin after running Wireshark as an unprivileged user. 18 | -------------------------------------------------------------------------------- /redis-wireshark.lua: -------------------------------------------------------------------------------- 1 | -- Wireshark packet dissector for Redis 2 | -- Protocol specification: http://redis.io/topics/protocol 3 | -- Written by John Zwinck, 29 November 2011 4 | 5 | do -- scope 6 | local proto = Proto('redis', 'Redis') 7 | 8 | local f = proto.fields 9 | -- we could make more of these, e.g. to distinguish keys from values 10 | f.bulk_reply_num = ProtoField.string('redis.bulk_reply_num', 'bulk_reply_num') 11 | f.value = ProtoField.string('redis.value', 'Value') 12 | f.size = ProtoField.string('redis.value_size', 'Value Size') 13 | 14 | function proto.dissector(buffer, pinfo, tree) 15 | pinfo.cols.protocol = 'Redis' 16 | 17 | mtypes = { 18 | ['+'] = 'Status', 19 | ['-'] = 'Error', 20 | [':'] = 'Integer', 21 | ['$'] = 'Bulk', 22 | ['*'] = 'Multi-Bulk', 23 | } 24 | 25 | local CRLF = 2 -- constant length of \r\n 26 | 27 | local function matches(buffer, match_offset) 28 | return buffer(match_offset):string():match('[^\r\n]+') 29 | end 30 | -- recursively parse and generate a tree of data from messages in a packet 31 | -- parent: the tree root to populate under 32 | -- buffer: the entire packet buffer 33 | -- offset: the current offset in the buffer 34 | -- returns: the new offset (i.e. the input offset plus the number of bytes consumed) 35 | local function recurse(parent, buffer, offset) 36 | local line = matches(buffer, offset) -- get next line 37 | local length = line:len() 38 | 39 | local prefix, text = line:match('([-+:$*])(.+)') 40 | local mtype = mtypes[prefix] 41 | 42 | if not prefix or not text then 43 | pinfo.desegment_len = DESEGMENT_ONE_MORE_SEGMENT 44 | return -1 45 | end 46 | 47 | assert(prefix and text, 'unrecognized line: '..line) 48 | assert(mtype, 'unrecognized message type: '..prefix) 49 | 50 | if prefix == '*' then -- multi-bulk, contains multiple sub-messages 51 | local replies = tonumber(text) 52 | local old_offset = offset 53 | 54 | local child = parent:add(proto, buffer(offset, 1), 'Redis '..mtype..' Reply') 55 | child:add(f.bulk_reply_num, buffer(offset + 1, length - 1)) 56 | 57 | offset = offset + length + CRLF 58 | 59 | -- recurse down for each message contained in this multi-bulk message 60 | for ii = 1, replies do 61 | offset = recurse(child, buffer, offset) 62 | if offset == -1 then 63 | pinfo.desegment_len = DESEGMENT_ONE_MORE_SEGMENT 64 | return -1 65 | end 66 | end 67 | child:set_len(offset - old_offset) 68 | 69 | elseif prefix == '$' then -- bulk, contains one binary string 70 | local bytes = tonumber(text) 71 | 72 | if bytes == -1 then 73 | local child = parent:add(proto, buffer(offset, length + CRLF), 74 | 'Redis '..mtype..' Reply') 75 | 76 | offset = offset + length + CRLF 77 | 78 | child:add(f.value, '') 79 | else 80 | if(buffer:len() < offset + length + CRLF + bytes + CRLF) then 81 | pinfo.desegment_len = DESEGMENT_ONE_MORE_SEGMENT 82 | return -1 83 | end 84 | 85 | local child = parent:add(proto, buffer(offset, length + CRLF + bytes + CRLF), 86 | 'Redis '..mtype..' Reply') 87 | -- add size 88 | child:add(f.size, buffer(offset + 1, length - 1)) 89 | 90 | offset = offset + length + CRLF 91 | 92 | -- get the string contained within this bulk message 93 | child:add(f.value, buffer(offset, bytes)) 94 | offset = offset + bytes + CRLF 95 | 96 | end 97 | else -- integer, status or error 98 | local child = parent:add(proto, buffer(offset, length + CRLF), 99 | 'Redis '..mtype..' Reply') 100 | child:add(f.value, buffer(offset + prefix:len(), length - prefix:len())) 101 | offset = offset + length + CRLF 102 | end 103 | 104 | return offset 105 | end 106 | 107 | -- parse top-level messages until the buffer is exhausted 108 | local offset = 0 109 | while offset < buffer():len() do 110 | offset = recurse(tree, buffer, offset) 111 | if offset < 0 then 112 | pinfo.desegment_len = DESEGMENT_ONE_MORE_SEGMENT 113 | return 114 | end 115 | end 116 | 117 | -- check that we consumed exactly the right number of bytes 118 | -- assert(offset == buffer():len(), 'consumed '..offset..' bytes of '..buffer():len()) 119 | end 120 | 121 | -- register this dissector for the standard Redis ports 122 | local dissectors = DissectorTable.get('tcp.port') 123 | for _, port in ipairs{ 6379, } do 124 | dissectors:add(port, proto) 125 | end 126 | end 127 | --------------------------------------------------------------------------------