Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 2 additions & 7 deletions dnp3/src/app/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ pub mod var {
}

/// Set to which a device attribute belongs
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum AttrSet {
/// The default attribute set defined by DNP3.org
#[default]
Default,
/// Non-zero privately defined attribute set
Private(u8),
Expand Down Expand Up @@ -168,12 +169,6 @@ impl AttrSet {
}
}

impl Default for AttrSet {
fn default() -> Self {
Self::Default
}
}

/// Enum that represent default or private set attributes
#[derive(Clone, Debug, PartialEq)]
pub enum AnyAttribute<'a> {
Expand Down
32 changes: 8 additions & 24 deletions dnp3/src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ pub struct DecodeLevel {
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Default)]
pub enum AppDecodeLevel {
/// Decode nothing
#[default]
Nothing,
/// Decode the header-only
Header,
Expand All @@ -36,75 +38,57 @@ pub enum AppDecodeLevel {
ObjectValues,
}

impl Default for AppDecodeLevel {
fn default() -> Self {
Self::Nothing
}
}

/// Controls how transmitted and received transport segments are decoded at the INFO log level
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Default)]
pub enum TransportDecodeLevel {
/// Decode nothing
#[default]
Nothing,
/// Decode the header
Header,
/// Decode the header and the raw payload as hexadecimal
Payload,
}

impl Default for TransportDecodeLevel {
fn default() -> Self {
Self::Nothing
}
}

/// Controls how transmitted and received link frames are decoded at the INFO log level
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Default)]
pub enum LinkDecodeLevel {
/// Decode nothing
#[default]
Nothing,
/// Decode the header
Header,
/// Decode the header and the raw payload as hexadecimal
Payload,
}

impl Default for LinkDecodeLevel {
fn default() -> Self {
Self::Nothing
}
}

/// Controls how data transmitted at the physical layer (TCP, serial, etc) is logged
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Default)]
pub enum PhysDecodeLevel {
/// Log nothing
#[default]
Nothing,
/// Log only the length of data that is sent and received
Length,
/// Log the length and the actual data that is sent and received
Data,
}

impl Default for PhysDecodeLevel {
fn default() -> Self {
Self::Nothing
}
}

