Skip to content

Commit

Permalink
Merge pull request #324 from lo-simon/bind_to_interface
Browse files Browse the repository at this point in the history
Enabling client and server sockets binding on specified network interfaces.
  • Loading branch information
lo-simon authored Apr 26, 2023
2 parents 427f455 + ea97715 commit 841212e
Show file tree
Hide file tree
Showing 13 changed files with 203 additions and 26 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,8 @@ jobs:
apt-get install -y software-properties-common
apt-get --allow-unauthenticated update -q
apt-get --allow-unauthenticated install -y curl g++ git make patch zlib1g-dev libssl-dev bsdmainutils dnsutils unzip
# ubuntu-14.04 ca-certificates are out of date
git config --global http.sslVerify false
curl -sS https://www.python.org/ftp/python/3.6.9/Python-3.6.9.tar.xz | tar -xJ
cd Python-3.6.9
./configure
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/src/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ jobs:
apt-get install -y software-properties-common
apt-get --allow-unauthenticated update -q
apt-get --allow-unauthenticated install -y curl g++ git make patch zlib1g-dev libssl-dev bsdmainutils dnsutils unzip
# ubuntu-14.04 ca-certificates are out of date
git config --global http.sslVerify false
curl -sS https://www.python.org/ftp/python/3.6.9/Python-3.6.9.tar.xz | tar -xJ
cd Python-3.6.9
./configure
Expand Down
12 changes: 12 additions & 0 deletions Development/cpprest/host_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <boost/asio/ip/host_name.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/algorithm/find.hpp>
#include <boost/range/algorithm/find_if.hpp>
#include "cpprest/asyncrt_utils.h" // for utility::conversions

#if defined(_WIN32)
Expand Down Expand Up @@ -328,6 +330,16 @@ namespace web
}
return addresses; // empty if host_name cannot be resolved
}

// get the associated network interface name from an IP address
utility::string_t get_interface_name(const utility::string_t& address, const std::vector<web::hosts::experimental::host_interface>& host_interfaces)
{
const auto interface = boost::range::find_if(host_interfaces, [&](const web::hosts::experimental::host_interface& interface)
{
return interface.addresses.end() != boost::range::find(interface.addresses, address);
});
return host_interfaces.end() != interface ? interface->name : utility::string_t{};
}
}
}
}
3 changes: 3 additions & 0 deletions Development/cpprest/host_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ namespace web

std::vector<utility::string_t> host_names(const utility::string_t& address);
std::vector<utility::string_t> host_addresses(const utility::string_t& host_name);

// get the associated network interface name from an IP address
utility::string_t get_interface_name(const utility::string_t& address, const std::vector<web::hosts::experimental::host_interface>& host_interfaces = web::hosts::experimental::host_interfaces());
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions Development/cpprest/uri_schemes.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ namespace web

inline utility::string_t http_scheme(bool secure) { return secure ? uri_schemes::https : uri_schemes::http; }
inline utility::string_t ws_scheme(bool secure) { return secure ? uri_schemes::wss : uri_schemes::ws; }

inline bool is_secure_uri_scheme(const utility::string_t& scheme)
{
return uri_schemes::https == scheme || uri_schemes::wss == scheme;
}
}

#endif
11 changes: 10 additions & 1 deletion Development/nmos-cpp-node/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,20 @@
//"settings_port": 3209,
//"logging_port": 5106,

// addresses [registry, node]: addresses on which to listen for each API, or empty string for the wildcard address
// addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address

// server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address
//"server_address": "",

// addresses [registry, node]: IP addresses on which to listen for specific APIs

//"settings_address": "127.0.0.1",
//"logging_address": "",

// client_address [registry, node]: IP address of the network interface to bind client connections
// for now, only supporting HTTP/HTTPS client connections on Linux
//"client_address": "",

// logging_limit [registry, node]: maximum number of log events cached for the Logging API
//"logging_limit": 1234,

Expand Down
13 changes: 11 additions & 2 deletions Development/nmos-cpp-registry/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,26 @@
//"mdns_port": 3208,
//"schemas_port": 3208,

// addresses [registry, node]: addresses on which to listen for each API, or empty string for the wildcard address
// addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address

// server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address
//"server_address": "",

// addresses [registry, node]: IP addresses on which to listen for specific APIs

//"settings_address": "127.0.0.1",
//"logging_address": "",

