commit f37d8e8283e38abb5d4f3e818ae73a6ed90ed1fb Author: Fijxu Date: Wed Dec 25 23:43:25 2024 -0300 init diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..86b560e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Fijxu + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..872b431 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# natpmpcrystal + +Work in progress library implementation of RFC6886 **NAT Port Mapping Protocol (NAT-PMP)** diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..ba7618e --- /dev/null +++ b/shard.yml @@ -0,0 +1,13 @@ +name: natpmpcrystal +version: 0.1.0 + +authors: + - Fijxu + +targets: + natpmpcrystal: + main: src/natpmpcrystal.cr + +crystal: '>= 1.14.0' + +license: MIT diff --git a/spec/natpmpcrystal_spec.cr b/spec/natpmpcrystal_spec.cr new file mode 100644 index 0000000..62a0e85 --- /dev/null +++ b/spec/natpmpcrystal_spec.cr @@ -0,0 +1,9 @@ +require "./spec_helper" + +describe Natpmpcrystal do + # TODO: Write tests + + it "works" do + false.should eq(true) + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..bd2c759 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/natpmpcrystal" diff --git a/src/natpmpcrystal.cr b/src/natpmpcrystal.cr new file mode 100644 index 0000000..d004902 --- /dev/null +++ b/src/natpmpcrystal.cr @@ -0,0 +1,144 @@ +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 + enum OP : UInt8 + UDP = 1_u8 + TCP = 2_u8 + end + + property vers : UInt8 = 0_u8 + property op : UInt8 + property reserved : UInt16 = 0_u16 + property internal_port : UInt16 + property external_port : UInt16 + property lifetime : UInt32 + + def initialize(@op = UDP, @internal_port = nil, @external_port = nil, @lifetime = 0) + end + end + + # private record MappingPacket, + # vers : UInt8 = 0, + # op : UInt8 = 0, + # reserved : UInt16 = 0, + # internal_port : UInt16 = 0, + # external_port : UInt16 = 0, + # lifetime : UInt32 = 0, + + class Client + @client : UDPSocket + @gateway_ip : String + + # Overload + def initialize(gateway_ip : URI) + initialize(gateway_ip.path) + end + + def initialize(gateway_ip : String) + @gateway_ip = gateway_ip + @client = UDPSocket.new + # A given host may have more than one independent + # NAT-PMP client running at the same time, and address announcements + # need to be available to all of them. Clients should therefore set + # the SO_REUSEPORT option or equivalent in order to allow other + # processes to also listen on port 5350. + @client.reuse_port = true + # Additionally, implementers + # have encountered issues when one or more processes on the same device + # listen to port 5350 on *all* addresses. Clients should therefore + # bind specifically to 224.0.0.1:5350, not to 0.0.0.0:5350. + # @client.bind("224.0.0.1", 5350) + connect() + end + + def connect + # @client.join_group(Socket::IPAddress.new("224.0.0.1", 5351)) + @client.connect(@gateway_ip, 5351) + end + + def send_public_address_request_raw : Bytes + @client.send("\x00\x00") + msg = Bytes.new(12) + @client.receive(msg) + return msg + end + + def send_public_address_request + @client.send("\x00\x00") + msg = Bytes.new(12) + @client.receive(msg) + vers = (msg[0]) + op = (msg[1]) + result_code = get_result_code(msg[2..3]) + epoch = get_epoch(msg[4..7]) + + # If the result code is non-zero, the value of the External + # IPv4 Address field is undefined (MUST be set to zero on transmission, + # and MUST be ignored on reception). + 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 + + # def request_mapping(op : Int32, internal_port : Int32, external_port : Int32, lifetime : Int32) + # begin + # request_mapping(op.to_u8, internal_port.to_u16, external_port.to_u16, lifetime.to_u32) + # rescue ex + # puts ex.message + # exit(1) + # end + # end + + def request_mapping(op : UInt8, internal_port : UInt16, external_port : UInt16, lifetime : UInt32) + request = MappingPacket.new(op, internal_port, external_port, lifetime) + end + + private def get_result_code(msg) + # Responses always contain a + # 16-bit result code in network byte order + return IO::ByteFormat::BigEndian.decode(UInt16, msg) + end + + # Seconds Since Start of Epoch + private def get_epoch(msg) + # epoch : Int32 = 0 + # msg.each_with_index do |byte, index| + # epoch |= (byte.to_i << (8 * (msg.size - 1 - index))) + # end + + # Responses also contain a 32-bit unsigned integer + # corresponding to the number of seconds since the NAT gateway was + # rebooted or since its port mapping state was otherwise reset. + return IO::ByteFormat::BigEndian.decode(UInt32, msg) + end + + private def get_ip_address(msg) + "#{msg[0]}.#{msg[1]}.#{msg[2]}.#{msg[3]}" + end + end +end + +client = NatPMP::Client.new("192.168.1.1") +pp client.send_public_address_request +xd = client.send_public_address_request_raw +pp xd + +request = client.request_mapping(NatPMP::MappingPacket::OP::UDP.value, 25580_u16, 25580_u16, 0_u32) + +# request2 = client.request_mapping(1, 255802, 25580, 0) + +pp request