TLS server
Creating a TLS server is the same process as creating a TCP server,
except that an extra TlsServerConfig is required. For more details about TLS support and the configuration options,
check the TLS general information page.
There are two kinds of TLS server:
create_tls_with_authz(spawn_tls_server_task_with_authzin Rust) implements Modbus Security: each client certificate must carry a role, and yourAuthorizationHandlerdecides which requests that role may make.create_tls(spawn_tls_server_taskin Rust) takes noAuthorizationHandler, doesn't require the role, and lets any authenticated client perform any Modbus operation.
Authorization handler
The examples on this page use this AuthorizationHandler:
- Rust
- C
- C++
- Java
- C#
/// Allows any role to read, and only the "operator" role to write
struct ExampleAuthorizationHandler;
impl ExampleAuthorizationHandler {
fn authorize_write(&self, unit_id: UnitId, role: &str) -> Authorization {
if role == "operator" {
Authorization::Allow
} else {
tracing::info!("denied write to unit {} for role '{role}'", unit_id.value);
Authorization::Deny
}
}
}
impl AuthorizationHandler for ExampleAuthorizationHandler {
fn read_coils(&self, _unit_id: UnitId, _range: AddressRange, _role: &str) -> Authorization {
Authorization::Allow
}
fn read_discrete_inputs(
&self,
_unit_id: UnitId,
_range: AddressRange,
_role: &str,
) -> Authorization {
Authorization::Allow
}
fn read_holding_registers(
&self,
_unit_id: UnitId,
_range: AddressRange,
_role: &str,
) -> Authorization {
Authorization::Allow
}
fn read_input_registers(
&self,
_unit_id: UnitId,
_range: AddressRange,
_role: &str,
) -> Authorization {
Authorization::Allow
}
fn write_single_coil(&self, unit_id: UnitId, _idx: u16, role: &str) -> Authorization {
self.authorize_write(unit_id, role)
}
fn write_single_register(&self, unit_id: UnitId, _idx: u16, role: &str) -> Authorization {
self.authorize_write(unit_id, role)
}
fn write_multiple_coils(
&self,
unit_id: UnitId,
_range: AddressRange,
role: &str,
) -> Authorization {
self.authorize_write(unit_id, role)
}
fn write_multiple_registers(
&self,
unit_id: UnitId,
_range: AddressRange,
role: &str,
) -> Authorization {
self.authorize_write(unit_id, role)
}
}
// allows any role to read, and only the "operator" role to write
rodbus_authorization_t authorize_read(uint8_t unit_id, rodbus_address_range_t range, const char *role, void *ctx)
{
return RODBUS_AUTHORIZATION_ALLOW;
}
rodbus_authorization_t authorize_write(uint8_t unit_id, const char *role)
{
if (strcmp(role, "operator") == 0) {
return RODBUS_AUTHORIZATION_ALLOW;
}
printf("denied write to unit %u for role '%s'\n", unit_id, role);
return RODBUS_AUTHORIZATION_DENY;
}
rodbus_authorization_t authorize_single_write(uint8_t unit_id, uint16_t index, const char *role, void *ctx)
{
return authorize_write(unit_id, role);
}
rodbus_authorization_t authorize_multiple_write(uint8_t unit_id, rodbus_address_range_t range, const char *role, void *ctx)
{
return authorize_write(unit_id, role);
}
rodbus_authorization_handler_t get_auth_handler(void)
{
return (rodbus_authorization_handler_t){
.read_coils = &authorize_read,
.read_discrete_inputs = &authorize_read,
.read_holding_registers = &authorize_read,
.read_input_registers = &authorize_read,
.write_single_coil = &authorize_single_write,
.write_single_register = &authorize_single_write,
.write_multiple_coils = &authorize_multiple_write,
.write_multiple_registers = &authorize_multiple_write,
.on_destroy = NULL,
.ctx = NULL,
};
}
// allows any role to read, and only the "operator" role to write
class AuthorizationHandler : public rodbus::AuthorizationHandler {
rodbus::Authorization read_coils(uint8_t unit_id, const rodbus::AddressRange &range, const char *role) override
{
return rodbus::Authorization::allow;
}
rodbus::Authorization read_discrete_inputs(uint8_t unit_id, const rodbus::AddressRange &range, const char *role) override
{
return rodbus::Authorization::allow;
}
rodbus::Authorization read_holding_registers(uint8_t unit_id, const rodbus::AddressRange &range, const char *role) override
{
return rodbus::Authorization::allow;
}
rodbus::Authorization read_input_registers(uint8_t unit_id, const rodbus::AddressRange &range, const char *role) override
{
return rodbus::Authorization::allow;
}
rodbus::Authorization write_single_coil(uint8_t unit_id, uint16_t index, const char *role) override
{
return authorize_write(unit_id, role);
}
rodbus::Authorization write_single_register(uint8_t unit_id, uint16_t index, const char *role) override
{
return authorize_write(unit_id, role);
}
rodbus::Authorization write_multiple_coils(uint8_t unit_id, const rodbus::AddressRange &range, const char *role) override
{
return authorize_write(unit_id, role);
}
rodbus::Authorization write_multiple_registers(uint8_t unit_id, const rodbus::AddressRange &range, const char *role) override
{
return authorize_write(unit_id, role);
}
static rodbus::Authorization authorize_write(uint8_t unit_id, const char *role)
{
if (std::strcmp(role, "operator") == 0) {
return rodbus::Authorization::allow;
}
std::cout << "denied write to unit " + std::to_string(unit_id) + " for role '" + role + "'\n";
return rodbus::Authorization::deny;
}
};
std::unique_ptr<rodbus::AuthorizationHandler> create_auth_handler()
{
return std::make_unique<AuthorizationHandler>();
}
// allows any role to read, and only the "operator" role to write
private static class ExampleAuthorizationHandler implements AuthorizationHandler {
@Override
public Authorization readCoils(UByte unitId, AddressRange range, String role) {
return Authorization.ALLOW;
}
@Override
public Authorization readDiscreteInputs(UByte unitId, AddressRange range, String role) {
return Authorization.ALLOW;
}
@Override
public Authorization readHoldingRegisters(UByte unitId, AddressRange range, String role) {
return Authorization.ALLOW;
}
@Override
public Authorization readInputRegisters(UByte unitId, AddressRange range, String role) {
return Authorization.ALLOW;
}
@Override
public Authorization writeSingleCoil(UByte unitId, UShort idx, String role) {
return authorizeWrite(unitId, role);
}
@Override
public Authorization writeSingleRegister(UByte unitId, UShort idx, String role) {
return authorizeWrite(unitId, role);
}
@Override
public Authorization writeMultipleCoils(UByte unitId, AddressRange range, String role) {
return authorizeWrite(unitId, role);
}
@Override
public Authorization writeMultipleRegisters(UByte unitId, AddressRange range, String role) {
return authorizeWrite(unitId, role);
}
private static Authorization authorizeWrite(UByte unitId, String role) {
if ("operator".equals(role)) {
return Authorization.ALLOW;
} else {
System.out.println("denied write to unit " + unitId.intValue() + " for role '" + role + "'");
return Authorization.DENY;
}
}
}
// Allows any role to read, and only the "operator" role to write
class AuthorizationHandler : IAuthorizationHandler
{
public Authorization ReadCoils(byte unitId, AddressRange range, string role)
{
return Authorization.Allow;
}
public Authorization ReadDiscreteInputs(byte unitId, AddressRange range, string role)
{
return Authorization.Allow;
}
public Authorization ReadHoldingRegisters(byte unitId, AddressRange range, string role)
{
return Authorization.Allow;
}
public Authorization ReadInputRegisters(byte unitId, AddressRange range, string role)
{
return Authorization.Allow;
}
public Authorization WriteSingleCoil(byte unitId, ushort idx, string role)
{
return AuthorizeWrite(unitId, role);
}
public Authorization WriteSingleRegister(byte unitId, ushort idx, string role)
{
return AuthorizeWrite(unitId, role);
}
public Authorization WriteMultipleCoils(byte unitId, AddressRange range, string role)
{
return AuthorizeWrite(unitId, role);
}
public Authorization WriteMultipleRegisters(byte unitId, AddressRange range, string role)
{
return AuthorizeWrite(unitId, role);
}
private static Authorization AuthorizeWrite(byte unitId, string role)
{
if (role == "operator")
{
return Authorization.Allow;
}
Console.WriteLine($"denied write to unit {unitId} for role '{role}'");
return Authorization.Deny;
}
}
Certificate chain configuration
- Rust
- C
- C++
- Java
- C#
let handler = SimpleHandler::new(POINT_COUNT).wrap();
// map unit ids to a handler for processing requests
let map = ServerHandlerMap::single(UnitId::new(UNIT_ID), handler.clone());
let tls_config = TlsServerConfig::new(
Path::new("./certs/ca_chain/ca_cert.pem"),
Path::new("./certs/ca_chain/server_cert.pem"),
Path::new("./certs/ca_chain/server_key.pem"),
None, // no password
MinTlsVersion::V1_2,
CertificateMode::AuthorityBased,
)?;
let server = rodbus::server::spawn_tls_server_task_with_authz(
MAX_SESSIONS,
endpoint,
map,
ExampleAuthorizationHandler.wrap(),
tls_config,
AddressFilter::Any,
DecodeLevel::new(
AppDecodeLevel::DataHeaders,
FrameDecodeLevel::Nothing,
PhysDecodeLevel::Nothing,
),
)
.await?;
rodbus_device_map_t *map = rodbus_device_map_create();
rodbus_device_map_add_endpoint(map,
UNIT_ID, // unit id
get_write_handler(), // handler for write requests
(rodbus_database_callback_t){.callback = &configure_db} // callback for the initial state of the database
);
rodbus_tls_server_config_t tls_config = rodbus_tls_server_config_init(
"./certs/ca_chain/ca_cert.pem", // CA certificate
"./certs/ca_chain/server_cert.pem", // server certificate
"./certs/ca_chain/server_key.pem", // server private key
"" // no password
);
tls_config.min_tls_version = RODBUS_MIN_TLS_VERSION_V12;
rodbus_server_t *server = NULL;
rodbus_decode_level_t decode_level = rodbus_decode_level_build(RODBUS_APP_DECODE_LEVEL_DATA_HEADERS, RODBUS_FRAME_DECODE_LEVEL_NOTHING, RODBUS_PHYS_DECODE_LEVEL_NOTHING);
rodbus_address_filter_t *filter = rodbus_address_filter_any();
rodbus_param_error_t err = rodbus_server_create_tls_with_authz(runtime, host, port, filter, MAX_SESSIONS, map, tls_config, get_auth_handler(), decode_level, &server);
rodbus_address_filter_destroy(filter);
rodbus_device_map_destroy(map);
if (err) {
printf("unable to create server: %s\n", rodbus_param_error_to_string(err));
return NULL;
}
// POINT_COUNT points of each type, starting at index 0
auto init_transaction = rodbus::functional::database_callback([](rodbus::Database &db) {
for (uint16_t i = 0; i < POINT_COUNT; ++i) {
db.add_coil(i, false);
db.add_discrete_input(i, false);
db.add_holding_register(i, 0);
db.add_input_register(i, 0);
}
});
auto map = rodbus::DeviceMap();
map.add_endpoint(UNIT_ID, create_write_handler(), init_transaction);
auto tls_config = rodbus::TlsServerConfig(
"./certs/ca_chain/ca_cert.pem", // CA certificate
"./certs/ca_chain/server_cert.pem", // server certificate
"./certs/ca_chain/server_key.pem", // server private key
"" // no password
);
tls_config.min_tls_version = rodbus::MinTlsVersion::v12;
auto filter = rodbus::AddressFilter::any();
auto server = rodbus::Server::create_tls_with_authz(
runtime,
endpoint.host,
endpoint.port,
filter,
MAX_SESSIONS,
map,
tls_config,
create_auth_handler(),
rodbus::DecodeLevel(rodbus::AppDecodeLevel::data_headers, rodbus::FrameDecodeLevel::nothing, rodbus::PhysDecodeLevel::nothing)
);
DeviceMap map = new DeviceMap();
map.addEndpoint(ubyte(UNIT_ID), new ExampleWriteHandler(), db -> {
for (int i = 0; i < POINT_COUNT; i++) {
db.addCoil(ushort(i), false);
db.addDiscreteInput(ushort(i), false);
db.addHoldingRegister(ushort(i), ushort(0));
db.addInputRegister(ushort(i), ushort(0));
}
});
TlsServerConfig tlsConfig = new TlsServerConfig(
"./certs/ca_chain/ca_cert.pem",
"./certs/ca_chain/server_cert.pem",
"./certs/ca_chain/server_key.pem",
"" // no password
).withMinTlsVersion(MinTlsVersion.V12);
Server server = Server.createTlsWithAuthz(
runtime,
host,
port,
AddressFilter.any(),
ushort(MAX_SESSIONS),
map,
tlsConfig,
new ExampleAuthorizationHandler(),
new DecodeLevel(AppDecodeLevel.DATA_HEADERS, FrameDecodeLevel.NOTHING, PhysDecodeLevel.NOTHING)
);
var map = new DeviceMap();
map.AddEndpoint(UnitId, new WriteHandler(), db =>
{
for (ushort i = 0; i < PointCount; ++i)
{
db.AddCoil(i, false);
db.AddDiscreteInput(i, false);
db.AddHoldingRegister(i, 0);
db.AddInputRegister(i, 0);
}
});
var tlsConfig = new TlsServerConfig(
"./certs/ca_chain/ca_cert.pem",
"./certs/ca_chain/server_cert.pem",
"./certs/ca_chain/server_key.pem",
"" // no password
).WithMinTlsVersion(MinTlsVersion.V12);
var server = Server.CreateTlsWithAuthz(
runtime,
address,
port,
AddressFilter.Any(),
MaxSessions,
map,
tlsConfig,
new AuthorizationHandler(),
new DecodeLevel(AppDecodeLevel.DataHeaders, FrameDecodeLevel.Nothing, PhysDecodeLevel.Nothing)
);
Self-signed certificate configuration
Only the TlsServerConfig differs from the certificate chain example:
- Rust
- C
- C++
- Java
- C#
let tls_config = TlsServerConfig::new(
Path::new("./certs/self_signed/entity1_cert.pem"),
Path::new("./certs/self_signed/entity2_cert.pem"),
Path::new("./certs/self_signed/entity2_key.pem"),
None, // no password
MinTlsVersion::V1_2,
CertificateMode::SelfSigned,
)?;
let server = rodbus::server::spawn_tls_server_task_with_authz(
MAX_SESSIONS,
endpoint,
map,
ExampleAuthorizationHandler.wrap(),
tls_config,
AddressFilter::Any,
DecodeLevel::new(
AppDecodeLevel::DataHeaders,
FrameDecodeLevel::Nothing,
PhysDecodeLevel::Nothing,
),
)
.await?;
rodbus_tls_server_config_t tls_config = rodbus_tls_server_config_init(
"./certs/self_signed/entity1_cert.pem", // client certificate
"./certs/self_signed/entity2_cert.pem", // server certificate
"./certs/self_signed/entity2_key.pem", // server private key
"" // no password
);
tls_config.min_tls_version = RODBUS_MIN_TLS_VERSION_V12;
tls_config.certificate_mode = RODBUS_CERTIFICATE_MODE_SELF_SIGNED;
rodbus_server_t *server = NULL;
rodbus_decode_level_t decode_level = rodbus_decode_level_build(RODBUS_APP_DECODE_LEVEL_DATA_HEADERS, RODBUS_FRAME_DECODE_LEVEL_NOTHING, RODBUS_PHYS_DECODE_LEVEL_NOTHING);
rodbus_address_filter_t *filter = rodbus_address_filter_any();
rodbus_param_error_t err = rodbus_server_create_tls_with_authz(runtime, host, port, filter, MAX_SESSIONS, map, tls_config, get_auth_handler(), decode_level, &server);
rodbus_address_filter_destroy(filter);
rodbus_device_map_destroy(map);
if (err) {
printf("unable to create server: %s\n", rodbus_param_error_to_string(err));
return NULL;
}
auto tls_config = rodbus::TlsServerConfig(
"./certs/self_signed/entity1_cert.pem", // client certificate
"./certs/self_signed/entity2_cert.pem", // server certificate
"./certs/self_signed/entity2_key.pem", // server private key
"" // no password
);
tls_config.min_tls_version = rodbus::MinTlsVersion::v12;
tls_config.certificate_mode = rodbus::CertificateMode::self_signed;
auto filter = rodbus::AddressFilter::any();
auto server = rodbus::Server::create_tls_with_authz(
runtime,
endpoint.host,
endpoint.port,
filter,
MAX_SESSIONS,
map,
tls_config,
create_auth_handler(),
rodbus::DecodeLevel(rodbus::AppDecodeLevel::data_headers, rodbus::FrameDecodeLevel::nothing, rodbus::PhysDecodeLevel::nothing)
);
TlsServerConfig tlsConfig = new TlsServerConfig(
"./certs/self_signed/entity1_cert.pem",
"./certs/self_signed/entity2_cert.pem",
"./certs/self_signed/entity2_key.pem",
"" // no password
).withMinTlsVersion(MinTlsVersion.V12).withCertificateMode(CertificateMode.SELF_SIGNED);
Server server = Server.createTlsWithAuthz(
runtime,
host,
port,
AddressFilter.any(),
ushort(MAX_SESSIONS),
map,
tlsConfig,
new ExampleAuthorizationHandler(),
new DecodeLevel(AppDecodeLevel.DATA_HEADERS, FrameDecodeLevel.NOTHING, PhysDecodeLevel.NOTHING)
);
var tlsConfig = new TlsServerConfig(
"./certs/self_signed/entity1_cert.pem",
"./certs/self_signed/entity2_cert.pem",
"./certs/self_signed/entity2_key.pem",
"" // no password
).WithMinTlsVersion(MinTlsVersion.V12).WithCertificateMode(CertificateMode.SelfSigned);
var server = Server.CreateTlsWithAuthz(
runtime,
address,
port,
AddressFilter.Any(),
MaxSessions,
map,
tlsConfig,
new AuthorizationHandler(),
new DecodeLevel(AppDecodeLevel.DataHeaders, FrameDecodeLevel.Nothing, PhysDecodeLevel.Nothing)
);