redhound 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Dockerfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -0
- data/Rakefile +4 -0
- data/docker-compose.yml +9 -0
- data/exe/redhound +7 -0
- data/lib/redhound/analyzer.rb +30 -0
- data/lib/redhound/command.rb +61 -0
- data/lib/redhound/header/ether.rb +68 -0
- data/lib/redhound/header/icmp.rb +77 -0
- data/lib/redhound/header/ipv4.rb +112 -0
- data/lib/redhound/header/udp.rb +63 -0
- data/lib/redhound/header.rb +6 -0
- data/lib/redhound/packet_mreq.rb +46 -0
- data/lib/redhound/receiver.rb +25 -0
- data/lib/redhound/socket_builder.rb +30 -0
- data/lib/redhound/version.rb +5 -0
- data/lib/redhound.rb +9 -0
- data/sig/redhound.rbs +4 -0
- metadata +69 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f6935c5774360fee584469172ce32fb3d5d75d9c6afac07471d6f81a1c2abeab
|
4
|
+
data.tar.gz: 98f8c7e7e2910174326df83669f9f2d48e3fbabed8ce74083e060299fd95b724
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 17a0eb8f7d9cf19e20c2e44a98c23054aaedc51e1fac0d314b4832c85a3c586d68057a3367887beb8a33ac41b27c1746569510cc4da874bf23ab92b130fe0c7c
|
7
|
+
data.tar.gz: 0c3cae42594bfc3e341885efb840212b6d1e36b39c7ef53ece6dddb7ad59fc97d59842d9397fe82ae2f7b1f9ad3775d4e493c9987611058b85b65d4d40e405b5
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
9
|
+
nationality, personal appearance, race, caste, color, religion, or sexual
|
10
|
+
identity and orientation.
|
11
|
+
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
13
|
+
diverse, inclusive, and healthy community.
|
14
|
+
|
15
|
+
## Our Standards
|
16
|
+
|
17
|
+
Examples of behavior that contributes to a positive environment for our
|
18
|
+
community include:
|
19
|
+
|
20
|
+
* Demonstrating empathy and kindness toward other people
|
21
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
22
|
+
* Giving and gracefully accepting constructive feedback
|
23
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
24
|
+
and learning from the experience
|
25
|
+
* Focusing on what is best not just for us as individuals, but for the overall
|
26
|
+
community
|
27
|
+
|
28
|
+
Examples of unacceptable behavior include:
|
29
|
+
|
30
|
+
* The use of sexualized language or imagery, and sexual attention or advances of
|
31
|
+
any kind
|
32
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
33
|
+
* Public or private harassment
|
34
|
+
* Publishing others' private information, such as a physical or email address,
|
35
|
+
without their explicit permission
|
36
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
37
|
+
professional setting
|
38
|
+
|
39
|
+
## Enforcement Responsibilities
|
40
|
+
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
44
|
+
or harmful.
|
45
|
+
|
46
|
+
Community leaders have the right and responsibility to remove, edit, or reject
|
47
|
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
48
|
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
49
|
+
decisions when appropriate.
|
50
|
+
|
51
|
+
## Scope
|
52
|
+
|
53
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
54
|
+
an individual is officially representing the community in public spaces.
|
55
|
+
Examples of representing our community include using an official email address,
|
56
|
+
posting via an official social media account, or acting as an appointed
|
57
|
+
representative at an online or offline event.
|
58
|
+
|
59
|
+
## Enforcement
|
60
|
+
|
61
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
62
|
+
reported to the community leaders responsible for enforcement at
|
63
|
+
[INSERT CONTACT METHOD].
|
64
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
65
|
+
|
66
|
+
All community leaders are obligated to respect the privacy and security of the
|
67
|
+
reporter of any incident.
|
68
|
+
|
69
|
+
## Enforcement Guidelines
|
70
|
+
|
71
|
+
Community leaders will follow these Community Impact Guidelines in determining
|
72
|
+
the consequences for any action they deem in violation of this Code of Conduct:
|
73
|
+
|
74
|
+
### 1. Correction
|
75
|
+
|
76
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
77
|
+
unprofessional or unwelcome in the community.
|
78
|
+
|
79
|
+
**Consequence**: A private, written warning from community leaders, providing
|
80
|
+
clarity around the nature of the violation and an explanation of why the
|
81
|
+
behavior was inappropriate. A public apology may be requested.
|
82
|
+
|
83
|
+
### 2. Warning
|
84
|
+
|
85
|
+
**Community Impact**: A violation through a single incident or series of
|
86
|
+
actions.
|
87
|
+
|
88
|
+
**Consequence**: A warning with consequences for continued behavior. No
|
89
|
+
interaction with the people involved, including unsolicited interaction with
|
90
|
+
those enforcing the Code of Conduct, for a specified period of time. This
|
91
|
+
includes avoiding interactions in community spaces as well as external channels
|
92
|
+
like social media. Violating these terms may lead to a temporary or permanent
|
93
|
+
ban.
|
94
|
+
|
95
|
+
### 3. Temporary Ban
|
96
|
+
|
97
|
+
**Community Impact**: A serious violation of community standards, including
|
98
|
+
sustained inappropriate behavior.
|
99
|
+
|
100
|
+
**Consequence**: A temporary ban from any sort of interaction or public
|
101
|
+
communication with the community for a specified period of time. No public or
|
102
|
+
private interaction with the people involved, including unsolicited interaction
|
103
|
+
with those enforcing the Code of Conduct, is allowed during this period.
|
104
|
+
Violating these terms may lead to a permanent ban.
|
105
|
+
|
106
|
+
### 4. Permanent Ban
|
107
|
+
|
108
|
+
**Community Impact**: Demonstrating a pattern of violation of community
|
109
|
+
standards, including sustained inappropriate behavior, harassment of an
|
110
|
+
individual, or aggression toward or disparagement of classes of individuals.
|
111
|
+
|
112
|
+
**Consequence**: A permanent ban from any sort of public interaction within the
|
113
|
+
community.
|
114
|
+
|
115
|
+
## Attribution
|
116
|
+
|
117
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
118
|
+
version 2.1, available at
|
119
|
+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
120
|
+
|
121
|
+
Community Impact Guidelines were inspired by
|
122
|
+
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
123
|
+
|
124
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
125
|
+
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
126
|
+
[https://www.contributor-covenant.org/translations][translations].
|
127
|
+
|
128
|
+
[homepage]: https://www.contributor-covenant.org
|
129
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
130
|
+
[Mozilla CoC]: https://github.com/mozilla/diversity
|
131
|
+
[FAQ]: https://www.contributor-covenant.org/faq
|
132
|
+
[translations]: https://www.contributor-covenant.org/translations
|
data/Dockerfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Yudai Takada
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Redhound
|
2
|
+
|
3
|
+
Pure Ruby packet analyzer.
|
4
|
+
At this time, it is only guaranteed to work on Linux.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Install the gem and add to the application's Gemfile by executing:
|
9
|
+
|
10
|
+
```bash
|
11
|
+
bundle add redhound
|
12
|
+
```
|
13
|
+
|
14
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
15
|
+
|
16
|
+
```bash
|
17
|
+
gem install redhound
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
```command
|
23
|
+
Usage: redhound [options] ...
|
24
|
+
|
25
|
+
Options:
|
26
|
+
-i, --interface INTERFACE name or idx of interface
|
27
|
+
-D, --list-interfaces print list of interfaces and exit
|
28
|
+
-h, --help display this help and exit
|
29
|
+
-v, --version display version information and exit
|
30
|
+
```
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ydah/redhound. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ydah/redhound/blob/main/CODE_OF_CONDUCT.md).
|
35
|
+
|
36
|
+
## License
|
37
|
+
|
38
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
39
|
+
|
40
|
+
## Code of Conduct
|
41
|
+
|
42
|
+
Everyone interacting in the Redhound project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ydah/redhound/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/docker-compose.yml
ADDED
data/exe/redhound
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redhound
|
4
|
+
class Analyzer
|
5
|
+
def self.analyze(msg:)
|
6
|
+
new(msg: msg).analyze
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(msg:)
|
10
|
+
@msg = msg
|
11
|
+
end
|
12
|
+
|
13
|
+
def analyze
|
14
|
+
puts 'Analyzing...'
|
15
|
+
ether = Header::Ether.generate(bytes: @msg.bytes[0..13])
|
16
|
+
ether.dump
|
17
|
+
return unless ether.ipv4?
|
18
|
+
|
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
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'socket'
|
5
|
+
|
6
|
+
module Redhound
|
7
|
+
class Command
|
8
|
+
def initialize
|
9
|
+
@options = { ifname: nil }
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(argv)
|
13
|
+
parse(argv)
|
14
|
+
if @options[:ifname].nil?
|
15
|
+
warn 'Error: interface is required'
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
Receiver.run(ifname: @options[:ifname])
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(argv)
|
22
|
+
OptionParser.new do |o|
|
23
|
+
o.banner = <<~'BANNER' + <<~BANNER2
|
24
|
+
___ ____ __
|
25
|
+
/ _ \___ ___/ / / ___ __ _____ ___/ /
|
26
|
+
/ , _/ -_) _ / _ \/ _ \/ // / _ \/ _ /
|
27
|
+
/_/|_|\__/\_,_/_//_/\___/\_,_/_//_/\_,_/
|
28
|
+
|
29
|
+
BANNER
|
30
|
+
Version: #{Redhound::VERSION}
|
31
|
+
Dump and analyze network packets.
|
32
|
+
|
33
|
+
Usage: redhound [options] ...
|
34
|
+
BANNER2
|
35
|
+
o.separator ''
|
36
|
+
o.separator 'Options:'
|
37
|
+
o.on('-i', '--interface INTERFACE', 'name or idx of interface') { |v| @options[:ifname] = v }
|
38
|
+
o.on('-D', '--list-interfaces', 'print list of interfaces and exit') do
|
39
|
+
list_interfaces
|
40
|
+
exit
|
41
|
+
end
|
42
|
+
o.on('-h', '--help', 'display this help and exit') do
|
43
|
+
puts o
|
44
|
+
exit
|
45
|
+
end
|
46
|
+
o.on('-v', '--version', 'display version information and exit') do
|
47
|
+
puts "Redhound #{Redhound::VERSION}"
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
o.on_tail
|
51
|
+
o.parse!(argv)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def list_interfaces
|
58
|
+
::Socket.getifaddrs.each { |ifaddr| puts ifaddr.name }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redhound
|
4
|
+
class Header
|
5
|
+
class Ether
|
6
|
+
ETH_P_IP = 0x0800
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def generate(bytes:)
|
10
|
+
new(bytes:).generate
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(bytes:)
|
15
|
+
raise ArgumentError, 'bytes must be 14 bytes' unless bytes.size == 14
|
16
|
+
|
17
|
+
@bytes = bytes
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate
|
21
|
+
pp @bytes[0..5]
|
22
|
+
@dhost = @bytes[0..5]
|
23
|
+
@shost = @bytes[6..11]
|
24
|
+
@type = @bytes[12..13]
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def ipv4?
|
29
|
+
hex_type == ETH_P_IP
|
30
|
+
end
|
31
|
+
|
32
|
+
def dump
|
33
|
+
puts 'ETHERNET HEADER----------------'
|
34
|
+
puts self
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
<<~ETHER
|
39
|
+
Destination MAC: #{dhost}
|
40
|
+
Source MAC: #{shost}
|
41
|
+
Type: #{type}
|
42
|
+
ETHER
|
43
|
+
end
|
44
|
+
|
45
|
+
def dhost
|
46
|
+
@dhost.map { |b| b.to_s(16).rjust(2, '0') }.join(':')
|
47
|
+
end
|
48
|
+
|
49
|
+
def shost
|
50
|
+
@shost.map { |b| b.to_s(16).rjust(2, '0') }.join(':')
|
51
|
+
end
|
52
|
+
|
53
|
+
def type
|
54
|
+
if ipv4?
|
55
|
+
'IPv4'
|
56
|
+
else
|
57
|
+
'Unknown'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def hex_type
|
64
|
+
@hex_type ||= @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,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redhound
|
4
|
+
class Header
|
5
|
+
class Icmp
|
6
|
+
class << self
|
7
|
+
def generate(bytes:)
|
8
|
+
new(bytes:).generate
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(bytes:)
|
13
|
+
raise ArgumentError, 'bytes must be bigger than 8 bytes' unless bytes.size >= 8
|
14
|
+
|
15
|
+
@bytes = bytes
|
16
|
+
end
|
17
|
+
|
18
|
+
def generate
|
19
|
+
@type = @bytes[0]
|
20
|
+
@code = @bytes[1]
|
21
|
+
@check = @bytes[2..3]
|
22
|
+
# refs: https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types
|
23
|
+
if @type.zero? || @type == 8
|
24
|
+
@id = @bytes[4..5]
|
25
|
+
@seq = @bytes[6..7]
|
26
|
+
@data = @bytes[8..]
|
27
|
+
else
|
28
|
+
@data = @bytes[4..]
|
29
|
+
end
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def dump
|
34
|
+
puts 'ICMP HEADER----------------'
|
35
|
+
puts self
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
if @type.zero? || @type == 8
|
40
|
+
<<~ICMP
|
41
|
+
Type: #{@type}
|
42
|
+
Code: #{@code}
|
43
|
+
Checksum: #{check}
|
44
|
+
ID: #{id}
|
45
|
+
Sequence: #{seq}
|
46
|
+
Data: #{data}
|
47
|
+
ICMP
|
48
|
+
else
|
49
|
+
<<~ICMP
|
50
|
+
Type: #{@type}
|
51
|
+
Code: #{@code}
|
52
|
+
Checksum: #{check}
|
53
|
+
Data: #{data}
|
54
|
+
ICMP
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def check
|
61
|
+
@check.map { |b| b.to_s(16).rjust(2, '0') }.join
|
62
|
+
end
|
63
|
+
|
64
|
+
def id
|
65
|
+
@id.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
66
|
+
end
|
67
|
+
|
68
|
+
def seq
|
69
|
+
@seq.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
70
|
+
end
|
71
|
+
|
72
|
+
def data
|
73
|
+
@data.map(&:chr).join
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redhound
|
4
|
+
class Header
|
5
|
+
class Ipv4
|
6
|
+
class << self
|
7
|
+
def generate(bytes:)
|
8
|
+
new(bytes:).generate
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# ref: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml#protocol-numbers-1
|
13
|
+
ICMP = 1
|
14
|
+
UDP = 17
|
15
|
+
|
16
|
+
def initialize(bytes:)
|
17
|
+
raise ArgumentError, 'bytes must be 20 bytes' unless bytes.size == 20
|
18
|
+
|
19
|
+
@bytes = bytes
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate
|
23
|
+
@version = @bytes[0]
|
24
|
+
@ihl = @bytes[0]
|
25
|
+
@tos = @bytes[1]
|
26
|
+
@tot_len = @bytes[2..3]
|
27
|
+
@id = @bytes[4..5]
|
28
|
+
@frag_off = @bytes[6..7]
|
29
|
+
@ttl = @bytes[8]
|
30
|
+
@protocol = @bytes[9]
|
31
|
+
@check = @bytes[10..11]
|
32
|
+
@saddr = @bytes[12..15]
|
33
|
+
@daddr = @bytes[16..19]
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def icmp?
|
38
|
+
@protocol == ICMP
|
39
|
+
end
|
40
|
+
|
41
|
+
def udp?
|
42
|
+
@protocol == UDP
|
43
|
+
end
|
44
|
+
|
45
|
+
def dump
|
46
|
+
puts 'IPv4 HEADER----------------'
|
47
|
+
puts self
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
<<~IPV4
|
52
|
+
Version: #{@version}
|
53
|
+
IHL: #{@ihl}
|
54
|
+
TOS: #{@tos}
|
55
|
+
Total Length: #{tot_len}
|
56
|
+
ID: #{id}
|
57
|
+
Fragment Offset: #{frag_off}
|
58
|
+
TTL: #{@ttl}
|
59
|
+
Protocol: #{protocol}
|
60
|
+
Checksum: #{check}
|
61
|
+
Source IP: #{saddr}
|
62
|
+
Destination IP: #{daddr}
|
63
|
+
IPV4
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def version
|
69
|
+
@version & 0xF0
|
70
|
+
end
|
71
|
+
|
72
|
+
def ihl
|
73
|
+
@ihl & 0x0F
|
74
|
+
end
|
75
|
+
|
76
|
+
def tot_len
|
77
|
+
@tot_len.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
78
|
+
end
|
79
|
+
|
80
|
+
def id
|
81
|
+
@id.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
82
|
+
end
|
83
|
+
|
84
|
+
def frag_off
|
85
|
+
@frag_off.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16) & 0x1FFF
|
86
|
+
end
|
87
|
+
|
88
|
+
def protocol
|
89
|
+
case @protocol
|
90
|
+
when ICMP
|
91
|
+
'ICMP'
|
92
|
+
when UDP
|
93
|
+
'UDP'
|
94
|
+
else
|
95
|
+
'Unknown'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def check
|
100
|
+
@check.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
101
|
+
end
|
102
|
+
|
103
|
+
def saddr
|
104
|
+
@saddr.map { |b| b.to_s(16).rjust(2, '0') }.join('.')
|
105
|
+
end
|
106
|
+
|
107
|
+
def daddr
|
108
|
+
@daddr.map { |b| b.to_s(16).rjust(2, '0') }.join('.')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redhound
|
4
|
+
class Header
|
5
|
+
class Udp
|
6
|
+
class << self
|
7
|
+
def generate(bytes:)
|
8
|
+
new(bytes:).generate
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(bytes:)
|
13
|
+
raise ArgumentError, 'bytes must be bigger than 8 bytes' unless bytes.size >= 8
|
14
|
+
|
15
|
+
@bytes = bytes
|
16
|
+
end
|
17
|
+
|
18
|
+
def generate
|
19
|
+
@sport = @bytes[0..1]
|
20
|
+
@dport = @bytes[2..3]
|
21
|
+
@len = @bytes[4..5]
|
22
|
+
@check = @bytes[6..7]
|
23
|
+
@data = @bytes[8..]
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def dump
|
28
|
+
puts 'UDP HEADER----------------'
|
29
|
+
puts self
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
<<~UDP
|
34
|
+
Source Port: #{sport}
|
35
|
+
Destination Port: #{dport}
|
36
|
+
Length: #{len}
|
37
|
+
Checksum: #{check}
|
38
|
+
Data: #{data}
|
39
|
+
UDP
|
40
|
+
end
|
41
|
+
|
42
|
+
def sport
|
43
|
+
@sport.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
44
|
+
end
|
45
|
+
|
46
|
+
def dport
|
47
|
+
@dport.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
48
|
+
end
|
49
|
+
|
50
|
+
def len
|
51
|
+
@len.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
52
|
+
end
|
53
|
+
|
54
|
+
def check
|
55
|
+
@check.map { |b| b.to_s(16).rjust(2, '0') }.join.to_i(16)
|
56
|
+
end
|
57
|
+
|
58
|
+
def data
|
59
|
+
@data.map(&:chr).join
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Redhound
|
6
|
+
class PacketMreq
|
7
|
+
PACKET_MR_PROMISC = 0x0001 # NOTE: netpacket/packet.h
|
8
|
+
|
9
|
+
def initialize(ifname:)
|
10
|
+
@ifname = ifname
|
11
|
+
end
|
12
|
+
|
13
|
+
# see: https://man7.org/linux/man-pages/man7/packet.7.html
|
14
|
+
# struct packet_mreq {
|
15
|
+
# int mr_ifindex; /* interface index */
|
16
|
+
# unsigned short mr_type; /* action */
|
17
|
+
# unsigned short mr_alen; /* address length */
|
18
|
+
# unsigned char mr_address[8]; /* physical-layer address */
|
19
|
+
# };
|
20
|
+
def generate
|
21
|
+
mr_ifindex + mr_type + mr_alen + mr_address
|
22
|
+
end
|
23
|
+
|
24
|
+
def mr_ifindex
|
25
|
+
@mr_ifindex ||= [[index].pack('c')].pack('a4')
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def index
|
31
|
+
::Socket.getifaddrs.find { |ifaddr| ifaddr.name == @ifname }&.ifindex
|
32
|
+
end
|
33
|
+
|
34
|
+
def mr_type
|
35
|
+
@mr_type ||= [PACKET_MR_PROMISC].pack('S')
|
36
|
+
end
|
37
|
+
|
38
|
+
def mr_alen
|
39
|
+
@mr_alen ||= [0].pack('S')
|
40
|
+
end
|
41
|
+
|
42
|
+
def mr_address
|
43
|
+
@mr_address ||= [0].pack('C') * 8
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Redhound
|
6
|
+
class Receiver
|
7
|
+
class << self
|
8
|
+
def run(ifname:)
|
9
|
+
new(ifname:).run
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(ifname:)
|
14
|
+
@ifname = ifname
|
15
|
+
@socket = SocketBuilder.build(ifname:)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
loop do
|
20
|
+
msg, = @socket.recvfrom(2048)
|
21
|
+
Analyzer.analyze(msg:)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Redhound
|
6
|
+
class SocketBuilder
|
7
|
+
class << self
|
8
|
+
def build(ifname:)
|
9
|
+
new(ifname:).build
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
SOL_PACKET = 0x0107 # bits/socket.h
|
14
|
+
PACKET_ADD_MEMBERSHIP = 0x0001 # NOTE: netpacket/packet.h
|
15
|
+
ETH_P_ALL = 768 # NOTE: htons(ETH_P_ALL) => linux/if_ether.h
|
16
|
+
PACKED_ETH_P_ALL = [ETH_P_ALL].pack('S').unpack1('S>')
|
17
|
+
|
18
|
+
def initialize(ifname:)
|
19
|
+
@ifname = ifname
|
20
|
+
@mq_req = PacketMreq.new(ifname: @ifname)
|
21
|
+
end
|
22
|
+
|
23
|
+
def build
|
24
|
+
socket = Socket.new(Socket::AF_PACKET, Socket::SOCK_RAW, ETH_P_ALL)
|
25
|
+
socket.bind([Socket::AF_PACKET, PACKED_ETH_P_ALL, @mq_req.mr_ifindex].pack('SS>a16'))
|
26
|
+
socket.setsockopt(SOL_PACKET, PACKET_ADD_MEMBERSHIP, @mq_req.generate)
|
27
|
+
socket
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/redhound.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'redhound/analyzer'
|
4
|
+
require_relative 'redhound/command'
|
5
|
+
require_relative 'redhound/header'
|
6
|
+
require_relative 'redhound/packet_mreq'
|
7
|
+
require_relative 'redhound/receiver'
|
8
|
+
require_relative 'redhound/socket_builder'
|
9
|
+
require_relative 'redhound/version'
|
data/sig/redhound.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redhound
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yudai Takada
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2024-11-05 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: Redhound is a pure Ruby packet analyzer that can be used to capture and
|
13
|
+
analyze network packets.
|
14
|
+
email:
|
15
|
+
- t.yudai92@gmail.com
|
16
|
+
executables:
|
17
|
+
- redhound
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- CHANGELOG.md
|
22
|
+
- CODE_OF_CONDUCT.md
|
23
|
+
- Dockerfile
|
24
|
+
- LICENSE.txt
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- docker-compose.yml
|
28
|
+
- exe/redhound
|
29
|
+
- lib/redhound.rb
|
30
|
+
- lib/redhound/analyzer.rb
|
31
|
+
- lib/redhound/command.rb
|
32
|
+
- lib/redhound/header.rb
|
33
|
+
- lib/redhound/header/ether.rb
|
34
|
+
- lib/redhound/header/icmp.rb
|
35
|
+
- lib/redhound/header/ipv4.rb
|
36
|
+
- lib/redhound/header/udp.rb
|
37
|
+
- lib/redhound/packet_mreq.rb
|
38
|
+
- lib/redhound/receiver.rb
|
39
|
+
- lib/redhound/socket_builder.rb
|
40
|
+
- lib/redhound/version.rb
|
41
|
+
- sig/redhound.rbs
|
42
|
+
homepage: https://github.com/ydah/redhound
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata:
|
46
|
+
homepage_uri: https://github.com/ydah/redhound
|
47
|
+
source_code_uri: https://github.com/ydah/redhound
|
48
|
+
documentation_uri: https://github.com/ydah/redhound
|
49
|
+
changelog_uri: https://github.com/ydah/redhound/releases
|
50
|
+
bug_tracker_uri: https://github.com/ydah/redhound/issues
|
51
|
+
rubygems_mfa_required: 'true'
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '3.3'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubygems_version: 3.6.0.dev
|
67
|
+
specification_version: 4
|
68
|
+
summary: Pure Ruby packet analyzer
|
69
|
+
test_files: []
|