redhound 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -2
  3. data/Dockerfile +1 -1
  4. data/README.md +10 -1
  5. data/Rakefile +12 -1
  6. data/Steepfile +4 -0
  7. data/lib/redhound/analyzer.rb +19 -15
  8. data/lib/redhound/builder/packet_mreq.rb +56 -0
  9. data/lib/redhound/builder/socket.rb +35 -0
  10. data/lib/redhound/builder.rb +4 -0
  11. data/lib/redhound/command.rb +8 -2
  12. data/lib/redhound/l2/ether.rb +68 -0
  13. data/lib/redhound/l2/protocol.rb +33 -0
  14. data/lib/redhound/l2.rb +4 -0
  15. data/lib/redhound/l3/arp.rb +114 -0
  16. data/lib/redhound/l3/base.rb +49 -0
  17. data/lib/redhound/{header → l3}/ipv4.rb +27 -44
  18. data/lib/redhound/l3/ipv6.rb +69 -0
  19. data/lib/redhound/l3/protocol.rb +173 -0
  20. data/lib/redhound/l3/resolver.rb +30 -0
  21. data/lib/redhound/l3.rb +8 -0
  22. data/lib/redhound/l4/base.rb +31 -0
  23. data/lib/redhound/{header → l4}/icmp.rb +20 -26
  24. data/lib/redhound/l4/resolver.rb +28 -0
  25. data/lib/redhound/{header → l4}/udp.rb +19 -19
  26. data/lib/redhound/l4.rb +6 -0
  27. data/lib/redhound/receiver.rb +26 -6
  28. data/lib/redhound/resolver.rb +22 -0
  29. data/lib/redhound/source/socket.rb +18 -0
  30. data/lib/redhound/source.rb +3 -0
  31. data/lib/redhound/version.rb +2 -1
  32. data/lib/redhound/writer.rb +53 -0
  33. data/lib/redhound.rb +7 -3
  34. data/rbs_collection.lock.yaml +20 -0
  35. data/rbs_collection.yaml +17 -0
  36. data/sig/generated/redhound/analyzer.rbs +14 -0
  37. data/sig/generated/redhound/builder/packet_mreq.rbs +39 -0
  38. data/sig/generated/redhound/builder/socket.rbs +24 -0
  39. data/sig/generated/redhound/command.rbs +19 -0
  40. data/sig/generated/redhound/l2/ether.rbs +41 -0
  41. data/sig/generated/redhound/l2/protocol.rbs +15 -0
  42. data/sig/generated/redhound/l3/arp.rbs +57 -0
  43. data/sig/generated/redhound/l3/base.rbs +28 -0
  44. data/sig/generated/redhound/l3/ipv4.rbs +53 -0
  45. data/sig/generated/redhound/l3/ipv6.rbs +38 -0
  46. data/sig/generated/redhound/l3/protocol.rbs +16 -0
  47. data/sig/generated/redhound/l3/resolver.rbs +16 -0
  48. data/sig/generated/redhound/l4/base.rbs +19 -0
  49. data/sig/generated/redhound/l4/icmp.rbs +33 -0
  50. data/sig/generated/redhound/l4/resolver.rbs +16 -0
  51. data/sig/generated/redhound/l4/udp.rbs +36 -0
  52. data/sig/generated/redhound/receiver.rbs +19 -0
  53. data/sig/generated/redhound/resolver.rbs +11 -0
  54. data/sig/generated/redhound/source/socket.rbs +13 -0
  55. data/sig/generated/redhound/version.rbs +5 -0
  56. data/sig/generated/redhound/writer.rbs +25 -0
  57. metadata +49 -11
  58. data/lib/redhound/header/ether.rb +0 -68
  59. data/lib/redhound/header.rb +0 -6
  60. data/lib/redhound/packet_mreq.rb +0 -46
  61. data/lib/redhound/socket_builder.rb +0 -30
  62. data/sig/redhound.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6935c5774360fee584469172ce32fb3d5d75d9c6afac07471d6f81a1c2abeab
4
- data.tar.gz: 98f8c7e7e2910174326df83669f9f2d48e3fbabed8ce74083e060299fd95b724
3
+ metadata.gz: 21334546fae10c6cbeb5b8d679e658317e39b569741ef671c45b7261dbb42106
4
+ data.tar.gz: 28046b9bfb2814928662e4d52768537bfd780778554c3073c1aebabf45da443b
5
5
  SHA512:
