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();
    }
}