#3 - DNS support + IPv6
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
#include "ClientArgs.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <charconv>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -25,12 +24,6 @@ std::string_view trim(std::string_view str) {
|
||||
return str.substr(start, end - start);
|
||||
}
|
||||
|
||||
bool is_valid_ipv4(std::string_view ip_str) {
|
||||
// Use inet_pton to validate IPv4 format
|
||||
struct in_addr addr;
|
||||
return inet_pton(AF_INET, std::string(ip_str).c_str(), &addr) == 1;
|
||||
}
|
||||
|
||||
tl::expected<int, std::string> parse_port(std::string_view port_str) {
|
||||
if(port_str.empty()) {
|
||||
return 8080; // Default port
|
||||
@@ -63,15 +56,27 @@ tl::expected<net::Address, std::string> parse_address(std::string_view text) {
|
||||
return tl::make_unexpected("address cannot be empty");
|
||||
}
|
||||
|
||||
// Find the colon to split host and port
|
||||
size_t colon_pos = text.rfind(':');
|
||||
|
||||
std::string_view host;
|
||||
std::string_view port_str;
|
||||
|
||||
if(colon_pos == std::string_view::npos) {
|
||||
// No colon found: treat entire string as port or host
|
||||
// If it's all digits, treat as port; otherwise as host (will fail validation)
|
||||
// An IPv6 literal carries colons of its own, so the brackets it is written
|
||||
// in are what says where the host ends. This is the form to_string() emits.
|
||||
if(text.front() == '[') {
|
||||
size_t closing = text.find(']');
|
||||
if(closing == std::string_view::npos) {
|
||||
return tl::make_unexpected("address is missing a closing bracket");
|
||||
}
|
||||
|
||||
host = text.substr(1, closing - 1);
|
||||
port_str = text.substr(closing + 1);
|
||||
|
||||
if(!port_str.empty()) {
|
||||
if(port_str.front() != ':') {
|
||||
return tl::make_unexpected("expected a port after the closing bracket");
|
||||
}
|
||||
port_str.remove_prefix(1);
|
||||
}
|
||||
} else if(size_t colon_pos = text.rfind(':'); colon_pos == std::string_view::npos) {
|
||||
bool all_digits = !text.empty() && std::all_of(text.begin(), text.end(),
|
||||
[](unsigned char c) { return std::isdigit(c); });
|
||||
|
||||
@@ -79,7 +84,6 @@ tl::expected<net::Address, std::string> parse_address(std::string_view text) {
|
||||
host = "127.0.0.1";
|
||||
port_str = text;
|
||||
} else {
|
||||
// Treat as host with no port
|
||||
host = text;
|
||||
port_str = "";
|
||||
}
|
||||
@@ -93,10 +97,6 @@ tl::expected<net::Address, std::string> parse_address(std::string_view text) {
|
||||
return tl::make_unexpected("host cannot be empty");
|
||||
}
|
||||
|
||||
if(!is_valid_ipv4(host)) {
|
||||
return tl::make_unexpected("not a valid IPv4 address");
|
||||
}
|
||||
|
||||
// Parse port
|
||||
auto port_result = parse_port(port_str);
|
||||
if(!port_result) {
|
||||
@@ -104,7 +104,13 @@ tl::expected<net::Address, std::string> parse_address(std::string_view text) {
|
||||
}
|
||||
|
||||
int port = port_result.value();
|
||||
return net::Address(std::optional<std::string>(std::string(host)), port);
|
||||
|
||||
auto address_r = net::Address::resolve(std::string(host), port);
|
||||
if(!address_r) {
|
||||
return tl::make_unexpected(address_r.error().message());
|
||||
}
|
||||
|
||||
return address_r.value();
|
||||
}
|
||||
|
||||
std::optional<std::string> server_arg(int argc, char** argv) {
|
||||
|
||||
@@ -12,10 +12,13 @@ namespace tw::app {
|
||||
/**
|
||||
* Parse an address string into a network address.
|
||||
*
|
||||
* Accepts "host:port" or a bare port number. Bare port uses 127.0.0.1.
|
||||
* Missing port defaults to 8080. Trims surrounding whitespace.
|
||||
* Validates the host with inet_pton and returns an error string
|
||||
* for non-IPv4 addresses or invalid ports.
|
||||
* Accepts "host:port" or a bare port number, where the host may be a name as
|
||||
* well as an address literal; an IPv6 literal has to be bracketed, as
|
||||
* "[::1]:8080". Bare port uses 127.0.0.1. Missing port defaults to 8080.
|
||||
* Trims surrounding whitespace.
|
||||
*
|
||||
* Looks the host up, so it blocks for as long as that takes, and returns an
|
||||
* error string for a host that does not resolve or an invalid port.
|
||||
*/
|
||||
tl::expected<net::Address, std::string> parse_address(std::string_view text);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -70,7 +70,13 @@ MessageConnection* MessageEndpoint::add_peer(net::quicr::QuicrConnection* connec
|
||||
}
|
||||
|
||||
tl::expected<MessageConnection*, MessageError> MessageEndpoint::connect(const std::string& host, int port) {
|
||||
auto connection_r = m_endpoint->connect(net::quicr::QuicrAddress(host, port));
|
||||
auto address_r = net::quicr::QuicrAddress::resolve(host, port, m_endpoint->family());
|
||||
if(!address_r) {
|
||||
return tl::make_unexpected(
|
||||
MessageError(MessageErrorType::ConnectFailed, address_r.error().message()));
|
||||
}
|
||||
|
||||
auto connection_r = m_endpoint->connect(address_r.value());
|
||||
if(!connection_r) {
|
||||
return tl::make_unexpected(
|
||||
MessageError(MessageErrorType::ConnectFailed, connection_r.error().message()));
|
||||
|
||||
@@ -113,7 +113,7 @@ public:
|
||||
};
|
||||
|
||||
int main() {
|
||||
const int NUM_CLIENTS = 10;
|
||||
const int NUM_CLIENTS = 300;
|
||||
tw::net::Address address = {"127.0.0.1", 8101};
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "io/HostResolver.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <string>
|
||||
@@ -7,6 +9,7 @@
|
||||
#include <optional>
|
||||
#include <sys/socket.h>
|
||||
#include <format>
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
@@ -38,6 +41,24 @@ public:
|
||||
: m_storage(storage)
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Look a host up, accepting a name where the constructor above takes only
|
||||
* an address literal.
|
||||
*
|
||||
* `family` should be the family of the socket the address will be used
|
||||
* with. The default suits an address that is only being validated or
|
||||
* displayed, and takes whatever the name resolves to.
|
||||
*/
|
||||
static tl::expected<Address, ResolutionError>
|
||||
resolve(const std::string& host, int port, sa_family_t family = AF_UNSPEC) {
|
||||
auto storage_r = resolve_host(host, port, family);
|
||||
if(!storage_r) {
|
||||
return tl::make_unexpected(storage_r.error());
|
||||
}
|
||||
|
||||
return Address(std::move(storage_r.value()));
|
||||
}
|
||||
|
||||
/** Return a const pointer suitable for connect / sendto / bind. */
|
||||
const struct sockaddr* sockaddr() const {
|
||||
return reinterpret_cast<const struct sockaddr*>(&m_storage);
|
||||
|
||||
@@ -15,7 +15,18 @@ QuicrPeerLink::QuicrPeerLink(uint32_t self_id, uint16_t port)
|
||||
{}
|
||||
|
||||
void QuicrPeerLink::connect_to(uint32_t peer_id, const tw::net::Address& addr) {
|
||||
auto r = m_endpoint->connect(net::quicr::QuicrAddress(addr.ip_string(), addr.port()));
|
||||
// The address is rebuilt from its text, which for a mapped or IPv6 peer is
|
||||
// more than the literal constructor can parse, so it goes back through the
|
||||
// resolver — into the endpoint's family, since that is what will send it.
|
||||
auto address_r = net::quicr::QuicrAddress::resolve(addr.ip_string(), addr.port(),
|
||||
m_endpoint->family());
|
||||
if (!address_r) {
|
||||
spdlog::warn("QuicrPeerLink[{}]: address of peer {} failed to resolve: {}",
|
||||
m_self_id, peer_id, address_r.error().message());
|
||||
return;
|
||||
}
|
||||
|
||||
auto r = m_endpoint->connect(address_r.value());
|
||||
if (!r) {
|
||||
spdlog::warn("QuicrPeerLink[{}]: connect to peer {} failed", m_self_id, peer_id);
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "io/HostResolver.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <string>
|
||||
@@ -7,6 +9,7 @@
|
||||
#include <optional>
|
||||
#include <sys/socket.h>
|
||||
#include <format>
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
namespace tw::net::quicr {
|
||||
|
||||
@@ -38,6 +41,24 @@ public:
|
||||
: m_storage(storage)
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Look a host up, accepting a name where the constructor above takes only
|
||||
* an address literal.
|
||||
*
|
||||
* `family` should be the family of the endpoint socket the address will be
|
||||
* sent from, so that a dual-stack socket is handed a mapped address rather
|
||||
* than a bare IPv4 one.
|
||||
*/
|
||||
static tl::expected<QuicrAddress, ResolutionError>
|
||||
resolve(const std::string& host, int port, sa_family_t family = AF_UNSPEC) {
|
||||
auto storage_r = resolve_host(host, port, family);
|
||||
if(!storage_r) {
|
||||
return tl::make_unexpected(storage_r.error());
|
||||
}
|
||||
|
||||
return QuicrAddress(std::move(storage_r.value()));
|
||||
}
|
||||
|
||||
/** Return a const pointer suitable for connect / sendto / bind. */
|
||||
const struct sockaddr* sockaddr() const {
|
||||
return reinterpret_cast<const struct sockaddr*>(&m_storage);
|
||||
|
||||
@@ -18,6 +18,7 @@ class QuicrConnectionListener;
|
||||
|
||||
class QuicrEndpoint {
|
||||
int32_t m_socket_fd;
|
||||
sa_family_t m_family;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<QuicrConnection>> m_connections;
|
||||
|
||||
std::vector<std::byte> m_inbound_buffer;
|
||||
@@ -26,7 +27,7 @@ class QuicrEndpoint {
|
||||
|
||||
void process_datagram(std::span<std::byte> datagram, QuicrAddress from);
|
||||
|
||||
QuicrEndpoint(int socket_fd);
|
||||
QuicrEndpoint(int socket_fd, sa_family_t family);
|
||||
|
||||
public:
|
||||
QuicrEndpoint(const QuicrEndpoint&) = delete;
|
||||
@@ -47,6 +48,12 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The family the socket was opened with. Addresses have to be resolved
|
||||
* into it before they can be sent to.
|
||||
*/
|
||||
sa_family_t family() const { return m_family; }
|
||||
|
||||
static tl::expected<std::unique_ptr<QuicrEndpoint>, QuicrError> create();
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
namespace tw::net::quicr {
|
||||
|
||||
QuicrEndpoint::QuicrEndpoint(int socket_fd)
|
||||
: m_inbound_buffer(64 * 1024), m_socket_fd(socket_fd),
|
||||
QuicrEndpoint::QuicrEndpoint(int socket_fd, sa_family_t family)
|
||||
: m_inbound_buffer(64 * 1024), m_socket_fd(socket_fd), m_family(family),
|
||||
m_new_connection_handler(nullptr) {
|
||||
|
||||
}
|
||||
@@ -31,29 +31,57 @@ tl::expected<std::unique_ptr<QuicrEndpoint>, QuicrError> QuicrEndpoint::create_a
|
||||
}
|
||||
|
||||
tl::expected<std::unique_ptr<QuicrEndpoint>, QuicrError> QuicrEndpoint::create() {
|
||||
const int domain = AF_INET;
|
||||
// An IPv6 socket with IPV6_V6ONLY cleared also carries IPv4 peers, which
|
||||
// arrive as ::ffff: mapped addresses. A host with IPv6 switched off answers
|
||||
// EAFNOSUPPORT instead, and there the endpoint stays IPv4 as it was.
|
||||
sa_family_t domain = AF_INET6;
|
||||
int socket_fd = socket(domain, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if(socket_fd < 0 && errno == EAFNOSUPPORT) {
|
||||
domain = AF_INET;
|
||||
socket_fd = socket(domain, SOCK_DGRAM, IPPROTO_UDP);
|
||||
}
|
||||
|
||||
if(socket_fd < 0) {
|
||||
spdlog::error("Failed to create socket: {}", strerror(errno));
|
||||
return tl::make_unexpected(QuicrError::from_errno(errno));
|
||||
}
|
||||
|
||||
if(domain == AF_INET6) {
|
||||
const int v6_only = 0;
|
||||
if(setsockopt(socket_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6_only, sizeof(v6_only)) < 0) {
|
||||
spdlog::error("Failed to accept IPv4 peers on the socket: {}", strerror(errno));
|
||||
::close(socket_fd);
|
||||
return tl::make_unexpected(QuicrError::from_errno(errno));
|
||||
}
|
||||
}
|
||||
|
||||
if(fcntl(socket_fd, F_SETFL, fcntl(socket_fd, F_GETFL, 0) | O_NONBLOCK, 1) == -1) {
|
||||
spdlog::error("Failed to set non-blocking mode: {}", strerror(errno));
|
||||
return tl::make_unexpected(QuicrError::from_errno(errno));
|
||||
}
|
||||
|
||||
return std::unique_ptr<QuicrEndpoint>(new QuicrEndpoint(socket_fd));
|
||||
return std::unique_ptr<QuicrEndpoint>(new QuicrEndpoint(socket_fd, domain));
|
||||
}
|
||||
|
||||
tl::expected<void, QuicrError> QuicrEndpoint::bind(int port) {
|
||||
const int domain = AF_INET;
|
||||
struct sockaddr_in addr = {};
|
||||
addr.sin_family = domain;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
sockaddr_storage storage = {};
|
||||
socklen_t length;
|
||||
|
||||
if(::bind(m_socket_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
if(m_family == AF_INET6) {
|
||||
auto& addr = reinterpret_cast<sockaddr_in6&>(storage);
|
||||
addr.sin6_family = AF_INET6;
|
||||
addr.sin6_port = htons(port);
|
||||
addr.sin6_addr = in6addr_any;
|
||||
length = sizeof(sockaddr_in6);
|
||||
} else {
|
||||
auto& addr = reinterpret_cast<sockaddr_in&>(storage);
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
length = sizeof(sockaddr_in);
|
||||
}
|
||||
|
||||
if(::bind(m_socket_fd, reinterpret_cast<struct sockaddr*>(&storage), length) < 0) {
|
||||
spdlog::error("Failed to bind socket: {}", strerror(errno));
|
||||
return tl::make_unexpected(QuicrError::from_errno(errno));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user