This is an automated email from the ASF dual-hosted git repository.

chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new 8b2410d2 Fix client source IP binding loss after socket revival (#3544)
8b2410d2 is described below

commit 8b2410d20b8b32c5700537a0ed1cdcdaa6bc491b
Author: Bright Chen <[email protected]>
AuthorDate: Tue Sep 15 21:55:07 2026 +0800

    Fix client source IP binding loss after socket revival (#3544)
---
 src/brpc/socket.cpp           |  50 ++++++++---
 src/brpc/socket.h             |  11 ++-
 test/brpc_socket_unittest.cpp | 197 ++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 240 insertions(+), 18 deletions(-)

diff --git a/src/brpc/socket.cpp b/src/brpc/socket.cpp
index a0b49662..57cfd229 100644
--- a/src/brpc/socket.cpp
+++ b/src/brpc/socket.cpp
@@ -742,6 +742,7 @@ int Socket::OnCreated(const SocketOptions& options) {
     _tos = 0;
     _remote_side = options.remote_side;
     _local_side = options.local_side;
+    _bind_local_side = options.local_side;
     _device_name = options.device_name;
     _on_edge_triggered_events = options.on_edge_triggered_events;
     _need_on_edge_trigger = options.need_on_edge_trigger;
@@ -797,6 +798,25 @@ int Socket::OnCreated(const SocketOptions& options) {
     _http_request_method = HTTP_METHOD_GET;
     CHECK(nullptr == _write_head.load(butil::memory_order_relaxed));
     _is_write_shutdown = false;
+    // EndPoint::port is a type tag for IPv6/UDS, not a network port.
+    // Normalize only IP ports and leave Unix-domain addresses intact.
+    if (!butil::is_endpoint_extended(_bind_local_side)) {
+        _bind_local_side.port = 0;
+    } else if (butil::get_endpoint_type(_bind_local_side) == AF_INET6) {
+        sockaddr_storage addr{};
+        socklen_t addr_size = 0;
+        if (butil::endpoint2sockaddr(_bind_local_side, &addr, &addr_size) != 
0) {
+            SetFailed(EINVAL, "Fail to get client binding sockaddr from %s",
+                      butil::endpoint2str(_bind_local_side).c_str());
+            return -1;
+        }
+        reinterpret_cast<sockaddr_in6*>(&addr)->sin6_port = 0;
+        if (butil::sockaddr2endpoint(&addr, addr_size, &_bind_local_side) != 
0) {
+            SetFailed(ENOMEM, "Fail to create client binding endpoint from %s",
+                      butil::endpoint2str(_bind_local_side).c_str());
+            return -1;
+        }
+    }
     int fd = options.fd;
     if (!ValidFileDescriptor(fd) && options.connect_on_create) {
         // Connect on create.
@@ -1015,6 +1035,7 @@ int Socket::WaitAndReset(int32_t expected_nref) {
     }
     _transport->Reset(expected_nref);
 
+    // Clear the runtime endpoint, keeping the configured binding intact.
     _local_side = butil::EndPoint();
     if (_ssl_session) {
         SSL_free(_ssl_session);
@@ -1265,8 +1286,8 @@ int Socket::Connect(const timespec* abstime,
         _ssl_state = SSL_OFF;
     }
     struct sockaddr_storage serv_addr;
-    socklen_t addr_size = 0;
-    if (butil::endpoint2sockaddr(remote_side(), &serv_addr, &addr_size) != 0) {
+    socklen_t serv_addr_size = 0;
+    if (butil::endpoint2sockaddr(remote_side(), &serv_addr, &serv_addr_size) 
!= 0) {
         PLOG(ERROR) << "Fail to get sockaddr";
         return -1;
     }
@@ -1294,19 +1315,20 @@ int Socket::Connect(const timespec* abstime,
         return -1;
 #endif
     }
-    if (local_side().ip != butil::IP_ANY) {
-        struct sockaddr_storage cli_addr;
-        if (butil::endpoint2sockaddr(local_side(), &cli_addr, &addr_size) != 
0) {
+    // Use the configured endpoint, not the runtime endpoint from 
getsockname().
+    if (butil::is_endpoint_extended(_bind_local_side) || _bind_local_side.ip 
!= butil::IP_ANY) {
+        struct sockaddr_storage cli_addr{};
+        socklen_t cli_addr_size = 0;
+        if (butil::endpoint2sockaddr(_bind_local_side, &cli_addr, 
&cli_addr_size) != 0) {
             PLOG(ERROR) << "Fail to get client sockaddr";
             return -1;
         }
-        if (::bind(sockfd, (struct sockaddr*)&cli_addr, addr_size) != 0) {
-            PLOG(ERROR) << "Fail to bind client socket, errno=" << 
strerror(errno);
+        if (::bind(sockfd, (struct sockaddr*)&cli_addr, cli_addr_size) != 0) {
+            PLOG(ERROR) << "Fail to bind client socket";
             return -1;
         }
     }
-    const int rc = ::connect(
-        sockfd, (struct sockaddr*)&serv_addr, addr_size);
+    const int rc = ::connect(sockfd, (struct sockaddr*)&serv_addr, 
serv_addr_size);
     if (rc != 0 && errno != EINPROGRESS) {
         PLOG(WARNING) << "Fail to connect to " << remote_side();
         return -1;
@@ -2797,7 +2819,10 @@ int Socket::GetPooledSocket(SocketUniquePtr* 
pooled_socket) {
     if (socket_pool == nullptr) {
         SocketOptions opt;
         opt.remote_side = remote_side();
-        opt.local_side = butil::EndPoint(local_side().ip, 0);
+        // Propagate the configured client binding (source IP + device) to
+        // pooled sub-sockets so that they keep the same binding policy.
+        opt.local_side = _bind_local_side;
+        opt.device_name = _device_name;
         opt.user = user();
         opt.on_edge_triggered_events = _on_edge_triggered_events;
         opt.need_on_edge_trigger = _need_on_edge_trigger;
@@ -2900,7 +2925,10 @@ int Socket::GetShortSocket(SocketUniquePtr* 
short_socket) {
     SocketId id;
     SocketOptions opt;
     opt.remote_side = remote_side();
-    opt.local_side = butil::EndPoint(local_side().ip, 0);
+    // Propagate the configured client binding (source IP + device) to short
+    // sub-sockets so that they keep the same binding policy.
+    opt.local_side = _bind_local_side;
+    opt.device_name = _device_name;
     opt.user = user();
     opt.on_edge_triggered_events = _on_edge_triggered_events;
     opt.need_on_edge_trigger = _need_on_edge_trigger;
diff --git a/src/brpc/socket.h b/src/brpc/socket.h
index 6f6f52fb..97765f63 100644
--- a/src/brpc/socket.h
+++ b/src/brpc/socket.h
@@ -263,6 +263,9 @@ struct SocketOptions {
     // user->BeforeRecycle() before recycling.
     int fd{-1};
     butil::EndPoint remote_side;
+    // Client source address. For IPv4/IPv6, the port is ignored for binding
+    // and the OS allocates a source port. IPv4 IP_ANY disables explicit 
binding.
+    // Unix-domain addresses are preserved, including their paths.
     butil::EndPoint local_side;
     std::string device_name;
     // If `connect_on_create' is true and `fd' is less than 0,
@@ -881,9 +884,15 @@ private:
     // Address of peer. Initialized by SocketOptions.remote_side.
     butil::EndPoint _remote_side;
 
-    // Address of self. Initialized in ResetFileDescriptor().
+    // Runtime local endpoint. Updated in ResetFileDescriptor() and cleared
+    // in WaitAndReset().
     butil::EndPoint _local_side;
 
+    // Client binding address from SocketOptions.local_side, preserved across
+    // health-check/revive. IPv4/IPv6 network ports are normalized to 0;
+    // Unix-domain addresses and extended endpoint type tags are preserved.
+    butil::EndPoint _bind_local_side;
+
     // The device name of the client's network adapter.
     std::string _device_name;
 
diff --git a/test/brpc_socket_unittest.cpp b/test/brpc_socket_unittest.cpp
index 740a1728..30e4e763 100644
--- a/test/brpc_socket_unittest.cpp
+++ b/test/brpc_socket_unittest.cpp
@@ -29,6 +29,8 @@
 #include "butil/macros.h"
 #include "butil/fd_utility.h"
 #include "butil/debug/leak_annotations.h"
+#include "butil/memory/scope_guard.h"
+#include "butil/files/scoped_temp_dir.h"
 #include <butil/fd_guard.h>
 #include "bthread/countdown_event.h"
 #include "bthread/unstable.h"
@@ -702,11 +704,11 @@ TEST_F(SocketTest, health_check) {
     brpc::SocketId id = 8888;
     butil::EndPoint point;
     ASSERT_NO_FATAL_FAILURE(PickUnusedEndPoint(&point));
-    const int kCheckInteval = 1;
+    const int kCheckInterval = 1;
     brpc::SocketOptions options;
     options.remote_side = point;
     options.user = new CheckRecycle;
-    options.health_check_interval_s = kCheckInteval/*s*/;
+    options.health_check_interval_s = kCheckInterval/*s*/;
     ASSERT_EQ(0, brpc::Socket::Create(options, &id));
     brpc::Socket* s = nullptr;
     {
@@ -794,7 +796,7 @@ TEST_F(SocketTest, health_check) {
     while (brpc::Socket::Status(id, &nref) != 0) {
         bthread_usleep(1000);
         ASSERT_LT(butil::cpuwide_time_us(),
-                  start_time + kCheckInteval * 1000000L + 100000L/*100ms*/);
+                  start_time + kCheckInterval * 1000000L + 100000L/*100ms*/);
     }
     //ASSERT_EQ(2, nref);
     ASSERT_TRUE(global_sock);
@@ -856,7 +858,7 @@ static void DoNothingOnEdgeTriggeredEvents(brpc::Socket*) {}
 // and get dispatched within that window. `OnInputEvent` must drop such a
 // stale event instead of crashing.
 TEST_F(SocketTest, input_event_on_revived_socket) {
-    const int kCheckInteval = 1;
+    const int kCheckInterval = 1;
     butil::EndPoint point;
     butil::fd_guard listening_fd;
     ASSERT_NO_FATAL_FAILURE(ListenOnFreePort(&point, &listening_fd));
@@ -864,7 +866,7 @@ TEST_F(SocketTest, input_event_on_revived_socket) {
     brpc::SocketId id = 8888;
     brpc::SocketOptions options;
     options.remote_side = point;
-    options.health_check_interval_s = kCheckInteval;
+    options.health_check_interval_s = kCheckInterval;
     // `OnInputEvent` returns early without an edge-triggered handler.
     options.on_edge_triggered_events = DoNothingOnEdgeTriggeredEvents;
     ASSERT_EQ(0, brpc::Socket::Create(options, &id));
@@ -897,7 +899,7 @@ TEST_F(SocketTest, input_event_on_revived_socket) {
     while (brpc::Socket::Status(id) != 0) {
         bthread_usleep(1000);
         ASSERT_LT(butil::cpuwide_time_us(),
-                  start_time + kCheckInteval * 1000000L + 1000000L);
+                  start_time + kCheckInterval * 1000000L + 1000000L);
     }
     ASSERT_EQ(-1, s->fd());
 
@@ -930,6 +932,189 @@ TEST_F(SocketTest, input_event_on_revived_socket) {
     ASSERT_EQ(0, brpc::Socket::SetFailed(id));
 }
 
+TEST_F(SocketTest, client_binding_endpoint_types) {
+    const char* inputs[] = {"127.0.0.1:12345", "[::1]:12345",
+                            "unix:client-binding-test.sock"};
+    const char* normalized[] = {"127.0.0.1:0", "[::1]:0",
+                                "unix:client-binding-test.sock"};
+    for (size_t i = 0; i < 3; ++i) {
+        SCOPED_TRACE(inputs[i]);
+        brpc::SocketOptions options;
+        // No connection is made: verify propagation without requiring a real
+        // network device or platform support for SO_BINDTODEVICE.
+        options.device_name = "test-device";
+        ASSERT_EQ(0, butil::str2endpoint(inputs[i], &options.local_side));
+        butil::EndPoint expected;
+        ASSERT_EQ(0, butil::str2endpoint(normalized[i], &expected));
+        brpc::SocketId id;
+        ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+        BRPC_SCOPE_EXIT { brpc::Socket::SetFailed(id); };
+        brpc::SocketUniquePtr ptr;
+        ASSERT_EQ(0, brpc::Socket::Address(id, &ptr));
+        ASSERT_EQ(expected, ptr->_bind_local_side);
+        ASSERT_EQ(options.device_name, ptr->_device_name);
+        // Normalization must not mutate a shared extended endpoint.
+        ASSERT_STREQ(inputs[i], 
butil::endpoint2str(options.local_side).c_str());
+
+        brpc::SocketUniquePtr short_socket;
+        ASSERT_EQ(0, ptr->GetShortSocket(&short_socket));
+        BRPC_SCOPE_EXIT { short_socket->SetFailed(); };
+        ASSERT_EQ(expected, short_socket->_bind_local_side);
+        ASSERT_EQ(options.device_name, short_socket->_device_name);
+        brpc::SocketUniquePtr pooled_socket;
+        ASSERT_EQ(0, ptr->GetPooledSocket(&pooled_socket));
+        BRPC_SCOPE_EXIT { pooled_socket->SetFailed(); };
+        ASSERT_EQ(expected, pooled_socket->_bind_local_side);
+        ASSERT_EQ(options.device_name, pooled_socket->_device_name);
+
+        ASSERT_EQ(0, ptr->SetFailed());
+        ptr->_is_hc_related_ref_held = true;
+        BRPC_SCOPE_EXIT { ptr->_is_hc_related_ref_held = false; };
+        ASSERT_EQ(0, ptr->WaitAndReset(1));
+        ASSERT_EQ(butil::EndPoint(), ptr->local_side());
+        ASSERT_EQ(expected, ptr->_bind_local_side);
+        ASSERT_EQ(options.device_name, ptr->_device_name);
+    }
+}
+
+TEST_F(SocketTest, client_binding_ipv6_connect) {
+    butil::EndPoint point;
+    ASSERT_EQ(0, butil::str2endpoint("[::1]:0", &point));
+    butil::fd_guard listening_fd(butil::tcp_listen(point));
+    ASSERT_GE(listening_fd, 0) << berror();
+    ASSERT_EQ(0, butil::get_local_side(listening_fd, &point));
+    brpc::SocketOptions options;
+    options.remote_side = point;
+    // This port is already occupied by the listener. Binding must use port 0.
+    options.local_side = point;
+    brpc::SocketId id;
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    BRPC_SCOPE_EXIT { brpc::Socket::SetFailed(id); };
+    brpc::SocketUniquePtr ptr;
+    ASSERT_EQ(0, brpc::Socket::Address(id, &ptr));
+    for (int i = 0; i < 2; ++i) {
+        const timespec deadline = butil::milliseconds_from_now(1000);
+        butil::fd_guard fd(ptr->Connect(&deadline, nullptr, nullptr));
+        ASSERT_GE(fd, 0) << berror();
+        butil::EndPoint actual;
+        ASSERT_EQ(0, butil::get_local_side(fd, &actual));
+        sockaddr_storage addr{};
+        ASSERT_EQ(0, butil::endpoint2sockaddr(actual, &addr));
+        ASSERT_EQ(AF_INET6, addr.ss_family);
+        const sockaddr_in6* in6 = reinterpret_cast<const sockaddr_in6*>(&addr);
+        ASSERT_TRUE(IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr));
+        ASSERT_NE(0, in6->sin6_port);
+    }
+}
+
+TEST_F(SocketTest, client_binding_uds_connect) {
+    butil::ScopedTempDir dir;
+    ASSERT_TRUE(dir.CreateUniqueTempDir());
+    // Deliberately use different path lengths for bind() and connect().
+    const std::string server = "unix:" + 
dir.path().Append("server-long.sock").value();
+    const std::string client = "unix:" + dir.path().Append("c.sock").value();
+    brpc::SocketOptions options;
+    ASSERT_EQ(0, butil::str2endpoint(server.c_str(), &options.remote_side));
+    ASSERT_EQ(0, butil::str2endpoint(client.c_str(), &options.local_side));
+    butil::fd_guard listening_fd(butil::tcp_listen(options.remote_side));
+    ASSERT_GE(listening_fd, 0) << berror();
+    brpc::SocketId id;
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    BRPC_SCOPE_EXIT { brpc::Socket::SetFailed(id); };
+    brpc::SocketUniquePtr ptr;
+    ASSERT_EQ(0, brpc::Socket::Address(id, &ptr));
+    const timespec deadline = butil::milliseconds_from_now(1000);
+    butil::fd_guard fd(ptr->Connect(&deadline, nullptr, nullptr));
+    ASSERT_GE(fd, 0) << berror();
+    butil::EndPoint actual;
+    ASSERT_EQ(0, butil::get_local_side(fd, &actual));
+    ASSERT_EQ(AF_UNIX, butil::get_endpoint_type(actual));
+    ASSERT_STREQ(client.c_str(), butil::endpoint2str(actual).c_str());
+}
+
+#if defined(OS_LINUX)
+TEST_F(SocketTest, keep_client_bind_after_revive) {
+    int kCheckInterval = 1;
+    butil::EndPoint point;
+    butil::fd_guard listening_fd;
+    ASSERT_NO_FATAL_FAILURE(ListenOnFreePort(&point, &listening_fd));
+
+    butil::EndPoint bind_point;
+    // A distinct loopback source address (127.0.0.2) is used so that
+    // getsockname() can tell whether the explicit bind() actually happened: 
the
+    // kernel would pick 127.0.0.1 as the source for a 127.0.0.1 destination 
when
+    // no bind() is performed. This relies on the whole 127/8 being loopback,
+    // which is Linux specific.
+    ASSERT_EQ(0, str2endpoint("127.0.0.2:0", &bind_point));
+
+    brpc::SocketId id = 8888;
+    brpc::SocketOptions options;
+    options.remote_side = point;
+    // The explicitly configured client source address 
(ChannelOptions::client_host).
+    options.local_side = bind_point;
+    options.health_check_interval_s = kCheckInterval;
+    options.on_edge_triggered_events = DoNothingOnEdgeTriggeredEvents;
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+
+    brpc::Socket* s = nullptr;
+    {
+        // `WaitAndReset' inside the health check waits until nref drops back
+        // to 2, thus no SocketUniquePtr may be held across `SetFailed'.
+        brpc::SocketUniquePtr ptr;
+        ASSERT_EQ(0, brpc::Socket::Address(id, &ptr));
+        s = ptr.get();
+    }
+    ASSERT_EQ(-1, s->fd());
+
+    // First connection: must bind to the configured source IP.
+    butil::IOBuf src;
+    src.append("hello");
+    ASSERT_EQ(0, s->Write(&src));
+    int64_t start_time = butil::cpuwide_time_us();
+    while (s->fd() < 0) {
+        bthread_usleep(1000);
+        ASSERT_LT(butil::cpuwide_time_us(), start_time + 1000000L);
+    }
+    ASSERT_EQ(bind_point.ip, s->local_side().ip);
+
+    // Fail the Socket and wait for the health check to revive it. This runs
+    // `WaitAndReset` which clears the runtime endpoint, not the binding 
config.
+    ASSERT_EQ(0, s->SetFailed());
+    start_time = butil::cpuwide_time_us();
+    while (brpc::Socket::Status(id) != 0) {
+        bthread_usleep(1000);
+        ASSERT_LT(butil::cpuwide_time_us(),
+                  start_time + kCheckInterval * 1000000L + 1000000L);
+    }
+
+    ASSERT_EQ(butil::EndPoint(), s->local_side());
+    ASSERT_EQ(bind_point, s->_bind_local_side);
+
+    // Reconnect on demand and verify the explicit source IP is still bound.
+    butil::EndPoint local_after_revive;
+    {
+        brpc::SocketUniquePtr ptr;
+        ASSERT_EQ(0, brpc::Socket::Address(id, &ptr));
+        butil::IOBuf src2;
+        src2.append("world");
+        ASSERT_EQ(0, ptr->Write(&src2));
+        start_time = butil::cpuwide_time_us();
+        while (ptr->fd() < 0) {
+            bthread_usleep(1000);
+            ASSERT_LT(butil::cpuwide_time_us(), start_time + 1000000L);
+        }
+        local_after_revive = ptr->local_side();
+    }
+    ASSERT_EQ(bind_point.ip, local_after_revive.ip);
+
+    s->ReleaseHCRelatedReference();
+    // Must close the listening fd before SetFailed, otherwise the health
+    // check still has chance to get reconnected and revive the id.
+    listening_fd.reset(-1);
+    ASSERT_EQ(0, brpc::Socket::SetFailed(id));
+}
+#endif  // OS_LINUX
+
 void* Writer(void* void_arg) {
     WriterArg* arg = static_cast<WriterArg*>(void_arg);
     brpc::SocketUniquePtr sock;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to