Expose a C++ Boost.Beast HTTP Server with rstream
Add a published rstream HTTP tunnel to a native C++ Boost.Beast server without putting an inbound port on the machine.
A Boost.Beast service usually starts with a TCP acceptor, an http::async_read, and an http::async_write. That shape is explicit, efficient, and easy to embed in gateways, appliances, desktop tools, and native backend services. This guide keeps that Beast session model while changing the ingress path. The process opens an outbound rstream session, publishes an HTTP tunnel, and accepts rstream sockets directly inside the C++ application.
The result is still a native C++ HTTP server. It can run on a customer site, factory network, lab machine, private VPC, or embedded gateway without exposing a local inbound port.
The companion sample contains a complete CMake project.
rstream examples / cpp-beast-rstream-tunnelOpen the C++ Boost.Beast rstream tunnel sample.git clone https://github.com/rstreamlabs/rstream-examples.git
cd rstream-examples/cpp-beast-rstream-tunnelWhat You Will Build
The native process owns the HTTP application and the rstream tunnel. It opens the tunnel outbound, and Beast receives rstream sockets directly.
Browser, webhook provider, operator
|
| HTTPS request
v
rstream public URL
|
| outbound agent session
v
C++ process
|
| rstream::io_rstrm::socket
v
Boost.Beast HTTP handlerThe guide uses the tunnel-specific C++ API first because it gives the service direct control over tunnel properties, labels, forwarding address reporting, and shutdown. The final section shows the generic io_rstrm::socket dial path for private service-to-service calls.
The sample uses C++20 coroutines with asio::use_awaitable to keep the code compact. The SDK operations follow Boost.Asio's completion token model, so the same asynchronous calls can also be used with classic completion handlers, asio::use_future, or another completion token style already used by the application.
Prerequisites
You need a C++20 compiler, CMake, Ninja, Conan 2, and the rstream CLI. The sample consumes the rstream C++ SDK through Conan, so you do not have to clone and install rstream-cpp by hand before following the guide.
Log in and select the project that will own the published HTTP tunnel.
rstream login
rstream project use <project-endpoint> --defaultPublished HTTP tunnels depend on the selected project or self-hosted engine capabilities. If you add rstream Auth, trusted IPs, or mTLS policy at the edge, keep the application’s own authentication and authorization rules in place as well.
Install the SDK with Conan
The example repository contains a conanfile.py that requires Boost and rstream/[>=1.8.1 <2]. Add the rstream Conan remote once, then let the sample Makefile run conan install, configure CMake from the generated preset, build, and install the binary under out/bin. Conan downloads compatible SDK binaries when the local profile matches a published package and builds missing packages from source otherwise. The sample disables SDK utility binaries and optional terminal and GeoIP features for application builds, so a source fallback focuses on the libraries used by the Beast server.
conan profile detect --force
conan remote add rstream https://nexus.rstream.io/repository/conan -f
make verifymake verify keeps the build path copy-pasteable while using the same Conan/CMake workflow a production project would use.
For an application that already consumes Conan, the important parts are the package requirement and the CMake target.
requires = (
"boost/[>=1.83 <2]",
"rstream/[>=1.8.1 <2]",
)
default_options = {
"rstream/*:build_bins": False,
"rstream/*:enable_testing": False,
"rstream/*:with_maxminddb": False,
"rstream/*:with_ncurses": False,
}find_package(rstream CONFIG REQUIRED)
target_link_libraries(my_server PRIVATE rstream::rstream Boost::boost)Start from a Beast Session
A normal Beast session reads an HTTP request from an asynchronous stream and writes a response. Beast works with any stream that satisfies AsyncReadStream and AsyncWriteStream, including rstream::io_rstrm::socket.
asio::awaitable<void> serve_session(rstrm::socket socket)
{
beast::flat_buffer buffer;
http::request<http::string_body> request;
co_await http::async_read(socket, buffer, request, asio::use_awaitable);
http::response<http::string_body> response{http::status::ok, request.version()};
response.set(http::field::content_type, "text/plain; charset=utf-8");
response.keep_alive(request.keep_alive());
response.body() = "Hello from Boost.Beast through rstream\n";
response.prepare_payload();
co_await http::async_write(socket, response, asio::use_awaitable);
}The session accepts rstream::io_rstrm::socket. The HTTP parser, response construction, coroutine style, and error handling remain idiomatic Beast code.
Create the Published Tunnel
Connect the SDK client to the engine, create a published HTTP tunnel, then print the forwarding address.
rstrm::client client(executor);
co_await client.async_connect(asio::use_awaitable);
rstrm::tunnel_properties properties;
properties.m_name = std::string("cpp-beast-http");
properties.m_publish = true;
properties.m_protocol = rstrm::protocol::http;
properties.m_http_version = std::string("http/1.1");
properties.m_labels = {
{"framework", "boost-beast"},
{"language", "cpp"},
{"service", "http"},
};
auto tunnel = co_await client.async_create_tunnel(properties, asio::use_awaitable);
auto forwarding = rstrm::format_forwarding_address(tunnel.properties());
if (forwarding) {
std::cout << "Forwarding address: " << forwarding.value() << std::endl;
}The labels make the service searchable from the CLI, the dashboard, or automation.
rstream tunnel list --filter 'labels.language=cpp,labels.framework=boost-beast' -o json |
jq -r '.[] | [.name, .labels.service, .status, (.hostname // "")] | @tsv'Accept rstream Streams
The accept loop creates a fresh io_rstrm::socket, accepts a stream from the tunnel, and spawns the Beast session.
for (;;) {
rstrm::socket socket(executor);
rstrm::endpoint peer;
co_await tunnel.async_accept(socket, peer, asio::use_awaitable);
asio::co_spawn(executor, serve_session(std::move(socket)), asio::detached);
}This loop is the rstream equivalent of accepting TCP connections. The edge already selected the tunnel and forwarded the HTTP request; the C++ process receives the stream and handles the request with the same application code.
Run the Sample
Run the server built by make verify.
./out/bin/cpp_beast_rstream_tunnelThe process prints the rstream forwarding address. Open that URL in a browser or call it with curl.
curl -i https://<published-rstream-host>/If your organization already installs rstream-cpp into a controlled CMake prefix instead of using Conan in the application repository, build the same source with CMAKE_PREFIX_PATH.
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/path/to/rstream-cpp/install
cmake --build buildAdd Edge Policy
The C++ tunnel properties can carry the same HTTP edge policy as other SDKs. Use rstream Auth for browser-facing internal services when it is enabled for the project.
properties.m_rstream_auth = true;Use token authentication for machine clients.
properties.m_token_auth = true;Use labels to keep native services discoverable by environment, product, and owner.
properties.m_labels = {
{"env", "lab"},
{"language", "cpp"},
{"owner", "platform"},
{"service", "device-gateway"},
};Edge policy controls who reaches the public URL. The application still owns request-level authorization after the request reaches Beast.
Dial a Private Tunnel from C++
The same C++ SDK can dial private tunnels by name. Use this path when a native service calls another private service through rstream instead of publishing a public endpoint.
C++ client
|
| rstream::io_rstrm::socket async_connect
v
private rstream tunnel name
|
| outbound session from service host
v
C++ or Go upstream serviceCreate an endpoint with the tunnel name and the engine address resolved by the connected client, then connect an io_rstrm::socket.
rstrm::client client(executor);
co_await client.async_connect(asio::use_awaitable);
boost::system::error_code ec;
auto server = client.address(ec);
if (ec) {
throw boost::system::system_error(ec);
}
rstrm::endpoint endpoint = {
.m_id_name = std::string("private-api"),
.m_server_address = server,
};
rstrm::socket socket(executor);
co_await socket.async_connect(endpoint, asio::use_awaitable);Once connected, the socket is a normal asynchronous stream. Use Beast for HTTP, a binary protocol, or your own framing on top of it.