Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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"]
Comment thread
ryanwixon-emerson marked this conversation as resolved.
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
15 changes: 15 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()
Comment thread
ckoellin marked this conversation as resolved.

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)
Comment thread
ryanwixon-emerson marked this conversation as resolved.
Expand Down Expand Up @@ -548,6 +553,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 @@ -657,6 +663,7 @@ set(server_lib_deps
${_UTF8CPP}
${CMAKE_DL_LIBS}
${_GRPC_SIDEBAND}
${_SPDLOG}
nlohmann_json::nlohmann_json
)

Expand Down Expand Up @@ -830,10 +837,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 +908,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:
Comment thread
ryanwixon-emerson marked this conversation as resolved.

`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
79 changes: 79 additions & 0 deletions source/server/client_connection_logger.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#include "client_connection_logger.h"
#include "logging.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)
Comment thread
ryanwixon-emerson marked this conversation as resolved.
{
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(':');
Comment thread
ryanwixon-emerson marked this conversation as resolved.
Outdated
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();
Comment thread
ryanwixon-emerson marked this conversation as resolved.
}

void ClientConnectionLogger::PreSynchronousRequest(grpc::ServerContext* context)
Comment thread
ryanwixon-emerson marked this conversation as resolved.
Comment thread
ryanwixon-emerson marked this conversation as resolved.
{
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.
Comment thread
ryanwixon-emerson marked this conversation as resolved.
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());
}

} // namespace nidevice_grpc
32 changes: 32 additions & 0 deletions source/server/client_connection_logger.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#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_;
Comment thread
ryanwixon-emerson marked this conversation as resolved.
};

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

} // namespace nidevice_grpc

#endif // NIDEVICE_GRPC_CLIENT_CONNECTION_LOGGER_H
6 changes: 5 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 Down
72 changes: 72 additions & 0 deletions source/server/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

#include <cstdio>
#include <iostream>
#include <string>

#if defined(_WIN32)
#include <memory>

#include <spdlog/sinks/win_eventlog_sink.h>
#include <spdlog/spdlog.h>
#else
#include "linux/syslog_logging.h"
#endif

namespace nidevice_grpc {
namespace logging {
Expand Down Expand Up @@ -37,5 +47,67 @@ void log(Level level, const char* fmt, ...)
va_end(args);
}

#if defined(_WIN32)
namespace {

std::shared_ptr<spdlog::logger> get_event_log_logger()
{
static std::shared_ptr<spdlog::logger> audit_logger = []() {
auto sink = std::make_shared<spdlog::sinks::win_eventlog_sink_mt>("ni-grpc-device-server");
auto logger = std::make_shared<spdlog::logger>("Server", sink);
logger->set_pattern("[ni-grpc-device-server][%n] %v");
return logger;
}();
return audit_logger;
}

std::string format_message(const char* fmt, va_list args)
{
va_list args_copy;
va_copy(args_copy, args);
const int size = std::vsnprintf(nullptr, 0, fmt, args_copy);
va_end(args_copy);
if (size <= 0) {
return std::string();
}
std::string result(static_cast<size_t>(size), '\0');
std::vsnprintf(&result[0], result.size() + 1, fmt, args);
return result;
}

} // namespace
#endif

void log_to_audit_source(Level level, const char* fmt, ...)
{
#if defined(_WIN32)
va_list args;
va_start(args, fmt);
const std::string message = format_message(fmt, args);
va_end(args);

auto audit_logger = get_event_log_logger();
switch (level) {
case Level_Info:
audit_logger->info("{}", message);
break;
case Level_Warning:
audit_logger->warn("{}", message);
break;
case Level_Error:
audit_logger->error("{}", message);
break;
}
#else
// We're manually prefixing audit logs in the Windows spdlog path, so manually add it here on the Linux path before sending it over to syslog.
const std::string message = "[ni-grpc-device-server][Server] " + std::string(fmt);

va_list args;
va_start(args, fmt);
log_syslog(level, message.c_str(), args);
va_end(args);
#endif
}

} // namespace logging
} // namespace nidevice_grpc
3 changes: 3 additions & 0 deletions source/server/logging.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ typedef void (*log_fn_impl)(Level level, const char* fmt, va_list args);
void set_logger(log_fn_impl impl);
void log(Level level, const char* fmt, ...);

// Special log option that routes to the platform audit log (Windows Event Log or Syslog)
void log_to_audit_source(Level level, const char* fmt, ...);

} // namespace logging
} // namespace nidevice_grpc

Expand Down
Loading
Loading