Friday, May 24, 2013

Getting the requestor's IP address.

Getting the client's IP

While it seems it should be straightforward to get the IP address of the client making a web request in the ASP.Net pipeline, there are a few surprises lurking.  In my link-click tracking application, I ended up with this snippet based on Grant Burton's great post.
namespace Phydeaux.Helpers
{
    using System;
    using System.Net;
    using System.Net.Sockets;
    using System.Web;

    public static class ClientIP
    {
        public static string ClientIPFromRequest(this HttpRequestBase request, bool skipPrivate)
        {
            foreach (var item in s_HeaderItems)
            {
                var ipString = request.Headers[item.Key];

                if (String.IsNullOrEmpty(ipString))
                    continue;

                if (item.Split)
                {
                    foreach (var ip in ipString.Split(','))
                        if (ValidIP(ip, skipPrivate))
                            return ip;
                }
                else
                {
                    if (ValidIP(ipString, skipPrivate))
                        return ipString;
                }
            }

            return request.UserHostAddress;
        }

        private static bool ValidIP(string ip, bool skipPrivate)
        {
            IPAddress ipAddr;

            ip = ip == null ? String.Empty : ip.Trim();

            if (0 == ip.Length
                || false == IPAddress.TryParse(ip, out ipAddr)
                || (ipAddr.AddressFamily != AddressFamily.InterNetwork
                    && ipAddr.AddressFamily != AddressFamily.InterNetworkV6))
                return false;

            if (skipPrivate && ipAddr.AddressFamily == AddressFamily.InterNetwork)
            {
                var addr = IpRange.AddrToUInt64(ipAddr);
                foreach (var range in s_PrivateRanges)
                {
                    if (range.Encompasses(addr))
                        return false;
                }
            }

            return true;
        }

        /// 
        /// Provides a simple class that understands how to parse and
        /// compare IP addresses (IPV4 and IPV6) ranges.
        /// 
        private sealed class IpRange
        {
            private readonly UInt64 _start;
            private readonly UInt64 _end;

            public IpRange(string startStr, string endStr)
            {
                _start = ParseToUInt64(startStr);
                _end = ParseToUInt64(endStr);
            }

            public static UInt64 AddrToUInt64(IPAddress ip)
            {
                var ipBytes = ip.GetAddressBytes();
                UInt64 value = 0;

                foreach (var abyte in ipBytes)
                {
                    value <<= 8;    // shift
                    value += abyte;
                }

                return value;
            }

            public static UInt64 ParseToUInt64(string ipStr)
            {
                var ip = IPAddress.Parse(ipStr);
                return AddrToUInt64(ip);
            }

            public bool Encompasses(UInt64 addrValue)
            {
                return _start <= addrValue && addrValue <= _end;
            }

            public bool Encompasses(IPAddress addr)
            {
                var value = AddrToUInt64(addr);
                return Encompasses(value);
            }
        };

        private static readonly IpRange[] s_PrivateRanges =
            new IpRange[] { 
                    new IpRange("0.0.0.0","2.255.255.255"),
                    new IpRange("10.0.0.0","10.255.255.255"),
                    new IpRange("127.0.0.0","127.255.255.255"),
                    new IpRange("169.254.0.0","169.254.255.255"),
                    new IpRange("172.16.0.0","172.31.255.255"),
                    new IpRange("192.0.2.0","192.0.2.255"),
                    new IpRange("192.168.0.0","192.168.255.255"),
                    new IpRange("255.255.255.0","255.255.255.255")
            };


        /// 
        /// Describes a header item (key) and if it is expected to be 
        /// a comma-delimited string
        /// 
        private sealed class HeaderItem
        {
            public readonly string Key;
            public readonly bool Split;

            public HeaderItem(string key, bool split)
            {
                Key = key;
                Split = split;
            }
        }

        // order is in trust/use order top to bottom
        private static readonly HeaderItem[] s_HeaderItems =
            new HeaderItem[] { 
                    new HeaderItem("HTTP_CLIENT_IP",false),
                    new HeaderItem("HTTP_X_FORWARDED_FOR",true),
                    new HeaderItem("HTTP_X_FORWARDED",false),
                    new HeaderItem("HTTP_X_CLUSTER_CLIENT_IP",false),
                    new HeaderItem("HTTP_FORWARDED_FOR",false),
                    new HeaderItem("HTTP_FORWARDED",false),
                    new HeaderItem("HTTP_VIA",false),
                    new HeaderItem("REMOTE_ADDR",false)
            };
    }
}

6 comments:

Anonymous said...

Hi.
The ValidIp() method has an if statement that always returns false, which i think is weird.

if (skipPrivate && ipAddr.AddressFamily == AddressFamily.InterNetwork) ....

/J

Marc Brooks said...

skipPrivate is an argument to the public function and is passed-through.

Anonymous said...

Yeah, but should't
if (range.Encompasses(addr))
return false;



return true instead?

Marc Brooks said...

No, we're deciding if the IP we're looking at is valid. If it is encompassed by one of the "private IP" ranges, it is not valid, so we return immediately.

Anonymous said...

There seems to be a bug in the code posted.

I think:

if (skipPrivate && ipAddr.AddressFamily == AddressFamily.InterNetwork)

should be !skipPrivate

And then outside of the iteration of s_PrivateRanges there should not be a 'return false;'. That line should be removed. We only want to return false inside the iteration if the supplied IP address falls within the range.

Marc Brooks said...

No.

Please read the entire function and see what it is doing.

1. Action method gets called and wants the IP for a request

2. Calls ClientIPFromRequest and expects a valid IP. IF the client wants to skip private IPs, it will pass true

3. For each header item that might have an IP (in order of interest/usefulness) we call ValidIP with the skip flag specified.

4. ValidIP then decides if the IP is potentially valid; No if blank (so return false), No if not a valid IP address string representation (so return false), No if not a IPV4 or IPV6 address (so return false).

4. If a IPV4 andwe've been asked to skip private IPs, then check all the private IP ranges listed in s_PrivateRanges one at a time. If one of the ranges encompasses the IP address, return false

5. If not filtering out private addresses, or the address isn't IPV4, or the address isn't in one of the private ranges, then we return true

6. For the first header that yields a true from ValidIP we return that string.

7. Failing that, we simply return the request.UserHostAddress that the .Net framework set.