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 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