// addresses [registry]: addresses on which to listen for each API, or empty string for the wildcard address
// addresses [registry]: IP addresses on which to listen for specific APIs

//"admin_address": "",
//"mdns_address": "",
//"schemas_address": "",

// client_address [registry, node]: IP address of the network interface to bind client connections
// for now, only supporting HTTP/HTTPS client connections on Linux
//"client_address": "",

// query_ws_paging_default/query_ws_paging_limit [registry]: default/maximum number of events per message when using the Query WebSocket API (a client may request a lower limit)
//"query_ws_paging_default": 10,
//"query_ws_paging_limit": 100,
Expand Down
132 changes: 123 additions & 9 deletions Development/nmos/client_utils.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#include "nmos/client_utils.h"

// cf. preprocessor conditions in nmos::details::make_client_ssl_context_callback
// cf. preprocessor conditions in nmos::details::make_client_ssl_context_callback and nmos::details::make_client_nativehandle_options
#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO)
#if defined(__linux__)
#include <boost/asio/ip/tcp.hpp>
#endif
#include "boost/asio/ssl/set_cipher_list.hpp"
#include "cpprest/host_utils.h"
#endif
#include "cpprest/basic_utils.h"
#include "cpprest/details/system_error.h"
Expand Down Expand Up @@ -55,39 +59,149 @@ namespace nmos
}
};
}
#endif

#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO)
// bind socket to a specific network interface
// for now, only supporting Linux because SO_BINDTODEVICE is not defined on Windows and Mac
inline void bind_to_device(const utility::string_t& interface_name, bool secure, void* native_handle)
{
#if defined(__linux__)
int socket_fd;
// hmm, frustrating that native_handle type has been erased so we need secure flag
if (secure)
{
auto socket = (boost::asio::ssl::stream<boost::asio::ip::tcp::socket&>*)native_handle;
if (!socket->lowest_layer().is_open())
{
// for now, limited to IPv4
socket->lowest_layer().open(boost::asio::ip::tcp::v4());
}
socket_fd = socket->lowest_layer().native_handle();
}
else
{
auto socket = (boost::asio::ip::tcp::socket*)native_handle;
if (!socket->is_open())
{
// for now, limited to IPv4
socket->open(boost::asio::ip::tcp::v4());
}
socket_fd = socket->lowest_layer().native_handle();
}
const auto interface_name_ = utility::us2s(interface_name);
if (0 != setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, interface_name_.data(), interface_name_.length()))
{
char error[1024];
throw std::runtime_error(strerror_r(errno, error, sizeof(error)));
}
#else
throw std::logic_error("unsupported");
#endif
}

inline std::function<void(web::http::client::native_handle)> make_client_nativehandle_options(bool secure, const utility::string_t& client_address, slog::base_gate& gate)
{
if (client_address.empty()) return {};
// get the associated network interface name from IP address
const auto interface_name = web::hosts::experimental::get_interface_name(client_address);
if (interface_name.empty())
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "No network interface found for " << client_address << " to bind for the HTTP client connection";
return {};
}

return [interface_name, secure, &gate](web::http::client::native_handle native_handle)
{
try
{
bind_to_device(interface_name, secure, native_handle);
}
catch (const std::exception& e)
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unable to bind HTTP client connection to " << interface_name << ": " << e.what();
}
};
}

#ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT
// The current version of the C++ REST SDK 2.10.18 does not provide the callback to enable the custom websocket setting
inline std::function<void(web::websockets::client::native_handle)> make_ws_client_nativehandle_options(bool secure, const utility::string_t& client_address, slog::base_gate& gate)
{
if (client_address.empty()) return {};
// get the associated network interface name from IP address
const auto interface_name = web::hosts::experimental::get_interface_name(client_address);
if (interface_name.empty())
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "No network interface found for " << client_address << " to bind for the websocket client connection";
return {};
}

return [interface_name, secure, &gate](web::websockets::client::native_handle native_handle)
{
try
{
bind_to_device(interface_name, secure, native_handle);
}
catch (const std::exception& e)
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unable to bind websocket client connection to " << interface_name << ": " << e.what();
}
};
}
#endif

#endif
}