6
- metadata.gz: 17a0eb8f7d9cf19e20c2e44a98c23054aaedc51e1fac0d314b4832c85a3c586d68057a3367887beb8a33ac41b27c1746569510cc4da874bf23ab92b130fe0c7c
7
- data.tar.gz: 0c3cae42594bfc3e341885efb840212b6d1e36b39c7ef53ece6dddb7ad59fc97d59842d9397fe82ae2f7b1f9ad3775d4e493c9987611058b85b65d4d40e405b5
6
+ metadata.gz: 81a384c6381bb1473013536513b1b5e12e2faf7c78300cebf0dde4d778e0521ac8d80d6e0c31b5e27199158464711b3cc450f8b00e2cefc6e650d5801fd94c9c
7
+ data.tar.gz: b9f2fec1904e462b2d650cfbdb3d1704408160005dc103f4d6db39fbffacb28b1e3d371dfc4f518306e349f1269f9bea36ffdbaeecf51e5ac28ac2f29fb55b8c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
- ## [Unreleased]
1
+ # Changelog
2
2
 
3
- ## [0.1.0] - 2024-11-05
3
+ ## Unreleased
4
+
5
+ ## 1.0.0 - 2025-01-16
6
+
7
+ - Add ARP header support.
8
+ - Add IPv6 header support.
9
+ - Improve formatting of packet output.
10
+ - Remove debug print statement from IPv4 header parsing.
11
+
12
+ ## 0.2.0 - 2025-01-03
13
+
14
+ - Add option to write packets to file as PCAP Capture File Format.
15
+
16
+ ## 0.1.0 - 2024-11-05
4
17
 
5
18
  - Initial release
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  FROM ruby:3.3.4
2
- RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
2
+ RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs iputils-ping iproute2 iptables
3
3
  WORKDIR /app
4
4
  COPY Gemfile /app/Gemfile
5
5
  RUN bundle install
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Redhound
1
+ # Redhound [![Gem Version](https://badge.fury.io/rb/redhound.svg)](https://badge.fury.io/rb/redhound) [![Test](https://github.com/ydah/redhound/actions/workflows/main.yml/badge.svg)](https://github.com/ydah/redhound/actions/workflows/main.yml)
2
2
 
3
3
  Pure Ruby packet analyzer.
4
4
  At this time, it is only guaranteed to work on Linux.
@@ -20,11 +20,20 @@ gem install redhound
20
20
  ## Usage
21
21
 
22
22
  ```command
23
+ ___ ____ __
24
+ / _ \___ ___/ / / ___ __ _____ ___/ /
25
+ / , _/ -_) _ / _ \/ _ \/ // / _ \/ _ /
26
+ /_/|_|\__/\_,_/_//_/\___/\_,_/_//_/\_,_/
27
+
28
+ Version: 1.0.0
29
+ Dump and analyze network packets.
30
+
23
31
  Usage: redhound [options] ...
24
32
 
25
33
  Options:
26
34
  -i, --interface INTERFACE name or idx of interface
27
35
  -D, --list-interfaces print list of interfaces and exit
36
+ -w FILE write packets to a pcap capture file format to file
28
37
  -h, --help display this help and exit
29
38
  -v, --version display version information and exit
30
39
  ```
data/Rakefile CHANGED
@@ -1,4 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
- task default: %i[]
4
+
5
+ desc "steep check"
6
+ task :steep do
7
+ sh "bundle exec steep check"
8
+ end
9
+
10
+ desc "Run rbs-inline"
11
+ task :rbs_inline do
12
+ sh "bundle exec rbs-inline --output lib/"
13
+ end
14
+
15
+ task default: %i[rbs_inline steep]
data/Steepfile ADDED
@@ -0,0 +1,4 @@
1
+ target :lib do
2
+ signature "sig"
3
+ check "lib"
4
+ end
@@ -1,30 +1,34 @@
1
+ # rbs_inline: enabled
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module Redhound
4
5
  class Analyzer
5
- def self.analyze(msg:)
6
- new(msg: msg).analyze
6
+ # @rbs (msg: String, count: Integer) -> void
7
+ def self.analyze(msg:, count:)
8
+ new(msg:, count:).analyze
7
9
  end
8
10
 
9
- def initialize(msg:)
11
+ # @rbs (msg: String, count: Integer) -> void
12
+ def initialize(msg:, count:)
10
13
  @msg = msg
14
+ @count = count
11
15
  end
12
16
 
17
+ # @rbs () -> void
13
18
  def analyze
14
- puts 'Analyzing...'
15
- ether = Header::Ether.generate(bytes: @msg.bytes[0..13])
16
- ether.dump
17
- return unless ether.ipv4?
19
+ l2 = L2::Ether.generate(bytes: @msg.bytes[0..], count: @count)
20
+ l2.dump
21
+ return unless l2.supported_type?
18
22
 
