Crate sozu_lib

source ·
Expand description

§What this library does

This library provides tools to build and start HTTP, HTTPS and TCP reverse proxies.

The proxies handles network polling, HTTP parsing, TLS in a fast single threaded event loop.

Each proxy is designed to receive configuration changes at runtime instead of reloading from a file regularly. The event loop runs in its own thread and receives commands through a message queue.

§Difference with the crate sozu

To create several workers and manage them all at once (which is the most common way to use Sōzu), the crate sozu is more indicated than using the lib directly.

The crate sozu provides a binary called the main process. The main process uses sozu_lib to start and manage workers. Each worker can handle HTTP, HTTPS and TCP traffic. The main process receives synchronizes the state of all workers, using UNIX sockets and custom channels to communicate with them. The main process itself is is configurable with a file, and has a CLI.

§How to use this library directly

This documentation here explains how to write a binary that will start a single Sōzu worker and give it orders. The method has two steps:

  1. Starts a Sōzu worker in a distinct thread
  2. sends instructions to the worker on a UNIX socket via a Sōzu channel

§How to start a Sōzu worker

Before creating an HTTP proxy, we first need to create an HTTP listener. The listener is an abstraction around a TCP socket provided by the kernel. We need the sozu_command_lib to build a listener.

use sozu_command_lib::{config::ListenerBuilder, proto::command::SocketAddress};

let address = SocketAddress::new_v4(127,0,0,1,8080);
let http_listener = ListenerBuilder::new_http(address)
    .to_http(None)
    .expect("Could not create HTTP listener");

The http_listener is of the type HttpListenerConfig, that we can be sent to the worker to start the proxy.

Then create a pair of channels to communicate with the proxy. The channel is a wrapper around a unix socket.

use sozu_command_lib::{
    channel::Channel,
    proto::command::{WorkerRequest, WorkerResponse},
};

let (mut command_channel, proxy_channel): (
    Channel<WorkerRequest, WorkerResponse>,
    Channel<WorkerResponse, WorkerRequest>,
) = Channel::generate(1000, 10000).expect("should create a channel");

Here, the command_channel end is blocking, it sends WorkerRequests and receives WorkerResponses, while the proxy_channel end is non-blocking, and the types are reversed. Writing the types here isn’t even necessary thanks to the compiler, but it brings the point accross.

You can now launch the worker in a separate thread, providing the HTTP listener config, the proxy end of the channel, and your custom number of buffers and their size:

use std::thread;

let worker_thread_join_handle = thread::spawn(move || {
    let max_buffers = 500;
    let buffer_size = 16384;
    sozu_lib::http::testing::start_http_worker(http_listener, proxy_channel, max_buffers, buffer_size);
});

§Send orders

Once the thread is launched, the proxy worker will start its event loop and handle events on the listening interface and port specified when building the HTTP Listener. Since no frontends or backends were specified for the proxy, it will receive the connections, parse the requests, then send a default (but configurable) answer.

Before defining a frontend and backends, we need to define a cluster, which describes a routing configuration. A cluster contains:

  • one frontend
  • one or several backends
  • routing rules

A cluster is identified by its cluster_id, which will be used to define frontends and backends later on.

use sozu_command_lib::proto::command::{Cluster, LoadBalancingAlgorithms};

let cluster = Cluster {
    cluster_id: "my-cluster".to_string(),
    sticky_session: false,
    https_redirect: false,
    load_balancing: LoadBalancingAlgorithms::RoundRobin as i32,
    answer_503: Some("A custom forbidden message".to_string()),
    ..Default::default()
};

The defaults are sensible, so we could define only the cluster_id.

We can now define a frontend. A frontend is a way to recognize a request and match it to a cluster_id, depending on the hostname and the beginning of the URL path. The address field must match the one of the HTTP listener we defined before:

use std::collections::BTreeMap;

 use sozu_command_lib::proto::command::{PathRule, RequestHttpFrontend, RulePosition, SocketAddress};

let http_front = RequestHttpFrontend {
    cluster_id: Some("my-cluster".to_string()),
    address: SocketAddress::new_v4(127,0,0,1,8080),
    hostname: "example.com".to_string(),
    path: PathRule::prefix(String::from("/")),
    position: RulePosition::Pre.into(),
    tags: BTreeMap::from([
        ("owner".to_owned(), "John".to_owned()),
        ("id".to_owned(), "my-own-http-front".to_owned()),
    ]),
    ..Default::default()
};

The tags are keys and values that will appear in the access logs, which can come in handy.

Now let’s define a backend. A backend is an instance of a backend application we want to route traffic to. The address field must match the IP and port of the backend server.

use sozu_command_lib::proto::command::{AddBackend, LoadBalancingParams, SocketAddress};

let http_backend = AddBackend {
    cluster_id: "my-cluster".to_string(),
    backend_id: "test-backend".to_string(),
    address: SocketAddress::new_v4(127,0,0,1,8000),
    load_balancing_parameters: Some(LoadBalancingParams::default()),
    ..Default::default()
};

