Tutorial¶
A brief introduction and tutorial how to get you started with the neuropil cybersecurity mesh.
Sending messages¶
This example explains how to setup a simple neuropil node that will send periodic messages to a destination.
Note
The source code of this example is available in examples/neuropil_sender.c
Note
You can modify this example program and (re)build it with:
scons bin/neuropil_sender
.
Note
You can run this example like this:
LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_sender
It will create and print events to a log file in the current directory.
First and foremost, we have to include header file which defines the API for the neuropil cybersecurity mesh.
#include "neuropil.h"
To initialize the neuropil cybersecurity mesh, we prepare a
np_settings
by populating it with the default settings usingnp_default_settings()
, and create a new application context withnp_new_context()
.
struct np_settings cfg;
np_default_settings(&cfg);
strncpy(cfg.log_file, "sender.log", 255);
np_context *ac = np_new_context(&cfg);
Next, we allocate a network address and port tuple to listen on for incoming connections using
np_listen()
.
assert(np_ok == np_listen(ac, "udp4", "localhost", 1234, NULL));
To join a neuropil network, we have to connect with our initial bootstrap node using
np_join()
. Other nodes in the network will be discovered automatically, but we need explicitly specify the network address and port tuple for our initial contact.
assert(np_ok == np_join(ac, "*:udp4:localhost:2345"));
We should also set an authorization callback via
np_set_authorize_cb()
to control access to this node. More on this later.
assert(np_ok == np_set_authorize_cb(ac, authorize));
We also need to convert our human readable subject string into an :c:type:np_subject instance. This can be done via
np_generate_subject()
.
np_subject subject_id = {0};
assert(np_ok == np_generate_subject(&subject_id, "mysubject", 9));
Now to our application logic. We will repeatedly run the neuropil event loop with
np_run()
, and then send our user defined message with thesubject_id
usingnp_send()
. If anything goes wrong we return the error code (annp_return
.)Effectively, this means that our node will process protocol requests continuously (for as long as there is no error situation) and send a message every five seconds periodically.
enum np_return status;
uint64_t _i = 0;
do {
status = np_run(ac, 0);
char message[100];
// printf("Enter message (max 100 chars): ");
// fgets(message, 200, stdin);
snprintf(message, 100, "msg %" PRIu64, _i++);
sleep(0.01);
// Remove trailing newline
if ((strlen(message) > 0) && (message[strlen(message) - 1] == '\n'))
message[strlen(message) - 1] = '\0';
size_t message_len = strlen(message);
np_send(ac, subject_id, message, message_len);
printf("Sent: %s\n", message);
} while (np_ok == status);
return status;
All that is left is to implement our authorization callback, a function of type
np_aaa_callback
. The one defined is eternally lenient, and authorizes every peer to receive our messages. To ensure that our message is not read by strangers, it should really returnfalse
fornp_token
of unknown identities.
bool authorize(np_context *ac, struct np_token *id) {
// TODO: Make sure that id->public_key is the intended recipient!
return true;
}
Receiving messages¶
This example explains how to setup a simple neuropil node that will receive messages on a subject.
Note
The source code of this example is available in examples/neuropil_receiver.c
Note
You can modify this example program and (re)build it with:
scons bin/neuropil_receiver
.
Note
You can run this example like this:
LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_receiver
.
It will create and print events to a log file in the current directory.
The simple receiver example looks very much like the sender we just discussed. Instead of sending messages it registers a receive callback for messages on the subject
subject_id
withnp_add_receive_cb()
.
assert(np_ok == np_add_receive_cb(ac, subject_id, receive));
In its in main loop it simply runs the neurpil event loop repeatedly, and handles any error situations by halting.
enum np_return status;
do
status = np_run(ac, 5.0);
while (np_ok == status);
return status;
The receive callback interprets the message payload as a string, and prints it to standard output.
bool receive(np_context *ac, struct np_message *message) {
printf("Received: %.*s\n", (int)message->data_length, message->data);
return true;
}
Using identities¶
This example shows you how you can store/load digital identities to/from a keystore.
Note
The source code of this example is available in examples/neuropil_identity.c
Note
You can modify this example program and (re)build it with
scons bin/neuropil_identity
Note
You can run this example like this:
LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_identity
.
It will create and print events to a log file in the current directory.
let’s create one keystore, for authorizations and for authentication token
static np_id authnz_keystore_id = {0};
create the hash value for a very looong password a better way: use KDF functions or shared secrets as passphrases to encrypt keys and token !
np_id passphrase_id = {0};
const char *passphrase = "ellenlangepassphrase";
np_get_id(&passphrase_id, passphrase, strnlen(passphrase, 32));
create a secret key file in the current directory, filename is “.npid”
..code-block::c
np_identity_create_secretkey(context, “./”, passphrase_id);
load the secret key from the file into our identity
..code-block::c
np_id my_id = {0}; np_identity_load_secretkey(context, “./”, &my_id, passphrase_id, &my_identity);
Next modify your identity as desired
..code-block::c
// e.g. push in a mail address memset(my_identity.subject, 0, 255); strncpy(my_identity.subject, “me@example.com”, 15); // e.g. set a realm (any kind of grouping you would like to use) np_get_id(&my_identity.realm, “example.com”, 11); // e.g. set expiry time to a higher value my_identity.expires_at = np_time_now() + 86400 * 2; // e.g. set the first-usage time to one hour in the future my_identity.not_before = np_time_now() + 3600;
make sure that the signature and thus the fingerprint is up-to-date
..code-block::c
np_id identity_fp = {0}; np_sign_identity(context, &my_identity, true); np_token_fingerprint(context, my_identity, false, &identity_fp);
store the secret key again with the correct identifier, our fingerprint
..code-block::c
np_identity_save_secretkey(context, “./”, passphrase_id, &my_identity);
save our identity to a file in a specific directory, filename is generated based on the fingerprint of teh token
..code-block::c
np_identity_save_token(context, “./”, passphrase_id, &my_identity);
from now on we can load the secret key and we know which token file is ours
..code-block::c
np_id check_identity_fp = {0}; np_identity_load_secretkey(context, “./”, &check_identity_fp, passphrase_id, &my_identity);
re-create the token filename, unless you know the filename that you would like to load create the filename
..code-block::c
char filename[79]; char identity_fp_str[65] = {0}; np_id_str(identity_fp_str, check_identity_fp); snprintf(filename, 75, “np:npt:%s”, identity_fp_str);
load the identity token from the file into the context
..code-block::c
np_identity_load_token(context, “./”, check_identity_fp, passphrase_id, &my_identity);
and now continue with the usual things to send / receive data
The simple receiver example looks very much like the sender we just …
..code-block::c
assert(np_ok == np_listen(context, “udp4”, “localhost”, 3456, NULL));
We need to give the keystore m a unique identifier, so we only use hash value of a random string. With this initializer we init the keystore and protect it with the same passphrase. afterwards, we can load all identities stored in the keystore, so that they are available in memory make sure to handle the return code of np_keystore_load_identites, it will return np_invalid_operation on an empty file.
np_get_id(&authnz_keystore_id, "np:authnz:keystore", 18);
assert(np_ok ==
np_keystore_init(context, authnz_keystore_id, "./", passphrase_id));
np_keystore_load_identities(context, authnz_keystore_id);
assert(np_ok == np_set_authenticate_cb(context, authenticate));
assert(np_ok == np_set_authorize_cb(context, authorize));
assert(np_ok == np_run(context, 0.0));
assert(np_ok == np_join(context, "*:udp4:localhost:2345"));
np_subject subject_id = {0};
assert(np_ok == np_generate_subject(&subject_id, "mysubject", 9));
The authentication callback uses a token and stores it in it’s keystore
if (np_ok == np_keystore_check_identity(ac, authnz_keystore_id, id)) {
return true;
} else {
np_keystore_store_identity(ac, authnz_keystore_id, id);
}
The authorize callback uses a token and stores it in it’s keystore
if (np_ok == np_keystore_check_identity(ac, authnz_keystore_id, id)) {
return true;
} else {
np_keystore_store_identity(ac, authnz_keystore_id, id);
}
return false;
Using identities (load balancing)¶
This example shows you how you can use digital identities to achieve load balancing between two nodes.
Note
The source code of this example is available in examples/neuropil_receiver_lb.c
Note
You can modify this example program and (re)build it with
scons bin/neuropil_receiver_lb
Note
You can run this example like this:
LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_receiver_lb
.
It will create and print events to a log file in the current directory.
first, let’s define a callback function that will be called each time a message is received by the node that you are currently starting
bool receive_this_is_a_test(np_context *context, struct np_message *msg) {
for this message exchange the message is send as a text element (if you used np_send_text) otherwise inspect the properties and payload np_tree_t structures …
char text[msg->data_length + 1];
memcpy(text, msg->data, msg->data_length);
return true to indicate successfull handling of the message. if you return false the message may get delivered a second time
return true;
}
second, let’s define a callback function that will be called each time a message intent is received by the node that you are currently running you can check and investigate the token and authorize the message exchange
bool authorize(np_context *ac, struct np_token *id) {
as an example you may check the the fingerprint of the token issuer. This is fine as the token is always authentic (integrity check already done), and you have authenticated this issuer fingerprint in your authentication callback before.
You could choose arbitrary attributes values in the token attributes as well to authorize data exchange.
char sender[65];
sender[64] = '\0';
char *ctx = (char *)np_get_userdata(ac);
sodium_bin2hex(sender, 65, id->issuer, 32U);
// fprintf(stdout, "AUTHZ(%s): subj: %s ## pk: %s\n", ctx, id->subject,
// sender); fflush (stdout);
return true to indicate the successful handling of the message. if you return false the message may get delivered a second time
return true;
}
in your main program, initialize the two neuropil nodes, but this time use the a single identity on top of both
struct np_settings *settings_1 = np_default_settings(NULL);
struct np_settings *settings_2 = np_default_settings(NULL);
settings_1->n_threads = 4;
settings_2->n_threads = 4;
snprintf(settings_1->log_file,
255,
"%s%s_%s.log",
logpath,
"/neuropil_receiver_lb_1",
port);
snprintf(settings_2->log_file,
255,
"%s%s_%s.log",
logpath,
"/neuropil_receiver_lb_2",
port);
fprintf(stdout, "logpath: %s\n", settings_1->log_file);
fprintf(stdout, "logpath: %s\n", settings_2->log_file);
np_context *context_1 = np_new_context(settings_1);
np_set_userdata(context_1, "context 1");
np_context *context_2 = np_new_context(settings_2);
np_set_userdata(context_2, "context 2");
create a new identity and use it for both nodes
struct np_token my_id =
np_new_identity(context_1, _np_time_now(NULL) + 3600.0, NULL);
strncpy(my_id.subject, "urn:np:id:this.is.a.test.identity", 255);
np_use_identity(context_1, my_id);
np_use_identity(context_2, my_id);
Make sure that you have implemented and registered the appropiate aaa callback functions to control with which nodes you exchange messages. By default everybody is allowed to interact with your node
as in the simple example: set the authorization callbacks and listen on a network interface
assert(np_ok == np_set_authorize_cb(context_1, authorize));
assert(np_ok == np_set_authorize_cb(context_2, authorize));
start up the job queue with and check the error code if the event loop can be processed.
if (np_ok != np_run(context_1, 0)) {
exit(EXIT_FAILURE);
}
if (np_ok != np_run(context_2, 0)) {
exit(EXIT_FAILURE);
}
join a network of nodes and wait until both nodes have joined the network
enum np_return status = np_ok;
if (NULL != j_key) {
status |= np_join(context_1, j_key);
status |= np_join(context_2, j_key);
}
NP_CHECK_ERROR(status);
while (np_has_joined(context_1) && np_has_joined(context_2) &&
status == np_ok) {
status |= np_run(context_1, 0.04);
status |= np_run(context_2, 0.04);
}
NP_CHECK_ERROR(status);
register the listener function to receive data from the sender for both nodes
status |= np_add_receive_cb(context_1,
"urn:np:subj:this.is.a.test",
receive_this_is_a_test);
struct np_mx_properties mx_1 =
np_get_mx_properties(context_1, "urn:np:subj:this.is.a.test");
mx_1.max_parallel = 7; // only receive seven messages in parallel
status |= np_set_mx_properties(context_1, "urn:np:subj:this.is.a.test", mx_1);
NP_CHECK_ERROR(status);
status |= np_add_receive_cb(context_2,
"urn:np:subj:this.is.a.test",
receive_this_is_a_test);
struct np_mx_properties mx_2 =
np_get_mx_properties(context_2, "urn:np:subj:this.is.a.test");
mx_2.max_parallel = 7;
status |= np_set_mx_properties(context_2, "urn:np:subj:this.is.a.test", mx_2);
NP_CHECK_ERROR(status);
the loopback function will be triggered each time a message is received make sure that you’ve understood how to alter the message exchange to change receiving of message from the default values
loop (almost) forever, and watch the messages drop in on the different nodes. et voila, identity based loadbalancing …
while (1) {
status |= np_run(context_1, 0.04);
NP_CHECK_ERROR(status);
status |= np_run(context_2, 0.04);
NP_CHECK_ERROR(status);
}
Bootstrapping a network¶
This example explains how to bootstrap a neuropil network.
Note
The source code of this example is available in examples/neuropil_controller.c
Note
You can modify this example program and (re)build it with:
scons bin/neuropil_controller
.
Note
You can run this example like this:
LD_LIBRARY_PATH=build/lib:$LD_LIBRARY_PATH bin/neuropil_controller
.
It will create and print events to a log file in the current directory.
In order to bootstrap a neuropil network we need an initial peer that will invite our node into the mesh. We call Nodes whose only function is providing transit services to the network “infrastructure nodes.” A simple infrastructure node could look very much like the simple receiver example above, and could serve as the initial contact for other neuropil nodes (such as our sender and receiver examples.)
This node will not receive any messages, and will not set an authorization callback. More importantly, our bootstrap node will not attempt to join a network via
np_join()
. Since it will be the first node in the network there is no network it could join. Still, it will just listen on a known network address/port tuple.
assert(np_ok == np_listen(ac, "udp4", "localhost", 2345, NULL));
Other nodes can now join the network by calling :c:func:`np_join`
with the bootstrap node’s address. Using the absolute address as
returned by c:func:`np_get_address` will guarantee that nodes will
connect to the intended node only, and not say an impersonator.
char address[256];
assert(np_ok == np_get_address(ac, address, sizeof(address)));
printf("Bootstrap address: %s\n", address);
Alternatively, you can attempt to join any node that listens on a
specific network address/port tuple by joining a wildcard address,
which in this case would be ``"*:udp4:localhost:2345"``.
In the neuropil cybersecurity mesh, nodes need no authorization to join
a network, but they do need to authenticate themselves. This node
sets an authentication callback via :c:func:`np_set_authenticate_cb`
that will be called each time a node attempts to join this node.
The authentication callback gets passed the identity of the node that requesting to join, and can reject the request by returning
false
. For convenience, we merely log the first seven bytes of the public key of each node that joins the network via this node in its authentication callback.
bool authenticate(np_context *ac, struct np_token *id) {
// TODO: Make sure that id->public_key is an authenticated peer!
printf("Joined: %02X%02X%02X%02X%02X%02X%02X...\n",
id->public_key[0],
id->public_key[1],
id->public_key[2],
id->public_key[3],
id->public_key[4],
id->public_key[5],
id->public_key[6]);
return true;
}