Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
[submodule "third_party/googletest"]
path = third_party/googletest
url = https://github.com/google/googletest.git
[submodule "third_party/spdlog"]
path = third_party/spdlog
url = https://github.com/gabime/spdlog.git
[submodule "third_party/ni-apis"]
path = third_party/ni-apis
url = https://github.com/ni/ni-apis

url = https://github.com/ni/ni-apis
28 changes: 28 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ if(USE_SUBMODULE_LIBS)
add_subdirectory(third_party/json ${CMAKE_CURRENT_BINARY_DIR}/json)
add_subdirectory(third_party/utfcpp ${CMAKE_CURRENT_BINARY_DIR}/utfcpp)
add_subdirectory(third_party/grpc-sideband ${CMAKE_CURRENT_BINARY_DIR}/grpc-sideband)
if(WIN32)
add_subdirectory(third_party/spdlog ${CMAKE_CURRENT_BINARY_DIR}/spdlog EXCLUDE_FROM_ALL)
set(_SPDLOG spdlog::spdlog)
endif()

set(_PROTOBUF_PROTOC $<TARGET_FILE:protobuf::protoc>)
set(_REFLECTION grpc++_reflection)
Expand All @@ -89,6 +93,7 @@ else()
if(USE_NILRT_LEGACY_TOOLCHAIN)
# The archetypical NILRT SDK toolchain build case
add_subdirectory(third_party/grpc ${CMAKE_CURRENT_BINARY_DIR}/grpc EXCLUDE_FROM_ALL)

set(_REFLECTION grpc++_reflection)
set(_GRPC_GRPCPP grpc++)
set(_PROTOBUF_LIBPROTOBUF libprotobuf)
Expand All @@ -109,6 +114,18 @@ else()
endif()
endif()

# Abseil logging targets, used to route gRPC's internal logs into our logger.
# These come from Abseil directly in the submodule/NILRT builds, and from the
# absl package that gRPC's config pulls in otherwise.
set(_ABSEIL_LOG
absl::log
absl::log_entry
absl::log_globals
absl::log_initialize
absl::log_sink
absl::log_sink_registry
)