19
- ip = Header::Ipv4.generate(bytes: @msg.bytes[14..33])
20
- ip.dump
21
- if ip.udp?
22
- udp = Header::Udp.generate(bytes: @msg.bytes[34..41])
23
- udp.dump
24
- elsif ip.icmp?
25
- icmp = Header::Icmp.generate(bytes: @msg.bytes[34..41])
26
- icmp.dump
23
+ l3 = L3::Resolver.resolve(bytes: @msg.bytes[l2.size..], l2:)
24
+ return if !l3 || @msg.bytes.size <= l2.size + l3.size
25
+ l3.dump
26
+ unless l3.supported_protocol?
27
+ puts " └─ Unsupported protocol #{l3.protocol}"
28
+ return
27
29
  end
30
+
31
+ L4::Resolver.resolve(bytes: @msg.bytes[(l2.size + l3.size)..], l3:)&.dump
28
32
  end
29
33
  end
30
34
  end
@@ -0,0 +1,56 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require 'socket'
5
+
6
+ module Redhound
7
+ module Builder
8
+ class PacketMreq
9
+ PACKET_MR_PROMISC = 0x0001 # NOTE: netpacket/packet.h
10
+
11
+ # @rbs (ifname: String) -> void
12
+ def initialize(ifname:)
13
+ @ifname = ifname
14
+ end
15
+
16
+ # see: https://man7.org/linux/man-pages/man7/packet.7.html
17
+ # struct packet_mreq {
18
+ # int mr_ifindex; /* interface index */
19
+ # unsigned short mr_type; /* action */
20
+ # unsigned short mr_alen; /* address length */
21
+ # unsigned char mr_address[8]; /* physical-layer address */
22
+ # };
23
+ # @rbs () -> String
24
+ def build
25
+ mr_ifindex + mr_type + mr_alen + mr_address
26
+ end
27
+
28
+ # @rbs () -> String
29
+ def mr_ifindex
30
+ @mr_ifindex ||= [[index].pack('I')].pack('a4')
31
+ end
32
+
33
+ private
34
+
35
+ # @rbs () -> Integer?
36
+ def index
37
+ ::Socket.getifaddrs.find { |ifaddr| ifaddr.name == @ifname }&.ifindex
38
+ end
39
+
40
+ # @rbs () -> String
41
+ def mr_type
42
+ @mr_type ||= [PACKET_MR_PROMISC].pack('S')
43
+ end
44
+
45
+ # @rbs () -> String
46
+ def mr_alen
47
+ @mr_alen ||= [0].pack('S')
48
+ end
49
+
50
+ # @rbs () -> String
51
+ def mr_address
52
+ @mr_address ||= [0].pack('C') * 8
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require 'socket'
5
+
6
+ module Redhound
7
+ module Builder
8
+ class Socket
9
+ SOL_PACKET = 0x0107 # bits/socket.h
10
+ PACKET_ADD_MEMBERSHIP = 0x0001 # NOTE: netpacket/packet.h
11
+ ETH_P_ALL = 768 # NOTE: htons(ETH_P_ALL) => linux/if_ether.h
12
+ PACKED_ETH_P_ALL = [ETH_P_ALL].pack('S').unpack1('S>')
13
+
14
+ class << self
15
+ # @rbs (ifname: String) -> Redhound::Source::Socket
16
+ def build(ifname:)
17
+ new(ifname:).build
18
+ end
19
+ end
20
+
21
+ # @rbs (ifname: String) -> void
22
+ def initialize(ifname:)
23
+ @mq_req = PacketMreq.new(ifname:)
24
+ end
25
+
26
+ # @rbs () -> Redhound::Source::Socket
27
+ def build
28
+ socket = ::Socket.new(::Socket::AF_PACKET, ::Socket::SOCK_RAW, ETH_P_ALL)
29
+ socket.bind([::Socket::AF_PACKET, PACKED_ETH_P_ALL, @mq_req.mr_ifindex].pack('SS>a16'))
30
+ socket.setsockopt(SOL_PACKET, PACKET_ADD_MEMBERSHIP, @mq_req.build)
31
+ Redhound::Source::Socket.new(socket:)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'builder/packet_mreq'
4
+ require_relative 'builder/socket'
@@ -1,3 +1,4 @@
1
+ # rbs_inline: enabled
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'optparse'
@@ -5,21 +6,24 @@ require 'socket'
5
6
 
6
7
  module Redhound
7
8
  class Command
9
+ # @rbs () -> void
8
10
  def initialize
9
11
  @options = { ifname: nil }
10
12
  end