// construct client config based on settings, e.g. using the specified proxy
// construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config
// with the remaining options defaulted, e.g. request timeout
web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
web::http::client::http_client_config config;
const auto proxy = proxy_uri(settings);
if (!proxy.is_empty()) config.set_proxy(proxy);
config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings));
if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings));
#if !defined(_WIN32) && !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO)
config.set_ssl_context_callback(details::make_client_ssl_context_callback<web::http::http_exception>(settings, load_ca_certificates, gate));
if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback<web::http::http_exception>(settings, load_ca_certificates, gate));
config.set_nativehandle_options(details::make_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate));
#endif

return config;
}

// construct client config based on settings, e.g. using the specified proxy
// construct client config based on settings, e.g. using the specified proxy and OCSP config
// with the remaining options defaulted, e.g. request timeout
web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
return make_http_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate);
}

// construct client config based on specified secure flag and settings, e.g. using the specified proxy
// with the remaining options defaulted
web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
web::websockets::client::websocket_client_config make_websocket_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
web::websockets::client::websocket_client_config config;
const auto proxy = proxy_uri(settings);
if (!proxy.is_empty()) config.set_proxy(proxy);
config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings));
if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings));
#if !defined(_WIN32) || !defined(__cplusplus_winrt)
config.set_ssl_context_callback(details::make_client_ssl_context_callback<web::websockets::client::websocket_exception>(settings, load_ca_certificates, gate));
if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback<web::websockets::client::websocket_exception>(settings, load_ca_certificates, gate));
#ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT
config.set_nativehandle_options(details::make_ws_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate));
#endif
#endif

return config;
}

// construct client config based on settings, e.g. using the specified proxy
// with the remaining options defaulted
web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
return make_websocket_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate);
}

// make a request with logging
pplx::task<web::http::http_response> api_request(web::http::client::http_client client, web::http::http_request request, slog::base_gate& gate, const pplx::cancellation_token& token)
{
Expand Down
8 changes: 7 additions & 1 deletion Development/nmos/client_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ namespace slog { class base_gate; }
// Utility types, constants and functions for implementing NMOS REST API clients
namespace nmos
{
// construct client config based on settings, e.g. using the specified proxy
// construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config
// with the remaining options defaulted, e.g. request timeout
web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate);
// construct client config based on settings, e.g. using the specified proxy and OCSP config
// with the remaining options defaulted, e.g. request timeout
web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate);

// construct client config based on specified secure flag and settings, e.g. using the specified proxy
// with the remaining options defaulted
web::websockets::client::websocket_client_config make_websocket_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate);
// construct client config based on settings, e.g. using the specified proxy
// with the remaining options defaulted
web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate);
Expand Down
10 changes: 6 additions & 4 deletions Development/nmos/node_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ namespace nmos

const auto hsts = nmos::experimental::get_hsts(node_model.settings);

const auto server_address = nmos::experimental::fields::server_address(node_model.settings);

// Configure the Settings API

const host_port settings_address(nmos::experimental::fields::settings_address(node_model.settings), nmos::experimental::fields::settings_port(node_model.settings));
Expand Down Expand Up @@ -70,8 +72,8 @@ namespace nmos

for (auto& api_router : node_server.api_routers)
{
// default empty string means the wildcard address
const auto& host = !api_router.first.first.empty() ? api_router.first.first : web::http::experimental::listener::host_wildcard;
// if IP address isn't specified for this router, use default server address or wildcard address
const auto& host = !api_router.first.first.empty() ? api_router.first.first : !server_address.empty() ? server_address : web::http::experimental::listener::host_wildcard;
// map the configured client port to the server port on which to listen
// hmm, this should probably also take account of the address
node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, nmos::experimental::server_port(api_router.first.second, node_model.settings), api_router.second, http_config, hsts, gate));
Expand All @@ -84,8 +86,8 @@ namespace nmos

for (auto& ws_handler : node_server.ws_handlers)
{
// default empty string means the wildcard address
const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : web::websockets::experimental::listener::host_wildcard;
// if IP address isn't specified for this router, use default server address or wildcard address
const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : !server_address.empty() ? server_address : web::websockets::experimental::listener::host_wildcard;
// map the configured client port to the server port on which to listen
// hmm, this should probably also take account of the address
node_server.ws_listeners.push_back(nmos::make_ws_api_listener(server_secure, host, nmos::experimental::server_port(ws_handler.first.second, node_model.settings), ws_handler.second.first, websocket_config, gate));
Expand Down
Loading

0 comments on commit 841212e

Please sign in to comment.