0.1.0
This commit is contained in:
parent
60d77e7a3a
commit
8d8b42f9be
2 changed files with 63 additions and 97 deletions
|
@ -1,13 +1,9 @@
|
||||||
name: natpmpcrystal
|
name: natpmp-crystal
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Fijxu <fijxu@nadeko.net>
|
- Fijxu <fijxu@nadeko.net>
|
||||||
|
|
||||||
targets:
|
|
||||||
natpmpcrystal:
|
|
||||||
main: src/natpmpcrystal.cr
|
|
||||||
|
|
||||||
crystal: '>= 1.14.0'
|
crystal: '>= 1.14.0'
|
||||||
|
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
|
@ -11,26 +11,16 @@ module NatPMP
|
||||||
UNSUPPORTED_OPCODE = 5
|
UNSUPPORTED_OPCODE = 5
|
||||||
end
|
end
|
||||||
|
|
||||||
enum OP : UInt8
|
|
||||||
UDP = 1_u8
|
|
||||||
TCP = 2_u8
|
|
||||||
end
|
|
||||||
|
|
||||||
struct MappingPacket
|
struct MappingPacket
|
||||||
@vers : UInt8 = 0_u8
|
@vers : UInt8 = 0_u8
|
||||||
@op : UInt8 = OP::UDP.value
|
@op : UInt8
|
||||||
@reserved : UInt16 = 0_u16
|
@reserved : UInt16 = 0_u16
|
||||||
@internal_port : UInt16
|
@internal_port : UInt16
|
||||||
@external_port : UInt16
|
@external_port : UInt16
|
||||||
@lifetime : UInt32 = 0_u32
|
@lifetime : UInt32 = 0_u32
|
||||||
|
|
||||||
def initialize(@internal_port, @external_port, @lifetime = 0, @op = UDP)
|
def initialize(@internal_port, @external_port, @lifetime = 7200, @op = 1)
|
||||||
unless [1, 2].include?(@op)
|
raise ArgumentError.new("operation should be either 1_u8 for UDP or 2_u8 for TCP") if ![1, 2].includes?(@op)
|
||||||
raise ArgumentError, "Operation should be either '1_u8' for UDP or '2_u8' for TCP (default: UDP)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(@internal_port, @external_port)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_io
|
def to_io
|
||||||
|
@ -45,17 +35,17 @@ module NatPMP
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_slice
|
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]
|
slice = uninitialized UInt8[12]
|
||||||
IO::ByteFormat::BigEndian.encode(@vers, v = Bytes.new(1))
|
|
||||||
IO::ByteFormat::BigEndian.encode(@op, o = Bytes.new(1))
|
IO::ByteFormat::BigEndian.encode(@op, o = Bytes.new(1))
|
||||||
# IO::ByteFormat::BigEndian.encode(@reserved, r = Bytes.new(2))
|
|
||||||
IO::ByteFormat::BigEndian.encode(@internal_port, i = Bytes.new(2))
|
IO::ByteFormat::BigEndian.encode(@internal_port, i = Bytes.new(2))
|
||||||
IO::ByteFormat::BigEndian.encode(@external_port, e = Bytes.new(2))
|
IO::ByteFormat::BigEndian.encode(@external_port, e = Bytes.new(2))
|
||||||
IO::ByteFormat::BigEndian.encode(@lifetime, l = Bytes.new(4))
|
IO::ByteFormat::BigEndian.encode(@lifetime, l = Bytes.new(4))
|
||||||
slice[0] = v[0]
|
slice[0] = 0 # vers is always 0
|
||||||
slice[1] = o[0]
|
slice[1] = o[0]
|
||||||
slice[2] = 0 # RESERVED
|
slice[2] = 0 # RESERVED, always 0
|
||||||
slice[3] = 0 # RESERVED
|
slice[3] = 0 # RESERVED, always 0
|
||||||
slice[4] = i[0]
|
slice[4] = i[0]
|
||||||
slice[5] = i[1]
|
slice[5] = i[1]
|
||||||
slice[6] = e[0]
|
slice[6] = e[0]
|
||||||
|
@ -69,7 +59,7 @@ module NatPMP
|
||||||
end
|
end
|
||||||
|
|
||||||
class Client
|
class Client
|
||||||
@client : UDPSocket
|
@socket : UDPSocket
|
||||||
@gateway_ip : String
|
@gateway_ip : String
|
||||||
|
|
||||||
# Overload
|
# Overload
|
||||||
|
@ -77,41 +67,46 @@ module NatPMP
|
||||||
initialize(gateway_ip.path)
|
initialize(gateway_ip.path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(@gateway_ip : String)
|
def initialize(@gateway_ip : String, autoconnect : Bool = true)
|
||||||
@client = UDPSocket.new
|
# The specification is IPV4 only!
|
||||||
|
@socket = UDPSocket.new(Socket::Family::INET)
|
||||||
# A given host may have more than one independent
|
# A given host may have more than one independent
|
||||||
# NAT-PMP client running at the same time, and address announcements
|
# NAT-PMP client running at the same time, and address announcements
|
||||||
# need to be available to all of them. Clients should therefore set
|
# need to be available to all of them. Clients should therefore set
|
||||||
# the SO_REUSEPORT option or equivalent in order to allow other
|
# the SO_REUSEPORT option or equivalent in order to allow other
|
||||||
# processes to also listen on port 5350.
|
# processes to also listen on port 5350.
|
||||||
@client.reuse_port = true
|
@socket.reuse_port = true
|
||||||
|
@socket.reuse_address = true
|
||||||
# Additionally, implementers
|
# Additionally, implementers
|
||||||
# have encountered issues when one or more processes on the same device
|
# have encountered issues when one or more processes on the same device
|
||||||
# listen to port 5350 on *all* addresses. Clients should therefore
|
# 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.
|
# bind specifically to 224.0.0.1:5350, not to 0.0.0.0:5350.
|
||||||
# @client.bind("224.0.0.1", 5350)
|
@socket.bind 5350
|
||||||
|
if autoconnect
|
||||||
connect()
|
connect()
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def connect
|
def connect
|
||||||
# @client.join_group(Socket::IPAddress.new("224.0.0.1", 5351))
|
# @socket.join_group(Socket::IPAddress.new("224.0.0.1", 5351))
|
||||||
@client.connect(@gateway_ip, 5351)
|
@socket.connect(@gateway_ip, 5351)
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_public_address_request_raw : Bytes
|
def send_public_address_request_raw : Bytes
|
||||||
@client.send("\x00\x00")
|
@socket.send("\x00\x00")
|
||||||
msg = Bytes.new(12)
|
msg = Bytes.new(12)
|
||||||
@client.receive(msg)
|
@socket.receive(msg)
|
||||||
return msg
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_public_address_request
|
def send_public_address_request
|
||||||
@client.send("\x00\x00")
|
@socket.send("\x00\x00")
|
||||||
msg = Bytes.new(12)
|
msg = Bytes.new(12)
|
||||||
@client.read_timeout = 250.milliseconds
|
@socket.read_timeout = 250.milliseconds
|
||||||
|
|
||||||
8.times do |i|
|
8.times do |i|
|
||||||
begin
|
begin
|
||||||
@client.receive(msg)
|
@socket.receive(msg)
|
||||||
break
|
break
|
||||||
rescue IO::TimeoutError
|
rescue IO::TimeoutError
|
||||||
# If no
|
# If no
|
||||||
|
@ -119,7 +114,7 @@ module NatPMP
|
||||||
# client retransmits its request and waits 500 ms. The client SHOULD
|
# client retransmits its request and waits 500 ms. The client SHOULD
|
||||||
# repeat this process with the interval between attempts doubling each
|
# repeat this process with the interval between attempts doubling each
|
||||||
# time.
|
# time.
|
||||||
@client.read_timeout = @client.read_timeout.not_nil!*2
|
@socket.read_timeout = @socket.read_timeout.not_nil!*2
|
||||||
next
|
next
|
||||||
rescue
|
rescue
|
||||||
raise "The gateway '#{@gateway_ip}' does not support NAT-PMP"
|
raise "The gateway '#{@gateway_ip}' does not support NAT-PMP"
|
||||||
|
@ -127,10 +122,10 @@ module NatPMP
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
vers = msg[0]
|
vers : UInt8 = msg[0]
|
||||||
op = msg[1]
|
op : UInt8 = msg[1]
|
||||||
result_code = get_result_code(msg[2..3])
|
result_code = decode_msg(UInt16, msg[2..3])
|
||||||
epoch = get_epoch(msg[4..7])
|
epoch = decode_msg(UInt32, msg[4..7])
|
||||||
|
|
||||||
# If the result code is non-zero, the value of the External
|
# If the result code is non-zero, the value of the External
|
||||||
# IPv4 Address field is undefined (MUST be set to zero on transmission,
|
# IPv4 Address field is undefined (MUST be set to zero on transmission,
|
||||||
|
@ -143,72 +138,47 @@ module NatPMP
|
||||||
return vers, op, result_code, epoch, ip_address
|
return vers, op, result_code, epoch, ip_address
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_mapping(internal_port : UInt16, external_port : UInt16, lifetime : Uint32, operation : UInt8)
|
def request_mapping(internal_port : UInt16, external_port : UInt16, operation : UInt8, lifetime : UInt32 = 7200)
|
||||||
request = MappingPacket.new(internal_port, external_port, lifetime, operation)
|
request = MappingPacket.new(internal_port, external_port, lifetime, operation).to_slice
|
||||||
msg = Bytes.new(16)
|
msg = Bytes.new(16)
|
||||||
@client.receive(msg)
|
@socket.send(request)
|
||||||
vers = msg[0]
|
@socket.receive(msg)
|
||||||
op = msg[1]
|
|
||||||
result_code = get_result_code(msg[2..3])
|
vers : UInt8 = msg[0]
|
||||||
epoch = get_epoch(msg[4..7])
|
op : UInt8 = msg[1]
|
||||||
internal_port = get_port(msg[8..9])
|
result_code = decode_msg(UInt16, msg[2..3])
|
||||||
external_port = get_port(msg[10..11])
|
epoch = decode_msg(UInt32, msg[4..7])
|
||||||
lifetime = get_lifetime(msg[12..15])
|
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
|
return vers, op, result_code, epoch, internal_port, external_port, lifetime
|
||||||
end
|
end
|
||||||
|
|
||||||
private def get_result_code(msg)
|
# https://datatracker.ietf.org/doc/html/rfc6886#section-3.4
|
||||||
# Responses always contain a
|
def destroy_mapping(internal_port : UInt16, operation : UInt8)
|
||||||
# 16-bit result code in network byte order
|
request = MappingPacket.new(internal_port, 0, 0, operation).to_slice
|
||||||
return IO::ByteFormat::BigEndian.decode(UInt16, msg)
|
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
|
end
|
||||||
|
|
||||||
# Seconds Since Start of Epoch
|
private macro decode_msg(type, msg)
|
||||||
private def get_epoch(msg)
|
IO::ByteFormat::BigEndian.decode({{type}}, {{msg}})
|
||||||
# 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_port(msg)
|
|
||||||
return IO::ByteFormat::BigEndian.decode(UInt16, msg)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def get_ip_address(msg)
|
private def get_ip_address(msg)
|
||||||
"#{msg[0]}.#{msg[1]}.#{msg[2]}.#{msg[3]}"
|
"#{msg[0]}.#{msg[1]}.#{msg[2]}.#{msg[3]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# client = NatPMP::Client.new("192.168.0.1")
|
|
||||||
# pp client.send_public_address_request
|
|
||||||
pp mapping_packet = NatPMP::MappingPacket.new(25555,25555)
|
|
||||||
|
|
||||||
Benchmark.ips do |x|
|
|
||||||
x.report("bytes") do
|
|
||||||
mapping_packet.to_io
|
|
||||||
end
|
|
||||||
|
|
||||||
x.report("staticarray") do
|
|
||||||
# pp mapping_packet.to_io.to_slice
|
|
||||||
mapping_packet.to_slice
|
|
||||||
end
|
|
||||||
|
|
||||||
x.report("staticarray to io") do
|
|
||||||
# pp mapping_packet.to_io.to_slice
|
|
||||||
mapping_packet.to_slice
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
pp typeof(mapping_packet)
|
|
||||||
|
|
||||||
# xd = client.send_public_address_request_raw
|
|
||||||
# pp xd
|
|
||||||
|
|
||||||
# request = client.request_mapping(25580, 25580)
|
|
||||||
|
|
||||||
# request2 = client.request_mapping(1, 255802, 25580, 0)
|
|
||||||
|
|
||||||
# pp request
|
|
Loading…
Reference in a new issue