natpmp 0.8

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ class NATPMP
2
+ VERSION='0.8'
3
+ end
@@ -0,0 +1,9 @@
1
+ # Simple smoke test
2
+ # To be improved!
3
+ #
4
+ require 'natpmp.rb'
5
+
6
+ NATPMP.map 633, 13033, 30, :tcp do |map|
7
+ puts "Executing sleep 10 with #{map.inspect}"
8
+ sleep 10
9
+ end
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