redhound 1.0.0 → 2.0.0.rc1
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 +35 -0
- data/README.md +97 -31
- data/docs/API.md +76 -0
- data/docs/FILTERS.md +37 -0
- data/docs/MIGRATION.md +23 -0
- data/docs/PROTOCOLS.md +45 -0
- data/docs/USAGE.md +82 -0
- data/docs/VALIDATION.md +59 -0
- data/docs/json-schema.json +64 -0
- data/exe/redhound +2 -1
- data/lib/redhound/analysis/flow_key.rb +53 -0
- data/lib/redhound/analysis/flow_table.rb +117 -0
- data/lib/redhound/analysis/ip_reassembler.rb +176 -0
- data/lib/redhound/analysis/stats.rb +138 -0
- data/lib/redhound/analysis/tcp_analysis.rb +43 -0
- data/lib/redhound/analysis/tcp_reassembler.rb +124 -0
- data/lib/redhound/analysis/tcp_stream.rb +151 -0
- data/lib/redhound/analysis.rb +129 -0
- data/lib/redhound/capture/bsd/bpf_device.rb +152 -0
- data/lib/redhound/capture/bsd/constants.rb +29 -0
- data/lib/redhound/capture/file_source.rb +83 -0
- data/lib/redhound/capture/interface.rb +66 -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 +180 -0
- data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
- data/lib/redhound/capture/linux/tpacket_v3.rb +132 -0
- data/lib/redhound/capture/source.rb +79 -0
- data/lib/redhound/capture/stats.rb +22 -0
- data/lib/redhound/capture.rb +69 -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 +138 -0
- data/lib/redhound/engine.rb +66 -0
- data/lib/redhound/errors.rb +38 -0
- data/lib/redhound/field.rb +62 -0
- data/lib/redhound/file/format.rb +56 -0
- data/lib/redhound/file/pcap_reader.rb +43 -0
- data/lib/redhound/file/pcap_writer.rb +72 -0
- data/lib/redhound/file/pcapng_reader.rb +209 -0
- data/lib/redhound/file/pcapng_writer.rb +143 -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 +108 -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 +59 -0
- data/lib/redhound/output/hexdump.rb +29 -0
- data/lib/redhound/output/json.rb +32 -0
- data/lib/redhound/output/summary.rb +59 -0
- data/lib/redhound/output/timestamp.rb +34 -0
- data/lib/redhound/output/tree.rb +24 -0
- data/lib/redhound/packet.rb +63 -0
- data/lib/redhound/protocols/arp.rb +39 -0
- data/lib/redhound/protocols/checksum.rb +38 -0
- data/lib/redhound/protocols/data.rb +21 -0
- data/lib/redhound/protocols/dhcp.rb +124 -0
- data/lib/redhound/protocols/dns.rb +268 -0
- data/lib/redhound/protocols/ethernet.rb +24 -0
- data/lib/redhound/protocols/http.rb +183 -0
- data/lib/redhound/protocols/icmp.rb +191 -0
- data/lib/redhound/protocols/ipv4.rb +56 -0
- data/lib/redhound/protocols/ipv6.rb +65 -0
- data/lib/redhound/protocols/link.rb +70 -0
- data/lib/redhound/protocols/llc.rb +37 -0
- data/lib/redhound/protocols/ntp.rb +47 -0
- data/lib/redhound/protocols/tcp.rb +80 -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 +98 -0
- data/sig/generated/redhound/analysis/ip_reassembler.rbs +40 -0
- data/sig/generated/redhound/analysis/stats.rbs +77 -0
- data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
- data/sig/generated/redhound/analysis/tcp_reassembler.rbs +27 -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/file_source.rbs +34 -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 +57 -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 +113 -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 +33 -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 +26 -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 +77 -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 +15 -0
- data/sig/generated/redhound/protocols/dhcp.rbs +28 -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 +175 -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 -8
- 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,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Redhound
|
|
7
|
+
# @api private
|
|
8
|
+
module Filter
|
|
9
|
+
# @api private
|
|
10
|
+
class Analyzer
|
|
11
|
+
PROTOCOL_NUMBERS = { 'icmp' => 1, 'igmp' => 2, 'tcp' => 6, 'udp' => 17, 'ipv6' => 41,
|
|
12
|
+
'ipv6-frag' => 44, 'esp' => 50, 'ah' => 51, 'icmp6' => 58,
|
|
13
|
+
'ipv6-icmp' => 58, 'ospf' => 89, 'sctp' => 132 }.freeze
|
|
14
|
+
ETHER_TYPES = { 'ip' => 0x0800, 'ip6' => 0x86dd, 'arp' => 0x0806, 'rarp' => 0x8035,
|
|
15
|
+
'vlan' => 0x8100 }.freeze
|
|
16
|
+
|
|
17
|
+
# @rbs (String expression) -> void
|
|
18
|
+
def initialize(expression)
|
|
19
|
+
@expression = expression
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs (untyped node) -> untyped
|
|
23
|
+
def analyze(node)
|
|
24
|
+
case node[0]
|
|
25
|
+
when :and, :or then [node[0], analyze(node[1]), analyze(node[2])]
|
|
26
|
+
when :not then [:not, analyze(node[1])]
|
|
27
|
+
when :primitive then primitive(node)
|
|
28
|
+
when :relation then [:relation, arithmetic(node[1]), node[2], arithmetic(node[3])]
|
|
29
|
+
else node
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# @rbs (untyped node) -> untyped
|
|
36
|
+
def primitive(node)
|
|
37
|
+
_, proto, direction, type, value, position = node
|
|
38
|
+
error('unsupported qualifier combination', position) if %w[tcp udp sctp].include?(proto) && !%i[port portrange protocol].include?(type)
|
|
39
|
+
case type
|
|
40
|
+
when :port, :portrange
|
|
41
|
+
error('ports require TCP, UDP or SCTP', position) if proto && !%w[tcp udp sctp].include?(proto)
|
|
42
|
+
value = ports(value, proto, type, position)
|
|
43
|
+
when :host, :net
|
|
44
|
+
if proto == 'ether'
|
|
45
|
+
error('Ethernet network qualifiers are unsupported', position) if type == :net
|
|
46
|
+
error('expected a MAC address', position) unless /\A(?:[\da-fA-F]{1,2}:){5}[\da-fA-F]{1,2}\z/.match?(value)
|
|
47
|
+
value = [value.split(':').map { |octet| octet.to_i(16) }.pack('C6')]
|
|
48
|
+
else
|
|
49
|
+
error('host qualifier requires ip, ip6, arp or rarp', position) if proto && !%w[ip ip6 arp rarp].include?(proto)
|
|
50
|
+
value = addresses(value, type, position)
|
|
51
|
+
value.select! { |family, _, _| proto == 'ip6' ? family == 6 : family == 4 } if proto
|
|
52
|
+
error('address family does not match qualifier', position) if value.empty?
|
|
53
|
+
end
|
|
54
|
+
when :proto
|
|
55
|
+
error('protocol qualifier cannot have a direction', position) unless direction == :either
|
|
56
|
+
error('proto requires ip, ip6 or ether', position) if proto && !%w[ip ip6 ether].include?(proto)
|
|
57
|
+
numbers = proto == 'ether' ? ETHER_TYPES : protocol_numbers
|
|
58
|
+
value = integer_or_name(value, numbers, position)
|
|
59
|
+
error('protocol number is out of range', position) unless value.between?(0, proto == 'ether' ? 65535 : 255)
|
|
60
|
+
when :vlan
|
|
61
|
+
error('VLAN ID is out of range', position) if value && !value.between?(0, 4095)
|
|
62
|
+
when :greater, :less
|
|
63
|
+
error('length is out of range', position) unless value.between?(0, 0xffffffff)
|
|
64
|
+
when :broadcast, :multicast
|
|
65
|
+
error('invalid broadcast/multicast protocol', position) if proto && !%w[ether ip ip6].include?(proto)
|
|
66
|
+
error('IPv6 has no broadcast address', position) if type == :broadcast && proto == 'ip6'
|
|
67
|
+
end
|
|
68
|
+
[:primitive, proto, direction, type, value, position]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @rbs (untyped node) -> untyped
|
|
72
|
+
def arithmetic(node)
|
|
73
|
+
case node[0]
|
|
74
|
+
when :number
|
|
75
|
+
error('arithmetic constant is out of range', 0) unless node[1].between?(0, 0xffffffff)
|
|
76
|
+
when :binary
|
|
77
|
+
node = [:binary, node[1], arithmetic(node[2]), arithmetic(node[3])]
|
|
78
|
+
if %w[/ %].include?(node[1]) && node[3] == [:number, 0]
|
|
79
|
+
error('division by zero', 0)
|
|
80
|
+
end
|
|
81
|
+
if %w[<< >>].include?(node[1]) && node[3][0] == :number && node[3][1] > 31
|
|
82
|
+
error('shift count is out of range', 0)
|
|
83
|
+
end
|
|
84
|
+
when :neg then node = [:neg, arithmetic(node[1])]
|
|
85
|
+
when :access then node = [:access, node[1], arithmetic(node[2]), node[3]]
|
|
86
|
+
end
|
|
87
|
+
node
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @rbs (String value, String? proto, Symbol type, Integer position) -> untyped
|
|
91
|
+
def ports(value, proto, type, position)
|
|
92
|
+
values = type == :port ? [value, value] : value.split('-', 2)
|
|
93
|
+
error('portrange requires two port values', position) unless values.size == 2
|
|
94
|
+
result = {} #: untyped
|
|
95
|
+
candidates = proto ? [proto] : %w[tcp udp]
|
|
96
|
+
candidates.each do |protocol|
|
|
97
|
+
numbers = values.map do |part|
|
|
98
|
+
if /\A(?:0x[\da-fA-F]+|\d+)\z/.match?(part)
|
|
99
|
+
integer_or_name(part, {}, position)
|
|
100
|
+
else
|
|
101
|
+
error('expected a service name or port number', position) unless /\A[a-zA-Z][a-zA-Z0-9_-]*\z/.match?(part)
|
|
102
|
+
begin
|
|
103
|
+
Socket.getservbyname(part, protocol)
|
|
104
|
+
rescue SocketError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
next if numbers.any?(&:nil?)
|
|
110
|
+
numbers = numbers.compact
|
|
111
|
+
error('port number is out of range', position) unless numbers.all? { |port| port.between?(0, 65535) }
|
|
112
|
+
result[PROTOCOL_NUMBERS.fetch(protocol)] = numbers.minmax
|
|
113
|
+
end
|
|
114
|
+
error('unknown service name', position) if result.empty?
|
|
115
|
+
if !proto && result.values.uniq.size == 1
|
|
116
|
+
result[132] = result.values.first
|
|
117
|
+
end
|
|
118
|
+
result
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @rbs (String value, Symbol type, Integer position) -> untyped
|
|
122
|
+
def addresses(value, type, position)
|
|
123
|
+
address_parts = value.split(' mask ', 2)
|
|
124
|
+
address = address_parts.fetch(0)
|
|
125
|
+
mask = address_parts[1]
|
|
126
|
+
if /\A(?:\d+\.){0,3}\d+(?:\/\d+)?\z/.match?(address) || address.start_with?('0x')
|
|
127
|
+
cidr_parts = address.split('/', 2)
|
|
128
|
+
literal = cidr_parts.fetch(0)
|
|
129
|
+
cidr = cidr_parts[1]
|
|
130
|
+
octets = literal.split('.')
|
|
131
|
+
if octets.size == 1
|
|
132
|
+
number = integer_or_name(literal, {}, position)
|
|
133
|
+
error('IPv4 address is out of range', position) unless number.between?(0, 0xffffffff)
|
|
134
|
+
if type == :net && number <= 255
|
|
135
|
+
number <<= 24
|
|
136
|
+
prefix = 8
|
|
137
|
+
else
|
|
138
|
+
prefix = 32
|
|
139
|
+
end
|
|
140
|
+
ip = IPAddr.new([number].pack('N').unpack('C4').join('.'))
|
|
141
|
+
else
|
|
142
|
+
error('IPv4 octet is out of range', position) unless octets.all? { |part| part.to_i <= 255 }
|
|
143
|
+
prefix = octets.size * 8
|
|
144
|
+
ip = IPAddr.new((octets + Array.new(4 - octets.size, '0')).join('.'))
|
|
145
|
+
end
|
|
146
|
+
prefix = cidr.to_i if cidr
|
|
147
|
+
error('CIDR requires a network qualifier', position) if type == :host && cidr && prefix != 32
|
|
148
|
+
bitmask = mask ? IPAddr.new(mask).to_i : (0xffffffff << (32 - prefix)) & 0xffffffff
|
|
149
|
+
error('invalid IPv4 netmask', position) unless prefix.between?(0, 32) && ((~bitmask & 0xffffffff) & ((~bitmask & 0xffffffff) + 1)).zero?
|
|
150
|
+
error('non-network bits set in network address', position) if type == :net && (ip.to_i & bitmask) != ip.to_i
|
|
151
|
+
return [[4, ip.to_i & bitmask, bitmask]]
|
|
152
|
+
end
|
|
153
|
+
if address.include?(':')
|
|
154
|
+
ip = IPAddr.new(address)
|
|
155
|
+
error('expected IPv6 address', position) unless ip.ipv6?
|
|
156
|
+
prefix = address.include?('/') ? address.split('/').last.to_i : 128
|
|
157
|
+
error('CIDR requires a network qualifier', position) if type == :host && prefix != 128
|
|
158
|
+
return [[6, ip.to_i, ((1 << 128) - 1) ^ ((1 << (128 - prefix)) - 1)]]
|
|
159
|
+
end
|
|
160
|
+
error('network names must be numerical', position) if type == :net
|
|
161
|
+
Addrinfo.getaddrinfo(address, nil, nil, Socket::SOCK_DGRAM).map do |info|
|
|
162
|
+
ip = IPAddr.new(info.ip_address)
|
|
163
|
+
[ip.ipv4? ? 4 : 6, ip.to_i, ip.ipv4? ? 0xffffffff : ((1 << 128) - 1)]
|
|
164
|
+
end.uniq
|
|
165
|
+
rescue IPAddr::Error, SocketError, ArgumentError => e
|
|
166
|
+
error("invalid address #{value.inspect}: #{e.message}", position)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @rbs (String value, Hash[String, Integer] names, Integer position) -> Integer
|
|
170
|
+
def integer_or_name(value, names, position)
|
|
171
|
+
value = value.delete_prefix('\\')
|
|
172
|
+
return names.fetch(value.downcase) if names.key?(value.downcase)
|
|
173
|
+
Integer(value, value.start_with?('0') && value.size > 1 ? 0 : 10)
|
|
174
|
+
rescue ArgumentError
|
|
175
|
+
error("unknown numeric value or name #{value.inspect}", position)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @rbs () -> Hash[String, Integer]
|
|
179
|
+
def protocol_numbers
|
|
180
|
+
names = PROTOCOL_NUMBERS.dup
|
|
181
|
+
if ::File.readable?('/etc/protocols')
|
|
182
|
+
::File.foreach('/etc/protocols') do |line|
|
|
183
|
+
tokens = line.split('#', 2).first.to_s.split
|
|
184
|
+
next unless tokens[1]&.match?(/\A\d+\z/)
|
|
185
|
+
([tokens[0]] + tokens.drop(2)).each { |name| names[name] = tokens[1].to_i }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
names
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @rbs (String message, Integer position) -> bot
|
|
192
|
+
def error(message, position)
|
|
193
|
+
raise FilterSyntaxError.new(message, expression: @expression, position: position)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Filter
|
|
7
|
+
# @api private
|
|
8
|
+
module BPF
|
|
9
|
+
# @api private
|
|
10
|
+
class Assembler
|
|
11
|
+
# @rbs () -> void
|
|
12
|
+
def initialize
|
|
13
|
+
@items = [] #: untyped
|
|
14
|
+
@sequence = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs () -> Symbol
|
|
18
|
+
def label
|
|
19
|
+
@sequence += 1
|
|
20
|
+
"label_#{@sequence}".to_sym
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs (Symbol label) -> void
|
|
24
|
+
def mark(label) = @items << [:label, label]
|
|
25
|
+
|
|
26
|
+
# @rbs (Integer code, ?Integer k) -> void
|
|
27
|
+
def emit(code, k = 0) = @items << [code, 0, 0, k & 0xffffffff]
|
|
28
|
+
|
|
29
|
+
# @rbs (Symbol target) -> void
|
|
30
|
+
def jump(target) = @items << [:jump, target]
|
|
31
|
+
|
|
32
|
+
# @rbs (Integer code, Integer k, Symbol yes, Symbol no) -> void
|
|
33
|
+
def branch(code, k, yes, no) = @items << [:branch, code, k & 0xffffffff, yes, no]
|
|
34
|
+
|
|
35
|
+
# @rbs () -> Array[[Integer, Integer, Integer, Integer]]
|
|
36
|
+
def assemble
|
|
37
|
+
compact!
|
|
38
|
+
widths = @items.map { |item| item[0] == :label ? 0 : 1 }
|
|
39
|
+
labels = {} #: untyped
|
|
40
|
+
loop do
|
|
41
|
+
offset = 0
|
|
42
|
+
@items.each_with_index do |item, index|
|
|
43
|
+
labels[item[1]] = offset if item[0] == :label
|
|
44
|
+
offset += widths[index]
|
|
45
|
+
end
|
|
46
|
+
raise FilterTooLarge, 'BPF programs are limited to 4096 instructions' if offset > 4096
|
|
47
|
+
changed = false
|
|
48
|
+
pc = 0
|
|
49
|
+
@items.each_with_index do |item, index|
|
|
50
|
+
if item[0] == :branch && widths[index] == 1 && [item[3], item[4]].any? { |target| labels.fetch(target) - pc - 1 > 255 }
|
|
51
|
+
widths[index] = 3
|
|
52
|
+
changed = true
|
|
53
|
+
end
|
|
54
|
+
pc += widths[index]
|
|
55
|
+
end
|
|
56
|
+
break unless changed
|
|
57
|
+
end
|
|
58
|
+
result = [] #: Array[[Integer, Integer, Integer, Integer]]
|
|
59
|
+
@items.each_with_index do |item, index|
|
|
60
|
+
pc = result.size
|
|
61
|
+
case item[0]
|
|
62
|
+
when :label then next
|
|
63
|
+
when :jump then result << [5, 0, 0, labels.fetch(item[1]) - pc - 1]
|
|
64
|
+
when :branch
|
|
65
|
+
_, code, k, yes, no = item
|
|
66
|
+
if widths[index] == 1
|
|
67
|
+
result << [code, labels.fetch(yes) - pc - 1, labels.fetch(no) - pc - 1, k]
|
|
68
|
+
else
|
|
69
|
+
result << [code, 0, 1, k]
|
|
70
|
+
result << [5, 0, 0, labels.fetch(yes) - pc - 2]
|
|
71
|
+
result << [5, 0, 0, labels.fetch(no) - pc - 3]
|
|
72
|
+
end
|
|
73
|
+
else result << item
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Remove unreachable blocks and thread jumps through unconditional jumps.
|
|
82
|
+
# @rbs () -> void
|
|
83
|
+
def compact!
|
|
84
|
+
labels = {} #: untyped
|
|
85
|
+
@items.each_with_index { |item, index| labels[item[1]] = index if item[0] == :label }
|
|
86
|
+
target = lambda do |name|
|
|
87
|
+
index = labels.fetch(name) + 1
|
|
88
|
+
index += 1 while @items[index]&.first == :label
|
|
89
|
+
@items[index]&.first == :jump ? target.call(@items[index][1]) : name
|
|
90
|
+
end
|
|
91
|
+
@items.each do |item|
|
|
92
|
+
item[1] = target.call(item[1]) if item[0] == :jump
|
|
93
|
+
if item[0] == :branch
|
|
94
|
+
item[3] = target.call(item[3])
|
|
95
|
+
item[4] = target.call(item[4])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
reachable = {} #: Hash[Integer, bool]
|
|
99
|
+
pending = [0]
|
|
100
|
+
until pending.empty?
|
|
101
|
+
index = pending.pop
|
|
102
|
+
next if !index || index >= @items.size || reachable[index]
|
|
103
|
+
reachable[index] = true
|
|
104
|
+
item = @items[index]
|
|
105
|
+
case item[0]
|
|
106
|
+
when :jump then pending << labels.fetch(item[1])
|
|
107
|
+
when :branch then pending.concat([labels.fetch(item[3]), labels.fetch(item[4])])
|
|
108
|
+
when 6, 0x16 then next
|
|
109
|
+
else pending << index + 1
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
@items = @items.each_with_index.filter_map { |item, index| item if reachable[index] || item[0] == :label }
|
|
113
|
+
@items = @items.each_with_index.filter_map do |item, index|
|
|
114
|
+
following = @items[(index + 1)..].take_while { |next_item| next_item[0] == :label }
|
|
115
|
+
item unless item[0] == :jump && following.any? { |label_item| label_item[1] == item[1] }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Filter
|
|
7
|
+
# @api private
|
|
8
|
+
module BPF
|
|
9
|
+
# @api private
|
|
10
|
+
module Disassembler
|
|
11
|
+
# @rbs (Array[[Integer, Integer, Integer, Integer]] instructions, ?format: Symbol) -> String
|
|
12
|
+
def self.disassemble(instructions, format: :text)
|
|
13
|
+
case format
|
|
14
|
+
when :ruby
|
|
15
|
+
"[\n" + instructions.map { |code, jt, jf, k| format(' [0x%04x, %d, %d, 0x%08x],', code, jt, jf, k) }.join("\n") + "\n]\n"
|
|
16
|
+
when :decimal
|
|
17
|
+
"#{instructions.size}\n" + instructions.map { |i| i.join(' ') }.join("\n") + "\n"
|
|
18
|
+
when :text
|
|
19
|
+
instructions.each_with_index.map do |(code, jt, jf, k), index|
|
|
20
|
+
text = instruction(code, k)
|
|
21
|
+
text += " jt #{index + jt + 1} jf #{index + jf + 1}" if Validator::JUMPS.include?(code)
|
|
22
|
+
text = "ja #{index + k + 1}" if code == 5
|
|
23
|
+
format('(%03d) %s', index, text)
|
|
24
|
+
end.join("\n") + "\n"
|
|
25
|
+
else raise ArgumentError, "unknown disassembly format #{format.inspect}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @rbs (Integer code, Integer k) -> String
|
|
30
|
+
def self.instruction(code, k)
|
|
31
|
+
source = (code & 8).zero? ? "#0x#{k.to_s(16)}" : 'x'
|
|
32
|
+
case code & 7
|
|
33
|
+
when 0, 1
|
|
34
|
+
mnemonic = (code & 7).zero? ? 'ld' : 'ldx'
|
|
35
|
+
mnemonic += { 0 => '', 8 => 'h', 16 => 'b' }.fetch(code & 0x18)
|
|
36
|
+
operand = case code & 0xe0
|
|
37
|
+
when 0 then "#0x#{k.to_s(16)}"
|
|
38
|
+
when 0x20 then "[#{k}]"
|
|
39
|
+
when 0x40 then "[x + #{k}]"
|
|
40
|
+
when 0x60 then "M[#{k}]"
|
|
41
|
+
when 0x80 then 'len'
|
|
42
|
+
when 0xa0 then "4*([#{k}]&0xf)"
|
|
43
|
+
end
|
|
44
|
+
"#{mnemonic} #{operand}"
|
|
45
|
+
when 2 then "st M[#{k}]"
|
|
46
|
+
when 3 then "stx M[#{k}]"
|
|
47
|
+
when 4
|
|
48
|
+
"#{ { 0 => 'add', 0x10 => 'sub', 0x20 => 'mul', 0x30 => 'div', 0x40 => 'or', 0x50 => 'and', 0x60 => 'lsh', 0x70 => 'rsh', 0x80 => 'neg', 0x90 => 'mod', 0xa0 => 'xor' }.fetch(code & 0xf0)} #{source}"
|
|
49
|
+
when 5 then "#{ { 0 => 'ja', 0x10 => 'jeq', 0x20 => 'jgt', 0x30 => 'jge', 0x40 => 'jset' }.fetch(code & 0xf0)} #{source}"
|
|
50
|
+
when 6 then "ret #{code == 0x16 ? 'a' : "##{k}"}"
|
|
51
|
+
when 7 then code == 7 ? 'tax' : 'txa'
|
|
52
|
+
else raise FilterError, 'invalid BPF opcode'
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Filter
|
|
7
|
+
# @api private
|
|
8
|
+
module BPF
|
|
9
|
+
# Validates the portable cBPF ISA and Linux VLAN ancillary loads.
|
|
10
|
+
# @api private
|
|
11
|
+
module Validator
|
|
12
|
+
LOADS = [0x00, 0x20, 0x28, 0x30, 0x40, 0x48, 0x50, 0x60, 0x80,
|
|
13
|
+
0x01, 0x61, 0x81, 0xb1].freeze
|
|
14
|
+
ALU = [0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x90, 0xa0].flat_map { |op| [op | 4, op | 12] }.freeze
|
|
15
|
+
JUMPS = [0x15, 0x1d, 0x25, 0x2d, 0x35, 0x3d, 0x45, 0x4d].freeze
|
|
16
|
+
OPCODES = (LOADS + ALU + JUMPS + [0x02, 0x03, 0x05, 0x06, 0x16, 0x07, 0x87, 0x84]).freeze
|
|
17
|
+
|
|
18
|
+
# @rbs (Array[[Integer, Integer, Integer, Integer]] instructions) -> bool
|
|
19
|
+
def self.validate!(instructions)
|
|
20
|
+
raise FilterError, 'BPF instructions must be an Array' unless instructions.is_a?(Array)
|
|
21
|
+
count = instructions.size
|
|
22
|
+
raise FilterTooLarge, 'BPF programs are limited to 4096 instructions' if count > 4096
|
|
23
|
+
raise FilterError, 'BPF program is empty' if count.zero?
|
|
24
|
+
|
|
25
|
+
instructions.each_with_index do |instruction, index|
|
|
26
|
+
unless instruction.is_a?(Array) && instruction.size == 4 && instruction.all? { |field| field.is_a?(Integer) }
|
|
27
|
+
raise FilterError, "invalid BPF instruction #{index}"
|
|
28
|
+
end
|
|
29
|
+
code, jt, jf, k = instruction
|
|
30
|
+
unless OPCODES.include?(code) && jt.between?(0, 255) && jf.between?(0, 255) && k.between?(0, 0xffffffff)
|
|
31
|
+
raise FilterError, "invalid BPF opcode or operand at #{index}"
|
|
32
|
+
end
|
|
33
|
+
raise FilterError, "invalid scratch address at #{index}" if [0x60, 0x61, 2, 3].include?(code) && k > 15
|
|
34
|
+
raise FilterError, "division by zero at #{index}" if [0x34, 0x94].include?(code) && k.zero?
|
|
35
|
+
raise FilterError, "invalid shift at #{index}" if [0x64, 0x74].include?(code) && k > 31
|
|
36
|
+
if [0x20, 0x28, 0x30].include?(code) && k >= 0xfffff000 && (code != 0x20 || ![0xfffff000, 0xfffff02c, 0xfffff030, 0xfffff03c].include?(k))
|
|
37
|
+
raise FilterError, "unsupported ancillary load at #{index}"
|
|
38
|
+
end
|
|
39
|
+
successors(code, jt, jf, k, index).each do |target|
|
|
40
|
+
raise FilterError, "invalid BPF jump at #{index}" unless target > index && target < count
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
raise FilterError, 'BPF program must end with RET' unless [6, 0x16].include?(instructions.last[0])
|
|
44
|
+
validate_memory!(instructions)
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs (Integer code, Integer jt, Integer jf, Integer k, Integer index) -> Array[Integer]
|
|
49
|
+
def self.successors(code, jt, jf, k, index)
|
|
50
|
+
return [] if [6, 0x16].include?(code)
|
|
51
|
+
return [index + 1 + k] if code == 5
|
|
52
|
+
return [index + 1 + jt, index + 1 + jf] if JUMPS.include?(code)
|
|
53
|
+
|
|
54
|
+
[index + 1]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @rbs (Array[[Integer, Integer, Integer, Integer]] instructions) -> void
|
|
58
|
+
def self.validate_memory!(instructions)
|
|
59
|
+
incoming = {} #: Hash[Integer, Integer]
|
|
60
|
+
incoming[0] = 0
|
|
61
|
+
instructions.each_with_index do |(code, jt, jf, k), index|
|
|
62
|
+
initialized = incoming[index]
|
|
63
|
+
next unless initialized
|
|
64
|
+
if [0x60, 0x61].include?(code) && (initialized & (1 << k)).zero?
|
|
65
|
+
raise FilterError, "uninitialized BPF scratch memory at #{index}"
|
|
66
|
+
end
|
|
67
|
+
initialized |= (1 << k) if [2, 3].include?(code)
|
|
68
|
+
successors(code, jt, jf, k, index).each do |target|
|
|
69
|
+
previous = incoming[target]
|
|
70
|
+
incoming[target] = previous ? previous & initialized : initialized
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Filter
|
|
7
|
+
# @api private
|
|
8
|
+
module BPF
|
|
9
|
+
# @api private
|
|
10
|
+
class VM
|
|
11
|
+
# @rbs (Array[[Integer, Integer, Integer, Integer]] instructions) -> void
|
|
12
|
+
def initialize(instructions)
|
|
13
|
+
Validator.validate!(instructions)
|
|
14
|
+
@instructions = instructions
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs (untyped packet) -> Integer
|
|
18
|
+
def evaluate(packet)
|
|
19
|
+
a = 0
|
|
20
|
+
x = 0
|
|
21
|
+
memory = Array.new(16, 0)
|
|
22
|
+
pc = 0
|
|
23
|
+
loop do
|
|
24
|
+
code, jt, jf, k = @instructions.fetch(pc)
|
|
25
|
+
pc += 1
|
|
26
|
+
case code & 7
|
|
27
|
+
when 0, 1
|
|
28
|
+
value = case code & 0xe0
|
|
29
|
+
when 0 then k
|
|
30
|
+
when 0x60 then memory.fetch(k)
|
|
31
|
+
when 0x80 then packet.original_length
|
|
32
|
+
else
|
|
33
|
+
offset = k + ((code & 0xe0) == 0x40 ? x : 0)
|
|
34
|
+
if k >= 0xfffff000 && (code & 0xe0) == 0x20
|
|
35
|
+
ancillary(k, packet.meta)
|
|
36
|
+
else
|
|
37
|
+
size = { 0x00 => 4, 0x08 => 2, 0x10 => 1 }.fetch(code & 0x18)
|
|
38
|
+
return 0 if offset.negative? || offset + size > packet.data.bytesize
|
|
39
|
+
packet.data.unpack1({ 4 => 'N', 2 => 'n', 1 => 'C' }.fetch(size), offset: offset)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
if code == 0xb1
|
|
43
|
+
x = (value & 15) * 4
|
|
44
|
+
elsif (code & 7) == 1
|
|
45
|
+
x = value & 0xffffffff
|
|
46
|
+
else
|
|
47
|
+
a = value & 0xffffffff
|
|
48
|
+
end
|
|
49
|
+
when 2 then memory[k] = a
|
|
50
|
+
when 3 then memory[k] = x
|
|
51
|
+
when 4
|
|
52
|
+
operand = (code & 8).zero? ? k : x
|
|
53
|
+
return 0 if [0x30, 0x90].include?(code & 0xf0) && operand.zero?
|
|
54
|
+
a = arithmetic(code & 0xf0, a, operand) & 0xffffffff
|
|
55
|
+
when 5
|
|
56
|
+
if code == 5
|
|
57
|
+
pc += k
|
|
58
|
+
else
|
|
59
|
+
operand = (code & 8).zero? ? k : x
|
|
60
|
+
result = case code & 0xf0
|
|
61
|
+
when 0x10 then a == operand
|
|
62
|
+
when 0x20 then a > operand
|
|
63
|
+
when 0x30 then a >= operand
|
|
64
|
+
when 0x40 then (a & operand) != 0
|
|
65
|
+
end
|
|
66
|
+
pc += result ? jt : jf
|
|
67
|
+
end
|
|
68
|
+
when 6 then return code == 0x16 ? a : k
|
|
69
|
+
when 7
|
|
70
|
+
code == 7 ? x = a : a = x
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# @rbs (Integer operation, Integer a, Integer operand) -> Integer
|
|
78
|
+
def arithmetic(operation, a, operand)
|
|
79
|
+
case operation
|
|
80
|
+
when 0x00 then a + operand
|
|
81
|
+
when 0x10 then a - operand
|
|
82
|
+
when 0x20 then a * operand
|
|
83
|
+
when 0x30 then a / operand
|
|
84
|
+
when 0x40 then a | operand
|
|
85
|
+
when 0x50 then a & operand
|
|
86
|
+
when 0x60 then operand >= 32 ? 0 : a << operand
|
|
87
|
+
when 0x70 then operand >= 32 ? 0 : a >> operand
|
|
88
|
+
when 0x80 then -a
|
|
89
|
+
when 0x90 then a % operand
|
|
90
|
+
when 0xa0 then a ^ operand
|
|
91
|
+
else 0
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @rbs (Integer offset, untyped meta) -> Integer
|
|
96
|
+
def ancillary(offset, meta)
|
|
97
|
+
case offset
|
|
98
|
+
when 0xfffff000 then meta.fetch(:protocol, 0)
|
|
99
|
+
when 0xfffff030 then meta.key?(:vlan_tci) ? 1 : 0
|
|
100
|
+
when 0xfffff02c then meta.fetch(:vlan_tci, 0)
|
|
101
|
+
when 0xfffff03c then meta.fetch(:vlan_tpid, 0x8100)
|
|
102
|
+
else 0
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|