Thank you so much! Binding to the port is fine (and needed for client to get past the NAT routers that 99% of end-users sit behind).
But binding to a specific address, such as the loopback/localhost (127.0.0.1) forces it to an interface that has no route to the destination address. Makes sense. In fact, I used to be a IP network engineer by trade. Kind of embarrassing that I didn't see a basic routing problem. :) The runtime error is awfully generic though. It should have said something like "No route to host" or something descriptive. Anyway, leaving the "from" address off fixed it. For the curious, here is my basic two-way UDP client proof-of-concept. This one is async, uses the select method, and allows one to pass in the server address by command-line. import asyncdispatch, asyncnet, os import std/[net, selectors, strformat, strutils] const cfgMaxPacket = 508 serverPort = 9900 if paramCount() == 0: echo "pass a server address" quit() let serverHost = paramStr(1) proc showMessage(message: string) = let parts = message.split("\n") for part in parts: echo "< " & part proc doit() {.async.} = let socket = newAsyncSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) events = newSelector[int]() events.registerHandle(socket.getFd, {Read}, 0) events.registerHandle(0, {Read}, 0) # stdin echo &"Listening to stdin and transmitting to {serverHost}:{serverPort}" while true: for got in events.select(-1): if got.fd == socket.getFd().int: var msgDetails = await socket.recvFrom(cfgMaxPacket) let message = msgDetails.data showMessage(message.strip) elif got.fd == 0: try: await socket.sendTo(serverHost, Port(serverPort), stdin.readLine()) except EOFError: quit() asyncCheck doit() runForever() Run