Library for parsing HAProxy SPOP protocol messages.
The protocol is described here: https://github.com/haproxy/haproxy/blob/master/doc/SPOE.txt
Install Rust. For the container-based HAProxy integration flow, also install podman. If you want to use the helper recipes, install just too.
Run the test suite:
cargo testCheck the examples compile:
cargo check --examplesThis repository includes three runnable agents:
agent_tcp: listens on0.0.0.0:12345agent_socket: listens onspoa_agent/spoa.sockagent_args: listens on127.0.0.1:12346and reads its message arguments back: unnamed arguments by position, a repeated name throughMessage::get, and a name that is not valid UTF-8. Start here to see how to read what HAProxy sends
Run one of them directly with Cargo:
cargo run --example agent_tcpcargo run --example agent_socketcargo run --example agent_argsIf you prefer autoreload during development, the helper recipes are:
just agent_tcp
just agent_socket
just agent_argsThese recipes require cargo-watch.
To run HAProxy in a container using the current helper recipes:
just build
just runOr directly with podman:
mkdir -p spoa_agent
chmod -R 777 spoa_agent
podman build -t haproxy-spoe .
podman run -d --name haproxy --network=host --security-opt label=disable -v "${PWD}/spoa_agent:/var/run/haproxy" haproxy-spoeTo stop the container:
just stopOr directly with podman:
podman stop haproxy
podman rm haproxyTo send a request to the haproxy container, type:
just testOr directly:
curl -v http://0:5000 -H "CF-IPCountry: xx"The current HAProxy configuration is in haproxy.cfg, and the current SPOE configuration is in spoe-test.conf.
The repository currently defines three SPOE engines:
test: TCP backend127.0.0.1:12345test-socket: UNIX socket backend/var/run/haproxy/spoa.sockargs: TCP backend127.0.0.1:12346, sending theecho-argsmessage toagent_args. Its summary of what it decoded comes back in theX-SPOE-ARGSresponse header
scripts/integration.sh runs the whole loop unattended: it starts the three agents, runs the
official HAProxy image with haproxy.cfg and spoe-test.conf, and checks that a response carries
the variables they set, including agent_args' summary of every argument it decoded. It fails if
any agent could not decode a frame. Last, it sends agent_args a malformed frame and checks that
the decode error names the problem and its offset without dumping the frame.
just integration # haproxy:latest
just integration 3.2 # any haproxy image tagIt uses podman by default; set CONTAINER_ENGINE=docker to use docker. It needs Linux, because
HAProxy reaches the agents through host networking. CI runs it against HAProxy 3.2 and 3.4.
A NOTIFY frame carries a list of messages, and each message keeps its arguments in declaration
order. Argument names are optional in spoe-message (args [name=]<sample> ...), so a name can
be empty or repeated:
use spop::{TypedData, frame::Message};
fn handle(message: &Message) {
// First argument with this name.
if let Some(TypedData::String(country)) = message.get("country") {
println!("country: {country}");
}
// Unnamed arguments, by position.
for (name, value) in &message.args {
if name.is_empty() {
println!("unnamed: {value:?}");
}
}
}global
log stdout format raw local0
daemon
defaults
log global
mode http
option httplog
timeout client 30s
timeout connect 10s
timeout server 30s
frontend main
bind :5000
filter spoe engine test-socket config /usr/local/etc/haproxy/spoe-test.conf
filter spoe engine test config /usr/local/etc/haproxy/spoe-test.conf
tcp-request content reject if { var(sess.spoe_test_socket.ip_score) -m int lt 20 }
http-after-response set-header X-SPOE-VAR_SOCKET %[var(txn.spoe_test_socket.my_var)]
http-after-response set-header X-SPOE_VAR_TCP %[var(txn.spoe_test.my_var)]
default_backend app
backend spoe-test
# "mode spop" is mandatory for backends holding SPOA servers; "mode tcp" is only
# auto-converted.
mode spop
# Health-check with a real SPOP HELLO handshake rather than a bare TCP connect.
option spop-check
timeout connect 5s
timeout server 3m
server rust-agent 127.0.0.1:12345 check
backend spoe-test-socket
mode spop
option spop-check
timeout connect 5s
timeout server 3m
server local-agent unix@/var/run/haproxy/spoa.sock check
backend app
mode http
http-request return status 200 content-type "text/plain" string "Hello"And the spoe agent conf:
[test]
spoe-agent test
messages check-client-ip log-request
option var-prefix spoe_test
option continue-on-error
timeout processing 10ms
use-backend spoe-test
log global
spoe-message check-client-ip
args ip=src
event on-client-session
# Argument names are optional: src and dst arrive unnamed (empty name), in this order.
spoe-message log-request
args ip=src country=hdr(CF-IPCountry) user_agent=hdr(User-Agent) src dst
event on-frontend-http-request
[test-socket]
spoe-agent test-socket
messages check-client-ip log-request
option var-prefix spoe_test_socket
option continue-on-error
timeout processing 10ms
use-backend spoe-test-socket
log global
# Each [scope] is self-contained, so the messages are declared again here.
spoe-message check-client-ip
args ip=src
event on-client-session
spoe-message log-request
args ip=src country=hdr(CF-IPCountry) user_agent=hdr(User-Agent) src dst
event on-frontend-http-request