11
13
 
14
+ # @rbs (Array[untyped] argv) -> void
12
15
  def run(argv)
13
16
  parse(argv)
14
17
  if @options[:ifname].nil?
15
18
  warn 'Error: interface is required'
16
19
  exit 1
17
20
  end
18
- Receiver.run(ifname: @options[:ifname])
21
+ Receiver.run(ifname: @options[:ifname], filename: @options[:filename])
19
22
  end
20
23
 
24
+ # @rbs (Array[untyped] argv) -> void
21
25
  def parse(argv)
22
- OptionParser.new do |o|
26
+ OptionParser.new do |o| # steep:ignore
23
27
  o.banner = <<~'BANNER' + <<~BANNER2
24
28
  ___ ____ __
25
29
  / _ \___ ___/ / / ___ __ _____ ___/ /
@@ -39,6 +43,7 @@ module Redhound
39
43
  list_interfaces
40
44
  exit
41
45
  end
46
+ o.on('-w FILE', 'write packets to a pcap capture file format to file') { |v| @options[:filename] = v }
42
47
  o.on('-h', '--help', 'display this help and exit') do
43
48
  puts o
44
49
  exit
@@ -54,6 +59,7 @@ module Redhound
54
59
 
55
60
  private
56
61
 
62
+ # @rbs () -> void
57
63
  def list_interfaces
58
64
  ::Socket.getifaddrs.each { |ifaddr| puts ifaddr.name }
59
65
  end
