Simon Miller Team : Web Development Tags : Web Development Tips & Tricks MVC

User IP address checking and Akami masking

Simon Miller Team : Web Development Tags : Web Development Tips & Tricks MVC

I was recently faced with a task that required a website to validate a user IP for the purpose of opening up an extra section of the site if it fell within a certain range.

There are a few ways of doing this, but the common accepted approach is to first check the user’s X_FORWARDED_FOR header with a fall-back to the default retrieved User Host Address. This isn’t 100% guaranteed to work and should not be relied on for ultra-secure requirements, but is enough for most purposes (IP addresses after all, can be spoofed).

A more reliable approach is to ask an external service what it sees as the incoming IP address. For example, you could scrape the result from a site such as http://icanhazip.com/ . The downside here of course is the latency in looking up an external service, or even worse, that service going offline entirely.

My implementation worked fine in the development environment, however when it was pushed to a staging environment, the results were strange. After adding some logging I could see that on every page refresh the IP lookup function would return a seemingly random IP, from a large pool of recurring addresses. The only logical answer I could come up with was that a proxy of some sort was at play.

This assumption was basically correct. The staging environment had been set up to test Akami cloud content caching. The IP addresses being returned from my function were from their cloud, and not the end user. After a little research and confirmation from Akami themselves, it appeared that they were sending the true IP address in a custom header:  True-Client-IP

The solution thankfully is simple: add a tertiary check to your IP lookup function for this header. My final code looks something like this (with credit to this answer on StackOverflow for providing the base):

 

        public static string GetClientIpAddress(HttpRequestBase request)
        {
            try
            {
                // Regular IP lookup
                var userHostAddress = request.UserHostAddress;
                IPAddress.Parse(userHostAddress);

                // Akami IP lookup
                var akamiAddress = request.Headers["True-Client-IP"];
                if (!string.IsNullOrEmpty(akamiAddress))
                    return akamiAddress;

                // X_FORWARDED_FOR IP lookup
                var xForwardedFor = request.ServerVariables["X_FORWARDED_FOR"];
                if (string.IsNullOrEmpty(xForwardedFor))
                    return userHostAddress;

                // Get a list of public ip addresses in the X_FORWARDED_FOR variable
                var publicForwardingIps = xForwardedFor.Split(',').Where(ip => !IsPrivateIpAddress(ip)).ToList();

                // If we found any, return the last one, otherwise return the user host address
                return publicForwardingIps.Any() ? publicForwardingIps.Last() : userHostAddress;
            }
            catch (Exception)
            {
                // Always return all zeroes for any failure 
                return "0.0.0.0";
            }
        }

        private static bool IsPrivateIpAddress(string ipAddress)
        {
            // http://en.wikipedia.org/wiki/Private_network
            // Private IP Addresses are: 
            //  24-bit block: 10.0.0.0 through 10.255.255.255
            //  20-bit block: 172.16.0.0 through 172.31.255.255
            //  16-bit block: 192.168.0.0 through 192.168.255.255
            //  Link-local addresses: 169.254.0.0 through 169.254.255.255 (http://en.wikipedia.org/wiki/Link-local_address)

            var ip = IPAddress.Parse(ipAddress);
            var octets = ip.GetAddressBytes();

            var is24BitBlock = octets[0] == 10;
            if (is24BitBlock) return true; // Return to prevent further processing

            var is20BitBlock = octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31;
            if (is20BitBlock) return true; // Return to prevent further processing

            var is16BitBlock = octets[0] == 192 && octets[1] == 168;
            if (is16BitBlock) return true; // Return to prevent further processing

            var isLinkLocalAddress = octets[0] == 169 && octets[1] == 254;
            return isLinkLocalAddress;
        }