This is an automated email from the ASF dual-hosted git repository.
wwbmmm 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 ae09e960 Serve builtin services only on ServerOptions.internal_port
(#3525)
ae09e960 is described below
commit ae09e960c7291605dda52356cc0c2d45567fb53e
Author: Bright Chen <[email protected]>
AuthorDate: Tue Sep 8 22:24:21 2026 +0800
Serve builtin services only on ServerOptions.internal_port (#3525)
---
docs/cn/server.md | 12 +-
docs/en/server.md | 12 +-
src/brpc/details/server_private_accessor.h | 50 +++++-
src/brpc/nshead_pb_service_adaptor.cpp | 17 ++-
src/brpc/policy/baidu_rpc_protocol.cpp | 23 ++-
src/brpc/policy/http_rpc_protocol.cpp | 6 +-
src/brpc/policy/hulu_pbrpc_protocol.cpp | 19 +--
src/brpc/policy/mongo_protocol.cpp | 3 +
src/brpc/policy/nshead_protocol.cpp | 3 +
src/brpc/policy/redis_protocol.cpp | 8 +
src/brpc/policy/sofa_pbrpc_protocol.cpp | 17 ++-
src/brpc/policy/thrift_protocol.cpp | 3 +
src/brpc/server.h | 3 +
test/brpc_http_rpc_protocol_unittest.cpp | 29 ++++
test/brpc_mongo_protocol_unittest.cpp | 23 +++
test/brpc_redis_unittest.cpp | 98 ++++++++++++
test/brpc_server_unittest.cpp | 235 +++++++++++++++++++++++++++++
test/brpc_socket_unittest.cpp | 23 ++-
18 files changed, 533 insertions(+), 51 deletions(-)
diff --git a/docs/cn/server.md b/docs/cn/server.md
index 7a9c47a4..565b7bb3 100644
--- a/docs/cn/server.md
+++ b/docs/cn/server.md
@@ -686,9 +686,19 @@ pthread模式可以让一些老代码快速尝试brpc,但我们仍然建议逐
-
设置内部端口。把ServerOptions.internal_port设为一个**仅允许内网访问**的端口。你可通过internal_port访问到内置服务,但通过对外端口(Server.Start时传入的那个)访问内置服务时将看到如下错误:
```
- [a27eda84bcdeef529a76f22872b78305] Not allowed to access builtin services,
try ServerOptions.internal_port=... instead if you're inside internal network
+ Not allowed to access builtin and Tabbed services, try
ServerOptions.internal_port=... instead if you're in internal network
```
+ 反过来,internal_port只提供内置服务(以及Tabbed服务),普通服务的请求打到这个端口上会被拒绝:
+
+ ```
+ Only builtin and Tabbed services are accessible on
ServerOptions.internal_port=..., send the request to the port passed to
Server::Start() instead
+ ```
+
+
这是必须的:internal_port上的内置服务请求不需要通过ServerOptions.auth的鉴权,而鉴权结果是记在连接上的,一条连接只在第一个请求时鉴权一次。如果普通服务也在这个端口上提供,那么先发一个内置服务请求就能把整条连接标记为已鉴权,后续在同一条连接上访问普通服务将完全跳过鉴权。
+
+
有两类服务不走上面这条错误路径:ServerOptions.http_master_service和ServerOptions.baidu_master_service会接管所有URL和服务名(内置服务的也一样),它们在internal_port上被直接忽略,请求回退到正常的服务查找,这样内置服务仍然可以从这个端口访问;ServerOptions.redis_service在解析阶段就把命令处理完了,没有Controller可以带回EPERM,因此internal_port上干脆不提供redis协议,连接会被直接关闭。
+
- http
proxy指定转发路径。nginx等可配置URL的映射关系,比如下面的配置把访问/MyAPI的外部流量映射到`target-server`的`/ServiceName/MethodName`。当外部流量尝试访问内置服务,比如说/status时,将直接被nginx拒绝。
```nginx
location /MyAPI {
diff --git a/docs/en/server.md b/docs/en/server.md
index 80806db5..f13193a8 100644
--- a/docs/en/server.md
+++ b/docs/en/server.md
@@ -680,9 +680,19 @@ Builtin services are useful, on the other hand include a
lot of internal informa
- Set internal port. Set ServerOptions.internal_port to a port which can
**only be accessible from internal**. You can view builtin services via
internal_port, while accesses from the public port (the one passed to
Server.Start) should see following error:
```
- [a27eda84bcdeef529a76f22872b78305] Not allowed to access builtin services,
try ServerOptions.internal_port=... instead if you're inside internal network
+ Not allowed to access builtin and Tabbed services, try
ServerOptions.internal_port=... instead if you're in internal network
```
+ Conversely internal_port serves builtin (and Tabbed) services only, requests
for ordinary services sent to it are rejected with:
+
+ ```
+ Only builtin and Tabbed services are accessible on
ServerOptions.internal_port=..., send the request to the port passed to
Server::Start() instead
+ ```
+
+ This is necessary: builtin requests on internal_port skip the authentication
of ServerOptions.auth, and the verdict is remembered per connection since a
connection is only authenticated once, on its first request. Were ordinary
services served there as well, sending a builtin request first would mark the
whole connection as authenticated and every later request on it would bypass
authentication entirely.
+
+ Two kinds of services do not take the error path above.
ServerOptions.http_master_service and ServerOptions.baidu_master_service answer
for every URL and service name, the builtin ones included, so they are ignored
on internal_port and the request falls back to the normal lookup, which keeps
the builtin services reachable from that port. ServerOptions.redis_service
handles the commands inside the parser and has no Controller to carry the
EPERM, so internal_port does not speak redis at [...]
+
- http proxies only proxy specified URLs. nginx etc is able to configure how
to map different URLs to back-end servers. For example the configure below maps
public traffic to /MyAPI to `/ServiceName/MethodName` of `target-server`. If
builtin services like /status are accessed from public, nginx rejects the
attempts directly.
```nginx
location /MyAPI {
diff --git a/src/brpc/details/server_private_accessor.h
b/src/brpc/details/server_private_accessor.h
index ee7929dd..0e6e4fbb 100644
--- a/src/brpc/details/server_private_accessor.h
+++ b/src/brpc/details/server_private_accessor.h
@@ -104,10 +104,23 @@ private:
const Server* _server;
};
+inline bool IsBuiltinOrTabbed(const Server::MethodProperty* mp) {
+ return nullptr != mp && (mp->is_builtin_service || mp->params.is_tabbed);
+}
+
+// True if `local_side' is ServerOptions.internal_port.
+inline bool IsInternalPort(const Server& server,
+ const butil::EndPoint& local_side) {
+ return server.options().internal_port >= 0 &&
+ local_side.port == server.options().internal_port;
+}
+
// Reject accesses to builtin services when the server is in security mode,
// in which case they are only reachable from ServerOptions.internal_port.
// Returns true if the access was rejected, in which case `cntl` was already
-// SetFailed() and the caller must stop dispatching the request immediately.
+// SetFailed() and the caller must not let the request reach the service's
+// normal processing. nshead hands the failed Controller to NsheadService
+// instead, as it does for the other pre-checks, see nshead_protocol.cpp.
// NOTE: Call this after ControllerPrivateAccessor::set_security_mode() and
// before the method is counted by MethodStatus::OnRequested(), so that
// rejected accesses do not pollute the stats of the method. `mp` may point
@@ -116,17 +129,42 @@ private:
// this beforehand, or make sure the listing is hidden in security mode.
inline bool RejectBuiltinAccess(Controller* cntl, const Server& server,
const Server::MethodProperty* mp) {
- if (!cntl->is_security_mode() ||
- (!mp->is_builtin_service && !mp->params.is_tabbed)) {
+ if (!cntl->is_security_mode() || !IsBuiltinOrTabbed(mp)) {
+ return false;
+ }
+ cntl->SetFailed(EPERM, "Not allowed to access builtin and Tabbed services,
try "
+ "ServerOptions.internal_port=%d instead if you're
in internal network",
+ server.options().internal_port);
+ return true;
+}
+
+// Reject accesses to non-builtin services arriving at
ServerOptions.internal_port,
+// which exposes builtin services away from the public listener, it is not a
second
+// entrance to the ordinary services of the server.
+// NOTE: Same return contract and placement rules as RejectBuiltinAccess().
+// This overload is for the protocols dispatching to a service that is never
+// builtin (NsheadService, ThriftService), hence has no MethodProperty.
+inline bool RejectNonBuiltinAccessFromInternalPort(Controller* cntl,
+ const Server& server) {
+ if (!IsInternalPort(server, cntl->local_side())) {
return false;
}
- cntl->SetFailed(EPERM, "Not allowed to access builtin services, try "
- "ServerOptions.internal_port=%d instead if
you're in "
- "internal network",
+ cntl->SetFailed(EPERM, "Only builtin and Tabbed services are accessible on
"
+ "ServerOptions.internal_port=%d, send the request
to the port "
+ "passed to Server::Start() instead",
server.options().internal_port);
return true;
}
+// Same as above, but lets builtin and Tabbed methods through.
+inline bool RejectNonBuiltinAccessFromInternalPort(
+ Controller* cntl, const Server& server, const Server::MethodProperty* mp) {
+ if (IsBuiltinOrTabbed(mp)) {
+ return false;
+ }
+ return RejectNonBuiltinAccessFromInternalPort(cntl, server);
+}
+
// Count one error if release() is not called before destruction of this
object.
class ScopedNonServiceError {
public:
diff --git a/src/brpc/nshead_pb_service_adaptor.cpp
b/src/brpc/nshead_pb_service_adaptor.cpp
index 17e29228..3be93224 100644
--- a/src/brpc/nshead_pb_service_adaptor.cpp
+++ b/src/brpc/nshead_pb_service_adaptor.cpp
@@ -114,22 +114,23 @@ void NsheadPbServiceAdaptor::ProcessNsheadRequest(
}
ServerPrivateAccessor server_accessor(&server);
- const Server::MethodProperty *sp = server_accessor
+ const Server::MethodProperty* mp = server_accessor
.FindMethodPropertyByFullName(meta->full_method_name());
- if (nullptr == sp ||
- sp->service->GetDescriptor() == BadMethodService::descriptor()) {
+ if (nullptr == mp ||
+ mp->service->GetDescriptor() == BadMethodService::descriptor()) {
controller->SetFailed(ENOMETHOD, "Fail to find method=%s",
meta->full_method_name().c_str());
break;
}
- if (RejectBuiltinAccess(controller, server, sp)) {
+ if (RejectBuiltinAccess(controller, server, mp) ||
+ RejectNonBuiltinAccessFromInternalPort(controller, server, mp)) {
break;
}
- pbdone->status = sp->status;
- sp->status->OnRequested();
+ pbdone->status = mp->status;
+ mp->status->OnRequested();
- google::protobuf::Service* svc = sp->service;
- const google::protobuf::MethodDescriptor* method = sp->method;
+ google::protobuf::Service* svc = mp->service;
+ const google::protobuf::MethodDescriptor* method = mp->method;
ControllerPrivateAccessor(controller).set_method(method);
done->SetMethodName(butil::EnsureString(method->full_name()));
pbdone->pbreq.reset(svc->GetRequestPrototype(method).New());
diff --git a/src/brpc/policy/baidu_rpc_protocol.cpp
b/src/brpc/policy/baidu_rpc_protocol.cpp
index 5a6451b0..baa691e0 100644
--- a/src/brpc/policy/baidu_rpc_protocol.cpp
+++ b/src/brpc/policy/baidu_rpc_protocol.cpp
@@ -278,6 +278,12 @@ struct BaiduProxyPBMessages : public RpcPBMessages {
};
}
+static bool IsBaiduMasterService(const Server* server,
+ const butil::EndPoint& local_side) {
+ return nullptr != server->options().baidu_master_service &&
+ !IsInternalPort(*server, local_side);
+}
+
// Used by UT, can't be static.
void SendRpcResponse(int64_t correlation_id, Controller* cntl,
RpcPBMessages* messages, const Server* server,
@@ -306,10 +312,10 @@ void SendRpcResponse(int64_t correlation_id, Controller*
cntl,
}
cntl->CallAfterRpcResp(req, res);
- if (nullptr == server->options().baidu_master_service) {
- server->options().rpc_pb_message_factory->Return(messages);
- } else {
+ if (IsBaiduMasterService(server, cntl->local_side())) {
BaiduProxyPBMessages::Return(static_cast<BaiduProxyPBMessages*>(messages));
+ } else {
+ server->options().rpc_pb_message_factory->Return(messages);
}
};
@@ -713,13 +719,13 @@ void ProcessRpcRequest(InputMessageBase* msg_base) {
google::protobuf::Service* svc = nullptr;
google::protobuf::MethodDescriptor* method = nullptr;
- if (nullptr != server->options().baidu_master_service) {
+ if (IsBaiduMasterService(server, cntl->local_side())) {
if (socket->is_overcrowded() &&
!server->options().ignore_eovercrowded &&
!server->options().baidu_master_service->ignore_eovercrowded()) {
- cntl->SetFailed(EOVERCROWDED, "Connection to %s is
overcrowded",
-
butil::endpoint2str(socket->remote_side()).c_str());
- break;
+ cntl->SetFailed(EOVERCROWDED, "Connection to %s is
overcrowded",
+
butil::endpoint2str(socket->remote_side()).c_str());
+ break;
}
svc = server->options().baidu_master_service;
auto sampled_request = new SampledRequest;
@@ -770,7 +776,8 @@ void ProcessRpcRequest(InputMessageBase* msg_base) {
request_meta.method_name().c_str());
break;
}
- if (RejectBuiltinAccess(cntl.get(), *server, mp)) {
+ if (RejectBuiltinAccess(cntl.get(), *server, mp) ||
+ RejectNonBuiltinAccessFromInternalPort(cntl.get(), *server,
mp)) {
break;
}
if (mp->service->GetDescriptor() ==
BadMethodService::descriptor()) {
diff --git a/src/brpc/policy/http_rpc_protocol.cpp
b/src/brpc/policy/http_rpc_protocol.cpp
index 4d1c8824..eb2ac11b 100644
--- a/src/brpc/policy/http_rpc_protocol.cpp
+++ b/src/brpc/policy/http_rpc_protocol.cpp
@@ -1558,7 +1558,8 @@ void ProcessHttpRequest(InputMessageBase *msg) {
return;
}
- if (server->options().http_master_service) {
+ if (server->options().http_master_service &&
+ !IsInternalPort(*server, cntl->local_side())) {
// If http_master_service is on, just call it.
google::protobuf::Service* svc = server->options().http_master_service;
const google::protobuf::MethodDescriptor* md =
@@ -1605,7 +1606,8 @@ void ProcessHttpRequest(InputMessageBase *msg) {
mp->service->CallMethod(mp->method, cntl, &breq, &bres, nullptr);
return;
}
- if (RejectBuiltinAccess(cntl, *server, mp)) {
+ if (RejectBuiltinAccess(cntl, *server, mp) ||
+ RejectNonBuiltinAccessFromInternalPort(cntl, *server, mp)) {
return;
}
// Switch to service-specific error.
diff --git a/src/brpc/policy/hulu_pbrpc_protocol.cpp
b/src/brpc/policy/hulu_pbrpc_protocol.cpp
index 4bacd9e5..146f8858 100644
--- a/src/brpc/policy/hulu_pbrpc_protocol.cpp
+++ b/src/brpc/policy/hulu_pbrpc_protocol.cpp
@@ -442,27 +442,28 @@ void ProcessHuluRequest(InputMessageBase* msg_base) {
break;
}
- const Server::MethodProperty *sp =
+ const Server::MethodProperty* mp =
server_accessor.FindMethodPropertyByNameAndIndex(
meta.service_name(), meta.method_index());
- if (nullptr == sp) {
+ if (nullptr == mp) {
cntl->SetFailed(ENOMETHOD, "Fail to find method=%d of service=%s",
meta.method_index(), meta.service_name().c_str());
break;
}
- if (RejectBuiltinAccess(cntl.get(), *server, sp)) {
+ if (RejectBuiltinAccess(cntl.get(), *server, mp) ||
+ RejectNonBuiltinAccessFromInternalPort(cntl.get(), *server, mp)) {
break;
}
- if (sp->service->GetDescriptor() == BadMethodService::descriptor()) {
+ if (mp->service->GetDescriptor() == BadMethodService::descriptor()) {
BadMethodRequest breq;
BadMethodResponse bres;
breq.set_service_name(meta.service_name());
- sp->service->CallMethod(sp->method, cntl.get(), &breq, &bres,
nullptr);
+ mp->service->CallMethod(mp->method, cntl.get(), &breq, &bres,
nullptr);
break;
}
if (socket->is_overcrowded() &&
!server->options().ignore_eovercrowded &&
- !sp->ignore_eovercrowded) {
+ !mp->ignore_eovercrowded) {
cntl->SetFailed(EOVERCROWDED, "Connection to %s is overcrowded",
butil::endpoint2str(socket->remote_side()).c_str());
break;
@@ -470,8 +471,8 @@ void ProcessHuluRequest(InputMessageBase* msg_base) {
// Switch to service-specific error.
non_service_error.release();
- method_status = sp->status;
- const google::protobuf::MethodDescriptor* method = sp->method;
+ method_status = mp->status;
+ const google::protobuf::MethodDescriptor* method = mp->method;
const std::string method_full_name =
butil::EnsureString(method->full_name());
if (method_status) {
int rejected_cc = 0;
@@ -482,7 +483,7 @@ void ProcessHuluRequest(InputMessageBase* msg_base) {
}
}
- google::protobuf::Service* svc = sp->service;
+ google::protobuf::Service* svc = mp->service;
accessor.set_method(method);
if (!server->AcceptRequest(cntl.get())) {
diff --git a/src/brpc/policy/mongo_protocol.cpp
b/src/brpc/policy/mongo_protocol.cpp
index 3c7262d4..35647d79 100644
--- a/src/brpc/policy/mongo_protocol.cpp
+++ b/src/brpc/policy/mongo_protocol.cpp
@@ -252,6 +252,9 @@ void ProcessMongoRequest(InputMessageBase* msg_base) {
mongo_done->cntl.SetFailed(ENOMETHOD, "Fail to find
default_method");
break;
}
+ if (RejectNonBuiltinAccessFromInternalPort(&mongo_done->cntl, *server,
mp)) {
+ break;
+ }
// Switch to service-specific error.
non_service_error.release();
MethodStatus* method_status = mp->status;
diff --git a/src/brpc/policy/nshead_protocol.cpp
b/src/brpc/policy/nshead_protocol.cpp
index 72fc8994..5042b922 100644
--- a/src/brpc/policy/nshead_protocol.cpp
+++ b/src/brpc/policy/nshead_protocol.cpp
@@ -314,6 +314,9 @@ void ProcessNsheadRequest(InputMessageBase* msg_base) {
cntl->SetFailed(ELOGOFF, "Server is stopping");
break;
}
+ if (RejectNonBuiltinAccessFromInternalPort(cntl, *server)) {
+ break;
+ }
if (socket->is_overcrowded() &&
!server->options().ignore_eovercrowded) {
cntl->SetFailed(EOVERCROWDED, "Connection to %s is overcrowded",
butil::endpoint2str(socket->remote_side()).c_str());
diff --git a/src/brpc/policy/redis_protocol.cpp
b/src/brpc/policy/redis_protocol.cpp
index 3009d7b7..e46bed95 100644
--- a/src/brpc/policy/redis_protocol.cpp
+++ b/src/brpc/policy/redis_protocol.cpp
@@ -125,6 +125,14 @@ ParseResult ParseRedisMessage(butil::IOBuf* source,
Socket* socket,
if (!rs) {
return MakeParseError(PARSE_ERROR_TRY_OTHERS);
}
+ if (IsInternalPort(*server, socket->local_side())) {
+ // ServerOptions.internal_port serves builtin and Tabbed services
+ // only and a RedisService is neither. The command handlers run
+ // right here rather than in ProcessRedisRequest(), there is no
+ // Controller to reject the request with, so behave as if redis
+ // was not enabled on this port at all.
+ return MakeParseError(PARSE_ERROR_TRY_OTHERS);
+ }
RedisConnContext* ctx =
static_cast<RedisConnContext*>(socket->parsing_context());
if (ctx == nullptr) {
ctx = new RedisConnContext(rs);
diff --git a/src/brpc/policy/sofa_pbrpc_protocol.cpp
b/src/brpc/policy/sofa_pbrpc_protocol.cpp
index d0c42cc4..fa512590 100644
--- a/src/brpc/policy/sofa_pbrpc_protocol.cpp
+++ b/src/brpc/policy/sofa_pbrpc_protocol.cpp
@@ -403,36 +403,37 @@ void ProcessSofaRequest(InputMessageBase* msg_base) {
break;
}
- const Server::MethodProperty *sp =
+ const Server::MethodProperty* mp =
server_accessor.FindMethodPropertyByFullName(meta.method());
- if (nullptr == sp) {
+ if (nullptr == mp) {
cntl->SetFailed(ENOMETHOD, "Fail to find method=%s",
meta.method().c_str());
break;
}
- if (RejectBuiltinAccess(cntl.get(), *server, sp)) {
+ if (RejectBuiltinAccess(cntl.get(), *server, mp) ||
+ RejectNonBuiltinAccessFromInternalPort(cntl.get(), *server, mp)) {
break;
}
if (socket->is_overcrowded() &&
!server->options().ignore_eovercrowded &&
- !sp->ignore_eovercrowded) {
+ !mp->ignore_eovercrowded) {
cntl->SetFailed(EOVERCROWDED, "Connection to %s is overcrowded",
butil::endpoint2str(socket->remote_side()).c_str());
break;
}
// Switch to service-specific error.
non_service_error.release();
- method_status = sp->status;
+ method_status = mp->status;
if (method_status) {
int rejected_cc = 0;
if (!method_status->OnRequested(&rejected_cc)) {
cntl->SetFailed(ELIMIT, "Rejected by %s's ConcurrencyLimiter,
concurrency=%d",
-
butil::EnsureString(sp->method->full_name()).c_str(), rejected_cc);
+
butil::EnsureString(mp->method->full_name()).c_str(), rejected_cc);
break;
}
}
- google::protobuf::Service* svc = sp->service;
- const google::protobuf::MethodDescriptor* method = sp->method;
+ google::protobuf::Service* svc = mp->service;
+ const google::protobuf::MethodDescriptor* method = mp->method;
accessor.set_method(method);
if (!server->AcceptRequest(cntl.get())) {
diff --git a/src/brpc/policy/thrift_protocol.cpp
b/src/brpc/policy/thrift_protocol.cpp
index e97dd00b..735abde4 100755
--- a/src/brpc/policy/thrift_protocol.cpp
+++ b/src/brpc/policy/thrift_protocol.cpp
@@ -510,6 +510,9 @@ void ProcessThriftRequest(InputMessageBase* msg_base) {
" ServerOptions.thrift_service, close the connection.";
return cntl->SetFailed(EINTERNAL, "ServerOptions.thrift_service is
NULL");
}
+ if (RejectNonBuiltinAccessFromInternalPort(cntl, *server)) {
+ return;
+ }
// Switch to service-specific error.
non_service_error.release();
diff --git a/src/brpc/server.h b/src/brpc/server.h
index 276e8795..6e7d2b2b 100644
--- a/src/brpc/server.h
+++ b/src/brpc/server.h
@@ -202,6 +202,9 @@ struct ServerOptions {
// hiding them from public. Setting this option also enables security
// protection code which we may add constantly.
// Update: this option affects Tabbed services as well.
+ // Update: this port carries builtin and Tabbed services only, requests
+ // for ordinary services are rejected with EPERM and must be sent to the
+ // port passed to Start().
// Default: -1
int internal_port;
diff --git a/test/brpc_http_rpc_protocol_unittest.cpp
b/test/brpc_http_rpc_protocol_unittest.cpp
index f23bbfb7..10fb5cc3 100644
--- a/test/brpc_http_rpc_protocol_unittest.cpp
+++ b/test/brpc_http_rpc_protocol_unittest.cpp
@@ -563,6 +563,35 @@ TEST_F(HttpTest,
builtin_auth_policy_on_public_and_internal_port) {
ASSERT_TRUE(protected_cntl.Failed());
}
+ {
+ // A builtin request is exempted from authentication on internal_port
+ // and its verdict latches the whole connection, so the exemption would
+ // carry over to whatever is sent next on that very connection. Only
+ // builtin services are served there, which keeps the latch harmless.
+ const std::string connection_group = "builtin-auth-policy-internal";
+ brpc::Channel builtin_channel;
+ brpc::Channel protected_channel;
+ brpc::ChannelOptions copt;
+ copt.protocol = brpc::PROTOCOL_HTTP;
+ copt.connection_type = brpc::CONNECTION_TYPE_POOLED;
+ copt.connection_group = connection_group;
+ copt.max_retry = 0;
+ ASSERT_EQ(0, builtin_channel.Init(internal_ep, &copt));
+ ASSERT_EQ(0, protected_channel.Init(internal_ep, &copt));
+
+ brpc::Controller builtin_cntl;
+ CallVersion(&builtin_channel, &builtin_cntl);
+ ASSERT_FALSE(builtin_cntl.Failed()) << builtin_cntl.ErrorText();
+ ASSERT_EQ(brpc::HTTP_STATUS_OK,
builtin_cntl.http_response().status_code());
+
+ brpc::Controller protected_cntl;
+ CallHttpEcho(&protected_channel, &protected_cntl);
+ ASSERT_TRUE(protected_cntl.Failed());
+ ASSERT_EQ(brpc::EHTTP, protected_cntl.ErrorCode()) <<
protected_cntl.ErrorText();
+ ASSERT_EQ(brpc::HTTP_STATUS_FORBIDDEN,
+ protected_cntl.http_response().status_code());
+ }
+
ASSERT_EQ(0, server.Stop(0));
ASSERT_EQ(0, server.Join());
brpc::FLAGS_max_connection_pool_size = saved_max_connection_pool_size;
diff --git a/test/brpc_mongo_protocol_unittest.cpp
b/test/brpc_mongo_protocol_unittest.cpp
index 68e4acdc..3ec22566 100644
--- a/test/brpc_mongo_protocol_unittest.cpp
+++ b/test/brpc_mongo_protocol_unittest.cpp
@@ -181,6 +181,29 @@ TEST_F(MongoTest, process_request_failed_socket) {
ASSERT_EQ(0ll, _server._nerror_bvar.get_value());
}
+TEST_F(MongoTest, process_request_from_internal_port) {
+ // The fixture talks over a pipe, which has no local address at all while
+ // Server::Start() never accepts 0 as ServerOptions.internal_port. Give the
+ // connection an address the server can recognize as internal, the port is
+ // arbitrary as nothing listens on it.
+ _socket->_local_side = butil::EndPoint(butil::IP_ANY, 8888);
+ _server._options.internal_port = _socket->local_side().port;
+
+ brpc::mongo_head_t header = { 0, 0, 0, 0 };
+ header.op_code = brpc::MONGO_OPCODE_REPLY;
+ header.message_length = sizeof(header) + EXP_REQUEST.length();
+ butil::IOBuf total_buf;
+ total_buf.append(static_cast<const void*>(&header), sizeof(header));
+ total_buf.append(EXP_REQUEST);
+ brpc::ParseResult req_pr = brpc::policy::ParseMongoMessage(
+ &total_buf, _socket.get(), false, &_server);
+ ASSERT_EQ(brpc::PARSE_OK, req_pr.error());
+ ProcessMessage(brpc::policy::ProcessMongoRequest, req_pr.message(), false);
+ // MyEchoService was never reached, the request was counted as an error of
+ // the server rather than as a call of the method.
+ ASSERT_EQ(1ll, _server._nerror_bvar.get_value());
+}
+
TEST_F(MongoTest, complete_flow) {
butil::IOBuf request_buf;
butil::IOBuf total_buf;
diff --git a/test/brpc_redis_unittest.cpp b/test/brpc_redis_unittest.cpp
index f4830d04..724b5e77 100644
--- a/test/brpc_redis_unittest.cpp
+++ b/test/brpc_redis_unittest.cpp
@@ -19,6 +19,8 @@
#include <iostream>
#include <memory>
#include <unordered_map>
+#include <butil/endpoint.h>
+#include <butil/fd_guard.h>
#include <butil/time.h>
#include <butil/logging.h>
#include <brpc/redis.h>
@@ -1223,6 +1225,102 @@ TEST_F(RedisTest, server_sanity) {
ASSERT_EQ("", response.reply(3).data());
}
+// Returns a port nothing is listening on, or -1. ServerOptions.internal_port
+// has to be an explicit number, Server::Start() rejects 0 because it stands
+// for an ephemeral port, so ask the system for a free one rather than hardcode
+// a port that another test may be listening on.
+static int PickUnusedPort() {
+ butil::fd_guard sockfd(butil::tcp_listen(butil::EndPoint(butil::IP_ANY,
0)));
+ if (sockfd < 0) {
+ return -1;
+ }
+ butil::EndPoint point;
+ if (butil::get_local_side(sockfd, &point) != 0) {
+ return -1;
+ }
+ return point.port;
+}
+
+// Starts `server' on an ephemeral port and fills options->internal_port with
+// another one. Both are released before Start() binds them and something else
+// may take one in between, hence the retries. Returns 0 on success.
+static int StartWithInternalPort(brpc::Server* server,
+ brpc::ServerOptions* options) {
+ for (int i = 0; i < 10; ++i) {
+ int internal_port = PickUnusedPort();
+ if (internal_port < 0) {
+ continue;
+ }
+ options->internal_port = internal_port;
+ if (0 == server->Start("127.0.0.1:0", options)) {
+ return 0;
+ }
+ }
+ return -1;
+}
+
+TEST_F(RedisTest, server_is_not_served_on_internal_port) {
+ std::string password = GeneratePassword();
+ std::unique_ptr<brpc::policy::RedisAuthenticator> redis_auth_holder(
+ new brpc::policy::RedisAuthenticator(password));
+ RedisServiceImpl* rsimpl = new RedisServiceImpl(password);
+ std::unique_ptr<SetCommandHandler> sh(new SetCommandHandler(rsimpl));
+ std::unique_ptr<AuthCommandHandler> ah(new AuthCommandHandler(rsimpl));
+ rsimpl->AddCommandHandler("set", sh.get());
+ rsimpl->AddCommandHandler("auth", ah.get());
+
+ brpc::Server server;
+ brpc::ServerOptions server_options;
+ server_options.redis_service = rsimpl;
+ ASSERT_EQ(0, StartWithInternalPort(&server, &server_options));
+
+ brpc::ChannelOptions options;
+ options.protocol = brpc::PROTOCOL_REDIS;
+ options.auth = redis_auth_holder.get();
+ options.max_retry = 0;
+
+ brpc::RedisRequest request;
+ ASSERT_TRUE(request.AddCommand("set key1 value1"));
+
+ // The internal port is up and serving its builtin services, it just does
+ // not speak redis, so the command below fails to be parsed rather than
+ // fails to be sent.
+ brpc::ChannelOptions http_options;
+ http_options.protocol = brpc::PROTOCOL_HTTP;
+ http_options.max_retry = 0;
+ brpc::Channel http_channel;
+ ASSERT_EQ(0, http_channel.Init(
+ "127.0.0.1", server_options.internal_port, &http_options));
+ brpc::Controller http_cntl;
+ http_cntl.http_request().uri() = "/version";
+ http_channel.CallMethod(nullptr, &http_cntl, nullptr, nullptr, nullptr);
+ ASSERT_FALSE(http_cntl.Failed()) << http_cntl.ErrorText();
+
+ brpc::Channel internal_channel;
+ ASSERT_EQ(0, internal_channel.Init(
+ "127.0.0.1", server_options.internal_port, &options));
+ brpc::RedisResponse response;
+ brpc::Controller cntl;
+ internal_channel.CallMethod(nullptr, &cntl, &request, &response, nullptr);
+ ASSERT_TRUE(cntl.Failed());
+ ASSERT_EQ(0, response.reply_size());
+
+ // The port passed to Start() speaks redis as before.
+ brpc::Channel channel;
+ ASSERT_EQ(0, channel.Init(
+ "127.0.0.1", server.listen_address().port, &options));
+ cntl.Reset();
+ response.Clear();
+ channel.CallMethod(nullptr, &cntl, &request, &response, nullptr);
+ ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+ ASSERT_EQ(1, response.reply_size());
+ ASSERT_EQ(brpc::REDIS_REPLY_STATUS, response.reply(0).type());
+ ASSERT_STREQ("OK", response.reply(0).c_str());
+
+ ASSERT_EQ(0, server.Stop(0));
+ ASSERT_EQ(0, server.Join());
+}
+
void* incr_thread(void* arg) {
brpc::Channel* c = static_cast<brpc::Channel*>(arg);
for (int i = 0; i < 5000; ++i) {
diff --git a/test/brpc_server_unittest.cpp b/test/brpc_server_unittest.cpp
index ed1fa91c..e94d8508 100644
--- a/test/brpc_server_unittest.cpp
+++ b/test/brpc_server_unittest.cpp
@@ -48,6 +48,7 @@
#include "brpc/builtin/sockets_service.h" // SocketsService
#include "brpc/builtin/bad_method_service.h"
#include "brpc/server.h"
+#include "brpc/nshead_service.h"
#include "brpc/restful.h"
#include "brpc/channel.h"
#include "brpc/socket_map.h"
@@ -57,6 +58,7 @@
#include "v1.pb.h"
#include "v2.pb.h"
#include "v3.pb.h"
+#include "health_check.pb.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
@@ -1719,6 +1721,158 @@ TEST_F(ServerTest,
builtin_services_are_gated_by_internal_port) {
ASSERT_EQ(0, server.Join());
}
+// Call the same ordinary service the way a browser would.
+void CallEchoByHttp(const butil::EndPoint& ep, brpc::Controller* cntl) {
+ brpc::ChannelOptions copt;
+ copt.protocol = brpc::PROTOCOL_HTTP;
+ copt.max_retry = 0;
+ brpc::Channel chan;
+ ASSERT_EQ(0, chan.Init(ep, &copt));
+ test::EchoRequest req;
+ test::EchoResponse res;
+ req.set_message(EXP_REQUEST);
+ cntl->http_request().uri() = "/EchoService/Echo";
+ cntl->http_request().set_method(brpc::HTTP_METHOD_POST);
+ cntl->http_request().set_content_type("application/json");
+ chan.CallMethod(nullptr, cntl, &req, &res, nullptr);
+}
+
+// Returns a port nothing is listening on, or -1. `ServerOptions.internal_port`
+// has to be an explicit number, Server::Start() rejects 0 because it stands
+// for an ephemeral port, so ask the system for a free one rather than hardcode
+// a port that another test may be listening on.
+int PickUnusedPort() {
+ butil::fd_guard sockfd(butil::tcp_listen(butil::EndPoint(butil::IP_ANY,
0)));
+ if (sockfd < 0) {
+ return -1;
+ }
+ butil::EndPoint point;
+ if (butil::get_local_side(sockfd, &point) != 0) {
+ return -1;
+ }
+ return point.port;
+}
+
+// Starts `server` on an ephemeral port and fills `options->internal_port` with
+// another one. Both are released before Start() binds them and something else
+// may take one in between, hence the retries. Returns 0 on success.
+int StartWithInternalPort(brpc::Server* server, brpc::ServerOptions* options) {
+ for (int i = 0; i < 10; ++i) {
+ int internal_port = PickUnusedPort();
+ if (internal_port < 0) {
+ continue;
+ }
+ options->internal_port = internal_port;
+ if (0 == server->Start("127.0.0.1:0", options)) {
+ return 0;
+ }
+ }
+ return -1;
+}
+
+TEST_F(ServerTest, ordinary_services_are_not_served_on_internal_port) {
+ const struct {
+ brpc::ProtocolType protocol;
+ const char* name;
+ } cases[] = {
+ { brpc::PROTOCOL_BAIDU_STD, "baidu_std" },
+ { brpc::PROTOCOL_HULU_PBRPC, "hulu_pbrpc" },
+ { brpc::PROTOCOL_SOFA_PBRPC, "sofa_pbrpc" },
+ };
+
+ brpc::Server server;
+ EchoServiceImpl echo_svc;
+ ASSERT_EQ(0, server.AddService(&echo_svc,
brpc::SERVER_DOESNT_OWN_SERVICE));
+ brpc::ServerOptions opt;
+ ASSERT_EQ(0, StartWithInternalPort(&server, &opt));
+ butil::EndPoint ep = server.listen_address();
+ butil::EndPoint internal_ep(ep.ip, opt.internal_port);
+
+ for (size_t i = 0; i < arraysize(cases); ++i) {
+ brpc::Controller cntl;
+ CallEchoByPb(internal_ep, cases[i].protocol, &cntl);
+ ASSERT_EQ(EPERM, cntl.ErrorCode())
+ << cases[i].name << ": " << cntl.ErrorText();
+
+ // The public port is where ordinary services live.
+ cntl.Reset();
+ CallEchoByPb(ep, cases[i].protocol, &cntl);
+ ASSERT_FALSE(cntl.Failed())
+ << cases[i].name << ": " << cntl.ErrorText();
+ }
+
+ brpc::Controller cntl;
+ CallEchoByHttp(internal_ep, &cntl);
+ ASSERT_TRUE(cntl.Failed());
+ ASSERT_EQ(brpc::HTTP_STATUS_FORBIDDEN, cntl.http_response().status_code())
+ << cntl.ErrorText();
+ cntl.Reset();
+ CallEchoByHttp(ep, &cntl);
+ ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+
+ ASSERT_EQ(0, server.Stop(0));
+ ASSERT_EQ(0, server.Join());
+}
+
+// NsheadService is dispatched to without a MethodProperty, the gate has to be
+// applied by the protocol itself. The service echoes back what the framework
+// decided so that the client can tell an acceptance from a rejection: nshead
+// carries no error field.
+class EchoNsheadService : public brpc::NsheadService {
+public:
+ void ProcessNsheadRequest(const brpc::Server&,
+ brpc::Controller* cntl,
+ const brpc::NsheadMessage& request,
+ brpc::NsheadMessage* response,
+ brpc::NsheadClosure* done) override {
+ brpc::ClosureGuard done_guard(done);
+ if (cntl->Failed()) {
+ response->body.append(butil::string_printf("%d",
cntl->ErrorCode()));
+ return;
+ }
+ response->body.append(EXP_RESPONSE);
+ }
+};
+
+// Same gate as ordinary_services_are_not_served_on_internal_port, for the
+// protocols that dispatch to a service which is never builtin. Their verify()
+// refuses every request when ServerOptions.auth is set, so the connection
+// latched by an exempted builtin request is the only way to reach them.
+TEST_F(ServerTest, nshead_service_is_not_served_on_internal_port) {
+ brpc::Server server;
+ brpc::ServerOptions opt;
+ opt.nshead_service = new EchoNsheadService;
+ ASSERT_EQ(0, StartWithInternalPort(&server, &opt));
+ butil::EndPoint ep = server.listen_address();
+ butil::EndPoint internal_ep(ep.ip, opt.internal_port);
+
+ brpc::ChannelOptions copt;
+ copt.protocol = brpc::PROTOCOL_NSHEAD;
+ copt.connection_type = brpc::CONNECTION_TYPE_POOLED;
+ copt.max_retry = 0;
+
+ brpc::Channel internal_chan;
+ ASSERT_EQ(0, internal_chan.Init(internal_ep, &copt));
+ brpc::NsheadMessage req;
+ brpc::NsheadMessage res;
+ brpc::Controller cntl;
+ req.body.append(EXP_REQUEST);
+ internal_chan.CallMethod(nullptr, &cntl, &req, &res, nullptr);
+ ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+ ASSERT_EQ(butil::string_printf("%d", EPERM), res.body.to_string());
+
+ brpc::Channel chan;
+ ASSERT_EQ(0, chan.Init(ep, &copt));
+ cntl.Reset();
+ res.body.clear();
+ chan.CallMethod(nullptr, &cntl, &req, &res, nullptr);
+ ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+ ASSERT_EQ(EXP_RESPONSE, res.body.to_string());
+
+ ASSERT_EQ(0, server.Stop(0));
+ ASSERT_EQ(0, server.Join());
+}
+
// A service-name-only URL is dispatched to the builtin BadMethodService which
// lists the methods of the service.
void CallServiceWithoutMethodByHttp(const butil::EndPoint& ep,
@@ -2144,6 +2298,87 @@ TEST_F(ServerTest, baidu_master_service) {
ASSERT_EQ(0, server.Join());
}
+class HttpMasterServiceImpl : public test::HealthCheckTestService {
+public:
+ void default_method(google::protobuf::RpcController* cntl_base,
+ const test::HealthCheckRequest*,
+ test::HealthCheckResponse*,
+ google::protobuf::Closure* done) override {
+ brpc::ClosureGuard done_guard(done);
+ brpc::Controller* cntl = static_cast<brpc::Controller*>(cntl_base);
+ cntl->response_attachment().append(EXP_RESPONSE);
+ }
+};
+
+// http_master_service answers every URL and carries the payload in the
+// attachments rather than in pb messages.
+void CallEchoByHttpWithoutPb(const butil::EndPoint& ep, brpc::Controller*
cntl) {
+ brpc::ChannelOptions copt;
+ copt.protocol = brpc::PROTOCOL_HTTP;
+ copt.max_retry = 0;
+ brpc::Channel chan;
+ ASSERT_EQ(0, chan.Init(ep, &copt));
+ cntl->http_request().uri() = "/EchoService/Echo";
+ chan.CallMethod(nullptr, cntl, nullptr, nullptr, nullptr);
+}
+
+TEST_F(ServerTest, master_services_are_not_served_on_internal_port) {
+ brpc::Server server;
+ EchoServiceImpl echo_svc;
+ ASSERT_EQ(0, server.AddService(&echo_svc,
brpc::SERVER_DOESNT_OWN_SERVICE));
+ brpc::ServerOptions opt;
+ opt.baidu_master_service = new BaiduMasterServiceImpl;
+ opt.http_master_service = new HttpMasterServiceImpl;
+ ASSERT_EQ(0, StartWithInternalPort(&server, &opt));
+ butil::EndPoint ep = server.listen_address();
+ butil::EndPoint internal_ep(ep.ip, opt.internal_port);
+
+ // The master services answer on the public port.
+ brpc::ChannelOptions copt;
+ copt.protocol = brpc::PROTOCOL_BAIDU_STD;
+ copt.max_retry = 0;
+ brpc::Channel channel;
+ ASSERT_EQ(0, channel.Init(ep, &copt));
+ TestBaiduMasterService(channel, brpc::COMPRESS_TYPE_NONE);
+
+ brpc::Controller cntl;
+ CallEchoByHttpWithoutPb(ep, &cntl);
+ ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+ ASSERT_EQ(EXP_RESPONSE, cntl.response_attachment().to_string());
+
+ // Not on internal_port though, where the request falls back to the normal
+ // resolution and EchoService is rejected as any other ordinary service.
+ cntl.Reset();
+ CallEchoByPb(internal_ep, brpc::PROTOCOL_BAIDU_STD, &cntl);
+ ASSERT_EQ(EPERM, cntl.ErrorCode()) << cntl.ErrorText();
+
+ cntl.Reset();
+ CallEchoByHttpWithoutPb(internal_ep, &cntl);
+ ASSERT_TRUE(cntl.Failed());
+ ASSERT_EQ(brpc::HTTP_STATUS_FORBIDDEN, cntl.http_response().status_code())
+ << cntl.ErrorText();
+
+ // The builtin services are still served there, which is what the port is
+ // for. Rejecting the master service outright would have hidden them.
+ cntl.Reset();
+ CallVersionByPb(internal_ep, brpc::PROTOCOL_BAIDU_STD, &cntl);
+ ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+
+ cntl.Reset();
+ CallVersionByHttp(internal_ep, &cntl);
+ ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+
+ // The requests above did not go through the master service and their
+ // messages must not have been recycled as if they did, otherwise the pool
+ // of the master service is left with objects of another type in it.
+ for (int i = 0; i < 10; ++i) {
+ TestBaiduMasterService(channel, brpc::COMPRESS_TYPE_NONE);
+ }
+
+ ASSERT_EQ(0, server.Stop(0));
+ ASSERT_EQ(0, server.Join());
+}
+
void TestGenericCall(brpc::Channel& channel, brpc::ContentType content_type,
brpc::CompressType compress_type,
brpc::ChecksumType checksum_type) {
diff --git a/test/brpc_socket_unittest.cpp b/test/brpc_socket_unittest.cpp
index 0d198106..6e616cbc 100644
--- a/test/brpc_socket_unittest.cpp
+++ b/test/brpc_socket_unittest.cpp
@@ -118,6 +118,17 @@ class CheckRecycle : public brpc::SocketUser {
}
};
+// Acceptor::Join() returns as soon as Acceptor::BeforeRecycle() cleared
+// _listened_fd, which Socket::BeforeRecycled() calls several statements before
+// it closes the fd, so the fd may still be open when Join() returns.
+void WaitForClosedFd(int fd) {
+ for (int i = 0; i < 100 && fcntl(fd, F_GETFD) != -1; ++i) {
+ bthread_usleep(10000);
+ }
+ ASSERT_EQ(-1, fcntl(fd, F_GETFD));
+ ASSERT_EQ(EBADF, errno);
+}
+
// Never hardcode a port in tests: it may be taken by another process or by
// another test in this binary, making the test fail for unrelated reasons.
// Listen on port 0 and ask the kernel which port it picked instead.
@@ -443,8 +454,7 @@ TEST_F(SocketTest, single_threaded_connect_and_write) {
messenger->StopAccept(0);
messenger->Join();
ASSERT_EQ(-1, messenger->listened_fd());
- ASSERT_EQ(-1, fcntl(listening_fd, F_GETFD));
- ASSERT_EQ(EBADF, errno);
+ ASSERT_NO_FATAL_FAILURE(WaitForClosedFd(listening_fd));
// The socket object is likely to be reused,
// and the local side should be initialized.
@@ -819,8 +829,7 @@ TEST_F(SocketTest, health_check) {
messenger->StopAccept(0);
messenger->Join();
ASSERT_EQ(-1, messenger->listened_fd());
- ASSERT_EQ(-1, fcntl(listening_fd, F_GETFD));
- ASSERT_EQ(EBADF, errno);
+ ASSERT_NO_FATAL_FAILURE(WaitForClosedFd(listening_fd));
ASSERT_EQ(0, brpc::Socket::SetFailed(id));
// StartHealthCheck is possibly still addressing the Socket.
@@ -1560,8 +1569,7 @@ TEST_F(SocketTest, keepalive_input_message) {
messenger->StopAccept(0);
messenger->Join();
ASSERT_EQ(-1, messenger->listened_fd());
- ASSERT_EQ(-1, fcntl(listening_fd, F_GETFD));
- ASSERT_EQ(EBADF, errno);
+ ASSERT_NO_FATAL_FAILURE(WaitForClosedFd(listening_fd));
}
TEST_F(SocketTest, socket_buffer_options_before_connect) {
@@ -1728,8 +1736,7 @@ TEST_F(SocketTest, tcp_user_timeout) {
messenger->StopAccept(0);
messenger->Join();
ASSERT_EQ(-1, messenger->listened_fd());
- ASSERT_EQ(-1, fcntl(listening_fd, F_GETFD));
- ASSERT_EQ(EBADF, errno);
+ ASSERT_NO_FATAL_FAILURE(WaitForClosedFd(listening_fd));
}
#endif
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]