redhound 1.0.1 → 2.0.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/README.md +97 -31
- data/docs/API.md +104 -0
- data/docs/FILTERS.md +37 -0
- data/docs/MIGRATION.md +23 -0
- data/docs/PROTOCOLS.md +54 -0
- data/docs/USAGE.md +86 -0
- data/docs/VALIDATION.md +175 -0
- data/docs/json-schema.json +64 -0
- data/exe/redhound +2 -1
- data/lib/redhound/analysis/flow_key.rb +59 -0
- data/lib/redhound/analysis/flow_table.rb +124 -0
- data/lib/redhound/analysis/ip_reassembler.rb +209 -0
- data/lib/redhound/analysis/stats.rb +150 -0
- data/lib/redhound/analysis/tcp_analysis.rb +43 -0
- data/lib/redhound/analysis/tcp_reassembler.rb +176 -0
- data/lib/redhound/analysis/tcp_stream.rb +156 -0
- data/lib/redhound/analysis.rb +157 -0
- data/lib/redhound/capture/bsd/bpf_device.rb +155 -0
- data/lib/redhound/capture/bsd/constants.rb +29 -0
- data/lib/redhound/capture/bsd/ifreq.rb +41 -0
- data/lib/redhound/capture/file_source.rb +214 -0
- data/lib/redhound/capture/interface.rb +70 -0
- data/lib/redhound/capture/linktype.rb +30 -0
- data/lib/redhound/capture/linux/constants.rb +37 -0
- data/lib/redhound/capture/linux/cooked.rb +18 -0
- data/lib/redhound/capture/linux/ifreq.rb +42 -0
- data/lib/redhound/capture/linux/packet_socket.rb +224 -0
- data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
- data/lib/redhound/capture/linux/tpacket_v3.rb +137 -0
- data/lib/redhound/capture/source.rb +79 -0
- data/lib/redhound/capture/stats.rb +22 -0
- data/lib/redhound/capture.rb +70 -0
- data/lib/redhound/cli/command.rb +42 -0
- data/lib/redhound/cli/options.rb +133 -0
- data/lib/redhound/cli/privileges.rb +29 -0
- data/lib/redhound/cli/runner.rb +129 -0
- data/lib/redhound/context.rb +20 -0
- data/lib/redhound/cursor.rb +69 -0
- data/lib/redhound/diagnostic.rb +7 -0
- data/lib/redhound/dissector.rb +152 -0
- data/lib/redhound/engine.rb +69 -0
- data/lib/redhound/errors.rb +38 -0
- data/lib/redhound/field.rb +62 -0
- data/lib/redhound/file/format.rb +63 -0
- data/lib/redhound/file/pcap_reader.rb +43 -0
- data/lib/redhound/file/pcap_writer.rb +79 -0
- data/lib/redhound/file/pcapng_reader.rb +221 -0
- data/lib/redhound/file/pcapng_writer.rb +151 -0
- data/lib/redhound/file/rotating_writer.rb +98 -0
- data/lib/redhound/filter/analyzer.rb +197 -0
- data/lib/redhound/filter/bpf/assembler.rb +121 -0
- data/lib/redhound/filter/bpf/disassembler.rb +58 -0
- data/lib/redhound/filter/bpf/validator.rb +77 -0
- data/lib/redhound/filter/bpf/vm.rb +109 -0
- data/lib/redhound/filter/codegen.rb +413 -0
- data/lib/redhound/filter/lexer.rb +46 -0
- data/lib/redhound/filter/parser.rb +206 -0
- data/lib/redhound/filter/program.rb +55 -0
- data/lib/redhound/filter.rb +30 -0
- data/lib/redhound/layer.rb +75 -0
- data/lib/redhound/output/fields.rb +20 -0
- data/lib/redhound/output/follow.rb +64 -0
- data/lib/redhound/output/hexdump.rb +29 -0
- data/lib/redhound/output/json.rb +32 -0
- data/lib/redhound/output/summary.rb +84 -0
- data/lib/redhound/output/timestamp.rb +34 -0
- data/lib/redhound/output/tree.rb +25 -0
- data/lib/redhound/packet.rb +77 -0
- data/lib/redhound/protocols/arp.rb +39 -0
- data/lib/redhound/protocols/checksum.rb +38 -0
- data/lib/redhound/protocols/data.rb +23 -0
- data/lib/redhound/protocols/dhcp.rb +140 -0
- data/lib/redhound/protocols/dns.rb +268 -0
- data/lib/redhound/protocols/ethernet.rb +24 -0
- data/lib/redhound/protocols/http.rb +185 -0
- data/lib/redhound/protocols/icmp.rb +205 -0
- data/lib/redhound/protocols/ipv4.rb +56 -0
- data/lib/redhound/protocols/ipv6.rb +68 -0
- data/lib/redhound/protocols/link.rb +70 -0
- data/lib/redhound/protocols/llc.rb +37 -0
- data/lib/redhound/protocols/ntp.rb +45 -0
- data/lib/redhound/protocols/tcp.rb +88 -0
- data/lib/redhound/protocols/tls.rb +218 -0
- data/lib/redhound/protocols/tunnel.rb +48 -0
- data/lib/redhound/protocols/udp.rb +32 -0
- data/lib/redhound/protocols/vlan.rb +23 -0
- data/lib/redhound/reader.rb +8 -0
- data/lib/redhound/registry.rb +64 -0
- data/lib/redhound/stream_dissector.rb +168 -0
- data/lib/redhound/util/seq.rb +21 -0
- data/lib/redhound/version.rb +2 -1
- data/lib/redhound/writer.rb +25 -43
- data/lib/redhound.rb +81 -9
- data/sig/generated/redhound/analysis/flow_key.rbs +15 -0
- data/sig/generated/redhound/analysis/flow_table.rbs +102 -0
- data/sig/generated/redhound/analysis/ip_reassembler.rbs +52 -0
- data/sig/generated/redhound/analysis/stats.rbs +80 -0
- data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
- data/sig/generated/redhound/analysis/tcp_reassembler.rbs +39 -0
- data/sig/generated/redhound/analysis/tcp_stream.rbs +60 -0
- data/sig/generated/redhound/analysis.rbs +38 -0
- data/sig/generated/redhound/capture/bsd/bpf_device.rbs +39 -0
- data/sig/generated/redhound/capture/bsd/constants.rbs +40 -0
- data/sig/generated/redhound/capture/bsd/ifreq.rbs +15 -0
- data/sig/generated/redhound/capture/file_source.rbs +67 -0
- data/sig/generated/redhound/capture/interface.rbs +53 -0
- data/sig/generated/redhound/capture/linktype.rbs +20 -0
- data/sig/generated/redhound/capture/linux/constants.rbs +58 -0
- data/sig/generated/redhound/capture/linux/cooked.rbs +15 -0
- data/sig/generated/redhound/capture/linux/ifreq.rbs +18 -0
- data/sig/generated/redhound/capture/linux/packet_socket.rbs +64 -0
- data/sig/generated/redhound/capture/linux/sockaddr_ll.rbs +31 -0
- data/sig/generated/redhound/capture/linux/tpacket_v3.rbs +35 -0
- data/sig/generated/redhound/capture/source.rbs +44 -0
- data/sig/generated/redhound/capture/stats.rbs +25 -0
- data/sig/generated/redhound/capture.rbs +14 -0
- data/sig/generated/redhound/cli/command.rbs +12 -0
- data/sig/generated/redhound/cli/options.rbs +25 -0
- data/sig/generated/redhound/cli/privileges.rbs +12 -0
- data/sig/generated/redhound/cli/runner.rbs +27 -0
- data/sig/generated/redhound/context.rbs +27 -0
- data/sig/generated/redhound/cursor.rbs +59 -0
- data/sig/generated/redhound/diagnostic.rbs +21 -0
- data/sig/generated/redhound/dissector.rbs +117 -0
- data/sig/generated/redhound/engine.rbs +23 -0
- data/sig/generated/redhound/errors.rbs +56 -0
- data/sig/generated/redhound/field.rbs +74 -0
- data/sig/generated/redhound/file/format.rbs +39 -0
- data/sig/generated/redhound/file/pcap_reader.rbs +19 -0
- data/sig/generated/redhound/file/pcap_writer.rbs +38 -0
- data/sig/generated/redhound/file/pcapng_reader.rbs +54 -0
- data/sig/generated/redhound/file/pcapng_writer.rbs +50 -0
- data/sig/generated/redhound/file/rotating_writer.rbs +42 -0
- data/sig/generated/redhound/filter/analyzer.rbs +42 -0
- data/sig/generated/redhound/filter/bpf/assembler.rbs +39 -0
- data/sig/generated/redhound/filter/bpf/disassembler.rbs +18 -0
- data/sig/generated/redhound/filter/bpf/validator.rbs +30 -0
- data/sig/generated/redhound/filter/bpf/vm.rbs +28 -0
- data/sig/generated/redhound/filter/codegen.rbs +101 -0
- data/sig/generated/redhound/filter/lexer.rbs +15 -0
- data/sig/generated/redhound/filter/parser.rbs +65 -0
- data/sig/generated/redhound/filter/program.rbs +48 -0
- data/sig/generated/redhound/filter.rbs +10 -0
- data/sig/generated/redhound/layer.rbs +81 -0
- data/sig/generated/redhound/output/fields.rbs +15 -0
- data/sig/generated/redhound/output/follow.rbs +21 -0
- data/sig/generated/redhound/output/hexdump.rbs +15 -0
- data/sig/generated/redhound/output/json.rbs +18 -0
- data/sig/generated/redhound/output/summary.rbs +21 -0
- data/sig/generated/redhound/output/timestamp.rbs +21 -0
- data/sig/generated/redhound/output/tree.rbs +15 -0
- data/sig/generated/redhound/packet.rbs +82 -0
- data/sig/generated/redhound/protocols/arp.rbs +15 -0
- data/sig/generated/redhound/protocols/checksum.rbs +15 -0
- data/sig/generated/redhound/protocols/data.rbs +17 -0
- data/sig/generated/redhound/protocols/dhcp.rbs +31 -0
- data/sig/generated/redhound/protocols/dns.rbs +42 -0
- data/sig/generated/redhound/protocols/ethernet.rbs +15 -0
- data/sig/generated/redhound/protocols/http.rbs +36 -0
- data/sig/generated/redhound/protocols/icmp.rbs +41 -0
- data/sig/generated/redhound/protocols/ipv4.rbs +15 -0
- data/sig/generated/redhound/protocols/ipv6.rbs +24 -0
- data/sig/generated/redhound/protocols/link.rbs +33 -0
- data/sig/generated/redhound/protocols/llc.rbs +15 -0
- data/sig/generated/redhound/protocols/ntp.rbs +17 -0
- data/sig/generated/redhound/protocols/tcp.rbs +25 -0
- data/sig/generated/redhound/protocols/tls.rbs +51 -0
- data/sig/generated/redhound/protocols/tunnel.rbs +24 -0
- data/sig/generated/redhound/protocols/udp.rbs +18 -0
- data/sig/generated/redhound/protocols/vlan.rbs +12 -0
- data/sig/generated/redhound/reader.rbs +7 -0
- data/sig/generated/redhound/registry.rbs +45 -0
- data/sig/generated/redhound/stream_dissector.rbs +44 -0
- data/sig/generated/redhound/util/seq.rbs +20 -0
- data/sig/generated/redhound/version.rbs +1 -0
- data/sig/generated/redhound/writer.rbs +6 -19
- data/sig/generated/redhound.rbs +18 -0
- data/sig/shims/io.rbs +7 -0
- data/sig/shims/process.rbs +3 -0
- metadata +177 -55
- data/CODE_OF_CONDUCT.md +0 -132
- data/Dockerfile +0 -7
- data/Rakefile +0 -15
- data/Steepfile +0 -4
- data/docker-compose.yml +0 -9
- data/lib/redhound/analyzer.rb +0 -34
- data/lib/redhound/builder/packet_mreq.rb +0 -56
- data/lib/redhound/builder/socket.rb +0 -35
- data/lib/redhound/builder.rb +0 -4
- data/lib/redhound/command.rb +0 -67
- data/lib/redhound/l2/ether.rb +0 -68
- data/lib/redhound/l2/protocol.rb +0 -33
- data/lib/redhound/l2.rb +0 -4
- data/lib/redhound/l3/arp.rb +0 -114
- data/lib/redhound/l3/base.rb +0 -49
- data/lib/redhound/l3/ipv4.rb +0 -95
- data/lib/redhound/l3/ipv6.rb +0 -69
- data/lib/redhound/l3/protocol.rb +0 -173
- data/lib/redhound/l3/resolver.rb +0 -30
- data/lib/redhound/l3.rb +0 -9
- data/lib/redhound/l4/base.rb +0 -31
- data/lib/redhound/l4/icmp.rb +0 -71
- data/lib/redhound/l4/resolver.rb +0 -28
- data/lib/redhound/l4/udp.rb +0 -63
- data/lib/redhound/l4.rb +0 -6
- data/lib/redhound/receiver.rb +0 -45
- data/lib/redhound/resolver.rb +0 -22
- data/lib/redhound/source/socket.rb +0 -18
- data/lib/redhound/source.rb +0 -3
- data/rbs_collection.lock.yaml +0 -20
- data/rbs_collection.yaml +0 -17
- data/sig/generated/redhound/analyzer.rbs +0 -14
- data/sig/generated/redhound/builder/packet_mreq.rbs +0 -39
- data/sig/generated/redhound/builder/socket.rbs +0 -24
- data/sig/generated/redhound/command.rbs +0 -19
- data/sig/generated/redhound/l2/ether.rbs +0 -41
- data/sig/generated/redhound/l2/protocol.rbs +0 -15
- data/sig/generated/redhound/l3/arp.rbs +0 -57
- data/sig/generated/redhound/l3/base.rbs +0 -28
- data/sig/generated/redhound/l3/ipv4.rbs +0 -53
- data/sig/generated/redhound/l3/ipv6.rbs +0 -38
- data/sig/generated/redhound/l3/protocol.rbs +0 -16
- data/sig/generated/redhound/l3/resolver.rbs +0 -16
- data/sig/generated/redhound/l4/base.rbs +0 -19
- data/sig/generated/redhound/l4/icmp.rbs +0 -33
- data/sig/generated/redhound/l4/resolver.rbs +0 -16
- data/sig/generated/redhound/l4/udp.rbs +0 -36
- data/sig/generated/redhound/receiver.rbs +0 -19
- data/sig/generated/redhound/resolver.rbs +0 -11
- data/sig/generated/redhound/source/socket.rbs +0 -13
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Filter
|
|
7
|
+
# @api private
|
|
8
|
+
class Program
|
|
9
|
+
KernelPacket = Struct.new(:data, :original_length, :meta)
|
|
10
|
+
attr_reader :instructions # : Array[[Integer, Integer, Integer, Integer]]
|
|
11
|
+
attr_reader :linktype # : Integer?
|
|
12
|
+
|
|
13
|
+
# @rbs (Array[[Integer, Integer, Integer, Integer]] instructions, ?linktype: Integer?, ?cooked_live: bool) -> void
|
|
14
|
+
def initialize(instructions, linktype: nil, cooked_live: false)
|
|
15
|
+
BPF::Validator.validate!(instructions)
|
|
16
|
+
@instructions = instructions.map { |instruction| instruction.dup.freeze }.freeze
|
|
17
|
+
@linktype = linktype
|
|
18
|
+
@cooked_live = cooked_live
|
|
19
|
+
@vm = BPF::VM.new(@instructions)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs (untyped packet) -> Integer
|
|
23
|
+
def evaluate(packet)
|
|
24
|
+
return 0 if linktype && packet.linktype != linktype
|
|
25
|
+
|
|
26
|
+
if @cooked_live
|
|
27
|
+
return 0 if packet.data.bytesize < 20
|
|
28
|
+
protocol = packet.data.unpack1('n')
|
|
29
|
+
packet = KernelPacket.new(packet.data.byteslice(20..), packet.original_length - 20, packet.meta.merge(protocol: protocol))
|
|
30
|
+
end
|
|
31
|
+
@vm.evaluate(packet)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs (untyped packet) -> bool
|
|
35
|
+
def match?(packet) = evaluate(packet).positive?
|
|
36
|
+
|
|
37
|
+
# Native struct sock_filter / struct bpf_insn bytes.
|
|
38
|
+
# @rbs () -> String
|
|
39
|
+
def packed = instructions.map { |instruction| instruction.pack('SCCL') }.join
|
|
40
|
+
|
|
41
|
+
alias serialize packed
|
|
42
|
+
alias to_binary packed
|
|
43
|
+
|
|
44
|
+
# @rbs (?format: Symbol) -> String
|
|
45
|
+
def disassemble(format: :text) = BPF::Disassembler.disassemble(instructions, format: format)
|
|
46
|
+
|
|
47
|
+
# Includes a String reference retained by pack('P') until setsockopt/ioctl finishes.
|
|
48
|
+
# @rbs () -> String
|
|
49
|
+
def sock_fprog
|
|
50
|
+
pointer_size = [nil].pack('P').bytesize
|
|
51
|
+
[instructions.size, packed].pack("S x#{pointer_size - 2} P")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
require_relative 'filter/bpf/validator'
|
|
6
|
+
require_relative 'filter/bpf/vm'
|
|
7
|
+
require_relative 'filter/bpf/disassembler'
|
|
8
|
+
require_relative 'filter/program'
|
|
9
|
+
require_relative 'filter/lexer'
|
|
10
|
+
require_relative 'filter/parser'
|
|
11
|
+
require_relative 'filter/analyzer'
|
|
12
|
+
require_relative 'filter/bpf/assembler'
|
|
13
|
+
require_relative 'filter/codegen'
|
|
14
|
+
|
|
15
|
+
module Redhound
|
|
16
|
+
# @api private
|
|
17
|
+
module Filter
|
|
18
|
+
# Compiles a pcap-filter subset without libpcap or external commands.
|
|
19
|
+
# @rbs (String expression, ?linktype: Symbol | Integer, ?snaplen: Integer, ?live: bool) -> Program
|
|
20
|
+
def self.compile(expression, linktype: :ethernet, snaplen: 262_144, live: false)
|
|
21
|
+
raise ArgumentError, 'filter expression must be a String' unless expression.is_a?(String)
|
|
22
|
+
raise ArgumentError, 'snaplen must be between 1 and 16777216' unless snaplen.between?(1, 16_777_216)
|
|
23
|
+
type = linktype.is_a?(Integer) ? linktype : CodeGen::LINKTYPES.fetch(linktype) { raise FilterError, "unknown linktype #{linktype}" }
|
|
24
|
+
ast = Analyzer.new(expression).analyze(Parser.new(expression).parse)
|
|
25
|
+
CodeGen.new(type, snaplen, live, expression).compile(ast)
|
|
26
|
+
rescue SystemStackError
|
|
27
|
+
raise FilterTooLarge, 'filter expression is too deeply nested'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# One decoded protocol header and its payload bounds, fields and diagnostics.
|
|
6
|
+
class Layer
|
|
7
|
+
# Protocol identity, captured offset, decoded values and diagnostics.
|
|
8
|
+
attr_reader :protocol, :offset, :diagnostics, :definitions, :values, :embedded
|
|
9
|
+
# Header size and absolute payload bounds used by child dissectors.
|
|
10
|
+
attr_accessor :header_length, :payload_offset, :payload_end
|
|
11
|
+
|
|
12
|
+
# @rbs (Symbol protocol, Integer offset, Integer length, Integer limit, ?definitions: Array[untyped], ?values: Hash[Symbol, untyped], ?embedded: bool) -> void
|
|
13
|
+
# Create a bounded header with optional fixed field definitions and values.
|
|
14
|
+
def initialize(protocol, offset, length, limit, definitions: [], values: {}, embedded: false)
|
|
15
|
+
@protocol, @offset, @header_length = protocol, offset, length
|
|
16
|
+
@payload_offset, @payload_end = offset + length, limit
|
|
17
|
+
@definitions, @values, @embedded = definitions, values, embedded
|
|
18
|
+
@diagnostics = [] # @rbs Array[untyped]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @rbs (Symbol key) -> untyped
|
|
22
|
+
# Read the raw value identified by a symbolic field key.
|
|
23
|
+
def [](key) = @values[key]
|
|
24
|
+
# @rbs () -> Integer
|
|
25
|
+
# Decoded header length in bytes.
|
|
26
|
+
def length = @header_length
|
|
27
|
+
# @rbs () -> bool
|
|
28
|
+
# Whether no bytes remain for a child protocol.
|
|
29
|
+
def payload_empty? = @payload_offset >= @payload_end
|
|
30
|
+
# @rbs () -> bool
|
|
31
|
+
# Whether an error or truncation stops the protocol chain.
|
|
32
|
+
def error? = @diagnostics.any? { |d| d.severity == :error || d.code == :truncated }
|
|
33
|
+
|
|
34
|
+
# @rbs (Symbol severity, Symbol code, ?String message, ?String? field) -> Layer
|
|
35
|
+
# Append a structured diagnostic and return this layer.
|
|
36
|
+
def diagnose(severity, code, message = code.to_s, field = nil)
|
|
37
|
+
@diagnostics << Diagnostic.new(severity, code, message, field)
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @rbs (Symbol key, String name, untyped value, ?type: Symbol, ?offset: Integer, ?length: Integer, ?format: Symbol?, ?enum: untyped) -> Layer
|
|
42
|
+
# Append a dynamic field with its value, representation and location.
|
|
43
|
+
def add(key, name, value, type: :uint, offset: 0, length: 0, format: nil, enum: nil)
|
|
44
|
+
@values[key] = value
|
|
45
|
+
@definitions << FieldDefinition.new(key, name, type, offset, length, 0, nil, nil, 1, format, enum)
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @rbs () -> Array[Field]
|
|
50
|
+
# Return Field objects for all fixed and dynamic definitions.
|
|
51
|
+
def fields = @definitions.map { |d| Field.new(d, @values[d.key], @offset) }
|
|
52
|
+
# @rbs (Symbol key) -> String
|
|
53
|
+
# Return escaped text for a single symbolic field.
|
|
54
|
+
def display(key)
|
|
55
|
+
definition = @definitions.find { |d| d.key == key }
|
|
56
|
+
definition ? Field.new(definition, @values[key], @offset).display : ''
|
|
57
|
+
end
|
|
58
|
+
# @rbs (String name) -> untyped
|
|
59
|
+
# Read the first decoded value with this dotted field name.
|
|
60
|
+
def field_value(name)
|
|
61
|
+
definition = @definitions.find { |d| d.name == name }
|
|
62
|
+
definition ? Field.new(definition, @values[definition.key], @offset).value : nil
|
|
63
|
+
end
|
|
64
|
+
# @rbs (String name) -> Array[untyped]
|
|
65
|
+
# Read the first decoded value with this dotted field name.
|
|
66
|
+
def field_values(name) = fields.select { |f| f.name == name }.map(&:value)
|
|
67
|
+
|
|
68
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
69
|
+
# Return JSON fields, grouping repeated names into arrays.
|
|
70
|
+
def to_h
|
|
71
|
+
{ protocol: @protocol, offset: @offset, length: length, embedded: @embedded,
|
|
72
|
+
fields: fields.group_by(&:name).to_h { |name, group| [name, group.length == 1 ? group.first.json_value : group.map(&:json_value)] } }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Output
|
|
7
|
+
# @api private
|
|
8
|
+
class Fields
|
|
9
|
+
# @rbs (Array[String] names) -> void
|
|
10
|
+
def initialize(names) = @names = names
|
|
11
|
+
# @rbs (Packet packet, untyped io) -> void
|
|
12
|
+
def format(packet, io)
|
|
13
|
+
cells = @names.map do |name|
|
|
14
|
+
packet.layers.flat_map(&:fields).select { |f| f.name == name }.map(&:display).join(',')
|
|
15
|
+
end
|
|
16
|
+
io.write(cells.join("\t") + "\n")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module Redhound
|
|
7
|
+
# @api private
|
|
8
|
+
module Output
|
|
9
|
+
# @api private
|
|
10
|
+
class Follow
|
|
11
|
+
# @rbs (String specification) -> void
|
|
12
|
+
def initialize(specification)
|
|
13
|
+
match = /\Atcp,(ascii|hex|raw),(\d+)\z/.match(specification)
|
|
14
|
+
raise ConfigurationError, 'follow must be tcp,ascii|hex|raw,N' unless match
|
|
15
|
+
@format, @stream_id, @offsets = match[1], match[2].to_i, [0, 0]
|
|
16
|
+
@file = Tempfile.new(['redhound-follow-', '.bin'])
|
|
17
|
+
@file.binmode
|
|
18
|
+
@key = nil # @rbs untyped
|
|
19
|
+
end
|
|
20
|
+
# @rbs (Analysis::Flow flow, Integer direction, String data) -> void
|
|
21
|
+
def write(flow, direction, data)
|
|
22
|
+
return unless flow.id == @stream_id && !data.empty?
|
|
23
|
+
@key ||= flow.key
|
|
24
|
+
@file.write([direction, data.bytesize].pack('CN'))
|
|
25
|
+
@file.write(data)
|
|
26
|
+
end
|
|
27
|
+
# @rbs (untyped io) -> void
|
|
28
|
+
def format(io)
|
|
29
|
+
@file.rewind
|
|
30
|
+
unless @format == 'raw'
|
|
31
|
+
io.puts("TCP stream #{@stream_id} (#{@format})")
|
|
32
|
+
if @key
|
|
33
|
+
io.puts("Node 0: #{@key[1]}:#{@key[2]}")
|
|
34
|
+
io.puts("Node 1: #{@key[3]}:#{@key[4]}")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
while (header = @file.read(5))
|
|
38
|
+
direction, length = header.unpack('CN')
|
|
39
|
+
data = @file.read(length) #: String
|
|
40
|
+
case @format
|
|
41
|
+
when 'raw' then io.write(data)
|
|
42
|
+
when 'ascii'
|
|
43
|
+
io.write(direction.zero? ? '' : "\t")
|
|
44
|
+
io.write(data.gsub(/[^\x20-\x7e\r\n\t]/n) { |byte| Kernel.format('\\x%02x', byte.getbyte(0)) })
|
|
45
|
+
when 'hex'
|
|
46
|
+
data.bytes.each_slice(16) do |bytes|
|
|
47
|
+
io.puts(Kernel.format('%s%08x %s', direction.zero? ? '' : "\t", @offsets[direction], bytes.map { |byte| Kernel.format('%02x', byte) }.join(' ')))
|
|
48
|
+
@offsets[direction] += bytes.size
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
ensure
|
|
53
|
+
original_error = $!
|
|
54
|
+
begin
|
|
55
|
+
close
|
|
56
|
+
rescue StandardError
|
|
57
|
+
raise unless original_error
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
# @rbs () -> void
|
|
61
|
+
def close = @file.close!
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Output
|
|
7
|
+
# @api private
|
|
8
|
+
class Hexdump
|
|
9
|
+
# @rbs (?ascii: bool, ?link_layer: bool) -> void
|
|
10
|
+
def initialize(ascii: false, link_layer: false)
|
|
11
|
+
@ascii, @link_layer = ascii, link_layer
|
|
12
|
+
end
|
|
13
|
+
# @rbs (Packet packet, untyped io) -> void
|
|
14
|
+
def format(packet, io)
|
|
15
|
+
offset = @link_layer ? 0 : (packet.layers.find { |l| %i[ipv4 ipv6 arp].include?(l.protocol) }&.offset || 0)
|
|
16
|
+
data = packet.data.byteslice(offset..) || ''.b
|
|
17
|
+
pos = 0
|
|
18
|
+
while pos < data.bytesize
|
|
19
|
+
chunk = data.byteslice(pos, 16) #: String
|
|
20
|
+
hex = chunk.unpack('H2' * chunk.bytesize).join(' ')
|
|
21
|
+
line = Kernel.format(' 0x%04x: %-47s', pos, hex)
|
|
22
|
+
line += ' ' + chunk.gsub(/[^\x20-\x7e]/n, '.') if @ascii
|
|
23
|
+
io.write(line.rstrip + "\n")
|
|
24
|
+
pos += 16
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Redhound
|
|
7
|
+
# @api private
|
|
8
|
+
module Output
|
|
9
|
+
# @api private
|
|
10
|
+
class Json
|
|
11
|
+
# @rbs (?ndjson: bool) -> void
|
|
12
|
+
def initialize(ndjson: false)
|
|
13
|
+
@ndjson, @started = ndjson, false
|
|
14
|
+
end
|
|
15
|
+
# @rbs (Packet packet, untyped io) -> void
|
|
16
|
+
def format(packet, io)
|
|
17
|
+
if @ndjson
|
|
18
|
+
io.write(JSON.generate(packet.to_h) + "\n")
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
io.write(@started ? ",\n" : "[\n")
|
|
22
|
+
io.write(JSON.generate(packet.to_h))
|
|
23
|
+
@started = true
|
|
24
|
+
end
|
|
25
|
+
# @rbs (untyped io) -> void
|
|
26
|
+
def finish(io)
|
|
27
|
+
return if @ndjson
|
|
28
|
+
io.write(@started ? "\n]\n" : "[]\n")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Output
|
|
7
|
+
# @api private
|
|
8
|
+
class Summary
|
|
9
|
+
# @rbs (?timestamp: Symbol, ?precision: Symbol, ?link_layer: bool, ?quick: bool, ?verbosity: Integer, ?resolve_names: bool) -> void
|
|
10
|
+
def initialize(timestamp: :clock, precision: :micro, link_layer: false, quick: false, verbosity: 0, resolve_names: false)
|
|
11
|
+
@timestamp = Timestamp.new(style: timestamp, precision: precision)
|
|
12
|
+
@link_layer, @quick, @verbosity, @resolve_names = link_layer, quick, verbosity, resolve_names
|
|
13
|
+
@names = {} #: Hash[String, String]
|
|
14
|
+
end
|
|
15
|
+
# @rbs (Packet packet) -> String
|
|
16
|
+
def line(packet)
|
|
17
|
+
parts = [@timestamp.format(packet)]
|
|
18
|
+
if packet.interface
|
|
19
|
+
parts << [packet.interface.name, packet.direction&.to_s&.capitalize].compact.join(' ')
|
|
20
|
+
end
|
|
21
|
+
if @link_layer && (eth = packet[:eth])
|
|
22
|
+
parts << "#{eth.display(:src)} > #{eth.display(:dst)}, ethertype #{eth.display(:type)}, length #{packet.original_length}:"
|
|
23
|
+
end
|
|
24
|
+
network = nil #: Layer?
|
|
25
|
+
transport = nil #: Layer?
|
|
26
|
+
layer = nil #: Layer?
|
|
27
|
+
packet.layers.each do |current|
|
|
28
|
+
next if current.embedded
|
|
29
|
+
case current.protocol
|
|
30
|
+
when :ipv4, :ipv6
|
|
31
|
+
network, transport, layer = current, nil, nil
|
|
32
|
+
when :tcp, :udp
|
|
33
|
+
transport = layer = current
|
|
34
|
+
when :data, :raw, :eth, :vlan, :ipv6_ext, :sll, :sll2, :null
|
|
35
|
+
else layer = current
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
if network
|
|
39
|
+
src, dst = address(network.display(:src)), address(network.display(:dst))
|
|
40
|
+
src += ".#{transport[:srcport]}" if transport
|
|
41
|
+
dst += ".#{transport[:dstport]}" if transport
|
|
42
|
+
parts << "#{network.protocol == :ipv4 ? 'IP' : 'IP6'} #{src} > #{dst}:"
|
|
43
|
+
end
|
|
44
|
+
layer ||= packet.layers.last
|
|
45
|
+
klass = layer && Registry.default.protocols[layer.protocol]
|
|
46
|
+
parts << if @quick && transport
|
|
47
|
+
"#{transport.protocol.to_s.upcase}, length #{[transport.payload_end - transport.payload_offset, 0].max}"
|
|
48
|
+
elsif klass && layer && !layer.error?
|
|
49
|
+
klass.new.summary(layer)
|
|
50
|
+
else
|
|
51
|
+
"#{layer&.protocol.to_s.upcase}, length #{packet.caplen}"
|
|
52
|
+
end
|
|
53
|
+
if @verbosity.positive? && network
|
|
54
|
+
parts << "ttl #{network[:ttl] || network[:hlim]}"
|
|
55
|
+
parts << "id #{network.display(:id)}, length #{network[:len]}" if network.protocol == :ipv4
|
|
56
|
+
parts << "checksum #{network.display(:checksum)}" if network[:checksum]
|
|
57
|
+
if transport
|
|
58
|
+
parts << "#{transport.protocol} checksum #{transport.display(:checksum)}"
|
|
59
|
+
options = transport.fields.select { |field| field.name.start_with?('tcp.options.') }
|
|
60
|
+
unless options.empty?
|
|
61
|
+
parts << 'options [' + options.map { |field| "#{field.name.delete_prefix('tcp.options.')} #{field.display}" }.join(', ') + ']'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
diagnostics = packet.layers.flat_map(&:diagnostics)
|
|
66
|
+
parts << diagnostics.map { |d| "[#{d.code}]" }.join(' ') unless diagnostics.empty?
|
|
67
|
+
# All protocol summaries and interface names pass this terminal boundary.
|
|
68
|
+
text = parts.reject(&:empty?).join(' ').b
|
|
69
|
+
text.match?(/[^\x20-\x7e]/n) ? text.gsub(/[^\x20-\x7e]/n) { |c| Kernel.format('\\x%02x', c.getbyte(0)) } : text
|
|
70
|
+
end
|
|
71
|
+
# @rbs (Packet packet, untyped io) -> void
|
|
72
|
+
def format(packet, io) = io.write(line(packet) + "\n")
|
|
73
|
+
# @rbs (String ip) -> String
|
|
74
|
+
def address(ip)
|
|
75
|
+
return ip unless @resolve_names
|
|
76
|
+
return @names[ip] if @names.key?(ip)
|
|
77
|
+
@names.shift if @names.length >= 256
|
|
78
|
+
@names[ip] = Addrinfo.ip(ip).getnameinfo.first
|
|
79
|
+
rescue SocketError
|
|
80
|
+
ip
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Output
|
|
7
|
+
# @api private
|
|
8
|
+
class Timestamp
|
|
9
|
+
# @rbs (?style: Symbol, ?precision: Symbol) -> void
|
|
10
|
+
def initialize(style: :clock, precision: :micro)
|
|
11
|
+
@style, @precision, @first, @previous = style, precision, nil, nil
|
|
12
|
+
end
|
|
13
|
+
# @rbs (Packet packet) -> String
|
|
14
|
+
def format(packet)
|
|
15
|
+
ns = packet.timestamp_ns
|
|
16
|
+
@first ||= ns
|
|
17
|
+
value = case @style
|
|
18
|
+
when :none then ''
|
|
19
|
+
when :epoch then seconds(ns)
|
|
20
|
+
when :delta then seconds(@previous ? ns - @previous : 0)
|
|
21
|
+
when :elapsed then seconds(ns - @first)
|
|
22
|
+
when :date then packet.time.strftime('%Y-%m-%d %H:%M:%S.') + fraction(ns)
|
|
23
|
+
else packet.time.strftime('%H:%M:%S.') + fraction(ns)
|
|
24
|
+
end
|
|
25
|
+
@previous = ns
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
# @rbs (Integer ns) -> String
|
|
29
|
+
def fraction(ns) = Kernel.format(@precision == :nano ? '%09d' : '%06d', @precision == :nano ? ns % 1_000_000_000 : (ns % 1_000_000_000) / 1000)
|
|
30
|
+
# @rbs (Integer ns) -> String
|
|
31
|
+
def seconds(ns) = "#{ns.negative? ? '-' : ''}#{ns.abs / 1_000_000_000}.#{fraction(ns.abs)}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Output
|
|
7
|
+
# @api private
|
|
8
|
+
class Tree
|
|
9
|
+
# @rbs (Packet packet, untyped io) -> void
|
|
10
|
+
def format(packet, io)
|
|
11
|
+
interface = packet.interface ? safe([packet.interface.name, packet.direction&.to_s&.capitalize].compact.join(' ')) + ', ' : ''
|
|
12
|
+
io.write("Frame #{packet.number}: #{packet.original_length} bytes on wire, #{packet.caplen} captured, #{interface}#{packet.time.utc.strftime('%Y-%m-%d %H:%M:%S')}.#{Kernel.format('%09d', packet.time.nsec)}\n")
|
|
13
|
+
packet.layers.each_with_index do |layer, depth|
|
|
14
|
+
klass = Registry.default.protocols[layer.protocol]
|
|
15
|
+
indent = ' ' * depth
|
|
16
|
+
io.write("#{indent}└─ #{klass&.protocol_name || layer.protocol}#{layer.embedded ? ' (embedded)' : ''}\n")
|
|
17
|
+
layer.fields.each { |field| io.write("#{indent} #{field.name}: #{field.display}\n") }
|
|
18
|
+
layer.diagnostics.each { |d| io.write("#{indent} [#{d.severity}] #{d.code}: #{safe(d.message)}\n") }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
# @rbs (String text) -> String
|
|
22
|
+
def safe(text) = text.b.gsub(/[^\x20-\x7e]/n) { |c| Kernel.format('\\x%02x', c.getbyte(0)) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# Captured immutable bytes, metadata, and lazily decoded protocol layers.
|
|
6
|
+
class Packet
|
|
7
|
+
# Capture bytes and metadata; timestamp_ns and lengths preserve kernel/file values.
|
|
8
|
+
attr_reader :data, :timestamp_ns, :original_length, :linktype, :interface, :direction, :number, :meta
|
|
9
|
+
# Override the dissection engine before accessing layers.
|
|
10
|
+
attr_accessor :engine
|
|
11
|
+
# @rbs (String data, ?timestamp_ns: Integer, ?original_length: Integer?, ?linktype: (Integer | Symbol), ?interface: untyped, ?direction: Symbol?, ?number: Integer, ?meta: Hash[Symbol, untyped]) -> void
|
|
12
|
+
# Copy binary data and preserve capture metadata; reject impossible original lengths.
|
|
13
|
+
def initialize(data, timestamp_ns: 0, original_length: nil, linktype: 1, interface: nil, direction: nil, number: 1, meta: {})
|
|
14
|
+
@data = data.b.freeze
|
|
15
|
+
raise ArgumentError, 'timestamp_ns must be an Integer' unless timestamp_ns.is_a?(Integer)
|
|
16
|
+
raise ArgumentError, 'number must be a positive Integer' unless number.is_a?(Integer) && number.positive?
|
|
17
|
+
raise ArgumentError, 'direction must be in, out or nil' unless direction.nil? || direction == :in || direction == :out
|
|
18
|
+
@timestamp_ns, @original_length = timestamp_ns, original_length.nil? ? data.bytesize : original_length
|
|
19
|
+
raise ArgumentError, 'original length must be an Integer' unless @original_length.is_a?(Integer)
|
|
20
|
+
raise ArgumentError, 'original length is shorter than captured length' if @original_length < data.bytesize
|
|
21
|
+
@linktype = Capture::Linktype.resolve(linktype)
|
|
22
|
+
@interface, @direction, @number, @meta = interface, direction, number, meta.dup
|
|
23
|
+
@engine = nil # @rbs Engine?
|
|
24
|
+
end
|
|
25
|
+
# @rbs () -> Integer
|
|
26
|
+
# Number of captured bytes.
|
|
27
|
+
def caplen = @data.bytesize
|
|
28
|
+
# @rbs () -> bool
|
|
29
|
+
# Whether capture omitted bytes from the original packet.
|
|
30
|
+
def truncated? = caplen < @original_length
|
|
31
|
+
# @rbs () -> Time
|
|
32
|
+
# Capture timestamp as a nanosecond-precision Time.
|
|
33
|
+
def time = Time.at(@timestamp_ns / 1_000_000_000, @timestamp_ns % 1_000_000_000, :nsec)
|
|
34
|
+
# @rbs () -> Array[Layer]
|
|
35
|
+
# Decode the protocol chain once and return its Layers.
|
|
36
|
+
def layers = (@layers ||= (@engine || Engine.default).dissect(self))
|
|
37
|
+
# @rbs ((Symbol | String) key) -> untyped
|
|
38
|
+
# Select the first layer by symbol or decoded field by dotted name.
|
|
39
|
+
def [](key)
|
|
40
|
+
return layers.find { |l| l.protocol == key } if key.is_a?(Symbol)
|
|
41
|
+
layers.each do |l|
|
|
42
|
+
val = l.field_value(key)
|
|
43
|
+
return val unless val.nil?
|
|
44
|
+
end
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
# @rbs (Symbol protocol) -> Array[Layer]
|
|
48
|
+
# Decode the protocol chain once and return its Layers.
|
|
49
|
+
def layers_of(protocol) = layers.select { |l| l.protocol == protocol }
|
|
50
|
+
# @rbs (Symbol protocol) -> Layer?
|
|
51
|
+
# Return the last matching layer in the protocol chain.
|
|
52
|
+
def innermost(protocol) = layers_of(protocol).last
|
|
53
|
+
# @rbs (String name) -> Array[untyped]
|
|
54
|
+
# Return every decoded value of a repeated field.
|
|
55
|
+
def field_values(name) = layers.flat_map { |l| l.field_values(name) }
|
|
56
|
+
# @rbs () -> String
|
|
57
|
+
# Format a numeric one-line packet summary.
|
|
58
|
+
def summary = Output::Summary.new.line(self)
|
|
59
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
60
|
+
# Return a JSON-compatible packet matching docs/json-schema.json.
|
|
61
|
+
def to_h
|
|
62
|
+
{ frame: { number: @number, time_epoch_ns: @timestamp_ns, time: time.utc.strftime('%Y-%m-%dT%H:%M:%S.') + format('%09dZ', time.nsec),
|
|
63
|
+
caplen: caplen, len: @original_length, interface: json_text(@interface&.name), direction: @direction, linktype: @linktype },
|
|
64
|
+
layers: layers.map(&:to_h), diagnostics: layers.flat_map { |l| l.diagnostics.map { |d| { layer: l.protocol, severity: d.severity, code: d.code, message: json_text(d.message), field: json_text(d.field) } } } }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# @rbs (String? text) -> String?
|
|
70
|
+
def json_text(text)
|
|
71
|
+
return nil unless text
|
|
72
|
+
utf8 = text.dup.force_encoding(Encoding::UTF_8)
|
|
73
|
+
return utf8 if utf8.valid_encoding?
|
|
74
|
+
text.unpack1('H*') #: String
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Arp < Dissector
|
|
9
|
+
protocol :arp, name: 'Address Resolution Protocol', short: 'ARP'
|
|
10
|
+
dissects_on 'ethertype', 0x0806
|
|
11
|
+
header do
|
|
12
|
+
uint16 :hwtype, 'arp.hw.type'
|
|
13
|
+
uint16 :protype, 'arp.proto.type'
|
|
14
|
+
uint8 :hwlen, 'arp.hw.size'
|
|
15
|
+
uint8 :prolen, 'arp.proto.size'
|
|
16
|
+
uint16 :opcode, 'arp.opcode'
|
|
17
|
+
end
|
|
18
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
19
|
+
def dissect(ctx, layer)
|
|
20
|
+
hlen, plen = layer[:hwlen], layer[:prolen]
|
|
21
|
+
cursor = ctx.cursor
|
|
22
|
+
cursor.check(8, (hlen + plen) * 2)
|
|
23
|
+
if hlen == 6 && plen == 4 && layer[:protype] == 0x0800
|
|
24
|
+
layer.add(:sha, 'arp.src.hw_mac', cursor.bytes(8, 6), type: :mac, offset: 8, length: 6)
|
|
25
|
+
layer.add(:spa, 'arp.src.proto_ipv4', cursor.u32(14), type: :ipv4, offset: 14, length: 4)
|
|
26
|
+
layer.add(:tha, 'arp.dst.hw_mac', cursor.bytes(18, 6), type: :mac, offset: 18, length: 6)
|
|
27
|
+
layer.add(:tpa, 'arp.dst.proto_ipv4', cursor.u32(24), type: :ipv4, offset: 24, length: 4)
|
|
28
|
+
end
|
|
29
|
+
layer.header_length = 8 + 2 * (hlen + plen)
|
|
30
|
+
layer.payload_end = layer.payload_offset = layer.offset + layer.header_length
|
|
31
|
+
end
|
|
32
|
+
# @rbs (Layer layer) -> String
|
|
33
|
+
def summary(layer)
|
|
34
|
+
layer[:opcode] == 1 ? "ARP, Request who-has #{layer.display(:tpa)} tell #{layer.display(:spa)}" :
|
|
35
|
+
"ARP, Reply #{layer.display(:spa)} is-at #{layer.display(:sha)}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
module Checksum
|
|
9
|
+
# @rbs (String bytes) -> Integer
|
|
10
|
+
def self.value(bytes)
|
|
11
|
+
sum = bytes.unpack('n*').sum
|
|
12
|
+
sum += bytes.getbyte(-1).to_i << 8 if bytes.bytesize.odd?
|
|
13
|
+
sum = (sum & 0xffff) + (sum >> 16) while sum > 0xffff
|
|
14
|
+
(~sum) & 0xffff
|
|
15
|
+
end
|
|
16
|
+
# @rbs (Context ctx, Layer layer, String bytes, ?Integer? proto) -> void
|
|
17
|
+
def self.verify(ctx, layer, bytes, proto = nil)
|
|
18
|
+
return unless ctx.verify_checksums?
|
|
19
|
+
network = ctx.network
|
|
20
|
+
fragmented = network&.[](:mf) == 1 || ctx.layers.any? { |part| part.protocol == :ipv6_ext && part[:more] == 1 }
|
|
21
|
+
if ctx.packet.direction == :out || ctx.packet.meta[:csum_not_ready] || ctx.embedded || fragmented
|
|
22
|
+
layer.diagnose(:note, :checksum_unverified, 'checksum not verified (offloaded or incomplete)')
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
if proto && network
|
|
26
|
+
src, dst = network[:src], network[:dst]
|
|
27
|
+
pseudo = if network.protocol == :ipv4
|
|
28
|
+
[src, dst, 0, proto, bytes.bytesize].pack('NNCCn')
|
|
29
|
+
else
|
|
30
|
+
src + dst + [bytes.bytesize, proto].pack('Nx3C')
|
|
31
|
+
end
|
|
32
|
+
bytes = pseudo + bytes
|
|
33
|
+
end
|
|
34
|
+
layer.diagnose(:warn, :bad_checksum, 'checksum does not match') unless value(bytes).zero?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Data < Dissector
|
|
9
|
+
LENGTH_FIELD = FieldDefinition.new(:length, 'data.len', :uint, 0, 0, 0, nil, nil, 1, nil, nil)
|
|
10
|
+
protocol :data, name: 'Data', short: 'DATA'
|
|
11
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
12
|
+
def dissect(ctx, layer)
|
|
13
|
+
length = ctx.cursor.remaining
|
|
14
|
+
layer.definitions << LENGTH_FIELD
|
|
15
|
+
layer.values[:length] = length
|
|
16
|
+
layer.add(:data, 'data.data', ctx.cursor.bytes(0, length), type: :bytes, length: length)
|
|
17
|
+
layer.payload_offset = layer.payload_end
|
|
18
|
+
end
|
|
19
|
+
# @rbs (Layer layer) -> String
|
|
20
|
+
def summary(layer) = "DATA, length #{layer[:length]}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|