0.1.0
This commit is contained in:
commit
99a079f5cb
6 changed files with 258 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Fijxu <fijxu@nadeko.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
50
README.md
Normal file
50
README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
# natpmp-crystal
|
||||
|
||||
Work in progress implementation of RFC 6886 **NAT Port Mapping Protocol (NAT-PMP)**, client side.
|
||||
|
||||
Since RFC 6886 is a pretty simple protocol, it should be ready to use to requests mappings
|
||||
|
||||
## Installation
|
||||
|
||||
1. Add the dependency to your `shard.yml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
test:
|
||||
github: fijxu/natpmp-crystal
|
||||
```
|
||||
|
||||
2. Run `shards install`
|
||||
|
||||
## Usage
|
||||
|
||||
```crystal
|
||||
# Create NAT-PMP client, replace the IP by the IP of your
|
||||
# gateway
|
||||
client = NatPMP::Client.new("192.168.1.1")
|
||||
# Public address request
|
||||
client.send_public_address_request # => {0, 128, 0, 22758, "104.0.0.0"}
|
||||
client.send_public_address_request_raw # => Bytes[0, 128, 0, 0, 0, 0, 88, 230, 104, 0, 0, 0]
|
||||
|
||||
# Maps the internal port 25565 to external port 25565, TCP
|
||||
client.request_mapping(25565, 25565, 2) # => {0, 130, 0, 22758, 25565, 25565, 7200}
|
||||
# Destroys the mapping with internal port 25565, TCP
|
||||
client.destroy_mapping(25565, 2) # => {0, 130, 0, 22758, 25565, 0, 0}
|
||||
|
||||
# Maps the internal port 22000 to external port 22000, UDP
|
||||
client.request_mapping(22000, 22000, 1) # => {0, 129, 0, 22758, 22000, 22000, 7200}
|
||||
# Destroys the mapping with internal port 25565, TCP
|
||||
client.destroy_mapping(22000, 1) # => {0, 129, 0, 22758, 22000, 0, 0}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it (<https://github.com/fijxu/natpmp-crystal/fork>)
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
5. Create a new Pull Request
|
||||
|
||||
## Contributors
|
||||
|
||||
- [Fijxu](https://github.com/fijxu) - creator and maintainer
|
9
shard.yml
Normal file
9
shard.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
name: natpmp-crystal
|
||||
version: 0.1.0
|
||||
|
||||
authors:
|
||||
- Fijxu <fijxu@nadeko.net>
|
||||
|
||||
crystal: '>= 1.14.0'
|
||||
|
||||
license: MIT
|
9
spec/natpmpcrystal_spec.cr
Normal file
9
spec/natpmpcrystal_spec.cr
Normal file
|
@ -0,0 +1,9 @@
|
|||
require "./spec_helper"
|
||||
|
||||
describe Natpmpcrystal do
|
||||
# TODO: Write tests
|
||||
|
||||
it "works" do
|
||||
false.should eq(true)
|
||||
end
|
||||
end
|
2
spec/spec_helper.cr
Normal file
2
spec/spec_helper.cr
Normal file
|
@ -0,0 +1,2 @@
|
|||
require "spec"
|
||||
require "../src/natpmpcrystal"
|
167
src/natpmp.cr
Normal file
167
src/natpmp.cr
Normal file
|
@ -0,0 +1,167 @@
|
|||
require "socket"
|
||||
require "benchmark"
|
||||
|
||||
module NatPMP
|
||||
enum ResultCodes
|
||||
SUCCESS = 0
|
||||
UNSUPPORTED_VERSION = 1
|
||||
NOT_AUTHORIZED_OR_REFUSED = 2
|
||||
NETWORK_FAILURE = 3
|
||||
OUT_OF_RESOURCES = 4
|
||||
UNSUPPORTED_OPCODE = 5
|
||||
end
|
||||
|
||||
struct MappingPacket
|
||||
@vers : UInt8 = 0_u8
|
||||
@op : UInt8
|
||||
@reserved : UInt16 = 0_u16
|
||||
@internal_port : UInt16
|
||||
@external_port : UInt16
|
||||
@lifetime : UInt32 = 0_u32
|
||||
|
||||
def initialize(@internal_port, @external_port, @lifetime = 7200, @op = 1)
|
||||
raise ArgumentError.new("operation should be either 1_u8 for UDP or 2_u8 for TCP") if ![1, 2].includes?(@op)
|
||||
end
|
||||
|
||||
def to_io
|
||||
io = IO::Memory.new(12)
|
||||
io.write_bytes(@vers, IO::ByteFormat::BigEndian)
|
||||
io.write_bytes(@op, IO::ByteFormat::BigEndian)
|
||||
io.write_bytes(@reserved, IO::ByteFormat::BigEndian)
|
||||
io.write_bytes(@internal_port, IO::ByteFormat::BigEndian)
|
||||
io.write_bytes(@external_port, IO::ByteFormat::BigEndian)
|
||||
io.write_bytes(@lifetime, IO::ByteFormat::BigEndian)
|
||||
return io
|
||||
end
|
||||
|
||||
def to_slice
|
||||
# This is not actually a Slice, it's an StaticArray so I don't
|
||||
# think this member function should be called like this.
|
||||
slice = uninitialized UInt8[12]
|
||||
IO::ByteFormat::BigEndian.encode(@op, o = Bytes.new(1))
|
||||
IO::ByteFormat::BigEndian.encode(@internal_port, i = Bytes.new(2))
|
||||
IO::ByteFormat::BigEndian.encode(@external_port, e = Bytes.new(2))
|
||||
IO::ByteFormat::BigEndian.encode(@lifetime, l = Bytes.new(4))
|
||||
slice[0] = 0 # vers is always 0
|
||||
slice[1] = o[0]
|
||||
slice[2] = 0 # RESERVED, always 0
|
||||
slice[3] = 0 # RESERVED, always 0
|
||||
slice[4] = i[0]
|
||||
slice[5] = i[1]
|
||||
slice[6] = e[0]
|
||||
slice[7] = e[1]
|
||||
slice[8] = l[0]
|
||||
slice[9] = l[1]
|
||||
slice[10] = l[2]
|
||||
slice[11] = l[3]
|
||||
return slice
|
||||
end
|
||||
end
|
||||
|
||||
class Client
|
||||
@socket : UDPSocket
|
||||
@gateway_ip : String
|
||||
|
||||
# Overload
|
||||
def initialize(gateway_ip : URI)
|
||||
initialize(gateway_ip.path)
|
||||
end
|
||||
|
||||
def initialize(@gateway_ip : String, autoconnect : Bool = true)
|
||||
# The specification is IPV4 only!
|
||||
@socket = UDPSocket.new(Socket::Family::INET)
|
||||
@socket.reuse_port = true
|
||||
@socket.reuse_address = true
|
||||
@socket.bind 5350
|
||||
if autoconnect
|
||||
connect()
|
||||
end
|
||||
end
|
||||
|
||||
def connect
|
||||
@socket.connect(@gateway_ip, 5351)
|
||||
end
|
||||
|
||||
def send_public_address_request_raw : Bytes
|
||||
@socket.send("\x00\x00")
|
||||
msg = Bytes.new(12)
|
||||
@socket.receive(msg)
|
||||
return msg
|
||||
end
|
||||
|
||||
def send_public_address_request
|
||||
@socket.send("\x00\x00")
|
||||
msg = Bytes.new(12)
|
||||
@socket.read_timeout = 250.milliseconds
|
||||
|
||||
8.times do |i|
|
||||
begin
|
||||
@socket.receive(msg)
|
||||
break
|
||||
rescue IO::TimeoutError
|
||||
@socket.read_timeout = @socket.read_timeout.not_nil!*2
|
||||
next
|
||||
rescue
|
||||
raise "The gateway '#{@gateway_ip}' does not support NAT-PMP"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
vers : UInt8 = msg[0]
|
||||
op : UInt8 = msg[1]
|
||||
result_code = decode_msg(UInt16, msg[2..3])
|
||||
epoch = decode_msg(UInt32, msg[4..7])
|
||||
|
||||
if result_code != 0
|
||||
ip_address = nil
|
||||
else
|
||||
ip_address = get_ip_address(msg[8..11])
|
||||
end
|
||||
return vers, op, result_code, epoch, ip_address
|
||||
end
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc6886#section-3.3
|
||||
def request_mapping(internal_port : UInt16, external_port : UInt16, operation : UInt8, lifetime : UInt32 = 7200)
|
||||
request = MappingPacket.new(internal_port, external_port, lifetime, operation).to_slice
|
||||
msg = Bytes.new(16)
|
||||
@socket.send(request)
|
||||
@socket.receive(msg)
|
||||
|
||||
vers : UInt8 = msg[0]
|
||||
op : UInt8 = msg[1]
|
||||
result_code = decode_msg(UInt16, msg[2..3])
|
||||
epoch = decode_msg(UInt32, msg[4..7])
|
||||
internal_port = decode_msg(UInt16, msg[8..9])
|
||||
external_port = decode_msg(UInt16, msg[10..11])
|
||||
lifetime = decode_msg(UInt32, msg[12..15])
|
||||
|
||||
return vers, op, result_code, epoch, internal_port, external_port, lifetime
|
||||
end
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc6886#section-3.4
|
||||
def destroy_mapping(internal_port : UInt16, operation : UInt8)
|
||||
request = MappingPacket.new(internal_port, 0, 0, operation).to_slice
|
||||
msg = Bytes.new(16)
|
||||
@socket.send(request)
|
||||
@socket.receive(msg)
|
||||
|
||||
vers : UInt8 = msg[0]
|
||||
op : UInt8 = msg[1]
|
||||
result_code = decode_msg(UInt16, msg[2..3])
|
||||
epoch = decode_msg(UInt32, msg[4..7])
|
||||
internal_port = decode_msg(UInt16, msg[8..9])
|
||||
external_port = decode_msg(UInt16, msg[10..11])
|
||||
lifetime = decode_msg(UInt32, msg[12..15])
|
||||
|
||||
return vers, op, result_code, epoch, internal_port, external_port, lifetime
|
||||
end
|
||||
|
||||
private macro decode_msg(type, msg)
|
||||
IO::ByteFormat::BigEndian.decode({{type}}, {{msg}})
|
||||
end
|
||||
|
||||
private def get_ip_address(msg)
|
||||
"#{msg[0]}.#{msg[1]}.#{msg[2]}.#{msg[3]}"
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue