jgillich
jgillich

Reputation: 76309

Using IO#pos and IO#seek with UDPSocket

I have a UDPSocket instance:

io = UDPSocket.new
io.connect "8.8.4.4", 53

It connects to a DNS server over port 53, sends a DNS query and retrieves the result. DNS has built-in message compression in the form of pointers, see RFC 1035, 4.1.4. Message compression. Instead of a domain name, resource records can contain a message offset to point to the domain of the question. This is so the domain name doesn't have to be repeated for each record.

I have implemented this as follows in my resource record class:

 def self.from_io(io : IO, format : IO::ByteFormat) : self
    domain = ""
    loop do
      codepoint = UInt8.from_io io, format
      break if codepoint == 0
      if codepoint >= 192 # if the octet starts with 11 as defined in the rfc
        current_pos = io.pos
        pointer = UInt8.from_io io, format
        io.seek(pointer)
        # read the string...
      end
      # ...
    end
end

This does not work because UDPSocket does not implement IO#pos and IO#seek:

Unhandled exception: Unable to pos

To fix this, I have created a subclass that utilizes IO::Memory:

class DNS::DNSSocket < UDPSocket
  def initialize(family : Socket::Family = Socket::Family::INET)
    super family
    @memory = IO::Memory.new
  end

  def read(slice : Bytes)
    if slice.size + pos > @memory.size
      super slice
      @memory.write slice
    else
      @memory.read slice
    end
    slice.size
  end

  def pos
    @memory.pos
  end

  def pos=(value)
    @memory.pos = value
  end

  def seek(offset, whence : Seek = IO::Seek::Set)
    @memory.seek offset, whence
  end

  def clear
    @memory.clear
  end
end

My questions are as follows:

  1. Is this a good solution, or do you know of something more elegant?

  2. The IO::Memory instance needs to be reset after each message. Is it possible to call clear at the end or beginning of a datagram (packet) from within my DNSSocket implementation? I can also call it in my message parser but I'd prefer not to.

Upvotes: 0

Views: 38

Answers (2)

jgillich
jgillich

Reputation: 76309

To add to the other reply, the solution is much much simpler than I thought.

slice = Bytes.new(512)
socket.read slice
io = IO::Memory.new slice

Upvotes: 0

Johannes M&#252;ller
Johannes M&#252;ller

Reputation: 5661

UDP is a connectionless communication model and thus doesn't support streaming. UDPSocket inherits IO, but it shouldn't and I think that's actually a flaw in the API caused by Socket inheriting from IO. It still works, because the underlying syscalls used by the IO implementation also work with UDP sockets. But using a UDPSocket as an IO is not ideal and should be avoided. Since datagrams are usually short messages anyway, it's perfectly fine to load them entirely into memory.

So, I'd advise to use UDPSocket#receive instead, which allows you to easily wrap the slice in IO::Memory.

Upvotes: 1

Related Questions