redhound 0.1.0 → 1.0.0

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.
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