Ockam est une suite de bibliothèques de programmation, d'outils de ligne de commande et de services cloud gérés pour orchestrer le chiffrement de bout en bout, l'authentification mutuelle, la gestion des clés, la gestion des informations d'identification et l'application des politiques d'autorisation, le tout à grande échelle. Le bout en bout d'Ockam
L'une des principales caractéristiques qui rendent cela possible est
Dans cet article de blog, nous allons explorer l'API Ockam Rust et voir comment le routage fonctionne dans Ockam. Nous travaillerons avec le code Rust et examinerons quelques exemples de code qui illustrent le cas simple et des cas d'utilisation plus avancés.
Avant de commencer, discutons rapidement des pièges liés à l'utilisation d'approches existantes pour sécuriser les applications. La sécurité n'est pas une chose à laquelle la plupart d'entre nous pensons lorsque nous construisons des systèmes et que nous nous concentrons sur le fonctionnement et l'expédition des choses.
Les implémentations de communication sécurisées traditionnelles sont généralement étroitement couplées aux protocoles de transport de manière à ce que toute leur sécurité soit limitée à la longueur et à la durée d'une connexion de transport sous-jacente.
Par exemple, la plupart des implémentations TLS sont étroitement couplées à la connexion TCP sous-jacente. Si les données et les requêtes de votre application transitent par deux sauts de connexion TCP (TCP → TCP), alors toutes les garanties TLS sont rompues au niveau du pont entre les deux réseaux. Ce pont, cette passerelle ou cet équilibreur de charge devient alors un point de faiblesse pour les données applicatives.
Les protocoles de communication sécurisés traditionnels sont également incapables de protéger les données de votre application si elles transitent par plusieurs protocoles de transport différents. Ils ne peuvent pas garantir l'authenticité ou l'intégrité des données si le chemin de communication de votre application est UDP → TCP ou BLE → TCP.
En d'autres termes, en utilisant des implémentations de communication sécurisée traditionnelles, vous risquez d'ouvrir la porte à une perte de confiance dans les données sur lesquelles vos applications travaillent. Voici quelques aspects de vos applications qui peuvent être à risque :
Qui l'a envoyé à mon application ?
S'agit-il réellement des données qu'ils ont envoyées à mon application ?
Authentification manquante et intégrité des données.
Dans cet article de blog, nous allons créer deux exemples de nœuds Ockam communiquant entre eux à l'aide du routage Ockam et des transports Ockam. Nous allons utiliser l' API Rust pour créer ces nœuds Ockam et mettre en place l'orchestration de routage Ockam. Le routage et les transports Ockam permettent à d'autres protocoles Ockam de fournir des garanties de bout en bout telles que la confiance, la sécurité, la confidentialité, la livraison fiable et la commande au niveau de la couche application.
Routage Ockam : est un protocole simple et léger basé sur les messages qui permet d'échanger des messages de manière bidirectionnelle sur une grande variété de topologies de communication : TCP -> TCP ou TCP -> TCP -> TCP ou BLE -> UDP -> TCP ou BLE -> TCP -> TCP ou TCP -> Kafka -> TCP ou toute autre topologie que vous pouvez imaginer.
Transports Ockam : adaptez le routage Ockam aux différents protocoles de transport.
Un nœud Ockam est une application en cours d'exécution qui peut communiquer avec d'autres applications à l'aide de divers protocoles Ockam tels que le routage, les relais et les portails, les canaux sécurisés, etc.
Un nœud Ockam peut être défini comme tout processus indépendant qui fournit une API prenant en charge le protocole de routage Ockam. Nous pouvons créer des nœuds Ockam en utilisant leockam
) ou en utilisant diverses bibliothèques de programmation Ockam comme nos bibliothèques Rust et Elixir. Nous utiliserons l'API Rust dans cet article de blog.
Lorsqu'un travailleur est démarré sur un
Pour notre premier exemple, nous allons créer un nœud Ockam simple qui enverra un message sur quelques sauts (dans le même nœud) à un travailleur (dans le même nœud) qui renvoie simplement le message. Aucun transport TCP n'est impliqué et tous les messages sont transmis dans les deux sens à l'intérieur du même nœud. Cela nous donnera une idée de la construction des travailleurs et du routage à un niveau de base.
Nous devrons créer un fichier source Rust avec un programme main()
et deux autres fichiers source Rust avec deux workers : Hopper
et Echoer
. Nous pouvons ensuite envoyer un message de chaîne et voir si nous pouvons le renvoyer en écho.
Avant de commencer, considérons le routage. Lorsque nous envoyons un message à l'intérieur d'un nœud, il contient 2 champs de métadonnées, onward_route
et return_route
, où une route
est simplement une liste d' adresses
. Chaque travailleur obtient une address
dans un nœud.
Donc, si nous voulions envoyer un message de l'adresse app
à l'adresse echoer
, avec 3 sauts au milieu, nous pouvons construire une route comme celle-ci.
┌───────────────────────┐ │ Node 1 │ ├───────────────────────┤ │ ┌────────────────┐ │ │ │ Address: │ │ │ │ 'app' │ │ │ └─┬────────────▲─┘ │ │ ┌─▼────────────┴─┐ │ │ │ Address: │ │ │ │ 'hopper1..3' │x3 │ │ └─┬────────────▲─┘ │ │ ┌─▼────────────┴─┐ │ │ │ Address: │ │ │ │ 'echoer' │ │ │ └────────────────┘ │ └───────────────────────┘
Voici le code Rust pour construire cette route.
/// Send a message to the echoer worker via the "hopper1", "hopper2", and "hopper3" workers. let route = route!["hopper1", "hopper2", "hopper3", "echoer"];
Ajoutons du code source pour que cela se produise ensuite. La première chose que nous allons faire est d'ajouter une autre dépendance à ce projet hello_ockam
vide. La caisse colored
nous donnera une sortie de console colorisée, ce qui rendra la sortie de nos exemples beaucoup plus facile à lire et à comprendre.
cargo add colored
Ensuite, nous ajoutons le worker echoer
(dans notre projet hello_ockam
) en créant un nouveau fichier /src/echoer.rs
et en copiant/collant le code suivant dedans.
use colored::Colorize; use ockam::{Context, Result, Routed, Worker}; pub struct Echoer; /// When a worker is started on a node, it is given one or more addresses. The node /// maintains a mailbox for each address and whenever a message arrives for a specific /// address it delivers that message to the corresponding registered worker. /// /// Workers can handle messages from other workers running on the same or a different /// node. In response to a message, an worker can: make local decisions, change its /// internal state, create more workers, or send more messages to other workers running on /// the same or a different node. #[ockam::worker] impl Worker for Echoer { type Context = Context; type Message = String; async fn handle_message(&mut self, ctx: &mut Context, msg: Routed<String>) -> Result<()> { // Echo the message body back on its return_route. let addr_str = ctx.address().to_string(); let msg_str = msg.as_body().to_string(); let new_msg_str = format!("👈 echo back: {}", msg); // Formatting stdout output. let lines = [ format!("📣 'echoer' worker → Address: {}", addr_str.bright_yellow()), format!(" Received: '{}'", msg_str.green()), format!(" Sent: '{}'", new_msg_str.cyan()), ]; lines .iter() .for_each(|line| println!("{}", line.white().on_black())); ctx.send(msg.return_route(), new_msg_str).await } }
Ensuite, nous ajoutons le hopper
worker (dans notre projet hello_ockam
) en créant un nouveau fichier /src/hopper.rs
et en y copiant/collant le code suivant.
Notez comment ce travailleur manipule les champs onward_route
& return_route
du message pour l'envoyer au saut suivant. Nous verrons cela dans la sortie de la console lorsque nous exécuterons bientôt ce code.
use colored::Colorize; use ockam::{Any, Context, Result, Routed, Worker}; pub struct Hopper; #[ockam::worker] impl Worker for Hopper { type Context = Context; type Message = Any; /// This handle function takes any incoming message and forwards. it to the next hop /// in it's onward route. async fn handle_message(&mut self, ctx: &mut Context, msg: Routed<Any>) -> Result<()> { // Cast the msg to a Routed<String> let msg: Routed<String> = msg.cast()?; let msg_str = msg.to_string().white().on_bright_black(); let addr_str = ctx.address().to_string().white().on_bright_black(); // Some type conversion. let mut message = msg.into_local_message(); let transport_message = message.transport_mut(); // Remove my address from the onward_route. let removed_address = transport_message.onward_route.step()?; let removed_addr_str = removed_address .to_string() .white() .on_bright_black() .strikethrough(); // Formatting stdout output. let lines = [ format!("🐇 'hopper' worker → Addr: '{}'", addr_str), format!(" Received: '{}'", msg_str), format!(" onward_route -> remove: '{}'", removed_addr_str), format!(" return_route -> prepend: '{}'", addr_str), ]; lines .iter() .for_each(|line| println!("{}", line.black().on_yellow())); // Insert my address at the beginning return_route. transport_message .return_route .modify() .prepend(ctx.address()); // Send the message on its onward_route. ctx.forward(message).await } }
Et enfin, ajoutons un main()
à notre projet hello_ockam
. Ce sera le point d'entrée de notre exemple.
Créez un fichier vide /examples/03-routing-many.hops.rs
(notez qu'il se trouve dans le dossier examples/
et non dans le dossier src/
comme les travailleurs ci-dessus).
use colored::Colorize; use hello_ockam::{Echoer, Hopper}; use ockam::{node, route, Context, Result}; #[rustfmt::skip] const HELP_TEXT: &str =r#" ┌───────────────────────┐ │ Node 1 │ ├───────────────────────┤ │ ┌────────────────┐ │ │ │ Address: │ │ │ │ 'app' │ │ │ └─┬────────────▲─┘ │ │ ┌─▼────────────┴─┐ │ │ │ Address: │ │ │ │ 'hopper1..3' │x3 │ │ └─┬────────────▲─┘ │ │ ┌─▼────────────┴─┐ │ │ │ Address: │ │ │ │ 'echoer' │ │ │ └────────────────┘ │ └───────────────────────┘ "#; /// This node routes a message through many hops. #[ockam::node] async fn main(ctx: Context) -> Result<()> { println!("{}", HELP_TEXT.green()); print_title(vec![ "Run a node w/ 'app', 'echoer' and 'hopper1', 'hopper2', 'hopper3' workers", "then send a message over 3 hops", "finally stop the node", ]); // Create a node with default implementations. let mut node = node(ctx); // Start an Echoer worker at address "echoer". node.start_worker("echoer", Echoer).await?; // Start 3 hop workers at addresses "hopper1", "hopper2" and "hopper3". node.start_worker("hopper1", Hopper).await?; node.start_worker("hopper2", Hopper).await?; node.start_worker("hopper3", Hopper).await?; // Send a message to the echoer worker via the "hopper1", "hopper2", and "hopper3" workers. let route = route!["hopper1", "hopper2", "hopper3", "echoer"]; let route_msg = format!("{:?}", route); let msg = "Hello Ockam!"; node.send(route, msg.to_string()).await?; // Wait to receive a reply and print it. let reply = node.receive::<String>().await?; // Formatting stdout output. let lines = [ "🏃 Node 1 →".to_string(), format!(" sending: {}", msg.green()), format!(" over route: {}", route_msg.blue()), format!(" and receiving: '{}'", reply.purple()), // Should print "👈 echo back: Hello Ockam!" format!(" then {}", "stopping".bold().red()), ]; lines .iter() .for_each(|line| println!("{}", line.black().on_white())); // Stop all workers, stop the node, cleanup and return. node.stop().await } fn print_title(title: Vec<&str>) { let line = format!("🚀 {}", title.join("\n → ").white()); println!("{}", line.black().on_bright_black()) }
Il est maintenant temps d'exécuter notre programme pour voir ce qu'il fait ! 🎉
Dans votre application de terminal, exécutez la commande suivante. Notez que OCKAM_LOG=none
est utilisé pour désactiver la sortie de journalisation de la bibliothèque Ockam. Ceci est fait pour rendre la sortie de l'exemple plus facile à lire.
OCKAM_LOG=none cargo run --example 03-routing-many-hops
Et vous devriez voir quelque chose comme ce qui suit. Notre exemple de programme crée plusieurs hop workers (trois hopper
workers) entre l' app
et l' echoer
et achemine notre message à travers eux 🚀.
Dans cet exemple, nous présenterons
Un transport Ockam est un plugin pour le routage Ockam. Il déplace les messages de routage Ockam en utilisant un protocole de transport spécifique comme TCP, UDP, WebSockets, Bluetooth, etc.
Nous aurons trois nœuds :
node_initiator
: le premier nœud initie l'envoi du message via TCP au nœud central (port 3000
).
node_middle
: Ensuite, le nœud du milieu transmet simplement ce message au dernier nœud via TCP à nouveau (port 4000
cette fois).
node_responder
: Et enfin, le nœud répondeur reçoit le message et renvoie une réponse au nœud initiateur.
Le schéma suivant illustre ce que nous allons construire ensuite. Dans cet exemple, tous ces nœuds se trouvent sur la même machine, mais ils peuvent facilement n'être que des nœuds sur différentes machines.
┌──────────────────────┐ │node_initiator │ ├──────────────────────┤ │ ┌──────────────────┐ │ │ │Address: │ │ ┌───────────────────────────┐ │ │'app' │ │ │node_middle │ │ └──┬────────────▲──┘ │ ├───────────────────────────┤ │ ┌──▼────────────┴──┐ │ │ ┌──────────────────┐ │ │ │TCP transport └─┼─────┼─►TCP transport │ │ │ │connect to 3000 ◄─┼─────┼─┐listening on 3000 │ │ │ └──────────────────┘ │ │ └──┬────────────▲──┘ │ └──────────────────────┘ │ ┌──▼────────────┴───────┐ │ │ │Address: │ │ ┌──────────────────────┐ │ │'forward_to_responder' │ │ │node_responder │ │ └──┬────────────▲───────┘ │ ├──────────────────────┤ │ ┌──▼────────────┴──┐ │ │ ┌──────────────────┐ │ │ │TCP transport └──────┼───┼─►TCP transport │ │ │ │connect to 4000 ◄──────┼───┼─┐listening on 4000 │ │ │ └──────────────────┘ │ │ └──┬────────────▲──┘ │ └───────────────────────────┘ │ ┌──▼────────────┴──┐ │ │ │Address: │ │ │ │'echoer' │ │ │ └──────────────────┘ │ └──────────────────────┘
Commençons par créer un nouveau fichier /examples/04-routing-over-two-transport-hops.rs
(dans le dossier /examples/
et non /src/
). Ensuite, copiez/collez le code suivant dans ce fichier.
use colored::Colorize; use hello_ockam::{Echoer, Forwarder}; use ockam::{ node, route, AsyncTryClone, Context, Result, TcpConnectionOptions, TcpListenerOptions, TcpTransportExtension, }; #[rustfmt::skip] const HELP_TEXT: &str =r#" ┌──────────────────────┐ │node_initiator │ ├──────────────────────┤ │ ┌──────────────────┐ │ │ │Address: │ │ ┌───────────────────────────┐ │ │'app' │ │ │node_middle │ │ └──┬────────────▲──┘ │ ├───────────────────────────┤ │ ┌──▼────────────┴──┐ │ │ ┌──────────────────┐ │ │ │TCP transport └─┼─────┼─►TCP transport │ │ │ │connect to 3000 ◄─┼─────┼─┐listening on 3000 │ │ │ └──────────────────┘ │ │ └──┬────────────▲──┘ │ └──────────────────────┘ │ ┌──▼────────────┴───────┐ │ │ │Address: │ │ ┌──────────────────────┐ │ │'forward_to_responder' │ │ │node_responder │ │ └──┬────────────▲───────┘ │ ├──────────────────────┤ │ ┌──▼────────────┴──┐ │ │ ┌──────────────────┐ │ │ │TCP transport └──────┼───┼─►TCP transport │ │ │ │connect to 4000 ◄──────┼───┼─┐listening on 4000 │ │ │ └──────────────────┘ │ │ └──┬────────────▲──┘ │ └───────────────────────────┘ │ ┌──▼────────────┴──┐ │ │ │Address: │ │ │ │'echoer' │ │ │ └──────────────────┘ │ └──────────────────────┘ "#; #[ockam::node] async fn main(ctx: Context) -> Result<()> { println!("{}", HELP_TEXT.green()); let ctx_clone = ctx.async_try_clone().await?; let ctx_clone_2 = ctx.async_try_clone().await?; let mut node_responder = create_responder_node(ctx).await.unwrap(); let mut node_middle = create_middle_node(ctx_clone).await.unwrap(); create_initiator_node(ctx_clone_2).await.unwrap(); node_responder.stop().await.ok(); node_middle.stop().await.ok(); println!( "{}", "App finished, stopping node_responder & node_middle".red() ); Ok(()) } fn print_title(title: Vec<&str>) { let line = format!("🚀 {}", title.join("\n → ").white()); println!("{}", line.black().on_bright_black()) }
Ce code ne compilera pas réellement, car il manque 3 fonctions dans ce fichier source. Nous ajoutons simplement ce fichier en premier afin de mettre en scène le reste du code que nous écrirons ensuite.
Cette fonction main()
crée les trois nœuds comme nous le voyons dans le diagramme ci-dessus, et les arrête également une fois l'exemple terminé.
Écrivons donc d'abord la fonction qui crée le nœud initiateur. Copiez ce qui suit dans le fichier source que nous avons créé précédemment ( /examples/04-routing-over-two-transport-hops.rs
) et collez-le sous le code existant :
/// This node routes a message, to a worker on a different node, over two TCP transport /// hops. async fn create_initiator_node(ctx: Context) -> Result<()> { print_title(vec![ "Create node_initiator that routes a message, over 2 TCP transport hops, to 'echoer' worker on node_responder", "stop", ]); // Create a node with default implementations. let mut node = node(ctx); // Initialize the TCP transport. let tcp_transport = node.create_tcp_transport().await?; // Create a TCP connection to the middle node. let connection_to_middle_node = tcp_transport .connect("localhost:3000", TcpConnectionOptions::new()) .await?; // Send a message to the "echoer" worker, on a different node, over two TCP hops. Wait // to receive a reply and print it. let route = route![connection_to_middle_node, "forward_to_responder", "echoer"]; let route_str = format!("{:?}", route); let msg = "Hello Ockam!"; let reply = node .send_and_receive::<String>(route, msg.to_string()) .await?; // Formatting stdout output. let lines = [ "🏃 node_initiator →".to_string(), format!(" sending: {}", msg.green()), format!(" over route: '{}'", route_str.blue()), format!(" and received: '{}'", reply.purple()), // Should print "👈 echo back: Hello Ockam!" format!(" then {}", "stopping".bold().red()), ]; lines .iter() .for_each(|line| println!("{}", line.black().on_white())); // Stop all workers, stop the node, cleanup and return. node.stop().await }
Ce nœud (initiateur) enverra un message au répondeur en utilisant la route suivante.
let route = route![connection_to_middle_node, "forward_to_responder", "echoer"];
Créons ensuite le nœud du milieu, qui exécutera le worker Forwarder
sur cette adresse : forward_to_responder
.
Copiez et collez ce qui suit dans le fichier source que nous avons créé ci-dessus ( /examples/04-routing-over-two-transport-hops.rs
).
Ce nœud intermédiaire transfère simplement tout ce qui entre dans son écouteur TCP (sur 3000
) vers le port 4000
.
Ce nœud a un agent Forwarder
sur l'adresse forward_to_responder
, c'est ainsi que l'initiateur peut atteindre l'adresse spécifiée dans sa route au début de cet exemple.
/// - Starts a TCP listener at 127.0.0.1:3000. /// - This node creates a TCP connection to a node at 127.0.0.1:4000. /// - Starts a forwarder worker to forward messages to 127.0.0.1:4000. /// - Then runs forever waiting to route messages. async fn create_middle_node(ctx: Context) -> Result<ockam::Node> { print_title(vec![ "Create node_middle that listens on 3000 and forwards to 4000", "wait for messages until stopped", ]); // Create a node with default implementations. let node = node(ctx); // Initialize the TCP transport. let tcp_transport = node.create_tcp_transport().await?; // Create a TCP connection to the responder node. let connection_to_responder = tcp_transport .connect("127.0.0.1:4000", TcpConnectionOptions::new()) .await?; // Create a Forwarder worker. node.start_worker( "forward_to_responder", Forwarder { address: connection_to_responder.into(), }, ) .await?; // Create a TCP listener and wait for incoming connections. let listener = tcp_transport .listen("127.0.0.1:3000", TcpListenerOptions::new()) .await?; // Allow access to the Forwarder via TCP connections from the TCP listener. node.flow_controls() .add_consumer("forward_to_responder", listener.flow_control_id()); // Don't call node.stop() here so this node runs forever. Ok(node) }
Enfin, nous allons créer le nœud du répondeur. Ce nœud exécutera l' echoer
du travailleur qui renvoie en fait le message à l'initiateur. Copiez et collez ce qui suit dans le fichier source ci-dessus ( /examples/04-routing-over-two-transport-hops.rs
).
Ce nœud a un agent Echoer
sur address echoer
, c'est ainsi que l'initiateur peut atteindre l'adresse spécifiée dans sa route au début de cet exemple.
/// This node starts a TCP listener and an echoer worker. It then runs forever waiting for /// messages. async fn create_responder_node(ctx: Context) -> Result<ockam::Node> { print_title(vec![ "Create node_responder that runs tcp listener on 4000 and 'echoer' worker", "wait for messages until stopped", ]); // Create a node with default implementations. let node = node(ctx); // Initialize the TCP transport. let tcp_transport = node.create_tcp_transport().await?; // Create an echoer worker. node.start_worker("echoer", Echoer).await?; // Create a TCP listener and wait for incoming connections. let listener = tcp_transport .listen("127.0.0.1:4000", TcpListenerOptions::new()) .await?; // Allow access to the Echoer via TCP connections from the TCP listener. node.flow_controls() .add_consumer("echoer", listener.flow_control_id()); Ok(node) }
Exécutons cet exemple pour voir ce qu'il fait 🎉.
Dans votre application de terminal, exécutez la commande suivante. Notez que OCKAM_LOG=none
est utilisé pour désactiver la sortie de journalisation de la bibliothèque Ockam. Ceci est fait pour rendre la sortie de l'exemple plus facile à lire.
cargo run --example 04-routing-over-two-transport-hops
Cela devrait produire une sortie similaire à la suivante. Notre exemple de programme crée une route qui traverse plusieurs nœuds et transports TCP de l' app
à l' echoer
et achemine notre message à travers eux 🚀.
Le routage et les transports Ockam sont extrêmement puissants et flexibles. Ils sont l'une des fonctionnalités clés qui permettent d'implémenter Ockam Secure Channels. En superposant Ockam Secure Channels et d'autres protocoles sur le routage Ockam, nous pouvons fournir des garanties de bout en bout sur des topologies de transport arbitraires qui couvrent de nombreux réseaux et clouds.
Dans un futur article de blog, nous couvrirons les canaux sécurisés Ockam et comment ils peuvent être utilisés pour fournir des garanties de bout en bout sur des topologies de transport arbitraires. Alors restez à l'écoute!
En attendant, voici quelques bons points de départ pour en savoir plus sur Ockam :
ockam
dépôt.
Également publié ici.