diff --git a/include/boost/corosio/detail/tcp_service.hpp b/include/boost/corosio/detail/tcp_service.hpp index eb9e487d..94a5fb92 100644 --- a/include/boost/corosio/detail/tcp_service.hpp +++ b/include/boost/corosio/detail/tcp_service.hpp @@ -49,6 +49,15 @@ class BOOST_COROSIO_DECL tcp_service int type, int protocol) = 0; + /** Bind a stream socket to a local endpoint. + + @param impl The socket implementation to bind. + @param ep The local endpoint to bind to. + @return Error code on failure, empty on success. + */ + virtual std::error_code + bind_socket(tcp_socket::implementation& impl, endpoint ep) = 0; + protected: /// Construct the TCP service. tcp_service() = default; diff --git a/include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp b/include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp index aa01e9da..723ec2e0 100644 --- a/include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp +++ b/include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp @@ -106,6 +106,9 @@ class BOOST_COROSIO_DECL epoll_tcp_service final int family, int type, int protocol) override; + + std::error_code + bind_socket(tcp_socket::implementation& impl, endpoint ep) override; }; inline void @@ -233,6 +236,13 @@ epoll_tcp_service::open_socket( return {}; } +inline std::error_code +epoll_tcp_service::bind_socket( + tcp_socket::implementation& impl, endpoint ep) +{ + return static_cast(&impl)->do_bind(ep); +} + } // namespace boost::corosio::detail #endif // BOOST_COROSIO_HAS_EPOLL diff --git a/include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp b/include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp index c44395c0..1117b7d6 100644 --- a/include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp +++ b/include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp @@ -403,33 +403,38 @@ win_tcp_socket_internal::connect( svc_.work_started(); - // Ephemeral bind — must match the socket's family, not the endpoint's - sockaddr_storage bind_storage{}; - socklen_t bind_len; - if (family_ == AF_INET6) + // ConnectEx requires the socket to be bound. Skip if already bound + // (e.g. the caller used tcp_socket::bind() before connect). + if (local_endpoint_ == endpoint{}) { - sockaddr_in6 sa6{}; - sa6.sin6_family = AF_INET6; - sa6.sin6_port = 0; - sa6.sin6_addr = in6addr_any; - std::memcpy(&bind_storage, &sa6, sizeof(sa6)); - bind_len = sizeof(sa6); - } - else - { - sockaddr_in sa4{}; - sa4.sin_family = AF_INET; - sa4.sin_addr.s_addr = INADDR_ANY; - sa4.sin_port = 0; - std::memcpy(&bind_storage, &sa4, sizeof(sa4)); - bind_len = sizeof(sa4); - } + sockaddr_storage bind_storage{}; + socklen_t bind_len; + if (family_ == AF_INET6) + { + sockaddr_in6 sa6{}; + sa6.sin6_family = AF_INET6; + sa6.sin6_port = 0; + sa6.sin6_addr = in6addr_any; + std::memcpy(&bind_storage, &sa6, sizeof(sa6)); + bind_len = sizeof(sa6); + } + else + { + sockaddr_in sa4{}; + sa4.sin_family = AF_INET; + sa4.sin_addr.s_addr = INADDR_ANY; + sa4.sin_port = 0; + std::memcpy(&bind_storage, &sa4, sizeof(sa4)); + bind_len = sizeof(sa4); + } - if (::bind(socket_, reinterpret_cast(&bind_storage), bind_len) == - SOCKET_ERROR) - { - svc_.on_completion(&op, ::WSAGetLastError(), 0); - return std::noop_coroutine(); + if (::bind( + socket_, reinterpret_cast(&bind_storage), + bind_len) == SOCKET_ERROR) + { + svc_.on_completion(&op, ::WSAGetLastError(), 0); + return std::noop_coroutine(); + } } auto connect_ex = svc_.connect_ex(); @@ -884,6 +889,28 @@ win_tcp_service::open_socket( return {}; } +inline std::error_code +win_tcp_service::bind_socket(win_tcp_socket_internal& impl, endpoint ep) +{ + SOCKET sock = impl.socket_; + + sockaddr_storage storage{}; + socklen_t addrlen = detail::to_sockaddr(ep, storage); + if (::bind( + sock, reinterpret_cast(&storage), + static_cast(addrlen)) == SOCKET_ERROR) + return make_err(::WSAGetLastError()); + + // Cache local endpoint (resolves ephemeral port) + sockaddr_storage local_storage{}; + int local_len = sizeof(local_storage); + if (::getsockname( + sock, reinterpret_cast(&local_storage), &local_len) == 0) + impl.local_endpoint_ = detail::from_sockaddr(local_storage); + + return {}; +} + inline void* win_tcp_service::native_handle() const noexcept { diff --git a/include/boost/corosio/native/detail/iocp/win_tcp_service.hpp b/include/boost/corosio/native/detail/iocp/win_tcp_service.hpp index 58f78af3..674721f3 100644 --- a/include/boost/corosio/native/detail/iocp/win_tcp_service.hpp +++ b/include/boost/corosio/native/detail/iocp/win_tcp_service.hpp @@ -98,6 +98,15 @@ class BOOST_COROSIO_DECL win_tcp_service final std::error_code open_socket(win_tcp_socket_internal& impl, int family, int type, int protocol); + /** Bind a stream socket to a local endpoint. + + @param impl The socket implementation internal to bind. + @param ep The local endpoint to bind to. + @return Error code, or success. + */ + std::error_code + bind_socket(win_tcp_socket_internal& impl, endpoint ep); + /** Destroy an acceptor implementation wrapper. Removes from tracking list and deletes. */ diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp index 9b00ccbd..ed82586d 100644 --- a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp +++ b/include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp @@ -165,6 +165,9 @@ class BOOST_COROSIO_DECL kqueue_tcp_service final int family, int type, int protocol) override; + + std::error_code + bind_socket(tcp_socket::implementation& impl, endpoint ep) override; }; inline void @@ -340,6 +343,13 @@ kqueue_tcp_service::open_socket( return {}; } +inline std::error_code +kqueue_tcp_service::bind_socket( + tcp_socket::implementation& impl, endpoint ep) +{ + return static_cast(&impl)->do_bind(ep); +} + } // namespace boost::corosio::detail #endif // BOOST_COROSIO_HAS_KQUEUE diff --git a/include/boost/corosio/native/detail/select/select_tcp_service.hpp b/include/boost/corosio/native/detail/select/select_tcp_service.hpp index 57d19444..d384e40a 100644 --- a/include/boost/corosio/native/detail/select/select_tcp_service.hpp +++ b/include/boost/corosio/native/detail/select/select_tcp_service.hpp @@ -67,6 +67,9 @@ class BOOST_COROSIO_DECL select_tcp_service final int family, int type, int protocol) override; + + std::error_code + bind_socket(tcp_socket::implementation& impl, endpoint ep) override; }; inline void @@ -234,6 +237,13 @@ select_tcp_service::open_socket( return {}; } +inline std::error_code +select_tcp_service::bind_socket( + tcp_socket::implementation& impl, endpoint ep) +{ + return static_cast(&impl)->do_bind(ep); +} + } // namespace boost::corosio::detail #endif // BOOST_COROSIO_HAS_SELECT diff --git a/include/boost/corosio/tcp_socket.hpp b/include/boost/corosio/tcp_socket.hpp index 6281f915..5a0e8638 100644 --- a/include/boost/corosio/tcp_socket.hpp +++ b/include/boost/corosio/tcp_socket.hpp @@ -277,6 +277,28 @@ class BOOST_COROSIO_DECL tcp_socket : public io_stream */ void open(tcp proto = tcp::v4()); + /** Bind the socket to a local endpoint. + + Associates the socket with a local address and port before + connecting. Useful for multi-homed hosts or source-port + pinning. + + @param ep The local endpoint to bind to. + + @return An error code indicating success or the reason for + failure. + + @par Error Conditions + @li `errc::address_in_use`: The endpoint is already in use. + @li `errc::address_not_available`: The address is not + available on any local interface. + @li `errc::permission_denied`: Insufficient privileges to + bind to the endpoint (e.g., privileged port). + + @throws std::logic_error if the socket is not open. + */ + [[nodiscard]] std::error_code bind(endpoint ep); + /** Close the socket. Releases socket resources. Any pending operations complete diff --git a/include/boost/corosio/udp_socket.hpp b/include/boost/corosio/udp_socket.hpp index beb42040..88a9290b 100644 --- a/include/boost/corosio/udp_socket.hpp +++ b/include/boost/corosio/udp_socket.hpp @@ -535,7 +535,7 @@ class BOOST_COROSIO_DECL udp_socket : public io_object @throws std::logic_error if the socket is not open. */ - std::error_code bind(endpoint ep); + [[nodiscard]] std::error_code bind(endpoint ep); /** Cancel any pending asynchronous operations. diff --git a/src/corosio/src/tcp_socket.cpp b/src/corosio/src/tcp_socket.cpp index 674ce568..34c351ee 100644 --- a/src/corosio/src/tcp_socket.cpp +++ b/src/corosio/src/tcp_socket.cpp @@ -61,6 +61,23 @@ tcp_socket::open_for_family(int family, int type, int protocol) detail::throw_system_error(ec, "tcp_socket::open"); } +std::error_code +tcp_socket::bind(endpoint ep) +{ + if (!is_open()) + detail::throw_logic_error("bind: socket not open"); +#if BOOST_COROSIO_HAS_IOCP + auto& svc = static_cast(h_.service()); + auto& wrapper = static_cast(*h_.get()); + return svc.bind_socket( + *static_cast(wrapper).get_internal(), ep); +#else + auto& svc = static_cast(h_.service()); + return svc.bind_socket( + static_cast(*h_.get()), ep); +#endif +} + void tcp_socket::close() { diff --git a/test/unit/tcp_acceptor.cpp b/test/unit/tcp_acceptor.cpp index aaf7da6b..ff41847d 100644 --- a/test/unit/tcp_acceptor.cpp +++ b/test/unit/tcp_acceptor.cpp @@ -556,6 +556,44 @@ struct tcp_acceptor_test acc.close(); } + void testBindClosedAcceptorThrows() + { + io_context ioc(Backend); + tcp_acceptor acc(ioc); + + bool caught = false; + try + { + auto ec = acc.bind(endpoint(ipv4_address::loopback(), 0)); + (void)ec; + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testBindAddressInUse() + { + io_context ioc(Backend); + + tcp_acceptor acc1(ioc); + acc1.open(); + acc1.set_option(socket_option::reuse_address(true)); + auto ec = acc1.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port = acc1.local_endpoint().port(); + + tcp_acceptor acc2(ioc); + acc2.open(); + ec = acc2.bind(endpoint(ipv4_address::loopback(), port)); + BOOST_TEST(ec); + + acc1.close(); + acc2.close(); + } + void testBindError() { io_context ioc(Backend); @@ -602,6 +640,8 @@ struct tcp_acceptor_test // Explicit bind+listen flow testBindThenListen(); + testBindClosedAcceptorThrows(); + testBindAddressInUse(); testBindError(); } }; diff --git a/test/unit/tcp_socket.cpp b/test/unit/tcp_socket.cpp index acc2a20b..6fd70679 100644 --- a/test/unit/tcp_socket.cpp +++ b/test/unit/tcp_socket.cpp @@ -82,6 +82,141 @@ struct tcp_socket_test BOOST_TEST_EQ(sock.is_open(), false); } + void testBind() + { + io_context ioc(Backend); + tcp_socket sock(ioc); + sock.open(); + + // Bind to loopback with ephemeral port + auto ec = sock.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + + // Local endpoint should reflect the bind + auto local = sock.local_endpoint(); + BOOST_TEST(local.port() != 0); + BOOST_TEST(local.is_v4()); + + sock.close(); + } + + void testBindThenConnect() + { + io_context ioc(Backend); + + tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(socket_option::reuse_address(true)); + auto ec = acc.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + ec = acc.listen(); + BOOST_TEST(!ec); + auto server_port = acc.local_endpoint().port(); + + tcp_socket client(ioc); + tcp_socket server(ioc); + client.open(); + + // Bind client to specific local address before connecting + ec = client.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto bound_port = client.local_endpoint().port(); + BOOST_TEST(bound_port != 0); + + auto connect_task = [&]() -> capy::task<> { + auto [conn_ec] = co_await client.connect( + endpoint(ipv4_address::loopback(), server_port)); + BOOST_TEST(!conn_ec); + }; + + auto accept_task = [&]() -> capy::task<> { + auto [acc_ec] = co_await acc.accept(server); + BOOST_TEST(!acc_ec); + }; + + capy::run_async(ioc.get_executor())(connect_task()); + capy::run_async(ioc.get_executor())(accept_task()); + ioc.run(); + + // Client's local port should be the one we bound to + BOOST_TEST(client.local_endpoint().port() == bound_port); + + // Server sees our bound port as the remote + BOOST_TEST(server.remote_endpoint().port() == bound_port); + + client.close(); + server.close(); + acc.close(); + } + + void testBindClosedSocketThrows() + { + io_context ioc(Backend); + tcp_socket sock(ioc); + + // Bind on a closed socket should throw + bool caught = false; + try + { + auto ec = sock.bind(endpoint(ipv4_address::loopback(), 0)); + (void)ec; + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testBindV6() + { + io_context ioc(Backend); + tcp_socket sock(ioc); + sock.open(tcp::v6()); + + auto ec = sock.bind(endpoint(ipv6_address::loopback(), 0)); + BOOST_TEST(!ec); + + auto local = sock.local_endpoint(); + BOOST_TEST(local.port() != 0); + BOOST_TEST(local.is_v6()); + + sock.close(); + } + + void testBindAddressInUse() + { + io_context ioc(Backend); + + // Bind first socket to a specific port + tcp_socket sock1(ioc); + sock1.open(); + auto ec = sock1.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port = sock1.local_endpoint().port(); + + // Second bind to same port should fail + tcp_socket sock2(ioc); + sock2.open(); + ec = sock2.bind(endpoint(ipv4_address::loopback(), port)); + BOOST_TEST(ec); + + sock1.close(); + sock2.close(); + } + + void testBindNonLocalAddress() + { + io_context ioc(Backend); + tcp_socket sock(ioc); + sock.open(); + + auto ec = sock.bind(endpoint(ipv4_address("1.2.3.4"), 0)); + BOOST_TEST(ec); + + sock.close(); + } + void testMoveConstruct() { io_context ioc(Backend); @@ -1457,6 +1592,12 @@ struct tcp_socket_test { testConstruction(); testOpen(); + testBind(); + testBindThenConnect(); + testBindV6(); + testBindClosedSocketThrows(); + testBindAddressInUse(); + testBindNonLocalAddress(); testMoveConstruct(); testMoveAssign(); diff --git a/test/unit/udp_socket.cpp b/test/unit/udp_socket.cpp index 3450d491..7d550d92 100644 --- a/test/unit/udp_socket.cpp +++ b/test/unit/udp_socket.cpp @@ -126,6 +126,55 @@ struct udp_socket_test sock.close(); } + void testBindClosedSocketThrows() + { + io_context ioc(Backend); + udp_socket sock(ioc); + + bool caught = false; + try + { + auto ec = sock.bind(endpoint(ipv4_address::loopback(), 0)); + (void)ec; + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testBindAddressInUse() + { + io_context ioc(Backend); + + udp_socket sock1(ioc); + sock1.open(); + auto ec = sock1.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port = sock1.local_endpoint().port(); + + udp_socket sock2(ioc); + sock2.open(); + ec = sock2.bind(endpoint(ipv4_address::loopback(), port)); + BOOST_TEST(ec); + + sock1.close(); + sock2.close(); + } + + void testBindNonLocalAddress() + { + io_context ioc(Backend); + udp_socket sock(ioc); + sock.open(); + + auto ec = sock.bind(endpoint(ipv4_address("1.2.3.4"), 0)); + BOOST_TEST(ec); + + sock.close(); + } + void testSetOption() { io_context ioc(Backend); @@ -872,6 +921,9 @@ struct udp_socket_test testMoveAssign(); testBind(); testBindV6(); + testBindClosedSocketThrows(); + testBindAddressInUse(); + testBindNonLocalAddress(); testSetOption(); testSendRecvLoopback(); testSendRecvV6Loopback();