Initial commit

This commit is contained in:
attermann 2023-10-06 17:24:48 -06:00
parent d3ea0eedd9
commit eef5f1b326
29 changed files with 3073 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

46
lib/README Normal file
View File

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

46
platformio.ini Normal file
View File

@ -0,0 +1,46 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
;default_envs = native
[env:avr-test]
platform = atmelavr
board = megaatmega2560
framework = arduino
debug_tool = simavr
[env:native]
platform = native
build_flags =
-std=c++11
-Wall
-Wextra
-Wno-missing-field-initializers
-Wno-format
-Isrc
-DNATIVE
lib_deps =
; operatorfoundation/Crypto@^0.4.0
lib_compat_mode = off
[env:ttgo-t-beam]
platform = espressif32
board = ttgo-t-beam
framework = arduino
monitor_speed = 115200
build_flags =
-Wall
-Wextra
-Wno-missing-field-initializers
-Wno-format
-Isrc
lib_deps =
operatorfoundation/Crypto@^0.4.0

88
src/Bytes.cpp Normal file
View File

@ -0,0 +1,88 @@
#include "Bytes.h"
using namespace RNS;
void Bytes::ownData() {
if (!_data) {
newData();
}
else if (!_owner) {
Data *data;
if (!_data->empty()) {
//extreme("Bytes is creating a writable copy of its shared data");
data = new Data(*_data.get());
}
else {
//extreme("Bytes is creating its own data because shared is empty");
data = new Data();
}
_data = SharedData(data);
_owner = true;
}
}
int8_t Bytes::compare(const Bytes &bytes) const {
if (_data == bytes._data) {
return 0;
}
else if (!_data) {
return -1;
}
else if (!bytes._data) {
return 1;
}
else if (*_data < *(bytes._data)) {
return -1;
}
else {
return 1;
}
}
char const hex_upper_chars[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
char const hex_lower_chars[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
std::string Bytes::toHex(bool upper /*= true*/) const {
if (!_data) {
return "";
}
std::string hex;
for (uint8_t byte : *_data) {
if (upper) {
hex += hex_upper_chars[ (byte & 0xF0) >> 4];
hex += hex_upper_chars[ (byte & 0x0F) >> 0];
}
else {
hex += hex_lower_chars[ (byte & 0xF0) >> 4];
hex += hex_lower_chars[ (byte & 0x0F) >> 0];
}
}
return hex;
}
void Bytes::assignHex(const char* hex) {
// if assignment is empty then clear data and don't bother creating new
if (hex == nullptr || hex[0] == 0) {
_data = nullptr;
return;
}
newData();
size_t len = strlen(hex);
for (size_t i = 0; i < len; i += 2) {
uint8_t byte = (hex[i] % 32 + 9) % 25 * 16 + (hex[i+1] % 32 + 9) % 25;
_data->push_back(byte);
}
}
void Bytes::appendHex(const char* hex) {
// if append is empty then do nothing
if (hex == nullptr || hex[0] == 0) {
return;
}
ownData();
size_t len = strlen(hex);
for (size_t i = 0; i < len; i += 2) {
uint8_t byte = (hex[i] % 32 + 9) % 25 * 16 + (hex[i+1] % 32 + 9) % 25;
_data->push_back(byte);
}
}

199
src/Bytes.h Normal file
View File

@ -0,0 +1,199 @@
#pragma once
#include "Log.h"
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
#include <vector>
#include <string>
#include <memory>
namespace RNS {
class Bytes {
private:
//typedef std::vector<uint8_t> Data;
using Data = std::vector<uint8_t>;
//typedef std::shared_ptr<Data> SharedData;
using SharedData = std::shared_ptr<Data>;
public:
Bytes() {
//extreme("Bytes object created from default, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_data.get()));
}
Bytes(const Bytes &bytes) {
//extreme("Bytes is using shared data");
_data = bytes.shareData();
_owner = false;
//extreme("Bytes object copy created from bytes \"" + toString() + "\", this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_data.get()));
}
Bytes(const uint8_t *chunk, size_t size) {
assign(chunk, size);
//extreme(std::string("Bytes object created from chunk \"") + toString() + "\", this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_data.get()));
}
Bytes(const char *string) {
assign(string);
//extreme(std::string("Bytes object created from string \"") + toString() + "\", this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_data.get()));
}
Bytes(const std::string &string) {
assign(string);
//extreme(std::string("Bytes object created from std::string \"") + toString() + "\", this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_data.get()));
}
~Bytes() {
//extreme(std::string("Bytes object destroyed \"") + toString() + "\", this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_data.get()));
}
inline Bytes& operator = (const Bytes &bytes) {
assign(bytes);
return *this;
}
inline Bytes& operator += (const Bytes &bytes) {
append(bytes);
return *this;
}
inline Bytes operator + (const Bytes &bytes) const {
Bytes newbytes(*this);
newbytes.append(bytes);
return newbytes;
}
inline bool operator == (const Bytes &bytes) const {
return compare(bytes) == 0;
}
inline bool operator != (const Bytes &bytes) const {
return compare(bytes) != 0;
}
inline bool operator < (const Bytes &bytes) const {
return compare(bytes) < 0;
}
inline bool operator > (const Bytes &bytes) const {
return compare(bytes) > 0;
}
inline operator bool() const {
return (_data && !_data->empty());
}
private:
inline SharedData shareData() const {
//extreme("Bytes is sharing its own data");
_owner = false;
return _data;
}
inline void newData(size_t size = 0) {
//extreme("Bytes is creating its own data");
if (size > 0) {
_data = SharedData(new Data(size));
}
else {
_data = SharedData(new Data());
}
_owner = true;
}
void ownData();
public:
int8_t compare(const Bytes &bytes) const;
inline size_t size() const { if (!_data) return 0; return _data->size(); }
inline bool empty() const { if (!_data) return true; return _data->empty(); }
inline size_t capacity() const { if (!_data) return 0; return _data->capacity(); }
inline const uint8_t *data() const { if (!_data) return nullptr; return _data->data(); }
inline std::string toString() const { if (!_data) return ""; return {(const char*)data(), size()}; }
std::string toHex(bool upper = true) const;
inline uint8_t *writable(size_t size) {
newData(size);
return _data->data();
}
inline void assign(const Bytes& bytes) {
// if assignment is empty then clear data and don't bother creating new
if (bytes.size() <= 0) {
_data = nullptr;
return;
}
newData();
_data->insert(_data->begin(), bytes._data->begin(), bytes._data->end());
}
inline void assign(const uint8_t *chunk, size_t size) {
// if assignment is empty then clear data and don't bother creating new
if (chunk == nullptr || size <= 0) {
_data = nullptr;
return;
}
newData();
_data->insert(_data->begin(), chunk, chunk + size);
}
inline void assign(const char* string) {
// if assignment is empty then clear data and don't bother creating new
if (string == nullptr || string[0] == 0) {
_data = nullptr;
return;
}
newData();
_data->insert(_data->begin(), (uint8_t *)string, (uint8_t *)string + strlen(string));
}
inline void assign(const std::string &string) { assign(string.c_str()); }
void assignHex(const char* hex);
inline void append(const Bytes& bytes) {
// if append is empty then do nothing
if (bytes.size() <= 0) {
return;
}
ownData();
_data->insert(_data->end(), bytes._data->begin(), bytes._data->end());
}
inline void append(const uint8_t *chunk, size_t size) {
// if append is empty then do nothing
if (chunk == nullptr || size <= 0) {
return;
}
ownData();
_data->insert(_data->end(), chunk, chunk + size);
}
inline void append(const char* string) {
// if append is empty then do nothing
if (string == nullptr || string[0] == 0) {
return;
}
ownData();
_data->insert(_data->end(), (uint8_t *)string, (uint8_t *)string + strlen(string));
}
inline void append(const std::string &string) { append(string.c_str()); }
inline void append(uint8_t byte) {
ownData();
_data->push_back(byte);
}
void appendHex(const char* hex);
private:
SharedData _data;
mutable bool _owner = true;
};
// following array function doesn't work without size since it's past as a pointer to the array sizeof() is of the pointer
//static inline Bytes bytesFromArray(const uint8_t arr[]) { return Bytes(arr, sizeof(arr)); }
//static inline Bytes bytesFromChunk(const uint8_t *ptr, size_t len) { return Bytes(ptr, len); }
static inline Bytes bytesFromChunk(const uint8_t *ptr, size_t len) { return {ptr, len}; }
//static inline Bytes bytesFromString(const char *str) { return Bytes((uint8_t*)str, strlen(str)); }
static inline Bytes bytesFromString(const char *str) { return {(uint8_t*)str, strlen(str)}; }
//zstatic inline Bytes bytesFromInt(const int) { return {(uint8_t*)str, strlen(str)}; }
static inline std::string stringFromBytes(const Bytes& bytes) { return {(const char*)bytes.data(), bytes.size()}; }
}
inline RNS::Bytes& operator << (RNS::Bytes &lhbytes, const RNS::Bytes &rhbytes) {
lhbytes.append(rhbytes);
return lhbytes;
}
inline RNS::Bytes& operator << (RNS::Bytes &lhbytes, uint8_t rhbyte) {
lhbytes.append(rhbyte);
return lhbytes;
}

View File

@ -0,0 +1,14 @@
#include "Fernet.h"
#include "../Log.h"
using namespace RNS::Cryptography;
Fernet::Fernet() {
log("Fernet object created", LOG_EXTREME);
}
Fernet::~Fernet() {
log("Fernet object destroyed", LOG_EXTREME);
}

18
src/Cryptography/Fernet.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
namespace RNS { namespace Cryptography {
class Fernet {
public:
static const uint8_t FERNET_OVERHEAD = 48; // Bytes
public:
Fernet();
~Fernet();
};
} }

View File

@ -0,0 +1,31 @@
#include "Hashes.h"
#include "../Log.h"
#include <SHA256.h>
#include <SHA512.h>
using namespace RNS::Cryptography;
/*
The SHA primitives are abstracted here to allow platform-
aware hardware acceleration in the future. Currently only
uses Python's internal SHA-256 implementation. All SHA-256
calls in RNS end up here.
*/
void RNS::Cryptography::sha256(uint8_t *hash, const uint8_t *data, uint16_t data_len) {
//extreme("Cryptography::sha256: data: " + data.toHex() );
SHA256 digest;
digest.reset();
digest.update(data, data_len);
digest.finalize(hash, 32);
//extreme("Cryptography::sha256: hash: " + hash.toHex() );
}
void RNS::Cryptography::sha512(uint8_t *hash, const uint8_t *data, uint16_t data_len) {
SHA512 digest;
digest.reset();
digest.update(data, data_len);
digest.finalize(hash, 64);
}

10
src/Cryptography/Hashes.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
namespace RNS { namespace Cryptography {
void sha256(uint8_t *hash, const uint8_t *data, uint16_t data_len);
void sha512(uint8_t *hash, const uint8_t *data, uint16_t data_len);
} }

347
src/Destination.cpp Normal file
View File

@ -0,0 +1,347 @@
#include "Destination.h"
#include "Log.h"
#include "Transport.h"
#include "Packet.h"
#include "Interfaces/Interface.h"
#include <time.h>
#include <string.h>
#include <stdexcept>
#include <vector>
using namespace RNS;
Destination::Destination(const Identity &identity, const directions direction, const types type, const char* app_name, const char *aspects) : _object(new Object(identity)) {
assert(_object);
// Check input values and build name string
if (strchr(app_name, '.') != nullptr) {
throw std::invalid_argument("Dots can't be used in app names");
}
_object->_type = type;
_object->_direction = direction;
std::string fullaspects(aspects);
if (!identity && direction == Destination::IN && _object->_type != Destination::PLAIN) {
debug("Destination::Destination: identity not provided, creating new one");
_object->_identity = Identity();
// CBA TODO should following include a "." delimiter?
fullaspects += _object->_identity.hexhash();
}
debug("Destination::Destination: full aspects: " + fullaspects);
if (_object->_identity && _object->_type == Destination::PLAIN) {
throw std::invalid_argument("Selected destination type PLAIN cannot hold an identity");
}
_object->_name = expand_name(_object->_identity, app_name, fullaspects.c_str());
debug("Destination::Destination: name: " + _object->_name);
// Generate the destination address hash
debug("Destination::Destination: creating hash...");
_object->_hash = hash(_object->_identity, app_name, fullaspects.c_str());
debug("Destination::Destination: hash: " + _object->_hash.toHex());
// CBA TEST CRASH
debug("Destination::Destination: creating name hash...");
_object->_name_hash = Identity::truncated_hash(expand_name(Identity::NONE, app_name, fullaspects.c_str()));
debug("Destination::Destination: name hash: " + _object->_name_hash.toHex());
_object->_hexhash = _object->_hash.toHex();
debug("Destination::Destination: hexhash: " + _object->_hexhash);
debug("Destination::Destination: calling register_destination");
Transport::register_destination(*this);
extreme("Destination object created");
}
Destination::~Destination() {
extreme("Destination object destroyed");
}
/*
:returns: A destination name in adressable hash form, for an app_name and a number of aspects.
*/
/*static*/ Bytes Destination::hash(const Identity &identity, const char *app_name, const char *aspects) {
//name_hash = Identity::full_hash(Destination.expand_name(None, app_name, *aspects).encode("utf-8"))[:(RNS.Identity.NAME_HASH_LENGTH//8)]
//addr_hash_material = name_hash
Bytes addr_hash_material = Identity::truncated_hash(expand_name(Identity::NONE, app_name, aspects));
//if identity != None:
// if isinstance(identity, RNS.Identity):
// addr_hash_material += identity.hash
// elif isinstance(identity, bytes) and len(identity) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
// addr_hash_material += identity
// else:
// raise TypeError("Invalid material supplied for destination hash calculation")
addr_hash_material << identity.hash();
return Identity::truncated_hash(addr_hash_material);
}
/*
:returns: A string containing the full human-readable name of the destination, for an app_name and a number of aspects.
*/
/*static*/ std::string Destination::expand_name(const Identity &identity, const char *app_name, const char *aspects) {
if (strchr(app_name, '.') != nullptr) {
throw std::invalid_argument("Dots can't be used in app names");
}
std::string name(app_name);
if (aspects != nullptr) {
name += std::string(".") + aspects;
}
if (identity) {
name += "." + identity.hexhash();
}
return name;
}
/*
Creates an announce packet for this destination and broadcasts it on all
relevant interfaces. Application specific data can be added to the announce.
:param app_data: *bytes* containing the app_data.
:param path_response: Internal flag used by :ref:`RNS.Transport<api-transport>`. Ignore.
*/
Packet Destination::announce(const Bytes &app_data, bool path_response, Interface *attached_interface, const Bytes &tag, bool send) {
assert(_object);
debug("Destination::announce: announcing destination...");
if (_object->_type != SINGLE) {
throw std::invalid_argument("Only SINGLE destination types can be announced");
}
if (_object->_direction != IN) {
throw std::invalid_argument("Only IN destination types can be announced");
}
time_t now = time(nullptr);
auto it = _object->_path_responses.begin();
while (it != _object->_path_responses.end()) {
// vector
//Response &entry = *it;
// map
Response &entry = (*it).second;
if (now > (entry.first + Destination::PR_TAG_WINDOW)) {
it = _object->_path_responses.erase(it);
}
else {
++it;
}
}
Bytes announce_data;
/*
// CBA TEST
debug("Destination::announce: performing path test...");
debug("Destination::announce: inserting path...");
_object->_path_responses.insert({Bytes("foo_tag"), {0, Bytes("this is foo tag")}});
debug("Destination::announce: inserting path...");
_object->_path_responses.insert({Bytes("test_tag"), {0, Bytes("this is test tag")}});
if (path_response) {
debug("Destination::announce: path_response is true");
}
if (!tag.empty()) {
debug("Destination::announce: tag is specified");
std::string tagstr((const char*)tag.data(), tag.size());
debug(std::string("Destination::announce: tag: ") + tagstr);
debug(std::string("Destination::announce: tag len: ") + std::to_string(tag.size()));
debug("Destination::announce: searching for tag...");
if (_object->_path_responses.find(tag) != _object->_path_responses.end()) {
debug("Destination::announce: found tag in _path_responses");
debug(std::string("Destination::announce: data: ") +_object->_path_responses[tag].second.toString());
}
else {
debug("Destination::announce: tag not found in _path_responses");
}
}
debug("Destination::announce: path test finished");
*/
if (path_response && !tag.empty() && _object->_path_responses.find(tag) != _object->_path_responses.end()) {
// This code is currently not used, since Transport will block duplicate
// path requests based on tags. When multi-path support is implemented in
// Transport, this will allow Transport to detect redundant paths to the
// same destination, and select the best one based on chosen criteria,
// since it will be able to detect that a single emitted announce was
// received via multiple paths. The difference in reception time will
// potentially also be useful in determining characteristics of the
// multiple available paths, and to choose the best one.
//zextreme("Using cached announce data for answering path request with tag "+RNS.prettyhexrep(tag));
announce_data << _object->_path_responses[tag].second;
}
else {
Bytes destination_hash = _object->_hash;
//zBytes random_hash = Identity::get_random_hash()[0:5] << int(time.time()).to_bytes(5, "big");
Bytes random_hash;
Bytes new_app_data(app_data);
if (new_app_data.empty() && !_object->_default_app_data.empty()) {
new_app_data = _object->_default_app_data;
}
Bytes signed_data;
debug("Destination::announce: hash: " + _object->_hash.toHex());
debug("Destination::announce: identity public key: " + _object->_identity.get_public_key().toHex());
debug("Destination::announce: name hash: " + _object->_name_hash.toHex());
debug("Destination::announce: random hash: " + random_hash.toHex());
debug("Destination::announce: new app data: " + new_app_data.toHex());
signed_data << _object->_hash << _object->_identity.get_public_key() << _object->_name_hash << random_hash;
if (new_app_data) {
signed_data << new_app_data;
}
debug("Destination::announce: signed data: " + signed_data.toHex());
Bytes signature(_object->_identity.sign(signed_data));
debug("Destination::announce: signature: " + signature.toHex());
announce_data << _object->_identity.get_public_key() << _object->_name_hash << random_hash << signature;
if (new_app_data) {
announce_data << new_app_data;
}
_object->_path_responses.insert({tag, {time(nullptr), announce_data}});
}
debug("Destination::announce: announce_data: " + announce_data.toHex());
Packet::context_types announce_context = Packet::CONTEXT_NONE;
if (path_response) {
announce_context = Packet::PATH_RESPONSE;
}
debug("Destination::announce: creating announce packet...");
Packet announce_packet(*this, announce_data, Packet::ANNOUNCE, announce_context,Transport::BROADCAST, Packet::HEADER_1, nullptr, attached_interface);
if (send) {
announce_packet.send();
return Packet::NONE;
}
else {
return announce_packet;
}
}
/*
Registers a function to be called when a link has been established to
this destination.
:param callback: A function or method with the signature *callback(link)* to be called when a new link is established with this destination.
*/
void Destination::set_link_established_callback(Callbacks::link_established callback) {
assert(_object);
_object->_callbacks._link_established = callback;
}
/*
Registers a function to be called when a packet has been received by
this destination.
:param callback: A function or method with the signature *callback(data, packet)* to be called when this destination receives a packet.
*/
void Destination::set_packet_callback(Callbacks::packet callback) {
assert(_object);
_object->_callbacks._packet = callback;
}
/*
Registers a function to be called when a proof has been requested for
a packet sent to this destination. Allows control over when and if
proofs should be returned for received packets.
:param callback: A function or method to with the signature *callback(packet)* be called when a packet that requests a proof is received. The callback must return one of True or False. If the callback returns True, a proof will be sent. If it returns False, a proof will not be sent.
*/
void Destination::set_proof_requested_callback(Callbacks::proof_requested callback) {
assert(_object);
_object->_callbacks._proof_requested = callback;
}
/*
Encrypts information for ``RNS.Destination.SINGLE`` or ``RNS.Destination.GROUP`` type destination.
:param plaintext: A *bytes-like* containing the plaintext to be encrypted.
:raises: ``ValueError`` if destination does not hold a necessary key for encryption.
*/
Bytes Destination::encrypt(const Bytes &data) {
assert(_object);
debug("Destination::encrypt: encrypting bytes");
/*
if (_object->_type == Destination::PLAIN) {
return data;
}
if (_object->_type == Destination::SINGLE && _object->_identity) {
return _object->_identity.encrypt(data)
}
if (_object->_type == Destination::GROUP {
if hasattr(self, "prv") and self.prv != None:
try:
return self.prv.encrypt(plaintext)
except Exception as e:
RNS.log("The GROUP destination could not encrypt data", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
}
*/
// MOCK
return data;
}
/*
Decrypts information for ``RNS.Destination.SINGLE`` or ``RNS.Destination.GROUP`` type destination.
:param ciphertext: *Bytes* containing the ciphertext to be decrypted.
:raises: ``ValueError`` if destination does not hold a necessary key for decryption.
*/
Bytes Destination::decrypt(const Bytes &data) {
assert(_object);
debug("Destination::decrypt: decrypting bytes");
/*
if (_object->_type == Destination::PLAIN) {
return data;
}
if (_object->_type == Destination::SINGLE && _object->_identity) {
return identity.decrypt(data);
}
if (_object->_type == Destination::GROUP) {
if hasattr(self, "prv") and self.prv != None:
try:
return self.prv.decrypt(ciphertext)
except Exception as e:
RNS.log("The GROUP destination could not decrypt data", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
}
*/
// MOCK
return data;
}
/*
Signs information for ``RNS.Destination.SINGLE`` type destination.
:param message: *Bytes* containing the message to be signed.
:returns: A *bytes-like* containing the message signature, or *None* if the destination could not sign the message.
*/
Bytes Destination::sign(const Bytes &message) {
assert(_object);
if (_object->_type == Destination::SINGLE && _object->_identity) {
return _object->_identity.sign(message);
}
return nullptr;
}

166
src/Destination.h Normal file
View File

@ -0,0 +1,166 @@
#pragma once
#include "Reticulum.h"
#include "Identity.h"
#include "Bytes.h"
#include <stdint.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <map>
namespace RNS {
class Interface;
class Packet;
class Link;
/**
* @brief A class used to describe endpoints in a Reticulum Network. Destination
* instances are used both to create outgoing and incoming endpoints. The
* destination type will decide if encryption, and what type, is used in
* communication with the endpoint. A destination can also announce its
* presence on the network, which will also distribute necessary keys for
* encrypted communication with it.
*
* @param identity An instance of :ref:`RNS.Identity<api-identity>`. Can hold only public keys for an outgoing destination, or holding private keys for an ingoing.
* @param direction ``RNS.Destination.IN`` or ``RNS.Destination.OUT``.
* @param type ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``.
* @param app_name A string specifying the app name.
* @param aspects Any non-zero number of string arguments.
*/
class Destination {
public:
class Callbacks {
public:
using link_established = void(*)(Link *link);
//using packet = void(*)(uint8_t *data, uint16_t data_len, Packet *packet);
using packet = void(*)(const Bytes &data, Packet *packet);
using proof_requested = void(*)(Packet *packet);
link_established _link_established = nullptr;
packet _packet = nullptr;
proof_requested _proof_requested = nullptr;
};
//typedef std::pair<time_t, std::string> Response;
using Response = std::pair<time_t, Bytes>;
//using Response = std::pair<time_t, std::vector<uint8_t>>;
enum NoneConstructor {
NONE
};
// Constants
enum types {
SINGLE = 0x00,
GROUP = 0x01,
PLAIN = 0x02,
LINK = 0x03,
};
enum proof_strategies {
PROVE_NONE = 0x21,
PROVE_APP = 0x22,
PROVE_ALL = 0x23,
};
enum request_policies {
ALLOW_NONE = 0x00,
ALLOW_ALL = 0x01,
ALLOW_LIST = 0x02,
};
enum directions {
IN = 0x11,
OUT = 0x12,
};
const uint8_t PR_TAG_WINDOW = 30;
public:
Destination(NoneConstructor none) {
extreme("Destination NONE object created");
}
Destination(const Destination &destination) : _object(destination._object) {
extreme("Destination object copy created");
}
Destination(const Identity &identity, const directions direction, const types type, const char* app_name, const char *aspects);
~Destination();
inline Destination& operator = (const Destination &destination) {
_object = destination._object;
extreme("Destination object copy created by assignment, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((uint32_t)_object.get()));
return *this;
}
inline operator bool() const {
return _object.get() != nullptr;
}
public:
static Bytes hash(const Identity &identity, const char *app_name, const char *aspects);
static std::string expand_name(const Identity &identity, const char *app_name, const char *aspects);
public:
Packet announce(const Bytes &app_data = {}, bool path_response = false, Interface *attached_interface = nullptr, const Bytes &tag = {}, bool send = true);
inline void set_accepts_links(bool accepts) { assert(_object); _object->_accept_link_requests = accepts; }
inline bool get_accepts_links() { assert(_object); return _object->_accept_link_requests; }
void set_link_established_callback(Callbacks::link_established callback);
void set_packet_callback(Callbacks::packet callback);
void set_proof_requested_callback(Callbacks::proof_requested callback);
Bytes encrypt(const Bytes &data);
Bytes decrypt(const Bytes &data);
Bytes sign(const Bytes &message);
// getters/setters
inline types type() const { assert(_object); return _object->_type; }
inline directions _direction() const { assert(_object); return _object->_direction; }
inline proof_strategies _proof_strategy() const { assert(_object); return _object->_proof_strategy; }
inline Bytes hash() const { assert(_object); return _object->_hash; }
inline Bytes link_id() const { assert(_object); return _object->_link_id; }
inline uint16_t mtu() const { assert(_object); return _object->_mtu; }
inline void mtu(uint16_t mtu) { assert(_object); _object->_mtu = mtu; }
private:
class Object {
public:
Object(const Identity &identity) : _identity(identity) {}
private:
bool _accept_link_requests = true;
Callbacks _callbacks;
//z_request_handlers = {}
types _type;
directions _direction;
proof_strategies _proof_strategy = PROVE_NONE;
uint16_t _mtu = 0;
//std::vector<Response> _path_responses;
std::map<Bytes, Response> _path_responses;
//z_links = []
Identity _identity;
std::string _name;
// Generate the destination address hash
Bytes _hash;
Bytes _name_hash;
std::string _hexhash;
// CBA TODO when is _default_app_data a "callable"?
Bytes _default_app_data;
//z_callback = None
//z_proofcallback = None
// CBA _link_id is expected by packet but only present in Link
// CBA TODO determine if Link needs to inherit from Destination or vice-versa
Bytes _link_id;
friend class Destination;
};
std::shared_ptr<Object> _object;
};
}

102
src/DumbBuffer.h Normal file
View File

@ -0,0 +1,102 @@
#pragma once
#include "Log.h"
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <vector>
#include <string>
#include <memory>
#define COW
namespace RNS {
class Bytes {
public:
Bytes(size_t capacity = 0) {
if (capacity > 0) {
//_vector.reserve(capacity);
_vector = std::shared_ptr<std::vector<uint8_t>>(new std::vector<uint8_t>(capacity));
}
else {
_vector = std::shared_ptr<std::vector<uint8_t>>(new std::vector<uint8_t>());
}
log("Bytes object created from default");
}
Bytes(const Bytes &bytes, size_t capacity = 0) : Bytes(capacity) {
//append(bytes);
_vector = bytes._vector;
_owner = false;
log(std::string("Bytes object created from bytes \"") + toString() + "\"");
}
Bytes(const uint8_t *chunk, size_t size, size_t capacity = 0) : Bytes(capacity) {
append(chunk, size);
log(std::string("Bytes object created from chunk \"") + toString() + "\"");
}
Bytes(const char *string, size_t capacity = 0) : Bytes(capacity) {
append(string);
log(std::string("Bytes object created from string \"") + toString() + "\"");
}
~Bytes() {
log(std::string("Bytes object destroyed \"") + toString() + "\"");
}
inline Bytes& operator + (const Bytes &bytes) {
append(bytes);
return *this;
}
inline Bytes& operator += (const Bytes &bytes) {
append(bytes);
return *this;
}
inline bool operator == (const Bytes &bytes) const {
return _vector == bytes._vector;
}
inline bool operator < (const Bytes &bytes) const {
return _vector < bytes._vector;
}
public:
inline size_t size() const { return _vector->size(); }
inline bool empty() const { return _vector->empty(); }
inline size_t capacity() const { return _vector->capacity(); }
inline const uint8_t *data() const { return _vector->data(); }
void append(const Bytes& bytes) {
_vector->insert(_vector->end(), bytes._vector->begin(), bytes._vector->end());
}
void append(const uint8_t *chunk, size_t size) {
_vector->insert(_vector->end(), chunk, chunk + size);
}
void append(const char* string) {
_vector->insert(_vector->end(), (uint8_t *)string, (uint8_t *)string + strlen(string));
}
inline std::string toString() { return {(const char*)data(), size()}; }
private:
//std::vector<uint8_t> _vector;
std::shared_ptr<std::vector<uint8_t>> _vector;
bool _owner = true;;
};
// following array function doesn't work without size since it's past as a pointer to the array sizeof() is of the pointer
//static inline Bytes bytesFromArray(const uint8_t arr[]) { return Bytes(arr, sizeof(arr)); }
//static inline Bytes bytesFromChunk(const uint8_t *ptr, size_t len) { return Bytes(ptr, len); }
static inline Bytes bytesFromChunk(const uint8_t *ptr, size_t len) { return {ptr, len}; }
//static inline Bytes bytesFromString(const char *str) { return Bytes((uint8_t*)str, strlen(str)); }
static inline Bytes bytesFromString(const char *str) { return {(uint8_t*)str, strlen(str)}; }
//zstatic inline Bytes bytesFromInt(const int) { return {(uint8_t*)str, strlen(str)}; }
static inline std::string stringFromBytes(const Bytes& bytes) { return {(const char*)bytes.data(), bytes.size()}; }
}
inline RNS::Bytes& operator << (RNS::Bytes &lhs, const RNS::Bytes &rhs) {
lhs.append(rhs);
return lhs;
}

102
src/Identity.cpp Normal file
View File

@ -0,0 +1,102 @@
#include "Identity.h"
#include "Log.h"
#include "Cryptography/Hashes.h"
#include <string.h>
using namespace RNS;
Identity::Identity(bool create_keys) : _object(new Object()) {
if (create_keys) {
createKeys();
}
extreme("Identity object created, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_object.get()));
}
void Identity::createKeys() {
assert(_object);
/*
self.prv = X25519PrivateKey.generate()
self.prv_bytes = self.prv.private_bytes()
self.sig_prv = Ed25519PrivateKey.generate()
self.sig_prv_bytes = self.sig_prv.private_bytes()
self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes()
self.sig_pub = self.sig_prv.public_key()
self.sig_pub_bytes = self.sig_pub.public_bytes()
*/
update_hashes();
//verbose("Identity keys created for " + _object->_hash.toHex());
}
Bytes Identity::get_public_key() {
assert(_object);
// MOCK
return "abc123";
}
void Identity::update_hashes() {
assert(_object);
_object->_hash = truncated_hash(get_public_key());
debug("Identity::update_hashes: hash: " + _object->_hash.toHex());
_object->_hexhash = _object->_hash.toHex();
debug("Identity::update_hashes: hexhash: " + _object->_hexhash);
};
/*
Signs information by the identity.
:param message: The message to be signed as *bytes*.
:returns: Signature as *bytes*.
:raises: *KeyError* if the instance does not hold a private key.
*/
Bytes Identity::sign(const Bytes &message) {
assert(_object);
/*
if self.sig_prv != None:
try:
return self.sig_prv.sign(message)
except Exception as e:
RNS.log("The identity "+str(self)+" could not sign the requested message. The contained exception was: "+str(e), RNS.LOG_ERROR)
raise e
else:
raise KeyError("Signing failed because identity does not hold a private key")
*/
// MOCK
return {message};
}
/*
Get a SHA-256 hash of passed data.
:param data: Data to be hashed as *bytes*.
:returns: SHA-256 hash as *bytes*
*/
/*static*/ Bytes Identity::full_hash(const Bytes &data) {
Bytes hash;
Cryptography::sha256(hash.writable(HASHLENGTH/8), data.data(), data.size());
//debug("Identity::full_hash: hash: " + hash.toHex());
return hash;
}
/*
Get a truncated SHA-256 hash of passed data.
:param data: Data to be hashed as *bytes*.
:returns: Truncated SHA-256 hash as *bytes*
*/
/*static*/ Bytes Identity::truncated_hash(const Bytes &data) {
Bytes hash = full_hash(data);
//Bytes truncated_hash(hash.data() + (TRUNCATED_HASHLENGTH/8), TRUNCATED_HASHLENGTH/8);
//debug("Identity::truncated_hash: truncated hash: " + truncated_hash.toHex());
return Bytes(hash.data() + (TRUNCATED_HASHLENGTH/8), TRUNCATED_HASHLENGTH/8);
}

89
src/Identity.h Normal file
View File

@ -0,0 +1,89 @@
#pragma once
#include "Reticulum.h"
#include "Log.h"
#include "Bytes.h"
#include "Cryptography/Fernet.h"
#include <memory>
#include <string>
namespace RNS {
class Identity {
public:
enum NoneConstructor {
NONE
};
//static const char CURVE[] = "Curve25519";
static constexpr const char* CURVE = "Curve25519";
// The curve used for Elliptic Curve DH key exchanges
static const uint16_t KEYSIZE = 256*2;
// X25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
// Non-configurable constants
static const uint8_t FERNET_OVERHEAD = RNS::Cryptography::Fernet::FERNET_OVERHEAD;
static const uint8_t AES128_BLOCKSIZE = 16; // In bytes
static const uint16_t HASHLENGTH = Reticulum::HASHLENGTH; // In bits
static const uint16_t SIGLENGTH = KEYSIZE; // In bits
static const uint8_t NAME_HASH_LENGTH = 80;
static const uint16_t TRUNCATED_HASHLENGTH = Reticulum::TRUNCATED_HASHLENGTH; // In bits
// Constant specifying the truncated hash length (in bits) used by Reticulum
// for addressable hashes and other purposes. Non-configurable.
public:
Identity(NoneConstructor none) {
extreme("Identity NONE object created, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_object.get()));
}
Identity(const Identity &identity) : _object(identity._object) {
extreme("Identity object copy created, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_object.get()));
}
Identity(bool create_keys = true);
~Identity() {
extreme("Identity object destroyed, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((ulong)_object.get()));
}
inline Identity& operator = (const Identity &identity) {
_object = identity._object;
extreme("Identity object copy created by assignment, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((uint32_t)_object.get()));
return *this;
}
inline operator bool() const {
return _object.get() != nullptr;
}
public:
void createKeys();
Bytes get_public_key();
void update_hashes();
static Bytes full_hash(const Bytes &data);
static Bytes truncated_hash(const Bytes &data);
Bytes sign(const Bytes &message);
inline Bytes hash() const { assert(_object); return _object->_hash; }
inline std::string hexhash() const { assert(_object); return _object->_hexhash; }
private:
class Object {
public:
Object() {
extreme("Identity::Data object created, this: " + std::to_string((ulong)this));
}
~Object() {
extreme("Identity::Data object destroyed, this: " + std::to_string((ulong)this));
}
private:
Bytes _hash;
std::string _hexhash;
friend class Identity;
};
std::shared_ptr<Object> _object;
};
}

View File

@ -0,0 +1,14 @@
#include "Interface.h"
#include "Log.h"
using namespace RNS;
Interface::Interface() {
log("Interface object created", LOG_EXTREME);
}
Interface::~Interface() {
log("Interface object destroyed", LOG_EXTREME);
}

View File

@ -0,0 +1,30 @@
#pragma once
//#include <Arduino.h>
namespace RNS {
class Interface {
public:
// Interface mode definitions
enum modes {
MODE_FULL = 0x01,
MODE_POINT_TO_POINT = 0x02,
MODE_ACCESS_POINT = 0x03,
MODE_ROAMING = 0x04,
MODE_BOUNDARY = 0x05,
MODE_GATEWAY = 0x06,
};
// Which interface modes a Transport Node
// should actively discover paths for.
//zDISCOVER_PATHS_FOR = [MODE_ACCESS_POINT, MODE_GATEWAY]
public:
Interface();
~Interface();
};
}

14
src/Link.cpp Normal file
View File

@ -0,0 +1,14 @@
#include "Link.h"
#include "Log.h"
using namespace RNS;
Link::Link() {
log("Link object created", LOG_EXTREME);
}
Link::~Link() {
log("Link object destroyed", LOG_EXTREME);
}

71
src/Link.h Normal file
View File

@ -0,0 +1,71 @@
#pragma once
#include "Reticulum.h"
#include "Identity.h"
namespace RNS {
class Link {
public:
class Callbacks {
public:
typedef void (*established)(Link *link);
typedef void (*closed)(Link *link);
};
static constexpr const char* CURVE = Identity::CURVE;
// The curve used for Elliptic Curve DH key exchanges
static const uint16_t ECPUBSIZE = 32+32;
static const uint8_t KEYSIZE = 32;
//static const uint16_t MDU = floor((Reticulum::MTU-Reticulum::IFAC_MIN_SIZE-Reticulum::HEADER_MINSIZE-Identity::FERNET_OVERHEAD)/Identity::AES128_BLOCKSIZE)*Identity::AES128_BLOCKSIZE - 1;
static const uint16_t MDU = ((Reticulum::MTU-Reticulum::IFAC_MIN_SIZE-Reticulum::HEADER_MINSIZE-Identity::FERNET_OVERHEAD)/Identity::AES128_BLOCKSIZE)*Identity::AES128_BLOCKSIZE - 1;
static const uint8_t ESTABLISHMENT_TIMEOUT_PER_HOP = Reticulum::DEFAULT_PER_HOP_TIMEOUT;
// Timeout for link establishment in seconds per hop to destination.
static const uint16_t TRAFFIC_TIMEOUT_FACTOR = 6;
static const uint16_t KEEPALIVE_TIMEOUT_FACTOR = 4;
// RTT timeout factor used in link timeout calculation.
static const uint8_t STALE_GRACE = 2;
// Grace period in seconds used in link timeout calculation.
static const uint16_t KEEPALIVE = 360;
// Interval for sending keep-alive packets on established links in seconds.
static const uint16_t STALE_TIME = 2*KEEPALIVE;
/*
If no traffic or keep-alive packets are received within this period, the
link will be marked as stale, and a final keep-alive packet will be sent.
If after this no traffic or keep-alive packets are received within ``RTT`` *
``KEEPALIVE_TIMEOUT_FACTOR`` + ``STALE_GRACE``, the link is considered timed out,
and will be torn down.
*/
enum status {
PENDING = 0x00,
HANDSHAKE = 0x01,
ACTIVE = 0x02,
STALE = 0x03,
CLOSED = 0x04
};
enum teardown_reasons {
TIMEOUT = 0x01,
INITIATOR_CLOSED = 0x02,
DESTINATION_CLOSED = 0x03,
};
enum resource_strategies {
ACCEPT_NONE = 0x00,
ACCEPT_APP = 0x01,
ACCEPT_ALL = 0x02,
};
public:
Link();
~Link();
};
}

47
src/Log.cpp Normal file
View File

@ -0,0 +1,47 @@
#include "Log.h"
#include <stdio.h>
using namespace RNS;
const char* getLevelName(LogLevel level) {
switch (level) {
case LOG_CRITICAL:
return "CRITICAL";
case LOG_ERROR:
return "ERROR";
case LOG_WARNING:
return "WARNING";
case LOG_NOTICE:
return "NOTICE";
case LOG_INFO:
return "INFO";
case LOG_VERBOSE:
return "VERBOSE";
case LOG_DEBUG:
return "DEBUG";
case LOG_EXTREME:
return "EXTRA";
default:
return "";
}
}
LogLevel _level = LOG_VERBOSE;
void RNS::loglevel(LogLevel level) {
_level = level;
}
void RNS::doLog(const char* msg, LogLevel level) {
if (level > _level) {
return;
}
#ifndef NATIVE
Serial.print(getLevelName(level));
Serial.print(" ");
Serial.println(msg);
#else
printf("%s: %s\n", getLevelName(level), msg);
#endif
}

56
src/Log.h Normal file
View File

@ -0,0 +1,56 @@
#pragma once
#ifndef NATIVE
#include <Arduino.h>
#endif
#include <string>
namespace RNS {
enum LogLevel {
LOG_CRITICAL = 0,
LOG_ERROR = 1,
LOG_WARNING = 2,
LOG_NOTICE = 3,
LOG_INFO = 4,
LOG_VERBOSE = 5,
LOG_DEBUG = 6,
LOG_EXTREME = 7,
};
void loglevel(LogLevel level);
void doLog(const char* msg, LogLevel level);
inline void log(const char* msg, LogLevel level = LOG_NOTICE) { doLog(msg, level); }
#ifndef NATIVE
inline void log(const String msg, LogLevel level = LOG_NOTICE) { doLog(msg.c_str(), level); }
#endif
inline void log(const std::string& msg, LogLevel level = LOG_NOTICE) { doLog(msg.c_str(), level); }
inline void critical(const char* msg) { doLog(msg, LOG_CRITICAL); }
inline void critical(const std::string& msg) { doLog(msg.c_str(), LOG_CRITICAL); }
inline void error(const char* msg) { doLog(msg, LOG_ERROR); }
inline void error(const std::string& msg) { doLog(msg.c_str(), LOG_ERROR); }
inline void warning(const char* msg) { doLog(msg, LOG_WARNING); }
inline void warning(const std::string& msg) { doLog(msg.c_str(), LOG_WARNING); }
inline void notice(const char* msg) { doLog(msg, LOG_NOTICE); }
inline void notice(const std::string& msg) { doLog(msg.c_str(), LOG_NOTICE); }
inline void info(const char* msg) { doLog(msg, LOG_INFO); }
inline void info(const std::string& msg) { doLog(msg.c_str(), LOG_INFO); }
inline void verbose(const char* msg) { doLog(msg, LOG_VERBOSE); }
inline void verbose(const std::string& msg) { doLog(msg.c_str(), LOG_VERBOSE); }
inline void debug(const char* msg) { doLog(msg, LOG_DEBUG); }
inline void debug(const std::string& msg) { doLog(msg.c_str(), LOG_DEBUG); }
inline void extreme(const char* msg) { doLog(msg, LOG_EXTREME); }
inline void extreme(const std::string& msg) { doLog(msg.c_str(), LOG_EXTREME); }
}

532
src/Packet.cpp Normal file
View File

@ -0,0 +1,532 @@
#include "Packet.h"
#include "Identity.h"
#include "Log.h"
#include <string.h>
#include <stdexcept>
using namespace RNS;
Packet::Packet(const Destination &destination, const Bytes &data, types packet_type, context_types context, Transport::types transport_type, header_types header_type, const uint8_t *transport_id, Interface *attached_interface, bool create_receipt) : _object(new Object(destination)) {
assert(_object);
if (_object->_destination) {
// CBA TODO handle NONE
if (transport_type == -1) {
transport_type = Transport::BROADCAST;
}
// following moved to object constructor to avoid extra NONE object
//_destination = destination;
_header_type = header_type;
_packet_type = packet_type;
_transport_type = transport_type;
_context = context;
//transport_id = transport_id;
//setTransportId(transport_id);
if (transport_id != nullptr) {
memcpy(_transport_id, transport_id, Reticulum::DESTINATION_LENGTH);
}
//data = data;
//setData(data);
if (data) {
if (data.size() > MDU) {
_truncated = true;
// CBA TODO add method to truncate
//zdata_len = MDU;
}
memcpy(_data, data.data(), data.size());
}
_flags = get_packed_flags();
_create_receipt = create_receipt;
}
else {
//_raw = data;
if (data) {
memcpy(_raw, data.data(), data.size());
}
_packed = true;
_fromPacked = true;
_create_receipt = false;
}
_attached_interface = attached_interface;
extreme("Packet object created");
}
Packet::~Packet() {
extreme("Packet object destroyed");
}
/*
void Packet::setTransportId(const uint8_t* transport_id) {
if (_transport_id == nullptr) {
delete[] _transport_id;
_transport_id = nullptr;
}
if (transport_id != nullptr) {
_transport_id = new uint8_t[Reticulum::ADDRESS_LENGTH];
memcpy(_transport_id, transport_id, Reticulum::ADDRESS_LENGTH);
}
}
void Packet::setTransportId(const uint8_t* header) {
if (_header == nullptr) {
delete[] _header;
_header = nullptr;
}
if (header != nullptr) {
_header = new uint8_t[HEADER_MAXSIZE];
memcpy(_header, header, HEADER_MAXSIZE);
}
}
void Packet::setRaw(const uint8_t* raw, uint16_t len) {
if (_raw == nullptr) {
delete[] _raw;
_raw = nullptr;
_raw_len = 0;
}
if (raw != nullptr) {
_raw = new uint8_t[_MTU];
memcpy(_raw, raw, len);
_raw_len = len;
}
}
void Packet::setData(const uint8_t* data, uint16_t len) {
if (_data == nullptr) {
delete[] _data;
_data = nullptr;
_data_len = 0;
}
if (data != nullptr) {
_data = new uint8_t[MDU];
memcpy(_data, data, len);
_data_len = len;
}
}
*/
uint8_t Packet::get_packed_flags() {
uint8_t packed_flags = 0;
if (_context == LRPROOF) {
packed_flags = (_header_type << 6) | (_transport_type << 4) | (Destination::LINK << 2) | _packet_type;
}
else {
packed_flags = (_header_type << 6) | (_transport_type << 4) | (_object->_destination.type() << 2) | _packet_type;
}
return packed_flags;
}
/*
== Reticulum Wire Format ======
A Reticulum packet is composed of the following fields:
[HEADER 2 bytes] [ADDRESSES 16/32 bytes] [CONTEXT 1 byte] [DATA 0-465 bytes]
* The HEADER field is 2 bytes long.
* Byte 1: [IFAC Flag], [Header Type], [Propagation Type], [Destination Type] and [Packet Type]
* Byte 2: Number of hops
* Interface Access Code field if the IFAC flag was set.
* The length of the Interface Access Code can vary from
1 to 64 bytes according to physical interface
capabilities and configuration.
* The ADDRESSES field contains either 1 or 2 addresses.
* Each address is 16 bytes long.
* The Header Type flag in the HEADER field determines
whether the ADDRESSES field contains 1 or 2 addresses.
* Addresses are SHA-256 hashes truncated to 16 bytes.
* The CONTEXT field is 1 byte.
* It is used by Reticulum to determine packet context.
* The DATA field is between 0 and 465 bytes.
* It contains the packets data payload.
IFAC Flag
-----------------
open 0 Packet for publically accessible interface
authenticated 1 Interface authentication is included in packet
Header Types
-----------------
type 1 0 Two byte header, one 16 byte address field
type 2 1 Two byte header, two 16 byte address fields
Propagation Types
-----------------
broadcast 00
transport 01
reserved 10
reserved 11
Destination Types
-----------------
single 00
group 01
plain 10
link 11
Packet Types
-----------------
data 00
announce 01
link request 10
proof 11
+- Packet Example -+
HEADER FIELD DESTINATION FIELDS CONTEXT FIELD DATA FIELD
_______|_______ ________________|________________ ________|______ __|_
| | | | | | | |
01010000 00000100 [HASH1, 16 bytes] [HASH2, 16 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 4
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = TRANSPORT
|+------------- Header Type = HEADER_2 (two byte header, two address fields)
+-------------- Access Codes = DISABLED
+- Packet Example -+
HEADER FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD
_______|_______ _______|_______ ________|______ __|_
| | | | | | | |
00000000 00000111 [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 7
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = BROADCAST
|+------------- Header Type = HEADER_1 (two byte header, one address field)
+-------------- Access Codes = DISABLED
+- Packet Example -+
HEADER FIELD IFAC FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD
_______|_______ ______|______ _______|_______ ________|______ __|_
| | | | | | | | | |
10000000 00000111 [IFAC, N bytes] [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 7
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = BROADCAST
|+------------- Header Type = HEADER_1 (two byte header, one address field)
+-------------- Access Codes = ENABLED
Size examples of different packet types
---------------------------------------
The following table lists example sizes of various
packet types. The size listed are the complete on-
wire size counting all fields including headers,
but excluding any interface access codes.
- Path Request : 51 bytes
- Announce : 167 bytes
- Link Request : 83 bytes
- Link Proof : 115 bytes
- Link RTT packet : 99 bytes
- Link keepalive : 20 bytes
*/
// Reticulum Packet Structure
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |I|H|PRT|DT |PT | hops | destination... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination | context | data ... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |I|H|PRT|DT |PT | hops | destination_1... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination_1... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination_1... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination_1... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination_1 | destination_2... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination_2... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination_2... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination_2... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ...destination_2 | context | data ... |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
void Packet::pack() {
assert(_object);
debug("Packet::pack: packing packet...");
//memcpy(_destination_hash, _destination->_hash.data(), Reticulum::DESTINATION_LENGTH);
memcpy(_destination_hash, _object->_destination.hash().data(), _object->_destination.hash().size());
_header[0] = _flags;
_header[1] = _hops;
//uint8_t *ciphertext;
if (_context == LRPROOF) {
// write header
//memcpy(_header+2, _destination->_link_id, Reticulum::DESTINATION_LENGTH);
debug("Packet::pack: destination link id: " + _object->_destination.link_id().toHex() );
memcpy(_header+2, _object->_destination.link_id().data(), _object->_destination.link_id().size());
_header[Reticulum::DESTINATION_LENGTH+2] = _context;
// prepend header to _data in _raw bytes
memcpy(_data-Reticulum::HEADER_MINSIZE, _header, Reticulum::HEADER_MINSIZE);
//ciphertext = _data;
}
else {
if (_header_type == HEADER_1) {
// write header
//memcpy(_header+2, _destination->_hash.data(), Reticulum::DESTINATION_LENGTH);
debug("Packet::pack: destination hash: " + _object->_destination.hash().toHex() );
memcpy(_header+2, _object->_destination.hash().data(), _object->_destination.hash().size());
_header[Reticulum::DESTINATION_LENGTH+2] = _context;
// prepend header to _data in _raw bytes
memcpy(_data-Reticulum::HEADER_MINSIZE, _header, Reticulum::HEADER_MINSIZE);
if (_packet_type == ANNOUNCE) {
// Announce packets are not encrypted
//ciphertext = _data;
}
else if (_packet_type == LINKREQUEST) {
// Link request packets are not encrypted
//ciphertext = _data;
}
else if (_packet_type == PROOF && _context == RESOURCE_PRF) {
// Resource proofs are not encrypted
//ciphertext = _data;
}
else if (_packet_type == PROOF && _object->_destination.type() == Destination::LINK) {
// Packet proofs over links are not encrypted
//ciphertext = _data;
}
else if (_context == RESOURCE) {
// A resource takes care of encryption
// by itself
//ciphertext = _data;
}
else if (_context == KEEPALIVE) {
// Keepalive packets contain no actual
// data
//ciphertext = _data;
}
else if (_context == CACHE_REQUEST) {
// Cache-requests are not encrypted
//ciphertext = _data;
}
else {
// In all other cases, we encrypt the packet
// with the destination's encryption method
// CBA TODO Figure out how to most efficiently pass in data and receive encrypted data back into _raw bytes
// CBA TODO Ensure that encrypted data does not exceed ENCRYPTED_MDU
// CBA TODO Determine if encrypt method can read from and write to the same bytes
//_data_len = _destination->encrypt(_data, _data, _data_len);
//uint8_t data[_data_len];
//memcpy(data, _data, _data_len);
//_data_len = _destination->encrypt(_data, data, _data_len);
Bytes plaintext(_data, _data_len);
Bytes bytes = _object->_destination.encrypt(plaintext);
_data_len = bytes.size();
}
}
else if (_header_type == HEADER_2) {
if (memcmp(_transport_id, EMPTY_DESTINATION, Reticulum::DESTINATION_LENGTH) == 0) {
throw std::invalid_argument("Packet with header type 2 must have a transport ID");
}
// write header
memcpy(_header+2, _transport_id, Reticulum::DESTINATION_LENGTH);
//memcpy(_header+Reticulum::DESTINATION_LENGTH+2, _destination->_hash.data(), Reticulum::DESTINATION_LENGTH);
debug("Packet::pack: destination hash: " + _object->_destination.hash().toHex() );
memcpy(_header+Reticulum::DESTINATION_LENGTH+2, _object->_destination.hash().data(), _object->_destination.hash().size());
_header[2*Reticulum::DESTINATION_LENGTH+2] = _context;
// prepend header to _data in _raw bytes
memcpy(_data-Reticulum::HEADER_MAXSIZE, _header, Reticulum::HEADER_MAXSIZE);
if (_packet_type == ANNOUNCE) {
// Announce packets are not encrypted
//ciphertext = _data;
}
}
}
if (_data_len > _mtu) {
throw std::length_error("Packet size of " + std::to_string(_data_len) + " exceeds MTU of " + std::to_string(_mtu) +" bytes");
}
_packed = true;
update_hash();
}
bool Packet::unpack() {
assert(_object);
debug("Packet::unpack: unpacking packet...");
try {
_flags = _raw[0];
_hops = _raw[1];
_header_type = static_cast<header_types>((_flags & 0b01000000) >> 6);
_transport_type = static_cast<Transport::types>((_flags & 0b00110000) >> 4);
_destination_type = static_cast<Destination::types>((_flags & 0b00001100) >> 2);
_packet_type = static_cast<types>(_flags & 0b00000011);
// CBA TODO detect invalid flags and throw error
if (false) {
log("Received malformed packet, dropping it.");
return false;
}
if (_header_type == HEADER_2) {
memcpy(_transport_id, _raw+2, Reticulum::DESTINATION_LENGTH);
memcpy(_destination_hash, _raw+Reticulum::DESTINATION_LENGTH+2, Reticulum::DESTINATION_LENGTH);
_context = static_cast<context_types>(_raw[2*Reticulum::DESTINATION_LENGTH+2]);
_data = _raw+2*Reticulum::DESTINATION_LENGTH+3;
}
else {
//memcpy(_transport_id, EMPTY_DESTINATION, Reticulum::DESTINATION_LENGTH);
memcpy(_destination_hash, _raw+2, Reticulum::DESTINATION_LENGTH);
_context = static_cast<context_types>(_raw[Reticulum::DESTINATION_LENGTH+2]);
_data = _raw+Reticulum::DESTINATION_LENGTH+3;
}
_packed = false;
update_hash();
}
catch (std::exception& ex) {
log(std::string("Received malformed packet, dropping it. The contained exception was: ") + ex.what(), LOG_EXTREME);
return false;
}
return true;
}
/*
Sends the packet.
:returns: A :ref:`RNS.PacketReceipt<api-packetreceipt>` instance if *create_receipt* was set to *True* when the packet was instantiated, if not returns *None*. If the packet could not be sent *False* is returned.
*/
bool Packet::send() {
assert(_object);
debug("Packet::send: sending packet...");
if (_sent) {
throw std::logic_error("Packet was already sent");
}
/*
if (_destination->type == RNS::Destination::LINK) {
if (_destination->status == RNS::Link::CLOSED) {
throw std::runtime_error("Attempt to transmit over a closed link");
}
else {
_destination->last_outbound = time();
_destination->tx += 1;
_destination->txbytes += _data_len;
}
}
*/
if (!_packed) {
pack();
}
if (RNS::Transport::outbound(*this)) {
//zreturn self.receipt
debug("Packet::send: successfully sent packet!!!");
// MOCK
return true;
}
else {
error("No interfaces could process the outbound packet");
_sent = false;
//z_receipt = None;
return false;
}
}
/*
Re-sends the packet.
:returns: A :ref:`RNS.PacketReceipt<api-packetreceipt>` instance if *create_receipt* was set to *True* when the packet was instantiated, if not returns *None*. If the packet could not be sent *False* is returned.
*/
bool Packet::resend() {
assert(_object);
debug("Packet::resend: re-sending packet...");
if (!_sent) {
throw std::logic_error("Packet was not sent yet");
}
// Re-pack the packet to obtain new ciphertext for
// encrypted destinations
pack();
if (RNS::Transport::outbound(*this)) {
//zreturn self.receipt
debug("Packet::resend: successfully sent packet!!!");
// MOCK
return true;
}
else {
error("No interfaces could process the outbound packet");
_sent = false;
//zself.receipt = None;
return false;
}
}
void Packet::update_hash() {
assert(_object);
_packet_hash = get_hash();
}
Bytes Packet::get_hash() {
assert(_object);
Bytes hashable_part = get_hashable_part();
return Identity::full_hash(hashable_part);
}
Bytes Packet::getTruncatedHash() {
assert(_object);
Bytes hashable_part = get_hashable_part();
return Identity::truncated_hash(hashable_part);
}
Bytes Packet::get_hashable_part() {
assert(_object);
Bytes hashable_part;
hashable_part << (uint8_t)(_raw[0] & 0b00001111);
hashable_part.append(_data-Reticulum::DESTINATION_LENGTH-1, _data_len+Reticulum::DESTINATION_LENGTH+1);
return hashable_part;
}

187
src/Packet.h Normal file
View File

@ -0,0 +1,187 @@
#pragma once
#include "Reticulum.h"
#include "Identity.h"
#include "Transport.h"
#include "Destination.h"
#include "Interfaces/Interface.h"
#include <memory>
#include <stdint.h>
#include <time.h>
namespace RNS {
class Packet;
class PacketProof;
class PacketReceipt;
class PacketReceiptCallbacks;
class Packet {
public:
enum NoneConstructor {
NONE
};
// Packet types
enum types {
DATA = 0x00, // Data packets
ANNOUNCE = 0x01, // Announces
LINKREQUEST = 0x02, // Link requests
PROOF = 0x03, // Proofs
};
// Header types
enum header_types {
HEADER_1 = 0x00, // Normal header format
HEADER_2 = 0x01, // Header format used for packets in transport
};
// Packet context types
enum context_types {
CONTEXT_NONE = 0x00, // Generic data packet
RESOURCE = 0x01, // Packet is part of a resource
RESOURCE_ADV = 0x02, // Packet is a resource advertisement
RESOURCE_REQ = 0x03, // Packet is a resource part request
RESOURCE_HMU = 0x04, // Packet is a resource hashmap update
RESOURCE_PRF = 0x05, // Packet is a resource proof
RESOURCE_ICL = 0x06, // Packet is a resource initiator cancel message
RESOURCE_RCL = 0x07, // Packet is a resource receiver cancel message
CACHE_REQUEST = 0x08, // Packet is a cache request
REQUEST = 0x09, // Packet is a request
RESPONSE = 0x0A, // Packet is a response to a request
PATH_RESPONSE = 0x0B, // Packet is a response to a path request
COMMAND = 0x0C, // Packet is a command
COMMAND_STATUS = 0x0D, // Packet is a status of an executed command
CHANNEL = 0x0E, // Packet contains link channel data
KEEPALIVE = 0xFA, // Packet is a keepalive packet
LINKIDENTIFY = 0xFB, // Packet is a link peer identification proof
LINKCLOSE = 0xFC, // Packet is a link close message
LINKPROOF = 0xFD, // Packet is a link packet proof
LRRTT = 0xFE, // Packet is a link request round-trip time measurement
LRPROOF = 0xFF, // Packet is a link request proof
};
// This is used to calculate allowable
// payload sizes
static const uint16_t HEADER_MAXSIZE = Reticulum::HEADER_MAXSIZE;
static const uint16_t MDU = Reticulum::MDU;
// With an MTU of 500, the maximum of data we can
// send in a single encrypted packet is given by
// the below calculation; 383 bytes.
//static const uint16_t ENCRYPTED_MDU = floor((Reticulum::MDU-Identity::FERNET_OVERHEAD-Identity::KEYSIZE/16)/Identity::AES128_BLOCKSIZE)*Identity::AES128_BLOCKSIZE - 1;
//static const uint16_t ENCRYPTED_MDU;
static const uint16_t ENCRYPTED_MDU = ((Reticulum::MDU-Identity::FERNET_OVERHEAD-Identity::KEYSIZE/16)/Identity::AES128_BLOCKSIZE)*Identity::AES128_BLOCKSIZE - 1;
// The maximum size of the payload data in a single encrypted packet
static const uint16_t PLAIN_MDU = MDU;
// The maximum size of the payload data in a single unencrypted packet
static const uint8_t TIMEOUT_PER_HOP = Reticulum::DEFAULT_PER_HOP_TIMEOUT;
//static constexpr const uint8_t EMPTY_DESTINATION[Reticulum::DESTINATION_LENGTH] = {0};
uint8_t EMPTY_DESTINATION[Reticulum::DESTINATION_LENGTH] = {0};
public:
Packet(const Destination &destination, const Bytes &data, types packet_type = DATA, context_types context = CONTEXT_NONE, Transport::types transport_type = Transport::BROADCAST, header_types header_type = HEADER_1, const uint8_t *transport_id = nullptr, Interface *attached_interface = nullptr, bool create_receipt = true);
Packet(NoneConstructor none) {
extreme("Packet NONE object created");
}
Packet(const Packet &packet) : _object(packet._object) {
extreme("Packet object copy created");
}
~Packet();
inline Packet& operator = (const Packet &packet) {
_object = packet._object;
extreme("Packet object copy created by assignment, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((uint32_t)_object.get()));
return *this;
}
inline operator bool() const {
return _object.get() != nullptr;
}
private:
/*
void setTransportId(const uint8_t* transport_id);
void setHeader(const uint8_t* header);
void setRaw(const uint8_t* raw, uint16_t len);
void setData(const uint8_t* rata, uint16_t len);
*/
private:
class Object {
public:
Object(const Destination &destination) : _destination(destination) {}
private:
Destination _destination;
friend class Packet;
};
std::shared_ptr<Object> _object;
public:
uint8_t get_packed_flags();
void pack();
bool unpack();
bool send();
bool resend();
void update_hash();
Bytes get_hash();
Bytes getTruncatedHash();
Bytes get_hashable_part();
private:
types _packet_type;
header_types _header_type;
context_types _context;
Transport::types _transport_type;
Destination::types _destination_type;
uint8_t _flags = 0;
uint8_t _hops = 0;
bool _packed = false;
bool _sent = false;
bool _create_receipt = false;
bool _fromPacked = false;
bool _truncated = false;
//z_receipt = nullptr;
uint16_t _mtu = Reticulum::MTU;
time_t _sent_at = 0;
Interface *_attached_interface = nullptr;
Interface *_receiving_interface = nullptr;
float _rssi = 0.0;
float _snr = 0.0;
//uint8_t _packet_hash[Reticulum::HASHLENGTH] = {0};
Bytes _packet_hash;
uint8_t _destination_hash[Reticulum::DESTINATION_LENGTH] = {0};
uint8_t _transport_id[Reticulum::DESTINATION_LENGTH] = {0};
uint8_t _raw[Reticulum::MTU];
uint8_t _header[Reticulum::HEADER_MAXSIZE];
uint8_t *_data = _raw + Reticulum::HEADER_MAXSIZE;
uint16_t _data_len = 0;
};
class ProofDestination {
};
class PacketReceipt {
public:
class Callbacks {
public:
typedef void (*delivery)(PacketReceipt *packet_receipt);
typedef void (*timeout)(PacketReceipt *packet_receipt);
};
};
}

14
src/Reticulum.cpp Normal file
View File

@ -0,0 +1,14 @@
#include "Reticulum.h"
#include "Log.h"
using namespace RNS;
Reticulum::Reticulum() : _object(new Object()) {
extreme("Reticulum object created");
}
Reticulum::~Reticulum() {
extreme("Reticulum object destroyed");
}

112
src/Reticulum.h Normal file
View File

@ -0,0 +1,112 @@
#pragma once
#include "Log.h"
#include <memory>
#include <stdint.h>
#include <vector>
namespace RNS {
class Reticulum {
private:
class Object {
private:
friend class Reticulum;
};
public:
enum NoneConstructor {
NONE
};
// Future minimum will probably be locked in at 251 bytes to support
// networks with segments of different MTUs. Absolute minimum is 219.
static const uint16_t MTU = 500;
/*
The MTU that Reticulum adheres to, and will expect other peers to
adhere to. By default, the MTU is 507 bytes. In custom RNS network
implementations, it is possible to change this value, but doing so will
completely break compatibility with all other RNS networks. An identical
MTU is a prerequisite for peers to communicate in the same network.
Unless you really know what you are doing, the MTU should be left at
the default value.
*/
static const uint16_t MAX_QUEUED_ANNOUNCES = 16384;
static const uint32_t QUEUED_ANNOUNCE_LIFE = 60*60*24;
static const uint8_t ANNOUNCE_CAP = 2;
/*
The maximum percentage of interface bandwidth that, at any given time,
may be used to propagate announces. If an announce was scheduled for
broadcasting on an interface, but doing so would exceed the allowed
bandwidth allocation, the announce will be queued for transmission
when there is bandwidth available.
Reticulum will always prioritise propagating announces with fewer
hops, ensuring that distant, large networks with many peers on fast
links don't overwhelm the capacity of smaller networks on slower
mediums. If an announce remains queued for an extended amount of time,
it will eventually be dropped.
This value will be applied by default to all created interfaces,
but it can be configured individually on a per-interface basis.
*/
static const uint16_t MINIMUM_BITRATE = 500;
// TODO: To reach the 300bps level without unreasonably impacting
// performance on faster links, we need a mechanism for setting
// this value more intelligently. One option could be inferring it
// from interface speed, but a better general approach would most
// probably be to let Reticulum somehow continously build a map of
// per-hop latencies and use this map for the timeout calculation.
static const uint8_t DEFAULT_PER_HOP_TIMEOUT = 6;
static const uint16_t HASHLENGTH = 256; // In bits
// Length of truncated hashes in bits.
static const uint16_t TRUNCATED_HASHLENGTH = 128; // In bits
static const uint16_t HEADER_MINSIZE = 2+1+(TRUNCATED_HASHLENGTH/8)*1; // In bytes
static const uint16_t HEADER_MAXSIZE = 2+1+(TRUNCATED_HASHLENGTH/8)*2; // In bytes
static const uint16_t IFAC_MIN_SIZE = 1;
//zIFAC_SALT = bytes.fromhex("adf54d882c9a9b80771eb4995d702d4a3e733391b2a0f53f416d9f907e55cff8")
static const uint16_t MDU = MTU - HEADER_MAXSIZE - IFAC_MIN_SIZE;
static const uint32_t RESOURCE_CACHE = 24*60*60;
static const uint16_t JOB_INTERVAL = 5*60;
static const uint16_t CLEAN_INTERVAL = 15*60;
static const uint16_t PERSIST_INTERVAL = 60*60*12;
static const uint16_t GRACIOUS_PERSIST_INTERVAL = 60*5;
static const uint8_t DESTINATION_LENGTH = TRUNCATED_HASHLENGTH/8; // In bytes
public:
Reticulum();
Reticulum(NoneConstructor none) {
extreme("Reticulum NONE object created");
}
Reticulum(const Reticulum &reticulum) : _object(reticulum._object) {
extreme("Reticulum object copy created");
}
~Reticulum();
inline Reticulum& operator = (const Reticulum &reticulum) {
_object = reticulum._object;
extreme("Reticulum object copy created by assignment, this: " + std::to_string((ulong)this) + ", data: " + std::to_string((uint32_t)_object.get()));
return *this;
}
inline operator bool() const {
return _object.get() != nullptr;
}
private:
std::shared_ptr<Object> _object;
};
}

43
src/Transport.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "Transport.h"
#include "Destination.h"
#include "Log.h"
using namespace RNS;
Transport::Transport() {
log("Transport object created", LOG_EXTREME);
}
Transport::~Transport() {
log("Transport object destroyed", LOG_EXTREME);
}
/*static*/ void Transport::register_destination(Destination &destination) {
destination.mtu(Reticulum::MTU);
/*
if (destination->direction == Destination::IN) {
for (registered_destination in Transport.destinations) {
if (destination->hash == registered_destination->hash) {
raise KeyError("Attempt to register an already registered destination.")
throw std::runtime_error("Attempt to register an already registered destination.");
}
}
Transport.destinations.append(destination);
if (Transport.owner.is_connected_to_shared_instance) {
if (destination->type == Destination::SINGLE) {
destination->announce(path_response=True);
}
}
}
*/
}
/*static*/ bool Transport::outbound(const Packet &packet) {
// mock
return true;
}

66
src/Transport.h Normal file
View File

@ -0,0 +1,66 @@
#pragma once
#include "Link.h"
namespace RNS {
class Packet;
class Destination;
class Transport {
public:
// Constants
enum types {
BROADCAST = 0x00,
TRANSPORT = 0x01,
RELAY = 0x02,
TUNNEL = 0x03,
NONE = 0xFF,
};
enum reachabilities {
REACHABILITY_UNREACHABLE = 0x00,
REACHABILITY_DIRECT = 0x01,
REACHABILITY_TRANSPORT = 0x02,
};
static constexpr const char* APP_NAME = "rnstransport";
static const uint8_t PATHFINDER_M = 128; // Max hops
// Maximum amount of hops that Reticulum will transport a packet.
static const uint8_t PATHFINDER_R = 1; // Retransmit retries
static const uint8_t PATHFINDER_G = 5; // Retry grace period
static constexpr const float PATHFINDER_RW = 0.5; // Random window for announce rebroadcast
static const uint32_t PATHFINDER_E = 60*60*24*7; // Path expiration of one week
static const uint32_t AP_PATH_TIME = 60*60*24; // Path expiration of one day for Access Point paths
static const uint32_t ROAMING_PATH_TIME = 60*60*6; // Path expiration of 6 hours for Roaming paths
// TODO: Calculate an optimal number for this in
// various situations
static const uint8_t LOCAL_REBROADCASTS_MAX = 2; // How many local rebroadcasts of an announce is allowed
static const uint8_t PATH_REQUEST_TIMEOUT = 15; // Default timuout for client path requests in seconds
static constexpr const float PATH_REQUEST_GRACE = 0.35; // Grace time before a path announcement is made, allows directly reachable peers to respond first
static const uint8_t PATH_REQUEST_RW = 2; // Path request random window
static const uint8_t PATH_REQUEST_MI = 5; // Minimum interval in seconds for automated path requests
static constexpr const float LINK_TIMEOUT = Link::STALE_TIME * 1.25;
static const uint16_t REVERSE_TIMEOUT = 30*60; // Reverse table entries are removed after 30 minutes
static const uint32_t DESTINATION_TIMEOUT = 60*60*24*7; // Destination table entries are removed if unused for one week
static const uint16_t MAX_RECEIPTS = 1024; // Maximum number of receipts to keep track of
static const uint8_t MAX_RATE_TIMESTAMPS = 16; // Maximum number of announce timestamps to keep per destination
public:
Transport();
~Transport();
public:
static void register_destination(Destination &destination);
static bool outbound(const Packet &packet);
};
}

456
src/main.cpp Normal file
View File

@ -0,0 +1,456 @@
//#define NDEBUG
#include "Reticulum.h"
#include "Identity.h"
#include "Destination.h"
#include "Packet.h"
#include "Bytes.h"
#ifndef NATIVE
#include <Arduino.h>
#endif
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <map>
//#include <sstream>
#include <assert.h>
// Let's define an app name. We'll use this for all
// destinations we create. Since this basic example
// is part of a range of example utilities, we'll put
// them all within the app namespace "example_utilities"
const char* APP_NAME = "example_utilities";
// We initialise two lists of strings to use as app_data
const char* fruits[] = {"Peach", "Quince", "Date", "Tangerine", "Pomelo", "Carambola", "Grape"};
const char* noble_gases[] = {"Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon", "Oganesson"};
void testMap()
{
const uint8_t prestr[] = "Hello";
const uint8_t poststr[] = "World";
RNS::Bytes prebuf(prestr, 5);
assert(prebuf.size() == 5);
assert(memcmp(prebuf.data(), "Hello", prebuf.size()) == 0);
RNS::Bytes postbuf(poststr, 5);
assert(postbuf.size() == 5);
assert(memcmp(postbuf.data(), "World", postbuf.size()) == 0);
std::map<RNS::Bytes, std::string> map;
map.insert({prebuf, "hello"});
map.insert({postbuf, "world"});
assert(map.size() == 2);
auto preit = map.find(prebuf);
assert(preit != map.end());
assert((*preit).second.compare("hello") == 0);
//if (preit != map.end()) {
// RNS::log(std::string("found prebuf: ") + (*preit).second);
//}
auto postit = map.find(postbuf);
assert(postit != map.end());
assert((*postit).second.compare("world") == 0);
//if (postit != map.end()) {
// RNS::log(std::string("found postbuf: ") + (*postit).second);
//}
const uint8_t newstr[] = "World";
RNS::Bytes newbuf(newstr, 5);
assert(newbuf.size() == 5);
assert(memcmp(newbuf.data(), "World", newbuf.size()) == 0);
auto newit = map.find(newbuf);
assert(newit != map.end());
assert((*newit).second.compare("world") == 0);
//if (newit != map.end()) {
// RNS::log(std::string("found newbuf: ") + (*newit).second);
//}
std::string str = map["World"];
assert(str.size() == 5);
assert(str.compare("world") == 0);
}
void testBytes() {
RNS::Bytes bytes;
assert(!bytes);
assert(bytes.size() == 0);
assert(bytes.empty() == true);
assert(bytes.data() == nullptr);
const uint8_t prestr[] = "Hello";
const uint8_t poststr[] = " World";
RNS::Bytes prebuf(prestr, 5);
assert(prebuf);
assert(prebuf.size() == 5);
assert(memcmp(prebuf.data(), "Hello", prebuf.size()) == 0);
assert(!(bytes == prebuf));
assert(bytes != prebuf);
assert(bytes < prebuf);
RNS::Bytes postbuf(poststr, 6);
assert(postbuf);
assert(postbuf.size() == 6);
assert(memcmp(postbuf.data(), " World", postbuf.size()) == 0);
assert(!(postbuf == bytes));
assert(postbuf != bytes);
assert(postbuf > bytes);
assert(!(prebuf == postbuf));
assert(prebuf != postbuf);
//if (prebuf == postbuf) {
// RNS::log("bytess are the same");
//}
//else {
// RNS::log("bytess are different");
//}
bytes += prebuf + postbuf;
assert(bytes.size() == 11);
assert(memcmp(bytes.data(), "Hello World", bytes.size()) == 0);
//RNS::log("assign bytes: " + bytes.toString());
//RNS::log("assign prebuf: " + prebuf.toString());
//RNS::log("assign postbuf: " + postbuf.toString());
bytes = "Foo";
assert(bytes.size() == 3);
assert(memcmp(bytes.data(), "Foo", bytes.size()) == 0);
bytes = prebuf + postbuf;
assert(bytes.size() == 11);
assert(memcmp(bytes.data(), "Hello World", bytes.size()) == 0);
// stream into empty bytes
{
RNS::Bytes strmbuf;
strmbuf << prebuf << postbuf;
//RNS::extreme("stream strmbuf: " + strmbuf.toString());
//RNS::extreme("stream prebuf: " + prebuf.toString());
//RNS::extreme("stream postbuf: " + postbuf.toString());
assert(strmbuf.size() == 11);
assert(memcmp(strmbuf.data(), "Hello World", strmbuf.size()) == 0);
assert(prebuf.size() == 5);
assert(memcmp(prebuf.data(), "Hello", prebuf.size()) == 0);
assert(postbuf.size() == 6);
assert(memcmp(postbuf.data(), " World", postbuf.size()) == 0);
}
// stream into populated bytes
{
RNS::Bytes strmbuf("Stream ");
assert(strmbuf);
assert(strmbuf.size() == 7);
assert(memcmp(strmbuf.data(), "Stream ", strmbuf.size()) == 0);
strmbuf << prebuf << postbuf;
//RNS::extreme("stream strmbuf: " + strmbuf.toString());
//RNS::extreme("stream prebuf: " + prebuf.toString());
//RNS::extreme("stream postbuf: " + postbuf.toString());
assert(strmbuf.size() == 18);
assert(memcmp(strmbuf.data(), "Stream Hello World", strmbuf.size()) == 0);
assert(prebuf.size() == 5);
assert(memcmp(prebuf.data(), "Hello", prebuf.size()) == 0);
assert(postbuf.size() == 6);
assert(memcmp(postbuf.data(), " World", postbuf.size()) == 0);
}
// stream with assignment
// (this is a known and correct but perhaps unexpected and non-intuitive side-effect of assignment with stream)
{
RNS::Bytes strmbuf = prebuf << postbuf;
//RNS::extreme("stream strmbuf: " + strmbuf.toString());
//RNS::extreme("stream prebuf: " + prebuf.toString());
//RNS::extreme("stream postbuf: " + postbuf.toString());
assert(strmbuf.size() == 11);
assert(memcmp(strmbuf.data(), "Hello World", strmbuf.size()) == 0);
assert(prebuf.size() == 11);
assert(memcmp(prebuf.data(), "Hello World", prebuf.size()) == 0);
assert(postbuf.size() == 6);
assert(memcmp(postbuf.data(), " World", postbuf.size()) == 0);
}
{
RNS::Bytes nonebuf = nullptr;
assert(!nonebuf);
assert(nonebuf.size() == 0);
assert(nonebuf.data() == nullptr);
}
}
void testCowBytes() {
RNS::Bytes bytes1("1");
assert(bytes1.size() == 1);
assert(memcmp(bytes1.data(), "1", bytes1.size()) == 0);
RNS::Bytes bytes2(bytes1);
assert(bytes2.size() == 1);
assert(memcmp(bytes2.data(), "1", bytes2.size()) == 0);
assert(bytes2.data() == bytes1.data());
RNS::Bytes bytes3(bytes2);
assert(bytes3.size() == 1);
assert(memcmp(bytes3.data(), "1", bytes3.size()) == 0);
assert(bytes3.data() == bytes2.data());
//RNS::log("pre bytes1 ptr: " + std::to_string((uint32_t)bytes1.data()) + " data: " + bytes1.toString());
//RNS::log("pre bytes2 ptr: " + std::to_string((uint32_t)bytes2.data()) + " data: " + bytes2.toString());
//RNS::log("pre bytes3 ptr: " + std::to_string((uint32_t)bytes3.data()) + " data: " + bytes3.toString());
//bytes1.append("mississippi");
//assert(bytes1.size() == 12);
//assert(memcmp(bytes1.data(), "1mississippi", bytes1.size()) == 0);
//assert(bytes1.data() != bytes2.data());
bytes2.append("mississippi");
assert(bytes2.size() == 12);
assert(memcmp(bytes2.data(), "1mississippi", bytes2.size()) == 0);
assert(bytes2.data() != bytes1.data());
bytes3.assign("mississippi");
assert(bytes3.size() == 11);
assert(memcmp(bytes3.data(), "mississippi", bytes3.size()) == 0);
assert(bytes3.data() != bytes2.data());
//RNS::log("post bytes1 ptr: " + std::to_string((uint32_t)bytes1.data()) + " data: " + bytes1.toString());
//RNS::log("post bytes2 ptr: " + std::to_string((uint32_t)bytes2.data()) + " data: " + bytes2.toString());
//RNS::log("post bytes3 ptr: " + std::to_string((uint32_t)bytes3.data()) + " data: " + bytes3.toString());
}
void testObjects() {
RNS::Reticulum reticulum_default;
assert(reticulum_default);
RNS::Reticulum reticulum_none(RNS::Reticulum::NONE);
assert(!reticulum_none);
RNS::Reticulum reticulum_default_copy(reticulum_default);
assert(reticulum_default_copy);
RNS::Reticulum reticulum_none_copy(reticulum_none);
assert(!reticulum_none_copy);
}
void testBytesConversion() {
{
RNS::Bytes bytes("Hello World");
std::string hex = bytes.toHex();
RNS::extreme("text: \"" + bytes.toString() + "\" upper hex: \"" + hex + "\"");
assert(hex.length() == 22);
assert(hex.compare("48656C6C6F20576F726C64") == 0);
}
{
RNS::Bytes bytes("Hello World");
std::string hex = bytes.toHex(false);
RNS::extreme("text: \"" + bytes.toString() + "\" lower hex: \"" + hex + "\"");
assert(hex.length() == 22);
assert(hex.compare("48656c6c6f20576f726c64") == 0);
}
{
std::string hex("48656C6C6F20576F726C64");
RNS::Bytes bytes;
bytes.assignHex(hex.c_str());
std::string text = bytes.toString();
RNS::extreme("hex: \"" + hex + "\" text: \"" + text + "\"");
assert(text.length() == 11);
assert(text.compare("Hello World") == 0);
}
{
std::string hex("48656c6c6f20576f726c64");
RNS::Bytes bytes;
bytes.assignHex(hex.c_str());
std::string text = bytes.toString();
RNS::extreme("hex: \"" + hex + "\" text: \"" + text + "\"");
assert(text.length() == 11);
assert(text.compare("Hello World") == 0);
bytes.assignHex(hex.c_str());
text = bytes.toString();
RNS::extreme("hex: \"" + hex + "\" text: \"" + text + "\"");
assert(text.length() == 11);
assert(text.compare("Hello World") == 0);
bytes.appendHex(hex.c_str());
text = bytes.toString();
RNS::extreme("hex: \"" + hex + "\" text: \"" + text + "\"");
assert(text.length() == 22);
assert(text.compare("Hello WorldHello World") == 0);
}
}
/*
void announceLoop(RNS::Destination &destination_1, RNS::Destination &destination_2) {
// Let the user know that everything is ready
RNS::log("Announce example running, hit enter to manually send an announce (Ctrl-C to quit)");
// We enter a loop that runs until the users exits.
// If the user hits enter, we will announce our server
// destination on the network, which will let clients
// know how to create messages directed towards it.
//while (true) {
//zentered = input();
RNS::log("Sending announce...");
// Randomly select a fruit
//const char* fruit = fruits[rand() % sizeof(fruits)];
const char* fruit = fruits[rand() % 7];
//RNS::log(fruit);
RNS::log(std::string("fruit: ") + fruit);
// Send the announce including the app data
if (destination_1) {
//destination_1.announce(RNS::bytesFromString(fruit));
// CBA TEST path
destination_1.announce(RNS::bytesFromString(fruit), true, nullptr, RNS::bytesFromString("test_tag"));
//zRNS::log(std::string("Sent announce from ") + RNS::prettyhexrep(destination_1->_hash) +" ("+ (const char*)destination_1->_name + ")");
}
// Randomly select a noble gas
//const char* noble_gas = noble_gases[rand() % sizeof(noble_gas)];
const char* noble_gas = noble_gases[rand() % 7];
//RNS::log(noble_gas);
RNS::log(std::string("noble_gas: ") + noble_gas);
// Send the announce including the app data
if (destination_2) {
destination_2.announce(RNS::bytesFromString(noble_gas));
//zRNS::log(std::string("Sent announce from ") + RNS::prettyhexrep(destination_2->_hash) + " (" + destination_2->_name + ")");
}
#ifndef NATIVE
delay(1000);
#else
usleep(1000000);
#endif
//}
}
// This initialisation is executed when the program is started
void program_setup() {
// We must first initialise Reticulum
RNS::Reticulum reticulum;
// Randomly create a new identity for our example
RNS::Identity identity;
// Using the identity we just created, we create two destinations
// in the "example_utilities.announcesample" application space.
//
// Destinations are endpoints in Reticulum, that can be addressed
// and communicated with. Destinations can also announce their
// existence, which will let the network know they are reachable
// and autoomatically create paths to them, from anywhere else
// in the network.
RNS::Destination destination_1(identity, RNS::Destination::IN, RNS::Destination::SINGLE, APP_NAME, "announcesample.fruits");
// CBA TEST no identity
//RNS::Destination destination_1(RNS::Identity::NONE, RNS::Destination::IN, RNS::Destination::SINGLE, APP_NAME, "announcesample.fruits");
//RNS::Destination *destination_2(identity, RNS::Destination::IN, RNS::Destination::SINGLE, APP_NAME, "announcesample.noble_gases");
RNS::Destination destination_2(RNS::Destination::NONE);
// We configure the destinations to automatically prove all
// packets adressed to it. By doing this, RNS will automatically
// generate a proof for each incoming packet and transmit it
// back to the sender of that packet. This will let anyone that
// tries to communicate with the destination know whether their
// communication was received correctly.
//zdestination_1->set_proof_strategy(RNS::Destination::PROVE_ALL);
//zdestination_2->set_proof_strategy(RNS::Destination::PROVE_ALL);
// We create an announce handler and configure it to only ask for
// announces from "example_utilities.announcesample.fruits".
// Try changing the filter and see what happens.
//zannounce_handler = ExampleAnnounceHandler(
//z aspect_filter="example_utilities.announcesample.fruits";
//z)
// We register the announce handler with Reticulum
//zRNS::Transport.register_announce_handler(announce_handler);
// Everything's ready!
// Let's hand over control to the announce loop
announceLoop(destination_1, destination_2);
}
*/
#ifndef NATIVE
void setup() {
Serial.begin(115200);
Serial.print("Hello from T-Beam on PlatformIO!\n");
//std::stringstream test;
// !!! just adding this single stringstream alone (not even using it) adds a whopping 17.1% !!!
// !!! JUST SAY NO TO STRINGSTREAM !!!
RNS::loglevel(RNS::LOG_EXTREME);
// 18.5% completely empty program
// 21.8% baseline here with serial
RNS::Reticulum reticulum;
// 21.9% (+0.1%)
RNS::Identity identity;
// 22.6% (+0.7%)
RNS::Destination destination(identity, RNS::Destination::IN, RNS::Destination::SINGLE, "test", "context");
// 23.0% (+0.4%)
destination.announce(RNS::bytesFromString("fruit"), true, nullptr, RNS::bytesFromString("test_tag"));
// 23.9% (+0.8%)
#ifndef NDEBUG
// begin with logging turned down for unit tests
RNS::loglevel(RNS::LOG_WARNING);
//RNS::loglevel(RNS::LOG_EXTREME);
testMap();
testBytes();
testCowBytes();
testObjects();
testBytesConversion();
// 24.8% (+0.9%)
#endif
// increase logging for functional tests
RNS::loglevel(RNS::LOG_EXTREME);
//program_setup();
Serial.print("Goodbye from T-Beam on PlatformIO!\n");
}
void loop() {
}
#else
int main(int argc, char **argv) {
printf("Hello from Native on PlatformIO!\n");
program_setup();
printf("Goodbye from Native on PlatformIO!\n");
}
#endif

11
test/README Normal file
View File

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html

157
test/test.cpp Normal file
View File

@ -0,0 +1,157 @@
#include "Reticulum.h"
#include "Identity.h"
#include "Destination.h"
#ifndef NATIVE
#include <Arduino.h>
#endif
#include <stdlib.h>
#include <unistd.h>
#include <unity.h>
// Let's define an app name. We'll use this for all
// destinations we create. Since this basic example
// is part of a range of example utilities, we'll put
// them all within the app namespace "example_utilities"
const char* APP_NAME = "example_utilities";
// We initialise two lists of strings to use as app_data
const char* fruits[] = {"Peach", "Quince", "Date", "Tangerine", "Pomelo", "Carambola", "Grape"};
const char* noble_gases[] = {"Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon", "Oganesson"};
void announceLoop(RNS::Destination* destination_1, RNS::Destination* destination_2) {
// Let the user know that everything is ready
RNS::log("Announce example running, hit enter to manually send an announce (Ctrl-C to quit)");
// We enter a loop that runs until the users exits.
// If the user hits enter, we will announce our server
// destination on the network, which will let clients
// know how to create messages directed towards it.
while (true) {
//zentered = input();
#ifndef NATIVE
delay(1000);
#else
usleep(1000);
#endif
RNS::log("Sending announce...");
// Randomly select a fruit
//const char* fruit = fruits[rand() % sizeof(fruits)];
const char* fruit = fruits[rand() % 7];
RNS::log(fruit);
//RNS::log(String("fruit: ") + fruit);
// Send the announce including the app data
/*
destination_1->announce(app_data=fruit.encode("utf-8"));
RNS::log(
"Sent announce from "+
RNS.prettyhexrep(destination_1.hash)+
" ("+destination_1.name+")"
);
*/
// Randomly select a noble gas
//const char* noble_gas = noble_gases[rand() % sizeof(noble_gas)];
const char* noble_gas = noble_gases[rand() % 7];
RNS::log(noble_gas);
//RNS::log(String("noble_gas: ") + noble_gas);
// Send the announce including the app data
/*
destination_2->announce(app_data=noble_gas.encode("utf-8"));
RNS::log(
"Sent announce from "+
RNS.prettyhexrep(destination_2.hash)+
" ("+destination_2.name+")"
);
*/
}
}
// This initialisation is executed when the program is started
void program_setup() {
// We must first initialise Reticulum
RNS::Reticulum* reticulum = new RNS::Reticulum();
// Randomly create a new identity for our example
RNS::Identity* identity = new RNS::Identity();
// Using the identity we just created, we create two destinations
// in the "example_utilities.announcesample" application space.
//
// Destinations are endpoints in Reticulum, that can be addressed
// and communicated with. Destinations can also announce their
// existence, which will let the network know they are reachable
// and autoomatically create paths to them, from anywhere else
// in the network.
RNS::Destination* destination_1 = new RNS::Destination(
identity,
RNS::Destination::IN,
RNS::Destination::SINGLE,
APP_NAME,
//z"announcesample",
//z"fruits"
nullptr
);
RNS::Destination* destination_2 = new RNS::Destination(
identity,
RNS::Destination::IN,
RNS::Destination::SINGLE,
APP_NAME,
//z"announcesample",
//z"noble_gases"
nullptr
);
// We configure the destinations to automatically prove all
// packets adressed to it. By doing this, RNS will automatically
// generate a proof for each incoming packet and transmit it
// back to the sender of that packet. This will let anyone that
// tries to communicate with the destination know whether their
// communication was received correctly.
//zdestination_1->set_proof_strategy(RNS::Destination::PROVE_ALL);
//zdestination_2->set_proof_strategy(RNS::Destination::PROVE_ALL);
// We create an announce handler and configure it to only ask for
// announces from "example_utilities.announcesample.fruits".
// Try changing the filter and see what happens.
//zannounce_handler = ExampleAnnounceHandler(
//z aspect_filter="example_utilities.announcesample.fruits";
//z)
// We register the announce handler with Reticulum
//zRNS::Transport.register_announce_handler(announce_handler);
// Everything's ready!
// Let's hand over control to the announce loop
announceLoop(destination_1, destination_2);
}
#ifndef NATIVE
void setup() {
Serial.begin(115200);
RNS::log("Hello T-Beam from PlatformIO!");
program_setup();
}
void loop() {
}
#else
int main(int argc, char **argv) {
RNS::log("Hello Native from PlatformIO!");
UNITY_BEGIN();
RUN_TEST(program_setup);
UNITY_END();
}
#endif