Skip to content

Commit 0cf9f49

Browse files
committed
Add tcp_socket::bind() for local endpoint binding before connect
Expose bind() on tcp_socket so TCP clients can bind to a specific local address/port before connecting (multi-homed hosts, source-port pinning). Also mark udp_socket::bind() [[nodiscard]] for consistency.
1 parent 9c7d1eb commit 0cf9f49

12 files changed

Lines changed: 373 additions & 26 deletions

File tree

include/boost/corosio/detail/tcp_service.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ class BOOST_COROSIO_DECL tcp_service
4949
int type,
5050
int protocol) = 0;
5151

52+
/** Bind a stream socket to a local endpoint.
53+
54+
@param impl The socket implementation to bind.
55+
@param ep The local endpoint to bind to.
56+
@return Error code on failure, empty on success.
57+
*/
58+
virtual std::error_code
59+
bind_socket(tcp_socket::implementation& impl, endpoint ep) = 0;
60+
5261
protected:
5362
/// Construct the TCP service.
5463
tcp_service() = default;

include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ class BOOST_COROSIO_DECL epoll_tcp_service final
106106
int family,
107107
int type,
108108
int protocol) override;
109+
110+
std::error_code
111+
bind_socket(tcp_socket::implementation& impl, endpoint ep) override;
109112
};
110113

111114
inline void
@@ -233,6 +236,13 @@ epoll_tcp_service::open_socket(
233236
return {};
234237
}
235238

239+
inline std::error_code
240+
epoll_tcp_service::bind_socket(
241+
tcp_socket::implementation& impl, endpoint ep)
242+
{
243+
return static_cast<epoll_tcp_socket*>(&impl)->do_bind(ep);
244+
}
245+
236246
} // namespace boost::corosio::detail
237247

238248
#endif // BOOST_COROSIO_HAS_EPOLL

