O Ockam é um conjunto de bibliotecas de programação, ferramentas de linha de comando e serviços de nuvem gerenciados para orquestrar criptografia de ponta a ponta, autenticação mútua, gerenciamento de chaves, gerenciamento de credenciais e aplicação de políticas de autorização - tudo em grande escala. Ockam de ponta a ponta
Uma das principais características que tornam isso possível é
Nesta postagem do blog, exploraremos a API Ockam Rust e veremos como o roteamento funciona no Ockam. Trabalharemos com código Rust e veremos alguns exemplos de código que demonstram o caso simples e casos de uso mais avançados.
Antes de começarmos, vamos discutir rapidamente as armadilhas do uso de abordagens existentes para proteger aplicativos. A segurança não é algo em que a maioria de nós pensa quando estamos construindo sistemas e estamos focados em fazer as coisas funcionarem e serem enviadas.
As implementações tradicionais de comunicação segura são tipicamente fortemente acopladas aos protocolos de transporte de forma que toda a sua segurança seja limitada ao comprimento e duração de uma conexão de transporte subjacente.
Por exemplo, a maioria das implementações de TLS são fortemente acopladas à conexão TCP subjacente. Se os dados e as solicitações do seu aplicativo trafegarem por dois saltos de conexão TCP (TCP → TCP), todas as garantias de TLS serão interrompidas na ponte entre as duas redes. Essa ponte, gateway ou balanceador de carga se torna um ponto fraco para os dados do aplicativo.
Os protocolos de comunicação segura tradicionais também são incapazes de proteger os dados do seu aplicativo se ele trafegar por vários protocolos de transporte diferentes. Eles não podem garantir a autenticidade ou integridade dos dados se o caminho de comunicação do seu aplicativo for UDP → TCP ou BLE → TCP.
Em outras palavras, usando implementações tradicionais de comunicação segura, você pode estar abrindo as portas para perder a confiança nos dados em que seus aplicativos estão trabalhando. Aqui estão alguns aspectos de seus aplicativos que podem estar em risco:
Quem o enviou para o meu aplicativo?
Na verdade, são os dados que eles enviaram ao meu aplicativo?
Autenticação ausente e integridade dos dados.
Nesta postagem do blog, criaremos dois exemplos de nós Ockam se comunicando entre si usando o roteamento Ockam e os transportes Ockam. Usaremos a API Rust para criar esses nós Ockam e configurar a orquestração de roteamento Ockam. O roteamento e os transportes Ockam permitem que outros protocolos Ockam forneçam garantias de ponta a ponta, como confiança, segurança, privacidade, entrega confiável e ordenação na camada do aplicativo.
Roteamento Ockam : é um protocolo simples e leve baseado em mensagens que possibilita a troca bidirecional de mensagens em uma grande variedade de topologias de comunicação: TCP -> TCP ou TCP -> TCP -> TCP ou BLE -> UDP -> TCP ou BLE -> TCP -> TCP ou TCP -> Kafka -> TCP ou qualquer outra topologia que você possa imaginar.
Transportes Ockam : adapte o roteamento Ockam a vários protocolos de transporte.
Um nó Ockam é qualquer aplicativo em execução que pode se comunicar com outros aplicativos usando vários protocolos Ockam, como roteamento, relés e portais, canais seguros, etc.
Um nó Ockam pode ser definido como qualquer processo independente que forneça uma API que suporte o protocolo de roteamento Ockam. Podemos criar nós Ockam usando oockam
) ou usando várias bibliotecas de programação Ockam como nossas bibliotecas Rust e Elixir. Estaremos usando a API Rust nesta postagem do blog.
Quando um trabalhador é iniciado em um
Para o nosso primeiro exemplo, criaremos um nó Ockam simples que enviará uma mensagem em alguns saltos (no mesmo nó) para um trabalhador (no mesmo nó) que apenas ecoa a mensagem de volta. Não há transporte TCP envolvido e todas as mensagens estão sendo passadas de um lado para o outro dentro do mesmo nó. Isso nos dará uma ideia de construção de trabalhadores e roteamento em um nível básico.
Precisamos criar um arquivo fonte Rust com um programa main()
e dois outros arquivos fonte Rust com dois workers: Hopper
e Echoer
. Podemos, então, enviar uma mensagem de string e ver se podemos recuperá-la.
Antes de começarmos, vamos considerar o roteamento. Quando enviamos uma mensagem dentro de um nó, ela carrega consigo 2 campos de metadados, onward_route
e return_route
, onde uma route
é simplesmente uma lista de adresses
. Cada trabalhador obtém um address
em um nó.
Então, se quisermos enviar uma mensagem do endereço app
para o endereço echoer
, com 3 saltos no meio, podemos construir uma rota como a seguinte.
┌───────────────────────┐ │ Node 1 │ ├───────────────────────┤ │ ┌────────────────┐ │ │ │ Address: │ │ │ │ 'app' │ │ │ └─┬────────────▲─┘ │ │ ┌─▼────────────┴─┐ │ │ │ Address: │ │ │ │ 'hopper1..3' │x3 │ │ └─┬────────────▲─┘ │ │ ┌─▼────────────┴─┐ │ │ │ Address: │ │ │ │ 'echoer' │ │ │ └────────────────┘ │ └───────────────────────┘
Aqui está o código Rust para construir esta rota.
/// Send a message to the echoer worker via the "hopper1", "hopper2", and "hopper3" workers. let route = route!["hopper1", "hopper2", "hopper3", "echoer"];
Vamos adicionar algum código-fonte para fazer isso acontecer a seguir. A primeira coisa que faremos é adicionar mais uma dependência a este projeto hello_ockam
vazio. A caixa colored
nos dará uma saída de console colorida, o que tornará a saída de nossos exemplos muito mais fácil de ler e entender.
cargo add colored
Em seguida, adicionamos o trabalhador echoer
(em nosso projeto hello_ockam
) criando um novo arquivo /src/echoer.rs
e copiando/colando o seguinte código nele.
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 } }
Em seguida, adicionamos o trabalhador hopper
(em nosso projeto hello_ockam
) criando um novo arquivo /src/hopper.rs
e copiando/colando o seguinte código nele.
Observe como esse trabalhador manipula os campos onward_route
e return_route
da mensagem para enviá-la ao próximo salto. Na verdade, veremos isso na saída do console quando executarmos esse código em breve.
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 } }
E finalmente, vamos adicionar um main()
ao nosso projeto hello_ockam
. Este será o ponto de entrada para o nosso exemplo.
Crie um arquivo vazio /examples/03-routing-many.hops.rs
(observe que ele está na pasta examples/
e não na pasta src/
como os workers acima).
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()) }
Agora é hora de executar nosso programa para ver o que ele faz! 🎉
Em seu aplicativo de terminal, execute o seguinte comando. Observe que OCKAM_LOG=none
é usado para desabilitar a saída de registro da biblioteca Ockam. Isso é feito para facilitar a leitura da saída do exemplo.
OCKAM_LOG=none cargo run --example 03-routing-many-hops
E você deve ver algo como o seguinte. Nosso programa de exemplo cria vários hop workers (três hopper
workers) entre o app
e o echoer
e encaminha nossa mensagem através deles 🚀.
Neste exemplo, vamos introduzir
Um transporte Ockam é um plugin para roteamento Ockam. Ele move mensagens de roteamento Ockam usando um protocolo de transporte específico como TCP, UDP, WebSockets, Bluetooth, etc.
Teremos três nós:
node_initiator
: O primeiro nó inicia o envio da mensagem por TCP para o nó do meio (porta 3000
).
node_middle
: Em seguida, o nó do meio simplesmente encaminha esta mensagem para o último nó por TCP novamente (porta 4000
desta vez).
node_responder
: E, finalmente, o nó respondente recebe a mensagem e envia uma resposta de volta ao nó iniciador.
O diagrama a seguir descreve o que construiremos a seguir. Neste exemplo, todos esses nós estão na mesma máquina, mas podem facilmente ser apenas nós em máquinas diferentes.
┌──────────────────────┐ │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' │ │ │ └──────────────────┘ │ └──────────────────────┘
Vamos começar criando um novo arquivo /examples/04-routing-over-two-transport-hops.rs
(na pasta /examples/
e não na pasta /src/
). Em seguida, copie/cole o seguinte código nesse arquivo.
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()) }
Este código não será realmente compilado, pois há 3 funções ausentes neste arquivo de origem. Estamos apenas adicionando este arquivo primeiro para preparar o restante do código que escreveremos a seguir.
Essa função main()
cria os três nós conforme vemos no diagrama acima e também os interrompe após a execução do exemplo.
Então vamos escrever a função que cria o nó iniciador primeiro. Copie o seguinte no arquivo de origem que criamos anteriormente ( /examples/04-routing-over-two-transport-hops.rs
) e cole-o abaixo do código existente:
/// 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 }
Este nó (iniciador) enviará uma mensagem para o respondente usando a seguinte rota.
let route = route![connection_to_middle_node, "forward_to_responder", "echoer"];
Vamos criar o nó intermediário a seguir, que executará o Forwarder
do trabalhador neste endereço: forward_to_responder
.
Copie e cole o seguinte no arquivo de origem que criamos acima ( /examples/04-routing-over-two-transport-hops.rs
).
Este nó intermediário simplesmente encaminha tudo o que entra em seu ouvinte TCP (em 3000
) para a porta 4000
.
Este nó tem um Forwarder
worker no endereço forward_to_responder
, então é assim que o iniciador pode alcançar o endereço especificado em sua rota no início deste exemplo.
/// - 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) }
Por fim, criaremos o nó respondedor. Esse nó executará o echoer
de trabalho que, na verdade, ecoa a mensagem de volta ao iniciador. Copie e cole o seguinte no arquivo de origem acima ( /examples/04-routing-over-two-transport-hops.rs
).
Este nó tem um trabalhador Echoer
no endereço echoer
, então é assim que o iniciador pode alcançar o endereço especificado em sua rota no início deste exemplo.
/// 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) }
Vamos executar este exemplo para ver o que ele faz 🎉.
Em seu aplicativo de terminal, execute o seguinte comando. Observe que OCKAM_LOG=none
é usado para desabilitar a saída de registro da biblioteca Ockam. Isso é feito para facilitar a leitura da saída do exemplo.
cargo run --example 04-routing-over-two-transport-hops
Isso deve produzir uma saída semelhante à seguinte. Nosso programa de exemplo cria uma rota que atravessa vários nós e transportes TCP do app
para o echoer
e roteia nossa mensagem através deles 🚀.
O roteamento e transporte Ockam são extremamente poderosos e flexíveis. Eles são um dos principais recursos que permitem a implementação dos Canais Seguros da Ockam. Colocando canais seguros Ockam e outros protocolos sobre o roteamento Ockam, podemos fornecer garantias de ponta a ponta sobre topologias de transporte arbitrárias que abrangem muitas redes e nuvens.
Em uma futura postagem no blog, abordaremos os canais seguros Ockam e como eles podem ser usados para fornecer garantias de ponta a ponta sobre topologias de transporte arbitrárias. Então fique ligado!
Enquanto isso, aqui estão alguns bons pontos de partida para aprender mais sobre Ockam:
ockam
.
Também publicado aqui.