natpmp 0.8
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/bin/natpmp +86 -0
- data/lib/natpmp.rb +138 -0
- data/lib/natpmp/version.rb +3 -0
- data/test/test_natpmp.rb +9 -0
- metadata +50 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b79a17201396b0daf760a7454fd50ca223fdd6ab
|
4
|
+
data.tar.gz: db52b1791207f0330e968f380afdab5238160327
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d287cc858eaecd16279e928acd2a63bc74c9c1f207494ec81d8480c25a74f86bba9ba37bc362da4a64d018fce9ee14d0a4961557d2837884f28abb333a24572d
|
7
|
+
data.tar.gz: 7436eb03437d9a262c7a97df52d14a45dd1c0061ded6a6a9ba1566a7dee0fad5cedb107b72bf85a85606100bd9b0fa84354011f6202359160c7c835a9048dcad
|
data/bin/natpmp
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
require 'natpmp'
|
4
|
+
require 'natpmp/version'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
# TODO Extend the mapping if the command takes longer
|
9
|
+
# than the timeout.
|
10
|
+
|
11
|
+
types = [:tcp, :udp]
|
12
|
+
|
13
|
+
opts = OpenStruct.new(port: nil, type: :tcp, verbose: false, public: 0, ttl: 7200)
|
14
|
+
|
15
|
+
OptionParser.new do |o|
|
16
|
+
o.banner = "usage: #{o.program_name} [options] [command]"
|
17
|
+
o.separator "Open a port on a NAT-PMP gateway. Options:"
|
18
|
+
|
19
|
+
o.on_tail( '-?', '--help', 'Display this screen' ) do
|
20
|
+
puts o
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
o.on('-t', '--type TYPE', types, "Type: #{types.join(', ')} (default: #{opts.type})") do |t|
|
24
|
+
opts.type = t
|
25
|
+
end
|
26
|
+
o.on('-p', '--port PORT', Integer, "Private port (default: auto)") do |n|
|
27
|
+
opts.port = n
|
28
|
+
end
|
29
|
+
o.on('--ttl TIME', Integer, "TTL if no command (default #{opts.ttl} sec)") do |n|
|
30
|
+
opts.ttl = n
|
31
|
+
end
|
32
|
+
o.on('-P', '--public PUBPORT', Integer, "External port (default: auto)") do |n|
|
33
|
+
opts.public = n
|
34
|
+
end
|
35
|
+
o.on('-v', '--verbose', "Verbose") do
|
36
|
+
opts.verbose = true
|
37
|
+
end
|
38
|
+
o.on('--version', "Version") do
|
39
|
+
STDERR.puts "#{o.program_name}: Version #{NATPMP::VERSION}"
|
40
|
+
exit;
|
41
|
+
end
|
42
|
+
o.separator "In the command string the following substitutions will be made:"
|
43
|
+
o.separator " %p the local port"
|
44
|
+
o.separator " %h the local IP address"
|
45
|
+
o.separator " %P the gateway port"
|
46
|
+
o.separator " %H the gateway IP address"
|
47
|
+
o.separator "(Use %% to avoid this)"
|
48
|
+
o.separator "The mapping will be closed on completion of the command"
|
49
|
+
o.parse! rescue (STDERR.puts "#{o.program_name}: #{$!}\n#{o.to_s}"; exit)
|
50
|
+
end
|
51
|
+
|
52
|
+
NATPMP.verbose opts.verbose
|
53
|
+
|
54
|
+
unless opts.port
|
55
|
+
require 'socket'
|
56
|
+
p = Addrinfo.send(opts.type, "0.0.0.0", 0).bind
|
57
|
+
opts.localhost, opts.port = p.local_address.ip_unpack
|
58
|
+
STDERR.puts "Local port: #{opts.port}" if opts.verbose
|
59
|
+
end
|
60
|
+
|
61
|
+
if opts.verbose
|
62
|
+
begin
|
63
|
+
STDERR.puts "Gateway: #{NATPMP.GW}"
|
64
|
+
STDERR.puts "External IP: #{NATPMP.addr}"
|
65
|
+
rescue
|
66
|
+
STDERR.puts "#{opts.programname}: Error #{$!}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if ARGV.size > 0
|
71
|
+
command = ARGV.join(' ')
|
72
|
+
NATPMP.map opts.port, opts.public, opts.ttl, opts.type do |map|
|
73
|
+
command.gsub! /(?<!%)%p/, map.priv.to_s
|
74
|
+
command.gsub! /(?<!%)%h/, opts.localhost.to_s
|
75
|
+
command.gsub! /(?<!%)%P/, map.mapped.to_s
|
76
|
+
command.gsub! /(?<!%)%H/, NATPMP.addr
|
77
|
+
command.gsub! /%%([hpHP])/,'%\\1'
|
78
|
+
STDERR.puts "Executing: #{command}" if opts.verbose
|
79
|
+
system command
|
80
|
+
end
|
81
|
+
else
|
82
|
+
puts "noarg"
|
83
|
+
map = NATPMP.map opts.port, opts.public, opts.ttl, opts.type
|
84
|
+
end
|
85
|
+
|
86
|
+
# vim: ft=ruby sts=2 sw=2 ts=8
|
data/lib/natpmp.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# Simple NAT-PMP client
|
2
|
+
# See: http://tools.ietf.org/html/rfc6886
|
3
|
+
#
|
4
|
+
require 'socket'
|
5
|
+
|
6
|
+
class NATPMP
|
7
|
+
PMP_VERSION = 0
|
8
|
+
DEFAULT_LIFETIME = 7200
|
9
|
+
DELAY_MSEC = 250
|
10
|
+
MAX_WAIT_SEC = 64
|
11
|
+
SERVER_PORT = 5351
|
12
|
+
CLIENT_PORT = 5350
|
13
|
+
|
14
|
+
OPCODE = { addr: 0, udp: 1, tcp: 2 }
|
15
|
+
|
16
|
+
# Return codes
|
17
|
+
#
|
18
|
+
RETCODE = { success: 0, # Success
|
19
|
+
unsupported: 1, # Unsupported Version
|
20
|
+
refused: 2, # Not Authorized/Refused (e.g., box supports mapping, but user has turned feature off)
|
21
|
+
failed: 3, # Network Failure (e.g., NAT box itself has not obtained a DHCP lease)
|
22
|
+
exhausted: 4, # Out of resources (NAT box cannot create any more mappings at this time)
|
23
|
+
opnotsupp: 5 # Unsupported opcode
|
24
|
+
}
|
25
|
+
|
26
|
+
# Determine the default gateway
|
27
|
+
#
|
28
|
+
def self.GW
|
29
|
+
return @gw if @gw
|
30
|
+
@gw = case RUBY_PLATFORM
|
31
|
+
when /darwin/
|
32
|
+
routes = `netstat -nrf inet`.split("\n").select{|l| l=~/^default/}
|
33
|
+
raise "Can't find default route" unless routes.size > 0
|
34
|
+
routes.first.split(/\s+/)[1]
|
35
|
+
when /linux/
|
36
|
+
routes = `ip route list match 0.0.0.0`.split("\n").select{|l| l =~ /^default/}
|
37
|
+
raise "Can't find default route" unless routes.size > 0
|
38
|
+
routes.first.split(/\s+/)[2]
|
39
|
+
else
|
40
|
+
raise "Platform not supported!"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.verbose flag = true
|
45
|
+
@verbose = flag
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.verbose?
|
49
|
+
return @verbose
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.send msg
|
53
|
+
sop = msg.unpack("xC").first
|
54
|
+
sock = UDPSocket.open
|
55
|
+
sock.connect(NATPMP.GW, SERVER_PORT)
|
56
|
+
cb = sock.send(msg, 0)
|
57
|
+
raise "Couldn't send!" unless cb == msg.size
|
58
|
+
delay = DELAY_MSEC/1000.0
|
59
|
+
begin
|
60
|
+
sleep delay # to give time for the response to arrive!
|
61
|
+
(reply, sendinfo) = sock.recvfrom_nonblock(16)
|
62
|
+
sender = Addrinfo.new sendinfo
|
63
|
+
raise "Being spoofed!" unless sender.ip_address == NATPMP.GW
|
64
|
+
(ver,op,res) = reply.unpack("CCn")
|
65
|
+
raise "Invalid version #{ver}" unless ver == PMP_VERSION
|
66
|
+
raise "Invalid reply opcode #{op}" unless op == 128 + sop
|
67
|
+
raise "Request failed (code #{RETCODE.key(res)})" unless res == RETCODE[:success]
|
68
|
+
return reply
|
69
|
+
rescue IO::WaitReadable
|
70
|
+
if delay < MAX_WAIT_SEC
|
71
|
+
puts "Retrying after #{delay}..." if NATPMP.verbose?
|
72
|
+
delay *= 2
|
73
|
+
retry
|
74
|
+
end
|
75
|
+
raise "Waited too long, got no response"
|
76
|
+
rescue Errno::ECONNREFUSED
|
77
|
+
raise "Remote NATPMP server not found"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return the externally facing IPv4 address
|
82
|
+
#
|
83
|
+
def self.addr
|
84
|
+
reply = self.send [0, OPCODE[:addr]].pack("CC")
|
85
|
+
sssoe = reply.unpack("x4N").first
|
86
|
+
addr = reply.unpack("x8CCCC")
|
87
|
+
return addr.join('.')
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :priv, :pub, :mapped, :maxlife, :life, :type
|
91
|
+
|
92
|
+
def initialize priv, pub, maxlife, type
|
93
|
+
@priv = priv
|
94
|
+
@pub = pub
|
95
|
+
@maxlife = maxlife
|
96
|
+
raise "Time must be >= 0" if maxlife < 0
|
97
|
+
@type = type
|
98
|
+
|
99
|
+
# These are filled in when a request is made
|
100
|
+
#
|
101
|
+
@life = 0
|
102
|
+
@mapped = 0
|
103
|
+
end
|
104
|
+
|
105
|
+
# See section 3.3
|
106
|
+
def request!
|
107
|
+
rsp = NATPMP.send [0, OPCODE[@type], 0, @priv, @pub, @maxlife].pack("CCnnnN")
|
108
|
+
(sssoe, priv, @mapped, @life) = rsp.unpack("x4NnnN")
|
109
|
+
raise "Port mismatch: requested #{@priv} received #{priv}" if @priv != priv
|
110
|
+
STDERR.puts "Mapped #{inspect}" if NATPMP.verbose
|
111
|
+
end
|
112
|
+
|
113
|
+
# See section 3.4
|
114
|
+
def revoke!
|
115
|
+
rsp = NATPMP.send [0, OPCODE[@type], 0, @priv, 0, 0].pack("CCnnnN")
|
116
|
+
STDERR.puts "Revoked #{inspect}" if NATPMP.verbose
|
117
|
+
end
|
118
|
+
|
119
|
+
def inspect
|
120
|
+
"#{NATPMP.GW}:#{@mapped}->#{@type}:#{@priv} (#{@life} sec)"
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.map priv, pub, maxlife = DEFAULT_LIFETIME, type = :tcp, &block
|
124
|
+
|
125
|
+
map = NATPMP.new(priv, pub, maxlife, type)
|
126
|
+
map.request!
|
127
|
+
if block_given?
|
128
|
+
begin
|
129
|
+
yield map
|
130
|
+
ensure
|
131
|
+
map.revoke!
|
132
|
+
map = nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
return map
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
data/test/test_natpmp.rb
ADDED
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: natpmp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.8'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Townsend
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Interface to NAT-PMP. Portable between Mac OS X and CentOS 5+.
|
14
|
+
email:
|
15
|
+
- nick.townsend@mac.com
|
16
|
+
executables:
|
17
|
+
- natpmp
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/natpmp.rb
|
22
|
+
- bin/natpmp
|
23
|
+
- lib/natpmp/version.rb
|
24
|
+
- test/test_natpmp.rb
|
25
|
+
homepage: https://github.com/townsen/c-pod/
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.9.2
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 2.0.5
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: Encapsulate NAT-PMP protocol
|
49
|
+
test_files:
|
50
|
+
- test/test_natpmp.rb
|