TL/DR: python script to combine Netscaler reverse proxy HTTP request and response objects and output the result as an Apache-style log. Github repo is here: https://github.com/scherma/ipfixwatch
So the time came where your organisation decided it needs a new and shiny reverse proxy, hopefully before the current bucket of bolts went to the great big datacentre in the sky. It’ll be fun, they said. I told them we needed to talk about the definition of fun. They said they’d schedule a meeting.
This is not the right place to provide (nor am I really qualified) to give an in depth explanation of appflow; the short version is that it is a binary protocol for logging application-level information in a form that is more flexible than syslog. It has the benefit of having a well-defined structure, which is a plus from a log collection perspective, but being binary means parsing it is tricky and requires specialised tools.
So how can you get the juicy details out of that content? Easier said than done. Citrix will happily sell you an appliance; I leave it to the reader to imagine how many arms and legs the price might be. Ditto Splunk. Then there are the free/OSS options, which is where we arrive at Logstash.
Logstash can receive appflow (or ipfix/netflow) data, parse it, and output information summarising each appflow record. This is great and works (mostly). But when one starts looking at the output, a fly appears in the ointment: requests and responses are logged in separate records. This means that if you’re looking to replace your existing logs like for like, you could have a problem on your hands. Let’s take a look at some of the data. Here is the output in Logstash’s standard json format for a HTTP request:
{
"@version":"1",
"host":"10.0.50.4",
"netflow":{
"destinationIPv4Address":"10.0.50.5",
"netscalerHttpReqUserAgent":"Mozilla/5.0",
"destinationTransportPort":443,
"netscalerHttpReqUrl":"/some/random/path?with=parameters&other=stuff",
"sourceIPv4Address":"123.234.123.234",
"netscalerHttpReqMethod":"GET",
"netscalerHttpReqHost":"internalhost.unsafehex.com",
"netscalerHttpReqReferer":"",
"sourceTransportPort":1337,
"netscalerHttpDomainName":"netscalerdemo.unsafehex.com",
"netscalerTransactionId":2459675,
"netscalerHttpReqXForwardedFor":""
},
"@timestamp":"2017-11-08T14:59:58.000Z",
"type":"ipfix"
}
Plenty of useful information, but as you can see, nothing to indicate what response this request got. This is because the raw appflow packets from the Netscaler output the request and response as separate records, and Logstash is doing a literal translation of each binary record into a separate JSON object. Now let’s have a look at the corresponding response record:
{
"@version":"1",
"host":"10.0.50.4",
"netflow":{
"destinationIPv4Address":"123.234.123.234",
"destinationTransportPort":1337,
"sourceIPv4Address":"10.0.50.5",
"netscalerHttpRspStatus":200,
"sourceTransportPort":443,
"netscalerHttpRspLen":35721,
"netscalerTransactionId":2459675
},
"@timestamp":"2017-11-08T14:59:59.000Z",
"type":"ipfix"
}
Fortunately for us, although it does not provide us the request and response in a single record, it does mark them as belonging to the same transaction, via the netscalerTransactionId field. This means that we are now able to combine the information and produce the information you might be expecting in an HTTP log.
Having discovered this this, I was able to throw together a python script which will read the JSON output of Logstash and rebuild the separate records into a unified message. At the time the Apache Extended format was the most suited for my requirements and so the current version of my script (here) writes this out to hourly log files. Given that the data becomes a python dict, it would be very easy to adapt this to whatever other format you are interested in.
The code’s clearly a hack job so if anyone feels like turning it into good code, I’d welcome the education. In any case, happy parsing!
A few notes:
- ipfix, and hence appflow, are UDP protocols. There is no guarantee that Logstash will capture the corresponding response for every request, or vice versa. The script maintains a dict of currently-unmatched requests and responses for which there is a size limit to prevent it eating all of your memory. While Logstash is operating normally I have not seen any issues with unpaired request/response but it is theoretically possible.
- If the script cannot match a request with a response, it will stay in memory until the script is stopped with SIGINT or keyboard interrupt, or the table size limit is reached. At this point, unpaired requests will be written to the output with a response code of 418. Unpaired responses will be discarded.
- It won’t auto-create the output directory at this point. Sorry, forgot that part, I made them manually and didn’t think of it.