net-natpmp 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +6 -0
- data/lib/net/natpmp/client.rb +42 -0
- data/lib/net/natpmp/config.rb +29 -0
- data/lib/net/natpmp/constants.rb +55 -0
- data/lib/net/natpmp/errors.rb +38 -0
- data/lib/net/natpmp/requests.rb +105 -0
- data/lib/net/natpmp/responses.rb +52 -0
- data/lib/net/natpmp.rb +27 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9450b999925ebdbe97f8cf23f24bbdc5dc5177bc29a4782a88b667ab5bbeeb06
|
|
4
|
+
data.tar.gz: b0afc58710a87288c518878a7079583651c0d10f3eba5ac1b3c17a7b9b2b91e9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d5aac53925fc713efbebe52bfc94f3f2b8ea632e7fe407e828c02e367aa6b9509a519b6a0117b66e0c75c6e234085933464bd8a5accd17f5bab728da12213dc4
|
|
7
|
+
data.tar.gz: cd672c6ca071227b65d9aac5e74f3f7db5dd3cf0d4ddb2d2a879fb9f9b85d499bba22e67616f62c63ed265486ee99d5b7b1138470a2527bb0cfd33ee02e80306
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Octavian Vaideanu
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module NATPMP
|
|
5
|
+
# Client class
|
|
6
|
+
class Client
|
|
7
|
+
include Constants
|
|
8
|
+
|
|
9
|
+
attr_reader :config
|
|
10
|
+
|
|
11
|
+
def initialize(config)
|
|
12
|
+
@config = config
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Replies with the external address of the gateway
|
|
16
|
+
def external_address
|
|
17
|
+
ExternalAddressRequest.req(@config)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Maps a port on the gateway
|
|
21
|
+
def map_port(
|
|
22
|
+
proto: DEFAULT_PROTO,
|
|
23
|
+
inside_port: DEFAULT_INSIDE_PORT,
|
|
24
|
+
outside_port: DEFAULT_OUTSIDE_PORT,
|
|
25
|
+
lifetime: DEFAULT_LIFETIME
|
|
26
|
+
)
|
|
27
|
+
MappingRequest.req(
|
|
28
|
+
@config,
|
|
29
|
+
proto: proto,
|
|
30
|
+
inside_port: inside_port,
|
|
31
|
+
outside_port: outside_port,
|
|
32
|
+
lifetime: lifetime
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Destroys a port mapping on the gateway
|
|
37
|
+
def destroy_mapping(port: 0, proto: DEFAULT_PROTO)
|
|
38
|
+
MappingRequest.req(@config, proto: proto, inside_port: port, outside_port: 0, lifetime: 0)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module NATPMP
|
|
5
|
+
# Provides all necessary configuration with some defaults
|
|
6
|
+
class Config
|
|
7
|
+
NATPMP_PORT = 5351
|
|
8
|
+
BIND_ADDRESS = '0.0.0.0'
|
|
9
|
+
BIND_PORT = 5350
|
|
10
|
+
|
|
11
|
+
attr_reader :bind_address, :bind_port, :gw, :port
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
gw:,
|
|
15
|
+
port: NATPMP_PORT,
|
|
16
|
+
bind_address: BIND_ADDRESS,
|
|
17
|
+
bind_port: BIND_PORT
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@bind_address = IPAddr.new(bind_address)
|
|
21
|
+
@bind_port = bind_port
|
|
22
|
+
@gw = IPAddr.new(gw)
|
|
23
|
+
@port = port
|
|
24
|
+
rescue IPAddr::InvalidAddressError
|
|
25
|
+
raise InvalidParameter, 'Value must be a valid IP Address'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module NATPMP
|
|
5
|
+
module Constants
|
|
6
|
+
VERSION = 0
|
|
7
|
+
|
|
8
|
+
PROTO_CODES = {
|
|
9
|
+
udp: :map_udp,
|
|
10
|
+
tcp: :map_tcp
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
OP_CODES = {
|
|
14
|
+
address: 0,
|
|
15
|
+
map_udp: 1,
|
|
16
|
+
map_tcp: 2
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# 0 - Success
|
|
20
|
+
# 1 - Unsupported Version
|
|
21
|
+
# 2 - Not Authorized/Refused
|
|
22
|
+
# (e.g., box supports mapping, but user has turned feature off)
|
|
23
|
+
# 3 - Network Failure
|
|
24
|
+
# (e.g., NAT box itself has not obtained a DHCP lease)
|
|
25
|
+
# 4 - Out of resources
|
|
26
|
+
# (NAT box cannot create any more mappings at this time)
|
|
27
|
+
# 5 - Unsupported opcode
|
|
28
|
+
RESULT_CODES = {
|
|
29
|
+
success: 0,
|
|
30
|
+
unsupported_version: 1,
|
|
31
|
+
not_authorized: 2,
|
|
32
|
+
network_failure: 3,
|
|
33
|
+
out_of_resources: 4,
|
|
34
|
+
unsupported_opcode: 5
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
RESULT_CODES_DESC = {
|
|
38
|
+
1 => 'Unsupported Version',
|
|
39
|
+
2 => 'Not Authorized/Refused',
|
|
40
|
+
3 => 'Network Failure',
|
|
41
|
+
4 => 'Out of resources',
|
|
42
|
+
5 => 'Unsupported opcode'
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# This is the initial delay before doubling it
|
|
46
|
+
BASE_DELAY = 0.25
|
|
47
|
+
MAX_WAIT = 60
|
|
48
|
+
|
|
49
|
+
DEFAULT_INSIDE_PORT = 0
|
|
50
|
+
DEFAULT_OUTSIDE_PORT = 0
|
|
51
|
+
DEFAULT_LIFETIME = 7200
|
|
52
|
+
DEFAULT_PROTO = :udp
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module NATPMP
|
|
5
|
+
# General exception
|
|
6
|
+
class Exception < ::RuntimeError; end
|
|
7
|
+
|
|
8
|
+
class InvalidParameter < Exception; end
|
|
9
|
+
|
|
10
|
+
# Timeout exception
|
|
11
|
+
class TimeoutException < Exception
|
|
12
|
+
def initialize(msg = 'Request timed out')
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class InvalidVersion < Exception; end
|
|
18
|
+
|
|
19
|
+
class InvalidReply < Exception; end
|
|
20
|
+
|
|
21
|
+
# Exception should be thrown when the request fails
|
|
22
|
+
class RequestFailed < Exception
|
|
23
|
+
include Constants
|
|
24
|
+
|
|
25
|
+
def initialize(opts = {})
|
|
26
|
+
msg = opts[:msg] || 'Failed to send the request'
|
|
27
|
+
|
|
28
|
+
if (@result_code = opts[:result_code])
|
|
29
|
+
msg = "#{msg}. Server replied: #{RESULT_CODES_DESC[@result_code]}(#{@result_code})"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
super(msg)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ConnectionRefused < Exception; end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'ipaddr'
|
|
5
|
+
|
|
6
|
+
module Net
|
|
7
|
+
module NATPMP
|
|
8
|
+
# Main request class
|
|
9
|
+
class Request
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :socket, :config
|
|
13
|
+
|
|
14
|
+
def initialize(config)
|
|
15
|
+
@config = config
|
|
16
|
+
|
|
17
|
+
@socket = UDPSocket.new
|
|
18
|
+
@socket.bind(config.bind_address.to_s, config.bind_port)
|
|
19
|
+
@socket.connect(config.gw.to_s, config.port)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.req(config, _opts = {})
|
|
23
|
+
new(config)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Send a message to the NAT-PMP server. Takes a message and an optional response size
|
|
27
|
+
def send(msg, expected_response_size = 16)
|
|
28
|
+
sent_op = msg.unpack1('xC') # To verify the response
|
|
29
|
+
size_sent = @socket.send(msg, 0)
|
|
30
|
+
|
|
31
|
+
raise RequestFailed unless size_sent == msg.size
|
|
32
|
+
|
|
33
|
+
delay = Constants::BASE_DELAY
|
|
34
|
+
attempts = 1
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
sleep delay
|
|
38
|
+
reply, = @socket.recvfrom_nonblock(expected_response_size)
|
|
39
|
+
|
|
40
|
+
check_reply(reply, sent_op)
|
|
41
|
+
|
|
42
|
+
reply
|
|
43
|
+
rescue IO::WaitReadable
|
|
44
|
+
if delay < MAX_WAIT
|
|
45
|
+
delay *= 2
|
|
46
|
+
puts "Timeout, retrying after #{delay} seconds"
|
|
47
|
+
attempts += 1
|
|
48
|
+
retry
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
raise TimeoutException, "Timeout after #{attempts} attempts"
|
|
52
|
+
rescue Errno::ECONNREFUSED
|
|
53
|
+
raise ConnectionRefused, 'Connection refused'
|
|
54
|
+
end
|
|
55
|
+
ensure
|
|
56
|
+
sleep 0.25 # Sleep for a bit to make sure the socket is not closed too soon
|
|
57
|
+
@socket.close # Close the socket because we don't need it anymore
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def check_reply(reply, sent_op)
|
|
61
|
+
# Check the first 4 bytes only (the rest are variable)
|
|
62
|
+
version, opcode, result = reply.unpack('CCn')
|
|
63
|
+
|
|
64
|
+
# Check the version in the reply
|
|
65
|
+
raise InvalidVersion, "Invalid version #{version}" unless version == VERSION
|
|
66
|
+
|
|
67
|
+
# Check the operation code in the reply. Always (128 + sent opcode)
|
|
68
|
+
expected_opcode = 128 + sent_op
|
|
69
|
+
if opcode != expected_opcode
|
|
70
|
+
raise InvalidReply,
|
|
71
|
+
"Invalid reply opcode. Was expecting #{expected_opcode}, got: #{opcode}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check the result code in the reply
|
|
75
|
+
raise RequestFailed, result_code: result unless result == RESULT_CODES[:success]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Request class specific to External address
|
|
80
|
+
class ExternalAddressRequest < Request
|
|
81
|
+
def self.req(config)
|
|
82
|
+
instance = super(config)
|
|
83
|
+
msg = [VERSION, OP_CODES[:address]].pack('CC')
|
|
84
|
+
|
|
85
|
+
ExternalAddressResponse.new(instance.send(msg, 12))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Represents a port mapping request
|
|
90
|
+
class MappingRequest < Request
|
|
91
|
+
def self.req(config, proto:, inside_port:, outside_port:, lifetime:)
|
|
92
|
+
# TODO: Ensure proto is either :udp or :tcp
|
|
93
|
+
|
|
94
|
+
instance = super(config)
|
|
95
|
+
msg = [
|
|
96
|
+
VERSION, OP_CODES[PROTO_CODES[proto]], 0,
|
|
97
|
+
inside_port, outside_port,
|
|
98
|
+
lifetime
|
|
99
|
+
].pack('CCnnnN')
|
|
100
|
+
|
|
101
|
+
MappingResponse.new(instance.send(msg, 16), proto: proto)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module NATPMP
|
|
5
|
+
# Response class
|
|
6
|
+
class Response
|
|
7
|
+
attr_reader :raw_response
|
|
8
|
+
|
|
9
|
+
def initialize(response, _ = {})
|
|
10
|
+
@raw_response = response
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Response for the external address request
|
|
15
|
+
class ExternalAddressResponse < Response
|
|
16
|
+
attr_reader :ip_int
|
|
17
|
+
|
|
18
|
+
def initialize(response)
|
|
19
|
+
super
|
|
20
|
+
@ip_int = response.unpack1('@8N')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def address
|
|
24
|
+
IPAddr.new(ip_int, Socket::AF_INET)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_s
|
|
28
|
+
address.to_s
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def inspect
|
|
32
|
+
"External address: #{address}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Response for the mapping request
|
|
37
|
+
class MappingResponse < Response
|
|
38
|
+
attr_reader :inside_port, :outside_port, :lifetime, :proto
|
|
39
|
+
|
|
40
|
+
def initialize(response, proto:)
|
|
41
|
+
super
|
|
42
|
+
|
|
43
|
+
@proto = proto
|
|
44
|
+
@inside_port, @outside_port, @lifetime = response.unpack('@8nnN')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inspect
|
|
48
|
+
"Port mapping: #{@outside_port} -> #{@proto}:#{@inside_port} (lifetime: #{@lifetime})"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/net/natpmp.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'natpmp/constants'
|
|
4
|
+
require_relative 'natpmp/config'
|
|
5
|
+
require_relative 'natpmp/client'
|
|
6
|
+
require_relative 'natpmp/errors'
|
|
7
|
+
require_relative 'natpmp/requests'
|
|
8
|
+
require_relative 'natpmp/responses'
|
|
9
|
+
|
|
10
|
+
module Net
|
|
11
|
+
# Main class
|
|
12
|
+
module NATPMP
|
|
13
|
+
# Instantiate class with default params.
|
|
14
|
+
# Takes a config instance or any other params.
|
|
15
|
+
# :gw is mandatory if config instance not provided
|
|
16
|
+
def self.client(config)
|
|
17
|
+
return Client.new(config) if config.is_a?(Config)
|
|
18
|
+
|
|
19
|
+
unless config[:gw]
|
|
20
|
+
raise Net::NATPMP::Exception,
|
|
21
|
+
'Gateway is missing. Call with: Net::NATPMP.client(gw: "x.x.x.x")'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Client.new(Config.new(**config))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: net-natpmp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Octavian Vaideanu
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2024-07-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |2
|
|
14
|
+
A NAT-PMP client for Ruby.
|
|
15
|
+
This gem allows you to interact with NAT-PMP enabled routers to map ports and get the external IP address.
|
|
16
|
+
email: octav@devroot.dev
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/net/natpmp.rb
|
|
24
|
+
- lib/net/natpmp/client.rb
|
|
25
|
+
- lib/net/natpmp/config.rb
|
|
26
|
+
- lib/net/natpmp/constants.rb
|
|
27
|
+
- lib/net/natpmp/errors.rb
|
|
28
|
+
- lib/net/natpmp/requests.rb
|
|
29
|
+
- lib/net/natpmp/responses.rb
|
|
30
|
+
homepage: https://rubygems.org/gems/net-natpmp
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata: {}
|
|
34
|
+
post_install_message:
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.0'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 3.5.11
|
|
50
|
+
signing_key:
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: NAT-PMP client for Ruby
|
|
53
|
+
test_files: []
|