include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -403,33 +403,38 @@ win_tcp_socket_internal::connect(
403403

404404
svc_.work_started();
405405

406-
// Ephemeral bind — must match the socket's family, not the endpoint's
407-
sockaddr_storage bind_storage{};
408-
socklen_t bind_len;
409-
if (family_ == AF_INET6)
406+
// ConnectEx requires the socket to be bound. Skip if already bound
407+
// (e.g. the caller used tcp_socket::bind() before connect).
408+
if (local_endpoint_ == endpoint{})
410409
{
411-
sockaddr_in6 sa6{};
412-
sa6.sin6_family = AF_INET6;
413-
sa6.sin6_port = 0;
414-
sa6.sin6_addr = in6addr_any;
415-
std::memcpy(&bind_storage, &sa6, sizeof(sa6));
416-
bind_len = sizeof(sa6);
417-
}
418-
else
419-
{
420-
sockaddr_in sa4{};
421-
sa4.sin_family = AF_INET;
422-
sa4.sin_addr.s_addr = INADDR_ANY;
423-
sa4.sin_port = 0;
424-
std::memcpy(&bind_storage, &sa4, sizeof(sa4));
425-
bind_len = sizeof(sa4);
426-
}
410+
sockaddr_storage bind_storage{};
411+
socklen_t bind_len;
412+
if (family_ == AF_INET6)
413+
{
414+
sockaddr_in6 sa6{};
415+
sa6.sin6_family = AF_INET6;
416+
sa6.sin6_port = 0;
417+
sa6.sin6_addr = in6addr_any;
418+
std::memcpy(&bind_storage, &sa6, sizeof(sa6));
419+
bind_len = sizeof(sa6);
420+
}
421+
else
422+
{
423+
sockaddr_in sa4{};
424+
sa4.sin_family = AF_INET;
425+
sa4.sin_addr.s_addr = INADDR_ANY;
426+
sa4.sin_port = 0;
427+
std::memcpy(&bind_storage, &sa4, sizeof(sa4));
428+
bind_len = sizeof(sa4);
429+
}
427430

428-
if (::bind(socket_, reinterpret_cast<sockaddr*>(&bind_storage), bind_len) ==
429-
SOCKET_ERROR)
430-
{
431-
svc_.on_completion(&op, ::WSAGetLastError(), 0);
432-
return std::noop_coroutine();
431+
if (::bind(
432+
socket_, reinterpret_cast<sockaddr*>(&bind_storage),
433+
bind_len) == SOCKET_ERROR)
434+
{
435+
svc_.on_completion(&op, ::WSAGetLastError(), 0);
436+
return std::noop_coroutine();
437+
}
433438
}
434439

435440
auto connect_ex = svc_.connect_ex();
@@ -884,6 +889,28 @@ win_tcp_service::open_socket(
884889
return {};
885890
}
886891

892+
inline std::error_code
893+
win_tcp_service::bind_socket(win_tcp_socket_internal& impl, endpoint ep)
894+
{
895+
SOCKET sock = impl.socket_;
896+
897+
sockaddr_storage storage{};
898+
socklen_t addrlen = detail::to_sockaddr(ep, storage);
899+
if (::bind(
900+
sock, reinterpret_cast<sockaddr*>(&storage),
901+
static_cast<int>(addrlen)) == SOCKET_ERROR)
902+
return make_err(::WSAGetLastError());
903+
904+
// Cache local endpoint (resolves ephemeral port)
905+
sockaddr_storage local_storage{};
906+
int local_len = sizeof(local_storage);
907+
if (::getsockname(
908+
sock, reinterpret_cast<sockaddr*>(&local_storage), &local_len) == 0)
909+
impl.local_endpoint_ = detail::from_sockaddr(local_storage);
910+
911+
return {};
912+
}
913+
887914
inline void*
888915
win_tcp_service::native_handle() const noexcept
889916
{

include/boost/corosio/native/detail/iocp/win_tcp_service.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ class BOOST_COROSIO_DECL win_tcp_service final
9898
std::error_code
9999
open_socket(win_tcp_socket_internal& impl, int family, int type, int protocol);
100100

101+
/** Bind a stream socket to a local endpoint.
102+
103+
@param impl The socket implementation internal to bind.
104+
@param ep The local endpoint to bind to.
105+
@return Error code, or success.
106+
*/
107+
std::error_code
108+
bind_socket(win_tcp_socket_internal& impl, endpoint ep);
109+
101110
/** Destroy an acceptor implementation wrapper.
102111
Removes from tracking list and deletes.
103112
*/

include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ class BOOST_COROSIO_DECL kqueue_tcp_service final
165165
int family,
166166
int type,
167167
int protocol) override;
168+
169+
std::error_code
170+
bind_socket(tcp_socket::implementation& impl, endpoint ep) override;
168171
};
169172

170173
inline void
@@ -340,6 +343,13 @@ kqueue_tcp_service::open_socket(
340343
return {};
341344
}
342345

346+
inline std::error_code
347+
kqueue_tcp_service::bind_socket(
348+
tcp_socket::implementation& impl, endpoint ep)
349+
{
350+
return static_cast<kqueue_tcp_socket*>(&impl)->do_bind(ep);
351+
}
352+
343353
} // namespace boost::corosio::detail
344354

345355
#endif // BOOST_COROSIO_HAS_KQUEUE

include/boost/corosio/native/detail/select/select_tcp_service.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class BOOST_COROSIO_DECL select_tcp_service final
6767
int family,
6868
int type,
6969
int protocol) override;
70+
71+
std::error_code
72+
bind_socket(tcp_socket::implementation& impl, endpoint ep) override;
7073
};
7174

7275
inline void
@@ -234,6 +237,13 @@ select_tcp_service::open_socket(
234237
return {};
235238
}
236239

240+
inline std::error_code
241+
select_tcp_service::bind_socket(
242+
tcp_socket::implementation& impl, endpoint ep)
243+
{
244+
return static_cast<select_tcp_socket*>(&impl)->do_bind(ep);
245+
}
246+
237247
} // namespace boost::corosio::detail
238248

239249
#endif // BOOST_COROSIO_HAS_SELECT

include/boost/corosio/tcp_socket.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,28 @@ class BOOST_COROSIO_DECL tcp_socket : public io_stream
277277
*/
278278
void open(tcp proto = tcp::v4());
279279

