Secure Request Reply
The previous example did not offer neither authentication nor encryption.
For a public TCP
connection, its a must. Let's fix that by adapting the
previous example.
This time we will the CURVE
mechanism, which is a public-key crypto.
To enable the usage of the CURVE
mechanism, the feature flag 'curve'
must be enabled.
However, this time we will use an external configuration file to get rid of all the boilerplate. This will also allows our application to run indepently of the socket configuration.
Based on some basic benchmarks, the CURVE
mechanism
might reduce the throughtput of I/O heavy applications by half due
to the overhead of the salsa20
encryption.
Config File
In this case we used yaml
configuration file, but any file format
supported by Serde
will work (as long as it supports typed enums).
# The curve keys where generated by running:
# `$ cargo run --example gen_curve_cert`
auth:
# The public keys allowed to authenticate. Note that this is
# the client's public key.
curve_registry:
- "n%3)5@(3pzp)v8yt6RW3eQVq5OQYb#TEodD^6oA^"
client:
# In a real life scenario the server would have a known addr.
#connect:
# - tcp: "127.0.0.1:3000"
heartbeat:
interval: 1s
timeout: 3s
ttl: 3s
send_high_water_mark: 10
send_timeout: 300ms
recv_high_water_mark: 100
recv_timeout: 300ms
mechanism:
curve_client:
client:
public: "n%3)5@(3pzp)v8yt6RW3eQVq5OQYb#TEodD^6oA^"
secret: "JiUDa>>owH1+mPTWs=>Jcyt%h.C1E4Js>)(g{geY"
# This is the server's public key.
server: "et189NB9uJC7?J+XU8JRhCbF?gOP9+o%kli=y2b8"
server:
# Here we use a system defined port so as to not conflict with the host
# machine. In a real life scenario we would have a port available.
bind:
- tcp: "127.0.0.1:*"
heartbeat:
interval: 1s
timeout: 3s
ttl: 3s
mechanism:
curve_server:
secret: "iaoRiIVA^VgV:f4a<@{8K{cP62cE:dh=4:oY+^l("
The code
Aside from the additionnal logic for reading the config file, the code is now simpler than before.
use libzmq::{config::*, prelude::*, *}; use serde::{Deserialize, Serialize}; use std::{ fs::File, io::Read, path::{Path, PathBuf}, thread, }; const CONFIG_FILE: &str = "secure_req_rep.yml"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { auth: AuthConfig, client: ClientConfig, server: ServerConfig, } fn read_file(name: &Path) -> std::io::Result<Vec<u8>> { let mut file = File::open(name)?; let mut buf = Vec::new(); file.read_to_end(&mut buf)?; Ok(buf) } fn main() -> Result<(), anyhow::Error> { let path = PathBuf::from("examples").join(CONFIG_FILE); let config: Config = serde_yaml::from_slice(&read_file(&path).unwrap()).unwrap(); // Configure the `AuthServer`. We won't need the returned `AuthClient`. let _ = config.auth.build()?; // Configure our two sockets. let server = config.server.build()?; let client = config.client.build()?; // Once again we used a system assigned port for our server. let bound = server.last_endpoint()?; client.connect(bound)?; // Spawn the server thread. In a real application, this // would be on another node. let handle = thread::spawn(move || -> Result<(), Error> { use ErrorKind::*; loop { let request = server.recv_msg()?; assert_eq!(request.to_str(), Ok("ping")); // Retrieve the routing_id to route the reply to the client. let id = request.routing_id().unwrap(); if let Err(err) = server.route("pong", id) { match err.kind() { // Cannot route msg, drop it. WouldBlock | HostUnreachable => (), _ => return Err(err.cast()), } } } }); // Do some request-reply work. client.send("ping")?; let msg = client.recv_msg()?; assert_eq!(msg.to_str(), Ok("pong")); // This will cause the server to fail with `InvalidCtx`. Ctx::global().shutdown(); // Join with the thread. let err = handle.join().unwrap().unwrap_err(); assert_eq!(err.kind(), ErrorKind::InvalidCtx); Ok(()) } #[cfg(test)] mod tests { use super::main; #[test] fn main_runs() { main().unwrap(); } }