The External Convergence Layer Agent allows implementing Convergence Layer Agents externally (e.g. outside the dtn7-rs codebase). It works by exposing a realtime JSON API via WebSocket or TCP. With the help of the ECLA it is possible to easily implement new transmission layers in different languages. All languages that can encode / decode JSON and communicate via WebSocket or TCP should in theory work. Additionally, the ECLA contains an optional and simple beacon system that can be used for peer discovery.
A client that connects to the ECLA and implements a new transmission layer is called an External Convergence Layer Module (in short ECL-Module).
To enable the ECLA add the argument --ecla
to dtnd.
The WebSocket is accessible under the same port as defined by -w
, --web-port
and the route /ws/ecla
. An example for a web port 3000 would be 127.0.0.1:3000/ws/ecla
.
If the TCP Transport Layer is used the packets use a big-endian length delimited codec. More information about the codec can be found here: tokio_util::codec::length_delimited. This layer will be activated if the tcp port is set via the -ecla-tcp 7263
flag.
+----------+--------------------------------+
| len: u32 | frame payload |
+----------+--------------------------------+
Configuration can also happen via a config file.
[ecla]
enabled = true
tcp_port = 0
Normally dntd won't accept static peers for CLAs that are not present at startup. In case of ECLAs where a CLA will be registered at a later time it is still possible to add peers with a different notation. A ecla+
will indicate dtnd that the peer is intended for a ECLA and added without the CLA presence check.
Example: -s ecla+mtcp://127.0.0.1:4223/node2
After the initial connect to the ECLA the first packet that must be send is the RegisterPacket
that contains the name of the CLA and if the beacon system should be enabled. If the registration is successful the ECLA responds with a RegisteredPacket
containing basic information about the connected dtnd node. If a error occured a ErrorPacket
will be returned. Reasons for error can be:
- CLA with the same name is already registered
- Illegal name (e.g. empty)
ForwardDataPacket
contains bundle data. You can either receive this packet from the dtnd that the ECL-Module is connected to or from the transmission layer that the module implements.
If you receive the packet from the dtnd that means the ECL-Module should send the packet to the address specified in dst
field. If no dst
is specified, for example when the transmission layer doesn't have addressable id's send the packet to all possible targets. In case the transmission layer has addressable id's you must set the src
field to the address of the ECL-Module.
If you receive a packet from the transmission layer you must pass it to the ECLA as it is.
If the beacon is enabled dtnd will periodically send beacons to the ECL-Module acting as a basic peer discovery. The interval is specified by the announcement_interval
(-interval
, -i
cli flag).
If you receive the packet from the dtnd that means the ECL-Module can send the beacon to all reachable devices. If the transmission layer has addressable id's the ECL-Module should set the addr
field to it's own id.
If you receive a packet from the transmission layer you can pass it to the ECLA as it is.
All packets are JSON encoded and contain a field called type
which specifies (as the name implies) the type of the packet. The protocol is compact and contains only 5 different packet types:
dtnd → external
The Error
packet will be emitted if an error happens while registration.
{
"type": "Error",
"reason": "error text"
}
dtnd → external
The Registered
packet will be emitted if the registration was successful.
eid
: Endpoint ID of connected Nodenodeid
: Raw Node ID as string
{
"type": "Registered",
"eid": [1, "//nodex/..."],
"nodeid": "nodex"
}
external → dtnd
The Register
packet must be sent as first packet to the ECLA to register the ECLA-Module.
name
: Name of the new CLAenable_beacon
: If beacons should be periodically sent
{
"type": "Register",
"name": "CLA Name",
"enable_beacon": true
}
dtnd ⇄ external
src
: Address of data source- If it is received from the ECLA it should be set to a reachable address in the transmission layer
dst
: Address of data destinationbundle_id
: String representation of Bundle IDdata
: Base64 and CBOR encoded data containing the bundle information
{
"type": "ForwardData",
"src": "...",
"dst": "...",
"bundle_id": "...",
"data": "aGVsbG8...gd29ybGQ="
}
dtnd ⇄ external
eid
: Endpoint ID- If it comes from the ECLA eid is the Endpoint ID of the connected node
- If it is received from the transmission layer the eid is the Endpoint ID of the foreign dtnd
addr
: In transmission layer reachable address (Optional)- If it is received from the ECLA it should be set to a reachable address in the transmission layer
service_block
: Base64 and CBOR encoded data containing available CLAs and Services
{
"type": "Beacon",
"eid": [1, "//nodex/..."],
"addr": "...",
"service_block": "aGVsbG8...gd29ybGQ="
}
An implementation for a Rust WebSocket Client is included in the ecla
module.
use anyhow::Result;
use dtn7::cla::ecla::ws_client::Command::SendPacket;
use dtn7::cla::ecla::{ws_client, Packet};
use futures_util::{future, pin_mut};
use log::{error, info};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> Result<()> {
let (tx, mut rx) = mpsc::channel::<Packet>(100);
let (ctx, mut crx) = mpsc::channel::<Packet>(100);
// Creating the client task
tokio::spawn(async move {
let mut c = ws_client::new("myprotocol", "127.0.0.1:3002", "", tx, true)
.expect("couldn't create client");
// Get the command channel of the client
let cmd_chan = c.command_channel();
// Pass the new commands to the clients command channel
let read = tokio::spawn(async move {
while let Some(packet) = crx.recv().await {
if let Err(err) = cmd_chan.send(SendPacket(packet)).await {
error!("couldn't pass packet to client command channel: {}", err);
}
}
});
let connecting = c.serve();
pin_mut!(connecting);
// Wait for finish
future::select(connecting, read).await;
});
// Read from incoming packets
let read = tokio::spawn(async move {
while let Some(packet) = rx.recv().await {
match packet {
Packet::ForwardData(packet) => {
info!("Got ForwardDataPacket {} -> {}", packet.src, packet.dst);
// Send the ForwardDataPacket to the dst via your transmission layer
}
Packet::Beacon(packet) => {
info!("Got Beacon {}", packet.eid);
// Send the beacon somewhere via your transmission layer
}
_ => {}
}
}
});
// Implement your transmission layer somewhere, receive ForwardDataPacket
// and optionally Beacon packets. Pass them to the ECLA Client via the
// ctx command channel (see 'Sending Packets' below).
// Wait for finish
if let Err(err) = read.await {
error!("error while joining {}", err);
}
Ok(())
}
Sending packet to the client if you received it from the transmission layer
if let Err(err) = ctx.send(Command::SendPacket(Packet::ForwardDataPacket(
ForwardDataPacket {
data: vec![],
dst: "dst".to_string(),
src: "src".to_string(),
bundle_id: "id".to_string(),
},
))).await {
error!("couldn't send packet");
}
if let Err(err) = ctx.send(Command::Close).await {
error!("couldn't send close command");
}