IPv6+Ruby part 4: Dual stack (IPv4+IPv6) UDP sockets
Table of Contents
This post is the fourth in a series about IPv6 and why it matters. If you are interested in the details, please read the first three articles. If you just want to know about how Ruby handles UDP in IPv6, this is the place for you.
Last time around we made a simple echo server in Ruby using TCP sockets that ran both IPv6 and IPv4. Now it is time to do the same with UDP.
Just a quick refresher if you forgot, UDP is a connectionless datagram protocol. That means that no one is handling re-transmission of lost information, and that messages are handled as single units or datagrams. There is also no reordering of datagrams, so we cannot be sure that the datagrams get to the receiver in the same order as we send them.
UDP Server
Let us jump straight into it!
To make this simple server, a few things are needed:
- Setting up a server socket
- Binding to an address and port
- Receiving messages
- Doing something with the connected client
- Sending back an answer
- Closing the client socket
The “new” thing here is binding to an address and a port, and in reality,
that was just done for us automatically with the TCPServer class in the
last post. There is however no UDPServer class, so we instead have to set
it up by hand.
The first part is setting up the socket. We need to require socket and create
the server socket. Since we want our service to listen on both IPv4 and IPv6,
we give the address family :INET6 to our socket. We then bind to the address
and port we want to listen on:
require "socket"
LISTEN_ADDR = "::"
LISTEN_PORT = 12345
MSG_LENGTH = 256
FLAGS = 0
# Create socket and bind it to the listen on all addresses and the given port
server_socket = UDPSocket.new :INET6
server_socket.bind(LISTEN_ADDR, LISTEN_PORT)
Now our server_socket is created but is not doing anything yet. We can
receive datagrams from clients with the #recvfrom method on the
server_socket, giving it the argument of how many bytes we want to receive:
message, client = server_socket.recvfrom(MSG_LENGTH)
This call will block until a client connects, and will upon client connection return the client message and an array with client information.
One thing we can do with the connected client is check whether the client
connected via IPv4 or IPv6, and log that. We do that by getting the Addrinfo
object. This time we have to make the object ourselves:
addr_info = Addrinfo.new(client)
puts "Client connected from #{addr_info.ip_address} using " +
"#{addr_info.ipv6_v4mapped? ? "IPv4" : "IPv6"}"
Lastly, we can write back to the client with the IPv4/IPv6 info, as well as the client message, and move on to the next message:
server_socket.send("[#{addr_info.ipv6_v4mapped? ? "IPv4" : "IPv6"}]" +
" #{message.chomp}",
FLAGS, addr_info.ip_address, addr_info.ip_port)
Notice how we do not have a separate socket for the client, and thus do not have a client socket to close.
Putting it all together and placing the client handling parts in a loop, gives:
require "socket"
LISTEN_ADDR = "::"
LISTEN_PORT = 12345
MSG_LENGTH = 256
FLAGS = 0
# Create socket and bind it to the listen on all addresses and the given port
server_socket = UDPSocket.new :INET6
server_socket.bind(LISTEN_ADDR, LISTEN_PORT)
loop do
# Listen for messages of up to specified length
message, client = server_socket.recvfrom(MSG_LENGTH)
# Extract client information given as array and log connection
addr_info = Addrinfo.new(client)
puts "Client connected from #{addr_info.ip_address} using " +
"#{addr_info.ipv6_v4mapped? ? "IPv4" : "IPv6"}"
# Write back to client with AddressFamily and reversed original message
server_socket.send("[#{addr_info.ipv6_v4mapped? ? "IPv4" : "IPv6"}]" +
" #{message.chomp.reverse}",
FLAGS, addr_info.ip_address, addr_info.ip_port)
end
We can quickly test our server by starting it and connecting to it using Netcat
or nc by first running the server:
cmol@qui-gon:~$ ruby src/chat_server_udp.rb
Client connected from ::ffff:127.0.0.1 using IPv4
Client connected from ::1 using IPv6
And then using nc to connect:
cmol@qui-gon:~$ nc -4 -u localhost 12345
Hello from IPv4!
[IPv4] Hello from IPv4!
cmol@qui-gon:~$ nc -6 -u localhost 12345
Hello from IPv6!
[IPv6] Hello from IPv6!
Great, this works as expected!
A few things to think about in a production environment:
- If you have a server handling multiple concurrent clients, you might want to create a new thread for the client handling or forking your process
- Calling
recvfromhere does not have a timeout. You can userecvfrom_nonblockand some code around that instead, or perhaps handle it with threads if you need it non-blocking
Ok, time to make a client!
Client
Just like last time, when connecting to some endpoint as a client, the OS
should really be the one deciding how to connect to that client based on
whatever DNS entries are available, as well as OS preference. With UDP we can
do this using Addrinfo as that will automatically create the correct socket
type. This would be done like:
require "socket"
CONNECT_ADDR = "localhost"
CONNECT_PORT = 12345
MSG_LENGTH = 256
FLAGS = 0
Addrinfo.udp(CONNECT_ADDR, CONNECT_PORT).connect do |socket|
socket.send("Connected via unspecified address family socket", FLAGS)
message, server = socket.recvfrom(MSG_LENGTH)
puts message
end
This works and returns:
mol@qui-gon:~$ ruby echo_client_udp.rb
[IPv6] Connected via unspecified address family socket
Just as in the last post, we can inspect our possibilities a bit more, and
choose where to connect for the purpose of digging a bit deeper using the
Socket#getaddrinfo method.
irb(main):003:0> Socket.getaddrinfo("localhost", 12345, :AF_UNSPEC, :DGRAM)
=> [
["AF_INET6", 12345, "::1", "::1", 10, 2, 17],
["AF_INET", 12345, "127.0.0.1", "127.0.0.1", 2, 2, 17]
]
Instead of 12345 you can use a string like "https" and let the system find
the port itself.
As a refresher, each element in the array represents “a way to connect” with the information:
- Address family
- Port
- IP or hostname if reverse DNS is used
- IP
- Protocol family (long story short, think of it as address family)
- Socket type (STREAM/DGRAM/RAW/…)
- Protocol type (TCP/UDP/IP/SCTP/…)
Using this, we can try them all, just for the fun of it with:
Socket.getaddrinfo(CONNECT_ADDR,CONNECT_PORT, :AF_UNSPEC, :DGRAM).each do | con |
af, port, hostname, ip, pf, sock_type, ipproto = con
Addrinfo.udp(ip, port).connect do |socket|
socket.send("Connected via #{af}", FLAGS)
message, server = socket.recvfrom(MSG_LENGTH)
puts message
end
end
Putting it all together:
require "socket"
CONNECT_ADDR = "localhost"
CONNECT_PORT = 12345
MSG_LENGTH = 256
FLAGS = 0
Addrinfo.udp(CONNECT_ADDR, CONNECT_PORT).connect do |socket|
socket.send("Connected via unspecified address family socket", FLAGS)
message, server = socket.recvfrom(MSG_LENGTH)
puts message
end
Socket.getaddrinfo(CONNECT_ADDR,CONNECT_PORT, :AF_UNSPEC, :DGRAM).each do | con |
af, port, hostname, ip, pf, sock_type, ipproto = con
Addrinfo.udp(ip, port).connect do |socket|
socket.send("Connected via #{af}", FLAGS)
message, server = socket.recvfrom(MSG_LENGTH)
puts message
end
end
With the output being:
cmol@qui-gon:~$ ruby echo_client_udp.rb
[IPv6] Connected via unspecified address family socket
[IPv6] Connected via AF_INET6
[IPv4] Connected via AF_INET
Wrapping up
In this article we managed to successfully create a UDP echo server and client using IPv6. The changes needed to applications can be fairly small in a lot of cases where there is not any heavy lifting network stuff.
This article is part 4 in a series consisting of:
- IPv6+Ruby part 1: Introduction to IPv6
- IPv6+Ruby part 2: IPv6 and web applications
- IPv6+Ruby part 3: Dual stack (IPv4+IPv6) TCP sockets
- IPv6+Ruby part 4: Dual stack (IPv4+IPv6) TCP sockets
- IPv6+Ruby part 5: Service discovery with IPv6 and Multicast