A cluster can have multiple backend servers, and they can be added or removed while the proxy is running. If a backend is removed from the configuration while the proxy is handling a request to that server, it will finish that request and stop sending new traffic to that server.

Now we can use the other end of the channel to send all these requests to the worker, using the WorkerRequest type:

use sozu_command_lib::{
    proto::command::{Request, request::RequestType, WorkerRequest},
};

command_channel
    .write_message(&WorkerRequest {
        id: String::from("add-the-cluster"),
        content: RequestType::AddCluster(cluster).into(),
    })
    .expect("Could not send AddHttpFrontend request");

command_channel
    .write_message(&WorkerRequest {
        id: String::from("add-the-frontend"),
        content: RequestType::AddHttpFrontend(http_front).into(),
    })
    .expect("Could not send AddHttpFrontend request");

command_channel
    .write_message(&WorkerRequest {
        id: String::from("add-the-backend"),
        content: RequestType::AddBackend(http_backend).into(),
    })
    .expect("Could not send AddBackend request");

println!("HTTP -> {:?}", command_channel.read_message());
println!("HTTP -> {:?}", command_channel.read_message());
println!("HTTP -> {:?}", command_channel.read_message());

The event loop of the worker will process these instructions and add them to its state, and the worker will send back an acknowledgement message.

Now we can let the worker thread run in the background:

let _ = worker_thread_join_handle.join();

Here is the complete example for reference, it matches the examples/http.rs example:

#[macro_use]
extern crate sozu_command_lib;

use std::{collections::BTreeMap, env, io::stdout, thread};

use anyhow::Context;
use sozu_command_lib::{
    channel::Channel,
    config::ListenerBuilder,
    logging::setup_default_logging,
    proto::command::{
        request::RequestType, AddBackend, Cluster, LoadBalancingAlgorithms, LoadBalancingParams,
        PathRule, Request, RequestHttpFrontend, RulePosition, SocketAddress,WorkerRequest,
    },
};

fn main() -> anyhow::Result<()> {
    setup_default_logging(true, "info", "EXAMPLE");

    info!("starting up");

    let http_listener = ListenerBuilder::new_http(SocketAddress::new_v4(127,0,0,1,8080))
        .to_http(None)
        .expect("Could not create HTTP listener");

    let (mut command_channel, proxy_channel) =
        Channel::generate(1000, 10000).with_context(|| "should create a channel")?;

    let worker_thread_join_handle = thread::spawn(move || {
        let max_buffers = 500;
        let buffer_size = 16384;
        sozu_lib::http::testing::start_http_worker(http_listener, proxy_channel, max_buffers, buffer_size)
            .expect("The worker could not be started, or shut down");
    });

    let cluster = Cluster {
        cluster_id: "my-cluster".to_string(),
        sticky_session: false,
        https_redirect: false,
        load_balancing: LoadBalancingAlgorithms::RoundRobin as i32,
        answer_503: Some("A custom forbidden message".to_string()),
        ..Default::default()
    };

    let http_front = RequestHttpFrontend {
        cluster_id: Some("my-cluster".to_string()),
        address: SocketAddress::new_v4(127,0,0,1,8080),
        hostname: "example.com".to_string(),
        path: PathRule::prefix(String::from("/")),
        position: RulePosition::Pre.into(),
        tags: BTreeMap::from([
            ("owner".to_owned(), "John".to_owned()),
            ("id".to_owned(), "my-own-http-front".to_owned()),
        ]),
        ..Default::default()
    };
    let http_backend = AddBackend {
        cluster_id: "my-cluster".to_string(),
        backend_id: "test-backend".to_string(),
        address: SocketAddress::new_v4(127,0,0,1,8000),
        load_balancing_parameters: Some(LoadBalancingParams::default()),
        ..Default::default()
    };

    command_channel
        .write_message(&WorkerRequest {
            id: String::from("add-the-cluster"),
            content: RequestType::AddCluster(cluster).into(),
        })
        .expect("Could not send AddHttpFrontend request");

    command_channel
        .write_message(&WorkerRequest {
            id: String::from("add-the-frontend"),
            content: RequestType::AddHttpFrontend(http_front).into(),
        })
        .expect("Could not send AddHttpFrontend request");

    command_channel
        .write_message(&WorkerRequest {
            id: String::from("add-the-backend"),
            content: RequestType::AddBackend(http_backend).into(),
        })
        .expect("Could not send AddBackend request");

    println!("HTTP -> {:?}", command_channel.read_message());
    println!("HTTP -> {:?}", command_channel.read_message());

    // uncomment to let it run in the background
    // let _ = worker_thread_join_handle.join();
    info!("good bye");
    Ok(())
}

Modules§

Macros§

Structs§

Enums§

Traits§

Functions§