#3 - DNS support + IPv6

This commit is contained in:
Martin Slachta
2026-08-05 17:14:04 +02:00
parent 0e639abcd2
commit b71f740b9a
13 changed files with 291 additions and 92 deletions
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <cerrno>
#include <cstring>
#include <netdb.h>
#include <string>
namespace tw::net {
/**
* A failed host lookup.
*
* Kept apart from NetworkError because getaddrinfo reports EAI_ codes, which
* are their own mostly-negative space: sharing one enum would map a lookup
* failure onto whichever errno happened to carry the same number.
*/
struct ResolutionError {
int m_code;
int m_errno;
public:
/**
* Only EAI_SYSTEM defers to errno, and errno will not have survived by the
* time message() runs, so it is captured here.
*/
static ResolutionError from_gai(int code) {
return { code, errno };
}
std::string message() const {
switch (m_code) {
case EAI_NONAME:
return "The host name is not known.";
case EAI_AGAIN:
return "The name server is unreachable or busy; the lookup may succeed later.";
case EAI_FAIL:
return "The name server returned a permanent failure.";
case EAI_FAMILY:
return "The requested address family is not supported.";
case EAI_SERVICE:
return "The requested port is not available for this socket type.";
case EAI_MEMORY:
return "Insufficient memory was available to complete the lookup.";
case EAI_SYSTEM:
return std::string(strerror(m_errno));
default:
return std::string(gai_strerror(m_code));
}
}
};
}
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include "ResolutionError.hpp"
#include "tl/expected.hpp"
#include <cstring>
#include <netdb.h>
#include <string>
#include <sys/socket.h>
namespace tw::net {
/**
* Turns a host name or an address literal into a socket address.
*
* `family` is the family of the socket the result will be given to. AF_INET6
* asks for IPv4-only names as ::ffff: mapped addresses, so that one dual-stack
* socket reaches both; AF_UNSPEC takes the name as it comes and suits addresses
* that are only being validated, displayed or stored.
*
* Blocks for the length of a DNS round trip when the name is not already known,
* so it belongs at connect time rather than anywhere periodic.
*/
inline tl::expected<sockaddr_storage, ResolutionError>
resolve_host(const std::string& host, int port, sa_family_t family = AF_UNSPEC) {
addrinfo hints {};
hints.ai_family = family;
hints.ai_socktype = SOCK_DGRAM;
// AI_ADDRCONFIG is deliberately absent. Together with AF_INET6 it discards
// every result on a host that carries no global IPv6 address, which is the
// default state of a container on a bridge network.
if(family == AF_INET6) {
hints.ai_flags = AI_V4MAPPED | AI_ALL;
}
// Passing the port as the service spares us setting sin_port or sin6_port
// by hand once the family of the answer is known.
const std::string service = std::to_string(port);
addrinfo* results = nullptr;
const int rc = ::getaddrinfo(host.c_str(), service.c_str(), &hints, &results);
if(rc != 0) {
return tl::make_unexpected(ResolutionError::from_gai(rc));
}
// The list arrives ordered by RFC 6724, so the head is the address the
// system would have picked for itself.
sockaddr_storage storage {};
std::memcpy(&storage, results->ai_addr, results->ai_addrlen);
::freeaddrinfo(results);
return storage;
}
}