Ockam は、エンドツーエンドの暗号化、相互認証、キー管理、資格情報管理、認可ポリシーの適用をすべて大規模に調整するための、プログラミング ライブラリ、コマンド ライン ツール、およびマネージド クラウド サービスのスイートです。オッカムのエンドツーエンド
これを可能にする重要な機能の 1 つは、
このブログ投稿では、 Ockam Rust API を調査し、Ockam でルーティングがどのように機能するかを確認します。 Rust コードを使用して、単純なケースとより高度な使用例を示すいくつかのコード例を見ていきます。
始める前に、アプリケーションを保護するために既存のアプローチを使用する場合の落とし穴について簡単に説明しましょう。私たちのほとんどは、システムを構築し、物事を機能させて出荷することに重点を置いているときに、セキュリティについて考えることはありません。
従来の安全な通信の実装は通常、すべてのセキュリティが 1 つの基礎となるトランスポート接続の長さと継続時間に制限されるように、トランスポート プロトコルと密接に結合されています。
たとえば、ほとんどの TLS 実装は、基礎となる TCP 接続と密接に結合されています。アプリケーションのデータとリクエストが 2 つの TCP 接続ホップ (TCP → TCP) を経由する場合、すべての TLS は 2 つのネットワーク間のブリッジでの切断を保証します。このブリッジ、ゲートウェイ、またはロード バランサーがアプリケーション データの弱点となります。
従来の安全な通信プロトコルでは、アプリケーションのデータが複数の異なるトランスポート プロトコルを介して移動する場合、そのデータを保護することはできません。アプリケーションの通信パスが UDP → TCP または BLE → TCP である場合、データの信頼性や整合性は保証できません。
言い換えれば、従来の安全な通信の実装を使用すると、アプリが処理しているデータに対する信頼を失う可能性があります。アプリにはリスクがある可能性のある側面がいくつかあります。
誰がそれを私のアプリに送信しましたか?
それは本当に彼らが私のアプリに送ったデータなのでしょうか?
認証が欠落しており、データの整合性が失われています。
このブログ投稿では、Ockam ルーティングと Ockam トランスポートを使用して相互に通信する Ockam ノードの 2 つの例を作成します。 Rust APIを使用してこれらの Ockam ノードを作成し、Ockam ルーティング オーケストレーションをセットアップします。 Ockam のルーティングとトランスポートにより、他の Ockam プロトコルが信頼性、セキュリティ、プライバシー、信頼性の高い配信、アプリケーション層での順序付けなどのエンドツーエンドの保証を提供できるようになります。
Ockam ルーティング: TCP -> TCP または TCP -> TCP -> TCP または BLE -> UDP -> TCP または BLE など、さまざまな通信トポロジ上で双方向にメッセージを交換できるようにするシンプルで軽量のメッセージベースのプロトコルです。 -> TCP -> TCP または TCP -> Kafka -> TCP またはその他の想像できるトポロジ。
Ockam トランスポート: Ockam ルーティングをさまざまなトランスポート プロトコルに適応させます。
Ockam ノードは、ルーティング、リレー、ポータル、セキュア チャネルなどのさまざまな Ockam プロトコルを使用して他のアプリケーションと通信できる実行中のアプリケーションです。
Ockam ノードは、Ockam ルーティング プロトコルをサポートする API を提供する任意の独立したプロセスとして定義できます。以下を使用して Ockam ノードを作成できます。ockam
コマンド)、または Rust ライブラリや Elixir ライブラリなどのさまざまな Ockam プログラミング ライブラリを使用します。このブログ投稿では Rust API を使用します。
ワーカーが起動されると、
最初の例では、いくつかのホップ (同じノード内) を介してメッセージをワーカー (同じノード内) に送信し、メッセージをエコーバックするだけの単純な Ockam ノードを作成します。 TCP トランスポートは関係せず、すべてのメッセージが同じノード内でやり取りされます。これにより、基本レベルでワーカーとルーティングを構築する感覚が得られます。
main()
プログラムを含む Rust ソース ファイルと、 Hopper
とEchoer
という 2 つのワーカーを含む他の 2 つの Rust ソース ファイルを作成する必要があります。次に、文字列メッセージを送信して、それがエコーバックされるかどうかを確認します。
始める前に、ルーティングについて考えてみましょう。ノード内でメッセージを送信すると、 onward_route
とreturn_route
2 つのメタデータ フィールドが送信されます。 route
単なるadresses
のリストです。各ワーカーはノード内のaddress
取得します。
したがって、 app
アドレスからechoer
アドレスにメッセージを送信したい場合は、中間に 3 ホップがあり、次のようなルートを構築できます。
┌───────────────────────┐ │ Node 1 │ ├───────────────────────┤ │ ┌────────────────┐ │ │ │ Address: │ │ │ │ 'app' │ │ │ └─┬────────────▲─┘ │ │ ┌─▼────────────┴─┐ │ │ │ Address: │ │ │ │ 'hopper1..3' │x3 │ │ └─┬────────────▲─┘ │ │ ┌─▼────────────┴─┐ │ │ │ Address: │ │ │ │ 'echoer' │ │ │ └────────────────┘ │ └───────────────────────┘
このルートを構築するための Rust コードは次のとおりです。
/// Send a message to the echoer worker via the "hopper1", "hopper2", and "hopper3" workers. let route = route!["hopper1", "hopper2", "hopper3", "echoer"];
次にこれを実現するためにソース コードを追加しましょう。最初に、この空のhello_ockam
プロジェクトに依存関係を 1 つ追加します。 colored
クレートにより、色分けされたコンソール出力が得られ、例からの出力が非常に読みやすく、理解しやすくなります。
cargo add colored
次に、新しい/src/echoer.rs
ファイルを作成し、そのファイルに次のコードをコピーして貼り付けることで、 echoer
ワーカーを ( hello_ockam
プロジェクト内に) 追加します。
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 } }
次に、新しい/src/hopper.rs
ファイルを作成し、次のコードをコピーして貼り付けて、 hopper
ワーカーを ( hello_ockam
プロジェクト内に) 追加します。
このワーカーがメッセージのonward_route
フィールドとreturn_route
フィールドを操作して次のホップに送信する方法に注目してください。このコードをすぐに実行すると、実際にコンソール出力にこれが表示されます。
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 } }
最後に、 hello_ockam
プロジェクトにmain()
を追加しましょう。これがこの例のエントリ ポイントになります。
空のファイル/examples/03-routing-many.hops.rs
を作成します (これは、上記のワーカーのようなsrc/
フォルダーではなく、 examples/
フォルダーにあることに注意してください)。
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()) }
今度はプログラムを実行して、その動作を確認します。 🎉
ターミナル アプリで次のコマンドを実行します。 OCKAM_LOG=none
、Ockam ライブラリからのログ出力を無効にするために使用されることに注意してください。これは、例の出力を読みやすくするために行われます。
OCKAM_LOG=none cargo run --example 03-routing-many-hops
そして、次のようなものが表示されるはずです。私たちのサンプル プログラムは、 app
とechoer
間に複数のホップ ワーカー (3 つのhopper
ワーカー) を作成し、それらを介してメッセージをルーティングします 🚀。
この例で紹介するのは、
Ockam トランスポートは、Ockam ルーティング用のプラグインです。 TCP、UDP、WebSocket、Bluetooth などの特定のトランスポート プロトコルを使用して、Ockam ルーティング メッセージを移動します。
3 つのノードがあります。
node_initiator
: 最初のノードは、TCP 経由で中間ノード (ポート3000
) へのメッセージの送信を開始します。
node_middle
: 次に、中間ノードはこのメッセージを再度 TCP (今回はポート4000
) 経由で最後のノードに転送します。
node_responder
: そして最後に、レスポンダー ノードがメッセージを受信し、イニシエーター ノードに応答を送り返します。
次の図は、次に構築するものを示しています。この例では、これらすべてのノードが同じマシン上にありますが、単に異なるマシン上のノードになることも簡単にあります。
┌──────────────────────┐ │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' │ │ │ └──────────────────┘ │ └──────────────────────┘
まず、新しいファイル/examples/04-routing-over-two-transport-hops.rs
( /src/
フォルダーではなく/examples/
/ フォルダー内) を作成します。次に、次のコードをコピーしてそのファイルに貼り付けます。
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()) }
このソース ファイルには 3 つの関数が欠落しているため、このコードは実際にはコンパイルされません。次に書く残りのコードをステージングするために、最初にこのファイルを追加するだけです。
このmain()
関数は、上の図にあるように 3 つのノードを作成し、サンプルの実行終了後にそれらのノードを停止します。
そこで、最初にイニシエーター ノードを作成する関数を作成しましょう。以下を先ほど作成したソース ファイル ( /examples/04-routing-over-two-transport-hops.rs
) にコピーし、そこにある既存のコードの下に貼り付けます。
/// 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 }
この (イニシエーター) ノードは、次のルートを使用してレスポンダーにメッセージを送信します。
let route = route![connection_to_middle_node, "forward_to_responder", "echoer"];
次に、このアドレスでワーカーForwarder
を実行する中間ノードを作成しましょう: forward_to_responder
。
以下をコピーして、上で作成したソース ファイル ( /examples/04-routing-over-two-transport-hops.rs
) に貼り付けます。
この中間ノードは、TCP リスナー ( 3000
上) に入ってくるものをすべてポート4000
に転送するだけです。
このノードにはアドレスforward_to_responder
にForwarder
ワーカーがあるため、イニシエータはこの例の冒頭でルートで指定されたアドレスに到達することができます。
/// - 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) }
最後に、レスポンダー ノードを作成します。このノードは、実際にメッセージをイニシエーターにエコーバックするワーカーechoer
を実行します。以下をコピーして、上記のソース ファイル ( /examples/04-routing-over-two-transport-hops.rs
) に貼り付けます。
このノードにはアドレスechoer
にEchoer
ワーカーがあるため、イニシエータはこの例の冒頭でルートで指定されたアドレスに到達することができます。
/// 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) }
この例を実行して、何が行われるかを見てみましょう 🎉。
ターミナル アプリで次のコマンドを実行します。 OCKAM_LOG=none
、Ockam ライブラリからのログ出力を無効にするために使用されることに注意してください。これは、例の出力を読みやすくするために行われます。
cargo run --example 04-routing-over-two-transport-hops
これにより、次のような出力が生成されるはずです。私たちのサンプル プログラムは、複数のノードとapp
からechoer
までの TCP トランスポートを通過するルートを作成し、それらを介してメッセージをルーティングします 🚀。
Ockam のルーティングとトランスポートは非常に強力で柔軟性があります。これらは、Ockam Secure Channel の実装を可能にする重要な機能の 1 つです。 Ockam セキュア チャネルとその他のプロトコルを Ockam ルーティング上に重ねることで、多くのネットワークやクラウドにまたがる任意のトランスポート トポロジ上でエンドツーエンドの保証を提供できます。
今後のブログ投稿では、Ockam Secure Channel と、それを使用して任意のトランスポート トポロジ上でエンドツーエンドの保証を提供する方法について説明する予定です。乞うご期待!
それまでの間、Ockam について詳しく知るための良い出発点をいくつか紹介します。
ockam
。