Important
The configuration in this guide requires HAProxy 3.1 or later. If your distribution of choice has an older version, you may need to look for backports or third party packages. For example, for Debian and Ubuntu, backports are provided here, by HAProxy’s Debian maintainer.
Getting started
In this guide, we’ll be working off of the assumption that we’ve set up iocaine with the default, built-in request handler, as shown in the Getting Started guide. On the HAProxy side, our example will have a web service at localhost:8000, which we want to proxy via HAProxy as example.com.
The relevant parts of our haproxy.cfg would look something along these lines:
frontend main
bind [::]:80
acl example_acl req.hdr(host) -i example.com
use_backend localhost_8000 if example_acl
backend localhost_8000
mode http
server localhost:8000
(View the full configuration)
global
log stdout format iso local0 debug
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
frontend main
bind [::]:80
acl example_acl req.hdr(host) -i example.com
use_backend localhost_8000 if example_acl
backend localhost_8000
mode http
server localhost:8000
HAProxy SPOE
HAProxy has a Stream Processing Offload Engine, which requires a Stream Processing Offload Agent - something iocaine can provide through haproxy-spoa-server. HAProxy works differently than the other reverse proxies: the decision and response generation steps are more clearly separated, and isolated: it’s possible to use iocaine for decision making only, and use something else to generate a response: some static content, another garbage generator - the possibilities are endless.
Because the decision and the output generation are isolated from one another, we need a way to carry the decision across to the output generation part. The simplest way to do that is to set a special, trusted HTTP header when talking to the output generating backend. Ideally, we’d remove the header before passing it through the filter, but that does not seem to be possible, so we’ll do the next best thing: if our trusted header - which we’ll name Iocaine-Decision - is present in the incoming request, we’ll treat that as a malicious request, and will skip decision making, marking it as such right there and then. This ensures that if the output generating backend sees an Iocaine-Decision header, it can be sure it was set inside HAProxy, and did not come from an untrusted source.
The built-in request handler supports short-circuiting the decision making if there’s a trusted decision header, which Iocaine-Decision will be. Setting trusted-decision-header will tell it which header to use for this purpose.
Integrating iocaine
Integrating iocaine into our HAProxy configuration will involve updating the configuration of both. We’ll start with iocaine’s. If you just want to get going fast, read the next two sections, and skip to the full haproxy.cfg
Updating the iocaine configuration
As discussed above, we’ll need an agent for HAProxy. Lets configure iocaine to spawn us one, by placing the following configuration into /opt/iocaine/etc/iocaine/config.d/04-haproxy-spoa.kdl:
haproxy-spoa-server spoa {
bind "/run/iocaine/iocaine.sock" unix-socket-access="group"
use handler-from=default metrics=metrics
}
declare-handler default {
trusted-decision-header "iocaine-decision"
}
This assumes that iocaine and HAProxy share at least one group. For testing purposes, you can use unix-socket-access="everbody" - just don’t keep that in production! It uses the same metrics and handler (the built-in one) as we ended up with in the Getting Started guide. This also sets the trusted-decision-header, as discussed previously.
Configuring the SPOA agent
Next, we need a separate configuration file for the HAProxy side of our Stream Processing Offload Agent. Lets place it into, say, iocaine-spoa.cfg, in the same directory where our haproxy.cfg is. For the sake of simplicity, lets assume we’ll place it into /etc/haproxy/iocaine-spoa.cfg. It should look something like this:
[iocaine]
spoe-agent iocaine
log global
option var-prefix iocaine
timeout processing 5s
messages check-request
use-backend iocaine
spoe-message check-request
args req_method=method req_hdrs=req.hdrs req_path=path req_query=query
event on-frontend-http-request
This snippet specifies an iocaine SPOE agent. The agent has a variable prefix of iocaine for variables set inside the handler. On incoming requests, a check-request messages gets sent to the agent. The message contains the following variables, which can be used to make decisions:
req_method- the method used to make the request.req_hdrs- the headers of the request.req_path- the path which has been requested.req_query- the query parameters passed with the request.
For more information on the SPOE configuration, see section 2.1 of doc/SPOA.txt.
Updating the HAProxy configuration
In the above configuration, we configured the agent to use a backend named “iocaine”. Lets set that up! Add the following to your haproxy.cfg:
backend iocaine
mode spop
option spop-check
server iocaine "/run/iocaine/iocaine.sock"
The path used here must match the path set in the iocaine configuration above.
Now, we must decide how to approach wrapping our frontend with iocaine. In this guide, we’ll filter all requests through iocaine, and depending on the decision made, we’ll choose a backend: either iocaine-output if the decision was to serve garbage, or localhost_8000 if the decision was to serve the Real Thing. If iocaine is unavailable, we shall return a HTTP 503.
We will be replacing our entire frontend main block, with a configuration that looks like the following:
frontend main
bind [::]:80
# If we see an `iocaine-decision` header, serve it garbage immediately.
# We do not accept this header from the outside.
acl forbidden_hdr hdr_cnt(iocaine-decision) gt 0
http-request set-header iocaine-decision "garbage" if forbidden_hdr
use_backend iocaine-output if forbidden_hdr
# filter all requests through iocaine
filter spoe engine iocaine config /etc/haproxy/iocaine-spoa.cfg
# fallback if iocaine is unavailable
http-request set-var(txn.iocaine.response,ifnotset) str("borked")
# iocaine decision check
acl iocaine_passed var(txn.iocaine.response) -m str eq "default"
acl iocaine_unavailable var(txn.iocaine.response) -m str eq "borked"
acl example_acl req.hdr(host) -i example.com
# Set the Iocaine-Decision trusted header, in case we'll use
# the `iocaine-output` backend.
http-request add-header Iocaine-Decision "%[var(txn.iocaine.response)]" unless forbidden_hdr
# show a challenge/garbage if the request has not passed checks and
# iocaine is available, and we're serving the appropriate site.
use_backend iocaine-output if !iocaine_passed !iocaine_unavailable example_acl
# only show the website if iocaine decided so
use_backend localhost_8000 if example_acl iocaine_passed
# for debugging purposes, the response of iocaine can be added as a
# response header. The previous Iocaine-Decision header only gets added for
# the request to be used by iocaine-output, not in the response.
# http-after-response add-header Iocaine-Decision "%[var(txn.iocaine.response)]"
We’ll also need to add another backend: iocaine-output. This will use the HTTP server we have set up in the Getting Started guide, the one that’s listening on 127.0.0.1:42069. Because its handler is configured to trust the Iocaine-Decision header, once we send the request to this backend, iocaine will short-circuit the decision making process, and use the value in the header, avoiding performing the same process again, skipping quickly to output generation.
Add the following to your haproxy.cfg:
backend iocaine-output
mode http
server iocaine-http 127.0.0.1:42069
And with this, our task is complete!
The full haproxy.cfg
To provide an overview, and a full configuration you can try, the haproxy.cfg assembled together in this guide are provided below, in full:
global
log stdout format iso local0 debug
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
frontend main
bind [::]:80
# If we see an `iocaine-decision` header, serve it garbage immediately.
# We do not accept this header from the outside.
acl forbidden_hdr hdr_cnt(iocaine-decision) gt 0
http-request set-header iocaine-decision "garbage" if forbidden_hdr
use_backend iocaine-output if forbidden_hdr
# filter all requests through iocaine
filter spoe engine iocaine config /etc/haproxy/iocaine-spoa.cfg
# fallback if iocaine is unavailable
http-request set-var(txn.iocaine.response,ifnotset) str("borked")
# iocaine decision check
acl iocaine_passed var(txn.iocaine.response) -m str eq "default"
acl iocaine_unavailable var(txn.iocaine.response) -m str eq "borked"
acl example_acl req.hdr(host) -i example.com
# Set the Iocaine-Decision trusted header, in case we'll use
# the `iocaine-output` backend.
http-request add-header Iocaine-Decision "%[var(txn.iocaine.response)]" unless forbidden_hdr
# show a challenge/garbage if the request has not passed checks and
# iocaine is available, and we're serving the appropriate site.
use_backend iocaine-output if !iocaine_passed !iocaine_unavailable example_acl
# only show the website if iocaine decided so
use_backend localhost_8000 if example_acl iocaine_passed
# for debugging purposes, the response of iocaine can be added as a
# response header. The previous Iocaine-Decision header only gets added for
# the request to be used by iocaine-output, not in the response.
# http-after-response add-header Iocaine-Decision "%[var(txn.iocaine.response)]"
backend localhost_8000
mode http
server localhost:8000
backend iocaine
mode spop
option spop-check
server iocaine "/run/iocaine/iocaine.sock"
backend iocaine-output
mode http
server iocaine-http 127.0.0.1:42069