natpmp 0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|