I’ve recently been planning to move over to using Varnish (http://varnish-cache.org/) as my primary reverse proxy on a couple of servers. To that end I’ve been having a bit of a play configuring it and trying to get it to do all the quirky little bits I have the current Apache doing.
One of those things is what I call IP whitelisting. So at present the Apache proxy takes a look at your IP address and if it’s on a list sets an additional request header to pass back to the actual servers saying “this IP is okay”. I use it as a light weight security mechanism for things, additional protection admin login pages, beta versions of sites, things like that. Anyhow in Apache we do it with a bit of clever mod_rewrite reading a map file (the IPs) and setting an environment variable.
How might I repeat this using Varnish? Do I need to? Well each back end Apache could probably do the same trick, but I seem to recall I had issues around the various execution orders of bits of Apache that caused me to shift the work onto the reverse proxy. Anyhow it’d be an excuse to try some more quirky stuff in Varnish.
Varnish does support Access Control lists and it would be easy to set a header based on those, but since the Varnish config is compiled and there doesn’t seem to be a way to read the ACLs dynamically from a file we’d be stuck with restarting Varnish every time the IPs changed (remember we’re really thinking about access from various places, by various people, so the IP list at any one time is pretty dynamic). So the obvious solution would be to use the built in ability to add your own C code into the Varnish configuration (see http://varnish-cache.org/wiki/ArchitectureInlineC). A simple example of this from the Varnish wiki is:
C{
#include <syslog.h>
}C
sub vcl_something {
C{
syslog(LOG_INFO, "Something happened at VCL line XX.");
}C
}
So we include the syslog header and then in some later subroutine make a C call (as contained by the C{ … }C braces) to pop some information into the syslog.
So my code was going to be simple (well if I was any good at C it would be simple to do!). I’d need to read a file and parse it for a given IP (_i.e._ the client IP), if it was present set a request header to given value. Pretty easy I guess. My existing whitelist files were already in a pretty simple format:
123.456.789.123 Fri May 21 18:29:33 +0100 2010
123.456.789.124 Fri May 21 19:29:33 +0100 2010
123.456.789.125 Fri May 21 20:29:33 +0100 2010
So it made sense to just start with that format. If at some later point I wanted better performance perhaps looking at making the whitelist a cdb
file would make sense, So I played with some simple C and ended up with this:
/*
* Varnish-powered IP whitelist lookup
*
* Eric Freeman, May 2010
* http://www.chillibear.org/2010/05/extending-varnish-with-c-code.html
*
* Whitelist file is assumed to be one line per
* IP address, where each line is the IP address
* (in dotted decimal) followed by some whitespace
* then any other junk up to the 200'th column. i.e.
*
* 10.164.10.10 some gibberish if you want
* 8.8.8.8 some more stuff, perhaps a timestamp?
* 1.4.23.190 obviously don't do the asterisks!
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define vcl_string char
#define WHITELIST_FILE "/path/to/whitelist.txt"
static inline int lookup_ip(vcl_string *ip) {
int ip_okay = 0;
int result = 1;
char address[16];
FILE *fp ;
fp = fopen ( WHITELIST_FILE, "rt" );
if ( fp == NULL )
{
fprintf( stderr, "\nError: Could not load whitelist file:\n%s\n\n", WHITELIST_FILE );
return 0;
}
char buffer[200];
while ( fgets( buffer, sizeof buffer, fp ) != NULL && result != 0) {
sscanf( buffer, "%15s", address );
//printf("addr : '%s'\n", address);
//printf("ip : '%s'\n", ip);
result = strcmp(address, ip);
//printf("Return Value is : %d\n\n", result);
if (result == 0) { ip_okay = 1; }
}
fclose(fp);
return ip_okay;
}
void vcl_whitelistip_set_header(const struct sess *sp) {
vcl_string *ip = VRT_IP_string(sp, VRT_r_client_ip(sp));
if (lookup_ip(ip)) {
VRT_SetHdr(sp, HDR_REQ, "\021X-WhiteListed-IP:", "1", vrt_magic_string_end);
} else {
VRT_SetHdr(sp, HDR_REQ, "\021X-WhiteListed-IP:", "0", vrt_magic_string_end);
}
}
For initial testing I replaced the dedicated Varnish function (_vcl_whitelistip_set_header_) with a simple main routine allowing me to call the lookupip_ function simply from the command line, that routine was:
int main(int argc, char **argv) {
if (argv[1]) {
if (lookup_ip(argv[1])) {
printf("IP okay\n");
} else {
printf("IP bad\n");
}
printf("done\n");
}
return 0;
}
I’ve also left the printf debug statements within the code, commented out, but they can easily be switched back on. The things I learnt from doing this was to scour the Varnish mailing list and bugs on their Trac instance for coding clues!
For example this bug ticket ticket/571 ensured I located my primary error which was causing Varnish to bomb out on each request. Just be careful with those header lengths, so in my case “\021X-WhiteListed-IP:” is 21 characters in total. Otherwise things were pretty straight forward. Bit of tracking down variable names and whatnot in Varnish, but if you run it with a -C switch it shows you the configuration VCL code “compiled” to C ready to be compiled, if you get my drift. So you can get most of the variables and whatnot from that. The two other existing Varnish extensions that proved invaluable to figuring things out were http://github.com/cosimo/varnish-geoip/ and http://github.com/cosimo/varnish-accept-language/.
Anyhow the application design is pretty simple, load the file from disk and read it line by line, popping the line into a 200 char buffer, to save stripping the IP address (minimum of 7 characters and a maximum of 15) of whitespace I used the sscanf function to extract the first 15 characters up to whitespace. I then do a simple comparison to the value passed in and set a return value. We continue to parse the file until we reach the end or find a match. There are probably more efficient ways of doing things, but hopefully this will work for me, but I’d guess the file will end up cached in RAM pretty quickly so hopefully access times shouldn’t be a massive issue. Time will tell I guess.
I popped the C code into a file within C{ … }C braces and then put an include command in my main Varnish configuration. I then popped the following code in sub vclrecv_ function to enable it:
remove req.http.X_WHITELISTED_IP;
C{
vcl_whitelistip_set_header(sp);
}C
All working nicely!
Written on 21 May 2010 and categorised in Apache and NIX, tagged as IP, Varnish, C, whitelist, and extend