# Python3 Virtual Environment
if(USE_PYTHON_VIRTUALENV)
include(CreateVirtualEnvironment)
Expand Down Expand Up @@ -548,6 +565,7 @@ add_executable(ni_grpc_device_server
"imports/include/nierr_Status.cpp"
"source/server/calibration_operations_restricted_service_registrar.cpp"
"source/server/calibration_operations_restricted_service.cpp"
"source/server/client_connection_logger.cpp"
"source/server/core_server.cpp"
"source/server/core_services_registrar.cpp"
"source/server/data_moniker_service.cpp"
Expand Down Expand Up @@ -649,6 +667,7 @@ endif()

set(server_lib_deps
${_ABSEIL_SYNC}
${_ABSEIL_LOG}
${_GRPC_GPR}
${_GRPC_GRPCPP}
${_GRPC}
Expand All @@ -657,6 +676,7 @@ set(server_lib_deps
${_UTF8CPP}
${CMAKE_DL_LIBS}
${_GRPC_SIDEBAND}
${_SPDLOG}
nlohmann_json::nlohmann_json
)

Expand Down Expand Up @@ -830,10 +850,13 @@ add_executable(UnitTestsRunner
"source/tests/unit/xnet_converters_tests.cpp"
"source/tests/unit/xnet_socket_converters_tests.cpp"
"source/tests/unit/tls_config_loader_tests.cpp"
"source/tests/unit/client_connection_logger_tests.cpp"
"source/server/calibration_operations_restricted_service.cpp"
"source/server/client_connection_logger.cpp"
"source/server/debug_session_properties_restricted_service.cpp"
"source/server/device_enumerator.cpp"
"source/server/feature_toggles.cpp"
"source/server/logging.cpp"
"source/server/tls_config_loader.cpp"
"source/server/semaphore.cpp"
"source/server/server_configuration_parser.cpp"
Expand Down Expand Up @@ -898,6 +921,11 @@ target_include_directories(UnitTestsRunner
PRIVATE "${service_output_dir}/nifake_non_ivi"
PRIVATE "source/server")

if(CMAKE_SYSTEM_NAME STREQUAL Linux)
target_sources(UnitTestsRunner
PRIVATE "source/server/linux/syslog_logging.cpp")
endif()

target_link_libraries(UnitTestsRunner
${CMAKE_DL_LIBS}
${server_lib_deps}
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,17 @@ With this setting the server reads TLS configuration at startup from the per-ser
If `"security": "ni-tls-config"` is configured but the `ni-tls-config` library is not installed, the server logs an error and exits instead of starting insecurely.

Once `ni-tls-config` is enabled, use NI Hardware Configuration Utility on each client machine to configure the desired security settings.

### Audit Logging

The server logs audit messages to the Windows Event Log and Linux Syslog. On Windows, events are logged from the `ni-grpc-device-server` source. These events will be automatically placed under the default Application log (seen under the Windows folder in the Event Viewer app). It is recommended to add the following registry key to properly register it as a source:

`HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\National Instruments\ni-grpc-device-server`

Then add the following values:
- `EventMessageFile (REG_EXPAND_SZ): %systemroot%\System32\mscoree.dll`
- `TypesSupported (REG_DWORD): 7`

This recommendation is taken directly from [spdlog's documentation](https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/win_eventlog_sink.h).

NI's official installer for `grpc-device` automatically sets this registry key. This installer is included with installations of InstrumentStudio, several MI drivers, and more.
49 changes: 49 additions & 0 deletions ThirdPartyNotices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,55 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

****** spdlog (https://github.com/gabime/spdlog) ******

The MIT License (MIT)

Copyright (c) 2016 - present, Gabi Melman and spdlog contributors.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

-- NOTE: Third party dependency used by this software --
This software depends on the fmt lib (MIT License),
and users must comply to its license:

Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

****** utfcpp (https://github.com/nemtrif/utfcpp) ******

Boost Software License - Version 1.0 - August 17th, 2003
Expand Down
123 changes: 123 additions & 0 deletions source/server/client_connection_logger.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#include "client_connection_logger.h"
#include "logging.h"

#include <absl/log/globals.h>
#include <absl/log/initialize.h>
#include <absl/log/log_entry.h>
#include <absl/log/log_sink.h>
#include <absl/log/log_sink_registry.h>
#include <absl/strings/match.h>

#include <grpc/grpc_security_constants.h>

namespace nidevice_grpc {

namespace {

std::string describe_authentication(const grpc::AuthContext& auth_context)
{
if (!auth_context.IsPeerAuthenticated())
return "Unauthenticated";

const auto transport_type = auth_context.FindPropertyValues(GRPC_TRANSPORT_SECURITY_TYPE_PROPERTY_NAME);
const auto common_names = auth_context.FindPropertyValues(GRPC_X509_CN_PROPERTY_NAME);

std::string description = "Authenticated via ";
description += transport_type.empty() ? "unknown transport" : std::string(transport_type[0].data(), transport_type[0].size());

if (!common_names.empty())
description += ", client cert CN: " + std::string(common_names[0].data(), common_names[0].size());

return description;
}

}

bool parse_peer(const std::string& peer, std::string& ip, std::string& port)
{
if (peer.rfind("ipv4:", 0) != 0 && peer.rfind("ipv6:", 0) != 0)
return false;

const auto scheme_end = peer.find(':');
const auto port_pos = peer.rfind(':');
if (port_pos <= scheme_end)
return false;

ip = peer.substr(scheme_end + 1, port_pos - scheme_end - 1);
port = peer.substr(port_pos + 1);

return !ip.empty() && !port.empty();
}

// Converts absl's severity levels to our own.
logging::Level to_logging_level(absl::LogSeverity severity)
{
switch (severity) {
case absl::LogSeverity::kWarning:
return logging::Level_Warning;
case absl::LogSeverity::kError:
case absl::LogSeverity::kFatal:
return logging::Level_Error;
case absl::LogSeverity::kInfo:
default:
return logging::Level_Info;
}
}

// This sink captures gRPC's own internal logs and calls Send for each of them. We only use it to log handshake failures.
class AuditLogSink : public absl::LogSink {
public:
void Send(const absl::LogEntry& entry) override
{
if (!absl::StrContainsIgnoreCase(entry.text_message(), "handshake"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have 2 "thinking out loud" comments on this:

  1. Is it possible that grpc has improved or will improve support for connection logging as part of its own CRA compliance efforts? It's quite cool that we've managed to make this work in a pretty reasonable way in a difficult environment. But it's hard to imagine that every C++ grpc server will need to do all of this to comply with CRA.
  2. What are some interesting failure modes of this text match? Is it possible for a "failed connection" to be missing the handshake token? Is it possible for a "normal message" to include the handshake token? Is it possible for a client (unauthenticated client?) to influence either failure? Would using an additional/different filter (like "severity"?) improve it?

Most likely what you're doing is about right. Just checking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chatted with Ryan about this a bit and I believe the preferred mechanism for authentication logging moving forward will be to use https://grpc.io/docs/guides/interceptors/. Currently these are under the experimental namespace so we did not want to use these in their current state. Mainly to avoid any API friction that could be incurred when upgrading to newer versions of gRPC in grpc-device. Once these have stabilized and have been moved out of experimental it's definitely worth looking into.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, gRPC has several experimental options that would likely streamline the logging process. They've existed for a while but have not been stabilized yet. I'm not sure if there are plans on their end to do this at the moment, but maybe the CRA deadline will provide some motivation. In particular, there's a CertificateVerifier which seems to have the capability to provide function hooks that will fire during a handshake itself.

From what I can tell throughout my testing the "handshake" token seems to be sufficient as a filtering method. I saw the correct message in all failure cases and did not notice any extraneous error messages appearing at any other point either. Exploring the gRPC source with Copilot seems to align with this behavior; I found only 7 error messages that include it, including the one I saw in testing. It is possible for a failed connection to not include the token, but the scope of this addition is only to log authentication failures, not any kind of failure.

I think that the current state is probably the best solution we have for today; the aforementioned CertificateVerifier is probably the best way to do it in a less "hacky" way and would definitely be something to consider if gRPC stabilizes it.

return;

const auto message = std::string(entry.text_message());
logging::log_to_audit_source(to_logging_level(entry.log_severity()), "%s", message.c_str());
}
};

void ClientConnectionLogger::PreSynchronousRequest(grpc::ServerContext* context)
{
std::string ip, port;
const auto peer = context->peer();
const bool parsed = parse_peer(peer, ip, port);

// Only log the first connection seen from a given IP.
{
std::lock_guard<std::mutex> lock(seen_ips_mutex_);
if (!seen_ips_.insert(parsed ? ip : peer).second)
return;
}

const auto auth_description = describe_authentication(*context->auth_context());

// Try to parse the IP + Port out of the peer uri so that the log message can be formatted nicely. If it fails, just log the raw string.
if (parsed)
nidevice_grpc::logging::log_to_audit_source(nidevice_grpc::logging::Level_Info, "Remote client successfully connected from %s:%s (%s)", ip.c_str(), port.c_str(), auth_description.c_str());
else
nidevice_grpc::logging::log_to_audit_source(nidevice_grpc::logging::Level_Info, "Remote client successfully connected from %s (%s)", peer.c_str(), auth_description.c_str());
}

void ClientConnectionLogger::PostSynchronousRequest(grpc::ServerContext*)
{
}

void register_client_connection_logger()
{
// gRPC stores this in an owning shared_ptr (see Server::SetGlobalCallbacks inserver_cc.cc) and deletes it at static destruction. Even
// if that changes to non-owning, intentionally leaking one process-wide object is correct and avoids a static-destruction-order hazard
// against grpc::Server.
grpc::Server::SetGlobalCallbacks(new ClientConnectionLogger());
}

void register_grpc_log_sink()
{
absl::InitializeLog();

// Abseil does not take ownership of the sink, so it will be a static object.
static AuditLogSink sink;
absl::AddLogSink(&sink);
Comment thread
ryanwixon-emerson marked this conversation as resolved.
Outdated
}

} // namespace nidevice_grpc
35 changes: 35 additions & 0 deletions source/server/client_connection_logger.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#ifndef NIDEVICE_GRPC_CLIENT_CONNECTION_LOGGER_H
#define NIDEVICE_GRPC_CLIENT_CONNECTION_LOGGER_H

#include <grpcpp/grpcpp.h>

#include <mutex>
#include <string>
#include <unordered_set>

namespace nidevice_grpc {

// Parses a gRPC peer URI (e.g. "ipv4:127.0.0.1:12345" or "ipv6:[::1]:12345") into its ip and port components.
// Returns false if the peer string doesn't match a recognized format.
bool parse_peer(const std::string& peer, std::string& ip, std::string& port);

// Implementation of callbacks that the grpc::Server will invoke for individual client RPC calls.
class ClientConnectionLogger : public grpc::Server::GlobalCallbacks {
public:
void PreSynchronousRequest(grpc::ServerContext* context) override;
void PostSynchronousRequest(grpc::ServerContext* context) override;

private:
std::mutex seen_ips_mutex_;
std::unordered_set<std::string> seen_ips_;
};

// Registers a process-wide ClientConnectionLogger with gRPC, must be called before any grpc::Server is built.
void register_client_connection_logger();

// Registers an absl::LogSink that captures all of gRPC's internal log messages; used for logging connection failures
void register_grpc_log_sink();

} // namespace nidevice_grpc

#endif // NIDEVICE_GRPC_CLIENT_CONNECTION_LOGGER_H
8 changes: 7 additions & 1 deletion source/server/core_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <thread>
#include <algorithm>

#include "client_connection_logger.h"
#include "feature_toggles.h"
#include "logging.h"
#include "tls_config_loader.h"
Expand Down Expand Up @@ -99,6 +100,8 @@ static void RunServer(const ServerConfiguration& config)
config.config_file_path.c_str());
}

nidevice_grpc::register_client_connection_logger();

grpc::EnableDefaultHealthCheckService(true);
grpc::reflection::InitProtoReflectionServerBuilderPlugin();

Expand Down Expand Up @@ -310,8 +313,9 @@ int main(int argc, char** argv)
auto config = GetConfiguration(options.config_file_path);
setlocale(LC_ALL, "");
#if defined(__GNUC__)
// syslog is always needed for audit logging even when general output goes to the terminal
nidevice_grpc::logging::setup_syslog(options.daemonize, options.identity);
if (options.use_syslog) {
nidevice_grpc::logging::setup_syslog(options.daemonize, options.identity);
nidevice_grpc::logging::set_logger(&nidevice_grpc::logging::log_syslog);
}

Expand All @@ -323,6 +327,8 @@ int main(int argc, char** argv)
nidevice_grpc::set_console_ctrl_handler(&StopServer);
#endif

nidevice_grpc::register_grpc_log_sink();

RunServer(config);
return EXIT_SUCCESS;
}
Loading
Loading