impl DecodeLevel {
/// construct a `DecodeLevel` with nothing enabled
pub fn nothing() -> Self {
Expand Down
4 changes: 4 additions & 0 deletions dnp3/src/outstation/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ impl OutstationSession {
}
}

pub(crate) fn change_master_address(&mut self, address: EndpointAddress) {
self.destination.link = address;
}

pub(crate) fn enabled(&self) -> Enabled {
self.enabled
}
Expand Down
5 changes: 5 additions & 0 deletions dnp3/src/outstation/task.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::app::parse::options::ParseOptions;
use crate::decode::DecodeLevel;
use crate::link::reader::LinkModes;
use crate::link::EndpointAddress;
use crate::outstation::config::*;
use crate::outstation::database::DatabaseHandle;
use crate::outstation::session::OutstationSession;
Expand Down Expand Up @@ -92,6 +93,10 @@ impl OutstationTask {
self.session.enabled()
}

pub(crate) fn change_master_address(&mut self, address: EndpointAddress) {
self.session.change_master_address(address);
}

/// run the outstation task asynchronously until a `SessionError` occurs
pub(crate) async fn run(&mut self, io: &mut PhysLayer) -> RunError {
let res = self
Expand Down
5 changes: 5 additions & 0 deletions dnp3/src/tcp/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ impl ClientTask {
async fn connect(&mut self) -> Result<(PhysLayer, SocketAddr, Option<Arc<String>>), Duration> {
loop {
let conn_info = self.connect_handler.next()?;
let settings = conn_info.settings;
if let Some((phys, addr, hostname)) = self.connect_to_endpoint(conn_info).await {
if let Some(master_address) = settings.master_address {
self.session.change_master_address(master_address);
}

self.connect_handler
.connected(addr, hostname.as_ref().map(|x| x.as_str()));
return Ok((phys, addr, hostname));
Expand Down
39 changes: 37 additions & 2 deletions dnp3/src/tcp/connector.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::app::{ConnectStrategy, ExponentialBackOff, RetryStrategy};
use crate::link::EndpointAddress;
use crate::tcp::{ConnectOptions, EndpointList};
use std::net::SocketAddr;
use std::sync::Arc;
Expand Down Expand Up @@ -45,12 +46,18 @@ impl From<String> for Endpoint {
}
}

#[derive(Default, Copy, Clone, Debug)]
pub(crate) struct SessionSettings {
pub(crate) master_address: Option<EndpointAddress>,
}

/// Information about the next connection attempt
#[derive(Clone, Debug)]
pub struct ConnectionInfo {
pub(crate) endpoint: Endpoint,
pub(crate) timeout: Option<Duration>,
pub(crate) local_endpoint: Option<SocketAddr>,
pub(crate) settings: SessionSettings,
}

impl ConnectionInfo {
Expand All @@ -61,6 +68,7 @@ impl ConnectionInfo {
endpoint,
timeout: None,
local_endpoint: None,
settings: Default::default(),
}
}

Expand All @@ -70,11 +78,38 @@ impl ConnectionInfo {
self.timeout = Some(timeout);
}

/// Set the local address to which the socket is bound. If not specified, then any available
/// adapter may be used with an OS-assigned port.
/// Set the local address to which the socket is bound.
///
/// If not specified, the OS will select the network adapter based on the routing table
/// for the destination address, and assign an ephemeral port.
///
/// # Use Cases
///
/// This is primarily useful for enforcing network segmentation in multi-homed systems,
/// such as OT gateways that bridge device and enterprise networks. By explicitly binding
/// to a specific adapter, you ensure traffic goes out the correct interface regardless
/// of routing table configuration, providing defense against misconfiguration.
///
/// # Port Selection
///
/// Typically you should specify port 0 to let the OS assign an ephemeral port while
/// forcing a specific network adapter. Using a specific non-zero port is rarely needed
/// for client connections and may cause bind failures if the port is already in use.
pub fn set_local_endpoint(&mut self, local: SocketAddr) {
self.local_endpoint = Some(local);
}

/// Change the master address for this and all subsequent connections.
///
/// The master address change is persistent and remains active across reconnections
/// until explicitly changed again. This is useful when an outstation needs to communicate
/// with different masters at different endpoints, such as during failover scenarios or
/// when connecting to a backup master.
///
/// By default, the outstation uses the master address specified in its configuration.
pub fn set_master_address(&mut self, address: EndpointAddress) {
self.settings.master_address = Some(address);
}
}

/// Provides fine-grained control over how TCP and TLS clients connect to endpoints
Expand Down
6 changes: 3 additions & 3 deletions dnp3/src/transport/mock/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,17 @@ impl MockReader {
unimplemented!()
}

pub(crate) fn peek(&self) -> Option<TransportData> {
pub(crate) fn peek(&self) -> Option<TransportData<'_>> {
Some(TransportData::Fragment(self.get(self.count)?))
}

pub(crate) fn pop(&mut self) -> Option<TransportData> {
pub(crate) fn pop(&mut self) -> Option<TransportData<'_>> {
let count = self.count;
self.count = 0;
Some(TransportData::Fragment(self.get(count)?))
}

fn get(&self, count: usize) -> Option<Fragment> {
fn get(&self, count: usize) -> Option<Fragment<'_>> {
if count == 0 {
return None;
}
Expand Down
12 changes: 12 additions & 0 deletions dnp3/src/util/session.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::app::Shutdown;
use crate::link::error::LinkError;
use crate::link::EndpointAddress;
use crate::util::phys::PhysLayer;
use std::time::Duration;

Expand Down Expand Up @@ -98,6 +99,17 @@ impl Session {
}
}

pub(crate) fn change_master_address(&mut self, address: EndpointAddress) {
match &mut self.inner {
SessionType::Master(_) => {
tracing::warn!("Attempted to change master address on a master session. This feature is only supported for outstation clients.");
}
Comment thread
jadamcrain marked this conversation as resolved.
SessionType::Outstation(x) => {
x.change_master_address(address);
}
}
}

pub(crate) fn enabled(&self) -> Enabled {
match &self.inner {
SessionType::Master(x) => x.enabled(),
Expand Down
66 changes: 33 additions & 33 deletions ffi/bindings/c/master_example.c
Original file line number Diff line number Diff line change
Expand Up @@ -651,37 +651,37 @@ dnp3_tls_client_config_t get_self_signed_tls_config()
}

// create a channel based on the command line arguments
dnp3_param_error_t create_and_run_channel(int argc, char *argv[], dnp3_runtime_t *runtime)
{
if(argc != 2) {
printf("you must specify a transport type\n");
printf("usage: master-example <channel> (tcp, serial, tls-ca, tls-self-signed)\n");
return -1;
}
if (strcmp(argv[1], "tcp") == 0) {
return run_tcp_channel(runtime);
}
else if (strcmp(argv[1], "serial") == 0) {
return run_serial_channel(runtime);
}
else if (strcmp(argv[1], "tls-ca") == 0) {
return run_tls_channel(runtime, get_ca_tls_config());
}
else if (strcmp(argv[1], "tls-self-signed") == 0) {
return run_tls_channel(runtime, get_self_signed_tls_config());
}
else {
printf("unknown channel type: %s\n", argv[1]);
return -1;
}
}
int main(int argc, char *argv[])
{
// ANCHOR: logging_init
// initialize logging with the default configuration
dnp3_configure_logging(dnp3_logging_config_init(), get_logger());
dnp3_param_error_t create_and_run_channel(const char* type, dnp3_runtime_t *runtime)
{
if (strcmp(type, "tcp") == 0) {
return run_tcp_channel(runtime);
}
else if (strcmp(type, "serial") == 0) {
return run_serial_channel(runtime);
}
else if (strcmp(type, "tls-ca") == 0) {
return run_tls_channel(runtime, get_ca_tls_config());
}
else if (strcmp(type, "tls-self-signed") == 0) {
return run_tls_channel(runtime, get_self_signed_tls_config());
}
else {
printf("unknown channel type: %s\n", type);
return -1;
}
}

int main(int argc, char *argv[])
{
if(argc != 2) {
printf("you must specify a transport type\n");
printf("usage: master-example <channel> (tcp, serial, tls-ca, tls-self-signed)\n");
return -1;
}

// ANCHOR: logging_init
// initialize logging with the default configuration
dnp3_configure_logging(dnp3_logging_config_init(), get_logger());
// ANCHOR_END: logging_init

// ANCHOR: runtime_create
Expand All @@ -696,8 +696,8 @@ int main(int argc, char *argv[])
return -1;
}

// create a channel based on the cmd line arguments and run it
int res = create_and_run_channel(argc, argv, runtime);
// create a channel based on the cmd line arguments and run it
int res = create_and_run_channel(argv[1], runtime);

// ANCHOR: runtime_destroy
dnp3_runtime_destroy(runtime);
Expand Down
12 changes: 6 additions & 6 deletions ffi/bindings/c/master_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,12 @@ dnp3::TlsClientConfig get_self_signed_tls_config()

int main(int argc, char *argv[])
{
if (argc != 2) {
std::cout << "you must specify a transport type" << std::endl;
std::cout << "usage: cpp-master-example <channel> (tcp, serial, tls-ca, tls-self-signed)" << std::endl;
return -1;
}

// ANCHOR: logging_init
dnp3::Logging::configure(dnp3::LoggingConfig(), std::make_unique<Logger>());
// ANCHOR_END: logging_init
Expand All @@ -581,12 +587,6 @@ int main(int argc, char *argv[])
auto runtime = dnp3::Runtime(dnp3::RuntimeConfig());
// ANCHOR_END: runtime_create

if (argc != 2) {
std::cout << "you must specify a transport type" << std::endl;
std::cout << "usage: cpp-outstation-example <channel> (tcp, serial, tls-ca, tls-self-signed)" << std::endl;
return -1;
}

const auto type = argv[1];

if (strcmp(type, "tcp") == 0) {
Expand Down
Loading
Loading