diff --git a/include/ylt/coro_io/listen_endpoint.hpp b/include/ylt/coro_io/listen_endpoint.hpp new file mode 100644 index 000000000..09b501100 --- /dev/null +++ b/include/ylt/coro_io/listen_endpoint.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +#include "asio/error_code.hpp" +#include "asio/ip/address.hpp" +#include "asio/ip/tcp.hpp" +#include "asio/ip/v6_only.hpp" + +namespace coro_io::detail { + +// Resolve a string address into a TCP endpoint. +// - If `address` is a numeric IP literal (e.g. "::", "0.0.0.0", "127.0.0.1"), +// it is parsed directly via `asio::ip::make_address` to avoid the ambiguity +// of `getaddrinfo()` on different glibc versions / container environments. +// - Otherwise (e.g. "localhost"), falls back to `asio::ip::tcp::resolver`. +// +// On success `ec` is cleared and the resulting endpoint is returned. +// On failure `ec` carries the resolver error and `std::nullopt` is returned. +template +inline std::optional resolve_listen_endpoint( + const Executor& executor, const std::string& address, uint16_t port, + asio::error_code& ec) { + using asio::ip::tcp; + + auto addr = asio::ip::make_address(address, ec); + if (!ec) { + return tcp::endpoint(addr, port); + } + + ec.clear(); + tcp::resolver resolver(executor); + auto it = + resolver.resolve(tcp::resolver::query(address, std::to_string(port)), ec); + if (ec || it == tcp::resolver::iterator{}) { + return std::nullopt; + } + + return it->endpoint(); +} + +// Disable IPV6_V6ONLY on a TCP acceptor so that an IPv6 socket also accepts +// IPv4-mapped connections (i.e. dual-stack listening). +// +// Only applied when the endpoint protocol is IPv6; for IPv4 endpoints this is +// a no-op. The returned `error_code` is non-zero only when the socket option +// cannot be set (e.g. on platforms that disallow toggling V6ONLY at runtime). +// Callers are expected to log a warning on failure rather than abort, since +// the listen socket can still serve IPv6 connections. +inline asio::error_code set_ipv6_only_false( + asio::ip::tcp::acceptor& acceptor, + const asio::ip::tcp::endpoint& endpoint) { + asio::error_code ec; + if (endpoint.protocol() == asio::ip::tcp::v6()) { + acceptor.set_option(asio::ip::v6_only(false), ec); + } + return ec; +} + +} // namespace coro_io::detail diff --git a/include/ylt/coro_io/server_acceptor.hpp b/include/ylt/coro_io/server_acceptor.hpp index 95cde8653..a8fb38ced 100644 --- a/include/ylt/coro_io/server_acceptor.hpp +++ b/include/ylt/coro_io/server_acceptor.hpp @@ -9,7 +9,7 @@ #include #include -#include "asio/ip/address.hpp" +#include "listen_endpoint.hpp" #include "socket_wrapper.hpp" #include "ylt/coro_io/coro_io.hpp" #include "ylt/coro_io/io_context_pool.hpp" @@ -81,19 +81,16 @@ struct tcp_server_acceptor : public server_acceptor_base { ELOG_INFO << "begin to listen"; using asio::ip::tcp; asio::error_code ec; - asio::ip::tcp::resolver::query query(address_, std::to_string(port_)); - asio::ip::tcp::resolver resolver(acceptor_->get_executor()); - asio::ip::tcp::resolver::iterator it = resolver.resolve(query, ec); - asio::ip::tcp::resolver::iterator it_end; - if (ec || it == it_end) { + auto endpoint = detail::resolve_listen_endpoint(acceptor_->get_executor(), + address_, port_, ec); + if (!endpoint) { ELOG_ERROR << "resolve address " << address_ << " error: " << ec.message(); return listen_errc::bad_address; } - auto endpoint = it->endpoint(); - acceptor_->open(endpoint.protocol(), ec); + acceptor_->open(endpoint->protocol(), ec); if (ec) { ELOG_ERROR << "open failed, error: " << ec.message(); return listen_errc::open_error; @@ -101,7 +98,11 @@ struct tcp_server_acceptor : public server_acceptor_base { #ifdef __GNUC__ acceptor_->set_option(tcp::acceptor::reuse_address(true), ec); #endif - acceptor_->bind(endpoint, ec); + if (auto opt_ec = detail::set_ipv6_only_false(*acceptor_, *endpoint); + opt_ec) { + ELOG_WARN << "set v6_only(false) failed: " << opt_ec.message(); + } + acceptor_->bind(*endpoint, ec); if (ec) { ELOG_ERROR << "bind port " << port_ << " error: " << ec.message(); acceptor_->cancel(ec); diff --git a/include/ylt/standalone/cinatra/coro_http_server.hpp b/include/ylt/standalone/cinatra/coro_http_server.hpp index 843789e43..1cf0eea59 100644 --- a/include/ylt/standalone/cinatra/coro_http_server.hpp +++ b/include/ylt/standalone/cinatra/coro_http_server.hpp @@ -12,6 +12,7 @@ #include "ylt/coro_io/coro_file.hpp" #include "ylt/coro_io/coro_io.hpp" #include "ylt/coro_io/io_context_pool.hpp" +#include "ylt/coro_io/listen_endpoint.hpp" #include "ylt/coro_io/load_balancer.hpp" namespace cinatra { @@ -736,13 +737,10 @@ class coro_http_server { using asio::ip::tcp; asio::error_code ec; - asio::ip::tcp::resolver::query query(address_, std::to_string(port_)); - asio::ip::tcp::resolver resolver(acceptor_.get_executor()); - asio::ip::tcp::resolver::iterator it = resolver.resolve(query, ec); - - asio::ip::tcp::resolver::iterator it_end; - if (ec || it == it_end) { - CINATRA_LOG_ERROR << "bad address: " << address_ + auto endpoint = coro_io::detail::resolve_listen_endpoint( + acceptor_.get_executor(), address_, port_, ec); + if (!endpoint) { + CINATRA_LOG_ERROR << "invalid address: " << address_ << " error: " << ec.message(); if (ec) { return ec; @@ -750,8 +748,7 @@ class coro_http_server { return std::make_error_code(std::errc::address_not_available); } - auto endpoint = it->endpoint(); - acceptor_.open(endpoint.protocol(), ec); + acceptor_.open(endpoint->protocol(), ec); if (ec) { CINATRA_LOG_ERROR << "acceptor open failed" << " error: " << ec.message(); @@ -760,7 +757,12 @@ class coro_http_server { #ifdef __GNUC__ acceptor_.set_option(tcp::acceptor::reuse_address(true), ec); #endif - acceptor_.bind(endpoint, ec); + if (auto opt_ec = + coro_io::detail::set_ipv6_only_false(acceptor_, *endpoint); + opt_ec) { + CINATRA_LOG_WARNING << "set v6_only(false) failed: " << opt_ec.message(); + } + acceptor_.bind(*endpoint, ec); if (ec) { CINATRA_LOG_ERROR << "bind port: " << port_ << " error: " << ec.message(); std::error_code ignore_ec;