280+
/** Bind the socket to a local endpoint.
281+
282+
Associates the socket with a local address and port before
283+
connecting. Useful for multi-homed hosts or source-port
284+
pinning.
285+
286+
@param ep The local endpoint to bind to.
287+
288+
@return An error code indicating success or the reason for
289+
failure.
290+
291+
@par Error Conditions
292+
@li `errc::address_in_use`: The endpoint is already in use.
293+
@li `errc::address_not_available`: The address is not
294+
available on any local interface.
295+
@li `errc::permission_denied`: Insufficient privileges to
296+
bind to the endpoint (e.g., privileged port).
297+
298+
@throws std::logic_error if the socket is not open.
299+
*/
300+
[[nodiscard]] std::error_code bind(endpoint ep);
301+
280302
/** Close the socket.
281303
282304
Releases socket resources. Any pending operations complete

include/boost/corosio/udp_socket.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ class BOOST_COROSIO_DECL udp_socket : public io_object
535535
536536
@throws std::logic_error if the socket is not open.
537537
*/
538-
std::error_code bind(endpoint ep);
538+
[[nodiscard]] std::error_code bind(endpoint ep);
539539

540540
/** Cancel any pending asynchronous operations.
541541

src/corosio/src/tcp_socket.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ tcp_socket::open_for_family(int family, int type, int protocol)
6161
detail::throw_system_error(ec, "tcp_socket::open");
6262
}
6363

64+
std::error_code
65+
tcp_socket::bind(endpoint ep)
66+
{
67+
if (!is_open())
68+
detail::throw_logic_error("bind: socket not open");
69+
#if BOOST_COROSIO_HAS_IOCP
70+
auto& svc = static_cast<detail::win_tcp_service&>(h_.service());
71+
auto& wrapper = static_cast<tcp_socket::implementation&>(*h_.get());
72+
return svc.bind_socket(
73+
*static_cast<detail::win_tcp_socket&>(wrapper).get_internal(), ep);
74+
#else
75+
auto& svc = static_cast<detail::tcp_service&>(h_.service());
76+
return svc.bind_socket(
77+
static_cast<tcp_socket::implementation&>(*h_.get()), ep);
78+
#endif
79+
}
80+
6481
void
6582
tcp_socket::close()
6683
{

test/unit/tcp_acceptor.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,44 @@ struct tcp_acceptor_test
556556
acc.close();
557557
}
558558

559+
void testBindClosedAcceptorThrows()
560+
{
561+
io_context ioc(Backend);
562+
tcp_acceptor acc(ioc);
563+
564+
bool caught = false;
565+
try
566+
{
567+
auto ec = acc.bind(endpoint(ipv4_address::loopback(), 0));
568+
(void)ec;
569+
}
570+
catch (std::logic_error const&)
571+
{
572+
caught = true;
573+
}
574+
BOOST_TEST(caught);
575+
}
576+
577+
void testBindAddressInUse()
578+
{
579+
io_context ioc(Backend);
580+
581+
tcp_acceptor acc1(ioc);
582+
acc1.open();
583+
acc1.set_option(socket_option::reuse_address(true));
584+
auto ec = acc1.bind(endpoint(ipv4_address::loopback(), 0));
585+
BOOST_TEST(!ec);
586+
auto port = acc1.local_endpoint().port();
587+
588+
tcp_acceptor acc2(ioc);
589+
acc2.open();
590+
ec = acc2.bind(endpoint(ipv4_address::loopback(), port));
591+
BOOST_TEST(ec);
592+
593+
acc1.close();
594+
acc2.close();
595+
}
596+
559597
void testBindError()
560598
{
561599
io_context ioc(Backend);
@@ -602,6 +640,8 @@ struct tcp_acceptor_test
602640

603641
// Explicit bind+listen flow
604642
testBindThenListen();
643+
testBindClosedAcceptorThrows();
644+
testBindAddressInUse();
605645
testBindError();
606646
}
607647
};

0 commit comments

Comments
 (0)