Monday, April 24, 2006

Handling X-Forwarded-For in Yaws

I run a Yaws webserver behind a Pound proxy. This works really well but all the IP addresses logged by Yaws appear as the localhost (127.0.0.1). This is due to Yaws picking up the IP address of the proxy as it forwards the request.

Pound sends the original IP address as a header in the HTTP request called X-Forwarded-For. This finally bugged me enough to modify Yaws to display the X-Forwarded-For IP address if the header was there, otherwise display the normal IP address.

The 'quick hack' change was quite simple. I modified the 'include/yaws_api.hrl' file to add 'x_fowarded_for' to the 'headers' record:
-record(headers, {
connection,
accept,
host,
if_modified_since,
if_match,
if_none_match,
if_range,
if_unmodified_since,
range,
referer,
user_agent,
accept_ranges,
cookie = [],
keep_alive,
location,
content_length,
content_type,
content_encoding,
authorization,
transfer_encoding,
x_forwarded_for,
other = [] %% misc other headers
}).

The idea being that this value will be set by Yaws when it first parses the HTTP request headers. This is done by modifying the http_collect_headers function in 'src/yaws.erl' to contain the following code:
{ok, {http_header, _Num, 'X-Forwarded-For',_, X}} ->
http_collect_headers(CliSock, Req, H#headers{x_forwarded_for = X},SSL)

This was basically a copy and paste of the way the other headers are handled. In the code that handles the logging I just check for the header and output the IP address if it is there, otherwise use the IP address from the request itself. This is done by a helper function I added to 'src/yaws_server.erl':
x_forwarded_for_or_ip(Ip, Item)->
case Item of
undefined -> Ip;
Item -> yaws:parse_ip(Item)
end.

The 'Item' passed to this function is the 'x_forwarded_for' element of the 'headers' record. If the 'X-Forwarded-For' header exists I parse the string into an IP address using yaws:parse_ip/1.

All that remains is to change the maybe_access_log/3 in 'src/yaws_server.erl' to call this helper function:
     TrueIp = x_forwarded_for_or_ip(Ip, H#headers.x_forwarded_for),
yaws_log:accesslog(SC#sconf.servername, TrueIp, User,
[Meth, $\s, Path, $\s, Ver],
Status, Len, Referrer, UserAgent);


Now all my IP's appear correctly forwarded from Pound in the Yaws server.

Categories: ,

7 Comments:

Anonymous Bruce Fitzsimons said...

Nice work, have you forwarded this to Klacke/mailing list for inclusion?

6:10 PM  
Blogger Chris Double said...

Not yet but it's a good idea. I'll do that.

6:28 PM  
Anonymous Russell Howe said...

Um... couldn't a client wanting to hide itself from the logs include an "X-Forwarded-For" header of its own creation, thereby causing yaws to log a falsified address?

Probably not an issue for your setup (I'd assume the only way to the server is via Pound, and that Pound will overwrite any X-Forwarded-For header), but for someone running plain old yaws, this strikes me as a potential security problem...

1:44 AM  
Blogger Chris Double said...

Hi Russell, yes you are right that a client could insert an X-Forwarded-For. You're also right that as I'm running behind Pound, Pound will overwrite the header and I only have localhost able to access the Yaws server directly.

It's probably not a good addition to have on by default.

11:06 PM  
Anonymous Anonymous said...

what is performance like in this setup?

8:36 AM  
Blogger Chris Double said...

I haven't done any performance stress testing but it runs my half dozen or so web sites without any problem.

10:00 PM  
Anonymous Anonymous said...

"couldn't a client wanting to hide itself from the logs include an "X-Forwarded-For" header of its own creation, thereby causing yaws to log a falsified address?"

No, because it's the proxy that adds the X-Forwarded-For header, not the client.

10:30 AM  

Post a Comment

<< Home