@@ -0,0 +1,68 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ class L2
6
+ class Ether
7
+ attr_reader :type #: Protocol
8
+
9
+ class << self
10
+ # @rbs (bytes: Array[Integer], count: Integer) -> Redhound::L2::Ether
11
+ def generate(bytes:, count:)
12
+ new(bytes:, count:).generate
13
+ end
14
+ end
15
+
16
+ # @rbs (bytes: Array[Integer], count: Integer) -> void
17
+ def initialize(bytes:, count:)
18
+ raise ArgumentError, "bytes must be #{size} bytes" unless bytes.size >= size
19
+
20
+ @bytes = bytes
21
+ @count = count
22
+ end
23
+
24
+ # @rbs () -> Integer
25
+ def size = 14
26
+
27
+ # @rbs () -> Redhound::L2::Ether
28
+ def generate
29
+ @dhost = @bytes[0..5]
30
+ @shost = @bytes[6..11]
31
+ @type = Protocol.new(protocol: hex_type(@bytes[12..13]))
32
+ self
33
+ end
34
+
35
+ # @rbs () -> void
36
+ def dump
37
+ puts self
38
+ end
39
+
40
+ # @rbs () -> String
41
+ def to_s
42
+ "[#{@count}] Ethernet Dst: #{dhost} Src: #{shost} Type: #{@type}"
43
+ end
44
+
45
+ # @rbs () -> bool
46
+ def supported_type?
47
+ @type.ipv4? || @type.ipv6? || @type.arp? # steep:ignore
48
+ end
49
+
50
+ private
51
+
52
+ # @rbs () -> String
53
+ def dhost
54
+ @dhost.map { |b| b.to_s(16).rjust(2, '0') }.join(':')
55
+ end
56
+
57
+ # @rbs () -> String
58
+ def shost
59
+ @shost.map { |b| b.to_s(16).rjust(2, '0') }.join(':')
60
+ end
61
+
62
+ # @rbs (Array[Integer] type) -> Integer
63
+ def hex_type(type)
64
+ type.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,33 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ class L2
6
+ class Protocol
7
+ PROTO_TABLE = {
8
+ 0x0060 => 'Loopback',
9
+ 0x0800 => 'IPv4',
10
+ 0x0806 => 'ARP',
11
+ 0x86DD => 'IPv6',
12
+ 0x8100 => 'VLAN',
13
+ }
14
+
15
+ # @rbs (protocol: Integer) -> void
16
+ def initialize(protocol:)
17
+ @protocol = protocol
18
+ end
19
+
20
+ # @rbs () -> String
21
+ def to_s
22
+ PROTO_TABLE[@protocol] || 'Unknown'
23
+ end
24
+
25
+ PROTO_TABLE.each do |id, name|
26
+ method_name = name.downcase.gsub(/[ \-]/, '_') + '?'
27
+ define_method(method_name) do
28
+ @protocol == id
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'l2/ether'
4
+ require_relative 'l2/protocol'
@@ -0,0 +1,114 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ class L3
6
+ class Arp < Base
7
+ class << self
8
+ # @rbs (bytes: Array[Integer]) -> Redhound::L3::Arp
9
+ def generate(bytes:)
10
+ new(bytes:).generate
11
+ end
12
+ end
13
+
14
+ # @rbs (bytes: Array[Integer]) -> void
15
+ def initialize(bytes:)
16
+ raise ArgumentError, "bytes must be bigger than #{arp_size} bytes" unless bytes.size >= arp_size
17
+
18
+ @bytes = bytes
19
+ end
20
+
21
+ # @rbs () -> Redhound::L3::Arp
22
+ def generate
23
+ @htype = @bytes[0..1]
24
+ @ptype = @bytes[2..3]
25
+ @hlen = @bytes[4]
26
+ @plen = @bytes[5]
27
+ @oper = @bytes[6..7]
28
+ @sha = @bytes[8..13]
29
+ @spa = @bytes[14..17]
30
+ @tha = @bytes[18..23]
31
+ @tpa = @bytes[24..27]
32
+ @type = Redhound::L2::Protocol.new(protocol: ptype)
33
+ @l3 = generate_l3
34
+ self
35
+ end
36
+
37
+ # @rbs () -> Integer
38
+ def arp_size = 28
39
+
40
+ # @rbs () -> Integer
41
+ def size
42
+ if @l3.nil?
43
+ arp_size
44
+ else
45
+ arp_size + @l3.size
46
+ end
47
+ end
48
+
49
+ # @rbs () -> String
50
+ def to_s
51
+ " └─ ARP HType: #{htype} PType: #{ptype} HLen: #{@hlen} PLen: #{@plen} Oper: #{oper} SHA: #{sha} SPA: #{spa} THA: #{tha} TPA: #{tpa}"
52
+ end
53
+
54
+ # @rbs () -> bool
55
+ def supported_protocol?
56
+ return false if @l3.nil?
57
+ @l3.supported_protocol?
58
+ end
59
+
60
+ # @rbs () -> String?
61
+ def protocol
62
+ @l3.protocol if @l3
63
+ end
64
+
65
+ private
66
+
67
+ # @rbs () -> Integer
68
+ def htype
69
+ @htype.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
70
+ end
71
+
72
+ # @rbs () -> Integer
73
+ def ptype
74
+ @ptype.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
75
+ end
76
+
77
+ # @rbs () -> Integer
78
+ def oper
79
+ @oper.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
80
+ end
81
+
82
+ # @rbs () -> String
83
+ def sha
84
+ @sha.map { |b| b.to_s(16).rjust(2, '0') }.join(':')
85
+ end
86
+
87
+ # @rbs () -> String
88
+ def spa
89
+ @spa.map { |b| b.to_s(16).rjust(2, '0') }.join('.')
90
+ end
91
+
92
+ # @rbs () -> String
93
+ def tha
94
+ @tha.map { |b| b.to_s(16).rjust(2, '0') }.join(':')
95
+ end
96
+
97
+ # @rbs () -> String
98
+ def tpa
99
+ @tpa.map { |b| b.to_s(16).rjust(2, '0') }.join('.')
100
+ end
101
+
102
+ # @rbs () -> Redhound::L3::Base?
103
+ def generate_l3
104
+ return if @bytes.size == arp_size
105
+
106
+ if @type.ipv4?
107
+ Ipv4.generate(bytes: @bytes[arp_size..])
108
+ elsif @type.ipv6?
109
+ Ipv6.generate(bytes: @bytes[arp_size..])
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,49 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ class L3
6
+ class Base
7
+ class << self
8
+ # @rbs (bytes: Array[Integer]) -> Redhound::L3::Base
9
+ def generate(bytes:)
10
+ new(bytes:).generate
11
+ end
12
+ end
13
+
14
+ # @rbs (bytes: Array[Integer]) -> void
15
+ def initialize(bytes:)
16
+ warn 'initialize method must be implemented'
17
+ end
18
+
19
+ # @rbs () -> Redhound::L3::Base
20
+ def generate
21
+ warn 'generate method must be implemented'
22
+ self
23
+ end
24
+
25
+ # @rbs () -> void
26
+ def dump
27
+ puts self
28
+ end
29
+
30
+ # @rbs () -> Integer
31
+ def size
32
+ warn 'size method must be implemented'
33
+ 0
34
+ end
35
+
36
+ # @rbs () -> bool
37
+ def supported_protocol?
38
+ warn 'supported_protocol? method must be implemented'
39
+ false
40
+ end
41
+
42
+ # @rbs () -> Protocol
43
+ def protocol
44
+ warn 'protocol method must be implemented'
45
+ Protocol.new(protocol: 0)
46
+ end
47
+ end
48
+ end
49
+ end