Merge branch 'ChannelTutorialUpdate' into 'master'

Update channel development tutorial

See merge request C3/C3!164
dependabot/npm_and_yarn/Src/WebController/UI/websocket-extensions-0.1.4
Grzegorz Rychlik 2020-03-05 14:40:45 +00:00
commit 5122ade4fb
1 changed files with 230 additions and 169 deletions

View File

@ -1,6 +1,6 @@
# Developing a Channel
The following tutorials describe the process of C3 channel development in a step by step matter, along with detailed explanations and best practices. This section will demonstrate how to create a channel that utilises Windows named pipes for communication. It will make use of MWRs helper class in order to achieve communication. The purpose of using this class is to abstract away from how to communicate with named pipes and focus on the C3 framework.
The following tutorials describe the process of C3 channel development in a step by step matter, along with detailed explanations and best practices. This section will demonstrate how to create a channel that utilizes regular files for communication.
## Basic Tutorial
This tutorial focuses only on a bare minimum effort required to develop a C3 Channel. First, open the C3 solution file in MS Visual Studio and add a new `*.cpp` file to the “Common” project. The Preferred location is: *“Src/Common/MWR/C3/Interfaces/Channels”* or its sub folder.
@ -10,62 +10,114 @@ This tutorial focuses only on a bare minimum effort required to develop a C3 Cha
Next, open the newly created file and enter a type that will represent the Channel. An example for a channel named `MyChannel` is as follows:
```cpp
#include "StdAfx.h"
#include "Common/MWR/WinTools/Pipe.h"
#include <fstream>
struct MyChannel : MWR::C3::Interfaces::Channel
namespace MWR::C3::Interfaces::Channels
{
MyChannel(MWR::ByteView arguments) : m_Pipe("MyChannelPipeName"_bv)
class MyChannel : public MWR::C3::Interfaces::Channel<MyChannel>
{
}
public:
MyChannel(MWR::ByteView bv) : m_Filename("MyChannelFile")
{
}
size_t OnSendToChannel(MWR::ByteView packet) override
{
return m_Pipe.Write(packet);
}
size_t OnSendToChannel(MWR::ByteView packet)
{
// Create/open binary file with m_Filename
std::ofstream file(m_Filename, std::ios::binary);
MWR::ByteVector OnReceiveFromChannel() override
{
return m_Pipe.Read();
}
// Write packet to file
file << std::string_view{ packet };
return packet.length();
}
MWR::ByteVector OnReceiveFromChannel()
{
// Open binary file with m_Filename
std::ifstream file{ m_Filename, std::ios::binary };
// read packet from file
return MWR::ByteVector{ std::istreambuf_iterator<char>{file}, {} };
}
private:
std::string m_Filename;
};
}
protected:
MWR::WinTools::Pipe m_Pipe;
};
```
A breakdown of this code is as such:
+ The first line is required by Visual Studio for projects using precompiled headers.
+ The `MyChannel` struct definition publicly inherits from a *CRTP type* called `Channel<>`, which is required by the C3 framework in order to register the Channel.
+ Next there are three overridden methods `OnSendToChannel`, `OnReceiveFromChannel` and a constructor that takes one `MWR::ByteView` argument. These methods are used to bind the channels functionality with the C3 framework.
+ Note the `m_Pipe` member variable, this is a wrapper for Windows named pipe.
+ For this basic tutorial the argument for the `MyChannel` constructor is ignored. Instead the named pipe member variable is initialised with a hardcoded name. Note that `_bv` is a literal for `MWR::ByteView`.
+ `OnSendToChannels` implementation simply takes its parameter (packet) and writes it to the pipe.
+ `OnReceiveFromChannel` reads from the pipe and returns the buffer.
+ Channels must be defined in `Something::Interfaces::Channels` namespace.
+ The `MyChannel` class definition publicly inherits from a *CRTP type* called `Channel<>`, which is required by the C3 framework in order to register the Channel.
+ Next there are three required methods `OnSendToChannel`, `OnReceiveFromChannel` and a constructor that takes one `MWR::ByteView` argument. These methods are used to bind the channels functionality with the C3 framework.
+ For this basic tutorial the argument for the `MyChannel` constructor is ignored. Instead the file name member variable is initialised with a hardcoded value.
+ `OnSendToChannel`'s implementation simply takes its parameter (packet) and writes it to the file.
+ `OnReceiveFromChannel` reads from the file and returns the buffer.
With this file added, the solution can be recompiled. Re-starting the C3 web application is all that is required after compilation to use the newly created channel.
With this file added, the solution can be recompiled.
Running ChannelLinter with -l switch should now include `MyChannel`
```
C:\Users\user\C3\Bin>ChannelLinter_d64.exe -l
Custom Command and Control - Channel linter. BUILD: C3-1.0.manual-build
Registered channels:
MyChannel
Slack
UncShareFile
```
and can be tested with `-i` switch: (`-a ""` is used to pass a empty `ByteView` to `MyChannel`'s constructor)
```
C:\Users\user\C3\Bin>ChannelLinter_d64.exe -n MyChannel -a "" -i
Custom Command and Control - Channel linter. BUILD: C3-1.0.manual-build
Channel: MyChannel
[Warning] create property does not exist, generating default one.
Parsing create arguments ... OK
Parsing command definitions ... OK
Registered commands:
id name
65534 Set UpdateDelayJitter
65535 Close
MyChannel's Capability json verified OK.
Creating channel ... OK
Creating complementary channel ... OK
Testing channel with 8 bytes of data ... OK
Testing channel with 64 bytes of data ... OK
Testing channel with 1024 bytes of data ... OK
Testing channel with 1048576 bytes of data ... OK
```
Re-starting the C3 web application and gateway is all that is required after compilation to use the newly created channel.
![C3](Res/ContributionGuide/02.png)
However this implementation has some limitations, critical for usage in C3:
+ It doesn't synchronize reads and writes to the file across processes.
+ It doesn't implement any mechanism that would prevent a channel to overwrite a packet that hasn't been read.
+ It doesn't clear the packet buffer (file) once it's read, so the same packet can be received multiple times.
For a complete implementation of a channel via files see `UncShareFile` channel which addresses those problems by using `.lock` files and not reusing the same file, but introducing a notion of packet direction and randomizing part of a file name.
## Advanced Tutorial
This tutorial will cover more advanced features that can allow developers to create complex channels with a rich feature set. This will be demonstrated by extending the previously written named pipe channel. This section will cover:
This tutorial will cover more advanced features that can allow developers to create complex channels with a rich feature set. This will be demonstrated by extending the previously written file channel. This section will cover:
+ How to define input parameters, allowing users to provide dynamic pipe names from the UI.
+ How to define input parameters, allowing users to provide dynamic file names from the UI.
+ How to add a custom command.
+ How to use Log functionality and the built-in string obfuscator.
+ How to set a channels initial *“Update Delay Jitter”* values.
+ How to set a channels initial *Update Delay Jitter* values.
+ How to perform refactoring, ensuring developers keep to code standards.
+ Discuss requirements of overridden methods and other implementation details.
### Adding a Channels Create Arguments
The previously created Channel from the Basic tutorial has a flaw in its implementation the pipe name is hard-coded. This not only prevents it from being reused multiple times in a single C3 network, but also disables some useful features. For example, this flaw prevents the channel from being used as a *Negotiation Channel*. Additionally, as C3 has no way of knowing the creation parameters of `MyChannel`, it will default them to a single binary blob of data:
The previously created Channel from the Basic tutorial has a flaw in its implementation the file name is hard-coded. This not only prevents it from being reused multiple times in a single C3 network, but also disables some useful features. For example, this flaw prevents the channel from being used as a *Negotiation Channel*. Additionally, as C3 has no way of knowing the creation parameters of `MyChannel`, it will default them to a single binary blob of data:
![C3](Res/ContributionGuide/03.png)
The UI can be made to explicitly ask for each parameter and validate their values in a few steps. First, the type of `m_Pipe` is changed from `MWR::WinTools::Pipe` to `MWR::WinTools::DuplexPipe`. The duplex pipe allows for two identifiers to be assigned for every `MyChannel` instance. Whilst not definitively required, this design decision adds a number of benefits.
The UI can be made to explicitly ask for each parameter and validate their values in a few steps. First, a single filename `m_Filename` is replaced by a pair of files `m_InFile` and `m_OutFile`, used to read and write data respectively. The two variables allow for two identifiers to be assigned for every `MyChannel` instance. Whilst not strictly required, this design decision adds a number of benefits.
Before further discussion is given as to the named pipe channel, it is important to understand what a *Double-ID design* is and how it operates. The core concept is to address two communicating interfaces with a unique pair of IDs as opposed to using one identifier for the pair.
Before we proceed with implementation, it is important to understand what a *Double-ID design* is and how it operates. The core concept is to address two communicating interfaces with a unique pair of IDs as opposed to using one identifier for the pair.
As an example, consider a file system Channel implementation and the various ways communication could be designed:
@ -80,35 +132,35 @@ All implementations are valid and will work with the C3 Framework, but some feat
As can be seen on the bottom diagram (compatible version), “Ch1” Interfaces output ID is the same as “Ch2” Interfaces input ID, and vice versa. The same goes for “Ch2” and “Ch3” pair. This diagram will now be applied to `MyChannel` every `MyChannel` instance will get an input ID and an output ID assigned to them.
To begin, the definition of the `GetCapability()` `static` method provides a JSON representation of `MyChannels` capabilities:
To begin, the definition of the `static const char* GetCapability()` method provides a JSON representation of `MyChannel`'s capabilities:
```cpp
static ByteView GetCapability()
static const char* GetCapability()
{
return R"({
"create":
{
"arguments":
[
return R"(
{
"create": {
"arguments": [
[
{
"type": "string",
"name": "Input ID",
"min": 4,
"randomize": true,
"description": "Used to distinguish packets for the channel"
},
{
"type": "string",
"name": "Output ID",
"min": 4,
"randomize": true,
"description": "Used to distinguish packets from the channel"
}
]
]
}
})";
"name": "Input ID",
"min": 4,
"randomize": true,
"description": "Used to distinguish packets for the channel"
},
{
"type": "string",
"name": "Output ID",
"min": 4,
"randomize": true,
"description": "Used to distinguish packets from the channel"
}
]
]
}
}
)";
}
```
@ -123,8 +175,9 @@ There are 2 parameters defined, both need to be at least 4 characters long and r
The next step in this tutorial involves updating `MyChannels` constructor to read the new arguments:
```cpp
MyChannel(ByteView arguments)
: m_Pipe(arguments.Read<std::string, std::string>())
MyChannel(MWR::ByteView bv)
: m_InFile(bv.Read<std::string>())
, m_OutFile(bv.Read<std::string>())
{
}
```
@ -160,7 +213,7 @@ Finally, C3 will add the “New Relay” option for all existing `MyChannels`, w
Every *Interface* provides two built-in *Commands*: Close and UpdateDelayJitter. However, channel developers are able to add their own custom *Commands*, by enlisting them in the `static` `GetCapability()` method:
```cpp
static ByteView GetCapability()
static const char* GetCapability()
{
return R"({
"commands":
@ -185,7 +238,7 @@ static ByteView GetCapability()
The JSON buffer that `GetCapability` returns defines one *Command* labeled “Test Command”, which takes one `string` argument. The `"id"` field (specified as equal to 0) is needed to distinguish between custom *Commands*, and is required to be in the range **0 32767**.
In `MyChannel`, an overriden implementation of `OnRunCommand` is defined:
In `MyChannel`, an overridden implementation of `OnRunCommand` is defined:
```cpp
ByteVector OnRunCommand(ByteView command) override
@ -196,7 +249,7 @@ ByteVector OnRunCommand(ByteView command) override
case 0:
return TestCommand(command);
default:
return __super::OnRunCommand(commandCopy);
return AbstractChannel::OnRunCommand(commandCopy);
}
}
```
@ -206,14 +259,14 @@ This method simply parses the first two bytes of the *Command* buffer as the *Co
```cpp
ByteVector TestCommand(ByteView args)
{
Log({ OBF("Custom Command : ") + args.Read(), LogMessage::Severity::DebugInformation });
Log({ OBF("Custom Command : ") + args.Read<std::string>(), LogMessage::Severity::DebugInformation });
return {};
}
```
The `TestCommand` method adds a debug text entry to C3s *Log*. `DebugInformation` is used which is rendered in a Relays console window only when compiled in debug mode. The message consists of two elements: a hard-coded text "Custom Command : " and a string that is provided by a user in the UI. Note the use of the `OBF` macro, this is used to encrypt strings at compile-time, which are decrypted at run-time only when required. Also of note is that the `TestCommand` handler returns an empty `MWR::ByteVector` as theres nothing to send back to the Gateway.
**Note:** The `GetCapability` method is compiled only when building Gateways executable and therefor its not required to use the `OBF` macro to obfuscate its strings.
**Note:** The `GetCapability` method is compiled only when building Gateways executable and therefore its not required to use the `OBF` macro to obfuscate its strings.
After rebuilding the solution once again and launching C3 the Test Command becomes available in `MyChannels` Command Center:
@ -227,26 +280,26 @@ After rebuilding the solution once again and launching C3 the Test Command becom
After developing a functionally working channel, the next stage is to refactor the code. The `MyChannel` type is still quite small, but for larger classes its common to split the implementation between a header and a compilation unit file. For `MyChannel` this involves adding a new *MyChannel.h* file in the same location as *MyChannel.cpp*. This also involves embracing the channel with the appropriate namespace declaration - `MWR::C3::Interfaces::Channels`. Finally, doxygen-style documentation is added to improve readability. *MyChannel.h* is then as such:
```cpp
#include "Common/MWR/WinTools/Pipe.h" //< For MWR::WinTools::Pipe.
#pragma once
namespace MWR::C3::Interfaces::Channels
{
/// Implementation of a Named Pipe Channel.
struct MyChannel : public Channel
/// Implementation of a File Channel.
class MyChannel : public MWR::C3::Interfaces::Channel<MyChannel>
{
public:
/// Public constructor.
/// @param arguments factory arguments.
MyChannel(ByteView arguments);
MyChannel(MWR::ByteView arguments);
/// OnSend callback implementation. Called every time attached Relay wants to send a packet through this Channel Device. @see Device::OnSend.
/// OnSend callback implementation. Called every time attached Relay wants to send a packet through this Channel Device. @see Device::OnSendToChannelInternal.
/// @param packet data to send through the Channel.
/// @return number of bytes successfully sent through the Channel. One call does not have to send all data. In that case chunking will take place, Chunks must be at least 64 bytes or equal to packet.size() to be accepted,
size_t OnSendToChannel(ByteView packet) override;
size_t OnSendToChannel(MWR::ByteView packet);
/// Reads a single C3 packet from Channel. Periodically called by attached Relay. Implementation should read the data (or return an empty buffer if there's nothing in the Channel waiting
/// to read) and leave as soon as possible.
/// Reads a single C3 packet from Channel. Periodically called by attached Relay. Implementation should read the data (or return an empty buffer if there's nothing in the Channel waiting to read) and leave as soon as possible.
/// @return ByteVector that contains a single packet retrieved from Channel.
ByteVector OnReceiveFromChannel() override;
MWR::ByteVector OnReceiveFromChannel();
/// Processes Commands addressed to this Channel.
/// @param command a buffer containing whole command and it's parameters.
@ -258,18 +311,22 @@ namespace MWR::C3::Interfaces::Channels
/// @returns ByteVector Command result.
ByteVector TestCommand(ByteView args);
/// Describes Channels creation parameters and custom Commands.
/// @return Channel's capability description in JSON format.
static ByteView GetCapability();
/// Describes Channels creation parameters and custom Commands.
/// @return Channel's capability description in JSON format.
static const char* GetCapability();
/// Explicit values used as the defaults for Channel's UpdateDelayJitter. Values can be changed later, at runtime.
constexpr static std::chrono::milliseconds s_MinUpdateDelayJitter = 100ms, s_MaxUpdateDelayJitter = 100ms;
/// Explicit values used as the defaults for Channel's UpdateDelayJitter. Values can be changed later, at runtime.
constexpr static std::chrono::milliseconds s_MinUpdateDelay = 1000ms, s_MaxUpdateDelay = 1000ms;
protected:
/// The pipe wrapper object.
MWR::WinTools::DuplexPipe m_Pipe;
private:
/// Name of file used to receive data.
std::string m_InFile;
/// Name of file used to send data.
std::string m_OutFile;
};
}
```
And the implementation within MyChannel.cpp:
@ -277,110 +334,114 @@ And the implementation within MyChannel.cpp:
```cpp
#include "StdAfx.h"
#include "MyChannel.h"
#include <fstream>
#include <filesystem>
/////////////////////////////////////////////////////////////////////////////////////////////////
MWR::C3::Interfaces::Channels::MyChannel::MyChannel(ByteView arguments)
: m_Pipe(arguments.Read < std::string, std::string > ())
namespace MWR::C3::Interfaces::Channels
{
Log({
OBF("Hello World from MyChannel!"),
LogMessage::Severity::DebugInformation
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////
size_t MWR::C3::Interfaces::Channels::MyChannel::OnSendToChannel(ByteView packet)
{
return m_Pipe.Write(packet);
}
/////////////////////////////////////////////////////////////////////////////////////////////////
MWR::ByteVector MWR::C3::Interfaces::Channels::MyChannel::OnReceiveFromChannel()
{
return m_Pipe.Read();
}
/////////////////////////////////////////////////////////////////////////////////////////////////
MWR::ByteVector MWR::C3::Interfaces::Channels::MyChannel::OnRunCommand(ByteView command)
{
/// Check if it's a custom or internal Command, then call appropriate handler.
auto commandCopy = command;
switch (command.Read < uint16_t > ())
{
case 0:
return TestCommand(command); //< Our custom Command.
default:
return __super::OnRunCommand(commandCopy); //< An internal Command.
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////
MWR::ByteVector MWR::C3::Interfaces::Channels::MyChannel::TestCommand(ByteView args)
{
Log({
OBF("Custom Command : ") + args.Read < std::string > (),
LogMessage::Severity::DebugInformation
});
return {};
}
/////////////////////////////////////////////////////////////////////////////////////////////////
MWR::ByteView MWR::C3::Interfaces::Channels::MyChannel::GetCapability()
{
return R "({
"create":
{
"arguments":
[
[
{
"type": "string",
"name": "Input ID",
"min": 4,
"randomize": true,
"description": "Used to distinguish packets for the channel"
},
{
"type": "string",
"name": "Output ID",
"min": 4,
"randomize": true,
"description": "Used to distinguish packets from the channel"
}
]
]
},
"commands":
[
MyChannel::MyChannel(MWR::ByteView bv)
: m_InFile(bv.Read<std::string>())
, m_OutFile(bv.Read<std::string>())
{
"name": "Test command",
"description": "Logs provided text",
"id": 0,
"arguments":
[
{
"name": "Message",
"description": "Message to Log."
}
]
}
]
})";
size_t MyChannel::OnSendToChannel(MWR::ByteView packet)
{
// Create/open binary file with m_OutFile name.
std::ofstream file(m_OutFile, std::ios::binary);
// Write packet to file
file << std::string_view{ packet };
return packet.length();
}
MWR::ByteVector MyChannel::OnReceiveFromChannel()
{
// Open binary file with m_InFile name.
std::ifstream file{ m_InFile, std::ios::binary };
// read packet from file
auto packet = MWR::ByteVector{ std::istreambuf_iterator<char>{file}, {} };
file.close();
std::filesystem::remove(m_OutFile);
return packet;
}
ByteVector MyChannel::OnRunCommand(ByteView command)
{
auto commandCopy = command;
switch (command.Read<uint16_t>())
{
case 0:
return TestCommand(command);
default:
return AbstractChannel::OnRunCommand(commandCopy);
}
}
ByteVector MyChannel::TestCommand(ByteView args)
{
Log({ OBF("Custom Command : ") + args.Read<std::string>(), LogMessage::Severity::DebugInformation });
return {};
}
const char* MyChannel::GetCapability()
{
return R"(
{
"create": {
"arguments": [
[
{
"type": "string",
"name": "Input ID",
"min": 4,
"randomize": true,
"description": "Used to distinguish packets for the channel"
},
{
"type": "string",
"name": "Output ID",
"min": 4,
"randomize": true,
"description": "Used to distinguish packets from the channel"
}
]
]
},
"commands": [
{
"name": "Test Command",
"description": "Logs provided text.",
"id": 0,
"arguments": [
{
"type": "string",
"name": "Message",
"description": "Message to Log."
}
]
}
]
}
)";
}
}
```
There are two new elements in the header file that have not previously been discussed - `s_MinUpdateDelayJitter` and `s_MaxUpdateDelayJitter`. All C3 Interfaces are updated periodically with some jitter. This is called the *Update Delay Jitter* and is specified by setting two range values in the UI minimal and maximal. If both values are the same, then *Update Delay Jitter* is constant. If they differ, each delay is randomised in the range specified by min and max. Default (initial) *Update Delay Jitter* is equal to 30ms. If an *interface* needs to specify a different initial value, then it should set it by defining `s_MinUpdateDelayJitter` and `s_MaxUpdateDelayJitter` `public constexpr static` members as demonstrated above. *Update Delay Jitter* can be changed at run-time with an *Interfaces Commands*.
There are two new elements in the header file that have not previously been discussed - `s_MinUpdateDelay` and `s_MaxUpdateDelay`. All C3 Interfaces are updated periodically with some jitter. This is called the *Update Delay Jitter* and is specified by setting two range values in the UI minimal and maximal. If both values are the same, then *Update Delay Jitter* is constant. If they differ, each delay is randomised in the range specified by min and max. Default (initial) *Update Delay Jitter* is equal to 30ms. If an *interface* needs to specify a different initial value, then it should set it by defining `s_MinUpdateDelay` and `s_MaxUpdateDelay` `public constexpr static` members as demonstrated above. *Update Delay Jitter* can be changed at run-time with an *Interfaces Commands*.
**Note:** When using third-party APIs for Command and Control (such as Slack), the values set in `s_MinUpdateDelayJitter` and `s_MaxUpdateDelayJitter` directly contribute to avoiding heavy rate limiting.
**Note:** When using third-party APIs for Command and Control (such as Slack), the values set in `s_MinUpdateDelay` and `s_MaxUpdateDelay` directly contribute to avoiding heavy rate limiting.
### Channel Development Summary
The tutorials provided here have discussed all the elements required to develop a channel in C3. `MyChannel` was intended to be as simple as possible in order to focus on C3 specifics. Channels can often be far more complicated and require longer implementations of `OnReceiveFromChannel` and `OnSendToChannel` methods. As such, the following details should be considered:
+ `OnReceiveFromChannel` is fired periodically, while `OnSendToChannel` is fired only if theres something to send though the Channel. Bear in mind that calls to both methods could be made at the same time from two different threads. Implement appropriate synchronization mechanisms if they share any non-atomic objects.
+ `OnReceiveFromChannel` is called periodically, while `OnSendToChannel` is called only if theres something to send through the Channel. Bear in mind that calls to both methods could be made at the same time from two different threads. Implement appropriate synchronization mechanisms if they share any non-atomic objects.
+ `OnSendToChannel` returns the number of bytes successfully sent to the channel. If its less than the size of the provided packet, but more than 63, *automatic chunking* kicks in. If a channel sends less than 64 bytes, C3 will attempt to resend the packet and therefore call `OnSendToChannel` with the same buffer again, while the complementary Channel automatically rejects the packet. It follows from the above that it is necessary for every C3 Channel to be able to send chunks of size 64 bytes and more. For Channels that fail to meet this requirement, C3 will be stuck useless. This "magic" value of 64 is called *C3 Minimal MTU*.
+ `OnReceiveFromChannel` should retrieve only one packet at a time or return an empty buffer (in case theres nothing to read). An option to return more than one packet is planned to be added in future C3 releases.
+ Both methods should perform the minimal number of operations and leave as soon as possible. There is a possibility to add single-threaded Relays to C3 in the future, which means that only one thread of execution will be shared between all interfaces and all their methods. Bear in mind that one stalling Channel will block the whole Relay, until this method returns. Thats why if a Channels overrides (especially updates - `OnReceiveFromChannel`) require more time to execute, the implementation should be split into smaller pieces, *Update Delay Jitter* should be increased, and those pieces invoked in an interlaced manner.
+ `OnReceiveFromChannel` can have one of two signatures:
+ `MWR::ByteVector OnReceiveFromChannel();` if it can retrieve only one packet at a time.
+ `std::vector<MWR::ByteVector> OnReceiveFromChannel();` if it can retrieve multiple packets at a time (eg. a whole page of messages in a single HTTP request)
In case there is nothing to read Channel should return an empty container.
+ Both methods should perform the minimal number of operations and leave as soon as possible. There is a possibility to add single-threaded Relays to C3 in the future, which means that only one thread of execution will be shared between all interfaces and all their methods. Bear in mind that one stalling Channel will block the whole Relay, until this method returns. Thats why if a Channels methods (especially updates - `OnReceiveFromChannel`) require more time to execute, the implementation should be split into smaller pieces, *Update Delay Jitter* should be increased, and those pieces invoked in an interlaced manner.