There are perhaps hundreds if not thousands of articles on obtaining your visitor’s IP address. The majority if these entries will refer to a small subset of global $_SERVER variables (HTTP_X_FORWARDED_FOR, HTTP_CLIENT_IP, and REMOTE_ADDR). Although both fast and simple solutions utilizing nested ternary operations exist, they are generally prone to a fairly large bug. The HTTP_X_FORWARDED_FOR server directive may contain a comma delimited list of IP addresses based upon several proxy hops prior to the client request packet reaching it’s destination.
After scouring the web I came across two sites demonstrating what appears to be the most accurate IP retrieval method I have come across. I found a number of inefficiencies in the two functions so I’m going to provide you with my optimized version.
/**
* Retrieves the best guess of the client's actual IP address.
* Takes into account numerous HTTP proxy headers due to variations
* in how different ISPs handle IP addresses in headers between hops.
*/
public function get_ip_address() {
// check for shared internet/ISP IP
if (!empty($_SERVER['HTTP_CLIENT_IP']) && $this->validate_ip($_SERVER['HTTP_CLIENT_IP']))
return $_SERVER['HTTP_CLIENT_IP'];
// check for IPs passing through proxies
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
// check if multiple ips exist in var
if (strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',') !== false) {
$iplist = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
foreach ($iplist as $ip) {
if ($this->validate_ip($ip))
return $ip;
}
} else {
if ($this->validate_ip($_SERVER['HTTP_X_FORWARDED_FOR']))
return $_SERVER['HTTP_X_FORWARDED_FOR'];
}
}
if (!empty($_SERVER['HTTP_X_FORWARDED']) && $this->validate_ip($_SERVER['HTTP_X_FORWARDED']))
return $_SERVER['HTTP_X_FORWARDED'];
if (!empty($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']) && $this->validate_ip($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']))
return $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
if (!empty($_SERVER['HTTP_FORWARDED_FOR']) && $this->validate_ip($_SERVER['HTTP_FORWARDED_FOR']))
return $_SERVER['HTTP_FORWARDED_FOR'];
if (!empty($_SERVER['HTTP_FORWARDED']) && $this->validate_ip($_SERVER['HTTP_FORWARDED']))
return $_SERVER['HTTP_FORWARDED'];
// return unreliable ip since all else failed
return $_SERVER['REMOTE_ADDR'];
}
/**
* Ensures an ip address is both a valid IP and does not fall within
* a private network range.
*/
public function validate_ip($ip) {
if (strtolower($ip) === 'unknown')
return false;
// generate ipv4 network address
$ip = ip2long($ip);
// if the ip is set and not equivalent to 255.255.255.255
if ($ip !== false && $ip !== -1) {
// make sure to get unsigned long representation of ip
// due to discrepancies between 32 and 64 bit OSes and
// signed numbers (ints default to signed in PHP)
$ip = sprintf('%u', $ip);
// do private network range checking
if ($ip >= 0 && $ip <= 50331647) return false;
if ($ip >= 167772160 && $ip <= 184549375) return false;
if ($ip >= 2130706432 && $ip <= 2147483647) return false;
if ($ip >= 2851995648 && $ip <= 2852061183) return false;
if ($ip >= 2886729728 && $ip <= 2887778303) return false;
if ($ip >= 3221225984 && $ip <= 3221226239) return false;
if ($ip >= 3232235520 && $ip <= 3232301055) return false;
if ($ip >= 4294967040) return false;
}
return true;
}
I can’t be sure which of the two sites to credit, either Grant Burton or webssense.com admin for his post here.
Modifications
- I added isset and !empty checks within get_ip_address() to avoid a wasted call to validate_ip() if no IP needed checking
- In the case of the $_SERVER['HTTP_X_FORWARDED_FOR'] variable I added a call to strpos to determine if the variable contained a comma separated list of IPs prior to using explode and iterating over a possibly non-existent array.
- In validate_ip(), I did a check against the value “unknown” as it has been noted this header value is set on occasion. The check is done using strtolower to ensure that the cases match.
- I converted the array of private network IP address ranges to their signed integer counterparts using ip2long to speed things up as these values are merely used as a lookup.
- I removed the array and foreach loop and replaced it with a series of if() blocks to test whether the ip falls within invalid ranges. Since the foreach loop no longer existed, I only need one conversion from the IP address to a signed integer.
- As noted in the php.net documentation for ip2long, I convert the IP address to an unsigned integer using sprintf. This alleviates the issue of the ip being converted to a negative signed integer on 32 bit machines.
- Since the ip is now an unsigned integer, the last if() statement can be simplified to only check that the IP is greater than the lower bound since the range carries all the way to 255.255.255.255.
One thing to note is the location I placed the unsigned integer conversion in validate_ip(). It appears after the check to determine if the IP was valid because ip2long returns a -1 in the event that the ip matches 255.255.255.255. If we have converted to an unsigned integer prior to the if statement, the IP would be considered valid. Doing the unsigned int conversion also allows us to simplify the last range of IPs because we know anything equal to 255.255.255.255 did not enter the loop. Other simplifications included a quick check for !empty on $_SERVER variables prior to hitting validate_ip() as we dont need the overhead from making that call if we already know the variable to be invalid.
Thank you. this was really helpful for the application i’m currently developing :)
[...] have already posted an article on retrieving a client’s ip so I will not go over that here. Instead, I will simply demonstrate an easy method for dropping the [...]